Compare commits

...

3 Commits

Author SHA1 Message Date
En
a3ec8f47f4 Merge branch 'main_dev' 2026-03-11 16:29:34 +08:00
zhonghua
128b89241e 接口同步 2026-03-11 16:27:37 +08:00
zhonghua
271e02032c 开发规范完善 2026-03-11 14:38:43 +08:00
13 changed files with 458 additions and 266 deletions

View File

@ -0,0 +1,113 @@
# 前端 API 开发规范Orval 生成代码)
本规范面向 `reading-platform-frontend`,以 `src/api/generated/` 为**接口类型与路径的唯一真源**,通过 Orval 从后端 OpenAPI 自动生成 TypeScript 类型 + 客户端方法。
## 1. 目录与职责边界
- **`reading-platform-frontend/src/api/generated/`**Orval 自动生成目录,**禁止手改**。
- `api.ts``getReadingPlatformAPI()` 工厂函数,返回包含全部接口方法的对象。
- `model/`OpenAPI 生成的 DTO/VO/Result/PageResult/Params 类型。
- **`reading-platform-frontend/src/api/client.ts`**:项目侧的“统一入口/别名层”,导出 `readingApi`(完整客户端实例)以及常用的类型工具(解包、分页别名等)。
- **`reading-platform-frontend/src/api/*.ts`**:业务侧“适配层”(可选),用于:
- 兼容既有页面期望的“扁平结构/字段名/返回形态”
- 补齐 OpenAPI 暂未覆盖的历史接口(短期过渡)
- 汇聚跨接口的业务逻辑(例如组合请求、额外校验)
## 2. 基本原则(必须遵守)
- **生成代码只读**:不得在 `src/api/generated/**` 内做任何手工修改(包括修复类型、改路径、加字段)。
- **以生成类型为准**:参数/返回类型优先使用 `src/api/generated/model` 导出的类型,避免手写 `interface` 漂移。
- **对外只暴露稳定的业务接口**:页面/组件尽量通过 `src/api/*.ts`(适配层)或 `src/api/client.ts`(直接调用)访问,避免散落调用方式导致难以迁移。
## 3. 推荐调用方式
### 3.1 直接使用 Orval 客户端
- 统一从 `src/api/client.ts` 引入:
- `readingApi`: `getReadingPlatformAPI()` 的实例
- `ApiResultOf` / `UnwrapResult` / `PageDataOf` 等类型工具
示例(以 `Result<T>` 为包裹结构):
```ts
import { readingApi } from "@/api/client";
import type { ResultTenant } from "@/api/generated/model";
async function loadTenant(id: number) {
const res = (await readingApi.getTenant(id)) as ResultTenant;
return res.data; // T可能为 undefined取决于后端返回与类型定义
}
```
### 3.2 使用“适配层”稳定返回结构
当页面已经依赖历史返回结构(例如直接要 `items/total/page/pageSize`),在 `src/api/*.ts` 内做一次性适配,页面只消费适配后的结构。
分页适配建议统一输出:
- `items: T[]`
- `total: number`
- `page: number`
- `pageSize: number`
## 4. Result / PageResult 约定与解包
后端统一响应通常为:
- **普通接口**`Result<T>`,字段一般为 `code/message/data`
- **分页接口**`Result<PageResult<T>>`,字段一般为 `items/total/page/pageSize`
在生成代码中常见类型形态:
- `ResultXXX`(如 `ResultTenant`、`ResultUserInfoResponse`
- `ResultPageResultXXX`(如 `ResultPageResultTenant`
- `PageResultXXX`(如 `PageResultTenant`
建议做法:
- **组件/页面层尽量不要直接处理 `ResultXXX`**,而是由适配层解包并做兜底(空数组、默认分页参数等)。
- **严禁在页面散落 `as any`**;确需兼容时,集中在 `src/api/*.ts` 适配层进行,并在适配层内把“最终对页面返回的类型”定义清楚。
## 5. 命名与重复接口(`getXxx`/`getXxx1`/`getXxx2`
由于不同角色端点teacher/school/parent/admin可能存在同名资源Orval 在生成时会用 `1/2/3` 后缀消歧,例如:
- `getTask`teacher vs `getTask1`school vs `getTask2`parent
规范建议:
- **业务层不要直接暴露带数字后缀的方法名**
- 在 `src/api/*.ts` 中封装为语义化名称,例如:
- `teacherGetTask` / `schoolGetTask` / `parentGetTask`
- 或按模块拆分到 `src/api/teacher/task.ts` 等(如后续重构允许)
## 6. 何时需要更新生成代码
当后端 Controller 或 DTO/VO 发生变更:
1. 后端更新 OpenAPIKnife4j/SpringDoc
2. 前端更新规范并重新生成(项目已有脚本):
```bash
cd reading-platform-frontend
npm run api:update
```
3. 提交生成物(通常包含 `api-spec.*``src/api/generated/**`
> 注意:如果某接口在后端已存在但 OpenAPI 未导出(例如缺少注解/返回类型不规范),应优先修后端文档,而不是在前端“硬编码路径”长期绕过。
## 7. 禁止事项(高频踩坑)
- **禁止**:手改 `src/api/generated/**`(下次生成会被覆盖,且会引入不可追踪差异)。
- **禁止**:页面里手写 axios 调用去访问 `/api/v1/...`(除非 OpenAPI 暂缺且已在适配层集中兜底)。
- **禁止**:在业务代码中扩散 `any` 来“快速通过类型检查”。
## 8. 迁移策略(从旧 `http` 到 Orval
若已有模块使用 `src/api/index.ts``http.get/post/...`
- **短期**:保留旧实现,但新增/变更接口优先走 `readingApi`
- **中期**:逐模块把旧 `http` 调用替换为 `readingApi`,并在适配层维持页面不改
- **长期**:页面全面只依赖适配层/生成客户端,减少重复封装

View File

@ -1,5 +1,5 @@
import { http } from "./index";
import { readingApi, GetTenantPageResult } from "./client";
import { readingApi } from "./client";
import type { ResultPageResultTenant, Tenant as ApiTenant } from "./generated/model";
// ==================== 类型定义 ====================
@ -202,8 +202,25 @@ export interface TenantQuotaUpdateRequest {
// ==================== 租户管理 ====================
export const getTenants = (params: TenantQueryParams) =>
readingApi.getTenantPage(params);
export const getTenants = (
params: TenantQueryParams,
): Promise<{
items: Tenant[];
total: number;
page: number;
pageSize: number;
}> =>
readingApi.getTenantPage(params as any).then((res) => {
const wrapped = res as ResultPageResultTenant;
const pageData = wrapped.data;
return {
items: (pageData?.items as unknown as ApiTenant[] as Tenant[]) ?? [],
total: pageData?.total ?? 0,
page: pageData?.page ?? params.page ?? 1,
pageSize: pageData?.pageSize ?? params.pageSize ?? 10,
};
});
export const getTenant = (id: number): Promise<TenantDetail> =>
readingApi.getTenant(id).then((res) => res.data as any);

View File

@ -1,13 +1,17 @@
import { readingApi } from "./client";
import type {
LoginRequest,
LoginResponse as ApiLoginResponse,
ResultLoginResponse,
ResultUserInfoResponse,
UserInfoResponse,
} from "./generated/model";
export type LoginParams = LoginRequest;
// 兼容现有登录页字段命名account
export interface LoginParams {
account: string;
password: string;
role: string;
}
// Java 后端返回的平铺结构(保持与现有业务使用一致)
export interface LoginResponse extends Required<
@ -30,7 +34,13 @@ export interface UserProfile {
// 登录
export function login(params: LoginParams): Promise<LoginResponse> {
return readingApi.login(params).then((res) => {
return readingApi
.login({
username: params.account,
password: params.password,
role: params.role,
})
.then((res) => {
const wrapped = res as ResultLoginResponse;
const data = (wrapped.data ?? {}) as ApiLoginResponse;
@ -42,7 +52,7 @@ export function login(params: LoginParams): Promise<LoginResponse> {
role: (data.role as LoginResponse["role"]) ?? "teacher",
tenantId: data.tenantId,
};
});
});
}
// 登出

View File

@ -1,4 +1,5 @@
import { readingApi } from "./client";
const { http } = require("./index");
import type {
ResultListCourseLesson,
CourseLesson as ApiCourseLesson,
@ -68,13 +69,8 @@ export interface CreateStepData {
// ==================== 超管端 API ====================
// 获取课程列表(系统课程课时)
export function getLessonList(courseId: number) {
return readingApi.getLessons1(courseId).then((res) => {
const wrapped = res as ResultListCourseLesson;
return wrapped.data?.items ?? [];
});
}
export const getLessonList = readingApi.getLessons1;
// 获取课程详情
export function getLessonDetail(courseId: number, lessonId: number) {
return readingApi
@ -100,7 +96,6 @@ export function updateLesson(
data: Partial<CreateLessonData>,
) {
// Orval 接口需要同时提供 courseId 和 lessonId这里仅有 lessonId 时保留旧实现
const { http } = require("./index");
return http.put(`/api/v1/admin/courses/0/lessons/${lessonId}`, data);
}
@ -111,7 +106,6 @@ export function deleteLesson(courseId: number, lessonId: number) {
// 重新排序课程
export function reorderLessons(courseId: number, lessonIds: number[]) {
const { http } = require("./index");
return http.put(`/api/v1/admin/courses/${courseId}/lessons/reorder`, {
lessonIds,
});
@ -121,7 +115,6 @@ export function reorderLessons(courseId: number, lessonIds: number[]) {
// 获取环节列表
export function getStepList(courseId: number, lessonId: number) {
const { http } = require("./index");
return http.get(
`/api/v1/admin/courses/${courseId}/lessons/${lessonId}/steps`,
);
@ -133,7 +126,6 @@ export function createStep(
lessonId: number,
data: CreateStepData,
) {
const { http } = require("./index");
return http.post(
`/api/v1/admin/courses/${courseId}/lessons/${lessonId}/steps`,
data,
@ -142,13 +134,11 @@ export function createStep(
// 更新环节
export function updateStep(stepId: number, data: Partial<CreateStepData>) {
const { http } = require("./index");
return http.put(`/api/v1/admin/courses/0/lessons/steps/${stepId}`, data);
}
// 删除环节
export function deleteStep(courseId: number, lessonId: number, stepId: number) {
const { http } = require("./index");
return http.delete(
`/api/v1/admin/courses/${courseId}/lessons/steps/${stepId}`,
);
@ -160,7 +150,6 @@ export function reorderSteps(
lessonId: number,
stepIds: number[],
) {
const { http } = require("./index");
return http.put(
`/admin/courses/${courseId}/lessons/${lessonId}/steps/reorder`,
{ stepIds },

View File

@ -1,5 +1,10 @@
import { http } from "./index";
import { readingApi, UnwrapResult, ApiResultOf, GetPackages1Result } from "./client";
import {
readingApi,
UnwrapResult,
ApiResultOf,
GetPackages1Result,
} from "./client";
// ==================== 套餐管理 ====================
@ -48,16 +53,10 @@ export interface CreatePackageData {
gradeLevels: string[];
}
type AdminPackageResult = UnwrapResult<ApiResultOf<"getPackage1">>
type AdminPackageListResult = GetPackages1Result
type AdminPackageResult = UnwrapResult<ApiResultOf<"getPackage1">>;
// 获取套餐列表(管理员端)
export function getPackageList(
params?: PackageListParams,
): Promise<AdminPackageListResult> {
return readingApi.getPackages1(params as any).then((res) => res.data as any);
}
export const getPackageList = readingApi.getPackages1;
// 获取套餐详情(管理员端)
export function getPackageDetail(id: number): Promise<AdminPackageResult> {
return readingApi.getPackage1(id).then((res) => res.data as any);

View File

@ -87,7 +87,7 @@ export const getLibraries = (params?: {
readingApi.getLibraries(params as any).then((res) => {
const list = (res.data as any as ResourceLibrary[]) || [];
const page = params?.page ?? 1;
const pageSize = params?.pageSize ?? list.length || 10;
const pageSize = params?.pageSize ?? (list.length || 10);
return {
items: list,
total: list.length,

View File

@ -114,7 +114,7 @@ export function updateSchoolCourseLesson(
data: Partial<SchoolCourseLesson>,
) {
return http.put(
`/school/school-courses/${schoolCourseId}/lessons/${lessonId}`,
`/api/v1/school/school-courses/${schoolCourseId}/lessons/${lessonId}`,
data,
);
}
@ -191,7 +191,7 @@ export function updateTeacherSchoolCourseLesson(
data: Partial<SchoolCourseLesson>,
) {
return http.put(
`/teacher/school-courses/${schoolCourseId}/lessons/${lessonId}`,
`/api/v1/teacher/school-courses/${schoolCourseId}/lessons/${lessonId}`,
data,
);
}

View File

@ -158,41 +158,43 @@ export interface PackageUsage {
export const getTeachers = (params: TeacherQueryParams) =>
http.get<{ items: Teacher[]; total: number; page: number; pageSize: number }>(
"/school/teachers",
"/api/v1/school/teachers",
{ params },
);
export const getTeacher = (id: number) =>
http.get<Teacher>(`/school/teachers/${id}`);
http.get<Teacher>(`/api/v1/school/teachers/${id}`);
export const createTeacher = (data: CreateTeacherDto) =>
http.post<Teacher>("/school/teachers", data);
http.post<Teacher>("/api/v1/school/teachers", data);
export const updateTeacher = (id: number, data: Partial<CreateTeacherDto>) =>
http.put<Teacher>(`/school/teachers/${id}`, data);
http.put<Teacher>(`/api/v1/school/teachers/${id}`, data);
export const deleteTeacher = (id: number) =>
http.delete(`/api/v1/school/teachers/${id}`);
export const resetTeacherPassword = (id: number) =>
http.post<{ tempPassword: string }>(`/school/teachers/${id}/reset-password`);
http.post<{ tempPassword: string }>(
`/api/v1/school/teachers/${id}/reset-password`,
);
// ==================== 学生管理 ====================
export const getStudents = (params: StudentQueryParams) =>
http.get<{ items: Student[]; total: number; page: number; pageSize: number }>(
"/school/students",
"/api/v1/school/students",
{ params },
);
export const getStudent = (id: number) =>
http.get<Student>(`/school/students/${id}`);
http.get<Student>(`/api/v1/school/students/${id}`);
export const createStudent = (data: CreateStudentDto) =>
http.post<Student>("/school/students", data);
http.post<Student>("/api/v1/school/students", data);
export const updateStudent = (id: number, data: Partial<CreateStudentDto>) =>
http.put<Student>(`/school/students/${id}`, data);
http.put<Student>(`/api/v1/school/students/${id}`, data);
export const deleteStudent = (id: number) =>
http.delete(`/api/v1/school/students/${id}`);
@ -212,7 +214,7 @@ export interface ImportTemplate {
}
export const getStudentImportTemplate = () =>
http.get<ImportTemplate>("/school/students/import/template");
http.get<ImportTemplate>("/api/v1/school/students/import/template");
export const importStudents = (
file: File,
@ -232,16 +234,16 @@ export const importStudents = (
// ==================== 班级管理 ====================
export const getClasses = () => http.get<ClassInfo[]>("/school/classes");
export const getClasses = () => http.get<ClassInfo[]>("/api/v1/school/classes");
export const getClass = (id: number) =>
http.get<ClassInfo>(`/school/classes/${id}`);
http.get<ClassInfo>(`/api/v1/school/classes/${id}`);
export const createClass = (data: CreateClassDto) =>
http.post<ClassInfo>("/school/classes", data);
http.post<ClassInfo>("/api/v1/school/classes", data);
export const updateClass = (id: number, data: Partial<CreateClassDto>) =>
http.put<ClassInfo>(`/school/classes/${id}`, data);
http.put<ClassInfo>(`/api/v1/school/classes/${id}`, data);
export const deleteClass = (id: number) =>
http.delete(`/api/v1/school/classes/${id}`);
@ -256,35 +258,37 @@ export const getClassStudents = (
page: number;
pageSize: number;
class?: ClassInfo;
}>(`/school/classes/${classId}/students`, { params });
}>(`/api/v1/school/classes/${classId}/students`, { params });
// ==================== 统计数据 ====================
export const getSchoolStats = () => http.get<SchoolStats>("/school/stats");
export const getSchoolStats = () =>
http.get<SchoolStats>("/api/v1/school/stats");
export const getActiveTeachers = (limit?: number) =>
http.get<Array<{ id: number; name: string; lessonCount: number }>>(
"/school/stats/teachers",
"/api/v1/school/stats/teachers",
{ params: { limit } },
);
export const getCourseUsageStats = () =>
http.get<Array<{ courseId: number; courseName: string; usageCount: number }>>(
"/school/stats/courses",
"/api/v1/school/stats/courses",
);
export const getRecentActivities = (limit?: number) =>
http.get<Array<{ id: number; type: string; title: string; time: string }>>(
"/school/stats/activities",
"/api/v1/school/stats/activities",
{ params: { limit } },
);
// ==================== 套餐信息旧API保留兼容 ====================
export const getPackageInfo = () => http.get<PackageInfo>("/school/package");
export const getPackageInfo = () =>
http.get<PackageInfo>("/api/v1/school/package");
export const getPackageUsage = () =>
http.get<PackageUsage>("/school/package/usage");
http.get<PackageUsage>("/api/v1/school/package/usage");
// ==================== 套餐管理新API ====================
@ -326,10 +330,10 @@ export interface RenewPackageDto {
}
export const getTenantPackages = () =>
http.get<TenantPackage[]>("/school/packages");
http.get<TenantPackage[]>("/api/v1/school/packages");
export const renewPackage = (packageId: number, data: RenewPackageDto) =>
http.post<TenantPackage>(`/school/packages/${packageId}/renew`, data);
http.post<TenantPackage>(`/api/v1/school/packages/${packageId}/renew`, data);
// ==================== 系统设置 ====================
@ -355,25 +359,26 @@ export interface UpdateSettingsDto {
notifyOnGrowth?: boolean;
}
export const getSettings = () => http.get<SystemSettings>("/school/settings");
export const getSettings = () =>
http.get<SystemSettings>("/api/v1/school/settings");
export const updateSettings = (data: UpdateSettingsDto) =>
http.put<SystemSettings>("/school/settings", data);
http.put<SystemSettings>("/api/v1/school/settings", data);
// ==================== 课程管理 ====================
export const getSchoolCourses = () => http.get<any[]>("/school/courses");
export const getSchoolCourses = () => http.get<any[]>("/api/v1/school/courses");
export const getSchoolCourse = (id: number) =>
http.get<any>(`/school/courses/${id}`);
http.get<any>(`/api/v1/school/courses/${id}`);
// ==================== 班级教师管理 ====================
export const getClassTeachers = (classId: number) =>
http.get<ClassTeacher[]>(`/school/classes/${classId}/teachers`);
http.get<ClassTeacher[]>(`/api/v1/school/classes/${classId}/teachers`);
export const addClassTeacher = (classId: number, data: AddClassTeacherDto) =>
http.post<ClassTeacher>(`/school/classes/${classId}/teachers`, data);
http.post<ClassTeacher>(`/api/v1/school/classes/${classId}/teachers`, data);
export const updateClassTeacher = (
classId: number,
@ -381,25 +386,27 @@ export const updateClassTeacher = (
data: UpdateClassTeacherDto,
) =>
http.put<ClassTeacher>(
`/school/classes/${classId}/teachers/${teacherId}`,
`/api/v1/school/classes/${classId}/teachers/${teacherId}`,
data,
);
export const removeClassTeacher = (classId: number, teacherId: number) =>
http.delete<{ message: string }>(
`/school/classes/${classId}/teachers/${teacherId}`,
`/api/v1/school/classes/${classId}/teachers/${teacherId}`,
);
// ==================== 学生调班 ====================
export const transferStudent = (studentId: number, data: TransferStudentDto) =>
http.post<{ message: string }>(
`/school/students/${studentId}/transfer`,
`/api/v1/school/students/${studentId}/transfer`,
data,
);
export const getStudentClassHistory = (studentId: number) =>
http.get<StudentClassHistory[]>(`/school/students/${studentId}/history`);
http.get<StudentClassHistory[]>(
`/api/v1/school/students/${studentId}/history`,
);
// ==================== 排课管理 ====================
@ -479,22 +486,22 @@ export const getSchedules = (params?: ScheduleQueryParams) =>
total: number;
page: number;
pageSize: number;
}>("/school/schedules", { params });
}>("/api/v1/school/schedules", { params });
export const getSchedule = (id: number) =>
http.get<SchedulePlan>(`/school/schedules/${id}`);
http.get<SchedulePlan>(`/api/v1/school/schedules/${id}`);
export const createSchedule = (data: CreateScheduleDto) =>
http.post<SchedulePlan>("/school/schedules", data);
http.post<SchedulePlan>("/api/v1/school/schedules", data);
export const updateSchedule = (id: number, data: UpdateScheduleDto) =>
http.put<SchedulePlan>(`/school/schedules/${id}`, data);
http.put<SchedulePlan>(`/api/v1/school/schedules/${id}`, data);
export const cancelSchedule = (id: number) =>
http.delete<{ message: string }>(`/school/schedules/${id}`);
http.delete<{ message: string }>(`/api/v1/school/schedules/${id}`);
export const getTimetable = (params: TimetableQueryParams) =>
http.get<TimetableItem[]>("/school/schedules/timetable", { params });
http.get<TimetableItem[]>("/api/v1/school/schedules/timetable", { params });
export interface BatchScheduleItem {
classId: number;
@ -513,7 +520,7 @@ export interface BatchCreateResult {
}
export const batchCreateSchedules = (schedules: BatchScheduleItem[]) =>
http.post<BatchCreateResult>("/school/schedules/batch", { schedules });
http.post<BatchCreateResult>("/api/v1/school/schedules/batch", { schedules });
// ==================== 趋势与分布统计 ====================
@ -529,12 +536,14 @@ export interface CourseDistributionItem {
}
export const getLessonTrend = (months?: number) =>
http.get<LessonTrendItem[]>("/school/stats/lesson-trend", {
http.get<LessonTrendItem[]>("/api/v1/school/stats/lesson-trend", {
params: { months },
});
export const getCourseDistribution = () =>
http.get<CourseDistributionItem[]>("/school/stats/course-distribution");
http.get<CourseDistributionItem[]>(
"/api/v1/school/stats/course-distribution",
);
// ==================== 数据导出 ====================
@ -668,24 +677,29 @@ export interface ApplyTemplateDto {
export const getScheduleTemplates = (params?: {
classId?: number;
courseId?: number;
}) => http.get<ScheduleTemplate[]>("/school/schedule-templates", { params });
}) =>
http.get<ScheduleTemplate[]>("/api/v1/school/schedule-templates", { params });
export const getScheduleTemplate = (id: number) =>
http.get<ScheduleTemplate>(`/school/schedule-templates/${id}`);
http.get<ScheduleTemplate>(`/api/v1/school/schedule-templates/${id}`);
export const createScheduleTemplate = (data: CreateScheduleTemplateDto) =>
http.post<ScheduleTemplate>("/school/schedule-templates", data);
http.post<ScheduleTemplate>("/api/v1/school/schedule-templates", data);
export const updateScheduleTemplate = (
id: number,
data: UpdateScheduleTemplateDto,
) => http.put<ScheduleTemplate>(`/school/schedule-templates/${id}`, data);
) =>
http.put<ScheduleTemplate>(`/api/v1/school/schedule-templates/${id}`, data);
export const deleteScheduleTemplate = (id: number) =>
http.delete<{ message: string }>(`/school/schedule-templates/${id}`);
http.delete<{ message: string }>(`/api/v1/school/schedule-templates/${id}`);
export const applyScheduleTemplate = (id: number, data: ApplyTemplateDto) =>
http.post<SchedulePlan>(`/school/schedule-templates/${id}/apply`, data);
http.post<SchedulePlan>(
`/api/v1/school/schedule-templates/${id}/apply`,
data,
);
// ==================== 操作日志 ====================
@ -723,15 +737,15 @@ export const getOperationLogs = (params?: {
total: number;
page: number;
pageSize: number;
}>("/school/operation-logs", { params });
}>("/api/v1/school/operation-logs", { params });
export const getOperationLogStats = (startDate?: string, endDate?: string) =>
http.get<OperationLogStats>("/school/operation-logs/stats", {
http.get<OperationLogStats>("/api/v1/school/operation-logs/stats", {
params: { startDate, endDate },
});
export const getOperationLogById = (id: number) =>
http.get<OperationLog>(`/school/operation-logs/${id}`);
http.get<OperationLog>(`/api/v1/school/operation-logs/${id}`);
// ==================== 任务模板 API ====================
@ -784,22 +798,24 @@ export const getTaskTemplates = (params?: {
total: number;
page: number;
pageSize: number;
}>("/school/task-templates", { params });
}>("/api/v1/school/task-templates", { params });
export const getTaskTemplate = (id: number) =>
http.get<TaskTemplate>(`/school/task-templates/${id}`);
http.get<TaskTemplate>(`/api/v1/school/task-templates/${id}`);
export const getDefaultTaskTemplate = (taskType: string) =>
http.get<TaskTemplate | null>(`/school/task-templates/default/${taskType}`);
http.get<TaskTemplate | null>(
`/api/v1/school/task-templates/default/${taskType}`,
);
export const createTaskTemplate = (data: CreateTaskTemplateDto) =>
http.post<TaskTemplate>("/school/task-templates", data);
http.post<TaskTemplate>("/api/v1/school/task-templates", data);
export const updateTaskTemplate = (id: number, data: UpdateTaskTemplateDto) =>
http.put<TaskTemplate>(`/school/task-templates/${id}`, data);
http.put<TaskTemplate>(`/api/v1/school/task-templates/${id}`, data);
export const deleteTaskTemplate = (id: number) =>
http.delete<{ message: string }>(`/school/task-templates/${id}`);
http.delete<{ message: string }>(`/api/v1/school/task-templates/${id}`);
// ==================== 任务统计 API ====================
@ -838,16 +854,17 @@ export interface MonthlyTaskStats {
rate: number;
}
export const getTaskStats = () => http.get<TaskStats>("/school/tasks/stats");
export const getTaskStats = () =>
http.get<TaskStats>("/api/v1/school/tasks/stats");
export const getTaskStatsByType = () =>
http.get<TaskStatsByType>("/school/tasks/stats/by-type");
http.get<TaskStatsByType>("/api/v1/school/tasks/stats/by-type");
export const getTaskStatsByClass = () =>
http.get<TaskStatsByClass[]>("/school/tasks/stats/by-class");
http.get<TaskStatsByClass[]>("/api/v1/school/tasks/stats/by-class");
export const getMonthlyTaskStats = (months?: number) =>
http.get<MonthlyTaskStats[]>("/school/tasks/stats/monthly", {
http.get<MonthlyTaskStats[]>("/api/v1/school/tasks/stats/monthly", {
params: { months },
});
@ -920,24 +937,25 @@ export const getSchoolTasks = (params?: {
total: number;
page: number;
pageSize: number;
}>("/school/tasks", { params });
}>("/api/v1/school/tasks", { params });
export const getSchoolTask = (id: number) =>
http.get<SchoolTask>(`/school/tasks/${id}`);
http.get<SchoolTask>(`/api/v1/school/tasks/${id}`);
export const createSchoolTask = (data: CreateSchoolTaskDto) =>
http.post<SchoolTask>("/school/tasks", data);
http.post<SchoolTask>("/api/v1/school/tasks", data);
export const updateSchoolTask = (id: number, data: UpdateSchoolTaskDto) =>
http.put<SchoolTask>(`/school/tasks/${id}`, data);
http.put<SchoolTask>(`/api/v1/school/tasks/${id}`, data);
export const deleteSchoolTask = (id: number) =>
http.delete<{ message: string }>(`/school/tasks/${id}`);
http.delete<{ message: string }>(`/api/v1/school/tasks/${id}`);
export const getSchoolTaskCompletions = (taskId: number) =>
http.get<TaskCompletion[]>(`/school/tasks/${taskId}/completions`);
http.get<TaskCompletion[]>(`/api/v1/school/tasks/${taskId}/completions`);
export const getSchoolClasses = () => http.get<ClassInfo[]>("/school/classes");
export const getSchoolClasses = () =>
http.get<ClassInfo[]>("/api/v1/school/classes");
// ==================== 数据报告 API ====================
@ -976,16 +994,16 @@ export interface StudentReport {
}
export const getReportOverview = () =>
http.get<ReportOverview>("/school/reports/overview");
http.get<ReportOverview>("/api/v1/school/reports/overview");
export const getTeacherReports = () =>
http.get<TeacherReport[]>("/school/reports/teachers");
http.get<TeacherReport[]>("/api/v1/school/reports/teachers");
export const getCourseReports = () =>
http.get<CourseReport[]>("/school/reports/courses");
http.get<CourseReport[]>("/api/v1/school/reports/courses");
export const getStudentReports = () =>
http.get<StudentReport[]>("/school/reports/students");
http.get<StudentReport[]>("/api/v1/school/reports/students");
// ==================== 家长管理 ====================
@ -1040,41 +1058,43 @@ export interface AddChildDto {
export const getParents = (params?: ParentQueryParams) =>
http.get<{ items: Parent[]; total: number; page: number; pageSize: number }>(
"/school/parents",
"/api/v1/school/parents",
{ params },
);
export const getParent = (id: number) =>
http.get<Parent>(`/school/parents/${id}`);
http.get<Parent>(`/api/v1/school/parents/${id}`);
export const createParent = (data: CreateParentDto) =>
http.post<Parent>("/school/parents", data);
http.post<Parent>("/api/v1/school/parents", data);
export const updateParent = (id: number, data: UpdateParentDto) =>
http.put<Parent>(`/school/parents/${id}`, data);
http.put<Parent>(`/api/v1/school/parents/${id}`, data);
export const deleteParent = (id: number) =>
http.delete<{ message: string }>(`/school/parents/${id}`);
http.delete<{ message: string }>(`/api/v1/school/parents/${id}`);
export const resetParentPassword = (id: number) =>
http.post<{ tempPassword: string }>(`/school/parents/${id}/reset-password`);
http.post<{ tempPassword: string }>(
`/api/v1/school/parents/${id}/reset-password`,
);
export const getParentChildren = async (
parentId: number,
): Promise<ParentChild[]> => {
const parent = await http.get<Parent & { children: ParentChild[] }>(
`/school/parents/${parentId}`,
`/api/v1/school/parents/${parentId}`,
);
return parent.children || [];
};
export const addChildToParent = (parentId: number, data: AddChildDto) =>
http.post<ParentChild>(
`/school/parents/${parentId}/children/${data.studentId}`,
`/api/v1/school/parents/${parentId}/children/${data.studentId}`,
{ relationship: data.relationship },
);
export const removeChildFromParent = (parentId: number, studentId: number) =>
http.delete<{ message: string }>(
`/school/parents/${parentId}/children/${studentId}`,
`/api/v1/school/parents/${parentId}/children/${studentId}`,
);

View File

@ -92,11 +92,12 @@ export const getTasks = (params?: {
keyword?: string;
}) =>
http.get<{ items: Task[]; total: number; page: number; pageSize: number }>(
"/school/tasks",
"/api/v1/school/tasks",
{ params },
);
export const getTask = (id: number) => http.get<Task>(`/school/tasks/${id}`);
export const getTask = (id: number) =>
http.get<Task>(`/api/v1/school/tasks/${id}`);
export const getTaskCompletions = (
taskId: number,
@ -111,13 +112,13 @@ export const getTaskCompletions = (
total: number;
page: number;
pageSize: number;
}>(`/school/tasks/${taskId}/completions`, { params });
}>(`/api/v1/school/tasks/${taskId}/completions`, { params });
export const createTask = (data: CreateTaskDto) =>
http.post<Task>("/school/tasks", data);
http.post<Task>("/api/v1/school/tasks", data);
export const updateTask = (id: number, data: UpdateTaskDto) =>
http.put<Task>(`/school/tasks/${id}`, data);
http.put<Task>(`/api/v1/school/tasks/${id}`, data);
export const deleteTask = (id: number) =>
http.delete(`/api/v1/school/tasks/${id}`);
@ -128,7 +129,7 @@ export const updateTaskCompletion = (
data: UpdateCompletionDto,
) =>
http.put<TaskCompletion>(
`/school/tasks/${taskId}/completions/${studentId}`,
`/api/v1/school/tasks/${taskId}/completions/${studentId}`,
data,
);
@ -145,12 +146,12 @@ export const getTeacherTasks = (params?: {
keyword?: string;
}) =>
http.get<{ items: Task[]; total: number; page: number; pageSize: number }>(
"/teacher/tasks",
"/api/v1/teacher/tasks",
{ params },
);
export const getTeacherTask = (id: number) =>
http.get<Task>(`/teacher/tasks/${id}`);
http.get<Task>(`/api/v1/teacher/tasks/${id}`);
export const getTeacherTaskCompletions = (
taskId: number,
@ -165,13 +166,13 @@ export const getTeacherTaskCompletions = (
total: number;
page: number;
pageSize: number;
}>(`/teacher/tasks/${taskId}/completions`, { params });
}>(`/api/v1/teacher/tasks/${taskId}/completions`, { params });
export const createTeacherTask = (data: CreateTaskDto) =>
http.post<Task>("/teacher/tasks", data);
http.post<Task>("/api/v1/teacher/tasks", data);
export const updateTeacherTask = (id: number, data: UpdateTaskDto) =>
http.put<Task>(`/teacher/tasks/${id}`, data);
http.put<Task>(`/api/v1/teacher/tasks/${id}`, data);
export const deleteTeacherTask = (id: number) =>
http.delete(`/api/v1/teacher/tasks/${id}`);
@ -182,7 +183,7 @@ export const updateTeacherTaskCompletion = (
data: UpdateCompletionDto,
) =>
http.put<TaskCompletion>(
`/teacher/tasks/${taskId}/completions/${studentId}`,
`/api/v1/teacher/tasks/${taskId}/completions/${studentId}`,
data,
);

View File

@ -87,7 +87,7 @@ export function getTeacherCourse(id: number): Promise<unknown> {
// 获取教师的班级列表
export function getTeacherClasses(): Promise<TeacherClass[]> {
return http.get("/teacher/courses/classes");
return http.get("/api/v1/teacher/courses/classes");
}
// 获取教师所有学生列表(跨班级)
@ -115,7 +115,7 @@ export function getTeacherStudents(params?: {
page: number;
pageSize: number;
}> {
return http.get("/teacher/students", { params });
return http.get("/api/v1/teacher/students", { params });
}
// 获取班级学生列表
@ -190,7 +190,7 @@ export function getLessons(params?: {
page: number;
pageSize: number;
}> {
return http.get("/teacher/lessons", { params });
return http.get("/api/v1/teacher/lessons", { params });
}
// 获取单个授课记录详情
@ -200,7 +200,7 @@ export function getLesson(id: number): Promise<unknown> {
// 创建授课记录(备课)
export function createLesson(data: CreateLessonDto): Promise<unknown> {
return http.post("/teacher/lessons", data);
return http.post("/api/v1/teacher/lessons", data);
}
// 开始上课
@ -319,16 +319,18 @@ export interface DashboardData {
}
export const getTeacherDashboard = () =>
http.get<DashboardData>("/teacher/dashboard");
http.get<DashboardData>("/api/v1/teacher/dashboard");
export const getTodayLessons = () =>
http.get<DashboardData["todayLessons"]>("/teacher/dashboard/today");
http.get<DashboardData["todayLessons"]>("/api/v1/teacher/dashboard/today");
export const getRecommendedCourses = () =>
http.get<DashboardData["recommendedCourses"]>("/teacher/dashboard/recommend");
http.get<DashboardData["recommendedCourses"]>(
"/api/v1/teacher/dashboard/recommend",
);
export const getWeeklyStats = () =>
http.get<DashboardData["weeklyStats"]>("/teacher/dashboard/weekly");
http.get<DashboardData["weeklyStats"]>("/api/v1/teacher/dashboard/weekly");
// ==================== 教师统计趋势 ====================
@ -344,12 +346,12 @@ export interface TeacherCourseUsageItem {
}
export const getTeacherLessonTrend = (months?: number) =>
http.get<TeacherLessonTrendItem[]>("/teacher/dashboard/lesson-trend", {
http.get<TeacherLessonTrendItem[]>("/api/v1/teacher/dashboard/lesson-trend", {
params: { months },
});
export const getTeacherCourseUsage = () =>
http.get<TeacherCourseUsageItem[]>("/teacher/dashboard/course-usage");
http.get<TeacherCourseUsageItem[]>("/api/v1/teacher/dashboard/course-usage");
// ==================== 课程反馈 API ====================
@ -432,12 +434,12 @@ export function getSchoolFeedbacks(params: FeedbackQueryParams): Promise<{
page: number;
pageSize: number;
}> {
return http.get("/school/feedbacks", { params });
return http.get("/api/v1/school/feedbacks", { params });
}
// 获取反馈统计
export function getFeedbackStats(): Promise<FeedbackStats> {
return http.get("/school/feedbacks/stats");
return http.get("/api/v1/school/feedbacks/stats");
}
// 获取教师自己的反馈列表
@ -447,12 +449,12 @@ export function getTeacherFeedbacks(params: FeedbackQueryParams): Promise<{
page: number;
pageSize: number;
}> {
return http.get("/teacher/feedbacks", { params });
return http.get("/api/v1/teacher/feedbacks", { params });
}
// 获取教师自己的反馈统计
export function getTeacherFeedbackStats(): Promise<FeedbackStats> {
return http.get("/teacher/feedbacks/stats");
return http.get("/api/v1/teacher/feedbacks/stats");
}
// ==================== 排课管理 API ====================
@ -516,26 +518,28 @@ export const getTeacherSchedules = (params?: {
total: number;
page: number;
pageSize: number;
}>("/teacher/schedules", { params });
}>("/api/v1/teacher/schedules", { params });
export const getTeacherTimetable = (params: {
startDate: string;
endDate: string;
}) =>
http.get<TeacherTimetableItem[]>("/teacher/schedules/timetable", { params });
http.get<TeacherTimetableItem[]>("/api/v1/teacher/schedules/timetable", {
params,
});
export const getTodayTeacherSchedules = () =>
http.get<TeacherSchedule[]>("/teacher/schedules/today");
http.get<TeacherSchedule[]>("/api/v1/teacher/schedules/today");
export const createTeacherSchedule = (data: CreateTeacherScheduleDto) =>
http.post<TeacherSchedule>("/teacher/schedules", data);
http.post<TeacherSchedule>("/api/v1/teacher/schedules", data);
export const updateTeacherSchedule = (
id: number,
data: Partial<CreateTeacherScheduleDto> & { status?: string },
) => http.put<TeacherSchedule>(`/teacher/schedules/${id}`, data);
) => http.put<TeacherSchedule>(`/api/v1/teacher/schedules/${id}`, data);
export const cancelTeacherSchedule = (id: number) =>
http.delete<{ message: string }>(`/teacher/schedules/${id}`);
http.delete<{ message: string }>(`/api/v1/teacher/schedules/${id}`);
// ==================== 阅读任务 API ====================
@ -609,10 +613,10 @@ export const getTeacherTasks = (params?: {
total: number;
page: number;
pageSize: number;
}>("/teacher/tasks", { params });
}>("/api/v1/teacher/tasks", { params });
export const getTeacherTask = (id: number) =>
http.get<TeacherTask>(`/teacher/tasks/${id}`);
http.get<TeacherTask>(`/api/v1/teacher/tasks/${id}`);
export const getTeacherTaskCompletions = (
taskId: number,
@ -627,18 +631,18 @@ export const getTeacherTaskCompletions = (
total: number;
page: number;
pageSize: number;
}>(`/teacher/tasks/${taskId}/completions`, { params });
}>(`/api/v1/teacher/tasks/${taskId}/completions`, { params });
export const createTeacherTask = (data: CreateTeacherTaskDto) =>
http.post<TeacherTask>("/teacher/tasks", data);
http.post<TeacherTask>("/api/v1/teacher/tasks", data);
export const updateTeacherTask = (
id: number,
data: Partial<CreateTeacherTaskDto> & { status?: string },
) => http.put<TeacherTask>(`/teacher/tasks/${id}`, data);
) => http.put<TeacherTask>(`/api/v1/teacher/tasks/${id}`, data);
export const deleteTeacherTask = (id: number) =>
http.delete<{ message: string }>(`/teacher/tasks/${id}`);
http.delete<{ message: string }>(`/api/v1/teacher/tasks/${id}`);
export const updateTaskCompletion = (
taskId: number,
@ -646,12 +650,12 @@ export const updateTaskCompletion = (
data: UpdateTaskCompletionDto,
) =>
http.put<TaskCompletion>(
`/teacher/tasks/${taskId}/completions/${studentId}`,
`/api/v1/teacher/tasks/${taskId}/completions/${studentId}`,
data,
);
export const sendTaskReminder = (taskId: number) =>
http.post<{ message: string }>(`/teacher/tasks/${taskId}/remind`);
http.post<{ message: string }>(`/api/v1/teacher/tasks/${taskId}/remind`);
// ==================== 任务模板 API ====================
@ -702,16 +706,18 @@ export const getTaskTemplates = (params?: {
total: number;
page: number;
pageSize: number;
}>("/teacher/task-templates", { params });
}>("/api/v1/teacher/task-templates", { params });
export const getTaskTemplate = (id: number) =>
http.get<TaskTemplate>(`/teacher/task-templates/${id}`);
http.get<TaskTemplate>(`/api/v1/teacher/task-templates/${id}`);
export const getDefaultTaskTemplate = (taskType: string) =>
http.get<TaskTemplate | null>(`/teacher/task-templates/default/${taskType}`);
http.get<TaskTemplate | null>(
`/api/v1/teacher/task-templates/default/${taskType}`,
);
export const createTaskFromTemplate = (data: CreateTaskFromTemplateDto) =>
http.post<TeacherTask>("/teacher/tasks/from-template", data);
http.post<TeacherTask>("/api/v1/teacher/tasks/from-template", data);
// ==================== 任务统计 API ====================
@ -750,15 +756,16 @@ export interface MonthlyTaskStats {
rate: number;
}
export const getTaskStats = () => http.get<TaskStats>("/teacher/tasks/stats");
export const getTaskStats = () =>
http.get<TaskStats>("/api/v1/teacher/tasks/stats");
export const getTaskStatsByType = () =>
http.get<TaskStatsByType>("/teacher/tasks/stats/by-type");
http.get<TaskStatsByType>("/api/v1/teacher/tasks/stats/by-type");
export const getTaskStatsByClass = () =>
http.get<TaskStatsByClass[]>("/teacher/tasks/stats/by-class");
http.get<TaskStatsByClass[]>("/api/v1/teacher/tasks/stats/by-class");
export const getMonthlyTaskStats = (months?: number) =>
http.get<MonthlyTaskStats[]>("/teacher/tasks/stats/monthly", {
http.get<MonthlyTaskStats[]>("/api/v1/teacher/tasks/stats/monthly", {
params: { months },
});

View File

@ -7,22 +7,59 @@ 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']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
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']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
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']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
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']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
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']

View File

@ -33,99 +33,91 @@
<a-spin :spinning="loading">
<div class="class-grid" v-if="classes.length > 0">
<!-- 班级卡片 -->
<div
v-for="cls in classes"
:key="cls.id"
class="class-card"
:class="'grade-' + getGradeKey(cls.grade)"
@click="viewClassDetail(cls)"
>
<!-- 年级标签 -->
<div class="grade-badge" :class="'badge-' + getGradeKey(cls.grade)">
<span class="badge-icon">
<TagsOutlined />
</span>
<span class="badge-text">{{ cls.grade }}</span>
</div>
<!-- 角色标签 -->
<div class="role-badge" :class="'role-' + cls.myRole?.toLowerCase()">
{{ getRoleLabel(cls.myRole) }}
<span v-if="cls.isPrimary" class="primary-mark">班主任</span>
</div>
<!-- 班级信息 -->
<div class="class-header">
<div class="class-avatar" :style="{ background: getGradeGradient(cls.grade) }">
<span class="avatar-text">{{ getGradeInitial(cls.grade) }}</span>
<div v-for="cls in classes" :key="cls.id" class="class-card" :class="'grade-' + getGradeKey(cls.grade)"
@click="viewClassDetail(cls)">
<!-- 年级标签 -->
<div class="grade-badge" :class="'badge-' + getGradeKey(cls.grade)">
<span class="badge-icon">
<TagsOutlined />
</span>
<span class="badge-text">{{ cls.grade }}</span>
</div>
<div class="class-name">{{ cls.name }}</div>
</div>
<!-- 统计信息 -->
<div class="class-stats">
<div class="stat-item">
<div class="stat-icon-wrapper pink">
<TeamOutlined />
<!-- 角色标签 -->
<div class="role-badge" :class="'role-' + cls.myRole?.toLowerCase()">
{{ getRoleLabel(cls.myRole) }}
<span v-if="cls.isPrimary" class="primary-mark">班主任</span>
</div>
<!-- 班级信息 -->
<div class="class-header">
<div class="class-avatar" :style="{ background: getGradeGradient(cls.grade) }">
<span class="avatar-text">{{ getGradeInitial(cls.grade) }}</span>
</div>
<div class="stat-content">
<div class="stat-number">{{ cls.studentCount }}</div>
<div class="stat-label">名学生</div>
<div class="class-name">{{ cls.name }}</div>
</div>
<!-- 统计信息 -->
<div class="class-stats">
<div class="stat-item">
<div class="stat-icon-wrapper pink">
<TeamOutlined />
</div>
<div class="stat-content">
<div class="stat-number">{{ cls.studentCount }}</div>
<div class="stat-label">名学生</div>
</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-icon-wrapper blue">
<ReadOutlined />
</div>
<div class="stat-content">
<div class="stat-number">{{ cls.lessonCount }}</div>
<div class="stat-label">授课次数</div>
</div>
</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-icon-wrapper blue">
<ReadOutlined />
</div>
<div class="stat-content">
<div class="stat-number">{{ cls.lessonCount }}</div>
<div class="stat-label">授课次数</div>
</div>
</div>
</div>
<!-- 进度条 -->
<div class="class-progress">
<div class="progress-label">本月授课进度</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{
<!-- 进度条 -->
<div class="class-progress">
<div class="progress-label">本月授课进度</div>
<div class="progress-bar">
<div class="progress-fill" :style="{
width: Math.min((cls.lessonCount / 20) * 100, 100) + '%',
background: getGradeGradient(cls.grade)
}"
></div>
}"></div>
</div>
<div class="progress-text">{{ Math.min(Math.round((cls.lessonCount / 20) * 100), 100) }}%</div>
</div>
<div class="progress-text">{{ Math.min(Math.round((cls.lessonCount / 20) * 100), 100) }}%</div>
</div>
<!-- 操作按钮 -->
<div class="class-actions">
<button class="action-btn primary" @click.stop="prepareLesson(cls)">
<EditOutlined />
备课
</button>
<button class="action-btn" @click.stop="viewStudents(cls)">
<UsergroupAddOutlined />
学生
</button>
<button class="action-btn" @click.stop="viewHistory(cls)">
<BarChartOutlined />
记录
</button>
<!-- 操作按钮 -->
<div class="class-actions">
<button class="action-btn primary" @click.stop="prepareLesson(cls)">
<EditOutlined />
备课
</button>
<button class="action-btn" @click.stop="viewStudents(cls)">
<UsergroupAddOutlined />
学生
</button>
<button class="action-btn" @click.stop="viewHistory(cls)">
<BarChartOutlined />
记录
</button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && classes.length === 0" class="empty-state">
<div class="empty-icon-wrapper">
<HomeOutlined />
<!-- 空状态 -->
<div v-if="!loading && classes.length === 0" class="empty-state">
<div class="empty-icon-wrapper">
<HomeOutlined />
</div>
<h3 class="empty-title">暂无分配班级</h3>
<p class="empty-desc">请联系学校管理员为您分配班级</p>
</div>
<h3 class="empty-title">暂无分配班级</h3>
<p class="empty-desc">请联系学校管理员为您分配班级</p>
</div>
</a-spin>
</div>
</template>
@ -195,7 +187,7 @@ onMounted(() => {
});
const totalStudents = computed(() => {
return classes.value.reduce((sum, cls) => sum + cls.studentCount, 0);
return classes.value?.reduce((sum, cls) => sum + cls.studentCount, 0) || 0;
});
const getGradeKey = (grade: string) => {

View File

@ -1,24 +1,24 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import viteCompression from 'vite-plugin-compression';
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
import viteCompression from "vite-plugin-compression";
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'vue-router',
'pinia',
"vue",
"vue-router",
"pinia",
{
'ant-design-vue': ['message', 'notification', 'Modal'],
"ant-design-vue": ["message", "notification", "Modal"],
},
],
dts: 'src/auto-imports.d.ts',
dts: "src/auto-imports.d.ts",
}),
Components({
resolvers: [
@ -26,31 +26,32 @@ export default defineConfig({
importStyle: false,
}),
],
dts: 'src/components.d.ts',
dts: "src/components.d.ts",
}),
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz',
algorithm: "gzip",
ext: ".gz",
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
"@": resolve(__dirname, "src"),
},
},
server: {
port: 5173,
host: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
"/api": {
// target: 'http://localhost:8080',
target: "http://192.168.1.110:8080",
changeOrigin: true,
},
'/uploads': {
target: 'http://localhost:8080',
"/uploads": {
target: "http://192.168.1.110:8080",
changeOrigin: true,
},
},
@ -59,10 +60,16 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
'ant-design-vue': ['ant-design-vue', '@ant-design/icons-vue'],
'echarts': ['echarts'],
'fullcalendar': ['@fullcalendar/vue3', '@fullcalendar/core', '@fullcalendar/daygrid', '@fullcalendar/timegrid', '@fullcalendar/interaction'],
'dayjs': ['dayjs'],
"ant-design-vue": ["ant-design-vue", "@ant-design/icons-vue"],
echarts: ["echarts"],
fullcalendar: [
"@fullcalendar/vue3",
"@fullcalendar/core",
"@fullcalendar/daygrid",
"@fullcalendar/timegrid",
"@fullcalendar/interaction",
],
dayjs: ["dayjs"],
},
},
},