| | |
| | | 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 */ |
| | | } |
| | | } |