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:
160
frontend/src/api/meeting.js
Normal file
160
frontend/src/api/meeting.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 会议模块 REST API
|
||||
*
|
||||
* 对应后端路由:/api/v1/meeting/*(见 backend/go-service/app/meeting/router.go)
|
||||
*
|
||||
* 设计原则:
|
||||
* - 本文件仅做 REST 封装,业务层(store/meeting.js)统一调本文件方法
|
||||
* - WebRTC 媒体层信令通过 WS(services/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
|
||||
}
|
||||
Reference in New Issue
Block a user