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 格式
414 lines
9.5 KiB
TypeScript
414 lines
9.5 KiB
TypeScript
/**
|
|
* 双人对练状态管理
|
|
*/
|
|
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import * as duoPracticeApi from '@/api/duoPractice'
|
|
import type {
|
|
RoomInfo,
|
|
RoomUser,
|
|
RoomMessage,
|
|
CreateRoomRequest
|
|
} from '@/api/duoPractice'
|
|
|
|
export const useDuoPracticeStore = defineStore('duoPractice', () => {
|
|
// ==================== 状态 ====================
|
|
|
|
/** 房间码 */
|
|
const roomCode = ref<string>('')
|
|
|
|
/** 房间信息 */
|
|
const roomInfo = ref<RoomInfo | null>(null)
|
|
|
|
/** 房主信息 */
|
|
const hostUser = ref<RoomUser | null>(null)
|
|
|
|
/** 嘉宾信息 */
|
|
const guestUser = ref<RoomUser | null>(null)
|
|
|
|
/** 我的角色 */
|
|
const myRole = ref<string>('')
|
|
|
|
/** 我的角色名称 */
|
|
const myRoleName = ref<string>('')
|
|
|
|
/** 是否是房主 */
|
|
const isHost = ref<boolean>(false)
|
|
|
|
/** 消息列表 */
|
|
const messages = ref<RoomMessage[]>([])
|
|
|
|
/** 最后消息序号(用于轮询) */
|
|
const lastSequence = ref<number>(0)
|
|
|
|
/** 是否正在加载 */
|
|
const isLoading = ref<boolean>(false)
|
|
|
|
/** 是否已连接(轮询中) */
|
|
const isConnected = ref<boolean>(false)
|
|
|
|
/** 轮询定时器 */
|
|
let pollingTimer: number | null = null
|
|
|
|
/** 输入框内容 */
|
|
const inputMessage = ref<string>('')
|
|
|
|
// ==================== 计算属性 ====================
|
|
|
|
/** 房间状态 */
|
|
const roomStatus = computed(() => roomInfo.value?.status || 'unknown')
|
|
|
|
/** 是否等待中 */
|
|
const isWaiting = computed(() => roomStatus.value === 'waiting')
|
|
|
|
/** 是否就绪 */
|
|
const isReady = computed(() => roomStatus.value === 'ready')
|
|
|
|
/** 是否对练中 */
|
|
const isPracticing = computed(() => roomStatus.value === 'practicing')
|
|
|
|
/** 是否已完成 */
|
|
const isCompleted = computed(() => roomStatus.value === 'completed')
|
|
|
|
/** 对方用户 */
|
|
const partnerUser = computed(() => {
|
|
if (isHost.value) {
|
|
return guestUser.value
|
|
} else {
|
|
return hostUser.value
|
|
}
|
|
})
|
|
|
|
/** 对方角色名称 */
|
|
const partnerRoleName = computed(() => {
|
|
if (!roomInfo.value) return ''
|
|
const partnerRole = myRole.value === 'A' ? 'B' : 'A'
|
|
return partnerRole === 'A' ? roomInfo.value.role_a_name : roomInfo.value.role_b_name
|
|
})
|
|
|
|
/** 聊天消息(过滤系统消息) */
|
|
const chatMessages = computed(() => {
|
|
return messages.value.filter(m => m.message_type === 'chat')
|
|
})
|
|
|
|
/** 系统消息 */
|
|
const systemMessages = computed(() => {
|
|
return messages.value.filter(m => m.message_type !== 'chat')
|
|
})
|
|
|
|
// ==================== 方法 ====================
|
|
|
|
/**
|
|
* 创建房间
|
|
*/
|
|
const createRoom = async (request: CreateRoomRequest) => {
|
|
isLoading.value = true
|
|
try {
|
|
const res: any = await duoPracticeApi.createRoom(request)
|
|
if (res.code === 200) {
|
|
roomCode.value = res.data.room_code
|
|
myRole.value = res.data.my_role
|
|
myRoleName.value = res.data.my_role_name
|
|
isHost.value = true
|
|
|
|
// 获取房间详情
|
|
await fetchRoomDetail()
|
|
|
|
return res.data
|
|
} else {
|
|
throw new Error(res.message || '创建房间失败')
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error.message || '创建房间失败')
|
|
throw error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 加入房间
|
|
*/
|
|
const joinRoom = async (code: string) => {
|
|
isLoading.value = true
|
|
try {
|
|
const res: any = await duoPracticeApi.joinRoom(code.toUpperCase())
|
|
if (res.code === 200) {
|
|
roomCode.value = res.data.room_code
|
|
myRole.value = res.data.my_role
|
|
myRoleName.value = res.data.my_role_name
|
|
isHost.value = false
|
|
|
|
// 获取房间详情
|
|
await fetchRoomDetail()
|
|
|
|
return res.data
|
|
} else {
|
|
throw new Error(res.message || '加入房间失败')
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error.message || '加入房间失败')
|
|
throw error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取房间详情
|
|
*/
|
|
const fetchRoomDetail = async () => {
|
|
if (!roomCode.value) return
|
|
|
|
try {
|
|
const res: any = await duoPracticeApi.getRoomDetail(roomCode.value)
|
|
if (res.code === 200) {
|
|
roomInfo.value = res.data.room
|
|
hostUser.value = res.data.host_user
|
|
guestUser.value = res.data.guest_user
|
|
myRole.value = res.data.my_role || myRole.value
|
|
myRoleName.value = res.data.my_role_name || myRoleName.value
|
|
isHost.value = res.data.is_host
|
|
}
|
|
} catch (error) {
|
|
console.error('获取房间详情失败:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 开始对练
|
|
*/
|
|
const startPractice = async () => {
|
|
if (!roomCode.value) return
|
|
|
|
isLoading.value = true
|
|
try {
|
|
const res: any = await duoPracticeApi.startPractice(roomCode.value)
|
|
if (res.code === 200) {
|
|
ElMessage.success('对练开始!')
|
|
await fetchRoomDetail()
|
|
} else {
|
|
throw new Error(res.message || '开始失败')
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error.message || '开始对练失败')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 结束对练
|
|
*/
|
|
const endPractice = async () => {
|
|
if (!roomCode.value) return
|
|
|
|
isLoading.value = true
|
|
try {
|
|
const res: any = await duoPracticeApi.endPractice(roomCode.value)
|
|
if (res.code === 200) {
|
|
ElMessage.success('对练结束')
|
|
await fetchRoomDetail()
|
|
stopPolling()
|
|
return res.data
|
|
} else {
|
|
throw new Error(res.message || '结束失败')
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error.message || '结束对练失败')
|
|
throw error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 离开房间
|
|
*/
|
|
const leaveRoom = async () => {
|
|
if (!roomCode.value) return
|
|
|
|
try {
|
|
await duoPracticeApi.leaveRoom(roomCode.value)
|
|
resetState()
|
|
} catch (error) {
|
|
console.error('离开房间失败:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送消息
|
|
*/
|
|
const sendMessage = async (content?: string) => {
|
|
const msg = content || inputMessage.value.trim()
|
|
if (!msg || !roomCode.value) return
|
|
|
|
try {
|
|
const res: any = await duoPracticeApi.sendMessage(roomCode.value, msg)
|
|
if (res.code === 200) {
|
|
inputMessage.value = ''
|
|
// 消息会通过轮询获取
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error.message || '发送失败')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取消息
|
|
*/
|
|
const fetchMessages = async () => {
|
|
if (!roomCode.value) return
|
|
|
|
try {
|
|
const res: any = await duoPracticeApi.getMessages(roomCode.value, lastSequence.value)
|
|
if (res.code === 200) {
|
|
const newMessages = res.data.messages
|
|
if (newMessages.length > 0) {
|
|
messages.value.push(...newMessages)
|
|
lastSequence.value = res.data.last_sequence
|
|
}
|
|
|
|
// 检查房间状态变化
|
|
if (res.data.room_status !== roomInfo.value?.status) {
|
|
await fetchRoomDetail()
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('获取消息失败:', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 开始轮询消息
|
|
*/
|
|
const startPolling = () => {
|
|
if (pollingTimer) return
|
|
|
|
isConnected.value = true
|
|
|
|
// 立即获取一次
|
|
fetchMessages()
|
|
|
|
// 每500ms轮询一次
|
|
pollingTimer = window.setInterval(() => {
|
|
fetchMessages()
|
|
}, 500)
|
|
|
|
console.log('[DuoPractice] 开始轮询消息')
|
|
}
|
|
|
|
/**
|
|
* 停止轮询
|
|
*/
|
|
const stopPolling = () => {
|
|
if (pollingTimer) {
|
|
clearInterval(pollingTimer)
|
|
pollingTimer = null
|
|
}
|
|
isConnected.value = false
|
|
console.log('[DuoPractice] 停止轮询消息')
|
|
}
|
|
|
|
/**
|
|
* 重置状态
|
|
*/
|
|
const resetState = () => {
|
|
stopPolling()
|
|
roomCode.value = ''
|
|
roomInfo.value = null
|
|
hostUser.value = null
|
|
guestUser.value = null
|
|
myRole.value = ''
|
|
myRoleName.value = ''
|
|
isHost.value = false
|
|
messages.value = []
|
|
lastSequence.value = 0
|
|
inputMessage.value = ''
|
|
isLoading.value = false
|
|
}
|
|
|
|
/**
|
|
* 生成分享链接
|
|
*/
|
|
const getShareLink = () => {
|
|
if (!roomCode.value) return ''
|
|
return duoPracticeApi.generateShareLink(roomCode.value)
|
|
}
|
|
|
|
/**
|
|
* 复制房间码
|
|
*/
|
|
const copyRoomCode = async () => {
|
|
if (!roomCode.value) return
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(roomCode.value)
|
|
ElMessage.success('房间码已复制')
|
|
} catch (error) {
|
|
ElMessage.error('复制失败')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 复制分享链接
|
|
*/
|
|
const copyShareLink = async () => {
|
|
const link = getShareLink()
|
|
if (!link) return
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(link)
|
|
ElMessage.success('链接已复制')
|
|
} catch (error) {
|
|
ElMessage.error('复制失败')
|
|
}
|
|
}
|
|
|
|
// ==================== 返回 ====================
|
|
|
|
return {
|
|
// 状态
|
|
roomCode,
|
|
roomInfo,
|
|
hostUser,
|
|
guestUser,
|
|
myRole,
|
|
myRoleName,
|
|
isHost,
|
|
messages,
|
|
lastSequence,
|
|
isLoading,
|
|
isConnected,
|
|
inputMessage,
|
|
|
|
// 计算属性
|
|
roomStatus,
|
|
isWaiting,
|
|
isReady,
|
|
isPracticing,
|
|
isCompleted,
|
|
partnerUser,
|
|
partnerRoleName,
|
|
chatMessages,
|
|
systemMessages,
|
|
|
|
// 方法
|
|
createRoom,
|
|
joinRoom,
|
|
fetchRoomDetail,
|
|
startPractice,
|
|
endPractice,
|
|
leaveRoom,
|
|
sendMessage,
|
|
fetchMessages,
|
|
startPolling,
|
|
stopPolling,
|
|
resetState,
|
|
getShareLink,
|
|
copyRoomCode,
|
|
copyShareLink
|
|
}
|
|
})
|