From 3198959cb747cc711fcd981f6c3104109e2df813 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, 1 Sep 2025 17:28:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BE=A4=E6=8E=A8=E4=BB=BB=E5=8A=A1):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=8E=B7=E5=8F=96=E7=BE=A4=E6=8E=A8=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E8=AF=A6=E6=83=85=E7=9A=84API=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(客服聊天): 实现搜索功能并优化联系人筛选逻辑 refactor(WebSocket): 重构连接逻辑,增加状态检查和调试信息 fix(WebSocket): 修复页面刷新后重连逻辑,避免重复连接 --- .../workspace/group-push/form/index.api.ts | 4 + .../workspace/group-push/form/index.tsx | 3 +- .../SidebarMenu/WechatFriends/index.tsx | 3 +- .../pc/ckbox/components/SidebarMenu/index.tsx | 29 +++--- Cunkebao/src/pages/pc/ckbox/index.tsx | 13 ++- Cunkebao/src/pages/pc/ckbox/main.ts | 40 ++++++-- Cunkebao/src/store/module/ckchat/ckchat.ts | 99 ++++++++++++++++--- Cunkebao/src/store/module/websocket.ts | 83 ++++++++++++++-- 8 files changed, 219 insertions(+), 55 deletions(-) diff --git a/Cunkebao/src/pages/mobile/workspace/group-push/form/index.api.ts b/Cunkebao/src/pages/mobile/workspace/group-push/form/index.api.ts index 819926da..2313200f 100644 --- a/Cunkebao/src/pages/mobile/workspace/group-push/form/index.api.ts +++ b/Cunkebao/src/pages/mobile/workspace/group-push/form/index.api.ts @@ -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"); diff --git a/Cunkebao/src/pages/mobile/workspace/group-push/form/index.tsx b/Cunkebao/src/pages/mobile/workspace/group-push/form/index.tsx index 0a07fa08..f62d8ee6 100644 --- a/Cunkebao/src/pages/mobile/workspace/group-push/form/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/group-push/form/index.tsx @@ -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({ name: "", startTime: "06:00", // 允许推送的开始时间 + dailyPushCount: 0, // 每日已推送次数 endTime: "23:59", // 允许推送的结束时间 maxPerDay: 20, pushOrder: 2, // 2: 按最新 diff --git a/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/WechatFriends/index.tsx b/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/WechatFriends/index.tsx index 47acabf8..33573b04 100644 --- a/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/WechatFriends/index.tsx +++ b/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/WechatFriends/index.tsx @@ -22,6 +22,7 @@ const ContactListSimple: React.FC = ({ ); 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 = ({ }; fetchNewContractList(); - }, [getNewContractListFn, kfSelected, countLables]); + }, [getNewContractListFn, kfSelected, countLables, searchKeyword]); const [activeKey, setActiveKey] = useState([]); // 默认展开第一个分组 diff --git a/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/index.tsx b/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/index.tsx index 31f23035..b143d406 100644 --- a/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/index.tsx +++ b/Cunkebao/src/pages/pc/ckbox/components/SidebarMenu/index.tsx @@ -26,29 +26,21 @@ const SidebarMenu: React.FC = ({ 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 = ({ } - value={searchText} + value={searchKeyword} onChange={e => handleSearch(e.target.value)} + onClear={handleClearSearch} allowClear /> @@ -142,7 +135,7 @@ const SidebarMenu: React.FC = ({ case "chats": return ( @@ -150,7 +143,7 @@ const SidebarMenu: React.FC = ({ case "contracts": return ( diff --git a/Cunkebao/src/pages/pc/ckbox/index.tsx b/Cunkebao/src/pages/pc/ckbox/index.tsx index a40e1c74..34514aa1 100644 --- a/Cunkebao/src/pages/pc/ckbox/index.tsx +++ b/Cunkebao/src/pages/pc/ckbox/index.tsx @@ -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([]); const [currentChat, setCurrentChat] = useState( 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); diff --git a/Cunkebao/src/pages/pc/ckbox/main.ts b/Cunkebao/src/pages/pc/ckbox/main.ts index 8faac6e1..47cd85f4 100644 --- a/Cunkebao/src/pages/pc/ckbox/main.ts +++ b/Cunkebao/src/pages/pc/ckbox/main.ts @@ -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) => { diff --git a/Cunkebao/src/store/module/ckchat/ckchat.ts b/Cunkebao/src/store/module/ckchat/ckchat.ts index c2257798..623995d0 100644 --- a/Cunkebao/src/store/module/ckchat/ckchat.ts +++ b/Cunkebao/src/store/module/ckchat/ckchat.ts @@ -20,6 +20,7 @@ export const useCkChatStore = createPersistStore( countLables: [], //标签列表 newContractList: [], //联系人分组 kfSelected: 0, //选中的客服 + searchKeyword: "", //搜索关键词 //客服列表 asyncKfUserList: async data => { set({ kfUserList: data }); @@ -42,6 +43,14 @@ export const useCkChatStore = createPersistStore( 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( 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( 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( 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( 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( 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( 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( 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(); diff --git a/Cunkebao/src/store/module/websocket.ts b/Cunkebao/src/store/module/websocket.ts index 5c21f900..928584fd 100644 --- a/Cunkebao/src/store/module/websocket.ts +++ b/Cunkebao/src/store/module/websocket.ts @@ -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( connect: (config: Partial) => { 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( 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( 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( 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); } },