/** * 课程作业本地 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 = {}; 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: "

各任课教师:

第 10 周期中教学检查将核查各班作业布置与批改情况,请于本周五前将本周各科作业单提交年级组长汇总。

作业量、批改反馈与订正要求见附件评分标准。

", receiveList: "ALL", classRead: ["1301", "1302", "1303", "1401"], files: "9001,9002", topmost: 1, isPulled: 0, smsFlag: 0, timingSendTime: null, createTime: "2026-09-01 10:35:00", approvalEnabled: 0, approvalStatus: "none", approvalRemark: null, approvalBy: null, approvalTime: null, }, { id: 2, title: "初二语文《这样的人让我敬佩》写人记叙文(600 字)", deptId: "D001", deptName: "语文教研组", publisherAccount: "lwj", publisherName: "刘文娟", content: "

请同学们完成一篇写人记叙文,题目《这样的人让我敬佩》。

要求:不少于 600 字,选取身边真实人物,通过具体事例表现其品质;周日前拍照上传至班级作业本,纸质稿交语文课代表。

", 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: "

课后分 A、B 两层作业:A 组(基础)教材 P88 第 1–5 题;B 组(提高)配套练习第 6–9 题。

请按所在层次完成并写在作业本上,明天数学课随堂订正。

", 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: "

假期数学作业分两部分:①计算巩固——口算题卡 3 页;②实践探究——记录假期家庭一周开支并制成统计表。

已发布给全部班级,请于截止前提交,收假后第一节课查验。

", 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: "

完成附件《阅读专项训练》Section A–B(6 篇阅读、2 篇完形),答案填写在答题卡上。

限时 40 分钟独立完成,周五前拍照上传答题卡。

", 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: "

以《我家的十年变化》为主题采写一篇时事观察,制作一份 A3 手抄报。

要求标注数据来源或访谈对象,下周一课前交政史地办公室。

", 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: "

按教材步骤完成凸透镜成像实验并撰写实验报告,附物距—像距对照表。

提交:数据记录表、成像规律结论、误差分析各一份,拍照上传或纸质交物理课代表。

", 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: "

利用平移、旋转与轴对称设计一幅图案,并标注所使用的变换方式。

(草拟中,暂未定稿)

", 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 = { 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 = {}; 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 = { 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 = { 1: "朱皓东", 2: "刘文娟", 3: "陈垠", 4: "王海燕", 5: "李强", 6: "何明明", 7: "周敏", 8: "孙丽娟", 9: "吴建国", 10: "赵晓琳", 11: "黄瑶", }; const teacherNames: Record = { zhd: "朱皓东", lwj: "刘文娟", lwm: "刘文娟", cy: "陈垠", hy: "黄瑶", hj: "何明明", why: "王海燕", lq: "李强", zm: "周敏", slj: "孙丽娟", wjg: "吴建国", zxl: "赵晓琳", }; /** 教师组织树(应用设置-自定义组选人在用;发布对象已改为班级树) */ const schoolTree = [ { id: 301, name: "校长室", isParent: true, children: [ { id: 1, name: "朱皓东", isParent: false }, { id: 2, name: "刘文娟", isParent: false } ] }, { id: 302, name: "教务处", isParent: true, children: [ { id: 3, name: "陈垠", isParent: false }, { id: 6, name: "何明明", isParent: false }, { id: 7, name: "周敏", isParent: false } ] }, { id: 303, name: "学生处", isParent: true, children: [ { id: 4, name: "王海燕", isParent: false }, { id: 8, name: "孙丽娟", isParent: false }, { id: 9, name: "吴建国", isParent: false } ] }, { id: 304, name: "总务处", isParent: true, children: [ { id: 5, name: "李强", isParent: false } ] }, { id: 305, name: "信息中心", isParent: true, children: [ { id: 10, name: "赵晓琳", isParent: false }, { id: 11, name: "黄瑶", isParent: false } ] }, ]; /** 年级班级树(ztree 结构,供选班级弹窗使用;叶子带 studentCount) */ const classTree = gradeClasses.map((g) => ({ id: g.gradeId, name: g.gradeName, isParent: true, children: g.classes.map((c) => ({ id: c.classId, name: c.className, isParent: false, studentCount: c.studentCount })), })); /* ============================ 工具 ============================ */ function ok(data: any) { return { code: 0, msg: "操作成功", data }; } function fail(msg: string, code = 4) { return { code, msg, data: null }; } function paginate(content: any[], pageIndex: number, pageSize: number) { const start = (pageIndex - 1) * pageSize; const totalPages = pageSize ? Math.ceil(content.length / pageSize) : 0; return { content: content.slice(start, start + pageSize), total: content.length, pageIndex, pageSize, totalPages, hasNext: pageIndex < totalPages }; } function parseBody(body: any): any { if (body == null) return {}; if (typeof body === "object") return body; const str = String(body).trim(); if (!str) return {}; if (str.startsWith("{") || str.startsWith("[")) { try { return JSON.parse(str); } catch { /* fallthrough */ } } const out: Record = {}; new URLSearchParams(str).forEach((v, k) => (out[k] = v)); return out; } /** 合并取参:query(postForm 已把参数拼入 URL)优先兜底 + JSON body(Create/Edit/Delete) */ function bodyOf(rawBody: any, query: any): any { const parsed = parseBody(rawBody); if (Array.isArray(parsed)) return parsed; // Delete 传 id 数组 return { ...(query || {}), ...(parsed || {}) }; } /** 接收班级 id 数组("ALL" → 全部班级) */ function targetClassIds(n: any): string[] { return /^all$/i.test(String(n.receiveList).trim()) ? ALL_CLASSES : String(n.receiveList).split(",").map((s) => s.trim()).filter(Boolean); } function isAllTarget(n: any): boolean { return /^all$/i.test(String(n.receiveList).trim()); } function allowCountOf(n: any): number { if (isAllTarget(n)) return ALL_STUDENTS; return targetClassIds(n).reduce((s, id) => s + (classMap[id]?.studentCount || 0), 0); } function readCountOf(n: any): number { const targets = targetClassIds(n); return (n.classRead || []).filter((id: string) => targets.includes(id)).reduce((s, id) => s + (classMap[id]?.studentCount || 0), 0); } /** 收件箱视角:本班是否已读 */ function isReadByMe(n: any): boolean { return (n.classRead || []).includes(ME_CLASS); } /** 是否已发布(对学生可见):审批通过或免审 */ function isPublished(n: any): boolean { return n.approvalStatus === "none" || n.approvalStatus === "approved"; } /** 定时未到的作业,收件箱不展示 */ function isScheduledFuture(n: any): boolean { return !!(n.timingSendTime && n.timingSendTime > "2026-09-10"); } const APPROVAL_DESC: Record = { none: "已发布", pending: "待审批", approved: "已发布", rejected: "已驳回" }; const HW_STATUS_DESC: Record = { 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 = {}; 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 = {}; 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 = {}; 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 = `${label}`; return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); }