Bang Hu
2025-09-03 a4188f41f3bc981fe8fa92c253a1195b76be04dd
src/views/tradeManage/confirm/index.vue
@@ -170,16 +170,29 @@
               {{ formatFileSize(row.size) }}
             </template>
           </el-table-column>
           <el-table-column label="操作" width="100">
           <el-table-column label="操作" width="180">
             <template #default="{ row }">
               <el-button
                 type="text"
                 size="small"
                 class="preview-btn"
                 @click="handlePreview(row)"
               >
                 预览
               </el-button>
               <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>
@@ -191,49 +204,24 @@
      <div class="title">交易信息备注</div>
      <el-table :data="form.items" border class="mt10 remark-table">
        <el-table-column label="详情" prop="name" min-width="200" />
        <el-table-column label="授权开始时间" width="200">
          <template #default="{ row, $index }">
            <el-date-picker
              v-model="form.items[$index].start"
              type="date"
              value-format="YYYY-MM-DD"
              placeholder="选择日期"
              size="small"
              style="width: 100%"
            />
          </template>
        </el-table-column>
        <el-table-column label="授权结束时间" width="200">
          <template #default="{ row, $index }">
            <div class="end-time-wrapper">
              <el-date-picker
                v-model="form.items[$index].end"
                type="date"
                value-format="YYYY-MM-DD"
                placeholder="选择日期"
                size="small"
                style="width: 100%"
                :disabled="form.items[$index].forever"
              />
              <el-checkbox
                v-model="form.items[$index].forever"
                class="forever-checkbox"
                @change="handleForeverChange($index)"
              >
                永久
              </el-checkbox>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="备注" min-width="300">
          <template #default="{ row, $index }">
            <el-input
              v-model="form.items[$index].remark"
              placeholder="请输入备注"
              size="small"
            />
          </template>
        </el-table-column>
                 <el-table-column label="授权开始时间" width="200">
           <template #default="{ row, $index }">
             <span>{{ form.items[$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="form.items[$index].forever" class="forever-text">永久</span>
               <span v-else>{{ form.items[$index].end || '-' }}</span>
             </div>
           </template>
         </el-table-column>
         <el-table-column label="备注" min-width="300">
           <template #default="{ row, $index }">
             <span>{{ form.items[$index].remark || '-' }}</span>
           </template>
         </el-table-column>
      </el-table>
      <div class="action-buttons">
        <el-button @click="goBack">返回</el-button>
@@ -247,11 +235,15 @@
import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Document, User, Goods, List } from '@element-plus/icons-vue'
import { fetchOrderDetail, confirmTrade } from '@/api/tradeManage'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import orderApi from '@/api/orderApi'
import pointsApi from '@/api/pointsApi' // 新增积分API导入
import { useUserInfo } from '@/stores/modules/userInfo'
import createAxios from '@/utils/axios'
const route = useRoute()
const router = useRouter()
const userStore = useUserInfo()
const detail = reactive<any>({ items: [] })
const form = reactive<any>({ items: [] })
const fileList = ref<any[]>([])
@@ -277,123 +269,227 @@
  return [...detail.items, summaryRow]
})
onMounted(async () => {
  // 使用前端模拟数据以便开发 UI(不改动后端服务)
  const mockDetail = {
    orderNo: '4348442557619205545',
    resourceTypeName: '软件产品',
    status: 'WAIT_CONFIRM',
    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 statusServerToUi: Record<string, string> = {
  '待上传文件': 'WAIT_UPLOAD',
  '待授权': 'WAIT_AUTHORIZE',
  '待交易确认': 'WAIT_CONFIRM',
  '已完成': 'COMPLETED',
  '已评价': 'EVALUATED',
}
  Object.assign(detail, mockDetail)
  form.items = (detail.items || []).map((item: any, index: number) => ({
    name: item.name,
    start: '2025-06-01',
    end: index === 0 || index === 3 ? '' : '2025-06-01',
    forever: index === 0 || index === 3,
    remark: index === 3 ? '开通管理员账号1个,账号admin' : '开通管理员账号1个,账号admin,登录管理员账号可管理普通用户'
  }))
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
  
  // 添加模拟文件数据用于展示
  fileList.value = [
    {
      name: '签字盖章文件.pdf',
      size: 2621440, // 2.5MB
      uid: '1',
      status: 'success'
    },
    {
      name: 'API Keys.txt',
      size: 354, // 354 Bytes
      uid: '2',
      status: 'success'
  try {
    const res = (await orderApi.getOrderDetail(orderId)) as any
    const data = res?.data || {}
    const statusName: string = data.orderStatus || ''
    const uiStatus = statusServerToUi[statusName] || 'INFO'
    // 映射订单详情头部信息
    const head = {
      orderNo: data.orderId,
      resourceTypeName: '软件产品',
      status: uiStatus,
      statusName,
      applyTime: formatDateTime(data.applyTime),
      unitName: data.unitName || '-',
      userName: data.userName || '-',
      userAccount: data.userAccount || '-',
      userDept: data.userDept || '-',
      userPhone: data.userPhone || '-',
      productName: data.productName || '-',
      supplier: data.providerName || '-',
      industry: data.industry || '-',
      projectUnit: data.projectUnit || '-',
      productType: data.productType || '-',
      productDesc: data.productDesc || '-',
    }
  ]
  // 注释掉原有的API调用,使用模拟数据
  // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any
  // Object.assign(detail, data || {})
  // form.items = (detail.items || []).map(() => ({ start: '', end: '', forever: false, remark: '' }))
    // 明细项映射
    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(),
    })
    // 初始化表单数据
    form.items = (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('获取订单详情失败')
  }
})
const goBack = () => router.back()
const submit = async () => {
  // 模拟提交成功响应
  const mockResponse = { code: 200 }
  // 注释掉原有的API调用,使用模拟数据
  // const { code } = (await confirmTrade({ id: route.params.id, ...form })) as any
  if (mockResponse.code === 200) {
    ElMessage.success('提交成功')
    router.back()
  try {
    const orderId = String(route.params.id || '')
    const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined
    if (!orderId || !userId) {
      ElMessage.error('订单ID或用户ID不能为空')
      return
    }
    // 检查订单是否涉及积分扣减
    const hasPointsDeduction = detail.items && detail.items.some((item: any) => Number(item.pricePoint || 0) > 0)
    if (hasPointsDeduction) {
      // 计算需要扣减的积分总额
      const totalPointsToDeduct = detail.items.reduce((sum: number, item: any) => {
        return sum + (Number(item.pricePoint || 0) * Number(item.quantity || 0))
      }, 0)
      if (totalPointsToDeduct > 0) {
        // 获取当前用户积分余额
        try {
          const userPointsRes = await pointsApi.getUserPoints(userId)
          if (userPointsRes.code !== 200 || !userPointsRes.data) {
            ElMessage.error('获取积分余额失败')
            return
          }
          const currentPoints = userPointsRes.data.balance || 0
          if (currentPoints < totalPointsToDeduct) {
            ElMessage.error(`积分余额不足!当前积分:${currentPoints.toLocaleString()},需要积分:${totalPointsToDeduct.toLocaleString()}`)
            return
          }
          // 积分余额充足,继续执行
          console.log(`积分余额检查通过:当前${currentPoints},需要${totalPointsToDeduct}`)
        } catch (error) {
          console.error('获取积分余额失败:', error)
          ElMessage.error('获取积分余额失败,请重试')
          return
        }
      }
    }
    // 确认操作
    await ElMessageBox.confirm('确定要确认交易?', '确认操作', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
    // 如果涉及积分扣减,先进行积分扣减和流水记录
    if (hasPointsDeduction) {
      const totalPointsToDeduct = detail.items.reduce((sum: number, item: any) => {
        return sum + (Number(item.pricePoint || 0) * Number(item.quantity || 0))
      }, 0)
      if (totalPointsToDeduct > 0) {
        try {
          // 获取用户单位ID
          const unitId = userStore.getUserInfo?.unitId || userStore.getUserInfo?.unitId || ''
          // 获取当前订购的产品名称作为备注(使用产品名称,不是套件名称)
          const productNames = detail.productName || '订单交易扣减积分'
          // 先执行积分扣减
          const deductRes: any = await pointsApi.deductPointsByFlow(
            userId.toString(),
            unitId,
            totalPointsToDeduct,
            orderId,
            productNames || '订单交易扣减积分',  // 使用产品名称作为备注
            '积分交易'  // 数据类别
          )
          if (!deductRes || deductRes.code !== 200) {
            ElMessage.error(deductRes?.msg || deductRes?.message || '积分扣减失败,交易确认终止')
            return
          }
          console.log(`积分扣减成功:${totalPointsToDeduct}`)
        } catch (error) {
          console.error('积分扣减失败:', error)
          ElMessage.error('积分扣减失败,交易确认终止')
          return
        }
      }
    }
    // 积分扣减成功后,更新订单状态进入下一个状态
    await orderApi.updateOrderStatusToNext(orderId)
    ElMessage.success('交易确认成功')
    router.back()
  } catch (error) {
    if (error !== 'cancel') {
      console.error('交易确认失败:', error)
      ElMessage.error('交易确认失败')
    }
  }
}
@@ -480,17 +576,202 @@
  return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 文件预览处理
const handlePreview = (file: any) => {
  ElMessage.info(`预览文件:${file.name}`)
// 判断文件是否可预览
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 handleForeverChange = (index: number) => {
  if (form.items[index].forever) {
    form.items[index].end = ''
// 判断文件是否已上传成功
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
        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('下载失败,请重试')
  }
}
// 症结与修复说明:
// 1) Element Plus 的 el-table 子列 width 百分比是相对于表格容器的像素宽度计算,但只有在表格容器有明确宽度时才生效。
@@ -694,6 +975,36 @@
  text-align: left !important;
}
/* 文件操作按钮样式 */
.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%;
@@ -701,8 +1012,9 @@
    display: flex;
    align-items: center;
    gap: 10px;
    .forever-checkbox {
      margin-left: 10px;
    .forever-text {
      color: #409eff;
      font-weight: 500;
    }
  }
}