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 | 317 +++++++++++++++++++++++++++++++++++++++++++++-------
1 files changed, 275 insertions(+), 42 deletions(-)
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
index 1881f60..6880f3f 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -21,12 +21,27 @@
</el-tag>
</div>
<div class="fce-toolbar-actions">
- <el-dropdown trigger="click" @command="applyPreset">
- <el-button size="small">浠庨璁惧鍏�</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-for="p in FORM_CONFIG_PRESETS" :key="p.key" :command="p.key">
- {{ p.label }}
+ <el-dropdown-item v-if="!templateImportOptions.length" disabled>
+ 鏆傛棤鍏朵粬瀹℃壒妯℃澘
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-for="t in templateImportOptions"
+ :key="t.id"
+ :command="t.id"
+ >
+ <span>{{ t.label }}</span>
+ <el-tag v-if="!t.enabled" size="small" type="info" class="import-tag">宸插仠鐢�</el-tag>
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -38,7 +53,7 @@
<el-empty
v-if="!inner.fields.length"
class="fce-empty"
- description="鏆傛棤濉姤椤癸紝鍙坊鍔犳垨浠庨璁惧揩閫熷鍏�"
+ description="鏆傛棤濉姤椤癸紝鍙坊鍔犳垨浠庡凡鏈夊鎵规ā鏉垮鍏�"
:image-size="72"
/>
@@ -47,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>
@@ -56,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>
@@ -90,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"
@@ -124,6 +155,7 @@
inline-prompt
active-text="蹇呭~"
inactive-text="閫夊~"
+ :disabled="isFieldLocked(field)"
@change="emitOut"
/>
</el-form-item>
@@ -136,6 +168,7 @@
:max="10"
controls-position="right"
style="width: 100%"
+ :disabled="isFieldLocked(field)"
@change="emitOut"
/>
</el-form-item>
@@ -147,6 +180,7 @@
v-model="field.min"
controls-position="right"
style="width: 100%"
+ :disabled="isFieldLocked(field)"
@change="emitOut"
/>
</el-form-item>
@@ -159,6 +193,7 @@
:max="4"
controls-position="right"
style="width: 100%"
+ :disabled="isFieldLocked(field)"
@change="emitOut"
/>
</el-form-item>
@@ -176,6 +211,7 @@
:type="field.type === 'textarea' ? 'textarea' : 'text'"
:rows="field.type === 'textarea' ? 2 : undefined"
:placeholder="defaultPlaceholder(field)"
+ :disabled="isFieldLocked(field)"
clearable
@input="emitOut"
/>
@@ -187,6 +223,7 @@
controls-position="right"
placeholder="閫夊~"
style="width: 100%"
+ :disabled="isFieldLocked(field)"
@change="emitOut"
/>
<el-date-picker
@@ -197,6 +234,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
+ :disabled="isFieldLocked(field)"
clearable
@change="emitOut"
/>
@@ -210,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"
/>
@@ -219,10 +258,13 @@
placeholder="閫夊~"
style="width: 100%"
clearable
+ filterable
+ :loading="optionSourceLoading"
+ :disabled="isFieldLocked(field)"
@change="emitOut"
>
<el-option
- v-for="o in field.options.filter((x) => x.value !== '' && x.value != null)"
+ v-for="o in resolvedSelectOptions(field)"
:key="String(o.value)"
:label="o.label || o.value"
:value="o.value"
@@ -231,28 +273,71 @@
</div>
<div v-if="field.type === 'select'" class="fce-section fce-section--options">
- <div class="fce-options-head">
- <span class="fce-section-title">涓嬫媺閫夐」</span>
- <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
- 娣诲姞閫夐」
- </el-button>
- </div>
- <div
- v-for="(opt, oi) in field.options"
- :key="oi"
- class="fce-option-row"
- >
- <span class="fce-option-index">{{ oi + 1 }}</span>
- <el-input v-model="opt.label" placeholder="鏄剧ず鏂囨湰" @input="emitOut" />
- <el-input v-model="opt.value" placeholder="閫夐」鍊�" class="fce-option-value" @input="emitOut" />
- <el-button
- type="danger"
- link
- :icon="Delete"
- :disabled="field.options.length <= 1"
- @click="removeOption(field, oi)"
- />
- </div>
+ <span class="fce-section-title">涓嬫媺閫夐」</span>
+ <el-row :gutter="16" class="fce-source-row">
+ <el-col :span="12">
+ <el-form-item label="閫夐」鏉ユ簮" class="fce-field-item">
+ <el-select
+ v-model="field.optionSource"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="onOptionSourceChange(field)"
+ >
+ <el-option
+ v-for="s in SELECT_OPTION_SOURCE_OPTIONS"
+ :key="s.value"
+ :label="s.label"
+ :value="s.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <p v-if="isDynamicOptionSource(field.optionSource)" class="fce-source-tip">
+ {{ optionSourceDesc(field.optionSource) }}銆傛彁浜ゅ鎵规椂灏嗚嚜鍔ㄥ姞杞芥渶鏂版暟鎹紝鏃犻渶鎵嬪姩缁存姢閫夐」銆�
+ </p>
+ <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"
+ :disabled="isFieldLocked(field)"
+ @click="addOption(field)"
+ >
+ 娣诲姞閫夐」
+ </el-button>
+ </div>
+ <div
+ v-for="(opt, oi) in field.options"
+ :key="oi"
+ class="fce-option-row"
+ >
+ <span class="fce-option-index">{{ oi + 1 }}</span>
+ <el-input
+ v-model="opt.label"
+ placeholder="鏄剧ず鏂囨湰"
+ :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="isFieldLocked(field) || field.options.length <= 1"
+ @click="removeOption(field, oi)"
+ />
+ </div>
+ </template>
</div>
</div>
</div>
@@ -262,23 +347,59 @@
<script setup>
import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue";
-import { reactive, watch } from "vue";
import {
- FORM_CONFIG_PRESETS,
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ TEMPLATE_TYPE_BUILTIN,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref, watch } from "vue";
+import {
+ mapEnabledFromApi,
+ unwrapTemplateDetail,
+ unwrapTemplateList,
+} from "../approveTemplateConstants.js";
+import {
FORM_FIELD_TYPE_OPTIONS,
- applyFormConfigPreset,
createEmptyFormConfigData,
createEmptyFormField,
formFieldTypeLabel,
+ parseFormConfigToData,
} from "../formConfigUtils.js";
+import {
+ SELECT_OPTION_SOURCE,
+ SELECT_OPTION_SOURCE_OPTIONS,
+ isDynamicOptionSource,
+} from "../selectOptionSource.js";
+import { useSelectOptionSources } from "../useSelectOptionSources.js";
const props = defineProps({
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"]);
const inner = reactive(createEmptyFormConfigData());
+
+const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources();
+
+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);
@@ -289,6 +410,15 @@
return `閫夊~锛岄�夋嫨妯℃澘鏃跺皢棰勫~${name}`;
}
+function optionSourceDesc(source) {
+ return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.desc || "";
+}
+
+function resolvedSelectOptions(field) {
+ if (field.type !== "select") return [];
+ return getOptions(field);
+}
+
function syncFromProps(v) {
const src = v || createEmptyFormConfigData();
inner.summaryPlaceholder = src.summaryPlaceholder || "";
@@ -296,8 +426,10 @@
...createEmptyFormField(),
...f,
_uid: f._uid || createEmptyFormField()._uid,
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })),
}));
+ ensureForFields(inner.fields);
}
function emitOut() {
@@ -313,6 +445,7 @@
min: f.min,
precision: f.precision,
defaultValue: cloneDefaultValue(f),
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
options: (f.options || []).map((o) => ({ label: o.label, value: o.value })),
})),
});
@@ -333,17 +466,21 @@
function addField() {
inner.fields.push(createEmptyFormField());
+ ensureForFields(inner.fields);
emitOut();
}
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;
@@ -357,10 +494,23 @@
}
function onTypeChange(field) {
- if (field.type === "select" && (!field.options || !field.options.length)) {
- field.options = [{ label: "", value: "" }];
+ if (field.type === "select") {
+ if (!field.optionSource) field.optionSource = SELECT_OPTION_SOURCE.STATIC;
+ if (!field.options || !field.options.length) {
+ field.options = [{ label: "", value: "" }];
+ }
+ ensureForFields(inner.fields);
}
resetDefaultValueForType(field);
+ emitOut();
+}
+
+function onOptionSourceChange(field) {
+ field.defaultValue = "";
+ if (!isDynamicOptionSource(field.optionSource) && (!field.options || !field.options.length)) {
+ field.options = [{ label: "", value: "" }];
+ }
+ ensureForFields(inner.fields);
emitOut();
}
@@ -375,10 +525,67 @@
emitOut();
}
-function applyPreset(key) {
- const data = applyFormConfigPreset(key);
- syncFromProps(data);
- emitOut();
+async function loadTemplateImportOptions() {
+ templateImportLoading.value = true;
+ try {
+ const [customRes, builtinRes] = await Promise.all([
+ listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
+ listApprovalTemplate(TEMPLATE_TYPE_BUILTIN),
+ ]);
+ const excludeId =
+ props.excludeTemplateId != null && props.excludeTemplateId !== ""
+ ? String(props.excludeTemplateId)
+ : "";
+ templateImportOptions.value = [...unwrapTemplateList(customRes), ...unwrapTemplateList(builtinRes)]
+ .filter((row) => row?.id != null && String(row.id) !== excludeId)
+ .map((row) => ({
+ id: row.id,
+ label: row.templateName || `妯℃澘 #${row.id}`,
+ enabled: mapEnabledFromApi(row.enabled),
+ }));
+ } catch {
+ templateImportOptions.value = [];
+ ElMessage.error("鍔犺浇瀹℃壒妯℃澘鍒楄〃澶辫触");
+ } finally {
+ templateImportLoading.value = false;
+ }
+}
+
+function onImportDropdownVisible(visible) {
+ if (props.disableImport) return;
+ if (visible) loadTemplateImportOptions();
+}
+
+async function importFromTemplate(templateId) {
+ if (!templateId) return;
+ if (inner.fields.length) {
+ try {
+ await ElMessageBox.confirm("灏嗚鐩栧綋鍓嶅~鎶ラ」閰嶇疆锛屾槸鍚︾户缁紵", "浠庢ā鏉垮鍏�", {
+ type: "warning",
+ confirmButtonText: "缁х画瀵煎叆",
+ cancelButtonText: "鍙栨秷",
+ });
+ } catch {
+ return;
+ }
+ }
+ templateImportLoading.value = true;
+ try {
+ const res = await getApprovalTemplateDetail(templateId);
+ const row = unwrapTemplateDetail(res);
+ const data = parseFormConfigToData(row?.formConfig);
+ if (!data.fields?.length) {
+ ElMessage.warning("璇ユā鏉挎湭閰嶇疆濉姤椤�");
+ return;
+ }
+ syncFromProps(data);
+ emitOut();
+ ElMessage.success(`宸插鍏ャ��${row.templateName || "妯℃澘"}銆嶇殑濉姤椤筦);
+ } catch {
+ ElMessage.error("鍔犺浇妯℃澘璇︽儏澶辫触");
+ } finally {
+ templateImportLoading.value = false;
+ }
}
</script>
@@ -436,6 +643,10 @@
align-items: center;
gap: 8px;
}
+.import-tag {
+ margin-left: 8px;
+ vertical-align: middle;
+}
.fce-empty {
padding: 24px 0;
@@ -464,6 +675,10 @@
.fce-card--required {
border-left: 3px solid var(--el-color-danger-light-3);
+}
+
+.fce-card--locked {
+ background: var(--el-fill-color-light);
}
.fce-card-badge {
@@ -585,10 +800,28 @@
margin-bottom: 10px;
}
-.fce-options-head .fce-section-title {
+.fce-options-head .fce-section-title,
+.fce-options-head .fce-section-subtitle {
margin-bottom: 0;
}
+.fce-section-subtitle {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--el-text-color-secondary);
+}
+
+.fce-source-row {
+ margin-bottom: 4px;
+}
+
+.fce-source-tip {
+ margin: 0 0 10px;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ line-height: 1.5;
+}
+
.fce-option-row {
display: flex;
align-items: center;
--
Gitblit v1.9.3