From 2c55b1a8a0700df79268550335506637b41610ce Mon Sep 17 00:00:00 2001
From: seatonwan9
Date: 星期日, 24 八月 2025 20:36:36 +0800
Subject: [PATCH] 提交源码

---
 src/views/tradeManage/upload/index.vue |  871 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 751 insertions(+), 120 deletions(-)

diff --git a/src/views/tradeManage/upload/index.vue b/src/views/tradeManage/upload/index.vue
index 344e497..eceaea1 100644
--- a/src/views/tradeManage/upload/index.vue
+++ b/src/views/tradeManage/upload/index.vue
@@ -163,6 +163,7 @@
           :on-exceed="onExceed"
           :on-remove="handleRemove"
           :show-file-list="false"
+          :before-upload="beforeUpload"
         >
           <el-button type="primary">閫夋嫨鏂囦欢</el-button>
         </el-upload>
@@ -190,16 +191,47 @@
               {{ formatFileSize(row.size) }}
             </template>
           </el-table-column>
-          <el-table-column label="鎿嶄綔" width="100">
+          <el-table-column label="鎿嶄綔" width="280">
             <template #default="{ row, $index }">
-              <el-button
-                type="text"
-                size="small"
-                class="delete-btn"
-                @click="handleRemove(row, fileList)"
-              >
-                鍒犻櫎
-              </el-button>
+              <div class="file-actions">
+                <el-button
+                  type="text"
+                  size="small"
+                  class="upload-btn"
+                  @click="handleUpload(row)"
+                  v-if="!row.status || row.status === 'ready'"
+                  :loading="row.uploading"
+                >
+                  {{ row.uploading ? '涓婁紶涓�' : '涓婁紶' }}
+                </el-button>
+                <el-button
+                  type="text"
+                  size="small"
+                  class="preview-btn"
+                  @click="handlePreview(row)"
+                  v-if="isPreviewable(row)"
+                  :disabled="!isFileUploaded(row)"
+                >
+                  棰勮
+                </el-button>
+                <el-button
+                  type="text"
+                  size="small"
+                  class="download-btn"
+                  @click="handleDownload(row)"
+                  :disabled="!isFileUploaded(row)"
+                >
+                  涓嬭浇
+                </el-button>
+                <el-button
+                  type="text"
+                  size="small"
+                  class="delete-btn"
+                  @click="handleRemove(row, fileList)"
+                >
+                  鍒犻櫎
+                </el-button>
+              </div>
             </template>
           </el-table-column>
         </el-table>
@@ -218,11 +250,15 @@
 import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { Document, User, Goods, List } from '@element-plus/icons-vue'
-import { fetchOrderDetail, uploadTradeFile } from '@/api/tradeManage'
-import { ElMessage } from 'element-plus'
+import { uploadTradeFile } from '@/api/tradeManage'
+import orderApi from '@/api/orderApi'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserInfo } from '@/stores/modules/userInfo'
+import createAxios from '@/utils/axios'
 
 const route = useRoute()
 const router = useRouter()
+const userStore = useUserInfo()
 const detail = reactive<any>({ items: [] })
 const fileList = ref<any[]>([])
 const orderTableWrapRef = ref<HTMLElement | null>(null)
@@ -247,116 +283,319 @@
   return [...detail.items, summaryRow]
 })
 
+// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛�
+const statusServerToUi: Record<string, string> = {
+  '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD',
+  '寰呮巿鏉�': 'WAIT_AUTHORIZE',
+  '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM',
+  '宸插畬鎴�': 'COMPLETED',
+  '宸茶瘎浠�': 'EVALUATED',
+}
+
+const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '')
+
+const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => {
+  if (!val) return 'currency'
+  const s = String(val)
+  if (/(绉垎|points)/i.test(s)) return 'points'
+  if (/(鍗忚|agreement)/i.test(s)) return 'agreement'
+  if (/(鍏嶈垂|free)/i.test(s)) return 'free'
+  return 'currency'
+}
+
 onMounted(async () => {
-  // 浣跨敤鍓嶇妯℃嫙鏁版嵁浠ヤ究寮�鍙� UI锛堜笉鏀瑰姩鍚庣鏈嶅姟锛�
-  const mockDetail = {
-    orderNo: '4348442557619205545',
-    resourceTypeName: '杞欢浜у搧',
-    status: 'WAIT_UPLOAD',
-    statusName: '寰呬笂浼犳枃浠�',
-    applyTime: '2025-05-21 10:00:00',
-    unitName: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃',
-    userName: '寮犻潤',
-    userAccount: 'L20159922',
-    userDept: '淇℃伅涓績',
-    userPhone: '13800000000',
-    productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺',
-    supplier: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃',
-    industry: '寤虹瓚宸ョ▼',
-    projectUnit: '鍦熷缓宸ョ▼',
-    productType: '杞欢搴�',
-    productDesc:
-      '闈㈠悜宸ョ▼椤圭洰鐨勮川閲忓疄娴嬪疄閲忔暟瀛楀寲绠$悊绯荤粺锛屾敮鎸佹爣鍑嗗寲閲囬泦銆佽嚜鍔ㄧ粺璁′笌杩囩▼绠℃帶銆�',
-    items: [
-      {
-        id: '1',
-        name: '浼佷笟绉佹湁SaaS鐗堣鍙�',
-        saleType: '涔版柇',
-        accountCount: 50,
-        customerTarget: '浼佷笟',
-        concurrentNodes: 50,
-        pricePoint: 50000,
-        priceCash: 0,
-        quantity: 1,
-        period: 0,
-      },
-      {
-        id: '2',
-        name: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟',
-        saleType: 'OTA鏈嶅姟',
-        accountCount: 50,
-        customerTarget: '浼佷笟',
-        concurrentNodes: 50,
-        pricePoint: 0,
-        priceCash: 7500,
-        quantity: 1,
-        period: 1,
-      },
-      {
-        id: '3',
-        name: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘',
-        saleType: '绉佹湁澧為噺鍖�',
-        accountCount: 100,
-        customerTarget: '浼佷笟',
-        concurrentNodes: 100,
-        pricePoint: 0,
-        priceCash: 0,
-        priceProtocol: true,
-        quantity: 1,
-        period: 1,
-      },
-      {
-        id: '4',
-        name: '涓汉鍏湁SaaS鐗堣鍙�',
-        saleType: '绉佹湁澧為噺鍖�',
-        accountCount: 50,
-        customerTarget: '涓汉',
-        concurrentNodes: 50,
-        pricePoint: 0,
-        priceCash: 0,
-        quantity: 1,
-        period: 0,
-      },
-    ],
-    pointTotal: '50,000',
-    cashTotal: '7,500',
+  const orderId = String(route.params.id || '')
+  if (!orderId) {
+    ElMessage.error('璁㈠崟ID涓嶈兘涓虹┖')
+    return
   }
 
-  Object.assign(detail, mockDetail)
-  
-  // 娣诲姞妯℃嫙鏂囦欢鏁版嵁鐢ㄤ簬灞曠ず
-  fileList.value = [
-    {
-      name: '绛惧瓧鐩栫珷鏂囦欢.pdf',
-      size: 2621440, // 2.5MB
-      uid: '1',
-      status: 'success'
-    },
-    {
-      name: 'API Keys.txt',
-      size: 354, // 354 Bytes
-      uid: '2',
-      status: 'success'
+  try {
+    const res = (await orderApi.getOrderDetail(orderId)) as any
+    const data = res?.data || {}
+
+    const statusName: string = data.orderStatus || ''
+    const uiStatus = statusServerToUi[statusName] || 'INFO'
+
+    // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅
+    const head = {
+      orderNo: data.orderId,
+      resourceTypeName: '杞欢浜у搧',
+      status: uiStatus,
+      statusName,
+      applyTime: formatDateTime(data.applyTime),
+      unitName: data.unitName || '-',
+      userName: data.userName || '-',
+      userAccount: data.userAccount || '-',
+      userDept: data.userDept || '-',
+      userPhone: data.userPhone || '-',
+      productName: data.productName || '-',
+      supplier: data.providerName || '-',
+      industry: data.industry || '-',
+      projectUnit: data.projectUnit || '-',
+      productType: data.productType || '-',
+      productDesc: data.productDesc || '-',
     }
-  ]
-  
-  // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹�
-  // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any
-  // Object.assign(detail, data || {})
+
+    // 鏄庣粏椤规槧灏�
+    const items: any[] = Array.isArray(data.orderDetails)
+      ? data.orderDetails.map((d: any, idx: number) => {
+          const pt = normalizePriceType(d.priceType)
+          return {
+            id: String(d.id ?? idx + 1),
+            name: d.suiteName,
+            saleType: d.salesForm,
+            accountCount: d.accountLimit,
+            customerTarget: d.customerType,
+            concurrentNodes: d.concurrentNodes,
+            pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0,
+            priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0,
+            priceProtocol: pt === 'agreement',
+            quantity: Number(d.quantity || 0),
+            period: Number(d.duration || 0),
+          }
+        })
+      : []
+
+    // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛�
+    const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0)
+    const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0)
+
+    Object.assign(detail, head, {
+      items,
+      pointTotal: pointTotalNum.toLocaleString(),
+      cashTotal: cashTotalNum.toLocaleString(),
+    })
+
+    // 濡傛灉鏈夊凡涓婁紶鐨勬枃浠讹紝鏄剧ず鍦ㄦ枃浠跺垪琛ㄤ腑
+    if (data.attachments && Array.isArray(data.attachments)) {
+      fileList.value = data.attachments.map((file: any) => ({
+        name: file.fileName,
+        size: file.fileSize,
+        uid: file.id,
+        status: 'success',
+        url: file.fileUrl
+      }))
+    }
+  } catch (error) {
+    console.error('鑾峰彇璁㈠崟璇︽儏澶辫触:', error)
+    ElMessage.error('鑾峰彇璁㈠崟璇︽儏澶辫触')
+  }
 })
 
 const onExceed = () => ElMessage.warning('鏈�澶氶�夋嫨5涓枃浠�')
+
+// 鏂囦欢涓婁紶鍓嶇殑楠岃瘉
+const beforeUpload = (file: File) => {
+  // 妫�鏌ユ枃浠跺ぇ灏忥紙闄愬埗涓�100MB锛�
+  const maxSize = 100 * 1024 * 1024
+  if (file.size > maxSize) {
+    ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃100MB')
+    return false
+  }
+  
+  // 妫�鏌ユ枃浠跺悕闀垮害
+  if (file.name.length > 100) {
+    ElMessage.error('鏂囦欢鍚嶇О涓嶈兘瓒呰繃100瀛楃')
+    return false
+  }
+  
+  // 璁剧疆鏂囦欢鍒濆鐘舵��
+  const fileObj = {
+    name: file.name,
+    size: file.size,
+    type: file.type,
+    raw: file,
+    status: 'ready', // 鍒濆鐘舵�佷负鍑嗗涓婁紶
+    uploading: false,
+    uploaded: false
+  }
+  
+  // 灏嗘枃浠舵坊鍔犲埌鏂囦欢鍒楄〃
+  fileList.value.push(fileObj)
+  
+  return false // 闃绘鑷姩涓婁紶锛屾敼涓烘墜鍔ㄤ笂浼�
+}
+
+// 涓婁紶鍗曚釜鏂囦欢鍒版湇鍔″櫒
+const uploadSingleFile = async (file: File) => {
+  const formData = new FormData()
+  formData.append('file', file)
+  formData.append('folder', 'order-attachments')
+  
+  try {
+    console.log('寮�濮嬩笂浼犳枃浠�:', file.name, '澶у皬:', file.size)
+    
+    const response = await createAxios({
+      url: '/admin/file/upload',
+      method: 'POST',
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      },
+      data: formData
+    })
+    
+    console.log('鏂囦欢涓婁紶鍝嶅簲:', response)
+    
+    // 妫�鏌ュ搷搴旀牸寮� - 鏍规嵁瀹為檯杩斿洖鏍煎紡璋冩暣
+    const responseData = response as any
+    
+    // 鏍规嵁瀹為檯鍝嶅簲鏍煎紡锛岀洿鎺ユ鏌� responseData.code
+    if (responseData && responseData.code === 200) {
+      console.log('鏂囦欢涓婁紶鎴愬姛锛岃繑鍥炴暟鎹�:', responseData.data)
+      return responseData.data // 杩斿洖鏂囦欢URL
+    } else if (responseData && responseData.data && responseData.data.code === 200) {
+      // 澶囩敤妫�鏌ワ細濡傛灉鍝嶅簲琚寘瑁呭湪 data 涓�
+      console.log('鏂囦欢涓婁紶鎴愬姛锛岃繑鍥炴暟鎹�:', responseData.data.data)
+      return responseData.data.data // 杩斿洖鏂囦欢URL
+    } else {
+      // 澶勭悊閿欒鎯呭喌
+      const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '鏂囦欢涓婁紶澶辫触'
+      console.error('鏂囦欢涓婁紶澶辫触锛岄敊璇俊鎭�:', errorMsg)
+      throw new Error(errorMsg)
+    }
+  } catch (error) {
+    console.error('鏂囦欢涓婁紶寮傚父:', error)
+    throw error
+  }
+}
+
+// 淇濆瓨鏂囦欢淇℃伅鍒版暟鎹簱
+const saveFileInfo = async (fileData: any) => {
+  try {
+    console.log('寮�濮嬩繚瀛樻枃浠朵俊鎭�:', fileData)
+    
+    // 浣跨敤FormData鏍煎紡锛屽洜涓哄悗绔娇鐢ˊRequestParam鎺ユ敹鍙傛暟
+    const formData = new FormData()
+    formData.append('orderId', fileData.orderId)
+    formData.append('fileName', fileData.fileName)
+    formData.append('originalName', fileData.originalName)
+    formData.append('fileType', fileData.fileType)
+    formData.append('fileSize', fileData.fileSize.toString())
+    formData.append('fileUrl', fileData.fileUrl)
+    formData.append('bucketName', fileData.bucketName)
+    formData.append('objectName', fileData.objectName)
+    formData.append('uploadUserId', fileData.uploadUserId.toString())
+    formData.append('uploadUserName', fileData.uploadUserName)
+    formData.append('attachmentType', fileData.attachmentType)
+    formData.append('description', fileData.description)
+    
+    console.log('鍑嗗鍙戦�佺殑鏂囦欢淇℃伅:', {
+      orderId: fileData.orderId,
+      fileName: fileData.fileName,
+      fileSize: fileData.fileSize,
+      fileUrl: fileData.fileUrl,
+      uploadUserId: fileData.uploadUserId,
+      uploadUserName: fileData.uploadUserName
+    })
+    
+    const response = await createAxios({
+      url: '/admin/api/order/attachment/upload',
+      method: 'POST',
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      },
+      data: formData
+    })
+    
+    console.log('淇濆瓨鏂囦欢淇℃伅鍝嶅簲:', response)
+    
+    // 浣跨敤涓庢枃浠朵笂浼犵浉鍚岀殑鍝嶅簲鍒ゆ柇閫昏緫
+    const responseData = response as any
+    
+    if (responseData && responseData.code === 200) {
+      console.log('鏂囦欢淇℃伅淇濆瓨鎴愬姛锛岃繑鍥炵殑闄勪欢ID:', responseData.data)
+      const attachmentId = responseData.data
+      
+      // 楠岃瘉attachmentId鏄惁涓烘湁鏁堢殑鏁板瓧
+      if (typeof attachmentId === 'number' && attachmentId > 0) {
+        return attachmentId
+      } else {
+        console.error('杩斿洖鐨勯檮浠禝D涓嶆槸鏈夋晥鏁板瓧:', attachmentId, typeof attachmentId)
+        throw new Error(`鏃犳晥鐨勯檮浠禝D: ${attachmentId}`)
+      }
+    } else if (responseData && responseData.data && responseData.data.code === 200) {
+      // 澶囩敤妫�鏌ワ細濡傛灉鍝嶅簲琚寘瑁呭湪 data 涓�
+      console.log('鏂囦欢淇℃伅淇濆瓨鎴愬姛锛堝鐢ㄦ鏌ワ級锛岃繑鍥炵殑闄勪欢ID:', responseData.data.data)
+      const attachmentId = responseData.data.data
+      
+      // 楠岃瘉attachmentId鏄惁涓烘湁鏁堢殑鏁板瓧
+      if (typeof attachmentId === 'number' && attachmentId > 0) {
+        return attachmentId
+      } else {
+        console.error('杩斿洖鐨勯檮浠禝D涓嶆槸鏈夋晥鏁板瓧锛堝鐢ㄦ鏌ワ級:', attachmentId, typeof attachmentId)
+        throw new Error(`鏃犳晥鐨勯檮浠禝D: ${attachmentId}`)
+      }
+    } else {
+      // 澶勭悊閿欒鎯呭喌
+      const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '淇濆瓨鏂囦欢淇℃伅澶辫触'
+      console.error('鏂囦欢淇℃伅淇濆瓨澶辫触:', errorMsg)
+      throw new Error(errorMsg)
+    }
+  } catch (error) {
+    console.error('淇濆瓨鏂囦欢淇℃伅寮傚父:', error)
+    throw error
+  }
+}
+
+
+
 const goBack = () => router.back()
+
+// 鎻愪氦鏂囦欢骞舵洿鏂拌鍗曠姸鎬�
 const submit = async () => {
-  // 妯℃嫙鎻愪氦鎴愬姛鍝嶅簲
-  const mockResponse = { code: 200 }
-  
-  // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹�
-  // const { code } = (await uploadTradeFile({ id: route.params.id, files: fileList.value })) as any
-  
-  if (mockResponse.code === 200) {
+  if (fileList.value.length === 0) {
+    ElMessage.warning('璇疯嚦灏戜笂浼犱竴涓枃浠�')
+    return
+  }
+
+  // 妫�鏌ユ槸鍚︽湁鏈笂浼犵殑鏂囦欢
+  const unuploadedFiles = fileList.value.filter(file => !file.uploaded && !file.url)
+  if (unuploadedFiles.length > 0) {
+    ElMessage.warning('璇峰厛涓婁紶鎵�鏈夋枃浠�')
+    return
+  }
+
+  try {
+    const orderId = String(route.params.id || '')
+    const userId = userStore.getUserId
+    const userName = userStore.username || userStore.name || '鏈煡鐢ㄦ埛'
+
+    // 涓婁紶鎵�鏈夋湭涓婁紶鐨勬枃浠�
+    const uploadPromises = fileList.value
+      .filter(fileItem => fileItem.raw && !fileItem.uploaded)
+      .map(async (fileItem) => {
+        const fileUrl = await uploadSingleFile(fileItem.raw)
+        
+        // 淇濆瓨鏂囦欢淇℃伅鍒版暟鎹簱
+        const attachmentData = {
+          orderId: orderId,
+          fileName: fileItem.name,
+          originalName: fileItem.name,
+          fileType: fileItem.type || 'application/octet-stream',
+          fileSize: fileItem.size,
+          fileUrl: fileUrl,
+          bucketName: 'order-attachments',
+          objectName: fileUrl.split('/').pop(),
+          uploadUserId: userId,
+          uploadUserName: userName,
+          attachmentType: 'TRADE_FILE',
+          description: '浜ゆ槗鏂囦欢'
+        }
+
+        await saveFileInfo(attachmentData)
+      })
+
+    await Promise.all(uploadPromises)
+
+    // 鏇存柊璁㈠崟鐘舵�佽繘鍏ヤ笅涓�涓姸鎬�
+    await orderApi.updateOrderStatusToNext(orderId)
+
     ElMessage.success('鎻愪氦鎴愬姛')
     router.back()
+  } catch (error) {
+    console.error('鎻愪氦澶辫触:', error)
+    ElMessage.error(error instanceof Error ? error.message : '鎻愪氦澶辫触')
   }
 }
 
@@ -456,13 +695,365 @@
 // 鏂囦欢鍒楄〃琛ㄦ牸琛ㄤ綋鏂囧瓧澶у皬
 const fileTableCellStyle: CSSProperties = { fontSize: '12px' };
 
+// 鍒ゆ柇鏂囦欢鏄惁鍙瑙�
+const isPreviewable = (file: any) => {
+  // 棣栧厛妫�鏌IME绫诲瀷
+  const previewableTypes = [
+    'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp',
+    'text/plain', 'text/html', 'text/css', 'text/javascript',
+    'application/pdf',
+    'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
+    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
+    'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
+  ]
+  
+  // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue
+  if (previewableTypes.includes(file.type || '')) {
+    return true
+  }
+  
+  // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇
+  const fileName = file.name || ''
+  const fileExtension = fileName.toLowerCase().split('.').pop()
+  
+  const previewableExtensions = [
+    'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp',
+    'txt', 'html', 'htm', 'css', 'js',
+    'pdf',
+    'docx', 'xlsx', 'pptx'
+  ]
+  
+  return previewableExtensions.includes(fileExtension)
+}
+
+// 鏂囦欢棰勮
+const handlePreview = async (file: any) => {
+  if (!file.url) {
+    ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�')
+    return
+  }
+  
+  // 鑾峰彇鏂囦欢鎵╁睍鍚�
+  const fileName = file.name || ''
+  const fileExtension = fileName.toLowerCase().split('.').pop()
+  
+  let previewUrl = file.url
+  
+  // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL
+  if (file.url.includes('order-attachments')) {
+    try {
+      // 棣栧厛灏濊瘯鑾峰彇棰勮URL
+      const previewResponse = await createAxios({
+        url: `/admin/file/preview`,
+        method: 'GET',
+        params: {
+          fileName: file.url
+        }
+      })
+      
+      console.log('棰勮URL鍝嶅簲:', previewResponse)
+      
+      // 妫�鏌ュ搷搴旀牸寮�
+      const responseData = previewResponse as any
+      if (responseData && responseData.code === 200 && responseData.data) {
+        previewUrl = responseData.data
+        console.log('浣跨敤棰勮URL:', previewUrl)
+      } else {
+        console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�')
+        // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮�
+        const response = await createAxios({
+          url: `/admin/file/download`,
+          method: 'GET',
+          responseType: 'blob',
+          params: {
+            fileName: file.url,
+            originalName: file.name
+          }
+        })
+        
+        // 鍒涘缓棰勮URL
+        const blob = new Blob([response as any])
+        previewUrl = window.URL.createObjectURL(blob)
+        console.log('浣跨敤Blob URL:', previewUrl)
+      }
+    } catch (error) {
+      console.error('鑾峰彇鏂囦欢澶辫触:', error)
+      ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�')
+      return
+    }
+  }
+  
+  // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑
+  if ((file.type && file.type.startsWith('image/')) || 
+      ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) {
+    console.log('棰勮鍥剧墖鏂囦欢:', previewUrl)
+    window.open(previewUrl, '_blank')
+    return
+  }
+  
+  // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑
+  if (file.type === 'application/pdf' || fileExtension === 'pdf') {
+    console.log('棰勮PDF鏂囦欢:', previewUrl)
+    window.open(previewUrl, '_blank')
+    return
+  }
+  
+  // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑
+  if ((file.type && file.type.startsWith('text/')) || 
+      ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) {
+    console.log('棰勮鏂囨湰鏂囦欢:', previewUrl)
+    window.open(previewUrl, '_blank')
+    return
+  }
+  
+  // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑
+  try {
+    console.log('棰勮鍏朵粬鏂囦欢:', previewUrl)
+    window.open(previewUrl, '_blank')
+  } catch (error) {
+    console.error('棰勮澶辫触:', error)
+    ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�')
+  }
+}
+
+// 鍒ゆ柇鏂囦欢鏄惁宸蹭笂浼犳垚鍔�
+const isFileUploaded = (file: any) => {
+  // 鏂囦欢鏈塽rl涓旂姸鎬佷负success琛ㄧず宸蹭笂浼犳垚鍔�
+  return file.url && (file.status === 'success' || file.uploaded)
+}
+
+// 鍗曚釜鏂囦欢涓婁紶
+const handleUpload = async (file: any) => {
+  if (!file.raw) {
+    ElMessage.warning('鏂囦欢鏁版嵁涓嶅瓨鍦�')
+    return
+  }
+
+  // 璁剧疆涓婁紶鐘舵��
+  file.uploading = true
+  
+  try {
+    // 涓婁紶鏂囦欢鍒版湇鍔″櫒
+    const fileUrl = await uploadSingleFile(file.raw)
+    
+    // 淇濆瓨鏂囦欢淇℃伅鍒版暟鎹簱
+    const orderId = String(route.params.id || '')
+    const userId = userStore.getUserId
+    const userName = userStore.username || userStore.name || '鏈煡鐢ㄦ埛'
+    
+    const attachmentData = {
+      orderId: orderId,
+      fileName: file.name,
+      originalName: file.name,
+      fileType: file.type || 'application/octet-stream',
+      fileSize: file.size,
+      fileUrl: fileUrl,
+      bucketName: 'order-attachments',
+      objectName: fileUrl.split('/').pop(),
+      uploadUserId: userId,
+      uploadUserName: userName,
+      attachmentType: 'TRADE_FILE',
+      description: '浜ゆ槗鏂囦欢'
+    }
+
+    const attachmentId = await saveFileInfo(attachmentData)
+    
+    // 鏇存柊鏂囦欢鐘舵��
+    file.url = fileUrl
+    file.uid = attachmentId // 璁剧疆姝g‘鐨勯檮浠禝D
+    file.status = 'success'
+    file.uploaded = true
+    file.uploading = false
+    
+    ElMessage.success('鏂囦欢涓婁紶鎴愬姛')
+  } catch (error) {
+    console.error('鏂囦欢涓婁紶澶辫触:', error)
+    file.uploading = false
+    ElMessage.error(error instanceof Error ? error.message : '鏂囦欢涓婁紶澶辫触')
+  }
+}
+
+// 鏂囦欢涓嬭浇
+const handleDownload = async (file: any) => {
+  if (!file.url) {
+    ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�')
+    return
+  }
+  
+  console.log('寮�濮嬩笅杞芥枃浠�:', file.name, 'URL:', file.url)
+  
+  try {
+    // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI
+    if (file.url.includes('order-attachments')) {
+      console.log('浣跨敤MinIO涓嬭浇API')
+      
+      // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API
+      const response = await createAxios({
+        url: `/admin/file/download`,
+        method: 'GET',
+        responseType: 'blob',
+        params: {
+          fileName: file.url,
+          originalName: file.name
+        }
+      })
+      
+      console.log('涓嬭浇鍝嶅簲:', response)
+      
+      // 鍒涘缓涓嬭浇閾炬帴
+      const blob = new Blob([response as any])
+      const downloadUrl = window.URL.createObjectURL(blob)
+      
+      console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl)
+      
+      const link = document.createElement('a')
+      link.href = downloadUrl
+      link.download = file.name || 'download'
+      link.target = '_blank'
+      link.rel = 'noopener noreferrer'
+      
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+      
+      // 娓呯悊URL瀵硅薄
+      window.URL.revokeObjectURL(downloadUrl)
+      
+      ElMessage.success('鏂囦欢涓嬭浇鎴愬姛')
+    } else {
+      console.log('浣跨敤鐩存帴URL涓嬭浇')
+      // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL
+      const link = document.createElement('a')
+      link.href = file.url
+      link.download = file.name || 'download'
+      link.target = '_blank'
+      link.rel = 'noopener noreferrer'
+      
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+      
+      ElMessage.success('寮�濮嬩笅杞芥枃浠�')
+    }
+  } catch (error) {
+    console.error('涓嬭浇澶辫触:', error)
+    ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯')
+  }
+}
+
+// 鍒犻櫎MinIO鏂囦欢
+const deleteMinioFile = async (fileName: string) => {
+  try {
+    console.log('寮�濮嬪垹闄inIO鏂囦欢:', fileName)
+    
+    const response = await createAxios({
+      url: `/admin/file/delete`,
+      method: 'DELETE',
+      params: {
+        fileName: fileName
+      }
+    })
+    
+    console.log('鍒犻櫎MinIO鏂囦欢鍝嶅簲:', response)
+    
+    const responseData = response as any
+    if (responseData && responseData.code === 200) {
+      console.log('MinIO鏂囦欢鍒犻櫎鎴愬姛')
+      return true
+    } else {
+      const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '鍒犻櫎MinIO鏂囦欢澶辫触'
+      console.error('鍒犻櫎MinIO鏂囦欢澶辫触:', errorMsg)
+      throw new Error(errorMsg)
+    }
+  } catch (error) {
+    console.error('鍒犻櫎MinIO鏂囦欢寮傚父:', error)
+    throw error
+  }
+}
+
+// 鍒犻櫎鏁版嵁搴撻檮浠惰褰�
+const deleteAttachmentRecord = async (attachmentId: number) => {
+  try {
+    console.log('寮�濮嬪垹闄ゆ暟鎹簱闄勪欢璁板綍:', attachmentId)
+    
+    const response = await createAxios({
+      url: `/admin/api/order/attachment/delete/${attachmentId}`,
+      method: 'DELETE'
+    })
+    
+    console.log('鍒犻櫎鏁版嵁搴撻檮浠惰褰曞搷搴�:', response)
+    
+    const responseData = response as any
+    if (responseData && responseData.code === 200) {
+      console.log('鏁版嵁搴撻檮浠惰褰曞垹闄ゆ垚鍔�')
+      return true
+    } else {
+      const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '鍒犻櫎鏁版嵁搴撻檮浠惰褰曞け璐�'
+      console.error('鍒犻櫎鏁版嵁搴撻檮浠惰褰曞け璐�:', errorMsg)
+      throw new Error(errorMsg)
+    }
+  } catch (error) {
+    console.error('鍒犻櫎鏁版嵁搴撻檮浠惰褰曞紓甯�:', error)
+    throw error
+  }
+}
+
 // 鏂囦欢鍒楄〃绉婚櫎鏂囦欢
-const handleRemove = (file: any, uploadFiles: any) => {
-  // 浠庢枃浠跺垪琛ㄤ腑绉婚櫎鎸囧畾鏂囦欢
-  const index = fileList.value.findIndex(item => item.uid === file.uid)
-  if (index > -1) {
-    fileList.value.splice(index, 1)
-    ElMessage.success('鏂囦欢宸插垹闄�')
+const handleRemove = async (file: any, uploadFiles: any) => {
+  try {
+    // 鏄剧ず纭瀵硅瘽妗�
+    await ElMessageBox.confirm(
+      `纭畾瑕佸垹闄ゆ枃浠� "${file.name}" 鍚楋紵`,
+      '鍒犻櫎纭',
+      {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning',
+        center: true
+      }
+    )
+
+    // 鐢ㄦ埛纭鍚庢墽琛屽垹闄ゆ搷浣�
+    // 濡傛灉鏄凡涓婁紶鐨勬枃浠讹紙鏈塽id涓斾负鏁板瓧锛岃〃绀烘暟鎹簱璁板綍ID锛�
+    if (file.uid && !isNaN(file.uid) && file.url) {
+      console.log('鍒犻櫎宸蹭笂浼犵殑鏂囦欢:', file.name, '闄勪欢ID:', file.uid)
+      
+      // 1. 鍒犻櫎MinIO涓殑鏂囦欢
+      if (file.url.includes('order-attachments')) {
+        await deleteMinioFile(file.url)
+      }
+      
+      // 2. 鍒犻櫎鏁版嵁搴撲腑鐨勯檮浠惰褰�
+      await deleteAttachmentRecord(file.uid)
+      
+      // 3. 浠庢枃浠跺垪琛ㄤ腑绉婚櫎
+      const index = fileList.value.findIndex(item => item.uid === file.uid)
+      if (index > -1) {
+        fileList.value.splice(index, 1)
+      }
+      
+      ElMessage.success('鏂囦欢鍒犻櫎鎴愬姛')
+    } else {
+      // 濡傛灉鏄湭涓婁紶鐨勬枃浠讹紙鍙湁raw鏂囦欢瀵硅薄锛�
+      console.log('鍒犻櫎鏈笂浼犵殑鏂囦欢:', file.name)
+      
+      // 鐩存帴浠庢枃浠跺垪琛ㄤ腑绉婚櫎
+      const index = fileList.value.findIndex(item => item.name === file.name && item.size === file.size)
+      if (index > -1) {
+        fileList.value.splice(index, 1)
+      }
+      
+      ElMessage.success('鏂囦欢宸插垹闄�')
+    }
+  } catch (error) {
+    // 濡傛灉鐢ㄦ埛鍙栨秷鍒犻櫎锛宔rror涓�'cancel'锛屼笉闇�瑕佹樉绀洪敊璇秷鎭�
+    if (error === 'cancel') {
+      console.log('鐢ㄦ埛鍙栨秷鍒犻櫎鎿嶄綔')
+      return
+    }
+    
+    console.error('鍒犻櫎鏂囦欢澶辫触:', error)
+    ElMessage.error(error instanceof Error ? error.message : '鍒犻櫎鏂囦欢澶辫触')
   }
 }
 </script>
@@ -649,10 +1240,50 @@
         color: #409eff;
       }
     }
-    .delete-btn {
-      color: #f56c6c;
-      &:hover {
-        text-decoration: underline;
+    .file-actions {
+      display: flex;
+      gap: 8px;
+      align-items: center;
+      justify-content: center;
+      
+      .upload-btn {
+        color: #e6a23c;
+        &:hover {
+          text-decoration: underline;
+        }
+        &:disabled {
+          color: #c0c4cc;
+          cursor: not-allowed;
+        }
+      }
+      
+      .preview-btn {
+        color: #409eff;
+        &:hover {
+          text-decoration: underline;
+        }
+        &:disabled {
+          color: #c0c4cc;
+          cursor: not-allowed;
+        }
+      }
+      
+      .download-btn {
+        color: #67c23a;
+        &:hover {
+          text-decoration: underline;
+        }
+        &:disabled {
+          color: #c0c4cc;
+          cursor: not-allowed;
+        }
+      }
+      
+      .delete-btn {
+        color: #f56c6c;
+        &:hover {
+          text-decoration: underline;
+        }
       }
     }
   }

--
Gitblit v1.8.0