From 930d38ed2a3c2131be3305a585602c7a5a275fe3 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期二, 19 五月 2026 17:09:12 +0800
Subject: [PATCH] Merge branch 'dev-new_pro_OA' of http://114.132.189.42:9002/r/product-inventory-management into dev-new_pro_OA
---
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue | 624 ++++++++++
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue | 310 +++--
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue | 116 +
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue | 117 -
src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js | 278 ++++
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js | 263 ++-
src/api/officeProcessAutomation/approvalTemplate.js | 63 +
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js | 465 +++++--
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js | 311 +++-
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js | 686 +++++++----
src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue | 147 ++
src/api/officeProcessAutomation/approvalInstance.js | 47
src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue | 16
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue | 89 +
14 files changed, 2,746 insertions(+), 786 deletions(-)
diff --git a/src/api/officeProcessAutomation/approvalInstance.js b/src/api/officeProcessAutomation/approvalInstance.js
new file mode 100644
index 0000000..054861c
--- /dev/null
+++ b/src/api/officeProcessAutomation/approvalInstance.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+/** 鍒嗛〉鏌ヨ瀹℃壒瀹炰緥 */
+export function listApprovalInstancePage(params) {
+ return request({
+ url: "/approvalInstance/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鎻愪氦/淇濆瓨瀹℃壒瀹炰緥 */
+export function saveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/save",
+ method: "post",
+ data: approvalInstanceDto,
+ });
+}
+
+/** 鏇存柊瀹℃壒瀹炰緥 */
+export function updateApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/update",
+ method: "put",
+ data: approvalInstanceDto,
+ });
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+export function approveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/approve",
+ method: "post",
+ data: approvalInstanceDto,
+ });
+}
+
+/** 鍒犻櫎瀹℃壒瀹炰緥锛坆ody 涓� ID 鏁扮粍锛� */
+export function deleteApprovalInstance(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
+ return request({
+ url: "/approvalInstance/delete",
+ method: "delete",
+ data: idList,
+ });
+}
diff --git a/src/api/officeProcessAutomation/approvalTemplate.js b/src/api/officeProcessAutomation/approvalTemplate.js
new file mode 100644
index 0000000..4aef140
--- /dev/null
+++ b/src/api/officeProcessAutomation/approvalTemplate.js
@@ -0,0 +1,63 @@
+import request from "@/utils/request";
+
+/** 妯℃澘绫诲瀷锛�0 绯荤粺鍐呯疆锛�1 鑷畾涔夛紙涓庡悗绔� templateType 涓�鑷达級 */
+export const TEMPLATE_TYPE_BUILTIN = 0;
+export const TEMPLATE_TYPE_CUSTOM = 1;
+
+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..80af992 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -1,4 +1,13 @@
-import dayjs from "dayjs";
+import {
+ createEmptyNode,
+ formatDisplayTime,
+ mapNodesFromApi,
+ mapSignModeFromApi,
+ mapSignModeToApi,
+ normalizeFlowNodes,
+ nodeSignModeLabel,
+} from "../approve-template/approveTemplateConstants.js";
+import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js";
/** 瀹℃壒绫诲瀷锛堜笌鍚庣瀛楁 approvalType 瀵归綈锛屽悗鏈熷彲鍚屾锛� */
export const APPROVAL_TYPE_OPTIONS = [
@@ -25,105 +34,401 @@
{ value: "cancelled", label: "宸叉挙閿�" },
];
-/** 瀹℃壒鏂瑰紡 approvalMode */
-export const APPROVAL_MODE_OPTIONS = [
- { value: "parallel", label: "涓庣" },
- { value: "or_sign", label: "鎴栫" },
-];
+export const LEGACY_APPROVE_LIST_STORAGE_KEY = "oa_unified_approve_list_v1";
+
+export function clearLegacyApproveListStorage() {
+ try {
+ localStorage.removeItem(LEGACY_APPROVE_LIST_STORAGE_KEY);
+ } catch {
+ /* ignore */
+ }
+}
+
+/** 鎻愪氦寮圭獥锛氭ā鏉垮崱鐗囷紙鏉ヨ嚜鍚庣鍒楄〃锛� */
+export function mapSubmitTemplateCard(row) {
+ const cfg = parseFormConfigToData(row?.formConfig);
+ return {
+ id: row?.id,
+ key: String(row?.id ?? ""),
+ approvalType: cfg.approvalType || row?.approvalType || "",
+ label: row?.templateName || "鈥�",
+ summaryPlaceholder: (row?.description || "").trim() || cfg.summaryPlaceholder || "鐐瑰嚮濉啓骞舵彁浜�",
+ };
+}
+
+/** 瀹℃壒璁板綍 approveAction 鈫� 椤甸潰 result */
+export function mapRecordResultFromApi(action) {
+ const s = String(action || "").toUpperCase();
+ if (s === "APPROVED" || s === "APPROVE" || s === "PASS") return "approved";
+ if (s === "REJECTED" || s === "REJECT" || s === "REFUSE") return "rejected";
+ return "pending";
+}
+
+/** 鍚庣 records 鈫� 鏃堕棿绾垮睍绀虹粨鏋� */
+export function mapRecordsFromApi(records) {
+ const list = Array.isArray(records) ? records : [];
+ return list.map((r) => ({
+ id: r.id,
+ operatorName: r.approverName || r.operatorName || r.createUserName || "",
+ result: mapRecordResultFromApi(r.approveAction ?? r.action ?? r.status),
+ opinion: r.approveComment || r.comment || r.opinion || "",
+ time: formatDisplayTime(r.approveTime || r.createTime || r.time || ""),
+ raw: r,
+ }));
+}
+
+export function mapTaskStatusLabel(status) {
+ const s = String(status || "").toUpperCase();
+ if (s === "APPROVED") return "宸查�氳繃";
+ if (s === "REJECTED") return "宸查┏鍥�";
+ if (s === "PENDING") return "寰呭鎵�";
+ if (s === "CANCELLED") return "宸叉挙閿�";
+ return status || "鈥�";
+}
+
+export function mapTaskStatusTagType(status) {
+ const s = String(status || "").toUpperCase();
+ if (s === "APPROVED") return "success";
+ if (s === "REJECTED") return "danger";
+ if (s === "CANCELLED") return "info";
+ return "warning";
+}
+
+/** 鍚庣 tasks 鈫� 椤甸潰 flowNodes锛堟寜 levelNo 鍒嗙粍锛屼緵娴佺▼缂栬緫/灞曠ず锛� */
+export function mapTasksToFlowNodes(tasks) {
+ const list = Array.isArray(tasks) ? tasks : [];
+ if (!list.length) return [];
+ const byLevel = new Map();
+ list.forEach((t) => {
+ const level = Number(t.levelNo ?? t.taskLevel ?? t.nodeOrder ?? 1);
+ if (!byLevel.has(level)) {
+ byLevel.set(level, {
+ id: t.nodeId,
+ templateId: t.templateId,
+ nodeOrder: level,
+ signMode: mapSignModeFromApi(t.approveType),
+ approvers: [],
+ tasks: [],
+ });
+ }
+ const node = byLevel.get(level);
+ node.approvers.push({
+ id: t.id,
+ nodeId: t.nodeId,
+ templateId: t.templateId,
+ approverId: t.approverId,
+ approverName: t.approverName || "",
+ status: t.status,
+ approveComment: t.approveComment,
+ approveTime: t.approveTime,
+ });
+ node.tasks.push(t);
+ if (t.approveType != null) {
+ node.signMode = mapSignModeFromApi(t.approveType);
+ }
+ });
+ return [...byLevel.entries()]
+ .sort(([a], [b]) => a - b)
+ .map(([, node]) => node);
+}
+
+/** 椤甸潰 flowNodes 鈫� 鍚庣 tasks */
+export function mapFlowNodesToTasks(flowNodes, { instanceId, templateId } = {}) {
+ const nodes = normalizeFlowNodes(flowNodes);
+ const tasks = [];
+ nodes.forEach((n) => {
+ const levelNo = n.nodeOrder ?? 1;
+ const approveType = mapSignModeToApi(n.signMode);
+ n.approvers.forEach((a, idx) => {
+ const task = {
+ levelNo,
+ approveType,
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ sortNo: a.sortNo ?? idx + 1,
+ };
+ if (a.id != null) task.id = a.id;
+ if (a.nodeId != null) task.nodeId = a.nodeId;
+ if (a.templateId != null) task.templateId = a.templateId;
+ else if (templateId) task.templateId = templateId;
+ if (instanceId) task.instanceId = instanceId;
+ if (a.status != null) task.status = a.status;
+ tasks.push(task);
+ });
+ });
+ return tasks;
+}
+
+function guessFieldTypeFromValue(val) {
+ if (Array.isArray(val) && val.length === 2) return "datetimerange";
+ if (typeof val === "number") return "number";
+ if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) return "date";
+ if (typeof val === "string" && val.length > 100) return "textarea";
+ return "text";
+}
+
+/** 鍗曞瓧娈靛睍绀哄�硷紙璇︽儏鍙锛� */
+export function formatFieldDisplayValue(field, val) {
+ if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "鈥�";
+ if (field?.type === "select" && field.options?.length) {
+ const hit = field.options.find((o) => String(o.value) === String(val));
+ return hit?.label || String(val);
+ }
+ if (Array.isArray(val)) return val.join(" 鑷� ");
+ return String(val);
+}
/**
- * 鎻愪氦瀹℃壒妯℃澘锛堟寜绫诲瀷涓�閿~鎶ワ紝瀛楁鍚庢湡涓庡悗绔ā鏉垮悓姝ワ級
+ * 浠庤鏁版嵁 / formConfig 瑙f瀽濉姤瀛楁瀹氫箟涓� formPayload锛堜笌鏂板鎻愪氦缁撴瀯涓�鑷达級
*/
-export const SUBMIT_TEMPLATES = {
- cost_reimburse: {
- approvalType: "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 },
- ],
- approvalMode: "parallel",
- },
- travel_reimburse: {
- approvalType: "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 },
- ],
- approvalMode: "parallel",
- },
- overtime: {
- approvalType: "overtime",
- label: "鍔犵彮鐢宠",
- fields: [
- { key: "summary", label: "鍔犵彮浜嬬敱", type: "textarea", required: true, rows: 3 },
- { key: "overtimeDate", label: "鍔犵彮鏃ユ湡", type: "date", required: true },
- { key: "hours", label: "鍔犵彮鏃堕暱(灏忔椂)", type: "number", required: true, min: 0.5, precision: 1 },
- ],
- approvalMode: "parallel",
- },
- leave: {
- approvalType: "leave",
- label: "璇峰亣鐢宠",
- 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 },
- ],
- approvalMode: "parallel",
- },
- work_handover: {
- approvalType: "work_handover",
- label: "宸ヤ綔浜ゆ帴",
- fields: [
- { key: "summary", label: "浜ゆ帴璇存槑", type: "textarea", required: true, rows: 3 },
- { key: "handoverTo", label: "浜ゆ帴瀵硅薄", type: "text", required: true },
- ],
- approvalMode: "parallel",
- },
- regular: {
- approvalType: "regular",
- label: "杞鐢宠",
- fields: [
- { key: "summary", label: "杞璇存槑", type: "textarea", required: true, rows: 3 },
- { key: "regularDate", label: "鎷熻浆姝f棩鏈�", type: "date", required: true },
- ],
- approvalMode: "parallel",
- },
- resign: {
- approvalType: "resign",
- label: "绂昏亴鐢宠",
- fields: [
- { key: "summary", label: "绂昏亴鍘熷洜", type: "textarea", required: true, rows: 3 },
- { key: "lastWorkDay", label: "鏈�鍚庡伐浣滄棩", type: "date", required: true },
- ],
- approvalMode: "or_sign",
- },
- transfer: {
- approvalType: "transfer",
- label: "璋冨矖鐢宠",
- fields: [
- { key: "summary", label: "璋冨矖璇存槑", type: "textarea", required: true, rows: 2 },
- { key: "targetDept", label: "鐩爣閮ㄩ棬", type: "text", required: true },
- { key: "targetPost", label: "鐩爣宀椾綅", type: "text", required: true },
- ],
- approvalMode: "parallel",
- },
-};
+export function resolveInstanceFormFields(row) {
+ const cfg = parseInstanceFormConfig(row?.formConfig);
+ let fields = (row?.formFieldDefs?.length ? row.formFieldDefs : cfg.fields) || [];
+ const formPayload = {
+ ...(fields.length ? buildFormPayloadFromFields(fields) : {}),
+ ...cfg.formPayload,
+ ...(row?.formPayload || {}),
+ };
+ if (!fields.length && Object.keys(formPayload).length) {
+ fields = Object.keys(formPayload)
+ .filter((k) => k && k !== "summary")
+ .map((k) => ({
+ key: k,
+ label: k,
+ type: guessFieldTypeFromValue(formPayload[k]),
+ required: false,
+ rows: 3,
+ min: 0,
+ precision: 0,
+ options: [],
+ }));
+ }
+ const templateSnapshot = {
+ label: row?.templateName || row?.title || "瀹℃壒",
+ approvalType: cfg.approvalType || row?.approvalType || "",
+ summaryPlaceholder: cfg.summaryPlaceholder || "",
+ templateId: row?.templateId,
+ fields,
+ };
+ return { fields, formPayload, templateSnapshot, formConfigData: cfg };
+}
-export const STORAGE_KEY = "oa_unified_approve_list_v1";
+/** 瑙f瀽瀹炰緥 formConfig */
+export function parseInstanceFormConfig(formConfig) {
+ let raw = {};
+ if (formConfig) {
+ if (typeof formConfig === "object") raw = formConfig;
+ else {
+ try {
+ raw = JSON.parse(formConfig);
+ } catch {
+ raw = {};
+ }
+ }
+ }
+ const data = parseFormConfigToData(formConfig);
+ const payload = raw.formPayload;
+ return {
+ summaryPlaceholder: raw.summaryPlaceholder || data.summaryPlaceholder || "",
+ approvalType: raw.approvalType || "",
+ fields: data.fields || [],
+ formPayload: payload && typeof payload === "object" ? payload : {},
+ };
+}
+
+export function unwrapInstanceDetail(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") return {};
+ if (data.id != null || data.instanceNo) return data;
+ if (data.approvalInstanceVo) return data.approvalInstanceVo;
+ return data;
+}
+
+/** 濉姤鍐呭 + 妯℃澘瀛楁瀹氫箟 鈫� formConfig JSON */
+export function buildInstanceFormConfigJson(templateSnapshot, formPayload) {
+ const payload = formPayload || {};
+ return JSON.stringify({
+ summaryPlaceholder: templateSnapshot?.summaryPlaceholder || "",
+ approvalType: templateSnapshot?.approvalType || "",
+ fields: templateSnapshot?.fields || [],
+ formPayload: payload,
+ });
+}
+
+/** 缁勮淇濆瓨/鏇存柊瀹℃壒 DTO */
+export function buildInstanceDto({ submitForm, activeTemplate, userStore, flowNodes, existingRow }) {
+ const payload = submitForm?.formPayload || {};
+ const tpl = activeTemplate || {};
+ const title =
+ String(payload.summary || payload.title || "").trim() ||
+ tpl.label ||
+ submitForm?.templateName ||
+ "瀹℃壒鐢宠";
+ const templateId = submitForm?.templateId || tpl.templateId;
+ const instanceId = existingRow?.id ?? submitForm?.instanceId;
+ const taskList = mapFlowNodesToTasks(flowNodes || submitForm?.flowNodes, {
+ instanceId,
+ templateId,
+ });
+ const isUpdate = Boolean(instanceId);
+
+ const dto = {
+ templateId,
+ templateName: submitForm?.templateName || tpl.label || "",
+ title,
+ formConfig: buildInstanceFormConfigJson({ ...tpl, fields: tpl.fields || submitForm?.formFieldDefs }, payload),
+ tasks: taskList,
+ };
+
+ if (isUpdate) {
+ dto.id = existingRow?.id ?? submitForm?.instanceId;
+ dto.instanceNo = existingRow?.instanceNo ?? submitForm?.instanceNo ?? "";
+ dto.status =
+ existingRow?.statusRaw || mapInstanceStatusToApi(existingRow?.approvalStatus) || "PENDING";
+ dto.currentLevel = existingRow?.currentLevel ?? submitForm?.currentLevel ?? 1;
+ dto.applicantId = existingRow?.applicantId ?? existingRow?.applicantNo;
+ dto.applicantName = existingRow?.applicantName || "";
+ } else {
+ dto.status = "PENDING";
+ dto.currentLevel = 1;
+ dto.applicantId = userStore?.id;
+ dto.applicantName = userStore?.nickName || userStore?.name || "";
+ }
+ return dto;
+}
+
+/** @deprecated 浣跨敤 buildInstanceDto */
+export function buildSaveInstanceDto(params) {
+ return buildInstanceDto(params);
+}
+
+/** 鏍¢獙鎻愪氦瀹℃壒娴佺▼锛堜笌妯℃澘椤佃鍒欎竴鑷达級 */
+export function validateSubmitFlowNodes(flowNodes) {
+ const nodes = normalizeFlowNodes(flowNodes);
+ if (!nodes.length) return { ok: false, message: "璇疯嚦灏戦厤缃竴涓鎵硅妭鐐�" };
+ for (let i = 0; i < nodes.length; i++) {
+ if (!nodes[i].approvers.length) {
+ return { ok: false, message: `璇蜂负绗� ${i + 1} 涓妭鐐归�夋嫨鑷冲皯涓�鍚嶅鎵逛汉` };
+ }
+ }
+ return { ok: true, nodes };
+}
+
+/** 鍚庣 status 鈫� 椤甸潰 approvalStatus */
+export function mapInstanceStatusFromApi(status) {
+ const s = String(status || "").toUpperCase();
+ if (s === "APPROVED") return "approved";
+ if (s === "REJECTED") return "rejected";
+ if (s === "CANCELLED") return "cancelled";
+ return "pending";
+}
+
+/** 椤甸潰 approvalStatus 鈫� 鍚庣 status */
+export function mapInstanceStatusToApi(approvalStatus) {
+ const s = String(approvalStatus || "").toLowerCase();
+ if (s === "approved") return "APPROVED";
+ if (s === "rejected") return "REJECTED";
+ if (s === "cancelled") return "CANCELLED";
+ return "PENDING";
+}
+
+export function unwrapInstancePage(res) {
+ const data = res?.data ?? res;
+ return {
+ records: Array.isArray(data?.records) ? data.records : [],
+ total: Number(data?.total ?? 0),
+ };
+}
+
+/** 鍒嗛〉鍒楄〃椤� 鈫� 琛ㄦ牸琛� */
+export function mapInstanceFromApi(row) {
+ if (!row) return {};
+ const approvalStatus = mapInstanceStatusFromApi(row.status);
+ const createTime = formatDisplayTime(row.createTime ?? row.applyTime ?? "");
+ const applyTime = formatDisplayTime(row.applyTime ?? "");
+ const finishTime = formatDisplayTime(row.finishTime ?? "");
+ const resolved = resolveInstanceFormFields(row);
+ const { fields, formPayload, templateSnapshot } = resolved;
+ const tasks = Array.isArray(row.tasks) ? row.tasks : [];
+ const flowNodes = tasks.length
+ ? mapTasksToFlowNodes(tasks)
+ : mapNodesFromApi(row.nodes || row.flowNodes);
+ const approvalRecords = mapRecordsFromApi(row.records);
+ return {
+ id: row.id,
+ bizId: row.instanceNo || String(row.id ?? ""),
+ instanceNo: row.instanceNo || "",
+ templateId: row.templateId,
+ templateName: row.templateName || "",
+ businessId: row.businessId,
+ businessType: row.businessType,
+ businessName: row.businessName || "",
+ applicantId: row.applicantId,
+ applicantNo: row.applicantId != null ? String(row.applicantId) : "",
+ applicantName: row.applicantName || "",
+ approvalType: row.templateName || "",
+ unread: Boolean(row.isApprove) && approvalStatus === "pending",
+ isApprove: Boolean(row.isApprove),
+ approvalStatus,
+ statusRaw: row.status,
+ createTime,
+ applyTime: applyTime === "鈥�" ? "" : applyTime,
+ finishTime: finishTime === "鈥�" ? "" : finishTime,
+ title: row.title || "",
+ summary: row.title || row.templateName || "",
+ currentLevel: row.currentLevel,
+ formConfig: row.formConfig,
+ formPayload,
+ formFieldDefs: fields,
+ templateSnapshot,
+ tasks,
+ records: Array.isArray(row.records) ? row.records : [],
+ flowNodes,
+ approvalFlowNodes: [],
+ currentNodeIndex: 0,
+ approvalRecords,
+ rejectReason:
+ approvalRecords.find((r) => r.result === "rejected")?.opinion || "",
+ };
+}
+
+/** 瀹℃壒鎿嶄綔锛氫笌鍚庣 status 鏋氫妇涓�鑷� */
+export const APPROVE_ACTION_APPROVED = "APPROVED";
+export const APPROVE_ACTION_REJECTED = "REJECTED";
+
+/** 椤甸潰鎿嶄綔 鈫� approveAction */
+export function mapApproveActionToApi(uiResult) {
+ return uiResult === "rejected" ? APPROVE_ACTION_REJECTED : APPROVE_ACTION_APPROVED;
+}
+
+/** 缁勮瀹℃壒鎻愪氦 DTO */
+export function buildApproveInstanceDto(row, uiResult, comment) {
+ const opinion = (comment || "").trim();
+ return {
+ id: row?.id,
+ approveAction: mapApproveActionToApi(uiResult),
+ approveComment: opinion || (uiResult === "approved" ? "鍚屾剰" : ""),
+ };
+}
+
+export function buildApprovalInstanceListParams({ page, searchForm }) {
+ const params = {
+ current: page.current,
+ size: page.size,
+ };
+ const dto = {};
+ const kw = (searchForm?.applicantKeyword || "").trim();
+ if (kw) dto.applicantName = kw;
+ if (searchForm?.approvalType) {
+ const opt = APPROVAL_TYPE_OPTIONS.find((x) => x.value === searchForm.approvalType);
+ if (opt?.label) dto.templateName = opt.label;
+ }
+ if (Object.keys(dto).length) params.approvalInstanceDto = dto;
+ return params;
+}
export function approvalTypeLabel(v) {
- return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || "鈥�";
+ return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "鈥�";
}
export function approvalTypeStyle(v) {
@@ -147,170 +452,57 @@
return "primary";
}
-export function approvalModeLabel(v) {
- if (v === "countersign") return "鎴栫";
- return APPROVAL_MODE_OPTIONS.find((x) => x.value === v)?.label || "涓庣";
-}
-
export function unreadLabel(v) {
return v ? "鏄�" : "鍚�";
}
-export function buildDefaultFlowNodes() {
- return [
- {
- approverId: "mock_supervisor",
- approverName: "鐩村睘涓婄骇",
- sortOrder: 1,
- nodeOrder: 1,
- nodeStatus: "process",
- approveOpinion: "",
- approveTime: "",
- },
- {
- approverId: "mock_manager",
- approverName: "閮ㄩ棬缁忕悊",
- sortOrder: 2,
- nodeOrder: 2,
- nodeStatus: "wait",
- approveOpinion: "",
- approveTime: "",
- },
- ];
-}
+/** 鍒楄〃琛� 鈫� 缂栬緫琛ㄥ崟锛堜粎鐢ㄨ鏁版嵁鍥炴樉锛� */
+export function buildEditFormFromInstanceRow(row) {
+ const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row);
+ const normalized = normalizeFlowNodes(
+ row?.flowNodes?.length ? row.flowNodes : mapTasksToFlowNodes(row?.tasks)
+ );
+ const flowNodes = normalized.length
+ ? JSON.parse(JSON.stringify(normalized))
+ : [createEmptyNode(1)];
-function demoRow(partial) {
- const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
return {
- id: partial.id,
- bizId: partial.bizId || partial.id,
- applicantNo: partial.applicantNo,
- applicantName: partial.applicantName,
- approvalType: partial.approvalType,
- approvalMode: partial.approvalMode || "parallel",
- unread: partial.unread ?? false,
- approvalStatus: partial.approvalStatus || "pending",
- createTime: partial.createTime || now,
- summary: partial.summary || "",
- formPayload: partial.formPayload || {},
- approvalFlowNodes: partial.approvalFlowNodes || buildDefaultFlowNodes(),
- currentNodeIndex: partial.currentNodeIndex ?? 0,
- approvalRecords: partial.approvalRecords || [],
- rejectReason: partial.rejectReason || "",
- sourceRoute: partial.sourceRoute || "",
+ templateKey: String(row?.templateId || ""),
+ templateId: row?.templateId,
+ templateName: row?.templateName || templateSnapshot.label,
+ instanceId: row?.id,
+ instanceNo: row?.instanceNo || "",
+ statusRaw: row?.statusRaw || row?.status || "PENDING",
+ currentLevel: row?.currentLevel ?? 1,
+ applicantId: row?.applicantId,
+ applicantName: row?.applicantName || "",
+ templateSnapshot,
+ formFieldDefs: fields,
+ formPayload,
+ flowNodes,
};
}
-/** 鍒濆婕旂ず鏁版嵁锛堝叡 22 鏉★紝涓庡師鍨嬫暟閲忎竴鑷达級 */
-export function createInitialMockRows() {
- const types = [
- "cost_reimburse",
- "travel_reimburse",
- "overtime",
- "leave",
- "work_handover",
- "regular",
- "resign",
- "transfer",
- "cost_reimburse",
- "leave",
- "overtime",
- "travel_reimburse",
- "work_handover",
- "regular",
- "cost_reimburse",
- "leave",
- "transfer",
- "resign",
- "overtime",
- "travel_reimburse",
- "cost_reimburse",
- "leave",
- ];
- const applicants = [
- { no: "007", name: "鑻规灉" },
- { no: "Guest001", name: "澶栭儴鐢ㄦ埛" },
- { no: "0056", name: "鐜嬩簲" },
- { no: "0042", name: "鏉庡洓" },
- { no: "0088", name: "鐚尗" },
- { no: "0012", name: "寮犱笁" },
- { no: "0033", name: "璧靛叚" },
- ];
- const summaries = [
- "鍔炲叕鐢ㄥ搧閲囪喘鎶ラ攢",
- "涓婃捣鍑哄樊宸梾璐�",
- "鍛ㄦ湯椤圭洰鍔犵彮",
- "骞村亣 3 澶�",
- "绂昏亴宸ヤ綔浜ゆ帴",
- "璇曠敤鏈熻浆姝g敵璇�",
- "涓汉鍘熷洜绂昏亴",
- "璋冭嚦閿�鍞儴",
- "瀹㈡埛鎺ュ緟椁愯垂",
- "鐥呭亣 1 澶�",
- "鑺傚亣鏃ュ�肩彮鍔犵彮",
- "鍖椾含鍩硅宸梾",
- "椤圭洰鏂囨。浜ゆ帴",
- "鐮斿彂宀楄浆姝�",
- "閫氳璐规姤閿�",
- "浜嬪亣鍗婂ぉ",
- "璋冨矖鑷冲競鍦洪儴",
- "鍗忓晢绂昏亴",
- "宸ヤ綔鏃ュ欢鏃跺姞鐝�",
- "鎴愰兘灞曚細宸梾",
- "浜ら�氳垂鎶ラ攢",
- "璋冧紤 1 澶�",
- ];
- const statuses = ["pending", "pending", "pending", "approved", "pending", "pending", "rejected", "pending"];
- return types.map((approvalType, i) => {
- const ap = applicants[i % applicants.length];
- const daysAgo = i % 14;
- return demoRow({
- id: `mock_${i + 1}`,
- bizId: `BIZ${String(2025031400 + i)}`,
- applicantNo: ap.no,
- applicantName: ap.name,
- approvalType,
- approvalMode: i % 5 === 0 ? "or_sign" : "parallel",
- unread: i % 3 === 0,
- approvalStatus: statuses[i % statuses.length],
- createTime: dayjs().subtract(daysAgo, "day").hour(9 + (i % 8)).minute((i * 7) % 60).second(0).format("YYYY-MM-DD HH:mm:ss"),
- summary: summaries[i],
- formPayload: { summary: summaries[i] },
- });
- });
-}
-
-export function loadStoredRows() {
- 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 saveStoredRows(rows) {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(rows));
- } catch {
- /* ignore quota */
- }
-}
-
-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, flowNodesOverride) {
+ const tpl = templateOverride || null;
+ const payload = tpl?.fields?.length ? buildFormPayloadFromFields(tpl.fields) : { summary: "" };
+ const normalized = normalizeFlowNodes(flowNodesOverride);
+ const flowNodes = normalized.length
+ ? JSON.parse(JSON.stringify(normalized))
+ : [createEmptyNode(1)];
return {
templateKey: templateKey || "",
- approvalMode: tpl?.approvalMode || "parallel",
+ templateId: tpl?.templateId || "",
+ templateName: tpl?.label || "",
+ instanceId: "",
+ instanceNo: "",
+ statusRaw: "",
+ currentLevel: 1,
+ applicantId: null,
+ applicantName: "",
+ templateSnapshot: templateOverride || null,
+ formFieldDefs: tpl?.fields || [],
formPayload: payload,
- approvalFlowNodes: buildDefaultFlowNodes(),
+ flowNodes,
};
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
index 19328af..f54c167 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -1,92 +1,83 @@
-<!-- 缁熶竴瀹℃壒锛氫笟鍔℃憳瑕� -->
+<!-- 瀹℃壒璇︽儏锛氬熀纭�淇℃伅 + 濉姤鍐呭 -->
<template>
- <el-descriptions :column="2" border>
- <el-descriptions-item label="涓氬姟鍗曞彿">{{ row.bizId || row.id || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="瀹℃壒鐘舵��">
- <el-tag :type="approvalStatusTagType(row.approvalStatus)" size="small" effect="plain">
- {{ approvalStatusLabel(row.approvalStatus) }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="瀹℃壒绫诲瀷">
- <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)">
- {{ approvalTypeLabel(row.approvalType) }}
- </span>
- </el-descriptions-item>
- <el-descriptions-item label="瀹℃壒鏂瑰紡">
- <span class="approval-method-text">{{ approvalModeLabel(row.approvalMode) }}</span>
- </el-descriptions-item>
- <el-descriptions-item label="鐢宠浜虹紪鍙�">{{ row.applicantNo || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="鐢宠浜哄悕绉�">{{ row.applicantName || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item label="鐢宠鎽樿" :span="2">{{ row.summary || "鈥�" }}</el-descriptions-item>
- <el-descriptions-item v-if="row.rejectReason" label="椹冲洖鍘熷洜" :span="2">
- <span class="reject-text">{{ row.rejectReason }}</span>
- </el-descriptions-item>
- <el-descriptions-item label="鍒涘缓鏃堕棿" :span="2">{{ row.createTime || "鈥�" }}</el-descriptions-item>
- </el-descriptions>
+ <div class="approve-detail-panel">
+ <div class="detail-block">
+ <div class="detail-block-title">鍩烘湰淇℃伅</div>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="涓氬姟鍗曞彿">{{ row.bizId || row.id || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒鐘舵��">
+ <el-tag :type="approvalStatusTagType(row.approvalStatus)" size="small" effect="plain">
+ {{ approvalStatusLabel(row.approvalStatus) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒绫诲瀷">
+ <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)">
+ {{ approvalTypeLabel(row.approvalType) }}
+ </span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜虹紪鍙�">{{ row.applicantNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜哄悕绉�">{{ row.applicantName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鎽樿">{{ row.summary || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item v-if="row.rejectReason" label="椹冲洖鍘熷洜" :span="2">
+ <span class="reject-text">{{ row.rejectReason }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿" :span="2">
+ {{ formatDisplayTime(row.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </div>
- <template v-if="extraFields.length">
- <el-divider content-position="left">濉姤鍐呭</el-divider>
- <el-descriptions :column="2" border size="small">
- <el-descriptions-item v-for="item in extraFields" :key="item.key" :label="item.label">
- {{ item.display }}
- </el-descriptions-item>
- </el-descriptions>
- </template>
+ <div class="detail-block">
+ <div class="detail-block-title">濉姤鍐呭</div>
+ <FormPayloadFields
+ :fields="formResolved.fields"
+ :form-payload="formResolved.formPayload"
+ readonly
+ />
+ </div>
+ </div>
</template>
<script setup>
import { computed } from "vue";
+import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js";
import {
approvalTypeLabel,
approvalTypeStyle,
- approvalModeLabel,
approvalStatusLabel,
approvalStatusTagType,
- SUBMIT_TEMPLATES,
+ resolveInstanceFormFields,
} from "../approveListConstants.js";
+import FormPayloadFields from "./FormPayloadFields.vue";
const props = defineProps({
row: { type: Object, default: () => ({}) },
});
-const extraFields = computed(() => {
- const payload = props.row?.formPayload || {};
- const tpl = Object.values(SUBMIT_TEMPLATES).find((t) => t.approvalType === props.row?.approvalType);
- if (!tpl?.fields?.length) {
- return Object.keys(payload)
- .filter((k) => k !== "summary" && payload[k] != null && payload[k] !== "")
- .map((k) => ({ key: k, label: k, display: formatValue(payload[k]) }));
- }
- return tpl.fields
- .map((f) => {
- const val = payload[f.key];
- if (val == null || val === "" || (Array.isArray(val) && !val.length)) return null;
- let display = formatValue(val);
- if (f.type === "select" && f.options) {
- display = f.options.find((o) => o.value === val)?.label || display;
- }
- return { key: f.key, label: f.label, display };
- })
- .filter(Boolean);
-});
-
-function formatValue(val) {
- if (Array.isArray(val)) return val.join(" 鑷� ");
- return String(val);
-}
+const formResolved = computed(() => resolveInstanceFormFields(props.row));
</script>
<style scoped>
+.approve-detail-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+.detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+}
.approve-type-cell {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 13px;
line-height: 1.5;
-}
-.approval-method-text {
- color: var(--el-color-danger);
- font-weight: 500;
}
.reject-text {
color: var(--el-color-danger);
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
new file mode 100644
index 0000000..6cdc627
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
@@ -0,0 +1,116 @@
+<!-- 濉姤椤癸細缂栬緫涓鸿〃鍗曟帶浠讹紝璇︽儏涓� descriptions 琛ㄦ牸锛堜笌涓婃柟鍩虹淇℃伅涓�鑷达級 -->
+<template>
+ <template v-if="fields?.length">
+ <el-descriptions
+ v-if="readonly"
+ :column="2"
+ border
+ class="form-payload-desc"
+ >
+ <el-descriptions-item
+ v-for="field in fields"
+ :key="field.key"
+ :label="field.label"
+ :span="field.type === 'textarea' || field.type === 'datetimerange' ? 2 : 1"
+ >
+ <span class="field-value">{{ displayValue(field) }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <el-form v-else label-width="120px" class="form-payload-edit">
+ <el-form-item
+ v-for="field in fields"
+ :key="field.key"
+ :label="field.label"
+ :prop="`formPayload.${field.key}`"
+ >
+ <el-input
+ v-if="field.type === 'text'"
+ v-model="formPayload[field.key]"
+ :placeholder="`璇疯緭鍏�${field.label}`"
+ maxlength="200"
+ />
+ <el-input
+ v-else-if="field.type === 'textarea'"
+ v-model="formPayload[field.key]"
+ type="textarea"
+ :rows="field.rows || 3"
+ :placeholder="`璇峰~鍐�${field.label}`"
+ maxlength="2000"
+ show-word-limit
+ />
+ <el-input-number
+ v-else-if="field.type === 'number'"
+ v-model="formPayload[field.key]"
+ :min="field.min ?? 0"
+ :precision="field.precision ?? 0"
+ controls-position="right"
+ style="width: 100%"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'date'"
+ v-model="formPayload[field.key]"
+ type="date"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'datetimerange'"
+ v-model="formPayload[field.key]"
+ 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%"
+ />
+ <el-select
+ v-else-if="field.type === 'select'"
+ v-model="formPayload[field.key]"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ style="width: 100%"
+ clearable
+ >
+ <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" />
+ </el-select>
+ <span v-else class="field-value">{{ displayValue(field) }}</span>
+ </el-form-item>
+ </el-form>
+ </template>
+ <el-empty v-else description="鏆傛棤濉姤椤�" :image-size="48" />
+</template>
+
+<script setup>
+import { formatFieldDisplayValue } from "../approveListConstants.js";
+
+const props = defineProps({
+ fields: { type: Array, default: () => [] },
+ formPayload: { type: Object, default: () => ({}) },
+ readonly: { type: Boolean, default: false },
+});
+
+function displayValue(field) {
+ return formatFieldDisplayValue(field, props.formPayload?.[field.key]);
+}
+</script>
+
+<style scoped>
+.form-payload-desc {
+ width: 100%;
+}
+.form-payload-desc :deep(.el-descriptions__label) {
+ width: 120px;
+ font-weight: 500;
+}
+.field-value {
+ color: var(--el-text-color-primary);
+ line-height: 1.6;
+ word-break: break-word;
+}
+.form-payload-edit {
+ width: 100%;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
new file mode 100644
index 0000000..e5f2eef
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
@@ -0,0 +1,147 @@
+<!-- 瀹℃壒瀹炰緥锛歵asks 瀹℃壒娴佺▼灞曠ず锛堟í鍚戞楠ゆ潯锛� -->
+<template>
+ <div v-if="displayNodes.length" class="flow-track">
+ <div
+ v-for="(node, index) in displayNodes"
+ :key="index"
+ class="flow-step"
+ :class="{ 'is-last': index === displayNodes.length - 1 }"
+ >
+ <div class="flow-step-card">
+ <div class="flow-step-badge">{{ index + 1 }}</div>
+ <div class="flow-step-main">
+ <div class="flow-step-head">
+ <span class="flow-step-name">鑺傜偣 {{ index + 1 }}</span>
+ <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'" effect="plain">
+ {{ nodeSignModeLabel(node.signMode) }}
+ </el-tag>
+ </div>
+ <div class="flow-approvers">
+ <div
+ v-for="a in node.approvers"
+ :key="String(a.approverId ?? a.id)"
+ class="flow-approver"
+ >
+ <span class="flow-approver-name">{{ a.approverName || "鈥�" }}</span>
+ <el-tag
+ v-if="a.status"
+ size="small"
+ :type="mapTaskStatusTagType(a.status)"
+ effect="plain"
+ >
+ {{ mapTaskStatusLabel(a.status) }}
+ </el-tag>
+ </div>
+ <span v-if="!node.approvers?.length" class="flow-empty">鏈厤缃鎵逛汉</span>
+ </div>
+ </div>
+ </div>
+ <div v-if="index < displayNodes.length - 1" class="flow-connector" aria-hidden="true">
+ <el-icon><ArrowRight /></el-icon>
+ </div>
+ </div>
+ </div>
+ <el-empty v-else description="鏆傛棤娴佺▼鑺傜偣" :image-size="48" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { ArrowRight } from "@element-plus/icons-vue";
+import { nodeSignModeLabel } from "../../approve-template/approveTemplateConstants.js";
+import {
+ mapTaskStatusLabel,
+ mapTaskStatusTagType,
+ mapTasksToFlowNodes,
+} from "../approveListConstants.js";
+
+const props = defineProps({
+ tasks: { type: Array, default: () => [] },
+ nodes: { type: Array, default: () => [] },
+});
+
+const displayNodes = computed(() => {
+ if (props.tasks?.length) return mapTasksToFlowNodes(props.tasks);
+ return props.nodes || [];
+});
+</script>
+
+<style scoped>
+.flow-track {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ overflow-x: auto;
+ padding: 4px 2px 8px;
+}
+.flow-step {
+ display: flex;
+ align-items: center;
+ flex: 0 0 auto;
+}
+.flow-step-card {
+ display: flex;
+ gap: 12px;
+ min-width: 200px;
+ max-width: 260px;
+ padding: 14px;
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: 8px;
+ background: var(--el-bg-color);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+}
+.flow-step-badge {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: var(--el-color-primary-light-9);
+ color: var(--el-color-primary);
+ font-size: 13px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.flow-step-main {
+ flex: 1;
+ min-width: 0;
+}
+.flow-step-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+.flow-step-name {
+ font-weight: 600;
+ font-size: 13px;
+ color: var(--el-text-color-primary);
+}
+.flow-approvers {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.flow-approver {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+}
+.flow-approver-name {
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+}
+.flow-empty {
+ font-size: 12px;
+ color: var(--el-text-color-placeholder);
+}
+.flow-connector {
+ display: flex;
+ align-items: center;
+ padding: 0 6px;
+ color: var(--el-text-color-placeholder);
+ font-size: 16px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index 774b322..cdae763 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -63,119 +63,85 @@
{{ approvalTypeLabel(row.approvalType) }}
</span>
</template>
- <template #approvalMethod="{ row }">
- <span class="approval-method-text">{{ approvalModeLabel(row.approvalMode) }}</span>
- </template>
</PIMTable>
</div>
<!-- 鎻愪氦瀹℃壒锛堟寜妯℃澘锛� -->
<el-dialog
v-model="submitDialog.visible"
- :title="submitDialog.step === 1 ? '閫夋嫨瀹℃壒妯℃澘' : `鎻愪氦${activeTemplate?.label || '瀹℃壒'}`"
+ :title="submitDialogTitle"
width="720px"
append-to-body
destroy-on-close
class="approve-submit-dialog"
- @closed="submitDialog.step = 1"
+ @closed="resetSubmitDialogState"
>
- <template v-if="submitDialog.step === 1">
- <p class="template-hint">璇烽�夋嫨瑕佹彁浜ょ殑瀹℃壒绫诲瀷锛岀郴缁熷皢鎸夊搴旀ā鏉垮紩瀵煎~鎶ワ紙瀛楁鍚庢湡涓庡悗绔悓姝ワ級銆�</p>
- <div class="template-grid">
+ <template v-if="submitDialog.step === 1 && !isSubmitEdit">
+ <p class="template-hint">璇烽�夋嫨宸插惎鐢ㄧ殑瀹℃壒妯℃澘锛岀郴缁熷皢鎸夋ā鏉块厤缃紩瀵煎~鎶ャ��</p>
+ <div v-loading="submitTemplatesLoading" class="template-grid">
<div
- v-for="(tpl, key) in SUBMIT_TEMPLATES"
- :key="key"
+ v-for="card in submitTemplateCards"
+ :key="card.key"
class="template-card"
- @click="onTemplatePick(key)"
+ @click="onTemplatePick(card)"
>
- <span class="template-card-type" :style="approvalTypeStyle(tpl.approvalType)">
- {{ tpl.label }}
+ <span class="template-card-type" :style="approvalTypeStyle(card.approvalType)">
+ {{ card.label }}
</span>
- <span class="template-card-desc">{{ tpl.summaryPlaceholder || "鐐瑰嚮濉啓骞舵彁浜�" }}</span>
+ <span class="template-card-desc">{{ card.summaryPlaceholder }}</span>
</div>
+ <el-empty
+ v-if="!submitTemplatesLoading && !submitTemplateCards.length"
+ description="鏆傛棤鍙敤瀹℃壒妯℃澘"
+ :image-size="80"
+ class="template-empty"
+ />
</div>
</template>
<template v-else>
+ <div v-loading="submitTemplatesLoading && !isSubmitEdit">
<el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px">
<el-form-item label="瀹℃壒绫诲瀷">
<span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)">
{{ activeTemplate.label }}
</span>
- <el-button type="primary" link class="ml12" @click="backToTemplatePick">鏇存崲妯℃澘</el-button>
+ <el-button
+ v-if="!isSubmitEdit"
+ type="primary"
+ link
+ class="ml12"
+ @click="backToTemplatePick"
+ >
+ 鏇存崲妯℃澘
+ </el-button>
</el-form-item>
- <el-form-item label="瀹℃壒鏂瑰紡" prop="approvalMode">
- <el-radio-group v-model="submitForm.approvalMode">
- <el-radio value="parallel">涓庣</el-radio>
- <el-radio value="or_sign">鎴栫</el-radio>
- </el-radio-group>
- </el-form-item>
- <template v-for="field in activeTemplate.fields" :key="field.key">
- <el-form-item :label="field.label" :prop="`formPayload.${field.key}`">
- <el-input
- v-if="field.type === 'text'"
- v-model="submitForm.formPayload[field.key]"
- :placeholder="`璇疯緭鍏�${field.label}`"
- maxlength="200"
- />
- <el-input
- v-else-if="field.type === 'textarea'"
- v-model="submitForm.formPayload[field.key]"
- type="textarea"
- :rows="field.rows || 3"
- :placeholder="`璇峰~鍐�${field.label}`"
- maxlength="2000"
- show-word-limit
- />
- <el-input-number
- v-else-if="field.type === 'number'"
- v-model="submitForm.formPayload[field.key]"
- :min="field.min ?? 0"
- :precision="field.precision ?? 0"
- controls-position="right"
- style="width: 100%"
- />
- <el-date-picker
- v-else-if="field.type === 'date'"
- v-model="submitForm.formPayload[field.key]"
- type="date"
- :placeholder="`璇烽�夋嫨${field.label}`"
- format="YYYY-MM-DD"
- value-format="YYYY-MM-DD"
- style="width: 100%"
- />
- <el-date-picker
- v-else-if="field.type === 'datetimerange'"
- v-model="submitForm.formPayload[field.key]"
- 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%"
- />
- <el-select
- v-else-if="field.type === 'select'"
- v-model="submitForm.formPayload[field.key]"
- :placeholder="`璇烽�夋嫨${field.label}`"
- style="width: 100%"
- clearable
- >
- <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" />
- </el-select>
- </el-form-item>
- </template>
- <el-form-item label="瀹℃壒娴佺▼">
- <ApprovalFlowEditor v-model="submitForm.approvalFlowNodes" :user-options="flowUserOptions" />
- <p class="flow-tip">鑷冲皯淇濈暀涓�涓鎵硅妭鐐癸紱鎻愪氦鍚庤繘鍏ャ�屽鏍镐腑銆嶇姸鎬併��</p>
+ <FormPayloadFields
+ :fields="submitFormFields"
+ :form-payload="submitForm.formPayload"
+ />
+ <el-form-item label="瀹℃壒娴佺▼" required>
+ <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
+ <p class="flow-tip">
+ 鎸夐『搴忔祦杞細鍙负姣忎釜鑺傜偣娣诲姞澶氬悕瀹℃壒浜猴紱浼氱闇�鍏ㄩ儴閫氳繃锛屾垨绛句换涓�浜洪�氳繃鍗冲彲杩涘叆涓嬩竴鑺傜偣銆�
+ </p>
</el-form-item>
</el-form>
+ </div>
</template>
<template #footer>
- <el-button v-if="submitDialog.step === 2" type="primary" @click="onSubmitNew">鎻� 浜�</el-button>
- <el-button @click="submitDialog.visible = false">{{ submitDialog.step === 1 ? "鍙� 娑�" : "鍏� 闂�" }}</el-button>
+ <el-button
+ v-if="submitDialog.step === 2 || isSubmitEdit"
+ type="primary"
+ :loading="submitSaving"
+ @click="onSubmitInstance"
+ >
+ {{ isSubmitEdit ? "淇� 瀛�" : "鎻� 浜�" }}
+ </el-button>
+ <el-button @click="submitDialog.visible = false">
+ {{ submitDialog.step === 1 && !isSubmitEdit ? "鍙� 娑�" : "鍏� 闂�" }}
+ </el-button>
</template>
</el-dialog>
@@ -186,28 +152,51 @@
width="920px"
append-to-body
destroy-on-close
+ class="approve-detail-dialog"
>
- <ApproveDetailPanel :row="detailRow" />
- <el-divider content-position="left">瀹℃壒娴佺▼</el-divider>
- <ApprovalFlowProgress
- :nodes="detailRow.approvalFlowNodes"
- :current-index="detailRow.currentNodeIndex ?? 0"
- />
- <el-divider content-position="left">瀹℃壒璁板綍</el-divider>
- <el-timeline v-if="detailRow.approvalRecords?.length">
- <el-timeline-item
- v-for="(rec, i) in detailRow.approvalRecords"
- :key="i"
- :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
- :timestamp="rec.time"
- >
- {{ rec.operatorName }} 鈥� {{ approvalActionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
- </el-timeline-item>
- </el-timeline>
- <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+ <div class="approve-detail-body">
+ <ApproveDetailPanel :row="detailRow" />
+ <div class="detail-block">
+ <div class="detail-block-title">
+ 瀹℃壒娴佺▼锛坽{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} 椤癸級
+ </div>
+ <InstanceFlowDisplay :tasks="detailRow.tasks" :nodes="detailRow.flowNodes" />
+ </div>
+ <div class="detail-block">
+ <div class="detail-block-title">瀹℃壒璁板綍</div>
+ <el-timeline v-if="detailRow.approvalRecords?.length" class="approve-record-timeline">
+ <el-timeline-item
+ v-for="(rec, i) in detailRow.approvalRecords"
+ :key="rec.id ?? i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="formatRecordTime(rec.time)"
+ placement="top"
+ >
+ <div class="record-item">
+ <span class="record-operator">{{ rec.operatorName || "鈥�" }}</span>
+ <el-tag
+ size="small"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'info'"
+ effect="plain"
+ >
+ {{ approvalActionLabel(rec.result) }}
+ </el-tag>
+ <p class="record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</p>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="48" />
+ </div>
+ </div>
<template #footer>
<el-button
v-if="detailRow.approvalStatus === 'pending'"
+ @click="openEditFromDetail"
+ >
+ 淇� 鏀�
+ </el-button>
+ <el-button
+ v-if="detailRow.approvalStatus === 'pending' && detailRow.isApprove"
type="primary"
@click="openApproveFromDetail"
>
@@ -227,11 +216,12 @@
@closed="approveOpinion = ''"
>
<ApproveDetailPanel :row="approveDialog.row" />
- <el-divider content-position="left">娴佺▼杩涘害</el-divider>
- <ApprovalFlowProgress
- :nodes="approveDialog.row?.approvalFlowNodes"
- :current-index="approveDialog.row?.currentNodeIndex ?? 0"
- />
+ <div class="detail-block mt16">
+ <div class="detail-block-title">
+ 瀹℃壒娴佺▼锛坽{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} 椤癸級
+ </div>
+ <InstanceFlowDisplay :tasks="approveDialog.row?.tasks" :nodes="approveDialog.row?.flowNodes" />
+ </div>
<el-form label-width="100px" class="mt16">
<el-form-item label="瀹℃壒鎰忚" required>
<el-input
@@ -245,9 +235,23 @@
</el-form-item>
</el-form>
<template #footer>
- <el-button type="success" @click="onApprove('approved')">閫� 杩�</el-button>
- <el-button type="danger" @click="onApprove('rejected')">椹� 鍥�</el-button>
- <el-button @click="approveDialog.visible = false">鍙� 娑�</el-button>
+ <el-button
+ type="success"
+ :loading="approveSubmitting"
+ @click="onApprove('approved')"
+ >
+ 閫� 杩�
+ </el-button>
+ <el-button
+ type="danger"
+ :loading="approveSubmitting"
+ @click="onApprove('rejected')"
+ >
+ 椹� 鍥�
+ </el-button>
+ <el-button :disabled="approveSubmitting" @click="approveDialog.visible = false">
+ 鍙� 娑�
+ </el-button>
</template>
</el-dialog>
</div>
@@ -258,19 +262,21 @@
import { ElMessage } from "element-plus";
import { onMounted, ref } from "vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
-import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue";
-import ApprovalFlowProgress from "@/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue";
+import TemplateFlowEditor from "../approve-template/components/TemplateFlowEditor.vue";
+import FormPayloadFields from "./components/FormPayloadFields.vue";
+import { formatDisplayTime } from "../approve-template/approveTemplateConstants.js";
import { approvalTypeStyle } from "./approveListConstants.js";
import ApproveDetailPanel from "./components/ApproveDetailPanel.vue";
+import InstanceFlowDisplay from "./components/InstanceFlowDisplay.vue";
import { useApproveList } from "./useApproveList.js";
const al = useApproveList();
const {
Search,
APPROVAL_TYPE_OPTIONS,
- SUBMIT_TEMPLATES,
+ submitTemplateCards,
+ submitTemplatesLoading,
approvalTypeLabel,
- approvalModeLabel,
approvalActionLabel,
searchForm,
tableLoading,
@@ -281,18 +287,25 @@
detailRow,
approveDialog,
approveOpinion,
+ approveSubmitting,
submitDialog,
+ isSubmitEdit,
+ submitDialogTitle,
submitForm,
submitFormRef,
+ submitSaving,
activeTemplate,
+ submitFormFields,
submitFormRules,
handleQuery,
resetSearch,
pagination,
+ resetSubmitDialogState,
openSubmitDialog,
+ openEditDialog,
onTemplatePick,
backToTemplatePick,
- submitNewApproval,
+ submitInstanceForm,
submitApprove,
openDetail,
openApprove,
@@ -322,13 +335,13 @@
}
}
-async function onSubmitNew() {
- const ok = await submitNewApproval();
- if (ok) ElMessage.success("瀹℃壒宸叉彁浜�");
+async function onSubmitInstance() {
+ const ok = await submitInstanceForm();
+ if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "瀹℃壒宸叉彁浜�");
}
-function onApprove(result) {
- const ret = submitApprove(result);
+async function onApprove(result) {
+ const ret = await submitApprove(result);
if (ret?.needOpinion) {
ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
return;
@@ -338,10 +351,20 @@
}
}
+function formatRecordTime(time) {
+ return formatDisplayTime(time) || "鈥�";
+}
+
function openApproveFromDetail() {
const row = detailRow.value;
detailDialog.visible = false;
openApprove(row);
+}
+
+function openEditFromDetail() {
+ const row = detailRow.value;
+ detailDialog.visible = false;
+ openEditDialog(row);
}
onMounted(() => {
@@ -385,10 +408,6 @@
font-size: 13px;
line-height: 1.5;
}
-.approval-method-text {
- color: var(--el-color-danger);
- font-weight: 500;
-}
.template-hint {
font-size: 13px;
color: var(--el-text-color-secondary);
@@ -398,6 +417,10 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
+ min-height: 120px;
+}
+.template-empty {
+ grid-column: 1 / -1;
}
.template-card {
padding: 14px 16px;
@@ -433,4 +456,47 @@
.approve-submit-dialog :deep(.el-dialog__body) {
padding-top: 12px;
}
+.approve-detail-dialog :deep(.el-dialog__body) {
+ padding-top: 16px;
+ max-height: 70vh;
+ overflow-y: auto;
+}
+.approve-detail-body .detail-block {
+ margin-top: 20px;
+}
+.approve-detail-body .detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+}
+.approve-record-timeline {
+ padding-left: 4px;
+}
+.record-item {
+ padding: 4px 0 2px;
+}
+.record-operator {
+ font-weight: 600;
+ margin-right: 8px;
+ color: var(--el-text-color-primary);
+}
+.record-opinion {
+ margin: 8px 0 0;
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+ line-height: 1.5;
+}
+.detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+}
</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index 48103aa..337b00d 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -1,56 +1,52 @@
-import { Search } from "@element-plus/icons-vue";
-import dayjs from "dayjs";
+import {
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ TEMPLATE_TYPE_BUILTIN,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import {
+ approveApprovalInstance,
+ deleteApprovalInstance,
+ listApprovalInstancePage,
+ saveApprovalInstance,
+ updateApprovalInstance,
+} from "@/api/officeProcessAutomation/approvalInstance.js";
import useUserStore from "@/store/modules/user";
-import { computed, reactive, ref, watch } from "vue";
+import { Search } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref } from "vue";
+import {
+ formatDisplayTime,
+ mapEnabledFromApi,
+ mapTemplateFromApi,
+ unwrapTemplateDetail,
+ unwrapTemplateList,
+} from "../approve-template/approveTemplateConstants.js";
+import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
import {
APPROVAL_TYPE_OPTIONS,
- SUBMIT_TEMPLATES,
- approvalModeLabel,
approvalStatusLabel,
approvalStatusTagType,
approvalTypeLabel,
+ buildApprovalInstanceListParams,
+ buildApproveInstanceDto,
+ buildEditFormFromInstanceRow,
+ buildInstanceDto,
+ clearLegacyApproveListStorage,
createEmptySubmitForm,
- createInitialMockRows,
- loadStoredRows,
- saveStoredRows,
- buildDefaultFlowNodes,
+ mapInstanceFromApi,
+ mapSubmitTemplateCard,
+ validateSubmitFlowNodes,
+ unwrapInstancePage,
} from "./approveListConstants.js";
-function advanceFlow(row, result, opinion) {
- const nodes = row.approvalFlowNodes || [];
- const idx = row.currentNodeIndex ?? 0;
- const node = nodes[idx];
- if (!node) return;
- node.nodeStatus = result === "approved" ? "finish" : "error";
- node.approveOpinion = opinion || (result === "approved" ? "鍚屾剰" : "椹冲洖");
- node.approveTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
- row.approvalRecords = row.approvalRecords || [];
- row.approvalRecords.push({
- operatorName: node.approverName || "瀹℃壒浜�",
- result,
- opinion: node.approveOpinion,
- time: node.approveTime,
- });
- if (result === "rejected") {
- row.approvalStatus = "rejected";
- row.rejectReason = opinion || node.approveOpinion;
- return;
- }
- const next = idx + 1;
- if (next < nodes.length) {
- row.currentNodeIndex = next;
- nodes[next].nodeStatus = "process";
- row.approvalStatus = "pending";
- } else {
- row.approvalStatus = "approved";
- row.rejectReason = "";
- }
-}
-
export function useApproveList() {
+ clearLegacyApproveListStorage();
const userStore = useUserStore();
- const stored = loadStoredRows();
- const allRows = ref(stored?.length ? stored : createInitialMockRows());
+
+ const tableData = ref([]);
+ const submitTemplateCards = ref([]);
+ const submitTemplatesLoading = ref(false);
const searchForm = reactive({
approvalType: "",
@@ -66,57 +62,37 @@
const approveDialog = reactive({ visible: false, row: null });
const approveOpinion = ref("");
+ const approveSubmitting = ref(false);
- const submitDialog = reactive({ visible: false, step: 1 });
+ const submitDialog = reactive({ visible: false, step: 1, mode: "add" });
+ const submitEditRow = ref(null);
const submitForm = reactive(createEmptySubmitForm(""));
const submitFormRef = ref();
+ const submitSaving = ref(false);
- const filteredList = computed(() => {
- let list = [...allRows.value];
- if (searchForm.approvalType) {
- list = list.filter((r) => r.approvalType === searchForm.approvalType);
+ const isSubmitEdit = computed(() => submitDialog.mode === "edit");
+ const submitDialogTitle = computed(() => {
+ if (submitDialog.mode === "edit") {
+ return `淇敼${activeTemplate.value?.label || submitForm.templateName || "瀹℃壒"}`;
}
- const kw = (searchForm.applicantKeyword || "").trim().toLowerCase();
- if (kw) {
- list = list.filter((r) => {
- const name = (r.applicantName || "").toLowerCase();
- const no = (r.applicantNo || "").toLowerCase();
- return name.includes(kw) || no.includes(kw);
- });
- }
- const range = searchForm.createTimeRange;
- if (range?.length === 2) {
- const [from, to] = range;
- list = list.filter((r) => {
- const t = (r.createTime || "").slice(0, 10);
- return t && t >= from && t <= to;
- });
- }
- return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
+ if (submitDialog.step === 1) return "閫夋嫨瀹℃壒妯℃澘";
+ return `鎻愪氦${activeTemplate.value?.label || "瀹℃壒"}`;
});
- 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 activeTemplate = computed(() => submitForm.templateSnapshot || null);
- const tableData = computed(() => {
- const start = (page.current - 1) * page.size;
- return filteredList.value.slice(start, start + page.size);
+ /** 濉姤椤瑰畾涔夛紙鏂板/淇敼涓� formConfig 涓�鑷达級 */
+ const submitFormFields = computed(() => {
+ const tplFields = activeTemplate.value?.fields;
+ if (tplFields?.length) return tplFields;
+ return submitForm.formFieldDefs || [];
});
-
- const activeTemplate = computed(() => SUBMIT_TEMPLATES[submitForm.templateKey] || null);
const submitFormRules = computed(() => {
const rules = {
templateKey: [{ required: true, message: "璇烽�夋嫨瀹℃壒绫诲瀷", trigger: "change" }],
};
- (activeTemplate.value?.fields || []).forEach((f) => {
+ submitFormFields.value.forEach((f) => {
if (!f.required) return;
if (f.type === "number") {
rules[`formPayload.${f.key}`] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
@@ -132,6 +108,7 @@
const tableColumn = ref([
{ label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
{ label: "鐢宠浜哄悕绉�", prop: "applicantName", minWidth: 100 },
+ { label: "涓氬姟绫诲瀷", prop: "businessName", minWidth: 120 },
{
label: "瀹℃壒绫诲瀷",
prop: "approvalType",
@@ -140,14 +117,7 @@
slot: "approveType",
},
{
- label: "瀹℃壒鏂瑰紡",
- prop: "approvalMode",
- width: 90,
- dataType: "slot",
- slot: "approvalMethod",
- },
- {
- label: "鏄惁鏈",
+ label: "寰呮垜瀹℃壒",
prop: "unread",
width: 90,
align: "center",
@@ -161,35 +131,82 @@
formatData: (v) => approvalStatusLabel(v),
formatType: (v) => approvalStatusTagType(v),
},
- { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 170,
+ formatData: (v) => formatDisplayTime(v),
+ },
{
dataType: "action",
label: "鎿嶄綔",
align: "center",
fixed: "right",
- width: 160,
+ width: 240,
operation: [
{ name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
{
- name: "瀹℃壒",
+ name: "淇敼",
type: "text",
disabled: (row) => row.approvalStatus !== "pending",
+ clickFun: (row) => openEditDialog(row),
+ },
+ {
+ name: "瀹℃壒",
+ type: "text",
+ disabled: (row) => row.approvalStatus !== "pending" || !row.isApprove,
clickFun: (row) => openApprove(row),
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ clickFun: (row) => removeInstance(row),
},
],
},
]);
- function persist() {
- saveStoredRows(allRows.value);
+ async function fetchApprovalList() {
+ tableLoading.value = true;
+ try {
+ const res = await listApprovalInstancePage(
+ buildApprovalInstanceListParams({ page, searchForm })
+ );
+ const { records, total } = unwrapInstancePage(res);
+ tableData.value = records.map(mapInstanceFromApi);
+ page.total = total;
+ } catch {
+ tableData.value = [];
+ page.total = 0;
+ ElMessage.error("瀹℃壒鍒楄〃鍔犺浇澶辫触");
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ async function loadSubmitTemplates() {
+ submitTemplatesLoading.value = true;
+ try {
+ const [builtinRes, customRes] = await Promise.all([
+ listApprovalTemplate(TEMPLATE_TYPE_BUILTIN),
+ listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
+ ]);
+ const merged = [
+ ...unwrapTemplateList(builtinRes),
+ ...unwrapTemplateList(customRes),
+ ].filter((row) => mapEnabledFromApi(row.enabled));
+ submitTemplateCards.value = merged.map(mapSubmitTemplateCard);
+ } catch {
+ submitTemplateCards.value = [];
+ ElMessage.error("鍔犺浇瀹℃壒妯℃澘澶辫触");
+ } finally {
+ submitTemplatesLoading.value = false;
+ }
}
function handleQuery() {
- tableLoading.value = true;
page.current = 1;
- setTimeout(() => {
- tableLoading.value = false;
- }, 200);
+ fetchApprovalList();
}
function resetSearch() {
@@ -202,43 +219,81 @@
function pagination({ page: p, limit }) {
page.current = p;
page.size = limit;
- }
-
- function markRead(row) {
- if (!row.unread) return;
- const hit = allRows.value.find((r) => r.id === row.id);
- if (hit) {
- hit.unread = false;
- persist();
- }
+ fetchApprovalList();
}
function openDetail(row) {
- markRead(row);
detailRow.value = { ...row };
detailDialog.visible = true;
}
function openApprove(row) {
- markRead(row);
approveDialog.row = { ...row };
approveOpinion.value = "";
approveDialog.visible = true;
}
- function openSubmitDialog() {
- Object.assign(submitForm, createEmptySubmitForm(""));
+ function resetSubmitDialogState() {
+ submitDialog.mode = "add";
submitDialog.step = 1;
+ submitEditRow.value = null;
+ Object.assign(submitForm, createEmptySubmitForm(""));
+ }
+
+ function openSubmitDialog() {
+ resetSubmitDialogState();
+ submitDialog.visible = true;
+ loadSubmitTemplates();
+ }
+
+ function openEditDialog(row) {
+ if (row?.approvalStatus !== "pending") {
+ ElMessage.warning("浠呭鏍镐腑鐨勫鎵瑰彲淇敼");
+ return;
+ }
+ if (!row?.id) {
+ ElMessage.warning("鏃犳硶淇敼锛氱己灏戝鎵瑰疄渚� ID");
+ return;
+ }
+ submitDialog.mode = "edit";
+ submitDialog.step = 2;
+ submitEditRow.value = { ...row };
+ Object.assign(submitForm, buildEditFormFromInstanceRow(row));
submitDialog.visible = true;
}
- function onTemplatePick(key) {
- Object.assign(submitForm, createEmptySubmitForm(key));
- submitDialog.step = 2;
+ async function onTemplatePick(card) {
+ if (!card?.id) return;
+ submitTemplatesLoading.value = true;
+ try {
+ const res = await getApprovalTemplateDetail(card.id);
+ const mapped = mapTemplateFromApi(unwrapTemplateDetail(res));
+ const tpl = {
+ ...buildSubmitTemplateFromRow(mapped),
+ templateId: mapped.id,
+ };
+ const base = createEmptySubmitForm(String(card.id), tpl, mapped.flowNodes);
+ Object.assign(submitForm, {
+ ...base,
+ templateName: mapped.templateName || tpl.label || "",
+ templateSnapshot: tpl,
+ formFieldDefs: tpl.fields || [],
+ });
+ submitDialog.step = 2;
+ } catch {
+ ElMessage.error("鍔犺浇妯℃澘璇︽儏澶辫触");
+ } finally {
+ submitTemplatesLoading.value = false;
+ }
}
function backToTemplatePick() {
submitDialog.step = 1;
+ }
+
+ async function submitInstanceForm() {
+ if (submitDialog.mode === "edit") return submitEditApproval();
+ return submitNewApproval();
}
async function submitNewApproval() {
@@ -248,56 +303,143 @@
} catch {
return false;
}
- const tpl = activeTemplate.value;
- if (!tpl) return false;
- const id = `user_${Date.now()}`;
- const summary =
- submitForm.formPayload.summary ||
- submitForm.formPayload.handoverTo ||
- `${tpl.label}鐢宠`;
- const row = {
- id,
- bizId: `BIZ${dayjs().format("YYYYMMDDHHmmss")}`,
- applicantNo: userStore.name || String(userStore.id || "褰撳墠鐢ㄦ埛"),
- applicantName: userStore.nickName || userStore.name || "褰撳墠鐢ㄦ埛",
- approvalType: tpl.approvalType,
- approvalMode: submitForm.approvalMode,
- unread: false,
- approvalStatus: "pending",
- createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
- summary,
- formPayload: { ...submitForm.formPayload },
- approvalFlowNodes: (submitForm.approvalFlowNodes?.length
- ? submitForm.approvalFlowNodes
- : buildDefaultFlowNodes()
- ).map((n, i) => ({ ...n, nodeStatus: i === 0 ? "process" : n.nodeStatus || "wait" })),
- currentNodeIndex: 0,
- approvalRecords: [],
- rejectReason: "",
- };
- allRows.value.unshift(row);
- persist();
- submitDialog.visible = false;
- page.current = 1;
- return true;
+ if (!activeTemplate.value) return false;
+ const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
+ if (!flowCheck.ok) {
+ ElMessage.warning(flowCheck.message);
+ return false;
+ }
+ if (!submitForm.templateId) {
+ ElMessage.warning("缂哄皯妯℃澘 ID锛屾棤娉曟彁浜�");
+ return false;
+ }
+ if (submitSaving.value) return false;
+ submitSaving.value = true;
+ try {
+ await saveApprovalInstance(
+ buildInstanceDto({
+ submitForm,
+ activeTemplate: activeTemplate.value,
+ userStore,
+ flowNodes: flowCheck.nodes,
+ })
+ );
+ submitDialog.visible = false;
+ page.current = 1;
+ await fetchApprovalList();
+ return true;
+ } catch {
+ return false;
+ } finally {
+ submitSaving.value = false;
+ }
}
- function submitApprove(result) {
+ async function submitEditApproval() {
+ if (!submitFormRef.value) return false;
+ try {
+ await submitFormRef.value.validate();
+ } catch {
+ return false;
+ }
+ if (!activeTemplate.value) return false;
+ const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
+ if (!flowCheck.ok) {
+ ElMessage.warning(flowCheck.message);
+ return false;
+ }
+ if (!submitForm.instanceId) {
+ ElMessage.warning("缂哄皯瀹℃壒瀹炰緥 ID锛屾棤娉曚繚瀛�");
+ return false;
+ }
+ if (submitSaving.value) return false;
+ submitSaving.value = true;
+ try {
+ await updateApprovalInstance(
+ buildInstanceDto({
+ submitForm,
+ activeTemplate: activeTemplate.value,
+ flowNodes: flowCheck.nodes,
+ existingRow: submitEditRow.value,
+ })
+ );
+ submitDialog.visible = false;
+ await fetchApprovalList();
+ if (detailDialog.visible && detailRow.value?.id === submitForm.instanceId) {
+ const hit = tableData.value.find((r) => r.id === submitForm.instanceId);
+ if (hit) detailRow.value = { ...hit };
+ else detailDialog.visible = false;
+ }
+ return true;
+ } catch {
+ return false;
+ } finally {
+ submitSaving.value = false;
+ }
+ }
+
+ async function removeInstance(row) {
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戝鎵瑰疄渚� ID");
+ return;
+ }
+ const title = row.title || row.templateName || row.instanceNo || "璇ュ鎵�";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゅ鎵广��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteApprovalInstance([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ if (detailDialog.visible && detailRow.value?.id === row.id) {
+ detailDialog.visible = false;
+ }
+ if (approveDialog.visible && approveDialog.row?.id === row.id) {
+ approveDialog.visible = false;
+ }
+ await fetchApprovalList();
+ } catch {
+ /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
+ }
+ }
+
+ async function submitApprove(result) {
const row = approveDialog.row;
- if (!row) return;
- const hit = allRows.value.find((r) => r.id === row.id);
- if (!hit || hit.approvalStatus !== "pending") return;
+ if (!row?.id) return { ok: false };
if (result === "rejected" && !(approveOpinion.value || "").trim()) {
return { needOpinion: true };
}
- advanceFlow(hit, result, (approveOpinion.value || "").trim());
- hit.unread = false;
- persist();
- approveDialog.visible = false;
- if (detailDialog.visible && detailRow.value?.id === hit.id) {
- detailRow.value = { ...hit };
+ if (approveSubmitting.value) return { ok: false };
+ approveSubmitting.value = true;
+ try {
+ await approveApprovalInstance(
+ buildApproveInstanceDto(row, result, approveOpinion.value)
+ );
+ approveDialog.visible = false;
+ await fetchApprovalList();
+ if (detailDialog.visible && detailRow.value?.id === row.id) {
+ const hit = tableData.value.find((r) => r.id === row.id);
+ if (hit) detailRow.value = { ...hit };
+ else detailDialog.visible = false;
+ }
+ return { ok: true, result };
+ } catch {
+ ElMessage.error("瀹℃壒鎿嶄綔澶辫触");
+ return { ok: false };
+ } finally {
+ approveSubmitting.value = false;
}
- return { ok: true };
}
function approvalActionLabel(result) {
@@ -309,9 +451,7 @@
return {
Search,
APPROVAL_TYPE_OPTIONS,
- SUBMIT_TEMPLATES,
approvalTypeLabel,
- approvalModeLabel,
approvalStatusLabel,
approvalStatusTagType,
approvalActionLabel,
@@ -324,20 +464,31 @@
detailRow,
approveDialog,
approveOpinion,
+ approveSubmitting,
submitDialog,
+ isSubmitEdit,
+ submitDialogTitle,
submitForm,
submitFormRef,
+ submitSaving,
activeTemplate,
+ submitFormFields,
submitFormRules,
+ submitTemplateCards,
+ submitTemplatesLoading,
handleQuery,
resetSearch,
pagination,
+ resetSubmitDialogState,
openSubmitDialog,
+ openEditDialog,
onTemplatePick,
backToTemplatePick,
+ submitInstanceForm,
submitNewApproval,
submitApprove,
openDetail,
openApprove,
+ fetchApprovalList,
};
}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
index 81884a1..3325e55 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -1,5 +1,23 @@
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) {
+ if (type == null || type === "") return "鈥�";
+ const n = Number(type);
+ return TEMPLATE_TYPE_OPTIONS.find((x) => x.value === n)?.label || "鈥�";
+}
/** 鑺傜偣鍐呭鎵规柟寮忥細浼氱 / 鎴栫 */
export const NODE_SIGN_MODE_OPTIONS = [
@@ -7,18 +25,205 @@
{ 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 != null ? Number(row.templateType) : undefined,
+ 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, templateType = TEMPLATE_TYPE_CUSTOM }) {
+ 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;
+ if (searchForm?.enabledOnly) params.enabled = "1";
+ return params;
}
export function nodeSignModeLabel(mode) {
@@ -42,6 +247,9 @@
id: "",
templateName: "",
description: "",
+ templateType: TEMPLATE_TYPE_CUSTOM,
+ formConfig: "",
+ formConfigData: createEmptyFormConfigData(),
enabled: true,
flowNodes: [createEmptyNode(1)],
};
@@ -50,11 +258,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 +284,8 @@
return { ok: false, message: `璇蜂负绗� ${i + 1} 涓妭鐐归�夋嫨鑷冲皯涓�鍚嶅鎵逛汉` };
}
}
+ const cfgCheck = validateFormConfigData(form.formConfigData);
+ if (!cfgCheck.ok) return cfgCheck;
return { ok: true, nodes, name };
}
@@ -83,78 +298,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