后端重构:
- 添加统一响应格式 ResultDto<T> 和 PageResultDto<T>
- 添加分页查询 DTO 基类 PageQueryDto
- 添加响应转换拦截器 TransformInterceptor
- 添加公共工具函数(JSON 解析、分页计算)
- 配置 Swagger/OpenAPI 文档(访问路径:/api-docs)
- Tenant 模块 DTO 规范化示例(添加 @ApiProperty 装饰器)
- CourseLesson 控制器重构 - 移除类级路径参数,修复 Orval 验证错误
- 后端 DTO 规范化 - 为 Course、Lesson、TeacherCourse、SchoolCourse 控制器添加完整的 Swagger 装饰器
前端重构:
- 配置 Orval 从后端 OpenAPI 自动生成 API 客户端
- 生成 API 客户端代码(带完整参数定义)
- 创建 API 客户端统一入口 (src/api/client.ts)
- 创建 API 适配层 (src/api/teacher.adapter.ts)
- 配置文件路由 (unplugin-vue-router)
- 课程模块迁移到新 API 客户端
- 修复 PrepareModeView.vue API 调用错误
- 教师模块迁移到新 API 客户端
- 修复 school-course.ts 类型错误
- 清理 teacher.adapter.ts 未使用导入
- 修复 client.ts API 客户端结构
- 创建文件路由目录结构
Bug 修复:
- 修复路由配置问题 - 移除 top-level await,改用手动路由配置
- 修复响应拦截器 - 正确解包 { code, message, data } 格式的响应
- 清理 teacher.adapter.ts 未使用导入
- 修复 client.ts API 客户端结构
- 创建文件路由目录结构
系统测试:
- 后端 API 测试通过 (7/7)
- 前端路由测试通过 (4/4)
- 数据库完整性验证通过
- Orval API 客户端验证通过
- 超管端功能测试通过 (97.8% 通过率)
新增文件:
- reading-platform-backend/src/common/dto/result.dto.ts
- reading-platform-backend/src/common/dto/page-query.dto.ts
- reading-platform-backend/src/common/interceptors/transform.interceptor.ts
- reading-platform-backend/src/common/utils/json.util.ts
- reading-platform-backend/src/common/utils/pagination.util.ts
- reading-platform-frontend/orval.config.ts
- reading-platform-frontend/src/api/generated/mutator.ts
- reading-platform-frontend/src/api/client.ts
- reading-platform-frontend/src/api/teacher.adapter.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
531 lines
18 KiB
TypeScript
531 lines
18 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
||
|
||
test.describe('备课模式完整流程', () => {
|
||
let baseURL = 'http://localhost:5173';
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto(baseURL);
|
||
await page.waitForLoadState('networkidle');
|
||
await page.click('text=教师');
|
||
await page.waitForTimeout(500);
|
||
|
||
const accountInput = page.locator('input[placeholder*="账号"]').or(page.locator('input[name="account"]'));
|
||
await accountInput.fill('teacher1');
|
||
|
||
const passwordInput = page.locator('input[placeholder*="密码"]').or(page.locator('input[type="password"]'));
|
||
await passwordInput.fill('123456');
|
||
|
||
const loginButton = page.locator('button[type="submit"]').or(page.locator('.login-btn')).or(page.locator('button:has-text("登录")'));
|
||
await loginButton.click();
|
||
|
||
await page.waitForURL('**/dashboard', { timeout: 10000 }).catch(() => {
|
||
return page.waitForURL('**/courses', { timeout: 5000 });
|
||
});
|
||
await page.waitForTimeout(1000);
|
||
});
|
||
|
||
test('测试1: 进入备课模式', async ({ page }) => {
|
||
// 1. 进入课程中心
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
// 2. 选择第一个课程
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 3. 点击开始备课
|
||
const prepareButton = page.locator('button:has-text("开始备课")');
|
||
await prepareButton.click();
|
||
|
||
// 4. 验证跳转
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 5. 验证备课模式结构
|
||
await expect(page.locator('aside, [class*="navigation"], [class*="sidebar"]').first()).toBeVisible();
|
||
await expect(page.locator('[class*="preview"], [class*="content"]').first()).toBeVisible();
|
||
|
||
test.info().annotations.push({
|
||
type: 'success',
|
||
description: '成功进入备课模式',
|
||
});
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-mode-layout.png' });
|
||
});
|
||
|
||
test('测试2: 左侧导航 - 课程包概览', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 点击课程包概览
|
||
const overviewNav = page.locator('text=课程包概览').or(page.locator('[class*="overview"]'));
|
||
if (await overviewNav.count() > 0) {
|
||
await overviewNav.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证基本信息展示
|
||
const basicInfo = page.locator('[class*="basic"], [class*="info"]');
|
||
if (await basicInfo.count() > 0) {
|
||
await expect(basicInfo.first()).toBeVisible();
|
||
}
|
||
|
||
// 3. 验证统计数据
|
||
const stats = page.locator('[class*="stat"], [class*="count"]');
|
||
const statCount = await stats.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `统计项数量: ${statCount}`,
|
||
});
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-overview.png' });
|
||
});
|
||
|
||
test('测试3: 左侧导航 - 包含课程', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 点击包含课程
|
||
const lessonsNav = page.locator('text=包含课程').or(page.locator('[class*="lessons"]'));
|
||
if (await lessonsNav.count() > 0) {
|
||
await lessonsNav.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证课程列表
|
||
const lessonList = page.locator('[class*="lesson"], [class*="course-item"]');
|
||
const lessonCount = await lessonList.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `课程数量: ${lessonCount}`,
|
||
});
|
||
|
||
expect(lessonCount).toBeGreaterThan(0);
|
||
|
||
// 3. 点击第一个课程
|
||
if (lessonCount > 0) {
|
||
await lessonList.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-lessons.png' });
|
||
});
|
||
|
||
test('测试4: 右侧内容预览 - 课程介绍', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 点击课程介绍Tab
|
||
const introTab = page.locator('text=课程介绍').or(page.locator('[class*="intro"]'));
|
||
if (await introTab.count() > 0) {
|
||
await introTab.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证介绍内容展示
|
||
const introContent = page.locator('[class*="intro"], [class*="content"]');
|
||
expect(await introContent.count()).toBeGreaterThan(0);
|
||
|
||
// 3. 验证8个介绍字段
|
||
const introFields = ['简介', '亮点', '目标', '安排', '重难点', '方法', '评价', '注意事项'];
|
||
let visibleFieldCount = 0;
|
||
|
||
for (const field of introFields) {
|
||
const fieldLocator = page.locator(`text=${field}`);
|
||
if (await fieldLocator.count() > 0) {
|
||
visibleFieldCount++;
|
||
}
|
||
}
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `介绍字段: ${visibleFieldCount}/${introFields.length}`,
|
||
});
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-intro-content.png', fullPage: true });
|
||
});
|
||
|
||
test('测试5: 右侧内容预览 - 排课参考', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 点击排课参考Tab
|
||
const scheduleTab = page.locator('text=排课参考').or(page.locator('[class*="schedule"]'));
|
||
if (await scheduleTab.count() > 0) {
|
||
await scheduleTab.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证排课表格或内容
|
||
const scheduleTable = page.locator('table').or(page.locator('[class*="table"]'));
|
||
const scheduleContent = page.locator('[class*="schedule-content"]');
|
||
|
||
const hasTable = await scheduleTable.count() > 0;
|
||
const hasContent = await scheduleContent.count() > 0;
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: hasTable ? '显示排课表格' : (hasContent ? '显示排课内容' : '未找到排课参考'),
|
||
});
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-schedule.png' });
|
||
});
|
||
|
||
test('测试6: 右侧内容预览 - 环创建设', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 点击环创建设Tab
|
||
const envTab = page.locator('text=环创建设').or(page.locator('[class*="environment"]'));
|
||
if (await envTab.count() > 0) {
|
||
await envTab.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证环创建设内容
|
||
const envContent = page.locator('text=主题环境').or(page.locator('text=区域活动')).or(page.locator('[class*="environment"]'));
|
||
const hasContent = await envContent.count() > 0;
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: hasContent ? '显示环创建设内容' : '未找到环创建设',
|
||
});
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-environment.png' });
|
||
});
|
||
|
||
test('测试7: 教学目标展示', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 选择导入课
|
||
const introLesson = page.locator('text=导入课').or(page.locator('[class*="introduction"]'));
|
||
if (await introLesson.count() > 0) {
|
||
await introLesson.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证教学目标展示
|
||
const objectives = page.locator('text=教学目标').or(page.locator('[class*="objective"]'));
|
||
if (await objectives.count() > 0) {
|
||
await expect(objectives.first()).toBeVisible();
|
||
}
|
||
|
||
// 3. 验证教学准备展示
|
||
const preparation = page.locator('text=教学准备').or(page.locator('[class*="preparation"]'));
|
||
if (await preparation.count() > 0) {
|
||
await expect(preparation.first()).toBeVisible();
|
||
}
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-objectives.png' });
|
||
});
|
||
|
||
test('测试8: 教学过程展示', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 选择导入课
|
||
const introLesson = page.locator('text=导入课').or(page.locator('[class*="introduction"]'));
|
||
if (await introLesson.count() > 0) {
|
||
await introLesson.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证教学过程展示
|
||
const process = page.locator('text=教学过程').or(page.locator('[class*="process"], [class*="steps"]'));
|
||
if (await process.count() > 0) {
|
||
await expect(process.first()).toBeVisible();
|
||
}
|
||
|
||
// 3. 验证环节列表
|
||
const steps = page.locator('[class*="step"], [class*="stage"]');
|
||
const stepCount = await steps.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `教学环节数量: ${stepCount}`,
|
||
});
|
||
|
||
// 4. 展开第一个环节查看详情
|
||
if (stepCount > 0) {
|
||
await steps.first().click();
|
||
await page.waitForTimeout(500);
|
||
}
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-process.png', fullPage: true });
|
||
});
|
||
|
||
test('测试9: 核心资源展示', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 选择导入课
|
||
const introLesson = page.locator('text=导入课').or(page.locator('[class*="introduction"]'));
|
||
if (await introLesson.count() > 0) {
|
||
await introLesson.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证核心资源展示
|
||
const resources = page.locator('text=核心资源').or(page.locator('[class*="resource"]'));
|
||
const resourceCount = await resources.count();
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: resourceCount > 0 ? `找到 ${resourceCount} 个资源区域` : '未找到核心资源区域',
|
||
});
|
||
|
||
// 3. 验证资源类型(视频、PPT、PDF等)
|
||
const resourceTypes = ['视频', 'PPT', 'PDF', '音频', '图片'];
|
||
const foundTypes: string[] = [];
|
||
|
||
for (const type of resourceTypes) {
|
||
const typeLocator = page.locator(`text=${type}`);
|
||
if (await typeLocator.count() > 0) {
|
||
foundTypes.push(type);
|
||
}
|
||
}
|
||
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `资源类型: ${foundTypes.join(', ') || '无'}`,
|
||
});
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-resources.png' });
|
||
});
|
||
|
||
test('测试10: 教学延伸和反思', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 选择导入课
|
||
const introLesson = page.locator('text=导入课').or(page.locator('[class*="introduction"]'));
|
||
if (await introLesson.count() > 0) {
|
||
await introLesson.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证教学延伸
|
||
const extension = page.locator('text=教学延伸').or(page.locator('[class*="extension"]'));
|
||
if (await extension.count() > 0) {
|
||
await expect(extension.first()).toBeVisible();
|
||
}
|
||
|
||
// 3. 验证教学反思
|
||
const reflection = page.locator('text=教学反思').or(page.locator('[class*="reflection"]'));
|
||
if (await reflection.count() > 0) {
|
||
await expect(reflection.first()).toBeVisible();
|
||
}
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-extension.png' });
|
||
});
|
||
|
||
test('测试11: 备课笔记功能', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 点击备课笔记Tab
|
||
const notesTab = page.locator('text=备课笔记').or(page.locator('[class*="notes"]'));
|
||
if (await notesTab.count() > 0) {
|
||
await notesTab.first().click();
|
||
await page.waitForTimeout(1000);
|
||
}
|
||
|
||
// 2. 验证笔记编辑器
|
||
const editor = page.locator('[contenteditable="true"], .ant-input, textarea');
|
||
const hasEditor = await editor.count() > 0;
|
||
|
||
if (hasEditor) {
|
||
// 3. 尝试输入笔记内容
|
||
await editor.first().fill('这是测试笔记内容');
|
||
await page.waitForTimeout(1000);
|
||
|
||
test.info().annotations.push({
|
||
type: 'success',
|
||
description: '备课笔记功能测试完成',
|
||
});
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到笔记编辑器',
|
||
});
|
||
}
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-notes.png' });
|
||
});
|
||
|
||
test('测试12: 从备课模式进入上课', async ({ page }) => {
|
||
test.slow();
|
||
|
||
// 进入备课模式
|
||
await page.click('text=课程中心');
|
||
await page.waitForURL('**/courses', { timeout: 10000 });
|
||
await page.waitForTimeout(1000);
|
||
|
||
const firstCourseCard = page.locator('.course-card').or(page.locator('[class*="course-card"]')).first();
|
||
await firstCourseCard.click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
await page.click('button:has-text("开始备课")');
|
||
await page.waitForURL('**/prepare', { timeout: 10000 });
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 1. 查找开始上课按钮
|
||
const startClassButton = page.locator('button:has-text("开始上课")').or(page.locator('button:has-text("进入上课")'));
|
||
const buttonExists = await startClassButton.count() > 0;
|
||
|
||
if (buttonExists) {
|
||
await startClassButton.first().click();
|
||
await page.waitForTimeout(2000);
|
||
|
||
// 2. 验证跳转到上课模式或课程选择弹窗
|
||
const url = page.url();
|
||
test.info().annotations.push({
|
||
type: 'info',
|
||
description: `点击后URL: ${url}`,
|
||
});
|
||
|
||
// 3. 如果显示课程选择弹窗,选择所有课程
|
||
const selectModal = page.locator('[class*="modal"], [class*="select"]');
|
||
if (await selectModal.count() > 0) {
|
||
const confirmButton = page.locator('button:has-text("确定")').or(page.locator('button:has-text("开始上课")'));
|
||
if (await confirmButton.count() > 0) {
|
||
await confirmButton.first().click();
|
||
await page.waitForTimeout(2000);
|
||
}
|
||
}
|
||
} else {
|
||
test.info().annotations.push({
|
||
type: 'warning',
|
||
description: '未找到"开始上课"按钮',
|
||
});
|
||
}
|
||
|
||
await page.screenshot({ path: 'test-results/prepare-to-class.png' });
|
||
});
|
||
});
|