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