From eb5dbe506602cdf2fc0a6fc675a75c6fd99391aa 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: Wed, 12 Nov 2025 16:17:29 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95API=E4=BB=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=96=B0=E7=AB=AF=E7=82=B9=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=A4=84=E7=90=86=E3=80=82=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?PushHistory=E7=BB=84=E4=BB=B6=E4=B8=AD=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E5=88=86=E9=A1=B5=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BB=A5=E6=94=B9=E5=96=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pc/ckbox/powerCenter/push-history/api.ts | 46 ++++-------------- .../ckbox/powerCenter/push-history/index.tsx | 47 ++++++++++--------- .../components/MessageEnter/index.tsx | 16 ------- .../components/QuickWords/index.tsx | 31 ++++++++++++ 4 files changed, 66 insertions(+), 74 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts index dceeed011..a64025380 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts @@ -25,41 +25,13 @@ export interface GetPushHistoryResponse { /** * 获取推送历史列表 */ -export const getPushHistory = async ( - params: GetPushHistoryParams -): Promise => { - try { - // TODO: 替换为实际的API接口地址 - const response = await request.get("/api/push-history", { params }); - - // 如果接口返回的数据格式不同,需要在这里进行转换 - if (response.data && response.data.success !== undefined) { - return response.data; - } - - // 兼容不同的响应格式 - return { - success: true, - data: { - list: response.data?.list || response.data?.data || [], - total: response.data?.total || 0, - page: response.data?.page || params.page || 1, - pageSize: response.data?.pageSize || params.pageSize || 10, - }, - }; - } catch (error: any) { - console.error("获取推送历史失败:", error); - return { - success: false, - message: error?.message || "获取推送历史失败", - }; - } +export interface GetGroupPushHistoryParams { + keyword?: string; + limit: string; + page: string; + workbenchId?: string; + [property: string]: any; +} +export const getPushHistory = async (params: GetGroupPushHistoryParams) => { + return request("/v1/workbench/group-push-history", { params }); }; - - - - - - - - diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx index 0697b99d3..bd2545a70 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx @@ -76,18 +76,31 @@ const PushHistory: React.FC = () => { } const response = await getPushHistory(params); + const result = response?.data ?? response ?? {}; - if (response.success) { - setDataSource(response.data?.list || []); - setPagination(prev => ({ - ...prev, - current: response.data?.page || page, - total: response.data?.total || 0, - })); - } else { - message.error(response.message || "获取推送历史失败"); + if (!result || typeof result !== "object") { + message.error("获取推送历史失败"); setDataSource([]); + return; } + + const toNumber = (value: unknown, fallback: number) => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; + }; + + const list = Array.isArray(result.list) ? result.list : []; + const total = toNumber(result.total, pagination.total); + const currentPage = toNumber(result.page, page); + const pageSize = toNumber(result.pageSize, pagination.pageSize); + + setDataSource(list); + setPagination(prev => ({ + ...prev, + current: currentPage, + pageSize, + total, + })); } catch (error) { console.error("获取推送历史失败:", error); message.error("获取推送历史失败,请稍后重试"); @@ -211,9 +224,7 @@ const PushHistory: React.FC = () => { dataIndex: "pushContent", key: "pushContent", ellipsis: true, - render: (text: string) => ( - {text} - ), + render: (text: string) => {text}, }, { title: "目标数量", @@ -287,7 +298,9 @@ const PushHistory: React.FC = () => { subtitle="查看所有推送任务的历史记录" showBackButton={true} backButtonText="返回" - onBackClick={() => navigate("/pc/powerCenter/message-push-assistant")} + onBackClick={() => + navigate("/pc/powerCenter/message-push-assistant") + } /> } @@ -369,11 +382,3 @@ const PushHistory: React.FC = () => { }; export default PushHistory; - - - - - - - - diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx index 8ae096e81..864ccb6e9 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx @@ -167,27 +167,11 @@ const MessageEnter: React.FC = ({ contract }) => { // AI 消息处理 useEffect(() => { if (quoteMessageContent) { - console.log( - "🤖 AI消息到达 - aiQuoteMessageContent:", - aiQuoteMessageContent, - ); - - // 检查:如果用户输入框已有内容(且不是之前的AI内容),不覆盖 - if (inputValue && inputValue !== quoteMessageContent) { - console.log("⚠️ 用户正在输入,不覆盖输入内容"); - updateQuoteMessageContent(""); // 清空AI回复 - return; - } - if (isAiAssist) { - // AI辅助模式:填充到输入框,等待人工确认 - console.log("✨ AI辅助模式:填充消息到输入框"); setInputValue(quoteMessageContent); } if (isAiTakeover) { - // AI接管模式:直接发送消息(传入内容,避免 state 闭包问题) - console.log("🚀 AI接管模式:自动发送消息"); handleSend(quoteMessageContent); } } diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx index 561200e53..8b1315a9d 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx @@ -39,6 +39,7 @@ import QuickReplyModal from "./components/QuickReplyModal"; import GroupModal from "./components/GroupModal"; import { useWeChatStore } from "@/store/module/weChat/weChat"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; +import { ChatRecord } from "@/pages/pc/ckbox/data"; // 消息类型枚举 export enum MessageType { @@ -82,10 +83,12 @@ const QuickWords: React.FC = ({ onInsert }) => { state => state.updateQuoteMessageContent, ); const currentContract = useWeChatStore(state => state.currentContract); + const addMessage = useWeChatStore(state => state.addMessage); const { sendCommand } = useWebSocketStore.getState(); const sendQuickReplyNow = (reply: QuickWordsReply) => { if (!currentContract) return; + const messageId = Date.now(); const params = { wechatAccountId: currentContract.wechatAccountId, wechatChatroomId: currentContract?.chatroomId ? currentContract.id : 0, @@ -93,7 +96,35 @@ const QuickWords: React.FC = ({ onInsert }) => { msgSubType: 0, msgType: reply.msgType, content: reply.content, + seq: messageId, } as any; + + if (reply.msgType !== MessageType.TEXT) { + const localMessage: ChatRecord = { + id: messageId, + wechatAccountId: params.wechatAccountId, + wechatFriendId: params.wechatFriendId, + wechatChatroomId: params.wechatChatroomId, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: params.content, + msgType: reply.msgType, + msgSubType: params.msgSubType, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + addMessage(localMessage); + } sendCommand("CmdSendMessage", params); }; From ae4a165b0740fbb3e6f1decb60d81cf97451cf1c 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: Thu, 13 Nov 2025 11:58:12 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE=E5=BA=93=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E6=B5=81=E7=A8=8B=E3=80=82=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=B1=BB=E4=BB=A5=E6=94=AF=E6=8C=81=E5=8A=A8=E6=80=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=90=8D=E7=A7=B0=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E3=80=82=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=90=AF=E5=8A=A8=E9=80=BB=E8=BE=91=E4=BB=A5?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=9C=A8=E7=94=A8=E6=88=B7=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=97=B6=E6=AD=A3=E7=A1=AE=E5=88=9D=E5=A7=8B=E5=8C=96=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E3=80=82=E5=A2=9E=E5=BC=BA=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E6=81=A2=E5=A4=8D=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9A=84=E5=8F=AF=E9=9D=A0=E6=80=A7=E5=92=8C=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/src/main.tsx | 90 +--------- .../pc/ckbox/powerCenter/push-history/api.ts | 2 +- .../components/ProfileCard/index.tsx | 65 +++---- Touchkebao/src/store/module/user.ts | 87 +++++++++- .../src/store/module/websocket/websocket.ts | 78 +++++++-- Touchkebao/src/store/persistUtils.ts | 6 + Touchkebao/src/utils/db.ts | 164 +++++++++++++++++- 7 files changed, 342 insertions(+), 150 deletions(-) diff --git a/Touchkebao/src/main.tsx b/Touchkebao/src/main.tsx index a3392df28..dde8d3ad6 100644 --- a/Touchkebao/src/main.tsx +++ b/Touchkebao/src/main.tsx @@ -7,97 +7,18 @@ import dayjs from "dayjs"; import "dayjs/locale/zh-cn"; import App from "./App"; import "./styles/global.scss"; -import { db } from "./utils/db"; // 引入数据库实例 +import { initializeDatabaseFromPersistedUser } from "./utils/db"; // 设置dayjs为中文 dayjs.locale("zh-cn"); -// 清理旧数据库 -async function cleanupOldDatabase() { +async function bootstrap() { try { - // 获取所有数据库 - const databases = await indexedDB.databases(); - - for (const dbInfo of databases) { - if (dbInfo.name === "CunkebaoDatabase") { - console.log("检测到旧版数据库,开始清理..."); - - // 打开数据库检查版本 - const openRequest = indexedDB.open(dbInfo.name); - - await new Promise((resolve, reject) => { - openRequest.onsuccess = async event => { - const database = (event.target as IDBOpenDBRequest).result; - const objectStoreNames = Array.from(database.objectStoreNames); - - // 检查是否存在旧表 - const hasOldTables = objectStoreNames.some(name => - [ - "kfUsers", - "weChatGroup", - "contracts", - "newContactList", - "messageList", - ].includes(name), - ); - - if (hasOldTables) { - console.log("发现旧表,删除整个数据库:", objectStoreNames); - database.close(); - - // 删除整个数据库 - const deleteRequest = indexedDB.deleteDatabase(dbInfo.name); - deleteRequest.onsuccess = () => { - console.log("旧数据库已删除"); - resolve(); - }; - deleteRequest.onerror = () => { - console.error("删除旧数据库失败"); - reject(); - }; - } else { - console.log("数据库结构正确,无需清理"); - database.close(); - resolve(); - } - }; - - openRequest.onerror = () => { - console.error("无法打开数据库进行检查"); - reject(); - }; - }); - } - } + await initializeDatabaseFromPersistedUser(); } catch (error) { - console.warn("清理旧数据库时出错(可忽略):", error); - } -} - -// 数据库初始化 -async function initializeApp() { - try { - // 1. 清理旧数据库 - await cleanupOldDatabase(); - - // 2. 打开新数据库 - await db.open(); - console.log("数据库初始化成功"); - - // 3. 开发环境清空数据(可选) - if (process.env.NODE_ENV === "development") { - console.log("开发环境:跳过数据清理"); - // 如需清空数据,取消下面的注释 - // await db.chatSessions.clear(); - // await db.contactsUnified.clear(); - // await db.contactLabelMap.clear(); - // await db.userLoginRecords.clear(); - } - } catch (error) { - console.error("数据库初始化失败:", error); + console.warn("Failed to prepare database before app bootstrap:", error); } - // 渲染应用 const root = createRoot(document.getElementById("root")!); root.render( @@ -106,5 +27,4 @@ async function initializeApp() { ); } -// 启动应用 -initializeApp(); +void bootstrap(); diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts index a64025380..a36270332 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts @@ -33,5 +33,5 @@ export interface GetGroupPushHistoryParams { [property: string]: any; } export const getPushHistory = async (params: GetGroupPushHistoryParams) => { - return request("/v1/workbench/group-push-history", { params }); + return request("/v1/workbench/group-push-history", params, "GET"); }; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx index d738db4b1..831a60a2b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Layout, Tabs } from "antd"; import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import styles from "./Person.module.scss"; @@ -9,6 +9,8 @@ import LayoutFiexd from "@/components/Layout/LayoutFiexd"; const { Sider } = Layout; +const noop = () => {}; + interface PersonProps { contract: ContractData | weChatGroup; } @@ -16,47 +18,46 @@ interface PersonProps { const Person: React.FC = ({ contract }) => { const [activeKey, setActiveKey] = useState("profile"); const isGroup = "chatroomId" in contract; + const tabItems = useMemo(() => { + const baseItems = [ + { + key: "quickwords", + label: "快捷语录", + }, + { + key: "profile", + label: isGroup ? "群资料" : "个人资料", + }, + ]; + if (!isGroup) { + baseItems.push({ + key: "moments", + label: "朋友圈", + }); + } + return baseItems; + }, [isGroup]); + + const handleTabChange = useCallback((key: string) => { + setActiveKey(key); + }, []); + + const tabBarStyle = useMemo(() => ({ padding: "0 30px" }), []); + return ( setActiveKey(key)} - tabBarStyle={{ - padding: "0 30px", - }} - items={[ - { - key: "quickwords", - label: "快捷语录", - }, - { - key: "profile", - label: isGroup ? "群资料" : "个人资料", - }, - - ...(!isGroup - ? [ - { - key: "moments", - label: "朋友圈", - }, - ] - : []), - ]} + onChange={handleTabChange} + tabBarStyle={tabBarStyle} + items={tabItems} /> } > {activeKey === "profile" && } - {activeKey === "quickwords" && ( - {}} - onAdd={() => {}} - onRemove={() => {}} - /> - )} + {activeKey === "quickwords" && } {activeKey === "moments" && !isGroup && ( )} diff --git a/Touchkebao/src/store/module/user.ts b/Touchkebao/src/store/module/user.ts index 745af11c5..7d1dea2fe 100644 --- a/Touchkebao/src/store/module/user.ts +++ b/Touchkebao/src/store/module/user.ts @@ -1,5 +1,52 @@ import { createPersistStore } from "@/store/createPersistStore"; import { Toast } from "antd-mobile"; +import { databaseManager } from "@/utils/db"; + +const STORE_CACHE_KEYS = [ + "user-store", + "app-store", + "settings-store", + "websocket-store", + "ckchat-store", + "wechat-storage", + "contacts-storage", + "message-storage", + "customer-storage", +]; + +const allStorages = (): Storage[] => { + if (typeof window === "undefined") { + return []; + } + const storages: Storage[] = []; + try { + storages.push(window.localStorage); + } catch (error) { + console.warn("无法访问 localStorage:", error); + } + try { + storages.push(window.sessionStorage); + } catch (error) { + console.warn("无法访问 sessionStorage:", error); + } + return storages; +}; + +const clearStoreCaches = () => { + const storages = allStorages(); + if (!storages.length) { + return; + } + STORE_CACHE_KEYS.forEach(key => { + storages.forEach(storage => { + try { + storage.removeItem(key); + } catch (error) { + console.warn(`清理持久化数据失败: ${key}`, error); + } + }); + }); +}; export interface User { id: number; @@ -28,7 +75,7 @@ interface UserState { setToken: (token: string) => void; setToken2: (token2: string) => void; clearUser: () => void; - login: (token: string, userInfo: User) => void; + login: (token: string, userInfo: User) => Promise; login2: (token2: string) => void; logout: () => void; } @@ -39,12 +86,27 @@ export const useUserStore = createPersistStore( token: null, token2: null, isLoggedIn: false, - setUser: user => set({ user, isLoggedIn: true }), + setUser: user => { + set({ user, isLoggedIn: true }); + databaseManager.ensureDatabase(user.id).catch(error => { + console.warn("Failed to initialize database for user:", error); + }); + }, setToken: token => set({ token }), setToken2: token2 => set({ token2 }), - clearUser: () => - set({ user: null, token: null, token2: null, isLoggedIn: false }), - login: (token, userInfo) => { + clearUser: () => { + databaseManager.closeCurrentDatabase().catch(error => { + console.warn("Failed to close database on clearUser:", error); + }); + clearStoreCaches(); + set({ user: null, token: null, token2: null, isLoggedIn: false }); + }, + login: async (token, userInfo) => { + clearStoreCaches(); + + // 清除旧的双token缓存 + localStorage.removeItem("token2"); + // 只将token存储到localStorage localStorage.setItem("token", token); @@ -66,6 +128,11 @@ export const useUserStore = createPersistStore( lastLoginIp: userInfo.lastLoginIp, lastLoginTime: userInfo.lastLoginTime, }; + try { + await databaseManager.ensureDatabase(user.id); + } catch (error) { + console.error("Failed to initialize user database:", error); + } set({ user, token, isLoggedIn: true }); Toast.show({ content: "登录成功", position: "top" }); @@ -80,6 +147,10 @@ export const useUserStore = createPersistStore( // 清除localStorage中的token localStorage.removeItem("token"); localStorage.removeItem("token2"); + databaseManager.closeCurrentDatabase().catch(error => { + console.warn("Failed to close user database on logout:", error); + }); + clearStoreCaches(); set({ user: null, token: null, token2: null, isLoggedIn: false }); }, }), @@ -92,7 +163,11 @@ export const useUserStore = createPersistStore( isLoggedIn: state.isLoggedIn, }), onRehydrateStorage: () => state => { - // console.log("User store hydrated:", state); + if (state?.user?.id) { + databaseManager.ensureDatabase(state.user!.id).catch(error => { + console.warn("Failed to restore user database:", error); + }); + } }, }, ); diff --git a/Touchkebao/src/store/module/websocket/websocket.ts b/Touchkebao/src/store/module/websocket/websocket.ts index 68a17c459..359fcf4aa 100644 --- a/Touchkebao/src/store/module/websocket/websocket.ts +++ b/Touchkebao/src/store/module/websocket/websocket.ts @@ -1,7 +1,7 @@ import { createPersistStore } from "@/store/createPersistStore"; -import { Toast } from "antd-mobile"; import { useUserStore } from "../user"; import { useCkChatStore } from "@/store/module/ckchat/ckchat"; +import { useCustomerStore } from "@/store/module/weChat/customer"; const { getAccountId } = useCkChatStore.getState(); import { msgManageCore } from "./msgManage"; // WebSocket消息类型 @@ -52,6 +52,7 @@ interface WebSocketState { reconnectAttempts: number; reconnectTimer: NodeJS.Timeout | null; aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器 + aliveStatusUnsubscribe: (() => void) | null; // 方法 connect: (config: Partial) => void; @@ -97,6 +98,7 @@ export const useWebSocketStore = createPersistStore( reconnectAttempts: 0, reconnectTimer: null, aliveStatusTimer: null, + aliveStatusUnsubscribe: null, // 连接WebSocket connect: (config: Partial) => { @@ -405,7 +407,7 @@ export const useWebSocketStore = createPersistStore( }, // 内部方法:处理连接关闭 - _handleClose: (event: CloseEvent) => { + _handleClose: () => { const currentState = get(); // console.log("WebSocket连接关闭:", event.code, event.reason); @@ -431,7 +433,7 @@ export const useWebSocketStore = createPersistStore( }, // 内部方法:处理连接错误 - _handleError: (event: Event) => { + _handleError: () => { // console.error("WebSocket连接错误:", event); set({ status: WebSocketStatus.ERROR }); @@ -477,42 +479,84 @@ export const useWebSocketStore = createPersistStore( // 先停止现有定时器 currentState._stopAliveStatusTimer(); - // 获取客服用户列表 - const { kfUserList } = useCkChatStore.getState(); + const requestAliveStatus = () => { + const state = get(); + if (state.status !== WebSocketStatus.CONNECTED) { + return; + } - // 如果没有客服用户,不启动定时器 - if (!kfUserList || kfUserList.length === 0) { - return; - } + const { customerList } = useCustomerStore.getState(); + const { kfUserList } = useCkChatStore.getState(); + const targets = + customerList && customerList.length > 0 + ? customerList + : kfUserList && kfUserList.length > 0 + ? kfUserList + : []; + + if (targets.length > 0) { + state.sendCommand("CmdRequestWechatAccountsAliveStatus", { + wechatAccountIds: targets.map(v => v.id), + }); + } + }; + + // 尝试立即请求一次,如果客服列表尚未加载,后续定时器会继续检查 + requestAliveStatus(); + + const unsubscribeCustomer = useCustomerStore.subscribe(state => { + if ( + get().status === WebSocketStatus.CONNECTED && + state.customerList && + state.customerList.length > 0 + ) { + requestAliveStatus(); + } + }); + + const unsubscribeKf = useCkChatStore.subscribe(state => { + if ( + get().status === WebSocketStatus.CONNECTED && + state.kfUserList && + state.kfUserList.length > 0 + ) { + requestAliveStatus(); + } + }); // 启动定时器,每5秒查询一次 const timer = setInterval(() => { const state = get(); // 检查连接状态 if (state.status === WebSocketStatus.CONNECTED) { - const { kfUserList: currentKfUserList } = useCkChatStore.getState(); - if (currentKfUserList && currentKfUserList.length > 0) { - state.sendCommand("CmdRequestWechatAccountsAliveStatus", { - wechatAccountIds: currentKfUserList.map(v => v.id), - }); - } + requestAliveStatus(); } else { // 如果连接断开,停止定时器 state._stopAliveStatusTimer(); } }, 5 * 1000); - set({ aliveStatusTimer: timer }); + set({ + aliveStatusTimer: timer, + aliveStatusUnsubscribe: () => { + unsubscribeCustomer(); + unsubscribeKf(); + }, + }); }, // 内部方法:停止客服状态查询定时器 _stopAliveStatusTimer: () => { const currentState = get(); + if (currentState.aliveStatusUnsubscribe) { + currentState.aliveStatusUnsubscribe(); + } + if (currentState.aliveStatusTimer) { clearInterval(currentState.aliveStatusTimer); - set({ aliveStatusTimer: null }); } + set({ aliveStatusTimer: null, aliveStatusUnsubscribe: null }); }, }), { diff --git a/Touchkebao/src/store/persistUtils.ts b/Touchkebao/src/store/persistUtils.ts index abfb1e8c5..bcbc93bb5 100644 --- a/Touchkebao/src/store/persistUtils.ts +++ b/Touchkebao/src/store/persistUtils.ts @@ -5,6 +5,12 @@ export const PERSIST_KEYS = { USER_STORE: "user-store", APP_STORE: "app-store", SETTINGS_STORE: "settings-store", + CKCHAT_STORE: "ckchat-store", + WEBSOCKET_STORE: "websocket-store", + WECHAT_STORAGE: "wechat-storage", + CONTACTS_STORAGE: "contacts-storage", + MESSAGE_STORAGE: "message-storage", + CUSTOMER_STORAGE: "customer-storage", } as const; // 存储类型 diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts index abd22d02a..61bffeb81 100644 --- a/Touchkebao/src/utils/db.ts +++ b/Touchkebao/src/utils/db.ts @@ -16,6 +16,8 @@ */ import Dexie, { Table } from "dexie"; +import { getPersistedData, PERSIST_KEYS } from "@/store/persistUtils"; +const DB_NAME_PREFIX = "CunkebaoDatabase"; // ==================== 用户登录记录 ==================== export interface UserLoginRecord { @@ -123,8 +125,8 @@ class CunkebaoDatabase extends Dexie { contactLabelMap!: Table; // 联系人标签映射表 userLoginRecords!: Table; // 用户登录记录表 - constructor() { - super("CunkebaoDatabase"); + constructor(dbName: string) { + super(dbName); // 版本1:统一表结构 this.version(1).stores({ @@ -188,12 +190,148 @@ class CunkebaoDatabase extends Dexie { } } -// 创建数据库实例 -export const db = new CunkebaoDatabase(); +class DatabaseManager { + private currentDb: CunkebaoDatabase | null = null; + private currentUserId: number | null = null; + + private getDatabaseName(userId: number) { + return `${DB_NAME_PREFIX}_${userId}`; + } + + private async openDatabase(dbName: string) { + const instance = new CunkebaoDatabase(dbName); + await instance.open(); + return instance; + } + + async ensureDatabase(userId: number) { + if (userId === undefined || userId === null) { + throw new Error("Invalid userId provided for database initialization"); + } + + if ( + this.currentDb && + this.currentUserId === userId && + this.currentDb.isOpen() + ) { + return this.currentDb; + } + + await this.closeCurrentDatabase(); + + const dbName = this.getDatabaseName(userId); + this.currentDb = await this.openDatabase(dbName); + this.currentUserId = userId; + + return this.currentDb; + } + + getCurrentDatabase(): CunkebaoDatabase { + if (!this.currentDb) { + throw new Error("Database has not been initialized for the current user"); + } + return this.currentDb; + } + + getCurrentUserId() { + return this.currentUserId; + } + + isInitialized(): boolean { + return !!this.currentDb && this.currentDb.isOpen(); + } + + async closeCurrentDatabase() { + if (this.currentDb) { + try { + this.currentDb.close(); + } catch (error) { + console.warn("Failed to close current database:", error); + } + this.currentDb = null; + } + this.currentUserId = null; + } +} + +export const databaseManager = new DatabaseManager(); + +let pendingDatabaseRestore: Promise | null = null; + +async function restoreDatabaseFromPersistedState() { + if (typeof window === "undefined") { + return null; + } + + const persistedData = getPersistedData>( + PERSIST_KEYS.USER_STORE, + "localStorage", + ); + + if (!persistedData) { + return null; + } + + let parsed: any = persistedData; + + if (typeof persistedData === "string") { + try { + parsed = JSON.parse(persistedData); + } catch (error) { + console.warn("Failed to parse persisted user-store value:", error); + return null; + } + } + + const state = parsed?.state ?? parsed; + const userId = state?.user?.id; + + if (!userId) { + return null; + } + + try { + return await databaseManager.ensureDatabase(userId); + } catch (error) { + console.warn("Failed to initialize database from persisted user:", error); + return null; + } +} + +export async function initializeDatabaseFromPersistedUser() { + if (databaseManager.isInitialized()) { + return databaseManager.getCurrentDatabase(); + } + + if (!pendingDatabaseRestore) { + pendingDatabaseRestore = restoreDatabaseFromPersistedState().finally(() => { + pendingDatabaseRestore = null; + }); + } + + return pendingDatabaseRestore; +} + +const dbProxy = new Proxy({} as CunkebaoDatabase, { + get(_target, prop: string | symbol) { + const currentDb = databaseManager.getCurrentDatabase(); + const value = (currentDb as any)[prop]; + if (typeof value === "function") { + return value.bind(currentDb); + } + return value; + }, +}); + +export const db = dbProxy; // 简单的数据库操作类 export class DatabaseService { - constructor(private table: Table) {} + constructor(private readonly tableAccessor: () => Table) {} + + private get table(): Table { + return this.tableAccessor(); + } // 基础 CRUD 操作 - 使用serverId作为主键 async create(data: Omit): Promise { @@ -446,10 +584,18 @@ export class DatabaseService { } // 创建统一表的服务实例 -export const chatSessionService = new DatabaseService(db.chatSessions); -export const contactUnifiedService = new DatabaseService(db.contactsUnified); -export const contactLabelMapService = new DatabaseService(db.contactLabelMap); -export const userLoginRecordService = new DatabaseService(db.userLoginRecords); +export const chatSessionService = new DatabaseService( + () => databaseManager.getCurrentDatabase().chatSessions, +); +export const contactUnifiedService = new DatabaseService( + () => databaseManager.getCurrentDatabase().contactsUnified, +); +export const contactLabelMapService = new DatabaseService( + () => databaseManager.getCurrentDatabase().contactLabelMap, +); +export const userLoginRecordService = new DatabaseService( + () => databaseManager.getCurrentDatabase().userLoginRecords, +); // 默认导出数据库实例 export default db; From a6ee45f3e326f0b37cca34f7a1cf61a1b186c677 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: Thu, 13 Nov 2025 16:07:52 +0800 Subject: [PATCH 03/15] =?UTF-8?q?=E9=87=8D=E6=9E=84ProfileCard=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E5=A2=9E=E5=BC=BA=E9=80=89=E9=A1=B9=E5=8D=A1?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=B9=B6=E6=94=B9=E5=96=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82=E5=BC=95=E5=85=A5=E5=9F=BA=E4=BA=8E?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E9=94=AE=E7=9A=84=E5=8A=A8=E6=80=81=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E5=8D=A1=E5=91=88=E7=8E=B0=EF=BC=8C=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=80=89=E9=A1=B9=E5=8D=A1=E6=A0=87=E9=A2=98=E7=9A=84?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E3=80=82=E8=B0=83=E6=95=B4=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E5=B8=A7=E5=92=8C=E6=B8=B2=E6=9F=93=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E5=B8=A7=E7=9A=84=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=80=82=E6=9B=B4=E6=96=B0QuickWords=E5=92=8CProfileModules?= =?UTF-8?q?=E9=9B=86=E6=88=90=E3=80=82=E4=BF=AE=E6=94=B9GroupModal?= =?UTF-8?q?=E5=92=8CQuickReplyModal=E4=BB=A5=E4=BD=BF=E7=94=A8=E2=80=9Cdes?= =?UTF-8?q?troyOnHidden=E2=80=9D=E4=BB=A5=E8=8E=B7=E5=BE=97=E6=9B=B4?= =?UTF-8?q?=E5=A5=BD=E7=9A=84=E6=A8=A1=E5=BC=8F=E5=A4=84=E7=90=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ProfileCard/Person.module.scss | 48 ++++++++++ .../components/ProfileModules/index.tsx | 19 ++-- .../QuickWords/components/GroupModal.tsx | 2 +- .../QuickWords/components/QuickReplyModal.tsx | 2 +- .../components/ProfileCard/index.tsx | 94 +++++++++++++++---- .../SidebarMenu/MessageList/index.tsx | 8 +- .../src/store/module/websocket/websocket.ts | 2 +- Touchkebao/src/utils/db.ts | 52 ++-------- Touchkebao/src/utils/dbAction/contact.ts | 4 +- Touchkebao/src/utils/dbAction/message.ts | 6 ++ 10 files changed, 157 insertions(+), 80 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss index 66cf4b59e..add242f87 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss @@ -4,3 +4,51 @@ height: 100%; overflow-y: auto; } + +.tabHeader { + display: flex; + align-items: center; + padding: 0 30px; + border-bottom: 1px solid #f0f0f0; + min-height: 48px; +} + +.tabItem { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 24px; + padding: 12px 0; + font-size: 14px; + color: #333; + cursor: pointer; + transition: color 0.2s ease; +} + +.tabItem:last-child { + margin-right: 0; +} + +.tabItem:hover { + color: #1677ff; +} + +.tabItemActive { + color: #1677ff; + font-weight: 500; +} + +.tabUnderline { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: transparent; + transition: background 0.2s ease; +} + +.tabItemActive .tabUnderline { + background: #1677ff; +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index 9e06f5939..847b3b417 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { Input, Button, @@ -22,7 +22,7 @@ import { SwapOutlined, } from "@ant-design/icons"; import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { useCkChatStore } from "@/store/module/ckchat/ckchat"; +import { useCustomerStore } from "@/store/module/weChat/customer"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; import { useWeChatStore } from "@/store/module/weChat/weChat"; import { useContactStore } from "@/store/module/weChat/contacts"; @@ -209,9 +209,14 @@ const Person: React.FC = ({ contract }) => { // 构建联系人或群聊详细信息 - const kfSelectedUser = useCkChatStore(state => - state.getKfUserInfo(contract.wechatAccountId || 0), - ); + const customerList = useCustomerStore(state => state.customerList); + const kfSelectedUser = useMemo(() => { + if (!contract.wechatAccountId) return null; + const matchedCustomer = customerList.find( + customer => customer.id === contract.wechatAccountId, + ); + return matchedCustomer || null; + }, [customerList, contract.wechatAccountId]); const { getContactsByCustomer } = useContactStore(); @@ -221,16 +226,12 @@ const Person: React.FC = ({ contract }) => { const hasGroupManagePermission = () => { // 暂时给所有用户完整的群管理权限 return true; - // if (!kfSelectedUser || !contract) return false; - // // 当客服的wechatId与contract的chatroomOwner相同时,才有完整的群管理权限 - // return kfSelectedUser.nickname === (contract as any).chatroomOwnerNickname; }; // 获取所有可用标签 useEffect(() => { const fetchAvailableTags = async () => { try { - // 从kfSelectedUser.labels和contract.labels合并获取所有标签 const kfTags = kfSelectedUser?.labels || []; const contractTags = contract.labels || []; const allTags = [...new Set([...kfTags, ...contractTags])]; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx index 420d942bb..0754db881 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx @@ -28,7 +28,7 @@ const GroupModal: React.FC = ({ form.resetFields(); }} footer={null} - destroyOnClose + destroyOnHidden >
= ({ form.resetFields(); }} footer={null} - destroyOnClose + destroyOnHidden > = ({ contract }) => { - const [activeKey, setActiveKey] = useState("profile"); + const [activeKey, setActiveKey] = useState("quickwords"); const isGroup = "chatroomId" in contract; const tabItems = useMemo(() => { const baseItems = [ { key: "quickwords", label: "快捷语录", + children: , }, { key: "profile", label: isGroup ? "群资料" : "个人资料", + children: , }, ]; if (!isGroup) { baseItems.push({ key: "moments", label: "朋友圈", + children: , }); } return baseItems; - }, [isGroup]); + }, [contract, isGroup]); - const handleTabChange = useCallback((key: string) => { - setActiveKey(key); - }, []); + useEffect(() => { + setActiveKey("quickwords"); + setRenderedKeys(["quickwords"]); + }, [contract]); - const tabBarStyle = useMemo(() => ({ padding: "0 30px" }), []); + const tabHeaderItems = useMemo( + () => tabItems.map(({ key, label }) => ({ key, label })), + [tabItems], + ); + + const availableKeys = useMemo( + () => tabItems.map(item => item.key), + [tabItems], + ); + + const [renderedKeys, setRenderedKeys] = useState(() => [ + "quickwords", + ]); + + useEffect(() => { + if (!availableKeys.includes(activeKey) && availableKeys.length > 0) { + setActiveKey(availableKeys[0]); + } + }, [activeKey, availableKeys]); + + useEffect(() => { + setRenderedKeys(keys => { + const filtered = keys.filter(key => availableKeys.includes(key)); + if (!filtered.includes(activeKey)) { + filtered.push(activeKey); + } + const isSameLength = filtered.length === keys.length; + const isSameOrder = + isSameLength && filtered.every((key, index) => key === keys[index]); + return isSameOrder ? keys : filtered; + }); + }, [activeKey, availableKeys]); return ( +
+ {tabHeaderItems.map(({ key, label }) => { + const isActive = key === activeKey; + return ( +
{ + setActiveKey(key); + }} + > + {label} +
+
+ ); + })} +
} > - {activeKey === "profile" && } - {activeKey === "quickwords" && } - {activeKey === "moments" && !isGroup && ( - - )} + {renderedKeys.map(key => { + const item = tabItems.find(tab => tab.key === key); + if (!item) return null; + const isActive = key === activeKey; + return ( +
+ {item.children} +
+ ); + })} ); diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index b24c8343f..21712a08c 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -343,9 +343,6 @@ const MessageList: React.FC = () => { }; }); - console.log("群聊数据示例:", groups[0]); // 调试:查看第一个群聊数据 - console.log("好友数据示例:", friends[0]); // 调试:查看第一个好友数据 - // 执行增量同步 const syncResult = await MessageManager.syncSessions(currentUserId, { friends, @@ -655,6 +652,8 @@ const MessageList: React.FC = () => { top: 0, }, sortKey: "", + phone: msgData.phone || "", + region: msgData.region || "", }; await MessageManager.addSession(newSession); @@ -681,6 +680,8 @@ const MessageList: React.FC = () => { top: 0, }, sortKey: "", + phone: msgData.phone || "", + region: msgData.region || "", }; await MessageManager.addSession(newSession); @@ -710,7 +711,6 @@ const MessageList: React.FC = () => { // 点击会话 const onContactClick = async (session: ChatSession) => { console.log("onContactClick", session); - console.log("session.aiType:", session.aiType); // 调试:查看 aiType 字段 // 设置当前会话 setCurrentContact(session as any); diff --git a/Touchkebao/src/store/module/websocket/websocket.ts b/Touchkebao/src/store/module/websocket/websocket.ts index 359fcf4aa..51134cc2f 100644 --- a/Touchkebao/src/store/module/websocket/websocket.ts +++ b/Touchkebao/src/store/module/websocket/websocket.ts @@ -394,7 +394,7 @@ export const useWebSocketStore = createPersistStore( set({ messages: [...currentState.messages, newMessage], - unreadCount: currentState.config.unreadCount + 1, + unreadCount: (currentState.unreadCount ?? 0) + 1, }); //消息处理器 msgManageCore(data); diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts index 61bffeb81..836d9b462 100644 --- a/Touchkebao/src/utils/db.ts +++ b/Touchkebao/src/utils/db.ts @@ -60,6 +60,8 @@ export interface ChatSession { chatroomOwner?: string; // 群主 selfDisplayName?: string; // 群内昵称 notice?: string; // 群公告 + phone?: string; // 联系人电话 + region?: string; // 联系人地区 } // ==================== 统一联系人表(兼容好友和群聊) ==================== @@ -128,15 +130,14 @@ class CunkebaoDatabase extends Dexie { constructor(dbName: string) { super(dbName); - // 版本1:统一表结构 this.version(1).stores({ // 会话表索引:支持按用户、类型、时间、置顶等查询 chatSessions: - "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], sortKey, nickname, conRemark, avatar, content, lastUpdateTime", + "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType, phone, region", // 联系人表索引:支持按用户、类型、标签、搜索等查询 contactsUnified: - "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId", + "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType, phone, region", // 联系人标签映射表索引:支持按用户、标签、联系人、类型查询 contactLabelMap: @@ -146,47 +147,6 @@ class CunkebaoDatabase extends Dexie { userLoginRecords: "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime", }); - - // 版本2:添加 aiType 字段 - this.version(2) - .stores({ - // 会话表索引:添加 aiType 索引 - chatSessions: - "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType", - - // 联系人表索引:添加 aiType 索引 - contactsUnified: - "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType", - - // 联系人标签映射表索引:保持不变 - contactLabelMap: - "serverId, userId, labelId, contactId, contactType, [userId+labelId], [userId+contactId], [userId+labelId+sortKey], sortKey, searchKey, avatar, nickname, conRemark, unreadCount, lastUpdateTime", - - // 用户登录记录表索引:保持不变 - userLoginRecords: - "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime", - }) - .upgrade(tx => { - // 数据迁移:为现有数据添加 aiType 默认值 - return tx - .table("chatSessions") - .toCollection() - .modify(session => { - if (session.aiType === undefined) { - session.aiType = 0; // 默认为普通类型 - } - }) - .then(() => { - return tx - .table("contactsUnified") - .toCollection() - .modify(contact => { - if (contact.aiType === undefined) { - contact.aiType = 0; // 默认为普通类型 - } - }); - }); - }); } } @@ -344,6 +304,8 @@ export class DatabaseService { const dataToInsert = { ...data, serverId: data.id, // 使用接口的id作为serverId主键 + phone: data.phone ?? "", + region: data.region ?? "", }; return await this.table.add(dataToInsert as T); } @@ -407,6 +369,8 @@ export class DatabaseService { const processedData = newData.map(item => ({ ...item, serverId: item.id, // 使用接口的id作为serverId主键 + phone: item.phone ?? "", + region: item.region ?? "", })); return await this.table.bulkAdd(processedData as T[], { allKeys: true }); diff --git a/Touchkebao/src/utils/dbAction/contact.ts b/Touchkebao/src/utils/dbAction/contact.ts index 254f2ec8e..5630d4d44 100644 --- a/Touchkebao/src/utils/dbAction/contact.ts +++ b/Touchkebao/src/utils/dbAction/contact.ts @@ -184,7 +184,9 @@ export class ContactManager { local.conRemark !== server.conRemark || local.avatar !== server.avatar || local.wechatAccountId !== server.wechatAccountId || - (local.aiType ?? 0) !== (server.aiType ?? 0) // 添加 aiType 比较 + (local.aiType ?? 0) !== (server.aiType ?? 0) || // 添加 aiType 比较 + (local.phone ?? "") !== (server.phone ?? "") || + (local.region ?? "") !== (server.region ?? "") ); } diff --git a/Touchkebao/src/utils/dbAction/message.ts b/Touchkebao/src/utils/dbAction/message.ts index f6deae3dc..9df031161 100644 --- a/Touchkebao/src/utils/dbAction/message.ts +++ b/Touchkebao/src/utils/dbAction/message.ts @@ -93,6 +93,8 @@ export class MessageManager { content: (friend as any).content || "", lastUpdateTime: friend.lastUpdateTime || new Date().toISOString(), aiType: (friend as any).aiType ?? 0, // AI类型,默认为0(普通) + phone: (friend as any).phone ?? "", + region: (friend as any).region ?? "", config: { unreadCount: friend.config?.unreadCount || 0, top: (friend.config as any)?.top || false, @@ -126,6 +128,8 @@ export class MessageManager { content: (group as any).content || "", lastUpdateTime: (group as any).lastUpdateTime || new Date().toISOString(), aiType: (group as any).aiType ?? 0, // AI类型,默认为0(普通) + phone: (group as any).phone ?? "", + region: (group as any).region ?? "", config: { unreadCount: (group.config as any)?.unreadCount || 0, top: (group.config as any)?.top || false, @@ -199,6 +203,8 @@ export class MessageManager { "avatar", "wechatAccountId", // 添加wechatAccountId比较 "aiType", // 添加aiType比较 + "phone", + "region", ]; for (const field of fieldsToCompare) { From c41d8125da2d12af208b6f591fbffe8c799068fd 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: Thu, 13 Nov 2025 17:51:22 +0800 Subject: [PATCH 04/15] =?UTF-8?q?=E6=9B=B4=E6=96=B0ContractData=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=BB=A5=E4=BD=BFextendFields=E5=8F=AF=E9=80=89?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=94=B9=E7=9B=B8=E5=85=B3=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91=E3=80=82=E5=A2=9E=E5=BC=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=A8=A1=E5=BC=8F=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?extendFields=E4=BD=9C=E4=B8=BAJSON=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E7=A1=AE=E4=BF=9D=E5=9C=A8=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=9C=9F=E9=97=B4=E8=BF=9B=E8=A1=8C=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E5=A4=84=E7=90=86=E3=80=82=E6=94=B9=E8=BF=9BContactMa?= =?UTF-8?q?nager=E4=B8=AD=E7=9A=84=E6=AF=94=E8=BE=83=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BB=A5=E5=8C=85=E5=90=ABextendFields=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/src/pages/pc/ckbox/data.ts | 2 +- .../SidebarMenu/MessageList/data.ts | 2 +- .../SidebarMenu/WechatFriends/extend.ts | 33 +++++++ Touchkebao/src/pages/pc/ckbox/weChat/data.ts | 2 +- .../src/store/module/weChat/contacts.data.ts | 2 +- Touchkebao/src/utils/db.ts | 88 ++++++++++++++++--- Touchkebao/src/utils/dbAction/contact.ts | 9 +- Touchkebao/src/utils/dbAction/message.ts | 17 ++++ 8 files changed, 136 insertions(+), 19 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/data.ts b/Touchkebao/src/pages/pc/ckbox/data.ts index 037a43678..9b8aa285a 100644 --- a/Touchkebao/src/pages/pc/ckbox/data.ts +++ b/Touchkebao/src/pages/pc/ckbox/data.ts @@ -146,7 +146,7 @@ export interface ContractData { labels: string[]; signature: string; accountId: number; - extendFields: null; + extendFields?: Record | null; city?: string; lastUpdateTime: string; isPassed: boolean; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts index 5aa5c726f..bf431076c 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts @@ -15,7 +15,7 @@ export interface ContractData { labels: string[]; signature: string; accountId: number; - extendFields: null; + extendFields?: Record | null; city?: string; lastUpdateTime: string; isPassed: boolean; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts index 220c4d56f..cf2b81174 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts @@ -82,6 +82,20 @@ export const getAllGroups = async () => { } }; +const serializeExtendFields = (value: any) => { + if (typeof value === "string") { + return value.trim() ? value : "{}"; + } + if (value && typeof value === "object") { + try { + return JSON.stringify(value); + } catch (error) { + console.warn("序列化 extendFields 失败:", error); + } + } + return "{}"; +}; + /** * 将好友数据转换为统一的 Contact 格式 */ @@ -95,11 +109,21 @@ export const convertFriendsToContacts = ( id: friend.id, type: "friend" as const, wechatAccountId: friend.wechatAccountId, + wechatFriendId: friend.id, wechatId: friend.wechatId, nickname: friend.nickname || "", conRemark: friend.conRemark || "", avatar: friend.avatar || "", + alias: friend.alias || "", + gender: friend.gender, + aiType: friend.aiType ?? 0, + phone: friend.phone ?? "", + region: friend.region ?? "", + quanPin: friend.quanPin || "", + signature: friend.signature || "", + config: friend.config || {}, groupId: friend.groupId, // 保留标签ID + extendFields: serializeExtendFields(friend.extendFields), lastUpdateTime: new Date().toISOString(), sortKey: "", searchKey: "", @@ -120,10 +144,19 @@ export const convertGroupsToContacts = ( type: "group" as const, wechatAccountId: group.wechatAccountId, wechatId: group.chatroomId || "", + chatroomId: group.chatroomId || "", + chatroomOwner: group.chatroomOwner || "", nickname: group.nickname || "", conRemark: group.conRemark || "", avatar: group.chatroomAvatar || group.avatar || "", + selfDisplayName: group.selfDisplyName || "", + notice: group.notice || "", + aiType: group.aiType ?? 0, + phone: group.phone ?? "", + region: group.region ?? "", + config: group.config || {}, groupId: group.groupId, // 保留标签ID + extendFields: serializeExtendFields(group.extendFields), lastUpdateTime: new Date().toISOString(), sortKey: "", searchKey: "", diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/data.ts b/Touchkebao/src/pages/pc/ckbox/weChat/data.ts index 7b0a7acaf..d69fe801b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/data.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/data.ts @@ -144,7 +144,7 @@ export interface ContractData { labels: string[]; signature: string; accountId: number; - extendFields: null; + extendFields?: Record | null; city?: string; lastUpdateTime: string; isPassed: boolean; diff --git a/Touchkebao/src/store/module/weChat/contacts.data.ts b/Touchkebao/src/store/module/weChat/contacts.data.ts index 0ef039e10..c0769fd90 100644 --- a/Touchkebao/src/store/module/weChat/contacts.data.ts +++ b/Touchkebao/src/store/module/weChat/contacts.data.ts @@ -50,7 +50,7 @@ export interface ContractData { labels: string[]; signature: string; accountId: number; - extendFields: null; + extendFields?: Record | null; city?: string; lastUpdateTime: string; isPassed: boolean; diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts index 836d9b462..69da02e2c 100644 --- a/Touchkebao/src/utils/db.ts +++ b/Touchkebao/src/utils/db.ts @@ -62,6 +62,7 @@ export interface ChatSession { notice?: string; // 群公告 phone?: string; // 联系人电话 region?: string; // 联系人地区 + extendFields?: string; // 扩展字段(JSON 字符串) } // ==================== 统一联系人表(兼容好友和群聊) ==================== @@ -92,6 +93,7 @@ export interface Contact { signature?: string; // 个性签名 phone?: string; // 手机号 quanPin?: string; // 全拼 + extendFields?: string; // 扩展字段(JSON 字符串) // 群聊特有字段(type='group'时有效) chatroomId?: string; // 群聊ID @@ -147,6 +149,41 @@ class CunkebaoDatabase extends Dexie { userLoginRecords: "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime", }); + + this.version(2) + .stores({ + chatSessions: + "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], [userId+aiType], sortKey, nickname, conRemark, avatar, content, lastUpdateTime, aiType, phone, region, extendFields", + contactsUnified: + "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+aiType], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId, aiType, phone, region, extendFields", + contactLabelMap: + "serverId, userId, labelId, contactId, contactType, [userId+labelId], [userId+contactId], [userId+labelId+sortKey], sortKey, searchKey, avatar, nickname, conRemark, unreadCount, lastUpdateTime", + userLoginRecords: + "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime", + }) + .upgrade(async tx => { + await tx + .table("chatSessions") + .toCollection() + .modify(session => { + if (!("extendFields" in session) || session.extendFields == null) { + session.extendFields = "{}"; + } else if (typeof session.extendFields !== "string") { + session.extendFields = JSON.stringify(session.extendFields); + } + }); + + await tx + .table("contactsUnified") + .toCollection() + .modify(contact => { + if (!("extendFields" in contact) || contact.extendFields == null) { + contact.extendFields = "{}"; + } else if (typeof contact.extendFields !== "string") { + contact.extendFields = JSON.stringify(contact.extendFields); + } + }); + }); } } @@ -295,18 +332,18 @@ export class DatabaseService { // 基础 CRUD 操作 - 使用serverId作为主键 async create(data: Omit): Promise { - return await this.table.add(data as T); + return await this.table.add(this.prepareDataForWrite(data) as T); } // 创建数据(直接使用接口数据) // 接口数据的id字段直接作为serverId主键,原id字段保留 async createWithServerId(data: any): Promise { - const dataToInsert = { + const dataToInsert = this.prepareDataForWrite({ ...data, serverId: data.id, // 使用接口的id作为serverId主键 phone: data.phone ?? "", region: data.region ?? "", - }; + }); return await this.table.add(dataToInsert as T); } @@ -325,7 +362,10 @@ export class DatabaseService { } async update(serverId: string | number, data: Partial): Promise { - return await this.table.update(serverId, data as any); + return await this.table.update( + serverId, + this.prepareDataForWrite(data) as any, + ); } async updateMany( @@ -334,7 +374,7 @@ export class DatabaseService { return await this.table.bulkUpdate( dataList.map(item => ({ key: item.serverId, - changes: item.data as any, + changes: this.prepareDataForWrite(item.data) as any, })), ); } @@ -342,7 +382,8 @@ export class DatabaseService { async createMany( dataList: Omit[], ): Promise<(string | number)[]> { - return await this.table.bulkAdd(dataList as T[], { allKeys: true }); + const processed = dataList.map(item => this.prepareDataForWrite(item)); + return await this.table.bulkAdd(processed as T[], { allKeys: true }); } // 批量创建数据(直接使用接口数据) @@ -366,12 +407,14 @@ export class DatabaseService { return []; } - const processedData = newData.map(item => ({ - ...item, - serverId: item.id, // 使用接口的id作为serverId主键 - phone: item.phone ?? "", - region: item.region ?? "", - })); + const processedData = newData.map(item => + this.prepareDataForWrite({ + ...item, + serverId: item.id, // 使用接口的id作为serverId主键 + phone: item.phone ?? "", + region: item.region ?? "", + }), + ); return await this.table.bulkAdd(processedData as T[], { allKeys: true }); } @@ -545,6 +588,27 @@ export class DatabaseService { .equals(value) .count(); } + + private prepareDataForWrite(data: any) { + if (!data || typeof data !== "object") { + return data; + } + + const prepared = { ...data }; + + if ("extendFields" in prepared) { + const value = prepared.extendFields; + if (typeof value === "string" && value.trim() !== "") { + prepared.extendFields = value; + } else if (value && typeof value === "object") { + prepared.extendFields = JSON.stringify(value); + } else { + prepared.extendFields = "{}"; + } + } + + return prepared; + } } // 创建统一表的服务实例 diff --git a/Touchkebao/src/utils/dbAction/contact.ts b/Touchkebao/src/utils/dbAction/contact.ts index 5630d4d44..abbb7fdb7 100644 --- a/Touchkebao/src/utils/dbAction/contact.ts +++ b/Touchkebao/src/utils/dbAction/contact.ts @@ -186,7 +186,8 @@ export class ContactManager { local.wechatAccountId !== server.wechatAccountId || (local.aiType ?? 0) !== (server.aiType ?? 0) || // 添加 aiType 比较 (local.phone ?? "") !== (server.phone ?? "") || - (local.region ?? "") !== (server.region ?? "") + (local.region ?? "") !== (server.region ?? "") || + (local.extendFields ?? "{}") !== (server.extendFields ?? "{}") ); } @@ -194,10 +195,12 @@ export class ContactManager { * 获取联系人分组列表 */ static async getContactGroups( - userId: number, - customerId?: number, + _userId: number, + _customerId?: number, ): Promise { try { + void _userId; + void _customerId; // 这里应该根据实际的标签系统来实现 // 暂时返回空数组,实际实现需要根据标签表来查询 return []; diff --git a/Touchkebao/src/utils/dbAction/message.ts b/Touchkebao/src/utils/dbAction/message.ts index 9df031161..d42289bfe 100644 --- a/Touchkebao/src/utils/dbAction/message.ts +++ b/Touchkebao/src/utils/dbAction/message.ts @@ -11,6 +11,20 @@ import Dexie from "dexie"; import { db, chatSessionService, ChatSession } from "../db"; import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; +const serializeExtendFields = (value: any) => { + if (typeof value === "string") { + return value.trim() ? value : "{}"; + } + if (value && typeof value === "object") { + try { + return JSON.stringify(value); + } catch (error) { + console.warn("序列化 extendFields 失败:", error); + } + } + return "{}"; +}; + export class MessageManager { private static updateCallbacks = new Set<(sessions: ChatSession[]) => void>(); @@ -103,6 +117,7 @@ export class MessageManager { wechatFriendId: friend.id, wechatId: friend.wechatId, alias: friend.alias, + extendFields: serializeExtendFields((friend as any).extendFields), }; } @@ -139,6 +154,7 @@ export class MessageManager { chatroomOwner: group.chatroomOwner, selfDisplayName: group.selfDisplyName, notice: group.notice, + extendFields: serializeExtendFields((group as any).extendFields), }; } @@ -205,6 +221,7 @@ export class MessageManager { "aiType", // 添加aiType比较 "phone", "region", + "extendFields", ]; for (const field of fieldsToCompare) { From b3924cdb71c90a6dbfd566418a3baf4c22d53905 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: Fri, 14 Nov 2025 10:07:38 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E4=BC=98=E5=8C=96CreatePushTask=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E7=9A=84=E8=81=94=E7=B3=BB=E4=BA=BA=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E6=A0=87=E7=AD=BE=E3=80=81=E5=9F=8E=E5=B8=82=E5=92=8C?= =?UTF-8?q?=E6=98=B5=E7=A7=B0/=E5=A4=87=E6=B3=A8=E7=9A=84=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E6=A8=A1=E6=80=81=E3=80=82=E6=9B=B4=E6=96=B0=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E6=A8=A1=E6=80=81=E7=9A=84=E6=A0=B7=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E8=BF=87=E6=BB=A4=E5=80=BC=E7=9A=84=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=8C=E4=BB=A5=E6=94=B9=E5=96=84?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Moncter/src/pages/pc/ckbox/weChat/api.ts | 0 .../components/ProfileCard/components/ProfileModules/index.tsx | 0 Moncter/src/store/module/websocket/websocket.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Moncter/src/pages/pc/ckbox/weChat/api.ts create mode 100644 Moncter/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx create mode 100644 Moncter/src/store/module/websocket/websocket.ts diff --git a/Moncter/src/pages/pc/ckbox/weChat/api.ts b/Moncter/src/pages/pc/ckbox/weChat/api.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Moncter/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Moncter/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/Moncter/src/store/module/websocket/websocket.ts b/Moncter/src/store/module/websocket/websocket.ts new file mode 100644 index 000000000..e69de29bb From dbe4ee692ca8edee3f1b6c52c48455c1a82c915f 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: Fri, 14 Nov 2025 11:00:21 +0800 Subject: [PATCH 06/15] =?UTF-8?q?=E6=9B=B4=E6=96=B0WeChat=20API=E4=BB=A5?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B0=E7=AB=AF=E7=82=B9=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E8=8E=B7=E5=8F=96=E5=8F=AF=E8=BD=AC=E7=A7=BB=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E5=88=97=E8=A1=A8=E7=9A=84=E9=80=BB=E8=BE=91=E3=80=82?= =?UTF-8?q?=E9=87=8D=E6=9E=84ProfileCard=E7=BB=84=E4=BB=B6=E4=BB=A5?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E9=BB=98=E8=AE=A4=E6=B4=BB=E5=8A=A8=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E5=8D=A1=E4=B8=BA=E2=80=9Cprofile=E2=80=9D=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=80=82=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/src/pages/pc/ckbox/weChat/api.ts | 2 +- .../components/toContract/index.tsx | 2 +- .../ProfileModules/components/detailValue.tsx | 102 ++++++++++++++++++ .../components/ProfileModules/index.tsx | 26 +---- .../components/ProfileCard/index.tsx | 10 +- 5 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/api.ts b/Touchkebao/src/pages/pc/ckbox/weChat/api.ts index 8ce1bcacc..32bb96047 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/api.ts @@ -94,7 +94,7 @@ export function WechatFriendAllot(params: { //获取可转移客服列表 export function getTransferableAgentList() { - return request2("/api/account/myDepartmentAccountsForTransfer", {}, "GET"); + return request("/v1/kefu/accounts/list", {}, "GET"); } // 微信好友列表 diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx index 1c7975f51..21dfa37a1 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx @@ -49,7 +49,7 @@ const ToContract: React.FC = ({ const openModal = () => { setVisible(true); getTransferableAgentList().then(data => { - setCustomerServiceList(data); + setCustomerServiceList(data.list); }); }; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx new file mode 100644 index 000000000..b63770584 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx @@ -0,0 +1,102 @@ +import React, { useCallback } from "react"; +import { Button, Input } from "antd"; + +import styles from "../Person.module.scss"; + +export interface DetailValueField { + label: string; + key: string; + ifEdit?: boolean; + placeholder?: string; +} + +export interface DetailValueProps { + fields: DetailValueField[]; + value?: Record; + onChange?: (next: Record) => void; + onSubmit?: (next: Record) => void; + submitText?: string; + submitting?: boolean; + renderFooter?: React.ReactNode; +} + +const DetailValue: React.FC = ({ + fields, + value, + onChange, + onSubmit, + submitText = "保存", + submitting = false, + renderFooter, +}) => { + const handleFieldChange = useCallback( + (fieldKey: string, nextVal: string) => { + const baseValue = value ?? {}; + const nextValue = { + ...baseValue, + [fieldKey]: nextVal, + }; + onChange?.(nextValue); + }, + [onChange, value], + ); + + const handleSubmit = useCallback(() => { + onSubmit?.(value ?? {}); + }, [onSubmit, value]); + + const formValue = value ?? {}; + + return ( +
+ {fields.map(field => { + const disabled = field.ifEdit === false; + const fieldValue = formValue[field.key] ?? ""; + + return ( +
+ {field.label}: +
+ {disabled ? ( + {fieldValue || field.placeholder || "--"} + ) : ( + + handleFieldChange(field.key, event.target.value) + } + onPressEnter={handleSubmit} + /> + )} +
+
+ ); + })} + + {(onSubmit || renderFooter) && ( +
+ {renderFooter} + {onSubmit && ( + + )} +
+ )} +
+ ); +}; + +export default DetailValue; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index 847b3b417..b71db1e0c 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -97,9 +97,7 @@ const Person: React.FC = ({ contract }) => { useState(false); const [isTransferOwnerSelectionVisible, setIsTransferOwnerSelectionVisible] = useState(false); - const [selectedFriends, setSelectedFriends] = useState( - [], - ); + const [contractList, setContractList] = useState([]); const handleAddFriend = member => { @@ -374,16 +372,6 @@ const Person: React.FC = ({ contract }) => { messageApi.success("已应用AI生成的群公告内容"); }; - // 点击编辑群公告按钮 - const handleEditGroupNotice = () => { - if (!hasGroupManagePermission()) { - messageApi.error("只有群主才能修改群公告"); - return; - } - setGroupNoticeValue(contract.notice || ""); - setIsGroupNoticeModalVisible(true); - }; - // 处理我在本群中的昵称保存 const handleSaveSelfDisplayName = () => { sendCommand("CmdChatroomOperate", { @@ -397,12 +385,6 @@ const Person: React.FC = ({ contract }) => { setIsEditingSelfDisplayName(false); }; - // 点击编辑群昵称按钮 - const handleEditSelfDisplayName = () => { - setSelfDisplayNameValue(contract.selfDisplyName || ""); - setIsEditingSelfDisplayName(true); - }; - // 处理取消编辑 const handleCancelEdit = () => { setRemarkValue(contract.conRemark || ""); @@ -508,18 +490,19 @@ const Person: React.FC = ({ contract }) => { }, }); }; - + const extendFields = JSON.parse(contract.extendFields || "{}"); // 构建联系人或群聊详细信息 const contractInfo = { name: contract.name || contract.nickname, nickname: contract.nickname, - conRemark: remarkValue, // 使用当前编辑的备注值 alias: contract.alias, wechatId: contract.wechatId, chatroomId: isGroup ? contract.chatroomId : undefined, chatroomOwner: isGroup ? contract.chatroomOwner : undefined, avatar: contract.avatar || contract.chatroomAvatar, phone: contract.phone || "-", + conRemark: remarkValue, // 使用当前编辑的备注值 + remark: extendFields.remark || "-", email: contract.email || "-", department: contract.department || "-", position: contract.position || "-", @@ -1278,7 +1261,6 @@ const Person: React.FC = ({ contract }) => { visible={isFriendSelectionVisible} onCancel={() => setIsFriendSelectionVisible(false)} onConfirm={(selectedIds, selectedItems) => { - setSelectedFriends(selectedItems); handleAddMember( selectedIds.map(id => parseInt(id)), selectedItems, diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx index 5fe73a754..7d322637b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx @@ -16,7 +16,7 @@ interface PersonProps { } const Person: React.FC = ({ contract }) => { - const [activeKey, setActiveKey] = useState("quickwords"); + const [activeKey, setActiveKey] = useState("profile"); const isGroup = "chatroomId" in contract; const tabItems = useMemo(() => { const baseItems = [ @@ -42,8 +42,8 @@ const Person: React.FC = ({ contract }) => { }, [contract, isGroup]); useEffect(() => { - setActiveKey("quickwords"); - setRenderedKeys(["quickwords"]); + setActiveKey("profile"); + setRenderedKeys(["profile"]); }, [contract]); const tabHeaderItems = useMemo( @@ -56,9 +56,7 @@ const Person: React.FC = ({ contract }) => { [tabItems], ); - const [renderedKeys, setRenderedKeys] = useState(() => [ - "quickwords", - ]); + const [renderedKeys, setRenderedKeys] = useState(() => ["profile"]); useEffect(() => { if (!availableKeys.includes(activeKey) && availableKeys.length > 0) { From 01bf7ee2711399bd5923f188db773eb4a90f256a 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: Fri, 14 Nov 2025 15:30:00 +0800 Subject: [PATCH 07/15] =?UTF-8?q?=E6=B7=BB=E5=8A=A0WebSocket=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E4=B8=AD=E7=9A=84=E6=B4=BB=E8=B7=83?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=AF=B7=E6=B1=82=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E9=A2=91=E7=B9=81=E8=AF=B7=E6=B1=82=E3=80=82?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E6=96=B0=E7=9A=84=E7=8A=B6=E6=80=81=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E4=BB=A5=E8=B7=9F=E8=B8=AA=E6=9C=80=E5=90=8E=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=97=B6=E9=97=B4=EF=BC=8C=E5=B9=B6=E5=9C=A8=E5=8F=91?= =?UTF-8?q?=E9=80=81=E6=B4=BB=E8=B7=83=E7=8A=B6=E6=80=81=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=97=B6=E8=BF=9B=E8=A1=8C=E6=97=B6=E9=97=B4=E9=97=B4=E9=9A=94?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E3=80=82=E6=B8=85=E7=90=86=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=BB=A5=E7=A1=AE=E4=BF=9D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/store/module/websocket/websocket.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Touchkebao/src/store/module/websocket/websocket.ts b/Touchkebao/src/store/module/websocket/websocket.ts index 51134cc2f..57ed7f15e 100644 --- a/Touchkebao/src/store/module/websocket/websocket.ts +++ b/Touchkebao/src/store/module/websocket/websocket.ts @@ -53,6 +53,7 @@ interface WebSocketState { reconnectTimer: NodeJS.Timeout | null; aliveStatusTimer: NodeJS.Timeout | null; // 客服用户状态查询定时器 aliveStatusUnsubscribe: (() => void) | null; + aliveStatusLastRequest: number | null; // 方法 connect: (config: Partial) => void; @@ -88,6 +89,8 @@ const DEFAULT_CONFIG: WebSocketConfig = { maxReconnectAttempts: 5, }; +const ALIVE_STATUS_MIN_INTERVAL = 5 * 1000; // ms + export const useWebSocketStore = createPersistStore( (set, get) => ({ status: WebSocketStatus.DISCONNECTED, @@ -99,6 +102,7 @@ export const useWebSocketStore = createPersistStore( reconnectTimer: null, aliveStatusTimer: null, aliveStatusUnsubscribe: null, + aliveStatusLastRequest: null, // 连接WebSocket connect: (config: Partial) => { @@ -234,11 +238,6 @@ export const useWebSocketStore = createPersistStore( currentState.status !== WebSocketStatus.CONNECTED || !currentState.ws ) { - // Toast.show({ - // content: "WebSocket未连接,正在重新连接...", - // position: "top", - // }); - // 重置连接状态并发起重新连接 set({ status: WebSocketStatus.DISCONNECTED }); if (currentState.config) { @@ -485,6 +484,14 @@ export const useWebSocketStore = createPersistStore( return; } + const now = Date.now(); + if ( + state.aliveStatusLastRequest && + now - state.aliveStatusLastRequest < ALIVE_STATUS_MIN_INTERVAL + ) { + return; + } + const { customerList } = useCustomerStore.getState(); const { kfUserList } = useCkChatStore.getState(); const targets = @@ -498,6 +505,7 @@ export const useWebSocketStore = createPersistStore( state.sendCommand("CmdRequestWechatAccountsAliveStatus", { wechatAccountIds: targets.map(v => v.id), }); + set({ aliveStatusLastRequest: now }); } }; @@ -556,7 +564,11 @@ export const useWebSocketStore = createPersistStore( if (currentState.aliveStatusTimer) { clearInterval(currentState.aliveStatusTimer); } - set({ aliveStatusTimer: null, aliveStatusUnsubscribe: null }); + set({ + aliveStatusTimer: null, + aliveStatusUnsubscribe: null, + aliveStatusLastRequest: null, + }); }, }), { @@ -568,6 +580,7 @@ export const useWebSocketStore = createPersistStore( messages: state.messages.slice(-100), // 只保留最近100条消息 unreadCount: state.unreadCount, reconnectAttempts: state.reconnectAttempts, + aliveStatusLastRequest: state.aliveStatusLastRequest, // 注意:定时器不需要持久化,重新连接时会重新创建 }), onRehydrateStorage: () => state => { From eca55495924fc3c120ddd96ab2f02545cc12056b 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: Fri, 14 Nov 2025 16:19:18 +0800 Subject: [PATCH 08/15] =?UTF-8?q?=E4=BC=98=E5=8C=96SidebarMenu=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=E5=86=85=E5=AE=B9=E6=B8=B2=E6=9F=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BD=BF=E7=94=A8useRef=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E5=8D=A1=E5=86=85=E5=AE=B9=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E6=80=A7=E8=83=BD=E3=80=82=E6=9B=B4=E6=96=B0=E8=81=94?= =?UTF-8?q?=E7=B3=BB=E4=BA=BA=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=90=9C=E7=B4=A2=E5=85=B3=E9=94=AE=E8=AF=8D?= =?UTF-8?q?=E7=9A=84=E9=98=B2=E6=8A=96=E5=A4=84=E7=90=86=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E6=90=9C=E7=B4=A2=E8=AF=B7=E6=B1=82=E4=B8=8D=E4=BC=9A?= =?UTF-8?q?=E9=A2=91=E7=B9=81=E8=A7=A6=E5=8F=91=E3=80=82=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E7=94=A8=E6=88=B7ID=E7=9A=84=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E4=BB=A5=E4=BC=98=E5=8C=96=E6=90=9C=E7=B4=A2=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E7=9A=84=E5=A4=84=E7=90=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../weChat/components/SidebarMenu/index.tsx | 52 +++++++++++++++---- .../src/store/module/weChat/contacts.ts | 23 +++++++- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx index 3699b995b..cf72b9622 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Input, Skeleton, Button, Dropdown, MenuProps } from "antd"; import { SearchOutlined, @@ -193,6 +193,27 @@ const SidebarMenu: React.FC = ({ loading = false }) => {
); + const tabContentCacheRef = useRef>({}); + + const getTabContent = (tabKey: string) => { + if (!tabContentCacheRef.current[tabKey]) { + switch (tabKey) { + case "chats": + tabContentCacheRef.current[tabKey] = ; + break; + case "contracts": + tabContentCacheRef.current[tabKey] = ; + break; + case "friendsCicle": + tabContentCacheRef.current[tabKey] = ; + break; + default: + tabContentCacheRef.current[tabKey] = null; + } + } + return tabContentCacheRef.current[tabKey]; + }; + // 渲染内容部分 const renderContent = () => { // 如果正在切换tab到聊天,显示骨架屏 @@ -200,16 +221,27 @@ const SidebarMenu: React.FC = ({ loading = false }) => { return renderSkeleton(); } - switch (activeTab) { - case "chats": - return ; - case "contracts": - return ; - case "friendsCicle": - return ; - default: - return null; + const availableTabs = ["chats", "contracts"]; + if (currentCustomer && currentCustomer.id !== 0) { + availableTabs.push("friendsCicle"); } + + return ( + <> + {availableTabs.map(tabKey => ( +
+ {getTabContent(tabKey)} +
+ ))} + + ); }; if (loading) { diff --git a/Touchkebao/src/store/module/weChat/contacts.ts b/Touchkebao/src/store/module/weChat/contacts.ts index ca875b974..ee8827f2b 100644 --- a/Touchkebao/src/store/module/weChat/contacts.ts +++ b/Touchkebao/src/store/module/weChat/contacts.ts @@ -3,6 +3,10 @@ import { persist } from "zustand/middleware"; import { ContactGroupByLabel } from "@/pages/pc/ckbox/data"; import { Contact } from "@/utils/db"; import { ContactManager } from "@/utils/dbAction"; +import { useUserStore } from "@/store/module/user"; + +const SEARCH_DEBOUNCE_DELAY = 300; +let searchDebounceTimer: ReturnType | null = null; /** * 联系人状态管理接口 @@ -171,8 +175,16 @@ export const useContactStore = create()( setSearchKeyword: (keyword: string) => { set({ searchKeyword: keyword }); + + if (searchDebounceTimer) { + clearTimeout(searchDebounceTimer); + searchDebounceTimer = null; + } + if (keyword.trim()) { - get().searchContacts(keyword); + searchDebounceTimer = setTimeout(() => { + get().searchContacts(keyword); + }, SEARCH_DEBOUNCE_DELAY); } else { set({ isSearchMode: false, searchResults: [] }); } @@ -204,8 +216,15 @@ export const useContactStore = create()( set({ loading: true, isSearchMode: true }); try { + const currentUserId = useUserStore.getState().user?.id; + + if (!currentUserId) { + set({ searchResults: [], isSearchMode: false, loading: false }); + return; + } + const results = await ContactManager.searchContacts( - get().currentContact?.userId || 0, + currentUserId, keyword, ); set({ searchResults: results }); From 8d5869e6c271cbc1c391c92c61165e154aad5ef8 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: Fri, 14 Nov 2025 17:09:43 +0800 Subject: [PATCH 09/15] /v1/kefu/message/details --- Touchkebao/src/pages/pc/ckbox/api.ts | 28 ++++++++++++++++++-- Touchkebao/src/store/module/weChat/weChat.ts | 4 +-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/api.ts b/Touchkebao/src/pages/pc/ckbox/api.ts index 1fe610860..3840caffc 100644 --- a/Touchkebao/src/pages/pc/ckbox/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/api.ts @@ -44,7 +44,7 @@ export function getChatMessages(params: { Count: number; olderData: boolean; }) { - return request2("/api/FriendMessage/SearchMessage", params, "GET"); + return request("/v1/kefu/message/details", params, "GET"); } export function getChatroomMessages(params: { wechatAccountId: number; @@ -55,8 +55,32 @@ export function getChatroomMessages(params: { Count: number; olderData: boolean; }) { - return request2("/api/ChatroomMessage/SearchMessage", params, "GET"); + return request("/v1/kefu/message/details", params, "GET"); } +//=====================旧============================== + +// export function getChatMessages(params: { +// wechatAccountId: number; +// wechatFriendId?: number; +// wechatChatroomId?: number; +// From: number; +// To: number; +// Count: number; +// olderData: boolean; +// }) { +// return request2("/api/FriendMessage/SearchMessage", params, "GET"); +// } +// export function getChatroomMessages(params: { +// wechatAccountId: number; +// wechatFriendId?: number; +// wechatChatroomId?: number; +// From: number; +// To: number; +// Count: number; +// olderData: boolean; +// }) { +// return request2("/api/ChatroomMessage/SearchMessage", params, "GET"); +// } //获取群列表 export function getGroupList(params: { prevId: number; count: number }) { diff --git a/Touchkebao/src/store/module/weChat/weChat.ts b/Touchkebao/src/store/module/weChat/weChat.ts index f161bac04..889d528b8 100644 --- a/Touchkebao/src/store/module/weChat/weChat.ts +++ b/Touchkebao/src/store/module/weChat/weChat.ts @@ -499,7 +499,7 @@ export const useWeChatStore = create()( } else { set({ currentMessages: [ - ...(messages || []), + ...(messages.list || []), ...state.currentMessages, ], }); @@ -513,7 +513,7 @@ export const useWeChatStore = create()( } else { set({ currentMessages: [ - ...(messages || []), + ...(messages.list || []), ...state.currentMessages, ], }); From 4684e880b18fb10f9d1eed8428c14bd589289ec0 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: Fri, 14 Nov 2025 18:23:54 +0800 Subject: [PATCH 10/15] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=A4=84=E7=90=86=E5=92=8C=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E3=80=82=E4=B8=BA=E6=B6=88=E6=81=AF=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=8F=82=E6=95=B0=E5=BC=95=E5=85=A5=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BB=A5=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8=E6=80=A7=E3=80=82?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=8E=B7=E5=8F=96=E8=81=8A=E5=A4=A9=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=92=8C=E8=81=8A=E5=A4=A9=E5=AE=A4=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E7=9A=84API=E8=B0=83=E7=94=A8=EF=BC=8C=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=88=86=E9=A1=B5=E3=80=82=E9=80=9A=E8=BF=87=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E5=99=A8=E5=92=8C=E4=BC=98=E5=8C=96=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E6=9D=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96MessageList=E7=BB=84=E4=BB=B6=E3=80=82=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E7=A1=AE=E4=BF=9D=E7=BB=84=E4=BB=B6=E4=B9=8B?= =?UTF-8?q?=E9=97=B4=E6=B6=88=E6=81=AF=E5=8A=A0=E8=BD=BD=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E7=9A=84=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/src/pages/pc/ckbox/api.ts | 48 ++-- .../components/toContract/index.tsx | 11 +- .../components/MessageRecord/index.tsx | 53 ++-- .../SidebarMenu/MessageList/index.tsx | 158 +++++------ .../src/store/module/weChat/message.data.ts | 22 +- Touchkebao/src/store/module/weChat/message.ts | 91 ++++++- .../src/store/module/weChat/weChat.data.ts | 8 +- Touchkebao/src/store/module/weChat/weChat.ts | 252 ++++++++++++++---- Touchkebao/src/utils/dbAction/message.ts | 17 +- 9 files changed, 448 insertions(+), 212 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/api.ts b/Touchkebao/src/pages/pc/ckbox/api.ts index 3840caffc..f5717fa7d 100644 --- a/Touchkebao/src/pages/pc/ckbox/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/api.ts @@ -35,26 +35,38 @@ export function updateConfig(params) { return request2("/api/WechatFriend/updateConfig", params, "PUT"); } //获取聊天记录-2 获取列表 -export function getChatMessages(params: { - wechatAccountId: number; - wechatFriendId?: number; - wechatChatroomId?: number; - From: number; - To: number; - Count: number; - olderData: boolean; -}) { +export interface messreocrParams { + From?: number | string; + To?: number | string; + /** + * 当前页码,从 1 开始 + */ + page?: number; + /** + * 每页条数 + */ + limit?: number; + /** + * 群id + */ + wechatChatroomId?: number | string; + /** + * 好友id + */ + wechatFriendId?: number | string; + /** + * 微信账号ID + */ + wechatAccountId?: number | string; + /** + * 关键词、类型等扩展参数 + */ + [property: string]: any; +} +export function getChatMessages(params: messreocrParams) { return request("/v1/kefu/message/details", params, "GET"); } -export function getChatroomMessages(params: { - wechatAccountId: number; - wechatFriendId?: number; - wechatChatroomId?: number; - From: number; - To: number; - Count: number; - olderData: boolean; -}) { +export function getChatroomMessages(params: messreocrParams) { return request("/v1/kefu/message/details", params, "GET"); } //=====================旧============================== diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx index 21dfa37a1..1b4d86f77 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx @@ -9,7 +9,6 @@ import { import { useCurrentContact } from "@/store/module/weChat/weChat"; import { ContactManager } from "@/utils/dbAction/contact"; import { MessageManager } from "@/utils/dbAction/message"; -import { triggerRefresh } from "@/store/module/weChat/message"; import { useUserStore } from "@/store/module/user"; import { useWeChatStore } from "@/store/module/weChat/weChat"; const { TextArea } = Input; @@ -110,10 +109,7 @@ const ToContract: React.FC = ({ await ContactManager.deleteContact(currentContact.id); console.log("✅ 已从联系人数据库删除"); - // 3. 触发会话列表刷新 - triggerRefresh(); - - // 4. 清空当前选中的联系人(关闭聊天窗口) + // 3. 清空当前选中的联系人(关闭聊天窗口) clearCurrentContact(); message.success("转接成功,已清理本地数据"); @@ -167,10 +163,7 @@ const ToContract: React.FC = ({ await ContactManager.deleteContact(currentContact.id); console.log("✅ 已从联系人数据库删除"); - // 3. 触发会话列表刷新 - triggerRefresh(); - - // 4. 清空当前选中的联系人(关闭聊天窗口) + // 3. 清空当前选中的联系人(关闭聊天窗口) clearCurrentContact(); message.success("转回成功,已清理本地数据"); diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx index 1da7da52a..29a014394 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx @@ -145,6 +145,9 @@ const MessageRecord: React.FC = ({ contract }) => { const [selectedRecords, setSelectedRecords] = useState([]); const currentMessages = useWeChatStore(state => state.currentMessages); + const currentMessagesHasMore = useWeChatStore( + state => state.currentMessagesHasMore, + ); const loadChatMessages = useWeChatStore(state => state.loadChatMessages); const messagesLoading = useWeChatStore(state => state.messagesLoading); @@ -552,8 +555,14 @@ const MessageRecord: React.FC = ({ contract }) => { }; // 用于分组消息并添加时间戳的辅助函数 - const groupMessagesByTime = (messages: ChatRecord[]) => { - return messages + const groupMessagesByTime = (messages: ChatRecord[] | null | undefined) => { + const safeMessages = Array.isArray(messages) + ? messages + : Array.isArray((messages as any)?.list) + ? ((messages as any).list as ChatRecord[]) + : []; + + return safeMessages .filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息 .map(msg => ({ time: formatWechatTime(String(msg?.wechatTime)), @@ -680,33 +689,10 @@ const MessageRecord: React.FC = ({ contract }) => { ); }; const loadMoreMessages = () => { - // 兼容性处理:检查消息数组和时间戳 - if (!currentMessages || currentMessages.length === 0) { - console.warn("No messages available for loading more"); + if (messagesLoading || !currentMessagesHasMore) { return; } - - const firstMessage = currentMessages[0]; - if (!firstMessage || !firstMessage.createTime) { - console.warn("Invalid message or createTime"); - return; - } - - // 兼容性处理:确保时间戳格式正确 - let timestamp; - try { - const date = new Date(firstMessage.createTime); - if (isNaN(date.getTime())) { - console.warn("Invalid createTime format:", firstMessage.createTime); - return; - } - timestamp = date.getTime() - 24 * 36000 * 1000; - } catch (error) { - console.error("Error parsing createTime:", error); - return; - } - - loadChatMessages(false, timestamp); + loadChatMessages(false); }; const handleForwardMessage = (messageData: ChatRecord) => { @@ -785,8 +771,17 @@ const MessageRecord: React.FC = ({ contract }) => { return (
-
loadMoreMessages()}> - 点击加载更早的信息 {messagesLoading ? : ""} +
loadMoreMessages()} + style={{ + cursor: + currentMessagesHasMore && !messagesLoading ? "pointer" : "default", + opacity: currentMessagesHasMore ? 1 : 0.6, + }} + > + {currentMessagesHasMore ? "点击加载更早的信息" : "已经没有更早的消息了"} + {messagesLoading ? : ""}
{groupMessagesByTime(currentMessages).map((group, groupIndex) => ( diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index 21712a08c..2625b5815 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -15,7 +15,7 @@ import { getWechatFriendDetail, getWechatChatroomDetail, } from "./api"; -import { useMessageStore, triggerRefresh } from "@weChatStore/message"; +import { useMessageStore } from "@weChatStore/message"; import { useWebSocketStore } from "@storeModule/websocket/websocket"; import { useCustomerStore } from "@weChatStore/customer"; import { useContactStore } from "@weChatStore/contacts"; @@ -39,14 +39,12 @@ const MessageList: React.FC = () => { // Store状态 const { loading, - refreshTrigger, hasLoadedOnce, setLoading, setHasLoadedOnce, + sessions, + setSessions: setSessionState, } = useMessageStore(); - - // 组件内部状态:会话列表数据 - const [sessions, setSessions] = useState([]); const [filteredSessions, setFilteredSessions] = useState([]); // 右键菜单相关状态 @@ -74,6 +72,8 @@ const MessageList: React.FC = () => { }); const contextMenuRef = useRef(null); + const previousUserIdRef = useRef(null); + const loadRequestRef = useRef(0); // 右键菜单事件处理 const handleContextMenu = (e: React.MouseEvent, session: ChatSession) => { @@ -105,7 +105,7 @@ const MessageList: React.FC = () => { try { // 1. 立即更新UI并重新排序(乐观更新) - setSessions(prev => { + setSessionState(prev => { const updatedSessions = prev.map(s => s.id === session.id ? { @@ -141,7 +141,7 @@ const MessageList: React.FC = () => { message.success(`${newPinned === 1 ? "置顶" : "取消置顶"}成功`); } catch (error) { // 4. 失败时回滚UI - setSessions(prev => + setSessionState(prev => prev.map(s => s.id === session.id ? { ...s, config: { ...s.config, top: currentPinned } } @@ -162,7 +162,7 @@ const MessageList: React.FC = () => { onOk: async () => { try { // 1. 立即从UI移除 - setSessions(prev => prev.filter(s => s.id !== session.id)); + setSessionState(prev => prev.filter(s => s.id !== session.id)); // 2. 后台调用API await updateConfig({ @@ -180,7 +180,7 @@ const MessageList: React.FC = () => { message.success("删除成功"); } catch (error) { // 4. 失败时恢复UI - setSessions(prev => [...prev, session]); + setSessionState(prev => [...prev, session]); message.error("删除失败"); } @@ -212,7 +212,7 @@ const MessageList: React.FC = () => { try { // 1. 立即更新UI - setSessions(prev => + setSessionState(prev => prev.map(s => s.id === session.id ? { ...s, conRemark: editRemarkModal.remark } : s, ), @@ -258,7 +258,7 @@ const MessageList: React.FC = () => { message.success("备注更新成功"); } catch (error) { // 4. 失败时回滚UI - setSessions(prev => + setSessionState(prev => prev.map(s => s.id === session.id ? { ...s, conRemark: oldRemark } : s, ), @@ -357,112 +357,93 @@ const MessageList: React.FC = () => { `会话同步完成: 新增${syncResult.added}, 更新${syncResult.updated}, 删除${syncResult.deleted}`, ); - // 如果有数据变更,触发UI刷新 - if ( - syncResult.added > 0 || - syncResult.updated > 0 || - syncResult.deleted > 0 - ) { - triggerRefresh(); - } + // 会话管理器会在有变更时触发订阅回调 } catch (error) { console.error("同步服务器数据失败:", error); } }; + // 切换账号时重置加载状态 + useEffect(() => { + if (!currentUserId) return; + if (previousUserIdRef.current === currentUserId) return; + previousUserIdRef.current = currentUserId; + setHasLoadedOnce(false); + setSessionState([]); + }, [currentUserId, setHasLoadedOnce, setSessionState]); + // 初始化加载会话列表 useEffect(() => { + if (!currentUserId || currentUserId === 0) { + console.warn("currentUserId 无效,跳过加载:", currentUserId); + return; + } + + let isCancelled = false; + const requestId = ++loadRequestRef.current; + const initializeSessions = async () => { - if (!currentUserId || currentUserId === 0) { - console.warn("currentUserId 无效,跳过加载:", currentUserId); - return; - } - - // 如果已经加载过一次,只从本地数据库读取,不请求接口 - if (hasLoadedOnce) { - console.log("已加载过,只从本地数据库读取"); - setLoading(true); // 显示骨架屏 - - try { - const cachedSessions = - await MessageManager.getUserSessions(currentUserId); - console.log("从本地加载会话数:", cachedSessions.length); - - // 如果本地数据为空,重置 hasLoadedOnce 并重新加载 - if (cachedSessions.length === 0) { - console.warn("本地数据为空,重置加载状态并重新加载"); - setHasLoadedOnce(false); - // 不 return,继续执行下面的首次加载逻辑 - } else { - setSessions(cachedSessions); - setLoading(false); // 数据加载完成,关闭骨架屏 - return; - } - } catch (error) { - console.error("从本地加载会话列表失败:", error); - setLoading(false); - return; - } - } - - console.log("首次加载,开始初始化..."); setLoading(true); try { - // 1. 优先从本地数据库加载 const cachedSessions = await MessageManager.getUserSessions(currentUserId); - console.log("本地缓存会话数:", cachedSessions.length); + if (isCancelled || loadRequestRef.current !== requestId) { + return; + } if (cachedSessions.length > 0) { - // 有缓存数据,立即显示 - console.log("有缓存数据,立即显示"); - setSessions(cachedSessions); - setLoading(false); + setSessionState(cachedSessions); + } - // 2. 后台静默同步(不显示同步提示) - console.log("后台静默同步中..."); + const needsFullSync = cachedSessions.length === 0 || !hasLoadedOnce; + + if (needsFullSync) { await syncWithServer(); - setHasLoadedOnce(true); // 标记已加载过 - console.log("同步完成"); + if (isCancelled || loadRequestRef.current !== requestId) { + return; + } + setHasLoadedOnce(true); } else { - // 无缓存,直接API加载 - console.log("无缓存,从服务器加载..."); - await syncWithServer(); - const newSessions = - await MessageManager.getUserSessions(currentUserId); - console.log("从服务器加载会话数:", newSessions.length); - setSessions(newSessions); - setLoading(false); - setHasLoadedOnce(true); // 标记已加载过 + syncWithServer().catch(error => { + console.error("后台同步失败:", error); + }); } } catch (error) { - console.error("初始化会话列表失败:", error); - setLoading(false); + if (!isCancelled) { + console.error("初始化会话列表失败:", error); + } + } finally { + if (!isCancelled && loadRequestRef.current === requestId) { + setLoading(false); + } } }; initializeSessions(); + + return () => { + isCancelled = true; + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentUserId]); - // 监听refreshTrigger,重新查询数据库 + // 订阅数据库变更,自动更新Store useEffect(() => { - const refreshSessions = async () => { - if (!currentUserId || refreshTrigger === 0) return; + if (!currentUserId) { + return; + } - try { - const updatedSessions = - await MessageManager.getUserSessions(currentUserId); - setSessions(updatedSessions); - } catch (error) { - console.error("刷新会话列表失败:", error); - } - }; + const unsubscribe = MessageManager.onSessionsUpdate( + ({ userId: ownerId, sessions: updatedSessions }) => { + if (ownerId !== currentUserId) return; + setSessionState(updatedSessions); + }, + ); - refreshSessions(); - }, [refreshTrigger, currentUserId]); + return unsubscribe; + }, [currentUserId, setSessionState]); // 根据客服和搜索关键词筛选会话 useEffect(() => { @@ -689,8 +670,7 @@ const MessageList: React.FC = () => { } } - // 触发静默刷新:通知组件从数据库重新查询 - triggerRefresh(); + // MessageManager 的回调会自动把最新数据发给 Store }; window.addEventListener( @@ -718,7 +698,7 @@ const MessageList: React.FC = () => { // 标记为已读(不更新时间和排序) if (session.config.unreadCount > 0) { // 立即更新UI(只更新未读数量) - setSessions(prev => + setSessionState(prev => prev.map(s => s.id === session.id ? { ...s, config: { ...s.config, unreadCount: 0 } } diff --git a/Touchkebao/src/store/module/weChat/message.data.ts b/Touchkebao/src/store/module/weChat/message.data.ts index 0925bbfd4..e59e75061 100644 --- a/Touchkebao/src/store/module/weChat/message.data.ts +++ b/Touchkebao/src/store/module/weChat/message.data.ts @@ -1,3 +1,5 @@ +import { ChatSession } from "@/utils/db"; + export interface Message { id: number; wechatId: string; @@ -26,13 +28,15 @@ export interface Message { } //Store State - 会话列表状态管理(不存储数据,只管理状态) +export type SessionsUpdater = + | ChatSession[] + | ((previous: ChatSession[]) => ChatSession[]); + export interface MessageState { //加载状态 loading: boolean; //后台同步状态 refreshing: boolean; - //刷新触发器(用于通知组件重新查询数据库) - refreshTrigger: number; //最后刷新时间 lastRefreshTime: string | null; //是否已经加载过一次(避免重复请求) @@ -42,8 +46,6 @@ export interface MessageState { setLoading: (loading: boolean) => void; //设置同步状态 setRefreshing: (refreshing: boolean) => void; - //触发刷新(通知组件重新查询) - triggerRefresh: () => void; //设置已加载标识 setHasLoadedOnce: (loaded: boolean) => void; //重置加载状态(用于登出或切换用户) @@ -60,4 +62,16 @@ export interface MessageState { updateMessageStatus: (messageId: number, status: string) => void; //更新当前选中的消息(废弃,保留兼容) updateCurrentMessage: (message: Message) => void; + + // ==================== 新的会话数据接口 ==================== + // 当前会话列表 + sessions: ChatSession[]; + // 设置或更新会话列表(支持回调写法) + setSessions: (updater: SessionsUpdater) => void; + // 新增或替换某个会话 + upsertSession: (session: ChatSession) => void; + // 按 ID 和类型移除会话 + removeSessionById: (sessionId: number, type: ChatSession["type"]) => void; + // 清空所有会话(登出/切账号使用) + clearSessions: () => void; } diff --git a/Touchkebao/src/store/module/weChat/message.ts b/Touchkebao/src/store/module/weChat/message.ts index 51503f193..c1b2d12c4 100644 --- a/Touchkebao/src/store/module/weChat/message.ts +++ b/Touchkebao/src/store/module/weChat/message.ts @@ -1,6 +1,43 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { Message, MessageState } from "./message.data"; +import { ChatSession } from "@/utils/db"; +import { Message, MessageState, SessionsUpdater } from "./message.data"; + +const computeSortKey = (session: ChatSession) => { + const isTop = session.config?.top ? 1 : 0; + const timestamp = new Date(session.lastUpdateTime || new Date()).getTime(); + const displayName = ( + session.conRemark || + session.nickname || + (session as any).wechatId || + "" + ).toLowerCase(); + + return `${isTop}|${timestamp}|${displayName}`; +}; + +const normalizeSessions = (sessions: ChatSession[]) => { + if (!Array.isArray(sessions)) { + return []; + } + + return [...sessions] + .map(session => ({ + ...session, + sortKey: computeSortKey(session), + })) + .sort((a, b) => b.sortKey.localeCompare(a.sortKey)); +}; + +const resolveUpdater = ( + updater: SessionsUpdater, + previous: ChatSession[], +): ChatSession[] => { + if (typeof updater === "function") { + return updater(previous); + } + return updater; +}; /** * 会话列表状态管理Store @@ -13,24 +50,18 @@ export const useMessageStore = create()( // ==================== 新增状态管理 ==================== loading: false, refreshing: false, - refreshTrigger: 0, lastRefreshTime: null, hasLoadedOnce: false, setLoading: (loading: boolean) => set({ loading }), setRefreshing: (refreshing: boolean) => set({ refreshing }), - triggerRefresh: () => - set({ - refreshTrigger: get().refreshTrigger + 1, - lastRefreshTime: new Date().toISOString(), - }), setHasLoadedOnce: (loaded: boolean) => set({ hasLoadedOnce: loaded }), resetLoadState: () => set({ hasLoadedOnce: false, loading: false, refreshing: false, - refreshTrigger: 0, + sessions: [], }), // ==================== 保留原有接口(向后兼容) ==================== @@ -45,6 +76,45 @@ export const useMessageStore = create()( message.id === messageId ? { ...message, status } : message, ), }), + + // ==================== 会话数据接口 ==================== + sessions: [], + setSessions: (updater: SessionsUpdater) => + set(state => ({ + sessions: normalizeSessions(resolveUpdater(updater, state.sessions)), + lastRefreshTime: new Date().toISOString(), + })), + upsertSession: (session: ChatSession) => + set(state => { + const next = [...state.sessions]; + const index = next.findIndex( + s => s.id === session.id && s.type === session.type, + ); + + if (index > -1) { + next[index] = session; + } else { + next.push(session); + } + return { + sessions: normalizeSessions(next), + lastRefreshTime: new Date().toISOString(), + }; + }), + removeSessionById: (sessionId: number, type: ChatSession["type"]) => + set(state => ({ + sessions: normalizeSessions( + state.sessions.filter( + s => !(s.id === sessionId && s.type === type), + ), + ), + lastRefreshTime: new Date().toISOString(), + })), + clearSessions: () => + set({ + sessions: [], + lastRefreshTime: new Date().toISOString(), + }), }), { name: "message-storage", @@ -105,11 +175,6 @@ export const setLoading = (loading: boolean) => export const setRefreshing = (refreshing: boolean) => useMessageStore.getState().setRefreshing(refreshing); -/** - * 触发刷新(通知组件重新查询数据库) - */ -export const triggerRefresh = () => useMessageStore.getState().triggerRefresh(); - /** * 设置已加载标识 * @param loaded 是否已加载 diff --git a/Touchkebao/src/store/module/weChat/weChat.data.ts b/Touchkebao/src/store/module/weChat/weChat.data.ts index d23dfdf86..dc9aa163e 100644 --- a/Touchkebao/src/store/module/weChat/weChat.data.ts +++ b/Touchkebao/src/store/module/weChat/weChat.data.ts @@ -40,6 +40,12 @@ export interface WeChatState { // ==================== 聊天消息管理 ==================== /** 当前聊天的消息列表 */ currentMessages: ChatRecord[]; + /** 当前聊天记录分页页码 */ + currentMessagesPage: number; + /** 单页消息条数 */ + currentMessagesPageSize: number; + /** 是否还有更多历史消息 */ + currentMessagesHasMore: boolean; /** 添加新消息 */ addMessage: (message: ChatRecord) => void; /** 更新指定消息 */ @@ -83,7 +89,7 @@ export interface WeChatState { // ==================== 消息加载方法 ==================== /** 加载聊天消息 */ - loadChatMessages: (Init: boolean, To?: number) => Promise; + loadChatMessages: (Init: boolean, pageOverride?: number) => Promise; /** 搜索消息 */ SearchMessage: (params: { From: number; diff --git a/Touchkebao/src/store/module/weChat/weChat.ts b/Touchkebao/src/store/module/weChat/weChat.ts index 889d528b8..89a001a95 100644 --- a/Touchkebao/src/store/module/weChat/weChat.ts +++ b/Touchkebao/src/store/module/weChat/weChat.ts @@ -28,6 +28,7 @@ let pendingMessages: ChatRecord[] = []; // 待处理的消息队列 let currentAiGenerationId: string | null = null; // 当前AI生成的唯一ID const AI_REQUEST_DELAY = 3000; // 3秒延迟 const FILE_MESSAGE_TYPE = "file"; +const DEFAULT_MESSAGE_PAGE_SIZE = 20; type FileMessagePayload = { type?: string; @@ -120,6 +121,108 @@ const isFileLikeMessage = (msg: ChatRecord): boolean => { return false; }; +const normalizeMessages = (source: any): ChatRecord[] => { + if (Array.isArray(source)) { + return source; + } + if (Array.isArray(source?.list)) { + return source.list; + } + return []; +}; + +const parseTimeValue = (value: unknown): number => { + if (value === null || value === undefined) { + return 0; + } + if (typeof value === "number") { + return value; + } + if (typeof value === "string") { + const numeric = Number(value); + if (!Number.isNaN(numeric)) { + return numeric; + } + const parsed = Date.parse(value); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + if (value instanceof Date) { + return value.getTime(); + } + return 0; +}; + +const getMessageTimestamp = (msg: ChatRecord): number => { + const candidates = [ + (msg as any)?.wechatTime, + (msg as any)?.createTime, + (msg as any)?.msgTime, + (msg as any)?.timestamp, + (msg as any)?.time, + ]; + + for (const candidate of candidates) { + const parsed = parseTimeValue(candidate); + if (parsed) { + return parsed; + } + } + + return typeof msg.id === "number" ? msg.id : 0; +}; + +const sortMessagesByTime = (messages: ChatRecord[]): ChatRecord[] => { + return [...messages].sort( + (a, b) => getMessageTimestamp(a) - getMessageTimestamp(b), + ); +}; + +const resolvePaginationState = ( + source: any, + requestedPage: number, + requestedLimit: number, + listLength: number, +) => { + const page = + typeof source?.page === "number" + ? source.page + : typeof source?.current === "number" + ? source.current + : requestedPage; + + const limit = + typeof source?.limit === "number" + ? source.limit + : typeof source?.pageSize === "number" + ? source.pageSize + : requestedLimit; + + let hasMore: boolean; + if (typeof source?.hasNext === "boolean") { + hasMore = source.hasNext; + } else if (typeof source?.hasNextPage === "boolean") { + hasMore = source.hasNextPage; + } else if (typeof source?.pages === "number") { + hasMore = page < source.pages; + } else if (typeof source?.total === "number" && limit > 0) { + hasMore = page * limit < source.total; + } else { + hasMore = listLength >= limit && listLength > 0; + } + + if (listLength === 0) { + hasMore = false; + } + + return { + page, + limit: limit || requestedLimit || DEFAULT_MESSAGE_PAGE_SIZE, + hasMore, + }; +}; + const normalizeFilePayload = ( payload: FileMessagePayload | null | undefined, msg: ChatRecord, @@ -348,6 +451,10 @@ export const useWeChatStore = create()( currentContract: null, /** 当前聊天的消息列表 */ currentMessages: [], + /** 当前消息分页信息 */ + currentMessagesPage: 1, + currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, + currentMessagesHasMore: true, // ==================== 聊天消息管理方法 ==================== /** 添加新消息到当前聊天 */ @@ -429,7 +536,13 @@ export const useWeChatStore = create()( aiRequestTimer = null; } pendingMessages = []; - set({ currentContract: null, currentMessages: [] }); + set({ + currentContract: null, + currentMessages: [], + currentMessagesPage: 1, + currentMessagesHasMore: true, + currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, + }); }, /** 设置当前联系人并加载相关数据 */ setCurrentContact: (contract: ContractData | weChatGroup) => { @@ -443,7 +556,13 @@ export const useWeChatStore = create()( const state = useWeChatStore.getState(); // 切换联系人时清空当前消息,等待重新加载 - set({ currentMessages: [], isLoadingAiChat: false }); + set({ + currentMessages: [], + currentMessagesPage: 1, + currentMessagesHasMore: true, + currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, + isLoadingAiChat: false, + }); const params: any = {}; @@ -468,62 +587,91 @@ export const useWeChatStore = create()( id: contract.id, config: { chat: true }, }); - state.loadChatMessages(true, 4704624000000); + state.loadChatMessages(true); }, // ==================== 消息加载方法 ==================== /** 加载聊天消息 */ - loadChatMessages: async (Init: boolean, To?: number) => { + loadChatMessages: async (Init: boolean, pageOverride?: number) => { const state = useWeChatStore.getState(); const contact = state.currentContract; - set({ messagesLoading: true }); - set({ isLoadingData: Init }); + + if (!contact) { + return; + } + + if (!Init && !state.currentMessagesHasMore) { + return; + } + + const nextPage = Init + ? 1 + : (pageOverride ?? state.currentMessagesPage + 1); + const limit = + state.currentMessagesPageSize || DEFAULT_MESSAGE_PAGE_SIZE; + + if (state.messagesLoading && !Init) { + return; + } + + set({ + messagesLoading: true, + isLoadingData: Init, + }); + try { const params: any = { wechatAccountId: contact.wechatAccountId, - From: 1, - To: To || +new Date(), - Count: 20, - olderData: true, + page: nextPage, + limit, }; - if ("chatroomId" in contact && contact.chatroomId) { - // 群聊消息加载 + const isGroup = + "chatroomId" in contact && Boolean(contact.chatroomId); + + if (isGroup) { params.wechatChatroomId = contact.id; - const messages = await getChatroomMessages(params); - const currentGroupMembers = await getGroupMembers({ + } else { + params.wechatFriendId = contact.id; + } + + const response = isGroup + ? await getChatroomMessages(params) + : await getChatMessages(params); + + const normalizedMessages = normalizeMessages(response); + const sortedMessages = sortMessagesByTime(normalizedMessages); + const paginationMeta = resolvePaginationState( + response, + nextPage, + limit, + sortedMessages.length, + ); + + let nextGroupMembers = state.currentGroupMembers; + if (Init && isGroup) { + nextGroupMembers = await getGroupMembers({ id: contact.id, }); - if (Init) { - set({ currentMessages: messages || [], currentGroupMembers }); - } else { - set({ - currentMessages: [ - ...(messages.list || []), - ...state.currentMessages, - ], - }); - } - } else { - // 私聊消息加载 - params.wechatFriendId = contact.id; - const messages = await getChatMessages(params); - if (Init) { - set({ currentMessages: messages || [] }); - } else { - set({ - currentMessages: [ - ...(messages.list || []), - ...state.currentMessages, - ], - }); - } } - set({ messagesLoading: false }); + + set(current => ({ + currentMessages: Init + ? sortedMessages + : [...sortedMessages, ...current.currentMessages], + currentGroupMembers: + Init && isGroup ? nextGroupMembers : current.currentGroupMembers, + currentMessagesPage: paginationMeta.page, + currentMessagesPageSize: paginationMeta.limit, + currentMessagesHasMore: paginationMeta.hasMore, + })); } catch (error) { console.error("获取聊天消息失败:", error); } finally { - set({ messagesLoading: false }); + set({ + messagesLoading: false, + isLoadingData: false, + }); } }, @@ -546,11 +694,11 @@ export const useWeChatStore = create()( try { const params: any = { wechatAccountId: contact.wechatAccountId, + keyword, From, To, - keyword, - Count, - olderData: true, + page: 1, + limit: Count, }; if ("chatroomId" in contact && contact.chatroomId) { @@ -560,12 +708,23 @@ export const useWeChatStore = create()( const currentGroupMembers = await getGroupMembers({ id: contact.id, }); - set({ currentMessages: messages || [], currentGroupMembers }); + set({ + currentMessages: sortMessagesByTime(normalizeMessages(messages)), + currentGroupMembers, + currentMessagesPage: 1, + currentMessagesHasMore: false, + currentMessagesPageSize: Count || state.currentMessagesPageSize, + }); } else { // 私聊消息搜索 params.wechatFriendId = contact.id; const messages = await getChatMessages(params); - set({ currentMessages: messages || [] }); + set({ + currentMessages: sortMessagesByTime(normalizeMessages(messages)), + currentMessagesPage: 1, + currentMessagesHasMore: false, + currentMessagesPageSize: Count || state.currentMessagesPageSize, + }); } set({ messagesLoading: false }); } catch (error) { @@ -831,6 +990,9 @@ export const useWeChatStore = create()( set({ currentContract: null, currentMessages: [], + currentMessagesPage: 1, + currentMessagesHasMore: true, + currentMessagesPageSize: DEFAULT_MESSAGE_PAGE_SIZE, messagesLoading: false, }); }, diff --git a/Touchkebao/src/utils/dbAction/message.ts b/Touchkebao/src/utils/dbAction/message.ts index d42289bfe..e34041b98 100644 --- a/Touchkebao/src/utils/dbAction/message.ts +++ b/Touchkebao/src/utils/dbAction/message.ts @@ -25,8 +25,15 @@ const serializeExtendFields = (value: any) => { return "{}"; }; +interface SessionUpdatePayload { + userId: number; + sessions: ChatSession[]; +} + export class MessageManager { - private static updateCallbacks = new Set<(sessions: ChatSession[]) => void>(); + private static updateCallbacks = new Set< + (payload: SessionUpdatePayload) => void + >(); // ==================== 回调管理 ==================== @@ -35,9 +42,11 @@ export class MessageManager { * @param callback 回调函数 * @returns 取消注册的函数 */ - static onSessionsUpdate(callback: (sessions: ChatSession[]) => void) { + static onSessionsUpdate(callback: (payload: SessionUpdatePayload) => void) { this.updateCallbacks.add(callback); - return () => this.updateCallbacks.delete(callback); + return () => { + this.updateCallbacks.delete(callback); + }; } /** @@ -49,7 +58,7 @@ export class MessageManager { const sessions = await this.getUserSessions(userId); this.updateCallbacks.forEach(callback => { try { - callback(sessions); + callback({ userId, sessions }); } catch (error) { console.error("会话更新回调执行失败:", error); } From 1582e22756a478ca6135009478b31d01625cc855 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: Fri, 14 Nov 2025 18:54:04 +0800 Subject: [PATCH 11/15] =?UTF-8?q?=E4=BC=98=E5=8C=96MessageRecord=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E7=9A=84=E6=9D=A1=E4=BB=B6=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BD=BF=E7=94=A8=E5=8F=8C=E9=87=8D?= =?UTF-8?q?=E5=90=A6=E5=AE=9A=E7=A1=AE=E4=BF=9DisOwn=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=9A=84=E6=AD=A3=E7=A1=AE=E5=88=A4=E6=96=AD=E3=80=82=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=A4=9A=E4=BD=99=E7=9A=84=E6=8D=A2=E8=A1=8C=E4=BB=A5?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ChatWindow/components/MessageRecord/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx index 29a014394..4bfe7c513 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx @@ -648,7 +648,7 @@ const MessageRecord: React.FC = ({ contract }) => {
)} - {isOwn && ( + {!!isOwn && ( <> {/* Checkbox 显示控制 */} {showCheckbox && ( @@ -659,7 +659,6 @@ const MessageRecord: React.FC = ({ contract }) => { />
)} - Date: Tue, 18 Nov 2025 11:57:54 +0800 Subject: [PATCH 12/15] =?UTF-8?q?feat=EF=BC=9A=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=88=90=E5=91=98=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TwoColumnSelection/TwoColumnSelection.tsx | 34 ++- .../components/ProfileModules/index.tsx | 198 +++++++++++++----- 2 files changed, 173 insertions(+), 59 deletions(-) diff --git a/Touchkebao/src/components/TwoColumnSelection/TwoColumnSelection.tsx b/Touchkebao/src/components/TwoColumnSelection/TwoColumnSelection.tsx index e5400bf60..6ba962742 100644 --- a/Touchkebao/src/components/TwoColumnSelection/TwoColumnSelection.tsx +++ b/Touchkebao/src/components/TwoColumnSelection/TwoColumnSelection.tsx @@ -17,6 +17,7 @@ const FriendListItem = memo<{ onClick={() => onSelect(friend)} > +     {friend.nickname?.charAt(0)} @@ -41,6 +42,9 @@ interface TwoColumnSelectionProps { deviceIds?: number[]; enableDeviceFilter?: boolean; dataSource?: FriendSelectionItem[]; + onLoadMore?: () => void; // 加载更多回调 + hasMore?: boolean; // 是否有更多数据 + loading?: boolean; // 是否正在加载 } const TwoColumnSelection: React.FC = ({ @@ -51,13 +55,16 @@ const TwoColumnSelection: React.FC = ({ deviceIds = [], enableDeviceFilter = true, dataSource, + onLoadMore, + hasMore = false, + loading = false, }) => { const [rawFriends, setRawFriends] = useState([]); const [selectedFriends, setSelectedFriends] = useState( [], ); const [searchQuery, setSearchQuery] = useState(""); - const [loading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); @@ -81,10 +88,10 @@ const TwoColumnSelection: React.FC = ({ const [displayPage, setDisplayPage] = useState(1); const friends = useMemo(() => { - const startIndex = 0; - const endIndex = displayPage * ITEMS_PER_PAGE; - return filteredFriends.slice(startIndex, endIndex); - }, [filteredFriends, displayPage]); + // 直接使用完整的过滤列表,不再进行本地分页 + // 因为我们已经在外部进行了分页加载 + return filteredFriends; + }, [filteredFriends]); const hasMoreFriends = filteredFriends.length > friends.length; @@ -100,7 +107,7 @@ const TwoColumnSelection: React.FC = ({ // 获取好友列表 const fetchFriends = useCallback( async (page: number, keyword: string = "") => { - setLoading(true); + setIsLoading(true); try { const params: any = { page, @@ -128,7 +135,7 @@ const TwoColumnSelection: React.FC = ({ console.error("获取好友列表失败:", error); message.error("获取好友列表失败"); } finally { - setLoading(false); + setIsLoading(false); } }, [deviceIds, enableDeviceFilter], @@ -148,7 +155,7 @@ const TwoColumnSelection: React.FC = ({ if (visible) { setSearchQuery(""); setSelectedFriends([]); - setLoading(false); + setIsLoading(false); } }, [visible]); @@ -257,7 +264,7 @@ const TwoColumnSelection: React.FC = ({
- {loading ? ( + {isLoading && !loading ? (
加载中...
) : friends.length > 0 ? ( // 使用 React.memo 优化列表项渲染 @@ -280,9 +287,14 @@ const TwoColumnSelection: React.FC = ({
)} - {hasMoreFriends && ( + {/* 使用外部传入的加载更多 */} + {hasMore && (
-
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index b71db1e0c..150e9ceb0 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -25,7 +25,7 @@ import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { useCustomerStore } from "@/store/module/weChat/customer"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; import { useWeChatStore } from "@/store/module/weChat/weChat"; -import { useContactStore } from "@/store/module/weChat/contacts"; +import { contactUnifiedService } from "@/utils/db"; import { generateAiText } from "@/api/ai"; import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection"; import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection"; @@ -216,7 +216,7 @@ const Person: React.FC = ({ contract }) => { return matchedCustomer || null; }, [customerList, contract.wechatAccountId]); - const { getContactsByCustomer } = useContactStore(); + // 不再需要从useContactStore获取getContactsByCustomer const { sendCommand } = useWebSocketStore(); @@ -516,6 +516,125 @@ const Person: React.FC = ({ contract }) => { bio: contract.bio || contract.signature || "-", }; + // 分页状态 + const [currentContactPage, setCurrentContactPage] = useState(1); + const [contactPageSize] = useState(10); + const [isLoadingContacts, setIsLoadingContacts] = useState(false); + + // 从数据库获取联系人数据的通用函数 + const fetchContacts = async (page = 1) => { + try { + const { databaseManager, initializeDatabaseFromPersistedUser } = + await import("@/utils/db"); + + // 检查数据库初始化状态 + if (!databaseManager.isInitialized()) { + await initializeDatabaseFromPersistedUser(); + } + + // 获取当前用户ID + const userId = kfSelectedUser?.userId || 0; + const storeUserId = databaseManager.getCurrentUserId(); + const effectiveUserId = storeUserId || userId; + + if (!effectiveUserId) { + messageApi.error("无法获取用户信息,请尝试重新登录"); + return []; + } + + // 查询联系人数据 + const allContacts = await contactUnifiedService.findWhereMultiple([ + { field: "userId", operator: "equals", value: effectiveUserId }, + { + field: "wechatAccountId", + operator: "equals", + value: contract.wechatAccountId, + }, + { field: "type", operator: "equals", value: "friend" }, + ]); + + // 手动分页 + const startIndex = (page - 1) * contactPageSize; + const endIndex = startIndex + contactPageSize; + return allContacts.slice(startIndex, endIndex); + } catch (error) { + console.error("获取联系人数据失败:", error); + messageApi.error("获取联系人数据失败"); + return []; + } + }; + + const addMember = async () => { + try { + setIsLoadingContacts(true); + const pagedContacts = await fetchContacts(currentContactPage); + // 转换为选择器需要的数据格式 + const friendSelectionData = pagedContacts.map(item => ({ + id: item.id || item.serverId, + wechatId: item.wechatId, + nickname: item.nickname, + avatar: item.avatar || "", + conRemark: item.conRemark, + name: item.conRemark || item.nickname, // 用于搜索显示 + })); + + setContractList(friendSelectionData); + setIsFriendSelectionVisible(true); + + // 如果没有联系人数据,显示提示 + if (friendSelectionData.length === 0) { + messageApi.info("未找到可添加的联系人,可能需要先同步联系人数据"); + } + } catch (error) { + console.error("获取联系人列表失败:", error); + messageApi.error("获取联系人列表失败"); + } finally { + setIsLoadingContacts(false); + } + }; + + // 加载更多联系人 + const loadMoreContacts = async () => { + if (isLoadingContacts) return; + try { + setIsLoadingContacts(true); + const nextPage = currentContactPage + 1; + setCurrentContactPage(nextPage); + // 使用通用函数获取下一页联系人数据 + const pagedContacts = await fetchContacts(nextPage); + // 转换数据格式 + const newFriendSelectionData = pagedContacts.map(item => ({ + id: item.id || item.serverId, + wechatId: item.wechatId, + nickname: item.nickname, + avatar: item.avatar || "", + conRemark: item.conRemark, + name: item.conRemark || item.nickname, + })); + + // 更新列表并去重 + setContractList(prev => { + const newList = [...prev, ...newFriendSelectionData]; + // 确保列表中没有重复项 + const uniqueMap = new Map(); + const uniqueList = newList.filter(item => { + if (uniqueMap.has(item.id)) { + return false; + } + uniqueMap.set(item.id, true); + return true; + }); + return uniqueList; + }); + + messageApi.success(`已加载${pagedContacts.length}条联系人数据`); + } catch (error) { + console.error("加载更多联系人失败:", error); + messageApi.error("加载更多联系人失败"); + } finally { + setIsLoadingContacts(false); + } + }; return ( <> {contextHolder} @@ -774,27 +893,25 @@ const Person: React.FC = ({ contract }) => {
{/* 渲染所有可用标签,选中的排在前面 */} - {[...new Set([...selectedTags, ...allAvailableTags])].map( - (tag, index) => { - const isSelected = selectedTags.includes(tag); - return ( - handleTagToggle(tag)} - > - {tag} - - ); - }, - )} + {[...new Set([...selectedTags, ...allAvailableTags])].map(tag => { + const isSelected = selectedTags.includes(tag); + return ( + handleTagToggle(tag)} + > + {tag} + + ); + })} {/* 新增标签区域 */} {isAddingTag ? ( @@ -917,29 +1034,7 @@ const Person: React.FC = ({ contract }) => { >
From 6eeabe420388ad19464680ba99b0bd821c2acd08 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: Tue, 18 Nov 2025 18:02:30 +0800 Subject: [PATCH 14/15] =?UTF-8?q?=E9=87=8D=E6=9E=84ProfileCard=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=9A=84DetailVal?= =?UTF-8?q?ue=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=B5=84=E6=96=99=E5=92=8C=E7=BE=A4=E7=BB=84=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E7=BC=96=E8=BE=91=E9=80=BB=E8=BE=91=E3=80=82?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E4=BB=A5?= =?UTF-8?q?=E5=A4=84=E7=90=86contract=E7=9A=84=E5=8F=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E6=95=B0=E6=8D=AE=E4=B8=80=E8=87=B4=E6=80=A7?= =?UTF-8?q?=E3=80=82=E6=9B=B4=E6=96=B0=E6=A0=B7=E5=BC=8F=E4=BB=A5=E6=94=B9?= =?UTF-8?q?=E5=96=84=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E9=80=BB=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProfileModules/Person.module.scss | 6 + .../components/ProfileModules/api.ts | 88 +++ .../ProfileModules/components/detailValue.tsx | 291 ++++++-- .../components/ProfileModules/index.tsx | 695 ++++++++++-------- .../components/ProfileCard/index.tsx | 16 +- 5 files changed, 757 insertions(+), 339 deletions(-) create mode 100644 Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss index 0a061cc43..89005006b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss @@ -197,6 +197,12 @@ } } +.footerActions { + display: flex; + justify-content: flex-end; + margin-top: 16px; +} + // 响应式设计 @media (max-width: 768px) { .profileSider { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts new file mode 100644 index 000000000..2f2ea0dce --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts @@ -0,0 +1,88 @@ +import request from "@/api/request"; +// 更新好友信息 +export interface UpdateFriendInfoParams { + id: number; + phone: string; + company: string; + name: string; + position: string; + email: string; + address: string; + qq: string; + remark: string; +} +export function updateFriendInfo(params: UpdateFriendInfoParams): Promise { + return request("/v1/kefu/wechatFriend/updateInfo", params, "POST"); +} + +// 更新本地数据库中的好友信息 +export interface UpdateLocalDBParams { + wechatFriendId: number; + extendFields: string; + updateConversation?: boolean; // 是否同时更新会话列表 +} + +export function updateLocalDBFriendInfo( + params: UpdateLocalDBParams, +): Promise { + return request("/v1/kefu/wechatFriend/updateLocalDB", params, "POST"); +} +// 获取好友信息 +export interface GetFriendInfoParams { + id: number; +} + +export interface FriendDetailResponse { + detail: { + id: number; + wechatAccountId: number; + alias: string; + wechatId: string; + conRemark: string; + nickname: string; + pyInitial: string; + quanPin: string; + avatar: string; + gender: number; + region: string; + addFrom: number; + labels: any[]; + siteLabels: string[]; + signature: string; + isDeleted: number; + isPassed: number; + deleteTime: number; + accountId: number; + extendFields: string; + accountUserName: string; + accountRealName: string; + accountNickname: string; + ownerAlias: string; + ownerWechatId: string; + ownerNickname: string; + ownerAvatar: string; + phone: string; + thirdParty: string; + groupId: number; + passTime: string; + additionalPicture: string; + desc: string; + country: string; + privince: string; + city: string; + createTime: string; + updateTime: string; + R: string; + F: string; + M: string; + realName: null | string; + company: null | string; + position: null | string; + aiType: number; + }; +} +export function getFriendInfo( + params: GetFriendInfoParams, +): Promise { + return request("/v1/kefu/wechatFriend/detail", params, "GET"); +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx index b63770584..4966a8631 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx @@ -1,5 +1,8 @@ -import React, { useCallback } from "react"; -import { Button, Input } from "antd"; +import React, { useCallback, useState, useEffect } from "react"; +import { Input, message } from "antd"; +import { Button } from "antd-mobile"; +import { EditOutlined } from "@ant-design/icons"; +import { updateFriendInfo, UpdateFriendInfoParams } from "../api"; import styles from "../Person.module.scss"; @@ -8,91 +11,295 @@ export interface DetailValueField { key: string; ifEdit?: boolean; placeholder?: string; + type?: "text" | "textarea"; + editable?: boolean; } export interface DetailValueProps { fields: DetailValueField[]; value?: Record; onChange?: (next: Record) => void; - onSubmit?: (next: Record) => void; + onSubmit?: (next: Record, changedKeys: string[]) => void; submitText?: string; submitting?: boolean; renderFooter?: React.ReactNode; + saveHandler?: ( + values: Record, + changedKeys: string[], + ) => Promise; + onSaveSuccess?: ( + values: Record, + changedKeys: string[], + ) => void; + isGroup?: boolean; } const DetailValue: React.FC = ({ fields, - value, + value = {}, onChange, onSubmit, submitText = "保存", submitting = false, renderFooter, + saveHandler, + onSaveSuccess, + isGroup = false, }) => { + const [messageApi, contextHolder] = message.useMessage(); + const [editingFields, setEditingFields] = useState>( + {}, + ); + const [fieldValues, setFieldValues] = useState>(value); + + const [originalValues, setOriginalValues] = + useState>(value); + const [changedKeys, setChangedKeys] = useState([]); + + // 当外部value变化时,更新内部状态 + useEffect(() => { + setFieldValues(value); + setOriginalValues(value); + setChangedKeys([]); + // 重置所有编辑状态 + const newEditingFields: Record = {}; + fields.forEach(field => { + newEditingFields[field.key] = false; + }); + setEditingFields(newEditingFields); + }, [value, fields]); + const handleFieldChange = useCallback( (fieldKey: string, nextVal: string) => { - const baseValue = value ?? {}; - const nextValue = { - ...baseValue, + setFieldValues(prev => ({ + ...prev, [fieldKey]: nextVal, - }; - onChange?.(nextValue); + })); + + // 检查值是否发生变化,更新changedKeys + if (nextVal !== originalValues[fieldKey]) { + if (!changedKeys.includes(fieldKey)) { + setChangedKeys(prev => [...prev, fieldKey]); + } + } else { + // 如果值恢复到原始值,从changedKeys中移除 + setChangedKeys(prev => prev.filter(key => key !== fieldKey)); + } + + // 调用外部onChange,但不触发自动保存 + if (onChange) { + onChange({ + ...fieldValues, + [fieldKey]: nextVal, + }); + } }, - [onChange, value], + [onChange, fieldValues, originalValues, changedKeys], ); - const handleSubmit = useCallback(() => { - onSubmit?.(value ?? {}); - }, [onSubmit, value]); + const handleEditField = useCallback((fieldKey: string) => { + setEditingFields(prev => ({ + ...prev, + [fieldKey]: true, + })); + }, []); - const formValue = value ?? {}; + const handleCancelEdit = useCallback( + (fieldKey: string) => { + // 恢复原始值 + setFieldValues(prev => ({ + ...prev, + [fieldKey]: originalValues[fieldKey] || "", + })); + + // 从changedKeys中移除 + setChangedKeys(prev => prev.filter(key => key !== fieldKey)); + + // 关闭编辑状态 + setEditingFields(prev => ({ + ...prev, + [fieldKey]: false, + })); + }, + [originalValues], + ); + + const handleSubmit = useCallback(async () => { + if (changedKeys.length === 0) { + messageApi.info("没有需要保存的更改"); + return; + } + + try { + if (isGroup) { + // 群组信息使用传入的saveHandler + if (saveHandler) { + await saveHandler(fieldValues, changedKeys); + } else { + onSubmit?.(fieldValues, changedKeys); + } + } else { + // 个人资料信息处理 + if (changedKeys.includes("conRemark")) { + // 微信备注是特例,使用WebSocket更新 + if (saveHandler) { + await saveHandler(fieldValues, changedKeys); + } else { + onSubmit?.(fieldValues, changedKeys); + } + } else { + // 其他个人资料信息使用updateFriendInfo API + const params: UpdateFriendInfoParams = { + id: Number(value.id) || 0, + phone: fieldValues.phone || "", + company: fieldValues.company || "", + name: fieldValues.name || "", + position: fieldValues.position || "", + email: fieldValues.email || "", + address: fieldValues.address || "", + qq: fieldValues.qq || "", + remark: fieldValues.remark || "", + }; + await updateFriendInfo(params); + } + } + + // 更新原始值 + setOriginalValues(fieldValues); + // 清空changedKeys + setChangedKeys([]); + // 关闭所有编辑状态 + const newEditingFields: Record = {}; + fields.forEach(field => { + newEditingFields[field.key] = false; + }); + setEditingFields(newEditingFields); + // 调用保存成功回调 + onSaveSuccess?.(fieldValues, changedKeys); + messageApi.success("保存成功"); + } catch (error) { + messageApi.error("保存失败"); + console.error("保存失败:", error); + } + }, [ + onSubmit, + saveHandler, + onSaveSuccess, + fieldValues, + changedKeys, + fields, + messageApi, + isGroup, + value.id, + ]); + + const isEditing = Object.values(editingFields).some(Boolean); return (
+ {contextHolder} {fields.map(field => { const disabled = field.ifEdit === false; - const fieldValue = formValue[field.key] ?? ""; + const fieldValue = fieldValues[field.key] ?? ""; + const isFieldEditing = editingFields[field.key]; + const InputComponent = + field.type === "textarea" ? Input.TextArea : Input; return (
{field.label}:
{disabled ? ( - {fieldValue || field.placeholder || "--"} + {fieldValue || field.placeholder || ""} + ) : isFieldEditing ? ( +
+ + handleFieldChange(field.key, event.target.value) + } + onPressEnter={undefined} + autoFocus + rows={field.type === "textarea" ? 4 : undefined} + /> +
+ + +
+
) : ( - - handleFieldChange(field.key, event.target.value) - } - onPressEnter={handleSubmit} - /> +
handleEditField(field.key)} + onMouseEnter={e => { + e.currentTarget.style.backgroundColor = "#f5f5f5"; + e.currentTarget.style.borderColor = "#d9d9d9"; + }} + onMouseLeave={e => { + e.currentTarget.style.backgroundColor = "transparent"; + e.currentTarget.style.borderColor = "transparent"; + }} + > + + {fieldValue || field.placeholder || ""} + + +
)}
); })} - {(onSubmit || renderFooter) && ( -
+ {(onSubmit || renderFooter) && !isEditing && changedKeys.length > 0 && ( +
{renderFooter} - {onSubmit && ( - - )} +
)}
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index 150e9ceb0..6f40e175e 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -9,11 +9,9 @@ import { message, Modal, } from "antd"; +// 不再需要导入updateFriendInfo,因为已经在detailValue.tsx中使用 import { - PhoneOutlined, UserOutlined, - TeamOutlined, - EnvironmentOutlined, CloseOutlined, EditOutlined, CheckOutlined, @@ -30,6 +28,12 @@ import { generateAiText } from "@/api/ai"; import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection"; import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; +import DetailValue from "./components/detailValue"; +import { + getFriendInfo, + FriendDetailResponse, + updateLocalDBFriendInfo, +} from "./api"; import styles from "./Person.module.scss"; interface PersonProps { contract: ContractData | weChatGroup; @@ -37,7 +41,6 @@ interface PersonProps { const Person: React.FC = ({ contract }) => { const [messageApi, contextHolder] = message.useMessage(); - const [isEditingRemark, setIsEditingRemark] = useState(false); const [remarkValue, setRemarkValue] = useState(contract.conRemark || ""); const [selectedTags, setSelectedTags] = useState( contract.labels || [], @@ -45,18 +48,18 @@ const Person: React.FC = ({ contract }) => { const [allAvailableTags, setAllAvailableTags] = useState([]); const [isAddingTag, setIsAddingTag] = useState(false); const [newTagValue, setNewTagValue] = useState(""); + const [friendDetail, setFriendDetail] = useState( + null, + ); // 判断是否为群聊 const isGroup = "chatroomId" in contract; // 群聊相关状态 - const [isEditingGroupName, setIsEditingGroupName] = useState(false); const [groupNameValue, setGroupNameValue] = useState(contract.name || ""); const [groupNoticeValue, setGroupNoticeValue] = useState( contract.notice || "", ); - const [isEditingSelfDisplayName, setIsEditingSelfDisplayName] = - useState(false); const [selfDisplayNameValue, setSelfDisplayNameValue] = useState( contract.selfDisplyName || "", ); @@ -241,76 +244,204 @@ const Person: React.FC = ({ contract }) => { fetchAvailableTags(); }, [kfSelectedUser, contract.labels]); + // 获取好友详细信息 - 静默请求,成功时更新数据,失败时不做任何处理 + const fetchFriendDetail = React.useCallback(async () => { + if (isGroup) return; // 群聊不需要获取好友详情 + + try { + // 静默请求,不显示加载状态 + const response = await getFriendInfo({ id: contract.id }); + // 请求成功时更新数据 + setFriendDetail(response); + + // 解析扩展字段 + try { + const extendFieldsObj = JSON.parse( + response.detail.extendFields || "{}", + ); + setExtendFields(extendFieldsObj); + } catch (e) { + console.error("Failed to parse extendFields:", e); + // 解析失败时不更新状态,保持原有数据 + } + } catch (err) { + // 请求失败时静默处理,只记录日志,不更新UI状态 + console.error("获取好友详情失败:", err); + } + }, [contract.id, isGroup]); + + // 当contract变化时在后台静默获取好友详情 + useEffect(() => { + if (!isGroup && contract.id) { + // 使用setTimeout将请求移至下一个事件循环,确保UI先渲染 + setTimeout(() => { + fetchFriendDetail(); + }, 0); + } + }, [contract.id, isGroup, fetchFriendDetail]); + // 当contract变化时更新各种值 useEffect(() => { setRemarkValue(contract.conRemark || ""); - setIsEditingRemark(false); setSelectedTags(contract.labels || []); + try { + // 确保extendFields是最新的值 + const extFieldsObj = + typeof contract.extendFields === "string" + ? JSON.parse(contract.extendFields || "{}") + : contract.extendFields || {}; + setExtendFields(extFieldsObj); + } catch (e) { + console.error("Failed to parse extendFields in useEffect:", e); + setExtendFields({}); + } if (isGroup) { setGroupNameValue(contract.name || ""); - setIsEditingGroupName(false); setGroupNoticeValue(contract.notice || ""); setSelfDisplayNameValue(contract.selfDisplyName || ""); - setIsEditingSelfDisplayName(false); } + + // 不再需要在这里触发获取好友详情,已在单独的useEffect中处理 }, [ contract.conRemark, contract.labels, contract.name, contract.notice, contract.selfDisplyName, + contract.extendFields, + contract.id, isGroup, + fetchFriendDetail, ]); // 处理备注保存 - const handleSaveRemark = () => { - if (isGroup) { - // 群聊备注修改 - sendCommand("CmdModifyGroupRemark", { - wechatAccountId: contract.wechatAccountId, - chatroomId: contract.chatroomId, - newRemark: remarkValue, - }); - } else { - // 好友备注修改 - sendCommand("CmdModifyFriendRemark", { - wechatAccountId: contract.wechatAccountId, - wechatFriendId: contract.id, - newRemark: remarkValue, - }); + const handleSaveRemark = async ( + values: Record, + changedKeys: string[], + ) => { + // 构建更新后的扩展字段 + const updatedExtendFields = { ...extendFields }; + + // 更新各个扩展字段 + const extendFieldKeys = [ + "phone", + "company", + "position", + "email", + "address", + "qq", + "remark", + ]; + extendFieldKeys.forEach(key => { + if (changedKeys.includes(key) && values[key] !== undefined) { + updatedExtendFields[key] = values[key]; + } + }); + + const extendFieldsStr = JSON.stringify(updatedExtendFields || {}); + + // 更新remarkValue + if (changedKeys.includes("conRemark")) { + setRemarkValue(values.conRemark); } - messageApi.success("备注保存成功"); - setIsEditingRemark(false); - // 更新contract对象中的备注(实际项目中应该通过props回调或状态管理) + // 更新所有扩展字段 + setExtendFields(updatedExtendFields); + + // 更新父组件中的contract副本,确保切换tab后数据不会丢失 + if (contract && typeof contract === "object") { + // 更新contract的extendFields字段 + contract.extendFields = extendFieldsStr; + + // 如果有备注变更,同时更新备注 + if (changedKeys.includes("conRemark")) { + contract.conRemark = values.conRemark; + } + } + + try { + // 仅使用WebSocket命令同步备注信息 + if ( + changedKeys.includes("conRemark") || + changedKeys.some(key => extendFieldKeys.includes(key)) + ) { + if (isGroup) { + // 群聊备注修改 + sendCommand("CmdModifyGroupRemark", { + wechatAccountId: contract.wechatAccountId, + chatroomId: contract.chatroomId, + newRemark: values.conRemark, + extendFields: extendFieldsStr, + }); + } else { + // 好友备注修改 + sendCommand("CmdModifyFriendRemark", { + wechatAccountId: contract.wechatAccountId, + wechatFriendId: contract.id, + newRemark: values.conRemark, + extendFields: extendFieldsStr, + }); + + // 同时更新会话列表和好友本地数据库中的extendFields字段 + updateLocalDBFriendInfo({ + wechatFriendId: contract.id, + extendFields: extendFieldsStr, + updateConversation: true, // 同时更新会话列表 + }).catch(err => { + console.error("更新本地数据库失败:", err); + }); + } + } + + // 如果有API返回的详情数据,更新本地状态 + if (friendDetail && !isGroup) { + setFriendDetail(prev => { + if (!prev) return prev; + return { + ...prev, + detail: { + ...prev.detail, + conRemark: changedKeys.includes("conRemark") + ? values.conRemark + : prev.detail.conRemark, + extendFields: extendFieldsStr, + }, + }; + }); + } + } catch (error) { + console.error("保存好友信息失败:", error); + return Promise.reject(error); + } + + // 返回Promise以便DetailValue组件处理成功状态 + return Promise.resolve(); }; // 处理群名称保存 - const handleSaveGroupName = () => { + const handleSaveGroupName = async ( + values: Record, + changedKeys: string[], + ) => { if (!hasGroupManagePermission()) { messageApi.error("只有群主才能修改群名称"); - return; + return Promise.reject("没有权限"); } + + // 更新groupNameValue + if (changedKeys.includes("groupName")) { + setGroupNameValue(values.groupName); + } + sendCommand("CmdChatroomOperate", { wechatAccountId: contract.wechatAccountId, wechatChatroomId: contract.id, chatroomOperateType: 6, - extra: `{"chatroomName":"${groupNameValue}"}`, + extra: `{"chatroomName":"${values.groupName}"}`, }); - messageApi.success("群名称修改成功"); - setIsEditingGroupName(false); - }; - - // 点击编辑群名称按钮 - const handleEditGroupName = () => { - if (!hasGroupManagePermission()) { - messageApi.error("只有群主才能修改群名称"); - return; - } - setGroupNameValue(contractInfo.name || ""); - setIsEditingGroupName(true); + return Promise.resolve(); }; // 处理群公告保存 @@ -373,23 +504,26 @@ const Person: React.FC = ({ contract }) => { }; // 处理我在本群中的昵称保存 - const handleSaveSelfDisplayName = () => { + const handleSaveSelfDisplayName = async ( + values: Record, + changedKeys: string[], + ) => { + // 更新selfDisplayNameValue + if (changedKeys.includes("selfDisplayName")) { + setSelfDisplayNameValue(values.selfDisplayName); + } + sendCommand("CmdChatroomOperate", { wechatAccountId: contract.wechatAccountId, wechatChatroomId: contract.id, chatroomOperateType: 8, - extra: `${selfDisplayNameValue}`, + extra: `${values.selfDisplayName}`, }); - messageApi.success("群昵称修改成功"); - setIsEditingSelfDisplayName(false); + return Promise.resolve(); }; - // 处理取消编辑 - const handleCancelEdit = () => { - setRemarkValue(contract.conRemark || ""); - setIsEditingRemark(false); - }; + // 这里不再需要handleCancelEdit,已由DetailValue组件内部处理 // 处理标签点击切换 const handleTagToggle = (tagName: string) => { @@ -490,31 +624,84 @@ const Person: React.FC = ({ contract }) => { }, }); }; - const extendFields = JSON.parse(contract.extendFields || "{}"); + const [extendFields, setExtendFields] = useState(() => { + try { + return JSON.parse(contract.extendFields || "{}"); + } catch (e) { + console.error("Failed to parse extendFields:", e); + return {}; + } + }); // 构建联系人或群聊详细信息 - const contractInfo = { - name: contract.name || contract.nickname, - nickname: contract.nickname, - alias: contract.alias, - wechatId: contract.wechatId, - chatroomId: isGroup ? contract.chatroomId : undefined, - chatroomOwner: isGroup ? contract.chatroomOwner : undefined, - avatar: contract.avatar || contract.chatroomAvatar, - phone: contract.phone || "-", - conRemark: remarkValue, // 使用当前编辑的备注值 - remark: extendFields.remark || "-", - email: contract.email || "-", - department: contract.department || "-", - position: contract.position || "-", - company: contract.company || "-", - region: contract.region || "-", - joinDate: contract.joinDate || "-", - notice: isGroup ? contract.notice : undefined, - selfDisplyName: isGroup ? contract.selfDisplyName : undefined, - status: "在线", - tags: selectedTags, - bio: contract.bio || contract.signature || "-", - }; + const contractInfo = useMemo(() => { + // 如果是个人资料且有API返回的详情数据,优先使用API数据 + if (!isGroup && friendDetail?.detail) { + const detail = friendDetail.detail; + // 解析扩展字段 + let extendFieldsObj: Record = {}; + try { + extendFieldsObj = JSON.parse(detail.extendFields || "{}"); + } catch (e) { + console.error("Failed to parse extendFields in contractInfo:", e); + } + + return { + name: detail.nickname || detail.alias, + nickname: detail.nickname, + alias: detail.alias, + wechatId: detail.wechatId, + avatar: detail.avatar, + phone: extendFieldsObj.phone || detail.phone || "", + conRemark: detail.conRemark || remarkValue, + remark: extendFieldsObj.remark || "", + email: extendFieldsObj.email || "", + department: detail.company || "", // 使用company作为department + position: extendFieldsObj.position || detail.position || "", + company: extendFieldsObj.company || detail.company || "", + region: detail.region || "", + joinDate: detail.createTime || "", // 使用createTime作为joinDate + status: "在线", + tags: detail.labels || selectedTags, + bio: detail.signature || "", + address: extendFieldsObj.address || "", + qq: extendFieldsObj.qq || "", + }; + } + + // 否则使用传入的contract数据 + return { + name: contract.name || contract.nickname, + nickname: contract.nickname, + alias: contract.alias, + wechatId: contract.wechatId, + chatroomId: isGroup ? contract.chatroomId : undefined, + chatroomOwner: isGroup ? contract.chatroomOwner : undefined, + avatar: contract.avatar || contract.chatroomAvatar, + phone: contract.phone || "", + conRemark: remarkValue, // 使用当前编辑的备注值 + remark: extendFields?.remark || "", + email: contract.email || "", + department: contract.department || "", + position: contract.position || "", + company: contract.company || "", + region: contract.region || "", + joinDate: contract.joinDate || "", + notice: isGroup ? contract.notice : undefined, + selfDisplyName: isGroup ? contract.selfDisplyName : undefined, + status: "在线", + tags: selectedTags, + bio: contract.bio || contract.signature || "", + address: contract.address || "", + qq: contract.qq || "", + }; + }, [ + contract, + friendDetail, + isGroup, + remarkValue, + selectedTags, + extendFields, + ]); // 分页状态 const [currentContactPage, setCurrentContactPage] = useState(1); @@ -635,6 +822,8 @@ const Person: React.FC = ({ contract }) => { setIsLoadingContacts(false); } }; + // 不再需要加载状态和错误状态的渲染,始终显示缓存数据 + return ( <> {contextHolder} @@ -665,226 +854,144 @@ const Person: React.FC = ({ contract }) => { {isGroup ? ( // 群聊信息 - <> -
- - 群名称: -
- {isEditingGroupName ? ( -
- setGroupNameValue(e.target.value)} - placeholder="请输入群名称" - size="small" - style={{ flex: 1 }} - /> -
- ) : ( -
- -

- {contractInfo.nickname || contractInfo.name} -

-
- {hasGroupManagePermission() && ( -
- )} -
-
-
- - 群ID: - - {contractInfo.chatroomId} - -
-
- - 群主: - - {contractInfo.chatroomOwner} - -
-
- - 群昵称: -
- {isEditingSelfDisplayName ? ( -
- setSelfDisplayNameValue(e.target.value)} - placeholder="请输入群昵称" - size="small" - style={{ flex: 1 }} - /> -
- ) : ( -
- - {contractInfo.selfDisplyName || "点击添加群昵称"} - -
- )} -
-
- + { + if (changedKeys.includes("groupName")) { + await handleSaveGroupName(values, ["groupName"]); + } + if (changedKeys.includes("selfDisplayName")) { + await handleSaveSelfDisplayName(values, ["selfDisplayName"]); + } + return Promise.resolve(); + }} + onSaveSuccess={(values, changedKeys) => { + // 更新本地值 + if (changedKeys.includes("groupName")) { + setGroupNameValue(values.groupName); + } + if (changedKeys.includes("selfDisplayName")) { + setSelfDisplayNameValue(values.selfDisplayName); + } + }} + isGroup={true} + /> ) : ( // 好友信息 - <> -
- - 微信号: - - {contractInfo.alias || contractInfo.wechatId} - -
-
- - 电话: - {contractInfo.phone} -
-
- - 地区: - {contractInfo.region} -
- - )} - {!isGroup && ( -
- - 备注: -
- {isEditingRemark ? ( -
- setRemarkValue(e.target.value)} - placeholder="请输入备注" - size="small" - style={{ flex: 1 }} - /> -
- ) : ( -
- {contractInfo.conRemark || "点击添加备注"} -
- )} -
-
+ { + // 更新本地值 + if (changedKeys.includes("conRemark")) { + setRemarkValue(values.conRemark); + } + }} + isGroup={false} + /> )}
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx index 7d322637b..df0667624 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx @@ -18,6 +18,16 @@ interface PersonProps { const Person: React.FC = ({ contract }) => { const [activeKey, setActiveKey] = useState("profile"); const isGroup = "chatroomId" in contract; + // 使用state保存当前contract的副本,确保在切换tab时不会丢失修改 + const [currentContract, setCurrentContract] = useState< + ContractData | weChatGroup + >(contract); + + // 当外部contract变化时,更新内部状态 + useEffect(() => { + setCurrentContract(contract); + }, [contract]); + const tabItems = useMemo(() => { const baseItems = [ { @@ -28,18 +38,18 @@ const Person: React.FC = ({ contract }) => { { key: "profile", label: isGroup ? "群资料" : "个人资料", - children: , + children: , }, ]; if (!isGroup) { baseItems.push({ key: "moments", label: "朋友圈", - children: , + children: , }); } return baseItems; - }, [contract, isGroup]); + }, [currentContract, isGroup]); useEffect(() => { setActiveKey("profile"); From e9aa500800dc986a4b26bb49d4c1662080461082 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: Wed, 19 Nov 2025 09:43:06 +0800 Subject: [PATCH 15/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E5=8A=A9=E6=89=8B=E7=BB=84=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=EF=BC=8C=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=AF=BC=E8=88=AA=E8=B7=AF=E5=BE=84=E7=9A=84=E6=8D=A2?= =?UTF-8?q?=E8=A1=8C=E6=96=B9=E5=BC=8F=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=8F=8F?= =?UTF-8?q?=E8=BF=B0=E6=96=87=E6=9C=AC=E4=BB=A5=E6=94=B9=E5=96=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/PushTaskModal.tsx | 2 +- .../create-push-task/index.tsx | 2 +- .../message-push-assistant/index.tsx | 21 +++++++++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx index 74cec1e1e..af9d73732 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx @@ -105,7 +105,7 @@ const PushTaskModal: React.FC = ({ }; const getSubtitle = () => { - return "智能批量推送,AI智能话术改写"; + return "智能批量推送,AI智能话术改写"; }; // 步骤2的标题 diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx index d682b46c6..5ee5b4bdf 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx @@ -127,7 +127,7 @@ const CreatePushTask: React.FC = () => { } }, [validPushType]); - const subtitle = "智能批量推送,AI智能话术改写"; + const subtitle = "智能批量推送,AI智能话术改写"; const step2Title = useMemo(() => { switch (validPushType) { diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx index acf205f30..2a8559372 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx @@ -12,7 +12,10 @@ import { } from "@ant-design/icons"; import styles from "./index.module.scss"; -export type PushType = "friend-message" | "group-message" | "group-announcement"; +export type PushType = + | "friend-message" + | "group-message" + | "group-announcement"; const MessagePushAssistant: React.FC = () => { const navigate = useNavigate(); @@ -26,7 +29,9 @@ const MessagePushAssistant: React.FC = () => { icon: , color: "#1890ff", onClick: () => { - navigate("/pc/powerCenter/message-push-assistant/create-push-task/friend-message"); + navigate( + "/pc/powerCenter/message-push-assistant/create-push-task/friend-message", + ); }, }, { @@ -36,17 +41,21 @@ const MessagePushAssistant: React.FC = () => { icon: , color: "#52c41a", onClick: () => { - navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-message"); + navigate( + "/pc/powerCenter/message-push-assistant/create-push-task/group-message", + ); }, }, { id: "group-announcement", title: "群公告推送", - description: "向选定的微信群发布群公告", + description: "向选定的微信群批量发布群公告", icon: , color: "#722ed1", onClick: () => { - navigate("/pc/powerCenter/message-push-assistant/create-push-task/group-announcement"); + navigate( + "/pc/powerCenter/message-push-assistant/create-push-task/group-announcement", + ); }, }, ]; @@ -81,7 +90,7 @@ const MessagePushAssistant: React.FC = () => {
navigate("/pc/powerCenter")}