From 930d38ed2a3c2131be3305a585602c7a5a275fe3 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期二, 19 五月 2026 17:09:12 +0800
Subject: [PATCH] Merge branch 'dev-new_pro_OA' of http://114.132.189.42:9002/r/product-inventory-management into dev-new_pro_OA

---
 src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue   |  624 ++++++++++
 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/components/ApproveDetailPanel.vue     |  117 -
 src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js                |  278 ++++
 src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js             |  263 ++-
 src/api/officeProcessAutomation/approvalTemplate.js                                                |   63 +
 src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js                     |  465 +++++--
 src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js       |  311 +++-
 src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js               |  686 +++++++----
 src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue    |  147 ++
 src/api/officeProcessAutomation/approvalInstance.js                                                |   47 
 src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue |   16 
 src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue                         |   89 +
 14 files changed, 2,746 insertions(+), 786 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/api/officeProcessAutomation/approvalTemplate.js b/src/api/officeProcessAutomation/approvalTemplate.js
new file mode 100644
index 0000000..4aef140
--- /dev/null
+++ b/src/api/officeProcessAutomation/approvalTemplate.js
@@ -0,0 +1,63 @@
+import request from "@/utils/request";
+
+/** 妯℃澘绫诲瀷锛�0 绯荤粺鍐呯疆锛�1 鑷畾涔夛紙涓庡悗绔� templateType 涓�鑷达級 */
+export const TEMPLATE_TYPE_BUILTIN = 0;
+export const TEMPLATE_TYPE_CUSTOM = 1;
+
+export const TEMPLATE_TYPE_OPTIONS = [
+  { value: TEMPLATE_TYPE_BUILTIN, label: "绯荤粺鍐呯疆" },
+  { value: TEMPLATE_TYPE_CUSTOM, label: "鑷畾涔�" },
+];
+
+/** 鏌ヨ鎵�鏈夊鎵规ā鏉� */
+export function listApprovalTemplate(type) {
+  return request({
+    url: `/approvalTemplate/list/${type}`,
+    method: "get",
+  });
+}
+
+/** 鍒嗛〉鏌ヨ瀹℃壒妯℃澘 */
+export function listApprovalTemplatePage(params) {
+  return request({
+    url: "/approvalTemplate/listPage",
+    method: "get",
+    params,
+  });
+}
+
+/** 鏌ヨ瀹℃壒妯℃澘璇︽儏 */
+export function getApprovalTemplateDetail(id) {
+  return request({
+    url: `/approvalTemplate/detail/${id}`,
+    method: "get",
+  });
+}
+
+/** 鏂板瀹℃壒妯℃澘锛坆ody 涓� ApprovalTemplateDto锛� */
+export function addApprovalTemplate(approvalTemplateDto) {
+  return request({
+    url: "/approvalTemplate/add",
+    method: "post",
+    data: approvalTemplateDto,
+  });
+}
+
+/** 淇敼瀹℃壒妯℃澘锛坆ody 涓� ApprovalTemplateDto锛� */
+export function updateApprovalTemplate(approvalTemplateDto) {
+  return request({
+    url: "/approvalTemplate/update",
+    method: "put",
+    data: approvalTemplateDto,
+  });
+}
+
+/** 鍒犻櫎瀹℃壒妯℃澘锛坆ody 涓烘ā鏉� ID 鏁扮粍锛� */
+export function deleteApprovalTemplate(ids) {
+  const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
+  return request({
+    url: "/approvalTemplate/delete",
+    method: "post",
+    data: idList,
+  });
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index 447627a..80af992 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -1,4 +1,13 @@
-import dayjs from "dayjs";
+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 = [
@@ -25,105 +34,401 @@
   { 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,
+    businessName: row.businessName || "",
+    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) {
@@ -147,170 +452,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) {
-  const tpl = SUBMIT_TEMPLATES[templateKey];
-  const payload = { summary: "" };
-  (tpl?.fields || []).forEach((f) => {
-    if (f.type === "number") payload[f.key] = undefined;
-    else if (f.type === "datetimerange") payload[f.key] = [];
-    else payload[f.key] = "";
-  });
+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 || "",
-    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 48103aa..337b00d 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -1,56 +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";
 
-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: "",
@@ -66,57 +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(() => 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" }];
@@ -132,6 +108,7 @@
   const tableColumn = ref([
     { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
     { label: "鐢宠浜哄悕绉�", prop: "applicantName", minWidth: 100 },
+    { label: "涓氬姟绫诲瀷", prop: "businessName", minWidth: 120 },
     {
       label: "瀹℃壒绫诲瀷",
       prop: "approvalType",
@@ -140,14 +117,7 @@
       slot: "approveType",
     },
     {
-      label: "瀹℃壒鏂瑰紡",
-      prop: "approvalMode",
-      width: 90,
-      dataType: "slot",
-      slot: "approvalMethod",
-    },
-    {
-      label: "鏄惁鏈",
+      label: "寰呮垜瀹℃壒",
       prop: "unread",
       width: 90,
       align: "center",
@@ -161,35 +131,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() {
@@ -202,43 +219,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;
   }
 
-  function onTemplatePick(key) {
-    Object.assign(submitForm, createEmptySubmitForm(key));
-    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() {
@@ -248,56 +303,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) {
@@ -309,9 +451,7 @@
   return {
     Search,
     APPROVAL_TYPE_OPTIONS,
-    SUBMIT_TEMPLATES,
     approvalTypeLabel,
-    approvalModeLabel,
     approvalStatusLabel,
     approvalStatusTagType,
     approvalActionLabel,
@@ -324,20 +464,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,
   };
 }
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
index 81884a1..3325e55 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -1,5 +1,23 @@
 import dayjs from "dayjs";
-import { APPROVAL_TYPE_OPTIONS, SUBMIT_TEMPLATES } from "../approve-list/approveListConstants.js";
+import {
+  TEMPLATE_TYPE_CUSTOM,
+  TEMPLATE_TYPE_OPTIONS,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js";
+import {
+  buildFormConfigJson,
+  createEmptyFormConfigData,
+  parseFormConfigToData,
+  validateFormConfigData,
+} from "./formConfigUtils.js";
+
+export { TEMPLATE_TYPE_OPTIONS };
+
+export function templateTypeLabel(type) {
+  if (type == null || type === "") return "鈥�";
+  const n = Number(type);
+  return TEMPLATE_TYPE_OPTIONS.find((x) => x.value === n)?.label || "鈥�";
+}
 
 /** 鑺傜偣鍐呭鎵规柟寮忥細浼氱 / 鎴栫 */
 export const NODE_SIGN_MODE_OPTIONS = [
@@ -7,18 +25,205 @@
   { value: "or_sign", label: "鎴栫", desc: "鏈妭鐐逛换涓�瀹℃壒浜洪�氳繃鍗冲彲" },
 ];
 
-export const STORAGE_KEY = "oa_approve_template_custom_v1";
+function parseFormConfig(formConfig) {
+  if (!formConfig) return {};
+  if (typeof formConfig === "object") return formConfig;
+  try {
+    return JSON.parse(formConfig);
+  } catch {
+    return {};
+  }
+}
 
-/** 绯荤粺鍐呯疆甯哥敤瀹℃壒锛堝彧璇诲睍绀猴紝鏉ユ簮浜庡鎵瑰垪琛ㄦ彁浜ゆā鏉匡級 */
-export function getBuiltinTemplates() {
-  return Object.entries(SUBMIT_TEMPLATES).map(([key, tpl]) => ({
-    key,
-    approvalType: tpl.approvalType,
-    label: tpl.label,
-    summary: tpl.summaryPlaceholder || "绯荤粺棰勭疆濉姤瀛楁",
-    fieldCount: (tpl.fields || []).length,
-    defaultMode: tpl.approvalMode,
+function resolveDefaultMode(row, cfg, nodes) {
+  let mode = cfg.approvalMode || cfg.defaultMode;
+  if (!mode && nodes.length) {
+    const t = String(nodes[0]?.approveType || "").toUpperCase();
+    mode = t === "OR" ? "or_sign" : "parallel";
+  }
+  const m = String(mode || "").toLowerCase();
+  if (m === "or" || m === "or_sign") return "or_sign";
+  return "parallel";
+}
+
+/** 灏嗘帴鍙h繑鍥炵殑妯℃澘杞负銆岀郴缁熷父鐢ㄥ鎵广�嶅崱鐗囨暟鎹� */
+export function mapBuiltinCardFromApi(row) {
+  const cfg = parseFormConfig(row?.formConfig);
+  const fields = cfg.fields || cfg.formFields || [];
+  const nodes = row?.nodes || row?.flowNodes || [];
+  return {
+    key: String(row?.id ?? row?.templateName ?? ""),
+    id: row?.id,
+    approvalType: cfg.approvalType || row?.approvalType || "",
+    label: row?.templateName || row?.name || "鈥�",
+    summary: (row?.description || "").trim() || cfg.summaryPlaceholder || "绯荤粺棰勭疆濉姤瀛楁",
+    fieldCount: fields.length,
+    defaultMode: resolveDefaultMode(row, cfg, nodes),
+  };
+}
+
+export function unwrapTemplateList(payload) {
+  const data = payload?.data ?? payload;
+  if (Array.isArray(data)) return data;
+  if (Array.isArray(data?.records)) return data.records;
+  if (Array.isArray(data?.list)) return data.list;
+  return [];
+}
+
+/** 鍚庣 approveType 鈫� 椤甸潰 signMode */
+export function mapSignModeFromApi(approveType) {
+  const t = String(approveType || "").toUpperCase();
+  return t === "OR" ? "or_sign" : "countersign";
+}
+
+/** 椤甸潰 signMode 鈫� 鍚庣 approveType */
+export function mapSignModeToApi(signMode) {
+  return signMode === "or_sign" ? "OR" : "AND";
+}
+
+/** 椤甸潰 enabled 鈫� 鍚庣 enabled锛�1 鍚敤锛�0 鍋滅敤锛� */
+export function mapEnabledToApi(enabled) {
+  return enabled !== false ? "1" : "0";
+}
+
+/** 鍚庣 nodes 鈫� 椤甸潰 flowNodes锛堜繚鐣� id 渚涗慨鏀规彁浜わ級 */
+export function mapNodesFromApi(nodes) {
+  const list = Array.isArray(nodes) ? nodes : [];
+  return list.map((n, i) => ({
+    id: n.id,
+    templateId: n.templateId,
+    nodeOrder: n.levelNo ?? i + 1,
+    signMode: mapSignModeFromApi(n.approveType ?? n.signMode),
+    approvers: (n.approvers || [])
+      .filter((a) => a?.approverId != null && a.approverId !== "")
+      .map((a) => ({
+        id: a.id,
+        nodeId: a.nodeId,
+        templateId: a.templateId,
+        approverId: a.approverId,
+        approverName: a.approverName || "",
+      })),
   }));
+}
+
+/** enabled锛�1 鍚敤锛�0 鍋滅敤 */
+export function mapEnabledFromApi(enabled) {
+  return enabled === "1" || enabled === 1 || enabled === true;
+}
+
+/** 鍏煎澶氱鍚庣鏃堕棿瀛楁鍚嶅苟鏍煎紡鍖栧睍绀� */
+export function pickTemplateTimes(row) {
+  const rawCreated =
+    row?.createdTime ?? row?.createTime ?? row?.gmtCreate ?? row?.created_at ?? "";
+  const rawUpdated =
+    row?.updatedTime ?? row?.updateTime ?? row?.gmtModified ?? row?.modifyTime ?? row?.updated_at ?? "";
+  const createdTime = normalizeTimeValue(rawCreated);
+  const updatedTime = normalizeTimeValue(rawUpdated);
+  return { createdTime, updatedTime, createTime: createdTime, updateTime: updatedTime };
+}
+
+function normalizeTimeValue(val) {
+  if (val == null || val === "") return "";
+  if (Array.isArray(val) && val.length >= 3) {
+    const [y, m, d, h = 0, min = 0, s = 0] = val;
+    return dayjs(new Date(y, m - 1, d, h, min, s)).format("YYYY-MM-DD HH:mm:ss");
+  }
+  if (typeof val === "number") {
+    const d = val > 1e12 ? dayjs(val) : dayjs.unix(val);
+    return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : "";
+  }
+  const s = String(val).trim();
+  if (!s) return "";
+  const parsed = dayjs(s.includes("T") ? s : s.replace(/-/g, "/"));
+  return parsed.isValid() ? parsed.format("YYYY-MM-DD HH:mm:ss") : s;
+}
+
+export function formatDisplayTime(val) {
+  const t = normalizeTimeValue(val);
+  return t || "鈥�";
+}
+
+/** 璇︽儏鎺ュ彛 data 瑙e寘 */
+export function unwrapTemplateDetail(res) {
+  const data = res?.data ?? res;
+  if (!data || typeof data !== "object") return {};
+  if (data.templateName != null || data.id != null) return data;
+  if (data.approvalTemplateVo) return data.approvalTemplateVo;
+  if (data.records && data.records[0]) return data.records[0];
+  return data;
+}
+
+/** 鍒嗛〉鍒楄〃椤� 鈫� 椤甸潰琛屾暟鎹紙涓昏〃 + 鑺傜偣锛� */
+export function mapTemplateFromApi(row) {
+  if (!row) return {};
+  const flowNodes = mapNodesFromApi(row.nodes || row.flowNodes);
+  const times = pickTemplateTimes(row);
+  return {
+    id: row.id,
+    templateName: row.templateName || "",
+    description: row.description || "",
+    enabled: mapEnabledFromApi(row.enabled),
+    enabledRaw: row.enabled,
+    templateType: row.templateType != null ? Number(row.templateType) : undefined,
+    formConfig: row.formConfig,
+    formConfigData: parseFormConfigToData(row.formConfig),
+    createdUser: row.createdUser,
+    createdUserName: row.createdUserName,
+    ...times,
+    flowNodes,
+    nodes: row.nodes || row.flowNodes,
+  };
+}
+
+/** 琛ㄥ崟鏁版嵁 鈫� 鎻愪氦 DTO锛圓pprovalTemplateDto锛� */
+export function mapTemplateToApi(form) {
+  const nodes = normalizeFlowNodes(form.flowNodes);
+  const templateId = form.id || null;
+  const dto = {
+    templateName: (form.templateName || "").trim(),
+    description: (form.description || "").trim(),
+    enabled: mapEnabledToApi(form.enabled),
+    templateType: form.templateType ?? TEMPLATE_TYPE_CUSTOM,
+    formConfig: buildFormConfigJson(form.formConfigData),
+    nodes: nodes.map((n, i) => {
+      const node = {
+        levelNo: n.nodeOrder ?? i + 1,
+        approveType: mapSignModeToApi(n.signMode),
+        approvers: n.approvers.map((a, idx) => {
+          const approver = {
+            approverId: a.approverId,
+            approverName: a.approverName || "",
+            sortNo: idx + 1,
+          };
+          if (a.id != null) approver.id = a.id;
+          if (a.nodeId != null) approver.nodeId = a.nodeId;
+          if (a.templateId != null) approver.templateId = a.templateId;
+          else if (templateId) approver.templateId = templateId;
+          return approver;
+        }),
+      };
+      if (n.id != null) node.id = n.id;
+      if (n.templateId != null) node.templateId = n.templateId;
+      else if (templateId) node.templateId = templateId;
+      return node;
+    }),
+  };
+  if (templateId) dto.id = templateId;
+  return dto;
+}
+
+export function buildApprovalTemplateListParams({ page, searchForm, templateType = TEMPLATE_TYPE_CUSTOM }) {
+  const params = {
+    current: page.current,
+    size: page.size,
+    templateType: searchForm?.templateType != null && searchForm.templateType !== ""
+      ? searchForm.templateType
+      : templateType,
+  };
+  const kw = (searchForm?.keyword || "").trim();
+  if (kw) params.templateName = kw;
+  if (searchForm?.enabledOnly) params.enabled = "1";
+  return params;
 }
 
 export function nodeSignModeLabel(mode) {
@@ -42,6 +247,9 @@
     id: "",
     templateName: "",
     description: "",
+    templateType: TEMPLATE_TYPE_CUSTOM,
+    formConfig: "",
+    formConfigData: createEmptyFormConfigData(),
     enabled: true,
     flowNodes: [createEmptyNode(1)],
   };
@@ -50,11 +258,16 @@
 export function normalizeFlowNodes(nodes) {
   const list = Array.isArray(nodes) ? nodes : [];
   return list.map((n, i) => ({
+    id: n.id,
+    templateId: n.templateId,
     nodeOrder: i + 1,
     signMode: n.signMode === "or_sign" ? "or_sign" : "countersign",
     approvers: (n.approvers || [])
       .filter((a) => a?.approverId != null && a.approverId !== "")
       .map((a) => ({
+        id: a.id,
+        nodeId: a.nodeId,
+        templateId: a.templateId,
         approverId: a.approverId,
         approverName: a.approverName || "",
       })),
@@ -71,6 +284,8 @@
       return { ok: false, message: `璇蜂负绗� ${i + 1} 涓妭鐐归�夋嫨鑷冲皯涓�鍚嶅鎵逛汉` };
     }
   }
+  const cfgCheck = validateFormConfigData(form.formConfigData);
+  if (!cfgCheck.ok) return cfgCheck;
   return { ok: true, nodes, name };
 }
 
@@ -83,78 +298,4 @@
       return `鑺傜偣${i + 1}(${nodeSignModeLabel(n.signMode)}:${names})`;
     })
     .join(" 鈫� ");
-}
-
-export function createInitialMockTemplates() {
-  const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
-  return [
-    {
-      id: "tpl_demo_1",
-      templateName: "椤圭洰绔嬮」瀹℃壒",
-      description: "璺ㄩ儴闂ㄩ」鐩珛椤癸紝闇�鎶�鏈�佽储鍔′緷娆′細绛�",
-      enabled: true,
-      createTime: dayjs().subtract(5, "day").format("YYYY-MM-DD HH:mm:ss"),
-      updateTime: now,
-      flowNodes: [
-        {
-          nodeOrder: 1,
-          signMode: "countersign",
-          approvers: [
-            { approverId: "mock_tech_lead", approverName: "鎶�鏈礋璐d汉" },
-            { approverId: "mock_pm", approverName: "椤圭洰缁忕悊" },
-          ],
-        },
-        {
-          nodeOrder: 2,
-          signMode: "or_sign",
-          approvers: [
-            { approverId: "mock_finance", approverName: "璐㈠姟涓荤" },
-            { approverId: "mock_cfo", approverName: "璐㈠姟鎬荤洃" },
-          ],
-        },
-      ],
-    },
-    {
-      id: "tpl_demo_2",
-      templateName: "鍚堝悓鐢ㄥ嵃鐢宠",
-      description: "娉曞姟涓庤鏀挎垨绛惧悗锛屾�荤粡鐞嗙粓瀹�",
-      enabled: true,
-      createTime: dayjs().subtract(12, "day").format("YYYY-MM-DD HH:mm:ss"),
-      updateTime: dayjs().subtract(2, "day").format("YYYY-MM-DD HH:mm:ss"),
-      flowNodes: [
-        {
-          nodeOrder: 1,
-          signMode: "or_sign",
-          approvers: [
-            { approverId: "mock_legal", approverName: "娉曞姟涓撳憳" },
-            { approverId: "mock_admin", approverName: "琛屾斂涓荤" },
-          ],
-        },
-        {
-          nodeOrder: 2,
-          signMode: "countersign",
-          approvers: [{ approverId: "mock_ceo", approverName: "鎬荤粡鐞�" }],
-        },
-      ],
-    },
-  ];
-}
-
-export function loadStoredTemplates() {
-  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 saveStoredTemplates(rows) {
-  try {
-    localStorage.setItem(STORAGE_KEY, JSON.stringify(rows));
-  } catch {
-    /* ignore */
-  }
 }
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
new file mode 100644
index 0000000..1881f60
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -0,0 +1,624 @@
+<!-- 瀹℃壒妯℃澘锛氬彲閰嶇疆濉姤椤癸紝搴忓垪鍖栧埌 formConfig -->
+<template>
+  <div class="fce">
+    <div class="fce-hint">
+      <span class="fce-hint-label">濉姤鎻愮ず</span>
+      <el-input
+        v-model="inner.summaryPlaceholder"
+        placeholder="濡傦細璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑"
+        maxlength="200"
+        show-word-limit
+        @input="emitOut"
+      />
+    </div>
+
+    <div class="fce-panel">
+      <div class="fce-toolbar">
+        <div class="fce-toolbar-left">
+          <span class="fce-title">濉姤椤归厤缃�</span>
+          <el-tag v-if="inner.fields.length" size="small" type="info" effect="plain">
+            鍏� {{ inner.fields.length }} 椤�
+          </el-tag>
+        </div>
+        <div class="fce-toolbar-actions">
+          <el-dropdown trigger="click" @command="applyPreset">
+            <el-button size="small">浠庨璁惧鍏�</el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item v-for="p in FORM_CONFIG_PRESETS" :key="p.key" :command="p.key">
+                  {{ p.label }}
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+          <el-button type="primary" size="small" :icon="Plus" @click="addField">娣诲姞濉姤椤�</el-button>
+        </div>
+      </div>
+
+      <el-empty
+        v-if="!inner.fields.length"
+        class="fce-empty"
+        description="鏆傛棤濉姤椤癸紝鍙坊鍔犳垨浠庨璁惧揩閫熷鍏�"
+        :image-size="72"
+      />
+
+      <div v-else class="fce-list">
+        <div
+          v-for="(field, index) in inner.fields"
+          :key="field._uid"
+          class="fce-card"
+          :class="{ 'fce-card--required': field.required }"
+        >
+          <div class="fce-card-badge">{{ index + 1 }}</div>
+
+          <div class="fce-card-head">
+            <div class="fce-card-title">
+              <span class="fce-card-name">{{ field.label || `濉姤椤� ${index + 1}` }}</span>
+              <el-tag size="small" effect="light" type="primary">{{ typeLabel(field.type) }}</el-tag>
+              <el-tag v-if="field.required" size="small" type="danger" effect="plain">蹇呭~</el-tag>
+            </div>
+            <div class="fce-card-btns">
+              <el-tooltip content="涓婄Щ" placement="top">
+                <el-button circle size="small" :disabled="index === 0" @click="moveField(index, -1)">
+                  <el-icon><Top /></el-icon>
+                </el-button>
+              </el-tooltip>
+              <el-tooltip content="涓嬬Щ" placement="top">
+                <el-button
+                  circle
+                  size="small"
+                  :disabled="index >= inner.fields.length - 1"
+                  @click="moveField(index, 1)"
+                >
+                  <el-icon><Bottom /></el-icon>
+                </el-button>
+              </el-tooltip>
+              <el-tooltip content="鍒犻櫎" placement="top">
+                <el-button circle size="small" type="danger" plain @click="removeField(index)">
+                  <el-icon><Delete /></el-icon>
+                </el-button>
+              </el-tooltip>
+            </div>
+          </div>
+
+          <div class="fce-section">
+            <span class="fce-section-title">鍩虹淇℃伅</span>
+            <el-row :gutter="16">
+              <el-col :span="8">
+                <el-form-item label="鏄剧ず鍚嶇О" required class="fce-field-item">
+                  <el-input
+                    v-model="field.label"
+                    placeholder="濡傦細鎶ラ攢璇存槑"
+                    maxlength="50"
+                    @input="emitOut"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="瀛楁鏍囪瘑" required class="fce-field-item">
+                  <el-input v-model="field.key" placeholder="濡傦細summary" maxlength="50" @input="emitOut" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="8">
+                <el-form-item label="鎺т欢绫诲瀷" class="fce-field-item">
+                  <el-select v-model="field.type" style="width: 100%" @change="onTypeChange(field)">
+                    <el-option
+                      v-for="t in FORM_FIELD_TYPE_OPTIONS"
+                      :key="t.value"
+                      :label="t.label"
+                      :value="t.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+
+          <div class="fce-section">
+            <span class="fce-section-title">鏍¢獙涓庢牸寮�</span>
+            <el-row :gutter="16" align="middle">
+              <el-col :span="8">
+                <el-form-item label="鏄惁蹇呭~" class="fce-field-item fce-field-item--switch">
+                  <el-switch
+                    v-model="field.required"
+                    inline-prompt
+                    active-text="蹇呭~"
+                    inactive-text="閫夊~"
+                    @change="emitOut"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col v-if="field.type === 'textarea'" :span="8">
+                <el-form-item label="琛屾暟" class="fce-field-item">
+                  <el-input-number
+                    v-model="field.rows"
+                    :min="1"
+                    :max="10"
+                    controls-position="right"
+                    style="width: 100%"
+                    @change="emitOut"
+                  />
+                </el-form-item>
+              </el-col>
+              <template v-if="field.type === 'number'">
+                <el-col :span="8">
+                  <el-form-item label="鏈�灏忓��" class="fce-field-item">
+                    <el-input-number
+                      v-model="field.min"
+                      controls-position="right"
+                      style="width: 100%"
+                      @change="emitOut"
+                    />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="灏忔暟浣�" class="fce-field-item">
+                    <el-input-number
+                      v-model="field.precision"
+                      :min="0"
+                      :max="4"
+                      controls-position="right"
+                      style="width: 100%"
+                      @change="emitOut"
+                    />
+                  </el-form-item>
+                </el-col>
+              </template>
+            </el-row>
+          </div>
+
+          <div class="fce-section fce-section--default">
+            <span class="fce-section-title">榛樿鍊�</span>
+            <p class="fce-section-desc">閫夋嫨璇ユā鏉挎彁浜ゅ鎵规椂锛屽皢鑷姩棰勫~浠ヤ笅鍐呭锛堢敤鎴蜂粛鍙慨鏀癸級</p>
+            <el-input
+              v-if="field.type === 'text' || field.type === 'textarea'"
+              v-model="field.defaultValue"
+              :type="field.type === 'textarea' ? 'textarea' : 'text'"
+              :rows="field.type === 'textarea' ? 2 : undefined"
+              :placeholder="defaultPlaceholder(field)"
+              clearable
+              @input="emitOut"
+            />
+            <el-input-number
+              v-else-if="field.type === 'number'"
+              v-model="field.defaultValue"
+              :min="field.min"
+              :precision="field.precision ?? 0"
+              controls-position="right"
+              placeholder="閫夊~"
+              style="width: 100%"
+              @change="emitOut"
+            />
+            <el-date-picker
+              v-else-if="field.type === 'date'"
+              v-model="field.defaultValue"
+              type="date"
+              placeholder="閫夊~"
+              format="YYYY-MM-DD"
+              value-format="YYYY-MM-DD"
+              style="width: 100%"
+              clearable
+              @change="emitOut"
+            />
+            <el-date-picker
+              v-else-if="field.type === 'datetimerange'"
+              v-model="field.defaultValue"
+              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%"
+              clearable
+              @change="emitOut"
+            />
+            <el-select
+              v-else-if="field.type === 'select'"
+              v-model="field.defaultValue"
+              placeholder="閫夊~"
+              style="width: 100%"
+              clearable
+              @change="emitOut"
+            >
+              <el-option
+                v-for="o in field.options.filter((x) => x.value !== '' && x.value != null)"
+                :key="String(o.value)"
+                :label="o.label || o.value"
+                :value="o.value"
+              />
+            </el-select>
+          </div>
+
+          <div v-if="field.type === 'select'" class="fce-section fce-section--options">
+            <div class="fce-options-head">
+              <span class="fce-section-title">涓嬫媺閫夐」</span>
+              <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
+                娣诲姞閫夐」
+              </el-button>
+            </div>
+            <div
+              v-for="(opt, oi) in field.options"
+              :key="oi"
+              class="fce-option-row"
+            >
+              <span class="fce-option-index">{{ oi + 1 }}</span>
+              <el-input v-model="opt.label" placeholder="鏄剧ず鏂囨湰" @input="emitOut" />
+              <el-input v-model="opt.value" placeholder="閫夐」鍊�" class="fce-option-value" @input="emitOut" />
+              <el-button
+                type="danger"
+                link
+                :icon="Delete"
+                :disabled="field.options.length <= 1"
+                @click="removeOption(field, oi)"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue";
+import { reactive, watch } from "vue";
+import {
+  FORM_CONFIG_PRESETS,
+  FORM_FIELD_TYPE_OPTIONS,
+  applyFormConfigPreset,
+  createEmptyFormConfigData,
+  createEmptyFormField,
+  formFieldTypeLabel,
+} from "../formConfigUtils.js";
+
+const props = defineProps({
+  modelValue: { type: Object, default: () => createEmptyFormConfigData() },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const inner = reactive(createEmptyFormConfigData());
+
+function typeLabel(type) {
+  return formFieldTypeLabel(type);
+}
+
+function defaultPlaceholder(field) {
+  const name = field.label || "璇ュ瓧娈�";
+  return `閫夊~锛岄�夋嫨妯℃澘鏃跺皢棰勫~${name}`;
+}
+
+function syncFromProps(v) {
+  const src = v || createEmptyFormConfigData();
+  inner.summaryPlaceholder = src.summaryPlaceholder || "";
+  inner.fields = (src.fields || []).map((f) => ({
+    ...createEmptyFormField(),
+    ...f,
+    _uid: f._uid || createEmptyFormField()._uid,
+    options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })),
+  }));
+}
+
+function emitOut() {
+  emit("update:modelValue", {
+    summaryPlaceholder: inner.summaryPlaceholder,
+    fields: inner.fields.map((f) => ({
+      _uid: f._uid,
+      key: f.key,
+      label: f.label,
+      type: f.type,
+      required: f.required,
+      rows: f.rows,
+      min: f.min,
+      precision: f.precision,
+      defaultValue: cloneDefaultValue(f),
+      options: (f.options || []).map((o) => ({ label: o.label, value: o.value })),
+    })),
+  });
+}
+
+function cloneDefaultValue(f) {
+  if (f.type === "datetimerange" && Array.isArray(f.defaultValue)) {
+    return [...f.defaultValue];
+  }
+  return f.defaultValue;
+}
+
+watch(
+  () => props.modelValue,
+  (v) => syncFromProps(v),
+  { deep: true, immediate: true }
+);
+
+function addField() {
+  inner.fields.push(createEmptyFormField());
+  emitOut();
+}
+
+function removeField(index) {
+  inner.fields.splice(index, 1);
+  emitOut();
+}
+
+function moveField(index, delta) {
+  const next = index + delta;
+  if (next < 0 || next >= inner.fields.length) return;
+  const t = inner.fields[index];
+  inner.fields[index] = inner.fields[next];
+  inner.fields[next] = t;
+  emitOut();
+}
+
+function resetDefaultValueForType(field) {
+  if (field.type === "number") field.defaultValue = undefined;
+  else if (field.type === "datetimerange") field.defaultValue = [];
+  else field.defaultValue = "";
+}
+
+function onTypeChange(field) {
+  if (field.type === "select" && (!field.options || !field.options.length)) {
+    field.options = [{ label: "", value: "" }];
+  }
+  resetDefaultValueForType(field);
+  emitOut();
+}
+
+function addOption(field) {
+  field.options.push({ label: "", value: "" });
+  emitOut();
+}
+
+function removeOption(field, oi) {
+  if (field.options.length <= 1) return;
+  field.options.splice(oi, 1);
+  emitOut();
+}
+
+function applyPreset(key) {
+  const data = applyFormConfigPreset(key);
+  syncFromProps(data);
+  emitOut();
+}
+</script>
+
+<style scoped>
+.fce {
+  width: 100%;
+}
+
+.fce-hint {
+  padding: 14px 16px;
+  margin-bottom: 14px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-blank) 100%);
+  border: 1px solid var(--el-color-primary-light-7);
+}
+
+.fce-hint-label {
+  display: block;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  margin-bottom: 8px;
+}
+
+.fce-panel {
+  padding: 16px;
+  border-radius: 12px;
+  background: var(--el-fill-color-lighter);
+  border: 1px solid var(--el-border-color-lighter);
+}
+
+.fce-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.fce-toolbar-left {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.fce-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.fce-toolbar-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.fce-empty {
+  padding: 24px 0;
+}
+
+.fce-list {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.fce-card {
+  position: relative;
+  padding: 16px 16px 12px;
+  border-radius: 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+  transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.fce-card:hover {
+  border-color: var(--el-color-primary-light-5);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
+}
+
+.fce-card--required {
+  border-left: 3px solid var(--el-color-danger-light-3);
+}
+
+.fce-card-badge {
+  position: absolute;
+  top: -10px;
+  left: 16px;
+  min-width: 22px;
+  height: 22px;
+  padding: 0 6px;
+  border-radius: 11px;
+  background: var(--el-color-primary);
+  color: #fff;
+  font-size: 12px;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.35);
+}
+
+.fce-card-head {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 14px;
+  padding-top: 4px;
+}
+
+.fce-card-title {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+
+.fce-card-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.fce-card-btns {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  flex-shrink: 0;
+}
+
+.fce-section {
+  margin-bottom: 12px;
+  padding-bottom: 12px;
+  border-bottom: 1px dashed var(--el-border-color-extra-light);
+}
+
+.fce-section:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  border-bottom: none;
+}
+
+.fce-section-title {
+  display: block;
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--el-text-color-secondary);
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  margin-bottom: 10px;
+}
+
+.fce-section-desc {
+  margin: -6px 0 10px;
+  font-size: 12px;
+  color: var(--el-text-color-placeholder);
+  line-height: 1.5;
+}
+
+.fce-section--default {
+  padding: 12px 14px;
+  border-radius: 8px;
+  background: var(--el-fill-color-lighter);
+  border-bottom: none;
+  margin-bottom: 0;
+}
+
+.fce-section--default .fce-section-title {
+  margin-bottom: 4px;
+  color: var(--el-color-primary);
+  text-transform: none;
+  letter-spacing: 0;
+  font-size: 13px;
+}
+
+.fce-section--options {
+  padding-top: 4px;
+  border-bottom: none;
+  margin-bottom: 0;
+}
+
+.fce-field-item {
+  margin-bottom: 0;
+}
+
+.fce-field-item :deep(.el-form-item__label) {
+  font-size: 13px;
+  color: var(--el-text-color-regular);
+}
+
+.fce-field-item--switch :deep(.el-form-item__content) {
+  line-height: 32px;
+}
+
+.fce-options-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+
+.fce-options-head .fce-section-title {
+  margin-bottom: 0;
+}
+
+.fce-option-row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 8px;
+  padding: 8px 10px;
+  border-radius: 8px;
+  background: var(--el-fill-color-lighter);
+}
+
+.fce-option-row:last-child {
+  margin-bottom: 0;
+}
+
+.fce-option-index {
+  flex-shrink: 0;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: var(--el-color-info-light-8);
+  color: var(--el-text-color-secondary);
+  font-size: 11px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.fce-option-value {
+  width: 140px;
+  flex-shrink: 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
index 45b32c0..f3f9540 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
@@ -127,6 +127,8 @@
   const normalized = normalizeFlowNodes(rows);
   return normalized.map((n) => ({
     _uid: newUid(),
+    id: n.id,
+    templateId: n.templateId,
     nodeOrder: n.nodeOrder,
     signMode: n.signMode,
     approverIds: n.approvers.map((a) => a.approverId),
@@ -137,6 +139,8 @@
 function publicShape(rows) {
   return normalizeFlowNodes(
     (rows || []).map((r) => ({
+      id: r.id,
+      templateId: r.templateId,
       nodeOrder: r.nodeOrder,
       signMode: r.signMode,
       approvers: r.approvers || [],
@@ -165,13 +169,21 @@
 
 function onApproversChange(ids, row) {
   const idList = Array.isArray(ids) ? ids : [];
+  const prevById = new Map((row.approvers || []).map((a) => [String(a.approverId), a]));
   row.approverIds = idList;
   row.approvers = idList.map((id) => {
+    const prev = prevById.get(String(id));
     const u = findUser(id);
-    return {
+    const item = {
       approverId: id,
-      approverName: u ? u.nickName || u.userName || "" : "",
+      approverName: u ? u.nickName || u.userName || "" : prev?.approverName || "",
     };
+    if (prev?.id != null) item.id = prev.id;
+    if (prev?.nodeId != null) item.nodeId = prev.nodeId;
+    else if (row.id != null) item.nodeId = row.id;
+    if (prev?.templateId != null) item.templateId = prev.templateId;
+    else if (row.templateId != null) item.templateId = row.templateId;
+    return item;
   });
   emitOut();
 }
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
new file mode 100644
index 0000000..0cb20ea
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
@@ -0,0 +1,278 @@
+/** 濉姤椤圭被鍨嬶紙涓庡鎵规彁浜ら〉 field.type 涓�鑷达級 */
+export const FORM_FIELD_TYPE_OPTIONS = [
+  { value: "text", label: "鍗曡鏂囨湰" },
+  { value: "textarea", label: "澶氳鏂囨湰" },
+  { value: "number", label: "鏁板瓧" },
+  { value: "date", label: "鏃ユ湡" },
+  { value: "datetimerange", label: "鏃ユ湡鏃堕棿鑼冨洿" },
+  { value: "select", label: "涓嬫媺閫夋嫨" },
+];
+
+/** 甯哥敤棰勮锛堝璐圭敤鎶ラ攢锛� */
+export const FORM_CONFIG_PRESETS = [
+  {
+    key: "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 },
+    ],
+  },
+  {
+    key: "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 },
+    ],
+  },
+  {
+    key: "leave",
+    label: "璇峰亣鐢宠",
+    summaryPlaceholder: "璇峰~鍐欒鍋囩被鍨嬩笌鏃堕棿",
+    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 },
+    ],
+  },
+];
+
+function newFieldUid() {
+  return `f_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+}
+
+export function createEmptyFormField() {
+  return {
+    _uid: newFieldUid(),
+    key: "",
+    label: "",
+    type: "text",
+    required: true,
+    rows: 3,
+    min: 0,
+    precision: 0,
+    defaultValue: "",
+    options: [{ label: "", value: "" }],
+  };
+}
+
+/** 瑙f瀽鍗曢」榛樿鍊硷紙渚涙彁浜ら〉 formPayload 鍒濆鍖栵級 */
+export function resolveFieldDefaultValue(field) {
+  const type = field?.type || "text";
+  const dv = field?.defaultValue;
+  if (dv === undefined || dv === null || dv === "") {
+    if (type === "number") return undefined;
+    if (type === "datetimerange") return [];
+    return "";
+  }
+  if (type === "number") {
+    const n = Number(dv);
+    return Number.isNaN(n) ? undefined : n;
+  }
+  if (type === "datetimerange") {
+    return Array.isArray(dv) ? [...dv] : [];
+  }
+  return dv;
+}
+
+function hasDefaultValue(field) {
+  const type = field?.type || "text";
+  const dv = field?.defaultValue;
+  if (dv === undefined || dv === null) return false;
+  if (type === "number") return dv !== "" && !Number.isNaN(Number(dv));
+  if (type === "datetimerange") return Array.isArray(dv) && dv.length === 2;
+  if (type === "select") return dv !== "";
+  return String(dv).trim() !== "";
+}
+
+/** 鏍规嵁瀛楁瀹氫箟鐢熸垚 formPayload 鍒濆鍊硷紙鍚粯璁ゅ�硷級 */
+export function buildFormPayloadFromFields(fields) {
+  const payload = {};
+  (fields || []).forEach((f) => {
+    const key = (f.key || "").trim();
+    if (!key) return;
+    payload[key] = resolveFieldDefaultValue(f);
+  });
+  return payload;
+}
+
+export function createEmptyFormConfigData() {
+  return {
+    summaryPlaceholder: "",
+    fields: [],
+  };
+}
+
+function parseFormConfigRaw(formConfig) {
+  if (!formConfig) return {};
+  if (typeof formConfig === "object") return formConfig;
+  try {
+    return JSON.parse(formConfig);
+  } catch {
+    return {};
+  }
+}
+
+function normalizeDefaultValueFromApi(f) {
+  const type = f.type || "text";
+  if (f.defaultValue === undefined || f.defaultValue === null) {
+    if (type === "number") return undefined;
+    if (type === "datetimerange") return [];
+    return "";
+  }
+  if (type === "datetimerange" && Array.isArray(f.defaultValue)) {
+    return [...f.defaultValue];
+  }
+  return f.defaultValue;
+}
+
+/** 鎺ュ彛 formConfig 鈫� 缂栬緫鍣ㄦ暟鎹� */
+export function parseFormConfigToData(formConfig) {
+  const raw = parseFormConfigRaw(formConfig);
+  const fields = (raw.fields || raw.formFields || []).map((f) => ({
+    _uid: newFieldUid(),
+    key: f.key || "",
+    label: f.label || "",
+    type: f.type || "text",
+    required: f.required !== false,
+    rows: f.rows ?? 3,
+    min: f.min ?? 0,
+    precision: f.precision ?? 0,
+    defaultValue: normalizeDefaultValueFromApi(f),
+    options: (f.options || []).length
+      ? f.options.map((o) => ({ label: o.label || "", value: o.value ?? "" }))
+      : [{ label: "", value: "" }],
+  }));
+  return {
+    summaryPlaceholder: raw.summaryPlaceholder || "",
+    fields,
+  };
+}
+
+/** 缂栬緫鍣ㄦ暟鎹� 鈫� 鎻愪氦鐢� JSON 瀛楃涓� */
+export function buildFormConfigJson(formConfigData) {
+  const data = formConfigData || createEmptyFormConfigData();
+  const fields = (data.fields || []).map((f) => {
+    const item = {
+      key: (f.key || "").trim(),
+      label: (f.label || "").trim(),
+      type: f.type || "text",
+      required: f.required !== false,
+    };
+    if (item.type === "textarea") item.rows = Number(f.rows) || 3;
+    if (item.type === "number") {
+      item.min = f.min ?? 0;
+      item.precision = f.precision ?? 0;
+    }
+    if (item.type === "select") {
+      item.options = (f.options || [])
+        .filter((o) => (o.label || "").trim() || o.value !== "" && o.value != null)
+        .map((o) => ({ label: (o.label || "").trim(), value: o.value }));
+    }
+    if (hasDefaultValue(f)) {
+      item.defaultValue =
+        f.type === "datetimerange" && Array.isArray(f.defaultValue)
+          ? f.defaultValue
+          : f.defaultValue;
+    }
+    return item;
+  });
+  const payload = {
+    summaryPlaceholder: (data.summaryPlaceholder || "").trim(),
+    fields,
+  };
+  return JSON.stringify(payload);
+}
+
+export function applyFormConfigPreset(presetKey) {
+  const preset = FORM_CONFIG_PRESETS.find((p) => p.key === presetKey);
+  if (!preset) return createEmptyFormConfigData();
+  return parseFormConfigToData({
+    summaryPlaceholder: preset.summaryPlaceholder,
+    fields: preset.fields,
+  });
+}
+
+export function validateFormConfigData(formConfigData) {
+  const fields = formConfigData?.fields || [];
+  if (!fields.length) {
+    return { ok: true };
+  }
+  const keys = new Set();
+  for (let i = 0; i < fields.length; i++) {
+    const f = fields[i];
+    const key = (f.key || "").trim();
+    const label = (f.label || "").trim();
+    if (!key) return { ok: false, message: `璇峰~鍐欑 ${i + 1} 涓~鎶ラ」鐨勫瓧娈垫爣璇哷 };
+    if (!label) return { ok: false, message: `璇峰~鍐欑 ${i + 1} 涓~鎶ラ」鐨勬樉绀哄悕绉癭 };
+    if (keys.has(key)) return { ok: false, message: `瀛楁鏍囪瘑銆�${key}銆嶉噸澶嶏紝璇蜂慨鏀筦 };
+    keys.add(key);
+    if (f.type === "select") {
+      const opts = (f.options || []).filter((o) => (o.label || "").trim() && o.value !== "" && o.value != null);
+      if (!opts.length) return { ok: false, message: `璇蜂负銆�${label}銆嶉厤缃嚦灏戜竴涓笅鎷夐�夐」` };
+    }
+  }
+  return { ok: true };
+}
+
+export function formFieldTypeLabel(type) {
+  return FORM_FIELD_TYPE_OPTIONS.find((x) => x.value === type)?.label || type || "鈥�";
+}
+
+export function formatDefaultValueDisplay(field) {
+  const dv = field?.defaultValue;
+  if (dv === undefined || dv === null || dv === "") return "鈥�";
+  if (field?.type === "datetimerange" && Array.isArray(dv)) {
+    return dv.length === 2 ? `${dv[0]} ~ ${dv[1]}` : "鈥�";
+  }
+  if (field?.type === "select") {
+    const opt = (field.options || []).find((o) => String(o.value) === String(dv));
+    return opt?.label || String(dv);
+  }
+  return String(dv);
+}
+
+/** 灏嗗悗绔ā鏉胯杞负鎻愪氦椤垫ā鏉跨粨鏋勶紙鍚� fields 榛樿鍊硷級 */
+export function buildSubmitTemplateFromRow(row) {
+  const cfg = parseFormConfigToData(row?.formConfig);
+  const fields = (cfg.fields || []).map(({ _uid, ...rest }) => ({
+    ...rest,
+    key: rest.key,
+    label: rest.label,
+    type: rest.type,
+    required: rest.required,
+    rows: rest.rows,
+    min: rest.min,
+    precision: rest.precision,
+    defaultValue: rest.defaultValue,
+    options: rest.options,
+  }));
+  return {
+    label: row?.templateName || "瀹℃壒",
+    approvalType: cfg.approvalType || "",
+    summaryPlaceholder: cfg.summaryPlaceholder || "",
+    approvalMode: cfg.approvalMode || "parallel",
+    fields,
+  };
+}
+
+export function formConfigFieldsSummary(formConfigData) {
+  const fields = formConfigData?.fields || [];
+  if (!fields.length) return "鈥�";
+  return fields.map((f) => f.label || f.key || "鏈懡鍚�").join("銆�");
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
index a79d546..9d7324c 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -1,4 +1,4 @@
-<!--OA妯″潡锛氬鎵规ā鏉匡紙绯荤粺甯哥敤 + 鑷畾涔夊鑺傜偣娴佺▼锛�-->
+<!--OA妯″潡锛氬鎵规ā鏉�-->
 <template>
   <div class="app-container approve-template-page">
     <el-tabs v-model="activeTab" class="template-tabs">
@@ -9,8 +9,9 @@
             浠ヤ笅涓� OA 妯″潡鍐呯疆鐨勫父鐢ㄥ鎵圭被鍨嬶紝濉姤瀛楁涓庨粯璁ゅ鎵规柟寮忕敱绯荤粺缁存姢锛涙彁浜ゅ鎵规椂鍙洿鎺ラ�夌敤銆�
           </template>
         </el-alert>
-        <div class="builtin-grid">
-          <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card">
+        <div v-loading="builtinLoading" class="builtin-grid">
+          <template v-if="builtinTemplates.length">
+            <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card">
             <span class="builtin-label">{{ item.label }}</span>
             <p class="builtin-summary">{{ item.summary }}</p>
             <div class="builtin-meta">
@@ -20,7 +21,9 @@
               </el-tag>
               <el-tag size="small" type="info" effect="plain">鍙</el-tag>
             </div>
-          </div>
+            </div>
+          </template>
+          <el-empty v-else-if="!builtinLoading" description="鏆傛棤绯荤粺甯哥敤瀹℃壒妯℃澘" :image-size="80" />
         </div>
       </el-tab-pane>
 
@@ -66,7 +69,7 @@
     <el-dialog
       v-model="formDialog.visible"
       :title="formDialog.title"
-      width="960px"
+      width="1020px"
       append-to-body
       destroy-on-close
       class="template-form-dialog"
@@ -74,12 +77,24 @@
     >
       <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
         <el-row :gutter="20">
-          <el-col :span="12">
+          <el-col :span="8">
             <el-form-item label="妯℃澘鍚嶇О" prop="templateName">
               <el-input v-model="form.templateName" placeholder="濡傦細椤圭洰绔嬮」瀹℃壒" maxlength="50" show-word-limit />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="8">
+            <el-form-item label="妯℃澘绫诲瀷" prop="templateType">
+              <el-select v-model="form.templateType" placeholder="璇烽�夋嫨" style="width: 100%">
+                <el-option
+                  v-for="opt in TEMPLATE_TYPE_OPTIONS"
+                  :key="opt.value"
+                  :label="opt.label"
+                  :value="opt.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
             <el-form-item label="鍚敤鐘舵��">
               <el-switch v-model="form.enabled" active-text="鍚敤" inactive-text="鍋滅敤" />
             </el-form-item>
@@ -94,6 +109,10 @@
             maxlength="200"
             show-word-limit
           />
+        </el-form-item>
+        <el-form-item label="濉姤閰嶇疆">
+          <FormConfigEditor v-model="form.formConfigData" />
+          <p class="flow-tip">閰嶇疆鎻愪氦瀹℃壒鏃堕渶濉啓鐨勮〃鍗曢」锛屼繚瀛樺悗鍐欏叆 formConfig锛圝SON锛夈��</p>
         </el-form-item>
         <el-form-item label="瀹℃壒娴佺▼" required>
           <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" />
@@ -110,17 +129,44 @@
 
     <!-- 璇︽儏 -->
     <el-dialog v-model="detailDialog.visible" title="妯℃澘璇︽儏" width="880px" append-to-body destroy-on-close>
+      <div v-loading="detailLoading" class="detail-dialog-body">
       <el-descriptions :column="2" border>
         <el-descriptions-item label="妯℃澘鍚嶇О">{{ detailRow.templateName }}</el-descriptions-item>
+        <el-descriptions-item label="妯℃澘绫诲瀷">{{ templateTypeLabel(detailRow.templateType) }}</el-descriptions-item>
         <el-descriptions-item label="鐘舵��">
           <el-tag :type="detailRow.enabled !== false ? 'success' : 'info'" size="small">
             {{ detailRow.enabled !== false ? "鍚敤" : "鍋滅敤" }}
           </el-tag>
         </el-descriptions-item>
         <el-descriptions-item label="璇存槑" :span="2">{{ detailRow.description || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鍒涘缓鏃堕棿">{{ detailRow.createTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鏇存柊鏃堕棿">{{ detailRow.updateTime || "鈥�" }}</el-descriptions-item>
+        <el-descriptions-item label="濉姤鎻愮ず" :span="2">
+          {{ detailFormConfig.summaryPlaceholder || "鈥�" }}
+        </el-descriptions-item>
+        <el-descriptions-item label="鍒涘缓浜�">{{ detailRow.createdUserName || "鈥�" }}</el-descriptions-item>
+        <el-descriptions-item label="鍒涘缓鏃堕棿">{{ formatDisplayTime(detailRow.createdTime) }}</el-descriptions-item>
+        <el-descriptions-item label="鏇存柊鏃堕棿">{{ formatDisplayTime(detailRow.updatedTime) }}</el-descriptions-item>
       </el-descriptions>
+      <el-divider content-position="left">濉姤椤癸紙{{ detailFormConfig.fields?.length || 0 }} 椤癸級</el-divider>
+      <el-table
+        v-if="detailFormConfig.fields?.length"
+        :data="detailFormConfig.fields"
+        border
+        size="small"
+        class="mb16"
+      >
+        <el-table-column prop="label" label="鏄剧ず鍚嶇О" min-width="120" />
+        <el-table-column prop="key" label="瀛楁鏍囪瘑" min-width="100" />
+        <el-table-column label="绫诲瀷" width="100">
+          <template #default="{ row }">{{ formFieldTypeLabel(row.type) }}</template>
+        </el-table-column>
+        <el-table-column label="蹇呭~" width="70" align="center">
+          <template #default="{ row }">{{ row.required !== false ? "鏄�" : "鍚�" }}</template>
+        </el-table-column>
+        <el-table-column label="榛樿鍊�" min-width="120" show-overflow-tooltip>
+          <template #default="{ row }">{{ formatDefaultValueDisplay(row) }}</template>
+        </el-table-column>
+      </el-table>
+      <el-empty v-else description="鏈厤缃~鎶ラ」" :image-size="48" class="mb16" />
       <el-divider content-position="left">瀹℃壒娴佺▼锛坽{ detailRow.flowNodes?.length || 0 }} 涓妭鐐癸級</el-divider>
       <div v-if="detailRow.flowNodes?.length" class="detail-flow">
         <div v-for="(node, index) in detailRow.flowNodes" :key="index" class="detail-node">
@@ -145,6 +191,7 @@
         </div>
       </div>
       <el-empty v-else description="鏆傛棤娴佺▼鑺傜偣" :image-size="60" />
+      </div>
       <template #footer>
         <el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
         <el-button type="primary" @click="editFromDetail">缂� 杈�</el-button>
@@ -156,16 +203,23 @@
 <script setup>
 import { ArrowRight, Document, Plus, RefreshRight } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
-import { onMounted, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { userListNoPageByTenantId } from "@/api/system/user.js";
+import FormConfigEditor from "./components/FormConfigEditor.vue";
 import TemplateFlowEditor from "./components/TemplateFlowEditor.vue";
+import { formatDisplayTime } from "./approveTemplateConstants.js";
+import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js";
 import { useApproveTemplate } from "./useApproveTemplate.js";
 
 const at = useApproveTemplate();
 const {
   Search,
+  TEMPLATE_TYPE_OPTIONS,
+  templateTypeLabel,
   activeTab,
   builtinTemplates,
+  builtinLoading,
+  loadBuiltinTemplates,
   nodeSignModeLabel,
   searchForm,
   tableLoading,
@@ -178,6 +232,8 @@
   formRules,
   detailDialog,
   detailRow,
+  detailLoading,
+  fetchTemplateList,
   handleQuery,
   resetSearch,
   pagination,
@@ -187,6 +243,10 @@
 } = at;
 
 const flowUserOptions = ref([]);
+
+const detailFormConfig = computed(() =>
+  parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig)
+);
 
 function unwrapArray(payload) {
   if (Array.isArray(payload)) return payload;
@@ -227,7 +287,8 @@
 
 onMounted(() => {
   loadUsers();
-  handleQuery();
+  loadBuiltinTemplates();
+  fetchTemplateList();
 });
 </script>
 
@@ -237,6 +298,9 @@
 }
 .mb16 {
   margin-bottom: 16px;
+}
+.mb16.el-empty {
+  padding: 8px 0;
 }
 .ml10 {
   margin-left: 10px;
@@ -355,6 +419,9 @@
   transform: translateY(-50%);
   color: var(--el-text-color-placeholder);
 }
+.detail-dialog-body {
+  min-height: 120px;
+}
 .text-muted {
   font-size: 12px;
   color: var(--el-text-color-placeholder);
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
index c56d055..8489e13 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -1,24 +1,49 @@
-import { Search } from "@element-plus/icons-vue";
-import dayjs from "dayjs";
-import { ElMessageBox } from "element-plus";
-import { computed, reactive, ref, watch } from "vue";
 import {
+  addApprovalTemplate,
+  deleteApprovalTemplate,
+  getApprovalTemplateDetail,
+  listApprovalTemplate,
+  listApprovalTemplatePage,
+  TEMPLATE_TYPE_BUILTIN,
+  TEMPLATE_TYPE_CUSTOM,
+  TEMPLATE_TYPE_OPTIONS,
+  updateApprovalTemplate,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { Search } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { reactive, ref } from "vue";
+import {
+  buildApprovalTemplateListParams,
   createEmptyTemplateForm,
-  createInitialMockTemplates,
   flowNodesSummary,
-  getBuiltinTemplates,
-  loadStoredTemplates,
+  mapBuiltinCardFromApi,
+  mapTemplateFromApi,
+  mapTemplateToApi,
   nodeSignModeLabel,
-  saveStoredTemplates,
+  templateTypeLabel,
+  unwrapTemplateList,
+  formatDisplayTime,
+  unwrapTemplateDetail,
   validateTemplateForm,
 } from "./approveTemplateConstants.js";
+import { parseFormConfigToData } from "./formConfigUtils.js";
+
+const LEGACY_STORAGE_KEY = "oa_approve_template_custom_v1";
+
+function clearLegacyStorage() {
+  try {
+    localStorage.removeItem(LEGACY_STORAGE_KEY);
+  } catch {
+    /* ignore */
+  }
+}
 
 export function useApproveTemplate() {
-  const stored = loadStoredTemplates();
-  const allTemplates = ref(stored?.length ? stored : createInitialMockTemplates());
+  clearLegacyStorage();
 
   const activeTab = ref("custom");
-  const builtinTemplates = getBuiltinTemplates();
+  const builtinTemplates = ref([]);
+  const builtinLoading = ref(false);
 
   const searchForm = reactive({
     keyword: "",
@@ -27,6 +52,7 @@
 
   const tableLoading = ref(false);
   const page = reactive({ current: 1, size: 10, total: 0 });
+  const tableData = ref([]);
 
   const formDialog = reactive({ visible: false, title: "", mode: "add" });
   const form = reactive(createEmptyTemplateForm());
@@ -34,44 +60,22 @@
 
   const detailDialog = reactive({ visible: false });
   const detailRow = ref({});
-
-  const filteredList = computed(() => {
-    let list = [...allTemplates.value];
-    const kw = (searchForm.keyword || "").trim().toLowerCase();
-    if (kw) {
-      list = list.filter((r) => {
-        const name = (r.templateName || "").toLowerCase();
-        const desc = (r.description || "").toLowerCase();
-        return name.includes(kw) || desc.includes(kw);
-      });
-    }
-    if (searchForm.enabledOnly) {
-      list = list.filter((r) => r.enabled !== false);
-    }
-    return list.sort((a, b) => (String(a.updateTime) < String(b.updateTime) ? 1 : -1));
-  });
-
-  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 tableData = computed(() => {
-    const start = (page.current - 1) * page.size;
-    return filteredList.value.slice(start, start + page.size);
-  });
+  const detailLoading = ref(false);
 
   const formRules = {
     templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
+    templateType: [{ required: true, message: "璇烽�夋嫨妯℃澘绫诲瀷", trigger: "change" }],
   };
 
   const tableColumn = ref([
     { label: "妯℃澘鍚嶇О", prop: "templateName", minWidth: 140 },
+    {
+      label: "妯℃澘绫诲瀷",
+      prop: "templateType",
+      width: 100,
+      align: "center",
+      formatData: (v) => templateTypeLabel(v),
+    },
     { label: "璇存槑", prop: "description", minWidth: 160, showOverflowTooltip: true },
     {
       label: "鑺傜偣鏁�",
@@ -96,31 +100,72 @@
       formatData: (v) => (v !== false ? "鍚敤" : "鍋滅敤"),
       formatType: (v) => (v !== false ? "success" : "info"),
     },
-    { label: "鏇存柊鏃堕棿", prop: "updateTime", width: 170 },
+    {
+      label: "鍒涘缓鏃堕棿",
+      prop: "createdTime",
+      width: 170,
+      showOverflowTooltip: true,
+      formatData: (v) => formatDisplayTime(v),
+    },
+    {
+      label: "鏇存柊鏃堕棿",
+      prop: "updatedTime",
+      width: 170,
+      showOverflowTooltip: true,
+      formatData: (v) => formatDisplayTime(v),
+    },
     {
       dataType: "action",
       label: "鎿嶄綔",
       align: "center",
       fixed: "right",
-      width: 200,
+      width: 220,
       operation: [
         { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
         { name: "缂栬緫", type: "text", clickFun: (row) => openFormDialog("edit", row) },
-        { name: "鍒犻櫎", type: "text", clickFun: (row) => removeTemplate(row) },
+        {
+          name: "鍒犻櫎",
+          type: "danger",
+          link: true,
+          clickFun: (row) => removeTemplate(row),
+        },
       ],
     },
   ]);
 
-  function persist() {
-    saveStoredTemplates(allTemplates.value);
+  async function loadBuiltinTemplates() {
+    builtinLoading.value = true;
+    try {
+      const res = await listApprovalTemplate(TEMPLATE_TYPE_BUILTIN);
+      builtinTemplates.value = unwrapTemplateList(res).map(mapBuiltinCardFromApi);
+    } catch {
+      builtinTemplates.value = [];
+      ElMessage.warning("绯荤粺甯哥敤瀹℃壒鍔犺浇澶辫触");
+    } finally {
+      builtinLoading.value = false;
+    }
+  }
+
+  async function fetchTemplateList() {
+    tableLoading.value = true;
+    try {
+      const res = await listApprovalTemplatePage(
+        buildApprovalTemplateListParams({ page, searchForm })
+      );
+      const data = res?.data || {};
+      tableData.value = (data.records || []).map(mapTemplateFromApi);
+      page.total = Number(data.total || 0);
+    } catch {
+      tableData.value = [];
+      page.total = 0;
+    } finally {
+      tableLoading.value = false;
+    }
   }
 
   function handleQuery() {
-    tableLoading.value = true;
     page.current = 1;
-    setTimeout(() => {
-      tableLoading.value = false;
-    }, 150);
+    fetchTemplateList();
   }
 
   function resetSearch() {
@@ -132,6 +177,7 @@
   function pagination({ page: p, limit }) {
     page.current = p;
     page.size = limit;
+    fetchTemplateList();
   }
 
   function resetForm(row) {
@@ -145,6 +191,11 @@
       id: row.id,
       templateName: row.templateName || "",
       description: row.description || "",
+      templateType: row.templateType ?? TEMPLATE_TYPE_CUSTOM,
+      formConfig: row.formConfig || "",
+      formConfigData: JSON.parse(
+        JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
+      ),
       enabled: row.enabled !== false,
       flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
     });
@@ -152,19 +203,27 @@
 
   function openFormDialog(mode, row) {
     formDialog.mode = mode;
-    formDialog.title = mode === "add" ? "鏂板缓鑷畾涔夊鎵规ā鏉�" : "缂栬緫鑷畾涔夊鎵规ā鏉�";
+    formDialog.title = mode === "add" ? "鏂板缓瀹℃壒妯℃澘" : "缂栬緫瀹℃壒妯℃澘";
     resetForm(mode === "edit" ? row : null);
     formDialog.visible = true;
   }
 
-  function openDetail(row) {
-    detailRow.value = { ...row };
+  async function openDetail(row) {
+    if (row?.id == null || row.id === "") {
+      ElMessage.warning("鏃犳硶鏌ョ湅璇︽儏锛氱己灏戞ā鏉� ID");
+      return;
+    }
     detailDialog.visible = true;
-  }
-
-  function isNameDuplicate(name, excludeId) {
-    const n = (name || "").trim();
-    return allTemplates.value.some((t) => t.templateName?.trim() === n && t.id !== excludeId);
+    detailLoading.value = true;
+    detailRow.value = {};
+    try {
+      const res = await getApprovalTemplateDetail(row.id);
+      detailRow.value = mapTemplateFromApi(unwrapTemplateDetail(res));
+    } catch {
+      detailDialog.visible = false;
+    } finally {
+      detailLoading.value = false;
+    }
   }
 
   async function submitForm() {
@@ -178,64 +237,70 @@
     if (!validated.ok) {
       return { message: validated.message };
     }
-    if (isNameDuplicate(validated.name, form.id)) {
-      return { message: "妯℃澘鍚嶇О宸插瓨鍦紝璇锋洿鎹㈠悕绉�" };
+    if (formDialog.mode === "edit" && !form.id) {
+      return { message: "缂哄皯妯℃澘 ID锛屾棤娉曚繚瀛樹慨鏀�" };
     }
-    const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
-    if (formDialog.mode === "add") {
-      allTemplates.value.unshift({
-        id: `tpl_${Date.now()}`,
-        templateName: validated.name,
-        description: (form.description || "").trim(),
-        enabled: form.enabled !== false,
-        createTime: now,
-        updateTime: now,
-        flowNodes: validated.nodes,
-      });
-    } else {
-      const hit = allTemplates.value.find((t) => t.id === form.id);
-      if (!hit) return { message: "妯℃澘涓嶅瓨鍦ㄦ垨宸插垹闄�" };
-      hit.templateName = validated.name;
-      hit.description = (form.description || "").trim();
-      hit.enabled = form.enabled !== false;
-      hit.flowNodes = validated.nodes;
-      hit.updateTime = now;
+    const dto = mapTemplateToApi(form);
+    try {
+      if (formDialog.mode === "add") {
+        await addApprovalTemplate(dto);
+      } else {
+        await updateApprovalTemplate(dto);
+      }
+    } catch {
+      return false;
     }
-    persist();
     formDialog.visible = false;
     page.current = 1;
+    await fetchTemplateList();
+    if (dto.templateType === TEMPLATE_TYPE_BUILTIN) {
+      await loadBuiltinTemplates();
+    }
     return { ok: true };
   }
 
   async function removeTemplate(row) {
+    if (row?.id == null || row.id === "") {
+      ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戞ā鏉� ID");
+      return;
+    }
+    const name = row.templateName || "鏈懡鍚嶆ā鏉�";
     try {
-      await ElMessageBox.confirm(`纭畾鍒犻櫎妯℃澘銆�${row.templateName}銆嶅悧锛焋, "鎻愮ず", {
-        type: "warning",
-        confirmButtonText: "鍒犻櫎",
-        cancelButtonText: "鍙栨秷",
-      });
+      await ElMessageBox.confirm(
+        `纭畾瑕佸垹闄ゅ鎵规ā鏉裤��${name}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+        "鍒犻櫎纭",
+        {
+          type: "warning",
+          confirmButtonText: "纭畾鍒犻櫎",
+          cancelButtonText: "鍙栨秷",
+          distinguishCancelAndClose: true,
+          autofocus: false,
+        }
+      );
     } catch {
       return;
     }
-    const idx = allTemplates.value.findIndex((t) => t.id === row.id);
-    if (idx >= 0) {
-      allTemplates.value.splice(idx, 1);
-      persist();
+    try {
+      await deleteApprovalTemplate([row.id]);
+      ElMessage.success("鍒犻櫎鎴愬姛");
+      await fetchTemplateList();
+      if (row.templateType === TEMPLATE_TYPE_BUILTIN) {
+        await loadBuiltinTemplates();
+      }
+    } catch {
+      /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
     }
-  }
-
-  function toggleEnabled(row) {
-    const hit = allTemplates.value.find((t) => t.id === row.id);
-    if (!hit) return;
-    hit.enabled = !hit.enabled;
-    hit.updateTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
-    persist();
   }
 
   return {
     Search,
+    TEMPLATE_TYPE_OPTIONS,
+    templateTypeLabel,
     activeTab,
     builtinTemplates,
+    builtinLoading,
+    loadBuiltinTemplates,
+    fetchTemplateList,
     nodeSignModeLabel,
     flowNodesSummary,
     searchForm,
@@ -249,12 +314,12 @@
     formRules,
     detailDialog,
     detailRow,
+    detailLoading,
     handleQuery,
     resetSearch,
     pagination,
     openFormDialog,
     openDetail,
     submitForm,
-    toggleEnabled,
   };
 }

--
Gitblit v1.9.3