特色功能:丰富报销清单并添加审批流程详情——新增功能以丰富报销清单行内容,为费用和差旅报销提供审批流程详情。——引入新的实用函数来处理审批流程节点和汇总信息。——更新组件以利用丰富后的审批流程数据,从而更好地展示审批进度。
已修改8个文件
332 ■■■■ 文件已修改
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
@@ -123,19 +123,7 @@
  return "warning";
}
export function formatApprovalFlowSummary(row) {
  const nodes = row?.approvalFlowNodes || [];
  if (!nodes.length) return "—";
  return nodes
    .map((n, i) => {
      const name = (n.approverName || "").trim() || `节点${i + 1}`;
      if (n.nodeStatus === "finish") return `${name}✓`;
      if (n.nodeStatus === "error") return `${name}✗`;
      if (n.nodeStatus === "process") return `${name}…`;
      return name;
    })
    .join(" → ");
}
export { formatApprovalFlowSummary } from "../shared/finReimbursementMappers.js";
export function resolveApprovalRoles(amount, expenseCategory) {
  const amt = Number(amount) || 0;
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -335,7 +335,10 @@
      <div v-loading="detailLoading">
      <DetailPanel :row="detailRow" />
      <el-divider content-position="left">审批流程</el-divider>
      <ApprovalFlowProgress :nodes="detailRow.approvalFlowNodes" :current-index="detailRow.currentNodeIndex ?? 0" />
      <ApprovalFlowProgress
        :nodes="detailRow.approvalFlowProgressNodes ?? detailRow.approvalFlowNodes"
        :current-index="detailRow.currentNodeIndex ?? 0"
      />
      <el-divider content-position="left">审批记录</el-divider>
      <el-timeline v-if="detailRow.approvalRecords?.length">
        <el-timeline-item
@@ -366,7 +369,7 @@
      <DetailPanel :row="approveDialog.row" />
      <el-divider content-position="left">流程进度</el-divider>
      <ApprovalFlowProgress
        :nodes="approveDialog.row?.approvalFlowNodes"
        :nodes="approveDialog.row?.approvalFlowProgressNodes ?? approveDialog.row?.approvalFlowNodes"
        :current-index="approveDialog.row?.currentNodeIndex ?? 0"
      />
      <el-form label-width="100px" class="mt16">
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
@@ -14,6 +14,7 @@
  buildFinReimbursementListParams,
  canDeleteReimbursementRow,
  canEditReimbursementRow,
  enrichReimbursementListRowsWithApprovalFlow,
  filterRowsByReimbursementType,
  FIN_REIMBURSEMENT_TYPE,
  mapCostReimbursementRow,
@@ -106,11 +107,19 @@
        })
      );
      const { records, total } = unwrapFinReimbursementPage(res);
      allRows.value = filterRowsByReimbursementType(
      const filtered = filterRowsByReimbursementType(
        records,
        FIN_REIMBURSEMENT_TYPE.COST
      ).map(mapCostReimbursementRow);
      page.total = total;
      );
      let mapped = filtered.map(mapCostReimbursementRow);
      mapped = await enrichReimbursementListRowsWithApprovalFlow(
        mapped,
        FIN_REIMBURSEMENT_TYPE.COST
      );
      allRows.value = mapped;
      const dropped = records.length - filtered.length;
      page.total =
        dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
    } catch {
      allRows.value = [];
      page.total = 0;
src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue
@@ -6,7 +6,7 @@
    <el-divider content-position="left">流程进度</el-divider>
    <ApprovalFlowProgress
      :nodes="reimburseRow.approvalFlowNodes"
      :nodes="reimburseRow.approvalFlowProgressNodes ?? reimburseRow.approvalFlowNodes"
      :current-index="reimburseRow.currentNodeIndex ?? 0"
    />
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js
@@ -127,10 +127,17 @@
  const approvalRecords = tasks.length
    ? mapTasksToApprovalRecords(tasks)
    : mapRecordsFromApi(source.records || source.approvalRecords);
  const approvalFlowNodes = tasks.length
  /** 表单编辑回显:保留 nodes 映射(含 approverId),勿用 tasks 覆盖 */
  const approvalFlowNodes = Array.isArray(mapped.approvalFlowNodes)
    ? mapped.approvalFlowNodes
    : [];
  /** 详情/进度条展示:有 tasks 时用任务状态节点 */
  const approvalFlowProgressNodes = tasks.length
    ? mapTasksToApprovalFlowNodes(tasks)
    : mapped.approvalFlowNodes || [];
  const currentNodeIndex = computeApprovalFlowCurrentIndex(approvalFlowNodes);
    : approvalFlowNodes;
  const currentNodeIndex = computeApprovalFlowCurrentIndex(
    approvalFlowProgressNodes.length ? approvalFlowProgressNodes : approvalFlowNodes
  );
  const rejectReason =
    approvalRecords.find(r => r.result === "rejected")?.opinion ||
    source.rejectReason ||
@@ -145,6 +152,7 @@
    approvalRecords,
    records: tasks.length ? tasks : source.records,
    approvalFlowNodes,
    approvalFlowProgressNodes,
    currentNodeIndex,
    rejectReason,
    flowNodes: tasks.length ? mapTasksToFlowNodes(tasks) : mapped.flowNodes,
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
@@ -1,8 +1,10 @@
import dayjs from "dayjs";
import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js";
import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js";
import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js";
import { mapTasksToFlowNodes } from "../../ApproveManage/approve-list/approveListConstants.js";
import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js";
/** 报销类型:1-差旅报销,2-费用报销 */
@@ -130,8 +132,18 @@
  } else {
    mapped = raw || {};
  }
  let formApprovalFlowNodes = mapNodesToFormFlow(resolveRowApiNodes(raw));
  if (!formApprovalFlowNodes.length && Array.isArray(raw?.tasks) && raw.tasks.length) {
    formApprovalFlowNodes = mapNodesToFormFlow(mapTasksToFlowNodes(raw.tasks));
  }
  const enriched = applyFinReimbursementDetailEnrichment(mapped, raw);
  return {
    ...applyFinReimbursementDetailEnrichment(mapped, raw),
    ...enriched,
    approvalFlowNodes: formApprovalFlowNodes.length
      ? formApprovalFlowNodes
      : enriched.approvalFlowNodes,
    reimbursementType: type,
    reimbursementTypeLabel: reimbursementTypeLabel(type),
    moduleKey: getModuleKeyByReimbursementType(type),
@@ -324,9 +336,10 @@
        ? row.travel
        : travel,
    details,
    nodes: row.nodes || [],
    approvalFlowNodes: mapNodesToFormFlow(row.nodes),
    nodes: resolveRowApiNodes(row),
    approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)),
    tasks: row.tasks || [],
    approvalFlowSummary: buildApprovalFlowSummaryForRow(row),
  };
  return base;
}
@@ -335,6 +348,8 @@
export function mapCostReimbursementRow(row) {
  if (!row) return {};
  const details = Array.isArray(row.details) ? row.details : [];
  const apiNodes = resolveRowApiNodes(row);
  const approvalFlowNodes = mapNodesToFormFlow(apiNodes);
  return {
    ...row,
@@ -351,7 +366,7 @@
    reimburseReason: row.reason || "",
    expenseCategory: row.expenseType || "",
    applyAmount: row.applyAmount,
    applyTime: row.createTime || "",
    applyTime: formatReimbursementDateTime(row.createTime),
    payee: row.payeeName || "",
    payeeAccount: row.payeeAccount || "",
    bankBranch: row.payeeBank || "",
@@ -363,9 +378,14 @@
      expenseSubject: d.expenseCategory,
    })),
    details,
    nodes: row.nodes || [],
    approvalFlowNodes: mapNodesToFormFlow(row.nodes),
    nodes: apiNodes,
    approvalFlowNodes,
    tasks: row.tasks || [],
    approvalFlowSummary: buildApprovalFlowSummaryForRow({
      ...row,
      nodes: apiNodes,
      approvalFlowNodes,
    }),
  };
}
@@ -385,17 +405,155 @@
  return hit?.label || category || "";
}
/** 列表/详情行上的审批节点(listPage 常不返回,需详情补全) */
export function resolveRowApiNodes(row) {
  if (!row || typeof row !== "object") return [];
  const list =
    row.nodes ||
    row.flowNodes ||
    row.approveNodes ||
    row.finReimbursementNodes ||
    row.nodeList ||
    row.reimbursementNodeList ||
    [];
  return Array.isArray(list) ? list : [];
}
function sortFlowNodesByLevel(nodes = []) {
  return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => {
    const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0);
    const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0);
    return la - lb;
  });
}
function formatApiNodeApproverLabel(node, index) {
  if (!node || typeof node !== "object") return "";
  const approvers = Array.isArray(node.approvers) ? node.approvers : [];
  const names = approvers
    .map((a) => (a?.approverName || "").trim())
    .filter(Boolean);
  if (names.length) return names.join("/");
  return (node.approverName || "").trim() || `节点${index + 1}`;
}
/** 接口 nodes → 页面审批流(单审批人节点) */
export function mapNodesToFormFlow(nodes = []) {
  return (Array.isArray(nodes) ? nodes : []).map((n, i) => {
    const first = Array.isArray(n.approvers) ? n.approvers[0] : null;
  return sortFlowNodesByLevel(nodes).map((n, i) => {
    const approvers = Array.isArray(n.approvers) ? n.approvers : [];
    const first = approvers[0] || null;
    const names = approvers
      .map((a) => (a?.approverName || "").trim())
      .filter(Boolean);
    return {
      ...n,
      nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1,
      signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign",
      approverId: first?.approverId ?? n.approverId ?? null,
      approverName: first?.approverName ?? n.approverName ?? "",
      approverId:
        toNumber(first?.approverId ?? n.approverId) ??
        first?.approverId ??
        n.approverId ??
        null,
      approverName:
        names.join("、") || first?.approverName || n.approverName || "",
      nodeStatus: n.nodeStatus,
    };
  });
}
function formatTasksToFlowSummary(tasks = []) {
  const list = sortFlowNodesByLevel(
    (Array.isArray(tasks) ? tasks : []).map((t, i) => ({
      levelNo: t.levelNo ?? t.taskLevel ?? i + 1,
      approverName:
        (t.approverName || t.operatorName || t.createUserName || "").trim() ||
        "",
    }))
  );
  const parts = list.map((t) => t.approverName).filter(Boolean);
  return parts.length ? parts.join(" → ") : "";
}
function buildApprovalFlowSummaryForRow(row) {
  const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row));
  let flowNodes =
    row?.approvalFlowNodes?.length > 0
      ? sortFlowNodesByLevel(row.approvalFlowNodes)
      : mapNodesToFormFlow(apiNodes);
  if (!flowNodes.length && apiNodes.length) {
    const line = apiNodes
      .map((n, i) => formatApiNodeApproverLabel(n, i))
      .filter(Boolean)
      .join(" → ");
    if (line) return line;
  }
  if (!flowNodes.length) {
    const fromTasks = formatTasksToFlowSummary(row?.tasks);
    if (fromTasks) return fromTasks;
    return "—";
  }
  return flowNodes
    .map((n, i) => {
      const name = (n.approverName || "").trim() || `节点${i + 1}`;
      if (n.nodeStatus === "finish") return `${name}✓`;
      if (n.nodeStatus === "error") return `${name}✗`;
      if (n.nodeStatus === "process") return `${name}…`;
      return name;
    })
    .join(" → ");
}
/** 列表「审批流程」列文案 */
export function formatApprovalFlowSummary(row) {
  return buildApprovalFlowSummaryForRow(row);
}
/** listPage 常不带完整 nodes,列表加载后统一拉详情补全多级审批流程 */
export async function enrichReimbursementListRowsWithApprovalFlow(
  rows,
  reimbursementType
) {
  const list = Array.isArray(rows) ? rows : [];
  if (!list.length) return list;
  const needIds = list
    .map((r) => resolveReimbursementDeleteId(r))
    .filter((id) => id != null);
  if (!needIds.length) return list;
  const detailById = new Map();
  await Promise.all(
    needIds.map(async (id) => {
      try {
        const res = await getFinReimbursementDetail(id);
        detailById.set(String(id), unwrapFinReimbursementDetail(res));
      } catch {
        /* 单行失败不影响列表 */
      }
    })
  );
  const mapRow =
    reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL
      ? mapTravelReimbursementRow
      : mapCostReimbursementRow;
  return list.map((row) => {
    const id = resolveReimbursementDeleteId(row);
    const detail = id != null ? detailById.get(String(id)) : null;
    if (!detail) return row;
    const merged = {
      ...row,
      ...detail,
      id: row.id ?? detail.id,
      reimbursementId: row.reimbursementId ?? row.id ?? detail.id,
      reimbursementType: detail.reimbursementType ?? row.reimbursementType,
    };
    return mapRow(merged);
  });
}
@@ -489,6 +647,81 @@
  return Math.round(sum * 100) / 100;
}
/** 表单附件列表(兼容多种字段名) */
export function resolveFormAttachmentList(form) {
  const list =
    form?.attachmentList ??
    form?.storageBlobDTOs ??
    form?.storageBlobVOList ??
    form?.invoiceAttachments ??
    [];
  return Array.isArray(list) ? list : [];
}
/** 页面附件 → 保存 DTO(storageBlobVOList / storageBlobDTOs) */
export function mapFormAttachmentsToApi(list = [], reimbursementId) {
  const rid =
    reimbursementId != null
      ? toNumber(reimbursementId) ?? reimbursementId
      : undefined;
  return (list || [])
    .map((item, i) => {
      if (!item) return null;
      const url =
        item.url ||
        item.fileUrl ||
        item.downloadUrl ||
        item.downloadURL ||
        item.previewUrl ||
        item.previewURL ||
        item.link ||
        "";
      const name =
        item.fileName ||
        item.originalFilename ||
        item.originalFileName ||
        item.blobName ||
        item.name ||
        `附件${i + 1}`;
      const idRaw = item.id ?? item.blobId;
      const isTempId =
        idRaw != null &&
        /^(inv_|att_|ed_|local_)/.test(String(idRaw));
      if (!url && (idRaw == null || isTempId)) return null;
      const blob = {
        fileName: name,
        originalFilename: name,
        fileUrl: url || undefined,
        url: url || undefined,
      };
      if (idRaw != null && !isTempId) {
        const n = toNumber(idRaw);
        blob.id = n != null ? n : idRaw;
        blob.blobId = blob.id;
      }
      if (rid != null) blob.reimbursementId = rid;
      return blob;
    })
    .filter(Boolean);
}
function applyStorageBlobsToSaveDto(dto, form) {
  const blobs = mapFormAttachmentsToApi(
    resolveFormAttachmentList(form),
    dto?.id ?? form?.reimbursementId ?? form?.id
  );
  if (blobs.length) {
    dto.storageBlobVOList = blobs;
    dto.storageBlobDTOs = blobs;
  }
  return dto;
}
/** 修改时补齐主表与子表关联 ID */
function applyReimbursementRelations(dto) {
  const rid = dto?.id;
@@ -501,6 +734,12 @@
      d.reimbursementId = rid;
    });
  }
  const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray);
  blobLists.forEach((list) => {
    list.forEach((b) => {
      b.reimbursementId = rid;
    });
  });
  return dto;
}
@@ -572,6 +811,7 @@
  }
  if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id);
  applyStorageBlobsToSaveDto(dto, form);
  return applyReimbursementRelations(dto);
}
@@ -616,6 +856,7 @@
    dto.approveProcessId = toNumber(form.approveProcessId);
  }
  applyStorageBlobsToSaveDto(dto, form);
  return applyReimbursementRelations(dto);
}
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -387,7 +387,7 @@
      <DetailPanel :row="detailRow" />
      <ApprovalFlowProgress
        class="mt16"
        :nodes="detailRow.approvalFlowNodes"
        :nodes="detailRow.approvalFlowProgressNodes ?? detailRow.approvalFlowNodes"
        :current-index="detailRow.currentNodeIndex ?? 0"
      />
      <el-divider content-position="left">审批记录(全流程留痕)</el-divider>
@@ -420,7 +420,7 @@
      <DetailPanel :row="approveDialog.row" />
      <el-divider content-position="left">流程进度</el-divider>
      <ApprovalFlowProgress
        :nodes="approveDialog.row?.approvalFlowNodes"
        :nodes="approveDialog.row?.approvalFlowProgressNodes ?? approveDialog.row?.approvalFlowNodes"
        :current-index="approveDialog.row?.currentNodeIndex ?? 0"
      />
      <el-form label-width="100px" class="mt16">
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
@@ -14,6 +14,7 @@
  buildTravelReimbursementSaveDto,
  canDeleteReimbursementRow,
  canEditReimbursementRow,
  enrichReimbursementListRowsWithApprovalFlow,
  filterRowsByReimbursementType,
  FIN_REIMBURSEMENT_TYPE,
  mapFinReimbursementDetailRow,
@@ -90,11 +91,19 @@
        })
      );
      const { records, total } = unwrapFinReimbursementPage(res);
      allRows.value = filterRowsByReimbursementType(
      const filtered = filterRowsByReimbursementType(
        records,
        FIN_REIMBURSEMENT_TYPE.TRAVEL
      ).map(mapTravelReimbursementRow);
      page.total = total;
      );
      let mapped = filtered.map(mapTravelReimbursementRow);
      mapped = await enrichReimbursementListRowsWithApprovalFlow(
        mapped,
        FIN_REIMBURSEMENT_TYPE.TRAVEL
      );
      allRows.value = mapped;
      const dropped = records.length - filtered.length;
      page.total =
        dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
    } catch {
      allRows.value = [];
      page.total = 0;