Merge branch 'retirado' of http://8.148.151.56:3000/tonytech/kindergarten_java
# Conflicts: # reading-platform-frontend/src/api/school.ts # reading-platform-frontend/src/api/teacher.ts # reading-platform-frontend/src/views/school/tasks/TaskListView.vue # reading-platform-frontend/src/views/teacher/tasks/TaskListView.vue # reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolTaskController.java # reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherTaskController.java # reading-platform-java/src/main/java/com/reading/platform/dto/response/TaskCompletionDetailResponse.java # reading-platform-java/src/main/java/com/reading/platform/service/TaskService.java # reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java
This commit is contained in:
commit
26f55da670
991
docs/design/reading-task-design-v2.md
Normal file
991
docs/design/reading-task-design-v2.md
Normal file
@ -0,0 +1,991 @@
|
||||
# 阅读任务模块 - 详细设计文档
|
||||
|
||||
> 版本:v2.0
|
||||
> 日期:2026-03-20
|
||||
> 状态:待开发
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [需求背景与目标](#一需求背景与目标)
|
||||
2. [用户角色与权限](#二用户角色与权限)
|
||||
3. [核心业务流程](#三核心业务流程)
|
||||
4. [功能需求详解](#四功能需求详解)
|
||||
5. [数据模型设计](#五数据模型设计)
|
||||
6. [API 接口设计](#六api-接口设计)
|
||||
7. [UI/UX 设计规范](#七uiux-设计规范)
|
||||
8. [代码重构计划](#八代码重构计划)
|
||||
9. [开发任务清单](#九开发任务清单)
|
||||
10. [验收标准](#十验收标准)
|
||||
|
||||
---
|
||||
|
||||
## 一、需求背景与目标
|
||||
|
||||
### 1.1 业务背景
|
||||
|
||||
幼儿园教师在完成绘本课后,需要给学生布置家庭阅读任务,建议孩子回家自主阅读相关绘本。本模块旨在实现:
|
||||
|
||||
- 教师端:创建/管理阅读任务,查看家长提交,给出评价反馈
|
||||
- 家长端:接收任务通知,提交完成证明(照片/视频/心得),查看教师评价
|
||||
- 学校端:查看全校阅读任务数据,统计分析,监督教学质量
|
||||
|
||||
### 1.2 产品目标
|
||||
|
||||
| 目标 | 指标 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 简化任务发布流程 | 教师发布任务时间 < 2分钟 | P0 |
|
||||
| 提升家长参与度 | 任务完成率 > 80% | P0 |
|
||||
| 增强家校互动 | 教师反馈率 > 90% | P0 |
|
||||
| 数据可追溯 | 学校可查看所有历史记录 | P1 |
|
||||
|
||||
### 1.3 本期范围
|
||||
|
||||
| 包含 | 不包含(后续迭代) |
|
||||
|------|-------------------|
|
||||
| ✅ 教师端任务管理 | ❌ 家长端绘本阅读功能 |
|
||||
| ✅ 家长端任务查看/提交(照片/视频/心得) | ❌ 绘本付费订阅 |
|
||||
| ✅ 教师端评价反馈(评语/评分) | ❌ 学生端独立 App |
|
||||
| ✅ 学校端数据查看(只读) | ❌ 绘本库/自动推荐 |
|
||||
| ✅ 关联绘本名称(手动填写) | ❌ |
|
||||
|
||||
### 1.4 核心设计原则
|
||||
|
||||
1. **学校端只读**:学校端不创建/编辑任务,仅查看和统计
|
||||
2. **教师端闭环**:创建 → 查看 → 评价 → 推送反馈
|
||||
3. **家长端简洁**:查看 → 提交 → 接收反馈
|
||||
|
||||
---
|
||||
|
||||
## 二、用户角色与权限
|
||||
|
||||
### 2.1 角色定义
|
||||
|
||||
| 角色 | 使用端 | 职责描述 |
|
||||
|------|--------|---------|
|
||||
| **教师** | 教师端 (Web) | 创建和管理阅读任务,查看提交内容,评价反馈 |
|
||||
| **家长** | 家长端 (H5) | 接收任务,提交完成证明,查看教师评价 |
|
||||
| **学校管理员** | 学校端 (Web) | 查看全校数据,统计分析,导出报表(**只读**) |
|
||||
| **学生** | - | 任务执行对象,通过家长端操作 |
|
||||
|
||||
### 2.2 权限矩阵
|
||||
|
||||
| 功能 | 教师 | 家长 | 学校 |
|
||||
|------|------|------|------|
|
||||
| 创建/编辑/删除任务 | ✅ | ❌ | ❌ |
|
||||
| 查看自己创建的任务 | ✅ | - | - |
|
||||
| 查看孩子的任务 | - | ✅ | - |
|
||||
| 查看全校所有任务 | - | - | ✅ |
|
||||
| 提交完成证明 | - | ✅ | ❌ |
|
||||
| 查看提交内容 | ✅ | 自己的 | ✅ |
|
||||
| 评价反馈 | ✅ | ❌ | ❌ |
|
||||
| 查看评价内容 | - | ✅ | ✅ |
|
||||
| 发送提醒 | ✅ | - | ❌ |
|
||||
| 导出报表 | - | - | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 三、核心业务流程
|
||||
|
||||
### 3.1 主流程图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 阅读任务完整流程 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 1. 教师创建任务│ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 2. 系统推送 │─────►│ 3. 家长接收 │ │
|
||||
│ │ 通知家长 │ │ 任务通知 │ │
|
||||
│ └──────────────┘ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 4. 家长陪伴 │ │
|
||||
│ │ 孩子阅读 │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 5. 家长提交 │ │
|
||||
│ │ 照片/视频/心得│ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────┴─────────────────────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 6a. 教师收到 │ │ 6b. 学校可查看│ │
|
||||
│ │ 提交通知 │ │ 提交记录 │ │
|
||||
│ └──────┬───────┘ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 7. 教师评价 │ │
|
||||
│ │ 优秀/通过/需改进│ │
|
||||
│ │ + 评语 + 评分 │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 8. 系统推送 │ │
|
||||
│ │ 评价通知 │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 9. 家长查看 │ │
|
||||
│ │ 教师评价 │ │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 状态流转
|
||||
|
||||
#### 任务状态
|
||||
```
|
||||
DRAFT(草稿)→ PUBLISHED(已发布)→ ARCHIVED(已归档)
|
||||
```
|
||||
|
||||
#### 完成记录状态
|
||||
```
|
||||
PENDING(待提交)→ SUBMITTED(已提交)→ REVIEWED(已评价)
|
||||
```
|
||||
|
||||
#### 评价结果
|
||||
```
|
||||
EXCELLENT(优秀) / PASSED(通过) / NEEDS_WORK(需改进)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、功能需求详解
|
||||
|
||||
### 4.1 教师端
|
||||
|
||||
#### 4.1.1 任务列表页
|
||||
|
||||
**页面路径**:`/teacher/tasks`
|
||||
|
||||
**功能点**:
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 任务列表 | 卡片式展示,显示标题、类型、状态、时间、完成率 | P0 |
|
||||
| 状态筛选 | 进行中/草稿/已归档 | P0 |
|
||||
| 类型筛选 | 阅读/活动/作业 | P0 |
|
||||
| 关键字搜索 | 按标题搜索 | P0 |
|
||||
| 新建任务 | 打开创建弹窗 | P0 |
|
||||
| 编辑任务 | 修改任务内容(未截止前) | P0 |
|
||||
| 删除任务 | 确认后删除 | P0 |
|
||||
| 归档任务 | 将进行中任务归档 | P1 |
|
||||
| 发送提醒 | 向未完成家长发送提醒 | P1 |
|
||||
| 查看完成情况 | 打开完成情况弹窗 | P0 |
|
||||
|
||||
**数据字段**:
|
||||
|
||||
```typescript
|
||||
interface TeacherTask {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK';
|
||||
relatedBookName?: string; // 关联绘本名称
|
||||
relatedCourseId?: number;
|
||||
relatedCourseName?: string;
|
||||
targetType: 'CLASS' | 'STUDENT';
|
||||
targetIds: number[];
|
||||
targetNames?: string[]; // 班级或学生名称
|
||||
targetCount: number; // 目标人数
|
||||
completionCount: number; // 已提交人数
|
||||
feedbackCount: number; // 已评价人数
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
||||
createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.2 创建/编辑任务弹窗
|
||||
|
||||
**功能点**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 任务标题 | 文本 | ✅ | 最多50字 |
|
||||
| 任务描述 | 文本域 | ❌ | 最多500字 |
|
||||
| 任务类型 | 下拉 | ✅ | 阅读/活动/作业 |
|
||||
| **关联绘本** | 文本 | ❌ | **新增**,手动填写绘本名称 |
|
||||
| 目标类型 | 单选 | ✅ | 班级/学生 |
|
||||
| 选择目标 | 多选 | ✅ | 根据类型显示班级或学生列表 |
|
||||
| 关联课程 | 下拉 | ❌ | 可选,关联课程包 |
|
||||
| 任务时间 | 日期范围 | ✅ | 开始日期-截止日期 |
|
||||
|
||||
#### 4.1.3 完成情况弹窗(重点改造)
|
||||
|
||||
**功能点**:
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 状态统计 | 待完成/已提交/已评价 数量 | P0 |
|
||||
| 状态筛选 | 按提交状态筛选 | P0 |
|
||||
| 学生列表 | 显示学生姓名、班级、提交状态 | P0 |
|
||||
| **查看提交内容** | 点击查看照片/视频/心得详情 | P0 |
|
||||
| **快捷评价** | 一键标记 优秀/通过/需改进 | P0 |
|
||||
| **详细评价** | 弹窗填写评语和评分 | P0 |
|
||||
|
||||
**学生完成记录数据**:
|
||||
|
||||
```typescript
|
||||
interface TaskCompletion {
|
||||
id: number;
|
||||
taskId: number;
|
||||
studentId: number;
|
||||
student: {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
gender: 'MALE' | 'FEMALE';
|
||||
class: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
status: 'PENDING' | 'SUBMITTED' | 'REVIEWED';
|
||||
photos: string[]; // 照片URL数组
|
||||
videoUrl?: string; // 视频URL
|
||||
audioUrl?: string; // 语音URL
|
||||
content?: string; // 文字心得
|
||||
submittedAt?: string; // 提交时间
|
||||
reviewedAt?: string; // 评价时间
|
||||
feedback?: TaskFeedback; // 教师评价
|
||||
}
|
||||
|
||||
interface TaskFeedback {
|
||||
id: number;
|
||||
completionId: number;
|
||||
teacherId: number;
|
||||
teacherName: string;
|
||||
result: 'EXCELLENT' | 'PASSED' | 'NEEDS_WORK';
|
||||
rating?: number; // 1-5星,可选
|
||||
comment?: string; // 评语
|
||||
createdAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.4 提交详情+评价弹窗(新增)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 张小明的提交 - 《好饿的毛毛虫》 [返回] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📷 阅读照片 │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 📸 │ │ 📸 │ │ 📸 │ 点击查看大图 │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ │
|
||||
│ 🎬 阅读视频(如有) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ ▶️ 播放视频 │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 📝 阅读心得 │
|
||||
│ "孩子很喜欢这本书,读了好几遍..." │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 教师评价 │
|
||||
│ │
|
||||
│ 评价结果: │
|
||||
│ [⭐ 优秀] [✓ 通过] [⚠ 需改进] │
|
||||
│ │
|
||||
│ 评分(可选): ⭐ ⭐ ⭐ ⭐ ⭐ │
|
||||
│ │
|
||||
│ 评语: │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ 阅读很认真,能够复述故事内容,继续保持! │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [取消] [保存并发送给家长] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 家长端(H5)
|
||||
|
||||
#### 4.2.1 任务列表页
|
||||
|
||||
**页面路径**:`/parent/tasks`
|
||||
|
||||
**功能点**:
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 任务列表 | 卡片式展示,按截止时间排序 | P0 |
|
||||
| 状态显示 | 待完成/已提交/已评价 | P0 |
|
||||
| 截止倒计时 | 显示剩余时间,即将截止标红 | P1 |
|
||||
| 孩子切换 | 多孩子家庭切换查看 | P0 |
|
||||
| 点击进入详情 | 跳转任务详情页 | P0 |
|
||||
|
||||
**任务卡片显示**:
|
||||
```
|
||||
┌───────────────────────────────────────────────┐
|
||||
│ ⏰ 明天截止 [待完成] │
|
||||
│ │
|
||||
│ 《好饿的毛毛虫》阅读任务 │
|
||||
│ 李老师 | 大一班 │
|
||||
│ │
|
||||
│ [去完成 →] │
|
||||
└───────────────────────────────────────────────┘
|
||||
|
||||
┌───────────────────────────────────────────────┐
|
||||
│ ✅ 已完成 优秀 ⭐⭐⭐⭐⭐ │
|
||||
│ │
|
||||
│ 《小蝌蚪找妈妈》阅读任务 │
|
||||
│ 王老师 | 大一班 │
|
||||
│ │
|
||||
│ [查看反馈 →] │
|
||||
└───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 4.2.2 任务提交页(重写)
|
||||
|
||||
**页面路径**:`/parent/tasks/:id/submit`
|
||||
|
||||
**功能点**:
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 任务信息展示 | 标题、描述、绘本名称、截止时间 | P0 |
|
||||
| **拍照上传** | 支持拍照或从相册选择,最多9张 | P0 |
|
||||
| **视频上传** | 录制或选择视频,限60秒 | P0 |
|
||||
| **语音上传** | 录制语音,限60秒(P1) | P1 |
|
||||
| **文字心得** | 文本输入,最多500字 | P0 |
|
||||
| 预览已上传内容 | 显示已上传的照片/视频缩略图 | P0 |
|
||||
| 删除已上传内容 | 可删除已上传的照片/视频 | P0 |
|
||||
| 提交按钮 | 校验后提交 | P0 |
|
||||
| 重新提交 | 截止前可修改 | P0 |
|
||||
|
||||
**提交表单数据**:
|
||||
|
||||
```typescript
|
||||
interface TaskSubmitRequest {
|
||||
photos: string[]; // OSS URL数组
|
||||
videoUrl?: string; // OSS URL
|
||||
audioUrl?: string; // OSS URL
|
||||
content?: string; // 文字心得
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.3 评价查看页(新增)
|
||||
|
||||
**页面路径**:`/parent/tasks/:id/feedback`
|
||||
|
||||
**显示内容**:
|
||||
- 教师评价结果(优秀/通过/需改进)
|
||||
- 评分(如有)
|
||||
- 教师评语
|
||||
- 评价时间
|
||||
|
||||
---
|
||||
|
||||
### 4.3 学校端- 只读
|
||||
|
||||
> **重要**:学校端**不能**创建/编辑/删除任务,仅查看数据
|
||||
|
||||
#### 4.3.1 任务列表页(重写)
|
||||
|
||||
**页面路径**:`/school/reading-tasks`
|
||||
|
||||
**筛选功能**(核心):
|
||||
|
||||
| 筛选项 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| 时间范围 | 快捷+自定义 | 今日/本周/本月/本学期/自定义日期 |
|
||||
| 班级 | 多选下拉 | 支持选择多个班级 |
|
||||
| 教师 | 多选下拉 | 支持选择多个教师 |
|
||||
| 任务类型 | 单选下拉 | 阅读/活动/作业/全部 |
|
||||
| 任务状态 | 单选下拉 | 进行中/已截止/已归档/全部 |
|
||||
| 完成率 | 单选下拉 | 高(>80%)/中(50-80%)/低(<50%)/全部 |
|
||||
| 搜索 | 文本框 | 按标题搜索 |
|
||||
|
||||
**已选条件显示**:
|
||||
- 在筛选区下方显示已选条件的标签
|
||||
- 点击标签可移除该条件
|
||||
- 提供"清空全部"按钮
|
||||
|
||||
**列表字段**:
|
||||
|
||||
| 列 | 说明 | 排序 |
|
||||
|----|------|------|
|
||||
| 任务标题 | 点击可查看详情 | ✅ |
|
||||
| 发布教师 | 教师姓名 | ✅ |
|
||||
| 班级 | 目标班级(多个用逗号分隔) | - |
|
||||
| 类型 | 阅读/活动/作业 | - |
|
||||
| 目标人数 | 学生总数 | - |
|
||||
| 提交数 | 已提交/总人数 | ✅ |
|
||||
| 完成率 | 进度条 | ✅ |
|
||||
| 反馈率 | 教师已评价比例 | ✅ |
|
||||
| 发布时间 | yyyy-MM-dd | ✅ |
|
||||
| 截止时间 | yyyy-MM-dd | ✅ |
|
||||
| 状态 | 进行中/已截止 | - |
|
||||
| 操作 | 查看详情 | - |
|
||||
|
||||
#### 4.3.2 任务详情页(新增)
|
||||
|
||||
**页面路径**:`/school/reading-tasks/:id`
|
||||
|
||||
**显示内容**:
|
||||
|
||||
1. 基本信息
|
||||
- 任务标题、描述
|
||||
- 发布教师、班级
|
||||
- 任务类型、关联绘本
|
||||
- 发布时间、截止时间
|
||||
|
||||
2. 完成统计
|
||||
- 目标人数、提交人数、完成率
|
||||
- 待完成/已提交/已评价 数量
|
||||
|
||||
3. 学生完成列表(可下钻查看)
|
||||
- 学生姓名、班级
|
||||
- 提交时间、提交状态
|
||||
- 评价结果(如有)
|
||||
- 操作:查看提交内容
|
||||
|
||||
#### 4.3.3 学生提交详情页(新增)
|
||||
|
||||
**页面路径**:`/school/reading-tasks/:taskId/completions/:completionId`
|
||||
|
||||
**显示内容**:
|
||||
- 提交的照片(可放大查看)
|
||||
- 提交的视频(可播放)
|
||||
- 提交的心得文字
|
||||
- 教师评价(如有)
|
||||
|
||||
> 注意:学校端此页面为**纯只读**,不能进行任何编辑操作
|
||||
|
||||
---
|
||||
|
||||
## 五、数据模型设计
|
||||
|
||||
### 5.1 表结构变更
|
||||
|
||||
#### 5.1.1 Task 表 - 新增字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE task ADD COLUMN related_book_name VARCHAR(200) COMMENT '关联绘本名称';
|
||||
```
|
||||
|
||||
#### 5.1.2 TaskCompletion 表 - 扩展字段
|
||||
|
||||
```sql
|
||||
-- 修改 photos 字段类型(如果原来是 VARCHAR)
|
||||
ALTER TABLE task_completion MODIFY COLUMN photos JSON COMMENT '照片URL数组(JSON)';
|
||||
|
||||
-- 新增字段
|
||||
ALTER TABLE task_completion ADD COLUMN video_url VARCHAR(500) COMMENT '视频URL';
|
||||
ALTER TABLE task_completion ADD COLUMN audio_url VARCHAR(500) COMMENT '语音URL';
|
||||
```
|
||||
|
||||
#### 5.1.3 TaskFeedback 表 - 新建
|
||||
|
||||
```sql
|
||||
CREATE TABLE task_feedback (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
|
||||
completion_id BIGINT NOT NULL COMMENT '完成记录ID',
|
||||
task_id BIGINT NOT NULL COMMENT '任务ID(冗余,方便查询)',
|
||||
student_id BIGINT NOT NULL COMMENT '学生ID(冗余)',
|
||||
teacher_id BIGINT NOT NULL COMMENT '教师ID',
|
||||
result VARCHAR(20) NOT NULL COMMENT '评价结果:EXCELLENT/PASSED/NEEDS_WORK',
|
||||
rating INT COMMENT '评分 1-5(可选)',
|
||||
comment TEXT COMMENT '评语',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at DATETIME DEFAULT NULL COMMENT '删除时间',
|
||||
|
||||
INDEX idx_completion_id (completion_id),
|
||||
INDEX idx_task_id (task_id),
|
||||
INDEX idx_teacher_id (teacher_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务评价表';
|
||||
```
|
||||
|
||||
### 5.2 实体类定义
|
||||
|
||||
#### Task.java(变更)
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("task")
|
||||
public class Task extends BaseEntity {
|
||||
private Long tenantId;
|
||||
private String title;
|
||||
private String description;
|
||||
private String type; // taskType
|
||||
private String relatedBookName; // 【新增】关联绘本名称
|
||||
private Long courseId;
|
||||
private Long creatorId;
|
||||
private String creatorRole;
|
||||
private LocalDate startDate;
|
||||
private LocalDate dueDate;
|
||||
private String status;
|
||||
private String attachments;
|
||||
}
|
||||
```
|
||||
|
||||
#### TaskCompletion.java(变更)
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("task_completion")
|
||||
public class TaskCompletion extends BaseEntity {
|
||||
private Long taskId;
|
||||
private Long studentId;
|
||||
private String status;
|
||||
private String photos; // JSON数组
|
||||
private String videoUrl; // 【新增】
|
||||
private String audioUrl; // 【新增】
|
||||
private String content;
|
||||
private LocalDateTime submittedAt;
|
||||
private LocalDateTime reviewedAt;
|
||||
}
|
||||
```
|
||||
|
||||
#### TaskFeedback.java(新增)
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("task_feedback")
|
||||
public class TaskFeedback extends BaseEntity {
|
||||
private Long completionId;
|
||||
private Long taskId;
|
||||
private Long studentId;
|
||||
private Long teacherId;
|
||||
private String result; // EXCELLENT/PASSED/NEEDS_WORK
|
||||
private Integer rating; // 1-5,可选
|
||||
private String comment;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 DTO 定义
|
||||
|
||||
#### TaskCompletionResponse.java
|
||||
|
||||
```java
|
||||
@Data
|
||||
@Builder
|
||||
public class TaskCompletionResponse {
|
||||
private Long id;
|
||||
private Long taskId;
|
||||
private StudentInfo student;
|
||||
private String status;
|
||||
private List<String> photos;
|
||||
private String videoUrl;
|
||||
private String audioUrl;
|
||||
private String content;
|
||||
private LocalDateTime submittedAt;
|
||||
private LocalDateTime reviewedAt;
|
||||
private TaskFeedbackResponse feedback;
|
||||
|
||||
@Data
|
||||
public static class StudentInfo {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String avatar;
|
||||
private String gender;
|
||||
private ClassInfo classInfo;
|
||||
|
||||
@Data
|
||||
public static class ClassInfo {
|
||||
private Long id;
|
||||
private String name;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### TaskFeedbackRequest.java
|
||||
|
||||
```java
|
||||
@Data
|
||||
public class TaskFeedbackRequest {
|
||||
@NotBlank(message = "评价结果不能为空")
|
||||
private String result; // EXCELLENT/PASSED/NEEDS_WORK
|
||||
|
||||
@Min(1) @Max(5)
|
||||
private Integer rating;
|
||||
|
||||
@Size(max = 500, message = "评语最多500字")
|
||||
private String comment;
|
||||
}
|
||||
```
|
||||
|
||||
#### TaskFeedbackResponse.java
|
||||
|
||||
```java
|
||||
@Data
|
||||
@Builder
|
||||
public class TaskFeedbackResponse {
|
||||
private Long id;
|
||||
private String result;
|
||||
private String resultText; // 优秀/通过/需改进
|
||||
private Integer rating;
|
||||
private String comment;
|
||||
private Long teacherId;
|
||||
private String teacherName;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、API 接口设计
|
||||
|
||||
### 6.1 教师端 API
|
||||
|
||||
#### 6.1.1 任务管理
|
||||
|
||||
| 方法 | 路径 | 说明 | 变更 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/v1/teacher/tasks` | 任务列表 | 无变更 |
|
||||
| POST | `/api/v1/teacher/tasks` | 创建任务 | **新增 relatedBookName** |
|
||||
| GET | `/api/v1/teacher/tasks/{id}` | 任务详情 | 无变更 |
|
||||
| PUT | `/api/v1/teacher/tasks/{id}` | 更新任务 | **新增 relatedBookName** |
|
||||
| DELETE | `/api/v1/teacher/tasks/{id}` | 删除任务 | 无变更 |
|
||||
| POST | `/api/v1/teacher/tasks/{id}/archive` | 归档任务 | **新增** |
|
||||
| POST | `/api/v1/teacher/tasks/{id}/remind` | 发送提醒 | 无变更 |
|
||||
|
||||
#### 6.1.2 完成情况与评价(重点变更)
|
||||
|
||||
| 方法 | 路径 | 说明 | 变更 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/v1/teacher/tasks/{id}/completions` | 完成情况列表 | **返回内容扩展** |
|
||||
| GET | `/api/v1/teacher/completions/{id}` | 提交详情 | **新增** |
|
||||
| POST | `/api/v1/teacher/completions/{id}/feedback` | 提交评价 | **新增** |
|
||||
| PUT | `/api/v1/teacher/completions/{id}/feedback` | 修改评价 | **新增** |
|
||||
|
||||
#### 请求/响应示例
|
||||
|
||||
**创建任务请求**:
|
||||
```json
|
||||
{
|
||||
"title": "《好饿的毛毛虫》亲子阅读",
|
||||
"description": "和孩子一起阅读这本经典绘本...",
|
||||
"taskType": "READING",
|
||||
"relatedBookName": "《好饿的毛毛虫》",
|
||||
"targetType": "CLASS",
|
||||
"targetIds": [1, 2],
|
||||
"relatedCourseId": 10,
|
||||
"startDate": "2026-03-20",
|
||||
"endDate": "2026-03-27"
|
||||
}
|
||||
```
|
||||
|
||||
**提交评价请求**:
|
||||
```json
|
||||
{
|
||||
"result": "EXCELLENT",
|
||||
"rating": 5,
|
||||
"comment": "阅读很认真,能够完整复述故事内容,继续保持!"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 家长端 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 变更 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/v1/parent/tasks` | 孩子的任务列表 | 无变更 |
|
||||
| GET | `/api/v1/parent/tasks/{id}` | 任务详情 | 无变更 |
|
||||
| **POST** | `/api/v1/parent/tasks/{id}/submit` | 提交完成 | **重写,支持文件** |
|
||||
| **PUT** | `/api/v1/parent/tasks/{id}/submit` | 修改提交 | **新增** |
|
||||
| **GET** | `/api/v1/parent/completions/{id}/feedback` | 查看评价 | **新增** |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
**提交完成请求**:
|
||||
```json
|
||||
{
|
||||
"photos": [
|
||||
"https://oss.example.com/photo1.jpg",
|
||||
"https://oss.example.com/photo2.jpg"
|
||||
],
|
||||
"videoUrl": "https://oss.example.com/video.mp4",
|
||||
"content": "孩子很喜欢这本书,读了好几遍..."
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 学校端 API
|
||||
|
||||
| 方法 | 路径 | 说明 | 变更 |
|
||||
|------|------|------|------|
|
||||
| **GET** | `/api/v1/school/reading-tasks` | 任务列表(支持多维度筛选) | **重写** |
|
||||
| **GET** | `/api/v1/school/reading-tasks/{id}` | 任务详情 | **新增** |
|
||||
| **GET** | `/api/v1/school/reading-tasks/{id}/completions` | 完成情况列表 | **新增** |
|
||||
| **GET** | `/api/v1/school/completions/{id}` | 学生提交详情 | **新增** |
|
||||
| **GET** | `/api/v1/school/reading-tasks/statistics` | 统计数据 | **新增** |
|
||||
| **GET** | `/api/v1/school/reading-tasks/export` | 导出报表 | **新增** |
|
||||
|
||||
#### 学校端筛选参数
|
||||
|
||||
```typescript
|
||||
interface SchoolTaskQueryParams {
|
||||
// 分页
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
|
||||
// 时间筛选
|
||||
dateType?: 'today' | 'week' | 'month' | 'semester' | 'custom';
|
||||
startDate?: string; // 自定义开始日期
|
||||
endDate?: string; // 自定义结束日期
|
||||
|
||||
// 组织筛选
|
||||
classIds?: number[]; // 班级ID数组
|
||||
teacherIds?: number[]; // 教师ID数组
|
||||
|
||||
// 状态筛选
|
||||
taskType?: 'READING' | 'ACTIVITY' | 'HOMEWORK';
|
||||
taskStatus?: 'PUBLISHED' | 'ARCHIVED';
|
||||
completionRate?: 'high' | 'medium' | 'low';
|
||||
|
||||
// 搜索
|
||||
keyword?: string;
|
||||
|
||||
// 排序
|
||||
sortBy?: 'createdAt' | 'completionRate' | 'feedbackRate';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、UI/UX 设计规范
|
||||
|
||||
### 7.1 评价结果颜色规范
|
||||
|
||||
| 结果 | 颜色 | 标签样式 |
|
||||
|------|------|---------|
|
||||
| EXCELLENT(优秀) | 绿色 `#52c41a` | `<a-tag color="success">` |
|
||||
| PASSED(通过) | 蓝色 `#1890ff` | `<a-tag color="blue">` |
|
||||
| NEEDS_WORK(需改进) | 橙色 `#fa8c16` | `<a-tag color="warning">` |
|
||||
|
||||
### 7.2 评分星星
|
||||
|
||||
- 使用 Ant Design 的 `<a-rate>` 组件
|
||||
- 默认禁用(仅展示),在评价时可编辑
|
||||
- 颜色:金色 `#fadb14`
|
||||
|
||||
### 7.3 照片上传组件
|
||||
|
||||
- 使用 Ant Design 的 `<a-upload>` 组件
|
||||
- 列表模式:`list-type="picture-card"`
|
||||
- 最大数量:9张
|
||||
- 支持预览和删除
|
||||
|
||||
### 7.4 视频上传
|
||||
|
||||
- 限制时长:60秒
|
||||
- 支持格式:mp4, mov
|
||||
- 文件大小:不超过 50MB
|
||||
|
||||
### 7.5 筛选组件规范
|
||||
|
||||
```
|
||||
时间快捷选择:
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ [今日] [本周 ✓] [本月] [本学期] [自定义 ▼] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
多选下拉:
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 班级 ▼ │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ ☑ 全选 │ │
|
||||
│ │ ☑ 大一班 │ │
|
||||
│ │ ☑ 大二班 │ │
|
||||
│ │ ☐ 中一班 │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
已选条件标签:
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 已选条件: │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 本周 ✕ │ │ 大一班 ✕ │ │ 李老师 ✕ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ [清空全部] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、代码重构计划
|
||||
|
||||
### 8.1 需要删除的文件/代码
|
||||
|
||||
| 文件 | 原因 |
|
||||
|------|------|
|
||||
| `school/TaskListView.vue` | 完全重写,改为只读模式 |
|
||||
| `api/school.ts` 中的 `createSchoolTask`, `updateSchoolTask`, `deleteSchoolTask` | 学校端不再需要这些API |
|
||||
|
||||
### 8.2 需要修改的文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `entity/Task.java` | 新增 `relatedBookName` 字段 |
|
||||
| `entity/TaskCompletion.java` | 新增 `videoUrl`, `audioUrl` 字段 |
|
||||
| `dto/request/TaskCreateRequest.java` | 新增 `relatedBookName` 字段 |
|
||||
| `dto/response/TaskResponse.java` | 新增 `relatedBookName` 字段 |
|
||||
| `service/TaskService.java` | 新增评价相关方法 |
|
||||
| `service/impl/TaskServiceImpl.java` | 实现评价相关方法 |
|
||||
| `controller/teacher/TeacherTaskController.java` | 新增评价API |
|
||||
| `controller/parent/ParentTaskController.java` | 重写提交API,新增查看评价API |
|
||||
| `controller/school/SchoolTaskController.java` | 重写为只读API |
|
||||
| `views/teacher/tasks/TaskListView.vue` | 新增关联绘本字段,改造评价功能 |
|
||||
| `views/parent/tasks/TaskListView.vue` | 改造提交功能 |
|
||||
|
||||
### 8.3 需要新增的文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `entity/TaskFeedback.java` | 评价实体类 |
|
||||
| `mapper/TaskFeedbackMapper.java` | 评价Mapper |
|
||||
| `service/TaskFeedbackService.java` | 评价服务接口 |
|
||||
| `service/impl/TaskFeedbackServiceImpl.java` | 评价服务实现 |
|
||||
| `dto/request/TaskFeedbackRequest.java` | 评价请求DTO |
|
||||
| `dto/response/TaskFeedbackResponse.java` | 评价响应DTO |
|
||||
| `dto/response/TaskCompletionResponse.java` | 完成记录响应DTO(扩展) |
|
||||
| `common/mapper/TaskFeedbackMapper.java` | MapStruct Mapper |
|
||||
| `views/school/reading-tasks/TaskListView.vue` | 学校端任务列表(重写) |
|
||||
| `views/school/reading-tasks/TaskDetailView.vue` | 学校端任务详情(新增) |
|
||||
| `views/school/reading-tasks/CompletionDetailView.vue` | 学校端提交详情(新增) |
|
||||
| `views/parent/tasks/TaskSubmitView.vue` | 家长端提交页面(重写) |
|
||||
| `views/parent/tasks/TaskFeedbackView.vue` | 家长端查看评价(新增) |
|
||||
| `views/teacher/tasks/components/CompletionDetailModal.vue` | 教师端提交详情弹窗(新增) |
|
||||
| `views/teacher/tasks/components/FeedbackModal.vue` | 教师端评价弹窗(新增) |
|
||||
| `api/reading-task.ts` | 阅读任务API(新模块,独立于原task) |
|
||||
| `components/TaskFilterPanel.vue` | 通用筛选面板组件 |
|
||||
|
||||
---
|
||||
|
||||
## 九、开发任务清单
|
||||
|
||||
### Phase 1:后端基础改造(2-3天)
|
||||
|
||||
| 任务 | 预估时间 | 依赖 |
|
||||
|------|---------|------|
|
||||
| 数据库迁移脚本(TaskFeedback表、字段扩展) | 0.5天 | 无 |
|
||||
| TaskFeedback 实体类和 Mapper | 0.5天 | 数据库 |
|
||||
| TaskFeedbackService 接口和实现 | 1天 | 实体类 |
|
||||
| 扩展 TaskService(评价相关方法) | 0.5天 | TaskFeedback |
|
||||
| 教师端评价 API | 0.5天 | Service |
|
||||
| 家长端提交 API(支持文件) | 0.5天 | Service |
|
||||
| 学校端只读 API(多维度筛选) | 1天 | Service |
|
||||
|
||||
### Phase 2:教师端改造(2天)
|
||||
|
||||
| 任务 | 预估时间 | 依赖 |
|
||||
|------|---------|------|
|
||||
| 创建任务弹窗添加绘本字段 | 0.5天 | 无 |
|
||||
| 完成情况弹窗改造(显示提交内容) | 0.5天 | 无 |
|
||||
| 新增提交详情弹窗组件 | 0.5天 | 无 |
|
||||
| 新增评价弹窗组件 | 0.5天 | 后端API |
|
||||
|
||||
### Phase 3:家长端改造(2天)
|
||||
|
||||
| 任务 | 预估时间 | 依赖 |
|
||||
|------|---------|------|
|
||||
| 重写任务提交页面(照片/视频上传) | 1天 | 后端API |
|
||||
| 新增查看评价页面 | 0.5天 | 后端API |
|
||||
| 完善任务列表页 | 0.5天 | 无 |
|
||||
|
||||
### Phase 4:学校端重写(2天)
|
||||
|
||||
| 任务 | 预估时间 | 依赖 |
|
||||
|------|---------|------|
|
||||
| 多维度筛选组件 | 0.5天 | 无 |
|
||||
| 任务列表页(只读) | 0.5天 | 筛选组件 |
|
||||
| 任务详情页 | 0.5天 | 后端API |
|
||||
| 学生提交详情页 | 0.5天 | 后端API |
|
||||
|
||||
### Phase 5:测试与优化(1-2天)
|
||||
|
||||
| 任务 | 预估时间 |
|
||||
|------|---------|
|
||||
| 三端联调测试 | 0.5天 |
|
||||
| Bug修复 | 0.5天 |
|
||||
| 性能优化 | 0.5天 |
|
||||
|
||||
---
|
||||
|
||||
## 十、验收标准
|
||||
|
||||
### 10.1 功能验收
|
||||
|
||||
#### 教师端
|
||||
|
||||
- [ ] 可以创建任务,填写关联绘本名称
|
||||
- [ ] 可以查看所有学生的提交情况
|
||||
- [ ] 可以查看学生提交的照片和视频
|
||||
- [ ] 可以给学生评价(优秀/通过/需改进)
|
||||
- [ ] 可以填写评语和评分
|
||||
- [ ] 评价后家长端能收到通知
|
||||
|
||||
#### 家长端
|
||||
|
||||
- [ ] 可以查看孩子的任务列表
|
||||
- [ ] 可以上传照片(最多9张)
|
||||
- [ ] 可以上传视频
|
||||
- [ ] 可以填写文字心得
|
||||
- [ ] 可以查看教师的评价
|
||||
|
||||
#### 学校端
|
||||
|
||||
- [ ] **不能**创建/编辑/删除任务
|
||||
- [ ] 可以按时间范围筛选
|
||||
- [ ] 可以按班级筛选(多选)
|
||||
- [ ] 可以按教师筛选(多选)
|
||||
- [ ] 可以按任务类型/状态筛选
|
||||
- [ ] 可以按完成率筛选
|
||||
- [ ] 可以搜索任务标题
|
||||
- [ ] 可以查看任务详情
|
||||
- [ ] 可以查看学生提交内容
|
||||
- [ ] 可以查看教师评价
|
||||
|
||||
### 10.2 技术验收
|
||||
|
||||
- [ ] 所有 API 有 Swagger 文档
|
||||
- [ ] 前端代码有 TypeScript 类型定义
|
||||
- [ ] 数据库迁移脚本可重复执行
|
||||
- [ ] 无 console.log 残留
|
||||
- [ ] 无未使用的导入和变量
|
||||
|
||||
### 10.3 UI/UX 验收
|
||||
|
||||
- [ ] 符合 Ant Design 设计规范
|
||||
- [ ] 移动端响应式适配
|
||||
- [ ] 加载状态有 loading 提示
|
||||
- [ ] 操作有成功/失败反馈
|
||||
- [ ] 表单有必填校验
|
||||
|
||||
---
|
||||
|
||||
## 附录:关键决策记录
|
||||
|
||||
| 决策 | 原因 | 日期 |
|
||||
|------|------|------|
|
||||
| 学校端只读,不创建任务 | 学校是管理监督角色,不是执行角色 | 2026-03-20 |
|
||||
| 关联绘本手动填写而非选择 | 绘本库功能复杂,先简化实现 | 2026-03-20 |
|
||||
| 评价结果用三档而非五档 | 简化教师操作,符合实际场景 | 2026-03-20 |
|
||||
| 评分设为可选 | 避免过度评价压力 | 2026-03-20 |
|
||||
| 学校端筛选参数用数组 | 支持多选,更灵活 | 2026-03-20 |
|
||||
|
||||
---
|
||||
|
||||
*文档版本历史:*
|
||||
- v2.0 (2026-03-20): 完整需求分析和设计文档
|
||||
655
docs/design/reading-task-prd.md
Normal file
655
docs/design/reading-task-prd.md
Normal file
@ -0,0 +1,655 @@
|
||||
# 阅读任务模块 PRD(产品需求文档)
|
||||
|
||||
> 版本:v1.1
|
||||
> 日期:2026-03-20
|
||||
> 状态:讨论稿
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
### 1.1 业务背景
|
||||
|
||||
幼儿园教师在完成绘本课后,需要给学生布置家庭阅读任务,建议孩子回家自主阅读相关绘本。目前缺乏一个系统化的工具来:
|
||||
- 管理阅读任务的发布和追踪
|
||||
- 让家长反馈孩子的完成情况
|
||||
- 让教师对学生完成情况进行评价
|
||||
- 让学校管理层了解全校阅读任务执行情况
|
||||
|
||||
### 1.2 产品目标
|
||||
|
||||
| 目标 | 指标 |
|
||||
|------|------|
|
||||
| 简化任务发布流程 | 教师发布任务时间 < 2分钟 |
|
||||
| 提升家长参与度 | 任务完成率 > 80% |
|
||||
| 增强家校互动 | 教师反馈率 > 90% |
|
||||
| 数据可追溯 | 学校可查看所有历史记录 |
|
||||
|
||||
### 1.3 本期范围
|
||||
|
||||
| 包含 | 不包含(后续迭代) |
|
||||
|------|-------------------|
|
||||
| ✅ 教师端任务管理 | ❌ 家长端绘本阅读功能 |
|
||||
| ✅ 家长端任务查看/提交 | ❌ 绘本付费订阅 |
|
||||
| ✅ 教师端反馈评价 | ❌ 学生端独立 App |
|
||||
| ✅ 学校端数据查看 | ❌ 绘本库/自动推荐 |
|
||||
| ✅ 关联绘本(手动填写) | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 二、竞品分析
|
||||
|
||||
### 2.1 Seesaw(核心参考)
|
||||
|
||||
**定位**:K-12 学生的数字学习档案平台
|
||||
|
||||
| 功能模块 | 说明 | 可借鉴点 |
|
||||
|---------|------|---------|
|
||||
| **作品提交** | 学生/家长上传照片、视频、绘画、语音 | 多种提交形式 |
|
||||
| **教师反馈** | 文字、语音、手写批注 | 多维度反馈方式 |
|
||||
| **数字档案** | 自动归档学生作品形成成长记录 | 生成成长档案 |
|
||||
| **家校消息** | 单向公告 + 双向消息 | 灵活的沟通模式 |
|
||||
| **进度报告** | 可生成并分享给家长 | 数据可视化 |
|
||||
|
||||
### 2.2 ClassDojo
|
||||
|
||||
**定位**:课堂行为管理 + 家校沟通
|
||||
|
||||
| 功能模块 | 说明 | 可借鉴点 |
|
||||
|---------|------|---------|
|
||||
| **积分系统** | 行为激励,学生获得积分/徽章 | 游戏化激励 |
|
||||
| **班级故事** | 教师分享班级动态 | 内容分享 |
|
||||
| **电子档案** | 家长帮助孩子上传作业 | 亲子协作 |
|
||||
| **多语言支持** | 130+ 语言自动翻译 | 降低使用门槛 |
|
||||
|
||||
### 2.3 关键洞察
|
||||
|
||||
1. **提交形式多样化** - 不仅限于文字,支持照片/视频/语音
|
||||
2. **反馈即时性** - 教师反馈能及时触达家长
|
||||
3. **成长可追溯** - 形成学生阅读成长档案
|
||||
4. **操作简便性** - 家长端必须极简,避免使用门槛
|
||||
|
||||
---
|
||||
|
||||
## 三、用户角色与场景
|
||||
|
||||
### 3.1 角色定义
|
||||
|
||||
| 角色 | 职责 | 使用端 |
|
||||
|------|------|--------|
|
||||
| **教师** | 创建任务、查看提交、评价反馈 | 教师端(Web) |
|
||||
| **家长** | 接收任务、提交完成、查看反馈 | 家长端(H5/小程序) |
|
||||
| **学校管理员** | 查看全校数据、统计分析 | 学校端(Web) |
|
||||
| **学生** | 实际完成任务的对象 | 通过家长端操作 |
|
||||
|
||||
### 3.2 核心场景
|
||||
|
||||
```
|
||||
场景一:课后布置阅读任务
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 教师 → 完成绘本课 → 创建阅读任务 → 填写关联绘本名称 │
|
||||
│ → 设置截止日期 → 指定班级/学生 → 发布 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
场景二:家长提交完成情况
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 家长 → 收到任务通知 → 陪伴孩子阅读 → 拍照/录视频 │
|
||||
│ → 上传完成证明 → 填写简单心得 → 提交 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
场景三:教师评价反馈
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 教师 → 查看待审核列表 → 查看提交内容 → 评分/写评语 │
|
||||
│ → 发送反馈 → 家长收到通知 → 查看教师评价 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
场景四:学校查看数据
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 学校 → 多维度筛选 → 查看任务列表 → 查看完成情况 │
|
||||
│ → 查看教师反馈 → 导出报表 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、功能需求
|
||||
|
||||
### 4.1 教师端(Web)
|
||||
|
||||
#### 4.1.1 任务管理
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **创建任务** | 填写标题、描述、关联绘本名称、截止日期 | P0 |
|
||||
| **选择目标** | 按班级或按学生分配任务 | P0 |
|
||||
| **任务模板** | 使用模板快速创建,支持保存为模板 | P1 |
|
||||
| **编辑/删除** | 修改任务内容或撤回(未截止前) | P0 |
|
||||
| **任务列表** | 按状态(进行中/已截止/草稿)筛选查看 | P0 |
|
||||
|
||||
#### 4.1.2 完成情况管理
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **提交列表** | 按任务查看所有学生提交情况 | P0 |
|
||||
| **状态筛选** | 按待审核/已通过/需改进筛选 | P0 |
|
||||
| **查看详情** | 查看照片/视频/文字内容 | P0 |
|
||||
| **快捷评价** | 一键标记"优秀"/"通过"/"需改进" | P0 |
|
||||
| **详细评语** | 输入文字评价 | P1 |
|
||||
| **评分打星** | 1-5 星评分(可选) | P2 |
|
||||
| **批量操作** | 批量标记通过 | P1 |
|
||||
|
||||
#### 4.1.3 提醒通知
|
||||
|
||||
| 功能 | 描述 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **一键提醒** | 向未完成家长发送提醒 | P0 |
|
||||
| **自动提醒** | 截止前 1 天自动提醒 | P1 |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 家长端(H5/小程序)
|
||||
|
||||
> **现状分析**:已有基础框架,需完善以下功能
|
||||
|
||||
#### 4.2.1 任务查看
|
||||
|
||||
| 功能 | 描述 | 优先级 | 现状 |
|
||||
|------|------|--------|------|
|
||||
| **任务列表** | 显示孩子收到的所有阅读任务 | P0 | ✅ 已有基础 |
|
||||
| **任务详情** | 标题、描述、关联绘本、截止日期 | P0 | ✅ 已有基础 |
|
||||
| **状态显示** | 待完成/已提交/已评价 | P0 | ✅ 已有基础 |
|
||||
| **倒计时** | 显示剩余时间,过期标红 | P1 | ❌ 需新增 |
|
||||
|
||||
#### 4.2.2 完成提交(重点完善)
|
||||
|
||||
| 功能 | 描述 | 优先级 | 现状 |
|
||||
|------|------|--------|------|
|
||||
| **拍照上传** | 拍摄孩子阅读照片(最多 9 张) | P0 | ❌ 需新增 |
|
||||
| **视频上传** | 录制孩子阅读视频(限 60 秒) | P0 | ❌ 需新增 |
|
||||
| **语音上传** | 录制孩子朗读语音(限 60 秒) | P1 | ❌ 需新增 |
|
||||
| **文字心得** | 填写阅读心得(可选) | P1 | ⚠️ 需完善 |
|
||||
| **重新提交** | 截止前可修改提交内容 | P0 | ❌ 需新增 |
|
||||
|
||||
#### 4.2.3 反馈查看
|
||||
|
||||
| 功能 | 描述 | 优先级 | 现状 |
|
||||
|------|------|--------|------|
|
||||
| **评价通知** | 收到教师评价的推送通知 | P0 | ❌ 需新增 |
|
||||
| **评价详情** | 查看评分、评语 | P0 | ❌ 需新增 |
|
||||
| **历史记录** | 查看所有任务的完成和评价记录 | P1 | ⚠️ 需完善 |
|
||||
|
||||
#### 4.2.4 消息通知
|
||||
|
||||
| 功能 | 描述 | 优先级 | 现状 |
|
||||
|------|------|--------|------|
|
||||
| **新任务通知** | 推送新任务提醒 | P0 | ❌ 需新增 |
|
||||
| **截止提醒** | 推送截止前提醒 | P0 | ❌ 需新增 |
|
||||
| **评价通知** | 推送教师评价提醒 | P0 | ❌ 需新增 |
|
||||
|
||||
---
|
||||
|
||||
### 4.3 学校端(Web)- 重点设计
|
||||
|
||||
> **核心原则**:学校端 **不创建任务**,仅查看数据和统计分析
|
||||
> **核心挑战**:数据量大(多班级 × 多教师 × 每日任务),需高效筛选
|
||||
|
||||
#### 4.3.1 筛选系统设计(参考最佳实践)
|
||||
|
||||
基于竞品分析和教育管理系统的最佳实践,设计 **多维度组合筛选** 系统:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🔍 筛选条件 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 时间范围 │ │
|
||||
│ │ [本周 ▼] [2026年3月 ▼] 或 [03-01] ~ [03-20] 自定义 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ 班级 │ │ 教师 │ │ 任务类型 │ │
|
||||
│ │ [全部 ▼] │ │ [全部 ▼] │ │ [全部 ▼] │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 任务状态 │ │ 完成状态 │ │ 搜索任务标题... │ │
|
||||
│ │ [全部 ▼] │ │ [全部 ▼] │ │ 🔍 │ │
|
||||
│ └────────────┘ └────────────┘ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ [重置筛选] [已选: 5 个条件] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
##### 筛选维度详解
|
||||
|
||||
| 维度 | 选项 | 说明 |
|
||||
|------|------|------|
|
||||
| **时间范围** | 今日/本周/本月/本学期/自定义 | 快捷选择 + 日期选择器 |
|
||||
| **班级** | 全部/大一班/大二班/... | 支持多选 |
|
||||
| **教师** | 全部/李老师/王老师/... | 支持多选 |
|
||||
| **任务类型** | 全部/阅读/活动/作业 | 单选 |
|
||||
| **任务状态** | 全部/进行中/已截止/已归档 | 单选 |
|
||||
| **完成状态** | 全部/高完成率(>80%)/中完成率(50-80%)/低完成率(<50%) | 单选 |
|
||||
| **搜索** | 任务标题关键字 | 模糊搜索 |
|
||||
|
||||
##### 筛选交互设计
|
||||
|
||||
1. **实时筛选**:选择条件后立即刷新结果(无需点击搜索按钮)
|
||||
2. **已选标签**:在筛选区下方显示已选条件的标签,可点击移除
|
||||
3. **条件持久化**:筛选条件保存在 URL 参数中,刷新页面不丢失
|
||||
4. **智能默认**:默认显示"本周"任务,避免数据量过大
|
||||
|
||||
#### 4.3.2 任务总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 📊 阅读任务总览 2026年3月 第3周 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 12 │ │ 85% │ │ 92% │ │ 156 │ │
|
||||
│ │ 发布任务 │ │ 平均完成率│ │ 教师反馈率│ │ 提交总数 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 📈 趋势图(按天/按班级) │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ ▓▓▓ │ │
|
||||
│ │ ▓▓▓▓▓ ▓▓▓ │ │
|
||||
│ │ ▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓ ▓▓▓ │ │
|
||||
│ │ ───────┬───────┬───────┬───────┬─────── │ │
|
||||
│ │ 周一 周二 周三 周四 周五 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 4.3.3 任务列表(表格视图)
|
||||
|
||||
| 列名 | 说明 | 排序 |
|
||||
|------|------|------|
|
||||
| 任务标题 | 点击可查看详情 | ✅ |
|
||||
| 发布教师 | 教师姓名 | ✅ |
|
||||
| 班级 | 目标班级 | ✅ |
|
||||
| 类型 | 阅读/活动/作业 | - |
|
||||
| 目标人数 | 学生总数 | - |
|
||||
| 提交数 | 已提交/总人数 | ✅ |
|
||||
| 完成率 | 进度条显示 | ✅ |
|
||||
| 反馈率 | 教师已评价比例 | ✅ |
|
||||
| 发布时间 | yyyy-MM-dd | ✅ |
|
||||
| 截止时间 | yyyy-MM-dd | ✅ |
|
||||
| 状态 | 进行中/已截止 | - |
|
||||
| 操作 | 查看详情 | - |
|
||||
|
||||
#### 4.3.4 任务详情(只读)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 《好饿的毛毛虫》阅读任务 [返回列表] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 基本信息 │
|
||||
│ 发布教师: 李老师 班级: 大一班 类型: 阅读 │
|
||||
│ 发布时间: 2026-03-18 截止时间: 2026-03-20 │
|
||||
│ 关联绘本: 《好饿的毛毛虫》 │
|
||||
│ 任务描述: 和孩子一起阅读这本经典绘本,完成后拍照上传... │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 完成情况 │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 目标: 25人 已提交: 20人 完成率: 80% │ │
|
||||
│ │ ████████████████░░░░ 80% │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 学生列表 │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ 学生 │ 班级 │ 提交时间 │ 状态 │ 教师评价 │ │
|
||||
│ ├──────────────────────────────────────────────────────────┤ │
|
||||
│ │ 张小明 │ 大一班│ 03-19 18:30│ ✅优秀 │ 很棒!阅读认真... │ │
|
||||
│ │ 李小红 │ 大一班│ 03-19 19:20│ ✅通过 │ 继续加油! │ │
|
||||
│ │ 王小刚 │ 大一班│ 03-20 09:15│ ⏳待审 │ - │ │
|
||||
│ │ 赵小丽 │ 大一班│ - │ ⭕未提交│ - │ │
|
||||
│ │ ... │ ... │ ... │ ... │ ... │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ [导出明细] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 4.3.5 学生提交详情(只读)
|
||||
|
||||
学校端可查看具体学生的提交内容:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 张小明的提交 [返回] [导出] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 任务: 《好饿的毛毛虫》阅读任务 │
|
||||
│ 提交时间: 2026-03-19 18:30 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 📷 照片 │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │ 📸 │ │ 📸 │ │ 📸 │ 点击查看大图 │
|
||||
│ └─────┘ └─────┘ └─────┘ │
|
||||
│ │
|
||||
│ 📝 阅读心得 │
|
||||
│ "孩子很喜欢这本书,读了好几遍,还给我讲了毛毛虫变蝴蝶的故事..." │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 教师评价 │
|
||||
│ 评价结果: ⭐⭐⭐⭐⭐ 优秀 │
|
||||
│ 评价内容: "很棒!阅读很认真,能复述故事内容,继续保持!" │
|
||||
│ 评价时间: 2026-03-19 20:15 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 4.3.6 统计报表
|
||||
|
||||
| 报表类型 | 内容 | 导出格式 |
|
||||
|---------|------|---------|
|
||||
| **任务统计** | 按时间/班级/教师的任务发布和完成情况 | Excel |
|
||||
| **班级对比** | 各班级完成率、反馈率对比 | Excel + 图表 |
|
||||
| **教师排行** | 发布数、反馈率、平均评分 | Excel |
|
||||
| **学生明细** | 指定条件下的学生完成明细 | Excel |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据模型设计
|
||||
|
||||
### 5.1 实体关系图
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Teacher │────>│ Task │<────│ Student │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ TaskTarget │ │TaskCompletion│
|
||||
└──────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│TaskFeedback │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 5.2 表结构设计
|
||||
|
||||
#### Task(任务表)- 新增字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 变更 |
|
||||
|------|------|------|------|
|
||||
| id | BIGINT | 主键 | - |
|
||||
| tenant_id | BIGINT | 租户ID(学校) | - |
|
||||
| title | VARCHAR(100) | 任务标题 | - |
|
||||
| description | TEXT | 任务描述 | - |
|
||||
| task_type | VARCHAR(20) | 类型:READING/ACTIVITY/HOMEWORK | - |
|
||||
| **related_book_name** | VARCHAR(200) | 关联绘本名称 | **新增** |
|
||||
| course_id | BIGINT | 关联课程ID(可选) | - |
|
||||
| course_lesson_id | BIGINT | 关联课程环节ID(可选) | - |
|
||||
| creator_id | BIGINT | 创建人ID(教师) | - |
|
||||
| start_date | DATE | 开始日期 | - |
|
||||
| due_date | DATE | 截止日期 | - |
|
||||
| status | VARCHAR(20) | 状态:DRAFT/PUBLISHED/ARCHIVED | - |
|
||||
| created_at | DATETIME | 创建时间 | - |
|
||||
| updated_at | DATETIME | 更新时间 | - |
|
||||
|
||||
#### TaskTarget(任务目标表)- 无变更
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | BIGINT | 主键 |
|
||||
| task_id | BIGINT | 任务ID |
|
||||
| target_type | VARCHAR(20) | 目标类型:CLASS/STUDENT |
|
||||
| target_id | BIGINT | 目标ID(班级ID或学生ID) |
|
||||
|
||||
#### TaskCompletion(任务完成表)- 扩展字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 变更 |
|
||||
|------|------|------|------|
|
||||
| id | BIGINT | 主键 | - |
|
||||
| task_id | BIGINT | 任务ID | - |
|
||||
| student_id | BIGINT | 学生ID | - |
|
||||
| status | VARCHAR(20) | 状态:PENDING/SUBMITTED/REVIEWED | - |
|
||||
| **photos** | JSON | 照片URL数组 | **扩展** |
|
||||
| **video_url** | VARCHAR(500) | 视频URL | **新增** |
|
||||
| **audio_url** | VARCHAR(500) | 语音URL | **新增** |
|
||||
| content | TEXT | 文字心得 | - |
|
||||
| submitted_at | DATETIME | 提交时间 | - |
|
||||
| reviewed_at | DATETIME | 审核时间 | - |
|
||||
| created_at | DATETIME | 创建时间 | - |
|
||||
| updated_at | DATETIME | 更新时间 | - |
|
||||
|
||||
#### TaskFeedback(任务反馈表)- 新增
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | BIGINT | 主键 |
|
||||
| completion_id | BIGINT | 完成记录ID |
|
||||
| teacher_id | BIGINT | 教师ID |
|
||||
| rating | INT | 评分 1-5(可选) |
|
||||
| result | VARCHAR(20) | 结果:EXCELLENT/PASSED/NEEDS_WORK |
|
||||
| comment | TEXT | 评语 |
|
||||
| created_at | DATETIME | 创建时间 |
|
||||
|
||||
---
|
||||
|
||||
## 六、API 接口设计
|
||||
|
||||
### 6.1 教师端 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/teacher/tasks` | 任务列表(支持筛选) |
|
||||
| POST | `/api/v1/teacher/tasks` | 创建任务 |
|
||||
| GET | `/api/v1/teacher/tasks/{id}` | 任务详情 |
|
||||
| PUT | `/api/v1/teacher/tasks/{id}` | 更新任务 |
|
||||
| DELETE | `/api/v1/teacher/tasks/{id}` | 删除任务 |
|
||||
| GET | `/api/v1/teacher/tasks/{id}/completions` | 完成情况列表 |
|
||||
| POST | `/api/v1/teacher/completions/{id}/feedback` | 提交评价 |
|
||||
| POST | `/api/v1/teacher/tasks/{id}/remind` | 发送提醒 |
|
||||
|
||||
### 6.2 家长端 API(完善)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/parent/tasks` | 孩子的任务列表 |
|
||||
| GET | `/api/v1/parent/tasks/{id}` | 任务详情 |
|
||||
| POST | `/api/v1/parent/tasks/{id}/submit` | 提交完成(含上传) |
|
||||
| PUT | `/api/v1/parent/tasks/{id}/submit` | 修改提交 |
|
||||
| GET | `/api/v1/parent/tasks/{id}/feedback` | 查看教师评价 |
|
||||
|
||||
### 6.3 学校端 API(新增/完善)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/school/tasks` | 全校任务列表(支持多维度筛选) |
|
||||
| GET | `/api/v1/school/tasks/{id}` | 任务详情 |
|
||||
| GET | `/api/v1/school/tasks/{id}/completions` | 完成情况列表 |
|
||||
| GET | `/api/v1/school/completions/{id}` | 学生提交详情 |
|
||||
| GET | `/api/v1/school/tasks/statistics` | 统计数据 |
|
||||
| GET | `/api/v1/school/tasks/statistics/trend` | 趋势数据 |
|
||||
| GET | `/api/v1/school/tasks/export` | 导出报表 |
|
||||
|
||||
#### 学校端筛选参数
|
||||
|
||||
```typescript
|
||||
// GET /api/v1/school/tasks
|
||||
interface SchoolTaskQuery {
|
||||
// 分页
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
|
||||
// 时间筛选
|
||||
dateType?: 'today' | 'week' | 'month' | 'semester' | 'custom';
|
||||
startDate?: string; // 自定义开始日期
|
||||
endDate?: string; // 自定义结束日期
|
||||
|
||||
// 组织筛选
|
||||
classIds?: number[]; // 班级ID数组
|
||||
teacherIds?: number[]; // 教师ID数组
|
||||
|
||||
// 状态筛选
|
||||
taskType?: 'READING' | 'ACTIVITY' | 'HOMEWORK';
|
||||
taskStatus?: 'PUBLISHED' | 'ARCHIVED';
|
||||
completionRate?: 'high' | 'medium' | 'low'; // >80% / 50-80% / <50%
|
||||
|
||||
// 搜索
|
||||
keyword?: string;
|
||||
|
||||
// 排序
|
||||
sortBy?: 'createTime' | 'completionRate' | 'feedbackRate';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、UI 设计要点
|
||||
|
||||
### 7.1 学校端筛选区交互规范
|
||||
|
||||
#### 快捷时间选择器
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ [今日] [本周 ✓] [本月] [本学期] [自定义] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 已选条件标签
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 已选条件: │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 本周 ✕ │ │ 大一班 ✕ │ │ 李老师 ✕ │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ [清空全部] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 班级/教师多选下拉
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 班级 ▼ │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ ☑ 全选 │ │
|
||||
│ │ ☑ 大一班 │ │
|
||||
│ │ ☑ 大二班 │ │
|
||||
│ │ ☐ 中一班 │ │
|
||||
│ │ ☐ 中二班 │ │
|
||||
│ │ │ │
|
||||
│ │ [确定] [取消] │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.2 家长端提交页面
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────┐
|
||||
│ 《好饿的毛毛虫》阅读任务 │
|
||||
│ 截止时间: 明天 18:00 │
|
||||
├───────────────────────────────────────┤
|
||||
│ 📷 上传阅读照片 │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌───┐ │
|
||||
│ │ 📸 │ │ 📸 │ │ 📸 │ │ + │ │
|
||||
│ └─────┘ └─────┘ └─────┘ └───┘ │
|
||||
│ 最多上传 9 张照片 │
|
||||
├───────────────────────────────────────┤
|
||||
│ 🎬 上传阅读视频(选填) │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ 📹 │ │
|
||||
│ │ 点击上传视频 │ │
|
||||
│ │ 限 60 秒以内 │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
├───────────────────────────────────────┤
|
||||
│ 📝 阅读心得(选填) │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ 孩子很喜欢这本书... │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ 0/500 │
|
||||
├───────────────────────────────────────┤
|
||||
│ │
|
||||
│ [ 提交完成 ] │
|
||||
│ │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、开发计划
|
||||
|
||||
### Phase 1:核心功能(2周)
|
||||
|
||||
| 模块 | 功能 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **后端** | Task 表新增 related_book_name 字段 | P0 |
|
||||
| | TaskCompletion 表扩展 photos/video_url/audio_url | P0 |
|
||||
| | 新增 TaskFeedback 表 | P0 |
|
||||
| | 完善教师端 API(评价、提醒) | P0 |
|
||||
| | 完善家长端 API(提交、查看评价) | P0 |
|
||||
| | 新增学校端 API(多维度筛选) | P0 |
|
||||
| **前端-教师端** | 完善任务创建(添加绘本名称字段) | P0 |
|
||||
| | 完善完成情况管理(评价功能) | P0 |
|
||||
| **前端-家长端** | 新增任务提交页面(照片/视频上传) | P0 |
|
||||
| | 新增评价查看功能 | P0 |
|
||||
| **前端-学校端** | 新增多维度筛选组件 | P0 |
|
||||
| | 新增任务列表(只读) | P0 |
|
||||
| | 新增任务详情和学生提交详情 | P0 |
|
||||
|
||||
### Phase 2:完善功能(1周)
|
||||
|
||||
| 模块 | 功能 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **前端-教师端** | 任务模板功能 | P1 |
|
||||
| | 批量评价 | P1 |
|
||||
| **前端-家长端** | 视频录制和上传 | P1 |
|
||||
| | 语音录制和上传 | P1 |
|
||||
| | 历史记录完善 | P1 |
|
||||
| **前端-学校端** | 统计图表 | P1 |
|
||||
| | 导出报表 | P1 |
|
||||
|
||||
### Phase 3:通知系统(1周)
|
||||
|
||||
| 模块 | 功能 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **通知** | 新任务通知(推送至家长端) | P0 |
|
||||
| | 截止提醒(推送至家长端) | P0 |
|
||||
| | 评价通知(推送至家长端) | P0 |
|
||||
| | 自动提醒(截止前1天) | P1 |
|
||||
|
||||
---
|
||||
|
||||
## 九、现有代码改造点
|
||||
|
||||
### 9.1 家长端后端
|
||||
|
||||
**文件**: `ParentTaskController.java`
|
||||
|
||||
**现状**: `getMyTasks` 方法返回空数据(TODO)
|
||||
|
||||
**改造**:
|
||||
1. 实现根据家长ID获取关联学生的任务
|
||||
2. 新增 `POST /api/v1/parent/tasks/{id}/submit` 接口处理提交
|
||||
3. 新增 `GET /api/v1/parent/tasks/{id}/feedback` 接口获取评价
|
||||
|
||||
### 9.2 家长端前端
|
||||
|
||||
**文件**: `TaskListView.vue`
|
||||
|
||||
**现状**: 有基础框架,仅支持文字反馈
|
||||
|
||||
**改造**:
|
||||
1. 新增提交页面组件(支持照片/视频上传)
|
||||
2. 新增评价查看组件
|
||||
3. 完善任务状态显示和倒计时
|
||||
|
||||
---
|
||||
|
||||
## 十、风险与依赖
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| 文件上传量增加 | OSS 存储成本 | 限制单个任务上传数量和大小 |
|
||||
| 推送通知依赖 | 消息触达率 | 确保家长端已集成推送 SDK |
|
||||
| 数据量增长 | 查询性能 | 添加索引,学校端分页查询 |
|
||||
|
||||
---
|
||||
|
||||
*文档版本历史:*
|
||||
- v1.0 (2026-03-20): 初稿
|
||||
- v1.1 (2026-03-20): 完善学校端筛选设计、家长端功能、开发计划
|
||||
320
docs/dev-logs/2026-03-20.md
Normal file
320
docs/dev-logs/2026-03-20.md
Normal file
@ -0,0 +1,320 @@
|
||||
# 开发日志 - 2026-03-20
|
||||
|
||||
## 阅读任务模块重写 Phase 1 - 后端基础改造
|
||||
|
||||
### 完成的工作
|
||||
|
||||
#### 1. 数据库迁移脚本
|
||||
- 文件: `V43__add_reading_task_features.sql`
|
||||
- 创建 `task_feedback` 表(评价表)
|
||||
- 为 `task` 表添加 `related_book_name` 字段
|
||||
- 为 `task_completion` 表添加 `video_url`, `audio_url` 字段
|
||||
|
||||
#### 2. 实体类
|
||||
- `TaskFeedback.java` - 新增评价实体
|
||||
- `Task.java` - 添加 `relatedBookName` 字段(已存在)
|
||||
- `TaskCompletion.java` - 添加新字段(已存在)
|
||||
|
||||
#### 3. DTO
|
||||
- `TaskFeedbackRequest.java` - 评价请求 DTO
|
||||
- `TaskFeedbackResponse.java` - 评价响应 DTO
|
||||
- `TaskCompletionDetailResponse.java` - 完成详情响应 DTO
|
||||
- `TaskSubmitRequest.java` - 家长提交请求 DTO
|
||||
|
||||
#### 4. Service 层
|
||||
|
||||
**新增**:
|
||||
- `TaskFeedbackService.java` - 评价服务接口
|
||||
- `TaskFeedbackServiceImpl.java` - 评价服务实现
|
||||
|
||||
**扩展**:
|
||||
- `TaskService.java` - 添加以下方法:
|
||||
- `getTaskCompletions()` - 获取任务完成情况列表
|
||||
- `getCompletionDetail()` - 获取提交详情
|
||||
- `submitTaskCompletion()` - 家长提交任务完成
|
||||
- `getSchoolTaskList()` - 学校端任务列表(多维度筛选)
|
||||
- `getTaskDetailForSchool()` - 学校端任务详情
|
||||
|
||||
- `ClassService.java` - 添加:
|
||||
- `getPrimaryClassByStudentId()` - 获取学生当前所在班级
|
||||
|
||||
#### 5. Controller 层
|
||||
|
||||
**教师端** (`TeacherTaskController.java`):
|
||||
- `GET /api/v1/teacher/tasks/{taskId}/completions` - 完成情况列表
|
||||
- `GET /api/v1/teacher/tasks/completions/{completionId}` - 提交详情
|
||||
- `POST /api/v1/teacher/tasks/completions/{completionId}/feedback` - 提交评价
|
||||
- `PUT /api/v1/teacher/tasks/completions/{completionId}/feedback` - 修改评价
|
||||
|
||||
**家长端** (`ParentTaskController.java`):
|
||||
- `POST /api/v1/parent/tasks/{taskId}/submit` - 提交任务完成
|
||||
- `PUT /api/v1/parent/tasks/{taskId}/submit` - 修改提交
|
||||
- `GET /api/v1/parent/tasks/completions/{completionId}/feedback` - 获取评价
|
||||
- `GET /api/v1/parent/tasks/completions/{completionId}` - 获取提交详情
|
||||
|
||||
**学校端** (`SchoolTaskController.java`) - **完全重写为只读**:
|
||||
- `GET /api/v1/school/reading-tasks` - 任务列表(多维度筛选)
|
||||
- `GET /api/v1/school/reading-tasks/{taskId}` - 任务详情
|
||||
- `GET /api/v1/school/reading-tasks/{taskId}/completions` - 完成情况列表
|
||||
- `GET /api/v1/school/reading-tasks/completions/{completionId}` - 学生提交详情
|
||||
- `GET /api/v1/school/reading-tasks/statistics` - 统计数据(TODO)
|
||||
|
||||
**移除的学校端 API**:
|
||||
- ~~POST 创建任务~~
|
||||
- ~~PUT 更新任务~~
|
||||
- ~~DELETE 删除任务~~
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **学校端只读**: 学校端 API 路径从 `/api/v1/school/tasks` 改为 `/api/v1/school/reading-tasks`,完全只读
|
||||
|
||||
2. **评价结果枚举**: `EXCELLENT`(优秀)/ `PASSED`(通过)/ `NEEDS_WORK`(需改进)
|
||||
|
||||
3. **完成状态**: `PENDING`(待提交)/ `SUBMITTED`(已提交)/ `REVIEWED`(已评价)
|
||||
|
||||
4. **文件存储**: 继续使用现有 OSS,照片以 JSON 数组存储
|
||||
|
||||
---
|
||||
|
||||
## 阅读任务模块重写 Phase 2 - 教师端前端改造 ✅
|
||||
|
||||
### 完成的工作
|
||||
|
||||
#### 1. API 类型更新 (`src/api/teacher.ts`)
|
||||
- 更新 `TaskCompletion` 接口添加新字段:
|
||||
- `photos: string[]` - 照片列表
|
||||
- `videoUrl: string` - 视频链接
|
||||
- `audioUrl: string` - 音频链接
|
||||
- `content: string` - 提交内容
|
||||
- `submittedAt: string` - 提交时间
|
||||
- `reviewedAt: string` - 评价时间
|
||||
- `feedback: TaskFeedback` - 关联评价
|
||||
- 添加 `TaskFeedbackDto` 接口(评价请求)
|
||||
- 添加新 API 函数:
|
||||
- `getTeacherTaskCompletions()` - 获取完成情况列表
|
||||
- `getCompletionDetail()` - 获取提交详情
|
||||
- `submitCompletionFeedback()` - 提交评价
|
||||
- `updateCompletionFeedback()` - 修改评价
|
||||
|
||||
#### 2. TaskListView.vue 改造
|
||||
|
||||
**创建任务弹窗**:
|
||||
- [x] 添加 `relatedBookName`(关联绘本名称)字段
|
||||
- [x] 编辑任务时加载已有绘本名称
|
||||
|
||||
**完成情况弹窗改造**:
|
||||
- [x] 状态筛选改为新状态值(PENDING/SUBMITTED/REVIEWED)
|
||||
- [x] 显示提交内容预览(照片数量、视频、文字内容)
|
||||
- [x] 统计标签更新为:待提交/已提交/已评价
|
||||
|
||||
**评价弹窗组件**:
|
||||
- [x] 新增评价弹窗 UI
|
||||
- [x] 提交内容预览区域(照片、视频、音频、文字)
|
||||
- [x] 评价结果选择(优秀/通过/需改进)
|
||||
- [x] 评分组件(1-5 星)
|
||||
- [x] 评语输入框
|
||||
- [x] 支持新建和编辑评价
|
||||
|
||||
**状态显示逻辑**:
|
||||
- [x] `getCompletionStatusColor()` - 新状态颜色映射
|
||||
- [x] `getCompletionStatusText()` - 新状态文本映射
|
||||
- [x] `completionStats` computed 使用新状态值
|
||||
|
||||
### 文件变更清单
|
||||
|
||||
**修改文件**:
|
||||
- `src/api/teacher.ts` - API 类型和函数
|
||||
- `src/views/teacher/tasks/TaskListView.vue` - 教师任务管理页面
|
||||
|
||||
### TypeScript 编译验证
|
||||
- `src/views/teacher/tasks/TaskListView.vue` ✅ 无错误
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
### Phase 3: 家长端前端改造
|
||||
- [ ] 改造提交功能
|
||||
- [ ] 新增查看评价页面
|
||||
|
||||
### Phase 4: 学校端前端重写
|
||||
- [ ] 重写任务列表页(只读+多维度筛选)
|
||||
- [ ] 新增任务详情页
|
||||
- [ ] 新增学生提交详情页
|
||||
|
||||
---
|
||||
|
||||
## 阅读任务模块重写 Phase 3 - 家长端前端改造 ✅
|
||||
|
||||
### 完成的工作
|
||||
|
||||
#### 1. API 类型更新 (`src/api/parent.ts`)
|
||||
- 添加新的类型定义:
|
||||
- `TaskCompletionStatus` - 完成状态类型
|
||||
- `FeedbackResult` - 评价结果类型
|
||||
- `TeacherFeedback` - 教师评价接口
|
||||
- `TaskCompletion` - 任务完成记录接口
|
||||
- `TaskSubmitRequest` - 提交请求接口
|
||||
- 更新 `TaskWithCompletion` 接口添加新字段:
|
||||
- `photos: string[]` - 照片列表
|
||||
- `videoUrl: string` - 视频链接
|
||||
- `audioUrl: string` - 音频链接
|
||||
- `content: string` - 提交内容
|
||||
- `submittedAt: string` - 提交时间
|
||||
- `reviewedAt: string` - 评价时间
|
||||
- `teacherFeedback: TeacherFeedback` - 教师评价
|
||||
- 添加新 API 函数:
|
||||
- `submitTaskCompletion()` - 提交任务完成
|
||||
- `updateTaskCompletion()` - 修改提交
|
||||
- `getCompletionDetail()` - 获取提交详情
|
||||
- `getCompletionFeedback()` - 获取教师评价
|
||||
|
||||
#### 2. TaskListView.vue 完全重写
|
||||
|
||||
**任务卡片展示**:
|
||||
- [x] 显示关联绘本名称
|
||||
- [x] 状态标签更新为新状态(待提交/已提交/已评价)
|
||||
- [x] 已提交内容预览(照片数量、视频、音频、文字)
|
||||
- [x] 教师评价显示区域(评价结果、评分、评语)
|
||||
|
||||
**提交功能改造**:
|
||||
- [x] 照片上传(最多9张)
|
||||
- [x] 视频链接输入
|
||||
- [x] 音频链接输入
|
||||
- [x] 阅读心得文字输入
|
||||
- [x] 支持新建和修改提交
|
||||
|
||||
**查看评价功能**:
|
||||
- [x] 评价详情弹窗
|
||||
- [x] 评价结果展示(优秀/通过/需改进)
|
||||
- [x] 评分显示(1-5星)
|
||||
- [x] 教师评语展示
|
||||
- [x] 提交内容回顾(照片网格、链接、文字)
|
||||
- [x] 图片预览功能
|
||||
|
||||
**状态驱动操作**:
|
||||
- [x] 待提交状态 → 显示"提交完成"按钮
|
||||
- [x] 已提交状态 → 显示"修改提交"按钮
|
||||
- [x] 已评价状态 → 显示"查看评价详情"按钮
|
||||
|
||||
### 文件变更清单
|
||||
|
||||
**修改文件**:
|
||||
- `src/api/parent.ts` - API 类型和函数
|
||||
- `src/views/parent/tasks/TaskListView.vue` - 家长任务管理页面
|
||||
|
||||
### TypeScript 编译验证
|
||||
- `src/api/parent.ts` ✅ 无错误
|
||||
- `src/views/parent/tasks/TaskListView.vue` ✅ 无错误
|
||||
|
||||
---
|
||||
|
||||
## 阅读任务模块重写 Phase 4 - 学校端前端重写 ✅
|
||||
|
||||
### 完成的工作
|
||||
|
||||
#### 1. API 层更新 (`src/api/school.ts`)
|
||||
|
||||
**新增只读接口类型**:
|
||||
- `TaskCompletionStatus` - 完成状态(PENDING/SUBMITTED/REVIEWED)
|
||||
- `FeedbackResult` - 评价结果(EXCELLENT/PASSED/NEEDS_WORK)
|
||||
- `TaskTeacherFeedback` - 教师评价接口
|
||||
- `TaskCompletionRecord` - 任务完成记录(新设计)
|
||||
- `SchoolTaskQueryParams` - 多维度筛选参数
|
||||
|
||||
**新增 API 函数**:
|
||||
- `getReadingTaskList()` - 获取任务列表(多维度筛选)
|
||||
- `getReadingTaskDetail()` - 获取任务详情
|
||||
- `getReadingTaskCompletions()` - 获取完成情况列表
|
||||
- `getCompletionDetail()` - 获取提交详情
|
||||
|
||||
**保留旧接口**: 保留 create/update/delete 函数以保持向后兼容
|
||||
|
||||
#### 2. TaskListView.vue 完全重写(只读模式)
|
||||
|
||||
**移除的功能**:
|
||||
- ❌ 创建任务
|
||||
- ❌ 编辑任务
|
||||
- ❌ 删除任务
|
||||
- ❌ 发布任务按钮
|
||||
|
||||
**新增/改进的功能**:
|
||||
|
||||
**统计卡片**:
|
||||
- [x] 全部任务数
|
||||
- [x] 进行中任务数
|
||||
- [x] 已提交数量
|
||||
- [x] 已评价数量
|
||||
|
||||
**多维度筛选**:
|
||||
- [x] 关键字搜索
|
||||
- [x] 任务类型筛选(阅读/活动/作业)
|
||||
- [x] 任务状态筛选(进行中/草稿/已归档)
|
||||
- [x] 日期范围筛选
|
||||
- [x] 排序方式选择
|
||||
|
||||
**任务列表**:
|
||||
- [x] 卡片式展示
|
||||
- [x] 显示关联绘本名称
|
||||
- [x] 显示创建人
|
||||
- [x] 完成情况统计(待提交/已提交/已评价)
|
||||
- [x] 完成率进度条
|
||||
- [x] 点击查看详情
|
||||
|
||||
**任务详情弹窗**:
|
||||
- [x] 基本信息(类型、状态、时间等)
|
||||
- [x] 关联绘本
|
||||
- [x] 完成情况统计
|
||||
- [x] 查看完成列表入口
|
||||
|
||||
**完成情况列表弹窗**:
|
||||
- [x] 学生信息(姓名、班级、头像)
|
||||
- [x] 状态标签
|
||||
- [x] 提交内容预览(照片数量、视频、音频)
|
||||
- [x] 教师评价显示
|
||||
- [x] 筛选和分页
|
||||
- [x] 查看详情按钮
|
||||
|
||||
**提交详情弹窗**:
|
||||
- [x] 学生信息卡片
|
||||
- [x] 提交状态显示
|
||||
- [x] 照片网格展示(支持预览)
|
||||
- [x] 视频/音频链接
|
||||
- [x] 阅读心得文字
|
||||
- [x] 提交时间
|
||||
- [x] 教师评价详情(结果、评分、评语)
|
||||
- [x] 评价时间
|
||||
|
||||
### 文件变更清单
|
||||
|
||||
**修改文件**:
|
||||
- `src/api/school.ts` - 新增只读接口类型和函数
|
||||
- `src/views/school/tasks/TaskListView.vue` - 完全重写为只读模式
|
||||
|
||||
### TypeScript 编译验证
|
||||
- `src/api/school.ts` ✅ 无错误
|
||||
- `src/views/school/tasks/TaskListView.vue` ✅ 无错误
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
### 阅读任务模块重写全部完成 ✅
|
||||
|
||||
| Phase | 端 | 状态 |
|
||||
|-------|-----|------|
|
||||
| Phase 1 | 后端基础改造 | ✅ 完成 |
|
||||
| Phase 2 | 教师端前端 | ✅ 完成 |
|
||||
| Phase 3 | 家长端前端 | ✅ 完成 |
|
||||
| Phase 4 | 学校端前端 | ✅ 完成 |
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **学校端只读**: 学校端 API 路径改为 `/api/v1/school/reading-tasks`,完全只读
|
||||
2. **新状态值**: `PENDING` → `SUBMITTED` → `REVIEWED`
|
||||
3. **评价结果**: `EXCELLENT`(优秀)/ `PASSED`(通过)/ `NEEDS_WORK`(需改进)
|
||||
4. **多维度筛选**: 支持关键字、类型、状态、日期范围、排序等筛选条件
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-03-20 19:30*
|
||||
@ -46,12 +46,63 @@ export interface LessonRecord {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 任务完成状态
|
||||
export type TaskCompletionStatus = 'PENDING' | 'SUBMITTED' | 'REVIEWED';
|
||||
|
||||
// 教师评价结果
|
||||
export type FeedbackResult = 'EXCELLENT' | 'PASSED' | 'NEEDS_WORK';
|
||||
|
||||
// 教师评价
|
||||
export interface TeacherFeedback {
|
||||
id: number;
|
||||
result: FeedbackResult;
|
||||
resultText?: string;
|
||||
rating?: number;
|
||||
comment?: string;
|
||||
createdAt: string;
|
||||
teacherName?: string;
|
||||
}
|
||||
|
||||
// 任务完成记录(新设计)
|
||||
export interface TaskCompletion {
|
||||
id: number;
|
||||
taskId: number;
|
||||
studentId: number;
|
||||
status: TaskCompletionStatus;
|
||||
statusText?: string;
|
||||
photos?: string[];
|
||||
videoUrl?: string;
|
||||
audioUrl?: string;
|
||||
content?: string;
|
||||
submittedAt?: string;
|
||||
reviewedAt?: string;
|
||||
createdAt: string;
|
||||
feedback?: TeacherFeedback;
|
||||
}
|
||||
|
||||
// 任务完成提交请求
|
||||
export interface TaskSubmitRequest {
|
||||
studentId: number;
|
||||
photos?: string[];
|
||||
videoUrl?: string;
|
||||
audioUrl?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// 带完成信息的任务(兼容旧接口)
|
||||
export interface TaskWithCompletion {
|
||||
id: number;
|
||||
status: string;
|
||||
status: TaskCompletionStatus;
|
||||
completedAt?: string;
|
||||
feedback?: string;
|
||||
parentFeedback?: string;
|
||||
submittedAt?: string;
|
||||
reviewedAt?: string;
|
||||
photos?: string[];
|
||||
videoUrl?: string;
|
||||
audioUrl?: string;
|
||||
content?: string;
|
||||
teacherFeedback?: TeacherFeedback;
|
||||
task: {
|
||||
id: number;
|
||||
title: string;
|
||||
@ -59,6 +110,7 @@ export interface TaskWithCompletion {
|
||||
taskType: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
relatedBookName?: string;
|
||||
course?: {
|
||||
id: number;
|
||||
name: string;
|
||||
@ -120,6 +172,33 @@ export const getChildTasks = (
|
||||
pageSize: res.pageSize || 10,
|
||||
}));
|
||||
|
||||
// 提交任务完成(新版)
|
||||
export const submitTaskCompletion = (
|
||||
taskId: number,
|
||||
data: TaskSubmitRequest
|
||||
): Promise<TaskCompletion> =>
|
||||
http.post(`/v1/parent/tasks/${taskId}/submit`, data) as any;
|
||||
|
||||
// 修改任务提交
|
||||
export const updateTaskCompletion = (
|
||||
taskId: number,
|
||||
data: TaskSubmitRequest
|
||||
): Promise<TaskCompletion> =>
|
||||
http.put(`/v1/parent/tasks/${taskId}/submit`, data) as any;
|
||||
|
||||
// 获取提交详情
|
||||
export const getCompletionDetail = (
|
||||
completionId: number
|
||||
): Promise<TaskCompletion> =>
|
||||
http.get(`/v1/parent/tasks/completions/${completionId}`) as any;
|
||||
|
||||
// 获取教师评价
|
||||
export const getCompletionFeedback = (
|
||||
completionId: number
|
||||
): Promise<TeacherFeedback> =>
|
||||
http.get(`/v1/parent/tasks/completions/${completionId}/feedback`) as any;
|
||||
|
||||
// 兼容旧接口(保留向后兼容)
|
||||
export const submitTaskFeedback = (
|
||||
childId: number,
|
||||
taskId: number,
|
||||
|
||||
@ -889,11 +889,11 @@ export const getTaskTemplates = (params?: {
|
||||
keyword?: string;
|
||||
}) => http.get<{ list: TaskTemplate[]; total: number; pageNum: number; pageSize: number; pages: number }>('/v1/school/task-templates', { params })
|
||||
.then(res => ({
|
||||
list: res.list || res.records || [],
|
||||
total: Number(res.total) || 0,
|
||||
pageNum: Number(res.pageNum) || 1,
|
||||
pageSize: Number(res.pageSize) || 10,
|
||||
pages: Number(res.pages) || 0,
|
||||
list: res.list || [],
|
||||
total: res.total || 0,
|
||||
pageNum: res.pageNum || 1,
|
||||
pageSize: res.pageSize || 10,
|
||||
pages: res.pages || 0,
|
||||
}));
|
||||
|
||||
export const getTaskTemplate = (id: number) =>
|
||||
@ -971,6 +971,51 @@ export const getMonthlyTaskStats = (_months?: number) =>
|
||||
|
||||
// ==================== 任务管理 API ====================
|
||||
|
||||
// 任务完成状态(新设计)
|
||||
export type TaskCompletionStatus = 'PENDING' | 'SUBMITTED' | 'REVIEWED';
|
||||
|
||||
// 评价结果
|
||||
export type FeedbackResult = 'EXCELLENT' | 'PASSED' | 'NEEDS_WORK';
|
||||
|
||||
// 教师评价
|
||||
export interface TaskTeacherFeedback {
|
||||
id: number;
|
||||
result: FeedbackResult;
|
||||
resultText?: string;
|
||||
rating?: number;
|
||||
comment?: string;
|
||||
createdAt: string;
|
||||
teacherName?: string;
|
||||
}
|
||||
|
||||
// 任务完成记录(新设计)
|
||||
export interface TaskCompletionRecord {
|
||||
id: number;
|
||||
taskId: number;
|
||||
studentId: number;
|
||||
status: TaskCompletionStatus;
|
||||
statusText?: string;
|
||||
photos?: string[];
|
||||
videoUrl?: string;
|
||||
audioUrl?: string;
|
||||
content?: string;
|
||||
submittedAt?: string;
|
||||
reviewedAt?: string;
|
||||
createdAt: string;
|
||||
student?: {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
gender?: string;
|
||||
classInfo?: {
|
||||
id: number;
|
||||
name: string;
|
||||
grade?: string;
|
||||
};
|
||||
};
|
||||
feedback?: TaskTeacherFeedback;
|
||||
}
|
||||
|
||||
export interface SchoolTask {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
@ -979,6 +1024,7 @@ export interface SchoolTask {
|
||||
taskType: 'READING' | 'ACTIVITY' | 'HOMEWORK';
|
||||
targetType: 'CLASS' | 'STUDENT';
|
||||
relatedCourseId?: number;
|
||||
relatedBookName?: string;
|
||||
course?: {
|
||||
id: number;
|
||||
name: string;
|
||||
@ -987,27 +1033,24 @@ export interface SchoolTask {
|
||||
endDate: string;
|
||||
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
||||
createdBy: number;
|
||||
creatorName?: string;
|
||||
targetCount?: number;
|
||||
completionCount?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 旧接口类型(兼容)
|
||||
export interface TaskCompletion {
|
||||
id: number;
|
||||
taskId: number;
|
||||
studentId: number;
|
||||
studentName: string;
|
||||
className: string;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
|
||||
completedAt?: string;
|
||||
feedback?: string;
|
||||
parentFeedback?: string;
|
||||
rating?: number;
|
||||
student?: {
|
||||
id: number;
|
||||
name: string;
|
||||
gender?: string;
|
||||
class?: { id: number; name: string };
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateSchoolTaskDto {
|
||||
@ -1031,6 +1074,61 @@ export interface UpdateSchoolTaskDto {
|
||||
status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
||||
}
|
||||
|
||||
// 学校端任务查询参数(多维度筛选)
|
||||
export interface SchoolTaskQueryParams {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
classIds?: number[];
|
||||
teacherIds?: number[];
|
||||
dateType?: 'startDate' | 'endDate' | 'deadline';
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
completionRate?: 'high' | 'medium' | 'low';
|
||||
sortBy?: 'createdAt' | 'startDate' | 'endDate' | 'completionRate';
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// ========== 新 API:只读接口 ==========
|
||||
|
||||
// 获取学校端任务列表(只读,多维度筛选)
|
||||
export const getReadingTaskList = (params?: SchoolTaskQueryParams) =>
|
||||
http.get<{ list: SchoolTask[]; total: number; pageNum: number; pageSize: number; pages: number }>('/v1/school/reading-tasks', { params })
|
||||
.then(res => ({
|
||||
items: res.list || [],
|
||||
total: res.total || 0,
|
||||
pageNum: res.pageNum || 1,
|
||||
pageSize: res.pageSize || 10,
|
||||
pages: res.pages || 0,
|
||||
}));
|
||||
|
||||
// 获取任务详情(只读)
|
||||
export const getReadingTaskDetail = (taskId: number) =>
|
||||
http.get<SchoolTask>(`/v1/school/reading-tasks/${taskId}`);
|
||||
|
||||
// 获取任务完成情况列表
|
||||
export const getReadingTaskCompletions = (taskId: number, params?: {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
}) => http.get<{ list: TaskCompletionRecord[]; total: number; pageNum: number; pageSize: number }>(
|
||||
`/v1/school/reading-tasks/${taskId}/completions`,
|
||||
{ params }
|
||||
).then(res => ({
|
||||
items: res.list || [],
|
||||
total: res.total || 0,
|
||||
pageNum: res.pageNum || 1,
|
||||
pageSize: res.pageSize || 10,
|
||||
}));
|
||||
|
||||
// 获取学生提交详情
|
||||
export const getCompletionDetail = (completionId: number) =>
|
||||
http.get<TaskCompletionRecord>(`/v1/school/reading-tasks/completions/${completionId}`);
|
||||
|
||||
// ========== 旧 API(保留向后兼容,但不再使用) ==========
|
||||
|
||||
export const getSchoolTasks = (params?: {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
@ -1051,27 +1149,8 @@ export const updateSchoolTask = (id: number, data: UpdateSchoolTaskDto) =>
|
||||
export const deleteSchoolTask = (id: number) =>
|
||||
http.delete<{ message: string }>(`/v1/school/tasks/${id}`);
|
||||
|
||||
export interface TaskCompletionListResponse {
|
||||
items: TaskCompletion[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
stats: { PENDING: number; IN_PROGRESS: number; COMPLETED: number };
|
||||
}
|
||||
|
||||
export const getSchoolTaskCompletions = (
|
||||
taskId: number,
|
||||
params?: { page?: number; pageSize?: number; status?: string }
|
||||
) =>
|
||||
http.get<TaskCompletionListResponse>(`/v1/school/tasks/${taskId}/completions`, {
|
||||
params: { page: params?.page ?? 1, pageSize: params?.pageSize ?? 20, status: params?.status },
|
||||
}).then((res: TaskCompletionListResponse) => ({
|
||||
items: res.items ?? [],
|
||||
total: res.total ?? 0,
|
||||
page: res.page ?? 1,
|
||||
pageSize: res.pageSize ?? 20,
|
||||
stats: res.stats ?? { PENDING: 0, IN_PROGRESS: 0, COMPLETED: 0 },
|
||||
}));
|
||||
export const getSchoolTaskCompletions = (taskId: number) =>
|
||||
http.get<TaskCompletion[]>(`/v1/school/tasks/${taskId}/completions`);
|
||||
|
||||
export const getSchoolClasses = () =>
|
||||
http.get<ClassInfo[]>('/v1/school/classes');
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import { http } from './index';
|
||||
import type {
|
||||
TeacherCourseControllerGetTeacherSchedulesParams,
|
||||
TeacherCourseControllerGetTeacherTimetableParams,
|
||||
TeacherFeedbackControllerFindAllParams,
|
||||
} from './generated/model';
|
||||
|
||||
// ============= 类型定义(保持向后兼容) =============
|
||||
|
||||
@ -644,6 +639,7 @@ export interface TeacherTask {
|
||||
targetType: 'CLASS' | 'STUDENT';
|
||||
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
||||
relatedCourseId?: number;
|
||||
relatedBookName?: string; // 关联绘本名称
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
createdBy: number;
|
||||
@ -661,20 +657,47 @@ export interface TaskCompletion {
|
||||
id: number;
|
||||
taskId: number;
|
||||
studentId: number;
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
|
||||
completedAt?: string;
|
||||
feedback?: string;
|
||||
parentFeedback?: string;
|
||||
status: 'PENDING' | 'SUBMITTED' | 'REVIEWED';
|
||||
statusText?: string;
|
||||
photos?: string[];
|
||||
videoUrl?: string;
|
||||
audioUrl?: string;
|
||||
content?: string;
|
||||
parentFeedback?: string; // 家长反馈(兼容旧字段)
|
||||
submittedAt?: string;
|
||||
reviewedAt?: string;
|
||||
completedAt?: string; // 完成时间(兼容旧字段)
|
||||
createdAt: string;
|
||||
student: {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
gender?: string;
|
||||
class?: {
|
||||
id: number;
|
||||
name: string;
|
||||
grade?: string;
|
||||
};
|
||||
classInfo?: {
|
||||
id: number;
|
||||
name: string;
|
||||
grade?: string;
|
||||
};
|
||||
};
|
||||
feedback?: TaskFeedback;
|
||||
}
|
||||
|
||||
export interface TaskFeedback {
|
||||
id: number;
|
||||
completionId: number;
|
||||
result: 'EXCELLENT' | 'PASSED' | 'NEEDS_WORK';
|
||||
resultText?: string;
|
||||
rating?: number;
|
||||
comment?: string;
|
||||
teacherId?: number;
|
||||
teacherName?: string;
|
||||
teacherAvatar?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateTeacherTaskDto {
|
||||
@ -684,10 +707,17 @@ export interface CreateTeacherTaskDto {
|
||||
targetType: 'CLASS' | 'STUDENT';
|
||||
targetIds: number[];
|
||||
relatedCourseId?: number;
|
||||
relatedBookName?: string; // 关联绘本名称
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
export interface TaskFeedbackDto {
|
||||
result: 'EXCELLENT' | 'PASSED' | 'NEEDS_WORK';
|
||||
rating?: number;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTaskCompletionDto {
|
||||
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED';
|
||||
feedback?: string;
|
||||
@ -706,18 +736,34 @@ export const getTeacherTasks = (params?: { pageNum?: number; pageSize?: number;
|
||||
export const getTeacherTask = (id: number) =>
|
||||
http.get(`/v1/teacher/tasks/${id}`) as any;
|
||||
|
||||
// 获取教师任务完成情况
|
||||
export const getTeacherTaskCompletions = (taskId: number, params?: { page?: number; pageSize?: number; status?: string }) =>
|
||||
http.get<{ items: TaskCompletion[]; total: number; page: number; pageSize: number }>(
|
||||
// 获取任务完成情况列表(新版,支持分页和状态筛选)
|
||||
export const getTeacherTaskCompletions = (taskId: number, params?: {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
}) =>
|
||||
http.get<{ list: any[]; total: number; pageNum: number; pageSize: number }>(
|
||||
`/v1/teacher/tasks/${taskId}/completions`,
|
||||
{ params: { page: params?.page ?? 1, pageSize: params?.pageSize ?? 20, status: params?.status } }
|
||||
{ params }
|
||||
).then(res => ({
|
||||
items: res.items ?? res.list ?? [],
|
||||
total: res.total ?? 0,
|
||||
page: res.page ?? res.pageNum ?? 1,
|
||||
pageSize: res.pageSize ?? 20,
|
||||
items: res.list || [],
|
||||
total: res.total || 0,
|
||||
page: res.pageNum || 1,
|
||||
pageSize: res.pageSize || 10,
|
||||
}));
|
||||
|
||||
// 获取提交详情
|
||||
export const getCompletionDetail = (completionId: number) =>
|
||||
http.get<TaskCompletion>(`/v1/teacher/tasks/completions/${completionId}`) as any;
|
||||
|
||||
// 提交评价
|
||||
export const submitCompletionFeedback = (completionId: number, data: TaskFeedbackDto) =>
|
||||
http.post<TaskFeedback>(`/v1/teacher/tasks/completions/${completionId}/feedback`, data) as any;
|
||||
|
||||
// 修改评价
|
||||
export const updateCompletionFeedback = (completionId: number, data: TaskFeedbackDto) =>
|
||||
http.put<TaskFeedback>(`/v1/teacher/tasks/completions/${completionId}/feedback`, data) as any;
|
||||
|
||||
export const createTeacherTask = (data: CreateTeacherTaskDto) =>
|
||||
http.post('/v1/teacher/tasks', data) as any;
|
||||
|
||||
@ -727,12 +773,12 @@ export const updateTeacherTask = (id: number, data: Partial<CreateTeacherTaskDto
|
||||
export const deleteTeacherTask = (id: number) =>
|
||||
http.delete(`/v1/teacher/tasks/${id}`) as any;
|
||||
|
||||
// 更新任务完成状态
|
||||
export const updateTaskCompletion = (taskId: number, studentId: number, data: UpdateTaskCompletionDto) =>
|
||||
http.put(`/v1/teacher/tasks/${taskId}/completions/${studentId}`, data) as Promise<any>;
|
||||
// 兼容旧代码的函数
|
||||
export const updateTaskCompletion = (_taskId: number, _studentId: number, _data: UpdateTaskCompletionDto) =>
|
||||
Promise.reject(new Error('请使用 submitCompletionFeedback 或 updateCompletionFeedback'));
|
||||
|
||||
export const sendTaskReminder = (_taskId: number) =>
|
||||
Promise.reject(new Error('接口未实现'));
|
||||
http.post(`/v1/teacher/tasks/${_taskId}/remind`) as any;
|
||||
|
||||
// ==================== 任务模板 API ====================
|
||||
|
||||
@ -772,8 +818,8 @@ export interface CreateTaskFromTemplateDto {
|
||||
startDate?: string;
|
||||
}
|
||||
|
||||
export const getTaskTemplates = () =>
|
||||
http.get<{ records: any[]; total: number }>('/v1/teacher/task-templates')
|
||||
export const getTaskTemplates = (params?: { pageNum?: number; pageSize?: number }) =>
|
||||
http.get<{ records: any[]; total: number }>('/v1/teacher/task-templates', { params })
|
||||
.then(res => ({
|
||||
items: res.records || [],
|
||||
total: res.total || 0,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="task-list-view">
|
||||
<div class="page-header">
|
||||
<h2><CheckSquareOutlined /> 阅读任务</h2>
|
||||
<p class="page-desc">查看孩子的阅读任务并提交反馈</p>
|
||||
<p class="page-desc">查看孩子的阅读任务并提交完成情况</p>
|
||||
</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
@ -10,10 +10,15 @@
|
||||
<div v-for="task in tasks" :key="task.id" class="task-card">
|
||||
<div class="card-header">
|
||||
<div class="task-info">
|
||||
<h3>{{ task.task.title }}</h3>
|
||||
<div class="task-title-row">
|
||||
<h3>{{ task.task.title }}</h3>
|
||||
<a-tag v-if="task.task.relatedBookName" color="purple" size="small">
|
||||
<BookOutlined /> {{ task.task.relatedBookName }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span v-if="task.task.course?.name">
|
||||
<BookOutlined /> {{ task.task.course.name }}
|
||||
<FolderOutlined /> {{ task.task.course.name }}
|
||||
</span>
|
||||
<span>
|
||||
<ClockCircleOutlined />
|
||||
@ -30,18 +35,68 @@
|
||||
<p>{{ task.task.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="feedback-section" v-if="task.parentFeedback">
|
||||
<span class="feedback-label">您的反馈:</span>
|
||||
<span class="feedback-content">{{ task.parentFeedback }}</span>
|
||||
<!-- 已提交内容预览 -->
|
||||
<div class="submission-preview" v-if="task.status !== 'PENDING' && (task.photos?.length || task.videoUrl || task.audioUrl || task.content)">
|
||||
<div class="preview-title">已提交内容:</div>
|
||||
<div class="preview-items">
|
||||
<span v-if="task.photos?.length" class="preview-item">
|
||||
<PictureOutlined /> {{ task.photos.length }} 张照片
|
||||
</span>
|
||||
<span v-if="task.videoUrl" class="preview-item">
|
||||
<VideoCameraOutlined /> 有视频
|
||||
</span>
|
||||
<span v-if="task.audioUrl" class="preview-item">
|
||||
<SoundOutlined /> 有音频
|
||||
</span>
|
||||
<span v-if="task.content" class="preview-item">
|
||||
<FileTextOutlined /> {{ task.content.substring(0, 30) }}{{ task.content.length > 30 ? '...' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="submission-time" v-if="task.submittedAt">
|
||||
提交于 {{ formatDateTime(task.submittedAt) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 教师评价显示 -->
|
||||
<div class="teacher-feedback" v-if="task.teacherFeedback">
|
||||
<div class="feedback-header">
|
||||
<StarFilled style="color: #faad14;" />
|
||||
<span>教师评价</span>
|
||||
<a-tag :color="getFeedbackResultColor(task.teacherFeedback.result)" size="small">
|
||||
{{ getFeedbackResultText(task.teacherFeedback.result) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="feedback-rating" v-if="task.teacherFeedback.rating">
|
||||
<a-rate :value="task.teacherFeedback.rating" disabled :count="5" />
|
||||
</div>
|
||||
<div class="feedback-comment" v-if="task.teacherFeedback.comment">
|
||||
{{ task.teacherFeedback.comment }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="action-buttons">
|
||||
<!-- 待提交状态 -->
|
||||
<template v-if="task.status === 'PENDING'">
|
||||
<a-button type="primary" @click="openSubmitModal(task)">
|
||||
<EditOutlined /> 提交完成
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 已提交但未评价 -->
|
||||
<template v-else-if="task.status === 'SUBMITTED'">
|
||||
<a-button @click="openSubmitModal(task)">
|
||||
<EditOutlined /> 修改提交
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 已评价 -->
|
||||
<template v-else-if="task.status === 'REVIEWED'">
|
||||
<a-button type="link" @click="openFeedbackDetail(task)">
|
||||
<EyeOutlined /> 查看评价详情
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
<a-button
|
||||
v-else
|
||||
type="link"
|
||||
@click="openFeedbackModal(task)"
|
||||
>
|
||||
<EditOutlined /> 提交反馈
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,36 +107,172 @@
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<!-- 反馈弹窗 -->
|
||||
<!-- 提交任务弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="submitModalVisible"
|
||||
:title="isEditSubmit ? '修改提交' : '提交任务完成'"
|
||||
@ok="handleSubmit"
|
||||
:confirm-loading="submitting"
|
||||
width="600px"
|
||||
okText="提交"
|
||||
cancelText="取消"
|
||||
>
|
||||
<div class="task-info-header" v-if="selectedTask">
|
||||
<h4>{{ selectedTask.task.title }}</h4>
|
||||
<p v-if="selectedTask.task.relatedBookName">
|
||||
<BookOutlined /> {{ selectedTask.task.relatedBookName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="照片">
|
||||
<div class="photo-upload-area">
|
||||
<div class="photo-list">
|
||||
<div v-for="(photo, index) in submitForm.photos" :key="index" class="photo-item">
|
||||
<img :src="photo" alt="照片" />
|
||||
<div class="photo-remove" @click="removePhoto(index)">
|
||||
<CloseCircleOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-upload
|
||||
:customRequest="handlePhotoUpload"
|
||||
:showUploadList="false"
|
||||
accept="image/*"
|
||||
multiple
|
||||
>
|
||||
<div class="upload-btn">
|
||||
<PlusOutlined />
|
||||
<span>添加照片</span>
|
||||
</div>
|
||||
</a-upload>
|
||||
</div>
|
||||
<div class="form-tip">最多上传9张照片</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="视频链接">
|
||||
<a-input
|
||||
v-model:value="submitForm.videoUrl"
|
||||
placeholder="请输入视频链接(可选,如腾讯视频、优酷等分享链接)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="音频链接">
|
||||
<a-input
|
||||
v-model:value="submitForm.audioUrl"
|
||||
placeholder="请输入音频链接(可选)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="阅读心得">
|
||||
<a-textarea
|
||||
v-model:value="submitForm.content"
|
||||
placeholder="请描述孩子的阅读情况、感受等(可选)"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 评价详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="feedbackModalVisible"
|
||||
title="提交家长反馈"
|
||||
@ok="submitFeedback"
|
||||
:confirm-loading="submitting"
|
||||
title="教师评价详情"
|
||||
:footer="null"
|
||||
width="500px"
|
||||
>
|
||||
<a-textarea
|
||||
v-model:value="feedbackContent"
|
||||
placeholder="请输入您对孩子完成任务的反馈..."
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
<div class="feedback-detail" v-if="selectedTask?.teacherFeedback">
|
||||
<div class="feedback-result-section">
|
||||
<div class="result-label">评价结果</div>
|
||||
<a-tag :color="getFeedbackResultColor(selectedTask.teacherFeedback.result)" size="large">
|
||||
{{ getFeedbackResultText(selectedTask.teacherFeedback.result) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="feedback-rating-section" v-if="selectedTask.teacherFeedback.rating">
|
||||
<div class="rating-label">评分</div>
|
||||
<a-rate :value="selectedTask.teacherFeedback.rating" disabled :count="5" />
|
||||
<span class="rating-text">{{ selectedTask.teacherFeedback.rating }} 分</span>
|
||||
</div>
|
||||
|
||||
<div class="feedback-comment-section" v-if="selectedTask.teacherFeedback.comment">
|
||||
<div class="comment-label">教师评语</div>
|
||||
<div class="comment-content">{{ selectedTask.teacherFeedback.comment }}</div>
|
||||
</div>
|
||||
|
||||
<div class="feedback-time">
|
||||
评价时间: {{ formatDateTime(selectedTask.teacherFeedback.createdAt) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交内容回顾 -->
|
||||
<div class="submission-review" v-if="selectedTask">
|
||||
<a-divider>提交内容</a-divider>
|
||||
<div class="review-photos" v-if="selectedTask.photos?.length">
|
||||
<div class="photos-grid">
|
||||
<img v-for="(photo, idx) in selectedTask.photos" :key="idx" :src="photo" alt="照片" @click="previewImage(photo)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-links" v-if="selectedTask.videoUrl || selectedTask.audioUrl">
|
||||
<a v-if="selectedTask.videoUrl" :href="selectedTask.videoUrl" target="_blank">
|
||||
<VideoCameraOutlined /> 查看视频
|
||||
</a>
|
||||
<a v-if="selectedTask.audioUrl" :href="selectedTask.audioUrl" target="_blank">
|
||||
<SoundOutlined /> 收听音频
|
||||
</a>
|
||||
</div>
|
||||
<div class="review-content" v-if="selectedTask.content">
|
||||
<div class="content-label">阅读心得</div>
|
||||
<p>{{ selectedTask.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<a-image
|
||||
:style="{ display: 'none' }"
|
||||
:preview="{
|
||||
visible: imagePreviewVisible,
|
||||
onVisibleChange: (visible: boolean) => { imagePreviewVisible = visible; },
|
||||
}"
|
||||
:src="previewImageUrl"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { message, Upload } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
|
||||
import {
|
||||
CheckSquareOutlined,
|
||||
BookOutlined,
|
||||
ClockCircleOutlined,
|
||||
EditOutlined,
|
||||
InboxOutlined,
|
||||
EyeOutlined,
|
||||
PlusOutlined,
|
||||
CloseCircleOutlined,
|
||||
PictureOutlined,
|
||||
VideoCameraOutlined,
|
||||
SoundOutlined,
|
||||
FileTextOutlined,
|
||||
FolderOutlined,
|
||||
StarFilled,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { getChildTasks, submitTaskFeedback, getChildren, type TaskWithCompletion } from '@/api/parent';
|
||||
import {
|
||||
getChildTasks,
|
||||
submitTaskCompletion,
|
||||
updateTaskCompletion,
|
||||
type TaskWithCompletion,
|
||||
type TaskSubmitRequest,
|
||||
} from '@/api/parent';
|
||||
import { uploadFile } from '@/api/file';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const route = useRoute();
|
||||
@ -90,32 +281,86 @@ const loading = ref(false);
|
||||
const tasks = ref<TaskWithCompletion[]>([]);
|
||||
const currentChildId = ref<number | null>(null);
|
||||
|
||||
const feedbackModalVisible = ref(false);
|
||||
const feedbackContent = ref('');
|
||||
const selectedTask = ref<TaskWithCompletion | null>(null);
|
||||
// 提交弹窗
|
||||
const submitModalVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
const selectedTask = ref<TaskWithCompletion | null>(null);
|
||||
const isEditSubmit = ref(false);
|
||||
const submitForm = reactive({
|
||||
photos: [] as string[],
|
||||
videoUrl: '',
|
||||
audioUrl: '',
|
||||
content: '',
|
||||
});
|
||||
|
||||
// 评价详情弹窗
|
||||
const feedbackModalVisible = ref(false);
|
||||
|
||||
// 图片预览
|
||||
const imagePreviewVisible = ref(false);
|
||||
const previewImageUrl = ref('');
|
||||
|
||||
// 状态映射(新设计)
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
PENDING: { text: '待完成', color: 'orange' },
|
||||
IN_PROGRESS: { text: '进行中', color: 'blue' },
|
||||
COMPLETED: { text: '已完成', color: 'green' },
|
||||
PENDING: { text: '待提交', color: 'orange' },
|
||||
SUBMITTED: { text: '已提交', color: 'blue' },
|
||||
REVIEWED: { text: '已评价', color: 'green' },
|
||||
};
|
||||
|
||||
// 评价结果映射
|
||||
const feedbackResultMap: Record<string, { text: string; color: string }> = {
|
||||
EXCELLENT: { text: '优秀', color: 'gold' },
|
||||
PASSED: { text: '通过', color: 'green' },
|
||||
NEEDS_WORK: { text: '需改进', color: 'orange' },
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => statusMap[status]?.text || status;
|
||||
const getStatusColor = (status: string) => statusMap[status]?.color || 'default';
|
||||
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD');
|
||||
const getFeedbackResultText = (result: string) => feedbackResultMap[result]?.text || result;
|
||||
const getFeedbackResultColor = (result: string) => feedbackResultMap[result]?.color || 'default';
|
||||
|
||||
const openFeedbackModal = (task: TaskWithCompletion) => {
|
||||
const formatDate = (date?: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-';
|
||||
const formatDateTime = (date?: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-';
|
||||
|
||||
// 打开提交弹窗
|
||||
const openSubmitModal = (task: TaskWithCompletion) => {
|
||||
selectedTask.value = task;
|
||||
feedbackContent.value = task.parentFeedback || '';
|
||||
feedbackModalVisible.value = true;
|
||||
isEditSubmit.value = task.status !== 'PENDING';
|
||||
|
||||
// 填充已有数据
|
||||
submitForm.photos = task.photos || [];
|
||||
submitForm.videoUrl = task.videoUrl || '';
|
||||
submitForm.audioUrl = task.audioUrl || '';
|
||||
submitForm.content = task.content || '';
|
||||
|
||||
submitModalVisible.value = true;
|
||||
};
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (!selectedTask.value || !feedbackContent.value.trim()) {
|
||||
message.warning('请输入反馈内容');
|
||||
return;
|
||||
// 处理照片上传
|
||||
const handlePhotoUpload = async (options: UploadRequestOption) => {
|
||||
const { file } = options;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file as File);
|
||||
const url = await uploadFile(formData);
|
||||
if (submitForm.photos.length < 9) {
|
||||
submitForm.photos.push(url);
|
||||
} else {
|
||||
message.warning('最多上传9张照片');
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error('上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 移除照片
|
||||
const removePhoto = (index: number) => {
|
||||
submitForm.photos.splice(index, 1);
|
||||
};
|
||||
|
||||
// 提交任务
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedTask.value) return;
|
||||
|
||||
const childId = currentChildId.value || (route.query.childId ? Number(route.query.childId) : null);
|
||||
if (!childId) {
|
||||
@ -125,9 +370,23 @@ const submitFeedback = async () => {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await submitTaskFeedback(childId, selectedTask.value.task.id, feedbackContent.value);
|
||||
message.success('反馈提交成功');
|
||||
feedbackModalVisible.value = false;
|
||||
const data: TaskSubmitRequest = {
|
||||
studentId: childId,
|
||||
photos: submitForm.photos.length > 0 ? submitForm.photos : undefined,
|
||||
videoUrl: submitForm.videoUrl || undefined,
|
||||
audioUrl: submitForm.audioUrl || undefined,
|
||||
content: submitForm.content || undefined,
|
||||
};
|
||||
|
||||
if (isEditSubmit.value) {
|
||||
await updateTaskCompletion(selectedTask.value.task.id, data);
|
||||
message.success('修改成功');
|
||||
} else {
|
||||
await submitTaskCompletion(selectedTask.value.task.id, data);
|
||||
message.success('提交成功');
|
||||
}
|
||||
|
||||
submitModalVisible.value = false;
|
||||
loadTasks();
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.message || '提交失败');
|
||||
@ -136,6 +395,19 @@ const submitFeedback = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 打开评价详情
|
||||
const openFeedbackDetail = (task: TaskWithCompletion) => {
|
||||
selectedTask.value = task;
|
||||
feedbackModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (url: string) => {
|
||||
previewImageUrl.value = url;
|
||||
imagePreviewVisible.value = true;
|
||||
};
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@ -143,21 +415,18 @@ const loadTasks = async () => {
|
||||
|
||||
// 如果没有指定childId,自动获取第一个孩子
|
||||
if (!childId) {
|
||||
const { getChildren } = await import('@/api/parent');
|
||||
const children = await getChildren();
|
||||
if (children && children.length > 0) {
|
||||
childId = children[0].id;
|
||||
// 更新URL以便后续操作(如提交反馈)可以使用
|
||||
router.replace({ query: { childId: String(childId) } });
|
||||
} else {
|
||||
// 没有孩子信息,显示空状态
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存当前childId供后续使用
|
||||
currentChildId.value = childId;
|
||||
|
||||
const data = await getChildTasks(childId);
|
||||
tasks.value = data.items;
|
||||
} catch (error: any) {
|
||||
@ -216,6 +485,13 @@ $primary-light: #f6ffed;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.task-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
@ -247,21 +523,74 @@ $primary-light: #f6ffed;
|
||||
}
|
||||
}
|
||||
|
||||
.submission-preview {
|
||||
background: #f6ffed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.preview-title {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.preview-item {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.submission-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-feedback {
|
||||
background: #fffbe6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.feedback-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-rating {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feedback-comment {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.feedback-section {
|
||||
.feedback-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.feedback-content {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -278,6 +607,181 @@ $primary-light: #f6ffed;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交弹窗样式
|
||||
.task-info-header {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-upload-area {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.photo-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-remove {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #ff4d4f;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// 评价详情弹窗样式
|
||||
.feedback-detail {
|
||||
.feedback-result-section,
|
||||
.feedback-rating-section {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.result-label,
|
||||
.rating-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.rating-text {
|
||||
margin-left: 8px;
|
||||
color: #faad14;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-comment-section {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.comment-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
background: #fafafa;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.submission-review {
|
||||
.review-photos {
|
||||
.photos-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.review-links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 12px 0;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.review-content {
|
||||
.content-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============== 移动端响应式 ===============
|
||||
@media screen and (max-width: 768px) {
|
||||
.task-list-view {
|
||||
@ -326,17 +830,16 @@ $primary-light: #f6ffed;
|
||||
}
|
||||
}
|
||||
|
||||
.submission-preview,
|
||||
.teacher-feedback {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: 10px;
|
||||
|
||||
.feedback-section {
|
||||
.feedback-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.feedback-content {
|
||||
font-size: 13px;
|
||||
}
|
||||
.action-buttons {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -349,13 +852,12 @@ $primary-light: #f6ffed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 平板适配
|
||||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||||
.task-list-view {
|
||||
.task-card {
|
||||
padding: 18px;
|
||||
.photo-upload-area {
|
||||
.photo-item,
|
||||
.upload-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,7 @@
|
||||
<div class="task-list-view">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h2>
|
||||
<CheckSquareOutlined /> 阅读任务
|
||||
</h2>
|
||||
<h2><CheckSquareOutlined /> 阅读任务</h2>
|
||||
<p class="page-desc">管理班级阅读任务,跟踪学生完成情况</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="openCreateModal">
|
||||
@ -46,20 +44,35 @@
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<a-space :size="16">
|
||||
<a-select v-model:value="filters.status" placeholder="任务状态" style="width: 120px;" allowClear
|
||||
@change="loadTasks">
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
placeholder="任务状态"
|
||||
style="width: 120px;"
|
||||
allowClear
|
||||
@change="loadTasks"
|
||||
>
|
||||
<a-select-option value="PUBLISHED">进行中</a-select-option>
|
||||
<a-select-option value="DRAFT">草稿</a-select-option>
|
||||
<a-select-option value="ARCHIVED">已归档</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filters.taskType" placeholder="任务类型" style="width: 120px;" allowClear
|
||||
@change="loadTasks">
|
||||
<a-select
|
||||
v-model:value="filters.taskType"
|
||||
placeholder="任务类型"
|
||||
style="width: 120px;"
|
||||
allowClear
|
||||
@change="loadTasks"
|
||||
>
|
||||
<a-select-option value="READING">阅读</a-select-option>
|
||||
<a-select-option value="ACTIVITY">活动</a-select-option>
|
||||
<a-select-option value="HOMEWORK">作业</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search v-model:value="filters.keyword" placeholder="搜索任务标题" style="width: 200px;" @search="loadTasks"
|
||||
allow-clear />
|
||||
<a-input-search
|
||||
v-model:value="filters.keyword"
|
||||
placeholder="搜索任务标题"
|
||||
style="width: 200px;"
|
||||
@search="loadTasks"
|
||||
allow-clear
|
||||
/>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
@ -92,8 +105,12 @@
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="progress-info">
|
||||
<a-progress :percent="getCompletionRate(task)" :stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
||||
size="small" style="width: 150px;" />
|
||||
<a-progress
|
||||
:percent="getCompletionRate(task)"
|
||||
:stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
||||
size="small"
|
||||
style="width: 150px;"
|
||||
/>
|
||||
<span class="progress-text">{{ getCompletedCount(task) }}/{{ task.targetCount || 0 }} 人完成</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@ -137,18 +154,34 @@
|
||||
</a-spin>
|
||||
|
||||
<div class="pagination-section" v-if="total > pageSize">
|
||||
<a-pagination v-model:current="currentPage" :total="total" :page-size="pageSize" @change="onPageChange"
|
||||
show-quick-jumper :show-total="(total: number) => `共 ${total} 条`" />
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
@change="onPageChange"
|
||||
show-quick-jumper
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑任务弹窗 -->
|
||||
<a-modal v-model:open="createModalVisible" :title="isEdit ? '编辑任务' : '新建阅读任务'" @ok="handleCreate"
|
||||
:confirm-loading="creating" width="600px">
|
||||
<a-modal
|
||||
v-model:open="createModalVisible"
|
||||
:title="isEdit ? '编辑任务' : '新建阅读任务'"
|
||||
@ok="handleCreate"
|
||||
:confirm-loading="creating"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="createForm" layout="vertical">
|
||||
<!-- 模板选择(仅新建时显示) -->
|
||||
<a-form-item label="使用模板" v-if="!isEdit">
|
||||
<a-select v-model:value="selectedTemplateId" placeholder="选择模板快速填充(可选)" style="width: 100%;" allowClear
|
||||
@change="onTemplateSelect">
|
||||
<a-select
|
||||
v-model:value="selectedTemplateId"
|
||||
placeholder="选择模板快速填充(可选)"
|
||||
style="width: 100%;"
|
||||
allowClear
|
||||
@change="onTemplateSelect as any"
|
||||
>
|
||||
<a-select-option v-for="tpl in templates" :key="tpl.id" :value="tpl.id">
|
||||
{{ tpl.name }}
|
||||
<a-tag size="small" :color="getTaskTypeColor(tpl.taskType)" style="margin-left: 8px;">
|
||||
@ -183,50 +216,86 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="选择目标" required v-if="createForm.targetType === 'CLASS'">
|
||||
<a-select v-model:value="createForm.targetIds" mode="multiple" placeholder="请选择班级" style="width: 100%;">
|
||||
<a-select
|
||||
v-model:value="createForm.targetIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择班级"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
|
||||
{{ cls.name }} ({{ cls.grade }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="选择学生" required v-if="createForm.targetType === 'STUDENT'">
|
||||
<a-select v-model:value="createForm.targetIds" mode="multiple" placeholder="请选择学生" style="width: 100%;"
|
||||
:filter-option="filterStudentOption" show-search>
|
||||
<a-select
|
||||
v-model:value="createForm.targetIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择学生"
|
||||
style="width: 100%;"
|
||||
:filter-option="filterStudentOption"
|
||||
show-search
|
||||
>
|
||||
<a-select-option v-for="student in students" :key="student.id" :value="student.id">
|
||||
{{ student.name }} - {{ student.class?.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联课程">
|
||||
<a-select v-model:value="createForm.relatedCourseId" placeholder="可选,关联课程包" style="width: 100%;" allowClear
|
||||
show-search :filter-option="filterCourseOption">
|
||||
<a-select
|
||||
v-model:value="createForm.relatedCourseId"
|
||||
placeholder="可选,关联课程包"
|
||||
style="width: 100%;"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterCourseOption"
|
||||
>
|
||||
<a-select-option v-for="course in courses" :key="course.id" :value="course.id">
|
||||
{{ course.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="关联绘本">
|
||||
<a-input
|
||||
v-model:value="createForm.relatedBookName"
|
||||
placeholder="请输入关联的绘本名称(可选)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="任务时间" required>
|
||||
<a-range-picker v-model:value="createForm.dateRange" style="width: 100%;" />
|
||||
<a-range-picker
|
||||
v-model:value="createForm.dateRange as any"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 完成情况详情弹窗 -->
|
||||
<a-modal v-model:open="completionModalVisible" :title="`完成情况 - ${selectedTask?.title || ''}`" width="800px"
|
||||
:footer="null">
|
||||
<a-modal
|
||||
v-model:open="completionModalVisible"
|
||||
:title="`完成情况 - ${selectedTask?.title || ''}`"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="completion-header">
|
||||
<a-space>
|
||||
<a-select v-model:value="completionFilter.status" placeholder="筛选状态" style="width: 120px;" allowClear
|
||||
@change="loadCompletions">
|
||||
<a-select-option value="PENDING">待完成</a-select-option>
|
||||
<a-select-option value="IN_PROGRESS">进行中</a-select-option>
|
||||
<a-select-option value="COMPLETED">已完成</a-select-option>
|
||||
<a-select
|
||||
v-model:value="completionFilter.status"
|
||||
placeholder="筛选状态"
|
||||
style="width: 120px;"
|
||||
allowClear
|
||||
@change="loadCompletions"
|
||||
>
|
||||
<a-select-option value="PENDING">待提交</a-select-option>
|
||||
<a-select-option value="SUBMITTED">已提交</a-select-option>
|
||||
<a-select-option value="REVIEWED">已评价</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
<div class="completion-stats">
|
||||
<a-tag color="orange">{{ completionStats.pending }} 待完成</a-tag>
|
||||
<a-tag color="blue">{{ completionStats.inProgress }} 进行中</a-tag>
|
||||
<a-tag color="green">{{ completionStats.completed }} 已完成</a-tag>
|
||||
<a-tag color="orange">{{ completionStats.pending }} 待提交</a-tag>
|
||||
<a-tag color="blue">{{ completionStats.submitted }} 已提交</a-tag>
|
||||
<a-tag color="green">{{ completionStats.reviewed }} 已评价</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -239,31 +308,45 @@
|
||||
</a-avatar>
|
||||
<div class="student-detail">
|
||||
<div class="student-name">{{ completion.student.name }}</div>
|
||||
<div class="student-class">{{ completion.student.class?.name || '-' }}</div>
|
||||
<div class="student-class">{{ completion.student.class?.name || completion.student.classInfo?.name || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="completion-status">
|
||||
<a-select :value="completion.status" style="width: 100px;" size="small"
|
||||
@change="(val: string) => updateCompletionStatus(completion, val)">
|
||||
<a-select-option value="PENDING">待完成</a-select-option>
|
||||
<a-select-option value="IN_PROGRESS">进行中</a-select-option>
|
||||
<a-select-option value="COMPLETED">已完成</a-select-option>
|
||||
</a-select>
|
||||
<a-tag :color="getCompletionStatusColor(completion.status)">
|
||||
{{ getCompletionStatusText(completion.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="completion-content">
|
||||
<div v-if="completion.photos && completion.photos.length > 0" class="content-preview">
|
||||
<PictureOutlined /> {{ completion.photos.length }} 张照片
|
||||
</div>
|
||||
<div v-if="completion.videoUrl" class="content-preview">
|
||||
<VideoCameraOutlined /> 有视频
|
||||
</div>
|
||||
<div v-if="completion.content" class="content-preview">
|
||||
<FileTextFilled /> {{ completion.content.substring(0, 20) }}...
|
||||
</div>
|
||||
</div>
|
||||
<div class="completion-feedback">
|
||||
<div v-if="completion.parentFeedback" class="parent-feedback">
|
||||
<div v-if="completion.parentFeedback || completion.feedback?.comment" class="parent-feedback">
|
||||
<MessageOutlined class="feedback-icon" />
|
||||
<a-tooltip :title="completion.parentFeedback">
|
||||
<span class="feedback-text">{{ completion.parentFeedback.substring(0, 30) }}{{
|
||||
completion.parentFeedback.length > 30 ? '...' : '' }}</span>
|
||||
<a-tooltip :title="completion.parentFeedback || completion.feedback?.comment">
|
||||
<span class="feedback-text">{{ (completion.parentFeedback || completion.feedback?.comment || '').substring(0, 30) }}{{ (completion.parentFeedback || completion.feedback?.comment || '').length > 30 ? '...' : '' }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<span v-else class="no-feedback">暂无家长反馈</span>
|
||||
<span v-else class="no-feedback">暂无反馈</span>
|
||||
</div>
|
||||
<div class="completion-actions">
|
||||
<a-button type="link" size="small" @click="openFeedbackModal(completion)">
|
||||
<StarFilled v-if="completion.feedback" style="color: #faad14;" />
|
||||
<EditOutlined v-else />
|
||||
{{ completion.feedback ? '查看评价' : '评价' }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="completion-time">
|
||||
<span v-if="completion.completedAt">
|
||||
<span v-if="completion.submittedAt || completion.completedAt">
|
||||
<CheckCircleOutlined style="color: #52c41a;" />
|
||||
{{ formatDate(completion.completedAt) }}
|
||||
{{ formatDate(completion.submittedAt || completion.completedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -275,10 +358,78 @@
|
||||
</a-spin>
|
||||
|
||||
<div class="pagination-section" v-if="completionTotal > completionPageSize" style="margin-top: 16px;">
|
||||
<a-pagination v-model:current="completionPage" :total="completionTotal" :page-size="completionPageSize"
|
||||
size="small" @change="onCompletionPageChange" />
|
||||
<a-pagination
|
||||
v-model:current="completionPage"
|
||||
:total="completionTotal"
|
||||
:page-size="completionPageSize"
|
||||
size="small"
|
||||
@change="onCompletionPageChange"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 评价弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="feedbackModalVisible"
|
||||
:title="selectedCompletionForFeedback ? `评价 - ${selectedCompletionForFeedback.student?.name || ''}` : '评价'"
|
||||
width="600px"
|
||||
:confirm-loading="submittingFeedback"
|
||||
@ok="submitFeedback"
|
||||
okText="提交评价"
|
||||
cancelText="取消"
|
||||
>
|
||||
<a-form layout="vertical" v-if="selectedCompletionForFeedback">
|
||||
<!-- 提交内容预览 -->
|
||||
<div class="submission-preview" style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;">
|
||||
<h4 style="margin-bottom: 12px;">提交内容</h4>
|
||||
<div v-if="selectedCompletionForFeedback.photos && selectedCompletionForFeedback.photos.length > 0" style="margin-bottom: 12px;">
|
||||
<PictureOutlined style="margin-right: 8px;" />
|
||||
<span>{{ selectedCompletionForFeedback.photos.length }} 张照片</span>
|
||||
</div>
|
||||
<div v-if="selectedCompletionForFeedback.videoUrl" style="margin-bottom: 12px;">
|
||||
<VideoCameraOutlined style="margin-right: 8px;" />
|
||||
<a :href="selectedCompletionForFeedback.videoUrl" target="_blank">查看视频</a>
|
||||
</div>
|
||||
<div v-if="selectedCompletionForFeedback.audioUrl" style="margin-bottom: 12px;">
|
||||
<SoundOutlined style="margin-right: 8px;" />
|
||||
<a :href="selectedCompletionForFeedback.audioUrl" target="_blank">收听音频</a>
|
||||
</div>
|
||||
<div v-if="selectedCompletionForFeedback.content">
|
||||
<FileTextFilled style="margin-right: 8px;" />
|
||||
<p style="margin: 8px 0 0; white-space: pre-wrap;">{{ selectedCompletionForFeedback.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-form-item label="评价结果" required>
|
||||
<a-radio-group v-model:value="feedbackForm.result">
|
||||
<a-radio-button value="EXCELLENT">
|
||||
<StarFilled style="color: #faad14;" /> 优秀
|
||||
</a-radio-button>
|
||||
<a-radio-button value="PASSED">
|
||||
<CheckCircleOutlined style="color: #52c41a;" /> 通过
|
||||
</a-radio-button>
|
||||
<a-radio-button value="NEEDS_WORK">
|
||||
<SyncOutlined style="color: #fa8c16;" /> 需改进
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="评分">
|
||||
<a-rate v-model:value="feedbackForm.rating" :count="5" allow-half />
|
||||
<span style="margin-left: 8px; color: #999;">{{ feedbackForm.rating }} 分</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="评语">
|
||||
<a-textarea
|
||||
v-model:value="feedbackForm.comment"
|
||||
placeholder="请输入评语(选填,最多500字)"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -302,6 +453,11 @@ import {
|
||||
SyncOutlined,
|
||||
CheckCircleOutlined,
|
||||
MessageOutlined,
|
||||
StarFilled,
|
||||
PictureOutlined,
|
||||
VideoCameraOutlined,
|
||||
SoundOutlined,
|
||||
FileTextFilled,
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
getTeacherTasks,
|
||||
@ -309,7 +465,8 @@ import {
|
||||
updateTeacherTask,
|
||||
deleteTeacherTask,
|
||||
getTeacherTaskCompletions,
|
||||
updateTaskCompletion,
|
||||
submitCompletionFeedback,
|
||||
updateCompletionFeedback,
|
||||
sendTaskReminder,
|
||||
getTeacherClasses,
|
||||
getTeacherStudents,
|
||||
@ -317,6 +474,7 @@ import {
|
||||
getTaskTemplates,
|
||||
type TeacherTask,
|
||||
type TaskCompletion,
|
||||
type TaskFeedbackDto,
|
||||
type TaskTemplate,
|
||||
} from '@/api/teacher';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
@ -367,6 +525,7 @@ const createForm = reactive({
|
||||
targetType: 'CLASS' as 'CLASS' | 'STUDENT',
|
||||
targetIds: [] as number[],
|
||||
relatedCourseId: undefined as number | undefined,
|
||||
relatedBookName: '', // 关联绘本名称
|
||||
dateRange: null as [Dayjs, Dayjs] | null,
|
||||
});
|
||||
|
||||
@ -387,8 +546,8 @@ const completionStats = computed(() => {
|
||||
const all = completions.value;
|
||||
return {
|
||||
pending: all.filter(c => c.status === 'PENDING').length,
|
||||
inProgress: all.filter(c => c.status === 'IN_PROGRESS').length,
|
||||
completed: all.filter(c => c.status === 'COMPLETED').length,
|
||||
submitted: all.filter(c => c.status === 'SUBMITTED').length,
|
||||
reviewed: all.filter(c => c.status === 'REVIEWED').length,
|
||||
};
|
||||
});
|
||||
|
||||
@ -409,7 +568,7 @@ const getTypeText = (type: string) => typeMap[type]?.text || type;
|
||||
const getTypeColor = (type: string) => typeMap[type]?.color || 'default';
|
||||
const getStatusText = (status: string) => statusMap[status]?.text || status;
|
||||
const getStatusColor = (status: string) => statusMap[status]?.color || 'default';
|
||||
const formatDate = (date: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-';
|
||||
const formatDate = (date?: string) => date ? dayjs(date).format('YYYY-MM-DD') : '-';
|
||||
|
||||
const getCompletionRate = (task: TeacherTask) => {
|
||||
if (!task.completionCount || !task.targetCount) return 0;
|
||||
@ -435,7 +594,7 @@ const loadTasks = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getTeacherTasks({
|
||||
page: currentPage.value,
|
||||
pageNum: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
...filters,
|
||||
});
|
||||
@ -480,6 +639,7 @@ const openCreateModal = async () => {
|
||||
createForm.targetType = 'CLASS';
|
||||
createForm.targetIds = [];
|
||||
createForm.relatedCourseId = undefined;
|
||||
createForm.relatedBookName = ''; // 重置绘本名称
|
||||
createForm.dateRange = null;
|
||||
createModalVisible.value = true;
|
||||
|
||||
@ -543,6 +703,7 @@ const openEditModal = (task: TeacherTask) => {
|
||||
createForm.taskType = task.taskType;
|
||||
createForm.targetType = task.targetType;
|
||||
createForm.relatedCourseId = task.relatedCourseId;
|
||||
createForm.relatedBookName = task.relatedBookName || ''; // 添加绘本名称
|
||||
createForm.dateRange = [
|
||||
dayjs(task.startDate),
|
||||
dayjs(task.endDate),
|
||||
@ -571,6 +732,7 @@ const handleCreate = async () => {
|
||||
targetType: createForm.targetType,
|
||||
targetIds: createForm.targetIds,
|
||||
relatedCourseId: createForm.relatedCourseId,
|
||||
relatedBookName: createForm.relatedBookName || undefined, // 添加绘本名称
|
||||
startDate: createForm.dateRange[0].format('YYYY-MM-DD'),
|
||||
endDate: createForm.dateRange[1].format('YYYY-MM-DD'),
|
||||
};
|
||||
@ -641,13 +803,87 @@ const viewCompletionDetail = async (task: TeacherTask) => {
|
||||
loadCompletions();
|
||||
};
|
||||
|
||||
// 完成状态相关
|
||||
const getCompletionStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'PENDING': 'orange',
|
||||
'SUBMITTED': 'blue',
|
||||
'REVIEWED': 'green',
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
};
|
||||
|
||||
const getCompletionStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
'PENDING': '待提交',
|
||||
'SUBMITTED': '已提交',
|
||||
'REVIEWED': '已评价',
|
||||
};
|
||||
return textMap[status] || status;
|
||||
};
|
||||
|
||||
// 评价弹窗相关
|
||||
const feedbackModalVisible = ref(false);
|
||||
const selectedCompletion = ref<TaskCompletion | null>(null);
|
||||
const selectedCompletionForFeedback = computed(() => selectedCompletion.value);
|
||||
const feedbackForm = reactive({
|
||||
result: 'PASSED' as 'EXCELLENT' | 'PASSED' | 'NEEDS_WORK',
|
||||
rating: 5,
|
||||
comment: '',
|
||||
});
|
||||
const submittingFeedback = ref(false);
|
||||
|
||||
const openFeedbackModal = (completion: TaskCompletion) => {
|
||||
selectedCompletion.value = completion;
|
||||
if (completion.feedback) {
|
||||
// 编辑现有评价
|
||||
feedbackForm.result = completion.feedback.result || 'PASSED';
|
||||
feedbackForm.rating = completion.feedback.rating || 5;
|
||||
feedbackForm.comment = completion.feedback.comment || '';
|
||||
} else {
|
||||
// 新建评价
|
||||
feedbackForm.result = 'PASSED';
|
||||
feedbackForm.rating = 5;
|
||||
feedbackForm.comment = '';
|
||||
}
|
||||
feedbackModalVisible.value = true;
|
||||
};
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (!selectedCompletion.value) return;
|
||||
|
||||
submittingFeedback.value = true;
|
||||
try {
|
||||
const data: TaskFeedbackDto = {
|
||||
result: feedbackForm.result,
|
||||
rating: feedbackForm.rating,
|
||||
comment: feedbackForm.comment,
|
||||
};
|
||||
|
||||
if (selectedCompletion.value.feedback) {
|
||||
await updateCompletionFeedback(selectedCompletion.value.id, data);
|
||||
message.success('评价已更新');
|
||||
} else {
|
||||
await submitCompletionFeedback(selectedCompletion.value.id, data);
|
||||
message.success('评价已提交');
|
||||
}
|
||||
|
||||
feedbackModalVisible.value = false;
|
||||
loadCompletions(); // 刷新列表
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '评价失败');
|
||||
} finally {
|
||||
submittingFeedback.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadCompletions = async () => {
|
||||
if (!selectedTask.value) return;
|
||||
|
||||
loadingCompletions.value = true;
|
||||
try {
|
||||
const data = await getTeacherTaskCompletions(selectedTask.value.id, {
|
||||
page: completionPage.value,
|
||||
pageNum: completionPage.value,
|
||||
pageSize: completionPageSize.value,
|
||||
status: completionFilter.status,
|
||||
});
|
||||
@ -665,21 +901,6 @@ const onCompletionPageChange = (page: number) => {
|
||||
loadCompletions();
|
||||
};
|
||||
|
||||
// 更新完成状态
|
||||
const updateCompletionStatus = async (completion: TaskCompletion, status: string) => {
|
||||
try {
|
||||
await updateTaskCompletion(completion.taskId, completion.studentId, { status: status as any });
|
||||
completion.status = status as any;
|
||||
if (status === 'COMPLETED') {
|
||||
completion.completedAt = new Date().toISOString();
|
||||
}
|
||||
message.success('状态已更新');
|
||||
} catch (error: any) {
|
||||
message.error('更新失败', error);
|
||||
loadCompletions();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadTasks();
|
||||
loadOptions();
|
||||
|
||||
@ -5,17 +5,22 @@ import com.reading.platform.common.mapper.TaskMapper;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
import com.reading.platform.dto.request.TaskSubmitRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskFeedbackResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.service.TaskFeedbackService;
|
||||
import com.reading.platform.service.TaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "Parent - Task", description = "Task APIs for Parent")
|
||||
@Tag(name = "家长端 - 任务管理", description = "家长端任务 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/parent/tasks")
|
||||
@RequiredArgsConstructor
|
||||
@ -23,15 +28,16 @@ public class ParentTaskController {
|
||||
|
||||
private final TaskService taskService;
|
||||
private final TaskMapper taskMapper;
|
||||
private final TaskFeedbackService taskFeedbackService;
|
||||
|
||||
@Operation(summary = "Get task by ID")
|
||||
@Operation(summary = "获取任务详情")
|
||||
@GetMapping("/{id}")
|
||||
public Result<TaskResponse> getTask(@PathVariable Long id) {
|
||||
Task task = taskService.getTaskById(id);
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get tasks by student ID")
|
||||
@Operation(summary = "获取孩子的任务列表")
|
||||
@GetMapping("/student/{studentId}")
|
||||
public Result<PageResult<TaskResponse>> getTasksByStudent(
|
||||
@PathVariable Long studentId,
|
||||
@ -43,18 +49,57 @@ public class ParentTaskController {
|
||||
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get my tasks")
|
||||
@Operation(summary = "获取我的任务列表")
|
||||
@GetMapping
|
||||
public Result<PageResult<TaskResponse>> getMyTasks(
|
||||
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long parentId = SecurityUtils.getCurrentUserId();
|
||||
// TODO: 根据 parentId 获取任务列表
|
||||
// TODO: 根据 parentId 获取关联学生的任务列表
|
||||
return Result.success(PageResult.of(List.of(), 0L, Long.valueOf(pageNum == null ? 1 : pageNum), Long.valueOf(pageSize == null ? 10 : pageSize)));
|
||||
}
|
||||
|
||||
@Operation(summary = "Complete task")
|
||||
// =============== 提交与评价 API ===============
|
||||
|
||||
@Operation(summary = "提交任务完成")
|
||||
@PostMapping("/{taskId}/submit")
|
||||
public Result<TaskCompletionDetailResponse> submitTask(
|
||||
@PathVariable Long taskId,
|
||||
@Valid @RequestBody TaskSubmitRequest request) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
TaskCompletionDetailResponse response = taskService.submitTaskCompletion(taskId, request, tenantId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "修改任务提交")
|
||||
@PutMapping("/{taskId}/submit")
|
||||
public Result<TaskCompletionDetailResponse> updateSubmission(
|
||||
@PathVariable Long taskId,
|
||||
@Valid @RequestBody TaskSubmitRequest request) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
TaskCompletionDetailResponse response = taskService.submitTaskCompletion(taskId, request, tenantId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取教师评价")
|
||||
@GetMapping("/completions/{completionId}/feedback")
|
||||
public Result<TaskFeedbackResponse> getFeedback(@PathVariable Long completionId) {
|
||||
TaskFeedbackResponse response = taskFeedbackService.getFeedbackByCompletionId(completionId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取提交详情")
|
||||
@GetMapping("/completions/{completionId}")
|
||||
public Result<TaskCompletionDetailResponse> getCompletionDetail(@PathVariable Long completionId) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
TaskCompletionDetailResponse response = taskService.getCompletionDetail(completionId, tenantId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
// =============== 兼容旧接口 ===============
|
||||
|
||||
@Operation(summary = "完成任务(旧接口,兼容使用)")
|
||||
@PostMapping("/{id}/complete")
|
||||
public Result<Void> completeTask(
|
||||
@PathVariable Long id,
|
||||
|
||||
@ -1,100 +1,105 @@
|
||||
package com.reading.platform.controller.school;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.reading.platform.common.mapper.TaskMapper;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.service.TaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "School - Task", description = "Task Management APIs for School")
|
||||
/**
|
||||
* 学校端 - 阅读任务 API(只读)
|
||||
*
|
||||
* 重要说明:
|
||||
* - 学校端只能查看任务数据,不能创建/编辑/删除任务
|
||||
* - 支持多维度筛选:时间、班级、教师、类型、状态、完成率
|
||||
* - 可查看任务详情、完成情况列表、学生提交详情
|
||||
*/
|
||||
@Tag(name = "学校端 - 阅读任务(只读)", description = "学校端阅读任务查看 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/school/tasks")
|
||||
@RequestMapping("/api/v1/school/reading-tasks")
|
||||
@RequiredArgsConstructor
|
||||
public class SchoolTaskController {
|
||||
|
||||
private final TaskService taskService;
|
||||
private final TaskMapper taskMapper;
|
||||
|
||||
@Operation(summary = "Create task")
|
||||
@PostMapping
|
||||
public Result<TaskResponse> createTask(@Valid @RequestBody TaskCreateRequest request) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Long userId = SecurityUtils.getCurrentUserId();
|
||||
String role = SecurityUtils.getCurrentRole();
|
||||
Task task = taskService.createTask(tenantId, userId, role, request);
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
// =============== 任务列表(只读)==============
|
||||
|
||||
@Operation(summary = "Update task")
|
||||
@PutMapping("/{id}")
|
||||
public Result<TaskResponse> updateTask(@PathVariable Long id, @RequestBody TaskUpdateRequest request) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Task task = taskService.updateTaskWithTenantCheck(id, tenantId, request);
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get task by ID")
|
||||
@GetMapping("/{id}")
|
||||
public Result<TaskResponse> getTask(@PathVariable Long id) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Task task = taskService.getTaskByIdWithTenantCheck(id, tenantId);
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get task completions")
|
||||
@GetMapping("/{id}/completions")
|
||||
public Result<Map<String, Object>> getTaskCompletions(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "20") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
var pageResult = taskService.getTaskCompletionsWithStudent(id, tenantId, page, pageSize, status);
|
||||
var stats = taskService.getTaskCompletionStats(id, tenantId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("items", pageResult.getList());
|
||||
data.put("total", pageResult.getTotal());
|
||||
data.put("page", pageResult.getPageNum());
|
||||
data.put("pageSize", pageResult.getPageSize());
|
||||
data.put("stats", stats);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get task page")
|
||||
@Operation(summary = "获取任务列表(支持多维度筛选)")
|
||||
@GetMapping
|
||||
public Result<PageResult<TaskResponse>> getTaskPage(
|
||||
public Result<PageResult<TaskResponse>> getTaskList(
|
||||
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String type,
|
||||
@RequestParam(required = false) String status) {
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) List<Long> classIds,
|
||||
@RequestParam(required = false) List<Long> teacherIds,
|
||||
@RequestParam(required = false) String dateType,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) String completionRate,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortOrder) {
|
||||
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Page<Task> page = taskService.getTaskPage(tenantId, pageNum, pageSize, keyword, type, status);
|
||||
List<TaskResponse> voList = taskMapper.toVO(page.getRecords());
|
||||
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
||||
PageResult<TaskResponse> result = taskService.getSchoolTaskList(
|
||||
tenantId, pageNum, pageSize, keyword, type, status,
|
||||
classIds, teacherIds, dateType, startDate, endDate,
|
||||
completionRate, sortBy, sortOrder
|
||||
);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "Delete task")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> deleteTask(@PathVariable Long id) {
|
||||
@Operation(summary = "获取任务详情")
|
||||
@GetMapping("/{taskId}")
|
||||
public Result<TaskResponse> getTaskDetail(@PathVariable Long taskId) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
taskService.deleteTaskWithTenantCheck(id, tenantId);
|
||||
return Result.success();
|
||||
TaskResponse response = taskService.getTaskDetailForSchool(taskId, tenantId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
// =============== 完成情况(只读)==============
|
||||
|
||||
@Operation(summary = "获取任务完成情况列表")
|
||||
@GetMapping("/{taskId}/completions")
|
||||
public Result<PageResult<TaskCompletionDetailResponse>> getTaskCompletions(
|
||||
@PathVariable Long taskId,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
PageResult<TaskCompletionDetailResponse> result = taskService.getTaskCompletions(taskId, tenantId, pageNum, pageSize, status);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取学生提交详情")
|
||||
@GetMapping("/completions/{completionId}")
|
||||
public Result<TaskCompletionDetailResponse> getCompletionDetail(@PathVariable Long completionId) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
TaskCompletionDetailResponse response = taskService.getCompletionDetail(completionId, tenantId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
// =============== 统计数据(只读)==============
|
||||
|
||||
@Operation(summary = "获取统计数据")
|
||||
@GetMapping("/statistics")
|
||||
public Result<Object> getStatistics(
|
||||
@RequestParam(required = false) String dateType,
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
// TODO: 实现统计数据接口
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,10 +6,13 @@ import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskFeedbackRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskFeedbackResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.service.TaskFeedbackService;
|
||||
import com.reading.platform.service.TaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@ -17,9 +20,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "教师端 - 任务管理", description = "教师端任务 API")
|
||||
@RestController
|
||||
@ -29,6 +30,7 @@ public class TeacherTaskController {
|
||||
|
||||
private final TaskService taskService;
|
||||
private final TaskMapper taskMapper;
|
||||
private final TaskFeedbackService taskFeedbackService;
|
||||
|
||||
@Operation(summary = "创建任务")
|
||||
@PostMapping
|
||||
@ -53,36 +55,6 @@ public class TeacherTaskController {
|
||||
return Result.success(taskMapper.toVO(task));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取任务完成情况")
|
||||
@GetMapping("/{id}/completions")
|
||||
public Result<Map<String, Object>> getTaskCompletions(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "20") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
PageResult<TaskCompletionDetailResponse> result = taskService.getTaskCompletionsWithStudent(
|
||||
id, tenantId, page, pageSize, status);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("items", result.getList());
|
||||
data.put("total", result.getTotal());
|
||||
data.put("page", result.getPageNum());
|
||||
data.put("pageSize", result.getPageSize());
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新任务完成状态")
|
||||
@PutMapping("/{taskId}/completions/{studentId}")
|
||||
public Result<Void> updateTaskCompletion(
|
||||
@PathVariable Long taskId,
|
||||
@PathVariable Long studentId,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
String status = body != null && body.containsKey("status") ? String.valueOf(body.get("status")) : null;
|
||||
taskService.updateTaskCompletionStatus(taskId, studentId, tenantId, status);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "获取任务分页列表")
|
||||
@GetMapping
|
||||
public Result<PageResult<TaskResponse>> getTaskPage(
|
||||
@ -104,4 +76,46 @@ public class TeacherTaskController {
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// =============== 完成情况与评价 API ===============
|
||||
|
||||
@Operation(summary = "获取任务完成情况列表")
|
||||
@GetMapping("/{taskId}/completions")
|
||||
public Result<PageResult<TaskCompletionDetailResponse>> getTaskCompletions(
|
||||
@PathVariable Long taskId,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String status) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
PageResult<TaskCompletionDetailResponse> result = taskService.getTaskCompletions(taskId, tenantId, pageNum, pageSize, status);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取提交详情")
|
||||
@GetMapping("/completions/{completionId}")
|
||||
public Result<TaskCompletionDetailResponse> getCompletionDetail(@PathVariable Long completionId) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
TaskCompletionDetailResponse response = taskService.getCompletionDetail(completionId, tenantId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "提交评价")
|
||||
@PostMapping("/completions/{completionId}/feedback")
|
||||
public Result<TaskFeedbackResponse> submitFeedback(
|
||||
@PathVariable Long completionId,
|
||||
@Valid @RequestBody TaskFeedbackRequest request) {
|
||||
Long teacherId = SecurityUtils.getCurrentUserId();
|
||||
TaskFeedbackResponse response = taskFeedbackService.createOrUpdateFeedback(completionId, request, teacherId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "修改评价")
|
||||
@PutMapping("/completions/{completionId}/feedback")
|
||||
public Result<TaskFeedbackResponse> updateFeedback(
|
||||
@PathVariable Long completionId,
|
||||
@Valid @RequestBody TaskFeedbackRequest request) {
|
||||
Long teacherId = SecurityUtils.getCurrentUserId();
|
||||
TaskFeedbackResponse response = taskFeedbackService.createOrUpdateFeedback(completionId, request, teacherId);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -21,6 +21,9 @@ public class TaskCreateRequest {
|
||||
@Schema(description = "任务类型:reading-阅读,homework-作业,activity-活动")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "关联绘本名称(手动填写)")
|
||||
private String relatedBookName;
|
||||
|
||||
@Schema(description = "课程 ID")
|
||||
private Long courseId;
|
||||
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package com.reading.platform.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 任务评价请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "任务评价请求")
|
||||
public class TaskFeedbackRequest {
|
||||
|
||||
@NotBlank(message = "评价结果不能为空")
|
||||
@Schema(description = "评价结果:EXCELLENT-优秀/PASSED-通过/NEEDS_WORK-需改进", required = true)
|
||||
private String result;
|
||||
|
||||
@Min(value = 1, message = "评分最小为1")
|
||||
@Max(value = 5, message = "评分最大为5")
|
||||
@Schema(description = "评分 1-5(可选)")
|
||||
private Integer rating;
|
||||
|
||||
@Size(max = 500, message = "评语最多500字")
|
||||
@Schema(description = "评语")
|
||||
private String comment;
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.reading.platform.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务提交请求(家长端)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "任务提交请求")
|
||||
public class TaskSubmitRequest {
|
||||
|
||||
@Schema(description = "学生ID")
|
||||
private Long studentId;
|
||||
|
||||
@Schema(description = "照片URL数组(最多9张)")
|
||||
@Size(max = 9, message = "照片最多上传9张")
|
||||
private List<String> photos;
|
||||
|
||||
@Schema(description = "视频URL")
|
||||
private String videoUrl;
|
||||
|
||||
@Schema(description = "语音URL")
|
||||
private String audioUrl;
|
||||
|
||||
@Schema(description = "阅读心得/完成内容")
|
||||
@Size(max = 1000, message = "阅读心得最多1000字")
|
||||
private String content;
|
||||
|
||||
}
|
||||
@ -1,73 +1,101 @@
|
||||
package com.reading.platform.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务完成详情响应(含学生信息)
|
||||
* 任务完成详情响应
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "任务完成详情响应(含学生信息)")
|
||||
@Schema(description = "任务完成详情响应")
|
||||
public class TaskCompletionDetailResponse {
|
||||
|
||||
@Schema(description = "ID")
|
||||
@Schema(description = "完成记录ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "任务 ID")
|
||||
@Schema(description = "任务ID")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "学生 ID")
|
||||
private Long studentId;
|
||||
|
||||
@Schema(description = "完成状态: PENDING, IN_PROGRESS, COMPLETED")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "完成时间")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Schema(description = "完成内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "附件")
|
||||
private String attachments;
|
||||
|
||||
@Schema(description = "评分")
|
||||
private Integer rating;
|
||||
|
||||
@Schema(description = "反馈/家长反馈")
|
||||
private String feedback;
|
||||
|
||||
@Schema(description = "家长反馈(与 feedback 相同,兼容前端)")
|
||||
private String parentFeedback;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updatedAt;
|
||||
@Schema(description = "任务标题")
|
||||
private String taskTitle;
|
||||
|
||||
@Schema(description = "学生信息")
|
||||
private StudentInfo student;
|
||||
|
||||
@Schema(description = "状态:PENDING/SUBMITTED/REVIEWED")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "状态文本:待完成/已提交/已评价")
|
||||
private String statusText;
|
||||
|
||||
@Schema(description = "照片URL数组")
|
||||
private List<String> photos;
|
||||
|
||||
@Schema(description = "视频URL")
|
||||
private String videoUrl;
|
||||
|
||||
@Schema(description = "语音URL")
|
||||
private String audioUrl;
|
||||
|
||||
@Schema(description = "完成内容/阅读心得")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "提交时间")
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
@Schema(description = "评价时间")
|
||||
private LocalDateTime reviewedAt;
|
||||
|
||||
@Schema(description = "教师评价")
|
||||
private TaskFeedbackResponse feedback;
|
||||
|
||||
/**
|
||||
* 学生信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "学生信息")
|
||||
public static class StudentInfo {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String gender;
|
||||
@JsonProperty("class")
|
||||
private ClassInfo clazz;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class ClassInfo {
|
||||
private Long id;
|
||||
private String name;
|
||||
}
|
||||
@Schema(description = "学生ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "学生姓名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "学生头像")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "性别:MALE/FEMALE")
|
||||
private String gender;
|
||||
|
||||
@Schema(description = "班级信息")
|
||||
private ClassInfo classInfo;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 班级信息
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "班级信息")
|
||||
public static class ClassInfo {
|
||||
|
||||
@Schema(description = "班级ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "班级名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "年级")
|
||||
private String grade;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package com.reading.platform.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 任务评价响应
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@Schema(description = "任务评价响应")
|
||||
public class TaskFeedbackResponse {
|
||||
|
||||
@Schema(description = "评价ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "完成记录ID")
|
||||
private Long completionId;
|
||||
|
||||
@Schema(description = "评价结果:EXCELLENT/PASSED/NEEDS_WORK")
|
||||
private String result;
|
||||
|
||||
@Schema(description = "评价结果文本:优秀/通过/需改进")
|
||||
private String resultText;
|
||||
|
||||
@Schema(description = "评分 1-5")
|
||||
private Integer rating;
|
||||
|
||||
@Schema(description = "评语")
|
||||
private String comment;
|
||||
|
||||
@Schema(description = "教师ID")
|
||||
private Long teacherId;
|
||||
|
||||
@Schema(description = "教师姓名")
|
||||
private String teacherName;
|
||||
|
||||
@Schema(description = "教师头像")
|
||||
private String teacherAvatar;
|
||||
|
||||
@Schema(description = "评价时间")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
}
|
||||
@ -31,6 +31,9 @@ public class TaskResponse {
|
||||
@Schema(description = "任务类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "关联绘本名称")
|
||||
private String relatedBookName;
|
||||
|
||||
@Schema(description = "课程 ID")
|
||||
private Long courseId;
|
||||
|
||||
|
||||
@ -28,6 +28,9 @@ public class Task extends BaseEntity {
|
||||
@Schema(description = "任务类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "关联绘本名称")
|
||||
private String relatedBookName;
|
||||
|
||||
@Schema(description = "课程 ID")
|
||||
private Long courseId;
|
||||
|
||||
|
||||
@ -22,22 +22,37 @@ public class TaskCompletion extends BaseEntity {
|
||||
@Schema(description = "学生 ID")
|
||||
private Long studentId;
|
||||
|
||||
@Schema(description = "完成状态")
|
||||
@Schema(description = "完成状态:PENDING-待提交/SUBMITTED-已提交/REVIEWED-已评价")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "完成时间")
|
||||
private LocalDateTime completedAt;
|
||||
@Schema(description = "照片URL数组(JSON格式)")
|
||||
private String photos;
|
||||
|
||||
@Schema(description = "完成内容")
|
||||
@Schema(description = "视频URL")
|
||||
private String videoUrl;
|
||||
|
||||
@Schema(description = "语音URL")
|
||||
private String audioUrl;
|
||||
|
||||
@Schema(description = "完成内容/阅读心得")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "提交时间")
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
@Schema(description = "评价时间")
|
||||
private LocalDateTime reviewedAt;
|
||||
|
||||
@Schema(description = "完成时间(兼容旧字段)")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Schema(description = "附件(JSON 数组)")
|
||||
private String attachments;
|
||||
|
||||
@Schema(description = "评分")
|
||||
@Schema(description = "评分(家长自评)")
|
||||
private Integer rating;
|
||||
|
||||
@Schema(description = "反馈")
|
||||
@Schema(description = "反馈(家长反馈)")
|
||||
private String feedback;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package com.reading.platform.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 任务评价实体
|
||||
* 教师对学生完成阅读任务后的评价反馈
|
||||
*/
|
||||
@Schema(description = "任务评价实体")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("task_feedback")
|
||||
public class TaskFeedback extends BaseEntity {
|
||||
|
||||
@Schema(description = "完成记录ID")
|
||||
private Long completionId;
|
||||
|
||||
@Schema(description = "任务ID(冗余字段,方便查询)")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "学生ID(冗余字段)")
|
||||
private Long studentId;
|
||||
|
||||
@Schema(description = "教师ID")
|
||||
private Long teacherId;
|
||||
|
||||
@Schema(description = "评价结果:EXCELLENT-优秀/PASSED-通过/NEEDS_WORK-需改进")
|
||||
private String result;
|
||||
|
||||
@Schema(description = "评分 1-5(可选)")
|
||||
private Integer rating;
|
||||
|
||||
@Schema(description = "评语")
|
||||
private String comment;
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.reading.platform.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.reading.platform.entity.TaskFeedback;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 任务评价 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface TaskFeedbackMapper extends BaseMapper<TaskFeedback> {
|
||||
|
||||
/**
|
||||
* 根据完成记录ID查询评价
|
||||
*/
|
||||
@Select("SELECT * FROM task_feedback WHERE completion_id = #{completionId} AND deleted_at IS NULL")
|
||||
Optional<TaskFeedback> findByCompletionId(@Param("completionId") Long completionId);
|
||||
|
||||
/**
|
||||
* 根据任务ID和学生ID查询评价
|
||||
*/
|
||||
@Select("SELECT tf.* FROM task_feedback tf " +
|
||||
"INNER JOIN task_completion tc ON tf.completion_id = tc.id " +
|
||||
"WHERE tf.task_id = #{taskId} AND tc.student_id = #{studentId} AND tf.deleted_at IS NULL")
|
||||
Optional<TaskFeedback> findByTaskIdAndStudentId(@Param("taskId") Long taskId, @Param("studentId") Long studentId);
|
||||
|
||||
}
|
||||
@ -82,4 +82,9 @@ public interface ClassService extends com.baomidou.mybatisplus.extension.service
|
||||
*/
|
||||
List<Clazz> getActiveClassesByTenantId(Long tenantId);
|
||||
|
||||
/**
|
||||
* 获取学生当前所在班级
|
||||
*/
|
||||
Clazz getPrimaryClassByStudentId(Long studentId);
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
package com.reading.platform.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.reading.platform.dto.request.TaskFeedbackRequest;
|
||||
import com.reading.platform.dto.response.TaskFeedbackResponse;
|
||||
import com.reading.platform.entity.TaskFeedback;
|
||||
|
||||
/**
|
||||
* 任务评价 Service
|
||||
*/
|
||||
public interface TaskFeedbackService extends IService<TaskFeedback> {
|
||||
|
||||
/**
|
||||
* 创建或更新评价
|
||||
* @param completionId 完成记录ID
|
||||
* @param request 评价请求
|
||||
* @return 评价响应
|
||||
*/
|
||||
TaskFeedbackResponse createOrUpdateFeedback(Long completionId, TaskFeedbackRequest request);
|
||||
|
||||
/**
|
||||
* 创建或更新评价(指定教师ID)
|
||||
* @param completionId 完成记录ID
|
||||
* @param request 评价请求
|
||||
* @param teacherId 教师ID(从当前登录用户获取)
|
||||
* @return 评价响应
|
||||
*/
|
||||
TaskFeedbackResponse createOrUpdateFeedback(Long completionId, TaskFeedbackRequest request, Long teacherId);
|
||||
|
||||
/**
|
||||
* 根据完成记录ID获取评价
|
||||
* @param completionId 完成记录ID
|
||||
* @return 评价响应
|
||||
*/
|
||||
TaskFeedbackResponse getFeedbackByCompletionId(Long completionId);
|
||||
|
||||
/**
|
||||
* 根据任务ID和学生ID获取评价
|
||||
* @param taskId 任务ID
|
||||
* @param studentId 学生ID
|
||||
* @return 评价响应
|
||||
*/
|
||||
TaskFeedbackResponse getFeedbackByTaskIdAndStudentId(Long taskId, Long studentId);
|
||||
|
||||
/**
|
||||
* 检查是否已评价
|
||||
* @param completionId 完成记录ID
|
||||
* @return 是否已评价
|
||||
*/
|
||||
boolean hasFeedback(Long completionId);
|
||||
}
|
||||
@ -3,9 +3,10 @@ package com.reading.platform.service;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskSubmitRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskCompletionResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.Task;
|
||||
|
||||
import java.util.List;
|
||||
@ -35,14 +36,39 @@ public interface TaskService extends com.baomidou.mybatisplus.extension.service.
|
||||
|
||||
void completeTask(Long taskId, Long studentId, String content, String attachments);
|
||||
|
||||
List<TaskCompletionResponse> getTaskCompletions(Long taskId, Long tenantId);
|
||||
|
||||
PageResult<TaskCompletionDetailResponse> getTaskCompletionsWithStudent(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, String status);
|
||||
|
||||
java.util.Map<String, java.lang.Long> getTaskCompletionStats(Long taskId, Long tenantId);
|
||||
|
||||
void updateTaskCompletionStatus(Long taskId, Long studentId, Long tenantId, String status);
|
||||
|
||||
List<Task> getTasksByClassId(Long classId);
|
||||
|
||||
// =============== 完成情况与评价相关方法 ===============
|
||||
|
||||
/**
|
||||
* 获取任务完成情况列表
|
||||
*/
|
||||
PageResult<TaskCompletionDetailResponse> getTaskCompletions(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, String status);
|
||||
|
||||
/**
|
||||
* 获取提交详情
|
||||
*/
|
||||
TaskCompletionDetailResponse getCompletionDetail(Long completionId, Long tenantId);
|
||||
|
||||
/**
|
||||
* 提交任务完成(家长端)
|
||||
*/
|
||||
TaskCompletionDetailResponse submitTaskCompletion(Long taskId, TaskSubmitRequest request, Long tenantId);
|
||||
|
||||
// =============== 学校端只读方法 ===============
|
||||
|
||||
/**
|
||||
* 获取学校端任务列表(多维度筛选)
|
||||
*/
|
||||
PageResult<TaskResponse> getSchoolTaskList(Long tenantId, Integer pageNum, Integer pageSize,
|
||||
String keyword, String type, String status,
|
||||
List<Long> classIds, List<Long> teacherIds,
|
||||
String dateType, String startDate, String endDate,
|
||||
String completionRate, String sortBy, String sortOrder);
|
||||
|
||||
/**
|
||||
* 获取任务详情(学校端)
|
||||
*/
|
||||
TaskResponse getTaskDetailForSchool(Long taskId, Long tenantId);
|
||||
|
||||
}
|
||||
|
||||
@ -262,4 +262,26 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Clazz getPrimaryClassByStudentId(Long studentId) {
|
||||
log.debug("获取学生当前所在班级,学生 ID: {}", studentId);
|
||||
|
||||
// 查找学生当前活跃的班级关联记录
|
||||
StudentClassHistory history = studentClassHistoryMapper.selectOne(
|
||||
new LambdaQueryWrapper<StudentClassHistory>()
|
||||
.eq(StudentClassHistory::getStudentId, studentId)
|
||||
.eq(StudentClassHistory::getStatus, "active")
|
||||
.isNull(StudentClassHistory::getEndDate)
|
||||
.orderByDesc(StudentClassHistory::getStartDate)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
if (history == null) {
|
||||
log.debug("学生当前没有活跃的班级关联,学生 ID: {}", studentId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return clazzMapper.selectById(history.getClassId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,149 @@
|
||||
package com.reading.platform.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.reading.platform.dto.request.TaskFeedbackRequest;
|
||||
import com.reading.platform.dto.response.TaskFeedbackResponse;
|
||||
import com.reading.platform.entity.TaskCompletion;
|
||||
import com.reading.platform.entity.TaskFeedback;
|
||||
import com.reading.platform.entity.Teacher;
|
||||
import com.reading.platform.mapper.TaskCompletionMapper;
|
||||
import com.reading.platform.mapper.TaskFeedbackMapper;
|
||||
import com.reading.platform.service.TaskFeedbackService;
|
||||
import com.reading.platform.service.TeacherService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 任务评价 Service 实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TaskFeedbackServiceImpl extends ServiceImpl<TaskFeedbackMapper, TaskFeedback>
|
||||
implements TaskFeedbackService {
|
||||
|
||||
private final TaskCompletionMapper taskCompletionMapper;
|
||||
private final TeacherService teacherService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public TaskFeedbackResponse createOrUpdateFeedback(Long completionId, TaskFeedbackRequest request) {
|
||||
return createOrUpdateFeedback(completionId, request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新评价(指定教师ID)
|
||||
* @param completionId 完成记录ID
|
||||
* @param request 评价请求
|
||||
* @param teacherId 教师ID(从当前登录用户获取)
|
||||
* @return 评价响应
|
||||
*/
|
||||
@Transactional
|
||||
public TaskFeedbackResponse createOrUpdateFeedback(Long completionId, TaskFeedbackRequest request, Long teacherId) {
|
||||
// 获取完成记录
|
||||
TaskCompletion completion = taskCompletionMapper.selectById(completionId);
|
||||
if (completion == null) {
|
||||
throw new RuntimeException("完成记录不存在");
|
||||
}
|
||||
|
||||
// 检查是否已评价
|
||||
Optional<TaskFeedback> existingFeedback = baseMapper.findByCompletionId(completionId);
|
||||
|
||||
TaskFeedback feedback;
|
||||
if (existingFeedback.isPresent()) {
|
||||
// 更新评价
|
||||
feedback = existingFeedback.get();
|
||||
feedback.setResult(request.getResult());
|
||||
feedback.setRating(request.getRating());
|
||||
feedback.setComment(request.getComment());
|
||||
feedback.setUpdatedAt(LocalDateTime.now());
|
||||
this.updateById(feedback);
|
||||
log.info("更新评价成功:completionId={}", completionId);
|
||||
} else {
|
||||
// 创建新评价
|
||||
feedback = new TaskFeedback();
|
||||
feedback.setCompletionId(completionId);
|
||||
feedback.setTaskId(completion.getTaskId());
|
||||
feedback.setStudentId(completion.getStudentId());
|
||||
feedback.setTeacherId(teacherId);
|
||||
feedback.setResult(request.getResult());
|
||||
feedback.setRating(request.getRating());
|
||||
feedback.setComment(request.getComment());
|
||||
feedback.setCreatedAt(LocalDateTime.now());
|
||||
feedback.setUpdatedAt(LocalDateTime.now());
|
||||
this.save(feedback);
|
||||
log.info("创建评价成功:completionId={}, teacherId={}", completionId, teacherId);
|
||||
}
|
||||
|
||||
// 更新完成记录状态为已评价
|
||||
completion.setStatus("REVIEWED");
|
||||
completion.setReviewedAt(LocalDateTime.now());
|
||||
taskCompletionMapper.updateById(completion);
|
||||
|
||||
return buildFeedbackResponse(feedback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskFeedbackResponse getFeedbackByCompletionId(Long completionId) {
|
||||
Optional<TaskFeedback> feedback = baseMapper.findByCompletionId(completionId);
|
||||
return feedback.map(this::buildFeedbackResponse).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskFeedbackResponse getFeedbackByTaskIdAndStudentId(Long taskId, Long studentId) {
|
||||
Optional<TaskFeedback> feedback = baseMapper.findByTaskIdAndStudentId(taskId, studentId);
|
||||
return feedback.map(this::buildFeedbackResponse).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFeedback(Long completionId) {
|
||||
return baseMapper.findByCompletionId(completionId).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建评价响应
|
||||
*/
|
||||
private TaskFeedbackResponse buildFeedbackResponse(TaskFeedback feedback) {
|
||||
TaskFeedbackResponse.TaskFeedbackResponseBuilder builder = TaskFeedbackResponse.builder()
|
||||
.id(feedback.getId())
|
||||
.completionId(feedback.getCompletionId())
|
||||
.result(feedback.getResult())
|
||||
.resultText(getResultText(feedback.getResult()))
|
||||
.rating(feedback.getRating())
|
||||
.comment(feedback.getComment())
|
||||
.teacherId(feedback.getTeacherId())
|
||||
.createdAt(feedback.getCreatedAt());
|
||||
|
||||
// 获取教师信息
|
||||
if (feedback.getTeacherId() != null) {
|
||||
Teacher teacher = teacherService.getById(feedback.getTeacherId());
|
||||
if (teacher != null) {
|
||||
builder.teacherName(teacher.getName());
|
||||
builder.teacherAvatar(teacher.getAvatarUrl());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评价结果文本
|
||||
*/
|
||||
private String getResultText(String result) {
|
||||
if (result == null) {
|
||||
return "";
|
||||
}
|
||||
return switch (result) {
|
||||
case "EXCELLENT" -> "优秀";
|
||||
case "PASSED" -> "通过";
|
||||
case "NEEDS_WORK" -> "需改进";
|
||||
default -> result;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -3,26 +3,22 @@ package com.reading.platform.service.impl;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.reading.platform.common.enums.ErrorCode;
|
||||
import com.reading.platform.common.exception.BusinessException;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.dto.request.TaskCreateRequest;
|
||||
import com.reading.platform.dto.request.TaskSubmitRequest;
|
||||
import com.reading.platform.dto.request.TaskUpdateRequest;
|
||||
import com.reading.platform.dto.response.TaskCompletionDetailResponse;
|
||||
import com.reading.platform.dto.response.TaskCompletionResponse;
|
||||
import com.reading.platform.entity.Clazz;
|
||||
import com.reading.platform.entity.Student;
|
||||
import com.reading.platform.entity.StudentClassHistory;
|
||||
import com.reading.platform.entity.Task;
|
||||
import com.reading.platform.entity.TaskCompletion;
|
||||
import com.reading.platform.entity.TaskTarget;
|
||||
import com.reading.platform.mapper.ClazzMapper;
|
||||
import com.reading.platform.mapper.StudentClassHistoryMapper;
|
||||
import com.reading.platform.mapper.StudentMapper;
|
||||
import com.reading.platform.dto.response.TaskFeedbackResponse;
|
||||
import com.reading.platform.dto.response.TaskResponse;
|
||||
import com.reading.platform.entity.*;
|
||||
import com.reading.platform.mapper.TaskCompletionMapper;
|
||||
import com.reading.platform.mapper.TaskMapper;
|
||||
import com.reading.platform.mapper.TaskTargetMapper;
|
||||
import com.reading.platform.service.TaskService;
|
||||
import com.reading.platform.service.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -30,12 +26,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -46,9 +39,10 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
private final TaskMapper taskMapper;
|
||||
private final TaskTargetMapper taskTargetMapper;
|
||||
private final TaskCompletionMapper taskCompletionMapper;
|
||||
private final StudentMapper studentMapper;
|
||||
private final StudentClassHistoryMapper studentClassHistoryMapper;
|
||||
private final ClazzMapper clazzMapper;
|
||||
private final StudentService studentService;
|
||||
private final ClassService classService;
|
||||
private final TaskFeedbackService taskFeedbackService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@ -232,150 +226,6 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TaskCompletionResponse> getTaskCompletions(Long taskId, Long tenantId) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
List<TaskCompletion> completions = taskCompletionMapper.selectList(
|
||||
new LambdaQueryWrapper<TaskCompletion>().eq(TaskCompletion::getTaskId, taskId)
|
||||
);
|
||||
return com.reading.platform.common.mapper.TaskCompletionMapper.INSTANCE.toVO(completions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<TaskCompletionDetailResponse> getTaskCompletionsWithStudent(Long taskId, Long tenantId,
|
||||
Integer pageNum, Integer pageSize, String status) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
|
||||
int pn = pageNum != null && pageNum > 0 ? pageNum : 1;
|
||||
int ps = pageSize != null && pageSize > 0 ? pageSize : 20;
|
||||
|
||||
LambdaQueryWrapper<TaskCompletion> wrapper = new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.orderByDesc(TaskCompletion::getUpdatedAt);
|
||||
if (StringUtils.hasText(status)) {
|
||||
wrapper.eq(TaskCompletion::getStatus, normalizeStatus(status));
|
||||
}
|
||||
|
||||
long total = taskCompletionMapper.selectCount(wrapper);
|
||||
Page<TaskCompletion> page = new Page<>(pn, ps);
|
||||
List<TaskCompletion> completions = taskCompletionMapper.selectPage(page, wrapper).getRecords();
|
||||
|
||||
List<TaskCompletionDetailResponse> items = completions.stream()
|
||||
.map(c -> buildCompletionDetail(c))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.of(items, total, (long) pn, (long) ps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Long> getTaskCompletionStats(Long taskId, Long tenantId) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
Map<String, Long> stats = new HashMap<>();
|
||||
stats.put("PENDING", taskCompletionMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStatus, Arrays.asList("PENDING", "pending"))
|
||||
));
|
||||
stats.put("IN_PROGRESS", taskCompletionMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStatus, Arrays.asList("IN_PROGRESS", "in_progress"))
|
||||
));
|
||||
stats.put("COMPLETED", taskCompletionMapper.selectCount(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.in(TaskCompletion::getStatus, Arrays.asList("COMPLETED", "completed"))
|
||||
));
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateTaskCompletionStatus(Long taskId, Long studentId, Long tenantId, String status) {
|
||||
getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
TaskCompletion completion = taskCompletionMapper.selectOne(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.eq(TaskCompletion::getStudentId, studentId)
|
||||
);
|
||||
if (completion == null) {
|
||||
completion = new TaskCompletion();
|
||||
completion.setTaskId(taskId);
|
||||
completion.setStudentId(studentId);
|
||||
taskCompletionMapper.insert(completion);
|
||||
}
|
||||
completion.setStatus(normalizeStatus(status));
|
||||
if ("COMPLETED".equals(completion.getStatus())) {
|
||||
completion.setCompletedAt(LocalDateTime.now());
|
||||
}
|
||||
taskCompletionMapper.updateById(completion);
|
||||
}
|
||||
|
||||
private TaskCompletionDetailResponse buildCompletionDetail(TaskCompletion c) {
|
||||
Student student = studentMapper.selectById(c.getStudentId());
|
||||
TaskCompletionDetailResponse.StudentInfo.ClassInfo classInfo = null;
|
||||
if (student != null) {
|
||||
StudentClassHistory sch = studentClassHistoryMapper.selectOne(
|
||||
new LambdaQueryWrapper<StudentClassHistory>()
|
||||
.eq(StudentClassHistory::getStudentId, c.getStudentId())
|
||||
.in(StudentClassHistory::getStatus, Arrays.asList("active", "ACTIVE"))
|
||||
.isNull(StudentClassHistory::getEndDate)
|
||||
.orderByDesc(StudentClassHistory::getStartDate)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (sch != null) {
|
||||
Clazz clazz = clazzMapper.selectById(sch.getClassId());
|
||||
if (clazz != null) {
|
||||
classInfo = TaskCompletionDetailResponse.StudentInfo.ClassInfo.builder()
|
||||
.id(clazz.getId())
|
||||
.name(clazz.getName())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
if (classInfo == null && student.getGrade() != null) {
|
||||
classInfo = TaskCompletionDetailResponse.StudentInfo.ClassInfo.builder()
|
||||
.id(null)
|
||||
.name(student.getGrade())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
String feedback = c.getFeedback();
|
||||
return TaskCompletionDetailResponse.builder()
|
||||
.id(c.getId())
|
||||
.taskId(c.getTaskId())
|
||||
.studentId(c.getStudentId())
|
||||
.status(normalizeStatusForResponse(c.getStatus()))
|
||||
.completedAt(c.getCompletedAt())
|
||||
.content(c.getContent())
|
||||
.attachments(c.getAttachments())
|
||||
.rating(c.getRating())
|
||||
.feedback(feedback)
|
||||
.parentFeedback(feedback)
|
||||
.createdAt(c.getCreatedAt())
|
||||
.updatedAt(c.getUpdatedAt())
|
||||
.student(student != null ? TaskCompletionDetailResponse.StudentInfo.builder()
|
||||
.id(student.getId())
|
||||
.name(student.getName())
|
||||
.gender(student.getGender())
|
||||
.clazz(classInfo)
|
||||
.build() : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String normalizeStatus(String status) {
|
||||
if (status == null) return "PENDING";
|
||||
String u = status.toUpperCase();
|
||||
if ("COMPLETED".equals(u) || "IN_PROGRESS".equals(u) || "PENDING".equals(u)) return u;
|
||||
if ("completed".equals(status.toLowerCase())) return "COMPLETED";
|
||||
if ("in_progress".equals(status.toLowerCase()) || "in progress".equalsIgnoreCase(status)) return "IN_PROGRESS";
|
||||
return "PENDING";
|
||||
}
|
||||
|
||||
private String normalizeStatusForResponse(String status) {
|
||||
if (status == null) return "PENDING";
|
||||
return normalizeStatus(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getTasksByClassId(Long classId) {
|
||||
List<TaskTarget> targets = taskTargetMapper.selectList(
|
||||
@ -399,4 +249,296 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
|
||||
);
|
||||
}
|
||||
|
||||
// =============== 完成情况与评价相关方法实现 ===============
|
||||
|
||||
@Override
|
||||
public PageResult<TaskCompletionDetailResponse> getTaskCompletions(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, String status) {
|
||||
// 验证任务存在且属于该租户
|
||||
Task task = getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
|
||||
Page<TaskCompletion> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<TaskCompletion> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TaskCompletion::getTaskId, taskId);
|
||||
|
||||
if (StringUtils.hasText(status)) {
|
||||
wrapper.eq(TaskCompletion::getStatus, status);
|
||||
}
|
||||
wrapper.orderByDesc(TaskCompletion::getCreatedAt);
|
||||
|
||||
Page<TaskCompletion> completionPage = taskCompletionMapper.selectPage(page, wrapper);
|
||||
|
||||
List<TaskCompletionDetailResponse> responses = completionPage.getRecords().stream()
|
||||
.map(this::buildCompletionDetailResponse)
|
||||
.toList();
|
||||
|
||||
return PageResult.of(responses, completionPage.getTotal(), completionPage.getCurrent(), completionPage.getSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskCompletionDetailResponse getCompletionDetail(Long completionId, Long tenantId) {
|
||||
TaskCompletion completion = taskCompletionMapper.selectById(completionId);
|
||||
if (completion == null) {
|
||||
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "完成记录不存在");
|
||||
}
|
||||
|
||||
// 验证任务属于该租户
|
||||
getTaskByIdWithTenantCheck(completion.getTaskId(), tenantId);
|
||||
|
||||
return buildCompletionDetailResponse(completion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完成详情响应
|
||||
*/
|
||||
private TaskCompletionDetailResponse buildCompletionDetailResponse(TaskCompletion completion) {
|
||||
TaskCompletionDetailResponse.TaskCompletionDetailResponseBuilder builder = TaskCompletionDetailResponse.builder()
|
||||
.id(completion.getId())
|
||||
.taskId(completion.getTaskId())
|
||||
.status(completion.getStatus())
|
||||
.statusText(getStatusText(completion.getStatus()))
|
||||
.photos(parsePhotos(completion.getPhotos()))
|
||||
.videoUrl(completion.getVideoUrl())
|
||||
.audioUrl(completion.getAudioUrl())
|
||||
.content(completion.getContent())
|
||||
.submittedAt(completion.getSubmittedAt())
|
||||
.reviewedAt(completion.getReviewedAt());
|
||||
|
||||
// 获取任务标题
|
||||
Task task = taskMapper.selectById(completion.getTaskId());
|
||||
if (task != null) {
|
||||
builder.taskTitle(task.getTitle());
|
||||
}
|
||||
|
||||
// 获取学生信息
|
||||
if (completion.getStudentId() != null) {
|
||||
Student student = studentService.getById(completion.getStudentId());
|
||||
if (student != null) {
|
||||
TaskCompletionDetailResponse.StudentInfo studentInfo = TaskCompletionDetailResponse.StudentInfo.builder()
|
||||
.id(student.getId())
|
||||
.name(student.getName())
|
||||
.avatar(student.getAvatarUrl())
|
||||
.gender(student.getGender())
|
||||
.build();
|
||||
|
||||
// 获取班级信息
|
||||
Clazz clazz = classService.getPrimaryClassByStudentId(student.getId());
|
||||
if (clazz != null) {
|
||||
TaskCompletionDetailResponse.ClassInfo classInfo = TaskCompletionDetailResponse.ClassInfo.builder()
|
||||
.id(clazz.getId())
|
||||
.name(clazz.getName())
|
||||
.grade(clazz.getGrade())
|
||||
.build();
|
||||
studentInfo.setClassInfo(classInfo);
|
||||
}
|
||||
|
||||
builder.student(studentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取评价信息
|
||||
TaskFeedbackResponse feedback = taskFeedbackService.getFeedbackByCompletionId(completion.getId());
|
||||
builder.feedback(feedback);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析照片 JSON 数组
|
||||
*/
|
||||
private List<String> parsePhotos(String photosJson) {
|
||||
if (!StringUtils.hasText(photosJson)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(photosJson, new TypeReference<List<String>>() {});
|
||||
} catch (Exception e) {
|
||||
log.warn("解析照片JSON失败: {}", e.getMessage());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
private String getStatusText(String status) {
|
||||
if (status == null) {
|
||||
return "";
|
||||
}
|
||||
return switch (status) {
|
||||
case "PENDING" -> "待完成";
|
||||
case "SUBMITTED" -> "已提交";
|
||||
case "REVIEWED" -> "已评价";
|
||||
case "completed" -> "已完成"; // 兼容旧数据
|
||||
default -> status;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public TaskCompletionDetailResponse submitTaskCompletion(Long taskId, TaskSubmitRequest request, Long tenantId) {
|
||||
log.info("提交任务完成:taskId={}, studentId={}", taskId, request.getStudentId());
|
||||
|
||||
// 验证任务存在且属于该租户
|
||||
Task task = getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
|
||||
// 验证学生ID
|
||||
if (request.getStudentId() == null) {
|
||||
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "学生ID不能为空");
|
||||
}
|
||||
|
||||
// 查找或创建完成记录
|
||||
TaskCompletion completion = taskCompletionMapper.selectOne(
|
||||
new LambdaQueryWrapper<TaskCompletion>()
|
||||
.eq(TaskCompletion::getTaskId, taskId)
|
||||
.eq(TaskCompletion::getStudentId, request.getStudentId())
|
||||
);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (completion == null) {
|
||||
// 创建新记录
|
||||
completion = new TaskCompletion();
|
||||
completion.setTaskId(taskId);
|
||||
completion.setStudentId(request.getStudentId());
|
||||
completion.setStatus("SUBMITTED");
|
||||
completion.setPhotos(serializePhotos(request.getPhotos()));
|
||||
completion.setVideoUrl(request.getVideoUrl());
|
||||
completion.setAudioUrl(request.getAudioUrl());
|
||||
completion.setContent(request.getContent());
|
||||
completion.setSubmittedAt(now);
|
||||
completion.setCreatedAt(now);
|
||||
completion.setUpdatedAt(now);
|
||||
taskCompletionMapper.insert(completion);
|
||||
log.info("创建完成记录成功:completionId={}", completion.getId());
|
||||
} else {
|
||||
// 更新记录
|
||||
completion.setStatus("SUBMITTED");
|
||||
completion.setPhotos(serializePhotos(request.getPhotos()));
|
||||
completion.setVideoUrl(request.getVideoUrl());
|
||||
completion.setAudioUrl(request.getAudioUrl());
|
||||
completion.setContent(request.getContent());
|
||||
completion.setSubmittedAt(now);
|
||||
completion.setUpdatedAt(now);
|
||||
taskCompletionMapper.updateById(completion);
|
||||
log.info("更新完成记录成功:completionId={}", completion.getId());
|
||||
}
|
||||
|
||||
return buildCompletionDetailResponse(completion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化照片列表为 JSON
|
||||
*/
|
||||
private String serializePhotos(List<String> photos) {
|
||||
if (photos == null || photos.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(photos);
|
||||
} catch (Exception e) {
|
||||
log.warn("序列化照片JSON失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =============== 学校端只读方法实现 ===============
|
||||
|
||||
@Override
|
||||
public PageResult<TaskResponse> getSchoolTaskList(Long tenantId, Integer pageNum, Integer pageSize,
|
||||
String keyword, String type, String status,
|
||||
List<Long> classIds, List<Long> teacherIds,
|
||||
String dateType, String startDate, String endDate,
|
||||
String completionRate, String sortBy, String sortOrder) {
|
||||
|
||||
log.info("获取学校端任务列表:tenantId={}", tenantId);
|
||||
|
||||
Page<Task> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 租户过滤
|
||||
wrapper.eq(Task::getTenantId, tenantId);
|
||||
|
||||
// 关键字搜索
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.like(Task::getTitle, keyword);
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
if (StringUtils.hasText(type)) {
|
||||
wrapper.eq(Task::getType, type);
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (StringUtils.hasText(status)) {
|
||||
wrapper.eq(Task::getStatus, status);
|
||||
}
|
||||
|
||||
// TODO: 实现更多筛选条件
|
||||
// - classIds: 班级筛选(需要关联 task_target 表)
|
||||
// - teacherIds: 教师筛选
|
||||
// - dateType/startDate/endDate: 时间筛选
|
||||
// - completionRate: 完成率筛选
|
||||
// - sortBy/sortOrder: 排序
|
||||
|
||||
// 默认排序
|
||||
if ("completionRate".equals(sortBy)) {
|
||||
// 完成率排序需要特殊处理,暂时使用创建时间
|
||||
wrapper.orderByDesc(Task::getCreatedAt);
|
||||
} else {
|
||||
wrapper.orderByDesc(Task::getCreatedAt);
|
||||
}
|
||||
|
||||
Page<Task> taskPage = taskMapper.selectPage(page, wrapper);
|
||||
|
||||
// 转换为响应对象
|
||||
// TODO: 使用 TaskMapper 转换
|
||||
List<TaskResponse> responses = taskPage.getRecords().stream()
|
||||
.map(task -> TaskResponse.builder()
|
||||
.id(task.getId())
|
||||
.tenantId(task.getTenantId())
|
||||
.title(task.getTitle())
|
||||
.description(task.getDescription())
|
||||
.type(task.getType())
|
||||
.relatedBookName(task.getRelatedBookName())
|
||||
.courseId(task.getCourseId())
|
||||
.creatorId(task.getCreatorId())
|
||||
.creatorRole(task.getCreatorRole())
|
||||
.startDate(task.getStartDate())
|
||||
.dueDate(task.getDueDate())
|
||||
.status(task.getStatus())
|
||||
.attachments(task.getAttachments())
|
||||
.createdAt(task.getCreatedAt())
|
||||
.updatedAt(task.getUpdatedAt())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
return PageResult.of(responses, taskPage.getTotal(), taskPage.getCurrent(), taskPage.getSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskResponse getTaskDetailForSchool(Long taskId, Long tenantId) {
|
||||
log.info("获取学校端任务详情:taskId={}, tenantId={}", taskId, tenantId);
|
||||
|
||||
// 验证任务存在且属于该租户
|
||||
Task task = getTaskByIdWithTenantCheck(taskId, tenantId);
|
||||
|
||||
return TaskResponse.builder()
|
||||
.id(task.getId())
|
||||
.tenantId(task.getTenantId())
|
||||
.title(task.getTitle())
|
||||
.description(task.getDescription())
|
||||
.type(task.getType())
|
||||
.relatedBookName(task.getRelatedBookName())
|
||||
.courseId(task.getCourseId())
|
||||
.creatorId(task.getCreatorId())
|
||||
.creatorRole(task.getCreatorRole())
|
||||
.startDate(task.getStartDate())
|
||||
.dueDate(task.getDueDate())
|
||||
.status(task.getStatus())
|
||||
.attachments(task.getAttachments())
|
||||
.createdAt(task.getCreatedAt())
|
||||
.updatedAt(task.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
-- =====================================================
|
||||
-- 阅读任务模块增强 - 数据库迁移脚本
|
||||
-- 版本: V43
|
||||
-- 日期: 2026-03-20
|
||||
-- 说明: 新增评价功能,扩展提交内容字段
|
||||
-- =====================================================
|
||||
|
||||
-- 1. Task 表新增关联绘本字段
|
||||
ALTER TABLE task ADD COLUMN related_book_name VARCHAR(200) DEFAULT NULL COMMENT '关联绘本名称';
|
||||
|
||||
-- 2. TaskCompletion 表扩展字段
|
||||
-- 确保 photos 字段是 JSON 类型(如果之前是 VARCHAR,需要转换)
|
||||
-- ALTER TABLE task_completion MODIFY COLUMN photos JSON COMMENT '照片URL数组(JSON)';
|
||||
|
||||
-- 新增视频和语音字段
|
||||
ALTER TABLE task_completion ADD COLUMN video_url VARCHAR(500) DEFAULT NULL COMMENT '视频URL';
|
||||
ALTER TABLE task_completion ADD COLUMN audio_url VARCHAR(500) DEFAULT NULL COMMENT '语音URL';
|
||||
|
||||
-- 3. 新建 TaskFeedback 评价表
|
||||
CREATE TABLE IF NOT EXISTS task_feedback (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
|
||||
completion_id BIGINT NOT NULL COMMENT '完成记录ID',
|
||||
task_id BIGINT NOT NULL COMMENT '任务ID(冗余,方便查询)',
|
||||
student_id BIGINT NOT NULL COMMENT '学生ID(冗余)',
|
||||
teacher_id BIGINT NOT NULL COMMENT '教师ID',
|
||||
result VARCHAR(20) NOT NULL COMMENT '评价结果:EXCELLENT-优秀/PASSED-通过/NEEDS_WORK-需改进',
|
||||
rating INT DEFAULT NULL COMMENT '评分 1-5(可选)',
|
||||
comment TEXT DEFAULT NULL COMMENT '评语',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at DATETIME DEFAULT NULL COMMENT '删除时间(逻辑删除)',
|
||||
|
||||
INDEX idx_completion_id (completion_id),
|
||||
INDEX idx_task_id (task_id),
|
||||
INDEX idx_student_id (student_id),
|
||||
INDEX idx_teacher_id (teacher_id),
|
||||
INDEX idx_deleted_at (deleted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务评价表';
|
||||
Loading…
Reference in New Issue
Block a user