- 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(附件上传)——尚未挂路由,页面切换随后续阶段
1473 lines
67 KiB
TypeScript
1473 lines
67 KiB
TypeScript
/**
|
||
* 课程作业本地 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)));
|
||
}
|