From 1b1bd7536dc09b5da8a25304741293ce838fb4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 18 Aug 2025 11:30:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20VITE=5FAPI=5FWS=5FURL=20?= =?UTF-8?q?=E7=92=B0=E5=A2=83=E8=AE=8A=E6=95=B8=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=B8=BB=E7=A8=8B=E5=BC=8F=E4=BB=A5=E5=BC=95=E5=85=A5=E5=9A=B4?= =?UTF-8?q?=E6=A0=BC=E6=A8=A1=E5=BC=8F=E5=8C=85=E8=A3=9D=E5=99=A8=EF=BC=8C?= =?UTF-8?q?=E4=B8=A6=E5=9C=A8=E7=99=BB=E9=8C=84=E9=A0=81=E9=9D=A2=E4=B8=AD?= =?UTF-8?q?=E6=95=B4=E5=90=88=E8=A7=B8=E5=AE=A2=E5=AF=B6=E7=94=A8=E6=88=B6?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E7=8D=B2=E5=8F=96=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E8=AA=BF=E6=95=B4=E8=AB=8B=E6=B1=82=E6=A8=A1=E7=B5=84=E4=BB=A5?= =?UTF-8?q?=E5=8B=95=E6=85=8B=E7=8D=B2=E5=8F=96=20token2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/.env.development | 1 + Cunkebao/.env.production | 1 + Cunkebao/src/api/request2.ts | 6 +- Cunkebao/src/components/WebSocketExample.tsx | 250 ++++++++++++ Cunkebao/src/main.tsx | 10 +- Cunkebao/src/pages/login/Login.tsx | 31 +- Cunkebao/src/pages/login/api.ts | 11 + Cunkebao/src/store/index.ts | 10 + Cunkebao/src/store/module/ckchat.data.ts | 51 +++ Cunkebao/src/store/module/ckchat.ts | 89 +++++ Cunkebao/src/store/module/websocket.ts | 376 +++++++++++++++++++ 11 files changed, 825 insertions(+), 11 deletions(-) create mode 100644 Cunkebao/src/components/WebSocketExample.tsx create mode 100644 Cunkebao/src/store/module/ckchat.data.ts create mode 100644 Cunkebao/src/store/module/ckchat.ts create mode 100644 Cunkebao/src/store/module/websocket.ts diff --git a/Cunkebao/.env.development b/Cunkebao/.env.development index 3fa6d21b..2ddb67f5 100644 --- a/Cunkebao/.env.development +++ b/Cunkebao/.env.development @@ -1,5 +1,6 @@ # 基础环境变量示例 VITE_API_BASE_URL=http://www.yishi.com VITE_API_BASE_URL2=https://kf.quwanzhi.com:9991 +VITE_API_WS_URL=wss://kf.quwanzhi.com:9993 # VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=存客宝 diff --git a/Cunkebao/.env.production b/Cunkebao/.env.production index 5b58400c..838935bb 100644 --- a/Cunkebao/.env.production +++ b/Cunkebao/.env.production @@ -1,5 +1,6 @@ # 基础环境变量示例 VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_API_BASE_URL2=https://kf.quwanzhi.com:9991 +VITE_API_WS_URL=wss://kf.quwanzhi.com:9993 # VITE_API_BASE_URL=http://www.yishi.com VITE_APP_TITLE=存客宝 diff --git a/Cunkebao/src/api/request2.ts b/Cunkebao/src/api/request2.ts index 544181c4..cacbdaf7 100644 --- a/Cunkebao/src/api/request2.ts +++ b/Cunkebao/src/api/request2.ts @@ -6,7 +6,6 @@ import axios, { } from "axios"; import { Toast } from "antd-mobile"; import { useUserStore } from "@/store/module/user"; -const { token2 } = useUserStore.getState(); const DEFAULT_DEBOUNCE_GAP = 1000; const debounceMap = new Map(); @@ -27,9 +26,12 @@ const instance: AxiosInstance = axios.create({ }); instance.interceptors.request.use((config: any) => { + // 在每次请求时动态获取最新的 token2 + const { token2 } = useUserStore.getState(); + if (token2) { config.headers = config.headers || {}; - config.headers["Authorization"] = `Bearer ${token2}`; + config.headers["Authorization"] = `bearer ${token2}`; } return config; }); diff --git a/Cunkebao/src/components/WebSocketExample.tsx b/Cunkebao/src/components/WebSocketExample.tsx new file mode 100644 index 00000000..060bd98b --- /dev/null +++ b/Cunkebao/src/components/WebSocketExample.tsx @@ -0,0 +1,250 @@ +import React, { useEffect, useState } from "react"; +import { Button, Card, List, Badge, Toast } from "antd-mobile"; +import { + useWebSocketStore, + WebSocketStatus, + WebSocketMessage, +} from "@/store/module/websocket"; + +/** + * WebSocket使用示例组件 + * 展示如何使用WebSocket store进行消息收发 + */ +const WebSocketExample: React.FC = () => { + const [messageInput, setMessageInput] = useState(""); + + // 使用WebSocket store + const { + status, + messages, + unreadCount, + connect, + disconnect, + sendMessage, + sendCommand, + clearMessages, + markAsRead, + reconnect, + } = useWebSocketStore(); + + // 连接状态显示 + const getStatusText = () => { + switch (status) { + case WebSocketStatus.DISCONNECTED: + return "未连接"; + case WebSocketStatus.CONNECTING: + return "连接中..."; + case WebSocketStatus.CONNECTED: + return "已连接"; + case WebSocketStatus.RECONNECTING: + return "重连中..."; + case WebSocketStatus.ERROR: + return "连接错误"; + default: + return "未知状态"; + } + }; + + // 获取状态颜色 + const getStatusColor = () => { + switch (status) { + case WebSocketStatus.CONNECTED: + return "success"; + case WebSocketStatus.CONNECTING: + case WebSocketStatus.RECONNECTING: + return "warning"; + case WebSocketStatus.ERROR: + return "danger"; + default: + return "default"; + } + }; + + // 发送消息 + const handleSendMessage = () => { + if (!messageInput.trim()) { + Toast.show({ content: "请输入消息内容", position: "top" }); + return; + } + + sendMessage({ + type: "chat", + content: { + text: messageInput, + timestamp: Date.now(), + }, + sender: "user", + receiver: "all", + }); + + setMessageInput(""); + }; + + // 发送命令 + const handleSendCommand = (cmdType: string) => { + sendCommand(cmdType, { + data: "示例数据", + timestamp: Date.now(), + }); + }; + + // 格式化时间 + const formatTime = (timestamp: number) => { + return new Date(timestamp).toLocaleTimeString(); + }; + + return ( +
+ +
+ +
+ + {getStatusText()} +
+ +
+ + + + + +
+ + + 0 ? `(${unreadCount} 条未读)` : ""}`} + extra={ +
+ + +
+ } + style={{ marginTop: "16px" }} + > + + {messages.length === 0 ? ( + 暂无消息 + ) : ( + messages.map((message: WebSocketMessage) => ( + +
+ {formatTime(message.timestamp)} - {message.type} +
+
+ {typeof message.content === "string" + ? message.content + : JSON.stringify(message.content, null, 2)} +
+
+ )) + )} +
+
+ + +
+ setMessageInput(e.target.value)} + placeholder="输入消息内容" + style={{ + flex: 1, + padding: "8px 12px", + border: "1px solid #d9d9d9", + borderRadius: "4px", + fontSize: "14px", + }} + onKeyPress={e => e.key === "Enter" && handleSendMessage()} + /> + +
+ +
+ + + + + +
+
+ + +
+

1. 点击"连接"按钮建立WebSocket连接

+

2. 连接成功后可以发送消息和命令

+

3. 收到的消息会显示在消息列表中

+

4. 页面刷新后会自动重连(如果之前是连接状态)

+

5. 支持自动重连和错误处理

+
+
+
+ ); +}; + +export default WebSocketExample; diff --git a/Cunkebao/src/main.tsx b/Cunkebao/src/main.tsx index f4ae337c..3fa26fe3 100644 --- a/Cunkebao/src/main.tsx +++ b/Cunkebao/src/main.tsx @@ -2,7 +2,15 @@ import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; import "./styles/global.scss"; +// 引入错误处理器来抑制findDOMNode警告 +import "./utils/errorHandler"; +import StrictModeWrapper from "./components/StrictModeWrapper"; // import VConsole from "vconsole"; // new VConsole(); + const root = createRoot(document.getElementById("root")!); -root.render(); +root.render( + + + +); diff --git a/Cunkebao/src/pages/login/Login.tsx b/Cunkebao/src/pages/login/Login.tsx index 9691a663..33f16e5b 100644 --- a/Cunkebao/src/pages/login/Login.tsx +++ b/Cunkebao/src/pages/login/Login.tsx @@ -7,11 +7,14 @@ import { UserOutline, } from "antd-mobile-icons"; import { useUserStore } from "@/store/module/user"; +import { useCkChatStore } from "@/store/module/ckchat"; +import { useWebSocketStore } from "@/store/module/websocket"; import { loginWithPassword, loginWithCode, sendVerificationCode, loginWithToken, + getChuKeBaoUserInfo, } from "./api"; import style from "./login.module.scss"; @@ -24,6 +27,7 @@ const Login: React.FC = () => { const [agreeToTerms, setAgreeToTerms] = useState(false); const { login, login2 } = useUserStore(); + const { setUserInfo, getAccountId } = useCkChatStore(); // 倒计时效果 useEffect(() => { @@ -71,15 +75,26 @@ const Login: React.FC = () => { Toast.show({ content: "请同意用户协议和隐私政策", position: "top" }); return; } - setLoading(true); - getToken(values) - .then(() => { - getToken2(); - }) - .finally(() => { - setLoading(false); + getToken(values).then(() => { + getChuKeBaoUserInfo().then(res => { + setUserInfo(res); + getToken2().then(Token => { + // // 使用WebSocket store连接 + // const { connect } = useWebSocketStore.getState(); + // connect({ + // accessToken: Token, + // accountId: getAccountId()?.toString() || "", + // client: "kefu-client", + // autoReconnect: true, + // reconnectInterval: 3000, + // maxReconnectAttempts: 5, + // }); + }); }); + setLoading(false); + }); }; + const getToken = (values: any) => { return new Promise((resolve, reject) => { // 添加typeId参数 @@ -118,7 +133,7 @@ const Login: React.FC = () => { const response = loginWithToken(params); response.then(res => { login2(res.access_token); - resolve(res); + resolve(res.access_token); }); response.catch(err => { reject(err); diff --git a/Cunkebao/src/pages/login/api.ts b/Cunkebao/src/pages/login/api.ts index 16d81e33..897abbc9 100644 --- a/Cunkebao/src/pages/login/api.ts +++ b/Cunkebao/src/pages/login/api.ts @@ -24,6 +24,12 @@ export function logout() { export function getUserInfo() { return request("/v1/auth/user-info", {}, "GET"); } + +// ================================================================== +// 触客宝接口; 2025年8月16日 17:19:15 +// 开发:yongpxu +// ================================================================== + //触客宝登陆 export function loginWithToken(params: any) { return request2( @@ -38,3 +44,8 @@ export function loginWithToken(params: any) { 1000, ); } + +// 获取触客宝用户信息 +export function getChuKeBaoUserInfo() { + return request2("/api/account/self", {}, "GET"); +} diff --git a/Cunkebao/src/store/index.ts b/Cunkebao/src/store/index.ts index 14263a67..f4f3d9af 100644 --- a/Cunkebao/src/store/index.ts +++ b/Cunkebao/src/store/index.ts @@ -2,11 +2,13 @@ export * from "./module/user"; export * from "./module/app"; export * from "./module/settings"; +export * from "./module/websocket"; // 导入store实例 import { useUserStore } from "./module/user"; import { useAppStore } from "./module/app"; import { useSettingsStore } from "./module/settings"; +import { useWebSocketStore } from "./module/websocket"; // 导出持久化store创建函数 export { @@ -32,6 +34,7 @@ export interface StoreState { user: ReturnType; app: ReturnType; settings: ReturnType; + websocket: ReturnType; } // 便利的store访问函数 @@ -39,12 +42,14 @@ export const getStores = (): StoreState => ({ user: useUserStore.getState(), app: useAppStore.getState(), settings: useSettingsStore.getState(), + websocket: useWebSocketStore.getState(), }); // 获取特定store状态 export const getUserStore = () => useUserStore.getState(); export const getAppStore = () => useAppStore.getState(); export const getSettingsStore = () => useSettingsStore.getState(); +export const getWebSocketStore = () => useWebSocketStore.getState(); // 清除所有持久化数据(使用工具函数) export const clearAllPersistedData = clearAllData; @@ -56,6 +61,7 @@ export const getPersistKeys = () => Object.values(PERSIST_KEYS); export const subscribeToUserStore = useUserStore.subscribe; export const subscribeToAppStore = useAppStore.subscribe; export const subscribeToSettingsStore = useSettingsStore.subscribe; +export const subscribeToWebSocketStore = useWebSocketStore.subscribe; // 组合订阅函数 export const subscribeToAllStores = (callback: (state: StoreState) => void) => { @@ -68,10 +74,14 @@ export const subscribeToAllStores = (callback: (state: StoreState) => void) => { const unsubscribeSettings = useSettingsStore.subscribe(() => { callback(getStores()); }); + const unsubscribeWebSocket = useWebSocketStore.subscribe(() => { + callback(getStores()); + }); return () => { unsubscribeUser(); unsubscribeApp(); unsubscribeSettings(); + unsubscribeWebSocket(); }; }; diff --git a/Cunkebao/src/store/module/ckchat.data.ts b/Cunkebao/src/store/module/ckchat.data.ts new file mode 100644 index 00000000..97c046a0 --- /dev/null +++ b/Cunkebao/src/store/module/ckchat.data.ts @@ -0,0 +1,51 @@ +// 账户信息接口 +export interface CkAccount { + id: number; + realName: string; + nickname: string | null; + memo: string | null; + avatar: string; + userName: string; + secret: string; + accountType: number; + departmentId: number; + useGoogleSecretKey: boolean; + hasVerifyGoogleSecret: boolean; +} + +// 权限片段接口 +export interface PrivilegeFrag { + // 根据实际数据结构补充 + [key: string]: any; +} + +// 租户信息接口 +export interface CkTenant { + id: number; + name: string; + guid: string; + thirdParty: string | null; + tenantType: number; + deployName: string; +} + +// 触客宝用户信息接口 +export interface CkUserInfo { + account: CkAccount; + privilegeFrags: PrivilegeFrag[]; + tenant: CkTenant; +} + +// 状态接口 +export interface CkChatState { + userInfo: CkUserInfo | null; + isLoggedIn: boolean; + setUserInfo: (userInfo: CkUserInfo) => void; + clearUserInfo: () => void; + updateAccount: (account: Partial) => void; + updateTenant: (tenant: Partial) => void; + getAccountId: () => number | null; + getTenantId: () => number | null; + getAccountName: () => string | null; + getTenantName: () => string | null; +} diff --git a/Cunkebao/src/store/module/ckchat.ts b/Cunkebao/src/store/module/ckchat.ts new file mode 100644 index 00000000..7d4a9337 --- /dev/null +++ b/Cunkebao/src/store/module/ckchat.ts @@ -0,0 +1,89 @@ +import { createPersistStore } from "@/store/createPersistStore"; + +import { CkChatState, CkUserInfo, CkAccount, CkTenant } from "./ckchat.data"; + +export const useCkChatStore = createPersistStore( + set => ({ + userInfo: null, + isLoggedIn: false, + + // 设置用户信息 + setUserInfo: (userInfo: CkUserInfo) => { + set({ userInfo, isLoggedIn: true }); + }, + + // 清除用户信息 + clearUserInfo: () => { + set({ userInfo: null, isLoggedIn: false }); + }, + + // 更新账户信息 + updateAccount: (account: Partial) => { + set(state => ({ + userInfo: state.userInfo + ? { + ...state.userInfo, + account: { ...state.userInfo.account, ...account }, + } + : null, + })); + }, + + // 更新租户信息 + updateTenant: (tenant: Partial) => { + set(state => ({ + userInfo: state.userInfo + ? { + ...state.userInfo, + tenant: { ...state.userInfo.tenant, ...tenant }, + } + : null, + })); + }, + + // 获取账户ID + getAccountId: () => { + const state = useCkChatStore.getState(); + return state.userInfo?.account?.id || null; + }, + + // 获取租户ID + getTenantId: () => { + const state = useCkChatStore.getState(); + return state.userInfo?.tenant?.id || null; + }, + + // 获取账户名称 + getAccountName: () => { + const state = useCkChatStore.getState(); + return ( + state.userInfo?.account?.realName || + state.userInfo?.account?.userName || + null + ); + }, + + // 获取租户名称 + getTenantName: () => { + const state = useCkChatStore.getState(); + return state.userInfo?.tenant?.name || null; + }, + }), + { + name: "ckchat-store", + partialize: state => ({ + userInfo: state.userInfo, + isLoggedIn: state.isLoggedIn, + }), + onRehydrateStorage: () => state => { + // console.log("CkChat store hydrated:", state); + }, + }, +); + +// 导出便捷的获取方法 +export const getCkAccountId = () => useCkChatStore.getState().getAccountId(); +export const getCkTenantId = () => useCkChatStore.getState().getTenantId(); +export const getCkAccountName = () => + useCkChatStore.getState().getAccountName(); +export const getCkTenantName = () => useCkChatStore.getState().getTenantName(); diff --git a/Cunkebao/src/store/module/websocket.ts b/Cunkebao/src/store/module/websocket.ts new file mode 100644 index 00000000..910fc7a7 --- /dev/null +++ b/Cunkebao/src/store/module/websocket.ts @@ -0,0 +1,376 @@ +import { createPersistStore } from "@/store/createPersistStore"; +import { Toast } from "antd-mobile"; +import { useUserStore } from "./user"; + +// WebSocket消息类型 +export interface WebSocketMessage { + id: string; + type: string; + content: any; + timestamp: number; + sender?: string; + receiver?: string; +} + +// WebSocket连接状态 +export enum WebSocketStatus { + DISCONNECTED = "disconnected", + CONNECTING = "connecting", + CONNECTED = "connected", + RECONNECTING = "reconnecting", + ERROR = "error", +} + +// WebSocket配置 +interface WebSocketConfig { + url: string; + client: string; + accountId: string; + accessToken: string; + autoReconnect: boolean; + reconnectInterval: number; + maxReconnectAttempts: number; +} + +interface WebSocketState { + // 连接状态 + status: WebSocketStatus; + ws: WebSocket | null; + + // 配置信息 + config: WebSocketConfig | null; + + // 消息相关 + messages: WebSocketMessage[]; + unreadCount: number; + + // 重连相关 + reconnectAttempts: number; + reconnectTimer: NodeJS.Timeout | null; + + // 方法 + connect: (config: Partial) => void; + disconnect: () => void; + sendMessage: (message: Omit) => void; + sendCommand: (cmdType: string, data?: any) => void; + clearMessages: () => void; + markAsRead: () => void; + reconnect: () => void; + + // 内部方法 + _handleOpen: () => void; + _handleMessage: (event: MessageEvent) => void; + _handleClose: (event: CloseEvent) => void; + _handleError: (event: Event) => void; + _startReconnectTimer: () => void; + _stopReconnectTimer: () => void; +} + +// 默认配置 +const DEFAULT_CONFIG: WebSocketConfig = { + url: (import.meta as any).env?.VITE_API_WS_URL || "ws://localhost:8080", + client: "kefu-client", + accountId: "", + accessToken: "", + autoReconnect: true, + reconnectInterval: 3000, + maxReconnectAttempts: 5, +}; + +export const useWebSocketStore = createPersistStore( + (set, get) => ({ + status: WebSocketStatus.DISCONNECTED, + ws: null, + config: null, + messages: [], + unreadCount: 0, + reconnectAttempts: 0, + reconnectTimer: null, + + // 连接WebSocket + connect: (config: Partial) => { + const currentState = get(); + + // 如果已经连接,先断开 + if (currentState.ws) { + currentState.disconnect(); + } + + // 合并配置 + const fullConfig: WebSocketConfig = { + ...DEFAULT_CONFIG, + ...config, + }; + + // 获取用户信息 + const { token, token2, user } = useUserStore.getState(); + const accessToken = fullConfig.accessToken || token2 || token; + + if (!accessToken) { + Toast.show({ content: "未找到有效的访问令牌", position: "top" }); + return; + } + + // 构建WebSocket URL + const params = { + client: fullConfig.client, + accountId: fullConfig.accountId || user?.s2_accountId || "", + accessToken: accessToken, + t: Date.now().toString(), + }; + + const wsUrl = + fullConfig.url + "?" + new URLSearchParams(params).toString(); + + set({ + status: WebSocketStatus.CONNECTING, + config: fullConfig, + }); + + try { + const ws = new WebSocket(wsUrl); + + // 绑定事件处理器 + ws.onopen = () => get()._handleOpen(); + ws.onmessage = event => get()._handleMessage(event); + ws.onclose = event => get()._handleClose(event); + ws.onerror = event => get()._handleError(event); + + set({ ws }); + + console.log("WebSocket连接创建成功", wsUrl); + } catch (error) { + console.error("WebSocket连接失败:", error); + set({ status: WebSocketStatus.ERROR }); + Toast.show({ content: "WebSocket连接失败", position: "top" }); + } + }, + + // 断开连接 + disconnect: () => { + const currentState = get(); + + if (currentState.ws) { + currentState.ws.close(); + } + + currentState._stopReconnectTimer(); + + set({ + status: WebSocketStatus.DISCONNECTED, + ws: null, + reconnectAttempts: 0, + }); + + console.log("WebSocket连接已断开"); + }, + + // 发送消息 + sendMessage: (message: Omit) => { + const currentState = get(); + + if ( + currentState.status !== WebSocketStatus.CONNECTED || + !currentState.ws + ) { + Toast.show({ content: "WebSocket未连接", position: "top" }); + return; + } + + const fullMessage: WebSocketMessage = { + ...message, + id: Date.now().toString(), + timestamp: Date.now(), + }; + + try { + currentState.ws.send(JSON.stringify(fullMessage)); + console.log("消息发送成功:", fullMessage); + } catch (error) { + console.error("消息发送失败:", error); + Toast.show({ content: "消息发送失败", position: "top" }); + } + }, + + // 发送命令 + sendCommand: (cmdType: string, data?: any) => { + const currentState = get(); + + if ( + currentState.status !== WebSocketStatus.CONNECTED || + !currentState.ws + ) { + Toast.show({ content: "WebSocket未连接", position: "top" }); + return; + } + + const { user } = useUserStore.getState(); + const { token, token2 } = useUserStore.getState(); + const accessToken = token2 || token; + + const command = { + accessToken: accessToken, + accountId: user?.s2_accountId, + client: currentState.config?.client || "kefu-client", + cmdType: cmdType, + seq: Date.now(), + ...data, + }; + + try { + currentState.ws.send(JSON.stringify(command)); + console.log("命令发送成功:", command); + } catch (error) { + console.error("命令发送失败:", error); + Toast.show({ content: "命令发送失败", position: "top" }); + } + }, + + // 清除消息 + clearMessages: () => { + set({ messages: [], unreadCount: 0 }); + }, + + // 标记为已读 + markAsRead: () => { + set({ unreadCount: 0 }); + }, + + // 重连 + reconnect: () => { + const currentState = get(); + + if (currentState.config) { + currentState.connect(currentState.config); + } + }, + + // 内部方法:处理连接打开 + _handleOpen: () => { + const currentState = get(); + + set({ + status: WebSocketStatus.CONNECTED, + reconnectAttempts: 0, + }); + + console.log("WebSocket连接成功"); + + // 发送登录命令 + if (currentState.config) { + currentState.sendCommand("CmdSignIn"); + } + + Toast.show({ content: "WebSocket连接成功", position: "top" }); + }, + + // 内部方法:处理消息接收 + _handleMessage: (event: MessageEvent) => { + try { + const data = JSON.parse(event.data); + console.log("收到WebSocket消息:", data); + + const currentState = get(); + const newMessage: WebSocketMessage = { + id: Date.now().toString(), + type: data.type || "message", + content: data, + timestamp: Date.now(), + sender: data.sender, + receiver: data.receiver, + }; + + set({ + messages: [...currentState.messages, newMessage], + unreadCount: currentState.unreadCount + 1, + }); + + // 可以在这里添加消息处理逻辑 + // 比如播放提示音、显示通知等 + } catch (error) { + console.error("解析WebSocket消息失败:", error); + } + }, + + // 内部方法:处理连接关闭 + _handleClose: (event: CloseEvent) => { + const currentState = get(); + + console.log("WebSocket连接关闭:", event.code, event.reason); + + set({ + status: WebSocketStatus.DISCONNECTED, + ws: null, + }); + + // 自动重连逻辑 + if ( + currentState.config?.autoReconnect && + currentState.reconnectAttempts < + (currentState.config?.maxReconnectAttempts || 5) + ) { + currentState._startReconnectTimer(); + } + }, + + // 内部方法:处理连接错误 + _handleError: (event: Event) => { + console.error("WebSocket连接错误:", event); + + set({ status: WebSocketStatus.ERROR }); + + Toast.show({ content: "WebSocket连接错误", position: "top" }); + }, + + // 内部方法:启动重连定时器 + _startReconnectTimer: () => { + const currentState = get(); + + currentState._stopReconnectTimer(); + + set({ + status: WebSocketStatus.RECONNECTING, + reconnectAttempts: currentState.reconnectAttempts + 1, + }); + + const timer = setTimeout(() => { + console.log( + `尝试重连 (${currentState.reconnectAttempts + 1}/${currentState.config?.maxReconnectAttempts})`, + ); + currentState.reconnect(); + }, currentState.config?.reconnectInterval || 3000); + + set({ reconnectTimer: timer }); + }, + + // 内部方法:停止重连定时器 + _stopReconnectTimer: () => { + const currentState = get(); + + if (currentState.reconnectTimer) { + clearTimeout(currentState.reconnectTimer); + set({ reconnectTimer: null }); + } + }, + }), + { + name: "websocket-store", + partialize: state => ({ + // 只持久化必要的状态,不持久化WebSocket实例 + status: state.status, + config: state.config, + messages: state.messages.slice(-100), // 只保留最近100条消息 + unreadCount: state.unreadCount, + reconnectAttempts: state.reconnectAttempts, + }), + onRehydrateStorage: () => state => { + // 页面刷新后,如果之前是连接状态,尝试重新连接 + if (state && state.status === WebSocketStatus.CONNECTED && state.config) { + // 延迟一下再重连,确保页面完全加载 + setTimeout(() => { + state.connect(state.config); + }, 1000); + } + }, + }, +);