From 9d89dedf542a7b9f8e2549c44723771133f79ef2 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期二, 19 五月 2026 18:04:10 +0800
Subject: [PATCH] 模板类型变更

---
 src/api/oa/approvalTemplate.js                              |   11 
 src/pages/oa/_utils/approvalTemplateType.js                 |   94 +++
 src/config/oaPaths.js                                       |    2 
 src/pages.json                                              |   14 
 src/pages/oa/ApproveManage/approve-list/index.vue           |  297 +++++++++++
 src/pages/oa/ApproveManage/approve-template/edit.vue        |  112 +++-
 src/pages/oa/ApproveManage/approve-list/template-select.vue |  308 ++++++++++++
 src/api/oa/approvalInstance.js                              |   28 +
 src/pages/oa/ApproveManage/approve-list/apply.vue           |  548 +++++++++++++++++++++
 src/api/basic/enum.js                                       |    9 
 src/pages/oa/ApproveManage/approve-template/detail.vue      |   23 
 src/pages/oa/ApproveManage/approve-template/index.vue       |   26 
 12 files changed, 1,417 insertions(+), 55 deletions(-)

diff --git a/src/api/basic/enum.js b/src/api/basic/enum.js
new file mode 100644
index 0000000..982123f
--- /dev/null
+++ b/src/api/basic/enum.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒妯℃澘绫诲瀷鏋氫妇 GET /basic/enum/TypeEnums */
+export function getTypeEnums() {
+  return request({
+    url: "/basic/enum/TypeEnums",
+    method: "get",
+  });
+}
diff --git a/src/api/oa/approvalInstance.js b/src/api/oa/approvalInstance.js
new file mode 100644
index 0000000..08b1f0c
--- /dev/null
+++ b/src/api/oa/approvalInstance.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒瀹炰緥鍒嗛〉鏌ヨ GET /approvalInstance/listPage */
+export function listApprovalInstancePage(params) {
+  return request({
+    url: "/approvalInstance/listPage",
+    method: "get",
+    params,
+  });
+}
+
+/** 鏂板缓瀹℃壒瀹炰緥 POST /approvalInstance/save */
+export function saveApprovalInstance(approvalInstanceDto) {
+  return request({
+    url: "/approvalInstance/save",
+    method: "post",
+    data: { approvalInstanceDto },
+  });
+}
+
+/** 瀹℃牳涓慨鏀瑰鎵瑰疄渚� PUT /approvalInstance/update */
+export function updateApprovalInstance(approvalInstanceDto) {
+  return request({
+    url: "/approvalInstance/update",
+    method: "put",
+    data: { approvalInstanceDto },
+  });
+}
diff --git a/src/api/oa/approvalTemplate.js b/src/api/oa/approvalTemplate.js
index 5d34aea..ab13897 100644
--- a/src/api/oa/approvalTemplate.js
+++ b/src/api/oa/approvalTemplate.js
@@ -1,5 +1,16 @@
 import request from "@/utils/request";
 
+/**
+ * 鎸� templateType 鏌ヨ宸插惎鐢ㄦā鏉垮垪琛紙闈� businessType锛�
+ * GET /approvalTemplate/list/{templateType}  渚嬶細list/1 = 鑷畾涔夊凡鍚敤
+ */
+export function listApprovalTemplateByType(templateType) {
+  return request({
+    url: `/approvalTemplate/list/${templateType}`,
+    method: "get",
+  });
+}
+
 /** 瀹℃壒妯℃澘鍒嗛〉鏌ヨ */
 export function listApprovalTemplatePage(params) {
   return request({
diff --git a/src/config/oaPaths.js b/src/config/oaPaths.js
index 03246bf..73cca36 100644
--- a/src/config/oaPaths.js
+++ b/src/config/oaPaths.js
@@ -24,6 +24,8 @@
   saleContract: `/${P}/ContractManage/sale-contract/index`,
   /** 瀹℃壒绠$悊 */
   approveList: `/${P}/ApproveManage/approve-list/index`,
+  approveListTemplateSelect: `/${P}/ApproveManage/approve-list/template-select`,
+  approveListApply: `/${P}/ApproveManage/approve-list/apply`,
   approveTemplate: `/${P}/ApproveManage/approve-template/index`,
   approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`,
   approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`,
diff --git a/src/pages.json b/src/pages.json
index 7a4dc16..e39fb89 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -1404,6 +1404,20 @@
       }
     },
     {
+      "path": "pages/oa/ApproveManage/approve-list/template-select",
+      "style": {
+        "navigationBarTitleText": "閫夋嫨瀹℃壒妯℃澘",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/oa/ApproveManage/approve-list/apply",
+      "style": {
+        "navigationBarTitleText": "鍙戣捣瀹℃壒",
+        "navigationStyle": "custom"
+      }
+    },
+    {
       "path": "pages/oa/ApproveManage/approve-template/index",
       "style": {
         "navigationBarTitleText": "瀹℃壒妯℃澘",
diff --git a/src/pages/oa/ApproveManage/approve-list/apply.vue b/src/pages/oa/ApproveManage/approve-list/apply.vue
new file mode 100644
index 0000000..12f49c5
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -0,0 +1,548 @@
+<!--
+  OA / 瀹℃壒绠$悊 / 鍙戣捣瀹℃壒
+  璺敱锛�/pages/oa/ApproveManage/approve-list/apply
+-->
+<template>
+  <view class="approve-apply-page">
+    <PageHeader :title="pageTitle"
+                @back="goBack" />
+
+    <scroll-view class="form-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="form-body">
+            <view class="form-row">
+              <text class="form-label required">瀹℃壒鏍囬</text>
+              <up-input v-model="form.title"
+                        placeholder="璇疯緭鍏ュ鎵规爣棰�"
+                        maxlength="100"
+                        clearable />
+            </view>
+            <view class="form-row">
+              <text class="form-label">瀹℃壒妯℃澘</text>
+              <text class="form-readonly">{{ templateName }}</text>
+            </view>
+            <view class="form-row">
+              <text class="form-label">鐢宠浜�</text>
+              <text class="form-readonly">{{ displayApplicantName }}</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="section">
+          <view class="section-title">濉姤鍐呭</view>
+          <view v-if="formConfigData.prompt"
+                class="form-prompt">
+            {{ formConfigData.prompt }}
+          </view>
+          <view v-if="formConfigData.fields.length"
+                class="form-body">
+            <view v-for="field in formConfigData.fields"
+                  :key="field.key"
+                  class="form-row form-row--field">
+              <text class="form-label"
+                    :class="{ required: field.required }">{{ field.label }}</text>
+              <up-textarea v-if="field.type === 'textarea'"
+                           v-model="formValues[field.key]"
+                           :placeholder="`璇疯緭鍏�${field.label}`"
+                           maxlength="500"
+                           border="surround"
+                           height="80" />
+              <view v-else-if="field.type === 'date'"
+                    class="date-trigger"
+                    @click="openDatePicker(field.key)">
+                <up-input :model-value="formValues[field.key]"
+                          :placeholder="`璇烽�夋嫨${field.label}`"
+                          readonly />
+              </view>
+              <up-input v-else
+                        v-model="formValues[field.key]"
+                        :type="field.type === 'number' ? 'digit' : 'text'"
+                        :placeholder="`璇疯緭鍏�${field.label}`"
+                        clearable />
+            </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="confirmText"
+                   :loading="submitting"
+                   @cancel="goBack"
+                   @confirm="handleSubmit" />
+
+    <up-popup :show="showDatePicker"
+              mode="bottom"
+              @close="showDatePicker = false">
+      <up-datetime-picker :show="true"
+                          v-model="datePickerTs"
+                          mode="date"
+                          @confirm="onDateConfirm"
+                          @cancel="showDatePicker = false" />
+    </up-popup>
+  </view>
+</template>
+
+<script setup>
+  import { computed, reactive, 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";
+  import {
+    saveApprovalInstance,
+    updateApprovalInstance,
+  } from "@/api/oa/approvalInstance.js";
+  import useUserStore from "@/store/modules/user";
+  import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
+
+  const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+  const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+  const userStore = useUserStore();
+  const templateId = ref("");
+  const instanceId = ref("");
+  const instanceRow = ref(null);
+  const detail = ref(null);
+  const loading = ref(false);
+  const submitting = ref(false);
+  const formValues = reactive({});
+  const form = reactive({ title: "" });
+
+  const showDatePicker = ref(false);
+  const datePickerTs = ref(Date.now());
+  const activeDateFieldKey = ref("");
+
+  const isEditMode = computed(() => !!instanceId.value);
+
+  const pageTitle = computed(() => (isEditMode.value ? "缂栬緫瀹℃壒" : "鍙戣捣瀹℃壒"));
+  const confirmText = computed(() => (isEditMode.value ? "淇濆瓨" : "鎻愪氦瀹℃壒"));
+
+  const applicantName = computed(
+    () => userStore.nickName || userStore.name || "-"
+  );
+
+  const displayApplicantName = computed(
+    () => instanceRow.value?.applicantName || applicantName.value
+  );
+
+  const templateName = computed(
+    () => detail.value?.templateName || instanceRow.value?.templateName || "-"
+  );
+
+  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 : [],
+      };
+    } catch {
+      return { prompt: "", fields: [] };
+    }
+  };
+
+  const formConfigData = computed(() => {
+    const raw = isEditMode.value
+      ? instanceRow.value?.formConfig
+      : detail.value?.formConfig;
+    return parseFormConfig(raw);
+  });
+
+  const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+  const approveTypeText = type => (type === "OR" ? "鎴栫" : "浼氱");
+
+  const initFormValues = fields => {
+    Object.keys(formValues).forEach(key => {
+      delete formValues[key];
+    });
+    fields.forEach(field => {
+      if (!field?.key) return;
+      formValues[field.key] = field.value ?? field.defaultValue ?? "";
+    });
+  };
+
+  const openDatePicker = fieldKey => {
+    activeDateFieldKey.value = fieldKey;
+    const current = formValues[fieldKey];
+    datePickerTs.value = current ? new Date(current).getTime() : Date.now();
+    showDatePicker.value = true;
+  };
+
+  const onDateConfirm = e => {
+    const ts = e?.value ?? datePickerTs.value;
+    if (activeDateFieldKey.value) {
+      formValues[activeDateFieldKey.value] = formatDateToYMD(ts);
+    }
+    showDatePicker.value = false;
+  };
+
+  const validateForm = () => {
+    if (!form.title?.trim()) {
+      uni.showToast({ title: "璇疯緭鍏ュ鎵规爣棰�", icon: "none" });
+      return false;
+    }
+    for (const field of formConfigData.value.fields) {
+      if (!field.required) continue;
+      const val = formValues[field.key];
+      if (val === undefined || val === null || String(val).trim() === "") {
+        uni.showToast({ title: `璇峰~鍐�${field.label}`, icon: "none" });
+        return false;
+      }
+    }
+    if (!detail.value?.nodes?.length) {
+      uni.showToast({ title: "妯℃澘鏈厤缃鎵规祦绋�", icon: "none" });
+      return false;
+    }
+    return true;
+  };
+
+  const buildFormConfigPayload = () =>
+    JSON.stringify({
+      prompt: formConfigData.value.prompt,
+      fields: formConfigData.value.fields.map(field => ({
+        ...field,
+        value: formValues[field.key] ?? "",
+      })),
+    });
+
+  const buildSavePayload = () => ({
+    templateId: detail.value.id,
+    templateName: detail.value.templateName,
+    businessType: detail.value.businessType,
+    title: form.title.trim(),
+    status: "PENDING",
+    currentLevel: 1,
+    applicantId: userStore.id,
+    applicantName: applicantName.value,
+    applyTime: parseTime(new Date()),
+    deptId: userStore.currentDeptId || undefined,
+    formConfig: buildFormConfigPayload(),
+  });
+
+  const buildUpdatePayload = () => {
+    const row = instanceRow.value || {};
+    return {
+      id: instanceId.value,
+      instanceNo: row.instanceNo,
+      templateId: row.templateId ?? detail.value?.id,
+      templateName: row.templateName ?? detail.value?.templateName,
+      businessId: row.businessId,
+      businessType: row.businessType,
+      title: form.title.trim(),
+      status: row.status || "PENDING",
+      currentLevel: row.currentLevel,
+      applicantId: row.applicantId,
+      applicantName: row.applicantName,
+      applyTime: row.applyTime,
+      deptId: row.deptId,
+      formConfig: buildFormConfigPayload(),
+    };
+  };
+
+  const handleSubmit = () => {
+    if (!validateForm() || submitting.value) return;
+
+    submitting.value = true;
+    const submitApi = isEditMode.value
+      ? updateApprovalInstance
+      : saveApprovalInstance;
+    const payload = isEditMode.value ? buildUpdatePayload() : buildSavePayload();
+
+    submitApi(payload)
+      .then(() => {
+        uni.showToast({
+          title: isEditMode.value ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛",
+          icon: "success",
+        });
+        if (isEditMode.value) {
+          uni.removeStorageSync(EDIT_STORAGE_KEY);
+        }
+        setTimeout(() => {
+          uni.navigateBack({ delta: isEditMode.value ? 1 : 2 });
+        }, 300);
+      })
+      .catch(() => {
+        uni.showToast({
+          title: isEditMode.value ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触",
+          icon: "none",
+        });
+      })
+      .finally(() => {
+        submitting.value = false;
+      });
+  };
+
+  const loadTemplateDetail = () => {
+    if (!templateId.value) return Promise.resolve();
+    return getApprovalTemplateDetail(templateId.value)
+      .then(res => {
+        detail.value = res?.data || null;
+        if (!detail.value) {
+          uni.showToast({ title: "鏈幏鍙栧埌妯℃澘璇︽儏", icon: "none" });
+        }
+        return detail.value;
+      })
+      .catch(() => {
+        uni.showToast({ title: "鑾峰彇妯℃澘璇︽儏澶辫触", icon: "none" });
+        return null;
+      });
+  };
+
+  const loadForCreate = async () => {
+    loading.value = true;
+    detail.value = null;
+    try {
+      await loadTemplateDetail();
+      if (!detail.value) return;
+      initFormValues(formConfigData.value.fields);
+      if (!form.title && detail.value.templateName) {
+        form.title = `${detail.value.templateName}鐢宠`;
+      }
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const loadForEdit = async () => {
+    const row = uni.getStorageSync(EDIT_STORAGE_KEY);
+    if (!row || String(row.id) !== String(instanceId.value)) {
+      uni.showToast({ title: "鏈幏鍙栧埌瀹℃壒鏁版嵁", icon: "none" });
+      return;
+    }
+    uni.removeStorageSync(EDIT_STORAGE_KEY);
+    instanceRow.value = row;
+    templateId.value = row.templateId;
+    form.title = row.title || "";
+
+    loading.value = true;
+    detail.value = null;
+    try {
+      await loadTemplateDetail();
+      initFormValues(formConfigData.value.fields);
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  onLoad(options => {
+    if (options?.id) {
+      instanceId.value = options.id;
+      loadForEdit();
+      return;
+    }
+    if (options?.templateId) {
+      templateId.value = options.templateId;
+      loadForCreate();
+      return;
+    }
+    uni.showToast({ title: "缂哄皯椤甸潰鍙傛暟", icon: "none" });
+  });
+</script>
+
+<style scoped lang="scss">
+  .approve-apply-page {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+    background: #f0f3f8;
+  }
+
+  .form-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;
+  }
+
+  .form-body {
+    padding: 8px 16px 16px;
+  }
+
+  .form-row {
+    padding: 10px 0;
+    border-bottom: 1px solid #f5f7fa;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    &--field {
+      flex-direction: column;
+      align-items: stretch;
+    }
+  }
+
+  .form-label {
+    display: block;
+    margin-bottom: 8px;
+    font-size: 14px;
+    color: #606266;
+
+    &.required::before {
+      content: "*";
+      color: #f56c6c;
+      margin-right: 4px;
+    }
+  }
+
+  .form-readonly {
+    font-size: 14px;
+    color: #303133;
+  }
+
+  .form-prompt {
+    margin: 12px 16px 0;
+    padding: 10px 12px;
+    font-size: 13px;
+    color: #606266;
+    background: #f8fafc;
+    border-radius: 8px;
+    line-height: 1.5;
+  }
+
+  .date-trigger {
+    width: 100%;
+  }
+
+  .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-list/index.vue b/src/pages/oa/ApproveManage/approve-list/index.vue
index fd00bae..db97060 100644
--- a/src/pages/oa/ApproveManage/approve-list/index.vue
+++ b/src/pages/oa/ApproveManage/approve-list/index.vue
@@ -3,16 +3,297 @@
   璺敱锛�/pages/oa/ApproveManage/approve-list/index
 -->
 <template>
-  <OaListPage v-if="config"
-              :page-key="pageKey"
-              :page-config="config" />
+  <view class="approve-list-page sales-account">
+    <PageHeader title="瀹℃壒鍒楄〃"
+                @back="goBack" />
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input v-model="queryParams.keyword"
+                    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.title || item.instanceNo || "-" }}</text>
+            </view>
+            <u-tag :type="statusTagType(item.status)"
+                   :text="statusText(item.status)" />
+          </view>
+          <up-divider />
+          <view class="item-details">
+            <view class="detail-row">
+              <text class="detail-label">瀹℃壒缂栧彿</text>
+              <text class="detail-value">{{ item.instanceNo || "-" }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">妯℃澘鍚嶇О</text>
+              <text class="detail-value">{{ item.templateName || "-" }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">涓氬姟鍚嶇О</text>
+              <text class="detail-value">{{ item.businessName || "-" }}</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">{{ formatLevel(item.currentLevel) }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">褰撳墠瀹℃壒浜�</text>
+              <text class="detail-value">{{ currentApproverName(item) }}</text>
+            </view>
+            <view class="detail-row">
+              <text class="detail-label">鐢宠鏃堕棿</text>
+              <text class="detail-value">{{ item.applyTime || "-" }}</text>
+            </view>
+            <view v-if="item.finishTime"
+                  class="detail-row">
+              <text class="detail-label">瀹屾垚鏃堕棿</text>
+              <text class="detail-value">{{ item.finishTime }}</text>
+            </view>
+          </view>
+          <view v-if="canEdit(item) || item.isApprove"
+                class="action-buttons">
+            <up-button v-if="canEdit(item)"
+                       class="action-btn"
+                       size="small"
+                       @click.stop="goEdit(item)">
+              缂栬緫
+            </up-button>
+            <up-button v-if="item.isApprove"
+                       class="action-btn"
+                       size="small"
+                       type="primary"
+                       @click.stop="handleApprove(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>
-  /** OA - 瀹℃壒绠$悊 - 瀹℃壒鍒楄〃 */
-  import OaListPage from "../../_components/OaListPage.vue";
-  import { useOaPage } from "../../_utils/useOaPage.js";
+  import { reactive, ref } from "vue";
+  import { onShow } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { listApprovalInstancePage } from "@/api/oa/approvalInstance.js";
+  import { OA_NAV } from "@/config/oaPaths.js";
+  import useUserStore from "@/store/modules/user";
 
-  const pageKey = "ApproveManage/approve-list";
-  const { config } = useOaPage(pageKey);
+  const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+  const userStore = useUserStore();
+
+  const queryParams = reactive({
+    keyword: "",
+  });
+
+  const list = ref([]);
+  const pageStatus = ref("loadmore");
+
+  const page = reactive({
+    current: 1,
+    size: 10,
+    total: 0,
+  });
+
+  const STATUS_TEXT = {
+    PENDING: "杩涜涓�",
+    APPROVED: "宸查�氳繃",
+    REJECTED: "宸查┏鍥�",
+  };
+
+  const STATUS_TAG = {
+    PENDING: "warning",
+    APPROVED: "success",
+    REJECTED: "error",
+  };
+
+  const statusText = status => STATUS_TEXT[status] || status || "-";
+
+  const statusTagType = status => STATUS_TAG[status] || "info";
+
+  const formatLevel = level => {
+    if (level == null || level === "") return "-";
+    return `绗� ${level} 绾;
+  };
+
+  const currentApproverName = item => {
+    const tasks = item?.tasks;
+    if (!Array.isArray(tasks) || !tasks.length) return "-";
+    const pending = tasks.find(t => t.taskStatus === "PENDING");
+    if (pending?.approverName) return pending.approverName;
+    const names = [...new Set(tasks.map(t => t.approverName).filter(Boolean))];
+    return names.length ? names.join("銆�") : "-";
+  };
+
+  const buildListParams = () => {
+    const keyword = queryParams.keyword?.trim();
+    const dto = {};
+    if (keyword) {
+      if (/[\u4e00-\u9fa5]/.test(keyword)) {
+        dto.title = keyword;
+      } else {
+        dto.instanceNo = keyword;
+      }
+    }
+    return {
+      page: {
+        current: page.current,
+        size: page.size,
+      },
+      approvalInstanceDto: dto,
+    };
+  };
+
+  const getList = () => {
+    if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+    pageStatus.value = "loading";
+    listApprovalInstancePage(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.navigateTo({ url: OA_NAV.approveListTemplateSelect });
+  };
+
+  const canEdit = item =>
+    item?.status === "PENDING" &&
+    String(item.applicantId) === String(userStore.id);
+
+  const goEdit = item => {
+    if (!item?.id) return;
+    uni.setStorageSync(EDIT_STORAGE_KEY, item);
+    uni.navigateTo({
+      url: `${OA_NAV.approveListApply}?id=${item.id}`,
+    });
+  };
+
+  const handleApprove = item => {
+    if (!item?.id) return;
+    uni.showToast({ title: "瀹℃壒璇︽儏椤靛緟瀵规帴", icon: "none" });
+  };
+
+  onShow(() => {
+    handleSearch();
+  });
 </script>
+
+<style scoped lang="scss">
+  @import "@/styles/sales-common.scss";
+
+  .approve-list-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/ApproveManage/approve-list/template-select.vue b/src/pages/oa/ApproveManage/approve-list/template-select.vue
new file mode 100644
index 0000000..628483b
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/template-select.vue
@@ -0,0 +1,308 @@
+<!--
+  OA / 瀹℃壒绠$悊 / 閫夋嫨瀹℃壒妯℃澘
+  璺敱锛�/pages/oa/ApproveManage/approve-list/template-select
+  Tab锛歍ypeEnums 鈫� businessType锛涘垪琛細GET /approvalTemplate/list/1锛堣嚜瀹氫箟宸插惎鐢級鍚庢寜 businessType 绛涢��
+-->
+<template>
+  <view class="template-select-page sales-account">
+    <PageHeader title="閫夋嫨瀹℃壒妯℃澘"
+                @back="goBack" />
+
+    <view v-if="typeOptions.length"
+          class="step-section">
+      <view class="tabs-wrap">
+        <up-tabs :list="tabList"
+                 :current="activeTab"
+                 line-color="#2979ff"
+                 @click="onTabClick" />
+      </view>
+    </view>
+
+    <view class="search-section">
+      <view class="search-bar">
+        <view class="search-input">
+          <up-input v-model="keyword"
+                    class="search-text"
+                    placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+                    clearable />
+        </view>
+        <view class="filter-button">
+          <up-icon name="search"
+                   size="24"
+                   color="#999" />
+        </view>
+      </view>
+    </view>
+
+    <scroll-view class="list-scroll"
+                 scroll-y
+                 :show-scrollbar="false">
+      <view v-if="loading"
+            class="loading-wrap">
+        <up-loading-icon mode="circle" />
+        <text class="loading-text">鍔犺浇涓�...</text>
+      </view>
+      <view v-else-if="!typeOptions.length"
+            class="empty-wrap">
+        <up-empty mode="list"
+                  text="鏈幏鍙栧埌瀹℃壒绫诲瀷" />
+      </view>
+      <view v-else-if="displayList.length"
+            class="ledger-list">
+        <view v-for="item in displayList"
+              :key="item.id"
+              class="ledger-item ledger-item--clickable"
+              @click="selectTemplate(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">{{ businessTypeText(item.businessType) }}</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>
+        </view>
+      </view>
+      <view v-else
+            class="empty-wrap">
+        <up-empty mode="list"
+                  :text="emptyText" />
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup>
+  import { computed, ref } from "vue";
+  import { onLoad } from "@dcloudio/uni-app";
+  import PageHeader from "@/components/PageHeader.vue";
+  import { listApprovalTemplateByType } from "@/api/oa/approvalTemplate.js";
+  import { OA_NAV } from "@/config/oaPaths.js";
+  import {
+    buildTypeLabelMap,
+    CUSTOM_TEMPLATE_LIST_TYPE,
+    fetchApprovalTemplateTypes,
+    filterTemplatesByBusinessType,
+    getBusinessTypeLabel,
+    getDefaultTypeTabIndex,
+  } from "../../_utils/approvalTemplateType.js";
+
+  const typeOptions = ref([]);
+  const typeLabelMap = ref({});
+  /** 鍏ㄩ儴鑷畾涔夊凡鍚敤妯℃澘锛坙ist/1 涓�娆℃媺鍙栵級 */
+  const allTemplates = ref([]);
+  const activeTab = ref(0);
+  const keyword = ref("");
+  const loading = ref(false);
+
+  const tabList = computed(() =>
+    typeOptions.value.map(opt => ({ name: opt.name }))
+  );
+
+  const currentTypeOption = computed(() => typeOptions.value[activeTab.value]);
+
+  const currentSource = computed(() => {
+    const businessType = currentTypeOption.value?.value;
+    return filterTemplatesByBusinessType(allTemplates.value, businessType);
+  });
+
+  const displayList = computed(() => {
+    const kw = keyword.value?.trim().toLowerCase();
+    if (!kw) return currentSource.value;
+    return currentSource.value.filter(item =>
+      (item.templateName || "").toLowerCase().includes(kw)
+    );
+  });
+
+  const emptyText = computed(() => {
+    const typeName = currentTypeOption.value?.name || "璇ュ鎵圭被鍨�";
+    return `鏆傛棤${typeName}涓嬬殑妯℃澘`;
+  });
+
+  const businessTypeText = type =>
+    getBusinessTypeLabel(type, typeLabelMap.value);
+
+  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 nodeCount = item => {
+    const count = item?.nodes?.length;
+    return count != null ? `${count} 涓猔 : "-";
+  };
+
+  const normalizeList = data => {
+    const list = Array.isArray(data)
+      ? data
+      : Array.isArray(data?.records)
+        ? data.records
+        : [];
+    return list.filter(item => String(item?.enabled ?? "1") === "1");
+  };
+
+  const loadCustomTemplates = () =>
+    listApprovalTemplateByType(CUSTOM_TEMPLATE_LIST_TYPE)
+      .then(res => {
+        allTemplates.value = normalizeList(res?.data);
+      })
+      .catch(() => {
+        allTemplates.value = [];
+        uni.showToast({ title: "鍔犺浇妯℃澘鍒楄〃澶辫触", icon: "none" });
+      });
+
+  const initPage = async () => {
+    loading.value = true;
+    keyword.value = "";
+    allTemplates.value = [];
+    try {
+      const [opts] = await Promise.all([
+        fetchApprovalTemplateTypes(),
+        loadCustomTemplates(),
+      ]);
+      typeOptions.value = opts;
+      typeLabelMap.value = buildTypeLabelMap(opts);
+      activeTab.value = getDefaultTypeTabIndex(opts);
+    } catch {
+      typeOptions.value = [];
+      typeLabelMap.value = {};
+      uni.showToast({ title: "鑾峰彇瀹℃壒绫诲瀷澶辫触", icon: "none" });
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const onTabClick = item => {
+    activeTab.value = item?.index ?? 0;
+    keyword.value = "";
+  };
+
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  const selectTemplate = item => {
+    if (!item?.id) return;
+    if (String(item.enabled) === "0") {
+      uni.showToast({ title: "璇ユā鏉垮凡鍋滅敤", icon: "none" });
+      return;
+    }
+    uni.navigateTo({
+      url: `${OA_NAV.approveListApply}?templateId=${item.id}`,
+    });
+  };
+
+  onLoad(() => {
+    initPage();
+  });
+</script>
+
+<style scoped lang="scss">
+  @import "@/styles/sales-common.scss";
+
+  .template-select-page {
+    display: flex;
+    flex-direction: column;
+    min-height: 100vh;
+  }
+
+  .step-section {
+    background: #fff;
+    border-bottom: 1px solid #f0f0f0;
+  }
+
+  .step-label {
+    display: block;
+    padding: 10px 16px 0;
+    font-size: 13px;
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .step-hint {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+    padding: 10px 16px 4px;
+    gap: 8px;
+  }
+
+  .step-desc {
+    flex-shrink: 0;
+    font-size: 12px;
+    color: #909399;
+  }
+
+  .tabs-wrap {
+    padding: 0 12px 4px;
+  }
+
+  .list-scroll {
+    flex: 1;
+    height: 0;
+    padding-bottom: 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;
+  }
+
+  .empty-wrap {
+    padding: 48px 20px;
+  }
+
+  .ledger-item--clickable:active {
+    opacity: 0.92;
+  }
+
+  .card-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 10px;
+    padding-top: 10px;
+    border-top: 1px dashed #e8ecf0;
+  }
+
+  .card-footer-tip {
+    font-size: 13px;
+    color: #2979ff;
+  }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/detail.vue b/src/pages/oa/ApproveManage/approve-template/detail.vue
index 804ddd9..75dba50 100644
--- a/src/pages/oa/ApproveManage/approve-template/detail.vue
+++ b/src/pages/oa/ApproveManage/approve-template/detail.vue
@@ -24,8 +24,8 @@
               <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>
+              <text class="info-label">瀹℃壒绫诲瀷</text>
+              <text class="info-value">{{ businessTypeText(detail.businessType) }}</text>
             </view>
             <view class="info-item">
               <text class="info-label">鍚敤鐘舵��</text>
@@ -125,6 +125,11 @@
   import PageHeader from "@/components/PageHeader.vue";
   import FooterButtons from "@/components/FooterButtons.vue";
   import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
+  import {
+    buildTypeLabelMap,
+    fetchApprovalTemplateTypes,
+    getTemplateTypeLabel,
+  } from "../../_utils/approvalTemplateType.js";
 
   const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
   const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
@@ -139,6 +144,7 @@
   const templateId = ref("");
   const detail = ref(null);
   const loading = ref(false);
+  const typeLabelMap = ref({});
 
   const formConfigData = computed(() => {
     const raw = detail.value?.formConfig;
@@ -156,12 +162,8 @@
 
   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 businessTypeText = type =>
+    getTemplateTypeLabel(type, typeLabelMap.value);
 
   const enabledText = enabled => {
     const val = String(enabled ?? "");
@@ -213,6 +215,11 @@
   };
 
   onLoad(options => {
+    fetchApprovalTemplateTypes()
+      .then(opts => {
+        typeLabelMap.value = buildTypeLabelMap(opts);
+      })
+      .catch(() => {});
     if (options?.id) {
       templateId.value = options.id;
       loadDetail();
diff --git a/src/pages/oa/ApproveManage/approve-template/edit.vue b/src/pages/oa/ApproveManage/approve-template/edit.vue
index 3b50270..aba2103 100644
--- a/src/pages/oa/ApproveManage/approve-template/edit.vue
+++ b/src/pages/oa/ApproveManage/approve-template/edit.vue
@@ -28,19 +28,18 @@
                       maxlength="50"
                       clearable />
           </up-form-item>
-          <up-form-item label="妯℃澘绫诲瀷"
-                        prop="templateType"
+          <up-form-item label="瀹℃壒绫诲瀷"
+                        prop="businessType"
                         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>
+                        class="form-item-select"
+                        @click="openBusinessTypeSheet">
+            <up-input :model-value="businessTypeText"
+                      placeholder="璇烽�夋嫨瀹℃壒绫诲瀷"
+                      readonly />
+            <template #right>
+              <up-icon name="arrow-right"
+                       @click.stop="openBusinessTypeSheet" />
+            </template>
           </up-form-item>
           <up-form-item label="鍚敤鐘舵��"
                         class="form-item-switch">
@@ -311,6 +310,12 @@
         </scroll-view>
       </view>
     </up-popup>
+
+    <up-action-sheet :show="showBusinessTypeSheet"
+                     title="閫夋嫨瀹℃壒绫诲瀷"
+                     :actions="businessTypeActions"
+                     @select="onSelectBusinessType"
+                     @close="showBusinessTypeSheet = false" />
   </view>
 </template>
 
@@ -325,6 +330,7 @@
   } from "@/api/oa/approvalTemplate.js";
   import { userListNoPageByTenantId } from "@/api/system/user";
   import { formatDateToYMD } from "@/utils/ruoyi";
+  import { fetchApprovalTemplateTypes } from "../../_utils/approvalTemplateType.js";
 
   const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
 
@@ -384,6 +390,7 @@
 
   const form = reactive({
     templateName: "",
+    businessType: null,
     templateType: 1,
     enabled: "1",
     description: "",
@@ -413,11 +420,11 @@
 
   const rules = {
     templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
-    templateType: [
+    businessType: [
       {
         validator: (_rule, value, callback) => {
           if (value === "" || value === null || value === undefined) {
-            callback(new Error("璇烽�夋嫨妯℃澘绫诲瀷"));
+            callback(new Error("璇烽�夋嫨瀹℃壒绫诲瀷"));
             return;
           }
           callback();
@@ -427,10 +434,22 @@
     ],
   };
 
-  const TEMPLATE_TYPE_OPTIONS = [
-    { name: "绯荤粺鍐呯疆", value: 0 },
-    { name: "鑷畾涔�", value: 1 },
-  ];
+  const businessTypeOptions = ref([]);
+  const showBusinessTypeSheet = ref(false);
+
+  const businessTypeActions = computed(() =>
+    businessTypeOptions.value.map(opt => ({
+      name: opt.name,
+      value: opt.value,
+    }))
+  );
+
+  const businessTypeText = computed(() => {
+    const matched = businessTypeOptions.value.find(
+      opt => String(opt.value) === String(form.businessType)
+    );
+    return matched?.name || "";
+  });
 
   const presetActions = FORM_PRESETS.map(item => ({
     name: item.name,
@@ -487,10 +506,12 @@
     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;
+    const parsedBusiness = Number(row.businessType);
+    form.businessType = Number.isNaN(parsedBusiness)
+      ? row.businessType
+      : parsedBusiness;
+    const parsedTemplateType = Number(row.templateType);
+    form.templateType = Number.isNaN(parsedTemplateType) ? 1 : parsedTemplateType;
     form.enabled = String(row.enabled ?? "1");
     form.description = row.description || "";
 
@@ -526,8 +547,18 @@
     uni.navigateBack();
   };
 
-  const onTemplateTypeChange = () => {
-    formRef.value?.validateField?.("templateType");
+  const openBusinessTypeSheet = () => {
+    if (!businessTypeOptions.value.length) {
+      uni.showToast({ title: "瀹℃壒绫诲瀷鍔犺浇涓�", icon: "none" });
+      return;
+    }
+    showBusinessTypeSheet.value = true;
+  };
+
+  const onSelectBusinessType = action => {
+    form.businessType = action.value;
+    showBusinessTypeSheet.value = false;
+    formRef.value?.validateField?.("businessType");
   };
 
   const onSelectPreset = action => {
@@ -710,6 +741,7 @@
       templateName: form.templateName.trim(),
       enabled: form.enabled,
       description: form.description?.trim() || "",
+      businessType: form.businessType,
       templateType: form.templateType,
       formConfig: JSON.stringify({
         prompt: formConfig.prompt?.trim() || "",
@@ -792,7 +824,25 @@
     }
   });
 
+  const loadTemplateTypes = () =>
+    fetchApprovalTemplateTypes()
+      .then(opts => {
+        businessTypeOptions.value = opts;
+        if (!templateId.value && opts.length) {
+          const matched = opts.some(
+            opt => String(opt.value) === String(form.businessType)
+          );
+          if (!matched) {
+            form.businessType = opts[0].value;
+          }
+        }
+      })
+      .catch(() => {
+        uni.showToast({ title: "鑾峰彇瀹℃壒绫诲瀷澶辫触", icon: "none" });
+      });
+
   onMounted(() => {
+    loadTemplateTypes();
     userListNoPageByTenantId()
       .then(res => {
         userList.value = res?.data || [];
@@ -926,18 +976,18 @@
     font-size: 15px !important;
   }
 
-  :deep(.form-item-type .u-form-item__body) {
+  :deep(.form-item-select .u-form-item__body) {
     align-items: center !important;
   }
 
-  .type-radio-group {
-    display: flex;
-    justify-content: flex-end;
-    flex-wrap: nowrap;
+  :deep(.form-item-select .u-form-item__content) {
+    flex: 1 !important;
+    min-width: 0 !important;
+    justify-content: flex-end !important;
   }
 
-  :deep(.type-radio-group .u-radio) {
-    margin-left: 20px;
+  :deep(.form-item-select .u-input__content__field-wrapper__field) {
+    text-align: right !important;
   }
 
   :deep(.form-item-switch .u-form-item__body) {
diff --git a/src/pages/oa/ApproveManage/approve-template/index.vue b/src/pages/oa/ApproveManage/approve-template/index.vue
index 96660bc..44b77c9 100644
--- a/src/pages/oa/ApproveManage/approve-template/index.vue
+++ b/src/pages/oa/ApproveManage/approve-template/index.vue
@@ -48,8 +48,8 @@
           <up-divider />
           <view class="item-details">
             <view class="detail-row">
-              <text class="detail-label">妯℃澘绫诲瀷</text>
-              <text class="detail-value">{{ templateTypeText(item.templateType) }}</text>
+              <text class="detail-label">瀹℃壒绫诲瀷</text>
+              <text class="detail-value">{{ businessTypeText(item.businessType) }}</text>
             </view>
             <view class="detail-row">
               <text class="detail-label">瀹℃壒鑺傜偣</text>
@@ -115,8 +115,14 @@
     deleteApprovalTemplate,
     listApprovalTemplatePage,
   } from "@/api/oa/approvalTemplate.js";
+  import {
+    buildTypeLabelMap,
+    fetchApprovalTemplateTypes,
+    getTemplateTypeLabel,
+  } from "../../_utils/approvalTemplateType.js";
 
   const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+  const typeLabelMap = ref({});
 
   const queryParams = reactive({
     templateName: "",
@@ -155,12 +161,15 @@
     return "info";
   };
 
-  const templateTypeText = type => {
-    const val = Number(type);
-    if (val === 0) return "绯荤粺鍐呯疆";
-    if (val === 1) return "鑷畾涔�";
-    return "-";
-  };
+  const businessTypeText = type =>
+    getTemplateTypeLabel(type, typeLabelMap.value);
+
+  const loadTemplateTypes = () =>
+    fetchApprovalTemplateTypes()
+      .then(opts => {
+        typeLabelMap.value = buildTypeLabelMap(opts);
+      })
+      .catch(() => {});
 
   const nodeCount = item => {
     const count = item?.nodes?.length;
@@ -266,6 +275,7 @@
   };
 
   onShow(() => {
+    loadTemplateTypes();
     handleSearch();
   });
 </script>
diff --git a/src/pages/oa/_utils/approvalTemplateType.js b/src/pages/oa/_utils/approvalTemplateType.js
new file mode 100644
index 0000000..fce013e
--- /dev/null
+++ b/src/pages/oa/_utils/approvalTemplateType.js
@@ -0,0 +1,94 @@
+import { getTypeEnums } from "@/api/basic/enum.js";
+
+/**
+ * GET /approvalTemplate/list/{type} 璺緞鍙傛暟涓� templateType
+ * 1 = 鑷畾涔変笖宸插惎鐢紙涓� businessType 鏃犲叧锛�
+ */
+export const CUSTOM_TEMPLATE_LIST_TYPE = 1;
+
+/** 涓氬姟绫诲瀷鏋氫妇鍏滃簳锛坅pproveType锛�1鍏嚭 2璇峰亣 鈥︼級 */
+export const FALLBACK_BUSINESS_TYPE_OPTIONS = [
+  { name: "鍏嚭绠$悊", value: 1 },
+  { name: "璇峰亣绠$悊", value: 2 },
+];
+
+/** 灏� /basic/enum/TypeEnums 鍝嶅簲瑙勮寖涓� { name, value }[] */
+export function normalizeEnumOptions(data) {
+  if (!data) return [];
+
+  if (Array.isArray(data)) {
+    return data
+      .map(item => {
+        const name =
+          item?.name ??
+          item?.label ??
+          item?.text ??
+          item?.dictLabel ??
+          item?.description;
+        const rawValue =
+          item?.value ?? item?.code ?? item?.dictValue ?? item?.key ?? item?.id;
+        if (name == null || rawValue === undefined || rawValue === null) {
+          return null;
+        }
+        const num = Number(rawValue);
+        return {
+          name: String(name),
+          value: Number.isNaN(num) ? rawValue : num,
+        };
+      })
+      .filter(Boolean);
+  }
+
+  if (typeof data === "object") {
+    return Object.entries(data).map(([value, name]) => {
+      const num = Number(value);
+      return {
+        name: String(name),
+        value: Number.isNaN(num) ? value : num,
+      };
+    });
+  }
+
+  return [];
+}
+
+/** 鎷夊彇涓氬姟绫诲瀷鏋氫妇锛圱ypeEnums 鈫� businessType锛� */
+export async function fetchApprovalTemplateTypes() {
+  const res = await getTypeEnums();
+  const options = normalizeEnumOptions(res?.data);
+  return options.length ? options : [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+}
+
+/** 鎸� businessType 绛涢�夋ā鏉� */
+export function filterTemplatesByBusinessType(templates, businessType) {
+  if (businessType == null || businessType === "") return [];
+  return (templates || []).filter(
+    item => String(item.businessType) === String(businessType)
+  );
+}
+
+/** 榛樿 Tab 涓嬫爣锛堝彲鎸変笟鍔$被鍨� value 鎸囧畾锛岄粯璁ょ涓�椤癸級 */
+export function getDefaultTypeTabIndex(options, defaultBusinessType) {
+  if (!options?.length) return 0;
+  if (defaultBusinessType == null) return 0;
+  const idx = options.findIndex(
+    opt => String(opt.value) === String(defaultBusinessType)
+  );
+  return idx >= 0 ? idx : 0;
+}
+
+export function buildTypeLabelMap(options) {
+  const map = {};
+  (options || []).forEach(opt => {
+    map[String(opt.value)] = opt.name;
+  });
+  return map;
+}
+
+/** 鏍规嵁 businessType 鏄剧ず涓氬姟绫诲瀷鍚嶇О */
+export function getTemplateTypeLabel(type, labelMap) {
+  if (type == null || type === "") return "-";
+  return labelMap?.[String(type)] ?? String(type);
+}
+
+export const getBusinessTypeLabel = getTemplateTypeLabel;

--
Gitblit v1.9.3