From a582fffa7d3f7283e809a7940538ab02e4677948 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期五, 24 四月 2026 09:55:21 +0800
Subject: [PATCH] Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_NEW_pro

---
 src/api/basicData/common.js                               |   13 
 src/components/AttachmentUpload/image/index.vue           |  335 ++++++++++++
 src/components/AttachmentPreview/image/index.vue          |   76 ++
 src/components/AttachmentUpload/file/index.vue            |  309 +++++++++++
 src/api/productionManagement/productBom.js                |    8 
 src/views/productionManagement/productStructure/index.vue |  869 ++++++++++++++++++--------------
 6 files changed, 1,227 insertions(+), 383 deletions(-)

diff --git a/src/api/basicData/common.js b/src/api/basicData/common.js
new file mode 100644
index 0000000..01c6f35
--- /dev/null
+++ b/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',
+    },
+  })
+}
diff --git a/src/api/productionManagement/productBom.js b/src/api/productionManagement/productBom.js
index 097d223..517208b 100644
--- a/src/api/productionManagement/productBom.js
+++ b/src/api/productionManagement/productBom.js
@@ -19,6 +19,14 @@
   });
 }
 
+// 澶嶅埗
+export function copy(data) {
+  return request({
+    url: "/technologyBom/copy",
+    method: "post",
+    data: data,
+  });
+}
 // 淇敼
 export function update(data) {
   return request({
diff --git a/src/components/AttachmentPreview/image/index.vue b/src/components/AttachmentPreview/image/index.vue
new file mode 100644
index 0000000..5211fac
--- /dev/null
+++ b/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>
diff --git a/src/components/AttachmentUpload/file/index.vue b/src/components/AttachmentUpload/file/index.vue
new file mode 100644
index 0000000..b68bd4f
--- /dev/null
+++ b/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>
diff --git a/src/components/AttachmentUpload/image/index.vue b/src/components/AttachmentUpload/image/index.vue
new file mode 100644
index 0000000..25bd13c
--- /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`
+})
+
+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>
diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
index 8c9cfd1..e05ed3c 100644
--- a/src/views/productionManagement/productStructure/index.vue
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -1,382 +1,514 @@
 <template>
   <div class="app-container">
-		<div class="table_list">
-			<div style="text-align: right; margin-bottom: 10px;">
-				<el-button type="primary" @click="handleAdd">鏂板</el-button>
-				<el-button type="info" plain icon="Upload" @click="handleImport"
-				           v-hasPermi="['product:bom:import']">瀵煎叆</el-button>
-				<el-button type="warning" plain icon="Download" @click="handleExport" :disabled="selectedRows.length !== 1"
-				           v-hasPermi="['product:bom:export']">瀵煎嚭</el-button>
-				<el-button type="danger" plain @click="handleBatchDelete" :disabled="selectedRows.length === 0">鍒犻櫎</el-button>
-			</div>
-			<PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true"
-			          @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination">
-				<template #detail="{ row }">
-					<el-button type="primary" text @click="showDetail(row)">{{ row.bomNo }}
-					</el-button>
-				</template>
-			</PIMTable>
-		</div>
-    <StructureEdit v-if="showEdit" v-model:show-model="showEdit" :record="currentRow" />
-
+    <div class="table_list">
+      <div style="text-align: right; margin-bottom: 10px;">
+        <el-button type="primary"
+                   @click="handleAdd">鏂板</el-button>
+        <el-button type="info"
+                   plain
+                   icon="Upload"
+                   @click="handleImport"
+                   v-hasPermi="['product:bom:import']">瀵煎叆</el-button>
+        <el-button type="warning"
+                   plain
+                   icon="Download"
+                   @click="handleExport"
+                   :disabled="selectedRows.length !== 1"
+                   v-hasPermi="['product:bom:export']">瀵煎嚭</el-button>
+        <el-button type="danger"
+                   plain
+                   @click="handleBatchDelete"
+                   :disabled="selectedRows.length === 0">鍒犻櫎</el-button>
+      </div>
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                @selection-change="handleSelectionChange"
+                :tableLoading="tableLoading"
+                @pagination="pagination">
+        <template #detail="{ row }">
+          <el-button type="primary"
+                     text
+                     @click="showDetail(row)">{{ row.bomNo }}
+          </el-button>
+        </template>
+      </PIMTable>
+    </div>
+    <StructureEdit v-if="showEdit"
+                   v-model:show-model="showEdit"
+                   :record="currentRow" />
     <!-- 鏂板/缂栬緫寮圭獥 -->
-    <el-dialog v-model="dialogVisible" :title="operationType === 'add' ? '鏂板BOM' : '缂栬緫BOM'" width="600px"
-      @close="closeDialog">
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
-        <el-form-item label="浜у搧鍚嶇О" prop="productModelId">
-          <el-button type="primary" @click="showProductSelectDialog = true">
+    <el-dialog v-model="dialogVisible"
+               :title="operationType === 'add' ? '鏂板BOM' : '缂栬緫BOM'"
+               width="600px"
+               @close="closeDialog">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="120px">
+        <el-form-item label="浜у搧鍚嶇О"
+                      prop="productModelId">
+          <el-button type="primary"
+                     @click="showProductSelectDialog = true">
             {{ form.productName || '閫夋嫨浜у搧' }}
           </el-button>
         </el-form-item>
-        <el-form-item label="鐗堟湰鍙�" prop="version">
-          <el-input v-model="form.version" placeholder="璇疯緭鍏ョ増鏈彿" clearable />
+        <el-form-item label="鐗堟湰鍙�"
+                      prop="version">
+          <el-input v-model="form.version"
+                    placeholder="璇疯緭鍏ョ増鏈彿"
+                    clearable />
         </el-form-item>
-        <el-form-item label="澶囨敞" prop="remark">
-          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" clearable />
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    clearable />
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button type="primary" @click="handleSubmit">纭畾</el-button>
+        <el-button type="primary"
+                   @click="handleSubmit">纭畾</el-button>
         <el-button @click="closeDialog">鍙栨秷</el-button>
       </template>
     </el-dialog>
-
     <!-- 浜у搧閫夋嫨寮圭獥 -->
-    <ProductSelectDialog v-model="showProductSelectDialog" @confirm="handleProductSelect" single />
-
+    <ProductSelectDialog v-model="showProductSelectDialog"
+                         @confirm="handleProductSelect"
+                         single />
     <!-- BOM瀵煎叆瀵硅瘽妗� -->
-    <ImportDialog ref="uploadRef" v-model="upload.open" :title="upload.title" :action="upload.url"
-      :headers="upload.headers" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress"
-      :on-success="handleFileSuccess" :show-download-template="true" @confirm="submitFileForm"
-      @download-template="handleDownloadTemplate" @close="handleImportClose" />
+    <ImportDialog ref="uploadRef"
+                  v-model="upload.open"
+                  :title="upload.title"
+                  :action="upload.url"
+                  :headers="upload.headers"
+                  :disabled="upload.isUploading"
+                  :on-progress="handleFileUploadProgress"
+                  :on-success="handleFileSuccess"
+                  :show-download-template="true"
+                  @confirm="submitFileForm"
+                  @download-template="handleDownloadTemplate"
+                  @close="handleImportClose" />
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, toRefs, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
-import { getToken } from "@/utils/auth";
-import { listPage, add, update, batchDelete, exportBom, downloadTemplate } from "@/api/productionManagement/productBom.js";
-import { useRouter } from 'vue-router'
-import { ElMessageBox } from 'element-plus'
-import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
-import ImportDialog from "@/components/Dialog/ImportDialog.vue";
+  import {
+    ref,
+    reactive,
+    toRefs,
+    onMounted,
+    getCurrentInstance,
+    defineAsyncComponent,
+  } from "vue";
+  import { getToken } from "@/utils/auth";
+  import {
+    listPage,
+    add,
+    copy,
+    update,
+    batchDelete,
+    exportBom,
+    downloadTemplate,
+  } from "@/api/productionManagement/productBom.js";
+  import { useRouter } from "vue-router";
+  import { ElMessageBox } from "element-plus";
+  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
 
-const router = useRouter()
-const { proxy } = getCurrentInstance()
-const StructureEdit = defineAsyncComponent(() => import('@/views/productionManagement/productStructure/StructureEdit.vue'))
+  const router = useRouter();
+  const { proxy } = getCurrentInstance();
+  const StructureEdit = defineAsyncComponent(() =>
+    import("@/views/productionManagement/productStructure/StructureEdit.vue")
+  );
 
-const tableColumn = ref([
-  {
-    label: "BOM缂栧彿",
-    prop: "bomNo",
-    dataType: 'slot',
-    slot: "detail",
-    minWidth: 140
-  },
-  {
-    label: "浜у搧鍚嶇О",
-    prop: "productName",
+  const tableColumn = ref([
+    {
+      label: "BOM缂栧彿",
+      prop: "bomNo",
+      dataType: "slot",
+      slot: "detail",
+      minWidth: 140,
+    },
+    {
+      label: "浜у搧鍚嶇О",
+      prop: "productName",
 
-    minWidth: 160
-  },
-  {
-    label: "瑙勬牸鍨嬪彿",
-    prop: "productModelName",
-    minWidth: 140
-  },
-  {
-    label: "鐗堟湰鍙�",
-    prop: "version",
-    width: 100
-  },
-  {
-    label: "澶囨敞",
-    prop: "remark",
-    minWidth: 160
-  },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    fixed: "right",
-    width: 150,
-    operation: [
-      {
-        name: "缂栬緫",
-        type: "text",
-        clickFun: (row) => {
-          handleEdit(row)
-        }
-      },
-      {
-        name: "鍒犻櫎",
-        type: "danger",
-        link: true,
-        clickFun: (row) => {
-          handleDelete(row)
-        }
-      }
-    ]
-  }
-]);
+      minWidth: 160,
+    },
+    {
+      label: "瑙勬牸鍨嬪彿",
+      prop: "productModelName",
+      minWidth: 140,
+    },
+    {
+      label: "鐗堟湰鍙�",
+      prop: "version",
+      width: 100,
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+      minWidth: 160,
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 250,
+      operation: [
+        {
+          name: "澶嶅埗",
+          type: "text",
+          clickFun: row => {
+            handleCopy(row);
+          },
+        },
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            handleEdit(row);
+          },
+        },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          link: true,
+          clickFun: row => {
+            handleDelete(row);
+          },
+        },
+      ],
+    },
+  ]);
 
-const tableData = ref([]);
-const tableLoading = ref(false);
-const showEdit = ref(false);
-const selectedRows = ref([]);
-const currentRow = ref({});
-const dialogVisible = ref(false);
-const operationType = ref('add'); // add | edit
-const formRef = ref(null);
-const showProductSelectDialog = ref(false);
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const showEdit = ref(false);
+  const selectedRows = ref([]);
+  const currentRow = ref({});
+  const dialogVisible = ref(false);
+  const operationType = ref("add"); // add | edit
+  const formRef = ref(null);
+  const showProductSelectDialog = ref(false);
 
-//  BOM瀵煎叆鍙傛暟
-const upload = reactive({
-  // 鏄惁鏄剧ず寮瑰嚭灞傦紙BOM瀵煎叆锛�
-  open: false,
-  // 寮瑰嚭灞傛爣棰橈紙BOM瀵煎叆锛�
-  title: "",
-  // 鏄惁绂佺敤涓婁紶
-  isUploading: false,
-  // 璁剧疆涓婁紶鐨勮姹傚ご閮�
-  headers: { Authorization: "Bearer " + getToken() },
-  // 涓婁紶鐨勫湴鍧�
-  url: import.meta.env.VITE_APP_BASE_API + "/technologyBom/uploadBom"
-});
-
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-
-const data = reactive({
-  form: {
-    id: undefined,
-    productName: "",
-    productModelName: "",
-    productModelId: "",
-    remark: "",
-    version: ""
-  },
-  rules: {
-    productModelId: [{ required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" }],
-    version: [{ required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur" }]
-  }
-});
-
-const { form, rules } = toRefs(data);
-
-// 琛ㄦ牸閫夋嫨鏁版嵁
-const handleSelectionChange = (selection) => {
-  selectedRows.value = selection;
-};
-
-// 鍒嗛〉
-const pagination = (obj) => {
-  page.current = obj.page;
-  page.size = obj.limit;
-  getList();
-};
-
-// 鏌ヨ鍒楄〃
-const getList = () => {
-  tableLoading.value = true;
-  listPage({
-    current: page.current,
-    size: page.size,
-  })
-    .then((res) => {
-      const records = res?.data?.records || [];
-      tableData.value = records;
-      page.total = res?.data?.total || 0;
-    })
-    .catch((err) => {
-      console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
-    })
-    .finally(() => {
-      tableLoading.value = false;
-    });
-};
-
-// 鏂板
-const handleAdd = () => {
-  operationType.value = 'add';
-  Object.assign(form.value, {
-    id: undefined,
-    productName: "",
-    productModelName: "",
-    productModelId: "",
-    remark: "",
-    version: ""
+  //  BOM瀵煎叆鍙傛暟
+  const upload = reactive({
+    // 鏄惁鏄剧ず寮瑰嚭灞傦紙BOM瀵煎叆锛�
+    open: false,
+    // 寮瑰嚭灞傛爣棰橈紙BOM瀵煎叆锛�
+    title: "",
+    // 鏄惁绂佺敤涓婁紶
+    isUploading: false,
+    // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+    headers: { Authorization: "Bearer " + getToken() },
+    // 涓婁紶鐨勫湴鍧�
+    url: import.meta.env.VITE_APP_BASE_API + "/technologyBom/uploadBom",
   });
-  dialogVisible.value = true;
-};
 
-// 缂栬緫
-const handleEdit = (row) => {
-  operationType.value = 'edit';
-  Object.assign(form.value, {
-    id: row.id,
-    productName: row.productName || "",
-    productModelName: row.productModelName || "",
-    productModelId: row.productModelId || "",
-    remark: row.remark || "",
-    version: row.version || ""
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
   });
-  dialogVisible.value = true;
-};
 
-// 鍒犻櫎锛堝崟鏉★級
-const handleDelete = (row) => {
-  ElMessageBox.confirm('纭鍒犻櫎璇OM锛�', '鎻愮ず', {
-    confirmButtonText: '纭',
-    cancelButtonText: '鍙栨秷',
-    type: 'warning'
-  })
-    .then(() => {
-      batchDelete([row.id])
-        .then(() => {
-          proxy.$modal.msgSuccess('鍒犻櫎鎴愬姛');
-          getList();
-        })
-        .catch(() => {
-          proxy.$modal.msgError('鍒犻櫎澶辫触');
-        });
-    })
-    .catch(() => { });
-};
-
-// 鎵归噺鍒犻櫎
-const handleBatchDelete = () => {
-  if (!selectedRows.value.length) {
-    proxy.$modal.msgWarning('璇烽�夋嫨鏁版嵁');
-    return;
-  }
-  const ids = selectedRows.value.map(item => item.id);
-  ElMessageBox.confirm('閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�', '鍒犻櫎鎻愮ず', {
-    confirmButtonText: '纭',
-    cancelButtonText: '鍙栨秷',
-    type: 'warning'
-  })
-    .then(() => {
-      batchDelete(ids)
-        .then(() => {
-          proxy.$modal.msgSuccess('鍒犻櫎鎴愬姛');
-          getList();
-        })
-        .catch(() => {
-          proxy.$modal.msgError('鍒犻櫎澶辫触');
-        });
-    })
-    .catch(() => { });
-};
-
-// 浜у搧閫夋嫨
-const handleProductSelect = (products) => {
-  if (products && products.length > 0) {
-    const product = products[0];
-    form.value.productModelId = product.id;
-    form.value.productName = product.productName;
-    form.value.productModelName = product.model;
-  }
-  showProductSelectDialog.value = false;
-};
-
-// 鎻愪氦琛ㄥ崟
-const handleSubmit = () => {
-  formRef.value.validate((valid) => {
-    if (valid) {
-      const payload = { ...form.value };
-      if (operationType.value === 'add') {
-        add(payload)
-          .then(() => {
-            proxy.$modal.msgSuccess('鏂板鎴愬姛');
-            closeDialog();
-            getList();
-          })
-          .catch(() => {
-            proxy.$modal.msgError('鏂板澶辫触');
-          });
-      } else {
-        update(payload)
-          .then(() => {
-            proxy.$modal.msgSuccess('淇敼鎴愬姛');
-            closeDialog();
-            getList();
-          })
-          .catch(() => {
-            proxy.$modal.msgError('淇敼澶辫触');
-          });
-      }
-    }
+  const data = reactive({
+    form: {
+      id: undefined,
+      productName: "",
+      productModelName: "",
+      productModelId: "",
+      remark: "",
+      version: "",
+    },
+    rules: {
+      productModelId: [
+        { required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" },
+      ],
+      version: [{ required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur" }],
+    },
   });
-};
 
-// 鍏抽棴寮圭獥
-const closeDialog = () => {
-  dialogVisible.value = false;
-  formRef.value?.resetFields();
-};
+  const { form, rules } = toRefs(data);
 
-//  瀵煎叆鎸夐挳鎿嶄綔
-const handleImport = () => {
-  upload.title = "BOM瀵煎叆";
-  upload.open = true;
-};
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
 
-// 鍏抽棴瀵煎叆瀵硅瘽妗嗘椂娓呴櫎鏂囦欢
-const handleImportClose = () => {
-  proxy.$refs["uploadRef"].clearFiles();
-};
-
-//  鏂囦欢涓婁紶涓鐞�
-const handleFileUploadProgress = (event, file, fileList) => {
-  upload.isUploading = true;
-};
-
-//  鏂囦欢涓婁紶鎴愬姛澶勭悊
-const handleFileSuccess = (response, file, fileList) => {
-  upload.open = false;
-  upload.isUploading = false;
-  proxy.$refs["uploadRef"].clearFiles();
-  if (response.code === 200) {
-    proxy.$modal.msgSuccess(response.msg || "瀵煎叆鎴愬姛");
+  // 鍒嗛〉
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
     getList();
-  } else {
-    proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "瀵煎叆缁撴灉", { dangerouslyUseHTMLString: true });
-  }
-};
+  };
 
-// 鎻愪氦涓婁紶鏂囦欢
-const submitFileForm = () => {
-  proxy.$refs["uploadRef"].submit();
-};
+  // 鏌ヨ鍒楄〃
+  const getList = () => {
+    tableLoading.value = true;
+    listPage({
+      current: page.current,
+      size: page.size,
+    })
+      .then(res => {
+        const records = res?.data?.records || [];
+        tableData.value = records;
+        page.total = res?.data?.total || 0;
+      })
+      .catch(err => {
+        console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+      })
+      .finally(() => {
+        tableLoading.value = false;
+      });
+  };
 
-//  瀵煎嚭鎸夐挳鎿嶄綔
-const handleExport = () => {
-  if (selectedRows.value.length !== 1) {
-    proxy.$modal.msgWarning("璇烽�夋嫨涓�鏉℃暟鎹繘琛屽鍑�");
-    return;
-  }
+  // 鏂板
+  const handleAdd = () => {
+    operationType.value = "add";
+    Object.assign(form.value, {
+      id: undefined,
+      productName: "",
+      productModelName: "",
+      productModelId: "",
+      remark: "",
+      version: "",
+    });
+    dialogVisible.value = true;
+  };
+  const handleCopy = row => {
+    // handleAdd(row);
+    ElMessageBox.confirm("纭澶嶅埗璇OM锛�", "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        copy({
+          id: row.id,
+        })
+          .then(() => {
+            proxy.$modal.msgSuccess("澶嶅埗鎴愬姛");
+            getList();
+          })
+          .catch(() => {
+            proxy.$modal.msgError("澶嶅埗澶辫触");
+          });
+      })
+      .catch(() => {});
+  };
 
-  const bomId = selectedRows.value[0].id;
-  const fileName = `BOM_${selectedRows.value[0].bomNo || bomId}.xlsx`;
+  // 缂栬緫
+  const handleEdit = row => {
+    operationType.value = "edit";
+    Object.assign(form.value, {
+      id: row.id,
+      productName: row.productName || "",
+      productModelName: row.productModelName || "",
+      productModelId: row.productModelId || "",
+      remark: row.remark || "",
+      version: row.version || "",
+    });
+    dialogVisible.value = true;
+  };
 
-  exportBom(bomId).then(res => {
-    // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
-    if (!res) {
-      proxy.$modal.msgError("瀵煎嚭澶辫触锛岃繑鍥炴暟鎹负绌�");
+  // 鍒犻櫎锛堝崟鏉★級
+  const handleDelete = row => {
+    ElMessageBox.confirm("纭鍒犻櫎璇OM锛�", "鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        batchDelete([row.id])
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {
+            proxy.$modal.msgError("鍒犻櫎澶辫触");
+          });
+      })
+      .catch(() => {});
+  };
+
+  // 鎵归噺鍒犻櫎
+  const handleBatchDelete = () => {
+    if (!selectedRows.value.length) {
+      proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+      return;
+    }
+    const ids = selectedRows.value.map(item => item.id);
+    ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(() => {
+        batchDelete(ids)
+          .then(() => {
+            proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+            getList();
+          })
+          .catch(() => {
+            proxy.$modal.msgError("鍒犻櫎澶辫触");
+          });
+      })
+      .catch(() => {});
+  };
+
+  // 浜у搧閫夋嫨
+  const handleProductSelect = products => {
+    if (products && products.length > 0) {
+      const product = products[0];
+      form.value.productModelId = product.id;
+      form.value.productName = product.productName;
+      form.value.productModelName = product.model;
+    }
+    showProductSelectDialog.value = false;
+  };
+
+  // 鎻愪氦琛ㄥ崟
+  const handleSubmit = () => {
+    formRef.value.validate(valid => {
+      if (valid) {
+        const payload = { ...form.value };
+        if (operationType.value === "add") {
+          add(payload)
+            .then(() => {
+              proxy.$modal.msgSuccess("鏂板鎴愬姛");
+              closeDialog();
+              getList();
+            })
+            .catch(() => {
+              proxy.$modal.msgError("鏂板澶辫触");
+            });
+        } else {
+          update(payload)
+            .then(() => {
+              proxy.$modal.msgSuccess("淇敼鎴愬姛");
+              closeDialog();
+              getList();
+            })
+            .catch(() => {
+              proxy.$modal.msgError("淇敼澶辫触");
+            });
+        }
+      }
+    });
+  };
+
+  // 鍏抽棴寮圭獥
+  const closeDialog = () => {
+    dialogVisible.value = false;
+    formRef.value?.resetFields();
+  };
+
+  //  瀵煎叆鎸夐挳鎿嶄綔
+  const handleImport = () => {
+    upload.title = "BOM瀵煎叆";
+    upload.open = true;
+  };
+
+  // 鍏抽棴瀵煎叆瀵硅瘽妗嗘椂娓呴櫎鏂囦欢
+  const handleImportClose = () => {
+    proxy.$refs["uploadRef"].clearFiles();
+  };
+
+  //  鏂囦欢涓婁紶涓鐞�
+  const handleFileUploadProgress = (event, file, fileList) => {
+    upload.isUploading = true;
+  };
+
+  //  鏂囦欢涓婁紶鎴愬姛澶勭悊
+  const handleFileSuccess = (response, file, fileList) => {
+    upload.open = false;
+    upload.isUploading = false;
+    proxy.$refs["uploadRef"].clearFiles();
+    if (response.code === 200) {
+      proxy.$modal.msgSuccess(response.msg || "瀵煎叆鎴愬姛");
+      getList();
+    } else {
+      proxy.$alert(
+        "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
+          response.msg +
+          "</div>",
+        "瀵煎叆缁撴灉",
+        { dangerouslyUseHTMLString: true }
+      );
+    }
+  };
+
+  // 鎻愪氦涓婁紶鏂囦欢
+  const submitFileForm = () => {
+    proxy.$refs["uploadRef"].submit();
+  };
+
+  //  瀵煎嚭鎸夐挳鎿嶄綔
+  const handleExport = () => {
+    if (selectedRows.value.length !== 1) {
+      proxy.$modal.msgWarning("璇烽�夋嫨涓�鏉℃暟鎹繘琛屽鍑�");
       return;
     }
 
-    const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
-    const downloadElement = document.createElement('a');
+    const bomId = selectedRows.value[0].id;
+    const fileName = `BOM_${selectedRows.value[0].bomNo || bomId}.xlsx`;
+
+    exportBom(bomId)
+      .then(res => {
+        // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+        if (!res) {
+          proxy.$modal.msgError("瀵煎嚭澶辫触锛岃繑鍥炴暟鎹负绌�");
+          return;
+        }
+
+        const blob = new Blob([res], {
+          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+        });
+        const downloadElement = document.createElement("a");
+        const href = window.URL.createObjectURL(blob);
+
+        downloadElement.style.display = "none";
+        downloadElement.href = href;
+        downloadElement.download = fileName;
+
+        document.body.appendChild(downloadElement);
+        downloadElement.click();
+
+        document.body.removeChild(downloadElement);
+        window.URL.revokeObjectURL(href);
+
+        proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+      })
+      .catch(err => {
+        console.error("瀵煎嚭寮傚父锛�", err);
+        proxy.$modal.msgError("绯荤粺寮傚父锛屽鍑哄け璐�");
+      });
+  };
+
+  //  涓嬭浇妯℃澘
+  const handleDownloadTemplate = async () => {
+    const res = await downloadTemplate();
+    // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+    if (!res) {
+      proxy.$modal.msgError("涓嬭浇澶辫触锛岃繑鍥炴暟鎹负绌�");
+      return;
+    }
+
+    const blob = new Blob([res], {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    });
+    const downloadElement = document.createElement("a");
     const href = window.URL.createObjectURL(blob);
 
-    downloadElement.style.display = 'none';
     downloadElement.href = href;
-    downloadElement.download = fileName;
+    downloadElement.download = "BOM妯℃澘.xlsx";
 
     document.body.appendChild(downloadElement);
     downloadElement.click();
@@ -384,52 +516,23 @@
     document.body.removeChild(downloadElement);
     window.URL.revokeObjectURL(href);
 
-    proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
-  }).catch(err => {
-    console.error("瀵煎嚭寮傚父锛�", err);
-    proxy.$modal.msgError("绯荤粺寮傚父锛屽鍑哄け璐�");
+    proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
+  };
+
+  // 鏌ョ湅璇︽儏
+  const showDetail = row => {
+    router.push({
+      path: "/productionManagement/productStructureDetail",
+      query: {
+        id: row.id,
+        bomNo: row.bomNo || "",
+        productName: row.productName || "",
+        productModelName: row.productModelName || "",
+      },
+    });
+  };
+
+  onMounted(() => {
+    getList();
   });
-};
-
-//  涓嬭浇妯℃澘
-const handleDownloadTemplate = async () => {
-  const res = await downloadTemplate();
-  // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
-  if (!res) {
-    proxy.$modal.msgError("涓嬭浇澶辫触锛岃繑鍥炴暟鎹负绌�");
-    return;
-  }
-
-  const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
-  const downloadElement = document.createElement('a');
-  const href = window.URL.createObjectURL(blob);
-
-  downloadElement.href = href;
-  downloadElement.download = "BOM妯℃澘.xlsx";
-
-  document.body.appendChild(downloadElement);
-  downloadElement.click();
-
-  document.body.removeChild(downloadElement);
-  window.URL.revokeObjectURL(href);
-
-  proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
-};
-
-// 鏌ョ湅璇︽儏
-const showDetail = (row) => {
-  router.push({
-    path: '/productionManagement/productStructureDetail',
-    query: {
-      id: row.id,
-      bomNo: row.bomNo || '',
-      productName: row.productName || '',
-      productModelName: row.productModelName || ''
-    }
-  });
-};
-
-onMounted(() => {
-  getList();
-});
 </script>

--
Gitblit v1.9.3