feat(phase2e-2): 前端 mediasoup-client 集成 + Pinia meeting Store(Task 9)

- frontend/src/api/meeting.js:12 个 REST 接口封装,统一 unwrap envelope.data
- frontend/src/services/websocket.js:新增 sendWithAck(Promise 化 + 超时 + 序列号)
- frontend/src/utils/mediasoup-client.js:MediaEngine 包装 Device/Transport/Producer/Consumer
- frontend/src/store/meeting.js:Pinia 会议状态机,桥接 14 个 WS 事件 + cleanupStaleMeetings
- frontend/src/constants/meeting.js:状态枚举 + 事件名集中管理
- frontend/src/pages/meeting/debug.vue:临时调试页(H5 原生 video/audio DOM 绕过 uni 组件限制)
- backend:meeting.consume.resume WS 事件 + create/join 响应透传 router_id + rtp_capabilities
- 文档:frontend/meeting.md、websocket.md、CURRENT_STATUS、plan 全部同步 Task 9 落地

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 11:22:49 +08:00
parent e7f6a32dfe
commit f97fec24a8
22 changed files with 2314 additions and 60 deletions

160
frontend/src/api/meeting.js Normal file
View File

@@ -0,0 +1,160 @@
/**
* 会议模块 REST API
*
* 对应后端路由:/api/v1/meeting/*(见 backend/go-service/app/meeting/router.go
*
* 设计原则:
* - 本文件仅做 REST 封装业务层store/meeting.js统一调本文件方法
* - WebRTC 媒体层信令通过 WSservices/websocket.js + sendWithAck发送不在此处
* - 响应中的 DTO 字段命名与后端 DTO 完全一致(下划线),前端按需再转换
*/
import { get, post } from '@/utils/request'
/**
* 统一拆包utils/request.js 返回完整 envelope { code, message, data, trace_id, time }
* 业务层只关心 data 负载;这里一次性拆包,避免每个 store/页面都写 .data.xxx
* 对无 body 的响应(如 leave/end返回 null 而非 undefined方便调用方判空
*/
const unwrap = (promise) => promise.then((resp) => (resp && 'data' in resp ? (resp.data ?? null) : (resp ?? null)))
// ==================== 会议房间6 接口) ====================
/**
* 创建即时会议
* @param {Object} payload - { title, password?, max_members? }
* @returns {Promise<{ room: MeetingRoomDTO }>}
*/
const createRoom = (payload) => {
return unwrap(post('/api/v1/meeting/rooms', payload))
}
/**
* 查询会议详情(含参与者列表)
* @param {string} roomCode - 会议号XXX-XXX-XXX 或 9 位纯数字)
* @returns {Promise<{ room, participants, online_count }>}
*/
const getRoom = (roomCode) => {
return unwrap(get(`/api/v1/meeting/rooms/${roomCode}`))
}
/**
* 加入会议
* @param {string} roomCode - 会议号
* @param {Object} [payload] - { password? }
* @returns {Promise<{ room, participant, router_id }>}
*/
const joinRoom = (roomCode, payload = {}) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/join`, payload))
}
/**
* 离开会议
* @param {string} roomCode
* @returns {Promise<{ duration }>}
*/
const leaveRoom = (roomCode) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/leave`))
}
/**
* 结束会议(仅主持人可调)
* @param {string} roomCode
* @returns {Promise<void>}
*/
const endRoom = (roomCode) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/end`))
}
/**
* 我的会议列表(历史 + 进行中)
* @param {Object} [params] - { status?, before_id?, limit? }
* @returns {Promise<{ list, has_more }>}
*/
const listMyMeetings = (params = {}) => {
return unwrap(get('/api/v1/meeting/rooms/mine', params))
}
// ==================== 会议成员管理3 接口) ====================
/**
* 转让主持人
* @param {string} roomCode
* @param {number} targetUserID
* @returns {Promise<void>}
*/
const transferHost = (roomCode, targetUserID) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/transfer-host`, {
target_user_id: targetUserID
}))
}
/**
* 移除成员(踢人,仅主持人)
* @param {string} roomCode
* @param {number} userID
* @returns {Promise<void>}
*/
const kickMember = (roomCode, userID) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/kick`, { user_id: userID }))
}
/**
* 邀请用户(批量)
* @param {string} roomCode
* @param {number[]} inviteeIDs - 被邀请用户 ID 数组1~50
* @returns {Promise<{ pushed, skipped }>}
*/
const inviteUsers = (roomCode, inviteeIDs) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/invite`, {
invitee_ids: inviteeIDs
}))
}
// ==================== 邀请兑换1 接口) ====================
/**
* 兑换邀请 Token从通知点进链接时使用
* @param {string} token - 邀请链接中的 token 字符串
* @returns {Promise<{ room_code, inviter_id, has_password }>}
*/
const redeemInvite = (token) => {
return unwrap(post(`/api/v1/meeting/invite-tokens/${token}/redeem`))
}
// ==================== 会议内聊天2 接口) ====================
/**
* 发送会议内聊天消息
* @param {string} roomCode
* @param {string} content - 1~500 字
* @returns {Promise<{ message }>}
*/
const sendChat = (roomCode, content) => {
return unwrap(post(`/api/v1/meeting/rooms/${roomCode}/chats`, { content }))
}
/**
* 查询会议聊天历史(游标分页)
* @param {string} roomCode
* @param {Object} [params] - { before_id?, limit? }
* @returns {Promise<{ list, has_more }>}
*/
const listChats = (roomCode, params = {}) => {
return unwrap(get(`/api/v1/meeting/rooms/${roomCode}/chats`, params))
}
export default {
createRoom,
getRoom,
joinRoom,
leaveRoom,
endRoom,
listMyMeetings,
transferHost,
kickMember,
inviteUsers,
redeemInvite,
sendChat,
listChats
}