refactor: ♻️ 重构图片上传,删除掉单图上传,一个图片上传支持多种情况

重构图片上传,删除掉单图上传,一个图片上传支持多种情况
This commit is contained in:
Theo
2024-12-25 11:40:14 +08:00
parent f4ba493fcd
commit a023b4b623
4 changed files with 400 additions and 478 deletions

View File

@@ -0,0 +1,380 @@
<!-- 图片上传组件 -->
<template>
<!-- 实际的上传组件隐藏 -->
<div style="display: none">
<el-upload
v-model:file-list="fileList"
:before-upload="handleBeforeUpload"
:action="props.action"
:headers="props.headers"
:data="props.data"
:name="props.name"
:on-success="handleSuccessFile"
:on-error="handleError"
:accept="props.accept"
:limit="props.limit"
/>
</div>
<!-- 自定义的显示区域 -->
<div class="custom-upload-list">
<!-- 已上传的图片列表 -->
<div v-for="(path, index) in valFileList" :key="index" class="custom-upload-item">
<img class="upload-thumbnail" :src="path" alt="" />
<div class="upload-actions">
<span class="action-item preview" @click="previewImg(path)">
<el-icon><zoom-in /></el-icon>
</span>
<span v-if="props.showDelBtn" class="action-item delete" @click="handleRemove(path)">
<el-icon><Delete /></el-icon>
</span>
</div>
</div>
<!-- 上传按钮 -->
<div
v-if="valFileList.length < props.limit && props.showUploadBtn"
class="custom-upload-trigger"
@click="triggerUpload"
>
<el-icon><Plus /></el-icon>
</div>
</div>
<!-- 图片预览组件 -->
<el-image-viewer
v-if="viewVisible"
:zoom-rate="1.2"
:initialIndex="initialIndex"
:url-list="viewFileList"
@close="closePreview"
/>
</template>
<script setup lang="ts">
import { UploadRawFile, UploadUserFile, UploadFile } from "element-plus";
import FileAPI from "@/api/file";
import { getToken } from "@/utils/auth";
import { ResultEnum } from "@/enums/ResultEnum";
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps({
/**
* 文件路径集合
*/
modelValue: {
type: [Array, String],
default: () => [],
},
/**
* 上传地址
*/
action: {
type: String,
default: FileAPI.uploadUrl,
},
/**
* 请求头
*/
headers: {
type: Object,
default: () => {
return {
Authorization: getToken(),
};
},
},
/**
* 请求携带的额外参数
*/
data: {
type: Object,
default: () => {
return {};
},
},
/**
* 上传文件的参数名
*/
name: {
type: String,
default: "file",
},
/**
* 文件上传数量限制
*/
limit: {
type: Number,
default: 10,
},
/**
* 是否显示删除按钮
*/
showDelBtn: {
type: Boolean,
default: true,
},
/**
* 是否显示上传按钮
*/
showUploadBtn: {
type: Boolean,
default: true,
},
/**
* 单张图片最大大小,单位MB
*/
maxSize: {
type: Number,
default: 10,
},
/**
* 上传文件类型
*/
accept: {
type: String,
default: "image/*",
},
/**
* 支持的文件类型,默认支持所有图片格式
* eg:['png','jpg','jpeg','gif']
*/
supportFileType: {
type: Array<string>,
default: () => [],
},
/**
* 自定义样式
*/
style: {
type: Object,
default: () => {
return {
width: "130px",
height: "130px",
};
},
},
});
const viewVisible = ref(false);
const initialIndex = ref(0);
const fileList = ref([] as UploadUserFile[]);
const valFileList = ref([] as string[]);
const viewFileList = ref([] as string[]);
watch(
() => props.modelValue,
(newVal) => {
if (typeof newVal === "string" && !newVal) {
fileList.value = [];
viewFileList.value = [];
valFileList.value = [];
return;
}
const modelValue = typeof newVal === "string" ? [newVal] : (newVal as string[]);
const filePaths = fileList.value.map((file) => file.url);
// 监听modelValue文件集合值未变化时跳过赋值
if (
filePaths.length > 0 &&
filePaths.length === modelValue.length &&
filePaths.every((x) => modelValue.some((y) => y === x)) &&
modelValue.every((y) => filePaths.some((x) => x === y))
) {
return;
}
if (modelValue.length <= 0) {
fileList.value = [];
viewFileList.value = [];
valFileList.value = [];
return;
}
fileList.value = modelValue.map((filePath) => {
return { url: filePath } as UploadUserFile;
});
valFileList.value = modelValue;
},
{ immediate: true }
);
/**
* 上传成功回调
*
* @param options
*/
const handleSuccessFile = (response: any, file: UploadFile) => {
if (response.code === ResultEnum.SUCCESS) {
ElMessage.success("上传成功");
valFileList.value.push(response.data.url);
if (props.limit === 1) {
emit("update:modelValue", response.data.url);
emit("change", response.data.url);
} else {
emit("update:modelValue", valFileList.value);
emit("change", valFileList.value);
}
return;
} else {
ElMessage.error(response.msg || "上传失败");
}
};
const handleError = (error: any) => {
ElMessage.error("上传失败");
};
/**
* 删除图片
*/
function handleRemove(path: string) {
if (path) {
FileAPI.deleteByPath(path).then(() => {
valFileList.value = valFileList.value.filter((x) => x !== path);
// 删除成功回调
if (props.limit === 1) {
emit("update:modelValue", "");
emit("change", "");
} else {
emit("update:modelValue", valFileList.value);
emit("change", valFileList.value);
}
});
}
}
/**
* 限制用户上传文件的格式和大小
*/
function handleBeforeUpload(file: UploadRawFile) {
// 限制文件大小
if (file.size > props.maxSize * 1024 * 1024) {
ElMessage.warning("上传图片不能大于" + props.maxSize + "M");
return false;
}
// 判断文件类型
// 获取文件后缀名
const fileExt = file.name.split(".").pop();
if (!fileExt) {
ElMessage.warning("上传图片格式错误,支持的文件类型:" + props.supportFileType.join(","));
return false;
}
// 给文件后缀名转换为小写
const lowerCaseFileExt = fileExt.toLowerCase();
// 判断文件后缀名是否在支持的文件类型中
if (props.supportFileType.length > 0) {
let isSupport = false;
props.supportFileType.forEach((type) => {
if (type.toLowerCase() === lowerCaseFileExt) {
isSupport = true;
}
});
if (!isSupport) {
ElMessage.warning("上传图片格式错误,支持的文件类型:" + props.supportFileType.join(","));
return false;
}
}
return true;
}
/**
* 预览图片
*/
const previewImg = (path: string) => {
viewFileList.value = fileList.value.map((file) => file.url!);
initialIndex.value = fileList.value.findIndex((file) => file.url === path);
viewVisible.value = true;
};
/**
* 关闭预览
*/
const closePreview = () => {
viewVisible.value = false;
};
// 新增一个触发上传的方法
const triggerUpload = () => {
const uploadEl = document.querySelector(".el-upload__input") as HTMLElement;
if (uploadEl) {
uploadEl.click();
}
};
</script>
<style lang="scss" scoped>
.custom-upload-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.custom-upload-item {
position: relative;
width: v-bind("props.style.width");
height: v-bind("props.style.height");
overflow: hidden;
border-radius: 6px;
.upload-thumbnail {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-actions {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
opacity: 0;
transition: opacity 0.3s;
&:hover {
opacity: 1;
}
.action-item {
padding: 8px;
color: #fff;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
}
}
.custom-upload-trigger {
display: flex;
align-items: center;
justify-content: center;
width: v-bind("props.style.width");
height: v-bind("props.style.height");
cursor: pointer;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
transition: var(--el-transition-duration);
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
.el-icon {
font-size: 20px;
color: #999;
}
&:hover .el-icon {
color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,243 +0,0 @@
<!-- 图片上传组件 -->
<template>
<el-upload
v-model:file-list="fileList"
list-type="picture-card"
:class="fileList.length >= props.limit || !props.showUploadBtn ? 'hide' : 'show'"
:before-upload="handleBeforeUpload"
:action="props.action"
:headers="props.headers"
:data="props.data"
:name="props.name"
:on-success="handleSuccessFile"
:on-error="handleError"
:accept="props.accept"
:limit="props.limit"
>
<el-icon><Plus /></el-icon>
<template #file="{ file }">
<div style="width: 100%">
<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="previewImg(file)">
<el-icon><zoom-in /></el-icon>
</span>
<span
v-if="props.showDelBtn"
class="el-upload-list__item-delete"
@click="handleRemove(file)"
>
<el-icon><Delete /></el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-image-viewer
v-if="viewVisible"
:zoom-rate="1.2"
:initialIndex="initialIndex"
:url-list="viewFileList"
@close="closePreview"
/>
</template>
<script setup lang="ts">
import { UploadRawFile, UploadUserFile, UploadFile, UploadProps } from "element-plus";
import FileAPI from "@/api/file";
import { getToken } from "@/utils/auth";
import { ResultEnum } from "@/enums/ResultEnum";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
/**
* 文件路径集合
*/
modelValue: {
type: Array<string>,
default: () => [],
},
/**
* 上传地址
*/
action: {
type: String,
default: FileAPI.uploadUrl,
},
/**
* 请求头
*/
headers: {
type: Object,
default: () => {
return {
Authorization: getToken(),
};
},
},
/**
* 请求携带的额外参数
*/
data: {
type: Object,
default: () => {
return {};
},
},
/**
* 上传文件的参数名
*/
name: {
type: String,
default: "file",
},
/**
* 文件上传数量限制
*/
limit: {
type: Number,
default: 10,
},
/**
* 是否显示删除按钮
*/
showDelBtn: {
type: Boolean,
default: true,
},
/**
* 是否显示上传按钮
*/
showUploadBtn: {
type: Boolean,
default: true,
},
/**
* 单张图片最大大小
*/
maxSize: {
type: Number,
default: 10,
},
/**
* 上传文件类型
*/
accept: {
type: String,
default: "image/*",
},
});
const viewVisible = ref(false);
const initialIndex = ref(0);
const fileList = ref([] as UploadUserFile[]);
const valFileList = ref([] as string[]);
const viewFileList = ref([] as string[]);
watch(
() => props.modelValue,
(newVal: string[]) => {
const filePaths = fileList.value.map((file) => file.url);
// 监听modelValue文件集合值未变化时跳过赋值
if (
filePaths.length > 0 &&
filePaths.length === newVal.length &&
filePaths.every((x) => newVal.some((y) => y === x)) &&
newVal.every((y) => filePaths.some((x) => x === y))
) {
return;
}
if (newVal.length <= 0) {
fileList.value = [];
viewFileList.value = [];
valFileList.value = [];
return;
}
fileList.value = newVal.map((filePath) => {
return { url: filePath } as UploadUserFile;
});
valFileList.value = newVal.map((filePath) => {
return filePath;
});
},
{ immediate: true }
);
/**
* 上传成功回调
*
* @param options
*/
const handleSuccessFile = (response: any, file: UploadFile) => {
if (response.code === ResultEnum.SUCCESS) {
ElMessage.success("上传成功");
valFileList.value.push(response.data.url);
emit("update:modelValue", valFileList.value);
return;
} else {
ElMessage.error(response.msg || "上传失败");
}
};
const handleError = (error: any) => {
ElMessage.error("上传失败");
};
/**
* 删除图片
*/
function handleRemove(removeFile: UploadFile) {
const filePath = removeFile.url;
if (filePath) {
FileAPI.deleteByPath(filePath).then(() => {
valFileList.value = valFileList.value.filter((x) => x !== filePath);
// 删除成功回调
emit("update:modelValue", valFileList.value);
});
}
}
/**
* 限制用户上传文件的格式和大小
*/
function handleBeforeUpload(file: UploadRawFile) {
if (file.size > props.maxSize * 1024 * 1024) {
ElMessage.warning("上传图片不能大于" + props.maxSize + "M");
return false;
}
return true;
}
/**
* 预览图片
*/
const previewImg: UploadProps["onPreview"] = (uploadFile: UploadFile) => {
viewFileList.value = fileList.value.map((file) => file.url!);
initialIndex.value = fileList.value.findIndex((file) => file.url === uploadFile.url);
viewVisible.value = true;
};
/**
* 关闭预览
*/
const closePreview = () => {
viewVisible.value = false;
};
</script>
<style lang="scss" scoped>
.hide {
:deep(.el-upload--picture-card) {
display: none;
}
}
.show {
:deep(.el-upload--picture-card) {
display: flex;
}
}
</style>

View File

@@ -1,159 +0,0 @@
<!-- 单图上传组件 -->
<template>
<el-upload
v-model="imgUrl"
class="img-upload"
:show-file-list="false"
list-type="picture-card"
:before-upload="handleBeforeUpload"
:http-request="uploadFile"
:style="{ width: props.size, height: props.size }"
:accept="props.accept"
>
<template #default>
<el-image v-if="imgUrl" :src="imgUrl" />
<div v-if="imgUrl" class="img-upload__overlay">
<el-icon class="img-upload__preview-icon" @click.stop="handlePreview">
<ZoomIn />
</el-icon>
<el-icon class="img-upload__delete-icon" @click.stop="handleDelete">
<Delete />
</el-icon>
</div>
<el-icon v-else class="img-upload__add-icon">
<Plus />
</el-icon>
</template>
</el-upload>
</template>
<script setup lang="ts">
import { ElImageViewer, UploadRawFile, UploadRequestOptions } from "element-plus";
import FileAPI from "@/api/file";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
maxSize: {
type: Number,
default: 10, // 默认限制为 10MB
},
accept: {
type: String,
default: "",
},
size: {
type: String,
default: "150px",
},
});
const emit = defineEmits(["update:modelValue"]);
const imgUrl = defineModel("modelValue", {
type: String,
required: true,
default: "",
});
/**
* 自定义图片上传
*
* @param options
*/
async function uploadFile(options: UploadRequestOptions): Promise<any> {
const data = await FileAPI.upload(options.file);
imgUrl.value = data.url;
}
/**
* 限制用户上传文件的格式和大小
*/
function handleBeforeUpload(file: UploadRawFile) {
if (file.size > props.maxSize * 1024 * 1024) {
ElMessage.warning(`上传图片不能大于${props.maxSize}MB`);
return false;
}
return true;
}
/**
* 预览图片
*/
function handlePreview() {
if (imgUrl.value) {
const imageViewerApp = createApp({
setup() {
return () =>
h(ElImageViewer, {
urlList: [imgUrl.value],
initialIndex: 0,
onClose: () => {
imageViewerApp.unmount();
document.body.removeChild(container);
},
});
},
});
const container = document.createElement("div");
document.body.appendChild(container);
imageViewerApp.mount(container);
}
}
/**
* 删除图片
*/
function handleDelete() {
imgUrl.value = "";
}
</script>
<style scoped lang="scss">
:deep(.el-upload--picture-card) {
/* width: var(--el-upload-picture-card-size);
height: var(--el-upload-picture-card-size); */
width: 100%;
height: 100%;
}
.img-upload {
position: relative;
overflow: hidden;
cursor: pointer;
border: 1px var(--el-border-color) solid;
border-radius: 5px;
&:hover {
border-color: var(--el-color-primary);
}
&__delete-icon {
margin-left: 5px;
}
&__overlay {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #fff;
background-color: var(--el-overlay-color-lighter);
border-radius: 6px;
opacity: 0;
transition: opacity var(--el-transition-duration);
&:hover {
opacity: 1;
}
}
}
</style>

View File

@@ -11,20 +11,11 @@
</el-link>
<el-form>
<el-form-item label="单图上传">
<SingleImageUpload v-model="picUrl" />
<el-form-item label="绑定值">
{{ picUrl }}
</el-form-item>
<el-form-item label="参数说明">
<el-table :data="singleImageUploadArgData" border>
<el-table-column prop="argsName" label="参数名称" width="300" />
<el-table-column prop="type" label="参数类型" width="200" />
<el-table-column prop="default" label="默认值" width="400" />
<el-table-column prop="desc" label="描述" width="300" />
</el-table>
</el-form-item>
<el-form-item label="多图上传">
<MultiImageUpload v-model="picUrls" />
<el-form-item label="图片上传">
<ImageUpload v-model="picUrl" :limit="1" :maxSize="10" />
</el-form-item>
<el-form-item label="参数说明">
<el-table :data="imageUploadArgData" border>
@@ -54,71 +45,12 @@
// 单图
const picUrl = ref("https://s2.loli.net/2023/05/24/yNsxFC8rLHMZQcK.jpg");
const singleImageUploadArgData = [
{
argsName: "v-model",
type: "String",
default: "",
desc: "已经上传的图片URL",
},
{
argsName: "size",
type: "String",
default: "150px",
desc: "图片上传组件的尺寸大小",
},
{
argsName: "max-size",
type: "Number",
default: "10",
desc: "单个图片上传大小限制(单位M)",
},
{
argsName: "accept",
type: "String",
default: "",
desc: "上传文件类型",
},
];
const imageCprpperUploadArgData = [
{
argsName: "v-model",
type: "Object",
default: "",
desc: "裁剪后图片Base64编码",
},
{
argsName: "presetMode",
type: "String",
default: "{ width: 295, height: 413 }",
desc: "裁剪窗口的长宽,及裁剪图片大小",
},
{
argsName: "title",
type: "String",
default: "上传图片",
desc: "组件内容",
},
];
const cprpperValue = ref();
// 多图
const picUrls = ref([
"https://s2.loli.net/2023/05/24/yNsxFC8rLHMZQcK.jpg",
"https://s2.loli.net/2023/05/24/RuHFMwW4rG5lIqs.jpg",
"https://s2.loli.net/2023/05/24/ZPiGbcpR91WqInB.jpg",
"https://s2.loli.net/2023/05/24/e1bcnEq3MFdmlNL.jpg",
"https://s2.loli.net/2023/05/24/wZTSPj1yDQNcuhU.jpg",
]);
const imageUploadArgData = [
{
argsName: "v-model",
type: "Array",
default: "[]",
desc: "已经上传的图片数组",
type: "[Array,String]",
default: "[] | ''",
desc: "已经上传的图片数组,单张图片时为String",
},
{
argsName: "action",
@@ -148,7 +80,7 @@ const imageUploadArgData = [
argsName: "limit",
type: "Number",
default: 10,
desc: "上传最大的图片数量",
desc: "上传最大的图片数量,单张图片时填写1",
},
{
argsName: "show-del-btn",
@@ -174,6 +106,18 @@ const imageUploadArgData = [
default: "image/*",
desc: "上传文件类型",
},
{
argsName: "supportFileType",
type: "Array",
default: "[]",
desc: "支持的文件类型,默认支持所有图片格式,eg:['png','jpg','jpeg','gif']",
},
{
argsName: "style",
type: "Object",
default: "{width: '130px',height: '130px'}",
desc: "上传组件的样式",
},
];
const fileUrls = ref([