diff --git a/.backup/20260915-drop-app-setting/src/api/module/homework.ts b/.backup/20260915-drop-app-setting/src/api/module/homework.ts new file mode 100644 index 0000000..af4eadb --- /dev/null +++ b/.backup/20260915-drop-app-setting/src/api/module/homework.ts @@ -0,0 +1,299 @@ +import { Method } from "alova"; +import { alovaInstance } from "@/utils/alova/alova-iview"; +import conf from "@/assets/config/host.config"; +import type { + HomeworkVo, + HomeworkDetailVo, + HomeworkPageResult, + BasePageResult, + UpFileResultVo, + UpFileVo, + InitializeRedVo, + NextPrevVo, + ReadGroup, + HomeworkStatisticsVo, + HomeworkRequest, + HomeworkQuery, + DictOption, + DeptManageItem, + ContactGroupPage, + ContactUserPage, + SchoolTreeNode, + ClassTreeNode, + SubmissionsResultVo, + SubmissionDetailVo, + GradeRequest, + MyHomeworksVo, + MySubmissionVo, + SubmitRequest, +} from "@/types/homework"; + +/** + * 课程作业接口封装(P0 标准化后新契约,P1-7 TS 契约化)。 + * + * 前缀说明:conf.serviceHost 本地为空(走 vite 代理),线上为基础平台域名; + * platform 固定使用 "api",与后端 Config.API_PATH 中的 api 匹配。 + * 统一响应由 alova-iview 解包为 data(code==0),页面直接使用返回值。 + * + * 变更要点(P0): + * 1. 作业域去掉 Homework_list 二级段(/api/homework/Homework_list/List → /api/homework/List) + * 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down) + * 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete) + * 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交) + * 5. 参数更名:chaoSend→fileIds、topmost→smsFlag + * 6. Delete 入参从 ids 逗号串改为 List JSON 数组 + * + * 变更要点(P1-7): + * 7. 全部方法加返回值泛型标注,替换 Record + * 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list) + */ + +const HOMEWORK = "/api/homework"; +const BASE = "/api/base"; +const DICT = "/api/dict"; +const FILE = "/api/file"; +const CONTACTS = "/api/contacts"; + +/** GET 请求,T 为返回数据类型 */ +function get(url: string, params?: Record): Method { + return new Method("GET", alovaInstance, url, { params }); +} + +/** POST JSON body(与后端 @RequestBody 对齐),T 为返回数据类型 */ +function postJSON(url: string, data?: Record | any[]): Method { + return new Method( + "POST", + alovaInstance, + url, + { headers: { "Content-Type": "application/json;charset=UTF-8" } }, + data ? JSON.stringify(data) : undefined + ); +} + +/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */ +function postForm(url: string, data?: Record): Method { + const pairs = data + ? Object.entries(data).filter(([, v]) => v !== undefined && v !== null) + : []; + const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&"); + // 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收, + // 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。 + const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url; + return new Method( + "POST", + alovaInstance, + fullUrl, + { headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } }, + form || undefined + ); +} + +/** 附件下载直链(浏览器 location.href 跳转) */ +function fileDownUrl(fileId: number): string { + return `${conf.serviceHost}${FILE}/Down?id=${fileId}`; +} + +/** 作业全部附件打包下载直链 */ +function allFileDownUrl(dataid: number): string { + return `${conf.serviceHost}${HOMEWORK}/AllFileDown?dataid=${dataid}`; +} + +class BaseApi { + // ========== 用户信息 & 模块权限 ========== + /** 获取当前登录用户信息 */ + getUserInfo() { + return get(`${BASE}/GetUserInfo`); + } + /** 获取当前用户已授权的模块列表 */ + getUserMod() { + return get(`${BASE}/GetUserMod`); + } + /** 当前学校组织树(部门→教师,用于选择用户) */ + getSchoolAll() { + return get(`${BASE}/GetSchoolAll`); + } + /** 年级→班级树(学生场景:选择通知对象到班级一级) */ + getClassTree() { + return get(`${BASE}/GetClassTree`); + } + /** 端分离入口:切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */ + setRole(as: "teacher" | "student" | "parent") { + return postJSON<{ role: string }>(`${BASE}/SetRole`, { as }); + } +} + +class HomeworkApi { + // ========== 收件箱 ========== + /** 已收作业列表 */ + list(params: HomeworkQuery) { + return get>(`${HOMEWORK}/List`, params); + } + /** 清单:一键全部已读 */ + initializeRed() { + return postForm(`${HOMEWORK}/InitializeRed`); + } + + // ========== 已发/全部作业 ========== + /** 已发/全部作业列表(scope="user" 只看自己,不传=全部) */ + sendList(params: HomeworkQuery) { + return get>(`${HOMEWORK}/SendList`, params); + } + + // ========== 详情 ========== + /** 查看详情(type=1 标读;type=0 编辑不标读) */ + modify(serid: number, type = 1) { + return get(`${HOMEWORK}/Detail`, { serid, type }); + } + /** 编辑回显(不标读) */ + getHomeworkDetails(serid: number) { + return get(`${HOMEWORK}/GetDetail`, { serid }); + } + /** 上一篇/下一篇 */ + getNextPrevHomework(serid: number, type: string) { + return get(`${HOMEWORK}/GetNextPrev`, { serid, type }); + } + /** 阅读情况(已读/未读分组) */ + orgizeModify(serid: number) { + return get(`${HOMEWORK}/ReadDetail`, { serid }); + } + /** 追加接收人 */ + receiveUsers(serid: number, users: string) { + return postForm(`${HOMEWORK}/ReceiveUsers`, { serid, users }); + } + + // ========== 发布/编辑/删除 ========== + /** 新建作业(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记) */ + create(data: HomeworkRequest) { + return postJSON(`${HOMEWORK}/Create`, data); + } + /** 编辑作业(JSON body,含 id) */ + editHomework(data: HomeworkRequest) { + return postJSON(`${HOMEWORK}/Edit`, data); + } + /** 删除作业(ids 数组,支持批量) */ + deleteHomework(ids: number | string) { + // 兼容页面传单个 id 或逗号串,统一转成数组 + const idArr = String(ids).split(",").map(Number).filter(Boolean); + return postJSON(`${HOMEWORK}/Delete`, idArr); + } + /** 撤回 */ + userPull(serid: number) { + return postForm(`${HOMEWORK}/UserPull`, { serid }); + } + /** 置顶/取消置顶 */ + setTopmost(serid: number, topmost = 1) { + return postForm(`${HOMEWORK}/SetTopmost`, { serid, topmost }); + } + /** 催收未读 */ + expedite(serid: number) { + return postForm(`${HOMEWORK}/Expedite`, { serid }); + } + + // ========== 课程作业 v1:提交情况 / 批改 / 催交 ========== + /** 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo */ + submissions(serid: number, submitStatus = "all") { + return get(`${HOMEWORK}/Submissions`, { serid, submitStatus }); + } + /** 单份提交详情(批改工作台右侧) */ + submissionDetail(subId: number) { + return get(`${HOMEWORK}/SubmissionDetail`, { subId }); + } + /** 批改(JSON body):action=grade/redo/excellent */ + grade(data: GradeRequest) { + return postJSON(`${HOMEWORK}/Grade`, data); + } + /** 催交未交学生 */ + remind(serid: number) { + return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid }); + } + + // ========== v2 学生端 ========== + /** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */ + myHomeworks() { + return get(`${HOMEWORK}/MyHomeworks`); + } + /** 我对某作业的详情(作业信息 + 我的提交/批改) */ + mySubmission(serid: number) { + return get(`${HOMEWORK}/MySubmission`, { serid }); + } + /** 提交 / 订正重交(JSON body:serid, text, images, attachments) */ + submit(data: SubmitRequest) { + return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data); + } + + // ========== 附件 ========== + fileDownUrl = fileDownUrl; + allFileDownUrl = allFileDownUrl; + + // ========== 部门字典 ========== + /** 发文部门下拉 */ + getSysDic() { + return get(`${DICT}/List`, { type: 1 }); + } + /** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */ + getSchModuleIsSms() { + return get(`${DICT}/Sync`); + } + /** 作业统计 */ + statistics(params?: { starttime?: string; endtime?: string }) { + return get(`${HOMEWORK}/Statis`, params); + } +} + +class SettingApi { + // ========== 发文部门管理 ========== + /** 获取发文部门管理列表 */ + getDictMangeList() { + return get(`${DICT}/ManageList`, { type: "dept" }); + } + /** 新增发文部门 */ + addDeptDic(name: string) { + return postForm(`${DICT}/Add`, { name }); + } + /** 更新发文部门(名称、启用状态) */ + updateDeptDic(SerID: number, Name: string, Enable = 1) { + return postForm(`${DICT}/Update`, { SerID, Name, Enable }); + } + /** 删除发文部门(原 GET DelDept,已 POST 化) */ + delDept(serid: number) { + return postForm(`${DICT}/Delete`, { serid }); + } + /** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */ + sortDeptDic(ids: string) { + return postForm(`${DICT}/Sort`, { ids }); + } + // ========== 自定义组管理 ========== + /** 获取联系组列表 */ + getGroup(page: number, count: number) { + return get(`${CONTACTS}/GetGroup`, { page, count }); + } + /** 新增联系组 */ + addGroup(name: string) { + return postForm(`${CONTACTS}/AddGroup`, { name }); + } + /** 编辑联系组 */ + editGroup(id: number, name: string) { + return postForm(`${CONTACTS}/EditGroup`, { name, id }); + } + /** 删除联系组 */ + delGroup(ids: string) { + return postForm(`${CONTACTS}/DelGroup`, { ids }); + } + /** 获取联系组用户 */ + getContactsTea(gid: number, page = 0, count = 0) { + return get(`${CONTACTS}/GetContactsTea`, { gid, page, count }); + } + /** 设置联系组用户 */ + setContactsTea(gid: number, ids: string) { + return postForm(`${CONTACTS}/SetContactsTea`, { gid, ids }); + } + /** 联系组用户树(选择用户-自定义组 tab) */ + getGroupTree() { + return get(`${CONTACTS}/GetGroupTree`, { type: 1 }); + } +} + +export const baseApi = new BaseApi(); +export const homeworkApi = new HomeworkApi(); +export const settingApi = new SettingApi(); +export default homeworkApi; diff --git a/.backup/20260915-drop-app-setting/src/assets/config/menu.ts b/.backup/20260915-drop-app-setting/src/assets/config/menu.ts new file mode 100644 index 0000000..5704d65 --- /dev/null +++ b/.backup/20260915-drop-app-setting/src/assets/config/menu.ts @@ -0,0 +1,18 @@ +import type { IModule } from "@/store"; + +/** + * 课程作业左侧菜单数据(对照后端 GetUserMod 现行模块)。 + * 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。 + * 注:课程作业不做审批,故不含「审批列表/审核设置」;平台模块配置若仍下发 modId 5/6, + * 由 utils/menuRoute 的 DROPPED_MOD_IDS 统一剔除(见 classwork-frontend-m5-plan.md §6)。 + */ +export function getMenuList(): Promise { + const menu: IModule[] = [ + { id: 1, modId: 1, name: "已收到", page: "/" }, + { id: 2, modId: 2, name: "已发送", page: "/list" }, + { id: 3, modId: 3, name: "全部作业", page: "/homeworklist" }, + { id: 4, modId: 4, name: "作业统计", page: "/stats" }, + { id: 21, modId: 21, name: "应用设置", page: "/setting" }, + ]; + return Promise.resolve(menu); +} diff --git a/.backup/20260915-drop-app-setting/src/assets/mock/homeworkapi.ts b/.backup/20260915-drop-app-setting/src/assets/mock/homeworkapi.ts new file mode 100644 index 0000000..46293bd --- /dev/null +++ b/.backup/20260915-drop-app-setting/src/assets/mock/homeworkapi.ts @@ -0,0 +1,1259 @@ +/** + * 课程作业本地 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/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. 当前登录演示身份 = 数学教师 朱皓东(zhd);收件箱以「初一(1)班(1301)」视角判定已读/未读。 + * 3. 课程作业不做审批(无送审环节,发布即可见),详见 classwork-frontend-m5-plan.md §6。 + */ + +/* ============================ 基础数据 ============================ */ + +/** 发布单位(作业归属教研组 / 教务处)字典 */ +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"; + +/** 年级 → 班级树(叶子=班级,带学生数) */ +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 列表(班级维度阅读统计)。 + * 无审批门控:仅草稿(hwStatus=draft)与定时未到点的作业不进入学生收件箱。 + */ +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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, + { + 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", + }, +]; +let homeworkSeq = 9; + +/* ===================== 课程作业扩展模型(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.hwStatus !== "draft"; +} +/** 定时未到的作业,收件箱不展示 */ +function isScheduledFuture(n: any): boolean { + return !!(n.timingSendTime && n.timingSendTime > "2026-09-10"); +} + +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, + 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, + 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", + }; + 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; + 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))); +} diff --git a/src/components/TypeSetBox.vue b/.backup/20260915-drop-app-setting/src/components/TypeSetBox.vue similarity index 100% rename from src/components/TypeSetBox.vue rename to .backup/20260915-drop-app-setting/src/components/TypeSetBox.vue diff --git a/src/components/UserCheckInput.vue b/.backup/20260915-drop-app-setting/src/components/UserCheckInput.vue similarity index 100% rename from src/components/UserCheckInput.vue rename to .backup/20260915-drop-app-setting/src/components/UserCheckInput.vue diff --git a/.backup/20260915-drop-app-setting/src/router/routes.ts b/.backup/20260915-drop-app-setting/src/router/routes.ts new file mode 100644 index 0000000..d058983 --- /dev/null +++ b/.backup/20260915-drop-app-setting/src/router/routes.ts @@ -0,0 +1,124 @@ +import type { RouteRecordRaw } from "vue-router"; + +/** + * 课程作业模块路由(扁平短路由,与基础平台模块 path 一致)。 + * 页面间参数通过 query 传递: + * - /list/new?serid= (编辑) + * - /list/new?fid= (转发) + * - /list/new?tempid=(模板引用) + * - /list/detail?serid=&type=、/homeworklist/detail?serid=&type= + * type: 0=编辑视角(不标读) 1=普通查看(标读) + * + * modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据): + * 1=已收到 2=已发送 3=全部作业 4=作业统计 21=应用设置 + * 发作业为「已发送」的深层子页(modId 22 不在菜单树,meta.parentId=2 关联高亮), + * 与 UserBase「应用管理>应用类型管理」范式一致。 + */ +const routes: RouteRecordRaw[] = [ + // v1 教师默认落地:我布置的 + { path: "/", redirect: "/list" }, + // 学生收件箱(v2 保留,不在菜单) + { + path: "/accept", + name: "homework_accept", + component: () => import("@/views/homework/HomeworkAccept.vue"), + meta: { title: "已收到", modId: 1 }, + }, + // 我布置的(教师端作业列表) + { + path: "/list", + name: "homework_list", + component: () => import("@/views/homework/HomeworkList.vue"), + meta: { title: "我布置的", modId: 2 }, + }, + // 提交情况(某作业的已交/未交名单;从列表行或菜单进入,无 serid 时页内选择作业) + { + path: "/submissions", + name: "homework_submissions", + component: () => import("@/views/homework/Submissions.vue"), + meta: { title: "提交情况", modId: 25 }, + }, + // 批改中心(待批作业→逐份批改工作台;无 serid 时先看待批作业列表) + { + path: "/grade", + name: "homework_grade", + component: () => import("@/views/homework/Grade.vue"), + meta: { title: "批改中心", modId: 26 }, + }, + // 发作业(新建/编辑/转发/模板):已发送的深层子页,不在菜单里, + // layout.vue 按.meta.parentId 推导面包屑「已发送>发作业」并高亮父菜单 + { + path: "/list/new", + name: "new_homework", + component: () => import("@/views/homework/NewHomework.vue"), + meta: { title: "发作业", modId: 22, parentId: 2 }, + }, + // 全部作业(管理端) + { + path: "/homeworklist", + name: "homework_manage", + component: () => import("@/views/homework/HomeworkList.vue"), + meta: { title: "全部作业", modId: 3 }, + }, + // 作业详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点: + // 已发送下 /list/detail,全部作业下 /homeworklist/detail;type 区分是否标读) + { + path: "/list/detail", + name: "homework_list_detail", + component: () => import("@/views/homework/HomeworkDetails.vue"), + meta: { title: "作业详情", modId: 23, parentId: 2 }, + }, + { + path: "/homeworklist/detail", + name: "homework_manage_detail", + component: () => import("@/views/homework/HomeworkDetails.vue"), + meta: { title: "作业详情", modId: 24, parentId: 3 }, + }, + // 作业统计(旧版行为:整页跳转外部统计系统) + { + path: "/stats", + name: "homework_stats", + component: () => import("@/views/homework/HomeworkStats.vue"), + meta: { title: "作业统计", modId: 4 }, + }, + // 应用设置(发文部门 + 自定义组) + { + path: "/setting", + name: "homework_setting", + component: () => import("@/views/homework/HomeworkSetting.vue"), + meta: { title: "应用设置", modId: 21 }, + }, + + /* ---- v2 学生端 ---- */ + // 学生·今日作业 + { + path: "/stu/today", + name: "student_today", + component: () => import("@/views/homework/StudentHomeworks.vue"), + meta: { title: "今日作业", modId: 31, role: "student" }, + }, + // 学生·待订正(复用同一页,默认过滤待订正) + { + path: "/stu/redo", + name: "student_redo", + component: () => import("@/views/homework/StudentHomeworks.vue"), + meta: { title: "待订正", modId: 32, role: "student", initTab: "redo" }, + }, + + /* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */ + { path: "/details", redirect: "/list/detail" }, + { path: "/newhomework", redirect: "/list/new" }, + { path: "/edit", redirect: "/list/new" }, + { path: "/manage", redirect: "/homeworklist" }, + { path: "/homework", redirect: "/accept" }, + { path: "/homework/newhomework", redirect: "/list/new" }, + { path: "/homework/edit", redirect: "/list/new" }, + { path: "/homework/homework_accept", redirect: "/accept" }, + { path: "/homework/homework_list", redirect: "/list" }, + { path: "/homework/manage", redirect: "/homeworklist" }, + { path: "/homework/homework_details", redirect: "/list/detail" }, + { path: "/homework/statistics", redirect: "/stats" }, + { path: "/homework/setting", redirect: "/setting" }, +]; + +export default routes; diff --git a/.backup/20260915-drop-app-setting/src/types/homework.ts b/.backup/20260915-drop-app-setting/src/types/homework.ts new file mode 100644 index 0000000..b421ea3 --- /dev/null +++ b/.backup/20260915-drop-app-setting/src/types/homework.ts @@ -0,0 +1,492 @@ +/** + * 课程作业前端类型契约(P1-7) + * + * 与后端 VO/DTO 一一对齐: + * - HomeworkVo → 后端 model/homework/HomeworkVo.java(Jackson 默认驼峰序列化) + * - HomeworkDetailVo → 后端 model/homework/HomeworkDetailVo.java(@JsonProperty 保留旧字段名) + * - BasePageResult → 后端 model/BasePageResult.java + * - HomeworkPageResult extends BasePageResult → 后端 model/homework/HomeworkPageResult.java + * - UpFileResultVo → 后端 model/homework/UpFileResultVo.java(@JsonProperty) + * - UpFileVo → 后端 model/homework/UpFileVo.java + * - InitializeRedVo → 后端 model/homework/InitializeRedVo.java(@JsonProperty) + * - HomeworkStatisticsVo → 后端 model/homework/HomeworkStatisticsVo.java + * - HomeworkRequest → 后端 model/homework/HomeworkRequest.java + * - HomeworkQuery → 后端 model/homework/HomeworkQuery.java + BasePageQuery.java + */ + +// ============================ 分页外壳 ============================ + +/** 统一分页响应外壳(对齐 BasePageResult.java) */ +export interface BasePageResult { + total: number; + content: T[]; + pageIndex: number; + pageSize: number; + totalPages: number; + hasNext: boolean; +} + +/** 作业分页结果(对齐 HomeworkPageResult.java,扩展 unreadCount) */ +export interface HomeworkPageResult extends BasePageResult { + /** 当前用户未读作业总数 */ + unreadCount: number; +} + +// ============================ 作业列表项 ============================ + +/** 作业视图对象(对齐 HomeworkVo.java,Jackson 默认驼峰序列化) */ +export interface HomeworkVo { + id: number; + title: string; + content: string; + type: number; + typeDesc: string; + publisherAccount: string; + publisherName: string; + deptId: string; + deptName: string; + status: number; + statusDesc: string; + topmost: number; + /** 已读人数 */ + readCount: number; + /** 应读人数 */ + allowCount: number; + /** 当前用户是否已读 */ + read: boolean; + /** 是否撤回:0 否 / 1 是 */ + isPulled: number; + /** 定时发送时间 */ + timingSendTime: string | null; + /** 父作业ID(转发时关联原作业) */ + parentId: number | null; + /** 短信发送标记 */ + smsFlag: number; + /** 接收对象类型:class=班级(学生场景) */ + receiveType?: string; + /** 接收列表:"all" 或 班级 id 逗号串 */ + receiveList: string | null; + /** ===== 课程作业 v1(教师核心闭环)===== */ + /** 学科 */ + subject?: string; + /** 截止时间 */ + deadline?: string | null; + /** 提交方式:text/image/attach/none */ + submitType?: string[]; + /** 作业流程状态:draft 草稿 / collecting 收集中 / closed 已截止 / done 已完成 */ + hwStatus?: HomeworkStatus; + hwStatusDesc?: string; + /** 提交/批改进度(草稿或仅查阅为 null) */ + progress?: HomeworkProgress | null; + createBy: string | null; + createTime: string | null; + updateBy: string | null; + updateTime: string | null; +} + +// ============================ 作业详情 ============================ + +/** 作业详情视图对象(对齐 HomeworkDetailVo.java,@JsonProperty 保留旧字段名) */ +export interface HomeworkDetailVo { + serid: number; + title: string; + /** 作业内容(旧字段名 Contents) */ + Contents: string; + /** 发布部门ID(旧字段名 DeptID) */ + DeptID: string; + /** 发布部门名称(旧字段名 DeptName) */ + DeptName: string; + /** 发布人姓名(旧字段名 TeaName) */ + TeaName: string; + /** 发布时间(旧字段名 EditDate,已格式化为字符串) */ + EditDate: string; + /** 应读人数 */ + numcount: number; + /** 已读人数 */ + readnumcount: number; + /** 接收人列表 */ + ReceiveList: string | null; + /** 定时发送时间(旧字段名 DsfsTime,已格式化) */ + DsfsTime: string | null; + /** 是否定时发送 */ + isDsfs: boolean; + /** ===== 课程作业 v1 ===== */ + subject?: string; + deadline?: string | null; + submitType?: string[]; + hwStatus?: HomeworkStatus; + hwStatusDesc?: string; + progress?: HomeworkProgress | null; + /** 附件列表(旧字段名 ChaoSend) */ + ChaoSend: HomeworkDetailFileItem[]; + /** 接收对象列表:"all" 或 班级 id 数组 */ + tlist: string | string[]; + /** 接收对象类型:class=班级 */ + receiveType?: string; + /** 接收对象可读文本(全部班级 / 班级名串) */ + receiveText?: string; +} + +/** 作业详情附件项(对齐 HomeworkDetailVo.FileItem) */ +export interface HomeworkDetailFileItem { + id: number; + name: string; + size: number; + fileid: number; + filename: string; +} + +// ============================ 文件上传 ============================ + +/** 文件上传结果(对齐 UpFileResultVo.java,@JsonProperty) */ +export interface UpFileResultVo { + fileid: number; + name: string; + fileurl: string; + host: string; + type: string; + size: number; +} + +/** 附件视图对象(对齐 UpFileVo.java) */ +export interface UpFileVo { + id: number; + fileTrueName: string; + fileName: string; + fileSize: number; + fileType: string; + topicId: number; + topicType: string; +} + +// ============================ 一键已读 ============================ + +/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */ +export interface InitializeRedVo { + count: number; +} + +// ============================ 上一篇/下一篇 ============================ + +/** 上一篇/下一篇作业ID */ +export interface NextPrevVo { + prevId: number; + nextId: number; +} + +// ============================ 阅读情况 ============================ + +/** 阅读情况分组项(后端 Map 返回,字段名固定) */ +export interface ReadGroup { + GroupName: string; + teaslist: ReadGroupUser[]; +} + +/** 阅读情况用户项(后端 Map 返回) */ +export interface ReadGroupUser { + account: string; + teaname: string; + states: 'read' | 'unread'; +} + +// ============================ 统计 ============================ + +/** 作业统计视图(对齐 HomeworkStatisticsVo.java) */ +export interface HomeworkStatisticsVo { + total: number; + publishedCount: number; + totalReadRecords: number; + unreadForCurrentUser: number; + byPublisher: { publisherName: string; count: number }[]; + byMonth: { year: number; month: number; count: number }[]; + // ===== 课程作业口径(v1,可选) ===== + /** 我布置的作业数 */ + myAssignCount?: number; + /** 待批改总量 */ + pendingGrade?: number; + /** 平均提交率(%) */ + avgSubmitRate?: number; + /** 按作业进度 */ + byHomework?: { id: number; title: string; subject: string; hwStatus: HomeworkStatus } & HomeworkProgress[]; + /** 分数分布 */ + scoreDist?: { label: string; key: string; count: number }[]; +} + +// ============================ 课程作业 v1:提交 / 批改 ============================ + +/** 作业流程状态 */ +export type HomeworkStatus = "draft" | "collecting" | "closed" | "done"; + +/** 单个提交状态 */ +export type SubmitStatus = "unsubmitted" | "submitted" | "graded" | "needRedo" | "redone"; + +/** 提交/批改进度 */ +export interface HomeworkProgress { + /** 应交人数 */ + should: number; + /** 已交人数 */ + submitted: number; + /** 已批改人数 */ + graded: number; + /** 待批改人数 */ + pendingGrade: number; + /** 待订正人数 */ + needRedo: number; + /** 已订正人数 */ + redone: number; + /** 未交人数 */ + unsubmitted: number; + /** 提交率(%) */ + submitRate: number; + /** 批改率(%) */ + gradeRate: number; +} + +/** 提交名单项(Submissions 接口返回,精简) */ +export interface SubmissionBrief { + subId: number; + studentId: number; + studentName: string; + submitStatus: SubmitStatus; + submittedAt: string | null; + hasGrade: boolean; + score: number | null; + level: string | null; + isExcellent: boolean; +} + +/** 提交名单按班级分组 */ +export interface SubmissionGroup { + className: string; + students: SubmissionBrief[]; +} + +/** 某作业提交情况结果 */ +export interface SubmissionsResultVo { + homeworkId: number; + title: string; + subject: string; + deadline: string | null; + hwStatus: HomeworkStatus; + hwStatusDesc: string; + summary: HomeworkProgress; + groups: SubmissionGroup[]; +} + +/** 批改结果 */ +export interface GradeResult { + score: number | null; + level: string | null; + comment: string; + isExcellent: boolean; + markedImages: string[]; +} + +/** 单份提交详情(批改工作台用) */ +export interface SubmissionDetailVo { + subId: number; + homeworkId: number; + homeworkTitle: string; + subject: string; + studentId: number; + studentName: string; + classId: string; + className: string; + submitStatus: SubmitStatus; + submittedAt: string | null; + text: string; + images: string[]; + attachments: string[]; + grade: GradeResult | null; +} + +/** 批改入参 */ +export interface GradeRequest { + subId: number; + /** grade 保存批改 / redo 要求订正 / excellent 标为优秀 */ + action?: "grade" | "redo" | "excellent"; + score?: number | string; + level?: string; + comment?: string; + isExcellent?: boolean; + markedImages?: string[]; +} + +// ============================ 请求入参 ============================ + +/** 作业发布/编辑入参(对齐 HomeworkRequest.java) */ +export interface HomeworkRequest { + title: string; + content?: string; + /** 学科 */ + subject?: string; + /** 截止时间 yyyy-MM-dd HH:mm:ss */ + deadline?: string; + /** 提交方式:text/image/attach/none */ + submitType?: string[] | string; + type?: number; + deptId?: string; + /** 是否允许逾期补交 */ + allowLate?: boolean; + /** 截止提醒:如 ["1d","1h"] */ + remindBefore?: string[]; + /** 存草稿:true 则不发布 */ + draft?: boolean; + deptName?: string; + topmost?: number; + /** 接收对象:"all" 或 班级 id 逗号串(receiveType=class) */ + receiveList?: string; + /** 接收对象类型:class=班级 */ + receiveType?: string; + /** 附件文件ID列表(逗号分隔串) */ + fileIds?: string; + timingSendTime?: string; + /** 短信发送标记 */ + smsFlag?: number; + /** 转发源作业ID */ + sourceId?: number; +} + +/** 作业列表查询参数(对齐 HomeworkQuery.java + BasePageQuery.java) */ +export interface HomeworkQuery { + pageIndex?: number; + pageSize?: number; + keyword?: string; + status?: number; + type?: number; + /** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */ + scope?: string; + deptId?: string; + /** 学科筛选 */ + subject?: string; + /** 作业流程状态筛选:draft/collecting/closed/done,缺省或 all=全部 */ + hwStatus?: string; + publisherAccount?: string; + /** 开始时间(yyyy-MM-dd 短日期) */ + startTime?: string; + /** 结束时间(yyyy-MM-dd 短日期) */ + endTime?: string; + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + readType?: number; + /** 排序:1 时间倒序 / 2 时间正序 */ + orderByTime?: string; +} + +// ============================ 字典/联系组 ============================ + +/** 系统字典下拉项 */ +export interface DictOption { + k: number | string; + v: string | null; + [key: string]: any; +} + +/** 发文部门管理列表项 */ +export interface DeptManageItem { + SerID: number; + CodeID: string; + Name: string; + Enable: number; +} + +/** 联系组列表项 */ +export interface ContactGroupItem { + serid: number; + GroupName: string; +} + +/** 联系组分页结果 */ +export interface ContactGroupPage { + list: ContactGroupItem[]; + total: number; +} + +/** 联系组用户项 */ +export interface ContactUser { + account: string; + name: string; + userId?: number; + id?: number; +} + +/** 联系组用户分页结果 */ +export interface ContactUserPage { + list: ContactUser[]; + total: number; +} + +/** 组织树节点(基础平台返回) */ +export interface SchoolTreeNode { + [key: string]: any; +} + +// ============================ 班级 ============================ + +/** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */ +export interface ClassTreeNode { + id: number; + name: string; + isParent: boolean; + studentCount?: number; + children?: ClassTreeNode[]; + [key: string]: any; +} + +// ============================ v2 学生端 ============================ + +/** 当前视角角色(家长即登录学生账号,不单列) */ +export type ViewerRole = "teacher" | "student"; + +/** 学生对某作业的提交/批改态 */ +export interface StudentSubmissionInfo { + subId: number; + submitStatus: SubmitStatus; + submittedAt: string | null; + text: string; + score: number | null; + level: string | null; + comment: string; + isExcellent: boolean; + images?: string[]; + attachments?: string[]; + grade?: GradeResult | null; +} + +/** 学生视角作业项 */ +export interface StudentHomeworkVo { + id: number; + title: string; + subject: string; + publisherName: string; + deptName: string; + deadline: string | null; + createTime: string | null; + content: string; + submitType: string[]; + viewOnly: boolean; + hwStatus?: HomeworkStatus; + allowLate?: boolean; + my: StudentSubmissionInfo | null; +} + +/** 今日/我的作业列表 */ +export interface MyHomeworksVo { + buckets: { todo: number; submitted: number; graded: number; redo: number; all: number }; + list: StudentHomeworkVo[]; +} + +/** 我的某作业详情(作业信息 + 我的提交/批改) */ +export interface MySubmissionVo { + homework: { id: number; title: string; subject: string; content: string; deadline: string | null; submitType: string[]; publisherName: string; deptName: string }; + my: StudentSubmissionInfo | null; +} + +/** 学生提交作业入参 */ +export interface SubmitRequest { + serid: number; + text?: string; + /** 图片 URL / dataURL 列表(逗号串或数组) */ + images?: string[] | string; + /** 附件 id 列表(逗号串或数组) */ + attachments?: string[] | string; +} diff --git a/src/views/homework/HomeworkSetting.vue b/.backup/20260915-drop-app-setting/src/views/homework/HomeworkSetting.vue similarity index 100% rename from src/views/homework/HomeworkSetting.vue rename to .backup/20260915-drop-app-setting/src/views/homework/HomeworkSetting.vue diff --git a/.backup/20260915-drop-approval/src/App.vue b/.backup/20260915-drop-approval/src/App.vue new file mode 100644 index 0000000..6763c29 --- /dev/null +++ b/.backup/20260915-drop-approval/src/App.vue @@ -0,0 +1,132 @@ + + + + + \ No newline at end of file diff --git a/.backup/20260915-drop-approval/src/api/module/homework.ts b/.backup/20260915-drop-approval/src/api/module/homework.ts new file mode 100644 index 0000000..7a0d487 --- /dev/null +++ b/.backup/20260915-drop-approval/src/api/module/homework.ts @@ -0,0 +1,301 @@ +import { Method } from "alova"; +import { alovaInstance } from "@/utils/alova/alova-iview"; +import conf from "@/assets/config/host.config"; +import type { + HomeworkVo, + HomeworkDetailVo, + HomeworkPageResult, + BasePageResult, + UpFileResultVo, + UpFileVo, + InitializeRedVo, + NextPrevVo, + ReadGroup, + HomeworkStatisticsVo, + HomeworkRequest, + HomeworkQuery, + DictOption, + DeptManageItem, + ContactGroupPage, + ContactUserPage, + SchoolTreeNode, + ClassTreeNode, + ApprovalQuery, + ApproverVo, + SubmissionsResultVo, + SubmissionDetailVo, + GradeRequest, + MyHomeworksVo, + MySubmissionVo, + SubmitRequest, +} from "@/types/homework"; + +/** + * 课程作业接口封装(P0 标准化后新契约,P1-7 TS 契约化)。 + * + * 前缀说明:conf.serviceHost 本地为空(走 vite 代理),线上为基础平台域名; + * platform 固定使用 "api",与后端 Config.API_PATH 中的 api 匹配。 + * 统一响应由 alova-iview 解包为 data(code==0),页面直接使用返回值。 + * + * 变更要点(P0): + * 1. 作业域去掉 Homework_list 二级段(/api/homework/Homework_list/List → /api/homework/List) + * 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down) + * 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete) + * 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交) + * 5. 参数更名:chaoSend→fileIds、topmost→smsFlag + * 6. Delete 入参从 ids 逗号串改为 List JSON 数组 + * + * 变更要点(P1-7): + * 7. 全部方法加返回值泛型标注,替换 Record + * 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list) + */ + +const HOMEWORK = "/api/homework"; +const BASE = "/api/base"; +const DICT = "/api/dict"; +const FILE = "/api/file"; +const CONTACTS = "/api/contacts"; + +/** GET 请求,T 为返回数据类型 */ +function get(url: string, params?: Record): Method { + return new Method("GET", alovaInstance, url, { params }); +} + +/** POST JSON body(与后端 @RequestBody 对齐),T 为返回数据类型 */ +function postJSON(url: string, data?: Record | any[]): Method { + return new Method( + "POST", + alovaInstance, + url, + { headers: { "Content-Type": "application/json;charset=UTF-8" } }, + data ? JSON.stringify(data) : undefined + ); +} + +/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */ +function postForm(url: string, data?: Record): Method { + const pairs = data + ? Object.entries(data).filter(([, v]) => v !== undefined && v !== null) + : []; + const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&"); + // 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收, + // 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。 + const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url; + return new Method( + "POST", + alovaInstance, + fullUrl, + { headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } }, + form || undefined + ); +} + +/** 附件下载直链(浏览器 location.href 跳转) */ +function fileDownUrl(fileId: number): string { + return `${conf.serviceHost}${FILE}/Down?id=${fileId}`; +} + +/** 作业全部附件打包下载直链 */ +function allFileDownUrl(dataid: number): string { + return `${conf.serviceHost}${HOMEWORK}/AllFileDown?dataid=${dataid}`; +} + +class BaseApi { + // ========== 用户信息 & 模块权限 ========== + /** 获取当前登录用户信息 */ + getUserInfo() { + return get(`${BASE}/GetUserInfo`); + } + /** 获取当前用户已授权的模块列表 */ + getUserMod() { + return get(`${BASE}/GetUserMod`); + } + /** 当前学校组织树(部门→教师,用于选择用户) */ + getSchoolAll() { + return get(`${BASE}/GetSchoolAll`); + } + /** 年级→班级树(学生场景:选择通知对象到班级一级) */ + getClassTree() { + return get(`${BASE}/GetClassTree`); + } + /** 端分离入口:切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */ + setRole(as: "teacher" | "student" | "parent") { + return postJSON<{ role: string }>(`${BASE}/SetRole`, { as }); + } +} + +class HomeworkApi { + // ========== 收件箱 ========== + /** 已收作业列表 */ + list(params: HomeworkQuery) { + return get>(`${HOMEWORK}/List`, params); + } + /** 清单:一键全部已读 */ + initializeRed() { + return postForm(`${HOMEWORK}/InitializeRed`); + } + + // ========== 已发/全部作业 ========== + /** 已发/全部作业列表(scope="user" 只看自己,不传=全部) */ + sendList(params: HomeworkQuery) { + return get>(`${HOMEWORK}/SendList`, params); + } + + // ========== 详情 ========== + /** 查看详情(type=1 标读;type=0 编辑不标读) */ + modify(serid: number, type = 1) { + return get(`${HOMEWORK}/Detail`, { serid, type }); + } + /** 编辑回显(不标读) */ + getHomeworkDetails(serid: number) { + return get(`${HOMEWORK}/GetDetail`, { serid }); + } + /** 上一篇/下一篇 */ + getNextPrevHomework(serid: number, type: string) { + return get(`${HOMEWORK}/GetNextPrev`, { serid, type }); + } + /** 阅读情况(已读/未读分组) */ + orgizeModify(serid: number) { + return get(`${HOMEWORK}/ReadDetail`, { serid }); + } + /** 追加接收人 */ + receiveUsers(serid: number, users: string) { + return postForm(`${HOMEWORK}/ReceiveUsers`, { serid, users }); + } + + // ========== 发布/编辑/删除 ========== + /** 新建作业(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记) */ + create(data: HomeworkRequest) { + return postJSON(`${HOMEWORK}/Create`, data); + } + /** 编辑作业(JSON body,含 id) */ + editHomework(data: HomeworkRequest) { + return postJSON(`${HOMEWORK}/Edit`, data); + } + /** 删除作业(ids 数组,支持批量) */ + deleteHomework(ids: number | string) { + // 兼容页面传单个 id 或逗号串,统一转成数组 + const idArr = String(ids).split(",").map(Number).filter(Boolean); + return postJSON(`${HOMEWORK}/Delete`, idArr); + } + /** 撤回 */ + userPull(serid: number) { + return postForm(`${HOMEWORK}/UserPull`, { serid }); + } + /** 置顶/取消置顶 */ + setTopmost(serid: number, topmost = 1) { + return postForm(`${HOMEWORK}/SetTopmost`, { serid, topmost }); + } + /** 催收未读 */ + expedite(serid: number) { + return postForm(`${HOMEWORK}/Expedite`, { serid }); + } + + // ========== 课程作业 v1:提交情况 / 批改 / 催交 ========== + /** 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo */ + submissions(serid: number, submitStatus = "all") { + return get(`${HOMEWORK}/Submissions`, { serid, submitStatus }); + } + /** 单份提交详情(批改工作台右侧) */ + submissionDetail(subId: number) { + return get(`${HOMEWORK}/SubmissionDetail`, { subId }); + } + /** 批改(JSON body):action=grade/redo/excellent */ + grade(data: GradeRequest) { + return postJSON(`${HOMEWORK}/Grade`, data); + } + /** 催交未交学生 */ + remind(serid: number) { + return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid }); + } + + // ========== v2 学生端 ========== + /** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */ + myHomeworks() { + return get(`${HOMEWORK}/MyHomeworks`); + } + /** 我对某作业的详情(作业信息 + 我的提交/批改) */ + mySubmission(serid: number) { + return get(`${HOMEWORK}/MySubmission`, { serid }); + } + /** 提交 / 订正重交(JSON body:serid, text, images, attachments) */ + submit(data: SubmitRequest) { + return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data); + } + + // ========== 附件 ========== + fileDownUrl = fileDownUrl; + allFileDownUrl = allFileDownUrl; + + // ========== 部门字典 ========== + /** 发文部门下拉 */ + getSysDic() { + return get(`${DICT}/List`, { type: 1 }); + } + /** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */ + getSchModuleIsSms() { + return get(`${DICT}/Sync`); + } + /** 作业统计 */ + statistics(params?: { starttime?: string; endtime?: string }) { + return get(`${HOMEWORK}/Statis`, params); + } +} + +class SettingApi { + // ========== 发文部门管理 ========== + /** 获取发文部门管理列表 */ + getDictMangeList() { + return get(`${DICT}/ManageList`, { type: "dept" }); + } + /** 新增发文部门 */ + addDeptDic(name: string) { + return postForm(`${DICT}/Add`, { name }); + } + /** 更新发文部门(名称、启用状态) */ + updateDeptDic(SerID: number, Name: string, Enable = 1) { + return postForm(`${DICT}/Update`, { SerID, Name, Enable }); + } + /** 删除发文部门(原 GET DelDept,已 POST 化) */ + delDept(serid: number) { + return postForm(`${DICT}/Delete`, { serid }); + } + /** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */ + sortDeptDic(ids: string) { + return postForm(`${DICT}/Sort`, { ids }); + } + // ========== 自定义组管理 ========== + /** 获取联系组列表 */ + getGroup(page: number, count: number) { + return get(`${CONTACTS}/GetGroup`, { page, count }); + } + /** 新增联系组 */ + addGroup(name: string) { + return postForm(`${CONTACTS}/AddGroup`, { name }); + } + /** 编辑联系组 */ + editGroup(id: number, name: string) { + return postForm(`${CONTACTS}/EditGroup`, { name, id }); + } + /** 删除联系组 */ + delGroup(ids: string) { + return postForm(`${CONTACTS}/DelGroup`, { ids }); + } + /** 获取联系组用户 */ + getContactsTea(gid: number, page = 0, count = 0) { + return get(`${CONTACTS}/GetContactsTea`, { gid, page, count }); + } + /** 设置联系组用户 */ + setContactsTea(gid: number, ids: string) { + return postForm(`${CONTACTS}/SetContactsTea`, { gid, ids }); + } + /** 联系组用户树(选择用户-自定义组 tab) */ + getGroupTree() { + return get(`${CONTACTS}/GetGroupTree`, { type: 1 }); + } +} + +export const baseApi = new BaseApi(); +export const homeworkApi = new HomeworkApi(); +export const settingApi = new SettingApi(); +export default homeworkApi; diff --git a/.backup/20260915-drop-approval/src/assets/config/menu.ts b/.backup/20260915-drop-approval/src/assets/config/menu.ts new file mode 100644 index 0000000..1237d71 --- /dev/null +++ b/.backup/20260915-drop-approval/src/assets/config/menu.ts @@ -0,0 +1,18 @@ +import type { IModule } from "@/store"; + +/** + * 课程作业左侧菜单数据(对照后端 GetUserMod 现行模块)。 + * 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。 + */ +export function getMenuList(): Promise { + const menu: IModule[] = [ + { id: 1, modId: 1, name: "已收到", page: "/" }, + { id: 2, modId: 2, name: "已发送", page: "/list" }, + { id: 3, modId: 3, name: "全部作业", page: "/homeworklist" }, + { id: 5, modId: 5, name: "审批列表", page: "/approval" }, + { id: 6, modId: 6, name: "审核设置", page: "/approval-setting" }, + { id: 4, modId: 4, name: "作业统计", page: "/stats" }, + { id: 21, modId: 21, name: "应用设置", page: "/setting" }, + ]; + return Promise.resolve(menu); +} diff --git a/.backup/20260915-drop-approval/src/assets/mock/homeworkapi.ts b/.backup/20260915-drop-approval/src/assets/mock/homeworkapi.ts new file mode 100644 index 0000000..5a0c204 --- /dev/null +++ b/.backup/20260915-drop-approval/src/assets/mock/homeworkapi.ts @@ -0,0 +1,1472 @@ +/** + * 课程作业本地 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))); +} diff --git a/.backup/20260915-drop-approval/src/types/homework.ts b/.backup/20260915-drop-approval/src/types/homework.ts new file mode 100644 index 0000000..11bbd93 --- /dev/null +++ b/.backup/20260915-drop-approval/src/types/homework.ts @@ -0,0 +1,575 @@ +/** + * 课程作业前端类型契约(P1-7) + * + * 与后端 VO/DTO 一一对齐: + * - HomeworkVo → 后端 model/homework/HomeworkVo.java(Jackson 默认驼峰序列化) + * - HomeworkDetailVo → 后端 model/homework/HomeworkDetailVo.java(@JsonProperty 保留旧字段名) + * - BasePageResult → 后端 model/BasePageResult.java + * - HomeworkPageResult extends BasePageResult → 后端 model/homework/HomeworkPageResult.java + * - UpFileResultVo → 后端 model/homework/UpFileResultVo.java(@JsonProperty) + * - UpFileVo → 后端 model/homework/UpFileVo.java + * - InitializeRedVo → 后端 model/homework/InitializeRedVo.java(@JsonProperty) + * - HomeworkStatisticsVo → 后端 model/homework/HomeworkStatisticsVo.java + * - HomeworkRequest → 后端 model/homework/HomeworkRequest.java + * - HomeworkQuery → 后端 model/homework/HomeworkQuery.java + BasePageQuery.java + */ + +// ============================ 分页外壳 ============================ + +/** 统一分页响应外壳(对齐 BasePageResult.java) */ +export interface BasePageResult { + total: number; + content: T[]; + pageIndex: number; + pageSize: number; + totalPages: number; + hasNext: boolean; +} + +/** 作业分页结果(对齐 HomeworkPageResult.java,扩展 unreadCount) */ +export interface HomeworkPageResult extends BasePageResult { + /** 当前用户未读作业总数 */ + unreadCount: number; +} + +// ============================ 作业列表项 ============================ + +/** 作业视图对象(对齐 HomeworkVo.java,Jackson 默认驼峰序列化) */ +export interface HomeworkVo { + id: number; + title: string; + content: string; + type: number; + typeDesc: string; + publisherAccount: string; + publisherName: string; + deptId: string; + deptName: string; + status: number; + statusDesc: string; + topmost: number; + /** 已读人数 */ + readCount: number; + /** 应读人数 */ + allowCount: number; + /** 当前用户是否已读 */ + read: boolean; + /** 是否撤回:0 否 / 1 是 */ + isPulled: number; + /** 定时发送时间 */ + timingSendTime: string | null; + /** 父作业ID(转发时关联原作业) */ + parentId: number | null; + /** 短信发送标记 */ + smsFlag: number; + /** 接收对象类型:class=班级(学生场景) */ + receiveType?: string; + /** 接收列表:"all" 或 班级 id 逗号串 */ + receiveList: string | null; + /** ===== 课程作业 v1(教师核心闭环)===== */ + /** 学科 */ + subject?: string; + /** 截止时间 */ + deadline?: string | null; + /** 提交方式:text/image/attach/none */ + submitType?: string[]; + /** 作业流程状态:draft 草稿 / collecting 收集中 / closed 已截止 / done 已完成 */ + hwStatus?: HomeworkStatus; + hwStatusDesc?: string; + /** 提交/批改进度(草稿或仅查阅为 null) */ + progress?: HomeworkProgress | null; + /** 是否启用审批:0 免审直接发布 / 1 需审批后发布 */ + approvalEnabled: number; + /** 审批状态:none 免审 / pending 待审批 / approved 已通过 / rejected 已驳回 */ + approvalStatus: ApprovalStatus; + approvalStatusDesc: string; + /** 当前待审步骤下标(从 0 起) */ + approvalLevel?: number; + /** 当前用户是否为该单当前步骤的待签审批人 */ + canApprove?: boolean; + /** 审批意见 */ + approvalRemark?: string | null; + /** 审批人 */ + approvalBy?: string | null; + /** 审批时间 */ + approvalTime?: string | null; + createBy: string | null; + createTime: string | null; + updateBy: string | null; + updateTime: string | null; +} + +/** 审批状态枚举 */ +export type ApprovalStatus = "none" | "pending" | "approved" | "rejected"; + +// ============================ 作业详情 ============================ + +/** 作业详情视图对象(对齐 HomeworkDetailVo.java,@JsonProperty 保留旧字段名) */ +export interface HomeworkDetailVo { + serid: number; + title: string; + /** 作业内容(旧字段名 Contents) */ + Contents: string; + /** 发布部门ID(旧字段名 DeptID) */ + DeptID: string; + /** 发布部门名称(旧字段名 DeptName) */ + DeptName: string; + /** 发布人姓名(旧字段名 TeaName) */ + TeaName: string; + /** 发布时间(旧字段名 EditDate,已格式化为字符串) */ + EditDate: string; + /** 应读人数 */ + numcount: number; + /** 已读人数 */ + readnumcount: number; + /** 接收人列表 */ + ReceiveList: string | null; + /** 定时发送时间(旧字段名 DsfsTime,已格式化) */ + DsfsTime: string | null; + /** 是否定时发送 */ + isDsfs: boolean; + /** ===== 课程作业 v1 ===== */ + subject?: string; + deadline?: string | null; + submitType?: string[]; + hwStatus?: HomeworkStatus; + hwStatusDesc?: string; + progress?: HomeworkProgress | null; + /** 附件列表(旧字段名 ChaoSend) */ + ChaoSend: HomeworkDetailFileItem[]; + /** 接收对象列表:"all" 或 班级 id 数组 */ + tlist: string | string[]; + /** 接收对象类型:class=班级 */ + receiveType?: string; + /** 接收对象可读文本(全部班级 / 班级名串) */ + receiveText?: string; + /** 是否需审批 */ + approvalEnabled?: number; + /** 审批状态 */ + approvalStatus?: ApprovalStatus; + /** 审批状态文本 */ + approvalStatusDesc?: string; + /** 审批意见 */ + approvalRemark?: string | null; + /** 审批人 */ + approvalBy?: string | null; + /** 审批时间 */ + approvalTime?: string | null; + /** 当前待审步骤下标(从 0 起) */ + approvalLevel?: number; + /** 多步审批流进度(也用于发布/编辑回显审批人配置) */ + approvalFlow?: { + name: string; + vote: "all" | "any"; + /** 审批人账号(编辑回显用) */ + approvers?: string[]; + /** 审批人姓名(展示用) */ + approverNames?: string[]; + status: "approved" | "pending" | "rejected" | "waiting"; + records: { account: string; name: string; result: "approved" | "rejected"; time: string; remark: string }[]; + /** 当前步待签人姓名 */ + pending: string[]; + }[]; + /** 当前用户是否为该单当前步骤的待签审批人 */ + canApprove?: boolean; +} + +/** 作业详情附件项(对齐 HomeworkDetailVo.FileItem) */ +export interface HomeworkDetailFileItem { + id: number; + name: string; + size: number; + fileid: number; + filename: string; +} + +// ============================ 文件上传 ============================ + +/** 文件上传结果(对齐 UpFileResultVo.java,@JsonProperty) */ +export interface UpFileResultVo { + fileid: number; + name: string; + fileurl: string; + host: string; + type: string; + size: number; +} + +/** 附件视图对象(对齐 UpFileVo.java) */ +export interface UpFileVo { + id: number; + fileTrueName: string; + fileName: string; + fileSize: number; + fileType: string; + topicId: number; + topicType: string; +} + +// ============================ 一键已读 ============================ + +/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */ +export interface InitializeRedVo { + count: number; +} + +// ============================ 上一篇/下一篇 ============================ + +/** 上一篇/下一篇作业ID */ +export interface NextPrevVo { + prevId: number; + nextId: number; +} + +// ============================ 阅读情况 ============================ + +/** 阅读情况分组项(后端 Map 返回,字段名固定) */ +export interface ReadGroup { + GroupName: string; + teaslist: ReadGroupUser[]; +} + +/** 阅读情况用户项(后端 Map 返回) */ +export interface ReadGroupUser { + account: string; + teaname: string; + states: 'read' | 'unread'; +} + +// ============================ 统计 ============================ + +/** 作业统计视图(对齐 HomeworkStatisticsVo.java) */ +export interface HomeworkStatisticsVo { + total: number; + publishedCount: number; + totalReadRecords: number; + unreadForCurrentUser: number; + byPublisher: { publisherName: string; count: number }[]; + byMonth: { year: number; month: number; count: number }[]; + // ===== 课程作业口径(v1,可选) ===== + /** 我布置的作业数 */ + myAssignCount?: number; + /** 待批改总量 */ + pendingGrade?: number; + /** 平均提交率(%) */ + avgSubmitRate?: number; + /** 按作业进度 */ + byHomework?: { id: number; title: string; subject: string; hwStatus: HomeworkStatus } & HomeworkProgress[]; + /** 分数分布 */ + scoreDist?: { label: string; key: string; count: number }[]; +} + +// ============================ 课程作业 v1:提交 / 批改 ============================ + +/** 作业流程状态 */ +export type HomeworkStatus = "draft" | "collecting" | "closed" | "done"; + +/** 单个提交状态 */ +export type SubmitStatus = "unsubmitted" | "submitted" | "graded" | "needRedo" | "redone"; + +/** 提交/批改进度 */ +export interface HomeworkProgress { + /** 应交人数 */ + should: number; + /** 已交人数 */ + submitted: number; + /** 已批改人数 */ + graded: number; + /** 待批改人数 */ + pendingGrade: number; + /** 待订正人数 */ + needRedo: number; + /** 已订正人数 */ + redone: number; + /** 未交人数 */ + unsubmitted: number; + /** 提交率(%) */ + submitRate: number; + /** 批改率(%) */ + gradeRate: number; +} + +/** 提交名单项(Submissions 接口返回,精简) */ +export interface SubmissionBrief { + subId: number; + studentId: number; + studentName: string; + submitStatus: SubmitStatus; + submittedAt: string | null; + hasGrade: boolean; + score: number | null; + level: string | null; + isExcellent: boolean; +} + +/** 提交名单按班级分组 */ +export interface SubmissionGroup { + className: string; + students: SubmissionBrief[]; +} + +/** 某作业提交情况结果 */ +export interface SubmissionsResultVo { + homeworkId: number; + title: string; + subject: string; + deadline: string | null; + hwStatus: HomeworkStatus; + hwStatusDesc: string; + summary: HomeworkProgress; + groups: SubmissionGroup[]; +} + +/** 批改结果 */ +export interface GradeResult { + score: number | null; + level: string | null; + comment: string; + isExcellent: boolean; + markedImages: string[]; +} + +/** 单份提交详情(批改工作台用) */ +export interface SubmissionDetailVo { + subId: number; + homeworkId: number; + homeworkTitle: string; + subject: string; + studentId: number; + studentName: string; + classId: string; + className: string; + submitStatus: SubmitStatus; + submittedAt: string | null; + text: string; + images: string[]; + attachments: string[]; + grade: GradeResult | null; +} + +/** 批改入参 */ +export interface GradeRequest { + subId: number; + /** grade 保存批改 / redo 要求订正 / excellent 标为优秀 */ + action?: "grade" | "redo" | "excellent"; + score?: number | string; + level?: string; + comment?: string; + isExcellent?: boolean; + markedImages?: string[]; +} + +// ============================ 请求入参 ============================ + +/** 作业发布/编辑入参(对齐 HomeworkRequest.java) */ +export interface HomeworkRequest { + title: string; + content?: string; + /** 学科 */ + subject?: string; + /** 截止时间 yyyy-MM-dd HH:mm:ss */ + deadline?: string; + /** 提交方式:text/image/attach/none */ + submitType?: string[] | string; + type?: number; + deptId?: string; + /** 是否允许逾期补交 */ + allowLate?: boolean; + /** 截止提醒:如 ["1d","1h"] */ + remindBefore?: string[]; + /** 存草稿:true 则不发布 */ + draft?: boolean; + deptName?: string; + topmost?: number; + /** 接收对象:"all" 或 班级 id 逗号串(receiveType=class) */ + receiveList?: string; + /** 接收对象类型:class=班级 */ + receiveType?: string; + /** 附件文件ID列表(逗号分隔串) */ + fileIds?: string; + timingSendTime?: string; + /** 短信发送标记 */ + smsFlag?: number; + /** 审批人账号列表(单步:选人即送审;空=不送审直接发布) */ + approvers?: string[]; + /** 单步表决:any=或签(默认)/ all=会签 */ + vote?: "all" | "any"; + /** 多步审批流(高级):[{name,vote,approvers}],传了则忽略 approvers/vote */ + flow?: { name: string; vote: "all" | "any"; approvers: string[] }[]; + /** 转发源作业ID */ + sourceId?: number; +} + +/** 作业列表查询参数(对齐 HomeworkQuery.java + BasePageQuery.java) */ +export interface HomeworkQuery { + pageIndex?: number; + pageSize?: number; + keyword?: string; + status?: number; + type?: number; + /** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */ + scope?: string; + deptId?: string; + /** 学科筛选 */ + subject?: string; + /** 作业流程状态筛选:draft/collecting/closed/done,缺省或 all=全部 */ + hwStatus?: string; + publisherAccount?: string; + /** 开始时间(yyyy-MM-dd 短日期) */ + startTime?: string; + /** 结束时间(yyyy-MM-dd 短日期) */ + endTime?: string; + /** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */ + readType?: number; + /** 排序:1 时间倒序 / 2 时间正序 */ + orderByTime?: string; +} + +// ============================ 字典/联系组 ============================ + +/** 系统字典下拉项 */ +export interface DictOption { + k: number | string; + v: string | null; + [key: string]: any; +} + +/** 发文部门管理列表项 */ +export interface DeptManageItem { + SerID: number; + CodeID: string; + Name: string; + Enable: number; +} + +/** 联系组列表项 */ +export interface ContactGroupItem { + serid: number; + GroupName: string; +} + +/** 联系组分页结果 */ +export interface ContactGroupPage { + list: ContactGroupItem[]; + total: number; +} + +/** 联系组用户项 */ +export interface ContactUser { + account: string; + name: string; + userId?: number; + id?: number; +} + +/** 联系组用户分页结果 */ +export interface ContactUserPage { + list: ContactUser[]; + total: number; +} + +/** 组织树节点(基础平台返回) */ +export interface SchoolTreeNode { + [key: string]: any; +} + +// ============================ 班级 / 审批 ============================ + +/** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */ +export interface ClassTreeNode { + id: number; + name: string; + isParent: boolean; + studentCount?: number; + children?: ClassTreeNode[]; + [key: string]: any; +} + +/** 审批列表查询参数 */ +export interface ApprovalQuery { + pageIndex?: number; + pageSize?: number; + /** pending / approved / rejected / all(缺省 pending) */ + status?: string; + keyword?: string; + /** "user" 只看我提交的审批 */ + scope?: string; +} + +/** 审批操作入参 */ +export interface ApprovalAction { + serid: number; + /** 驳回意见 */ + remark?: string; +} + +// ============================ 审批人 ============================ + +/** 审批人 */ +export interface ApproverVo { + id: number; + account: string; + name: string; + deptId: string; + deptName: string; + enabled: number; +} + +// ============================ v2 学生端 ============================ + +/** 当前视角角色(家长即登录学生账号,不单列) */ +export type ViewerRole = "teacher" | "student"; + +/** 学生对某作业的提交/批改态 */ +export interface StudentSubmissionInfo { + subId: number; + submitStatus: SubmitStatus; + submittedAt: string | null; + text: string; + score: number | null; + level: string | null; + comment: string; + isExcellent: boolean; + images?: string[]; + attachments?: string[]; + grade?: GradeResult | null; +} + +/** 学生视角作业项 */ +export interface StudentHomeworkVo { + id: number; + title: string; + subject: string; + publisherName: string; + deptName: string; + deadline: string | null; + createTime: string | null; + content: string; + submitType: string[]; + viewOnly: boolean; + hwStatus?: HomeworkStatus; + allowLate?: boolean; + my: StudentSubmissionInfo | null; +} + +/** 今日/我的作业列表 */ +export interface MyHomeworksVo { + buckets: { todo: number; submitted: number; graded: number; redo: number; all: number }; + list: StudentHomeworkVo[]; +} + +/** 我的某作业详情(作业信息 + 我的提交/批改) */ +export interface MySubmissionVo { + homework: { id: number; title: string; subject: string; content: string; deadline: string | null; submitType: string[]; publisherName: string; deptName: string }; + my: StudentSubmissionInfo | null; +} + +/** 学生提交作业入参 */ +export interface SubmitRequest { + serid: number; + text?: string; + /** 图片 URL / dataURL 列表(逗号串或数组) */ + images?: string[] | string; + /** 附件 id 列表(逗号串或数组) */ + attachments?: string[] | string; +} diff --git a/.backup/20260915-drop-approval/src/utils/menuRoute.ts b/.backup/20260915-drop-approval/src/utils/menuRoute.ts new file mode 100644 index 0000000..20fa4fb --- /dev/null +++ b/.backup/20260915-drop-approval/src/utils/menuRoute.ts @@ -0,0 +1,163 @@ +import routes from "@/router/routes"; + +/** + * 平台模块配置 → 前端内部路由的统一解析器。 + * + * 基础平台里配置的模块 path 格式不可控(可能带 /homework 旧前缀、带 + * index.html#/hash、是应用入口地址甚至完整 URL、或干脆配错),直接 + * router.push(item.page) 匹配不到就会白屏或被重定向兜到默认页。所有消费 + * 模块 page 的地方(侧边栏跳转/高亮、登录后默认跳转)必须先经过这里。 + * + * 解析顺序:路径精确匹配 → 模块名匹配 → 外链判定 → modId 契约匹配。 + * 全部落空时返回空结果并 console.warn,绝不返回未注册的原始路径—— + * 把它 push 给路由器会被 / 重定向吞掉,表象就是"点了菜单跳到默认页"。 + */ + +/** 路由 path 集合:重定向路由穿透到目标 path,保证解析结果可直接用于高亮对照 */ +const ROUTE_PATHS = routes + .map((r) => (typeof r.redirect === "string" ? r.redirect : r.path)) + .filter((p): p is string => !!p && !p.includes(":")); + +/** modId → 路由 path(取该 modId 的第一个非重定向路由) */ +const ROUTE_BY_MODID = new Map(); +for (const r of routes) { + const modId = (r.meta as any)?.modId as number | undefined; + if (!modId || !r.path || r.path.includes(":") || r.redirect) continue; + if (!ROUTE_BY_MODID.has(modId)) ROUTE_BY_MODID.set(modId, r.path); +} + +/** 标题 → 路由 path(重复标题取先注册的,如 发作业 → /list/new) */ +const ROUTE_BY_TITLE = new Map(); +for (const r of routes) { + if (r.redirect || !r.path || r.path.includes(":")) continue; + const title = (r.meta as any)?.title as string | undefined; + if (title && !ROUTE_BY_TITLE.has(title)) ROUTE_BY_TITLE.set(title, r.path); +} + +/** 平台历史模块名 → 路由(旧菜单叫法与现页面标题不一致的对照) */ +const NAME_ALIASES: Record = { + 作业发送: "/list/new", + 发作业: "/list/new", + 已发作业: "/list", + 已收作业: "/accept", + 作业列表: "/homeworklist", + 统计分析: "/stats", +}; + +const EXTERNAL_RE = /^(https?:)?\/\//i; + +/** 从平台原始配置里提取内部路由候选串(去掉 hash、query、尾斜杠) */ +function extractCandidate(raw: string): string { + let p = raw.trim(); + try { + p = decodeURIComponent(p); + } catch { + /* 含非法编码时保持原样 */ + } + // 带 hash 的地址(.../index.html#/setting):取 # 后的 SPA 路由 + const hashIdx = p.indexOf("#"); + if (hashIdx >= 0) { + p = p.slice(hashIdx + 1); + } else if (EXTERNAL_RE.test(p)) { + // 无 hash 的完整 URL:取 pathname(如 https://host/setting) + try { + p = new URL(p, "http://placeholder.local").pathname; + } catch { + /* 保持原样 */ + } + } + const qIdx = p.indexOf("?"); + if (qIdx >= 0) p = p.slice(0, qIdx); + // 入口文件名去掉后即可命中根重定向路由 + p = p.replace(/index\.html?$/i, ""); + p = p.replace(/\/+$/, ""); + if (p && !p.startsWith("/")) p = "/" + p; + return p; +} + +/** 候选串 → 已注册路由 path;未命中返回 null */ +function matchRoute(candidate: string): string | null { + if (!candidate) return null; + const lower = candidate.toLowerCase(); + // 1. 精确匹配(含重定向穿透后的目标路径) + const exact = ROUTE_PATHS.find((p) => p.toLowerCase() === lower); + if (exact) return exact; + // 2. 兼容旧 /homework 前缀(/homework/setting → /setting)。 + // 不做通用后缀匹配,避免 /stats/OA/Homework 这类外部路径被误匹配 + if (lower.startsWith("/homework/")) { + const stripped = lower.slice("/homework".length); + return ROUTE_PATHS.find((p) => p.toLowerCase() === stripped) ?? null; + } + return null; +} + +/** 按模块名解析路由:精确标题 → 历史别名 → 双向包含(命中多个则放弃) */ +function matchByName(name?: string | null): string | null { + const n = String(name ?? "").trim(); + if (!n) return null; + const byTitle = ROUTE_BY_TITLE.get(n); + if (byTitle) return byTitle; + const alias = NAME_ALIASES[n]; + if (alias) return alias; + const hits = [...ROUTE_BY_TITLE.entries()].filter( + ([title]) => title.length >= 2 && (n.includes(title) || title.includes(n)) + ); + return hits.length === 1 ? hits[0][1] : null; +} + +/** 是否为本应用入口类地址(/、/homework、…/index.html):只表示"打开应用", + * 不携带页面信息,不能当作有效跳转目标 */ +function isAppEntryCandidate(candidate: string): boolean { + return !candidate || candidate === "/" || candidate === "/homework"; +} + +export interface ResolvedMenuPage { + /** 内部路由 path;解析不出时为空串 */ + path: string; + /** 外部链接(完整 URL 且不属于本应用路由),需整页跳转而非 router.push */ + external: boolean; +} + +/** 空配置的统一返回值 */ +export const EMPTY_MENU_PAGE: ResolvedMenuPage = { path: "", external: false }; + +export function resolveMenuPage( + page?: string | null, + modId?: number | null, + name?: string | null +): ResolvedMenuPage { + const raw = String(page ?? "").trim(); + const byName = (): string | null => matchByName(name); + const byModId = (): string | null => (modId != null ? ROUTE_BY_MODID.get(modId) ?? null : null); + + if (raw) { + const candidate = extractCandidate(raw); + const byPath = matchRoute(candidate); + if (byPath) return { path: byPath, external: false }; + + // 完整 URL:指向本应用入口的不算有效配置(继续用名字/modId 识别), + // 其余按外部链接整页打开 + if (EXTERNAL_RE.test(raw) && !raw.includes("#")) { + if (!isAppEntryCandidate(candidate)) { + return { path: raw, external: true }; + } + } + + const fallback = byName() ?? byModId(); + if (fallback) return { path: fallback, external: false }; + + console.warn( + "[menuRoute] 模块路径/名称/modId 均无法匹配前端路由,请检查基础平台模块配置:", + raw, + "modId =", modId, + "name =", name + ); + return EMPTY_MENU_PAGE; + } + + // 未配置 path:按模块名 → modId 契约解析 + const fallback = byName() ?? byModId(); + if (fallback) return { path: fallback, external: false }; + console.warn("[menuRoute] 模块未配置路径且名称/modId 无法识别:", name, "modId =", modId); + return EMPTY_MENU_PAGE; +} diff --git a/src/views/homework/ApprovalList.vue b/.backup/20260915-drop-approval/src/views/homework/ApprovalList.vue similarity index 100% rename from src/views/homework/ApprovalList.vue rename to .backup/20260915-drop-approval/src/views/homework/ApprovalList.vue diff --git a/src/views/homework/ApprovalSetting.vue b/.backup/20260915-drop-approval/src/views/homework/ApprovalSetting.vue similarity index 100% rename from src/views/homework/ApprovalSetting.vue rename to .backup/20260915-drop-approval/src/views/homework/ApprovalSetting.vue diff --git a/.env.dev b/.env.dev index 359f483..32ce03b 100644 --- a/.env.dev +++ b/.env.dev @@ -5,7 +5,7 @@ # 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO/基础平台用真实环境 VITE_BASE_HOST=http://base.yunhaixingtong.com VITE_SSO_HOST=http://sso.yunhaixingtong.com -VITE_SERVICE_HOST=http://base.yunhaixingtong.com/campusnotice/ +VITE_SERVICE_HOST=http://base.yunhaixingtong.com/campushomework/ VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd VITE_IS_MOCK=false diff --git a/classwork-frontend-m5-plan.md b/classwork-frontend-m5-plan.md new file mode 100644 index 0000000..76d3a59 --- /dev/null +++ b/classwork-frontend-m5-plan.md @@ -0,0 +1,319 @@ +# 课堂作业前端 M5 切换计划 —— 对齐后端 V2 接口完成页面 + +| 项 | 内容 | +|---|---| +| 项目 | 前端 `E:\YHXT\view\CourseHomework`(Vue3 + Vite + TS + view-ui-plus + alova) | +| 依据 | 后端 `E:\YHXT\java\ClassHomeWork` 已实现接口(M1~M4 完成,V3.2 运行验证通过)+ `classwork-homework-plan-v3.md` §11 前端适配计划 / §15.5 遗留待办第 1 条 | +| 目标 | 前端 9 个核心页面全部切换到 V2 接口面,打通「发布→统计→批阅→学生提交→结果」全链路联调 | +| 后端地址 | 本地 `http://localhost:8102`(vite proxy 已配置,无需改动) | +| 文档版本 | V1.0(2026-09-15) | + +--- + +## 1. 背景与现状差距 + +### 1.1 后端权威接口面(已实现,本计划的对齐目标) + +后端按 PRD §18 完成了 V2 接口(28 个新端点),同时保留 V1 冻结接口(17 个)。统一响应 `{code,msg,data}`,分页 `BasePageResult`,时间格式全局 `yyyy-MM-dd HH:mm:ss`(无 "T" 分隔符)。 + +**教师端 `/api/teacher/homeworks`(19 端点,PRD 页 1~6):** + +| 方法 | 路径 | 用途 | +|---|---|---| +| POST | `` | 创建作业(一律落草稿 DRAFT_UNPUBLISHED) | +| PUT | `/{id}` | 编辑作业(仅草稿两态) | +| DELETE | `/{id}` | 删除作业(仅草稿两态) | +| POST | `/{id}/publish` | 立即发布 | +| POST | `/{id}/schedule` | 定时发布 / 取消定时(`{scheduledAt}` 或 `{cancel:true}`) | +| GET | `/drafts` | 草稿箱分页(`subject`/`hwStatus`/`keyword` 筛选) | +| GET | `/published` | 已发布列表分页(含应交/已读/已交/未读未交聚合) | +| POST | `/{id}/remind` | 提醒未读/未交(`{type: NOT_READ\|NOT_SUBMITTED, studentIds?}`,返回 `{reminded}`) | +| POST | `/{id}/extend` | 延长截止(`{deadlineAt}` 须晚于当前值) | +| POST | `/{id}/copy` | 复制为新草稿,返回新 id | +| POST | `/{id}/archive` | 归档(只读,屏蔽提醒) | +| GET | `/{id}/export` | 导出学生矩阵 Excel(文件流) | +| GET | `/{id}` | 作业详情 | +| GET | `/{id}/stats` | 汇总统计卡片(总进度 + 分班进度) | +| GET | `/{id}/students` | 学生矩阵分页(`classId`/`submitStatus`/`reviewStatus`/`pendingGrade` 筛选) | +| POST | `/{id}/submissions/{studentId}/review` | 保存批阅(`{score?, comment?, markedImages?}`,score/comment 至少一项) | +| POST | `/{id}/submissions/{studentId}/return` | 打回(`{comment}` 必填) | +| GET | `/{id}/submissions/{studentId}/versions` | 历史提交版本列表 | +| GET | `/classes` | 年级→班级树(叶子 id=classId,含 studentCount) | +| POST | `/classes/sync` | 手动触发名册同步 | + +**学生端 `/api/student/homeworks`(6 端点,PRD 学生页 1~3):** + +| 方法 | 路径 | 用途 | +|---|---|---| +| GET | `` | 我的作业列表(`tab=all/todo/done/closed` + 分页/keyword) | +| GET | `/{id}` | 作业详情(含 description/attachments/myAttachments/myText) | +| POST | `/{id}/read` | 阅读上报(前端停留 ≥3 秒后调用,幂等) | +| POST | `/{id}/submissions` | 提交作业(`{text?, attachments[]}`),返回 `{submissionId}` | +| PUT | `/{id}/submissions/{submissionId}` | 撤回/打回重交(`{action: withdraw\|resubmit, text?, attachments?}`) | +| GET | `/{id}/result` | 批阅结果(当前版本 + 历史版本含各版评语) | + +**通知 `/api/notifications`(3 端点)**:GET 分页(含未读数)、`POST /{id}/read`、`POST /read-all`。 + +**辅助域(已存在,继续沿用)**:文件 `/api/file/Upload|UploadImage|Down|Preview|Delete|List|Bind`;字典 `/api/dict/List|ManageList|Add|Update|Delete|Sort|Sync`;联系组 `/api/contacts/*`;`/api/base/GetUserInfo|GetUserMod|GetSchoolAll|GetGroupUser|FileUpload|Logout`。 + +**V1 冻结接口 `/api/homework/*`(17 个)**:继续服务未切换页面(如「已收到」收件箱、「全部作业」管理端、「作业统计」),M5 完成后评估退役。 + +### 1.2 前端现状盘点与技术债 + +页面与数据源核对结果(`homeworkApi.*` 实际调用 vs 后端真伪): + +| 页面 | 路由 | 现状数据源 | 判定 | +|---|---|---|---| +| NewHomework.vue | /list/new | 旧 Create/Edit/GetDetail | 真实接口,需切 V2 | +| HomeworkList.vue | /list、/homeworklist | 旧 SendList + **remind(伪)** | 部分伪接口 | +| HomeworkDetails.vue | /list/detail | 旧 Detail/GetNextPrev/ReadDetail | 真实接口,需切 V2 | +| Submissions.vue | /submissions | **Submissions/SubmissionDetail/grade(伪)** | 全部伪接口 | +| Grade.vue | /grade | **sendList + Submissions/SubmissionDetail/grade(伪)** | 部分伪接口 | +| StudentHomeworks.vue | /stu/today、/stu/redo、/par/kids | **MyHomeworks/parentNudge(伪)** | 全部伪接口 | +| HomeworkAccept.vue | /accept | 旧 List/Detail(v1 收件箱) | 真实接口,需改造为学生详情页 | +| HomeworkStats.vue | /stats | 旧 Statis | 真实接口,保留 | +| HomeworkSetting.vue | /setting | dict/contacts | 真实接口,保留 | +| ApprovalList.vue | /approval | **approvalList/approve/reject(伪)** | 后端无审批域 | +| ApprovalSetting.vue | /approval-setting | **approvers 系(伪)** | 后端无审批域 | + +**「伪接口」清单**(后端不存在,真实环境必 404,仅 mock 模式可跑):`/api/homework/` 下的 `Submissions、SubmissionDetail、Grade、Remind、MyHomeworks、MySubmission、Submit、ParentNudge、ApprovalList、ApprovalCount、Approve、Reject`;`/api/dict/` 下的 `ApproverPool、Approvers、AddApprover、UpdateApprover、DelApprover`;`/api/base/SetRole`。 + +**类型契约差距**:`types/homework.ts` 为 V1 契约(hwStatus 四态 `draft/collecting/closed/done`、submitStatus 小写五值);V2 要求 hwStatus 五态 `DRAFT_UNPUBLISHED/DRAFT_SCHEDULED/PUBLISHED/CLOSED/ARCHIVED`、submitStatus 五态 `NOT_SUBMITTED/SUBMITTED/LATE_SUBMITTED/RETURNED/REVIEWED`、reviewStatus 三态 `PENDING/REVIEWED/RETURNED`。 + +**工作区未提交改动**:Grade/StudentHomeworks/Submissions 三页的图片预览/上传优化 + mock/env 配置微调(与本次切换方向一致,保留并在其上继续)。 + +## 2. 总体方案 + +三层推进:**契约层(类型+API 客户端)→ 教师端页面 → 学生端页面**,最后清理菜单路由与 mock。旧 V1 页面(已收到/全部作业/统计/设置)不动,继续走冻结接口。 + +### 2.1 契约层(P0) + +**新增 `src/types/homeworkV2.ts`**(V2 契约独立成文件,不污染 V1 类型;与后端 `model/v2/*.java` 一一对齐): + +```ts +/** 作业五态(PRD §17) */ +export type HwStatusV2 = "DRAFT_UNPUBLISHED" | "DRAFT_SCHEDULED" | "PUBLISHED" | "CLOSED" | "ARCHIVED"; +/** 提交五态 */ +export type SubmitStatusV2 = "NOT_SUBMITTED" | "SUBMITTED" | "LATE_SUBMITTED" | "RETURNED" | "REVIEWED"; +/** 批阅三态 */ +export type ReviewStatus = "PENDING" | "REVIEWED" | "RETURNED"; + +export interface AttachmentItem { fileId?: number; fileUrl: string; fileType?: "image" | "file"; fileName?: string; sort?: number; } +export interface ClassItem { classId: string; className: string; } +export interface ProgressVo { should: number; read: number; submitted: number; graded: number; pendingGrade: number; unsubmitted: number; unread: number; unreadUndone: number; readRate: number; submitRate: number; gradeRate: number; } +export interface HomeworkV2Vo { id; title; subject; description; hwStatus: HwStatusV2; hwStatusDesc; scheduledAt; publishedAt; deadlineAt; archivedAt; submitType: string[]; allowLate; remind1d; remind1h; classes: ClassItem[]; attachments: AttachmentItem[]; publisherId; publisherName; createTime; updateTime; progress?: ProgressVo | null; } +export interface HomeworkV2Request { title: string; subject?; description?; classIds: string[]; attachments?: AttachmentItem[]; deadlineAt?; submitType: string[]; allowLate?; remind1d?; remind1h?; } +export interface TeacherHomeworkQuery { pageIndex?; pageSize?; keyword?; subject?; hwStatus?; } +export interface StudentMatrixVo / StudentMatrixQuery / HomeworkStatsVo / ClassTreeNodeV2(id: string)/ SubmissionVersionVo / StudentHomeworkV2Vo(含 MySubmission)/ StudentHomeworkDetailV2Vo / StudentSubmitRequest / StudentResultVo / NotificationVo … +``` + +**新增 3 个 API 客户端**(风格沿 `api/module/homework.ts` 的 `get/postJSON` 封装): + +- `src/api/module/teacherHomework.ts` → `teacherApi`:19 端点全覆盖 + `exportUrl(id)`(Excel 直链,浏览器跳转下载) +- `src/api/module/studentHomework.ts` → `studentApi`:6 端点 +- `src/api/module/notification.ts` → `notificationApi`:3 端点 + +**修正 `api/module/homework.ts`**:`baseApi.setRole` 标注仅 mock 可用(后端无此端点,真实环境学生身份由 CAS 登录账号 ↔ class_student 映射,联调用种子账号 stu01~stu06);`baseApi.getClassTree` 的调用方改走 `teacherApi.classTree()`。 + +### 2.2 页面改造映射(P1~P3) + +| PRD 页面 | 前端页面 | 目标接口 | 关键改造点 | +|---|---|---|---| +| 页 1 发布作业 | NewHomework.vue | POST/PUT + /classes + publish/schedule + file/Upload | ① 表单重构:classIds(年级→班级树多选,数据源换 `/teacher/homeworks/classes`)② 新增:截止时间(精确到分钟,空=仅查阅型)、submitType(图片/文件/两者/文本)、allowLate、remind1d/remind1h、定时发布时间 ③ 保存语义拆分:「存草稿」(POST/PUT)与「立即发布」(POST 后调 publish)/「定时发布」(POST 后调 schedule)④ 附件走 file/Upload,组装 AttachmentItem[] ⑤ 编辑回显改 GET /teacher/homeworks/{id}(旧 GetDetail 保留给转发场景) | +| 页 2 草稿箱 | HomeworkList.vue(/list 增加「草稿箱」段) | GET /drafts | 草稿两态(未发布/定时待发)标签展示;操作:编辑、发布、取消定时(schedule cancel)、删除 | +| 页 3 已发布列表 | HomeworkList.vue(/list 主段) | GET /published + remind/extend/copy/archive/export | ① 统计列:应交/已读/已交/**未读未交高亮** + 阅读率/提交率(progress)② 状态筛选 PUBLISHED/CLOSED/ARCHIVED ③ 操作组:提醒(NOT_READ/NOT_SUBMITTED,返回 reminded 提示)、延长截止(日期选择,须晚于当前)、复制为新草稿、归档、导出 Excel(直链)④ 行点击进详情 | +| 页 4 详情/统计 | HomeworkDetails.vue | GET /{id} + GET /{id}/stats | 作业信息卡(五态标签、学科、截止、提交方式、班级、附件下载)+ 统计卡片(总进度 + 分班进度:阅读率/提交率/批阅率、待批阅数) | +| 页 5 学生矩阵 | Submissions.vue | GET /{id}/students | ① 列:学生/班级/阅读状态(含首末阅读时间)/提交状态(含逾期角标)/提交时间/评分/批阅状态/附件缩略图 ② 筛选:classId、submitStatus、reviewStatus、只看待批阅(pendingGrade)③ 未交学生占位行(submissionId=0)④ 行操作:批阅、打回、提醒单个学生(remind 定向) | +| 页 6 批阅/打回/版本 | Grade.vue(批改中心) | /published(pendingGrade 作业列表) + versions + review/return | ① 待批作业列表改 GET /published(按 progress.pendingGrade 排序/展示)② 选中学生(矩阵 pendingGrade=true)→ 右侧工作台 ③ 版本切换(GET versions,默认最新)④ 批阅保存(score+comment 至少一项)⑤ 打回(comment 必填,二次确认)⑥ 提交内容展示:text + 附件/图片(复用已有缩略图放大预览) | +| 学生页 1 列表 | StudentHomeworks.vue | GET /student/homeworks?tab= | ① tab 改 all/todo/done/closed(对齐 meta.initTab=redo→todo 语义调整)② 倒计时(deadlineAt)③ my 提交状态徽标(五态 + 逾期角标)④ 分页 + keyword | +| 学生页 2 详情/结果 | HomeworkAccept.vue(改造为学生详情页,挂 /stu/detail?serid=) | GET /{id} + read + result | ① 作业说明富文本 + 附件预览/下载 ② **进入页 3 秒定时器调 read(onUnmounted 清理)** ③ 我的提交区(当前版本 text/附件/评分/评语)④ 历史版本 + 各版评语(GET result)⑤ 提交/撤回/重交入口 | +| 学生页 3 提交 | HomeworkAccept.vue 内提交区(+列表页快捷弹窗保留) | POST submissions + PUT withdraw/resubmit | ① 按 submitType 动态渲染:文本框 / 图片上传(file/UploadImage 产物→AttachmentItem)/文件上传 ② 截止校验:截止后 allowLate=0 禁交 ③ 撤回(截止前)→ 重新填写重交 ④ 打回(RETURNED)→ 显示订正原因 + 重交 | + +**不动页面**:HomeworkStats(旧 Statis)、HomeworkSetting(dict/contacts)、/accept 的 v1 收件箱语义由 HomeworkAccept 改造承接(路由 `/accept` 重定向到 `/stu/today`)。 + +### 2.3 路由与菜单调整(P3) + +| 项 | 处置 | 理由 | +|---|---|---| +| 新增 `/stu/detail`(HomeworkAccept 改造后) | meta `{ role: "student", title: "作业详情", modId: 33 }` | PRD 学生页 2/3 载体 | +| `/accept` | 重定向 `/stu/today` | v1 收件箱语义被学生列表替代 | +| `/approval`、`/approval-setting` + 菜单 modId 5/6 | **路由与菜单摘除**(页面文件保留不引) | 后端无审批域(plan-v3 §4.6 顺延),伪接口联调必挂 | +| `/par/kids` 家长端 | 保留路由但隐藏入口 | 后端无家长接口(plan-v3 §4.6 顺延),parentNudge 为伪接口;页面内调用加守卫 | +| `/homeworklist` 全部作业、`/stats` 统计、`/setting` 设置 | 保留走 V1 冻结接口 | 冻结接口可用,M5 后评估退役 | +| 通知中心(可选) | 视排期加 `/notify` 轻量页(notificationApi 3 端点,未读角标) | PRD §18 未列页面,plan-v3 §7.3 补充接口;列为可选项 | + +### 2.4 mock 同步(P4) + +`src/assets/mock/homeworkapi.ts` 增补 V2 端点 mock(teacher 19 + student 6 + notifications 3),数据模型对齐 `types/homeworkV2.ts`,保证 `npm run dev:mock` 全页面可演示;同时清理伪接口 mock(Submissions/Grade/MyHomeworks/审批系),避免继续误导联调。 + +## 3. 实施顺序与验收 + +| 阶段 | 内容 | 验收标准 | +|---|---|---| +| P0 契约层 | types/homeworkV2.ts + teacherHomework/studentHomework/notification 三客户端 + homework.ts 修正 | `vue-tsc` 通过;接口路径/参数与本文 §1.1 表逐项一致 | +| P1 教师端发布链路 | NewHomework + HomeworkList(草稿箱+已发布) | 真实后端(8102):建作业→存草稿→草稿箱可见→立即发布/定时发布→已发布列表出统计聚合 | +| P2 教师端批阅链路 | HomeworkDetails(stats)+ Submissions(矩阵)+ Grade(批阅/打回/版本) | 矩阵筛选/缩略图/占位行正确;批阅→学生端可见结果;打回→重交→版本递增 | +| P3 学生端闭环 | StudentHomeworks + HomeworkAccept + 路由菜单调整 | 用种子账号(stu01~stu06):列表 tab→详情 3 秒已读→提交→倒计时/逾期分流→撤回留版本→打回重交→查结果 | +| P4 清理与全量走查 | mock 更新、审批/家长端摘除、全页面回归 | dev(dev) + dev:mock 双模式走查;PRD §18 九页面映射逐格核对 | + +每阶段一组语义化 commit;P1 起需要后端 8102 运行 + `sql/v3_seed.sql` 名册种子(库名 `class_homework`,utf8mb4)。 + +## 4. 风险与开放问题 + +| # | 事项 | 影响 | 处置建议 | +|---|---|---|---| +| 1 | 学生身份联调:后端无 SetRole,学生接口按 CAS 登录账号 ↔ class_student 映射 | 高(P3 阻塞) | 与后端确认本地登录方案(种子账号 stu01~stu06 走 CAS 或本地放行);mock 模式保留 SetRole 演示用 | +| 2 | 审批域下线需产品确认 | 中 | 本计划先摘菜单/路由不删代码;产品若要保留需后端补接口 | +| 3 | 提交附件契约:v2 只收 file/Upload 产物(fileId/fileUrl),拒绝 dataURL | 中 | HomeworkUpload 组件改造为真实上传后再组装 AttachmentItem;改造前 dataURL 直交必被后端拒 | +| 4 | 学生端入口与菜单结构(/stu/* 不在现有静态菜单) | 低 | 沿用现有约定,学生视角由平台角色决定入口;具体待 GetUserMod 对接方案 | +| 5 | Excel 导出/export 直链需带登录态 cookie | 低 | 同源代理下 `location.href` 直跳即可(与 AllFileDown 同模式) | +| 6 | V1 页面(已收到/全部作业/统计)与新数据并存的两套状态口径 | 低 | 双轨期 hwStatus 派生映射(后端已做),前端不做转换;M5 后评估 V1 退役 | +| 7 | tab=redo(待订正)语义:V2 无独立 redo 桶,RETURNED 归入 todo | 低 | /stu/redo 路由 initTab 改 todo 并在页内强化 RETURNED 过滤,或改路由为 /stu/todo | + +## 5. 交付物清单 + +1. `src/types/homeworkV2.ts` —— V2 全量类型契约 +2. `src/api/module/teacherHomework.ts` / `studentHomework.ts` / `notification.ts` —— 28 端点客户端 +3. 改造后 7 个页面:NewHomework / HomeworkList / HomeworkDetails / Submissions / Grade / StudentHomeworks / HomeworkAccept +4. `src/router/routes.ts` + `src/assets/config/menu.ts` 调整 +5. `src/assets/mock/homeworkapi.ts` V2 增补与伪接口清理 + +--- + +## 6. 审核(审批)功能下线(2026-09-15) + +### 6.1 裁决变化 + +§2.3 与 §4 风险 2 原口径为「摘菜单/路由、页面文件保留不引,等产品确认」。本次确认**彻底删除审批(审核)功能**:产品定位为任课教师直接布置,不存在送审环节;后端 `ClassHomeWork` 从未实现审批域(无 `approval_*` 列、无 `ApprovalList/Approve/Reject`、无审批人池接口),而前端两页调用的 `homeworkApi.approvalList/approve/reject` 与整个 `approvalSettingApi` **在 API 层根本不存在**,属编译期即悬空的死代码。 + +注意边界:本节只删「审批/审核」(approval),**不动「批改/批阅」(grade/review)**——`Grade.vue`、`Submissions.vue`、`ReviewModal.vue` 是作业核心链路。 + +### 6.2 牵连面盘点(grep `approval|审核|审批|approver|canApprove|vote|flow` 全仓) + +| # | 文件 | 现状 | 处置 | +|---|---|---|---| +| 1 | `src/views/homework/ApprovalList.vue` | 审批列表页,未挂路由,调用不存在的 `homeworkApi.approvalList/approve/reject` | 删除(git 跟踪,可 `git checkout` 恢复) | +| 2 | `src/views/homework/ApprovalSetting.vue` | 审批人池设置页,未挂路由,调用不存在的 `approvalSettingApi` | 删除(同上) | +| 3 | `src/components/ApprovalFlowEditor.vue` | 工作区已处于删除态(`git status` 显示 ` D`) | 保持删除,不恢复 | +| 4 | `src/types/homework.ts` | `HomeworkVo` 81-95、`ApprovalStatus` 103、`HomeworkDetailVo` 146-174(含 `approvalFlow`/`canApprove`)、`HomeworkRequest` 393-398(`approvers`/`vote`/`flow`)、`ApprovalQuery` 489、`ApprovalAction` 500、`ApproverVo` 509、分节标题 476 | 全部删除;分节标题改「班级」 | +| 5 | `src/api/module/homework.ts` | 23-24 `import { ApprovalQuery, ApproverVo }`(导入后零使用) | 删两行导入 | +| 6 | `src/assets/config/menu.ts` | 12-13 静态菜单 `id/modId 5 审批列表 /approval`、`6 审核设置 /approval-setting`(指向不存在的路由) | 删两行 | +| 7 | `src/utils/menuRoute.ts` | 无审批别名;但 `resolveMenuPage` 对无法识别的模块只 `console.warn` 并返回 `path:""`,而 `layout.vue:192` 直接渲染 `modList`(不过滤空 path)→ 平台 `GetUserMod` 若仍下发 modId 5/6,侧栏会出现点了没反应的死菜单 | 新增 `DROPPED_MOD_IDS={5,6}` + `dropDroppedModules(list)`(顶层 + children,按 `modId ?? id`,不改入参),`App.vue` 赋值 `layoutData.modList` 前调用 | +| 8 | `src/assets/mock/homeworkapi.ts` | 审批引擎与数据:头注释 7/15-16、审批人池 41-52、`buildFlowFromInput` 62-72、`pendingApprovers` 75-79、`canAct` 81-84、`flowProgressText` 86-92、种子 `approval*` 字段 155-313(7 处)、`seedFlows` 318-343、复位循环 345-353、`isPublished` 639-641、`APPROVAL_DESC` 648、列表映射 682-689、详情映射 728-753、Create 1026-1032、Edit 1059-1092 | 全删;`isPublished` 简化为「非草稿即可见」;种子行不再带 `approval*` 键 | +| 9 | 其余 `src/views`、`src/components`、`src/router`、`App.vue` | grep **零命中** | 无需改动 | + +后端 `E:\YHXT\java\ClassHomeWork`:审批域从未落地,无可删代码;仅需把 `classwork-homework-plan-v4.md` 的 §0 排除说明与 Q-08 由「待确认」改为「已裁决下线」。 + +### 6.3 执行步骤与验收 + +1. 备份:`types/homework.ts`、`api/module/homework.ts`、`assets/mock/homeworkapi.ts`、`assets/config/menu.ts`、`utils/menuRoute.ts`、`App.vue` 快照到 `.backup/20260915-drop-approval/`(这些文件工作区相对 HEAD 已有 M5 未提交改动,`git restore` 会连带丢改动,故单独快照)。 +2. 删 2 个页面文件(`git rm` 语义,历史可查)。 +3. 按 6.2 顺序改 types → api → menu → menuRoute+App → mock(自底向上,先断引用再清数据)。 +4. 验收(实测见 §6.4):① `grep -rniE "approval|approver|canApprove" src` 命中数为 0;② `npm run build` 成功;③ `dev:mock` 下逐端点冒烟正常、审批端点 404、侧栏无审批入口。 + > 原定的 `npx vue-tsc --noEmit` 门禁在本工程**跑不起来**:`vue-tsc@1.8.27` 与 `typescript@5.9.3` 不兼容会直接崩溃,且工程 tsc 基线本就有 65 条存量报错。故类型侧只作「不得新增引用已删符号的报错」的对照检查,见 §6.4 第 4 条。 +5. 结果回填 §6.4。 + +### 6.4 执行结果回填(2026-09-15 完成) + +**改动清单** + +| 文件 | 动作 | +|---|---| +| `src/views/homework/ApprovalList.vue`、`ApprovalSetting.vue` | `git rm`(索引内 `D`,历史可查) | +| `src/components/ApprovalFlowEditor.vue` | 保持工作区已删除状态,未恢复 | +| `src/types/homework.ts` | 删 `HomeworkVo` 8 个 approval 字段、`ApprovalStatus`、`HomeworkDetailVo` 9 个 approval 字段(含 `approvalFlow`/`canApprove`)、`HomeworkRequest` 的 `approvers/vote/flow`、`ApprovalQuery`/`ApprovalAction`/`ApproverVo`;分节标题「班级 / 审批」→「班级」 | +| `src/api/module/homework.ts` | 删 `ApprovalQuery`、`ApproverVo` 两个死导入 | +| `src/assets/config/menu.ts` | 删 modId 5「审批列表」/6「审核设置」两项,头注释记录「平台若仍下发则由 DROPPED_MOD_IDS 剔除」 | +| `src/utils/menuRoute.ts` | 新增 `DROPPED_MOD_IDS={5,6}`、`DROPPED_NAMES`(审批/审核 7 个叫法)、`dropDroppedModules()`(顶层+children,按 `modId ?? id` 或名称命中,返回新数组不改入参) | +| `src/App.vue` | `GetUserMod` 结果入壳层前经 `dropDroppedModules` 过滤(侧栏渲染、默认跳转、console.table 共用同一份) | +| `src/assets/mock/homeworkapi.ts` | 删审批引擎(审批人池 `approvers/approverSeq/approverName/approverPoolAccounts`、`FlowLevel`、`buildFlowFromInput`、`pendingApprovers`、`canAct`、`flowProgressText`、`APPROVAL_DESC`)、`seedFlows()` 与其后的复位 forEach、种子 8 处 approval 属性、`toVo`/详情两处 8 行映射与 `approvalFlow` 块、Create 新对象的 7 行 approval 初始化、Edit 的 flow 重建整段(35 行);`isPublished()` 由「审批通过或免审」改为 `hwStatus !== "draft"`;头注释与存储注释同步改写 | + +**验收实测** + +1. **残留归零**:`grep -rniE "approval|approver|canapprove|flowprogress|buildflow" src` → **0 命中**(中文「审批/审核」仅存于注释与 `DROPPED_NAMES` 名单,属有意保留)。 +2. **构建通过**:`npm run build`(`vite build --mode base`)**EXIT=0**,18.82s。 +3. **mock 冒烟**(`npm run dev:mock` @8077,node fetch 逐端点,全部 `code==0`): + - `List` 5/7 条、`SendList?scope=user` 3/3 条、`Detail?serid=1&type=0` 出参键仍为 `serid/Contents/DeptID/...ChaoSend/tlist` 且**不再含任何 approval 键**、`ReadDetail` 按班级分组(`account=1301`/`teaname=初一(1)班(45人)`)、`MyHomeworks` buckets `{todo:3,submitted:1,graded:2,redo:0,all:7}`、`Statis` 11 字段齐全、`GetClassTree` 年级→班级带 `studentCount`。 + - 审批端点确认消失:`/api/homework/ApprovalList`、`/api/dict/ApproverPool` 均 **HTTP 404**。 + - 临时服务已按 PID 校验(命令行确为本工程 `vite.js --mode mock`)后 taskkill,8077 已释放。 +4. **类型检查工具链本身不可用(既有问题,非本次引入)**:`vue-tsc@1.8.27` 在 `typescript@5.9.3` 下直接崩溃(`Search string not found: "/supportedTSExtensions = .*(?=;)/"`,vue-tsc 1.x 的 monkey-patch 与 TS 5.5+ 不兼容)。退化用 `npx tsc --noEmit` 得 **65 个报错,全部为存量**:alova 泛型 `TS2344 Type 'T' does not satisfy the constraint 'AlovaGenerics'`(连未改动的 `notification.ts`/`teacherHomework.ts`/`studentHomework.ts` 同样报)、mock 种子数组属性推断 `TS2339`、`menu.ts` 的 `IModule` 未从 `@/store` 导出、`teacherNames` 未使用等;**其中无一条引用被删除的审批符号**(已 grep 验证)。→ 建议后续单列一项:升级 `vue-tsc` 到 2.x 或锁 `typescript<=5.4`,否则类型门禁形同虚设。 + +**行为变化(预期内)**:原先 `pending`/`rejected` 的演示种子不再被门控,收件箱从 6 条变 7 条、`Statis.publishedCount` 7/8;这符合「无送审环节,发布即可见」的产品口径。 + +**回退方式**:`git checkout HEAD -- src/views/homework/ApprovalList.vue src/views/homework/ApprovalSetting.vue` 可复原页面;types/api/menu/menuRoute/App/mock 六个文件的改动前快照在 `.backup/20260915-drop-approval/`(因工作区相对 HEAD 另有未提交的 M5 改动,直接 `git restore` 会连带丢 M5,故用快照)。 + +--- + +## 7. 应用设置功能下线(2026-09-15) + +### 7.1 裁决 + +「应用设置」(路由 `/setting`,modId 21)承载两块能力:**发文部门字典**(`dict/ManageList·Add·Update·Delete·Sort`)与**自定义联系组**(`contacts/GetGroup·AddGroup·EditGroup·DelGroup·GetContactsTea·SetContactsTea·GetGroupTree`),任课教师布置作业不依赖它们。裁决: + +- **只删前端**:后端 `DictController`/`ContactsController` 及其 `sys_dict`/`contacts_group`/`contacts_group_user` 表**保留不动**(`HomeworkService.resolveDeptName` 仍要查 `sys_dict`;端点无人调用不影响运行)。 +- **仅该页使用的组件与死 API 一并清掉**(与 §6 同一口径)。 + +### 7.2 牵连面盘点 + +| 文件 | 现状 | 处置 | +|---|---|---| +| `src/views/homework/HomeworkSetting.vue` | 应用设置页(部门拖拽排序 + 联系组及组员维护) | 删除(git 跟踪) | +| `src/components/TypeSetBox.vue` | 仅被 HomeworkSetting 引用(全仓 grep 零其他消费者) | 删除 | +| `src/components/UserCheckInput.vue` | 零消费者(其 `loader` 用 `baseApi.getSchoolAll` + `settingApi.getGroupTree`) | 删除 | +| `src/router/routes.ts` | `/setting` 路由(86-91)+ 旧路径重定向 `/homework/setting`(121)+ 头注释 `21=应用设置` | 删两处路由与注释项 | +| `src/assets/config/menu.ts` | `{ id: 21, modId: 21, name: "应用设置", page: "/setting" }` | 删除 | +| `src/api/module/homework.ts` | `class SettingApi`(12 个方法)+ `export const settingApi`;`BaseApi.getSchoolAll`;`HomeworkApi.getSysDic`(dict/List)、`getSchModuleIsSms`(dict/Sync) 自 §6 后零调用 | 全部删除 | +| `src/types/homework.ts` | `DeptManageItem`、`DictOption`、`ContactGroupPage`、`ContactUserPage`、`ContactGroupItem`、`ContactUser` | 删除 | +| `src/assets/mock/homeworkapi.ts` | `/api/base/GetSchoolAll`(682)、`/api/dict/*`(692-741)、`/api/contacts/*`(1173-1250) 三组桩;种子 `groups/groupSeq`(412-418)、`idNames`(420)、`teacherNames`(425,本就是死码)、`schoolTree`(431)、`deptSeq`(31) | 全部删除;头注释端点清单同步改写 | + +### 7.3 明确保留项(勿顺手删) + +| 保留 | 理由 | +|---|---| +| mock 的 `deptDict`(21-30) | Create(830)/Edit(867) 仍用 `deptDict.find(d => d.CodeID === b.deptId)` 解析部门名,是作业域数据来源,不是设置页专属 | +| `src/components/UserCheckModal.vue`、`ClassCheckInput.vue` | 发作业选班级仍在用 | +| `src/components/MyConfirm` | HomeworkList / TeacherHomeworks 仍在用 | +| 类型 `SchoolTreeNode` | `BaseApi.getUserInfo` 的返回类型仍引用它 | +| `vuedraggable` 依赖 | 删除后成未用依赖,但改 `package.json`/lock 属额外风险,本次不动(后续清依赖时一并处理) | +| 后端 `dict/*`、`contacts/*` 端点与三张表 | 按 7.1 裁决保留 | + +### 7.4 对后端 V4 方案的连带影响(需同步改 `classwork-homework-plan-v4.md`) + +1. **E-01 修法调整**:原计划「`deptId` 为空时回填字典默认发文部门」失去维护入口,改为**从当前教师主组/`BoaUser.getCurrentRole()` 推导** `deptId/deptName`,`sys_dict` 仅作为已有数据的名称解析(`resolveDeptName` 保持)。 +2. **E-05 面收窄但过滤器仍必需**:`postForm` 的 14 处调用点里,dict 4 处 + contacts 6 处随本功能下线归零,剩余 `Remind`(N-04 新门面)、`ReceiveUsers`、`UserPull`、`SetTopmost`、`Expedite`、`InitializeRed`(无参) —— `SingleValueParameterFilter` 照旧要做,只是回归面变小。 + +### 7.5 执行步骤与验收 + +1. 快照受影响文件到 `.backup/20260915-drop-app-setting/`。 +2. `git rm` 三个文件 → 删路由/菜单 → 删 SettingApi 与死方法 → 删 types → 清 mock(端点 + 专属种子)。 +3. 验收:① `grep -rniE "settingApi|DeptManageItem|ContactGroup|ContactUser|DictOption|getSchoolAll|getGroupTree|TypeSetBox|UserCheckInput|homework_setting" src` 命中 0;② `npm run build` EXIT=0;③ `dev:mock` 冒烟:`List/SendList/Detail/ReadDetail/MyHomeworks/Statis/GetClassTree` 仍全 `code=0`,`/api/dict/*`、`/api/contacts/*`、`/api/base/GetSchoolAll` 均 404,`Create` 仍能解析 `deptName`(回归 §7.3 第一条);④ 侧栏无「应用设置」。 +4. 结果回填 §7.6。 + +### 7.6 执行结果回填(2026-09-15 完成) + +**改动清单** + +| 文件 | 动作 | +|---|---| +| `src/views/homework/HomeworkSetting.vue`、`src/components/TypeSetBox.vue`、`src/components/UserCheckInput.vue` | `git rm` | +| `src/router/routes.ts` | 删 `/setting` 路由 + `/homework/setting` 重定向(目标已不存在,留着会把用户导向未知路由);头注释 modId 对照去掉 `21=应用设置` | +| `src/assets/config/menu.ts` | 删 `{ id: 21, modId: 21, 应用设置 }`,头注释改为「不做审批、不开放应用设置」 | +| `src/utils/menuRoute.ts` | `DROPPED_MOD_IDS` 扩为 `{5, 6, 21}`,`DROPPED_NAMES` 增「应用设置 / 作业设置」——平台 `GetUserMod` 若仍下发 modId 21,侧栏不会出现死菜单 | +| `src/api/module/homework.ts` | 删 `class SettingApi`(12 方法)与 `settingApi` 导出、`BaseApi.getSchoolAll`、`HomeworkApi.getSysDic`/`getSchModuleIsSms`、`DICT`/`CONTACTS` 前缀常量、4 个类型导入;头注释第 3 条改写 | +| `src/types/homework.ts` | 删 `DictOption`、`DeptManageItem`、`ContactGroupItem`、`ContactGroupPage`、`ContactUser`、`ContactUserPage`;分节改名为「基础平台」(保留 `SchoolTreeNode`) | +| `src/assets/mock/homeworkapi.ts` | 删 `dict` 6 桩 + `contacts` 7 桩 + `GetSchoolAll` 桩;删种子 `groups/groupSeq`、`idNames`、`teacherNames`(本就是死码)、`schoolTree`、`deptSeq`;头注释与 `deptDict` 注释同步改写。**`deptDict` 按 §7.3 保留** | + +**验收实测** + +1. **残留归零**:手写代码中 `settingApi|DeptManageItem|ContactGroup|ContactUser|DictOption|getSchoolAll|getGroupTree|TypeSetBox|UserCheckInput|homework_setting|deptSeq` **0 命中**。另有 12 处 grep 命中经逐条核对均非残留:`src/api/apiDefinitions.ts` 与 `globals.d.ts` 是 `npm run api:gen` 生成的 swagger 快照(镜像后端现状,后端端点仍在,属正确内容);其余为 `SchoolTreeNode` 被大小写不敏感的 `schoolTree` 模式误命中。 +2. **构建通过**:`npm run build` **EXIT=0**,16.79s。 +3. **mock 冒烟**(`dev:mock` @8077):保留面 9 个端点全 `code=0`(`List` 5/7、`SendList` 3/3、`Detail`、`ReadDetail` 3 组、`MyHomeworks` buckets `{todo:3,submitted:1,graded:2,redo:0,all:7}`、`Statis`、`GetClassTree`、`GetUserInfo`、`GetUserMod`);**§7.3 保留项回归**:`Create(deptId:"D002")` → `GetDetail` 仍解析出 `DeptName="数学教研组"`、`DeptID="D002"`,说明 `deptDict` 未误删;已删端点 6/6 全部 **404**(`dict/List`、`dict/ManageList`、`dict/Sync`、`contacts/GetGroup`、`contacts/GetGroupTree`、`base/GetSchoolAll`)。临时服务按 PID 命令行确认归属后 taskkill,8077 已释放。 +4. **类型侧**:沿用 §6.4 第 4 条结论(`vue-tsc@1.8.27` 与 `typescript@5.9.3` 不兼容、工程 tsc 基线 65 条存量错),本次只核对「无引用已删符号的新报错」。 + +**遗留(有意不动)**:`vuedraggable` 依赖失去唯一使用者,留待统一清依赖时处理;`apiDefinitions.ts`/`globals.d.ts` 待下次 `npm run api:gen` 自然刷新;后端 `dict/*`、`contacts/*` 与三张表按裁决保留。 + +**回退方式**:`git checkout HEAD -- src/views/homework/HomeworkSetting.vue src/components/TypeSetBox.vue src/components/UserCheckInput.vue` 复原三文件;routes/menu/menuRoute/api/types/mock 六个文件的改动前快照在 `.backup/20260915-drop-app-setting/`。 + + + + diff --git a/src/App.vue b/src/App.vue index 6226fb7..d0515dd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,7 @@ import layout from "./components/layout.vue"; import { useRouter } from "vue-router"; import config from "@/assets/config/host.config"; import { baseApi } from "@/api/module/homework"; -import { resolveMenuPage } from "@/utils/menuRoute"; +import { resolveMenuPage, dropDroppedModules } from "@/utils/menuRoute"; import { Message } from "view-ui-plus"; const router = useRouter(); @@ -19,9 +19,8 @@ const layoutData = reactive({ isAlone: false, }); -function roleOfPath(p: string): "teacher" | "student" | "parent" { +function roleOfPath(p: string): "teacher" | "student" { if (p.startsWith("/stu")) return "student"; - if (p.startsWith("/par")) return "parent"; return "teacher"; } @@ -38,8 +37,8 @@ async function initShell() { const user: any = await baseApi.getUserInfo().send(); const { id, name, profilePicture } = user || {}; - // 2. 获取当前用户已授权的模块列表 - const modList: any[] = await baseApi.getUserMod().send(); + // 2. 获取当前用户已授权的模块列表(剔除已下线的审批模块,避免死菜单) + const modList: any[] = dropDroppedModules(await baseApi.getUserMod().send()); layoutData.userInfo = { id, name: name || "管理员", avatar: profilePicture || "" }; layoutData.modList = modList || []; diff --git a/src/api/module/homework.ts b/src/api/module/homework.ts index 3f1c3bf..ee4620f 100644 --- a/src/api/module/homework.ts +++ b/src/api/module/homework.ts @@ -14,14 +14,8 @@ import type { HomeworkStatisticsVo, HomeworkRequest, HomeworkQuery, - DictOption, - DeptManageItem, - ContactGroupPage, - ContactUserPage, SchoolTreeNode, ClassTreeNode, - ApprovalQuery, - ApproverVo, SubmissionsResultVo, SubmissionDetailVo, GradeRequest, @@ -40,7 +34,8 @@ import type { * 变更要点(P0): * 1. 作业域去掉 Homework_list 二级段(/api/homework/Homework_list/List → /api/homework/List) * 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down) - * 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete) + * 3. 字典域与联系组域(发文部门管理、自定义联系组)随「应用设置」功能下线整体移除 + * (见 classwork-frontend-m5-plan.md §7),本文件不再封装 /api/dict/* 与 /api/contacts/* * 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交) * 5. 参数更名:chaoSend→fileIds、topmost→smsFlag * 6. Delete 入参从 ids 逗号串改为 List JSON 数组 @@ -52,9 +47,7 @@ import type { const HOMEWORK = "/api/homework"; const BASE = "/api/base"; -const DICT = "/api/dict"; const FILE = "/api/file"; -const CONTACTS = "/api/contacts"; /** GET 请求,T 为返回数据类型 */ function get(url: string, params?: Record): Method { @@ -110,10 +103,6 @@ class BaseApi { getUserMod() { return get(`${BASE}/GetUserMod`); } - /** 当前学校组织树(部门→教师,用于选择用户) */ - getSchoolAll() { - return get(`${BASE}/GetSchoolAll`); - } /** 年级→班级树(学生场景:选择通知对象到班级一级) */ getClassTree() { return get(`${BASE}/GetClassTree`); @@ -209,7 +198,7 @@ class HomeworkApi { return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid }); } - // ========== v2 学生 / 家长 ========== + // ========== v2 学生端 ========== /** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */ myHomeworks() { return get(`${HOMEWORK}/MyHomeworks`); @@ -222,127 +211,17 @@ class HomeworkApi { submit(data: SubmitRequest) { return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data); } - /** 家长提醒孩子(只读端) */ - parentNudge(serid: number) { - return postForm(`${HOMEWORK}/ParentNudge`, { serid }); - } // ========== 附件 ========== fileDownUrl = fileDownUrl; allFileDownUrl = allFileDownUrl; - // ========== 部门字典 ========== - /** 发文部门下拉 */ - getSysDic() { - return get(`${DICT}/List`, { type: 1 }); - } - /** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */ - getSchModuleIsSms() { - return get(`${DICT}/Sync`); - } /** 作业统计 */ statistics(params?: { starttime?: string; endtime?: string }) { return get(`${HOMEWORK}/Statis`, params); } - - // ========== 审批(可选流程:needApproval=1 的作业进入审批列表) ========== - /** 审批列表(status: pending/approved/rejected/all,缺省 pending) */ - approvalList(params: ApprovalQuery) { - return get>(`${HOMEWORK}/ApprovalList`, params); - } - /** 待审批数量(菜单角标) */ - approvalCount() { - return get(`${HOMEWORK}/ApprovalCount`); - } - /** 审批通过(作业置为已发布) */ - approve(serid: number) { - return postForm(`${HOMEWORK}/Approve`, { serid }); - } - /** 审批驳回(附审批意见,作业退回发布人可编辑重提) */ - reject(serid: number, remark: string) { - return postForm(`${HOMEWORK}/Reject`, { serid, remark }); - } -} - -class SettingApi { - // ========== 发文部门管理 ========== - /** 获取发文部门管理列表 */ - getDictMangeList() { - return get(`${DICT}/ManageList`, { type: "dept" }); - } - /** 新增发文部门 */ - addDeptDic(name: string) { - return postForm(`${DICT}/Add`, { name }); - } - /** 更新发文部门(名称、启用状态) */ - updateDeptDic(SerID: number, Name: string, Enable = 1) { - return postForm(`${DICT}/Update`, { SerID, Name, Enable }); - } - /** 删除发文部门(原 GET DelDept,已 POST 化) */ - delDept(serid: number) { - return postForm(`${DICT}/Delete`, { serid }); - } - /** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */ - sortDeptDic(ids: string) { - return postForm(`${DICT}/Sort`, { ids }); - } - // ========== 自定义组管理 ========== - /** 获取联系组列表 */ - getGroup(page: number, count: number) { - return get(`${CONTACTS}/GetGroup`, { page, count }); - } - /** 新增联系组 */ - addGroup(name: string) { - return postForm(`${CONTACTS}/AddGroup`, { name }); - } - /** 编辑联系组 */ - editGroup(id: number, name: string) { - return postForm(`${CONTACTS}/EditGroup`, { name, id }); - } - /** 删除联系组 */ - delGroup(ids: string) { - return postForm(`${CONTACTS}/DelGroup`, { ids }); - } - /** 获取联系组用户 */ - getContactsTea(gid: number, page = 0, count = 0) { - return get(`${CONTACTS}/GetContactsTea`, { gid, page, count }); - } - /** 设置联系组用户 */ - setContactsTea(gid: number, ids: string) { - return postForm(`${CONTACTS}/SetContactsTea`, { gid, ids }); - } - /** 联系组用户树(选择用户-自定义组 tab) */ - getGroupTree() { - return get(`${CONTACTS}/GetGroupTree`, { type: 1 }); - } -} - -/** 审批人维护 + 审批人候选池(发布时选人即送审) */ -class ApprovalSettingApi { - /** 审批人候选池(发布界面选审批人用,仅启用者) */ - approverPool() { - return get<{ id: number; name: string; account: string; deptName: string; isParent: boolean }[]>(`${DICT}/ApproverPool`); - } - /** 审批人列表(管理页) */ - approvers() { - return get(`${DICT}/Approvers`); - } - /** 新增审批人 */ - addApprover(data: { account: string; name: string; deptId: string }) { - return postJSON(`${DICT}/AddApprover`, data); - } - /** 更新审批人(姓名/部门/启用状态) */ - updateApprover(data: { id: number; name?: string; deptId?: string; enabled?: number }) { - return postJSON(`${DICT}/UpdateApprover`, data); - } - /** 删除审批人 */ - delApprover(id: number) { - return postForm(`${DICT}/DelApprover`, { id }); - } } export const baseApi = new BaseApi(); export const homeworkApi = new HomeworkApi(); -export const settingApi = new SettingApi(); -export const approvalSettingApi = new ApprovalSettingApi(); export default homeworkApi; diff --git a/src/api/module/notification.ts b/src/api/module/notification.ts new file mode 100644 index 0000000..23c9f88 --- /dev/null +++ b/src/api/module/notification.ts @@ -0,0 +1,38 @@ +import { Method } from "alova"; +import { alovaInstance } from "@/utils/alova/alova-iview"; +import type { BasePageResult, NotificationVo, PageQueryV2 } from "@/types/homeworkV2"; + +/** + * 课堂作业通知中心接口封装(M5,对齐 NotificationController)。 + * 教师端同接口,按 receiver_role 区分(后端按登录账号过滤)。 + */ + +const NOTIFY = "/api/notifications"; + +/** GET 请求 */ +function get(url: string, params?: Record): Method { + return new Method("GET", alovaInstance, url, { params }); +} + +/** POST(无 body 写操作) */ +function post(url: string): Method { + return new Method("POST", alovaInstance, url); +} + +class NotificationApi { + /** 通知分页(unreadOnly=true 只看未读;返回含 unreadCount 时由页面取用) */ + list(params?: PageQueryV2 & { unreadOnly?: boolean }) { + return get & { unreadCount?: number }>(NOTIFY, params); + } + /** 单条已读 */ + markRead(id: number) { + return post(`${NOTIFY}/${id}/read`); + } + /** 全部已读 */ + markAllRead() { + return post(`${NOTIFY}/read-all`); + } +} + +export const notificationApi = new NotificationApi(); +export default notificationApi; diff --git a/src/api/module/studentHomework.ts b/src/api/module/studentHomework.ts new file mode 100644 index 0000000..5dafc68 --- /dev/null +++ b/src/api/module/studentHomework.ts @@ -0,0 +1,77 @@ +import { Method } from "alova"; +import { alovaInstance } from "@/utils/alova/alova-iview"; +import type { + BasePageResult, + PageQueryV2, + StudentHomeworkDetailV2Vo, + StudentHomeworkV2Vo, + StudentResultVo, + StudentSubmitRequest, + StudentTab, + SubmissionActionRequest, +} from "@/types/homeworkV2"; + +/** + * 课堂作业 V2 学生端接口封装(M5,对齐 StudentHomeworkController,PRD §18 学生页 1~3)。 + * + * 身份:当前登录用户账号 ↔ class_student 名册;仅可见本人班级作业(后端禁 IDOR)。 + */ + +const STUDENT = "/api/student/homeworks"; + +/** GET 请求 */ +function get(url: string, params?: Record): Method { + return new Method("GET", alovaInstance, url, { params }); +} + +/** POST JSON body(@RequestBody) */ +function postJSON(url: string, data?: Record | any[]): Method { + return new Method( + "POST", + alovaInstance, + url, + { headers: { "Content-Type": "application/json;charset=UTF-8" } }, + data ? JSON.stringify(data) : undefined, + ); +} + +/** PUT JSON body(@RequestBody) */ +function putJSON(url: string, data?: Record | any[]): Method { + return new Method( + "PUT", + alovaInstance, + url, + { headers: { "Content-Type": "application/json;charset=UTF-8" } }, + data ? JSON.stringify(data) : undefined, + ); +} + +class StudentHomeworkApi { + /** 我的作业列表(tab=all/todo/done/closed + 分页/keyword) */ + list(tab: StudentTab | string = "all", params?: PageQueryV2) { + return get>(STUDENT, { tab, ...params }); + } + /** 作业详情(含说明/附件/我的当前提交内容) */ + detail(id: number) { + return get(`${STUDENT}/${id}`); + } + /** 阅读上报(前端停留 ≥3 秒后调用;幂等) */ + read(id: number) { + return postJSON(`${STUDENT}/${id}/read`); + } + /** 提交作业,返回 { submissionId } */ + submit(id: number, data: StudentSubmitRequest) { + return postJSON<{ submissionId: number }>(`${STUDENT}/${id}/submissions`, data); + } + /** 撤回(截止前)/ 打回后重交,返回 { submissionId: 新提交行 id } */ + action(id: number, submissionId: number, data: SubmissionActionRequest) { + return putJSON<{ submissionId: number }>(`${STUDENT}/${id}/submissions/${submissionId}`, data); + } + /** 批阅结果(当前版本 + 历史版本含各版评语) */ + result(id: number) { + return get(`${STUDENT}/${id}/result`); + } +} + +export const studentApi = new StudentHomeworkApi(); +export default studentApi; diff --git a/src/api/module/teacherHomework.ts b/src/api/module/teacherHomework.ts new file mode 100644 index 0000000..9459ff6 --- /dev/null +++ b/src/api/module/teacherHomework.ts @@ -0,0 +1,161 @@ +import { Method } from "alova"; +import { alovaInstance } from "@/utils/alova/alova-iview"; +import conf from "@/assets/config/host.config"; +import type { + BasePageResult, + ClassTreeNodeV2, + HomeworkStatsVo, + HomeworkV2Request, + HomeworkV2Vo, + RemindType, + ReviewRequest, + ReturnRequest, + StudentMatrixQuery, + StudentMatrixVo, + SubmissionVersionVo, + TeacherHomeworkQuery, +} from "@/types/homeworkV2"; + +/** + * 课堂作业 V2 教师端接口封装(M5,对齐 TeacherHomeworkV2Controller,PRD §18 页 1~6)。 + * + * 路径 /api/teacher/homeworks…,RESTful 动词与后端一字对齐;统一响应由 + * alova-iview 解包为 data(code==0),页面直接使用返回值。 + */ + +const TEACHER = "/api/teacher/homeworks"; + +/** GET 请求 */ +function get(url: string, params?: Record): Method { + return new Method("GET", alovaInstance, url, { params }); +} + +/** POST JSON body(@RequestBody) */ +function postJSON(url: string, data?: Record | any[]): Method { + return new Method( + "POST", + alovaInstance, + url, + { headers: { "Content-Type": "application/json;charset=UTF-8" } }, + data ? JSON.stringify(data) : undefined, + ); +} + +/** PUT JSON body(@RequestBody) */ +function putJSON(url: string, data?: Record | any[]): Method { + return new Method( + "PUT", + alovaInstance, + url, + { headers: { "Content-Type": "application/json;charset=UTF-8" } }, + data ? JSON.stringify(data) : undefined, + ); +} + +/** DELETE(无 body) */ +function del(url: string): Method { + return new Method("DELETE", alovaInstance, url); +} + +class TeacherHomeworkApi { + // ========== 页 1:发布作业 ========== + + /** 创建作业(落草稿),返回新作业 id */ + create(data: HomeworkV2Request) { + return postJSON(TEACHER, data); + } + /** 编辑作业(仅草稿两态) */ + edit(id: number, data: HomeworkV2Request) { + return putJSON(`${TEACHER}/${id}`, data); + } + /** 删除作业(仅草稿两态) */ + remove(id: number) { + return del(`${TEACHER}/${id}`); + } + /** 立即发布(草稿 → 进行中) */ + publish(id: number) { + return postJSON(`${TEACHER}/${id}/publish`); + } + /** 定时发布 / 取消定时(cancel=true 回退草稿并清空定时时间) */ + schedule(id: number, payload: { scheduledAt?: string; cancel?: boolean }) { + return postJSON(`${TEACHER}/${id}/schedule`, payload); + } + + // ========== 页 2:草稿箱 ========== + + drafts(params: TeacherHomeworkQuery) { + return get>(`${TEACHER}/drafts`, params); + } + + // ========== 页 3:已发布列表 ========== + + published(params: TeacherHomeworkQuery) { + return get>(`${TEACHER}/published`, params); + } + /** 提醒未读/未交学生,返回 { reminded: 实际提醒人数 } */ + remind(id: number, type: RemindType, studentIds?: string[]) { + return postJSON<{ reminded: number }>(`${TEACHER}/${id}/remind`, { type, studentIds }); + } + /** 延长截止时间(新截止须晚于当前时间) */ + extend(id: number, deadlineAt: string) { + return postJSON(`${TEACHER}/${id}/extend`, { deadlineAt }); + } + /** 复制为新草稿,返回新作业 id */ + copy(id: number) { + return postJSON(`${TEACHER}/${id}/copy`); + } + /** 归档(只读,不再触发任何提醒) */ + archive(id: number) { + return postJSON(`${TEACHER}/${id}/archive`); + } + /** 导出学生矩阵 Excel 直链(浏览器 location.href 跳转) */ + exportUrl(id: number): string { + return `${conf.serviceHost}${TEACHER}/${id}/export`; + } + + // ========== 页 4:详情 / 统计 ========== + + detail(id: number) { + return get(`${TEACHER}/${id}`); + } + /** 汇总统计卡片(总进度 + 分班进度) */ + stats(id: number) { + return get(`${TEACHER}/${id}/stats`); + } + + // ========== 页 5:学生矩阵 ========== + + /** 学生矩阵分页(阅读/提交/批阅状态,未交学生生成占位行 submissionId=0) */ + students(id: number, params: StudentMatrixQuery) { + return get>(`${TEACHER}/${id}/students`, params); + } + + // ========== 页 6:批阅 / 打回 / 版本 ========== + + /** 保存批阅(→ REVIEWED,推送批阅完成通知);score 与 comment 至少一项 */ + review(id: number, studentId: string, data: ReviewRequest) { + return postJSON(`${TEACHER}/${id}/submissions/${studentId}/review`, data); + } + /** 打回(→ RETURNED,推送打回通知);comment 必填 */ + returnSubmission(id: number, studentId: string, data: ReturnRequest) { + return postJSON(`${TEACHER}/${id}/submissions/${studentId}/return`, data); + } + /** 历史提交版本(批阅弹窗切换版本) */ + versions(id: number, studentId: string) { + return get(`${TEACHER}/${id}/submissions/${studentId}/versions`); + } + + // ========== 辅助:班级树 / 名册同步 ========== + + /** 年级→班级树(发布选班;叶子 id=classId,含在班学生数) */ + classTree() { + return get(`${TEACHER}/classes`); + } + /** 手动触发名册同步(bschclass 接口就绪前为空操作) */ + syncClasses() { + return postJSON<{ synced: number }>(`${TEACHER}/classes/sync`); + } +} + +export const teacherApi = new TeacherHomeworkApi(); +export default teacherApi; diff --git a/src/assets/config/menu.ts b/src/assets/config/menu.ts index 1237d71..bb53f08 100644 --- a/src/assets/config/menu.ts +++ b/src/assets/config/menu.ts @@ -3,16 +3,16 @@ import type { IModule } from "@/store"; /** * 课程作业左侧菜单数据(对照后端 GetUserMod 现行模块)。 * 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。 + * 注:课程作业不做审批、不开放应用设置,故不含「审批列表/审核设置/应用设置」; + * 平台模块配置若仍下发 modId 5/6/21,由 utils/menuRoute 的 DROPPED_MOD_IDS + * 统一剔除(见 classwork-frontend-m5-plan.md §6、§7)。 */ export function getMenuList(): Promise { const menu: IModule[] = [ { id: 1, modId: 1, name: "已收到", page: "/" }, { id: 2, modId: 2, name: "已发送", page: "/list" }, { id: 3, modId: 3, name: "全部作业", page: "/homeworklist" }, - { id: 5, modId: 5, name: "审批列表", page: "/approval" }, - { id: 6, modId: 6, name: "审核设置", page: "/approval-setting" }, { id: 4, modId: 4, name: "作业统计", page: "/stats" }, - { id: 21, modId: 21, name: "应用设置", page: "/setting" }, ]; return Promise.resolve(menu); } diff --git a/src/assets/mock/homeworkapi.ts b/src/assets/mock/homeworkapi.ts index b60471a..739dc55 100644 --- a/src/assets/mock/homeworkapi.ts +++ b/src/assets/mock/homeworkapi.ts @@ -4,22 +4,20 @@ * 契约对齐 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} + * - /api/base/{GetUserInfo,GetUserMod,GetLayout,GetClassTree,FileUpload} + * - /api/file/{UploadImage,Preview} + * (原 /api/dict/* 与 /api/contacts/* 随「应用设置」功能下线移除,见 classwork-frontend-m5-plan.md §7) * 统一响应 {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)」视角判定已读/未读。 + * 2. 当前登录演示身份 = 数学教师 朱皓东(zhd);收件箱以「初一(1)班(1301)」视角判定已读/未读。 + * 3. 课程作业不做审批(无送审环节,发布即可见),详见 classwork-frontend-m5-plan.md §6。 */ /* ============================ 基础数据 ============================ */ -/** 发布单位(作业归属教研组 / 教务处)字典 */ +/** 部门字典(仅作 Create/Edit 解析 deptName 之用,无维护入口——「应用设置」已下线) */ const deptDict = [ { SerID: 101, CodeID: "D001", Name: "语文教研组", Enable: 1 }, { SerID: 102, CodeID: "D002", Name: "数学教研组", Enable: 1 }, @@ -30,7 +28,6 @@ const deptDict = [ { SerID: 107, CodeID: "D007", Name: "体育教研组", Enable: 0 }, { SerID: 108, CodeID: "D008", Name: "教务处(作业管理)", Enable: 1 }, ]; -let deptSeq = 109; let nextFileId = 9001; /** 当前登录用户(发布教师) */ @@ -38,60 +35,6 @@ 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 = [ { @@ -132,7 +75,7 @@ const ALL_STUDENTS = ALL_CLASSES.reduce((s, id) => s + classMap[id].studentCount /** * 作业存储。classRead = 已读班级 id 列表(班级维度阅读统计)。 - * approvalEnabled/approvalStatus:见文件头说明;pending/rejected 不进入学生收件箱。 + * 无审批门控:仅草稿(hwStatus=draft)与定时未到点的作业不进入学生收件箱。 */ const homeworks = [ { @@ -152,11 +95,6 @@ const homeworks = [ smsFlag: 0, timingSendTime: null, createTime: "2026-09-01 10:35:00", - approvalEnabled: 0, - approvalStatus: "none", - approvalRemark: null, - approvalBy: null, - approvalTime: null, }, { id: 2, @@ -174,11 +112,6 @@ const homeworks = [ 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, @@ -196,33 +129,23 @@ const homeworks = [ smsFlag: 0, timingSendTime: null, createTime: "2026-09-08 09:00:00", - approvalEnabled: 1, - approvalStatus: "pending", - approvalRemark: null, - approvalBy: null, - approvalTime: null, }, { id: 4, - title: "国庆假期数学作业(计算巩固 + 生活中的数学)(定时发布)", + title: "国庆假期数学作业(计算巩固 + 生活中的数学)", deptId: "D002", deptName: "数学教研组", publisherAccount: "zhd", publisherName: "朱皓东", - content: "

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

定于 9 月 30 日 08:00 发布给全部班级,收假后第一节课查验。

", + content: "

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

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

", receiveList: "ALL", classRead: [], files: null, topmost: 0, isPulled: 0, smsFlag: 0, - timingSendTime: "2026-09-30 08:00:00", + timingSendTime: null, createTime: "2026-09-05 16:40:00", - approvalEnabled: 0, - approvalStatus: "none", - approvalRemark: null, - approvalBy: null, - approvalTime: null, }, { id: 5, @@ -240,11 +163,6 @@ const homeworks = [ 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, @@ -262,11 +180,6 @@ const homeworks = [ smsFlag: 0, timingSendTime: null, createTime: "2026-09-07 08:30:00", - approvalEnabled: 0, - approvalStatus: "none", - approvalRemark: null, - approvalBy: null, - approvalTime: null, }, { id: 7, @@ -284,11 +197,6 @@ const homeworks = [ smsFlag: 0, timingSendTime: null, createTime: "2026-09-09 10:10:00", - approvalEnabled: 1, - approvalStatus: "pending", - approvalRemark: null, - approvalBy: null, - approvalTime: null, }, { id: 8, @@ -306,42 +214,10 @@ const homeworks = [ 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(); - /* ===================== 课程作业扩展模型(v1 核心闭环:布置→提交→批改→统计) ===================== */ /** 每份作业的“作业化”元数据(学科 / 截止 / 提交方式 / 流程状态),叠加到 homeworks 上 */ @@ -386,11 +262,13 @@ const submissions: any[] = []; let subSeq = 1; homeworks.forEach((n: any) => { if (n.hwStatus === "draft") return; - if ((n.submitType || []).includes("none")) 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 = r < (n.hwStatus === "collecting" ? 0.62 : 0.9); + 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; @@ -418,10 +296,11 @@ homeworks.forEach((n: any) => { studentName: stu.studentName, classId: cid, className: classMap[cid]?.className || cid, + read, submittedAt, submitStatus, text: submitted ? "(学生作答内容 · mock)" : "", - images: submitted && (n.submitType || []).includes("image") ? [] : [], + images: submitted && (n.submitType || []).includes("image") ? [mockImageDataURL(stu.studentId)] : [], attachments: submitted && (n.submitType || []).includes("attach") ? [] : [], grade, }); @@ -441,6 +320,7 @@ function progressOf(homeworkId: number) { 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, @@ -449,22 +329,24 @@ function progressOf(homeworkId: number) { 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" | "parent"; +/* ============ 多角色视角(v2 学生端;家长即登录学生账号,端分离:SetRole 入口切换) ============ */ +type Role = "teacher" | "student"; let viewerRole: Role = "teacher"; -/** 演示学生:取 ME_CLASS(初一(1)班)花名册第一名;家长为其监护人 */ +/** 演示学生:取 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" || r === "parent" ? (r as Role) : "teacher"; + viewerRole = r === "student" ? "student" : "teacher"; return viewerRole; } /** 当前学生视角可见作业(已发布、面向本班、非未来定时、非草稿),附本人提交/批改态 */ @@ -486,12 +368,16 @@ function viewerHomeworks() { 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 : "", @@ -522,33 +408,6 @@ const filesStore: 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, @@ -615,16 +474,15 @@ function readCountOf(n: any): number { function isReadByMe(n: any): boolean { return (n.classRead || []).includes(ME_CLASS); } -/** 是否已发布(对学生可见):审批通过或免审 */ +/** 是否已发布(对学生可见):非草稿即可见(课程作业无送审环节) */ function isPublished(n: any): boolean { - return n.approvalStatus === "none" || n.approvalStatus === "approved"; + return n.hwStatus !== "draft"; } /** 定时未到的作业,收件箱不展示 */ 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) { @@ -639,7 +497,7 @@ function toVo(n: any) { submitType: n.submitType || ["text"], hwStatus: n.hwStatus || "collecting", hwStatusDesc: HW_STATUS_DESC[n.hwStatus] || "收集中", - progress: n.hwStatus === "draft" || (n.submitType || []).includes("none") ? null : progressOf(n.id), + progress: n.hwStatus === "draft" ? null : progressOf(n.id), receiveText: receiveText(n), classCount: isAllTarget(n) ? ALL_CLASSES.length : targetClassIds(n).length, publisherAccount: n.publisherAccount, @@ -658,14 +516,6 @@ function toVo(n: any) { 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, @@ -701,37 +551,9 @@ function toDetail(n: any) { submitType: n.submitType || ["text"], hwStatus: n.hwStatus || "collecting", hwStatusDesc: HW_STATUS_DESC[n.hwStatus] || "收集中", - progress: n.hwStatus === "draft" || (n.submitType || []).includes("none") ? null : progressOf(n.id), + 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 @@ -780,8 +602,8 @@ export default [ : ok({ id: DEMO_STUDENT.studentId, account: "stu" + DEMO_STUDENT.studentId, - name: viewerRole === "parent" ? DEMO_STUDENT.studentName + "家长" : DEMO_STUDENT.studentName, - role: viewerRole, + name: DEMO_STUDENT.studentName, + role: "student", studentId: DEMO_STUDENT.studentId, studentName: DEMO_STUDENT.studentName, classId: DEMO_STUDENT.classId, @@ -800,14 +622,10 @@ export default [ { 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: [] }, ] - : viewerRole === "parent" - ? [{ id: 41, modId: 41, name: "孩子作业", parentId: 0, page: "/par/kids", orderNumber: 1, usable: true, children: [] }] : [ { id: 2, modId: 2, name: "我布置的", parentId: 0, page: "/list", orderNumber: 1, usable: true, children: [] }, - { id: 25, modId: 25, name: "提交情况", parentId: 0, page: "/submissions", orderNumber: 2, usable: true, children: [] }, - { id: 26, modId: 26, name: "批改中心", parentId: 0, page: "/grade", orderNumber: 3, usable: true, children: [] }, - { id: 4, modId: 4, name: "作业统计", parentId: 0, page: "/stats", orderNumber: 4, usable: true, children: [] }, - { id: 21, modId: 21, name: "应用设置", parentId: 0, page: "/setting", orderNumber: 5, 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: [] }, ] ), }, @@ -817,7 +635,7 @@ export default [ response: () => ok({ ssoUrl: "", - username: viewerRole === "teacher" ? ME.name : viewerRole === "parent" ? DEMO_STUDENT.studentName + "家长" : DEMO_STUDENT.studentName, + username: viewerRole === "teacher" ? ME.name : DEMO_STUDENT.studentName, pathList: [], topName: "课程作业", pageUrl: "", @@ -833,7 +651,6 @@ export default [ 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", @@ -841,57 +658,6 @@ export default [ 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); - }, - }, - /* ---------- 作业列表 ---------- */ { // 已收到(学生/收件视角):仅已发布、非未来定时、面向本班 @@ -982,24 +748,6 @@ export default [ response: ({ body, query }: any) => { const b = bodyOf(body, query); const dept = deptDict.find((d) => d.CodeID === b.deptId); - // 选人即送审:解析发布入参里的审批流(flow 多步 / approvers 单步),有审批人则进入待审批 - let flowInput: any = b; - if (typeof b.flow === "string") { - try { - flowInput = { ...flowInput, flow: JSON.parse(b.flow) }; - } catch { - /* ignore */ - } - } - if (typeof b.approvers === "string") { - try { - flowInput = { ...flowInput, approvers: JSON.parse(b.approvers) }; - } catch { - /* ignore */ - } - } - const flow = buildFlowFromInput(flowInput); - const approvalEnabled = flow ? 1 : 0; const receive = b.receiveList || "all"; const isAll = /^all$/i.test(String(receive).trim()); const item: any = { @@ -1021,15 +769,9 @@ export default [ 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), - hwStatus: "collecting", - approvalEnabled, - // 未选审批人 → 直接发布(none);选了 → 待审批(pending) - approvalStatus: approvalEnabled ? "pending" : "none", - approvalRemark: null, - approvalBy: null, - approvalTime: null, - flow, - approvalLevel: 0, + allowLate: !!b.allowLate, + remindBefore: Array.isArray(b.remindBefore) ? b.remindBefore : String(b.remindBefore || "").split(/[,,\s]+/).filter(Boolean), + hwStatus: b.draft ? "draft" : "collecting", }; homeworks.unshift(item); return ok(item.id); @@ -1056,41 +798,6 @@ export default [ 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); }, }, @@ -1285,7 +992,7 @@ export default [ }, }, - /* ---------- v2 学生 / 家长:我的作业 / 提交 / 反馈 ---------- */ + /* ---------- v2 学生端:我的作业 / 提交 / 反馈 ---------- */ { url: "/api/homework/MyHomeworks", method: "get", @@ -1327,9 +1034,8 @@ export default [ const b = bodyOf(body, query); const hw = homeworks.find((h) => h.id === Number(b.serid)); if (!hw) return fail("作业不存在"); - if (viewerRole === "parent") return fail("家长端为只读,不能提交"); if ((hw.submitType || []).includes("none")) return fail("该作业仅要求查阅,无需提交"); - if (hw.hwStatus === "closed") 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) { @@ -1358,147 +1064,8 @@ export default [ return ok({ subId: s.id, submitStatus: s.submitStatus }); }, }, - { - // 家长“提醒孩子”(mock 只记录) - url: "/api/homework/ParentNudge", - 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("作业不存在"); - return ok({ nudged: true, studentName: DEMO_STUDENT.studentName, title: hw.title }); - }, - }, - /* ---------- 审批(多级:会签/或签) ---------- */ - { - url: "/api/homework/ApprovalList", - method: "get", - response: ({ query }: any) => { - const pageIndex = Number(query.pageIndex || 1); - const pageSize = Number(query.pageSize || 10); - const status = query.status || "pending"; - let list = homeworks.filter((n) => n.approvalEnabled === 1); - if (status && status !== "all") list = list.filter((n) => n.approvalStatus === status); - if (query.scope === "user") list = list.filter((n) => n.publisherAccount === ME.account); - list = applyFilters(list, query); - list = sortByTimeDesc(list); - return ok(paginate(list.map(toVo), pageIndex, pageSize)); - }, - }, - { - url: "/api/homework/ApprovalCount", - method: "get", - // 待我审批:当前步待签人包含我的单子数 - response: () => ok(homeworks.filter((n) => n.approvalEnabled === 1 && canAct(n)).length), - }, - { - url: "/api/homework/Approve", - method: "post", - response: ({ body, query }: any) => { - const b = bodyOf(body, query); - const n: any = homeworks.find((x) => x.id === Number(b.serid)); - if (!n) return fail("作业不存在"); - if (n.approvalStatus !== "pending") return fail("该作业已处理,无需重复审批"); - if (!n.flow) return fail("该作业未配置审批流程"); - const actor = b.account || ME.account; - const lv = n.flow[n.approvalLevel || 0]; - if (!lv) { - n.approvalStatus = "approved"; - return ok({ status: "approved" }); - } - if (!lv.approvers.includes(actor)) return fail("您不是当前步骤的审批人,无权处理"); - if (lv.records.some((r: any) => r.account === actor)) return fail("您已处理过本步骤,请等待其他审批人"); - lv.records.push({ account: actor, name: approverName(actor), result: "approved", time: nowStr(), remark: b.remark || "同意" }); - const remaining = pendingApprovers(n); - const stepPass = lv.vote === "any" ? true : remaining.length === 0; // 或签=任一通过;会签=全部通过 - if (stepPass) { - n.approvalLevel = (n.approvalLevel || 0) + 1; - if (n.approvalLevel >= n.flow.length) { - n.approvalStatus = "approved"; - n.approvalBy = approverName(actor); - n.approvalTime = nowStr(); - n.approvalRemark = b.remark || "同意发布"; - } - } - return ok({ status: n.approvalStatus, level: n.approvalLevel, nextPending: n.approvalStatus === "pending" ? pendingApprovers(n) : [] }); - }, - }, - { - url: "/api/homework/Reject", - method: "post", - response: ({ body, query }: any) => { - const b = bodyOf(body, query); - const n: any = homeworks.find((x) => x.id === Number(b.serid)); - if (!n) return fail("作业不存在"); - if (n.approvalStatus !== "pending") return fail("该作业已处理,无需重复审批"); - const actor = b.account || ME.account; - const lv = (n.flow || [])[n.approvalLevel || 0]; - if (lv && !lv.approvers.includes(actor)) return fail("您不是当前步骤的审批人,无权处理"); - if (lv) lv.records.push({ account: actor, name: approverName(actor), result: "rejected", time: nowStr(), remark: b.remark || "不同意" }); - n.approvalStatus = "rejected"; - n.approvalBy = approverName(actor); - n.approvalTime = nowStr(); - n.approvalRemark = b.remark || "已驳回"; - return ok(null); - }, - }, - - /* ---------- 审批人维护(发布时从这里选审批人) ---------- */ - { - // 审批人候选池(供发布界面选择审批人,仅启用者;id 供选人组件,account 为真实审批人标识) - url: "/api/dict/ApproverPool", - method: "get", - response: () => - ok( - approvers - .filter((a) => a.enabled === 1) - .map((a) => ({ id: a.id, name: a.name, account: a.account, deptName: a.deptName, isParent: false })) - ), - }, - { url: "/api/dict/Approvers", method: "get", response: () => ok(approvers.map((a) => ({ ...a }))) }, - { - url: "/api/dict/AddApprover", - method: "post", - response: ({ body, query }: any) => { - const b = bodyOf(body, query); - approvers.push({ - id: approverSeq++, - account: b.account || "", - name: b.name || "", - deptId: b.deptId || "", - deptName: b.deptName || deptDict.find((d) => d.CodeID === b.deptId)?.Name || "", - enabled: 1, - }); - return ok(null); - }, - }, - { - url: "/api/dict/UpdateApprover", - method: "post", - response: ({ body, query }: any) => { - const b = bodyOf(body, query); - const a = approvers.find((x) => x.id === Number(b.id)); - if (a) { - if (b.name) a.name = b.name; - if (b.deptId) { - a.deptId = b.deptId; - a.deptName = deptDict.find((d) => d.CodeID === b.deptId)?.Name || a.deptName; - } - if (b.enabled !== undefined && b.enabled !== "") a.enabled = Number(b.enabled); - } - return ok(null); - }, - }, - { - url: "/api/dict/DelApprover", - method: "post", - response: ({ body, query }: any) => { - const i = approvers.findIndex((x) => x.id === Number(bodyOf(body, query).id)); - if (i > -1) approvers.splice(i, 1); - return ok(null); - }, - }, + /* ---------- 审批功能已移除(课程作业不做审批) ---------- */ /* ---------- 附件上传 ---------- */ { @@ -1516,96 +1083,16 @@ export default [ 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" }]); + 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() { +/** 内联 SVG data URL,作为 mock 预览图片(可带编号区分) */ +function mockImageDataURL(id?: number) { + const label = id ? `课程作业 图片#${id}` : "课程作业 Mock 图片"; const svg = - "课程作业 Mock 图片"; + `${label}`; return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); } diff --git a/src/components/ApprovalFlowEditor.vue b/src/components/ApprovalFlowEditor.vue deleted file mode 100644 index 6b7b72d..0000000 --- a/src/components/ApprovalFlowEditor.vue +++ /dev/null @@ -1,217 +0,0 @@ - - - - - diff --git a/src/components/AttachmentUpload.vue b/src/components/AttachmentUpload.vue new file mode 100644 index 0000000..67d24d6 --- /dev/null +++ b/src/components/AttachmentUpload.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/components/ReviewModal.vue b/src/components/ReviewModal.vue new file mode 100644 index 0000000..67ba02e --- /dev/null +++ b/src/components/ReviewModal.vue @@ -0,0 +1,315 @@ + + + + + diff --git a/src/components/layout.vue b/src/components/layout.vue index ef5da9a..49b6039 100644 --- a/src/components/layout.vue +++ b/src/components/layout.vue @@ -134,13 +134,13 @@ const getMenuIcon = (item?: any): string => { const page: string = item?.page || ""; if (page === "/list/new") return "icon-xiugaijl"; if (page === "/list") return "icon-fasong"; + if (page === "/submissions") return "icon-liebiao"; + if (page === "/grade") return "icon-chengji"; if (page === "/" || page === "/accept") return "icon-sjx"; if (page === "/homeworklist") return "icon-liebiao"; - if (page === "/approval") return "icon-shenpi"; - if (page === "/approval-setting") return "icon-quanxian"; if (page === "/stats") return "icon-tongji"; if (page === "/setting") return "icon-shezhi"; - if (page === "/stu/today" || page === "/par/kids") return "icon-sjx"; + if (page === "/stu/today") return "icon-rili"; if (page === "/stu/redo") return "icon-xiugaijl"; switch (item?.modId ?? item?.id) { case 1: return "icon-xiugaijl"; diff --git a/src/router/routes.ts b/src/router/routes.ts index ba7d36a..5be5ec8 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -10,7 +10,7 @@ import type { RouteRecordRaw } from "vue-router"; * type: 0=编辑视角(不标读) 1=普通查看(标读) * * modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据): - * 1=已收到 2=已发送 3=全部作业 4=作业统计 21=应用设置 + * 1=已收到 2=已发送 3=全部作业 4=作业统计 * 发作业为「已发送」的深层子页(modId 22 不在菜单树,meta.parentId=2 关联高亮), * 与 UserBase「应用管理>应用类型管理」范式一致。 */ @@ -74,20 +74,6 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/views/homework/HomeworkDetails.vue"), meta: { title: "作业详情", modId: 24, parentId: 3 }, }, - // 审批列表(可选流程:需审批后发布的作业在此审批) - { - path: "/approval", - name: "homework_approval", - component: () => import("@/views/homework/ApprovalList.vue"), - meta: { title: "审批列表", modId: 5 }, - }, - // 审核设置(审批策略 / 部门覆盖 / 审批人) - { - path: "/approval-setting", - name: "homework_approval_setting", - component: () => import("@/views/homework/ApprovalSetting.vue"), - meta: { title: "审核设置", modId: 6 }, - }, // 作业统计(旧版行为:整页跳转外部统计系统) { path: "/stats", @@ -95,15 +81,8 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/views/homework/HomeworkStats.vue"), meta: { title: "作业统计", modId: 4 }, }, - // 应用设置(发文部门 + 自定义组) - { - path: "/setting", - name: "homework_setting", - component: () => import("@/views/homework/HomeworkSetting.vue"), - meta: { title: "应用设置", modId: 21 }, - }, - /* ---- v2 端分离:学生 / 家长 ---- */ + /* ---- v2 学生端 ---- */ // 学生·今日作业 { path: "/stu/today", @@ -118,13 +97,6 @@ const routes: RouteRecordRaw[] = [ component: () => import("@/views/homework/StudentHomeworks.vue"), meta: { title: "待订正", modId: 32, role: "student", initTab: "redo" }, }, - // 家长·孩子作业(只读) - { - path: "/par/kids", - name: "parent_kids", - component: () => import("@/views/homework/StudentHomeworks.vue"), - meta: { title: "孩子作业", modId: 41, role: "parent" }, - }, /* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */ { path: "/details", redirect: "/list/detail" }, @@ -139,7 +111,6 @@ const routes: RouteRecordRaw[] = [ { path: "/homework/manage", redirect: "/homeworklist" }, { path: "/homework/homework_details", redirect: "/list/detail" }, { path: "/homework/statistics", redirect: "/stats" }, - { path: "/homework/setting", redirect: "/setting" }, ]; export default routes; diff --git a/src/types/homework.ts b/src/types/homework.ts index cffd6f9..2e69f45 100644 --- a/src/types/homework.ts +++ b/src/types/homework.ts @@ -78,30 +78,12 @@ export interface HomeworkVo { hwStatusDesc?: string; /** 提交/批改进度(草稿或仅查阅为 null) */ progress?: HomeworkProgress | null; - /** 是否启用审批:0 免审直接发布 / 1 需审批后发布 */ - approvalEnabled: number; - /** 审批状态:none 免审 / pending 待审批 / approved 已通过 / rejected 已驳回 */ - approvalStatus: ApprovalStatus; - approvalStatusDesc: string; - /** 当前待审步骤下标(从 0 起) */ - approvalLevel?: number; - /** 当前用户是否为该单当前步骤的待签审批人 */ - canApprove?: boolean; - /** 审批意见 */ - approvalRemark?: string | null; - /** 审批人 */ - approvalBy?: string | null; - /** 审批时间 */ - approvalTime?: string | null; createBy: string | null; createTime: string | null; updateBy: string | null; updateTime: string | null; } -/** 审批状态枚举 */ -export type ApprovalStatus = "none" | "pending" | "approved" | "rejected"; - // ============================ 作业详情 ============================ /** 作业详情视图对象(对齐 HomeworkDetailVo.java,@JsonProperty 保留旧字段名) */ @@ -143,35 +125,6 @@ export interface HomeworkDetailVo { receiveType?: string; /** 接收对象可读文本(全部班级 / 班级名串) */ receiveText?: string; - /** 是否需审批 */ - approvalEnabled?: number; - /** 审批状态 */ - approvalStatus?: ApprovalStatus; - /** 审批状态文本 */ - approvalStatusDesc?: string; - /** 审批意见 */ - approvalRemark?: string | null; - /** 审批人 */ - approvalBy?: string | null; - /** 审批时间 */ - approvalTime?: string | null; - /** 当前待审步骤下标(从 0 起) */ - approvalLevel?: number; - /** 多步审批流进度(也用于发布/编辑回显审批人配置) */ - approvalFlow?: { - name: string; - vote: "all" | "any"; - /** 审批人账号(编辑回显用) */ - approvers?: string[]; - /** 审批人姓名(展示用) */ - approverNames?: string[]; - status: "approved" | "pending" | "rejected" | "waiting"; - records: { account: string; name: string; result: "approved" | "rejected"; time: string; remark: string }[]; - /** 当前步待签人姓名 */ - pending: string[]; - }[]; - /** 当前用户是否为该单当前步骤的待签审批人 */ - canApprove?: boolean; } /** 作业详情附件项(对齐 HomeworkDetailVo.FileItem) */ @@ -372,7 +325,13 @@ export interface HomeworkRequest { /** 提交方式:text/image/attach/none */ submitType?: string[] | string; type?: number; - deptId: string; + deptId?: string; + /** 是否允许逾期补交 */ + allowLate?: boolean; + /** 截止提醒:如 ["1d","1h"] */ + remindBefore?: string[]; + /** 存草稿:true 则不发布 */ + draft?: boolean; deptName?: string; topmost?: number; /** 接收对象:"all" 或 班级 id 逗号串(receiveType=class) */ @@ -384,12 +343,6 @@ export interface HomeworkRequest { timingSendTime?: string; /** 短信发送标记 */ smsFlag?: number; - /** 审批人账号列表(单步:选人即送审;空=不送审直接发布) */ - approvers?: string[]; - /** 单步表决:any=或签(默认)/ all=会签 */ - vote?: "all" | "any"; - /** 多步审批流(高级):[{name,vote,approvers}],传了则忽略 approvers/vote */ - flow?: { name: string; vote: "all" | "any"; approvers: string[] }[]; /** 转发源作业ID */ sourceId?: number; } @@ -419,55 +372,14 @@ export interface HomeworkQuery { orderByTime?: string; } -// ============================ 字典/联系组 ============================ - -/** 系统字典下拉项 */ -export interface DictOption { - k: number | string; - v: string | null; - [key: string]: any; -} - -/** 发文部门管理列表项 */ -export interface DeptManageItem { - SerID: number; - CodeID: string; - Name: string; - Enable: number; -} - -/** 联系组列表项 */ -export interface ContactGroupItem { - serid: number; - GroupName: string; -} - -/** 联系组分页结果 */ -export interface ContactGroupPage { - list: ContactGroupItem[]; - total: number; -} - -/** 联系组用户项 */ -export interface ContactUser { - account: string; - name: string; - userId?: number; - id?: number; -} - -/** 联系组用户分页结果 */ -export interface ContactUserPage { - list: ContactUser[]; - total: number; -} +// ============================ 基础平台 ============================ /** 组织树节点(基础平台返回) */ export interface SchoolTreeNode { [key: string]: any; } -// ============================ 班级 / 审批 ============================ +// ============================ 班级 ============================ /** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */ export interface ClassTreeNode { @@ -479,40 +391,10 @@ export interface ClassTreeNode { [key: string]: any; } -/** 审批列表查询参数 */ -export interface ApprovalQuery { - pageIndex?: number; - pageSize?: number; - /** pending / approved / rejected / all(缺省 pending) */ - status?: string; - keyword?: string; - /** "user" 只看我提交的审批 */ - scope?: string; -} +// ============================ v2 学生端 ============================ -/** 审批操作入参 */ -export interface ApprovalAction { - serid: number; - /** 驳回意见 */ - remark?: string; -} - -// ============================ 审批人 ============================ - -/** 审批人 */ -export interface ApproverVo { - id: number; - account: string; - name: string; - deptId: string; - deptName: string; - enabled: number; -} - -// ============================ v2 学生 / 家长端 ============================ - -/** 当前视角角色 */ -export type ViewerRole = "teacher" | "student" | "parent"; +/** 当前视角角色(家长即登录学生账号,不单列) */ +export type ViewerRole = "teacher" | "student"; /** 学生对某作业的提交/批改态 */ export interface StudentSubmissionInfo { @@ -541,6 +423,8 @@ export interface StudentHomeworkVo { content: string; submitType: string[]; viewOnly: boolean; + hwStatus?: HomeworkStatus; + allowLate?: boolean; my: StudentSubmissionInfo | null; } diff --git a/src/types/homeworkV2.ts b/src/types/homeworkV2.ts new file mode 100644 index 0000000..865e6fe --- /dev/null +++ b/src/types/homeworkV2.ts @@ -0,0 +1,307 @@ +/** + * 课堂作业 V2 类型契约(M5 切换,P0) + * + * 与后端 model/v2/*.java 一一对齐(Jackson 默认驼峰序列化;LocalDateTime 全局 + * "yyyy-MM-dd HH:mm:ss" 字符串,见后端 JacksonConfig)。V1 契约仍在 homework.ts + * 保留服务未切换页面,互不污染:本文件类型一律带 V2 后缀或独立命名。 + */ + +// ============================ 枚举 ============================ + +/** 作业五态(PRD §17.2 上半部) */ +export type HwStatusV2 = "DRAFT_UNPUBLISHED" | "DRAFT_SCHEDULED" | "PUBLISHED" | "CLOSED" | "ARCHIVED"; + +/** 提交五态(PRD §17.2 下半部;NOT_SUBMITTED 为矩阵占位行) */ +export type SubmitStatusV2 = "NOT_SUBMITTED" | "SUBMITTED" | "LATE_SUBMITTED" | "RETURNED" | "REVIEWED"; + +/** 批阅三态 */ +export type ReviewStatus = "PENDING" | "REVIEWED" | "RETURNED"; + +/** 学生列表 tab(GET /student/homeworks?tab=) */ +export type StudentTab = "all" | "todo" | "done" | "closed"; + +/** 教师提醒类型(POST /{id}/remind) */ +export type RemindType = "NOT_READ" | "NOT_SUBMITTED"; + +/** 通知类型(8 类:PRD 6 类 + SCHEDULE_FAILED / DEADLINE_CHANGED 补充) */ +export type NotificationType = + | "HOMEWORK_PUBLISHED" + | "DEADLINE_REMINDER" + | "NOT_SUBMITTED" + | "NOT_READ" + | "REVIEW_DONE" + | "RETURNED" + | "SCHEDULE_FAILED" + | "DEADLINE_CHANGED"; + +// ============================ 公共结构 ============================ + +/** 统一分页响应外壳(对齐 BasePageResult.java) */ +export interface BasePageResult { + total: number; + content: T[]; + pageIndex: number; + pageSize: number; + totalPages: number; + hasNext: boolean; +} + +/** 分页查询入参(对齐 BasePageQuery.java:pageIndex 从 1 起) */ +export interface PageQueryV2 { + pageIndex?: number; + pageSize?: number; + keyword?: string; +} + +/** 提交方式(PRD 图片/文件/两者 → image/attach 组合;text 文本;none 仅查阅) */ +export type SubmitTypeItem = "image" | "attach" | "text" | "none"; + +/** 附件条目(作业附件与提交附件共用;fileUrl 必传,dataURL 会被后端拒绝) */ +export interface AttachmentItem { + /** up_file.id(file/Upload 产物回传) */ + fileId?: number; + fileUrl: string; + /** image / file */ + fileType?: string; + fileName?: string; + sort?: number; +} + +/** 参与班级(HomeworkV2Vo.classes) */ +export interface ClassItem { + classId: string; + className: string; +} + +/** 提交/批阅/阅读进度(应交为分母;打回未重交计入已交) */ +export interface ProgressVo { + /** 应交人数 */ + should: number; + read: number; + submitted: number; + graded: number; + pendingGrade: number; + unsubmitted: number; + unread: number; + /** 未读且未交(高亮口径) */ + unreadUndone: number; + readRate: number; + submitRate: number; + gradeRate: number; +} + +/** 年级→班级树节点(叶子 id=classId,学生数) */ +export interface ClassTreeNodeV2 { + /** 年级节点为 gradeName,班级叶子为 classId */ + id: string; + name: string; + isParent: boolean; + studentCount?: number; + children?: ClassTreeNodeV2[]; +} + +// ============================ 教师端 ============================ + +/** 创建/编辑作业请求(POST / PUT /teacher/homeworks) */ +export interface HomeworkV2Request { + /** 必填 ≤50 字 */ + title: string; + subject?: string; + /** 富文本 */ + description?: string; + classIds: string[]; + attachments?: AttachmentItem[]; + /** 精确到分钟;空 = 仅查阅型 */ + deadlineAt?: string; + submitType: SubmitTypeItem[]; + allowLate?: number; + remind1d?: number; + remind1h?: number; +} + +/** 草稿箱/已发布列表/详情共用视图 */ +export interface HomeworkV2Vo { + id: number; + title: string; + subject?: string; + description?: string; + hwStatus: HwStatusV2; + hwStatusDesc: string; + scheduledAt?: string | null; + publishedAt?: string | null; + deadlineAt?: string | null; + archivedAt?: string | null; + submitType: string[]; + allowLate: number; + remind1d: number; + remind1h: number; + classes: ClassItem[]; + attachments: AttachmentItem[]; + publisherId?: string; + publisherName?: string; + createTime?: string; + updateTime?: string; + /** 非草稿返回 */ + progress?: ProgressVo | null; +} + +/** 教师列表查询(草稿箱/已发布共用) */ +export interface TeacherHomeworkQuery extends PageQueryV2 { + subject?: string; + /** 已发布列表:PUBLISHED/CLOSED/ARCHIVED */ + hwStatus?: string; +} + +/** 汇总统计卡片(GET /{id}/stats) */ +export interface HomeworkStatsVo { + homeworkId: number; + title: string; + hwStatus: HwStatusV2; + hwStatusDesc: string; + total: ProgressVo; + byClass: { classId: string; className: string; progress: ProgressVo }[]; +} + +/** 学生矩阵查询(GET /{id}/students) */ +export interface StudentMatrixQuery extends PageQueryV2 { + classId?: string; + submitStatus?: SubmitStatusV2; + reviewStatus?: ReviewStatus; + /** 只看待批阅快捷过滤 */ + pendingGrade?: boolean; +} + +/** 学生矩阵行(未交学生 submissionId=0 占位) */ +export interface StudentMatrixVo { + submissionId: number; + studentId: string; + studentName: string; + classId: string; + className: string; + /** 0 未读 / 1 已读 */ + isRead: number; + firstReadAt?: string | null; + lastReadAt?: string | null; + submitStatus: SubmitStatusV2; + submittedAt?: string | null; + /** 0/1 */ + isLate: number; + reviewStatus: ReviewStatus; + score?: number | null; + /** 最新提交附件 URL 列表(缩略图) */ + thumbnails?: string[]; +} + +/** 批阅保存请求(score 与 comment 至少一项) */ +export interface ReviewRequest { + score?: number; + comment?: string; + /** 圈划标注图片(P2,先收参) */ + markedImages?: string[]; +} + +/** 打回请求(comment 必填) */ +export interface ReturnRequest { + comment: string; +} + +/** 提交版本条目(教师版本列表/学生结果页共用) */ +export interface SubmissionVersionVo { + submissionId: number; + homeworkId: number; + studentId: string; + studentName: string; + version: number; + /** 0/1 */ + isLatest: number; + submitStatus: SubmitStatusV2; + submitStatusDesc: string; + submittedAt?: string | null; + isLate: number; + text?: string | null; + attachments: AttachmentItem[]; + score?: number | null; + comment?: string | null; + reviewStatus: ReviewStatus; + reviewStatusDesc: string; + reviewedBy?: string | null; + reviewedAt?: string | null; +} + +// ============================ 学生端 ============================ + +/** 学生列表项(GET /student/homeworks) */ +export interface StudentHomeworkV2Vo { + id: number; + title: string; + subject?: string; + teacherName?: string; + deadlineAt?: string | null; + submitType: string[]; + hwStatus: HwStatusV2; + hwStatusDesc: string; + my: StudentMySubmission | null; +} + +/** 我的提交概要(学生列表项内嵌) */ +export interface StudentMySubmission { + submissionId: number; + submitStatus: SubmitStatusV2; + submitStatusDesc: string; + submittedAt?: string | null; + isLate: number; + version: number; + reviewStatus: ReviewStatus; + score?: number | null; + comment?: string | null; +} + +/** 学生作业详情(GET /student/homeworks/{id}) */ +export interface StudentHomeworkDetailV2Vo extends StudentHomeworkV2Vo { + description?: string; + attachments: AttachmentItem[]; + classes: ClassItem[]; + /** 我当前版本的提交附件(未提交为 null) */ + myAttachments?: AttachmentItem[] | null; + myText?: string | null; +} + +/** 学生提交请求(附件只收 file/Upload 产物,拒绝 dataURL) */ +export interface StudentSubmitRequest { + text?: string; + attachments?: AttachmentItem[]; +} + +/** 撤回/重交动作(PUT /student/homeworks/{id}/submissions/{submissionId}) */ +export interface SubmissionActionRequest { + action: "withdraw" | "resubmit"; + text?: string; + attachments?: AttachmentItem[]; +} + +/** 学生批阅结果(GET /{id}/result) */ +export interface StudentResultVo { + homeworkId: number; + homeworkTitle: string; + /** 当前版本批阅结果(未批阅为 null) */ + latest: SubmissionVersionVo | null; + /** 全部版本(新→旧) */ + versions: SubmissionVersionVo[]; +} + +// ============================ 通知 ============================ + +/** 通知列表项(GET /notifications) */ +export interface NotificationVo { + id: number; + type: NotificationType; + typeDesc: string; + title: string; + content?: string; + homeworkId?: number | null; + relatedId?: number | null; + /** 0/1 */ + isRead: number; + readAt?: string | null; + createTime?: string; +} diff --git a/src/utils/menuRoute.ts b/src/utils/menuRoute.ts index 8ad49d8..601496e 100644 --- a/src/utils/menuRoute.ts +++ b/src/utils/menuRoute.ts @@ -41,13 +41,42 @@ const NAME_ALIASES: Record = { 已发作业: "/list", 已收作业: "/accept", 作业列表: "/homeworklist", - 审批列表: "/approval", - 作业审批: "/approval", - 审核设置: "/approval-setting", - 审批设置: "/approval-setting", 统计分析: "/stats", }; +/** + * 已下线模块:课程作业不做审批(§6)、不开放应用设置(§7)。 + * 页面与路由已删除,但平台模块配置(GetUserMod)仍可能下发这些模块, + * 若不在前端剔除,侧栏会出现「点了没反应」的死菜单(resolveMenuPage 返回空 path)。 + * modId 与名称双重命中:平台侧改 modId 或改名后仍能兜住。 + */ +export const DROPPED_MOD_IDS: ReadonlySet = new Set([5, 6, 21]); +const DROPPED_NAMES: ReadonlySet = new Set([ + "审批", + "审批列表", + "审批设置", + "审批人管理", + "审核", + "审核列表", + "审核设置", + "应用设置", + "作业设置", +]); + +/** 剔除已下线模块(含 children),按 modId ?? id 或名称命中;返回新数组,不修改入参 */ +export function dropDroppedModules( + list: T[] | null | undefined +): T[] { + if (!Array.isArray(list)) return []; + return list + .filter((m) => { + const key = Number((m?.modId ?? m?.id) as number); + if (Number.isFinite(key) && DROPPED_MOD_IDS.has(key)) return false; + return !DROPPED_NAMES.has(String(m?.name ?? "").trim()); + }) + .map((m) => (Array.isArray(m.children) && m.children.length ? { ...m, children: dropDroppedModules(m.children) } : m)); +} + const EXTERNAL_RE = /^(https?:)?\/\//i; /** 从平台原始配置里提取内部路由候选串(去掉 hash、query、尾斜杠) */ diff --git a/src/views/homework/Grade.vue b/src/views/homework/Grade.vue index 095832a..b3f1773 100644 --- a/src/views/homework/Grade.vue +++ b/src/views/homework/Grade.vue @@ -4,6 +4,20 @@ import { useRoute, useRouter } from "vue-router"; import { Message } from "view-ui-plus"; import { homeworkApi } from "@/api/module/homework"; import type { HomeworkVo, SubmissionBrief, SubmissionDetailVo } from "@/types/homework"; +import config from "@/assets/config/host.config"; + +const serviceBase = config.isMock ? "" : config.serviceHost; +/** 学生提交图片:可能是可直接显示的 URL/dataURL,或 fileid(回退拼预览地址) */ +function previewUrl(v: number | string) { + const s = String(v); + if (s.startsWith("data:") || s.startsWith("http")) return s; + return `${serviceBase}/api/file/Preview?id=${s}`; +} +const imgPreview = reactive({ open: false, url: "" }); +function zoom(url: string) { + imgPreview.url = url; + imgPreview.open = true; +} const route = useRoute(); const router = useRouter(); @@ -170,13 +184,14 @@ onMounted(async () => {
提交时间:{{ detail.submittedAt || "—" }}
-
- 图片: - 图{{ ii + 1 }} +
+
+ +
附件: - 附件{{ ai + 1 }} + 附件{{ ai + 1 }}
@@ -201,10 +216,10 @@ onMounted(async () => {
- +
- - + +
@@ -213,10 +228,22 @@ onMounted(async () => { + + + +
+
+ diff --git a/src/views/homework/NewHomework.vue b/src/views/homework/NewHomework.vue index e9c81f2..4b4ff2d 100644 --- a/src/views/homework/NewHomework.vue +++ b/src/views/homework/NewHomework.vue @@ -1,78 +1,82 @@ diff --git a/src/views/homework/Submissions.vue b/src/views/homework/Submissions.vue index 0fdaee3..a0a3754 100644 --- a/src/views/homework/Submissions.vue +++ b/src/views/homework/Submissions.vue @@ -4,6 +4,20 @@ import { useRoute } from "vue-router"; import { Message } from "view-ui-plus"; import { homeworkApi } from "@/api/module/homework"; import type { SubmissionsResultVo, SubmissionBrief, HomeworkVo } from "@/types/homework"; +import config from "@/assets/config/host.config"; +import Utils from "@/utils"; + +const serviceBase = config.isMock ? "" : config.serviceHost; +function previewUrl(v: number | string) { + const s = String(v); + if (s.startsWith("data:") || s.startsWith("http")) return s; + return `${serviceBase}/api/file/Preview?id=${s}`; +} +const imgPreview = reactive({ open: false, url: "" }); +function zoom(url: string) { + imgPreview.url = url; + imgPreview.open = true; +} const route = useRoute(); const serid = ref(Number(route.query.serid) || 0); @@ -78,13 +92,25 @@ const stuColumns = [ title: "操作", width: 110, align: "center", - render: (h: any, p: any) => h("Button", { type: "text", size: "small", onClick: () => openGrade(p.row as SubmissionBrief) }, () => (p.row.hasGrade ? "再批" : "批改")), + render: (h: any, p: any) => Utils.operatingHandle(h, p.row.hasGrade ? "再批" : "批改", p.row.hasGrade ? "icon-edit" : "icon-chakan", () => openGrade(p.row as SubmissionBrief)), }, ]; /* ============ 内联批改(走 Grade 接口,核心闭环即时批改) ============ */ -const gradeModal = reactive({ open: false, subId: 0, studentName: "", mode: "grade", action: "grade", score: "" as string | number, level: "A", comment: "" }); -function openGrade(stu: SubmissionBrief) { +const gradeModal = reactive({ + open: false, + subId: 0, + studentName: "", + mode: "grade", + action: "grade", + score: "" as string | number, + level: "A", + comment: "", + answerText: "", + answerImages: [] as string[], + loadingAnswer: false, +}); +async function openGrade(stu: SubmissionBrief) { gradeModal.open = true; gradeModal.subId = stu.subId; gradeModal.studentName = stu.studentName; @@ -93,6 +119,18 @@ function openGrade(stu: SubmissionBrief) { gradeModal.score = stu.score ?? ""; gradeModal.level = stu.level || "A"; gradeModal.comment = ""; + gradeModal.answerText = ""; + gradeModal.answerImages = []; + gradeModal.loadingAnswer = true; + try { + const d = await homeworkApi.submissionDetail(stu.subId).send(); + gradeModal.answerText = d.text || ""; + gradeModal.answerImages = (d.images || []).map((v: any) => String(v)).filter(Boolean); + } catch (e: any) { + Message.error(e.message || "加载作答失败"); + } finally { + gradeModal.loadingAnswer = false; + } } async function doGrade(action: "grade" | "redo" | "excellent") { const payload: any = { subId: gradeModal.subId, action }; @@ -166,7 +204,20 @@ function onSave() { - + +
+
学生作答
+ + +
+
@@ -192,15 +243,67 @@ function onSave() {
+ + + +
+