From 7c2cdcbc7f5585b96fba76a07b0e4417a09c4d7e Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 02 六月 2026 11:06:10 +0800
Subject: [PATCH] 新疆马铃薯 1.过程检验下载添加3个模版

---
 src/components/AttachmentUpload/image/index.vue |  335 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 335 insertions(+), 0 deletions(-)

diff --git a/src/components/AttachmentUpload/image/index.vue b/src/components/AttachmentUpload/image/index.vue
new file mode 100644
index 0000000..8243f9c
--- /dev/null
+++ b/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锛屾渶澶氫笂浼� ${props.limit} 寮犲浘鐗嘸
+})
+
+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>

--
Gitblit v1.9.3