<!--
|
审批实例详情展示:基本信息 + 填报 + 流程 + 审批记录
|
-->
|
<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">填报内容</text>
|
</view>
|
<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>
|
</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 } from "vue";
|
import { APPROVAL_MODULE_KEYS } from "../../../_utils/approvalModuleRegistry.js";
|
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);
|
</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>
|