• 前言

    最近用vue写过一个用于提交反馈的小组件的前端,可以嵌入到页面中,让用户可以直接对产品问题提交工单。同时还带有实时搜索功能,让用户在输入问题时,智能检索是否有相似问题。

效果

image
image

代码

Features:

  • 使用了vuetify
  • 对实时搜索的请求进行深度优化,在几乎不影响用户体验的情况下,有效降低了对后台接口的请求数量。核心代码如下:
watch: {
	getContent () { //使用了锁来控制实时搜索的频率
	if (this.form.Content.length >= 10 && this.form.Content !== this.nowStr && !this.lock) {
        this.lock = true
        this.sleep(200).then(() => {
          this.nowStr = this.form.Content
          this.getList()
        })
      }
    }
}
  • 食用方法:
    • 创建FeedBack.vue组件,代码在最后。
    • 在父组件中引用,注册,传入相应props即可。
    • PS:父组件中实现悬浮在页面中的效果即可
// FeedBack.vue
<template>
  <div v-if="showSelf" class="dialog" :style="{'z-index': zIndex}">
    <div class="dialog-mark" :style="{'z-index': zIndex + 1}" />
    <transition name="dialog">
      <div class="dialog-sprite" :style="{'z-index': zIndex + 2}">
        <!-- 标题 -->
        <section v-if="title" class="header">
          {{ title }}
        </section>
        <div class="dialog-body">
          <v-row>
            <v-col :cols="12">
              <v-card>
                <v-card-title>{{ `xxx` }}</v-card-title>
                <v-card-content>
                  <v-col>
                    <v-form v-model="valid">
                      <v-container>
                        <v-row>
                          <v-col
                            cols="12"
                            md="4"
                          >
                            <v-select v-model="form.Category" :items="sortNames" sm="3" md="3" label="请选择反馈类别" />
                          </v-col>
                          <v-col
                            cols="12"
                            md="4"
                          >
                            <v-text-field v-model="form.Name" label="请填写您的姓名" :rules="nameRules" required />
                          </v-col>
                          <v-col
                            cols="12"
                            md="4"
                          >
                            <v-text-field
                              v-model="form.Email"
                              :rules="emailRules"
                              label="请留下您的邮箱"
                              required
                            />
                          </v-col>
                        </v-row>
                        <v-row>
                          <v-col :cols="6">
                            <v-textarea v-model="form.Content" outlined label="请输入要反馈的问题(至少20字)" />
                          </v-col>
                          <v-col :cols="6">
                            <h3>
                              猜你想找:
                            </h3>
                            <p v-if="fbList.length === 0">
                              请输入你想反馈的问题
                            </p>
                            <v-chip v-for="list in fbList" :key="list.FeedbackID" class="ma-2" color="primary">
                              {{ `${list.FeedbackContent.length >= 30 ? list.FeedbackContent.substr(0,30) + '...' : list.FeedbackContent}` }}
                              <!-- {{ list.FeedbackContent.substr(0,20) }} -->
                            </v-chip>
                          </v-col>
                        </v-row>
                      </v-container>
                    </v-form>
                    <v-row>
                      <v-spacer />
                      <v-btn medium color="primary" @click="sumbit()">
                        提交
                      </v-btn>
                      <v-spacer />
                    </v-row>
                    <!-- {{ this.nowStr }}
                    {{ this.form.Content }} -->
                  </v-col>
                </v-card-content>
              </v-card>
            </v-col>
          </v-row>
        </div>
        <section class="dialog-footer">
          <div v-if="showCancel" class="btn btn-refuse" @click="cancel">
            {{ cancelText }}
          </div>
        </section>
      </div>
    </transition>
  </div>
</template>

<script>
export default {

  props: {
    show: {
      type: Boolean,
      default: false,
      required: true
    },
    showCancel: {
      typs: Boolean,
      default: false,
      required: false
    },
    title: {
      type: String,
      required: true
    },

    cancelText: {
      type: String,
      default: '取消',
      required: false
    },
    confirmText: {
      type: String,
      default: '确定',
      required: false
    }
  },
  data () {
    return {
      nameRules: [
        v => !!v || 'Name is required',
        v => v.length <= 10 || 'Name must be less than 10 characters'
      ],
      emailRules: [
        v => !!v || 'E-mail is required',
        v => /.+@.+/.test(v) || 'E-mail must be valid'
      ],
      name: 'dialog',
      valid: false,
      showSelf: false,
      zIndex: this.getZIndex(),
      bodyOverflow: '',
      detail: {},
      sort: [],
      sortNames: ['无(默认类别)'],
      form: {
        Category: '',
        Crop: '',
        Name: '',
        Email: '',
        Content: ''
      },
      fbList: [],
      nowStr: '',
      lock: false
    }
  },
  computed: {
    getContent () {
      return this.form.Content
    }
  },
  watch: {
    show (val) {
      if (!val) {
        this.closeMyself()
      } else {
        this.showSelf = val
      }
    },
	//添加了锁来控制实时搜索的请求频率
    getContent () {
      if (this.form.Content.length >= 10 && this.form.Content !== this.nowStr && !this.lock) {
        this.lock = true
        this.sleep(200).then(() => {
          this.nowStr = this.form.Content
          this.getList()
        })
      }
    }
  },
  async created () {
    await this.getName()
    await this.getSort()
    this.showSelf = this.show
  },
  mounted () {
    this.forbidScroll()
  },
  methods: {
    async getName () {
      const { data: name } = await this.$axios.get(
        '获取反馈title的接口', {}
      )
      this.detail = name.data.detail
    },
    async getSort () {
      const { data: sort } = await this.$axios.get(
        '获取反馈的分类接口', {}
      )
      this.sort = sort.data.detail
      for (const i of this.sort) {
        this.sortNames.push(String(i.Name))
      }
    },
    async sumbit () {
      this.form.Crop = this.detail.UID
      if (this.form.Category === '无(默认类别)') {
        this.form.Category = ''
      } else {
        for (const i of this.sort) {
          if (this.form.Category === i.Name) {
            this.form.Category = i.UID
          }
        }
      } //验证表单
      if (!this.valid) {
        alert('您填写的表单有误,请检查后再提交')
      } else if (this.form.Content.length < 20 || this.form.Content.length > 500) {
        alert('您的反馈内容字数不合规,请检查后再提交')
      } else {
        // console.log(this.form)
        await this.$axios.post(
          '发送反馈接口', this.form
        ).then((res) => {
          if (res.status === 200) {
            this.form = {
              Category: '',
              Crop: '',
              Name: '',
              Email: '',
              Content: ''
            }
            alert('提交成功!我们通过邮件回复您的反馈')
            this.cancel()
          } else {
            alert('提交失败,请联系管理员')
          }
        })
      }
    },
    sleep (time) {
      return new Promise(resolve => setTimeout(resolve, time))
    },
    async getList () {
      let url = ''
      await this.sleep(300).then(() => {
        this.lock = false
      })
      url = `` //实时搜索请求的url
      const { data: list } = await this.$axios.get(
        url, {}
      )
      //   console.log(list)
      this.fbList = list.data.detail.ResultSet
      // this.fbList.push()
    },
    forbidScroll () {
      this.bodyOverflow = document.body.style.overflow
      document.body.style.overflow = 'hidden'
    },
    getZIndex () {
      let zIndexInit = 20190315
      return zIndexInit++
    },
    cancel () {
      this.$emit('cancel', false)
    },

    confirm () {
      this.$emit('confirm', false)
    },
    closeMyself () {
      this.showSelf = false
      this.sloveBodyOverflow()
    },
    sloveBodyOverflow () {
      document.body.style.overflow = this.bodyOverflow
    }
  }
}
</script>

<style scoped lang="scss">

    .dialog-enter-active,
    .dialog-leave-active {
        transition: opacity .5s;
    }

    .dialog-enter,
    .dialog-leave-to {
        opacity: 0;
    }

    .dialog {
        position: fixed;
        top: 0;
        right: 0;
        width: 100%;
        height: 100%;

        .dialog-mark {
            position: absolute;
            top: 0;
            height: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, .6);
        }
    }

    .dialog-sprite {

        position: absolute;
        top: 10%;
        left: 15%;
        right: 15%;
        bottom: 25%;
        display: flex;
        flex-direction: column;
        max-height: 75%;
        min-height: 180px;
        overflow: hidden;
        z-index: 23456765435;
        background: #fff;
        border-radius: 8px;
        .header {
            padding: 15px;
            text-align: center;
            font-size: 18px;
            font-weight: 700;
            color: #333;
        }
        .dialog-body {
            flex: 1;
            overflow-x: hidden;
            overflow-y: scroll;
            padding: 0 15px 20px 15px;
        }
        .dialog-footer {
            position: relative;
            display: flex;
            width: 100%;

            &::after {
                content: '';
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 1px;
                background: #ddd;
                transform: scaleY(.5);
            }
            .btn {
                flex: 1;
                text-align: center;
                padding: 15px;
                font-size: 17px;
                &:nth-child(2) {
                    position: relative;
                    &::after {
                        content: '';
                        position: absolute;
                        left: 0;
                        top: 0;
                        width: 1px;
                        height: 100%;
                        background: #ddd;
                        transform: scaleX(.5);
                    }
                }
            }
            .btn-confirm {
                color: #43ac43;
            }
        }
    }
</style>

// 父组件
	<the-dialog
        :show="showDialog"
        :title="'提交反馈'"
        :show-cancel="true"
        :confirm-text="`提交`"
        :cancel-text="`关闭`"
        @confirm="confirm"
        @cancel="cancel"
      />
    <v-btn
      small
      color="primary"
      style="
      position: fixed;
      right: 0;
      top: 85%;"
      @click="() => showDialog = true"
    >
      提交反馈
    </v-btn>

What is broken can be reforged.