From 352f7bbb74f1b6c57b3d3e576849d0565932fbd4 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期三, 20 五月 2026 16:50:36 +0800
Subject: [PATCH] 审批模板集成页面

---
 src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue                                     |   36 
 src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js          |  100 +
 src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js               |   98 +
 src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue                                    |  846 +---------
 src/views/officeProcessAutomation/HrManage/work-handover/index.vue                                         |  570 +-----
 src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js                |  396 ++++
 src/views/officeProcessAutomation/HrManage/regular-apply/index.vue                                         |  494 -----
 src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js                             |   23 
 src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceRowMappers.js               |   10 
 src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js                   |   34 
 src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue                                       |  748 +-------
 src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue                                        |  610 +-----
 src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue                                     |  538 +++--
 src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js                       |  151 +
 src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue |  123 +
 src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue |  112 +
 src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue  |   17 
 17 files changed, 1,857 insertions(+), 3,049 deletions(-)

diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index acbed98..3f6bf88 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -8,7 +8,10 @@
   nodeSignModeLabel,
 } from "../approve-template/approveTemplateConstants.js";
 import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js";
-import { isDynamicOptionSource, selectOptionSourceLabel } from "../approve-template/selectOptionSource.js";
+import {
+  isDynamicOptionSource,
+  resolveSelectDisplayLabel,
+} from "../approve-template/selectOptionSource.js";
 
 /** 瀹℃壒绫诲瀷锛堜笌鍚庣瀛楁 approvalType 瀵归綈锛屽悗鏈熷彲鍚屾锛� */
 export const APPROVAL_TYPE_OPTIONS = [
@@ -25,15 +28,47 @@
   { value: "procurement", label: "閲囪喘瀹℃壒", cellBg: "#f4f4f5", cellColor: "#909399" },
   { value: "quotation", label: "鎶ヤ环瀹℃壒", cellBg: "#f4ecfc", cellColor: "#9b59b6" },
   { value: "shipment", label: "鍙戣揣瀹℃壒", cellBg: "#e8faf6", cellColor: "#1abc9c" },
+  { value: "enterprise_news", label: "浼佷笟鏂伴椈", cellBg: "#ecf5ff", cellColor: "#409eff" },
 ];
 
-/** 瀹℃壒鐘舵�� approvalStatus */
-export const APPROVAL_STATUS_OPTIONS = [
-  { value: "pending", label: "瀹℃牳涓�" },
-  { value: "approved", label: "宸查�氳繃" },
-  { value: "rejected", label: "宸查┏鍥�" },
-  { value: "cancelled", label: "宸叉挙閿�" },
+/** 鍒楄〃鏌ヨ锛氬鎵圭姸鎬侊紙涓庡悗绔� status 鏋氫妇涓�鑷达級 */
+export const APPROVAL_STATUS_SEARCH_OPTIONS = [
+  { value: "PENDING", label: "寰呭鎵�" },
+  { value: "APPROVED", label: "宸查�氳繃" },
+  { value: "REJECTED", label: "宸查┏鍥�" },
 ];
+
+/**
+ * 瀹℃壒鐘舵�佸睍绀猴紙涓庡悗绔� status 鏋氫妇涓�鑷达級
+ * PENDING 鈫� 寰呭鎵�/杩涜涓�  APPROVED 鈫� 宸查�氳繃/宸插畬鎴�  REJECTED 鈫� 宸查┏鍥�
+ */
+export const APPROVAL_STATUS_OPTIONS = [
+  { value: "pending", api: "PENDING", label: "寰呭鎵�" },
+  { value: "approved", api: "APPROVED", label: "宸查�氳繃" },
+  { value: "rejected", api: "REJECTED", label: "宸查┏鍥�" },
+  { value: "cancelled", api: "CANCELLED", label: "宸叉挙閿�" },
+];
+
+/** 鍚庣 status / 椤甸潰 approvalStatus 鈫� 缁熶竴椤甸潰 key锛坧ending | approved | rejected | cancelled锛� */
+export function normalizeApprovalStatusKey(v) {
+  const s = String(v ?? "").trim();
+  if (!s) return "pending";
+  const upper = s.toUpperCase();
+  if (upper === "APPROVED" || upper === "APPROVE" || upper === "PASS") return "approved";
+  if (upper === "REJECTED" || upper === "REJECT" || upper === "REFUSE") return "rejected";
+  if (upper === "CANCELLED" || upper === "CANCEL") return "cancelled";
+  if (
+    upper === "PENDING" ||
+    upper === "IN_PROGRESS" ||
+    upper === "PROCESSING" ||
+    upper === "RUNNING"
+  ) {
+    return "pending";
+  }
+  const lower = s.toLowerCase();
+  if (["pending", "approved", "rejected", "cancelled"].includes(lower)) return lower;
+  return "pending";
+}
 
 /** 鎻愪氦寮圭獥锛氭ā鏉垮崱鐗囷紙鏉ヨ嚜鍚庣鍒楄〃锛� */
 export function mapSubmitTemplateCard(row) {
@@ -75,20 +110,11 @@
 }
 
 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 || "鈥�";
+  return approvalStatusLabel(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";
+  return approvalStatusTagType(status);
 }
 
 /** 鍚庣 tasks 鈫� 椤甸潰 flowNodes锛堟寜 levelNo 鍒嗙粍锛屼緵娴佺▼缂栬緫/灞曠ず锛� */
@@ -164,11 +190,16 @@
   return "text";
 }
 
-/** 鍗曞瓧娈靛睍绀哄�硷紙璇︽儏鍙锛� */
-export function formatFieldDisplayValue(field, val) {
+/**
+ * 鍗曞瓧娈靛睍绀哄�硷紙璇︽儏鍙銆佸垪琛ㄤ富琛級
+ * @param {object} [caches] 浜哄憳/閮ㄩ棬涓嬫媺缂撳瓨锛岀敤浜庤В鏋愩�屼汉鍛樺垪琛ㄣ�嶇被瀛楁涓哄鍚�
+ */
+export function formatFieldDisplayValue(field, val, caches) {
   if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "鈥�";
   if (field?.type === "select" && isDynamicOptionSource(field.optionSource)) {
-    return `${selectOptionSourceLabel(field.optionSource)}锛�${String(val)}`;
+    const label = resolveSelectDisplayLabel(field, val, caches || {});
+    if (label && label !== "鈥�") return label;
+    return String(val);
   }
   if (field?.type === "select" && field.options?.length) {
     const hit = field.options.find((o) => String(o.value) === String(val));
@@ -323,20 +354,14 @@
 
 /** 鍚庣 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";
+  return normalizeApprovalStatusKey(status);
 }
 
 /** 椤甸潰 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";
+  const key = normalizeApprovalStatusKey(approvalStatus);
+  const hit = APPROVAL_STATUS_OPTIONS.find((x) => x.value === key);
+  return hit?.api || "PENDING";
 }
 
 export function unwrapInstancePage(res) {
@@ -418,19 +443,26 @@
   };
 }
 
-export function buildApprovalInstanceListParams({ page, searchForm }) {
+export function buildApprovalInstanceListParams({ page, searchForm, businessType, extraParams }) {
   const params = {
     current: page.current,
     size: page.size,
+    ...(extraParams && typeof extraParams === "object" ? extraParams : {}),
   };
-  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;
+  const bizType = businessType ?? searchForm?.businessType;
+  if (bizType != null && bizType !== "") {
+    params.businessType = bizType;
   }
-  if (Object.keys(dto).length) params.approvalInstanceDto = dto;
+  if (searchForm?.status) {
+    params.status = searchForm.status;
+  }
+  const range = searchForm?.createTimeRange;
+  if (Array.isArray(range) && range[0]) {
+    params.createTime = range[0];
+  }
+  if (Array.isArray(range) && range[1]) {
+    params.createTimeEnd = range[1];
+  }
   return params;
 }
 
@@ -449,14 +481,45 @@
 }
 
 export function approvalStatusLabel(v) {
-  return APPROVAL_STATUS_OPTIONS.find((x) => x.value === v)?.label || "鈥�";
+  const key = normalizeApprovalStatusKey(v);
+  return APPROVAL_STATUS_OPTIONS.find((x) => x.value === key)?.label || "鈥�";
+}
+
+/** 涓氬姟鐢宠椤电姸鎬佹枃妗堬細PENDING鈫掕繘琛屼腑 APPROVED鈫掑凡瀹屾垚 REJECTED鈫掑凡椹冲洖 */
+export function businessApprovalStatusLabel(v) {
+  const key = normalizeApprovalStatusKey(v);
+  if (key === "pending") return "杩涜涓�";
+  if (key === "approved") return "宸插畬鎴�";
+  if (key === "rejected") return "宸查┏鍥�";
+  if (key === "cancelled") return "宸叉挙閿�";
+  return "鈥�";
+}
+
+/**
+ * 涓氬姟鐢宠椤垫槸鍚﹀厑璁镐慨鏀癸紙浜斾釜鐢宠椤碉級
+ * 杩涜涓�(PENDING)銆佸凡瀹屾垚(APPROVED) 涓嶅彲淇敼锛涘凡椹冲洖銆佸凡鎾ら攢绛夊彲淇敼
+ */
+export function canEditBusinessInstanceRow(row) {
+  const key = normalizeApprovalStatusKey(
+    row?.approvalStatus ?? row?.statusRaw ?? row?.status
+  );
+  return key !== "pending" && key !== "approved";
+}
+
+export function businessApprovalStatusTagType(v) {
+  const key = normalizeApprovalStatusKey(v);
+  if (key === "approved") return "success";
+  if (key === "rejected") return "danger";
+  if (key === "cancelled") return "info";
+  return "warning";
 }
 
 export function approvalStatusTagType(v) {
-  if (v === "approved") return "success";
-  if (v === "rejected") return "danger";
-  if (v === "cancelled") return "info";
-  return "primary";
+  const key = normalizeApprovalStatusKey(v);
+  if (key === "approved") return "success";
+  if (key === "rejected") return "danger";
+  if (key === "cancelled") return "info";
+  return "warning";
 }
 
 export function unreadLabel(v) {
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index b87a964..eba9586 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -3,30 +3,35 @@
   <div class="app-container">
     <div class="search_form mb20">
       <div class="search_fields">
-        <span class="search_title">瀹℃壒绫诲瀷锛�</span>
+        <span class="search_title">妯℃澘绫诲瀷锛�</span>
         <el-select
-          v-model="searchForm.approvalType"
-          placeholder="璇烽�夋嫨瀹℃壒绫诲瀷"
+          v-model="searchForm.businessType"
+          placeholder="璇烽�夋嫨妯℃澘绫诲瀷"
           clearable
           filterable
           style="width: 200px"
         >
           <el-option
-            v-for="opt in APPROVAL_TYPE_OPTIONS"
+            v-for="opt in searchBusinessTypeOptions"
+            :key="`search-biz-type-${opt.value}`"
+            :label="opt.label"
+            :value="opt.value"
+          />
+        </el-select>
+        <span class="search_title" style="margin-left: 12px">瀹℃壒鐘舵�侊細</span>
+        <el-select
+          v-model="searchForm.status"
+          placeholder="璇烽�夋嫨瀹℃壒鐘舵��"
+          clearable
+          style="width: 140px"
+        >
+          <el-option
+            v-for="opt in APPROVAL_STATUS_SEARCH_OPTIONS"
             :key="opt.value"
             :label="opt.label"
             :value="opt.value"
           />
         </el-select>
-        <span class="search_title" style="margin-left: 12px">鐢宠浜哄悕绉帮細</span>
-        <el-input
-          v-model="searchForm.applicantKeyword"
-          style="width: 200px"
-          placeholder="璇疯緭鍏ョ敵璇蜂汉鍚嶇О"
-          clearable
-          :prefix-icon="Search"
-          @keyup.enter="handleQuery"
-        />
         <span class="search_title" style="margin-left: 12px">鍒涘缓鏃堕棿锛�</span>
         <el-date-picker
           v-model="searchForm.createTimeRange"
@@ -285,7 +290,9 @@
 const al = useApproveList();
 const {
   Search,
-  APPROVAL_TYPE_OPTIONS,
+  APPROVAL_STATUS_SEARCH_OPTIONS,
+  searchBusinessTypeOptions,
+  loadSearchBusinessTypeOptions,
   submitBusinessTypeOptions,
   submitTemplateCards,
   selectedBusinessTypeLabel,
@@ -364,6 +371,7 @@
 
 onMounted(() => {
   loadFlowUsers();
+  loadSearchBusinessTypeOptions();
   handleQuery();
 });
 </script>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index c17e88a..3523ef4 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -26,6 +26,7 @@
   validateTemplateBinding,
 } from "../approve-shared/approvalTemplateBindingUtils.js";
 import {
+  APPROVAL_STATUS_SEARCH_OPTIONS,
   APPROVAL_TYPE_OPTIONS,
   approvalStatusLabel,
   approvalStatusTagType,
@@ -45,6 +46,7 @@
   const userStore = useUserStore();
 
   const tableData = ref([]);
+  const searchBusinessTypeOptions = ref([]);
   const submitBusinessTypeOptions = ref([]);
   const allSubmitTemplates = ref([]);
   const selectedBusinessType = ref("");
@@ -58,8 +60,8 @@
   });
 
   const searchForm = reactive({
-    approvalType: "",
-    applicantKeyword: "",
+    businessType: "",
+    status: "",
     createTimeRange: [],
   });
 
@@ -118,7 +120,7 @@
   const tableColumn = ref([
     { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
     { label: "鐢宠浜哄悕绉�", prop: "applicantName", minWidth: 100 },
-    { label: "涓氬姟绫诲瀷", prop: "businessName", minWidth: 120 },
+    { label: "妯℃澘绫诲瀷", prop: "businessName", minWidth: 120 },
     {
       label: "瀹℃壒绫诲瀷",
       prop: "approvalType",
@@ -220,10 +222,18 @@
   }
 
   function resetSearch() {
-    searchForm.approvalType = "";
-    searchForm.applicantKeyword = "";
+    searchForm.businessType = "";
+    searchForm.status = "";
     searchForm.createTimeRange = [];
     handleQuery();
+  }
+
+  async function loadSearchBusinessTypeOptions() {
+    try {
+      searchBusinessTypeOptions.value = await fetchBusinessTypeOptions();
+    } catch {
+      searchBusinessTypeOptions.value = [];
+    }
   }
 
   function pagination({ page: p, limit }) {
@@ -471,6 +481,9 @@
   return {
     Search,
     APPROVAL_TYPE_OPTIONS,
+    APPROVAL_STATUS_SEARCH_OPTIONS,
+    searchBusinessTypeOptions,
+    loadSearchBusinessTypeOptions,
     approvalTypeLabel,
     approvalStatusLabel,
     approvalStatusTagType,
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js
new file mode 100644
index 0000000..895ff0d
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js
@@ -0,0 +1,100 @@
+import { computed } from "vue";
+import {
+  businessApprovalStatusLabel,
+  businessApprovalStatusTagType,
+  formatFieldDisplayValue,
+  resolveInstanceFormFields,
+} from "../approve-list/approveListConstants.js";
+
+/** 鍒楄〃/璇︽儏涓嶅洖鏄句负鐙珛鍒楃殑濉姤椤� key */
+const DEFAULT_EXCLUDE_KEYS = new Set(["summary"]);
+
+/**
+ * 浠庤鏁版嵁 formConfig 瑙f瀽瀛楁瀹氫箟涓庡~鎶ュ�硷紝骞堕摵骞冲埌琛屼笂渚涗富琛� prop 缁戝畾锛堝睍绀虹敤鏍煎紡鍖栧�硷級
+ */
+export function enrichInstanceRowFromFormConfig(row, caches) {
+  const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row);
+  const formDisplay = {};
+  const displayRow = {
+    ...row,
+    formFieldDefs: fields,
+    formPayload,
+    templateSnapshot: row.templateSnapshot || templateSnapshot,
+    formDisplay,
+  };
+
+  for (const f of fields) {
+    if (!f?.key || DEFAULT_EXCLUDE_KEYS.has(f.key)) continue;
+    const val = formPayload[f.key];
+    let text = formatFieldDisplayValue(f, val, caches);
+    if (
+      text === String(val) &&
+      row?.applicantName &&
+      (f.label === "鐢宠浜�" || f.key === "applicant" || f.key === "applicantName")
+    ) {
+      const idMatch =
+        String(val) === String(row.applicantId) ||
+        String(val) === String(row.applicantNo);
+      if (idMatch) text = row.applicantName;
+    }
+    formDisplay[f.key] = text;
+    displayRow[f.key] = text;
+  }
+
+  return displayRow;
+}
+
+/**
+ * 浠庡垪琛ㄩ琛� formConfig 鐢熸垚涓昏〃鍔ㄦ�佸垪锛坙abel 鍙栬嚜妯℃澘瀛楁 label锛�
+ */
+export function getFormConfigFieldColumns(firstRow, { excludeKeys = DEFAULT_EXCLUDE_KEYS } = {}) {
+  const fields = (firstRow?.formFieldDefs || []).filter(
+    (f) => f?.key && !excludeKeys.has(f.key)
+  );
+  return fields.map((f) => ({
+    label: f.label || f.key,
+    prop: f.key,
+    minWidth: f.type === "textarea" ? 200 : f.type === "datetimerange" ? 160 : 120,
+    showOverflowTooltip: true,
+  }));
+}
+
+/**
+ * 涓氬姟鐢宠涓昏〃鍒楋細鍥哄畾鍒� + formConfig 鍔ㄦ�佸垪 + 瀹℃壒鐘舵�� + 鎿嶄綔
+ */
+export function buildInstanceTableColumns(tableDataRef, buildTableActions, options = {}) {
+  const {
+    excludeKeys = DEFAULT_EXCLUDE_KEYS,
+    beforeFormColumns = [],
+    extraColumns = [],
+    afterFormColumns = [],
+    actionWidth = 260,
+  } = options;
+
+  return computed(() => {
+    const formCols = getFormConfigFieldColumns(tableDataRef.value?.[0], { excludeKeys });
+    return [
+      ...beforeFormColumns,
+      ...formCols,
+      ...extraColumns,
+      ...afterFormColumns,
+      { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
+      {
+        label: "瀹℃壒鐘舵��",
+        prop: "approvalStatus",
+        width: 110,
+        dataType: "tag",
+        formatData: (v) => businessApprovalStatusLabel(v),
+        formatType: (v) => businessApprovalStatusTagType(v),
+      },
+      {
+        dataType: "action",
+        label: "鎿嶄綔",
+        align: "center",
+        fixed: "right",
+        width: actionWidth,
+        operation: buildTableActions(),
+      },
+    ];
+  });
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceRowMappers.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceRowMappers.js
new file mode 100644
index 0000000..8503d6a
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceRowMappers.js
@@ -0,0 +1,10 @@
+import { enrichInstanceRowFromFormConfig } from "./approvalInstanceFormConfigTable.js";
+
+/** @deprecated 缁熶竴浣跨敤 enrichInstanceRowFromFormConfig */
+export const enrichLeaveListRow = enrichInstanceRowFromFormConfig;
+export const enrichOvertimeListRow = enrichInstanceRowFromFormConfig;
+export const enrichRegularListRow = enrichInstanceRowFromFormConfig;
+export const enrichTransferListRow = enrichInstanceRowFromFormConfig;
+export const enrichWorkHandoverListRow = enrichInstanceRowFromFormConfig;
+
+export { enrichInstanceRowFromFormConfig };
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
index ad85890..3b60cb7 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
@@ -1,6 +1,7 @@
+import { matchBusinessTypeValue } from "../approve-list/approveListConstants.js";
+
 /**
- * 鍚勪笟鍔℃ā鍧椾笌瀹℃壒妯℃澘绫诲瀷鐨勬槧灏勶紙閰嶇疆鍖栧叆鍙o級
- *
+ * 鍚勪笟鍔℃ā鍧椾笌瀹℃壒妯℃澘绫诲瀷鐨勬槧灏勶紙閰嶇疆鍖栧叆鍙o級 *
  * 浣跨敤鏂瑰紡锛�
  * 1. 鍦ㄤ笟鍔¢〉寮曞叆 ApprovalTemplateBindDialog锛屼紶鍏� moduleKey
  * 2. 鎴栧湪琛ㄥ崟鍐呭祵 ApprovalTemplateFormSection + useApprovalTemplateBinding({ moduleKey })
@@ -16,6 +17,7 @@
   OVERTIME: "overtime",
   TRAVEL_REIMBURSE: "travel_reimburse",
   COST_REIMBURSE: "cost_reimburse",
+  ENTERPRISE_NEWS: "enterprise_news",
 };
 
 /** @type {Record<string, import('./approvalModuleRegistry.js').ApprovalModuleConfig>} */
@@ -60,6 +62,11 @@
     approvalType: "cost_reimburse",
     typeLabels: ["璐圭敤", "璐圭敤鎶ラ攢"],
   },
+  [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: {
+    label: "浼佷笟鏂伴椈",
+    approvalType: "enterprise_news",
+    typeLabels: ["浼佷笟鏂伴椈", "鏂伴椈", "鏂伴椈鍙戝竷"],
+  },
 };
 
 /**
@@ -75,6 +82,14 @@
   return APPROVAL_MODULE_REGISTRY[moduleKey] || null;
 }
 
+/** 鍒楄〃鏌ヨ榛樿 businessType锛堜笌瀹℃壒鍒楄〃 listPage 绾﹀畾涓�鑷达級 */
+export function getModuleListBusinessType(moduleKey) {
+  const cfg = getApprovalModuleConfig(moduleKey);
+  if (!cfg) return "";
+  if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
+  return cfg.approvalType || "";
+}
+
 export function listApprovalModuleEntries() {
   return Object.entries(APPROVAL_MODULE_REGISTRY).map(([moduleKey, cfg]) => ({
     moduleKey,
@@ -82,21 +97,30 @@
   }));
 }
 
-/** 浠� TypeEnums 閫夐」涓В鏋愭湰妯″潡鐨� businessType */
+/** 浠� TypeEnums 閫夐」涓В鏋愭湰妯″潡鐨� businessType锛堜笌瀹℃壒鍒楄〃涓嬫媺涓�鑷达級 */
 export function resolveModuleBusinessType(moduleKey, typeOptions = []) {
   const cfg = getApprovalModuleConfig(moduleKey);
   if (!cfg) return null;
   if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
 
   const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
-  const hit = (typeOptions || []).find((opt) => {
+  const hitByLabel = (typeOptions || []).find((opt) => {
     const optLabel = String(opt?.label || "").trim();
     if (!optLabel) return false;
     return labels.some(
       (l) => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
     );
   });
-  if (hit?.value != null && hit.value !== "") return hit.value;
+  if (hitByLabel?.value != null && hitByLabel.value !== "") return hitByLabel.value;
+
+  if (cfg.approvalType) {
+    const hitByValue = (typeOptions || []).find(
+      (opt) =>
+        matchBusinessTypeValue(opt?.value, cfg.approvalType) ||
+        matchBusinessTypeValue(opt?.code, cfg.approvalType)
+    );
+    if (hitByValue?.value != null && hitByValue.value !== "") return hitByValue.value;
+  }
 
   return cfg.approvalType || null;
 }
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue
new file mode 100644
index 0000000..2488216
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue
@@ -0,0 +1,123 @@
+<!-- 涓庡鎵瑰垪琛ㄨ鎯呭脊绐椾竴鑷� -->
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="920px"
+    append-to-body
+    destroy-on-close
+    class="approve-detail-dialog"
+    @closed="emit('closed')"
+  >
+    <div class="approve-detail-body">
+      <ApproveDetailPanel :row="row" />
+      <div class="detail-block">
+        <div class="detail-block-title">
+          瀹℃壒娴佺▼锛坽{ row?.tasks?.length || row?.flowNodes?.length || 0 }} 椤癸級
+        </div>
+        <InstanceFlowDisplay :tasks="row?.tasks" :nodes="row?.flowNodes" />
+      </div>
+      <div class="detail-block">
+        <div class="detail-block-title">瀹℃壒璁板綍</div>
+        <el-timeline v-if="row?.approvalRecords?.length" class="approve-record-timeline">
+          <el-timeline-item
+            v-for="(rec, i) in row.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="canEditRow(row)" @click="emit('edit', row)">淇� 鏀�</el-button>
+      <el-button @click="visible = false">鍏� 闂�</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { canEditBusinessInstanceRow } from "../../approve-list/approveListConstants.js";
+import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js";
+import ApproveDetailPanel from "../../approve-list/components/ApproveDetailPanel.vue";
+import InstanceFlowDisplay from "../../approve-list/components/InstanceFlowDisplay.vue";
+
+function canEditRow(row) {
+  return canEditBusinessInstanceRow(row);
+}
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  row: { type: Object, default: () => ({}) },
+  title: { type: String, default: "瀹℃壒璇︽儏" },
+});
+
+const emit = defineEmits(["update:modelValue", "edit", "closed"]);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (v) => emit("update:modelValue", v),
+});
+
+function approvalActionLabel(result) {
+  if (result === "approved") return "閫氳繃";
+  if (result === "rejected") return "椹冲洖";
+  return "寰呭鐞�";
+}
+
+function formatRecordTime(time) {
+  return formatDisplayTime(time) || "鈥�";
+}
+</script>
+
+<style scoped>
+.approve-detail-dialog :deep(.el-dialog__body) {
+  padding-top: 16px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+.approve-detail-body .detail-block {
+  margin-top: 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-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;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue
new file mode 100644
index 0000000..53de9e1
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue
@@ -0,0 +1,112 @@
+<!-- 涓庡鎵瑰垪琛ㄦ彁浜�/淇敼寮圭獥锛堢涓夋锛変竴鑷� -->
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="720px"
+    append-to-body
+    destroy-on-close
+    class="approve-submit-dialog"
+    @closed="emit('closed')"
+  >
+    <el-form ref="innerFormRef" :model="form" :rules="rules" label-width="120px">
+      <el-form-item v-if="isEdit" label="瀹℃壒绫诲瀷">
+        <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate?.approvalType)">
+          {{ activeTemplate?.label || form.templateName || "鈥�" }}
+        </span>
+      </el-form-item>
+      <slot name="before" :form="form" :fields="fields" />
+      <ApprovalTemplateFormSection
+        :active-template="activeTemplate"
+        :fields="fields"
+        :form-payload="form.formPayload"
+        v-model:flow-nodes="form.flowNodes"
+        v-model:attachments="form.storageBlobDTOs"
+        :template-attachments="form.templateAttachments"
+        :user-options="userOptions"
+        :show-template-name="!isEdit"
+        :allow-change-template="false"
+        :flow-attachments-only="flowAttachmentsOnly"
+        :flow-only="flowOnly"
+      />
+      <slot name="after" :form="form" :fields="fields" />
+    </el-form>
+    <template #footer>
+      <el-button type="primary" :loading="saving" @click="handleSubmitClick">
+        {{ isEdit ? "淇� 瀛�" : "鎻� 浜�" }}
+      </el-button>
+      <el-button @click="visible = false">鍙� 娑�</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { approvalTypeStyle } from "../../approve-list/approveListConstants.js";
+import ApprovalTemplateFormSection from "./ApprovalTemplateFormSection.vue";
+
+const innerFormRef = ref(null);
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  title: { type: String, default: "" },
+  form: { type: Object, required: true },
+  rules: { type: Object, default: () => ({}) },
+  fields: { type: Array, default: () => [] },
+  activeTemplate: { type: Object, default: null },
+  userOptions: { type: Array, default: () => [] },
+  isEdit: { type: Boolean, default: false },
+  saving: { type: Boolean, default: false },
+  formRef: { type: Object, default: null },
+  /** 濉姤椤圭敱 before 鎻掓Ы鍗曠嫭娓叉煋鏃惰涓� true */
+  flowAttachmentsOnly: { type: Boolean, default: false },
+  flowOnly: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:modelValue", "submit", "closed"]);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (v) => emit("update:modelValue", v),
+});
+
+watch(
+  innerFormRef,
+  (el) => {
+    if (props.formRef) props.formRef.value = el;
+  },
+  { flush: "post" }
+);
+
+watch(visible, (v) => {
+  if (!v && props.formRef) props.formRef.value = null;
+});
+
+async function handleSubmitClick() {
+  if (!innerFormRef.value) {
+    ElMessage.warning("琛ㄥ崟鏈氨缁紝璇风◢鍚庡啀璇�");
+    return;
+  }
+  try {
+    await innerFormRef.value.validate();
+  } catch {
+    ElMessage.warning("璇峰畬鍠勮〃鍗曞繀濉」鍚庡啀淇濆瓨");
+    return;
+  }
+  emit("submit");
+}
+</script>
+
+<style scoped>
+.approve-type-cell {
+  display: inline-block;
+  padding: 2px 10px;
+  border-radius: 4px;
+  font-size: 13px;
+  line-height: 1.5;
+}
+.approve-submit-dialog :deep(.el-dialog__body) {
+  padding-top: 12px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
index aff58fd..eaa0104 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
@@ -1,21 +1,28 @@
 <!-- 妯℃澘缁戝畾琛ㄥ崟鍖猴細濉姤椤� + 瀹℃壒娴佺▼ + 闄勪欢锛堥』鎸傚湪澶栧眰 el-form 涓嬶級 -->
 <template>
   <template v-if="activeTemplate">
-    <el-form-item v-if="showTemplateName && !hideTemplateName && !flowAttachmentsOnly" label="瀹℃壒妯℃澘">
+    <el-form-item
+      v-if="showTemplateName && !hideTemplateName && !flowAttachmentsOnly && !flowOnly"
+      label="瀹℃壒妯℃澘"
+    >
       <span class="template-name">{{ activeTemplate.label }}</span>
       <el-button v-if="allowChangeTemplate" type="primary" link class="ml12" @click="emit('change-template')">
         鏇存崲妯℃澘
       </el-button>
     </el-form-item>
 
-    <FormPayloadFields v-if="!hideFormFields && !flowAttachmentsOnly" :fields="fields" :form-payload="formPayload" />
+    <FormPayloadFields
+      v-if="!hideFormFields && !flowAttachmentsOnly && !flowOnly"
+      :fields="fields"
+      :form-payload="formPayload"
+    />
 
     <el-form-item label="瀹℃壒娴佺▼" required>
       <TemplateFlowEditor v-model="flowNodesModel" :user-options="userOptions" />
       <p class="section-tip">娴佺▼涓庡鎵逛汉鐢辨ā鏉块缃紝鍙寜闇�寰皟鑺傜偣瀹℃壒浜恒��</p>
     </el-form-item>
 
-    <el-form-item v-if="templateAttachments.length" label="妯℃澘鍙傝��">
+    <el-form-item v-if="!flowOnly && templateAttachments.length" label="妯℃澘鍙傝��">
       <el-tag
         v-for="(f, i) in templateAttachments"
         :key="`tpl-${i}`"
@@ -28,7 +35,7 @@
       <p class="section-tip">浠ヤ笂涓烘ā鏉块檮甯︽枃浠讹紝浠呬緵鍙傝�冿紱鎻愪氦闄勪欢璇峰湪涓嬫柟涓婁紶銆�</p>
     </el-form-item>
 
-    <el-form-item label="闄勪欢">
+    <el-form-item v-if="!flowOnly" label="闄勪欢">
       <FileUpload
         v-model:file-list="attachmentsModel"
         :limit="uploadLimit"
@@ -65,6 +72,8 @@
   hideTemplateName: { type: Boolean, default: false },
   /** 涓� true 鏃朵粎灞曠ず瀹℃壒娴佺▼涓庨檮浠讹紙濉姤椤圭敱鐖剁骇鍗曠嫭娓叉煋锛� */
   flowAttachmentsOnly: { type: Boolean, default: false },
+  /** 涓� true 鏃朵粎灞曠ず瀹℃壒娴佺▼锛堜笉灞曠ず妯℃澘濉姤椤广�侀檮浠剁瓑锛� */
+  flowOnly: { type: Boolean, default: false },
   uploadLimit: { type: Number, default: 10 },
 });
 
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
new file mode 100644
index 0000000..01d90cb
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
@@ -0,0 +1,396 @@
+import {
+  deleteApprovalInstance,
+  listApprovalInstancePage,
+  saveApprovalInstance,
+  updateApprovalInstance,
+} from "@/api/officeProcessAutomation/approvalInstance.js";
+import useUserStore from "@/store/modules/user";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref } from "vue";
+import {
+  applyBindingToForm,
+  buildFormPayloadRules,
+  validateTemplateBinding,
+} from "./approvalTemplateBindingUtils.js";
+import {
+  buildApprovalInstanceListParams,
+  buildEditFormFromInstanceRow,
+  buildInstanceDto,
+  canEditBusinessInstanceRow,
+  createEmptySubmitForm,
+  mapInstanceFromApi,
+  resolveInstanceFormFields,
+  unwrapInstancePage,
+} from "../approve-list/approveListConstants.js";
+import { fetchBusinessTypeOptions } from "../approve-template/approveTemplateConstants.js";
+import {
+  collectOptionSourcesFromFields,
+  fetchSelectOptionCaches,
+} from "../approve-template/selectOptionSource.js";
+import { enrichInstanceRowFromFormConfig } from "./approvalInstanceFormConfigTable.js";
+import {
+  getApprovalModuleConfig,
+  getModuleListBusinessType,
+  resolveModuleBusinessType,
+} from "./approvalModuleRegistry.js";
+
+/**
+ * 涓氬姟鐢宠椤靛叡鐢細瀹℃壒瀹炰緥鍒楄〃鏌ヨ銆佹柊澧�/淇敼淇濆瓨銆佽鎯�/缂栬緫寮圭獥锛堜笌瀹℃壒鍒楄〃涓�鑷达級
+ *
+ * @param {object} options
+ * @param {string} options.moduleKey approvalModuleRegistry 涓殑 key
+ * @param {(row: object) => object} [options.enrichListRow] 鍒楄〃琛屽寮猴紙浠� formPayload 瑙f瀽灞曠ず瀛楁锛�
+ * @param {(base: object) => object} [options.buildExtraListParams] 杩藉姞鏌ヨ鍙傛暟
+ * @param {() => void} [options.beforeSave] 淇濆瓨鍓嶉挬瀛愶紙濡傚悓姝ヤ笟鍔″瓧娈靛埌 formPayload锛�
+ * @param {import('vue').ComputedRef|object} [options.extraFormRules] 棰濆琛ㄥ崟鏍¢獙
+ */
+export function useApprovalInstanceModule(options = {}) {
+  const {
+    moduleKey,
+    enrichListRow,
+    buildExtraListParams,
+    beforeSave,
+    extraFormRules,
+  } = options;
+
+  const userStore = useUserStore();
+  const moduleConfig = computed(() => getApprovalModuleConfig(moduleKey));
+  const businessTypeOptions = ref([]);
+
+  /** 涓庡鎵瑰垪琛ㄤ竴鑷达細浼樺厛鐢� TypeEnums 鐨� value锛屽尮閰嶄笉鍒板啀鍥為�� approvalType */
+  const defaultListBusinessType = computed(() => {
+    const resolved = resolveModuleBusinessType(moduleKey, businessTypeOptions.value);
+    if (resolved != null && resolved !== "") return resolved;
+    return getModuleListBusinessType(moduleKey);
+  });
+
+  async function loadBusinessTypeOptions() {
+    if (businessTypeOptions.value.length) return;
+    try {
+      businessTypeOptions.value = await fetchBusinessTypeOptions();
+    } catch {
+      businessTypeOptions.value = [];
+    }
+  }
+
+  const tableData = ref([]);
+  const tableLoading = ref(false);
+  const page = reactive({ current: 1, size: 10, total: 0 });
+
+  const detailDialog = reactive({ visible: false });
+  const detailRow = ref({});
+
+  const submitDialog = reactive({ visible: false, mode: "add" });
+  const submitEditRow = ref(null);
+  const submitForm = reactive(createEmptySubmitForm(""));
+  const submitFormRef = ref();
+  const submitSaving = ref(false);
+
+  const templateBindVisible = ref(false);
+  const pendingTemplateBinding = ref(null);
+  /** 鏈�杩戜竴娆″垪琛ㄦ煡璇㈡潯浠讹紙淇濆瓨鍚庡埛鏂板垪琛ㄦ椂娌跨敤锛� */
+  let lastListSearchForm = null;
+
+  const isSubmitEdit = computed(() => submitDialog.mode === "edit");
+  const activeTemplate = computed(() => submitForm.templateSnapshot || null);
+  const submitFormFields = computed(() => {
+    const tplFields = activeTemplate.value?.fields;
+    if (tplFields?.length) return tplFields;
+    return submitForm.formFieldDefs || [];
+  });
+
+  const submitFormRules = computed(() => ({
+    ...buildFormPayloadRules(submitFormFields.value),
+    ...(extraFormRules?.value ?? extraFormRules ?? {}),
+  }));
+
+  const submitDialogTitle = computed(() => {
+    const label = moduleConfig.value?.label || "鐢宠";
+    if (submitDialog.mode === "edit") {
+      return `淇敼${activeTemplate.value?.label || submitForm.templateName || label}`;
+    }
+    return `鏂板${label}`;
+  });
+
+  function mapListRow(row, caches) {
+    const mapped = mapInstanceFromApi(row);
+    const fromFormConfig = enrichInstanceRowFromFormConfig(mapped, caches);
+    return enrichListRow ? enrichListRow(fromFormConfig) : fromFormConfig;
+  }
+
+  async function fetchList(searchForm = {}) {
+    await loadBusinessTypeOptions();
+    tableLoading.value = true;
+    try {
+      let extraParams = {};
+      if (buildExtraListParams) {
+        extraParams = buildExtraListParams(searchForm) || {};
+      }
+      const res = await listApprovalInstancePage(
+        buildApprovalInstanceListParams({
+          page,
+          searchForm,
+          businessType: defaultListBusinessType.value,
+          extraParams,
+        })
+      );
+      const { records, total } = unwrapInstancePage(res);
+      const mapped = records.map(mapInstanceFromApi);
+      const allFields = [];
+      for (const row of mapped) {
+        const { fields } = resolveInstanceFormFields(row);
+        allFields.push(...fields);
+      }
+      const caches = await fetchSelectOptionCaches(
+        collectOptionSourcesFromFields(allFields)
+      );
+      tableData.value = mapped.map((row) => mapListRow(row, caches));
+      page.total = total;
+    } catch {
+      tableData.value = [];
+      page.total = 0;
+      ElMessage.error(`${moduleConfig.value?.label || "鐢宠"}鍒楄〃鍔犺浇澶辫触`);
+    } finally {
+      tableLoading.value = false;
+    }
+  }
+
+  function handleQuery(searchForm) {
+    lastListSearchForm = searchForm;
+    page.current = 1;
+    return fetchList(searchForm);
+  }
+
+  /** 杩涘叆椤甸潰锛氬厛鎷� TypeEnums 瑙f瀽 businessType锛屽啀鏌ュ垪琛� */
+  async function initModuleList(searchForm) {
+    await loadBusinessTypeOptions();
+    return handleQuery(searchForm);
+  }
+
+  function pagination({ page: p, limit }, searchForm) {
+    page.current = p;
+    page.size = limit;
+    return fetchList(searchForm);
+  }
+
+  function openDetail(row) {
+    detailRow.value = { ...row };
+    detailDialog.visible = true;
+  }
+
+  function openEdit(row) {
+    if (!canEditBusinessInstanceRow(row)) {
+      ElMessage.warning("杩涜涓垨宸插畬鎴愮殑瀹℃壒涓嶅彲淇敼");
+      return;
+    }
+    if (!row?.id) {
+      ElMessage.warning("鏃犳硶淇敼锛氱己灏戝鎵瑰疄渚� ID");
+      return;
+    }
+    submitDialog.mode = "edit";
+    submitEditRow.value = { ...row };
+    Object.assign(submitForm, buildEditFormFromInstanceRow(row));
+    submitDialog.visible = true;
+  }
+
+  function openEditFromDetail() {
+    const row = detailRow.value;
+    detailDialog.visible = false;
+    openEdit(row);
+  }
+
+  function resetSubmitForm() {
+    Object.assign(submitForm, createEmptySubmitForm(""));
+    submitEditRow.value = null;
+  }
+
+  function openAddWithTemplate() {
+    submitDialog.visible = false;
+    pendingTemplateBinding.value = null;
+    templateBindVisible.value = true;
+  }
+
+  function onTemplateBound(binding) {
+    pendingTemplateBinding.value = binding;
+  }
+
+  function onTemplateBindClosed() {
+    const binding = pendingTemplateBinding.value;
+    if (!binding) return;
+    pendingTemplateBinding.value = null;
+    openAddFromBinding(binding);
+  }
+
+  function openAddFromBinding(binding) {
+    resetSubmitForm();
+    applyBindingToForm(submitForm, binding);
+    submitDialog.mode = "add";
+    submitEditRow.value = null;
+    submitDialog.visible = true;
+  }
+
+  function closeSubmitDialog() {
+    submitDialog.visible = false;
+  }
+
+  async function submitInstanceForm(options = {}) {
+    const { skipValidate = false } = options;
+    if (!skipValidate) {
+      if (!submitFormRef.value?.validate) {
+        ElMessage.warning("琛ㄥ崟鏈氨缁紝璇峰叧闂脊绐楀悗閲嶈瘯");
+        return false;
+      }
+      try {
+        await submitFormRef.value.validate();
+      } catch {
+        ElMessage.warning("璇峰畬鍠勮〃鍗曞繀濉」鍚庡啀淇濆瓨");
+        return false;
+      }
+    }
+    if (!activeTemplate.value) {
+      ElMessage.warning("鏈姞杞藉鎵规ā鏉匡紝鏃犳硶淇濆瓨");
+      return false;
+    }
+    const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+    if (!bindingCheck.ok) {
+      ElMessage.warning(bindingCheck.message);
+      return false;
+    }
+    if (!submitForm.templateId) {
+      ElMessage.warning("缂哄皯妯℃澘 ID锛屾棤娉曟彁浜�");
+      return false;
+    }
+    if (beforeSave) {
+      try {
+        await beforeSave(submitForm, { isEdit: isSubmitEdit.value, editRow: submitEditRow.value });
+      } catch {
+        return false;
+      }
+    }
+    if (submitSaving.value) return false;
+    submitSaving.value = true;
+    try {
+      const dto = buildInstanceDto({
+        submitForm,
+        activeTemplate: activeTemplate.value,
+        userStore,
+        flowNodes: bindingCheck.nodes,
+        existingRow: isSubmitEdit.value ? submitEditRow.value : null,
+      });
+      if (isSubmitEdit.value) {
+        await updateApprovalInstance(dto);
+      } else {
+        await saveApprovalInstance(dto);
+      }
+      submitDialog.visible = false;
+      if (!isSubmitEdit.value) page.current = 1;
+      await fetchList(lastListSearchForm ?? {});
+      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 {
+      ElMessage.error(isSubmitEdit.value ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触");
+      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 (submitDialog.visible && submitEditRow.value?.id === row.id) {
+        submitDialog.visible = false;
+      }
+      await fetchList(lastListSearchForm ?? {});
+    } catch {
+      /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
+    }
+  }
+
+  /** 鏋勫缓鏍囧噯鎿嶄綔鍒楋細璇︽儏銆佷慨鏀广�佸垹闄わ紙涓庡鎵瑰垪琛ㄤ竴鑷达級 */
+  function buildTableActions(extraOperations = []) {
+    return [
+      { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+      {
+        name: "淇敼",
+        type: "text",
+        disabled: (row) => !canEditBusinessInstanceRow(row),
+        clickFun: (row) => openEdit(row),
+      },
+      {
+        name: "鍒犻櫎",
+        type: "danger",
+        clickFun: (row) => removeInstance(row),
+      },
+      ...extraOperations,
+    ];
+  }
+
+  return {
+    moduleConfig,
+    defaultListBusinessType,
+    tableData,
+    tableLoading,
+    page,
+    detailDialog,
+    detailRow,
+    submitDialog,
+    submitEditRow,
+    submitForm,
+    submitFormRef,
+    submitSaving,
+    isSubmitEdit,
+    activeTemplate,
+    submitFormFields,
+    submitFormRules,
+    submitDialogTitle,
+    templateBindVisible,
+    pendingTemplateBinding,
+    fetchList,
+    handleQuery,
+    initModuleList,
+    pagination,
+    openDetail,
+    openEdit,
+    openEditFromDetail,
+    openAddWithTemplate,
+    onTemplateBound,
+    onTemplateBindClosed,
+    openAddFromBinding,
+    closeSubmitDialog,
+    resetSubmitForm,
+    submitInstanceForm,
+    removeInstance,
+    buildTableActions,
+    loadBusinessTypeOptions,
+    canEditBusinessInstanceRow,
+  };
+}
diff --git a/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
index 7bd7b58..546ca87 100644
--- a/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
+++ b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
@@ -10,13 +10,13 @@
           placeholder="濮撳悕鎴栫紪鍙�"
           clearable
           :prefix-icon="Search"
-          @keyup.enter="handleQuery"
+          @keyup.enter="onSearch"
         />
         <span class="search_title" style="margin-left: 12px">璇峰亣绫诲瀷锛�</span>
         <el-select v-model="searchForm.leaveType" placeholder="鍏ㄩ儴" clearable style="width: 180px">
           <el-option v-for="opt in LEAVE_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
         </el-select>
-        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+        <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
         <el-button @click="resetSearch">閲嶇疆</el-button>
       </div>
       <div>
@@ -31,37 +31,27 @@
         :page="page"
         :isSelection="false"
         :tableLoading="tableLoading"
-        @pagination="pagination"
+        @pagination="onPagination"
         :total="page.total"
       />
     </div>
 
-    <!-- 鏂板 / 缂栬緫 -->
-    <el-dialog
-      v-if="formDialog.visible"
-      v-model="formDialog.visible"
-      :title="formDialog.title"
-      width="960px"
-      append-to-body
-      destroy-on-close
-      class="leave-apply-form-dialog"
-      @closed="onFormClosed"
+    <ApprovalInstanceSubmitDialog
+      v-model="submitDialog.visible"
+      :title="submitDialogTitle"
+      :form="submitForm"
+      :rules="submitFormRules"
+      :fields="submitFormFields"
+      :active-template="activeTemplate"
+      :user-options="flowUserOptions"
+      :is-edit="isSubmitEdit"
+      :saving="submitSaving"
+      :form-ref="submitFormRef"
+      flow-attachments-only
+      @submit="onSubmit"
     >
-      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="leave-apply-form">
-        <el-form-item v-if="form.templateSnapshot" label="瀹℃壒妯℃澘">
-          <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span>
-          <el-button
-            v-if="formDialog.mode === 'add'"
-            type="primary"
-            link
-            class="ml12"
-            @click="reopenTemplateBind"
-          >
-            鏇存崲妯℃澘
-          </el-button>
-        </el-form-item>
-
-        <FormPayloadFields :fields="templateDisplayFields" :form-payload="form.formPayload" />
+      <template #before="{ form, fields }">
+        <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
         <el-row :gutter="24">
           <el-col :span="12">
             <el-form-item label="鍋囨湡浣欓" prop="leaveBalanceDays">
@@ -79,32 +69,14 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="璇峰亣鏃堕暱">
-              <el-input :model-value="leaveDurationDisplay" readonly placeholder="鏍规嵁妯℃澘涓鍋囨椂闂磋嚜鍔ㄨ绠�">
+              <el-input :model-value="leaveDurationDisplay(form)" readonly placeholder="鏍规嵁妯℃澘涓鍋囨椂闂磋嚜鍔ㄨ绠�">
                 <template #append>澶�</template>
               </el-input>
             </el-form-item>
           </el-col>
         </el-row>
-        <ApprovalTemplateFormSection
-          :active-template="form.templateSnapshot"
-          :fields="form.formFieldDefs"
-          :form-payload="form.formPayload"
-          v-model:flow-nodes="form.flowNodes"
-          v-model:attachments="form.storageBlobDTOs"
-          :template-attachments="form.templateAttachments"
-          :user-options="flowUserOptions"
-          flow-attachments-only
-          hide-template-name
-          :allow-change-template="false"
-        />
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
-          <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
-        </div>
       </template>
-    </el-dialog>
+    </ApprovalInstanceSubmitDialog>
 
     <ApprovalTemplateBindDialog
       v-model:visible="templateBindVisible"
@@ -114,55 +86,12 @@
       @closed="onTemplateBindClosed"
     />
 
-    <!-- 璇︽儏 -->
-    <el-dialog v-model="detailDialog.visible" title="璇峰亣鐢宠璇︽儏" width="720px" append-to-body>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="鐢宠浜虹紪鍙�">{{ detailRow.applicantNo || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鐢宠浜�">{{ detailRow.applicantName }}</el-descriptions-item>
-        <el-descriptions-item label="璇峰亣绫诲瀷">{{ leaveTypeLabel(detailRow.leaveType) }}</el-descriptions-item>
-        <el-descriptions-item label="鍋囨湡浣欓">{{ formatBalance(detailRow.leaveBalanceDays) }}</el-descriptions-item>
-        <el-descriptions-item label="璇峰亣寮�濮嬫椂闂�">{{ detailRow.leaveStartTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="璇峰亣缁撴潫鏃堕棿">{{ detailRow.leaveEndTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="璇峰亣鏃堕暱">{{ formatDuration(detailRow.leaveDurationDays) }}</el-descriptions-item>
-        <el-descriptions-item label="璇峰亣浜嬬敱">{{ detailRow.leaveReason }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒缁撴灉">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒鏂瑰紡">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒浜�">{{ detailRow.approverNames || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鍒涘缓鏃堕棿">{{ detailRow.createTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="闄勪欢">
-          <template v-if="rowAttachmentList(detailRow).length">
-            <el-tag v-for="(f, i) in rowAttachmentList(detailRow)" :key="i" class="mr6 mb6" type="info">
-              {{ f.name }}
-            </el-tag>
-          </template>
-          <span v-else>鏃�</span>
-        </el-descriptions-item>
-      </el-descriptions>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 闄勪欢鍒楄〃 -->
-    <el-dialog v-model="filesDialog.visible" title="闄勪欢" width="520px" append-to-body>
-      <el-table v-if="rowAttachmentList(filesDialog.row).length" :data="rowAttachmentList(filesDialog.row)" border>
-        <el-table-column type="index" label="搴忓彿" width="60" align="center" />
-        <el-table-column prop="name" label="鏂囦欢鍚�" min-width="200" show-overflow-tooltip />
-        <el-table-column label="鎿嶄綔" width="100" align="center">
-          <template #default="{ row }">
-            <el-button link type="primary" @click="mockDownload(row)">涓嬭浇</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-empty v-else description="鏆傛棤闄勪欢" />
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="filesDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceDetailDialog
+      v-model="detailDialog.visible"
+      title="璇峰亣鐢宠璇︽儏"
+      :row="detailRow"
+      @edit="openEditFromDetail"
+    />
   </div>
 </template>
 
@@ -170,21 +99,18 @@
 import { Search } from "@element-plus/icons-vue";
 import dayjs from "dayjs";
 import { userListNoPageByTenantId } from "@/api/system/user.js";
-import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
-import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
-import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
+import { ElMessage } from "element-plus";
+import { computed, onMounted, reactive, ref, watch } from "vue";
 import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
+import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
+import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
+import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
+import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
 import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
-import {
-  applyBindingToForm,
-  attachmentDisplayName,
-  buildFormPayloadRules,
-  validateTemplateBinding,
-} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
 import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
 import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
 
-/** 璇峰亣绫诲瀷锛坴alue 涓庡悗绔榻愬崰浣嶏級 */
 const LEAVE_TYPE_OPTIONS = [
   { label: "骞村亣", value: "annual" },
   { label: "鐥呭亣", value: "sick" },
@@ -196,70 +122,6 @@
   { label: "璋冧紤", value: "compensatory" },
 ];
 
-function leaveTypeLabel(v) {
-  const hit = LEAVE_TYPE_OPTIONS.find((x) => x.value === v);
-  return hit?.label || "鈥�";
-}
-
-/** 涓庡悗绔害瀹氬瓧娈碉紙鍗犱綅锛� */
-const createEmptyForm = () => ({
-  id: undefined,
-  applicantId: "",
-  applicantNo: "",
-  applicantName: "",
-  leaveType: "",
-  leaveBalanceDays: undefined,
-  leaveStartTime: "",
-  leaveEndTime: "",
-  leaveDurationDays: null,
-  leaveReason: "",
-  hasTemplateBinding: false,
-  templateId: "",
-  templateName: "",
-  templateSnapshot: null,
-  formFieldDefs: [],
-  formPayload: {},
-  flowNodes: [],
-  templateAttachments: [],
-  storageBlobDTOs: [],
-});
-
-const { proxy } = getCurrentInstance();
-
-function unwrapArray(payload) {
-  if (Array.isArray(payload)) return payload;
-  if (payload && Array.isArray(payload.data)) return payload.data;
-  if (payload && Array.isArray(payload.rows)) return payload.rows;
-  return [];
-}
-
-function isActiveUser(u) {
-  if (u.delFlag === "2" || u.delFlag === 2) return false;
-  if (u.status == null) return true;
-  return String(u.status) === "0";
-}
-
-function userById(id) {
-  if (id == null || id === "") return undefined;
-  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
-}
-
-function applicantNoFromUser(u) {
-  if (!u) return "";
-  return (
-    u.userName ??
-    u.userCode ??
-    u.jobNumber ??
-    u.workNo ??
-    (u.userId != null ? String(u.userId) : "")
-  );
-}
-
-/** 鍋囨湡浣欓锛堝鎺ヨ�冨嫟 API 鍓嶄笉灞曠ず鍋囨暟鎹級 */
-function mockLeaveBalance() {
-  return undefined;
-}
-
 function isLeaveBalanceField(field) {
   const label = String(field?.label || "");
   return label.includes("鍋囨湡浣欓") || field?.key === "leaveBalanceDays";
@@ -268,6 +130,10 @@
 function isLeaveDurationField(field) {
   const label = String(field?.label || "");
   return label.includes("璇峰亣鏃堕暱") || field?.key === "leaveDurationDays";
+}
+
+function displayTemplateFields(fields = []) {
+  return (fields || []).filter((f) => !isLeaveBalanceField(f) && !isLeaveDurationField(f));
 }
 
 function findLeaveTimeTemplateField(fields = []) {
@@ -287,7 +153,6 @@
   );
 }
 
-/** 浠庢ā鏉垮~鎶ラ」瑙f瀽璇峰亣璧锋鏃堕棿 */
 function resolveLeaveTimeRange(payload, leaveTimeField) {
   if (!leaveTimeField?.key) return { start: "", end: "" };
   const val = payload?.[leaveTimeField.key];
@@ -295,7 +160,6 @@
   return { start: val[0] || "", end: val[1] || "" };
 }
 
-/** 鎸夎捣姝㈡椂闂磋绠楄鍋囧ぉ鏁帮紙鍚椂鍒嗙锛岀粨鏋滀繚鐣欎袱浣嶅皬鏁帮級 */
 function computeLeaveDays(startStr, endStr) {
   if (!startStr || !endStr) return null;
   const t0 = dayjs(startStr);
@@ -305,61 +169,75 @@
   return Math.round(days * 100) / 100;
 }
 
-function formatDuration(v) {
-  if (v == null || v === "") return "鈥�";
-  return `${v} 澶ー;
+function leaveDurationDisplay(form) {
+  const leaveTimeField = findLeaveTimeTemplateField(form.formFieldDefs);
+  const { start, end } = resolveLeaveTimeRange(form.formPayload, leaveTimeField);
+  const d = computeLeaveDays(start, end);
+  return d == null ? "" : String(d);
 }
 
-function formatBalance(v) {
-  if (v == null || v === "") return "鈥�";
-  return `${v} 澶ー;
-}
+const searchForm = reactive({
+  applicantKeyword: "",
+  leaveType: "",
+});
 
-function mapStorageBlobsToAttachmentList(blobs) {
-  return (blobs || []).map((f) => ({
-    name: attachmentDisplayName(f),
-    url: f.url || f.downloadURL || f.previewURL || f.previewUrl,
-  }));
-}
-
-function rowAttachmentList(row) {
-  if (!row) return [];
-  if (row.attachmentList?.length) return row.attachmentList;
-  return mapStorageBlobsToAttachmentList(row.storageBlobDTOs);
-}
-
-function approvalModeLabel(mode) {
-  if (mode === "countersign") return "浼氱";
-  if (mode === "or_sign") return "鎴栫";
-  return "涓庣";
-}
-
-function approvalResultLabel(v) {
-  if (v === "approved") return "宸查�氳繃";
-  if (v === "rejected") return "宸查┏鍥�";
-  if (v === "cancelled") return "宸叉挙閿�";
-  return "寰呭鎵�";
-}
-
-function syncApplicantFromUser(uid) {
-  const u = userById(uid);
-  if (u) {
-    form.applicantId = uid != null && uid !== "" ? uid : "";
-    form.applicantName = u.nickName || u.userName || "";
-    form.applicantNo = applicantNoFromUser(u);
-    form.leaveBalanceDays = mockLeaveBalance(u);
-  } else {
-    form.applicantId = "";
-    form.applicantName = "";
-    form.applicantNo = "";
-    if (uid == null || uid === "") {
-      form.leaveBalanceDays = undefined;
-    }
+function validateLeaveBeforeSave() {
+  const leaveTimeField = findLeaveTimeTemplateField(submitForm.formFieldDefs);
+  const { start, end } = resolveLeaveTimeRange(submitForm.formPayload, leaveTimeField);
+  if (computeLeaveDays(start, end) == null) {
+    ElMessage.warning("璇锋鏌ユā鏉夸腑鐨勮鍋囨椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�");
+    throw new Error("invalid leave time");
   }
 }
 
-/** 绯荤粺鐢ㄦ埛缂撳瓨 */
+const mod = useApprovalInstanceModule({
+  moduleKey: APPROVAL_MODULE_KEYS.LEAVE,
+  beforeSave: validateLeaveBeforeSave,
+  extraFormRules: {
+    leaveBalanceDays: [{ required: true, message: "璇峰~鍐欏亣鏈熶綑棰�", trigger: "blur" }],
+  },
+});
+
+const {
+  tableData,
+  tableLoading,
+  page,
+  detailDialog,
+  detailRow,
+  submitDialog,
+  submitEditRow,
+  submitForm,
+  submitFormRef,
+  submitSaving,
+  isSubmitEdit,
+  activeTemplate,
+  submitFormFields,
+  submitFormRules,
+  submitDialogTitle,
+  templateBindVisible,
+  handleQuery,
+  initModuleList,
+  pagination,
+  openAddWithTemplate,
+  onTemplateBound,
+  onTemplateBindClosed,
+  openEditFromDetail,
+  submitInstanceForm,
+  buildTableActions,
+} = mod;
+
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
 const allUsersCache = ref([]);
+
+const applicantTemplateField = computed(() =>
+  findApplicantTemplateField(submitForm.formFieldDefs)
+);
+
+function unwrapArray(payload) {
+  if (Array.isArray(payload)) return payload;
+  if (payload && Array.isArray(payload.data)) return payload.data;
+  return [];
+}
 
 async function loadUserPool() {
   try {
@@ -370,412 +248,56 @@
   }
 }
 
-const allRows = ref([]);
-
-const searchForm = reactive({
-  applicantKeyword: "",
-  leaveType: "",
-});
-
-const tableLoading = ref(false);
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-
-const filteredList = computed(() => {
-  let list = [...allRows.value];
-  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);
-    });
-  }
-  if (searchForm.leaveType) {
-    list = list.filter((r) => r.leaveType === searchForm.leaveType);
-  }
-  return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 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;
+  () => submitDialog.visible,
+  (v) => {
+    if (!v) return;
+    if (submitForm.leaveBalanceDays == null && isSubmitEdit.value) {
+      submitForm.leaveBalanceDays =
+        submitEditRow.value?.formPayload?.leaveBalanceDays ??
+        submitEditRow.value?.leaveBalanceDays;
     }
-  },
-  { immediate: true }
+    if (submitForm.leaveBalanceDays == null && !isSubmitEdit.value) {
+      submitForm.leaveBalanceDays = undefined;
+    }
+  }
 );
-
-const tableData = computed(() => {
-  const list = filteredList.value;
-  const start = (page.current - 1) * page.size;
-  return list.slice(start, start + page.size);
-});
-
-const tableColumn = ref([
-  { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 120 },
-  { label: "鐢宠浜�", prop: "applicantName", minWidth: 100 },
-  {
-    label: "璇峰亣绫诲瀷",
-    prop: "leaveType",
-    width: 100,
-    formatData: (v) => leaveTypeLabel(v),
-  },
-  {
-    label: "璇峰亣鏃堕暱",
-    prop: "leaveDurationDays",
-    width: 120,
-    formatData: (v) => (v == null || v === "" ? "鈥�" : `${v} 澶ー),
-  },
-  { label: "璇峰亣浜嬬敱", prop: "leaveReason", minWidth: 180 },
-  { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
-  {
-    label: "瀹℃壒缁撴灉",
-    prop: "approvalResult",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => approvalResultLabel(v),
-    formatType: (v) => {
-      if (v === "approved") return "success";
-      if (v === "rejected") return "danger";
-      if (v === "cancelled") return "info";
-      return "warning";
-    },
-  },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    fixed: "right",
-    width: 220,
-    operation: [
-      {
-        name: "缂栬緫",
-        type: "text",
-        clickFun: (row) => openFormDialog("edit", row),
-      },
-      {
-        name: "鏌ョ湅璇︽儏",
-        type: "text",
-        clickFun: (row) => openDetail(row),
-      },
-      {
-        name: "闄勪欢",
-        type: "text",
-        clickFun: (row) => openFiles(row),
-      },
-    ],
-  },
-]);
-
-const formDialog = reactive({
-  visible: false,
-  title: "",
-  mode: "add",
-});
-const formRef = ref();
-const form = reactive(createEmptyForm());
-const templateBindVisible = ref(false);
-const pendingTemplateBinding = ref(null);
-const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
-
-const leaveTimeTemplateField = computed(() => findLeaveTimeTemplateField(form.formFieldDefs));
-const applicantTemplateField = computed(() => findApplicantTemplateField(form.formFieldDefs));
-
-const templateDisplayFields = computed(() =>
-  (form.formFieldDefs || []).filter((f) => !isLeaveBalanceField(f) && !isLeaveDurationField(f))
-);
-
-const leaveDurationDisplay = computed(() => {
-  const { start, end } = resolveLeaveTimeRange(form.formPayload, leaveTimeTemplateField.value);
-  const d = computeLeaveDays(start, end);
-  return d == null ? "" : String(d);
-});
-
-const formRules = computed(() => ({
-  ...buildFormPayloadRules(templateDisplayFields.value),
-  leaveBalanceDays: [
-    {
-      required: true,
-      message: "璇峰~鍐欏亣鏈熶綑棰�",
-      trigger: "blur",
-    },
-  ],
-}));
 
 watch(
   () => {
     const key = applicantTemplateField.value?.key;
-    return key ? form.formPayload[key] : undefined;
+    return key ? submitForm.formPayload[key] : undefined;
   },
   async (uid) => {
-    if (!applicantTemplateField.value) return;
-    if (!allUsersCache.value.length) {
-      await loadUserPool();
-    }
-    syncApplicantFromUser(uid);
+    if (!applicantTemplateField.value || !uid) return;
+    if (!allUsersCache.value.length) await loadUserPool();
   }
 );
 
-watch(
-  () => {
-    const key = leaveTimeTemplateField.value?.key;
-    return key ? form.formPayload[key] : undefined;
-  },
-  () => {
-    const { start, end } = resolveLeaveTimeRange(form.formPayload, leaveTimeTemplateField.value);
-    form.leaveStartTime = start;
-    form.leaveEndTime = end;
-    form.leaveDurationDays = computeLeaveDays(start, end);
-  },
-  { deep: true }
-);
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
 
-const detailDialog = reactive({ visible: false });
-const detailRow = ref({});
-
-const filesDialog = reactive({ visible: false, row: null });
-
-function handleQuery() {
-  page.current = 1;
-  tableLoading.value = true;
-  setTimeout(() => {
-    tableLoading.value = false;
-  }, 150);
+function onSearch() {
+  handleQuery(searchForm);
 }
 
 function resetSearch() {
   searchForm.applicantKeyword = "";
   searchForm.leaveType = "";
-  handleQuery();
+  onSearch();
 }
 
-function pagination(obj) {
-  page.current = obj.page;
-  page.size = obj.limit;
+function onPagination(obj) {
+  pagination(obj, searchForm);
 }
 
-function openDetail(row) {
-  detailRow.value = { ...row };
-  detailDialog.visible = true;
+async function onSubmit() {
+  const ok = await submitInstanceForm({ skipValidate: true });
+  if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
 }
 
-function openFiles(row) {
-  filesDialog.row = row;
-  filesDialog.visible = true;
-}
-
-function mockDownload(row) {
-  const url = row.url || row.downloadURL || row.previewURL || row.previewUrl;
-  if (url) {
-    window.open(url, "_blank");
-    return;
-  }
-  proxy?.$modal?.msgWarning?.("鏆傛棤涓嬭浇鍦板潃");
-}
-
-function openAddWithTemplate() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function onTemplateBound(binding) {
-  pendingTemplateBinding.value = binding;
-}
-
-async function onTemplateBindClosed() {
-  const binding = pendingTemplateBinding.value;
-  if (!binding) return;
-  pendingTemplateBinding.value = null;
-  await openFormWithBinding(binding);
-}
-
-async function openFormWithBinding(binding) {
-  Object.assign(form, createEmptyForm());
-  applyBindingToForm(form, binding);
-  form.hasTemplateBinding = true;
-  formDialog.mode = "add";
-  formDialog.title = "鏂板璇峰亣鐢宠";
-  await Promise.all([loadUserPool(), loadFlowUsers()]);
-  const applicantKey = applicantTemplateField.value?.key;
-  if (applicantKey && form.formPayload[applicantKey]) {
-    syncApplicantFromUser(form.formPayload[applicantKey]);
-  }
-  const { start, end } = resolveLeaveTimeRange(form.formPayload, leaveTimeTemplateField.value);
-  form.leaveStartTime = start;
-  form.leaveEndTime = end;
-  form.leaveDurationDays = computeLeaveDays(start, end);
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function reopenTemplateBind() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-async function openFormDialog(mode, row) {
-  if (mode === "edit" && row && !row.hasTemplateBinding) {
-    proxy?.$modal?.msgWarning?.("璇ヨ褰曚负鏃х増鏁版嵁锛岃閲嶆柊閫氳繃妯℃澘鍙戣捣鐢宠");
-    return;
-  }
-  formDialog.mode = mode;
-  formDialog.title = "缂栬緫璇峰亣鐢宠";
-  Object.assign(form, createEmptyForm());
-  if (mode === "edit" && row) {
-    Object.assign(form, {
-      id: row.id,
-      applicantId: row.applicantId,
-      applicantNo: row.applicantNo,
-      applicantName: row.applicantName,
-      leaveType: row.leaveType,
-      leaveBalanceDays: row.leaveBalanceDays,
-      leaveStartTime: row.leaveStartTime,
-      leaveEndTime: row.leaveEndTime,
-      leaveDurationDays: row.leaveDurationDays,
-      leaveReason: row.leaveReason,
-      hasTemplateBinding: true,
-      templateId: row.templateId,
-      templateName: row.templateName,
-      templateSnapshot: row.templateSnapshot,
-      formFieldDefs: row.formFieldDefs || [],
-      formPayload: JSON.parse(JSON.stringify(row.formPayload || {})),
-      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])),
-      templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])),
-      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
-    });
-    await loadUserPool();
-    const applicantKey = applicantTemplateField.value?.key;
-    if (applicantKey) {
-      syncApplicantFromUser(form.formPayload[applicantKey]);
-    }
-    loadFlowUsers();
-  }
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function onFormClosed() {
-  formRef.value?.resetFields?.();
-}
-
-/** 浠庢ā鏉垮~鎶ラ」鍚屾鍒楄〃灞曠ず瀛楁 */
-function syncLeaveFieldsFromPayload() {
-  const defs = form.formFieldDefs || [];
-  const payload = form.formPayload || {};
-  const leaveTimeField = findLeaveTimeTemplateField(defs);
-
-  for (const f of defs) {
-    const label = String(f.label || "");
-    const val = payload[f.key];
-
-    if (label.includes("鐢宠浜�") && !label.includes("鏃ユ湡") && !label.includes("鏃堕棿")) {
-      if (val != null && val !== "") {
-        form.applicantId = val;
-        const u = userById(val);
-        if (u) {
-          form.applicantName = u.nickName || u.userName || "";
-          form.applicantNo = applicantNoFromUser(u);
-        }
-      }
-    }
-    if ((label.includes("璇峰亣绫诲瀷") || f.key === "leaveType") && f.type === "select") {
-      form.leaveType = val != null && val !== "" ? val : "";
-    }
-    if (label.includes("浜嬬敱") || f.key === "summary" || label.includes("璇峰亣浜嬬敱")) {
-      form.leaveReason = val != null ? String(val) : "";
-    }
-  }
-
-  const { start, end } = resolveLeaveTimeRange(payload, leaveTimeField);
-  form.leaveStartTime = start;
-  form.leaveEndTime = end;
-  form.leaveDurationDays = computeLeaveDays(start, end);
-}
-
-async function submitForm() {
-  try {
-    await formRef.value?.validate?.();
-  } catch {
-    return;
-  }
-  const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes });
-  if (!flowCheck.ok) {
-    proxy?.$modal?.msgWarning?.(flowCheck.message || "璇峰畬鍠勫鎵规祦绋�");
-    return;
-  }
-  form.flowNodes = flowCheck.nodes;
-
-  const applicantKey = applicantTemplateField.value?.key;
-  if (applicantKey) {
-    syncApplicantFromUser(form.formPayload[applicantKey]);
-  }
-  syncLeaveFieldsFromPayload();
-
-  if (form.leaveDurationDays == null) {
-    proxy?.$modal?.msgWarning?.("璇锋鏌ユā鏉夸腑鐨勮鍋囨椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�");
-    return;
-  }
-
-  const attachmentList = mapStorageBlobsToAttachmentList(form.storageBlobDTOs);
-  const payload = {
-    applicantId: form.applicantId,
-    applicantNo: form.applicantNo,
-    applicantName: form.applicantName,
-    leaveType: form.leaveType,
-    leaveBalanceDays: form.leaveBalanceDays,
-    leaveStartTime: form.leaveStartTime,
-    leaveEndTime: form.leaveEndTime,
-    leaveDurationDays: form.leaveDurationDays,
-    leaveReason: form.leaveReason,
-    hasTemplateBinding: true,
-    templateId: form.templateId,
-    templateName: form.templateName,
-    templateSnapshot: form.templateSnapshot,
-    formFieldDefs: form.formFieldDefs,
-    formPayload: JSON.parse(JSON.stringify(form.formPayload || {})),
-    flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])),
-    templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])),
-    storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])),
-    attachmentList,
-  };
-  if (formDialog.mode === "add") {
-    const id = `local_${Date.now()}`;
-    allRows.value.unshift({
-      id,
-      ...payload,
-      approvalResult: "pending",
-      createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
-    });
-    proxy?.$modal?.msgSuccess?.("鏂板鎴愬姛");
-  } else {
-    const idx = allRows.value.findIndex((r) => r.id === form.id);
-    if (idx !== -1) {
-      const prev = allRows.value[idx];
-      allRows.value[idx] = {
-        ...prev,
-        id: form.id,
-        ...payload,
-        approvalResult: prev.approvalResult ?? "pending",
-        createTime: prev.createTime ?? dayjs().format("YYYY-MM-DD HH:mm:ss"),
-      };
-    }
-    proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
-  }
-  formDialog.visible = false;
-  handleQuery();
-}
-
-onMounted(() => {
+onMounted(async () => {
   loadFlowUsers();
+  await initModuleList(searchForm);
 });
 </script>
 
@@ -793,27 +315,5 @@
 .search_title {
   font-size: 14px;
   color: var(--el-text-color-regular);
-}
-.mr6 {
-  margin-right: 6px;
-}
-.mb6 {
-  margin-bottom: 6px;
-}
-.leave-apply-form :deep(.el-row) {
-  margin-bottom: 0;
-}
-.leave-apply-form :deep(.el-form-item) {
-  margin-bottom: 18px;
-}
-.template-name {
-  font-weight: 600;
-  color: var(--el-text-color-primary);
-}
-.ml12 {
-  margin-left: 12px;
-}
-.leave-apply-form-dialog :deep(.el-dialog__body) {
-  padding-top: 12px;
 }
 </style>
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
index b04a9cd..17303aa 100644
--- a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
@@ -10,22 +10,20 @@
           placeholder="濮撳悕鎴栫紪鍙�"
           clearable
           :prefix-icon="Search"
-          @keyup.enter="handleQuery"
+          @keyup.enter="onSearch"
         />
         <span class="search_title" style="margin-left: 12px">鍔犵彮绫诲瀷锛�</span>
         <el-select v-model="searchForm.overtimeType" placeholder="鍏ㄩ儴" clearable style="width: 180px">
           <el-option v-for="opt in OVERTIME_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
         </el-select>
-        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+        <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
         <el-button @click="resetSearch">閲嶇疆</el-button>
       </div>
       <div class="search_actions">
-        <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>
         <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
         <el-button type="primary" @click="openAddWithTemplate">鏂板鍔犵彮鐢宠</el-button>
       </div>
     </div>
-    <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" />
 
     <div class="table_list">
       <PIMTable
@@ -35,66 +33,38 @@
         :page="page"
         :isSelection="false"
         :tableLoading="tableLoading"
-        @pagination="pagination"
+        @pagination="onPagination"
         :total="page.total"
       />
     </div>
 
-    <!-- 鏂板 / 缂栬緫 -->
-    <el-dialog
-      v-if="formDialog.visible"
-      v-model="formDialog.visible"
-      :title="formDialog.title"
-      width="960px"
-      append-to-body
-      destroy-on-close
-      class="overtime-apply-form-dialog"
-      @closed="onFormClosed"
+    <ApprovalInstanceSubmitDialog
+      v-model="submitDialog.visible"
+      :title="submitDialogTitle"
+      :form="submitForm"
+      :rules="submitFormRules"
+      :fields="submitFormFields"
+      :active-template="activeTemplate"
+      :user-options="flowUserOptions"
+      :is-edit="isSubmitEdit"
+      :saving="submitSaving"
+      :form-ref="submitFormRef"
+      flow-attachments-only
+      @submit="onSubmit"
     >
-      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="overtime-apply-form">
-        <el-form-item v-if="form.templateSnapshot" label="瀹℃壒妯℃澘">
-          <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span>
-          <el-button
-            v-if="formDialog.mode === 'add'"
-            type="primary"
-            link
-            class="ml12"
-            @click="reopenTemplateBind"
-          >
-            鏇存崲妯℃澘
-          </el-button>
-        </el-form-item>
-
-        <FormPayloadFields :fields="templateDisplayFields" :form-payload="form.formPayload" />
+      <template #before="{ form, fields }">
+        <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
         <el-row :gutter="24">
           <el-col :span="12">
             <el-form-item label="鍔犵彮鏃堕暱">
-              <el-input :model-value="overtimeHoursDisplay" readonly placeholder="鏍规嵁妯℃澘涓姞鐝椂闂磋嚜鍔ㄨ绠�">
+              <el-input :model-value="overtimeHoursDisplay(form)" readonly placeholder="鏍规嵁妯℃澘涓姞鐝椂闂磋嚜鍔ㄨ绠�">
                 <template #append>灏忔椂</template>
               </el-input>
             </el-form-item>
           </el-col>
         </el-row>
-        <ApprovalTemplateFormSection
-          :active-template="form.templateSnapshot"
-          :fields="form.formFieldDefs"
-          :form-payload="form.formPayload"
-          v-model:flow-nodes="form.flowNodes"
-          v-model:attachments="form.storageBlobDTOs"
-          :template-attachments="form.templateAttachments"
-          :user-options="flowUserOptions"
-          flow-attachments-only
-          hide-template-name
-          :allow-change-template="false"
-        />
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
-          <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
-        </div>
       </template>
-    </el-dialog>
+    </ApprovalInstanceSubmitDialog>
 
     <ApprovalTemplateBindDialog
       v-model:visible="templateBindVisible"
@@ -104,167 +74,52 @@
       @closed="onTemplateBindClosed"
     />
 
-    <!-- 璇︽儏 -->
-    <el-dialog v-model="detailDialog.visible" title="鍔犵彮鐢宠璇︽儏" width="720px" append-to-body>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="鐢宠浜虹紪鍙�">{{ detailRow.applicantNo || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鐢宠浜�">{{ detailRow.applicantName }}</el-descriptions-item>
-        <el-descriptions-item label="鍔犵彮绫诲瀷">{{ overtimeTypeLabel(detailRow.overtimeType) }}</el-descriptions-item>
-        <el-descriptions-item label="鍔犵彮鏃ユ湡">{{ detailRow.overtimeDate || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鍔犵彮寮�濮嬫棩鏈�">{{ detailRow.overtimeStartTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鍔犵彮缁撴潫鏃ユ湡">{{ detailRow.overtimeEndTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="鍔犵彮鏃堕暱">{{ formatHours(detailRow.overtimeHours) }}</el-descriptions-item>
-        <el-descriptions-item label="鍔犵彮浜嬬敱">{{ detailRow.overtimeReason }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒娴佺▼">
-          <template v-if="detailFlowSteps(detailRow).length">
-            <div class="detail-flow-chain">
-              <template v-for="(step, i) in detailFlowSteps(detailRow)" :key="i">
-                <span class="detail-flow-step">{{ step }}</span>
-                <span v-if="i < detailFlowSteps(detailRow).length - 1" class="detail-flow-sep">鈫�</span>
-              </template>
-            </div>
-          </template>
-          <span v-else>鈥�</span>
-        </el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒缁撴灉">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
-        <el-descriptions-item label="鍒涘缓鏃堕棿">{{ detailRow.createTime || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="闄勪欢">
-          <template v-if="rowAttachmentList(detailRow).length">
-            <el-tag v-for="(f, i) in rowAttachmentList(detailRow)" :key="i" class="mr6 mb6" type="info">
-              {{ f.name }}
-            </el-tag>
-          </template>
-          <span v-else>鏃�</span>
-        </el-descriptions-item>
-      </el-descriptions>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 闄勪欢鍒楄〃 -->
-    <el-dialog v-model="filesDialog.visible" title="闄勪欢" width="520px" append-to-body>
-      <el-table v-if="rowAttachmentList(filesDialog.row).length" :data="rowAttachmentList(filesDialog.row)" border>
-        <el-table-column type="index" label="搴忓彿" width="60" align="center" />
-        <el-table-column prop="name" label="鏂囦欢鍚�" min-width="200" show-overflow-tooltip />
-        <el-table-column label="鎿嶄綔" width="100" align="center">
-          <template #default="{ row }">
-            <el-button link type="primary" @click="mockDownload(row)">涓嬭浇</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-empty v-else description="鏆傛棤闄勪欢" />
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="filesDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceDetailDialog
+      v-model="detailDialog.visible"
+      title="鍔犵彮鐢宠璇︽儏"
+      :row="detailRow"
+      @edit="openEditFromDetail"
+    />
   </div>
 </template>
 
 <script setup>
 import { Search } from "@element-plus/icons-vue";
 import dayjs from "dayjs";
-import { userListNoPageByTenantId } from "@/api/system/user.js";
-import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
-import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
-import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
+import { ElMessage } from "element-plus";
+import { getCurrentInstance, onMounted, reactive, ref } from "vue";
 import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
+import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
+import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
+import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
+import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
 import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
-import {
-  applyBindingToForm,
-  attachmentDisplayName,
-  buildFormPayloadRules,
-  validateTemplateBinding,
-} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
 import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
-import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
 
-/** 鍔犵彮绫诲瀷锛坴alue 涓庡悗绔榻愬崰浣嶏級 */
 const OVERTIME_TYPE_OPTIONS = [
   { label: "宸ヤ綔鏃ュ姞鐝�", value: "weekday" },
   { label: "浼戞伅鏃ュ姞鐝�", value: "weekend" },
   { label: "娉曞畾鑺傚亣鏃ュ姞鐝�", value: "holiday" },
 ];
 
-function overtimeTypeLabel(v) {
-  const hit = OVERTIME_TYPE_OPTIONS.find((x) => x.value === v);
-  return hit?.label || "鈥�";
-}
-
-const createEmptyForm = () => ({
-  id: undefined,
-  applicantId: "",
-  applicantNo: "",
-  applicantName: "",
-  overtimeType: "",
-  overtimeDate: "",
-  overtimeStartTime: "",
-  overtimeEndTime: "",
-  overtimeHours: null,
-  overtimeReason: "",
-  hasTemplateBinding: false,
-  templateId: "",
-  templateName: "",
-  templateSnapshot: null,
-  formFieldDefs: [],
-  formPayload: {},
-  flowNodes: [],
-  templateAttachments: [],
-  storageBlobDTOs: [],
-});
-
-const { proxy } = getCurrentInstance();
-
-function unwrapArray(payload) {
-  if (Array.isArray(payload)) return payload;
-  if (payload && Array.isArray(payload.data)) return payload.data;
-  if (payload && Array.isArray(payload.rows)) return payload.rows;
-  return [];
-}
-
-function userById(id) {
-  if (id == null || id === "") return undefined;
-  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
-}
-
-function applicantNoFromUser(u) {
-  if (!u) return "";
-  return (
-    u.userName ??
-    u.userCode ??
-    u.jobNumber ??
-    u.workNo ??
-    (u.userId != null ? String(u.userId) : "")
-  );
-}
-
-function isOvertimeHoursField(field) {
+function isOvertimeDurationField(field) {
   const label = String(field?.label || "");
   return label.includes("鍔犵彮鏃堕暱") || field?.key === "overtimeHours";
+}
+
+function displayTemplateFields(fields = []) {
+  return (fields || []).filter((f) => !isOvertimeDurationField(f));
 }
 
 function findOvertimeTimeTemplateField(fields = []) {
   return (
     fields.find((f) => f?.type === "datetimerange" && String(f?.label || "").includes("鍔犵彮鏃堕棿")) ||
-    fields.find((f) => f?.type === "datetimerange" && f?.key === "dateRange") ||
     fields.find((f) => f?.type === "datetimerange") ||
     null
   );
 }
 
-function findApplicantTemplateField(fields = []) {
-  return (
-    fields.find((f) => String(f?.label || "").includes("鐢宠浜�")) ||
-    fields.find((f) => f?.type === "select" && f?.optionSource === SELECT_OPTION_SOURCE.USER) ||
-    null
-  );
-}
-
-/** 浠庢ā鏉垮~鎶ラ」瑙f瀽鍔犵彮璧锋鏃堕棿 */
 function resolveOvertimeTimeRange(payload, overtimeTimeField) {
   if (!overtimeTimeField?.key) return { start: "", end: "" };
   const val = payload?.[overtimeTimeField.key];
@@ -272,296 +127,94 @@
   return { start: val[0] || "", end: val[1] || "" };
 }
 
-/** 鎸夎捣姝㈡椂闂磋绠楀姞鐝椂闀匡紙灏忔椂锛屼繚鐣欎袱浣嶅皬鏁帮級 */
 function computeOvertimeHours(startStr, endStr) {
   if (!startStr || !endStr) return null;
   const t0 = dayjs(startStr);
   const t1 = dayjs(endStr);
   if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
-  const hours = t1.diff(t0, "millisecond") / (60 * 60 * 1000);
-  return Math.round(hours * 100) / 100;
+  return Math.round((t1.diff(t0, "millisecond") / 3600000) * 100) / 100;
 }
 
-function formatHours(v) {
-  if (v == null || v === "") return "鈥�";
-  return `${v} 灏忔椂`;
+function overtimeHoursDisplay(form) {
+  const field = findOvertimeTimeTemplateField(form.formFieldDefs);
+  const { start, end } = resolveOvertimeTimeRange(form.formPayload, field);
+  const h = computeOvertimeHours(start, end);
+  return h == null ? "" : String(h);
 }
 
-function mapStorageBlobsToAttachmentList(blobs) {
-  return (blobs || []).map((f) => ({
-    name: attachmentDisplayName(f),
-    url: f.url || f.downloadURL || f.previewURL || f.previewUrl,
-  }));
-}
-
-function rowAttachmentList(row) {
-  if (!row) return [];
-  if (row.attachmentList?.length) return row.attachmentList;
-  return mapStorageBlobsToAttachmentList(row.storageBlobDTOs);
-}
-
-function approvalResultLabel(v) {
-  if (v === "approved") return "宸查�氳繃";
-  if (v === "rejected") return "宸查┏鍥�";
-  if (v === "cancelled") return "宸叉挙閿�";
-  return "寰呭鎵�";
-}
-
-function sortedApprovalNodes(row) {
-  const list = row?.approvalFlowNodes;
-  if (!Array.isArray(list) || !list.length) return [];
-  return [...list].sort((a, b) => (a.sortOrder ?? a.nodeOrder ?? 0) - (b.sortOrder ?? b.nodeOrder ?? 0));
-}
-
-function approvalNodeLabel(n) {
-  const name = (n.approverName || "").trim();
-  return name || "鏈�夋嫨瀹℃壒浜�";
-}
-
-/** 璇︽儏瀹℃壒娴佺▼锛氫紭鍏堟ā鏉� flowNodes锛屽吋瀹规棫鐗� approvalFlowNodes */
-function detailFlowSteps(row) {
-  const nodes = row?.flowNodes;
-  if (Array.isArray(nodes) && nodes.length) {
-    return [...nodes]
-      .sort((a, b) => (a.nodeOrder ?? 0) - (b.nodeOrder ?? 0))
-      .map((n, i) => {
-        const names = (n.approvers || [])
-          .map((a) => (a.approverName || "").trim())
-          .filter(Boolean)
-          .join("銆�");
-        return `${i + 1}. ${names || "鏈�夋嫨瀹℃壒浜�"}`;
-      });
-  }
-  return sortedApprovalNodes(row).map((n, i) => `${i + 1}. ${approvalNodeLabel(n)}`);
-}
-
-function syncApplicantFromUser(uid) {
-  const u = userById(uid);
-  if (u) {
-    form.applicantId = uid != null && uid !== "" ? uid : "";
-    form.applicantName = u.nickName || u.userName || "";
-    form.applicantNo = applicantNoFromUser(u);
-  } else {
-    form.applicantId = "";
-    form.applicantName = "";
-    form.applicantNo = "";
-  }
-}
-
-const allUsersCache = ref([]);
-
-async function loadUserPool() {
-  try {
-    const res = await userListNoPageByTenantId();
-    allUsersCache.value = unwrapArray(res);
-  } catch {
-    allUsersCache.value = [];
-  }
-}
-
-const allRows = ref([]);
+const { proxy } = getCurrentInstance();
 
 const searchForm = reactive({
   applicantKeyword: "",
   overtimeType: "",
 });
 
-const tableLoading = ref(false);
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
+const mod = useApprovalInstanceModule({
+  moduleKey: APPROVAL_MODULE_KEYS.OVERTIME,
+  beforeSave: validateOvertimeBeforeSave,
 });
 
-const filteredList = computed(() => {
-  let list = [...allRows.value];
-  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);
-    });
-  }
-  if (searchForm.overtimeType) {
-    list = list.filter((r) => r.overtimeType === searchForm.overtimeType);
-  }
-  return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
-});
+const {
+  tableData,
+  tableLoading,
+  page,
+  detailDialog,
+  detailRow,
+  submitDialog,
+  submitForm,
+  submitFormRef,
+  submitSaving,
+  isSubmitEdit,
+  activeTemplate,
+  submitFormFields,
+  submitFormRules,
+  submitDialogTitle,
+  templateBindVisible,
+  handleQuery,
+  initModuleList,
+  pagination,
+  openAddWithTemplate,
+  onTemplateBound,
+  onTemplateBindClosed,
+  openEditFromDetail,
+  submitInstanceForm,
+  buildTableActions,
+} = mod;
 
-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 list = filteredList.value;
-  const start = (page.current - 1) * page.size;
-  return list.slice(start, start + page.size);
-});
-
-const tableColumn = ref([
-  { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 120 },
-  { label: "鐢宠浜�", prop: "applicantName", minWidth: 100 },
-  { label: "鍔犵彮鏃ユ湡", prop: "overtimeDate", width: 120 },
-  { label: "鍔犵彮寮�濮嬫棩鏈�", prop: "overtimeStartTime", width: 170 },
-  { label: "鍔犵彮缁撴潫鏃ユ湡", prop: "overtimeEndTime", width: 170 },
-  {
-    label: "鍔犵彮鏃堕暱",
-    prop: "overtimeHours",
-    width: 120,
-    formatData: (v) => (v == null || v === "" ? "鈥�" : `${v} 灏忔椂`),
-  },
-  {
-    label: "瀹℃壒缁撴灉",
-    prop: "approvalResult",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => approvalResultLabel(v),
-    formatType: (v) => {
-      if (v === "approved") return "success";
-      if (v === "rejected") return "danger";
-      if (v === "cancelled") return "info";
-      return "warning";
-    },
-  },
-  { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    fixed: "right",
-    width: 220,
-    operation: [
-      {
-        name: "缂栬緫",
-        type: "text",
-        clickFun: (row) => openFormDialog("edit", row),
-      },
-      {
-        name: "鏌ョ湅璇︽儏",
-        type: "text",
-        clickFun: (row) => openDetail(row),
-      },
-      {
-        name: "闄勪欢",
-        type: "text",
-        clickFun: (row) => openFiles(row),
-      },
-    ],
-  },
-]);
-
-const formDialog = reactive({
-  visible: false,
-  title: "",
-  mode: "add",
-});
-const formRef = ref();
-const form = reactive(createEmptyForm());
-const templateBindVisible = ref(false);
-const pendingTemplateBinding = ref(null);
 const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
 
-const overtimeTimeTemplateField = computed(() => findOvertimeTimeTemplateField(form.formFieldDefs));
-const applicantTemplateField = computed(() => findApplicantTemplateField(form.formFieldDefs));
-
-const templateDisplayFields = computed(() =>
-  (form.formFieldDefs || []).filter((f) => !isOvertimeHoursField(f))
-);
-
-const overtimeHoursDisplay = computed(() => {
-  const { start, end } = resolveOvertimeTimeRange(form.formPayload, overtimeTimeTemplateField.value);
-  const h = computeOvertimeHours(start, end);
-  return h == null ? "" : String(h);
-});
-
-const formRules = computed(() => buildFormPayloadRules(templateDisplayFields.value));
-
-watch(
-  () => {
-    const key = applicantTemplateField.value?.key;
-    return key ? form.formPayload[key] : undefined;
-  },
-  async (uid) => {
-    if (!applicantTemplateField.value) return;
-    if (!allUsersCache.value.length) {
-      await loadUserPool();
-    }
-    syncApplicantFromUser(uid);
+function validateOvertimeBeforeSave() {
+  const field = findOvertimeTimeTemplateField(submitForm.formFieldDefs);
+  const { start, end } = resolveOvertimeTimeRange(submitForm.formPayload, field);
+  if (computeOvertimeHours(start, end) == null) {
+    ElMessage.warning("璇锋鏌ユā鏉夸腑鐨勫姞鐝椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�");
+    throw new Error("invalid overtime time");
   }
-);
+}
 
-watch(
-  () => {
-    const key = overtimeTimeTemplateField.value?.key;
-    return key ? form.formPayload[key] : undefined;
-  },
-  () => {
-    const { start, end } = resolveOvertimeTimeRange(form.formPayload, overtimeTimeTemplateField.value);
-    form.overtimeStartTime = start;
-    form.overtimeEndTime = end;
-    form.overtimeHours = computeOvertimeHours(start, end);
-    if (start) {
-      form.overtimeDate = dayjs(start).format("YYYY-MM-DD");
-    }
-  },
-  { deep: true }
-);
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
 
-const detailDialog = reactive({ visible: false });
-const detailRow = ref({});
-
-const filesDialog = reactive({ visible: false, row: null });
-const importInputRef = ref(null);
-
-function handleQuery() {
-  page.current = 1;
-  tableLoading.value = true;
-  setTimeout(() => {
-    tableLoading.value = false;
-  }, 150);
+function onSearch() {
+  handleQuery(searchForm);
 }
 
 function resetSearch() {
   searchForm.applicantKeyword = "";
   searchForm.overtimeType = "";
-  handleQuery();
+  onSearch();
 }
 
-function pagination(obj) {
-  page.current = obj.page;
-  page.size = obj.limit;
+function onPagination(obj) {
+  pagination(obj, searchForm);
 }
 
-function openDetail(row) {
-  detailRow.value = { ...row };
-  detailDialog.visible = true;
-}
-
-function openFiles(row) {
-  filesDialog.row = row;
-  filesDialog.visible = true;
-}
-
-function mockDownload(row) {
-  const url = row.url || row.downloadURL || row.previewURL || row.previewUrl;
-  if (url) {
-    window.open(url, "_blank");
-    return;
-  }
-  proxy?.$modal?.msgWarning?.("鏆傛棤涓嬭浇鍦板潃");
+async function onSubmit() {
+  const ok = await submitInstanceForm({ skipValidate: true });
+  if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
 }
 
 function handleExport() {
-  const data = filteredList.value;
+  const data = tableData.value;
   const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
   const url = URL.createObjectURL(blob);
   const a = document.createElement("a");
@@ -569,274 +222,12 @@
   a.download = `鍔犵彮鐢宠瀵煎嚭_${dayjs().format("YYYYMMDDHHmmss")}.json`;
   a.click();
   URL.revokeObjectURL(url);
-  proxy?.$modal?.msgSuccess?.(`宸插鍑� ${data.length} 鏉★紙褰撳墠绛涢�夌粨鏋滐紝JSON锛塦);
+  proxy?.$modal?.msgSuccess?.(`宸插鍑� ${data.length} 鏉★紙褰撳墠椤靛垪琛ㄦ暟鎹級`);
 }
 
-function handleImportClick() {
-  importInputRef.value?.click?.();
-}
-
-function normalizeImportedRow(raw, idx) {
-  const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`;
-  const hours =
-    raw.overtimeHours != null && raw.overtimeHours !== ""
-      ? Number(raw.overtimeHours)
-      : computeOvertimeHours(raw.overtimeStartTime, raw.overtimeEndTime);
-  return {
-    id,
-    applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`,
-    applicantNo: raw.applicantNo ?? "",
-    applicantName: raw.applicantName ?? "鏈煡",
-    overtimeType: raw.overtimeType || "weekday",
-    overtimeDate: raw.overtimeDate ?? "",
-    overtimeStartTime: raw.overtimeStartTime ?? "",
-    overtimeEndTime: raw.overtimeEndTime ?? "",
-    overtimeHours: hours == null || Number.isNaN(hours) ? 0 : Math.round(hours * 100) / 100,
-    overtimeReason: raw.overtimeReason ?? "",
-    hasTemplateBinding: false,
-    approvalFlowNodes: Array.isArray(raw.approvalFlowNodes) && raw.approvalFlowNodes.length
-      ? raw.approvalFlowNodes.map((n) => ({ ...n }))
-      : [],
-    approvalResult: raw.approvalResult && ["pending", "approved", "rejected", "cancelled"].includes(raw.approvalResult)
-      ? raw.approvalResult
-      : "pending",
-    attachmentList: Array.isArray(raw.attachmentList) ? raw.attachmentList : [],
-    createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"),
-  };
-}
-
-function onImportFile(e) {
-  const input = e.target;
-  const file = input.files?.[0];
-  input.value = "";
-  if (!file) return;
-  const reader = new FileReader();
-  reader.onload = () => {
-    try {
-      const text = String(reader.result || "");
-      const parsed = JSON.parse(text);
-      const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data;
-      if (!Array.isArray(arr) || !arr.length) {
-        proxy?.$modal?.msgWarning?.("瀵煎叆鏂囦欢鏍煎紡涓嶆纭紝闇�涓哄姞鐝敵璇峰璞℃暟缁� JSON");
-        return;
-      }
-      let n = 0;
-      for (let i = 0; i < arr.length; i++) {
-        allRows.value.unshift(normalizeImportedRow(arr[i], i));
-        n++;
-      }
-      proxy?.$modal?.msgSuccess?.(`鎴愬姛瀵煎叆 ${n} 鏉★紙鏈湴鍚堝苟锛塦);
-      handleQuery();
-    } catch {
-      proxy?.$modal?.msgError?.("瑙f瀽澶辫触锛岃浣跨敤瀵煎嚭鏂囦欢鎴栫害瀹� JSON 缁撴瀯");
-    }
-  };
-  reader.readAsText(file, "utf-8");
-}
-
-function openAddWithTemplate() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function onTemplateBound(binding) {
-  pendingTemplateBinding.value = binding;
-}
-
-async function onTemplateBindClosed() {
-  const binding = pendingTemplateBinding.value;
-  if (!binding) return;
-  pendingTemplateBinding.value = null;
-  await openFormWithBinding(binding);
-}
-
-async function openFormWithBinding(binding) {
-  Object.assign(form, createEmptyForm());
-  applyBindingToForm(form, binding);
-  form.hasTemplateBinding = true;
-  formDialog.mode = "add";
-  formDialog.title = "鏂板鍔犵彮鐢宠";
-  await Promise.all([loadUserPool(), loadFlowUsers()]);
-  const applicantKey = applicantTemplateField.value?.key;
-  if (applicantKey && form.formPayload[applicantKey]) {
-    syncApplicantFromUser(form.formPayload[applicantKey]);
-  }
-  const { start, end } = resolveOvertimeTimeRange(form.formPayload, overtimeTimeTemplateField.value);
-  form.overtimeStartTime = start;
-  form.overtimeEndTime = end;
-  form.overtimeHours = computeOvertimeHours(start, end);
-  if (start) form.overtimeDate = dayjs(start).format("YYYY-MM-DD");
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function reopenTemplateBind() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-async function openFormDialog(mode, row) {
-  if (mode === "edit" && row && !row.hasTemplateBinding) {
-    proxy?.$modal?.msgWarning?.("璇ヨ褰曚负鏃х増鏁版嵁锛岃閲嶆柊閫氳繃妯℃澘鍙戣捣鐢宠");
-    return;
-  }
-  formDialog.mode = mode;
-  formDialog.title = "缂栬緫鍔犵彮鐢宠";
-  Object.assign(form, createEmptyForm());
-  if (mode === "edit" && row) {
-    Object.assign(form, {
-      id: row.id,
-      applicantId: row.applicantId,
-      applicantNo: row.applicantNo,
-      applicantName: row.applicantName,
-      overtimeType: row.overtimeType,
-      overtimeDate: row.overtimeDate,
-      overtimeStartTime: row.overtimeStartTime,
-      overtimeEndTime: row.overtimeEndTime,
-      overtimeHours: row.overtimeHours,
-      overtimeReason: row.overtimeReason,
-      hasTemplateBinding: true,
-      templateId: row.templateId,
-      templateName: row.templateName,
-      templateSnapshot: row.templateSnapshot,
-      formFieldDefs: row.formFieldDefs || [],
-      formPayload: JSON.parse(JSON.stringify(row.formPayload || {})),
-      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])),
-      templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])),
-      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
-    });
-    await loadUserPool();
-    const applicantKey = applicantTemplateField.value?.key;
-    if (applicantKey) {
-      syncApplicantFromUser(form.formPayload[applicantKey]);
-    }
-    loadFlowUsers();
-  }
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function onFormClosed() {
-  formRef.value?.resetFields?.();
-}
-
-/** 浠庢ā鏉垮~鎶ラ」鍚屾鍒楄〃灞曠ず瀛楁 */
-function syncOvertimeFieldsFromPayload() {
-  const defs = form.formFieldDefs || [];
-  const payload = form.formPayload || {};
-  const overtimeTimeField = findOvertimeTimeTemplateField(defs);
-
-  for (const f of defs) {
-    const label = String(f.label || "");
-    const val = payload[f.key];
-
-    if (label.includes("鐢宠浜�") && !label.includes("鏃ユ湡") && !label.includes("鏃堕棿")) {
-      if (val != null && val !== "") {
-        form.applicantId = val;
-        const u = userById(val);
-        if (u) {
-          form.applicantName = u.nickName || u.userName || "";
-          form.applicantNo = applicantNoFromUser(u);
-        }
-      }
-    }
-    if ((label.includes("鍔犵彮绫诲瀷") || f.key === "overtimeType") && f.type === "select") {
-      form.overtimeType = val != null && val !== "" ? val : "";
-    }
-    if (label.includes("鍔犵彮鏃ユ湡") && f.type === "date") {
-      form.overtimeDate = val || "";
-    }
-    if (label.includes("浜嬬敱") || f.key === "summary" || label.includes("鍔犵彮浜嬬敱")) {
-      form.overtimeReason = val != null ? String(val) : "";
-    }
-  }
-
-  const { start, end } = resolveOvertimeTimeRange(payload, overtimeTimeField);
-  form.overtimeStartTime = start;
-  form.overtimeEndTime = end;
-  form.overtimeHours = computeOvertimeHours(start, end);
-  if (!form.overtimeDate && start) {
-    form.overtimeDate = dayjs(start).format("YYYY-MM-DD");
-  }
-}
-
-async function submitForm() {
-  try {
-    await formRef.value?.validate?.();
-  } catch {
-    return;
-  }
-  const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes });
-  if (!flowCheck.ok) {
-    proxy?.$modal?.msgWarning?.(flowCheck.message || "璇峰畬鍠勫鎵规祦绋�");
-    return;
-  }
-  form.flowNodes = flowCheck.nodes;
-
-  const applicantKey = applicantTemplateField.value?.key;
-  if (applicantKey) {
-    syncApplicantFromUser(form.formPayload[applicantKey]);
-  }
-  syncOvertimeFieldsFromPayload();
-
-  if (form.overtimeHours == null) {
-    proxy?.$modal?.msgWarning?.("璇锋鏌ユā鏉夸腑鐨勫姞鐝椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�");
-    return;
-  }
-
-  const attachmentList = mapStorageBlobsToAttachmentList(form.storageBlobDTOs);
-  const payload = {
-    applicantId: form.applicantId,
-    applicantNo: form.applicantNo,
-    applicantName: form.applicantName,
-    overtimeType: form.overtimeType,
-    overtimeDate: form.overtimeDate,
-    overtimeStartTime: form.overtimeStartTime,
-    overtimeEndTime: form.overtimeEndTime,
-    overtimeHours: form.overtimeHours,
-    overtimeReason: form.overtimeReason,
-    hasTemplateBinding: true,
-    templateId: form.templateId,
-    templateName: form.templateName,
-    templateSnapshot: form.templateSnapshot,
-    formFieldDefs: form.formFieldDefs,
-    formPayload: JSON.parse(JSON.stringify(form.formPayload || {})),
-    flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])),
-    templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])),
-    storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])),
-    attachmentList,
-  };
-  if (formDialog.mode === "add") {
-    const id = `local_${Date.now()}`;
-    allRows.value.unshift({
-      id,
-      ...payload,
-      approvalResult: "pending",
-      createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
-    });
-    proxy?.$modal?.msgSuccess?.("鏂板鎴愬姛");
-  } else {
-    const idx = allRows.value.findIndex((r) => r.id === form.id);
-    if (idx !== -1) {
-      const prev = allRows.value[idx];
-      allRows.value[idx] = {
-        ...prev,
-        id: form.id,
-        ...payload,
-        approvalResult: prev.approvalResult ?? "pending",
-        createTime: prev.createTime ?? dayjs().format("YYYY-MM-DD HH:mm:ss"),
-      };
-    }
-    proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
-  }
-  formDialog.visible = false;
-  handleQuery();
-}
-
-onMounted(() => {
+onMounted(async () => {
   loadFlowUsers();
+  await initModuleList(searchForm);
 });
 </script>
 
@@ -854,59 +245,10 @@
 .search_actions {
   display: flex;
   flex-wrap: wrap;
-  align-items: center;
   gap: 8px;
 }
 .search_title {
   font-size: 14px;
   color: var(--el-text-color-regular);
-}
-.sr-only-input {
-  position: absolute;
-  width: 1px;
-  height: 1px;
-  padding: 0;
-  margin: -1px;
-  overflow: hidden;
-  clip: rect(0, 0, 0, 0);
-  white-space: nowrap;
-  border: 0;
-}
-.mr6 {
-  margin-right: 6px;
-}
-.mb6 {
-  margin-bottom: 6px;
-}
-.overtime-apply-form :deep(.el-row) {
-  margin-bottom: 0;
-}
-.overtime-apply-form :deep(.el-form-item) {
-  margin-bottom: 18px;
-}
-.template-name {
-  font-weight: 600;
-  color: var(--el-text-color-primary);
-}
-.ml12 {
-  margin-left: 12px;
-}
-.overtime-apply-form-dialog :deep(.el-dialog__body) {
-  padding-top: 12px;
-}
-.detail-flow-chain {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 6px 8px;
-  line-height: 1.6;
-}
-.detail-flow-step {
-  font-size: 14px;
-  color: var(--el-text-color-primary);
-}
-.detail-flow-sep {
-  color: var(--el-text-color-secondary);
-  font-size: 13px;
 }
 </style>
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js
new file mode 100644
index 0000000..f61d9a9
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js
@@ -0,0 +1,98 @@
+import {
+  createEmptyForm,
+  publishStatusLabel,
+  PUBLISH_STATUS_OPTIONS,
+} from "./enterpriseNewsUtils.js";
+import { normalizeApprovalStatusKey } from "../../ApproveManage/approve-list/approveListConstants.js";
+
+/** formPayload 涓瓨鏀惧畬鏁翠紒涓氭柊闂讳笟鍔℃暟鎹殑閿� */
+export const ENTERPRISE_NEWS_PAYLOAD_KEY = "enterpriseNews";
+
+export function extractEnterpriseNewsFromRow(row) {
+  const payload = row?.formPayload || {};
+  const raw = payload[ENTERPRISE_NEWS_PAYLOAD_KEY];
+  if (raw && typeof raw === "object") {
+    return { ...createEmptyForm(), ...raw };
+  }
+  return {
+    ...createEmptyForm(),
+    title: payload.title || row?.title || "",
+    summary: payload.summary || "",
+    newsType: payload.newsType || "announcement",
+    contentHtml: payload.contentHtml || "",
+  };
+}
+
+/** 鍒楄〃琛屽寮猴細涓昏〃灞曠ず鏂伴椈瀛楁 */
+export function enrichEnterpriseNewsListRow(row) {
+  const news = extractEnterpriseNewsFromRow(row);
+  const publishStatus =
+    news.publishStatus || mapApprovalStatusToPublishStatus(row?.approvalStatus);
+  return {
+    ...row,
+    newsNo: news.newsNo || row.instanceNo || "鈥�",
+    title: news.title || row.title || "鈥�",
+    summary: news.summary,
+    newsType: news.newsType,
+    publisherName: news.publisherName || row.applicantName || "鈥�",
+    publishTime: news.publishTime || row.createTime || "",
+    updateTime: news.updateTime || row.createTime || "",
+    publishStatus,
+    _news: news,
+  };
+}
+
+function mapApprovalStatusToPublishStatus(approvalStatus) {
+  const key = normalizeApprovalStatusKey(approvalStatus);
+  if (key === "approved") return "published";
+  if (key === "pending") return "pending_review";
+  if (key === "rejected") return "draft";
+  return "draft";
+}
+
+/** 浼佷笟鏂伴椈琛ㄥ崟 鈫� 瀹℃壒瀹炰緥 formPayload */
+export function syncNewsFormToSubmitPayload(newsForm, submitForm) {
+  const snapshot = JSON.parse(JSON.stringify(newsForm));
+  submitForm.formPayload = {
+    ...(submitForm.formPayload || {}),
+    [ENTERPRISE_NEWS_PAYLOAD_KEY]: snapshot,
+    title: snapshot.title,
+    summary: snapshot.summary,
+  };
+}
+
+export function buildEnterpriseNewsTableColumns(buildTableActions) {
+  return [
+    { label: "缂栧彿", prop: "newsNo", width: 150 },
+    { label: "鏍囬", prop: "title", minWidth: 180, showOverflowTooltip: true },
+    {
+      label: "鍒嗙被",
+      prop: "newsType",
+      width: 100,
+      dataType: "slot",
+      slot: "newsType",
+    },
+    {
+      label: "鐘舵��",
+      prop: "publishStatus",
+      width: 90,
+      dataType: "tag",
+      formatData: (v) => publishStatusLabel(v),
+      formatType: (v) => {
+        const hit = PUBLISH_STATUS_OPTIONS.find((x) => x.value === v);
+        return hit?.tag || "info";
+      },
+    },
+    { label: "鍙戝竷浜�", prop: "publisherName", width: 110 },
+    { label: "鍙戝竷鏃堕棿", prop: "publishTime", width: 170 },
+    { label: "鏇存柊鏃堕棿", prop: "updateTime", width: 170 },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 220,
+      operation: buildTableActions(),
+    },
+  ];
+}
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
index 8a36ffc..29302a0 100644
--- a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
@@ -1,7 +1,6 @@
-<!--OA妯″潡锛欵nterpriseNews 浼佷笟鏂伴椈-->
+<!--OA妯″潡锛欵nterpriseNews 浼佷笟鏂伴椈锛堝垪琛ㄨ蛋瀹℃壒瀹炰緥锛屾柊澧�/淇敼淇濈暀鍘熻〃鍗� + 妯℃澘瀹℃壒娴佺▼锛�-->
 <template>
   <div class="app-container enterprise-news-page">
-
     <div class="search_form mb20">
       <div class="search_fields">
         <span class="search_title">鍏抽敭璇嶏細</span>
@@ -11,19 +10,24 @@
           placeholder="鏍囬 / 缂栧彿 / 鎽樿"
           clearable
           :prefix-icon="Search"
-          @keyup.enter="handleQuery"
+          @keyup.enter="onSearch"
         />
         <span class="search_title" style="margin-left: 12px">鍒嗙被锛�</span>
         <el-select v-model="searchForm.newsType" placeholder="鍏ㄩ儴" clearable style="width: 140px">
           <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
         </el-select>
-        <span class="search_title" style="margin-left: 12px">鐘舵�侊細</span>
-        <el-select v-model="searchForm.publishStatus" placeholder="鍏ㄩ儴" clearable style="width: 120px">
-          <el-option v-for="opt in PUBLISH_STATUS_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
+        <span class="search_title" style="margin-left: 12px">瀹℃壒鐘舵�侊細</span>
+        <el-select v-model="searchForm.status" placeholder="鍏ㄩ儴" clearable style="width: 120px">
+          <el-option
+            v-for="opt in APPROVAL_STATUS_SEARCH_OPTIONS"
+            :key="opt.value"
+            :label="opt.label"
+            :value="opt.value"
+          />
         </el-select>
-        <span class="search_title" style="margin-left: 12px">鍙戝竷鏃堕棿锛�</span>
+        <span class="search_title" style="margin-left: 12px">鐢宠鏃ユ湡锛�</span>
         <el-date-picker
-          v-model="searchForm.publishTimeRange"
+          v-model="searchForm.createTimeRange"
           type="daterange"
           range-separator="-"
           start-placeholder="寮�濮�"
@@ -33,11 +37,11 @@
           style="width: 260px"
           clearable
         />
-        <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">鎼滅储</el-button>
+        <el-button type="primary" :icon="Search" class="ml10" @click="onSearch">鎼滅储</el-button>
         <el-button :icon="RefreshRight" @click="resetSearch">閲嶇疆</el-button>
       </div>
       <div class="search_actions">
-        <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">鏂板缓鏂伴椈</el-button>
+        <el-button type="primary" :icon="Plus" @click="openAddWithTemplate">鏂板缓鏂伴椈</el-button>
       </div>
     </div>
 
@@ -50,7 +54,7 @@
         :isSelection="false"
         :tableLoading="tableLoading"
         :total="page.total"
-        @pagination="pagination"
+        @pagination="onPagination"
       >
         <template #newsType="{ row }">
           <span class="news-type-tag" :style="{ color: newsTypeColor(row.newsType) }">
@@ -60,34 +64,42 @@
       </PIMTable>
     </div>
 
-    <!-- 鏂板缓 / 缂栬緫 -->
+    <ApprovalTemplateBindDialog
+      v-model:visible="templateBindVisible"
+      :module-key="APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS"
+      skip-form-confirm
+      @confirm="onTemplateBound"
+      @closed="onTemplateBindClosed"
+    />
+
+    <!-- 鏂板缓 / 缂栬緫锛氬師浼佷笟鏂伴椈琛ㄥ崟 + 妯℃澘瀹℃壒娴佺▼ -->
     <el-dialog
-      v-model="formDialog.visible"
-      :title="formDialog.title"
+      v-model="newsFormDialog.visible"
+      :title="newsFormDialog.title"
       width="960px"
       append-to-body
       destroy-on-close
       class="news-form-dialog"
-      @closed="formRef?.resetFields?.()"
+      @closed="onNewsFormClosed"
     >
       <el-form
-        ref="formRef"
-        :model="form"
-        :rules="formRules"
+        ref="newsFormRef"
+        :model="newsForm"
+        :rules="newsFormRules"
         label-width="110px"
-        :disabled="formDialog.readonly"
+        :disabled="newsFormDialog.readonly"
       >
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="鏂伴椈鍒嗙被" prop="newsType">
-              <el-select v-model="form.newsType" placeholder="璇烽�夋嫨" style="width: 100%">
+              <el-select v-model="newsForm.newsType" placeholder="璇烽�夋嫨" style="width: 100%">
                 <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="鎺掔増妯℃澘">
-              <el-select v-model="form.layoutTemplate" style="width: 100%">
+              <el-select v-model="newsForm.layoutTemplate" style="width: 100%">
                 <el-option
                   v-for="opt in LAYOUT_TEMPLATE_OPTIONS"
                   :key="opt.value"
@@ -99,29 +111,29 @@
           </el-col>
         </el-row>
         <el-form-item label="鏍囬" prop="title">
-          <el-input v-model="form.title" placeholder="鏂伴椈鏍囬" maxlength="100" show-word-limit />
+          <el-input v-model="newsForm.title" placeholder="鏂伴椈鏍囬" maxlength="100" show-word-limit />
         </el-form-item>
         <el-form-item label="鎽樿">
-          <el-input v-model="form.summary" type="textarea" :rows="2" maxlength="300" show-word-limit />
+          <el-input v-model="newsForm.summary" type="textarea" :rows="2" maxlength="300" show-word-limit />
         </el-form-item>
         <el-form-item label="姝f枃" prop="contentHtml">
-          <Editor v-model="form.contentHtml" :min-height="280" />
+          <Editor v-model="newsForm.contentHtml" :min-height="280" />
         </el-form-item>
         <el-form-item label="闄勪欢">
-          <FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="涓婁紶 PDF / 鏂囨。" />
+          <FileUpload v-model:file-list="newsForm.attachmentList" :limit="10" button-text="涓婁紶 PDF / 鏂囨。" />
         </el-form-item>
-        <el-form-item v-if="form.layoutTemplate === 'gallery'" label="鍥鹃泦/瑙嗛">
+        <el-form-item v-if="newsForm.layoutTemplate === 'gallery'" label="鍥鹃泦/瑙嗛">
           <el-input
             v-model="galleryInput"
             placeholder="杈撳叆璧勬簮鍚嶇О鍚庡洖杞︽坊鍔狅紙婕旂ず锛�"
             @keyup.enter="addGalleryItem"
           />
           <el-tag
-            v-for="(m, i) in form.mediaList"
+            v-for="(m, i) in newsForm.mediaList"
             :key="i"
             closable
             class="media-tag"
-            @close="form.mediaList.splice(i, 1)"
+            @close="newsForm.mediaList.splice(i, 1)"
           >
             {{ m.name }}
           </el-tag>
@@ -131,276 +143,326 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="缂栬緫瑙掕壊">
-              <el-select v-model="form.editorRole" style="width: 100%">
+              <el-select v-model="newsForm.editorRole" style="width: 100%">
                 <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="瀹℃牳瑙掕壊">
-              <el-select v-model="form.reviewerRole" style="width: 100%">
+              <el-select v-model="newsForm.reviewerRole" style="width: 100%">
                 <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
               </el-select>
             </el-form-item>
           </el-col>
         </el-row>
         <el-form-item label="闃呰鑼冨洿" prop="readScope">
-          <el-radio-group v-model="form.readScope">
+          <el-radio-group v-model="newsForm.readScope">
             <el-radio v-for="opt in READ_SCOPE_OPTIONS" :key="opt.value" :value="opt.value">
               {{ opt.label }}
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item v-if="form.readScope === 'department'" label="鍙閮ㄩ棬">
-          <el-select v-model="form.targetDeptIds" multiple placeholder="閫夋嫨閮ㄩ棬" style="width: 100%">
+        <el-form-item v-if="newsForm.readScope === 'department'" label="鍙閮ㄩ棬">
+          <el-select v-model="newsForm.targetDeptIds" multiple placeholder="閫夋嫨閮ㄩ棬" style="width: 100%">
             <el-option v-for="d in DEPT_OPTIONS" :key="d.value" :label="d.label" :value="d.value" />
           </el-select>
         </el-form-item>
         <el-form-item label="鏀跨瓥绫诲繀璇�">
-          <el-switch v-model="form.requireReadConfirm" active-text="闇�闃呰纭锛堜究浜庣粺璁℃湭璇伙級" />
+          <el-switch v-model="newsForm.requireReadConfirm" active-text="闇�闃呰纭锛堜究浜庣粺璁℃湭璇伙級" />
         </el-form-item>
         <el-form-item label="鍙戝竷浜�">
-          <el-input v-model="form.publisherName" placeholder="濡傦細浜哄姏璧勬簮閮�" maxlength="50" />
+          <el-input v-model="newsForm.publisherName" placeholder="濡傦細浜哄姏璧勬簮閮�" maxlength="50" />
         </el-form-item>
+
+        <template v-if="activeTemplate">
+          <el-divider content-position="left">瀹℃壒娴佺▼</el-divider>
+          <el-form-item label="瀹℃壒妯℃澘">
+            <span class="template-name">{{ activeTemplate.label || submitForm.templateName }}</span>
+          </el-form-item>
+          <el-form-item label="瀹℃壒娴佺▼" required>
+            <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
+            <p class="section-tip">娴佺▼涓庡鎵逛汉鐢辨ā鏉块缃紝鍙寜闇�寰皟鑺傜偣瀹℃壒浜恒��</p>
+          </el-form-item>
+        </template>
+        <el-alert v-else type="warning" show-icon :closable="false" title="璇峰厛閫氳繃銆屾柊寤烘柊闂汇�嶉�夋嫨瀹℃壒妯℃澘" />
       </el-form>
-      <template v-if="!formDialog.readonly" #footer>
-        <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
-        <el-button @click="onSave('save')">瀛樿崏绋�</el-button>
-        <el-button type="warning" @click="onSave('submit_review')">鎻愪氦瀹℃牳</el-button>
-        <el-button type="primary" @click="onSave('publish')">鐩存帴鍙戝竷</el-button>
+      <template v-if="!newsFormDialog.readonly" #footer>
+        <el-button @click="newsFormDialog.visible = false">鍙� 娑�</el-button>
+        <el-button :loading="submitSaving" @click="onNewsSave('draft')">瀛樿崏绋�</el-button>
+        <el-button type="warning" :loading="submitSaving" @click="onNewsSave('submit_review')">
+          鎻愪氦瀹℃牳
+        </el-button>
+        <el-button type="primary" :loading="submitSaving" @click="onNewsSave('submit_review')">
+          淇� 瀛�
+        </el-button>
       </template>
     </el-dialog>
 
     <!-- 璇︽儏 -->
     <el-dialog v-model="detailDialog.visible" title="鏂伴椈璇︽儏" width="880px" append-to-body destroy-on-close>
-      <NewsDetailPanel
-        :row="detailRow"
-        @like="onDetailLike"
-        @comment="onDetailComment"
-      />
+      <NewsDetailPanel :row="detailNewsRow" @like="onDetailLike" @comment="onDetailComment" />
+      <el-divider content-position="left">瀹℃壒淇℃伅</el-divider>
+      <ApproveDetailPanel :row="detailRow" />
       <template #footer>
         <el-button
-          v-if="detailRow.publishStatus === 'published' && getUnreadEmployees(detailRow).length"
-          type="warning"
-          @click="openUnreadFromDetail"
+          v-if="canEditBusinessInstanceRow(detailRow)"
+          type="primary"
+          @click="openNewsEditFromDetail"
         >
-          鏈鎻愰啋
+          淇敼
         </el-button>
-        <el-button @click="openVersionFromDetail">鐗堟湰鐣欒瘉</el-button>
         <el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
       </template>
-    </el-dialog>
-
-    <!-- 鏈鎻愰啋 -->
-    <el-dialog
-      v-model="unreadDialog.visible"
-      :title="`鏈槄璇诲憳宸� 路 ${unreadDialog.row?.title || ''}`"
-      width="720px"
-      append-to-body
-      destroy-on-close
-    >
-      <el-alert type="warning" show-icon :closable="false" class="mb12">
-        鏀跨瓥浼犺揪鍦烘櫙锛氬彂甯冩柊鑰冨嫟鍒跺害绛夊繀璇讳俊鎭悗锛屽彲鍕鹃�夋湭璇诲憳宸ョ敱 HR 瀹氬悜鎻愰啋锛堟紨绀烘暟鎹紝鍚庢湡瀵规帴娑堟伅涓績锛夈��
-      </el-alert>
-      <div class="unread-toolbar mb12">
-        <el-button size="small" @click="selectAllUnread">鍏ㄩ�夋湭璇�</el-button>
-        <span class="unread-stat">鍏� {{ unreadList.length }} 浜烘湭璇�</span>
-      </div>
-      <el-table
-        :data="unreadList"
-        border
-        size="small"
-        max-height="360"
-        @selection-change="onUnreadSelectionChange"
-      >
-        <el-table-column type="selection" width="48" />
-        <el-table-column prop="employeeNo" label="宸ュ彿" width="100" />
-        <el-table-column prop="name" label="濮撳悕" width="90" />
-        <el-table-column prop="deptName" label="閮ㄩ棬" min-width="120" />
-      </el-table>
-      <el-divider v-if="unreadDialog.row?.remindLogs?.length" content-position="left">鎻愰啋璁板綍</el-divider>
-      <el-timeline v-if="unreadDialog.row?.remindLogs?.length">
-        <el-timeline-item
-          v-for="(log, i) in unreadDialog.row.remindLogs"
-          :key="i"
-          :timestamp="log.time"
-        >
-          {{ log.operator }} 宸插悜 {{ log.count }} 浜哄彂閫侀槄璇绘彁閱�
-        </el-timeline-item>
-      </el-timeline>
-      <template #footer>
-        <el-button type="primary" @click="onSendRemind">鍙戦�佸畾鍚戞彁閱�</el-button>
-        <el-button @click="unreadDialog.visible = false">鍏� 闂�</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 鐗堟湰鐣欒瘉 -->
-    <el-dialog
-      v-model="versionDialog.visible"
-      :title="`鍘嗗彶鐗堟湰鐣欒瘉 路 ${versionDialog.row?.title || ''}`"
-      width="800px"
-      append-to-body
-      destroy-on-close
-    >
-      <el-alert type="info" show-icon :closable="false" class="mb12">
-        浜夎鍙戠敓鏃跺彲鏌ラ槄鍘嗗彶鐗堟湰锛岃瘉鏄庡綋鏃跺彂甯冨唴瀹逛笌鍙戝竷鏃堕棿锛堝悎瑙勭暀璇侊級銆�
-      </el-alert>
-      <el-descriptions :column="2" border class="mb16">
-        <el-descriptions-item label="褰撳墠鐗堟湰">v{{ versionDialog.row?.versionNo || 1 }}</el-descriptions-item>
-        <el-descriptions-item label="鏈�杩戝彂甯�">{{ versionDialog.row?.publishTime || "鈥�" }}</el-descriptions-item>
-      </el-descriptions>
-      <el-table :data="versionList" border size="small" empty-text="鏆傛棤鍘嗗彶鐗堟湰">
-        <el-table-column prop="versionNo" label="鐗堟湰" width="70" align="center" />
-        <el-table-column prop="title" label="鏍囬" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="changeNote" label="鍙樻洿璇存槑" width="120" />
-        <el-table-column prop="publishTime" label="鍙戝竷鏃堕棿" width="170" />
-        <el-table-column prop="archivedAt" label="褰掓。鏃堕棿" width="170" />
-        <el-table-column label="鎿嶄綔" width="90" align="center">
-          <template #default="{ row: ver }">
-            <el-button type="primary" link @click="previewVersion(ver)">鏌ョ湅</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <template #footer>
-        <el-button @click="versionDialog.visible = false">鍏� 闂�</el-button>
-      </template>
-    </el-dialog>
-
-    <!-- 鐗堟湰棰勮 -->
-    <el-dialog v-model="versionPreview.visible" title="鍘嗗彶鐗堟湰鍐呭" width="640px" append-to-body>
-      <p class="version-meta">
-        v{{ versionPreview.data?.versionNo }} 路 {{ versionPreview.data?.changeNote }} 路
-        {{ versionPreview.data?.publishTime }}
-      </p>
-      <div class="version-html" v-html="versionPreview.data?.contentHtml || ''" />
     </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { Plus, RefreshRight } from "@element-plus/icons-vue";
+import { Plus, RefreshRight, Search } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
 import { computed, onMounted, reactive, ref } from "vue";
+import useUserStore from "@/store/modules/user";
 import Editor from "@/components/Editor/index.vue";
 import FileUpload from "@/components/AttachmentUpload/file/index.vue";
-import { newsTypeColor } from "./enterpriseNewsUtils.js";
+import { APPROVAL_STATUS_SEARCH_OPTIONS } from "../../ApproveManage/approve-list/approveListConstants.js";
+import ApproveDetailPanel from "../../ApproveManage/approve-list/components/ApproveDetailPanel.vue";
+import { buildEditFormFromInstanceRow } from "../../ApproveManage/approve-list/approveListConstants.js";
+import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
+import TemplateFlowEditor from "../../ApproveManage/approve-template/components/TemplateFlowEditor.vue";
+import {
+  applyBindingToForm,
+  validateTemplateBinding,
+} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
+import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
 import NewsDetailPanel from "./components/NewsDetailPanel.vue";
-import { useEnterpriseNews } from "./useEnterpriseNews.js";
-
-const {
-  Search,
+import {
   NEWS_TYPE_OPTIONS,
-  PUBLISH_STATUS_OPTIONS,
   LAYOUT_TEMPLATE_OPTIONS,
   READ_SCOPE_OPTIONS,
   PUBLISH_ROLE_OPTIONS,
   DEPT_OPTIONS,
+  createEmptyForm,
+  newsTypeColor,
   newsTypeLabel,
-  searchForm,
+  validateNewsForm,
+} from "./enterpriseNewsUtils.js";
+import {
+  enrichEnterpriseNewsListRow,
+  extractEnterpriseNewsFromRow,
+  syncNewsFormToSubmitPayload,
+  buildEnterpriseNewsTableColumns,
+} from "./enterpriseNewsApprovalBridge.js";
+
+const userStore = useUserStore();
+
+const searchForm = reactive({
+  keyword: "",
+  newsType: "",
+  status: "",
+  createTimeRange: null,
+});
+
+const newsFormDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
+const newsForm = reactive(createEmptyForm());
+const newsFormRef = ref();
+const galleryInput = ref("");
+
+const newsFormRules = {
+  title: [{ required: true, message: "璇疯緭鍏ユ柊闂绘爣棰�", trigger: "blur" }],
+  newsType: [{ required: true, message: "璇烽�夋嫨鏂伴椈鍒嗙被", trigger: "change" }],
+  readScope: [{ required: true, message: "璇烽�夋嫨闃呰鑼冨洿", trigger: "change" }],
+};
+
+const mod = useApprovalInstanceModule({
+  moduleKey: APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS,
+  enrichListRow: enrichEnterpriseNewsListRow,
+  buildExtraListParams(sf) {
+    const extra = {};
+    const kw = (sf?.keyword || "").trim();
+    if (kw) extra.title = kw;
+    if (sf?.newsType) extra.newsType = sf.newsType;
+    return extra;
+  },
+  async beforeSave(submitForm) {
+    const v = validateNewsForm(newsForm);
+    if (!v.ok) {
+      ElMessage.warning(v.message);
+      throw new Error(v.message);
+    }
+    if (!activeTemplate.value) {
+      ElMessage.warning("璇峰厛閫夋嫨瀹℃壒妯℃澘");
+      throw new Error("no template");
+    }
+    const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+    if (!bindingCheck.ok) {
+      ElMessage.warning(bindingCheck.message);
+      throw new Error(bindingCheck.message);
+    }
+    syncNewsFormToSubmitPayload(newsForm, submitForm);
+  },
+});
+
+const {
+  tableData,
   tableLoading,
   page,
-  tableData,
-  tableColumn,
-  formDialog,
-  form,
-  formRef,
-  formRules,
   detailDialog,
   detailRow,
-  unreadDialog,
-  unreadList,
-  versionDialog,
-  getUnreadEmployees,
+  submitDialog,
+  submitForm,
+  submitSaving,
+  isSubmitEdit,
+  activeTemplate,
+  templateBindVisible,
+  pendingTemplateBinding,
+  submitEditRow,
   handleQuery,
-  resetSearch,
+  initModuleList,
   pagination,
-  openFormDialog,
-  openDetail,
-  openUnreadRemind,
-  openVersionHistory,
-  saveForm,
-  sendUnreadRemind,
-  toggleLike,
-  addComment,
-} = useEnterpriseNews();
+  openAddWithTemplate,
+  onTemplateBound,
+  resetSubmitForm,
+  submitInstanceForm,
+  removeInstance,
+  canEditBusinessInstanceRow,
+} = mod;
 
-const galleryInput = ref("");
-const unreadSelected = ref([]);
-const versionPreview = reactive({ visible: false, data: null });
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
 
-const versionList = computed(() => {
-  const row = versionDialog.row;
-  if (!row) return [];
-  const history = [...(row.versions || [])];
-  return history.sort((a, b) => (b.versionNo || 0) - (a.versionNo || 0));
+const detailNewsRow = computed(() => {
+  if (!detailRow.value?.id) return {};
+  return extractEnterpriseNewsFromRow(detailRow.value);
 });
+
+const tableColumn = ref(
+  buildEnterpriseNewsTableColumns(() => [
+    { name: "璇︽儏", type: "text", clickFun: (row) => openNewsDetail(row) },
+    {
+      name: "淇敼",
+      type: "text",
+      disabled: (row) => !canEditBusinessInstanceRow(row),
+      clickFun: (row) => openNewsEdit(row),
+    },
+    {
+      name: "鍒犻櫎",
+      type: "danger",
+      clickFun: (row) => removeInstance(row),
+    },
+  ])
+);
+
+function resetNewsForm(target = createEmptyForm()) {
+  Object.assign(newsForm, createEmptyForm(), target);
+}
+
+function openNewsFormDialog(mode, row) {
+  newsFormDialog.mode = mode;
+  newsFormDialog.readonly = mode === "view";
+  newsFormDialog.title =
+    mode === "add" ? "鏂板缓浼佷笟鏂伴椈" : mode === "edit" ? "缂栬緫浼佷笟鏂伴椈" : "鏌ョ湅浼佷笟鏂伴椈";
+  if (mode === "add") {
+    resetNewsForm({
+      publisherName: userStore?.nickName || userStore?.name || "褰撳墠鐢ㄦ埛",
+    });
+  } else if (row) {
+    resetNewsForm(extractEnterpriseNewsFromRow(row));
+  }
+  newsFormDialog.visible = true;
+}
+
+function onTemplateBindClosed() {
+  const binding = pendingTemplateBinding.value;
+  if (!binding) return;
+  pendingTemplateBinding.value = null;
+  resetSubmitForm();
+  applyBindingToForm(submitForm, binding);
+  submitDialog.mode = "add";
+  submitEditRow.value = null;
+  openNewsFormDialog("add");
+}
+
+function openNewsEdit(row) {
+  if (!canEditBusinessInstanceRow(row)) {
+    ElMessage.warning("杩涜涓垨宸插畬鎴愮殑瀹℃壒涓嶅彲淇敼");
+    return;
+  }
+  submitDialog.mode = "edit";
+  submitEditRow.value = { ...row };
+  Object.assign(submitForm, buildEditFormFromInstanceRow(row));
+  openNewsFormDialog("edit", row);
+}
+
+function openNewsDetail(row) {
+  detailRow.value = { ...row };
+  detailDialog.visible = true;
+}
+
+function openNewsEditFromDetail() {
+  const row = detailRow.value;
+  detailDialog.visible = false;
+  openNewsEdit(row);
+}
+
+function onNewsFormClosed() {
+  newsFormRef.value?.resetFields?.();
+}
 
 function addGalleryItem() {
   const name = (galleryInput.value || "").trim();
   if (!name) return;
-  form.mediaList = form.mediaList || [];
-  form.mediaList.push({ type: "image", name, url: "" });
+  newsForm.mediaList = newsForm.mediaList || [];
+  newsForm.mediaList.push({ type: "image", name, url: "" });
   galleryInput.value = "";
 }
 
-function onSave(action) {
-  const ret = saveForm(action);
-  if (ret?.message) {
-    ElMessage.warning(ret.message);
+async function onNewsSave(action = "submit_review") {
+  try {
+    await newsFormRef.value?.validate();
+  } catch {
+    ElMessage.warning("璇峰畬鍠勮〃鍗曞繀濉」鍚庡啀淇濆瓨");
     return;
   }
-  if (ret?.ok) {
-    ElMessage.success(action === "publish" ? "宸插彂甯�" : action === "submit_review" ? "宸叉彁浜ゅ鏍�" : "宸蹭繚瀛�");
+  if (action === "draft") newsForm.publishStatus = "draft";
+  else newsForm.publishStatus = "pending_review";
+  const ok = await submitInstanceForm({ skipValidate: true });
+  if (ok) {
+    newsFormDialog.visible = false;
+    const msg =
+      action === "draft" ? "宸蹭繚瀛樿崏绋�" : isSubmitEdit.value ? "淇敼鎴愬姛" : "宸叉彁浜ゅ鏍�";
+    ElMessage.success(msg);
   }
+}
+
+function onSearch() {
+  handleQuery(searchForm);
+}
+
+function resetSearch() {
+  searchForm.keyword = "";
+  searchForm.newsType = "";
+  searchForm.status = "";
+  searchForm.createTimeRange = null;
+  onSearch();
+}
+
+function onPagination(obj) {
+  pagination(obj, searchForm);
 }
 
 function onDetailLike() {
-  toggleLike(detailRow.value);
+  /* 璇︽儏浜掑姩浠嶈蛋琛屽唴鏁版嵁锛屽埛鏂板垪琛ㄥ悗鏇存柊 */
 }
 
-function onDetailComment(text) {
-  const ret = addComment(detailRow.value, text);
-  if (ret?.message) ElMessage.warning(ret.message);
-  else if (ret?.ok) ElMessage.success("璇勮宸插彂甯�");
+function onDetailComment() {
+  ElMessage.info("璇勮宸茶褰曪紙婕旂ず锛�");
 }
 
-function openUnreadFromDetail() {
-  const row = detailRow.value;
-  detailDialog.visible = false;
-  openUnreadRemind(row);
-}
-
-function openVersionFromDetail() {
-  const row = detailRow.value;
-  detailDialog.visible = false;
-  openVersionHistory(row);
-}
-
-function onUnreadSelectionChange(rows) {
-  unreadSelected.value = rows.map((r) => r.userId);
-}
-
-function selectAllUnread() {
-  unreadSelected.value = unreadList.value.map((u) => u.userId);
-}
-
-function onSendRemind() {
-  const ids = unreadSelected.value;
-  const ret = sendUnreadRemind(ids);
-  if (ret?.message) {
-    ElMessage.warning(ret.message);
-    return;
-  }
-  if (ret?.ok) ElMessage.success(`宸插悜 ${ret.count} 鍚嶅憳宸ュ彂閫侀槄璇绘彁閱抈);
-}
-
-function previewVersion(ver) {
-  versionPreview.data = ver;
-  versionPreview.visible = true;
-}
-
-onMounted(() => {
-  handleQuery();
+onMounted(async () => {
+  loadFlowUsers();
+  await initModuleList(searchForm);
 });
 </script>
 
@@ -421,6 +483,10 @@
 .search_actions {
   flex-shrink: 0;
 }
+.search_title {
+  font-size: 14px;
+  color: var(--el-text-color-regular);
+}
 .news-type-tag {
   font-weight: 600;
   font-size: 13px;
@@ -428,32 +494,18 @@
 .media-tag {
   margin: 6px 8px 0 0;
 }
-.unread-toolbar {
-  display: flex;
-  align-items: center;
-  gap: 12px;
+.template-name {
+  font-weight: 600;
+  color: var(--el-text-color-primary);
 }
-.unread-stat {
+.section-tip {
+  font-size: 12px;
   color: var(--el-text-color-secondary);
-  font-size: 13px;
+  margin: 8px 0 0;
+  line-height: 1.5;
 }
-.version-meta {
-  color: var(--el-text-color-secondary);
-  font-size: 13px;
-  margin-bottom: 12px;
-}
-.version-html {
-  padding: 12px;
-  background: var(--el-fill-color-light);
-  border-radius: 6px;
-  max-height: 400px;
-  overflow-y: auto;
-}
-.mb16 {
-  margin-bottom: 16px;
-}
-.mb12 {
-  margin-bottom: 12px;
+.mb20 {
+  margin-bottom: 20px;
 }
 .ml10 {
   margin-left: 10px;
diff --git a/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
index 4821495..83bfefb 100644
--- a/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
+++ b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
@@ -10,7 +10,7 @@
           placeholder="璇疯緭鍏ョ敵璇蜂汉"
           clearable
           :prefix-icon="Search"
-          @keyup.enter="handleQuery"
+          @keyup.enter="onSearch"
         />
         <span class="search_title" style="margin-left: 12px">鐢宠鏃ユ湡锛�</span>
         <el-date-picker
@@ -24,7 +24,7 @@
           style="width: 260px"
           clearable
         />
-        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+        <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
         <el-button @click="resetSearch">閲嶇疆</el-button>
       </div>
       <div>
@@ -39,56 +39,24 @@
         :page="page"
         :isSelection="false"
         :tableLoading="tableLoading"
-        @pagination="pagination"
+        @pagination="onPagination"
         :total="page.total"
       />
     </div>
 
-    <!-- 鏂板 / 缂栬緫 -->
-    <el-dialog
-      v-if="formDialog.visible"
-      v-model="formDialog.visible"
-      :title="formDialog.title"
-      width="960px"
-      append-to-body
-      destroy-on-close
-      class="regular-apply-form-dialog"
-      @closed="onFormClosed"
-    >
-      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="regular-apply-form">
-        <el-form-item v-if="form.templateSnapshot" label="瀹℃壒妯℃澘">
-          <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span>
-          <el-button
-            v-if="formDialog.mode === 'add'"
-            type="primary"
-            link
-            class="ml12"
-            @click="reopenTemplateBind"
-          >
-            鏇存崲妯℃澘
-          </el-button>
-        </el-form-item>
-        <FormPayloadFields :fields="form.formFieldDefs" :form-payload="form.formPayload" />
-        <ApprovalTemplateFormSection
-          :active-template="form.templateSnapshot"
-          :fields="form.formFieldDefs"
-          :form-payload="form.formPayload"
-          v-model:flow-nodes="form.flowNodes"
-          v-model:attachments="form.storageBlobDTOs"
-          :template-attachments="form.templateAttachments"
-          :user-options="flowUserOptions"
-          flow-attachments-only
-          hide-template-name
-          :allow-change-template="false"
-        />
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
-          <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceSubmitDialog
+      v-model="submitDialog.visible"
+      :title="submitDialogTitle"
+      :form="submitForm"
+      :rules="submitFormRules"
+      :fields="submitFormFields"
+      :active-template="activeTemplate"
+      :user-options="flowUserOptions"
+      :is-edit="isSubmitEdit"
+      :saving="submitSaving"
+      :form-ref="submitFormRef"
+      @submit="onSubmit"
+    />
 
     <ApprovalTemplateBindDialog
       v-model:visible="templateBindVisible"
@@ -98,392 +66,96 @@
       @closed="onTemplateBindClosed"
     />
 
-    <!-- 璇︽儏锛堝彧璇伙級 -->
-    <el-dialog v-model="detailDialog.visible" title="杞鐢宠璇︽儏" width="640px" append-to-body>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="鐢宠浜�">{{ detailRow.applicantName }}</el-descriptions-item>
-        <el-descriptions-item label="鐢宠鏃ユ湡">{{ detailRow.applyDate }}</el-descriptions-item>
-        <el-descriptions-item label="杞鏃ユ湡">{{ detailRow.regularizationDate }}</el-descriptions-item>
-        <el-descriptions-item label="璇曠敤鏈熷伐浣滄�荤粨">{{ detailRow.probationSummary }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒缁撴灉">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒鏂瑰紡">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒浜�">{{ detailRow.approverNames || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="闄勪欢">
-          <template v-if="detailRow.attachmentList?.length">
-            <el-tag
-              v-for="(f, i) in detailRow.attachmentList"
-              :key="i"
-              class="mr6 mb6"
-              type="info"
-            >
-              {{ f.name }}
-            </el-tag>
-          </template>
-          <span v-else>鏃�</span>
-        </el-descriptions-item>
-      </el-descriptions>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 闄勪欢鍒楄〃 -->
-    <el-dialog v-model="filesDialog.visible" title="闄勪欢" width="520px" append-to-body>
-      <el-table v-if="filesDialog.row?.attachmentList?.length" :data="filesDialog.row.attachmentList" border>
-        <el-table-column type="index" label="搴忓彿" width="60" align="center" />
-        <el-table-column prop="name" label="鏂囦欢鍚�" min-width="200" show-overflow-tooltip />
-        <el-table-column label="鎿嶄綔" width="100" align="center">
-          <template #default="{ row }">
-            <el-button link type="primary" @click="mockDownload(row)">涓嬭浇</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-empty v-else description="鏆傛棤闄勪欢" />
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="filesDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceDetailDialog
+      v-model="detailDialog.visible"
+      title="杞鐢宠璇︽儏"
+      :row="detailRow"
+      @edit="openEditFromDetail"
+    />
   </div>
 </template>
 
 <script setup>
 import { Search } from "@element-plus/icons-vue";
-import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { onMounted, reactive } from "vue";
+import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
+import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
 import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
-import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
-import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
+import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
 import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
-import {
-  applyBindingToForm,
-  buildFormPayloadRules,
-  validateTemplateBinding,
-} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
 import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
-
-/** 涓庡悗绔害瀹氬瓧娈碉紙鍗犱綅锛� */
-const createEmptyForm = () => ({
-  id: undefined,
-  applicantName: "",
-  applyDate: "",
-  regularizationDate: "",
-  probationSummary: "",
-  hasTemplateBinding: false,
-  templateId: "",
-  templateName: "",
-  templateSnapshot: null,
-  formFieldDefs: [],
-  formPayload: {},
-  flowNodes: [],
-  templateAttachments: [],
-  storageBlobDTOs: [],
-});
-
-const { proxy } = getCurrentInstance();
-
-function approvalModeLabel(mode) {
-  if (mode === "countersign") return "浼氱";
-  return "涓庣";
-}
-
-function approvalResultLabel(v) {
-  if (v === "approved") return "宸查�氳繃";
-  if (v === "rejected") return "宸查┏鍥�";
-  if (v === "cancelled") return "宸叉挙閿�";
-  return "寰呭鎵�";
-}
-
-const allRows = ref([]);
 
 const searchForm = reactive({
   applicantName: "",
   applyDateRange: null,
 });
 
-const tableLoading = ref(false);
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-
-const filteredList = computed(() => {
-  let list = [...allRows.value];
-  const name = (searchForm.applicantName || "").trim();
-  if (name) {
-    list = list.filter((r) => r.applicantName.includes(name));
-  }
-  const range = searchForm.applyDateRange;
-  if (range && range.length === 2) {
-    const [start, end] = range;
-    list = list.filter((r) => r.applyDate >= start && r.applyDate <= end);
-  }
-  return list.sort((a, b) => (a.applyDate < b.applyDate ? 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;
+const mod = useApprovalInstanceModule({
+  moduleKey: APPROVAL_MODULE_KEYS.REGULAR,
+  buildExtraListParams(sf) {
+    const range = sf?.applyDateRange;
+    if (Array.isArray(range) && range[0]) {
+      return { createTime: range[0], createTimeEnd: range[1] };
     }
+    return {};
   },
-  { immediate: true }
-);
-
-const tableData = computed(() => {
-  const list = filteredList.value;
-  const start = (page.current - 1) * page.size;
-  return list.slice(start, start + page.size);
 });
 
-const tableColumn = ref([
-  { label: "鐢宠浜�", prop: "applicantName", minWidth: 100 },
-  { label: "鐢宠鏃ユ湡", prop: "applyDate", width: 120 },
-  { label: "杞鏃ユ湡", prop: "regularizationDate", width: 120 },
-  { label: "璇曠敤鏈熷伐浣滄�荤粨", prop: "probationSummary", minWidth: 200 },
-  {
-    label: "瀹℃壒缁撴灉",
-    prop: "approvalResult",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => approvalResultLabel(v),
-    formatType: (v) => {
-      if (v === "approved") return "success";
-      if (v === "rejected") return "danger";
-      if (v === "cancelled") return "info";
-      return "warning";
-    },
-  },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    fixed: "right",
-    width: 200,
-    operation: [
-      {
-        name: "缂栬緫",
-        type: "text",
-        clickFun: (row) => openFormDialog("edit", row),
-      },
-      {
-        name: "鏌ョ湅璇︽儏",
-        type: "text",
-        clickFun: (row) => openDetail(row),
-      },
-      {
-        name: "闄勪欢",
-        type: "text",
-        clickFun: (row) => openFiles(row),
-      },
-    ],
-  },
-]);
+const {
+  tableData,
+  tableLoading,
+  page,
+  detailDialog,
+  detailRow,
+  submitDialog,
+  submitForm,
+  submitFormRef,
+  submitSaving,
+  isSubmitEdit,
+  activeTemplate,
+  submitFormFields,
+  submitFormRules,
+  submitDialogTitle,
+  templateBindVisible,
+  handleQuery,
+  initModuleList,
+  pagination,
+  openAddWithTemplate,
+  onTemplateBound,
+  onTemplateBindClosed,
+  openEditFromDetail,
+  submitInstanceForm,
+  buildTableActions,
+} = mod;
 
-const formDialog = reactive({
-  visible: false,
-  title: "",
-  mode: "add",
-});
-const formRef = ref();
-const form = reactive(createEmptyForm());
-const templateBindVisible = ref(false);
-const pendingTemplateBinding = ref(null);
 const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
 
-const formRules = computed(() => buildFormPayloadRules(form.formFieldDefs));
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
 
-const detailDialog = reactive({ visible: false });
-const detailRow = ref({});
-
-const filesDialog = reactive({ visible: false, row: null });
-
-function handleQuery() {
-  page.current = 1;
-  tableLoading.value = true;
-  setTimeout(() => {
-    tableLoading.value = false;
-  }, 150);
+function onSearch() {
+  handleQuery(searchForm);
 }
 
 function resetSearch() {
   searchForm.applicantName = "";
   searchForm.applyDateRange = null;
-  handleQuery();
+  onSearch();
 }
 
-function pagination(obj) {
-  page.current = obj.page;
-  page.size = obj.limit;
+function onPagination(obj) {
+  pagination(obj, searchForm);
 }
 
-function openDetail(row) {
-  detailRow.value = { ...row };
-  detailDialog.visible = true;
+async function onSubmit() {
+  const ok = await submitInstanceForm({ skipValidate: true });
+  if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
 }
 
-function openFiles(row) {
-  filesDialog.row = row;
-  filesDialog.visible = true;
-}
-
-function mockDownload(row) {
-  const url = row.url || row.downloadURL || row.previewURL || row.previewUrl;
-  if (url) {
-    window.open(url, "_blank");
-    return;
-  }
-  proxy?.$modal?.msgWarning?.("鏆傛棤涓嬭浇鍦板潃");
-}
-
-function openAddWithTemplate() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function onTemplateBound(binding) {
-  pendingTemplateBinding.value = binding;
-}
-
-function onTemplateBindClosed() {
-  const binding = pendingTemplateBinding.value;
-  if (!binding) return;
-  pendingTemplateBinding.value = null;
-  openFormWithBinding(binding);
-}
-
-function openFormWithBinding(binding) {
-  Object.assign(form, createEmptyForm());
-  applyBindingToForm(form, binding);
-  form.hasTemplateBinding = true;
-  formDialog.mode = "add";
-  formDialog.title = "鏂板杞鐢宠";
+onMounted(async () => {
   loadFlowUsers();
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function reopenTemplateBind() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function openFormDialog(mode, row) {
-  if (mode === "edit" && row && !row.hasTemplateBinding) {
-    proxy?.$modal?.msgWarning?.("璇ヨ褰曚负鏃х増鏁版嵁锛岃閲嶆柊閫氳繃妯℃澘鍙戣捣鐢宠");
-    return;
-  }
-  formDialog.mode = mode;
-  formDialog.title = "缂栬緫杞鐢宠";
-  Object.assign(form, createEmptyForm());
-  if (mode === "edit" && row) {
-    Object.assign(form, {
-      id: row.id,
-      applicantName: row.applicantName,
-      applyDate: row.applyDate,
-      regularizationDate: row.regularizationDate,
-      probationSummary: row.probationSummary,
-      hasTemplateBinding: true,
-      templateId: row.templateId,
-      templateName: row.templateName,
-      templateSnapshot: row.templateSnapshot,
-      formFieldDefs: row.formFieldDefs || [],
-      formPayload: JSON.parse(JSON.stringify(row.formPayload || {})),
-      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])),
-      templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])),
-      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
-    });
-    loadFlowUsers();
-  }
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function onFormClosed() {
-  formRef.value?.resetFields?.();
-}
-
-async function submitForm() {
-  try {
-    await formRef.value?.validate?.();
-  } catch {
-    return;
-  }
-  const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes });
-  if (!flowCheck.ok) {
-    proxy?.$modal?.msgWarning?.(flowCheck.message || "璇峰畬鍠勫鎵规祦绋�");
-    return;
-  }
-  form.flowNodes = flowCheck.nodes;
-  syncRegularFieldsFromPayload();
-  const payload = {
-    applicantName: form.applicantName,
-    applyDate: form.applyDate,
-    regularizationDate: form.regularizationDate,
-    probationSummary: form.probationSummary,
-    hasTemplateBinding: true,
-    templateId: form.templateId,
-    templateName: form.templateName,
-    templateSnapshot: form.templateSnapshot,
-    formFieldDefs: form.formFieldDefs,
-    formPayload: JSON.parse(JSON.stringify(form.formPayload || {})),
-    flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])),
-    templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])),
-    storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])),
-  };
-  if (formDialog.mode === "add") {
-    const id = `local_${Date.now()}`;
-    allRows.value.unshift({ id, ...payload, approvalResult: "pending" });
-    proxy?.$modal?.msgSuccess?.("鏂板鎴愬姛");
-  } else {
-    const idx = allRows.value.findIndex((r) => r.id === form.id);
-    if (idx !== -1) {
-      const prev = allRows.value[idx];
-      allRows.value[idx] = {
-        ...prev,
-        id: form.id,
-        ...payload,
-        approvalResult: prev.approvalResult ?? "pending",
-      };
-    }
-    proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
-  }
-  formDialog.visible = false;
-  handleQuery();
-}
-
-/** 浠庢ā鏉垮~鎶ラ」鍚屾鍒楄〃灞曠ず瀛楁 */
-function syncRegularFieldsFromPayload() {
-  const defs = form.formFieldDefs || [];
-  const payload = form.formPayload || {};
-  for (const f of defs) {
-    const label = String(f.label || "");
-    const val = payload[f.key];
-    if (label.includes("鐢宠浜�") && !label.includes("鏃ユ湡")) {
-      form.applicantName = val != null && val !== "" ? String(val) : form.applicantName;
-    }
-    if (label.includes("鐢宠鏃ユ湡") && f.type === "date") {
-      form.applyDate = val || "";
-    }
-    if (label.includes("杞") && (label.includes("鏃ユ湡") || label.includes("鏃堕棿")) && f.type === "date") {
-      form.regularizationDate = val || "";
-    }
-    if (label.includes("璇曠敤鏈�") || label.includes("宸ヤ綔鎬荤粨")) {
-      form.probationSummary = val != null ? String(val) : "";
-    }
-  }
-}
-
-onMounted(() => {
-  loadFlowUsers();
+  await initModuleList(searchForm);
 });
 </script>
 
@@ -501,27 +173,5 @@
 .search_title {
   font-size: 14px;
   color: var(--el-text-color-regular);
-}
-.mr6 {
-  margin-right: 6px;
-}
-.mb6 {
-  margin-bottom: 6px;
-}
-.regular-apply-form :deep(.el-row) {
-  margin-bottom: 0;
-}
-.regular-apply-form :deep(.el-form-item) {
-  margin-bottom: 18px;
-}
-.template-name {
-  font-weight: 600;
-  color: var(--el-text-color-primary);
-}
-.ml12 {
-  margin-left: 12px;
-}
-.regular-apply-form-dialog :deep(.el-dialog__body) {
-  padding-top: 12px;
 }
 </style>
diff --git a/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue b/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
index 854d945..5be9b2b 100644
--- a/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
+++ b/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
@@ -34,7 +34,7 @@
           style="width: 260px"
           clearable
         />
-        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+        <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
         <el-button @click="resetSearch">閲嶇疆</el-button>
       </div>
       <div>
@@ -49,64 +49,32 @@
         :page="page"
         :isSelection="false"
         :tableLoading="tableLoading"
-        @pagination="pagination"
+        @pagination="onPagination"
         :total="page.total"
       />
     </div>
 
-    <!-- 鏂板 / 缂栬緫 -->
-    <el-dialog
-      v-if="formDialog.visible"
-      v-model="formDialog.visible"
-      :title="formDialog.title"
-      width="960px"
-      append-to-body
-      destroy-on-close
-      class="transfer-apply-form-dialog"
-      @closed="onFormClosed"
+    <ApprovalInstanceSubmitDialog
+      v-model="submitDialog.visible"
+      :title="submitDialogTitle"
+      :form="submitForm"
+      :rules="submitFormRules"
+      :fields="submitFormFields"
+      :active-template="activeTemplate"
+      :user-options="flowUserOptions"
+      :is-edit="isSubmitEdit"
+      :saving="submitSaving"
+      :form-ref="submitFormRef"
+      flow-attachments-only
+      @submit="onSubmit"
     >
-      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="transfer-apply-form">
-        <el-form-item v-if="form.templateSnapshot" label="瀹℃壒妯℃澘">
-          <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span>
-          <el-button
-            v-if="formDialog.mode === 'add'"
-            type="primary"
-            link
-            class="ml12"
-            @click="reopenTemplateBind"
-          >
-            鏇存崲妯℃澘
-          </el-button>
+      <template #before="{ form, fields }">
+        <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
+        <el-form-item label="鍘熷矖浣�">
+          <el-input :model-value="originalPostName" placeholder="閫夋嫨鐢宠浜哄悗鑷姩甯﹀嚭" disabled />
         </el-form-item>
-
-        <FormPayloadFields :fields="templateDisplayFields" :form-payload="form.formPayload" />
-        <el-row :gutter="24">
-          <el-col :span="12">
-            <el-form-item label="鍘熷矖浣�" prop="originalPostName">
-              <el-input v-model="form.originalPostName" placeholder="閫夋嫨鐢宠浜哄悗鑷姩甯﹀嚭" disabled />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <ApprovalTemplateFormSection
-            :active-template="form.templateSnapshot"
-            :fields="form.formFieldDefs"
-            :form-payload="form.formPayload"
-            v-model:flow-nodes="form.flowNodes"
-            v-model:attachments="form.storageBlobDTOs"
-            :template-attachments="form.templateAttachments"
-            :user-options="flowUserOptions"
-            flow-attachments-only
-            hide-template-name
-            :allow-change-template="false"
-          />
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
-          <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
-        </div>
       </template>
-    </el-dialog>
+    </ApprovalInstanceSubmitDialog>
 
     <ApprovalTemplateBindDialog
       v-model:visible="templateBindVisible"
@@ -116,64 +84,29 @@
       @closed="onTemplateBindClosed"
     />
 
-    <!-- 璇︽儏 -->
-    <el-dialog v-model="detailDialog.visible" title="璋冨矖鐢宠璇︽儏" width="560px" append-to-body>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="鐢宠浜�">{{ detailRow.applicantName }}</el-descriptions-item>
-        <el-descriptions-item label="杞矖鏃ユ湡">{{ detailRow.transferDate }}</el-descriptions-item>
-        <el-descriptions-item label="鍘熷矖浣�">{{ detailRow.originalPostName }}</el-descriptions-item>
-        <el-descriptions-item label="杞叆宀椾綅">{{ detailRow.targetPostName }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒缁撴灉">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒鏂瑰紡">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒浜�">{{ detailRow.approverNames || "鈥�" }}</el-descriptions-item>
-      </el-descriptions>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceDetailDialog
+      v-model="detailDialog.visible"
+      title="璋冨矖鐢宠璇︽儏"
+      :row="detailRow"
+      @edit="openEditFromDetail"
+    />
   </div>
 </template>
 
 <script setup>
 import { findPostOptions } from "@/api/system/post.js";
 import { userListNoPageByTenantId } from "@/api/system/user.js";
-import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
-import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
-import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
+import { ElMessage } from "element-plus";
+import { computed, onMounted, reactive, ref, watch } from "vue";
 import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
+import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
+import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
+import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
+import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
 import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
-import {
-  applyBindingToForm,
-  buildFormPayloadRules,
-  validateTemplateBinding,
-} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
 import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
 import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
-
-const { proxy } = getCurrentInstance();
-
-/** 涓庡悗绔害瀹氬瓧娈碉紙鏈湴鍗犱綅锛屽悗鏈熸帴鍙e榻愶級 */
-const createEmptyForm = () => ({
-  id: undefined,
-  applicantId: "",
-  applicantName: "",
-  transferDate: "",
-  originalPostId: "",
-  originalPostName: "",
-  targetPostId: "",
-  targetPostName: "",
-  hasTemplateBinding: false,
-  templateId: "",
-  templateName: "",
-  templateSnapshot: null,
-  formFieldDefs: [],
-  formPayload: {},
-  flowNodes: [],
-  templateAttachments: [],
-  storageBlobDTOs: [],
-});
 
 function isOriginalPostField(field) {
   const label = String(field?.label || "");
@@ -185,6 +118,10 @@
   );
 }
 
+function displayTemplateFields(fields = []) {
+  return (fields || []).filter((f) => !isOriginalPostField(f));
+}
+
 function findApplicantTemplateField(fields = []) {
   return (
     fields.find((f) => String(f?.label || "").includes("鐢宠浜�")) ||
@@ -193,45 +130,73 @@
   );
 }
 
-function syncApplicantFromUser(uid) {
-  const u = userById(uid);
-  if (u) {
-    form.applicantId = uid != null && uid !== "" ? uid : "";
-    form.applicantName = u.nickName || u.userName || "";
-    const { originalPostId, originalPostName } = resolveOriginalPost(u);
-    form.originalPostId = originalPostId;
-    form.originalPostName = originalPostName;
-  } else {
-    form.applicantId = "";
-    form.applicantName = "";
-    form.originalPostId = "";
-    form.originalPostName = "";
-  }
-}
+const searchForm = reactive({
+  applicantId: "",
+  transferDateRange: null,
+});
 
-/** 绯荤粺鐢ㄦ埛缂撳瓨锛�/system/user/userListNoPageByTenantId锛屼笌杞鐢宠绛変竴鑷达級 */
+const mod = useApprovalInstanceModule({
+  moduleKey: APPROVAL_MODULE_KEYS.TRANSFER,
+  buildExtraListParams(sf) {
+    const range = sf?.transferDateRange;
+    if (Array.isArray(range) && range[0]) {
+      return { createTime: range[0], createTimeEnd: range[1] };
+    }
+    return {};
+  },
+});
+
+const {
+  tableData,
+  tableLoading,
+  page,
+  detailDialog,
+  detailRow,
+  submitDialog,
+  submitForm,
+  submitFormRef,
+  submitSaving,
+  isSubmitEdit,
+  activeTemplate,
+  submitFormFields,
+  submitFormRules,
+  submitDialogTitle,
+  templateBindVisible,
+  handleQuery,
+  initModuleList,
+  pagination,
+  openAddWithTemplate,
+  onTemplateBound,
+  onTemplateBindClosed,
+  openEditFromDetail,
+  submitInstanceForm,
+  buildTableActions,
+} = mod;
+
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+
 const allUsersCache = ref([]);
-/** 宀椾綅瀛楀吀 postId -> postName锛�/system/post/optionselect锛屼笌鍛樺伐妗f鍏ヨ亴琛ㄥ崟涓�鑷达級 */
 const postIdToName = ref({});
 const targetPostOptions = ref([]);
+const applicantSearchLoading = ref(false);
+const applicantSearchOptions = ref([]);
+const originalPostName = ref("");
 
-function rebuildPostIdMap() {
-  const m = {};
-  for (const p of targetPostOptions.value || []) {
-    const id = p.postId ?? p.value ?? p.id;
-    if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? "";
-  }
-  postIdToName.value = m;
+const applicantTemplateField = computed(() =>
+  findApplicantTemplateField(submitForm.formFieldDefs)
+);
+
+function unwrapArray(payload) {
+  if (Array.isArray(payload)) return payload;
+  if (payload && Array.isArray(payload.data)) return payload.data;
+  if (payload && Array.isArray(payload.rows)) return payload.rows;
+  return [];
 }
 
-function targetPostNameById(postId) {
-  if (postId == null || postId === "") return "";
-  const k = String(postId);
-  return (
-    postIdToName.value[k] ||
-    targetPostOptions.value.find((x) => String(x.postId ?? x.id ?? x.value) === k)?.postName ||
-    ""
-  );
+function isActiveUser(u) {
+  if (u.delFlag === "2" || u.delFlag === 2) return false;
+  if (u.status == null) return true;
+  return String(u.status) === "0";
 }
 
 function userSelectLabel(u) {
@@ -248,30 +213,19 @@
   return undefined;
 }
 
-/** 浠庣敤鎴峰璞¤В鏋愩�屽師宀椾綅銆嶏紙鍏煎 postName / postIds / posts 绛夊父瑙佽繑鍥烇級 */
 function resolveOriginalPost(user) {
-  if (!user) return { originalPostId: "", originalPostName: "" };
+  if (!user) return { originalPostName: "" };
   const nameStr = (user.postName ?? user.postname ?? "").toString().trim();
-  if (nameStr) {
-    const pid = firstPostId(user);
-    return { originalPostId: pid != null && pid !== "" ? String(pid) : "", originalPostName: nameStr };
-  }
+  if (nameStr) return { originalPostName: nameStr };
   if (Array.isArray(user.posts) && user.posts.length) {
-    const p0 = user.posts[0];
-    return {
-      originalPostId: p0.postId != null ? String(p0.postId) : "",
-      originalPostName: (p0.postName ?? "").toString() || "鏈懡鍚嶅矖浣�",
-    };
+    return { originalPostName: (user.posts[0].postName ?? "").toString() || "鏈懡鍚嶅矖浣�" };
   }
   const pid = firstPostId(user);
   if (pid != null && pid !== "") {
     const n = postIdToName.value[String(pid)] || "";
-    return {
-      originalPostId: String(pid),
-      originalPostName: n || "褰撳墠宀椾綅锛堟湭鍦ㄥ矖浣嶅瓧鍏镐腑锛�",
-    };
+    return { originalPostName: n || "褰撳墠宀椾綅锛堟湭鍦ㄥ矖浣嶅瓧鍏镐腑锛�" };
   }
-  return { originalPostId: "", originalPostName: "鏈垎閰嶅矖浣�" };
+  return { originalPostName: "鏈垎閰嶅矖浣�" };
 }
 
 function userById(id) {
@@ -308,353 +262,79 @@
   } catch {
     targetPostOptions.value = [];
   }
-  rebuildPostIdMap();
+  const m = {};
+  for (const p of targetPostOptions.value) {
+    const id = p.postId ?? p.value ?? p.id;
+    if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? "";
+  }
+  postIdToName.value = m;
 }
-
-/** 鏌ヨ鍖猴細涓嬫媺杩滅▼妯$硦锛堟暟鎹潵鑷� userListNoPageByTenantId锛屽墠绔繃婊わ級 */
-const applicantSearchLoading = ref(false);
-const applicantSearchOptions = ref([]);
 
 async function remoteSearchApplicant(query) {
   applicantSearchLoading.value = true;
   try {
-    if (!allUsersCache.value.length) {
-      await loadUserPool();
-    }
+    if (!allUsersCache.value.length) await loadUserPool();
     applicantSearchOptions.value = filterUsersByQuery(query);
   } finally {
     applicantSearchLoading.value = false;
   }
 }
 
-
-function unwrapArray(payload) {
-  if (Array.isArray(payload)) return payload;
-  if (payload && Array.isArray(payload.data)) return payload.data;
-  if (payload && Array.isArray(payload.rows)) return payload.rows;
-  return [];
+function syncOriginalPostFromApplicant(uid) {
+  const u = userById(uid);
+  originalPostName.value = resolveOriginalPost(u).originalPostName;
 }
-
-function isActiveUser(u) {
-  if (u.delFlag === "2" || u.delFlag === 2) return false;
-  if (u.status == null) return true;
-  return String(u.status) === "0";
-}
-
-function approvalModeLabel(mode) {
-  if (mode === "countersign") return "浼氱";
-  return "涓庣";
-}
-
-function approvalResultLabel(v) {
-  if (v === "approved") return "宸查�氳繃";
-  if (v === "rejected") return "宸查┏鍥�";
-  if (v === "cancelled") return "宸叉挙閿�";
-  return "寰呭鎵�";
-}
-
-const allRows = ref([]);
-
-const searchForm = reactive({
-  applicantId: "",
-  transferDateRange: null,
-});
-
-const tableLoading = ref(false);
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-
-const filteredList = computed(() => {
-  let list = [...allRows.value];
-  if (searchForm.applicantId) {
-    list = list.filter((r) => String(r.applicantId) === String(searchForm.applicantId));
-  }
-  const range = searchForm.transferDateRange;
-  if (range && range.length === 2) {
-    const [start, end] = range;
-    list = list.filter((r) => r.transferDate >= start && r.transferDate <= end);
-  }
-  return list.sort((a, b) => (a.transferDate < b.transferDate ? 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 list = filteredList.value;
-  const start = (page.current - 1) * page.size;
-  return list.slice(start, start + page.size);
-});
-
-const tableColumn = ref([
-  { label: "鐢宠浜�", prop: "applicantName", minWidth: 100 },
-  { label: "杞矖鏃ユ湡", prop: "transferDate", width: 120 },
-  { label: "鍘熷矖浣�", prop: "originalPostName", minWidth: 140 },
-  { label: "杞叆宀椾綅", prop: "targetPostName", minWidth: 160 },
-  {
-    label: "瀹℃壒缁撴灉",
-    prop: "approvalResult",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => approvalResultLabel(v),
-    formatType: (v) => {
-      if (v === "approved") return "success";
-      if (v === "rejected") return "danger";
-      if (v === "cancelled") return "info";
-      return "warning";
-    },
-  },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    fixed: "right",
-    width: 180,
-    operation: [
-      { name: "缂栬緫", type: "text", clickFun: (row) => openFormDialog("edit", row) },
-      { name: "鏌ョ湅璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
-    ],
-  },
-]);
-
-const formDialog = reactive({
-  visible: false,
-  title: "",
-  mode: "add",
-});
-const formRef = ref();
-const form = reactive(createEmptyForm());
-const templateBindVisible = ref(false);
-const pendingTemplateBinding = ref(null);
-const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
-
-const templateDisplayFields = computed(() =>
-  (form.formFieldDefs || []).filter((f) => !isOriginalPostField(f))
-);
-
-const applicantTemplateField = computed(() => findApplicantTemplateField(form.formFieldDefs));
-
-const formRules = computed(() => ({
-  ...buildFormPayloadRules(templateDisplayFields.value),
-  originalPostName: [{ required: true, message: "璇烽�夋嫨鐢宠浜轰互甯﹀嚭鍘熷矖浣�", trigger: "change" }],
-}));
 
 watch(
   () => {
     const key = applicantTemplateField.value?.key;
-    return key ? form.formPayload[key] : undefined;
+    return key ? submitForm.formPayload[key] : undefined;
   },
   async (uid) => {
-    if (!allUsersCache.value.length) {
-      await loadUserPool();
-    }
-    syncApplicantFromUser(uid);
+    if (!applicantTemplateField.value) return;
+    if (!allUsersCache.value.length) await loadUserPool();
+    syncOriginalPostFromApplicant(uid);
   }
 );
 
-const detailDialog = reactive({ visible: false });
-const detailRow = ref({});
+watch(
+  () => submitDialog.visible,
+  async (v) => {
+    if (!v) return;
+    const key = applicantTemplateField.value?.key;
+    if (key && submitForm.formPayload[key]) {
+      syncOriginalPostFromApplicant(submitForm.formPayload[key]);
+    }
+  }
+);
 
-function handleQuery() {
-  page.current = 1;
-  tableLoading.value = true;
-  setTimeout(() => {
-    tableLoading.value = false;
-  }, 150);
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
+
+function onSearch() {
+  handleQuery(searchForm);
 }
 
 async function resetSearch() {
   searchForm.applicantId = "";
   searchForm.transferDateRange = null;
-  handleQuery();
+  onSearch();
   await remoteSearchApplicant("");
 }
 
-function pagination(obj) {
-  page.current = obj.page;
-  page.size = obj.limit;
+function onPagination(obj) {
+  pagination(obj, searchForm);
 }
 
-function openDetail(row) {
-  detailRow.value = { ...row };
-  detailDialog.visible = true;
-}
-
-function openAddWithTemplate() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function onTemplateBound(binding) {
-  pendingTemplateBinding.value = binding;
-}
-
-async function onTemplateBindClosed() {
-  const binding = pendingTemplateBinding.value;
-  if (!binding) return;
-  pendingTemplateBinding.value = null;
-  await openFormWithBinding(binding);
-}
-
-async function openFormWithBinding(binding) {
-  Object.assign(form, createEmptyForm());
-  applyBindingToForm(form, binding);
-  form.hasTemplateBinding = true;
-  formDialog.mode = "add";
-  formDialog.title = "鏂板璋冨矖鐢宠";
-  await Promise.all([loadUserPool(), loadPostOptions(), loadFlowUsers()]);
-  const applicantKey = applicantTemplateField.value?.key;
-  if (applicantKey && form.formPayload[applicantKey]) {
-    syncApplicantFromUser(form.formPayload[applicantKey]);
-  }
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function reopenTemplateBind() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-async function openFormDialog(mode, row) {
-  if (mode === "edit" && row && !row.hasTemplateBinding) {
-    proxy?.$modal?.msgWarning?.("璇ヨ褰曚负鏃х増鏁版嵁锛岃閲嶆柊閫氳繃妯℃澘鍙戣捣鐢宠");
-    return;
-  }
-  formDialog.mode = mode;
-  formDialog.title = "缂栬緫璋冨矖鐢宠";
-  Object.assign(form, createEmptyForm());
-  if (mode === "edit" && row) {
-    Object.assign(form, {
-      id: row.id,
-      applicantId: row.applicantId,
-      applicantName: row.applicantName,
-      transferDate: row.transferDate,
-      originalPostId: row.originalPostId,
-      originalPostName: row.originalPostName,
-      targetPostId: row.targetPostId,
-      targetPostName: row.targetPostName,
-      hasTemplateBinding: true,
-      templateId: row.templateId,
-      templateName: row.templateName,
-      templateSnapshot: row.templateSnapshot,
-      formFieldDefs: row.formFieldDefs || [],
-      formPayload: JSON.parse(JSON.stringify(row.formPayload || {})),
-      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])),
-      templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])),
-      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
-    });
-    await loadUserPool();
-    const applicantKey = applicantTemplateField.value?.key;
-    if (applicantKey) {
-      syncApplicantFromUser(form.formPayload[applicantKey]);
-    }
-    loadFlowUsers();
-  }
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function onFormClosed() {
-  formRef.value?.resetFields?.();
-}
-
-async function submitForm() {
-  try {
-    await formRef.value?.validate?.();
-  } catch {
-    return;
-  }
-  const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes });
-  if (!flowCheck.ok) {
-    proxy?.$modal?.msgWarning?.(flowCheck.message || "璇峰畬鍠勫鎵规祦绋�");
-    return;
-  }
-  form.flowNodes = flowCheck.nodes;
-  const applicantKey = applicantTemplateField.value?.key;
-  if (applicantKey) {
-    syncApplicantFromUser(form.formPayload[applicantKey]);
-  }
-  syncTransferFieldsFromPayload();
-  form.targetPostName = targetPostNameById(form.targetPostId);
-  const payload = {
-    applicantId: form.applicantId,
-    applicantName: form.applicantName,
-    transferDate: form.transferDate,
-    originalPostId: form.originalPostId,
-    originalPostName: form.originalPostName,
-    targetPostId: form.targetPostId,
-    targetPostName: form.targetPostName,
-    hasTemplateBinding: true,
-    templateId: form.templateId,
-    templateName: form.templateName,
-    templateSnapshot: form.templateSnapshot,
-    formFieldDefs: form.formFieldDefs,
-    formPayload: JSON.parse(JSON.stringify(form.formPayload || {})),
-    flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])),
-    templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])),
-    storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])),
-  };
-  if (formDialog.mode === "add") {
-    const id = `local_${Date.now()}`;
-    allRows.value.unshift({
-      id,
-      ...payload,
-      approvalResult: "pending",
-    });
-    proxy?.$modal?.msgSuccess?.("鏂板鎴愬姛");
-  } else {
-    const idx = allRows.value.findIndex((r) => r.id === form.id);
-    const prev = idx !== -1 ? allRows.value[idx] : {};
-    if (idx !== -1) {
-      allRows.value[idx] = {
-        ...prev,
-        id: form.id,
-        ...payload,
-      };
-    }
-    proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
-  }
-  formDialog.visible = false;
-  handleQuery();
-}
-
-/** 浠庢ā鏉垮~鎶ラ」鍚屾杞矖鏃ユ湡銆佽浆鍏ュ矖浣嶅埌鍒楄〃瀛楁 */
-function syncTransferFieldsFromPayload() {
-  const defs = form.formFieldDefs || [];
-  const payload = form.formPayload || {};
-  for (const f of defs) {
-    const label = String(f.label || "");
-    const val = payload[f.key];
-    if (label.includes("杞矖") && (label.includes("鏃ユ湡") || label.includes("鏃堕棿")) && f.type === "date") {
-      form.transferDate = val || "";
-    }
-    if (label.includes("杞叆宀椾綅") && f.type === "select") {
-      form.targetPostId = val != null && val !== "" ? val : "";
-      form.targetPostName = targetPostNameById(form.targetPostId);
-    }
-  }
+async function onSubmit() {
+  const ok = await submitInstanceForm({ skipValidate: true });
+  if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
 }
 
 onMounted(async () => {
   await Promise.all([loadUserPool(), loadPostOptions()]);
-  rebuildPostIdMap();
   loadFlowUsers();
   await remoteSearchApplicant("");
+  await initModuleList(searchForm);
 });
 </script>
 
@@ -672,21 +352,5 @@
 .search_title {
   font-size: 14px;
   color: var(--el-text-color-regular);
-}
-.transfer-apply-form :deep(.el-row) {
-  margin-bottom: 0;
-}
-.transfer-apply-form :deep(.el-form-item) {
-  margin-bottom: 18px;
-}
-.transfer-apply-form-dialog :deep(.el-dialog__body) {
-  padding-top: 12px;
-}
-.template-name {
-  font-weight: 600;
-  color: var(--el-text-color-primary);
-}
-.ml12 {
-  margin-left: 12px;
 }
 </style>
diff --git a/src/views/officeProcessAutomation/HrManage/work-handover/index.vue b/src/views/officeProcessAutomation/HrManage/work-handover/index.vue
index ed2e8d0..9078e97 100644
--- a/src/views/officeProcessAutomation/HrManage/work-handover/index.vue
+++ b/src/views/officeProcessAutomation/HrManage/work-handover/index.vue
@@ -30,7 +30,7 @@
         <el-select v-model="searchForm.handoverType" placeholder="鍏ㄩ儴" clearable style="width: 140px">
           <el-option v-for="o in handoverTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
         </el-select>
-        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+        <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
         <el-button @click="resetSearch">閲嶇疆</el-button>
       </div>
       <div>
@@ -45,56 +45,24 @@
         :page="page"
         :isSelection="false"
         :tableLoading="tableLoading"
-        @pagination="pagination"
+        @pagination="onPagination"
         :total="page.total"
       />
     </div>
 
-    <!-- 鏂板 / 缂栬緫 -->
-    <el-dialog
-      v-if="formDialog.visible"
-      v-model="formDialog.visible"
-      :title="formDialog.title"
-      width="960px"
-      append-to-body
-      destroy-on-close
-      class="work-handover-form-dialog"
-      @closed="onFormClosed"
-    >
-      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="work-handover-form">
-        <el-form-item v-if="form.templateSnapshot" label="瀹℃壒妯℃澘">
-          <span class="template-name">{{ form.templateSnapshot.label || form.templateName }}</span>
-          <el-button
-            v-if="formDialog.mode === 'add'"
-            type="primary"
-            link
-            class="ml12"
-            @click="reopenTemplateBind"
-          >
-            鏇存崲妯℃澘
-          </el-button>
-        </el-form-item>
-        <FormPayloadFields :fields="form.formFieldDefs" :form-payload="form.formPayload" :columns="2" />
-        <ApprovalTemplateFormSection
-          :active-template="form.templateSnapshot"
-          :fields="form.formFieldDefs"
-          :form-payload="form.formPayload"
-          v-model:flow-nodes="form.flowNodes"
-          v-model:attachments="form.storageBlobDTOs"
-          :template-attachments="form.templateAttachments"
-          :user-options="flowUserOptions"
-          flow-attachments-only
-          hide-template-name
-          :allow-change-template="false"
-        />
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
-          <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceSubmitDialog
+      v-model="submitDialog.visible"
+      :title="submitDialogTitle"
+      :form="submitForm"
+      :rules="submitFormRules"
+      :fields="submitFormFields"
+      :active-template="activeTemplate"
+      :user-options="flowUserOptions"
+      :is-edit="isSubmitEdit"
+      :saving="submitSaving"
+      :form-ref="submitFormRef"
+      @submit="onSubmit"
+    />
 
     <ApprovalTemplateBindDialog
       v-model:visible="templateBindVisible"
@@ -104,42 +72,26 @@
       @closed="onTemplateBindClosed"
     />
 
-    <!-- 璇︽儏 -->
-    <el-dialog v-model="detailDialog.visible" title="宸ヤ綔浜ゆ帴璇︽儏" width="560px" append-to-body>
-      <el-descriptions :column="1" border>
-        <el-descriptions-item label="鐢宠浜�">{{ detailRow.applicantName }}</el-descriptions-item>
-        <el-descriptions-item label="绂昏亴鏃ユ湡">{{ detailRow.leaveDate || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="浜ゆ帴鐘舵��">{{ handoverStatusLabel(detailRow.handoverStatus) }}</el-descriptions-item>
-        <el-descriptions-item label="浜ゆ帴绫诲瀷">{{ handoverTypeLabel(detailRow.handoverType) }}</el-descriptions-item>
-        <el-descriptions-item label="浜ゆ帴浜�">{{ detailRow.handoverPersonName || "鈥�" }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒缁撴灉">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒鏂瑰紡">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
-        <el-descriptions-item label="瀹℃壒浜�">{{ detailRow.approverNames || "鈥�" }}</el-descriptions-item>
-      </el-descriptions>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <ApprovalInstanceDetailDialog
+      v-model="detailDialog.visible"
+      title="宸ヤ綔浜ゆ帴璇︽儏"
+      :row="detailRow"
+      @edit="openEditFromDetail"
+    />
   </div>
 </template>
 
 <script setup>
 import { userListNoPageByTenantId } from "@/api/system/user.js";
-import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { onMounted, reactive, ref } from "vue";
+import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
+import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
 import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
-import ApprovalTemplateFormSection from "../../ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue";
-import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue";
+import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
 import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
-import {
-  applyBindingToForm,
-  buildFormPayloadRules,
-  validateTemplateBinding,
-} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
 import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
-
-const { proxy } = getCurrentInstance();
 
 const handoverStatusOptions = [
   { value: "in_progress", label: "杩涜涓�" },
@@ -152,84 +104,48 @@
   { value: "transfer", label: "璋冨矖浜ゆ帴" },
 ];
 
-function handoverStatusLabel(v) {
-  return handoverStatusOptions.find((o) => o.value === v)?.label || "鈥�";
-}
-
-function handoverTypeLabel(v) {
-  return handoverTypeOptions.find((o) => o.value === v)?.label || "鈥�";
-}
-
-/** 涓庡悗绔害瀹氬瓧娈碉紙鏈湴鍗犱綅锛屽悗鏈熸帴鍙e榻愶級 */
-const createEmptyForm = () => ({
-  id: undefined,
+const searchForm = reactive({
   applicantId: "",
-  applicantName: "",
-  leaveDate: "",
-  handoverStatus: "in_progress",
-  handoverType: "resignation",
-  handoverPersonId: "",
-  handoverPersonName: "",
-  hasTemplateBinding: false,
-  templateId: "",
-  templateName: "",
-  templateSnapshot: null,
-  formFieldDefs: [],
-  formPayload: {},
-  flowNodes: [],
-  templateAttachments: [],
-  storageBlobDTOs: [],
+  handoverStatus: "",
+  handoverType: "",
 });
 
+const mod = useApprovalInstanceModule({
+  moduleKey: APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+});
+
+const {
+  tableData,
+  tableLoading,
+  page,
+  detailDialog,
+  detailRow,
+  submitDialog,
+  submitForm,
+  submitFormRef,
+  submitSaving,
+  isSubmitEdit,
+  activeTemplate,
+  submitFormFields,
+  submitFormRules,
+  submitDialogTitle,
+  templateBindVisible,
+  handleQuery,
+  initModuleList,
+  pagination,
+  openAddWithTemplate,
+  onTemplateBound,
+  onTemplateBindClosed,
+  openEditFromDetail,
+  submitInstanceForm,
+  buildTableActions,
+} = mod;
+
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+
 const allUsersCache = ref([]);
-
-function userSelectLabel(u) {
-  const nick = u.nickName || "";
-  const name = u.userName || "";
-  if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
-  return nick || name || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
-}
-
-function userById(id) {
-  if (id == null || id === "") return undefined;
-  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
-}
-
-function filterUsersByQuery(query) {
-  const list = allUsersCache.value.filter((u) => isActiveUser(u));
-  const q = (query || "").trim().toLowerCase();
-  if (!q) return [...list];
-  return list.filter((u) => {
-    const nick = (u.nickName || "").toLowerCase();
-    const uname = (u.userName || "").toLowerCase();
-    const phone = (u.phonenumber || u.phone || "").toString();
-    return nick.includes(q) || uname.includes(q) || phone.includes(q);
-  });
-}
-
-async function loadUserPool() {
-  try {
-    const res = await userListNoPageByTenantId();
-    allUsersCache.value = unwrapArray(res);
-  } catch {
-    allUsersCache.value = [];
-  }
-}
-
-const applicantSearchLoading = ref(false);
 const applicantSearchOptions = ref([]);
-
-async function remoteSearchApplicant(query) {
-  applicantSearchLoading.value = true;
-  try {
-    if (!allUsersCache.value.length) {
-      await loadUserPool();
-    }
-    applicantSearchOptions.value = filterUsersByQuery(query);
-  } finally {
-    applicantSearchLoading.value = false;
-  }
-}
+const applicantSearchLoading = ref(false);
 
 function unwrapArray(payload) {
   if (Array.isArray(payload)) return payload;
@@ -244,330 +160,74 @@
   return String(u.status) === "0";
 }
 
-function approvalModeLabel(mode) {
-  if (mode === "countersign") return "浼氱";
-  return "涓庣";
+function userSelectLabel(u) {
+  const nick = u.nickName || "";
+  const name = u.userName || "";
+  if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+  return nick || name || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
 }
 
-function approvalResultLabel(v) {
-  if (v === "approved") return "宸查�氳繃";
-  if (v === "rejected") return "宸查┏鍥�";
-  if (v === "cancelled") return "宸叉挙閿�";
-  return "寰呭鎵�";
+function filterUsersByQuery(query) {
+  const list = allUsersCache.value.filter((u) => isActiveUser(u));
+  const q = (query || "").trim().toLowerCase();
+  if (!q) return list.slice(0, 50);
+  return list
+    .filter((u) => {
+      const nick = (u.nickName || "").toLowerCase();
+      const name = (u.userName || "").toLowerCase();
+      const id = String(u.userId ?? u.id ?? "");
+      return nick.includes(q) || name.includes(q) || id.includes(q);
+    })
+    .slice(0, 50);
 }
 
-function handoverStatusTagType(v) {
-  if (v === "completed") return "success";
-  if (v === "returned") return "danger";
-  return "warning";
-}
-
-function handoverTypeTagType(v) {
-  return v === "transfer" ? "info" : "";
-}
-
-const allRows = ref([]);
-
-const searchForm = reactive({
-  applicantId: "",
-  handoverStatus: "",
-  handoverType: "",
-});
-
-const tableLoading = ref(false);
-const page = reactive({
-  current: 1,
-  size: 10,
-  total: 0,
-});
-
-const filteredList = computed(() => {
-  let list = [...allRows.value];
-  if (searchForm.applicantId) {
-    list = list.filter((r) => String(r.applicantId) === String(searchForm.applicantId));
+async function loadUserPool() {
+  try {
+    const res = await userListNoPageByTenantId();
+    allUsersCache.value = unwrapArray(res);
+  } catch {
+    allUsersCache.value = [];
   }
-  if (searchForm.handoverStatus) {
-    list = list.filter((r) => r.handoverStatus === searchForm.handoverStatus);
+}
+
+async function remoteSearchApplicant(query) {
+  applicantSearchLoading.value = true;
+  try {
+    if (!allUsersCache.value.length) await loadUserPool();
+    applicantSearchOptions.value = filterUsersByQuery(query);
+  } finally {
+    applicantSearchLoading.value = false;
   }
-  if (searchForm.handoverType) {
-    list = list.filter((r) => r.handoverType === searchForm.handoverType);
-  }
-  return list.sort((a, b) => (a.leaveDate < b.leaveDate ? 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 tableColumn = buildInstanceTableColumns(tableData, buildTableActions);
 
-const tableData = computed(() => {
-  const list = filteredList.value;
-  const start = (page.current - 1) * page.size;
-  return list.slice(start, start + page.size);
-});
-
-const tableColumn = ref([
-  { label: "鐢宠浜�", prop: "applicantName", minWidth: 100 },
-  { label: "绂昏亴鏃ユ湡", prop: "leaveDate", width: 120 },
-  {
-    label: "浜ゆ帴鐘舵��",
-    prop: "handoverStatus",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => handoverStatusLabel(v),
-    formatType: (v) => handoverStatusTagType(v),
-  },
-  {
-    label: "浜ゆ帴绫诲瀷",
-    prop: "handoverType",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => handoverTypeLabel(v),
-    formatType: (v) => handoverTypeTagType(v),
-  },
-  { label: "浜ゆ帴浜�", prop: "handoverPersonName", minWidth: 100 },
-  {
-    label: "瀹℃壒缁撴灉",
-    prop: "approvalResult",
-    width: 110,
-    dataType: "tag",
-    formatData: (v) => approvalResultLabel(v),
-    formatType: (v) => {
-      if (v === "approved") return "success";
-      if (v === "rejected") return "danger";
-      if (v === "cancelled") return "info";
-      return "warning";
-    },
-  },
-  {
-    dataType: "action",
-    label: "鎿嶄綔",
-    align: "center",
-    fixed: "right",
-    width: 200,
-    operation: [
-      { name: "缂栬緫", type: "text", clickFun: (row) => openFormDialog("edit", row) },
-      { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
-    ],
-  },
-]);
-
-const formDialog = reactive({
-  visible: false,
-  title: "",
-  mode: "add",
-});
-const formRef = ref();
-const form = reactive(createEmptyForm());
-const templateBindVisible = ref(false);
-const pendingTemplateBinding = ref(null);
-const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
-
-const formRules = computed(() => buildFormPayloadRules(form.formFieldDefs));
-
-const detailDialog = reactive({ visible: false });
-const detailRow = ref({});
-
-function handleQuery() {
-  page.current = 1;
-  tableLoading.value = true;
-  setTimeout(() => {
-    tableLoading.value = false;
-  }, 150);
+function onSearch() {
+  handleQuery(searchForm);
 }
 
 async function resetSearch() {
   searchForm.applicantId = "";
   searchForm.handoverStatus = "";
   searchForm.handoverType = "";
-  handleQuery();
+  onSearch();
   await remoteSearchApplicant("");
 }
 
-function pagination(obj) {
-  page.current = obj.page;
-  page.size = obj.limit;
+function onPagination(obj) {
+  pagination(obj, searchForm);
 }
 
-function openDetail(row) {
-  detailRow.value = { ...row };
-  detailDialog.visible = true;
-}
-
-function openAddWithTemplate() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function onTemplateBound(binding) {
-  pendingTemplateBinding.value = binding;
-}
-
-async function onTemplateBindClosed() {
-  const binding = pendingTemplateBinding.value;
-  if (!binding) return;
-  pendingTemplateBinding.value = null;
-  await openFormWithBinding(binding);
-}
-
-async function openFormWithBinding(binding) {
-  Object.assign(form, createEmptyForm());
-  applyBindingToForm(form, binding);
-  form.hasTemplateBinding = true;
-  formDialog.mode = "add";
-  formDialog.title = "鏂板宸ヤ綔浜ゆ帴";
-  await Promise.all([loadUserPool(), loadFlowUsers()]);
-  await syncHandoverFieldsFromPayload();
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function reopenTemplateBind() {
-  formDialog.visible = false;
-  pendingTemplateBinding.value = null;
-  templateBindVisible.value = true;
-}
-
-function syncApplicantFromUser(uid) {
-  const u = userById(uid);
-  form.applicantId = uid != null && uid !== "" ? uid : "";
-  form.applicantName = u ? u.nickName || u.userName || "" : "";
-}
-
-function syncHandoverPersonFromUser(uid) {
-  const u = userById(uid);
-  form.handoverPersonId = uid != null && uid !== "" ? uid : "";
-  form.handoverPersonName = u ? u.nickName || u.userName || "" : "";
-}
-
-/** 浠庢ā鏉垮~鎶ラ」鍚屾鍒楄〃灞曠ず瀛楁 */
-async function syncHandoverFieldsFromPayload() {
-  const defs = form.formFieldDefs || [];
-  const payload = form.formPayload || {};
-  for (const f of defs) {
-    const label = String(f.label || "");
-    const val = payload[f.key];
-    if (label.includes("鐢宠浜�") && !label.includes("浜ゆ帴浜�")) {
-      syncApplicantFromUser(val);
-    } else if (label.includes("浜ゆ帴浜�")) {
-      syncHandoverPersonFromUser(val);
-    } else if (label.includes("绂昏亴") && f.type === "date") {
-      form.leaveDate = val || "";
-    } else if (label.includes("浜ゆ帴鐘舵��")) {
-      form.handoverStatus = val != null && val !== "" ? val : form.handoverStatus;
-    } else if (label.includes("浜ゆ帴绫诲瀷")) {
-      form.handoverType = val != null && val !== "" ? val : form.handoverType;
-    }
-  }
-}
-
-async function openFormDialog(mode, row) {
-  if (mode === "edit" && row && !row.hasTemplateBinding) {
-    proxy?.$modal?.msgWarning?.("璇ヨ褰曚负鏃х増鏁版嵁锛岃閲嶆柊閫氳繃妯℃澘鍙戣捣鐢宠");
-    return;
-  }
-  formDialog.mode = mode;
-  formDialog.title = "缂栬緫宸ヤ綔浜ゆ帴";
-  Object.assign(form, createEmptyForm());
-  if (mode === "edit" && row) {
-    Object.assign(form, {
-      id: row.id,
-      applicantId: row.applicantId,
-      applicantName: row.applicantName,
-      leaveDate: row.leaveDate,
-      handoverStatus: row.handoverStatus,
-      handoverType: row.handoverType,
-      handoverPersonId: row.handoverPersonId,
-      handoverPersonName: row.handoverPersonName,
-      hasTemplateBinding: true,
-      templateId: row.templateId,
-      templateName: row.templateName,
-      templateSnapshot: row.templateSnapshot,
-      formFieldDefs: row.formFieldDefs || [],
-      formPayload: JSON.parse(JSON.stringify(row.formPayload || {})),
-      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [])),
-      templateAttachments: JSON.parse(JSON.stringify(row.templateAttachments || [])),
-      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
-    });
-    await loadUserPool();
-    await syncHandoverFieldsFromPayload();
-    loadFlowUsers();
-  }
-  formDialog.visible = true;
-  nextTick(() => formRef.value?.clearValidate?.());
-}
-
-function onFormClosed() {
-  formRef.value?.resetFields?.();
-}
-
-async function submitForm() {
-  try {
-    await formRef.value?.validate?.();
-  } catch {
-    return;
-  }
-  const flowCheck = validateTemplateBinding({ flowNodes: form.flowNodes });
-  if (!flowCheck.ok) {
-    proxy?.$modal?.msgWarning?.(flowCheck.message || "璇峰畬鍠勫鎵规祦绋�");
-    return;
-  }
-  form.flowNodes = flowCheck.nodes;
-  await syncHandoverFieldsFromPayload();
-  const payload = {
-    applicantId: form.applicantId,
-    applicantName: form.applicantName,
-    leaveDate: form.leaveDate,
-    handoverStatus: form.handoverStatus,
-    handoverType: form.handoverType,
-    handoverPersonId: form.handoverPersonId,
-    handoverPersonName: form.handoverPersonName,
-    hasTemplateBinding: true,
-    templateId: form.templateId,
-    templateName: form.templateName,
-    templateSnapshot: form.templateSnapshot,
-    formFieldDefs: form.formFieldDefs,
-    formPayload: JSON.parse(JSON.stringify(form.formPayload || {})),
-    flowNodes: JSON.parse(JSON.stringify(form.flowNodes || [])),
-    templateAttachments: JSON.parse(JSON.stringify(form.templateAttachments || [])),
-    storageBlobDTOs: JSON.parse(JSON.stringify(form.storageBlobDTOs || [])),
-  };
-  if (formDialog.mode === "add") {
-    const id = `local_${Date.now()}`;
-    allRows.value.unshift({
-      id,
-      ...payload,
-      approvalResult: "pending",
-    });
-    proxy?.$modal?.msgSuccess?.("鏂板鎴愬姛");
-  } else {
-    const idx = allRows.value.findIndex((r) => r.id === form.id);
-    const prev = idx !== -1 ? allRows.value[idx] : {};
-    if (idx !== -1) {
-      allRows.value[idx] = {
-        ...prev,
-        id: form.id,
-        ...payload,
-      };
-    }
-    proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
-  }
-  formDialog.visible = false;
-  handleQuery();
+async function onSubmit() {
+  const ok = await submitInstanceForm({ skipValidate: true });
+  if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
 }
 
 onMounted(async () => {
   await loadUserPool();
   loadFlowUsers();
   await remoteSearchApplicant("");
+  await initModuleList(searchForm);
 });
 </script>
 
@@ -585,21 +245,5 @@
 .search_title {
   font-size: 14px;
   color: var(--el-text-color-regular);
-}
-.work-handover-form :deep(.el-row) {
-  margin-bottom: 0;
-}
-.work-handover-form :deep(.el-form-item) {
-  margin-bottom: 18px;
-}
-.work-handover-form-dialog :deep(.el-dialog__body) {
-  padding-top: 12px;
-}
-.template-name {
-  font-weight: 600;
-  color: var(--el-text-color-primary);
-}
-.ml12 {
-  margin-left: 12px;
 }
 </style>

--
Gitblit v1.9.3