Files
012-kaopeilian/frontend/src/stores/duoPracticeStore.ts
yuliang_guo 64f5d567fa
Some checks failed
continuous-integration/drone/push Build is failing
feat: 实现 KPL 系统功能改进计划
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 格式
2026-01-30 14:22:35 +08:00

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