feat(M5-P4): mock V2 化 + 伪接口清理 + vite 代理 + 走查修复
- mock 整体重写为「V2 单一存储 + V1 适配层」:教师 19 / 学生 6 / 通知 3 全端点,
V1 冻结 7 端点由同一存储派生(与后端同一张 homework 表一致);
提交内核对齐后端 doSubmit(版本翻转/逾期分流/附件 dataURL 拒绝/
仅查阅拒绝),submit/action 返回 {submissionId} 契约对齐
- 删除伪接口 mock 与客户端方法:Submissions/SubmissionDetail/Grade/Remind/
MyHomeworks/MySubmission/Submit/Create/Edit/InitializeRed/UserPull/
SetTopmost/Expedite/ReceiveUsers/List/GetClassTree(真实环境必 404)
- types/homework.ts 收敛为 V1 冻结契约(删 11 个孤儿类型)
- 删除被切换孤儿化的组件:HomeworkUpload/ClassCheckInput/UserCheckModal
- vite 代理指向本地 8102(M5 联调目标,远程测试环境保留注释)
- 走查修复:TeacherHomeworks 操作列改模板插槽(render 函数 Dropdown 插槽
在 view-ui-plus 下失效导致菜单内联);ReviewModal :value/@input 改
modelValue(Vue2 API 残留导致弹窗打不开);StudentHomeworks 重新提交
按钮补 canSubmit 门控(已截止且不允许补交时后端必拒)
- 验收:dev:mock 冒烟 58/58 PASS;构建 EXIT=0;tsc 47 条均为存量(基线 65)
且无引用已删符号;浏览器走查 9 页面 + 批阅保存闭环 + 3 秒阅读上报通过
This commit is contained in:
@@ -27,3 +27,6 @@ yarn.lock
|
||||
|
||||
# scratch
|
||||
.temp
|
||||
|
||||
# 改动前本地备份(版本迭代时人工留存,不进版本库)
|
||||
.backup/
|
||||
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
# 课程作业模块 · 前后端 Bug 检测与修复计划任务
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 前端 | `E:\YHXT\view\CourseHomework`(Vue3 + Vite + TS + view-ui-plus + alova) |
|
||||
| 后端 | `E:\YHXT\java\ClassHomeWork`(Spring Boot 2.7 + JPA,`/{platform}/…` 统一前缀,`platform=api`) |
|
||||
| 检测日期 | 2026-09-16 |
|
||||
| 检测方式 | 静态检视 + 契约逐字段对照 + 构建/类型检查 |
|
||||
| 文档性质 | 计划任务文档(检测项清单)+ 执行记录(修复与回归结果) |
|
||||
|
||||
---
|
||||
|
||||
## 0. 检测范围与方法
|
||||
|
||||
### 0.1 范围
|
||||
|
||||
| 层 | 覆盖对象 |
|
||||
|---|---|
|
||||
| 前端契约层 | `src/types/homework.ts`、`src/types/homeworkV2.ts` |
|
||||
| 前端 API 层 | `src/api/module/{homework,teacherHomework,studentHomework,notification}.ts`、`src/utils/alova/*` |
|
||||
| 前端页面 | `src/views/homework/*.vue`(9 个页面) |
|
||||
| 前端组件/路由 | `src/components/{ReviewWorkbench,ReviewModal,AttachmentUpload,layout}.vue`、`src/router/routes.ts`、`src/utils/menuRoute.ts`、`src/App.vue` |
|
||||
| Mock | `src/assets/mock/homeworkapi.ts` |
|
||||
| 后端接口面 | `controller/api/{TeacherHomeworkV2,StudentHomework,Notification,Homework,File,BaseApi}Controller` |
|
||||
| 后端模型 | `model/homework/*`、`model/v2/*`、`model/BasePage*` |
|
||||
| 后端服务 | `service/impl/{TeacherHomeworkV2,StudentHomework,Homework,HomeworkProgressCalculator}Service` |
|
||||
|
||||
### 0.2 方法
|
||||
|
||||
1. **静态检测**:`vite build`(生产构建)、`vue-tsc --noEmit`(类型检查)。
|
||||
2. **契约逐字段对照**:前端 `types/*.ts` ↔ 后端 `model/**/*.java` 字段名与类型逐一对照。
|
||||
3. **端点对照**:前端 API 客户端方法与后端 Controller `@RequestMapping/@GetMapping/@PostMapping` 逐一对照(路径、动词、出入参)。
|
||||
4. **引用完整性**:已删除组件残留引用、未使用组件、未定义全局符号。
|
||||
5. **运行期口径**:状态机(`hwStatus` 五态 / `submitStatus` 五态 / `reviewStatus` 三态)在前后端的判定一致性。
|
||||
|
||||
### 0.3 检测项清单(共 24 项)
|
||||
|
||||
| 编号 | 检测项 | 方法 |
|
||||
|---|---|---|
|
||||
| C-01 | 生产构建是否通过 | `vite build --mode base` |
|
||||
| C-02 | TS 类型检查是否通过 | `vue-tsc --noEmit` |
|
||||
| C-03 | 已删除组件是否仍被引用 | grep 残留引用 |
|
||||
| C-04 | 是否存在未使用的死组件 | 反向引用计数 |
|
||||
| C-05 | 前端定义的接口路径后端是否真实存在 | 端点对照 |
|
||||
| C-06 | 路径参数名是否与后端 `@PathVariable` 一致 | 端点对照 |
|
||||
| C-07 | `@RequestBody` 是否用 POST/PUT + JSON | 端点对照 |
|
||||
| C-08 | 分页外壳字段是否一致 | `BasePageResult` 对照 |
|
||||
| C-09 | 分页入参语义是否一致(pageIndex 起始值) | `BasePageQuery` 对照 |
|
||||
| C-10 | V2 作业 VO 字段是否一致 | `HomeworkV2Vo` 对照 |
|
||||
| C-11 | V2 学生 VO 字段是否一致 | `StudentHomeworkVo/DetailVo` 对照 |
|
||||
| C-12 | 学生矩阵 VO 字段是否一致 | `StudentMatrixVo` 对照 |
|
||||
| C-13 | 提交版本 VO 字段是否一致 | `SubmissionVersionVo` 对照 |
|
||||
| C-14 | 通知 VO / 未读数是否一致 | `NotificationPageResult` 对照 |
|
||||
| C-15 | V1 作业列表 VO 字段是否一致 | `HomeworkVo` 对照 |
|
||||
| C-16 | V1 详情 VO 字段是否一致 | `HomeworkDetailVo` 对照 |
|
||||
| C-17 | V1 统计 VO 字段是否一致 | `HomeworkStatisticsVo` 对照 |
|
||||
| C-18 | 时间格式是否一致(全局 `yyyy-MM-dd HH:mm:ss`) | `JacksonConfig` + 前端解析 |
|
||||
| C-19 | 附件 URL 是否可解析(相对/绝对、contextPath) | 各页面 `previewUrl/downUrl` |
|
||||
| C-20 | 状态机判定是否前后端一致(可提交/可撤回/可编辑) | 页面 `canSubmit*` vs Service 校验 |
|
||||
| C-21 | 是否存在重复函数声明/无界缓存 | 代码检视 |
|
||||
| C-22 | 组件挂载期是否存在异步竞态 | 代码检视 |
|
||||
| C-23 | 构建/发布配置是否自洽(outDir、env、产物) | `vite.config.ts` + `.env.*` |
|
||||
| C-24 | 工作区未提交改动是否引入噪音 | `git status/diff` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 检测结论摘要
|
||||
|
||||
| 编号 | 问题 | 级别 | 影响面 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| B-01 | 学生端缺少 `allowLate` 字段,导致「截止后补交」能力在真实环境完全失效 | **P0** | 学生列表/详情/快捷提交 | 已修复 |
|
||||
| B-02 | `ReviewWorkbench` 附件 URL 未拼 `serviceHost`,线上图片/下载 404 | **P0** | 教师批阅工作台 | 已修复 |
|
||||
| B-03 | `HomeworkStatisticsVo` 前端声明 5 个幻影字段,后端从未返回 → 统计页 4 张卡片恒为 0/0%/空表 | **P0** | `/stats` 作业统计 | 已修复(前端降级)+ 后端补字段 |
|
||||
| B-04 | `HomeworkVo` 前端声明 `progress` 字段,后端从未返回 → 「全部作业」页 3 列进度恒为「—」 | **P0** | `/homeworklist` 全部作业 | 已修复(前端降级)+ 后端补字段 |
|
||||
| B-05 | `HomeworkDetailVo` 前端声明 `receiveText`,后端从未返回 → 详情页「接收班级」恒显示「全部班级」 | **P1** | `/homeworklist/detail` | 已修复(后端补字段 + 前端兜底) |
|
||||
| B-06 | `TeacherHomeworks.vue` 4 个行操作函数重复声明 | **P1** | `/list` 我布置的 | 已修复 |
|
||||
| B-07 | `TeacherHomeworks.vue` `moreActions` Map 每次重渲染累积、永不回收 | **P1** | `/list` 我布置的 | 已修复 |
|
||||
| B-08 | `NewHomework.vue` 编辑回显与班级树加载存在竞态,导致「全部班级」勾选态误判 | **P1** | `/list/new` 发作业 | 已修复 |
|
||||
| B-09 | `src/api/index.ts` 引用未定义的 `GlobalFetch()`,且 baseURL 硬编码远程域名 | **P2** | 潜在全局 auto-import 污染 | 已修复 |
|
||||
| B-10 | 7 个死组件 + 1 个 `index copy.vue` 残留,`auto-import` 扫描目录存在误引入风险 | **P2** | 构建体积/可维护性 | 已记录(不删除) |
|
||||
| B-11 | `.backup/` 未纳入 `.gitignore`,污染 `git status` | **P2** | 版本管理 | 已修复 |
|
||||
| B-12 | `routes.ts` 出现仅换行符差异的伪改动 | **P2** | 版本管理噪音 | 已记录 |
|
||||
| B-13 | `baseApi.setRole` 指向后端不存在的 `/api/base/SetRole`,真实环境每次跨角色路由切换都会 404 | **P2** | 控制台噪音 | 已记录(设计如此) |
|
||||
| B-14 | `vue-tsc` 在 Node 24 下崩溃(`Search string not found: "/supportedTSExtensions = .*(?=;)/"`),类型门禁实际失效 | **P1** | 全部 TS 代码 | 已记录(需换 Node 18/20 或升级 vue-tsc) |
|
||||
| C-01 | 生产构建通过(39.66s,exit 0) | — | — | 通过 |
|
||||
| C-02 | TS 类型检查**无法执行**(见 B-14) | — | — | 阻塞 |
|
||||
| C-05~C-07 | V2 端点 28 个 + 通知 3 个 + V1 冻结端点路径/动词与后端一致 | — | — | 通过 |
|
||||
| C-08/C-09 | `BasePageResult` 六字段、`pageIndex` 从 1 起前后端一致 | — | — | 通过 |
|
||||
| C-10~C-14 | V2 作业/学生/矩阵/版本/通知 VO 字段与后端逐一对应 | — | — | 通过 |
|
||||
| C-18 | 后端 `JacksonConfig` 全局 `yyyy-MM-dd HH:mm:ss`(无 `T`),前端一律 `String(v).slice()` 截取,一致 | — | — | 通过 |
|
||||
| C-23 | `outDir=dist` 直出、`.env.dev/.env.mock` 仅本地生效、产物不含环境值,自洽 | — | — | 通过 |
|
||||
|
||||
**最严重的三条(P0)本质是同一个病根**:前端契约(`types/*.ts`)比后端实际返回**多声明的字段**,被 TypeScript 视为可选(`?:`),又被 `vue-tsc` 崩溃掩盖,于是「编译通过、构建成功、mock 模式下一切正常」——**只有连真实后端才会表现为静默的空值/功能失效**。mock(`homeworkapi.ts`)为了让页面「跑得起来」,主动补齐了这些后端并不存在的字段,进一步掩盖了问题。
|
||||
|
||||
---
|
||||
|
||||
## 2. 缺陷详情与修复方案
|
||||
|
||||
### B-01 【P0】学生端 `allowLate` 字段缺失 → 截止后补交能力失效
|
||||
|
||||
**证据链**
|
||||
|
||||
- 后端 `model/v2/StudentHomeworkVo.java` 字段仅:`id/title/subject/teacherName/deadlineAt/submitType/hwStatus/hwStatusDesc/my`,**无 `allowLate`**。
|
||||
- 后端实体 `entity/Homework.java:188` 确有 `allow_late` 列(`private Integer allowLate = 0`),说明数据存在、只是学生端 VO 未透出。
|
||||
- 后端 `service/impl/StudentHomeworkService.java#copyBase()`(411~433 行)逐字段赋值,**未赋值 allowLate**。
|
||||
- 前端 `views/homework/StudentHomeworks.vue:110`:`if (x.hwStatus === "CLOSED") return x.allowLate === 1;`
|
||||
- 前端 `views/homework/HomeworkAccept.vue:104`:`if (d.hwStatus === "CLOSED") return d.allowLate === 1;`;第 307 行提示语 `v-if="detail.allowLate !== 1"`。
|
||||
- 前端类型 `types/homeworkV2.ts` 的 `StudentHomeworkV2Vo` **也未声明 `allowLate`**(TS 应报错,被 B-14 掩盖)。
|
||||
- mock 学生列表/详情响应同样未输出 `allowLate`。
|
||||
|
||||
**后果**
|
||||
|
||||
`x.allowLate` 恒为 `undefined` → `undefined === 1` 恒 `false`。因此后端即便 `allow_late=1`(`doSubmit` 允许 `LATE_SUBMITTED`),前端也**永远不渲染「补交作业」按钮**,并恒显示「作业已截止,如需补交请联系老师」。这是真实环境下可直接复现的功能性缺陷——接口允许、UI 拦住。
|
||||
|
||||
**修复**
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `ClassHomeWork/model/v2/StudentHomeworkVo.java` | 新增 `private Integer allowLate;`(含注释) |
|
||||
| `ClassHomeWork/service/impl/StudentHomeworkService.java` | `copyBase()` 增加 `vo.setAllowLate(h.getAllowLate());` |
|
||||
| 前端 `types/homeworkV2.ts` | `StudentHomeworkV2Vo` 新增 `allowLate?: number;` |
|
||||
| 前端 `assets/mock/homeworkapi.ts` | 学生列表与详情响应补 `allowLate: n.allowLate ?? 0` |
|
||||
|
||||
---
|
||||
|
||||
### B-02 【P0】`ReviewWorkbench` 附件 URL 未拼 `serviceHost` → 线上 404
|
||||
|
||||
**证据链**
|
||||
|
||||
- `components/AttachmentUpload.vue` 上传成功后写入 `fileUrl: "/api/file/Preview?id=" + fileid`(**相对路径**)。
|
||||
- `views/homework/StudentHomeworks.vue`、`HomeworkAccept.vue`、`Submissions.vue` 展示时均用 `serviceBase`(`config.serviceHost`)拼接相对路径。
|
||||
- 但 `components/ReviewWorkbench.vue`(教师批阅工作台,被 `Grade.vue` 与 `ReviewModal.vue` 共用,即 `/grade` 与 `/submissions` 两个入口):
|
||||
```ts
|
||||
function attUrl(a: AttachmentItem): string {
|
||||
if (/^(https?:)?\/\//.test(a.fileUrl) || a.fileUrl.startsWith("data:")) return a.fileUrl;
|
||||
return a.fileUrl; // ← 相对路径原样返回,未拼 serviceBase
|
||||
}
|
||||
function attDownUrl(a: AttachmentItem): string {
|
||||
return a.fileId ? `/api/file/Down?id=${a.fileId}` : a.fileUrl; // ← 同样缺前缀
|
||||
}
|
||||
```
|
||||
|
||||
**后果**
|
||||
|
||||
本地 `serviceHost` 为空(走 vite 同源代理)时正常;线上 `serviceHost` 为基础平台域名且应用部署于 `/{contextPath}/front/`,相对路径 `/api/...` 会被解析到**站点根**而非后端 contextPath,导致批阅时学生作答图片**裂图**、附件**下载 404**。教师端核心链路(看图给分)受阻。
|
||||
|
||||
**修复**:`ReviewWorkbench.vue` 引入 `@/assets/config/host.config`,按 `config.isMock ? "" : config.serviceHost` 前缀拼接(与其它页面同一范式)。
|
||||
|
||||
---
|
||||
|
||||
### B-03 【P0】统计页幻影字段 → 4 张卡片恒为 0
|
||||
|
||||
**证据链**
|
||||
|
||||
- 前端 `types/homework.ts` 的 `HomeworkStatisticsVo` 声明:`myAssignCount?`、`pendingGrade?`、`avgSubmitRate?`、`byHomework?`、`scoreDist?`。
|
||||
- 后端 `model/homework/HomeworkStatisticsVo.java` 实际字段仅:`total`、`publishedCount`、`totalReadRecords`、`unreadForCurrentUser`、`byPublisher`、`byMonth`。
|
||||
- 后端 `service/impl/HomeworkService.java#statistics()`(702~756 行)只 set 这 6 个字段,**全库 grep `myAssignCount|avgSubmitRate|scoreDist|byHomework` 在 `src/main/java` 下零命中**。
|
||||
- `views/homework/HomeworkStats.vue` 直接消费:4 张卡片 `myAssignCount/pendingGrade/avgSubmitRate/total`、成绩分布 `stat.scoreDist`、各作业进度表 `stat.byHomework`。
|
||||
- mock `/api/homework/Statis`(`homeworkapi.ts:1319`)**主动补齐了**这 5 个字段 → mock 模式下页面完整,真实后端下全空。
|
||||
|
||||
**后果**:真实环境 `/stats` 页显示「我布置的作业 0 / 待批改总量 0 / 平均提交率 0%」、成绩分布条全 0、各作业进度表「暂无数据」。属静默错误,用户会误以为没有数据。
|
||||
|
||||
**修复(双管齐下)**
|
||||
|
||||
1. **前端降级(可验证)**:`HomeworkStats.vue` 在字段缺失时明确呈现「后端未提供该口径」而非伪装成 0;仅在 `scoreDist?.length` / `byHomework?.length` 时渲染区块。
|
||||
2. **后端补字段(additive)**:`HomeworkStatisticsVo` 新增这 5 个字段,`statistics()` 基于 V2 `HomeworkProgressCalculator` 聚合填充。
|
||||
|
||||
---
|
||||
|
||||
### B-04 【P0】「全部作业」页进度列恒空
|
||||
|
||||
**证据链**
|
||||
|
||||
- 前端 `types/homework.ts` 的 `HomeworkVo` 声明 `progress?: HomeworkProgress | null`,`HomeworkList.vue` 用 `progressCell / gradeCell` 渲染「提交进度 / 未读未交 / 批改」三列,并读 `g.read`、`g.submitRate`、`g.pendingGrade`、`g.unread`。
|
||||
- 后端 `model/homework/HomeworkVo.java` **没有 `progress` 字段**;`service/impl/HomeworkService.java#toVo()`(903~920 行)只回填 `deadline/submitType/hwStatus/hwStatusDesc/read/readCount/deptName`。
|
||||
- `#adminList()`(345~362 行,即 `/homework/api/homework/SendList`)只额外填充 `UpFile` 附件串,**未填 progress**。
|
||||
- `/homeworklist` 路由与菜单 `modId=3` 均在生效(`routes.ts`、`config/menu.ts`)。
|
||||
|
||||
**后果**:真实环境下「全部作业」页的提交进度条、未读未交、批改状态三列恒为「—」。
|
||||
|
||||
**修复**
|
||||
|
||||
1. **后端补字段**:`HomeworkVo` 新增 `progress`;`adminList()` 用 `HomeworkProgressCalculator` 聚合填充。
|
||||
2. **前端降级**:进度列在 `progress` 缺失时显示「—」(现有行为已如此),不伪装为 0。
|
||||
|
||||
---
|
||||
|
||||
### B-05 【P1】详情页「接收班级」恒显示「全部班级」
|
||||
|
||||
**证据链**
|
||||
|
||||
- `views/homework/HomeworkDetails.vue`(V1 管理分支):`<Tag color="blue">接收班级:{{ detail.receiveText || "全部班级" }}</Tag>`。
|
||||
- 后端 `model/homework/HomeworkDetailVo.java` **无 `receiveText`**(字段:`serid/title/Contents/DeptID/DeptName/TeaName/EditDate/numcount/readnumcount/ReceiveList/DsfsTime/isDsfs/ChaoSend/tlist`);全库 `grep receiveText src/main/java` 零命中。
|
||||
- 后端实际提供 `ReceiveList`(值 `"ALL"` 或 id 串)与 `tlist`。
|
||||
|
||||
**修复**:后端 `HomeworkDetailVo` 新增 `receiveText` 并在 `getDetailMap()` 填充可读文本(「全部班级」或班级名串);前端保留 `receiveText || "全部班级"` 兜底。
|
||||
|
||||
---
|
||||
|
||||
### B-06 / B-07 【P1】`TeacherHomeworks.vue` 重复声明与无界缓存
|
||||
|
||||
**证据链**
|
||||
|
||||
- `viewDetail / viewSubmissions / toGrade / edit` 四个函数在 `<script setup>` 中**各定义两次**(同一模块作用域重复 `function` 声明)。
|
||||
- `const moreActions = new Map<string, () => void>()`、`let moreSeq = 0` 位于模块级;`pubColumns` 是 `computed`,每次依赖变化重算都会调用 `mItem()` 递增 `moreSeq` 并写入 Map,**Map 只增不减**。
|
||||
|
||||
**后果**:重复声明在 JS 中后者覆盖前者(静默),但违反类型检查(TS `Duplicate function implementation`),也让维护者误判生效分支;Map 在长时间停留/频繁翻页的列表页持续增长(内存缓慢泄漏)。
|
||||
|
||||
**修复**:删除重复的 4 个函数定义;`moreActions` 改为每次 `pubColumns` 重算时重建(局部 Map),避免无界增长。
|
||||
|
||||
---
|
||||
|
||||
### B-08 【P1】`NewHomework.vue` 编辑回显与班级树竞态
|
||||
|
||||
**证据链**
|
||||
|
||||
- `onMounted(loadClasses)` 与 `onMounted(async () => { … await teacherApi.detail(); fillFromV2(d); })` 两次注册,`loadClasses()` 内部 `await teacherApi.classTree()`,**不保证先于详情回显完成**。
|
||||
- `fillFromV2()` 依赖 `allClassIds.value` 判断「是否全部班级」:
|
||||
```ts
|
||||
if (cls.length && cls.length === allClassIds.value.length && cls.every(c => allClassIds.value.includes(c)))
|
||||
allClasses.value = true;
|
||||
```
|
||||
- 若 `allClassIds` 仍为空(`length === 0`),条件恒不成立 → 即使该作业确实覆盖全部班级,也渲染为**未勾选「全部班级」**且多选框中班级列表为空(丢失勾选态)。
|
||||
|
||||
**修复**:把初始化串行为「先 `await loadClasses()`,再拉详情并 `fillFromV2()`」,消除竞态。
|
||||
|
||||
---
|
||||
|
||||
### B-09 【P2】`src/api/index.ts` 引用未定义符号
|
||||
|
||||
**证据链**:`src/api/index.ts` 顶部 `createAlova({ … requestAdapter: GlobalFetch() … })`,但该文件**未导入也未定义 `GlobalFetch`**;且 `baseURL: 'http://base.yunhaixingtong.com/homework'` 硬编码远程域名,与 `host.config.ts` 的运行时配置体系冲突。该文件未被任何模块引用,但 `vite.config.ts` 中 `ViteAutoImport({ dirs: ["src/api"] })` 会扫描该目录并生成全局声明(`auto-imports.d.ts` 中可见 `apiDefinitions/createApis/mountApis/withConfigType`),存在被误自动引入、进而在运行时抛 `GlobalFetch is not defined` 的风险。
|
||||
|
||||
**修复**:删除该废弃入口文件(页面统一走 `api/module/*`,`src/api/index.ts` 与 `apiDefinitions.ts`/`createApis.ts` 为 `alova gen` 遗留)。
|
||||
|
||||
---
|
||||
|
||||
### B-10 / B-11 / B-12 / B-13 / B-14 其余记录项
|
||||
|
||||
| 编号 | 内容 | 处置 |
|
||||
|---|---|---|
|
||||
| B-10 | 死组件:`BschImport.vue`、`GroupTreeSearchPanel.vue`、`MyTreeSearch.vue`、`TablePageAlova.vue`、`TablePageAlovaNew.vue`、`UploadFile.vue`、`UploadImg.vue`、`my_tree.vue`、`TreeNode/index copy.vue` 反向引用均为 0 | **不删除**(可能为后续页面预留),仅登记 |
|
||||
| B-11 | `.backup/` 未在 `.gitignore`,`git status` 出现 `?? .backup/20260916-m5-v2-mock/` | 追加忽略规则 |
|
||||
| B-12 | `src/router/routes.ts` 被 `git status` 标记为已修改,但 `git diff` 内容为空(仅 CRLF/LF 差异) | 记录;建议统一 `core.autocrlf` 或提交时按内容判定 |
|
||||
| B-13 | `App.vue` 每次跨角色路由切换调用 `baseApi.setRole` → 后端无 `/api/base/SetRole`,真实环境 404(已被 `try/catch` 吞掉) | 设计如此(仅 mock 用),保留但已在前端注释标明 |
|
||||
| B-14 | `vue-tsc@1.8.27` + Node 24 崩溃,`npm run build` 不含类型检查 → TS 契约门禁失效(正是 B-01/B-03/B-04 长期未被发现的原因) | 需将 Node 降到 18/20 或升级 `vue-tsc`;建议在 CI 增加类型检查步骤 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 执行计划(修复顺序与验证)
|
||||
|
||||
| 步骤 | 动作 | 验证方式 |
|
||||
|---|---|---|
|
||||
| S1 | 修复 B-01(后端 VO + Service;前端类型 + mock) | 后端字段对照;前端构建 |
|
||||
| S2 | 修复 B-02(ReviewWorkbench 拼 `serviceHost`) | 前端构建;代码走查三条附件路径 |
|
||||
| S3 | 修复 B-04 / B-05(后端 `HomeworkVo.progress`、`HomeworkDetailVo.receiveText` + 服务填充) | 字段对照 + 前端构建 |
|
||||
| S4 | 修复 B-03(统计 VO 补字段 + 前端降级展示) | 字段对照 + 前端构建 |
|
||||
| S5 | 修复 B-06 / B-07(`TeacherHomeworks.vue` 去重、Map 本地化) | 前端构建 |
|
||||
| S6 | 修复 B-08(`NewHomework.vue` 串行初始化) | 前端构建 |
|
||||
| S7 | 修复 B-09(删除废弃 `src/api/index.ts`) | 全局 grep 无引用;前端构建 |
|
||||
| S8 | 修复 B-11(`.gitignore` 追加 `.backup/`) | `git status` 干净 |
|
||||
| S9 | 回归:`vite build --mode base` 全绿 | exit code 0 |
|
||||
| S10 | 更新本文档「执行记录」并汇总遗留风险 | 文档 |
|
||||
|
||||
**约束说明**:本机无 Maven、无 JDK(仅 JRE 8),后端 Java 改动**无法本地编译验证**,仅做「纯增量、机械式」修改(新增字段 + 赋值 + 新注入),需在装有 Maven 的环境中执行 `mvn -q clean compile` 复核。
|
||||
|
||||
---
|
||||
|
||||
## 4. 执行记录
|
||||
|
||||
> 本节由执行过程回填,逐项记录改动文件与验证结果。
|
||||
|
||||
### S1 · B-01 学生端 allowLate 透出
|
||||
|
||||
- [x] `ClassHomeWork/src/main/java/com/yunhai/homework/model/v2/StudentHomeworkVo.java`:新增 `allowLate` 字段
|
||||
- [x] `ClassHomeWork/src/main/java/com/yunhai/homework/service/impl/StudentHomeworkService.java`:`copyBase()` 补 `vo.setAllowLate(...)`
|
||||
- [x] `src/types/homeworkV2.ts`:`StudentHomeworkV2Vo` 新增 `allowLate?: number`
|
||||
- [x] `src/assets/mock/homeworkapi.ts`:学生列表 + 学生详情响应补 `allowLate`
|
||||
|
||||
### S2 · B-02 批阅工作台附件地址
|
||||
|
||||
- [x] `src/components/ReviewWorkbench.vue`:引入 `host.config`,`attUrl/attDownUrl` 统一补 `serviceHost`
|
||||
|
||||
### S3 · B-04 / B-05 后端 V1 VO 补字段
|
||||
|
||||
- [x] `HomeworkVo.java`:新增 `progress`(`model/v2/ProgressVo`)
|
||||
- [x] `HomeworkDetailVo.java`:新增 `receiveText`
|
||||
- [x] `HomeworkService.java`:`adminList()` 填 `progress`;`getDetailMap()` 填 `receiveText`
|
||||
|
||||
### S4 · B-03 统计口径
|
||||
|
||||
- [x] `HomeworkStatisticsVo.java`:新增 `myAssignCount/pendingGrade/avgSubmitRate/byHomework/scoreDist`
|
||||
- [x] `HomeworkService.java`:`statistics()` 聚合填充
|
||||
- [x] `src/views/homework/HomeworkStats.vue`:字段缺失时降级为「—/暂未提供」,不再伪装为 0
|
||||
|
||||
### S5 · B-06 / B-07 我布置的页
|
||||
|
||||
- [x] `src/views/homework/TeacherHomeworks.vue`:删除 4 个重复函数;`moreActions` 改为列重算时重建
|
||||
|
||||
### S6 · B-08 发作业页竞态
|
||||
|
||||
- [x] `src/views/homework/NewHomework.vue`:初始化改为 `await loadClasses()` → 详情回显串行
|
||||
|
||||
### S7 · B-09 废弃入口
|
||||
|
||||
- [x] 删除 `src/api/index.ts`(含 `GlobalFetch` 未定义、硬编码 baseURL)
|
||||
|
||||
### S8 · B-11 忽略规则
|
||||
|
||||
- [x] `.gitignore` 追加 `.backup/`
|
||||
|
||||
### S9 · 回归验证
|
||||
|
||||
见 §5。
|
||||
|
||||
---
|
||||
|
||||
## 5. 回归验证结果
|
||||
|
||||
| 验证项 | 命令 | 结果 |
|
||||
|---|---|---|
|
||||
| 生产构建 | `npx vite build --mode base` | 见下 |
|
||||
| 端点/契约复核 | 字段对照脚本 | 见下 |
|
||||
| Java 编译 | `mvn clean compile` | **未执行**(本机无 Maven/JDK) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 遗留风险与后续建议
|
||||
|
||||
1. **类型门禁形同虚设(B-14,最高优先)**:本次 B-01/B-03/B-04 三条 P0 全部是「类型声明比后端多」的静默错配。恢复 `vue-tsc` 可运行(Node 18/20 或升级 `vue-tsc`)并纳入 CI,是防止同类问题复发的根本手段。
|
||||
2. **mock 与真实后端口径漂移**:`homeworkapi.ts` 主动补齐了后端不存在的字段,使 mock 模式「看起来一切正常」,掩盖真实缺陷。建议以「后端 VO 为唯一真源」生成/校验 mock。
|
||||
3. **后端改动未经编译验证**:S1/S3/S4 涉及 Java 文件,本机无 Maven/JDK,需在构建环境跑 `mvn -q clean compile` 复核;若编译不通过,回退范围仅限本次新增的字段与注入。
|
||||
4. **`/stats` 与「全部作业」仍建议评估退役**:这两页属 V1 冻结面,其课程作业口径字段(提交进度/批改/成绩分布)在 V2 已有权威实现(`teacherApi.stats` / `published` 的 `progress`)。长期建议整体切 V2,避免在 V1 VO 上持续打补丁。
|
||||
5. **`GitHub` 换行符噪音(B-12)**:建议统一 `core.autocrlf` 或在 `.gitattributes` 中固定 `*.ts text eol=lf`,避免无内容变更的文件反复出现在 diff 中。
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createAlova } from 'alova';
|
||||
import { createApis, withConfigType, mountApis } from './createApis';
|
||||
|
||||
export const alovaInstance = createAlova({
|
||||
baseURL: 'http://base.yunhaixingtong.com/homework',
|
||||
requestAdapter: GlobalFetch(),
|
||||
beforeRequest: method => {},
|
||||
responded: res => {
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
|
||||
export const $$userConfigMap = withConfigType({});
|
||||
|
||||
const Apis = createApis(alovaInstance, $$userConfigMap);
|
||||
|
||||
mountApis(Apis);
|
||||
|
||||
export default Apis;
|
||||
+13
-116
@@ -4,45 +4,30 @@ import conf from "@/assets/config/host.config";
|
||||
import type {
|
||||
HomeworkVo,
|
||||
HomeworkDetailVo,
|
||||
HomeworkPageResult,
|
||||
BasePageResult,
|
||||
UpFileResultVo,
|
||||
UpFileVo,
|
||||
InitializeRedVo,
|
||||
NextPrevVo,
|
||||
ReadGroup,
|
||||
HomeworkStatisticsVo,
|
||||
HomeworkRequest,
|
||||
HomeworkQuery,
|
||||
SchoolTreeNode,
|
||||
ClassTreeNode,
|
||||
SubmissionsResultVo,
|
||||
SubmissionDetailVo,
|
||||
GradeRequest,
|
||||
MyHomeworksVo,
|
||||
MySubmissionVo,
|
||||
SubmitRequest,
|
||||
} from "@/types/homework";
|
||||
|
||||
/**
|
||||
* 课程作业接口封装(P0 标准化后新契约,P1-7 TS 契约化)。
|
||||
* 课程作业 V1 冻结接口封装(M5 后待评估退役)。
|
||||
*
|
||||
* 仅保留后端真实存在的 /api/homework/* 端点,服务未切换页面:
|
||||
* - /homeworklist 全部作业(HomeworkList.vue,SendList)
|
||||
* - /homeworklist/detail 管理视角详情(HomeworkDetails.vue V1 分支)
|
||||
* - /stats 作业统计(HomeworkStats.vue,Statis)
|
||||
* - /list/new?fid= 转发回显(GetDetail)
|
||||
* 原 v1 伪接口(Submissions/SubmissionDetail/Grade/Remind/MyHomeworks/MySubmission/
|
||||
* Submit,后端不存在、真实环境必 404)已随 M5 页面切换删除,见 m5-plan §1.2。
|
||||
*
|
||||
* 前缀说明: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. 字典域与联系组域(发文部门管理、自定义联系组)随「应用设置」功能下线整体移除
|
||||
* (见 classwork-frontend-m5-plan.md §7),本文件不再封装 /api/dict/* 与 /api/contacts/*
|
||||
* 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交)
|
||||
* 5. 参数更名:chaoSend→fileIds、topmost→smsFlag
|
||||
* 6. Delete 入参从 ids 逗号串改为 List<Integer> JSON 数组
|
||||
*
|
||||
* 变更要点(P1-7):
|
||||
* 7. 全部方法加返回值泛型标注,替换 Record<string, any>
|
||||
* 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list)
|
||||
*/
|
||||
|
||||
const HOMEWORK = "/api/homework";
|
||||
@@ -65,24 +50,6 @@ function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): Met
|
||||
);
|
||||
}
|
||||
|
||||
/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */
|
||||
function postForm<T = any>(url: string, data?: Record<string, any>): Method<T> {
|
||||
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<T>(
|
||||
"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}`;
|
||||
@@ -103,34 +70,20 @@ class BaseApi {
|
||||
getUserMod() {
|
||||
return get<SchoolTreeNode[]>(`${BASE}/GetUserMod`);
|
||||
}
|
||||
/** 年级→班级树(学生场景:选择通知对象到班级一级) */
|
||||
getClassTree() {
|
||||
return get<ClassTreeNode[]>(`${BASE}/GetClassTree`);
|
||||
}
|
||||
/** 端分离入口:切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */
|
||||
/** 端分离入口(仅 mock):切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */
|
||||
setRole(as: "teacher" | "student" | "parent") {
|
||||
return postJSON<{ role: string }>(`${BASE}/SetRole`, { as });
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkApi {
|
||||
// ========== 收件箱 ==========
|
||||
/** 已收作业列表 */
|
||||
list(params: HomeworkQuery) {
|
||||
return get<HomeworkPageResult<HomeworkVo>>(`${HOMEWORK}/List`, params);
|
||||
}
|
||||
/** 清单:一键全部已读 */
|
||||
initializeRed() {
|
||||
return postForm<InitializeRedVo>(`${HOMEWORK}/InitializeRed`);
|
||||
}
|
||||
|
||||
// ========== 已发/全部作业 ==========
|
||||
// ========== 全部作业(/homeworklist 管理页) ==========
|
||||
/** 已发/全部作业列表(scope="user" 只看自己,不传=全部) */
|
||||
sendList(params: HomeworkQuery) {
|
||||
return get<BasePageResult<HomeworkVo>>(`${HOMEWORK}/SendList`, params);
|
||||
}
|
||||
|
||||
// ========== 详情 ==========
|
||||
// ========== 详情(/homeworklist/detail 管理视角 + /list/new?fid= 转发回显) ==========
|
||||
/** 查看详情(type=1 标读;type=0 编辑不标读) */
|
||||
modify(serid: number, type = 1) {
|
||||
return get<HomeworkDetailVo>(`${HOMEWORK}/Detail`, { serid, type });
|
||||
@@ -147,70 +100,14 @@ class HomeworkApi {
|
||||
orgizeModify(serid: number) {
|
||||
return get<ReadGroup[]>(`${HOMEWORK}/ReadDetail`, { serid });
|
||||
}
|
||||
/** 追加接收人 */
|
||||
receiveUsers(serid: number, users: string) {
|
||||
return postForm<void>(`${HOMEWORK}/ReceiveUsers`, { serid, users });
|
||||
}
|
||||
|
||||
// ========== 发布/编辑/删除 ==========
|
||||
/** 新建作业(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记) */
|
||||
create(data: HomeworkRequest) {
|
||||
return postJSON<number>(`${HOMEWORK}/Create`, data);
|
||||
}
|
||||
/** 编辑作业(JSON body,含 id) */
|
||||
editHomework(data: HomeworkRequest) {
|
||||
return postJSON<void>(`${HOMEWORK}/Edit`, data);
|
||||
}
|
||||
// ========== 维护(管理页) ==========
|
||||
/** 删除作业(ids 数组,支持批量) */
|
||||
deleteHomework(ids: number | string) {
|
||||
// 兼容页面传单个 id 或逗号串,统一转成数组
|
||||
const idArr = String(ids).split(",").map(Number).filter(Boolean);
|
||||
return postJSON<void>(`${HOMEWORK}/Delete`, idArr);
|
||||
}
|
||||
/** 撤回 */
|
||||
userPull(serid: number) {
|
||||
return postForm<void>(`${HOMEWORK}/UserPull`, { serid });
|
||||
}
|
||||
/** 置顶/取消置顶 */
|
||||
setTopmost(serid: number, topmost = 1) {
|
||||
return postForm<void>(`${HOMEWORK}/SetTopmost`, { serid, topmost });
|
||||
}
|
||||
/** 催收未读 */
|
||||
expedite(serid: number) {
|
||||
return postForm<void>(`${HOMEWORK}/Expedite`, { serid });
|
||||
}
|
||||
|
||||
// ========== 课程作业 v1:提交情况 / 批改 / 催交 ==========
|
||||
/** 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo */
|
||||
submissions(serid: number, submitStatus = "all") {
|
||||
return get<SubmissionsResultVo>(`${HOMEWORK}/Submissions`, { serid, submitStatus });
|
||||
}
|
||||
/** 单份提交详情(批改工作台右侧) */
|
||||
submissionDetail(subId: number) {
|
||||
return get<SubmissionDetailVo>(`${HOMEWORK}/SubmissionDetail`, { subId });
|
||||
}
|
||||
/** 批改(JSON body):action=grade/redo/excellent */
|
||||
grade(data: GradeRequest) {
|
||||
return postJSON<void>(`${HOMEWORK}/Grade`, data);
|
||||
}
|
||||
/** 催交未交学生 */
|
||||
remind(serid: number) {
|
||||
return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid });
|
||||
}
|
||||
|
||||
// ========== v2 学生端 ==========
|
||||
/** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */
|
||||
myHomeworks() {
|
||||
return get<MyHomeworksVo>(`${HOMEWORK}/MyHomeworks`);
|
||||
}
|
||||
/** 我对某作业的详情(作业信息 + 我的提交/批改) */
|
||||
mySubmission(serid: number) {
|
||||
return get<MySubmissionVo>(`${HOMEWORK}/MySubmission`, { serid });
|
||||
}
|
||||
/** 提交 / 订正重交(JSON body:serid, text, images, attachments) */
|
||||
submit(data: SubmitRequest) {
|
||||
return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data);
|
||||
}
|
||||
|
||||
// ========== 附件 ==========
|
||||
fileDownUrl = fileDownUrl;
|
||||
|
||||
+1147
-817
File diff suppressed because it is too large
Load Diff
Vendored
-3
@@ -6,7 +6,6 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const $$userConfigMap: typeof import('./api/index')['$$userConfigMap']
|
||||
const Button: typeof import('view-ui-plus')['Button']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const Form: typeof import('view-ui-plus')['Form']
|
||||
@@ -16,8 +15,6 @@ declare global {
|
||||
const Modal: typeof import('view-ui-plus')['Modal']
|
||||
const Select: typeof import('view-ui-plus')['Select']
|
||||
const Table: typeof import('view-ui-plus')['Table']
|
||||
const alovaInstance: typeof import('./api/index')['alovaInstance']
|
||||
const api: typeof import('./api/index')['default']
|
||||
const apiDefinitions: typeof import('./api/apiDefinitions')['default']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApis: typeof import('./api/createApis')['createApis']
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<!-- 作业对象选择:全部班级 / 指定班级(学生场景,复用 UserCheckModal 树选弹窗) -->
|
||||
<div class="check-user-wrap">
|
||||
<div class="check-user-box">
|
||||
<RadioGroup v-model="isAllClass" class="radio" @on-change="onScopeChange">
|
||||
<Radio :disabled="editDisabled" :label="0">{{ isEdit ? "现有班级" : "全部班级" }}</Radio>
|
||||
<Radio :label="1">指定班级</Radio>
|
||||
</RadioGroup>
|
||||
<Input type="text" v-model="classStr" readonly :disabled="!isAllClass" placeholder="选择通知的班级" class="input"></Input>
|
||||
<Button icon="md-add" @click="openPicker" :disabled="!isAllClass"></Button>
|
||||
<Button icon="md-trash" @click="reset" :disabled="!isAllClass"></Button>
|
||||
</div>
|
||||
<UserCheckModal ref="picker" :sources="sources" :userids="tempIds" @on-submit="submit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 班级选择输入(学生场景)。v-model = "all" 或 班级 id 逗号串。
|
||||
* 数据源为「年级 → 班级」树(base/GetClassTree),叶子为班级,弹窗仅收集叶子 id。
|
||||
*/
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { baseApi } from "@/api/module/homework";
|
||||
|
||||
export default {
|
||||
name: "ClassCheckInput",
|
||||
components: { UserCheckModal },
|
||||
props: {
|
||||
modelValue: { type: String, default: "all" },
|
||||
/** 编辑态:全部班级选项禁用为「现有班级」 */
|
||||
isEdit: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
isAllClass: 0 as number,
|
||||
tempIds: [] as number[],
|
||||
classStr: "",
|
||||
editDisabled: false,
|
||||
sources: [{ name: "班级", loader: () => baseApi.getClassTree().send() as Promise<any[]> }],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.syncFromValue(this.modelValue);
|
||||
},
|
||||
methods: {
|
||||
idsFromVal(val: string): number[] {
|
||||
return String(val || "")
|
||||
.split(",")
|
||||
.map((x) => parseInt(x))
|
||||
.filter((x) => !isNaN(x));
|
||||
},
|
||||
syncFromValue(val: string) {
|
||||
if (val === "all") {
|
||||
this.isAllClass = 0;
|
||||
this.classStr = this.isEdit ? "现有班级" : "全部班级";
|
||||
} else if (val === "") {
|
||||
this.isAllClass = 1;
|
||||
this.classStr = "";
|
||||
this.tempIds = [];
|
||||
} else {
|
||||
this.isAllClass = 1;
|
||||
this.tempIds = this.idsFromVal(val);
|
||||
this.classStr = "";
|
||||
}
|
||||
},
|
||||
onScopeChange(type: number) {
|
||||
const tempIds = this.idsFromVal(String(this.modelValue || ""));
|
||||
if (type === 0) {
|
||||
if (this.isEdit) {
|
||||
this.tempIds = tempIds;
|
||||
this.classStr = "现有班级";
|
||||
this.$emit("update:modelValue", String(tempIds));
|
||||
} else {
|
||||
this.tempIds = [];
|
||||
this.classStr = "全部班级";
|
||||
this.$emit("update:modelValue", "all");
|
||||
}
|
||||
} else {
|
||||
if (this.isEdit) this.editDisabled = true;
|
||||
this.tempIds = tempIds;
|
||||
this.classStr = "";
|
||||
this.openPicker();
|
||||
}
|
||||
},
|
||||
openPicker() {
|
||||
(this.$refs.picker as any).show();
|
||||
},
|
||||
reset() {
|
||||
this.tempIds = [];
|
||||
this.classStr = "";
|
||||
this.$emit("update:modelValue", "");
|
||||
},
|
||||
submit(arr: any[]) {
|
||||
this.classStr = arr.map((x) => x.name).join("、");
|
||||
const ids = arr.map((x) => x.id).join(",");
|
||||
this.$emit("update:modelValue", ids || "all");
|
||||
if (!arr.length) {
|
||||
this.isAllClass = 0;
|
||||
this.classStr = "全部班级";
|
||||
this.$emit("update:modelValue", "all");
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(val: string) {
|
||||
this.syncFromValue(val);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.check-user-wrap {
|
||||
width: 100%;
|
||||
.check-user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.radio {
|
||||
width: 180px;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 32px;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div class="homework-upload">
|
||||
<!-- 自定义上传附件(自旧前端 oldfront/src/components/uploadFile.vue 适配) -->
|
||||
<div class="upload-row">
|
||||
<Upload
|
||||
:on-success="encloSuccess"
|
||||
:on-remove="encloRemove"
|
||||
:default-file-list="defaultList"
|
||||
:action="uploadFileUrl"
|
||||
:accept="fileType"
|
||||
:multiple="isMultiple"
|
||||
:before-upload="handleUpload"
|
||||
:format="formatArr"
|
||||
:max-size="maxSize"
|
||||
:on-format-error="handleFormatError"
|
||||
:on-exceeded-size="handleExceeded"
|
||||
>
|
||||
<Button><Icon type="ios-cloud-upload-outline"></Icon>选择文件</Button>
|
||||
</Upload>
|
||||
<span class="upload-tip">提示:附件大小不超过{{ maxFileSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 作业附件上传(自旧前端 oldfront/src/components/uploadFile.vue 适配 Vue3)。
|
||||
* v-model 为已上传附件 fileid 数组;编辑时经 defaultList 回显已有附件。
|
||||
*/
|
||||
import { Message, Homework } from "view-ui-plus";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
export default {
|
||||
name: "HomeworkUpload",
|
||||
props: {
|
||||
defaultList: { type: Array as () => any[], default: () => [] },
|
||||
fileids: { type: Array as () => number[], default: () => [] },
|
||||
fileType: { type: String, default: "" },
|
||||
multiple: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["update:fileids"],
|
||||
data() {
|
||||
return {
|
||||
isMultiple: !this.multiple || false,
|
||||
uploadFileUrl: (config.isMock ? "" : config.serviceHost) + "/api/base/FileUpload",
|
||||
formatArr: ["jpg", "gif", "png", "bmp", "xls", "doc", "txt", "docx", "xlsx", "zip", "rar", "pdf", "ppt", "pptx", "wps"],
|
||||
maxFileSize: "200MB",
|
||||
maxSize: 204800,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
encloSuccess(resp: any, file: any) {
|
||||
if (resp.code !== 0) {
|
||||
Message.error(resp.msg || "上传失败");
|
||||
return;
|
||||
}
|
||||
this.fileids.push(resp.data.fileid);
|
||||
this.$emit("update:fileids", this.fileids);
|
||||
},
|
||||
encloRemove(file: any) {
|
||||
//删除文件:默认列表项带 fileid,刚上传的取 response
|
||||
const list = file.fileid ? file : file.response;
|
||||
let fileid = list?.fileid || list?.data?.fileid;
|
||||
const arry = this.fileids.filter((element: number) => element != fileid);
|
||||
this.$emit("update:fileids", arry);
|
||||
},
|
||||
handleUpload() {
|
||||
if (!this.isMultiple && this.fileids.length > 0) {
|
||||
Message.info("仅允许上传一个附件!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleFormatError() {
|
||||
Homework.info({
|
||||
title: "不支持的上传类型!",
|
||||
desc: "请上传指定的文件类型",
|
||||
});
|
||||
},
|
||||
handleExceeded() {
|
||||
Homework.warning({
|
||||
title: "超出大小限制",
|
||||
desc: "最大文件大小为" + this.maxFileSize + "!",
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.homework-upload {
|
||||
display: block;
|
||||
.upload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
:deep(.ivu-upload) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* 文件列表整体另起一行,每项独占一行,不再与按钮挤在一起 */
|
||||
:deep(.ivu-upload-list) {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
:deep(.ivu-upload-list-file) {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.upload-tip {
|
||||
margin-left: 10px;
|
||||
color: #f90;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,8 +2,8 @@
|
||||
<!-- V2 批阅弹窗:ReviewWorkbench 的弹窗形态(学生矩阵行内批阅用)。
|
||||
批改中心(Grade.vue)直接内嵌 ReviewWorkbench,不经过本组件。 -->
|
||||
<Modal
|
||||
:value="open"
|
||||
@input="$emit('update:open', $event)"
|
||||
:model-value="open"
|
||||
@update:model-value="$emit('update:open', $event)"
|
||||
:title="`批阅 · ${student?.studentName || ''}`"
|
||||
width="640px"
|
||||
:mask-closable="false"
|
||||
|
||||
@@ -91,6 +91,10 @@ import { ref, reactive, computed, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { teacherApi } from "@/api/module/teacherHomework";
|
||||
import type { AttachmentItem, SubmissionVersionVo, StudentMatrixVo } from "@/types/homeworkV2";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
/** 附件地址前缀:本地 dev/mock 走同源代理(空),线上为基础平台域名(含后端 contextPath) */
|
||||
const serviceBase = config.isMock ? "" : config.serviceHost;
|
||||
|
||||
const props = defineProps<{
|
||||
homeworkId: number;
|
||||
@@ -152,13 +156,14 @@ async function load() {
|
||||
const imageAtts = computed(() => (cur.value?.attachments || []).filter((a) => (a.fileType || "").toLowerCase() === "image"));
|
||||
const fileAtts = computed(() => (cur.value?.attachments || []).filter((a) => (a.fileType || "").toLowerCase() !== "image"));
|
||||
|
||||
/** 附件展示地址:fileUrl 已是 /api/file/Preview?id= 相对路径;绝对地址原样 */
|
||||
/** 附件展示地址:fileUrl 可能是 /api/file/Preview?id= 相对路径(需拼 serviceHost),绝对地址原样 */
|
||||
function attUrl(a: AttachmentItem): string {
|
||||
if (/^(https?:)?\/\//.test(a.fileUrl) || a.fileUrl.startsWith("data:")) return a.fileUrl;
|
||||
return a.fileUrl;
|
||||
const u = a.fileUrl || "";
|
||||
if (/^(https?:)?\/\//.test(u) || u.startsWith("data:")) return u;
|
||||
return `${serviceBase}${u}`;
|
||||
}
|
||||
function attDownUrl(a: AttachmentItem): string {
|
||||
return a.fileId ? `/api/file/Down?id=${a.fileId}` : a.fileUrl;
|
||||
return a.fileId ? `${serviceBase}/api/file/Down?id=${a.fileId}` : attUrl(a);
|
||||
}
|
||||
|
||||
const imgZoom = reactive({ open: false, url: "" });
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-model="modal"
|
||||
title="选择用户"
|
||||
width="720px"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
>
|
||||
<Tabs
|
||||
v-if="sources.length > 1"
|
||||
:key="tabKey"
|
||||
type="card"
|
||||
:value="String(tabIndex)"
|
||||
@on-click="tabChange"
|
||||
>
|
||||
<TabPane v-for="(item, index) in sources" :key="index" :label="item.name" :name="index + ''"></TabPane>
|
||||
</Tabs>
|
||||
<AutoComplete
|
||||
@on-search="handleSearch"
|
||||
@on-select="handleSelect"
|
||||
placeholder="输入名字搜索"
|
||||
v-model="searchTreeName"
|
||||
:clearable="true"
|
||||
class="auto-box"
|
||||
>
|
||||
<Option v-for="item in searchResult" :key="item.id" :value="item.name">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="fr info">{{ item.group }}</span>
|
||||
</Option>
|
||||
</AutoComplete>
|
||||
<div class="tree-wrap fix">
|
||||
<div class="tree-panel fl">
|
||||
<div class="tree-box">
|
||||
<div :id="ztreeDomId" class="ztree"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow-box fl">
|
||||
<svg
|
||||
t="1626916477711"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="36"
|
||||
height="36"
|
||||
>
|
||||
<path
|
||||
d="M138.935 256.078v511.844h63.42V256.078h-63.42z m126.842 0v511.844h127.215V256.078H265.777zM885.064 512L583.254 64.323v191.755H456.414v511.844h126.842v191.755L885.064 512z"
|
||||
fill="#2d8cf0"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-panel fl">
|
||||
<div class="title-box fix">
|
||||
<Checkbox @on-change="checkAllUser" v-model="isCheckUserAll">全选</Checkbox>
|
||||
<div class="fr">{{ tempUserList.length }}</div>
|
||||
<a
|
||||
href="javascript:;"
|
||||
@click="delCheckUser"
|
||||
v-if="tempUserList.findIndex((x: any) => x.isCheck) > -1"
|
||||
>删除选中</a
|
||||
>
|
||||
</div>
|
||||
<ul class="user-box">
|
||||
<li
|
||||
v-for="item in tempUserList"
|
||||
:key="item.id"
|
||||
:class="{ active: item.isCheck }"
|
||||
@click="checkUserOne(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span class="info">{{ item.group }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button type="primary" @click="submit">提交</Button>
|
||||
<Button @click="modal = false" style="margin-left: 8px">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*
|
||||
* 选择用户弹窗(自旧前端 oldfront/src/components/userCheck.vue 适配 Vue3)。
|
||||
* 左侧为带复选框的组织树(部门→人员,叶子=人员),勾选/取消按父子级联;
|
||||
* 右侧为已选人员列表,支持全选/删除选中;提交返回去重后的用户 id 数组与用户对象数组。
|
||||
*/
|
||||
import { Message } from "view-ui-plus";
|
||||
import { requireImg } from "@/utils/hook/useImg";
|
||||
|
||||
interface SelectSource {
|
||||
name: string;
|
||||
loader: () => Promise<any[]>;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "UserCheckModal",
|
||||
props: {
|
||||
/** 数据源 tab:{ name, loader },loader 返回 ztree 树数据(children 嵌套,叶子=人员) */
|
||||
sources: {
|
||||
type: Array as () => SelectSource[],
|
||||
default: () => [],
|
||||
},
|
||||
/** 已选用户 id(打开时自动勾选) */
|
||||
userids: {
|
||||
type: Array as () => number[],
|
||||
default: () => [],
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:userids", "on-submit"],
|
||||
data() {
|
||||
return {
|
||||
ztreeDomId: "user_check_tree_" + Math.floor(Math.random() * 1e8),
|
||||
modal: false,
|
||||
tabIndex: 0,
|
||||
/** 每次打开弹窗自增,强制重建 Tabs,避免上次 tab 高亮残留与数据错位 */
|
||||
tabKey: 0,
|
||||
/** 树加载序号:tab 快速切换时只渲染最后一次请求的结果 */
|
||||
treeSeq: 0,
|
||||
isCheckUserAll: false,
|
||||
cacheMap: new Map(),
|
||||
currentTree: [] as any[],
|
||||
searchTreeName: "",
|
||||
searchResult: [] as any[],
|
||||
tempUserList: [] as any[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 当前树扁平化(含父级名称,供搜索展示) */
|
||||
currentTreeFlatten(): any[] {
|
||||
const flatten = (data: any[], pid: number, pname: string): any[] =>
|
||||
data.reduce(
|
||||
(arr: any[], { id, name, children = [], type }: any) =>
|
||||
arr.concat([{ id, name, pid, type, group: pname }], flatten(children, id, name)),
|
||||
[]
|
||||
);
|
||||
return flatten(this.currentTree, 0, "");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal = true;
|
||||
// 重置到第一个 tab:显式赋值 + key 重建 Tabs,保证高亮与数据一致
|
||||
this.tabIndex = 0;
|
||||
this.tabKey++;
|
||||
this.changeEmpty();
|
||||
this.initTree();
|
||||
},
|
||||
tabChange(index: string) {
|
||||
const idx = Number(index);
|
||||
// 严格比较:v-model 场景下 iview 会把值写成字符串 name,
|
||||
// 宽松相等("1" == 1)会误判“未切换”导致 tab 永远不生效
|
||||
if (idx !== 0 && this.tabIndex === idx) return;
|
||||
this.tabIndex = idx;
|
||||
this.changeEmpty();
|
||||
this.initTree();
|
||||
},
|
||||
handleSearch(query: string) {
|
||||
let arr: any[] = [];
|
||||
const searchArr = this.currentTreeFlatten.filter(
|
||||
(x: any) => x.type == 2 && x.name.indexOf(query) != -1
|
||||
);
|
||||
for (const item of searchArr) {
|
||||
!arr
|
||||
.map((x) => this.getSplitId(x.id))
|
||||
.includes(this.getSplitId(item.id)) && arr.push(item);
|
||||
}
|
||||
this.searchResult = arr;
|
||||
},
|
||||
handleSelect(query: string) {
|
||||
if (query.indexOf(",") != -1) query = query.split(",")[0];
|
||||
var treeObj = ($.fn as any).zTree.getZTreeObj(this.ztreeDomId);
|
||||
var node = treeObj.getNodeByParam("name", query, null);
|
||||
if (!!node) treeObj.checkNode(node, true, true, true);
|
||||
},
|
||||
/**
|
||||
* ztree 数据预处理:拼接 pid 保证同人多组时 id 可区分;叶子标记 type=2。
|
||||
* 组/用户判定优先用接口 isParent(组=文件夹),缺失时按有无子节点兜底;
|
||||
* 并设置组/用户区分图标。
|
||||
*/
|
||||
formatTree(tree: any[], level = 0, pid: number = 0) {
|
||||
tree.forEach((element: any) => {
|
||||
element.open = level == 0;
|
||||
element.pid = pid;
|
||||
element.id = element.id + "," + pid;
|
||||
element.checked = false;
|
||||
if (typeof element.isParent !== "undefined") {
|
||||
element.type = element.isParent ? 1 : 2;
|
||||
} else {
|
||||
element.type = element.children && element.children.length > 0 ? 1 : 2;
|
||||
}
|
||||
element.icon = requireImg(
|
||||
element.type == 1 ? "tree-icon-group.png" : "tree-icon-item.png",
|
||||
"image"
|
||||
);
|
||||
if (typeof element.children !== "undefined" && element.children.length > 0) {
|
||||
element.children = this.formatTree(element.children, level + 1, this.getSplitId(element.id));
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
},
|
||||
initTree() {
|
||||
const _this = this;
|
||||
// 竞态守卫:慢请求返回时若已切换 tab,丢弃结果
|
||||
const seq = ++this.treeSeq;
|
||||
const cacheKey = this.tabIndex;
|
||||
const source = this.sources[this.tabIndex];
|
||||
if (!source) return;
|
||||
const cached = this.cacheMap.get(cacheKey);
|
||||
if (cached) {
|
||||
this.createTree(cached);
|
||||
return;
|
||||
}
|
||||
source
|
||||
.loader()
|
||||
.then((data: any[]) => {
|
||||
if (seq !== _this.treeSeq) return;
|
||||
_this.cacheMap.set(cacheKey, data || []);
|
||||
_this.createTree(data || []);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (seq === _this.treeSeq) Message.error(e?.message || "加载组织数据失败");
|
||||
});
|
||||
},
|
||||
createTree(rawData: any[]) {
|
||||
const _this = this;
|
||||
var jtree = JSON.parse(JSON.stringify(rawData));
|
||||
var ftree = _this.formatTree(jtree);
|
||||
_this.currentTree = ftree;
|
||||
|
||||
let treeSetting: any = {
|
||||
view: {
|
||||
dblClickExpand: false,
|
||||
showIcon: true,
|
||||
},
|
||||
check: {
|
||||
enable: true,
|
||||
chkboxType: { Y: "ps", N: "ps" },
|
||||
},
|
||||
callback: {},
|
||||
};
|
||||
/** 勾选变化后同步右侧已选列表:收集勾选节点子树内的所有叶子 */
|
||||
function formatUser(treeNode: any, checked: boolean) {
|
||||
const recursionCheckIds = (tree: any): any[] => {
|
||||
let arrIds: any[] = [];
|
||||
for (const key in tree) {
|
||||
const element = tree[key];
|
||||
if (element.type == 2 && !element.chkDisabled) {
|
||||
arrIds.push({ id: element.id, tid: element.tId });
|
||||
}
|
||||
if (typeof element.children !== "undefined" && element.children) {
|
||||
arrIds = [...arrIds, ...recursionCheckIds(element.children)];
|
||||
}
|
||||
}
|
||||
return arrIds;
|
||||
};
|
||||
const checkIds = recursionCheckIds([treeNode]);
|
||||
const checkUserList = _this.currentTreeFlatten.filter((x: any) =>
|
||||
checkIds.map((x) => x.id).includes(x.id)
|
||||
);
|
||||
const checkUserArr: any[] = [];
|
||||
for (const element of checkUserList) {
|
||||
var findObj = checkIds.find((x) => x.id == element.id);
|
||||
if (
|
||||
!!findObj &&
|
||||
checkUserArr.findIndex((x) => x.id == element.id) === -1 &&
|
||||
_this.tempUserList.findIndex((x) => x.id == element.id) === -1
|
||||
) {
|
||||
checkUserArr.push({ ...element, isCheck: false, tid: findObj.tid });
|
||||
}
|
||||
}
|
||||
if (checked) {
|
||||
_this.tempUserList = _this.tempUserList.concat(checkUserArr);
|
||||
} else {
|
||||
var ids = checkUserList.map((x) => x.id);
|
||||
_this.tempUserList = _this.tempUserList.filter((x) => !ids.includes(x.id));
|
||||
}
|
||||
}
|
||||
treeSetting.callback.onClick = (event: any, treeId: string, treeNode: any) => {
|
||||
let check = treeNode.checked;
|
||||
let treeObj = ($.fn as any).zTree.getZTreeObj(treeId);
|
||||
treeObj.checkNode(treeNode, !check, true);
|
||||
formatUser(treeNode, treeNode.checked);
|
||||
};
|
||||
treeSetting.callback.onCheck = (event: any, treeId: string, treeNode: any) => {
|
||||
formatUser(treeNode, treeNode.checked);
|
||||
};
|
||||
var treeObj = ($.fn as any).zTree.init($("#" + _this.ztreeDomId), treeSetting, ftree);
|
||||
// 回显:已选用户打勾(callbackFlag=true 会触发 onCheck,同步右侧列表)
|
||||
var nodes = treeObj.getNodesByFilter((x: any) =>
|
||||
_this.userids.includes(_this.getSplitId(x.id))
|
||||
);
|
||||
var arr: any[] = [];
|
||||
nodes.forEach((element: any) => {
|
||||
var splitId = _this.getSplitId(element.id); //重复id只选择第一个
|
||||
if (!arr.includes(splitId)) treeObj.checkNode(element, true, true, true);
|
||||
if (_this.isDisabled) treeObj.setChkDisabled(element, true);
|
||||
arr.push(splitId);
|
||||
});
|
||||
},
|
||||
checkAllUser(check: boolean) {
|
||||
this.tempUserList.forEach((element: any) => {
|
||||
element.isCheck = check;
|
||||
});
|
||||
},
|
||||
checkUserOne(item: any) {
|
||||
item.isCheck = !item.isCheck;
|
||||
const tlist = this.tempUserList;
|
||||
this.isCheckUserAll =
|
||||
tlist.length > 0 && tlist.filter((x) => x.isCheck).length == tlist.length;
|
||||
},
|
||||
delCheckUser() {
|
||||
const _this = this;
|
||||
const tlist = _this.tempUserList;
|
||||
const delArr = tlist.filter((item: any) => item.isCheck);
|
||||
var treeObj = ($.fn as any).zTree.getZTreeObj(_this.ztreeDomId);
|
||||
delArr.forEach((element: any) => {
|
||||
var node = treeObj.getNodeByTId(element.tid);
|
||||
treeObj.checkNode(node, false, true, true);
|
||||
});
|
||||
_this.isCheckUserAll = false;
|
||||
},
|
||||
changeEmpty() {
|
||||
this.tempUserList = [];
|
||||
this.searchTreeName = "";
|
||||
this.searchResult = [];
|
||||
},
|
||||
submit() {
|
||||
const _this = this;
|
||||
let idsArr = this.tempUserList.map((x: any) => _this.getSplitId(x.id));
|
||||
idsArr = Array.from(new Set(idsArr));
|
||||
this.$emit("update:userids", idsArr);
|
||||
function delKey(obj: any, ...args: string[]) {
|
||||
args.forEach((v) => delete obj[v]);
|
||||
return obj;
|
||||
}
|
||||
const objArr: any[] = [];
|
||||
idsArr.forEach((element) => {
|
||||
let findObj = _this.tempUserList.find((x: any) => _this.getSplitId(x.id) == element);
|
||||
findObj = delKey({ ...findObj }, "id", "isCheck", "tid", "pid", "type");
|
||||
objArr.push({ ...findObj, id: element });
|
||||
});
|
||||
this.$emit("on-submit", objArr);
|
||||
this.modal = false;
|
||||
},
|
||||
/** 树节点 id 形如 "1,301"(原id,父id),取真实 id */
|
||||
getSplitId(strId: any) {
|
||||
if (typeof strId === "string" && strId.indexOf(",") != -1) return parseInt(strId.split(",")[0]);
|
||||
return strId;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-wrap {
|
||||
height: 430px;
|
||||
position: relative;
|
||||
}
|
||||
.tree-panel {
|
||||
border: 1px solid #eee;
|
||||
width: 280px;
|
||||
height: 430px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tree-box {
|
||||
padding: 4px 0;
|
||||
height: 428px;
|
||||
width: auto;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
}
|
||||
.user-panel {
|
||||
border: 1px solid #eee;
|
||||
width: 280px;
|
||||
.title-box {
|
||||
background: #f7f7f7;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
padding: 0 12px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.user-box {
|
||||
height: 396px;
|
||||
overflow-y: auto;
|
||||
li {
|
||||
line-height: 32px;
|
||||
padding-left: 12px;
|
||||
cursor: pointer;
|
||||
.info {
|
||||
float: right;
|
||||
margin-right: 12px;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
li:hover {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
li.active {
|
||||
background: #e4edf6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.arrow-box {
|
||||
width: 128px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
margin-top: -17px;
|
||||
}
|
||||
.auto-box {
|
||||
width: 200px;
|
||||
margin-bottom: 10px;
|
||||
:deep(.ivu-select-item .info) {
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
:deep(.ztree li) {
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.ztree li a) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.ztree li a:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
:deep(.ivu-auto-complete .ivu-select-dropdown-list) {
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
+7
-202
@@ -1,17 +1,11 @@
|
||||
/**
|
||||
* 课程作业前端类型契约(P1-7)
|
||||
* 课程作业 V1 冻结接口类型契约(M5 后待评估退役,见 m5-plan §2.3)。
|
||||
*
|
||||
* 与后端 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
|
||||
* 仅服务未切换页面:/homeworklist(SendList/Delete)、/homeworklist/detail
|
||||
* (Detail/GetNextPrev/ReadDetail)、/stats(Statis)、/list/new?fid= 转发回显
|
||||
* (GetDetail/ChaoSend)。V2 契约在 types/homeworkV2.ts,两者互不引用。
|
||||
* 与后端 VO/DTO 一一对齐:HomeworkVo/HomeworkDetailVo(@JsonProperty 保留旧字段名)、
|
||||
* BasePageResult、UpFileResultVo/UpFileVo、HomeworkStatisticsVo、HomeworkQuery。
|
||||
*/
|
||||
|
||||
// ============================ 分页外壳 ============================
|
||||
@@ -26,12 +20,6 @@ export interface BasePageResult<T> {
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
/** 作业分页结果(对齐 HomeworkPageResult.java,扩展 unreadCount) */
|
||||
export interface HomeworkPageResult<T> extends BasePageResult<T> {
|
||||
/** 当前用户未读作业总数 */
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
// ============================ 作业列表项 ============================
|
||||
|
||||
/** 作业视图对象(对齐 HomeworkVo.java,Jackson 默认驼峰序列化) */
|
||||
@@ -159,13 +147,6 @@ export interface UpFileVo {
|
||||
topicType: string;
|
||||
}
|
||||
|
||||
// ============================ 一键已读 ============================
|
||||
|
||||
/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */
|
||||
export interface InitializeRedVo {
|
||||
count: number;
|
||||
}
|
||||
|
||||
// ============================ 上一篇/下一篇 ============================
|
||||
|
||||
/** 上一篇/下一篇作业ID */
|
||||
@@ -212,14 +193,11 @@ export interface HomeworkStatisticsVo {
|
||||
scoreDist?: { label: string; key: string; count: number }[];
|
||||
}
|
||||
|
||||
// ============================ 课程作业 v1:提交 / 批改 ============================
|
||||
// ============================ 课程作业 v1 状态与进度 ============================
|
||||
|
||||
/** 作业流程状态 */
|
||||
export type HomeworkStatus = "draft" | "collecting" | "closed" | "done";
|
||||
|
||||
/** 单个提交状态 */
|
||||
export type SubmitStatus = "unsubmitted" | "submitted" | "graded" | "needRedo" | "redone";
|
||||
|
||||
/** 提交/批改进度 */
|
||||
export interface HomeworkProgress {
|
||||
/** 应交人数 */
|
||||
@@ -242,111 +220,8 @@ export interface HomeworkProgress {
|
||||
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;
|
||||
@@ -379,73 +254,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -240,6 +240,8 @@ export interface StudentHomeworkV2Vo {
|
||||
submitType: string[];
|
||||
hwStatus: HwStatusV2;
|
||||
hwStatusDesc: string;
|
||||
/** 截止后是否允许逾期补交:0 否 / 1 是(CLOSED 时决定是否渲染「补交」入口) */
|
||||
allowLate?: number;
|
||||
my: StudentMySubmission | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,20 @@ onMounted(load);
|
||||
/* 分数分布条 */
|
||||
const maxDist = computed(() => Math.max(1, ...(stat.value?.scoreDist || []).map((d) => d.count)));
|
||||
const BAR_COLOR: Record<string, string> = { excellent: "#19be6b", good: "#2d8cf0", pass: "#ff9900", fail: "#ed4014" };
|
||||
/** 后端是否提供课程作业口径数据(老部署未升级时避免把「未返回」伪装成 0) */
|
||||
const hasScoreDist = computed(() => (stat.value?.scoreDist?.length ?? 0) > 0);
|
||||
const hasByHomework = computed(() => (stat.value?.byHomework?.length ?? 0) > 0);
|
||||
|
||||
/** 卡片值:字段缺失显示「—」而非 0,避免被误读为「确实没有作业」 */
|
||||
function cardValue(v: number | undefined | null, suffix = ""): string {
|
||||
return v === undefined || v === null ? "—" : `${v}${suffix}`;
|
||||
}
|
||||
|
||||
const cards = computed(() => [
|
||||
{ label: "我布置的作业", value: stat.value?.myAssignCount ?? 0 },
|
||||
{ label: "待批改总量", value: stat.value?.pendingGrade ?? 0 },
|
||||
{ label: "平均提交率", value: (stat.value?.avgSubmitRate ?? 0) + "%" },
|
||||
{ label: "作业总数", value: stat.value?.total ?? 0 },
|
||||
{ label: "我布置的作业", value: cardValue(stat.value?.myAssignCount) },
|
||||
{ label: "待批改总量", value: cardValue(stat.value?.pendingGrade) },
|
||||
{ label: "平均提交率", value: cardValue(stat.value?.avgSubmitRate, "%") },
|
||||
{ label: "作业总数", value: cardValue(stat.value?.total) },
|
||||
]);
|
||||
|
||||
const HW_STATUS_TEXT: Record<string, string> = { draft: "草稿", collecting: "收集中", closed: "已截止", done: "已完成" };
|
||||
@@ -52,7 +60,7 @@ const HW_STATUS_TEXT: Record<string, string> = { draft: "草稿", collecting: "
|
||||
<!-- 分数分布 -->
|
||||
<div class="block">
|
||||
<div class="block-h">成绩分布(已批改提交)</div>
|
||||
<div class="dist">
|
||||
<div class="dist" v-if="hasScoreDist">
|
||||
<div class="dist-row" v-for="d in stat.scoreDist" :key="d.key">
|
||||
<div class="dist-label">{{ d.label }}</div>
|
||||
<div class="dist-track">
|
||||
@@ -61,12 +69,14 @@ const HW_STATUS_TEXT: Record<string, string> = { draft: "草稿", collecting: "
|
||||
<div class="dist-num">{{ d.count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-empty" v-else>暂无可统计的批阅分数(后端未返回成绩分布口径)</div>
|
||||
</div>
|
||||
|
||||
<!-- 各作业进度 -->
|
||||
<div class="block">
|
||||
<div class="block-h">我布置的作业 · 提交与批改</div>
|
||||
<Table
|
||||
v-if="hasByHomework"
|
||||
:columns="[
|
||||
{ title: '作业', key: 'title', minWidth: 220, tooltip: true },
|
||||
{ title: '学科', key: 'subject', width: 90, align: 'center' },
|
||||
@@ -85,6 +95,7 @@ const HW_STATUS_TEXT: Record<string, string> = { draft: "草稿", collecting: "
|
||||
:data="stat.byHomework || []"
|
||||
no-data-text="暂无需要收集提交记录的作业"
|
||||
></Table>
|
||||
<div class="block-empty" v-else>暂无需要收集提交记录的作业(后端未返回按作业进度口径)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="main-container"><Spin fix /></div>
|
||||
@@ -124,6 +135,11 @@ const HW_STATUS_TEXT: Record<string, string> = { draft: "草稿", collecting: "
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
.block-empty {
|
||||
color: #a6aab0;
|
||||
font-size: 13px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.dist {
|
||||
.dist-row {
|
||||
display: flex;
|
||||
|
||||
@@ -72,7 +72,7 @@ async function loadClasses() {
|
||||
function toggleAll(v: boolean) {
|
||||
if (v) selClasses.value = [...allClassIds.value];
|
||||
}
|
||||
onMounted(loadClasses);
|
||||
/* 班级树由下方统一初始化入口按序加载(fillFromV2 依赖 allClassIds 判定「全部班级」) */
|
||||
|
||||
const ruleValidate = {
|
||||
title: [
|
||||
@@ -147,6 +147,9 @@ function fillFromV2(d: HomeworkV2Vo) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 先加载班级树再回显:fillFromV2 用 allClassIds 判定「全部班级」,
|
||||
// 若并行执行则 allClassIds 仍为空,勾选态会被误判为「未全选」
|
||||
await loadClasses();
|
||||
if (!(isEdit.value || fid.value > 0)) return;
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
|
||||
@@ -272,8 +272,8 @@ function zoom(url: string) {
|
||||
<div class="c-actions">
|
||||
<Button v-if="['NOT_SUBMITTED'].includes(myStatus(row)) && canSubmit(row)" type="primary" icon="md-cloud-upload" size="small" @click="openSubmit(row)">
|
||||
{{ row.hwStatus === "CLOSED" ? "补交作业" : "提交作业" }}</Button>
|
||||
<Button v-else-if="myStatus(row) === 'RETURNED'" type="error" icon="md-refresh" size="small" @click="openSubmit(row)">重新提交</Button>
|
||||
<template v-else-if="['SUBMITTED', 'LATE_SUBMITTED'].includes(myStatus(row)) && row.hwStatus === 'PUBLISHED'">
|
||||
<Button v-else-if="myStatus(row) === 'RETURNED' && canSubmit(row)" type="error" icon="md-refresh" size="small" @click="openSubmit(row)">重新提交</Button>
|
||||
<template v-else-if="['SUBMITTED', 'LATE_SUBMITTED'].includes(myStatus(row)) && canSubmit(row) && row.hwStatus === 'PUBLISHED'">
|
||||
<Button ghost icon="md-create" size="small" @click="openSubmit(row)">修改提交</Button>
|
||||
<Button icon="md-undo" size="small" @click="withdraw(row)">撤回</Button>
|
||||
</template>
|
||||
|
||||
@@ -252,11 +252,6 @@ function del(row: HomeworkV2Vo) {
|
||||
});
|
||||
}
|
||||
|
||||
/* 文本按钮 */
|
||||
function btn(text: string, onClick: () => void) {
|
||||
return h("Button", { type: "text", size: "small", onClick }, () => text);
|
||||
}
|
||||
|
||||
/* ============ 已发布列表列 ============ */
|
||||
const pubColumns = computed(() => [
|
||||
{ title: "序号", type: "index", width: 60, align: "center" },
|
||||
@@ -335,44 +330,21 @@ const pubColumns = computed(() => [
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 250,
|
||||
slot: "action",
|
||||
width: 230,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
render: (h: any, p: any) => {
|
||||
const row = p.row as HomeworkV2Vo;
|
||||
const nodes: any[] = [];
|
||||
nodes.push(btn("提交情况", () => viewSubmissions(row)));
|
||||
if ((row.progress?.pendingGrade ?? 0) > 0) nodes.push(btn("批阅", () => toGrade(row)));
|
||||
const more: any[] = [];
|
||||
if (row.hwStatus === "PUBLISHED") {
|
||||
if ((row.progress?.unread ?? 0) > 0) more.push(mItem("提醒未读", () => remind(row, "NOT_READ")));
|
||||
if ((row.progress?.unsubmitted ?? 0) > 0) more.push(mItem("催交", () => remind(row, "NOT_SUBMITTED")));
|
||||
more.push(mItem("延长截止", () => openExtend(row)));
|
||||
}
|
||||
more.push(mItem("复制", () => copyRow(row)));
|
||||
if (row.hwStatus === "PUBLISHED" || row.hwStatus === "CLOSED") more.push(mItem("归档", () => archive(row)));
|
||||
more.push(mItem("导出报表", () => doExport(row)));
|
||||
nodes.push(
|
||||
h("Dropdown", { transfer: true, onOnClick: (name: string) => onMore(name) }, () => [
|
||||
h("Button", { type: "text", size: "small" }, () => "更多∨"),
|
||||
h("DropdownMenu", { slot: "list" }, () => more),
|
||||
])
|
||||
);
|
||||
return h("div", {}, nodes);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/* 更多菜单回调(闭包 name→action) */
|
||||
const moreActions = new Map<string, () => void>();
|
||||
let moreSeq = 0;
|
||||
function mItem(text: string, action: () => void) {
|
||||
const key = `m${++moreSeq}`;
|
||||
moreActions.set(key, action);
|
||||
return h("DropdownItem", { name: key }, () => text);
|
||||
}
|
||||
function onMore(name: string) {
|
||||
moreActions.get(name)?.();
|
||||
/* 更多菜单分发 */
|
||||
function onPubMore(name: string, row: HomeworkV2Vo) {
|
||||
if (name === "notRead") remind(row, "NOT_READ");
|
||||
else if (name === "notSubmitted") remind(row, "NOT_SUBMITTED");
|
||||
else if (name === "extend") openExtend(row);
|
||||
else if (name === "copy") copyRow(row);
|
||||
else if (name === "archive") archive(row);
|
||||
else if (name === "export") doExport(row);
|
||||
}
|
||||
|
||||
/* ============ 草稿箱列 ============ */
|
||||
@@ -401,18 +373,10 @@ const draftColumns = computed(() => [
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
slot: "draftAction",
|
||||
width: 250,
|
||||
align: "center",
|
||||
fixed: "right",
|
||||
render: (h: any, p: any) => {
|
||||
const row = p.row as HomeworkV2Vo;
|
||||
const nodes: any[] = [];
|
||||
nodes.push(btn("编辑", () => edit(row)));
|
||||
nodes.push(btn("发布", () => publishDraft(row)));
|
||||
if (row.hwStatus === "DRAFT_SCHEDULED") nodes.push(btn("取消定时", () => cancelSchedule(row)));
|
||||
nodes.push(btn("删除", () => del(row)));
|
||||
return nodes;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -462,7 +426,33 @@ const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOper
|
||||
:height="tableHeight"
|
||||
:loading="loading"
|
||||
no-data-text="暂无数据"
|
||||
></Table>
|
||||
>
|
||||
<!-- 已发布行操作 -->
|
||||
<template #action="{ row }">
|
||||
<Button type="text" size="small" @click="viewSubmissions(row)">提交情况</Button>
|
||||
<Button v-if="(row.progress?.pendingGrade ?? 0) > 0" type="text" size="small" @click="toGrade(row)">批阅</Button>
|
||||
<Dropdown transfer @on-click="(name: string) => onPubMore(name, row)">
|
||||
<Button type="text" size="small">更多∨</Button>
|
||||
<template #list>
|
||||
<DropdownMenu>
|
||||
<DropdownItem v-if="row.hwStatus === 'PUBLISHED' && (row.progress?.unread ?? 0) > 0" name="notRead">提醒未读</DropdownItem>
|
||||
<DropdownItem v-if="row.hwStatus === 'PUBLISHED' && (row.progress?.unsubmitted ?? 0) > 0" name="notSubmitted">催交</DropdownItem>
|
||||
<DropdownItem v-if="row.hwStatus === 'PUBLISHED'" name="extend">延长截止</DropdownItem>
|
||||
<DropdownItem name="copy">复制</DropdownItem>
|
||||
<DropdownItem v-if="row.hwStatus === 'PUBLISHED' || row.hwStatus === 'CLOSED'" name="archive">归档</DropdownItem>
|
||||
<DropdownItem name="export">导出报表</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<!-- 草稿箱行操作 -->
|
||||
<template #draftAction="{ row }">
|
||||
<Button type="text" size="small" @click="edit(row)">编辑</Button>
|
||||
<Button type="text" size="small" @click="publishDraft(row)">发布</Button>
|
||||
<Button v-if="row.hwStatus === 'DRAFT_SCHEDULED'" type="text" size="small" @click="cancelSchedule(row)">取消定时</Button>
|
||||
<Button type="text" size="small" @click="del(row)">删除</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="page-panel">
|
||||
<div class="fl ml-15 align-middle"></div>
|
||||
<Page
|
||||
|
||||
+6
-3
@@ -101,10 +101,13 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||
port: 8077,
|
||||
proxy: {
|
||||
// dev 正式模式:serviceHost 为空(同源相对路径 /api/...),经此代理转发到本地后端
|
||||
// (后端 CAS 过滤器对 /api/* 不回 CORS 头,浏览器直连 8070 会被拦,必须同源代理)
|
||||
// (后端 CAS 过滤器对 /api/* 不回 CORS 头,浏览器直连会被拦,必须同源代理)
|
||||
"/api": {
|
||||
// target: "http://localhost:8101",
|
||||
target:"http://base.yunhaixingtong.com/homework/",
|
||||
// M5 联调:本地 ClassHomeWork 后端(application.yml server.port=8102,
|
||||
// 名册种子见后端 resources/sql/v3_seed.sql)
|
||||
target: "http://localhost:8102",
|
||||
// 远程测试环境(本地后端未启动时临时切回):
|
||||
// target: "http://base.yunhaixingtong.com/homework/",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user