| | |
| | | :on-success="handleUploadSuccess" |
| | | :show-file-list="false" |
| | | :headers="headers" |
| | | :http-request="UploadImage" |
| | | class="upload-file-uploader" |
| | | ref="fileUpload" |
| | | v-if="!disabled" |
| | |
| | | <!-- 上传提示 --> |
| | | <div class="el-upload__tip" v-if="showTip && !disabled"> |
| | | 请上传 |
| | | <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template> |
| | | <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template> |
| | | <template v-if="fileSize"> |
| | | 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> |
| | | </template> |
| | | <template v-if="fileType"> |
| | | 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> |
| | | </template> |
| | | 的文件 |
| | | </div> |
| | | <!-- 文件列表 --> |
| | | <transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul"> |
| | | <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList"> |
| | | <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank"> |
| | | <span class="el-icon-document"> {{ getFileName(file.name) }} </span> |
| | | <transition-group |
| | | ref="uploadFileList" |
| | | class="upload-file-list el-upload-list el-upload-list--text" |
| | | name="el-fade-in-linear" |
| | | tag="ul" |
| | | > |
| | | <li |
| | | :key="file.uid" |
| | | class="el-upload-list__item ele-upload-list__item-content" |
| | | v-for="(file, index) in fileList" |
| | | > |
| | | <el-link :href="`${file.url}`" :underline="false" target="_blank"> |
| | | <!-- 取8位 --> |
| | | <span class="el-icon-document"> |
| | | {{ file.bucketFilename.substring(0, 18) }} |
| | | </span> |
| | | </el-link> |
| | | <div class="ele-upload-list__item-content-action"> |
| | | <el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled"> 删除</el-link> |
| | | <el-link |
| | | :underline="false" |
| | | @click="handleDownload(index)" |
| | | type="primary" |
| | | v-if="!disabled" |
| | | > 下载</el-link |
| | | > |
| | | <el-link |
| | | :underline="false" |
| | | @click="handleDelete(index)" |
| | | type="danger" |
| | | v-if="!disabled" |
| | | > 删除</el-link |
| | | > |
| | | </div> |
| | | </li> |
| | | </transition-group> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { getToken } from "@/utils/auth" |
| | | import Sortable from 'sortablejs' |
| | | import { ref, computed, watch, onMounted, nextTick, getCurrentInstance } from 'vue'; |
| | | import { getToken } from "@/utils/auth"; |
| | | import Sortable from "sortablejs"; |
| | | import { ElMessage } from "element-plus"; |
| | | import axios from "axios"; |
| | | |
| | | // Props 定义 |
| | | const props = defineProps({ |
| | | modelValue: [String, Object, Array], |
| | | // 上传接口地址 |
| | | action: { |
| | | type: String, |
| | | default: "/common/upload" |
| | | action: { type: String, default: "/common/minioUploads" }, |
| | | data: { type: Object }, |
| | | limit: { type: Number, default: 5 }, |
| | | fileSize: { type: Number, default: 5 }, |
| | | fileType: { |
| | | type: Array, |
| | | default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"] |
| | | }, |
| | | // 上传携带的参数 |
| | | data: { |
| | | type: Object |
| | | }, |
| | | // 数量限制 |
| | | limit: { |
| | | type: Number, |
| | | default: 5 |
| | | }, |
| | | // 大小限制(MB) |
| | | fileSize: { |
| | | type: Number, |
| | | default: 5 |
| | | }, |
| | | // 文件类型, 例如['png', 'jpg', 'jpeg'] |
| | | fileType: { |
| | | type: Array, |
| | | default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"] |
| | | }, |
| | | // 是否显示提示 |
| | | isShowTip: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | // 禁用组件(仅查看文件) |
| | | disabled: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | // 拖动排序 |
| | | drag: { |
| | | type: Boolean, |
| | | default: true |
| | | } |
| | | }) |
| | | isShowTip: { type: Boolean, default: true }, |
| | | disabled: { type: Boolean, default: false }, |
| | | drag: { type: Boolean, default: true }, |
| | | statusType: { type: Number, default: "" }, // 用于区分不同状态的上传 |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits() |
| | | const number = ref(0) |
| | | const uploadList = ref([]) |
| | | const baseUrl = import.meta.env.VITE_APP_BASE_API |
| | | const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址 |
| | | const headers = ref({ Authorization: "Bearer " + getToken() }) |
| | | const fileList = ref([]) |
| | | const showTip = computed( |
| | | () => props.isShowTip && (props.fileType || props.fileSize) |
| | | ) |
| | | // 组件实例和事件 |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(['update:modelValue']); |
| | | |
| | | watch(() => props.modelValue, val => { |
| | | if (val) { |
| | | let temp = 1 |
| | | // 首先将值转为数组 |
| | | const list = Array.isArray(val) ? val : props.modelValue.split(',') |
| | | // 然后将数组转为对象数组 |
| | | fileList.value = list.map(item => { |
| | | if (typeof item === "string") { |
| | | item = { name: item, url: item } |
| | | // 响应式数据 |
| | | const number = ref(0); |
| | | const uploadList = ref([]); |
| | | const fileList = ref([]); |
| | | |
| | | // 计算属性 |
| | | const uploadFileUrl = computed(() => import.meta.env.VITE_APP_BASE_API + props.action); |
| | | const headers = computed(() => ({ Authorization: "Bearer " + getToken() })); |
| | | const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); |
| | | // 初始化和编辑初始化方法 |
| | | const init = () => { |
| | | fileList.value = []; |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | }; |
| | | |
| | | const editInit = (val) => { |
| | | fileList.value = []; |
| | | val.storageBlobDTO.forEach((element) => { |
| | | fileList.value.push(element); |
| | | uploadedSuccessfully(); |
| | | }); |
| | | }; |
| | | |
| | | // 暴露方法 |
| | | defineExpose({ init, editInit }); |
| | | |
| | | // 监听 modelValue 变化 |
| | | watch( |
| | | () => props.modelValue, |
| | | (val) => { |
| | | if (val) { |
| | | let temp = 1; |
| | | const list = Array.isArray(val) ? val : props.modelValue.split(","); |
| | | fileList.value = list.map((item) => { |
| | | if (typeof item === "string") { |
| | | item = { name: item, url: item }; |
| | | } |
| | | item.uid = item.uid || new Date().getTime() + temp++; |
| | | return item; |
| | | }); |
| | | } else { |
| | | fileList.value = []; |
| | | } |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | // 文件上传处理 |
| | | const UploadImage = (param) => { |
| | | const formData = new FormData(); |
| | | formData.append("files", param.file); |
| | | formData.append("type", props.statusType); |
| | | axios.post(uploadFileUrl.value, formData, { |
| | | headers: { |
| | | "Content-Type": "multipart/form-data", |
| | | ...headers.value, |
| | | }, |
| | | onUploadProgress: (progressEvent) => { |
| | | const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); |
| | | param.onProgress({ percent }); |
| | | }, |
| | | }) |
| | | .then((response) => { |
| | | if (response.data.code === 200) { |
| | | handleUploadSuccess(response.data, param.file); |
| | | ElMessage.success("上传成功"); |
| | | emit("update:modelValue", fileList.value); |
| | | } else { |
| | | param.onError(new Error(response.data.msg)); |
| | | ElMessage.error(response.data.msg); |
| | | handleUploadError(); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | param.onError(error); |
| | | }); |
| | | }; |
| | | // 获取文件Base64(可选功能,暂时保留) |
| | | const getBase64ByUrl = (url) => { |
| | | return new Promise((resolve, reject) => { |
| | | const xhr = new XMLHttpRequest(); |
| | | xhr.open("GET", url, true); |
| | | xhr.responseType = "blob"; |
| | | xhr.onload = function () { |
| | | if (this.status === 200) { |
| | | const reader = new FileReader(); |
| | | reader.onloadend = () => resolve(reader.result); |
| | | reader.readAsDataURL(this.response); |
| | | } else { |
| | | reject(new Error("Failed to fetch image")); |
| | | } |
| | | item.uid = item.uid || new Date().getTime() + temp++ |
| | | return item |
| | | }) |
| | | } else { |
| | | fileList.value = [] |
| | | return [] |
| | | } |
| | | },{ deep: true, immediate: true }) |
| | | }; |
| | | xhr.onerror = () => reject(new Error("Network error")); |
| | | xhr.send(); |
| | | }); |
| | | }; |
| | | |
| | | // 上传前校检格式和大小 |
| | | function handleBeforeUpload(file) { |
| | | // 校检文件类型 |
| | | if (props.fileType.length) { |
| | | const fileName = file.name.split('.') |
| | | const fileExt = fileName[fileName.length - 1] |
| | | const isTypeOk = props.fileType.indexOf(fileExt) >= 0 |
| | | if (!isTypeOk) { |
| | | proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`) |
| | | return false |
| | | } |
| | | // 移动端下载图片 |
| | | const downloadImageForMobile = (imgSrc, name) => { |
| | | const image = new Image(); |
| | | image.setAttribute("crossOrigin", "anonymous"); |
| | | image.onload = () => { |
| | | const canvas = document.createElement("canvas"); |
| | | canvas.width = image.width; |
| | | canvas.height = image.height; |
| | | const ctx = canvas.getContext("2d"); |
| | | ctx.drawImage(image, 0, 0, image.width, image.height); |
| | | const url = canvas.toDataURL("image/png"); |
| | | const a = document.createElement("a"); |
| | | a.download = name; |
| | | a.href = url; |
| | | a.click(); |
| | | }; |
| | | image.src = imgSrc; |
| | | }; |
| | | |
| | | // 下载文件 |
| | | const handleDownload = (index) => { |
| | | const file = fileList.value[index]; |
| | | const url = file.url; |
| | | if (!url) { |
| | | proxy.$modal.msgError("文件链接不存在,无法下载"); |
| | | return; |
| | | } |
| | | // 校检文件名是否包含特殊字符 |
| | | if (file.name.includes(',')) { |
| | | proxy.$modal.msgError('文件名不正确,不能包含英文逗号!') |
| | | return false |
| | | // 移动端特殊处理 |
| | | if (window.navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)) { |
| | | const fileName = file.bucketFilename || file.key; |
| | | downloadImageForMobile(url, fileName); |
| | | return; |
| | | } |
| | | // 校检文件大小 |
| | | // 桌面端下载 |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.download = file.bucketFilename || file.key; |
| | | link.click(); |
| | | }; |
| | | // 上传前校验 |
| | | const handleBeforeUpload = (file) => { |
| | | // 校验文件名特殊字符 |
| | | if (file.name.includes(",")) { |
| | | proxy.$modal.msgError("文件名不正确,不能包含英文逗号!"); |
| | | return false; |
| | | } |
| | | |
| | | // 校验文件大小 |
| | | if (props.fileSize) { |
| | | const isLt = file.size / 1024 / 1024 < props.fileSize |
| | | const isLt = file.size / 1024 / 1024 < props.fileSize; |
| | | if (!isLt) { |
| | | proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`) |
| | | return false |
| | | proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`); |
| | | return false; |
| | | } |
| | | } |
| | | proxy.$modal.loading("正在上传文件,请稍候...") |
| | | number.value++ |
| | | return true |
| | | } |
| | | |
| | | // 文件个数超出 |
| | | function handleExceed() { |
| | | proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`) |
| | | } |
| | | proxy.$modal.loading("正在上传文件,请稍候..."); |
| | | number.value++; |
| | | return true; |
| | | }; |
| | | |
| | | // 上传失败 |
| | | function handleUploadError(err) { |
| | | proxy.$modal.msgError("上传文件失败") |
| | | proxy.$modal.closeLoading() |
| | | } |
| | | // 文件个数超出限制 |
| | | const handleExceed = () => { |
| | | proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`); |
| | | }; |
| | | |
| | | // 上传失败处理 |
| | | const handleUploadError = () => { |
| | | ElMessage.error("上传文件失败"); |
| | | proxy.$modal.closeLoading(); |
| | | }; |
| | | |
| | | // 上传成功回调 |
| | | function handleUploadSuccess(res, file) { |
| | | const handleUploadSuccess = (res, file) => { |
| | | if (res.code === 200) { |
| | | uploadList.value.push({ name: res.fileName, url: res.fileName }) |
| | | uploadedSuccessfully() |
| | | uploadList.value.push(res.data[0]); |
| | | uploadedSuccessfully(); |
| | | } else { |
| | | number.value-- |
| | | proxy.$modal.closeLoading() |
| | | proxy.$modal.msgError(res.msg) |
| | | proxy.$refs.fileUpload.handleRemove(file) |
| | | uploadedSuccessfully() |
| | | number.value--; |
| | | proxy.$modal.closeLoading(); |
| | | proxy.$modal.msgError(res.msg); |
| | | proxy.$refs.fileUpload.handleRemove(file); |
| | | uploadedSuccessfully(); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 删除文件 |
| | | function handleDelete(index) { |
| | | fileList.value.splice(index, 1) |
| | | emit("update:modelValue", listToString(fileList.value)) |
| | | } |
| | | const handleDelete = (index) => { |
| | | fileList.value.splice(index, 1); |
| | | emit("update:modelValue", listToString(fileList.value)); |
| | | }; |
| | | |
| | | // 上传结束处理 |
| | | function uploadedSuccessfully() { |
| | | const uploadedSuccessfully = () => { |
| | | if (number.value > 0 && uploadList.value.length === number.value) { |
| | | fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value) |
| | | uploadList.value = [] |
| | | number.value = 0 |
| | | emit("update:modelValue", listToString(fileList.value)) |
| | | proxy.$modal.closeLoading() |
| | | fileList.value = fileList.value |
| | | .filter((f) => f.url !== undefined) |
| | | .concat(uploadList.value); |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | emit("update:modelValue"); |
| | | proxy.$modal.closeLoading(); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 获取文件名称 |
| | | function getFileName(name) { |
| | | // 如果是url那么取最后的名字 如果不是直接返回 |
| | | if (name.lastIndexOf("/") > -1) { |
| | | return name.slice(name.lastIndexOf("/") + 1) |
| | | } else { |
| | | return name |
| | | } |
| | | } |
| | | |
| | | // 对象转成指定字符串分隔 |
| | | function listToString(list, separator) { |
| | | let strs = "" |
| | | separator = separator || "," |
| | | for (let i in list) { |
| | | if (list[i].url) { |
| | | strs += list[i].url + separator |
| | | } |
| | | } |
| | | return strs != '' ? strs.substr(0, strs.length - 1) : '' |
| | | } |
| | | const listToString = (list, separator = ",") => { |
| | | const strs = list |
| | | .filter(item => item.url) |
| | | .map(item => item.url) |
| | | .join(separator); |
| | | return strs; |
| | | }; |
| | | |
| | | // 初始化拖拽排序 |
| | | onMounted(() => { |
| | | if (props.drag && !props.disabled) { |
| | | nextTick(() => { |
| | | const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList |
| | | Sortable.create(element, { |
| | | ghostClass: 'file-upload-darg', |
| | | onEnd: (evt) => { |
| | | const movedItem = fileList.value.splice(evt.oldIndex, 1)[0] |
| | | fileList.value.splice(evt.newIndex, 0, movedItem) |
| | | emit('update:modelValue', listToString(fileList.value)) |
| | | } |
| | | }) |
| | | }) |
| | | const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList; |
| | | if (element) { |
| | | Sortable.create(element, { |
| | | ghostClass: "file-upload-darg", |
| | | onEnd: (evt) => { |
| | | const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]; |
| | | fileList.value.splice(evt.newIndex, 0, movedItem); |
| | | emit("update:modelValue", listToString(fileList.value)); |
| | | }, |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }) |
| | | }); |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .file-upload-darg { |
| | |
| | | } |
| | | .upload-file-uploader { |
| | | margin-bottom: 5px; |
| | | } |
| | | .upload-file-list { |
| | | margin-top: 10px; |
| | | max-height: 150px; |
| | | overflow-y: auto; |
| | | } |
| | | .upload-file-list .el-upload-list__item { |
| | | border: 1px solid #e4e7ed; |
| | |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | color: inherit; |
| | | max-height: 20px; |
| | | } |
| | | .ele-upload-list__item-content-action .el-link { |
| | | margin-right: 10px; |
| | | } |
| | | .el-icon-document { |
| | | padding: 10px; |
| | | } |
| | | </style> |