From 6c324a234060820d031014ea657af5aa0b0d478e Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期二, 19 五月 2026 13:29:20 +0800
Subject: [PATCH] 审批模板
---
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue | 624 ++++++++++++++++++++++
src/api/officeProcessAutomation/approvalTemplate.js | 63 ++
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js | 16
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js | 310 ++++++++---
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js | 13
src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js | 278 +++++++++
src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue | 16
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js | 263 +++++---
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue | 89 ++
9 files changed, 1,464 insertions(+), 208 deletions(-)
diff --git a/src/api/officeProcessAutomation/approvalTemplate.js b/src/api/officeProcessAutomation/approvalTemplate.js
new file mode 100644
index 0000000..28c4234
--- /dev/null
+++ b/src/api/officeProcessAutomation/approvalTemplate.js
@@ -0,0 +1,63 @@
+import request from "@/utils/request";
+
+/** 妯℃澘绫诲瀷锛�1 绯荤粺鍐呯疆锛�2 鑷畾涔� */
+export const TEMPLATE_TYPE_BUILTIN = 1;
+export const TEMPLATE_TYPE_CUSTOM = 2;
+
+export const TEMPLATE_TYPE_OPTIONS = [
+ { value: TEMPLATE_TYPE_BUILTIN, label: "绯荤粺鍐呯疆" },
+ { value: TEMPLATE_TYPE_CUSTOM, label: "鑷畾涔�" },
+];
+
+/** 鏌ヨ鎵�鏈夊鎵规ā鏉� */
+export function listApprovalTemplate(type) {
+ return request({
+ url: `/approvalTemplate/list/${type}`,
+ method: "get",
+ });
+}
+
+/** 鍒嗛〉鏌ヨ瀹℃壒妯℃澘 */
+export function listApprovalTemplatePage(params) {
+ return request({
+ url: "/approvalTemplate/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏌ヨ瀹℃壒妯℃澘璇︽儏 */
+export function getApprovalTemplateDetail(id) {
+ return request({
+ url: `/approvalTemplate/detail/${id}`,
+ method: "get",
+ });
+}
+
+/** 鏂板瀹℃壒妯℃澘锛坆ody 涓� ApprovalTemplateDto锛� */
+export function addApprovalTemplate(approvalTemplateDto) {
+ return request({
+ url: "/approvalTemplate/add",
+ method: "post",
+ data: approvalTemplateDto,
+ });
+}
+
+/** 淇敼瀹℃壒妯℃澘锛坆ody 涓� ApprovalTemplateDto锛� */
+export function updateApprovalTemplate(approvalTemplateDto) {
+ return request({
+ url: "/approvalTemplate/update",
+ method: "put",
+ data: approvalTemplateDto,
+ });
+}
+
+/** 鍒犻櫎瀹℃壒妯℃澘锛坆ody 涓烘ā鏉� ID 鏁扮粍锛� */
+export function deleteApprovalTemplate(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
+ return request({
+ url: "/approvalTemplate/delete",
+ method: "post",
+ data: idList,
+ });
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index 447627a..e4bb66f 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -1,4 +1,5 @@
import dayjs from "dayjs";
+import { buildFormPayloadFromFields } from "../approve-template/formConfigUtils.js";
/** 瀹℃壒绫诲瀷锛堜笌鍚庣瀛楁 approvalType 瀵归綈锛屽悗鏈熷彲鍚屾锛� */
export const APPROVAL_TYPE_OPTIONS = [
@@ -299,16 +300,12 @@
}
}
-export function createEmptySubmitForm(templateKey) {
- const tpl = SUBMIT_TEMPLATES[templateKey];
- const payload = { summary: "" };
- (tpl?.fields || []).forEach((f) => {
- if (f.type === "number") payload[f.key] = undefined;
- else if (f.type === "datetimerange") payload[f.key] = [];
- else payload[f.key] = "";
- });
+export function createEmptySubmitForm(templateKey, templateOverride) {
+ const tpl = templateOverride || SUBMIT_TEMPLATES[templateKey];
+ const payload = tpl?.fields?.length ? buildFormPayloadFromFields(tpl.fields) : { summary: "" };
return {
templateKey: templateKey || "",
+ templateSnapshot: null,
approvalMode: tpl?.approvalMode || "parallel",
formPayload: payload,
approvalFlowNodes: buildDefaultFlowNodes(),
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index 48103aa..c3c3241 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -15,6 +15,7 @@
saveStoredRows,
buildDefaultFlowNodes,
} from "./approveListConstants.js";
+import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
function advanceFlow(row, result, opinion) {
const nodes = row.approvalFlowNodes || [];
@@ -110,7 +111,9 @@
return filteredList.value.slice(start, start + page.size);
});
- const activeTemplate = computed(() => SUBMIT_TEMPLATES[submitForm.templateKey] || null);
+ const activeTemplate = computed(
+ () => submitForm.templateSnapshot || SUBMIT_TEMPLATES[submitForm.templateKey] || null
+ );
const submitFormRules = computed(() => {
const rules = {
@@ -232,8 +235,15 @@
submitDialog.visible = true;
}
- function onTemplatePick(key) {
- Object.assign(submitForm, createEmptySubmitForm(key));
+ /** @param {string} key 鍐呯疆妯℃澘 key 鎴栬嚜瀹氫箟 id */
+ function onTemplatePick(key, templateRow) {
+ const base = templateRow
+ ? createEmptySubmitForm(key, buildSubmitTemplateFromRow(templateRow))
+ : createEmptySubmitForm(key);
+ Object.assign(submitForm, {
+ ...base,
+ templateSnapshot: templateRow ? buildSubmitTemplateFromRow(templateRow) : null,
+ });
submitDialog.step = 2;
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
index 81884a1..1ffdbf5 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -1,5 +1,21 @@
import dayjs from "dayjs";
-import { APPROVAL_TYPE_OPTIONS, SUBMIT_TEMPLATES } from "../approve-list/approveListConstants.js";
+import {
+ TEMPLATE_TYPE_CUSTOM,
+ TEMPLATE_TYPE_OPTIONS,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js";
+import {
+ buildFormConfigJson,
+ createEmptyFormConfigData,
+ parseFormConfigToData,
+ validateFormConfigData,
+} from "./formConfigUtils.js";
+
+export { TEMPLATE_TYPE_OPTIONS };
+
+export function templateTypeLabel(type) {
+ return TEMPLATE_TYPE_OPTIONS.find((x) => x.value === type)?.label || "鈥�";
+}
/** 鑺傜偣鍐呭鎵规柟寮忥細浼氱 / 鎴栫 */
export const NODE_SIGN_MODE_OPTIONS = [
@@ -7,18 +23,206 @@
{ value: "or_sign", label: "鎴栫", desc: "鏈妭鐐逛换涓�瀹℃壒浜洪�氳繃鍗冲彲" },
];
-export const STORAGE_KEY = "oa_approve_template_custom_v1";
+function parseFormConfig(formConfig) {
+ if (!formConfig) return {};
+ if (typeof formConfig === "object") return formConfig;
+ try {
+ return JSON.parse(formConfig);
+ } catch {
+ return {};
+ }
+}
-/** 绯荤粺鍐呯疆甯哥敤瀹℃壒锛堝彧璇诲睍绀猴紝鏉ユ簮浜庡鎵瑰垪琛ㄦ彁浜ゆā鏉匡級 */
-export function getBuiltinTemplates() {
- return Object.entries(SUBMIT_TEMPLATES).map(([key, tpl]) => ({
- key,
- approvalType: tpl.approvalType,
- label: tpl.label,
- summary: tpl.summaryPlaceholder || "绯荤粺棰勭疆濉姤瀛楁",
- fieldCount: (tpl.fields || []).length,
- defaultMode: tpl.approvalMode,
+function resolveDefaultMode(row, cfg, nodes) {
+ let mode = cfg.approvalMode || cfg.defaultMode;
+ if (!mode && nodes.length) {
+ const t = String(nodes[0]?.approveType || "").toUpperCase();
+ mode = t === "OR" ? "or_sign" : "parallel";
+ }
+ const m = String(mode || "").toLowerCase();
+ if (m === "or" || m === "or_sign") return "or_sign";
+ return "parallel";
+}
+
+/** 灏嗘帴鍙h繑鍥炵殑妯℃澘杞负銆岀郴缁熷父鐢ㄥ鎵广�嶅崱鐗囨暟鎹� */
+export function mapBuiltinCardFromApi(row) {
+ const cfg = parseFormConfig(row?.formConfig);
+ const fields = cfg.fields || cfg.formFields || [];
+ const nodes = row?.nodes || row?.flowNodes || [];
+ return {
+ key: String(row?.id ?? row?.templateName ?? ""),
+ id: row?.id,
+ approvalType: cfg.approvalType || row?.approvalType || "",
+ label: row?.templateName || row?.name || "鈥�",
+ summary: (row?.description || "").trim() || cfg.summaryPlaceholder || "绯荤粺棰勭疆濉姤瀛楁",
+ fieldCount: fields.length,
+ defaultMode: resolveDefaultMode(row, cfg, nodes),
+ };
+}
+
+export function unwrapTemplateList(payload) {
+ const data = payload?.data ?? payload;
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.records)) return data.records;
+ if (Array.isArray(data?.list)) return data.list;
+ return [];
+}
+
+/** 鍚庣 approveType 鈫� 椤甸潰 signMode */
+export function mapSignModeFromApi(approveType) {
+ const t = String(approveType || "").toUpperCase();
+ return t === "OR" ? "or_sign" : "countersign";
+}
+
+/** 椤甸潰 signMode 鈫� 鍚庣 approveType */
+export function mapSignModeToApi(signMode) {
+ return signMode === "or_sign" ? "OR" : "AND";
+}
+
+/** 椤甸潰 enabled 鈫� 鍚庣 enabled锛�1 鍚敤锛�0 鍋滅敤锛� */
+export function mapEnabledToApi(enabled) {
+ return enabled !== false ? "1" : "0";
+}
+
+/** 鍚庣 nodes 鈫� 椤甸潰 flowNodes锛堜繚鐣� id 渚涗慨鏀规彁浜わ級 */
+export function mapNodesFromApi(nodes) {
+ const list = Array.isArray(nodes) ? nodes : [];
+ return list.map((n, i) => ({
+ id: n.id,
+ templateId: n.templateId,
+ nodeOrder: n.levelNo ?? i + 1,
+ signMode: mapSignModeFromApi(n.approveType ?? n.signMode),
+ approvers: (n.approvers || [])
+ .filter((a) => a?.approverId != null && a.approverId !== "")
+ .map((a) => ({
+ id: a.id,
+ nodeId: a.nodeId,
+ templateId: a.templateId,
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ })),
}));
+}
+
+/** enabled锛�1 鍚敤锛�0 鍋滅敤 */
+export function mapEnabledFromApi(enabled) {
+ return enabled === "1" || enabled === 1 || enabled === true;
+}
+
+/** 鍏煎澶氱鍚庣鏃堕棿瀛楁鍚嶅苟鏍煎紡鍖栧睍绀� */
+export function pickTemplateTimes(row) {
+ const rawCreated =
+ row?.createdTime ?? row?.createTime ?? row?.gmtCreate ?? row?.created_at ?? "";
+ const rawUpdated =
+ row?.updatedTime ?? row?.updateTime ?? row?.gmtModified ?? row?.modifyTime ?? row?.updated_at ?? "";
+ const createdTime = normalizeTimeValue(rawCreated);
+ const updatedTime = normalizeTimeValue(rawUpdated);
+ return { createdTime, updatedTime, createTime: createdTime, updateTime: updatedTime };
+}
+
+function normalizeTimeValue(val) {
+ if (val == null || val === "") return "";
+ if (Array.isArray(val) && val.length >= 3) {
+ const [y, m, d, h = 0, min = 0, s = 0] = val;
+ return dayjs(new Date(y, m - 1, d, h, min, s)).format("YYYY-MM-DD HH:mm:ss");
+ }
+ if (typeof val === "number") {
+ const d = val > 1e12 ? dayjs(val) : dayjs.unix(val);
+ return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : "";
+ }
+ const s = String(val).trim();
+ if (!s) return "";
+ const parsed = dayjs(s.includes("T") ? s : s.replace(/-/g, "/"));
+ return parsed.isValid() ? parsed.format("YYYY-MM-DD HH:mm:ss") : s;
+}
+
+export function formatDisplayTime(val) {
+ const t = normalizeTimeValue(val);
+ return t || "鈥�";
+}
+
+/** 璇︽儏鎺ュ彛 data 瑙e寘 */
+export function unwrapTemplateDetail(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") return {};
+ if (data.templateName != null || data.id != null) return data;
+ if (data.approvalTemplateVo) return data.approvalTemplateVo;
+ if (data.records && data.records[0]) return data.records[0];
+ return data;
+}
+
+/** 鍒嗛〉鍒楄〃椤� 鈫� 椤甸潰琛屾暟鎹紙涓昏〃 + 鑺傜偣锛� */
+export function mapTemplateFromApi(row) {
+ if (!row) return {};
+ const flowNodes = mapNodesFromApi(row.nodes || row.flowNodes);
+ const times = pickTemplateTimes(row);
+ return {
+ id: row.id,
+ templateName: row.templateName || "",
+ description: row.description || "",
+ enabled: mapEnabledFromApi(row.enabled),
+ enabledRaw: row.enabled,
+ templateType: row.templateType,
+ formConfig: row.formConfig,
+ formConfigData: parseFormConfigToData(row.formConfig),
+ createdUser: row.createdUser,
+ createdUserName: row.createdUserName,
+ ...times,
+ flowNodes,
+ nodes: row.nodes || row.flowNodes,
+ };
+}
+
+/** 琛ㄥ崟鏁版嵁 鈫� 鎻愪氦 DTO锛圓pprovalTemplateDto锛� */
+export function mapTemplateToApi(form) {
+ const nodes = normalizeFlowNodes(form.flowNodes);
+ const templateId = form.id || null;
+ const dto = {
+ templateName: (form.templateName || "").trim(),
+ description: (form.description || "").trim(),
+ enabled: mapEnabledToApi(form.enabled),
+ templateType: form.templateType ?? TEMPLATE_TYPE_CUSTOM,
+ formConfig: buildFormConfigJson(form.formConfigData),
+ nodes: nodes.map((n, i) => {
+ const node = {
+ levelNo: n.nodeOrder ?? i + 1,
+ approveType: mapSignModeToApi(n.signMode),
+ approvers: n.approvers.map((a, idx) => {
+ const approver = {
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ sortNo: idx + 1,
+ };
+ if (a.id != null) approver.id = a.id;
+ if (a.nodeId != null) approver.nodeId = a.nodeId;
+ if (a.templateId != null) approver.templateId = a.templateId;
+ else if (templateId) approver.templateId = templateId;
+ return approver;
+ }),
+ };
+ if (n.id != null) node.id = n.id;
+ if (n.templateId != null) node.templateId = n.templateId;
+ else if (templateId) node.templateId = templateId;
+ return node;
+ }),
+ };
+ if (templateId) dto.id = templateId;
+ return dto;
+}
+
+/** 鏋勫缓鍒嗛〉鏌ヨ鍙傛暟 */
+export function buildApprovalTemplateListParams({ page, searchForm }) {
+ const params = {
+ current: page.current,
+ size: page.size,
+ };
+ if (searchForm?.templateType != null && searchForm.templateType !== "") {
+ params.templateType = searchForm.templateType;
+ }
+ const kw = (searchForm?.keyword || "").trim();
+ if (kw) params.templateName = kw;
+ if (searchForm?.enabledOnly) params.enabled = "1";
+ return params;
}
export function nodeSignModeLabel(mode) {
@@ -42,6 +246,9 @@
id: "",
templateName: "",
description: "",
+ templateType: TEMPLATE_TYPE_CUSTOM,
+ formConfig: "",
+ formConfigData: createEmptyFormConfigData(),
enabled: true,
flowNodes: [createEmptyNode(1)],
};
@@ -50,11 +257,16 @@
export function normalizeFlowNodes(nodes) {
const list = Array.isArray(nodes) ? nodes : [];
return list.map((n, i) => ({
+ id: n.id,
+ templateId: n.templateId,
nodeOrder: i + 1,
signMode: n.signMode === "or_sign" ? "or_sign" : "countersign",
approvers: (n.approvers || [])
.filter((a) => a?.approverId != null && a.approverId !== "")
.map((a) => ({
+ id: a.id,
+ nodeId: a.nodeId,
+ templateId: a.templateId,
approverId: a.approverId,
approverName: a.approverName || "",
})),
@@ -71,6 +283,8 @@
return { ok: false, message: `璇蜂负绗� ${i + 1} 涓妭鐐归�夋嫨鑷冲皯涓�鍚嶅鎵逛汉` };
}
}
+ const cfgCheck = validateFormConfigData(form.formConfigData);
+ if (!cfgCheck.ok) return cfgCheck;
return { ok: true, nodes, name };
}
@@ -83,78 +297,4 @@
return `鑺傜偣${i + 1}(${nodeSignModeLabel(n.signMode)}:${names})`;
})
.join(" 鈫� ");
-}
-
-export function createInitialMockTemplates() {
- const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
- return [
- {
- id: "tpl_demo_1",
- templateName: "椤圭洰绔嬮」瀹℃壒",
- description: "璺ㄩ儴闂ㄩ」鐩珛椤癸紝闇�鎶�鏈�佽储鍔′緷娆′細绛�",
- enabled: true,
- createTime: dayjs().subtract(5, "day").format("YYYY-MM-DD HH:mm:ss"),
- updateTime: now,
- flowNodes: [
- {
- nodeOrder: 1,
- signMode: "countersign",
- approvers: [
- { approverId: "mock_tech_lead", approverName: "鎶�鏈礋璐d汉" },
- { approverId: "mock_pm", approverName: "椤圭洰缁忕悊" },
- ],
- },
- {
- nodeOrder: 2,
- signMode: "or_sign",
- approvers: [
- { approverId: "mock_finance", approverName: "璐㈠姟涓荤" },
- { approverId: "mock_cfo", approverName: "璐㈠姟鎬荤洃" },
- ],
- },
- ],
- },
- {
- id: "tpl_demo_2",
- templateName: "鍚堝悓鐢ㄥ嵃鐢宠",
- description: "娉曞姟涓庤鏀挎垨绛惧悗锛屾�荤粡鐞嗙粓瀹�",
- enabled: true,
- createTime: dayjs().subtract(12, "day").format("YYYY-MM-DD HH:mm:ss"),
- updateTime: dayjs().subtract(2, "day").format("YYYY-MM-DD HH:mm:ss"),
- flowNodes: [
- {
- nodeOrder: 1,
- signMode: "or_sign",
- approvers: [
- { approverId: "mock_legal", approverName: "娉曞姟涓撳憳" },
- { approverId: "mock_admin", approverName: "琛屾斂涓荤" },
- ],
- },
- {
- nodeOrder: 2,
- signMode: "countersign",
- approvers: [{ approverId: "mock_ceo", approverName: "鎬荤粡鐞�" }],
- },
- ],
- },
- ];
-}
-
-export function loadStoredTemplates() {
- try {
- const raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) return null;
- const parsed = JSON.parse(raw);
- return Array.isArray(parsed) ? parsed : null;
- } catch {
- return null;
- }
-}
-
-export function saveStoredTemplates(rows) {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(rows));
- } catch {
- /* ignore */
- }
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
new file mode 100644
index 0000000..1881f60
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -0,0 +1,624 @@
+<!-- 瀹℃壒妯℃澘锛氬彲閰嶇疆濉姤椤癸紝搴忓垪鍖栧埌 formConfig -->
+<template>
+ <div class="fce">
+ <div class="fce-hint">
+ <span class="fce-hint-label">濉姤鎻愮ず</span>
+ <el-input
+ v-model="inner.summaryPlaceholder"
+ placeholder="濡傦細璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑"
+ maxlength="200"
+ show-word-limit
+ @input="emitOut"
+ />
+ </div>
+
+ <div class="fce-panel">
+ <div class="fce-toolbar">
+ <div class="fce-toolbar-left">
+ <span class="fce-title">濉姤椤归厤缃�</span>
+ <el-tag v-if="inner.fields.length" size="small" type="info" effect="plain">
+ 鍏� {{ inner.fields.length }} 椤�
+ </el-tag>
+ </div>
+ <div class="fce-toolbar-actions">
+ <el-dropdown trigger="click" @command="applyPreset">
+ <el-button size="small">浠庨璁惧鍏�</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>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ <el-button type="primary" size="small" :icon="Plus" @click="addField">娣诲姞濉姤椤�</el-button>
+ </div>
+ </div>
+
+ <el-empty
+ v-if="!inner.fields.length"
+ class="fce-empty"
+ description="鏆傛棤濉姤椤癸紝鍙坊鍔犳垨浠庨璁惧揩閫熷鍏�"
+ :image-size="72"
+ />
+
+ <div v-else class="fce-list">
+ <div
+ v-for="(field, index) in inner.fields"
+ :key="field._uid"
+ class="fce-card"
+ :class="{ 'fce-card--required': field.required }"
+ >
+ <div class="fce-card-badge">{{ index + 1 }}</div>
+
+ <div class="fce-card-head">
+ <div class="fce-card-title">
+ <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>
+ </div>
+ <div 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>
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="涓嬬Щ" placement="top">
+ <el-button
+ circle
+ size="small"
+ :disabled="index >= inner.fields.length - 1"
+ @click="moveField(index, 1)"
+ >
+ <el-icon><Bottom /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top">
+ <el-button circle size="small" type="danger" plain @click="removeField(index)">
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </el-tooltip>
+ </div>
+ </div>
+
+ <div class="fce-section">
+ <span class="fce-section-title">鍩虹淇℃伅</span>
+ <el-row :gutter="16">
+ <el-col :span="8">
+ <el-form-item label="鏄剧ず鍚嶇О" required class="fce-field-item">
+ <el-input
+ v-model="field.label"
+ placeholder="濡傦細鎶ラ攢璇存槑"
+ maxlength="50"
+ @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-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-option
+ v-for="t in FORM_FIELD_TYPE_OPTIONS"
+ :key="t.value"
+ :label="t.label"
+ :value="t.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <div class="fce-section">
+ <span class="fce-section-title">鏍¢獙涓庢牸寮�</span>
+ <el-row :gutter="16" align="middle">
+ <el-col :span="8">
+ <el-form-item label="鏄惁蹇呭~" class="fce-field-item fce-field-item--switch">
+ <el-switch
+ v-model="field.required"
+ inline-prompt
+ active-text="蹇呭~"
+ inactive-text="閫夊~"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col v-if="field.type === 'textarea'" :span="8">
+ <el-form-item label="琛屾暟" class="fce-field-item">
+ <el-input-number
+ v-model="field.rows"
+ :min="1"
+ :max="10"
+ controls-position="right"
+ style="width: 100%"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <template v-if="field.type === 'number'">
+ <el-col :span="8">
+ <el-form-item label="鏈�灏忓��" class="fce-field-item">
+ <el-input-number
+ v-model="field.min"
+ controls-position="right"
+ style="width: 100%"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="灏忔暟浣�" class="fce-field-item">
+ <el-input-number
+ v-model="field.precision"
+ :min="0"
+ :max="4"
+ controls-position="right"
+ style="width: 100%"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ </template>
+ </el-row>
+ </div>
+
+ <div class="fce-section fce-section--default">
+ <span class="fce-section-title">榛樿鍊�</span>
+ <p class="fce-section-desc">閫夋嫨璇ユā鏉挎彁浜ゅ鎵规椂锛屽皢鑷姩棰勫~浠ヤ笅鍐呭锛堢敤鎴蜂粛鍙慨鏀癸級</p>
+ <el-input
+ v-if="field.type === 'text' || field.type === 'textarea'"
+ v-model="field.defaultValue"
+ :type="field.type === 'textarea' ? 'textarea' : 'text'"
+ :rows="field.type === 'textarea' ? 2 : undefined"
+ :placeholder="defaultPlaceholder(field)"
+ clearable
+ @input="emitOut"
+ />
+ <el-input-number
+ v-else-if="field.type === 'number'"
+ v-model="field.defaultValue"
+ :min="field.min"
+ :precision="field.precision ?? 0"
+ controls-position="right"
+ placeholder="閫夊~"
+ style="width: 100%"
+ @change="emitOut"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'date'"
+ v-model="field.defaultValue"
+ type="date"
+ placeholder="閫夊~"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ clearable
+ @change="emitOut"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'datetimerange'"
+ v-model="field.defaultValue"
+ type="datetimerange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫椂闂�"
+ end-placeholder="缁撴潫鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ clearable
+ @change="emitOut"
+ />
+ <el-select
+ v-else-if="field.type === 'select'"
+ v-model="field.defaultValue"
+ placeholder="閫夊~"
+ style="width: 100%"
+ clearable
+ @change="emitOut"
+ >
+ <el-option
+ v-for="o in field.options.filter((x) => x.value !== '' && x.value != null)"
+ :key="String(o.value)"
+ :label="o.label || o.value"
+ :value="o.value"
+ />
+ </el-select>
+ </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>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue";
+import { reactive, watch } from "vue";
+import {
+ FORM_CONFIG_PRESETS,
+ FORM_FIELD_TYPE_OPTIONS,
+ applyFormConfigPreset,
+ createEmptyFormConfigData,
+ createEmptyFormField,
+ formFieldTypeLabel,
+} from "../formConfigUtils.js";
+
+const props = defineProps({
+ modelValue: { type: Object, default: () => createEmptyFormConfigData() },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const inner = reactive(createEmptyFormConfigData());
+
+function typeLabel(type) {
+ return formFieldTypeLabel(type);
+}
+
+function defaultPlaceholder(field) {
+ const name = field.label || "璇ュ瓧娈�";
+ return `閫夊~锛岄�夋嫨妯℃澘鏃跺皢棰勫~${name}`;
+}
+
+function syncFromProps(v) {
+ const src = v || createEmptyFormConfigData();
+ inner.summaryPlaceholder = src.summaryPlaceholder || "";
+ inner.fields = (src.fields || []).map((f) => ({
+ ...createEmptyFormField(),
+ ...f,
+ _uid: f._uid || createEmptyFormField()._uid,
+ options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })),
+ }));
+}
+
+function emitOut() {
+ emit("update:modelValue", {
+ summaryPlaceholder: inner.summaryPlaceholder,
+ fields: inner.fields.map((f) => ({
+ _uid: f._uid,
+ key: f.key,
+ label: f.label,
+ type: f.type,
+ required: f.required,
+ rows: f.rows,
+ min: f.min,
+ precision: f.precision,
+ defaultValue: cloneDefaultValue(f),
+ options: (f.options || []).map((o) => ({ label: o.label, value: o.value })),
+ })),
+ });
+}
+
+function cloneDefaultValue(f) {
+ if (f.type === "datetimerange" && Array.isArray(f.defaultValue)) {
+ return [...f.defaultValue];
+ }
+ return f.defaultValue;
+}
+
+watch(
+ () => props.modelValue,
+ (v) => syncFromProps(v),
+ { deep: true, immediate: true }
+);
+
+function addField() {
+ inner.fields.push(createEmptyFormField());
+ emitOut();
+}
+
+function removeField(index) {
+ inner.fields.splice(index, 1);
+ emitOut();
+}
+
+function moveField(index, delta) {
+ const next = index + delta;
+ if (next < 0 || next >= inner.fields.length) return;
+ const t = inner.fields[index];
+ inner.fields[index] = inner.fields[next];
+ inner.fields[next] = t;
+ emitOut();
+}
+
+function resetDefaultValueForType(field) {
+ if (field.type === "number") field.defaultValue = undefined;
+ else if (field.type === "datetimerange") field.defaultValue = [];
+ else field.defaultValue = "";
+}
+
+function onTypeChange(field) {
+ if (field.type === "select" && (!field.options || !field.options.length)) {
+ field.options = [{ label: "", value: "" }];
+ }
+ resetDefaultValueForType(field);
+ emitOut();
+}
+
+function addOption(field) {
+ field.options.push({ label: "", value: "" });
+ emitOut();
+}
+
+function removeOption(field, oi) {
+ if (field.options.length <= 1) return;
+ field.options.splice(oi, 1);
+ emitOut();
+}
+
+function applyPreset(key) {
+ const data = applyFormConfigPreset(key);
+ syncFromProps(data);
+ emitOut();
+}
+</script>
+
+<style scoped>
+.fce {
+ width: 100%;
+}
+
+.fce-hint {
+ padding: 14px 16px;
+ margin-bottom: 14px;
+ border-radius: 10px;
+ background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-blank) 100%);
+ border: 1px solid var(--el-color-primary-light-7);
+}
+
+.fce-hint-label {
+ display: block;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin-bottom: 8px;
+}
+
+.fce-panel {
+ padding: 16px;
+ border-radius: 12px;
+ background: var(--el-fill-color-lighter);
+ border: 1px solid var(--el-border-color-lighter);
+}
+
+.fce-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.fce-toolbar-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.fce-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+
+.fce-toolbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.fce-empty {
+ padding: 24px 0;
+}
+
+.fce-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.fce-card {
+ position: relative;
+ padding: 16px 16px 12px;
+ border-radius: 12px;
+ background: var(--el-bg-color);
+ border: 1px solid var(--el-border-color-lighter);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.fce-card:hover {
+ border-color: var(--el-color-primary-light-5);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
+}
+
+.fce-card--required {
+ border-left: 3px solid var(--el-color-danger-light-3);
+}
+
+.fce-card-badge {
+ position: absolute;
+ top: -10px;
+ left: 16px;
+ min-width: 22px;
+ height: 22px;
+ padding: 0 6px;
+ border-radius: 11px;
+ background: var(--el-color-primary);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.35);
+}
+
+.fce-card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+ padding-top: 4px;
+}
+
+.fce-card-title {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.fce-card-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+
+.fce-card-btns {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.fce-section {
+ margin-bottom: 12px;
+ padding-bottom: 12px;
+ border-bottom: 1px dashed var(--el-border-color-extra-light);
+}
+
+.fce-section:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.fce-section-title {
+ display: block;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--el-text-color-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 10px;
+}
+
+.fce-section-desc {
+ margin: -6px 0 10px;
+ font-size: 12px;
+ color: var(--el-text-color-placeholder);
+ line-height: 1.5;
+}
+
+.fce-section--default {
+ padding: 12px 14px;
+ border-radius: 8px;
+ background: var(--el-fill-color-lighter);
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.fce-section--default .fce-section-title {
+ margin-bottom: 4px;
+ color: var(--el-color-primary);
+ text-transform: none;
+ letter-spacing: 0;
+ font-size: 13px;
+}
+
+.fce-section--options {
+ padding-top: 4px;
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.fce-field-item {
+ margin-bottom: 0;
+}
+
+.fce-field-item :deep(.el-form-item__label) {
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+}
+
+.fce-field-item--switch :deep(.el-form-item__content) {
+ line-height: 32px;
+}
+
+.fce-options-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.fce-options-head .fce-section-title {
+ margin-bottom: 0;
+}
+
+.fce-option-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ background: var(--el-fill-color-lighter);
+}
+
+.fce-option-row:last-child {
+ margin-bottom: 0;
+}
+
+.fce-option-index {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--el-color-info-light-8);
+ color: var(--el-text-color-secondary);
+ font-size: 11px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.fce-option-value {
+ width: 140px;
+ flex-shrink: 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
index 45b32c0..f3f9540 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
@@ -127,6 +127,8 @@
const normalized = normalizeFlowNodes(rows);
return normalized.map((n) => ({
_uid: newUid(),
+ id: n.id,
+ templateId: n.templateId,
nodeOrder: n.nodeOrder,
signMode: n.signMode,
approverIds: n.approvers.map((a) => a.approverId),
@@ -137,6 +139,8 @@
function publicShape(rows) {
return normalizeFlowNodes(
(rows || []).map((r) => ({
+ id: r.id,
+ templateId: r.templateId,
nodeOrder: r.nodeOrder,
signMode: r.signMode,
approvers: r.approvers || [],
@@ -165,13 +169,21 @@
function onApproversChange(ids, row) {
const idList = Array.isArray(ids) ? ids : [];
+ const prevById = new Map((row.approvers || []).map((a) => [String(a.approverId), a]));
row.approverIds = idList;
row.approvers = idList.map((id) => {
+ const prev = prevById.get(String(id));
const u = findUser(id);
- return {
+ const item = {
approverId: id,
- approverName: u ? u.nickName || u.userName || "" : "",
+ approverName: u ? u.nickName || u.userName || "" : prev?.approverName || "",
};
+ if (prev?.id != null) item.id = prev.id;
+ if (prev?.nodeId != null) item.nodeId = prev.nodeId;
+ else if (row.id != null) item.nodeId = row.id;
+ if (prev?.templateId != null) item.templateId = prev.templateId;
+ else if (row.templateId != null) item.templateId = row.templateId;
+ return item;
});
emitOut();
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
new file mode 100644
index 0000000..0cb20ea
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
@@ -0,0 +1,278 @@
+/** 濉姤椤圭被鍨嬶紙涓庡鎵规彁浜ら〉 field.type 涓�鑷达級 */
+export const FORM_FIELD_TYPE_OPTIONS = [
+ { value: "text", label: "鍗曡鏂囨湰" },
+ { value: "textarea", label: "澶氳鏂囨湰" },
+ { value: "number", label: "鏁板瓧" },
+ { value: "date", label: "鏃ユ湡" },
+ { value: "datetimerange", label: "鏃ユ湡鏃堕棿鑼冨洿" },
+ { value: "select", label: "涓嬫媺閫夋嫨" },
+];
+
+/** 甯哥敤棰勮锛堝璐圭敤鎶ラ攢锛� */
+export const FORM_CONFIG_PRESETS = [
+ {
+ key: "cost_reimburse",
+ label: "璐圭敤鎶ラ攢",
+ summaryPlaceholder: "璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑",
+ fields: [
+ { key: "summary", label: "鎶ラ攢璇存槑", type: "textarea", required: true, rows: 3 },
+ { key: "amount", label: "鎶ラ攢閲戦(鍏�)", type: "number", required: true, min: 0, precision: 2 },
+ ],
+ },
+ {
+ key: "travel_reimburse",
+ label: "宸梾鎶ラ攢",
+ summaryPlaceholder: "鍑哄樊琛岀▼涓庤垂鐢ㄨ鏄�",
+ fields: [
+ { key: "summary", label: "宸梾璇存槑", type: "textarea", required: true, rows: 3 },
+ { key: "amount", label: "鎶ラ攢閲戦(鍏�)", type: "number", required: true, min: 0, precision: 2 },
+ { key: "tripDays", label: "鍑哄樊澶╂暟", type: "number", required: false, min: 0, precision: 0 },
+ ],
+ },
+ {
+ key: "leave",
+ label: "璇峰亣鐢宠",
+ summaryPlaceholder: "璇峰~鍐欒鍋囩被鍨嬩笌鏃堕棿",
+ fields: [
+ {
+ key: "leaveType",
+ label: "璇峰亣绫诲瀷",
+ type: "select",
+ required: true,
+ options: [
+ { label: "骞村亣", value: "annual" },
+ { label: "鐥呭亣", value: "sick" },
+ { label: "浜嬪亣", value: "personal" },
+ { label: "璋冧紤", value: "compensatory" },
+ ],
+ },
+ { key: "summary", label: "璇峰亣浜嬬敱", type: "textarea", required: true, rows: 2 },
+ { key: "dateRange", label: "璇峰亣鏃堕棿", type: "datetimerange", required: true },
+ ],
+ },
+];
+
+function newFieldUid() {
+ return `f_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+}
+
+export function createEmptyFormField() {
+ return {
+ _uid: newFieldUid(),
+ key: "",
+ label: "",
+ type: "text",
+ required: true,
+ rows: 3,
+ min: 0,
+ precision: 0,
+ defaultValue: "",
+ options: [{ label: "", value: "" }],
+ };
+}
+
+/** 瑙f瀽鍗曢」榛樿鍊硷紙渚涙彁浜ら〉 formPayload 鍒濆鍖栵級 */
+export function resolveFieldDefaultValue(field) {
+ const type = field?.type || "text";
+ const dv = field?.defaultValue;
+ if (dv === undefined || dv === null || dv === "") {
+ if (type === "number") return undefined;
+ if (type === "datetimerange") return [];
+ return "";
+ }
+ if (type === "number") {
+ const n = Number(dv);
+ return Number.isNaN(n) ? undefined : n;
+ }
+ if (type === "datetimerange") {
+ return Array.isArray(dv) ? [...dv] : [];
+ }
+ return dv;
+}
+
+function hasDefaultValue(field) {
+ const type = field?.type || "text";
+ const dv = field?.defaultValue;
+ if (dv === undefined || dv === null) return false;
+ if (type === "number") return dv !== "" && !Number.isNaN(Number(dv));
+ if (type === "datetimerange") return Array.isArray(dv) && dv.length === 2;
+ if (type === "select") return dv !== "";
+ return String(dv).trim() !== "";
+}
+
+/** 鏍规嵁瀛楁瀹氫箟鐢熸垚 formPayload 鍒濆鍊硷紙鍚粯璁ゅ�硷級 */
+export function buildFormPayloadFromFields(fields) {
+ const payload = {};
+ (fields || []).forEach((f) => {
+ const key = (f.key || "").trim();
+ if (!key) return;
+ payload[key] = resolveFieldDefaultValue(f);
+ });
+ return payload;
+}
+
+export function createEmptyFormConfigData() {
+ return {
+ summaryPlaceholder: "",
+ fields: [],
+ };
+}
+
+function parseFormConfigRaw(formConfig) {
+ if (!formConfig) return {};
+ if (typeof formConfig === "object") return formConfig;
+ try {
+ return JSON.parse(formConfig);
+ } catch {
+ return {};
+ }
+}
+
+function normalizeDefaultValueFromApi(f) {
+ const type = f.type || "text";
+ if (f.defaultValue === undefined || f.defaultValue === null) {
+ if (type === "number") return undefined;
+ if (type === "datetimerange") return [];
+ return "";
+ }
+ if (type === "datetimerange" && Array.isArray(f.defaultValue)) {
+ return [...f.defaultValue];
+ }
+ return f.defaultValue;
+}
+
+/** 鎺ュ彛 formConfig 鈫� 缂栬緫鍣ㄦ暟鎹� */
+export function parseFormConfigToData(formConfig) {
+ const raw = parseFormConfigRaw(formConfig);
+ const fields = (raw.fields || raw.formFields || []).map((f) => ({
+ _uid: newFieldUid(),
+ key: f.key || "",
+ label: f.label || "",
+ type: f.type || "text",
+ required: f.required !== false,
+ rows: f.rows ?? 3,
+ min: f.min ?? 0,
+ precision: f.precision ?? 0,
+ defaultValue: normalizeDefaultValueFromApi(f),
+ options: (f.options || []).length
+ ? f.options.map((o) => ({ label: o.label || "", value: o.value ?? "" }))
+ : [{ label: "", value: "" }],
+ }));
+ return {
+ summaryPlaceholder: raw.summaryPlaceholder || "",
+ fields,
+ };
+}
+
+/** 缂栬緫鍣ㄦ暟鎹� 鈫� 鎻愪氦鐢� JSON 瀛楃涓� */
+export function buildFormConfigJson(formConfigData) {
+ const data = formConfigData || createEmptyFormConfigData();
+ const fields = (data.fields || []).map((f) => {
+ const item = {
+ key: (f.key || "").trim(),
+ label: (f.label || "").trim(),
+ type: f.type || "text",
+ required: f.required !== false,
+ };
+ if (item.type === "textarea") item.rows = Number(f.rows) || 3;
+ if (item.type === "number") {
+ item.min = f.min ?? 0;
+ item.precision = f.precision ?? 0;
+ }
+ if (item.type === "select") {
+ item.options = (f.options || [])
+ .filter((o) => (o.label || "").trim() || o.value !== "" && o.value != null)
+ .map((o) => ({ label: (o.label || "").trim(), value: o.value }));
+ }
+ if (hasDefaultValue(f)) {
+ item.defaultValue =
+ f.type === "datetimerange" && Array.isArray(f.defaultValue)
+ ? f.defaultValue
+ : f.defaultValue;
+ }
+ return item;
+ });
+ const payload = {
+ summaryPlaceholder: (data.summaryPlaceholder || "").trim(),
+ fields,
+ };
+ return JSON.stringify(payload);
+}
+
+export function applyFormConfigPreset(presetKey) {
+ const preset = FORM_CONFIG_PRESETS.find((p) => p.key === presetKey);
+ if (!preset) return createEmptyFormConfigData();
+ return parseFormConfigToData({
+ summaryPlaceholder: preset.summaryPlaceholder,
+ fields: preset.fields,
+ });
+}
+
+export function validateFormConfigData(formConfigData) {
+ const fields = formConfigData?.fields || [];
+ if (!fields.length) {
+ return { ok: true };
+ }
+ const keys = new Set();
+ for (let i = 0; i < fields.length; i++) {
+ const f = fields[i];
+ const key = (f.key || "").trim();
+ const label = (f.label || "").trim();
+ if (!key) return { ok: false, message: `璇峰~鍐欑 ${i + 1} 涓~鎶ラ」鐨勫瓧娈垫爣璇哷 };
+ if (!label) return { ok: false, message: `璇峰~鍐欑 ${i + 1} 涓~鎶ラ」鐨勬樉绀哄悕绉癭 };
+ if (keys.has(key)) return { ok: false, message: `瀛楁鏍囪瘑銆�${key}銆嶉噸澶嶏紝璇蜂慨鏀筦 };
+ keys.add(key);
+ if (f.type === "select") {
+ const opts = (f.options || []).filter((o) => (o.label || "").trim() && o.value !== "" && o.value != null);
+ if (!opts.length) return { ok: false, message: `璇蜂负銆�${label}銆嶉厤缃嚦灏戜竴涓笅鎷夐�夐」` };
+ }
+ }
+ return { ok: true };
+}
+
+export function formFieldTypeLabel(type) {
+ return FORM_FIELD_TYPE_OPTIONS.find((x) => x.value === type)?.label || type || "鈥�";
+}
+
+export function formatDefaultValueDisplay(field) {
+ const dv = field?.defaultValue;
+ if (dv === undefined || dv === null || dv === "") return "鈥�";
+ if (field?.type === "datetimerange" && Array.isArray(dv)) {
+ return dv.length === 2 ? `${dv[0]} ~ ${dv[1]}` : "鈥�";
+ }
+ if (field?.type === "select") {
+ const opt = (field.options || []).find((o) => String(o.value) === String(dv));
+ return opt?.label || String(dv);
+ }
+ return String(dv);
+}
+
+/** 灏嗗悗绔ā鏉胯杞负鎻愪氦椤垫ā鏉跨粨鏋勶紙鍚� fields 榛樿鍊硷級 */
+export function buildSubmitTemplateFromRow(row) {
+ const cfg = parseFormConfigToData(row?.formConfig);
+ const fields = (cfg.fields || []).map(({ _uid, ...rest }) => ({
+ ...rest,
+ key: rest.key,
+ label: rest.label,
+ type: rest.type,
+ required: rest.required,
+ rows: rest.rows,
+ min: rest.min,
+ precision: rest.precision,
+ defaultValue: rest.defaultValue,
+ options: rest.options,
+ }));
+ return {
+ label: row?.templateName || "瀹℃壒",
+ approvalType: cfg.approvalType || "",
+ summaryPlaceholder: cfg.summaryPlaceholder || "",
+ approvalMode: cfg.approvalMode || "parallel",
+ fields,
+ };
+}
+
+export function formConfigFieldsSummary(formConfigData) {
+ const fields = formConfigData?.fields || [];
+ if (!fields.length) return "鈥�";
+ return fields.map((f) => f.label || f.key || "鏈懡鍚�").join("銆�");
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
index a79d546..9d7324c 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -1,4 +1,4 @@
-<!--OA妯″潡锛氬鎵规ā鏉匡紙绯荤粺甯哥敤 + 鑷畾涔夊鑺傜偣娴佺▼锛�-->
+<!--OA妯″潡锛氬鎵规ā鏉�-->
<template>
<div class="app-container approve-template-page">
<el-tabs v-model="activeTab" class="template-tabs">
@@ -9,8 +9,9 @@
浠ヤ笅涓� OA 妯″潡鍐呯疆鐨勫父鐢ㄥ鎵圭被鍨嬶紝濉姤瀛楁涓庨粯璁ゅ鎵规柟寮忕敱绯荤粺缁存姢锛涙彁浜ゅ鎵规椂鍙洿鎺ラ�夌敤銆�
</template>
</el-alert>
- <div class="builtin-grid">
- <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card">
+ <div v-loading="builtinLoading" class="builtin-grid">
+ <template v-if="builtinTemplates.length">
+ <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card">
<span class="builtin-label">{{ item.label }}</span>
<p class="builtin-summary">{{ item.summary }}</p>
<div class="builtin-meta">
@@ -20,7 +21,9 @@
</el-tag>
<el-tag size="small" type="info" effect="plain">鍙</el-tag>
</div>
- </div>
+ </div>
+ </template>
+ <el-empty v-else-if="!builtinLoading" description="鏆傛棤绯荤粺甯哥敤瀹℃壒妯℃澘" :image-size="80" />
</div>
</el-tab-pane>
@@ -66,7 +69,7 @@
<el-dialog
v-model="formDialog.visible"
:title="formDialog.title"
- width="960px"
+ width="1020px"
append-to-body
destroy-on-close
class="template-form-dialog"
@@ -74,12 +77,24 @@
>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-row :gutter="20">
- <el-col :span="12">
+ <el-col :span="8">
<el-form-item label="妯℃澘鍚嶇О" prop="templateName">
<el-input v-model="form.templateName" placeholder="濡傦細椤圭洰绔嬮」瀹℃壒" maxlength="50" show-word-limit />
</el-form-item>
</el-col>
- <el-col :span="12">
+ <el-col :span="8">
+ <el-form-item label="妯℃澘绫诲瀷" prop="templateType">
+ <el-select v-model="form.templateType" placeholder="璇烽�夋嫨" style="width: 100%">
+ <el-option
+ v-for="opt in TEMPLATE_TYPE_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
<el-form-item label="鍚敤鐘舵��">
<el-switch v-model="form.enabled" active-text="鍚敤" inactive-text="鍋滅敤" />
</el-form-item>
@@ -94,6 +109,10 @@
maxlength="200"
show-word-limit
/>
+ </el-form-item>
+ <el-form-item label="濉姤閰嶇疆">
+ <FormConfigEditor v-model="form.formConfigData" />
+ <p class="flow-tip">閰嶇疆鎻愪氦瀹℃壒鏃堕渶濉啓鐨勮〃鍗曢」锛屼繚瀛樺悗鍐欏叆 formConfig锛圝SON锛夈��</p>
</el-form-item>
<el-form-item label="瀹℃壒娴佺▼" required>
<TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" />
@@ -110,17 +129,44 @@
<!-- 璇︽儏 -->
<el-dialog v-model="detailDialog.visible" title="妯℃澘璇︽儏" width="880px" append-to-body destroy-on-close>
+ <div v-loading="detailLoading" class="detail-dialog-body">
<el-descriptions :column="2" border>
<el-descriptions-item label="妯℃澘鍚嶇О">{{ detailRow.templateName }}</el-descriptions-item>
+ <el-descriptions-item label="妯℃澘绫诲瀷">{{ templateTypeLabel(detailRow.templateType) }}</el-descriptions-item>
<el-descriptions-item label="鐘舵��">
<el-tag :type="detailRow.enabled !== false ? 'success' : 'info'" size="small">
{{ detailRow.enabled !== false ? "鍚敤" : "鍋滅敤" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="璇存槑" :span="2">{{ detailRow.description || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="鍒涘缓鏃堕棿">{{ detailRow.createTime || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="鏇存柊鏃堕棿">{{ detailRow.updateTime || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="濉姤鎻愮ず" :span="2">
+ {{ detailFormConfig.summaryPlaceholder || "鈥�" }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ detailRow.createdUserName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">{{ formatDisplayTime(detailRow.createdTime) }}</el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">{{ formatDisplayTime(detailRow.updatedTime) }}</el-descriptions-item>
</el-descriptions>
+ <el-divider content-position="left">濉姤椤癸紙{{ detailFormConfig.fields?.length || 0 }} 椤癸級</el-divider>
+ <el-table
+ v-if="detailFormConfig.fields?.length"
+ :data="detailFormConfig.fields"
+ border
+ size="small"
+ class="mb16"
+ >
+ <el-table-column prop="label" label="鏄剧ず鍚嶇О" min-width="120" />
+ <el-table-column prop="key" label="瀛楁鏍囪瘑" min-width="100" />
+ <el-table-column label="绫诲瀷" width="100">
+ <template #default="{ row }">{{ formFieldTypeLabel(row.type) }}</template>
+ </el-table-column>
+ <el-table-column label="蹇呭~" width="70" align="center">
+ <template #default="{ row }">{{ row.required !== false ? "鏄�" : "鍚�" }}</template>
+ </el-table-column>
+ <el-table-column label="榛樿鍊�" min-width="120" show-overflow-tooltip>
+ <template #default="{ row }">{{ formatDefaultValueDisplay(row) }}</template>
+ </el-table-column>
+ </el-table>
+ <el-empty v-else description="鏈厤缃~鎶ラ」" :image-size="48" class="mb16" />
<el-divider content-position="left">瀹℃壒娴佺▼锛坽{ detailRow.flowNodes?.length || 0 }} 涓妭鐐癸級</el-divider>
<div v-if="detailRow.flowNodes?.length" class="detail-flow">
<div v-for="(node, index) in detailRow.flowNodes" :key="index" class="detail-node">
@@ -145,6 +191,7 @@
</div>
</div>
<el-empty v-else description="鏆傛棤娴佺▼鑺傜偣" :image-size="60" />
+ </div>
<template #footer>
<el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
<el-button type="primary" @click="editFromDetail">缂� 杈�</el-button>
@@ -156,16 +203,23 @@
<script setup>
import { ArrowRight, Document, Plus, RefreshRight } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
-import { onMounted, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
+import FormConfigEditor from "./components/FormConfigEditor.vue";
import TemplateFlowEditor from "./components/TemplateFlowEditor.vue";
+import { formatDisplayTime } from "./approveTemplateConstants.js";
+import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js";
import { useApproveTemplate } from "./useApproveTemplate.js";
const at = useApproveTemplate();
const {
Search,
+ TEMPLATE_TYPE_OPTIONS,
+ templateTypeLabel,
activeTab,
builtinTemplates,
+ builtinLoading,
+ loadBuiltinTemplates,
nodeSignModeLabel,
searchForm,
tableLoading,
@@ -178,6 +232,8 @@
formRules,
detailDialog,
detailRow,
+ detailLoading,
+ fetchTemplateList,
handleQuery,
resetSearch,
pagination,
@@ -187,6 +243,10 @@
} = at;
const flowUserOptions = ref([]);
+
+const detailFormConfig = computed(() =>
+ parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig)
+);
function unwrapArray(payload) {
if (Array.isArray(payload)) return payload;
@@ -227,7 +287,8 @@
onMounted(() => {
loadUsers();
- handleQuery();
+ loadBuiltinTemplates();
+ fetchTemplateList();
});
</script>
@@ -237,6 +298,9 @@
}
.mb16 {
margin-bottom: 16px;
+}
+.mb16.el-empty {
+ padding: 8px 0;
}
.ml10 {
margin-left: 10px;
@@ -355,6 +419,9 @@
transform: translateY(-50%);
color: var(--el-text-color-placeholder);
}
+.detail-dialog-body {
+ min-height: 120px;
+}
.text-muted {
font-size: 12px;
color: var(--el-text-color-placeholder);
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
index c56d055..8489e13 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -1,24 +1,49 @@
-import { Search } from "@element-plus/icons-vue";
-import dayjs from "dayjs";
-import { ElMessageBox } from "element-plus";
-import { computed, reactive, ref, watch } from "vue";
import {
+ addApprovalTemplate,
+ deleteApprovalTemplate,
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ listApprovalTemplatePage,
+ TEMPLATE_TYPE_BUILTIN,
+ TEMPLATE_TYPE_CUSTOM,
+ TEMPLATE_TYPE_OPTIONS,
+ 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 {
+ buildApprovalTemplateListParams,
createEmptyTemplateForm,
- createInitialMockTemplates,
flowNodesSummary,
- getBuiltinTemplates,
- loadStoredTemplates,
+ mapBuiltinCardFromApi,
+ mapTemplateFromApi,
+ mapTemplateToApi,
nodeSignModeLabel,
- saveStoredTemplates,
+ templateTypeLabel,
+ unwrapTemplateList,
+ formatDisplayTime,
+ unwrapTemplateDetail,
validateTemplateForm,
} from "./approveTemplateConstants.js";
+import { parseFormConfigToData } from "./formConfigUtils.js";
+
+const LEGACY_STORAGE_KEY = "oa_approve_template_custom_v1";
+
+function clearLegacyStorage() {
+ try {
+ localStorage.removeItem(LEGACY_STORAGE_KEY);
+ } catch {
+ /* ignore */
+ }
+}
export function useApproveTemplate() {
- const stored = loadStoredTemplates();
- const allTemplates = ref(stored?.length ? stored : createInitialMockTemplates());
+ clearLegacyStorage();
const activeTab = ref("custom");
- const builtinTemplates = getBuiltinTemplates();
+ const builtinTemplates = ref([]);
+ const builtinLoading = ref(false);
const searchForm = reactive({
keyword: "",
@@ -27,6 +52,7 @@
const tableLoading = ref(false);
const page = reactive({ current: 1, size: 10, total: 0 });
+ const tableData = ref([]);
const formDialog = reactive({ visible: false, title: "", mode: "add" });
const form = reactive(createEmptyTemplateForm());
@@ -34,44 +60,22 @@
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
-
- const filteredList = computed(() => {
- let list = [...allTemplates.value];
- const kw = (searchForm.keyword || "").trim().toLowerCase();
- if (kw) {
- list = list.filter((r) => {
- const name = (r.templateName || "").toLowerCase();
- const desc = (r.description || "").toLowerCase();
- return name.includes(kw) || desc.includes(kw);
- });
- }
- if (searchForm.enabledOnly) {
- list = list.filter((r) => r.enabled !== false);
- }
- return list.sort((a, b) => (String(a.updateTime) < String(b.updateTime) ? 1 : -1));
- });
-
- watch(
- filteredList,
- (list) => {
- page.total = list.length;
- const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
- if (page.current > maxPage) page.current = maxPage;
- },
- { immediate: true }
- );
-
- const tableData = computed(() => {
- const start = (page.current - 1) * page.size;
- return filteredList.value.slice(start, start + page.size);
- });
+ const detailLoading = ref(false);
const formRules = {
templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
+ templateType: [{ required: true, message: "璇烽�夋嫨妯℃澘绫诲瀷", trigger: "change" }],
};
const tableColumn = ref([
{ label: "妯℃澘鍚嶇О", prop: "templateName", minWidth: 140 },
+ {
+ label: "妯℃澘绫诲瀷",
+ prop: "templateType",
+ width: 100,
+ align: "center",
+ formatData: (v) => templateTypeLabel(v),
+ },
{ label: "璇存槑", prop: "description", minWidth: 160, showOverflowTooltip: true },
{
label: "鑺傜偣鏁�",
@@ -96,31 +100,72 @@
formatData: (v) => (v !== false ? "鍚敤" : "鍋滅敤"),
formatType: (v) => (v !== false ? "success" : "info"),
},
- { label: "鏇存柊鏃堕棿", prop: "updateTime", width: 170 },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createdTime",
+ width: 170,
+ showOverflowTooltip: true,
+ formatData: (v) => formatDisplayTime(v),
+ },
+ {
+ label: "鏇存柊鏃堕棿",
+ prop: "updatedTime",
+ width: 170,
+ showOverflowTooltip: true,
+ formatData: (v) => formatDisplayTime(v),
+ },
{
dataType: "action",
label: "鎿嶄綔",
align: "center",
fixed: "right",
- width: 200,
+ width: 220,
operation: [
{ name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
{ name: "缂栬緫", type: "text", clickFun: (row) => openFormDialog("edit", row) },
- { name: "鍒犻櫎", type: "text", clickFun: (row) => removeTemplate(row) },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ clickFun: (row) => removeTemplate(row),
+ },
],
},
]);
- function persist() {
- saveStoredTemplates(allTemplates.value);
+ async function loadBuiltinTemplates() {
+ builtinLoading.value = true;
+ try {
+ const res = await listApprovalTemplate(TEMPLATE_TYPE_BUILTIN);
+ builtinTemplates.value = unwrapTemplateList(res).map(mapBuiltinCardFromApi);
+ } catch {
+ builtinTemplates.value = [];
+ ElMessage.warning("绯荤粺甯哥敤瀹℃壒鍔犺浇澶辫触");
+ } finally {
+ builtinLoading.value = false;
+ }
+ }
+
+ async function fetchTemplateList() {
+ tableLoading.value = true;
+ try {
+ const res = await listApprovalTemplatePage(
+ buildApprovalTemplateListParams({ page, searchForm })
+ );
+ const data = res?.data || {};
+ tableData.value = (data.records || []).map(mapTemplateFromApi);
+ page.total = Number(data.total || 0);
+ } catch {
+ tableData.value = [];
+ page.total = 0;
+ } finally {
+ tableLoading.value = false;
+ }
}
function handleQuery() {
- tableLoading.value = true;
page.current = 1;
- setTimeout(() => {
- tableLoading.value = false;
- }, 150);
+ fetchTemplateList();
}
function resetSearch() {
@@ -132,6 +177,7 @@
function pagination({ page: p, limit }) {
page.current = p;
page.size = limit;
+ fetchTemplateList();
}
function resetForm(row) {
@@ -145,6 +191,11 @@
id: row.id,
templateName: row.templateName || "",
description: row.description || "",
+ templateType: row.templateType ?? TEMPLATE_TYPE_CUSTOM,
+ formConfig: row.formConfig || "",
+ formConfigData: JSON.parse(
+ JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
+ ),
enabled: row.enabled !== false,
flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
});
@@ -152,19 +203,27 @@
function openFormDialog(mode, row) {
formDialog.mode = mode;
- formDialog.title = mode === "add" ? "鏂板缓鑷畾涔夊鎵规ā鏉�" : "缂栬緫鑷畾涔夊鎵规ā鏉�";
+ formDialog.title = mode === "add" ? "鏂板缓瀹℃壒妯℃澘" : "缂栬緫瀹℃壒妯℃澘";
resetForm(mode === "edit" ? row : null);
formDialog.visible = true;
}
- function openDetail(row) {
- detailRow.value = { ...row };
+ async function openDetail(row) {
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鏌ョ湅璇︽儏锛氱己灏戞ā鏉� ID");
+ return;
+ }
detailDialog.visible = true;
- }
-
- function isNameDuplicate(name, excludeId) {
- const n = (name || "").trim();
- return allTemplates.value.some((t) => t.templateName?.trim() === n && t.id !== excludeId);
+ detailLoading.value = true;
+ detailRow.value = {};
+ try {
+ const res = await getApprovalTemplateDetail(row.id);
+ detailRow.value = mapTemplateFromApi(unwrapTemplateDetail(res));
+ } catch {
+ detailDialog.visible = false;
+ } finally {
+ detailLoading.value = false;
+ }
}
async function submitForm() {
@@ -178,64 +237,70 @@
if (!validated.ok) {
return { message: validated.message };
}
- if (isNameDuplicate(validated.name, form.id)) {
- return { message: "妯℃澘鍚嶇О宸插瓨鍦紝璇锋洿鎹㈠悕绉�" };
+ if (formDialog.mode === "edit" && !form.id) {
+ return { message: "缂哄皯妯℃澘 ID锛屾棤娉曚繚瀛樹慨鏀�" };
}
- const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
- if (formDialog.mode === "add") {
- allTemplates.value.unshift({
- id: `tpl_${Date.now()}`,
- templateName: validated.name,
- description: (form.description || "").trim(),
- enabled: form.enabled !== false,
- createTime: now,
- updateTime: now,
- flowNodes: validated.nodes,
- });
- } else {
- const hit = allTemplates.value.find((t) => t.id === form.id);
- if (!hit) return { message: "妯℃澘涓嶅瓨鍦ㄦ垨宸插垹闄�" };
- hit.templateName = validated.name;
- hit.description = (form.description || "").trim();
- hit.enabled = form.enabled !== false;
- hit.flowNodes = validated.nodes;
- hit.updateTime = now;
+ const dto = mapTemplateToApi(form);
+ try {
+ if (formDialog.mode === "add") {
+ await addApprovalTemplate(dto);
+ } else {
+ await updateApprovalTemplate(dto);
+ }
+ } catch {
+ return false;
}
- persist();
formDialog.visible = false;
page.current = 1;
+ await fetchTemplateList();
+ if (dto.templateType === TEMPLATE_TYPE_BUILTIN) {
+ await loadBuiltinTemplates();
+ }
return { ok: true };
}
async function removeTemplate(row) {
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戞ā鏉� ID");
+ return;
+ }
+ const name = row.templateName || "鏈懡鍚嶆ā鏉�";
try {
- await ElMessageBox.confirm(`纭畾鍒犻櫎妯℃澘銆�${row.templateName}銆嶅悧锛焋, "鎻愮ず", {
- type: "warning",
- confirmButtonText: "鍒犻櫎",
- cancelButtonText: "鍙栨秷",
- });
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゅ鎵规ā鏉裤��${name}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
} catch {
return;
}
- const idx = allTemplates.value.findIndex((t) => t.id === row.id);
- if (idx >= 0) {
- allTemplates.value.splice(idx, 1);
- persist();
+ try {
+ await deleteApprovalTemplate([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await fetchTemplateList();
+ if (row.templateType === TEMPLATE_TYPE_BUILTIN) {
+ await loadBuiltinTemplates();
+ }
+ } catch {
+ /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
}
- }
-
- function toggleEnabled(row) {
- const hit = allTemplates.value.find((t) => t.id === row.id);
- if (!hit) return;
- hit.enabled = !hit.enabled;
- hit.updateTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
- persist();
}
return {
Search,
+ TEMPLATE_TYPE_OPTIONS,
+ templateTypeLabel,
activeTab,
builtinTemplates,
+ builtinLoading,
+ loadBuiltinTemplates,
+ fetchTemplateList,
nodeSignModeLabel,
flowNodesSummary,
searchForm,
@@ -249,12 +314,12 @@
formRules,
detailDialog,
detailRow,
+ detailLoading,
handleQuery,
resetSearch,
pagination,
openFormDialog,
openDetail,
submitForm,
- toggleEnabled,
};
}
--
Gitblit v1.9.3