Merge branch 'dev-new_pro_OA' into dev_NEW_pro
| | |
| | | import request from "@/utils/request.js"; |
| | | |
| | | /** å®¡æ¹æ¨¡æ¿ç±»åçéç¨æä¸¾ï¼TypeEnumsï¼ */ |
| | | export function getTypeEnums() { |
| | | return request({ |
| | | url: '/basic/enum/TypeEnums', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | export function findAllStockRecordTypeOptions() { |
| | | return request({ |
| | | url: '/basic/enum/stockRecordType', |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 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 request from "@/utils/request"; |
| | | |
| | | /** å页æ¥è¯¢ä¼ä¸æ°é» */ |
| | | export function listEnterpriseNewsPage(params) { |
| | | return request({ |
| | | url: "/enterpriseNews/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢ä¼ä¸æ°é» */ |
| | | export function saveEnterpriseNews(enterpriseNewsDto) { |
| | | return request({ |
| | | url: "/enterpriseNews/save", |
| | | method: "post", |
| | | data: enterpriseNewsDto, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹ä¼ä¸æ°é» */ |
| | | export function updateEnterpriseNews(enterpriseNewsDto) { |
| | | return request({ |
| | | url: "/enterpriseNews/update", |
| | | method: "put", |
| | | data: enterpriseNewsDto, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤ä¼ä¸æ°é»ï¼body 为 ID æ°ç»ï¼ */ |
| | | export function deleteEnterpriseNews(ids) { |
| | | const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== ""); |
| | | return request({ |
| | | url: "/enterpriseNews/delete", |
| | | method: "delete", |
| | | data: idList, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å页æ¥è¯¢è´¢å¡æ¥é GET /finReimbursement/listPage */ |
| | | export function listFinReimbursementPage(params) { |
| | | return request({ |
| | | url: "/finReimbursement/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** 详æ
queryï¼Spring ç»å® finReimbursementDto.idï¼å¿ç¨ finReimbursementDto[id] */ |
| | | function buildFinReimbursementDetailParams(idOrDto) { |
| | | const raw = |
| | | typeof idOrDto === "object" && idOrDto !== null |
| | | ? idOrDto.id ?? idOrDto.reimbursementId |
| | | : idOrDto; |
| | | return { |
| | | "finReimbursementDto.id": raw, |
| | | id: raw, |
| | | }; |
| | | } |
| | | |
| | | /** æ¥è¯¢è´¢å¡æ¥é详æ
GET /finReimbursement/detail */ |
| | | export function getFinReimbursementDetail(idOrDto) { |
| | | return request({ |
| | | url: "/finReimbursement/detail", |
| | | method: "get", |
| | | params: buildFinReimbursementDetailParams(idOrDto), |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢è´¢å¡æ¥é POST /finReimbursement/save */ |
| | | export function saveFinReimbursement(finReimbursementDto) { |
| | | return request({ |
| | | url: "/finReimbursement/save", |
| | | method: "post", |
| | | data: finReimbursementDto, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹è´¢å¡æ¥é POST /finReimbursement/update */ |
| | | export function updateFinReimbursement(finReimbursementDto) { |
| | | return request({ |
| | | url: "/finReimbursement/update", |
| | | method: "post", |
| | | data: finReimbursementDto, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤è´¢å¡æ¥é DELETE /finReimbursement/deleteï¼body 为 ID æ°ç»ï¼ */ |
| | | export function deleteFinReimbursement(ids) { |
| | | const idList = (Array.isArray(ids) ? ids : [ids]).filter( |
| | | (id) => id != null && id !== "" |
| | | ); |
| | | return request({ |
| | | url: "/finReimbursement/delete", |
| | | method: "delete", |
| | | data: idList, |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢èµ° saveï¼ä¿®æ¹èµ° updateï¼ä¸æ¥å£ææ¡£ä¸è´ï¼ */ |
| | | export function persistFinReimbursement(finReimbursementDto, isEdit = false) { |
| | | if (isEdit) { |
| | | return updateFinReimbursement(finReimbursementDto); |
| | | } |
| | | const payload = { ...finReimbursementDto }; |
| | | delete payload.id; |
| | | return saveFinReimbursement(payload); |
| | | } |
| | |
| | | dialogTitle.value = "ç¼è¾å¼ç¥¨ç³è¯·"; |
| | | fillFormFromRow(row); |
| | | dialogVisible.value = true; |
| | | loadOutboundBatches(form.customerId, true); |
| | | }; |
| | | |
| | | const view = (row) => { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { |
| | | createEmptyNode, |
| | | formatDisplayTime, |
| | | mapNodesFromApi, |
| | | mapSignModeFromApi, |
| | | mapSignModeToApi, |
| | | normalizeFlowNodes, |
| | | nodeSignModeLabel, |
| | | } from "../approve-template/approveTemplateConstants.js"; |
| | | import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js"; |
| | | import { |
| | | isDynamicOptionSource, |
| | | resolveSelectDisplayLabel, |
| | | } from "../approve-template/selectOptionSource.js"; |
| | | import { |
| | | appendDotNotationQuery, |
| | | buildApprovalInstanceSearchDto, |
| | | } from "../approve-shared/approvalInstanceListSearch.js"; |
| | | |
| | | /** 审æ¹ç±»åï¼ä¸åç«¯åæ®µ approvalType 对é½ï¼åæå¯åæ¥ï¼ */ |
| | | export const APPROVAL_TYPE_OPTIONS = [ |
| | | { value: "cost_reimburse", label: "è´¹ç¨æ¥éç³è¯·", cellBg: "#e8f8ef", cellColor: "#1a7f4b" }, |
| | | { value: "travel_reimburse", label: "å·®æ
æ¥éç³è¯·", cellBg: "#f0f2f5", cellColor: "#606266" }, |
| | | { value: "overtime", label: "å çç³è¯·", cellBg: "#fdf3e8", cellColor: "#c45c26" }, |
| | | { value: "leave", label: "请åç³è¯·", cellBg: "#fce8f0", cellColor: "#b84d7a" }, |
| | | { value: "work_handover", label: "å·¥ä½äº¤æ¥ç³è¯·", cellBg: "#f0e8fc", cellColor: "#6b4d9e" }, |
| | | { value: "regular", label: "转æ£ç³è¯·", cellBg: "#e8f4fc", cellColor: "#2b6cb0" }, |
| | | { value: "resign", label: "离èç³è¯·", cellBg: "#ffffff", cellColor: "#303133", border: "1px solid #e4e7ed" }, |
| | | { value: "transfer", label: "è°å²ç³è¯·", cellBg: "#ffffff", cellColor: "#303133", border: "1px solid #e4e7ed" }, |
| | | { value: "out_office", label: "å
¬åºç³è¯·", cellBg: "#e8f4ff", cellColor: "#409eff" }, |
| | | { value: "business_trip", label: "åºå·®ç³è¯·", cellBg: "#fdf6ec", cellColor: "#e6a23c" }, |
| | | { value: "procurement", label: "éè´å®¡æ¹", cellBg: "#f4f4f5", cellColor: "#909399" }, |
| | | { value: "quotation", label: "æ¥ä»·å®¡æ¹", cellBg: "#f4ecfc", cellColor: "#9b59b6" }, |
| | | { value: "shipment", label: "å货审æ¹", cellBg: "#e8faf6", cellColor: "#1abc9c" }, |
| | | { value: "enterprise_news", label: "ä¼ä¸æ°é»", cellBg: "#ecf5ff", cellColor: "#409eff" }, |
| | | ]; |
| | | |
| | | /** å表æ¥è¯¢ï¼å®¡æ¹ç¶æï¼ä¸å端 status æä¸¾ä¸è´ï¼ */ |
| | | export const APPROVAL_STATUS_SEARCH_OPTIONS = [ |
| | | { value: "DRAFT", label: "è稿" }, |
| | | { value: "PENDING", label: "å¾
审æ¹" }, |
| | | { value: "APPROVED", label: "å·²éè¿" }, |
| | | { value: "REJECTED", label: "已驳å" }, |
| | | ]; |
| | | |
| | | /** |
| | | * 审æ¹ç¶æå±ç¤ºï¼ä¸å端 status æä¸¾ä¸è´ï¼ |
| | | * DRAFTâè稿 PENDINGâå¾
审æ¹/è¿è¡ä¸ APPROVEDâå·²éè¿/已宿 REJECTEDâ已驳å |
| | | */ |
| | | export const APPROVAL_STATUS_OPTIONS = [ |
| | | { value: "draft", api: "DRAFT", label: "è稿" }, |
| | | { value: "pending", api: "PENDING", label: "å¾
审æ¹" }, |
| | | { value: "approved", api: "APPROVED", label: "å·²éè¿" }, |
| | | { value: "rejected", api: "REJECTED", label: "已驳å" }, |
| | | { value: "cancelled", api: "CANCELLED", label: "å·²æ¤é" }, |
| | | ]; |
| | | |
| | | /** æ°åç¶æç ï¼é¨ååç«¯ç¨ 0/1/2ï¼ */ |
| | | const STATUS_NUMERIC_MAP = { |
| | | 0: "pending", |
| | | 1: "approved", |
| | | 2: "rejected", |
| | | 3: "cancelled", |
| | | 4: "cancelled", |
| | | }; |
| | | |
| | | /** å端 status / é¡µé¢ approvalStatus â ç»ä¸é¡µé¢ keyï¼pending | approved | rejected | cancelledï¼ */ |
| | | export function normalizeApprovalStatusKey(v) { |
| | | if (v == null || v === "") return "pending"; |
| | | if (typeof v === "number" || (typeof v === "string" && /^\d+$/.test(v.trim()))) { |
| | | const numKey = STATUS_NUMERIC_MAP[Number(v)]; |
| | | if (numKey) return numKey; |
| | | } |
| | | const s = String(v).trim(); |
| | | if (!s) return "pending"; |
| | | const upper = s.toUpperCase(); |
| | | if (upper === "DRAFT") return "draft"; |
| | | if (upper === "PUBLISHED") return "approved"; |
| | | if (upper === "OFFLINE") return "cancelled"; |
| | | if (upper === "APPROVED" || upper === "APPROVE" || upper === "PASS" || upper === "AGREE") { |
| | | return "approved"; |
| | | } |
| | | if ( |
| | | upper === "REJECTED" || |
| | | upper === "REJECT" || |
| | | upper === "REFUSE" || |
| | | upper === "REFUSED" || |
| | | upper === "DENIED" |
| | | ) { |
| | | return "rejected"; |
| | | } |
| | | if (upper === "CANCELLED" || upper === "CANCEL" || upper === "REVOKED") return "cancelled"; |
| | | if ( |
| | | upper === "PENDING" || |
| | | upper === "IN_PROGRESS" || |
| | | upper === "PROCESSING" || |
| | | upper === "RUNNING" || |
| | | upper === "WAIT" || |
| | | upper === "WAITING" |
| | | ) { |
| | | return "pending"; |
| | | } |
| | | if (s.includes("è稿")) return "draft"; |
| | | if (s.includes("驳å") || s.includes("æç»")) return "rejected"; |
| | | if (s.includes("ä¸çº¿")) return "cancelled"; |
| | | if (s.includes("æ¤é")) return "cancelled"; |
| | | if (s.includes("åå¸") || s.includes("éè¿") || s.includes("宿")) return "approved"; |
| | | if (s.includes("å¾
审") || s.includes("è¿è¡ä¸") || s.includes("审æ¹ä¸")) return "pending"; |
| | | const lower = s.toLowerCase(); |
| | | if (["draft", "pending", "approved", "rejected", "cancelled"].includes(lower)) return lower; |
| | | return "pending"; |
| | | } |
| | | |
| | | /** ä»å表/详æ
è¡è§£æå端åå§ç¶æï¼å
¼å®¹å¤å段å½åï¼ */ |
| | | export function resolveInstanceStatusRaw(row) { |
| | | if (!row || typeof row !== "object") return ""; |
| | | const candidates = [ |
| | | row.status, |
| | | row.statusRaw, |
| | | row.approvalStatus, |
| | | row.statusName, |
| | | row.statusLabel, |
| | | row.approvalStatusName, |
| | | row.statusDesc, |
| | | row.instanceStatus, |
| | | row.approvalInstanceStatus, |
| | | row.approveStatus, |
| | | row.auditStatus, |
| | | row.approvalInstance?.status, |
| | | row.approvalInstanceVo?.status, |
| | | ]; |
| | | for (const c of candidates) { |
| | | if (c != null && c !== "") return c; |
| | | } |
| | | const tasks = row.tasks; |
| | | if (Array.isArray(tasks) && tasks.length) { |
| | | const rejected = tasks.some((t) => |
| | | normalizeApprovalStatusKey(t?.status ?? t?.taskStatus) === "rejected" |
| | | ); |
| | | if (rejected) return "REJECTED"; |
| | | const allApproved = tasks.every((t) => |
| | | normalizeApprovalStatusKey(t?.status ?? t?.taskStatus) === "approved" |
| | | ); |
| | | if (allApproved) return "APPROVED"; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | /** æäº¤å¼¹çªï¼æ¨¡æ¿å¡çï¼æ¥èªå端åè¡¨ï¼ */ |
| | | export function mapSubmitTemplateCard(row) { |
| | | const cfg = parseFormConfigToData(row?.formConfig); |
| | | return { |
| | | id: row?.id, |
| | | key: String(row?.id ?? ""), |
| | | businessType: row?.businessType ?? cfg.approvalType ?? row?.approvalType ?? "", |
| | | approvalType: cfg.approvalType || row?.approvalType || "", |
| | | label: row?.templateName || "â", |
| | | summaryPlaceholder: (row?.description || "").trim() || cfg.summaryPlaceholder || "ç¹å»å¡«åå¹¶æäº¤", |
| | | }; |
| | | } |
| | | |
| | | export function matchBusinessTypeValue(a, b) { |
| | | if (a == null || a === "" || b == null || b === "") return false; |
| | | return a === b || a === Number(b) || Number(a) === b || String(a) === String(b); |
| | | } |
| | | |
| | | /** 审æ¹è®°å½ 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) { |
| | | return approvalStatusLabel(status); |
| | | } |
| | | |
| | | export function mapTaskStatusTagType(status) { |
| | | return approvalStatusTagType(status); |
| | | } |
| | | |
| | | /** å端 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"; |
| | | } |
| | | |
| | | /** |
| | | * ååæ®µå±ç¤ºå¼ï¼è¯¦æ
åªè¯»ãåè¡¨ä¸»è¡¨ï¼ |
| | | * @param {object} [caches] 人å/é¨é¨ä¸æç¼åï¼ç¨äºè§£æã人åå表ãç±»åæ®µä¸ºå§å |
| | | */ |
| | | export function formatFieldDisplayValue(field, val, caches) { |
| | | if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "â"; |
| | | if (field?.type === "select" && isDynamicOptionSource(field.optionSource)) { |
| | | const label = resolveSelectDisplayLabel(field, val, caches || {}); |
| | | if (label && label !== "â") return label; |
| | | return String(val); |
| | | } |
| | | 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 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 }; |
| | | } |
| | | |
| | | /** è§£æå®ä¾ 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 || "", |
| | | businessType: tpl.businessType ?? submitForm?.businessType ?? "", |
| | | title, |
| | | formConfig: buildInstanceFormConfigJson({ ...tpl, fields: tpl.fields || submitForm?.formFieldDefs }, payload), |
| | | tasks: taskList, |
| | | }; |
| | | |
| | | const attachments = |
| | | (Array.isArray(submitForm?.storageBlobDTOs) && submitForm.storageBlobDTOs.length |
| | | ? submitForm.storageBlobDTOs |
| | | : null) || tpl.storageBlobDTOs; |
| | | if (attachments?.length) dto.storageBlobDTOs = attachments; |
| | | |
| | | if (isUpdate) { |
| | | dto.id = existingRow?.id ?? submitForm?.instanceId; |
| | | dto.instanceNo = existingRow?.instanceNo ?? submitForm?.instanceNo ?? ""; |
| | | dto.status = |
| | | submitForm?.saveStatusApi || |
| | | 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 = submitForm?.saveStatusApi || "PENDING"; |
| | | dto.currentLevel = 1; |
| | | dto.applicantId = userStore?.id; |
| | | dto.applicantName = userStore?.nickName || userStore?.name || ""; |
| | | } |
| | | return dto; |
| | | } |
| | | |
| | | /** æ ¡éªæäº¤å®¡æ¹æµç¨ï¼ä¸æ¨¡æ¿é¡µè§åä¸è´ï¼ */ |
| | | 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) { |
| | | return normalizeApprovalStatusKey(status); |
| | | } |
| | | |
| | | /** å表/详æ
è¡ â é¡µé¢ approvalStatus key */ |
| | | export function mapInstanceApprovalStatusFromRow(row) { |
| | | const raw = resolveInstanceStatusRaw(row); |
| | | return normalizeApprovalStatusKey(raw); |
| | | } |
| | | |
| | | /** é¡µé¢ approvalStatus â å端 status */ |
| | | export function mapInstanceStatusToApi(approvalStatus) { |
| | | const key = normalizeApprovalStatusKey(approvalStatus); |
| | | const hit = APPROVAL_STATUS_OPTIONS.find((x) => x.value === key); |
| | | return hit?.api || "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 statusRaw = resolveInstanceStatusRaw(row); |
| | | const approvalStatus = normalizeApprovalStatusKey(statusRaw); |
| | | 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.approvalType || row.templateName || "", |
| | | unread: Boolean(row.isApprove) && approvalStatus === "pending", |
| | | isApprove: Boolean(row.isApprove), |
| | | approvalStatus, |
| | | statusRaw: 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, |
| | | businessType, |
| | | extraParams, |
| | | }) { |
| | | const dto = buildApprovalInstanceSearchDto(searchForm, extraParams); |
| | | const bizType = businessType ?? searchForm?.businessType; |
| | | if (bizType != null && bizType !== "") { |
| | | dto.businessType = bizType; |
| | | } |
| | | |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | "page.current": page.current, |
| | | "page.size": page.size, |
| | | ...dto, |
| | | }; |
| | | appendDotNotationQuery(params, "approvalInstanceDto", dto); |
| | | return params; |
| | | } |
| | | |
| | | export function approvalTypeLabel(v) { |
| | | return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function approvalTypeStyle(v) { |
| | | const hit = APPROVAL_TYPE_OPTIONS.find((x) => x.value === v); |
| | | if (!hit) return {}; |
| | | return { |
| | | backgroundColor: hit.cellBg, |
| | | color: hit.cellColor, |
| | | border: hit.border || "none", |
| | | }; |
| | | } |
| | | |
| | | export function approvalStatusLabel(v) { |
| | | const key = normalizeApprovalStatusKey(v); |
| | | return APPROVAL_STATUS_OPTIONS.find((x) => x.value === key)?.label || "â"; |
| | | } |
| | | |
| | | /** ä¸å¡ç³è¯·é¡µç¶æææ¡ï¼PENDINGâè¿è¡ä¸ APPROVEDâ已宿 REJECTEDâ已驳å */ |
| | | export function businessApprovalStatusLabel(v) { |
| | | const key = normalizeApprovalStatusKey(v); |
| | | if (key === "draft") return "è稿"; |
| | | if (key === "pending") return "è¿è¡ä¸"; |
| | | if (key === "approved") return "已宿"; |
| | | if (key === "rejected") return "已驳å"; |
| | | if (key === "cancelled") return "å·²æ¤é"; |
| | | return "â"; |
| | | } |
| | | |
| | | /** |
| | | * ä¸å¡ç³è¯·é¡µæ¯å¦å
许修æ¹ï¼äºä¸ªç³è¯·é¡µï¼ |
| | | * è¿è¡ä¸(PENDING)ã已宿(APPROVED) ä¸å¯ä¿®æ¹ï¼å·²é©³åãå·²æ¤éçå¯ä¿®æ¹ |
| | | */ |
| | | export function canEditBusinessInstanceRow(row) { |
| | | const key = normalizeApprovalStatusKey( |
| | | row?.approvalStatus ?? row?.statusRaw ?? row?.status |
| | | ); |
| | | return key !== "pending" && key !== "approved"; |
| | | } |
| | | |
| | | export function businessApprovalStatusTagType(v) { |
| | | const key = normalizeApprovalStatusKey(v); |
| | | if (key === "draft") return "info"; |
| | | if (key === "approved") return "success"; |
| | | if (key === "rejected") return "danger"; |
| | | if (key === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | | export function approvalStatusTagType(v) { |
| | | const key = normalizeApprovalStatusKey(v); |
| | | if (key === "draft") return "info"; |
| | | if (key === "approved") return "success"; |
| | | if (key === "rejected") return "danger"; |
| | | if (key === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | | /** åè¡¨è¡ â ç¼è¾è¡¨åï¼ä»
ç¨è¡æ°æ®åæ¾ï¼ */ |
| | | 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)]; |
| | | |
| | | return { |
| | | 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, |
| | | templateAttachments: initTemplateAttachmentsFromSnapshot(templateSnapshot), |
| | | storageBlobDTOs: row?.storageBlobDTOs?.length |
| | | ? JSON.parse(JSON.stringify(row.storageBlobDTOs)) |
| | | : [], |
| | | }; |
| | | } |
| | | |
| | | 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 || "", |
| | | templateId: tpl?.templateId || "", |
| | | templateName: tpl?.label || "", |
| | | instanceId: "", |
| | | instanceNo: "", |
| | | statusRaw: "", |
| | | currentLevel: 1, |
| | | applicantId: null, |
| | | applicantName: "", |
| | | templateSnapshot: templateOverride || null, |
| | | formFieldDefs: tpl?.fields || [], |
| | | formPayload: payload, |
| | | flowNodes, |
| | | templateAttachments: tpl?.storageBlobDTOs |
| | | ? JSON.parse(JSON.stringify(tpl.storageBlobDTOs)) |
| | | : [], |
| | | storageBlobDTOs: [], |
| | | }; |
| | | } |
| | | |
| | | export function initTemplateAttachmentsFromSnapshot(templateSnapshot) { |
| | | const list = templateSnapshot?.storageBlobDTOs; |
| | | return list?.length ? JSON.parse(JSON.stringify(list)) : []; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- 审æ¹è¯¦æ
ï¼åºç¡ä¿¡æ¯ + å¡«æ¥å
容 --> |
| | | <template> |
| | | <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> |
| | | |
| | | <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, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | resolveInstanceFormFields, |
| | | } from "../approveListConstants.js"; |
| | | import FormPayloadFields from "./FormPayloadFields.vue"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | 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; |
| | | } |
| | | .reject-text { |
| | | color: var(--el-color-danger); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å¡«æ¥é¡¹ï¼ç¼è¾ä¸ºè¡¨åæ§ä»¶ï¼è¯¦æ
为 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> |
| | | |
| | | <div |
| | | v-else |
| | | class="form-payload-edit" |
| | | v-loading="optionSourceLoading" |
| | | > |
| | | <el-form-item |
| | | v-for="field in fields" |
| | | :key="field.key" |
| | | :label="field.label" |
| | | :prop="`formPayload.${field.key}`" |
| | | :required="Boolean(field.required)" |
| | | > |
| | | <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 |
| | | filterable |
| | | > |
| | | <el-option |
| | | v-for="o in getOptions(field)" |
| | | :key="String(o.value)" |
| | | :label="o.label" |
| | | :value="o.value" |
| | | /> |
| | | </el-select> |
| | | <span v-else class="field-value">{{ displayValue(field) }}</span> |
| | | </el-form-item> |
| | | </div> |
| | | </template> |
| | | <el-empty v-else description="ææ å¡«æ¥é¡¹" :image-size="48" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, watch } from "vue"; |
| | | import { useSelectOptionSources } from "../../approve-template/useSelectOptionSources.js"; |
| | | import { formatFieldDisplayValue } from "../approveListConstants.js"; |
| | | |
| | | const props = defineProps({ |
| | | fields: { type: Array, default: () => [] }, |
| | | formPayload: { type: Object, default: () => ({}) }, |
| | | readonly: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const { loading: optionSourceLoading, ensureForFields, getOptions, getDisplayLabel } = |
| | | useSelectOptionSources(); |
| | | |
| | | async function loadOptionCaches() { |
| | | await ensureForFields(props.fields); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadOptionCaches(); |
| | | }); |
| | | |
| | | watch( |
| | | () => props.fields, |
| | | () => { |
| | | loadOptionCaches(); |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | function displayValue(field) { |
| | | const val = props.formPayload?.[field.key]; |
| | | if (field.type === "select" && field.optionSource && field.optionSource !== "static") { |
| | | return getDisplayLabel(field, val); |
| | | } |
| | | return formatFieldDisplayValue(field, val); |
| | | } |
| | | </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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å®¡æ¹å表--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div class="search_fields"> |
| | | <span class="search_title">模æ¿ç±»åï¼</span> |
| | | <el-select |
| | | v-model="searchForm.businessType" |
| | | placeholder="è¯·éæ©æ¨¡æ¿ç±»å" |
| | | clearable |
| | | filterable |
| | | style="width: 200px" |
| | | > |
| | | <el-option |
| | | v-for="opt in searchBusinessTypeOptions" |
| | | :key="`search-biz-type-${opt.value}`" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | <span class="search_title" style="margin-left: 12px">审æ¹ç¶æï¼</span> |
| | | <el-select |
| | | v-model="searchForm.status" |
| | | placeholder="è¯·éæ©å®¡æ¹ç¶æ" |
| | | clearable |
| | | style="width: 140px" |
| | | > |
| | | <el-option |
| | | v-for="opt in APPROVAL_STATUS_SEARCH_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | <span class="search_title" style="margin-left: 12px">å建æ¶é´ï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.createTimeRange" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 260px" |
| | | clearable |
| | | /> |
| | | <el-button type="primary" :icon="Search" style="margin-left: 10px" @click="handleQuery">æç´¢</el-button> |
| | | <el-button :icon="RefreshRight" @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="primary" :icon="Plus" @click="openSubmitDialog">æäº¤å®¡æ¹</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | :total="page.total" |
| | | @pagination="pagination" |
| | | > |
| | | <template #approveType="{ row }"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)"> |
| | | {{ approvalTypeLabel(row.approvalType) }} |
| | | </span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <!-- æäº¤å®¡æ¹ï¼ææ¨¡æ¿ï¼ --> |
| | | <el-dialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialogTitle" |
| | | width="720px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-submit-dialog" |
| | | @closed="resetSubmitDialogState" |
| | | > |
| | | <template v-if="submitDialog.step === 1 && !isSubmitEdit"> |
| | | <p class="template-hint">请å
éæ©æ¨¡æ¿ç±»åï¼åéæ©è¯¥ç±»åä¸å·²å¯ç¨çå®¡æ¹æ¨¡æ¿ã</p> |
| | | <div v-loading="submitTemplatesLoading" class="template-grid"> |
| | | <div |
| | | v-for="opt in submitBusinessTypeOptions" |
| | | :key="`biz-type-${opt.value}`" |
| | | class="template-card" |
| | | :class="{ 'is-disabled': !countTemplatesByBusinessType(opt.value) }" |
| | | @click="onBusinessTypePick(opt.value)" |
| | | > |
| | | <span class="template-card-type">{{ opt.label }}</span> |
| | | <span class="template-card-desc"> |
| | | {{ countTemplatesByBusinessType(opt.value) }} 个å¯ç¨æ¨¡æ¿ |
| | | </span> |
| | | </div> |
| | | <el-empty |
| | | v-if="!submitTemplatesLoading && !submitBusinessTypeOptions.length" |
| | | description="ææ æ¨¡æ¿ç±»å" |
| | | :image-size="80" |
| | | class="template-empty" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <template v-else-if="submitDialog.step === 2 && !isSubmitEdit"> |
| | | <p class="template-hint"> |
| | | å½åç±»åï¼{{ selectedBusinessTypeLabel || "â" }}ï¼è¯·éæ©å
·ä½å®¡æ¹æ¨¡æ¿ã |
| | | <el-button type="primary" link class="ml8" @click="backToBusinessTypePick">æ´æ¢ç±»å</el-button> |
| | | </p> |
| | | <ApprovalTemplatePicker |
| | | :cards="submitTemplateCards" |
| | | :loading="submitTemplatesLoading" |
| | | @pick="onTemplatePick" |
| | | /> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <div v-loading="submitTemplatesLoading && !isSubmitEdit"> |
| | | <el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px"> |
| | | <el-form-item v-if="isSubmitEdit" label="审æ¹ç±»å"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)"> |
| | | {{ activeTemplate.label }} |
| | | </span> |
| | | </el-form-item> |
| | | <ApprovalTemplateFormSection |
| | | :active-template="activeTemplate" |
| | | :fields="submitFormFields" |
| | | :form-payload="submitForm.formPayload" |
| | | v-model:flow-nodes="submitForm.flowNodes" |
| | | v-model:attachments="submitForm.storageBlobDTOs" |
| | | :template-attachments="submitForm.templateAttachments" |
| | | :user-options="flowUserOptions" |
| | | :show-template-name="!isSubmitEdit" |
| | | :allow-change-template="!isSubmitEdit" |
| | | @change-template="backToTemplatePick" |
| | | /> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <template #footer> |
| | | <el-button |
| | | v-if="submitDialog.step === 3 || isSubmitEdit" |
| | | type="primary" |
| | | :loading="submitSaving" |
| | | @click="onSubmitInstance" |
| | | > |
| | | {{ isSubmitEdit ? "ä¿ å" : "æ 交" }} |
| | | </el-button> |
| | | <el-button |
| | | v-if="submitDialog.step === 2 && !isSubmitEdit" |
| | | @click="backToBusinessTypePick" |
| | | > |
| | | ä¸ä¸æ¥ |
| | | </el-button> |
| | | <el-button @click="submitDialog.visible = false"> |
| | | {{ submitDialog.step === 1 && !isSubmitEdit ? "å æ¶" : "å
³ é" }} |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详æ
--> |
| | | <el-dialog |
| | | v-model="detailDialog.visible" |
| | | title="审æ¹è¯¦æ
" |
| | | width="920px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-detail-dialog" |
| | | > |
| | | <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" |
| | | > |
| | | å»å®¡æ¹ |
| | | </el-button> |
| | | <el-button @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å·®æ
/è´¹ç¨æ¥é详æ
ï¼å®¡æ¹åè¡¨ï¼ --> |
| | | <el-dialog |
| | | v-model="reimburseDialog.visible" |
| | | :title="reimburseDialog.mode === 'approve' ? reimburseApproveTitle : reimburseDetailTitle" |
| | | width="1000px" |
| | | append-to-body |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <FinReimburseApprovePanel |
| | | :mode="reimburseDialog.mode" |
| | | :module-key="reimburseDialog.moduleKey" |
| | | :reimburse-row="reimburseDialog.reimburseRow" |
| | | :loading="reimburseDialog.loading" |
| | | v-model:approve-opinion="approveOpinion" |
| | | /> |
| | | <template #footer> |
| | | <template v-if="reimburseDialog.mode === 'approve'"> |
| | | <el-button |
| | | type="success" |
| | | :loading="approveSubmitting" |
| | | @click="onReimburseApprove('approved')" |
| | | > |
| | | é è¿ |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | :loading="approveSubmitting" |
| | | @click="onReimburseApprove('rejected')" |
| | | > |
| | | 驳 å |
| | | </el-button> |
| | | <el-button :disabled="approveSubmitting" @click="reimburseDialog.visible = false"> |
| | | å æ¶ |
| | | </el-button> |
| | | </template> |
| | | <template v-else> |
| | | <el-button |
| | | v-if="reimburseDialog.instanceRow?.approvalStatus === 'pending'" |
| | | @click="openEditFromReimburseDetail" |
| | | > |
| | | ä¿® æ¹ |
| | | </el-button> |
| | | <el-button |
| | | v-if=" |
| | | reimburseDialog.instanceRow?.approvalStatus === 'pending' && |
| | | reimburseDialog.instanceRow?.isApprove |
| | | " |
| | | type="primary" |
| | | @click="openReimburseApproveFromDetail" |
| | | > |
| | | å»å®¡æ¹ |
| | | </el-button> |
| | | <el-button type="primary" @click="reimburseDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å®¡æ¹æä½ --> |
| | | <el-dialog |
| | | v-model="approveDialog.visible" |
| | | title="审æ¹å¤ç" |
| | | width="960px" |
| | | append-to-body |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <ApproveDetailPanel :row="approveDialog.row" /> |
| | | <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 |
| | | v-model="approveOpinion" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="500" |
| | | show-word-limit |
| | | placeholder="éè¿å¯ç空ï¼é©³å请填åå
·ä½åå " |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <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> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { APPROVAL_MODULE_KEYS } from "../approve-shared/approvalModuleRegistry.js"; |
| | | import FinReimburseApprovePanel from "../../ReimburseManage/shared/components/FinReimburseApprovePanel.vue"; |
| | | import ApprovalTemplateFormSection from "../approve-shared/components/ApprovalTemplateFormSection.vue"; |
| | | import ApprovalTemplatePicker from "../approve-shared/components/ApprovalTemplatePicker.vue"; |
| | | import { useFlowUserOptions } from "../approve-shared/useFlowUserOptions.js"; |
| | | 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_STATUS_SEARCH_OPTIONS, |
| | | searchBusinessTypeOptions, |
| | | loadSearchBusinessTypeOptions, |
| | | submitBusinessTypeOptions, |
| | | submitTemplateCards, |
| | | selectedBusinessTypeLabel, |
| | | countTemplatesByBusinessType, |
| | | submitTemplatesLoading, |
| | | onBusinessTypePick, |
| | | backToBusinessTypePick, |
| | | approvalTypeLabel, |
| | | approvalActionLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | reimburseDialog, |
| | | approveDialog, |
| | | approveOpinion, |
| | | approveSubmitting, |
| | | submitReimburseApprove, |
| | | submitDialog, |
| | | isSubmitEdit, |
| | | submitDialogTitle, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | resetSubmitDialogState, |
| | | openSubmitDialog, |
| | | openEditDialog, |
| | | onTemplatePick, |
| | | backToTemplatePick, |
| | | submitInstanceForm, |
| | | submitApprove, |
| | | openDetail, |
| | | openApprove, |
| | | } = al; |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | async function onSubmitInstance() { |
| | | const ok = await submitInstanceForm(); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "审æ¹å·²æäº¤"); |
| | | } |
| | | |
| | | const reimburseDetailTitle = computed(() => |
| | | reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE |
| | | ? "è´¹ç¨æ¥é详æ
" |
| | | : "å·®æ
æ¥é详æ
" |
| | | ); |
| | | const reimburseApproveTitle = computed(() => |
| | | reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE |
| | | ? "è´¹ç¨æ¥é审æ¹" |
| | | : "å·®æ
æ¥é审æ¹" |
| | | ); |
| | | |
| | | async function onApprove(result) { |
| | | const ret = await submitApprove(result); |
| | | if (ret?.needOpinion) { |
| | | ElMessage.warning("é©³åæ¶è¯·å¡«åå®¡æ¹æè§"); |
| | | return; |
| | | } |
| | | if (ret?.ok) { |
| | | ElMessage.success(result === "approved" ? "å·²éè¿" : "已驳å"); |
| | | } |
| | | } |
| | | |
| | | async function onReimburseApprove(result) { |
| | | const ret = await submitReimburseApprove(result); |
| | | if (ret?.needOpinion) { |
| | | ElMessage.warning("é©³åæ¶è¯·å¡«åå®¡æ¹æè§"); |
| | | return; |
| | | } |
| | | if (ret?.ok) { |
| | | ElMessage.success(result === "approved" ? "å·²éè¿" : "已驳å"); |
| | | } |
| | | } |
| | | |
| | | function formatRecordTime(time) { |
| | | return formatDisplayTime(time) || "â"; |
| | | } |
| | | |
| | | async function openApproveFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | await openApprove(row); |
| | | } |
| | | |
| | | function openEditFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openEditDialog(row); |
| | | } |
| | | |
| | | function openEditFromReimburseDetail() { |
| | | const row = reimburseDialog.instanceRow; |
| | | reimburseDialog.visible = false; |
| | | if (row) openEditDialog(row); |
| | | } |
| | | |
| | | async function openReimburseApproveFromDetail() { |
| | | const row = reimburseDialog.instanceRow; |
| | | if (!row) return; |
| | | reimburseDialog.mode = "approve"; |
| | | approveOpinion.value = ""; |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadFlowUsers(); |
| | | loadSearchBusinessTypeOptions(); |
| | | handleQuery(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .ml12 { |
| | | margin-left: 12px; |
| | | } |
| | | .mt16 { |
| | | margin-top: 16px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_fields { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | .search_actions { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | .approve-type-cell { |
| | | display: inline-block; |
| | | padding: 2px 10px; |
| | | border-radius: 4px; |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | .template-hint { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary); |
| | | margin: 0 0 16px; |
| | | } |
| | | .template-grid { |
| | | 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; |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: var(--radius-md, 8px); |
| | | cursor: pointer; |
| | | transition: border-color 0.2s, box-shadow 0.2s; |
| | | background: var(--el-fill-color-blank); |
| | | } |
| | | .template-card:hover { |
| | | border-color: var(--el-color-primary); |
| | | box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06)); |
| | | } |
| | | .template-card.is-disabled { |
| | | opacity: 0.5; |
| | | cursor: not-allowed; |
| | | } |
| | | .template-card.is-disabled:hover { |
| | | border-color: var(--el-border-color-lighter); |
| | | box-shadow: none; |
| | | } |
| | | .ml8 { |
| | | margin-left: 8px; |
| | | } |
| | | .template-card-type { |
| | | display: inline-block; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | margin-bottom: 8px; |
| | | } |
| | | .template-card-desc { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | line-height: 1.5; |
| | | } |
| | | .flow-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin-top: 8px; |
| | | } |
| | | .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 { |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplate, |
| | | 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 { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { computed, getCurrentInstance, reactive, ref } from "vue"; |
| | | import { |
| | | inferReimburseModuleKeyFromInstance, |
| | | loadReimburseDetailForInstance, |
| | | navigateToReimburseManageForEdit, |
| | | resolveFinReimbursementIdFromInstance, |
| | | } from "../../ReimburseManage/shared/reimburseApproveBridge.js"; |
| | | import { |
| | | fetchBusinessTypeOptions, |
| | | formatDisplayTime, |
| | | mapEnabledFromApi, |
| | | unwrapTemplateList, |
| | | } from "../approve-template/approveTemplateConstants.js"; |
| | | import { |
| | | buildFormPayloadRules, |
| | | buildTemplateBindingFromDetail, |
| | | validateTemplateBinding, |
| | | } from "../approve-shared/approvalTemplateBindingUtils.js"; |
| | | import { |
| | | APPROVAL_STATUS_SEARCH_OPTIONS, |
| | | APPROVAL_TYPE_OPTIONS, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | approvalTypeLabel, |
| | | buildApprovalInstanceListParams, |
| | | buildApproveInstanceDto, |
| | | buildEditFormFromInstanceRow, |
| | | buildInstanceDto, |
| | | createEmptySubmitForm, |
| | | mapInstanceFromApi, |
| | | mapSubmitTemplateCard, |
| | | matchBusinessTypeValue, |
| | | unwrapInstancePage, |
| | | } from "./approveListConstants.js"; |
| | | |
| | | export function useApproveList() { |
| | | const { proxy } = getCurrentInstance() || {}; |
| | | const userStore = useUserStore(); |
| | | |
| | | const tableData = ref([]); |
| | | const searchBusinessTypeOptions = ref([]); |
| | | const submitBusinessTypeOptions = ref([]); |
| | | const allSubmitTemplates = ref([]); |
| | | const selectedBusinessType = ref(""); |
| | | const submitTemplatesLoading = ref(false); |
| | | |
| | | const submitTemplateCards = computed(() => { |
| | | if (selectedBusinessType.value == null || selectedBusinessType.value === "") return []; |
| | | return allSubmitTemplates.value.filter((card) => |
| | | matchBusinessTypeValue(card.businessType, selectedBusinessType.value) |
| | | ); |
| | | }); |
| | | |
| | | const searchForm = reactive({ |
| | | businessType: "", |
| | | status: "", |
| | | createTimeRange: [], |
| | | }); |
| | | |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | const approveSubmitting = ref(false); |
| | | |
| | | /** å·®æ
/è´¹ç¨æ¥éä¸ç¨è¯¦æ
ã审æ¹å¼¹çª */ |
| | | const reimburseDialog = reactive({ |
| | | visible: false, |
| | | mode: "detail", |
| | | moduleKey: "", |
| | | loading: false, |
| | | reimburseRow: {}, |
| | | instanceRow: null, |
| | | }); |
| | | |
| | | 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 isSubmitEdit = computed(() => submitDialog.mode === "edit"); |
| | | const submitDialogTitle = computed(() => { |
| | | if (submitDialog.mode === "edit") { |
| | | return `ä¿®æ¹${activeTemplate.value?.label || submitForm.templateName || "审æ¹"}`; |
| | | } |
| | | if (submitDialog.step === 1) return "éæ©æ¨¡æ¿ç±»å"; |
| | | if (submitDialog.step === 2) return `éæ©å®¡æ¹æ¨¡æ¿${businessTypeLabel(selectedBusinessType.value) ? `ï¼${businessTypeLabel(selectedBusinessType.value)}ï¼` : ""}`; |
| | | return `æäº¤${activeTemplate.value?.label || "审æ¹"}`; |
| | | }); |
| | | |
| | | const selectedBusinessTypeLabel = computed(() => businessTypeLabel(selectedBusinessType.value)); |
| | | |
| | | function businessTypeLabel(type) { |
| | | if (type == null || type === "") return ""; |
| | | const hit = submitBusinessTypeOptions.value.find((x) => matchBusinessTypeValue(x.value, type)); |
| | | return hit?.label || ""; |
| | | } |
| | | |
| | | function countTemplatesByBusinessType(type) { |
| | | return allSubmitTemplates.value.filter((card) => matchBusinessTypeValue(card.businessType, type)).length; |
| | | } |
| | | |
| | | const activeTemplate = computed(() => submitForm.templateSnapshot || null); |
| | | |
| | | /** å¡«æ¥é¡¹å®ä¹ï¼æ°å¢/ä¿®æ¹ä¸ formConfig ä¸è´ï¼ */ |
| | | const submitFormFields = computed(() => { |
| | | const tplFields = activeTemplate.value?.fields; |
| | | if (tplFields?.length) return tplFields; |
| | | return submitForm.formFieldDefs || []; |
| | | }); |
| | | |
| | | const submitFormRules = computed(() => ({ |
| | | templateKey: [{ required: true, message: "è¯·éæ©å®¡æ¹ç±»å", trigger: "change" }], |
| | | ...buildFormPayloadRules(submitFormFields.value), |
| | | })); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äººåç§°", prop: "applicantName", minWidth: 100 }, |
| | | { label: "模æ¿ç±»å", prop: "businessName", minWidth: 120 }, |
| | | { |
| | | label: "审æ¹ç±»å", |
| | | prop: "approvalType", |
| | | minWidth: 140, |
| | | dataType: "slot", |
| | | slot: "approveType", |
| | | }, |
| | | { |
| | | label: "å¾
æå®¡æ¹", |
| | | prop: "unread", |
| | | width: 90, |
| | | align: "center", |
| | | formatData: (v) => (v ? "æ¯" : "å¦"), |
| | | }, |
| | | { |
| | | label: "审æ¹ç¶æ", |
| | | prop: "approvalStatus", |
| | | width: 100, |
| | | dataType: "tag", |
| | | formatData: (v) => approvalStatusLabel(v), |
| | | formatType: (v) => approvalStatusTagType(v), |
| | | }, |
| | | { |
| | | label: "å建æ¶é´", |
| | | prop: "createTime", |
| | | width: 170, |
| | | formatData: (v) => formatDisplayTime(v), |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 240, |
| | | operation: [ |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | 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), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | 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 [typeOptions, customRes] = await Promise.all([ |
| | | fetchBusinessTypeOptions(), |
| | | listApprovalTemplate(TEMPLATE_TYPE_CUSTOM), |
| | | ]); |
| | | submitBusinessTypeOptions.value = typeOptions; |
| | | allSubmitTemplates.value = unwrapTemplateList(customRes) |
| | | .filter((row) => mapEnabledFromApi(row.enabled)) |
| | | .map(mapSubmitTemplateCard); |
| | | } catch { |
| | | submitBusinessTypeOptions.value = []; |
| | | allSubmitTemplates.value = []; |
| | | ElMessage.error("å è½½å®¡æ¹æ¨¡æ¿å¤±è´¥"); |
| | | } finally { |
| | | submitTemplatesLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | fetchApprovalList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.businessType = ""; |
| | | searchForm.status = ""; |
| | | searchForm.createTimeRange = []; |
| | | handleQuery(); |
| | | } |
| | | |
| | | async function loadSearchBusinessTypeOptions() { |
| | | try { |
| | | searchBusinessTypeOptions.value = await fetchBusinessTypeOptions(); |
| | | } catch { |
| | | searchBusinessTypeOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | fetchApprovalList(); |
| | | } |
| | | |
| | | async function openReimburseDetail(row, mode) { |
| | | const moduleKey = inferReimburseModuleKeyFromInstance(row); |
| | | if (!moduleKey) return false; |
| | | reimburseDialog.mode = mode; |
| | | reimburseDialog.moduleKey = moduleKey; |
| | | reimburseDialog.instanceRow = row; |
| | | reimburseDialog.visible = true; |
| | | reimburseDialog.loading = true; |
| | | reimburseDialog.reimburseRow = {}; |
| | | try { |
| | | const { reimburseRow, moduleKey: resolvedMk } = |
| | | await loadReimburseDetailForInstance(row, moduleKey); |
| | | reimburseDialog.moduleKey = resolvedMk || moduleKey; |
| | | reimburseDialog.reimburseRow = reimburseRow; |
| | | return true; |
| | | } catch { |
| | | ElMessage.error("å è½½æ¥é详æ
失败"); |
| | | reimburseDialog.visible = false; |
| | | return false; |
| | | } finally { |
| | | reimburseDialog.loading = false; |
| | | } |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | if (isReimburseApprovalInstance(row)) { |
| | | await openReimburseDetail(row, "detail"); |
| | | return; |
| | | } |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | async function openApprove(row) { |
| | | if (inferReimburseModuleKeyFromInstance(row)) { |
| | | approveOpinion.value = ""; |
| | | await openReimburseDetail(row, "approve"); |
| | | return; |
| | | } |
| | | approveDialog.row = { ...row }; |
| | | approveOpinion.value = ""; |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | | function isReimburseApprovalInstance(row) { |
| | | return Boolean(inferReimburseModuleKeyFromInstance(row)); |
| | | } |
| | | |
| | | function resetSubmitDialogState() { |
| | | submitDialog.mode = "add"; |
| | | submitDialog.step = 1; |
| | | selectedBusinessType.value = ""; |
| | | submitEditRow.value = null; |
| | | Object.assign(submitForm, createEmptySubmitForm("")); |
| | | } |
| | | |
| | | function openSubmitDialog() { |
| | | resetSubmitDialogState(); |
| | | submitDialog.visible = true; |
| | | loadSubmitTemplates(); |
| | | } |
| | | |
| | | async function openEditDialog(row) { |
| | | if (row?.approvalStatus !== "pending") { |
| | | ElMessage.warning("ä»
å®¡æ ¸ä¸ç审æ¹å¯ä¿®æ¹"); |
| | | return; |
| | | } |
| | | const moduleKey = inferReimburseModuleKeyFromInstance(row); |
| | | if (moduleKey) { |
| | | const rid = resolveFinReimbursementIdFromInstance(row); |
| | | if (rid == null) { |
| | | ElMessage.warning("æ æ³ä¿®æ¹ï¼ç¼ºå°æ¥éå ID"); |
| | | return; |
| | | } |
| | | try { |
| | | await navigateToReimburseManageForEdit(proxy?.$router, moduleKey, rid); |
| | | } catch { |
| | | ElMessage.warning("æªæ¾å°å·®æ
/è´¹ç¨æ¥éèåè·¯ç±ï¼è¯·ä»å·¦ä¾§èåè¿å
¥ååç¼è¾"); |
| | | } |
| | | return; |
| | | } |
| | | if (!row?.id) { |
| | | ElMessage.warning("æ æ³ä¿®æ¹ï¼ç¼ºå°å®¡æ¹å®ä¾ ID"); |
| | | return; |
| | | } |
| | | submitDialog.mode = "edit"; |
| | | submitDialog.step = 3; |
| | | submitEditRow.value = { ...row }; |
| | | Object.assign(submitForm, buildEditFormFromInstanceRow(row)); |
| | | submitDialog.visible = true; |
| | | } |
| | | |
| | | async function onTemplatePick(card) { |
| | | if (!card?.id) return; |
| | | submitTemplatesLoading.value = true; |
| | | try { |
| | | const res = await getApprovalTemplateDetail(card.id); |
| | | const applied = buildTemplateBindingFromDetail(res); |
| | | Object.assign(submitForm, { |
| | | templateKey: String(card.id), |
| | | ...applied, |
| | | businessType: |
| | | applied.businessType ?? card.businessType ?? selectedBusinessType.value, |
| | | }); |
| | | submitDialog.step = 3; |
| | | } catch { |
| | | ElMessage.error("å 载模æ¿è¯¦æ
失败"); |
| | | } finally { |
| | | submitTemplatesLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function onBusinessTypePick(type) { |
| | | if (!countTemplatesByBusinessType(type)) { |
| | | ElMessage.warning("该类å䏿æ å¯ç¨å®¡æ¹æ¨¡æ¿"); |
| | | return; |
| | | } |
| | | selectedBusinessType.value = type; |
| | | submitDialog.step = 2; |
| | | } |
| | | |
| | | function backToBusinessTypePick() { |
| | | selectedBusinessType.value = ""; |
| | | submitDialog.step = 1; |
| | | } |
| | | |
| | | function backToTemplatePick() { |
| | | submitDialog.step = 2; |
| | | } |
| | | |
| | | async function submitInstanceForm() { |
| | | if (submitDialog.mode === "edit") return submitEditApproval(); |
| | | return submitNewApproval(); |
| | | } |
| | | |
| | | async function submitNewApproval() { |
| | | if (!submitFormRef.value) return false; |
| | | try { |
| | | await submitFormRef.value.validate(); |
| | | } catch { |
| | | return false; |
| | | } |
| | | if (!activeTemplate.value) return false; |
| | | const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes }); |
| | | if (!bindingCheck.ok) { |
| | | ElMessage.warning(bindingCheck.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: bindingCheck.nodes, |
| | | }) |
| | | ); |
| | | submitDialog.visible = false; |
| | | page.current = 1; |
| | | await fetchApprovalList(); |
| | | return true; |
| | | } catch { |
| | | return false; |
| | | } finally { |
| | | submitSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitEditApproval() { |
| | | if (!submitFormRef.value) return false; |
| | | try { |
| | | await submitFormRef.value.validate(); |
| | | } catch { |
| | | return false; |
| | | } |
| | | if (!activeTemplate.value) return false; |
| | | const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes }); |
| | | if (!bindingCheck.ok) { |
| | | ElMessage.warning(bindingCheck.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: bindingCheck.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 submitReimburseApprove(result) { |
| | | const row = reimburseDialog.instanceRow; |
| | | if (!row?.id) return { ok: false }; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | return { needOpinion: true }; |
| | | } |
| | | if (approveSubmitting.value) return { ok: false }; |
| | | approveSubmitting.value = true; |
| | | try { |
| | | await approveApprovalInstance( |
| | | buildApproveInstanceDto(row, result, approveOpinion.value) |
| | | ); |
| | | reimburseDialog.visible = false; |
| | | await fetchApprovalList(); |
| | | return { ok: true, result }; |
| | | } catch { |
| | | ElMessage.error("å®¡æ¹æä½å¤±è´¥"); |
| | | return { ok: false }; |
| | | } finally { |
| | | approveSubmitting.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | | const row = approveDialog.row; |
| | | if (!row?.id) return { ok: false }; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | return { needOpinion: true }; |
| | | } |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | function approvalActionLabel(result) { |
| | | if (result === "approved") return "éè¿"; |
| | | if (result === "rejected") return "驳å"; |
| | | return "å¾
å¤ç"; |
| | | } |
| | | |
| | | return { |
| | | Search, |
| | | APPROVAL_TYPE_OPTIONS, |
| | | APPROVAL_STATUS_SEARCH_OPTIONS, |
| | | searchBusinessTypeOptions, |
| | | loadSearchBusinessTypeOptions, |
| | | approvalTypeLabel, |
| | | approvalStatusLabel, |
| | | approvalStatusTagType, |
| | | approvalActionLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | detailDialog, |
| | | detailRow, |
| | | reimburseDialog, |
| | | approveDialog, |
| | | approveOpinion, |
| | | approveSubmitting, |
| | | submitReimburseApprove, |
| | | isReimburseApprovalInstance, |
| | | submitDialog, |
| | | isSubmitEdit, |
| | | submitDialogTitle, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitBusinessTypeOptions, |
| | | submitTemplateCards, |
| | | selectedBusinessType, |
| | | selectedBusinessTypeLabel, |
| | | businessTypeLabel, |
| | | countTemplatesByBusinessType, |
| | | submitTemplatesLoading, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | resetSubmitDialogState, |
| | | openSubmitDialog, |
| | | openEditDialog, |
| | | onBusinessTypePick, |
| | | onTemplatePick, |
| | | backToBusinessTypePick, |
| | | backToTemplatePick, |
| | | submitInstanceForm, |
| | | submitNewApproval, |
| | | submitApprove, |
| | | openDetail, |
| | | openApprove, |
| | | fetchApprovalList, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { computed } from "vue"; |
| | | import { |
| | | businessApprovalStatusLabel, |
| | | businessApprovalStatusTagType, |
| | | formatFieldDisplayValue, |
| | | resolveInstanceFormFields, |
| | | } from "../approve-list/approveListConstants.js"; |
| | | import { |
| | | INSTANCE_NO_SEARCH_MODULE_KEYS, |
| | | INSTANCE_NO_TABLE_COLUMN, |
| | | } from "./approvalInstanceListSearch.js"; |
| | | |
| | | /** å表/详æ
ä¸åæ¾ä¸ºç¬ç«åçå¡«æ¥é¡¹ keyï¼é¿å
è¦çå®ä¾ç³»ç»åæ®µï¼ */ |
| | | const DEFAULT_EXCLUDE_KEYS = new Set([ |
| | | "summary", |
| | | "status", |
| | | "approvalStatus", |
| | | "approvalstatus", |
| | | "instanceStatus", |
| | | "publishStatus", |
| | | "newsStatus", |
| | | ]); |
| | | |
| | | /** enrich åå¿
é¡»ä¿ççå®ä¾å段ï¼ä¸è¢« formConfig éºå¹³è¦çï¼ */ |
| | | const PRESERVE_INSTANCE_FIELDS = [ |
| | | "id", |
| | | "approvalStatus", |
| | | "statusRaw", |
| | | "status", |
| | | "instanceNo", |
| | | "templateId", |
| | | "templateName", |
| | | "businessType", |
| | | "businessId", |
| | | "businessName", |
| | | "applicantId", |
| | | "applicantNo", |
| | | "applicantName", |
| | | "createTime", |
| | | "applyTime", |
| | | "finishTime", |
| | | "title", |
| | | "isApprove", |
| | | "unread", |
| | | "currentLevel", |
| | | "newsStatus", |
| | | ]; |
| | | |
| | | /** |
| | | * ä»è¡æ°æ® formConfig è§£æå段å®ä¹ä¸å¡«æ¥å¼ï¼å¹¶éºå¹³å°è¡ä¸ä¾ä¸»è¡¨ prop ç»å®ï¼å±ç¤ºç¨æ ¼å¼åå¼ï¼ |
| | | */ |
| | | export function enrichInstanceRowFromFormConfig(row, caches) { |
| | | const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row); |
| | | const formDisplay = {}; |
| | | const displayRow = { |
| | | ...row, |
| | | formFieldDefs: fields, |
| | | formPayload, |
| | | templateSnapshot: row.templateSnapshot || templateSnapshot, |
| | | formDisplay, |
| | | }; |
| | | |
| | | for (const f of fields) { |
| | | if (!f?.key || DEFAULT_EXCLUDE_KEYS.has(f.key)) continue; |
| | | const val = formPayload[f.key]; |
| | | let text = formatFieldDisplayValue(f, val, caches); |
| | | if ( |
| | | text === String(val) && |
| | | row?.applicantName && |
| | | (f.label === "ç³è¯·äºº" || f.key === "applicant" || f.key === "applicantName") |
| | | ) { |
| | | const idMatch = |
| | | String(val) === String(row.applicantId) || |
| | | String(val) === String(row.applicantNo); |
| | | if (idMatch) text = row.applicantName; |
| | | } |
| | | formDisplay[f.key] = text; |
| | | displayRow[f.key] = text; |
| | | } |
| | | |
| | | for (const key of PRESERVE_INSTANCE_FIELDS) { |
| | | if (row[key] !== undefined) displayRow[key] = row[key]; |
| | | } |
| | | |
| | | return displayRow; |
| | | } |
| | | |
| | | /** |
| | | * ä»å表é¦è¡ formConfig çæä¸»è¡¨å¨æåï¼label åèªæ¨¡æ¿å段 labelï¼ |
| | | */ |
| | | export function getFormConfigFieldColumns(firstRow, { excludeKeys = DEFAULT_EXCLUDE_KEYS } = {}) { |
| | | const fields = (firstRow?.formFieldDefs || []).filter( |
| | | (f) => f?.key && !excludeKeys.has(f.key) |
| | | ); |
| | | return fields.map((f) => ({ |
| | | label: f.label || f.key, |
| | | prop: f.key, |
| | | minWidth: f.type === "textarea" ? 200 : f.type === "datetimerange" ? 160 : 120, |
| | | showOverflowTooltip: true, |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * ä¸å¡ç³è¯·ä¸»è¡¨åï¼åºå®å + formConfig 卿å + 审æ¹ç¶æ + æä½ |
| | | */ |
| | | export function buildInstanceTableColumns(tableDataRef, buildTableActions, options = {}) { |
| | | const { |
| | | moduleKey, |
| | | excludeKeys = DEFAULT_EXCLUDE_KEYS, |
| | | beforeFormColumns = [], |
| | | extraColumns = [], |
| | | afterFormColumns = [], |
| | | actionWidth = 260, |
| | | } = options; |
| | | |
| | | const leadingCols = |
| | | moduleKey && INSTANCE_NO_SEARCH_MODULE_KEYS.has(moduleKey) |
| | | ? [INSTANCE_NO_TABLE_COLUMN] |
| | | : []; |
| | | |
| | | return computed(() => { |
| | | const formCols = getFormConfigFieldColumns(tableDataRef.value?.[0], { excludeKeys }); |
| | | return [ |
| | | ...leadingCols, |
| | | ...beforeFormColumns, |
| | | ...formCols, |
| | | ...extraColumns, |
| | | ...afterFormColumns, |
| | | { label: "å建æ¶é´", prop: "createTime", width: 170 }, |
| | | { |
| | | label: "审æ¹ç¶æ", |
| | | prop: "approvalStatus", |
| | | width: 110, |
| | | dataType: "tag", |
| | | formatData: (v) => businessApprovalStatusLabel(v), |
| | | formatType: (v) => businessApprovalStatusTagType(v), |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: actionWidth, |
| | | operation: buildTableActions(), |
| | | }, |
| | | ]; |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { APPROVAL_MODULE_KEYS } from "./approvalModuleRegistry.js"; |
| | | |
| | | /** æ¯æå®¡æ¹åå·æ¥è¯¢/主表å±ç¤ºç审æ¹ç³è¯·æ¨¡å */ |
| | | export const INSTANCE_NO_SEARCH_MODULE_KEYS = new Set([ |
| | | APPROVAL_MODULE_KEYS.REGULAR, |
| | | APPROVAL_MODULE_KEYS.TRANSFER, |
| | | APPROVAL_MODULE_KEYS.WORK_HANDOVER, |
| | | APPROVAL_MODULE_KEYS.LEAVE, |
| | | APPROVAL_MODULE_KEYS.OVERTIME, |
| | | ]); |
| | | |
| | | export const INSTANCE_NO_TABLE_COLUMN = { |
| | | label: "审æ¹åå·", |
| | | prop: "instanceNo", |
| | | width: 170, |
| | | showOverflowTooltip: true, |
| | | }; |
| | | |
| | | /** æå¹³å为 Spring GET å¯ç»å®ç queryï¼approvalInstanceDto.xxxï¼å¿ç¨æ¹æ¬å·ï¼ */ |
| | | export function appendDotNotationQuery(target, prefix, fields) { |
| | | if (!fields || typeof fields !== "object") return; |
| | | for (const [key, value] of Object.entries(fields)) { |
| | | if (value == null || value === "") continue; |
| | | target[`${prefix}.${key}`] = value; |
| | | } |
| | | } |
| | | |
| | | function pickApplicantFromSearchForm(searchForm = {}) { |
| | | const out = {}; |
| | | const sf = searchForm || {}; |
| | | const name = (sf.applicantName || "").trim(); |
| | | const kw = (sf.applicantKeyword || "").trim(); |
| | | const id = sf.applicantId; |
| | | |
| | | if (name) out.applicantName = name; |
| | | if (kw) { |
| | | out.applicantName = kw; |
| | | if (/^\d+$/.test(kw)) out.applicantId = Number(kw); |
| | | } |
| | | if (id != null && id !== "") { |
| | | out.applicantId = typeof id === "number" ? id : Number(id) || id; |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | function pickInstanceNoFromSearchForm(searchForm = {}) { |
| | | const no = (searchForm?.instanceNo || "").trim(); |
| | | return no ? { instanceNo: no } : {}; |
| | | } |
| | | |
| | | /** ç»è£
approvalInstanceDto æ¥è¯¢ç段ï¼ç³è¯·äºº + 审æ¹åå·ï¼ */ |
| | | export function buildApprovalInstanceSearchDto(searchForm = {}, extraParams = {}) { |
| | | const dto = { |
| | | ...(extraParams && typeof extraParams === "object" ? extraParams : {}), |
| | | }; |
| | | Object.assign(dto, pickApplicantFromSearchForm(searchForm)); |
| | | Object.assign(dto, pickInstanceNoFromSearchForm(searchForm)); |
| | | delete dto.createTime; |
| | | delete dto.createTimeStart; |
| | | delete dto.createTimeEnd; |
| | | return dto; |
| | | } |
| | | |
| | | function getRowPayloadValue(row, keys) { |
| | | const keyList = Array.isArray(keys) ? keys : [keys]; |
| | | const payload = row?.formPayload || {}; |
| | | for (const k of keyList) { |
| | | if (row?.[k] != null && row[k] !== "") return row[k]; |
| | | if (payload[k] != null && payload[k] !== "") return payload[k]; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | function matchApplicantKeyword(row, keyword) { |
| | | const kw = (keyword || "").trim().toLowerCase(); |
| | | if (!kw) return true; |
| | | const parts = [ |
| | | row?.applicantName, |
| | | row?.applicantNo, |
| | | row?.applicantId, |
| | | getRowPayloadValue(row, ["applicant", "applicantName", "applicantId"]), |
| | | ] |
| | | .filter((v) => v != null && v !== "") |
| | | .map((v) => String(v).toLowerCase()); |
| | | return parts.some((p) => p.includes(kw)); |
| | | } |
| | | |
| | | function matchApplicantId(row, applicantId) { |
| | | if (applicantId == null || applicantId === "") return true; |
| | | const id = String(applicantId); |
| | | if (row?.applicantId != null && String(row.applicantId) === id) return true; |
| | | const payloadApplicant = getRowPayloadValue(row, [ |
| | | "applicant", |
| | | "applicantId", |
| | | "applicantUserId", |
| | | ]); |
| | | return String(payloadApplicant) === id; |
| | | } |
| | | |
| | | function matchSelectValue(row, keys, expected) { |
| | | if (!expected) return true; |
| | | const raw = getRowPayloadValue(row, keys); |
| | | return String(raw) === String(expected); |
| | | } |
| | | |
| | | function matchInstanceNo(row, instanceNo) { |
| | | const kw = (instanceNo || "").trim().toLowerCase(); |
| | | if (!kw) return true; |
| | | const parts = [row?.instanceNo, row?.bizId] |
| | | .filter((v) => v != null && v !== "") |
| | | .map((v) => String(v).toLowerCase()); |
| | | return parts.some((p) => p.includes(kw)); |
| | | } |
| | | |
| | | /** æ¯å¦åå¨å表ç鿡件ï¼ç³è¯·äºº / 审æ¹åå·ï¼ */ |
| | | export function hasActiveModuleSearch(moduleKey, searchForm = {}) { |
| | | const sf = searchForm || {}; |
| | | if ((sf.instanceNo || "").trim()) return true; |
| | | if ((sf.applicantKeyword || "").trim()) return true; |
| | | if ((sf.applicantName || "").trim()) return true; |
| | | return sf.applicantId != null && sf.applicantId !== ""; |
| | | } |
| | | |
| | | /** æç³è¯·äººã审æ¹åå·åå端å
åºçé */ |
| | | export function filterInstanceRowsByModuleSearch(moduleKey, rows, searchForm = {}) { |
| | | const sf = searchForm || {}; |
| | | const list = Array.isArray(rows) ? rows : []; |
| | | if (!hasActiveModuleSearch(moduleKey, sf)) return list; |
| | | |
| | | return list.filter( |
| | | (row) => |
| | | matchInstanceNo(row, sf.instanceNo) && |
| | | matchApplicantId(row, sf.applicantId) && |
| | | matchApplicantKeyword(row, sf.applicantKeyword || sf.applicantName) |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { matchBusinessTypeValue } from "../approve-list/approveListConstants.js"; |
| | | |
| | | /** |
| | | * åä¸å¡æ¨¡åä¸å®¡æ¹æ¨¡æ¿ç±»åçæ å°ï¼é
ç½®åå
¥å£ï¼ |
| | | * businessType ä¸å端 TypeEnums / listPage 约å®ä¸è´ï¼åæ»æä¸¾å¼ï¼ |
| | | */ |
| | | export const APPROVAL_MODULE_KEYS = { |
| | | REGULAR: "regular", |
| | | TRANSFER: "transfer", |
| | | RESIGN: "resign", |
| | | WORK_HANDOVER: "work_handover", |
| | | LEAVE: "leave", |
| | | OVERTIME: "overtime", |
| | | TRAVEL_REIMBURSE: "travel_reimburse", |
| | | COST_REIMBURSE: "cost_reimburse", |
| | | ENTERPRISE_NEWS: "enterprise_news", |
| | | }; |
| | | |
| | | /** 审æ¹å®ä¾ listPage / ä¿å 使ç¨ç businessType æä¸¾ */ |
| | | export const APPROVAL_BUSINESS_TYPE = { |
| | | [APPROVAL_MODULE_KEYS.REGULAR]: 10, |
| | | [APPROVAL_MODULE_KEYS.TRANSFER]: 11, |
| | | [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: 13, |
| | | [APPROVAL_MODULE_KEYS.LEAVE]: 14, |
| | | [APPROVAL_MODULE_KEYS.OVERTIME]: 15, |
| | | [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: 16, |
| | | [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: 17, |
| | | [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: 18, |
| | | }; |
| | | |
| | | /** @type {Record<string, import('./approvalModuleRegistry.js').ApprovalModuleConfig>} */ |
| | | export const APPROVAL_MODULE_REGISTRY = { |
| | | [APPROVAL_MODULE_KEYS.REGULAR]: { |
| | | label: "转æ£ç³è¯·", |
| | | approvalType: "regular", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.REGULAR], |
| | | typeLabels: ["转æ£", "转æ£ç³è¯·"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.TRANSFER]: { |
| | | label: "è°å²ç³è¯·", |
| | | approvalType: "transfer", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.TRANSFER], |
| | | typeLabels: ["è°å²", "è°å¨", "è°å²ç³è¯·", "è°å¨ç³è¯·"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.RESIGN]: { |
| | | label: "离èç³è¯·", |
| | | approvalType: "resign", |
| | | typeLabels: ["离è", "离èç³è¯·", "离è审æ¹"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: { |
| | | label: "å·¥ä½äº¤æ¥", |
| | | approvalType: "work_handover", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.WORK_HANDOVER], |
| | | typeLabels: ["å·¥ä½äº¤æ¥", "交æ¥", "å·¥ä½äº¤æ¥å®¡æ¹"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.LEAVE]: { |
| | | label: "请åç³è¯·", |
| | | approvalType: "leave", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.LEAVE], |
| | | typeLabels: ["请å", "请åç³è¯·", "请å审æ¹"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.OVERTIME]: { |
| | | label: "å çç³è¯·", |
| | | approvalType: "overtime", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.OVERTIME], |
| | | typeLabels: ["å ç", "å çç³è¯·", "å ç审æ¹"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: { |
| | | label: "å·®æ
æ¥é", |
| | | approvalType: "travel_reimburse", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE], |
| | | typeLabels: ["å·®æ
", "å·®æ
æ¥é", "åºå·®æ¥é"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: { |
| | | label: "è´¹ç¨æ¥é", |
| | | approvalType: "cost_reimburse", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.COST_REIMBURSE], |
| | | typeLabels: ["è´¹ç¨", "è´¹ç¨æ¥é"], |
| | | }, |
| | | [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: { |
| | | label: "ä¼ä¸æ°é»", |
| | | approvalType: "enterprise_news", |
| | | businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS], |
| | | typeLabels: ["ä¼ä¸æ°é»", "æ°é»", "æ°é»åå¸"], |
| | | }, |
| | | }; |
| | | |
| | | /** |
| | | * @typedef {object} ApprovalModuleConfig |
| | | * @property {string} label |
| | | * @property {string} [approvalType] |
| | | * @property {string|number} [businessType] |
| | | * @property {string[]} [typeLabels] |
| | | */ |
| | | |
| | | export function getApprovalModuleConfig(moduleKey) { |
| | | if (!moduleKey) return null; |
| | | return APPROVAL_MODULE_REGISTRY[moduleKey] || null; |
| | | } |
| | | |
| | | /** å表æ¥è¯¢ businessTypeï¼ä¼å
é
ç½®æä¸¾ï¼ä¸ååé approvalType åç¬¦ä¸²ï¼ */ |
| | | export function getModuleListBusinessType(moduleKey) { |
| | | const cfg = getApprovalModuleConfig(moduleKey); |
| | | if (!cfg) return ""; |
| | | if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType; |
| | | return APPROVAL_BUSINESS_TYPE[moduleKey] ?? ""; |
| | | } |
| | | |
| | | /** ä» TypeEnums è§£ææ¬æ¨¡å businessTypeï¼å·²é
ç½®æä¸¾æ¶ç´æ¥è¿å */ |
| | | export function resolveModuleBusinessType(moduleKey, typeOptions = []) { |
| | | const cfg = getApprovalModuleConfig(moduleKey); |
| | | if (!cfg) return null; |
| | | |
| | | const fixed = getModuleListBusinessType(moduleKey); |
| | | if (fixed != null && fixed !== "") return fixed; |
| | | |
| | | const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean); |
| | | const hitByLabel = (typeOptions || []).find((opt) => { |
| | | const optLabel = String(opt?.label || opt?.name || "").trim(); |
| | | if (!optLabel) return false; |
| | | return labels.some( |
| | | (l) => optLabel === l || optLabel.includes(l) || l.includes(optLabel) |
| | | ); |
| | | }); |
| | | if (hitByLabel?.value != null && hitByLabel.value !== "") return hitByLabel.value; |
| | | |
| | | return cfg.approvalType || null; |
| | | } |
| | | |
| | | /** å表/模æ¿è¿æ»¤ç¨ç businessType éå */ |
| | | export function getModuleMatchingBusinessTypes(moduleKey, typeOptions = []) { |
| | | const cfg = getApprovalModuleConfig(moduleKey); |
| | | if (!cfg) return []; |
| | | |
| | | const fixed = getModuleListBusinessType(moduleKey); |
| | | if (fixed != null && fixed !== "") return [fixed]; |
| | | |
| | | const values = new Set(); |
| | | const primary = resolveModuleBusinessType(moduleKey, typeOptions); |
| | | if (primary != null && primary !== "") values.add(primary); |
| | | |
| | | const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean); |
| | | for (const opt of typeOptions || []) { |
| | | const optLabel = String(opt?.label || opt?.name || "").trim(); |
| | | if (!optLabel) continue; |
| | | const matched = labels.some( |
| | | (l) => optLabel === l || optLabel.includes(l) || l.includes(optLabel) |
| | | ); |
| | | if (matched && opt.value != null && opt.value !== "") { |
| | | values.add(opt.value); |
| | | } |
| | | } |
| | | return [...values]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { |
| | | mapAttachmentsFromApi, |
| | | mapTemplateFromApi, |
| | | unwrapTemplateDetail, |
| | | } from "../approve-template/approveTemplateConstants.js"; |
| | | import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js"; |
| | | import { |
| | | createEmptySubmitForm, |
| | | validateSubmitFlowNodes, |
| | | } from "../approve-list/approveListConstants.js"; |
| | | |
| | | export function attachmentDisplayName(file) { |
| | | return ( |
| | | file?.fileName || |
| | | file?.originalFilename || |
| | | file?.name || |
| | | file?.blobName || |
| | | "éä»¶" |
| | | ); |
| | | } |
| | | |
| | | /** æ¥å£è¯¦æ
â æäº¤ç»å®å¿«ç
§ï¼å«æµç¨ãéä»¶ãå¡«æ¥é¡¹ï¼ */ |
| | | export function buildTemplateBindingFromDetail(detailRow) { |
| | | const mapped = mapTemplateFromApi(unwrapTemplateDetail(detailRow)); |
| | | const templateAttachments = mapAttachmentsFromApi(mapped); |
| | | const tpl = { |
| | | ...buildSubmitTemplateFromRow(mapped), |
| | | templateId: mapped.id, |
| | | businessType: mapped.businessType, |
| | | storageBlobDTOs: templateAttachments, |
| | | }; |
| | | const base = createEmptySubmitForm(String(mapped.id ?? ""), tpl, mapped.flowNodes); |
| | | return { |
| | | templateId: mapped.id, |
| | | templateName: mapped.templateName || tpl.label || "", |
| | | businessType: mapped.businessType ?? "", |
| | | templateSnapshot: tpl, |
| | | formFieldDefs: tpl.fields || [], |
| | | formPayload: base.formPayload, |
| | | flowNodes: base.flowNodes, |
| | | templateAttachments: JSON.parse(JSON.stringify(templateAttachments)), |
| | | storageBlobDTOs: [], |
| | | }; |
| | | } |
| | | |
| | | /** æ ¹æ®æ¨¡æ¿ fields çæ el-form rulesï¼prop 为 formPayload.xxxï¼ */ |
| | | export function buildFormPayloadRules(fields = []) { |
| | | const rules = {}; |
| | | (fields || []).forEach((f) => { |
| | | if (!f.required || !f.key) return; |
| | | const prop = `formPayload.${f.key}`; |
| | | if (f.type === "number") { |
| | | rules[prop] = [{ required: true, message: `请填å${f.label}`, trigger: "blur" }]; |
| | | } else if (f.type === "datetimerange" || f.type === "date" || f.type === "select") { |
| | | rules[prop] = [{ required: true, message: `è¯·éæ©${f.label}`, trigger: "change" }]; |
| | | } else { |
| | | rules[prop] = [{ required: true, message: `请填å${f.label}`, trigger: "blur" }]; |
| | | } |
| | | }); |
| | | return rules; |
| | | } |
| | | |
| | | /** æ ¡éªæ¨¡æ¿ç»å®ï¼å®¡æ¹æµç¨ï¼éä»¶éå¡«ï¼ç±ç¨æ·èªè¡ä¸ä¼ ï¼ */ |
| | | export function validateTemplateBinding({ flowNodes }) { |
| | | const flowCheck = validateSubmitFlowNodes(flowNodes); |
| | | if (!flowCheck.ok) return flowCheck; |
| | | return { ok: true, nodes: flowCheck.nodes }; |
| | | } |
| | | |
| | | /** åå¹¶ç»å®ç»æå°ä¸å¡è¡¨å对象ï¼å段å坿ä¸å¡è¦çï¼ */ |
| | | export function applyBindingToForm(target, binding, fieldMap = {}) { |
| | | if (!target || !binding) return target; |
| | | const map = { |
| | | templateId: "templateId", |
| | | templateName: "templateName", |
| | | businessType: "businessType", |
| | | templateSnapshot: "templateSnapshot", |
| | | formFieldDefs: "formFieldDefs", |
| | | formPayload: "formPayload", |
| | | flowNodes: "flowNodes", |
| | | templateAttachments: "templateAttachments", |
| | | storageBlobDTOs: "storageBlobDTOs", |
| | | ...fieldMap, |
| | | }; |
| | | Object.entries(map).forEach(([srcKey, destKey]) => { |
| | | if (binding[srcKey] !== undefined) { |
| | | target[destKey] = binding[srcKey]; |
| | | } |
| | | }); |
| | | return target; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- ä¸å®¡æ¹å表详æ
å¼¹çªä¸è´ --> |
| | | <template> |
| | | <el-dialog |
| | | v-model="visible" |
| | | :title="title" |
| | | width="920px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-detail-dialog" |
| | | @closed="emit('closed')" |
| | | > |
| | | <div class="approve-detail-body"> |
| | | <ApproveDetailPanel :row="row" /> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title"> |
| | | å®¡æ¹æµç¨ï¼{{ row?.tasks?.length || row?.flowNodes?.length || 0 }} é¡¹ï¼ |
| | | </div> |
| | | <InstanceFlowDisplay :tasks="row?.tasks" :nodes="row?.flowNodes" /> |
| | | </div> |
| | | <div class="detail-block"> |
| | | <div class="detail-block-title">审æ¹è®°å½</div> |
| | | <el-timeline v-if="row?.approvalRecords?.length" class="approve-record-timeline"> |
| | | <el-timeline-item |
| | | v-for="(rec, i) in row.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="canEditRow(row)" @click="emit('edit', row)">ä¿® æ¹</el-button> |
| | | <el-button @click="visible = false">å
³ é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { canEditBusinessInstanceRow } from "../../approve-list/approveListConstants.js"; |
| | | import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js"; |
| | | import ApproveDetailPanel from "../../approve-list/components/ApproveDetailPanel.vue"; |
| | | import InstanceFlowDisplay from "../../approve-list/components/InstanceFlowDisplay.vue"; |
| | | |
| | | function canEditRow(row) { |
| | | return canEditBusinessInstanceRow(row); |
| | | } |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | row: { type: Object, default: () => ({}) }, |
| | | title: { type: String, default: "审æ¹è¯¦æ
" }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "edit", "closed"]); |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (v) => emit("update:modelValue", v), |
| | | }); |
| | | |
| | | function approvalActionLabel(result) { |
| | | if (result === "approved") return "éè¿"; |
| | | if (result === "rejected") return "驳å"; |
| | | return "å¾
å¤ç"; |
| | | } |
| | | |
| | | function formatRecordTime(time) { |
| | | return formatDisplayTime(time) || "â"; |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .approve-detail-dialog :deep(.el-dialog__body) { |
| | | padding-top: 16px; |
| | | max-height: 70vh; |
| | | overflow-y: auto; |
| | | } |
| | | .approve-detail-body .detail-block { |
| | | margin-top: 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-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; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- ä¸å®¡æ¹å表æäº¤/ä¿®æ¹å¼¹çªï¼ç¬¬ä¸æ¥ï¼ä¸è´ --> |
| | | <template> |
| | | <el-dialog |
| | | v-model="visible" |
| | | :title="title" |
| | | width="720px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="approve-submit-dialog" |
| | | @closed="emit('closed')" |
| | | > |
| | | <el-form ref="innerFormRef" :model="form" :rules="rules" label-width="120px"> |
| | | <el-form-item v-if="isEdit" label="审æ¹ç±»å"> |
| | | <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate?.approvalType)"> |
| | | {{ activeTemplate?.label || form.templateName || "â" }} |
| | | </span> |
| | | </el-form-item> |
| | | <slot name="before" :form="form" :fields="fields" /> |
| | | <ApprovalTemplateFormSection |
| | | :active-template="activeTemplate" |
| | | :fields="fields" |
| | | :form-payload="form.formPayload" |
| | | v-model:flow-nodes="form.flowNodes" |
| | | v-model:attachments="form.storageBlobDTOs" |
| | | :template-attachments="form.templateAttachments" |
| | | :user-options="userOptions" |
| | | :show-template-name="!isEdit" |
| | | :allow-change-template="false" |
| | | :flow-attachments-only="flowAttachmentsOnly" |
| | | :flow-only="flowOnly" |
| | | /> |
| | | <slot name="after" :form="form" :fields="fields" /> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" :loading="saving" @click="handleSubmitClick"> |
| | | {{ isEdit ? "ä¿ å" : "æ 交" }} |
| | | </el-button> |
| | | <el-button @click="visible = false">å æ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { approvalTypeStyle } from "../../approve-list/approveListConstants.js"; |
| | | import ApprovalTemplateFormSection from "./ApprovalTemplateFormSection.vue"; |
| | | |
| | | const innerFormRef = ref(null); |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | title: { type: String, default: "" }, |
| | | form: { type: Object, required: true }, |
| | | rules: { type: Object, default: () => ({}) }, |
| | | fields: { type: Array, default: () => [] }, |
| | | activeTemplate: { type: Object, default: null }, |
| | | userOptions: { type: Array, default: () => [] }, |
| | | isEdit: { type: Boolean, default: false }, |
| | | saving: { type: Boolean, default: false }, |
| | | formRef: { type: Object, default: null }, |
| | | /** å¡«æ¥é¡¹ç± before ææ§½åç¬æ¸²ææ¶è®¾ä¸º true */ |
| | | flowAttachmentsOnly: { type: Boolean, default: false }, |
| | | flowOnly: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "submit", "closed"]); |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (v) => emit("update:modelValue", v), |
| | | }); |
| | | |
| | | watch( |
| | | innerFormRef, |
| | | (el) => { |
| | | if (props.formRef) props.formRef.value = el; |
| | | }, |
| | | { flush: "post" } |
| | | ); |
| | | |
| | | watch(visible, (v) => { |
| | | if (!v && props.formRef) props.formRef.value = null; |
| | | }); |
| | | |
| | | async function handleSubmitClick() { |
| | | if (!innerFormRef.value) { |
| | | ElMessage.warning("è¡¨åæªå°±ç»ªï¼è¯·ç¨ååè¯"); |
| | | return; |
| | | } |
| | | try { |
| | | await innerFormRef.value.validate(); |
| | | } catch { |
| | | ElMessage.warning("请å®å表åå¿
填项ååä¿å"); |
| | | return; |
| | | } |
| | | emit("submit"); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .approve-type-cell { |
| | | display: inline-block; |
| | | padding: 2px 10px; |
| | | border-radius: 4px; |
| | | font-size: 13px; |
| | | line-height: 1.5; |
| | | } |
| | | .approve-submit-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ä¸å¡æ¨¡åãæ°å¢ãæ¶å¯¼å
¥å®¡æ¹æ¨¡æ¿ï¼åºå® moduleKeyï¼ä»
å±ç¤ºè¯¥ç±»å䏿¨¡æ¿ï¼ |
| | | |
| | | ç¨æ³ï¼ |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="visible" |
| | | module-key="regular" |
| | | @confirm="onTemplateBound" |
| | | /> |
| | | --> |
| | | <template> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | :width="step === formStep ? 720 : 640" |
| | | append-to-body |
| | | class="approval-template-bind-dialog" |
| | | @closed="onClosed" |
| | | > |
| | | <template v-if="step === 1"> |
| | | <div v-loading="templatesLoading || confirming"> |
| | | <ApprovalTemplatePicker |
| | | :cards="templateCards" |
| | | :loading="false" |
| | | :hint="pickerHint" |
| | | @pick="onPickTemplate" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <div v-loading="templatesLoading"> |
| | | <el-form |
| | | ref="formRef" |
| | | :model="bindingForm" |
| | | :rules="mergedRules" |
| | | label-width="120px" |
| | | > |
| | | <ApprovalTemplateFormSection |
| | | :active-template="activeTemplate" |
| | | :fields="formFields" |
| | | :form-payload="bindingForm.formPayload" |
| | | v-model:flow-nodes="bindingForm.flowNodes" |
| | | v-model:attachments="bindingForm.storageBlobDTOs" |
| | | :template-attachments="bindingForm.templateAttachments" |
| | | :user-options="flowUserOptions" |
| | | allow-change-template |
| | | @change-template="step = 1" |
| | | /> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <template #footer> |
| | | <el-button v-if="step === formStep" type="primary" :loading="confirming" @click="onConfirm"> |
| | | ç¡® å® |
| | | </el-button> |
| | | <el-button v-if="step === formStep" @click="step = 1">é鿍¡æ¿</el-button> |
| | | <el-button @click="dialogVisible = false">{{ step === 1 ? "å æ¶" : "å
³ é" }}</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import ApprovalTemplatePicker from "./ApprovalTemplatePicker.vue"; |
| | | import ApprovalTemplateFormSection from "./ApprovalTemplateFormSection.vue"; |
| | | import { useApprovalTemplateBinding } from "../useApprovalTemplateBinding.js"; |
| | | import { useFlowUserOptions } from "../useFlowUserOptions.js"; |
| | | import { getApprovalModuleConfig } from "../approvalModuleRegistry.js"; |
| | | |
| | | const props = defineProps({ |
| | | visible: { type: Boolean, default: false }, |
| | | /** approvalModuleRegistry ä¸ç moduleKey */ |
| | | moduleKey: { type: String, required: true }, |
| | | /** 为 true æ¶é模æ¿åç´æ¥ç¡®è®¤ï¼è·³è¿ã确认审æ¹ä¿¡æ¯ãå¡«æ¥æ¥éª¤ */ |
| | | skipFormConfirm: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:visible", "confirm", "closed"]); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.visible, |
| | | set: (v) => emit("update:visible", v), |
| | | }); |
| | | |
| | | const { |
| | | step, |
| | | bindingForm, |
| | | templateCards, |
| | | activeTemplate, |
| | | formFields, |
| | | formRules, |
| | | templatesLoading, |
| | | loadTemplates, |
| | | resetBinding, |
| | | pickTemplate, |
| | | validateBinding, |
| | | getBindingPayload, |
| | | moduleConfig, |
| | | } = useApprovalTemplateBinding({ moduleKey: props.moduleKey, mode: "module" }); |
| | | |
| | | const formStep = 2; |
| | | const formRef = ref(); |
| | | const confirming = ref(false); |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | const mergedRules = computed(() => ({ ...formRules.value })); |
| | | |
| | | const dialogTitle = computed(() => { |
| | | const label = moduleConfig.value?.label || "审æ¹"; |
| | | return step.value === 1 ? `éæ©${label}模æ¿` : `确认${label}审æ¹ä¿¡æ¯`; |
| | | }); |
| | | |
| | | const pickerHint = computed( |
| | | () => `è¯·éæ©ã${moduleConfig.value?.label || "â"}ãç±»åä¸å·²å¯ç¨çå®¡æ¹æ¨¡æ¿ï¼å®¡æ¹æµç¨å°èªå¨å¸¦å
¥ã` |
| | | ); |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | async (v) => { |
| | | if (!v) return; |
| | | resetBinding(); |
| | | step.value = 1; |
| | | await Promise.all([loadTemplates(), loadFlowUsers()]); |
| | | const cfg = getApprovalModuleConfig(props.moduleKey); |
| | | if (!cfg) { |
| | | ElMessage.warning(`æªé
置模åã${props.moduleKey}ãï¼è¯·æ£æ¥ approvalModuleRegistry`); |
| | | return; |
| | | } |
| | | if (!templateCards.value.length) { |
| | | ElMessage.warning( |
| | | `ã${cfg.label}ã䏿æ å·²å¯ç¨çå®¡æ¹æ¨¡æ¿ï¼è¯·å
å¨å®¡æ¹æ¨¡æ¿ç®¡çä¸å建并å¯ç¨å¯¹åºç±»åçæ¨¡æ¿` |
| | | ); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | async function onPickTemplate(card) { |
| | | const ok = await pickTemplate(card); |
| | | if (!ok) return; |
| | | if (props.skipFormConfirm) { |
| | | step.value = 1; |
| | | await onConfirm(); |
| | | return; |
| | | } |
| | | step.value = formStep; |
| | | } |
| | | |
| | | async function onConfirm() { |
| | | confirming.value = true; |
| | | try { |
| | | const check = await validateBinding(props.skipFormConfirm ? null : formRef.value); |
| | | if (!check.ok) { |
| | | if (check.message) ElMessage.warning(check.message); |
| | | return; |
| | | } |
| | | emit("confirm", { ...getBindingPayload(), flowNodes: check.nodes }); |
| | | dialogVisible.value = false; |
| | | } finally { |
| | | confirming.value = false; |
| | | } |
| | | } |
| | | |
| | | function onClosed() { |
| | | resetBinding(); |
| | | emit("closed"); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .approval-template-bind-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- 模æ¿ç»å®è¡¨ååºï¼å¡«æ¥é¡¹ + å®¡æ¹æµç¨ + éä»¶ï¼é¡»æå¨å¤å± el-form ä¸ï¼ --> |
| | | <template> |
| | | <template v-if="activeTemplate"> |
| | | <el-form-item |
| | | v-if="showTemplateName && !hideTemplateName && !flowAttachmentsOnly && !flowOnly" |
| | | label="å®¡æ¹æ¨¡æ¿" |
| | | > |
| | | <span class="template-name">{{ activeTemplate.label }}</span> |
| | | <el-button v-if="allowChangeTemplate" type="primary" link class="ml12" @click="emit('change-template')"> |
| | | æ´æ¢æ¨¡æ¿ |
| | | </el-button> |
| | | </el-form-item> |
| | | |
| | | <FormPayloadFields |
| | | v-if="!hideFormFields && !flowAttachmentsOnly && !flowOnly" |
| | | :fields="fields" |
| | | :form-payload="formPayload" |
| | | /> |
| | | |
| | | <el-form-item label="å®¡æ¹æµç¨" required> |
| | | <TemplateFlowEditor |
| | | v-model="flowNodesModel" |
| | | :user-options="userOptions" |
| | | :readonly="!flowEditable" |
| | | /> |
| | | <p class="section-tip"> |
| | | {{ |
| | | flowEditable |
| | | ? "æµç¨ä¸å®¡æ¹äººç±æ¨¡æ¿é¢ç½®ï¼å¯æéå¾®è°èç¹å®¡æ¹äººã" |
| | | : "æµç¨ä¸å®¡æ¹äººç±æé模æ¿åºå®ï¼ä¸å¯ä¿®æ¹ã" |
| | | }} |
| | | </p> |
| | | </el-form-item> |
| | | |
| | | <el-form-item v-if="!flowOnly && templateAttachments.length" label="模æ¿åè"> |
| | | <el-tag |
| | | v-for="(f, i) in templateAttachments" |
| | | :key="`tpl-${i}`" |
| | | class="attachment-tag" |
| | | type="info" |
| | | effect="plain" |
| | | > |
| | | {{ attachmentDisplayName(f) }} |
| | | </el-tag> |
| | | <p class="section-tip">以ä¸ä¸ºæ¨¡æ¿é带æä»¶ï¼ä»
ä¾åèï¼æäº¤é件请å¨ä¸æ¹ä¸ä¼ ã</p> |
| | | </el-form-item> |
| | | |
| | | <el-form-item v-if="!flowOnly" label="éä»¶"> |
| | | <FileUpload |
| | | v-model:file-list="attachmentsModel" |
| | | :limit="uploadLimit" |
| | | button-text="ç¹å»éæ©æä»¶" |
| | | /> |
| | | <p class="section-tip">éå¡«ï¼å¯ä¸ä¼ ä¸ç³è¯·ç¸å
³çè¯´æææã</p> |
| | | </el-form-item> |
| | | </template> |
| | | <el-empty v-else description="请å
éæ©å®¡æ¹æ¨¡æ¿" :image-size="64" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import FileUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | import TemplateFlowEditor from "../../approve-template/components/TemplateFlowEditor.vue"; |
| | | import FormPayloadFields from "../../approve-list/components/FormPayloadFields.vue"; |
| | | import { attachmentDisplayName } from "../approvalTemplateBindingUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | activeTemplate: { type: Object, default: null }, |
| | | fields: { type: Array, default: () => [] }, |
| | | formPayload: { type: Object, required: true }, |
| | | flowNodes: { type: Array, default: () => [] }, |
| | | /** ç¨æ·èªè¡ä¸ä¼ çéä»¶ */ |
| | | attachments: { type: Array, default: () => [] }, |
| | | /** 模æ¿é¢ç½®éä»¶ï¼åªè¯»å±ç¤ºï¼ */ |
| | | templateAttachments: { type: Array, default: () => [] }, |
| | | userOptions: { type: Array, default: () => [] }, |
| | | showTemplateName: { type: Boolean, default: true }, |
| | | allowChangeTemplate: { type: Boolean, default: true }, |
| | | /** 为 true æ¶ä¸å±ç¤ºæ¨¡æ¿èªå®ä¹å¡«æ¥é¡¹ï¼ä»
ä¿çå®¡æ¹æµç¨ä¸éä»¶ï¼ */ |
| | | hideFormFields: { type: Boolean, default: false }, |
| | | /** 为 true æ¶ä¸å±ç¤ºå®¡æ¹æ¨¡æ¿åç§°è¡ï¼ç±ç¶çº§ç½®é¡¶å±ç¤ºï¼ */ |
| | | hideTemplateName: { type: Boolean, default: false }, |
| | | /** 为 true æ¶ä»
å±ç¤ºå®¡æ¹æµç¨ä¸éä»¶ï¼å¡«æ¥é¡¹ç±ç¶çº§åç¬æ¸²æï¼ */ |
| | | flowAttachmentsOnly: { type: Boolean, default: false }, |
| | | /** 为 true æ¶ä»
å±ç¤ºå®¡æ¹æµç¨ï¼ä¸å±ç¤ºæ¨¡æ¿å¡«æ¥é¡¹ãéä»¶çï¼ */ |
| | | flowOnly: { type: Boolean, default: false }, |
| | | uploadLimit: { type: Number, default: 10 }, |
| | | /** 为 true æ¶å¯ç¼è¾æ¨¡æ¿é¢ç½®ç审æ¹äººï¼ä»
å®¡æ¹æ¨¡æ¿ç®¡ç页使ç¨ï¼ */ |
| | | flowEditable: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:flowNodes", "update:attachments", "change-template"]); |
| | | |
| | | const flowNodesModel = computed({ |
| | | get: () => props.flowNodes, |
| | | set: (v) => emit("update:flowNodes", v), |
| | | }); |
| | | |
| | | const attachmentsModel = computed({ |
| | | get: () => props.attachments, |
| | | set: (v) => emit("update:attachments", v), |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .template-name { |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | .ml12 { |
| | | margin-left: 12px; |
| | | } |
| | | .section-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin: 8px 0 0; |
| | | line-height: 1.5; |
| | | } |
| | | .attachment-tag { |
| | | margin: 0 8px 8px 0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å®¡æ¹æ¨¡æ¿å¡çéæ©ï¼æ businessType è¿æ»¤ï¼ --> |
| | | <template> |
| | | <div class="approval-template-picker"> |
| | | <p v-if="hint" class="picker-hint">{{ hint }}</p> |
| | | <div v-loading="loading" class="template-grid"> |
| | | <div |
| | | v-for="card in cards" |
| | | :key="card.key || card.id" |
| | | class="template-card" |
| | | @click="emit('pick', card)" |
| | | > |
| | | <span class="template-card-type" :style="typeStyle(card.approvalType)"> |
| | | {{ card.label }} |
| | | </span> |
| | | <span class="template-card-desc">{{ card.summaryPlaceholder }}</span> |
| | | </div> |
| | | <el-empty |
| | | v-if="!loading && !cards.length" |
| | | :description="emptyText" |
| | | :image-size="80" |
| | | class="template-empty" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { approvalTypeStyle } from "../../approve-list/approveListConstants.js"; |
| | | |
| | | defineProps({ |
| | | cards: { type: Array, default: () => [] }, |
| | | loading: { type: Boolean, default: false }, |
| | | hint: { type: String, default: "" }, |
| | | emptyText: { type: String, default: "该类å䏿æ å¯ç¨å®¡æ¹æ¨¡æ¿" }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["pick"]); |
| | | |
| | | function typeStyle(approvalType) { |
| | | return approvalTypeStyle(approvalType); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .picker-hint { |
| | | font-size: 13px; |
| | | color: var(--el-text-color-secondary); |
| | | margin: 0 0 16px; |
| | | } |
| | | .template-grid { |
| | | 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; |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: var(--radius-md, 8px); |
| | | cursor: pointer; |
| | | transition: border-color 0.2s, box-shadow 0.2s; |
| | | background: var(--el-fill-color-blank); |
| | | } |
| | | .template-card:hover { |
| | | border-color: var(--el-color-primary); |
| | | box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06)); |
| | | } |
| | | .template-card-type { |
| | | display: inline-block; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | margin-bottom: 8px; |
| | | } |
| | | .template-card-desc { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | line-height: 1.5; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { |
| | | deleteApprovalInstance, |
| | | listApprovalInstancePage, |
| | | saveApprovalInstance, |
| | | updateApprovalInstance, |
| | | } from "@/api/officeProcessAutomation/approvalInstance.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { |
| | | applyBindingToForm, |
| | | buildFormPayloadRules, |
| | | validateTemplateBinding, |
| | | } from "./approvalTemplateBindingUtils.js"; |
| | | import { |
| | | buildApprovalInstanceListParams, |
| | | buildEditFormFromInstanceRow, |
| | | buildInstanceDto, |
| | | canEditBusinessInstanceRow, |
| | | createEmptySubmitForm, |
| | | mapInstanceFromApi, |
| | | resolveInstanceFormFields, |
| | | unwrapInstancePage, |
| | | } from "../approve-list/approveListConstants.js"; |
| | | import { fetchBusinessTypeOptions } from "../approve-template/approveTemplateConstants.js"; |
| | | import { |
| | | collectOptionSourcesFromFields, |
| | | fetchSelectOptionCaches, |
| | | } from "../approve-template/selectOptionSource.js"; |
| | | import { enrichInstanceRowFromFormConfig } from "./approvalInstanceFormConfigTable.js"; |
| | | import { |
| | | filterInstanceRowsByModuleSearch, |
| | | hasActiveModuleSearch, |
| | | } from "./approvalInstanceListSearch.js"; |
| | | import { |
| | | getApprovalModuleConfig, |
| | | getModuleListBusinessType, |
| | | resolveModuleBusinessType, |
| | | } from "./approvalModuleRegistry.js"; |
| | | |
| | | /** |
| | | * ä¸å¡ç³è¯·é¡µå
±ç¨ï¼å®¡æ¹å®ä¾å表æ¥è¯¢ãæ°å¢/ä¿®æ¹ä¿åã详æ
/ç¼è¾å¼¹çªï¼ä¸å®¡æ¹å表ä¸è´ï¼ |
| | | * |
| | | * @param {object} options |
| | | * @param {string} options.moduleKey approvalModuleRegistry ä¸ç key |
| | | * @param {(row: object) => object} [options.enrichListRow] å表è¡å¢å¼ºï¼ä» formPayload è§£æå±ç¤ºåæ®µï¼ |
| | | * @param {(base: object) => object} [options.buildExtraListParams] è¿½å æ¥è¯¢åæ° |
| | | * @param {() => void} [options.beforeSave] ä¿ååé©åï¼å¦åæ¥ä¸å¡åæ®µå° formPayloadï¼ |
| | | * @param {import('vue').ComputedRef|object} [options.extraFormRules] é¢å¤è¡¨åæ ¡éª |
| | | */ |
| | | export function useApprovalInstanceModule(options = {}) { |
| | | const { |
| | | moduleKey, |
| | | enrichListRow, |
| | | buildExtraListParams, |
| | | beforeSave, |
| | | extraFormRules, |
| | | } = options; |
| | | |
| | | const userStore = useUserStore(); |
| | | const moduleConfig = computed(() => getApprovalModuleConfig(moduleKey)); |
| | | const businessTypeOptions = ref([]); |
| | | |
| | | /** å表æ¥è¯¢ businessTypeï¼ä¼å
registry åæ»æä¸¾ï¼ååé TypeEnums */ |
| | | const defaultListBusinessType = computed(() => { |
| | | const fixed = getModuleListBusinessType(moduleKey); |
| | | if (fixed != null && fixed !== "") return fixed; |
| | | const resolved = resolveModuleBusinessType(moduleKey, businessTypeOptions.value); |
| | | if (resolved != null && resolved !== "") return resolved; |
| | | return ""; |
| | | }); |
| | | |
| | | async function loadBusinessTypeOptions() { |
| | | if (businessTypeOptions.value.length) return; |
| | | try { |
| | | businessTypeOptions.value = await fetchBusinessTypeOptions(); |
| | | } catch { |
| | | businessTypeOptions.value = []; |
| | | } |
| | | } |
| | | |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | |
| | | const submitDialog = reactive({ visible: false, mode: "add" }); |
| | | const submitEditRow = ref(null); |
| | | const submitForm = reactive(createEmptySubmitForm("")); |
| | | const submitFormRef = ref(); |
| | | const submitSaving = ref(false); |
| | | |
| | | const templateBindVisible = ref(false); |
| | | const pendingTemplateBinding = ref(null); |
| | | /** æè¿ä¸æ¬¡å表æ¥è¯¢æ¡ä»¶ï¼ä¿ååå·æ°åè¡¨æ¶æ²¿ç¨ï¼ */ |
| | | let lastListSearchForm = null; |
| | | |
| | | const isSubmitEdit = computed(() => submitDialog.mode === "edit"); |
| | | const activeTemplate = computed(() => submitForm.templateSnapshot || null); |
| | | const submitFormFields = computed(() => { |
| | | const tplFields = activeTemplate.value?.fields; |
| | | if (tplFields?.length) return tplFields; |
| | | return submitForm.formFieldDefs || []; |
| | | }); |
| | | |
| | | const submitFormRules = computed(() => ({ |
| | | ...buildFormPayloadRules(submitFormFields.value), |
| | | ...(extraFormRules?.value ?? extraFormRules ?? {}), |
| | | })); |
| | | |
| | | const submitDialogTitle = computed(() => { |
| | | const label = moduleConfig.value?.label || "ç³è¯·"; |
| | | if (submitDialog.mode === "edit") { |
| | | return `ä¿®æ¹${activeTemplate.value?.label || submitForm.templateName || label}`; |
| | | } |
| | | return `æ°å¢${label}`; |
| | | }); |
| | | |
| | | function mapListRow(row, caches) { |
| | | const mapped = mapInstanceFromApi(row); |
| | | const fromFormConfig = enrichInstanceRowFromFormConfig(mapped, caches); |
| | | return enrichListRow ? enrichListRow(fromFormConfig) : fromFormConfig; |
| | | } |
| | | |
| | | async function fetchList(searchForm = {}) { |
| | | await loadBusinessTypeOptions(); |
| | | tableLoading.value = true; |
| | | try { |
| | | let extraParams = {}; |
| | | if (buildExtraListParams) { |
| | | extraParams = buildExtraListParams(searchForm) || {}; |
| | | } |
| | | const res = await listApprovalInstancePage( |
| | | buildApprovalInstanceListParams({ |
| | | page, |
| | | searchForm, |
| | | businessType: defaultListBusinessType.value, |
| | | extraParams, |
| | | }) |
| | | ); |
| | | const { records, total } = unwrapInstancePage(res); |
| | | const mapped = records.map(mapInstanceFromApi); |
| | | const allFields = []; |
| | | for (const row of mapped) { |
| | | const { fields } = resolveInstanceFormFields(row); |
| | | allFields.push(...fields); |
| | | } |
| | | const caches = await fetchSelectOptionCaches( |
| | | collectOptionSourcesFromFields(allFields) |
| | | ); |
| | | let rows = mapped.map((row) => mapListRow(row, caches)); |
| | | if (hasActiveModuleSearch(moduleKey, searchForm)) { |
| | | rows = filterInstanceRowsByModuleSearch(moduleKey, rows, searchForm); |
| | | } |
| | | tableData.value = rows; |
| | | page.total = hasActiveModuleSearch(moduleKey, searchForm) |
| | | ? rows.length |
| | | : total; |
| | | } catch { |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | ElMessage.error(`${moduleConfig.value?.label || "ç³è¯·"}å表å 载失败`); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleQuery(searchForm) { |
| | | lastListSearchForm = searchForm; |
| | | page.current = 1; |
| | | return fetchList(searchForm); |
| | | } |
| | | |
| | | /** è¿å
¥é¡µé¢ï¼å
æ TypeEnums è§£æ businessTypeï¼åæ¥å表 */ |
| | | async function initModuleList(searchForm) { |
| | | await loadBusinessTypeOptions(); |
| | | return handleQuery(searchForm); |
| | | } |
| | | |
| | | function pagination({ page: p, limit }, searchForm) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | return fetchList(searchForm); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function openEdit(row) { |
| | | if (!canEditBusinessInstanceRow(row)) { |
| | | ElMessage.warning("è¿è¡ä¸æå·²å®æç审æ¹ä¸å¯ä¿®æ¹"); |
| | | return; |
| | | } |
| | | if (!row?.id) { |
| | | ElMessage.warning("æ æ³ä¿®æ¹ï¼ç¼ºå°å®¡æ¹å®ä¾ ID"); |
| | | return; |
| | | } |
| | | submitDialog.mode = "edit"; |
| | | submitEditRow.value = { ...row }; |
| | | Object.assign(submitForm, buildEditFormFromInstanceRow(row)); |
| | | submitDialog.visible = true; |
| | | } |
| | | |
| | | function openEditFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openEdit(row); |
| | | } |
| | | |
| | | function resetSubmitForm() { |
| | | Object.assign(submitForm, createEmptySubmitForm("")); |
| | | submitEditRow.value = null; |
| | | } |
| | | |
| | | function openAddWithTemplate() { |
| | | submitDialog.visible = false; |
| | | pendingTemplateBinding.value = null; |
| | | templateBindVisible.value = true; |
| | | } |
| | | |
| | | function onTemplateBound(binding) { |
| | | pendingTemplateBinding.value = binding; |
| | | } |
| | | |
| | | function onTemplateBindClosed() { |
| | | const binding = pendingTemplateBinding.value; |
| | | if (!binding) return; |
| | | pendingTemplateBinding.value = null; |
| | | openAddFromBinding(binding); |
| | | } |
| | | |
| | | function openAddFromBinding(binding) { |
| | | resetSubmitForm(); |
| | | applyBindingToForm(submitForm, binding); |
| | | submitDialog.mode = "add"; |
| | | submitEditRow.value = null; |
| | | submitDialog.visible = true; |
| | | } |
| | | |
| | | function closeSubmitDialog() { |
| | | submitDialog.visible = false; |
| | | } |
| | | |
| | | async function submitInstanceForm(options = {}) { |
| | | const { skipValidate = false } = options; |
| | | if (!skipValidate) { |
| | | if (!submitFormRef.value?.validate) { |
| | | ElMessage.warning("è¡¨åæªå°±ç»ªï¼è¯·å
³éå¼¹çªåéè¯"); |
| | | return false; |
| | | } |
| | | try { |
| | | await submitFormRef.value.validate(); |
| | | } catch { |
| | | ElMessage.warning("请å®å表åå¿
填项ååä¿å"); |
| | | return false; |
| | | } |
| | | } |
| | | if (!activeTemplate.value) { |
| | | ElMessage.warning("æªå è½½å®¡æ¹æ¨¡æ¿ï¼æ æ³ä¿å"); |
| | | return false; |
| | | } |
| | | const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes }); |
| | | if (!bindingCheck.ok) { |
| | | ElMessage.warning(bindingCheck.message); |
| | | return false; |
| | | } |
| | | if (!submitForm.templateId) { |
| | | ElMessage.warning("ç¼ºå°æ¨¡æ¿ IDï¼æ æ³æäº¤"); |
| | | return false; |
| | | } |
| | | if (beforeSave) { |
| | | try { |
| | | await beforeSave(submitForm, { isEdit: isSubmitEdit.value, editRow: submitEditRow.value }); |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | | if (submitSaving.value) return false; |
| | | submitSaving.value = true; |
| | | try { |
| | | const dto = buildInstanceDto({ |
| | | submitForm, |
| | | activeTemplate: activeTemplate.value, |
| | | userStore, |
| | | flowNodes: bindingCheck.nodes, |
| | | existingRow: isSubmitEdit.value ? submitEditRow.value : null, |
| | | }); |
| | | if (isSubmitEdit.value) { |
| | | await updateApprovalInstance(dto); |
| | | } else { |
| | | await saveApprovalInstance(dto); |
| | | } |
| | | submitDialog.visible = false; |
| | | if (!isSubmitEdit.value) page.current = 1; |
| | | await fetchList(lastListSearchForm ?? {}); |
| | | 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 { |
| | | ElMessage.error(isSubmitEdit.value ? "ä¿å失败" : "æäº¤å¤±è´¥"); |
| | | 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 (submitDialog.visible && submitEditRow.value?.id === row.id) { |
| | | submitDialog.visible = false; |
| | | } |
| | | await fetchList(lastListSearchForm ?? {}); |
| | | } catch { |
| | | /* éè¯¯ç±æ¦æªå¨æç¤º */ |
| | | } |
| | | } |
| | | |
| | | /** æå»ºæ åæä½åï¼è¯¦æ
ãä¿®æ¹ãå é¤ï¼ä¸å®¡æ¹å表ä¸è´ï¼ */ |
| | | function buildTableActions(extraOperations = []) { |
| | | return [ |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "ä¿®æ¹", |
| | | type: "text", |
| | | disabled: (row) => !canEditBusinessInstanceRow(row), |
| | | clickFun: (row) => openEdit(row), |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | clickFun: (row) => removeInstance(row), |
| | | }, |
| | | ...extraOperations, |
| | | ]; |
| | | } |
| | | |
| | | return { |
| | | moduleConfig, |
| | | defaultListBusinessType, |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | detailDialog, |
| | | detailRow, |
| | | submitDialog, |
| | | submitEditRow, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | isSubmitEdit, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitDialogTitle, |
| | | templateBindVisible, |
| | | pendingTemplateBinding, |
| | | fetchList, |
| | | handleQuery, |
| | | initModuleList, |
| | | pagination, |
| | | openDetail, |
| | | openEdit, |
| | | openEditFromDetail, |
| | | openAddWithTemplate, |
| | | onTemplateBound, |
| | | onTemplateBindClosed, |
| | | openAddFromBinding, |
| | | closeSubmitDialog, |
| | | resetSubmitForm, |
| | | submitInstanceForm, |
| | | removeInstance, |
| | | buildTableActions, |
| | | loadBusinessTypeOptions, |
| | | canEditBusinessInstanceRow, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplate, |
| | | TEMPLATE_TYPE_CUSTOM, |
| | | } from "@/api/officeProcessAutomation/approvalTemplate.js"; |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { |
| | | fetchBusinessTypeOptions, |
| | | mapEnabledFromApi, |
| | | unwrapTemplateList, |
| | | } from "../approve-template/approveTemplateConstants.js"; |
| | | import { |
| | | createEmptySubmitForm, |
| | | mapSubmitTemplateCard, |
| | | matchBusinessTypeValue, |
| | | } from "../approve-list/approveListConstants.js"; |
| | | import { |
| | | getApprovalModuleConfig, |
| | | getModuleMatchingBusinessTypes, |
| | | resolveModuleBusinessType, |
| | | } from "./approvalModuleRegistry.js"; |
| | | import { |
| | | buildFormPayloadRules, |
| | | buildTemplateBindingFromDetail, |
| | | validateTemplateBinding, |
| | | } from "./approvalTemplateBindingUtils.js"; |
| | | |
| | | /** |
| | | * å®¡æ¹æ¨¡æ¿ç»å®ï¼ä¸å¡æ¨¡ååºå®ç±»å / 审æ¹å表éç¨ï¼ |
| | | * |
| | | * @param {object} options |
| | | * @param {string} [options.moduleKey] ä¸å¡æ¨¡å keyï¼è§ approvalModuleRegistry |
| | | * @param {string|number} [options.businessType] ç´æ¥æå®ç±»åï¼ä¼å
级é«äº moduleKeyï¼ |
| | | * @param {'module'|'universal'} [options.mode] module=ä»
æ¬ç±»å模æ¿ï¼universal=éå
éç±»å |
| | | */ |
| | | export function useApprovalTemplateBinding(options = {}) { |
| | | const { moduleKey = null, businessType: fixedBusinessType = null, mode = moduleKey ? "module" : "universal" } = |
| | | options; |
| | | |
| | | const isUniversal = mode === "universal" && !moduleKey && fixedBusinessType == null; |
| | | |
| | | const allTemplates = ref([]); |
| | | const businessTypeOptions = ref([]); |
| | | const selectedBusinessType = ref(fixedBusinessType ?? ""); |
| | | const templatesLoading = ref(false); |
| | | const step = ref(isUniversal ? 1 : 1); |
| | | |
| | | const bindingForm = reactive(createEmptySubmitForm("")); |
| | | |
| | | const moduleConfig = computed(() => getApprovalModuleConfig(moduleKey)); |
| | | |
| | | const resolvedBusinessType = computed(() => { |
| | | if (fixedBusinessType != null && fixedBusinessType !== "") return fixedBusinessType; |
| | | if (selectedBusinessType.value != null && selectedBusinessType.value !== "") { |
| | | return selectedBusinessType.value; |
| | | } |
| | | if (moduleKey) { |
| | | return resolveModuleBusinessType(moduleKey, businessTypeOptions.value); |
| | | } |
| | | return ""; |
| | | }); |
| | | |
| | | const matchingBusinessTypes = computed(() => { |
| | | if (fixedBusinessType != null && fixedBusinessType !== "") return [fixedBusinessType]; |
| | | if (isUniversal) { |
| | | const t = selectedBusinessType.value; |
| | | return t != null && t !== "" ? [t] : []; |
| | | } |
| | | if (moduleKey) { |
| | | return getModuleMatchingBusinessTypes(moduleKey, businessTypeOptions.value); |
| | | } |
| | | const t = resolvedBusinessType.value; |
| | | return t != null && t !== "" ? [t] : []; |
| | | }); |
| | | |
| | | const templateCards = computed(() => { |
| | | const types = matchingBusinessTypes.value; |
| | | if (!types.length) return []; |
| | | return allTemplates.value.filter((card) => |
| | | types.some( |
| | | (t) => |
| | | matchBusinessTypeValue(card.businessType, t) || |
| | | matchBusinessTypeValue(card.approvalType, t) |
| | | ) |
| | | ); |
| | | }); |
| | | |
| | | const activeTemplate = computed(() => bindingForm.templateSnapshot || null); |
| | | |
| | | const formFields = computed(() => { |
| | | const tplFields = activeTemplate.value?.fields; |
| | | if (tplFields?.length) return tplFields; |
| | | return bindingForm.formFieldDefs || []; |
| | | }); |
| | | |
| | | const formRules = computed(() => buildFormPayloadRules(formFields.value)); |
| | | |
| | | const hasTemplateBound = computed(() => Boolean(activeTemplate.value?.templateId || bindingForm.templateId)); |
| | | |
| | | function businessTypeLabel(type) { |
| | | if (type == null || type === "") return ""; |
| | | const hit = businessTypeOptions.value.find((x) => matchBusinessTypeValue(x.value, type)); |
| | | return hit?.label || moduleConfig.value?.label || ""; |
| | | } |
| | | |
| | | const selectedBusinessTypeLabel = computed(() => businessTypeLabel(resolvedBusinessType.value)); |
| | | |
| | | function countTemplatesByBusinessType(type) { |
| | | const types = |
| | | moduleKey && !fixedBusinessType |
| | | ? getModuleMatchingBusinessTypes(moduleKey, businessTypeOptions.value) |
| | | : [type]; |
| | | return allTemplates.value.filter((card) => |
| | | types.some( |
| | | (t) => |
| | | matchBusinessTypeValue(card.businessType, t) || |
| | | matchBusinessTypeValue(card.approvalType, t) |
| | | ) |
| | | ).length; |
| | | } |
| | | |
| | | async function loadTemplates() { |
| | | templatesLoading.value = true; |
| | | try { |
| | | const [typeOptions, customRes] = await Promise.all([ |
| | | fetchBusinessTypeOptions(), |
| | | listApprovalTemplate(TEMPLATE_TYPE_CUSTOM), |
| | | ]); |
| | | businessTypeOptions.value = typeOptions; |
| | | allTemplates.value = unwrapTemplateList(customRes) |
| | | .filter((row) => mapEnabledFromApi(row.enabled)) |
| | | .map(mapSubmitTemplateCard); |
| | | |
| | | if (moduleKey && !fixedBusinessType) { |
| | | const resolved = resolveModuleBusinessType(moduleKey, typeOptions); |
| | | if (resolved != null && resolved !== "") selectedBusinessType.value = resolved; |
| | | } |
| | | } catch { |
| | | businessTypeOptions.value = []; |
| | | allTemplates.value = []; |
| | | ElMessage.error("å è½½å®¡æ¹æ¨¡æ¿å¤±è´¥"); |
| | | } finally { |
| | | templatesLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function resetBinding() { |
| | | step.value = isUniversal ? 1 : 1; |
| | | if (!fixedBusinessType && !moduleKey) selectedBusinessType.value = ""; |
| | | else if (moduleKey) { |
| | | selectedBusinessType.value = |
| | | fixedBusinessType ?? resolveModuleBusinessType(moduleKey, businessTypeOptions.value) ?? ""; |
| | | } |
| | | Object.assign(bindingForm, createEmptySubmitForm("")); |
| | | } |
| | | |
| | | function pickBusinessType(type) { |
| | | if (!countTemplatesByBusinessType(type)) { |
| | | ElMessage.warning("该类å䏿æ å¯ç¨å®¡æ¹æ¨¡æ¿"); |
| | | return; |
| | | } |
| | | selectedBusinessType.value = type; |
| | | step.value = 2; |
| | | } |
| | | |
| | | function backToBusinessTypePick() { |
| | | selectedBusinessType.value = ""; |
| | | step.value = 1; |
| | | } |
| | | |
| | | function backToTemplatePick() { |
| | | step.value = isUniversal ? 2 : 1; |
| | | } |
| | | |
| | | async function pickTemplate(card) { |
| | | if (!card?.id) return false; |
| | | templatesLoading.value = true; |
| | | try { |
| | | const res = await getApprovalTemplateDetail(card.id); |
| | | const applied = buildTemplateBindingFromDetail(res); |
| | | Object.assign(bindingForm, { |
| | | templateKey: String(card.id), |
| | | ...applied, |
| | | }); |
| | | step.value = isUniversal ? 3 : 2; |
| | | return true; |
| | | } catch { |
| | | ElMessage.error("å 载模æ¿è¯¦æ
失败"); |
| | | return false; |
| | | } finally { |
| | | templatesLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** ç´æ¥ä»¥è¯¦æ
è¡ç»å®ï¼ç¼è¾åæ¾ï¼ */ |
| | | function applyBindingState(state) { |
| | | if (!state) return; |
| | | Object.assign(bindingForm, createEmptySubmitForm(""), state); |
| | | step.value = isUniversal ? 3 : 2; |
| | | } |
| | | |
| | | async function validateBinding(formRef) { |
| | | if (formRef?.validate) { |
| | | try { |
| | | await formRef.validate(); |
| | | } catch { |
| | | return { ok: false }; |
| | | } |
| | | } |
| | | if (!hasTemplateBound.value) { |
| | | return { ok: false, message: "è¯·éæ©å®¡æ¹æ¨¡æ¿" }; |
| | | } |
| | | return validateTemplateBinding({ flowNodes: bindingForm.flowNodes }); |
| | | } |
| | | |
| | | function getBindingPayload() { |
| | | return { |
| | | templateId: bindingForm.templateId, |
| | | templateName: bindingForm.templateName, |
| | | businessType: bindingForm.businessType ?? resolvedBusinessType.value, |
| | | templateSnapshot: bindingForm.templateSnapshot, |
| | | formFieldDefs: bindingForm.formFieldDefs, |
| | | formPayload: bindingForm.formPayload, |
| | | flowNodes: bindingForm.flowNodes, |
| | | templateAttachments: bindingForm.templateAttachments, |
| | | storageBlobDTOs: bindingForm.storageBlobDTOs, |
| | | }; |
| | | } |
| | | |
| | | return { |
| | | isUniversal, |
| | | moduleConfig, |
| | | step, |
| | | bindingForm, |
| | | allTemplates, |
| | | businessTypeOptions, |
| | | selectedBusinessType, |
| | | resolvedBusinessType, |
| | | selectedBusinessTypeLabel, |
| | | templateCards, |
| | | activeTemplate, |
| | | formFields, |
| | | formRules, |
| | | hasTemplateBound, |
| | | templatesLoading, |
| | | loadTemplates, |
| | | resetBinding, |
| | | pickBusinessType, |
| | | backToBusinessTypePick, |
| | | backToTemplatePick, |
| | | pickTemplate, |
| | | applyBindingState, |
| | | validateBinding, |
| | | getBindingPayload, |
| | | countTemplatesByBusinessType, |
| | | businessTypeLabel, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ref } from "vue"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | /** å®¡æ¹æµç¨éäººä¸æï¼æ¨¡æ¿/å®ä¾å
±ç¨ï¼ */ |
| | | export function useFlowUserOptions() { |
| | | const flowUserOptions = ref([]); |
| | | const loading = ref(false); |
| | | |
| | | async function loadFlowUsers() { |
| | | loading.value = true; |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | flowUserOptions.value = unwrapArray(res).filter(isActiveUser); |
| | | } catch { |
| | | flowUserOptions.value = []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | return { flowUserOptions, loading, loadFlowUsers }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | import { getTypeEnums } from "@/api/basicData/enum.js"; |
| | | import { |
| | | TEMPLATE_TYPE_BUILTIN, |
| | | TEMPLATE_TYPE_CUSTOM, |
| | | } from "@/api/officeProcessAutomation/approvalTemplate.js"; |
| | | import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js"; |
| | | import { |
| | | buildFormConfigJson, |
| | | createEmptyFormConfigData, |
| | | parseFormConfigToData, |
| | | validateFormConfigData, |
| | | } from "./formConfigUtils.js"; |
| | | |
| | | export function unwrapEnumList(data) { |
| | | if (Array.isArray(data)) return data; |
| | | if (!data || typeof data !== "object") return []; |
| | | if (Array.isArray(data.TypeEnums)) return data.TypeEnums; |
| | | if (Array.isArray(data.typeEnums)) return data.typeEnums; |
| | | const nested = Object.values(data).find((v) => Array.isArray(v)); |
| | | return nested || []; |
| | | } |
| | | |
| | | export function normalizeBusinessTypeOptions(data) { |
| | | return unwrapEnumList(data) |
| | | .map((item) => { |
| | | const rawValue = item?.value ?? item?.code ?? item?.businessType ?? item?.dictValue ?? item?.key; |
| | | if (rawValue == null || rawValue === "") return null; |
| | | const num = Number(rawValue); |
| | | const value = |
| | | typeof rawValue === "number" || (Number.isFinite(num) && String(rawValue).trim() !== "") |
| | | ? num |
| | | : rawValue; |
| | | const label = |
| | | item?.label ?? item?.name ?? item?.desc ?? item?.dictLabel ?? item?.text ?? String(value); |
| | | return { label, value }; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | |
| | | export async function fetchBusinessTypeOptions() { |
| | | try { |
| | | const res = await getTypeEnums(); |
| | | return normalizeBusinessTypeOptions(res?.data); |
| | | } catch { |
| | | return []; |
| | | } |
| | | } |
| | | |
| | | /** æ¯å¦ä¸ºç³»ç»å
置模æ¿ï¼templateType === 0ï¼ */ |
| | | export function isBuiltinTemplate(row) { |
| | | return Number(row?.templateType) === TEMPLATE_TYPE_BUILTIN; |
| | | } |
| | | |
| | | /** èç¹å
å®¡æ¹æ¹å¼ï¼ä¼ç¾ / æç¾ */ |
| | | export const NODE_SIGN_MODE_OPTIONS = [ |
| | | { value: "countersign", label: "ä¼ç¾", desc: "æ¬èç¹ææå®¡æ¹äººåééè¿" }, |
| | | { value: "or_sign", label: "æç¾", desc: "æ¬èç¹ä»»ä¸å®¡æ¹äººéè¿å³å¯" }, |
| | | ]; |
| | | |
| | | function parseFormConfig(formConfig) { |
| | | if (!formConfig) return {}; |
| | | if (typeof formConfig === "object") return formConfig; |
| | | try { |
| | | return JSON.parse(formConfig); |
| | | } catch { |
| | | return {}; |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | |
| | | /** å端éä»¶åæ®µ â é¡µé¢ storageBlobDTOs */ |
| | | export function mapAttachmentsFromApi(row) { |
| | | const list = |
| | | row?.storageBlobDTOs || |
| | | row?.storageBlobDTOS || |
| | | row?.storageBlobVOS || |
| | | row?.storageBlobVOList || |
| | | row?.attachmentList || |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | /** å页å表项 â 页é¢è¡æ°æ®ï¼ä¸»è¡¨ + èç¹ï¼ */ |
| | | 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, |
| | | businessType: row.businessType ?? "", |
| | | formConfig: row.formConfig, |
| | | formConfigData: parseFormConfigToData(row.formConfig), |
| | | storageBlobDTOs: mapAttachmentsFromApi(row), |
| | | 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 != null ? Number(form.templateType) : TEMPLATE_TYPE_CUSTOM, |
| | | businessType: form.businessType ?? "", |
| | | 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; |
| | | const attachments = Array.isArray(form.storageBlobDTOs) ? form.storageBlobDTOs : []; |
| | | if (attachments.length) dto.storageBlobDTOs = attachments; |
| | | return dto; |
| | | } |
| | | |
| | | export function buildApprovalTemplateListParams({ page, searchForm }) { |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | }; |
| | | const kw = (searchForm?.keyword || "").trim(); |
| | | if (kw) params.templateName = kw; |
| | | if (searchForm?.enabledOnly) params.enabled = "1"; |
| | | return params; |
| | | } |
| | | |
| | | export function nodeSignModeLabel(mode) { |
| | | return NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.label || "â"; |
| | | } |
| | | |
| | | export function approvalTypeLabel(type) { |
| | | return APPROVAL_TYPE_OPTIONS.find((x) => x.value === type)?.label || type || "â"; |
| | | } |
| | | |
| | | export function createEmptyNode(order = 1) { |
| | | return { |
| | | nodeOrder: order, |
| | | signMode: "countersign", |
| | | approvers: [], |
| | | }; |
| | | } |
| | | |
| | | export function createEmptyTemplateForm() { |
| | | return { |
| | | id: "", |
| | | templateName: "", |
| | | description: "", |
| | | templateType: TEMPLATE_TYPE_CUSTOM, |
| | | lockedFormFieldUids: [], |
| | | businessType: "", |
| | | formConfig: "", |
| | | formConfigData: createEmptyFormConfigData(), |
| | | enabled: true, |
| | | flowNodes: [createEmptyNode(1)], |
| | | storageBlobDTOs: [], |
| | | }; |
| | | } |
| | | |
| | | 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 || "", |
| | | })), |
| | | })); |
| | | } |
| | | |
| | | export function validateTemplateForm(form) { |
| | | const name = (form.templateName || "").trim(); |
| | | if (!name) return { ok: false, message: "è¯·å¡«åæ¨¡æ¿åç§°" }; |
| | | if (form.businessType == null || form.businessType === "") { |
| | | return { ok: false, message: "è¯·éæ©æ¨¡æ¿ç±»å" }; |
| | | } |
| | | const nodes = normalizeFlowNodes(form.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} 个èç¹éæ©è³å°ä¸å审æ¹äºº` }; |
| | | } |
| | | } |
| | | const cfgCheck = validateFormConfigData(form.formConfigData); |
| | | if (!cfgCheck.ok) return cfgCheck; |
| | | return { ok: true, nodes, name }; |
| | | } |
| | | |
| | | export function flowNodesSummary(nodes) { |
| | | const list = normalizeFlowNodes(nodes); |
| | | if (!list.length) return "â"; |
| | | return list |
| | | .map((n, i) => { |
| | | const names = n.approvers.map((a) => a.approverName || "æªå½å").join("ã") || "æªé
ç½®"; |
| | | return `èç¹${i + 1}(${nodeSignModeLabel(n.signMode)}:${names})`; |
| | | }) |
| | | .join(" â "); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å®¡æ¹æ¨¡æ¿ï¼å¯é
置填æ¥é¡¹ï¼åºååå° 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" |
| | | :disabled="disableImport" |
| | | @visible-change="onImportDropdownVisible" |
| | | @command="importFromTemplate" |
| | | > |
| | | <el-button size="small" :loading="templateImportLoading" :disabled="disableImport"> |
| | | ä»å·²ææ¨¡æ¿å¯¼å
¥ |
| | | </el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item v-if="!templateImportOptions.length" disabled> |
| | | ææ å
¶ä»å®¡æ¹æ¨¡æ¿ |
| | | </el-dropdown-item> |
| | | <el-dropdown-item |
| | | v-for="t in templateImportOptions" |
| | | :key="t.id" |
| | | :command="t.id" |
| | | > |
| | | <span>{{ t.label }}</span> |
| | | <el-tag v-if="!t.enabled" size="small" type="info" class="import-tag">å·²åç¨</el-tag> |
| | | </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, |
| | | 'fce-card--locked': isFieldLocked(field), |
| | | }" |
| | | > |
| | | <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> |
| | | <el-tag v-if="isFieldLocked(field)" size="small" type="info" effect="plain">å
置项</el-tag> |
| | | </div> |
| | | <div v-if="!isFieldLocked(field)" 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" |
| | | :disabled="isFieldLocked(field)" |
| | | @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" |
| | | :disabled="isFieldLocked(field)" |
| | | @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%" |
| | | :disabled="isFieldLocked(field)" |
| | | @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="éå¡«" |
| | | :disabled="isFieldLocked(field)" |
| | | @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%" |
| | | :disabled="isFieldLocked(field)" |
| | | @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%" |
| | | :disabled="isFieldLocked(field)" |
| | | @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%" |
| | | :disabled="isFieldLocked(field)" |
| | | @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)" |
| | | :disabled="isFieldLocked(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%" |
| | | :disabled="isFieldLocked(field)" |
| | | @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%" |
| | | :disabled="isFieldLocked(field)" |
| | | 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%" |
| | | :disabled="isFieldLocked(field)" |
| | | clearable |
| | | @change="emitOut" |
| | | /> |
| | | <el-select |
| | | v-else-if="field.type === 'select'" |
| | | v-model="field.defaultValue" |
| | | placeholder="éå¡«" |
| | | style="width: 100%" |
| | | clearable |
| | | filterable |
| | | :loading="optionSourceLoading" |
| | | :disabled="isFieldLocked(field)" |
| | | @change="emitOut" |
| | | > |
| | | <el-option |
| | | v-for="o in resolvedSelectOptions(field)" |
| | | :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"> |
| | | <span class="fce-section-title">䏿é项</span> |
| | | <el-row :gutter="16" class="fce-source-row"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éé¡¹æ¥æº" class="fce-field-item"> |
| | | <el-select |
| | | v-model="field.optionSource" |
| | | style="width: 100%" |
| | | :disabled="isFieldLocked(field)" |
| | | @change="onOptionSourceChange(field)" |
| | | > |
| | | <el-option |
| | | v-for="s in SELECT_OPTION_SOURCE_OPTIONS" |
| | | :key="s.value" |
| | | :label="s.label" |
| | | :value="s.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <p v-if="isDynamicOptionSource(field.optionSource)" class="fce-source-tip"> |
| | | {{ optionSourceDesc(field.optionSource) }}ãæäº¤å®¡æ¹æ¶å°èªå¨å è½½ææ°æ°æ®ï¼æ éæå¨ç»´æ¤é项ã |
| | | </p> |
| | | <template v-if="!isDynamicOptionSource(field.optionSource)"> |
| | | <div class="fce-options-head"> |
| | | <span class="fce-section-subtitle">æå¨é项</span> |
| | | <el-button |
| | | type="primary" |
| | | link |
| | | size="small" |
| | | :icon="Plus" |
| | | :disabled="isFieldLocked(field)" |
| | | @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="æ¾ç¤ºææ¬" |
| | | :disabled="isFieldLocked(field)" |
| | | @input="emitOut" |
| | | /> |
| | | <el-input |
| | | v-model="opt.value" |
| | | placeholder="é项å¼" |
| | | class="fce-option-value" |
| | | :disabled="isFieldLocked(field)" |
| | | @input="emitOut" |
| | | /> |
| | | <el-button |
| | | type="danger" |
| | | link |
| | | :icon="Delete" |
| | | :disabled="isFieldLocked(field) || field.options.length <= 1" |
| | | @click="removeOption(field, oi)" |
| | | /> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue"; |
| | | import { |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplate, |
| | | TEMPLATE_TYPE_BUILTIN, |
| | | TEMPLATE_TYPE_CUSTOM, |
| | | } from "@/api/officeProcessAutomation/approvalTemplate.js"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { |
| | | mapEnabledFromApi, |
| | | unwrapTemplateDetail, |
| | | unwrapTemplateList, |
| | | } from "../approveTemplateConstants.js"; |
| | | import { |
| | | FORM_FIELD_TYPE_OPTIONS, |
| | | createEmptyFormConfigData, |
| | | createEmptyFormField, |
| | | formFieldTypeLabel, |
| | | parseFormConfigToData, |
| | | } from "../formConfigUtils.js"; |
| | | import { |
| | | SELECT_OPTION_SOURCE, |
| | | SELECT_OPTION_SOURCE_OPTIONS, |
| | | isDynamicOptionSource, |
| | | } from "../selectOptionSource.js"; |
| | | import { useSelectOptionSources } from "../useSelectOptionSources.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Object, default: () => createEmptyFormConfigData() }, |
| | | /** ç¼è¾å½åæ¨¡æ¿æ¶æé¤èªèº«ï¼é¿å
ä»èªå·±å¯¼å
¥ */ |
| | | excludeTemplateId: { type: [String, Number], default: null }, |
| | | /** ç¦ç¨ãä»å·²ææ¨¡æ¿å¯¼å
¥ã */ |
| | | disableImport: { type: Boolean, default: false }, |
| | | /** ç³»ç»å
置模æ¿ç¼è¾æ¶ï¼æå¼å¼¹çªå³åå¨çå¡«æ¥é¡¹ _uidï¼ä¸å¯æ¹å */ |
| | | lockedFieldUids: { type: Array, default: () => [] }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | const inner = reactive(createEmptyFormConfigData()); |
| | | |
| | | const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources(); |
| | | |
| | | const templateImportOptions = ref([]); |
| | | const templateImportLoading = ref(false); |
| | | |
| | | const lockedUidSet = computed( |
| | | () => new Set((props.lockedFieldUids || []).filter(Boolean)) |
| | | ); |
| | | |
| | | function isFieldLocked(field) { |
| | | return field?._uid != null && lockedUidSet.value.has(field._uid); |
| | | } |
| | | |
| | | function typeLabel(type) { |
| | | return formFieldTypeLabel(type); |
| | | } |
| | | |
| | | function defaultPlaceholder(field) { |
| | | const name = field.label || "è¯¥åæ®µ"; |
| | | return `éå¡«ï¼éæ©æ¨¡æ¿æ¶å°é¢å¡«${name}`; |
| | | } |
| | | |
| | | function optionSourceDesc(source) { |
| | | return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.desc || ""; |
| | | } |
| | | |
| | | function resolvedSelectOptions(field) { |
| | | if (field.type !== "select") return []; |
| | | return getOptions(field); |
| | | } |
| | | |
| | | function syncFromProps(v) { |
| | | const src = v || createEmptyFormConfigData(); |
| | | inner.summaryPlaceholder = src.summaryPlaceholder || ""; |
| | | inner.fields = (src.fields || []).map((f) => ({ |
| | | ...createEmptyFormField(), |
| | | ...f, |
| | | _uid: f._uid || createEmptyFormField()._uid, |
| | | optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC, |
| | | options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })), |
| | | })); |
| | | ensureForFields(inner.fields); |
| | | } |
| | | |
| | | 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), |
| | | optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC, |
| | | 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()); |
| | | ensureForFields(inner.fields); |
| | | emitOut(); |
| | | } |
| | | |
| | | function removeField(index) { |
| | | if (isFieldLocked(inner.fields[index])) return; |
| | | inner.fields.splice(index, 1); |
| | | emitOut(); |
| | | } |
| | | |
| | | function moveField(index, delta) { |
| | | if (isFieldLocked(inner.fields[index])) return; |
| | | const next = index + delta; |
| | | if (next < 0 || next >= inner.fields.length) return; |
| | | if (isFieldLocked(inner.fields[next])) 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") { |
| | | if (!field.optionSource) field.optionSource = SELECT_OPTION_SOURCE.STATIC; |
| | | if (!field.options || !field.options.length) { |
| | | field.options = [{ label: "", value: "" }]; |
| | | } |
| | | ensureForFields(inner.fields); |
| | | } |
| | | resetDefaultValueForType(field); |
| | | emitOut(); |
| | | } |
| | | |
| | | function onOptionSourceChange(field) { |
| | | field.defaultValue = ""; |
| | | if (!isDynamicOptionSource(field.optionSource) && (!field.options || !field.options.length)) { |
| | | field.options = [{ label: "", value: "" }]; |
| | | } |
| | | ensureForFields(inner.fields); |
| | | 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(); |
| | | } |
| | | |
| | | async function loadTemplateImportOptions() { |
| | | templateImportLoading.value = true; |
| | | try { |
| | | const [customRes, builtinRes] = await Promise.all([ |
| | | listApprovalTemplate(TEMPLATE_TYPE_CUSTOM), |
| | | listApprovalTemplate(TEMPLATE_TYPE_BUILTIN), |
| | | ]); |
| | | const excludeId = |
| | | props.excludeTemplateId != null && props.excludeTemplateId !== "" |
| | | ? String(props.excludeTemplateId) |
| | | : ""; |
| | | templateImportOptions.value = [...unwrapTemplateList(customRes), ...unwrapTemplateList(builtinRes)] |
| | | .filter((row) => row?.id != null && String(row.id) !== excludeId) |
| | | .map((row) => ({ |
| | | id: row.id, |
| | | label: row.templateName || `æ¨¡æ¿ #${row.id}`, |
| | | enabled: mapEnabledFromApi(row.enabled), |
| | | })); |
| | | } catch { |
| | | templateImportOptions.value = []; |
| | | ElMessage.error("å è½½å®¡æ¹æ¨¡æ¿å表失败"); |
| | | } finally { |
| | | templateImportLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function onImportDropdownVisible(visible) { |
| | | if (props.disableImport) return; |
| | | if (visible) loadTemplateImportOptions(); |
| | | } |
| | | |
| | | async function importFromTemplate(templateId) { |
| | | if (!templateId) return; |
| | | if (inner.fields.length) { |
| | | try { |
| | | await ElMessageBox.confirm("å°è¦çå½åå¡«æ¥é¡¹é
ç½®ï¼æ¯å¦ç»§ç»ï¼", "仿¨¡æ¿å¯¼å
¥", { |
| | | type: "warning", |
| | | confirmButtonText: "ç»§ç»å¯¼å
¥", |
| | | cancelButtonText: "åæ¶", |
| | | }); |
| | | } catch { |
| | | return; |
| | | } |
| | | } |
| | | templateImportLoading.value = true; |
| | | try { |
| | | const res = await getApprovalTemplateDetail(templateId); |
| | | const row = unwrapTemplateDetail(res); |
| | | const data = parseFormConfigToData(row?.formConfig); |
| | | if (!data.fields?.length) { |
| | | ElMessage.warning("è¯¥æ¨¡æ¿æªé
置填æ¥é¡¹"); |
| | | return; |
| | | } |
| | | syncFromProps(data); |
| | | emitOut(); |
| | | ElMessage.success(`已导å
¥ã${row.templateName || "模æ¿"}ãçå¡«æ¥é¡¹`); |
| | | } catch { |
| | | ElMessage.error("å 载模æ¿è¯¦æ
失败"); |
| | | } finally { |
| | | templateImportLoading.value = false; |
| | | } |
| | | } |
| | | </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; |
| | | } |
| | | .import-tag { |
| | | margin-left: 8px; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | .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--locked { |
| | | background: var(--el-fill-color-light); |
| | | } |
| | | |
| | | .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, |
| | | .fce-options-head .fce-section-subtitle { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .fce-section-subtitle { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-secondary); |
| | | } |
| | | |
| | | .fce-source-row { |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .fce-source-tip { |
| | | margin: 0 0 10px; |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å®¡æ¹æ¨¡æ¿ï¼å¯é
ç½®èç¹æ°ï¼æ¯èç¹å¤äºº + ä¼ç¾/æç¾ --> |
| | | <template> |
| | | <div class="tfe"> |
| | | <div v-if="innerList.length" class="tfe-flow"> |
| | | <div v-for="(item, index) in innerList" :key="item._uid" class="tfe-flow-item"> |
| | | <div class="tfe-card" :class="{ 'tfe-card--empty': !item.approvers?.length }"> |
| | | <div class="tfe-badge">{{ index + 1 }}</div> |
| | | <div class="tfe-head"> |
| | | <span class="tfe-level">{{ levelText(index) }}</span> |
| | | <el-radio-group |
| | | v-if="!readonly" |
| | | v-model="item.signMode" |
| | | size="small" |
| | | @change="emitOut" |
| | | > |
| | | <el-radio-button value="countersign">ä¼ç¾</el-radio-button> |
| | | <el-radio-button value="or_sign">æç¾</el-radio-button> |
| | | </el-radio-group> |
| | | <el-tag v-else size="small" type="info" effect="plain"> |
| | | {{ signModeLabel(item.signMode) }} |
| | | </el-tag> |
| | | </div> |
| | | <p class="tfe-mode-tip">{{ signModeTip(item.signMode) }}</p> |
| | | <div v-if="!readonly" class="tfe-select"> |
| | | <el-select |
| | | v-model="item.approverIds" |
| | | multiple |
| | | collapse-tags |
| | | collapse-tags-tooltip |
| | | :max-collapse-tags="2" |
| | | filterable |
| | | placeholder="è¯·éæ©å®¡æ¹äººï¼å¯å¤éï¼" |
| | | style="width: 100%" |
| | | @change="(ids) => onApproversChange(ids, item)" |
| | | > |
| | | <el-option |
| | | v-for="u in userOptions" |
| | | :key="String(u.userId ?? u.id)" |
| | | :label="optionLabel(u)" |
| | | :value="u.userId ?? u.id" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | <div v-if="item.approvers?.length" class="tfe-chips" :class="{ 'tfe-chips--readonly': readonly }"> |
| | | <el-tag |
| | | v-for="a in item.approvers" |
| | | :key="String(a.approverId)" |
| | | size="small" |
| | | type="info" |
| | | effect="plain" |
| | | > |
| | | {{ a.approverName || "â" }} |
| | | </el-tag> |
| | | </div> |
| | | <div v-if="!readonly" class="tfe-actions"> |
| | | <el-button type="primary" circle size="small" :disabled="index === 0" title="åç§»" @click="moveLeft(index)"> |
| | | <el-icon><ArrowLeft /></el-icon> |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | circle |
| | | size="small" |
| | | :disabled="index === innerList.length - 1" |
| | | title="åç§»" |
| | | @click="moveRight(index)" |
| | | > |
| | | <el-icon><ArrowRight /></el-icon> |
| | | </el-button> |
| | | <el-button type="danger" circle size="small" title="å é¤èç¹" @click="remove(index)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | <p v-else-if="!item.approvers?.length" class="tfe-empty-approver">ææ å®¡æ¹äºº</p> |
| | | </div> |
| | | <div v-if="index < innerList.length - 1" class="tfe-conn"> |
| | | <div class="tfe-conn-line"></div> |
| | | <el-icon class="tfe-conn-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="!readonly" class="tfe-add-wrap"> |
| | | <div v-if="innerList.length" class="tfe-conn"> |
| | | <div class="tfe-conn-line"></div> |
| | | <el-icon class="tfe-conn-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | <div class="tfe-add-card" @click="addNode"> |
| | | <div class="tfe-add-icon"><el-icon :size="26"><Plus /></el-icon></div> |
| | | <span>æ°å¢èç¹</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else class="tfe-empty"> |
| | | <el-icon :size="44" color="#c0c4cc"><User /></el-icon> |
| | | <p>ææ å®¡æ¹èç¹</p> |
| | | <el-button v-if="!readonly" type="primary" @click="addNode">æ·»å 第ä¸ä¸ªèç¹</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ArrowLeft, ArrowRight, Delete, Plus, User } from "@element-plus/icons-vue"; |
| | | import { ref, watch } from "vue"; |
| | | import { NODE_SIGN_MODE_OPTIONS, normalizeFlowNodes } from "../approveTemplateConstants.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Array, default: () => [] }, |
| | | userOptions: { type: Array, default: () => [] }, |
| | | /** éæ©æ¨¡æ¿åç³è¯·åºæ¯ï¼ä»
å±ç¤ºï¼ä¸å¯æ¹å®¡æ¹äºº/èç¹ */ |
| | | readonly: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | const innerList = ref([]); |
| | | |
| | | function signModeTip(mode) { |
| | | return NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.desc || ""; |
| | | } |
| | | |
| | | function signModeLabel(mode) { |
| | | return ( |
| | | NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.label || |
| | | (mode === "or_sign" ? "æç¾" : "ä¼ç¾") |
| | | ); |
| | | } |
| | | |
| | | function levelText(i) { |
| | | const t = ["第ä¸çº§", "第äºçº§", "第ä¸çº§", "第å级", "第äºçº§", "第å
级", "第ä¸çº§", "第å
«çº§"]; |
| | | return t[i] || `第${i + 1}级`; |
| | | } |
| | | |
| | | function optionLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const un = u.userName || ""; |
| | | if (nick && un && nick !== un) return `${nick}ï¼${un}ï¼`; |
| | | return nick || un || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function newUid() { |
| | | return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; |
| | | } |
| | | |
| | | function mapIn(rows) { |
| | | 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), |
| | | approvers: [...n.approvers], |
| | | })); |
| | | } |
| | | |
| | | function publicShape(rows) { |
| | | return normalizeFlowNodes( |
| | | (rows || []).map((r) => ({ |
| | | id: r.id, |
| | | templateId: r.templateId, |
| | | nodeOrder: r.nodeOrder, |
| | | signMode: r.signMode, |
| | | approvers: r.approvers || [], |
| | | })) |
| | | ); |
| | | } |
| | | |
| | | function emitOut() { |
| | | emit("update:modelValue", publicShape(innerList.value)); |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | (v) => { |
| | | const next = publicShape(v || []); |
| | | if (JSON.stringify(next) === JSON.stringify(publicShape(innerList.value))) return; |
| | | innerList.value = mapIn(v || []); |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | function findUser(id) { |
| | | if (id == null || id === "") return null; |
| | | return props.userOptions.find((u) => String(u.userId ?? u.id) === String(id)) ?? null; |
| | | } |
| | | |
| | | 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); |
| | | const item = { |
| | | approverId: id, |
| | | 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(); |
| | | } |
| | | |
| | | function addNode() { |
| | | if (props.readonly) return; |
| | | innerList.value.push({ |
| | | _uid: newUid(), |
| | | nodeOrder: innerList.value.length + 1, |
| | | signMode: "countersign", |
| | | approverIds: [], |
| | | approvers: [], |
| | | }); |
| | | emitOut(); |
| | | } |
| | | |
| | | function remove(index) { |
| | | if (props.readonly) return; |
| | | innerList.value.splice(index, 1); |
| | | emitOut(); |
| | | } |
| | | |
| | | function moveLeft(index) { |
| | | if (props.readonly) return; |
| | | if (index < 1) return; |
| | | const t = innerList.value[index]; |
| | | innerList.value[index] = innerList.value[index - 1]; |
| | | innerList.value[index - 1] = t; |
| | | emitOut(); |
| | | } |
| | | |
| | | function moveRight(index) { |
| | | if (props.readonly) return; |
| | | if (index >= innerList.value.length - 1) return; |
| | | const t = innerList.value[index]; |
| | | innerList.value[index] = innerList.value[index + 1]; |
| | | innerList.value[index + 1] = t; |
| | | emitOut(); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .tfe { |
| | | width: 100%; |
| | | } |
| | | .tfe-flow { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | flex-wrap: nowrap; |
| | | overflow-x: auto; |
| | | padding: 6px 0 10px; |
| | | } |
| | | .tfe-flow-item { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .tfe-card { |
| | | width: 248px; |
| | | flex-shrink: 0; |
| | | border: 2px solid var(--el-border-color); |
| | | border-radius: 12px; |
| | | padding: 14px 12px 12px; |
| | | position: relative; |
| | | background: var(--el-bg-color); |
| | | } |
| | | .tfe-card--empty { |
| | | border-style: dashed; |
| | | background: var(--el-fill-color-lighter); |
| | | } |
| | | .tfe-badge { |
| | | position: absolute; |
| | | top: -8px; |
| | | left: 12px; |
| | | width: 22px; |
| | | height: 22px; |
| | | border-radius: 50%; |
| | | background: var(--el-color-primary); |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .tfe-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | margin: 8px 0 4px; |
| | | } |
| | | .tfe-level { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | .tfe-mode-tip { |
| | | font-size: 11px; |
| | | color: var(--el-text-color-secondary); |
| | | margin: 0 0 10px; |
| | | line-height: 1.4; |
| | | min-height: 30px; |
| | | } |
| | | .tfe-select { |
| | | margin-bottom: 8px; |
| | | } |
| | | .tfe-chips { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 4px; |
| | | margin-bottom: 8px; |
| | | min-height: 24px; |
| | | } |
| | | .tfe-chips--readonly { |
| | | margin-top: 4px; |
| | | margin-bottom: 0; |
| | | } |
| | | .tfe-empty-approver { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-placeholder); |
| | | margin: 4px 0 0; |
| | | } |
| | | .tfe-actions { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding-top: 10px; |
| | | border-top: 1px solid var(--el-border-color-lighter); |
| | | } |
| | | .tfe-conn { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 40px; |
| | | flex-shrink: 0; |
| | | align-self: center; |
| | | } |
| | | .tfe-conn-line { |
| | | flex: 1; |
| | | height: 2px; |
| | | background: var(--el-border-color); |
| | | } |
| | | .tfe-conn-icon { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-placeholder); |
| | | margin-left: -2px; |
| | | } |
| | | .tfe-add-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .tfe-add-card { |
| | | width: 120px; |
| | | min-height: 200px; |
| | | flex-shrink: 0; |
| | | border: 2px dashed var(--el-border-color); |
| | | border-radius: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | cursor: pointer; |
| | | color: var(--el-text-color-regular); |
| | | font-size: 13px; |
| | | background: var(--el-fill-color-lighter); |
| | | transition: border-color 0.2s, background 0.2s; |
| | | } |
| | | .tfe-add-card:hover { |
| | | border-color: var(--el-color-primary); |
| | | background: var(--el-color-primary-light-9); |
| | | color: var(--el-color-primary); |
| | | } |
| | | .tfe-add-icon { |
| | | width: 44px; |
| | | height: 44px; |
| | | border-radius: 50%; |
| | | background: var(--el-color-primary); |
| | | color: #fff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .tfe-empty { |
| | | text-align: center; |
| | | padding: 28px 16px; |
| | | border: 1px dashed var(--el-border-color); |
| | | border-radius: 12px; |
| | | background: var(--el-fill-color-lighter); |
| | | } |
| | | .tfe-empty p { |
| | | margin: 10px 0 14px; |
| | | color: var(--el-text-color-secondary); |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { mapAttachmentsFromApi } from "./approveTemplateConstants.js"; |
| | | import { |
| | | isDynamicOptionSource, |
| | | SELECT_OPTION_SOURCE, |
| | | selectOptionSourceLabel, |
| | | } from "./selectOptionSource.js"; |
| | | |
| | | export { selectOptionSourceLabel }; |
| | | |
| | | /** å¡«æ¥é¡¹ç±»åï¼ä¸å®¡æ¹æäº¤é¡µ 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: "", |
| | | optionSource: SELECT_OPTION_SOURCE.STATIC, |
| | | 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), |
| | | optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC, |
| | | 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") { |
| | | const source = f.optionSource || SELECT_OPTION_SOURCE.STATIC; |
| | | item.optionSource = source; |
| | | if (!isDynamicOptionSource(source)) { |
| | | 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 source = f.optionSource || SELECT_OPTION_SOURCE.STATIC; |
| | | if (isDynamicOptionSource(source)) continue; |
| | | 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") { |
| | | if (isDynamicOptionSource(field.optionSource)) { |
| | | return `${selectOptionSourceLabel(field.optionSource)}ï¼${String(dv)}`; |
| | | } |
| | | 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, |
| | | optionSource: rest.optionSource, |
| | | options: rest.options, |
| | | })); |
| | | return { |
| | | label: row?.templateName || "审æ¹", |
| | | businessType: row?.businessType ?? cfg.approvalType ?? "", |
| | | approvalType: cfg.approvalType || "", |
| | | summaryPlaceholder: cfg.summaryPlaceholder || "", |
| | | approvalMode: cfg.approvalMode || "parallel", |
| | | fields, |
| | | storageBlobDTOs: mapAttachmentsFromApi(row), |
| | | }; |
| | | } |
| | | |
| | | export function formConfigFieldsSummary(formConfigData) { |
| | | const fields = formConfigData?.fields || []; |
| | | if (!fields.length) return "â"; |
| | | return fields.map((f) => f.label || f.key || "æªå½å").join("ã"); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å®¡æ¹æ¨¡æ¿--> |
| | | |
| | | <template> |
| | | |
| | | <div class="app-container approve-template-page"> |
| | | |
| | | <div class="search_form mb20"> |
| | | |
| | | <div class="search_fields"> |
| | | |
| | | <span class="search_title">模æ¿åç§°ï¼</span> |
| | | |
| | | <el-input |
| | | |
| | | v-model="searchForm.keyword" |
| | | |
| | | style="width: 220px" |
| | | |
| | | placeholder="æç´¢åç§°æè¯´æ" |
| | | |
| | | clearable |
| | | |
| | | :prefix-icon="Search" |
| | | |
| | | @keyup.enter="handleQuery" |
| | | |
| | | /> |
| | | |
| | | <el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery"> |
| | | |
| | | ä»
æ¾ç¤ºå¯ç¨ |
| | | |
| | | </el-checkbox> |
| | | |
| | | <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">æç´¢</el-button> |
| | | |
| | | <el-button :icon="RefreshRight" @click="resetSearch">éç½®</el-button> |
| | | |
| | | </div> |
| | | |
| | | <div class="search_actions"> |
| | | |
| | | <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">æ°å»ºæ¨¡æ¿</el-button> |
| | | |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | |
| | | |
| | | <div class="table_list"> |
| | | |
| | | <PIMTable |
| | | |
| | | rowKey="id" |
| | | |
| | | :column="tableColumn" |
| | | |
| | | :tableData="tableData" |
| | | |
| | | :page="page" |
| | | |
| | | :isSelection="false" |
| | | |
| | | :tableLoading="tableLoading" |
| | | |
| | | :total="page.total" |
| | | |
| | | @pagination="pagination" |
| | | |
| | | /> |
| | | |
| | | </div> |
| | | |
| | | |
| | | |
| | | <!-- æ°å»º / ç¼è¾ --> |
| | | |
| | | <el-dialog |
| | | |
| | | v-model="formDialog.visible" |
| | | |
| | | :title="formDialog.title" |
| | | |
| | | width="1020px" |
| | | |
| | | append-to-body |
| | | |
| | | destroy-on-close |
| | | |
| | | class="template-form-dialog" |
| | | |
| | | @closed="onFormDialogClosed" |
| | | |
| | | > |
| | | |
| | | <el-form |
| | | |
| | | v-if="formDialog.visible" |
| | | |
| | | ref="formRef" |
| | | |
| | | :model="form" |
| | | |
| | | :rules="formRules" |
| | | |
| | | label-width="100px" |
| | | |
| | | > |
| | | |
| | | <el-row :gutter="20"> |
| | | |
| | | <el-col :span="8"> |
| | | |
| | | <el-form-item label="模æ¿åç§°" prop="templateName"> |
| | | |
| | | <el-input |
| | | v-model="form.templateName" |
| | | placeholder="å¦ï¼é¡¹ç®ç«é¡¹å®¡æ¹" |
| | | maxlength="50" |
| | | show-word-limit |
| | | :disabled="isEditingBuiltin" |
| | | /> |
| | | |
| | | </el-form-item> |
| | | |
| | | </el-col> |
| | | |
| | | <el-col :span="8"> |
| | | |
| | | <el-form-item label="模æ¿ç±»å" prop="businessType"> |
| | | |
| | | <el-select |
| | | v-model="form.businessType" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | :disabled="isEditingBuiltin" |
| | | > |
| | | |
| | | <el-option |
| | | |
| | | v-for="opt in templateTypeOptions" |
| | | |
| | | :key="`tpl-type-${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> |
| | | |
| | | </el-col> |
| | | |
| | | </el-row> |
| | | |
| | | <el-form-item label="模æ¿è¯´æ"> |
| | | |
| | | <el-input |
| | | |
| | | v-model="form.description" |
| | | |
| | | type="textarea" |
| | | |
| | | :rows="2" |
| | | |
| | | placeholder="ç®è¦è¯´æè¯¥æ¨¡æ¿çéç¨åºæ¯" |
| | | |
| | | maxlength="200" |
| | | |
| | | show-word-limit |
| | | |
| | | /> |
| | | |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å¡«æ¥é
ç½®"> |
| | | |
| | | <FormConfigEditor |
| | | v-model="form.formConfigData" |
| | | :exclude-template-id="form.id" |
| | | :disable-import="isEditingBuiltin" |
| | | :locked-field-uids="isEditingBuiltin ? form.lockedFormFieldUids : []" |
| | | /> |
| | | |
| | | <p class="flow-tip">é
ç½®æäº¤å®¡æ¹æ¶éå¡«åç表å项ï¼ä¿åååå
¥ formConfigï¼JSONï¼ã</p> |
| | | |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å®¡æ¹æµç¨" required> |
| | | |
| | | <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" /> |
| | | |
| | | <p class="flow-tip"> |
| | | |
| | | æé¡ºåºæµè½¬ï¼å¯ä¸ºæ¯ä¸ªèç¹æ·»å å¤å审æ¹äººï¼ä¼ç¾éå
¨é¨éè¿ï¼æç¾ä»»ä¸äººéè¿å³å¯è¿å
¥ä¸ä¸èç¹ã |
| | | |
| | | </p> |
| | | |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="éä»¶"> |
| | | |
| | | <div class="upload-block"> |
| | | |
| | | <FileUpload v-model:file-list="form.storageBlobDTOs" :limit="10" button-text="ç¹å»éæ©æä»¶" /> |
| | | |
| | | </div> |
| | | |
| | | <p class="flow-tip">å¯ä¸ä¼ 模æ¿è¯´æææ¡£ãå¶åº¦æä»¶çï¼éå¡«ï¼ã</p> |
| | | |
| | | </el-form-item> |
| | | |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | |
| | | <el-button type="primary" @click="onSubmitForm">ä¿ å</el-button> |
| | | |
| | | <el-button @click="formDialog.visible = false">å æ¶</el-button> |
| | | |
| | | </template> |
| | | |
| | | </el-dialog> |
| | | |
| | | |
| | | |
| | | <!-- 详æ
--> |
| | | |
| | | <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.businessType) }}</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="å¡«æ¥æç¤º" :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="100"> |
| | | |
| | | <template #default="{ row }"> |
| | | |
| | | {{ row.type === 'select' ? selectOptionSourceLabel(row.optionSource) : 'â' }} |
| | | |
| | | </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 class="detail-node-head"> |
| | | |
| | | <span class="detail-node-order">èç¹ {{ index + 1 }}</span> |
| | | |
| | | <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'"> |
| | | |
| | | {{ nodeSignModeLabel(node.signMode) }} |
| | | |
| | | </el-tag> |
| | | |
| | | </div> |
| | | |
| | | <div class="detail-approvers"> |
| | | |
| | | <el-tag |
| | | |
| | | v-for="a in node.approvers" |
| | | |
| | | :key="String(a.approverId)" |
| | | |
| | | class="detail-approver-tag" |
| | | |
| | | effect="plain" |
| | | |
| | | > |
| | | |
| | | {{ a.approverName || "â" }} |
| | | |
| | | </el-tag> |
| | | |
| | | <span v-if="!node.approvers?.length" class="text-muted">æªé
置审æ¹äºº</span> |
| | | |
| | | </div> |
| | | |
| | | <el-icon v-if="index < detailRow.flowNodes.length - 1" class="detail-arrow"><ArrowRight /></el-icon> |
| | | |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | <el-empty v-else description="ææ æµç¨èç¹" :image-size="60" /> |
| | | |
| | | <el-divider content-position="left">éä»¶ï¼{{ detailAttachments.length }} 个ï¼</el-divider> |
| | | |
| | | <template v-if="detailAttachments.length"> |
| | | |
| | | <el-tag |
| | | |
| | | v-for="(f, i) in detailAttachments" |
| | | |
| | | :key="i" |
| | | |
| | | class="detail-attachment-tag" |
| | | |
| | | type="info" |
| | | |
| | | effect="plain" |
| | | |
| | | > |
| | | |
| | | {{ attachmentDisplayName(f) }} |
| | | |
| | | </el-tag> |
| | | |
| | | </template> |
| | | |
| | | <el-empty v-else description="ææ éä»¶" :image-size="48" /> |
| | | |
| | | </div> |
| | | |
| | | <template #footer> |
| | | |
| | | <el-button @click="detailDialog.visible = false">å
³ é</el-button> |
| | | |
| | | <el-button type="primary" @click="editFromDetail">ç¼ è¾</el-button> |
| | | |
| | | </template> |
| | | |
| | | </el-dialog> |
| | | |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | | |
| | | |
| | | <script setup> |
| | | |
| | | import { ArrowRight, Plus, RefreshRight } from "@element-plus/icons-vue"; |
| | | |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | import { computed, nextTick, onMounted, ref } from "vue"; |
| | | |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | import FileUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | |
| | | import FormConfigEditor from "./components/FormConfigEditor.vue"; |
| | | |
| | | import TemplateFlowEditor from "./components/TemplateFlowEditor.vue"; |
| | | |
| | | import { formatDisplayTime, mapAttachmentsFromApi } from "./approveTemplateConstants.js"; |
| | | |
| | | import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js"; |
| | | import { selectOptionSourceLabel } from "./selectOptionSource.js"; |
| | | |
| | | import { useApproveTemplate } from "./useApproveTemplate.js"; |
| | | |
| | | |
| | | |
| | | const { |
| | | |
| | | Search, |
| | | |
| | | templateTypeOptions, |
| | | |
| | | loadTemplateTypeOptions, |
| | | |
| | | templateTypeLabel, |
| | | |
| | | nodeSignModeLabel, |
| | | |
| | | searchForm, |
| | | |
| | | tableLoading, |
| | | |
| | | page, |
| | | |
| | | tableData, |
| | | |
| | | tableColumn, |
| | | |
| | | formDialog, |
| | | |
| | | form, |
| | | |
| | | formRef, |
| | | |
| | | formRules, |
| | | |
| | | isEditingBuiltin, |
| | | |
| | | detailDialog, |
| | | |
| | | detailRow, |
| | | |
| | | detailLoading, |
| | | |
| | | fetchTemplateList, |
| | | |
| | | handleQuery, |
| | | |
| | | resetSearch, |
| | | |
| | | pagination, |
| | | |
| | | openFormDialog, |
| | | |
| | | openDetail, |
| | | |
| | | submitForm, |
| | | |
| | | } = useApproveTemplate(); |
| | | |
| | | |
| | | |
| | | const flowUserOptions = ref([]); |
| | | |
| | | |
| | | |
| | | const detailFormConfig = computed(() => |
| | | |
| | | parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig) |
| | | |
| | | ); |
| | | |
| | | |
| | | |
| | | const detailAttachments = computed(() => mapAttachmentsFromApi(detailRow.value)); |
| | | |
| | | |
| | | |
| | | function attachmentDisplayName(file) { |
| | | |
| | | if (!file) return "æªå½å"; |
| | | |
| | | return file.name || file.originalFilename || file.fileName || "æªå½å"; |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | function unwrapArray(payload) { |
| | | |
| | | if (Array.isArray(payload)) return payload; |
| | | |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | |
| | | return []; |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | function isActiveUser(u) { |
| | | |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | |
| | | if (u.status == null) return true; |
| | | |
| | | return String(u.status) === "0"; |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | async function loadUsers() { |
| | | |
| | | try { |
| | | |
| | | const res = await userListNoPageByTenantId(); |
| | | |
| | | flowUserOptions.value = unwrapArray(res).filter(isActiveUser); |
| | | |
| | | } catch { |
| | | |
| | | flowUserOptions.value = []; |
| | | |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | async function onSubmitForm() { |
| | | |
| | | const ret = await submitForm(); |
| | | |
| | | if (ret?.message) { |
| | | |
| | | ElMessage.warning(ret.message); |
| | | |
| | | return; |
| | | |
| | | } |
| | | |
| | | if (ret?.ok) ElMessage.success("ä¿åæå"); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | function onFormDialogClosed() { |
| | | |
| | | formRef.value?.resetFields?.(); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | async function editFromDetail() { |
| | | |
| | | const row = detailRow.value; |
| | | |
| | | detailDialog.visible = false; |
| | | |
| | | await nextTick(); |
| | | |
| | | openFormDialog("edit", row); |
| | | |
| | | } |
| | | |
| | | |
| | | |
| | | onMounted(() => { |
| | | |
| | | loadUsers(); |
| | | |
| | | loadTemplateTypeOptions(); |
| | | |
| | | fetchTemplateList(); |
| | | |
| | | }); |
| | | |
| | | </script> |
| | | |
| | | |
| | | |
| | | <style scoped> |
| | | |
| | | .mb20 { |
| | | |
| | | margin-bottom: 20px; |
| | | |
| | | } |
| | | |
| | | .mb16 { |
| | | |
| | | margin-bottom: 16px; |
| | | |
| | | } |
| | | |
| | | .mb16.el-empty { |
| | | |
| | | padding: 8px 0; |
| | | |
| | | } |
| | | |
| | | .ml10 { |
| | | |
| | | margin-left: 10px; |
| | | |
| | | } |
| | | |
| | | .ml12 { |
| | | |
| | | margin-left: 12px; |
| | | |
| | | } |
| | | |
| | | .search_form { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | align-items: center; |
| | | |
| | | justify-content: space-between; |
| | | |
| | | gap: 12px; |
| | | |
| | | } |
| | | |
| | | .search_fields { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | align-items: center; |
| | | |
| | | gap: 4px; |
| | | |
| | | } |
| | | |
| | | .search_actions { |
| | | |
| | | display: flex; |
| | | |
| | | gap: 8px; |
| | | |
| | | } |
| | | |
| | | .flow-tip { |
| | | |
| | | font-size: 12px; |
| | | |
| | | color: var(--el-text-color-secondary); |
| | | |
| | | margin: 8px 0 0; |
| | | |
| | | line-height: 1.5; |
| | | |
| | | } |
| | | |
| | | .detail-flow { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | align-items: flex-start; |
| | | |
| | | gap: 8px; |
| | | |
| | | } |
| | | |
| | | .detail-node { |
| | | |
| | | position: relative; |
| | | |
| | | min-width: 180px; |
| | | |
| | | max-width: 240px; |
| | | |
| | | padding: 12px; |
| | | |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | |
| | | border-radius: 8px; |
| | | |
| | | background: var(--el-fill-color-lighter); |
| | | |
| | | } |
| | | |
| | | .detail-node-head { |
| | | |
| | | display: flex; |
| | | |
| | | align-items: center; |
| | | |
| | | justify-content: space-between; |
| | | |
| | | margin-bottom: 8px; |
| | | |
| | | } |
| | | |
| | | .detail-node-order { |
| | | |
| | | font-weight: 600; |
| | | |
| | | font-size: 13px; |
| | | |
| | | } |
| | | |
| | | .detail-approvers { |
| | | |
| | | display: flex; |
| | | |
| | | flex-wrap: wrap; |
| | | |
| | | gap: 4px; |
| | | |
| | | } |
| | | |
| | | .detail-approver-tag { |
| | | |
| | | margin: 0; |
| | | |
| | | } |
| | | |
| | | .detail-arrow { |
| | | |
| | | position: absolute; |
| | | |
| | | right: -20px; |
| | | |
| | | top: 50%; |
| | | |
| | | transform: translateY(-50%); |
| | | |
| | | color: var(--el-text-color-placeholder); |
| | | |
| | | } |
| | | |
| | | .detail-dialog-body { |
| | | |
| | | min-height: 120px; |
| | | |
| | | } |
| | | |
| | | .upload-block { |
| | | |
| | | width: 100%; |
| | | |
| | | } |
| | | |
| | | .detail-attachment-tag { |
| | | |
| | | margin: 0 8px 8px 0; |
| | | |
| | | } |
| | | |
| | | .text-muted { |
| | | |
| | | font-size: 12px; |
| | | |
| | | color: var(--el-text-color-placeholder); |
| | | |
| | | } |
| | | |
| | | .template-form-dialog :deep(.el-dialog__body) { |
| | | |
| | | padding-top: 8px; |
| | | |
| | | } |
| | | |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | /** 䏿éé¡¹æ¥æºï¼åå
¥ formConfigï¼æäº¤é¡µææ¥æºæåæ°æ®ï¼ */ |
| | | export const SELECT_OPTION_SOURCE = { |
| | | STATIC: "static", |
| | | USER: "user", |
| | | DEPT: "dept", |
| | | }; |
| | | |
| | | export const SELECT_OPTION_SOURCE_OPTIONS = [ |
| | | { value: SELECT_OPTION_SOURCE.STATIC, label: "æå¨é
ç½®", desc: "卿¨¡æ¿ä¸èªå®ä¹éé¡¹ææ¬ä¸å¼" }, |
| | | { value: SELECT_OPTION_SOURCE.USER, label: "人åå表", desc: "ä»ç³»ç»ç¨æ·ä¸éæ©ï¼å¼ä¸ºç¨æ· ID" }, |
| | | { value: SELECT_OPTION_SOURCE.DEPT, label: "é¨é¨å表", desc: "ä»ç»ç»æ¶æä¸éæ©ï¼å¼ä¸ºé¨é¨ ID" }, |
| | | ]; |
| | | |
| | | export function selectOptionSourceLabel(source) { |
| | | return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.label || "â"; |
| | | } |
| | | |
| | | export function isDynamicOptionSource(source) { |
| | | return source === SELECT_OPTION_SOURCE.USER || source === SELECT_OPTION_SOURCE.DEPT; |
| | | } |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | /** ç¨æ· â 䏿 option */ |
| | | export function mapUserToSelectOption(u) { |
| | | const value = u.userId ?? u.id; |
| | | return { |
| | | label: u.nickName || u.userName || `ç¨æ·${value}`, |
| | | value, |
| | | }; |
| | | } |
| | | |
| | | /** é¨é¨æ æå¹³ä¸ºä¸æ option */ |
| | | export function flattenDeptToSelectOptions(nodes, result = []) { |
| | | (nodes || []).forEach((node) => { |
| | | const value = node.id ?? node.deptId ?? node.value; |
| | | if (value != null && value !== "") { |
| | | result.push({ |
| | | label: node.label ?? node.deptName ?? node.name ?? String(value), |
| | | value, |
| | | }); |
| | | } |
| | | if (node.children?.length) flattenDeptToSelectOptions(node.children, result); |
| | | }); |
| | | return result; |
| | | } |
| | | |
| | | function filterDisabledDept(deptList) { |
| | | if (!Array.isArray(deptList)) return []; |
| | | return deptList.filter((dept) => { |
| | | if (dept.disabled) return false; |
| | | if (dept.children?.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | } |
| | | |
| | | /** æå段é
置解æä¸æ optionsï¼éä¼ å
¥å·²å è½½çç¼åï¼ */ |
| | | export function resolveFieldSelectOptions(field, caches = {}) { |
| | | const source = field?.optionSource || SELECT_OPTION_SOURCE.STATIC; |
| | | if (source === SELECT_OPTION_SOURCE.USER) { |
| | | return (caches.users || []).map(mapUserToSelectOption); |
| | | } |
| | | if (source === SELECT_OPTION_SOURCE.DEPT) { |
| | | return caches.deptOptions || []; |
| | | } |
| | | return (field?.options || []).filter((o) => o.value !== "" && o.value != null); |
| | | } |
| | | |
| | | /** æ ¹æ®å·²è§£æç options 忥å±ç¤ºææ¬ */ |
| | | export function resolveSelectDisplayLabel(field, val, caches = {}) { |
| | | if (val == null || val === "") return "â"; |
| | | const options = resolveFieldSelectOptions(field, caches); |
| | | const hit = options.find((o) => String(o.value) === String(val)); |
| | | return hit?.label || String(val); |
| | | } |
| | | |
| | | /** å 载人å / é¨é¨ç¼åï¼å¤å¤å¤ç¨ï¼ */ |
| | | export async function fetchSelectOptionCaches(sources = []) { |
| | | const needUser = sources.includes(SELECT_OPTION_SOURCE.USER); |
| | | const needDept = sources.includes(SELECT_OPTION_SOURCE.DEPT); |
| | | const caches = { users: [], deptOptions: [] }; |
| | | |
| | | if (!needUser && !needDept) return caches; |
| | | |
| | | const tasks = []; |
| | | if (needUser) { |
| | | tasks.push( |
| | | userListNoPageByTenantId() |
| | | .then((res) => { |
| | | caches.users = unwrapArray(res).filter(isActiveUser); |
| | | }) |
| | | .catch(() => { |
| | | caches.users = []; |
| | | }) |
| | | ); |
| | | } |
| | | if (needDept) { |
| | | tasks.push( |
| | | deptTreeSelect() |
| | | .then((res) => { |
| | | let tree = unwrapArray(res); |
| | | tree = tree.length ? filterDisabledDept(JSON.parse(JSON.stringify(tree))) : []; |
| | | if (!tree.length) tree = unwrapArray(res); |
| | | caches.deptOptions = flattenDeptToSelectOptions(tree); |
| | | }) |
| | | .catch(() => { |
| | | caches.deptOptions = []; |
| | | }) |
| | | ); |
| | | } |
| | | |
| | | await Promise.all(tasks); |
| | | return caches; |
| | | } |
| | | |
| | | /** ä»å段å表æ¶ééè¦é¢å è½½çå¨ææ¥æº */ |
| | | export function collectOptionSourcesFromFields(fields) { |
| | | const set = new Set(); |
| | | (fields || []).forEach((f) => { |
| | | if (f?.type === "select" && isDynamicOptionSource(f.optionSource)) { |
| | | set.add(f.optionSource); |
| | | } |
| | | }); |
| | | return [...set]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { |
| | | addApprovalTemplate, |
| | | deleteApprovalTemplate, |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplatePage, |
| | | TEMPLATE_TYPE_BUILTIN, |
| | | updateApprovalTemplate, |
| | | } from "@/api/officeProcessAutomation/approvalTemplate.js"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { |
| | | buildApprovalTemplateListParams, |
| | | createEmptyTemplateForm, |
| | | fetchBusinessTypeOptions, |
| | | flowNodesSummary, |
| | | isBuiltinTemplate, |
| | | mapTemplateFromApi, |
| | | mapTemplateToApi, |
| | | nodeSignModeLabel, |
| | | formatDisplayTime, |
| | | unwrapTemplateDetail, |
| | | validateTemplateForm, |
| | | } from "./approveTemplateConstants.js"; |
| | | import { parseFormConfigToData } from "./formConfigUtils.js"; |
| | | |
| | | const FALLBACK_TEMPLATE_TYPE_OPTIONS = [ |
| | | { value: 0, label: "ç³»ç»å
ç½®" }, |
| | | { value: 1, label: "èªå®ä¹" }, |
| | | ]; |
| | | |
| | | function matchTemplateTypeValue(options, type) { |
| | | if (type == null || type === "") return false; |
| | | return options.some( |
| | | (x) => x.value === type || x.value === Number(type) || String(x.value) === String(type) |
| | | ); |
| | | } |
| | | |
| | | export function useApproveTemplate() { |
| | | const templateTypeOptions = ref([...FALLBACK_TEMPLATE_TYPE_OPTIONS]); |
| | | |
| | | function templateTypeLabel(type) { |
| | | if (type == null || type === "") return "â"; |
| | | const hit = templateTypeOptions.value.find( |
| | | (x) => x.value === type || x.value === Number(type) || String(x.value) === String(type) |
| | | ); |
| | | return hit?.label || "â"; |
| | | } |
| | | |
| | | const searchForm = reactive({ |
| | | keyword: "", |
| | | enabledOnly: false, |
| | | }); |
| | | |
| | | 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 formRef = ref(); |
| | | |
| | | const isEditingBuiltin = computed( |
| | | () => formDialog.mode === "edit" && Number(form.templateType) === TEMPLATE_TYPE_BUILTIN |
| | | ); |
| | | |
| | | async function loadTemplateTypeOptions() { |
| | | try { |
| | | const list = await fetchBusinessTypeOptions(); |
| | | templateTypeOptions.value = list.length ? list : [...FALLBACK_TEMPLATE_TYPE_OPTIONS]; |
| | | } catch { |
| | | templateTypeOptions.value = [...FALLBACK_TEMPLATE_TYPE_OPTIONS]; |
| | | } |
| | | if (!matchTemplateTypeValue(templateTypeOptions.value, form.businessType)) { |
| | | form.businessType = templateTypeOptions.value[0]?.value ?? ""; |
| | | } |
| | | } |
| | | |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | const detailLoading = ref(false); |
| | | |
| | | const formRules = { |
| | | templateName: [{ required: true, message: "请è¾å
¥æ¨¡æ¿åç§°", trigger: "blur" }], |
| | | businessType: [{ required: true, message: "è¯·éæ©æ¨¡æ¿ç±»å", trigger: "change" }], |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "模æ¿åç§°", prop: "templateName", minWidth: 140 }, |
| | | { |
| | | label: "模æ¿ç±»å", |
| | | prop: "businessType", |
| | | width: 100, |
| | | align: "center", |
| | | formatData: (v) => templateTypeLabel(v), |
| | | }, |
| | | { label: "说æ", prop: "description", minWidth: 160, showOverflowTooltip: true }, |
| | | { |
| | | label: "èç¹æ°", |
| | | prop: "flowNodes", |
| | | width: 80, |
| | | align: "center", |
| | | formatData: (v) => (Array.isArray(v) ? v.length : 0), |
| | | }, |
| | | { |
| | | label: "æµç¨æ¦è¦", |
| | | prop: "flowNodes", |
| | | minWidth: 220, |
| | | showOverflowTooltip: true, |
| | | formatData: (v) => flowNodesSummary(v), |
| | | }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "enabled", |
| | | width: 90, |
| | | align: "center", |
| | | dataType: "tag", |
| | | formatData: (v) => (v !== false ? "å¯ç¨" : "åç¨"), |
| | | formatType: (v) => (v !== false ? "success" : "info"), |
| | | }, |
| | | { |
| | | 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: 220, |
| | | operation: [ |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { name: "ç¼è¾", type: "text", clickFun: (row) => openFormDialog("edit", row) }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | link: true, |
| | | disabled: (row) => isBuiltinTemplate(row), |
| | | clickFun: (row) => removeTemplate(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | 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() { |
| | | page.current = 1; |
| | | fetchTemplateList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.keyword = ""; |
| | | searchForm.enabledOnly = false; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination({ page: p, limit }) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | fetchTemplateList(); |
| | | } |
| | | |
| | | function resetForm(row) { |
| | | const base = createEmptyTemplateForm(); |
| | | if (!row) { |
| | | Object.assign(form, base); |
| | | return; |
| | | } |
| | | const formConfigData = JSON.parse( |
| | | JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig)) |
| | | ); |
| | | const builtin = isBuiltinTemplate(row); |
| | | Object.assign(form, { |
| | | ...base, |
| | | id: row.id, |
| | | templateName: row.templateName || "", |
| | | description: row.description || "", |
| | | templateType: row.templateType != null ? Number(row.templateType) : base.templateType, |
| | | businessType: row.businessType ?? "", |
| | | formConfig: row.formConfig || "", |
| | | formConfigData, |
| | | lockedFormFieldUids: builtin |
| | | ? (formConfigData.fields || []).map((f) => f._uid).filter(Boolean) |
| | | : [], |
| | | enabled: row.enabled !== false, |
| | | flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])), |
| | | storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])), |
| | | }); |
| | | } |
| | | |
| | | function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.title = mode === "add" ? "æ°å»ºå®¡æ¹æ¨¡æ¿" : "ç¼è¾å®¡æ¹æ¨¡æ¿"; |
| | | resetForm(mode === "edit" ? row : null); |
| | | formDialog.visible = true; |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | if (row?.id == null || row.id === "") { |
| | | ElMessage.warning("æ æ³æ¥ç详æ
ï¼ç¼ºå°æ¨¡æ¿ ID"); |
| | | return; |
| | | } |
| | | detailDialog.visible = true; |
| | | 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 (!formRef.value) return false; |
| | | try { |
| | | await formRef.value.validate(); |
| | | } catch { |
| | | return false; |
| | | } |
| | | const validated = validateTemplateForm(form); |
| | | if (!validated.ok) { |
| | | return { message: validated.message }; |
| | | } |
| | | if (formDialog.mode === "edit" && !form.id) { |
| | | return { message: "ç¼ºå°æ¨¡æ¿ IDï¼æ æ³ä¿åä¿®æ¹" }; |
| | | } |
| | | const dto = mapTemplateToApi(form); |
| | | try { |
| | | if (formDialog.mode === "add") { |
| | | await addApprovalTemplate(dto); |
| | | } else { |
| | | await updateApprovalTemplate(dto); |
| | | } |
| | | } catch { |
| | | return false; |
| | | } |
| | | formDialog.visible = false; |
| | | page.current = 1; |
| | | await fetchTemplateList(); |
| | | return { ok: true }; |
| | | } |
| | | |
| | | async function removeTemplate(row) { |
| | | if (isBuiltinTemplate(row)) { |
| | | ElMessage.warning("ç³»ç»å
置模æ¿ä¸å
许å é¤"); |
| | | return; |
| | | } |
| | | if (row?.id == null || row.id === "") { |
| | | ElMessage.warning("æ æ³å é¤ï¼ç¼ºå°æ¨¡æ¿ ID"); |
| | | return; |
| | | } |
| | | const name = row.templateName || "æªå½å模æ¿"; |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤å®¡æ¹æ¨¡æ¿ã${name}ãåï¼å é¤åä¸å¯æ¢å¤ã`, |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "ç¡®å®å é¤", |
| | | cancelButtonText: "åæ¶", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | try { |
| | | await deleteApprovalTemplate([row.id]); |
| | | ElMessage.success("å 餿å"); |
| | | await fetchTemplateList(); |
| | | } catch { |
| | | /* éè¯¯ç±æ¦æªå¨æç¤º */ |
| | | } |
| | | } |
| | | |
| | | return { |
| | | Search, |
| | | templateTypeOptions, |
| | | loadTemplateTypeOptions, |
| | | templateTypeLabel, |
| | | fetchTemplateList, |
| | | nodeSignModeLabel, |
| | | flowNodesSummary, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | formDialog, |
| | | form, |
| | | formRef, |
| | | formRules, |
| | | isEditingBuiltin, |
| | | detailDialog, |
| | | detailRow, |
| | | detailLoading, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | openFormDialog, |
| | | openDetail, |
| | | submitForm, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { reactive, ref } from "vue"; |
| | | import { |
| | | collectOptionSourcesFromFields, |
| | | fetchSelectOptionCaches, |
| | | resolveFieldSelectOptions, |
| | | resolveSelectDisplayLabel, |
| | | } from "./selectOptionSource.js"; |
| | | |
| | | /** 䏿卿é项ï¼äººå / é¨é¨ç¼åä¸è§£æ */ |
| | | export function useSelectOptionSources() { |
| | | const loading = ref(false); |
| | | const caches = reactive({ |
| | | users: [], |
| | | deptOptions: [], |
| | | }); |
| | | |
| | | async function ensureForFields(fields) { |
| | | const sources = collectOptionSourcesFromFields(fields); |
| | | if (!sources.length) return; |
| | | loading.value = true; |
| | | try { |
| | | const next = await fetchSelectOptionCaches(sources); |
| | | caches.users = next.users; |
| | | caches.deptOptions = next.deptOptions; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function getOptions(field) { |
| | | return resolveFieldSelectOptions(field, caches); |
| | | } |
| | | |
| | | function getDisplayLabel(field, val) { |
| | | return resolveSelectDisplayLabel(field, val, caches); |
| | | } |
| | | |
| | | return { |
| | | loading, |
| | | caches, |
| | | ensureForFields, |
| | | getOptions, |
| | | getDisplayLabel, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼è¯·åç³è¯·--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">审æ¹åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.instanceNo" |
| | | style="width: 220px" |
| | | placeholder="请è¾å
¥å®¡æ¹åå·" |
| | | clearable |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">ç³è¯·äººï¼</span> |
| | | <el-input |
| | | v-model="searchForm.applicantKeyword" |
| | | style="width: 220px" |
| | | placeholder="å§åæç¼å·" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <el-button type="primary" style="margin-left: 10px" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openAddWithTemplate">æ°å¢è¯·åç³è¯·</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | @pagination="onPagination" |
| | | :total="page.total" |
| | | /> |
| | | </div> |
| | | |
| | | <ApprovalInstanceSubmitDialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialogTitle" |
| | | :form="submitForm" |
| | | :rules="submitFormRules" |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | | flow-attachments-only |
| | | @submit="onSubmit" |
| | | > |
| | | <template #before="{ form, fields }"> |
| | | <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" /> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åæä½é¢" prop="leaveBalanceDays"> |
| | | <el-input-number |
| | | v-model="form.leaveBalanceDays" |
| | | :min="0" |
| | | :max="999" |
| | | :precision="2" |
| | | :step="0.5" |
| | | controls-position="right" |
| | | placeholder="天" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è¯·åæ¶é¿"> |
| | | <el-input :model-value="leaveDurationDisplay(form)" readonly placeholder="æ ¹æ®æ¨¡æ¿ä¸è¯·åæ¶é´èªå¨è®¡ç®"> |
| | | <template #append>天</template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </template> |
| | | </ApprovalInstanceSubmitDialog> |
| | | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.LEAVE" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <ApprovalInstanceDetailDialog |
| | | v-model="detailDialog.visible" |
| | | title="请åç³è¯·è¯¦æ
" |
| | | :row="detailRow" |
| | | @edit="openEditFromDetail" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { computed, onMounted, reactive, ref, watch } from "vue"; |
| | | import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue"; |
| | | import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue"; |
| | | import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue"; |
| | | import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue"; |
| | | import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js"; |
| | | |
| | | const LEAVE_TYPE_OPTIONS = [ |
| | | { label: "å¹´å", value: "annual" }, |
| | | { label: "ç
å", value: "sick" }, |
| | | { label: "äºå", value: "personal" }, |
| | | { label: "å©å", value: "marriage" }, |
| | | { label: "产å", value: "maternity" }, |
| | | { label: "åºä¹³å", value: "nursing" }, |
| | | { label: "æ
°åå", value: "condolence" }, |
| | | { label: "è°ä¼", value: "compensatory" }, |
| | | ]; |
| | | |
| | | function isLeaveBalanceField(field) { |
| | | const label = String(field?.label || ""); |
| | | return label.includes("åæä½é¢") || field?.key === "leaveBalanceDays"; |
| | | } |
| | | |
| | | function isLeaveDurationField(field) { |
| | | const label = String(field?.label || ""); |
| | | return label.includes("è¯·åæ¶é¿") || field?.key === "leaveDurationDays"; |
| | | } |
| | | |
| | | function displayTemplateFields(fields = []) { |
| | | return (fields || []).filter((f) => !isLeaveBalanceField(f) && !isLeaveDurationField(f)); |
| | | } |
| | | |
| | | function findLeaveTimeTemplateField(fields = []) { |
| | | return ( |
| | | fields.find((f) => f?.type === "datetimerange" && String(f?.label || "").includes("è¯·åæ¶é´")) || |
| | | fields.find((f) => f?.type === "datetimerange" && f?.key === "dateRange") || |
| | | fields.find((f) => f?.type === "datetimerange") || |
| | | null |
| | | ); |
| | | } |
| | | |
| | | function findApplicantTemplateField(fields = []) { |
| | | return ( |
| | | fields.find((f) => String(f?.label || "").includes("ç³è¯·äºº")) || |
| | | fields.find((f) => f?.type === "select" && f?.optionSource === SELECT_OPTION_SOURCE.USER) || |
| | | null |
| | | ); |
| | | } |
| | | |
| | | function resolveLeaveTimeRange(payload, leaveTimeField) { |
| | | if (!leaveTimeField?.key) return { start: "", end: "" }; |
| | | const val = payload?.[leaveTimeField.key]; |
| | | if (!Array.isArray(val) || val.length < 2) return { start: "", end: "" }; |
| | | return { start: val[0] || "", end: val[1] || "" }; |
| | | } |
| | | |
| | | function computeLeaveDays(startStr, endStr) { |
| | | if (!startStr || !endStr) return null; |
| | | const t0 = dayjs(startStr); |
| | | const t1 = dayjs(endStr); |
| | | if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null; |
| | | const days = t1.diff(t0, "millisecond") / (24 * 60 * 60 * 1000); |
| | | return Math.round(days * 100) / 100; |
| | | } |
| | | |
| | | function leaveDurationDisplay(form) { |
| | | const leaveTimeField = findLeaveTimeTemplateField(form.formFieldDefs); |
| | | const { start, end } = resolveLeaveTimeRange(form.formPayload, leaveTimeField); |
| | | const d = computeLeaveDays(start, end); |
| | | return d == null ? "" : String(d); |
| | | } |
| | | |
| | | const searchForm = reactive({ |
| | | instanceNo: "", |
| | | applicantKeyword: "", |
| | | }); |
| | | |
| | | function validateLeaveBeforeSave() { |
| | | const leaveTimeField = findLeaveTimeTemplateField(submitForm.formFieldDefs); |
| | | const { start, end } = resolveLeaveTimeRange(submitForm.formPayload, leaveTimeField); |
| | | if (computeLeaveDays(start, end) == null) { |
| | | ElMessage.warning("è¯·æ£æ¥æ¨¡æ¿ä¸çè¯·åæ¶é´ï¼ç»ææ¶é´é¡»æäºå¼å§æ¶é´"); |
| | | throw new Error("invalid leave time"); |
| | | } |
| | | } |
| | | |
| | | const mod = useApprovalInstanceModule({ |
| | | moduleKey: APPROVAL_MODULE_KEYS.LEAVE, |
| | | beforeSave: validateLeaveBeforeSave, |
| | | extraFormRules: { |
| | | leaveBalanceDays: [{ required: true, message: "请填ååæä½é¢", trigger: "blur" }], |
| | | }, |
| | | }); |
| | | |
| | | const { |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | detailDialog, |
| | | detailRow, |
| | | submitDialog, |
| | | submitEditRow, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | isSubmitEdit, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitDialogTitle, |
| | | templateBindVisible, |
| | | handleQuery, |
| | | initModuleList, |
| | | pagination, |
| | | openAddWithTemplate, |
| | | onTemplateBound, |
| | | onTemplateBindClosed, |
| | | openEditFromDetail, |
| | | submitInstanceForm, |
| | | buildTableActions, |
| | | } = mod; |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | const allUsersCache = ref([]); |
| | | |
| | | const applicantTemplateField = computed(() => |
| | | findApplicantTemplateField(submitForm.formFieldDefs) |
| | | ); |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload && Array.isArray(payload.data)) return payload.data; |
| | | return []; |
| | | } |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | allUsersCache.value = unwrapArray(res); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => submitDialog.visible, |
| | | (v) => { |
| | | if (!v) return; |
| | | if (submitForm.leaveBalanceDays == null && isSubmitEdit.value) { |
| | | submitForm.leaveBalanceDays = |
| | | submitEditRow.value?.formPayload?.leaveBalanceDays ?? |
| | | submitEditRow.value?.leaveBalanceDays; |
| | | } |
| | | if (submitForm.leaveBalanceDays == null && !isSubmitEdit.value) { |
| | | submitForm.leaveBalanceDays = undefined; |
| | | } |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => { |
| | | const key = applicantTemplateField.value?.key; |
| | | return key ? submitForm.formPayload[key] : undefined; |
| | | }, |
| | | async (uid) => { |
| | | if (!applicantTemplateField.value || !uid) return; |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | } |
| | | ); |
| | | |
| | | const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, { |
| | | moduleKey: APPROVAL_MODULE_KEYS.LEAVE, |
| | | }); |
| | | |
| | | function onSearch() { |
| | | handleQuery(searchForm); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.instanceNo = ""; |
| | | searchForm.applicantKeyword = ""; |
| | | onSearch(); |
| | | } |
| | | |
| | | function onPagination(obj) { |
| | | pagination(obj, searchForm); |
| | | } |
| | | |
| | | async function onSubmit() { |
| | | const ok = await submitInstanceForm({ skipValidate: true }); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "æäº¤æå"); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | loadFlowUsers(); |
| | | await initModuleList(searchForm); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å çç³è¯·æ¨¡åå
ï¼å¯å¢å 审æ¹èç¹ï¼æ¯èç¹å¿
é 1 人 --> |
| | | <template> |
| | | <div class="afe"> |
| | | <div v-if="innerList.length" class="afe-flow"> |
| | | <div v-for="(item, index) in innerList" :key="item._uid" class="afe-flow-item"> |
| | | <div class="afe-card" :class="{ 'afe-card--empty': !item.approverId }"> |
| | | <div class="afe-badge">{{ index + 1 }}</div> |
| | | <div class="afe-avatar-wrap"> |
| | | <div |
| | | class="afe-avatar" |
| | | :class="{ 'afe-avatar--on': item.approverId }" |
| | | :style="item.approverId ? { backgroundColor: avatarColor(item.approverName) } : {}" |
| | | > |
| | | <span v-if="item.approverId">{{ (item.approverName || '?').charAt(0) }}</span> |
| | | <el-icon v-else :size="22"><User /></el-icon> |
| | | </div> |
| | | <div class="afe-level">{{ levelText(index) }}</div> |
| | | </div> |
| | | <div class="afe-select"> |
| | | <el-select |
| | | v-model="item.approverId" |
| | | placeholder="è¯·éæ©å®¡æ¹äºº" |
| | | filterable |
| | | clearable |
| | | style="width: 100%" |
| | | @change="(v) => onPick(v, item)" |
| | | > |
| | | <el-option |
| | | v-for="u in userOptions" |
| | | :key="String(u.userId ?? u.id)" |
| | | :label="optionLabel(u)" |
| | | :value="u.userId ?? u.id" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | <div class="afe-actions"> |
| | | <el-button type="primary" circle size="small" :disabled="index === 0" title="åç§»" @click="moveLeft(index)"> |
| | | <el-icon><ArrowLeft /></el-icon> |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | circle |
| | | size="small" |
| | | :disabled="index === innerList.length - 1" |
| | | title="åç§»" |
| | | @click="moveRight(index)" |
| | | > |
| | | <el-icon><ArrowRight /></el-icon> |
| | | </el-button> |
| | | <el-button type="danger" circle size="small" title="å é¤èç¹" @click="remove(index)"> |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <div v-if="index < innerList.length - 1" class="afe-conn"> |
| | | <div class="afe-conn-line"></div> |
| | | <el-icon class="afe-conn-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="afe-add-wrap"> |
| | | <div class="afe-conn" v-if="innerList.length"> |
| | | <div class="afe-conn-line"></div> |
| | | <el-icon class="afe-conn-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | <div class="afe-add-card" @click="addNode"> |
| | | <div class="afe-add-icon"><el-icon :size="26"><Plus /></el-icon></div> |
| | | <span>æ°å¢èç¹</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else class="afe-empty"> |
| | | <el-icon :size="44" color="#c0c4cc"><User /></el-icon> |
| | | <p>ææ å®¡æ¹èç¹</p> |
| | | <el-button type="primary" @click="addNode">æ·»å 第ä¸ä¸ªèç¹</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ArrowLeft, ArrowRight, Delete, Plus, User } from "@element-plus/icons-vue"; |
| | | import { ref, watch } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Array, default: () => [] }, |
| | | /** ä¸ç¶é¡µ userList ç»æä¸è´ï¼userId / idãnickNameãuserName */ |
| | | userOptions: { type: Array, default: () => [] }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | const innerList = ref([]); |
| | | |
| | | const palette = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#9B59B6", "#1ABC9C"]; |
| | | |
| | | function avatarColor(name) { |
| | | if (!name) return "#c0c4cc"; |
| | | let h = 0; |
| | | for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h); |
| | | return palette[Math.abs(h) % palette.length]; |
| | | } |
| | | |
| | | function levelText(i) { |
| | | const t = ["第ä¸çº§", "第äºçº§", "第ä¸çº§", "第å级", "第äºçº§", "第å
级", "第ä¸çº§", "第å
«çº§"]; |
| | | return t[i] || `第${i + 1}级`; |
| | | } |
| | | |
| | | function optionLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const un = u.userName || ""; |
| | | if (nick && un && nick !== un) return `${nick}ï¼${un}ï¼`; |
| | | return nick || un || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function newUid() { |
| | | return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; |
| | | } |
| | | |
| | | function mapIn(rows) { |
| | | if (!Array.isArray(rows)) return []; |
| | | return rows.map((r, i) => ({ |
| | | _uid: newUid(), |
| | | approverId: r.approverId ?? r.approver_id ?? null, |
| | | approverName: r.approverName ?? r.approver_name ?? "", |
| | | sortOrder: r.sortOrder ?? r.nodeOrder ?? i + 1, |
| | | nodeOrder: r.nodeOrder ?? r.sortOrder ?? i + 1, |
| | | roleName: r.roleName ?? "", |
| | | roleCode: r.roleCode ?? "", |
| | | })); |
| | | } |
| | | |
| | | function publicShape(rows) { |
| | | const arr = Array.isArray(rows) ? rows : []; |
| | | return arr.map((r, i) => ({ |
| | | approverId: r.approverId ?? null, |
| | | approverName: r.approverName ?? "", |
| | | roleName: r.roleName ?? "", |
| | | roleCode: r.roleCode ?? "", |
| | | sortOrder: i + 1, |
| | | })); |
| | | } |
| | | |
| | | function emitOut() { |
| | | const out = innerList.value.map((r, i) => ({ |
| | | approverId: r.approverId ?? null, |
| | | approverName: r.approverName ?? "", |
| | | sortOrder: i + 1, |
| | | nodeOrder: i + 1, |
| | | roleName: r.roleName ?? "", |
| | | roleCode: r.roleCode ?? "", |
| | | })); |
| | | emit("update:modelValue", out); |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | (v) => { |
| | | const next = publicShape(v || []); |
| | | if (JSON.stringify(next) === JSON.stringify(publicShape(innerList.value))) return; |
| | | innerList.value = mapIn(v || []); |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | function findUser(id) { |
| | | if (id == null || id === "") return null; |
| | | return props.userOptions.find((u) => String(u.userId ?? u.id) === String(id)) ?? null; |
| | | } |
| | | |
| | | function onPick(userId, row) { |
| | | if (!userId) { |
| | | row.approverName = ""; |
| | | emitOut(); |
| | | return; |
| | | } |
| | | const u = findUser(userId); |
| | | row.approverName = u ? u.nickName || u.userName || "" : ""; |
| | | emitOut(); |
| | | } |
| | | |
| | | function addNode() { |
| | | innerList.value.push({ |
| | | _uid: newUid(), |
| | | approverId: null, |
| | | approverName: "", |
| | | roleName: "", |
| | | roleCode: "", |
| | | }); |
| | | emitOut(); |
| | | } |
| | | |
| | | function remove(index) { |
| | | innerList.value.splice(index, 1); |
| | | emitOut(); |
| | | } |
| | | |
| | | function moveLeft(index) { |
| | | if (index < 1) return; |
| | | const t = innerList.value[index]; |
| | | innerList.value[index] = innerList.value[index - 1]; |
| | | innerList.value[index - 1] = t; |
| | | emitOut(); |
| | | } |
| | | |
| | | function moveRight(index) { |
| | | if (index >= innerList.value.length - 1) return; |
| | | const t = innerList.value[index]; |
| | | innerList.value[index] = innerList.value[index + 1]; |
| | | innerList.value[index + 1] = t; |
| | | emitOut(); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .afe { |
| | | width: 100%; |
| | | } |
| | | .afe-flow { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | flex-wrap: nowrap; |
| | | overflow-x: auto; |
| | | padding: 6px 0 10px; |
| | | gap: 0; |
| | | } |
| | | .afe-flow-item { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .afe-card { |
| | | width: 200px; |
| | | flex-shrink: 0; |
| | | border: 2px solid var(--el-border-color); |
| | | border-radius: 12px; |
| | | padding: 14px 12px 12px; |
| | | position: relative; |
| | | background: var(--el-bg-color); |
| | | } |
| | | .afe-card--empty { |
| | | border-style: dashed; |
| | | background: var(--el-fill-color-lighter); |
| | | } |
| | | .afe-badge { |
| | | position: absolute; |
| | | top: -8px; |
| | | left: 12px; |
| | | width: 22px; |
| | | height: 22px; |
| | | border-radius: 50%; |
| | | background: var(--el-color-primary); |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .afe-avatar-wrap { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | margin: 6px 0 10px; |
| | | } |
| | | .afe-avatar { |
| | | width: 48px; |
| | | height: 48px; |
| | | border-radius: 50%; |
| | | background: var(--el-fill-color); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: var(--el-text-color-placeholder); |
| | | margin-bottom: 6px; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | } |
| | | .afe-avatar--on { |
| | | color: #fff; |
| | | } |
| | | .afe-level { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | } |
| | | .afe-select { |
| | | margin-bottom: 10px; |
| | | } |
| | | .afe-actions { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | padding-top: 10px; |
| | | border-top: 1px solid var(--el-border-color-lighter); |
| | | } |
| | | .afe-conn { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 40px; |
| | | flex-shrink: 0; |
| | | align-self: center; |
| | | } |
| | | .afe-conn-line { |
| | | flex: 1; |
| | | height: 2px; |
| | | background: var(--el-border-color); |
| | | } |
| | | .afe-conn-icon { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-placeholder); |
| | | margin-left: -2px; |
| | | } |
| | | .afe-add-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .afe-add-card { |
| | | width: 120px; |
| | | min-height: 168px; |
| | | flex-shrink: 0; |
| | | border: 2px dashed var(--el-border-color); |
| | | border-radius: 12px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 10px; |
| | | cursor: pointer; |
| | | color: var(--el-text-color-regular); |
| | | font-size: 13px; |
| | | background: var(--el-fill-color-lighter); |
| | | transition: border-color 0.2s, background 0.2s; |
| | | } |
| | | .afe-add-card:hover { |
| | | border-color: var(--el-color-primary); |
| | | background: var(--el-color-primary-light-9); |
| | | color: var(--el-color-primary); |
| | | } |
| | | .afe-add-icon { |
| | | width: 44px; |
| | | height: 44px; |
| | | border-radius: 50%; |
| | | background: var(--el-color-primary); |
| | | color: #fff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .afe-empty { |
| | | text-align: center; |
| | | padding: 28px 16px; |
| | | border: 1px dashed var(--el-border-color); |
| | | border-radius: 12px; |
| | | background: var(--el-fill-color-lighter); |
| | | } |
| | | .afe-empty p { |
| | | margin: 10px 0 14px; |
| | | color: var(--el-text-color-secondary); |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å çç³è¯·--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">审æ¹åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.instanceNo" |
| | | style="width: 220px" |
| | | placeholder="请è¾å
¥å®¡æ¹åå·" |
| | | clearable |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">ç³è¯·äººï¼</span> |
| | | <el-input |
| | | v-model="searchForm.applicantKeyword" |
| | | style="width: 220px" |
| | | placeholder="å§åæç¼å·" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <el-button type="primary" style="margin-left: 10px" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="warning" plain @click="handleExport">导åº</el-button> |
| | | <el-button type="primary" @click="openAddWithTemplate">æ°å¢å çç³è¯·</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | @pagination="onPagination" |
| | | :total="page.total" |
| | | /> |
| | | </div> |
| | | |
| | | <ApprovalInstanceSubmitDialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialogTitle" |
| | | :form="submitForm" |
| | | :rules="submitFormRules" |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | | flow-attachments-only |
| | | @submit="onSubmit" |
| | | > |
| | | <template #before="{ form, fields }"> |
| | | <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" /> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å çæ¶é¿"> |
| | | <el-input :model-value="overtimeHoursDisplay(form)" readonly placeholder="æ ¹æ®æ¨¡æ¿ä¸å çæ¶é´èªå¨è®¡ç®"> |
| | | <template #append>å°æ¶</template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </template> |
| | | </ApprovalInstanceSubmitDialog> |
| | | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.OVERTIME" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <ApprovalInstanceDetailDialog |
| | | v-model="detailDialog.visible" |
| | | title="å çç³è¯·è¯¦æ
" |
| | | :row="detailRow" |
| | | @edit="openEditFromDetail" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { getCurrentInstance, onMounted, reactive, ref } from "vue"; |
| | | import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue"; |
| | | import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue"; |
| | | import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue"; |
| | | import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue"; |
| | | import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | |
| | | const OVERTIME_TYPE_OPTIONS = [ |
| | | { label: "工使¥å ç", value: "weekday" }, |
| | | { label: "伿¯æ¥å ç", value: "weekend" }, |
| | | { label: "æ³å®è忥å ç", value: "holiday" }, |
| | | ]; |
| | | |
| | | function isOvertimeDurationField(field) { |
| | | const label = String(field?.label || ""); |
| | | return label.includes("å çæ¶é¿") || field?.key === "overtimeHours"; |
| | | } |
| | | |
| | | function displayTemplateFields(fields = []) { |
| | | return (fields || []).filter((f) => !isOvertimeDurationField(f)); |
| | | } |
| | | |
| | | function findOvertimeTimeTemplateField(fields = []) { |
| | | return ( |
| | | fields.find((f) => f?.type === "datetimerange" && String(f?.label || "").includes("å çæ¶é´")) || |
| | | fields.find((f) => f?.type === "datetimerange") || |
| | | null |
| | | ); |
| | | } |
| | | |
| | | function resolveOvertimeTimeRange(payload, overtimeTimeField) { |
| | | if (!overtimeTimeField?.key) return { start: "", end: "" }; |
| | | const val = payload?.[overtimeTimeField.key]; |
| | | if (!Array.isArray(val) || val.length < 2) return { start: "", end: "" }; |
| | | return { start: val[0] || "", end: val[1] || "" }; |
| | | } |
| | | |
| | | function computeOvertimeHours(startStr, endStr) { |
| | | if (!startStr || !endStr) return null; |
| | | const t0 = dayjs(startStr); |
| | | const t1 = dayjs(endStr); |
| | | if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null; |
| | | return Math.round((t1.diff(t0, "millisecond") / 3600000) * 100) / 100; |
| | | } |
| | | |
| | | function overtimeHoursDisplay(form) { |
| | | const field = findOvertimeTimeTemplateField(form.formFieldDefs); |
| | | const { start, end } = resolveOvertimeTimeRange(form.formPayload, field); |
| | | const h = computeOvertimeHours(start, end); |
| | | return h == null ? "" : String(h); |
| | | } |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const searchForm = reactive({ |
| | | instanceNo: "", |
| | | applicantKeyword: "", |
| | | }); |
| | | |
| | | const mod = useApprovalInstanceModule({ |
| | | moduleKey: APPROVAL_MODULE_KEYS.OVERTIME, |
| | | beforeSave: validateOvertimeBeforeSave, |
| | | }); |
| | | |
| | | const { |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | detailDialog, |
| | | detailRow, |
| | | submitDialog, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | isSubmitEdit, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitDialogTitle, |
| | | templateBindVisible, |
| | | handleQuery, |
| | | initModuleList, |
| | | pagination, |
| | | openAddWithTemplate, |
| | | onTemplateBound, |
| | | onTemplateBindClosed, |
| | | openEditFromDetail, |
| | | submitInstanceForm, |
| | | buildTableActions, |
| | | } = mod; |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | function validateOvertimeBeforeSave() { |
| | | const field = findOvertimeTimeTemplateField(submitForm.formFieldDefs); |
| | | const { start, end } = resolveOvertimeTimeRange(submitForm.formPayload, field); |
| | | if (computeOvertimeHours(start, end) == null) { |
| | | ElMessage.warning("è¯·æ£æ¥æ¨¡æ¿ä¸çå çæ¶é´ï¼ç»ææ¶é´é¡»æäºå¼å§æ¶é´"); |
| | | throw new Error("invalid overtime time"); |
| | | } |
| | | } |
| | | |
| | | const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, { |
| | | moduleKey: APPROVAL_MODULE_KEYS.OVERTIME, |
| | | }); |
| | | |
| | | function onSearch() { |
| | | handleQuery(searchForm); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.instanceNo = ""; |
| | | searchForm.applicantKeyword = ""; |
| | | onSearch(); |
| | | } |
| | | |
| | | function onPagination(obj) { |
| | | pagination(obj, searchForm); |
| | | } |
| | | |
| | | async function onSubmit() { |
| | | const ok = await submitInstanceForm({ skipValidate: true }); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "æäº¤æå"); |
| | | } |
| | | |
| | | function handleExport() { |
| | | const data = tableData.value; |
| | | const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" }); |
| | | const url = URL.createObjectURL(blob); |
| | | const a = document.createElement("a"); |
| | | a.href = url; |
| | | a.download = `å çç³è¯·å¯¼åº_${dayjs().format("YYYYMMDDHHmmss")}.json`; |
| | | a.click(); |
| | | URL.revokeObjectURL(url); |
| | | proxy?.$modal?.msgSuccess?.(`å·²å¯¼åº ${data.length} æ¡ï¼å½å页åè¡¨æ°æ®ï¼`); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | loadFlowUsers(); |
| | | await initModuleList(searchForm); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_actions { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 模å䏿åï¼éè´åå |
| | | ç®å½æ è¯ï¼ContractManage/purchase-contractï¼purchase-contract â 䏿ï¼éè´ååï¼ |
| | | å¤ç¨é¡µé¢ï¼@/views/procurementManagement/procurementLedger/index.vueï¼éè´å°è´¦ï¼æä»¶å index.vue â å
¥å£é¡µï¼ |
| | | --> |
| | | <template> |
| | | <ProcurementLedger /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue' |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 模å䏿åï¼éå®åå |
| | | ç®å½æ è¯ï¼ContractManage/sale-contractï¼sale-contract â 䏿ï¼éå®ååï¼ |
| | | å¤ç¨é¡µé¢ï¼@/views/procurementManagement/procurementLedger/index.vueï¼éè´å°è´¦ï¼æä»¶å index.vue â å
¥å£é¡µï¼ |
| | | --> |
| | | <template> |
| | | <ProcurementLedger /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue' |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- EnterpriseNewsï¼è¯¦æ
åªè¯»é¢æ¿ï¼å«äºå¨ï¼ --> |
| | | <template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="æ°é»ç¼å·">{{ row.newsNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¶æ"> |
| | | <el-tag :type="publishStatusTag(row.newsStatus ?? row.publishStatus)" size="small"> |
| | | {{ publishStatusLabel(row.newsStatus ?? row.publishStatus) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ°é»åç±»"> |
| | | <span class="type-badge" :style="{ color: newsTypeColor(row.newsType) }"> |
| | | {{ newsTypeLabel(row.newsType) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æç模æ¿">{{ layoutTemplateLabel(row.layoutTemplate) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ é¢" :span="2">{{ row.title || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æè¦" :span="2">{{ row.summary || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
读èå´">{{ readScopeLabel(row.readScope) }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
读ç"> |
| | | {{ readRate(row) }}%ï¼æªè¯» {{ unreadCount }} äººï¼ |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ç¼è¾æé">{{ publishRoleLabel(row.editorRole) }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸è§è²">{{ publishRoleLabel(row.reviewerRole) }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå¸äºº">{{ row.publisherName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å叿¶é´">{{ row.publishTime || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½åçæ¬">v{{ row.versionNo || 1 }}</el-descriptions-item> |
| | | <el-descriptions-item label="éé
读确认"> |
| | | {{ row.requireReadConfirm ? "æ¯" : "å¦" }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left">æ£æå
容</el-divider> |
| | | <div v-if="row.contentHtml" class="news-html-body" v-html="row.contentHtml" /> |
| | | <el-empty v-else description="ææ æ£æ" :image-size="48" /> |
| | | |
| | | <template v-if="row.mediaList?.length"> |
| | | <el-divider content-position="left">å¾é / è§é¢</el-divider> |
| | | <div class="media-grid"> |
| | | <div v-for="(m, i) in row.mediaList" :key="i" class="media-item"> |
| | | <el-tag size="small" type="info">{{ m.type === "video" ? "è§é¢" : "å¾ç" }}</el-tag> |
| | | <span class="media-name">{{ m.name }}</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-divider content-position="left">éä»¶</el-divider> |
| | | <template v-if="row.attachmentList?.length"> |
| | | <el-tag |
| | | v-for="(f, i) in row.attachmentList" |
| | | :key="i" |
| | | class="file-tag" |
| | | type="info" |
| | | @click="openFile(f)" |
| | | > |
| | | {{ f.name }} |
| | | </el-tag> |
| | | </template> |
| | | <el-empty v-else description="ææ éä»¶" :image-size="48" /> |
| | | |
| | | <template v-if="row.newsType === 'culture' && (row.publishStatus === 'PUBLISHED' || row.publishStatus === 'published')"> |
| | | <el-divider content-position="left">äºå¨ï¼ç¹èµ {{ likeCount }} · è¯è®º {{ commentCount }}ï¼</el-divider> |
| | | <div class="interaction-bar"> |
| | | <el-button type="primary" plain size="small" @click="$emit('like')"> |
| | | {{ likedByMe ? "åæ¶ç¹èµ" : "ç¹èµ" }} |
| | | </el-button> |
| | | </div> |
| | | <el-input |
| | | v-model="commentDraft" |
| | | type="textarea" |
| | | :rows="2" |
| | | maxlength="300" |
| | | show-word-limit |
| | | placeholder="åä¸ä½ çè¯è®ºâ¦" |
| | | class="mb8" |
| | | /> |
| | | <el-button type="primary" size="small" @click="submitComment">å表è¯è®º</el-button> |
| | | <el-timeline v-if="row.comments?.length" class="comment-timeline mt12"> |
| | | <el-timeline-item v-for="c in row.comments" :key="c.id" :timestamp="c.time"> |
| | | <strong>{{ c.name }}</strong>ï¼{{ c.content }} |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ è¯è®º" :image-size="40" /> |
| | | </template> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { |
| | | newsTypeLabel, |
| | | newsTypeColor, |
| | | publishStatusLabel, |
| | | publishStatusTag, |
| | | layoutTemplateLabel, |
| | | readScopeLabel, |
| | | publishRoleLabel, |
| | | readRate, |
| | | getUnreadEmployees, |
| | | } from "../enterpriseNewsUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["like", "comment"]); |
| | | |
| | | const commentDraft = ref(""); |
| | | |
| | | const unreadCount = computed(() => getUnreadEmployees(props.row).length); |
| | | const likeCount = computed(() => props.row?.likes?.length || 0); |
| | | const commentCount = computed(() => props.row?.comments?.length || 0); |
| | | const likedByMe = computed(() => (props.row?.likes || []).some((l) => l.userId === "u1")); |
| | | |
| | | function openFile(f) { |
| | | const url = f?.url || f?.downloadURL; |
| | | if (url) window.open(url, "_blank"); |
| | | } |
| | | |
| | | function submitComment() { |
| | | emit("comment", commentDraft.value); |
| | | commentDraft.value = ""; |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .type-badge { |
| | | font-weight: 600; |
| | | } |
| | | .news-html-body { |
| | | padding: 12px 16px; |
| | | background: var(--el-fill-color-light); |
| | | border-radius: 6px; |
| | | line-height: 1.7; |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | } |
| | | .media-grid { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 12px; |
| | | } |
| | | .media-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 12px; |
| | | background: var(--el-fill-color-lighter); |
| | | border-radius: 4px; |
| | | } |
| | | .media-name { |
| | | font-size: 13px; |
| | | } |
| | | .file-tag { |
| | | margin: 0 8px 8px 0; |
| | | cursor: pointer; |
| | | } |
| | | .interaction-bar { |
| | | margin-bottom: 8px; |
| | | } |
| | | .comment-timeline { |
| | | max-height: 200px; |
| | | overflow-y: auto; |
| | | } |
| | | .mb8 { |
| | | margin-bottom: 8px; |
| | | } |
| | | .mt12 { |
| | | margin-top: 12px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** @deprecated è¯·ä½¿ç¨ enterpriseNewsMappers.js */ |
| | | export { |
| | | ENTERPRISE_NEWS_PAYLOAD_KEY, |
| | | buildEnterpriseNewsSaveDto, |
| | | buildEnterpriseNewsTableColumns, |
| | | canEditEnterpriseNewsRow, |
| | | extractEnterpriseNewsFromRow, |
| | | mapApiRowToNewsForm, |
| | | mapEnterpriseNewsFromApi, |
| | | syncNewsFormToSubmitPayload, |
| | | } from "./enterpriseNewsMappers.js"; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { |
| | | createEmptyForm, |
| | | normalizeEnterpriseNewsStatus, |
| | | publishStatusLabel, |
| | | publishStatusTag, |
| | | } from "./enterpriseNewsUtils.js"; |
| | | |
| | | /** formPayload ä¸åæ¾å®æ´ä¼ä¸æ°é»ä¸å¡æ°æ®çé®ï¼å®¡æ¹å®ä¾ä¿åç¨ï¼ */ |
| | | export const ENTERPRISE_NEWS_PAYLOAD_KEY = "enterpriseNews"; |
| | | |
| | | const READ_SCOPE_FROM_API = { |
| | | all: "all", |
| | | dept: "department", |
| | | department: "department", |
| | | custom: "custom", |
| | | management: "management", |
| | | }; |
| | | |
| | | const READ_SCOPE_TO_API = { |
| | | all: "all", |
| | | department: "dept", |
| | | dept: "dept", |
| | | custom: "custom", |
| | | management: "all", |
| | | }; |
| | | |
| | | export function mapReadScopeFromApi(scope) { |
| | | const key = String(scope ?? "").trim().toLowerCase(); |
| | | return READ_SCOPE_FROM_API[key] || key || "all"; |
| | | } |
| | | |
| | | export function mapReadScopeToApi(scope) { |
| | | return READ_SCOPE_TO_API[scope] || scope || "all"; |
| | | } |
| | | |
| | | export function unwrapEnterpriseNewsPage(res) { |
| | | const data = res?.data ?? res; |
| | | if (!data || typeof data !== "object") { |
| | | return { records: [], total: 0 }; |
| | | } |
| | | if (Array.isArray(data.records)) { |
| | | return { records: data.records, total: Number(data.total ?? 0) }; |
| | | } |
| | | const nested = data.data; |
| | | if (nested && typeof nested === "object" && Array.isArray(nested.records)) { |
| | | return { records: nested.records, total: Number(nested.total ?? 0) }; |
| | | } |
| | | return { records: [], total: 0 }; |
| | | } |
| | | |
| | | /** ç»è£
listPage æ¥è¯¢åæ° */ |
| | | export function buildEnterpriseNewsListParams({ page, searchForm }) { |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | }; |
| | | const kw = (searchForm?.keyword || "").trim(); |
| | | if (kw) params.title = kw; |
| | | if (searchForm?.newsType) params.category = searchForm.newsType; |
| | | if (searchForm?.status) params.status = searchForm.status; |
| | | const range = searchForm?.createTimeRange; |
| | | if (Array.isArray(range) && range[0]) { |
| | | params.createTimeStart = range[0]; |
| | | } |
| | | if (Array.isArray(range) && range[1]) { |
| | | params.createTimeEnd = range[1]; |
| | | } |
| | | return params; |
| | | } |
| | | |
| | | /** æ¥å£ EnterpriseNewsVo â åè¡¨è¡ */ |
| | | export function mapEnterpriseNewsFromApi(row) { |
| | | if (!row) return {}; |
| | | const newsStatus = normalizeEnterpriseNewsStatus(row.status); |
| | | return { |
| | | ...row, |
| | | newsNo: row.id != null ? String(row.id) : "â", |
| | | newsType: row.category || "", |
| | | contentHtml: row.content || "", |
| | | publisherName: row.createUserName || "â", |
| | | publishTime: row.createTime || "", |
| | | updateTime: row.updateTime || "", |
| | | newsStatus, |
| | | requireReadConfirm: row.isRequired === "1" || row.isRequired === 1, |
| | | readScope: mapReadScopeFromApi(row.readScope), |
| | | readCount: row.readCount ?? 0, |
| | | requiredReadCount: row.requiredReadCount ?? 0, |
| | | }; |
| | | } |
| | | |
| | | /** æ¯å¦å
许修æ¹ï¼è稿ã驳å坿¹ï¼ */ |
| | | export function canEditEnterpriseNewsRow(row) { |
| | | const status = normalizeEnterpriseNewsStatus(row?.newsStatus ?? row?.status); |
| | | return status === "DRAFT" || status === "REJECTED"; |
| | | } |
| | | |
| | | /** æ¥å£è¡ / 详æ
â 表å */ |
| | | export function mapApiRowToNewsForm(row) { |
| | | if (!row) return createEmptyForm(); |
| | | return { |
| | | ...createEmptyForm(), |
| | | id: row.id != null ? String(row.id) : "", |
| | | newsNo: row.id != null ? String(row.id) : "", |
| | | title: row.title || "", |
| | | summary: row.summary || "", |
| | | contentHtml: row.content || row.contentHtml || "", |
| | | newsType: row.newsType || row.category || "announcement", |
| | | readScope: mapReadScopeFromApi(row.readScope), |
| | | requireReadConfirm: Boolean(row.requireReadConfirm ?? row.isRequired === "1"), |
| | | publisherName: row.createUserName || row.publisherName || "", |
| | | publishStatus: normalizeEnterpriseNewsStatus(row.newsStatus ?? row.status), |
| | | templateId: row.templateId, |
| | | templateName: row.templateName || "", |
| | | targetDeptIds: [...(row.deptIds || row.targetDeptIds || [])], |
| | | targetUserIds: [...(row.userIds || row.targetUserIds || [])], |
| | | }; |
| | | } |
| | | |
| | | /** 审æ¹å®ä¾è¡ formPayload â 表åï¼å
¼å®¹æ§æ°æ®ï¼ */ |
| | | export function extractEnterpriseNewsFromRow(row) { |
| | | if (!row?.formPayload && !row?.formFieldDefs && !row?.instanceNo) { |
| | | return mapApiRowToNewsForm(row); |
| | | } |
| | | const payload = row?.formPayload || {}; |
| | | const raw = payload[ENTERPRISE_NEWS_PAYLOAD_KEY]; |
| | | if (raw && typeof raw === "object") { |
| | | return { ...createEmptyForm(), ...raw }; |
| | | } |
| | | return { |
| | | ...createEmptyForm(), |
| | | title: payload.title || row?.title || "", |
| | | summary: payload.summary || "", |
| | | newsType: payload.newsType || row?.category || "announcement", |
| | | contentHtml: payload.contentHtml || row?.content || "", |
| | | }; |
| | | } |
| | | |
| | | export function syncNewsFormToSubmitPayload(newsForm, submitForm) { |
| | | const snapshot = JSON.parse(JSON.stringify(newsForm)); |
| | | submitForm.formPayload = { |
| | | ...(submitForm.formPayload || {}), |
| | | [ENTERPRISE_NEWS_PAYLOAD_KEY]: snapshot, |
| | | title: snapshot.title, |
| | | summary: snapshot.summary, |
| | | }; |
| | | } |
| | | |
| | | function toIdList(ids) { |
| | | if (!Array.isArray(ids) || !ids.length) return undefined; |
| | | const list = ids |
| | | .map((id) => (typeof id === "number" ? id : Number(id))) |
| | | .filter((n) => !Number.isNaN(n)); |
| | | return list.length ? list : undefined; |
| | | } |
| | | |
| | | /** 表å â POST /enterpriseNews/save 请æ±ä½ */ |
| | | export function buildEnterpriseNewsSaveDto(newsForm, { status } = {}) { |
| | | const dto = { |
| | | title: (newsForm.title || "").trim(), |
| | | summary: newsForm.summary || "", |
| | | content: newsForm.contentHtml || "", |
| | | category: newsForm.newsType || "", |
| | | readScope: mapReadScopeToApi(newsForm.readScope), |
| | | isRequired: newsForm.requireReadConfirm ? "1" : "0", |
| | | status: normalizeEnterpriseNewsStatus(status ?? newsForm.publishStatus), |
| | | }; |
| | | |
| | | const rawId = newsForm.id; |
| | | if (rawId != null && rawId !== "") { |
| | | const id = Number(rawId); |
| | | if (!Number.isNaN(id)) dto.id = id; |
| | | } |
| | | |
| | | const deptIds = toIdList(newsForm.targetDeptIds); |
| | | if (deptIds) dto.deptIds = deptIds; |
| | | |
| | | const userIds = toIdList(newsForm.targetUserIds); |
| | | if (userIds) dto.userIds = userIds; |
| | | |
| | | const templateId = newsForm.templateId; |
| | | if (templateId != null && templateId !== "") { |
| | | const tid = Number(templateId); |
| | | if (!Number.isNaN(tid)) dto.templateId = tid; |
| | | } |
| | | if (newsForm.templateName) dto.templateName = newsForm.templateName; |
| | | |
| | | return dto; |
| | | } |
| | | |
| | | export function buildEnterpriseNewsTableColumns(buildTableActions) { |
| | | return [ |
| | | { label: "ç¼å·", prop: "newsNo", width: 120 }, |
| | | { label: "æ é¢", prop: "title", minWidth: 180, showOverflowTooltip: true }, |
| | | { |
| | | label: "åç±»", |
| | | prop: "newsType", |
| | | width: 100, |
| | | dataType: "slot", |
| | | slot: "newsType", |
| | | }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "newsStatus", |
| | | width: 100, |
| | | dataType: "tag", |
| | | formatData: (v) => publishStatusLabel(v), |
| | | formatType: (v) => publishStatusTag(v), |
| | | }, |
| | | { label: "å建人", prop: "publisherName", width: 110 }, |
| | | { label: "å建æ¶é´", prop: "createTime", width: 170 }, |
| | | { label: "æ´æ°æ¶é´", prop: "updateTime", width: 170 }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 220, |
| | | operation: buildTableActions(), |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | /** æ°é»åç±»ï¼ç»ä¸ä¿¡æ¯åºå£ */ |
| | | export const NEWS_TYPE_OPTIONS = [ |
| | | { value: "announcement", label: "ä¼ä¸å
Œ", color: "#409eff" }, |
| | | { value: "policy", label: "æ¿ç解读", color: "#e6a23c" }, |
| | | { value: "industry", label: "è¡ä¸å¨æ", color: "#909399" }, |
| | | { value: "culture", label: "æåæ´»å¨", color: "#67c23a" }, |
| | | ]; |
| | | |
| | | /** ä¼ä¸æ°é»ç¶æï¼ä¸å端æä¸¾ä¸è´ï¼ */ |
| | | export const PUBLISH_STATUS_OPTIONS = [ |
| | | { value: "DRAFT", label: "è稿", tag: "info" }, |
| | | { value: "PENDING", label: "å¾
审æ¹", tag: "warning" }, |
| | | { value: "PUBLISHED", label: "å·²åå¸", tag: "success" }, |
| | | { value: "REJECTED", label: "驳å", tag: "danger" }, |
| | | { value: "OFFLINE", label: "å·²ä¸çº¿", tag: "info" }, |
| | | ]; |
| | | |
| | | /** ä¼ä¸æ°é»å表çé */ |
| | | export const ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS = [...PUBLISH_STATUS_OPTIONS]; |
| | | |
| | | const LEGACY_PUBLISH_STATUS_MAP = { |
| | | draft: "DRAFT", |
| | | pending_review: "PENDING", |
| | | published: "PUBLISHED", |
| | | archived: "OFFLINE", |
| | | }; |
| | | |
| | | /** å端æ°åç¶æç â æä¸¾ï¼0 è稿 1 å¾
å®¡æ¹ 2 å·²åå¸ 3 驳å 4 å·²ä¸çº¿ï¼ */ |
| | | const ENTERPRISE_NEWS_STATUS_NUMERIC_MAP = { |
| | | 0: "DRAFT", |
| | | 1: "PENDING", |
| | | 2: "PUBLISHED", |
| | | 3: "REJECTED", |
| | | 4: "OFFLINE", |
| | | }; |
| | | |
| | | const ENTERPRISE_NEWS_STATUS_LABEL_MAP = { |
| | | è稿: "DRAFT", |
| | | å¾
审æ¹: "PENDING", |
| | | å·²åå¸: "PUBLISHED", |
| | | 驳å: "REJECTED", |
| | | 已驳å: "REJECTED", |
| | | å·²ä¸çº¿: "OFFLINE", |
| | | }; |
| | | |
| | | /** ç»ä¸ä¸ºåç«¯ç¶ææä¸¾å¼ */ |
| | | export function normalizeEnterpriseNewsStatus(v) { |
| | | if (v == null || v === "") return "DRAFT"; |
| | | if (typeof v === "number" || (typeof v === "string" && /^\d+$/.test(v.trim()))) { |
| | | const numKey = ENTERPRISE_NEWS_STATUS_NUMERIC_MAP[Number(v)]; |
| | | if (numKey) return numKey; |
| | | } |
| | | const raw = String(v).trim(); |
| | | if (ENTERPRISE_NEWS_STATUS_LABEL_MAP[raw]) { |
| | | return ENTERPRISE_NEWS_STATUS_LABEL_MAP[raw]; |
| | | } |
| | | const upper = raw.toUpperCase(); |
| | | if (upper === "APPROVED") return "PUBLISHED"; |
| | | const hit = PUBLISH_STATUS_OPTIONS.find((x) => x.value === upper); |
| | | if (hit) return hit.value; |
| | | const legacy = LEGACY_PUBLISH_STATUS_MAP[raw.toLowerCase()]; |
| | | if (legacy) return legacy; |
| | | return upper; |
| | | } |
| | | |
| | | /** æçæ¨¡æ¿ */ |
| | | export const LAYOUT_TEMPLATE_OPTIONS = [ |
| | | { value: "standard", label: "æ å徿" }, |
| | | { value: "policy", label: "æ¿çæ¡æ" }, |
| | | { value: "gallery", label: "å¾éç¸å" }, |
| | | { value: "briefing", label: "ç®æ¥æè¦" }, |
| | | ]; |
| | | |
| | | /** é
读å¯è§èå´ */ |
| | | export const READ_SCOPE_OPTIONS = [ |
| | | { value: "all", label: "å
¨åå¯è§" }, |
| | | { value: "management", label: "管çå±" }, |
| | | { value: "department", label: "æå®é¨é¨" }, |
| | | { value: "custom", label: "èªå®ä¹åå" }, |
| | | ]; |
| | | |
| | | /** ç¼è¾/å®¡æ ¸è§è²ï¼å叿éï¼ */ |
| | | export const PUBLISH_ROLE_OPTIONS = [ |
| | | { value: "hr", label: "HRï¼äººäºæ¿çï¼" }, |
| | | { value: "admin", label: "管çåï¼å¤é¨æ°é»å®¡æ ¸ï¼" }, |
| | | { value: "dept_manager", label: "é¨é¨è´è´£äºº" }, |
| | | { value: "editor", label: "å
容ç¼è¾" }, |
| | | ]; |
| | | |
| | | /** ç®æ åä¼ï¼å¯¹æ¥ç»ç»æ¶æ API åä¸ºç©ºï¼ */ |
| | | export const MOCK_AUDIENCE = []; |
| | | |
| | | const DEPT_OPTIONS = [ |
| | | { value: "101", label: "ç åé¨" }, |
| | | { value: "102", label: "éå®é¨" }, |
| | | { value: "103", label: "è¡æ¿é¨" }, |
| | | { value: "104", label: "è´¢å¡é¨" }, |
| | | { value: "105", label: "æ»ç»å" }, |
| | | { value: "106", label: "人åèµæºé¨" }, |
| | | ]; |
| | | |
| | | export { DEPT_OPTIONS }; |
| | | |
| | | export function newsTypeLabel(v) { |
| | | return NEWS_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function newsTypeColor(v) { |
| | | return NEWS_TYPE_OPTIONS.find((x) => x.value === v)?.color || "#909399"; |
| | | } |
| | | |
| | | export function publishStatusLabel(v) { |
| | | const key = normalizeEnterpriseNewsStatus(v); |
| | | return PUBLISH_STATUS_OPTIONS.find((x) => x.value === key)?.label || v || "â"; |
| | | } |
| | | |
| | | export function publishStatusTag(v) { |
| | | const key = normalizeEnterpriseNewsStatus(v); |
| | | return PUBLISH_STATUS_OPTIONS.find((x) => x.value === key)?.tag || "info"; |
| | | } |
| | | |
| | | export function layoutTemplateLabel(v) { |
| | | return LAYOUT_TEMPLATE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function readScopeLabel(v) { |
| | | return READ_SCOPE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function publishRoleLabel(v) { |
| | | return PUBLISH_ROLE_OPTIONS.find((x) => x.value === v)?.label || v || "â"; |
| | | } |
| | | |
| | | export function createEmptyForm() { |
| | | return { |
| | | id: "", |
| | | newsNo: "", |
| | | title: "", |
| | | summary: "", |
| | | newsType: "announcement", |
| | | layoutTemplate: "standard", |
| | | contentHtml: "", |
| | | coverImage: "", |
| | | mediaList: [], |
| | | attachmentList: [], |
| | | editorRole: "hr", |
| | | reviewerRole: "admin", |
| | | readScope: "all", |
| | | targetDeptIds: [], |
| | | targetUserIds: [], |
| | | publishStatus: "DRAFT", |
| | | publisherName: "", |
| | | publishTime: "", |
| | | readRecords: [], |
| | | remindLogs: [], |
| | | likes: [], |
| | | comments: [], |
| | | versions: [], |
| | | versionNo: 1, |
| | | requireReadConfirm: false, |
| | | templateId: null, |
| | | templateName: "", |
| | | }; |
| | | } |
| | | |
| | | /** æé
读èå´è§£æç®æ åä¼ */ |
| | | export function resolveTargetAudience(row) { |
| | | const scope = row.readScope || "all"; |
| | | if (scope === "management") { |
| | | return MOCK_AUDIENCE.filter((u) => u.isManagement); |
| | | } |
| | | if (scope === "department" && row.targetDeptIds?.length) { |
| | | const names = DEPT_OPTIONS.filter((d) => row.targetDeptIds.includes(d.value)).map((d) => d.label); |
| | | return MOCK_AUDIENCE.filter((u) => names.includes(u.deptName)); |
| | | } |
| | | if (scope === "custom" && row.targetUserIds?.length) { |
| | | return MOCK_AUDIENCE.filter((u) => row.targetUserIds.includes(u.userId)); |
| | | } |
| | | return [...MOCK_AUDIENCE]; |
| | | } |
| | | |
| | | export function getUnreadEmployees(row) { |
| | | const audience = resolveTargetAudience(row); |
| | | const readSet = new Set( |
| | | (row.readRecords || []).filter((r) => r.readAt).map((r) => r.userId) |
| | | ); |
| | | return audience.filter((u) => !readSet.has(u.userId)); |
| | | } |
| | | |
| | | export function readRate(row) { |
| | | const audience = resolveTargetAudience(row); |
| | | if (!audience.length) return 0; |
| | | const readCount = (row.readRecords || []).filter((r) => r.readAt).length; |
| | | return Math.round((readCount / audience.length) * 100); |
| | | } |
| | | |
| | | export function validateNewsForm(form) { |
| | | const title = (form.title || "").trim(); |
| | | if (!title) return { ok: false, message: "è¯·å¡«åæ°é»æ é¢" }; |
| | | if (!form.newsType) return { ok: false, message: "è¯·éæ©æ°é»åç±»" }; |
| | | if (form.readScope === "department" && !(form.targetDeptIds || []).length) { |
| | | return { ok: false, message: "è¯·éæ©å¯è§é¨é¨" }; |
| | | } |
| | | return { ok: true, title }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼EnterpriseNews ä¼ä¸æ°é»ï¼listPage|save|update|deleteï¼æ°å»ºä¿çå®¡æ¹æ¨¡æ¿ï¼--> |
| | | <template> |
| | | <div class="app-container enterprise-news-page"> |
| | | <div class="search_form mb20"> |
| | | <div class="search_fields"> |
| | | <span class="search_title">å
³é®è¯ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.keyword" |
| | | style="width: 200px" |
| | | placeholder="æ é¢" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">åç±»ï¼</span> |
| | | <el-select v-model="searchForm.newsType" placeholder="å
¨é¨" clearable style="width: 140px"> |
| | | <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | <span class="search_title" style="margin-left: 12px">ç¶æï¼</span> |
| | | <el-select v-model="searchForm.status" placeholder="å
¨é¨" clearable style="width: 120px"> |
| | | <el-option |
| | | v-for="opt in ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | <span class="search_title" style="margin-left: 12px">å建æ¶é´ï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.createTimeRange" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="å¼å§" |
| | | end-placeholder="ç»æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 260px" |
| | | clearable |
| | | /> |
| | | <el-button type="primary" :icon="Search" class="ml10" @click="onSearch">æç´¢</el-button> |
| | | <el-button :icon="RefreshRight" @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="primary" :icon="Plus" @click="openAddWithTemplate">æ°å»ºæ°é»</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | :total="page.total" |
| | | @pagination="onPagination" |
| | | > |
| | | <template #newsType="{ row }"> |
| | | <span class="news-type-tag" :style="{ color: newsTypeColor(row.newsType) }"> |
| | | {{ newsTypeLabel(row.newsType) }} |
| | | </span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <el-dialog |
| | | v-model="newsFormDialog.visible" |
| | | :title="newsFormDialog.title" |
| | | width="960px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="news-form-dialog" |
| | | @closed="onNewsFormClosed" |
| | | > |
| | | <el-form |
| | | ref="newsFormRef" |
| | | :model="newsForm" |
| | | :rules="newsFormRules" |
| | | label-width="110px" |
| | | :disabled="newsFormDialog.readonly" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°é»åç±»" prop="newsType"> |
| | | <el-select v-model="newsForm.newsType" placeholder="è¯·éæ©" style="width: 100%"> |
| | | <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æç模æ¿"> |
| | | <el-select v-model="newsForm.layoutTemplate" style="width: 100%"> |
| | | <el-option |
| | | v-for="opt in LAYOUT_TEMPLATE_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="æ é¢" prop="title"> |
| | | <el-input v-model="newsForm.title" placeholder="æ°é»æ é¢" maxlength="100" show-word-limit /> |
| | | </el-form-item> |
| | | <el-form-item label="æè¦"> |
| | | <el-input v-model="newsForm.summary" type="textarea" :rows="2" maxlength="300" show-word-limit /> |
| | | </el-form-item> |
| | | <el-form-item label="æ£æ" prop="contentHtml"> |
| | | <Editor v-model="newsForm.contentHtml" :min-height="280" /> |
| | | </el-form-item> |
| | | <el-form-item label="éä»¶"> |
| | | <FileUpload v-model:file-list="newsForm.attachmentList" :limit="10" button-text="ä¸ä¼ PDF / ææ¡£" /> |
| | | </el-form-item> |
| | | <el-form-item v-if="newsForm.layoutTemplate === 'gallery'" label="å¾é/è§é¢"> |
| | | <el-input |
| | | v-model="galleryInput" |
| | | placeholder="è¾å
¥èµæºåç§°åå车添å ï¼æ¼ç¤ºï¼" |
| | | @keyup.enter="addGalleryItem" |
| | | /> |
| | | <el-tag |
| | | v-for="(m, i) in newsForm.mediaList" |
| | | :key="i" |
| | | closable |
| | | class="media-tag" |
| | | @close="newsForm.mediaList.splice(i, 1)" |
| | | > |
| | | {{ m.name }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">æé管æ§</el-divider> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¼è¾è§è²"> |
| | | <el-select v-model="newsForm.editorRole" style="width: 100%"> |
| | | <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å®¡æ ¸è§è²"> |
| | | <el-select v-model="newsForm.reviewerRole" style="width: 100%"> |
| | | <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="é
读èå´" prop="readScope"> |
| | | <el-radio-group v-model="newsForm.readScope"> |
| | | <el-radio v-for="opt in READ_SCOPE_OPTIONS" :key="opt.value" :value="opt.value"> |
| | | {{ opt.label }} |
| | | </el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="newsForm.readScope === 'department'" label="å¯è§é¨é¨"> |
| | | <el-select v-model="newsForm.targetDeptIds" multiple placeholder="éæ©é¨é¨" style="width: 100%"> |
| | | <el-option v-for="d in DEPT_OPTIONS" :key="d.value" :label="d.label" :value="d.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¿çç±»å¿
读"> |
| | | <el-switch v-model="newsForm.requireReadConfirm" active-text="éé
读确认ï¼ä¾¿äºç»è®¡æªè¯»ï¼" /> |
| | | </el-form-item> |
| | | |
| | | <template v-if="hasApprovalTemplate"> |
| | | <el-divider content-position="left">å®¡æ¹æµç¨</el-divider> |
| | | <el-form-item label="å®¡æ¹æ¨¡æ¿"> |
| | | <span class="template-name">{{ approvalTemplateLabel }}</span> |
| | | </el-form-item> |
| | | <el-form-item v-if="activeTemplate" label="å®¡æ¹æµç¨" required> |
| | | <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" /> |
| | | <p class="section-tip">æµç¨ä¸å®¡æ¹äººç±æ¨¡æ¿é¢ç½®ï¼å¯æéå¾®è°èç¹å®¡æ¹äººã</p> |
| | | </el-form-item> |
| | | </template> |
| | | <el-alert |
| | | v-else-if="!isNewsEdit" |
| | | type="warning" |
| | | show-icon |
| | | :closable="false" |
| | | title="请å
éè¿ãæ°å»ºæ°é»ãéæ©å®¡æ¹æ¨¡æ¿" |
| | | /> |
| | | </el-form> |
| | | <template v-if="!newsFormDialog.readonly" #footer> |
| | | <el-button @click="newsFormDialog.visible = false">å æ¶</el-button> |
| | | <el-button :loading="newsSaving" @click="onNewsSave('draft')">åè稿</el-button> |
| | | <el-button type="warning" :loading="newsSaving" @click="onNewsSave('submit_review')"> |
| | | æäº¤å®¡æ ¸ |
| | | </el-button> |
| | | <el-button type="primary" :loading="newsSaving" @click="onNewsSave('submit_review')"> |
| | | ä¿ å |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="detailDialog.visible" title="æ°é»è¯¦æ
" width="880px" append-to-body destroy-on-close> |
| | | <NewsDetailPanel :row="detailNewsRow" /> |
| | | <template #footer> |
| | | <el-button v-if="canEditEnterpriseNewsRow(detailRow)" type="primary" @click="openNewsEditFromDetail"> |
| | | ä¿®æ¹ |
| | | </el-button> |
| | | <el-button @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Plus, RefreshRight, Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | deleteEnterpriseNews, |
| | | saveEnterpriseNews, |
| | | updateEnterpriseNews, |
| | | } from "@/api/officeProcessAutomation/enterpriseNews.js"; |
| | | import { computed, onMounted, reactive, ref } from "vue"; |
| | | import Editor from "@/components/Editor/index.vue"; |
| | | import FileUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue"; |
| | | import TemplateFlowEditor from "../../ApproveManage/approve-template/components/TemplateFlowEditor.vue"; |
| | | import { |
| | | applyBindingToForm, |
| | | validateTemplateBinding, |
| | | } from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js"; |
| | | import { createEmptySubmitForm } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | import NewsDetailPanel from "./components/NewsDetailPanel.vue"; |
| | | import { |
| | | NEWS_TYPE_OPTIONS, |
| | | LAYOUT_TEMPLATE_OPTIONS, |
| | | READ_SCOPE_OPTIONS, |
| | | PUBLISH_ROLE_OPTIONS, |
| | | DEPT_OPTIONS, |
| | | createEmptyForm, |
| | | ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS, |
| | | newsTypeColor, |
| | | newsTypeLabel, |
| | | validateNewsForm, |
| | | } from "./enterpriseNewsUtils.js"; |
| | | import { |
| | | buildEnterpriseNewsSaveDto, |
| | | buildEnterpriseNewsTableColumns, |
| | | canEditEnterpriseNewsRow, |
| | | mapApiRowToNewsForm, |
| | | } from "./enterpriseNewsMappers.js"; |
| | | import { useEnterpriseNewsList } from "./useEnterpriseNewsList.js"; |
| | | |
| | | const searchForm = reactive({ |
| | | keyword: "", |
| | | newsType: "", |
| | | status: "", |
| | | createTimeRange: null, |
| | | }); |
| | | |
| | | const newsFormDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | const newsForm = reactive(createEmptyForm()); |
| | | const newsFormRef = ref(); |
| | | const galleryInput = ref(""); |
| | | |
| | | const newsFormRules = { |
| | | title: [{ required: true, message: "请è¾å
¥æ°é»æ é¢", trigger: "blur" }], |
| | | newsType: [{ required: true, message: "è¯·éæ©æ°é»åç±»", trigger: "change" }], |
| | | readScope: [{ required: true, message: "è¯·éæ©é
读èå´", trigger: "change" }], |
| | | }; |
| | | |
| | | const newsList = useEnterpriseNewsList(); |
| | | const { tableData, tableLoading, page, handleQuery: fetchNewsList, pagination: paginateNewsList } = |
| | | newsList; |
| | | |
| | | const submitForm = reactive(createEmptySubmitForm("")); |
| | | const templateBindVisible = ref(false); |
| | | const pendingTemplateBinding = ref(null); |
| | | const newsSaving = ref(false); |
| | | |
| | | const isNewsEdit = computed(() => newsFormDialog.mode === "edit"); |
| | | const activeTemplate = computed(() => submitForm.templateSnapshot || null); |
| | | const hasApprovalTemplate = computed( |
| | | () => Boolean(activeTemplate.value || newsForm.templateId) |
| | | ); |
| | | const approvalTemplateLabel = computed( |
| | | () => |
| | | activeTemplate.value?.label || |
| | | newsForm.templateName || |
| | | submitForm.templateName || |
| | | "â" |
| | | ); |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | function openAddWithTemplate() { |
| | | pendingTemplateBinding.value = null; |
| | | templateBindVisible.value = true; |
| | | } |
| | | |
| | | function onTemplateBound(binding) { |
| | | pendingTemplateBinding.value = binding; |
| | | } |
| | | |
| | | function resetSubmitForm() { |
| | | Object.assign(submitForm, createEmptySubmitForm("")); |
| | | } |
| | | |
| | | const detailNewsRow = computed(() => mapApiRowToNewsForm(detailRow.value)); |
| | | |
| | | const tableColumn = ref( |
| | | buildEnterpriseNewsTableColumns(() => [ |
| | | { name: "详æ
", type: "text", clickFun: (row) => openNewsDetail(row) }, |
| | | { |
| | | name: "ä¿®æ¹", |
| | | type: "text", |
| | | disabled: (row) => !canEditEnterpriseNewsRow(row), |
| | | clickFun: (row) => openNewsEdit(row), |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | disabled: (row) => !canEditEnterpriseNewsRow(row), |
| | | clickFun: (row) => handleNewsDelete(row), |
| | | }, |
| | | ]) |
| | | ); |
| | | |
| | | function resetNewsForm(target = createEmptyForm()) { |
| | | Object.assign(newsForm, createEmptyForm(), target); |
| | | } |
| | | |
| | | function openNewsFormDialog(mode, row) { |
| | | newsFormDialog.mode = mode; |
| | | newsFormDialog.readonly = mode === "view"; |
| | | newsFormDialog.title = |
| | | mode === "add" ? "æ°å»ºä¼ä¸æ°é»" : mode === "edit" ? "ç¼è¾ä¼ä¸æ°é»" : "æ¥çä¼ä¸æ°é»"; |
| | | if (mode === "add") { |
| | | resetNewsForm(); |
| | | } else if (row) { |
| | | resetNewsForm(mapApiRowToNewsForm(row)); |
| | | } |
| | | newsFormDialog.visible = true; |
| | | } |
| | | |
| | | function onTemplateBindClosed() { |
| | | const binding = pendingTemplateBinding.value; |
| | | if (!binding) return; |
| | | pendingTemplateBinding.value = null; |
| | | resetSubmitForm(); |
| | | applyBindingToForm(submitForm, binding); |
| | | if (binding.templateId) { |
| | | newsForm.templateId = binding.templateId; |
| | | newsForm.templateName = binding.templateName || ""; |
| | | } |
| | | openNewsFormDialog("add"); |
| | | } |
| | | |
| | | function openNewsEdit(row) { |
| | | if (!canEditEnterpriseNewsRow(row)) { |
| | | ElMessage.warning("å½åç¶æä¸å¯ä¿®æ¹"); |
| | | return; |
| | | } |
| | | resetSubmitForm(); |
| | | if (row?.templateId != null) { |
| | | submitForm.templateId = row.templateId; |
| | | submitForm.templateName = row.templateName || ""; |
| | | } |
| | | openNewsFormDialog("edit", row); |
| | | } |
| | | |
| | | function openNewsDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function openNewsEditFromDetail() { |
| | | const row = detailRow.value; |
| | | detailDialog.visible = false; |
| | | openNewsEdit(row); |
| | | } |
| | | |
| | | async function handleNewsDelete(row) { |
| | | if (!canEditEnterpriseNewsRow(row)) { |
| | | ElMessage.warning("å½åç¶æä¸å¯å é¤"); |
| | | return; |
| | | } |
| | | if (row?.id == null || row.id === "") { |
| | | ElMessage.warning("æ æ³å é¤ï¼ç¼ºå°æ°é» ID"); |
| | | return; |
| | | } |
| | | const title = (row.title || "").trim() || "è¯¥æ¡æ°é»"; |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤ã${title}ãåï¼å é¤åä¸å¯æ¢å¤ã`, |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "ç¡®å®å é¤", |
| | | cancelButtonText: "åæ¶", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | try { |
| | | await deleteEnterpriseNews([row.id]); |
| | | ElMessage.success("å 餿å"); |
| | | await fetchNewsList(searchForm); |
| | | } catch { |
| | | /* é误ç±è¯·æ±æ¦æªå¨æç¤º */ |
| | | } |
| | | } |
| | | |
| | | function onNewsFormClosed() { |
| | | newsFormRef.value?.resetFields?.(); |
| | | } |
| | | |
| | | function addGalleryItem() { |
| | | const name = (galleryInput.value || "").trim(); |
| | | if (!name) return; |
| | | newsForm.mediaList = newsForm.mediaList || []; |
| | | newsForm.mediaList.push({ type: "image", name, url: "" }); |
| | | galleryInput.value = ""; |
| | | } |
| | | |
| | | async function onNewsSave(action = "submit_review") { |
| | | try { |
| | | await newsFormRef.value?.validate(); |
| | | } catch { |
| | | ElMessage.warning("请å®å表åå¿
填项ååä¿å"); |
| | | return; |
| | | } |
| | | const v = validateNewsForm(newsForm); |
| | | if (!v.ok) { |
| | | ElMessage.warning(v.message); |
| | | return; |
| | | } |
| | | const status = action === "draft" ? "DRAFT" : "PENDING"; |
| | | newsForm.publishStatus = status; |
| | | |
| | | if (!isNewsEdit.value) { |
| | | const templateId = newsForm.templateId || submitForm.templateId; |
| | | if (!templateId) { |
| | | ElMessage.warning("请å
éæ©å®¡æ¹æ¨¡æ¿"); |
| | | return; |
| | | } |
| | | if (!newsForm.templateId) newsForm.templateId = templateId; |
| | | if (!newsForm.templateName && submitForm.templateName) { |
| | | newsForm.templateName = submitForm.templateName; |
| | | } |
| | | if (action !== "draft") { |
| | | const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes }); |
| | | if (!bindingCheck.ok) { |
| | | ElMessage.warning(bindingCheck.message); |
| | | return; |
| | | } |
| | | } |
| | | } else if (!newsForm.templateId && submitForm.templateId) { |
| | | newsForm.templateId = submitForm.templateId; |
| | | newsForm.templateName = submitForm.templateName || newsForm.templateName; |
| | | } |
| | | |
| | | const dto = buildEnterpriseNewsSaveDto(newsForm, { status }); |
| | | if (isNewsEdit.value) { |
| | | if (dto.id == null) { |
| | | ElMessage.warning("æ æ³ä¿®æ¹ï¼ç¼ºå°æ°é» ID"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | if (newsSaving.value) return; |
| | | newsSaving.value = true; |
| | | try { |
| | | if (isNewsEdit.value) { |
| | | await updateEnterpriseNews(dto); |
| | | } else { |
| | | await saveEnterpriseNews(dto); |
| | | } |
| | | newsFormDialog.visible = false; |
| | | const msg = |
| | | action === "draft" ? "å·²ä¿åè稿" : isNewsEdit.value ? "ä¿®æ¹æå" : "å·²æäº¤å®¡æ ¸"; |
| | | ElMessage.success(msg); |
| | | if (!isNewsEdit.value) page.current = 1; |
| | | await fetchNewsList(searchForm); |
| | | } catch { |
| | | /* é误ç±è¯·æ±æ¦æªå¨æç¤º */ |
| | | } finally { |
| | | newsSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | function onSearch() { |
| | | fetchNewsList(searchForm); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.keyword = ""; |
| | | searchForm.newsType = ""; |
| | | searchForm.status = ""; |
| | | searchForm.createTimeRange = null; |
| | | onSearch(); |
| | | } |
| | | |
| | | function onPagination(obj) { |
| | | paginateNewsList(obj, searchForm); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadFlowUsers(); |
| | | fetchNewsList(searchForm); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .enterprise-news-page .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | gap: 12px; |
| | | } |
| | | .search_fields { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | .search_actions { |
| | | flex-shrink: 0; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | .news-type-tag { |
| | | font-weight: 600; |
| | | font-size: 13px; |
| | | } |
| | | .media-tag { |
| | | margin: 6px 8px 0 0; |
| | | } |
| | | .template-name { |
| | | font-weight: 600; |
| | | color: var(--el-text-color-primary); |
| | | } |
| | | .section-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin: 8px 0 0; |
| | | line-height: 1.5; |
| | | } |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { listEnterpriseNewsPage } from "@/api/officeProcessAutomation/enterpriseNews.js"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { reactive, ref } from "vue"; |
| | | import { |
| | | buildEnterpriseNewsListParams, |
| | | mapEnterpriseNewsFromApi, |
| | | unwrapEnterpriseNewsPage, |
| | | } from "./enterpriseNewsMappers.js"; |
| | | |
| | | /** ä¼ä¸æ°é»å表ï¼å页æ¥è¯¢ /enterpriseNews/listPage */ |
| | | export function useEnterpriseNewsList() { |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | let lastSearchForm = null; |
| | | |
| | | async function fetchList(searchForm = {}) { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listEnterpriseNewsPage( |
| | | buildEnterpriseNewsListParams({ page, searchForm }) |
| | | ); |
| | | const { records, total } = unwrapEnterpriseNewsPage(res); |
| | | tableData.value = records.map(mapEnterpriseNewsFromApi); |
| | | page.total = total; |
| | | } catch { |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | ElMessage.error("ä¼ä¸æ°é»å表å 载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleQuery(searchForm) { |
| | | lastSearchForm = searchForm; |
| | | page.current = 1; |
| | | return fetchList(searchForm); |
| | | } |
| | | |
| | | function pagination({ page: p, limit }, searchForm) { |
| | | page.current = p; |
| | | page.size = limit; |
| | | return fetchList(searchForm ?? lastSearchForm ?? {}); |
| | | } |
| | | |
| | | return { |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | fetchList, |
| | | handleQuery, |
| | | pagination, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å²ä½ç®¡ç--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch"> |
| | | <el-form-item label="å²ä½ç¼ç " prop="postCode"> |
| | | <el-input |
| | | v-model="queryParams.postCode" |
| | | placeholder="请è¾å
¥å²ä½ç¼ç " |
| | | clearable |
| | | style="width: 200px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="å²ä½åç§°" prop="postName"> |
| | | <el-input |
| | | v-model="queryParams.postName" |
| | | placeholder="请è¾å
¥å²ä½åç§°" |
| | | clearable |
| | | style="width: 200px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select v-model="queryParams.status" placeholder="å²ä½ç¶æ" clearable style="width: 200px"> |
| | | <el-option |
| | | v-for="dict in sys_normal_disable" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="Search" @click="handleQuery">æç´¢</el-button> |
| | | <el-button icon="Refresh" @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="primary" |
| | | plain |
| | | icon="Plus" |
| | | @click="handleAdd" |
| | | v-hasPermi="['system:post:add']" |
| | | >æ°å¢</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="success" |
| | | plain |
| | | icon="Edit" |
| | | :disabled="single" |
| | | @click="handleUpdate" |
| | | v-hasPermi="['system:post:edit']" |
| | | >ä¿®æ¹</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | icon="Delete" |
| | | :disabled="multiple" |
| | | @click="handleDelete" |
| | | v-hasPermi="['system:post:remove']" |
| | | >å é¤</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="warning" |
| | | plain |
| | | icon="Download" |
| | | @click="handleExport" |
| | | v-hasPermi="['system:post:export']" |
| | | >导åº</el-button> |
| | | </el-col> |
| | | <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> |
| | | </el-row> |
| | | |
| | | <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="55" align="center" /> |
| | | <el-table-column label="å²ä½ç¼å·" align="center" prop="postId" /> |
| | | <el-table-column label="å²ä½ç¼ç " align="center" prop="postCode" /> |
| | | <el-table-column label="å²ä½åç§°" align="center" prop="postName" /> |
| | | <el-table-column label="å²ä½æåº" align="center" prop="postSort" /> |
| | | <el-table-column label="ç¶æ" align="center" prop="status"> |
| | | <template #default="scope"> |
| | | <dict-tag :options="sys_normal_disable" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å建æ¶é´" align="center" prop="createTime" width="180"> |
| | | <template #default="scope"> |
| | | <span>{{ parseTime(scope.row.createTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="180" align="center" class-name="small-padding fixed-width"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:post:edit']">ä¿®æ¹</el-button> |
| | | <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:post:remove']">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | v-model:page="queryParams.pageNum" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | |
| | | <!-- æ·»å æä¿®æ¹å²ä½å¯¹è¯æ¡ --> |
| | | <el-dialog :title="title" v-model="open" width="500px" append-to-body> |
| | | <el-form ref="postRef" :model="form" :rules="rules" label-width="80px"> |
| | | <el-form-item label="å²ä½åç§°" prop="postName"> |
| | | <el-input v-model="form.postName" placeholder="请è¾å
¥å²ä½åç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="å²ä½ç¼ç " prop="postCode"> |
| | | <el-input v-model="form.postCode" placeholder="请è¾å
¥ç¼ç åç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="å²ä½é¡ºåº" prop="postSort"> |
| | | <el-input-number v-model="form.postSort" controls-position="right" :min="0" /> |
| | | </el-form-item> |
| | | <el-form-item label="å²ä½ç¶æ" prop="status"> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio |
| | | v-for="dict in sys_normal_disable" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | >{{ dict.label }}</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input v-model="form.remark" type="textarea" placeholder="请è¾å
¥å
容" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">ç¡® å®</el-button> |
| | | <el-button @click="cancel">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="Post"> |
| | | import { listPost, addPost, delPost, getPost, updatePost } from "@/api/system/post" |
| | | import {onMounted} from "vue"; |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const { sys_normal_disable } = proxy.useDict("sys_normal_disable") |
| | | |
| | | const postList = ref([]) |
| | | const open = ref(false) |
| | | const loading = ref(true) |
| | | const showSearch = ref(true) |
| | | const ids = ref([]) |
| | | const single = ref(true) |
| | | const multiple = ref(true) |
| | | const total = ref(0) |
| | | const title = ref("") |
| | | |
| | | const data = reactive({ |
| | | form: {}, |
| | | queryParams: { |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | postCode: undefined, |
| | | postName: undefined, |
| | | status: undefined |
| | | }, |
| | | rules: { |
| | | postName: [{ required: true, message: "å²ä½åç§°ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | postCode: [{ required: true, message: "å²ä½ç¼ç ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | postSort: [{ required: true, message: "å²ä½é¡ºåºä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | } |
| | | }) |
| | | |
| | | const { queryParams, form, rules } = toRefs(data) |
| | | |
| | | /** æ¥è¯¢å²ä½å表 */ |
| | | function getList() { |
| | | loading.value = true |
| | | listPost(queryParams.value).then(response => { |
| | | postList.value = response.rows |
| | | total.value = response.total |
| | | loading.value = false |
| | | }) |
| | | } |
| | | |
| | | /** åæ¶æé® */ |
| | | function cancel() { |
| | | open.value = false |
| | | reset() |
| | | } |
| | | |
| | | /** 表åéç½® */ |
| | | function reset() { |
| | | form.value = { |
| | | postId: undefined, |
| | | postCode: undefined, |
| | | postName: undefined, |
| | | postSort: 0, |
| | | status: "0", |
| | | remark: undefined |
| | | } |
| | | proxy.resetForm("postRef") |
| | | } |
| | | |
| | | /** æç´¢æé®æä½ */ |
| | | function handleQuery() { |
| | | queryParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | /** éç½®æé®æä½ */ |
| | | function resetQuery() { |
| | | proxy.resetForm("queryRef") |
| | | handleQuery() |
| | | } |
| | | |
| | | /** å¤éæ¡é䏿°æ® */ |
| | | function handleSelectionChange(selection) { |
| | | ids.value = selection.map(item => item.postId) |
| | | single.value = selection.length != 1 |
| | | multiple.value = !selection.length |
| | | } |
| | | |
| | | /** æ°å¢æé®æä½ */ |
| | | function handleAdd() { |
| | | reset() |
| | | open.value = true |
| | | title.value = "æ·»å å²ä½" |
| | | } |
| | | |
| | | /** ä¿®æ¹æé®æä½ */ |
| | | function handleUpdate(row) { |
| | | reset() |
| | | const postId = row.postId || ids.value |
| | | getPost(postId).then(response => { |
| | | form.value = response.data |
| | | open.value = true |
| | | title.value = "ä¿®æ¹å²ä½" |
| | | }) |
| | | } |
| | | |
| | | /** æäº¤æé® */ |
| | | function submitForm() { |
| | | proxy.$refs["postRef"].validate(valid => { |
| | | if (valid) { |
| | | if (form.value.postId != undefined) { |
| | | updatePost(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | open.value = false |
| | | getList() |
| | | }) |
| | | } else { |
| | | addPost(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå") |
| | | open.value = false |
| | | getList() |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** å é¤æé®æä½ */ |
| | | function handleDelete(row) { |
| | | const postIds = row.postId || ids.value |
| | | proxy.$modal.confirm('æ¯å¦ç¡®è®¤å é¤å²ä½ç¼å·ä¸º"' + postIds + '"çæ°æ®é¡¹ï¼').then(function() { |
| | | return delPost(postIds) |
| | | }).then(() => { |
| | | getList() |
| | | proxy.$modal.msgSuccess("å 餿å") |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | /** å¯¼åºæé®æä½ */ |
| | | function handleExport() { |
| | | proxy.download("system/post/export", { |
| | | ...queryParams.value |
| | | }, `post_${new Date().getTime()}.xlsx`) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼è½¬æ£ç³è¯·--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">审æ¹åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.instanceNo" |
| | | style="width: 220px" |
| | | placeholder="请è¾å
¥å®¡æ¹åå·" |
| | | clearable |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">ç³è¯·äººï¼</span> |
| | | <el-input |
| | | v-model="searchForm.applicantName" |
| | | style="width: 220px" |
| | | placeholder="请è¾å
¥ç³è¯·äºº" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <el-button type="primary" style="margin-left: 10px" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openAddWithTemplate">æ°å¢è½¬æ£ç³è¯·</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | @pagination="onPagination" |
| | | :total="page.total" |
| | | /> |
| | | </div> |
| | | |
| | | <ApprovalInstanceSubmitDialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialogTitle" |
| | | :form="submitForm" |
| | | :rules="submitFormRules" |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | | @submit="onSubmit" |
| | | /> |
| | | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.REGULAR" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <ApprovalInstanceDetailDialog |
| | | v-model="detailDialog.visible" |
| | | title="转æ£ç³è¯·è¯¦æ
" |
| | | :row="detailRow" |
| | | @edit="openEditFromDetail" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { onMounted, reactive } from "vue"; |
| | | import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue"; |
| | | import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue"; |
| | | import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue"; |
| | | import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | |
| | | const searchForm = reactive({ |
| | | instanceNo: "", |
| | | applicantName: "", |
| | | }); |
| | | |
| | | const mod = useApprovalInstanceModule({ |
| | | moduleKey: APPROVAL_MODULE_KEYS.REGULAR, |
| | | buildExtraListParams(sf) { |
| | | const extra = {}; |
| | | const name = (sf?.applicantName || "").trim(); |
| | | if (name) extra.applicantName = name; |
| | | return extra; |
| | | }, |
| | | }); |
| | | |
| | | const { |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | detailDialog, |
| | | detailRow, |
| | | submitDialog, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | isSubmitEdit, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitDialogTitle, |
| | | templateBindVisible, |
| | | handleQuery, |
| | | initModuleList, |
| | | pagination, |
| | | openAddWithTemplate, |
| | | onTemplateBound, |
| | | onTemplateBindClosed, |
| | | openEditFromDetail, |
| | | submitInstanceForm, |
| | | buildTableActions, |
| | | } = mod; |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, { |
| | | moduleKey: APPROVAL_MODULE_KEYS.REGULAR, |
| | | }); |
| | | |
| | | function onSearch() { |
| | | handleQuery(searchForm); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.instanceNo = ""; |
| | | searchForm.applicantName = ""; |
| | | onSearch(); |
| | | } |
| | | |
| | | function onPagination(obj) { |
| | | pagination(obj, searchForm); |
| | | } |
| | | |
| | | async function onSubmit() { |
| | | const ok = await submitInstanceForm({ skipValidate: true }); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "æäº¤æå"); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | loadFlowUsers(); |
| | | await initModuleList(searchForm); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢ç¦»è' : 'ç¼è¾ç¦»è'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <!-- å工信æ¯å±ç¤ºåºå --> |
| | | <div class="info-section"> |
| | | <div class="info-title">å工信æ¯</div> |
| | | <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å§åï¼" prop="staffOnJobId"> |
| | | <el-select v-model="form.staffOnJobId" |
| | | placeholder="è¯·éæ©äººå" |
| | | style="width: 100%" |
| | | :disabled="operationType === 'edit'" |
| | | @change="handleSelect"> |
| | | <el-option |
| | | v-for="item in personList" |
| | | :key="item.id" |
| | | :label="item.staffName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥ç¼å·ï¼"> |
| | | {{ currentStaffRecord.staffNo || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ§å«ï¼"> |
| | | {{ currentStaffRecord.sex || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ·ç±ä½åï¼"> |
| | | {{ currentStaffRecord.nativePlace || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å²ä½ï¼"> |
| | | {{ currentStaffRecord.postName || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç°ä½åï¼"> |
| | | {{ currentStaffRecord.adress || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="第ä¸å¦åï¼"> |
| | | {{ currentStaffRecord.firstStudy || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸ä¸ï¼"> |
| | | {{ currentStaffRecord.profession || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¹´é¾ï¼"> |
| | | {{ currentStaffRecord.age || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="èç³»çµè¯ï¼"> |
| | | {{ currentStaffRecord.phone || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç´§æ¥è系人ï¼"> |
| | | {{ currentStaffRecord.emergencyContact || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç´§æ¥è系人èç³»çµè¯ï¼"> |
| | | {{ currentStaffRecord.emergencyContactPhone || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¦»èæ¥æï¼" prop="leaveDate"> |
| | | <el-date-picker |
| | | v-model="form.leaveDate" |
| | | type="date" |
| | | :disabled="operationType === 'edit'" |
| | | :disabled-date="disabledFutureDate" |
| | | placeholder="è¯·éæ©ç¦»èæ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="离èåå ï¼" prop="reason"> |
| | | <el-select v-model="form.reason" placeholder="è¯·éæ©ç¦»èåå " style="width: 100%" @change="handleSelectDimissionReason"> |
| | | <el-option |
| | | v-for="(item, index) in dimissionReasonOptions" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="夿³¨ï¼" prop="remark" v-if="form.reason === 'other'"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="夿³¨" |
| | | maxlength="500" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- <el-row :gutter="30">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <div class="info-item">--> |
| | | <!-- <span class="info-label">离èåå ï¼</span>--> |
| | | <!-- <el-select v-model="form.reason" placeholder="è¯·éæ©äººå" style="width: 100%" @change="handleSelect">--> |
| | | <!-- <el-option--> |
| | | <!-- v-for="(item, index) in dimissionReasonOptions"--> |
| | | <!-- :key="index"--> |
| | | <!-- :label="item.label"--> |
| | | <!-- :value="item.value"--> |
| | | <!-- />--> |
| | | <!-- </el-select>--> |
| | | <!-- </div>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <div class="info-item">--> |
| | | <!-- <span class="info-label">åå·¥ç¼å·ï¼</span>--> |
| | | <!-- <span class="info-value">{{ form.staffNo || '-' }}</span>--> |
| | | <!-- </div>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, reactive, toRefs, getCurrentInstance} from "vue"; |
| | | import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; |
| | | import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const getTodayDate = () => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = `${now.getMonth() + 1}`.padStart(2, '0'); |
| | | const day = `${now.getDate()}`.padStart(2, '0'); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | const disabledFutureDate = (time) => { |
| | | const todayEnd = new Date(); |
| | | todayEnd.setHours(23, 59, 59, 999); |
| | | return time.getTime() > todayEnd.getTime(); |
| | | }; |
| | | const data = reactive({ |
| | | form: { |
| | | staffOnJobId: undefined, |
| | | leaveDate: "", |
| | | reason: "", |
| | | remark: "", |
| | | }, |
| | | rules: { |
| | | staffName: [{ required: true, message: "è¯·éæ©äººå" }], |
| | | leaveDate: [{ required: true, message: "è¯·éæ©ç¦»èæ¥æ", trigger: "change" }], |
| | | reason: [{ required: true, message: "è¯·éæ©ç¦»èåå "}], |
| | | }, |
| | | dimissionReasonOptions: [ |
| | | {label: 'èªèµå¾
é', value: 'salary'}, |
| | | {label: 'èä¸åå±', value: 'career_development'}, |
| | | {label: 'å·¥ä½ç¯å¢', value: 'work_environment'}, |
| | | {label: '个人åå ', value: 'personal_reason'}, |
| | | {label: 'å
¶ä»', value: 'other'}, |
| | | ], |
| | | currentStaffRecord: {}, |
| | | }); |
| | | const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data); |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'edit') { |
| | | currentStaffRecord.value = row |
| | | form.value.staffOnJobId = row.staffOnJobId |
| | | form.value.leaveDate = row.leaveDate |
| | | form.value.reason = row.reason |
| | | form.value.remark = row.remark |
| | | personList.value = [ |
| | | { |
| | | staffName: row.staffName, |
| | | id: row.staffOnJobId, |
| | | } |
| | | ] |
| | | } else { |
| | | form.value.leaveDate = getTodayDate() |
| | | getList() |
| | | } |
| | | } |
| | | |
| | | const handleSelectDimissionReason = (val) => { |
| | | if (val === 'other') { |
| | | form.value.remark = '' |
| | | } |
| | | } |
| | | // æäº¤äº§å表å |
| | | const submitForm = () => { |
| | | form.value.staffState = 0 |
| | | if (form.value.reason !== 'other') { |
| | | form.value.remark = '' |
| | | } |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | if (operationType.value === "add") { |
| | | createStaffLeave(form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | } else { |
| | | updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | |
| | | } |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | // 表å已注éï¼æå¨éç½®è¡¨åæ°æ® |
| | | form.value = { |
| | | staffOnJobId: undefined, |
| | | leaveDate: "", |
| | | reason: "", |
| | | remark: "", |
| | | }; |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | }; |
| | | |
| | | const personList = ref([]); |
| | | |
| | | /** |
| | | * è·åå½åå¨è人åå表 |
| | | */ |
| | | const getList = () => { |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | }).then(res => { |
| | | personList.value = res.data.records || [] |
| | | }) |
| | | }; |
| | | |
| | | const handleSelect = (val) => { |
| | | let obj = personList.value.find(item => item.id === val) |
| | | currentStaffRecord.value = {} |
| | | if (obj) { |
| | | // ä¿çç¦»èæ¥æå离èåå ï¼åªæ´æ°åå·¥ä¿¡æ¯ |
| | | currentStaffRecord.value = obj |
| | | } |
| | | } |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .info-section { |
| | | background: #f5f7fa; |
| | | padding: 20px; |
| | | border-radius: 8px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .info-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 16px; |
| | | min-height: 32px; |
| | | } |
| | | |
| | | .info-label { |
| | | min-width: 140px; |
| | | color: #606266; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | color: #303133; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼ç¦»èç³è¯·--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input |
| | | v-model="searchForm.staffName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å§åæç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢ç¦»è</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref} from "vue"; |
| | | import FormDia from "@/views/personnelManagement/dimission/components/formDia.vue"; |
| | | import { findStaffLeaveListPage } from "@/api/personnelManagement/staffLeave.js"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | staffName: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | | return "å¨è"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | | return "primary"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | }, |
| | | { |
| | | label: "ç¦»èæ¥æ", |
| | | prop: "leaveDate", |
| | | }, |
| | | { |
| | | label: "åå·¥ç¼å·", |
| | | prop: "staffNo", |
| | | }, |
| | | { |
| | | label: "å§å", |
| | | prop: "staffName", |
| | | }, |
| | | { |
| | | label: "æ§å«", |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "é¨é¨", |
| | | prop: "deptName", |
| | | }, |
| | | { |
| | | label: "å²ä½", |
| | | prop: "postName", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width:200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | | prop: "firstStudy", |
| | | }, |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width:100 |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | | prop: "age", |
| | | }, |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width:150 |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width:150 |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const formDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | findStaffLeaveListPage({...page, ...searchForm.value}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/staff/staffLeave/export", {}, "人å离è.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | åºæ¬ä¿¡æ¯ |
| | | </span> |
| | | </template> |
| | | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="5"> |
| | | <el-form-item label="åå·¥ç¼å·" prop="staffNo"> |
| | | <el-input |
| | | v-model="form.staffNo" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | :disabled="operationType !== 'add'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="å§å" prop="staffName"> |
| | | <el-input |
| | | v-model="form.staffName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="å«å" prop="alias"> |
| | | <el-input |
| | | v-model="form.alias" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="ææº" prop="phone"> |
| | | <el-input |
| | | v-model="form.phone" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="11" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="æ§å«" prop="sex"> |
| | | <el-select |
| | | v-model="form.sex" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="ç·" value="ç·" /> |
| | | <el-option label="女" value="女" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="5"> |
| | | <el-form-item label="åºçæ¥æ" prop="birthDate"> |
| | | <el-date-picker |
| | | v-model="form.birthDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="å¹´é¾" prop="age"> |
| | | <el-input-number |
| | | v-model="form.age" |
| | | :min="0" |
| | | :max="150" |
| | | :precision="0" |
| | | :step="1" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="ç±è´¯" prop="nativePlace"> |
| | | <el-input |
| | | v-model="form.nativePlace" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="æ°æ" prop="nation"> |
| | | <el-input |
| | | v-model="form.nation" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="å©å§»ç¶åµ" prop="maritalStatus"> |
| | | <el-select |
| | | v-model="form.maritalStatus" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="æªå©" value="æªå©" /> |
| | | <el-option label="å·²å©" value="å·²å©" /> |
| | | <el-option label="离å¼" value="离å¼" /> |
| | | <el-option label="丧å¶" value="丧å¶" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="10"> |
| | | <el-form-item label="è§è²" prop="roleId"> |
| | | <el-select |
| | | v-model="form.roleId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in roleOptions" |
| | | :key="item.roleId" |
| | | :label="item.roleName" |
| | | :value="item.roleId" |
| | | :disabled="item.status == 1" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true }, |
| | | operationType: { type: String, default: "add" }, |
| | | roleOptions: { type: Array, default: () => [] }, |
| | | }); |
| | | |
| | | const { form, operationType, roleOptions } = toRefs(props); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <!-- æè²ç»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | æè²ç»å |
| | | </span> |
| | | </template> |
| | | <el-table :data="form.staffEducationList" border> |
| | | <el-table-column label="å¦å" prop="education" width="120"> |
| | | <template #default="{ row }"> |
| | | <el-select |
| | | v-model="row.education" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="ä¸ä¸å以ä¸" value="secondary" /> |
| | | <el-option label="大ä¸" value="junior_college" /> |
| | | <el-option label="æ¬ç§" value="bachelor" /> |
| | | <el-option label="ç¡å£«" value="master" /> |
| | | <el-option label="å士å以ä¸" value="doctor" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¯ä¸é¢æ ¡" prop="schoolName" min-width="160"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.schoolName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="30" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¥å¦æ¶é´" prop="enrollTime" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.enrollTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¯ä¸æ¶é´" prop="graduateTime" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.graduateTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ä¸ä¸" prop="major" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.major" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¦ä½" prop="degree" width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.degree" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="form.staffEducationList.length > 1" |
| | | type="primary" |
| | | link |
| | | @click="removeEducationRow(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-add-row" @click="addEducationRow">æ°å»ºä¸è¡</div> |
| | | </el-card> |
| | | |
| | | <!-- å·¥ä½ç»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | å·¥ä½ç»å |
| | | </span> |
| | | </template> |
| | | <el-table :data="form.staffWorkExperienceList" border> |
| | | <el-table-column label="åå
¬å¸" prop="formerCompany" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.formerCompany" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="30" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åå
¬å¸é¨é¨" prop="formerDept" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.formerDept" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åå
¬å¸èä½" prop="formerPosition" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.formerPosition" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¼å§æ¥æ" prop="startDate" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.startDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç»ææ¥æ" prop="endDate" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.endDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥ä½æè¿°" prop="workDesc" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.workDesc" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="500" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="form.staffWorkExperienceList.length > 1" |
| | | type="primary" |
| | | link |
| | | @click="removeWorkRow(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-add-row" @click="addWorkRow">æ°å»ºä¸è¡</div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:form"]); |
| | | |
| | | const { form } = toRefs(props); |
| | | |
| | | const addEducationRow = () => { |
| | | form.value.staffEducationList.push({ |
| | | education: "", |
| | | schoolName: "", |
| | | enrollTime: "", |
| | | graduateTime: "", |
| | | major: "", |
| | | degree: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeEducationRow = (index) => { |
| | | if (form.value.staffEducationList.length <= 1) return; |
| | | form.value.staffEducationList.splice(index, 1); |
| | | }; |
| | | |
| | | const addWorkRow = () => { |
| | | form.value.staffWorkExperienceList.push({ |
| | | formerCompany: "", |
| | | formerDept: "", |
| | | formerPosition: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | workDesc: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeWorkRow = (index) => { |
| | | if (form.value.staffWorkExperienceList.length <= 1) return; |
| | | form.value.staffWorkExperienceList.splice(index, 1); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .table-add-row { |
| | | margin-top: 8px; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <!-- ç´§æ¥è系人 --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | ç´§æ¥è系人 |
| | | </span> |
| | | </template> |
| | | <el-table :data="form.staffEmergencyContactList" border> |
| | | <el-table-column label="ç´§æ¥è系人å§å" prop="contactName" min-width="160"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç´§æ¥è系人å
³ç³»" prop="contactRelation" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactRelation" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç´§æ¥èç³»äººææº" prop="contactPhone" width="160"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactPhone" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="11" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç´§æ¥è系人ä½å" prop="contactAddress" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactAddress" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="form.staffEmergencyContactList.length > 1" |
| | | type="primary" |
| | | link |
| | | @click="removeEmergencyRow(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-add-row" @click="addEmergencyRow">æ°å»ºä¸è¡</div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true } |
| | | }); |
| | | |
| | | const { form } = toRefs(props); |
| | | |
| | | const addEmergencyRow = () => { |
| | | form.value.staffEmergencyContactList.push({ |
| | | contactName: "", |
| | | contactRelation: "", |
| | | contactPhone: "", |
| | | contactAddress: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeEmergencyRow = (index) => { |
| | | if (form.value.staffEmergencyContactList.length <= 1) return; |
| | | form.value.staffEmergencyContactList.splice(index, 1); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .table-add-row { |
| | | margin-top: 8px; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | å¨èä¿¡æ¯ |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- 第ä¸è¡ï¼ååå¼å§ / ååç»æ / è¯ç¨æ / è½¬æ£ --> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å
¥èæ¥æ" prop="contractStartTime"> |
| | | <el-date-picker |
| | | v-model="form.contractStartTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | @change="calculateContractTerm" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item |
| | | label="ååç»ææ¥æ" |
| | | prop="contractEndTime" |
| | | required |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©ååç»ææ¥æ', |
| | | trigger: 'change', |
| | | }, |
| | | ]" |
| | | > |
| | | <el-date-picker |
| | | v-model="form.contractEndTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | @change="calculateContractTerm" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="è¯ç¨æï¼æï¼" prop="probationPeriod"> |
| | | <el-input-number |
| | | v-model="form.proTerm" |
| | | :min="0" |
| | | :max="24" |
| | | :precision="0" |
| | | :step="1" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="è½¬æ£æ¥æ" prop="positiveDate"> |
| | | <el-date-picker |
| | | v-model="form.positiveDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 第äºè¡ï¼é¨é¨ / å²ä½ / åºæ¬å·¥èµ --> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="é¨é¨" prop="sysDeptId"> |
| | | <el-tree-select |
| | | v-model="form.sysDeptId" |
| | | :data="deptOptions" |
| | | check-strictly |
| | | :render-after-expand="false" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="å²ä½" prop="sysPostId"> |
| | | <el-select |
| | | v-model="form.sysPostId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in postOptions" |
| | | :key="item.postId" |
| | | :label="item.postName" |
| | | :value="item.postId" |
| | | :disabled="item.status === '1'" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="åºæ¬å·¥èµ" prop="basicSalary"> |
| | | <el-input-number |
| | | v-model="form.basicSalary" |
| | | :min="0" |
| | | :max="999999" |
| | | :precision="2" |
| | | :step="100" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true }, |
| | | postOptions: { type: Array, default: () => [] }, |
| | | deptOptions: { type: Array, default: () => [] }, |
| | | }); |
| | | |
| | | const { form, postOptions, deptOptions } = toRefs(props); |
| | | |
| | | // 计ç®ååå¹´é |
| | | const calculateContractTerm = () => { |
| | | if (form.value.contractStartTime && form.value.contractEndTime) { |
| | | const startDate = new Date(form.value.contractStartTime); |
| | | const endDate = new Date(form.value.contractEndTime); |
| | | |
| | | if (endDate > startDate) { |
| | | // 计ç®å¹´ä»½å·® |
| | | const yearDiff = endDate.getFullYear() - startDate.getFullYear(); |
| | | const monthDiff = endDate.getMonth() - startDate.getMonth(); |
| | | const dayDiff = endDate.getDate() - startDate.getDate(); |
| | | |
| | | let years = yearDiff; |
| | | |
| | | // å¦æç»ææ¥æçææ¥å°äºå¼å§æ¥æçææ¥ï¼ååå»1å¹´ |
| | | if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { |
| | | years = yearDiff - 1; |
| | | } |
| | | |
| | | form.value.contractTerm = Math.max(0, years); |
| | | } else { |
| | | form.value.contractTerm = 0; |
| | | } |
| | | } else { |
| | | form.value.contractTerm = 0; |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <FormDialog |
| | | v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="90%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia" |
| | | > |
| | | <div class="form-dia-body"> |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top" |
| | | > |
| | | <BasicInfoSection |
| | | :form="form" |
| | | :operation-type="operationType" |
| | | :role-options="roleOptions" |
| | | /> |
| | | <JobInfoSection |
| | | :form="form" |
| | | :post-options="postOptions" |
| | | :dept-options="deptOptions" |
| | | /> |
| | | <EducationWorkSection :form="form" /> |
| | | <EmergencyAndAttachmentSection :form="form" /> |
| | | </el-form> |
| | | </div> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | onMounted, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { findPostOptions } from "@/api/system/post.js"; |
| | | import { deptTreeSelect, getUser } from "@/api/system/user.js"; |
| | | import { |
| | | staffOnJobInfo, |
| | | createStaffOnJob, |
| | | updateStaffOnJob, |
| | | } from "@/api/personnelManagement/staffOnJob.js"; |
| | | |
| | | import BasicInfoSection from "./BasicInfoSection.vue"; |
| | | import JobInfoSection from "./JobInfoSection.vue"; |
| | | import EducationWorkSection from "./EducationWorkSection.vue"; |
| | | import EmergencyAndAttachmentSection from "./EmergencyAndAttachmentSection.vue"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const id = ref(0); |
| | | const formRef = ref(null); |
| | | |
| | | const dialogTitle = () => |
| | | operationType.value === "add" ? "æ°å¢å
¥è" : "ç¼è¾äººå"; |
| | | |
| | | const createEmptyEducation = () => ({ |
| | | education: "", |
| | | schoolName: "", |
| | | enrollTime: "", |
| | | graduateTime: "", |
| | | major: "", |
| | | degree: "", |
| | | }); |
| | | |
| | | const createEmptyWork = () => ({ |
| | | formerCompany: "", |
| | | formerDept: "", |
| | | formerPosition: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | workDesc: "", |
| | | }); |
| | | |
| | | const createEmptyEmergency = () => ({ |
| | | contactName: "", |
| | | contactRelation: "", |
| | | contactPhone: "", |
| | | contactAddress: "", |
| | | }); |
| | | |
| | | const createDefaultForm = () => ({ |
| | | id: undefined, |
| | | // åºæ¬ä¿¡æ¯ |
| | | staffNo: "", |
| | | staffName: "", |
| | | alias: "", |
| | | phone: "", |
| | | sex: "", |
| | | birthDate: "", |
| | | age: undefined, |
| | | nativePlace: "", |
| | | nation: "", |
| | | maritalStatus: "", |
| | | politicalStatus: "", |
| | | firstWorkDate: "", |
| | | workingYears: undefined, |
| | | idCardNo: "", |
| | | hukouType: "", |
| | | email: "", |
| | | currentAddress: "", |
| | | // å¨èä¿¡æ¯ |
| | | contractStartTime: "", |
| | | contractEndTime: "", |
| | | proTerm: undefined, |
| | | positiveDate: "", |
| | | sysDeptId: undefined, |
| | | sysPostId: undefined, |
| | | basicSalary: undefined, |
| | | // é¶è¡å¡ä¿¡æ¯ |
| | | bankName: "", |
| | | bankCardNo: "", |
| | | // æè²ç»å |
| | | staffEducationList: [createEmptyEducation()], |
| | | // å·¥ä½ç»å |
| | | staffWorkExperienceList: [createEmptyWork()], |
| | | // ç´§æ¥è系人 |
| | | staffEmergencyContactList: [createEmptyEmergency()], |
| | | // è§è²ï¼åéï¼ |
| | | roleId: undefined, |
| | | }); |
| | | |
| | | const state = reactive({ |
| | | form: createDefaultForm(), |
| | | rules: { |
| | | staffNo: [{ required: true, message: "请è¾å
¥åå·¥ç¼å·", trigger: "blur" }], |
| | | staffName: [{ required: true, message: "请è¾å
¥å§å", trigger: "blur" }], |
| | | phone: [{ required: true, message: "请è¾å
¥ææº", trigger: "blur" }], |
| | | sex: [{ required: true, message: "è¯·éæ©æ§å«", trigger: "change" }], |
| | | birthDate: [ |
| | | { required: true, message: "è¯·éæ©åºçæ¥æ", trigger: "change" }, |
| | | ], |
| | | contractStartTime: [ |
| | | { required: true, message: "è¯·éæ©å
¥èæ¥æ", trigger: "change" }, |
| | | ], |
| | | contractEndTime: [ |
| | | { required: true, message: "è¯·éæ©ååç»ææ¥æ", trigger: "change" }, |
| | | ], |
| | | sysDeptId: [ |
| | | { required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }, |
| | | ], |
| | | roleId: [{ required: true, message: "è¯·éæ©è§è²", trigger: "change" }], |
| | | }, |
| | | postOptions: [], |
| | | deptOptions: [], |
| | | }); |
| | | |
| | | const { form, rules, postOptions, deptOptions } = toRefs(state); |
| | | const roleOptions = ref([]); |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(form.value, createDefaultForm()); |
| | | nextTick(() => { |
| | | formRef.value?.clearValidate(); |
| | | }); |
| | | }; |
| | | |
| | | const fetchPostOptions = () => { |
| | | findPostOptions().then((res) => { |
| | | postOptions.value = res.data || []; |
| | | }); |
| | | }; |
| | | |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then((response) => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data || [])) |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | const fetchRoleOptions = () => { |
| | | getUser().then((res) => { |
| | | roleOptions.value = res.roles || []; |
| | | }); |
| | | }; |
| | | |
| | | function filterDisabledDept(deptList) { |
| | | return deptList.filter((dept) => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | } |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | fetchPostOptions(); |
| | | fetchDeptOptions(); |
| | | fetchRoleOptions(); |
| | | resetForm(); |
| | | if (type === "edit" && row?.id) { |
| | | id.value = row.id; |
| | | staffOnJobInfo(id.value, {}).then((res) => { |
| | | const d = res.data || {}; |
| | | Object.assign(form.value, { |
| | | ...form.value, |
| | | ...d, |
| | | }); |
| | | if ( |
| | | !Array.isArray(form.value.staffEducationList) || |
| | | !form.value.staffEducationList.length |
| | | ) { |
| | | form.value.staffEducationList = [createEmptyEducation()]; |
| | | } |
| | | if ( |
| | | !Array.isArray(form.value.staffWorkExperienceList) || |
| | | !form.value.staffWorkExperienceList.length |
| | | ) { |
| | | form.value.staffWorkExperienceList = [createEmptyWork()]; |
| | | } |
| | | if ( |
| | | !Array.isArray(form.value.staffEmergencyContactList) || |
| | | !form.value.staffEmergencyContactList.length |
| | | ) { |
| | | form.value.staffEmergencyContactList = [createEmptyEmergency()]; |
| | | } |
| | | if (form.value.sysPostId === 0) { |
| | | form.value.sysPostId = undefined; |
| | | } |
| | | if (form.value.sysDeptId === 0) { |
| | | form.value.sysDeptId = undefined; |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | fetchPostOptions(); |
| | | fetchDeptOptions(); |
| | | }); |
| | | |
| | | const submitForm = () => { |
| | | if (!form.value.sysPostId) { |
| | | form.value.sysPostId = undefined; |
| | | } |
| | | if (!form.value.sysDeptId) { |
| | | form.value.sysDeptId = undefined; |
| | | } |
| | | // å
¼å®¹å端å¯è½ä»ä½¿ç¨ roleIds æ°ç» |
| | | form.value.roleIds = form.value.roleId ? [form.value.roleId] : []; |
| | | formRef.value?.validate((valid) => { |
| | | if (valid) { |
| | | if (operationType.value === "add") { |
| | | createStaffOnJob(form.value).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | updateStaffOnJob(id.value, form.value).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | formRef.value?.resetFields(); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-dia-body { |
| | | padding: 0; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="ç»ç¾åå" |
| | | width="800px" |
| | | @close="closeModal" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | <el-form-item label="ååå¼å§æ¥æï¼" prop="contractStartTime"> |
| | | <el-date-picker |
| | | v-model="form.contractStartTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="calculateContractTerm" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ååç»ææ¥æï¼" prop="contractEndTime"> |
| | | <el-date-picker |
| | | v-model="form.contractEndTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="calculateContractTerm" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ååå¹´éï¼" prop="contractTerm"> |
| | | <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeModal">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | // ç»ç¾åå |
| | | import { renewContract } from "@/api/personnelManagement/staffOnJob.js"; |
| | | import {computed, getCurrentInstance,} from "vue"; |
| | | |
| | | const emit = defineEmits(['update:visible', 'completed']); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | contractTerm: 0, |
| | | contractStartTime: "", |
| | | contractEndTime: "", |
| | | }, |
| | | rules: { |
| | | contractTerm: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | contractStartTime: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | contractEndTime: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | } |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | let { proxy } = getCurrentInstance() |
| | | |
| | | const props = defineProps({ |
| | | id: { |
| | | type: Number, |
| | | default: 0, |
| | | }, |
| | | |
| | | visible: { |
| | | type: Boolean, |
| | | required: true, |
| | | }, |
| | | }) |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | | }, |
| | | set(val) { |
| | | emit('update:visible', val); |
| | | }, |
| | | }); |
| | | |
| | | // 计ç®ååå¹´é |
| | | const calculateContractTerm = () => { |
| | | if (form.value.contractStartTime && form.value.contractEndTime) { |
| | | const startDate = new Date(form.value.contractStartTime); |
| | | const endDate = new Date(form.value.contractEndTime); |
| | | |
| | | if (endDate > startDate) { |
| | | // 计ç®å¹´ä»½å·® |
| | | const yearDiff = endDate.getFullYear() - startDate.getFullYear(); |
| | | const monthDiff = endDate.getMonth() - startDate.getMonth(); |
| | | const dayDiff = endDate.getDate() - startDate.getDate(); |
| | | |
| | | let years = yearDiff; |
| | | |
| | | // å¦æç»ææ¥æçææ¥å°äºå¼å§æ¥æçææ¥ï¼ååå»1å¹´ |
| | | if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { |
| | | years = yearDiff - 1; |
| | | } |
| | | |
| | | form.value.contractTerm = Math.max(0, years); |
| | | } else { |
| | | form.value.contractTerm = 0; |
| | | } |
| | | } else { |
| | | form.value.contractTerm = 0; |
| | | } |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | renewContract(props.id, form.value).then(res => { |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("ç»ç¾ååæå"); |
| | | emit('completed'); |
| | | closeModal(); |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeModal = () => { |
| | | // éç½®è¡¨åæ°æ® |
| | | form.value = { |
| | | contractTerm: 0, |
| | | contractStartTime: "", |
| | | contractEndTime: "", |
| | | }; |
| | | isShow.value = false; |
| | | }; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | title="详æ
" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :tableLoading="tableLoading" |
| | | height="600" |
| | | ></PIMTable> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref} from "vue"; |
| | | import {staffOnJobInfo} from "@/api/personnelManagement/staffOnJob.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "ååå¼å§æ¥æ", |
| | | prop: "contractStartTime", |
| | | }, |
| | | { |
| | | label: "ååç»ææ¥æ", |
| | | prop: "contractEndTime", |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'edit') { |
| | | staffOnJobInfo({staffNo: row.staffNo}).then(res => { |
| | | tableData.value = res.data |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å工档æ¡--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input |
| | | v-model="searchForm.staffName" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥å§åæç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span class="search_title search_title2">é¨é¨ï¼</span> |
| | | <el-tree-select |
| | | v-model="searchForm.sysDeptId" |
| | | :data="deptOptions" |
| | | check-strictly |
| | | :render-after-expand="false" |
| | | style="width: 240px" |
| | | placeholder="è¯·éæ©" |
| | | /> |
| | | <span class="search_title search_title2">å
¥èæ¥æï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.contractStartTime" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openFormNewOrEditFormDia('add')">æ°å¢å
¥è</el-button> |
| | | <el-button type="info" @click="handleImport">导å
¥</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | </div> |
| | | <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia> |
| | | <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia> |
| | | <renew-contract |
| | | v-if="isShowRenewContractModal" |
| | | v-model:visible="isShowRenewContractModal" |
| | | :id="id" |
| | | @completed="handleQuery" |
| | | /> |
| | | |
| | | <!-- 导å
¥å¯¹è¯æ¡ --> |
| | | <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body> |
| | | <el-upload |
| | | ref="uploadRef" |
| | | :limit="1" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | | :action="upload.url" |
| | | :disabled="upload.isUploading" |
| | | :on-progress="handleFileUploadProgress" |
| | | :on-success="handleFileSuccess" |
| | | :auto-upload="false" |
| | | drag |
| | | > |
| | | <el-icon class="el-icon--upload"><upload-filled /></el-icon> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | <template #tip> |
| | | <div class="el-upload__tip text-center"> |
| | | <span>ä»
å
许导å
¥xlsãxlsxæ ¼å¼æä»¶ã</span> |
| | | <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">ä¸è½½æ¨¡æ¿</el-link> |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button @click="upload.open = false">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search, UploadFilled } from "@element-plus/icons-vue"; |
| | | import {onMounted, ref} from "vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | import { deptTreeSelect } from "@/api/system/user.js"; |
| | | import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue")); |
| | | const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue")); |
| | | const RenewContract = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/RenewContract.vue")); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | staffName: "", |
| | | entryDate: undefined, // å½å
¥æ¥æ |
| | | entryDateStart: undefined, |
| | | entryDateEnd: undefined, |
| | | }, |
| | | deptOptions: [], |
| | | }); |
| | | const { searchForm, deptOptions } = toRefs(data); |
| | | const isShowRenewContractModal = ref(false); |
| | | const id = ref(0); |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | | return "å¨è"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | | return "primary"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | }, |
| | | { |
| | | label: "åå·¥ç¼å·", |
| | | prop: "staffNo", |
| | | }, |
| | | { |
| | | label: "å§å", |
| | | prop: "staffName", |
| | | }, |
| | | { |
| | | label: "å«å", |
| | | prop: "alias", |
| | | }, |
| | | { |
| | | label: "ææº", |
| | | prop: "phone", |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "æ§å«", |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "åºçæ¥æ", |
| | | prop: "birthDate", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å
¥èæ¥æ", |
| | | prop: "contractStartTime", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | | prop: "age", |
| | | }, |
| | | { |
| | | label: "ç±è´¯", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "æ°æ", |
| | | prop: "nation", |
| | | width: 100, |
| | | }, |
| | | { |
| | | label: "å©å§»ç¶åµ", |
| | | prop: "maritalStatus", |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | width: 180, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openFormNewOrEditFormDia("edit", row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç»ç¾åå", |
| | | type: "text", |
| | | showHide: row => row.staffState === 1, |
| | | clickFun: (row) => { |
| | | isShowRenewContractModal.value = true; |
| | | id.value = row.id; |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0 |
| | | }); |
| | | const formDia = ref() |
| | | const formDiaNewOrEditFormDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | // 导å
¥ç¸å
³ |
| | | const uploadRef = ref(null) |
| | | const upload = reactive({ |
| | | // æ¯å¦æ¾ç¤ºå¼¹åºå± |
| | | open: false, |
| | | // å¼¹åºå±æ é¢ |
| | | title: "", |
| | | // æ¯å¦ç¦ç¨ä¸ä¼ |
| | | isUploading: false, |
| | | // 设置ä¸ä¼ ç请æ±å¤´é¨ |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // ä¸ä¼ çå°å |
| | | url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import" |
| | | }) |
| | | |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data)) |
| | | ); |
| | | }); |
| | | }; |
| | | const filterDisabledDept = deptList => { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | }; |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | fetchDeptOptions(); |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | params.entryDate = undefined |
| | | staffOnJobListPage({...params}).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | }; |
| | | const openFormNewOrEditFormDia = (type, row) => { |
| | | nextTick(() => { |
| | | formDiaNewOrEditFormDia.value?.openDialog(type, row) |
| | | }) |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/staff/staffOnJob/export", {staffState: 1}, "åå·¥å°è´¦.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // 导å
¥æé®æä½ |
| | | const handleImport = () => { |
| | | upload.title = "å工导å
¥" |
| | | upload.open = true |
| | | } |
| | | |
| | | // ä¸è½½æ¨¡æ¿æä½ |
| | | const importTemplate = () => { |
| | | proxy.download("/staff/staffOnJob/downloadTemplate", {}, `å工导å
¥æ¨¡æ¿_${new Date().getTime()}.xlsx`) |
| | | } |
| | | |
| | | // æä»¶ä¸ä¼ ä¸å¤ç |
| | | const handleFileUploadProgress = (event, file, fileList) => { |
| | | upload.isUploading = true |
| | | } |
| | | |
| | | // æä»¶ä¸ä¼ æåå¤ç |
| | | const handleFileSuccess = (response, file, fileList) => { |
| | | upload.open = false |
| | | upload.isUploading = false |
| | | proxy.$refs["uploadRef"].handleRemove(file) |
| | | proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导å
¥ç»æ", { dangerouslyUseHTMLString: true }) |
| | | getList() |
| | | } |
| | | |
| | | // æäº¤ä¸ä¼ æä»¶ |
| | | const submitFileForm = () => { |
| | | proxy.$refs["uploadRef"].submit() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .search_title2 { |
| | | margin-left: 10px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | title="详æ
" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :tableLoading="tableLoading" |
| | | height="600" |
| | | ></PIMTable> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <Files ref="filesDia"></Files> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref} from "vue"; |
| | | import {findStaffContractListPage} from "@/api/personnelManagement/staffContract.js"; |
| | | const Files = defineAsyncComponent(() => import( "@/views/personnelManagement/contractManagement/filesDia.vue")); |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const filesDia = ref() |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "ååå¹´é", |
| | | prop: "contractTerm", |
| | | }, |
| | | { |
| | | label: "ååå¼å§æ¥æ", |
| | | prop: "contractStartTime", |
| | | }, |
| | | { |
| | | label: "ååç»ææ¥æ", |
| | | prop: "contractEndTime", |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | width: 120, |
| | | operation: [ |
| | | { |
| | | name: "ä¸ä¼ éä»¶", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | filesDia.value.openDialog( row,'åå') |
| | | }, |
| | | } |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'edit') { |
| | | findStaffContractListPage({staffOnJobId: row.id}).then(res => { |
| | | tableData.value = res.data.records |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const openUploadFile = (row) => { |
| | | filesDia.value.open = true |
| | | filesDia.value.row = row |
| | | } |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | title="ä¸ä¼ éä»¶" |
| | | width="50%" |
| | | @close="closeDia" |
| | | > |
| | | <div style="margin-bottom: 10px;text-align: right"> |
| | | <el-upload |
| | | v-model:file-list="fileList" |
| | | class="upload-demo" |
| | | :action="uploadUrl" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | name="file" |
| | | :show-file-list="false" |
| | | :headers="headers" |
| | | style="display: inline;margin-right: 10px" |
| | | > |
| | | <el-button type="primary">ä¸ä¼ éä»¶</el-button> |
| | | </el-upload> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :tableLoading="tableLoading" |
| | | :isSelection="true" |
| | | :page="page" |
| | | @selection-change="handleSelectionChange" |
| | | height="500" |
| | | @pagination="paginationSearch" |
| | | :total="page.total" |
| | | > |
| | | </PIMTable> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <filePreview ref="filePreviewRef" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref} from "vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | import {getToken} from "@/utils/auth.js"; |
| | | import filePreview from '@/components/filePreview/index.vue' |
| | | import { |
| | | fileAdd, |
| | | fileDel, |
| | | fileListPage |
| | | } from "@/api/financialManagement/revenueManagement.js"; |
| | | import Pagination from "@/components/PIMTable/Pagination.vue"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const currentId = ref('') |
| | | const selectedRows = ref([]); |
| | | const filePreviewRef = ref() |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "æä»¶åç§°", |
| | | prop: "name", |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | operation: [ |
| | | { |
| | | name: "ä¸è½½", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | downLoadFile(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "é¢è§", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | lookFile(row); |
| | | }, |
| | | } |
| | | ], |
| | | }, |
| | | ]); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }); |
| | | const total = ref(0); |
| | | const tableData = ref([]); |
| | | const fileList = ref([]); |
| | | const tableLoading = ref(false); |
| | | const accountType = ref('') |
| | | const headers = ref({ |
| | | Authorization: "Bearer " + getToken(), |
| | | }); |
| | | const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // ä¸ä¼ çå¾çæå¡å¨å°å |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (row,type) => { |
| | | accountType.value = type; |
| | | dialogFormVisible.value = true; |
| | | currentId.value = row.id; |
| | | getList() |
| | | } |
| | | const paginationSearch = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => { |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }) |
| | | } |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | }; |
| | | // ä¸ä¼ æåå¤ç |
| | | function handleUploadSuccess(res, file) { |
| | | // 妿ä¸ä¼ æå |
| | | if (res.code == 200) { |
| | | const fileRow = {} |
| | | fileRow.name = res.data.originalName |
| | | fileRow.url = res.data.tempPath |
| | | uploadFile(fileRow) |
| | | } else { |
| | | proxy.$modal.msgError("æä»¶ä¸ä¼ 失败"); |
| | | } |
| | | } |
| | | function uploadFile(file) { |
| | | file.accountId = currentId.value; |
| | | file.accountType = accountType.value; |
| | | fileAdd(file).then(res => { |
| | | proxy.$modal.msgSuccess("æä»¶ä¸ä¼ æå"); |
| | | getList() |
| | | }) |
| | | } |
| | | // ä¸ä¼ 失败å¤ç |
| | | function handleUploadError() { |
| | | proxy.$modal.msgError("æä»¶ä¸ä¼ 失败"); |
| | | } |
| | | // ä¸è½½éä»¶ |
| | | const downLoadFile = (row) => { |
| | | proxy.$download.byUrl(row.url, row.originalFilename); |
| | | } |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | fileDel(ids).then((res) => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }); |
| | | }).catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | // é¢è§éä»¶ |
| | | const lookFile = (row) => { |
| | | filePreviewRef.value.open(row.url) |
| | | } |
| | | |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼åå·¥åå--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å§åï¼</span> |
| | | <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="请è¾å
¥å§åæç´¢" @change="handleQuery" |
| | | clearable :prefix-icon="Search" /> |
| | | <span style="margin-left: 10px" class="search_title">ååç»ææ¥æï¼</span> |
| | | <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" |
| | | placeholder="è¯·éæ©" clearable @change="changeDaterange" /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" |
| | | @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination" |
| | | :total="page.total"></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | |
| | | <!-- åå导å
¥å¯¹è¯æ¡ --> |
| | | <el-dialog |
| | | :title="upload.title" |
| | | v-model="upload.open" |
| | | width="400px" |
| | | append-to-body |
| | | > |
| | | <el-upload |
| | | ref="uploadRef" |
| | | :limit="1" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | | :action="upload.url + '?updateSupport=' + upload.updateSupport" |
| | | :disabled="upload.isUploading" |
| | | :on-progress="handleFileUploadProgress" |
| | | :on-success="handleFileSuccess" |
| | | :auto-upload="false" |
| | | drag |
| | | > |
| | | <el-icon class="el-icon--upload"><upload-filled /></el-icon> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | <template #tip> |
| | | <div class="el-upload__tip text-center"> |
| | | <span>ä»
å
许导å
¥xlsãxlsxæ ¼å¼æä»¶ã</span> |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button @click="upload.open = false">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <files-dia ref="filesDia"></files-dia> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref } from "vue"; |
| | | import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js"; |
| | | import dayjs from "dayjs"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | import FilesDia from "./filesDia.vue"; |
| | | const data = reactive({ |
| | | searchForm: { |
| | | staffName: "", |
| | | entryDate: null, // å½å
¥æ¥æ |
| | | entryDateStart: undefined, |
| | | entryDateEnd: undefined, |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "staffState", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | if (params == 0) { |
| | | return "离è"; |
| | | } else if (params == 1) { |
| | | return "å¨è"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | if (params == 0) { |
| | | return "danger"; |
| | | } else if (params == 1) { |
| | | return "primary"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | }, |
| | | { |
| | | label: "åå·¥ç¼å·", |
| | | prop: "staffNo", |
| | | }, |
| | | { |
| | | label: "å§å", |
| | | prop: "staffName", |
| | | }, |
| | | { |
| | | label: "æ§å«", |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "å²ä½", |
| | | prop: "postName", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width: 200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | | prop: "firstStudy", |
| | | }, |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width: 100 |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | | prop: "age", |
| | | }, |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width: 150 |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width: 150 |
| | | }, |
| | | { |
| | | label: "ååç»ææ¥æ", |
| | | prop: "contractExpireTime", |
| | | width: 120 |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | width: 120, |
| | | operation: [ |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | } |
| | | ], |
| | | }, |
| | | ]); |
| | | const filesDia = ref() |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const formDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const changeDaterange = (value) => { |
| | | searchForm.value.entryDateStart = undefined; |
| | | searchForm.value.entryDateEnd = undefined; |
| | | if (value) { |
| | | searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD"); |
| | | searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD"); |
| | | } |
| | | getList(); |
| | | }; |
| | | // æå¼éä»¶å¼¹æ¡ |
| | | const openFilesFormDia = (row) => { |
| | | nextTick(() => { |
| | | filesDia.value?.openDialog( row,'åå') |
| | | }) |
| | | }; |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | params.entryDate = undefined |
| | | params.staffState = 1 |
| | | staffOnJobListPage(params).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | }; |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/staff/staffOnJob/export", {staffState: 1}, "åå管ç.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | const upload = reactive({ |
| | | // æ¯å¦æ¾ç¤ºå¼¹åºå±ï¼åå导å
¥ï¼ |
| | | open: false, |
| | | // å¼¹åºå±æ é¢ï¼åå导å
¥ï¼ |
| | | title: "", |
| | | // æ¯å¦ç¦ç¨ä¸ä¼ |
| | | isUploading: false, |
| | | // æ¯å¦æ´æ°å·²ç»åå¨çç¨æ·æ°æ® |
| | | updateSupport: 1, |
| | | // 设置ä¸ä¼ ç请æ±å¤´é¨ |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // ä¸ä¼ çå°å |
| | | url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import", |
| | | }); |
| | | /** 导å
¥æé®æä½ */ |
| | | function handleImport() { |
| | | upload.title = "åå导å
¥"; |
| | | upload.open = true; |
| | | } |
| | | /** æäº¤ä¸ä¼ æä»¶ */ |
| | | function submitFileForm() { |
| | | proxy.$refs["uploadRef"].submit(); |
| | | } |
| | | /**æä»¶ä¸ä¼ ä¸å¤ç */ |
| | | const handleFileUploadProgress = (event, file, fileList) => { |
| | | upload.isUploading = true; |
| | | }; |
| | | /** æä»¶ä¸ä¼ æåå¤ç */ |
| | | const handleFileSuccess = (response, file, fileList) => { |
| | | upload.open = false; |
| | | upload.isUploading = false; |
| | | proxy.$refs["uploadRef"].handleRemove(file); |
| | | getList(); |
| | | }; |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼è°å²ç³è¯·--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">审æ¹åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.instanceNo" |
| | | style="width: 220px" |
| | | placeholder="请è¾å
¥å®¡æ¹åå·" |
| | | clearable |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">ç³è¯·äººï¼</span> |
| | | <el-select |
| | | v-model="searchForm.applicantId" |
| | | filterable |
| | | remote |
| | | clearable |
| | | reserve-keyword |
| | | placeholder="è¯·éæ©ææç´¢ç³è¯·äºº" |
| | | style="width: 220px" |
| | | :remote-method="remoteSearchApplicant" |
| | | :loading="applicantSearchLoading" |
| | | > |
| | | <el-option |
| | | v-for="u in applicantSearchOptions" |
| | | :key="u.userId" |
| | | :label="userSelectLabel(u)" |
| | | :value="u.userId" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" style="margin-left: 10px" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openAddWithTemplate">æ°å¢è°å²ç³è¯·</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | @pagination="onPagination" |
| | | :total="page.total" |
| | | /> |
| | | </div> |
| | | |
| | | <ApprovalInstanceSubmitDialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialogTitle" |
| | | :form="submitForm" |
| | | :rules="submitFormRules" |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | | flow-attachments-only |
| | | @submit="onSubmit" |
| | | > |
| | | <template #before="{ form, fields }"> |
| | | <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" /> |
| | | <el-form-item label="åå²ä½"> |
| | | <el-input :model-value="originalPostName" placeholder="éæ©ç³è¯·äººåèªå¨å¸¦åº" disabled /> |
| | | </el-form-item> |
| | | </template> |
| | | </ApprovalInstanceSubmitDialog> |
| | | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.TRANSFER" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <ApprovalInstanceDetailDialog |
| | | v-model="detailDialog.visible" |
| | | title="è°å²ç³è¯·è¯¦æ
" |
| | | :row="detailRow" |
| | | @edit="openEditFromDetail" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { findPostOptions } from "@/api/system/post.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { computed, onMounted, reactive, ref, watch } from "vue"; |
| | | import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.vue"; |
| | | import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue"; |
| | | import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue"; |
| | | import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue"; |
| | | import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js"; |
| | | |
| | | function isOriginalPostField(field) { |
| | | const label = String(field?.label || ""); |
| | | return ( |
| | | label.includes("åå²ä½") || |
| | | field?.key === "originalPost" || |
| | | field?.key === "originalPostName" || |
| | | field?.key === "originalPostId" |
| | | ); |
| | | } |
| | | |
| | | function displayTemplateFields(fields = []) { |
| | | return (fields || []).filter((f) => !isOriginalPostField(f)); |
| | | } |
| | | |
| | | function findApplicantTemplateField(fields = []) { |
| | | return ( |
| | | fields.find((f) => String(f?.label || "").includes("ç³è¯·äºº")) || |
| | | fields.find((f) => f?.type === "select" && f?.optionSource === SELECT_OPTION_SOURCE.USER) || |
| | | null |
| | | ); |
| | | } |
| | | |
| | | const searchForm = reactive({ |
| | | instanceNo: "", |
| | | applicantId: "", |
| | | }); |
| | | |
| | | const mod = useApprovalInstanceModule({ |
| | | moduleKey: APPROVAL_MODULE_KEYS.TRANSFER, |
| | | }); |
| | | |
| | | const { |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | detailDialog, |
| | | detailRow, |
| | | submitDialog, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | isSubmitEdit, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitDialogTitle, |
| | | templateBindVisible, |
| | | handleQuery, |
| | | initModuleList, |
| | | pagination, |
| | | openAddWithTemplate, |
| | | onTemplateBound, |
| | | onTemplateBindClosed, |
| | | openEditFromDetail, |
| | | submitInstanceForm, |
| | | buildTableActions, |
| | | } = mod; |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | const allUsersCache = ref([]); |
| | | const postIdToName = ref({}); |
| | | const targetPostOptions = ref([]); |
| | | const applicantSearchLoading = ref(false); |
| | | const applicantSearchOptions = ref([]); |
| | | const originalPostName = ref(""); |
| | | |
| | | const applicantTemplateField = computed(() => |
| | | findApplicantTemplateField(submitForm.formFieldDefs) |
| | | ); |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload && Array.isArray(payload.data)) return payload.data; |
| | | if (payload && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | function userSelectLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const name = u.userName || ""; |
| | | if (nick && name && nick !== name) return `${nick}ï¼${name}ï¼`; |
| | | return nick || name || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function firstPostId(user) { |
| | | if (!user) return undefined; |
| | | if (Array.isArray(user.postIds) && user.postIds.length) return user.postIds[0]; |
| | | if (user.postId != null && user.postId !== "") return user.postId; |
| | | return undefined; |
| | | } |
| | | |
| | | function resolveOriginalPost(user) { |
| | | if (!user) return { originalPostName: "" }; |
| | | const nameStr = (user.postName ?? user.postname ?? "").toString().trim(); |
| | | if (nameStr) return { originalPostName: nameStr }; |
| | | if (Array.isArray(user.posts) && user.posts.length) { |
| | | return { originalPostName: (user.posts[0].postName ?? "").toString() || "æªå½åå²ä½" }; |
| | | } |
| | | const pid = firstPostId(user); |
| | | if (pid != null && pid !== "") { |
| | | const n = postIdToName.value[String(pid)] || ""; |
| | | return { originalPostName: n || "å½åå²ä½ï¼æªå¨å²ä½åå
¸ä¸ï¼" }; |
| | | } |
| | | return { originalPostName: "æªåé
å²ä½" }; |
| | | } |
| | | |
| | | function userById(id) { |
| | | if (id == null || id === "") return undefined; |
| | | return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id)); |
| | | } |
| | | |
| | | function filterUsersByQuery(query) { |
| | | const list = allUsersCache.value.filter((u) => isActiveUser(u)); |
| | | const q = (query || "").trim().toLowerCase(); |
| | | if (!q) return [...list]; |
| | | return list.filter((u) => { |
| | | const nick = (u.nickName || "").toLowerCase(); |
| | | const uname = (u.userName || "").toLowerCase(); |
| | | const phone = (u.phonenumber || u.phone || "").toString(); |
| | | return nick.includes(q) || uname.includes(q) || phone.includes(q); |
| | | }); |
| | | } |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | allUsersCache.value = unwrapArray(res); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | async function loadPostOptions() { |
| | | try { |
| | | const res = await findPostOptions(); |
| | | const rows = res.data ?? res.rows ?? []; |
| | | targetPostOptions.value = Array.isArray(rows) ? rows : []; |
| | | } catch { |
| | | targetPostOptions.value = []; |
| | | } |
| | | const m = {}; |
| | | for (const p of targetPostOptions.value) { |
| | | const id = p.postId ?? p.value ?? p.id; |
| | | if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? ""; |
| | | } |
| | | postIdToName.value = m; |
| | | } |
| | | |
| | | async function remoteSearchApplicant(query) { |
| | | applicantSearchLoading.value = true; |
| | | try { |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | applicantSearchOptions.value = filterUsersByQuery(query); |
| | | } finally { |
| | | applicantSearchLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function syncOriginalPostFromApplicant(uid) { |
| | | const u = userById(uid); |
| | | originalPostName.value = resolveOriginalPost(u).originalPostName; |
| | | } |
| | | |
| | | watch( |
| | | () => { |
| | | const key = applicantTemplateField.value?.key; |
| | | return key ? submitForm.formPayload[key] : undefined; |
| | | }, |
| | | async (uid) => { |
| | | if (!applicantTemplateField.value) return; |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | syncOriginalPostFromApplicant(uid); |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => submitDialog.visible, |
| | | async (v) => { |
| | | if (!v) return; |
| | | const key = applicantTemplateField.value?.key; |
| | | if (key && submitForm.formPayload[key]) { |
| | | syncOriginalPostFromApplicant(submitForm.formPayload[key]); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, { |
| | | moduleKey: APPROVAL_MODULE_KEYS.TRANSFER, |
| | | }); |
| | | |
| | | function onSearch() { |
| | | handleQuery(searchForm); |
| | | } |
| | | |
| | | async function resetSearch() { |
| | | searchForm.instanceNo = ""; |
| | | searchForm.applicantId = ""; |
| | | onSearch(); |
| | | await remoteSearchApplicant(""); |
| | | } |
| | | |
| | | function onPagination(obj) { |
| | | pagination(obj, searchForm); |
| | | } |
| | | |
| | | async function onSubmit() { |
| | | const ok = await submitInstanceForm({ skipValidate: true }); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "æäº¤æå"); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await Promise.all([loadUserPool(), loadPostOptions()]); |
| | | loadFlowUsers(); |
| | | await remoteSearchApplicant(""); |
| | | await initModuleList(searchForm); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å·¥ä½äº¤æ¥--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">审æ¹åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.instanceNo" |
| | | style="width: 220px" |
| | | placeholder="请è¾å
¥å®¡æ¹åå·" |
| | | clearable |
| | | @keyup.enter="onSearch" |
| | | /> |
| | | <span class="search_title" style="margin-left: 12px">ç³è¯·äººï¼</span> |
| | | <el-select |
| | | v-model="searchForm.applicantId" |
| | | filterable |
| | | remote |
| | | clearable |
| | | reserve-keyword |
| | | placeholder="è¯·éæ©ææç´¢ç³è¯·äºº" |
| | | style="width: 220px" |
| | | :remote-method="remoteSearchApplicant" |
| | | :loading="applicantSearchLoading" |
| | | > |
| | | <el-option |
| | | v-for="u in applicantSearchOptions" |
| | | :key="u.userId" |
| | | :label="userSelectLabel(u)" |
| | | :value="u.userId" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" style="margin-left: 10px" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openAddWithTemplate">æ°å¢å·¥ä½äº¤æ¥</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | @pagination="onPagination" |
| | | :total="page.total" |
| | | /> |
| | | </div> |
| | | |
| | | <ApprovalInstanceSubmitDialog |
| | | v-model="submitDialog.visible" |
| | | :title="submitDialogTitle" |
| | | :form="submitForm" |
| | | :rules="submitFormRules" |
| | | :fields="submitFormFields" |
| | | :active-template="activeTemplate" |
| | | :user-options="flowUserOptions" |
| | | :is-edit="isSubmitEdit" |
| | | :saving="submitSaving" |
| | | :form-ref="submitFormRef" |
| | | @submit="onSubmit" |
| | | /> |
| | | |
| | | <ApprovalTemplateBindDialog |
| | | v-model:visible="templateBindVisible" |
| | | :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER" |
| | | skip-form-confirm |
| | | @confirm="onTemplateBound" |
| | | @closed="onTemplateBindClosed" |
| | | /> |
| | | |
| | | <ApprovalInstanceDetailDialog |
| | | v-model="detailDialog.visible" |
| | | title="å·¥ä½äº¤æ¥è¯¦æ
" |
| | | :row="detailRow" |
| | | @edit="openEditFromDetail" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { onMounted, reactive, ref } from "vue"; |
| | | import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue"; |
| | | import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue"; |
| | | import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue"; |
| | | import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js"; |
| | | import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js"; |
| | | |
| | | const handoverStatusOptions = [ |
| | | { value: "in_progress", label: "è¿è¡ä¸" }, |
| | | { value: "completed", label: "已宿" }, |
| | | { value: "returned", label: "å·²éå" }, |
| | | ]; |
| | | |
| | | const handoverTypeOptions = [ |
| | | { value: "resignation", label: "离è交æ¥" }, |
| | | { value: "transfer", label: "è°å²äº¤æ¥" }, |
| | | ]; |
| | | |
| | | const searchForm = reactive({ |
| | | instanceNo: "", |
| | | applicantId: "", |
| | | }); |
| | | |
| | | const mod = useApprovalInstanceModule({ |
| | | moduleKey: APPROVAL_MODULE_KEYS.WORK_HANDOVER, |
| | | }); |
| | | |
| | | const { |
| | | tableData, |
| | | tableLoading, |
| | | page, |
| | | detailDialog, |
| | | detailRow, |
| | | submitDialog, |
| | | submitForm, |
| | | submitFormRef, |
| | | submitSaving, |
| | | isSubmitEdit, |
| | | activeTemplate, |
| | | submitFormFields, |
| | | submitFormRules, |
| | | submitDialogTitle, |
| | | templateBindVisible, |
| | | handleQuery, |
| | | initModuleList, |
| | | pagination, |
| | | openAddWithTemplate, |
| | | onTemplateBound, |
| | | onTemplateBindClosed, |
| | | openEditFromDetail, |
| | | submitInstanceForm, |
| | | buildTableActions, |
| | | } = mod; |
| | | |
| | | const { flowUserOptions, loadFlowUsers } = useFlowUserOptions(); |
| | | |
| | | const allUsersCache = ref([]); |
| | | const applicantSearchOptions = ref([]); |
| | | const applicantSearchLoading = ref(false); |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload && Array.isArray(payload.data)) return payload.data; |
| | | if (payload && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | function userSelectLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const name = u.userName || ""; |
| | | if (nick && name && nick !== name) return `${nick}ï¼${name}ï¼`; |
| | | return nick || name || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function filterUsersByQuery(query) { |
| | | const list = allUsersCache.value.filter((u) => isActiveUser(u)); |
| | | const q = (query || "").trim().toLowerCase(); |
| | | if (!q) return list.slice(0, 50); |
| | | return list |
| | | .filter((u) => { |
| | | const nick = (u.nickName || "").toLowerCase(); |
| | | const name = (u.userName || "").toLowerCase(); |
| | | const id = String(u.userId ?? u.id ?? ""); |
| | | return nick.includes(q) || name.includes(q) || id.includes(q); |
| | | }) |
| | | .slice(0, 50); |
| | | } |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | allUsersCache.value = unwrapArray(res); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | async function remoteSearchApplicant(query) { |
| | | applicantSearchLoading.value = true; |
| | | try { |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | applicantSearchOptions.value = filterUsersByQuery(query); |
| | | } finally { |
| | | applicantSearchLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, { |
| | | moduleKey: APPROVAL_MODULE_KEYS.WORK_HANDOVER, |
| | | }); |
| | | |
| | | function onSearch() { |
| | | handleQuery(searchForm); |
| | | } |
| | | |
| | | async function resetSearch() { |
| | | searchForm.instanceNo = ""; |
| | | searchForm.applicantId = ""; |
| | | onSearch(); |
| | | await remoteSearchApplicant(""); |
| | | } |
| | | |
| | | function onPagination(obj) { |
| | | pagination(obj, searchForm); |
| | | } |
| | | |
| | | async function onSubmit() { |
| | | const ok = await submitInstanceForm({ skipValidate: true }); |
| | | if (ok) ElMessage.success(isSubmitEdit.value ? "ä¿®æ¹æå" : "æäº¤æå"); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadUserPool(); |
| | | loadFlowUsers(); |
| | | await remoteSearchApplicant(""); |
| | | await initModuleList(searchForm); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 模å䏿åï¼éç¥å
Œ |
| | | ç®å½æ è¯ï¼NoticeAnnouncement/notice-manage |
| | | å¤ç¨é¡µé¢ï¼@/views/collaborativeApproval/noticeManagement/index.vueï¼åå审æ¹-éç¥å
¬åï¼ |
| | | --> |
| | | <template> |
| | | <NoticeManagement /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import NoticeManagement from "@/views/collaborativeApproval/noticeManagement/index.vue"; |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- è´¹ç¨æ¥éï¼è¯¦æ
åªè¯»é¢æ¿ --> |
| | | <template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="æ¥éåå·">{{ row.reimburseNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥éç¶æ"> |
| | | <el-tag :type="statusTagType(row.approvalResult)" size="small">{{ statusLabel(row.approvalResult) }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="è´¹ç¨ç±»å">{{ expenseCategoryLabel(row.expenseCategory) }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç³è¯·æ¶é´">{{ row.applyTime || row.createTime || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå·¥ç¼å·">{{ row.employeeNo || row.applicantNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå·¥å§å">{{ row.employeeName || row.applicantName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥éåå " :span="2">{{ row.reimburseReason || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ééé¢">{{ row.applyAmount != null ? `${row.applyAmount} å
` : "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¶æ¬¾äºº">{{ row.payee || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¶æ¬¾è´¦å·">{{ row.payeeAccount || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="弿·æ¯è¡">{{ row.bankBranch || "â" }}</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> |
| | | |
| | | <el-divider content-position="left">æ¥éæç»</el-divider> |
| | | <el-table v-if="row.expenseDetails?.length" :data="row.expenseDetails" border size="small"> |
| | | <el-table-column type="index" label="åºå·" width="55" align="center" /> |
| | | <el-table-column prop="invoiceDate" label="åç¥¨æ¥æ" width="120" /> |
| | | <el-table-column label="è´¹ç¨ç§ç®" width="100"> |
| | | <template #default="{ row: d }">{{ expenseSubjectLabel(d.expenseSubject) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="amount" label="éé¢" width="100" /> |
| | | <el-table-column prop="description" label="æè¿°" min-width="140" show-overflow-tooltip /> |
| | | </el-table> |
| | | <el-empty v-else description="ææ æç»" :image-size="48" /> |
| | | |
| | | <el-divider content-position="left">å票éä»¶</el-divider> |
| | | <template v-if="attachmentFiles.length"> |
| | | <el-tag v-for="(f, i) in attachmentFiles" :key="i" class="file-tag" type="info" @click="openFile(f)"> |
| | | {{ f.name }} |
| | | </el-tag> |
| | | </template> |
| | | <el-empty v-else description="ææ éä»¶" :image-size="48" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { expenseCategoryLabel, expenseSubjectLabel, statusLabel, statusTagType } from "../costReimburseUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const attachmentFiles = computed(() => { |
| | | const list = |
| | | props.row?.attachmentList || |
| | | props.row?.storageBlobVOList || |
| | | props.row?.invoiceAttachments; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | | function openFile(f) { |
| | | const url = f?.url || f?.downloadURL || f?.previewURL; |
| | | if (url) window.open(url, "_blank"); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .reject-text { |
| | | color: var(--el-color-danger); |
| | | } |
| | | .file-tag { |
| | | margin: 0 8px 8px 0; |
| | | cursor: pointer; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | /** è´¹ç¨æ¥é大类 */ |
| | | export const EXPENSE_CATEGORY_OPTIONS = [ |
| | | { label: "å·®æ
", value: "travel" }, |
| | | { label: "åå
¬éè´", value: "office_procurement" }, |
| | | { label: "ä¸å¡æå¾
", value: "business_entertainment" }, |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "é讯费", value: "communication" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | /** æç»è´¹ç¨ç§ç® */ |
| | | export const EXPENSE_SUBJECT_OPTIONS = [ |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "ä½å®¿è´¹", value: "hotel" }, |
| | | { label: "é¤é¥®è´¹", value: "meal" }, |
| | | { label: "åå
¬ç¨å", value: "office_supply" }, |
| | | { label: "æå¾
è´¹", value: "entertainment" }, |
| | | { label: "é讯费", value: "phone" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | /** åç±»å¡«æ¥æ¨¡æ¿ï¼ä¸é®è°ç¨ï¼ */ |
| | | export const CATEGORY_TEMPLATES = { |
| | | travel: { |
| | | label: "å·®æ
è´¹ç¨", |
| | | reason: "å å
¬åºå·®äº§çç交éãä½å®¿ãé¤é¥®çè´¹ç¨æ¥éã", |
| | | details: [ |
| | | { expenseSubject: "transport", description: "å¾è¿äº¤éè´¹" }, |
| | | { expenseSubject: "hotel", description: "ä½å®¿è´¹" }, |
| | | { expenseSubject: "meal", description: "åºå·®é¤é¥®" }, |
| | | ], |
| | | }, |
| | | office_procurement: { |
| | | label: "åå
¬éè´", |
| | | reason: "é¨é¨æ¥å¸¸åå
¬ç¨åãèæéè´æ¥éã", |
| | | details: [ |
| | | { expenseSubject: "office_supply", description: "åå
¬ç¨åéè´" }, |
| | | { expenseSubject: "office_supply", description: "æå°èæ" }, |
| | | ], |
| | | }, |
| | | business_entertainment: { |
| | | label: "ä¸å¡æå¾
", |
| | | reason: "å®¢æ·æ¥å¾
ãåå¡å®´è¯·çè´¹ç¨æ¥éã", |
| | | details: [ |
| | | { expenseSubject: "entertainment", description: "å®¢æ·æ¥å¾
é¤è´¹" }, |
| | | { expenseSubject: "entertainment", description: "åå¡ç¤¼å" }, |
| | | ], |
| | | }, |
| | | transport: { |
| | | label: "交éè´¹", |
| | | reason: "å¸å
éå¤ãæè½¦ãå车ç交éè´¹ç¨æ¥éã", |
| | | details: [{ expenseSubject: "transport", description: "å¸å
交é" }], |
| | | }, |
| | | communication: { |
| | | label: "é讯费", |
| | | reason: "å å
¬éè®¯ãæµéãè¯è´¹è¡¥è´´æ¥éã", |
| | | details: [{ expenseSubject: "phone", description: "è¯è´¹/æµé" }], |
| | | }, |
| | | other: { |
| | | label: "å
¶ä»è´¹ç¨", |
| | | reason: "å
¶ä»å å
¬æ¯åºè´¹ç¨æ¥éã", |
| | | details: [{ expenseSubject: "other", description: "å
¶ä»è´¹ç¨" }], |
| | | }, |
| | | }; |
| | | |
| | | /** 审æ¹è§è²å±ç¤ºåï¼èç¹å®¡æ¹äººé¡»å¨åç«¯éæ©ï¼ */ |
| | | export const APPROVAL_ROLE_LABELS = { |
| | | direct_supervisor: "ç´å±ä¸çº§", |
| | | dept_manager: "é¨é¨ç»ç", |
| | | cfo: "è´¢å¡æ»ç", |
| | | compliance: "åè§å®¡æ ¸", |
| | | }; |
| | | |
| | | /** æéé¢é¢è®¾å®¡æ¹é¾ */ |
| | | export const APPROVAL_AMOUNT_RULES = [ |
| | | { |
| | | maxAmount: 500, |
| | | description: "500å
以å
ï¼ç´å±ä¸çº§å®¡æ¹", |
| | | roles: ["direct_supervisor"], |
| | | }, |
| | | { |
| | | maxAmount: 5000, |
| | | description: "500ï½5000å
ï¼ç´å±ä¸çº§ + é¨é¨ç»ç", |
| | | roles: ["direct_supervisor", "dept_manager"], |
| | | }, |
| | | { |
| | | maxAmount: Infinity, |
| | | description: "è¶
5000å
ï¼ç´å±ä¸çº§ + é¨é¨ç»ç + è´¢å¡æ»ç夿 ¸", |
| | | roles: ["direct_supervisor", "dept_manager", "cfo"], |
| | | }, |
| | | ]; |
| | | |
| | | /** é¨ååç±»é¢å¤å®¡æ¹èç¹ */ |
| | | export const CATEGORY_EXTRA_APPROVAL = { |
| | | business_entertainment: ["compliance"], |
| | | office_procurement: [], |
| | | }; |
| | | |
| | | export function expenseCategoryLabel(v) { |
| | | return EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === v)?.label || "â"; |
| | | } |
| | | |
| | | export function expenseSubjectLabel(v) { |
| | | return EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === v)?.label || "â"; |
| | | } |
| | | |
| | | export function statusLabel(v) { |
| | | if (v === "draft") return "è稿"; |
| | | if (v === "approved") return "å·²éè¿"; |
| | | if (v === "paid") return "已仿¬¾"; |
| | | if (v === "rejected") return "已驳å"; |
| | | if (v === "cancelled") return "å·²æ¤å"; |
| | | return "å®¡æ ¸ä¸"; |
| | | } |
| | | |
| | | export function statusTagType(v) { |
| | | if (v === "draft") return "info"; |
| | | if (v === "approved" || v === "paid") return "success"; |
| | | if (v === "rejected") return "danger"; |
| | | if (v === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | | export { formatApprovalFlowSummary } from "../shared/finReimbursementMappers.js"; |
| | | |
| | | export function resolveApprovalRoles(amount, expenseCategory) { |
| | | const amt = Number(amount) || 0; |
| | | let roles = []; |
| | | for (const rule of APPROVAL_AMOUNT_RULES) { |
| | | if (amt <= rule.maxAmount) { |
| | | roles = [...rule.roles]; |
| | | break; |
| | | } |
| | | } |
| | | if (!roles.length) roles = ["direct_supervisor"]; |
| | | const extra = CATEGORY_EXTRA_APPROVAL[expenseCategory] || []; |
| | | extra.forEach((r) => { |
| | | if (!roles.includes(r)) roles.push(r); |
| | | }); |
| | | return roles; |
| | | } |
| | | |
| | | export function buildAutoApprovalFlow(amount, expenseCategory, previousNodes = []) { |
| | | const roles = resolveApprovalRoles(amount, expenseCategory); |
| | | const prevByRole = new Map(); |
| | | (previousNodes || []).forEach((n, idx) => { |
| | | if (n?.roleKey) prevByRole.set(n.roleKey, n); |
| | | else if (n?.approverId != null && n.approverId !== "") { |
| | | prevByRole.set(`__idx_${idx}`, n); |
| | | } |
| | | }); |
| | | return roles.map((role, i) => { |
| | | const prev = prevByRole.get(role) || prevByRole.get(`__idx_${i}`); |
| | | const hasApprover = prev?.approverId != null && prev.approverId !== ""; |
| | | return { |
| | | approverId: hasApprover ? prev.approverId : null, |
| | | approverName: hasApprover |
| | | ? prev.approverName || "" |
| | | : APPROVAL_ROLE_LABELS[role] || role, |
| | | roleKey: role, |
| | | signMode: prev?.signMode || "countersign", |
| | | sortOrder: i + 1, |
| | | nodeOrder: i + 1, |
| | | nodeStatus: i === 0 ? "process" : "wait", |
| | | approveOpinion: "", |
| | | approveTime: "", |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | export function getApprovalRuleHint(amount, expenseCategory) { |
| | | const amt = Number(amount) || 0; |
| | | const rule = APPROVAL_AMOUNT_RULES.find((r) => amt <= r.maxAmount) || APPROVAL_AMOUNT_RULES[APPROVAL_AMOUNT_RULES.length - 1]; |
| | | const extra = CATEGORY_EXTRA_APPROVAL[expenseCategory] || []; |
| | | const extraText = extra.length |
| | | ? `ï¼${expenseCategoryLabel(expenseCategory)}ç±»å¦éï¼${extra.map((r) => APPROVAL_ROLE_LABELS[r] || r).join("ã")}` |
| | | : ""; |
| | | return `${rule.description}${extraText}`; |
| | | } |
| | | |
| | | export function createEmptyExpenseDetail() { |
| | | return { |
| | | id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: undefined, |
| | | description: "", |
| | | }; |
| | | } |
| | | |
| | | export function createEmptyForm() { |
| | | return { |
| | | id: undefined, |
| | | reimburseNo: "", |
| | | applicantId: "", |
| | | employeeNo: "", |
| | | employeeName: "", |
| | | expenseCategory: "", |
| | | reimburseReason: "", |
| | | applyAmount: undefined, |
| | | payee: "", |
| | | payeeAccount: "", |
| | | bankBranch: "", |
| | | expenseDetails: [], |
| | | attachmentList: [], |
| | | approvalFlowNodes: [], |
| | | currentNodeIndex: 0, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | deptId: "", |
| | | deptName: "", |
| | | }; |
| | | } |
| | | |
| | | export function applyCategoryTemplate(form, category) { |
| | | const tpl = CATEGORY_TEMPLATES[category]; |
| | | if (!tpl) return; |
| | | form.expenseCategory = category; |
| | | if (!form.reimburseReason?.trim()) form.reimburseReason = tpl.reason; |
| | | form.expenseDetails = (tpl.details || []).map((d) => ({ |
| | | ...createEmptyExpenseDetail(), |
| | | expenseSubject: d.expenseSubject, |
| | | description: d.description, |
| | | invoiceDate: dayjs().format("YYYY-MM-DD"), |
| | | })); |
| | | } |
| | | |
| | | export function initApprovalFlowNodes(nodes) { |
| | | return (nodes || []).map((n, i) => ({ |
| | | ...n, |
| | | sortOrder: i + 1, |
| | | nodeOrder: i + 1, |
| | | nodeStatus: i === 0 ? "process" : "wait", |
| | | approveOpinion: n.approveOpinion || "", |
| | | approveTime: n.approveTime || "", |
| | | })); |
| | | } |
| | | |
| | | export function advanceApprovalFlow(row, opinion) { |
| | | const nodes = [...(row.approvalFlowNodes || [])]; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | if (!nodes.length) return { nodes, currentNodeIndex: idx, approvalResult: row.approvalResult }; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | nodes[idx] = { |
| | | ...nodes[idx], |
| | | nodeStatus: "finish", |
| | | approveOpinion: opinion || "åæ", |
| | | approveTime: now, |
| | | }; |
| | | const next = idx + 1; |
| | | if (next >= nodes.length) { |
| | | return { nodes, currentNodeIndex: idx, approvalResult: "approved", rejectReason: "" }; |
| | | } |
| | | nodes[next] = { ...nodes[next], nodeStatus: "process" }; |
| | | return { nodes, currentNodeIndex: next, approvalResult: "pending", rejectReason: "" }; |
| | | } |
| | | |
| | | export function rejectApprovalFlow(row, opinion) { |
| | | const nodes = [...(row.approvalFlowNodes || [])]; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | const reason = (opinion || "").trim() || "驳å"; |
| | | if (nodes[idx]) { |
| | | nodes[idx] = { |
| | | ...nodes[idx], |
| | | nodeStatus: "error", |
| | | approveOpinion: reason, |
| | | approveTime: now, |
| | | }; |
| | | } |
| | | return { nodes, currentNodeIndex: idx, approvalResult: "rejected", rejectReason: reason }; |
| | | } |
| | | |
| | | export function normalizeImportedRow(raw, idx) { |
| | | const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`; |
| | | const expenseDetails = Array.isArray(raw.expenseDetails) ? raw.expenseDetails : []; |
| | | const applyAmount = raw.applyAmount ?? expenseDetails.reduce((s, d) => s + (Number(d.amount) || 0), 0); |
| | | const expenseCategory = raw.expenseCategory || "other"; |
| | | const approvalFlowNodes = |
| | | Array.isArray(raw.approvalFlowNodes) && raw.approvalFlowNodes.length |
| | | ? raw.approvalFlowNodes |
| | | : buildAutoApprovalFlow(applyAmount, expenseCategory); |
| | | |
| | | return { |
| | | id, |
| | | reimburseNo: raw.reimburseNo || `CR${dayjs().format("YYYYMMDD")}${String(idx).padStart(4, "0")}`, |
| | | applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`, |
| | | employeeNo: raw.employeeNo ?? raw.applicantNo ?? "", |
| | | employeeName: raw.employeeName ?? raw.applicantName ?? "æªç¥", |
| | | applicantNo: raw.employeeNo ?? raw.applicantNo ?? "", |
| | | applicantName: raw.employeeName ?? raw.applicantName ?? "æªç¥", |
| | | expenseCategory, |
| | | reimburseReason: raw.reimburseReason ?? "", |
| | | applyAmount, |
| | | payee: raw.payee ?? "", |
| | | payeeAccount: raw.payeeAccount ?? "", |
| | | bankBranch: raw.bankBranch ?? "", |
| | | expenseDetails, |
| | | attachmentList: Array.isArray(raw.attachmentList) ? raw.attachmentList : [], |
| | | invoiceAttachments: Array.isArray(raw.invoiceAttachments) ? raw.invoiceAttachments : [], |
| | | approvalFlowNodes, |
| | | currentNodeIndex: raw.currentNodeIndex ?? 0, |
| | | approvalResult: ["pending", "approved", "rejected"].includes(raw.approvalResult) ? raw.approvalResult : "pending", |
| | | rejectReason: raw.rejectReason ?? "", |
| | | approvalRecords: Array.isArray(raw.approvalRecords) ? raw.approvalRecords : [], |
| | | applyTime: raw.applyTime || raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | deptId: raw.deptId ?? "", |
| | | deptName: raw.deptName ?? "", |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼è´¹ç¨æ¥éï¼å表 /finReimbursement/listPageï¼reimbursementType=2ï¼--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div class="search_fields"> |
| | | <span class="search_title">ç³è¯·äººï¼</span> |
| | | <el-input |
| | | v-model="searchForm.applicantKeyword" |
| | | style="width: 220px" |
| | | placeholder="å§åæç¼å·" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <el-button type="primary" style="margin-left: 10px" @click="handleQuery">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="success" plain @click="handleImportClick">导å
¥</el-button> |
| | | <el-button type="warning" plain @click="handleExport">导åº</el-button> |
| | | <el-button type="primary" @click="openFormDialog('add')">æ°å¢è´¹ç¨æ¥é</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" /> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | :total="page.total" |
| | | @pagination="pagination" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- æ°å¢ / ç¼è¾ --> |
| | | <el-dialog |
| | | v-model="formDialog.visible" |
| | | :title="formDialog.title" |
| | | width="1120px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="cost-reimburse-form-dialog" |
| | | @closed="onFormClosed" |
| | | > |
| | | <el-alert type="info" show-icon :closable="false" class="mb16"> |
| | | <template #title>å
¨åç±»è´¹ç¨æ¥é · å类模æ¿ä¸é®å¡«æ¥</template> |
| | | <template #default> |
| | | æ¯æå·®æ
ãåå
¬éè´ãä¸å¡æå¾
ã交éè´¹ãé讯费çï¼æéé¢èªå¨å¹é
审æ¹é¾ï¼500å
å
ç´å±ä¸çº§ï¼è¶
5000å
è´¢å¡æ»ç夿 ¸ï¼ã |
| | | </template> |
| | | </el-alert> |
| | | |
| | | <div v-if="!formDialog.readonly" class="template-bar mb16"> |
| | | <span class="template-label">å类模æ¿ï¼</span> |
| | | <el-button |
| | | v-for="(tpl, key) in CATEGORY_TEMPLATES" |
| | | :key="key" |
| | | size="small" |
| | | :type="form.expenseCategory === key ? 'primary' : 'default'" |
| | | plain |
| | | @click="applyTemplate(key)" |
| | | > |
| | | {{ tpl.label }} |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="formRules" |
| | | label-width="120px" |
| | | class="cost-reimburse-form" |
| | | :disabled="formDialog.readonly" |
| | | > |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">åºæ¬ä¿¡æ¯</span></template> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥ç¼å·"> |
| | | <el-input v-model="form.employeeNo" readonly placeholder="éæ©åå·¥åèªå¨å¸¦åº" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥å§å" prop="applicantId"> |
| | | <el-select |
| | | v-model="form.applicantId" |
| | | filterable |
| | | remote |
| | | clearable |
| | | reserve-keyword |
| | | placeholder="è¯·éæ©ææç´¢åå·¥" |
| | | style="width: 100%" |
| | | :remote-method="remoteSearchApplicantForm" |
| | | :loading="applicantFormSearchLoading" |
| | | @change="onApplicantChange" |
| | | > |
| | | <el-option |
| | | v-for="u in applicantFormOptions" |
| | | :key="u.userId" |
| | | :label="userSelectLabel(u)" |
| | | :value="u.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è´¹ç¨ç±»å" prop="expenseCategory"> |
| | | <el-select |
| | | v-model="form.expenseCategory" |
| | | placeholder="è¯·éæ©è´¹ç¨ç±»å" |
| | | style="width: 100%" |
| | | @change="onExpenseCategoryChange" |
| | | > |
| | | <el-option |
| | | v-for="opt in EXPENSE_CATEGORY_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¥éç¶æ"> |
| | | <el-tag |
| | | :type="form.approvalResult === 'approved' ? 'success' : form.approvalResult === 'rejected' ? 'danger' : 'warning'" |
| | | effect="plain" |
| | | > |
| | | {{ |
| | | form.approvalResult === "approved" |
| | | ? "å·²éè¿" |
| | | : form.approvalResult === "rejected" |
| | | ? "已驳å" |
| | | : "å®¡æ ¸ä¸" |
| | | }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="æ¥éåå " prop="reimburseReason"> |
| | | <el-input |
| | | v-model="form.reimburseReason" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="è¯·å¡«åæ¥éåå " |
| | | maxlength="2000" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¥ééé¢" prop="applyAmount"> |
| | | <div class="amount-row"> |
| | | <el-input-number |
| | | v-model="form.applyAmount" |
| | | :min="0" |
| | | :precision="2" |
| | | controls-position="right" |
| | | class="amount-input" |
| | | @change="autoAssignApprovalFlow" |
| | | /> |
| | | <el-button v-if="!formDialog.readonly" type="primary" link @click="syncApplyAmountFromDetails"> |
| | | ææç»æ±æ» {{ detailTotalAmount }} å
|
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header-row"> |
| | | <span class="card-header-title">æ¥éæç»</span> |
| | | <el-button v-if="!formDialog.readonly" type="primary" plain size="small" @click="addExpenseDetail"> |
| | | æ°å¢æç» |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="form.expenseDetails" border size="small" class="detail-table"> |
| | | <el-table-column type="index" label="åºå·" width="55" align="center" /> |
| | | <el-table-column label="åç¥¨æ¥æ" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-if="!formDialog.readonly" |
| | | v-model="row.invoiceDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | size="small" |
| | | style="width: 100%" |
| | | /> |
| | | <span v-else>{{ row.invoiceDate || "â" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è´¹ç¨ç§ç®" width="130"> |
| | | <template #default="{ row }"> |
| | | <el-select v-if="!formDialog.readonly" v-model="row.expenseSubject" size="small" style="width: 100%"> |
| | | <el-option |
| | | v-for="opt in EXPENSE_SUBJECT_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | <span v-else>{{ expenseSubjectLabel(row.expenseSubject) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="éé¢" width="120"> |
| | | <template #default="{ row }"> |
| | | <el-input-number |
| | | v-if="!formDialog.readonly" |
| | | v-model="row.amount" |
| | | :min="0" |
| | | :precision="2" |
| | | size="small" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | @change="onDetailAmountChange" |
| | | /> |
| | | <span v-else>{{ row.amount ?? "â" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æè¿°" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input v-if="!formDialog.readonly" v-model="row.description" size="small" placeholder="说æ" /> |
| | | <span v-else>{{ row.description || "â" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="!formDialog.readonly" label="æä½" width="70" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button type="danger" link size="small" @click="removeExpenseDetail($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">æ¶æ¬¾ä¿¡æ¯</span></template> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æ¶æ¬¾äºº" prop="payee"> |
| | | <el-input v-model="form.payee" placeholder="请è¾å
¥æ¶æ¬¾äºº" maxlength="50" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æ¶æ¬¾è´¦å·" prop="payeeAccount"> |
| | | <el-input v-model="form.payeeAccount" placeholder="é¶è¡å¡å·" maxlength="30" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="弿·æ¯è¡" prop="bankBranch"> |
| | | <el-input v-model="form.bankBranch" placeholder="弿·æ¯è¡å
¨ç§°" maxlength="100" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">éä»¶ï¼å票ï¼</span></template> |
| | | <el-form-item label-width="0" class="attachment-form-item"> |
| | | <div class="upload-block"> |
| | | <FileUpload v-model:file-list="form.attachmentList" :limit="20" button-text="ç¹å»éæ©æä»¶" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header-row"> |
| | | <span class="card-header-title">å®¡æ¹æµç¨</span> |
| | | <el-button v-if="!formDialog.readonly" type="primary" link size="small" @click="autoAssignApprovalFlow"> |
| | | æè§åéæ°åé
|
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <el-alert type="success" :title="approvalRuleHint" show-icon :closable="false" class="mb12" /> |
| | | <el-form-item prop="approvalFlowNodes" label-width="0"> |
| | | <ApprovalFlowEditor |
| | | v-if="!formDialog.readonly" |
| | | v-model="form.approvalFlowNodes" |
| | | :user-options="flowUserOptions" |
| | | @update:model-value="onApprovalFlowChange" |
| | | /> |
| | | <ApprovalFlowProgress v-else :nodes="form.approvalFlowNodes" :current-index="form.currentNodeIndex" /> |
| | | <p v-if="!formDialog.readonly" class="flow-tip">ç³»ç»å·²æéé¢ä¸è´¹ç¨ç±»åèªå¨åé
审æ¹äººï¼å¯æå¨è°æ´ã</p> |
| | | </el-form-item> |
| | | </el-card> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button |
| | | v-if="!formDialog.readonly" |
| | | type="primary" |
| | | :loading="submitSaving" |
| | | @click="submitForm" |
| | | > |
| | | æ 交 |
| | | </el-button> |
| | | <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "å
³ é" : "å æ¶" }}</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详æ
--> |
| | | <el-dialog v-model="detailDialog.visible" title="è´¹ç¨æ¥é详æ
" width="900px" append-to-body destroy-on-close> |
| | | <div v-loading="detailLoading"> |
| | | <DetailPanel :row="detailRow" /> |
| | | <el-divider content-position="left">å®¡æ¹æµç¨</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="detailRow.approvalFlowProgressNodes ?? 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> |
| | | <template #footer> |
| | | <el-button type="primary" @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å®¡æ¹ --> |
| | | <el-dialog |
| | | v-model="approveDialog.visible" |
| | | title="è´¹ç¨æ¥é审æ¹" |
| | | width="1000px" |
| | | append-to-body |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <DetailPanel :row="approveDialog.row" /> |
| | | <el-divider content-position="left">æµç¨è¿åº¦</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="approveDialog.row?.approvalFlowProgressNodes ?? approveDialog.row?.approvalFlowNodes" |
| | | :current-index="approveDialog.row?.currentNodeIndex ?? 0" |
| | | /> |
| | | <el-form label-width="100px" class="mt16"> |
| | | <el-form-item label="å®¡æ¹æè§" required> |
| | | <el-input |
| | | v-model="approveOpinion" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="500" |
| | | show-word-limit |
| | | placeholder="éè¿å¯ç空ï¼é©³å请填åå
·ä½åå ï¼å¦ï¼å票模ç³ééä¼ ï¼" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="success" @click="submitApprove('approved')">é è¿</el-button> |
| | | <el-button type="danger" @click="submitApprove('rejected')">驳 å</el-button> |
| | | <el-button @click="approveDialog.visible = false">å æ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import FileUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue"; |
| | | import ApprovalFlowProgress from "../travel-reimburse/components/ApprovalFlowProgress.vue"; |
| | | import DetailPanel from "./components/DetailPanel.vue"; |
| | | import { useCostReimburse } from "./useCostReimburse.js"; |
| | | |
| | | const cr = useCostReimburse(); |
| | | const { |
| | | Search, |
| | | EXPENSE_CATEGORY_OPTIONS, |
| | | CATEGORY_TEMPLATES, |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | importInputRef, |
| | | formRef, |
| | | form, |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | applicantFormSearchLoading, |
| | | applicantFormOptions, |
| | | flowUserOptions, |
| | | detailTotalAmount, |
| | | approvalRuleHint, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | remoteSearchApplicantForm, |
| | | userSelectLabel, |
| | | onApplicantChange, |
| | | onExpenseCategoryChange, |
| | | applyTemplate, |
| | | onDetailAmountChange, |
| | | onApprovalFlowChange, |
| | | addExpenseDetail, |
| | | removeExpenseDetail, |
| | | syncApplyAmountFromDetails, |
| | | autoAssignApprovalFlow, |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | | handleExport, |
| | | handleImportClick, |
| | | onImportFile, |
| | | } = cr; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | .mb12 { |
| | | margin-bottom: 12px; |
| | | } |
| | | .mt16 { |
| | | margin-top: 16px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_fields { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | .search_actions { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | .sr-only-input { |
| | | position: absolute; |
| | | width: 1px; |
| | | height: 1px; |
| | | padding: 0; |
| | | margin: -1px; |
| | | overflow: hidden; |
| | | clip: rect(0, 0, 0, 0); |
| | | white-space: nowrap; |
| | | border: 0; |
| | | } |
| | | .template-bar { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .template-label { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-secondary); |
| | | flex-shrink: 0; |
| | | } |
| | | .form-section { |
| | | margin-bottom: 16px; |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | } |
| | | .form-section :deep(.el-card__header) { |
| | | padding: 12px 16px; |
| | | background: var(--el-fill-color-lighter); |
| | | } |
| | | .form-section :deep(.el-card__body) { |
| | | padding: 16px 16px 4px; |
| | | } |
| | | .card-header-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | } |
| | | .card-header-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .amount-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | width: 100%; |
| | | } |
| | | .amount-input { |
| | | flex: 1; |
| | | min-width: 160px; |
| | | } |
| | | .attachment-form-item { |
| | | margin-bottom: 0; |
| | | } |
| | | .detail-table { |
| | | margin-bottom: 0; |
| | | } |
| | | .upload-block { |
| | | width: 100%; |
| | | } |
| | | .flow-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin-top: 8px; |
| | | } |
| | | .cost-reimburse-form-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |
| | | .cost-reimburse-form :deep(.el-form-item) { |
| | | margin-bottom: 18px; |
| | | } |
| | | .cost-reimburse-form :deep(.el-input-number) { |
| | | width: 100%; |
| | | } |
| | | .cost-reimburse-form :deep(.el-row) { |
| | | margin-bottom: 0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | deleteFinReimbursement, |
| | | getFinReimbursementDetail, |
| | | listFinReimbursementPage, |
| | | persistFinReimbursement, |
| | | } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue"; |
| | | import { |
| | | buildCostReimbursementSaveDto, |
| | | buildFinReimbursementListParams, |
| | | filterReimbursementRowsBySearch, |
| | | hasActiveReimbursementSearch, |
| | | canDeleteReimbursementRow, |
| | | canEditReimbursementRow, |
| | | enrichReimbursementListRowsWithApprovalFlow, |
| | | filterRowsByReimbursementType, |
| | | FIN_REIMBURSEMENT_TYPE, |
| | | mapCostReimbursementRow, |
| | | mapFinReimbursementDetailRow, |
| | | resolveReimbursementDeleteId, |
| | | unwrapFinReimbursementDetail, |
| | | unwrapFinReimbursementPage, |
| | | validateReimbursementApprovalNodes, |
| | | validateReimbursementPersistDto, |
| | | } from "../shared/finReimbursementMappers.js"; |
| | | import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js"; |
| | | import { |
| | | EXPENSE_CATEGORY_OPTIONS, |
| | | CATEGORY_TEMPLATES, |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseCategoryLabel, |
| | | expenseSubjectLabel, |
| | | statusLabel, |
| | | statusTagType, |
| | | formatApprovalFlowSummary, |
| | | buildAutoApprovalFlow, |
| | | getApprovalRuleHint, |
| | | createEmptyExpenseDetail, |
| | | createEmptyForm, |
| | | applyCategoryTemplate, |
| | | initApprovalFlowNodes, |
| | | advanceApprovalFlow, |
| | | rejectApprovalFlow, |
| | | normalizeImportedRow, |
| | | } from "./costReimburseUtils.js"; |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | function demoFlowNodes(amount = 1200, category = "transport") { |
| | | return buildAutoApprovalFlow(amount, category); |
| | | } |
| | | |
| | | export function useCostReimburse() { |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const allRows = ref([]); |
| | | |
| | | const searchForm = reactive({ |
| | | applicantKeyword: "", |
| | | }); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | const importInputRef = ref(null); |
| | | const allUsersCache = ref([]); |
| | | const applicantFormSearchLoading = ref(false); |
| | | const applicantFormOptions = ref([]); |
| | | const formRef = ref(); |
| | | const form = reactive(createEmptyForm()); |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailLoading = ref(false); |
| | | const detailRow = ref({}); |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | const submitSaving = ref(false); |
| | | |
| | | const tableData = computed(() => |
| | | allRows.value.map((r) => ({ |
| | | ...r, |
| | | approvalFlowSummary: formatApprovalFlowSummary(r), |
| | | })) |
| | | ); |
| | | |
| | | async function fetchList() { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listFinReimbursementPage( |
| | | buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, |
| | | }) |
| | | ); |
| | | const { records, total } = unwrapFinReimbursementPage(res); |
| | | const filtered = filterRowsByReimbursementType( |
| | | records, |
| | | FIN_REIMBURSEMENT_TYPE.COST |
| | | ); |
| | | let mapped = filtered.map(mapCostReimbursementRow); |
| | | mapped = await enrichReimbursementListRowsWithApprovalFlow( |
| | | mapped, |
| | | FIN_REIMBURSEMENT_TYPE.COST |
| | | ); |
| | | if (hasActiveReimbursementSearch(searchForm)) { |
| | | mapped = filterReimbursementRowsBySearch(mapped, searchForm); |
| | | } |
| | | allRows.value = mapped; |
| | | const dropped = records.length - filtered.length; |
| | | let nextTotal = |
| | | dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total); |
| | | if (hasActiveReimbursementSearch(searchForm)) { |
| | | nextTotal = mapped.length; |
| | | } |
| | | page.total = nextTotal; |
| | | } catch { |
| | | allRows.value = []; |
| | | page.total = 0; |
| | | proxy?.$modal?.msgError?.("è´¹ç¨æ¥éå表å 载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); |
| | | |
| | | const detailTotalAmount = computed(() => { |
| | | const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); |
| | | return Math.round(sum * 100) / 100; |
| | | }); |
| | | |
| | | const approvalRuleHint = computed(() => |
| | | getApprovalRuleHint(form.applyAmount ?? detailTotalAmount.value, form.expenseCategory) |
| | | ); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "æ¥éåå·", prop: "reimburseNo", width: 150 }, |
| | | { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äºº", prop: "applicantName", minWidth: 90 }, |
| | | { label: "æ¥ééé¢(å
)", prop: "applyAmount", width: 110 }, |
| | | { label: "æ¥éåå ", prop: "reimburseReason", minWidth: 160, showOverflowTooltip: true }, |
| | | { label: "ç³è¯·æ¶é´", prop: "applyTime", width: 165 }, |
| | | { label: "å建æ¶é´", prop: "createTime", width: 165 }, |
| | | { |
| | | label: "æ¥éç¶æ", |
| | | prop: "approvalResult", |
| | | width: 100, |
| | | dataType: "tag", |
| | | formatData: (v) => statusLabel(v), |
| | | formatType: (v) => statusTagType(v), |
| | | }, |
| | | { |
| | | label: "å®¡æ¹æµç¨", |
| | | prop: "approvalFlowSummary", |
| | | minWidth: 200, |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 220, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: (row) => !canEditReimbursementRow(row), |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | disabled: (row) => !canDeleteReimbursementRow(row), |
| | | clickFun: (row) => confirmRemoveRow(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const formRules = { |
| | | applicantId: [{ required: true, message: "è¯·éæ©åå·¥", trigger: "change" }], |
| | | expenseCategory: [{ required: true, message: "è¯·éæ©è´¹ç¨ç±»å", trigger: "change" }], |
| | | reimburseReason: [{ required: true, message: "è¯·å¡«åæ¥éåå ", trigger: "blur" }], |
| | | applyAmount: [{ required: true, message: "è¯·å¡«åæ¥ééé¢", trigger: "blur" }], |
| | | payee: [{ required: true, message: "è¯·å¡«åæ¶æ¬¾äºº", trigger: "blur" }], |
| | | payeeAccount: [{ required: true, message: "è¯·å¡«åæ¶æ¬¾è´¦å·", trigger: "blur" }], |
| | | bankBranch: [{ required: true, message: "请填å弿·æ¯è¡", trigger: "blur" }], |
| | | approvalFlowNodes: [ |
| | | { |
| | | validator: (_r, _v, cb) => { |
| | | const nodes = form.approvalFlowNodes || []; |
| | | if (!nodes.length) { |
| | | cb(new Error("请è³å°é
ç½®ä¸ä¸ªå®¡æ¹èç¹")); |
| | | return; |
| | | } |
| | | if (nodes.some((n) => n.approverId == null || n.approverId === "")) { |
| | | cb(new Error("æ¯ä¸ªèç¹é¡»éæ©å®¡æ¹äºº")); |
| | | return; |
| | | } |
| | | cb(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | allUsersCache.value = unwrapArray(await userListNoPageByTenantId()); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | function userSelectLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const name = u.userName || ""; |
| | | if (nick && name && nick !== name) return `${nick}ï¼${name}ï¼`; |
| | | return nick || name || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function userById(id) { |
| | | return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id)); |
| | | } |
| | | |
| | | function employeeNoFromUser(u) { |
| | | if (!u) return ""; |
| | | return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : ""); |
| | | } |
| | | |
| | | function filterUsersByQuery(query) { |
| | | const list = allUsersCache.value.filter(isActiveUser); |
| | | const q = (query || "").trim().toLowerCase(); |
| | | if (!q) return [...list]; |
| | | return list.filter((u) => { |
| | | const nick = (u.nickName || "").toLowerCase(); |
| | | const uname = (u.userName || "").toLowerCase(); |
| | | return nick.includes(q) || uname.includes(q); |
| | | }); |
| | | } |
| | | |
| | | async function remoteSearchApplicantForm(query) { |
| | | applicantFormSearchLoading.value = true; |
| | | try { |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | applicantFormOptions.value = filterUsersByQuery(query); |
| | | } finally { |
| | | applicantFormSearchLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function onApplicantChange(uid) { |
| | | const u = userById(uid); |
| | | if (u) { |
| | | form.employeeName = u.nickName || u.userName || ""; |
| | | form.employeeNo = employeeNoFromUser(u); |
| | | form.payee = form.payee || form.employeeName; |
| | | form.deptId = String(u.deptId ?? u.sysDeptId ?? ""); |
| | | form.deptName = u.dept?.deptName ?? u.deptName ?? ""; |
| | | } else { |
| | | form.employeeName = ""; |
| | | form.employeeNo = ""; |
| | | } |
| | | } |
| | | |
| | | function autoAssignApprovalFlow() { |
| | | const amount = Number(form.applyAmount) || detailTotalAmount.value || 0; |
| | | form.approvalFlowNodes = buildAutoApprovalFlow( |
| | | amount, |
| | | form.expenseCategory || "other", |
| | | form.approvalFlowNodes |
| | | ); |
| | | nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); |
| | | } |
| | | |
| | | function onExpenseCategoryChange(val) { |
| | | if (val && !(form.expenseDetails || []).length) { |
| | | applyCategoryTemplate(form, val); |
| | | syncApplyAmountFromDetails(); |
| | | } |
| | | autoAssignApprovalFlow(); |
| | | } |
| | | |
| | | function applyTemplate(category) { |
| | | applyCategoryTemplate(form, category); |
| | | syncApplyAmountFromDetails(); |
| | | autoAssignApprovalFlow(); |
| | | proxy?.$modal?.msgSuccess?.(`å·²åºç¨ã${CATEGORY_TEMPLATES[category]?.label || category}ãå¡«æ¥æ¨¡æ¿`); |
| | | } |
| | | |
| | | function onDetailAmountChange() { |
| | | syncApplyAmountFromDetails(); |
| | | autoAssignApprovalFlow(); |
| | | } |
| | | |
| | | function onApprovalFlowChange() { |
| | | nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); |
| | | } |
| | | |
| | | function addExpenseDetail() { |
| | | form.expenseDetails.push(createEmptyExpenseDetail()); |
| | | } |
| | | |
| | | function removeExpenseDetail(index) { |
| | | form.expenseDetails.splice(index, 1); |
| | | syncApplyAmountFromDetails(); |
| | | autoAssignApprovalFlow(); |
| | | } |
| | | |
| | | function syncApplyAmountFromDetails() { |
| | | form.applyAmount = detailTotalAmount.value; |
| | | } |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | return fetchList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.applicantKeyword = ""; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | return fetchList(); |
| | | } |
| | | |
| | | async function loadCostDetailRow(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | throw new Error("missing id"); |
| | | } |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | return mapFinReimbursementDetailRow(raw, FIN_REIMBURSEMENT_TYPE.COST); |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | proxy?.$modal?.msgWarning?.("æ æ³æ¥ç详æ
ï¼ç¼ºå°æ¥éå ID"); |
| | | return; |
| | | } |
| | | detailDialog.visible = true; |
| | | detailLoading.value = true; |
| | | detailRow.value = { ...row }; |
| | | try { |
| | | detailRow.value = await loadCostDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å 载详æ
失败"); |
| | | detailDialog.visible = false; |
| | | } finally { |
| | | detailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirmRemoveRow(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | proxy?.$modal?.msgWarning?.("æ æ³å é¤ï¼ç¼ºå°æ¥éå ID"); |
| | | return; |
| | | } |
| | | const title = row.reimburseNo || row.billNo || row.reimburseReason || "该æ¥éå"; |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤ã${title}ãåï¼å é¤åä¸å¯æ¢å¤ã`, |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "ç¡®å®å é¤", |
| | | cancelButtonText: "åæ¶", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | try { |
| | | await deleteFinReimbursement([id]); |
| | | proxy?.$modal?.msgSuccess?.("å 餿å"); |
| | | if (detailDialog.visible && resolveReimbursementDeleteId(detailRow.value) === id) { |
| | | detailDialog.visible = false; |
| | | } |
| | | await handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å é¤å¤±è´¥"); |
| | | } |
| | | } |
| | | |
| | | function openApprove(row) { |
| | | approveDialog.row = { ...row }; |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | | function approvalActionLabel(v) { |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "rejected") return "驳å"; |
| | | return "æäº¤"; |
| | | } |
| | | |
| | | async function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.readonly = false; |
| | | formDialog.title = mode === "add" ? "æ°å¢è´¹ç¨æ¥é" : "ç¼è¾è´¹ç¨æ¥é"; |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | Object.assign(form, createEmptyForm()); |
| | | if (mode === "edit" && row) { |
| | | let editRow = row; |
| | | try { |
| | | editRow = await loadCostDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å è½½æ¥é详æ
失败"); |
| | | return; |
| | | } |
| | | Object.assign(form, { |
| | | ...JSON.parse(JSON.stringify(editRow)), |
| | | reimbursementId: editRow.reimbursementId ?? editRow.id, |
| | | attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])), |
| | | approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])), |
| | | expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])), |
| | | }); |
| | | const u = userById(editRow.applicantId); |
| | | applicantFormOptions.value = u |
| | | ? [u] |
| | | : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }]; |
| | | } else { |
| | | form.approvalFlowNodes = buildAutoApprovalFlow(0, "other"); |
| | | remoteSearchApplicantForm(""); |
| | | } |
| | | formDialog.visible = true; |
| | | nextTick(() => { |
| | | formRef.value?.clearValidate?.(); |
| | | }); |
| | | } |
| | | |
| | | function onFormClosed() { |
| | | formRef.value?.resetFields?.(); |
| | | } |
| | | |
| | | async function submitForm() { |
| | | try { |
| | | await formRef.value?.validate?.(); |
| | | } catch { |
| | | return; |
| | | } |
| | | if (!(form.expenseDetails || []).length) { |
| | | proxy?.$modal?.msgWarning?.("请è³å°æ·»å 䏿¡æ¥éæç»"); |
| | | return; |
| | | } |
| | | syncApplyAmountFromDetails(); |
| | | |
| | | if (submitSaving.value) return; |
| | | const isEdit = formDialog.mode === "edit"; |
| | | const dto = buildCostReimbursementSaveDto(form); |
| | | const check = validateReimbursementPersistDto(dto, isEdit); |
| | | if (!check.ok) { |
| | | proxy?.$modal?.msgWarning?.(check.message); |
| | | return; |
| | | } |
| | | const nodeCheck = validateReimbursementApprovalNodes(dto); |
| | | if (!nodeCheck.ok) { |
| | | proxy?.$modal?.msgWarning?.(nodeCheck.message); |
| | | return; |
| | | } |
| | | submitSaving.value = true; |
| | | try { |
| | | await persistFinReimbursement(dto, isEdit); |
| | | proxy?.$modal?.msgSuccess?.(isEdit ? "ä¿åæå" : "æäº¤æå"); |
| | | formDialog.visible = false; |
| | | await handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.(isEdit ? "ä¿å失败" : "æäº¤å¤±è´¥"); |
| | | } finally { |
| | | submitSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | | const row = approveDialog.row; |
| | | if (!row) return; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | proxy?.$modal?.msgWarning?.("驳å须填åå®¡æ¹æè§ï¼å¦ï¼å票模ç³ééä¼ ï¼"); |
| | | return; |
| | | } |
| | | const idx = allRows.value.findIndex((r) => r.id === row.id); |
| | | if (idx === -1) return; |
| | | const cur = allRows.value[idx]; |
| | | const operatorName = "å½å审æ¹äºº"; |
| | | const record = { |
| | | operatorName, |
| | | result, |
| | | opinion: approveOpinion.value || (result === "approved" ? "åæ" : "驳å"), |
| | | time: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | const records = [...(cur.approvalRecords || []), record]; |
| | | let flowUpdate; |
| | | if (result === "approved") { |
| | | flowUpdate = advanceApprovalFlow(cur, approveOpinion.value); |
| | | } else { |
| | | flowUpdate = rejectApprovalFlow(cur, approveOpinion.value); |
| | | } |
| | | allRows.value[idx] = { |
| | | ...cur, |
| | | approvalFlowNodes: flowUpdate.nodes, |
| | | currentNodeIndex: flowUpdate.currentNodeIndex, |
| | | approvalResult: flowUpdate.approvalResult, |
| | | rejectReason: flowUpdate.rejectReason ?? cur.rejectReason, |
| | | approvalRecords: records, |
| | | }; |
| | | proxy?.$modal?.msgSuccess?.(result === "approved" ? "å·²éè¿" : "已驳å"); |
| | | approveDialog.visible = false; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function handleExport() { |
| | | const data = allRows.value; |
| | | const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" }); |
| | | const url = URL.createObjectURL(blob); |
| | | const a = document.createElement("a"); |
| | | a.href = url; |
| | | a.download = `è´¹ç¨æ¥é导åº_${dayjs().format("YYYYMMDDHHmmss")}.json`; |
| | | a.click(); |
| | | URL.revokeObjectURL(url); |
| | | proxy?.$modal?.msgSuccess?.(`å·²å¯¼åº ${data.length} æ¡`); |
| | | } |
| | | |
| | | function handleImportClick() { |
| | | importInputRef.value?.click?.(); |
| | | } |
| | | |
| | | function onImportFile(e) { |
| | | const file = e.target.files?.[0]; |
| | | e.target.value = ""; |
| | | if (!file) return; |
| | | const reader = new FileReader(); |
| | | reader.onload = () => { |
| | | try { |
| | | const parsed = JSON.parse(String(reader.result || "")); |
| | | const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data; |
| | | if (!Array.isArray(arr) || !arr.length) { |
| | | proxy?.$modal?.msgWarning?.("导å
¥æ ¼å¼é¡»ä¸ºè´¹ç¨æ¥é JSON æ°ç»"); |
| | | return; |
| | | } |
| | | arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i))); |
| | | proxy?.$modal?.msgSuccess?.(`æå导å
¥ ${arr.length} æ¡`); |
| | | handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("è§£æå¤±è´¥"); |
| | | } |
| | | }; |
| | | reader.readAsText(file, "utf-8"); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | loadUserPool(); |
| | | await fetchList(); |
| | | const editPayload = consumeReimburseEditFromApprove(); |
| | | if (editPayload?.reimbursementId != null) { |
| | | await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId }); |
| | | } |
| | | }); |
| | | |
| | | return { |
| | | Search, |
| | | EXPENSE_CATEGORY_OPTIONS, |
| | | CATEGORY_TEMPLATES, |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseCategoryLabel, |
| | | expenseSubjectLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | importInputRef, |
| | | formRef, |
| | | form, |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | applicantFormSearchLoading, |
| | | applicantFormOptions, |
| | | flowUserOptions, |
| | | detailTotalAmount, |
| | | approvalRuleHint, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | remoteSearchApplicantForm, |
| | | userSelectLabel, |
| | | onApplicantChange, |
| | | onExpenseCategoryChange, |
| | | applyTemplate, |
| | | onDetailAmountChange, |
| | | onApprovalFlowChange, |
| | | addExpenseDetail, |
| | | removeExpenseDetail, |
| | | syncApplyAmountFromDetails, |
| | | autoAssignApprovalFlow, |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | | handleExport, |
| | | handleImportClick, |
| | | onImportFile, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å·®æ
/è´¹ç¨æ¥éï¼å®¡æ¹å表å
详æ
/审æ¹å¼¹çªå
容ï¼ä¸æ¥é页弹çªä¸è´ï¼ --> |
| | | <template> |
| | | <div v-loading="loading"> |
| | | <TravelDetailPanel v-if="isTravel" :row="reimburseRow" /> |
| | | <CostDetailPanel v-else :row="reimburseRow" /> |
| | | |
| | | <el-divider content-position="left">æµç¨è¿åº¦</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="reimburseRow.approvalFlowProgressNodes ?? reimburseRow.approvalFlowNodes" |
| | | :current-index="reimburseRow.currentNodeIndex ?? 0" |
| | | /> |
| | | |
| | | <template v-if="mode === 'detail'"> |
| | | <el-divider content-position="left">审æ¹è®°å½ï¼å
¨æµç¨ççï¼</el-divider> |
| | | <el-timeline v-if="reimburseRow.approvalRecords?.length"> |
| | | <el-timeline-item |
| | | v-for="(rec, i) in reimburseRow.approvalRecords" |
| | | :key="i" |
| | | :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'" |
| | | :timestamp="rec.time" |
| | | > |
| | | {{ rec.operatorName }} â {{ actionLabel(rec.result) }}ï¼{{ rec.opinion || "æ æè§" }} |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ å®¡æ¹è®°å½" :image-size="60" /> |
| | | </template> |
| | | |
| | | <el-form v-else label-width="100px" class="mt16"> |
| | | <el-form-item label="å®¡æ¹æè§"> |
| | | <el-input |
| | | :model-value="approveOpinion" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="500" |
| | | show-word-limit |
| | | :placeholder="isTravel ? 'éè¿å¯ç空ï¼é©³å请填ååå ' : 'éè¿å¯ç空ï¼é©³å请填åå
·ä½åå ï¼å¦ï¼å票模ç³ééä¼ ï¼'" |
| | | @update:model-value="$emit('update:approveOpinion', $event)" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { isTravelReimbursementType } from "../finReimbursementMappers.js"; |
| | | import ApprovalFlowProgress from "../../travel-reimburse/components/ApprovalFlowProgress.vue"; |
| | | import CostDetailPanel from "../../cost-reimburse/components/DetailPanel.vue"; |
| | | import TravelDetailPanel from "../../travel-reimburse/components/DetailPanel.vue"; |
| | | |
| | | const props = defineProps({ |
| | | mode: { type: String, default: "detail" }, |
| | | moduleKey: { type: String, default: "" }, |
| | | reimburseRow: { type: Object, default: () => ({}) }, |
| | | loading: { type: Boolean, default: false }, |
| | | approveOpinion: { type: String, default: "" }, |
| | | }); |
| | | |
| | | defineEmits(["update:approveOpinion"]); |
| | | |
| | | const isTravel = computed(() => |
| | | isTravelReimbursementType(props.reimburseRow?.reimbursementType ?? props.moduleKey) |
| | | ); |
| | | |
| | | function actionLabel(v) { |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "rejected") return "驳å"; |
| | | return "æäº¤"; |
| | | } |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { formatDisplayTime } from "../../ApproveManage/approve-template/approveTemplateConstants.js"; |
| | | import { |
| | | mapRecordResultFromApi, |
| | | mapRecordsFromApi, |
| | | mapTasksToFlowNodes, |
| | | } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | |
| | | function taskStatusToNodeStatus(taskStatus) { |
| | | const s = String(taskStatus ?? "").toUpperCase(); |
| | | if (["APPROVED", "COMPLETED", "FINISHED", "PASSED", "AGREE"].includes(s)) { |
| | | return "finish"; |
| | | } |
| | | if (["REJECTED", "REJECT", "REFUSE", "REFUSED"].includes(s)) { |
| | | return "error"; |
| | | } |
| | | if (["PENDING", "IN_APPROVAL", "PROCESS", "PROCESSING"].includes(s)) { |
| | | return "process"; |
| | | } |
| | | return "wait"; |
| | | } |
| | | |
| | | /** storageBlobVOList â 页é¢éä»¶å表 */ |
| | | export function mapReimbursementAttachments(source = {}) { |
| | | const list = |
| | | source.storageBlobVOList || |
| | | source.storageBlobDTOs || |
| | | source.storageBlobDTOS || |
| | | source.storageBlobVOS || |
| | | source.attachmentList || |
| | | source.invoiceAttachments || |
| | | []; |
| | | if (!Array.isArray(list)) return []; |
| | | return list.map((b, i) => ({ |
| | | ...b, |
| | | id: b.id ?? b.blobId ?? `att_${i}`, |
| | | name: |
| | | b.fileName || |
| | | b.originalFilename || |
| | | b.originalFileName || |
| | | b.blobName || |
| | | b.name || |
| | | "éä»¶", |
| | | url: |
| | | b.url || |
| | | b.fileUrl || |
| | | b.downloadUrl || |
| | | b.downloadURL || |
| | | b.previewUrl || |
| | | b.previewURL || |
| | | b.link || |
| | | "", |
| | | })); |
| | | } |
| | | |
| | | /** 审æ¹è®°å½æ¥èª tasksï¼æ¯æ¡ä»»å¡ä¸æ¡ççï¼ */ |
| | | export function mapTasksToApprovalRecords(tasks) { |
| | | const list = Array.isArray(tasks) ? tasks : []; |
| | | return list |
| | | .map((t, index) => ({ |
| | | id: t.id ?? index, |
| | | operatorName: t.approverName || t.operatorName || t.createUserName || "â", |
| | | result: mapRecordResultFromApi( |
| | | t.approveAction ?? t.taskStatus ?? t.status |
| | | ), |
| | | opinion: t.approveComment || t.comment || t.opinion || "", |
| | | time: formatDisplayTime( |
| | | t.approveTime || t.finishTime || t.updateTime || t.createTime || "" |
| | | ), |
| | | levelNo: t.levelNo ?? t.taskLevel, |
| | | raw: t, |
| | | })) |
| | | .sort((a, b) => { |
| | | const la = Number(a.levelNo ?? 0); |
| | | const lb = Number(b.levelNo ?? 0); |
| | | if (la !== lb) return la - lb; |
| | | return String(a.time).localeCompare(String(b.time)); |
| | | }); |
| | | } |
| | | |
| | | /** tasks â ApprovalFlowProgress èç¹ */ |
| | | export function mapTasksToApprovalFlowNodes(tasks) { |
| | | const grouped = mapTasksToFlowNodes(tasks); |
| | | return grouped.map((node, i) => { |
| | | const approvers = node.approvers || []; |
| | | const statuses = approvers.map(a => |
| | | taskStatusToNodeStatus(a.taskStatus ?? a.status) |
| | | ); |
| | | let nodeStatus = "wait"; |
| | | if (statuses.includes("error")) nodeStatus = "error"; |
| | | else if (statuses.length && statuses.every(s => s === "finish")) { |
| | | nodeStatus = "finish"; |
| | | } else if (statuses.includes("process")) nodeStatus = "process"; |
| | | |
| | | const names = approvers.map(a => a.approverName).filter(Boolean).join("ã"); |
| | | const opinions = approvers |
| | | .map(a => a.approveComment) |
| | | .filter(Boolean) |
| | | .join("ï¼"); |
| | | |
| | | return { |
| | | nodeOrder: node.nodeOrder ?? node.levelNo ?? i + 1, |
| | | sortOrder: node.nodeOrder ?? node.levelNo ?? i + 1, |
| | | approverName: names || "â", |
| | | approveOpinion: opinions, |
| | | approveTime: approvers.find(a => a.approveTime)?.approveTime || "", |
| | | nodeStatus, |
| | | signMode: node.signMode, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | export function computeApprovalFlowCurrentIndex(approvalFlowNodes = []) { |
| | | const list = approvalFlowNodes || []; |
| | | const processing = list.findIndex(n => n.nodeStatus === "process"); |
| | | if (processing >= 0) return processing; |
| | | const errorIdx = list.findIndex(n => n.nodeStatus === "error"); |
| | | if (errorIdx >= 0) return errorIdx; |
| | | return list.filter(n => n.nodeStatus === "finish").length; |
| | | } |
| | | |
| | | /** 详æ
DTO è¡¥å
tasks / éä»¶ / 审æ¹è®°å½ */ |
| | | export function applyFinReimbursementDetailEnrichment(mapped, raw = {}) { |
| | | if (!mapped || typeof mapped !== "object") return mapped; |
| | | const source = { ...raw, ...mapped }; |
| | | const tasks = Array.isArray(source.tasks) ? source.tasks : []; |
| | | const attachments = mapReimbursementAttachments(source); |
| | | const approvalRecords = tasks.length |
| | | ? mapTasksToApprovalRecords(tasks) |
| | | : mapRecordsFromApi(source.records || source.approvalRecords); |
| | | /** 表åç¼è¾åæ¾ï¼ä¿ç nodes æ å°ï¼å« approverIdï¼ï¼å¿ç¨ tasks è¦ç */ |
| | | const approvalFlowNodes = Array.isArray(mapped.approvalFlowNodes) |
| | | ? mapped.approvalFlowNodes |
| | | : []; |
| | | /** 详æ
/è¿åº¦æ¡å±ç¤ºï¼æ tasks æ¶ç¨ä»»å¡ç¶æèç¹ */ |
| | | const approvalFlowProgressNodes = tasks.length |
| | | ? mapTasksToApprovalFlowNodes(tasks) |
| | | : approvalFlowNodes; |
| | | const currentNodeIndex = computeApprovalFlowCurrentIndex( |
| | | approvalFlowProgressNodes.length ? approvalFlowProgressNodes : approvalFlowNodes |
| | | ); |
| | | const rejectReason = |
| | | approvalRecords.find(r => r.result === "rejected")?.opinion || |
| | | source.rejectReason || |
| | | ""; |
| | | |
| | | return { |
| | | ...mapped, |
| | | tasks, |
| | | storageBlobVOList: attachments, |
| | | attachmentList: attachments, |
| | | invoiceAttachments: attachments, |
| | | approvalRecords, |
| | | records: tasks.length ? tasks : source.records, |
| | | approvalFlowNodes, |
| | | approvalFlowProgressNodes, |
| | | currentNodeIndex, |
| | | rejectReason, |
| | | flowNodes: tasks.length ? mapTasksToFlowNodes(tasks) : mapped.flowNodes, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js"; |
| | | import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js"; |
| | | import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js"; |
| | | import { mapTasksToFlowNodes } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js"; |
| | | |
| | | /** æ¥éç±»åï¼1-å·®æ
æ¥éï¼2-è´¹ç¨æ¥é */ |
| | | export const FIN_REIMBURSEMENT_TYPE = { |
| | | TRAVEL: "1", |
| | | COST: "2", |
| | | }; |
| | | |
| | | const REIMBURSEMENT_TYPE_LABEL = { |
| | | [FIN_REIMBURSEMENT_TYPE.TRAVEL]: "å·®æ
æ¥é", |
| | | [FIN_REIMBURSEMENT_TYPE.COST]: "è´¹ç¨æ¥é", |
| | | }; |
| | | |
| | | /** å½ä¸åæ¥éç±»åï¼1-å·®æ
ï¼2-è´¹ç¨ */ |
| | | export function normalizeReimbursementType(val) { |
| | | const s = String(val ?? "").trim(); |
| | | if (s === "1" || s === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | return FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | } |
| | | if (s === "2" || s === FIN_REIMBURSEMENT_TYPE.COST) { |
| | | return FIN_REIMBURSEMENT_TYPE.COST; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | export function reimbursementTypeLabel(type) { |
| | | return REIMBURSEMENT_TYPE_LABEL[normalizeReimbursementType(type)] || "â"; |
| | | } |
| | | |
| | | export function getModuleKeyByReimbursementType(type) { |
| | | const t = normalizeReimbursementType(type); |
| | | if (t === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | return APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE; |
| | | } |
| | | if (t === FIN_REIMBURSEMENT_TYPE.COST) { |
| | | return APPROVAL_MODULE_KEYS.COST_REIMBURSE; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | /** ä¼å
æ¥å£ reimbursementTypeï¼å
¶æ¬¡é¡µé¢ moduleKey / å
¥å */ |
| | | export function resolveReimbursementType(raw, fallback) { |
| | | const fromApi = normalizeReimbursementType(raw?.reimbursementType); |
| | | if (fromApi) return fromApi; |
| | | return ( |
| | | normalizeReimbursementType(fallback) || |
| | | getReimbursementTypeByModuleKey(fallback) || |
| | | "" |
| | | ); |
| | | } |
| | | |
| | | export function isTravelReimbursementType(type) { |
| | | return ( |
| | | resolveReimbursementType({ reimbursementType: type }, type) === |
| | | FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ); |
| | | } |
| | | |
| | | export function filterRowsByReimbursementType(rows, expectedType) { |
| | | const expected = normalizeReimbursementType(expectedType); |
| | | if (!expected) return rows || []; |
| | | return (rows || []).filter((row) => { |
| | | const t = resolveReimbursementType(row, expected); |
| | | return t === expected; |
| | | }); |
| | | } |
| | | |
| | | export function getReimbursementTypeByModuleKey(moduleKey) { |
| | | if (moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE) { |
| | | return FIN_REIMBURSEMENT_TYPE.TRAVEL; |
| | | } |
| | | if (moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE) { |
| | | return FIN_REIMBURSEMENT_TYPE.COST; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | export function unwrapFinReimbursementPage(res) { |
| | | const data = res?.data ?? res; |
| | | if (!data || typeof data !== "object") { |
| | | return { records: [], total: 0 }; |
| | | } |
| | | if (Array.isArray(data.records)) { |
| | | return { records: data.records, total: Number(data.total ?? 0) }; |
| | | } |
| | | const nested = data.data; |
| | | if (nested && typeof nested === "object" && Array.isArray(nested.records)) { |
| | | return { records: nested.records, total: Number(nested.total ?? 0) }; |
| | | } |
| | | return { records: [], total: 0 }; |
| | | } |
| | | |
| | | /** 详æ
æ¥å£ data è§£å
*/ |
| | | export function unwrapFinReimbursementDetail(res) { |
| | | const data = res?.data ?? res; |
| | | if (!data || typeof data !== "object") return {}; |
| | | if (data.billNo != null || data.id != null || data.reimbursementType != null) { |
| | | return data; |
| | | } |
| | | const nested = data.data; |
| | | if (nested && typeof nested === "object" && !Array.isArray(nested)) { |
| | | return nested; |
| | | } |
| | | if (data.finReimbursementDto && typeof data.finReimbursementDto === "object") { |
| | | return data.finReimbursementDto; |
| | | } |
| | | return data; |
| | | } |
| | | |
| | | /** 详æ
æ¥è¯¢åæ°ï¼query finReimbursementDtoï¼ */ |
| | | export function buildFinReimbursementDetailParams(id) { |
| | | const raw = id?.id != null ? id.id : id; |
| | | const n = toNumber(raw); |
| | | return { finReimbursementDto: { id: n != null ? n : raw } }; |
| | | } |
| | | |
| | | /** 详æ
DTO â 页é¢è¡ï¼æ reimbursementType æ å°ï¼å« tasks / storageBlobVOListï¼ */ |
| | | export function mapFinReimbursementDetailRow(raw, reimbursementTypeOrModuleKey) { |
| | | const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey); |
| | | let mapped = {}; |
| | | if (type === FIN_REIMBURSEMENT_TYPE.TRAVEL) { |
| | | mapped = mapTravelReimbursementRow(raw); |
| | | } else if (type === FIN_REIMBURSEMENT_TYPE.COST) { |
| | | mapped = mapCostReimbursementRow(raw); |
| | | } else { |
| | | mapped = raw || {}; |
| | | } |
| | | |
| | | let formApprovalFlowNodes = mapNodesToFormFlow(resolveRowApiNodes(raw)); |
| | | if (!formApprovalFlowNodes.length && Array.isArray(raw?.tasks) && raw.tasks.length) { |
| | | formApprovalFlowNodes = mapNodesToFormFlow(mapTasksToFlowNodes(raw.tasks)); |
| | | } |
| | | |
| | | const enriched = applyFinReimbursementDetailEnrichment(mapped, raw); |
| | | return { |
| | | ...enriched, |
| | | approvalFlowNodes: formApprovalFlowNodes.length |
| | | ? formApprovalFlowNodes |
| | | : enriched.approvalFlowNodes, |
| | | reimbursementType: type, |
| | | reimbursementTypeLabel: reimbursementTypeLabel(type), |
| | | moduleKey: getModuleKeyByReimbursementType(type), |
| | | }; |
| | | } |
| | | |
| | | /** åæ®ç¶æ â é¡µé¢ approvalResultï¼å
¼å®¹ statusLabelï¼ */ |
| | | export function mapBillStatusToApprovalResult(billStatus) { |
| | | const upper = String(billStatus ?? "").trim().toUpperCase(); |
| | | if (upper === "DRAFT") return "draft"; |
| | | if (upper === "IN_APPROVAL") return "pending"; |
| | | if (upper === "APPROVED") return "approved"; |
| | | if (upper === "REJECTED") return "rejected"; |
| | | if (upper === "WITHDRAWN") return "cancelled"; |
| | | if (upper === "PAID") return "paid"; |
| | | return "pending"; |
| | | } |
| | | |
| | | function pickApplicantQuery(searchForm = {}) { |
| | | const kw = (searchForm.applicantKeyword || "").trim(); |
| | | if (!kw) return {}; |
| | | // å ä½ãå§åæç¼å·ãï¼å§åèµ° applicantNameï¼ç¼å·å¦ä¼ applicantCode |
| | | const out = { applicantName: kw }; |
| | | if (!/[\u4e00-\u9fa5]/.test(kw)) { |
| | | out.applicantCode = kw; |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | /** æ¯å¦åå¨å表ç鿡件ï¼ä»
ç³è¯·äººï¼ */ |
| | | export function hasActiveReimbursementSearch(searchForm = {}) { |
| | | return Boolean((searchForm?.applicantKeyword || "").trim()); |
| | | } |
| | | |
| | | /** æå¡ç«¯æªçææ¶ï¼æç³è¯·äººåå端å
åºçé */ |
| | | export function filterReimbursementRowsBySearch(rows, searchForm = {}) { |
| | | const list = Array.isArray(rows) ? rows : []; |
| | | const kw = (searchForm?.applicantKeyword || "").trim().toLowerCase(); |
| | | if (!kw) return list; |
| | | |
| | | return list.filter((row) => { |
| | | const parts = [ |
| | | row.applicantName, |
| | | row.employeeName, |
| | | row.applicantNo, |
| | | row.applicantCode, |
| | | row.employeeNo, |
| | | ] |
| | | .filter((v) => v != null && v !== "") |
| | | .map((v) => String(v).toLowerCase()); |
| | | return parts.some((p) => p.includes(kw)); |
| | | }); |
| | | } |
| | | |
| | | /** æå¹³å为 Spring GET å¯ç»å®ç queryï¼finReimbursementDto.xxxï¼å¿ç¨æ¹æ¬å·ï¼ */ |
| | | function appendDotNotationQuery(target, prefix, fields) { |
| | | if (!fields || typeof fields !== "object") return; |
| | | for (const [key, value] of Object.entries(fields)) { |
| | | if (value == null || value === "") continue; |
| | | target[`${prefix}.${key}`] = value; |
| | | } |
| | | } |
| | | |
| | | /** ç»è£
listPage æ¥è¯¢åæ°ï¼æå¹³ page.* / finReimbursementDto.*ï¼ä¸ detail æ¥å£ä¸è´ï¼ */ |
| | | export function buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType, |
| | | extraDto = {}, |
| | | }) { |
| | | const dto = { |
| | | reimbursementType, |
| | | ...pickApplicantQuery(searchForm), |
| | | ...(extraDto && typeof extraDto === "object" ? extraDto : {}), |
| | | }; |
| | | |
| | | const params = { |
| | | current: page.current, |
| | | size: page.size, |
| | | "page.current": page.current, |
| | | "page.size": page.size, |
| | | ...dto, |
| | | }; |
| | | appendDotNotationQuery(params, "finReimbursementDto", dto); |
| | | return params; |
| | | } |
| | | |
| | | function pickTravelField(obj, keys) { |
| | | if (!obj || typeof obj !== "object") return ""; |
| | | for (const key of keys) { |
| | | const v = obj[key]; |
| | | if (v != null && v !== "") return v; |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | /** å
¼å®¹ list/detail å¤ç§å·®æ
åå¯¹è±¡ç»æ */ |
| | | export function pickTravelFromRow(row) { |
| | | if (!row || typeof row !== "object") return {}; |
| | | const nested = |
| | | (row.travel && typeof row.travel === "object" ? row.travel : null) || |
| | | row.finReimbursementTravel || |
| | | row.finReimbursementTravelDto || |
| | | row.travelDto || |
| | | row.travelVO || |
| | | {}; |
| | | const src = |
| | | nested && typeof nested === "object" && Object.keys(nested).length |
| | | ? nested |
| | | : row; |
| | | return { |
| | | startTime: pickTravelField(src, [ |
| | | "startTime", |
| | | "travelStartTime", |
| | | "startDate", |
| | | "travelStartDate", |
| | | "departureTime", |
| | | ]), |
| | | endTime: pickTravelField(src, [ |
| | | "endTime", |
| | | "travelEndTime", |
| | | "endDate", |
| | | "travelEndDate", |
| | | "returnTime", |
| | | ]), |
| | | travelDays: src.travelDays, |
| | | departureCity: pickTravelField(src, [ |
| | | "departureCity", |
| | | "departurePlace", |
| | | "departure", |
| | | ]), |
| | | destinationCity: pickTravelField(src, [ |
| | | "destinationCity", |
| | | "destination", |
| | | "destinationPlace", |
| | | ]), |
| | | hotelStandard: src.hotelStandard, |
| | | lodgingDays: src.lodgingDays ?? src.hotelDays, |
| | | mealAllowance: src.mealAllowance ?? src.livingSubsidy, |
| | | transportAllowance: src.transportAllowance ?? src.transportSubsidy, |
| | | lodgingLimit: src.lodgingLimit, |
| | | withinStandard: src.withinStandard, |
| | | standardTag: src.standardTag || "", |
| | | id: src.id, |
| | | reimbursementId: src.reimbursementId, |
| | | }; |
| | | } |
| | | |
| | | /** å表/详æ
æ¶é´å±ç¤ºï¼ISO â YYYY-MM-DD HH:mm:ssï¼ */ |
| | | export function formatReimbursementDateTime(val) { |
| | | if (val == null || val === "") return ""; |
| | | const d = dayjs(val); |
| | | if (!d.isValid()) return String(val); |
| | | const raw = String(val); |
| | | const hasTime = raw.includes("T") || /:\d{2}/.test(raw); |
| | | return hasTime ? d.format("YYYY-MM-DD HH:mm:ss") : d.format("YYYY-MM-DD"); |
| | | } |
| | | |
| | | /** æ¥å£è¡ â å·®æ
æ¥éå表è¡ï¼å
¼å®¹ useTravelReimburse åæ®µï¼ */ |
| | | export function mapTravelReimbursementRow(row) { |
| | | if (!row) return {}; |
| | | const travel = pickTravelFromRow(row); |
| | | const details = Array.isArray(row.details) ? row.details : []; |
| | | |
| | | const base = { |
| | | ...row, |
| | | id: row.id, |
| | | reimbursementId: row.id, |
| | | approvalInstanceId: row.approvalInstanceId, |
| | | reimburseNo: row.billNo || "", |
| | | applicantId: row.applicantId, |
| | | applicantNo: row.applicantCode || "", |
| | | applicantName: row.applicantName || "", |
| | | employeeNo: row.applicantCode || "", |
| | | employeeName: row.applicantName || "", |
| | | applicantDeptName: row.applicantDeptName || "", |
| | | reimburseReason: row.reason || "", |
| | | travelStartTime: formatReimbursementDateTime(travel.startTime), |
| | | travelEndTime: formatReimbursementDateTime(travel.endTime), |
| | | travelDays: travel.travelDays, |
| | | departurePlace: travel.departureCity || "", |
| | | destination: travel.destinationCity || "", |
| | | hotelStandard: travel.hotelStandard, |
| | | hotelDays: travel.lodgingDays, |
| | | livingSubsidy: travel.mealAllowance, |
| | | transportSubsidy: travel.transportAllowance, |
| | | lodgingLimit: travel.lodgingLimit, |
| | | needSpecialApproval: travel.withinStandard === "0" || travel.withinStandard === 0, |
| | | standardTag: travel.standardTag || "", |
| | | applyAmount: row.applyAmount, |
| | | payee: row.payeeName || "", |
| | | payeeAccount: row.payeeAccount || "", |
| | | payeeBank: row.payeeBank || "", |
| | | billStatus: row.billStatus, |
| | | approvalResult: mapBillStatusToApprovalResult(row.billStatus), |
| | | createTime: formatReimbursementDateTime(row.createTime), |
| | | expenseDetails: details.map((d) => ({ |
| | | ...d, |
| | | expenseSubject: d.expenseCategory, |
| | | })), |
| | | travel: |
| | | row.travel && typeof row.travel === "object" && Object.keys(row.travel).length |
| | | ? row.travel |
| | | : travel, |
| | | details, |
| | | nodes: resolveRowApiNodes(row), |
| | | approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)), |
| | | tasks: row.tasks || [], |
| | | approvalFlowSummary: buildApprovalFlowSummaryForRow(row), |
| | | }; |
| | | return base; |
| | | } |
| | | |
| | | /** æ¥å£è¡ â è´¹ç¨æ¥éå表è¡ï¼å
¼å®¹ useCostReimburse åæ®µï¼ */ |
| | | export function mapCostReimbursementRow(row) { |
| | | if (!row) return {}; |
| | | const details = Array.isArray(row.details) ? row.details : []; |
| | | const apiNodes = resolveRowApiNodes(row); |
| | | const approvalFlowNodes = mapNodesToFormFlow(apiNodes); |
| | | |
| | | return { |
| | | ...row, |
| | | id: row.id, |
| | | reimbursementId: row.id, |
| | | approvalInstanceId: row.approvalInstanceId, |
| | | reimburseNo: row.billNo || "", |
| | | applicantId: row.applicantId, |
| | | applicantNo: row.applicantCode || "", |
| | | applicantName: row.applicantName || "", |
| | | employeeNo: row.applicantCode || "", |
| | | employeeName: row.applicantName || "", |
| | | applicantDeptName: row.applicantDeptName || "", |
| | | reimburseReason: row.reason || "", |
| | | expenseCategory: row.expenseType || "", |
| | | applyAmount: row.applyAmount, |
| | | applyTime: formatReimbursementDateTime(row.createTime), |
| | | payee: row.payeeName || "", |
| | | payeeAccount: row.payeeAccount || "", |
| | | bankBranch: row.payeeBank || "", |
| | | billStatus: row.billStatus, |
| | | approvalResult: mapBillStatusToApprovalResult(row.billStatus), |
| | | createTime: formatReimbursementDateTime(row.createTime), |
| | | expenseDetails: details.map((d) => ({ |
| | | ...d, |
| | | expenseSubject: d.expenseCategory, |
| | | })), |
| | | details, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | tasks: row.tasks || [], |
| | | approvalFlowSummary: buildApprovalFlowSummaryForRow({ |
| | | ...row, |
| | | nodes: apiNodes, |
| | | approvalFlowNodes, |
| | | }), |
| | | }; |
| | | } |
| | | |
| | | function toNumber(val) { |
| | | if (val == null || val === "") return undefined; |
| | | const n = Number(val); |
| | | return Number.isNaN(n) ? undefined : n; |
| | | } |
| | | |
| | | function expenseSubjectToCategory(subject) { |
| | | const hit = EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === subject); |
| | | return hit?.label || subject || ""; |
| | | } |
| | | |
| | | function expenseCategoryToType(category) { |
| | | const hit = EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === category); |
| | | return hit?.label || category || ""; |
| | | } |
| | | |
| | | /** å表/详æ
è¡ä¸ç审æ¹èç¹ï¼listPage 常ä¸è¿åï¼é详æ
è¡¥å
¨ï¼ */ |
| | | export function resolveRowApiNodes(row) { |
| | | if (!row || typeof row !== "object") return []; |
| | | const list = |
| | | row.nodes || |
| | | row.flowNodes || |
| | | row.approveNodes || |
| | | row.finReimbursementNodes || |
| | | row.nodeList || |
| | | row.reimbursementNodeList || |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | function sortFlowNodesByLevel(nodes = []) { |
| | | return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => { |
| | | const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0); |
| | | const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0); |
| | | return la - lb; |
| | | }); |
| | | } |
| | | |
| | | function formatApiNodeApproverLabel(node, index) { |
| | | if (!node || typeof node !== "object") return ""; |
| | | const approvers = Array.isArray(node.approvers) ? node.approvers : []; |
| | | const names = approvers |
| | | .map((a) => (a?.approverName || "").trim()) |
| | | .filter(Boolean); |
| | | if (names.length) return names.join("/"); |
| | | return (node.approverName || "").trim() || `èç¹${index + 1}`; |
| | | } |
| | | |
| | | /** æ¥å£ nodes â 页é¢å®¡æ¹æµï¼å审æ¹äººèç¹ï¼ */ |
| | | export function mapNodesToFormFlow(nodes = []) { |
| | | return sortFlowNodesByLevel(nodes).map((n, i) => { |
| | | const approvers = Array.isArray(n.approvers) ? n.approvers : []; |
| | | const first = approvers[0] || null; |
| | | const names = approvers |
| | | .map((a) => (a?.approverName || "").trim()) |
| | | .filter(Boolean); |
| | | return { |
| | | ...n, |
| | | nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1, |
| | | signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign", |
| | | approverId: |
| | | toNumber(first?.approverId ?? n.approverId) ?? |
| | | first?.approverId ?? |
| | | n.approverId ?? |
| | | null, |
| | | approverName: |
| | | names.join("ã") || first?.approverName || n.approverName || "", |
| | | nodeStatus: n.nodeStatus, |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | function formatTasksToFlowSummary(tasks = []) { |
| | | const list = sortFlowNodesByLevel( |
| | | (Array.isArray(tasks) ? tasks : []).map((t, i) => ({ |
| | | levelNo: t.levelNo ?? t.taskLevel ?? i + 1, |
| | | approverName: |
| | | (t.approverName || t.operatorName || t.createUserName || "").trim() || |
| | | "", |
| | | })) |
| | | ); |
| | | const parts = list.map((t) => t.approverName).filter(Boolean); |
| | | return parts.length ? parts.join(" â ") : ""; |
| | | } |
| | | |
| | | function buildApprovalFlowSummaryForRow(row) { |
| | | const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row)); |
| | | let flowNodes = |
| | | row?.approvalFlowNodes?.length > 0 |
| | | ? sortFlowNodesByLevel(row.approvalFlowNodes) |
| | | : mapNodesToFormFlow(apiNodes); |
| | | |
| | | if (!flowNodes.length && apiNodes.length) { |
| | | const line = apiNodes |
| | | .map((n, i) => formatApiNodeApproverLabel(n, i)) |
| | | .filter(Boolean) |
| | | .join(" â "); |
| | | if (line) return line; |
| | | } |
| | | |
| | | if (!flowNodes.length) { |
| | | const fromTasks = formatTasksToFlowSummary(row?.tasks); |
| | | if (fromTasks) return fromTasks; |
| | | return "â"; |
| | | } |
| | | |
| | | return flowNodes |
| | | .map((n, i) => { |
| | | const name = (n.approverName || "").trim() || `èç¹${i + 1}`; |
| | | if (n.nodeStatus === "finish") return `${name}â`; |
| | | if (n.nodeStatus === "error") return `${name}â`; |
| | | if (n.nodeStatus === "process") return `${name}â¦`; |
| | | return name; |
| | | }) |
| | | .join(" â "); |
| | | } |
| | | |
| | | /** å表ãå®¡æ¹æµç¨ãåææ¡ */ |
| | | export function formatApprovalFlowSummary(row) { |
| | | return buildApprovalFlowSummaryForRow(row); |
| | | } |
| | | |
| | | /** listPage 常ä¸å¸¦å®æ´ nodesï¼å表å è½½åç»ä¸æè¯¦æ
è¡¥å
¨å¤çº§å®¡æ¹æµç¨ */ |
| | | export async function enrichReimbursementListRowsWithApprovalFlow( |
| | | rows, |
| | | reimbursementType |
| | | ) { |
| | | const list = Array.isArray(rows) ? rows : []; |
| | | if (!list.length) return list; |
| | | |
| | | const needIds = list |
| | | .map((r) => resolveReimbursementDeleteId(r)) |
| | | .filter((id) => id != null); |
| | | |
| | | if (!needIds.length) return list; |
| | | |
| | | const detailById = new Map(); |
| | | await Promise.all( |
| | | needIds.map(async (id) => { |
| | | try { |
| | | const res = await getFinReimbursementDetail(id); |
| | | detailById.set(String(id), unwrapFinReimbursementDetail(res)); |
| | | } catch { |
| | | /* åè¡å¤±è´¥ä¸å½±åå表 */ |
| | | } |
| | | }) |
| | | ); |
| | | |
| | | const mapRow = |
| | | reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ? mapTravelReimbursementRow |
| | | : mapCostReimbursementRow; |
| | | |
| | | return list.map((row) => { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | const detail = id != null ? detailById.get(String(id)) : null; |
| | | if (!detail) return row; |
| | | const merged = { |
| | | ...row, |
| | | ...detail, |
| | | id: row.id ?? detail.id, |
| | | reimbursementId: row.reimbursementId ?? row.id ?? detail.id, |
| | | reimbursementType: detail.reimbursementType ?? row.reimbursementType, |
| | | }; |
| | | return mapRow(merged); |
| | | }); |
| | | } |
| | | |
| | | /** 表åä¸çå®¡æ¹æµï¼å
¼å®¹ approvalFlowNodes / nodes / flowNodesï¼ */ |
| | | export function resolveFormApprovalFlowNodes(form) { |
| | | const list = |
| | | form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | /** 页é¢å®¡æ¹èç¹ â æ¥å£ nodes */ |
| | | export function mapApprovalFlowNodesToApi(nodes = [], templateId) { |
| | | const list = Array.isArray(nodes) ? nodes : []; |
| | | return list |
| | | .map((n, i) => { |
| | | let approvers = []; |
| | | if (Array.isArray(n.approvers) && n.approvers.length) { |
| | | approvers = n.approvers |
| | | .filter((a) => a?.approverId != null && a.approverId !== "") |
| | | .map((a, idx) => { |
| | | const item = { |
| | | approverId: toNumber(a.approverId) ?? a.approverId, |
| | | approverName: a.approverName || "", |
| | | sortNo: a.sortNo ?? idx + 1, |
| | | }; |
| | | if (a.id != null) item.id = a.id; |
| | | if (a.nodeId != null) item.nodeId = a.nodeId; |
| | | if (a.templateId != null) item.templateId = a.templateId; |
| | | else if (templateId != null) item.templateId = templateId; |
| | | if (a.roleKey) item.roleKey = a.roleKey; |
| | | return item; |
| | | }); |
| | | } else if (n.approverId != null && n.approverId !== "") { |
| | | const item = { |
| | | approverId: toNumber(n.approverId) ?? n.approverId, |
| | | approverName: n.approverName || "", |
| | | sortNo: 1, |
| | | }; |
| | | if (n.roleKey) item.roleKey = n.roleKey; |
| | | approvers = [item]; |
| | | } |
| | | if (!approvers.length) return null; |
| | | |
| | | const node = { |
| | | levelNo: n.levelNo ?? n.nodeOrder ?? n.sortOrder ?? i + 1, |
| | | approveType: n.approveType || mapSignModeToApi(n.signMode), |
| | | approvers, |
| | | }; |
| | | if (n.id != null) node.id = n.id; |
| | | if (n.templateId != null) node.templateId = n.templateId; |
| | | else if (templateId != null) node.templateId = templateId; |
| | | if (n.roleKey) node.roleKey = n.roleKey; |
| | | return node; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | |
| | | /** ä¿ååæ ¡éª nodes å·²é
ç½® */ |
| | | export function validateReimbursementApprovalNodes(dto) { |
| | | if (Array.isArray(dto?.nodes) && dto.nodes.length > 0) { |
| | | return { ok: true }; |
| | | } |
| | | return { ok: false, message: "请é
ç½®å®¡æ¹æµç¨å¹¶éæ©å®¡æ¹äºº" }; |
| | | } |
| | | |
| | | function mapDetailsToApi(details = []) { |
| | | return (details || []).map((d, i) => { |
| | | const item = { |
| | | rowNo: d.rowNo ?? i + 1, |
| | | invoiceDate: d.invoiceDate || undefined, |
| | | expenseCategory: expenseSubjectToCategory(d.expenseSubject ?? d.expenseCategory), |
| | | amount: toNumber(d.amount), |
| | | description: d.description || "", |
| | | invoiceNo: d.invoiceNo || undefined, |
| | | invoiceType: d.invoiceType || undefined, |
| | | invoiceAmount: toNumber(d.invoiceAmount), |
| | | taxRate: toNumber(d.taxRate), |
| | | taxAmount: toNumber(d.taxAmount), |
| | | remark: d.remark || undefined, |
| | | }; |
| | | if (d.id != null && !String(d.id).startsWith("ed_")) { |
| | | item.id = toNumber(d.id) ?? d.id; |
| | | } |
| | | if (d.reimbursementId != null) item.reimbursementId = toNumber(d.reimbursementId); |
| | | return item; |
| | | }); |
| | | } |
| | | |
| | | function sumDetailAmount(details = []) { |
| | | const sum = (details || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); |
| | | return Math.round(sum * 100) / 100; |
| | | } |
| | | |
| | | /** 表åéä»¶å表ï¼å
¼å®¹å¤ç§å段åï¼ */ |
| | | export function resolveFormAttachmentList(form) { |
| | | const list = |
| | | form?.attachmentList ?? |
| | | form?.storageBlobDTOs ?? |
| | | form?.storageBlobVOList ?? |
| | | form?.invoiceAttachments ?? |
| | | []; |
| | | return Array.isArray(list) ? list : []; |
| | | } |
| | | |
| | | /** 页é¢éä»¶ â ä¿å DTOï¼storageBlobVOList / storageBlobDTOsï¼ */ |
| | | export function mapFormAttachmentsToApi(list = [], reimbursementId) { |
| | | const rid = |
| | | reimbursementId != null |
| | | ? toNumber(reimbursementId) ?? reimbursementId |
| | | : undefined; |
| | | |
| | | return (list || []) |
| | | .map((item, i) => { |
| | | if (!item) return null; |
| | | const url = |
| | | item.url || |
| | | item.fileUrl || |
| | | item.downloadUrl || |
| | | item.downloadURL || |
| | | item.previewUrl || |
| | | item.previewURL || |
| | | item.link || |
| | | ""; |
| | | const name = |
| | | item.fileName || |
| | | item.originalFilename || |
| | | item.originalFileName || |
| | | item.blobName || |
| | | item.name || |
| | | `éä»¶${i + 1}`; |
| | | |
| | | const idRaw = item.id ?? item.blobId; |
| | | const isTempId = |
| | | idRaw != null && |
| | | /^(inv_|att_|ed_|local_)/.test(String(idRaw)); |
| | | |
| | | if (!url && (idRaw == null || isTempId)) return null; |
| | | |
| | | const blob = { |
| | | fileName: name, |
| | | originalFilename: name, |
| | | fileUrl: url || undefined, |
| | | url: url || undefined, |
| | | }; |
| | | |
| | | if (idRaw != null && !isTempId) { |
| | | const n = toNumber(idRaw); |
| | | blob.id = n != null ? n : idRaw; |
| | | blob.blobId = blob.id; |
| | | } |
| | | if (rid != null) blob.reimbursementId = rid; |
| | | return blob; |
| | | }) |
| | | .filter(Boolean); |
| | | } |
| | | |
| | | function applyStorageBlobsToSaveDto(dto, form) { |
| | | const blobs = mapFormAttachmentsToApi( |
| | | resolveFormAttachmentList(form), |
| | | dto?.id ?? form?.reimbursementId ?? form?.id |
| | | ); |
| | | if (blobs.length) { |
| | | dto.storageBlobVOList = blobs; |
| | | dto.storageBlobDTOs = blobs; |
| | | } |
| | | return dto; |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¶è¡¥é½ä¸»è¡¨ä¸å表å
³è ID */ |
| | | function applyReimbursementRelations(dto) { |
| | | const rid = dto?.id; |
| | | if (rid == null) return dto; |
| | | if (dto.travel && typeof dto.travel === "object") { |
| | | dto.travel.reimbursementId = rid; |
| | | } |
| | | if (Array.isArray(dto.details)) { |
| | | dto.details.forEach((d) => { |
| | | d.reimbursementId = rid; |
| | | }); |
| | | } |
| | | const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray); |
| | | blobLists.forEach((list) => { |
| | | list.forEach((b) => { |
| | | b.reimbursementId = rid; |
| | | }); |
| | | }); |
| | | return dto; |
| | | } |
| | | |
| | | function resolveReimbursementId(form) { |
| | | const rawId = form?.reimbursementId ?? form?.id; |
| | | if (rawId == null || rawId === "" || String(rawId).startsWith("local_")) { |
| | | return undefined; |
| | | } |
| | | return toNumber(rawId) ?? rawId; |
| | | } |
| | | |
| | | /** å·®æ
表å â FinReimbursementDto */ |
| | | export function buildTravelReimbursementSaveDto(form, { computeTravelDays } = {}) { |
| | | const details = mapDetailsToApi(form.expenseDetails); |
| | | const detailTotal = sumDetailAmount(form.expenseDetails); |
| | | const applyAmount = toNumber(form.applyAmount) ?? detailTotal; |
| | | const travelDays = |
| | | form.travelDays != null |
| | | ? toNumber(form.travelDays) |
| | | : computeTravelDays?.(form.travelStartTime, form.travelEndTime); |
| | | |
| | | const dto = { |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | | expenseType: "å·®æ
è´¹", |
| | | applicantId: toNumber(form.applicantId), |
| | | applicantCode: form.employeeNo || form.applicantNo || "", |
| | | applicantName: form.employeeName || form.applicantName || "", |
| | | applicantDeptId: toNumber(form.applicantDeptId), |
| | | applicantDeptName: form.applicantDeptName || form.deptName || "", |
| | | reason: (form.reimburseReason || "").trim(), |
| | | applyAmount, |
| | | detailTotalAmount: detailTotal, |
| | | payeeName: form.payee || "", |
| | | payeeAccount: form.payeeAccount || undefined, |
| | | payeeBank: form.payeeBank || undefined, |
| | | billStatus: "IN_APPROVAL", |
| | | deptId: toNumber(form.deptId), |
| | | travel: { |
| | | startTime: form.travelStartTime || undefined, |
| | | endTime: form.travelEndTime || undefined, |
| | | travelDays, |
| | | departureCity: form.departurePlace || "", |
| | | destinationCity: form.destination || "", |
| | | hotelStandard: toNumber(form.hotelStandard), |
| | | lodgingDays: toNumber(form.hotelDays), |
| | | mealAllowance: toNumber(form.livingSubsidy), |
| | | transportAllowance: toNumber(form.transportSubsidy), |
| | | lodgingLimit: toNumber(form.lodgingLimit), |
| | | standardTag: form.standardTag || (form.needSpecialApproval ? "è¶
æ ç¹æ¹" : "卿 åèå´å
"), |
| | | withinStandard: form.needSpecialApproval ? "0" : "1", |
| | | }, |
| | | details, |
| | | nodes: mapApprovalFlowNodesToApi( |
| | | resolveFormApprovalFlowNodes(form), |
| | | form.templateId |
| | | ), |
| | | }; |
| | | |
| | | const id = resolveReimbursementId(form); |
| | | if (id != null) dto.id = id; |
| | | if (form.billNo || form.reimburseNo) { |
| | | dto.billNo = form.billNo || form.reimburseNo; |
| | | } |
| | | if (form.approvalInstanceId != null) { |
| | | dto.approvalInstanceId = toNumber(form.approvalInstanceId); |
| | | } |
| | | if (form.approveProcessId != null) { |
| | | dto.approveProcessId = toNumber(form.approveProcessId); |
| | | } |
| | | if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id); |
| | | |
| | | applyStorageBlobsToSaveDto(dto, form); |
| | | return applyReimbursementRelations(dto); |
| | | } |
| | | |
| | | /** è´¹ç¨è¡¨å â FinReimbursementDto */ |
| | | export function buildCostReimbursementSaveDto(form) { |
| | | const details = mapDetailsToApi(form.expenseDetails); |
| | | const detailTotal = sumDetailAmount(form.expenseDetails); |
| | | const applyAmount = toNumber(form.applyAmount) ?? detailTotal; |
| | | |
| | | const dto = { |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, |
| | | expenseType: expenseCategoryToType(form.expenseCategory), |
| | | applicantId: toNumber(form.applicantId), |
| | | applicantCode: form.employeeNo || form.applicantNo || "", |
| | | applicantName: form.employeeName || form.applicantName || "", |
| | | applicantDeptId: toNumber(form.applicantDeptId), |
| | | applicantDeptName: form.applicantDeptName || form.deptName || "", |
| | | reason: (form.reimburseReason || "").trim(), |
| | | applyAmount, |
| | | detailTotalAmount: detailTotal, |
| | | payeeName: form.payee || "", |
| | | payeeAccount: form.payeeAccount || "", |
| | | payeeBank: form.bankBranch || form.payeeBank || "", |
| | | billStatus: "IN_APPROVAL", |
| | | deptId: toNumber(form.deptId), |
| | | details, |
| | | nodes: mapApprovalFlowNodesToApi( |
| | | resolveFormApprovalFlowNodes(form), |
| | | form.templateId |
| | | ), |
| | | }; |
| | | |
| | | const id = resolveReimbursementId(form); |
| | | if (id != null) dto.id = id; |
| | | if (form.billNo || form.reimburseNo) { |
| | | dto.billNo = form.billNo || form.reimburseNo; |
| | | } |
| | | if (form.approvalInstanceId != null) { |
| | | dto.approvalInstanceId = toNumber(form.approvalInstanceId); |
| | | } |
| | | if (form.approveProcessId != null) { |
| | | dto.approveProcessId = toNumber(form.approveProcessId); |
| | | } |
| | | |
| | | applyStorageBlobsToSaveDto(dto, form); |
| | | return applyReimbursementRelations(dto); |
| | | } |
| | | |
| | | /** å表è¡ä¸»é®ï¼å é¤/ä¿®æ¹ç¨ fin_reimbursement.idï¼ */ |
| | | export function resolveReimbursementDeleteId(row) { |
| | | const raw = row?.reimbursementId ?? row?.id; |
| | | if (raw == null || raw === "" || String(raw).startsWith("local_")) { |
| | | return undefined; |
| | | } |
| | | const n = toNumber(raw); |
| | | return n != null ? n : raw; |
| | | } |
| | | |
| | | /** æ¯å¦å
许å é¤ï¼å®¡æ¹ä¸ãå·²éè¿ã已仿¬¾ä¸å¯å ï¼ */ |
| | | export function canDeleteReimbursementRow(row) { |
| | | const key = mapBillStatusToApprovalResult( |
| | | row?.billStatus ?? row?.approvalResult ?? row?.status |
| | | ); |
| | | return key !== "pending" && key !== "approved" && key !== "paid"; |
| | | } |
| | | |
| | | /** æ¯å¦å
许ç¼è¾ï¼ä¸å é¤è§åä¸è´ï¼ */ |
| | | export function canEditReimbursementRow(row) { |
| | | return canDeleteReimbursementRow(row); |
| | | } |
| | | |
| | | /** ä¿®æ¹åºæ¯å¿
é¡»å¸¦ä¸»é® ID */ |
| | | export function validateReimbursementPersistDto(dto, isEdit) { |
| | | if (!isEdit) return { ok: true }; |
| | | if (dto?.id != null && dto.id !== "") return { ok: true }; |
| | | return { ok: false, message: "æ æ³ä¿®æ¹ï¼ç¼ºå°æ¥éå ID" }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { matchBusinessTypeValue } from "../../ApproveManage/approve-list/approveListConstants.js"; |
| | | import { |
| | | APPROVAL_MODULE_KEYS, |
| | | getApprovalModuleConfig, |
| | | } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js"; |
| | | import { |
| | | getModuleKeyByReimbursementType, |
| | | mapFinReimbursementDetailRow, |
| | | resolveReimbursementType, |
| | | unwrapFinReimbursementDetail, |
| | | } from "./finReimbursementMappers.js"; |
| | | |
| | | export const REIMBURSE_EDIT_FROM_APPROVE_KEY = "oa_reimburse_edit_from_approve"; |
| | | |
| | | const REIMBURSE_MODULE_KEYS = [ |
| | | APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE, |
| | | APPROVAL_MODULE_KEYS.COST_REIMBURSE, |
| | | ]; |
| | | |
| | | /** 审æ¹å®ä¾æ¯å¦å·®æ
/è´¹ç¨æ¥é */ |
| | | export function inferReimburseModuleKeyFromInstance(row) { |
| | | if (!row) return ""; |
| | | for (const moduleKey of REIMBURSE_MODULE_KEYS) { |
| | | const cfg = getApprovalModuleConfig(moduleKey); |
| | | if (!cfg) continue; |
| | | if ( |
| | | cfg.businessType != null && |
| | | cfg.businessType !== "" && |
| | | matchBusinessTypeValue(row.businessType, cfg.businessType) |
| | | ) { |
| | | return moduleKey; |
| | | } |
| | | if (matchBusinessTypeValue(row.businessType, cfg.approvalType)) { |
| | | return moduleKey; |
| | | } |
| | | const text = `${row.templateName || ""}${row.title || ""}${row.businessName || ""}`; |
| | | if ((cfg.typeLabels || []).some((l) => l && text.includes(l))) { |
| | | return moduleKey; |
| | | } |
| | | } |
| | | return ""; |
| | | } |
| | | |
| | | export function isReimburseApprovalInstance(row) { |
| | | return Boolean(inferReimburseModuleKeyFromInstance(row)); |
| | | } |
| | | |
| | | /** 审æ¹å®ä¾å
³èç fin_reimbursement.id */ |
| | | export function resolveFinReimbursementIdFromInstance(row) { |
| | | const raw = row?.businessId ?? row?.formPayload?.reimbursementId; |
| | | if (raw == null || raw === "") return undefined; |
| | | const n = Number(raw); |
| | | return Number.isNaN(n) ? raw : n; |
| | | } |
| | | |
| | | /** æåæ¥é详æ
å¹¶æ å°ä¸ºå·®æ
/è´¹ç¨é¡µé¢è¡ï¼ä»¥æ¥å£ reimbursementType 为åï¼ */ |
| | | export async function loadReimburseDetailForInstance(instanceRow, moduleKey) { |
| | | const mk = moduleKey || inferReimburseModuleKeyFromInstance(instanceRow); |
| | | const id = resolveFinReimbursementIdFromInstance(instanceRow); |
| | | if (id == null) { |
| | | throw new Error("missing reimbursement id"); |
| | | } |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | const reimburseRow = mapFinReimbursementDetailRow(raw, mk); |
| | | const reimbursementType = resolveReimbursementType(raw, mk); |
| | | const resolvedMk = |
| | | getModuleKeyByReimbursementType(reimbursementType) || mk; |
| | | return { |
| | | reimburseRow, |
| | | instanceRow, |
| | | moduleKey: resolvedMk, |
| | | reimbursementType, |
| | | }; |
| | | } |
| | | |
| | | export function stashReimburseEditFromApprove(moduleKey, reimbursementId) { |
| | | sessionStorage.setItem( |
| | | REIMBURSE_EDIT_FROM_APPROVE_KEY, |
| | | JSON.stringify({ moduleKey, reimbursementId }) |
| | | ); |
| | | } |
| | | |
| | | export function consumeReimburseEditFromApprove() { |
| | | const raw = sessionStorage.getItem(REIMBURSE_EDIT_FROM_APPROVE_KEY); |
| | | if (!raw) return null; |
| | | sessionStorage.removeItem(REIMBURSE_EDIT_FROM_APPROVE_KEY); |
| | | try { |
| | | return JSON.parse(raw); |
| | | } catch { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** ä»å·²æ³¨åè·¯ç±è§£æå·®æ
/è´¹ç¨æ¥éèå pathï¼é¿å
åæ» path å¯¼è´ 404ï¼ */ |
| | | export function resolveReimburseManageRoutePath(router, moduleKey) { |
| | | if (!router?.getRoutes) return ""; |
| | | const needle = |
| | | moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE |
| | | ? "travel-reimburse" |
| | | : moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE |
| | | ? "cost-reimburse" |
| | | : ""; |
| | | if (!needle) return ""; |
| | | const labelHint = |
| | | moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE ? "å·®æ
" : "è´¹ç¨"; |
| | | const hit = router.getRoutes().find((r) => { |
| | | const path = r.path || ""; |
| | | if (path.includes(needle)) return true; |
| | | const title = r.meta?.title || ""; |
| | | return title.includes(labelHint) && title.includes("æ¥é"); |
| | | }); |
| | | return hit?.path || ""; |
| | | } |
| | | |
| | | export async function navigateToReimburseManageForEdit(router, moduleKey, reimbursementId) { |
| | | stashReimburseEditFromApprove(moduleKey, reimbursementId); |
| | | const path = resolveReimburseManageRoutePath(router, moduleKey); |
| | | if (!path) { |
| | | throw new Error("route not found"); |
| | | } |
| | | await router.push(path); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å·®æ
æ¥éï¼å®¡æ¹æµç¨è¿åº¦å±ç¤º --> |
| | | <template> |
| | | <el-steps :active="activeStep" finish-status="success" align-center> |
| | | <el-step |
| | | v-for="(node, index) in sortedNodes" |
| | | :key="index" |
| | | :title="`èç¹ ${index + 1}`" |
| | | :description="stepDescription(node)" |
| | | :status="stepStatus(node, index)" |
| | | /> |
| | | </el-steps> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | nodes: { type: Array, default: () => [] }, |
| | | currentIndex: { type: Number, default: 0 }, |
| | | }); |
| | | |
| | | const sortedNodes = computed(() => { |
| | | const list = props.nodes || []; |
| | | return [...list].sort((a, b) => (a.sortOrder ?? a.nodeOrder ?? 0) - (b.sortOrder ?? b.nodeOrder ?? 0)); |
| | | }); |
| | | |
| | | const activeStep = computed(() => { |
| | | const list = sortedNodes.value; |
| | | if (!list.length) return 0; |
| | | const finished = list.filter((n) => n.nodeStatus === "finish").length; |
| | | const hasError = list.some((n) => n.nodeStatus === "error"); |
| | | if (hasError) return Math.max(0, props.currentIndex); |
| | | return finished; |
| | | }); |
| | | |
| | | function stepDescription(node) { |
| | | const name = (node.approverName || "").trim() || "æªæå®"; |
| | | const opinion = (node.approveOpinion || "").trim(); |
| | | if (opinion) return `${name}ï¼${opinion}`; |
| | | return name; |
| | | } |
| | | |
| | | function stepStatus(node, index) { |
| | | if (node.nodeStatus === "error") return "error"; |
| | | if (node.nodeStatus === "finish") return "success"; |
| | | if (node.nodeStatus === "process" || index === props.currentIndex) return "process"; |
| | | return "wait"; |
| | | } |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- å·®æ
æ¥éï¼è¯¦æ
åªè¯»é¢æ¿ --> |
| | | <template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="æ¥éåå·">{{ row.reimburseNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¶æ"> |
| | | <el-tag :type="statusTagType(row.approvalResult)" size="small">{{ statusLabel(row.approvalResult) }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="åå·¥ç¼å·">{{ row.employeeNo || row.applicantNo || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå·¥å§å">{{ row.employeeName || row.applicantName || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥éåå " :span="2">{{ row.reimburseReason || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åºå·®å¼å§">{{ row.travelStartTime || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åºå·®ç»æ">{{ row.travelEndTime || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="åºå·®å°">{{ row.departurePlace || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç®çå°">{{ row.destination || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="é
åºæ å">{{ row.hotelStandard != null ? `${row.hotelStandard} å
/æ` : "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä½å®¿å¤©æ°">{{ row.hotelDays ?? "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="çæ´»è¡¥è´´">{{ row.livingSubsidy != null ? `${row.livingSubsidy} å
` : "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç³è¯·éé¢">{{ row.applyAmount != null ? `${row.applyAmount} å
` : "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¶æ¬¾äºº">{{ row.payee || "â" }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¹æ¹"> |
| | | <el-tag :type="row.needSpecialApproval ? 'danger' : 'info'" size="small"> |
| | | {{ row.needSpecialApproval ? "è¶
æ¯éç¹æ¹" : "æ åå
" }} |
| | | </el-tag> |
| | | </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> |
| | | |
| | | <el-divider content-position="left">æ¥éæç»</el-divider> |
| | | <el-table v-if="row.expenseDetails?.length" :data="row.expenseDetails" border size="small"> |
| | | <el-table-column type="index" label="åºå·" width="55" align="center" /> |
| | | <el-table-column prop="invoiceDate" label="åç¥¨æ¥æ" width="120" /> |
| | | <el-table-column label="è´¹ç¨ç§ç®" width="100"> |
| | | <template #default="{ row: d }">{{ expenseSubjectLabel(d.expenseSubject) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="amount" label="éé¢" width="100" /> |
| | | <el-table-column prop="description" label="æè¿°" min-width="140" show-overflow-tooltip /> |
| | | </el-table> |
| | | <el-empty v-else description="ææ æç»" :image-size="48" /> |
| | | |
| | | <el-divider content-position="left">å票éä»¶</el-divider> |
| | | <template v-if="attachmentFiles.length"> |
| | | <el-tag v-for="(f, i) in attachmentFiles" :key="i" class="file-tag" type="info" @click="openFile(f)"> |
| | | {{ f.name }} |
| | | </el-tag> |
| | | </template> |
| | | <el-empty v-else description="ææ éä»¶" :image-size="48" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | import { expenseSubjectLabel, statusLabel, statusTagType } from "../travelReimburseUtils.js"; |
| | | |
| | | const props = defineProps({ |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const attachmentFiles = computed(() => { |
| | | const list = |
| | | props.row?.attachmentList || |
| | | props.row?.storageBlobVOList || |
| | | props.row?.invoiceAttachments; |
| | | return Array.isArray(list) ? list : []; |
| | | }); |
| | | |
| | | function openFile(f) { |
| | | const url = f?.url || f?.downloadURL || f?.previewURL; |
| | | if (url) window.open(url, "_blank"); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .reject-text { |
| | | color: var(--el-color-danger); |
| | | } |
| | | .file-tag { |
| | | margin: 0 8px 8px 0; |
| | | cursor: pointer; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼å·®æ
æ¥éï¼å表 /finReimbursement/listPageï¼reimbursementType=1ï¼--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">ç³è¯·äººï¼</span> |
| | | <el-input |
| | | v-model="searchForm.applicantKeyword" |
| | | style="width: 220px" |
| | | placeholder="å§åæç¼å·" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <el-button type="primary" style="margin-left: 10px" @click="handleQuery">æç´¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div class="search_actions"> |
| | | <el-button type="success" plain @click="handleImportClick">导å
¥</el-button> |
| | | <el-button type="warning" plain @click="handleExport">导åº</el-button> |
| | | <el-button type="primary" @click="openFormDialog('add')">æ°å¢å·®æ
æ¥é</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" /> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="false" |
| | | :tableLoading="tableLoading" |
| | | :total="page.total" |
| | | @pagination="pagination" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- æ°å¢ / ç¼è¾ --> |
| | | <el-dialog |
| | | v-model="formDialog.visible" |
| | | :title="formDialog.title" |
| | | width="1120px" |
| | | append-to-body |
| | | destroy-on-close |
| | | class="travel-reimburse-form-dialog" |
| | | @closed="onFormClosed" |
| | | > |
| | | <el-alert |
| | | v-if="budgetHint.visible" |
| | | :title="budgetHint.title" |
| | | :type="budgetHint.type" |
| | | :description="budgetHint.description" |
| | | show-icon |
| | | :closable="false" |
| | | class="mb16" |
| | | /> |
| | | <el-alert v-if="overBudgetWarnings.length" type="warning" show-icon :closable="false" class="mb16"> |
| | | <template #title>å·®æ
æ åè¶
æ¯æéï¼éç¹æ¹ï¼</template> |
| | | <ul class="warn-list"> |
| | | <li v-for="(w, i) in overBudgetWarnings" :key="i">{{ w }}</li> |
| | | </ul> |
| | | </el-alert> |
| | | |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="formRules" |
| | | label-width="120px" |
| | | class="travel-reimburse-form" |
| | | :disabled="formDialog.readonly" |
| | | > |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">åºæ¬ä¿¡æ¯</span></template> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥ç¼å·"> |
| | | <el-input v-model="form.employeeNo" readonly placeholder="éæ©åå·¥åèªå¨å¸¦åº" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥å§å" prop="applicantId"> |
| | | <el-select |
| | | v-model="form.applicantId" |
| | | filterable |
| | | remote |
| | | clearable |
| | | reserve-keyword |
| | | placeholder="è¯·éæ©ææç´¢åå·¥" |
| | | style="width: 100%" |
| | | :remote-method="remoteSearchApplicantForm" |
| | | :loading="applicantFormSearchLoading" |
| | | @change="onApplicantChange" |
| | | > |
| | | <el-option |
| | | v-for="u in applicantFormOptions" |
| | | :key="u.userId" |
| | | :label="userSelectLabel(u)" |
| | | :value="u.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="æ¥éåå " prop="reimburseReason"> |
| | | <el-input |
| | | v-model="form.reimburseReason" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请填ååºå·®åæ¥éåå " |
| | | maxlength="2000" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åºå·®å¼å§" prop="travelStartTime"> |
| | | <el-date-picker |
| | | v-model="form.travelStartTime" |
| | | type="datetime" |
| | | placeholder="å¼å§æ¶é´" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | style="width: 100%" |
| | | @change="onTravelRangeChange" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åºå·®ç»æ" prop="travelEndTime"> |
| | | <el-date-picker |
| | | v-model="form.travelEndTime" |
| | | type="datetime" |
| | | placeholder="ç»ææ¶é´" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | style="width: 100%" |
| | | @change="onTravelRangeChange" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="åºå·®å¤©æ°"> |
| | | <el-input :model-value="travelDaysDisplay" readonly> |
| | | <template #append>天</template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="åºå·®å°" prop="departurePlace"> |
| | | <el-input v-model="form.departurePlace" placeholder="åºååå¸" @blur="recalcTravelStandards" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="ç®çå°" prop="destination"> |
| | | <el-input v-model="form.destination" placeholder="ç®çåå¸" @blur="recalcTravelStandards" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header-row"> |
| | | <span class="card-header-title">å·®æ
æ å</span> |
| | | <el-text type="info" size="small">{{ travelTierLabel }} · çæ´»è¡¥è´´å»ºè®® {{ suggestedLivingSubsidy }} å
</el-text> |
| | | </div> |
| | | </template> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="é
åºæ å"> |
| | | <el-input-number |
| | | v-model="form.hotelStandard" |
| | | :min="0" |
| | | :precision="2" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | @change="recalcTravelStandards" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="ä½å®¿å¤©æ°"> |
| | | <el-input-number |
| | | v-model="form.hotelDays" |
| | | :min="0" |
| | | :max="365" |
| | | :precision="0" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | @change="recalcTravelStandards" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="çæ´»è¡¥è´´"> |
| | | <el-input-number |
| | | v-model="form.livingSubsidy" |
| | | :min="0" |
| | | :precision="2" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | @change="recalcTravelStandards" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="交é补贴"> |
| | | <el-input :model-value="String(suggestedTransportSubsidy)" readonly><template #append>å
</template></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="ä½å®¿éé¢"> |
| | | <el-input :model-value="String(suggestedHotelLimit)" readonly><template #append>å
</template></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="ç¹æ¹æ è®°"> |
| | | <el-tag :type="form.needSpecialApproval ? 'danger' : 'success'" effect="plain"> |
| | | {{ form.needSpecialApproval ? "è¶
æ¯éç¹æ¹" : "卿 åèå´å
" }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">éé¢ä¸æ¶æ¬¾</span></template> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç³è¯·éé¢" prop="applyAmount"> |
| | | <div class="amount-row"> |
| | | <el-input-number v-model="form.applyAmount" :min="0" :precision="2" controls-position="right" class="amount-input" /> |
| | | <el-button v-if="!formDialog.readonly" type="primary" link @click="syncApplyAmountFromDetails"> |
| | | ææç»æ±æ» {{ detailTotalAmount }} å
|
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¶æ¬¾äºº" prop="payee"> |
| | | <el-input v-model="form.payee" placeholder="请è¾å
¥æ¶æ¬¾äºº" maxlength="50" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header-row"> |
| | | <span class="card-header-title">æ¥éæç»</span> |
| | | <el-button v-if="!formDialog.readonly" type="primary" plain size="small" @click="addExpenseDetail">æ°å¢æç»</el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="form.expenseDetails" border size="small" class="detail-table"> |
| | | <el-table-column type="index" label="åºå·" width="55" align="center" /> |
| | | <el-table-column label="åç¥¨æ¥æ" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-if="!formDialog.readonly" |
| | | v-model="row.invoiceDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | size="small" |
| | | style="width: 100%" |
| | | /> |
| | | <span v-else>{{ row.invoiceDate || "â" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è´¹ç¨ç§ç®" width="130"> |
| | | <template #default="{ row }"> |
| | | <el-select |
| | | v-if="!formDialog.readonly" |
| | | v-model="row.expenseSubject" |
| | | size="small" |
| | | style="width: 100%" |
| | | @change="recalcTravelStandards" |
| | | > |
| | | <el-option |
| | | v-for="opt in EXPENSE_SUBJECT_OPTIONS" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | <span v-else>{{ expenseSubjectLabel(row.expenseSubject) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="éé¢" width="120"> |
| | | <template #default="{ row }"> |
| | | <el-input-number |
| | | v-if="!formDialog.readonly" |
| | | v-model="row.amount" |
| | | :min="0" |
| | | :precision="2" |
| | | size="small" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | @change="onDetailAmountChange" |
| | | /> |
| | | <span v-else>{{ row.amount ?? "â" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æè¿°" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input v-if="!formDialog.readonly" v-model="row.description" size="small" placeholder="说æ" /> |
| | | <span v-else>{{ row.description || "â" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="!formDialog.readonly" label="æä½" width="70" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button type="danger" link size="small" @click="removeExpenseDetail($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">éä»¶ï¼å票ï¼</span></template> |
| | | <el-form-item label-width="0" class="attachment-form-item"> |
| | | <div class="upload-block"> |
| | | <FileUpload v-model:file-list="form.attachmentList" :limit="20" button-text="ç¹å»éæ©æä»¶" /> |
| | | </div> |
| | | </el-form-item> |
| | | </el-card> |
| | | |
| | | <el-card class="form-section" shadow="never"> |
| | | <template #header><span class="card-header-title">å®¡æ¹æµç¨</span></template> |
| | | <el-form-item prop="approvalFlowNodes" label-width="0"> |
| | | <ApprovalFlowEditor |
| | | v-if="!formDialog.readonly" |
| | | v-model="form.approvalFlowNodes" |
| | | :user-options="flowUserOptions" |
| | | @update:model-value="onApprovalFlowChange" |
| | | /> |
| | | <ApprovalFlowProgress v-else :nodes="form.approvalFlowNodes" :current-index="form.currentNodeIndex" /> |
| | | <p v-if="!formDialog.readonly" class="flow-tip">è³å°ä¿çä¸ä¸ªèç¹ï¼å®¡æ ¸ä¸ãå·²éè¿çåæ®ä¸å¯ç¼è¾ã</p> |
| | | </el-form-item> |
| | | </el-card> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button |
| | | v-if="!formDialog.readonly" |
| | | type="primary" |
| | | :loading="submitSaving" |
| | | @click="submitForm" |
| | | > |
| | | æ 交 |
| | | </el-button> |
| | | <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "å
³ é" : "å æ¶" }}</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 详æ
--> |
| | | <el-dialog v-model="detailDialog.visible" title="å·®æ
æ¥é详æ
" width="900px" append-to-body destroy-on-close> |
| | | <div v-loading="detailLoading"> |
| | | <DetailPanel :row="detailRow" /> |
| | | <ApprovalFlowProgress |
| | | class="mt16" |
| | | :nodes="detailRow.approvalFlowProgressNodes ?? 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> |
| | | <template #footer> |
| | | <el-button type="primary" @click="detailDialog.visible = false">å
³ é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å®¡æ¹ --> |
| | | <el-dialog |
| | | v-model="approveDialog.visible" |
| | | title="å·®æ
æ¥é审æ¹" |
| | | width="1000px" |
| | | append-to-body |
| | | destroy-on-close |
| | | @closed="approveOpinion = ''" |
| | | > |
| | | <DetailPanel :row="approveDialog.row" /> |
| | | <el-divider content-position="left">æµç¨è¿åº¦</el-divider> |
| | | <ApprovalFlowProgress |
| | | :nodes="approveDialog.row?.approvalFlowProgressNodes ?? approveDialog.row?.approvalFlowNodes" |
| | | :current-index="approveDialog.row?.currentNodeIndex ?? 0" |
| | | /> |
| | | <el-form label-width="100px" class="mt16"> |
| | | <el-form-item label="å®¡æ¹æè§"> |
| | | <el-input |
| | | v-model="approveOpinion" |
| | | type="textarea" |
| | | :rows="3" |
| | | maxlength="500" |
| | | show-word-limit |
| | | placeholder="éè¿å¯ç空ï¼é©³å请填ååå " |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="success" @click="submitApprove('approved')">é è¿</el-button> |
| | | <el-button type="danger" @click="submitApprove('rejected')">驳 å</el-button> |
| | | <el-button @click="approveDialog.visible = false">å æ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import FileUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue"; |
| | | import ApprovalFlowProgress from "./components/ApprovalFlowProgress.vue"; |
| | | import DetailPanel from "./components/DetailPanel.vue"; |
| | | import { useTravelReimburse } from "./useTravelReimburse.js"; |
| | | |
| | | const tr = useTravelReimburse(); |
| | | const { |
| | | Search, |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | importInputRef, |
| | | formRef, |
| | | form, |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | applicantFormSearchLoading, |
| | | applicantFormOptions, |
| | | flowUserOptions, |
| | | travelDaysDisplay, |
| | | travelTierLabel, |
| | | suggestedLivingSubsidy, |
| | | suggestedTransportSubsidy, |
| | | suggestedHotelLimit, |
| | | detailTotalAmount, |
| | | overBudgetWarnings, |
| | | budgetHint, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | remoteSearchApplicantForm, |
| | | userSelectLabel, |
| | | onApplicantChange, |
| | | recalcTravelStandards, |
| | | onTravelRangeChange, |
| | | onDetailAmountChange, |
| | | onApprovalFlowChange, |
| | | addExpenseDetail, |
| | | removeExpenseDetail, |
| | | syncApplyAmountFromDetails, |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | | handleExport, |
| | | handleImportClick, |
| | | onImportFile, |
| | | } = tr; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | .mb8 { |
| | | margin-bottom: 8px; |
| | | } |
| | | .mt16 { |
| | | margin-top: 16px; |
| | | } |
| | | .search_form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .search_actions { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | .search_title { |
| | | font-size: 14px; |
| | | color: var(--el-text-color-regular); |
| | | } |
| | | .sr-only-input { |
| | | position: absolute; |
| | | width: 1px; |
| | | height: 1px; |
| | | padding: 0; |
| | | margin: -1px; |
| | | overflow: hidden; |
| | | clip: rect(0, 0, 0, 0); |
| | | white-space: nowrap; |
| | | border: 0; |
| | | } |
| | | .form-section { |
| | | margin-bottom: 16px; |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | } |
| | | .form-section :deep(.el-card__header) { |
| | | padding: 12px 16px; |
| | | background: var(--el-fill-color-lighter); |
| | | } |
| | | .form-section :deep(.el-card__body) { |
| | | padding: 16px 16px 4px; |
| | | } |
| | | .card-header-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | } |
| | | .card-header-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | .amount-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | width: 100%; |
| | | } |
| | | .amount-input { |
| | | flex: 1; |
| | | min-width: 160px; |
| | | } |
| | | .w-full { |
| | | width: 100%; |
| | | } |
| | | .attachment-form-item { |
| | | margin-bottom: 0; |
| | | } |
| | | .detail-table { |
| | | margin-bottom: 0; |
| | | } |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | margin: 8px 0 12px; |
| | | color: var(--el-text-color-primary); |
| | | border-left: 3px solid var(--el-color-primary); |
| | | padding-left: 8px; |
| | | } |
| | | .field-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin-top: 4px; |
| | | } |
| | | .warn-list { |
| | | margin: 0; |
| | | padding-left: 18px; |
| | | } |
| | | .detail-toolbar { |
| | | margin-bottom: 8px; |
| | | } |
| | | .upload-block { |
| | | width: 100%; |
| | | } |
| | | .flow-tip { |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | margin-top: 8px; |
| | | } |
| | | .sync-btn { |
| | | margin-top: 4px; |
| | | } |
| | | .travel-reimburse-form-dialog :deep(.el-dialog__body) { |
| | | padding-top: 12px; |
| | | } |
| | | .travel-reimburse-form :deep(.el-form-item) { |
| | | margin-bottom: 18px; |
| | | } |
| | | .travel-reimburse-form :deep(.el-input-number) { |
| | | width: 100%; |
| | | } |
| | | .travel-reimburse-form :deep(.el-row) { |
| | | margin-bottom: 0; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import dayjs from "dayjs"; |
| | | |
| | | /** è´¹ç¨ç§ç® */ |
| | | export const EXPENSE_SUBJECT_OPTIONS = [ |
| | | { label: "交éè´¹", value: "transport" }, |
| | | { label: "ä½å®¿è´¹", value: "hotel" }, |
| | | { label: "é¤é¥®è´¹", value: "meal" }, |
| | | { label: "å
¶ä»", value: "other" }, |
| | | ]; |
| | | |
| | | const TIER1_CITIES = ["å京", "䏿µ·", "广å·", "æ·±å³"]; |
| | | |
| | | export function expenseSubjectLabel(v) { |
| | | return EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === v)?.label || "â"; |
| | | } |
| | | |
| | | export function statusLabel(v) { |
| | | if (v === "draft") return "è稿"; |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "paid") return "已仿¬¾"; |
| | | if (v === "rejected") return "驳å"; |
| | | if (v === "cancelled") return "å·²æ¤å"; |
| | | return "å®¡æ ¸ä¸"; |
| | | } |
| | | |
| | | export function statusTagType(v) { |
| | | if (v === "draft") return "info"; |
| | | if (v === "approved" || v === "paid") return "success"; |
| | | if (v === "rejected") return "danger"; |
| | | if (v === "cancelled") return "info"; |
| | | return "warning"; |
| | | } |
| | | |
| | | export function detectTravelTier(destination) { |
| | | const city = (destination || "").trim(); |
| | | if (!city) return "tier3"; |
| | | if (TIER1_CITIES.some((c) => city.includes(c))) return "tier1"; |
| | | const tier2Keywords = ["æå·", "å京", "æ¦æ±", "æé½", "éåº", "西å®", "天津", "èå·", "é¿æ²", "éå·"]; |
| | | if (tier2Keywords.some((c) => city.includes(c))) return "tier2"; |
| | | return "tier3"; |
| | | } |
| | | |
| | | export function getTravelStandardByTier(tier) { |
| | | const map = { |
| | | tier1: { hotelPerNight: 600, transportPerDay: 80, mealPerDay: 100, label: "ä¸çº¿åå¸" }, |
| | | tier2: { hotelPerNight: 450, transportPerDay: 60, mealPerDay: 80, label: "äºçº¿åå¸" }, |
| | | tier3: { hotelPerNight: 350, transportPerDay: 40, mealPerDay: 60, label: "å
¶ä»åå¸" }, |
| | | }; |
| | | return map[tier] || map.tier3; |
| | | } |
| | | |
| | | export function computeTravelDays(startStr, endStr) { |
| | | if (!startStr || !endStr) return null; |
| | | const t0 = dayjs(startStr); |
| | | const t1 = dayjs(endStr); |
| | | if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null; |
| | | const days = Math.ceil(t1.diff(t0, "day", true)); |
| | | return Math.max(1, days); |
| | | } |
| | | |
| | | export function createEmptyExpenseDetail() { |
| | | return { |
| | | id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, |
| | | invoiceDate: "", |
| | | expenseSubject: "", |
| | | amount: undefined, |
| | | description: "", |
| | | }; |
| | | } |
| | | |
| | | export function createEmptyForm() { |
| | | return { |
| | | id: undefined, |
| | | reimburseNo: "", |
| | | applicantId: "", |
| | | employeeNo: "", |
| | | employeeName: "", |
| | | reimburseReason: "", |
| | | travelStartTime: "", |
| | | travelEndTime: "", |
| | | travelDays: undefined, |
| | | departurePlace: "", |
| | | destination: "", |
| | | hotelStandard: undefined, |
| | | hotelDays: undefined, |
| | | livingSubsidy: undefined, |
| | | applyAmount: undefined, |
| | | payee: "", |
| | | expenseDetails: [], |
| | | attachmentList: [], |
| | | approvalFlowNodes: [], |
| | | currentNodeIndex: 0, |
| | | needSpecialApproval: false, |
| | | deptId: "", |
| | | deptName: "", |
| | | travelTier: "tier3", |
| | | }; |
| | | } |
| | | |
| | | export function initApprovalFlowNodes(nodes) { |
| | | return (nodes || []).map((n, i) => ({ |
| | | ...n, |
| | | sortOrder: i + 1, |
| | | nodeOrder: i + 1, |
| | | nodeStatus: i === 0 ? "process" : "wait", |
| | | approveOpinion: n.approveOpinion || "", |
| | | approveTime: n.approveTime || "", |
| | | })); |
| | | } |
| | | |
| | | export function advanceApprovalFlow(row, opinion) { |
| | | const nodes = [...(row.approvalFlowNodes || [])]; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | if (!nodes.length) return { nodes, currentNodeIndex: idx, approvalResult: row.approvalResult }; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | nodes[idx] = { |
| | | ...nodes[idx], |
| | | nodeStatus: "finish", |
| | | approveOpinion: opinion || "åæ", |
| | | approveTime: now, |
| | | }; |
| | | const next = idx + 1; |
| | | if (next >= nodes.length) { |
| | | return { nodes, currentNodeIndex: idx, approvalResult: "approved" }; |
| | | } |
| | | nodes[next] = { ...nodes[next], nodeStatus: "process" }; |
| | | return { nodes, currentNodeIndex: next, approvalResult: "pending" }; |
| | | } |
| | | |
| | | export function rejectApprovalFlow(row, opinion) { |
| | | const nodes = [...(row.approvalFlowNodes || [])]; |
| | | const idx = row.currentNodeIndex ?? 0; |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | if (nodes[idx]) { |
| | | nodes[idx] = { |
| | | ...nodes[idx], |
| | | nodeStatus: "error", |
| | | approveOpinion: opinion || "驳å", |
| | | approveTime: now, |
| | | }; |
| | | } |
| | | return { nodes, currentNodeIndex: idx, approvalResult: "rejected", rejectReason: opinion || "驳å" }; |
| | | } |
| | | |
| | | /** é¨é¨é¢ç®ï¼å¯¹æ¥é¢ç®ç³»ç»åè¿åç©ºï¼ */ |
| | | export function mockDeptBudget(deptId) { |
| | | if (!deptId) return null; |
| | | return null; |
| | | } |
| | | |
| | | export function normalizeImportedRow(raw, idx) { |
| | | const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`; |
| | | const travelDays = |
| | | raw.travelDays != null |
| | | ? Number(raw.travelDays) |
| | | : computeTravelDays(raw.travelStartTime, raw.travelEndTime); |
| | | return { |
| | | id, |
| | | reimburseNo: raw.reimburseNo || `TR${dayjs().format("YYYYMMDD")}${String(idx).padStart(4, "0")}`, |
| | | applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`, |
| | | employeeNo: raw.employeeNo ?? raw.applicantNo ?? "", |
| | | employeeName: raw.employeeName ?? raw.applicantName ?? "æªç¥", |
| | | applicantNo: raw.employeeNo ?? raw.applicantNo ?? "", |
| | | applicantName: raw.employeeName ?? raw.applicantName ?? "æªç¥", |
| | | reimburseReason: raw.reimburseReason ?? "", |
| | | travelStartTime: raw.travelStartTime ?? "", |
| | | travelEndTime: raw.travelEndTime ?? "", |
| | | travelDays: travelDays == null || Number.isNaN(travelDays) ? 1 : travelDays, |
| | | departurePlace: raw.departurePlace ?? "", |
| | | destination: raw.destination ?? "", |
| | | hotelStandard: raw.hotelStandard, |
| | | hotelDays: raw.hotelDays, |
| | | livingSubsidy: raw.livingSubsidy, |
| | | applyAmount: raw.applyAmount ?? 0, |
| | | payee: raw.payee ?? "", |
| | | expenseDetails: Array.isArray(raw.expenseDetails) ? raw.expenseDetails : [], |
| | | invoiceAttachments: Array.isArray(raw.invoiceAttachments) ? raw.invoiceAttachments : [], |
| | | approvalFlowNodes: Array.isArray(raw.approvalFlowNodes) ? raw.approvalFlowNodes : [], |
| | | currentNodeIndex: raw.currentNodeIndex ?? 0, |
| | | approvalResult: ["pending", "approved", "rejected"].includes(raw.approvalResult) ? raw.approvalResult : "pending", |
| | | rejectReason: raw.rejectReason ?? "", |
| | | approvalRecords: Array.isArray(raw.approvalRecords) ? raw.approvalRecords : [], |
| | | needSpecialApproval: !!raw.needSpecialApproval, |
| | | deptId: raw.deptId ?? "", |
| | | deptName: raw.deptName ?? "", |
| | | travelTier: raw.travelTier || detectTravelTier(raw.destination), |
| | | createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | deleteFinReimbursement, |
| | | getFinReimbursementDetail, |
| | | listFinReimbursementPage, |
| | | persistFinReimbursement, |
| | | } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue"; |
| | | import { |
| | | buildFinReimbursementListParams, |
| | | filterReimbursementRowsBySearch, |
| | | hasActiveReimbursementSearch, |
| | | buildTravelReimbursementSaveDto, |
| | | canDeleteReimbursementRow, |
| | | canEditReimbursementRow, |
| | | enrichReimbursementListRowsWithApprovalFlow, |
| | | filterRowsByReimbursementType, |
| | | FIN_REIMBURSEMENT_TYPE, |
| | | mapFinReimbursementDetailRow, |
| | | mapTravelReimbursementRow, |
| | | resolveReimbursementDeleteId, |
| | | unwrapFinReimbursementDetail, |
| | | unwrapFinReimbursementPage, |
| | | validateReimbursementApprovalNodes, |
| | | validateReimbursementPersistDto, |
| | | } from "../shared/finReimbursementMappers.js"; |
| | | import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js"; |
| | | import { |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | | statusLabel, |
| | | statusTagType, |
| | | detectTravelTier, |
| | | getTravelStandardByTier, |
| | | computeTravelDays, |
| | | createEmptyExpenseDetail, |
| | | createEmptyForm, |
| | | initApprovalFlowNodes, |
| | | advanceApprovalFlow, |
| | | rejectApprovalFlow, |
| | | mockDeptBudget, |
| | | normalizeImportedRow, |
| | | } from "./travelReimburseUtils.js"; |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | export function useTravelReimburse() { |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const allRows = ref([]); |
| | | |
| | | const searchForm = reactive({ applicantKeyword: "" }); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | const importInputRef = ref(null); |
| | | const allUsersCache = ref([]); |
| | | const applicantFormSearchLoading = ref(false); |
| | | const applicantFormOptions = ref([]); |
| | | const formRef = ref(); |
| | | const form = reactive(createEmptyForm()); |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailLoading = ref(false); |
| | | const detailRow = ref({}); |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | const submitSaving = ref(false); |
| | | |
| | | const tableData = computed(() => allRows.value); |
| | | |
| | | async function fetchList() { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listFinReimbursementPage( |
| | | buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL, |
| | | }) |
| | | ); |
| | | const { records, total } = unwrapFinReimbursementPage(res); |
| | | const filtered = filterRowsByReimbursementType( |
| | | records, |
| | | FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ); |
| | | let mapped = filtered.map(mapTravelReimbursementRow); |
| | | mapped = await enrichReimbursementListRowsWithApprovalFlow( |
| | | mapped, |
| | | FIN_REIMBURSEMENT_TYPE.TRAVEL |
| | | ); |
| | | if (hasActiveReimbursementSearch(searchForm)) { |
| | | mapped = filterReimbursementRowsBySearch(mapped, searchForm); |
| | | } |
| | | allRows.value = mapped; |
| | | const dropped = records.length - filtered.length; |
| | | let nextTotal = |
| | | dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total); |
| | | if (hasActiveReimbursementSearch(searchForm)) { |
| | | nextTotal = mapped.length; |
| | | } |
| | | page.total = nextTotal; |
| | | } catch { |
| | | allRows.value = []; |
| | | page.total = 0; |
| | | proxy?.$modal?.msgError?.("å·®æ
æ¥éå表å 载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); |
| | | |
| | | const travelDaysDisplay = computed(() => { |
| | | const d = computeTravelDays(form.travelStartTime, form.travelEndTime); |
| | | return d == null ? "" : String(d); |
| | | }); |
| | | |
| | | const travelTierLabel = computed(() => { |
| | | const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination)); |
| | | return `æ${std.label}æ å`; |
| | | }); |
| | | |
| | | const suggestedLivingSubsidy = computed(() => { |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0; |
| | | const std = getTravelStandardByTier(form.travelTier); |
| | | return Math.round(std.mealPerDay * days * 100) / 100; |
| | | }); |
| | | |
| | | const suggestedTransportSubsidy = computed(() => { |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0; |
| | | const std = getTravelStandardByTier(form.travelTier); |
| | | return Math.round(std.transportPerDay * days * 100) / 100; |
| | | }); |
| | | |
| | | const suggestedHotelLimit = computed(() => { |
| | | const nights = form.hotelDays || 0; |
| | | const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight; |
| | | return Math.round(perNight * nights * 100) / 100; |
| | | }); |
| | | |
| | | const detailTotalAmount = computed(() => { |
| | | const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); |
| | | return Math.round(sum * 100) / 100; |
| | | }); |
| | | |
| | | const overBudgetWarnings = computed(() => buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value)); |
| | | |
| | | const budgetHint = computed(() => { |
| | | if (!form.deptId) return { visible: false }; |
| | | const b = mockDeptBudget(form.deptId); |
| | | const apply = Number(form.applyAmount) || detailTotalAmount.value || 0; |
| | | const after = b.remainingAmount - apply; |
| | | return { |
| | | visible: true, |
| | | type: after < 0 ? "error" : "info", |
| | | title: `é¨é¨é¢ç®èå¨ï¼${form.deptName || b.deptId}ï¼`, |
| | | description: `年度é¢ç® ${b.totalBudget} å
ï¼å·²ç¨ ${b.usedAmount} å
ï¼å©ä½ ${b.remainingAmount} å
ï¼æ¬åç³è¯·åé¢è®¡å©ä½ ${Math.round(after * 100) / 100} å
ã`, |
| | | }; |
| | | }); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "æ¥éåå·", prop: "reimburseNo", width: 150 }, |
| | | { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äºº", prop: "applicantName", minWidth: 90 }, |
| | | { label: "åºå·®å¼å§", prop: "travelStartTime", width: 165 }, |
| | | { label: "åºå·®ç»æ", prop: "travelEndTime", width: 165 }, |
| | | { label: "å建æ¶é´", prop: "createTime", width: 165 }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "approvalResult", |
| | | width: 100, |
| | | dataType: "tag", |
| | | formatData: (v) => statusLabel(v), |
| | | formatType: (v) => statusTagType(v), |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 220, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: (row) => !canEditReimbursementRow(row), |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "å é¤", |
| | | type: "danger", |
| | | disabled: (row) => !canDeleteReimbursementRow(row), |
| | | clickFun: (row) => confirmRemoveRow(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const formRules = { |
| | | applicantId: [{ required: true, message: "è¯·éæ©åå·¥", trigger: "change" }], |
| | | reimburseReason: [{ required: true, message: "è¯·å¡«åæ¥éåå ", trigger: "blur" }], |
| | | travelStartTime: [{ required: true, message: "è¯·éæ©åºå·®å¼å§æ¶é´", trigger: "change" }], |
| | | travelEndTime: [ |
| | | { required: true, message: "è¯·éæ©åºå·®ç»ææ¶é´", trigger: "change" }, |
| | | { |
| | | validator: (_r, val, cb) => { |
| | | if (!form.travelStartTime || !val) { cb(); return; } |
| | | if (computeTravelDays(form.travelStartTime, val) == null) cb(new Error("ç»ææ¶é´é¡»æäºå¼å§æ¶é´")); |
| | | else cb(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | departurePlace: [{ required: true, message: "请填ååºå·®å°", trigger: "blur" }], |
| | | destination: [{ required: true, message: "请填åç®çå°", trigger: "blur" }], |
| | | applyAmount: [{ required: true, message: "请填åç³è¯·éé¢", trigger: "blur" }], |
| | | payee: [{ required: true, message: "è¯·å¡«åæ¶æ¬¾äºº", trigger: "blur" }], |
| | | approvalFlowNodes: [ |
| | | { |
| | | validator: (_r, _v, cb) => { |
| | | const nodes = form.approvalFlowNodes || []; |
| | | if (!nodes.length) { cb(new Error("请è³å°é
ç½®ä¸ä¸ªå®¡æ¹èç¹")); return; } |
| | | if (nodes.some((n) => n.approverId == null || n.approverId === "")) { cb(new Error("æ¯ä¸ªèç¹é¡»éæ©å®¡æ¹äºº")); return; } |
| | | cb(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) { |
| | | const warnings = []; |
| | | const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 }; |
| | | (f.expenseDetails || []).forEach((d) => { |
| | | const key = d.expenseSubject || "other"; |
| | | bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0); |
| | | }); |
| | | if (bySubject.transport > transportLimit && transportLimit > 0) { |
| | | warnings.push(`交éè´¹ ${bySubject.transport} å
è¶
åºæ å ${transportLimit} å
`); |
| | | } |
| | | if (bySubject.hotel > hotelLimit && hotelLimit > 0) { |
| | | warnings.push(`ä½å®¿è´¹ ${bySubject.hotel} å
è¶
åºéé¢ ${hotelLimit} å
`); |
| | | } |
| | | if (bySubject.meal > mealLimit && mealLimit > 0) { |
| | | warnings.push(`é¤é¥®è´¹ ${bySubject.meal} å
è¶
åºç活补贴建议 ${mealLimit} å
`); |
| | | } |
| | | const std = getTravelStandardByTier(f.travelTier); |
| | | if (f.hotelStandard > std.hotelPerNight) { |
| | | warnings.push(`é
åºæ å ${f.hotelStandard} å
/æé«äº${std.label}æ å ${std.hotelPerNight} å
/æ`); |
| | | } |
| | | const apply = Number(f.applyAmount) || detailTotal; |
| | | const standardTotal = transportLimit + hotelLimit + mealLimit; |
| | | if (apply > standardTotal && standardTotal > 0) { |
| | | warnings.push(`ç³è¯·æ»é¢ ${apply} å
é«äºå·®æ
æ åå计约 ${standardTotal} å
`); |
| | | } |
| | | return warnings; |
| | | } |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | allUsersCache.value = unwrapArray(await userListNoPageByTenantId()); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | function userSelectLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const name = u.userName || ""; |
| | | if (nick && name && nick !== name) return `${nick}ï¼${name}ï¼`; |
| | | return nick || name || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function userById(id) { |
| | | return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id)); |
| | | } |
| | | |
| | | function employeeNoFromUser(u) { |
| | | if (!u) return ""; |
| | | return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : ""); |
| | | } |
| | | |
| | | function filterUsersByQuery(query) { |
| | | const list = allUsersCache.value.filter(isActiveUser); |
| | | const q = (query || "").trim().toLowerCase(); |
| | | if (!q) return [...list]; |
| | | return list.filter((u) => { |
| | | const nick = (u.nickName || "").toLowerCase(); |
| | | const uname = (u.userName || "").toLowerCase(); |
| | | return nick.includes(q) || uname.includes(q); |
| | | }); |
| | | } |
| | | |
| | | async function remoteSearchApplicantForm(query) { |
| | | applicantFormSearchLoading.value = true; |
| | | try { |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | applicantFormOptions.value = filterUsersByQuery(query); |
| | | } finally { |
| | | applicantFormSearchLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function onApplicantChange(uid) { |
| | | const u = userById(uid); |
| | | if (u) { |
| | | form.employeeName = u.nickName || u.userName || ""; |
| | | form.employeeNo = employeeNoFromUser(u); |
| | | form.payee = form.payee || form.employeeName; |
| | | form.deptId = String(u.deptId ?? u.sysDeptId ?? ""); |
| | | form.deptName = u.dept?.deptName ?? u.deptName ?? ""; |
| | | } else { |
| | | form.employeeName = ""; |
| | | form.employeeNo = ""; |
| | | } |
| | | } |
| | | |
| | | function recalcTravelStandards() { |
| | | form.travelTier = detectTravelTier(form.destination); |
| | | const std = getTravelStandardByTier(form.travelTier); |
| | | if (form.hotelStandard == null || form.hotelStandard === 0) form.hotelStandard = std.hotelPerNight; |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime); |
| | | if (days != null) { |
| | | form.travelDays = days; |
| | | if (form.hotelDays == null) form.hotelDays = Math.max(0, days - 1); |
| | | if (form.livingSubsidy == null || form.livingSubsidy === 0) form.livingSubsidy = suggestedLivingSubsidy.value; |
| | | } |
| | | form.needSpecialApproval = buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value).length > 0; |
| | | } |
| | | |
| | | function onTravelRangeChange() { |
| | | recalcTravelStandards(); |
| | | nextTick(() => formRef.value?.validateField?.("travelEndTime")); |
| | | } |
| | | |
| | | function onDetailAmountChange() { |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function onApprovalFlowChange() { |
| | | nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); |
| | | } |
| | | |
| | | function addExpenseDetail() { |
| | | form.expenseDetails.push(createEmptyExpenseDetail()); |
| | | } |
| | | |
| | | function removeExpenseDetail(index) { |
| | | form.expenseDetails.splice(index, 1); |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function mapAttachmentList(list) { |
| | | return (list || []).map((f, i) => ({ |
| | | id: f.id ?? f.uid ?? `inv_${Date.now()}_${i}`, |
| | | name: f.name || f.fileName || f.originalFilename || "æªå½å", |
| | | url: f.url || f.downloadURL || f.previewURL || f.previewUrl || "", |
| | | })); |
| | | } |
| | | |
| | | function syncApplyAmountFromDetails() { |
| | | form.applyAmount = detailTotalAmount.value; |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | return fetchList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.applicantKeyword = ""; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | return fetchList(); |
| | | } |
| | | |
| | | async function loadTravelDetailRow(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | throw new Error("missing id"); |
| | | } |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | return mapFinReimbursementDetailRow(raw, FIN_REIMBURSEMENT_TYPE.TRAVEL); |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | proxy?.$modal?.msgWarning?.("æ æ³æ¥ç详æ
ï¼ç¼ºå°æ¥éå ID"); |
| | | return; |
| | | } |
| | | detailDialog.visible = true; |
| | | detailLoading.value = true; |
| | | detailRow.value = { ...row }; |
| | | try { |
| | | detailRow.value = await loadTravelDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å 载详æ
失败"); |
| | | detailDialog.visible = false; |
| | | } finally { |
| | | detailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirmRemoveRow(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | proxy?.$modal?.msgWarning?.("æ æ³å é¤ï¼ç¼ºå°æ¥éå ID"); |
| | | return; |
| | | } |
| | | const title = row.reimburseNo || row.billNo || row.reimburseReason || "该æ¥éå"; |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤ã${title}ãåï¼å é¤åä¸å¯æ¢å¤ã`, |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "ç¡®å®å é¤", |
| | | cancelButtonText: "åæ¶", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | try { |
| | | await deleteFinReimbursement([id]); |
| | | proxy?.$modal?.msgSuccess?.("å 餿å"); |
| | | if (detailDialog.visible && resolveReimbursementDeleteId(detailRow.value) === id) { |
| | | detailDialog.visible = false; |
| | | } |
| | | await handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å é¤å¤±è´¥"); |
| | | } |
| | | } |
| | | |
| | | function openApprove(row) { |
| | | approveDialog.row = { ...row }; |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | | function approvalActionLabel(v) { |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "rejected") return "驳å"; |
| | | return "æäº¤"; |
| | | } |
| | | |
| | | async function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.readonly = false; |
| | | formDialog.title = mode === "add" ? "æ°å¢å·®æ
æ¥é" : "ç¼è¾å·®æ
æ¥é"; |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | Object.assign(form, createEmptyForm()); |
| | | if (mode === "edit" && row) { |
| | | let editRow = row; |
| | | try { |
| | | editRow = await loadTravelDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("å è½½æ¥é详æ
失败"); |
| | | return; |
| | | } |
| | | Object.assign(form, { |
| | | ...JSON.parse(JSON.stringify(editRow)), |
| | | reimbursementId: editRow.reimbursementId ?? editRow.id, |
| | | attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])), |
| | | approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])), |
| | | expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])), |
| | | }); |
| | | const u = userById(editRow.applicantId); |
| | | applicantFormOptions.value = u |
| | | ? [u] |
| | | : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }]; |
| | | } else { |
| | | form.approvalFlowNodes = [{ approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1 }]; |
| | | remoteSearchApplicantForm(""); |
| | | } |
| | | formDialog.visible = true; |
| | | nextTick(() => { |
| | | formRef.value?.clearValidate?.(); |
| | | recalcTravelStandards(); |
| | | }); |
| | | } |
| | | |
| | | function onFormClosed() { |
| | | formRef.value?.resetFields?.(); |
| | | } |
| | | |
| | | async function submitForm() { |
| | | try { |
| | | await formRef.value?.validate?.(); |
| | | } catch { |
| | | return; |
| | | } |
| | | if (!(form.expenseDetails || []).length) { |
| | | proxy?.$modal?.msgWarning?.("请è³å°æ·»å 䏿¡æ¥éæç»"); |
| | | return; |
| | | } |
| | | recalcTravelStandards(); |
| | | if (form.needSpecialApproval) { |
| | | try { |
| | | await proxy.$modal.confirm("åå¨è¶
æ¯é¡¹ï¼æäº¤åå°æ 记为éç¹æ¹ï¼æ¯å¦ç»§ç»ï¼"); |
| | | } catch { |
| | | return; |
| | | } |
| | | } |
| | | if (submitSaving.value) return; |
| | | const isEdit = formDialog.mode === "edit"; |
| | | const dto = buildTravelReimbursementSaveDto(form, { computeTravelDays }); |
| | | const check = validateReimbursementPersistDto(dto, isEdit); |
| | | if (!check.ok) { |
| | | proxy?.$modal?.msgWarning?.(check.message); |
| | | return; |
| | | } |
| | | const nodeCheck = validateReimbursementApprovalNodes(dto); |
| | | if (!nodeCheck.ok) { |
| | | proxy?.$modal?.msgWarning?.(nodeCheck.message); |
| | | return; |
| | | } |
| | | submitSaving.value = true; |
| | | try { |
| | | await persistFinReimbursement(dto, isEdit); |
| | | proxy?.$modal?.msgSuccess?.(isEdit ? "ä¿åæå" : "æäº¤æå"); |
| | | formDialog.visible = false; |
| | | await handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.(isEdit ? "ä¿å失败" : "æäº¤å¤±è´¥"); |
| | | } finally { |
| | | submitSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | | const row = approveDialog.row; |
| | | if (!row) return; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | proxy?.$modal?.msgWarning?.("驳å须填åå®¡æ¹æè§"); |
| | | return; |
| | | } |
| | | const idx = allRows.value.findIndex((r) => r.id === row.id); |
| | | if (idx === -1) return; |
| | | const cur = allRows.value[idx]; |
| | | const operatorName = "å½å审æ¹äºº"; |
| | | const record = { |
| | | operatorName, |
| | | result, |
| | | opinion: approveOpinion.value || (result === "approved" ? "åæ" : "驳å"), |
| | | time: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | const records = [...(cur.approvalRecords || []), record]; |
| | | let flowUpdate; |
| | | if (result === "approved") { |
| | | flowUpdate = advanceApprovalFlow(cur, approveOpinion.value); |
| | | } else { |
| | | flowUpdate = rejectApprovalFlow(cur, approveOpinion.value); |
| | | } |
| | | allRows.value[idx] = { |
| | | ...cur, |
| | | approvalFlowNodes: flowUpdate.nodes, |
| | | currentNodeIndex: flowUpdate.currentNodeIndex, |
| | | approvalResult: flowUpdate.approvalResult, |
| | | rejectReason: flowUpdate.rejectReason ?? cur.rejectReason, |
| | | approvalRecords: records, |
| | | }; |
| | | proxy?.$modal?.msgSuccess?.(result === "approved" ? "å·²éè¿" : "已驳å"); |
| | | approveDialog.visible = false; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function handleExport() { |
| | | const data = allRows.value; |
| | | const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" }); |
| | | const url = URL.createObjectURL(blob); |
| | | const a = document.createElement("a"); |
| | | a.href = url; |
| | | a.download = `å·®æ
æ¥é导åº_${dayjs().format("YYYYMMDDHHmmss")}.json`; |
| | | a.click(); |
| | | URL.revokeObjectURL(url); |
| | | proxy?.$modal?.msgSuccess?.(`å·²å¯¼åº ${data.length} æ¡`); |
| | | } |
| | | |
| | | function handleImportClick() { |
| | | importInputRef.value?.click?.(); |
| | | } |
| | | |
| | | function onImportFile(e) { |
| | | const file = e.target.files?.[0]; |
| | | e.target.value = ""; |
| | | if (!file) return; |
| | | const reader = new FileReader(); |
| | | reader.onload = () => { |
| | | try { |
| | | const parsed = JSON.parse(String(reader.result || "")); |
| | | const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data; |
| | | if (!Array.isArray(arr) || !arr.length) { |
| | | proxy?.$modal?.msgWarning?.("导å
¥æ ¼å¼é¡»ä¸ºå·®æ
æ¥é JSON æ°ç»"); |
| | | return; |
| | | } |
| | | arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i))); |
| | | proxy?.$modal?.msgSuccess?.(`æå导å
¥ ${arr.length} æ¡`); |
| | | handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("è§£æå¤±è´¥"); |
| | | } |
| | | }; |
| | | reader.readAsText(file, "utf-8"); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | loadUserPool(); |
| | | await fetchList(); |
| | | const editPayload = consumeReimburseEditFromApprove(); |
| | | if (editPayload?.reimbursementId != null) { |
| | | await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId }); |
| | | } |
| | | }); |
| | | |
| | | return { |
| | | Search, |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | importInputRef, |
| | | formRef, |
| | | form, |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | applicantFormSearchLoading, |
| | | applicantFormOptions, |
| | | flowUserOptions, |
| | | travelDaysDisplay, |
| | | travelTierLabel, |
| | | suggestedLivingSubsidy, |
| | | suggestedTransportSubsidy, |
| | | suggestedHotelLimit, |
| | | detailTotalAmount, |
| | | overBudgetWarnings, |
| | | budgetHint, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | remoteSearchApplicantForm, |
| | | userSelectLabel, |
| | | onApplicantChange, |
| | | recalcTravelStandards, |
| | | onTravelRangeChange, |
| | | onDetailAmountChange, |
| | | onApprovalFlowChange, |
| | | addExpenseDetail, |
| | | removeExpenseDetail, |
| | | syncApplyAmountFromDetails, |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | confirmRemoveRow, |
| | | openApprove, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | | handleExport, |
| | | handleImportClick, |
| | | onImportFile, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼é¨é¨ç®¡ç--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch"> |
| | | <el-form-item label="é¨é¨åç§°" prop="deptName"> |
| | | <el-input |
| | | v-model="queryParams.deptName" |
| | | placeholder="请è¾å
¥é¨é¨åç§°" |
| | | clearable |
| | | style="width: 200px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select v-model="queryParams.status" placeholder="é¨é¨ç¶æ" clearable style="width: 200px"> |
| | | <el-option |
| | | v-for="dict in sys_normal_disable" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="Search" @click="handleQuery">æç´¢</el-button> |
| | | <el-button icon="Refresh" @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="primary" |
| | | plain |
| | | icon="Plus" |
| | | @click="handleAdd" |
| | | v-hasPermi="['system:dept:add']" |
| | | >æ°å¢</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="info" |
| | | plain |
| | | icon="Sort" |
| | | @click="toggleExpandAll" |
| | | >å±å¼/æå </el-button> |
| | | </el-col> |
| | | <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> |
| | | </el-row> |
| | | <el-table |
| | | v-if="refreshTable" |
| | | v-loading="loading" |
| | | :data="deptList" |
| | | row-key="deptId" |
| | | :default-expand-all="isExpandAll" |
| | | :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" |
| | | > |
| | | <el-table-column prop="deptName" label="é¨é¨åç§°" width="260"></el-table-column> |
| | | <el-table-column prop="orderNum" label="æåº" width="200"></el-table-column> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <dict-tag :options="sys_normal_disable" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å建æ¶é´" align="center" prop="createTime" width="200"> |
| | | <template #default="scope"> |
| | | <span>{{ parseTime(scope.row.createTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" align="center" class-name="small-padding fixed-width"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">ä¿®æ¹</el-button> |
| | | <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">æ°å¢</el-button> |
| | | <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- æ·»å æä¿®æ¹é¨é¨å¯¹è¯æ¡ --> |
| | | <el-dialog :title="title" v-model="open" width="600px" append-to-body> |
| | | <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px"> |
| | | <el-row> |
| | | <el-col :span="24" v-if="form.parentId !== 0"> |
| | | <el-form-item label="ä¸çº§é¨é¨" prop="parentId"> |
| | | <el-tree-select |
| | | v-model="form.parentId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'deptId', label: 'deptName', children: 'children' }" |
| | | value-key="deptId" |
| | | placeholder="éæ©ä¸çº§é¨é¨" |
| | | check-strictly |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨é¨åç§°" prop="deptName"> |
| | | <el-input v-model="form.deptName" placeholder="请è¾å
¥é¨é¨åç§°" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¾ç¤ºæåº" prop="orderNum"> |
| | | <el-input-number v-model="form.orderNum" controls-position="right" :min="0"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è´è´£äºº" prop="leader"> |
| | | <el-input v-model="form.leader" placeholder="请è¾å
¥è´è´£äºº" maxlength="20" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="èç³»çµè¯" prop="phone"> |
| | | <el-input v-model="form.phone" placeholder="请è¾å
¥èç³»çµè¯" maxlength="11" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é®ç®±" prop="email"> |
| | | <el-input v-model="form.email" placeholder="请è¾å
¥é®ç®±" maxlength="50" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨é¨ç¶æ"> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio |
| | | v-for="dict in sys_normal_disable" |
| | | :key="dict.value" |
| | | :value="dict.value" |
| | | >{{ dict.label }}</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨é¨ç¼å·" prop="deptNick"> |
| | | <el-input v-model="form.deptNick" placeholder="请è¾å
¥é¨é¨ç¼å·" maxlength="50" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">ç¡® å®</el-button> |
| | | <el-button @click="cancel">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="Dept"> |
| | | import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept" |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const { sys_normal_disable } = proxy.useDict("sys_normal_disable") |
| | | |
| | | const deptList = ref([]) |
| | | const open = ref(false) |
| | | const loading = ref(true) |
| | | const showSearch = ref(true) |
| | | const title = ref("") |
| | | const deptOptions = ref([]) |
| | | const isExpandAll = ref(true) |
| | | const refreshTable = ref(true) |
| | | |
| | | const data = reactive({ |
| | | form: {}, |
| | | queryParams: { |
| | | deptName: undefined, |
| | | status: undefined |
| | | }, |
| | | rules: { |
| | | parentId: [{ required: true, message: "ä¸çº§é¨é¨ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | deptName: [{ required: true, message: "é¨é¨åç§°ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | orderNum: [{ required: true, message: "æ¾ç¤ºæåºä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | email: [{ type: "email", message: "请è¾å
¥æ£ç¡®çé®ç®±å°å", trigger: ["blur", "change"] }], |
| | | phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请è¾å
¥æ£ç¡®çææºå·ç ", trigger: "blur" }], |
| | | deptNick: [{ required: true, message: "é¨é¨ç¼å·ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | }, |
| | | }) |
| | | |
| | | const { queryParams, form, rules } = toRefs(data) |
| | | |
| | | /** æ¥è¯¢é¨é¨å表 */ |
| | | function getList() { |
| | | loading.value = true |
| | | listDept(queryParams.value).then(response => { |
| | | deptList.value = proxy.handleTree(response.data, "deptId") |
| | | loading.value = false |
| | | }) |
| | | } |
| | | |
| | | /** åæ¶æé® */ |
| | | function cancel() { |
| | | open.value = false |
| | | reset() |
| | | } |
| | | |
| | | /** 表åéç½® */ |
| | | function reset() { |
| | | form.value = { |
| | | deptId: undefined, |
| | | parentId: undefined, |
| | | deptName: undefined, |
| | | orderNum: 0, |
| | | leader: undefined, |
| | | phone: undefined, |
| | | email: undefined, |
| | | status: "0", |
| | | deptNick: undefined, |
| | | } |
| | | proxy.resetForm("deptRef") |
| | | } |
| | | |
| | | /** æç´¢æé®æä½ */ |
| | | function handleQuery() { |
| | | getList() |
| | | } |
| | | |
| | | /** éç½®æé®æä½ */ |
| | | function resetQuery() { |
| | | proxy.resetForm("queryRef") |
| | | handleQuery() |
| | | } |
| | | |
| | | /** æ°å¢æé®æä½ */ |
| | | function handleAdd(row) { |
| | | reset() |
| | | listDept().then(response => { |
| | | deptOptions.value = proxy.handleTree(response.data, "deptId") |
| | | }) |
| | | if (row != undefined) { |
| | | form.value.parentId = row.deptId |
| | | } |
| | | open.value = true |
| | | title.value = "æ·»å é¨é¨" |
| | | } |
| | | |
| | | /** å±å¼/æå æä½ */ |
| | | function toggleExpandAll() { |
| | | refreshTable.value = false |
| | | isExpandAll.value = !isExpandAll.value |
| | | nextTick(() => { |
| | | refreshTable.value = true |
| | | }) |
| | | } |
| | | |
| | | /** ä¿®æ¹æé®æä½ */ |
| | | function handleUpdate(row) { |
| | | reset() |
| | | listDeptExcludeChild(row.deptId).then(response => { |
| | | deptOptions.value = proxy.handleTree(response.data, "deptId") |
| | | }) |
| | | getDept(row.deptId).then(response => { |
| | | form.value = response.data |
| | | open.value = true |
| | | title.value = "ä¿®æ¹é¨é¨" |
| | | }) |
| | | } |
| | | |
| | | /** æäº¤æé® */ |
| | | function submitForm() { |
| | | proxy.$refs["deptRef"].validate(valid => { |
| | | if (valid) { |
| | | if (form.value.deptId != undefined) { |
| | | updateDept(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | open.value = false |
| | | getList() |
| | | }) |
| | | } else { |
| | | addDept(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå") |
| | | open.value = false |
| | | getList() |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** å é¤æé®æä½ */ |
| | | function handleDelete(row) { |
| | | proxy.$modal.confirm('æ¯å¦ç¡®è®¤å é¤å称为"' + row.deptName + '"çæ°æ®é¡¹?').then(function() { |
| | | return delDept(row.deptId) |
| | | }).then(() => { |
| | | getList() |
| | | proxy.$modal.msgSuccess("å 餿å") |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | getList() |
| | | </script> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼æ¥å¿ç®¡ç--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px"> |
| | | <el-form-item label="æä½å°å" prop="operIp"> |
| | | <el-input |
| | | v-model="queryParams.operIp" |
| | | placeholder="请è¾å
¥æä½å°å" |
| | | clearable |
| | | style="width: 240px;" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç³»ç»æ¨¡å" prop="title"> |
| | | <el-input |
| | | v-model="queryParams.title" |
| | | placeholder="请è¾å
¥ç³»ç»æ¨¡å" |
| | | clearable |
| | | style="width: 240px;" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="æä½äººå" prop="operName"> |
| | | <el-input |
| | | v-model="queryParams.operName" |
| | | placeholder="请è¾å
¥æä½äººå" |
| | | clearable |
| | | style="width: 240px;" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç±»å" prop="businessType"> |
| | | <el-select |
| | | v-model="queryParams.businessType" |
| | | placeholder="æä½ç±»å" |
| | | clearable |
| | | style="width: 240px" |
| | | > |
| | | <el-option |
| | | v-for="dict in sys_oper_type" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select |
| | | v-model="queryParams.status" |
| | | placeholder="æä½ç¶æ" |
| | | clearable |
| | | style="width: 240px" |
| | | > |
| | | <el-option |
| | | v-for="dict in sys_common_status" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æä½æ¶é´" style="width: 308px"> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]" |
| | | ></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="Search" @click="handleQuery">æç´¢</el-button> |
| | | <el-button icon="Refresh" @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | icon="Delete" |
| | | :disabled="multiple" |
| | | @click="handleDelete" |
| | | v-hasPermi="['monitor:operlog:remove']" |
| | | >å é¤</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | icon="Delete" |
| | | @click="handleClean" |
| | | v-hasPermi="['monitor:operlog:remove']" |
| | | >æ¸
空</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="warning" |
| | | plain |
| | | icon="Download" |
| | | @click="handleExport" |
| | | v-hasPermi="['monitor:operlog:export']" |
| | | >导åº</el-button> |
| | | </el-col> |
| | | <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> |
| | | </el-row> |
| | | |
| | | <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange"> |
| | | <el-table-column type="selection" width="50" align="center" /> |
| | | <el-table-column label="æ¥å¿ç¼å·" align="center" prop="operId" /> |
| | | <el-table-column label="ç³»ç»æ¨¡å" align="center" prop="title" :show-overflow-tooltip="true" /> |
| | | <el-table-column label="æä½ç±»å" align="center" prop="businessType"> |
| | | <template #default="scope"> |
| | | <dict-tag :options="sys_oper_type" :value="scope.row.businessType" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½äººå" align="center" width="110" prop="operName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" /> |
| | | <el-table-column label="æä½å°å" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" /> |
| | | <el-table-column label="æä½ç¶æ" align="center" prop="status"> |
| | | <template #default="scope"> |
| | | <dict-tag :options="sys_common_status" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½æ¥æ" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']"> |
| | | <template #default="scope"> |
| | | <span>{{ parseTime(scope.row.operTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¶èæ¶é´" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']"> |
| | | <template #default="scope"> |
| | | <span>{{ scope.row.costTime }}毫ç§</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" align="center" class-name="small-padding fixed-width"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">详ç»</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | v-model:page="queryParams.pageNum" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | |
| | | <!-- æä½æ¥å¿è¯¦ç» --> |
| | | <el-dialog title="æä½æ¥å¿è¯¦ç»" v-model="open" width="800px" append-to-body> |
| | | <el-form :model="form" label-width="100px"> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æä½æ¨¡åï¼">{{ form.title }} / {{ typeFormat(form) }}</el-form-item> |
| | | <el-form-item |
| | | label="ç»å½ä¿¡æ¯ï¼" |
| | | >{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="请æ±å°åï¼">{{ form.operUrl }}</el-form-item> |
| | | <el-form-item label="è¯·æ±æ¹å¼ï¼">{{ form.requestMethod }}</el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="æä½æ¹æ³ï¼">{{ form.method }}</el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="请æ±åæ°ï¼">{{ form.operParam }}</el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="è¿ååæ°ï¼">{{ form.jsonResult }}</el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æä½ç¶æï¼"> |
| | | <div v-if="form.status === 0">æ£å¸¸</div> |
| | | <div v-else-if="form.status === 1">失败</div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æ¶èæ¶é´ï¼">{{ form.costTime }}毫ç§</el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æä½æ¶é´ï¼">{{ parseTime(form.operTime) }}</el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="å¼å¸¸ä¿¡æ¯ï¼" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="open = false">å
³ é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="Operlog"> |
| | | import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog" |
| | | import {onMounted} from "vue"; |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type","sys_common_status") |
| | | |
| | | const operlogList = ref([]) |
| | | const open = ref(false) |
| | | const loading = ref(true) |
| | | const showSearch = ref(true) |
| | | const ids = ref([]) |
| | | const single = ref(true) |
| | | const multiple = ref(true) |
| | | const total = ref(0) |
| | | const title = ref("") |
| | | const dateRange = ref([]) |
| | | const defaultSort = ref({ prop: "operTime", order: "descending" }) |
| | | |
| | | const data = reactive({ |
| | | form: {}, |
| | | queryParams: { |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | operIp: undefined, |
| | | title: undefined, |
| | | operName: undefined, |
| | | businessType: undefined, |
| | | status: undefined |
| | | } |
| | | }) |
| | | |
| | | const { queryParams, form } = toRefs(data) |
| | | |
| | | /** æ¥è¯¢ç»å½æ¥å¿ */ |
| | | function getList() { |
| | | loading.value = true |
| | | list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => { |
| | | operlogList.value = response.rows |
| | | total.value = response.total |
| | | loading.value = false |
| | | }) |
| | | } |
| | | |
| | | /** æä½æ¥å¿ç±»ååå
¸ç¿»è¯ */ |
| | | function typeFormat(row, column) { |
| | | return proxy.selectDictLabel(sys_oper_type.value, row.businessType) |
| | | } |
| | | |
| | | /** æç´¢æé®æä½ */ |
| | | function handleQuery() { |
| | | queryParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | /** éç½®æé®æä½ */ |
| | | function resetQuery() { |
| | | dateRange.value = [] |
| | | proxy.resetForm("queryRef") |
| | | queryParams.value.pageNum = 1 |
| | | proxy.$refs["operlogRef"].sort(defaultSort.value.prop, defaultSort.value.order) |
| | | } |
| | | |
| | | /** å¤éæ¡é䏿°æ® */ |
| | | function handleSelectionChange(selection) { |
| | | ids.value = selection.map(item => item.operId) |
| | | multiple.value = !selection.length |
| | | } |
| | | |
| | | /** æåºè§¦åäºä»¶ */ |
| | | function handleSortChange(column, prop, order) { |
| | | queryParams.value.orderByColumn = column.prop |
| | | queryParams.value.isAsc = column.order |
| | | getList() |
| | | } |
| | | |
| | | /** è¯¦ç»æé®æä½ */ |
| | | function handleView(row) { |
| | | open.value = true |
| | | form.value = row |
| | | } |
| | | |
| | | /** å é¤æé®æä½ */ |
| | | function handleDelete(row) { |
| | | const operIds = row.operId || ids.value |
| | | proxy.$modal.confirm('æ¯å¦ç¡®è®¤å 餿¥å¿ç¼å·ä¸º"' + operIds + '"çæ°æ®é¡¹?').then(function () { |
| | | return delOperlog(operIds) |
| | | }).then(() => { |
| | | getList() |
| | | proxy.$modal.msgSuccess("å 餿å") |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | /** æ¸
空æé®æä½ */ |
| | | function handleClean() { |
| | | proxy.$modal.confirm("æ¯å¦ç¡®è®¤æ¸
ç©ºæææä½æ¥å¿æ°æ®é¡¹?").then(function () { |
| | | return cleanOperlog() |
| | | }).then(() => { |
| | | getList() |
| | | proxy.$modal.msgSuccess("æ¸
空æå") |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | /** å¯¼åºæé®æä½ */ |
| | | function handleExport() { |
| | | proxy.download("monitor/operlog/export",{ |
| | | ...queryParams.value, |
| | | }, `config_${new Date().getTime()}.xlsx`) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <h4 class="form-header h4">åºæ¬ä¿¡æ¯</h4> |
| | | <el-form :model="form" label-width="80px"> |
| | | <el-row> |
| | | <el-col :span="8" :offset="2"> |
| | | <el-form-item label="ç¨æ·æµç§°" prop="nickName"> |
| | | <el-input v-model="form.nickName" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8" :offset="2"> |
| | | <el-form-item label="ç»å½è´¦å·" prop="userName"> |
| | | <el-input v-model="form.userName" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <h4 class="form-header h4">è§è²ä¿¡æ¯</h4> |
| | | <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)"> |
| | | <el-table-column label="åºå·" width="55" type="index" align="center"> |
| | | <template #default="scope"> |
| | | <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column> |
| | | <el-table-column label="è§è²ç¼å·" align="center" prop="roleId" /> |
| | | <el-table-column label="è§è²åç§°" align="center" prop="roleName" /> |
| | | <el-table-column label="æéå符" align="center" prop="roleKey" /> |
| | | <el-table-column label="å建æ¶é´" align="center" prop="createTime" width="180"> |
| | | <template #default="scope"> |
| | | <span>{{ parseTime(scope.row.createTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" /> |
| | | |
| | | <el-form label-width="100px"> |
| | | <div style="text-align: center;margin-left:-120px;margin-top:30px;"> |
| | | <el-button type="primary" @click="submitForm()">æäº¤</el-button> |
| | | <el-button @click="close()">è¿å</el-button> |
| | | </div> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="AuthRole"> |
| | | import { getAuthRole, updateAuthRole } from "@/api/system/user" |
| | | |
| | | const route = useRoute() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const loading = ref(true) |
| | | const total = ref(0) |
| | | const pageNum = ref(1) |
| | | const pageSize = ref(10) |
| | | const roleIds = ref([]) |
| | | const roles = ref([]) |
| | | const form = ref({ |
| | | nickName: undefined, |
| | | userName: undefined, |
| | | userId: undefined |
| | | }) |
| | | |
| | | /** åå»éä¸è¡æ°æ® */ |
| | | function clickRow(row) { |
| | | if (checkSelectable(row)) { |
| | | proxy.$refs["roleRef"].toggleRowSelection(row) |
| | | } |
| | | } |
| | | |
| | | /** å¤éæ¡é䏿°æ® */ |
| | | function handleSelectionChange(selection) { |
| | | roleIds.value = selection.map(item => item.roleId) |
| | | } |
| | | |
| | | /** ä¿åéä¸çæ°æ®ç¼å· */ |
| | | function getRowKey(row) { |
| | | return row.roleId |
| | | } |
| | | |
| | | // æ£æ¥è§è²ç¶æ |
| | | function checkSelectable(row) { |
| | | return row.status === "0" ? true : false |
| | | } |
| | | |
| | | /** å
³éæé® */ |
| | | function close() { |
| | | const obj = { path: "/system/user" } |
| | | proxy.$tab.closeOpenPage(obj) |
| | | } |
| | | |
| | | /** æäº¤æé® */ |
| | | function submitForm() { |
| | | const userId = form.value.userId |
| | | const rIds = roleIds.value.join(",") |
| | | updateAuthRole({ userId: userId, roleIds: rIds }).then(response => { |
| | | proxy.$modal.msgSuccess("æææå") |
| | | close() |
| | | }) |
| | | } |
| | | |
| | | (() => { |
| | | const userId = route.params && route.params.userId |
| | | if (userId) { |
| | | loading.value = true |
| | | getAuthRole(userId).then(response => { |
| | | form.value = response.user |
| | | roles.value = response.roles |
| | | total.value = roles.value.length |
| | | nextTick(() => { |
| | | roles.value.forEach(row => { |
| | | if (row.flag) { |
| | | proxy.$refs["roleRef"].toggleRowSelection(row) |
| | | } |
| | | }) |
| | | }) |
| | | loading.value = false |
| | | }) |
| | | } |
| | | })() |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼ç¨æ·ç®¡ç--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-row :gutter="20" style="height: calc(100vh - 8em)"> |
| | | <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme"> |
| | | <!--é¨é¨æ°æ®--> |
| | | <pane size="16"> |
| | | <el-col style="padding: 10px"> |
| | | <div class="head-container"> |
| | | <el-input v-model="deptNames" placeholder="请è¾å
¥é¨é¨åç§°" clearable prefix-icon="Search" style="margin-bottom: 20px" /> |
| | | </div> |
| | | <div class="head-container"> |
| | | <el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" /> |
| | | </div> |
| | | </el-col> |
| | | </pane> |
| | | <!--ç¨æ·æ°æ®--> |
| | | <pane size="84"> |
| | | <el-col style="padding: 10px; height: 100%; display: flex; flex-direction: column;"> |
| | | <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px"> |
| | | <el-form-item label="ç»å½è´¦å·" prop="userName"> |
| | | <el-input v-model="queryParams.userName" placeholder="请è¾å
¥ç»å½è´¦å·" clearable style="width: 240px" @keyup.enter="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="ææºå·ç " prop="phonenumber"> |
| | | <el-input v-model="queryParams.phonenumber" placeholder="请è¾å
¥ææºå·ç " clearable style="width: 240px" @keyup.enter="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select v-model="queryParams.status" placeholder="ç¨æ·ç¶æ" clearable style="width: 240px"> |
| | | <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å建æ¶é´" style="width: 308px"> |
| | | <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="Search" @click="handleQuery">æç´¢</el-button> |
| | | <el-button icon="Refresh" @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:user:add']">æ°å¢</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">ä¿®æ¹</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">å é¤</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['system:user:import']">导å
¥</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:user:export']">导åº</el-button> |
| | | </el-col> |
| | | <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar> |
| | | </el-row> |
| | | |
| | | <div style="flex: 1; overflow: hidden;"> |
| | | <el-table v-loading="loading" :data="userList" height="100%" @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="50" align="center" /> |
| | | <el-table-column label="ç¨æ·ç¼å·" align="center" key="userId" prop="userId" v-if="columns[0].visible" /> |
| | | <el-table-column label="ç»å½è´¦å·" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" /> |
| | | <el-table-column label="ç¨æ·æµç§°" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" /> |
| | | <el-table-column label="é¨é¨" align="center" key="deptNames" prop="deptNames" v-if="columns[3].visible" :show-overflow-tooltip="true" /> |
| | | <el-table-column label="ææºå·ç " align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" /> |
| | | <el-table-column label="ç¶æ" align="center" key="status" v-if="columns[5].visible"> |
| | | <template #default="scope"> |
| | | <el-switch |
| | | v-model="scope.row.status" |
| | | active-value="0" |
| | | inactive-value="1" |
| | | @change="handleStatusChange(scope.row)" |
| | | ></el-switch> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å建æ¶é´" align="center" prop="createTime" v-if="columns[6].visible" width="160"> |
| | | <template #default="scope"> |
| | | <span>{{ parseTime(scope.row.createTime) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" align="center" width="150" class-name="small-padding fixed-width"> |
| | | <template #default="scope"> |
| | | <el-tooltip content="ä¿®æ¹" placement="top" v-if="scope.row.userId !== 1"> |
| | | <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="å é¤" placement="top" v-if="scope.row.userId !== 1"> |
| | | <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="éç½®å¯ç " placement="top" v-if="scope.row.userId !== 1"> |
| | | <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button> |
| | | </el-tooltip> |
| | | <el-tooltip content="åé
è§è²" placement="top" v-if="scope.row.userId !== 1"> |
| | | <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button> |
| | | </el-tooltip> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> |
| | | </el-col> |
| | | </pane> |
| | | </splitpanes> |
| | | </el-row> |
| | | |
| | | <!-- æ·»å æä¿®æ¹ç¨æ·é
ç½®å¯¹è¯æ¡ --> |
| | | <el-dialog :title="title" v-model="open" width="600px" append-to-body> |
| | | <el-form :model="form" :rules="rules" ref="userRef" label-width="80px"> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item v-if="form.userId == undefined" label="ç»å½è´¦å·" prop="userName"> |
| | | <el-input v-model="form.userName" placeholder="请è¾å
¥ç¨æ·åç§°" maxlength="30" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item v-if="form.userId == undefined" label="ç¨æ·å¯ç " prop="password"> |
| | | <el-input v-model="form.password" placeholder="请è¾å
¥ç¨æ·å¯ç " type="password" maxlength="20" show-password /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¨æ·æµç§°" prop="nickName"> |
| | | <el-input v-model="form.nickName" placeholder="请è¾å
¥ç¨æ·æµç§°" maxlength="30" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å½å±é¨é¨" prop="deptId"> |
| | | <el-tree-select v-model="form.deptId" :data="enabledDeptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="è¯·éæ©å½å±é¨é¨" check-strictly /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å²ä½" prop="postIds"> |
| | | <el-select v-model="form.postIds" multiple placeholder="è¯·éæ©"> |
| | | <el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§è²" prop="roleIds"> |
| | | <el-select v-model="form.roleIds" multiple placeholder="è¯·éæ©"> |
| | | <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" :disabled="item.status == 1"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ææºå·ç " prop="phonenumber"> |
| | | <el-input v-model="form.phonenumber" placeholder="请è¾å
¥ææºå·ç " maxlength="11" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é®ç®±" prop="email"> |
| | | <el-input v-model="form.email" placeholder="请è¾å
¥é®ç®±" maxlength="50" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¨æ·æ§å«"> |
| | | <el-select v-model="form.sex" placeholder="è¯·éæ©"> |
| | | <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¶æ"> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item label="夿³¨"> |
| | | <el-input v-model="form.remark" type="textarea" placeholder="请è¾å
¥å
容"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">ç¡® å®</el-button> |
| | | <el-button @click="cancel">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- ç¨æ·å¯¼å
¥å¯¹è¯æ¡ --> |
| | | <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body> |
| | | <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag> |
| | | <el-icon class="el-icon--upload"><upload-filled /></el-icon> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | <template #tip> |
| | | <div class="el-upload__tip text-center"> |
| | | <div class="el-upload__tip"> |
| | | <el-checkbox v-model="upload.updateSupport" />æ¯å¦æ´æ°å·²ç»åå¨çç¨æ·æ°æ® |
| | | </div> |
| | | <span>ä»
å
许导å
¥xlsãxlsxæ ¼å¼æä»¶ã</span> |
| | | <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">ä¸è½½æ¨¡æ¿</el-link> |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button @click="upload.open = false">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="User"> |
| | | import { getToken } from "@/utils/auth" |
| | | import useAppStore from '@/store/modules/app' |
| | | import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user" |
| | | import { Splitpanes, Pane } from "splitpanes" |
| | | import "splitpanes/dist/splitpanes.css" |
| | | |
| | | const router = useRouter() |
| | | const appStore = useAppStore() |
| | | const { proxy } = getCurrentInstance() |
| | | const { sys_normal_disable, sys_user_sex } = proxy.useDict("sys_normal_disable", "sys_user_sex") |
| | | |
| | | const userList = ref([]) |
| | | const open = ref(false) |
| | | const loading = ref(true) |
| | | const showSearch = ref(true) |
| | | const ids = ref([]) |
| | | const single = ref(true) |
| | | const multiple = ref(true) |
| | | const total = ref(0) |
| | | const title = ref("") |
| | | const dateRange = ref([]) |
| | | const deptNames = ref("") |
| | | const deptOptions = ref(undefined) |
| | | const enabledDeptOptions = ref(undefined) |
| | | const initPassword = ref(undefined) |
| | | const postOptions = ref([]) |
| | | const roleOptions = ref([]) |
| | | /*** ç¨æ·å¯¼å
¥åæ° */ |
| | | const upload = reactive({ |
| | | // æ¯å¦æ¾ç¤ºå¼¹åºå±ï¼ç¨æ·å¯¼å
¥ï¼ |
| | | open: false, |
| | | // å¼¹åºå±æ é¢ï¼ç¨æ·å¯¼å
¥ï¼ |
| | | title: "", |
| | | // æ¯å¦ç¦ç¨ä¸ä¼ |
| | | isUploading: false, |
| | | // æ¯å¦æ´æ°å·²ç»åå¨çç¨æ·æ°æ® |
| | | updateSupport: 0, |
| | | // 设置ä¸ä¼ ç请æ±å¤´é¨ |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // ä¸ä¼ çå°å |
| | | url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData" |
| | | }) |
| | | // åæ¾éä¿¡æ¯ |
| | | const columns = ref([ |
| | | { key: 0, label: `ç¨æ·ç¼å·`, visible: true }, |
| | | { key: 1, label: `ç»å½è´¦å·`, visible: true }, |
| | | { key: 2, label: `ç¨æ·æµç§°`, visible: true }, |
| | | { key: 3, label: `é¨é¨`, visible: true }, |
| | | { key: 4, label: `ææºå·ç `, visible: true }, |
| | | { key: 5, label: `ç¶æ`, visible: true }, |
| | | { key: 6, label: `å建æ¶é´`, visible: true } |
| | | ]) |
| | | |
| | | const data = reactive({ |
| | | form: {}, |
| | | queryParams: { |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | userName: undefined, |
| | | phonenumber: undefined, |
| | | status: undefined, |
| | | deptId: undefined |
| | | }, |
| | | rules: { |
| | | userName: [{ required: true, message: "ç¨æ·åç§°ä¸è½ä¸ºç©º", trigger: "blur" }, { min: 2, max: 20, message: "ç¨æ·åç§°é¿åº¦å¿
é¡»ä»äº 2 å 20 ä¹é´", trigger: "blur" }], |
| | | nickName: [{ required: true, message: "ç¨æ·æµç§°ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | password: [{ required: true, message: "ç¨æ·å¯ç ä¸è½ä¸ºç©º", trigger: "blur" }, { min: 5, max: 20, message: "ç¨æ·å¯ç é¿åº¦å¿
é¡»ä»äº 5 å 20 ä¹é´", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "ä¸è½å
å«éæ³å符ï¼< > \" ' \\\ |", trigger: "blur" }], |
| | | email: [{ type: "email", message: "请è¾å
¥æ£ç¡®çé®ç®±å°å", trigger: ["blur", "change"] }], |
| | | phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请è¾å
¥æ£ç¡®çææºå·ç ", trigger: "blur" }], |
| | | deptId: [{ required: true, message: "å½å±é¨é¨ä¸è½ä¸ºç©º", trigger: "change" }], |
| | | postIds: [{ required: true, message: "å²ä½ä¸è½ä¸ºç©º", trigger: "change" }], |
| | | roleIds: [{ required: true, message: "è§è²ä¸è½ä¸ºç©º", trigger: "change" }] |
| | | } |
| | | }) |
| | | |
| | | const { queryParams, form, rules } = toRefs(data) |
| | | |
| | | /** éè¿æ¡ä»¶è¿æ»¤èç¹ */ |
| | | const filterNode = (value, data) => { |
| | | if (!value) return true |
| | | return data.label.indexOf(value) !== -1 |
| | | } |
| | | |
| | | /** æ ¹æ®åç§°çéé¨é¨æ */ |
| | | watch(deptNames, val => { |
| | | proxy.$refs["deptTreeRef"].filter(val) |
| | | }) |
| | | |
| | | /** æ¥è¯¢ç¨æ·å表 */ |
| | | function getList() { |
| | | loading.value = true |
| | | listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => { |
| | | loading.value = false |
| | | userList.value = res.rows |
| | | total.value = res.total |
| | | }) |
| | | } |
| | | |
| | | /** æ¥è¯¢é¨é¨ä¸ææ ç»æ */ |
| | | function getDeptTree() { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = response.data |
| | | enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data))) |
| | | }) |
| | | } |
| | | |
| | | /** è¿æ»¤ç¦ç¨çé¨é¨ */ |
| | | function filterDisabledDept(deptList) { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children) |
| | | } |
| | | return true |
| | | }) |
| | | } |
| | | |
| | | /** èç¹åå»äºä»¶ */ |
| | | function handleNodeClick(data) { |
| | | queryParams.value.deptId = data.id |
| | | handleQuery() |
| | | } |
| | | |
| | | /** æç´¢æé®æä½ */ |
| | | function handleQuery() { |
| | | queryParams.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | /** éç½®æé®æä½ */ |
| | | function resetQuery() { |
| | | dateRange.value = [] |
| | | proxy.resetForm("queryRef") |
| | | queryParams.value.deptId = undefined |
| | | proxy.$refs.deptTreeRef.setCurrentKey(null) |
| | | handleQuery() |
| | | } |
| | | |
| | | /** å é¤æé®æä½ */ |
| | | function handleDelete(row) { |
| | | const userIds = row.userId || ids.value |
| | | proxy.$modal.confirm('æ¯å¦ç¡®è®¤å é¤ç¨æ·ç¼å·ä¸º"' + userIds + '"çæ°æ®é¡¹ï¼').then(function () { |
| | | return delUser(userIds) |
| | | }).then(() => { |
| | | getList() |
| | | proxy.$modal.msgSuccess("å 餿å") |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | /** å¯¼åºæé®æä½ */ |
| | | function handleExport() { |
| | | proxy.download("system/user/export", { |
| | | ...queryParams.value, |
| | | },`user_${new Date().getTime()}.xlsx`) |
| | | } |
| | | |
| | | /** ç¨æ·ç¶æä¿®æ¹ */ |
| | | function handleStatusChange(row) { |
| | | let text = row.status === "0" ? "å¯ç¨" : "åç¨" |
| | | proxy.$modal.confirm('确认è¦"' + text + '""' + row.userName + '"ç¨æ·å?').then(function () { |
| | | return changeUserStatus(row.userId, row.status) |
| | | }).then(() => { |
| | | proxy.$modal.msgSuccess(text + "æå") |
| | | }).catch(function () { |
| | | row.status = row.status === "0" ? "1" : "0" |
| | | }) |
| | | } |
| | | |
| | | /** æ´å¤æä½ */ |
| | | function handleCommand(command, row) { |
| | | switch (command) { |
| | | case "handleResetPwd": |
| | | handleResetPwd(row) |
| | | break |
| | | case "handleAuthRole": |
| | | handleAuthRole(row) |
| | | break |
| | | default: |
| | | break |
| | | } |
| | | } |
| | | |
| | | /** 跳转è§è²åé
*/ |
| | | function handleAuthRole(row) { |
| | | const userId = row.userId |
| | | router.push("/system/user-auth/role/" + userId) |
| | | } |
| | | |
| | | /** éç½®å¯ç æé®æä½ */ |
| | | function handleResetPwd(row) { |
| | | proxy.$prompt('请è¾å
¥"' + row.userName + '"çæ°å¯ç ', "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | closeOnClickModal: false, |
| | | inputPattern: /^.{5,20}$/, |
| | | inputErrorMessage: "ç¨æ·å¯ç é¿åº¦å¿
é¡»ä»äº 5 å 20 ä¹é´", |
| | | inputValidator: (value) => { |
| | | if (/<|>|"|'|\||\\/.test(value)) { |
| | | return "ä¸è½å
å«éæ³å符ï¼< > \" ' \\\ |" |
| | | } |
| | | }, |
| | | }).then(({ value }) => { |
| | | resetUserPwd(row.userId, value).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æåï¼æ°å¯ç æ¯ï¼" + value) |
| | | }) |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | /** éæ©æ¡æ° */ |
| | | function handleSelectionChange(selection) { |
| | | ids.value = selection.map(item => item.userId) |
| | | single.value = selection.length != 1 |
| | | multiple.value = !selection.length |
| | | } |
| | | |
| | | /** 导å
¥æé®æä½ */ |
| | | function handleImport() { |
| | | upload.title = "ç¨æ·å¯¼å
¥" |
| | | upload.open = true |
| | | } |
| | | |
| | | /** ä¸è½½æ¨¡æ¿æä½ */ |
| | | function importTemplate() { |
| | | proxy.download("system/user/importTemplate", { |
| | | }, `user_template_${new Date().getTime()}.xlsx`) |
| | | } |
| | | |
| | | /**æä»¶ä¸ä¼ ä¸å¤ç */ |
| | | const handleFileUploadProgress = (event, file, fileList) => { |
| | | upload.isUploading = true |
| | | } |
| | | |
| | | /** æä»¶ä¸ä¼ æåå¤ç */ |
| | | const handleFileSuccess = (response, file, fileList) => { |
| | | upload.open = false |
| | | upload.isUploading = false |
| | | proxy.$refs["uploadRef"].handleRemove(file) |
| | | proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导å
¥ç»æ", { dangerouslyUseHTMLString: true }) |
| | | getList() |
| | | } |
| | | |
| | | /** æäº¤ä¸ä¼ æä»¶ */ |
| | | function submitFileForm() { |
| | | proxy.$refs["uploadRef"].submit() |
| | | } |
| | | |
| | | /** éç½®æä½è¡¨å */ |
| | | function reset() { |
| | | form.value = { |
| | | userId: undefined, |
| | | deptId: undefined, |
| | | userName: undefined, |
| | | nickName: undefined, |
| | | password: undefined, |
| | | phonenumber: undefined, |
| | | email: undefined, |
| | | sex: undefined, |
| | | status: "0", |
| | | remark: undefined, |
| | | postIds: [], |
| | | roleIds: [] |
| | | } |
| | | proxy.resetForm("userRef") |
| | | } |
| | | |
| | | /** åæ¶æé® */ |
| | | function cancel() { |
| | | open.value = false |
| | | reset() |
| | | } |
| | | |
| | | /** æ°å¢æé®æä½ */ |
| | | function handleAdd() { |
| | | reset() |
| | | getUser().then(response => { |
| | | postOptions.value = response.posts |
| | | roleOptions.value = response.roles |
| | | open.value = true |
| | | title.value = "æ·»å ç¨æ·" |
| | | form.value.password = initPassword.value |
| | | }) |
| | | } |
| | | |
| | | /** ä¿®æ¹æé®æä½ */ |
| | | function handleUpdate(row) { |
| | | reset() |
| | | const userId = row.userId || ids.value |
| | | getUser(userId).then(response => { |
| | | form.value = response.data |
| | | postOptions.value = response.posts |
| | | roleOptions.value = response.roles |
| | | form.value.postIds = response.postIds |
| | | form.value.roleIds = response.roleIds |
| | | open.value = true |
| | | title.value = "ä¿®æ¹ç¨æ·" |
| | | form.password = "" |
| | | }) |
| | | } |
| | | |
| | | /** æäº¤æé® */ |
| | | function submitForm() { |
| | | proxy.$refs["userRef"].validate(valid => { |
| | | if (valid) { |
| | | // å½å±é¨é¨è½ç¶æ¯åéï¼ä½å端éè¦ä¼ æ°ç»å段 deptIds |
| | | const payload = { |
| | | ...form.value, |
| | | deptIds: form.value.deptId ? [form.value.deptId] : [] |
| | | } |
| | | if (form.value.userId != undefined) { |
| | | updateUser(payload).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | open.value = false |
| | | getList() |
| | | }) |
| | | } else { |
| | | addUser(payload).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå") |
| | | open.value = false |
| | | getList() |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | getDeptTree() |
| | | getList() |
| | | </script> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6" :xs="24"> |
| | | <el-card class="box-card"> |
| | | <template v-slot:header> |
| | | <div class="clearfix"> |
| | | <span>个人信æ¯</span> |
| | | </div> |
| | | </template> |
| | | <div> |
| | | <div class="text-center"> |
| | | <userAvatar /> |
| | | </div> |
| | | <ul class="list-group list-group-striped"> |
| | | <li class="list-group-item"> |
| | | <svg-icon icon-class="user" />ç¨æ·åç§° |
| | | <div class="pull-right">{{ state.user.userName }}</div> |
| | | </li> |
| | | <li class="list-group-item"> |
| | | <svg-icon icon-class="phone" />ææºå·ç |
| | | <div class="pull-right">{{ state.user.phonenumber }}</div> |
| | | </li> |
| | | <li class="list-group-item"> |
| | | <svg-icon icon-class="email" />ç¨æ·é®ç®± |
| | | <div class="pull-right">{{ state.user.email }}</div> |
| | | </li> |
| | | <li class="list-group-item"> |
| | | <svg-icon icon-class="tree" />æå±é¨é¨ |
| | | <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div> |
| | | </li> |
| | | <li class="list-group-item"> |
| | | <svg-icon icon-class="peoples" />æå±è§è² |
| | | <div class="pull-right">{{ state.roleGroup }}</div> |
| | | </li> |
| | | <li class="list-group-item"> |
| | | <svg-icon icon-class="date" />åå»ºæ¥æ |
| | | <div class="pull-right">{{ state.user.createTime }}</div> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="18" :xs="24"> |
| | | <el-card> |
| | | <template v-slot:header> |
| | | <div class="clearfix"> |
| | | <span>åºæ¬èµæ</span> |
| | | </div> |
| | | </template> |
| | | <el-tabs v-model="activeTab"> |
| | | <el-tab-pane label="åºæ¬èµæ" name="userinfo"> |
| | | <userInfo :user="state.user" /> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="ä¿®æ¹å¯ç " name="resetPwd"> |
| | | <resetPwd /> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="Profile"> |
| | | import userAvatar from "./userAvatar" |
| | | import userInfo from "./userInfo" |
| | | import resetPwd from "./resetPwd" |
| | | import { getUserProfile } from "@/api/system/user" |
| | | |
| | | const activeTab = ref("userinfo") |
| | | const state = reactive({ |
| | | user: {}, |
| | | roleGroup: {}, |
| | | postGroup: {} |
| | | }) |
| | | |
| | | function getUser() { |
| | | getUserProfile().then(response => { |
| | | state.user = response.data |
| | | state.roleGroup = response.roleGroup |
| | | state.postGroup = response.postGroup |
| | | }) |
| | | } |
| | | |
| | | getUser() |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px"> |
| | | <el-form-item label="æ§å¯ç " prop="oldPassword"> |
| | | <el-input v-model="user.oldPassword" placeholder="请è¾å
¥æ§å¯ç " type="password" show-password /> |
| | | </el-form-item> |
| | | <el-form-item label="æ°å¯ç " prop="newPassword"> |
| | | <el-input v-model="user.newPassword" placeholder="请è¾å
¥æ°å¯ç " type="password" show-password /> |
| | | </el-form-item> |
| | | <el-form-item label="确认å¯ç " prop="confirmPassword"> |
| | | <el-input v-model="user.confirmPassword" placeholder="请确认æ°å¯ç " type="password" show-password/> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="submit">ä¿å</el-button> |
| | | <el-button type="danger" @click="close">å
³é</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { updateUserPwd } from "@/api/system/user" |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const user = reactive({ |
| | | oldPassword: undefined, |
| | | newPassword: undefined, |
| | | confirmPassword: undefined |
| | | }) |
| | | |
| | | const equalToPassword = (rule, value, callback) => { |
| | | if (user.newPassword !== value) { |
| | | callback(new Error("两次è¾å
¥çå¯ç ä¸ä¸è´")) |
| | | } else { |
| | | callback() |
| | | } |
| | | } |
| | | |
| | | const rules = ref({ |
| | | oldPassword: [{ required: true, message: "æ§å¯ç ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | newPassword: [{ required: true, message: "æ°å¯ç ä¸è½ä¸ºç©º", trigger: "blur" }, { min: 6, max: 20, message: "é¿åº¦å¨ 6 å° 20 个å符", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "ä¸è½å
å«éæ³å符ï¼< > \" ' \\\ |", trigger: "blur" }], |
| | | confirmPassword: [{ required: true, message: "确认å¯ç ä¸è½ä¸ºç©º", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }] |
| | | }) |
| | | |
| | | /** æäº¤æé® */ |
| | | function submit() { |
| | | proxy.$refs.pwdRef.validate(valid => { |
| | | if (valid) { |
| | | updateUserPwd(user.oldPassword, user.newPassword).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** å
³éæé® */ |
| | | function close() { |
| | | proxy.$tab.closePage() |
| | | } |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="user-info-head" @click="editCropper()"> |
| | | <img :src="options.img" title="ç¹å»ä¸ä¼ 头å" class="img-circle img-lg" /> |
| | | <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog"> |
| | | <el-row> |
| | | <el-col :xs="24" :md="12" :style="{ height: '350px' }"> |
| | | <vue-cropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop" |
| | | :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox" |
| | | :outputType="options.outputType" @realTime="realTime" v-if="visible" /> |
| | | </el-col> |
| | | <el-col :xs="24" :md="12" :style="{ height: '350px' }"> |
| | | <div class="avatar-upload-preview"> |
| | | <img :src="options.previews.url" :style="options.previews.img" /> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <br /> |
| | | <el-row> |
| | | <el-col :lg="2" :md="2"> |
| | | <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload"> |
| | | <el-button> |
| | | éæ© |
| | | <el-icon class="el-icon--right"> |
| | | <Upload /> |
| | | </el-icon> |
| | | </el-button> |
| | | </el-upload> |
| | | </el-col> |
| | | <el-col :lg="{ span: 1, offset: 2 }" :md="2"> |
| | | <el-button icon="Plus" @click="changeScale(1)"></el-button> |
| | | </el-col> |
| | | <el-col :lg="{ span: 1, offset: 1 }" :md="2"> |
| | | <el-button icon="Minus" @click="changeScale(-1)"></el-button> |
| | | </el-col> |
| | | <el-col :lg="{ span: 1, offset: 1 }" :md="2"> |
| | | <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button> |
| | | </el-col> |
| | | <el-col :lg="{ span: 1, offset: 1 }" :md="2"> |
| | | <el-button icon="RefreshRight" @click="rotateRight()"></el-button> |
| | | </el-col> |
| | | <el-col :lg="{ span: 2, offset: 6 }" :md="2"> |
| | | <el-button type="primary" @click="uploadImg()">æ 交</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import "vue-cropper/dist/index.css" |
| | | import { VueCropper } from "vue-cropper" |
| | | import { uploadAvatar } from "@/api/system/user" |
| | | import useUserStore from "@/store/modules/user" |
| | | |
| | | const userStore = useUserStore() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const open = ref(false) |
| | | const visible = ref(false) |
| | | const title = ref("ä¿®æ¹å¤´å") |
| | | |
| | | //å¾çè£åªæ°æ® |
| | | const options = reactive({ |
| | | img: userStore.avatar, // è£åªå¾ççå°å |
| | | autoCrop: true, // æ¯å¦é»è®¤çææªå¾æ¡ |
| | | autoCropWidth: 200, // é»è®¤çææªå¾æ¡å®½åº¦ |
| | | autoCropHeight: 200, // é»è®¤çææªå¾æ¡é«åº¦ |
| | | fixedBox: true, // åºå®æªå¾æ¡å¤§å° ä¸å
许æ¹å |
| | | outputType: "png", // é»è®¤çææªå¾ä¸ºPNGæ ¼å¼ |
| | | filename: 'avatar', // æä»¶åç§° |
| | | previews: {} //é¢è§æ°æ® |
| | | }) |
| | | |
| | | /** ç¼è¾å¤´å */ |
| | | function editCropper() { |
| | | open.value = true |
| | | } |
| | | |
| | | /** æå¼å¼¹åºå±ç»ææ¶çåè° */ |
| | | function modalOpened() { |
| | | visible.value = true |
| | | } |
| | | |
| | | /** è¦çé»è®¤ä¸ä¼ è¡ä¸º */ |
| | | function requestUpload() { } |
| | | |
| | | /** åå·¦æè½¬ */ |
| | | function rotateLeft() { |
| | | proxy.$refs.cropper.rotateLeft() |
| | | } |
| | | |
| | | /** åå³æè½¬ */ |
| | | function rotateRight() { |
| | | proxy.$refs.cropper.rotateRight() |
| | | } |
| | | |
| | | /** å¾çç¼©æ¾ */ |
| | | function changeScale(num) { |
| | | num = num || 1 |
| | | proxy.$refs.cropper.changeScale(num) |
| | | } |
| | | |
| | | /** ä¸ä¼ é¢å¤ç */ |
| | | function beforeUpload(file) { |
| | | if (file.type.indexOf("image/") == -1) { |
| | | proxy.$modal.msgError("æä»¶æ ¼å¼é误ï¼è¯·ä¸ä¼ å¾çç±»å,å¦ï¼JPGï¼PNGåç¼çæä»¶ã") |
| | | } else { |
| | | const reader = new FileReader() |
| | | reader.readAsDataURL(file) |
| | | reader.onload = () => { |
| | | options.img = reader.result |
| | | options.filename = file.name |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** ä¸ä¼ å¾ç */ |
| | | function uploadImg() { |
| | | proxy.$refs.cropper.getCropBlob(data => { |
| | | let formData = new FormData() |
| | | formData.append("avatarfile", data, options.filename) |
| | | uploadAvatar(formData).then(response => { |
| | | open.value = false |
| | | options.img = import.meta.env.VITE_APP_BASE_API + '/profile/' + response.imgUrl |
| | | userStore.avatar = options.img |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | visible.value = false |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | /** 宿¶é¢è§ */ |
| | | function realTime(data) { |
| | | options.previews = data |
| | | } |
| | | |
| | | /** å
³éçªå£ */ |
| | | function closeDialog() { |
| | | options.img = userStore.avatar |
| | | options.visible = false |
| | | } |
| | | </script> |
| | | |
| | | <style lang='scss' scoped> |
| | | .user-info-head { |
| | | position: relative; |
| | | display: inline-block; |
| | | height: 120px; |
| | | } |
| | | |
| | | .user-info-head:hover:after { |
| | | content: "+"; |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | color: #eee; |
| | | background: rgba(0, 0, 0, 0.5); |
| | | font-size: 24px; |
| | | font-style: normal; |
| | | -webkit-font-smoothing: antialiased; |
| | | -moz-osx-font-smoothing: grayscale; |
| | | cursor: pointer; |
| | | line-height: 110px; |
| | | border-radius: 50%; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-form ref="userRef" :model="form" :rules="rules" label-width="80px"> |
| | | <el-form-item label="ç¨æ·æµç§°" prop="nickName"> |
| | | <el-input v-model="form.nickName" maxlength="30" /> |
| | | </el-form-item> |
| | | <el-form-item label="ææºå·ç " prop="phonenumber"> |
| | | <el-input v-model="form.phonenumber" maxlength="11" /> |
| | | </el-form-item> |
| | | <el-form-item label="é®ç®±" prop="email"> |
| | | <el-input v-model="form.email" maxlength="50" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ§å«"> |
| | | <el-radio-group v-model="form.sex"> |
| | | <el-radio value="0">ç·</el-radio> |
| | | <el-radio value="1">女</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="submit">ä¿å</el-button> |
| | | <el-button type="danger" @click="close">å
³é</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { updateUserProfile } from "@/api/system/user" |
| | | |
| | | const props = defineProps({ |
| | | user: { |
| | | type: Object |
| | | } |
| | | }) |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const form = ref({}) |
| | | const rules = ref({ |
| | | nickName: [{ required: true, message: "ç¨æ·æµç§°ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | email: [{ required: true, message: "é®ç®±å°åä¸è½ä¸ºç©º", trigger: "blur" }, { type: "email", message: "请è¾å
¥æ£ç¡®çé®ç®±å°å", trigger: ["blur", "change"] }], |
| | | phonenumber: [{ required: true, message: "ææºå·ç ä¸è½ä¸ºç©º", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请è¾å
¥æ£ç¡®çææºå·ç ", trigger: "blur" }], |
| | | }) |
| | | |
| | | /** æäº¤æé® */ |
| | | function submit() { |
| | | proxy.$refs.userRef.validate(valid => { |
| | | if (valid) { |
| | | updateUserProfile(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå") |
| | | props.user.phonenumber = form.value.phonenumber |
| | | props.user.email = form.value.email |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | /** å
³éæé® */ |
| | | function close() { |
| | | proxy.$tab.closePage() |
| | | } |
| | | |
| | | // åæ¾å½åç»å½ç¨æ·ä¿¡æ¯ |
| | | watch(() => props.user, user => { |
| | | if (user) { |
| | | form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex } |
| | | } |
| | | },{ immediate: true }) |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!--OA模åï¼ç¼åçæ§--> |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-row :gutter="10"> |
| | | <el-col :span="24" class="card-box"> |
| | | <el-card> |
| | | <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">åºæ¬ä¿¡æ¯</span></template> |
| | | <div class="el-table el-table--enable-row-hover el-table--medium"> |
| | | <table cellspacing="0" style="width: 100%"> |
| | | <tbody> |
| | | <tr> |
| | | <td class="el-table__cell is-leaf"><div class="cell">Redisçæ¬</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">è¿è¡æ¨¡å¼</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "åæº" : "é群" }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">端å£</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">客æ·ç«¯æ°</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td> |
| | | </tr> |
| | | <tr> |
| | | <td class="el-table__cell is-leaf"><div class="cell">è¿è¡æ¶é´(天)</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">使ç¨å
å</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">使ç¨CPU</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">å
åé
ç½®</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td> |
| | | </tr> |
| | | <tr> |
| | | <td class="el-table__cell is-leaf"><div class="cell">AOFæ¯å¦å¼å¯</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "å¦" : "æ¯" }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">RDBæ¯å¦æå</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">Keyæ°é</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell">ç½ç»å
¥å£/åºå£</div></td> |
| | | <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12" class="card-box"> |
| | | <el-card> |
| | | <template #header><PieChart style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">å½ä»¤ç»è®¡</span></template> |
| | | <div class="el-table el-table--enable-row-hover el-table--medium"> |
| | | <div ref="commandstats" style="height: 420px" /> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="12" class="card-box"> |
| | | <el-card> |
| | | <template #header><Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">å
åä¿¡æ¯</span></template> |
| | | <div class="el-table el-table--enable-row-hover el-table--medium"> |
| | | <div ref="usedmemory" style="height: 420px" /> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="Cache"> |
| | | import { getCache } from '@/api/monitor/cache' |
| | | import * as echarts from 'echarts' |
| | | |
| | | const cache = ref([]) |
| | | const commandstats = ref(null) |
| | | const usedmemory = ref(null) |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | function getList() { |
| | | proxy.$modal.loading("æ£å¨å è½½ç¼åçæ§æ°æ®ï¼è¯·ç¨åï¼") |
| | | getCache().then(response => { |
| | | proxy.$modal.closeLoading() |
| | | cache.value = response.data |
| | | |
| | | const commandstatsIntance = echarts.init(commandstats.value, "macarons") |
| | | commandstatsIntance.setOption({ |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{a} <br/>{b} : {c} ({d}%)" |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "å½ä»¤", |
| | | type: "pie", |
| | | roseType: "radius", |
| | | radius: [15, 95], |
| | | center: ["50%", "38%"], |
| | | data: response.data.commandStats, |
| | | animationEasing: "cubicInOut", |
| | | animationDuration: 1000 |
| | | } |
| | | ] |
| | | }) |
| | | const usedmemoryInstance = echarts.init(usedmemory.value, "macarons") |
| | | usedmemoryInstance.setOption({ |
| | | tooltip: { |
| | | formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "å³°å¼", |
| | | type: "gauge", |
| | | min: 0, |
| | | max: 1000, |
| | | detail: { |
| | | formatter: cache.value.info.used_memory_human |
| | | }, |
| | | data: [ |
| | | { |
| | | value: parseFloat(cache.value.info.used_memory_human), |
| | | name: "å
åæ¶è" |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | }) |
| | | window.addEventListener("resize", () => { |
| | | commandstatsIntance.resize() |
| | | usedmemoryInstance.resize() |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | getList() |
| | | </script> |
| | | |
| src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue
src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue
src/views/personnelManagement/contractManagement/index.vue |