/** * 校园通知本地 Mock 数据(vite-plugin-mock,仅在 `npm run dev:mock`(--mode mock)下生效)。 * * 契约对齐 src/api/module/notice.ts(P0 标准化): * - /api/notice/{List,ReadTabCount,SendList,Detail,GetDetail,GetNextPrev,ReadDetail,Create,Edit,Delete, * InitializeRed,UserPull,SetTopmost,Expedite,ReceiveUsers,Statis} * - /api/notice/{ApprovalList,ApprovalCount,Approve,Reject} ← 审批(可选流程) * - /api/dict/{List,Sync,ManageList,Add,Update,Delete,Sort} * - /api/base/{GetUserInfo,GetUserMod,GetLayout,GetSchoolAll,GetClassTree,FileUpload} * - /api/contacts/* /api/file/{UploadImage,Preview} * 统一响应 {code,msg,data},由 alova-core 解包为 data(code==0)。 * * 校园通知(学生场景)关键点: * 1. 通知对象到「班级」一级:receiveList = "ALL" 或 班级 id 逗号串;应读/已读人数按班级学生数累加。 * 2. 审批可选:approvalEnabled=1 的通知先入 pending,审批通过(approved)后才对学生可见; * approvalEnabled=0 直接 none 发布。rejected 退回发布人可编辑重提。 * 3. 当前登录演示身份 = 发布教师 朱皓东(zhd);收件箱以「初一(1)班(1301)」视角判定已读/未读。 */ /* ============================ 基础数据 ============================ */ /** 发文部门(发布单位)字典 */ const deptDict = [ { SerID: 101, CodeID: "D001", Name: "校长室", Enable: 1 }, { SerID: 102, CodeID: "D002", Name: "学校办公室", Enable: 1 }, { SerID: 103, CodeID: "D003", Name: "教务处", Enable: 1 }, { SerID: 104, CodeID: "D004", Name: "学生处", Enable: 1 }, { SerID: 105, CodeID: "D005", Name: "总务处", Enable: 1 }, { SerID: 106, CodeID: "D006", Name: "信息中心", Enable: 1 }, { SerID: 107, CodeID: "D007", Name: "英语教研组", Enable: 0 }, { SerID: 108, CodeID: "D008", Name: "语文教研组", Enable: 1 }, ]; let deptSeq = 109; let nextFileId = 9001; /** 当前登录用户(发布教师) */ const ME = { account: "zhd", name: "朱皓东" }; /** 收件箱演示视角:所在班级 id(已读/未读按该班判定) */ const ME_CLASS = "1301"; /** 审批人演示数据(v18:审核设置维护端点已删,仅 mock 审批流/ApproverSuggest 记忆消费) */ const approvers = [ { id: 1, account: "zhd", name: "朱皓东", deptId: "D001", deptName: "校长室", enabled: 1, teaId: 1 }, { id: 2, account: "lwj", name: "刘文娟", deptId: "D002", deptName: "学校办公室", enabled: 1, teaId: 2 }, { id: 3, account: "why", name: "王海燕", deptId: "D004", deptName: "学生处", enabled: 1, teaId: 4 }, { id: 4, account: "hy", name: "黄瑶", deptId: "D006", deptName: "信息中心", enabled: 1, teaId: 11 }, { id: 5, account: "cy", name: "陈垠", deptId: "D003", deptName: "教务处", enabled: 1, teaId: 3 }, { id: 6, account: "zm", name: "周敏", deptId: "D003", deptName: "教务处", enabled: 1, teaId: 7 }, ]; const approverName = (account: string) => approvers.find((a) => a.account === account)?.name || account; /** * 审批流 = 有序多步;每步 { name, vote, approvers:[account...] }。 * - vote=all 会签:该步全部审批人通过才进入下一步; * - vote=any 或签:该步任一人通过即进入下一步;任一步有人驳回 → 整单驳回。 * 「选人即送审」:发布时选了审批人就自动生成流程。 */ type FlowLevel = { name: string; vote: "all" | "any"; approvers: string[]; records: { account: string; name: string; result: "approved" | "rejected"; time: string; remark: string }[] }; /** 由发布入参构建流程:flow=[{name,vote,approvers}] 多步;否则 approvers=[] 单步(默认或签) */ function buildFlowFromInput(input: any): FlowLevel[] | null { const mk = (name: string, vote: "all" | "any", accs: string[]): FlowLevel => ({ name, vote, approvers: accs.filter(Boolean), records: [] }); const src = Array.isArray(input?.flow) ? input.flow : null; if (src && src.length) { const levels = src .map((l: any, i: number) => mk(String(l.name || `第${i + 1}步`), l.vote === "all" ? "all" : "any", (l.approvers || []).map(String))) .filter((l: FlowLevel) => l.approvers.length); return levels.length ? levels : null; } const accs = (input?.approvers || []).map(String).filter(Boolean); return accs.length ? [mk("审批", input?.vote === "all" ? "all" : "any", accs)] : null; } /** 当前步尚未处理的审批人 */ function pendingApprovers(n: any): string[] { const lv = (n.flow || [])[n.approvalLevel || 0]; if (!lv) return []; const acted = new Set(lv.records.map((r: any) => r.account)); return lv.approvers.filter((a: string) => !acted.has(a)); } /** 当前用户是否为该单当前步的待签审批人 */ function canAct(n: any, account = ME.account) { return n.approvalStatus === "pending" && pendingApprovers(n).includes(account); } /** 状态展示文本(含多步进度) */ function flowProgressText(n: any): string { if (n.approvalStatus !== "pending" || !n.flow) return APPROVAL_DESC[n.approvalStatus] || "已发布"; const lv = n.flow[n.approvalLevel || 0]; if (!lv) return "待审批"; const done = lv.records.filter((r: any) => r.result === "approved").length; const tag = lv.vote === "all" ? `第${(n.approvalLevel || 0) + 1}/${n.flow.length}步 ${lv.name}·会签 ${done}/${lv.approvers.length}` : `第${(n.approvalLevel || 0) + 1}/${n.flow.length}步 ${lv.name}·或签`; return `待审批(${tag})`; } /** v7:最新轮末条处理意见(列表「审批意见」列):驳回=驳回意见/通过=末步通过意见;pending/免审=null */ function lastOpinionOf(n: any): string | null { if (n.approvalStatus !== "approved" && n.approvalStatus !== "rejected") return null; const recs = (n.flow || []).flatMap((lv: FlowLevel) => lv.records); if (!recs.length) return null; return recs[recs.length - 1].remark || null; } /** 年级 → 班级树(叶子=班级,带学生数) */ const gradeClasses = [ { gradeId: 101, gradeName: "初一", classes: [ { classId: 1301, className: "初一(1)班", studentCount: 45 }, { classId: 1302, className: "初一(2)班", studentCount: 44 }, { classId: 1303, className: "初一(3)班", studentCount: 46 }, ], }, { gradeId: 102, gradeName: "初二", classes: [ { classId: 1401, className: "初二(1)班", studentCount: 43 }, { classId: 1402, className: "初二(2)班", studentCount: 45 }, ], }, { gradeId: 103, gradeName: "初三", classes: [ { classId: 1501, className: "初三(1)班", studentCount: 42 }, { classId: 1502, className: "初三(2)班", studentCount: 44 }, ], }, ]; /** classId → { className, gradeName, studentCount } */ const classMap: Record = {}; gradeClasses.forEach((g) => g.classes.forEach((c) => (classMap[String(c.classId)] = { className: c.className, gradeName: g.gradeName, studentCount: c.studentCount })) ); const ALL_CLASSES = Object.keys(classMap); /** 全校学生数(接收对象为「全部班级」时的应读人数) */ const ALL_STUDENTS = ALL_CLASSES.reduce((s, id) => s + classMap[id].studentCount, 0); /** * 通知存储。classRead = 已读班级 id 列表(班级维度阅读统计)。 * approvalEnabled/approvalStatus:见文件头说明;pending/rejected 不进入学生收件箱。 */ const notices = [ { id: 1, title: "关于开展 2026 年秋季学期期中教学检查的通知", deptId: "D001", deptName: "校长室", publisherAccount: "lwj", publisherName: "刘文娟", content: "

各班级:

学校定于第 10 周开展秋季学期期中教学检查,请各班配合任课教师准备相关材料。

具体安排另行通知。

", receiveList: "ALL", classRead: ["1301", "1302", "1303", "1401"], files: "9001,9002", topmost: 1, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-01 10:35:00", approvalEnabled: 0, approvalStatus: "none", approvalRemark: null, approvalBy: null, approvalTime: null, }, { id: 2, title: "校园网络安全与个人信息保护讲座通知", deptId: "D006", deptName: "信息中心", publisherAccount: "hy", publisherName: "黄瑶", content: "

定于本周五下午 15:00 在学术报告厅开展网络安全与个人信息保护讲座,各班按课表有序参加。

", receiveList: "ALL", classRead: ["1401", "1402"], files: null, topmost: 0, isPulled: 0, smsFlag: 1, timingSendTime: null, createTime: "2026-09-03 14:22:00", approvalEnabled: 1, approvalStatus: "approved", approvalRemark: "同意发布", approvalBy: "张校长", approvalTime: "2026-09-03 16:00:00", }, { id: 3, title: "初一年级家长会通知", deptId: "D004", deptName: "学生处", publisherAccount: "zhd", publisherName: "朱皓东", content: "

初一年级定于下周三 19:00 召开家长会,请初一(1)(2)班班主任通知到每位家长。

", receiveList: "1301,1302", classRead: [], files: null, topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-08 09:00:00", approvalEnabled: 1, approvalStatus: "pending", approvalRemark: null, approvalBy: null, approvalTime: null, }, { id: 4, title: "国庆假期作业与安全提示(定时发送)", deptId: "D003", deptName: "教务处", publisherAccount: "zhd", publisherName: "朱皓东", content: "

放假前各科老师请布置假期作业,各班完成假期安全提醒。

", receiveList: "ALL", classRead: [], files: null, topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: "2026-09-30 08:00:00", createTime: "2026-09-05 16:40:00", approvalEnabled: 0, approvalStatus: "none", approvalRemark: null, approvalBy: null, approvalTime: null, }, { id: 5, title: "第十二届校园科技节活动方案通知", deptId: "D003", deptName: "教务处", publisherAccount: "zhd", publisherName: "朱皓东", content: "

第十二届校园科技节报名启动,详见附件方案,各班积极组织学生参加。

", receiveList: "ALL", classRead: ["1301", "1302", "1501"], files: "9003", topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-06 11:05:00", approvalEnabled: 1, approvalStatus: "approved", approvalRemark: "内容合规,同意", approvalBy: "张校长", approvalTime: "2026-09-06 14:30:00", }, { id: 6, title: "学生体质健康测试数据采集通知", deptId: "D004", deptName: "学生处", publisherAccount: "cy", publisherName: "陈垠", content: "

请各班主任配合体育组完成本学期学生体质健康测试,测试地点为田径场,时间以班级通知为准。

", receiveList: "ALL", classRead: ["1401", "1501"], files: null, topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-07 08:30:00", approvalEnabled: 0, approvalStatus: "none", approvalRemark: null, approvalBy: null, approvalTime: null, }, { id: 7, title: "2026 年秋季田径运动会报名通知", deptId: "D004", deptName: "学生处", publisherAccount: "cy", publisherName: "陈垠", content: "

秋季田径运动会定于 10 月下旬举行,请各班于本周五前完成报名并提交至体育组。

", receiveList: "ALL", classRead: [], files: null, topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-09 10:10:00", approvalEnabled: 1, approvalStatus: "pending", approvalRemark: null, approvalBy: null, approvalTime: null, }, { id: 8, title: "校园开放日邀请函(待修改)", deptId: "D002", deptName: "学校办公室", publisherAccount: "zhd", publisherName: "朱皓东", content: "

拟定于 9 月 20 日举办校园开放日,向家长开放。

", receiveList: "ALL", classRead: [], files: null, topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-08 15:20:00", approvalEnabled: 1, approvalStatus: "rejected", approvalRemark: "请补充开放日流程与安保方案后再提交", approvalBy: "张校长", approvalTime: "2026-09-09 09:00:00", }, ]; let noticeSeq = 9; /** 给需要审批的种子通知构建多步流程(演示会签/或签与不同进度) */ function seedFlows() { const twoSteps = () => buildFlowFromInput({ flow: [{ name: "校级审核", vote: "all", approvers: ["zhd", "lwj"] }, { name: "教务处复核", vote: "any", approvers: ["cy", "zm", "why"] }] })!; notices.forEach((n: any) => { if (!n.approvalEnabled) { n.flow = null; n.approvalLevel = 0; return; } const flow = twoSteps(); n.flow = flow; n.approvalLevel = 0; if (n.approvalStatus === "approved") { flow.forEach((lv) => lv.approvers.forEach((a) => lv.records.push({ account: a, name: approverName(a), result: "approved", time: n.approvalTime || "2026-09-06 14:30:00", remark: "同意" }))); n.approvalLevel = flow.length; } else if (n.approvalStatus === "rejected") { n.flow = buildFlowFromInput({ flow: [{ name: "校级审核", vote: "any", approvers: ["zhd", "lwj"] }] })!; n.approvalLevel = 0; n.flow[0].records.push({ account: "zhd", name: approverName("zhd"), result: "rejected", time: n.approvalTime || "2026-09-09 09:00:00", remark: n.approvalRemark || "已驳回" }); } }); // 运动会(id7):校级会签已过 1/2,等待刘文娟 const n7 = notices.find((n: any) => n.id === 7); if (n7?.flow) n7.flow[0].records.push({ account: "zhd", name: approverName("zhd"), result: "approved", time: "2026-09-09 15:00:00", remark: "内容属实" }); } seedFlows(); /** 附件存储 */ const filesStore: Record = { 9001: { id: 9001, filename: "期中教学检查安排表.xlsx", fileTrueName: "期中教学检查安排表.xlsx", size: 57344 }, 9002: { id: 9002, filename: "检查评分标准.docx", fileTrueName: "检查评分标准.docx", size: 122880 }, 9003: { id: 9003, filename: "校园科技节活动方案.pdf", fileTrueName: "校园科技节活动方案.pdf", size: 819200 }, }; /** 自定义联系组(教师协作组,成员为 userId) */ const groups = [ { serid: 1, GroupName: "语文教研组", members: [1, 2, 3] }, { serid: 2, GroupName: "信息中心", members: [10, 11] }, { serid: 3, GroupName: "初一年级组", members: [4, 6, 8] }, ]; let groupSeq = 4; const idNames: Record = { 1: "朱皓东", 2: "刘文娟", 3: "陈垠", 4: "王海燕", 5: "李强", 6: "何明明", 7: "周敏", 8: "孙丽娟", 9: "吴建国", 10: "赵晓琳", 11: "黄瑶", }; const teacherNames: Record = { zhd: "朱皓东", lwj: "刘文娟", lwm: "刘文娟", cy: "陈垠", hy: "黄瑶", hj: "何明明", why: "王海燕", lq: "李强", zm: "周敏", slj: "孙丽娟", wjg: "吴建国", zxl: "赵晓琳", }; /** 教师组织树(应用设置-自定义组选人在用;发布对象已改为班级树) */ const schoolTree = [ { id: 301, name: "校长室", isParent: true, children: [ { id: 1, name: "朱皓东", isParent: false }, { id: 2, name: "刘文娟", isParent: false } ] }, { id: 302, name: "教务处", isParent: true, children: [ { id: 3, name: "陈垠", isParent: false }, { id: 6, name: "何明明", isParent: false }, { id: 7, name: "周敏", isParent: false } ] }, { id: 303, name: "学生处", isParent: true, children: [ { id: 4, name: "王海燕", isParent: false }, { id: 8, name: "孙丽娟", isParent: false }, { id: 9, name: "吴建国", isParent: false } ] }, { id: 304, name: "总务处", isParent: true, children: [ { id: 5, name: "李强", isParent: false } ] }, { id: 305, name: "信息中心", isParent: true, children: [ { id: 10, name: "赵晓琳", isParent: false }, { id: 11, name: "黄瑶", isParent: false } ] }, ]; /** 年级班级树(ztree 结构,供选班级弹窗使用;叶子带 studentCount) */ const classTree = gradeClasses.map((g) => ({ id: g.gradeId, name: g.gradeName, isParent: true, children: g.classes.map((c) => ({ id: c.classId, name: c.className, isParent: false, studentCount: c.studentCount })), })); /* ============================ 工具 ============================ */ function ok(data: any) { return { code: 0, msg: "操作成功", data }; } function fail(msg: string, code = 4) { return { code, msg, data: null }; } function paginate(content: any[], pageIndex: number, pageSize: number) { const start = (pageIndex - 1) * pageSize; const totalPages = pageSize ? Math.ceil(content.length / pageSize) : 0; return { content: content.slice(start, start + pageSize), total: content.length, pageIndex, pageSize, totalPages, hasNext: pageIndex < totalPages }; } function parseBody(body: any): any { if (body == null) return {}; if (typeof body === "object") return body; const str = String(body).trim(); if (!str) return {}; if (str.startsWith("{") || str.startsWith("[")) { try { return JSON.parse(str); } catch { /* fallthrough */ } } const out: Record = {}; new URLSearchParams(str).forEach((v, k) => (out[k] = v)); return out; } /** 合并取参:query(postForm 已把参数拼入 URL)优先兜底 + JSON body(Create/Edit/Delete) */ function bodyOf(rawBody: any, query: any): any { const parsed = parseBody(rawBody); if (Array.isArray(parsed)) return parsed; // Delete 传 id 数组 return { ...(query || {}), ...(parsed || {}) }; } /** 接收班级 id 数组("ALL" → 全部班级) */ function targetClassIds(n: any): string[] { return /^all$/i.test(String(n.receiveList).trim()) ? ALL_CLASSES : String(n.receiveList).split(",").map((s) => s.trim()).filter(Boolean); } function isAllTarget(n: any): boolean { return /^all$/i.test(String(n.receiveList).trim()); } function allowCountOf(n: any): number { if (isAllTarget(n)) return ALL_STUDENTS; return targetClassIds(n).reduce((s, id) => s + (classMap[id]?.studentCount || 0), 0); } function readCountOf(n: any): number { const targets = targetClassIds(n); return (n.classRead || []).filter((id: string) => targets.includes(id)).reduce((s, id) => s + (classMap[id]?.studentCount || 0), 0); } /** 收件箱视角:本班是否已读 */ function isReadByMe(n: any): boolean { return (n.classRead || []).includes(ME_CLASS); } /** 是否已发布(对学生可见):审批通过或免审 */ function isPublished(n: any): boolean { return n.approvalStatus === "none" || n.approvalStatus === "approved"; } /** 定时未到的通知,收件箱不展示 */ function isScheduledFuture(n: any): boolean { return !!(n.timingSendTime && n.timingSendTime > "2026-09-10"); } const APPROVAL_DESC: Record = { none: "已发布", pending: "待审批", approved: "已发布", rejected: "已驳回" }; function toVo(n: any) { return { id: n.id, title: n.title, content: n.content, type: 0, typeDesc: "普通", publisherAccount: n.publisherAccount, publisherName: n.publisherName, deptId: n.deptId, deptName: n.deptName, status: isReadByMe(n) ? 2 : 1, statusDesc: isReadByMe(n) ? "已读" : "未读", topmost: n.topmost, readCount: readCountOf(n), allowCount: allowCountOf(n), read: isReadByMe(n), isPulled: n.isPulled, timingSendTime: n.timingSendTime, parentId: null, smsFlag: n.smsFlag, receiveType: "class", receiveList: n.receiveList, approvalEnabled: n.approvalEnabled, approvalStatus: n.approvalStatus, approvalStatusDesc: flowProgressText(n), approvalRemark: n.approvalRemark, approvalBy: n.approvalBy, approvalTime: n.approvalTime, approvalLevel: n.approvalLevel || 0, canApprove: canAct(n), pendingApproverNames: n.approvalStatus === "pending" ? pendingApprovers(n).map(approverName) : null, lastOpinion: lastOpinionOf(n), createBy: n.publisherAccount, createTime: n.createTime, updateBy: n.publisherAccount, updateTime: n.createTime, UpFile: n.files, }; } /** 接收对象可读文本:全部班级 / 班级名顿号串 */ function receiveText(n: any): string { if (isAllTarget(n)) return "全部班级"; return targetClassIds(n) .map((id) => classMap[id]?.className || id) .join("、"); } function toDetail(n: any) { return { serid: n.id, title: n.title, Contents: n.content, DeptID: n.deptId, DeptName: n.deptName, TeaName: n.publisherName, EditDate: n.createTime, numcount: allowCountOf(n), readnumcount: readCountOf(n), ReceiveList: n.receiveList, receiveType: "class", receiveText: receiveText(n), DsfsTime: n.timingSendTime, isDsfs: !!n.timingSendTime, approvalEnabled: n.approvalEnabled, approvalStatus: n.approvalStatus, approvalStatusDesc: flowProgressText(n), approvalRemark: n.approvalRemark, approvalBy: n.approvalBy, approvalTime: n.approvalTime, approvalLevel: n.approvalLevel || 0, canApprove: canAct(n), pendingApproverNames: n.approvalStatus === "pending" ? pendingApprovers(n).map(approverName) : null, lastOpinion: lastOpinionOf(n), // 审批流进度(供详情时间线 & 发布/编辑回显) approvalFlow: (n.flow || []).map((lv: any, idx: number) => { const acted = new Set(lv.records.map((r: any) => r.account)); return { name: lv.name, vote: lv.vote, approvers: [...lv.approvers], approverNames: lv.approvers.map(approverName), status: n.approvalStatus === "approved" || idx < (n.approvalLevel || 0) ? "approved" : n.approvalStatus === "rejected" && idx === (n.approvalLevel || 0) ? "rejected" : idx === (n.approvalLevel || 0) ? "pending" : "waiting", records: lv.records.map((r: any) => ({ ...r })), pending: idx === (n.approvalLevel || 0) && n.approvalStatus === "pending" ? lv.approvers.filter((a: string) => !acted.has(a)).map(approverName) : [], }; }), // 审批轨迹(v15 对齐后端 ApprovalRecordVo:提交/通过/驳回按时间线,姓名取快照) approvalRecords: approvalTrailOf(n), ChaoSend: (n.files ? String(n.files).split(",") : []).map((id: string) => { const f = filesStore[Number(id)]; return f ? { id: f.id, name: f.fileTrueName, size: f.size, fileid: f.id, filename: f.filename } : { id: Number(id), name: "附件", size: 0, fileid: Number(id), filename: "附件" }; }), tlist: isAllTarget(n) ? "all" : targetClassIds(n), }; } /** 审批轨迹(v15 dev:mock 对齐后端 approvalRecords):提交行置顶 + 各步审核记录按时间升序 */ function approvalTrailOf(n: any) { if (!(n.flow || []).length) return []; const rows: any[] = [ { action: 1, actionDesc: "提交审批", operatorAccount: n.publisherAccount, operatorName: n.publisherName, opinion: "", actionTime: n.createTime, }, ]; (n.flow || []).forEach((lv: any) => { (lv.records || []).forEach((r: any) => { rows.push({ action: r.result === "rejected" ? 3 : 2, actionDesc: r.result === "rejected" ? "驳回" : "通过", operatorAccount: r.account, operatorName: r.name, opinion: r.remark || "", actionTime: r.time, }); }); }); return rows.sort((a, b) => String(a.actionTime).localeCompare(String(b.actionTime))); } function sortByTimeDesc(list: any[]) { return [...list].sort((a, b) => (a.createTime < b.createTime ? 1 : a.createTime > b.createTime ? -1 : 0)); } function applyFilters(list: any[], query: any) { let out = [...list]; const kw = query.keyword || query.Name; if (kw) out = out.filter((n) => n.title.includes(kw) || n.publisherName.includes(kw) || n.deptName.includes(kw)); const dept = query.deptId || query.Depttype; if (dept) out = out.filter((n) => n.deptId === dept); const st = query.startTime || query.starttime; const et = query.endTime || query.endtime; if (st) out = out.filter((n) => n.createTime.slice(0, 10) >= String(st).slice(0, 10)); if (et) out = out.filter((n) => n.createTime.slice(0, 10) <= String(et).slice(0, 10)); return out; } function nowStr() { const now = new Date(); const pad = (n: number) => String(n).padStart(2, "0"); return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; } /** * 收件箱(已收到)可见集(v17):已发布 + 定时已到点 + 未撤回 + 本班在范围内, * 再套 keyword/deptId/时间筛选与 readType 分桶,顺序与后端一致(时间倒序)。 * List / ReadTabCount / GetNextPrev 三个 mock 接口共用,避免各自写一份过滤而口径漂移。 */ function inboxVisible(query: any = {}) { let list = notices.filter((n) => { if (n.isPulled || !isPublished(n) || isScheduledFuture(n)) return false; return isAllTarget(n) || targetClassIds(n).includes(ME_CLASS); }); list = sortByTimeDesc(applyFilters(list, query || {})); const rt = Number((query && query.readType) || 0); if (rt === 1) list = list.filter((n) => !isReadByMe(n)); else if (rt === 2) list = list.filter((n) => isReadByMe(n)); return list; } /* ============================ Mock 定义 ============================ */ export default [ /* ---------- 布局 / 用户 ---------- */ { url: "/api/base/GetUserInfo", method: "get", response: () => ok({ id: 186532, account: ME.account, name: ME.name, profilePicture: "", school: { id: 3260, name: "上海市第一实验中学" } }), }, { url: "/api/base/GetUserMod", method: "get", response: () => ok([ { id: 1, modId: 1, name: "已收到", parentId: 0, page: "/", orderNumber: 1, usable: true, children: [] }, { id: 2, modId: 2, name: "已发送", parentId: 0, page: "/list", orderNumber: 2, usable: true, children: [] }, { id: 3, modId: 3, name: "全部通知", parentId: 0, page: "/noticelist", orderNumber: 3, usable: true, children: [] }, { id: 5, modId: 5, name: "审批列表", parentId: 0, page: "/approval", orderNumber: 4, usable: true, children: [] }, { id: 6, modId: 6, name: "审核设置", parentId: 0, page: "/approval-setting", orderNumber: 5, usable: true, children: [] }, { id: 4, modId: 4, name: "通知统计", parentId: 0, page: "/stats", orderNumber: 6, usable: true, children: [] }, { id: 21, modId: 21, name: "应用设置", parentId: 0, page: "/setting", orderNumber: 7, usable: true, children: [] }, ]), }, { url: "/api/base/GetLayout", method: "get", response: () => ok({ ssoUrl: "", username: ME.name, pathList: [], topName: "校园公告", pageUrl: "", logoUrl: "", initUrl: "", id: 0, avatar: "" }), }, { url: "/api/base/GetSchoolAll", method: "get", response: () => ok(schoolTree) }, { // 年级 → 班级树(发布对象选择:到班级一级) url: "/api/base/GetClassTree", method: "get", response: () => ok(classTree), }, { // 学员组织树(学校→学段→年级→班级,组节点树;toUser=true 时的学生叶子本 mock 不涉及) url: "/api/base/getStuTree", method: "get", response: () => { const group = (id: number, name: string, pid: number, no: number, children: any[] | null) => ({ id, name, pid, no, type: 1, children }); const grades = gradeClasses.map((g, gi) => group(g.gradeId, g.gradeName, 9101, gi + 1, g.classes.map((c, ci) => group(c.classId, c.className, g.gradeId, ci + 1, null))) ); return ok([group(9001, "示范初级中学", 0, 1, [group(9101, "初中部", 9001, 1, grades)])]); }, }, { // 教师组织树(学校→部门组→教师;节点结构对齐 SchTreeNodeDto:type 1组/2人员、人员叶子 children=null) url: "/api/base/getTeaTree", method: "get", response: () => { const teaGroups = schoolTree.map((d, di) => ({ id: d.id, name: d.name, pid: 9001, no: di + 1, type: 1, isParent: true, photo: "", children: (d.children || []).map((t: any) => ({ id: t.id, name: t.name, pid: d.id, no: 0, type: 2, isParent: false, photo: "", children: null, })), })); return ok([{ id: 9001, name: "示范初级中学", pid: 0, no: 1, type: 1, isParent: true, photo: "", children: teaGroups }]); }, }, /* ---------- 发文部门字典 ---------- */ { url: "/api/dict/List", method: "get", response: ({ query }: any) => ok(deptDict.filter((d) => d.Enable === 1 || query.type !== "1").map((d) => ({ k: d.CodeID, v: d.Name }))), }, { url: "/api/dict/Sync", method: "get", response: () => ok(true) }, { url: "/api/dict/ManageList", method: "get", response: () => ok(deptDict.map((d) => ({ ...d }))) }, { url: "/api/dict/Add", method: "post", response: ({ body, query }: any) => { const name = bodyOf(body, query).name || "未命名部门"; deptDict.push({ SerID: deptSeq, CodeID: "D" + String(deptSeq).padStart(3, "0"), Name: name, Enable: 1 }); deptSeq++; return ok(null); }, }, { url: "/api/dict/Update", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const item = deptDict.find((d) => d.SerID === Number(b.SerID)); if (item) { if (b.Name) item.Name = b.Name; if (b.Enable !== undefined && b.Enable !== null && b.Enable !== "") item.Enable = Number(b.Enable); } return ok(null); }, }, { url: "/api/dict/Delete", method: "post", response: ({ body, query }: any) => { const i = deptDict.findIndex((d) => d.SerID === Number(bodyOf(body, query).serid)); if (i > -1) deptDict.splice(i, 1); return ok(null); }, }, { url: "/api/dict/Sort", method: "post", response: ({ body, query }: any) => { const ids = String(bodyOf(body, query).ids || "").split(",").map(Number).filter(Boolean); const sorted = ids.map((id) => deptDict.find((d) => d.SerID === id)).filter(Boolean) as any[]; if (sorted.length === deptDict.length) deptDict.splice(0, deptDict.length, ...sorted); return ok(null); }, }, /* ---------- 通知列表 ---------- */ { // 已收到(学生/收件视角):仅已发布、非未来定时、面向本班 url: "/api/notice/List", method: "get", response: ({ query }: any) => { const pageIndex = Number(query.pageIndex || 1); const pageSize = Number(query.pageSize || 50); const list = inboxVisible(query); // 顶部「未读 N 条」是全局未读,不随 keyword 变化(v17 与后端 countUnreadForCurrentUser 同口径) const unreadCount = inboxVisible({ readType: 1 }).length; return ok({ ...paginate(list.map(toVo), pageIndex, pageSize), unreadCount }); }, }, { // 「已收到」三 tab 统计(v17):与 List 同一过滤,readType 不作为入参 url: "/api/notice/ReadTabCount", method: "get", response: ({ query }: any) => { const filters = { keyword: query.keyword, type: query.type, deptId: query.deptId, startTime: query.startTime, endTime: query.endTime, }; const all = inboxVisible({ ...filters, readType: 0 }).length; const unread = inboxVisible({ ...filters, readType: 1 }).length; return ok({ all, unread, read: all - unread }); }, }, { // 已发送(scope=user) / 全部通知(不传) url: "/api/notice/SendList", method: "get", response: ({ query }: any) => { const pageIndex = Number(query.pageIndex || 1); const pageSize = Number(query.pageSize || 10); let list = [...notices]; if (query.scope === "user") list = list.filter((n) => n.publisherAccount === ME.account); list = sortByTimeDesc(applyFilters(list, query)); return ok(paginate(list.map(toVo), pageIndex, pageSize)); }, }, /* ---------- 通知详情 ---------- */ { url: "/api/notice/Detail", method: "get", response: ({ query }: any) => { const n = notices.find((x) => x.id === Number(query.serid)); if (!n) return fail("通知不存在或已被删除"); if (String(query.type) !== "0" && !n.classRead.includes(ME_CLASS)) n.classRead.push(ME_CLASS); return ok(toDetail(n)); }, }, { url: "/api/notice/GetDetail", method: "get", response: ({ query }: any) => { const n = notices.find((x) => x.id === Number(query.serid)); if (!n) return fail("通知不存在或已被删除"); return ok(toDetail(n)); }, }, { url: "/api/notice/GetNextPrev", method: "get", response: ({ query }: any) => { // 与后端 v17 对齐:type=0 已发(本人发布);type=1 已收(可见集合含未读,忽略 readType,沿用筛选) // 只显式挑筛选字段:本接口的 query.type 是「序列类型」,不能混进筛选 // (公告重要程度后端走 noticeType;mock 数据没有重要程度字段,故不参与过滤) const filters = { keyword: query.keyword, deptId: query.deptId, startTime: query.startTime, endTime: query.endTime, readType: 0 }; const seq = String(query.type) === "0" ? sortByTimeDesc(notices.filter((n) => n.publisherAccount === ME.account)) : inboxVisible(filters); const ids = seq.map((n) => n.id); const i = ids.indexOf(Number(query.serid)); return ok({ prevId: i > 0 ? ids[i - 1] : 0, nextId: i > -1 && i < ids.length - 1 ? ids[i + 1] : 0 }); }, }, { // 阅读情况:按年级分组,叶子为班级(读/未读) url: "/api/notice/ReadDetail", method: "get", response: ({ query }: any) => { const n = notices.find((x) => x.id === Number(query.serid)); if (!n) return fail("通知不存在或已被删除"); const targets = targetClassIds(n); const readSet = new Set(n.classRead || []); const byGrade: Record = {}; targets.forEach((id) => { const info = classMap[id]; if (!info) return; (byGrade[info.gradeName] = byGrade[info.gradeName] || []).push({ account: id, teaname: info.className + "(" + info.studentCount + "人)", states: readSet.has(id) ? "read" : "unread", }); }); return ok(Object.keys(byGrade).map((name) => ({ GroupName: name, teaslist: byGrade[name] }))); }, }, /* ---------- 通知写入操作 ---------- */ { url: "/api/notice/Create", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const dept = deptDict.find((d) => d.CodeID === b.deptId); // 选人即送审:解析发布入参里的审批流(flow 多步 / approvers 单步),有审批人则进入待审批 let flowInput: any = b; if (typeof b.flow === "string") { try { flowInput = { ...flowInput, flow: JSON.parse(b.flow) }; } catch { /* ignore */ } } if (typeof b.approvers === "string") { try { flowInput = { ...flowInput, approvers: JSON.parse(b.approvers) }; } catch { /* ignore */ } } const flow = buildFlowFromInput(flowInput); const approvalEnabled = flow ? 1 : 0; const receive = b.receiveList || "all"; const isAll = /^all$/i.test(String(receive).trim()); const item: any = { id: noticeSeq++, title: b.title || "未命名通知", deptId: b.deptId || "", deptName: dept ? dept.Name : "", publisherAccount: ME.account, publisherName: ME.name, content: b.content || "", receiveList: isAll ? "ALL" : String(receive).split(/[,,\s]+/).filter(Boolean).join(","), classRead: [], files: b.fileIds || null, topmost: 0, isPulled: 0, smsFlag: Number(b.smsFlag) || 0, timingSendTime: b.timingSendTime ? String(b.timingSendTime).replace(/\//g, "-") : null, createTime: nowStr(), approvalEnabled, // 未选审批人 → 直接发布(none);选了 → 待审批(pending) approvalStatus: approvalEnabled ? "pending" : "none", approvalRemark: null, approvalBy: null, approvalTime: null, flow, approvalLevel: 0, }; notices.unshift(item); return ok(item.id); }, }, { url: "/api/notice/Edit", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const n = notices.find((x) => x.id === Number(b.sourceId ?? b.id)); if (!n) return fail("通知不存在或已被删除"); const dept = deptDict.find((d) => d.CodeID === b.deptId); n.title = b.title || n.title; n.content = b.content ?? n.content; if (b.deptId) { n.deptId = b.deptId; n.deptName = dept ? dept.Name : n.deptName; } if (b.receiveList !== undefined && b.receiveList !== null && b.receiveList !== "") { const isAll = /^all$/i.test(String(b.receiveList).trim()); n.receiveList = isAll ? "ALL" : String(b.receiveList).split(/[,,\s]+/).filter(Boolean).join(","); } if (b.fileIds !== undefined) n.files = b.fileIds || null; if (b.smsFlag !== undefined) n.smsFlag = Number(b.smsFlag) || 0; if (b.timingSendTime !== undefined) n.timingSendTime = b.timingSendTime ? String(b.timingSendTime).replace(/\//g, "-") : null; // 编辑可重新配置审批流:传了 flow/approvers 就按“选人即送审”重建 let flowInput: any = b; if (typeof b.flow === "string") { try { flowInput = { ...flowInput, flow: JSON.parse(b.flow) }; } catch { /* ignore */ } } if (typeof b.approvers === "string") { try { flowInput = { ...flowInput, approvers: JSON.parse(b.approvers) }; } catch { /* ignore */ } } const flowChanged = b.flow !== undefined || b.approvers !== undefined; if (flowChanged) { const nf = buildFlowFromInput(flowInput); n.flow = nf; n.approvalEnabled = nf ? 1 : 0; n.approvalLevel = 0; n.approvalStatus = nf ? "pending" : "none"; n.approvalRemark = null; n.approvalBy = null; n.approvalTime = null; } else if (n.approvalEnabled && n.approvalStatus === "rejected") { // 未改动审批流,仅内容修改后重新送审:驳回→待审批(保留原流程,回到第 0 步) n.approvalStatus = "pending"; n.approvalLevel = 0; (n.flow || []).forEach((lv: any) => (lv.records = [])); n.approvalRemark = null; n.approvalBy = null; n.approvalTime = null; } return ok(null); }, }, { url: "/api/notice/Delete", method: "post", response: ({ body, query }: any) => { const parsed = bodyOf(body, query); const ids: number[] = Array.isArray(parsed) ? parsed.map(Number) : [Number(parsed.ids ?? parsed.id)].filter(Boolean); for (const id of ids) { const i = notices.findIndex((n) => n.id === id); if (i > -1) notices.splice(i, 1); } return ok(null); }, }, { url: "/api/notice/InitializeRed", method: "post", response: () => { notices.forEach((n) => { if (isPublished(n) && !n.classRead.includes(ME_CLASS)) n.classRead.push(ME_CLASS); }); return ok({ count: notices.length }); }, }, { url: "/api/notice/UserPull", method: "post", response: ({ body, query }: any) => { const n = notices.find((x) => x.id === Number(bodyOf(body, query).serid)); if (n) n.isPulled = 1; return ok(null); }, }, { url: "/api/notice/SetTopmost", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const n = notices.find((x) => x.id === Number(b.serid)); if (n) n.topmost = Number(b.topmost ?? 1) ? 1 : 0; return ok(null); }, }, { url: "/api/notice/Expedite", method: "post", response: () => ok(null) }, { url: "/api/notice/ReceiveUsers", method: "post", response: () => ok(null) }, { url: "/api/notice/Statis", method: "get", response: () => { const byPublisher: Record = {}; notices.forEach((n) => (byPublisher[n.publisherName] = (byPublisher[n.publisherName] || 0) + 1)); return ok({ total: notices.length, publishedCount: notices.filter((n) => isPublished(n) && !n.isPulled).length, totalReadRecords: notices.reduce((s, n) => s + readCountOf(n), 0), unreadForCurrentUser: notices.filter((n) => isPublished(n) && !isReadByMe(n)).length, byPublisher: Object.keys(byPublisher).map((publisherName) => ({ publisherName, count: byPublisher[publisherName] })), byMonth: [{ year: 2026, month: 9, count: notices.length }], }); }, }, /* ---------- 审批(多级:会签/或签) ---------- */ { url: "/api/notice/ApprovalList", method: "get", response: ({ query }: any) => { const pageIndex = Number(query.pageIndex || 1); const pageSize = Number(query.pageSize || 10); // v7:后端新参数名 auditState 优先,旧名 status 别名兼容 const status = query.auditState || query.status || "pending"; let list = notices.filter((n) => n.approvalEnabled === 1); // v13:口径对齐真实后端——pending 只列当前步待签人含我的单,all=已审合一(approved+rejected) if (status === "pending") list = list.filter((n) => canAct(n)); else if (status && status !== "all") list = list.filter((n) => n.approvalStatus === status); else if (status === "all") list = list.filter((n) => n.approvalStatus === "approved" || n.approvalStatus === "rejected"); if (query.scope === "user") list = list.filter((n) => n.publisherAccount === ME.account); list = applyFilters(list, query); list = sortByTimeDesc(list); return ok(paginate(list.map(toVo), pageIndex, pageSize)); }, }, { url: "/api/notice/ApprovalCount", method: "get", // 待我审批:当前步待签人包含我的单子数 response: () => ok(notices.filter((n) => n.approvalEnabled === 1 && canAct(n)).length), }, { // v13:审批中心四 tab 统计(与各 tab 列表默认口径一致,不含 keyword) url: "/api/notice/ApprovalTabCount", method: "get", response: () => { const enabled = notices.filter((n) => n.approvalEnabled === 1); return ok({ pending: enabled.filter((n) => canAct(n)).length, approved: enabled.filter((n) => n.approvalStatus === "approved").length, rejected: enabled.filter((n) => n.approvalStatus === "rejected").length, all: enabled.filter((n) => n.approvalStatus === "approved" || n.approvalStatus === "rejected").length, }); }, }, { // v7 补:审批人记忆(演示取启用池前两名 teaId) url: "/api/notice/ApproverSuggest", method: "get", response: () => ok({ teaIds: approvers.filter((a) => a.enabled === 1 && a.teaId).slice(0, 2).map((a) => a.teaId) }), }, { // v7 补:teaId → 姓名解析(先审批人池、再教师树,未命中降级 ID 串) url: "/api/notice/ApproverResolve", method: "get", response: ({ query }: any) => { const ids = String(query.ids || "").split(",").map(Number).filter(Boolean); const teaName = (id: number) => schoolTree.flatMap((d: any) => d.children || []).find((t: any) => t.id === id)?.name || null; return ok( ids.map((id) => ({ id, name: approvers.find((a) => a.teaId === id)?.name || teaName(id) || String(id), photo: "", })) ); }, }, { url: "/api/notice/Approve", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const n: any = notices.find((x) => x.id === Number(b.serid)); if (!n) return fail("通知不存在"); if (n.approvalStatus !== "pending") return fail("该通知已处理,无需重复审批"); if (!n.flow) return fail("该通知未配置审批流程"); const actor = b.account || ME.account; const lv = n.flow[n.approvalLevel || 0]; if (!lv) { n.approvalStatus = "approved"; return ok({ status: "approved" }); } if (!lv.approvers.includes(actor)) return fail("您不是当前步骤的审批人,无权处理"); if (lv.records.some((r: any) => r.account === actor)) return fail("您已处理过本步骤,请等待其他审批人"); lv.records.push({ account: actor, name: approverName(actor), result: "approved", time: nowStr(), remark: b.remark || "同意" }); const remaining = pendingApprovers(n); const stepPass = lv.vote === "any" ? true : remaining.length === 0; // 或签=任一通过;会签=全部通过 if (stepPass) { n.approvalLevel = (n.approvalLevel || 0) + 1; if (n.approvalLevel >= n.flow.length) { n.approvalStatus = "approved"; n.approvalBy = approverName(actor); n.approvalTime = nowStr(); n.approvalRemark = b.remark || "同意发布"; } } return ok({ status: n.approvalStatus, level: n.approvalLevel, nextPending: n.approvalStatus === "pending" ? pendingApprovers(n) : [] }); }, }, { url: "/api/notice/Reject", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const n: any = notices.find((x) => x.id === Number(b.serid)); if (!n) return fail("通知不存在"); if (n.approvalStatus !== "pending") return fail("该通知已处理,无需重复审批"); const actor = b.account || ME.account; const lv = (n.flow || [])[n.approvalLevel || 0]; if (lv && !lv.approvers.includes(actor)) return fail("您不是当前步骤的审批人,无权处理"); if (lv) lv.records.push({ account: actor, name: approverName(actor), result: "rejected", time: nowStr(), remark: b.remark || "不同意" }); n.approvalStatus = "rejected"; n.approvalBy = approverName(actor); n.approvalTime = nowStr(); n.approvalRemark = b.remark || "已驳回"; return ok(null); }, }, /* ---------- 推送范围 / 草稿 / 提交审批 / 阅读回执(校园通知改造新增契约) ---------- */ { // 推送范围树:全校 + 初中(学段) → 年级 → 班级(scopeType 1/2/3/4) url: "/api/notice/ScopeTree", method: "get", response: () => ok([ { scopeType: 1, scopeId: 1, name: "示范初级中学", children: [ { scopeType: 2, scopeId: 11, name: "初中部", children: gradeClasses.map((g) => ({ scopeType: 3, scopeId: g.gradeId, name: g.gradeName, children: g.classes.map((c) => ({ scopeType: 4, scopeId: c.classId, name: c.className, children: [] })), })), }, ], }, ]), }, { // 存为草稿:不圈人,仅内容 + 范围快照 + 附件 url: "/api/notice/SaveDraft", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const dept = deptDict.find((d) => d.CodeID === b.deptId); const id = noticeSeq++; notices.unshift({ id, title: b.title || "未命名草稿", deptId: b.deptId || "", deptName: dept ? dept.Name : "", publisherAccount: ME.account, publisherName: ME.name, content: b.content || "", receiveList: "ALL", classRead: [], files: b.fileIds || null, topmost: 0, isPulled: 0, smsFlag: 0, timingSendTime: b.timingSendTime ? String(b.timingSendTime).replace(/\//g, "-") : null, createTime: nowStr(), approvalEnabled: 0, approvalStatus: "none", approvalRemark: null, approvalBy: null, approvalTime: null, draft: 1, pushScopes: b.pushScopes || [], receiptFlag: Number(b.receiptFlag) || 0, }); return ok(id); }, }, { // 草稿箱:status=0 本人草稿 url: "/api/notice/DraftList", method: "get", response: ({ query }: any) => { const pageIndex = Number(query.pageIndex || 1); const pageSize = Number(query.pageSize || 10); const list = sortByTimeDesc(notices.filter((n: any) => n.draft === 1 && n.publisherAccount === ME.account)); const kwd = query.keyword || ""; const filtered = kwd ? list.filter((n: any) => n.title.includes(kwd)) : list; return ok(paginate(filtered.map(toVo), pageIndex, pageSize)); }, }, { // 提交审批:草稿/已驳回 → 待审批(steps 优先,缺省复用已存流程) url: "/api/notice/Submit", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const n: any = notices.find((x) => x.id === Number(b.serid)); if (!n) return fail("通知不存在"); const steps = Array.isArray(b.steps) && b.steps.length ? b.steps : n.flow ? n.flow : null; if (!steps) return fail("请选择审批人"); n.flow = steps.map((s: any, i: number) => ({ name: s.name || `第${i + 1}步`, vote: s.vote === "all" ? "all" : "any", approvers: [...(s.approvers || [])], records: [], })); n.approvalEnabled = 1; n.approvalStatus = "pending"; n.approvalLevel = 0; n.draft = 0; return ok(null); }, }, { // 阅读回执:按 年级→班级→学生 分组(mock 按班级整班已读/未读简化) url: "/api/notice/ReadReceipt", method: "get", response: ({ query }: any) => { const n: any = notices.find((x) => x.id === Number(query.serid)); if (!n) return fail("通知不存在"); const readClasses = new Set((n.classRead || []).map(String)); const target = isAllTarget(n) ? ALL_CLASSES : targetClassIds(n); const grades = gradeClasses .map((g) => ({ gradeId: g.gradeId, gradeName: g.gradeName, classes: g.classes .filter((c) => target.includes(String(c.classId))) .map((c) => ({ classId: c.classId, className: c.className, students: Array.from({ length: Math.min(c.studentCount, 6) }, (_, i) => { const read = readClasses.has(String(c.classId)); return { account: `stu${c.classId}_${i + 1}`, name: `学生${i + 1}`, read, readTime: read ? (n.createTime || nowStr()) : null, }; }), })), })) .filter((g: any) => g.classes.length); const readCount = grades.reduce( (sum: number, g: any) => sum + g.classes.reduce((s: number, c: any) => s + c.students.filter((x: any) => x.read).length, 0), 0 ); const allowCount = grades.reduce( (sum: number, g: any) => sum + g.classes.reduce((s: number, c: any) => s + c.students.length, 0), 0 ); return ok({ serid: n.id, allowCount, readCount, grades, teachers: [] }); }, }, /* ---------- 附件上传 ---------- */ { url: "/api/base/FileUpload", method: "post", response: () => { const id = nextFileId++; filesStore[id] = { id, filename: `校园通知附件${id}.png`, fileTrueName: `校园通知附件${id}.png`, size: 12288 }; return ok({ fileid: id, name: filesStore[id].filename, host: "", fileurl: `/api/file/Preview?id=${id}`, type: "png", size: 12288 }); }, }, { url: "/api/file/UploadImage", method: "post", response: () => { const id = nextFileId++; filesStore[id] = { id, filename: `mock图片${id}.svg`, fileTrueName: `mock图片${id}.png`, size: 2048 }; return ok([{ id, fileid: id, fileTrueName: filesStore[id].fileTrueName, fileName: filesStore[id].filename, fileSize: 2048, fileType: "png" }]); }, }, { url: "/api/file/Preview", method: "get", response: () => ok({ id: 0, fileurl: mockImageDataURL() }) }, /* ---------- 联系组(应用设置:自定义组) ---------- */ { url: "/api/contacts/GetGroup", method: "get", response: ({ query }: any) => { const pageIndex = Number(query.page || 1); const pageSize = Number(query.count || 10); const mapped = groups.map((g) => ({ serid: g.serid, GroupName: g.GroupName, count: g.members.length })); const start = (pageIndex - 1) * pageSize; return ok({ list: mapped.slice(start, start + pageSize), total: mapped.length }); }, }, { url: "/api/contacts/AddGroup", method: "post", response: ({ body, query }: any) => { const name = bodyOf(body, query).name || "未命名组"; groups.push({ serid: groupSeq++, GroupName: name, members: [] }); return ok(null); }, }, { url: "/api/contacts/EditGroup", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const g = groups.find((x) => x.serid === Number(b.id)); if (g) g.GroupName = b.name || g.GroupName; return ok(null); }, }, { url: "/api/contacts/DelGroup", method: "post", response: ({ body, query }: any) => { const parsed = bodyOf(body, query); const ids = String(parsed.ids ?? "").split(",").map(Number).filter(Boolean); for (const id of ids) { const i = groups.findIndex((g) => g.serid === id); if (i > -1) groups.splice(i, 1); } return ok(null); }, }, { url: "/api/contacts/GetContactsTea", method: "get", response: ({ query }: any) => { const g = groups.find((x) => x.serid === Number(query.gid)); const mapped = (g ? g.members : []).map((id) => ({ account: "user" + id, userId: id, id, name: idNames[id] || String(id) })); const pageIndex = Number(query.page || 0); const pageSize = Number(query.count || 0); if (!pageIndex || !pageSize) return ok({ list: mapped, total: mapped.length }); const start = (pageIndex - 1) * pageSize; return ok({ list: mapped.slice(start, start + pageSize), total: mapped.length }); }, }, { url: "/api/contacts/SetContactsTea", method: "post", response: ({ body, query }: any) => { const b = bodyOf(body, query); const g = groups.find((x) => x.serid === Number(b.gid)); if (g) g.members = String(b.ids || "").split(",").map(Number).filter(Boolean); return ok(null); }, }, { url: "/api/contacts/GetGroupTree", method: "get", response: () => ok( groups.map((g) => ({ id: 1000 + g.serid, name: g.GroupName, isParent: true, children: g.members.map((id) => ({ id, name: idNames[id] || String(id), isParent: false })), })) ), }, ]; /** 内联 SVG data URL,作为 mock 预览图片 */ function mockImageDataURL() { const svg = "校园通知 Mock 图片"; return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); }