feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
/**
* 认证 API 测试
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { login, register, logout, getCurrentUser } from '../auth'
import { mockApiResponse, createMockUser } from '@/test/utils'
// Mock request module
vi.mock('../request', () => ({
request: {
post: vi.fn(),
get: vi.fn()
}
}))
import { request } from '../request'
describe('Auth API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('login', () => {
it('应该成功登录', async () => {
const mockUser = createMockUser()
const mockResponse = mockApiResponse.success({
access_token: 'mock.access.token',
refresh_token: 'mock.refresh.token',
user: mockUser,
expires_in: 3600
})
vi.mocked(request.post).mockResolvedValue(mockResponse)
const credentials = { username: 'testuser', password: 'password' }
const result = await login(credentials)
expect(request.post).toHaveBeenCalledWith('/api/v1/auth/login', credentials)
expect(result).toEqual(mockResponse)
})
it('应该处理登录失败', async () => {
const errorResponse = mockApiResponse.error('用户名或密码错误', 401)
vi.mocked(request.post).mockRejectedValue(errorResponse)
const credentials = { username: 'wronguser', password: 'wrongpassword' }
await expect(login(credentials)).rejects.toEqual(errorResponse)
expect(request.post).toHaveBeenCalledWith('/api/v1/auth/login', credentials)
})
})
describe('register', () => {
it('应该成功注册', async () => {
const mockUser = createMockUser()
const mockResponse = mockApiResponse.success(mockUser)
vi.mocked(request.post).mockResolvedValue(mockResponse)
const registerData = {
username: 'newuser',
email: 'newuser@example.com',
password: 'password',
confirm_password: 'password',
full_name: '新用户'
}
const result = await register(registerData)
expect(request.post).toHaveBeenCalledWith('/api/v1/auth/register', registerData)
expect(result).toEqual(mockResponse)
})
it('应该处理注册失败', async () => {
const errorResponse = mockApiResponse.error('用户名已存在', 400)
vi.mocked(request.post).mockRejectedValue(errorResponse)
const registerData = {
username: 'existinguser',
email: 'existing@example.com',
password: 'password',
confirm_password: 'password',
full_name: '已存在用户'
}
await expect(register(registerData)).rejects.toEqual(errorResponse)
})
})
describe('logout', () => {
it('应该成功登出', async () => {
const mockResponse = mockApiResponse.success(null)
vi.mocked(request.post).mockResolvedValue(mockResponse)
const result = await logout()
expect(request.post).toHaveBeenCalledWith('/api/v1/auth/logout')
expect(result).toEqual(mockResponse)
})
})
describe('getCurrentUser', () => {
it('应该成功获取当前用户信息', async () => {
const mockUser = createMockUser()
const mockResponse = mockApiResponse.success(mockUser)
vi.mocked(request.get).mockResolvedValue(mockResponse)
const result = await getCurrentUser()
expect(request.get).toHaveBeenCalledWith('/api/v1/auth/me')
expect(result).toEqual(mockResponse)
})
it('应该处理未认证错误', async () => {
const errorResponse = mockApiResponse.error('未登录', 401)
vi.mocked(request.get).mockRejectedValue(errorResponse)
await expect(getCurrentUser()).rejects.toEqual(errorResponse)
})
})
})

View File

@@ -0,0 +1,57 @@
/**
* 管理员仪表盘 API
*/
import { get } from '../request'
// 仪表盘统计数据接口
export interface DashboardStats {
users: {
total: number
growth: number
growthRate: string
}
courses: {
total: number
completed: number
completionRate: string
}
exams: {
total: number
avgScore: number
passRate: string
}
learning: {
totalHours: number
avgHours: number
activeRate: string
}
}
// 用户增长数据接口
export interface UserGrowthData {
dates: string[]
newUsers: number[]
activeUsers: number[]
}
// 课程完成率数据接口
export interface CourseCompletionData {
courses: string[]
completionRates: number[]
}
// 获取仪表盘统计数据
export const getDashboardStats = () => {
return get('/api/v1/admin/dashboard/stats')
}
// 获取用户增长数据
export const getUserGrowthData = () => {
return get('/api/v1/admin/dashboard/user-growth')
}
// 获取课程完成率数据
export const getCourseCompletionData = () => {
return get('/api/v1/admin/dashboard/course-completion')
}

View File

@@ -0,0 +1,81 @@
/**
* 岗位管理 API
*/
import { get, post, put, del } from '../request'
import type { PageParams } from '../config'
// 岗位接口
export interface Position {
id: number
name: string
code: string
parentId: number | null
parentName?: string
memberCount: number
description: string
status: 'active' | 'inactive'
createTime: string
children?: Position[]
}
// 岗位表单接口
export interface PositionForm {
name: string
code: string
parentId: number | null
description: string
status: 'active' | 'inactive'
skills?: string[]
level?: string
}
// 获取岗位列表
export const getPositionList = (params?: PageParams & { keyword?: string }) => {
const q: any = { ...(params || {}) }
if ('pageSize' in q) {
q.page_size = q.pageSize
delete q.pageSize
}
return get('/api/v1/admin/positions', q)
}
// 获取岗位树形结构
export const getPositionTree = () => {
return get('/api/v1/admin/positions/tree')
}
// 获取岗位详情
export const getPositionDetail = (id: number) => {
return get(`/api/v1/admin/positions/${id}`)
}
// 创建岗位
export const createPosition = (data: PositionForm) => {
return post('/api/v1/admin/positions', data)
}
// 更新岗位
export const updatePosition = (id: number, data: PositionForm) => {
return put(`/api/v1/admin/positions/${id}`, data)
}
// 删除岗位
export const deletePosition = (id: number) => {
return del(`/api/v1/admin/positions/${id}`)
}
// 检查岗位是否可删除
export const checkPositionDeletable = (id: number) => {
return get(`/api/v1/admin/positions/${id}/check-delete`)
}
// 添加岗位成员
export const addPositionMembers = (positionId: number, userIds: number[]) => {
return post(`/api/v1/admin/positions/${positionId}/members`, { user_ids: userIds })
}
// 移除岗位成员
export const removePositionMember = (positionId: number, userId: number) => {
return del(`/api/v1/admin/positions/${positionId}/members/${userId}`)
}

View File

@@ -0,0 +1,124 @@
/**
* 用户管理 API
*/
import { get, post, put, del } from '../request'
import type { PageParams } from '../config'
// 用户状态枚举
export type UserStatus = 'active' | 'inactive' | 'pending'
// 用户接口
export interface User {
id: number
username: string
realName: string
email: string
phone: string
position: string
department: string
role: string
status: UserStatus
lastLoginTime: string
createTime: string
avatar?: string
}
// 用户表单接口
export interface UserForm {
username: string
realName: string
email: string
phone: string
position: string
department: string
role: string
status: UserStatus
}
// 批量操作接口
export interface BatchOperation {
ids: number[]
action: 'delete' | 'activate' | 'deactivate' | 'change_role' | 'assign_position' | 'assign_team'
value?: string | number // 角色值、岗位ID、团队ID等
}
// 密码重置响应接口
export interface PasswordResetResponse {
tempPassword: string
expiresAt: string
}
// 获取用户列表
export const getUserList = (params?: PageParams & {
keyword?: string
status?: UserStatus
role?: string
}) => {
const q: any = { ...params }
if ('pageSize' in q) {
q.page_size = q.pageSize
delete q.pageSize
}
// 清理空参数,避免传递空字符串导致后端校验报错
if (!q.keyword) delete q.keyword
if (!q.role) delete q.role
// 映射前端 status -> 后端 is_active仅支持 active/disabled
if (q.status === 'active') {
q.is_active = true
} else if (q.status === 'disabled') {
q.is_active = false
}
delete q.status
// 列表路由为 "/" 结尾,追加斜杠可避免 307
return get('/api/v1/users/', q)
}
// 获取用户详情
export const getUserDetail = (id: number) => {
return get(`/api/v1/users/${id}`)
}
// 创建用户
export const createUser = (data: UserForm) => {
return post('/api/v1/users', data)
}
// 更新用户
export const updateUser = (id: number | string, data: UserForm) => {
return put(`/api/v1/users/${id}`, data)
}
// 团队相关操作改为调用 /api/v1/users 路由
export const addUserToTeamAdmin = (userId: number, teamId: number, role: 'member'|'leader' = 'member') => {
return post(`/api/v1/users/${userId}/teams/${teamId}?role=${role}`)
}
export const removeUserFromTeamAdmin = (userId: number, teamId: number) => {
return del(`/api/v1/users/${userId}/teams/${teamId}`)
}
// 删除用户
export const deleteUser = (id: number) => {
return del(`/api/v1/users/${id}`)
}
// 重置密码
export const resetUserPassword = (id: number) => {
return post(`/api/v1/admin/users/${id}/reset-password`)
}
// 批量操作
export const batchOperation = (data: BatchOperation) => {
return post('/api/v1/admin/users/batch', data)
}
// 获取用户统计
export const getUserStatistics = () => {
return get('/api/v1/admin/dashboard/stats')
}
// 与钉钉同步员工
export const syncWithDingTalk = () => {
return post('/api/v1/employee-sync/incremental-sync')
}

View File

@@ -0,0 +1,428 @@
/**
* 数据分析模块 API
*/
import request from '../request'
// 时间范围类型
export type TimeRange = '7d' | '30d' | '90d' | '1y' | 'custom'
// 统计数据基础类型
export interface StatisticItem {
name: string
value: number
change?: number // 变化百分比
trend?: 'up' | 'down' | 'stable'
}
// 时间序列数据点
export interface TimeSeriesPoint {
date: string
value: number
label?: string
}
// 分布数据
export interface DistributionItem {
category: string
count: number
percentage: number
color?: string
}
// 系统整体统计
export interface SystemOverview {
totalUsers: number
activeUsers: number
totalCourses: number
totalExams: number
totalPracticeSessions: number
totalLearningHours: number
averageScore: number
completionRate: number
userGrowth: TimeSeriesPoint[]
activityTrend: TimeSeriesPoint[]
popularCourses: Array<{
id: number
name: string
learnerCount: number
completionRate: number
averageRating: number
}>
}
// 用户学习分析
export interface UserLearningAnalysis {
userId: number
userName: string
totalLearningHours: number
completedCourses: number
averageScore: number
examCount: number
practiceSessionCount: number
abilityRadar: Array<{
ability: string
score: number
maxScore: number
level: 'beginner' | 'intermediate' | 'advanced' | 'expert'
}>
learningProgress: Array<{
courseId: number
courseName: string
progress: number
startDate: string
lastActiveDate: string
estimatedCompletionDate?: string
}>
performanceTrend: TimeSeriesPoint[]
weeklyActivity: Array<{
week: string
hours: number
sessions: number
}>
strongSubjects: string[]
weakSubjects: string[]
recommendations: Array<{
type: 'course' | 'practice' | 'review'
title: string
reason: string
priority: 'high' | 'medium' | 'low'
}>
}
// 团队分析数据
export interface TeamAnalysis {
teamId: number
teamName: string
memberCount: number
activeMemberCount: number
totalLearningHours: number
averageProgress: number
averageScore: number
completedCourses: number
memberPerformance: Array<{
userId: number
userName: string
progress: number
score: number
rank: number
learningHours: number
lastActiveDate: string
}>
progressDistribution: DistributionItem[]
scoreDistribution: DistributionItem[]
activityTrend: TimeSeriesPoint[]
topPerformers: Array<{
userId: number
userName: string
score: number
improvement: number
}>
needsAttention: Array<{
userId: number
userName: string
issue: string
severity: 'high' | 'medium' | 'low'
}>
}
// 课程分析数据
export interface CourseAnalysis {
courseId: number
courseName: string
totalLearners: number
activeLearners: number
completionRate: number
averageRating: number
averageCompletionTime: number
difficultyDistribution: DistributionItem[]
progressDistribution: DistributionItem[]
ratingDistribution: DistributionItem[]
learningTrend: TimeSeriesPoint[]
dropOffPoints: Array<{
materialId: number
materialName: string
dropOffRate: number
avgTimeSpent: number
}>
feedback: Array<{
userId: number
userName: string
rating: number
comment: string
date: string
}>
knowledgePointMastery: Array<{
pointId: string
pointName: string
masteryRate: number
avgAttempts: number
difficulty: string
}>
}
// 考试分析数据
export interface ExamAnalysis {
totalExams: number
totalParticipants: number
averageScore: number
passRate: number
scoreDistribution: DistributionItem[]
difficultyDistribution: DistributionItem[]
subjectPerformance: Array<{
subject: string
averageScore: number
passRate: number
participantCount: number
}>
questionAnalysis: Array<{
questionId: string
questionType: string
subject: string
difficulty: string
correctRate: number
averageTime: number
mistakeCount: number
}>
timeTrend: TimeSeriesPoint[]
improvementTrend: TimeSeriesPoint[]
}
// AI陪练分析数据
export interface PracticeAnalysis {
totalSessions: number
totalParticipants: number
averageScore: number
averageDuration: number
completionRate: number
scenePopularity: Array<{
sceneId: number
sceneName: string
sessionCount: number
averageScore: number
averageDuration: number
}>
performanceDistribution: DistributionItem[]
skillImprovement: Array<{
skill: string
beforeScore: number
afterScore: number
improvement: number
}>
sessionTrend: TimeSeriesPoint[]
userEngagement: Array<{
userId: number
userName: string
sessionCount: number
averageScore: number
totalDuration: number
lastSessionDate: string
}>
}
/**
* 获取系统整体统计
*/
export const getSystemOverview = (timeRange: TimeRange = '30d') => {
return request.get<SystemOverview>('/api/v1/analytics/overview', {
params: { timeRange }
})
}
/**
* 获取用户学习分析
*/
export const getUserLearningAnalysis = (userId?: number, timeRange: TimeRange = '30d') => {
const params: any = { timeRange }
if (userId) params.userId = userId
return request.get<UserLearningAnalysis>('/api/v1/analytics/user-learning', { params })
}
/**
* 获取团队分析数据
*/
export const getTeamAnalysis = (teamId?: number, timeRange: TimeRange = '30d') => {
const params: any = { timeRange }
if (teamId) params.teamId = teamId
return request.get<TeamAnalysis>('/api/v1/analytics/team', { params })
}
/**
* 获取课程分析数据
*/
export const getCourseAnalysis = (courseId?: number, timeRange: TimeRange = '30d') => {
const params: any = { timeRange }
if (courseId) params.courseId = courseId
return request.get<CourseAnalysis>('/api/v1/analytics/course', { params })
}
/**
* 获取考试分析数据
*/
export const getExamAnalysis = (params: {
examType?: string
subject?: string
timeRange?: TimeRange
courseId?: number
} = {}) => {
return request.get<ExamAnalysis>('/api/v1/analytics/exam', { params })
}
/**
* 获取AI陪练分析数据
*/
export const getPracticeAnalysis = (params: {
sceneId?: number
category?: string
timeRange?: TimeRange
} = {}) => {
return request.get<PracticeAnalysis>('/api/v1/analytics/practice', { params })
}
/**
* 获取学习趋势数据
*/
export const getLearningTrends = (params: {
type: 'user' | 'team' | 'course' | 'exam' | 'practice'
id?: number
metrics: string[]
timeRange: TimeRange
granularity?: 'day' | 'week' | 'month'
}) => {
return request.get<{
[metric: string]: TimeSeriesPoint[]
}>('/api/v1/analytics/trends', { params })
}
/**
* 获取能力雷达图数据
*/
export const getAbilityRadar = (userId?: number) => {
const params = userId ? { userId } : {}
return request.get<{
categories: Array<{
name: string
score: number
maxScore: number
skills: Array<{
name: string
score: number
maxScore: number
level: string
}>
}>
overallScore: number
maxOverallScore: number
lastUpdated: string
comparison?: {
teamAverage: number
positionAverage: number
improvement: number
}
}>('/api/v1/analytics/ability-radar', { params })
}
/**
* 获取学习路径进度分析
*/
export const getLearningPathProgress = (userId?: number, pathId?: number) => {
const params: any = {}
if (userId) params.userId = userId
if (pathId) params.pathId = pathId
return request.get<{
pathId: number
pathName: string
totalCourses: number
completedCourses: number
progress: number
estimatedCompletionDate: string
courses: Array<{
courseId: number
courseName: string
isRequired: boolean
isCompleted: boolean
progress: number
estimatedDays: number
actualDays?: number
prerequisites: number[]
status: 'not_started' | 'in_progress' | 'completed' | 'blocked'
}>
milestones: Array<{
name: string
completedAt?: string
isCompleted: boolean
requiredCourses: number[]
}>
}>('/api/v1/analytics/learning-path-progress', { params })
}
/**
* 获取排行榜数据
*/
export const getRankings = (params: {
type: 'score' | 'progress' | 'activity' | 'improvement'
scope: 'global' | 'team' | 'position'
scopeId?: number
timeRange: TimeRange
limit?: number
}) => {
return request.get<{
rankings: Array<{
rank: number
userId: number
userName: string
userAvatar?: string
teamName?: string
value: number
change?: number
trend?: 'up' | 'down' | 'stable'
}>
currentUser?: {
rank: number
value: number
change?: number
}
total: number
}>('/api/v1/analytics/rankings', { params })
}
/**
* 导出分析报告
*/
export const exportAnalysisReport = (params: {
type: 'overview' | 'user' | 'team' | 'course' | 'exam' | 'practice'
id?: number
timeRange: TimeRange
format: 'pdf' | 'excel' | 'csv'
includeCharts?: boolean
}) => {
return request.post<{
downloadUrl: string
fileName: string
fileSize: number
expiresAt: string
}>('/api/v1/analytics/export', params)
}
/**
* 获取实时数据
*/
export const getRealTimeData = () => {
return request.get<{
onlineUsers: number
activeExams: number
activePracticeSessions: number
recentActivities: Array<{
id: string
type: 'login' | 'course_start' | 'exam_complete' | 'practice_complete'
userId: number
userName: string
description: string
timestamp: string
}>
systemLoad: {
cpu: number
memory: number
storage: number
}
}>('/api/v1/analytics/realtime')
}

View File

@@ -0,0 +1,104 @@
/**
* 认证相关API
*/
import { request } from '../request'
import type { ApiResponse } from '../config'
// 登录请求参数
export interface LoginParams {
username: string
password: string
}
// 登录响应数据
export interface LoginResult {
user: {
id: number
username: string
email: string
full_name: string
role: string
is_active: boolean
is_verified: boolean
created_at?: string
updated_at?: string
}
token: {
access_token: string
refresh_token: string
token_type: string
}
}
// 注册请求参数
export interface RegisterParams {
username: string
email: string
password: string
confirm_password: string
full_name: string
}
// 用户信息
export interface UserInfo {
id: number
username: string
email: string
full_name: string
role: string
is_active: boolean
created_at: string
updated_at: string
phone?: string
bio?: string
gender?: string
avatar_url?: string
school?: string
major?: string
position_name?: string // 职位名称
teams?: any[]
is_verified?: boolean
last_login_at?: string
}
/**
* 用户登录
*/
export const login = (data: LoginParams): Promise<ApiResponse<LoginResult>> => {
return request.post('/api/v1/auth/login', data)
}
/**
* 用户注册
*/
export const register = (data: RegisterParams): Promise<ApiResponse<UserInfo>> => {
return request.post('/api/v1/auth/register', data)
}
/**
* 用户登出
*/
export const logout = (): Promise<ApiResponse<null>> => {
return request.post('/api/v1/auth/logout')
}
/**
* 刷新Token
*/
export const refreshToken = (token: string): Promise<ApiResponse<LoginResult>> => {
return request.post('/api/v1/auth/refresh', { refresh_token: token })
}
/**
* 获取当前用户信息
*/
export const getCurrentUser = (): Promise<ApiResponse<UserInfo>> => {
return request.get('/api/v1/users/me')
}
/**
* 重置密码请求
*/
export const resetPasswordRequest = (email: string): Promise<ApiResponse<null>> => {
return request.post('/api/v1/auth/reset-password', { email })
}

View File

@@ -0,0 +1,18 @@
import http from '@/utils/http'
import type { BroadcastInfo, GenerateBroadcastResponse } from '@/types/broadcast'
export const broadcastApi = {
/**
* 触发播课生成
*/
generate(courseId: number) {
return http.post<GenerateBroadcastResponse>(`/api/v1/courses/${courseId}/generate-broadcast`)
},
/**
* 获取播课信息
*/
getInfo(courseId: number) {
return http.get<BroadcastInfo>(`/api/v1/courses/${courseId}/broadcast`)
}
}

View File

@@ -0,0 +1,38 @@
/**
* API 配置文件
* 用于管理 API 基础配置和请求拦截
*/
import { env } from '@/config/env'
// API 基础配置
export const API_CONFIG = {
// 是否使用模拟数据
useMockData: env.USE_MOCK_DATA,
baseURL: env.API_BASE_URL,
timeout: env.API_TIMEOUT,
// WebSocket配置
wsBaseURL: env.WS_BASE_URL,
wsReconnectInterval: env.WS_RECONNECT_INTERVAL,
wsMaxReconnectAttempts: env.WS_MAX_RECONNECT_ATTEMPTS,
}
// API 响应接口
export interface ApiResponse<T = any> {
code: number
message: string
data: T
}
// 分页参数接口
export interface PageParams {
page: number
pageSize: number
}
// 分页响应接口
export interface PageResponse<T> {
list: T[]
total: number
page: number
pageSize: number
}

View File

@@ -0,0 +1,316 @@
import { request } from '../request'
// 课程相关接口
export interface Course {
id: number
name: string
description?: string
category: string
status: string
cover_image?: string
duration_hours?: number
difficulty_level?: number
tags?: string[]
is_featured: boolean
student_count?: number
is_new?: boolean
created_at: string
updated_at: string
}
export interface CourseCreate {
name: string
description?: string
category: string
status?: string
cover_image?: string
duration_hours?: number
difficulty_level?: number
tags?: string[]
is_featured?: boolean
}
export interface CourseUpdate extends Partial<CourseCreate> {}
// 考试设置接口
export interface CourseExamSettings {
id: number
course_id: number
single_choice_count: number
multiple_choice_count: number
true_false_count: number
fill_blank_count: number
essay_count: number
duration_minutes: number
difficulty_level: number
passing_score: number
is_enabled: boolean
show_answer_immediately: boolean
allow_retake: boolean
max_retake_times?: number
}
export interface CourseExamSettingsUpdate extends Partial<Omit<CourseExamSettings, 'id' | 'course_id'>> {}
// 岗位分配接口
export interface CoursePositionAssignment {
position_id: number
course_type: 'required' | 'optional'
priority?: number
}
export interface CoursePositionAssignmentInDB extends CoursePositionAssignment {
id: number
course_id: number
position_name?: string
position_description?: string
member_count?: number
}
// 课程资料接口
export interface CourseMaterial {
id: number
course_id: number
name: string
description?: string
file_url: string
file_type: string
file_size: number
sort_order: number
created_at: string
}
export interface CourseMaterialCreate {
name: string
description?: string
file_url: string
file_type: string
file_size: number
}
// 知识点接口
export interface KnowledgePoint {
id: number
course_id: number
material_id?: number
name: string
description?: string
type: string
source: number
topic_relation?: string
created_at: string
}
export interface KnowledgePointCreate {
name: string
description?: string
type: string
source: number
topic_relation?: string
material_id?: number
}
export interface KnowledgePointUpdate extends Partial<KnowledgePointCreate> {}
/**
* 课程管理API
*/
export const courseApi = {
// 基本CRUD操作
/**
* 获取课程列表
*/
list: (params?: {
page?: number
size?: number
status?: string
category?: string
is_featured?: boolean
keyword?: string
}) => {
return request.get<{
items: Course[]
total: number
page: number
page_size: number
pages: number
}>('/api/v1/courses', { params })
},
/**
* 获取课程详情
*/
get: (id: number) => {
return request.get<Course>(`/api/v1/courses/${id}`)
},
/**
* 创建课程
*/
create: (data: CourseCreate) => {
return request.post<Course>('/api/v1/courses', data)
},
/**
* 更新课程
*/
update: (id: number, data: CourseUpdate) => {
return request.put<Course>(`/api/v1/courses/${id}`, data)
},
/**
* 删除课程
*/
delete: (id: number) => {
return request.delete<boolean>(`/api/v1/courses/${id}`)
},
// 考试设置相关
/**
* 获取课程考试设置
*/
getExamSettings: (courseId: number) => {
return request.get<CourseExamSettings | null>(`/api/v1/courses/${courseId}/exam-settings`)
},
/**
* 创建或更新课程考试设置
*/
saveExamSettings: (courseId: number, data: CourseExamSettingsUpdate) => {
return request.post<CourseExamSettings>(`/api/v1/courses/${courseId}/exam-settings`, data)
},
/**
* 更新课程考试设置
*/
updateExamSettings: (courseId: number, data: CourseExamSettingsUpdate) => {
return request.put<CourseExamSettings>(`/api/v1/courses/${courseId}/exam-settings`, data)
},
// 岗位分配相关
/**
* 获取课程的岗位分配列表
*/
getPositions: (courseId: number, courseType?: 'required' | 'optional') => {
return request.get<CoursePositionAssignmentInDB[]>(`/api/v1/courses/${courseId}/positions`, {
params: { course_type: courseType }
})
},
/**
* 批量分配课程到岗位
*/
assignPositions: (courseId: number, assignments: CoursePositionAssignment[]) => {
return request.post<CoursePositionAssignmentInDB[]>(`/api/v1/courses/${courseId}/positions`, assignments)
},
/**
* 移除课程的岗位分配
*/
removePosition: (courseId: number, positionId: number) => {
return request.delete<boolean>(`/api/v1/courses/${courseId}/positions/${positionId}`)
},
// 课程资料相关
/**
* 添加课程资料
*/
addMaterial: (courseId: number, data: CourseMaterialCreate) => {
return request.post<CourseMaterial>(`/api/v1/courses/${courseId}/materials`, data)
},
/**
* 获取课程资料列表
*/
getMaterials: (courseId: number) => {
return request.get<CourseMaterial[]>(`/api/v1/courses/${courseId}/materials`)
},
/**
* 删除课程资料
*/
deleteMaterial: (courseId: number, materialId: number) => {
return request.delete<boolean>(`/api/v1/courses/${courseId}/materials/${materialId}`)
},
// 知识点相关
/**
* 获取课程的知识点列表
*/
getKnowledgePoints: (courseId: number, materialId?: number) => {
const params: Record<string, any> = {}
if (materialId !== undefined) {
params.material_id = materialId
}
return request.get<KnowledgePoint[]>(`/api/v1/courses/${courseId}/knowledge-points`, { params })
},
/**
* 创建知识点
*/
createKnowledgePoint: (courseId: number, data: KnowledgePointCreate) => {
return request.post<KnowledgePoint>(`/api/v1/courses/${courseId}/knowledge-points`, data)
},
/**
* 更新知识点
*/
updateKnowledgePoint: (pointId: number, data: KnowledgePointUpdate) => {
return request.put<KnowledgePoint>(`/api/v1/courses/knowledge-points/${pointId}`, data)
},
/**
* 删除知识点
*/
deleteKnowledgePoint: (pointId: number) => {
return request.delete<boolean>(`/api/v1/courses/knowledge-points/${pointId}`)
},
// 资料知识点关联API已废弃使用getKnowledgePoints替代
/**
* 获取资料关联的知识点列表通过课程知识点API筛选
*/
getMaterialKnowledgePoints: (materialId: number, courseId?: number) => {
if (courseId) {
return courseApi.getKnowledgePoints(courseId, materialId)
} else {
// 如果没有courseId直接调用资料知识点API
return request.get<KnowledgePoint[]>(`/api/v1/courses/materials/${materialId}/knowledge-points`)
}
},
/**
* 移除资料与知识点的关联
*/
removeMaterialKnowledgePoint: (materialId: number, knowledgePointId: number) => {
return request.delete(`/api/v1/courses/materials/${materialId}/knowledge-points/${knowledgePointId}`)
}
}
// 岗位API获取可分配的岗位列表
export const positionApi = {
/**
* 获取所有岗位列表(用于课程分配)
*/
list: (params?: { page?: number; size?: number }) => {
// 转换参数名前端用size后端用page_size
const apiParams: { page?: number; page_size?: number } = {}
if (params?.page) apiParams.page = params.page
if (params?.size) apiParams.page_size = params.size
return request.get<{
items: Array<{
id: number
name: string
description?: string
parent_id?: number
member_count?: number
parent_name?: string
}>
total: number
page: number
page_size: number
pages: number
}>('/api/v1/admin/positions', { params: apiParams })
}
}

View File

@@ -0,0 +1,132 @@
/**
* 课程对话 API
*
* 使用 Python 原生 AI 服务实现,支持流式对话
*/
import http from '@/utils/http'
// 基础 URL 配置(仅用于 SSE 流式请求,普通请求走 http 封装)
// 开发环境使用空字符串走Vite代理生产环境使用当前域名支持多域名部署
const SSE_BASE_URL = (() => {
if (import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'development') {
return ''
}
return window.location.origin
})()
/**
* 课程对话消息参数
*/
export interface CourseChatMessageParams {
course_id: number
query: string
conversation_id?: string
}
/**
* SSE 事件类型
*/
export interface CourseChatEvent {
event: 'conversation_started' | 'message_chunk' | 'message_end' | 'error'
conversation_id?: string
chunk?: string // 文本块(逐字返回)
message?: string // 错误消息
}
/**
* 会话信息
*/
export interface Conversation {
id: string
course_id: number
created_at: number
updated_at: number
last_message: string
message_count: number
}
/**
* 对话消息
*/
export interface ChatMessage {
role: 'user' | 'assistant'
content: string
timestamp?: number
}
/**
* 课程对话 API
*/
export const courseChatApi = {
/**
* 发送消息SSE 流式)
*
* 注意SSE 流式响应需要使用原生 fetchAxios 不支持 ReadableStream
*
* 使用方式:
* ```typescript
* const stream = await courseChatApi.sendMessage({...})
* const reader = stream.getReader()
* const decoder = new TextDecoder()
*
* while (true) {
* const { done, value } = await reader.read()
* if (done) break
*
* const text = decoder.decode(value)
* // 处理 SSE 事件...
* }
* ```
*/
async sendMessage(params: CourseChatMessageParams): Promise<ReadableStream<Uint8Array>> {
const token = localStorage.getItem('access_token')
const response = await fetch(`${SSE_BASE_URL}/api/v1/course/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(params)
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`HTTP ${response.status}: ${errorText}`)
}
if (!response.body) {
throw new Error('响应体为空')
}
return response.body
},
/**
* 获取历史对话列表
*
* 使用统一的 http 封装,自动处理认证、错误、重试
*/
async getConversations(limit: number = 20): Promise<Conversation[]> {
const response = await http.get<{ conversations: Conversation[]; total: number }>(
'/api/v1/course/conversations',
{ params: { limit } }
)
return response.data?.conversations || []
},
/**
* 获取历史消息
*
* 使用统一的 http 封装,自动处理认证、错误、重试
*/
async getMessages(conversationId: string, limit: number = 50): Promise<ChatMessage[]> {
const response = await http.get<{ messages: ChatMessage[]; total: number }>(
'/api/v1/course/messages',
{ params: { conversation_id: conversationId, limit } }
)
return response.data?.messages || []
}
}

View File

@@ -0,0 +1,150 @@
/**
* Coze AI 相关 API
*/
import request from '../request'
// Coze会话相关类型
export interface CozeSession {
sessionId: string
conversationId: string
botId: string
createdAt: string
}
// Coze消息相关类型
export interface CozeMessage {
messageId?: string
role: 'user' | 'assistant' | 'system'
content: string
contentType?: 'text' | 'card'
timestamp?: string
}
// 流式事件类型
export interface StreamEvent {
event: 'message_delta' | 'message_completed' | 'error' | 'done'
data: {
content?: string
contentType?: string
messageId?: string
error?: string
sessionId?: string
usage?: {
tokens?: number
}
}
}
// 结束会话请求
export interface EndSessionRequest {
reason?: string
feedback?: {
rating?: number
comment?: string
}
}
/**
* 创建课程对话会话
*/
export const createCourseChatSession = (courseId: string) => {
return request.get<{ data: CozeSession }>('/api/v1/course-chat/sessions', {
params: { course_id: courseId }
})
}
/**
* 创建陪练会话
*/
export const createTrainingSession = (trainingTopic?: string) => {
return request.get<{ data: CozeSession }>('/api/v1/training/sessions', {
params: { training_topic: trainingTopic }
})
}
/**
* 结束陪练会话
*/
export const endTrainingSession = (sessionId: string, data: EndSessionRequest = {}) => {
return request.post<{ data: any }>(`/api/v1/training/sessions/${sessionId}/end`, data)
}
/**
* 发送消息(支持流式响应)
* @param sessionId 会话ID
* @param content 消息内容
* @param stream 是否使用流式响应
*/
export const sendMessage = async (
sessionId: string,
content: string,
stream: boolean = true,
onMessage?: (event: StreamEvent) => void
) => {
if (stream && onMessage) {
// 流式响应处理
// 开发环境使用空字符串(走代理),生产环境使用当前域名(支持多域名部署)
const baseURL = (import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'development')
? ''
: window.location.origin
const response = await fetch(`${baseURL}/api/v1/chat/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}` // 获取认证token
},
body: JSON.stringify({
session_id: sessionId,
content,
stream: true
})
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('No response body')
}
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6))
onMessage(data)
} catch (e) {
console.error('Failed to parse SSE data:', e)
}
}
}
}
} else {
// 非流式响应
return request.post<{ data: CozeMessage }>('/api/v1/chat/messages', {
session_id: sessionId,
content,
stream: false
})
}
}
/**
* 获取会话消息历史
*/
export const getSessionMessages = (sessionId: string, limit: number = 50, offset: number = 0) => {
return request.get<{ data: { messages: CozeMessage[] } }>(`/api/v1/sessions/${sessionId}/messages`, {
params: { limit, offset }
})
}

View File

@@ -0,0 +1,20 @@
/**
* 首页数据API
*/
import request from './request'
/**
* 获取用户统计数据
*/
export function getUserStatistics() {
return request.get('/api/v1/users/me/statistics')
}
/**
* 获取最近考试列表
* @param limit 返回数量默认5条
*/
export function getRecentExams(limit: number = 5) {
return request.get('/api/v1/users/me/recent-exams', { limit })
}

164
frontend/src/api/exam.ts Normal file
View File

@@ -0,0 +1,164 @@
/**
* 考试相关API
*
* 注意本文件包含Dify考试工作流相关API
* 同时重新导出exam/index.ts中的传统考试API避免导入路径冲突
*/
import { http } from '@/utils/http'
// 重新导出传统考试API来自exam/index.ts
export {
createExam,
getExamDetail,
startExam,
submitExam,
getExamResult,
getUserExamRecords,
startDynamicExam,
submitDynamicAnswer,
getDynamicExamSession,
endDynamicExam,
// 新增成绩报告和错题本相关API
getExamReport,
getMistakesList,
getMistakesStatistics,
updateRoundScore,
markMistakeMastered
} from './exam/index'
// 重新导出类型定义
export type {
ExamReportResponse,
MistakeListItem,
MistakesStatisticsResponse
} from './exam/index'
// ==================== 类型定义 ====================
/**
* 错题记录项
*/
export interface MistakeRecordItem {
id?: number
question_id?: number | null
knowledge_point_id?: number | null
question_content: string
correct_answer: string
user_answer: string
}
/**
* 生成考试请求
*/
export interface GenerateExamRequest {
course_id: number
position_id?: number
current_round?: number // 当前轮次(1/2/3)
exam_id?: number // 已存在的exam_id(第2、3轮传入)
mistake_records?: string // 错题记录JSON字符串,第一轮不传此参数,第二三轮传入上一轮错题的JSON字符串
single_choice_count?: number
multiple_choice_count?: number
true_false_count?: number
fill_blank_count?: number
essay_count?: number
difficulty_level?: number
}
/**
* 生成考试响应
*/
export interface GenerateExamResponse {
result: string // 试题JSON数组(字符串格式)
workflow_run_id?: string
task_id?: string
exam_id: number // 真实的考试ID
}
/**
* 判断答案请求
*/
export interface JudgeAnswerRequest {
question: string
correct_answer: string
user_answer: string
analysis: string
}
/**
* 判断答案响应
*/
export interface JudgeAnswerResponse {
is_correct: boolean
correct_answer: string
feedback?: string
}
/**
* 记录错题请求
*/
export interface RecordMistakeRequest {
exam_id: number
question_id?: number | null
knowledge_point_id?: number | null
question_content: string
correct_answer: string
user_answer: string
question_type?: string // 题型(single/multiple/judge/blank/essay)
}
/**
* 记录错题响应
*/
export interface RecordMistakeResponse {
id: number
created_at: string
}
/**
* 获取错题记录响应
*/
export interface GetMistakesResponse {
mistakes: MistakeRecordItem[]
}
// ==================== API 接口 ====================
/**
* 生成考试试题(调用Dify试题生成器工作流)
*/
export function generateExam(data: GenerateExamRequest) {
return http.post<GenerateExamResponse>('/api/v1/exams/generate', data, {
showLoading: false, // 页面已有"试题动态生成中"提示不需要全局loading
timeout: 300000, // 5分钟超时
})
}
/**
* 判断主观题答案(调用Dify答案判断工作流)
*/
export function judgeAnswer(data: JudgeAnswerRequest) {
return http.post<JudgeAnswerResponse>('/api/v1/exams/judge-answer', data, {
showLoading: false,
timeout: 60000, // 1分钟超时
})
}
/**
* 记录错题
*/
export function recordMistake(data: RecordMistakeRequest) {
return http.post<RecordMistakeResponse>('/api/v1/exams/record-mistake', data, {
showLoading: false,
})
}
/**
* 获取错题记录
*/
export function getMistakes(examId: number) {
return http.get<GetMistakesResponse>(`/api/v1/exams/mistakes?exam_id=${examId}`, {
showLoading: false,
})
}

View File

@@ -0,0 +1,546 @@
/**
* 考试模块 API
*/
import request from '../request'
// 题目类型
export type QuestionType = 'single' | 'multiple' | 'judge' | 'fill'
// 题目选项
export interface QuestionOption {
id: string
content: string
isCorrect?: boolean
}
// 题目信息
export interface Question {
id: string
type: QuestionType
content: string
options?: QuestionOption[]
correctAnswer: string
explanation: string
difficulty: 'easy' | 'medium' | 'hard'
subject: string
tags: string[]
score: number
}
// 考试配置
export interface ExamConfig {
courseId?: number
questionCount: number
difficulty?: 'easy' | 'medium' | 'hard'
subjects?: string[]
tags?: string[]
timeLimit?: number // 分钟
passingScore?: number
}
// 考试实例
export interface ExamInstance {
id: string
name: string
type: 'practice' | 'formal' | 'mock'
config: ExamConfig
questions: Question[]
totalScore: number
timeLimit?: number
status: 'not_started' | 'in_progress' | 'completed' | 'timeout'
startTime?: string
endTime?: string
createdAt: string
}
// 考试答案
export interface ExamAnswer {
questionId: string
answer: string | string[]
isCorrect?: boolean
score?: number
timeSpent?: number // 秒
}
// 考试提交数据
export interface ExamSubmission {
examId: string
answers: ExamAnswer[]
submitTime: string
totalTimeSpent: number // 秒
}
// 考试结果
export interface ExamResult {
id: string
examId: string
examName: string
examType: string
userId: number
userName: string
totalScore: number
userScore: number
accuracy: number
passingScore: number
isPassed: boolean
timeSpent: number
answers: Array<ExamAnswer & {
question: Question
}>
scoreByType: Array<{
type: QuestionType
totalQuestions: number
correctAnswers: number
accuracy: number
score: number
maxScore: number
}>
scoreBySubject: Array<{
subject: string
totalQuestions: number
correctAnswers: number
accuracy: number
score: number
maxScore: number
}>
startTime: string
endTime: string
createdAt: string
}
// 动态考试轮次结果
export interface ExamRoundResult {
round: number
question: Question
userAnswer: string | string[]
isCorrect: boolean
score: number
timeSpent: number
explanation: string
}
// 动态考试会话
export interface DynamicExamSession {
sessionId: string
courseId?: number
currentRound: number
totalRounds: number
currentScore: number
averageScore: number
correctCount: number
incorrectCount: number
status: 'active' | 'completed' | 'timeout'
rounds: ExamRoundResult[]
startTime: string
lastActiveTime: string
}
/**
* 开始动态考试
*/
export const startDynamicExam = (config: {
courseId?: number
totalRounds?: number
difficulty?: string
subject?: string
}) => {
return request.post<{
sessionId: string
firstQuestion: Question
session: DynamicExamSession
}>('/api/v1/exams/dynamic/start', config)
}
/**
* 提交动态考试答案并获取下一题
*/
export const submitDynamicAnswer = (data: {
sessionId: string
questionId: string
answer: string | string[]
timeSpent?: number
}) => {
return request.post<{
result: ExamRoundResult
nextQuestion?: Question
session: DynamicExamSession
isCompleted: boolean
}>('/api/v1/exams/dynamic/submit', data)
}
/**
* 获取动态考试会话状态
*/
export const getDynamicExamSession = (sessionId: string) => {
return request.get<DynamicExamSession>(`/api/v1/exams/dynamic/sessions/${sessionId}`)
}
/**
* 结束动态考试
*/
export const endDynamicExam = (sessionId: string) => {
return request.post<{
result: ExamResult
session: DynamicExamSession
}>(`/api/v1/exams/dynamic/sessions/${sessionId}/end`)
}
/**
* 创建标准考试
*/
export const createExam = (config: ExamConfig) => {
return request.post<ExamInstance>('/api/v1/exams/create', config)
}
/**
* 获取考试详情
*/
export const getExamDetail = (examId: string) => {
return request.get<ExamInstance>(`/api/v1/exams/${examId}`)
}
/**
* 开始考试
*/
export const startExam = (examId: string) => {
return request.post<{
exam: ExamInstance
startTime: string
}>(`/api/v1/exams/${examId}/start`)
}
/**
* 提交考试
*/
export const submitExam = (submission: ExamSubmission) => {
return request.post<ExamResult>('/api/v1/exams/submit', submission)
}
/**
* 获取考试结果
*/
export const getExamResult = (resultId: string) => {
return request.get<ExamResult>(`/api/v1/exams/results/${resultId}`)
}
/**
* 获取用户考试记录列表
*/
export const getUserExamRecords = (params: {
page?: number
size?: number
examType?: string
subject?: string
status?: string
startDate?: string
endDate?: string
} = {}) => {
return request.get<{
items: ExamResult[]
total: number
page: number
size: number
statistics: {
totalExams: number
passedExams: number
averageScore: number
averageAccuracy: number
totalTimeSpent: number
bestScore: number
recentImprovement: number
}
}>('/api/v1/exams/records', { params })
}
/**
* 获取错题统计
*/
export const getMistakeStatistics = (params: {
subject?: string
difficulty?: string
tags?: string[]
startDate?: string
endDate?: string
} = {}) => {
return request.get<{
totalMistakes: number
masteredCount: number
unmasteredCount: number
recentMistakes: number
mistakesBySubject: Array<{
subject: string
count: number
masteredCount: number
}>
mistakesByDifficulty: Array<{
difficulty: string
count: number
masteredCount: number
}>
mistakesByType: Array<{
type: QuestionType
count: number
masteredCount: number
}>
}>('/api/v1/exams/mistakes/statistics', { params })
}
/**
* 获取错题列表
*/
export const getMistakeQuestions = (params: {
page?: number
size?: number
subject?: string
difficulty?: string
type?: QuestionType
isMastered?: boolean
tags?: string[]
keyword?: string
} = {}) => {
return request.get<{
items: Array<{
id: string
question: Question
mistakeCount: number
lastMistakeTime: string
firstMistakeTime: string
isMastered: boolean
userAnswers: string[]
examSources: Array<{
examId: string
examName: string
examTime: string
}>
}>
total: number
page: number
size: number
}>('/api/v1/exams/mistakes', { params })
}
/**
* 标记错题为已掌握
*/
export const markMistakeAsMastered = (questionId: string) => {
return request.post(`/api/v1/exams/mistakes/${questionId}/mastered`)
}
/**
* 错题重练
*/
export const practiceeMistakes = (questionIds: string[]) => {
return request.post<{
examId: string
questions: Question[]
}>('/api/v1/exams/mistakes/practice', { questionIds })
}
/**
* 获取推荐练习题
*/
export const getRecommendedQuestions = (params: {
courseId?: number
weakSubjects?: string[]
difficulty?: string
count?: number
}) => {
return request.get<{
questions: Question[]
reason: string
estimatedTime: number
}>('/api/v1/exams/recommend', { params })
}
// ==================== 成绩报告和错题本相关API ====================
/**
* 三轮得分
*/
export interface RoundScores {
round1: number | null
round2: number | null
round3: number | null
}
/**
* 成绩报告概览
*/
export interface ExamReportOverview {
avg_score: number
total_exams: number
pass_rate: number
total_questions: number
}
/**
* 成绩趋势项
*/
export interface ExamTrendItem {
date: string
avg_score: number
}
/**
* 科目统计项
*/
export interface SubjectStatItem {
course_id: number
course_name: string
avg_score: number
exam_count: number
max_score: number
min_score: number
pass_rate: number
}
/**
* 最近考试记录项
*/
export interface RecentExamItem {
id: number
course_id: number
course_name: string
score: number | null
total_score: number
is_passed: boolean | null
duration_seconds: number | null
start_time: string
end_time: string | null
round_scores: RoundScores
}
/**
* 成绩报告响应
*/
export interface ExamReportResponse {
overview: ExamReportOverview
trends: ExamTrendItem[]
subjects: SubjectStatItem[]
recent_exams: RecentExamItem[]
}
/**
* 错题列表项
*/
export interface MistakeListItem {
id: number
exam_id: number
course_id: number
course_name: string
question_content: string
correct_answer: string
user_answer: string
question_type: string | null
knowledge_point_id: number | null
knowledge_point_name: string | null
created_at: string
}
/**
* 错题列表响应
*/
export interface MistakeListResponse {
items: MistakeListItem[]
total: number
page: number
size: number
pages: number
}
/**
* 按课程统计错题
*/
export interface MistakeByCourse {
course_id: number
course_name: string
count: number
}
/**
* 按题型统计错题
*/
export interface MistakeByType {
type: string
type_name: string
count: number
}
/**
* 按时间统计错题
*/
export interface MistakeByTime {
week: number
month: number
quarter: number
}
/**
* 错题统计响应
*/
export interface MistakesStatisticsResponse {
total: number
by_course: MistakeByCourse[]
by_type: MistakeByType[]
by_time: MistakeByTime
}
/**
* 获取成绩报告
*/
export const getExamReport = (params?: {
start_date?: string
end_date?: string
}) => {
return request.get<ExamReportResponse>('/api/v1/exams/statistics/report', { params })
}
/**
* 获取错题列表(扩展版)
*/
export const getMistakesList = (params: {
exam_id?: number
course_id?: number
question_type?: string
search?: string
start_date?: string
end_date?: string
page?: number
size?: number
}) => {
return request.get<MistakeListResponse>('/api/v1/exams/mistakes/list', { params })
}
/**
* 获取错题统计
*/
export const getMistakesStatistics = (params?: {
course_id?: number
}) => {
return request.get<MistakesStatisticsResponse>('/api/v1/exams/mistakes/statistics', { params })
}
/**
* 更新轮次得分
*/
export const updateRoundScore = (data: {
exam_id: number
round: number
score: number
is_final?: boolean
}) => {
return request.put<{ exam_id: number }>(`/api/v1/exams/${data.exam_id}/round-score`, {
round: data.round,
score: data.score,
is_final: data.is_final
})
}
/**
* 标记错题为已掌握
*/
export const markMistakeMastered = (mistakeId: number) => {
return request.put<{ mistake_id: number; mastery_status: string }>(
`/api/v1/exams/mistakes/${mistakeId}/mastered`
)
}

39
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,39 @@
/**
* API 统一导出
*/
// 认证授权模块
export * from './auth'
// 用户管理模块
export * from './user'
// 学员功能模块
export * from './trainee'
// 管理者功能模块
export * from './manager'
// 考试模块
// 注意getMistakeQuestions 在 trainee 模块已导出,避免重复
export {
createExam,
getExamDetail,
startExam,
submitExam,
getExamResult,
getUserExamRecords,
startDynamicExam,
submitDynamicAnswer,
getDynamicExamSession,
endDynamicExam
} from './exam'
// 数据分析模块
export * from './analysis'
// 管理员模块
export * from './admin/dashboard'
// 注意:避免与通用 user 模块导出重名,管理员用户与岗位 API 请按需从具体路径导入:
// import { ... } from '@/api/admin/user'
// import { ... } from '@/api/admin/position'

View File

@@ -0,0 +1,518 @@
/**
* 管理者功能模块 API
*/
import request from '../request'
import { CourseInfo, CourseMaterial } from '../trainee'
import { UserInfo } from '../auth'
// 团队统计数据
export interface TeamStats {
totalMembers: number
activeMembers: number
averageProgress: number
averageScore: number
completedCourses: number
totalCourses: number
monthlyActiveUsers: number
learningHours: number
}
// 团队成员详情
export interface TeamMemberDetail extends UserInfo {
learningProgress: number
averageScore: number
completedCourses: number
totalLearningHours: number
lastActiveTime: string
currentCourses: Array<{
id: number
name: string
progress: number
}>
}
// 学习任务
export interface LearningTask {
id: number
title: string
description?: string
courseIds: number[]
assignedUserIds: number[]
assignedTeamIds: number[]
dueDate?: string
priority: 'low' | 'medium' | 'high'
status: 'draft' | 'published' | 'completed' | 'overdue'
createdBy: number
createdByName: string
createdAt: string
updatedAt: string
completionRate: number
}
// 创建任务请求
export interface CreateTaskRequest {
title: string
description?: string
courseIds: number[]
assignedUserIds?: number[]
assignedTeamIds?: number[]
dueDate?: string
priority: 'low' | 'medium' | 'high'
}
// 课程统计信息
export interface CourseStats {
totalCourses: number
publishedCourses: number
draftCourses: number
totalMaterials: number
totalLearners: number
averageRating: number
completionRate: number
}
// 创建课程请求
export interface CreateCourseRequest {
title: string
description?: string
category: string
difficulty: 'beginner' | 'intermediate' | 'advanced'
coverImage?: string
tags?: string[]
estimatedDuration?: number
assignedPositions?: number[]
}
// 课程详情(管理视图)
export interface CourseDetailForManager extends CourseInfo {
materials: CourseMaterial[]
assignedPositions: Array<{
id: number
name: string
}>
learnerCount: number
completionRate: number
averageRating: number
knowledgePoints: Array<{
id: string
title: string
description: string
difficulty: string
tags: string[]
}>
}
// 成长路径配置
export interface GrowthPathConfig {
id: number
positionId: number
positionName: string
name: string
description?: string
courses: Array<{
courseId: number
courseName: string
isRequired: boolean
prerequisites: number[]
order: number
estimatedDays: number
}>
totalCourses: number
requiredCourses: number
optionalCourses: number
estimatedDays: number
status: 'active' | 'inactive'
createdAt: string
updatedAt: string
}
// AI陪练场景管理视图
export interface PracticeSceneForManager {
id: number
name: string
description: string
category: string
difficulty: 'easy' | 'medium' | 'hard'
estimatedDuration: number
usageCount: number
averageRating: number
status: 'active' | 'inactive'
tags: string[]
aiRole: string
objectives: string[]
keywords: string[]
backgroundInfo: string
botId?: string
createdBy: number
createdByName: string
createdAt: string
updatedAt: string
}
// 创建陪练场景请求
export interface CreatePracticeSceneRequest {
name: string
description: string
category: string
difficulty: 'easy' | 'medium' | 'hard'
estimatedDuration: number
tags?: string[]
aiRole: string
objectives: string[]
keywords: string[]
backgroundInfo: string
botId?: string
}
// 学员考试成绩
export interface StudentExamScore {
id: string
studentId: number
studentName: string
studentEmail: string
teamName?: string
examName: string
examType: string
subject: string
totalScore: number
userScore: number
accuracy: number
duration: number
examTime: string
status: 'completed' | 'in_progress' | 'abandoned'
}
// 学员陪练记录
export interface StudentPracticeRecord {
id: string
studentId: number
studentName: string
studentEmail: string
teamName?: string
sceneName: string
sceneCategory: string
duration: number
messageCount: number
overallScore: number
result: 'excellent' | 'good' | 'average' | 'needs_improvement'
practiceTime: string
}
/**
* 获取团队统计数据
*/
export const getTeamStats = (teamId?: number) => {
const params = teamId ? { teamId } : {}
return request.get<TeamStats>('/api/v1/manager/team-stats', { params })
}
/**
* 获取团队成员详情列表
*/
export const getTeamMemberDetails = (params: {
teamId?: number
page?: number
size?: number
keyword?: string
} = {}) => {
return request.get<{
items: TeamMemberDetail[]
total: number
page: number
size: number
}>('/api/v1/manager/team-members', { params })
}
/**
* 获取学习任务列表
*/
export const getLearningTasks = (params: {
page?: number
size?: number
status?: string
priority?: string
keyword?: string
} = {}) => {
return request.get<{
items: LearningTask[]
total: number
page: number
size: number
}>('/api/v1/manager/tasks', { params })
}
/**
* 创建学习任务
*/
export const createLearningTask = (data: CreateTaskRequest) => {
return request.post<LearningTask>('/api/v1/manager/tasks', data)
}
/**
* 更新学习任务
*/
export const updateLearningTask = (taskId: number, data: Partial<CreateTaskRequest>) => {
return request.put<LearningTask>(`/api/v1/manager/tasks/${taskId}`, data)
}
/**
* 删除学习任务
*/
export const deleteLearningTask = (taskId: number) => {
return request.delete(`/api/v1/manager/tasks/${taskId}`)
}
/**
* 发布学习任务
*/
export const publishLearningTask = (taskId: number) => {
return request.post(`/api/v1/manager/tasks/${taskId}/publish`)
}
/**
* 获取课程统计信息
*/
export const getCourseStats = () => {
return request.get<CourseStats>('/api/v1/manager/course-stats')
}
/**
* 获取课程列表(管理视图)
*/
export const getManagerCourses = (params: {
page?: number
size?: number
category?: string
status?: string
keyword?: string
} = {}) => {
return request.get<{
items: CourseInfo[]
total: number
page: number
size: number
}>('/api/v1/manager/courses', { params })
}
/**
* 创建课程
*/
export const createCourse = (data: CreateCourseRequest) => {
return request.post<CourseInfo>('/api/v1/manager/courses', data)
}
/**
* 获取课程详情(管理视图)
*/
export const getManagerCourseDetail = (courseId: number) => {
return request.get<CourseDetailForManager>(`/api/v1/manager/courses/${courseId}`)
}
/**
* 更新课程
*/
export const updateCourse = (courseId: number, data: Partial<CreateCourseRequest>) => {
return request.put<CourseInfo>(`/api/v1/manager/courses/${courseId}`, data)
}
/**
* 删除课程
*/
export const deleteCourse = (courseId: number) => {
return request.delete(`/api/v1/manager/courses/${courseId}`)
}
/**
* 上传课程材料
*/
export const uploadCourseMaterial = (courseId: number, file: File, title?: string) => {
const formData = new FormData()
formData.append('file', file)
if (title) {
formData.append('title', title)
}
return request.post<CourseMaterial>(`/api/v1/manager/courses/${courseId}/materials`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
/**
* 删除课程材料
*/
export const deleteCourseMaterial = (courseId: number, materialId: number) => {
return request.delete(`/api/v1/manager/courses/${courseId}/materials/${materialId}`)
}
/**
* 分配课程到岗位
*/
export const assignCourseToPositions = (courseId: number, positionIds: number[]) => {
return request.post(`/api/v1/manager/courses/${courseId}/assign-positions`, { positionIds })
}
/**
* AI分析课程知识点
*/
export const analyzeKnowledgePoints = (courseId: number) => {
return request.post(`/api/v1/manager/courses/${courseId}/analyze-knowledge`)
}
/**
* 获取成长路径配置列表
*/
export const getGrowthPathConfigs = (params: {
page?: number
size?: number
positionId?: number
status?: string
} = {}) => {
return request.get<{
items: GrowthPathConfig[]
total: number
page: number
size: number
}>('/api/v1/manager/growth-paths', { params })
}
/**
* 创建成长路径
*/
export const createGrowthPath = (data: {
positionId: number
name: string
description?: string
courses: Array<{
courseId: number
isRequired: boolean
prerequisites?: number[]
estimatedDays?: number
}>
}) => {
return request.post<GrowthPathConfig>('/api/v1/manager/growth-paths', data)
}
/**
* 更新成长路径
*/
export const updateGrowthPath = (pathId: number, data: {
name?: string
description?: string
courses?: Array<{
courseId: number
isRequired: boolean
prerequisites?: number[]
estimatedDays?: number
}>
}) => {
return request.put<GrowthPathConfig>(`/api/v1/manager/growth-paths/${pathId}`, data)
}
/**
* 获取AI陪练场景列表管理视图
*/
export const getManagerPracticeScenes = (params: {
page?: number
size?: number
category?: string
difficulty?: string
status?: string
keyword?: string
} = {}) => {
return request.get<{
items: PracticeSceneForManager[]
total: number
page: number
size: number
}>('/api/v1/manager/practice-scenes', { params })
}
/**
* 创建AI陪练场景
*/
export const createPracticeScene = (data: CreatePracticeSceneRequest) => {
return request.post<PracticeSceneForManager>('/api/v1/manager/practice-scenes', data)
}
/**
* 更新AI陪练场景
*/
export const updatePracticeScene = (sceneId: number, data: Partial<CreatePracticeSceneRequest>) => {
return request.put<PracticeSceneForManager>(`/api/v1/manager/practice-scenes/${sceneId}`, data)
}
/**
* 删除AI陪练场景
*/
export const deletePracticeScene = (sceneId: number) => {
return request.delete(`/api/v1/manager/practice-scenes/${sceneId}`)
}
/**
* 启用/禁用AI陪练场景
*/
export const togglePracticeSceneStatus = (sceneId: number, status: 'active' | 'inactive') => {
return request.post(`/api/v1/manager/practice-scenes/${sceneId}/toggle-status`, { status })
}
/**
* 复制AI陪练场景
*/
export const copyPracticeScene = (sceneId: number) => {
return request.post<PracticeSceneForManager>(`/api/v1/manager/practice-scenes/${sceneId}/copy`)
}
/**
* 获取学员考试成绩列表
*/
export const getStudentExamScores = (params: {
page?: number
size?: number
studentId?: number
teamId?: number
examType?: string
subject?: string
startDate?: string
endDate?: string
keyword?: string
} = {}) => {
return request.get<{
items: StudentExamScore[]
total: number
page: number
size: number
statistics: {
averageScore: number
passRate: number
totalExams: number
}
}>('/api/v1/manager/student-scores', { params })
}
/**
* 获取学员陪练记录列表
*/
export const getStudentPracticeRecords = (params: {
page?: number
size?: number
studentId?: number
teamId?: number
sceneId?: number
result?: string
startDate?: string
endDate?: string
keyword?: string
} = {}) => {
return request.get<{
items: StudentPracticeRecord[]
total: number
page: number
size: number
statistics: {
totalSessions: number
averageScore: number
averageDuration: number
}
}>('/api/v1/manager/student-practice', { params })
}

View File

@@ -0,0 +1,83 @@
/**
* 管理员 - 学员陪练记录API
*/
import request from '@/api/request'
/**
* 学员陪练记录查询参数
*/
export interface StudentPracticeParams {
page: number
size: number
student_name?: string
position?: string
scene_type?: string
result?: string // excellent/good/average/needs_improvement
start_date?: string
end_date?: string
}
/**
* 学员陪练记录
*/
export interface StudentPracticeRecord {
id: number
student_id: number
student_name: string
position: string | null
session_id: string
scene_name: string
scene_type: string
duration_seconds: number
round_count: number
score: number | null
result: string
practice_time: string | null
}
/**
* 陪练统计数据
*/
export interface PracticeStatistics {
total_count: number
avg_score: number
total_duration_hours: number
excellent_rate: number
}
/**
* 获取学员陪练记录列表
*/
export function getStudentPracticeRecords(params: StudentPracticeParams) {
return request.get<{
items: StudentPracticeRecord[]
total: number
page: number
page_size: number
pages: number
}>('/api/v1/manager/student-practice', params)
}
/**
* 获取学员陪练统计
*/
export function getStudentPracticeStatistics() {
return request.get<PracticeStatistics>('/api/v1/manager/student-practice/statistics')
}
/**
* 获取指定会话的对话记录
*/
export function getSessionConversation(sessionId: string) {
return request.get<{
session_id: string
conversation: Array<{
role: string
content: string
timestamp: string
sequence: number
}>
total_count: number
}>(`/api/v1/manager/student-practice/${sessionId}/conversation`)
}

View File

@@ -0,0 +1,116 @@
/**
* 管理员 - 学员考试成绩API
*/
import request from '@/api/request'
/**
* 学员考试成绩查询参数
*/
export interface StudentScoresParams {
page: number
size: number
student_name?: string
position?: string
course_id?: number
score_range?: string // excellent/good/pass/fail
start_date?: string
end_date?: string
}
/**
* 学员考试成绩记录
*/
export interface StudentScoreRecord {
id: number
student_id: number
student_name: string
position: string | null
course_id: number
course_name: string
exam_type: string
score: number
round1_score: number | null
round2_score: number | null
round3_score: number | null
total_score: number
accuracy: number | null
correct_count: number | null
wrong_count: number
total_count: number
duration_seconds: number | null
exam_date: string | null
}
/**
* 错题记录
*/
export interface MistakeRecord {
id: number
question_content: string
correct_answer: string
user_answer: string
question_type: string
analysis: string
created_at: string | null
}
/**
* 考试成绩统计数据
*/
export interface ScoresStatistics {
total_exams: number
avg_score: number
pass_rate: number
excellent_rate: number
}
/**
* 获取学员考试成绩列表
*/
export function getStudentScores(params: StudentScoresParams) {
return request.get<{
items: StudentScoreRecord[]
total: number
page: number
page_size: number
pages: number
}>('/api/v1/manager/student-scores', { params })
}
/**
* 获取学员考试成绩统计
*/
export function getStudentScoresStatistics() {
return request.get<ScoresStatistics>('/api/v1/manager/student-scores/statistics')
}
/**
* 获取考试的错题记录
*/
export function getExamMistakes(examId: number) {
return request.get<{
items: MistakeRecord[]
total: number
page: number
page_size: number
pages: number
}>(`/api/v1/manager/student-scores/${examId}/mistakes`)
}
/**
* 删除单条考试记录
*/
export function deleteExamRecord(examId: number) {
return request.delete<{ deleted_id: number }>(`/api/v1/manager/student-scores/${examId}`)
}
/**
* 批量删除考试记录
*/
export function batchDeleteExamRecords(ids: number[]) {
return request.delete<{ deleted_count: number; deleted_ids: number[] }>(
'/api/v1/manager/student-scores/batch/delete',
{ data: { ids } }
)
}

View File

@@ -0,0 +1,72 @@
/**
* 课程资料相关API
*/
import request from '@/utils/http'
import type { Material, PreviewInfo } from '@/types/material'
/**
* 获取课程资料列表
*/
export function getMaterials(courseId: number) {
return request.get<{
code: number
message: string
data: Material[]
}>(`/api/v1/courses/${courseId}/materials`)
}
/**
* 获取资料预览信息
*/
export function getPreview(materialId: number) {
return request.get<{
code: number
message: string
data: PreviewInfo
}>(`/api/v1/preview/material/${materialId}`)
}
/**
* 下载资料
*/
export function downloadMaterial(materialId: number, fileName: string) {
// 创建隐藏的a标签触发下载
const link = document.createElement('a')
link.style.display = 'none'
link.href = `/api/v1/preview/material/${materialId}`
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* 直接下载文件通过URL
*/
export function downloadFile(fileUrl: string, fileName: string) {
const link = document.createElement('a')
link.style.display = 'none'
link.href = fileUrl
link.download = fileName
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* 检查转换服务状态(调试用)
*/
export function checkConverterStatus() {
return request.get<{
code: number
message: string
data: {
libreoffice_installed: boolean
libreoffice_version: string | null
supported_formats: string[]
converted_path: string
}
}>('/api/v1/preview/check-converter')
}

View File

@@ -0,0 +1,12 @@
/**
* 课程完成率数据模拟
*/
import type { CourseCompletionData } from '../admin/dashboard'
const mockData: CourseCompletionData = {
courses: ['销售技巧', '产品知识', '客户服务', '商务礼仪', '沟通技巧'],
completionRates: [85, 92, 78, 88, 75]
}
export default mockData

View File

@@ -0,0 +1,30 @@
/**
* 管理员仪表盘统计数据模拟
*/
import type { DashboardStats } from '../admin/dashboard'
const mockData: DashboardStats = {
users: {
total: 1234,
growth: 156,
growthRate: '+12.5%'
},
courses: {
total: 48,
completed: 892,
completionRate: '78.5%'
},
exams: {
total: 326,
avgScore: 82.5,
passRate: '85.2%'
},
learning: {
totalHours: 4567,
avgHours: 3.7,
activeRate: '76.8%'
}
}
export default mockData

View File

@@ -0,0 +1,32 @@
/**
* 用户增长数据模拟
*/
import type { UserGrowthData } from '../admin/dashboard'
// 生成最近30天的日期
const generateDates = () => {
const dates: string[] = []
const today = new Date()
for (let i = 29; i >= 0; i--) {
const date = new Date(today)
date.setDate(date.getDate() - i)
dates.push(date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }))
}
return dates
}
// 生成模拟数据
const generateData = (base: number, variance: number) => {
return Array.from({ length: 30 }, () =>
base + Math.floor(Math.random() * variance * 2 - variance)
)
}
const mockData: UserGrowthData = {
dates: generateDates(),
newUsers: generateData(50, 20),
activeUsers: generateData(300, 50)
}
export default mockData

View File

@@ -0,0 +1,96 @@
/**
* 岗位树形结构模拟数据
*/
import type { Position } from '../admin/position'
const mockData: Position[] = [
{
id: 1,
name: '总经理',
code: 'GM',
parentId: null,
memberCount: 1,
description: '公司最高管理者',
status: 'active',
createTime: '2024-01-01 10:00:00',
children: [
{
id: 2,
name: '销售部',
code: 'SALES',
parentId: 1,
memberCount: 25,
description: '负责产品销售与市场拓展',
status: 'active',
createTime: '2024-01-05 14:30:00',
children: [
{
id: 4,
name: '销售经理',
code: 'SALES_MGR',
parentId: 2,
memberCount: 3,
description: '管理销售团队,制定销售策略',
status: 'active',
createTime: '2024-01-10 09:00:00'
},
{
id: 5,
name: '销售专员',
code: 'SALES_SPEC',
parentId: 2,
memberCount: 20,
description: '执行销售任务,维护客户关系',
status: 'active',
createTime: '2024-01-10 09:10:00'
}
]
},
{
id: 3,
name: '技术部',
code: 'TECH',
parentId: 1,
memberCount: 30,
description: '负责技术研发与系统维护',
status: 'active',
createTime: '2024-01-05 14:35:00',
children: [
{
id: 6,
name: '前端开发',
code: 'FE_DEV',
parentId: 3,
memberCount: 10,
description: '负责前端应用开发',
status: 'active',
createTime: '2024-01-10 09:20:00'
},
{
id: 7,
name: '后端开发',
code: 'BE_DEV',
parentId: 3,
memberCount: 12,
description: '负责后端服务开发',
status: 'active',
createTime: '2024-01-10 09:25:00'
},
{
id: 8,
name: '测试工程师',
code: 'QA',
parentId: 3,
memberCount: 8,
description: '负责软件质量保证',
status: 'active',
createTime: '2024-01-10 09:30:00'
}
]
}
]
}
]
export default mockData

View File

@@ -0,0 +1,183 @@
/**
* 岗位列表模拟数据
*/
import type { Position } from '../admin/position'
import type { PageResponse } from '../config'
// 扩展 Position 接口以包含页面所需字段
interface ExtendedPosition extends Position {
requiredCourses?: number
optionalCourses?: number
skills?: string[]
avgProgress?: number
avgScore?: number
updateTime?: string
}
const positions: ExtendedPosition[] = [
{
id: 1,
name: '总经理',
code: 'GM',
parentId: null,
memberCount: 1,
description: '公司最高管理者',
status: 'active',
createTime: '2024-01-01 10:00:00',
requiredCourses: 20,
optionalCourses: 15,
skills: ['战略规划', '团队管理', '决策能力', '沟通协调'],
avgProgress: 95,
avgScore: 92.5,
updateTime: '2024-03-20 14:30:00'
},
{
id: 2,
name: '美容部',
code: 'BEAUTY',
parentId: 1,
parentName: '总经理',
memberCount: 25,
description: '负责美容项目和客户服务',
status: 'active',
createTime: '2024-01-05 14:30:00',
requiredCourses: 15,
optionalCourses: 10,
skills: ['美容咨询', '项目管理', '客户服务', '团队管理'],
avgProgress: 82,
avgScore: 86.7,
updateTime: '2024-03-18 16:20:00'
},
{
id: 3,
name: '医美部',
code: 'MEDICAL',
parentId: 1,
parentName: '总经理',
memberCount: 30,
description: '负责轻医美项目和医美技术支持',
status: 'active',
createTime: '2024-01-05 14:35:00',
requiredCourses: 18,
optionalCourses: 20,
skills: ['医美技术', '项目管理', '安全管理', '团队协作'],
avgProgress: 88,
avgScore: 90.2,
updateTime: '2024-03-19 10:15:00'
},
{
id: 4,
name: '美容顾问',
code: 'BEAUTY_CONSULTANT',
parentId: 2,
parentName: '美容部',
memberCount: 3,
description: '为客户提供专业美容咨询和方案建议',
status: 'active',
createTime: '2024-01-10 09:00:00',
requiredCourses: 15,
optionalCourses: 10,
skills: ['美容知识', '咨询技巧', '客户管理', '沟通技巧'],
avgProgress: 85,
avgScore: 88.3,
updateTime: '2024-03-10 11:45:00'
},
{
id: 5,
name: '美容师',
code: 'BEAUTICIAN',
parentId: 2,
parentName: '美容部',
memberCount: 20,
description: '为客户提供专业美容护理和治疗服务',
status: 'active',
createTime: '2024-01-10 09:10:00',
requiredCourses: 12,
optionalCourses: 8,
skills: ['美容技能', '操作技术', '产品知识', '客户服务'],
avgProgress: 78,
avgScore: 82.5,
updateTime: '2024-03-18 15:30:00'
},
{
id: 6,
name: '医美医生',
code: 'DOCTOR',
parentId: 3,
parentName: '医美部',
memberCount: 10,
description: '负责医美项目的医学指导和技术支持',
status: 'active',
createTime: '2024-01-10 09:20:00',
requiredCourses: 10,
optionalCourses: 15,
skills: ['医学知识', '美容技术', '安全操作', '效果评估'],
avgProgress: 85,
avgScore: 87.6,
updateTime: '2024-03-20 09:00:00'
},
{
id: 7,
name: '美容技师',
code: 'TECHNICIAN',
parentId: 3,
parentName: '医美部',
memberCount: 12,
description: '负责美容设备操作和技术支持',
status: 'active',
createTime: '2024-01-10 09:25:00',
requiredCourses: 12,
optionalCourses: 18,
skills: ['设备操作', '技术支持', '安全管理', '效果监控'],
avgProgress: 90,
avgScore: 91.3,
updateTime: '2024-03-19 17:20:00'
},
{
id: 8,
name: '客户服务专员',
code: 'SERVICE',
parentId: 3,
parentName: '医美部',
memberCount: 8,
description: '负责客户服务和售后支持',
status: 'active',
createTime: '2024-01-10 09:30:00',
requiredCourses: 8,
optionalCourses: 10,
skills: ['客户服务', '售后支持', '问题处理', '沟通技巧'],
avgProgress: 82,
avgScore: 85.9,
updateTime: '2024-03-21 13:40:00'
}
]
// 模拟分页和搜索
const mockData = (params?: any): PageResponse<Position> => {
const { page = 1, pageSize = 10, keyword = '' } = params || {}
// 搜索过滤
let filteredList = positions
if (keyword) {
filteredList = positions.filter(item =>
item.name.includes(keyword) ||
item.code.includes(keyword) ||
item.description.includes(keyword)
)
}
// 分页
const start = (page - 1) * pageSize
const end = start + pageSize
const list = filteredList.slice(start, end)
return {
list,
total: filteredList.length,
page,
pageSize
}
}
export default mockData

View File

@@ -0,0 +1,29 @@
/**
* 重置密码响应模拟
*/
import type { PasswordResetResponse } from '../admin/user'
// 生成随机临时密码
const generateTempPassword = () => {
const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
let result = ''
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return 'Kpl' + result
}
// 生成过期时间24小时后
const generateExpiresAt = () => {
const now = new Date()
now.setHours(now.getHours() + 24)
return now.toLocaleString('zh-CN')
}
const mockData: PasswordResetResponse = {
tempPassword: generateTempPassword(),
expiresAt: generateExpiresAt()
}
export default mockData

View File

@@ -0,0 +1,12 @@
/**
* 用户统计数据模拟
*/
const mockData = {
total: 50,
activeCount: 42,
pendingCount: 5,
inactiveCount: 3
}
export default mockData

View File

@@ -0,0 +1,133 @@
/**
* 用户列表模拟数据
*/
import type { User } from '../admin/user'
import type { PageResponse } from '../config'
const users: User[] = [
{
id: 1,
username: 'zhangsan',
realName: '张三',
email: 'zhangsan@example.com',
phone: '13800138001',
position: '销售经理',
department: '销售部',
role: '管理员',
status: 'active',
lastLoginTime: '2024-03-15 10:30:00',
createTime: '2024-01-10 09:00:00'
},
{
id: 2,
username: 'lisi',
realName: '李四',
email: 'lisi@example.com',
phone: '13800138002',
position: '销售专员',
department: '销售部',
role: '学员',
status: 'active',
lastLoginTime: '2024-03-15 09:15:00',
createTime: '2024-01-15 14:20:00'
},
{
id: 3,
username: 'wangwu',
realName: '王五',
email: 'wangwu@example.com',
phone: '13800138003',
position: '前端开发',
department: '技术部',
role: '学员',
status: 'pending',
lastLoginTime: '',
createTime: '2024-02-01 11:30:00'
},
{
id: 4,
username: 'zhaoliu',
realName: '赵六',
email: 'zhaoliu@example.com',
phone: '13800138004',
position: '后端开发',
department: '技术部',
role: '讲师',
status: 'active',
lastLoginTime: '2024-03-14 16:45:00',
createTime: '2024-01-20 10:00:00'
},
{
id: 5,
username: 'qianqi',
realName: '钱七',
email: 'qianqi@example.com',
phone: '13800138005',
position: '测试工程师',
department: '技术部',
role: '学员',
status: 'inactive',
lastLoginTime: '2024-02-28 14:20:00',
createTime: '2024-01-25 15:30:00'
}
]
// 生成更多模拟数据
for (let i = 6; i <= 50; i++) {
users.push({
id: i,
username: `user${i}`,
realName: `用户${i}`,
email: `user${i}@example.com`,
phone: `1380013${8000 + i}`,
position: ['销售专员', '技术支持', '客服专员', '产品经理'][i % 4],
department: ['销售部', '技术部', '客服部', '产品部'][i % 4],
role: ['学员', '讲师', '管理员'][i % 3],
status: ['active', 'inactive', 'pending'][i % 3] as any,
lastLoginTime: i % 3 === 0 ? '' : new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toLocaleString('zh-CN'),
createTime: new Date(Date.now() - Math.random() * 60 * 24 * 60 * 60 * 1000).toLocaleString('zh-CN')
})
}
// 模拟分页、搜索和筛选
const mockData = (params?: any): PageResponse<User> => {
const { page = 1, pageSize = 10, keyword = '', status = '', role = '' } = params || {}
// 筛选
let filteredList = users
// 关键词搜索
if (keyword) {
filteredList = filteredList.filter(item =>
item.username.includes(keyword) ||
item.realName.includes(keyword) ||
item.email.includes(keyword) ||
item.phone.includes(keyword)
)
}
// 状态筛选
if (status) {
filteredList = filteredList.filter(item => item.status === status)
}
// 角色筛选
if (role) {
filteredList = filteredList.filter(item => item.role === role)
}
// 分页
const start = (page - 1) * pageSize
const end = start + pageSize
const list = filteredList.slice(start, end)
return {
list,
total: filteredList.length,
page,
pageSize
}
}
export default mockData

View File

@@ -0,0 +1,144 @@
/**
* 站内消息通知 API
* 提供通知的查询、标记已读、删除等功能
*/
import { request } from './request'
/**
* 通知类型枚举
*/
export type NotificationType =
| 'position_assign' // 岗位分配
| 'course_assign' // 课程分配
| 'exam_remind' // 考试提醒
| 'task_assign' // 任务分配
| 'system' // 系统通知
/**
* 通知响应接口
*/
export interface Notification {
id: number
user_id: number
title: string
content?: string
type: NotificationType
is_read: boolean
related_id?: number
related_type?: string
sender_id?: number
sender_name?: string
created_at: string
updated_at: string
}
/**
* 通知列表响应接口
*/
export interface NotificationListResponse {
items: Notification[]
total: number
unread_count: number
}
/**
* 未读数量响应接口
*/
export interface NotificationCountResponse {
unread_count: number
total: number
}
/**
* 创建通知请求接口(管理员使用)
*/
export interface NotificationCreateRequest {
user_id: number
title: string
content?: string
type?: NotificationType
related_id?: number
related_type?: string
}
/**
* 批量创建通知请求接口(管理员使用)
*/
export interface NotificationBatchCreateRequest {
user_ids: number[]
title: string
content?: string
type?: NotificationType
related_id?: number
related_type?: string
}
/**
* 标记已读请求接口
*/
export interface MarkReadRequest {
notification_ids?: number[]
}
/**
* 通知 API
*/
export const notificationApi = {
/**
* 获取当前用户的通知列表
* @param params 查询参数
*/
getNotifications(params?: {
is_read?: boolean
type?: NotificationType
page?: number
page_size?: number
}) {
return request.get<NotificationListResponse>('/api/v1/notifications', { params })
},
/**
* 获取未读通知数量
*/
getUnreadCount() {
return request.get<NotificationCountResponse>('/api/v1/notifications/unread-count')
},
/**
* 标记通知为已读
* @param notification_ids 通知ID列表不传则标记全部
*/
markAsRead(notification_ids?: number[]) {
return request.post('/api/v1/notifications/mark-read', { notification_ids })
},
/**
* 删除单条通知
* @param notificationId 通知ID
*/
deleteNotification(notificationId: number) {
return request.delete(`/api/v1/notifications/${notificationId}`)
},
/**
* 发送单条通知(管理员接口)
* @param data 通知数据
*/
sendNotification(data: NotificationCreateRequest) {
return request.post<Notification>('/api/v1/notifications/send', data)
},
/**
* 批量发送通知(管理员接口)
* @param data 批量通知数据
*/
sendBatchNotifications(data: NotificationBatchCreateRequest) {
return request.post('/api/v1/notifications/send-batch', data)
}
}
export default notificationApi

View File

@@ -0,0 +1,190 @@
/**
* 陪练功能API
*/
import { http } from '@/utils/http'
import type {
GetScenesParams,
StartPracticeParams,
InterruptPracticeParams,
PracticeScene,
PaginatedResponse,
ResponseModel
} from '@/types/practice'
/**
* 陪练API
*/
export const practiceApi = {
/**
* 获取Coze OAuth Token用于前端直连WebSocket
*/
getCozeToken(): Promise<ResponseModel<{ token: string }>> {
return http.get('/api/v1/practice/coze-token')
},
/**
* 获取可用场景列表
*/
getScenes(params: GetScenesParams): Promise<ResponseModel<PaginatedResponse<PracticeScene>>> {
return http.get('/api/v1/practice/scenes', { params })
},
/**
* 获取场景详情
*/
getSceneDetail(id: number): Promise<ResponseModel<PracticeScene>> {
return http.get(`/api/v1/practice/scenes/${id}`)
},
/**
* 开始陪练对话SSE流式
*
* ⚠️ 注意此方法返回ReadableStream不能使用普通的request封装
* 必须使用原生fetch处理SSE
*/
startPractice(params: StartPracticeParams): Promise<ReadableStream> {
const token = localStorage.getItem('access_token')
// 生产环境使用当前域名,开发环境使用空字符串(走代理)
const baseURL = (import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'development')
? ''
: window.location.origin
return fetch(`${baseURL}/api/v1/practice/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(params)
}).then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
if (!response.body) {
throw new Error('Response body is null')
}
return response.body
})
},
/**
* 创建对话
*
* ⚠️ 重要必须先创建conversation才能续接对话
*/
createConversation(): Promise<ResponseModel<{ conversation_id: string }>> {
return http.post('/api/v1/practice/conversation/create')
},
/**
* 中断对话
*/
interruptPractice(params: InterruptPracticeParams): Promise<ResponseModel> {
return http.post('/api/v1/practice/interrupt', params)
},
/**
* 从课程提取陪练场景通过Dify工作流
*
* @param data 包含course_id的请求数据
* @returns Promise 包含提取的场景数据
*/
extractScene(data: { course_id: number }): Promise<ResponseModel<{
scene: {
name: string
description: string
type: string
difficulty: string
background: string
ai_role: string
objectives: string[]
keywords: string[]
}
workflow_run_id: string
task_id: string
}>> {
return http.post('/api/v1/practice/extract-scene', data)
},
/**
* 创建陪练会话
*/
createSession(data: {
scene_id?: number
scene_name: string
scene_type?: string
conversation_id?: string
}): Promise<ResponseModel<{
session_id: string
user_id: number
start_time: string
}>> {
return http.post('/api/v1/practice/sessions/create', data)
},
/**
* 保存对话记录
*/
saveDialogue(data: {
session_id: string
speaker: string
content: string
sequence: number
}): Promise<ResponseModel> {
return http.post('/api/v1/practice/dialogues/save', data)
},
/**
* 结束陪练会话
*/
endSession(sessionId: string): Promise<ResponseModel> {
return http.post(`/api/v1/practice/sessions/${sessionId}/end`)
},
/**
* 生成陪练分析报告调用Dify
*/
analyzePractice(sessionId: string): Promise<ResponseModel<{
session_id: string
total_score: number
workflow_run_id: string
}>> {
return http.post(`/api/v1/practice/sessions/${sessionId}/analyze`)
},
/**
* 获取陪练分析报告详情
*/
getPracticeReport(sessionId: string): Promise<ResponseModel<any>> {
return http.get(`/api/v1/practice/reports/${sessionId}`)
},
/**
* 获取陪练记录列表
*/
getPracticeSessions(params: {
page?: number
size?: number
keyword?: string
scene_type?: string
start_date?: string
end_date?: string
min_score?: number
max_score?: number
}): Promise<ResponseModel<any>> {
return http.get('/api/v1/practice/sessions/list', { params })
},
/**
* 获取陪练统计数据
*/
getPracticeStats(): Promise<ResponseModel<{
total_count: number
avg_score: number
total_duration_hours: number
month_improvement: number
}>> {
return http.get('/api/v1/practice/stats')
}
}

View File

@@ -0,0 +1,117 @@
/**
* 陪练场景管理 API
*/
import request from '@/api/request'
/**
* 陪练场景数据结构
*/
export interface PracticeScene {
id: number
name: string
description?: string
type: string
difficulty: string
status: string
background: string
ai_role: string
objectives: string[]
keywords?: string[]
duration: number
usage_count?: number
rating?: number
created_at?: string
updated_at?: string
}
/**
* 创建场景请求
*/
export interface CreateSceneRequest {
name: string
description?: string
type: string
difficulty: string
status?: string
background: string
ai_role: string
objectives: string[]
keywords?: string[]
duration: number
}
/**
* 更新场景请求
*/
export interface UpdateSceneRequest {
name?: string
description?: string
type?: string
difficulty?: string
status?: string
background?: string
ai_role?: string
objectives?: string[]
keywords?: string[]
duration?: number
}
/**
* 场景列表查询参数
*/
export interface SceneQueryParams {
page?: number
size?: number
type?: string
difficulty?: string
search?: string
}
/**
* 场景列表响应
*/
export interface SceneListResponse {
items: PracticeScene[]
total: number
page: number
size: number
pages: number
}
/**
* 获取陪练场景列表
*/
export function getPracticeScenes(params: SceneQueryParams) {
return request.get<SceneListResponse>('/api/v1/practice/scenes', params)
}
/**
* 获取场景详情
*/
export function getSceneDetail(sceneId: number) {
return request.get<PracticeScene>(`/api/v1/practice/scenes/${sceneId}`)
}
/**
* 创建陪练场景
*/
export function createPracticeScene(data: CreateSceneRequest) {
return request.post<PracticeScene>('/api/v1/practice/scenes', data)
}
/**
* 更新陪练场景
*/
export function updatePracticeScene(sceneId: number, data: UpdateSceneRequest) {
return request.put<PracticeScene>(`/api/v1/practice/scenes/${sceneId}`, data)
}
/**
* 删除陪练场景
*/
export function deletePracticeScene(sceneId: number) {
return request.delete<{ scene_id: number }>(`/api/v1/practice/scenes/${sceneId}`)
}

273
frontend/src/api/request.ts Normal file
View File

@@ -0,0 +1,273 @@
/**
* 请求封装
* 统一处理请求和响应,支持模拟数据和真实接口切换
*/
import { API_CONFIG, ApiResponse } from './config'
import { handleHttpError } from '@/utils/errorHandler'
import { loadingManager } from '@/utils/loadingManager'
// 模拟延迟,使体验更真实
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// 扩展RequestInit接口以支持transformRequest
interface ExtendedRequestInit extends RequestInit {
transformRequest?: Array<(data: any, headers?: any) => any>
}
class Request {
/**
* 通用请求方法
* @param url 请求路径
* @param options 请求选项
* @param showLoading 是否显示加载状态
* @returns Promise<ApiResponse<T>>
*/
async request<T = any>(
url: string,
options: ExtendedRequestInit = {},
showLoading: boolean = false
): Promise<ApiResponse<T>> {
const loadingKey = `api-${url}-${options.method || 'GET'}`
if (showLoading) {
loadingManager.start(loadingKey, {
message: '请求中...',
background: 'rgba(0, 0, 0, 0.3)'
})
}
// 添加认证头
const token = localStorage.getItem('access_token')
if (token && !url.includes('/auth/login') && !url.includes('/auth/register')) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
}
}
try {
// 如果使用模拟数据,直接返回模拟数据
if (API_CONFIG.useMockData) {
// 模拟网络延迟
await delay(Math.random() * 500 + 200)
// 动态导入对应的模拟数据
const mockModule = await this.getMockData(url, options.method || 'GET')
if (mockModule) {
return {
code: 200,
message: '操作成功',
data: mockModule
}
}
// 如果没有模拟数据,抛出错误
throw new Error(`未找到模拟数据: ${url}`)
}
// 真实 API 请求
// 强制使用配置的基础URL不使用代理
let fullUrl = url.startsWith('http') ? url : `${API_CONFIG.baseURL}${url.startsWith('/') ? url : '/' + url}`
// 生产环境安全检查:强制升级 HTTP 为 HTTPS
if (fullUrl.startsWith('http://') && !fullUrl.includes('localhost') && !fullUrl.includes('127.0.0.1')) {
fullUrl = fullUrl.replace('http://', 'https://')
console.warn('[安全] 请求 URL 已自动升级为 HTTPS:', fullUrl)
}
const response = await fetch(fullUrl, {
...options,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json; charset=utf-8',
...options.headers
}
})
// 确保正确解码 UTF-8 响应
const text = await response.text()
let data: any
try {
data = JSON.parse(text)
} catch {
data = { detail: text || `请求失败: ${response.status}` }
}
if (!response.ok) {
// 解析后端返回的错误详情
const errorDetail = data?.detail || data?.message || `请求失败: ${response.status}`
const error = new Error(typeof errorDetail === 'string' ? errorDetail : JSON.stringify(errorDetail)) as any
error.status = response.status
error.detail = errorDetail
error.response = { data, status: response.status }
throw error
}
return data
} catch (error) {
// 处理HTTP错误
const errorInfo = handleHttpError(error)
// 401 统一处理:清理本地状态并跳转登录
try {
const status = (errorInfo as any)?.status || (error as any)?.status
if (status === 401) {
console.warn('[Auth] Token过期或无效正在清理认证状态', { url, status })
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('current_user')
// 避免死循环,仅在非登录页执行
if (!location.pathname.startsWith('/login')) {
console.info('[Auth] 重定向到登录页')
location.href = '/login'
}
}
} catch (authError) {
// 认证处理过程中的异常不应影响主流程,但需要记录
console.error('[Auth] 处理401错误时发生异常:', authError)
}
throw errorInfo
} finally {
if (showLoading) {
loadingManager.stop(loadingKey)
}
}
}
/**
* GET 请求
*/
get<T = any>(url: string, options?: { params?: Record<string, any> }, showLoading?: boolean): Promise<ApiResponse<T>> {
// 提取params参数
const params = options?.params || {}
// 过滤掉 undefined 和 null 的参数
const filteredParams = Object.fromEntries(
Object.entries(params).filter(([_, value]) => value !== undefined && value !== null)
)
const queryString = Object.keys(filteredParams).length > 0
? '?' + new URLSearchParams(filteredParams as any).toString()
: ''
return this.request<T>(url + queryString, { method: 'GET' }, showLoading)
}
/**
* POST 请求
*/
post<T = any>(url: string, data?: any, options?: ExtendedRequestInit, showLoading?: boolean): Promise<ApiResponse<T>> {
let body = data
let headers = { 'Content-Type': 'application/json', ...options?.headers }
// 处理 transformRequest
if (options?.transformRequest && Array.isArray(options.transformRequest)) {
for (const transform of options.transformRequest) {
if (typeof transform === 'function') {
body = transform(data)
}
}
} else if (data && typeof data === 'object' && !(data instanceof FormData) && !(data instanceof URLSearchParams)) {
body = JSON.stringify(data)
}
return this.request<T>(url, {
method: 'POST',
body,
...options,
headers
}, showLoading)
}
/**
* PUT 请求
*/
put<T = any>(url: string, data?: any, showLoading?: boolean): Promise<ApiResponse<T>> {
return this.request<T>(url, {
method: 'PUT',
body: JSON.stringify(data)
}, showLoading)
}
/**
* DELETE 请求
*/
delete<T = any>(url: string, showLoading?: boolean): Promise<ApiResponse<T>> {
return this.request<T>(url, { method: 'DELETE' }, showLoading)
}
/**
* 文件上传请求
*/
upload<T = any>(url: string, file: File, showLoading?: boolean): Promise<ApiResponse<T>> {
const formData = new FormData()
formData.append('file', file)
return this.request<T>(url, {
method: 'POST',
body: formData,
headers: {
// 不设置 Content-Type让浏览器自动设置包含 boundary
}
}, showLoading)
}
/**
* 获取模拟数据
* @private
*/
private async getMockData(url: string, _method: string) {
// 根据 URL 和 method 动态加载对应的模拟数据
const mockPath = this.parseMockPath(url, _method)
try {
const module = await import(`./mock/${mockPath}.ts`)
let mockData = module.default || module[_method.toLowerCase()]
// 如果模拟数据是函数,则调用它
if (typeof mockData === 'function') {
// 解析查询参数
const urlObj = new URL(url, 'http://localhost')
const params: any = {}
urlObj.searchParams.forEach((value, key) => {
params[key] = value
})
mockData = mockData(params)
}
return mockData
} catch (error) {
console.warn(`未找到模拟数据: ${mockPath}`, error)
return null
}
}
/**
* 解析模拟数据路径
* @private
*/
private parseMockPath(url: string, _method: string): string {
// 移除查询参数
const cleanUrl = url.split('?')[0]
// 移除开头的斜杠
const path = cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl
// 将路径转换为模拟数据文件路径
return path.replace(/\//g, '-')
}
}
// 导出请求实例
export const request = new Request()
// 导出便捷方法
export const get = (url: string, params?: Record<string, any>, showLoading?: boolean) =>
request.get(url, params ? { params } : undefined, showLoading)
export const post = (url: string, data?: any, showLoading?: boolean) =>
request.post(url, data, undefined, showLoading)
export const put = (url: string, data?: any, showLoading?: boolean) =>
request.put(url, data, showLoading)
export const del = (url: string, showLoading?: boolean) =>
request.delete(url, showLoading)
export default request

126
frontend/src/api/score.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* 成绩查询相关API
*/
import request from './request'
/**
* 考试记录列表参数
*/
export interface ExamRecordsParams {
page: number
size: number
course_id?: number
start_date?: string
end_date?: string
}
/**
* 题型统计
*/
export interface QuestionTypeStat {
type: string
type_code: string
total: number
correct: number
wrong: number
accuracy: number
total_score: number
}
/**
* 考试记录
*/
export interface ExamRecord {
id: number
course_id: number
course_name: string
exam_name: string
question_count: number
total_score: number
score: number | null
is_passed: boolean | null
status: string
start_time: string | null
end_time: string | null
created_at: string
// 统计字段
accuracy: number | null
correct_count: number | null
wrong_count: number | null
duration_seconds: number | null
question_type_stats: QuestionTypeStat[]
}
/**
* 考试记录列表响应
*/
export interface ExamRecordsResponse {
items: ExamRecord[]
total: number
page: number
size: number
pages: number
}
/**
* 考试统计概览
*/
export interface ExamStatistics {
total_exams: number
avg_score: number
pass_rate: number
total_questions: number
}
/**
* 考试详情
*/
export interface ExamDetail {
id: number
course_id: number
exam_name: string
question_count: number
total_score: number
pass_score: number
start_time: string | null
end_time: string | null
duration_minutes: number
status: string
score: number | null
is_passed: boolean | null
questions: any
answers: any
}
/**
* 获取考试记录列表
*/
export function getExamRecords(params: ExamRecordsParams) {
return request.get<ExamRecordsResponse>('/api/v1/exams/records', params)
}
/**
* 获取考试统计概览
*/
export function getExamStatistics(params?: { course_id?: number }) {
return request.get<ExamStatistics>('/api/v1/exams/statistics/summary', params)
}
/**
* 获取考试详情
*/
export function getExamDetail(examId: number) {
return request.get<ExamDetail>(`/api/v1/exams/${examId}`)
}
/**
* 获取课程列表(用于筛选下拉框)
*/
export function getCourseList() {
return request.get('/api/v1/courses', {
page: 1,
size: 100,
status: 'published'
})
}

View File

@@ -0,0 +1,137 @@
/**
* 统计分析API
*/
import request from './request'
/**
* 统计参数接口
*/
export interface StatisticsParams {
course_id?: number // 课程ID不传则统计全部课程
period?: string // 时间范围: week/month/quarter/halfYear/year
}
/**
* 关键指标响应接口
*/
export interface KeyMetricsResponse {
learningEfficiency: {
value: number
unit: string
change: number
description: string
}
knowledgeCoverage: {
value: number
unit: string
change: number
description: string
}
avgTimePerQuestion: {
value: number
unit: string
change: number
description: string
}
progressSpeed: {
value: number
unit: string
change: number
description: string
}
}
/**
* 成绩分布响应接口
*/
export interface ScoreDistributionResponse {
excellent: number // 优秀(90-100)
good: number // 良好(80-89)
medium: number // 中等(70-79)
pass: number // 及格(60-69)
fail: number // 不及格(<60)
}
/**
* 难度分析响应接口
*/
export interface DifficultyAnalysisResponse {
简单题: number
中等题: number
困难题: number
综合题: number
应用题: number
}
/**
* 知识点掌握度响应接口
*/
export interface KnowledgeMasteryItem {
name: string
mastery: number
}
/**
* 学习时长统计响应接口
*/
export interface StudyTimeStatsResponse {
labels: string[]
studyTime: number[]
practiceTime: number[]
}
/**
* 详细数据响应接口
*/
export interface DetailDataItem {
date: string
examCount: number
avgScore: number
studyTime: number
questionCount: number
accuracy: number
improvement: number
}
/**
* 获取关键指标
*/
export const getKeyMetrics = (params: StatisticsParams = {}) => {
return request.get<KeyMetricsResponse>('/api/v1/statistics/key-metrics', params)
}
/**
* 获取成绩分布
*/
export const getScoreDistribution = (params: StatisticsParams = {}) => {
return request.get<ScoreDistributionResponse>('/api/v1/statistics/score-distribution', params)
}
/**
* 获取难度分析
*/
export const getDifficultyAnalysis = (params: StatisticsParams = {}) => {
return request.get<DifficultyAnalysisResponse>('/api/v1/statistics/difficulty-analysis', params)
}
/**
* 获取知识点掌握度
*/
export const getKnowledgeMastery = (params: { course_id?: number } = {}) => {
return request.get<KnowledgeMasteryItem[]>('/api/v1/statistics/knowledge-mastery', params)
}
/**
* 获取学习时长统计
*/
export const getStudyTimeStats = (params: StatisticsParams = {}) => {
return request.get<StudyTimeStatsResponse>('/api/v1/statistics/study-time', params)
}
/**
* 获取详细数据
*/
export const getDetailData = (params: StatisticsParams = {}) => {
return request.get<DetailDataItem[]>('/api/v1/statistics/detail', params)
}

View File

@@ -0,0 +1,72 @@
/**
* 系统日志 API
*/
import request from '@/api/request'
/**
* 系统日志项
*/
export interface SystemLog {
id: number
level: string // debug, info, warning, error
type: string // system, user, api, error, security
user: string | null
user_id: number | null
ip: string | null
message: string
user_agent: string | null
path: string | null
method: string | null
extra_data: string | null
created_at: string
updated_at: string
}
/**
* 系统日志列表响应
*/
export interface SystemLogListResponse {
items: SystemLog[]
total: number
page: number
page_size: number
total_pages: number
}
/**
* 系统日志查询参数
*/
export interface SystemLogQuery {
level?: string
type?: string
user?: string
keyword?: string
start_date?: string
end_date?: string
page?: number
page_size?: number
}
/**
* 获取系统日志列表
*/
export function getSystemLogs(params: SystemLogQuery) {
return request.get<SystemLogListResponse>('/api/v1/admin/logs', params)
}
/**
* 获取日志详情
*/
export function getLogDetail(logId: number) {
return request.get<SystemLog>(`/api/v1/admin/logs/${logId}`)
}
/**
* 清理旧日志
*/
export function cleanupOldLogs(beforeDays: number) {
return request.delete<{ deleted_count: number }>(`/api/v1/admin/logs/cleanup?before_days=${beforeDays}`)
}

108
frontend/src/api/task.ts Normal file
View File

@@ -0,0 +1,108 @@
/**
* 任务管理API
*/
import { http } from '@/utils/http'
import type { ResponseModel } from '@/types/practice'
/**
* 任务接口
*/
export interface Task {
id: number
title: string
description?: string
priority: string
status: string
creator_id: number
deadline?: string
requirements?: any
progress: number
created_at: string
updated_at: string
courses: string[]
assigned_count: number
completed_count: number
}
/**
* 任务创建请求
*/
export interface TaskCreate {
title: string
description?: string
priority: string
deadline?: string
course_ids: number[]
user_ids: number[]
requirements?: any
}
/**
* 任务更新请求
*/
export interface TaskUpdate {
title?: string
description?: string
priority?: string
status?: string
deadline?: string
progress?: number
}
/**
* 任务统计响应
*/
export interface TaskStats {
total: number
ongoing: number
completed: number
expired: number
avg_completion_rate: number
}
/**
* 获取任务列表
*/
export function getTasks(params?: {
status?: string
page?: number
page_size?: number
}): Promise<ResponseModel<{ items: Task[]; total: number; page: number; page_size: number; total_pages: number }>> {
return http.get('/api/v1/manager/tasks', { params })
}
/**
* 获取任务统计
*/
export function getTaskStats(): Promise<ResponseModel<TaskStats>> {
return http.get('/api/v1/manager/tasks/stats')
}
/**
* 获取任务详情
*/
export function getTaskDetail(id: number): Promise<ResponseModel<Task>> {
return http.get(`/api/v1/manager/tasks/${id}`)
}
/**
* 创建任务
*/
export function createTask(data: TaskCreate): Promise<ResponseModel<Task>> {
return http.post('/api/v1/manager/tasks', data)
}
/**
* 更新任务
*/
export function updateTask(id: number, data: TaskUpdate): Promise<ResponseModel<Task>> {
return http.put(`/api/v1/manager/tasks/${id}`, data)
}
/**
* 删除任务
*/
export function deleteTask(id: number): Promise<ResponseModel<void>> {
return http.delete(`/api/v1/manager/tasks/${id}`)
}

View File

@@ -0,0 +1,117 @@
/**
* 团队看板API
*/
import request from './request'
export interface TeamOverview {
team_count: number
member_count: number
avg_progress: number
avg_score: number
course_completion_rate: number
trends: {
member_trend: number
progress_trend: number
score_trend: number
completion_trend: number
}
}
export interface ProgressData {
members: string[]
weeks: string[]
data: Array<{
name: string
values: number[]
}>
}
export interface CourseDistribution {
completed: number
in_progress: number
not_started: number
}
export interface AbilityAnalysis {
radar_data: {
dimensions: string[]
values: number[]
}
weaknesses: Array<{
name: string
avg_score: number
suggestion: string
}>
}
export interface RankingMember {
id: number
name: string
position: string
avatar: string
study_time?: number
avg_score?: number
}
export interface Rankings {
study_time_ranking: RankingMember[]
score_ranking: RankingMember[]
}
export interface Activity {
id: number | string
user_name: string
action: string
target: string
time: string
type: string
result?: { type: string; text: string }
}
export interface ActivitiesResponse {
activities: Activity[]
}
/**
* 获取团队概览统计
*/
export const getTeamOverview = () => {
return request.get<TeamOverview>('/api/v1/team/dashboard/overview')
}
/**
* 获取学习进度数据
*/
export const getProgressData = () => {
return request.get<ProgressData>('/api/v1/team/dashboard/progress')
}
/**
* 获取课程完成分布
*/
export const getCourseDistribution = () => {
return request.get<CourseDistribution>('/api/v1/team/dashboard/course-distribution')
}
/**
* 获取能力分析数据
*/
export const getAbilityAnalysis = () => {
return request.get<AbilityAnalysis>('/api/v1/team/dashboard/ability-analysis')
}
/**
* 获取排行榜数据
*/
export const getRankings = () => {
return request.get<Rankings>('/api/v1/team/dashboard/rankings')
}
/**
* 获取团队动态
*/
export const getActivities = () => {
return request.get<ActivitiesResponse>('/api/v1/team/dashboard/activities')
}

View File

@@ -0,0 +1,134 @@
/**
* 团队成员管理 API
*/
import request from '@/api/request'
/**
* 团队统计数据
*/
export interface TeamStatistics {
teamCount: number // 团队总人数
activeMembers: number // 活跃成员数
avgProgress: number // 平均学习进度
avgScore: number // 团队平均分
}
/**
* 团队成员信息
*/
export interface TeamMember {
id: number
name: string
avatar: string
position: string
status: 'active' | 'learning' | 'rest' // 活跃、学习中、休息
progress: number // 学习进度0-100
completedCourses: number // 完成课程数
totalCourses: number // 总课程数
avgScore: number // 平均成绩
studyTime: number // 学习时长(小时)
lastActive: string // 最近活跃时间
joinTime: string // 加入时间
email: string
phone: string
passRate: number // 通过率
}
/**
* 成员列表查询参数
*/
export interface MemberListParams {
page: number
size: number
search_text?: string // 搜索姓名、岗位
status?: 'active' | 'learning' | 'rest' // 筛选状态
position?: string // 筛选岗位
}
/**
* 成员详情
*/
export interface MemberDetail {
id: number
name: string
avatar: string
position: string
status: string
joinTime: string
email: string
phone: string
studyTime: number
completedCourses: number
avgScore: number
passRate: number
recentRecords: Array<{
id: string
time: string
content: string
type: string
}>
}
/**
* 成员学习报告
*/
export interface MemberReport {
overview: Array<{
label: string
value: string
icon: string
color: string
bgColor: string
}>
progressTrend: {
dates: string[]
data: number[]
}
abilities: Array<{
name: string
score: number
description: string
}>
records: Array<{
date: string
course: string
duration: number
score: number | null
status: string
}>
}
/**
* 获取团队统计数据
*/
export function getTeamStatistics() {
return request.get<TeamStatistics>('/api/v1/team/management/statistics')
}
/**
* 获取团队成员列表
*/
export function getTeamMembers(params: MemberListParams) {
return request.get<{
items: TeamMember[]
total: number
page: number
page_size: number
pages: number
}>('/api/v1/team/management/members', params)
}
/**
* 获取成员详情
*/
export function getMemberDetail(memberId: number) {
return request.get<MemberDetail>(`/api/v1/team/management/members/${memberId}/detail`)
}
/**
* 获取成员学习报告
*/
export function getMemberReport(memberId: number) {
return request.get<MemberReport>(`/api/v1/team/management/members/${memberId}/report`)
}

View File

@@ -0,0 +1,447 @@
/**
* 学员功能模块 API
*/
import request from '../request'
// 课程信息
export interface CourseInfo {
id: number
title: string
name?: string // 与title兼容
description?: string
coverImage?: string
category: string
difficulty: 'beginner' | 'intermediate' | 'advanced'
difficulty_level?: string // 与difficulty的字符串版本兼容
duration: number // 分钟
duration_hours?: number // 与duration的小时版本兼容
learner_count?: number // 学习人数
materialCount: number
progress: number // 0-100
rating: number
status: 'published' | 'draft' | 'archived'
tags: string[]
allow_download?: boolean // 是否允许下载资料
createdAt: string
updatedAt: string
}
// 课程材料
export interface CourseMaterial {
id: number
courseId: number
title: string
type: 'video' | 'audio' | 'document' | 'pdf' | 'ppt' | 'image' | 'text' | 'download' | 'html' | 'other'
url: string
duration?: number
size?: number
order: number
isCompleted: boolean
createdAt: string
}
// 成长路径节点
export interface GrowthPathNode {
id: number
courseId: number
courseName: string
courseDescription?: string
isRequired: boolean
isCompleted: boolean
progress: number
prerequisiteIds: number[]
estimatedDays: number
order: number
}
// 成长路径
export interface GrowthPath {
id: number
name: string
description?: string
positionId: number
positionName: string
totalCourses: number
completedCourses: number
requiredCourses: number
optionalCourses: number
estimatedDays: number
progress: number
nodes: GrowthPathNode[]
}
// 能力评估数据
export interface AbilityAssessment {
categories: Array<{
name: string
score: number
maxScore: number
skills: Array<{
name: string
score: number
maxScore: number
}>
}>
overallScore: number
maxOverallScore: number
lastUpdated: string
}
// AI陪练场景
export interface PracticeScene {
id: number
name: string
description: string
category: string
difficulty: 'easy' | 'medium' | 'hard'
estimatedDuration: number // 分钟
usageCount: number
averageRating: number
status: 'active' | 'inactive'
tags: string[]
aiRole: string
objectives: string[]
keywords: string[]
backgroundInfo: string
createdAt: string
}
// 陪练记录
export interface PracticeRecord {
id: string
sceneId: number
sceneName: string
sceneCategory: string
duration: number // 秒
messageCount: number
overallScore: number
result: 'excellent' | 'good' | 'average' | 'needs_improvement'
startTime: string
endTime: string
createdAt: string
}
// 陪练会话消息
export interface PracticeMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: string
audioUrl?: string
}
// 陪练分析报告
export interface PracticeReport {
id: string
recordId: string
overallScore: number
detailedScores: {
communication: number
professionalism: number
problemSolving: number
knowledge: number
}
strengths: string[]
improvements: string[]
suggestions: string[]
keyMoments: Array<{
timestamp: string
description: string
score: number
}>
summary: string
nextSteps: string[]
}
// 考试记录
export interface ExamRecord {
id: string
examName: string
examType: string
subject: string
totalScore: number
userScore: number
accuracy: number
duration: number // 秒
status: 'completed' | 'in_progress' | 'abandoned'
startTime: string
endTime?: string
createdAt: string
}
// 错题信息
export interface MistakeQuestion {
id: string
questionId: string
question: string
questionType: 'single' | 'multiple' | 'judge' | 'fill'
correctAnswer: string
userAnswer: string
explanation: string
mistakeCount: number
lastMistakeTime: string
isMastered: boolean
subject: string
difficulty: string
tags: string[]
}
/**
* 获取课程列表
*/
export const getCourseList = (params: {
page?: number
size?: number
category?: string
difficulty?: string
keyword?: string
} = {}) => {
return request.get<{
items: CourseInfo[]
total: number
page: number
size: number
}>('/api/v1/courses', { params })
}
/**
* 获取课程详情
*/
export const getCourseDetail = (courseId: number) => {
return request.get<CourseInfo>(`/api/v1/courses/${courseId}`)
}
/**
* 获取课程材料列表
*/
export const getCourseMaterials = (courseId: number) => {
return request.get<CourseMaterial[]>(`/api/v1/trainee/courses/${courseId}/materials`)
}
/**
* 标记材料为已完成
*/
export const markMaterialCompleted = (materialId: number) => {
return request.post(`/api/v1/trainee/materials/${materialId}/complete`)
}
/**
* 获取成长路径
*/
export const getGrowthPath = () => {
return request.get<GrowthPath>('/api/v1/trainee/growth-path')
}
/**
* 获取能力评估数据
*/
export const getAbilityAssessment = () => {
return request.get<AbilityAssessment>('/api/v1/trainee/ability-assessment')
}
/**
* 获取AI陪练场景列表
*/
export const getPracticeScenes = (params: {
category?: string
difficulty?: string
keyword?: string
} = {}) => {
return request.get<PracticeScene[]>('/api/v1/trainee/practice-scenes', { params })
}
/**
* 获取陪练场景详情
*/
export const getPracticeSceneDetail = (sceneId: number) => {
return request.get<PracticeScene>(`/api/v1/trainee/practice-scenes/${sceneId}`)
}
/**
* 开始AI陪练
*/
export const startPractice = (sceneId: number) => {
return request.post<{ sessionId: string }>('/api/v1/trainee/practice/start', { sceneId })
}
/**
* 结束AI陪练
*/
export const endPractice = (sessionId: string) => {
return request.post<{ recordId: string }>('/api/v1/trainee/practice/end', { sessionId })
}
/**
* 获取陪练记录列表
*/
export const getPracticeRecords = (params: {
page?: number
size?: number
sceneId?: number
result?: string
startDate?: string
endDate?: string
keyword?: string
} = {}) => {
return request.get<{
items: PracticeRecord[]
total: number
page: number
size: number
statistics: {
totalSessions: number
averageScore: number
totalDuration: number
monthlyProgress: number
}
}>('/api/v1/trainee/practice-records', { params })
}
/**
* 获取陪练记录详情(对话回放)
*/
export const getPracticeRecordDetail = (recordId: string) => {
return request.get<{
record: PracticeRecord
messages: PracticeMessage[]
}>(`/api/v1/trainee/practice-records/${recordId}`)
}
/**
* 获取陪练分析报告
*/
export const getPracticeReport = (recordId: string) => {
return request.get<PracticeReport>(`/api/v1/trainee/practice-reports/${recordId}`)
}
/**
* 获取考试记录列表
*/
export const getExamRecords = (params: {
page?: number
size?: number
examType?: string
subject?: string
minScore?: number
maxScore?: number
startDate?: string
endDate?: string
} = {}) => {
return request.get<{
items: ExamRecord[]
total: number
page: number
size: number
statistics: {
totalExams: number
averageScore: number
passRate: number
ranking: number
}
}>('/api/v1/trainee/exam-records', { params })
}
/**
* 获取考试记录详情
*/
export const getExamRecordDetail = (recordId: string) => {
return request.get<{
record: ExamRecord
questions: Array<{
questionId: string
question: string
options?: string[]
correctAnswer: string
userAnswer: string
isCorrect: boolean
score: number
}>
scoreByType: Array<{
type: string
totalQuestions: number
correctAnswers: number
accuracy: number
score: number
}>
}>(`/api/v1/trainee/exam-records/${recordId}`)
}
/**
* 获取错题列表
*/
export const getMistakeQuestions = (params: {
page?: number
size?: number
subject?: string
difficulty?: string
isMastered?: boolean
keyword?: string
} = {}) => {
return request.get<{
items: MistakeQuestion[]
total: number
page: number
size: number
statistics: {
totalMistakes: number
masteredCount: number
recentMistakes: number
}
}>('/api/v1/trainee/mistakes', { params })
}
/**
* 标记错题为已掌握
*/
export const markQuestionMastered = (questionId: string) => {
return request.post(`/api/v1/trainee/mistakes/${questionId}/mastered`)
}
/**
* 错题重练
*/
export const practicemistake = (questionIds: string[]) => {
return request.post<{ examId: string }>('/api/v1/trainee/mistakes/practice', { questionIds })
}
/**
* 创建课程对话会话
*/
export const createCourseChat = (courseId: number) => {
return request.post<{ sessionId: string }>('/api/v1/trainee/course-chat/sessions', { courseId })
}
/**
* 发送课程对话消息
*/
export const sendCourseMessage = (sessionId: string, content: string) => {
return request.post<{ messageId: string; response: string }>('/api/v1/trainee/course-chat/messages', {
sessionId,
content
})
}
/**
* 分析智能工牌数据
* 调用后端API分析言迹智能工牌的录音数据生成能力评估报告和课程推荐
*/
export const analyzeYanjiBadge = () => {
return request.post<{
assessment_id: number
total_score: number
dimensions: Array<{
name: string
score: number
feedback: string
}>
recommended_courses: Array<{
course_id: number
course_name: string
recommendation_reason: string
priority: string
match_score: number
}>
conversation_count: number
analyzed_at: string
}>('/api/v1/ability/analyze-yanji')
}

View File

@@ -0,0 +1,248 @@
/**
* 用户管理模块 API
*/
import request from '../request'
import { UserInfo } from '../auth'
// 用户查询参数
export interface UserQueryParams {
page?: number
size?: number
keyword?: string
role?: string
status?: string
teamId?: number
}
// 用户列表响应
export interface UserListResponse {
items: UserInfo[]
total: number
page: number
size: number
}
// 更新用户信息请求
export interface UpdateUserRequest {
name?: string
email?: string
phone?: string
avatar?: string
role?: string
status?: string
teamId?: number
}
// 团队信息
export interface TeamInfo {
id: number
name: string
description?: string
leaderId: number
leaderName: string
memberCount: number
status: 'active' | 'inactive'
createdAt: string
updatedAt: string
}
// 团队查询参数
export interface TeamQueryParams {
page?: number
size?: number
keyword?: string
status?: string
}
// 团队列表响应
export interface TeamListResponse {
items: TeamInfo[]
total: number
page: number
size: number
}
// 创建团队请求
export interface CreateTeamRequest {
name: string
description?: string
leaderId?: number
}
// 团队成员
export interface TeamMember {
id: number
userId: number
userName: string
userEmail: string
role: string
joinedAt: string
}
/**
* 获取用户列表
*/
export const getUserList = (params: UserQueryParams = {}) => {
return request.get<UserListResponse>('/api/v1/users', { params })
}
/**
* 获取用户详情
*/
export const getUserDetail = (id: number) => {
return request.get<UserInfo>(`/api/v1/users/${id}`)
}
/**
* 更新用户信息
*/
export const updateUser = (id: number, data: UpdateUserRequest) => {
return request.put<UserInfo>(`/api/v1/users/${id}`, data)
}
/**
* 删除用户
*/
export const deleteUser = (id: number) => {
return request.delete(`/api/v1/users/${id}`)
}
/**
* 获取当前用户信息
*/
export const getCurrentUserProfile = () => {
return request.get<UserInfo>('/api/v1/users/me')
}
/**
* 更新当前用户信息
*/
export const updateCurrentUserProfile = (data: UpdateUserRequest) => {
return request.put<UserInfo>('/api/v1/users/me', data)
}
/**
* 修改密码请求参数
*/
export interface ChangePasswordRequest {
old_password: string
new_password: string
}
/**
* 修改当前用户密码
*/
export const changePassword = (data: ChangePasswordRequest) => {
return request.put('/api/v1/users/me/password', data)
}
/**
* 获取团队列表(用于下拉)
*/
export interface TeamBasic {
id: number
name: string
code: string
team_type: string
}
export const getTeams = (params?: { keyword?: string }) => {
return request.get<TeamBasic[]>('/api/v1/teams', params)
}
/**
* 将用户加入团队
*/
export const addUserToTeam = (userId: number, teamId: number, role: 'member'|'leader' = 'member') => {
return request.post(`/api/v1/users/${userId}/teams/${teamId}?role=${role}`)
}
/**
* 将用户从团队移除
*/
export const removeUserFromTeam = (userId: number, teamId: number) => {
return request.delete(`/api/v1/users/${userId}/teams/${teamId}`)
}
/**
* 获取用户所属岗位列表
*/
export interface UserPositionBasic {
id: number
name: string
code: string
}
export const getUserPositions = (userId: number) => {
return request.get<UserPositionBasic[]>(`/api/v1/users/${userId}/positions`)
}
/**
* 获取当前用户学习统计
*/
export interface UserStatistics {
learningDays: number
totalHours: number
practiceQuestions: number
averageScore: number
}
export const getCurrentUserStatistics = () => {
return request.get<UserStatistics>('/api/v1/users/me/statistics')
}
/**
* 获取团队列表
*/
export const getTeamList = (params: TeamQueryParams = {}) => {
return request.get<TeamListResponse>('/api/v1/teams', { params })
}
/**
* 创建团队
*/
export const createTeam = (data: CreateTeamRequest) => {
return request.post<TeamInfo>('/api/v1/teams', data)
}
/**
* 获取团队详情
*/
export const getTeamDetail = (id: number) => {
return request.get<TeamInfo>(`/api/v1/teams/${id}`)
}
/**
* 更新团队信息
*/
export const updateTeam = (id: number, data: Partial<CreateTeamRequest>) => {
return request.put<TeamInfo>(`/api/v1/teams/${id}`, data)
}
/**
* 删除团队
*/
export const deleteTeam = (id: number) => {
return request.delete(`/api/v1/teams/${id}`)
}
/**
* 添加团队成员
*/
export const addTeamMember = (teamId: number, userIds: number[]) => {
return request.post(`/api/v1/teams/${teamId}/members`, { userIds })
}
/**
* 移除团队成员
*/
export const removeTeamMember = (teamId: number, userId: number) => {
return request.delete(`/api/v1/teams/${teamId}/members/${userId}`)
}
/**
* 获取团队成员列表
*/
export const getTeamMembers = (teamId: number) => {
return request.get<TeamMember[]>(`/api/v1/teams/${teamId}/members`)
}