build / build (push) Successful in 13m15s
- 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 记录。
1410 lines
61 KiB
TypeScript
1410 lines
61 KiB
TypeScript
/**
|
||
* 校园通知本地 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)));
|
||
}
|