feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
123
frontend/src/api/__tests__/auth.test.ts
Normal file
123
frontend/src/api/__tests__/auth.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
57
frontend/src/api/admin/dashboard.ts
Normal file
57
frontend/src/api/admin/dashboard.ts
Normal 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')
|
||||
}
|
||||
81
frontend/src/api/admin/position.ts
Normal file
81
frontend/src/api/admin/position.ts
Normal 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}`)
|
||||
}
|
||||
124
frontend/src/api/admin/user.ts
Normal file
124
frontend/src/api/admin/user.ts
Normal 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')
|
||||
}
|
||||
428
frontend/src/api/analysis/index.ts
Normal file
428
frontend/src/api/analysis/index.ts
Normal 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')
|
||||
}
|
||||
104
frontend/src/api/auth/index.ts
Normal file
104
frontend/src/api/auth/index.ts
Normal 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 })
|
||||
}
|
||||
18
frontend/src/api/broadcast.ts
Normal file
18
frontend/src/api/broadcast.ts
Normal 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`)
|
||||
}
|
||||
}
|
||||
38
frontend/src/api/config.ts
Normal file
38
frontend/src/api/config.ts
Normal 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
|
||||
}
|
||||
316
frontend/src/api/course/index.ts
Normal file
316
frontend/src/api/course/index.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
132
frontend/src/api/courseChat.ts
Normal file
132
frontend/src/api/courseChat.ts
Normal 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 流式响应需要使用原生 fetch(Axios 不支持 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 || []
|
||||
}
|
||||
}
|
||||
|
||||
150
frontend/src/api/coze/index.ts
Normal file
150
frontend/src/api/coze/index.ts
Normal 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 }
|
||||
})
|
||||
}
|
||||
20
frontend/src/api/dashboard.ts
Normal file
20
frontend/src/api/dashboard.ts
Normal 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
164
frontend/src/api/exam.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
546
frontend/src/api/exam/index.ts
Normal file
546
frontend/src/api/exam/index.ts
Normal 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
39
frontend/src/api/index.ts
Normal 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'
|
||||
518
frontend/src/api/manager/index.ts
Normal file
518
frontend/src/api/manager/index.ts
Normal 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 })
|
||||
}
|
||||
83
frontend/src/api/manager/practice.ts
Normal file
83
frontend/src/api/manager/practice.ts
Normal 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`)
|
||||
}
|
||||
|
||||
116
frontend/src/api/manager/scores.ts
Normal file
116
frontend/src/api/manager/scores.ts
Normal 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 } }
|
||||
)
|
||||
}
|
||||
|
||||
72
frontend/src/api/material.ts
Normal file
72
frontend/src/api/material.ts
Normal 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')
|
||||
}
|
||||
|
||||
12
frontend/src/api/mock/admin-dashboard-course-completion.ts
Normal file
12
frontend/src/api/mock/admin-dashboard-course-completion.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 课程完成率数据模拟
|
||||
*/
|
||||
|
||||
import type { CourseCompletionData } from '../admin/dashboard'
|
||||
|
||||
const mockData: CourseCompletionData = {
|
||||
courses: ['销售技巧', '产品知识', '客户服务', '商务礼仪', '沟通技巧'],
|
||||
completionRates: [85, 92, 78, 88, 75]
|
||||
}
|
||||
|
||||
export default mockData
|
||||
30
frontend/src/api/mock/admin-dashboard-stats.ts
Normal file
30
frontend/src/api/mock/admin-dashboard-stats.ts
Normal 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
|
||||
32
frontend/src/api/mock/admin-dashboard-user-growth.ts
Normal file
32
frontend/src/api/mock/admin-dashboard-user-growth.ts
Normal 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
|
||||
96
frontend/src/api/mock/admin-positions-tree.ts
Normal file
96
frontend/src/api/mock/admin-positions-tree.ts
Normal 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
|
||||
183
frontend/src/api/mock/admin-positions.ts
Normal file
183
frontend/src/api/mock/admin-positions.ts
Normal 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
|
||||
29
frontend/src/api/mock/admin-users-1-reset-password.ts
Normal file
29
frontend/src/api/mock/admin-users-1-reset-password.ts
Normal 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
|
||||
12
frontend/src/api/mock/admin-users-statistics.ts
Normal file
12
frontend/src/api/mock/admin-users-statistics.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 用户统计数据模拟
|
||||
*/
|
||||
|
||||
const mockData = {
|
||||
total: 50,
|
||||
activeCount: 42,
|
||||
pendingCount: 5,
|
||||
inactiveCount: 3
|
||||
}
|
||||
|
||||
export default mockData
|
||||
133
frontend/src/api/mock/admin-users.ts
Normal file
133
frontend/src/api/mock/admin-users.ts
Normal 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
|
||||
144
frontend/src/api/notification.ts
Normal file
144
frontend/src/api/notification.ts
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
190
frontend/src/api/practice.ts
Normal file
190
frontend/src/api/practice.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
|
||||
117
frontend/src/api/practiceScene.ts
Normal file
117
frontend/src/api/practiceScene.ts
Normal 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
273
frontend/src/api/request.ts
Normal 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
126
frontend/src/api/score.ts
Normal 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'
|
||||
})
|
||||
}
|
||||
|
||||
137
frontend/src/api/statistics.ts
Normal file
137
frontend/src/api/statistics.ts
Normal 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)
|
||||
}
|
||||
|
||||
72
frontend/src/api/systemLogs.ts
Normal file
72
frontend/src/api/systemLogs.ts
Normal 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
108
frontend/src/api/task.ts
Normal 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}`)
|
||||
}
|
||||
|
||||
117
frontend/src/api/teamDashboard.ts
Normal file
117
frontend/src/api/teamDashboard.ts
Normal 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')
|
||||
}
|
||||
|
||||
134
frontend/src/api/teamManagement.ts
Normal file
134
frontend/src/api/teamManagement.ts
Normal 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`)
|
||||
}
|
||||
|
||||
447
frontend/src/api/trainee/index.ts
Normal file
447
frontend/src/api/trainee/index.ts
Normal 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')
|
||||
}
|
||||
248
frontend/src/api/user/index.ts
Normal file
248
frontend/src/api/user/index.ts
Normal 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`)
|
||||
}
|
||||
Reference in New Issue
Block a user