| src/api/basicData/common.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/AttachmentPreview/image/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/AttachmentUpload/file/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/AttachmentUpload/image/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/basicData/common.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,13 @@ import request from '@/utils/request' // éç¨ä¸ä¼ æ¥å£ï¼æ¯æ FormData æ¹éä¼ æä»¶ export function uploadFile(data) { return request({ url: '/common/upload', method: 'post', data, headers: { 'Content-Type': 'multipart/form-data', }, }) } src/components/AttachmentPreview/image/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,76 @@ <script setup> const props = defineProps({ list: { type: Array, default: () => [], }, thumbSize: { type: Number, default: 72, }, gap: { type: Number, default: 10, }, }) const normalizedList = computed(() => { return (props.list || []) .filter((item) => item && item.previewURL) .map((item, index) => ({ id: item.id ?? index, name: item.originalFilename || `image-${index + 1}`, url: item.previewURL, })) }) const previewUrls = computed(() => normalizedList.value.map((item) => item.url)) </script> <template> <div class="attachment-image-preview"> <div v-if="!normalizedList.length" class="empty">ææ å¾ç</div> <div v-else class="thumbs" :style="{ gap: `${gap}px` }"> <el-image v-for="(item, index) in normalizedList" :key="item.id" class="thumb" :style="{ width: `${thumbSize}px`, height: `${thumbSize}px` }" :src="item.url" :preview-src-list="previewUrls" :initial-index="index" fit="cover" preview-teleported /> </div> </div> </template> <style scoped lang="scss"> .attachment-image-preview { width: 100%; } .empty { height: 120px; display: flex; align-items: center; justify-content: center; color: var(--el-text-color-secondary); border: 1px dashed var(--el-border-color); border-radius: 8px; } .thumbs { display: flex; flex-wrap: wrap; } .thumb { border: 1px solid var(--el-border-color); border-radius: 6px; overflow: hidden; cursor: pointer; background: #fff; } </style> src/components/AttachmentUpload/file/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,309 @@ <script setup> import { UploadFilled } from '@element-plus/icons-vue' import { uploadFile } from '@/api/basicData/common' const props = defineProps({ fileList: { type: Array, default: () => [], }, index: { type: Number, default: -1, }, childrenKey: { type: String, default: 'files', }, limit: { type: Number, default: 20, }, fileSize: { type: Number, default: 50, }, fileType: { type: Array, default: () => [], }, buttonText: { type: String, default: 'åå»éæ©æä»¶', }, disabled: { type: Boolean, default: false, }, uploadFieldName: { type: String, default: 'files', }, }) const emit = defineEmits(['update:fileList', 'change']) const { proxy } = getCurrentInstance() const uploadRef = ref() const uploadQueueTimer = ref(null) const uploading = ref(false) const queuedUidSet = ref(new Set()) const innerList = ref([]) function readListFromProps() { if (props.index > -1) { const row = props.fileList?.[props.index] return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : [] } return Array.isArray(props.fileList) ? props.fileList : [] } watch( () => props.fileList, () => { innerList.value = [...readListFromProps()] }, { deep: true, immediate: true }, ) const currentList = computed({ get() { return innerList.value }, set(value) { const nextList = Array.isArray(value) ? value : [] innerList.value = nextList if (props.index > -1) { const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : [] const currentRow = nextModelValue[props.index] || {} nextModelValue[props.index] = { ...currentRow, [props.childrenKey]: nextList, } emit('update:fileList', nextModelValue) emit('change', nextList, nextModelValue) return } emit('update:fileList', nextList) emit('change', nextList, nextList) }, }) const displayFileList = computed(() => { return currentList.value.map((item, index) => ({ uid: getItemUid(item, index), name: getItemName(item, index), url: getItemUrl(item), status: 'success', rawData: item, })) }) const uploadTip = computed(() => { if (!props.fileType.length) return `å个æä»¶ä¸è¶ è¿ ${props.fileSize}MB` return `æ¯æ ${props.fileType.join('/')}ï¼å个æä»¶ä¸è¶ è¿ ${props.fileSize}MB` }) function getItemUid(item, index) { if (item?.id !== undefined && item?.id !== null) return `${item.id}` return `${getItemName(item, index)}-${getItemUrl(item) || index}` } function getItemUrl(item) { if (!item) return '' if (typeof item === 'string') return item return item.url || item.downloadURL || item.previewURL || item.previewUrl || '' } function getItemName(item, index = 0) { if (!item) return `file-${index + 1}` if (typeof item === 'string') return `file-${index + 1}` return item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${index + 1}` } function normalizeResponseItem(item, index) { if (typeof item === 'string') { return { name: `file-${currentList.value.length + index + 1}`, url: item, } } return Object.assign({}, item, { url: item.url || item.downloadURL || item.previewURL || item.previewUrl || '', name: item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${currentList.value.length + index + 1}`, }) } function extractResponseArray(response) { if (Array.isArray(response)) return response if (Array.isArray(response?.data)) return response.data if (Array.isArray(response?.data?.data)) return response.data.data if (Array.isArray(response?.payload)) return response.payload if (Array.isArray(response?.payload?.data)) return response.payload.data if (Array.isArray(response?.rows)) return response.rows if (Array.isArray(response?.result)) return response.result return [] } function validateFile(rawFile) { const extension = rawFile.name.includes('.') ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase() : '' if (props.fileType.length) { const isValidType = props.fileType.some((type) => { const normalizedType = String(type).toLowerCase() return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType }) if (!isValidType) { proxy.$modal.msgError(`请ä¸ä¼ ${props.fileType.join('/')} æ ¼å¼çæä»¶`) return false } } const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize if (!isWithinSize) { proxy.$modal.msgError(`æä»¶å¤§å°ä¸è½è¶ è¿ ${props.fileSize}MB`) return false } return true } function scheduleUpload(uploadFiles) { clearTimeout(uploadQueueTimer.value) uploadQueueTimer.value = setTimeout(() => { const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid)) if (!readyFiles.length) return const remainCount = props.limit - currentList.value.length if (remainCount <= 0) { proxy.$modal.msgError(`æå¤ä¸ä¼ ${props.limit} 个æä»¶`) uploadRef.value?.clearFiles() return } const selectedFiles = readyFiles.slice(0, remainCount) if (selectedFiles.length < readyFiles.length) { proxy.$modal.msgWarning(`æå¤ä¸ä¼ ${props.limit} 个æä»¶ï¼è¶ åºé¨å已忽ç¥`) } selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid)) uploadSelectedFiles(selectedFiles) }, 0) } async function uploadSelectedFiles(files) { const validFiles = files.filter((file) => validateFile(file.raw)) const invalidFiles = files.filter((file) => !validFiles.includes(file)) invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid)) if (!validFiles.length) { uploadRef.value?.clearFiles() return } const formData = new FormData() validFiles.forEach((file) => { formData.append(props.uploadFieldName, file.raw) }) uploading.value = true proxy.$modal.loading('æä»¶ä¸ä¼ ä¸ï¼è¯·ç¨å...') try { const response = await uploadFile(formData) const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index)) if (!responseList.length) { proxy.$modal.msgError('ä¸ä¼ æ¥å£æªè¿åæ°ç»æ°æ®') return } currentList.value = [...currentList.value, ...responseList] proxy.$modal.msgSuccess('ä¸ä¼ æå') } catch (error) { proxy.$modal.msgError(error?.message || 'ä¸ä¼ 失败') } finally { validFiles.forEach((file) => queuedUidSet.value.delete(file.uid)) invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid)) uploadRef.value?.clearFiles() uploading.value = false proxy.$modal.closeLoading() } } function handleChange(file, uploadFiles) { if (props.disabled || uploading.value) return scheduleUpload(uploadFiles) } function handleRemove(file) { const targetUrl = file.url || getItemUrl(file.rawData) const nextList = currentList.value.filter((item, index) => { const itemUrl = getItemUrl(item) const itemName = getItemName(item, index) return !(itemUrl === targetUrl && itemName === file.name) }) currentList.value = nextList } function handleExceed() { proxy.$modal.msgError(`æå¤ä¸ä¼ ${props.limit} 个æä»¶`) } function openFile(file) { const fileUrl = file.url || getItemUrl(file.rawData) if (!fileUrl) return window.open(fileUrl, '_blank') } onBeforeUnmount(() => { clearTimeout(uploadQueueTimer.value) }) </script> <template> <div class="attachment-upload-file"> <el-upload ref="uploadRef" drag :auto-upload="false" :multiple="true" :show-file-list="true" :file-list="displayFileList" :disabled="disabled || uploading" :limit="limit" :on-change="handleChange" :on-remove="handleRemove" :on-exceed="handleExceed" :on-preview="openFile" > <el-icon class="upload-drag-icon"><UploadFilled /></el-icon> <div class="el-upload__text"> å°æä»¶æå°æ¤å¤ï¼æ <em>{{ buttonText }}</em> </div> <div class="upload-tip">{{ uploadTip }}</div> </el-upload> </div> </template> <style scoped lang="scss"> .attachment-upload-file { width: 100%; } .upload-drag-icon { font-size: 40px; color: var(--el-text-color-secondary); } .upload-tip { margin-top: 8px; color: var(--el-text-color-secondary); font-size: 12px; } </style> src/components/AttachmentUpload/image/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,335 @@ <script setup> import {Plus} from '@element-plus/icons-vue' import {uploadFile} from '@/api/basicData/common' const props = defineProps({ fileList: { type: Array, default: () => [], }, index: { type: Number, default: -1, }, childrenKey: { type: String, default: 'images', }, limit: { type: Number, default: 10, }, fileSize: { type: Number, default: 10, }, fileType: { type: Array, default: () => ['png', 'jpg', 'jpeg', 'webp'], }, buttonText: { type: String, default: 'ä¸ä¼ å¾ç', }, disabled: { type: Boolean, default: false, }, uploadFieldName: { type: String, default: 'files', }, }) const emit = defineEmits(['update:fileList', 'change']) const {proxy} = getCurrentInstance() const uploadRef = ref() const previewVisible = ref(false) const previewUrl = ref('') const uploadQueueTimer = ref(null) const uploading = ref(false) const queuedUidSet = ref(new Set()) const currentList = computed({ get() { if (props.index > -1) { const row = props.fileList?.[props.index] return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : [] } return Array.isArray(props.fileList) ? props.fileList : [] }, set(value) { const nextList = Array.isArray(value) ? value : [] if (props.index > -1) { const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : [] const currentRow = nextModelValue[props.index] || {} nextModelValue[props.index] = { ...currentRow, [props.childrenKey]: nextList, } emit('update:fileList', nextModelValue) emit('change', nextList, nextModelValue) return } emit('update:fileList', nextList) emit('change', nextList, nextList) }, }) const displayFileList = computed(() => { return currentList.value.map((item, index) => ({ uid: getItemUid(item, index), name: getItemName(item, index), url: getItemUrl(item), status: 'success', rawData: item, })) }) const uploadTip = computed(() => { return `æ¯æ ${props.fileType.join('/')}ï¼åå¼ ä¸è¶ è¿ ${props.fileSize}MB` }) function getItemUid(item, index) { if (item?.id !== undefined && item?.id !== null) return `${item.id}` return `${getItemName(item, index)}-${getItemUrl(item) || index}` } function getItemUrl(item) { if (!item) return '' if (typeof item === 'string') return item return item.url || item.previewURL || '' } function getItemName(item, index = 0) { if (!item) return `image-${index + 1}` if (typeof item === 'string') return `image-${index + 1}` return item.name || item.fileName || item.originalFilename || `image-${index + 1}` } function normalizeResponseItem(item, index) { if (typeof item === 'string') { return { name: `image-${currentList.value.length + index + 1}`, url: item, } } return Object.assign({}, item, { url: item.url || item.previewURL || item.previewUrl || '', name: item.name || item.originalFilename || item.fileName || `image-${currentList.value.length + index + 1}`, }) } function extractResponseArray(response) { if (Array.isArray(response)) return response if (Array.isArray(response?.data)) return response.data if (Array.isArray(response?.data?.data)) return response.data.data if (Array.isArray(response?.payload)) return response.payload if (Array.isArray(response?.payload?.data)) return response.payload.data if (Array.isArray(response?.rows)) return response.rows if (Array.isArray(response?.result)) return response.result return [] } function validateFile(rawFile) { let isValidType = false const extension = rawFile.name.includes('.') ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase() : '' if (props.fileType.length) { isValidType = props.fileType.some((type) => { const normalizedType = String(type).toLowerCase() return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType }) } else { isValidType = rawFile.type.includes('image') } if (!isValidType) { proxy.$modal.msgError(`请ä¸ä¼ ${props.fileType.join('/')} æ ¼å¼çå¾ç`) return false } const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize if (!isWithinSize) { proxy.$modal.msgError(`å¾ç大å°ä¸è½è¶ è¿ ${props.fileSize}MB`) return false } return true } function scheduleUpload(uploadFiles) { clearTimeout(uploadQueueTimer.value) uploadQueueTimer.value = setTimeout(() => { const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid)) if (!readyFiles.length) return const remainCount = props.limit - currentList.value.length if (remainCount <= 0) { proxy.$modal.msgError(`æå¤ä¸ä¼ ${props.limit} å¼ å¾ç`) uploadRef.value?.clearFiles() return } const selectedFiles = readyFiles.slice(0, remainCount) if (selectedFiles.length < readyFiles.length) { proxy.$modal.msgWarning(`æå¤ä¸ä¼ ${props.limit} å¼ å¾çï¼è¶ åºé¨å已忽ç¥`) } selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid)) uploadSelectedFiles(selectedFiles) }, 0) } async function uploadSelectedFiles(files) { const validFiles = files.filter((file) => validateFile(file.raw)) const invalidFiles = files.filter((file) => !validFiles.includes(file)) invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid)) if (!validFiles.length) { uploadRef.value?.clearFiles() return } const formData = new FormData() validFiles.forEach((file) => { formData.append(props.uploadFieldName, file.raw) }) uploading.value = true proxy.$modal.loading('å¾çä¸ä¼ ä¸ï¼è¯·ç¨å...') try { const response = await uploadFile(formData) const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index)) if (!responseList.length) { proxy.$modal.msgError('ä¸ä¼ æ¥å£æªè¿åæ°ç»æ°æ®') return } console.log('responseList', responseList) currentList.value = [...currentList.value, ...responseList] console.log('currentList.value', currentList.value) proxy.$modal.msgSuccess('ä¸ä¼ æå') } catch (error) { proxy.$modal.msgError(error?.message || 'ä¸ä¼ 失败') } finally { validFiles.forEach((file) => queuedUidSet.value.delete(file.uid)) invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid)) uploadRef.value?.clearFiles() uploading.value = false proxy.$modal.closeLoading() } } function handleChange(file, uploadFiles) { if (props.disabled || uploading.value) return scheduleUpload(uploadFiles) } function handleRemove(file) { const targetUrl = file.url || getItemUrl(file.rawData) const nextList = currentList.value.filter((item, index) => { const itemUrl = getItemUrl(item) const itemName = getItemName(item, index) return !(itemUrl === targetUrl && itemName === file.name) }) currentList.value = nextList } function handlePreview(file) { previewUrl.value = file.url || getItemUrl(file.rawData) previewVisible.value = true } function handleExceed() { proxy.$modal.msgError(`æå¤ä¸ä¼ ${props.limit} å¼ å¾ç`) } onBeforeUnmount(() => { clearTimeout(uploadQueueTimer.value) }) </script> <template> <div class="attachment-upload-image"> <el-upload ref="uploadRef" :auto-upload="false" :multiple="true" :show-file-list="true" :file-list="displayFileList" list-type="picture-card" accept="image/*" :disabled="disabled || uploading" :limit="limit" :on-change="handleChange" :on-remove="handleRemove" :on-preview="handlePreview" :on-exceed="handleExceed" > <div class="upload-trigger"> <el-icon> <Plus/> </el-icon> <span>{{ buttonText }}</span> </div> </el-upload> <div class="upload-tip"> {{ uploadTip }} </div> <el-dialog v-model="previewVisible" title="å¾çé¢è§" width="720px" append-to-body> <img class="preview-image" :src="previewUrl" alt="preview"/> </el-dialog> </div> </template> <style scoped lang="scss"> .attachment-upload-image { width: 100%; } .upload-trigger { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.2; } .upload-tip { margin-top: 8px; color: var(--el-text-color-secondary); font-size: 12px; } .preview-image { display: block; max-width: 100%; margin: 0 auto; } :deep(.el-upload-list--picture-card) { margin: 0; } :deep(.el-upload--picture-card) { width: 132px; height: 132px; } :deep(.el-upload-list--picture-card .el-upload-list__item) { width: 132px; height: 132px; } </style>