p-honggang.li
5 天以前 751dfe21d19a22bb130a6a14857470868d7be53a
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,18 @@
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'
import productApi from "@/api/productApi";
import sysUserService from "@/api/sysUser";
import {queryUserDetail} from "@/api/userInfo";
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 +286,369 @@
  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版OTA升级服务',
        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'
  // 获取用户信息
  if (!userStore.getUserId) {
    try {
      const res: any = await queryUserDetail()
      if (res?.code === 200 && res.data) {
        userStore.updateUserDetail(res.data)
      } else {
        ElMessage.error(res?.msg || '无法获取用户信息,请先登录')
        return
      }
    } catch (e) {
      console.error('获取用户详情失败:', e)
      ElMessage.error('获取用户信息失败,请稍后重试')
      return
    }
  ]
  // 注释掉原有的API调用,使用模拟数据
  // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any
  // Object.assign(detail, data || {})
  }
  try {
    const res = (await orderApi.getOrderDetail(orderId)) as any
    const data = res?.data || {}
    const statusName: string = data.orderStatus || ''
    const uiStatus = statusServerToUi[statusName] || 'INFO'
    // 根据产品id获取产品信息,更新头部展示
    try {
      if (data.productId) {
        const detailRes: any = await productApi.getProductById({ id: data.productId })
        if (detailRes?.code === 200 && detailRes.data) {
          // 用产品详情补全头信息
          data.productName = detailRes.data.name || data.productName
          data.providerName = detailRes.data.submissionUnit || data.providerName
          data.industry = detailRes.data.industrialChainName || data.industry
          data.productDesc = detailRes.data.describe || data.productDesc
          data.projectUnit = detailRes.data.importantAreaName || data.productDesc
          data.productType = detailRes.data.typeName || data.productDesc
        }
      }
    } catch (e) {
      // 忽略产品详情失败,不阻塞订单详情
    }
    // 获取用户信息
    try {
      const userRes: any = await sysUserService.getUserdetail({ userId: data.userId })
      if (userRes?.code === 200 && userRes.data) {
        // 用产品详情补全头信息
        data.unitName = userRes.data.unitName || data.unitName
        data.userName = userRes.data.name || data.userName
        data.userDept = userRes.data.departmentName || data.userDept
        data.userPhone = userRes.data.phone || data.userPhone
        data.userAccount = userRes.data.username || data.userAccount
      }
    }catch (e){
    }
    // 映射订单详情头部信息
    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 || '-',
    }
    // 明细项映射
    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 = 500 * 1024 * 1024
  if (file.size > maxSize) {
    ElMessage.error('文件大小不能超过500MB')
    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('返回的附件ID不是有效数字:', attachmentId, typeof attachmentId)
        throw new Error(`无效的附件ID: ${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('返回的附件ID不是有效数字(备用检查):', attachmentId, typeof attachmentId)
        throw new Error(`无效的附件ID: ${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: 'dev',
          objectName: fileUrl.split('/').pop(),
          uploadUserId: userId,
          uploadUserName: userName,
          attachmentType: '其他',
          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 +748,365 @@
// 文件列表表格表体文字大小
const fileTableCellStyle: CSSProperties = { fontSize: '12px' };
// 判断文件是否可预览
const isPreviewable = (file: any) => {
  // 首先检查MIME类型
  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类型匹配,直接返回true
  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
  // 如果文件存储在MinIO,优先使用预览URL
  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.replaceAll("http://192.168.20.52:9000", import.meta.env.VITE_FILE_PREVIEW_URL)
        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) => {
  // 文件有url且状态为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: 'dev',
      objectName: fileUrl.split('/').pop(),
      uploadUserId: userId,
      uploadUserName: userName,
      attachmentType: '其他',
      description: '交易文件'
    }
    const attachmentId = await saveFileInfo(attachmentData)
    // 更新文件状态
    file.url = fileUrl
    file.uid = attachmentId // 设置正确的附件ID
    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 {
    // 如果文件存储在MinIO,使用后端直接下载API
    if (file.url.includes('order-attachments')) {
      console.log('使用MinIO下载API')
      // 使用axios通过代理访问后端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下载')
      // 其他情况直接使用原URL
      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('开始删除MinIO文件:', 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
      }
    )
    // 用户确认后执行删除操作
    // 如果是已上传的文件(有uid且为数字,表示数据库记录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) {
    // 如果用户取消删除,error为'cancel',不需要显示错误消息
    if (error === 'cancel') {
      console.log('用户取消删除操作')
      return
    }
    console.error('删除文件失败:', error)
    ElMessage.error(error instanceof Error ? error.message : '删除文件失败')
  }
}
</script>
@@ -649,10 +1293,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;
        }
      }
    }
  }