<template>
|
<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>
|
|
<!-- 交易内容(紧随申请人信息,同卡片,复用分隔标题样式) -->
|
<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" 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-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>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
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'
|
|
const route = useRoute()
|
const router = useRouter()
|
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)' }
|
|
// 计算表格数据,添加汇总行
|
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]
|
})
|
|
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: '处理中',
|
},
|
],
|
}
|
|
Object.assign(detail, mockDetail)
|
})
|
|
// 与列表页保持一致的状态类型映射(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 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; }
|
.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; }
|
/* 统一表格内容文字大小 */
|
.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;
|
}
|
}
|
</style>
|