yyb
2 天以前 a1a9521e1f537d742c4f3ebada9b102bfefa6583
审批列表
已添加3个文件
已修改4个文件
1891 ■■■■■ 文件已修改
src/api/officeProcessAutomation/approvalInstance.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js 680 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue 117 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue 310 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js 474 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/officeProcessAutomation/approvalInstance.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
import request from "@/utils/request";
/** åˆ†é¡µæŸ¥è¯¢å®¡æ‰¹å®žä¾‹ */
export function listApprovalInstancePage(params) {
  return request({
    url: "/approvalInstance/listPage",
    method: "get",
    params,
  });
}
/** æäº¤/保存审批实例 */
export function saveApprovalInstance(approvalInstanceDto) {
  return request({
    url: "/approvalInstance/save",
    method: "post",
    data: approvalInstanceDto,
  });
}
/** æ›´æ–°å®¡æ‰¹å®žä¾‹ */
export function updateApprovalInstance(approvalInstanceDto) {
  return request({
    url: "/approvalInstance/update",
    method: "put",
    data: approvalInstanceDto,
  });
}
/** å®¡æ‰¹ï¼ˆé€šè¿‡/驳回) */
export function approveApprovalInstance(approvalInstanceDto) {
  return request({
    url: "/approvalInstance/approve",
    method: "post",
    data: approvalInstanceDto,
  });
}
/** åˆ é™¤å®¡æ‰¹å®žä¾‹ï¼ˆbody ä¸º ID æ•°ç»„) */
export function deleteApprovalInstance(ids) {
  const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
  return request({
    url: "/approvalInstance/delete",
    method: "delete",
    data: idList,
  });
}
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -1,5 +1,13 @@
import dayjs from "dayjs";
import { buildFormPayloadFromFields } from "../approve-template/formConfigUtils.js";
import {
  createEmptyNode,
  formatDisplayTime,
  mapNodesFromApi,
  mapSignModeFromApi,
  mapSignModeToApi,
  normalizeFlowNodes,
  nodeSignModeLabel,
} from "../approve-template/approveTemplateConstants.js";
import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js";
/** å®¡æ‰¹ç±»åž‹ï¼ˆä¸ŽåŽç«¯å­—段 approvalType å¯¹é½ï¼ŒåŽæœŸå¯åŒæ­¥ï¼‰ */
export const APPROVAL_TYPE_OPTIONS = [
@@ -26,105 +34,400 @@
  { value: "cancelled", label: "已撤销" },
];
/** å®¡æ‰¹æ–¹å¼ approvalMode */
export const APPROVAL_MODE_OPTIONS = [
  { value: "parallel", label: "与签" },
  { value: "or_sign", label: "或签" },
];
export const LEGACY_APPROVE_LIST_STORAGE_KEY = "oa_unified_approve_list_v1";
export function clearLegacyApproveListStorage() {
  try {
    localStorage.removeItem(LEGACY_APPROVE_LIST_STORAGE_KEY);
  } catch {
    /* ignore */
  }
}
/** æäº¤å¼¹çª—:模板卡片(来自后端列表) */
export function mapSubmitTemplateCard(row) {
  const cfg = parseFormConfigToData(row?.formConfig);
  return {
    id: row?.id,
    key: String(row?.id ?? ""),
    approvalType: cfg.approvalType || row?.approvalType || "",
    label: row?.templateName || "—",
    summaryPlaceholder: (row?.description || "").trim() || cfg.summaryPlaceholder || "点击填写并提交",
  };
}
/** å®¡æ‰¹è®°å½• approveAction â†’ é¡µé¢ result */
export function mapRecordResultFromApi(action) {
  const s = String(action || "").toUpperCase();
  if (s === "APPROVED" || s === "APPROVE" || s === "PASS") return "approved";
  if (s === "REJECTED" || s === "REJECT" || s === "REFUSE") return "rejected";
  return "pending";
}
/** åŽç«¯ records â†’ æ—¶é—´çº¿å±•示结构 */
export function mapRecordsFromApi(records) {
  const list = Array.isArray(records) ? records : [];
  return list.map((r) => ({
    id: r.id,
    operatorName: r.approverName || r.operatorName || r.createUserName || "",
    result: mapRecordResultFromApi(r.approveAction ?? r.action ?? r.status),
    opinion: r.approveComment || r.comment || r.opinion || "",
    time: formatDisplayTime(r.approveTime || r.createTime || r.time || ""),
    raw: r,
  }));
}
export function mapTaskStatusLabel(status) {
  const s = String(status || "").toUpperCase();
  if (s === "APPROVED") return "已通过";
  if (s === "REJECTED") return "已驳回";
  if (s === "PENDING") return "待审批";
  if (s === "CANCELLED") return "已撤销";
  return status || "—";
}
export function mapTaskStatusTagType(status) {
  const s = String(status || "").toUpperCase();
  if (s === "APPROVED") return "success";
  if (s === "REJECTED") return "danger";
  if (s === "CANCELLED") return "info";
  return "warning";
}
/** åŽç«¯ tasks â†’ é¡µé¢ flowNodes(按 levelNo åˆ†ç»„,供流程编辑/展示) */
export function mapTasksToFlowNodes(tasks) {
  const list = Array.isArray(tasks) ? tasks : [];
  if (!list.length) return [];
  const byLevel = new Map();
  list.forEach((t) => {
    const level = Number(t.levelNo ?? t.taskLevel ?? t.nodeOrder ?? 1);
    if (!byLevel.has(level)) {
      byLevel.set(level, {
        id: t.nodeId,
        templateId: t.templateId,
        nodeOrder: level,
        signMode: mapSignModeFromApi(t.approveType),
        approvers: [],
        tasks: [],
      });
    }
    const node = byLevel.get(level);
    node.approvers.push({
      id: t.id,
      nodeId: t.nodeId,
      templateId: t.templateId,
      approverId: t.approverId,
      approverName: t.approverName || "",
      status: t.status,
      approveComment: t.approveComment,
      approveTime: t.approveTime,
    });
    node.tasks.push(t);
    if (t.approveType != null) {
      node.signMode = mapSignModeFromApi(t.approveType);
    }
  });
  return [...byLevel.entries()]
    .sort(([a], [b]) => a - b)
    .map(([, node]) => node);
}
/** é¡µé¢ flowNodes â†’ åŽç«¯ tasks */
export function mapFlowNodesToTasks(flowNodes, { instanceId, templateId } = {}) {
  const nodes = normalizeFlowNodes(flowNodes);
  const tasks = [];
  nodes.forEach((n) => {
    const levelNo = n.nodeOrder ?? 1;
    const approveType = mapSignModeToApi(n.signMode);
    n.approvers.forEach((a, idx) => {
      const task = {
        levelNo,
        approveType,
        approverId: a.approverId,
        approverName: a.approverName || "",
        sortNo: a.sortNo ?? idx + 1,
      };
      if (a.id != null) task.id = a.id;
      if (a.nodeId != null) task.nodeId = a.nodeId;
      if (a.templateId != null) task.templateId = a.templateId;
      else if (templateId) task.templateId = templateId;
      if (instanceId) task.instanceId = instanceId;
      if (a.status != null) task.status = a.status;
      tasks.push(task);
    });
  });
  return tasks;
}
function guessFieldTypeFromValue(val) {
  if (Array.isArray(val) && val.length === 2) return "datetimerange";
  if (typeof val === "number") return "number";
  if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) return "date";
  if (typeof val === "string" && val.length > 100) return "textarea";
  return "text";
}
/** å•字段展示值(详情只读) */
export function formatFieldDisplayValue(field, val) {
  if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "—";
  if (field?.type === "select" && field.options?.length) {
    const hit = field.options.find((o) => String(o.value) === String(val));
    return hit?.label || String(val);
  }
  if (Array.isArray(val)) return val.join(" è‡³ ");
  return String(val);
}
/**
 * æäº¤å®¡æ‰¹æ¨¡æ¿ï¼ˆæŒ‰ç±»åž‹ä¸€é”®å¡«æŠ¥ï¼Œå­—段后期与后端模板同步)
 * ä»Žè¡Œæ•°æ® / formConfig è§£æžå¡«æŠ¥å­—段定义与 formPayload(与新增提交结构一致)
 */
export const SUBMIT_TEMPLATES = {
  cost_reimburse: {
    approvalType: "cost_reimburse",
    label: "费用报销",
    summaryPlaceholder: "请填写报销事由、金额等",
    fields: [
      { key: "summary", label: "申请事由", type: "textarea", required: true, rows: 3 },
      { key: "amount", label: "报销金额(元)", type: "number", required: true, min: 0, precision: 2 },
    ],
    approvalMode: "parallel",
  },
  travel_reimburse: {
    approvalType: "travel_reimburse",
    label: "差旅报销",
    summaryPlaceholder: "出差行程与费用说明",
    fields: [
      { key: "summary", label: "差旅说明", type: "textarea", required: true, rows: 3 },
      { key: "amount", label: "报销金额(元)", type: "number", required: true, min: 0, precision: 2 },
      { key: "tripDays", label: "出差天数", type: "number", required: false, min: 0, precision: 0 },
    ],
    approvalMode: "parallel",
  },
  overtime: {
    approvalType: "overtime",
    label: "加班申请",
    fields: [
      { key: "summary", label: "加班事由", type: "textarea", required: true, rows: 3 },
      { key: "overtimeDate", label: "加班日期", type: "date", required: true },
      { key: "hours", label: "加班时长(小时)", type: "number", required: true, min: 0.5, precision: 1 },
    ],
    approvalMode: "parallel",
  },
  leave: {
    approvalType: "leave",
    label: "请假申请",
    fields: [
      { key: "leaveType", label: "请假类型", type: "select", required: true, options: [
        { label: "年假", value: "annual" },
        { label: "病假", value: "sick" },
        { label: "事假", value: "personal" },
        { label: "调休", value: "compensatory" },
      ] },
      { key: "summary", label: "请假事由", type: "textarea", required: true, rows: 2 },
      { key: "dateRange", label: "请假时间", type: "datetimerange", required: true },
    ],
    approvalMode: "parallel",
  },
  work_handover: {
    approvalType: "work_handover",
    label: "工作交接",
    fields: [
      { key: "summary", label: "交接说明", type: "textarea", required: true, rows: 3 },
      { key: "handoverTo", label: "交接对象", type: "text", required: true },
    ],
    approvalMode: "parallel",
  },
  regular: {
    approvalType: "regular",
    label: "转正申请",
    fields: [
      { key: "summary", label: "转正说明", type: "textarea", required: true, rows: 3 },
      { key: "regularDate", label: "拟转正日期", type: "date", required: true },
    ],
    approvalMode: "parallel",
  },
  resign: {
    approvalType: "resign",
    label: "离职申请",
    fields: [
      { key: "summary", label: "离职原因", type: "textarea", required: true, rows: 3 },
      { key: "lastWorkDay", label: "最后工作日", type: "date", required: true },
    ],
    approvalMode: "or_sign",
  },
  transfer: {
    approvalType: "transfer",
    label: "调岗申请",
    fields: [
      { key: "summary", label: "调岗说明", type: "textarea", required: true, rows: 2 },
      { key: "targetDept", label: "目标部门", type: "text", required: true },
      { key: "targetPost", label: "目标岗位", type: "text", required: true },
    ],
    approvalMode: "parallel",
  },
};
export function resolveInstanceFormFields(row) {
  const cfg = parseInstanceFormConfig(row?.formConfig);
  let fields = (row?.formFieldDefs?.length ? row.formFieldDefs : cfg.fields) || [];
  const formPayload = {
    ...(fields.length ? buildFormPayloadFromFields(fields) : {}),
    ...cfg.formPayload,
    ...(row?.formPayload || {}),
  };
  if (!fields.length && Object.keys(formPayload).length) {
    fields = Object.keys(formPayload)
      .filter((k) => k && k !== "summary")
      .map((k) => ({
        key: k,
        label: k,
        type: guessFieldTypeFromValue(formPayload[k]),
        required: false,
        rows: 3,
        min: 0,
        precision: 0,
        options: [],
      }));
  }
  const templateSnapshot = {
    label: row?.templateName || row?.title || "审批",
    approvalType: cfg.approvalType || row?.approvalType || "",
    summaryPlaceholder: cfg.summaryPlaceholder || "",
    templateId: row?.templateId,
    fields,
  };
  return { fields, formPayload, templateSnapshot, formConfigData: cfg };
}
export const STORAGE_KEY = "oa_unified_approve_list_v1";
/** è§£æžå®žä¾‹ formConfig */
export function parseInstanceFormConfig(formConfig) {
  let raw = {};
  if (formConfig) {
    if (typeof formConfig === "object") raw = formConfig;
    else {
      try {
        raw = JSON.parse(formConfig);
      } catch {
        raw = {};
      }
    }
  }
  const data = parseFormConfigToData(formConfig);
  const payload = raw.formPayload;
  return {
    summaryPlaceholder: raw.summaryPlaceholder || data.summaryPlaceholder || "",
    approvalType: raw.approvalType || "",
    fields: data.fields || [],
    formPayload: payload && typeof payload === "object" ? payload : {},
  };
}
export function unwrapInstanceDetail(res) {
  const data = res?.data ?? res;
  if (!data || typeof data !== "object") return {};
  if (data.id != null || data.instanceNo) return data;
  if (data.approvalInstanceVo) return data.approvalInstanceVo;
  return data;
}
/** å¡«æŠ¥å†…容 + æ¨¡æ¿å­—段定义 â†’ formConfig JSON */
export function buildInstanceFormConfigJson(templateSnapshot, formPayload) {
  const payload = formPayload || {};
  return JSON.stringify({
    summaryPlaceholder: templateSnapshot?.summaryPlaceholder || "",
    approvalType: templateSnapshot?.approvalType || "",
    fields: templateSnapshot?.fields || [],
    formPayload: payload,
  });
}
/** ç»„装保存/更新审批 DTO */
export function buildInstanceDto({ submitForm, activeTemplate, userStore, flowNodes, existingRow }) {
  const payload = submitForm?.formPayload || {};
  const tpl = activeTemplate || {};
  const title =
    String(payload.summary || payload.title || "").trim() ||
    tpl.label ||
    submitForm?.templateName ||
    "审批申请";
  const templateId = submitForm?.templateId || tpl.templateId;
  const instanceId = existingRow?.id ?? submitForm?.instanceId;
  const taskList = mapFlowNodesToTasks(flowNodes || submitForm?.flowNodes, {
    instanceId,
    templateId,
  });
  const isUpdate = Boolean(instanceId);
  const dto = {
    templateId,
    templateName: submitForm?.templateName || tpl.label || "",
    title,
    formConfig: buildInstanceFormConfigJson({ ...tpl, fields: tpl.fields || submitForm?.formFieldDefs }, payload),
    tasks: taskList,
  };
  if (isUpdate) {
    dto.id = existingRow?.id ?? submitForm?.instanceId;
    dto.instanceNo = existingRow?.instanceNo ?? submitForm?.instanceNo ?? "";
    dto.status =
      existingRow?.statusRaw || mapInstanceStatusToApi(existingRow?.approvalStatus) || "PENDING";
    dto.currentLevel = existingRow?.currentLevel ?? submitForm?.currentLevel ?? 1;
    dto.applicantId = existingRow?.applicantId ?? existingRow?.applicantNo;
    dto.applicantName = existingRow?.applicantName || "";
  } else {
    dto.status = "PENDING";
    dto.currentLevel = 1;
    dto.applicantId = userStore?.id;
    dto.applicantName = userStore?.nickName || userStore?.name || "";
  }
  return dto;
}
/** @deprecated ä½¿ç”¨ buildInstanceDto */
export function buildSaveInstanceDto(params) {
  return buildInstanceDto(params);
}
/** æ ¡éªŒæäº¤å®¡æ‰¹æµç¨‹ï¼ˆä¸Žæ¨¡æ¿é¡µè§„则一致) */
export function validateSubmitFlowNodes(flowNodes) {
  const nodes = normalizeFlowNodes(flowNodes);
  if (!nodes.length) return { ok: false, message: "请至少配置一个审批节点" };
  for (let i = 0; i < nodes.length; i++) {
    if (!nodes[i].approvers.length) {
      return { ok: false, message: `请为第 ${i + 1} ä¸ªèŠ‚ç‚¹é€‰æ‹©è‡³å°‘ä¸€åå®¡æ‰¹äºº` };
    }
  }
  return { ok: true, nodes };
}
/** åŽç«¯ status â†’ é¡µé¢ approvalStatus */
export function mapInstanceStatusFromApi(status) {
  const s = String(status || "").toUpperCase();
  if (s === "APPROVED") return "approved";
  if (s === "REJECTED") return "rejected";
  if (s === "CANCELLED") return "cancelled";
  return "pending";
}
/** é¡µé¢ approvalStatus â†’ åŽç«¯ status */
export function mapInstanceStatusToApi(approvalStatus) {
  const s = String(approvalStatus || "").toLowerCase();
  if (s === "approved") return "APPROVED";
  if (s === "rejected") return "REJECTED";
  if (s === "cancelled") return "CANCELLED";
  return "PENDING";
}
export function unwrapInstancePage(res) {
  const data = res?.data ?? res;
  return {
    records: Array.isArray(data?.records) ? data.records : [],
    total: Number(data?.total ?? 0),
  };
}
/** åˆ†é¡µåˆ—表项 â†’ è¡¨æ ¼è¡Œ */
export function mapInstanceFromApi(row) {
  if (!row) return {};
  const approvalStatus = mapInstanceStatusFromApi(row.status);
  const createTime = formatDisplayTime(row.createTime ?? row.applyTime ?? "");
  const applyTime = formatDisplayTime(row.applyTime ?? "");
  const finishTime = formatDisplayTime(row.finishTime ?? "");
  const resolved = resolveInstanceFormFields(row);
  const { fields, formPayload, templateSnapshot } = resolved;
  const tasks = Array.isArray(row.tasks) ? row.tasks : [];
  const flowNodes = tasks.length
    ? mapTasksToFlowNodes(tasks)
    : mapNodesFromApi(row.nodes || row.flowNodes);
  const approvalRecords = mapRecordsFromApi(row.records);
  return {
    id: row.id,
    bizId: row.instanceNo || String(row.id ?? ""),
    instanceNo: row.instanceNo || "",
    templateId: row.templateId,
    templateName: row.templateName || "",
    businessId: row.businessId,
    businessType: row.businessType,
    applicantId: row.applicantId,
    applicantNo: row.applicantId != null ? String(row.applicantId) : "",
    applicantName: row.applicantName || "",
    approvalType: row.templateName || "",
    unread: Boolean(row.isApprove) && approvalStatus === "pending",
    isApprove: Boolean(row.isApprove),
    approvalStatus,
    statusRaw: row.status,
    createTime,
    applyTime: applyTime === "—" ? "" : applyTime,
    finishTime: finishTime === "—" ? "" : finishTime,
    title: row.title || "",
    summary: row.title || row.templateName || "",
    currentLevel: row.currentLevel,
    formConfig: row.formConfig,
    formPayload,
    formFieldDefs: fields,
    templateSnapshot,
    tasks,
    records: Array.isArray(row.records) ? row.records : [],
    flowNodes,
    approvalFlowNodes: [],
    currentNodeIndex: 0,
    approvalRecords,
    rejectReason:
      approvalRecords.find((r) => r.result === "rejected")?.opinion || "",
  };
}
/** å®¡æ‰¹æ“ä½œï¼šä¸ŽåŽç«¯ status æžšä¸¾ä¸€è‡´ */
export const APPROVE_ACTION_APPROVED = "APPROVED";
export const APPROVE_ACTION_REJECTED = "REJECTED";
/** é¡µé¢æ“ä½œ â†’ approveAction */
export function mapApproveActionToApi(uiResult) {
  return uiResult === "rejected" ? APPROVE_ACTION_REJECTED : APPROVE_ACTION_APPROVED;
}
/** ç»„装审批提交 DTO */
export function buildApproveInstanceDto(row, uiResult, comment) {
  const opinion = (comment || "").trim();
  return {
    id: row?.id,
    approveAction: mapApproveActionToApi(uiResult),
    approveComment: opinion || (uiResult === "approved" ? "同意" : ""),
  };
}
export function buildApprovalInstanceListParams({ page, searchForm }) {
  const params = {
    current: page.current,
    size: page.size,
  };
  const dto = {};
  const kw = (searchForm?.applicantKeyword || "").trim();
  if (kw) dto.applicantName = kw;
  if (searchForm?.approvalType) {
    const opt = APPROVAL_TYPE_OPTIONS.find((x) => x.value === searchForm.approvalType);
    if (opt?.label) dto.templateName = opt.label;
  }
  if (Object.keys(dto).length) params.approvalInstanceDto = dto;
  return params;
}
export function approvalTypeLabel(v) {
  return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || "—";
  return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "—";
}
export function approvalTypeStyle(v) {
@@ -148,166 +451,57 @@
  return "primary";
}
export function approvalModeLabel(v) {
  if (v === "countersign") return "或签";
  return APPROVAL_MODE_OPTIONS.find((x) => x.value === v)?.label || "与签";
}
export function unreadLabel(v) {
  return v ? "是" : "否";
}
export function buildDefaultFlowNodes() {
  return [
    {
      approverId: "mock_supervisor",
      approverName: "直属上级",
      sortOrder: 1,
      nodeOrder: 1,
      nodeStatus: "process",
      approveOpinion: "",
      approveTime: "",
    },
    {
      approverId: "mock_manager",
      approverName: "部门经理",
      sortOrder: 2,
      nodeOrder: 2,
      nodeStatus: "wait",
      approveOpinion: "",
      approveTime: "",
    },
  ];
}
/** åˆ—表行 â†’ ç¼–辑表单(仅用行数据回显) */
export function buildEditFormFromInstanceRow(row) {
  const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row);
  const normalized = normalizeFlowNodes(
    row?.flowNodes?.length ? row.flowNodes : mapTasksToFlowNodes(row?.tasks)
  );
  const flowNodes = normalized.length
    ? JSON.parse(JSON.stringify(normalized))
    : [createEmptyNode(1)];
function demoRow(partial) {
  const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
  return {
    id: partial.id,
    bizId: partial.bizId || partial.id,
    applicantNo: partial.applicantNo,
    applicantName: partial.applicantName,
    approvalType: partial.approvalType,
    approvalMode: partial.approvalMode || "parallel",
    unread: partial.unread ?? false,
    approvalStatus: partial.approvalStatus || "pending",
    createTime: partial.createTime || now,
    summary: partial.summary || "",
    formPayload: partial.formPayload || {},
    approvalFlowNodes: partial.approvalFlowNodes || buildDefaultFlowNodes(),
    currentNodeIndex: partial.currentNodeIndex ?? 0,
    approvalRecords: partial.approvalRecords || [],
    rejectReason: partial.rejectReason || "",
    sourceRoute: partial.sourceRoute || "",
    templateKey: String(row?.templateId || ""),
    templateId: row?.templateId,
    templateName: row?.templateName || templateSnapshot.label,
    instanceId: row?.id,
    instanceNo: row?.instanceNo || "",
    statusRaw: row?.statusRaw || row?.status || "PENDING",
    currentLevel: row?.currentLevel ?? 1,
    applicantId: row?.applicantId,
    applicantName: row?.applicantName || "",
    templateSnapshot,
    formFieldDefs: fields,
    formPayload,
    flowNodes,
  };
}
/** åˆå§‹æ¼”示数据(共 22 æ¡ï¼Œä¸ŽåŽŸåž‹æ•°é‡ä¸€è‡´ï¼‰ */
export function createInitialMockRows() {
  const types = [
    "cost_reimburse",
    "travel_reimburse",
    "overtime",
    "leave",
    "work_handover",
    "regular",
    "resign",
    "transfer",
    "cost_reimburse",
    "leave",
    "overtime",
    "travel_reimburse",
    "work_handover",
    "regular",
    "cost_reimburse",
    "leave",
    "transfer",
    "resign",
    "overtime",
    "travel_reimburse",
    "cost_reimburse",
    "leave",
  ];
  const applicants = [
    { no: "007", name: "苹果" },
    { no: "Guest001", name: "外部用户" },
    { no: "0056", name: "王五" },
    { no: "0042", name: "李四" },
    { no: "0088", name: "猫猫" },
    { no: "0012", name: "张三" },
    { no: "0033", name: "赵六" },
  ];
  const summaries = [
    "办公用品采购报销",
    "上海出差差旅费",
    "周末项目加班",
    "年假 3 å¤©",
    "离职工作交接",
    "试用期转正申请",
    "个人原因离职",
    "调至销售部",
    "客户接待餐费",
    "病假 1 å¤©",
    "节假日值班加班",
    "北京培训差旅",
    "项目文档交接",
    "研发岗转正",
    "通讯费报销",
    "事假半天",
    "调岗至市场部",
    "协商离职",
    "工作日延时加班",
    "成都展会差旅",
    "交通费报销",
    "调休 1 å¤©",
  ];
  const statuses = ["pending", "pending", "pending", "approved", "pending", "pending", "rejected", "pending"];
  return types.map((approvalType, i) => {
    const ap = applicants[i % applicants.length];
    const daysAgo = i % 14;
    return demoRow({
      id: `mock_${i + 1}`,
      bizId: `BIZ${String(2025031400 + i)}`,
      applicantNo: ap.no,
      applicantName: ap.name,
      approvalType,
      approvalMode: i % 5 === 0 ? "or_sign" : "parallel",
      unread: i % 3 === 0,
      approvalStatus: statuses[i % statuses.length],
      createTime: dayjs().subtract(daysAgo, "day").hour(9 + (i % 8)).minute((i * 7) % 60).second(0).format("YYYY-MM-DD HH:mm:ss"),
      summary: summaries[i],
      formPayload: { summary: summaries[i] },
    });
  });
}
export function loadStoredRows() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : null;
  } catch {
    return null;
  }
}
export function saveStoredRows(rows) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(rows));
  } catch {
    /* ignore quota */
  }
}
export function createEmptySubmitForm(templateKey, templateOverride) {
  const tpl = templateOverride || SUBMIT_TEMPLATES[templateKey];
export function createEmptySubmitForm(templateKey, templateOverride, flowNodesOverride) {
  const tpl = templateOverride || null;
  const payload = tpl?.fields?.length ? buildFormPayloadFromFields(tpl.fields) : { summary: "" };
  const normalized = normalizeFlowNodes(flowNodesOverride);
  const flowNodes = normalized.length
    ? JSON.parse(JSON.stringify(normalized))
    : [createEmptyNode(1)];
  return {
    templateKey: templateKey || "",
    templateSnapshot: null,
    approvalMode: tpl?.approvalMode || "parallel",
    templateId: tpl?.templateId || "",
    templateName: tpl?.label || "",
    instanceId: "",
    instanceNo: "",
    statusRaw: "",
    currentLevel: 1,
    applicantId: null,
    applicantName: "",
    templateSnapshot: templateOverride || null,
    formFieldDefs: tpl?.fields || [],
    formPayload: payload,
    approvalFlowNodes: buildDefaultFlowNodes(),
    flowNodes,
  };
}
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -1,92 +1,83 @@
<!-- ç»Ÿä¸€å®¡æ‰¹ï¼šä¸šåŠ¡æ‘˜è¦ -->
<!-- å®¡æ‰¹è¯¦æƒ…:基础信息 + å¡«æŠ¥å†…容 -->
<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="申请人名称">{{ row.applicantName || "—" }}</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>
</template>
<script setup>
import { computed } from "vue";
import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js";
import {
  approvalTypeLabel,
  approvalTypeStyle,
  approvalModeLabel,
  approvalStatusLabel,
  approvalStatusTagType,
  SUBMIT_TEMPLATES,
  resolveInstanceFormFields,
} 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);
});
function formatValue(val) {
  if (Array.isArray(val)) return val.join(" è‡³ ");
  return String(val);
}
const formResolved = computed(() => resolveInstanceFormFields(props.row));
</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;
  border-radius: 4px;
  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);
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
<!-- å¡«æŠ¥é¡¹ï¼šç¼–辑为表单控件,详情为 descriptions è¡¨æ ¼ï¼ˆä¸Žä¸Šæ–¹åŸºç¡€ä¿¡æ¯ä¸€è‡´ï¼‰ -->
<template>
  <template v-if="fields?.length">
    <el-descriptions
      v-if="readonly"
      :column="2"
      border
      class="form-payload-desc"
    >
      <el-descriptions-item
        v-for="field in fields"
        :key="field.key"
        :label="field.label"
        :span="field.type === 'textarea' || field.type === 'datetimerange' ? 2 : 1"
      >
        <span class="field-value">{{ displayValue(field) }}</span>
      </el-descriptions-item>
    </el-descriptions>
    <el-form v-else label-width="120px" class="form-payload-edit">
      <el-form-item
        v-for="field in fields"
        :key="field.key"
        :label="field.label"
        :prop="`formPayload.${field.key}`"
      >
        <el-input
          v-if="field.type === 'text'"
          v-model="formPayload[field.key]"
          :placeholder="`请输入${field.label}`"
          maxlength="200"
        />
        <el-input
          v-else-if="field.type === 'textarea'"
          v-model="formPayload[field.key]"
          type="textarea"
          :rows="field.rows || 3"
          :placeholder="`请填写${field.label}`"
          maxlength="2000"
          show-word-limit
        />
        <el-input-number
          v-else-if="field.type === 'number'"
          v-model="formPayload[field.key]"
          :min="field.min ?? 0"
          :precision="field.precision ?? 0"
          controls-position="right"
          style="width: 100%"
        />
        <el-date-picker
          v-else-if="field.type === 'date'"
          v-model="formPayload[field.key]"
          type="date"
          :placeholder="`请选择${field.label}`"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 100%"
        />
        <el-date-picker
          v-else-if="field.type === 'datetimerange'"
          v-model="formPayload[field.key]"
          type="datetimerange"
          range-separator="至"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          format="YYYY-MM-DD HH:mm:ss"
          value-format="YYYY-MM-DD HH:mm:ss"
          style="width: 100%"
        />
        <el-select
          v-else-if="field.type === 'select'"
          v-model="formPayload[field.key]"
          :placeholder="`请选择${field.label}`"
          style="width: 100%"
          clearable
        >
          <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" />
        </el-select>
        <span v-else class="field-value">{{ displayValue(field) }}</span>
      </el-form-item>
    </el-form>
  </template>
  <el-empty v-else description="暂无填报项" :image-size="48" />
</template>
<script setup>
import { formatFieldDisplayValue } from "../approveListConstants.js";
const props = defineProps({
  fields: { type: Array, default: () => [] },
  formPayload: { type: Object, default: () => ({}) },
  readonly: { type: Boolean, default: false },
});
function displayValue(field) {
  return formatFieldDisplayValue(field, props.formPayload?.[field.key]);
}
</script>
<style scoped>
.form-payload-desc {
  width: 100%;
}
.form-payload-desc :deep(.el-descriptions__label) {
  width: 120px;
  font-weight: 500;
}
.field-value {
  color: var(--el-text-color-primary);
  line-height: 1.6;
  word-break: break-word;
}
.form-payload-edit {
  width: 100%;
}
</style>
src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
<!-- å®¡æ‰¹å®žä¾‹ï¼štasks å®¡æ‰¹æµç¨‹å±•示(横向步骤条) -->
<template>
  <div v-if="displayNodes.length" class="flow-track">
    <div
      v-for="(node, index) in displayNodes"
      :key="index"
      class="flow-step"
      :class="{ 'is-last': index === displayNodes.length - 1 }"
    >
      <div class="flow-step-card">
        <div class="flow-step-badge">{{ index + 1 }}</div>
        <div class="flow-step-main">
          <div class="flow-step-head">
            <span class="flow-step-name">节点 {{ index + 1 }}</span>
            <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'" effect="plain">
              {{ nodeSignModeLabel(node.signMode) }}
            </el-tag>
          </div>
          <div class="flow-approvers">
            <div
              v-for="a in node.approvers"
              :key="String(a.approverId ?? a.id)"
              class="flow-approver"
            >
              <span class="flow-approver-name">{{ a.approverName || "—" }}</span>
              <el-tag
                v-if="a.status"
                size="small"
                :type="mapTaskStatusTagType(a.status)"
                effect="plain"
              >
                {{ mapTaskStatusLabel(a.status) }}
              </el-tag>
            </div>
            <span v-if="!node.approvers?.length" class="flow-empty">未配置审批人</span>
          </div>
        </div>
      </div>
      <div v-if="index < displayNodes.length - 1" class="flow-connector" aria-hidden="true">
        <el-icon><ArrowRight /></el-icon>
      </div>
    </div>
  </div>
  <el-empty v-else description="暂无流程节点" :image-size="48" />
</template>
<script setup>
import { computed } from "vue";
import { ArrowRight } from "@element-plus/icons-vue";
import { nodeSignModeLabel } from "../../approve-template/approveTemplateConstants.js";
import {
  mapTaskStatusLabel,
  mapTaskStatusTagType,
  mapTasksToFlowNodes,
} from "../approveListConstants.js";
const props = defineProps({
  tasks: { type: Array, default: () => [] },
  nodes: { type: Array, default: () => [] },
});
const displayNodes = computed(() => {
  if (props.tasks?.length) return mapTasksToFlowNodes(props.tasks);
  return props.nodes || [];
});
</script>
<style scoped>
.flow-track {
  display: flex;
  align-items: stretch;
  gap: 0;
  overflow-x: auto;
  padding: 4px 2px 8px;
}
.flow-step {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}
.flow-step-card {
  display: flex;
  gap: 12px;
  min-width: 200px;
  max-width: 260px;
  padding: 14px;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 8px;
  background: var(--el-bg-color);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.flow-step-badge {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
  font-size: 13px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
}
.flow-step-main {
  flex: 1;
  min-width: 0;
}
.flow-step-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}
.flow-step-name {
  font-weight: 600;
  font-size: 13px;
  color: var(--el-text-color-primary);
}
.flow-approvers {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.flow-approver {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.flow-approver-name {
  font-size: 13px;
  color: var(--el-text-color-regular);
}
.flow-empty {
  font-size: 12px;
  color: var(--el-text-color-placeholder);
}
.flow-connector {
  display: flex;
  align-items: center;
  padding: 0 6px;
  color: var(--el-text-color-placeholder);
  font-size: 16px;
}
</style>
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -63,119 +63,85 @@
            {{ approvalTypeLabel(row.approvalType) }}
          </span>
        </template>
        <template #approvalMethod="{ row }">
          <span class="approval-method-text">{{ approvalModeLabel(row.approvalMode) }}</span>
        </template>
      </PIMTable>
    </div>
    <!-- æäº¤å®¡æ‰¹ï¼ˆæŒ‰æ¨¡æ¿ï¼‰ -->
    <el-dialog
      v-model="submitDialog.visible"
      :title="submitDialog.step === 1 ? '选择审批模板' : `提交${activeTemplate?.label || '审批'}`"
      :title="submitDialogTitle"
      width="720px"
      append-to-body
      destroy-on-close
      class="approve-submit-dialog"
      @closed="submitDialog.step = 1"
      @closed="resetSubmitDialogState"
    >
      <template v-if="submitDialog.step === 1">
        <p class="template-hint">请选择要提交的审批类型,系统将按对应模板引导填报(字段后期与后端同步)。</p>
        <div class="template-grid">
      <template v-if="submitDialog.step === 1 && !isSubmitEdit">
        <p class="template-hint">请选择已启用的审批模板,系统将按模板配置引导填报。</p>
        <div v-loading="submitTemplatesLoading" class="template-grid">
          <div
            v-for="(tpl, key) in SUBMIT_TEMPLATES"
            :key="key"
            v-for="card in submitTemplateCards"
            :key="card.key"
            class="template-card"
            @click="onTemplatePick(key)"
            @click="onTemplatePick(card)"
          >
            <span class="template-card-type" :style="approvalTypeStyle(tpl.approvalType)">
              {{ tpl.label }}
            <span class="template-card-type" :style="approvalTypeStyle(card.approvalType)">
              {{ card.label }}
            </span>
            <span class="template-card-desc">{{ tpl.summaryPlaceholder || "点击填写并提交" }}</span>
            <span class="template-card-desc">{{ card.summaryPlaceholder }}</span>
          </div>
          <el-empty
            v-if="!submitTemplatesLoading && !submitTemplateCards.length"
            description="暂无可用审批模板"
            :image-size="80"
            class="template-empty"
          />
        </div>
      </template>
      <template v-else>
        <div v-loading="submitTemplatesLoading && !isSubmitEdit">
        <el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px">
          <el-form-item label="审批类型">
            <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)">
              {{ activeTemplate.label }}
            </span>
            <el-button type="primary" link class="ml12" @click="backToTemplatePick">更换模板</el-button>
            <el-button
              v-if="!isSubmitEdit"
              type="primary"
              link
              class="ml12"
              @click="backToTemplatePick"
            >
              æ›´æ¢æ¨¡æ¿
            </el-button>
          </el-form-item>
          <el-form-item label="审批方式" prop="approvalMode">
            <el-radio-group v-model="submitForm.approvalMode">
              <el-radio value="parallel">与签</el-radio>
              <el-radio value="or_sign">或签</el-radio>
            </el-radio-group>
          </el-form-item>
          <template v-for="field in activeTemplate.fields" :key="field.key">
            <el-form-item :label="field.label" :prop="`formPayload.${field.key}`">
              <el-input
                v-if="field.type === 'text'"
                v-model="submitForm.formPayload[field.key]"
                :placeholder="`请输入${field.label}`"
                maxlength="200"
              />
              <el-input
                v-else-if="field.type === 'textarea'"
                v-model="submitForm.formPayload[field.key]"
                type="textarea"
                :rows="field.rows || 3"
                :placeholder="`请填写${field.label}`"
                maxlength="2000"
                show-word-limit
              />
              <el-input-number
                v-else-if="field.type === 'number'"
                v-model="submitForm.formPayload[field.key]"
                :min="field.min ?? 0"
                :precision="field.precision ?? 0"
                controls-position="right"
                style="width: 100%"
              />
              <el-date-picker
                v-else-if="field.type === 'date'"
                v-model="submitForm.formPayload[field.key]"
                type="date"
                :placeholder="`请选择${field.label}`"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
              <el-date-picker
                v-else-if="field.type === 'datetimerange'"
                v-model="submitForm.formPayload[field.key]"
                type="datetimerange"
                range-separator="至"
                start-placeholder="开始时间"
                end-placeholder="结束时间"
                format="YYYY-MM-DD HH:mm:ss"
                value-format="YYYY-MM-DD HH:mm:ss"
                style="width: 100%"
              />
              <el-select
                v-else-if="field.type === 'select'"
                v-model="submitForm.formPayload[field.key]"
                :placeholder="`请选择${field.label}`"
                style="width: 100%"
                clearable
              >
                <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" />
              </el-select>
            </el-form-item>
          </template>
          <el-form-item label="审批流程">
            <ApprovalFlowEditor v-model="submitForm.approvalFlowNodes" :user-options="flowUserOptions" />
            <p class="flow-tip">至少保留一个审批节点;提交后进入「审核中」状态。</p>
          <FormPayloadFields
            :fields="submitFormFields"
            :form-payload="submitForm.formPayload"
          />
          <el-form-item label="审批流程" required>
            <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
            <p class="flow-tip">
              æŒ‰é¡ºåºæµè½¬ï¼šå¯ä¸ºæ¯ä¸ªèŠ‚ç‚¹æ·»åŠ å¤šåå®¡æ‰¹äººï¼›ä¼šç­¾éœ€å…¨éƒ¨é€šè¿‡ï¼Œæˆ–ç­¾ä»»ä¸€äººé€šè¿‡å³å¯è¿›å…¥ä¸‹ä¸€èŠ‚ç‚¹ã€‚
            </p>
          </el-form-item>
        </el-form>
        </div>
      </template>
      <template #footer>
        <el-button v-if="submitDialog.step === 2" type="primary" @click="onSubmitNew">提 äº¤</el-button>
        <el-button @click="submitDialog.visible = false">{{ submitDialog.step === 1 ? "取 æ¶ˆ" : "关 é—­" }}</el-button>
        <el-button
          v-if="submitDialog.step === 2 || isSubmitEdit"
          type="primary"
          :loading="submitSaving"
          @click="onSubmitInstance"
        >
          {{ isSubmitEdit ? "保 å­˜" : "提 äº¤" }}
        </el-button>
        <el-button @click="submitDialog.visible = false">
          {{ submitDialog.step === 1 && !isSubmitEdit ? "取 æ¶ˆ" : "关 é—­" }}
        </el-button>
      </template>
    </el-dialog>
@@ -186,28 +152,51 @@
      width="920px"
      append-to-body
      destroy-on-close
      class="approve-detail-dialog"
    >
      <ApproveDetailPanel :row="detailRow" />
      <el-divider content-position="left">审批流程</el-divider>
      <ApprovalFlowProgress
        :nodes="detailRow.approvalFlowNodes"
        :current-index="detailRow.currentNodeIndex ?? 0"
      />
      <el-divider content-position="left">审批记录</el-divider>
      <el-timeline v-if="detailRow.approvalRecords?.length">
        <el-timeline-item
          v-for="(rec, i) in detailRow.approvalRecords"
          :key="i"
          :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
          :timestamp="rec.time"
        >
          {{ rec.operatorName }} â€” {{ approvalActionLabel(rec.result) }}:{{ rec.opinion || "无意见" }}
        </el-timeline-item>
      </el-timeline>
      <el-empty v-else description="暂无审批记录" :image-size="60" />
      <div class="approve-detail-body">
        <ApproveDetailPanel :row="detailRow" />
        <div class="detail-block">
          <div class="detail-block-title">
            å®¡æ‰¹æµç¨‹ï¼ˆ{{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} é¡¹ï¼‰
          </div>
          <InstanceFlowDisplay :tasks="detailRow.tasks" :nodes="detailRow.flowNodes" />
        </div>
        <div class="detail-block">
          <div class="detail-block-title">审批记录</div>
          <el-timeline v-if="detailRow.approvalRecords?.length" class="approve-record-timeline">
            <el-timeline-item
              v-for="(rec, i) in detailRow.approvalRecords"
              :key="rec.id ?? i"
              :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
              :timestamp="formatRecordTime(rec.time)"
              placement="top"
            >
              <div class="record-item">
                <span class="record-operator">{{ rec.operatorName || "—" }}</span>
                <el-tag
                  size="small"
                  :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'info'"
                  effect="plain"
                >
                  {{ approvalActionLabel(rec.result) }}
                </el-tag>
                <p class="record-opinion">{{ rec.opinion || "无意见" }}</p>
              </div>
            </el-timeline-item>
          </el-timeline>
          <el-empty v-else description="暂无审批记录" :image-size="48" />
        </div>
      </div>
      <template #footer>
        <el-button
          v-if="detailRow.approvalStatus === 'pending'"
          @click="openEditFromDetail"
        >
          ä¿® æ”¹
        </el-button>
        <el-button
          v-if="detailRow.approvalStatus === 'pending' && detailRow.isApprove"
          type="primary"
          @click="openApproveFromDetail"
        >
@@ -227,11 +216,12 @@
      @closed="approveOpinion = ''"
    >
      <ApproveDetailPanel :row="approveDialog.row" />
      <el-divider content-position="left">流程进度</el-divider>
      <ApprovalFlowProgress
        :nodes="approveDialog.row?.approvalFlowNodes"
        :current-index="approveDialog.row?.currentNodeIndex ?? 0"
      />
      <div class="detail-block mt16">
        <div class="detail-block-title">
          å®¡æ‰¹æµç¨‹ï¼ˆ{{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} é¡¹ï¼‰
        </div>
        <InstanceFlowDisplay :tasks="approveDialog.row?.tasks" :nodes="approveDialog.row?.flowNodes" />
      </div>
      <el-form label-width="100px" class="mt16">
        <el-form-item label="审批意见" required>
          <el-input
@@ -245,9 +235,23 @@
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="success" @click="onApprove('approved')">通 è¿‡</el-button>
        <el-button type="danger" @click="onApprove('rejected')">驳 å›ž</el-button>
        <el-button @click="approveDialog.visible = false">取 æ¶ˆ</el-button>
        <el-button
          type="success"
          :loading="approveSubmitting"
          @click="onApprove('approved')"
        >
          é€š è¿‡
        </el-button>
        <el-button
          type="danger"
          :loading="approveSubmitting"
          @click="onApprove('rejected')"
        >
          é©³ å›ž
        </el-button>
        <el-button :disabled="approveSubmitting" @click="approveDialog.visible = false">
          å– æ¶ˆ
        </el-button>
      </template>
    </el-dialog>
  </div>
@@ -258,19 +262,21 @@
import { ElMessage } from "element-plus";
import { onMounted, ref } from "vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue";
import ApprovalFlowProgress from "@/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue";
import TemplateFlowEditor from "../approve-template/components/TemplateFlowEditor.vue";
import FormPayloadFields from "./components/FormPayloadFields.vue";
import { formatDisplayTime } from "../approve-template/approveTemplateConstants.js";
import { approvalTypeStyle } from "./approveListConstants.js";
import ApproveDetailPanel from "./components/ApproveDetailPanel.vue";
import InstanceFlowDisplay from "./components/InstanceFlowDisplay.vue";
import { useApproveList } from "./useApproveList.js";
const al = useApproveList();
const {
  Search,
  APPROVAL_TYPE_OPTIONS,
  SUBMIT_TEMPLATES,
  submitTemplateCards,
  submitTemplatesLoading,
  approvalTypeLabel,
  approvalModeLabel,
  approvalActionLabel,
  searchForm,
  tableLoading,
@@ -281,18 +287,25 @@
  detailRow,
  approveDialog,
  approveOpinion,
  approveSubmitting,
  submitDialog,
  isSubmitEdit,
  submitDialogTitle,
  submitForm,
  submitFormRef,
  submitSaving,
  activeTemplate,
  submitFormFields,
  submitFormRules,
  handleQuery,
  resetSearch,
  pagination,
  resetSubmitDialogState,
  openSubmitDialog,
  openEditDialog,
  onTemplatePick,
  backToTemplatePick,
  submitNewApproval,
  submitInstanceForm,
  submitApprove,
  openDetail,
  openApprove,
@@ -322,13 +335,13 @@
  }
}
async function onSubmitNew() {
  const ok = await submitNewApproval();
  if (ok) ElMessage.success("审批已提交");
async function onSubmitInstance() {
  const ok = await submitInstanceForm();
  if (ok) ElMessage.success(isSubmitEdit.value ? "修改成功" : "审批已提交");
}
function onApprove(result) {
  const ret = submitApprove(result);
async function onApprove(result) {
  const ret = await submitApprove(result);
  if (ret?.needOpinion) {
    ElMessage.warning("驳回时请填写审批意见");
    return;
@@ -338,10 +351,20 @@
  }
}
function formatRecordTime(time) {
  return formatDisplayTime(time) || "—";
}
function openApproveFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openApprove(row);
}
function openEditFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openEditDialog(row);
}
onMounted(() => {
@@ -385,10 +408,6 @@
  font-size: 13px;
  line-height: 1.5;
}
.approval-method-text {
  color: var(--el-color-danger);
  font-weight: 500;
}
.template-hint {
  font-size: 13px;
  color: var(--el-text-color-secondary);
@@ -398,6 +417,10 @@
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px;
  min-height: 120px;
}
.template-empty {
  grid-column: 1 / -1;
}
.template-card {
  padding: 14px 16px;
@@ -433,4 +456,47 @@
.approve-submit-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
.approve-detail-dialog :deep(.el-dialog__body) {
  padding-top: 16px;
  max-height: 70vh;
  overflow-y: auto;
}
.approve-detail-body .detail-block {
  margin-top: 20px;
}
.approve-detail-body .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-record-timeline {
  padding-left: 4px;
}
.record-item {
  padding: 4px 0 2px;
}
.record-operator {
  font-weight: 600;
  margin-right: 8px;
  color: var(--el-text-color-primary);
}
.record-opinion {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--el-text-color-regular);
  line-height: 1.5;
}
.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;
}
</style>
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -1,57 +1,52 @@
import { Search } from "@element-plus/icons-vue";
import dayjs from "dayjs";
import {
  getApprovalTemplateDetail,
  listApprovalTemplate,
  TEMPLATE_TYPE_BUILTIN,
  TEMPLATE_TYPE_CUSTOM,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import {
  approveApprovalInstance,
  deleteApprovalInstance,
  listApprovalInstancePage,
  saveApprovalInstance,
  updateApprovalInstance,
} from "@/api/officeProcessAutomation/approvalInstance.js";
import useUserStore from "@/store/modules/user";
import { computed, reactive, ref, watch } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { computed, reactive, ref } from "vue";
import {
  formatDisplayTime,
  mapEnabledFromApi,
  mapTemplateFromApi,
  unwrapTemplateDetail,
  unwrapTemplateList,
} from "../approve-template/approveTemplateConstants.js";
import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
import {
  APPROVAL_TYPE_OPTIONS,
  SUBMIT_TEMPLATES,
  approvalModeLabel,
  approvalStatusLabel,
  approvalStatusTagType,
  approvalTypeLabel,
  buildApprovalInstanceListParams,
  buildApproveInstanceDto,
  buildEditFormFromInstanceRow,
  buildInstanceDto,
  clearLegacyApproveListStorage,
  createEmptySubmitForm,
  createInitialMockRows,
  loadStoredRows,
  saveStoredRows,
  buildDefaultFlowNodes,
  mapInstanceFromApi,
  mapSubmitTemplateCard,
  validateSubmitFlowNodes,
  unwrapInstancePage,
} from "./approveListConstants.js";
import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
function advanceFlow(row, result, opinion) {
  const nodes = row.approvalFlowNodes || [];
  const idx = row.currentNodeIndex ?? 0;
  const node = nodes[idx];
  if (!node) return;
  node.nodeStatus = result === "approved" ? "finish" : "error";
  node.approveOpinion = opinion || (result === "approved" ? "同意" : "驳回");
  node.approveTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
  row.approvalRecords = row.approvalRecords || [];
  row.approvalRecords.push({
    operatorName: node.approverName || "审批人",
    result,
    opinion: node.approveOpinion,
    time: node.approveTime,
  });
  if (result === "rejected") {
    row.approvalStatus = "rejected";
    row.rejectReason = opinion || node.approveOpinion;
    return;
  }
  const next = idx + 1;
  if (next < nodes.length) {
    row.currentNodeIndex = next;
    nodes[next].nodeStatus = "process";
    row.approvalStatus = "pending";
  } else {
    row.approvalStatus = "approved";
    row.rejectReason = "";
  }
}
export function useApproveList() {
  clearLegacyApproveListStorage();
  const userStore = useUserStore();
  const stored = loadStoredRows();
  const allRows = ref(stored?.length ? stored : createInitialMockRows());
  const tableData = ref([]);
  const submitTemplateCards = ref([]);
  const submitTemplatesLoading = ref(false);
  const searchForm = reactive({
    approvalType: "",
@@ -67,59 +62,37 @@
  const approveDialog = reactive({ visible: false, row: null });
  const approveOpinion = ref("");
  const approveSubmitting = ref(false);
  const submitDialog = reactive({ visible: false, step: 1 });
  const submitDialog = reactive({ visible: false, step: 1, mode: "add" });
  const submitEditRow = ref(null);
  const submitForm = reactive(createEmptySubmitForm(""));
  const submitFormRef = ref();
  const submitSaving = ref(false);
  const filteredList = computed(() => {
    let list = [...allRows.value];
    if (searchForm.approvalType) {
      list = list.filter((r) => r.approvalType === searchForm.approvalType);
  const isSubmitEdit = computed(() => submitDialog.mode === "edit");
  const submitDialogTitle = computed(() => {
    if (submitDialog.mode === "edit") {
      return `修改${activeTemplate.value?.label || submitForm.templateName || "审批"}`;
    }
    const kw = (searchForm.applicantKeyword || "").trim().toLowerCase();
    if (kw) {
      list = list.filter((r) => {
        const name = (r.applicantName || "").toLowerCase();
        const no = (r.applicantNo || "").toLowerCase();
        return name.includes(kw) || no.includes(kw);
      });
    }
    const range = searchForm.createTimeRange;
    if (range?.length === 2) {
      const [from, to] = range;
      list = list.filter((r) => {
        const t = (r.createTime || "").slice(0, 10);
        return t && t >= from && t <= to;
      });
    }
    return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
    if (submitDialog.step === 1) return "选择审批模板";
    return `提交${activeTemplate.value?.label || "审批"}`;
  });
  watch(
    filteredList,
    (list) => {
      page.total = list.length;
      const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
      if (page.current > maxPage) page.current = maxPage;
    },
    { immediate: true }
  );
  const activeTemplate = computed(() => submitForm.templateSnapshot || null);
  const tableData = computed(() => {
    const start = (page.current - 1) * page.size;
    return filteredList.value.slice(start, start + page.size);
  /** å¡«æŠ¥é¡¹å®šä¹‰ï¼ˆæ–°å¢ž/修改与 formConfig ä¸€è‡´ï¼‰ */
  const submitFormFields = computed(() => {
    const tplFields = activeTemplate.value?.fields;
    if (tplFields?.length) return tplFields;
    return submitForm.formFieldDefs || [];
  });
  const activeTemplate = computed(
    () => submitForm.templateSnapshot || SUBMIT_TEMPLATES[submitForm.templateKey] || null
  );
  const submitFormRules = computed(() => {
    const rules = {
      templateKey: [{ required: true, message: "请选择审批类型", trigger: "change" }],
    };
    (activeTemplate.value?.fields || []).forEach((f) => {
    submitFormFields.value.forEach((f) => {
      if (!f.required) return;
      if (f.type === "number") {
        rules[`formPayload.${f.key}`] = [{ required: true, message: `请填写${f.label}`, trigger: "blur" }];
@@ -143,14 +116,7 @@
      slot: "approveType",
    },
    {
      label: "审批方式",
      prop: "approvalMode",
      width: 90,
      dataType: "slot",
      slot: "approvalMethod",
    },
    {
      label: "是否未读",
      label: "待我审批",
      prop: "unread",
      width: 90,
      align: "center",
@@ -164,35 +130,82 @@
      formatData: (v) => approvalStatusLabel(v),
      formatType: (v) => approvalStatusTagType(v),
    },
    { label: "创建时间", prop: "createTime", width: 170 },
    {
      label: "创建时间",
      prop: "createTime",
      width: 170,
      formatData: (v) => formatDisplayTime(v),
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 160,
      width: 240,
      operation: [
        { name: "详情", type: "text", clickFun: (row) => openDetail(row) },
        {
          name: "审批",
          name: "修改",
          type: "text",
          disabled: (row) => row.approvalStatus !== "pending",
          clickFun: (row) => openEditDialog(row),
        },
        {
          name: "审批",
          type: "text",
          disabled: (row) => row.approvalStatus !== "pending" || !row.isApprove,
          clickFun: (row) => openApprove(row),
        },
        {
          name: "删除",
          type: "danger",
          clickFun: (row) => removeInstance(row),
        },
      ],
    },
  ]);
  function persist() {
    saveStoredRows(allRows.value);
  async function fetchApprovalList() {
    tableLoading.value = true;
    try {
      const res = await listApprovalInstancePage(
        buildApprovalInstanceListParams({ page, searchForm })
      );
      const { records, total } = unwrapInstancePage(res);
      tableData.value = records.map(mapInstanceFromApi);
      page.total = total;
    } catch {
      tableData.value = [];
      page.total = 0;
      ElMessage.error("审批列表加载失败");
    } finally {
      tableLoading.value = false;
    }
  }
  async function loadSubmitTemplates() {
    submitTemplatesLoading.value = true;
    try {
      const [builtinRes, customRes] = await Promise.all([
        listApprovalTemplate(TEMPLATE_TYPE_BUILTIN),
        listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
      ]);
      const merged = [
        ...unwrapTemplateList(builtinRes),
        ...unwrapTemplateList(customRes),
      ].filter((row) => mapEnabledFromApi(row.enabled));
      submitTemplateCards.value = merged.map(mapSubmitTemplateCard);
    } catch {
      submitTemplateCards.value = [];
      ElMessage.error("加载审批模板失败");
    } finally {
      submitTemplatesLoading.value = false;
    }
  }
  function handleQuery() {
    tableLoading.value = true;
    page.current = 1;
    setTimeout(() => {
      tableLoading.value = false;
    }, 200);
    fetchApprovalList();
  }
  function resetSearch() {
@@ -205,50 +218,81 @@
  function pagination({ page: p, limit }) {
    page.current = p;
    page.size = limit;
  }
  function markRead(row) {
    if (!row.unread) return;
    const hit = allRows.value.find((r) => r.id === row.id);
    if (hit) {
      hit.unread = false;
      persist();
    }
    fetchApprovalList();
  }
  function openDetail(row) {
    markRead(row);
    detailRow.value = { ...row };
    detailDialog.visible = true;
  }
  function openApprove(row) {
    markRead(row);
    approveDialog.row = { ...row };
    approveOpinion.value = "";
    approveDialog.visible = true;
  }
  function openSubmitDialog() {
    Object.assign(submitForm, createEmptySubmitForm(""));
  function resetSubmitDialogState() {
    submitDialog.mode = "add";
    submitDialog.step = 1;
    submitEditRow.value = null;
    Object.assign(submitForm, createEmptySubmitForm(""));
  }
  function openSubmitDialog() {
    resetSubmitDialogState();
    submitDialog.visible = true;
    loadSubmitTemplates();
  }
  function openEditDialog(row) {
    if (row?.approvalStatus !== "pending") {
      ElMessage.warning("仅审核中的审批可修改");
      return;
    }
    if (!row?.id) {
      ElMessage.warning("无法修改:缺少审批实例 ID");
      return;
    }
    submitDialog.mode = "edit";
    submitDialog.step = 2;
    submitEditRow.value = { ...row };
    Object.assign(submitForm, buildEditFormFromInstanceRow(row));
    submitDialog.visible = true;
  }
  /** @param {string} key å†…置模板 key æˆ–自定义 id */
  function onTemplatePick(key, templateRow) {
    const base = templateRow
      ? createEmptySubmitForm(key, buildSubmitTemplateFromRow(templateRow))
      : createEmptySubmitForm(key);
    Object.assign(submitForm, {
      ...base,
      templateSnapshot: templateRow ? buildSubmitTemplateFromRow(templateRow) : null,
    });
    submitDialog.step = 2;
  async function onTemplatePick(card) {
    if (!card?.id) return;
    submitTemplatesLoading.value = true;
    try {
      const res = await getApprovalTemplateDetail(card.id);
      const mapped = mapTemplateFromApi(unwrapTemplateDetail(res));
      const tpl = {
        ...buildSubmitTemplateFromRow(mapped),
        templateId: mapped.id,
      };
      const base = createEmptySubmitForm(String(card.id), tpl, mapped.flowNodes);
      Object.assign(submitForm, {
        ...base,
        templateName: mapped.templateName || tpl.label || "",
        templateSnapshot: tpl,
        formFieldDefs: tpl.fields || [],
      });
      submitDialog.step = 2;
    } catch {
      ElMessage.error("加载模板详情失败");
    } finally {
      submitTemplatesLoading.value = false;
    }
  }
  function backToTemplatePick() {
    submitDialog.step = 1;
  }
  async function submitInstanceForm() {
    if (submitDialog.mode === "edit") return submitEditApproval();
    return submitNewApproval();
  }
  async function submitNewApproval() {
@@ -258,56 +302,143 @@
    } catch {
      return false;
    }
    const tpl = activeTemplate.value;
    if (!tpl) return false;
    const id = `user_${Date.now()}`;
    const summary =
      submitForm.formPayload.summary ||
      submitForm.formPayload.handoverTo ||
      `${tpl.label}申请`;
    const row = {
      id,
      bizId: `BIZ${dayjs().format("YYYYMMDDHHmmss")}`,
      applicantNo: userStore.name || String(userStore.id || "当前用户"),
      applicantName: userStore.nickName || userStore.name || "当前用户",
      approvalType: tpl.approvalType,
      approvalMode: submitForm.approvalMode,
      unread: false,
      approvalStatus: "pending",
      createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
      summary,
      formPayload: { ...submitForm.formPayload },
      approvalFlowNodes: (submitForm.approvalFlowNodes?.length
        ? submitForm.approvalFlowNodes
        : buildDefaultFlowNodes()
      ).map((n, i) => ({ ...n, nodeStatus: i === 0 ? "process" : n.nodeStatus || "wait" })),
      currentNodeIndex: 0,
      approvalRecords: [],
      rejectReason: "",
    };
    allRows.value.unshift(row);
    persist();
    submitDialog.visible = false;
    page.current = 1;
    return true;
    if (!activeTemplate.value) return false;
    const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
    if (!flowCheck.ok) {
      ElMessage.warning(flowCheck.message);
      return false;
    }
    if (!submitForm.templateId) {
      ElMessage.warning("缺少模板 ID,无法提交");
      return false;
    }
    if (submitSaving.value) return false;
    submitSaving.value = true;
    try {
      await saveApprovalInstance(
        buildInstanceDto({
          submitForm,
          activeTemplate: activeTemplate.value,
          userStore,
          flowNodes: flowCheck.nodes,
        })
      );
      submitDialog.visible = false;
      page.current = 1;
      await fetchApprovalList();
      return true;
    } catch {
      return false;
    } finally {
      submitSaving.value = false;
    }
  }
  function submitApprove(result) {
  async function submitEditApproval() {
    if (!submitFormRef.value) return false;
    try {
      await submitFormRef.value.validate();
    } catch {
      return false;
    }
    if (!activeTemplate.value) return false;
    const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
    if (!flowCheck.ok) {
      ElMessage.warning(flowCheck.message);
      return false;
    }
    if (!submitForm.instanceId) {
      ElMessage.warning("缺少审批实例 ID,无法保存");
      return false;
    }
    if (submitSaving.value) return false;
    submitSaving.value = true;
    try {
      await updateApprovalInstance(
        buildInstanceDto({
          submitForm,
          activeTemplate: activeTemplate.value,
          flowNodes: flowCheck.nodes,
          existingRow: submitEditRow.value,
        })
      );
      submitDialog.visible = false;
      await fetchApprovalList();
      if (detailDialog.visible && detailRow.value?.id === submitForm.instanceId) {
        const hit = tableData.value.find((r) => r.id === submitForm.instanceId);
        if (hit) detailRow.value = { ...hit };
        else detailDialog.visible = false;
      }
      return true;
    } catch {
      return false;
    } finally {
      submitSaving.value = false;
    }
  }
  async function removeInstance(row) {
    if (row?.id == null || row.id === "") {
      ElMessage.warning("无法删除:缺少审批实例 ID");
      return;
    }
    const title = row.title || row.templateName || row.instanceNo || "该审批";
    try {
      await ElMessageBox.confirm(
        `确定要删除审批「${title}」吗?删除后不可恢复。`,
        "删除确认",
        {
          type: "warning",
          confirmButtonText: "确定删除",
          cancelButtonText: "取消",
          distinguishCancelAndClose: true,
          autofocus: false,
        }
      );
    } catch {
      return;
    }
    try {
      await deleteApprovalInstance([row.id]);
      ElMessage.success("删除成功");
      if (detailDialog.visible && detailRow.value?.id === row.id) {
        detailDialog.visible = false;
      }
      if (approveDialog.visible && approveDialog.row?.id === row.id) {
        approveDialog.visible = false;
      }
      await fetchApprovalList();
    } catch {
      /* é”™è¯¯ç”±æ‹¦æˆªå™¨æç¤º */
    }
  }
  async function submitApprove(result) {
    const row = approveDialog.row;
    if (!row) return;
    const hit = allRows.value.find((r) => r.id === row.id);
    if (!hit || hit.approvalStatus !== "pending") return;
    if (!row?.id) return { ok: false };
    if (result === "rejected" && !(approveOpinion.value || "").trim()) {
      return { needOpinion: true };
    }
    advanceFlow(hit, result, (approveOpinion.value || "").trim());
    hit.unread = false;
    persist();
    approveDialog.visible = false;
    if (detailDialog.visible && detailRow.value?.id === hit.id) {
      detailRow.value = { ...hit };
    if (approveSubmitting.value) return { ok: false };
    approveSubmitting.value = true;
    try {
      await approveApprovalInstance(
        buildApproveInstanceDto(row, result, approveOpinion.value)
      );
      approveDialog.visible = false;
      await fetchApprovalList();
      if (detailDialog.visible && detailRow.value?.id === row.id) {
        const hit = tableData.value.find((r) => r.id === row.id);
        if (hit) detailRow.value = { ...hit };
        else detailDialog.visible = false;
      }
      return { ok: true, result };
    } catch {
      ElMessage.error("审批操作失败");
      return { ok: false };
    } finally {
      approveSubmitting.value = false;
    }
    return { ok: true };
  }
  function approvalActionLabel(result) {
@@ -319,9 +450,7 @@
  return {
    Search,
    APPROVAL_TYPE_OPTIONS,
    SUBMIT_TEMPLATES,
    approvalTypeLabel,
    approvalModeLabel,
    approvalStatusLabel,
    approvalStatusTagType,
    approvalActionLabel,
@@ -334,20 +463,31 @@
    detailRow,
    approveDialog,
    approveOpinion,
    approveSubmitting,
    submitDialog,
    isSubmitEdit,
    submitDialogTitle,
    submitForm,
    submitFormRef,
    submitSaving,
    activeTemplate,
    submitFormFields,
    submitFormRules,
    submitTemplateCards,
    submitTemplatesLoading,
    handleQuery,
    resetSearch,
    pagination,
    resetSubmitDialogState,
    openSubmitDialog,
    openEditDialog,
    onTemplatePick,
    backToTemplatePick,
    submitInstanceForm,
    submitNewApproval,
    submitApprove,
    openDetail,
    openApprove,
    fetchApprovalList,
  };
}