新增 VITE_API_WS_URL 環境變數,更新主程式以引入嚴格模式包裝器,並在登錄頁面中整合觸客寶用戶信息獲取功能,調整請求模組以動態獲取 token2。
This commit is contained in:
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