新增 VITE_API_WS_URL 環境變數,更新主程式以引入嚴格模式包裝器,並在登錄頁面中整合觸客寶用戶信息獲取功能,調整請求模組以動態獲取 token2。
This commit is contained in:
@@ -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=存客宝
|
||||
|
||||
@@ -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=存客宝
|
||||
|
||||
@@ -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<string, number>();
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
250
Cunkebao/src/components/WebSocketExample.tsx
Normal file
250
Cunkebao/src/components/WebSocketExample.tsx
Normal file
@@ -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 (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Card title="WebSocket 连接状态">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Badge color={getStatusColor()}>
|
||||
<div style={{ width: "8px", height: "8px", borderRadius: "50%" }} />
|
||||
</Badge>
|
||||
<span>{getStatusText()}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: "16px",
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() =>
|
||||
connect({
|
||||
client: "kefu-client",
|
||||
autoReconnect: true,
|
||||
})
|
||||
}
|
||||
disabled={
|
||||
status === WebSocketStatus.CONNECTING ||
|
||||
status === WebSocketStatus.CONNECTED
|
||||
}
|
||||
>
|
||||
连接
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
color="danger"
|
||||
onClick={disconnect}
|
||||
disabled={status === WebSocketStatus.DISCONNECTED}
|
||||
>
|
||||
断开
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
color="warning"
|
||||
onClick={reconnect}
|
||||
disabled={status === WebSocketStatus.CONNECTED}
|
||||
>
|
||||
重连
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={`消息列表 ${unreadCount > 0 ? `(${unreadCount} 条未读)` : ""}`}
|
||||
extra={
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<Button size="small" onClick={markAsRead}>
|
||||
标记已读
|
||||
</Button>
|
||||
<Button size="small" onClick={clearMessages}>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
style={{ marginTop: "16px" }}
|
||||
>
|
||||
<List style={{ maxHeight: "300px", overflowY: "auto" }}>
|
||||
{messages.length === 0 ? (
|
||||
<List.Item>暂无消息</List.Item>
|
||||
) : (
|
||||
messages.map((message: WebSocketMessage) => (
|
||||
<List.Item key={message.id}>
|
||||
<div style={{ fontSize: "12px", color: "#666" }}>
|
||||
{formatTime(message.timestamp)} - {message.type}
|
||||
</div>
|
||||
<div style={{ marginTop: "4px" }}>
|
||||
{typeof message.content === "string"
|
||||
? message.content
|
||||
: JSON.stringify(message.content, null, 2)}
|
||||
</div>
|
||||
</List.Item>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
</Card>
|
||||
|
||||
<Card title="发送消息" style={{ marginTop: "16px" }}>
|
||||
<div style={{ display: "flex", gap: "8px", marginBottom: "12px" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={messageInput}
|
||||
onChange={e => 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()}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleSendMessage}
|
||||
disabled={status !== WebSocketStatus.CONNECTED}
|
||||
>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleSendCommand("CmdHeartbeat")}
|
||||
disabled={status !== WebSocketStatus.CONNECTED}
|
||||
>
|
||||
心跳
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleSendCommand("CmdGetStatus")}
|
||||
disabled={status !== WebSocketStatus.CONNECTED}
|
||||
>
|
||||
获取状态
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleSendCommand("CmdSignIn")}
|
||||
disabled={status !== WebSocketStatus.CONNECTED}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="使用说明" style={{ marginTop: "16px" }}>
|
||||
<div style={{ fontSize: "14px", lineHeight: "1.6", color: "#666" }}>
|
||||
<p>1. 点击"连接"按钮建立WebSocket连接</p>
|
||||
<p>2. 连接成功后可以发送消息和命令</p>
|
||||
<p>3. 收到的消息会显示在消息列表中</p>
|
||||
<p>4. 页面刷新后会自动重连(如果之前是连接状态)</p>
|
||||
<p>5. 支持自动重连和错误处理</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketExample;
|
||||
@@ -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(<App />);
|
||||
root.render(
|
||||
<StrictModeWrapper enableStrictMode={false}>
|
||||
<App />
|
||||
</StrictModeWrapper>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<typeof useUserStore.getState>;
|
||||
app: ReturnType<typeof useAppStore.getState>;
|
||||
settings: ReturnType<typeof useSettingsStore.getState>;
|
||||
websocket: ReturnType<typeof useWebSocketStore.getState>;
|
||||
}
|
||||
|
||||
// 便利的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();
|
||||
};
|
||||
};
|
||||
|
||||
51
Cunkebao/src/store/module/ckchat.data.ts
Normal file
51
Cunkebao/src/store/module/ckchat.data.ts
Normal file
@@ -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<CkAccount>) => void;
|
||||
updateTenant: (tenant: Partial<CkTenant>) => void;
|
||||
getAccountId: () => number | null;
|
||||
getTenantId: () => number | null;
|
||||
getAccountName: () => string | null;
|
||||
getTenantName: () => string | null;
|
||||
}
|
||||
89
Cunkebao/src/store/module/ckchat.ts
Normal file
89
Cunkebao/src/store/module/ckchat.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createPersistStore } from "@/store/createPersistStore";
|
||||
|
||||
import { CkChatState, CkUserInfo, CkAccount, CkTenant } from "./ckchat.data";
|
||||
|
||||
export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
set => ({
|
||||
userInfo: null,
|
||||
isLoggedIn: false,
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo: (userInfo: CkUserInfo) => {
|
||||
set({ userInfo, isLoggedIn: true });
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUserInfo: () => {
|
||||
set({ userInfo: null, isLoggedIn: false });
|
||||
},
|
||||
|
||||
// 更新账户信息
|
||||
updateAccount: (account: Partial<CkAccount>) => {
|
||||
set(state => ({
|
||||
userInfo: state.userInfo
|
||||
? {
|
||||
...state.userInfo,
|
||||
account: { ...state.userInfo.account, ...account },
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
},
|
||||
|
||||
// 更新租户信息
|
||||
updateTenant: (tenant: Partial<CkTenant>) => {
|
||||
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();
|
||||
376
Cunkebao/src/store/module/websocket.ts
Normal file
376
Cunkebao/src/store/module/websocket.ts
Normal file
@@ -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<WebSocketConfig>) => void;
|
||||
disconnect: () => void;
|
||||
sendMessage: (message: Omit<WebSocketMessage, "id" | "timestamp">) => 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<WebSocketState>(
|
||||
(set, get) => ({
|
||||
status: WebSocketStatus.DISCONNECTED,
|
||||
ws: null,
|
||||
config: null,
|
||||
messages: [],
|
||||
unreadCount: 0,
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: null,
|
||||
|
||||
// 连接WebSocket
|
||||
connect: (config: Partial<WebSocketConfig>) => {
|
||||
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<WebSocketMessage, "id" | "timestamp">) => {
|
||||
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);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user