<!--
|
审批实例详情展示:基本信息 + 填报 + 流程 + 审批记录
|
-->
|
<template>
|
<view class="detail-body">
|
<view class="section-card">
|
<view class="section-head">
|
<text class="section-title">基本信息</text>
|
</view>
|
<view class="info-rows">
|
<view class="info-row">
|
<text class="info-label">业务单号</text>
|
<text class="info-value">{{ row.instanceNo || row.id || "—" }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">审批状态</text>
|
<u-tag :type="statusTagType(row.status)"
|
:text="statusLabel(row.status)"
|
size="mini" />
|
</view>
|
<view class="info-row">
|
<text class="info-label">模板名称</text>
|
<text class="info-value">{{ row.templateName || "—" }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">业务名称</text>
|
<text class="info-value">{{ row.businessName || "—" }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">申请人</text>
|
<text class="info-value">{{ row.applicantName || "—" }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">申请标题</text>
|
<text class="info-value">{{ row.title || "—" }}</text>
|
</view>
|
<view v-if="rejectReason"
|
class="info-row">
|
<text class="info-label">驳回原因</text>
|
<text class="info-value reject-text">{{ rejectReason }}</text>
|
</view>
|
<view class="info-row">
|
<text class="info-label">申请时间</text>
|
<text class="info-value">{{ formatDateTime(row.applyTime || row.createTime) }}</text>
|
</view>
|
<view v-if="row.finishTime"
|
class="info-row">
|
<text class="info-label">完成时间</text>
|
<text class="info-value">{{ formatDateTime(row.finishTime) }}</text>
|
</view>
|
</view>
|
</view>
|
<view class="section-card">
|
<view class="section-head">
|
<text class="section-title">{{ isSpecialApprovalType ? specialDetailTitle : "填报内容" }}</text>
|
</view>
|
<!-- 采购/报价/发货审批:展示业务单据详情(与 Web approve-list 一致) -->
|
<template v-if="isSpecialApprovalType">
|
<view v-if="businessDetailLoading"
|
class="empty-hint">加载中...</view>
|
<view v-else-if="!hasBusinessDetail"
|
class="empty-hint">{{ specialDetailEmptyText }}</view>
|
<view v-else
|
class="info-rows">
|
<view v-for="(item, idx) in specialDetailRows"
|
:key="`special-${idx}`"
|
class="info-row">
|
<text class="info-label">{{ item.label }}</text>
|
<text class="info-value">{{ item.value }}</text>
|
</view>
|
</view>
|
</template>
|
<template v-else>
|
<view v-if="displayFields.length"
|
class="info-rows">
|
<view v-for="field in displayFields"
|
:key="field.key"
|
class="info-row">
|
<text class="info-label">{{ field.label }}</text>
|
<text class="info-value">{{ displayFieldValue(field) }}</text>
|
</view>
|
<view v-for="(extra, idx) in moduleExtraRows"
|
:key="`extra-${idx}`"
|
class="info-row">
|
<text class="info-label">{{ extra.label }}</text>
|
<text class="info-value">{{ extra.value }}</text>
|
</view>
|
</view>
|
<view v-else
|
class="empty-hint">暂无填报内容</view>
|
</template>
|
</view>
|
<view class="section-card">
|
<view class="section-head">
|
<text class="section-title">审批流程({{ flowNodes.length }} 项)</text>
|
</view>
|
<view v-if="flowNodes.length"
|
class="flow-wrap">
|
<view v-for="(node, nodeIndex) in flowNodes"
|
:key="nodeIndex"
|
class="flow-node-block">
|
<view class="flow-node-card">
|
<view class="node-header">
|
<view class="node-level-badge">{{ node.levelNo }}</view>
|
<text class="node-level-text">第{{ levelLabel(node.levelNo) }}级</text>
|
<u-tag size="mini"
|
:type="node.approveType === 'OR' ? 'warning' : 'primary'"
|
:text="node.approveType === 'OR' ? '或签' : '会签'"
|
plain />
|
</view>
|
<view class="approver-list">
|
<view v-for="(a, aIdx) in node.approvers"
|
:key="aIdx"
|
class="approver-row">
|
<text class="approver-name">{{ a.approverName }}</text>
|
<u-tag v-if="a.taskStatus"
|
size="mini"
|
:type="taskStatusTagType(a.taskStatus)"
|
:text="taskStatusText(a.taskStatus)"
|
plain />
|
</view>
|
</view>
|
</view>
|
<view v-if="nodeIndex < flowNodes.length - 1"
|
class="flow-connector-line" />
|
</view>
|
</view>
|
<view v-else
|
class="empty-hint">暂无流程节点</view>
|
</view>
|
<view class="section-card">
|
<view class="section-head">
|
<text class="section-title">审批记录</text>
|
</view>
|
<view v-if="approvalRecords.length"
|
class="record-list">
|
<view v-for="(rec, index) in approvalRecords"
|
:key="rec.id ?? index"
|
class="record-item">
|
<view class="record-head">
|
<text class="record-operator">{{ rec.operatorName }}</text>
|
<u-tag size="mini"
|
:type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'error' : 'info'"
|
:text="recordActionLabel(rec.result)"
|
plain />
|
</view>
|
<text class="record-time">{{ rec.time }}</text>
|
<text class="record-opinion">{{ rec.opinion || "无意见" }}</text>
|
</view>
|
</view>
|
<view v-else
|
class="empty-hint">暂无审批记录</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref, watch } from "vue";
|
import { APPROVAL_MODULE_KEYS } from "../../../_utils/approvalModuleRegistry.js";
|
import { getQuotationList } from "@/api/salesManagement/salesQuotation";
|
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
|
import { getDeliveryDetailByShippingNo } from "@/api/collaborativeApproval/approvalProcess";
|
import {
|
computeLeaveDurationDisplay,
|
computeOvertimeHoursDisplay,
|
} from "../../../_utils/approvalModuleApplyExtras.js";
|
import { resolveInstanceFormPayload } from "../../../_utils/approvalModuleListSearch.js";
|
import {
|
businessStatusTagType,
|
businessStatusText,
|
displayFieldValue,
|
formatDateTime,
|
getRejectReasonFromRecords,
|
instanceStatusTagType,
|
instanceStatusText,
|
mapApprovalRecords,
|
mapTasksToFlowNodes,
|
recordActionLabel,
|
resolveInstanceDisplayFields,
|
taskStatusTagType,
|
taskStatusText,
|
} from "../../../_utils/approveListUtils.js";
|
|
const props = defineProps({
|
row: { type: Object, default: () => ({}) },
|
moduleKey: { type: String, default: "" },
|
});
|
|
const LEVEL_TEXT = [
|
"",
|
"一",
|
"二",
|
"三",
|
"四",
|
"五",
|
"六",
|
"七",
|
"八",
|
"九",
|
"十",
|
];
|
|
const isBusinessModule = computed(() =>
|
[
|
APPROVAL_MODULE_KEYS.LEAVE,
|
APPROVAL_MODULE_KEYS.OVERTIME,
|
APPROVAL_MODULE_KEYS.TRANSFER,
|
APPROVAL_MODULE_KEYS.REGULAR,
|
APPROVAL_MODULE_KEYS.WORK_HANDOVER,
|
].includes(props.moduleKey)
|
);
|
|
const statusLabel = status =>
|
isBusinessModule.value
|
? businessStatusText(status)
|
: instanceStatusText(status);
|
|
const statusTagType = status =>
|
isBusinessModule.value
|
? businessStatusTagType(status)
|
: instanceStatusTagType(status);
|
|
const displayFields = computed(() => resolveInstanceDisplayFields(props.row));
|
|
const moduleExtraRows = computed(() => {
|
const rows = [];
|
const { fields, formPayload } = resolveInstanceFormPayload(props.row);
|
const payload = { ...formPayload };
|
(fields || []).forEach(f => {
|
if (f?.key && payload[f.key] == null) payload[f.key] = f.value ?? "";
|
});
|
if (props.moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
|
const balance = payload.leaveBalanceDays;
|
if (balance != null && balance !== "") {
|
rows.push({ label: "假期余额", value: `${balance} 天` });
|
}
|
const days = computeLeaveDurationDisplay(fields, payload);
|
if (days) rows.push({ label: "请假时长", value: `${days} 天` });
|
}
|
if (props.moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
|
const hours = computeOvertimeHoursDisplay(fields, payload);
|
if (hours) rows.push({ label: "加班时长", value: `${hours} 小时` });
|
}
|
if (props.moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
|
const post = payload.originalPostName || payload.originalPost;
|
if (post) rows.push({ label: "原岗位", value: post });
|
}
|
return rows;
|
});
|
|
const flowNodes = computed(() => mapTasksToFlowNodes(props.row?.tasks));
|
|
const approvalRecords = computed(() => mapApprovalRecords(props.row?.records));
|
|
const rejectReason = computed(() =>
|
getRejectReasonFromRecords(props.row?.records)
|
);
|
|
const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
|
|
/** 采购/报价/发货审批:业务单据详情(与 Web approve-list 一致) */
|
const SPECIAL_BUSINESS_TYPES = [5, 6, 7];
|
|
const businessType = computed(() => Number(props.row?.businessType));
|
const isSpecialApprovalType = computed(() =>
|
SPECIAL_BUSINESS_TYPES.includes(businessType.value)
|
);
|
|
const businessDetail = ref({});
|
const businessProductList = ref([]);
|
const businessDetailLoading = ref(false);
|
|
const specialDetailTitle = computed(() => {
|
if (businessType.value === 5) return "采购详情";
|
if (businessType.value === 6) return "报价详情";
|
return "发货详情";
|
});
|
|
const specialDetailEmptyText = computed(() => {
|
if (businessType.value === 5) return "未查询到对应采购详情";
|
if (businessType.value === 6) return "未查询到对应报价详情";
|
return "未查询到对应发货详情";
|
});
|
|
const hasBusinessDetail = computed(() => {
|
const d = businessDetail.value || {};
|
if (businessType.value === 5) return Boolean(d.purchaseContractNumber);
|
if (businessType.value === 6) return Boolean(d.quotationNo);
|
if (businessType.value === 7) return Boolean(d.shippingInfo);
|
return false;
|
});
|
|
const formatMoney = val => `¥${Number(val ?? 0).toFixed(2)}`;
|
|
const joinDesc = parts => parts.filter(Boolean).join(" · ");
|
|
const specialDetailRows = computed(() => {
|
const d = businessDetail.value || {};
|
const products = businessProductList.value || [];
|
const rows = [];
|
const push = (label, value) => rows.push({ label, value: value || "--" });
|
|
if (businessType.value === 6) {
|
push("报价单号", d.quotationNo);
|
push("客户名称", d.customer);
|
push("业务员", d.salesperson);
|
push("报价日期", d.quotationDate);
|
push("有效期至", d.validDate);
|
push("付款方式", d.paymentMethod);
|
push("报价总额", formatMoney(d.totalAmount));
|
products.forEach((p, i) => {
|
push(
|
p.product || `产品${i + 1}`,
|
joinDesc([p.specification, p.unit, formatMoney(p.unitPrice)])
|
);
|
});
|
if (d.remark) push("备注", d.remark);
|
} else if (businessType.value === 5) {
|
push("采购合同号", d.purchaseContractNumber);
|
push("供应商名称", d.supplierName);
|
push("项目名称", d.projectName);
|
push("销售合同号", d.salesContractNo);
|
push("签订日期", d.executionDate);
|
push("录入日期", d.entryDate);
|
push("付款方式", d.paymentMethod);
|
push("合同金额", formatMoney(d.contractAmount));
|
products.forEach((p, i) => {
|
push(
|
p.productCategory || `产品${i + 1}`,
|
joinDesc([
|
p.specificationModel,
|
p.quantity != null && p.quantity !== ""
|
? `${p.quantity}${p.unit || ""}`
|
: "",
|
`含税单价 ${formatMoney(p.taxInclusiveUnitPrice)}`,
|
`含税总价 ${formatMoney(p.taxInclusiveTotalPrice)}`,
|
])
|
);
|
});
|
} else if (businessType.value === 7) {
|
const info = d.shippingInfo || {};
|
push("销售订单", info.salesContractNo);
|
push("发货订单号", info.shippingNo);
|
push("客户名称", info.customerName);
|
push("发货类型", info.type);
|
push("发货日期", info.shippingDate);
|
push("审核状态", info.status);
|
push("发货车牌号", info.shippingCarNumber);
|
push("快递公司", info.expressCompany);
|
push("快递单号", info.expressNumber);
|
products.forEach((p, i) => {
|
push(
|
p.productName || `产品${i + 1}`,
|
joinDesc([
|
p.batchNo ? `批号 ${p.batchNo}` : "",
|
p.specificationModel,
|
p.deliveryQuantity != null ? `发货数量 ${p.deliveryQuantity}` : "",
|
])
|
);
|
});
|
}
|
return rows;
|
});
|
|
async function loadBusinessDetail() {
|
const row = props.row || {};
|
const type = Number(row.businessType);
|
businessDetail.value = {};
|
businessProductList.value = [];
|
if (!SPECIAL_BUSINESS_TYPES.includes(type)) return;
|
|
businessDetailLoading.value = true;
|
try {
|
if (type === 6) {
|
if (!row.quotationNo) return;
|
const res = await getQuotationList({ quotationNo: row.quotationNo });
|
const hit = res?.data?.records?.[0] || {};
|
businessDetail.value = hit;
|
businessProductList.value = hit.products || [];
|
} else if (type === 5) {
|
if (!row.purchaseContractNumber) return;
|
const res = await getPurchaseByCode({
|
purchaseContractNumber: row.purchaseContractNumber,
|
approvalInstanceId: row.id,
|
});
|
const hit = res?.data || res || {};
|
businessDetail.value = hit;
|
businessProductList.value = hit.productData || [];
|
} else if (type === 7) {
|
if (!row.shippingNo) return;
|
const res = await getDeliveryDetailByShippingNo({
|
shippingNo: row.shippingNo,
|
});
|
const hit = res?.data || res || {};
|
businessDetail.value = hit;
|
businessProductList.value = hit.shippingProductDetailDtoList || [];
|
}
|
} catch {
|
businessDetail.value = {};
|
businessProductList.value = [];
|
} finally {
|
businessDetailLoading.value = false;
|
}
|
}
|
|
watch(
|
() => [props.row?.id, props.row?.businessType],
|
() => {
|
loadBusinessDetail();
|
},
|
{ immediate: true }
|
);
|
</script>
|
|
<style scoped lang="scss">
|
$primary: #2979ff;
|
$text: #1f2d3d;
|
$text-muted: #909399;
|
|
.detail-body {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.section-card {
|
background: #fff;
|
border-radius: 12px;
|
overflow: hidden;
|
box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
|
}
|
|
.section-head {
|
padding: 12px 16px;
|
border-bottom: 1px solid #f2f4f7;
|
}
|
|
.section-title {
|
font-size: 15px;
|
font-weight: 600;
|
color: $text;
|
padding-left: 10px;
|
border-left: 3px solid $primary;
|
line-height: 1.2;
|
}
|
|
.info-rows {
|
padding: 4px 16px 12px;
|
}
|
|
.info-row {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 12px;
|
padding: 10px 0;
|
border-bottom: 1px solid #f5f6f8;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
}
|
|
.info-label {
|
flex-shrink: 0;
|
font-size: 14px;
|
color: $text-muted;
|
min-width: 72px;
|
}
|
|
.info-value {
|
flex: 1;
|
font-size: 14px;
|
color: $text;
|
text-align: right;
|
word-break: break-all;
|
}
|
|
.reject-text {
|
color: #f56c6c;
|
}
|
|
.flow-wrap {
|
padding: 10px 16px 14px;
|
}
|
|
.flow-node-block {
|
display: flex;
|
flex-direction: column;
|
align-items: stretch;
|
}
|
|
.flow-node-card {
|
background: #fafbfd;
|
border: 1px solid #e8eef5;
|
border-radius: 10px;
|
padding: 12px;
|
}
|
|
.node-header {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 10px;
|
}
|
|
.node-level-badge {
|
width: 26px;
|
height: 26px;
|
border-radius: 8px;
|
background: $primary;
|
color: #fff;
|
font-size: 13px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.node-level-text {
|
flex: 1;
|
font-size: 14px;
|
font-weight: 600;
|
color: $text;
|
}
|
|
.approver-list {
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
}
|
|
.approver-row {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
}
|
|
.approver-name {
|
font-size: 13px;
|
color: #606266;
|
}
|
|
.flow-connector-line {
|
width: 2px;
|
height: 12px;
|
background: #d0dff0;
|
margin: 4px auto;
|
}
|
|
.record-list {
|
padding: 8px 16px 14px;
|
}
|
|
.record-item {
|
padding: 10px 0;
|
border-bottom: 1px solid #f0f2f5;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
}
|
|
.record-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
}
|
|
.record-operator {
|
font-size: 14px;
|
font-weight: 600;
|
color: $text;
|
}
|
|
.record-time {
|
display: block;
|
margin-top: 4px;
|
font-size: 12px;
|
color: $text-muted;
|
}
|
|
.record-opinion {
|
display: block;
|
margin-top: 6px;
|
font-size: 13px;
|
color: #606266;
|
line-height: 1.5;
|
}
|
|
.empty-hint {
|
padding: 12px 16px 16px;
|
font-size: 13px;
|
color: $text-muted;
|
}
|
</style>
|