Files
coursehomework-front/.backup/20260915-drop-approval/src/assets/mock/homeworkapi.ts
T
wanghu e6639f7925 feat(M5): P0 契约层 + 审批/应用设置下线
- types/homeworkV2.ts:V2 全量类型契约(五态/枚举/矩阵/版本/通知)
- teacherHomework/studentHomework/notification 三客户端(28 端点)
- §6 审批功能下线:删 ApprovalList/ApprovalSetting/ApprovalFlowEditor,
  types/api/menu/menuRoute/App/mock 同步清理,DROPPED_MOD_IDS={5,6}
- §7 应用设置下线:删 HomeworkSetting/TypeSetBox/UserCheckInput,
  SettingApi 与 dict/contacts 死代码清理,DROPPED_MOD_IDS 扩 {21}
- 新增 V2 组件:TeacherHomeworks(已发布+草稿箱)、ReviewModal(批阅弹窗)、
  AttachmentUpload(附件上传)——尚未挂路由,页面切换随后续阶段
2026-09-16 02:29:32 +08:00

1473 lines
67 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/homework.ts(P0 标准化):
* - /api/homework/{List,SendList,Detail,GetDetail,GetNextPrev,ReadDetail,Create,Edit,Delete,
* InitializeRed,UserPull,SetTopmost,Expedite,ReceiveUsers,Statis}
* - /api/homework/{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";
/** 审批人候选池(发布时从这里选审批人;维护见「审批人」设置) */
let approverSeq = 7;
const approvers = [
{ id: 1, account: "zhd", name: "朱皓东", deptId: "D002", deptName: "数学教研组", enabled: 1 },
{ id: 2, account: "lwj", name: "刘文娟", deptId: "D001", deptName: "语文教研组", enabled: 1 },
{ id: 3, account: "why", name: "王海燕", deptId: "D005", deptName: "政史地教研组", enabled: 1 },
{ id: 4, account: "hy", name: "黄瑶", deptId: "D004", deptName: "理化生教研组", enabled: 1 },
{ id: 5, account: "cy", name: "陈垠", deptId: "D003", deptName: "英语教研组", enabled: 1 },
{ id: 6, account: "zm", name: "周敏", deptId: "D008", deptName: "教务处(作业管理)", enabled: 1 },
];
const approverName = (account: string) => approvers.find((a) => a.account === account)?.name || account;
const approverPoolAccounts = () => approvers.filter((a) => a.enabled === 1).map((a) => a.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})`;
}
/** 年级 → 班级树(叶子=班级,带学生数) */
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 homeworks = [
{
id: 1,
title: "初一·初二年级第 9 周各科作业汇总(教学检查配套)",
deptId: "D008",
deptName: "教务处(作业管理)",
publisherAccount: "zm",
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: "初二语文《这样的人让我敬佩》写人记叙文(600 字)",
deptId: "D001",
deptName: "语文教研组",
publisherAccount: "lwj",
publisherName: "刘文娟",
content: "<p>请同学们完成一篇写人记叙文,题目《这样的人让我敬佩》。</p><p>要求:不少于 600 字,选取身边真实人物,通过具体事例表现其品质;周日前拍照上传至班级作业本,纸质稿交语文课代表。</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: "初一数学·一元一次方程组应用题分层练(第 3 课时)",
deptId: "D002",
deptName: "数学教研组",
publisherAccount: "zhd",
publisherName: "朱皓东",
content: "<p>课后分 A、B 两层作业:A 组(基础)教材 P88 第 1–5 题;B 组(提高)配套练习第 6–9 题。</p><p>请按所在层次完成并写在作业本上,明天数学课随堂订正。</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: "D002",
deptName: "数学教研组",
publisherAccount: "zhd",
publisherName: "朱皓东",
content: "<p>假期数学作业分两部分:①计算巩固——口算题卡 3 页;②实践探究——记录假期家庭一周开支并制成统计表。</p><p>已发布给全部班级,请于截止前提交,收假后第一节课查验。</p>",
receiveList: "ALL",
classRead: [],
files: null,
topmost: 0,
isPulled: 0,
smsFlag: 0,
timingSendTime: null,
createTime: "2026-09-05 16:40:00",
approvalEnabled: 0,
approvalStatus: "none",
approvalRemark: null,
approvalBy: null,
approvalTime: null,
},
{
id: 5,
title: "初三英语·阅读理解与完形填空专项训练(附答题卡)",
deptId: "D003",
deptName: "英语教研组",
publisherAccount: "cy",
publisherName: "陈垠",
content: "<p>完成附件《阅读专项训练》Section A–B(6 篇阅读、2 篇完形),答案填写在答题卡上。</p><p>限时 40 分钟独立完成,周五前拍照上传答题卡。</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: "D005",
deptName: "政史地教研组",
publisherAccount: "why",
publisherName: "王海燕",
content: "<p>以《我家的十年变化》为主题采写一篇时事观察,制作一份 A3 手抄报。</p><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: "初二物理·凸透镜成像规律实验报告(含数据记录表)",
deptId: "D004",
deptName: "理化生教研组",
publisherAccount: "hy",
publisherName: "黄瑶",
content: "<p>按教材步骤完成凸透镜成像实验并撰写实验报告,附物距—像距对照表。</p><p>提交:数据记录表、成像规律结论、误差分析各一份,拍照上传或纸质交物理课代表。</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>利用平移、旋转与轴对称设计一幅图案,并标注所使用的变换方式。</p><p>(草拟中,暂未定稿)</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 homeworkSeq = 9;
/** 给需要审批的种子作业构建多步流程(演示会签/或签与不同进度) */
function seedFlows() {
const twoSteps = () => buildFlowFromInput({ flow: [{ name: "教研组初审", vote: "all", approvers: ["zhd", "lwj"] }, { name: "教务处复核", vote: "any", approvers: ["cy", "zm", "why"] }] })!;
homeworks.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: "lwj", name: approverName("lwj"), result: "rejected", time: n.approvalTime || "2026-09-09 09:00:00", remark: n.approvalRemark || "已驳回" });
}
});
// 实验报告(id7):教研组初审会签已过 1/2(朱皓东已签),等待刘文娟
const n7 = homeworks.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();
/* 移除审批:统一将种子作业复位为“直接发布” */
homeworks.forEach((n: any) => {
n.approvalEnabled = 0;
n.approvalStatus = "none";
n.approvalRemark = null;
n.approvalBy = null;
n.approvalTime = null;
n.flow = null;
n.approvalLevel = 0;
});
/* ===================== 课程作业扩展模型(v1 核心闭环:布置→提交→批改→统计) ===================== */
/** 每份作业的“作业化”元数据(学科 / 截止 / 提交方式 / 流程状态),叠加到 homeworks 上 */
const hwMeta: Record<number, { subject: string; deadline: string; submitType: string[]; hwStatus: string }> = {
1: { subject: "综合", deadline: "2026-09-12 18:00:00", submitType: ["none"], hwStatus: "collecting" },
2: { subject: "语文", deadline: "2026-09-07 18:00:00", submitType: ["text", "image"], hwStatus: "done" },
3: { subject: "数学", deadline: "2026-09-15 18:00:00", submitType: ["text"], hwStatus: "collecting" },
4: { subject: "数学", deadline: "2026-10-08 08:00:00", submitType: ["text", "image"], hwStatus: "collecting" },
5: { subject: "英语", deadline: "2026-09-12 18:00:00", submitType: ["image", "attach"], hwStatus: "closed" },
6: { subject: "道德与法治", deadline: "2026-09-15 18:00:00", submitType: ["image"], hwStatus: "closed" },
7: { subject: "物理", deadline: "2026-09-16 18:00:00", submitType: ["text", "image"], hwStatus: "collecting" },
8: { subject: "数学", deadline: "2026-09-18 18:00:00", submitType: ["image", "attach"], hwStatus: "draft" },
};
homeworks.forEach((n: any) => Object.assign(n, hwMeta[n.id] || { subject: "综合", deadline: null, submitType: ["text"], hwStatus: "collecting" }));
/** 班级花名册:为每个班级生成稳定学生个体(id、姓名),用于提交/批改名单 */
const SURNAMES = ["王", "李", "张", "刘", "陈", "杨", "赵", "黄", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "何", "林", "高", "罗", "郑", "梁"];
const GIVEN = ["伟", "芳", "娜", "敏", "静", "磊", "军", "洋", "勇", "艳", "杰", "涛", "明", "超", "霞", "平", "子轩", "梓涵", "浩然", "欣怡", "雨桐", "一鸣", "思远", "梦琪"];
const roster: Record<string, { studentId: number; studentName: string }[]> = {};
ALL_CLASSES.forEach((cid, ci) => {
const count = classMap[cid].studentCount;
const arr: { studentId: number; studentName: string }[] = [];
for (let i = 0; i < count; i++) {
arr.push({ studentId: Number(cid) * 100 + i + 1, studentName: SURNAMES[(ci * 7 + i) % SURNAMES.length] + GIVEN[(ci * 13 + i * 5) % GIVEN.length] });
}
roster[cid] = arr;
});
/** 确定性伪随机(同种子稳定,刷新不跳变) */
function seededRand(seed: number) {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
const GRADE_LEVELS = ["A", "B", "C"];
/**
* 提交记录(学生 × 作业)。按作业流程状态生成不同分布:
* draft(草稿)不生成;submitType 含 none(仅查阅)不收集提交。
* 状态:unsubmitted 未交 / submitted 已交待批 / graded 已批改 / needRedo 待订正 / redone 已订正。
*/
const submissions: any[] = [];
let subSeq = 1;
homeworks.forEach((n: any) => {
if (n.hwStatus === "draft") return;
const viewOnly = (n.submitType || []).includes("none");
targetClassIds(n).forEach((cid) => {
(roster[cid] || []).forEach((stu) => {
const r = seededRand(n.id * 1000 + stu.studentId);
const submitted = !viewOnly && r < (n.hwStatus === "collecting" ? 0.62 : 0.9);
// 已读:交了的必然读过;未交的按更高概率“读了没交”。仅查阅只统计已读。
const read = viewOnly ? r < 0.85 : submitted || r < 0.9;
let submitStatus = "unsubmitted";
let submittedAt: string | null = null;
let grade: any = null;
if (submitted) {
const day = Math.min(1 + Math.floor(r * 7), 28);
submittedAt = `${String(n.createTime).slice(0, 8)}${String(day).padStart(2, "0")} ${8 + Math.floor(r * 12)}:${String(Math.floor(r * 60)).padStart(2, "0")}:00`;
if (n.hwStatus === "done") submitStatus = r < 0.85 ? "graded" : r < 0.93 ? "redone" : "needRedo";
else if (n.hwStatus === "closed") submitStatus = r < 0.5 ? "graded" : r < 0.82 ? "submitted" : "needRedo";
else submitStatus = r < 0.35 ? "graded" : "submitted";
if (submitStatus === "graded" || submitStatus === "redone" || submitStatus === "needRedo") {
const sr = seededRand(stu.studentId * 3 + n.id);
grade = {
score: 60 + Math.floor(sr * 41),
level: GRADE_LEVELS[Math.floor(sr * 3)],
comment: sr > 0.8 ? "完成很好,注意书写规范" : sr > 0.5 ? "思路正确,个别步骤需订正" : "第 3、5 题有误,订正后重交",
isExcellent: sr > 0.93,
markedImages: [],
};
}
}
submissions.push({
id: subSeq++,
homeworkId: n.id,
studentId: stu.studentId,
studentName: stu.studentName,
classId: cid,
className: classMap[cid]?.className || cid,
read,
submittedAt,
submitStatus,
text: submitted ? "(学生作答内容 · mock)" : "",
images: submitted && (n.submitType || []).includes("image") ? [mockImageDataURL(stu.studentId)] : [],
attachments: submitted && (n.submitType || []).includes("attach") ? [] : [],
grade,
});
});
});
});
const GRADED_SET = ["graded", "redone", "needRedo"];
function subsOf(homeworkId: number) {
return submissions.filter((s) => s.homeworkId === homeworkId);
}
/** 提交/批改派生统计(应交/已交/已批/待批/待订正/已订正/未交 + 提交率/批改率) */
function progressOf(homeworkId: number) {
const list = subsOf(homeworkId);
const should = list.length;
const submitted = list.filter((s) => s.submitStatus !== "unsubmitted").length;
const graded = list.filter((s) => GRADED_SET.includes(s.submitStatus)).length;
const needRedo = list.filter((s) => s.submitStatus === "needRedo").length;
const redone = list.filter((s) => s.submitStatus === "redone").length;
const read = list.filter((s) => s.read).length;
return {
should,
submitted,
graded,
pendingGrade: submitted - graded,
needRedo,
redone,
unsubmitted: should - submitted,
read,
unread: should - read,
submitRate: should ? Number(((submitted / should) * 100).toFixed(1)) : 0,
gradeRate: submitted ? Number(((graded / submitted) * 100).toFixed(1)) : 0,
};
}
/* ============ 多角色视角(v2 学生端;家长即登录学生账号,端分离:SetRole 入口切换) ============ */
type Role = "teacher" | "student";
let viewerRole: Role = "teacher";
/** 演示学生:取 ME_CLASS(初一(1)班)花名册第一名 */
const DEMO_STUDENT = (() => {
const arr = roster[ME_CLASS] || [];
const s = arr[0] || { studentId: 130101, studentName: "演示学生" };
return { studentId: s.studentId, studentName: s.studentName, classId: ME_CLASS, className: classMap[ME_CLASS]?.className || "初一(1)班" };
})();
function setViewerRole(r: any): Role {
viewerRole = r === "student" ? "student" : "teacher";
return viewerRole;
}
/** 当前学生视角可见作业(已发布、面向本班、非未来定时、非草稿),附本人提交/批改态 */
function viewerHomeworks() {
const cid = String(DEMO_STUDENT.classId);
return homeworks
.filter((n: any) => isPublished(n) && !n.isPulled && n.hwStatus !== "draft" && !isScheduledFuture(n) && (isAllTarget(n) || targetClassIds(n).includes(cid)))
.sort((a: any, b: any) => ((a.deadline || "9") < (b.deadline || "9") ? -1 : 1))
.map((n: any) => {
const mine = submissions.find((s) => s.homeworkId === n.id && s.studentId === DEMO_STUDENT.studentId) || null;
return {
id: n.id,
title: n.title,
subject: n.subject || "",
publisherName: n.publisherName,
deptName: n.deptName,
deadline: n.deadline || null,
createTime: n.createTime,
content: n.content,
submitType: n.submitType || ["text"],
viewOnly: (n.submitType || []).includes("none"),
hwStatus: n.hwStatus || "collecting",
allowLate: !!n.allowLate,
my: mine
? {
subId: mine.id,
submitStatus: mine.submitStatus,
submittedAt: mine.submittedAt,
text: mine.text,
images: mine.images || [],
attachments: mine.attachments || [],
score: mine.grade ? mine.grade.score : null,
level: mine.grade ? mine.grade.level : null,
comment: mine.grade ? mine.grade.comment : "",
isExcellent: !!(mine.grade && mine.grade.isExcellent),
}
: null,
};
});
}
/** 学生视角分组计数:todo 待完成 / submitted 已交待批 / graded 已批改 / redo 待订正 */
function viewerBuckets(list: any[]) {
const b = { todo: 0, submitted: 0, graded: 0, redo: 0, all: list.length };
list.forEach((x) => {
if (x.viewOnly) return;
const st = x.my ? x.my.submitStatus : "unsubmitted";
if (st === "unsubmitted") b.todo++;
else if (st === "submitted") b.submitted++;
else if (st === "needRedo") b.redo++;
else b.graded++;
});
return b;
}
/** 附件存储 */
const filesStore: Record<number, { id: number; filename: string; fileTrueName: string; size: number }> = {
9001: { id: 9001, filename: "第 9 周各科作业汇总表.xlsx", fileTrueName: "第 9 周各科作业汇总表.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: "已驳回" };
const HW_STATUS_DESC: Record<string, string> = { draft: "草稿", collecting: "收集中", closed: "已截止", done: "已完成" };
function toVo(n: any) {
return {
id: n.id,
title: n.title,
content: n.content,
type: 0,
typeDesc: "课程作业",
subject: n.subject || "",
deadline: n.deadline || null,
submitType: n.submitType || ["text"],
hwStatus: n.hwStatus || "collecting",
hwStatusDesc: HW_STATUS_DESC[n.hwStatus] || "收集中",
progress: n.hwStatus === "draft" ? null : progressOf(n.id),
receiveText: receiveText(n),
classCount: isAllTarget(n) ? ALL_CLASSES.length : targetClassIds(n).length,
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),
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),
subject: n.subject || "",
deadline: n.deadline || null,
submitType: n.submitType || ["text"],
hwStatus: n.hwStatus || "collecting",
hwStatusDesc: HW_STATUS_DESC[n.hwStatus] || "收集中",
progress: n.hwStatus === "draft" ? null : progressOf(n.id),
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),
// 审批流进度(供详情时间线 & 发布/编辑回显)
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) : [],
};
}),
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),
};
}
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);
if (query.subject) out = out.filter((n) => (n.subject || "") === query.subject);
if (query.hwStatus && query.hwStatus !== "all") out = out.filter((n) => (n.hwStatus || "collecting") === query.hwStatus);
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())}`;
}
/* ============================ Mock 定义 ============================ */
export default [
/* ---------- 布局 / 用户 ---------- */
{
url: "/api/base/GetUserInfo",
method: "get",
response: () =>
viewerRole === "teacher"
? ok({ id: 186532, account: ME.account, name: ME.name, role: "teacher", profilePicture: "", school: { id: 3260, name: "上海市第一实验中学" } })
: ok({
id: DEMO_STUDENT.studentId,
account: "stu" + DEMO_STUDENT.studentId,
name: DEMO_STUDENT.studentName,
role: "student",
studentId: DEMO_STUDENT.studentId,
studentName: DEMO_STUDENT.studentName,
classId: DEMO_STUDENT.classId,
className: DEMO_STUDENT.className,
profilePicture: "",
school: { id: 3260, name: "上海市第一实验中学" },
}),
},
{
url: "/api/base/GetUserMod",
method: "get",
response: () =>
ok(
viewerRole === "student"
? [
{ id: 31, modId: 31, name: "今日作业", parentId: 0, page: "/stu/today", orderNumber: 1, usable: true, children: [] },
{ id: 32, modId: 32, name: "待订正", parentId: 0, page: "/stu/redo", orderNumber: 2, usable: true, children: [] },
]
: [
{ id: 2, modId: 2, name: "我布置的", parentId: 0, page: "/list", orderNumber: 1, usable: true, children: [] },
{ id: 26, modId: 26, name: "批改中心", parentId: 0, page: "/grade", orderNumber: 2, usable: true, children: [] },
{ id: 4, modId: 4, name: "作业统计", parentId: 0, page: "/stats", orderNumber: 3, usable: true, children: [] },
]
),
},
{
url: "/api/base/GetLayout",
method: "get",
response: () =>
ok({
ssoUrl: "",
username: viewerRole === "teacher" ? ME.name : DEMO_STUDENT.studentName,
pathList: [],
topName: "课程作业",
pageUrl: "",
logoUrl: "",
initUrl: "",
id: 0,
avatar: "",
}),
},
{
// 端分离入口:按 URL 前缀切换当前视角(teacher / student / parent)
url: "/api/base/SetRole",
method: "post",
response: ({ query, body }: any) => ok({ role: setViewerRole((bodyOf(body, query) || {}).as || query.as) }),
},
{ url: "/api/base/GetSchoolAll", method: "get", response: () => ok(schoolTree) },
{
// 年级 → 班级树(发布对象选择:到班级一级)
url: "/api/base/GetClassTree",
method: "get",
response: () => ok(classTree),
},
/* ---------- 发文部门字典 ---------- */
{
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/homework/List",
method: "get",
response: ({ query }: any) => {
const pageIndex = Number(query.pageIndex || 1);
const pageSize = Number(query.pageSize || 50);
let list = homeworks.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 unreadCount = list.filter((n) => !isReadByMe(n)).length;
return ok({ ...paginate(list.map(toVo), pageIndex, pageSize), unreadCount });
},
},
{
// 已发送(scope=user) / 全部作业(不传)
url: "/api/homework/SendList",
method: "get",
response: ({ query }: any) => {
const pageIndex = Number(query.pageIndex || 1);
const pageSize = Number(query.pageSize || 10);
let list = [...homeworks];
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/homework/Detail",
method: "get",
response: ({ query }: any) => {
const n = homeworks.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/homework/GetDetail",
method: "get",
response: ({ query }: any) => {
const n = homeworks.find((x) => x.id === Number(query.serid));
if (!n) return fail("作业不存在或已被删除");
return ok(toDetail(n));
},
},
{
url: "/api/homework/GetNextPrev",
method: "get",
response: ({ query }: any) => {
const ids = sortByTimeDesc(homeworks.filter(isPublished)).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/homework/ReadDetail",
method: "get",
response: ({ query }: any) => {
const n = homeworks.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/homework/Create",
method: "post",
response: ({ body, query }: any) => {
const b = bodyOf(body, query);
const dept = deptDict.find((d) => d.CodeID === b.deptId);
const receive = b.receiveList || "all";
const isAll = /^all$/i.test(String(receive).trim());
const item: any = {
id: homeworkSeq++,
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(),
subject: b.subject || "综合",
deadline: b.deadline ? String(b.deadline).replace(/\//g, "-") : null,
submitType: Array.isArray(b.submitType) ? b.submitType : String(b.submitType || "text").split(/[,,\s]+/).filter(Boolean),
allowLate: !!b.allowLate,
remindBefore: Array.isArray(b.remindBefore) ? b.remindBefore : String(b.remindBefore || "").split(/[,,\s]+/).filter(Boolean),
hwStatus: b.draft ? "draft" : "collecting",
approvalEnabled: 0,
approvalStatus: "none",
approvalRemark: null,
approvalBy: null,
approvalTime: null,
flow: null,
approvalLevel: 0,
};
homeworks.unshift(item);
return ok(item.id);
},
},
{
url: "/api/homework/Edit",
method: "post",
response: ({ body, query }: any) => {
const b = bodyOf(body, query);
const n = homeworks.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/homework/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 = homeworks.findIndex((n) => n.id === id);
if (i > -1) homeworks.splice(i, 1);
}
return ok(null);
},
},
{
url: "/api/homework/InitializeRed",
method: "post",
response: () => {
homeworks.forEach((n) => {
if (isPublished(n) && !n.classRead.includes(ME_CLASS)) n.classRead.push(ME_CLASS);
});
return ok({ count: homeworks.length });
},
},
{
url: "/api/homework/UserPull",
method: "post",
response: ({ body, query }: any) => {
const n = homeworks.find((x) => x.id === Number(bodyOf(body, query).serid));
if (n) n.isPulled = 1;
return ok(null);
},
},
{
url: "/api/homework/SetTopmost",
method: "post",
response: ({ body, query }: any) => {
const b = bodyOf(body, query);
const n = homeworks.find((x) => x.id === Number(b.serid));
if (n) n.topmost = Number(b.topmost ?? 1) ? 1 : 0;
return ok(null);
},
},
{ url: "/api/homework/Expedite", method: "post", response: () => ok(null) },
{ url: "/api/homework/ReceiveUsers", method: "post", response: () => ok(null) },
{
url: "/api/homework/Statis",
method: "get",
response: () => {
const byPublisher: Record<string, number> = {};
homeworks.forEach((n) => (byPublisher[n.publisherName] = (byPublisher[n.publisherName] || 0) + 1));
const mine = homeworks.filter((n: any) => n.publisherAccount === ME.account);
// 分数分布(仅统计已批改且有分数的提交)
const scored = submissions.filter((s) => s.grade && typeof s.grade.score === "number").map((s) => s.grade.score as number);
const bucket = { excellent: 0, good: 0, pass: 0, fail: 0 };
scored.forEach((x) => (x >= 90 ? bucket.excellent++ : x >= 80 ? bucket.good++ : x >= 60 ? bucket.pass++ : bucket.fail++));
// 按作业进度(我布置的、需收集的)
const byHomework = mine
.filter((n: any) => n.hwStatus !== "draft" && !(n.submitType || []).includes("none"))
.map((n: any) => ({ id: n.id, title: n.title, subject: n.subject, hwStatus: n.hwStatus, ...progressOf(n.id) }));
const pendingGrade = byHomework.reduce((s, h) => s + h.pendingGrade, 0);
const rates = byHomework.map((h) => h.submitRate).filter((r) => r > 0);
const avgSubmitRate = rates.length ? Number((rates.reduce((a, b) => a + b, 0) / rates.length).toFixed(1)) : 0;
return ok({
total: homeworks.length,
publishedCount: homeworks.filter((n) => isPublished(n) && !n.isPulled).length,
totalReadRecords: homeworks.reduce((s, n) => s + readCountOf(n), 0),
unreadForCurrentUser: homeworks.filter((n) => isPublished(n) && !isReadByMe(n)).length,
byPublisher: Object.keys(byPublisher).map((publisherName) => ({ publisherName, count: byPublisher[publisherName] })),
byMonth: [{ year: 2026, month: 9, count: homeworks.length }],
// ===== 课程作业口径(v1)=====
myAssignCount: mine.length,
pendingGrade,
avgSubmitRate,
byHomework,
scoreDist: [
{ label: "优秀(≥90)", key: "excellent", count: bucket.excellent },
{ label: "良好(80-89)", key: "good", count: bucket.good },
{ label: "及格(60-79)", key: "pass", count: bucket.pass },
{ label: "不及格(<60)", key: "fail", count: bucket.fail },
],
});
},
},
/* ---------- 课程作业 v1:提交情况 / 批改 / 催交 ---------- */
{
// 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo
url: "/api/homework/Submissions",
method: "get",
response: ({ query }: any) => {
const n = homeworks.find((x) => x.id === Number(query.serid));
if (!n) return fail("作业不存在或已被删除");
let list = subsOf(n.id);
const st = query.submitStatus || query.status;
if (st && st !== "all") {
if (st === "unsubmitted") list = list.filter((s) => s.submitStatus === "unsubmitted");
else if (st === "submitted" || st === "pending") list = list.filter((s) => s.submitStatus === "submitted");
else if (st === "graded") list = list.filter((s) => s.submitStatus === "graded" || s.submitStatus === "redone");
else if (st === "needRedo") list = list.filter((s) => s.submitStatus === "needRedo");
}
const byClass: Record<string, any[]> = {};
list.forEach((s) => {
(byClass[s.className] = byClass[s.className] || []).push({
subId: s.id,
studentId: s.studentId,
studentName: s.studentName,
submitStatus: s.submitStatus,
submittedAt: s.submittedAt,
hasGrade: !!s.grade,
score: s.grade ? s.grade.score : null,
level: s.grade ? s.grade.level : null,
isExcellent: s.grade ? !!s.grade.isExcellent : false,
});
});
return ok({
homeworkId: n.id,
title: n.title,
subject: n.subject,
deadline: n.deadline,
hwStatus: n.hwStatus,
hwStatusDesc: HW_STATUS_DESC[n.hwStatus] || "收集中",
summary: progressOf(n.id),
groups: Object.keys(byClass).map((cn) => ({ className: cn, students: byClass[cn] })),
});
},
},
{
// 单份提交详情(供批改工作台右侧展示)
url: "/api/homework/SubmissionDetail",
method: "get",
response: ({ query }: any) => {
const s = submissions.find((x) => x.id === Number(query.subId));
if (!s) return fail("提交记录不存在");
const n = homeworks.find((h) => h.id === s.homeworkId);
return ok({
subId: s.id,
homeworkId: s.homeworkId,
homeworkTitle: n ? n.title : "",
subject: n ? n.subject : "",
studentId: s.studentId,
studentName: s.studentName,
classId: s.classId,
className: s.className,
submitStatus: s.submitStatus,
submittedAt: s.submittedAt,
text: s.text,
images: s.images,
attachments: s.attachments,
grade: s.grade,
});
},
},
{
// 批改:action=grade(默认)/redo(要求订正)/excellent(标优秀);JSON body
url: "/api/homework/Grade",
method: "post",
response: ({ body, query }: any) => {
const b = bodyOf(body, query);
const s = submissions.find((x) => x.id === Number(b.subId ?? b.id));
if (!s) return fail("提交记录不存在");
const action = b.action || "grade";
if (action === "redo") {
s.submitStatus = "needRedo";
if (s.grade) s.grade.comment = b.comment ?? s.grade.comment;
return ok({ submitStatus: s.submitStatus, progress: progressOf(s.homeworkId) });
}
s.grade = {
score: b.score !== undefined && b.score !== "" ? Number(b.score) : s.grade ? s.grade.score : null,
level: b.level !== undefined ? b.level : s.grade ? s.grade.level : null,
comment: b.comment !== undefined ? b.comment : s.grade ? s.grade.comment : "",
isExcellent: action === "excellent" ? true : b.isExcellent !== undefined ? !!b.isExcellent : s.grade ? !!s.grade.isExcellent : false,
markedImages: b.markedImages !== undefined ? b.markedImages : s.grade ? s.grade.markedImages : [],
};
s.submitStatus = "graded";
return ok({ submitStatus: s.submitStatus, grade: s.grade, progress: progressOf(s.homeworkId) });
},
},
{
// 催交:对未交学生发提醒(mock 返回提醒人数,并把作业 smsFlag 置 1)
url: "/api/homework/Remind",
method: "post",
response: ({ body, query }: any) => {
const b = bodyOf(body, query);
const n = homeworks.find((x) => x.id === Number(b.serid));
if (!n) return fail("作业不存在");
const un = subsOf(n.id).filter((s) => s.submitStatus === "unsubmitted").length;
n.smsFlag = 1;
return ok({ reminded: un });
},
},
/* ---------- v2 学生端:我的作业 / 提交 / 反馈 ---------- */
{
url: "/api/homework/MyHomeworks",
method: "get",
response: () => {
const list = viewerHomeworks();
return ok({ buckets: viewerBuckets(list), list });
},
},
{
// 我的某份作业:作业信息 + 本人提交/批改
url: "/api/homework/MySubmission",
method: "get",
response: ({ query }: any) => {
const hw = homeworks.find((h) => h.id === Number(query.serid));
if (!hw) return fail("作业不存在");
const s = submissions.find((x) => x.homeworkId === hw.id && x.studentId === DEMO_STUDENT.studentId);
return ok({
homework: {
id: hw.id,
title: hw.title,
subject: hw.subject,
content: hw.content,
deadline: hw.deadline,
submitType: hw.submitType,
publisherName: hw.publisherName,
deptName: hw.deptName,
},
my: s
? { subId: s.id, submitStatus: s.submitStatus, submittedAt: s.submittedAt, text: s.text, images: s.images, attachments: s.attachments, grade: s.grade }
: null,
});
},
},
{
// 学生提交 / 订正重交(body: serid, text, images, attachments)
url: "/api/homework/Submit",
method: "post",
response: ({ body, query }: any) => {
const b = bodyOf(body, query);
const hw = homeworks.find((h) => h.id === Number(b.serid));
if (!hw) return fail("作业不存在");
if ((hw.submitType || []).includes("none")) return fail("该作业仅要求查阅,无需提交");
if (hw.hwStatus === "closed" && !hw.allowLate) return fail("该作业已过截止时间且未开放补交,请联系老师");
const parseArr = (v: any) => (Array.isArray(v) ? v : String(v || "").split(",").filter(Boolean));
let s = submissions.find((x) => x.homeworkId === hw.id && x.studentId === DEMO_STUDENT.studentId);
if (!s) {
s = {
id: subSeq++,
homeworkId: hw.id,
studentId: DEMO_STUDENT.studentId,
studentName: DEMO_STUDENT.studentName,
classId: String(DEMO_STUDENT.classId),
className: DEMO_STUDENT.className,
submittedAt: null,
submitStatus: "unsubmitted",
text: "",
images: [],
attachments: [],
grade: null,
};
submissions.push(s);
}
s.text = b.text !== undefined ? b.text : s.text;
s.images = parseArr(b.images);
s.attachments = parseArr(b.attachments);
s.submittedAt = nowStr();
s.submitStatus = s.submitStatus === "needRedo" ? "redone" : "submitted";
s.grade = null;
return ok({ subId: s.id, submitStatus: s.submitStatus });
},
},
/* ---------- 审批功能已移除(课程作业不做审批) ---------- */
/* ---------- 附件上传 ---------- */
{
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: mockImageDataURL(id) }]);
},
},
{ 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(id?: number) {
const label = id ? `课程作业 图片#${id}` : "课程作业 Mock 图片";
const svg =
`<svg xmlns='http://www.w3.org/2000/svg' width='240' height='180'><rect width='100%' height='100%' fill='#e8f1ff'/><rect x='8' y='8' width='224' height='164' fill='none' stroke='#5c9edc' stroke-width='2' stroke-dasharray='6 4'/><text x='50%' y='50%' font-size='16' fill='#5c9edc' text-anchor='middle' dominant-baseline='middle'>${label}</text></svg>`;
return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg)));
}