feat: 教师端数据看板与学校端课程统计功能

教师端数据看板:
- 新增 TeacherDashboardResponse/TeacherLessonVO/TeacherLessonTrendVO
- 新增 TeacherWeeklyStatsResponse 周统计响应
- 新增 TeacherActivityLevel 枚举和 TeacherActivityRankResponse 活跃度排行
- 实现教师端课程统计、任务完成详情、任务反馈接口

学校端课程统计:
- 新增 CourseUsageVO/CourseUsageStatsVO/CoursePackageVO
- 新增 SchoolCourseResponse 和学校端课程使用查询接口
- 实现学校端统计数据和课程趋势接口

用户资料功能:
- 新增 UpdateProfileRequest/UpdateProfileResponse
- 实现用户资料更新接口

前后端对齐:
- 更新 OpenAPI 规范和前端 API 类型生成
- 优化 DashboardView 组件和 API 调用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
En 2026-03-21 12:45:56 +08:00
parent c93d325cee
commit 6f64723428
119 changed files with 6325 additions and 2154 deletions

View File

@ -497,6 +497,63 @@ definePage({
- **命名**: `YYYY-MM-DD.md`
- **创建时机**: 每天开始开发时检查并创建
---
## 文件写入最佳实践JetBrains 环境)
> **重要**: 在 JetBrains IDE 插件中使用 Claude Code 时,按以下优先级选择写入方式:
### 写入方式优先级
| 优先级 | 方式 | 适用场景 | 示例 |
|--------|------|----------|------|
| **1** | Write 工具 | 简单文件、单文件写入 | `Write(file_path="...", content="...")` |
| **2** | Bash heredoc | 复杂文件、批量写入、Write 失败时 | `cat > /path/to/file << 'EOF'` |
| **3** | Python 写入 | 需要复杂逻辑处理时 | `python3 -c "..."` |
| **❌ 避免** | sed/awk | Windows Git Bash 中兼容性差 | — |
### Write 工具使用规范
```bash
# ✅ 正确:使用 Unix 风格路径
Write(file_path="/f/LesingleProject/.../file.txt", content="...")
# ❌ 错误:不要使用 Windows 路径
Write(file_path="F:\\LesingleProject\\...\\file.txt", content="...")
```
### Bash heredoc 方式(推荐备选)
```bash
# 多行文件写入
cat > /f/LesingleProject/.../file.txt << 'EOF'
第一行内容
第二行内容
EOF
# 单行追加
echo "追加内容" >> /f/LesingleProject/.../file.txt
```
### 写入前检查清单
- [ ] 确保目录存在:`mkdir -p /f/.../父目录`
- [ ] 使用 Unix 路径格式:`/f/...` 不是 `F:\...`
- [ ] 检查文件是否被占用IDE 索引、格式化中)
### 写入失败诊断
```bash
# 检查目录是否存在
ls -la /f/路径/到的/父目录/
# 检查文件属性
ls -la /f/路径/到/文件.txt
# 测试写入权限
echo 'test' > /f/路径/到/文件.txt && echo '成功' || echo '失败'
```
### 测试记录
- **位置**: `/docs/test-logs/{端}/`
- **命名**: `YYYY-MM-DD.md`

View File

@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(mvn compile:*)",
"Bash(sed:*)"
"Bash(sed:*)",
"Bash(grep:*)"
]
}
}

View File

@ -349,7 +349,7 @@
│ │
│ 📊 平台整体数据 │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ 租户总数 │ 活跃租户 │ 月授课次数 │ 覆盖学生数 │ │
│ │ 租户总数 │ 活跃租户 │ 月授课次数 │ 学生总数数 │ │
│ │ 1,254所 │ 986所 │ 45,678次 │ 234,567人 │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
│ │

View File

@ -54,7 +54,7 @@
- ✅ 账号不存在登录失败
#### 02-dashboard.spec.ts - 数据看板测试 (7 个用例)
- ✅ 验证统计卡片显示(租户数、课程包数、月授课次数、覆盖学生)
- ✅ 验证统计卡片显示(租户数、课程包数、月授课次数、学生总数
- ✅ 验证趋势图加载
- ✅ 验证活跃租户 TOP5 列表
- ✅ 验证热门课程包 TOP5 列表

View File

@ -0,0 +1,176 @@
# 个人中心功能实现记录
## 日期2026-03-20
## 实现内容
### 需求目标
在个人中心页面新增:
1. **密码修改功能**:输入旧密码和新密码进行修改
2. **信息编辑功能**:可修改姓名、手机号、邮箱
---
## 后端实现
### 1. 新建 DTO 文件
#### `UpdateProfileRequest.java`
- 路径:`reading-platform-java/src/main/java/com/reading/platform/dto/request/UpdateProfileRequest.java`
- 字段name姓名、phone手机号、email邮箱
- 校验注解:
- name: `@Pattern(regexp = "^[\u4e00-\u9fa5a-zA-Z\s]{2,20}$")`
- phone: `@Pattern(regexp = "^1[3-9]\d{9}$")`
- email: `@Email`
#### `UpdateProfileResponse.java`
- 路径:`reading-platform-java/src/main/java/com/reading/platform/dto/response/UpdateProfileResponse.java`
- 字段userInfo用户信息、token新 token
### 2. 修改 Service 接口
#### `AuthService.java`
新增方法:
```java
UpdateProfileResponse updateProfile(UpdateProfileRequest request);
void changePassword(String oldPassword, String newPassword, String currentToken);
```
### 3. 修改 Service 实现
#### `AuthServiceImpl.java`
- 新增 `updateProfile()` 方法:
- 根据当前用户角色更新对应表AdminUser/Tenant/Teacher/Parent
- 学校管理员Tenant更新 `contactName/contactPhone/contactEmail` 字段
- 其他角色更新 `name/phone/email` 字段
- 生成新 token 并更新 Redis
- 返回更新后的用户信息和新 token
- 新增 `changePassword(String oldPassword, String newPassword, String currentToken)` 方法:
- 调用原有 `changePassword()` 方法修改密码
- 将当前 token 加入黑名单(使旧 token 失效)
- 新增 `convertToUserInfoResponse(Object userInfo, String role)` 私有方法:
- 将 Entity 对象转换为 UserInfoResponse
### 4. 修改 Controller
#### `AuthController.java`
- 新增接口 `PUT /api/v1/auth/profile`:修改个人信息
- 修改接口 `POST /api/v1/auth/change-password`:增加 HttpServletRequest 参数获取 token
- 新增 `resolveToken(HttpServletRequest request)` 辅助方法:从 Authorization header 获取 token
### 5. 扩展 JwtTokenProvider
#### `JwtTokenProvider.java`
新增方法:
```java
public long getRemainingExpiration(String token)
```
- 用于获取 token 剩余过期时间(秒)
- 用于将 token 加入黑名单时设置过期时间
---
## 前端实现
### 1. 扩展 API 文件
#### `src/api/auth.ts`
新增类型和方法:
```typescript
export interface UpdateProfileDto {
name?: string;
phone?: string;
email?: string;
}
export interface UpdateProfileResponse {
userInfo: UserProfile;
token: string;
}
export function updateProfile(data: UpdateProfileDto): Promise<UpdateProfileResponse>
export function changePassword(oldPassword: string, newPassword: string): Promise<void>
```
### 2. 修改个人中心页面
#### `src/views/profile/ProfileView.vue`
- 新增「编辑资料」按钮:点击后弹出编辑弹窗
- 新增「修改密码」按钮:点击后弹出密码修改弹窗
- 编辑表单:
- 姓名输入框必填2-20 位中文或英文)
- 手机号输入框选填11 位数字正则校验)
- 邮箱输入框选填email 格式校验)
- 密码修改表单:
- 旧密码输入框(必填)
- 新密码输入框(必填,最少 6 位)
- 确认密码输入框(必填,与 new password 一致)
- 修改信息成功后:刷新本地用户信息
- 修改密码成功后:清除 token 和用户信息,跳转到登录页
---
## 后端 API 列表
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取个人信息 | GET | `/api/v1/auth/profile` | 已有 |
| 修改个人信息 | PUT | `/api/v1/auth/profile` | 新增,返回新 token |
| 修改密码 | POST | `/api/v1/auth/change-password` | 已有,修改后 token 失效 |
---
## 验证方案
### 后端验证
1. 启动后端服务(端口 8480
2. 使用 Swagger 测试接口:
- `GET /api/v1/auth/profile` - 获取个人信息
- `PUT /api/v1/auth/profile` - 修改个人信息
- `POST /api/v1/auth/change-password?oldPassword=xxx&newPassword=yyy` - 修改密码
### 前端验证
1. 启动前端服务(端口 5173
2. 登录任意角色账户
3. 访问个人中心页面
4. 测试编辑信息功能
5. 测试修改密码功能
---
## 注意事项
1. **字段映射**
- 前端统一使用 `name/phone/email`
- 后端 Service 层根据角色映射到对应字段
- 学校管理员Tenant映射到 `contactName/contactPhone/contactEmail`
2. **Token 处理**
- 修改个人信息:返回新 token前端替换 localStorage 中的旧 token
- 修改密码:将当前 token 加入黑名单,前端清除 token 并跳转到登录页
3. **表单校验**
- 前端:手机号正则 `/^1[3-9]\d{9}$/`,邮箱使用 Ant Design 内置校验
- 后端:使用 `@Valid` + `@Pattern`/`@Email` 注解校验
---
## 待验证事项
- [ ] 超管角色修改信息功能
- [ ] 学校管理员修改信息功能
- [ ] 教师角色修改信息功能
- [ ] 家长角色修改信息功能
- [ ] 所有角色修改密码功能
- [ ] 修改信息后 token 是否正确刷新
- [ ] 修改密码后 token 是否失效并需重新登录
---
## 下一步计划
1. 启动服务进行功能验证
2. 修复可能发现的问题
3. 更新测试日志

View File

@ -0,0 +1,204 @@
# 开发日志 - 2026-03-20
## 教师端课程使用统计功能实现
### 实现内容
实现了教师端课程使用统计功能(增强版),支持按时间周期筛选和更多维度的统计。
### 后端改动
#### 1. 新增 DTO 类
**CourseUsageQuery.java** - 请求参数 DTO
- `periodType`: 统计周期类型TODAY/WEEK/MONTH/CUSTOM
- `startDate`: 自定义周期开始日期
- `endDate`: 自定义周期结束日期
**CourseUsageStatsVO.java** - 响应对象 DTO增强版
- `coursePackageId`: 课程包 ID
- `coursePackageName`: 课程包名称
- `usageCount`: 使用次数(基于实际授课记录统计)
- `studentCount`: 参与学生数(去重)
- `avgDuration`: 平均时长(分钟)
- `lastUsedAt`: 最后使用时间
#### 2. LessonMapper.java
新增 `getCourseUsageStats()` 方法:
- 使用自定义 SQL 统计课程包使用情况
- 基于 `lesson` 表实际授课记录
- 支持时间范围筛选
- 支持教师 ID 筛选
- 只统计 `COMPLETED` 状态的课程
- 返回 TOP 10 课程包
#### 3. TeacherStatsService.java
新增接口方法:
```java
List<CourseUsageStatsVO> getCourseUsageStats(Long tenantId, Long teacherId, CourseUsageQuery query);
```
#### 4. TeacherStatsServiceImpl.java
实现 `getCourseUsageStats()` 方法:
- 调用 `calculateTimeRange()` 计算时间范围
- 支持四种周期类型TODAY、WEEK、MONTH、CUSTOM
- 调用 Mapper 获取统计数据
实现 `calculateTimeRange()` 辅助方法:
- TODAY: 当天 00:00:00 至今
- WEEK: 本周一至今
- MONTH: 本月 1 号至今
- CUSTOM: 自定义日期范围
#### 5. TeacherStatsController.java
新增接口:
- `GET /api/v1/teacher/course-usage-stats` - 增强版课程使用统计
- 支持参数:`periodType`、`startDate`、`endDate`
- 保留旧接口 `/api/v1/teacher/course-usage`(标记为 @Deprecated
### 前端改动
#### 1. src/api/teacher.ts
新增类型定义:
- `CourseUsageQueryParams` - 查询参数类型
- `CourseUsageStatsItem` - 响应数据类型
新增 API 方法:
- `getTeacherCourseUsageStats()` - 获取增强版课程使用统计
保留旧 API向后兼容
- `getTeacherCourseUsage()`
#### 2. src/views/teacher/DashboardView.vue
**新增响应式数据**
- `usagePeriodType`: 当前选择的周期类型
- `courseUsageStatsData`: 增强版课程使用统计数据
- `periodOptions`: 周期选项(今日/本周/本月)
**UI 改动**
- 在课程使用卡片添加周期选择器a-segmented
- 用户可通过点击切换统计周期
**图表增强**
- `initUsageChart()` 支持新旧两种数据类型
- tooltip 显示更详细信息:
- 使用次数
- 参与学生数
- 平均时长
- 最后使用时间
**数据加载**
- `loadUsageData()` 调用新 API `getTeacherCourseUsageStats()`
- 周期切换时自动重新加载数据
### 统计逻辑
1. **使用次数**:统计周期内 COMPLETED 状态的 lesson 数量
2. **参与学生数**:去重统计 student_record 中的 student_id
3. **平均时长**:计算 lesson 的 start_datetime 到 end_datetime 的分钟差平均值
4. **最后使用时间**:最近一次授课完成时间
### 数据流向
```
用户选择周期 → 前端调用 API → Controller 接收参数
→ Service 计算时间范围 → Mapper 执行 SQL 统计
→ 返回 CourseUsageStatsVO 列表 → 前端图表展示
```
### SQL 统计逻辑
```sql
SELECT
cp.id AS coursePackageId,
cp.name AS coursePackageName,
COUNT(l.id) AS usageCount,
COUNT(DISTINCT sr.student_id) AS studentCount,
AVG(TIMESTAMPDIFF(MINUTE, l.start_datetime, l.end_datetime)) AS avgDuration,
MAX(l.end_datetime) AS lastUsedAt
FROM lesson l
INNER JOIN course_package cp ON l.course_id = cp.id
LEFT JOIN student_record sr ON l.id = sr.lesson_id
WHERE l.tenant_id = #{tenantId}
AND l.end_datetime >= #{startTime}
AND l.end_datetime <= #{endTime}
AND l.status = 'COMPLETED'
AND l.teacher_id = #{teacherId}
GROUP BY cp.id, cp.name
ORDER BY usageCount DESC
LIMIT 10
```
### 测试验证
- [x] 后端编译通过 ✅
- [x] 启动后端服务测试 API ✅
- [x] API 返回数据正确 ✅
- [ ] 前端展示周期选择器
- [ ] 切换周期数据正确刷新
- [ ] tooltip 显示完整信息
### API 测试响应示例
**请求**: `GET /api/v1/teacher/course-usage-stats?periodType=MONTH`
**响应**:
```json
{
"code": 200,
"message": "操作成功",
"data": [
{
"coursePackageId": "17",
"coursePackageName": "课程简介课程简介课程简介课程简介课程简介课程简介课程简介",
"usageCount": 15,
"studentCount": 5,
"avgDuration": 0,
"lastUsedAt": "2026-03-16T00:00:00"
},
{
"coursePackageId": "16",
"coursePackageName": "T 色他",
"usageCount": 1,
"studentCount": 0,
"avgDuration": 0,
"lastUsedAt": "2026-03-16T00:00:00"
}
]
}
```
### 技术难点解决
**问题**: MyBatis `@SelectProvider` 不支持 XML 格式的 `<if>` 动态标签
**解决方案**: 使用 SQL 条件 `AND (#{teacherId} IS NULL OR l.teacher_id = #{teacherId})` 替代动态 SQL实现可选参数过滤。
### 文件清单
**后端新增**
- `dto/request/CourseUsageQuery.java`
- `dto/response/CourseUsageStatsVO.java`
**后端修改**
- `mapper/LessonMapper.java`
- `service/TeacherStatsService.java`
- `service/impl/TeacherStatsServiceImpl.java`
- `controller/teacher/TeacherStatsController.java`
**前端修改**
- `src/api/teacher.ts`
- `src/views/teacher/DashboardView.vue`
### 后续优化建议
1. **前端展示优化**:可以考虑将饼图改为条形图,更适合展示 TOP 排行
2. **导出功能**:支持导出统计数据为 Excel
3. **更多维度**:增加课程类型分布、班级使用对比等
4. **缓存优化**:统计数据可以缓存在 Redis 中,提高查询性能

169
docs/dev-logs/2026-03-21.md Normal file
View File

@ -0,0 +1,169 @@
# 开发日志 2026-03-21
## 学校端 - 课程使用统计功能实现
### 需求背景
学校端 Dashboard 页面已有"课程使用统计"卡片组件,但后端返回空数据。需要实现该功能,让学校管理员能够查看本校各课程包的使用情况。
### 实现内容
#### 1. 后端修改
**Controller 层** (`SchoolStatsController.java`)
- 添加日期范围参数支持,允许前端传入 `startDate``endDate`
- 不传参数时默认统计本月数据
```java
@GetMapping("/courses")
@Operation(summary = "获取课程使用统计")
public Result<List<Map<String, Object>>> getCourseUsageStats(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getCourseUsageStats(tenantId, startDate, endDate));
}
```
**Service 接口** (`SchoolStatsService.java`)
- 更新方法签名,添加日期参数
- 添加 `LocalDate` 导入
```java
List<Map<String, Object>> getCourseUsageStats(Long tenantId, LocalDate startDate, LocalDate endDate);
```
**Service 实现** (`SchoolStatsServiceImpl.java`)
- 使用 `LessonMapper.getCourseUsageStats()` 查询实际数据
- 支持日期范围筛选,默认统计本月
- 将 `CourseUsageStatsVO` 转换为 `Map` 格式返回
```java
@Override
public List<Map<String, Object>> getCourseUsageStats(Long tenantId, LocalDate startDate, LocalDate endDate) {
// 计算时间范围
LocalDateTime startTime;
LocalDateTime endTime;
if (startDate != null) {
startTime = startDate.atStartOfDay();
} else {
startTime = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
}
if (endDate != null) {
endTime = endDate.atTime(23, 59, 59);
} else {
endTime = LocalDateTime.now();
}
// 调用 Mapper 查询
List<CourseUsageStatsVO> stats = lessonMapper.getCourseUsageStats(
tenantId, null, startTime, endTime
);
// 转换为 Map 格式返回
return stats.stream().map(vo -> {
Map<String, Object> map = new HashMap<>();
map.put("courseId", vo.getCoursePackageId());
map.put("courseName", vo.getCoursePackageName());
map.put("usageCount", vo.getUsageCount());
map.put("studentCount", vo.getStudentCount() != null ? vo.getStudentCount() : 0);
map.put("avgDuration", vo.getAvgDuration() != null ? vo.getAvgDuration() : 0);
map.put("lastUsedAt", vo.getLastUsedAt());
return map;
}).collect(Collectors.toList());
}
```
#### 2. 前端修改
**API 层** (`src/api/school.ts`)
- 更新 `getCourseUsageStats` 支持日期参数
- 增强返回类型定义
```typescript
export const getCourseUsageStats = (startDate?: string, endDate?: string) => {
const params: Record<string, string> = {};
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
return http.get<Array<{
courseId: number;
courseName: string;
usageCount: number;
studentCount?: number;
avgDuration?: number;
lastUsedAt?: string;
}>>('/v1/school/stats/courses', { params });
};
```
**组件层** (`src/views/school/DashboardView.vue`)
- `loadCourseStats` 函数传递日期范围参数
- `onMounted` 中设置默认日期范围为当月
```typescript
const loadCourseStats = async () => {
courseStatsLoading.value = true;
try {
const startDate = dateRange.value?.[0]?.format('YYYY-MM-DD');
const endDate = dateRange.value?.[1]?.format('YYYY-MM-DD');
const data = await getCourseUsageStats(startDate, endDate);
courseStats.value = data.slice(0, 10);
} catch (error) {
console.error('Failed to load course stats:', error);
} finally {
courseStatsLoading.value = false;
}
};
onMounted(() => {
// ... 其他初始化
// 设置默认日期范围为当月
const now = dayjs();
const monthStart = now.startOf('month');
const monthEnd = now.endOf('month');
dateRange.value = [monthStart, monthEnd];
});
```
### 数据来源
课程使用统计基于 `lesson` 表的实际授课记录统计:
- **usageCount**: 统计周期内 COMPLETED 状态的 lesson 数量
- **studentCount**: 参与学生数(去重统计 student_record 中的 student_id
- **avgDuration**: 平均时长lesson 的 start_datetime 到 end_datetime 的分钟差平均值)
- **lastUsedAt**: 最后使用时间(最近一次授课完成时间)
### 文件变更列表
| 文件 | 变更说明 |
|------|---------|
| `reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStatsController.java` | 添加日期范围参数 |
| `reading-platform-java/src/main/java/com/reading/platform/service/SchoolStatsService.java` | 更新方法签名 |
| `reading-platform-java/src/main/java/com/reading/platform/service/impl/SchoolStatsServiceImpl.java` | 实现实际查询逻辑 |
| `reading-platform-frontend/src/api/school.ts` | 更新 API 函数和类型 |
| `reading-platform-frontend/src/views/school/DashboardView.vue` | 传递日期参数,设置默认日期范围 |
### 测试验证
- [ ] 后端编译通过
- [ ] 启动后端服务(端口 8480
- [ ] 启动前端服务(端口 5173
- [ ] 登录学校管理员账号
- [ ] 访问 Dashboard 页面,查看"课程使用统计"卡片
- [ ] 验证日期范围筛选功能
- [ ] 验证数据显示正确性
### 后续优化建议
1. **增加更多统计维度**:按班级、按教师统计
2. **可视化增强**:趋势图、热力图
3. **导出功能**Excel 导出课程使用明细
4. **性能优化**:大数据量时考虑缓存

View File

@ -114,7 +114,7 @@
| 租户总数 | 显示真实数据 | 显示 2 | ✅ |
| 课程包总数 | 显示真实数据 | 显示 5 | ✅ |
| 月授课次数 | 显示真实数据 | 显示 22 | ✅ |
| 覆盖学生 | 显示真实数据 | 显示 5 | ✅ |
| 学生总数 | 显示真实数据 | 显示 5 | ✅ |
### 3.2 使用趋势图
| 测试项 | 预期结果 | 实际结果 | 状态 |

View File

@ -51,7 +51,7 @@ reading-platform-frontend/tests/e2e/admin/
| 测试项 | 状态 | 说明 |
|--------|------|------|
| 验证统计卡片显示 | ✅ | 租户数、课程包数、月授课次数、覆盖学生 |
| 验证统计卡片显示 | ✅ | 租户数、课程包数、月授课次数、学生总数 |
| 验证趋势图加载 | ✅ | 验证图表容器显示 |
| 验证活跃租户 TOP5 列表 | ✅ | 验证列表数据 |
| 验证热门课程包 TOP5 列表 | ✅ | 验证列表数据 |

File diff suppressed because it is too large Load Diff

View File

@ -107,6 +107,7 @@ export interface AdminStatsResponse {
totalStudents: number;
totalTeachers: number;
totalLessons: number;
monthlyLessons: number;
}
// 前端使用的统计数据结构
@ -124,9 +125,8 @@ export interface AdminStats {
// 后端返回的趋势数据结构(分离数组格式)
export interface StatsTrendResponse {
dates: string[];
newStudents: number[];
newTeachers: number[];
newCourses: number[];
lessonCounts: number[];
studentCounts: number[];
}
// 前端使用的趋势数据结构
@ -141,17 +141,16 @@ export interface TrendData {
export interface ActiveTenantResponse {
tenantId: number;
tenantName: string;
activeUsers: number;
courseCount: number;
activeTeacherCount: number;
completedLessonCount: number;
}
// 前端使用的活跃租户结构
export interface ActiveTenant {
id: number;
name: string;
lessonCount: number;
teacherCount: number | string;
studentCount: number | string;
activeTeacherCount: number;
completedLessonCount: number;
}
// 后端返回的热门课程结构
@ -273,7 +272,7 @@ const mapStatsData = (data: AdminStatsResponse): AdminStats => ({
teacherCount: data.totalTeachers || 0,
lessonCount: data.totalLessons || 0,
publishedCourseCount: 0, // 后端暂无此数据
monthlyLessons: 0, // 后端暂无此数据
monthlyLessons: data.monthlyLessons || 0,
});
// 趋势数据映射:分离数组 -> 对象数组
@ -282,8 +281,8 @@ const mapTrendData = (data: StatsTrendResponse): TrendData[] => {
return data.dates.map((date, index) => ({
month: date,
tenantCount: 0, // 后端无此数据
lessonCount: data.newCourses?.[index] || 0,
studentCount: data.newStudents?.[index] || 0,
lessonCount: data.lessonCounts?.[index] || 0,
studentCount: data.studentCounts?.[index] || 0,
}));
};
@ -293,9 +292,8 @@ const mapActiveTenants = (data: ActiveTenantResponse[]): ActiveTenant[] => {
return data.map(item => ({
id: item.tenantId,
name: item.tenantName,
teacherCount: '-', // 后端无单独字段
studentCount: '-', // 后端无单独字段
lessonCount: item.courseCount, // 使用 courseCount 替代
activeTeacherCount: item.activeTeacherCount ?? 0,
completedLessonCount: item.completedLessonCount ?? 0,
}));
};

View File

@ -55,3 +55,26 @@ export function refreshToken(): Promise<{ token: string }> {
export function getProfile(): Promise<UserProfile> {
return http.get('/v1/auth/profile');
}
// 修改个人信息
export interface UpdateProfileDto {
name?: string;
phone?: string;
email?: string;
}
export interface UpdateProfileResponse {
userInfo: UserProfile;
token: string;
}
export function updateProfile(data: UpdateProfileDto): Promise<UpdateProfileResponse> {
return http.put('/v1/auth/profile', data);
}
// 修改密码(修改成功后 token 失效,需重新登录)
export function changePassword(oldPassword: string, newPassword: string): Promise<void> {
return http.post('/v1/auth/change-password', null, {
params: { oldPassword, newPassword },
});
}

View File

@ -29,6 +29,7 @@ import type {
GenerateReadOnlyTokenParams,
GetActiveTeachersParams,
GetActiveTenantsParams,
GetAllCoursesParams,
GetAllStudentsParams,
GetCalendarViewDataParams,
GetClassPageParams,
@ -57,8 +58,11 @@ import type {
GetSchedules1Params,
GetSchedulesParams,
GetSchoolCoursesParams,
GetStatisticsParams,
GetStudentPageParams,
GetTaskPage1Params,
GetTaskCompletions1Params,
GetTaskCompletionsParams,
GetTaskListParams,
GetTaskPageParams,
GetTasksByStudentParams,
GetTeacherPageParams,
@ -82,7 +86,6 @@ import type {
RefreshTokenRequest,
RenewRequest,
ResetPassword1Params,
ResetPasswordParams,
ResourceItemCreateRequest,
ResourceItemUpdateRequest,
ResourceLibraryCreateRequest,
@ -96,6 +99,8 @@ import type {
StudentRecordRequest,
StudentUpdateRequest,
TaskCreateRequest,
TaskFeedbackRequest,
TaskSubmitRequest,
TaskTemplateCreateRequest,
TaskUpdateRequest,
TeacherCreateRequest,
@ -106,6 +111,7 @@ import type {
UpdateBasicSettings1Body,
UpdateClassTeacherBody,
UpdateNotificationSettings1Body,
UpdateProfileRequest,
UpdateSecuritySettings1Body,
UpdateSettings1Body,
UpdateStorageSettingsBody,
@ -162,6 +168,38 @@ const deleteTask = (
);
}
/**
* @summary
*/
const updateFeedback = (
completionId: number,
taskFeedbackRequest: TaskFeedbackRequest,
) => {
return customMutator<Blob>(
{url: `/v1/teacher/tasks/completions/${completionId}/feedback`, method: 'PUT',
headers: {'Content-Type': 'application/json', },
data: taskFeedbackRequest,
responseType: 'blob'
},
);
}
/**
* @summary
*/
const submitFeedback = (
completionId: number,
taskFeedbackRequest: TaskFeedbackRequest,
) => {
return customMutator<Blob>(
{url: `/v1/teacher/tasks/completions/${completionId}/feedback`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: taskFeedbackRequest,
responseType: 'blob'
},
);
}
/**
* @summary
*/
@ -388,48 +426,6 @@ const deleteTeacher = (
);
}
/**
* @summary Get task by ID
*/
const getTask1 = (
id: number,
) => {
return customMutator<Blob>(
{url: `/v1/school/tasks/${id}`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary Update task
*/
const updateTask1 = (
id: number,
taskUpdateRequest: TaskUpdateRequest,
) => {
return customMutator<Blob>(
{url: `/v1/school/tasks/${id}`, method: 'PUT',
headers: {'Content-Type': 'application/json', },
data: taskUpdateRequest,
responseType: 'blob'
},
);
}
/**
* @summary Delete task
*/
const deleteTask1 = (
id: number,
) => {
return customMutator<Blob>(
{url: `/v1/school/tasks/${id}`, method: 'DELETE',
responseType: 'blob'
},
);
}
/**
* @summary
*/
@ -825,6 +821,38 @@ const removeClassTeacher = (
);
}
/**
* @summary
*/
const updateSubmission = (
taskId: number,
taskSubmitRequest: TaskSubmitRequest,
) => {
return customMutator<Blob>(
{url: `/v1/parent/tasks/${taskId}/submit`, method: 'PUT',
headers: {'Content-Type': 'application/json', },
data: taskSubmitRequest,
responseType: 'blob'
},
);
}
/**
* @summary
*/
const submitTask = (
taskId: number,
taskSubmitRequest: TaskSubmitRequest,
) => {
return customMutator<Blob>(
{url: `/v1/parent/tasks/${taskId}/submit`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: taskSubmitRequest,
responseType: 'blob'
},
);
}
/**
* @summary Get growth record by ID
*/
@ -867,6 +895,34 @@ const deleteGrowthRecord2 = (
);
}
/**
* @summary
*/
const getCurrentUser = (
) => {
return customMutator<Blob>(
{url: `/v1/auth/profile`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary
*/
const updateProfile = (
updateProfileRequest: UpdateProfileRequest,
) => {
return customMutator<Blob>(
{url: `/v1/auth/profile`, method: 'PUT',
headers: {'Content-Type': 'application/json', },
data: updateProfileRequest,
responseType: 'blob'
},
);
}
/**
* @summary
*/
@ -1650,7 +1706,7 @@ const getLessonFeedback = (
/**
* @summary
*/
const submitFeedback = (
const submitFeedback1 = (
id: number,
lessonFeedbackRequest: LessonFeedbackRequest,
) => {
@ -1778,40 +1834,9 @@ const createTeacher = (
*/
const resetPassword = (
id: number,
params: ResetPasswordParams,
) => {
return customMutator<Blob>(
{url: `/v1/school/teachers/${id}/reset-password`, method: 'POST',
params,
responseType: 'blob'
},
);
}
/**
* @summary Get task page
*/
const getTaskPage1 = (
params?: GetTaskPage1Params,
) => {
return customMutator<Blob>(
{url: `/v1/school/tasks`, method: 'GET',
params,
responseType: 'blob'
},
);
}
/**
* @summary Create task
*/
const createTask1 = (
taskCreateRequest: TaskCreateRequest,
) => {
return customMutator<Blob>(
{url: `/v1/school/tasks`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: taskCreateRequest,
responseType: 'blob'
},
);
@ -2157,7 +2182,7 @@ const assignStudents = (
}
/**
* @summary Complete task
* @summary 使
*/
const completeTask = (
id: number,
@ -2723,6 +2748,34 @@ const getTodayLessons = (
);
}
/**
* @summary
*/
const getTaskCompletions = (
taskId: number,
params?: GetTaskCompletionsParams,
) => {
return customMutator<Blob>(
{url: `/v1/teacher/tasks/${taskId}/completions`, method: 'GET',
params,
responseType: 'blob'
},
);
}
/**
* @summary
*/
const getCompletionDetail = (
completionId: number,
) => {
return customMutator<Blob>(
{url: `/v1/teacher/tasks/completions/${completionId}`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary
*/
@ -2941,10 +2994,11 @@ const getCourse = (
* @summary
*/
const getAllCourses = (
params?: GetAllCoursesParams,
) => {
return customMutator<Blob>(
{url: `/v1/teacher/courses/all`, method: 'GET',
params,
responseType: 'blob'
},
);
@ -3191,6 +3245,75 @@ const getCourseReports = (
);
}
/**
* @summary
*/
const getTaskList = (
params?: GetTaskListParams,
) => {
return customMutator<Blob>(
{url: `/v1/school/reading-tasks`, method: 'GET',
params,
responseType: 'blob'
},
);
}
/**
* @summary
*/
const getTaskDetail = (
taskId: number,
) => {
return customMutator<Blob>(
{url: `/v1/school/reading-tasks/${taskId}`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary
*/
const getTaskCompletions1 = (
taskId: number,
params?: GetTaskCompletions1Params,
) => {
return customMutator<Blob>(
{url: `/v1/school/reading-tasks/${taskId}/completions`, method: 'GET',
params,
responseType: 'blob'
},
);
}
/**
* @summary
*/
const getStatistics = (
params?: GetStatisticsParams,
) => {
return customMutator<Blob>(
{url: `/v1/school/reading-tasks/statistics`, method: 'GET',
params,
responseType: 'blob'
},
);
}
/**
* @summary
*/
const getCompletionDetail1 = (
completionId: number,
) => {
return customMutator<Blob>(
{url: `/v1/school/reading-tasks/completions/${completionId}`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary Get children of parent
*/
@ -3418,7 +3541,7 @@ const getSchoolCourse = (
}
/**
* @summary Get my tasks
* @summary
*/
const getMyTasks = (
params?: GetMyTasksParams,
@ -3432,9 +3555,9 @@ const getMyTasks = (
}
/**
* @summary Get task by ID
* @summary
*/
const getTask2 = (
const getTask1 = (
id: number,
) => {
return customMutator<Blob>(
@ -3445,7 +3568,7 @@ const getTask2 = (
}
/**
* @summary Get tasks by student ID
* @summary
*/
const getTasksByStudent = (
studentId: number,
@ -3459,6 +3582,32 @@ const getTasksByStudent = (
);
}
/**
* @summary
*/
const getCompletionDetail2 = (
completionId: number,
) => {
return customMutator<Blob>(
{url: `/v1/parent/tasks/completions/${completionId}`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary
*/
const getFeedback = (
completionId: number,
) => {
return customMutator<Blob>(
{url: `/v1/parent/tasks/completions/${completionId}/feedback`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary Get my notifications
*/
@ -3612,19 +3761,6 @@ const getOssToken = (
);
}
/**
* @summary
*/
const getCurrentUser = (
) => {
return customMutator<Blob>(
{url: `/v1/auth/profile`, method: 'GET',
responseType: 'blob'
},
);
}
/**
* @summary
*/
@ -3800,10 +3936,12 @@ const deleteFile = (
);
}
return {getTask,updateTask,deleteTask,getTemplate,updateTemplate,deleteTemplate,getSchedule,updateSchedule,cancelSchedule,getLesson,updateLesson,getLessonProgress,saveLessonProgress,getGrowthRecord,updateGrowthRecord,deleteGrowthRecord,getTeacher,updateTeacher,deleteTeacher,getTask1,updateTask1,deleteTask1,getTemplate1,updateTemplate1,deleteTemplate1,getStudent,updateStudent,deleteStudent,getSettings,updateSettings,getSecuritySettings,updateSecuritySettings,getNotificationSettings,updateNotificationSettings,getBasicSettings,updateBasicSettings,getSchedule1,updateSchedule1,cancelSchedule1,getParent,updateParent,deleteParent,getGrowthRecord1,updateGrowthRecord1,deleteGrowthRecord1,getClass,updateClass,deleteClass,updateClassTeacher,removeClassTeacher,getGrowthRecord2,updateGrowthRecord2,deleteGrowthRecord2,findOne,update,_delete,reorder,getTenant,updateTenant,deleteTenant,updateTenantStatus,updateTenantQuota,getAllSettings,updateSettings1,getStorageSettings,updateStorageSettings,getSecuritySettings1,updateSecuritySettings1,getNotificationSettings1,updateNotificationSettings1,getBasicSettings1,updateBasicSettings1,findLibrary,updateLibrary,deleteLibrary,findItem,updateItem,deleteItem,getCourse1,updateCourse,deleteCourse,reorderSteps,findOne1,update1,delete1,updateStep,removeStep,reorder1,findOne2,update2,delete2,setPackages,getTaskPage,createTask,getTemplates,createTemplate,createFromTemplate,getSchedules,createSchedule,markAsRead,markAllAsRead,getMyLessons,createLesson,saveStudentRecord,batchSaveStudentRecords,startLesson,getLessonFeedback,submitFeedback,completeLesson,cancelLesson,createLessonFromSchedule,startLessonFromSchedule,getGrowthRecordPage,createGrowthRecord,getTeacherPage,createTeacher,resetPassword,getTaskPage1,createTask1,getTemplates1,createTemplate1,getStudentPage,createStudent,getSchedules1,createSchedule1,checkConflict,batchCreateSchedules,createSchedulesByClasses,getParentPage,createParent,bindStudent,unbindStudent,resetPassword1,renewCollection,getGrowthRecordPage1,createGrowthRecord1,getClassPage,createClass,getClassTeachers1,assignTeachers,getClassStudents1,assignStudents,completeTask,markAsRead1,markAllAsRead1,createGrowthRecord2,refreshToken,uploadFile,refreshToken1,logout,login,changePassword,findAll,create,getTenantPage,createTenant,resetTenantPassword,findAllLibraries,createLibrary,findAllItems,createItem,batchDeleteItems,getCoursePage1,createCourse,submitCourse,rejectCourse,publishCourse,archiveCourse,findAll1,create1,findSteps,createStep,page,create2,withdraw,submit,republish,reject,publish,archive,getWeeklyStats,getTodayLessons,getDefaultTemplate,getAllStudents,getTodaySchedules,getTimetable,getRecommendedCourses,getMyNotifications,getNotification,getUnreadCount,getStudentRecords,getTodayLessons1,getLessonTrend,getFeedbacks,getFeedbackStats,getDashboard,getCoursePage,getCourse,getAllCourses,getCourseUsage,getClasses,getClassTeachers,getClassStudents,getDefaultTemplate1,getSchoolStats,getActiveTeachers,getLessonTrend1,getCourseUsageStats,getCourseDistribution,getRecentActivities,getTimetable1,getCoursePackageLessonTypes,getCalendarViewData,getTeacherReports,getStudentReports,getOverview,getCourseReports,getParentChildren,findTenantCollections,getPackagesByCollection,getPackageCourses,getPackageInfo,getPackageUsage,getLogList,getLogDetail,getLogStats,getFeedbacks1,getFeedbackStats1,exportTeacherStats,exportStudentStats,exportLessons,exportGrowthRecords,getSchoolCourses,getSchoolCourse,getMyTasks,getTask2,getTasksByStudent,getMyNotifications1,getNotification1,getUnreadCount1,getGrowthRecordsByStudent,getRecentGrowthRecords,getMyChildren,getChild,getChildGrowth,generateEditToken,generateReadOnlyToken,getOssToken,getCurrentUser,getTenantStats,getAllActiveTenants,getStats,getTrendData,getActiveTenants,getPopularCourses,getRecentActivities1,getTenantDefaults,getStats1,getAllPublishedCourses,findByType,getAllPublishedCollections,deleteFile}};
return {getTask,updateTask,deleteTask,updateFeedback,submitFeedback,getTemplate,updateTemplate,deleteTemplate,getSchedule,updateSchedule,cancelSchedule,getLesson,updateLesson,getLessonProgress,saveLessonProgress,getGrowthRecord,updateGrowthRecord,deleteGrowthRecord,getTeacher,updateTeacher,deleteTeacher,getTemplate1,updateTemplate1,deleteTemplate1,getStudent,updateStudent,deleteStudent,getSettings,updateSettings,getSecuritySettings,updateSecuritySettings,getNotificationSettings,updateNotificationSettings,getBasicSettings,updateBasicSettings,getSchedule1,updateSchedule1,cancelSchedule1,getParent,updateParent,deleteParent,getGrowthRecord1,updateGrowthRecord1,deleteGrowthRecord1,getClass,updateClass,deleteClass,updateClassTeacher,removeClassTeacher,updateSubmission,submitTask,getGrowthRecord2,updateGrowthRecord2,deleteGrowthRecord2,getCurrentUser,updateProfile,findOne,update,_delete,reorder,getTenant,updateTenant,deleteTenant,updateTenantStatus,updateTenantQuota,getAllSettings,updateSettings1,getStorageSettings,updateStorageSettings,getSecuritySettings1,updateSecuritySettings1,getNotificationSettings1,updateNotificationSettings1,getBasicSettings1,updateBasicSettings1,findLibrary,updateLibrary,deleteLibrary,findItem,updateItem,deleteItem,getCourse1,updateCourse,deleteCourse,reorderSteps,findOne1,update1,delete1,updateStep,removeStep,reorder1,findOne2,update2,delete2,setPackages,getTaskPage,createTask,getTemplates,createTemplate,createFromTemplate,getSchedules,createSchedule,markAsRead,markAllAsRead,getMyLessons,createLesson,saveStudentRecord,batchSaveStudentRecords,startLesson,getLessonFeedback,submitFeedback1,completeLesson,cancelLesson,createLessonFromSchedule,startLessonFromSchedule,getGrowthRecordPage,createGrowthRecord,getTeacherPage,createTeacher,resetPassword,getTemplates1,createTemplate1,getStudentPage,createStudent,getSchedules1,createSchedule1,checkConflict,batchCreateSchedules,createSchedulesByClasses,getParentPage,createParent,bindStudent,unbindStudent,resetPassword1,renewCollection,getGrowthRecordPage1,createGrowthRecord1,getClassPage,createClass,getClassTeachers1,assignTeachers,getClassStudents1,assignStudents,completeTask,markAsRead1,markAllAsRead1,createGrowthRecord2,refreshToken,uploadFile,refreshToken1,logout,login,changePassword,findAll,create,getTenantPage,createTenant,resetTenantPassword,findAllLibraries,createLibrary,findAllItems,createItem,batchDeleteItems,getCoursePage1,createCourse,submitCourse,rejectCourse,publishCourse,archiveCourse,findAll1,create1,findSteps,createStep,page,create2,withdraw,submit,republish,reject,publish,archive,getWeeklyStats,getTodayLessons,getTaskCompletions,getCompletionDetail,getDefaultTemplate,getAllStudents,getTodaySchedules,getTimetable,getRecommendedCourses,getMyNotifications,getNotification,getUnreadCount,getStudentRecords,getTodayLessons1,getLessonTrend,getFeedbacks,getFeedbackStats,getDashboard,getCoursePage,getCourse,getAllCourses,getCourseUsage,getClasses,getClassTeachers,getClassStudents,getDefaultTemplate1,getSchoolStats,getActiveTeachers,getLessonTrend1,getCourseUsageStats,getCourseDistribution,getRecentActivities,getTimetable1,getCoursePackageLessonTypes,getCalendarViewData,getTeacherReports,getStudentReports,getOverview,getCourseReports,getTaskList,getTaskDetail,getTaskCompletions1,getStatistics,getCompletionDetail1,getParentChildren,findTenantCollections,getPackagesByCollection,getPackageCourses,getPackageInfo,getPackageUsage,getLogList,getLogDetail,getLogStats,getFeedbacks1,getFeedbackStats1,exportTeacherStats,exportStudentStats,exportLessons,exportGrowthRecords,getSchoolCourses,getSchoolCourse,getMyTasks,getTask1,getTasksByStudent,getCompletionDetail2,getFeedback,getMyNotifications1,getNotification1,getUnreadCount1,getGrowthRecordsByStudent,getRecentGrowthRecords,getMyChildren,getChild,getChildGrowth,generateEditToken,generateReadOnlyToken,getOssToken,getTenantStats,getAllActiveTenants,getStats,getTrendData,getActiveTenants,getPopularCourses,getRecentActivities1,getTenantDefaults,getStats1,getAllPublishedCourses,findByType,getAllPublishedCollections,deleteFile}};
export type GetTaskResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTask']>>>
export type UpdateTaskResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateTask']>>>
export type DeleteTaskResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteTask']>>>
export type UpdateFeedbackResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateFeedback']>>>
export type SubmitFeedbackResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['submitFeedback']>>>
export type GetTemplateResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTemplate']>>>
export type UpdateTemplateResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateTemplate']>>>
export type DeleteTemplateResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteTemplate']>>>
@ -3820,9 +3958,6 @@ export type DeleteGrowthRecordResult = NonNullable<Awaited<ReturnType<ReturnType
export type GetTeacherResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTeacher']>>>
export type UpdateTeacherResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateTeacher']>>>
export type DeleteTeacherResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteTeacher']>>>
export type GetTask1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTask1']>>>
export type UpdateTask1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateTask1']>>>
export type DeleteTask1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteTask1']>>>
export type GetTemplate1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTemplate1']>>>
export type UpdateTemplate1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateTemplate1']>>>
export type DeleteTemplate1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteTemplate1']>>>
@ -3851,9 +3986,13 @@ export type UpdateClassResult = NonNullable<Awaited<ReturnType<ReturnType<typeof
export type DeleteClassResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteClass']>>>
export type UpdateClassTeacherResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateClassTeacher']>>>
export type RemoveClassTeacherResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['removeClassTeacher']>>>
export type UpdateSubmissionResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateSubmission']>>>
export type SubmitTaskResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['submitTask']>>>
export type GetGrowthRecord2Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getGrowthRecord2']>>>
export type UpdateGrowthRecord2Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateGrowthRecord2']>>>
export type DeleteGrowthRecord2Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['deleteGrowthRecord2']>>>
export type GetCurrentUserResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getCurrentUser']>>>
export type UpdateProfileResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['updateProfile']>>>
export type FindOneResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['findOne']>>>
export type UpdateResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['update']>>>
export type _DeleteResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['_delete']>>>
@ -3908,7 +4047,7 @@ export type SaveStudentRecordResult = NonNullable<Awaited<ReturnType<ReturnType<
export type BatchSaveStudentRecordsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['batchSaveStudentRecords']>>>
export type StartLessonResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['startLesson']>>>
export type GetLessonFeedbackResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getLessonFeedback']>>>
export type SubmitFeedbackResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['submitFeedback']>>>
export type SubmitFeedback1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['submitFeedback1']>>>
export type CompleteLessonResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['completeLesson']>>>
export type CancelLessonResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['cancelLesson']>>>
export type CreateLessonFromScheduleResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['createLessonFromSchedule']>>>
@ -3918,8 +4057,6 @@ export type CreateGrowthRecordResult = NonNullable<Awaited<ReturnType<ReturnType
export type GetTeacherPageResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTeacherPage']>>>
export type CreateTeacherResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['createTeacher']>>>
export type ResetPasswordResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['resetPassword']>>>
export type GetTaskPage1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTaskPage1']>>>
export type CreateTask1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['createTask1']>>>
export type GetTemplates1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTemplates1']>>>
export type CreateTemplate1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['createTemplate1']>>>
export type GetStudentPageResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getStudentPage']>>>
@ -3983,6 +4120,8 @@ export type PublishResult = NonNullable<Awaited<ReturnType<ReturnType<typeof get
export type ArchiveResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['archive']>>>
export type GetWeeklyStatsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getWeeklyStats']>>>
export type GetTodayLessonsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTodayLessons']>>>
export type GetTaskCompletionsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTaskCompletions']>>>
export type GetCompletionDetailResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getCompletionDetail']>>>
export type GetDefaultTemplateResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getDefaultTemplate']>>>
export type GetAllStudentsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getAllStudents']>>>
export type GetTodaySchedulesResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTodaySchedules']>>>
@ -4018,6 +4157,11 @@ export type GetTeacherReportsResult = NonNullable<Awaited<ReturnType<ReturnType<
export type GetStudentReportsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getStudentReports']>>>
export type GetOverviewResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getOverview']>>>
export type GetCourseReportsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getCourseReports']>>>
export type GetTaskListResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTaskList']>>>
export type GetTaskDetailResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTaskDetail']>>>
export type GetTaskCompletions1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTaskCompletions1']>>>
export type GetStatisticsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getStatistics']>>>
export type GetCompletionDetail1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getCompletionDetail1']>>>
export type GetParentChildrenResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getParentChildren']>>>
export type FindTenantCollectionsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['findTenantCollections']>>>
export type GetPackagesByCollectionResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getPackagesByCollection']>>>
@ -4036,8 +4180,10 @@ export type ExportGrowthRecordsResult = NonNullable<Awaited<ReturnType<ReturnTyp
export type GetSchoolCoursesResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getSchoolCourses']>>>
export type GetSchoolCourseResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getSchoolCourse']>>>
export type GetMyTasksResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getMyTasks']>>>
export type GetTask2Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTask2']>>>
export type GetTask1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTask1']>>>
export type GetTasksByStudentResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTasksByStudent']>>>
export type GetCompletionDetail2Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getCompletionDetail2']>>>
export type GetFeedbackResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getFeedback']>>>
export type GetMyNotifications1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getMyNotifications1']>>>
export type GetNotification1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getNotification1']>>>
export type GetUnreadCount1Result = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getUnreadCount1']>>>
@ -4049,7 +4195,6 @@ export type GetChildGrowthResult = NonNullable<Awaited<ReturnType<ReturnType<typ
export type GenerateEditTokenResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['generateEditToken']>>>
export type GenerateReadOnlyTokenResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['generateReadOnlyToken']>>>
export type GetOssTokenResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getOssToken']>>>
export type GetCurrentUserResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getCurrentUser']>>>
export type GetTenantStatsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getTenantStats']>>>
export type GetAllActiveTenantsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getAllActiveTenants']>>>
export type GetStatsResult = NonNullable<Awaited<ReturnType<ReturnType<typeof getReadingPlatformAPI>['getStats']>>>

View File

@ -14,8 +14,8 @@ export interface ActiveTenantItemResponse {
tenantId?: number;
/** 租户名称 */
tenantName?: string;
/** 活跃用户数 */
activeUsers?: number;
/** 课程使用数 */
courseCount?: number;
/** 活跃教师数(近 30 天有完成课程的老师数) */
activeTeacherCount?: number;
/** 完成课次数(近 30 天 COMPLETED 状态的 lesson 总数) */
completedLessonCount?: number;
}

View File

@ -0,0 +1,19 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface ClassInfo {
/** 班级ID */
id?: number;
/** 班级名称 */
name?: string;
/** 年级 */
grade?: string;
}

View File

@ -16,4 +16,6 @@ export interface CourseCollectionPageQueryRequest {
pageSize?: number;
/** 状态 */
status?: string;
/** 年级(支持多个,逗号分隔) */
gradeLevels?: string;
}

View File

@ -0,0 +1,31 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface CoursePackageVO {
/** ID */
id?: number;
/** 名称 */
name?: string;
/** 描述 */
description?: string;
/** 适用年级 */
gradeLevel?: string;
/** 课程数量 */
courseCount?: number;
/** 状态 */
status?: string;
/** 使用次数 */
usageCount?: number;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
updatedAt?: string;
}

View File

@ -20,6 +20,8 @@ export interface CoursePageQueryRequest {
category?: string;
/** 状态 */
status?: string;
/** 年级(支持多个,逗号分隔) */
gradeTags?: string;
/** 是否仅查询待审核 */
reviewOnly?: boolean;
}

View File

@ -6,6 +6,7 @@
* OpenAPI spec version: 1.0.0
*/
import type { CourseLessonResponse } from './courseLessonResponse';
import type { LessonTagResponse } from './lessonTagResponse';
/**
*
@ -89,10 +90,10 @@ export interface CourseResponse {
activitiesData?: string;
/** 评估数据 */
assessmentData?: string;
/** 年级标签 */
gradeTags?: string;
/** 领域标签 */
domainTags?: string;
/** 年级标签(规范为数组,与套餐管理适用年级对齐) */
gradeTags?: string[];
/** 领域标签(规范为数组) */
domainTags?: string[];
/** 是否有集体课 */
hasCollectiveLesson?: number;
/** 版本号 */
@ -129,4 +130,6 @@ export interface CourseResponse {
updatedAt?: string;
/** 关联的课程环节 */
courseLessons?: CourseLessonResponse[];
/** 课程环节标签(列表展示用,仅 name 和 lessonType */
lessonTags?: LessonTagResponse[];
}

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
* 使
*/
export interface CourseUsageVO {
/** 课程包名称 */
name?: string;
/** 使用次数 */
value?: number;
}

View File

@ -16,6 +16,8 @@ export interface DayScheduleItem {
className?: string;
/** 课程包名称 */
coursePackageName?: string;
/** 课程类型代码 (如 DOMAIN_HEALTH) */
lessonType?: string;
/** 课程类型名称 */
lessonTypeName?: string;
/** 教师名称 */

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
export type GetAllCoursesParams = {
keyword?: string;
grade?: string;
domain?: string;
};

View File

@ -10,5 +10,7 @@ export type GetCoursePageParams = {
pageNum?: number;
pageSize?: number;
keyword?: string;
category?: string;
grade?: string;
domain?: string;
lessonType?: string;
};

View File

@ -7,5 +7,5 @@
*/
export type GetLessonTrend1Params = {
months?: number;
days?: number;
};

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
export type GetStatisticsParams = {
dateType?: string;
startDate?: string;
endDate?: string;
};

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
export type GetTaskCompletions1Params = {
pageNum?: number;
pageSize?: number;
status?: string;
};

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
export type GetTaskCompletionsParams = {
pageNum?: number;
pageSize?: number;
status?: string;
};

View File

@ -0,0 +1,23 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
export type GetTaskListParams = {
pageNum?: number;
pageSize?: number;
keyword?: string;
type?: string;
status?: string;
classIds?: number[];
teacherIds?: number[];
dateType?: string;
startDate?: string;
endDate?: string;
completionRate?: string;
sortBy?: string;
sortOrder?: string;
};

View File

@ -10,4 +10,6 @@ export type GetTemplates1Params = {
pageNum?: number;
pageSize?: number;
type?: string;
taskType?: string;
keyword?: string;
};

View File

@ -10,4 +10,5 @@ export type GetTemplatesParams = {
pageNum?: number;
pageSize?: number;
type?: string;
keyword?: string;
};

View File

@ -12,7 +12,6 @@ export * from './addClassTeacherDto';
export * from './addPackageToCollectionParams';
export * from './adminStatsControllerGetActiveTenantsParams';
export * from './adminStatsControllerGetPopularCoursesParams';
export * from './adminStatsControllerGetRecentActivitiesParams';
export * from './approveCourseDto';
export * from './approveCourseDtoChecklist';
export * from './basicSettingsResponse';
@ -26,6 +25,7 @@ export * from './calendarViewResponseSchedules';
export * from './changePasswordParams';
export * from './checkConflictParams';
export * from './classCreateRequest';
export * from './classInfo';
export * from './classResponse';
export * from './classTeacherResponse';
export * from './classUpdateRequest';
@ -48,11 +48,13 @@ export * from './coursePackageControllerFindAllParams';
export * from './coursePackageCourseItem';
export * from './coursePackageItem';
export * from './coursePackageResponse';
export * from './coursePackageVO';
export * from './coursePageQueryRequest';
export * from './courseRejectRequest';
export * from './courseReportResponse';
export * from './courseResponse';
export * from './courseUpdateRequest';
export * from './courseUsageVO';
export * from './createClassDto';
export * from './createCollectionRequest';
export * from './createFromSourceDto';
@ -95,6 +97,7 @@ export * from './getActiveTeachersParams';
export * from './getActiveTenants200';
export * from './getActiveTenants200DataItem';
export * from './getActiveTenantsParams';
export * from './getAllCoursesParams';
export * from './getAllStudentsParams';
export * from './getCalendarViewDataParams';
export * from './getClassPageParams';
@ -119,15 +122,15 @@ export * from './getParentPageParams';
export * from './getPopularCourses200';
export * from './getPopularCourses200DataItem';
export * from './getPopularCoursesParams';
export * from './getRecentActivities1200';
export * from './getRecentActivities1200DataItem';
export * from './getRecentActivities1Params';
export * from './getRecentActivitiesParams';
export * from './getRecentGrowthRecordsParams';
export * from './getSchedules1Params';
export * from './getSchedulesParams';
export * from './getSchoolCoursesParams';
export * from './getStatisticsParams';
export * from './getStudentPageParams';
export * from './getTaskCompletions1Params';
export * from './getTaskCompletionsParams';
export * from './getTaskListParams';
export * from './getTaskPage1Params';
export * from './getTaskPageParams';
export * from './getTasksByStudentParams';
@ -164,6 +167,7 @@ export * from './lessonResponse';
export * from './lessonStep';
export * from './lessonStepCreateRequest';
export * from './lessonStepResponse';
export * from './lessonTagResponse';
export * from './lessonTypeInfo';
export * from './lessonUpdateRequest';
export * from './libraryCreateRequest';
@ -211,9 +215,11 @@ export * from './pageResultResourceItemResponse';
export * from './pageResultResourceLibrary';
export * from './pageResultResourceLibraryResponse';
export * from './pageResultSchedulePlanResponse';
export * from './pageResultSchoolCourseResponse';
export * from './pageResultStudent';
export * from './pageResultStudentResponse';
export * from './pageResultTask';
export * from './pageResultTaskCompletionDetailResponse';
export * from './pageResultTaskResponse';
export * from './pageResultTaskTemplateResponse';
export * from './pageResultTeacher';
@ -227,8 +233,6 @@ export * from './parentStudentResponse';
export * from './parentUpdateRequest';
export * from './popularCourseItemResponse';
export * from './popularCoursesQueryRequest';
export * from './recentActivitiesQueryRequest';
export * from './recentActivityItemResponse';
export * from './refreshTokenRequest';
export * from './rejectCourseDto';
export * from './rejectCourseDtoChecklist';
@ -278,8 +282,10 @@ export * from './resultListCourseLesson';
export * from './resultListCourseLessonResponse';
export * from './resultListCoursePackage';
export * from './resultListCoursePackageResponse';
export * from './resultListCoursePackageVO';
export * from './resultListCourseReportResponse';
export * from './resultListCourseResponse';
export * from './resultListCourseUsageVO';
export * from './resultListGrowthRecord';
export * from './resultListGrowthRecordResponse';
export * from './resultListLesson';
@ -291,12 +297,13 @@ export * from './resultListMapStringObject';
export * from './resultListMapStringObjectDataItem';
export * from './resultListParentStudentResponse';
export * from './resultListPopularCourseItemResponse';
export * from './resultListRecentActivityItemResponse';
export * from './resultListSchedulePlanResponse';
export * from './resultListStudent';
export * from './resultListStudentRecordResponse';
export * from './resultListStudentReportResponse';
export * from './resultListStudentResponse';
export * from './resultListTeacherLessonTrendVO';
export * from './resultListTeacherLessonVO';
export * from './resultListTeacherReportResponse';
export * from './resultListTeacherResponse';
export * from './resultListTenantPackage';
@ -308,6 +315,8 @@ export * from './resultLoginResponse';
export * from './resultLong';
export * from './resultMapStringObject';
export * from './resultMapStringObjectData';
export * from './resultMapStringString';
export * from './resultMapStringStringData';
export * from './resultNotification';
export * from './resultNotificationResponse';
export * from './resultNotificationSettingsResponse';
@ -341,9 +350,11 @@ export * from './resultPageResultResourceItemResponse';
export * from './resultPageResultResourceLibrary';
export * from './resultPageResultResourceLibraryResponse';
export * from './resultPageResultSchedulePlanResponse';
export * from './resultPageResultSchoolCourseResponse';
export * from './resultPageResultStudent';
export * from './resultPageResultStudentResponse';
export * from './resultPageResultTask';
export * from './resultPageResultTaskCompletionDetailResponse';
export * from './resultPageResultTaskResponse';
export * from './resultPageResultTaskTemplateResponse';
export * from './resultPageResultTeacher';
@ -358,6 +369,7 @@ export * from './resultResourceItemResponse';
export * from './resultResourceLibrary';
export * from './resultResourceLibraryResponse';
export * from './resultSchedulePlanResponse';
export * from './resultSchoolCourseResponse';
export * from './resultSchoolSettingsResponse';
export * from './resultSecuritySettingsResponse';
export * from './resultStatsResponse';
@ -367,16 +379,21 @@ export * from './resultStudent';
export * from './resultStudentRecordResponse';
export * from './resultStudentResponse';
export * from './resultTask';
export * from './resultTaskCompletionDetailResponse';
export * from './resultTaskFeedbackResponse';
export * from './resultTaskResponse';
export * from './resultTaskTemplateResponse';
export * from './resultTeacher';
export * from './resultTeacherDashboardResponse';
export * from './resultTeacherResponse';
export * from './resultTeacherWeeklyStatsResponse';
export * from './resultTenant';
export * from './resultTenantResponse';
export * from './resultTheme';
export * from './resultThemeResponse';
export * from './resultTimetableResponse';
export * from './resultTokenResponse';
export * from './resultUpdateProfileResponse';
export * from './resultUserInfoResponse';
export * from './resultVoid';
export * from './resultVoidData';
@ -387,6 +404,7 @@ export * from './schedulePlanCreateRequest';
export * from './schedulePlanResponse';
export * from './schedulePlanUpdateRequest';
export * from './schoolControllerImportStudentsParams';
export * from './schoolCourseResponse';
export * from './schoolFeedbackControllerFindAllParams';
export * from './schoolSettingsResponse';
export * from './schoolSettingsUpdateRequest';
@ -395,12 +413,12 @@ export * from './securitySettingsResponse';
export * from './securitySettingsUpdateRequest';
export * from './statsControllerGetActiveTeachersParams';
export * from './statsControllerGetLessonTrendParams';
export * from './statsControllerGetRecentActivitiesParams';
export * from './statsResponse';
export * from './statsTrendResponse';
export * from './stepCreateRequest';
export * from './student';
export * from './studentCreateRequest';
export * from './studentInfo';
export * from './studentRecordDto';
export * from './studentRecordRequest';
export * from './studentRecordResponse';
@ -409,8 +427,12 @@ export * from './studentResponse';
export * from './studentUpdateRequest';
export * from './submitCourseDto';
export * from './task';
export * from './taskCompletionDetailResponse';
export * from './taskCreateRequest';
export * from './taskFeedbackRequest';
export * from './taskFeedbackResponse';
export * from './taskResponse';
export * from './taskSubmitRequest';
export * from './taskTemplateCreateRequest';
export * from './taskTemplateResponse';
export * from './taskUpdateRequest';
@ -422,11 +444,17 @@ export * from './teacherCourseControllerGetLessonTrendParams';
export * from './teacherCourseControllerGetTeacherSchedulesParams';
export * from './teacherCourseControllerGetTeacherTimetableParams';
export * from './teacherCreateRequest';
export * from './teacherDashboardResponse';
export * from './teacherFeedbackControllerFindAllParams';
export * from './teacherLessonTrendVO';
export * from './teacherLessonVO';
export * from './teacherReportResponse';
export * from './teacherResponse';
export * from './teacherResponseClassNames';
export * from './teacherStats';
export * from './teacherTaskControllerGetMonthlyStatsParams';
export * from './teacherUpdateRequest';
export * from './teacherWeeklyStatsResponse';
export * from './tenant';
export * from './tenantControllerFindAllPackageType';
export * from './tenantControllerFindAllParams';
@ -452,6 +480,8 @@ export * from './updateLessonDto';
export * from './updateLibraryDto';
export * from './updateNotificationSettings1Body';
export * from './updateNotificationSettingsBody';
export * from './updateProfileRequest';
export * from './updateProfileResponse';
export * from './updateResourceItemDto';
export * from './updateSchedule1Body';
export * from './updateScheduleBody';

View File

@ -15,7 +15,7 @@ import type { LessonResponse } from './lessonResponse';
export interface LessonDetailResponse {
lesson?: LessonResponse;
course?: CourseResponse;
class?: ClassResponse;
/** 排课选择的课程类型(子课程模式时用于直接进入对应子课程) */
lessonType?: string;
class?: ClassResponse;
}

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface LessonTagResponse {
/** 环节名称 */
name?: string;
/** 环节类型INTRODUCTION、COLLECTIVE、LANGUAGE、HEALTH、SCIENCE、SOCIAL、ART */
lessonType?: string;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { SchoolCourseResponse } from './schoolCourseResponse';
export interface PageResultSchoolCourseResponse {
list?: SchoolCourseResponse[];
total?: number;
pageNum?: number;
pageSize?: number;
pages?: number;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TaskCompletionDetailResponse } from './taskCompletionDetailResponse';
export interface PageResultTaskCompletionDetailResponse {
list?: TaskCompletionDetailResponse[];
total?: number;
pageNum?: number;
pageSize?: number;
pages?: number;
}

View File

@ -1,25 +0,0 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface RecentActivityItemResponse {
/** 活动 ID */
activityId?: number;
/** 活动类型 */
activityType?: string;
/** 活动描述 */
description?: string;
/** 操作人 ID */
operatorId?: number;
/** 操作人名称 */
operatorName?: string;
/** 操作时间 */
operationTime?: string;
}

View File

@ -5,10 +5,10 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { RecentActivityItemResponse } from './recentActivityItemResponse';
import type { CoursePackageVO } from './coursePackageVO';
export interface ResultListRecentActivityItemResponse {
export interface ResultListCoursePackageVO {
code?: number;
message?: string;
data?: RecentActivityItemResponse[];
data?: CoursePackageVO[];
}

View File

@ -5,10 +5,10 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { GetRecentActivities1200DataItem } from './getRecentActivities1200DataItem';
import type { CourseUsageVO } from './courseUsageVO';
export type GetRecentActivities1200 = {
export interface ResultListCourseUsageVO {
code?: number;
message?: string;
data?: GetRecentActivities1200DataItem[];
};
data?: CourseUsageVO[];
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TeacherLessonTrendVO } from './teacherLessonTrendVO';
export interface ResultListTeacherLessonTrendVO {
code?: number;
message?: string;
data?: TeacherLessonTrendVO[];
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TeacherLessonVO } from './teacherLessonVO';
export interface ResultListTeacherLessonVO {
code?: number;
message?: string;
data?: TeacherLessonVO[];
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { ResultMapStringStringData } from './resultMapStringStringData';
export interface ResultMapStringString {
code?: number;
message?: string;
data?: ResultMapStringStringData;
}

View File

@ -6,4 +6,4 @@
* OpenAPI spec version: 1.0.0
*/
export type GetRecentActivities1200DataItem = { [key: string]: unknown };
export type ResultMapStringStringData = {[key: string]: string};

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { PageResultSchoolCourseResponse } from './pageResultSchoolCourseResponse';
export interface ResultPageResultSchoolCourseResponse {
code?: number;
message?: string;
data?: PageResultSchoolCourseResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { PageResultTaskCompletionDetailResponse } from './pageResultTaskCompletionDetailResponse';
export interface ResultPageResultTaskCompletionDetailResponse {
code?: number;
message?: string;
data?: PageResultTaskCompletionDetailResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { SchoolCourseResponse } from './schoolCourseResponse';
export interface ResultSchoolCourseResponse {
code?: number;
message?: string;
data?: SchoolCourseResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TaskCompletionDetailResponse } from './taskCompletionDetailResponse';
export interface ResultTaskCompletionDetailResponse {
code?: number;
message?: string;
data?: TaskCompletionDetailResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TaskFeedbackResponse } from './taskFeedbackResponse';
export interface ResultTaskFeedbackResponse {
code?: number;
message?: string;
data?: TaskFeedbackResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TeacherDashboardResponse } from './teacherDashboardResponse';
export interface ResultTeacherDashboardResponse {
code?: number;
message?: string;
data?: TeacherDashboardResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TeacherWeeklyStatsResponse } from './teacherWeeklyStatsResponse';
export interface ResultTeacherWeeklyStatsResponse {
code?: number;
message?: string;
data?: TeacherWeeklyStatsResponse;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { UpdateProfileResponse } from './updateProfileResponse';
export interface ResultUpdateProfileResponse {
code?: number;
message?: string;
data?: UpdateProfileResponse;
}

View File

@ -0,0 +1,50 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { LessonTagResponse } from './lessonTagResponse';
/**
*
*/
export interface SchoolCourseResponse {
/** ID */
id?: number;
/** 租户 ID */
tenantId?: number;
/** 课程名称 */
name?: string;
/** 课程编码 */
code?: string;
/** 描述 */
description?: string;
/** 绘本名称 */
pictureBookName?: string;
/** 封面图片路径 */
coverImagePath?: string;
/** 封面 URL */
coverUrl?: string;
/** 年级标签(规范为数组) */
gradeTags?: string[];
/** 领域标签(规范为数组) */
domainTags?: string[];
/** 课程时长(分钟) */
duration?: number;
/** 使用次数 */
usageCount?: number;
/** 教师数量 */
teacherCount?: number;
/** 平均评分 */
avgRating?: number;
/** 状态 */
status?: string;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
updatedAt?: string;
/** 课程环节标签(列表展示用,仅 name 和 lessonType */
lessonTags?: LessonTagResponse[];
}

View File

@ -22,4 +22,6 @@ export interface StatsResponse {
totalCourses?: number;
/** 课时总数 */
totalLessons?: number;
/** 月授课次数(本月 COMPLETED 状态的 lesson 总数) */
monthlyLessons?: number;
}

View File

@ -12,10 +12,8 @@
export interface StatsTrendResponse {
/** 日期列表 */
dates?: string[];
/** 新增学生数列表 */
newStudents?: number[];
/** 新增教师数列表 */
newTeachers?: number[];
/** 新增课程数列表 */
newCourses?: number[];
/** 授课次数列表(近 7 天每天完成的课程数) */
lessonCounts?: number[];
/** 活跃学生数列表(近 7 天每天有上课记录的去重学生数) */
studentCounts?: number[];
}

View File

@ -0,0 +1,23 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { ClassInfo } from './classInfo';
/**
*
*/
export interface StudentInfo {
/** 学生ID */
id?: number;
/** 学生姓名 */
name?: string;
/** 学生头像 */
avatar?: string;
/** 性别MALE/FEMALE */
gender?: string;
classInfo?: ClassInfo;
}

View File

@ -28,6 +28,8 @@ export interface Task {
description?: string;
/** 任务类型 */
type?: string;
/** 关联绘本名称 */
relatedBookName?: string;
/** 课程 ID */
courseId?: number;
/** 创建人 ID */

View File

@ -0,0 +1,39 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { StudentInfo } from './studentInfo';
import type { TaskFeedbackResponse } from './taskFeedbackResponse';
/**
*
*/
export interface TaskCompletionDetailResponse {
/** 完成记录ID */
id?: number;
/** 任务ID */
taskId?: number;
/** 任务标题 */
taskTitle?: string;
student?: StudentInfo;
/** 状态PENDING/SUBMITTED/REVIEWED */
status?: string;
/** 状态文本:待完成/已提交/已评价 */
statusText?: string;
/** 照片URL数组 */
photos?: string[];
/** 视频URL */
videoUrl?: string;
/** 语音URL */
audioUrl?: string;
/** 完成内容/阅读心得 */
content?: string;
/** 提交时间 */
submittedAt?: string;
/** 评价时间 */
reviewedAt?: string;
feedback?: TaskFeedbackResponse;
}

View File

@ -16,6 +16,8 @@ export interface TaskCreateRequest {
description?: string;
/** 任务类型reading-阅读homework-作业activity-活动 */
type?: string;
/** 关联绘本名称(手动填写) */
relatedBookName?: string;
/** 课程 ID */
courseId?: number;
/** 开始日期 */

View File

@ -0,0 +1,27 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface TaskFeedbackRequest {
/** 评价结果EXCELLENT-优秀/PASSED-通过/NEEDS_WORK-需改进 */
result: string;
/**
* 1-5
* @minimum 1
* @maximum 5
*/
rating?: number;
/**
*
* @minLength 0
* @maxLength 500
*/
comment?: string;
}

View File

@ -0,0 +1,33 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface TaskFeedbackResponse {
/** 评价ID */
id?: number;
/** 完成记录ID */
completionId?: number;
/** 评价结果EXCELLENT/PASSED/NEEDS_WORK */
result?: string;
/** 评价结果文本:优秀/通过/需改进 */
resultText?: string;
/** 评分 1-5 */
rating?: number;
/** 评语 */
comment?: string;
/** 教师ID */
teacherId?: number;
/** 教师姓名 */
teacherName?: string;
/** 教师头像 */
teacherAvatar?: string;
/** 评价时间 */
createdAt?: string;
}

View File

@ -20,6 +20,8 @@ export interface TaskResponse {
description?: string;
/** 任务类型 */
type?: string;
/** 关联绘本名称 */
relatedBookName?: string;
/** 课程 ID */
courseId?: number;
/** 创建人 ID */

View File

@ -0,0 +1,31 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface TaskSubmitRequest {
/** 学生ID */
studentId?: number;
/**
* URL数组9
* @minItems 0
* @maxItems 9
*/
photos?: string[];
/** 视频URL */
videoUrl?: string;
/** 语音URL */
audioUrl?: string;
/**
* /
* @minLength 0
* @maxLength 1000
*/
content?: string;
}

View File

@ -23,9 +23,9 @@ export interface TaskTemplateCreateRequest {
/** 默认持续时间 (天) */
defaultDuration?: number;
/** 是否默认模板 */
isDefault?: number;
isDefault?: boolean;
/** 模板内容 */
content?: string;
/** 是否公开 */
isPublic?: number;
isPublic?: boolean;
}

View File

@ -10,18 +10,20 @@
*
*/
export interface TeacherCreateRequest {
/** 用户名 */
/** 用户名/登录账号 */
username: string;
/** 密码 */
password: string;
/** 姓名 */
name: string;
/** 电话 */
phone?: string;
phone: string;
/** 邮箱 */
email?: string;
/** 性别 */
gender?: string;
/** 简介 */
bio?: string;
/** 负责班级ID列表 */
classIds?: number[];
}

View File

@ -0,0 +1,26 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { CoursePackageVO } from './coursePackageVO';
import type { TeacherDashboardResponseRecentActivitiesItem } from './teacherDashboardResponseRecentActivitiesItem';
import type { TeacherLessonVO } from './teacherLessonVO';
import type { TeacherStats } from './teacherStats';
import type { TeacherWeeklyStatsResponse } from './teacherWeeklyStatsResponse';
/**
*
*/
export interface TeacherDashboardResponse {
stats?: TeacherStats;
/** 今日课程 */
todayLessons?: TeacherLessonVO[];
/** 推荐课程 */
recommendedCourses?: CoursePackageVO[];
weeklyStats?: TeacherWeeklyStatsResponse;
/** 近期活动 */
recentActivities?: TeacherDashboardResponseRecentActivitiesItem[];
}

View File

@ -0,0 +1,19 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface TeacherLessonTrendVO {
/** 月份yyyy-MM 格式) */
month?: string;
/** 课时数量 */
lessonCount?: number;
/** 平均评分 */
avgRating?: number;
}

View File

@ -0,0 +1,44 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { LocalTime } from './localTime';
/**
*
*/
export interface TeacherLessonVO {
/** ID */
id?: number;
/** 租户 ID */
tenantId?: number;
/** 课程 ID */
courseId?: number;
/** 班级 ID */
classId?: number;
/** 课程名称 */
courseName?: string;
/** 班级名称 */
className?: string;
/** 教师 ID */
teacherId?: number;
/** 标题 */
title?: string;
/** 课时日期 */
lessonDate?: string;
startTime?: LocalTime;
endTime?: LocalTime;
/** 地点 */
location?: string;
/** 状态 */
status?: string;
/** 备注 */
notes?: string;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
updatedAt?: string;
}

View File

@ -5,6 +5,7 @@
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { TeacherResponseClassNames } from './teacherResponseClassNames';
/**
*
@ -14,8 +15,6 @@ export interface TeacherResponse {
id?: number;
/** 租户 ID */
tenantId?: number;
/** 用户名 */
username?: string;
/** 姓名 */
name?: string;
/** 电话 */
@ -30,10 +29,18 @@ export interface TeacherResponse {
bio?: string;
/** 状态 */
status?: string;
/** 负责班级ID列表 */
classIds?: number[];
/** 负责班级名称 */
classNames?: TeacherResponseClassNames;
/** 授课次数 */
lessonCount?: number;
/** 最后登录时间 */
lastLoginAt?: string;
/** 创建时间 */
createdAt?: string;
/** 更新时间 */
updatedAt?: string;
/** 登录账号 */
loginAccount?: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export type TeacherResponseClassNames = { [key: string]: unknown };

View File

@ -0,0 +1,21 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface TeacherStats {
/** 班级数量 */
classCount?: number;
/** 学生数量 */
studentCount?: number;
/** 课程包数量 */
courseCount?: number;
/** 课时数量 */
lessonCount?: number;
}

View File

@ -24,4 +24,6 @@ export interface TeacherUpdateRequest {
bio?: string;
/** 状态 */
status?: string;
/** 负责班级ID列表 */
classIds?: number[];
}

View File

@ -0,0 +1,21 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface TeacherWeeklyStatsResponse {
/** 本周课时数量 */
lessonCount?: number;
/** 学生参与率 */
studentParticipation?: number;
/** 平均评分 */
avgRating?: number;
/** 总时长(分钟) */
totalDuration?: number;
}

View File

@ -26,8 +26,6 @@ export interface TenantCreateRequest {
address?: string;
/** Logo URL */
logoUrl?: string;
/** 套餐类型 */
packageType?: string;
/** 教师配额 */
teacherQuota?: number;
/** 学生配额 */
@ -36,6 +34,6 @@ export interface TenantCreateRequest {
startDate?: string;
/** 结束日期 */
expireDate?: string;
/** 课程套餐 ID(可选) */
collectionId?: number;
/** 课程套餐 ID 列表(可选,支持多选) */
collectionIds?: number[];
}

View File

@ -36,8 +36,8 @@ export interface TenantResponse {
maxStudents?: number;
/** 最大教师数 */
maxTeachers?: number;
/** 套餐类型 */
packageType?: string;
/** 套餐名称列表 */
packageNames?: string[];
/** 教师配额 */
teacherQuota?: number;
/** 学生配额 */

View File

@ -24,10 +24,8 @@ export interface TenantUpdateRequest {
logoUrl?: string;
/** 状态 */
status?: string;
/** 套餐类型 */
packageType?: string;
/** 课程套餐ID用于三层架构 */
collectionId?: number;
collectionIds?: number[];
/** 教师配额 */
teacherQuota?: number;
/** 学生配额 */

View File

@ -0,0 +1,25 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
/**
*
*/
export interface UpdateProfileRequest {
/**
*
* @pattern ^[-a-zA-Z\s]{2,20}$
*/
name?: string;
/**
*
* @pattern ^1[3-9]\d{9}$
*/
phone?: string;
/** 邮箱 */
email?: string;
}

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v8.5.3 🍺
* Do not edit manually.
* Reading Platform API
* Reading Platform Backend Service API Documentation
* OpenAPI spec version: 1.0.0
*/
import type { UserInfoResponse } from './userInfoResponse';
/**
*
*/
export interface UpdateProfileResponse {
userInfo?: UserInfoResponse;
/** 新的 Token用于刷新 */
token?: string;
}

View File

@ -247,13 +247,23 @@ export const getSchoolStats = () =>
http.get<SchoolStats>('/v1/school/stats');
export const getActiveTeachers = (limit?: number) =>
http.get<Array<{ id: number; name: string; lessonCount: number }>>('/v1/school/stats/teachers', { params: { limit } });
http.get<Array<{
teacherId: number | string;
teacherName: string;
classNames: string;
lessonCount: number;
courseCount: number;
lastActiveAt?: string;
activityLevelCode: string;
activityLevelDesc: string;
}>>('/v1/school/stats/teachers', { params: { limit } });
export const getCourseUsageStats = () =>
http.get<Array<{ courseId: number; courseName: string; usageCount: number }>>('/v1/school/stats/courses');
export const getRecentActivities = (limit?: number) =>
http.get<Array<{ id: number; type: string; title: string; time: string }>>('/v1/school/stats/activities', { params: { limit } });
export const getCourseUsageStats = (startDate?: string, endDate?: string) => {
const params: Record<string, string> = {};
if (startDate) params.startDate = startDate;
if (endDate) params.endDate = endDate;
return http.get<Array<{ courseId: number; courseName: string; usageCount: number; studentCount?: number; avgDuration?: number; lastUsedAt?: string }>>('/v1/school/stats/courses', { params });
};
// ==================== 套餐信息(旧 API保留兼容 ====================
@ -632,7 +642,7 @@ export const getCalendarViewData = (params?: {
// ==================== 趋势与分布统计 ====================
export interface LessonTrendItem {
month: string;
date: string; // 日期MM-dd 格式)
lessonCount: number;
studentCount: number;
}
@ -642,8 +652,11 @@ export interface CourseDistributionItem {
value: number;
}
export const getLessonTrend = (months?: number) =>
http.get<LessonTrendItem[]>('/v1/school/stats/lesson-trend', { params: { months } });
// 后端趋势数据响应(对象数组格式)
export type LessonTrendResponse = LessonTrendItem[];
export const getLessonTrend = (days?: number) =>
http.get<LessonTrendResponse>('/v1/school/stats/lesson-trend', { params: { days } });
export const getCourseDistribution = () =>
http.get<CourseDistributionItem[]>('/v1/school/stats/course-distribution');

View File

@ -312,59 +312,69 @@ export function batchSaveStudentRecords(
// ==================== 教师首页 API ====================
export interface TeacherDashboardStats {
classCount: number;
studentCount: number;
lessonCount: number;
courseCount: number;
}
export interface TeacherLessonItem {
id: number;
tenantId: number;
courseId: number;
classId: number;
courseName: string;
className: string;
teacherId: number;
title: string;
lessonDate: string;
startTime: string;
endTime: string;
location: string;
status: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface TeacherCoursePackageItem {
id: number;
name: string;
description?: string;
gradeLevel?: string;
courseCount?: number;
status: string;
usageCount?: number;
createdAt: string;
updatedAt: string;
}
export interface TeacherWeeklyStatsData {
lessonCount: number;
studentParticipation: number;
avgRating: number;
totalDuration: number;
}
export interface DashboardData {
stats: {
classCount: number;
studentCount: number;
lessonCount: number;
courseCount: number;
};
todayLessons: Array<{
id: number;
courseId: number;
courseName: string;
pictureBookName?: string;
classId: number;
className: string;
plannedDatetime: string;
status: string;
duration: number;
}>;
recommendedCourses: Array<{
id: number;
name: string;
pictureBookName?: string;
coverImagePath?: string;
duration: number;
usageCount: number;
avgRating: number;
gradeTags: string[];
}>;
weeklyStats: {
lessonCount: number;
studentParticipation: number;
avgRating: number;
totalDuration: number;
};
recentActivities: Array<{
id: number;
type: string;
description: string;
time: string;
}>;
stats: TeacherDashboardStats;
todayLessons: TeacherLessonItem[];
recommendedCourses: TeacherCoursePackageItem[];
weeklyStats: TeacherWeeklyStatsData;
}
export const getTeacherDashboard = () =>
http.get('/v1/teacher/dashboard') as any;
http.get<DashboardData>('/v1/teacher/dashboard');
export const getTodayLessons = () =>
http.get('/v1/teacher/today-lessons') as any;
http.get<TeacherLessonItem[]>('/v1/teacher/today-lessons');
export const getRecommendedCourses = () =>
http.get('/v1/teacher/recommended-courses') as any;
http.get<TeacherCoursePackageItem[]>('/v1/teacher/recommended-courses');
export const getWeeklyStats = () =>
http.get('/v1/teacher/weekly-stats') as any;
http.get<TeacherWeeklyStatsData>('/v1/teacher/weekly-stats');
// ==================== 教师统计趋势 ====================
@ -379,12 +389,33 @@ export interface TeacherCourseUsageItem {
value: number;
}
// 增强版课程使用统计类型
export interface CourseUsageQueryParams {
periodType?: 'TODAY' | 'WEEK' | 'MONTH' | 'CUSTOM';
startDate?: string;
endDate?: string;
}
export interface CourseUsageStatsItem {
coursePackageId: number;
coursePackageName: string;
usageCount: number;
studentCount: number;
avgDuration: number;
lastUsedAt?: string;
}
export const getTeacherLessonTrend = (months?: number) => {
return http.get('/v1/teacher/lesson-trend', { params: { months } }) as any;
return http.get<TeacherLessonTrendItem[]>('/v1/teacher/lesson-trend', { params: { months } });
};
// 旧版 API保留向后兼容
export const getTeacherCourseUsage = () =>
http.get('/v1/teacher/course-usage') as any;
http.get<TeacherCourseUsageItem[]>('/v1/teacher/course-usage');
// 增强版课程使用统计 API
export const getTeacherCourseUsageStats = (params?: CourseUsageQueryParams) =>
http.get<CourseUsageStatsItem[]>('/v1/teacher/course-usage-stats', { params });
// ==================== 课程反馈 API ====================

View File

@ -107,28 +107,6 @@
</a-col>
</a-row>
<!-- 最近活动 -->
<a-row :gutter="24" style="margin-top: 24px">
<a-col :span="24">
<a-card title="最近活动" :bordered="false" :loading="activitiesLoading" class="modern-card">
<div v-if="recentActivities.length > 0" class="activity-timeline">
<div v-for="activity in recentActivities" :key="activity.id" class="activity-item">
<div class="activity-dot" :class="'type-' + activity.type"></div>
<div class="activity-content">
<div class="activity-header">
<a-tag :color="getActivityTagColor(activity.type)" class="activity-tag">
{{ getActivityTypeText(activity.type) }}
</a-tag>
<span class="activity-title">{{ activity.title }}</span>
</div>
<span class="activity-time">{{ formatTime(activity.time) }}</span>
</div>
</div>
</div>
<a-empty v-else description="暂无活动记录" />
</a-card>
</a-col>
</a-row>
</div>
</template>
@ -167,7 +145,6 @@ let trendChart: echarts.ECharts | null = null;
const trendLoading = ref(false);
const tenantsLoading = ref(false);
const coursesLoading = ref(false);
const activitiesLoading = ref(false);
//
const statsData = ref<AdminStats>({
@ -187,37 +164,6 @@ const activeTenants = ref<ActiveTenant[]>([]);
//
const popularCourses = ref<PopularCourse[]>([]);
//
const recentActivities = ref<Array<{
id: number;
type: string;
title: string;
time: string;
}>>([]);
// Activity colors
const getActivityTagColor = (type: string) => {
const colors: Record<string, string> = {
lesson: 'blue',
tenant: 'green',
course: 'orange',
};
return colors[type] || 'default';
};
const getActivityTypeText = (type: string) => {
const texts: Record<string, string> = {
lesson: '授课',
tenant: '租户',
course: '课程',
};
return texts[type] || '其他';
};
const formatTime = (time: string) => {
return dayjs(time).format('YYYY-MM-DD HH:mm');
};
// Navigation
const viewTenantDetail = (id: number) => {
router.push(`/admin/tenants?id=${id}`);
@ -393,24 +339,6 @@ const fetchPopularCourses = async () => {
}
};
const fetchRecentActivities = async () => {
activitiesLoading.value = true;
try {
const response = await fetch('/api/v1/admin/stats/activities?limit=10', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
if (response.ok) {
recentActivities.value = await response.json();
}
} catch (error) {
console.error('Failed to fetch recent activities:', error);
} finally {
activitiesLoading.value = false;
}
};
// Handle window resize
const handleResize = () => {
trendChart?.resize();
@ -421,7 +349,6 @@ onMounted(() => {
fetchTrendData();
fetchActiveTenants();
fetchPopularCourses();
fetchRecentActivities();
window.addEventListener('resize', handleResize);
});
@ -624,65 +551,6 @@ $bg-light: #F9FAFB;
}
}
}
// 线
.activity-timeline {
.activity-item {
display: flex;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid $border-color;
&:last-child {
border-bottom: none;
}
.activity-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-top: 6px;
margin-right: 16px;
flex-shrink: 0;
&.type-lesson {
background: $primary-color;
}
&.type-tenant {
background: $success-color;
}
&.type-course {
background: $warning-color;
}
}
.activity-content {
flex: 1;
.activity-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
.activity-tag {
margin: 0;
}
.activity-title {
color: $text-color;
}
}
.activity-time {
font-size: 12px;
color: $text-secondary;
}
}
}
}
}
@media (max-width: 1200px) {

View File

@ -34,27 +34,145 @@
<a-descriptions-item label="邮箱">
{{ profile.email || '-' }}
</a-descriptions-item>
<!-- <a-descriptions-item label="所属机构" v-if="profile.tenantId">
{{ profile.tenantName || `租户ID: ${profile.tenantId}` }}
</a-descriptions-item> -->
</a-descriptions>
</div>
<div class="action-section">
<a-button type="primary" @click="enterEditMode">
<EditOutlined /> 编辑资料
</a-button>
<a-button @click="showChangePasswordModal" style="margin-left: 12px">
<LockOutlined /> 修改密码
</a-button>
</div>
</div>
</div>
<a-empty v-else-if="!loading" description="加载失败,请刷新重试" />
</a-spin>
<!-- 编辑资料弹窗 -->
<a-modal
v-model:open="editModalOpen"
title="编辑个人信息"
@ok="submitEdit"
:confirmLoading="editLoading"
width="480px"
>
<a-form
:model="editForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item
label="姓名"
name="name"
:rules="[
{ required: true, message: '请输入姓名' },
{ pattern: /^[\u4e00-\u9fa5a-zA-Z\s]{2,20}$/, message: '姓名长度为 2-20 位,只能包含中文或英文' }
]"
>
<a-input v-model:value="editForm.name" placeholder="请输入姓名" />
</a-form-item>
<a-form-item
label="手机号"
name="phone"
:rules="[
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
]"
>
<a-input v-model:value="editForm.phone" placeholder="请输入手机号" />
</a-form-item>
<a-form-item
label="邮箱"
name="email"
:rules="[
{ type: 'email', message: '请输入正确的邮箱格式' }
]"
>
<a-input v-model:value="editForm.email" placeholder="请输入邮箱" />
</a-form-item>
</a-form>
</a-modal>
<!-- 修改密码弹窗 -->
<a-modal
v-model:open="passwordModalOpen"
title="修改密码"
@ok="submitChangePassword"
:confirmLoading="passwordLoading"
width="480px"
>
<a-form
:model="passwordForm"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item
label="旧密码"
name="oldPassword"
:rules="[{ required: true, message: '请输入旧密码' }]"
>
<a-input-password v-model:value="passwordForm.oldPassword" placeholder="请输入旧密码" />
</a-form-item>
<a-form-item
label="新密码"
name="newPassword"
:rules="[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度不能少于 6 位' }
]"
>
<a-input-password v-model:value="passwordForm.newPassword" placeholder="请输入新密码" />
</a-form-item>
<a-form-item
label="确认密码"
name="confirmPassword"
:rules="[
{ required: true, message: '请确认新密码' },
{ validator: validateConfirmPassword }
]"
>
<a-input-password v-model:value="passwordForm.confirmPassword" placeholder="请再次输入新密码" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { UserOutlined } from '@ant-design/icons-vue';
import { getProfile, type UserProfile } from '@/api/auth';
import { UserOutlined, EditOutlined, LockOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { getProfile, type UserProfile, updateProfile, changePassword, type UpdateProfileDto } from '@/api/auth';
import { useRouter } from 'vue-router';
const router = useRouter();
const loading = ref(false);
const profile = ref<UserProfile | null>(null);
//
const editModalOpen = ref(false);
const editLoading = ref(false);
const editForm = ref<UpdateProfileDto>({
name: '',
phone: '',
email: '',
});
//
const passwordModalOpen = ref(false);
const passwordLoading = ref(false);
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const avatarUrl = computed(() => {
const p = profile.value;
if (!p) return '';
@ -84,6 +202,73 @@ const loadProfile = async () => {
}
};
//
const enterEditMode = () => {
if (profile.value) {
editForm.value = {
name: profile.value.name || '',
phone: profile.value.phone || '',
email: profile.value.email || '',
};
}
editModalOpen.value = true;
};
//
const submitEdit = async () => {
try {
editLoading.value = true;
await updateProfile(editForm.value);
message.success('修改成功');
editModalOpen.value = false;
await loadProfile();
} catch (error: any) {
console.error('修改失败', error);
message.error(error.message || '修改失败,请重试');
} finally {
editLoading.value = false;
}
};
//
const showChangePasswordModal = () => {
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: '',
};
passwordModalOpen.value = true;
};
//
const validateConfirmPassword = async (_rule: any, value: string) => {
if (value !== passwordForm.value.newPassword) {
throw new Error('两次输入的密码不一致');
}
};
//
const submitChangePassword = async () => {
try {
passwordLoading.value = true;
await changePassword(passwordForm.value.oldPassword, passwordForm.value.newPassword);
message.success('密码修改成功,请重新登录');
passwordModalOpen.value = false;
//
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
setTimeout(() => {
router.push('/login');
}, 1000);
} catch (error: any) {
console.error('修改密码失败', error);
message.error(error.message || '修改密码失败,请重试');
} finally {
passwordLoading.value = false;
}
};
onMounted(() => {
loadProfile();
});
@ -139,4 +324,11 @@ onMounted(() => {
font-weight: 500;
color: #666;
}
.action-section {
margin-top: 24px;
display: flex;
justify-content: center;
gap: 12px;
}
</style>

View File

@ -53,69 +53,55 @@
</div>
</div>
<!-- 主要内容区域 -->
<div class="content-grid">
<!-- 近期活动 -->
<div class="content-card activities-card">
<div class="card-header">
<span class="card-icon"><CalendarOutlined /></span>
<h3>近期课程活动</h3>
<!-- 教师活跃度排行全宽 -->
<div class="teachers-card-full">
<div class="card-header">
<span class="card-icon"><TrophyOutlined /></span>
<h3>教师活跃度排行</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="activeTeachers.length === 0" class="empty-state">
<span class="empty-icon"><TeamOutlined /></span>
<p>暂无数据</p>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="recentActivities.length === 0" class="empty-state">
<span class="empty-icon"><InboxOutlined /></span>
<p>暂无近期活动</p>
</div>
<div v-else class="activity-list">
<div
v-for="item in recentActivities"
:key="item.id"
class="activity-item"
>
<div class="activity-avatar">
<BookOutlined />
<div v-else class="teacher-list">
<div
v-for="(item, index) in activeTeachers"
:key="item.teacherId"
class="teacher-item"
>
<div class="rank-badge" :class="'rank-' + (index + 1)">
{{ index + 1 }}
</div>
<div class="teacher-info">
<div class="teacher-name-row">
<span class="teacher-name">{{ item.teacherName }}</span>
<a-tag :color="getActivityLevelColor(item.activityLevelCode)" size="small">
{{ item.activityLevelDesc }}
</a-tag>
</div>
<div class="activity-content">
<div class="activity-title">{{ item.title }}</div>
<div class="activity-time">{{ formatTime(item.time) }}</div>
<div class="teacher-details">
<span class="detail-item">
<HomeOutlined />
{{ item.classNames || '未分配班级' }}
</span>
<span class="detail-item">
<ReadOutlined />
授课 {{ item.lessonCount }}
</span>
<span class="detail-item">
<BookOutlined />
课程 {{ item.courseCount }}
</span>
<span class="detail-item" v-if="item.lastActiveAt">
<ClockCircleOutlined />
{{ formatLastActive(item.lastActiveAt) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 教师活跃度排行 -->
<div class="content-card teachers-card">
<div class="card-header">
<span class="card-icon"><TrophyOutlined /></span>
<h3>教师活跃度排行</h3>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin v-if="loading" />
<div v-else-if="activeTeachers.length === 0" class="empty-state">
<span class="empty-icon"><TeamOutlined /></span>
<p>暂无数据</p>
</div>
<div v-else class="teacher-list">
<div
v-for="(item, index) in activeTeachers"
:key="item.id"
class="teacher-item"
>
<div class="rank-badge" :class="'rank-' + (index + 1)">
{{ index + 1 }}
</div>
<div class="teacher-info">
<div class="teacher-name">{{ item.name }}</div>
<div class="teacher-lessons">
<span class="lesson-icon"><ReadOutlined /></span>
授课 {{ item.lessonCount }}
</div>
</div>
<div class="teacher-medal">
<component :is="getMedalIcon(index)" :style="getMedalStyle(index)" />
</div>
<div class="teacher-medal">
<component :is="getMedalIcon(index)" :style="getMedalStyle(index)" />
</div>
</div>
</div>
@ -225,21 +211,19 @@ import {
TeamOutlined,
UserOutlined,
ReadOutlined,
CalendarOutlined,
InboxOutlined,
TrophyOutlined,
TrophyFilled,
StarFilled,
BarChartOutlined,
LineChartOutlined,
DownloadOutlined,
ClockCircleOutlined,
} from '@ant-design/icons-vue';
import * as echarts from 'echarts';
import { message } from 'ant-design-vue';
import {
getSchoolStats,
getActiveTeachers,
getRecentActivities,
getCourseUsageStats,
getLessonTrend,
getCourseDistribution,
@ -249,7 +233,7 @@ import {
} from '@/api/school';
import type { SchoolStats, LessonTrendItem, CourseDistributionItem } from '@/api/school';
import type { Component } from 'vue';
import { Dayjs } from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
const loading = ref(false);
const courseStatsLoading = ref(false);
@ -269,8 +253,16 @@ const stats = ref<SchoolStats>({
lessonCount: 0,
});
const recentActivities = ref<Array<{ id: number; type: string; title: string; time: string }>>([]);
const activeTeachers = ref<Array<{ id: number; name: string; lessonCount: number }>>([]);
const activeTeachers = ref<Array<{
teacherId: number | string;
teacherName: string;
classNames: string;
lessonCount: number;
courseCount: number;
lastActiveAt?: string;
activityLevelCode: string;
activityLevelDesc: string;
}>>([]);
const courseStats = ref<Array<{ courseId: number; courseName: string; usageCount: number }>>([]);
const dateRange = ref<[Dayjs, Dayjs] | undefined>(undefined);
@ -331,6 +323,42 @@ const getMedalIcon = (index: number): Component => {
return icons[index] || StarOutlined;
};
/**
* 根据活跃度等级获取标签颜色
*/
const getActivityLevelColor = (code: string) => {
const colors: Record<string, string> = {
HIGH: 'red',
MEDIUM: 'orange',
LOW: 'blue',
INACTIVE: 'default',
};
return colors[code] || 'default';
};
/**
* 格式化最后活跃时间
*/
const formatLastActive = (time: string) => {
if (!time) return '';
const date = new Date(time);
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today.getTime() - 86400000);
const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate());
if (dateOnly.getTime() >= today.getTime()) {
return `今天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
} else if (dateOnly.getTime() >= yesterday.getTime()) {
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;
} else if (now.getTime() - date.getTime() < 7 * 86400000) {
const days = Math.floor((now.getTime() - date.getTime()) / 86400000);
return `${days} 天前`;
} else {
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
}
};
const getMedalStyle = (index: number) => {
const styles = [
{ color: '#FFD700', fontSize: '20px' },
@ -394,7 +422,7 @@ const initTrendChart = (data: LessonTrendItem[]) => {
//
const validData = data.map(d => ({
month: d.month || '',
date: d.date || '',
lessonCount: d.lessonCount || 0,
studentCount: d.studentCount || 0,
}));
@ -426,7 +454,7 @@ const initTrendChart = (data: LessonTrendItem[]) => {
},
xAxis: {
type: 'category',
data: validData.map((d) => d.month),
data: validData.map((d) => d.date),
axisLine: {
lineStyle: {
color: '#E5E7EB',
@ -515,27 +543,39 @@ const initDistributionChart = (data: CourseDistributionItem[]) => {
}
//
const validData = data.map((item, index) => ({
name: item.name || `课程${index + 1}`,
value: item.value || 0,
itemStyle: {
color: [
'#FF8C42',
'#667eea',
'#f093fb',
'#4facfe',
'#43e97b',
'#fa709a',
'#fee140',
'#30cfd0',
][index % 8],
},
}));
const validData = data.map((item, index) => {
// 10
const displayName = item.name && item.name.length > 10
? item.name.substring(0, 10) + '...'
: (item.name || `课程${index + 1}`);
return {
name: displayName,
value: item.value || 0,
fullName: item.name, // tooltip
itemStyle: {
color: [
'#FF8C42',
'#667eea',
'#f093fb',
'#4facfe',
'#43e97b',
'#fa709a',
'#fee140',
'#30cfd0',
][index % 8],
},
};
});
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)',
formatter: (params: any) => {
// 使 tooltip
const fullName = params.data.fullName || params.name;
return `${fullName}: ${params.value}次 (${params.percent}%)`;
},
},
legend: {
orient: 'vertical',
@ -584,15 +624,13 @@ const handleResize = () => {
const loadData = async () => {
loading.value = true;
try {
const [statsData, teachersData, activitiesData] = await Promise.all([
const [statsData, teachersData] = await Promise.all([
getSchoolStats(),
getActiveTeachers(5),
getRecentActivities(10),
getActiveTeachers(10), // TOP10
]);
stats.value = statsData;
activeTeachers.value = teachersData;
recentActivities.value = activitiesData;
} catch (error) {
console.error('Failed to load dashboard data:', error);
} finally {
@ -603,7 +641,10 @@ const loadData = async () => {
const loadCourseStats = async () => {
courseStatsLoading.value = true;
try {
const data = await getCourseUsageStats();
const startDate = dateRange.value?.[0]?.format('YYYY-MM-DD');
const endDate = dateRange.value?.[1]?.format('YYYY-MM-DD');
const data = await getCourseUsageStats(startDate, endDate);
courseStats.value = data.slice(0, 10);
} catch (error) {
console.error('Failed to load course stats:', error);
@ -616,7 +657,7 @@ const loadCourseStats = async () => {
const loadTrendData = async () => {
trendLoading.value = true;
try {
const data = await getLessonTrend(6);
const data = await getLessonTrend(7);
lessonTrendData.value = data;
} catch (error) {
console.error('Failed to load trend data:', error);
@ -685,6 +726,12 @@ onMounted(() => {
loadTrendData();
loadDistributionData();
//
const now = dayjs();
const monthStart = now.startOf('month');
const monthEnd = now.endOf('month');
dateRange.value = [monthStart, monthEnd];
window.addEventListener('resize', handleResize);
});
@ -808,11 +855,20 @@ onUnmounted(() => {
/* 内容网格 */
.content-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: 1fr;
gap: 24px;
margin-bottom: 24px;
}
/* 教师活跃度排行(全宽) */
.teachers-card-full {
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
}
/* 图表网格 */
.charts-grid {
display: grid;
@ -905,50 +961,6 @@ onUnmounted(() => {
font-size: 14px;
}
/* 活动列表 */
.activity-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #FAFAFA;
border-radius: 12px;
transition: all 0.2s ease;
}
.activity-item:hover {
background: #FFF8F0;
}
.activity-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
background: linear-gradient(135deg, #FF8C42 0%, #FFB347 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.activity-title {
font-size: 14px;
font-weight: 500;
color: #2D3436;
}
.activity-time {
font-size: 12px;
color: #B2BEC3;
margin-top: 4px;
}
/* 教师列表 */
.teacher-list {
display: flex;
@ -991,26 +1003,37 @@ onUnmounted(() => {
flex: 1;
}
.teacher-name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.teacher-name {
font-size: 14px;
font-weight: 500;
font-weight: 600;
color: #2D3436;
}
.teacher-lessons {
font-size: 12px;
color: #636E72;
margin-top: 4px;
.teacher-details {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.detail-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #636E72;
}
.lesson-icon {
font-size: 14px;
color: #636E72;
display: flex;
align-items: center;
.detail-item .anticon {
font-size: 12px;
color: #999;
}
.teacher-medal {

View File

@ -331,7 +331,7 @@
</div>
<div class="detail-stat-item">
<div class="stat-number">{{ selectedCourse.studentCount }}</div>
<div class="stat-label">覆盖学生</div>
<div class="stat-label">学生总数</div>
</div>
</div>
<a-divider />

View File

@ -91,6 +91,12 @@
</div>
<span>课程使用</span>
</div>
<a-segmented
v-model:value="usagePeriodType"
:options="periodOptions"
@change="loadUsageData"
size="small"
/>
</div>
<div class="card-body" :class="{ 'is-loading': usageLoading }">
<a-spin v-if="usageLoading" />
@ -131,8 +137,8 @@
:class="{ 'finished': lesson.status === 'FINISHED' }"
>
<div class="lesson-time">
<div class="time-value">{{ formatTime(lesson.plannedDatetime) }}</div>
<div class="time-duration">{{ lesson.duration }}分钟</div>
<div class="time-value">{{ formatTime(lesson.startTime || lesson.lessonDate) }}</div>
<div class="time-duration">30 分钟</div>
</div>
<div class="lesson-info">
<div class="lesson-name">{{ lesson.courseName }}</div>
@ -193,23 +199,23 @@
>
<div class="recommend-cover">
<img
v-if="course.coverImagePath"
:src="getImageUrl(course.coverImagePath)"
v-if="(course as any).coverImagePath || course.description"
:src="getImageUrl((course as any).coverImagePath || course.description || '')"
class="cover-img"
/>
<div v-else class="cover-placeholder">
<BookFilled />
</div>
<div class="duration-tag">{{ course.duration || 30 }}分钟</div>
<div class="duration-tag">{{ course.courseCount || 30 }}分钟</div>
</div>
<div class="recommend-info">
<div class="recommend-name">{{ course.name }}</div>
<div class="recommend-meta">
<span class="meta-item">
<FireOutlined /> {{ course.usageCount }}次使用
<FireOutlined /> {{ course.usageCount || 0 }}次使用
</span>
<span v-if="course.avgRating > 0" class="meta-item">
<StarFilled class="star-icon" /> {{ course.avgRating.toFixed(1) }}
<span v-if="course.gradeLevel" class="meta-item">
<StarFilled class="star-icon" /> {{ course.gradeLevel }}
</span>
</div>
</div>
@ -219,46 +225,6 @@
</div>
</div>
</div>
<!-- 近期活动 -->
<div class="activity-section">
<div class="content-card activity-card">
<div class="card-header">
<div class="card-title">
<div class="title-icon-wrapper list">
<UnorderedListOutlined />
</div>
<span>近期活动</span>
</div>
</div>
<div class="card-body" :class="{ 'is-loading': loading }">
<a-spin :spinning="loading">
<div v-if="recentActivities.length === 0" class="empty-state-horizontal">
<div class="empty-icon-wrapper small">
<FileTextOutlined />
</div>
<p class="empty-text">暂无近期活动</p>
</div>
<div v-else class="activity-timeline">
<div
v-for="(item, index) in recentActivities"
:key="item.id"
class="activity-item"
:class="'type-' + item.type"
>
<div class="activity-dot">
<component :is="getActivityIcon(item.type)" />
</div>
<div class="activity-content">
<div class="activity-text">{{ item.description }}</div>
<div class="activity-time">{{ item.time }}</div>
</div>
</div>
</div>
</a-spin>
</div>
</div>
</div>
</div>
</template>
@ -284,11 +250,6 @@ import {
FireOutlined,
InboxOutlined,
RightOutlined,
UnorderedListOutlined,
FileTextOutlined,
MessageOutlined,
UserOutlined,
FolderOutlined,
ClockCircleOutlined,
LineChartOutlined,
PieChartOutlined,
@ -296,9 +257,14 @@ import {
import {
getTeacherDashboard,
getTeacherLessonTrend,
getTeacherCourseUsage,
getTeacherCourseUsageStats,
type DashboardData,
type TeacherLessonItem,
type TeacherCoursePackageItem,
type TeacherLessonTrendItem,
type TeacherCourseUsageItem,
type CourseUsageStatsItem,
} from '@/api/teacher';
import type { TeacherLessonTrendItem, TeacherCourseUsageItem } from '@/api/teacher';
const router = useRouter();
@ -314,15 +280,25 @@ let usageChart: echarts.ECharts | null = null;
// Chart data
const lessonTrendData = ref<TeacherLessonTrendItem[]>([]);
const courseUsageData = ref<TeacherCourseUsageItem[]>([]);
const stats = ref({
const stats = ref<DashboardData['stats']>({
classCount: 0,
studentCount: 0,
lessonCount: 0,
courseCount: 0,
});
// 使
const usagePeriodType = ref<'TODAY' | 'WEEK' | 'MONTH' | 'CUSTOM'>('MONTH');
const periodOptions = [
{ label: '今日', value: 'TODAY' },
{ label: '本周', value: 'WEEK' },
{ label: '本月', value: 'MONTH' },
];
// 使
const courseUsageStatsData = ref<CourseUsageStatsItem[]>([]);
const currentDate = computed(() => {
const now = new Date();
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
@ -332,39 +308,8 @@ const currentDate = computed(() => {
return `${month}${date}${day}`;
});
interface TodayLesson {
id: number;
courseId: number;
courseName: string;
pictureBookName?: string;
classId: number;
className: string;
plannedDatetime: string;
status: string;
duration: number;
}
interface RecommendedCourse {
id: number;
name: string;
pictureBookName?: string;
coverImagePath?: string;
duration: number;
usageCount: number;
avgRating: number;
gradeTags: string[];
}
interface RecentActivity {
id: number;
type: string;
description: string;
time: string;
}
const todayLessons = ref<TodayLesson[]>([]);
const recommendedCourses = ref<RecommendedCourse[]>([]);
const recentActivities = ref<RecentActivity[]>([]);
const todayLessons = ref<TeacherLessonItem[]>([]);
const recommendedCourses = ref<TeacherCoursePackageItem[]>([]);
const getImageUrl = (path: string) => {
if (!path) return '';
@ -379,36 +324,6 @@ const formatTime = (datetime: string) => {
return `${hours}:${minutes}`;
};
const formatActivityTime = (time: string) => {
const date = new Date(time);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString();
}
};
const getActivityIcon = (type: string) => {
const iconMap: Record<string, any> = {
lesson: ReadOutlined,
feedback: MessageOutlined,
student: UserOutlined,
course: FolderOutlined,
};
return iconMap[type] || FileTextOutlined;
};
//
const initTrendChart = (data: TeacherLessonTrendItem[]) => {
if (!trendChartRef.value) return;
@ -526,8 +441,8 @@ const initTrendChart = (data: TeacherLessonTrendItem[]) => {
trendChart.setOption(option);
};
// 使
const initUsageChart = (data: TeacherCourseUsageItem[]) => {
// 使
const initUsageChart = (data: TeacherCourseUsageItem[] | CourseUsageStatsItem[]) => {
if (!usageChartRef.value) return;
if (usageChart) {
@ -536,10 +451,45 @@ const initUsageChart = (data: TeacherCourseUsageItem[]) => {
usageChart = echarts.init(usageChartRef.value);
//
const chartData = data.map(item => {
if ('coursePackageName' in item) {
// CourseUsageStatsItem
return {
name: (item as CourseUsageStatsItem).coursePackageName,
value: (item as CourseUsageStatsItem).usageCount,
studentCount: (item as CourseUsageStatsItem).studentCount,
avgDuration: (item as CourseUsageStatsItem).avgDuration,
lastUsedAt: (item as CourseUsageStatsItem).lastUsedAt,
};
} else {
// TeacherCourseUsageItem
return {
name: (item as TeacherCourseUsageItem).name,
value: (item as TeacherCourseUsageItem).value,
};
}
});
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}次 ({d}%)',
formatter: (params: any) => {
const data = params.data;
let tooltip = `${data.name}: ${data.value}次 (${params.percent}%)`;
if (data.studentCount !== undefined) {
tooltip += `\n参与学生${data.studentCount}`;
}
if (data.avgDuration !== undefined) {
tooltip += `\n平均时长${data.avgDuration}分钟`;
}
if (data.lastUsedAt) {
const lastUsed = new Date(data.lastUsedAt);
const lastUsedStr = `${lastUsed.getMonth() + 1}${lastUsed.getDate()}${lastUsed.getHours().toString().padStart(2, '0')}:${lastUsed.getMinutes().toString().padStart(2, '0')}`;
tooltip += `\n最后使用${lastUsedStr}`;
}
return tooltip;
},
},
legend: {
orient: 'vertical',
@ -571,7 +521,7 @@ const initUsageChart = (data: TeacherCourseUsageItem[]) => {
labelLine: {
show: false,
},
data: data.map((item, index) => ({
data: chartData.map((item, index) => ({
...item,
itemStyle: {
color: [
@ -609,22 +559,20 @@ const loadTrendData = async () => {
}
};
// 使
// 使
const loadUsageData = async () => {
usageLoading.value = true;
try {
const data = await getTeacherCourseUsage();
courseUsageData.value = data;
const data = await getTeacherCourseUsageStats({
periodType: usagePeriodType.value
});
courseUsageStatsData.value = data;
initUsageChart(courseUsageStatsData.value);
} catch (error) {
console.error('Failed to load usage data:', error);
} finally {
usageLoading.value = false;
}
// loading
await nextTick();
if (courseUsageData.value.length > 0) {
initUsageChart(courseUsageData.value);
}
};
//
@ -639,36 +587,8 @@ const loadDashboard = async () => {
const data = await getTeacherDashboard();
stats.value = data.stats || { classCount: 0, studentCount: 0, lessonCount: 0, courseCount: 0 };
//
todayLessons.value = (data.todayLessons || []).map((lesson: any) => ({
id: lesson.id,
courseId: lesson.courseId,
courseName: lesson.courseName || lesson.course?.name || '未命名课程',
pictureBookName: lesson.pictureBookName,
classId: lesson.classId,
className: lesson.className || lesson.class?.name || '未命名班级',
plannedDatetime: lesson.plannedDatetime || lesson.startDatetime || lesson.lessonDate,
status: lesson.status,
duration: lesson.duration || lesson.actualDuration || 30, // 30
}));
//
recommendedCourses.value = (data.recommendedCourses || []).map((course: any) => ({
id: course.id,
name: course.name || '未命名课程',
pictureBookName: course.pictureBookName,
coverImagePath: course.coverImagePath,
duration: course.duration || 30, // 30
usageCount: course.usageCount || 0,
avgRating: course.avgRating || 0, // 0
gradeTags: course.gradeTags || [],
}));
//
recentActivities.value = (data.recentActivities || []).map((item: RecentActivity) => ({
...item,
time: formatActivityTime(item.time),
}));
todayLessons.value = data.todayLessons || [];
recommendedCourses.value = data.recommendedCourses || [];
} catch (error: any) {
console.error('Failed to load dashboard:', error);
message.error(error?.response?.data?.message || '加载数据失败');
@ -677,11 +597,11 @@ const loadDashboard = async () => {
}
};
const startLesson = (lesson: TodayLesson) => {
const startLesson = (lesson: TeacherLessonItem) => {
router.push(`/teacher/lessons/${lesson.id}`);
};
const viewCourse = (course: RecommendedCourse) => {
const viewCourse = (course: TeacherCoursePackageItem) => {
router.push(`/teacher/courses/${course.id}`);
};
@ -1244,75 +1164,6 @@ onUnmounted(() => {
font-size: 11px;
}
/* 活动区域 */
.activity-section {
margin-bottom: 24px;
}
.activity-card .card-body {
min-height: auto;
}
.activity-timeline {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
gap: 16px;
align-items: flex-start;
}
.activity-dot {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 14px;
}
.activity-item.type-lesson .activity-dot {
background: linear-gradient(135deg, #FFE4C9 0%, #FFF0E0 100%);
color: #FF8C42;
}
.activity-item.type-feedback .activity-dot {
background: linear-gradient(135deg, #D6E4FF 0%, #E8F0FF 100%);
color: #1890ff;
}
.activity-item.type-student .activity-dot {
background: linear-gradient(135deg, #D9F7BE 0%, #E8FFD4 100%);
color: #52c41a;
}
.activity-item.type-course .activity-dot {
background: linear-gradient(135deg, #FFE7BA 0%, #FFF1D6 100%);
color: #FA8C16;
}
.activity-content {
flex: 1;
padding-top: 6px;
}
.activity-text {
font-size: 14px;
color: #333;
line-height: 1.5;
}
.activity-time {
font-size: 12px;
color: #999;
margin-top: 4px;
}
/* 响应式 */
@media (max-width: 1200px) {
.stats-grid {

View File

@ -27,8 +27,8 @@ test.describe('数据看板', () => {
const lessonCard = page.getByText(/月授课|授课次数/).first();
await expect(lessonCard).toBeVisible();
// 验证覆盖学生卡片
const studentCard = page.getByText(/覆盖学生|学生数/).first();
// 验证学生总数卡片
const studentCard = page.getByText(/学生总数|学生数/).first();
await expect(studentCard).toBeVisible();
});

View File

@ -0,0 +1,74 @@
package com.reading.platform.common.enums;
import lombok.Getter;
/**
* 教师活跃度等级枚举
* 根据统计周期内的授课次数划分活跃程度
*/
@Getter
public enum TeacherActivityLevel {
/**
* 高频活跃统计周期内授课次数 >= 10
*/
HIGH("HIGH", "高频", 10),
/**
* 中频活跃统计周期内授课次数 5-9
*/
MEDIUM("MEDIUM", "中频", 5),
/**
* 低频活跃统计周期内授课次数 1-4
*/
LOW("LOW", "低频", 1),
/**
* 未活跃统计周期内授课次数为 0
*/
INACTIVE("INACTIVE", "未活跃", 0);
private final String code;
private final String description;
private final int minLessonCount;
TeacherActivityLevel(String code, String description, int minLessonCount) {
this.code = code;
this.description = description;
this.minLessonCount = minLessonCount;
}
/**
* 根据授课次数获取活跃度等级
*
* @param lessonCount 授课次数
* @return 活跃度等级
*/
public static TeacherActivityLevel fromLessonCount(int lessonCount) {
if (lessonCount >= HIGH.minLessonCount) {
return HIGH;
} else if (lessonCount >= MEDIUM.minLessonCount) {
return MEDIUM;
} else if (lessonCount >= LOW.minLessonCount) {
return LOW;
} else {
return INACTIVE;
}
}
/**
* 根据编码获取活跃度等级
*
* @param code 编码
* @return 活跃度等级
*/
public static TeacherActivityLevel fromCode(String code) {
for (TeacherActivityLevel level : values()) {
if (level.getCode().equals(code)) {
return level;
}
}
return INACTIVE;
}
}

View File

@ -119,4 +119,25 @@ public class JwtTokenProvider {
.getSubject();
}
/**
* 获取 token 剩余过期时间
* @param token token 字符串
* @return 剩余秒数如果 token 无效则返回 0
*/
public long getRemainingExpiration(String token) {
try {
Date expiryDate = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
long remaining = (expiryDate.getTime() - System.currentTimeMillis()) / 1000;
return remaining > 0 ? remaining : 0;
} catch (Exception e) {
log.warn("获取 token 剩余过期时间失败:{}", e.getMessage());
return 0;
}
}
}

View File

@ -6,8 +6,10 @@ import com.reading.platform.common.mapper.TenantMapper;
import com.reading.platform.common.mapper.TeacherMapper;
import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.LoginRequest;
import com.reading.platform.dto.request.UpdateProfileRequest;
import com.reading.platform.dto.response.LoginResponse;
import com.reading.platform.dto.response.TokenResponse;
import com.reading.platform.dto.response.UpdateProfileResponse;
import com.reading.platform.dto.response.UserInfoResponse;
import com.reading.platform.entity.AdminUser;
import com.reading.platform.entity.Parent;
@ -16,8 +18,10 @@ import com.reading.platform.entity.Teacher;
import com.reading.platform.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@Tag(name = "认证管理", description = "Authentication APIs")
@ -63,11 +67,21 @@ public class AuthController {
@PostMapping("/change-password")
public Result<Void> changePassword(
@RequestParam String oldPassword,
@RequestParam String newPassword) {
authService.changePassword(oldPassword, newPassword);
@RequestParam String newPassword,
HttpServletRequest request) {
String token = resolveToken(request);
authService.changePassword(oldPassword, newPassword, token);
return Result.success();
}
@Operation(summary = "修改个人信息")
@PutMapping("/profile")
public Result<UpdateProfileResponse> updateProfile(
@Valid @RequestBody UpdateProfileRequest request) {
UpdateProfileResponse response = authService.updateProfile(request);
return Result.success(response);
}
private UserInfoResponse convertToUserInfoResponse(Object userInfo) {
if (userInfo instanceof Tenant) {
Tenant tenant = (Tenant) userInfo;
@ -121,4 +135,15 @@ public class AuthController {
return null;
}
/**
* 从请求头获取 token
*/
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

View File

@ -5,10 +5,8 @@ import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.ActiveTenantsQueryRequest;
import com.reading.platform.dto.request.PopularCoursesQueryRequest;
import com.reading.platform.dto.request.RecentActivitiesQueryRequest;
import com.reading.platform.dto.response.ActiveTenantItemResponse;
import com.reading.platform.dto.response.PopularCourseItemResponse;
import com.reading.platform.dto.response.RecentActivityItemResponse;
import com.reading.platform.dto.response.StatsResponse;
import com.reading.platform.dto.response.StatsTrendResponse;
import com.reading.platform.service.StatsService;
@ -58,10 +56,4 @@ public class AdminStatsController {
return Result.success(statsService.getPopularCourses(request));
}
@GetMapping("/activities")
@Operation(summary = "获取最近活动")
public Result<List<RecentActivityItemResponse>> getRecentActivities(@ModelAttribute RecentActivitiesQueryRequest request) {
return Result.success(statsService.getRecentActivities(request));
}
}

View File

@ -9,6 +9,7 @@ import com.reading.platform.dto.response.LessonTagResponse;
import com.reading.platform.dto.response.SchoolCourseResponse;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.mapper.LessonMapper;
import com.reading.platform.service.CourseLessonService;
import com.reading.platform.service.CoursePackageService;
import io.swagger.v3.oas.annotations.Operation;
@ -19,6 +20,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@ -34,6 +36,7 @@ public class SchoolCourseController {
private final CoursePackageService courseService;
private final CourseLessonService courseLessonService;
private final LessonMapper lessonMapper;
@GetMapping
@Operation(summary = "获取学校课程包列表(分页)")
@ -56,6 +59,25 @@ public class SchoolCourseController {
.map(SchoolCourseResponse::toSchoolCourseResponse)
.collect(Collectors.toList());
// 按租户 ID 统计每个课程的使用次数已完成状态的 Lesson 数量
if (!list.isEmpty()) {
List<Long> courseIds = list.stream().map(SchoolCourseResponse::getId).collect(Collectors.toList());
List<Map<String, Object>> usageStats = lessonMapper.countUsageByTenantAndCourseIds(tenantId, courseIds);
// 构建课程 ID -> 使用次数的映射
Map<Long, Integer> usageCountMap = usageStats.stream()
.collect(Collectors.toMap(
m -> ((Number) m.get("course_id")).longValue(),
m -> ((Number) m.get("usageCount")).intValue(),
(v1, v2) -> v1 // 如果有重复取第一个值
));
// 填充 usageCount如果没有统计数据显示为 0
for (SchoolCourseResponse vo : list) {
vo.setUsageCount(usageCountMap.getOrDefault(vo.getId(), 0));
}
}
// 填充 lessonTags
for (SchoolCourseResponse vo : list) {
List<CourseLesson> lessons = courseLessonService.findByCourseId(vo.getId());

View File

@ -2,12 +2,15 @@ package com.reading.platform.controller.school;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.response.TeacherActivityRankResponse;
import com.reading.platform.service.SchoolStatsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@ -31,33 +34,27 @@ public class SchoolStatsController {
@GetMapping("/teachers")
@Operation(summary = "获取活跃教师排行")
public Result<List<Map<String, Object>>> getActiveTeachers(
@RequestParam(defaultValue = "5") int limit) {
public Result<List<TeacherActivityRankResponse>> getActiveTeachers(
@RequestParam(defaultValue = "10") int limit) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getActiveTeachers(tenantId, limit));
}
@GetMapping("/courses")
@Operation(summary = "获取课程使用统计")
public Result<List<Map<String, Object>>> getCourseUsageStats() {
public Result<List<Map<String, Object>>> getCourseUsageStats(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getCourseUsageStats(tenantId));
}
@GetMapping("/activities")
@Operation(summary = "获取近期活动")
public Result<List<Map<String, Object>>> getRecentActivities(
@RequestParam(defaultValue = "10") int limit) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getRecentActivities(tenantId, limit));
return Result.success(schoolStatsService.getCourseUsageStats(tenantId, startDate, endDate));
}
@GetMapping("/lesson-trend")
@Operation(summary = "获取授课趋势")
public Result<List<Map<String, Object>>> getLessonTrend(
@RequestParam(defaultValue = "6") int months) {
@RequestParam(defaultValue = "7") int days) {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(schoolStatsService.getLessonTrend(tenantId, months));
return Result.success(schoolStatsService.getLessonTrend(tenantId, days));
}
@GetMapping("/course-distribution")

View File

@ -2,16 +2,16 @@ package com.reading.platform.controller.teacher;
import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.entity.Lesson;
import com.reading.platform.dto.request.CourseUsageQuery;
import com.reading.platform.dto.response.*;
import com.reading.platform.service.TeacherStatsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 统计数据控制器教师端
@ -26,7 +26,7 @@ public class TeacherStatsController {
@GetMapping("/dashboard")
@Operation(summary = "获取教师端首页统计数据")
public Result<Map<String, Object>> getDashboard() {
public Result<TeacherDashboardResponse> getDashboard() {
Long teacherId = SecurityUtils.getCurrentUserId();
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(teacherStatsService.getDashboard(teacherId, tenantId));
@ -34,36 +34,56 @@ public class TeacherStatsController {
@GetMapping("/today-lessons")
@Operation(summary = "获取今日课程")
public Result<List<Lesson>> getTodayLessons() {
public Result<List<TeacherLessonVO>> getTodayLessons() {
Long teacherId = SecurityUtils.getCurrentUserId();
return Result.success(teacherStatsService.getTodayLessons(teacherId));
}
@GetMapping("/recommended-courses")
@Operation(summary = "获取推荐课程")
public Result<List<CoursePackage>> getRecommendedCourses() {
public Result<List<CoursePackageVO>> getRecommendedCourses() {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(teacherStatsService.getRecommendedCourses(tenantId));
}
@GetMapping("/weekly-stats")
@Operation(summary = "获取本周统计")
public Result<Map<String, Object>> getWeeklyStats() {
public Result<TeacherWeeklyStatsResponse> getWeeklyStats() {
Long teacherId = SecurityUtils.getCurrentUserId();
return Result.success(teacherStatsService.getWeeklyStats(teacherId));
}
@GetMapping("/lesson-trend")
@Operation(summary = "获取授课趋势")
public Result<List<Map<String, Object>>> getLessonTrend(
public Result<List<TeacherLessonTrendVO>> getLessonTrend(
@RequestParam(defaultValue = "6") int months) {
Long teacherId = SecurityUtils.getCurrentUserId();
return Result.success(teacherStatsService.getLessonTrend(teacherId, months));
}
@GetMapping("/course-usage-stats")
@Operation(summary = "获取课程使用统计(增强版)")
public Result<List<CourseUsageStatsVO>> getCourseUsageStats(
@RequestParam(required = false) String periodType,
@RequestParam(required = false) LocalDate startDate,
@RequestParam(required = false) LocalDate endDate) {
Long teacherId = SecurityUtils.getCurrentUserId();
Long tenantId = SecurityUtils.getCurrentTenantId();
CourseUsageQuery query = CourseUsageQuery.builder()
.periodType(periodType)
.startDate(startDate)
.endDate(endDate)
.build();
return Result.success(teacherStatsService.getCourseUsageStats(tenantId, teacherId, query));
}
@GetMapping("/course-usage")
@Operation(summary = "获取课程使用统计")
public Result<List<Map<String, Object>>> getCourseUsage() {
@Operation(summary = "获取课程使用统计(旧版)")
@Deprecated
public Result<List<CourseUsageVO>> getCourseUsage() {
Long tenantId = SecurityUtils.getCurrentTenantId();
return Result.success(teacherStatsService.getCourseUsage(tenantId));
}

View File

@ -0,0 +1,29 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDate;
/**
* 课程使用统计查询参数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "课程使用统计查询参数")
public class CourseUsageQuery {
@Schema(description = "统计周期类型TODAY-今日WEEK-本周MONTH-本月CUSTOM-自定义")
private String periodType;
@Schema(description = "自定义周期开始日期")
private LocalDate startDate;
@Schema(description = "自定义周期结束日期")
private LocalDate endDate;
}

View File

@ -1,15 +0,0 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 最近活动查询请求
*/
@Data
@Schema(description = "最近活动查询请求")
public class RecentActivitiesQueryRequest {
@Schema(description = "返回数量限制", example = "10")
private Integer limit = 10;
}

View File

@ -0,0 +1,26 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 修改个人信息请求 DTO
*/
@Data
@Schema(description = "修改个人信息请求")
public class UpdateProfileRequest {
@Schema(description = "姓名", example = "张三")
@Pattern(regexp = "^[\u4e00-\u9fa5a-zA-Z\\s]{2,20}$", message = "姓名长度为 2-20 位,只能包含中文或英文")
private String name;
@Schema(description = "手机号", example = "13800138000")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Schema(description = "邮箱", example = "zhangsan@example.com")
@Email(message = "邮箱格式不正确")
private String email;
}

View File

@ -22,9 +22,9 @@ public class ActiveTenantItemResponse {
@Schema(description = "租户名称")
private String tenantName;
@Schema(description = "活跃用户数")
private Integer activeUsers;
@Schema(description = "活跃教师数(近 30 天有完成课程的老师数)")
private Integer activeTeacherCount;
@Schema(description = "课程使用数")
private Integer courseCount;
@Schema(description = "完成课次数(近 30 天 COMPLETED 状态的 lesson 总数)")
private Integer completedLessonCount;
}

View File

@ -0,0 +1,48 @@
package com.reading.platform.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 课程包视图对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "课程包视图对象")
public class CoursePackageVO {
@Schema(description = "ID")
private Long id;
@Schema(description = "名称")
private String name;
@Schema(description = "描述")
private String description;
@Schema(description = "适用年级")
private String gradeLevel;
@Schema(description = "课程数量")
private Integer courseCount;
@Schema(description = "状态")
private String status;
@Schema(description = "使用次数")
private Integer usageCount;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,38 @@
package com.reading.platform.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
/**
* 课程使用统计响应对象增强版
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "课程使用统计响应对象")
public class CourseUsageStatsVO {
@Schema(description = "课程包 ID")
private Long coursePackageId;
@Schema(description = "课程包名称")
private String coursePackageName;
@Schema(description = "使用次数")
private Integer usageCount;
@Schema(description = "参与学生数(去重)")
private Integer studentCount;
@Schema(description = "平均时长(分钟)")
private Integer avgDuration;
@Schema(description = "最后使用时间")
private LocalDateTime lastUsedAt;
}

View File

@ -0,0 +1,24 @@
package com.reading.platform.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* 课程使用统计视图对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "课程使用统计视图对象")
public class CourseUsageVO {
@Schema(description = "课程包名称")
private String name;
@Schema(description = "使用次数")
private Integer value;
}

View File

@ -1,38 +0,0 @@
package com.reading.platform.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
/**
* 最近活动项响应
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "最近活动项响应")
public class RecentActivityItemResponse {
@Schema(description = "活动 ID")
private Long activityId;
@Schema(description = "活动类型")
private String activityType;
@Schema(description = "活动描述")
private String description;
@Schema(description = "操作人 ID")
private Long operatorId;
@Schema(description = "操作人名称")
private String operatorName;
@Schema(description = "操作时间")
private LocalDateTime operationTime;
}

View File

@ -33,4 +33,7 @@ public class StatsResponse {
@Schema(description = "课时总数")
private Long totalLessons;
@Schema(description = "月授课次数(本月 COMPLETED 状态的 lesson 总数)")
private Long monthlyLessons;
}

Some files were not shown because too many files have changed in this diff Show More