Files
CampusnoticeFront/src/assets/mock/noticeapi.ts
T
lijing a48c5f6343
build / build (push) Successful in 13m15s
fix: 修复日期筛选与表单参数双写、下线范围预估,并改造列表三列样式
- v29 删除前端对 notice/ScopePreview(推送范围「已选学员数」预估)的请求:
  api 方法、ScopePreviewVo 类型、dev:mock 桩、ClassScopeInput/PushScopePanel 两个组件的预估 UI 一并移除。
- v30 修复发文部门/联系组名称重复且带逗号(如「校园部,校园部」并逐次翻倍):
  postForm 原先把参数同时拼进 URL query 与 urlencoded body,Servlet 会合并两处取值、
  Spring @RequestParam String 再以「,」拼接。改为只发 body,仅在 dev:mock 下附带 query
  (vite-plugin-mock 不解析 urlencoded body),不影响线上。
- 修复时间筛选失效:DatePicker 的 Date 实例被 alova 直接 encodeURIComponent 成浏览器本地化
  日期串(Fri Aug 21 2026 ... GMT+0800),后端按 yyyy-MM-dd 解析不了。列表侧提交前归一化为
  短日期,alova-core 另加一层 Date → 本地 'yyyy-MM-dd HH:mm:ss' 的兜底归一化。
- 列表「类型 / 状态 / 阅读情况」三列改醒目样式(口径不变,仅样式):
  类型改胶囊(重要=实心红、普通=浅灰弱化);状态改彩色圆点胶囊,已发布绿/定时中蓝/
  待审批橙/已驳回红/草稿灰,已撤回、置顶改实心小徽标;阅读情况改渐变进度条 + 已读/应读·百分比,
  读满转绿。同步微调类型(80→90)、阅读情况(110→180)列宽。
- 同步更新《执行计划任务方案.md》v29 / v30 记录。
2026-09-22 09:46:33 +08:00

1410 lines
61 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 校园通知本地 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<string, { className: string; gradeName: string; studentCount: number }> = {};
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:
"<p>各班级:</p><p>学校定于第 10 周开展秋季学期期中教学检查,请各班配合任课教师准备相关材料。</p><p>具体安排另行通知。</p>",
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: "<p>定于本周五下午 15:00 在学术报告厅开展网络安全与个人信息保护讲座,各班按课表有序参加。</p>",
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: "<p>初一年级定于下周三 19:00 召开家长会,请初一(1)(2)班班主任通知到每位家长。</p>",
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: "<p>放假前各科老师请布置假期作业,各班完成假期安全提醒。</p>",
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: "<p>第十二届校园科技节报名启动,详见附件方案,各班积极组织学生参加。</p>",
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: "<p>请各班主任配合体育组完成本学期学生体质健康测试,测试地点为田径场,时间以班级通知为准。</p>",
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: "<p>秋季田径运动会定于 10 月下旬举行,请各班于本周五前完成报名并提交至体育组。</p>",
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: "<p>拟定于 9 月 20 日举办校园开放日,向家长开放。</p>",
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<number, { id: number; filename: string; fileTrueName: string; size: number }> = {
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<number, string> = {
1: "朱皓东", 2: "刘文娟", 3: "陈垠", 4: "王海燕", 5: "李强",
6: "何明明", 7: "周敏", 8: "孙丽娟", 9: "吴建国", 10: "赵晓琳", 11: "黄瑶",
};
const teacherNames: Record<string, string> = {
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<string, any> = {};
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<string, string> = { 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<string, any[]> = {};
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<string, number> = {};
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 =
"<svg xmlns='http://www.w3.org/2000/svg' width='240' height='120'><rect width='100%' height='100%' fill='#e8f1ff'/><text x='50%' y='50%' font-size='16' fill='#5c9edc' text-anchor='middle' dominant-baseline='middle'>校园通知 Mock 图片</text></svg>";
return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg)));
}