Merge branch 'dev-new_pro_OA' of http://114.132.189.42:9002/r/product-inventory-management into dev-new_pro_OA
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤å®¡æ¹å®ä¾ï¼body 为 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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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", |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢å®¡æ¹æ¨¡æ¿ï¼body 为 ApprovalTemplateDtoï¼ */ |
| | | export function addApprovalTemplate(approvalTemplateDto) { |
| | | return request({ |
| | | url: "/approvalTemplate/add", |
| | | method: "post", |
| | | data: approvalTemplateDto, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹å®¡æ¹æ¨¡æ¿ï¼body 为 ApprovalTemplateDtoï¼ */ |
| | | export function updateApprovalTemplate(approvalTemplateDto) { |
| | | return request({ |
| | | url: "/approvalTemplate/update", |
| | | method: "put", |
| | | data: approvalTemplateDto, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤å®¡æ¹æ¨¡æ¿ï¼body ä¸ºæ¨¡æ¿ 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, |
| | | }); |
| | | } |
| | |
| | | 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 = [ |
| | |
| | | { 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 è§£æå¡«æ¥å段å®ä¹ä¸ 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: "æè½¬æ£æ¥æ", 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"; |
| | | /** è§£æå®ä¾ 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) { |
| | |
| | | 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 天", |
| | | "离èå·¥ä½äº¤æ¥", |
| | | "è¯ç¨æè½¬æ£ç³è¯·", |
| | | "个人åå 离è", |
| | | "è°è³éå®é¨", |
| | | "å®¢æ·æ¥å¾
é¤è´¹", |
| | | "ç
å 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, |
| | | }; |
| | | } |
| | |
| | | <!-- ç»ä¸å®¡æ¹ï¼ä¸å¡æè¦ --> |
| | | <!-- 审æ¹è¯¦æ
ï¼åºç¡ä¿¡æ¯ + å¡«æ¥å
容 --> |
| | | <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); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å¡«æ¥é¡¹ï¼ç¼è¾ä¸ºè¡¨åæ§ä»¶ï¼è¯¦æ
为 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- 审æ¹å®ä¾ï¼tasks å®¡æ¹æµç¨å±ç¤ºï¼æ¨ªåæ¥éª¤æ¡ï¼ --> |
| | | <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> |
| | |
| | | {{ 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> |
| | | |
| | |
| | | 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" |
| | | > |
| | |
| | | @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 |
| | |
| | | </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> |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | 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(() => { |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | .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> |
| | |
| | | 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: "", |
| | |
| | | |
| | | 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" }]; |
| | |
| | | const tableColumn = ref([ |
| | | { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äººåç§°", prop: "applicantName", minWidth: 100 }, |
| | | { label: "ä¸å¡ç±»å", prop: "businessName", minWidth: 120 }, |
| | | { |
| | | label: "审æ¹ç±»å", |
| | | prop: "approvalType", |
| | |
| | | slot: "approveType", |
| | | }, |
| | | { |
| | | label: "å®¡æ¹æ¹å¼", |
| | | prop: "approvalMode", |
| | | width: 90, |
| | | dataType: "slot", |
| | | slot: "approvalMethod", |
| | | }, |
| | | { |
| | | label: "æ¯å¦æªè¯»", |
| | | label: "å¾
æå®¡æ¹", |
| | | prop: "unread", |
| | | width: 90, |
| | | align: "center", |
| | |
| | | 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() { |
| | |
| | | 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() { |
| | |
| | | } 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) { |
| | |
| | | return { |
| | | Search, |
| | | APPROVAL_TYPE_OPTIONS, |
| | | SUBMIT_TEMPLATES, |
| | | approvalTypeLabel, |
| | | approvalModeLabel, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | approvalActionLabel, |
| | |
| | | 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, |
| | | }; |
| | | } |
| | |
| | | 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 = [ |
| | |
| | | { 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"; |
| | | } |
| | | |
| | | /** å°æ¥å£è¿åçæ¨¡æ¿è½¬ä¸ºãç³»ç»å¸¸ç¨å®¡æ¹ãå¡çæ°æ® */ |
| | | 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 è§£å
*/ |
| | | 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ï¼ApprovalTemplateDtoï¼ */ |
| | | 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) { |
| | |
| | | id: "", |
| | | templateName: "", |
| | | description: "", |
| | | templateType: TEMPLATE_TYPE_CUSTOM, |
| | | formConfig: "", |
| | | formConfigData: createEmptyFormConfigData(), |
| | | enabled: true, |
| | | flowNodes: [createEmptyNode(1)], |
| | | }; |
| | |
| | | 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 || "", |
| | | })), |
| | |
| | | return { ok: false, message: `请为第 ${i + 1} 个èç¹éæ©è³å°ä¸å审æ¹äºº` }; |
| | | } |
| | | } |
| | | const cfgCheck = validateFormConfigData(form.formConfigData); |
| | | if (!cfgCheck.ok) return cfgCheck; |
| | | return { ok: true, nodes, name }; |
| | | } |
| | | |
| | |
| | | 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: "ææ¯è´è´£äºº" }, |
| | | { 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 */ |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å®¡æ¹æ¨¡æ¿ï¼å¯é
置填æ¥é¡¹ï¼åºååå° 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> |
| | |
| | | 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), |
| | |
| | | function publicShape(rows) { |
| | | return normalizeFlowNodes( |
| | | (rows || []).map((r) => ({ |
| | | id: r.id, |
| | | templateId: r.templateId, |
| | | nodeOrder: r.nodeOrder, |
| | | signMode: r.signMode, |
| | | approvers: r.approvers || [], |
| | |
| | | |
| | | 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(); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** å¡«æ¥é¡¹ç±»åï¼ä¸å®¡æ¹æäº¤é¡µ 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: "" }], |
| | | }; |
| | | } |
| | | |
| | | /** è§£æå项é»è®¤å¼ï¼ä¾æäº¤é¡µ 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("ã"); |
| | | } |
| | |
| | | <!--OA模åï¼å®¡æ¹æ¨¡æ¿ï¼ç³»ç»å¸¸ç¨ + èªå®ä¹å¤èç¹æµç¨ï¼--> |
| | | <!--OA模åï¼å®¡æ¹æ¨¡æ¿--> |
| | | <template> |
| | | <div class="app-container approve-template-page"> |
| | | <el-tabs v-model="activeTab" class="template-tabs"> |
| | |
| | | 以ä¸ä¸º 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"> |
| | |
| | | </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> |
| | | |
| | |
| | | <el-dialog |
| | | v-model="formDialog.visible" |
| | | :title="formDialog.title" |
| | | width="960px" |
| | | width="1020px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="template-form-dialog" |
| | |
| | | > |
| | | <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> |
| | |
| | | maxlength="200" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="å¡«æ¥é
ç½®"> |
| | | <FormConfigEditor v-model="form.formConfigData" /> |
| | | <p class="flow-tip">é
ç½®æäº¤å®¡æ¹æ¶éå¡«åç表å项ï¼ä¿åååå
¥ formConfigï¼JSONï¼ã</p> |
| | | </el-form-item> |
| | | <el-form-item label="å®¡æ¹æµç¨" required> |
| | | <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" /> |
| | |
| | | |
| | | <!-- 详æ
--> |
| | | <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"> |
| | |
| | | </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> |
| | |
| | | <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, |
| | |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | detailLoading, |
| | | fetchTemplateList, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | |
| | | } = at; |
| | | |
| | | const flowUserOptions = ref([]); |
| | | |
| | | const detailFormConfig = computed(() => |
| | | parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig) |
| | | ); |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | |
| | | |
| | | onMounted(() => { |
| | | loadUsers(); |
| | | handleQuery(); |
| | | loadBuiltinTemplates(); |
| | | fetchTemplateList(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | } |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | .mb16.el-empty { |
| | | padding: 8px 0; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | |
| | | 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); |
| | |
| | | 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: "", |
| | |
| | | |
| | | 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()); |
| | |
| | | |
| | | 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: "èç¹æ°", |
| | |
| | | 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() { |
| | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | fetchTemplateList(); |
| | | } |
| | | |
| | | function resetForm(row) { |
| | |
| | | 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]])), |
| | | }); |
| | |
| | | |
| | | 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() { |
| | |
| | | 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, |
| | |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | detailLoading, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openFormDialog, |
| | | openDetail, |
| | | submitForm, |
| | | toggleEnabled, |
| | | }; |
| | | } |