From a1a9521e1f537d742c4f3ebada9b102bfefa6583 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期二, 19 五月 2026 16:21:13 +0800
Subject: [PATCH] 审批列表

---
 src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js                  |  474 +++++++++-----
 src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue                          |  310 +++++---
 src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue   |  116 +++
 src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js            |  680 +++++++++++++-------
 src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue |  147 ++++
 src/api/officeProcessAutomation/approvalInstance.js                                             |   47 +
 src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue  |  117 +-
 7 files changed, 1,296 insertions(+), 595 deletions(-)

diff --git a/src/api/officeProcessAutomation/approvalInstance.js b/src/api/officeProcessAutomation/approvalInstance.js
new file mode 100644
index 0000000..054861c
--- /dev/null
+++ b/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,
+  });
+}
+
+/** 鍒犻櫎瀹℃壒瀹炰緥锛坆ody 涓� 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,
+  });
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index e4bb66f..6d8d1a5 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/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 瑙f瀽濉姤瀛楁瀹氫箟涓� 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: "鎷熻浆姝f棩鏈�", 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";
+/** 瑙f瀽瀹炰緥 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 澶�",
-    "绂昏亴宸ヤ綔浜ゆ帴",
-    "璇曠敤鏈熻浆姝g敵璇�",
-    "涓汉鍘熷洜绂昏亴",
-    "璋冭嚦閿�鍞儴",
-    "瀹㈡埛鎺ュ緟椁愯垂",
-    "鐥呭亣 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,
   };
 }
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
index 19328af..f54c167 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
+++ b/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);
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
new file mode 100644
index 0000000..6cdc627
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
new file mode 100644
index 0000000..e5f2eef
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
@@ -0,0 +1,147 @@
+<!-- 瀹℃壒瀹炰緥锛歵asks 瀹℃壒娴佺▼灞曠ず锛堟í鍚戞楠ゆ潯锛� -->
+<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>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index 774b322..cdae763 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/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>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index c3c3241..fa9fade 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/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,
   };
 }

--
Gitblit v1.9.3