// 本地 mock 服务:仅为预览「教师时长」页视觉,供 preview-ut 专用 vite 代理使用 // 用法:node preview-ut/mock-server.mjs8109 import http from "node:http"; const USERS = [ { userId: 1, userName: "王雅静", usableHours: 24, remark: "语文组教研组长" }, { userId: 2, userName: "李国栋", usableHours: 16, remark: null }, { userId: 3, userName: "张明远", usableHours: 6, remark: null }, { userId: 4, userName: "陈思彤", usableHours: 0, remark: null }, { userId: 5, userName: "周建华", usableHours: 40, remark: null }, { userId: 6, userName: "孙丽娟", usableHours: 12.5, remark: null }, { userId: 7, userName: "吴强", usableHours: 3, remark: null }, { userId: 8, userName: "郑小满", usableHours: 32, remark: null }, ]; const TYPES = [ { SerID: "1", CodeID: "sj", Name: "事假", Enable: 1 }, { SerID: "2", CodeID: "bj", Name: "病假", Enable: 1 }, { SerID: "3", CodeID: "tx", Name: "调休", Enable: 1 }, { SerID: "4", CodeID: "hq", Name: "婚假", Enable: 1 }, { SerID: "5", CodeID: "cq", Name: "产假", Enable: 1 }, { SerID: "6", CodeID: "sq", Name: "丧假", Enable: 1 }, ]; // 复刻后端 UserCenterService.getSchoolAllTree 的扁平结构: // 组 type=1/isParent=true,教师 type=2,用 pid 指向父组 const TREE = [ { id: 100, name: "云海中学", pid: 0, type: 1, isParent: true }, { id: 200, name: "语文组", pid: 100, type: 1, isParent: true }, { id: 201, name: "数学组", pid: 100, type: 1, isParent: true }, { id: 202, name: "英语组", pid: 100, type: 1, isParent: true }, { id: 203, name: "综合组", pid: 100, type: 1, isParent: true }, { id: 1, name: "王雅静", pid: 200, type: 2 }, { id: 2, name: "李国栋", pid: 200, type: 2 }, { id: 5, name: "周建华", pid: 200, type: 2 }, { id: 3, name: "张明远", pid: 201, type: 2 }, { id: 4, name: "陈思彤", pid: 201, type: 2 }, { id: 6, name: "孙丽娟", pid: 202, type: 2 }, { id: 7, name: "吴强", pid: 202, type: 2 }, { id: 8, name: "郑小满", pid: 203, type: 2 }, ]; const OPTION = { reasonMust: 1, showExplain: 1, timeLimitEnabled: 1, timeManagedTypes: "sj,bj,tx", enableCourse: 0, allowExchange: 0, calcWay: 1 }; const TIMESET = { forenoon1: "08:00", forenoon2: "12:00", afternoon1: "13:00", afternoon2: "17:00", intervalMinutes: 10, minMinutes: 0 }; // 平台 GetUserMod 的真实返回(2026-10 环境):扁平一级菜单,id = modId = 97–105, // page 是历史路径。用于整壳验收侧栏「点击 → 跳转 → 高亮」链路(preview-app.html)。 const USER_MOD = [ { id: 97, modId: 97, name: "我要请假", page: "/leave/", parentId: 0, level: 1, orderNumber: 100, usable: true, children: [] }, { id: 98, modId: 98, name: "我的请假", page: "/leave/myleave", parentId: 0, level: 1, orderNumber: 110, usable: true, children: [] }, { id: 99, modId: 99, name: "审批列表", page: "/leave/shenhe", parentId: 0, level: 1, orderNumber: 120, usable: true, children: [] }, { id: 100, modId: 100, name: "抄送列表", page: "/leave/leave_send", parentId: 0, level: 1, orderNumber: 130, usable: true, children: [] }, { id: 101, modId: 101, name: "全部请假", page: "/leave/totallist", parentId: 0, level: 1, orderNumber: 140, usable: true, children: [] }, { id: 102, modId: 102, name: "流程管理", page: "/leave/liucheng", parentId: 0, level: 1, orderNumber: 160, usable: true, children: [] }, { id: 103, modId: 103, name: "请假设置", page: "/leave/leave_timeSetting", parentId: 0, level: 1, orderNumber: 170, usable: true, children: [] }, { id: 104, modId: 104, name: "时长日志", page: "/leave/leave_amendant", parentId: 0, level: 1, orderNumber: 180, usable: true, children: [] }, { id: 105, modId: 105, name: "统计分析", page: "/leave/tongji", parentId: 0, level: 1, orderNumber: 190, usable: true, children: [] }, ]; const USER_INFO = { id: 1, name: "王雅静", profilePicture: "" }; // ===== 流程管理(/api/leave/flow/*):内存态,可交互(开关/绑定即时生效)===== // 复刻流程管理页 FlowManageVo;通知流程默认「未启用」,演示 oapi 混入非本应用流程的场景 const CENTER_FLOWS = [ { flowGuid: "35dc167902ef436e8767809eb6bf11ee", name: "设课(5天)", version: 6, nodeSummary: "审核中", enabled: true }, { flowGuid: "28bc3ca8acc24611809d7dd1b6b9a597", name: "产假(10天)", version: 5, nodeSummary: "审核中", enabled: true }, { flowGuid: "d2237a5b2b95492e845da9caeaa35590", name: "请假申请(小于3天或大于等于3天)", version: 4, nodeSummary: "审批节点—222", enabled: true }, { flowGuid: "fdb2c9046ede4db583b0a4899d065f75", name: "通知流程", version: 2, nodeSummary: "审批节点", enabled: false }, ]; let BIND_SEQ = 3; const FLOW_BINDS = [ { id: 1, scope: 0, scopeKey: "-", scopeName: "全校默认", flowGuid: "d2237a5b2b95492e845da9caeaa35590", flowName: "请假申请(小于3天或大于等于3天)", flowVersion: 4, nodeSummary: "审批节点—222", existsInCenter: true }, { id: 2, scope: 1, scopeKey: "cq", scopeName: "产假", flowGuid: "28bc3ca8acc24611809d7dd1b6b9a597", flowName: "产假(10天)", flowVersion: 5, nodeSummary: "审核中", existsInCenter: true }, ]; function flowManageData() { const flows = CENTER_FLOWS.map((f) => { const typeKeys = FLOW_BINDS.filter((b) => b.scope === 1 && b.flowGuid === f.flowGuid).map((b) => b.scopeKey); return { ...f, defaultBind: FLOW_BINDS.some((b) => b.scope === 0 && b.flowGuid === f.flowGuid), typeKeys, typeNames: typeKeys.map((k) => TYPES.find((t) => t.CodeID === k)?.Name || k), }; }); return { bizType: "leave", configFlowGuid: "d2237a5b2b95492e845da9caeaa35590", centerReachable: true, centerError: null, flows, binds: FLOW_BINDS.map((b) => ({ ...b })), leaveTypes: TYPES.map((t) => ({ key: t.CodeID, name: t.Name })), }; } /** 写操作:返回 true 表示已识别并处理(POST /api/leave/flow/*) */ function flowMutate(url, body) { const guid = body?.flowGuid; if (/flow\/Enable/i.test(url)) { const f = CENTER_FLOWS.find((x) => x.flowGuid === guid); if (f) f.enabled = true; return true; } if (/flow\/Disable/i.test(url)) { const f = CENTER_FLOWS.find((x) => x.flowGuid === guid); if (f) f.enabled = false; for (let i = FLOW_BINDS.length - 1; i >= 0; i--) if (FLOW_BINDS[i].flowGuid === guid) FLOW_BINDS.splice(i, 1); return true; } if (/flow\/Bind/i.test(url)) { const scope = Number(body?.scope ?? 0); const scopeKey = scope === 1 ? String(body?.scopeKey || "") : "-"; const f = CENTER_FLOWS.find((x) => x.flowGuid === guid); if (!f) return true; const hit = FLOW_BINDS.find((b) => b.scope === scope && b.scopeKey === scopeKey); const row = { id: hit ? hit.id : BIND_SEQ++, scope, scopeKey, scopeName: scope === 0 ? "全校默认" : TYPES.find((t) => t.CodeID === scopeKey)?.Name || scopeKey, flowGuid: f.flowGuid, flowName: f.name, flowVersion: f.version, nodeSummary: f.nodeSummary, existsInCenter: true, }; if (hit) Object.assign(hit, row); else FLOW_BINDS.push(row); return true; } if (/flow\/Unbind/i.test(url)) { const scope = Number(body?.scope ?? 0); const scopeKey = scope === 1 ? String(body?.scopeKey || "") : "-"; for (let i = FLOW_BINDS.length - 1; i >= 0; i--) { if (FLOW_BINDS[i].scope === scope && FLOW_BINDS[i].scopeKey === scopeKey) FLOW_BINDS.splice(i, 1); } return true; } return false; } // ===== 请假列表(/api/leave/List):我的请假 / 全部请假 / 抄送列表 / 审批列表 共用一个接口 ===== // 复刻 LeaveVo;状态 0审批中 1已通过 2已拒绝 3已销假 4已撤回 const LEAVE_ROWS = [ { id: 1, leaveType: "sj", leaveTypeName: "事假", startTime: "2026-10-22 08:00:00", endTime: "2026-10-22 12:00:00", totalHours: 4, reason: "E2E扣减链路验证-事假", status: 0, canEdit: true, canWithdraw: true, canDelete: true }, { id: 2, leaveType: "sj", leaveTypeName: "事假", startTime: "2026-10-21 08:00:00", endTime: "2026-10-21 12:00:00", totalHours: 4, reason: "E2E扣减链路验证-事假", status: 4, canEdit: true, canDelete: true }, { id: 3, leaveType: "tx", leaveTypeName: "调休", startTime: "2026-10-20 08:00:00", endTime: "2026-10-20 12:00:00", totalHours: 4, reason: "E2E回调链路验证-调休", status: 0, canEdit: true, canWithdraw: true, canDelete: true }, { id: 4, leaveType: "cq", leaveTypeName: "产假", startTime: "2026-10-09 08:00:00", endTime: "2026-10-18 17:00:00", totalHours: 80, reason: "1111", status: 1, canCancel: true }, { id: 5, leaveType: "bj", leaveTypeName: "病假", startTime: "2026-09-15 13:00:00", endTime: "2026-09-15 17:00:00", totalHours: 4, reason: "感冒发热,需居家休息", status: 1, canCancel: true }, { id: 6, leaveType: "hq", leaveTypeName: "婚假", startTime: "2026-08-03 08:00:00", endTime: "2026-08-07 17:00:00", totalHours: 40, reason: "婚假(含往返)", status: 2 }, { id: 7, leaveType: "sq", leaveTypeName: "丧假", startTime: "2026-06-11 08:00:00", endTime: "2026-06-13 17:00:00", totalHours: 24, reason: "家中长辈丧事", status: 3 }, { id: 8, leaveType: "sj", leaveTypeName: "事假", startTime: "2026-05-06 13:00:00", endTime: "2026-05-06 17:00:00", totalHours: 4, reason: "子女家长会", status: 1, canCancel: true }, ]; function leaveList(url) { const q = new URL(url, "http://x").searchParams; const scope = q.get("scope") || "user"; const status = q.get("status"); const kw = (q.get("keyword") || "").trim(); const type = q.get("leaveType"); const pageIndex = Number(q.get("pageIndex") || 1); const pageSize = Number(q.get("pageSize") || 10); let rows = LEAVE_ROWS.map((r, i) => ({ ...r, userId: 1, userName: scope === "user" ? "王雅静" : USERS[i % USERS.length].userName, statusName: ["审批中", "已通过", "已拒绝", "已销假", "已撤回"][r.status], applyTime: `2026-10-0${(i % 9) + 1} 0${(i % 9) + 1}:3${i % 10}:${10 + i}`, })); if (scope === "cc") rows = rows.map((r) => ({ ...r, ccTime: r.applyTime })); if (status !== null && status !== undefined && status !== "") rows = rows.filter((r) => String(r.status) === String(status)); if (type) rows = rows.filter((r) => r.leaveType === type); if (kw) rows = rows.filter((r) => (r.reason || "").includes(kw) || r.leaveTypeName.includes(kw) || r.userName.includes(kw)); const total = rows.length; const content = rows.slice((pageIndex - 1) * pageSize, pageIndex * pageSize); return { content, total, pageIndex, pageSize }; } function resolve(url) { // 应用外壳:用户信息 / 授权模块(整壳验收用) if (/Base\/GetUserMod|base\/GetUserMod/i.test(url)) return USER_MOD; if (/leave\/List/i.test(url)) return leaveList(url); if (/Base\/GetUserInfo|base\/GetUserInfo/i.test(url)) return USER_INFO; if (/GetSchoolAll/.test(url)) return TREE; if (/UserTimeList/.test(url)) { const kw = new URL(url, "http://x").searchParams.get("keyword"); return kw ? USERS.filter((u) => u.userName.includes(kw)) : USERS; } // 下拉接口返回 { k, v }(与后端 DictService.List 一致);管理列表返回完整字段 if (/Dict\/List/i.test(url)) return TYPES.map((t) => ({ k: t.CodeID, v: t.Name })); if (/\/dict|Dict/.test(url)) return TYPES; if (/Option/.test(url)) return OPTION; if (/TimeSet/.test(url)) return TIMESET; if (/Section/.test(url)) return []; if (/leave\/flow\/List/i.test(url)) return flowManageData(); return null; } http .createServer((req, res) => { console.log("[mock] ->", req.method, req.url); const chunks = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => { let body = null; const raw = Buffer.concat(chunks).toString("utf8"); if (raw) { try { body = JSON.parse(raw); } catch { body = Object.fromEntries(new URLSearchParams(raw)); } } const url = req.url || ""; let data; if (req.method === "POST" && /leave\/flow\/(Enable|Disable|Bind|Unbind)/i.test(url)) { flowMutate(url, body); data = undefined; // 写接口返回 data:null } else { data = resolve(url); } res.setHeader("Content-Type", "application/json;charset=utf-8"); res.setHeader("Access-Control-Allow-Origin", "*"); res.end(JSON.stringify({ code: 0, msg: "ok", data: data === undefined ? null : data })); }); }) .listen(8109, () => console.log("mock server on http://localhost:8109"));