feat: 实现 KPL 系统功能改进计划
Some checks failed
continuous-integration/drone/push Build is failing

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:
yuliang_guo
2026-01-30 14:22:35 +08:00
parent 9793013a56
commit 64f5d567fa
66 changed files with 18067 additions and 14330 deletions

View File

@@ -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)
}

View File

@@ -175,4 +175,4 @@ export function getTeamDashboard() {
*/
export function getFullDashboardData() {
return request.get<FullDashboardData>('/dashboard/all')
}
}

View File

@@ -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`)
}

View File

@@ -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
}

View 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,
}