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:
bujinyuan
2026-03-02 17:15:21 +08:00
parent a77669ee99
commit 618c3f4409
5 changed files with 630 additions and 20 deletions

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

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