diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 3c96db4..9d605ef 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -6,6 +6,47 @@
## [Unreleased]
+### 多地点登录支持实现 ✅ (2026-03-17)
+
+**实现了多地点同时登录功能,支持同一账号在多个设备同时在线:**
+
+**问题背景**:
+- 之前系统采用单点登录 (SSO) 模式,每次登录会生成新 token 并存储到 Redis
+- `JwtTokenRedisService.validateToken()` 会验证 token 是否与 Redis 中存储的一致
+- 导致同一账号在不同地方登录时,之前的 token 会失效(互踢下线)
+
+**解决方案**:
+1. 修改 `JwtTokenRedisService.validateToken()` 方法,移除 token 一致性检查
+2. 在 `JwtAuthenticationFilter` 中增加账户状态检查 (`isAccountActive()`)
+3. 保留黑名单机制,用于主动踢人、登出等场景
+4. 状态判断修改为忽略大小写 (`equalsIgnoreCase`)
+
+**修改文件**:
+| 文件 | 修改内容 |
+|------|----------|
+| `JwtTokenRedisService.java` | 修改 `validateToken()` 方法,仅检查黑名单 |
+| `JwtAuthenticationFilter.java` | 新增 Mapper 依赖注入,增加账户状态检查逻辑,使用 `equalsIgnoreCase` 判断状态 |
+| `AuthServiceImpl.java` | 更新 `logout()` 方法注释,所有状态判断改为 `equalsIgnoreCase` |
+
+**功能特性**:
+- ✅ 同一账号可以在多个设备/浏览器同时登录
+- ✅ 各个登录状态的 token 都有效,不会互踢下线
+- ✅ 每次请求都会验证账户状态是否为 "active"
+- ✅ 支持所有角色:admin, school, teacher, parent
+- ✅ 黑名单机制仍然有效
+
+**安全性考虑**:
+- JWT token 有过期时间(默认 24 小时),过期后自动失效
+- 黑名单机制仍然有效,可以主动使特定 token 失效
+- 每次请求都会验证账户状态,确保禁用账号无法访问
+
+**验证结果**:
+- ✅ 使用同一账号 (admin) 在两个不同设备登录,两个 token 都有效
+- ✅ 两个 token 都能正常访问 API 接口
+- ✅ 代码编译通过,服务启动正常
+
+---
+
### 超管端 E2E 全面自动化测试 ✅ (2026-03-15 晚上)
**创建了超管端全面 E2E 测试,覆盖所有页面的新增、修改、查看功能:**
diff --git a/docs/design/23-校本课程包功能完善设计.md b/docs/design/23-校本课程包功能完善设计.md
index f249154..cf9f667 100644
--- a/docs/design/23-校本课程包功能完善设计.md
+++ b/docs/design/23-校本课程包功能完善设计.md
@@ -47,7 +47,7 @@ model SchoolCourse {
createdBy Int @map("created_by")
changesSummary String? @map("changes_summary") // 修改说明
usageCount Int @default(0) @map("usage_count")
- status String @default("ACTIVE") // ACTIVE, PENDING_REVIEW, REJECTED
+ status String @default("ACTIVE") // ACTIVE, PENDING, REJECTED
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
diff --git a/docs/dev-logs/2026-03-15.md b/docs/dev-logs/2026-03-15.md
index ae9a263..bdd898c 100644
--- a/docs/dev-logs/2026-03-15.md
+++ b/docs/dev-logs/2026-03-15.md
@@ -82,7 +82,7 @@ PUT /api/v1/admin/packages/{id}/courses
POST /api/v1/admin/packages/{id}/submit
```
-✅ 提交成功,状态变为 `PENDING_REVIEW`
+✅ 提交成功,状态变为 `PENDING`
#### 4. 审核通过
diff --git a/docs/dev-logs/2026-03-17.md b/docs/dev-logs/2026-03-17.md
new file mode 100644
index 0000000..adad915
--- /dev/null
+++ b/docs/dev-logs/2026-03-17.md
@@ -0,0 +1,145 @@
+# 开发日志 - 2026-03-17
+
+## 多地点登录支持实现
+
+### 修改内容
+
+#### 1. JwtTokenRedisService.java
+
+**文件路径**: `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtTokenRedisService.java`
+
+**修改内容**:
+- 修改 `validateToken()` 方法,移除了 token 一致性检查
+- 现在只检查 token 是否在黑名单中
+- 允许同一账号有多个有效的 token,支持多地点同时登录
+
+**修改前逻辑**:
+```java
+// 检查是否与存储的 token 一致
+String storedToken = getStoredToken(username);
+if (storedToken == null) {
+ return true;
+}
+boolean isValid = token.equals(storedToken);
+return isValid; // 如果不一致则返回 false,导致互踢下线
+```
+
+**修改后逻辑**:
+```java
+// 仅检查是否在黑名单中
+if (isBlacklisted(token)) {
+ return false;
+}
+// 不再检查是否与存储的 token 一致,允许同一账号有多个有效 token
+return true;
+```
+
+#### 2. JwtAuthenticationFilter.java
+
+**文件路径**: `reading-platform-java/src/main/java/com/reading/platform/common/security/JwtAuthenticationFilter.java`
+
+**修改内容**:
+- 新增依赖注入:`AdminUserMapper`, `TenantMapper`, `TeacherMapper`, `ParentMapper`
+- 在 token 验证通过后,增加账户状态检查
+- 新增 `isAccountActive()` 方法,根据用户角色查询对应表验证账户状态
+
+**新增代码**:
+```java
+// 检查账户状态是否为 active
+if (!isAccountActive(payload)) {
+ log.debug("Account is not active for user: {}", payload.getUsername());
+ sendError(response, HttpStatus.UNAUTHORIZED, "账户已被禁用,请联系管理员");
+ return;
+}
+```
+
+**新增方法**:
+```java
+private boolean isAccountActive(JwtPayload payload) {
+ String role = payload.getRole();
+ Long userId = payload.getUserId();
+
+ return switch (role) {
+ case "admin" -> {
+ AdminUser adminUser = adminUserMapper.selectById(userId);
+ yield adminUser != null && "active".equalsIgnoreCase(adminUser.getStatus());
+ }
+ case "school" -> {
+ Tenant tenant = tenantMapper.selectById(userId);
+ yield tenant != null && "active".equalsIgnoreCase(tenant.getStatus());
+ }
+ case "teacher" -> {
+ Teacher teacher = teacherMapper.selectById(userId);
+ yield teacher != null && "active".equalsIgnoreCase(teacher.getStatus());
+ }
+ case "parent" -> {
+ Parent parent = parentMapper.selectById(userId);
+ yield parent != null && "active".equalsIgnoreCase(parent.getStatus());
+ }
+ default -> false;
+ };
+}
+```
+
+#### 3. AuthServiceImpl.java
+
+**文件路径**: `reading-platform-java/src/main/java/com/reading/platform/service/impl/AuthServiceImpl.java`
+
+**修改内容**:
+- 更新 `logout()` 方法的注释,说明当前实现
+
+#### 4. 状态判断忽略大小写修改
+
+**修改内容**:
+- 将所有 `"active".equals(status)` 修改为 `"active".equalsIgnoreCase(status)`
+- 确保大小写不敏感的状态判断,如 "Active"、"ACTIVE"、"active" 都被识别为激活状态
+
+**修改文件**:
+- `JwtAuthenticationFilter.java` - `isAccountActive()` 方法中的 4 处判断
+- `AuthServiceImpl.java` - `login()` 方法中的 8 处判断(admin、teacher、parent、tenant 各 2 处)
+
+### 功能说明
+
+#### 多地点登录
+- 同一账号可以在多个设备/浏览器同时登录
+- 各个登录状态的 token 都有效,不会互踢下线
+- JWT token 本身的过期时间(默认 24 小时)保证安全性
+
+#### 账户状态验证
+- 每次请求都会验证账户状态是否为 "active"
+- 如果管理员在后台禁用某个账号,该账号的所有已登录会话将立即失效
+- 支持所有角色:admin、school、teacher、parent
+
+#### 黑名单机制
+- 黑名单机制仍然有效
+- 可以用于主动使特定 token 失效(如踢人下线场景)
+
+### 验证步骤
+
+1. **多地点登录测试**:
+ - 使用同一账号在两个不同的浏览器登录
+ - 在两个浏览器都发起 API 请求
+ - 预期:两个登录都保持有效
+
+2. **账户状态禁用测试**:
+ - 使用账号 A 在浏览器 1 登录
+ - 在超管后台将账号 A 的状态修改为"非激活"
+ - 在浏览器 1 再次发起请求,应返回"账户已被禁用"
+
+### 安全性考虑
+
+1. JWT token 有过期时间(默认 24 小时),过期后自动失效
+2. 黑名单机制仍然有效,可以主动使特定 token 失效
+3. 每次请求都会验证账户状态,确保禁用账号无法访问
+
+### 影响范围
+
+- 所有角色的登录认证流程
+- Token 验证流程
+- 账户状态检查
+
+### 兼容性
+
+- 此修改不影响现有功能
+- 登出、黑名单等功能仍然正常工作
+- 前端无需修改
diff --git a/reading-platform-backend/src/modules/course-package/course-package.service.ts b/reading-platform-backend/src/modules/course-package/course-package.service.ts
index a6385b0..8ddcbb8 100644
--- a/reading-platform-backend/src/modules/course-package/course-package.service.ts
+++ b/reading-platform-backend/src/modules/course-package/course-package.service.ts
@@ -241,7 +241,7 @@ export class CoursePackageService {
return this.prisma.coursePackage.update({
where: { id },
data: {
- status: 'PENDING_REVIEW',
+ status: 'PENDING',
submittedAt: new Date(),
submittedBy: userId,
},
@@ -262,7 +262,7 @@ export class CoursePackageService {
throw new Error('套餐不存在');
}
- if (pkg.status !== 'PENDING_REVIEW') {
+ if (pkg.status !== 'PENDING') {
throw new Error('只有待审核状态的套餐可以审核');
}
diff --git a/reading-platform-frontend/index.html b/reading-platform-frontend/index.html
index 23e8c02..dcb49f5 100644
--- a/reading-platform-frontend/index.html
+++ b/reading-platform-frontend/index.html
@@ -9,6 +9,10 @@
幼儿阅读教学服务平台
+
+
diff --git a/reading-platform-frontend/src/api/admin.ts b/reading-platform-frontend/src/api/admin.ts
index 3e53880..35341c6 100644
--- a/reading-platform-frontend/src/api/admin.ts
+++ b/reading-platform-frontend/src/api/admin.ts
@@ -71,6 +71,7 @@ export interface CreateTenantDto {
contactPerson?: string;
contactPhone?: string;
packageType?: string;
+ packageId?: number;
teacherQuota?: number;
studentQuota?: number;
startDate?: string;
@@ -96,6 +97,17 @@ export interface UpdateTenantQuotaDto {
studentQuota?: number;
}
+// 后端返回的统计数据结构
+export interface AdminStatsResponse {
+ totalTenants: number;
+ activeTenants: number;
+ totalCourses: number;
+ totalStudents: number;
+ totalTeachers: number;
+ totalLessons: number;
+}
+
+// 前端使用的统计数据结构
export interface AdminStats {
tenantCount: number;
activeTenantCount: number;
@@ -107,6 +119,15 @@ export interface AdminStats {
monthlyLessons: number;
}
+// 后端返回的趋势数据结构(分离数组格式)
+export interface StatsTrendResponse {
+ dates: string[];
+ newStudents: number[];
+ newTeachers: number[];
+ newCourses: number[];
+}
+
+// 前端使用的趋势数据结构
export interface TrendData {
month: string;
tenantCount: number;
@@ -114,14 +135,32 @@ export interface TrendData {
studentCount: number;
}
+// 后端返回的活跃租户结构
+export interface ActiveTenantResponse {
+ tenantId: number;
+ tenantName: string;
+ activeUsers: number;
+ courseCount: number;
+}
+
+// 前端使用的活跃租户结构
export interface ActiveTenant {
id: number;
name: string;
lessonCount: number;
- teacherCount: number;
- studentCount: number;
+ teacherCount: number | string;
+ studentCount: number | string;
}
+// 后端返回的热门课程结构
+export interface PopularCourseResponse {
+ courseId: number;
+ courseName: string;
+ usageCount: number;
+ teacherCount: number;
+}
+
+// 前端使用的热门课程结构
export interface PopularCourse {
id: number;
name: string;
@@ -129,6 +168,21 @@ export interface PopularCourse {
teacherCount: number;
}
+export interface CoursePackage {
+ id: number;
+ name: string;
+ description: string;
+ price: number;
+ discountPrice?: number;
+ discountType?: string;
+ gradeLevels?: string[];
+ courseCount: number;
+ status: string;
+ publishedAt?: string;
+ createdAt?: string;
+ updatedAt?: string;
+}
+
export interface AdminSettings {
// Basic settings
systemName?: string;
@@ -195,17 +249,76 @@ export const deleteTenant = (id: number) =>
// ==================== 统计数据 ====================
-export const getAdminStats = () =>
- http.get('/v1/admin/stats');
+// 数据映射函数:后端 -> 前端
+const mapStatsData = (data: AdminStatsResponse): AdminStats => ({
+ tenantCount: data.totalTenants || 0,
+ activeTenantCount: data.activeTenants || 0,
+ courseCount: data.totalCourses || 0,
+ studentCount: data.totalStudents || 0,
+ teacherCount: data.totalTeachers || 0,
+ lessonCount: data.totalLessons || 0,
+ publishedCourseCount: 0, // 后端暂无此数据
+ monthlyLessons: 0, // 后端暂无此数据
+});
-export const getTrendData = () =>
- http.get('/v1/admin/stats/trend');
+// 趋势数据映射:分离数组 -> 对象数组
+const mapTrendData = (data: StatsTrendResponse): TrendData[] => {
+ if (!data || !data.dates || data.dates.length === 0) return [];
+ return data.dates.map((date, index) => ({
+ month: date,
+ tenantCount: 0, // 后端无此数据
+ lessonCount: data.newCourses?.[index] || 0,
+ studentCount: data.newStudents?.[index] || 0,
+ }));
+};
-export const getActiveTenants = (limit?: number) =>
- http.get('/v1/admin/stats/tenants/active', { params: { limit } });
+// 活跃租户数据映射
+const mapActiveTenants = (data: ActiveTenantResponse[]): ActiveTenant[] => {
+ if (!data || data.length === 0) return [];
+ return data.map(item => ({
+ id: item.tenantId,
+ name: item.tenantName,
+ teacherCount: '-', // 后端无单独字段
+ studentCount: '-', // 后端无单独字段
+ lessonCount: item.courseCount, // 使用 courseCount 替代
+ }));
+};
-export const getPopularCourses = (limit?: number) =>
- http.get('/v1/admin/stats/courses/popular', { params: { limit } });
+// 热门课程数据映射
+const mapPopularCourses = (data: PopularCourseResponse[]): PopularCourse[] => {
+ if (!data || data.length === 0) return [];
+ return data.map(item => ({
+ id: item.courseId,
+ name: item.courseName,
+ usageCount: item.usageCount,
+ teacherCount: item.teacherCount,
+ }));
+};
+
+export const getAdminStats = async () => {
+ const data = await http.get('/v1/admin/stats');
+ return mapStatsData(data);
+};
+
+export const getTrendData = async () => {
+ const data = await http.get('/v1/admin/stats/trend');
+ return mapTrendData(data);
+};
+
+export const getActiveTenants = async (limit?: number) => {
+ const data = await http.get('/v1/admin/stats/tenants/active', { params: { limit } });
+ return mapActiveTenants(data);
+};
+
+export const getPopularCourses = async (limit?: number) => {
+ const data = await http.get('/v1/admin/stats/courses/popular', { params: { limit } });
+ return mapPopularCourses(data);
+};
+
+// ==================== 课程套餐 ====================
+
+export const getPublishedPackages = () =>
+ http.get('/v1/admin/packages/all');
// ==================== 系统设置 ====================
diff --git a/reading-platform-frontend/src/api/file.ts b/reading-platform-frontend/src/api/file.ts
index 4856642..3aab427 100644
--- a/reading-platform-frontend/src/api/file.ts
+++ b/reading-platform-frontend/src/api/file.ts
@@ -174,6 +174,7 @@ export const fileApi = {
| "ppt"
| "poster"
| "document"
+ | "resource"
| "other",
options?: {
onProgress?: (percent: number) => void;
@@ -235,6 +236,7 @@ export const FILE_TYPES = {
PPT: "ppt",
POSTER: "poster",
DOCUMENT: "document",
+ RESOURCE: "resource",
OTHER: "other",
} as const;
@@ -249,6 +251,7 @@ export const FILE_SIZE_LIMITS = {
PPT: 300 * 1024 * 1024, // 300MB
POSTER: 10 * 1024 * 1024, // 10MB
DOCUMENT: 300 * 1024 * 1024, // 300MB
+ RESOURCE: 100 * 1024 * 1024, // 100MB(资源库单文件限制)
OTHER: 300 * 1024 * 1024, // 300MB
} as const;
diff --git a/reading-platform-frontend/src/api/generated/model/coursePackage.ts b/reading-platform-frontend/src/api/generated/model/coursePackage.ts
index b02502f..8252a67 100644
--- a/reading-platform-frontend/src/api/generated/model/coursePackage.ts
+++ b/reading-platform-frontend/src/api/generated/model/coursePackage.ts
@@ -34,7 +34,7 @@ export interface CoursePackage {
gradeLevels?: string;
/** 课程数量 */
courseCount?: number;
- /** 状态:DRAFT、PENDING_REVIEW、APPROVED、REJECTED、PUBLISHED、OFFLINE */
+ /** 状态:DRAFT、PENDING、APPROVED、REJECTED、PUBLISHED、OFFLINE */
status?: string;
/** 提交时间 */
submittedAt?: string;
diff --git a/reading-platform-frontend/src/api/package.ts b/reading-platform-frontend/src/api/package.ts
index 7d82674..de56069 100644
--- a/reading-platform-frontend/src/api/package.ts
+++ b/reading-platform-frontend/src/api/package.ts
@@ -97,7 +97,7 @@ export function submitPackage(id: number | string) {
}
// 审核套餐
-export function reviewPackage(id: number | string, data: { approved: boolean; comment?: string }) {
+export function reviewPackage(id: number | string, data: { approved: boolean; comment?: string; publish?: boolean }) {
return http.post(`/v1/admin/packages/${id}/review`, data);
}
diff --git a/reading-platform-frontend/src/api/resource.ts b/reading-platform-frontend/src/api/resource.ts
index 79e07b7..39ba2a0 100644
--- a/reading-platform-frontend/src/api/resource.ts
+++ b/reading-platform-frontend/src/api/resource.ts
@@ -128,7 +128,7 @@ export const deleteResourceItem = (id: number) =>
http.delete(`/v1/admin/resources/items/${id}`);
export const batchDeleteResourceItems = (ids: number[]) =>
- http.post<{ message: string }>('/v1/admin/resources/items/batch-delete', { ids });
+ http.post<{ message: string }>('/v1/admin/resources/items/batch-delete', ids);
// ==================== 统计数据 ====================
diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts
index a9bcf0d..fb9415d 100644
--- a/reading-platform-frontend/src/api/school.ts
+++ b/reading-platform-frontend/src/api/school.ts
@@ -334,11 +334,34 @@ export const updateSettings = (data: UpdateSettingsDto) =>
// ==================== 课程管理 ====================
+export interface Course {
+ id: number;
+ tenantId?: number;
+ name: string;
+ code?: string;
+ description?: string;
+ coverUrl?: string;
+ coverImagePath?: string;
+ category?: string;
+ ageRange?: string;
+ difficultyLevel?: string;
+ durationMinutes?: number;
+ objectives?: string;
+ status: string;
+ isSystem: number;
+ version?: string;
+ usageCount?: number;
+ teacherCount?: number;
+ createdAt?: string;
+ updatedAt?: string;
+ publishedAt?: string;
+}
+
export const getSchoolCourses = () =>
- http.get('/v1/school/courses');
+ http.get('/v1/school/courses');
export const getSchoolCourse = (id: number) =>
- http.get(`/v1/school/courses/${id}`);
+ http.get(`/v1/school/courses/${id}`);
// ==================== 班级教师管理 ====================
diff --git a/reading-platform-frontend/src/api/teacher.ts b/reading-platform-frontend/src/api/teacher.ts
index a05ca17..fbd3221 100644
--- a/reading-platform-frontend/src/api/teacher.ts
+++ b/reading-platform-frontend/src/api/teacher.ts
@@ -181,26 +181,52 @@ export interface StudentRecordDto {
notes?: string;
}
+// 后端状态值: scheduled, in_progress, completed, cancelled
+const STATUS_TO_BACKEND: Record = {
+ PLANNED: 'scheduled',
+ IN_PROGRESS: 'in_progress',
+ COMPLETED: 'completed',
+ CANCELLED: 'cancelled',
+};
+
// 获取授课记录列表
export function getLessons(params?: {
pageNum?: number;
+ page?: number;
pageSize?: number;
status?: string;
- courseId?: number;
+ startDate?: string;
+ endDate?: string;
}): Promise<{
items: any[];
total: number;
page: number;
pageSize: number;
}> {
- return http.get('/v1/teacher/lessons', {
- params: {
- pageNum: params?.pageNum,
- pageSize: params?.pageSize,
- status: params?.status,
- startDate: params?.courseId, // 如果需要可以传其他参数
- },
- }) as any;
+ const pageNum = params?.pageNum ?? params?.page ?? 1;
+ const status = params?.status ? (STATUS_TO_BACKEND[params.status] || params.status) : undefined;
+ return http
+ .get<{ list?: any[]; records?: any[]; total?: number | string; pageNum?: number; pageSize?: number }>(
+ '/v1/teacher/lessons',
+ {
+ params: {
+ pageNum,
+ pageSize: params?.pageSize ?? 10,
+ status,
+ startDate: params?.startDate,
+ endDate: params?.endDate,
+ },
+ }
+ )
+ .then((res) => {
+ const list = res?.list ?? res?.records ?? [];
+ return {
+ items: Array.isArray(list) ? list : [],
+ total: typeof res?.total === 'string' ? parseInt(res.total, 10) || 0 : (res?.total ?? 0),
+ page: res?.pageNum ?? pageNum,
+ pageSize: res?.pageSize ?? 10,
+ };
+ });
}
// 获取单个授课记录详情(id 使用 string 避免 Long 精度丢失)
diff --git a/reading-platform-frontend/src/components.d.ts b/reading-platform-frontend/src/components.d.ts
index 70e729b..6c38de9 100644
--- a/reading-platform-frontend/src/components.d.ts
+++ b/reading-platform-frontend/src/components.d.ts
@@ -7,16 +7,11 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
- AAlert: typeof import('ant-design-vue/es')['Alert']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
- AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
ACard: typeof import('ant-design-vue/es')['Card']
- ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
- ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
- ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
@@ -39,34 +34,24 @@ declare module 'vue' {
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
- APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
- AProgress: typeof import('ant-design-vue/es')['Progress']
- ARadio: typeof import('ant-design-vue/es')['Radio']
- ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
- ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpace: typeof import('ant-design-vue/es')['Space']
- ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
- AStep: typeof import('ant-design-vue/es')['Step']
- ASteps: typeof import('ant-design-vue/es')['Steps']
ATable: typeof import('ant-design-vue/es')['Table']
- ATabPane: typeof import('ant-design-vue/es')['TabPane']
- ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
- ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
- ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+ AUpload: typeof import('ant-design-vue/es')['Upload']
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
LessonConfigPanel: typeof import('./components/course/LessonConfigPanel.vue')['default']
LessonStepsEditor: typeof import('./components/course/LessonStepsEditor.vue')['default']
NotificationBell: typeof import('./components/NotificationBell.vue')['default']
+ PressDrag: typeof import('./components/PressDrag.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Step1BasicInfo: typeof import('./components/course-edit/Step1BasicInfo.vue')['default']
diff --git a/reading-platform-frontend/src/components/FilePreviewModal.vue b/reading-platform-frontend/src/components/FilePreviewModal.vue
index 0f2ab15..8544d1f 100644
--- a/reading-platform-frontend/src/components/FilePreviewModal.vue
+++ b/reading-platform-frontend/src/components/FilePreviewModal.vue
@@ -1,12 +1,16 @@
-
-
-
+
+
-
无法预览此PDF文件
-
下载查看
+
PDF 文件预览
+
使用 WebOffice 在新页面中预览
+
+
+ PDF 预览
+
+
@@ -26,10 +30,11 @@
-
-
diff --git a/reading-platform-frontend/src/views/admin/packages/PackageDetailView.vue b/reading-platform-frontend/src/views/admin/packages/PackageDetailView.vue
index 0fb15a2..e738f97 100644
--- a/reading-platform-frontend/src/views/admin/packages/PackageDetailView.vue
+++ b/reading-platform-frontend/src/views/admin/packages/PackageDetailView.vue
@@ -84,7 +84,7 @@ const courseColumns = [
const statusColors: Record = {
DRAFT: 'default',
- PENDING_REVIEW: 'processing',
+ PENDING: 'processing',
APPROVED: 'success',
REJECTED: 'error',
PUBLISHED: 'blue',
@@ -93,7 +93,7 @@ const statusColors: Record = {
const statusTexts: Record = {
DRAFT: '草稿',
- PENDING_REVIEW: '待审核',
+ PENDING: '待审核',
APPROVED: '已通过',
REJECTED: '已拒绝',
PUBLISHED: '已发布',
diff --git a/reading-platform-frontend/src/views/admin/packages/PackageListView.vue b/reading-platform-frontend/src/views/admin/packages/PackageListView.vue
index b95f9ae..73b82f3 100644
--- a/reading-platform-frontend/src/views/admin/packages/PackageListView.vue
+++ b/reading-platform-frontend/src/views/admin/packages/PackageListView.vue
@@ -27,7 +27,7 @@
@change="fetchData"
>
草稿
- 待审核
+ 待审核
已通过
已拒绝
已发布
@@ -81,7 +81,7 @@
审核
@@ -143,7 +143,7 @@ const columns = [
const statusColors: Record = {
DRAFT: 'default',
- PENDING_REVIEW: 'processing',
+ PENDING: 'processing',
APPROVED: 'success',
REJECTED: 'error',
PUBLISHED: 'blue',
@@ -152,7 +152,7 @@ const statusColors: Record = {
const statusTexts: Record = {
DRAFT: '草稿',
- PENDING_REVIEW: '待审核',
+ PENDING: '待审核',
APPROVED: '已通过',
REJECTED: '已拒绝',
PUBLISHED: '已发布',
@@ -183,7 +183,7 @@ const fetchData = async () => {
pagination.total = res.total || 0;
// 获取待审核数量
try {
- const pendingRes = await getPackageList({ status: 'PENDING_REVIEW', pageNum: 1, pageSize: 1 }) as any;
+ const pendingRes = await getPackageList({ status: 'PENDING', pageNum: 1, pageSize: 1 }) as any;
pendingCount.value = pendingRes.total || 0;
} catch {
pendingCount.value = 0;
diff --git a/reading-platform-frontend/src/views/admin/packages/PackageReviewView.vue b/reading-platform-frontend/src/views/admin/packages/PackageReviewView.vue
index da00be1..4290618 100644
--- a/reading-platform-frontend/src/views/admin/packages/PackageReviewView.vue
+++ b/reading-platform-frontend/src/views/admin/packages/PackageReviewView.vue
@@ -5,7 +5,7 @@
- 待审核
+ 待审核
已驳回
@@ -39,8 +39,8 @@
-
- {{ record.status === 'PENDING_REVIEW' ? '待审核' : '已驳回' }}
+
+ {{ record.status === 'PENDING' ? '待审核' : '已驳回' }}
@@ -48,7 +48,7 @@
-
+
审核
@@ -107,12 +107,15 @@