feat(frontend): WebSocket 客户端 + Contact Store + API
- services/websocket.js: 单例 WS 连接管理(心跳/重连/事件分发) - store/websocket.js: Pinia Store 管理连接状态 - store/contact.js: 联系人 Store(好友/申请/分组/黑名单/在线状态) - api/contact.js: 联系人 REST API 封装(17 个接口) - api/user.js: 用户搜索 API Made-with: Cursor
This commit is contained in:
116
frontend/src/api/contact.js
Normal file
116
frontend/src/api/contact.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 联系人模块 API
|
||||||
|
*
|
||||||
|
* 对应后端路由:/api/v1/contacts/*
|
||||||
|
* 接口文档见:docs/api/frontend/contact.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get, post, put, del } from '@/utils/request'
|
||||||
|
|
||||||
|
/** 获取好友列表 */
|
||||||
|
const getFriendList = (groupId) => {
|
||||||
|
const params = groupId ? { group_id: groupId } : {}
|
||||||
|
return get('/api/v1/contacts', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送好友申请 */
|
||||||
|
const sendFriendRequest = (targetId, message = '') => {
|
||||||
|
return post('/api/v1/contacts/request', { target_id: targetId, message })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接受好友申请 */
|
||||||
|
const acceptRequest = (requestId) => {
|
||||||
|
return post('/api/v1/contacts/accept', { request_id: requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝好友申请 */
|
||||||
|
const rejectRequest = (requestId) => {
|
||||||
|
return post('/api/v1/contacts/reject', { request_id: requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取待处理的好友申请 */
|
||||||
|
const getPendingRequests = () => {
|
||||||
|
return get('/api/v1/contacts/requests')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除好友 */
|
||||||
|
const deleteFriend = (friendId) => {
|
||||||
|
return del(`/api/v1/contacts/${friendId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新好友备注 */
|
||||||
|
const updateRemark = (friendId, remark) => {
|
||||||
|
return put(`/api/v1/contacts/${friendId}/remark`, { remark })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉黑用户 */
|
||||||
|
const blockUser = (targetId) => {
|
||||||
|
return post('/api/v1/contacts/block', { target_id: targetId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消拉黑 */
|
||||||
|
const unblockUser = (userId) => {
|
||||||
|
return del(`/api/v1/contacts/block/${userId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取黑名单 */
|
||||||
|
const getBlockList = () => {
|
||||||
|
return get('/api/v1/contacts/block')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取分组列表 */
|
||||||
|
const getGroups = () => {
|
||||||
|
return get('/api/v1/contacts/groups')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建分组 */
|
||||||
|
const createGroup = (name) => {
|
||||||
|
return post('/api/v1/contacts/groups', { name })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改分组 */
|
||||||
|
const updateGroup = (groupId, name, sortOrder) => {
|
||||||
|
const data = { name }
|
||||||
|
if (sortOrder !== undefined) data.sort_order = sortOrder
|
||||||
|
return put(`/api/v1/contacts/groups/${groupId}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除分组 */
|
||||||
|
const deleteGroup = (groupId) => {
|
||||||
|
return del(`/api/v1/contacts/groups/${groupId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移动好友到分组 */
|
||||||
|
const moveToGroup = (friendId, groupId) => {
|
||||||
|
return put(`/api/v1/contacts/${friendId}/group`, { group_id: groupId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索用户 */
|
||||||
|
const searchUsers = (keyword, page = 1, pageSize = 20) => {
|
||||||
|
return get('/api/v1/users/search', { keyword, page, page_size: pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 好友推荐 */
|
||||||
|
const getRecommendFriends = () => {
|
||||||
|
return get('/api/v1/contacts/recommend')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getFriendList,
|
||||||
|
sendFriendRequest,
|
||||||
|
acceptRequest,
|
||||||
|
rejectRequest,
|
||||||
|
getPendingRequests,
|
||||||
|
deleteFriend,
|
||||||
|
updateRemark,
|
||||||
|
blockUser,
|
||||||
|
unblockUser,
|
||||||
|
getBlockList,
|
||||||
|
getGroups,
|
||||||
|
createGroup,
|
||||||
|
updateGroup,
|
||||||
|
deleteGroup,
|
||||||
|
moveToGroup,
|
||||||
|
searchUsers,
|
||||||
|
getRecommendFriends
|
||||||
|
}
|
||||||
16
frontend/src/api/user.js
Normal file
16
frontend/src/api/user.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 用户搜索 API
|
||||||
|
*
|
||||||
|
* 对应后端路由:/api/v1/users/*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get } from '@/utils/request'
|
||||||
|
|
||||||
|
/** 搜索用户(按用户名/昵称模糊匹配) */
|
||||||
|
const searchUsers = (keyword, page = 1, pageSize = 20) => {
|
||||||
|
return get('/api/v1/users/search', { keyword, page, page_size: pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
searchUsers
|
||||||
|
}
|
||||||
@@ -1,27 +1,241 @@
|
|||||||
/**
|
/**
|
||||||
* WebSocket 服务模块(占位)
|
* WebSocket 连接管理服务
|
||||||
*
|
*
|
||||||
* 后续阶段实现 IM 即时通讯和会议信令时使用。
|
* 单例模式,管理与后端的 WebSocket 长连接,提供:
|
||||||
* 职责:
|
* - JWT Token 认证连接
|
||||||
* - 建立和管理 WebSocket 连接
|
* - 心跳保活(30s 间隔)
|
||||||
* - 心跳保活机制
|
* - 断线自动重连(指数退避 1s→2s→4s→8s→30s max)
|
||||||
* - 断线自动重连
|
* - 事件分发:on(event, callback) / off(event, callback)
|
||||||
* - 消息收发与事件分发
|
|
||||||
*
|
*
|
||||||
* 对应架构设计见:docs/architecture/system-architecture.md 第 4.1 节
|
* 对应后端:GET /ws?token=xxx
|
||||||
* 日志规范见:docs/architecture/system-architecture.md 第 8.7 节
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: Phase 2 - IM 模块开发时实现
|
import { BASE_URL } from '@/utils/request'
|
||||||
// 预期功能:
|
import { getToken } from '@/utils/storage'
|
||||||
// - connect(token) — 建立 WebSocket 连接,附带认证 Token
|
|
||||||
// - disconnect() — 主动断开连接
|
|
||||||
// - send(event, data) — 发送消息
|
|
||||||
// - on(event, callback) — 监听事件
|
|
||||||
// - off(event, callback) — 移除监听
|
|
||||||
// - 自动心跳(30s 间隔)
|
|
||||||
// - 断线重连(指数退避,最大 5 次)
|
|
||||||
|
|
||||||
export default {
|
const WS_BASE = BASE_URL.replace(/^http/, 'ws')
|
||||||
// 占位导出,后续实现时替换
|
const HEARTBEAT_INTERVAL = 30000
|
||||||
|
const RECONNECT_BASE_DELAY = 1000
|
||||||
|
const RECONNECT_MAX_DELAY = 30000
|
||||||
|
const MAX_RECONNECT_ATTEMPTS = Infinity
|
||||||
|
|
||||||
|
class WebSocketService {
|
||||||
|
constructor() {
|
||||||
|
this.ws = null
|
||||||
|
this.listeners = new Map()
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
this.reconnectTimer = null
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.isManualClose = false
|
||||||
|
this.seq = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立 WebSocket 连接
|
||||||
|
* @param {string} [token] - JWT Token,不传则从本地存储获取
|
||||||
|
*/
|
||||||
|
connect(token) {
|
||||||
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = token || getToken()
|
||||||
|
if (!accessToken) {
|
||||||
|
console.warn('[WS] 无 Token,无法连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isManualClose = false
|
||||||
|
const url = `${WS_BASE}/ws?token=${accessToken}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
// #ifdef H5
|
||||||
|
this.ws = new WebSocket(url)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
this.ws = uni.connectSocket({
|
||||||
|
url,
|
||||||
|
complete: () => {}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
this._bindEvents()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[WS] 连接创建失败', err)
|
||||||
|
this._scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主动断开连接 */
|
||||||
|
disconnect() {
|
||||||
|
this.isManualClose = true
|
||||||
|
this._clearTimers()
|
||||||
|
if (this.ws) {
|
||||||
|
// #ifdef H5
|
||||||
|
this.ws.close()
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
uni.closeSocket()
|
||||||
|
// #endif
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
* @param {string} event - 事件类型
|
||||||
|
* @param {Object} [data] - 业务数据
|
||||||
|
* @returns {number} 消息序号
|
||||||
|
*/
|
||||||
|
send(event, data = {}) {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn('[WS] 连接未就绪,无法发送')
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.seq++
|
||||||
|
const msg = JSON.stringify({
|
||||||
|
event,
|
||||||
|
seq: this.seq,
|
||||||
|
data,
|
||||||
|
time: new Date().toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
this.ws.send(msg)
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
uni.sendSocketMessage({ data: msg })
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
return this.seq
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册事件监听
|
||||||
|
* @param {string} event - 事件类型
|
||||||
|
* @param {Function} callback - 回调函数
|
||||||
|
*/
|
||||||
|
on(event, callback) {
|
||||||
|
if (!this.listeners.has(event)) {
|
||||||
|
this.listeners.set(event, new Set())
|
||||||
|
}
|
||||||
|
this.listeners.get(event).add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件监听
|
||||||
|
* @param {string} event - 事件类型
|
||||||
|
* @param {Function} callback - 回调函数
|
||||||
|
*/
|
||||||
|
off(event, callback) {
|
||||||
|
const cbs = this.listeners.get(event)
|
||||||
|
if (cbs) {
|
||||||
|
cbs.delete(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定 WebSocket 事件处理 */
|
||||||
|
_bindEvents() {
|
||||||
|
// #ifdef H5
|
||||||
|
this.ws.onopen = () => this._onOpen()
|
||||||
|
this.ws.onclose = (e) => this._onClose(e)
|
||||||
|
this.ws.onerror = (e) => this._onError(e)
|
||||||
|
this.ws.onmessage = (e) => this._onMessage(e.data)
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef H5
|
||||||
|
uni.onSocketOpen(() => this._onOpen())
|
||||||
|
uni.onSocketClose((e) => this._onClose(e))
|
||||||
|
uni.onSocketError((e) => this._onError(e))
|
||||||
|
uni.onSocketMessage((e) => this._onMessage(e.data))
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
_onOpen() {
|
||||||
|
console.log('[WS] 连接成功')
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this._startHeartbeat()
|
||||||
|
this._emit('_connected')
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClose(e) {
|
||||||
|
console.log('[WS] 连接关闭', e)
|
||||||
|
this._clearTimers()
|
||||||
|
this._emit('_disconnected')
|
||||||
|
|
||||||
|
if (!this.isManualClose) {
|
||||||
|
this._scheduleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onError(e) {
|
||||||
|
console.error('[WS] 连接错误', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMessage(data) {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(data)
|
||||||
|
this._emit(msg.event, msg)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WS] 消息解析失败', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分发事件给注册的监听器 */
|
||||||
|
_emit(event, data) {
|
||||||
|
const cbs = this.listeners.get(event)
|
||||||
|
if (cbs) {
|
||||||
|
cbs.forEach(cb => {
|
||||||
|
try {
|
||||||
|
cb(data)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[WS] 事件处理器异常: ${event}`, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动心跳 */
|
||||||
|
_startHeartbeat() {
|
||||||
|
this._clearTimers()
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
this.send('heartbeat')
|
||||||
|
}, HEARTBEAT_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 调度重连(指数退避) */
|
||||||
|
_scheduleReconnect() {
|
||||||
|
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||||
|
console.error('[WS] 超过最大重连次数')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.min(
|
||||||
|
RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts),
|
||||||
|
RECONNECT_MAX_DELAY
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`[WS] ${delay}ms 后重连 (第 ${this.reconnectAttempts + 1} 次)`)
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectAttempts++
|
||||||
|
this.connect()
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearTimers() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer)
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
}
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全局单例 */
|
||||||
|
const wsService = new WebSocketService()
|
||||||
|
export default wsService
|
||||||
|
|||||||
195
frontend/src/store/contact.js
Normal file
195
frontend/src/store/contact.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* 联系人状态 Store
|
||||||
|
*
|
||||||
|
* 管理好友列表、好友申请、分组、黑名单和在线状态,提供:
|
||||||
|
* - 好友列表增删改查
|
||||||
|
* - 好友申请发送/接受/拒绝
|
||||||
|
* - 好友分组管理
|
||||||
|
* - 在线状态跟踪(结合 WebSocket 推送)
|
||||||
|
*
|
||||||
|
* 对应后端 API:/api/v1/contacts/*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import contactApi from '@/api/contact'
|
||||||
|
import wsService from '@/services/websocket'
|
||||||
|
|
||||||
|
export const useContactStore = defineStore('contact', () => {
|
||||||
|
/** 好友列表 */
|
||||||
|
const friendList = ref([])
|
||||||
|
|
||||||
|
/** 待处理的好友申请列表 */
|
||||||
|
const pendingRequests = ref([])
|
||||||
|
|
||||||
|
/** 好友分组列表 */
|
||||||
|
const groups = ref([])
|
||||||
|
|
||||||
|
/** 黑名单列表 */
|
||||||
|
const blockList = ref([])
|
||||||
|
|
||||||
|
/** 在线状态映射 userID -> boolean */
|
||||||
|
const onlineMap = ref({})
|
||||||
|
|
||||||
|
/** 未读好友申请数 */
|
||||||
|
const pendingCount = computed(() => pendingRequests.value.length)
|
||||||
|
|
||||||
|
// ==================== Actions ====================
|
||||||
|
|
||||||
|
/** 获取好友列表 */
|
||||||
|
const fetchFriends = async (groupId) => {
|
||||||
|
const res = await contactApi.getFriendList(groupId)
|
||||||
|
friendList.value = res.data || []
|
||||||
|
return friendList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取待处理的好友申请 */
|
||||||
|
const fetchPendingRequests = async () => {
|
||||||
|
const res = await contactApi.getPendingRequests()
|
||||||
|
pendingRequests.value = res.data || []
|
||||||
|
return pendingRequests.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送好友申请 */
|
||||||
|
const sendRequest = async (targetId, message) => {
|
||||||
|
await contactApi.sendFriendRequest(targetId, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接受好友申请 */
|
||||||
|
const acceptRequest = async (requestId) => {
|
||||||
|
await contactApi.acceptRequest(requestId)
|
||||||
|
pendingRequests.value = pendingRequests.value.filter(r => r.id !== requestId)
|
||||||
|
await fetchFriends()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝好友申请 */
|
||||||
|
const rejectRequest = async (requestId) => {
|
||||||
|
await contactApi.rejectRequest(requestId)
|
||||||
|
pendingRequests.value = pendingRequests.value.filter(r => r.id !== requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除好友 */
|
||||||
|
const deleteFriend = async (friendId) => {
|
||||||
|
await contactApi.deleteFriend(friendId)
|
||||||
|
friendList.value = friendList.value.filter(f => f.user_id !== friendId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新好友备注 */
|
||||||
|
const updateRemark = async (friendId, remark) => {
|
||||||
|
await contactApi.updateRemark(friendId, remark)
|
||||||
|
const friend = friendList.value.find(f => f.user_id === friendId)
|
||||||
|
if (friend) friend.remark = remark
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉黑用户 */
|
||||||
|
const blockUser = async (targetId) => {
|
||||||
|
await contactApi.blockUser(targetId)
|
||||||
|
friendList.value = friendList.value.filter(f => f.user_id !== targetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消拉黑 */
|
||||||
|
const unblockUser = async (userId) => {
|
||||||
|
await contactApi.unblockUser(userId)
|
||||||
|
blockList.value = blockList.value.filter(b => b.user_id !== userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取黑名单列表 */
|
||||||
|
const fetchBlockList = async () => {
|
||||||
|
const res = await contactApi.getBlockList()
|
||||||
|
blockList.value = res.data || []
|
||||||
|
return blockList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取分组列表 */
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
const res = await contactApi.getGroups()
|
||||||
|
groups.value = res.data || []
|
||||||
|
return groups.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建分组 */
|
||||||
|
const createGroup = async (name) => {
|
||||||
|
const res = await contactApi.createGroup(name)
|
||||||
|
groups.value.push(res.data)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新分组 */
|
||||||
|
const updateGroup = async (groupId, name, sortOrder) => {
|
||||||
|
await contactApi.updateGroup(groupId, name, sortOrder)
|
||||||
|
const group = groups.value.find(g => g.id === groupId)
|
||||||
|
if (group) {
|
||||||
|
group.name = name
|
||||||
|
if (sortOrder !== undefined) group.sort_order = sortOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除分组 */
|
||||||
|
const deleteGroup = async (groupId) => {
|
||||||
|
await contactApi.deleteGroup(groupId)
|
||||||
|
groups.value = groups.value.filter(g => g.id !== groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移动好友到分组 */
|
||||||
|
const moveToGroup = async (friendId, groupId) => {
|
||||||
|
await contactApi.moveToGroup(friendId, groupId)
|
||||||
|
const friend = friendList.value.find(f => f.user_id === friendId)
|
||||||
|
if (friend) friend.group_id = groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新单个用户的在线状态 */
|
||||||
|
const setOnline = (userId, online) => {
|
||||||
|
onlineMap.value[userId] = online
|
||||||
|
const friend = friendList.value.find(f => f.user_id === userId)
|
||||||
|
if (friend) friend.is_online = online
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 WebSocket 事件监听 */
|
||||||
|
const initWsListeners = () => {
|
||||||
|
wsService.on('notify.friend.request', () => {
|
||||||
|
fetchPendingRequests()
|
||||||
|
})
|
||||||
|
|
||||||
|
wsService.on('contact.request.accepted', () => {
|
||||||
|
fetchFriends()
|
||||||
|
})
|
||||||
|
|
||||||
|
wsService.on('user.status.online', (msg) => {
|
||||||
|
if (msg && msg.data) {
|
||||||
|
setOnline(msg.data.user_id, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wsService.on('user.status.offline', (msg) => {
|
||||||
|
if (msg && msg.data) {
|
||||||
|
setOnline(msg.data.user_id, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
friendList,
|
||||||
|
pendingRequests,
|
||||||
|
groups,
|
||||||
|
blockList,
|
||||||
|
onlineMap,
|
||||||
|
pendingCount,
|
||||||
|
fetchFriends,
|
||||||
|
fetchPendingRequests,
|
||||||
|
sendRequest,
|
||||||
|
acceptRequest,
|
||||||
|
rejectRequest,
|
||||||
|
deleteFriend,
|
||||||
|
updateRemark,
|
||||||
|
blockUser,
|
||||||
|
unblockUser,
|
||||||
|
fetchBlockList,
|
||||||
|
fetchGroups,
|
||||||
|
createGroup,
|
||||||
|
updateGroup,
|
||||||
|
deleteGroup,
|
||||||
|
moveToGroup,
|
||||||
|
setOnline,
|
||||||
|
initWsListeners
|
||||||
|
}
|
||||||
|
})
|
||||||
69
frontend/src/store/websocket.js
Normal file
69
frontend/src/store/websocket.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket 连接状态 Store
|
||||||
|
*
|
||||||
|
* 管理 WebSocket 连接生命周期,提供:
|
||||||
|
* - 连接/断开操作
|
||||||
|
* - 连接状态跟踪(connected/disconnected)
|
||||||
|
* - 与 user store 联动(登录时自动连接,登出时断开)
|
||||||
|
*
|
||||||
|
* 对应服务层:services/websocket.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import wsService from '@/services/websocket'
|
||||||
|
|
||||||
|
export const useWebSocketStore = defineStore('websocket', () => {
|
||||||
|
/** 连接状态 */
|
||||||
|
const connected = ref(false)
|
||||||
|
|
||||||
|
/** 是否已连接 */
|
||||||
|
const isConnected = computed(() => connected.value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 WebSocket 连接
|
||||||
|
* @param {string} [token] - JWT Token
|
||||||
|
*/
|
||||||
|
const connect = (token) => {
|
||||||
|
wsService.on('_connected', () => {
|
||||||
|
connected.value = true
|
||||||
|
})
|
||||||
|
wsService.on('_disconnected', () => {
|
||||||
|
connected.value = false
|
||||||
|
})
|
||||||
|
wsService.connect(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 断开 WebSocket 连接 */
|
||||||
|
const disconnect = () => {
|
||||||
|
wsService.disconnect()
|
||||||
|
connected.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册事件监听(代理 wsService)
|
||||||
|
* @param {string} event - 事件类型
|
||||||
|
* @param {Function} callback - 回调函数
|
||||||
|
*/
|
||||||
|
const on = (event, callback) => {
|
||||||
|
wsService.on(event, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件监听(代理 wsService)
|
||||||
|
* @param {string} event - 事件类型
|
||||||
|
* @param {Function} callback - 回调函数
|
||||||
|
*/
|
||||||
|
const off = (event, callback) => {
|
||||||
|
wsService.off(event, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connected,
|
||||||
|
isConnected,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
on,
|
||||||
|
off
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user