liding
5 天以前 ac5dac41df2b75de998a79701d846b33a47ce64c
feat(approve): 优化审批详情面板显示逻辑并增加附件功能

- 实现申请人名称的动态显示逻辑,支持从表单字段中获取申请人信息
- 添加附件展示区域,支持查看和下载审批相关的文件附件
- 集成选项源缓存机制,提升下拉选择字段的显示性能
- 完善审批实例创建逻辑,支持从表单载荷中解析申请人信息
- 修复审批记录中存储blob列表的数据映射问题
已修改2个文件
109 ■■■■■ 文件已修改
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -362,10 +362,36 @@
  });
}
/** 从模板字段中找到申请人字段 */
function findApplicantField(fields) {
  if (!Array.isArray(fields)) return null;
  return (
    fields.find((f) => String(f?.label || "").includes("申请人")) ||
    fields.find((f) => f?.type === "select" && f?.optionSource === "user") ||
    null
  );
}
/** 从 formPayload 的申请人字段解析 applicantId / applicantName */
function resolveApplicantFromFormPayload(payload, fields) {
  const field = findApplicantField(fields);
  if (!field) return {};
  const val = payload?.[field.key];
  if (val == null || val === "") return {};
  const result = { applicantId: val };
  const opts = field.options;
  if (Array.isArray(opts) && opts.length) {
    const hit = opts.find((o) => String(o.value) === String(val));
    if (hit?.label) result.applicantName = hit.label;
  }
  return result;
}
/** 组装保存/更新审批 DTO */
export function buildInstanceDto({ submitForm, activeTemplate, userStore, flowNodes, existingRow }) {
  const payload = submitForm?.formPayload || {};
  const tpl = activeTemplate || {};
  const fields = tpl.fields || submitForm?.formFieldDefs || [];
  const title =
    String(payload.summary || payload.title || "").trim() ||
    tpl.label ||
@@ -378,6 +404,7 @@
    templateId,
  });
  const isUpdate = Boolean(instanceId);
  const fromPayload = resolveApplicantFromFormPayload(payload, fields);
  const dto = {
    templateId,
@@ -408,8 +435,9 @@
  } else {
    dto.status = submitForm?.saveStatusApi || "PENDING";
    dto.currentLevel = 1;
    dto.applicantId = userStore?.id;
    dto.applicantName = userStore?.nickName || userStore?.name || "";
    dto.applicantId = fromPayload.applicantId || submitForm?.applicantId || userStore?.id;
    dto.applicantName = fromPayload.applicantName || submitForm?.applicantName
      || (fromPayload.applicantId ? "" : (userStore?.nickName || userStore?.name || ""));
  }
  return dto;
}
@@ -502,6 +530,7 @@
    approvalRecords,
    rejectReason:
      approvalRecords.find((r) => r.result === "rejected")?.opinion || "",
    storageBlobVOList: row.storageBlobVOList || row.storageBlobDTOs || row.attachmentList || [],
  };
}
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -16,7 +16,7 @@
          </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="申请人名称">{{ 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>
@@ -35,18 +35,30 @@
        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,
  approvalStatusLabel,
  approvalStatusTagType,
  resolveInstanceFormFields,
  formatFieldDisplayValue,
} from "../approveListConstants.js";
import FormPayloadFields from "./FormPayloadFields.vue";
@@ -54,7 +66,63 @@
  row: { type: Object, default: () => ({}) },
});
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
  );
});
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>
@@ -82,4 +150,8 @@
.reject-text {
  color: var(--el-color-danger);
}
.file-tag {
  margin: 0 8px 8px 0;
  cursor: pointer;
}
</style>