Bang Hu
1 天以前 8a709ba6db50831048f9c3e2452ea6dc6c3de36f
src/views/tradeManage/detail/index.vue
@@ -2,79 +2,104 @@
  <div class="default-main">
    <!-- 订单信息 + 申请人信息 + 交易内容(合并为同一卡片) -->
    <el-card shadow="never">
      <el-descriptions
        :column="2"
        border
        class="mt10 order-desc fixed-label"
        label-width="180px"
        :label-style="labelStyle"
        :content-style="contentStyle"
      >
        <el-descriptions-item :span="2" class="section-header">
          <template #label>
            <el-icon class="section-icon"><Document /></el-icon>
            <span>订单信息</span>
          </template>
          <template #default></template>
        </el-descriptions-item>
        <el-descriptions-item label="订单编号">{{ detail.orderNo }}</el-descriptions-item>
        <el-descriptions-item label="交易资源类型">{{ detail.resourceTypeName }}</el-descriptions-item>
        <el-descriptions-item label="申请时间">{{ detail.applyTime }}</el-descriptions-item>
        <el-descriptions-item label="交易状态">
          <el-tag :type="getStatusType(detail.status)" size="small">{{ detail.statusName }}</el-tag>
        </el-descriptions-item>
      </el-descriptions>
      <!-- 申请人信息(与订单信息同卡片,复用分隔标题样式) -->
      <el-descriptions
        :column="2"
        border
        class="mt15 order-desc fixed-label"
        label-width="180px"
        :label-style="labelStyle"
        :content-style="contentStyle"
      >
        <el-descriptions-item :span="2" class="section-header">
          <template #label>
            <el-icon class="section-icon"><User /></el-icon>
            <span>申请人信息</span>
          </template>
          <template #default></template>
        </el-descriptions-item>
        <el-descriptions-item label="姓名">{{ detail.userName || '-' }}</el-descriptions-item>
        <el-descriptions-item label="单位">{{ detail.unitName || '-' }}</el-descriptions-item>
        <el-descriptions-item label="部门">{{ detail.userDept || '-' }}</el-descriptions-item>
        <el-descriptions-item label="用户名">{{ detail.userAccount || '-' }}</el-descriptions-item>
      </el-descriptions>
      <!-- 订单信息 - 使用自定义表格布局 -->
      <div class="order-info-section">
        <div class="section-header">
          <el-icon class="section-icon"><Document /></el-icon>
          <span>订单信息</span>
        </div>
        <div class="order-info-grid">
          <div class="order-info-item">
            <div class="order-label">订单编号</div>
            <div class="order-content">{{ detail.orderNo }}</div>
          </div>
          <div class="order-info-item">
            <div class="order-label">交易资源类型</div>
            <div class="order-content">{{ detail.resourceTypeName }}</div>
          </div>
          <div class="order-info-item">
            <div class="order-label">申请时间</div>
            <div class="order-content">{{ detail.applyTime }}</div>
          </div>
          <div class="order-info-item">
            <div class="order-label">交易状态</div>
            <div class="order-content">
              <el-tag :type="getStatusType(detail.status)" size="small">{{ detail.statusName }}</el-tag>
            </div>
          </div>
        </div>
      </div>
      <!-- 交易内容(紧随申请人信息,同卡片,复用分隔标题样式) -->
      <el-descriptions
        :column="2"
        border
        class="mt15 order-desc fixed-label"
        label-width="180px"
        :label-style="labelStyle"
        :content-style="contentStyle"
      >
        <el-descriptions-item :span="2" class="section-header">
          <template #label>
            <el-icon class="section-icon"><Goods /></el-icon>
            <span>交易内容</span>
          </template>
          <template #default></template>
        </el-descriptions-item>
        <el-descriptions-item label="产品名称">
          <el-link type="primary" :underline="false">{{ detail.productName }}</el-link>
        </el-descriptions-item>
        <el-descriptions-item label="提供者">{{ detail.supplier }}</el-descriptions-item>
        <el-descriptions-item label="行业领域">{{ detail.industry }}</el-descriptions-item>
        <el-descriptions-item label="单位工程">{{ detail.projectUnit }}</el-descriptions-item>
        <el-descriptions-item label="产品类型">{{ detail.productType || '-' }}</el-descriptions-item>
        <el-descriptions-item label="产品简介">
          <div class="desc-wrap">{{ detail.productDesc }}</div>
        </el-descriptions-item>
      </el-descriptions>
      <!-- 申请人信息 - 使用自定义表格布局 -->
      <div class="applicant-info-section">
        <div class="section-header">
          <el-icon class="section-icon"><User /></el-icon>
          <span>申请人信息</span>
        </div>
        <div class="applicant-info-grid">
          <div class="applicant-info-item">
            <div class="applicant-label">姓名</div>
            <div class="applicant-content">{{ detail.userName || '-' }}</div>
          </div>
          <div class="applicant-info-item">
            <div class="applicant-label">单位</div>
            <div class="applicant-content">{{ detail.unitName || '-' }}</div>
          </div>
          <div class="applicant-info-item">
            <div class="applicant-label">部门</div>
            <div class="applicant-content">{{ detail.userDept || '-' }}</div>
          </div>
          <div class="applicant-info-item">
            <div class="applicant-label">用户名</div>
            <div class="applicant-content">{{ detail.userAccount || '-' }}</div>
          </div>
        </div>
      </div>
      <!-- 交易内容 - 使用自定义表格布局 -->
      <div class="transaction-content-section">
        <div class="section-header">
          <el-icon class="section-icon"><Goods /></el-icon>
          <span>交易内容</span>
        </div>
        <div class="transaction-content-grid">
          <div class="transaction-content-item">
            <div class="transaction-label">产品名称</div>
            <div class="transaction-content">
              <el-link type="primary" :underline="false">{{ detail.productName }}</el-link>
            </div>
          </div>
          <div class="transaction-content-item">
            <div class="transaction-label">提供者</div>
            <div class="transaction-content">{{ detail.supplier }}</div>
          </div>
          <div class="transaction-content-item">
            <div class="transaction-label">行业领域</div>
            <div class="transaction-content">{{ detail.industry }}</div>
          </div>
          <div class="transaction-content-item">
            <div class="transaction-label">单位工程</div>
            <div class="transaction-content">
              <el-tooltip effect="dark" :content="detail.projectUnit || '-'" placement="top" :disabled="!(detail.projectUnit && String(detail.projectUnit).trim())" popper-class="tooltip-wrap">
                <div class="ellipsis-1">{{ detail.projectUnit || '-' }}</div>
              </el-tooltip>
            </div>
          </div>
          <div class="transaction-content-item">
            <div class="transaction-label">产品类型</div>
            <div class="transaction-content">{{ detail.productType || '-' }}</div>
          </div>
          <div class="transaction-content-item">
            <div class="transaction-label">产品简介</div>
            <div class="transaction-content">
              <el-tooltip effect="dark" :content="detail.productDesc || '-'" placement="top" :disabled="!(detail.productDesc && String(detail.productDesc).trim())" popper-class="tooltip-wrap" trigger="click">
                <div class="desc-wrap ellipsis-3">{{ detail.productDesc || '-' }}</div>
              </el-tooltip>
            </div>
          </div>
        </div>
      </div>
      <!-- 订单详情(移动到交易内容下面,同一卡片内) -->
      <div ref="orderTableWrapRef">
        <el-table
@@ -143,14 +168,94 @@
      </div>
      <!-- 移除原来的表格底部信息,因为已经移到表格最后一行 -->
    </el-card>
       <!-- 交易文件(移动到订单详情下面,同一卡片内) -->
       <div class="file-section" v-if="isAgreementOrder && fileList.length > 0">
         <el-table
           :data="fileList"
           border
           class="file-table"
           :header-cell-style="fileTableHeaderStyle"
           :cell-style="fileTableCellStyle"
         >
           <el-table-column min-width="200">
             <template #header>
               <el-icon class="header-icon"><Document /></el-icon>
               <span>交易文件</span>
             </template>
             <template #default="{ row }">
               <div class="file-name">
                 <el-icon class="file-icon"><Document /></el-icon>
                 <span>{{ row.name }}</span>
               </div>
             </template>
           </el-table-column>
           <el-table-column label="文件大小" prop="size" width="120">
             <template #default="{ row }">
               {{ formatFileSize(row.size) }}
             </template>
           </el-table-column>
           <el-table-column label="操作" width="180">
            <template #default="{ row }">
              <div class="file-actions">
                <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>
              </div>
            </template>
          </el-table-column>
         </el-table>
       </div>
     </el-card>
     <!-- 交易信息备注(仅在订单状态为"待交易确认"或之后时显示) -->
     <el-card class="mt15" shadow="never" v-if="shouldShowRemark">
       <div class="title">交易信息备注</div>
       <el-table :data="remarkItems" border class="mt10 remark-table">
         <el-table-column label="详情" prop="name" min-width="200" />
         <el-table-column label="授权开始时间" width="200">
           <template #default="{ row, $index }">
             <span>{{ remarkItems[$index].start || '-' }}</span>
           </template>
         </el-table-column>
         <el-table-column label="授权结束时间" width="200">
           <template #default="{ row, $index }">
             <div class="end-time-wrapper">
               <span v-if="remarkItems[$index].forever" class="forever-text">永久</span>
               <span v-else>{{ remarkItems[$index].end || '-' }}</span>
             </div>
           </template>
         </el-table-column>
         <el-table-column label="备注" min-width="300">
           <template #default="{ row, $index }">
             <span>{{ remarkItems[$index].remark || '-' }}</span>
           </template>
         </el-table-column>
       </el-table>
     </el-card>
    <!-- 审批追踪 -->
    <el-card class="mt15" shadow="never" v-if="detail.records?.length">
    <el-card class="mt15" shadow="never">
      <div class="title">审批追踪</div>
      <!-- 标签页 -->
      <el-tabs v-model="activeTab" class="approval-tabs">
      <!-- <el-tabs v-model="activeTab" class="approval-tabs">
        <el-tab-pane label="审批记录" name="records">
          <el-table
            :data="detail.records"
@@ -199,13 +304,65 @@
            </el-table-column>
          </el-table>
        </el-tab-pane>
      </el-tabs>
      <!-- 返回按钮 -->
      <div class="action-buttons">
        <el-button @click="goBack">返回</el-button>
      </el-tabs> -->
      <!-- 外部系统审批轨迹 iframe -->
      <div class="iframe-wrap" v-if="workflowIframeUrl">
        <iframe :src="workflowIframeUrl" class="workflow-iframe" referrerpolicy="no-referrer"></iframe>
      </div>
    </el-card>
    <!-- 交易评价(当已评价时显示) -->
    <el-card class="mt15" shadow="never" v-if="showEvaluation">
      <div class="title">交易评价</div>
      <div class="evaluation-content">
        <div class="evaluation-form">
          <div class="rating-section">
            <div class="rating-items">
              <div class="rating-row">
                <div class="rating-item">
                  <label class="required">综合评分:</label>
                  <el-rate :model-value="evaluation.overallRating" :max="5" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']" />
                </div>
                <div class="rating-item">
                  <label class="required">服务评分:</label>
                  <el-rate :model-value="evaluation.serviceRating" :max="5" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']" />
                </div>
              </div>
              <div class="rating-row">
                <div class="rating-item">
                  <label class="required">质量评分:</label>
                  <el-rate :model-value="evaluation.qualityRating" :max="5" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']" />
                </div>
                <div class="rating-item">
                  <label class="required">速度评分:</label>
                  <el-rate :model-value="evaluation.speedRating" :max="5" disabled :colors="['#99A9BF', '#F7BA2A', '#FF9900']" />
                </div>
              </div>
            </div>
          </div>
          <div class="form-item">
            <label class="required">评价内容:</label>
            <div class="eval-text">{{ evaluation.content || '-' }}</div>
          </div>
          <div class="form-item">
            <label>其他信息:</label>
            <div class="eval-meta">
              <span>是否匿名:{{ evaluation.isAnonymous ? '是' : '否' }}</span>
              <span class="split">|</span>
              <span>评价时间:{{ evaluation.evaluateTime || '-' }}</span>
            </div>
          </div>
        </div>
      </div>
    </el-card>
    <!-- 返回按钮 -->
    <div class="action-buttons">
        <el-button @click="goBack">返回</el-button>
      </div>
  </div>
</template>
@@ -213,14 +370,58 @@
import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue'
import { Document, User, Goods, List } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import orderApi from '@/api/orderApi'
import createAxios from '@/utils/axios'
import productApi from '@/api/productApi'
import sysUserService from '@/api/sysUser'
import { useUserInfo } from '@/stores/modules/userInfo'
const hostUrl =  import.meta.env.VITE_AXIOS_BASE_URL
const route = useRoute()
const router = useRouter()
const detail = reactive<any>({ items: [], records: [], nodes: [] })
const evaluation = reactive<any>({
  content: '',
  overallRating: 0,
  serviceRating: 0,
  qualityRating: 0,
  speedRating: 0,
  isAnonymous: false,
  evaluateTime: ''
})
const orderTableWrapRef = ref<HTMLElement | null>(null)
const activeTab = ref('records')
const userStore = useUserInfo()
// 外部审批轨迹iframe地址
const workflowIframeUrl = computed(() => {
  const pid = (detail.workflowId || route.query.processinstId || '').toString().trim()
  if (!pid) return ''
  return `${hostUrl}/activity/history?processinstId=${encodeURIComponent(pid)}&token=${userStore.getAdminToken}`
})
const labelStyle = { width: '180px', maxWidth: '180px' }
const contentStyle = { width: 'calc(50% - 180px)' }
// 文件相关
const fileList = ref<any[]>([])
const isAgreementOrder = ref(false)
// 交易信息备注
const remarkItems = ref<any[]>([])
const shouldShowRemark = computed(() => {
  const statusName = detail.statusName || ''
  // 当订单状态为"待交易确认"或之后的状态时显示交易信息备注
  // "待上传文件"和"待授权"状态不显示交易信息备注
  const statusOrder = ['WAIT_UPLOAD', 'WAIT_AUTHORIZE', 'WAIT_CONFIRM', 'COMPLETED', 'EVALUATED']
  const currentStatus = statusServerToUi[statusName]
  return currentStatus && statusOrder.indexOf(currentStatus) >= statusOrder.indexOf('WAIT_CONFIRM')
})
// 是否显示评价卡片(已评价时显示)
const showEvaluation = computed(() => {
  return (detail.isEvaluate === '已评价')
})
// 计算表格数据,添加汇总行
const tableData = computed(() => {
@@ -240,120 +441,185 @@
  return [...detail.items, summaryRow]
})
onMounted(async () => {
  // 使用前端模拟数据以便开发 UI(不改动后端服务)
  const mockDetail = {
    orderNo: '4348442557619205545',
    resourceTypeName: '软件产品',
    status: 'WAIT_APPROVAL',
    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',
    records: [
      {
        nodeName: '提交人',
        approver: '张静',
        department: '门户系统临时组',
        startTime: '2025-05-21 16:08:09',
        endTime: '2025-05-21 16:08:09',
        statusName: '已完成',
        opinion: '',
      },
      {
        nodeName: '交易审批',
        approver: '喻会峰',
        department: '门户系统临时组',
        startTime: '2025-05-21 16:08:09',
        endTime: '',
        statusName: '审阅中',
        opinion: '',
      },
    ],
    nodes: [
      {
        nodeName: '提交申请',
        nodeType: '开始节点',
        handler: '张静',
        department: '门户系统临时组',
        status: 'completed',
        statusName: '已完成',
      },
      {
        nodeName: '交易审批',
        nodeType: '审批节点',
        handler: '喻会峰',
        department: '门户系统临时组',
        status: 'processing',
        statusName: '处理中',
      },
    ],
  }
// 状态映射(后端中文 -> 前端枚举)
const statusServerToUi: Record<string, string> = {
  '待上传文件': 'WAIT_UPLOAD',
  '待授权': 'WAIT_AUTHORIZE',
  '待交易确认': 'WAIT_CONFIRM',
  '已完成': 'COMPLETED',
  '已评价': 'EVALUATED',
}
  Object.assign(detail, mockDetail)
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 () => {
  const orderId = String(route.params.id || '')
  if (!orderId) return
  try {
    // 并行获取订单详情和协议类型检查
    const [orderRes, agreementRes] = await Promise.all([
      orderApi.getOrderDetail(orderId),
      orderApi.checkAgreementPriceType(orderId)
    ])
    const res = orderRes as any
    const data = res?.data || {}
    // 设置是否为协议订单
    const agreementResult = agreementRes as any
    isAgreementOrder.value = agreementResult?.data === true
    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 || '-',
      isEvaluate: data.isEvaluate || '未评价'
    }
    // 明细项映射
    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),
            remarks: d.remarks || '', // 新增 remarks 字段
          }
        })
      : []
    // 汇总(简单相加:单价*数量)
    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(),
      records: [],
      nodes: [],
      workflowId: data.workflowId || data.processinstId || ''
    })
    // 映射交易评价信息(如果存在)
    if (data.evaluation) {
      evaluation.content = data.evaluation.content || ''
      evaluation.overallRating = Number(data.evaluation.rating || data.evaluation.overallRating || 0)
      evaluation.serviceRating = Number(data.evaluation.serviceRating || 0)
      evaluation.qualityRating = Number(data.evaluation.qualityRating || 0)
      // 后端为 deliveryRating,对齐前端 speedRating 命名
      evaluation.speedRating = Number(data.evaluation.deliveryRating || data.evaluation.speedRating || 0)
      evaluation.isAnonymous = Boolean(data.evaluation.isAnonymous)
      evaluation.evaluateTime = formatDateTime((data.evaluation.replyTime || data.evaluation.createdAt || data.evaluation.createTime) as any)
    }
    // 初始化交易信息备注数据
    remarkItems.value = (detail.items || []).map((item: any, index: number) => {
      // 计算授权结束时间
      let endDate = ''
      if (item.period > 0) {
        // 如果有期限,计算结束时间
        const startDate = new Date(data.applyTime || new Date())
        const endDateObj = new Date(startDate)
        endDateObj.setFullYear(endDateObj.getFullYear() + item.period)
        endDate = endDateObj.toISOString().split('T')[0] // 格式化为 YYYY-MM-DD
      }
      return {
        name: item.name,
        start: data.applyTime ? data.applyTime.split('T')[0] : '', // 使用订单申请时间
        end: endDate,
        forever: item.period === 0, // 期限为0时设置为永久
        remark: item.remarks || '' // 使用套件信息中的remarks字段
      }
    })
    // 获取订单附件列表(如果有的话)
    if (data.attachments && Array.isArray(data.attachments)) {
      fileList.value = data.attachments.map((file: any) => ({
        name: file.fileName || file.originalName,
        size: file.fileSize || 0,
        uid: file.id,
        status: 'success',
        url: file.fileUrl,
        uploaded: true
      }))
    } else {
      fileList.value = []
    }
  } catch (error) {
    console.error('获取订单详情失败:', error)
    ElMessage.error('获取订单详情失败')
  }
})
// 与列表页保持一致的状态类型映射(UI展示用)
@@ -394,7 +660,7 @@
}
// 表头文字居中,但第一行的"详情"文字靠左对齐
const headerCenterStyle: CSSProperties = {
const headerCenterStyle: CSSProperties = {
  textAlign: 'center',
  fontSize: '14px',
  background: '#f3f6fb'
@@ -404,12 +670,21 @@
const bodyCellStyle: CSSProperties = { fontSize: '12px' }
// 审批追踪表格样式
const recordTableHeaderStyle: CSSProperties = {
const recordTableHeaderStyle: CSSProperties = {
  textAlign: 'center',
  fontSize: '14px',
  background: '#f3f6fb'
}
const recordTableCellStyle: CSSProperties = { fontSize: '12px' }
// 文件列表表格表头文字居中,但第一列的"交易文件"文字靠左对齐
const fileTableHeaderStyle: CSSProperties = {
  textAlign: 'center',
  fontSize: '14px',
  background: '#f3f6fb'
}
// 文件列表表格表体文字大小
const fileTableCellStyle: CSSProperties = { fontSize: '12px' }
// 为汇总行添加特殊样式类名
const getRowClassName = ({ row }: { row: any }) => {
@@ -441,6 +716,211 @@
// 返回按钮
const goBack = () => router.back()
// 文件大小格式化
const formatFileSize = (size: number) => {
  if (!size || size === 0) return '0 Bytes';
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(size) / Math.log(k));
  return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 判断文件是否可预览
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 isFileUploaded = (file: any) => {
  // 文件有url且状态为success表示已上传成功
  return file.url && (file.status === 'success' || file.uploaded)
}
// 文件预览
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)
        // 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 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('下载失败,请重试')
  }
}
// 单元格合并方法
const arraySpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
@@ -626,10 +1106,183 @@
  width: calc(50% - 180px) !important;
}
/* 订单信息自定义布局样式 */
.order-info-section {
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  margin-top: 10px;
  overflow: hidden;
}
.order-info-section .section-header {
  background: #f3f6fb;
  padding: 12px 16px;
  border-bottom: 1px solid #e4e7ed;
  font-weight: 600;
  display: flex;
  align-items: center;
}
.order-info-section .section-icon {
  margin-right: 6px;
  color: #409eff;
}
.order-info-grid {
  display: grid;
  grid-template-columns: 15% 30% 15% 40%;
  width: 100%;
}
.order-info-item {
  display: contents;
}
.order-info-item .order-label {
  padding: 12px 16px;
  background: #fafafa;
  border: 1px solid #e4e7ed;
  font-size: 14px;
  color: #606266;
  display: flex;
  align-items: center;
}
.order-info-item .order-content {
  padding: 12px 16px;
  border: 1px solid #e4e7ed;
  font-size: 14px;
  color: #303133;
  display: flex;
  align-items: center;
}
/* 申请人信息自定义布局样式 */
.applicant-info-section {
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  margin-top: 15px;
  overflow: hidden;
}
.applicant-info-section .section-header {
  background: #f3f6fb;
  padding: 12px 16px;
  border-bottom: 1px solid #e4e7ed;
  font-weight: 600;
  display: flex;
  align-items: center;
}
.applicant-info-section .section-icon {
  margin-right: 6px;
  color: #409eff;
}
.applicant-info-grid {
  display: grid;
  grid-template-columns: 15% 30% 15% 40%;
  width: 100%;
}
.applicant-info-item {
  display: contents;
}
.applicant-info-item .applicant-label {
  padding: 12px 16px;
  background: #fafafa;
  border: 1px solid #e4e7ed;
  font-size: 14px;
  color: #606266;
  display: flex;
  align-items: center;
}
.applicant-info-item .applicant-content {
  padding: 12px 16px;
  border: 1px solid #e4e7ed;
  font-size: 14px;
  color: #303133;
  display: flex;
  align-items: center;
}
/* 交易内容自定义布局样式 */
.transaction-content-section {
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  margin-top: 15px;
  overflow: hidden;
}
.transaction-content-section .section-header {
  background: #f3f6fb;
  padding: 12px 16px;
  border-bottom: 1px solid #e4e7ed;
  font-weight: 600;
  display: flex;
  align-items: center;
}
.transaction-content-section .section-icon {
  margin-right: 6px;
  color: #409eff;
}
.transaction-content-grid {
  display: grid;
  grid-template-columns: 15% 30% 15% 40%;
  width: 100%;
}
.transaction-content-item {
  display: contents;
}
.transaction-content-item .transaction-label {
  padding: 12px 16px;
  background: #fafafa;
  border: 1px solid #e4e7ed;
  font-size: 14px;
  color: #606266;
  display: flex;
  align-items: center;
}
.transaction-content-item .transaction-content {
  padding: 12px 16px;
  border: 1px solid #e4e7ed;
  font-size: 14px;
  color: #303133;
  display: flex;
  align-items: center;
}
.desc-wrap {
  white-space: pre-wrap;
  line-height: 22px;
}
/* 单行省略(用于“单位工程”) */
.ellipsis-1 {
  max-height: 22px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 多行省略(用于“产品简介”,固定为3行,可按需调整) */
.ellipsis-3 {
  display: -webkit-box;
  line-clamp: 3;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* 审批追踪标签页样式 */
.approval-tabs {
@@ -645,6 +1298,85 @@
    margin: 0 10px;
  }
}
/* 文件列表表格样式 */
.file-section {
  margin-top: 15px;
  .file-title {
    font-weight: 600;
    margin-bottom: 10px;
  }
  .file-table {
    width: 100%;
    .file-name {
      display: flex;
      align-items: center;
      .file-icon {
        margin-right: 8px;
        color: #409eff;
      }
    }
    .preview-btn {
      color: #409eff;
      &:hover {
        text-decoration: underline;
      }
    }
  }
}
/* 文件表格表头第一列"交易文件"文字靠左对齐 */
.file-table :deep(.el-table__header-wrapper thead tr th:first-child) {
  text-align: left !important;
}
/* 外部审批轨迹 iframe 样式 */
.iframe-wrap { margin-top: 10px; }
.workflow-iframe { width: 100%; height: 500px; border: none; border-radius: 6px; }
/* 文件操作按钮样式 */
.file-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: center;
  .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;
    }
  }
}
/* 交易信息备注表格样式 */
.remark-table {
  width: 100%;
  .end-time-wrapper {
    display: flex;
    align-items: center;
    gap: 10px;
    .forever-text {
      color: #409eff;
      font-weight: 500;
    }
  }
}
</style>