seatonwan9
2025-08-24 2c55b1a8a0700df79268550335506637b41610ce
src/views/approveManage/tradeApproval/checkFiles.vue
@@ -1,112 +1,1154 @@
<template>
  <div class="default-main">
    <!-- 订单信息 + 申请人信息 + 交易内容(合并为同一卡片) -->
    <el-card shadow="never">
      <div class="title">订单信息</div>
      <el-descriptions :column="3" border class="mt10">
      <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.statusName }}</el-descriptions-item>
        <el-descriptions-item label="申请时间">{{ detail.applyTime }}</el-descriptions-item>
        <el-descriptions-item label="单位">{{ detail.unitName }}</el-descriptions-item>
        <el-descriptions-item label="用户名">{{ detail.userName }}</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-card>
    <el-card class="mt15" shadow="never">
      <div class="title">交易内容</div>
      <el-table :data="detail.items" border class="mt10">
        <el-table-column label="详情" min-width="280">
          <template #default="{ row }">
            <div>
              <div>{{ row.name }}</div>
              <div class="gray">客户对象:{{ row.customerTarget }}</div>
              <div class="gray">并发节点数:{{ row.concurrentNodes }}</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"><User /></el-icon>
            <span>申请人信息</span>
          </template>
        </el-table-column>
        <el-table-column label="单价" prop="priceName" width="140" />
        <el-table-column label="数量" prop="quantity" width="80" />
        <el-table-column label="期限(年)" prop="period" width="120" />
      </el-table>
          <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="total">
      <!-- 交易内容(紧随申请人信息,同卡片,复用分隔标题样式) -->
      <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 ref="orderTableWrapRef">
        <el-table
          :data="tableData"
          border
          class="mt10 order-table"
          :header-cell-style="headerCenterStyle"
          :cell-style="bodyCellStyle"
          :row-class-name="getRowClassName"
          :span-method="arraySpanMethod"
        >
        <el-table-column>
          <template #header>
            <el-icon class="header-icon"><List /></el-icon>
            <span>详情</span>
          </template>
          <el-table-column label="" :width="colWidths.detail1">
            <template #default="{ row }">
              <div v-if="!row.isSummary">{{ row.name }}</div>
              <div v-else class="summary-merged">
                <div class="summary-left">
                  共 <span class="count">{{ detail.items.length }}</span> 件
                </div>
                <div class="summary-right">
        总计:<span class="price">{{ detail.pointTotal }}</span> 积分
        <span class="ml20 price">{{ detail.cashTotal }}</span> 元
      </div>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="" :width="colWidths.detail2">
            <template #default="{ row }">
              <div v-if="!row.isSummary" class="gray">销售形式:{{ row.saleType || '-' }}</div>
              <div v-if="!row.isSummary" class="gray">账户数量:{{ row.accountCount ?? '-' }}</div>
            </template>
          </el-table-column>
          <el-table-column label="" :width="colWidths.detail3">
            <template #default="{ row }">
              <div v-if="!row.isSummary" class="gray">客户对象:{{ row.customerTarget || '-' }}</div>
              <div v-if="!row.isSummary" class="gray">并发节点数量:{{ row.concurrentNodes ?? '-' }}</div>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="单价">
          <el-table-column label="" :width="colWidths.price">
            <template #default="{ row }">
              <div v-if="!row.isSummary">{{ formatPrice(row) }}</div>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="数量">
          <el-table-column label="" :width="colWidths.quantity">
            <template #default="{ row }">
              <div v-if="!row.isSummary">{{ row.quantity }}</div>
            </template>
          </el-table-column>
        </el-table-column>
        <el-table-column label="期限(年)">
          <el-table-column label="" :width="colWidths.period">
            <template #default="{ row }">
              <div v-if="!row.isSummary">{{ formatPeriod(row) }}</div>
            </template>
          </el-table-column>
        </el-table-column>
        </el-table>
      </div>
      <!-- 移除原来的表格底部信息,因为已经移到表格最后一行 -->
    </el-card>
    <el-card class="mt15" shadow="never">
      <div class="title">交易文件</div>
      <el-table :data="files" border class="mt10">
        <el-table-column label="文件名称" prop="name" />
        <el-table-column label="文件大小" prop="size" width="140" />
        <el-table-column label="操作" width="160">
    <!-- 审批追踪 -->
    <el-card class="mt15" shadow="never" v-if="detail.records?.length">
      <div class="title">审批追踪</div>
      <!-- 标签页 -->
      <el-tabs v-model="activeTab" class="approval-tabs">
        <el-tab-pane label="审批记录" name="records">
          <el-table
            :data="detail.records"
            border
            class="mt10 record-table"
            :header-cell-style="recordTableHeaderStyle"
            :cell-style="recordTableCellStyle"
          >
            <el-table-column label="节点名称" prop="nodeName" width="120" />
            <el-table-column label="审批人" prop="approver" width="120" />
            <el-table-column label="审批部门" prop="department" width="200" />
            <el-table-column label="开始时间" prop="startTime" width="180" />
            <el-table-column label="结束时间" prop="endTime" width="180" />
            <el-table-column label="状态" width="120">
          <template #default="{ row }">
            <el-button type="primary" link @click="preview(row)">预览</el-button>
            <el-button type="primary" link @click="download(row)">下载</el-button>
                <el-tag :type="getRecordStatusType(row.statusName)" size="small">
                  {{ row.statusName }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="审批意见" width="200">
              <template #default="{ row }">
                {{ row.opinion || '-' }}
              </template>
            </el-table-column>
          </el-table>
        </el-tab-pane>
        <el-tab-pane label="流程节点" name="nodes">
          <el-table
            :data="detail.nodes"
            border
            class="mt10 node-table"
            :header-cell-style="recordTableHeaderStyle"
            :cell-style="recordTableCellStyle"
          >
            <el-table-column label="节点名称" prop="nodeName" width="120" />
            <el-table-column label="节点类型" prop="nodeType" width="120" />
            <el-table-column label="处理人" prop="handler" width="160" />
            <el-table-column label="处理部门" prop="department" width="200" />
            <el-table-column label="状态" width="120">
              <template #default="{ row }">
                <el-tag :type="getNodeStatusType(row.status)" size="small">
                  {{ row.statusName }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </el-tab-pane>
      </el-tabs>
      <!-- 返回按钮 -->
      <div class="action-buttons">
        <el-button @click="goBack">返回</el-button>
      </div>
    </el-card>
    <!-- 交易信息备注 -->
    <el-card class="mt15" shadow="never">
      <div class="title">文件核查</div>
      <!-- 文件列表 -->
      <el-table
        :data="fileList"
        border
        class="mt10 file-table"
        :header-cell-style="fileTableHeaderStyle"
        :cell-style="fileTableCellStyle"
        v-loading="fileLoading"
      >
        <el-table-column label="序号" type="index" width="60" align="center" />
        <el-table-column label="文件名" prop="originalName" min-width="200">
          <template #default="{ row }">
            <div class="file-name">
              <el-icon class="file-icon" :class="getFileIconClass(row.fileType)">
                <Document v-if="getFileIconClass(row.fileType) === 'doc'" />
                <Picture v-else-if="getFileIconClass(row.fileType) === 'image'" />
                <VideoPlay v-else-if="getFileIconClass(row.fileType) === 'video'" />
                <Document v-else />
              </el-icon>
              <span class="file-text">{{ row.originalName || row.fileName }}</span>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="文件类型" prop="fileType" width="100" align="center">
          <template #default="{ row }">
            <el-tag :type="getFileTypeTag(row.fileType)" size="small">
              {{ getFileTypeName(row.fileType) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="文件大小" prop="fileSize" width="100" align="center">
          <template #default="{ row }">
            {{ formatFileSize(row.fileSize) }}
          </template>
        </el-table-column>
        <el-table-column label="上传时间" prop="createdAt" width="140" align="center">
          <template #default="{ row }">
            {{ formatDateTime(row.createdAt) }}
          </template>
        </el-table-column>
        <el-table-column label="上传人" prop="uploadUserName" width="100" align="center" />
        <el-table-column label="附件类型" prop="attachmentType" width="100" align="center">
          <template #default="{ row }">
            <el-tag :type="getAttachmentTypeTag(row.attachmentType)" size="small">
              {{ row.attachmentType || '其他' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="180" align="center">
          <template #default="{ row }">
            <el-button
              type="primary"
              size="small"
              @click="handlePreview(row)"
              :disabled="!isPreviewable(row)"
            >
              预览
            </el-button>
            <el-button
              type="success"
              size="small"
              @click="handleDownload(row)"
            >
              下载
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="title mt15">交易信息备注</div>
      <el-form :model="form" label-width="120px" class="mt10">
        <div v-for="(item, i) in detail.items" :key="i" class="item-block">
          <div class="sub-title">{{ item.name }}</div>
          <el-form-item label="备注">
            <el-input v-model="form.items[i].remark" placeholder="请输入备注" />
      <!-- 审批意见 -->
      <div class="approval-section mt15">
        <div class="section-title">审批意见</div>
        <el-form :model="approvalForm" label-width="100px" class="mt10">
          <el-form-item label="审批意见" prop="opinion">
            <el-input
              v-model="approvalForm.opinion"
              type="textarea"
              :rows="4"
              placeholder="请输入审批意见"
              maxlength="500"
              show-word-limit
            />
          </el-form-item>
        </div>
      </el-form>
      <div class="ba-center mt15">
        <el-button @click="goBack">返回</el-button>
        <el-button type="success" @click="submit(true)">通过</el-button>
        <el-button type="danger" @click="submit(false)">驳回</el-button>
      </div>
      <!-- 操作按钮 -->
      <div class="approval-actions">
        <el-button @click="goBack" class="back-btn">返回</el-button>
        <el-button
          type="success"
          @click="handleApprove(true)"
          class="approve-btn"
          :loading="approvalLoading"
        >
          通过
        </el-button>
        <el-button
          type="danger"
          @click="handleApprove(false)"
          class="reject-btn"
          :loading="approvalLoading"
        >
          驳回
        </el-button>
      </div>
    </el-card>
    <!-- 文件预览弹窗 -->
    <el-dialog
      v-model="previewVisible"
      title="文件预览"
      width="80%"
      :close-on-click-modal="false"
      :close-on-press-escape="false"
      class="file-preview-dialog"
    >
      <div class="preview-content">
        <filePreview ref="filePreviewRef" @closePreview="previewVisible = false" />
      </div>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import { onMounted, reactive, ref, computed, nextTick, type CSSProperties } from 'vue'
import { Document, User, Goods, List, Picture, VideoPlay } from '@element-plus/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchApprovalDetail, checkFiles } from '@/api/approvalManage'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import orderApi from '@/api/orderApi'
import { checkFiles } from '@/api/approvalManage'
import filePreview from '@/components/filePreview/index.vue'
import createAxios from '@/utils/axios'
import { useUserInfo } from '@/stores/modules/userInfo'
const route = useRoute()
const router = useRouter()
const detail = reactive<any>({ items: [] })
const files = reactive<any[]>([])
const form = reactive<any>({ items: [] })
const userStore = useUserInfo()
const detail = reactive<any>({ items: [], records: [], nodes: [] })
const orderTableWrapRef = ref<HTMLElement | null>(null)
const activeTab = ref('records')
const labelStyle = { width: '180px', maxWidth: '180px' }
const contentStyle = { width: 'calc(50% - 180px)' }
onMounted(async () => {
  const { data } = (await fetchApprovalDetail({ id: route.params.id })) as any
  Object.assign(detail, data || {})
  files.splice(0, files.length, ...(data?.files || []))
  form.items = (detail.items || []).map(() => ({ remark: '' }))
// 文件相关数据
const fileList = ref<any[]>([])
const fileLoading = ref(false)
const previewVisible = ref(false)
const filePreviewRef = ref()
// 审批相关数据
const approvalForm = reactive({
  opinion: ''
})
const approvalLoading = ref(false)
// 文件表格样式
const fileTableHeaderStyle: CSSProperties = {
  textAlign: 'center',
  fontSize: '14px',
  background: '#f3f6fb'
}
const fileTableCellStyle: CSSProperties = { fontSize: '12px' }
// 计算表格数据,添加汇总行
const tableData = computed(() => {
  const summaryRow = {
    id: 'summary',
    isSummary: true,
    name: '',
    saleType: '',
    accountCount: 0,
    customerTarget: '',
    concurrentNodes: 0,
    pricePoint: 0,
    priceCash: 0,
    quantity: 0,
    period: 0,
  }
  return [...detail.items, summaryRow]
})
const preview = (file: any) => window.open(file.previewUrl)
const download = (file: any) => window.open(file.downloadUrl)
// 状态映射(后端中文 -> 前端枚举)
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 () => {
  const orderId = String(route.params.id || '')
  if (!orderId) return
  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: '-',
    userName: '-',
    userAccount: '-',
    userDept: '-',
    userPhone: '-',
    productName: data.productName || '-',
    supplier: data.providerName || '-',
    industry: '-',
    projectUnit: '-',
    productType: '-',
    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(),
    records: [],
    nodes: [],
  })
  // 初始化文件列表
  if (data.attachments && Array.isArray(data.attachments)) {
    fileList.value = data.attachments
  }
})
// 与列表页保持一致的状态类型映射(UI展示用)
const getStatusType = (status: string) => {
  const statusMap: Record<string, 'warning' | 'danger' | 'success' | 'info'> = {
    WAIT_APPROVAL: 'warning',
    WAIT_UPLOAD: 'warning',
    WAIT_CHECK: 'warning',
    WAIT_CONFIRM: 'warning',
    REJECTED: 'danger',
    FINISH: 'success',
  }
  return statusMap[status] || 'info'
}
// 订单详情中"单价"显示:优先显示积分,其次显示货币;格式示例:
// "积分:50,000/套" 或 "货币:7,500/套/年" 或 "免费:/年"
const formatPrice = (row: any) => {
  const point = Number(row.pricePoint || 0)
  const cash = Number(row.priceCash || 0)
  const protocol = Boolean(row.priceProtocol)
  // 免费
  if (!point && !cash) {
    return protocol ? '协议:/年' : '免费:/年'
  }
  if (point) {
    return `积分:${point.toLocaleString()}/套`
  }
  // 仅现金
  return `货币:${cash.toLocaleString()}/套/年`
}
// 期限展示:0 表示"永久",其他显示数字
const formatPeriod = (row: any) => {
  const p = Number(row.period || 0)
  return p === 0 ? '永久' : `${p}`
}
// 表头文字居中,但第一行的"详情"文字靠左对齐
const headerCenterStyle: CSSProperties = {
  textAlign: 'center',
  fontSize: '14px',
  background: '#f3f6fb'
}
// 表体文字大小
const bodyCellStyle: CSSProperties = { fontSize: '12px' }
// 审批追踪表格样式
const recordTableHeaderStyle: CSSProperties = {
  textAlign: 'center',
  fontSize: '14px',
  background: '#f3f6fb'
}
const recordTableCellStyle: CSSProperties = { fontSize: '12px' }
// 为汇总行添加特殊样式类名
const getRowClassName = ({ row }: { row: any }) => {
  return row.isSummary ? 'summary-row' : ''
}
// 审批记录状态类型映射
const getRecordStatusType = (statusName: string) => {
  const statusMap: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
    '已完成': 'success',
    '审阅中': 'warning',
    '已提交': 'info',
    '待审核': 'warning',
    '已拒绝': 'danger',
  }
  return statusMap[statusName] || 'info'
}
// 流程节点状态类型映射
const getNodeStatusType = (status: string) => {
  const statusMap: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
    'completed': 'success',
    'processing': 'warning',
    'pending': 'info',
    'rejected': 'danger',
  }
  return statusMap[status] || 'info'
}
// 返回按钮
const goBack = () => router.back()
const submit = async (pass: boolean) => {
  const { code } = (await checkFiles({ id: route.params.id, pass, ...form })) as any
  if (code === 200) {
    ElMessage.success('操作成功')
// 文件相关方法
const getFileIconClass = (fileType: string) => {
  const type = fileType?.toLowerCase() || ''
  if (['doc', 'docx', 'pdf', 'txt'].includes(type)) return 'doc'
  if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) return 'image'
  if (['mp4', 'avi', 'mov', 'wmv'].includes(type)) return 'video'
  return 'doc'
}
const getFileTypeTag = (fileType: string) => {
  const type = fileType?.toLowerCase() || ''
  if (['doc', 'docx'].includes(type)) return 'primary'
  if (['pdf'].includes(type)) return 'danger'
  if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) return 'success'
  if (['mp4', 'avi', 'mov', 'wmv'].includes(type)) return 'warning'
  return 'info'
}
const getFileTypeName = (fileType: string) => {
  const type = fileType?.toLowerCase() || ''
  if (['doc', 'docx'].includes(type)) return 'Word'
  if (['pdf'].includes(type)) return 'PDF'
  if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) return '图片'
  if (['mp4', 'avi', 'mov', 'wmv'].includes(type)) return '视频'
  if (['xls', 'xlsx'].includes(type)) return 'Excel'
  return '其他'
}
const formatFileSize = (size: number) => {
  if (!size) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB']
  let index = 0
  let fileSize = size
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024
    index++
  }
  return `${fileSize.toFixed(2)} ${units[index]}`
}
const getAttachmentTypeTag = (type: string) => {
  switch (type) {
    case '合同': return 'primary'
    case '发票': return 'success'
    default: return 'info'
  }
}
// 判断文件是否可预览
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.fileType || '')) {
    return true
  }
  // 如果MIME类型为空或不匹配,根据文件扩展名判断
  const fileName = file.originalName || file.fileName || ''
  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) => {
  // 优先使用fileUrl,如果没有则使用fileName
  const fileUrl = file.fileUrl || file.fileName
  if (!fileUrl) {
    ElMessage.warning('文件链接不存在')
    return
  }
  // 获取文件扩展名
  const fileName = file.originalName || file.fileName || ''
  const fileExtension = fileName.toLowerCase().split('.').pop()
  let previewUrl = fileUrl
  // 如果文件存储在MinIO,优先使用预览URL
  if (fileUrl.includes('order-attachments')) {
    try {
      // 首先尝试获取预览URL
      const previewResponse = await createAxios({
        url: `/admin/file/preview`,
        method: 'GET',
        params: {
          fileName: fileUrl
        }
      })
      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: fileUrl,
            originalName: file.originalName || file.fileName
          }
        })
        // 创建预览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.fileType && file.fileType.startsWith('image/')) ||
      ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) {
    console.log('预览图片文件:', previewUrl)
    window.open(previewUrl, '_blank')
    return
  }
  // PDF文件在新窗口打开
  if (file.fileType === 'application/pdf' || fileExtension === 'pdf') {
    console.log('预览PDF文件:', previewUrl)
    window.open(previewUrl, '_blank')
    return
  }
  // 文本文件在新窗口打开
  if ((file.fileType && file.fileType.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) => {
  // 优先使用fileUrl,如果没有则使用fileName
  const fileUrl = file.fileUrl || file.fileName
  if (!fileUrl) {
    ElMessage.warning('文件链接不存在')
    return
  }
  console.log('开始下载文件:', file.originalName || file.fileName, 'URL:', fileUrl)
  try {
    // 如果文件存储在MinIO,使用后端直接下载API
    if (fileUrl.includes('order-attachments')) {
      console.log('使用MinIO下载API')
      // 使用axios通过代理访问后端API
      const response = await createAxios({
        url: `/admin/file/download`,
        method: 'GET',
        responseType: 'blob',
        params: {
          fileName: fileUrl,
          originalName: file.originalName || file.fileName
        }
      })
      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.originalName || file.fileName || '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 = fileUrl
      link.download = file.originalName || file.fileName || '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 handleApprove = async (isApprove: boolean) => {
  if (!approvalForm.opinion.trim()) {
    ElMessage.warning('请输入审批意见')
    return
  }
  const actionText = isApprove ? '通过' : '驳回'
  try {
    await ElMessageBox.confirm(`确定要${actionText}该订单吗?`, '确认操作', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    approvalLoading.value = true
    const orderId = String(route.params.id || '')
    // 调用文件核查API
    const result = await checkFiles({
      orderId: orderId,
      isApprove: isApprove,
      approvalOpinion: approvalForm.opinion,
      approverId: Number(userStore.getUserId) || 1,
      approverName: userStore.getUserDetail || '管理员'
    })
    if (result && result.code === 200) {
      ElMessage.success(`${actionText}成功`)
    router.back()
    } else {
      ElMessage.error(result?.msg || `${actionText}失败`)
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('审批失败:', error)
      ElMessage.error('审批失败,请重试')
    }
  } finally {
    approvalLoading.value = false
  }
}
// 单元格合并方法
const arraySpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
  if (row.isSummary) {
    // 汇总行:第一列显示合并内容,其他列隐藏
    if (columnIndex === 0) {
      return [1, 6] // 合并1行6列
    } else {
      return [0, 0] // 隐藏其他列
    }
  }
  return [1, 1] // 普通行正常显示
}
// 症结与修复说明:
// 1) Element Plus 的 el-table 子列 width 百分比是相对于表格容器的像素宽度计算,但只有在表格容器有明确宽度时才生效。
// 2) 在父列/多级表头下,子列 width 为百分比时,更稳定的做法是将其计算为像素值绑定给子列。
// 3) 因此我们读取表格外层容器宽度,按比例计算像素宽度,避免出现百分比与 table-layout 导致的错位与拉伸。
const colWidths = computed(() => {
  const containerWidth = orderTableWrapRef.value?.clientWidth || 0
  // 百分比分别为:详情 20% x 3,单价 15%,数量 10%,期限 15% => 合计 100%
  return {
    detail1: Math.floor(containerWidth * 0.2),
    detail2: Math.floor(containerWidth * 0.2),
    detail3: Math.floor(containerWidth * 0.2),
    price: Math.floor(containerWidth * 0.15),
    quantity: Math.floor(containerWidth * 0.1),
    period: Math.floor(containerWidth * 0.15),
  }
})
</script>
<style scoped lang="scss">
.title { font-weight: 600; }
.sub-title { font-weight: 600; margin: 10px 0; }
.mt10 { margin-top: 10px; }
.mt15 { margin-top: 15px; }
.gray { color: #909399; font-size: 12px; }
.total { text-align: right; margin-top: 10px; }
.price { color: #f56c6c; font-weight: 600; }
.ml20 { margin-left: 20px; }
.item-block { padding: 10px; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 10px; }
/* 统一表格内容文字大小 */
.order-table :deep(.el-table__body),
.order-table :deep(.el-table__header) {
  font-size: 12px;
}
/* 审批追踪表格:表头背景 + 字号 */
.record-table :deep(.el-table__body),
.record-table :deep(.el-table__header) {
  font-size: 12px;
}
.record-table :deep(.el-table__header-wrapper thead tr:first-child > th) {
  background: #f5f7fa;
}
/* 表头第一行背景色(与交易内容标题行保持一致) */
.order-table :deep(.el-table__header-wrapper thead tr:first-child > th) {
  background: #f3f6fb !important;
  font-size: 14px !important;
}
/* 表头第二行高度与内边距 */
.order-table :deep(.el-table__header-wrapper thead tr:nth-child(2) > th) {
  height: 5px !important;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
}
/* 强制表格列固定布局,避免内容影响列宽 */
.order-table :deep(table) {
  table-layout: fixed;
  width: 100% !important;
}
/* 汇总行样式 */
.summary-row {
  font-weight: 600;
  color: #f56c6c;
}
.summary-total {
  text-align: right;
  font-weight: 600;
}
.summary-merged {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.summary-left {
  text-align: left;
}
.summary-left .count {
  color: #f56c6c;
  font-weight: 600;
}
.summary-right {
  text-align: right;
}
.order-table :deep(.summary-row) {
  background-color: #fafafa;
}
.order-table :deep(.summary-row td) {
  border-top: 2px solid #e4e7ed;
}
/* 表头第一行"详情"文字靠左对齐 */
.order-table :deep(.el-table__header-wrapper thead tr:first-child th:first-child) {
  text-align: left !important;
}
/* 表头图标样式 */
.header-icon {
  margin-right: 6px;
  color: #409eff;
  vertical-align: middle;
}
/* 分隔标题风格 */
.section-header {
  background: #f3f6fb;
  font-weight: 600;
  --el-border-color: #e4e7ed;
}
.section-header :deep(.el-descriptions__label) {
  background: #f3f6fb;
  border-right: none !important;
  width: 100%;
}
.section-header :deep(.el-descriptions__cell) {
  background: #f3f6fb;
}
.section-header :deep(.el-descriptions__content) {
  display: none !important;
  padding: 0 !important;
  border: 0 !important;
}
.section-icon {
  margin-right: 6px;
  color: #409eff;
}
/* 调整描述组件边框以便标题行颜色覆盖中间分隔线 */
:deep(.el-descriptions--border .el-descriptions__body .el-descriptions__table .el-descriptions__cell) {
  border-right: 1px solid var(--el-border-color);
}
.section-header :deep(.el-descriptions__cell) {
  border-right-color: transparent !important;
}
/* 强制 Element Plus 描述项的 label 宽度遵循 label-width(避免内容撑开) */
/* 兜底:即使内联样式被覆盖,也用 important 强制固定 */
.fixed-label :deep(.el-descriptions__label) {
  width: 180px !important;
  max-width: 180px !important;
}
.fixed-label :deep(.el-descriptions__content) {
  width: calc(50% - 180px) !important;
}
/* 强化第一行分隔标题的背景与边框覆盖 */
.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__cell),
.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__label),
.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__content) {
  background: #eef3fb !important;
  border-top-color: transparent !important;
  border-bottom-color: #dcdfe6 !important;
}
.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__label) {
  border-right-color: transparent !important;
}
/* 统一两个描述表格的列对齐(标签列固定宽度,内容列等分剩余宽度) */
.order-desc :deep(.el-descriptions__table) {
  table-layout: fixed;
  width: 100%;
}
/* 使用类选择器而非 nth-child,提升稳定性,确保每行两列严格对齐 */
.order-desc :deep(.el-descriptions__table tr:not(:first-child) .el-descriptions__label) {
  width: 180px !important;
  max-width: 180px !important;
  box-sizing: border-box;
}
.order-desc :deep(.el-descriptions__table tr:not(:first-child) .el-descriptions__content) {
  width: calc(50% - 180px) !important;
}
.desc-wrap {
  white-space: pre-wrap;
  line-height: 22px;
}
/* 审批追踪标签页样式 */
.approval-tabs {
  margin-top: 15px;
}
/* 操作按钮样式 */
.action-buttons {
  display: flex;
  justify-content: center;
  margin-top: 15px;
  .el-button {
    margin: 0 10px;
  }
}
/* 文件表格样式 */
.file-table {
  .file-name {
    display: flex;
    align-items: center;
    gap: 8px;
    .file-icon {
      font-size: 14px;
      &.doc {
        color: #409eff;
      }
      &.image {
        color: #67c23a;
      }
      &.video {
        color: #e6a23c;
      }
    }
    .file-text {
      color: #303133;
      font-size: 14px;
    }
  }
}
/* 审批意见区域 */
.approval-section {
  .section-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    margin-bottom: 15px;
    padding-bottom: 8px;
    border-bottom: 1px solid #e4e7ed;
  }
}
/* 审批操作按钮样式 */
.approval-actions {
  display: flex;
  justify-content: center;
  margin-top: 20px;
  gap: 20px;
  .back-btn {
    background: #ffffff;
    border: 1px solid #dcdfe6;
    color: #606266;
    &:hover {
      background: #f5f7fa;
      border-color: #c0c4cc;
    }
  }
  .approve-btn {
    background: #67c23a;
    border-color: #67c23a;
    color: #ffffff;
    &:hover {
      background: #85ce61;
      border-color: #85ce61;
    }
  }
  .reject-btn {
    background: #f56c6c;
    border-color: #f56c6c;
    color: #ffffff;
    &:hover {
      background: #f78989;
      border-color: #f78989;
    }
  }
}
/* 文件预览弹窗样式 */
.file-preview-dialog {
  :deep(.el-dialog__body) {
    padding: 0;
    height: 70vh;
  }
  .preview-content {
    height: 100%;
  }
}
</style>