| | |
| | | <!-- 统一审批:业务摘要 --> |
| | | <!-- 审批详情:基础信息 + 填报内容 --> |
| | | <template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="业务单号">{{ row.bizId || row.id || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="审批状态"> |
| | | <el-tag :type="approvalStatusTagType(row.approvalStatus)" size="small" effect="plain"> |
| | | {{ approvalStatusLabel(row.approvalStatus) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="审批类型"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)"> |
| | | {{ approvalTypeLabel(row.approvalType) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="审批方式"> |
| | | <span class="approval-method-text">{{ approvalModeLabel(row.approvalMode) }}</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="申请人编号">{{ row.applicantNo || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请人名称">{{ row.applicantName || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请摘要" :span="2">{{ row.summary || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item v-if="row.rejectReason" label="驳回原因" :span="2"> |
| | | <span class="reject-text">{{ row.rejectReason }}</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建时间" :span="2">{{ row.createTime || "—" }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div class="approve-detail-panel"> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title">基本信息</div> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="业务单号">{{ row.bizId || row.id || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="审批状态"> |
| | | <el-tag :type="approvalStatusTagType(row.approvalStatus)" size="small" effect="plain"> |
| | | {{ approvalStatusLabel(row.approvalStatus) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="审批类型"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)"> |
| | | {{ approvalTypeLabel(row.approvalType) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="申请人编号">{{ row.applicantNo || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请人名称">{{ applicantNameDisplay }}</el-descriptions-item> |
| | | <el-descriptions-item label="申请摘要">{{ row.summary || "—" }}</el-descriptions-item> |
| | | <el-descriptions-item v-if="row.rejectReason" label="驳回原因" :span="2"> |
| | | <span class="reject-text">{{ row.rejectReason }}</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建时间" :span="2"> |
| | | {{ formatDisplayTime(row.createTime) }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </div> |
| | | |
| | | <template v-if="extraFields.length"> |
| | | <el-divider content-position="left">填报内容</el-divider> |
| | | <el-descriptions :column="2" border size="small"> |
| | | <el-descriptions-item v-for="item in extraFields" :key="item.key" :label="item.label"> |
| | | {{ item.display }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </template> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title">填报内容</div> |
| | | <FormPayloadFields |
| | | :fields="formResolved.fields" |
| | | :form-payload="formResolved.formPayload" |
| | | readonly |
| | | /> |
| | | </div> |
| | | |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title">附件</div> |
| | | <template v-if="attachmentFiles.length"> |
| | | <el-tag v-for="(f, i) in attachmentFiles" :key="i" class="file-tag" type="info" @click="openFile(f)"> |
| | | {{ f.name || f.originalFilename }} |
| | | </el-tag> |
| | | </template> |
| | | <el-empty v-else description="暂无附件" :image-size="48" /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { computed, onMounted, watch } from "vue"; |
| | | import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js"; |
| | | import { useSelectOptionSources } from "../../approve-template/useSelectOptionSources.js"; |
| | | import { |
| | | approvalTypeLabel, |
| | | approvalTypeStyle, |
| | | approvalModeLabel, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | SUBMIT_TEMPLATES, |
| | | resolveInstanceFormFields, |
| | | formatFieldDisplayValue, |
| | | } from "../approveListConstants.js"; |
| | | import FormPayloadFields from "./FormPayloadFields.vue"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const extraFields = computed(() => { |
| | | const payload = props.row?.formPayload || {}; |
| | | const tpl = Object.values(SUBMIT_TEMPLATES).find((t) => t.approvalType === props.row?.approvalType); |
| | | if (!tpl?.fields?.length) { |
| | | return Object.keys(payload) |
| | | .filter((k) => k !== "summary" && payload[k] != null && payload[k] !== "") |
| | | .map((k) => ({ key: k, label: k, display: formatValue(payload[k]) })); |
| | | } |
| | | return tpl.fields |
| | | .map((f) => { |
| | | const val = payload[f.key]; |
| | | if (val == null || val === "" || (Array.isArray(val) && !val.length)) return null; |
| | | let display = formatValue(val); |
| | | if (f.type === "select" && f.options) { |
| | | display = f.options.find((o) => o.value === val)?.label || display; |
| | | } |
| | | return { key: f.key, label: f.label, display }; |
| | | }) |
| | | .filter(Boolean); |
| | | const { ensureForFields, getDisplayLabel } = useSelectOptionSources(); |
| | | |
| | | const formResolved = computed(() => resolveInstanceFormFields(props.row)); |
| | | |
| | | const applicantField = computed(() => { |
| | | const fields = formResolved.value.fields || []; |
| | | return ( |
| | | fields.find((f) => String(f?.label || "").includes("申请人")) || |
| | | fields.find((f) => f?.type === "select" && f?.optionSource === "user") || |
| | | null |
| | | ); |
| | | }); |
| | | |
| | | function formatValue(val) { |
| | | if (Array.isArray(val)) return val.join(" 至 "); |
| | | return String(val); |
| | | const applicantNameDisplay = computed(() => { |
| | | if (!applicantField.value) return props.row?.applicantName || "—"; |
| | | const val = formResolved.value.formPayload?.[applicantField.value.key]; |
| | | if (val == null || val === "") return props.row?.applicantName || "—"; |
| | | if (applicantField.value.optionSource && applicantField.value.optionSource !== "static") { |
| | | const label = getDisplayLabel(applicantField.value, val); |
| | | if (label && label !== "—") return label; |
| | | } |
| | | const formatted = formatFieldDisplayValue(applicantField.value, val); |
| | | if (formatted && formatted !== "—") return formatted; |
| | | return props.row?.applicantName || "—"; |
| | | }); |
| | | |
| | | async function loadOptionCaches() { |
| | | await ensureForFields(formResolved.value.fields || []); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadOptionCaches(); |
| | | }); |
| | | |
| | | watch( |
| | | () => formResolved.value.fields, |
| | | () => { |
| | | loadOptionCaches(); |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | const attachmentFiles = computed(() => { |
| | | const list = |
| | | props.row?.storageBlobVOList || |
| | | props.row?.storageBlobDTOs || |
| | | props.row?.storageBlobDTOS || |
| | | props.row?.storageBlobVOS || |
| | | props.row?.attachmentList || |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | | function openFile(f) { |
| | | const url = f?.url || f?.previewURL || f?.downloadURL; |
| | | if (url) window.open(url, "_blank"); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .approve-detail-panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | } |
| | | .detail-block-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | margin: 0 0 12px; |
| | | padding-left: 10px; |
| | | border-left: 3px solid var(--el-color-primary); |
| | | line-height: 1.4; |
| | | } |
| | | .approve-type-cell { |
| | | display: inline-block; |
| | | padding: 2px 10px; |
| | |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | .approval-method-text { |
| | | color: var(--el-color-danger); |
| | | font-weight: 500; |
| | | } |
| | | .reject-text { |
| | | color: var(--el-color-danger); |
| | | } |
| | | .file-tag { |
| | | margin: 0 8px 8px 0; |
| | | cursor: pointer; |
| | | } |
| | | </style> |