用户反馈组件实现(Vue3+ElementPlus)含图片拖拽上传

1. 页面效果

1.1 正常展示

在这里插入图片描述

1.2 鼠标悬浮

在这里插入图片描述

1.3 表单

在这里插入图片描述

2. 代码部分

1.2 html、ts

<template>
  <Teleport>
    <div
      class="feedback"
      @mouseenter="() => (showText = true)"
      @mouseleave="() => (showText = false)"
    >
      <el-popover :visible="visible" trigger="manual" placement="left" :width="510">
        <div class="feedback-content" @dragover="handleDragOver" @drop="handleDrop">
          <header class="flex">
            <strong>反馈中心</strong>
            <el-link type="primary" @click="toJiraPage"> <strong>我的反馈</strong> </el-link>
          </header>
          <hr style="margin: 10px 0 0 -13px; border-top: 1px solid #dbdbdb" />
          <section>
            <p style="margin-top: 10px; letter-spacing: 1px"><strong>尊敬的用户:</strong></p>
            <p style="letter-spacing: 1px; text-indent: 4ch"
              >感谢您提供诚挚的建议,我们将尽快帮您处理解决。</p
            >
            <el-form
              ref="refForm"
              :model="fromData"
              :rules="fromRules"
              label-position="top"
              size="large"
              style="margin-top: 20px"
              class="from-content"
            >
              <el-form-item
                label="问题类型"
                prop="issueType"
                :rules="{ required: true, message: '请选择问题类型', trigger: ['blur', 'change'] }"
              >
                <div class="card-list">
                  <div
                    v-for="t in feedbackType"
                    :key="t.name"
                    :class="['card-item', { active: fromData.issueType === t.id }]"
                    @click="fromData.issueType = t.id"
                    >{
   {
    t.name }}
                  </div>
                </div>
              </el-form-item>
              <el-form-item label="概述" prop="summary">
                <el-input v-model="fromData.summary"></el-input>
              </el-form-item>
              <el-form-item label="问题描述" prop="description">
                <el-input v-model="fromData.description" type="textarea" :rows="4"></el-input>
              </el-form-item>

              <el-upload
                action="none"
                list-type="picture-card"
                :auto-upload="false"
                :before-upload="beforeAvatarUpload"
                :on-exceed="handleExceed"
                :file-list="fromData.imgs"
                :on-preview="handlePictureCardPreview"
              >
                <el-icon><Plus /></el-icon>
                <template #file="{ file }">
                  <div>
                    <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
                    <span class="el-upload-list__item-actions">
                      <span
                        class="el-upload-list__item-preview"
                        @click="handlePictureCardPreview(file)"
                      >
                        <el-icon><zoom-in /></el-icon>
                      </span>
                      <span
                        v-if="!disabled"
                        class="el-upload-list__item-delete"
                        @click="handleRemove(file)"
                      >
                        <el-icon><Delete /></el-icon>
                      </span>
                    </span>
                  </div>
                </template>
              </el-upload>

              <div class="btn-row">
                <el-button class="btn-row-left" type="default" size="small" round @click="close"
                  >取 消
                </el-button>
                <el-button
                  class="btn-row-right"
                  size="small"
                  type="primary"
                  round
                  :disabled="loading"
                  @click="handleSubmit(refForm)"
                  >提 交
                </el-button>
              </div>
            </el-form>
          </section>
          <div class="dot"></div>
        </div>
        <template #reference>
          <div v-if="visible" class="line"></div>
          <div v-else class="slot-content" @click="visible = true">
            <ChatLineSquare class="feedback-icon" />
            <div v-if="showText" class="feedback-text">意见反馈 </div>
          </div>
        </template>
      </el-popover>
    </div>

    <el-dialog v-model="dialogVisible">
      <img w-full :src="dialogImageUrl" alt="Preview Image" />
    </el-dialog>
  </Teleport>
</template>

<script setup lang="ts">
  import {
    ElMessage } from 'element-plus';
  import type {
    UploadFile, ElForm, UploadProps } from 'element-plus';
  import {
    ChatLineSquare, Delete, Plus, ZoomIn } from '@element-plus/icons-vue';
  import {
    submitFeedback } from '@/api/config-center';

  +(() => {
   
    // 初始化数据准备。。。
  })();

  const handleDragOver = (event) => {
   
    event.preventDefault();
  };
  const allowedFormats = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/bmp',
    'image/tiff',
    'image/x-icon',
    'image/svg+xml',
  ] as const;
  const handleDrop = (event) => {
   
    event.preventDefault();

    const file = event.dataTransfer.files[0];

    if (!allowedFormats.includes(file.type)) {
   
      ElMessage.warning('只能上传 JPEG、PNG、GIF、BMP、TIFF、ICO 或 SVG 格式的图片');
      return;
    }
    if (fromData.value.imgs?.length >= 5) {
   
      ElMessage.warning('抱歉,最多只能上传5张图片!');
      return;
    }
    if (file.size > 2 * 1024 * 1024) {
   
      ElMessage.warning('图片大小不能超过 2MB');
      return;
    }

    const reader = new FileReader();
    reader.onload = () => {
   
      const image = {
   
        name: file.name,
        url: reader.result, // 用于页面回显
        raw: file, // 将图片的原始文件对象存储到 raw 属性中
      };

      fromData.value.imgs.push(image);
    };
    reader.readAsDataURL(file);
  };
  const showText = ref(false);

  const visible = ref(false);
  type FormInstance = InstanceType<typeof ElForm>;
  const refForm = ref<FormInstance>();

  const feedbackType = ref([]);

  const toJiraPage = () => {
   };

  const loading = ref(false);
  const handleSubmit = (formEl: FormInstance | undefined): void => {
   
    if (!formEl) return;
    formEl.validate((valid: any) => {
   
      if (valid) {
   
        loading.value = true;
        let fd = new FormData();
        fd.append('issueType', fromData.value.issueType);
        fd.append('summary', fromData.value.summary);
        fd.append('description', fromData.value.description);

        fromData.value.imgs.forEach((v) => fd.append('files', v.raw));

        submitFeedback(fd)
          .then((res: any) => {
   
            if (res.code === 200) {
   
              ElMessage.success('反馈成功,感谢您的关注!');
              visible.value = false;
              fromData.value = {
   
                issueType: 0,
                summary: '',
                description: '',
                imgs: [],
              };
            } else {
   
              ElMessage.error('反馈失败:' + res.message);
            }
          })
          .catch((e) => ElMessage.error('反馈失败:' + e))
          .finally(() => (loading.value = false));
      } else {
   
        return false;
      }
    });
  };
  const fromData = ref({
   
    issueType: '', // 问题类型
    summary: '', // 概要
    description: '', // 描述
    imgs: [], // 图片
  });

  const close = () => {
   
    visible.value = false;
    showText.value = false;
    refForm.value?.resetFields();
    fromData.value = {
   
      issueType: '',
      summary: '',
      description: '',
      imgs: [],
    };
  };

  const dialogImageUrl = ref('');
  const dialogVisible = ref(false);
  const disabled = ref(false);
  const handlePictureCardPreview = (file: UploadFile) => {
   
    dialogImageUrl.value = file.url!;
    dialogVisible.value = true;
  };
  const handleRemove = (file: UploadFile) => {
   
    const index = fromData.value.imgs.findIndex((f: any) => f.uid === file.uid);
    fromData.value.imgs.splice(index, 1);
  };
  const fromRules = reactive({
   
    // issueType: [{ required: true, message: '请选择问题类型', trigger: 'blur' }],
    summary: [{
    required: true, message: '请输入概要', trigger: 'blur' }],
    description: [{
    required: true, message: '请输入描述', trigger: 'blur' }],
  });
</script>

由于我这边项目的需求,反馈组件我是和菜单组件放在一起
在这里插入图片描述

1.2 less部分

<style lang="less" scoped>
  ::v-deep(.el-upload-list--picture-card .el-upload-list__item-actions span + span) {
   
    margin-left: 0.6rem !important;
  }

  ::v-deep(.el-upload.el-upload--picture-card),
  ::v-deep(li.el-upload-list__item) {
   
    width: 70px !important;
    height: 70px !important;
  }
  ::v-deep .el-upload-dragger {
   
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .feedback-content {
   
    height: 561px;
    position: relative;
    header {
   
      display: flex;
      justify-content: space-between;
      margin: 0 10px;
    }
    section {
   
      .from-content {
   
        height: 454px;
        // overflow-y: scroll;
      }
      .card-list {
   
        display: flex;
        gap: 20px;
        .card-item {
   
          padding: 0 20px;
          border-radius: 5px;
          background-color: #f2f3f5;
          border: 1px solid #dfdfdf;
          cursor: pointer;
          width: 100%;
          height: 35px;
          line-height: 35px;
          font-size: 12px;
          &.active {
   
            color: #fff;
            background-color: #4c7cee;
          }
        }
      }
      .upload {
   
        width: 60px;
        height: 60px;
        cursor: pointer;
        border: 1px dashed var(--el-border-color-darker);
        background-color: #fafafa;
        &:hover {
   
          border-color: var(--el-color-primary);
          color: var(--el-color-primary);
        }
      }
    }
    .dot {
   
      position: absolute;
      left: -12px;
      top: 0;
      width: 4px;
      height: 21px;
      border-radius: 5px;
      background-color: #4c7cee;
    }
  }
  .feedback {
   
    position: fixed;
    top: 50%;
    right: 0;
    color: #fff;
    cursor: pointer;
    border-radius: 6px;
    transform: translateY(-50%);
    background-color: #4c7cea;
    z-index: 999999999999;
    .line {
   
      width: 7px;
      height: 100px;
      border-radius: 6px;
      background-color: #4c7cea;
    }
    .feedback-text {
   
      letter-spacing: 0.3em;
      writing-mode: vertical-lr;
      text-orientation: upright;
    }
    @media only screen and (min-width: 1280px) {
   
      .slot-content {
   
        margin: 6px;
        .feedback-icon {
   
          width: 24px;
          height: 24px;
          margin-bottom: 5px;
        }
        .feedback-text {
   
          font-size: 16px;
        }
      }
    }
    @media only screen and (max-width: 1280px) {
   
      .slot-content {
   
        margin: 3px;
        .feedback-icon {
   
          width: 19px;
          height: 19px;
          margin-bottom: 3px;
        }
        .feedback-text {
   
          font-size: 13px;
        }
      }
    }
  }
  .btn-row {
   
    margin: 16px 8px 0;
    text-align: end;
    &-left {
   
      border-color: #4c7cee;
      color: #4c7cee;
    }
  }
</style>

3. 编码过程遇到的问题

  1. Teleport 是 Vue3 的一个内置组件,详细使用请查阅 Vue3官网
  2. 关于图片拖拽
    1. 最初的时候,是采用 el-uploaddrag 属性,来实现,但是后面有用户提出拖拽上传目标的框太小,建议可以把图片拖拽进整个表单,最开始时候的想法是在最外层的div加一个拖拽事件,但是实现起来有一个问题, el-upload 拖拽事件添加 .stop,会造成下方区域无法实现拖拽上传,其他区域OK,后采取的解决方式是, el-upload 去除拖拽属性,全部采用最外层的原生拖拽事件上传
      在这里插入图片描述
  3. 图片的上传
    图片需要和文字一起上传,最初的时候实在没有想到实现方式,后面查了好些文章,发现是通过 FormData 实现

相关推荐

  1. vue+element ui实现图片进行图片排序

    2023-12-05 20:42:04       38 阅读
  2. vue基于element封装图片

    2023-12-05 20:42:04       15 阅读
  3. vue3组件vuedraggable

    2023-12-05 20:42:04       44 阅读
  4. el-upload组件实现排序图片顺序

    2023-12-05 20:42:04       28 阅读
  5. vue3 实现一个自定义指令

    2023-12-05 20:42:04       17 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-05 20:42:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-05 20:42:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-05 20:42:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-05 20:42:04       20 阅读

热门阅读

  1. 25.Oracle的回收站

    2023-12-05 20:42:04       31 阅读
  2. 初识Dockerfile

    2023-12-05 20:42:04       37 阅读
  3. Linux如何查看系统时间

    2023-12-05 20:42:04       36 阅读
  4. MySql之锁表、锁行解决方案

    2023-12-05 20:42:04       42 阅读
  5. Doccker常用的命令

    2023-12-05 20:42:04       39 阅读
  6. k8s学习

    2023-12-05 20:42:04       33 阅读
  7. xxl-job分布式定时任务

    2023-12-05 20:42:04       43 阅读