feat(群推任务): 添加获取群推任务详情的API接口

feat(客服聊天): 实现搜索功能并优化联系人筛选逻辑

refactor(WebSocket): 重构连接逻辑,增加状态检查和调试信息

fix(WebSocket): 修复页面刷新后重连逻辑,避免重复连接
This commit is contained in:
超级老白兔
2025-09-01 17:28:33 +08:00
parent 832be374b5
commit 3198959cb7
8 changed files with 219 additions and 55 deletions

View File

@@ -3,6 +3,10 @@ import request from "@/api/request";
export function createGroupPushTask(data) {
return request("/v1/workbench/create", { ...data, type: 3 }, "POST");
}
// 获取自动点赞任务详情
export function fetchGroupPushTaskDetail(id: string) {
return request("/v1/workbench/detail", { id }, "GET");
}
export function updateGroupPushTask(data) {
return request("/v1/workbench/update", { ...data, type: 3 }, "POST");

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button } from "antd";
import { Toast } from "antd-mobile";
import { createGroupPushTask } from "./index.api";
import { createGroupPushTask, fetchGroupPushTaskDetail } from "./index.api";
import Layout from "@/components/Layout/Layout";
import StepIndicator from "@/components/StepIndicator";
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
@@ -35,6 +35,7 @@ const NewGroupPush: React.FC = () => {
const [formData, setFormData] = useState<FormData>({
name: "",
startTime: "06:00", // 允许推送的开始时间
dailyPushCount: 0, // 每日已推送次数
endTime: "23:59", // 允许推送的结束时间
maxPerDay: 20,
pushOrder: 2, // 2: 按最新

View File

@@ -22,6 +22,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
);
const kfSelected = useCkChatStore(state => state.kfSelected);
const countLables = useCkChatStore(state => state.countLables);
const searchKeyword = useCkChatStore(state => state.searchKeyword);
// 使用useEffect来处理异步的getNewContractList调用
useEffect(() => {
@@ -36,7 +37,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
};
fetchNewContractList();
}, [getNewContractListFn, kfSelected, countLables]);
}, [getNewContractListFn, kfSelected, countLables, searchKeyword]);
const [activeKey, setActiveKey] = useState<string[]>([]); // 默认展开第一个分组

View File

@@ -26,29 +26,21 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
loading = false,
}) => {
const chatSessions = useCkChatStore(state => state.getChatSessions());
const searchKeyword = useCkChatStore(state => state.searchKeyword);
const setSearchKeyword = useCkChatStore(state => state.setSearchKeyword);
const clearSearchKeyword = useCkChatStore(state => state.clearSearchKeyword);
const [searchText, setSearchText] = useState("");
const [activeTab, setActiveTab] = useState("chats");
const handleSearch = (value: string) => {
setSearchText(value);
setSearchKeyword(value);
};
const getFilteredContacts = () => {
if (!searchText) return contracts;
return contracts.filter(
contract =>
contract.nickname.toLowerCase().includes(searchText.toLowerCase()) ||
contract.phone.includes(searchText),
);
const handleClearSearch = () => {
clearSearchKeyword();
};
const getFilteredSessions = () => {
if (!searchText) return chatSessions;
return chatSessions.filter(session =>
session.nickname.toLowerCase().includes(searchText.toLowerCase()),
);
};
// 过滤逻辑已移至store中这里直接使用store返回的已过滤数据
// 渲染骨架屏
const renderSkeleton = () => (
@@ -103,8 +95,9 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
<Input
placeholder="搜索联系人、群组"
prefix={<SearchOutlined />}
value={searchText}
value={searchKeyword}
onChange={e => handleSearch(e.target.value)}
onClear={handleClearSearch}
allowClear
/>
</div>
@@ -142,7 +135,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
case "chats":
return (
<MessageList
chatSessions={getFilteredSessions()}
chatSessions={chatSessions}
onContactClick={onContactClick}
currentChat={currentChat}
/>
@@ -150,7 +143,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
case "contracts":
return (
<WechatFriends
contracts={getFilteredContacts() as ContractData[]}
contracts={contracts as ContractData[]}
onContactClick={onContactClick}
selectedContactId={currentChat}
/>

View File

@@ -9,23 +9,27 @@ import PageSkeleton from "./components/Skeleton";
import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat/ckchat";
const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main";
import { chatInitAPIdata, initSocket } from "./main";
import { clearUnreadCount } from "@/pages/pc/ckbox/api";
import {
KfUserListData,
weChatGroup,
ContractData,
} from "@/pages/pc/ckbox/data";
import { useWebSocketStore } from "@/store/module/websocket";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
const CkboxPage: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const [contracts, setContacts] = useState<any[]>([]);
const [currentChat, setCurrentChat] = useState<ContractData | weChatGroup>(
null,
);
const status = useWebSocketStore(state => state.status);
const messages = useWebSocketStore(state => state.messages);
// 不要在组件初始化时获取sendCommand而是在需要时动态获取
const [loading, setLoading] = useState(false);
const [showProfile, setShowProfile] = useState(true);
const kfUserList = useCkChatStore(state => state.kfUserList);
useEffect(() => {
// 方法一:使用 Promise 链式调用处理异步函数
@@ -49,6 +53,9 @@ const CkboxPage: React.FC = () => {
});
setContacts(isChatList);
// 数据加载完成后初始化WebSocket连接
initSocket();
})
.catch(error => {
console.error("获取联系人列表失败:", error);

View File

@@ -4,6 +4,7 @@ import {
asyncChatSessions,
asyncWeChatGroup,
asyncCountLables,
useCkChatStore,
} from "@/store/module/ckchat/ckchat";
import { useWebSocketStore } from "@/store/module/websocket";
@@ -13,7 +14,7 @@ import {
getContactList,
getGroupList,
} from "./api";
const { sendCommand } = useWebSocketStore.getState();
import { useUserStore } from "@/store/module/user";
import { KfUserListData } from "@/pages/pc/ckbox/data";
@@ -92,6 +93,35 @@ export const chatInitAPIdata = async () => {
return [];
}
};
//发起soket连接
export const initSocket = () => {
// 检查WebSocket是否已经连接
const { status } = useWebSocketStore.getState();
// 如果已经连接或正在连接,则不重复连接
if (status === "connected" || status === "connecting") {
console.log("WebSocket已连接或正在连接跳过重复连接", { status });
return;
}
// 从store获取token和accountId
const { token2 } = useUserStore.getState();
const { getAccountId } = useCkChatStore.getState();
const Token = token2;
const accountId = getAccountId();
// 使用WebSocket store初始化连接
const { connect } = useWebSocketStore.getState();
console.log("发起链接", Token, accountId);
// 连接WebSocket
connect({
accessToken: Token,
accountId: Number(accountId),
client: "kefu-client",
cmdType: "CmdSignIn",
seq: +new Date(),
});
};
export const getCountLables = async () => {
const LablesRes = await Promise.all(
@@ -240,14 +270,6 @@ export const getAllGroupList = async () => {
}
};
export const getChatInfo = () => {
//获取UserId
sendCommand("CmdRequestWechatAccountsAliveStatus", {
wechatAccountIds: ["300745", "4880930", "32686452"],
seq: +new Date(),
});
console.log("发送链接信息");
};
//获取token
const getToken = () => {
return new Promise((resolve, reject) => {

View File

@@ -20,6 +20,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
countLables: [], //标签列表
newContractList: [], //联系人分组
kfSelected: 0, //选中的客服
searchKeyword: "", //搜索关键词
//客服列表
asyncKfUserList: async data => {
set({ kfUserList: data });
@@ -42,6 +43,14 @@ export const useCkChatStore = createPersistStore<CkChatState>(
await state.getNewContractList();
}
},
// 设置搜索关键词
setSearchKeyword: (keyword: string) => {
set({ searchKeyword: keyword });
},
// 清除搜索关键词
clearSearchKeyword: () => {
set({ searchKeyword: "" });
},
asyncKfSelected: async (data: number) => {
set({ kfSelected: data });
// 清除getChatSessions、getContractList和getNewContractList缓存
@@ -74,6 +83,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
let cachedResult: any = null;
let lastKfSelected: number | null = null;
let lastCountLablesLength: number = 0;
let lastSearchKeyword: string = "";
return async () => {
const state = useCkChatStore.getState();
@@ -82,16 +92,38 @@ export const useCkChatStore = createPersistStore<CkChatState>(
const shouldRecalculate =
cachedResult === null ||
lastKfSelected !== state.kfSelected ||
lastCountLablesLength !== (state.countLables?.length || 0);
lastCountLablesLength !== (state.countLables?.length || 0) ||
lastSearchKeyword !== state.searchKeyword;
if (shouldRecalculate) {
// 使用createContractList构建联系人分组数据
cachedResult = await createContractList(
let contractList = await createContractList(
state.kfSelected,
state.countLables,
);
// 根据搜索关键词筛选联系人分组
if (state.searchKeyword.trim()) {
const keyword = state.searchKeyword.toLowerCase();
contractList = contractList
.map(group => ({
...group,
contracts:
group.contracts?.filter(item => {
const nickname = (item.nickname || "").toLowerCase();
const conRemark = (item.conRemark || "").toLowerCase();
return (
nickname.includes(keyword) || conRemark.includes(keyword)
);
}) || [],
}))
.filter(group => group.contracts.length > 0);
}
cachedResult = contractList;
lastKfSelected = state.kfSelected;
lastCountLablesLength = state.countLables?.length || 0;
lastSearchKeyword = state.searchKeyword;
}
return cachedResult;
@@ -142,6 +174,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
let cachedResult: any = null;
let lastKfSelected: number | null = null;
let lastContractListLength: number = 0;
let lastSearchKeyword: string = "";
return () => {
const state = useCkChatStore.getState();
@@ -150,17 +183,33 @@ export const useCkChatStore = createPersistStore<CkChatState>(
const shouldRecalculate =
cachedResult === null ||
lastKfSelected !== state.kfSelected ||
lastContractListLength !== state.contractList.length;
lastContractListLength !== state.contractList.length ||
lastSearchKeyword !== state.searchKeyword;
if (shouldRecalculate) {
const filteredContracts = state.contractList.filter(
item => item.wechatAccountId === state.kfSelected,
);
let filteredContracts = state.contractList;
cachedResult =
state.kfSelected !== 0 ? filteredContracts : state.contractList;
// 根据客服筛选
if (state.kfSelected !== 0) {
filteredContracts = filteredContracts.filter(
item => item.wechatAccountId === state.kfSelected,
);
}
// 根据搜索关键词筛选
if (state.searchKeyword.trim()) {
const keyword = state.searchKeyword.toLowerCase();
filteredContracts = filteredContracts.filter(item => {
const nickname = (item.nickname || "").toLowerCase();
const conRemark = (item.conRemark || "").toLowerCase();
return nickname.includes(keyword) || conRemark.includes(keyword);
});
}
cachedResult = filteredContracts;
lastKfSelected = state.kfSelected;
lastContractListLength = state.contractList.length;
lastSearchKeyword = state.searchKeyword;
}
return cachedResult;
@@ -203,6 +252,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
let cachedResult: any = null;
let lastKfSelected: number | null = null;
let lastChatSessionsLength: number = 0;
let lastSearchKeyword: string = "";
return () => {
const state = useCkChatStore.getState();
@@ -211,17 +261,33 @@ export const useCkChatStore = createPersistStore<CkChatState>(
const shouldRecalculate =
cachedResult === null ||
lastKfSelected !== state.kfSelected ||
lastChatSessionsLength !== state.chatSessions.length;
lastChatSessionsLength !== state.chatSessions.length ||
lastSearchKeyword !== state.searchKeyword;
if (shouldRecalculate) {
const filteredSessions = state.chatSessions.filter(
item => item.wechatAccountId === state.kfSelected,
);
let filteredSessions = state.chatSessions;
cachedResult =
state.kfSelected !== 0 ? filteredSessions : state.chatSessions;
// 根据客服筛选
if (state.kfSelected !== 0) {
filteredSessions = filteredSessions.filter(
item => item.wechatAccountId === state.kfSelected,
);
}
// 根据搜索关键词筛选
if (state.searchKeyword.trim()) {
const keyword = state.searchKeyword.toLowerCase();
filteredSessions = filteredSessions.filter(item => {
const nickname = (item.nickname || "").toLowerCase();
const conRemark = (item.conRemark || "").toLowerCase();
return nickname.includes(keyword) || conRemark.includes(keyword);
});
}
cachedResult = filteredSessions;
lastKfSelected = state.kfSelected;
lastChatSessionsLength = state.chatSessions.length;
lastSearchKeyword = state.searchKeyword;
}
return cachedResult;
@@ -348,6 +414,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
partialize: state => ({
userInfo: state.userInfo,
isLoggedIn: state.isLoggedIn,
kfUserList: state.kfUserList,
}),
onRehydrateStorage: () => state => {
// console.log("CkChat store hydrated:", state);
@@ -399,4 +466,8 @@ export const asyncCountLables = (data: ContactGroupByLabel[]) =>
export const asyncNewContractList = (data: any[]) =>
useCkChatStore.getState().asyncNewContractList(data);
export const getCountLables = () => useCkChatStore.getState().countLables;
export const setSearchKeyword = (keyword: string) =>
useCkChatStore.getState().setSearchKeyword(keyword);
export const clearSearchKeyword = () =>
useCkChatStore.getState().clearSearchKeyword();
useCkChatStore.getState().getKfSelectedUser();

View File

@@ -72,7 +72,7 @@ interface WebSocketState {
// 默认配置
const DEFAULT_CONFIG: WebSocketConfig = {
url: (import.meta as any).env?.VITE_API_WS_URL,
url: "wss://kf.quwanzhi.com:9993",
client: "kefu-client",
accountId: 0,
accessToken: "",
@@ -97,8 +97,21 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
connect: (config: Partial<WebSocketConfig>) => {
const currentState = get();
// 如果已经连接,先断开
// 检查当前连接状态,避免重复连接
if (
currentState.status === WebSocketStatus.CONNECTED ||
currentState.status === WebSocketStatus.CONNECTING
) {
console.log("WebSocket已连接或正在连接跳过重复连接", {
currentStatus: currentState.status,
hasWebSocket: !!currentState.ws,
});
return;
}
// 如果已经有WebSocket实例先断开
if (currentState.ws) {
console.log("断开现有WebSocket连接");
currentState.disconnect();
}
@@ -124,8 +137,18 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
t: Date.now().toString(),
});
// 检查URL是否为localhost如果是则不连接
// 调试信息:输出配置和环境变量
console.log("WebSocket配置信息:", {
configUrl: fullConfig.url,
envUrl: (import.meta as any).env?.VITE_API_WS_URL,
fullConfig,
params: params.toString(),
});
const wsUrl = fullConfig.url + "?" + params;
console.log("最终WebSocket URL:", wsUrl);
// 检查URL是否为localhost如果是则不连接
if (wsUrl.includes("localhost") || wsUrl.includes("127.0.0.1")) {
console.error("WebSocket连接被拦截不允许连接到本地地址", wsUrl);
Toast.show({
@@ -273,11 +296,6 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
client: currentState.config?.client || "kefu-client",
seq: +new Date(),
});
//获取UserId
currentState.sendCommand("CmdRequestWechatAccountsAliveStatus", {
wechatAccountIds: ["300745", "4880930", "32686452"],
seq: +new Date(),
});
}
Toast.show({ content: "WebSocket连接成功", position: "top" });
@@ -416,9 +434,56 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
onRehydrateStorage: () => state => {
// 页面刷新后,如果之前是连接状态,尝试重新连接
if (state && state.status === WebSocketStatus.CONNECTED && state.config) {
console.log("页面刷新后恢复WebSocket连接", {
persistedConfig: state.config,
currentDefaultConfig: DEFAULT_CONFIG,
});
// 使用最新的默认配置,而不是持久化的配置
const freshConfig = {
...DEFAULT_CONFIG,
client: state.config.client,
accountId: state.config.accountId,
accessToken: state.config.accessToken,
autoReconnect: state.config.autoReconnect,
};
console.log("使用刷新后的配置重连:", freshConfig);
// 延迟一下再重连,确保页面完全加载
// 同时检查当前状态,避免重复连接
setTimeout(() => {
state.connect(state.config);
// 重新获取最新的状态而不是使用闭包中的state
const currentState = useWebSocketStore.getState();
console.log("页面刷新后检查状态", {
status: currentState.status,
hasWs: !!currentState.ws,
});
// 强制重置状态为disconnected因为页面刷新后WebSocket实例已失效
if (
currentState.status === WebSocketStatus.CONNECTED &&
!currentState.ws
) {
console.log("检测到状态不一致重置为disconnected");
useWebSocketStore.setState({
status: WebSocketStatus.DISCONNECTED,
});
}
// 重新获取状态进行连接
const latestState = useWebSocketStore.getState();
if (
latestState.status === WebSocketStatus.DISCONNECTED ||
latestState.status === WebSocketStatus.ERROR
) {
console.log("页面刷新后开始重连");
latestState.connect(freshConfig);
} else {
console.log("WebSocket已连接或正在连接跳过页面刷新重连", {
status: latestState.status,
});
}
}, 1000);
}
},