1. 课程学习进度追踪
- 新增 UserCourseProgress 和 UserMaterialProgress 模型
- 新增 /api/v1/progress/* 进度追踪 API
- 更新 admin.py 使用真实课程完成率数据
2. 路由权限检查完善
- 新增前端 permissionChecker.ts 权限检查工具
- 更新 router/guard.ts 实现团队和课程权限验证
- 新增后端 permission_service.py
3. AI 陪练音频转文本
- 新增 speech_recognition.py 语音识别服务
- 新增 /api/v1/speech/* API
- 更新 ai-practice-coze.vue 支持语音输入
4. 双人对练报告生成
- 更新 practice_room_service.py 添加报告生成功能
- 新增 /rooms/{room_code}/report API
- 更新 duo-practice-report.vue 调用真实 API
5. 学习提醒推送
- 新增 notification_service.py 通知服务
- 新增 scheduler_service.py 定时任务服务
- 支持钉钉、企微、站内消息推送
6. 智能学习推荐
- 新增 recommendation_service.py 推荐服务
- 新增 /api/v1/recommendations/* API
- 支持错题、能力、进度、热门多维度推荐
7. 安全问题修复
- DEBUG 默认值改为 False
- 添加 SECRET_KEY 安全警告
- 新增 check_security_settings() 检查函数
8. 证书 PDF 生成
- 更新 certificate_service.py 添加 PDF 生成
- 添加 weasyprint、Pillow、qrcode 依赖
- 更新下载 API 支持 PDF 和 PNG 格式
This commit is contained in:
@@ -1,149 +1,149 @@
|
||||
/**
|
||||
* 证书系统 API
|
||||
*/
|
||||
import request from '@/api/request'
|
||||
|
||||
// 证书类型
|
||||
export type CertificateType = 'course' | 'exam' | 'achievement'
|
||||
|
||||
// 证书模板
|
||||
export interface CertificateTemplate {
|
||||
id: number
|
||||
name: string
|
||||
type: CertificateType
|
||||
background_url?: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 证书信息
|
||||
export interface Certificate {
|
||||
id: number
|
||||
certificate_no: string
|
||||
title: string
|
||||
description?: string
|
||||
type: CertificateType
|
||||
type_name: string
|
||||
issued_at: string
|
||||
valid_until?: string
|
||||
score?: number
|
||||
completion_rate?: number
|
||||
pdf_url?: string
|
||||
image_url?: string
|
||||
course_id?: number
|
||||
exam_id?: number
|
||||
badge_id?: number
|
||||
meta_data?: Record<string, any>
|
||||
template?: {
|
||||
id: number
|
||||
name: string
|
||||
background_url?: string
|
||||
}
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
full_name?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 证书列表响应
|
||||
export interface CertificateListResponse {
|
||||
items: Certificate[]
|
||||
total: number
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
export interface VerifyResult {
|
||||
valid: boolean
|
||||
certificate_no: string
|
||||
title?: string
|
||||
type_name?: string
|
||||
issued_at?: string
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
full_name?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书模板列表
|
||||
*/
|
||||
export function getCertificateTemplates(type?: CertificateType) {
|
||||
return request.get<CertificateTemplate[]>('/certificates/templates', {
|
||||
params: { cert_type: type }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的证书列表
|
||||
*/
|
||||
export function getMyCertificates(params?: {
|
||||
cert_type?: CertificateType
|
||||
offset?: number
|
||||
limit?: number
|
||||
}) {
|
||||
return request.get<CertificateListResponse>('/certificates/me', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定用户的证书列表
|
||||
*/
|
||||
export function getUserCertificates(userId: number, params?: {
|
||||
cert_type?: CertificateType
|
||||
offset?: number
|
||||
limit?: number
|
||||
}) {
|
||||
return request.get<CertificateListResponse>(`/certificates/user/${userId}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书详情
|
||||
*/
|
||||
export function getCertificateDetail(certId: number) {
|
||||
return request.get<Certificate>(`/certificates/${certId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书分享图片URL
|
||||
*/
|
||||
export function getCertificateImageUrl(certId: number): string {
|
||||
return `/api/v1/certificates/${certId}/image`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书下载URL
|
||||
*/
|
||||
export function getCertificateDownloadUrl(certId: number): string {
|
||||
return `/api/v1/certificates/${certId}/download`
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证证书
|
||||
*/
|
||||
export function verifyCertificate(certNo: string) {
|
||||
return request.get<VerifyResult>(`/certificates/verify/${certNo}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 颁发课程证书
|
||||
*/
|
||||
export function issueCoursCertificate(data: {
|
||||
course_id: number
|
||||
course_name: string
|
||||
completion_rate?: number
|
||||
}) {
|
||||
return request.post<Certificate>('/certificates/issue/course', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 颁发考试证书
|
||||
*/
|
||||
export function issueExamCertificate(data: {
|
||||
exam_id: number
|
||||
exam_name: string
|
||||
score: number
|
||||
}) {
|
||||
return request.post<Certificate>('/certificates/issue/exam', data)
|
||||
}
|
||||
/**
|
||||
* 证书系统 API
|
||||
*/
|
||||
import request from '@/api/request'
|
||||
|
||||
// 证书类型
|
||||
export type CertificateType = 'course' | 'exam' | 'achievement'
|
||||
|
||||
// 证书模板
|
||||
export interface CertificateTemplate {
|
||||
id: number
|
||||
name: string
|
||||
type: CertificateType
|
||||
background_url?: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 证书信息
|
||||
export interface Certificate {
|
||||
id: number
|
||||
certificate_no: string
|
||||
title: string
|
||||
description?: string
|
||||
type: CertificateType
|
||||
type_name: string
|
||||
issued_at: string
|
||||
valid_until?: string
|
||||
score?: number
|
||||
completion_rate?: number
|
||||
pdf_url?: string
|
||||
image_url?: string
|
||||
course_id?: number
|
||||
exam_id?: number
|
||||
badge_id?: number
|
||||
meta_data?: Record<string, any>
|
||||
template?: {
|
||||
id: number
|
||||
name: string
|
||||
background_url?: string
|
||||
}
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
full_name?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 证书列表响应
|
||||
export interface CertificateListResponse {
|
||||
items: Certificate[]
|
||||
total: number
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
export interface VerifyResult {
|
||||
valid: boolean
|
||||
certificate_no: string
|
||||
title?: string
|
||||
type_name?: string
|
||||
issued_at?: string
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
full_name?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书模板列表
|
||||
*/
|
||||
export function getCertificateTemplates(type?: CertificateType) {
|
||||
return request.get<CertificateTemplate[]>('/certificates/templates', {
|
||||
params: { cert_type: type }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的证书列表
|
||||
*/
|
||||
export function getMyCertificates(params?: {
|
||||
cert_type?: CertificateType
|
||||
offset?: number
|
||||
limit?: number
|
||||
}) {
|
||||
return request.get<CertificateListResponse>('/certificates/me', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定用户的证书列表
|
||||
*/
|
||||
export function getUserCertificates(userId: number, params?: {
|
||||
cert_type?: CertificateType
|
||||
offset?: number
|
||||
limit?: number
|
||||
}) {
|
||||
return request.get<CertificateListResponse>(`/certificates/user/${userId}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书详情
|
||||
*/
|
||||
export function getCertificateDetail(certId: number) {
|
||||
return request.get<Certificate>(`/certificates/${certId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书分享图片URL
|
||||
*/
|
||||
export function getCertificateImageUrl(certId: number): string {
|
||||
return `/api/v1/certificates/${certId}/image`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取证书下载URL
|
||||
*/
|
||||
export function getCertificateDownloadUrl(certId: number): string {
|
||||
return `/api/v1/certificates/${certId}/download`
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证证书
|
||||
*/
|
||||
export function verifyCertificate(certNo: string) {
|
||||
return request.get<VerifyResult>(`/certificates/verify/${certNo}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 颁发课程证书
|
||||
*/
|
||||
export function issueCoursCertificate(data: {
|
||||
course_id: number
|
||||
course_name: string
|
||||
completion_rate?: number
|
||||
}) {
|
||||
return request.post<Certificate>('/certificates/issue/course', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 颁发考试证书
|
||||
*/
|
||||
export function issueExamCertificate(data: {
|
||||
exam_id: number
|
||||
exam_name: string
|
||||
score: number
|
||||
}) {
|
||||
return request.post<Certificate>('/certificates/issue/exam', data)
|
||||
}
|
||||
|
||||
@@ -175,4 +175,4 @@ export function getTeamDashboard() {
|
||||
*/
|
||||
export function getFullDashboardData() {
|
||||
return request.get<FullDashboardData>('/dashboard/all')
|
||||
}
|
||||
}
|
||||
@@ -1,222 +1,222 @@
|
||||
/**
|
||||
* 双人对练 API
|
||||
*/
|
||||
import request from '@/api/request'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export interface CreateRoomRequest {
|
||||
scene_id?: number
|
||||
scene_name?: string
|
||||
scene_type?: string
|
||||
scene_background?: string
|
||||
role_a_name?: string
|
||||
role_b_name?: string
|
||||
role_a_description?: string
|
||||
role_b_description?: string
|
||||
host_role?: 'A' | 'B'
|
||||
room_name?: string
|
||||
}
|
||||
|
||||
export interface CreateRoomResponse {
|
||||
room_code: string
|
||||
room_id: number
|
||||
room_name: string
|
||||
my_role: string
|
||||
my_role_name: string
|
||||
}
|
||||
|
||||
export interface JoinRoomResponse {
|
||||
room_code: string
|
||||
room_id: number
|
||||
room_name: string
|
||||
status: string
|
||||
my_role: string
|
||||
my_role_name: string
|
||||
}
|
||||
|
||||
export interface RoomUser {
|
||||
id: number
|
||||
username: string
|
||||
full_name: string
|
||||
avatar_url?: string
|
||||
}
|
||||
|
||||
export interface RoomInfo {
|
||||
id: number
|
||||
room_code: string
|
||||
room_name?: string
|
||||
scene_id?: number
|
||||
scene_name?: string
|
||||
scene_type?: string
|
||||
scene_background?: string
|
||||
role_a_name: string
|
||||
role_b_name: string
|
||||
role_a_description?: string
|
||||
role_b_description?: string
|
||||
host_role: string
|
||||
status: string
|
||||
created_at?: string
|
||||
started_at?: string
|
||||
ended_at?: string
|
||||
duration_seconds: number
|
||||
total_turns: number
|
||||
role_a_turns: number
|
||||
role_b_turns: number
|
||||
}
|
||||
|
||||
export interface RoomDetailResponse {
|
||||
room: RoomInfo
|
||||
host_user?: RoomUser
|
||||
guest_user?: RoomUser
|
||||
host_role_name?: string
|
||||
guest_role_name?: string
|
||||
my_role?: string
|
||||
my_role_name?: string
|
||||
is_host: boolean
|
||||
}
|
||||
|
||||
export type MessageType =
|
||||
| 'chat'
|
||||
| 'system'
|
||||
| 'join'
|
||||
| 'leave'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'voice_start'
|
||||
| 'voice_offer'
|
||||
| 'voice_answer'
|
||||
| 'ice_candidate'
|
||||
| 'voice_end'
|
||||
|
||||
export interface RoomMessage {
|
||||
id: number
|
||||
room_id: number
|
||||
user_id?: number
|
||||
message_type: MessageType
|
||||
content?: string
|
||||
role_name?: string
|
||||
sequence: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface WebRTCSignalRequest {
|
||||
signal_type: 'voice_start' | 'voice_offer' | 'voice_answer' | 'ice_candidate' | 'voice_end'
|
||||
payload: Record<string, any>
|
||||
}
|
||||
|
||||
export interface MessagesResponse {
|
||||
messages: RoomMessage[]
|
||||
room_status: string
|
||||
last_sequence: number
|
||||
}
|
||||
|
||||
export interface RoomListItem {
|
||||
id: number
|
||||
room_code: string
|
||||
room_name?: string
|
||||
scene_name?: string
|
||||
status: string
|
||||
is_host: boolean
|
||||
created_at?: string
|
||||
duration_seconds: number
|
||||
total_turns: number
|
||||
}
|
||||
|
||||
// ==================== API 函数 ====================
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
export function createRoom(data: CreateRoomRequest) {
|
||||
return request.post<CreateRoomResponse>('/api/v1/practice/rooms', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
export function joinRoom(roomCode: string) {
|
||||
return request.post<JoinRoomResponse>('/api/v1/practice/rooms/join', {
|
||||
room_code: roomCode
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间详情
|
||||
*/
|
||||
export function getRoomDetail(roomCode: string) {
|
||||
return request.get<RoomDetailResponse>(`/api/v1/practice/rooms/${roomCode}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始对练
|
||||
*/
|
||||
export function startPractice(roomCode: string) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/start`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束对练
|
||||
*/
|
||||
export function endPractice(roomCode: string) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/end`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
export function leaveRoom(roomCode: string) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/leave`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
export function sendMessage(roomCode: string, content: string) {
|
||||
return request.post<RoomMessage>(`/api/v1/practice/rooms/${roomCode}/message`, {
|
||||
content
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
*/
|
||||
export function getMessages(roomCode: string, sinceSequence: number = 0) {
|
||||
return request.get<MessagesResponse>(`/api/v1/practice/rooms/${roomCode}/messages`, {
|
||||
params: { since_sequence: sinceSequence }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的房间列表
|
||||
*/
|
||||
export function getMyRooms(status?: string, limit: number = 20) {
|
||||
return request.get<{ rooms: RoomListItem[] }>('/api/v1/practice/rooms', {
|
||||
params: { status, limit }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分享链接
|
||||
*/
|
||||
export function generateShareLink(roomCode: string): string {
|
||||
const baseUrl = window.location.origin
|
||||
return `${baseUrl}/trainee/duo-practice/join/${roomCode}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 WebRTC 信令
|
||||
*/
|
||||
export function sendSignal(roomCode: string, signalType: string, payload: Record<string, any>) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/signal`, {
|
||||
signal_type: signalType,
|
||||
payload
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对练报告
|
||||
*/
|
||||
export function getPracticeReport(roomCode: string) {
|
||||
return request.get(`/api/v1/practice/rooms/${roomCode}/report`)
|
||||
}
|
||||
/**
|
||||
* 双人对练 API
|
||||
*/
|
||||
import request from '@/api/request'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export interface CreateRoomRequest {
|
||||
scene_id?: number
|
||||
scene_name?: string
|
||||
scene_type?: string
|
||||
scene_background?: string
|
||||
role_a_name?: string
|
||||
role_b_name?: string
|
||||
role_a_description?: string
|
||||
role_b_description?: string
|
||||
host_role?: 'A' | 'B'
|
||||
room_name?: string
|
||||
}
|
||||
|
||||
export interface CreateRoomResponse {
|
||||
room_code: string
|
||||
room_id: number
|
||||
room_name: string
|
||||
my_role: string
|
||||
my_role_name: string
|
||||
}
|
||||
|
||||
export interface JoinRoomResponse {
|
||||
room_code: string
|
||||
room_id: number
|
||||
room_name: string
|
||||
status: string
|
||||
my_role: string
|
||||
my_role_name: string
|
||||
}
|
||||
|
||||
export interface RoomUser {
|
||||
id: number
|
||||
username: string
|
||||
full_name: string
|
||||
avatar_url?: string
|
||||
}
|
||||
|
||||
export interface RoomInfo {
|
||||
id: number
|
||||
room_code: string
|
||||
room_name?: string
|
||||
scene_id?: number
|
||||
scene_name?: string
|
||||
scene_type?: string
|
||||
scene_background?: string
|
||||
role_a_name: string
|
||||
role_b_name: string
|
||||
role_a_description?: string
|
||||
role_b_description?: string
|
||||
host_role: string
|
||||
status: string
|
||||
created_at?: string
|
||||
started_at?: string
|
||||
ended_at?: string
|
||||
duration_seconds: number
|
||||
total_turns: number
|
||||
role_a_turns: number
|
||||
role_b_turns: number
|
||||
}
|
||||
|
||||
export interface RoomDetailResponse {
|
||||
room: RoomInfo
|
||||
host_user?: RoomUser
|
||||
guest_user?: RoomUser
|
||||
host_role_name?: string
|
||||
guest_role_name?: string
|
||||
my_role?: string
|
||||
my_role_name?: string
|
||||
is_host: boolean
|
||||
}
|
||||
|
||||
export type MessageType =
|
||||
| 'chat'
|
||||
| 'system'
|
||||
| 'join'
|
||||
| 'leave'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'voice_start'
|
||||
| 'voice_offer'
|
||||
| 'voice_answer'
|
||||
| 'ice_candidate'
|
||||
| 'voice_end'
|
||||
|
||||
export interface RoomMessage {
|
||||
id: number
|
||||
room_id: number
|
||||
user_id?: number
|
||||
message_type: MessageType
|
||||
content?: string
|
||||
role_name?: string
|
||||
sequence: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface WebRTCSignalRequest {
|
||||
signal_type: 'voice_start' | 'voice_offer' | 'voice_answer' | 'ice_candidate' | 'voice_end'
|
||||
payload: Record<string, any>
|
||||
}
|
||||
|
||||
export interface MessagesResponse {
|
||||
messages: RoomMessage[]
|
||||
room_status: string
|
||||
last_sequence: number
|
||||
}
|
||||
|
||||
export interface RoomListItem {
|
||||
id: number
|
||||
room_code: string
|
||||
room_name?: string
|
||||
scene_name?: string
|
||||
status: string
|
||||
is_host: boolean
|
||||
created_at?: string
|
||||
duration_seconds: number
|
||||
total_turns: number
|
||||
}
|
||||
|
||||
// ==================== API 函数 ====================
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*/
|
||||
export function createRoom(data: CreateRoomRequest) {
|
||||
return request.post<CreateRoomResponse>('/api/v1/practice/rooms', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入房间
|
||||
*/
|
||||
export function joinRoom(roomCode: string) {
|
||||
return request.post<JoinRoomResponse>('/api/v1/practice/rooms/join', {
|
||||
room_code: roomCode
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间详情
|
||||
*/
|
||||
export function getRoomDetail(roomCode: string) {
|
||||
return request.get<RoomDetailResponse>(`/api/v1/practice/rooms/${roomCode}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始对练
|
||||
*/
|
||||
export function startPractice(roomCode: string) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/start`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束对练
|
||||
*/
|
||||
export function endPractice(roomCode: string) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/end`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 离开房间
|
||||
*/
|
||||
export function leaveRoom(roomCode: string) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/leave`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
export function sendMessage(roomCode: string, content: string) {
|
||||
return request.post<RoomMessage>(`/api/v1/practice/rooms/${roomCode}/message`, {
|
||||
content
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息列表
|
||||
*/
|
||||
export function getMessages(roomCode: string, sinceSequence: number = 0) {
|
||||
return request.get<MessagesResponse>(`/api/v1/practice/rooms/${roomCode}/messages`, {
|
||||
params: { since_sequence: sinceSequence }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的房间列表
|
||||
*/
|
||||
export function getMyRooms(status?: string, limit: number = 20) {
|
||||
return request.get<{ rooms: RoomListItem[] }>('/api/v1/practice/rooms', {
|
||||
params: { status, limit }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分享链接
|
||||
*/
|
||||
export function generateShareLink(roomCode: string): string {
|
||||
const baseUrl = window.location.origin
|
||||
return `${baseUrl}/trainee/duo-practice/join/${roomCode}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 WebRTC 信令
|
||||
*/
|
||||
export function sendSignal(roomCode: string, signalType: string, payload: Record<string, any>) {
|
||||
return request.post(`/api/v1/practice/rooms/${roomCode}/signal`, {
|
||||
signal_type: signalType,
|
||||
payload
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对练报告
|
||||
*/
|
||||
export function getPracticeReport(roomCode: string) {
|
||||
return request.get(`/api/v1/practice/rooms/${roomCode}/report`)
|
||||
}
|
||||
|
||||
@@ -1,182 +1,182 @@
|
||||
/**
|
||||
* 等级与奖章 API
|
||||
*/
|
||||
|
||||
import request from '@/api/request'
|
||||
|
||||
// 类型定义
|
||||
export interface LevelInfo {
|
||||
user_id: number
|
||||
level: number
|
||||
exp: number
|
||||
total_exp: number
|
||||
title: string
|
||||
color: string
|
||||
login_streak: number
|
||||
max_login_streak: number
|
||||
last_checkin_at: string | null
|
||||
next_level_exp: number
|
||||
exp_to_next_level: number
|
||||
is_max_level: boolean
|
||||
}
|
||||
|
||||
export interface ExpHistoryItem {
|
||||
id: number
|
||||
exp_change: number
|
||||
exp_type: string
|
||||
description: string
|
||||
level_before: number | null
|
||||
level_after: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface LeaderboardItem {
|
||||
rank: number
|
||||
user_id: number
|
||||
username: string
|
||||
full_name: string | null
|
||||
avatar_url: string | null
|
||||
level: number
|
||||
title: string
|
||||
color: string
|
||||
total_exp: number
|
||||
login_streak: number
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
category: string
|
||||
condition_type?: string
|
||||
condition_value?: number
|
||||
exp_reward: number
|
||||
unlocked?: boolean
|
||||
unlocked_at?: string | null
|
||||
}
|
||||
|
||||
export interface CheckinResult {
|
||||
success: boolean
|
||||
message: string
|
||||
exp_gained: number
|
||||
base_exp?: number
|
||||
bonus_exp?: number
|
||||
login_streak: number
|
||||
leveled_up?: boolean
|
||||
new_level?: number | null
|
||||
already_checked_in?: boolean
|
||||
new_badges?: Badge[]
|
||||
}
|
||||
|
||||
// API 函数
|
||||
|
||||
/**
|
||||
* 获取当前用户等级信息
|
||||
*/
|
||||
export function getMyLevel() {
|
||||
return request.get<LevelInfo>('/level/me')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定用户等级信息
|
||||
*/
|
||||
export function getUserLevel(userId: number) {
|
||||
return request.get<LevelInfo>(`/level/user/${userId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日签到
|
||||
*/
|
||||
export function dailyCheckin() {
|
||||
return request.post<CheckinResult>('/level/checkin')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取经验值历史
|
||||
*/
|
||||
export function getExpHistory(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
exp_type?: string
|
||||
}) {
|
||||
return request.get<{
|
||||
items: ExpHistoryItem[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}>('/level/exp-history', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取等级排行榜
|
||||
*/
|
||||
export function getLeaderboard(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}) {
|
||||
return request.get<{
|
||||
items: LeaderboardItem[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
my_rank: number
|
||||
my_level_info: LevelInfo
|
||||
}>('/level/leaderboard', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有奖章定义
|
||||
*/
|
||||
export function getAllBadges() {
|
||||
return request.get<Badge[]>('/level/badges/all')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户奖章(含解锁状态)
|
||||
*/
|
||||
export function getMyBadges() {
|
||||
return request.get<{
|
||||
badges: Badge[]
|
||||
total: number
|
||||
unlocked_count: number
|
||||
}>('/level/badges/me')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未通知的新奖章
|
||||
*/
|
||||
export function getUnnotifiedBadges() {
|
||||
return request.get<Badge[]>('/level/badges/unnotified')
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记奖章为已通知
|
||||
*/
|
||||
export function markBadgesNotified(badgeIds?: number[]) {
|
||||
return request.post('/level/badges/mark-notified', badgeIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动检查并授予奖章
|
||||
*/
|
||||
export function checkAndAwardBadges() {
|
||||
return request.post<{
|
||||
new_badges: Badge[]
|
||||
count: number
|
||||
}>('/level/check-badges')
|
||||
}
|
||||
|
||||
export default {
|
||||
getMyLevel,
|
||||
getUserLevel,
|
||||
dailyCheckin,
|
||||
getExpHistory,
|
||||
getLeaderboard,
|
||||
getAllBadges,
|
||||
getMyBadges,
|
||||
getUnnotifiedBadges,
|
||||
markBadgesNotified,
|
||||
checkAndAwardBadges
|
||||
}
|
||||
/**
|
||||
* 等级与奖章 API
|
||||
*/
|
||||
|
||||
import request from '@/api/request'
|
||||
|
||||
// 类型定义
|
||||
export interface LevelInfo {
|
||||
user_id: number
|
||||
level: number
|
||||
exp: number
|
||||
total_exp: number
|
||||
title: string
|
||||
color: string
|
||||
login_streak: number
|
||||
max_login_streak: number
|
||||
last_checkin_at: string | null
|
||||
next_level_exp: number
|
||||
exp_to_next_level: number
|
||||
is_max_level: boolean
|
||||
}
|
||||
|
||||
export interface ExpHistoryItem {
|
||||
id: number
|
||||
exp_change: number
|
||||
exp_type: string
|
||||
description: string
|
||||
level_before: number | null
|
||||
level_after: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface LeaderboardItem {
|
||||
rank: number
|
||||
user_id: number
|
||||
username: string
|
||||
full_name: string | null
|
||||
avatar_url: string | null
|
||||
level: number
|
||||
title: string
|
||||
color: string
|
||||
total_exp: number
|
||||
login_streak: number
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
category: string
|
||||
condition_type?: string
|
||||
condition_value?: number
|
||||
exp_reward: number
|
||||
unlocked?: boolean
|
||||
unlocked_at?: string | null
|
||||
}
|
||||
|
||||
export interface CheckinResult {
|
||||
success: boolean
|
||||
message: string
|
||||
exp_gained: number
|
||||
base_exp?: number
|
||||
bonus_exp?: number
|
||||
login_streak: number
|
||||
leveled_up?: boolean
|
||||
new_level?: number | null
|
||||
already_checked_in?: boolean
|
||||
new_badges?: Badge[]
|
||||
}
|
||||
|
||||
// API 函数
|
||||
|
||||
/**
|
||||
* 获取当前用户等级信息
|
||||
*/
|
||||
export function getMyLevel() {
|
||||
return request.get<LevelInfo>('/level/me')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定用户等级信息
|
||||
*/
|
||||
export function getUserLevel(userId: number) {
|
||||
return request.get<LevelInfo>(`/level/user/${userId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日签到
|
||||
*/
|
||||
export function dailyCheckin() {
|
||||
return request.post<CheckinResult>('/level/checkin')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取经验值历史
|
||||
*/
|
||||
export function getExpHistory(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
exp_type?: string
|
||||
}) {
|
||||
return request.get<{
|
||||
items: ExpHistoryItem[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}>('/level/exp-history', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取等级排行榜
|
||||
*/
|
||||
export function getLeaderboard(params?: {
|
||||
limit?: number
|
||||
offset?: number
|
||||
}) {
|
||||
return request.get<{
|
||||
items: LeaderboardItem[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
my_rank: number
|
||||
my_level_info: LevelInfo
|
||||
}>('/level/leaderboard', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有奖章定义
|
||||
*/
|
||||
export function getAllBadges() {
|
||||
return request.get<Badge[]>('/level/badges/all')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户奖章(含解锁状态)
|
||||
*/
|
||||
export function getMyBadges() {
|
||||
return request.get<{
|
||||
badges: Badge[]
|
||||
total: number
|
||||
unlocked_count: number
|
||||
}>('/level/badges/me')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未通知的新奖章
|
||||
*/
|
||||
export function getUnnotifiedBadges() {
|
||||
return request.get<Badge[]>('/level/badges/unnotified')
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记奖章为已通知
|
||||
*/
|
||||
export function markBadgesNotified(badgeIds?: number[]) {
|
||||
return request.post('/level/badges/mark-notified', badgeIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动检查并授予奖章
|
||||
*/
|
||||
export function checkAndAwardBadges() {
|
||||
return request.post<{
|
||||
new_badges: Badge[]
|
||||
count: number
|
||||
}>('/level/check-badges')
|
||||
}
|
||||
|
||||
export default {
|
||||
getMyLevel,
|
||||
getUserLevel,
|
||||
dailyCheckin,
|
||||
getExpHistory,
|
||||
getLeaderboard,
|
||||
getAllBadges,
|
||||
getMyBadges,
|
||||
getUnnotifiedBadges,
|
||||
markBadgesNotified,
|
||||
checkAndAwardBadges
|
||||
}
|
||||
|
||||
158
frontend/src/api/progress.ts
Normal file
158
frontend/src/api/progress.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 用户学习进度 API
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// ============ 类型定义 ============
|
||||
|
||||
export interface MaterialProgress {
|
||||
material_id: number
|
||||
material_name: string
|
||||
is_completed: boolean
|
||||
progress_percent: number
|
||||
last_position: number
|
||||
study_time: number
|
||||
first_accessed_at: string | null
|
||||
last_accessed_at: string | null
|
||||
completed_at: string | null
|
||||
}
|
||||
|
||||
export interface CourseProgress {
|
||||
course_id: number
|
||||
course_name: string
|
||||
status: 'not_started' | 'in_progress' | 'completed'
|
||||
progress_percent: number
|
||||
completed_materials: number
|
||||
total_materials: number
|
||||
total_study_time: number
|
||||
first_accessed_at: string | null
|
||||
last_accessed_at: string | null
|
||||
completed_at: string | null
|
||||
materials?: MaterialProgress[]
|
||||
}
|
||||
|
||||
export interface ProgressSummary {
|
||||
total_courses: number
|
||||
completed_courses: number
|
||||
in_progress_courses: number
|
||||
not_started_courses: number
|
||||
total_study_time: number
|
||||
average_progress: number
|
||||
}
|
||||
|
||||
export interface MaterialProgressUpdate {
|
||||
progress_percent: number
|
||||
last_position?: number
|
||||
study_time_delta?: number
|
||||
is_completed?: boolean
|
||||
}
|
||||
|
||||
// ============ API 方法 ============
|
||||
|
||||
/**
|
||||
* 获取学习进度摘要
|
||||
*/
|
||||
export const getProgressSummary = () => {
|
||||
return request.get<ProgressSummary>('/api/v1/progress/summary')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有课程学习进度
|
||||
*/
|
||||
export const getAllCourseProgress = (status?: string) => {
|
||||
return request.get<CourseProgress[]>('/api/v1/progress/courses', {
|
||||
params: status ? { status } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定课程的详细学习进度
|
||||
*/
|
||||
export const getCourseProgress = (courseId: number) => {
|
||||
return request.get<CourseProgress>(`/api/v1/progress/courses/${courseId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新资料学习进度
|
||||
*/
|
||||
export const updateMaterialProgress = (
|
||||
materialId: number,
|
||||
data: MaterialProgressUpdate
|
||||
) => {
|
||||
return request.post<MaterialProgress>(
|
||||
`/api/v1/progress/materials/${materialId}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记资料为已完成
|
||||
*/
|
||||
export const markMaterialComplete = (materialId: number) => {
|
||||
return request.post<MaterialProgress>(
|
||||
`/api/v1/progress/materials/${materialId}/complete`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始学习课程
|
||||
*/
|
||||
export const startCourse = (courseId: number) => {
|
||||
return request.post(`/api/v1/progress/courses/${courseId}/start`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化学习时长
|
||||
*/
|
||||
export const formatStudyTime = (seconds: number): string => {
|
||||
if (seconds < 60) {
|
||||
return `${seconds}秒`
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度状态文本
|
||||
*/
|
||||
export const getProgressStatusText = (
|
||||
status: 'not_started' | 'in_progress' | 'completed'
|
||||
): string => {
|
||||
const statusMap = {
|
||||
not_started: '未开始',
|
||||
in_progress: '学习中',
|
||||
completed: '已完成',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进度状态颜色
|
||||
*/
|
||||
export const getProgressStatusType = (
|
||||
status: 'not_started' | 'in_progress' | 'completed'
|
||||
): 'info' | 'warning' | 'success' => {
|
||||
const typeMap: Record<string, 'info' | 'warning' | 'success'> = {
|
||||
not_started: 'info',
|
||||
in_progress: 'warning',
|
||||
completed: 'success',
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
export default {
|
||||
getProgressSummary,
|
||||
getAllCourseProgress,
|
||||
getCourseProgress,
|
||||
updateMaterialProgress,
|
||||
markMaterialComplete,
|
||||
startCourse,
|
||||
formatStudyTime,
|
||||
getProgressStatusText,
|
||||
getProgressStatusType,
|
||||
}
|
||||
Reference in New Issue
Block a user