From 6d6e3204f92d763e5df11d26702f6642a993e49e Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期四, 21 五月 2026 10:21:40 +0800
Subject: [PATCH] 增强审批模板功能,新增内置模板类型支持,优化模板编辑和导入逻辑,确保内置模板不可编辑和删除,提升用户体验和代码可维护性。

---
 src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue |   93 +++++++++++++++++++++++++++---
 src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js     |   19 ++++--
 src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js           |   27 +++++++-
 src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue                       |   24 +++++++-
 4 files changed, 138 insertions(+), 25 deletions(-)

diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
index 4d7d7a6..727f896 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -1,6 +1,9 @@
 import dayjs from "dayjs";
 import { getTypeEnums } from "@/api/basicData/enum.js";
-import { TEMPLATE_TYPE_CUSTOM } from "@/api/officeProcessAutomation/approvalTemplate.js";
+import {
+  TEMPLATE_TYPE_BUILTIN,
+  TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
 import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js";
 import {
   buildFormConfigJson,
@@ -42,6 +45,11 @@
   } catch {
     return [];
   }
+}
+
+/** 鏄惁涓虹郴缁熷唴缃ā鏉匡紙templateType === 0锛� */
+export function isBuiltinTemplate(row) {
+  return Number(row?.templateType) === TEMPLATE_TYPE_BUILTIN;
 }
 
 /** 鑺傜偣鍐呭鎵规柟寮忥細浼氱 / 鎴栫 */
@@ -222,7 +230,8 @@
     templateName: (form.templateName || "").trim(),
     description: (form.description || "").trim(),
     enabled: mapEnabledToApi(form.enabled),
-    templateType: TEMPLATE_TYPE_CUSTOM,
+    templateType:
+      form.templateType != null ? Number(form.templateType) : TEMPLATE_TYPE_CUSTOM,
     businessType: form.businessType ?? "",
     formConfig: buildFormConfigJson(form.formConfigData),
     nodes: nodes.map((n, i) => {
@@ -254,13 +263,10 @@
   return dto;
 }
 
-export function buildApprovalTemplateListParams({ page, searchForm, templateType = TEMPLATE_TYPE_CUSTOM }) {
+export function buildApprovalTemplateListParams({ page, searchForm }) {
   const params = {
     current: page.current,
     size: page.size,
-    templateType: searchForm?.templateType != null && searchForm.templateType !== ""
-      ? searchForm.templateType
-      : templateType,
   };
   const kw = (searchForm?.keyword || "").trim();
   if (kw) params.templateName = kw;
@@ -290,6 +296,7 @@
     templateName: "",
     description: "",
     templateType: TEMPLATE_TYPE_CUSTOM,
+    lockedFormFieldUids: [],
     businessType: "",
     formConfig: "",
     formConfigData: createEmptyFormConfigData(),
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
index a5d1da7..6880f3f 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -21,8 +21,15 @@
           </el-tag>
         </div>
         <div class="fce-toolbar-actions">
-          <el-dropdown trigger="click" @visible-change="onImportDropdownVisible" @command="importFromTemplate">
-            <el-button size="small" :loading="templateImportLoading">浠庡凡鏈夋ā鏉垮鍏�</el-button>
+          <el-dropdown
+            trigger="click"
+            :disabled="disableImport"
+            @visible-change="onImportDropdownVisible"
+            @command="importFromTemplate"
+          >
+            <el-button size="small" :loading="templateImportLoading" :disabled="disableImport">
+              浠庡凡鏈夋ā鏉垮鍏�
+            </el-button>
             <template #dropdown>
               <el-dropdown-menu>
                 <el-dropdown-item v-if="!templateImportOptions.length" disabled>
@@ -55,7 +62,10 @@
           v-for="(field, index) in inner.fields"
           :key="field._uid"
           class="fce-card"
-          :class="{ 'fce-card--required': field.required }"
+          :class="{
+            'fce-card--required': field.required,
+            'fce-card--locked': isFieldLocked(field),
+          }"
         >
           <div class="fce-card-badge">{{ index + 1 }}</div>
 
@@ -64,8 +74,9 @@
               <span class="fce-card-name">{{ field.label || `濉姤椤� ${index + 1}` }}</span>
               <el-tag size="small" effect="light" type="primary">{{ typeLabel(field.type) }}</el-tag>
               <el-tag v-if="field.required" size="small" type="danger" effect="plain">蹇呭~</el-tag>
+              <el-tag v-if="isFieldLocked(field)" size="small" type="info" effect="plain">鍐呯疆椤�</el-tag>
             </div>
-            <div class="fce-card-btns">
+            <div v-if="!isFieldLocked(field)" class="fce-card-btns">
               <el-tooltip content="涓婄Щ" placement="top">
                 <el-button circle size="small" :disabled="index === 0" @click="moveField(index, -1)">
                   <el-icon><Top /></el-icon>
@@ -98,18 +109,30 @@
                     v-model="field.label"
                     placeholder="濡傦細鎶ラ攢璇存槑"
                     maxlength="50"
+                    :disabled="isFieldLocked(field)"
                     @input="emitOut"
                   />
                 </el-form-item>
               </el-col>
               <el-col :span="8">
                 <el-form-item label="瀛楁鏍囪瘑" required class="fce-field-item">
-                  <el-input v-model="field.key" placeholder="濡傦細summary" maxlength="50" @input="emitOut" />
+                  <el-input
+                    v-model="field.key"
+                    placeholder="濡傦細summary"
+                    maxlength="50"
+                    :disabled="isFieldLocked(field)"
+                    @input="emitOut"
+                  />
                 </el-form-item>
               </el-col>
               <el-col :span="8">
                 <el-form-item label="鎺т欢绫诲瀷" class="fce-field-item">
-                  <el-select v-model="field.type" style="width: 100%" @change="onTypeChange(field)">
+                  <el-select
+                    v-model="field.type"
+                    style="width: 100%"
+                    :disabled="isFieldLocked(field)"
+                    @change="onTypeChange(field)"
+                  >
                     <el-option
                       v-for="t in FORM_FIELD_TYPE_OPTIONS"
                       :key="t.value"
@@ -132,6 +155,7 @@
                     inline-prompt
                     active-text="蹇呭~"
                     inactive-text="閫夊~"
+                    :disabled="isFieldLocked(field)"
                     @change="emitOut"
                   />
                 </el-form-item>
@@ -144,6 +168,7 @@
                     :max="10"
                     controls-position="right"
                     style="width: 100%"
+                    :disabled="isFieldLocked(field)"
                     @change="emitOut"
                   />
                 </el-form-item>
@@ -155,6 +180,7 @@
                       v-model="field.min"
                       controls-position="right"
                       style="width: 100%"
+                      :disabled="isFieldLocked(field)"
                       @change="emitOut"
                     />
                   </el-form-item>
@@ -167,6 +193,7 @@
                       :max="4"
                       controls-position="right"
                       style="width: 100%"
+                      :disabled="isFieldLocked(field)"
                       @change="emitOut"
                     />
                   </el-form-item>
@@ -184,6 +211,7 @@
               :type="field.type === 'textarea' ? 'textarea' : 'text'"
               :rows="field.type === 'textarea' ? 2 : undefined"
               :placeholder="defaultPlaceholder(field)"
+              :disabled="isFieldLocked(field)"
               clearable
               @input="emitOut"
             />
@@ -195,6 +223,7 @@
               controls-position="right"
               placeholder="閫夊~"
               style="width: 100%"
+              :disabled="isFieldLocked(field)"
               @change="emitOut"
             />
             <el-date-picker
@@ -205,6 +234,7 @@
               format="YYYY-MM-DD"
               value-format="YYYY-MM-DD"
               style="width: 100%"
+              :disabled="isFieldLocked(field)"
               clearable
               @change="emitOut"
             />
@@ -218,6 +248,7 @@
               format="YYYY-MM-DD HH:mm:ss"
               value-format="YYYY-MM-DD HH:mm:ss"
               style="width: 100%"
+              :disabled="isFieldLocked(field)"
               clearable
               @change="emitOut"
             />
@@ -229,6 +260,7 @@
               clearable
               filterable
               :loading="optionSourceLoading"
+              :disabled="isFieldLocked(field)"
               @change="emitOut"
             >
               <el-option
@@ -248,6 +280,7 @@
                   <el-select
                     v-model="field.optionSource"
                     style="width: 100%"
+                    :disabled="isFieldLocked(field)"
                     @change="onOptionSourceChange(field)"
                   >
                     <el-option
@@ -266,7 +299,14 @@
             <template v-if="!isDynamicOptionSource(field.optionSource)">
               <div class="fce-options-head">
                 <span class="fce-section-subtitle">鎵嬪姩閫夐」</span>
-                <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
+                <el-button
+                  type="primary"
+                  link
+                  size="small"
+                  :icon="Plus"
+                  :disabled="isFieldLocked(field)"
+                  @click="addOption(field)"
+                >
                   娣诲姞閫夐」
                 </el-button>
               </div>
@@ -276,13 +316,24 @@
                 class="fce-option-row"
               >
                 <span class="fce-option-index">{{ oi + 1 }}</span>
-                <el-input v-model="opt.label" placeholder="鏄剧ず鏂囨湰" @input="emitOut" />
-                <el-input v-model="opt.value" placeholder="閫夐」鍊�" class="fce-option-value" @input="emitOut" />
+                <el-input
+                  v-model="opt.label"
+                  placeholder="鏄剧ず鏂囨湰"
+                  :disabled="isFieldLocked(field)"
+                  @input="emitOut"
+                />
+                <el-input
+                  v-model="opt.value"
+                  placeholder="閫夐」鍊�"
+                  class="fce-option-value"
+                  :disabled="isFieldLocked(field)"
+                  @input="emitOut"
+                />
                 <el-button
                   type="danger"
                   link
                   :icon="Delete"
-                  :disabled="field.options.length <= 1"
+                  :disabled="isFieldLocked(field) || field.options.length <= 1"
                   @click="removeOption(field, oi)"
                 />
               </div>
@@ -303,7 +354,7 @@
   TEMPLATE_TYPE_CUSTOM,
 } from "@/api/officeProcessAutomation/approvalTemplate.js";
 import { ElMessage, ElMessageBox } from "element-plus";
-import { reactive, ref, watch } from "vue";
+import { computed, reactive, ref, watch } from "vue";
 import {
   mapEnabledFromApi,
   unwrapTemplateDetail,
@@ -327,6 +378,10 @@
   modelValue: { type: Object, default: () => createEmptyFormConfigData() },
   /** 缂栬緫褰撳墠妯℃澘鏃舵帓闄よ嚜韬紝閬垮厤浠庤嚜宸卞鍏� */
   excludeTemplateId: { type: [String, Number], default: null },
+  /** 绂佺敤銆屼粠宸叉湁妯℃澘瀵煎叆銆� */
+  disableImport: { type: Boolean, default: false },
+  /** 绯荤粺鍐呯疆妯℃澘缂栬緫鏃讹紝鎵撳紑寮圭獥鍗冲瓨鍦ㄧ殑濉姤椤� _uid锛屼笉鍙敼鍒� */
+  lockedFieldUids: { type: Array, default: () => [] },
 });
 
 const emit = defineEmits(["update:modelValue"]);
@@ -337,6 +392,14 @@
 
 const templateImportOptions = ref([]);
 const templateImportLoading = ref(false);
+
+const lockedUidSet = computed(
+  () => new Set((props.lockedFieldUids || []).filter(Boolean))
+);
+
+function isFieldLocked(field) {
+  return field?._uid != null && lockedUidSet.value.has(field._uid);
+}
 
 function typeLabel(type) {
   return formFieldTypeLabel(type);
@@ -408,13 +471,16 @@
 }
 
 function removeField(index) {
+  if (isFieldLocked(inner.fields[index])) return;
   inner.fields.splice(index, 1);
   emitOut();
 }
 
 function moveField(index, delta) {
+  if (isFieldLocked(inner.fields[index])) return;
   const next = index + delta;
   if (next < 0 || next >= inner.fields.length) return;
+  if (isFieldLocked(inner.fields[next])) return;
   const t = inner.fields[index];
   inner.fields[index] = inner.fields[next];
   inner.fields[next] = t;
@@ -486,6 +552,7 @@
 }
 
 function onImportDropdownVisible(visible) {
+  if (props.disableImport) return;
   if (visible) loadTemplateImportOptions();
 }
 
@@ -610,6 +677,10 @@
   border-left: 3px solid var(--el-color-danger-light-3);
 }
 
+.fce-card--locked {
+  background: var(--el-fill-color-light);
+}
+
 .fce-card-badge {
   position: absolute;
   top: -10px;
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
index edd9c56..d094c13 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -114,7 +114,13 @@
 
             <el-form-item label="妯℃澘鍚嶇О" prop="templateName">
 
-              <el-input v-model="form.templateName" placeholder="濡傦細椤圭洰绔嬮」瀹℃壒" maxlength="50" show-word-limit />
+              <el-input
+                v-model="form.templateName"
+                placeholder="濡傦細椤圭洰绔嬮」瀹℃壒"
+                maxlength="50"
+                show-word-limit
+                :disabled="isEditingBuiltin"
+              />
 
             </el-form-item>
 
@@ -124,7 +130,12 @@
 
             <el-form-item label="妯℃澘绫诲瀷" prop="businessType">
 
-              <el-select v-model="form.businessType" placeholder="璇烽�夋嫨" style="width: 100%">
+              <el-select
+                v-model="form.businessType"
+                placeholder="璇烽�夋嫨"
+                style="width: 100%"
+                :disabled="isEditingBuiltin"
+              >
 
                 <el-option
 
@@ -178,7 +189,12 @@
 
         <el-form-item label="濉姤閰嶇疆">
 
-          <FormConfigEditor v-model="form.formConfigData" :exclude-template-id="form.id" />
+          <FormConfigEditor
+            v-model="form.formConfigData"
+            :exclude-template-id="form.id"
+            :disable-import="isEditingBuiltin"
+            :locked-field-uids="isEditingBuiltin ? form.lockedFormFieldUids : []"
+          />
 
           <p class="flow-tip">閰嶇疆鎻愪氦瀹℃壒鏃堕渶濉啓鐨勮〃鍗曢」锛屼繚瀛樺悗鍐欏叆 formConfig锛圝SON锛夈��</p>
 
@@ -459,6 +475,8 @@
 
   formRules,
 
+  isEditingBuiltin,
+
   detailDialog,
 
   detailRow,
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
index 0dbf2ca..61aa6c0 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -3,17 +3,18 @@
   deleteApprovalTemplate,
   getApprovalTemplateDetail,
   listApprovalTemplatePage,
-  TEMPLATE_TYPE_CUSTOM,
+  TEMPLATE_TYPE_BUILTIN,
   updateApprovalTemplate,
 } from "@/api/officeProcessAutomation/approvalTemplate.js";
 import { Search } from "@element-plus/icons-vue";
 import { ElMessage, ElMessageBox } from "element-plus";
-import { reactive, ref } from "vue";
+import { computed, reactive, ref } from "vue";
 import {
   buildApprovalTemplateListParams,
   createEmptyTemplateForm,
   fetchBusinessTypeOptions,
   flowNodesSummary,
+  isBuiltinTemplate,
   mapTemplateFromApi,
   mapTemplateToApi,
   nodeSignModeLabel,
@@ -58,6 +59,10 @@
   const formDialog = reactive({ visible: false, title: "", mode: "add" });
   const form = reactive(createEmptyTemplateForm());
   const formRef = ref();
+
+  const isEditingBuiltin = computed(
+    () => formDialog.mode === "edit" && Number(form.templateType) === TEMPLATE_TYPE_BUILTIN
+  );
 
   async function loadTemplateTypeOptions() {
     try {
@@ -140,6 +145,7 @@
           name: "鍒犻櫎",
           type: "danger",
           link: true,
+          disabled: (row) => isBuiltinTemplate(row),
           clickFun: (row) => removeTemplate(row),
         },
       ],
@@ -186,16 +192,22 @@
       Object.assign(form, base);
       return;
     }
+    const formConfigData = JSON.parse(
+      JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
+    );
+    const builtin = isBuiltinTemplate(row);
     Object.assign(form, {
       ...base,
       id: row.id,
       templateName: row.templateName || "",
       description: row.description || "",
+      templateType: row.templateType != null ? Number(row.templateType) : base.templateType,
       businessType: row.businessType ?? "",
       formConfig: row.formConfig || "",
-      formConfigData: JSON.parse(
-        JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
-      ),
+      formConfigData,
+      lockedFormFieldUids: builtin
+        ? (formConfigData.fields || []).map((f) => f._uid).filter(Boolean)
+        : [],
       enabled: row.enabled !== false,
       flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
       storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
@@ -258,6 +270,10 @@
   }
 
   async function removeTemplate(row) {
+    if (isBuiltinTemplate(row)) {
+      ElMessage.warning("绯荤粺鍐呯疆妯℃澘涓嶅厑璁稿垹闄�");
+      return;
+    }
     if (row?.id == null || row.id === "") {
       ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戞ā鏉� ID");
       return;
@@ -304,6 +320,7 @@
     form,
     formRef,
     formRules,
+    isEditingBuiltin,
     detailDialog,
     detailRow,
     detailLoading,

--
Gitblit v1.9.3