| | |
| | | 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, |
| | | 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, |
| | | }; |
| | | } |