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

news2024/10/6 12:21:54

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

  • 1. 页面效果
    • 1.1 正常展示
    • 1.2 鼠标悬浮
    • 1.3 表单
  • 2. 代码部分
    • 1.2 html、ts
    • 1.2 less部分
  • 3. 编码过程遇到的问题

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 实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1276907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

gorm修改操作中两个update方法的小细节

在使用gorm进行修改操作时&#xff0c;修改操作中如下两个方法&#xff1a; Update() Updates() 都可以实现修改&#xff0c;根据名称可以看出Update是针对单个字段&#xff0c;而后者应该是多个。 下面是主要实际操作&#xff1a; ​​ Updates() 即&#xff0c;前者确实是…

vector是如何扩容的

vector容器扩容 vector是成倍扩容的&#xff0c;一般是2倍。 vector管理内存的成员函数 开始填值 没有填值之前&#xff0c;vector元素个数和容量大小都为0 加入一个值之后&#xff1a; 加入两个值&#xff1a;重点在加入三个值&#xff0c;此时容量变为4&#xff1a;加入第…

开源图床Qchan本地部署远程访问,轻松打造个人专属轻量级图床

文章目录 前言1. Qchan网站搭建1.1 Qchan下载和安装1.2 Qchan网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar云端设置2.2 Cpolar本地设置 3. 公网访问测试总结 前言 图床作为云存储的一项重要应用场景&#xff0c;在大量开发人员的努力下&#xff0c;已经开发出大…

优彩云采集器最新版免费下载,优彩云采集器免费

随着网络时代的发展&#xff0c;SEO&#xff08;Search Engine Optimization&#xff0c;搜索引擎优化&#xff09;已经成为网站推广和营销的关键一环。在SEO的世界里&#xff0c;原创内容的重要性愈发凸显。想要做到每天更新大量原创文章&#xff0c;并不是一件轻松的事情。优…

RocketMQ主从同步原理

一. 主从同步概述 主从同步这个概念相信大家在平时的工作中&#xff0c;多少都会听到。其目的主要是用于做一备份类操作&#xff0c;以及一些读写分离场景。比如我们常用的关系型数据库mysql&#xff0c;就有主从同步功能在。 主从同步&#xff0c;就是将主服务器上的数据同步…

接口测试基础知识

一、接口测试简介 什么是接口测试&#xff1f; 接口测试是测试系统组件间接口的一种测试&#xff0c;主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。 测试的重点&#xff1a; 检查数据的交换&#xff0c;传递和控制管理过程&#xff1b;检查系统间的相互…

人工智能对我们的生活影响有多大?

一、标题解析 本文标题为“人工智能对我们的生活影响有多大&#xff1f;”&#xff0c;这是一个典型的知乎风格SEO文案标题&#xff0c;既能够吸引读者&#xff0c;又能够体现文章的核心内容。 二、内容创作 1. 引言&#xff1a;在开头&#xff0c;我们可以简要介绍人工智能…

PVE系列-LVM安装MacOS的各个版本

PVE系列-LVM安装MacOS的各个版本 环境配置大概过程&#xff1a;详细步骤&#xff1a;1.建立安装环境和下载安装工具2. 重启后&#xff0c;执行osx-setup配置虚拟机3. 安装到硬盘&#xff0c;4.设定引导盘&#xff0c;以方便自动开机启动5.打开屏幕共享和系统VNC最后的结果 引子…

【Node.js】基础梳理 6 - MongoDB

写在最前&#xff1a;跟着视频学习只是为了在新手期快速入门。想要学习全面、进阶的知识&#xff0c;需要格外注重实战和官方技术文档&#xff0c;文档建议作为手册使用 系列文章 【Node.js】笔记整理 1 - 基础知识【Node.js】笔记整理 2 - 常用模块【Node.js】笔记整理 3 - n…

第三节:提供者、消费者、Eureka

一、 提供者 消费者&#xff08;就是个说法、定义&#xff0c;以防别人叭叭时听不懂&#xff09; 服务提供者&#xff1a;业务中被其他微服务调用的服务。&#xff08;提供接口给其他服务调用&#xff09;服务消费者&#xff1a;业务中调用其他微服务的服务。&#xff08;调用…

java基于springboot框架的中小企业人力资源管理系统的设计及实现+jsp

&#xff08;1&#xff09;员工信息管理&#xff1a;员工的基本信息&#xff0c;人员编制&#xff0c;岗位管理&#xff0c;人员流动管理&#xff08;老员工转出&#xff0c;辞职&#xff0c;退休等&#xff09;&#xff0c;职工业绩考核归公管理&#xff0c;工人工种管理。 &…

负电源电压转换-TP7660H

负电源电压转换-TP7660H 简介引脚说明典型应用电路倍压与反压的应用电路 简介 TP7660H 是一款 DC/DC 电荷泵电压反转器专用集成电路。芯片能将输入范围为 2.5V&#xff5e;11V 的电压转换成相应的-2.5V&#xff5e;-11V 的输出&#xff0c;电压转换精度可达99.9%&#xff0c;电…

【矩阵论】Chapter 2—内积空间知识点总结复习

文章目录 内积空间1 内积空间2 标准正交向量集3 Gram-Schmidt正交化方法4 正交子空间5 最小二乘问题6 正交矩阵和酉矩阵 内积空间 1 内积空间 内积空间定义 设 V V V是在数域 F F F上的向量空间&#xff0c;则 V V V到 F F F的一个代数运算记为 ( α , β ) (\alpha,\beta) (α…

VS安装QT VS Tools编译无法通过

场景&#xff1a; 项目拷贝到虚拟机内部后&#xff0c;配置好相关环境后无法编译&#xff0c;安装QT VS Tools后依旧无法编译&#xff0c;查找资料网上说的是QT工具版本不一致导致的&#xff0c;但反复试了几个版本后依旧无法编译通过。错误信息如下&#xff1a; C:\Users\Ad…

Gee教程5.中间件

鉴权认证、日志记录等这些保障和支持系统业务属于全系统的业务&#xff0c;和具体的系统业务没有关联&#xff0c;对于系统中的很多业务都适用。 因此&#xff0c;在业务开发过程中&#xff0c;为了更好的梳理系统架构&#xff0c;可以将上述描述所涉及的一些通用业务单独抽离…

1.2 Ubauntu 使用

一、完成VMware Tools安装 双击 VMwareTool 打开 Ubuntu 终端快捷键 AltControlT 切换汉语的快捷键是Alt空格 ls 打印出当前所在目录中所有文件和文件夹 cd 桌面 进入桌面文件夹 sudo ./vmware-install.pl 安装tool&#xff0c;输入之前设置的密码。 地址默认&#xff0c;按…

matlab simulink 永磁同步电机PI调速控制

1、内容简介 略 27-可以交流、咨询、答疑 2、内容说明 永磁同步电机调速控制 永磁同步电机PI调速控制 永磁同步电机PI调速控制、PMSM 3、仿真分析 略 4、参考论文 略 链接&#xff1a;https://pan.baidu.com/s/1AAJ_SlHseYpa5HAwMJlk1w 提取码&#xff1a;rvol 路…

程序猿无烦恼:让养生专家来写代码!!!

自己的经验&#xff0c;也是看旁边焦虑的开发总结的一些经验&#xff0c;讲道理不一定有用&#xff0c;但是道理本身一定是对的。 文章目录 持续学习少烦恼明确需求少问题少盯荧幕多冥想少吃奶茶多锻炼亲近自然要放空 持续学习少烦恼 C、JAVA、python、数据库…… 唯有持续学…

HarmonyOs 4 (三) ArkTS语言

目录 一 认识ArkTs语言1.1 ArkTs1.2 基本结构 二 基本语法2.1 声明式UI2.1.1 创建组件2.1.1.1 无参数2.1.1.2 有参数2.1.1.3 组件样式2.1.1.4 组件方法2.1.1.5 组件嵌套 2.1.2 自定义组件2.1.2.1 基本结构2.1.2.2 成员函数/变量2.1.2.3 自定义组件的参数规定2.1.2.4 Build函数2…

Android RatingBar实现五星好评

属性 isIndicatorRatingBar 是否为指示器&#xff0c;为true时&#xff0c;用户将无法交互操作&#xff0c;默认为false。 numStars 显示的星型数量&#xff0c;必须是一个整形值&#xff0c;像“50”&#xff0c;虽然可以设置很大&#xff0c;但一般…