liding
2026-06-05 ac5dac41df2b75de998a79701d846b33a47ce64c
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -1,82 +1,145 @@
<!-- 统一审批:业务摘要 -->
<!-- 审批详情:基础信息 + 填报内容 -->
<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;
@@ -84,11 +147,11 @@
  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>