From 2256ff02d95fd71e1522bc9422356774b701c153 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期二, 19 五月 2026 14:24:55 +0800
Subject: [PATCH] OA页面/审批模板

---
 src/config/oaPaths.js                                   |   41 
 src/pages.json                                          |  133 ++
 src/pages/oa/ApproveManage/approve-list/index.vue       |   18 
 src/pages/oa/_components/OaListPage.vue                 |  182 +++
 src/pages/oa/_utils/useOaPage.js                        |    6 
 src/pages/indexItem.vue                                 |    6 
 src/pages/oa/_utils/oaUi.js                             |   13 
 src/pages/oa/HrManage/work-handover/index.vue           |   18 
 src/pages/oa/HrManage/post-manage/index.vue             |   18 
 src/pages/oa/ApproveManage/approve-template/edit.vue    | 1567 +++++++++++++++++++++++++++
 src/pages/oa/EnterpriseNews/news-manage/index.vue       |   18 
 src/pages/oa/HrManage/regular-apply/index.vue           |   18 
 src/pages/oa/ApproveManage/approve-template/detail.vue  |  418 +++++++
 src/pages/oa/ApproveManage/approve-template/index.vue   |  304 +++++
 src/pages/oa/HrManage/staff-archive/index.vue           |   18 
 src/pages/oa/NoticeAnnouncement/notice-manage/index.vue |   18 
 src/api/oa/approvalTemplate.js                          |   45 
 src/pages/oa/ContractManage/sale-contract/index.vue     |   26 
 src/pages/oa/_utils/oaPageRegistry.js                   |  256 ++++
 src/pages/oa/AttendManage/overtime-apply/index.vue      |   18 
 src/pages/oa/HrManage/staff-contract/index.vue          |   18 
 src/pages/oa/ReimburseManage/travel-reimburse/index.vue |   18 
 src/pages/works.vue                                     |   42 
 src/pages/oa/_utils/oaStorage.js                        |   26 
 src/pages/oa/AttendManage/leave-apply/index.vue         |   18 
 src/pages/oa/HrManage/resign-apply/index.vue            |   18 
 src/pages/oa/ContractManage/purchase-contract/index.vue |   26 
 src/pages/oa/HrManage/transfer-apply/index.vue          |   18 
 src/config/oaWorkbench.js                               |   75 +
 src/pages/oa/ReimburseManage/cost-reimburse/index.vue   |   18 
 30 files changed, 3,418 insertions(+), 0 deletions(-)

diff --git a/src/api/oa/approvalTemplate.js b/src/api/oa/approvalTemplate.js
new file mode 100644
index 0000000..5d34aea
--- /dev/null
+++ b/src/api/oa/approvalTemplate.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒妯℃澘鍒嗛〉鏌ヨ */
+export function listApprovalTemplatePage(params) {
+  return request({
+    url: "/approvalTemplate/listPage",
+    method: "get",
+    params,
+  });
+}
+
+/** 瀹℃壒妯℃澘璇︽儏 */
+export function getApprovalTemplateDetail(id) {
+  return request({
+    url: `/approvalTemplate/detail/${id}`,
+    method: "get",
+  });
+}
+
+/** 鏂板瀹℃壒妯℃澘 */
+export function addApprovalTemplate(data) {
+  return request({
+    url: "/approvalTemplate/add",
+    method: "post",
+    data,
+  });
+}
+
+/** 淇敼瀹℃壒妯℃澘 */
+export function updateApprovalTemplate(data) {
+  return request({
+    url: "/approvalTemplate/update",
+    method: "put",
+    data,
+  });
+}
+
+/** 鍒犻櫎瀹℃壒妯℃澘锛堜紶 ID 鏁扮粍锛� */
+export function deleteApprovalTemplate(ids) {
+  return request({
+    url: "/approvalTemplate/delete",
+    method: "post",
+    data: ids,
+  });
+}
diff --git a/src/config/oaPaths.js b/src/config/oaPaths.js
new file mode 100644
index 0000000..03246bf
--- /dev/null
+++ b/src/config/oaPaths.js
@@ -0,0 +1,41 @@
+/**
+ * OA 妯″潡璺緞甯搁噺锛坧ages.json path 涓嶅惈鍓嶇紑 /锛�
+ * 瀵艰埅浣跨敤锛歶ni.navigateTo({ url: OA_NAV.xxx })
+ */
+const P = "pages/oa";
+
+export const OA_NAV = {
+  /** 浜轰簨绠$悊 */
+  staffArchive: `/${P}/HrManage/staff-archive/index`,
+  staffContract: `/${P}/HrManage/staff-contract/index`,
+  regularApply: `/${P}/HrManage/regular-apply/index`,
+  transferApply: `/${P}/HrManage/transfer-apply/index`,
+  resignApply: `/${P}/HrManage/resign-apply/index`,
+  workHandover: `/${P}/HrManage/work-handover/index`,
+  postManage: `/${P}/HrManage/post-manage/index`,
+  /** 鍋囧嫟绠$悊 */
+  leaveApply: `/${P}/AttendManage/leave-apply/index`,
+  overtimeApply: `/${P}/AttendManage/overtime-apply/index`,
+  /** 鎶ラ攢绠$悊 */
+  travelReimburse: `/${P}/ReimburseManage/travel-reimburse/index`,
+  costReimburse: `/${P}/ReimburseManage/cost-reimburse/index`,
+  /** 鍚堝悓绠$悊 */
+  purchaseContract: `/${P}/ContractManage/purchase-contract/index`,
+  saleContract: `/${P}/ContractManage/sale-contract/index`,
+  /** 瀹℃壒绠$悊 */
+  approveList: `/${P}/ApproveManage/approve-list/index`,
+  approveTemplate: `/${P}/ApproveManage/approve-template/index`,
+  approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`,
+  approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`,
+  /** 浼佷笟鏂伴椈 / 鍏憡閫氱煡 */
+  enterpriseNews: `/${P}/EnterpriseNews/news-manage/index`,
+  noticeAnnouncement: `/${P}/NoticeAnnouncement/notice-manage/index`,
+};
+
+/** pages.json 娉ㄥ唽鐢� path锛堟棤 / 鍓嶇紑锛� */
+export const OA_PAGE_PATHS = Object.fromEntries(
+  Object.entries(OA_NAV).map(([key, url]) => [
+    key,
+    url.replace(/^\//, ""),
+  ])
+);
diff --git a/src/config/oaWorkbench.js b/src/config/oaWorkbench.js
new file mode 100644
index 0000000..78d8689
--- /dev/null
+++ b/src/config/oaWorkbench.js
@@ -0,0 +1,75 @@
+import { OA_NAV } from "./oaPaths.js";
+
+/**
+ * OA 妯″潡鍒嗙粍锛堝伐浣滃彴灞曠ず / 鏂囨。瀵圭収锛�
+ */
+export const OA_MODULES = [
+  {
+    key: "HrManage",
+    name: "浜轰簨绠$悊",
+    children: [
+      { label: "鍛樺伐妗f", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.staffArchive },
+      { label: "鍛樺伐鍚堝悓", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.staffContract },
+      { label: "杞鐢宠", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.regularApply },
+      { label: "璋冨矖鐢宠", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.transferApply },
+      { label: "绂昏亴鐢宠", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.resignApply },
+      { label: "宸ヤ綔浜ゆ帴", icon: "/static/images/icon/gongchuguanli.svg", path: OA_NAV.workHandover },
+      { label: "宀椾綅绠$悊", icon: "/static/images/icon/gongxuguanli.svg", path: OA_NAV.postManage },
+    ],
+  },
+  {
+    key: "AttendManage",
+    name: "鍋囧嫟绠$悊",
+    children: [
+      { label: "璇峰亣鐢宠", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.leaveApply },
+      { label: "鍔犵彮鐢宠", icon: "/static/images/icon/dakaqiandao.svg", path: OA_NAV.overtimeApply },
+    ],
+  },
+  {
+    key: "ReimburseManage",
+    name: "鎶ラ攢绠$悊",
+    children: [
+      { label: "宸梾鎶ラ攢", icon: "/static/images/icon/chuchaiguanli.svg", path: OA_NAV.travelReimburse },
+      { label: "璐圭敤鎶ラ攢", icon: "/static/images/icon/baoxiaoguanli.svg", path: OA_NAV.costReimburse },
+    ],
+  },
+  {
+    key: "ContractManage",
+    name: "鍚堝悓绠$悊",
+    children: [
+      { label: "閲囪喘鍚堝悓", icon: "/static/images/icon/caigoutaizhang.svg", path: OA_NAV.purchaseContract },
+      { label: "閿�鍞悎鍚�", icon: "/static/images/icon/xiaoshoutaizhang.svg", path: OA_NAV.saleContract },
+    ],
+  },
+  {
+    key: "ApproveManage",
+    name: "瀹℃壒绠$悊",
+    children: [
+      { label: "瀹℃壒鍒楄〃", icon: "/static/images/icon/xietongshenpi.svg", path: OA_NAV.approveList },
+      { label: "瀹℃壒妯℃澘", icon: "/static/images/icon/guizhangzhidu.svg", path: OA_NAV.approveTemplate },
+    ],
+  },
+  {
+    key: "EnterpriseNews",
+    name: "浼佷笟鏂伴椈",
+    children: [
+      { label: "浼佷笟鏂伴椈", icon: "/static/images/icon/zhishiku.svg", path: OA_NAV.enterpriseNews },
+    ],
+  },
+  {
+    key: "NoticeAnnouncement",
+    name: "鍏憡閫氱煡",
+    children: [
+      { label: "鍏憡閫氱煡", icon: "/static/images/icon/tongzhigonggao.svg", path: OA_NAV.noticeAnnouncement },
+    ],
+  },
+];
+
+/** 宸ヤ綔鍙版墎骞宠彍鍗曪紙绾墠绔厤缃級 */
+export const OA_WORKBENCH_ITEMS = OA_MODULES.flatMap(module =>
+  module.children.map(item => ({
+    ...item,
+    module: module.name,
+    moduleKey: module.key,
+  }))
+);
diff --git a/src/pages.json b/src/pages.json
index b3d204b..7a4dc16 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -1304,6 +1304,139 @@
         "navigationBarTitleText": "褰掕繕鐧昏",
         "navigationStyle": "custom"
       }
+    },
+    {
+      "path": "pages/oa/HrManage/staff-archive/index",
+      "style": {
+        "navigationBarTitleText": "鍛樺伐妗f",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/HrManage/staff-contract/index",
+      "style": {
+        "navigationBarTitleText": "鍛樺伐鍚堝悓",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/HrManage/regular-apply/index",
+      "style": {
+        "navigationBarTitleText": "杞鐢宠",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/HrManage/transfer-apply/index",
+      "style": {
+        "navigationBarTitleText": "璋冨矖鐢宠",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/HrManage/resign-apply/index",
+      "style": {
+        "navigationBarTitleText": "绂昏亴鐢宠",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/HrManage/work-handover/index",
+      "style": {
+        "navigationBarTitleText": "宸ヤ綔浜ゆ帴",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/HrManage/post-manage/index",
+      "style": {
+        "navigationBarTitleText": "宀椾綅绠$悊",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/AttendManage/leave-apply/index",
+      "style": {
+        "navigationBarTitleText": "璇峰亣鐢宠",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/AttendManage/overtime-apply/index",
+      "style": {
+        "navigationBarTitleText": "鍔犵彮鐢宠",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ReimburseManage/travel-reimburse/index",
+      "style": {
+        "navigationBarTitleText": "宸梾鎶ラ攢",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ReimburseManage/cost-reimburse/index",
+      "style": {
+        "navigationBarTitleText": "璐圭敤鎶ラ攢",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ContractManage/purchase-contract/index",
+      "style": {
+        "navigationBarTitleText": "閲囪喘鍚堝悓",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ContractManage/sale-contract/index",
+      "style": {
+        "navigationBarTitleText": "閿�鍞悎鍚�",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ApproveManage/approve-list/index",
+      "style": {
+        "navigationBarTitleText": "瀹℃壒鍒楄〃",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ApproveManage/approve-template/index",
+      "style": {
+        "navigationBarTitleText": "瀹℃壒妯℃澘",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ApproveManage/approve-template/edit",
+      "style": {
+        "navigationBarTitleText": "鏂板缓瀹℃壒妯℃澘",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ApproveManage/approve-template/detail",
+      "style": {
+        "navigationBarTitleText": "妯℃澘璇︽儏",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/EnterpriseNews/news-manage/index",
+      "style": {
+        "navigationBarTitleText": "浼佷笟鏂伴椈",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/NoticeAnnouncement/notice-manage/index",
+      "style": {
+        "navigationBarTitleText": "鍏憡閫氱煡",
+        "navigationStyle": "custom"
+      }
     }
   ],
   "subPackages": [
diff --git a/src/pages/indexItem.vue b/src/pages/indexItem.vue
index 61f6c15..35d13cf 100644
--- a/src/pages/indexItem.vue
+++ b/src/pages/indexItem.vue
@@ -27,6 +27,7 @@
 
 <script setup>
   import { onMounted, reactive, ref } from "vue";
+  import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js";
   import useUserStore from "@/store/modules/user";
   import { onLoad } from "@dcloudio/uni-app";
   import PageHeader from "@/components/PageHeader.vue";
@@ -110,10 +111,15 @@
       { icon: "/static/images/icon/baojiaguanli.svg", label: "鎶ヤ环瀹℃壒" },
       { icon: "/static/images/icon/fahuoguanli.svg", label: "鍙戣揣瀹℃壒" },
     ],
+    "OA鍔炲叕": OA_WORKBENCH_ITEMS.map(item => ({ ...item })),
   };
 
   // 澶勭悊甯哥敤鍔熻兘鐐瑰嚮
   const handleCommonItemClick = item => {
+    if (item.path) {
+      uni.navigateTo({ url: item.path });
+      return;
+    }
     const url = routeMapping[item.label];
     if (url) {
       uni.navigateTo({ url });
diff --git a/src/pages/oa/ApproveManage/approve-list/index.vue b/src/pages/oa/ApproveManage/approve-list/index.vue
new file mode 100644
index 0000000..fd00bae
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 瀹℃壒绠$悊 / 瀹℃壒鍒楄〃
+  璺敱锛�/pages/oa/ApproveManage/approve-list/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 瀹℃壒绠$悊 - 瀹℃壒鍒楄〃 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "ApproveManage/approve-list";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/ApproveManage/approve-template/detail.vue b/src/pages/oa/ApproveManage/approve-template/detail.vue
new file mode 100644
index 0000000..804ddd9
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-template/detail.vue
@@ -0,0 +1,418 @@
+<!--
+  OA / 瀹℃壒绠$悊 / 瀹℃壒妯℃澘璇︽儏
+  璺敱锛�/pages/oa/ApproveManage/approve-template/detail
+-->
+<template>
+  <view class="template-detail-page">
+    <PageHeader title="妯℃澘璇︽儏"
+                @back="goBack" />
+
+    <scroll-view class="detail-scroll"
+                 scroll-y
+                 :show-scrollbar="false">
+      <view v-if="loading"
+            class="loading-wrap">
+        <up-loading-icon mode="circle" />
+        <text class="loading-text">鍔犺浇涓�...</text>
+      </view>
+      <template v-else-if="detail">
+        <view class="section">
+          <view class="section-title">鍩烘湰淇℃伅</view>
+          <view class="info-list">
+            <view class="info-item">
+              <text class="info-label">妯℃澘鍚嶇О</text>
+              <text class="info-value">{{ detail.templateName || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">妯℃澘绫诲瀷</text>
+              <text class="info-value">{{ templateTypeText(detail.templateType) }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">鍚敤鐘舵��</text>
+              <text class="info-value"
+                    :class="enabledClass(detail.enabled)">
+                {{ enabledText(detail.enabled) }}
+              </text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">妯℃澘璇存槑</text>
+              <text class="info-value">{{ detail.description || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">鍒涘缓浜�</text>
+              <text class="info-value">{{ detail.createdUserName || "-" }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">鍒涘缓鏃堕棿</text>
+              <text class="info-value">{{ detail.createTime || "-" }}</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="section">
+          <view class="section-title">濉姤閰嶇疆</view>
+          <view class="info-list">
+            <view class="info-item">
+              <text class="info-label">濉姤鎻愮ず</text>
+              <text class="info-value">{{ formConfigData.prompt || "-" }}</text>
+            </view>
+          </view>
+          <view v-if="formConfigData.fields.length"
+                class="field-block">
+            <view v-for="(field, index) in formConfigData.fields"
+                  :key="field.key || index"
+                  class="field-card">
+              <view class="field-card-head">
+                <text class="field-card-name">{{ field.label }}</text>
+                <text class="field-tag">{{ fieldTypeLabel(field.type) }}</text>
+                <text v-if="field.required"
+                      class="field-tag field-tag--req">蹇呭~</text>
+              </view>
+              <text v-if="field.defaultValue"
+                    class="field-card-default">
+                榛樿鍊硷細{{ field.defaultValue }}
+              </text>
+            </view>
+          </view>
+          <view v-else
+                class="empty-hint">鏆傛棤濉姤椤�</view>
+        </view>
+
+        <view class="section">
+          <view class="section-title">瀹℃壒娴佺▼</view>
+          <view v-if="detail.nodes?.length"
+                class="flow-list">
+            <view v-for="(node, index) in detail.nodes"
+                  :key="node.id || index"
+                  class="flow-card">
+              <view class="flow-card-head">
+                <text class="flow-level">绗瑊{ levelLabel(node.levelNo || index + 1) }}绾�</text>
+                <text class="flow-type">{{ approveTypeText(node.approveType) }}</text>
+              </view>
+              <view class="approver-tags">
+                <text v-for="(approver, aIdx) in node.approvers || []"
+                      :key="approver.id || aIdx"
+                      class="approver-tag">
+                  {{ approver.approverName || "-" }}
+                </text>
+                <text v-if="!(node.approvers || []).length"
+                      class="empty-hint inline">鏆傛棤瀹℃壒浜�</text>
+              </view>
+            </view>
+          </view>
+          <view v-else
+                class="empty-hint">鏆傛棤瀹℃壒鑺傜偣</view>
+        </view>
+      </template>
+      <view v-else
+            class="empty-wrap">
+        <up-empty mode="data"
+                  text="鏈幏鍙栧埌妯℃澘璇︽儏" />
+      </view>
+    </scroll-view>
+
+    <FooterButtons v-if="!loading && detail"
+                   cancel-text="杩斿洖"
+                   confirm-text="缂栬緫"
+                   @cancel="goBack"
+                   @confirm="goEdit" />
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref } from "vue";
+  import { onLoad } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
+
+  const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+  const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+  const FIELD_TYPE_MAP = {
+    text: "鍗曡鏂囨湰",
+    textarea: "澶氳鏂囨湰",
+    number: "鏁板瓧",
+    date: "鏃ユ湡",
+  };
+
+  const templateId = ref("");
+  const detail = ref(null);
+  const loading = ref(false);
+
+  const formConfigData = computed(() => {
+    const raw = detail.value?.formConfig;
+    if (!raw) return { prompt: "", fields: [] };
+    try {
+      const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
+      return {
+        prompt: obj?.prompt || "",
+        fields: Array.isArray(obj?.fields) ? obj.fields : [],
+      };
+    } catch {
+      return { prompt: "", fields: [] };
+    }
+  });
+
+  const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+
+  const templateTypeText = type => {
+    const val = Number(type);
+    if (val === 0) return "绯荤粺鍐呯疆";
+    if (val === 1) return "鑷畾涔�";
+    return "-";
+  };
+
+  const enabledText = enabled => {
+    const val = String(enabled ?? "");
+    if (val === "1") return "鍚敤";
+    if (val === "0") return "鍋滅敤";
+    return "-";
+  };
+
+  const enabledClass = enabled => {
+    const val = String(enabled ?? "");
+    if (val === "1") return "status-on";
+    if (val === "0") return "status-off";
+    return "";
+  };
+
+  const fieldTypeLabel = type => FIELD_TYPE_MAP[type] || type || "-";
+
+  const approveTypeText = type => (type === "OR" ? "鎴栫" : "浼氱");
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goEdit = () => {
+    if (!templateId.value || !detail.value) return;
+    uni.setStorageSync(EDIT_STORAGE_KEY, detail.value);
+    uni.navigateTo({
+      url: `/pages/oa/ApproveManage/approve-template/edit?id=${templateId.value}`,
+    });
+  };
+
+  const loadDetail = () => {
+    if (!templateId.value) return;
+    loading.value = true;
+    detail.value = null;
+    getApprovalTemplateDetail(templateId.value)
+      .then(res => {
+        detail.value = res?.data || null;
+        if (!detail.value) {
+          uni.showToast({ title: "鏈幏鍙栧埌璇︽儏", icon: "none" });
+        }
+      })
+      .catch(() => {
+        uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "none" });
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  onLoad(options => {
+    if (options?.id) {
+      templateId.value = options.id;
+      loadDetail();
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  .template-detail-page {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+    background: #f0f3f8;
+  }
+
+  .detail-scroll {
+    flex: 1;
+    height: 0;
+    padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
+  }
+
+  .loading-wrap {
+    padding: 48px 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .loading-text {
+    font-size: 14px;
+    color: #909399;
+  }
+
+  .section {
+    background: #fff;
+    border-radius: 12px;
+    margin-bottom: 10px;
+    overflow: hidden;
+    box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
+  }
+
+  .section-title {
+    padding: 12px 16px;
+    font-size: 15px;
+    font-weight: 600;
+    color: #1f2d3d;
+    border-bottom: 1px solid #f2f4f7;
+    border-left: 3px solid #2979ff;
+    padding-left: 13px;
+  }
+
+  .info-list {
+    padding: 4px 0;
+  }
+
+  .info-item {
+    display: flex;
+    align-items: flex-start;
+    padding: 11px 16px;
+    border-bottom: 1px solid #f5f7fa;
+    gap: 12px;
+
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+
+  .info-label {
+    flex-shrink: 0;
+    width: 88px;
+    font-size: 14px;
+    color: #606266;
+  }
+
+  .info-value {
+    flex: 1;
+    font-size: 14px;
+    color: #303133;
+    text-align: right;
+    word-break: break-all;
+  }
+
+  .status-on {
+    color: #18a058;
+  }
+
+  .status-off {
+    color: #909399;
+  }
+
+  .field-block {
+    padding: 0 12px 12px;
+  }
+
+  .field-card {
+    padding: 10px 12px;
+    margin-bottom: 8px;
+    background: #f8fafc;
+    border-radius: 8px;
+    border: 1px solid #eef2f6;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .field-card-head {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 6px;
+  }
+
+  .field-card-name {
+    font-size: 14px;
+    font-weight: 500;
+    color: #303133;
+  }
+
+  .field-tag {
+    font-size: 11px;
+    padding: 2px 8px;
+    border-radius: 4px;
+    color: #2979ff;
+    background: #ecf5ff;
+
+    &--req {
+      color: #f56c6c;
+      background: #fef0f0;
+    }
+  }
+
+  .field-card-default {
+    display: block;
+    margin-top: 6px;
+    font-size: 12px;
+    color: #909399;
+  }
+
+  .flow-list {
+    padding: 12px;
+  }
+
+  .flow-card {
+    padding: 12px;
+    margin-bottom: 8px;
+    background: #f8fafc;
+    border-radius: 8px;
+    border: 1px solid #eef2f6;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .flow-card-head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 8px;
+  }
+
+  .flow-level {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .flow-type {
+    font-size: 13px;
+    color: #2979ff;
+  }
+
+  .approver-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .approver-tag {
+    padding: 4px 10px;
+    font-size: 13px;
+    color: #303133;
+    background: #fff;
+    border: 1px solid #dce8f8;
+    border-radius: 16px;
+  }
+
+  .empty-hint {
+    padding: 12px 16px 16px;
+    font-size: 13px;
+    color: #909399;
+
+    &.inline {
+      padding: 0;
+    }
+  }
+
+  .empty-wrap {
+    padding: 48px 20px;
+  }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/edit.vue b/src/pages/oa/ApproveManage/approve-template/edit.vue
new file mode 100644
index 0000000..3b50270
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-template/edit.vue
@@ -0,0 +1,1567 @@
+<!--
+  OA / 瀹℃壒绠$悊 / 鏂板缓瀹℃壒妯℃澘
+  璺敱锛�/pages/oa/ApproveManage/approve-template/edit
+-->
+<template>
+  <view class="template-edit-page">
+    <PageHeader :title="pageTitle"
+                @back="goBack" />
+
+    <scroll-view class="form-scroll"
+                 scroll-y
+                 :show-scrollbar="false">
+      <up-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="88"
+               input-align="right"
+               error-message-align="right">
+        <u-cell-group title="鍩烘湰淇℃伅"
+                      class="form-section">
+          <up-form-item label="妯℃澘鍚嶇О"
+                        prop="templateName"
+                        required
+                        class="form-item-name">
+            <up-input v-model="form.templateName"
+                      class="name-input-inline"
+                      placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+                      maxlength="50"
+                      clearable />
+          </up-form-item>
+          <up-form-item label="妯℃澘绫诲瀷"
+                        prop="templateType"
+                        required
+                        class="form-item-type">
+            <up-radio-group v-model="form.templateType"
+                            class="type-radio-group"
+                            placement="row"
+                            @change="onTemplateTypeChange">
+              <up-radio v-for="opt in TEMPLATE_TYPE_OPTIONS"
+                        :key="opt.value"
+                        :name="opt.value"
+                        :label="opt.name" />
+            </up-radio-group>
+          </up-form-item>
+          <up-form-item label="鍚敤鐘舵��"
+                        class="form-item-switch">
+            <view class="switch-wrap">
+              <up-switch v-model="enabledBool" />
+            </view>
+          </up-form-item>
+          <up-form-item label="妯℃澘璇存槑"
+                        class="form-item-desc"
+                        label-position="top">
+            <view class="desc-input-shell">
+              <up-textarea v-model="form.description"
+                           placeholder="閫夊~"
+                           maxlength="200"
+                           border="none"
+                           height="72" />
+            </view>
+          </up-form-item>
+        </u-cell-group>
+
+        <view class="section-card">
+          <view class="section-head section-head--between">
+            <text class="section-title">濉姤閰嶇疆</text>
+            <view class="head-actions">
+              <text class="head-link"
+                    @click="showPresetSheet = true">棰勮</text>
+              <text class="head-link head-link--primary"
+                    @click="openFieldEditor()">娣诲姞</text>
+            </view>
+          </view>
+          <view class="section-body">
+            <view class="prompt-row">
+              <text class="prompt-label">濉姤鎻愮ず</text>
+              <up-input v-model="formConfig.prompt"
+                        class="prompt-input"
+                        placeholder="閫夊~"
+                        maxlength="200"
+                        clearable />
+            </view>
+            <view v-if="formConfig.fields.length"
+                  class="field-list">
+              <view v-for="(field, index) in formConfig.fields"
+                    :key="field.key"
+                    class="field-item">
+                <view class="field-main">
+                  <view class="field-title-row">
+                    <text class="field-name">{{ field.label }}</text>
+                    <text class="type-tag"
+                          :class="fieldTypeTagClass(field.type)">
+                      {{ fieldTypeLabel(field.type) }}
+                    </text>
+                    <text v-if="field.required"
+                          class="req-tag">蹇呭~</text>
+                  </view>
+                  <text v-if="field.defaultValue"
+                        class="field-default">榛樿锛歿{ field.defaultValue }}</text>
+                </view>
+                <view class="field-actions">
+                  <view class="icon-btn icon-btn--edit"
+                        @click="openFieldEditor(field, index)">
+                    <up-icon name="edit-pen"
+                             size="16"
+                             color="#2979ff" />
+                  </view>
+                  <view class="icon-btn icon-btn--del"
+                        @click="removeField(index)">
+                    <up-icon name="trash"
+                             size="16"
+                             color="#f56c6c" />
+                  </view>
+                </view>
+              </view>
+            </view>
+            <view v-else
+                  class="empty-mini">
+              <text>鏆傛棤濉姤椤�</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="section-card">
+          <view class="section-head">
+            <text class="section-title">瀹℃壒娴佺▼</text>
+          </view>
+          <view class="flow-wrap">
+            <view v-for="(node, nodeIndex) in flowNodes"
+                  :key="node._key"
+                  class="flow-node-block">
+              <view class="flow-node-card">
+                <view class="node-header">
+                  <view class="node-level-badge">{{ nodeIndex + 1 }}</view>
+                  <text class="node-level-text">绗瑊{ levelLabel(nodeIndex + 1) }}绾�</text>
+                  <view v-if="flowNodes.length > 1"
+                        class="node-delete"
+                        @click="removeNode(nodeIndex)">
+                    <up-icon name="trash"
+                             size="16"
+                             color="#f56c6c" />
+                  </view>
+                </view>
+                <view class="approve-type-row">
+                  <view class="type-btn"
+                        :class="{ active: node.approveType === 'AND' }"
+                        @click="node.approveType = 'AND'">
+                    浼氱
+                  </view>
+                  <view class="type-btn"
+                        :class="{ active: node.approveType === 'OR' }"
+                        @click="node.approveType = 'OR'">
+                    鎴栫
+                  </view>
+                </view>
+                <view class="approver-list">
+                  <view v-for="(approver, approverIndex) in node.approvers"
+                        :key="`${node._key}-${approver.approverId}-${approverIndex}`"
+                        class="approver-chip">
+                    <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view>
+                    <text class="approver-name">{{ approver.approverName }}</text>
+                    <view class="approver-remove"
+                          hover-class="approver-remove--active"
+                          @tap.stop="removeApprover(nodeIndex, approverIndex)"
+                          @click.stop="removeApprover(nodeIndex, approverIndex)">
+                      <text class="remove-icon">脳</text>
+                    </view>
+                  </view>
+                  <view class="add-approver"
+                        @click="openUserPicker(nodeIndex)">
+                    <up-icon name="plus"
+                             size="14"
+                             color="#2979ff" />
+                    <text>娣诲姞</text>
+                  </view>
+                </view>
+              </view>
+              <view v-if="nodeIndex < flowNodes.length - 1"
+                    class="flow-connector">
+                <view class="flow-connector-line" />
+              </view>
+            </view>
+            <view class="add-node-bar"
+                  @click="addNode">
+              <up-icon name="plus-circle"
+                       size="20"
+                       color="#2979ff" />
+              <text>娣诲姞绾ф</text>
+            </view>
+          </view>
+        </view>
+      </up-form>
+    </scroll-view>
+
+    <FooterButtons :loading="submitting"
+                   confirm-text="淇濆瓨"
+                   @cancel="goBack"
+                   @confirm="handleSubmit" />
+
+    <up-action-sheet :show="showPresetSheet"
+                     title="浠庨璁惧鍏�"
+                     :actions="presetActions"
+                     @select="onSelectPreset"
+                     @close="showPresetSheet = false" />
+
+    <up-popup :show="showFieldEditor"
+              mode="bottom"
+              round="16"
+              @close="closeFieldEditor">
+      <view class="field-editor">
+        <view class="sheet-handle" />
+        <text class="editor-title">{{ editingFieldIndex >= 0 ? "缂栬緫濉姤椤�" : "娣诲姞濉姤椤�" }}</text>
+        <view class="editor-form">
+          <view class="editor-row">
+            <text class="editor-label required">瀛楁鍚嶇О</text>
+            <up-input v-model="fieldDraft.label"
+                      placeholder="璇疯緭鍏�"
+                      clearable />
+          </view>
+          <view class="editor-row editor-row--block">
+            <text class="editor-label required">瀛楁绫诲瀷</text>
+            <view class="type-chip-grid">
+              <view v-for="opt in FIELD_TYPE_OPTIONS"
+                    :key="opt.value"
+                    class="type-chip"
+                    :class="{ active: fieldDraft.type === opt.value }"
+                    @click="selectFieldType(opt.value)">
+                {{ opt.name }}
+              </view>
+            </view>
+          </view>
+          <view class="editor-row editor-row--block">
+            <text class="editor-label">榛樿鍊�</text>
+            <up-textarea v-if="fieldDraft.type === 'textarea'"
+                         v-model="fieldDraft.defaultValue"
+                         placeholder="閫夊~"
+                         maxlength="500"
+                         height="72" />
+            <view v-else-if="fieldDraft.type === 'date'"
+                  class="default-date-row"
+                  @click="showDefaultDatePicker = true">
+              <up-input :model-value="fieldDraft.defaultValue"
+                        placeholder="閫夋嫨鏃ユ湡"
+                        readonly />
+              <up-icon name="calendar"
+                       size="18"
+                       color="#909399" />
+            </view>
+            <up-input v-else
+                      v-model="fieldDraft.defaultValue"
+                      :type="fieldDraft.type === 'number' ? 'digit' : 'text'"
+                      placeholder="閫夊~"
+                      clearable />
+          </view>
+          <view class="editor-row editor-row--switch">
+            <text class="editor-label">鏄惁蹇呭~</text>
+            <up-switch v-model="fieldDraft.required" />
+          </view>
+        </view>
+        <view class="editor-footer">
+          <view class="editor-btn editor-btn--cancel"
+                @click="closeFieldEditor">鍙栨秷</view>
+          <view class="editor-btn editor-btn--confirm"
+                @click="confirmFieldEditor">纭畾</view>
+        </view>
+      </view>
+    </up-popup>
+
+    <up-popup :show="showDefaultDatePicker"
+              mode="bottom"
+              @close="showDefaultDatePicker = false">
+      <up-datetime-picker :show="true"
+                          v-model="defaultDateTs"
+                          mode="date"
+                          @confirm="onDefaultDateConfirm"
+                          @cancel="showDefaultDatePicker = false" />
+    </up-popup>
+
+    <up-popup :show="showUserPicker"
+              mode="bottom"
+              round="16"
+              @close="closeUserPicker">
+      <view class="user-picker">
+        <view class="sheet-handle" />
+        <view class="picker-head">
+          <text class="picker-cancel"
+                @click="closeUserPicker">鍙栨秷</text>
+          <text class="picker-title">閫夋嫨瀹℃壒浜�</text>
+          <text class="picker-confirm"
+                @click="confirmUserPicker">
+            纭畾{{ pickerSelectedIds.length ? `(${pickerSelectedIds.length})` : "" }}
+          </text>
+        </view>
+        <scroll-view class="user-scroll"
+                     scroll-y>
+          <view v-for="user in availableUsers"
+                :key="user.userId"
+                class="user-item"
+                :class="{ selected: isUserSelected(user.userId) }"
+                @click="toggleUser(user)">
+            <view class="user-avatar">{{ (user.nickName || "?").charAt(0) }}</view>
+            <text class="user-name">{{ user.nickName }}</text>
+            <view class="user-check"
+                  :class="{ checked: isUserSelected(user.userId) }">
+              <up-icon v-if="isUserSelected(user.userId)"
+                       name="checkmark"
+                       size="14"
+                       color="#fff" />
+            </view>
+          </view>
+        </scroll-view>
+      </view>
+    </up-popup>
+  </view>
+</template>
+
+<script setup>
+  import { computed, onMounted, reactive, ref } from "vue";
+  import { onLoad } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import FooterButtons from "@/components/FooterButtons.vue";
+  import {
+    addApprovalTemplate,
+    updateApprovalTemplate,
+  } from "@/api/oa/approvalTemplate.js";
+  import { userListNoPageByTenantId } from "@/api/system/user";
+  import { formatDateToYMD } from "@/utils/ruoyi";
+
+  const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+
+  const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+  const FORM_PRESETS = [
+    {
+      name: "閫氱敤鎶ラ攢",
+      prompt: "璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑",
+      fields: [
+        { key: "reason", label: "鎶ラ攢浜嬬敱", type: "textarea", required: true },
+        { key: "amount", label: "鎶ラ攢閲戦(鍏�)", type: "number", required: true },
+        { key: "applyDate", label: "鐢宠鏃ユ湡", type: "date", required: true },
+      ],
+    },
+    {
+      name: "璇峰亣鐢宠",
+      prompt: "璇峰~鍐欒鍋囩被鍨嬨�佽捣姝㈡椂闂寸瓑",
+      fields: [
+        { key: "leaveType", label: "璇峰亣绫诲瀷", type: "text", required: true },
+        { key: "startTime", label: "寮�濮嬫椂闂�", type: "date", required: true },
+        { key: "endTime", label: "缁撴潫鏃堕棿", type: "date", required: true },
+        { key: "reason", label: "璇峰亣浜嬬敱", type: "textarea", required: true },
+      ],
+    },
+    {
+      name: "閲囪喘鐢宠",
+      prompt: "璇峰~鍐欓噰璐簨鐢便�侀浼伴噾棰濈瓑",
+      fields: [
+        { key: "title", label: "閲囪喘浜嬬敱", type: "textarea", required: true },
+        { key: "amount", label: "棰勪及閲戦(鍏�)", type: "number", required: true },
+      ],
+    },
+  ];
+
+  const FIELD_TYPE_OPTIONS = [
+    { name: "鍗曡鏂囨湰", value: "text" },
+    { name: "澶氳鏂囨湰", value: "textarea" },
+    { name: "鏁板瓧", value: "number" },
+    { name: "鏃ユ湡", value: "date" },
+  ];
+
+  const formRef = ref();
+  const submitting = ref(false);
+  const userList = ref([]);
+  const templateId = ref(null);
+
+  const showPresetSheet = ref(false);
+  const showFieldEditor = ref(false);
+  const showUserPicker = ref(false);
+  const showDefaultDatePicker = ref(false);
+  const defaultDateTs = ref(Date.now());
+
+  const editingFieldIndex = ref(-1);
+  const editingNodeIndex = ref(-1);
+  const pickerSelectedIds = ref([]);
+
+  const form = reactive({
+    templateName: "",
+    templateType: 1,
+    enabled: "1",
+    description: "",
+  });
+
+  const formConfig = reactive({
+    prompt: "",
+    fields: [],
+  });
+
+  const fieldDraft = reactive({
+    label: "",
+    type: "text",
+    defaultValue: "",
+    required: true,
+  });
+
+  let nodeKeySeed = 1;
+
+  const createNode = () => ({
+    _key: `node_${nodeKeySeed++}`,
+    approveType: "AND",
+    approvers: [],
+  });
+
+  const flowNodes = ref([createNode()]);
+
+  const rules = {
+    templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
+    templateType: [
+      {
+        validator: (_rule, value, callback) => {
+          if (value === "" || value === null || value === undefined) {
+            callback(new Error("璇烽�夋嫨妯℃澘绫诲瀷"));
+            return;
+          }
+          callback();
+        },
+        trigger: "change",
+      },
+    ],
+  };
+
+  const TEMPLATE_TYPE_OPTIONS = [
+    { name: "绯荤粺鍐呯疆", value: 0 },
+    { name: "鑷畾涔�", value: 1 },
+  ];
+
+  const presetActions = FORM_PRESETS.map(item => ({
+    name: item.name,
+    value: item.name,
+  }));
+
+  const enabledBool = computed({
+    get: () => form.enabled === "1",
+    set: val => {
+      form.enabled = val ? "1" : "0";
+    },
+  });
+
+  const isEditMode = computed(() => templateId.value != null && templateId.value !== "");
+
+  const pageTitle = computed(() =>
+    isEditMode.value ? "缂栬緫瀹℃壒妯℃澘" : "鏂板缓瀹℃壒妯℃澘"
+  );
+
+  const parseFormConfig = raw => {
+    if (!raw) return { prompt: "", fields: [] };
+    try {
+      const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
+      return {
+        prompt: obj?.prompt || "",
+        fields: Array.isArray(obj?.fields) ? obj.fields.map(f => ({ ...f })) : [],
+      };
+    } catch {
+      return { prompt: "", fields: [] };
+    }
+  };
+
+  const mapNodesFromRow = nodes => {
+    if (!Array.isArray(nodes) || !nodes.length) {
+      return [createNode()];
+    }
+    return nodes.map(node => ({
+      _key: `node_${nodeKeySeed++}`,
+      id: node.id,
+      templateId: node.templateId,
+      approveType: node.approveType || "AND",
+      approvers: (node.approvers || []).map((approver, idx) => ({
+        id: approver.id,
+        nodeId: approver.nodeId,
+        templateId: approver.templateId,
+        approverId: approver.approverId,
+        approverName: approver.approverName,
+        sortNo: approver.sortNo ?? idx + 1,
+      })),
+    }));
+  };
+
+  const fillFormFromRow = row => {
+    if (!row) return;
+    templateId.value = row.id;
+    form.templateName = row.templateName || "";
+    form.templateType =
+      row.templateType === 0 || row.templateType === 1
+        ? row.templateType
+        : Number(row.templateType) || 1;
+    form.enabled = String(row.enabled ?? "1");
+    form.description = row.description || "";
+
+    const config = parseFormConfig(row.formConfig);
+    formConfig.prompt = config.prompt;
+    formConfig.fields = config.fields;
+    flowNodes.value = mapNodesFromRow(row.nodes);
+  };
+
+  const availableUsers = computed(() => {
+    const node = flowNodes.value[editingNodeIndex.value];
+    if (!node) return userList.value;
+    const selectedIds = new Set(node.approvers.map(a => a.approverId));
+    return userList.value.filter(user => !selectedIds.has(user.userId));
+  });
+
+  const levelLabel = n => LEVEL_TEXT[n] || String(n);
+
+  const fieldTypeLabel = type =>
+    FIELD_TYPE_OPTIONS.find(item => item.value === type)?.name || type;
+
+  const fieldTypeTagClass = type => {
+    const map = {
+      text: "type-tag--text",
+      textarea: "type-tag--area",
+      number: "type-tag--num",
+      date: "type-tag--date",
+    };
+    return map[type] || "type-tag--text";
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const onTemplateTypeChange = () => {
+    formRef.value?.validateField?.("templateType");
+  };
+
+  const onSelectPreset = action => {
+    const preset = FORM_PRESETS.find(item => item.name === action.value);
+    if (!preset) return;
+    formConfig.prompt = preset.prompt;
+    formConfig.fields = preset.fields.map(field => ({ ...field }));
+    showPresetSheet.value = false;
+    uni.showToast({ title: "宸插鍏ラ璁�", icon: "success" });
+  };
+
+  const selectFieldType = type => {
+    if (fieldDraft.type === type) return;
+    fieldDraft.type = type;
+    fieldDraft.defaultValue = "";
+  };
+
+  const onDefaultDateConfirm = e => {
+    fieldDraft.defaultValue = formatDateToYMD(e.value);
+    showDefaultDatePicker.value = false;
+  };
+
+  const openFieldEditor = (field, index = -1) => {
+    editingFieldIndex.value = index;
+    if (field) {
+      fieldDraft.label = field.label;
+      fieldDraft.type = field.type || "text";
+      fieldDraft.defaultValue = field.defaultValue ?? "";
+      fieldDraft.required = !!field.required;
+    } else {
+      fieldDraft.label = "";
+      fieldDraft.type = "text";
+      fieldDraft.defaultValue = "";
+      fieldDraft.required = true;
+    }
+    if (fieldDraft.type === "date" && fieldDraft.defaultValue) {
+      const parsed = Date.parse(fieldDraft.defaultValue);
+      defaultDateTs.value = Number.isNaN(parsed) ? Date.now() : parsed;
+    } else {
+      defaultDateTs.value = Date.now();
+    }
+    showFieldEditor.value = true;
+  };
+
+  const closeFieldEditor = () => {
+    showFieldEditor.value = false;
+    editingFieldIndex.value = -1;
+  };
+
+  const buildFieldKey = label => {
+    const base = (label || "field")
+      .trim()
+      .replace(/\s+/g, "_")
+      .replace(/[^\w\u4e00-\u9fa5]/g, "");
+    let key = base || "field";
+    let index = 1;
+    while (formConfig.fields.some((item, idx) => item.key === key && idx !== editingFieldIndex.value)) {
+      key = `${base}_${index++}`;
+    }
+    return key;
+  };
+
+  const confirmFieldEditor = () => {
+    if (!fieldDraft.label?.trim()) {
+      uni.showToast({ title: "璇疯緭鍏ュ瓧娈靛悕绉�", icon: "none" });
+      return;
+    }
+    const defaultValue = String(fieldDraft.defaultValue ?? "").trim();
+    const existingKey =
+      editingFieldIndex.value >= 0
+        ? formConfig.fields[editingFieldIndex.value]?.key
+        : null;
+    const payload = {
+      key: existingKey || buildFieldKey(fieldDraft.label),
+      label: fieldDraft.label.trim(),
+      type: fieldDraft.type,
+      required: !!fieldDraft.required,
+      defaultValue,
+    };
+    if (editingFieldIndex.value >= 0) {
+      formConfig.fields.splice(editingFieldIndex.value, 1, payload);
+    } else {
+      formConfig.fields.push(payload);
+    }
+    closeFieldEditor();
+  };
+
+  const removeField = index => {
+    formConfig.fields.splice(index, 1);
+  };
+
+  const addNode = () => {
+    flowNodes.value.push(createNode());
+  };
+
+  const removeNode = index => {
+    if (flowNodes.value.length <= 1) {
+      uni.showToast({ title: "鑷冲皯淇濈暀涓�涓鎵硅妭鐐�", icon: "none" });
+      return;
+    }
+    flowNodes.value.splice(index, 1);
+  };
+
+  const openUserPicker = nodeIndex => {
+    editingNodeIndex.value = nodeIndex;
+    pickerSelectedIds.value = [];
+    showUserPicker.value = true;
+  };
+
+  const closeUserPicker = () => {
+    showUserPicker.value = false;
+    editingNodeIndex.value = -1;
+    pickerSelectedIds.value = [];
+  };
+
+  const isUserSelected = userId => pickerSelectedIds.value.includes(userId);
+
+  const toggleUser = user => {
+    const ids = pickerSelectedIds.value;
+    const index = ids.indexOf(user.userId);
+    if (index >= 0) {
+      ids.splice(index, 1);
+    } else {
+      ids.push(user.userId);
+    }
+  };
+
+  const confirmUserPicker = () => {
+    const node = flowNodes.value[editingNodeIndex.value];
+    if (!node) {
+      closeUserPicker();
+      return;
+    }
+    const selectedUsers = userList.value.filter(user =>
+      pickerSelectedIds.value.includes(user.userId)
+    );
+    if (!selectedUsers.length) {
+      uni.showToast({ title: "璇烽�夋嫨瀹℃壒浜�", icon: "none" });
+      return;
+    }
+    const startSort = node.approvers.length;
+    selectedUsers.forEach((user, idx) => {
+      node.approvers.push({
+        approverId: user.userId,
+        approverName: user.nickName,
+        sortNo: startSort + idx + 1,
+      });
+    });
+    closeUserPicker();
+  };
+
+  const removeApprover = (nodeIndex, approverIndex) => {
+    const node = flowNodes.value[nodeIndex];
+    if (!node?.approvers?.length) return;
+    const next = node.approvers
+      .filter((_, idx) => idx !== approverIndex)
+      .map((item, idx) => ({
+        ...item,
+        sortNo: idx + 1,
+      }));
+    node.approvers = next;
+  };
+
+  const validateFlow = () => {
+    if (!flowNodes.value.length) {
+      uni.showToast({ title: "璇烽厤缃鎵规祦绋�", icon: "none" });
+      return false;
+    }
+    const emptyNode = flowNodes.value.find(node => !node.approvers.length);
+    if (emptyNode) {
+      uni.showToast({ title: "璇蜂负姣忎釜瀹℃壒鑺傜偣娣诲姞瀹℃壒浜�", icon: "none" });
+      return false;
+    }
+    return true;
+  };
+
+  const buildSubmitPayload = () => {
+    const tid = templateId.value;
+    const payload = {
+      templateName: form.templateName.trim(),
+      enabled: form.enabled,
+      description: form.description?.trim() || "",
+      templateType: form.templateType,
+      formConfig: JSON.stringify({
+        prompt: formConfig.prompt?.trim() || "",
+        fields: formConfig.fields,
+      }),
+      nodes: flowNodes.value.map((node, index) => {
+        const nodePayload = {
+          levelNo: index + 1,
+          approveType: node.approveType,
+          approvers: node.approvers.map((approver, approverIndex) => {
+            const approverPayload = {
+              approverId: approver.approverId,
+              approverName: approver.approverName,
+              sortNo: approverIndex + 1,
+            };
+            if (isEditMode.value) {
+              if (approver.id != null) approverPayload.id = approver.id;
+              if (approver.nodeId != null) approverPayload.nodeId = approver.nodeId;
+              else if (node.id != null) approverPayload.nodeId = node.id;
+              if (approver.templateId != null) approverPayload.templateId = approver.templateId;
+              else if (tid != null) approverPayload.templateId = tid;
+            }
+            return approverPayload;
+          }),
+        };
+        if (isEditMode.value) {
+          if (node.id != null) nodePayload.id = node.id;
+          if (node.templateId != null) nodePayload.templateId = node.templateId;
+          else if (tid != null) nodePayload.templateId = tid;
+        }
+        return nodePayload;
+      }),
+    };
+
+    if (isEditMode.value) {
+      payload.id = tid;
+    }
+
+    return payload;
+  };
+
+  const handleSubmit = async () => {
+    const valid = await formRef.value.validate().catch(() => false);
+    if (!valid || !validateFlow()) return;
+
+    submitting.value = true;
+    const submitApi = isEditMode.value ? updateApprovalTemplate : addApprovalTemplate;
+    submitApi(buildSubmitPayload())
+      .then(() => {
+        uni.showToast({
+          title: isEditMode.value ? "淇敼鎴愬姛" : "淇濆瓨鎴愬姛",
+          icon: "success",
+        });
+        uni.removeStorageSync(EDIT_STORAGE_KEY);
+        setTimeout(() => {
+          uni.navigateBack();
+        }, 300);
+      })
+      .catch(() => {
+        uni.showToast({
+          title: isEditMode.value ? "淇敼澶辫触" : "淇濆瓨澶辫触",
+          icon: "none",
+        });
+      })
+      .finally(() => {
+        submitting.value = false;
+      });
+  };
+
+  onLoad(options => {
+    if (options?.id) {
+      const row = uni.getStorageSync(EDIT_STORAGE_KEY);
+      if (row && String(row.id) === String(options.id)) {
+        fillFormFromRow(row);
+      } else {
+        templateId.value = options.id;
+        uni.showToast({ title: "鏈幏鍙栧埌妯℃澘鏁版嵁", icon: "none" });
+      }
+      uni.removeStorageSync(EDIT_STORAGE_KEY);
+    }
+  });
+
+  onMounted(() => {
+    userListNoPageByTenantId()
+      .then(res => {
+        userList.value = res?.data || [];
+      })
+      .catch(() => {
+        userList.value = [];
+      });
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/static/scss/form-common.scss";
+
+  $primary: #2979ff;
+  $primary-light: #ecf5ff;
+  $text: #1f2d3d;
+  $text-secondary: #606266;
+  $text-muted: #909399;
+  $border: #ebeef5;
+  $bg-page: #f0f3f8;
+  $radius-lg: 12px;
+  $radius-md: 10px;
+  $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05);
+
+  .template-edit-page {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+    background: $bg-page;
+  }
+
+  .form-scroll {
+    flex: 1;
+    height: 0;
+    padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
+  }
+
+  .section-card {
+    margin-bottom: 10px;
+    background: #fff;
+    border-radius: $radius-lg;
+    overflow: hidden;
+    box-shadow: $shadow-card;
+  }
+
+  .section-head {
+    padding: 12px 16px;
+    border-bottom: 1px solid #f2f4f7;
+
+    &--between {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+  }
+
+  .section-title {
+    font-size: 15px;
+    font-weight: 600;
+    color: $text;
+    padding-left: 10px;
+    border-left: 3px solid $primary;
+    line-height: 1.2;
+  }
+
+  .head-actions {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+  }
+
+  .head-link {
+    font-size: 14px;
+    color: $text-secondary;
+
+    &--primary {
+      color: $primary;
+      font-weight: 500;
+    }
+  }
+
+  .section-body {
+    padding: 2px 16px 14px;
+  }
+
+  .form-section {
+    margin-bottom: 10px;
+    border-radius: $radius-lg;
+    overflow: hidden;
+    box-shadow: $shadow-card;
+  }
+
+  :deep(.form-section .u-cell-group__title) {
+    padding: 12px 16px 8px !important;
+    font-size: 15px !important;
+    font-weight: 600 !important;
+    color: $text !important;
+    background: #fff !important;
+  }
+
+  :deep(.form-section .u-form-item) {
+    padding: 0 16px !important;
+  }
+
+  :deep(.form-section .u-form-item__body) {
+    padding: 10px 0 !important;
+    min-height: auto !important;
+  }
+
+  :deep(.form-item-name .u-form-item__body) {
+    flex-direction: row !important;
+    align-items: center !important;
+  }
+
+  :deep(.form-item-name .u-form-item__content) {
+    flex: 1 !important;
+    min-width: 0 !important;
+    justify-content: flex-end !important;
+  }
+
+  :deep(.name-input-inline),
+  :deep(.name-input-inline .u-input__content) {
+    width: 100% !important;
+    flex: 1 !important;
+  }
+
+  :deep(.name-input-inline input),
+  :deep(.name-input-inline .u-input__content__field-wrapper__field) {
+    width: 100% !important;
+    text-align: right !important;
+    font-size: 15px !important;
+  }
+
+  :deep(.form-item-type .u-form-item__body) {
+    align-items: center !important;
+  }
+
+  .type-radio-group {
+    display: flex;
+    justify-content: flex-end;
+    flex-wrap: nowrap;
+  }
+
+  :deep(.type-radio-group .u-radio) {
+    margin-left: 20px;
+  }
+
+  :deep(.form-item-switch .u-form-item__body) {
+    flex-direction: row !important;
+    align-items: center !important;
+  }
+
+  :deep(.form-item-switch .u-form-item__content) {
+    flex: 1 !important;
+    min-width: 0 !important;
+    display: flex !important;
+    justify-content: flex-end !important;
+  }
+
+  .switch-wrap {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    width: 100%;
+  }
+
+  :deep(.form-item-desc .u-form-item__body) {
+    flex-direction: column !important;
+    align-items: stretch !important;
+    padding: 10px 0 12px !important;
+  }
+
+  :deep(.form-item-desc .u-form-item__content) {
+    justify-content: stretch !important;
+    width: 100% !important;
+  }
+
+  .desc-input-shell {
+    width: 100%;
+    box-sizing: border-box;
+    padding: 8px 12px;
+    background: #fff;
+    border: 1px solid #dcdfe6;
+    border-radius: 6px;
+  }
+
+  :deep(.desc-input-shell .u-textarea),
+  :deep(.desc-input-shell textarea) {
+    width: 100% !important;
+    font-size: 15px !important;
+    text-align: left !important;
+  }
+
+  .form-row-item {
+    margin: 0 !important;
+    padding: 0 !important;
+  }
+
+  :deep(.form-row-item .u-form-item__body) {
+    padding: 0;
+  }
+
+  :deep(.form-row-item .u-form-item__body__right__message) {
+    margin-top: 4px;
+    padding-left: 0;
+  }
+
+  .form-row {
+    padding: 10px 0;
+    border-bottom: 1px solid #f5f7fa;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &--column {
+      flex-direction: column;
+      align-items: stretch;
+      gap: 8px;
+    }
+
+    &--compact {
+      padding-top: 8px;
+    }
+  }
+
+  .form-row-label {
+    display: block;
+    font-size: 14px;
+    color: $text-secondary;
+    margin-bottom: 8px;
+
+    &.required::before {
+      content: "*";
+      color: #f56c6c;
+      margin-right: 3px;
+    }
+  }
+
+  .form-row--column .form-row-label {
+    margin-bottom: 0;
+  }
+
+  .prompt-row {
+    display: flex;
+    align-items: center;
+    padding: 12px 0;
+    margin-bottom: 4px;
+    border-bottom: 1px solid #f5f7fa;
+    gap: 8px;
+  }
+
+  .prompt-label {
+    flex-shrink: 0;
+    width: 88px;
+    font-size: 14px;
+    color: $text-secondary;
+  }
+
+  .prompt-input {
+    flex: 1;
+    min-width: 0;
+  }
+
+  :deep(.prompt-input),
+  :deep(.prompt-input .u-input__content) {
+    width: 100% !important;
+  }
+
+  :deep(.prompt-input input),
+  :deep(.prompt-input .u-input__content__field-wrapper__field) {
+    width: 100% !important;
+    text-align: right !important;
+    font-size: 15px !important;
+  }
+
+  .input-box,
+  .textarea-box {
+    background: #f7f9fc;
+    border-radius: 10px;
+    border: 1px solid #eef1f6;
+    overflow: hidden;
+  }
+
+  .textarea-box {
+    padding: 4px 0;
+  }
+
+  .field-list {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    margin-top: 8px;
+  }
+
+  .field-item {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 10px 12px;
+    background: #f8fafc;
+    border-radius: $radius-md;
+    border: 1px solid #eef2f6;
+  }
+
+  .field-main {
+    flex: 1;
+    min-width: 0;
+  }
+
+  .field-title-row {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    gap: 6px;
+  }
+
+  .field-name {
+    font-size: 15px;
+    font-weight: 600;
+    color: $text;
+  }
+
+  .type-tag {
+    font-size: 11px;
+    padding: 2px 8px;
+    border-radius: 4px;
+
+    &--text {
+      color: #2979ff;
+      background: #ecf5ff;
+    }
+
+    &--area {
+      color: #7c5cfc;
+      background: #f3efff;
+    }
+
+    &--num {
+      color: #e6a23c;
+      background: #fdf6ec;
+    }
+
+    &--date {
+      color: #18a058;
+      background: #e8faf0;
+    }
+  }
+
+  .req-tag {
+    font-size: 11px;
+    padding: 2px 6px;
+    color: #f56c6c;
+    background: #fef0f0;
+    border-radius: 4px;
+  }
+
+  .field-default {
+    display: block;
+    margin-top: 4px;
+    font-size: 12px;
+    color: $text-muted;
+  }
+
+  .field-actions {
+    display: flex;
+    gap: 6px;
+    flex-shrink: 0;
+  }
+
+  .icon-btn {
+    width: 32px;
+    height: 32px;
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    &--edit {
+      background: #ecf5ff;
+    }
+
+    &--del {
+      background: #fef0f0;
+    }
+  }
+
+  .empty-mini {
+    padding: 20px 0;
+    text-align: center;
+    font-size: 13px;
+    color: $text-muted;
+  }
+
+  .flow-wrap {
+    padding: 10px 16px 14px;
+  }
+
+  .flow-node-block {
+    display: flex;
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .flow-node-card {
+    background: #fafbfd;
+    border: 1px solid #e8eef5;
+    border-radius: $radius-md;
+    padding: 12px;
+  }
+
+  .node-header {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-bottom: 10px;
+  }
+
+  .node-level-badge {
+    width: 26px;
+    height: 26px;
+    border-radius: 8px;
+    background: $primary;
+    color: #fff;
+    font-size: 14px;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+  }
+
+  .node-level-text {
+    flex: 1;
+    font-size: 15px;
+    font-weight: 600;
+    color: $text;
+  }
+
+  .node-delete {
+    padding: 6px;
+    flex-shrink: 0;
+  }
+
+  .approve-type-row {
+    display: flex;
+    background: #f0f3f8;
+    border-radius: 8px;
+    padding: 3px;
+    margin-bottom: 10px;
+  }
+
+  .type-btn {
+    flex: 1;
+    text-align: center;
+    padding: 8px 0;
+    font-size: 14px;
+    color: $text-secondary;
+    border-radius: 6px;
+
+    &.active {
+      background: #fff;
+      color: $primary;
+      font-weight: 500;
+    }
+  }
+
+  .approver-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .approver-chip {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 6px 12px 6px 6px;
+    background: #fff;
+    border: 1px solid #dce8f8;
+    border-radius: 24px;
+    box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06);
+  }
+
+  .approver-avatar {
+    width: 26px;
+    height: 26px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: #fff;
+    font-size: 12px;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .approver-name {
+    font-size: 13px;
+    color: $text;
+    max-width: 80px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .approver-remove {
+    flex-shrink: 0;
+    width: 22px;
+    height: 22px;
+    margin-left: 2px;
+    border-radius: 50%;
+    background: #f2f3f5;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .approver-remove--active {
+    background: #fde2e2;
+  }
+
+  .remove-icon {
+    font-size: 16px;
+    line-height: 1;
+    color: #909399;
+    font-weight: 300;
+  }
+
+  .add-approver {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    padding: 8px 14px;
+    border: 1.5px dashed #a8cfff;
+    border-radius: 24px;
+    background: $primary-light;
+    color: $primary;
+    font-size: 13px;
+  }
+
+  .flow-connector {
+    display: flex;
+    justify-content: center;
+    padding: 4px 0;
+  }
+
+  .flow-connector-line {
+    width: 2px;
+    height: 14px;
+    background: #d0dff0;
+  }
+
+  .add-node-bar {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    margin-top: 8px;
+    padding: 11px;
+    border: 1px dashed #c6daf5;
+    border-radius: $radius-md;
+    color: $primary;
+    font-size: 14px;
+  }
+
+  .sheet-handle {
+    width: 40px;
+    height: 4px;
+    margin: 10px auto 6px;
+    background: #e4e7ed;
+    border-radius: 2px;
+  }
+
+  .field-editor,
+  .user-picker {
+    padding: 0 18px calc(18px + env(safe-area-inset-bottom));
+    background: #fff;
+    max-height: 85vh;
+  }
+
+  .editor-title {
+    display: block;
+    font-size: 16px;
+    font-weight: 600;
+    color: $text;
+    text-align: center;
+    margin-bottom: 14px;
+  }
+
+  .editor-form {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .editor-row {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+
+    &--switch {
+      flex-direction: row;
+      align-items: center;
+      justify-content: space-between;
+      padding: 4px 0;
+    }
+  }
+
+  .editor-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: $text-secondary;
+
+    &.required::before {
+      content: "*";
+      color: #f56c6c;
+      margin-right: 4px;
+    }
+  }
+
+  .editor-row .input-box,
+  .editor-row .textarea-box {
+    background: #f7f9fc;
+    border-radius: 10px;
+    border: 1px solid #eef1f6;
+  }
+
+  .type-chip-grid {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 10px;
+  }
+
+  .type-chip {
+    text-align: center;
+    padding: 10px 6px;
+    font-size: 13px;
+    color: $text-secondary;
+    background: #f7f9fc;
+    border: 1px solid #eef1f6;
+    border-radius: 8px;
+
+    &.active {
+      color: $primary;
+      background: $primary-light;
+      border-color: $primary;
+      font-weight: 500;
+    }
+  }
+
+  .default-date-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 0 12px;
+    min-height: 44px;
+    background: #f7f9fc;
+    border-radius: 10px;
+    border: 1px solid #eef1f6;
+  }
+
+  .editor-footer {
+    display: flex;
+    gap: 10px;
+    margin-top: 16px;
+    padding-top: 14px;
+    border-top: 1px solid #f5f7fa;
+  }
+
+  .editor-btn {
+    flex: 1;
+    text-align: center;
+    padding: 11px 0;
+    border-radius: 8px;
+    font-size: 15px;
+
+    &--cancel {
+      color: $text-secondary;
+      background: #f5f7fa;
+    }
+
+    &--confirm {
+      color: #fff;
+      background: $primary;
+    }
+  }
+
+  .picker-head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-bottom: 14px;
+    border-bottom: 1px solid #f5f7fa;
+    margin-bottom: 8px;
+  }
+
+  .picker-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: $text;
+  }
+
+  .picker-cancel {
+    font-size: 15px;
+    color: $text-muted;
+    min-width: 48px;
+  }
+
+  .picker-confirm {
+    font-size: 15px;
+    color: $primary;
+    font-weight: 600;
+    min-width: 48px;
+    text-align: right;
+  }
+
+  .user-scroll {
+    max-height: 52vh;
+  }
+
+  .user-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 14px 4px;
+    border-bottom: 1px solid #f5f7fa;
+    border-radius: 10px;
+    margin-bottom: 4px;
+    transition: background 0.2s;
+
+    &.selected {
+      background: #f5f9ff;
+    }
+  }
+
+  .user-avatar {
+    width: 40px;
+    height: 40px;
+    border-radius: 12px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: #fff;
+    font-size: 16px;
+    font-weight: 600;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+  }
+
+  .user-name {
+    flex: 1;
+    font-size: 15px;
+    color: $text;
+  }
+
+  .user-check {
+    width: 22px;
+    height: 22px;
+    border-radius: 50%;
+    border: 2px solid #dcdfe6;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+
+    &.checked {
+      background: $primary;
+      border-color: $primary;
+    }
+  }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/index.vue b/src/pages/oa/ApproveManage/approve-template/index.vue
new file mode 100644
index 0000000..96660bc
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-template/index.vue
@@ -0,0 +1,304 @@
+<!--
+  OA / 瀹℃壒绠$悊 / 瀹℃壒妯℃澘
+  璺敱锛�/pages/oa/ApproveManage/approve-template/index
+-->
+<template>
+  <view class="approve-template-page sales-account">
+    <PageHeader title="瀹℃壒妯℃澘"
+                @back="goBack" />
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input v-model="queryParams.templateName"
+                    class="search-text"
+                    placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+                    clearable
+                    @confirm="handleSearch" />
+        </view>
+        <view class="filter-button"
+              @click="handleSearch">
+          <up-icon name="search"
+                   size="24"
+                   color="#999" />
+        </view>
+      </view>
+    </view>
+
+    <scroll-view class="list-scroll"
+                 scroll-y
+                 :show-scrollbar="false"
+                 @scrolltolower="loadMore">
+      <view v-if="list.length"
+            class="ledger-list">
+        <view v-for="item in list"
+              :key="item.id"
+              class="ledger-item">
+          <view class="item-header">
+            <view class="item-left">
+              <view class="document-icon">
+                <up-icon name="file-text"
+                         size="16"
+                         color="#ffffff" />
+              </view>
+              <text class="item-id">{{ item.templateName || "-" }}</text>
+            </view>
+            <u-tag :type="enabledTagType(item.enabled)"
+                   :text="enabledText(item.enabled)" />
+          </view>
+          <up-divider />
+          <view class="item-details">
+            <view class="detail-row">
+              <text class="detail-label">妯℃澘绫诲瀷</text>
+              <text class="detail-value">{{ templateTypeText(item.templateType) }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">瀹℃壒鑺傜偣</text>
+              <text class="detail-value">{{ nodeCount(item) }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">妯℃澘璇存槑</text>
+              <text class="detail-value">{{ item.description || "-" }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">鍒涘缓浜�</text>
+              <text class="detail-value">{{ item.createdUserName || "-" }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">鍒涘缓鏃堕棿</text>
+              <text class="detail-value">{{ item.createTime || "-" }}</text>
+            </view>
+          </view>
+          <view class="action-buttons">
+            <up-button class="action-btn"
+                       size="small"
+                       @click.stop="goDetail(item)">
+              璇︽儏
+            </up-button>
+            <up-button class="action-btn"
+                       size="small"
+                       type="primary"
+                       @click.stop="goEdit(item)">
+              缂栬緫
+            </up-button>
+            <up-button class="action-btn"
+                       size="small"
+                       type="error"
+                       plain
+                       @click.stop="handleDelete(item)">
+              鍒犻櫎
+            </up-button>
+          </view>
+        </view>
+        <up-loadmore :status="pageStatus" />
+      </view>
+      <view v-else
+            class="empty-wrap">
+        <up-empty mode="list"
+                  text="鏆傛棤瀹℃壒妯℃澘鏁版嵁" />
+      </view>
+    </scroll-view>
+
+    <view class="fab-button"
+          @click="goAdd">
+      <up-icon name="plus"
+               size="28"
+               color="#ffffff" />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { reactive, ref } from "vue";
+  import { onShow } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import {
+    deleteApprovalTemplate,
+    listApprovalTemplatePage,
+  } from "@/api/oa/approvalTemplate.js";
+
+  const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+
+  const queryParams = reactive({
+    templateName: "",
+  });
+
+  const list = ref([]);
+  const pageStatus = ref("loadmore");
+
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+
+  const buildListParams = () => ({
+    page: {
+      current: page.current,
+      size: page.size,
+    },
+    approvalTemplateDto: {
+      templateName: queryParams.templateName?.trim() || undefined,
+    },
+  });
+
+  const enabledText = enabled => {
+    const val = String(enabled ?? "");
+    if (val === "1") return "鍚敤";
+    if (val === "0") return "鍋滅敤";
+    return "-";
+  };
+
+  const enabledTagType = enabled => {
+    const val = String(enabled ?? "");
+    if (val === "1") return "success";
+    if (val === "0") return "info";
+    return "info";
+  };
+
+  const templateTypeText = type => {
+    const val = Number(type);
+    if (val === 0) return "绯荤粺鍐呯疆";
+    if (val === 1) return "鑷畾涔�";
+    return "-";
+  };
+
+  const nodeCount = item => {
+    const count = item?.nodes?.length;
+    return count != null ? `${count} 涓猔 : "-";
+  };
+
+  const getList = () => {
+    if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+    pageStatus.value = "loading";
+    listApprovalTemplatePage(buildListParams())
+      .then(res => {
+        const pageData = res?.data || {};
+        const records = pageData.records || [];
+        const total = pageData.total ?? 0;
+
+        if (page.current === 1) {
+          list.value = records;
+        } else {
+          list.value = [...list.value, ...records];
+        }
+
+        page.total = total;
+        if (list.value.length >= total || records.length < page.size) {
+          pageStatus.value = "nomore";
+        } else {
+          pageStatus.value = "loadmore";
+          page.current += 1;
+        }
+      })
+      .catch(() => {
+        if (page.current === 1) {
+          list.value = [];
+        }
+        pageStatus.value = "loadmore";
+        uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+      });
+  };
+
+  const handleSearch = () => {
+    page.current = 1;
+    pageStatus.value = "loadmore";
+    list.value = [];
+    getList();
+  };
+
+  const loadMore = () => {
+    if (pageStatus.value === "loadmore") {
+      getList();
+    }
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const goAdd = () => {
+    uni.removeStorageSync(EDIT_STORAGE_KEY);
+    uni.navigateTo({
+      url: "/pages/oa/ApproveManage/approve-template/edit",
+    });
+  };
+
+  const goDetail = item => {
+    if (!item?.id) return;
+    uni.navigateTo({
+      url: `/pages/oa/ApproveManage/approve-template/detail?id=${item.id}`,
+    });
+  };
+
+  const goEdit = item => {
+    if (!item?.id) return;
+    uni.setStorageSync(EDIT_STORAGE_KEY, item);
+    uni.navigateTo({
+      url: `/pages/oa/ApproveManage/approve-template/edit?id=${item.id}`,
+    });
+  };
+
+  const handleDelete = item => {
+    if (!item?.id) return;
+    const name = item.templateName || "璇ユā鏉�";
+    uni.showModal({
+      title: "鍒犻櫎纭",
+      content: `纭畾鍒犻櫎銆�${name}銆嶅悧锛熷垹闄ゅ悗鏃犳硶鎭㈠銆俙,
+      confirmText: "鍒犻櫎",
+      confirmColor: "#f56c6c",
+      success: res => {
+        if (!res.confirm) return;
+        uni.showLoading({ title: "鍒犻櫎涓�...", mask: true });
+        deleteApprovalTemplate([item.id])
+          .then(() => {
+            uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+            handleSearch();
+          })
+          .catch(() => {
+            uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+          })
+          .finally(() => {
+            uni.hideLoading();
+          });
+      },
+    });
+  };
+
+  onShow(() => {
+    handleSearch();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/styles/sales-common.scss";
+
+  .approve-template-page {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+  }
+
+  .list-scroll {
+    flex: 1;
+    height: 0;
+    padding-bottom: calc(80px + env(safe-area-inset-bottom));
+  }
+
+  .empty-wrap {
+    padding: 48px 20px;
+  }
+
+  .action-buttons {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+    margin-top: 12px;
+    padding-top: 12px;
+    border-top: 1px solid #f0f0f0;
+  }
+
+  .action-btn {
+    min-width: 72px;
+  }
+</style>
diff --git a/src/pages/oa/AttendManage/leave-apply/index.vue b/src/pages/oa/AttendManage/leave-apply/index.vue
new file mode 100644
index 0000000..237b92b
--- /dev/null
+++ b/src/pages/oa/AttendManage/leave-apply/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 鍋囧嫟绠$悊 / 璇峰亣鐢宠
+  璺敱锛�/pages/oa/AttendManage/leave-apply/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 鍋囧嫟绠$悊 - 璇峰亣鐢宠 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "AttendManage/leave-apply";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/AttendManage/overtime-apply/index.vue b/src/pages/oa/AttendManage/overtime-apply/index.vue
new file mode 100644
index 0000000..04b071a
--- /dev/null
+++ b/src/pages/oa/AttendManage/overtime-apply/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 鍋囧嫟绠$悊 / 鍔犵彮鐢宠
+  璺敱锛�/pages/oa/AttendManage/overtime-apply/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 鍋囧嫟绠$悊 - 鍔犵彮鐢宠 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "AttendManage/overtime-apply";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/ContractManage/purchase-contract/index.vue b/src/pages/oa/ContractManage/purchase-contract/index.vue
new file mode 100644
index 0000000..ac2eb6b
--- /dev/null
+++ b/src/pages/oa/ContractManage/purchase-contract/index.vue
@@ -0,0 +1,26 @@
+<!--
+  OA / 鍚堝悓绠$悊 / 閲囪喘鍚堝悓
+  璺敱锛�/pages/oa/ContractManage/purchase-contract/index
+  璇存槑锛氳烦杞嚦閲囪喘鍙拌处 /pages/procurementManagement/procurementLedger/index
+-->
+<template>
+  <view class="redirect-page" />
+</template>
+
+<script setup>
+  /** OA - 鍚堝悓绠$悊 - 閲囪喘鍚堝悓锛堣烦杞噰璐彴璐︼級 */
+  import { onLoad } from "@dcloudio/uni-app";
+
+  onLoad(() => {
+    uni.redirectTo({
+      url: "/pages/procurementManagement/procurementLedger/index",
+    });
+  });
+</script>
+
+<style scoped>
+  .redirect-page {
+    min-height: 100vh;
+    background: #f8f9fa;
+  }
+</style>
diff --git a/src/pages/oa/ContractManage/sale-contract/index.vue b/src/pages/oa/ContractManage/sale-contract/index.vue
new file mode 100644
index 0000000..a05f4e9
--- /dev/null
+++ b/src/pages/oa/ContractManage/sale-contract/index.vue
@@ -0,0 +1,26 @@
+<!--
+  OA / 鍚堝悓绠$悊 / 閿�鍞悎鍚�
+  璺敱锛�/pages/oa/ContractManage/sale-contract/index
+  璇存槑锛氳烦杞嚦閿�鍞彴璐� /pages/sales/salesAccount/index
+-->
+<template>
+  <view class="redirect-page" />
+</template>
+
+<script setup>
+  /** OA - 鍚堝悓绠$悊 - 閿�鍞悎鍚岋紙璺宠浆閿�鍞彴璐︼級 */
+  import { onLoad } from "@dcloudio/uni-app";
+
+  onLoad(() => {
+    uni.redirectTo({
+      url: "/pages/sales/salesAccount/index",
+    });
+  });
+</script>
+
+<style scoped>
+  .redirect-page {
+    min-height: 100vh;
+    background: #f8f9fa;
+  }
+</style>
diff --git a/src/pages/oa/EnterpriseNews/news-manage/index.vue b/src/pages/oa/EnterpriseNews/news-manage/index.vue
new file mode 100644
index 0000000..67a2d1a
--- /dev/null
+++ b/src/pages/oa/EnterpriseNews/news-manage/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浼佷笟鏂伴椈
+  璺敱锛�/pages/oa/EnterpriseNews/news-manage/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浼佷笟鏂伴椈 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "EnterpriseNews/news-manage";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/post-manage/index.vue b/src/pages/oa/HrManage/post-manage/index.vue
new file mode 100644
index 0000000..75be5aa
--- /dev/null
+++ b/src/pages/oa/HrManage/post-manage/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 宀椾綅绠$悊
+  璺敱锛�/pages/oa/HrManage/post-manage/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 宀椾綅绠$悊 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/post-manage";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/regular-apply/index.vue b/src/pages/oa/HrManage/regular-apply/index.vue
new file mode 100644
index 0000000..ae962c6
--- /dev/null
+++ b/src/pages/oa/HrManage/regular-apply/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 杞鐢宠
+  璺敱锛�/pages/oa/HrManage/regular-apply/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 杞鐢宠 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/regular-apply";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/resign-apply/index.vue b/src/pages/oa/HrManage/resign-apply/index.vue
new file mode 100644
index 0000000..1052832
--- /dev/null
+++ b/src/pages/oa/HrManage/resign-apply/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 绂昏亴鐢宠
+  璺敱锛�/pages/oa/HrManage/resign-apply/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 绂昏亴鐢宠 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/resign-apply";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/staff-archive/index.vue b/src/pages/oa/HrManage/staff-archive/index.vue
new file mode 100644
index 0000000..8d485c0
--- /dev/null
+++ b/src/pages/oa/HrManage/staff-archive/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 鍛樺伐妗f
+  璺敱锛�/pages/oa/HrManage/staff-archive/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 鍛樺伐妗f */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/staff-archive";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/staff-contract/index.vue b/src/pages/oa/HrManage/staff-contract/index.vue
new file mode 100644
index 0000000..f12b78d
--- /dev/null
+++ b/src/pages/oa/HrManage/staff-contract/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 鍛樺伐鍚堝悓
+  璺敱锛�/pages/oa/HrManage/staff-contract/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 鍛樺伐鍚堝悓 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/staff-contract";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/transfer-apply/index.vue b/src/pages/oa/HrManage/transfer-apply/index.vue
new file mode 100644
index 0000000..f3161bf
--- /dev/null
+++ b/src/pages/oa/HrManage/transfer-apply/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 璋冨矖鐢宠
+  璺敱锛�/pages/oa/HrManage/transfer-apply/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 璋冨矖鐢宠 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/transfer-apply";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/work-handover/index.vue b/src/pages/oa/HrManage/work-handover/index.vue
new file mode 100644
index 0000000..c5d0e19
--- /dev/null
+++ b/src/pages/oa/HrManage/work-handover/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 浜轰簨绠$悊 / 宸ヤ綔浜ゆ帴
+  璺敱锛�/pages/oa/HrManage/work-handover/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 浜轰簨绠$悊 - 宸ヤ綔浜ゆ帴 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "HrManage/work-handover";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/NoticeAnnouncement/notice-manage/index.vue b/src/pages/oa/NoticeAnnouncement/notice-manage/index.vue
new file mode 100644
index 0000000..351ddf0
--- /dev/null
+++ b/src/pages/oa/NoticeAnnouncement/notice-manage/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 鍏憡閫氱煡
+  璺敱锛�/pages/oa/NoticeAnnouncement/notice-manage/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 鍏憡閫氱煡 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "NoticeAnnouncement/notice-manage";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/ReimburseManage/cost-reimburse/index.vue b/src/pages/oa/ReimburseManage/cost-reimburse/index.vue
new file mode 100644
index 0000000..5343c75
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/cost-reimburse/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 鎶ラ攢绠$悊 / 璐圭敤鎶ラ攢
+  璺敱锛�/pages/oa/ReimburseManage/cost-reimburse/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 鎶ラ攢绠$悊 - 璐圭敤鎶ラ攢 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "ReimburseManage/cost-reimburse";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/ReimburseManage/travel-reimburse/index.vue b/src/pages/oa/ReimburseManage/travel-reimburse/index.vue
new file mode 100644
index 0000000..df4dac1
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/travel-reimburse/index.vue
@@ -0,0 +1,18 @@
+<!--
+  OA / 鎶ラ攢绠$悊 / 宸梾鎶ラ攢
+  璺敱锛�/pages/oa/ReimburseManage/travel-reimburse/index
+-->
+<template>
+  <OaListPage v-if="config"
+              :page-key="pageKey"
+              :page-config="config" />
+</template>
+
+<script setup>
+  /** OA - 鎶ラ攢绠$悊 - 宸梾鎶ラ攢 */
+  import OaListPage from "../../_components/OaListPage.vue";
+  import { useOaPage } from "../../_utils/useOaPage.js";
+
+  const pageKey = "ReimburseManage/travel-reimburse";
+  const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/_components/OaListPage.vue b/src/pages/oa/_components/OaListPage.vue
new file mode 100644
index 0000000..47614fc
--- /dev/null
+++ b/src/pages/oa/_components/OaListPage.vue
@@ -0,0 +1,182 @@
+<template>
+  <view class="oa-page sales-account">
+    <PageHeader :title="pageConfig.title"
+                @back="goBack" />
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input v-model="keyword"
+                    class="search-text"
+                    :placeholder="`鎼滅储${pageConfig.title}`"
+                    clearable
+                    @change="handleSearch" />
+        </view>
+        <view class="filter-button"
+              @click="handleSearch">
+          <up-icon name="search"
+                   size="24"
+                   color="#999" />
+        </view>
+      </view>
+    </view>
+
+    <scroll-view class="list-scroll"
+                 scroll-y
+                 :show-scrollbar="false">
+      <view v-if="displayList.length"
+            class="ledger-list">
+        <view v-for="item in displayList"
+              :key="item.id"
+              class="ledger-item"
+              @click="openDetail(item)">
+          <view class="item-header">
+            <view class="item-left">
+              <view class="document-icon">
+                <up-icon name="file-text"
+                         size="16"
+                         color="#ffffff" />
+              </view>
+              <text class="item-id">{{ item.summary || item.applicantName || pageConfig.title }}</text>
+            </view>
+            <u-tag :type="getStatusMeta(item.status).type"
+                   :text="getStatusMeta(item.status).text" />
+          </view>
+          <up-divider />
+          <view class="item-details">
+            <view v-for="field in pageConfig.fields"
+                  :key="field.prop"
+                  class="detail-row">
+              <text class="detail-label">{{ field.label }}</text>
+              <text class="detail-value">{{ item[field.prop] || "-" }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">鐢宠浜�</text>
+              <text class="detail-value">{{ item.applicantName || "-" }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">鐢宠鏃堕棿</text>
+              <text class="detail-value">{{ item.createTime || "-" }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view v-else
+            class="empty-wrap">
+        <up-empty mode="list"
+                  :text="`鏆傛棤${pageConfig.title}鏁版嵁`" />
+      </view>
+    </scroll-view>
+
+    <view class="footer-add">
+      <up-button type="primary"
+                 text="鏂板"
+                 @click="handleAdd" />
+    </view>
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref } from "vue";
+  import { onShow } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { ensureList, saveList } from "../_utils/oaStorage.js";
+  import { getStatusMeta } from "../_utils/oaPageRegistry.js";
+  import { showToast } from "../_utils/oaUi.js";
+
+  const props = defineProps({
+    pageKey: {
+      type: String,
+      required: true,
+    },
+    pageConfig: {
+      type: Object,
+      required: true,
+    },
+  });
+
+  const keyword = ref("");
+  const list = ref([]);
+
+  const displayList = computed(() => {
+    const kw = keyword.value.trim();
+    if (!kw) return list.value;
+    return list.value.filter(item => {
+      const text = [
+        item.summary,
+        item.applicantName,
+        item.deptName,
+        ...props.pageConfig.fields.map(f => item[f.prop]),
+      ]
+        .filter(Boolean)
+        .join(" ");
+      return text.includes(kw);
+    });
+  });
+
+  const loadData = () => {
+    list.value = ensureList(
+      props.pageConfig.storageKey,
+      props.pageConfig.mockRows || []
+    );
+  };
+
+  const handleSearch = () => {
+    /* 鍏抽敭瀛楃敱 computed 杩囨护 */
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const openDetail = item => {
+    showToast(`鏌ョ湅锛�${item.summary || props.pageConfig.title}`);
+  };
+
+  const handleAdd = () => {
+    const row = {
+      ...(props.pageConfig.mockRows?.[0] || {}),
+      id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
+      applicantName: "褰撳墠鐢ㄦ埛",
+      status: "pending",
+      createTime: new Date().toISOString().slice(0, 19).replace("T", " "),
+      summary: `鏂板缓${props.pageConfig.title}`,
+    };
+    list.value = [row, ...list.value];
+    saveList(props.pageConfig.storageKey, list.value);
+    showToast("宸叉柊澧烇紙鏈湴绀轰緥锛�", "success");
+  };
+
+  onShow(() => {
+    loadData();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/styles/sales-common.scss";
+
+  .oa-page {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+  }
+
+  .list-scroll {
+    flex: 1;
+    height: 0;
+    padding-bottom: 80px;
+  }
+
+  .empty-wrap {
+    padding: 48px 20px;
+  }
+
+  .footer-add {
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
+    background: #fff;
+    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
+  }
+</style>
diff --git a/src/pages/oa/_utils/oaPageRegistry.js b/src/pages/oa/_utils/oaPageRegistry.js
new file mode 100644
index 0000000..22019bc
--- /dev/null
+++ b/src/pages/oa/_utils/oaPageRegistry.js
@@ -0,0 +1,256 @@
+import { OA_NAV } from "@/config/oaPaths.js";
+
+const STATUS_MAP = {
+  pending: { text: "瀹℃牳涓�", type: "warning" },
+  approved: { text: "宸查�氳繃", type: "success" },
+  rejected: { text: "宸查┏鍥�", type: "error" },
+  draft: { text: "鑽夌", type: "info" },
+  published: { text: "宸插彂甯�", type: "success" },
+};
+
+function baseRow(extra = {}) {
+  return {
+    id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
+    applicantName: "寮犱笁",
+    deptName: "鐮斿彂閮�",
+    status: "pending",
+    createTime: "2026-05-18 09:00:00",
+    summary: "绀轰緥鏁版嵁锛屽彲瀵规帴鍚庣鎺ュ彛",
+    ...extra,
+  };
+}
+
+/** 鍚勫瓙椤甸潰閰嶇疆锛歵itle銆乻torageKey銆佸垪琛ㄥ睍绀哄瓧娈点�佸垵濮� mock */
+export const OA_PAGE_REGISTRY = {
+  "HrManage/staff-archive": {
+    title: "鍛樺伐妗f",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_staff_archive_v1",
+    path: OA_NAV.staffArchive,
+    fields: [
+      { label: "鍛樺伐缂栧彿", prop: "staffNo" },
+      { label: "宀椾綅", prop: "postJob" },
+      { label: "鑱旂郴鐢佃瘽", prop: "phone" },
+    ],
+    mockRows: [
+      baseRow({
+        staffNo: "E2026001",
+        postJob: "宸ョ▼甯�",
+        phone: "13800000001",
+        summary: "鏉庢槑 路 鍦ㄨ亴",
+      }),
+    ],
+  },
+  "HrManage/staff-contract": {
+    title: "鍛樺伐鍚堝悓",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_staff_contract_v1",
+    path: OA_NAV.staffContract,
+    fields: [
+      { label: "鍚堝悓缂栧彿", prop: "contractNo" },
+      { label: "鍚堝悓绫诲瀷", prop: "contractType" },
+      { label: "鍒版湡鏃�", prop: "endDate" },
+    ],
+    mockRows: [
+      baseRow({
+        contractNo: "HT-2026-001",
+        contractType: "鍔冲姩鍚堝悓",
+        endDate: "2027-12-31",
+      }),
+    ],
+  },
+  "HrManage/regular-apply": {
+    title: "杞鐢宠",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_regular_apply_v1",
+    path: OA_NAV.regularApply,
+    fields: [
+      { label: "鍏ヨ亴鏃ユ湡", prop: "entryDate" },
+      { label: "杞鏃ユ湡", prop: "regularDate" },
+    ],
+    mockRows: [baseRow({ entryDate: "2025-11-01", regularDate: "2026-05-20" })],
+  },
+  "HrManage/transfer-apply": {
+    title: "璋冨矖鐢宠",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_transfer_apply_v1",
+    path: OA_NAV.transferApply,
+    fields: [
+      { label: "鍘熷矖浣�", prop: "fromPost" },
+      { label: "鐩爣宀椾綅", prop: "toPost" },
+    ],
+    mockRows: [
+      baseRow({ fromPost: "寮�鍙戝伐绋嬪笀", toPost: "楂樼骇寮�鍙戝伐绋嬪笀" }),
+    ],
+  },
+  "HrManage/resign-apply": {
+    title: "绂昏亴鐢宠",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_resign_apply_v1",
+    path: OA_NAV.resignApply,
+    fields: [
+      { label: "棰勮绂昏亴鏃�", prop: "leaveDate" },
+      { label: "绂昏亴鍘熷洜", prop: "reason" },
+    ],
+    mockRows: [baseRow({ leaveDate: "2026-06-30", reason: "涓汉鍙戝睍" })],
+  },
+  "HrManage/work-handover": {
+    title: "宸ヤ綔浜ゆ帴",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_work_handover_v1",
+    path: OA_NAV.workHandover,
+    fields: [
+      { label: "浜ゆ帴浜�", prop: "handoverTo" },
+      { label: "浜ゆ帴浜嬮」", prop: "handoverItems" },
+    ],
+    mockRows: [
+      baseRow({ handoverTo: "鐜嬩簲", handoverItems: "椤圭洰鏂囨。銆佸鎴疯祫鏂�" }),
+    ],
+  },
+  "HrManage/post-manage": {
+    title: "宀椾綅绠$悊",
+    module: "浜轰簨绠$悊",
+    storageKey: "oa_hr_post_manage_v1",
+    path: OA_NAV.postManage,
+    fields: [
+      { label: "宀椾綅缂栫爜", prop: "postCode" },
+      { label: "鎵�灞為儴闂�", prop: "deptName" },
+    ],
+    mockRows: [baseRow({ postCode: "DEV-01", summary: "寮�鍙戝伐绋嬪笀" })],
+  },
+  "AttendManage/leave-apply": {
+    title: "璇峰亣鐢宠",
+    module: "鍋囧嫟绠$悊",
+    storageKey: "oa_attend_leave_apply_v1",
+    path: OA_NAV.leaveApply,
+    fields: [
+      { label: "璇峰亣绫诲瀷", prop: "leaveType" },
+      { label: "寮�濮嬫椂闂�", prop: "startTime" },
+      { label: "缁撴潫鏃堕棿", prop: "endTime" },
+    ],
+    mockRows: [
+      baseRow({
+        leaveType: "骞村亣",
+        startTime: "2026-05-20 09:00",
+        endTime: "2026-05-21 18:00",
+      }),
+    ],
+  },
+  "AttendManage/overtime-apply": {
+    title: "鍔犵彮鐢宠",
+    module: "鍋囧嫟绠$悊",
+    storageKey: "oa_attend_overtime_apply_v1",
+    path: OA_NAV.overtimeApply,
+    fields: [
+      { label: "鍔犵彮鏃ユ湡", prop: "overtimeDate" },
+      { label: "鏃堕暱(灏忔椂)", prop: "hours" },
+    ],
+    mockRows: [
+      baseRow({ overtimeDate: "2026-05-18", hours: "3", summary: "鐗堟湰涓婄嚎" }),
+    ],
+  },
+  "ReimburseManage/travel-reimburse": {
+    title: "宸梾鎶ラ攢",
+    module: "鎶ラ攢绠$悊",
+    storageKey: "oa_reimburse_travel_v1",
+    path: OA_NAV.travelReimburse,
+    fields: [
+      { label: "鍑哄樊鍦扮偣", prop: "destination" },
+      { label: "閲戦(鍏�)", prop: "amount" },
+    ],
+    mockRows: [
+      baseRow({ destination: "涓婃捣", amount: "2680.50", summary: "瀹㈡埛鎷滆宸梾" }),
+    ],
+  },
+  "ReimburseManage/cost-reimburse": {
+    title: "璐圭敤鎶ラ攢",
+    module: "鎶ラ攢绠$悊",
+    storageKey: "oa_reimburse_cost_v1",
+    path: OA_NAV.costReimburse,
+    fields: [
+      { label: "璐圭敤绉戠洰", prop: "category" },
+      { label: "閲戦(鍏�)", prop: "amount" },
+    ],
+    mockRows: [
+      baseRow({ category: "鍔炲叕鐢ㄥ搧", amount: "356.00", summary: "閲囪喘鏂囧叿" }),
+    ],
+  },
+  "ApproveManage/approve-list": {
+    title: "瀹℃壒鍒楄〃",
+    module: "瀹℃壒绠$悊",
+    storageKey: "oa_unified_approve_list_v1",
+    path: OA_NAV.approveList,
+    fields: [
+      { label: "瀹℃壒绫诲瀷", prop: "approvalTypeLabel" },
+      { label: "褰撳墠鑺傜偣", prop: "currentNode" },
+    ],
+    mockRows: [
+      baseRow({
+        approvalTypeLabel: "璇峰亣鐢宠",
+        currentNode: "閮ㄩ棬璐熻矗浜�",
+      }),
+    ],
+  },
+  "ApproveManage/approve-template": {
+    title: "瀹℃壒妯℃澘",
+    module: "瀹℃壒绠$悊",
+    storageKey: "oa_approve_template_custom_v1",
+    path: OA_NAV.approveTemplate,
+    fields: [
+      { label: "妯℃澘鍚嶇О", prop: "templateName" },
+      { label: "鑺傜偣鏁�", prop: "nodeCount" },
+    ],
+    mockRows: [
+      baseRow({
+        templateName: "閫氱敤瀹℃壒娴�",
+        nodeCount: "3",
+        status: "approved",
+        summary: "绯荤粺鍐呯疆妯℃澘",
+      }),
+    ],
+  },
+  "EnterpriseNews/news-manage": {
+    title: "浼佷笟鏂伴椈",
+    module: "浼佷笟鏂伴椈",
+    storageKey: "oa_enterprise_news_v1",
+    path: OA_NAV.enterpriseNews,
+    fields: [
+      { label: "鏍忕洰", prop: "category" },
+      { label: "闃呰閲�", prop: "readCount" },
+    ],
+    mockRows: [
+      baseRow({
+        category: "鍏徃鍔ㄦ��",
+        readCount: "128",
+        status: "published",
+        summary: "2026骞寸涓�瀛e害缁忚惀閫氭姤",
+      }),
+    ],
+  },
+  "NoticeAnnouncement/notice-manage": {
+    title: "鍏憡閫氱煡",
+    module: "鍏憡閫氱煡",
+    storageKey: "oa_notice_announcement_v1",
+    path: OA_NAV.noticeAnnouncement,
+    fields: [
+      { label: "鍏憡绫诲瀷", prop: "noticeType" },
+      { label: "浼樺厛绾�", prop: "priority" },
+    ],
+    mockRows: [
+      baseRow({
+        noticeType: "浼佷笟鍏憡",
+        priority: "鏅��",
+        status: "published",
+        summary: "浜斾竴鍔冲姩鑺傛斁鍋囧畨鎺�",
+      }),
+    ],
+  },
+};
+
+export function getOaPageConfig(pageKey) {
+  return OA_PAGE_REGISTRY[pageKey] || null;
+}
+
+export function getStatusMeta(status) {
+  return STATUS_MAP[status] || { text: status || "鈥�", type: "info" };
+}
diff --git a/src/pages/oa/_utils/oaStorage.js b/src/pages/oa/_utils/oaStorage.js
new file mode 100644
index 0000000..67b2cd0
--- /dev/null
+++ b/src/pages/oa/_utils/oaStorage.js
@@ -0,0 +1,26 @@
+export function loadList(storageKey, defaultRows = []) {
+  try {
+    const raw = uni.getStorageSync(storageKey);
+    if (!raw) {
+      return defaultRows.map(row => ({ ...row }));
+    }
+    const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
+    return Array.isArray(parsed)
+      ? parsed.map(row => ({ ...row }))
+      : defaultRows.map(row => ({ ...row }));
+  } catch {
+    return defaultRows.map(row => ({ ...row }));
+  }
+}
+
+export function saveList(storageKey, rows) {
+  uni.setStorageSync(storageKey, JSON.stringify(rows));
+}
+
+export function ensureList(storageKey, defaultRows) {
+  const list = loadList(storageKey, defaultRows);
+  if (!uni.getStorageSync(storageKey)) {
+    saveList(storageKey, list);
+  }
+  return list;
+}
diff --git a/src/pages/oa/_utils/oaUi.js b/src/pages/oa/_utils/oaUi.js
new file mode 100644
index 0000000..f6af87e
--- /dev/null
+++ b/src/pages/oa/_utils/oaUi.js
@@ -0,0 +1,13 @@
+export function showToast(title, icon = "none") {
+  uni.showToast({ title, icon });
+}
+
+export function confirmModal(content, title = "鎻愮ず") {
+  return new Promise(resolve => {
+    uni.showModal({
+      title,
+      content,
+      success: res => resolve(Boolean(res.confirm)),
+    });
+  });
+}
diff --git a/src/pages/oa/_utils/useOaPage.js b/src/pages/oa/_utils/useOaPage.js
new file mode 100644
index 0000000..960344d
--- /dev/null
+++ b/src/pages/oa/_utils/useOaPage.js
@@ -0,0 +1,6 @@
+import { getOaPageConfig } from "./oaPageRegistry.js";
+
+export function useOaPage(pageKey) {
+  const config = getOaPageConfig(pageKey);
+  return { pageKey, config };
+}
diff --git a/src/pages/works.vue b/src/pages/works.vue
index 6164fb0..963efe5 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -1,5 +1,28 @@
 <template>
   <view class="content">
+    <!-- OA鍔炲叕妯″潡 -->
+    <view class="common-module oa-module"
+          v-if="hasOaItems">
+      <view class="module-header">
+        <view class="module-title-container">
+          <text class="module-title">OA鍔炲叕</text>
+        </view>
+      </view>
+      <view class="module-content">
+        <up-grid :border="false"
+                 col="4">
+          <up-grid-item v-for="(item, index) in oaItems"
+                        :key="index"
+                        @click="handleCommonItemClick(item)">
+            <view class="icon-container">
+              <image :src="item.icon"
+                     class="item-icon"></image>
+            </view>
+            <text class="item-label">{{item.label}}</text>
+          </up-grid-item>
+        </up-grid>
+      </view>
+    </view>
     <!-- 鍗忓悓鍔炲叕妯″潡 -->
     <view class="common-module collaboration-module"
           v-if="hasCollaborationItems">
@@ -308,6 +331,7 @@
   import { userLoginFacotryList } from "@/api/login";
   import { getProductWorkOrderById } from "@/api/productionManagement/productionReporting";
   import DownloadProgressMask from "@/components/DownloadProgressMask.vue";
+  import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js";
   import modal from "@/plugins/modal";
   import useUserStore from "@/store/modules/user";
 
@@ -535,6 +559,11 @@
       label: "瀹夊叏鍩硅鑰冩牳",
     },
   ]);
+  // OA鍔炲叕鍔熻兘鏁版嵁锛堢函鍓嶇閰嶇疆锛屼笉鍙備笌鍚庣鏉冮檺杩囨护锛�
+  const oaItems = reactive(
+    OA_WORKBENCH_ITEMS.map(item => ({ ...item }))
+  );
+
   // 鍗忓悓鍔炲叕鍔熻兘鏁版嵁
   const collaborationItems = reactive([
     {
@@ -630,6 +659,10 @@
 
   // 澶勭悊甯哥敤鍔熻兘鐐瑰嚮
   const handleCommonItemClick = item => {
+    if (item.path) {
+      uni.navigateTo({ url: item.path });
+      return;
+    }
     // 鏍规嵁涓嶅悓鐨勫姛鑳介」杩涜璺宠浆
     switch (item.label) {
       case "瀹㈡埛妗f":
@@ -1268,6 +1301,7 @@
   const hasAfterSalesServiceItems = computed(
     () => afterSalesServiceItems.length > 0
   );
+  const hasOaItems = computed(() => oaItems.length > 0);
   const hasCollaborationItems = computed(() => collaborationItems.length > 0);
   const hasSafetyItems = computed(() => safetyItems.length > 0);
   const hasQualityItems = computed(() => qualityItems.length > 0);
@@ -1646,6 +1680,10 @@
     --module-color: #4caf50;
   }
 
+  .oa-module {
+    --module-color: #673ab7;
+  }
+
   .production-module {
     --module-color: #ff9800;
   }
@@ -1888,6 +1926,10 @@
     --module-color: #4caf50;
   }
 
+  .oa-module {
+    --module-color: #673ab7;
+  }
+
   .production-module {
     --module-color: #ff9800;
   }

--
Gitblit v1.9.3