diff --git a/Touchkebao/src/api/module/group.ts b/Touchkebao/src/api/module/group.ts new file mode 100644 index 00000000..5090fb0e --- /dev/null +++ b/Touchkebao/src/api/module/group.ts @@ -0,0 +1,44 @@ +import request from "@/api/request"; +// 添加分组 +interface AddGroupData { + groupName: string; + groupMemo: string; + groupType: number; + sort: number; +} +export function addGroup(data: AddGroupData) { + return request("/v1/kefu/wechatGroup/add", data, "POST"); +} + +// 更新分组 +interface UpdateGroupData { + id: number; + groupName: string; + groupMemo: string; + groupType: number; + sort: number; +} + +export function updateGroup(data: UpdateGroupData) { + return request("/v1/kefu/wechatGroup/update", data, "POST"); +} + +// 删除分组 +export function deleteGroup(id: number) { + return request(`/v1/kefu/wechatGroup/delete/${id}`, null, "DELETE"); +} + +// 获取分组列表 +export function getGroupList() { + return request("/v1/kefu/wechatGroup/list", null, "GET"); +} + +// 移动分组 +interface MoveGroupData { + type: "friend" | "group"; + groupId: number; + id: number; +} +export function moveGroup(data: MoveGroupData) { + return request("/v1/kefu/wechatGroup/move", data, "POST"); +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx index 76d7f87a..4f99bb33 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx @@ -84,7 +84,7 @@ const FriendsCircle: React.FC = ({ wechatFriendId }) => { className={styles.avatar} src={currentKf?.avatar || defaultAvatar} alt="客服头像" - onError={(e) => { + onError={e => { // 如果图片加载失败,使用默认头像 const target = e.target as HTMLImageElement; if (target.src !== defaultAvatar) { diff --git a/Touchkebao/提示词/React代码编写规范.md b/Touchkebao/提示词/React代码编写规范.md deleted file mode 100644 index f256faa5..00000000 --- a/Touchkebao/提示词/React代码编写规范.md +++ /dev/null @@ -1,767 +0,0 @@ -# React 代码编写规范大全 - -> 参考来源:[React 代码编写规范大全:从基础到架构,打造可维护的企业级应用](https://blog.csdn.net/qq_16242613/article/details/150996674) - -编写 React 代码不仅仅是让功能跑起来,更重要的是保证代码的可读性、可维护性和团队协作效率。一套良好的编码规范是达成这些目标的基石。 - -本文将从**基础规范**、**组件设计**、**状态管理**、**样式处理**、**性能优化**和**项目架构**六个层次,由浅入深地详解 React 的代码编写规范。 - ---- - -## 📋 目录 - -- [一、核心六层规范架构](#一核心六层规范架构) -- [二、L1 - 基础与核心规范](#二l1---基础与核心规范) -- [三、L2 - 组件设计模式](#三l2---组件设计模式) -- [四、L3 - 状态管理规范](#四l3---状态管理规范) -- [五、L4 - 样式与CSS策略](#五l4---样式与css策略) -- [六、L5 - 性能优化指南](#六l5---性能优化指南) -- [七、L6 - 项目结构与架构](#七l6---项目结构与架构) - ---- - -## 一、核心六层规范架构 - -React 代码规范的六个核心层次,构成了一个完整、健壮的应用开发体系: - -``` -React 代码规范六层体系 -├── L1: 基础与核心规范 (命名、文件组织、JSX、PropTypes) -│ └── 目标: 代码一致性与可读性 -├── L2: 组件设计模式 (组件拆分、组合、解耦) -│ └── 目标: 可复用与可维护性 -├── L3: 状态管理规范 (State原则、Reducer、Context、Redux) -│ └── 目标: 数据流清晰与可预测 -├── L4: 样式与CSS策略 (CSS Modules、Styled-Components、方案选型) -│ └── 目标: 样式可控与避免冲突 -├── L5: 性能优化指南 (Memo、Callback、懒加载、列表优化) -│ └── 目标: 应用流畅与用户体验 -└── L6: 项目结构与架构 (目录组织、路由、配置、静态资源) - └── 目标: 项目可扩展与易于协作 -``` - ---- - -## 二、L1 - 基础与核心规范 - -这是最基础也是必须遵守的规范,保证了代码的一致性和可读性。 - -### 1. 命名规范 (Naming Conventions) - -#### 组件命名 - -使用 **PascalCase** (大驼峰命名法),且名称与文件名一致。 - -```tsx -// ✅ 正确 -// UserProfile.tsx -function UserProfile() { ... } - -// ❌ 错误 -// userProfile.tsx -function user_profile() { ... } -function User_Profile() { ... } -``` - -#### 属性命名 - -使用 **camelCase** (小驼峰命名法)。 - -```tsx -// ✅ 正确 - - ); -}; -``` - -#### 容器组件 (Container Components) - -- 负责数据获取和状态管理 -- 包含业务逻辑 -- 将数据传递给展示组件 - -```tsx -// ✅ 容器组件 -const UserListContainer = () => { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetchUsers().then(data => { - setUsers(data); - setLoading(false); - }); - }, []); - - if (loading) return ; - - return ; -}; -``` - ---- - -## 四、L3 - 状态管理规范 - -### 1. State 放置原则 - -#### 状态提升 (Lifting State Up) - -将共享状态提升到最近的公共父组件。 - -```tsx -// ❌ 错误:状态分散 -const ChildA = () => { - const [sharedValue, setSharedValue] = useState(""); - // ... -}; - -const ChildB = () => { - const [sharedValue, setSharedValue] = useState(""); - // ... -}; - -// ✅ 正确:状态提升 -const Parent = () => { - const [sharedValue, setSharedValue] = useState(""); - return ( - <> - - - - ); -}; -``` - -#### 状态位置判断 - -- **组件内部状态**:只在该组件内使用 -- **共享状态**:提升到公共父组件或使用全局状态管理 -- **服务器状态**:使用 React Query 或 SWR - -### 2. 使用 useReducer 管理复杂状态 - -当状态逻辑复杂时,使用 `useReducer` 替代多个 `useState`。 - -```tsx -// ✅ 使用 useReducer -interface State { - count: number; - step: number; -} - -type Action = - | { type: "increment" } - | { type: "decrement" } - | { type: "reset" } - | { type: "setStep"; step: number }; - -const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "increment": - return { ...state, count: state.count + state.step }; - case "decrement": - return { ...state, count: state.count - state.step }; - case "reset": - return { ...state, count: 0 }; - case "setStep": - return { ...state, step: action.step }; - default: - return state; - } -}; - -const Counter = () => { - const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 }); - // ... -}; -``` - -### 3. 全局状态管理 - -#### Zustand (本项目使用) - -```tsx -// ✅ 使用 Zustand -import { create } from "zustand"; - -interface UserStore { - user: User | null; - setUser: (user: User) => void; - clearUser: () => void; -} - -const useUserStore = create(set => ({ - user: null, - setUser: user => set({ user }), - clearUser: () => set({ user: null }), -})); - -// 使用 -const UserProfile = () => { - const user = useUserStore(state => state.user); - const setUser = useUserStore(state => state.setUser); - // ... -}; -``` - -#### Context API - -适用于中等规模的全局状态。 - -```tsx -// ✅ 使用 Context -const UserContext = createContext(null); - -export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - const [user, setUser] = useState(null); - - return ( - - {children} - - ); -}; -``` - ---- - -## 五、L4 - 样式与CSS策略 - -### 1. CSS Modules (推荐) - -本项目使用 **CSS Modules** 配合 **SCSS**。 - -```tsx -// ✅ 使用 CSS Modules -import styles from "./UserProfile.module.scss"; - -const UserProfile = () => { - return
...
; -}; -``` - -```scss -// UserProfile.module.scss -.container { - padding: 16px; - background-color: #fff; - - .title { - font-size: 24px; - font-weight: bold; - } -} -``` - -#### 优势 - -- ✅ 样式作用域隔离,避免冲突 -- ✅ 支持 TypeScript 类型检查 -- ✅ 支持 SCSS 嵌套和变量 -- ✅ 构建时优化,自动移除未使用的样式 - -### 2. 样式命名规范 - -```scss -// ✅ 推荐:BEM 命名法 -.user-profile { - &__header { - // ... - } - - &__body { - // ... - } - - &__footer { - // ... - } - - &--active { - // ... - } -} -``` - -### 3. 样式变量 - -```scss -// ✅ 使用 SCSS 变量 -$primary-color: #1890ff; -$secondary-color: #52c41a; -$font-size-base: 14px; - -.button { - background-color: $primary-color; - font-size: $font-size-base; -} -``` - ---- - -## 六、L5 - 性能优化指南 - -### 1. 避免不必要的重新渲染 - -#### 使用 React.memo - -```tsx -// ✅ 使用 React.memo 优化子组件 -const UserItem = React.memo( - ({ user }: { user: User }) => { - return
{user.name}
; - }, - (prevProps, nextProps) => { - // 自定义比较函数 - return prevProps.user.id === nextProps.user.id; - }, -); -``` - -#### 使用 useMemo - -```tsx -// ✅ 使用 useMemo 缓存计算结果 -const ExpensiveComponent = ({ items }: { items: Item[] }) => { - const sortedItems = useMemo(() => { - return items.sort((a, b) => a.price - b.price); - }, [items]); - - return
{/* 使用 sortedItems */}
; -}; -``` - -#### 使用 useCallback - -```tsx -// ✅ 使用 useCallback 缓存函数 -const Parent = () => { - const [count, setCount] = useState(0); - - const handleClick = useCallback(() => { - setCount(prev => prev + 1); - }, []); - - return ; -}; -``` - -### 2. 代码分割与懒加载 (Lazy Loading) - -```tsx -// ✅ 使用 React.lazy 和 Suspense -import { lazy, Suspense } from "react"; - -const UserProfile = lazy(() => import("./pages/UserProfile")); - -const App = () => { - return ( - }> - - - ); -}; -``` - -### 3. 列表渲染优化 - -#### 虚拟列表 - -对于长列表,使用虚拟滚动。 - -```tsx -// ✅ 使用虚拟列表组件 -import { FixedSizeList } from "react-window"; - -const VirtualizedList = ({ items }: { items: Item[] }) => { - return ( - - {({ index, style }) => ( -
- -
- )} -
- ); -}; -``` - ---- - -## 七、L6 - 项目结构与架构 - -### 1. 功能分区目录结构 (Feature-Based Structure) - -本项目采用功能分区 + 类型分区的混合结构: - -``` -src/ -├── components/ # 通用组件(可复用) -│ ├── Button/ -│ ├── Modal/ -│ └── ... -├── pages/ # 页面组件(路由级别) -│ ├── Home/ -│ ├── Profile/ -│ └── ... -├── hooks/ # 自定义 Hooks -│ ├── useAuth.ts -│ └── useLocalStorage.ts -├── utils/ # 工具函数 -│ ├── formatDate.ts -│ └── validate.ts -├── store/ # 状态管理(Zustand) -│ ├── module/ -│ │ ├── user.ts -│ │ └── weChat.ts -│ └── index.ts -├── api/ # API 接口 -│ ├── request.ts -│ └── module/ -│ └── wechat.ts -├── types/ # TypeScript 类型定义 -│ ├── user.ts -│ └── weChat.ts -└── router/ # 路由配置 - ├── index.tsx - └── module/ -``` - -### 2. 使用绝对路径导入 - -本项目已配置路径别名 `@` 指向 `src` 目录。 - -```tsx -// ✅ 使用绝对路径 -import { Button } from "@/components/Button"; -import { useUserStore } from "@/store/module/user"; -import { formatDate } from "@/utils/formatDate"; - -// ❌ 避免相对路径 -import { Button } from "../../../components/Button"; -``` - -### 3. 组件导出规范 - -```tsx -// ✅ 推荐:使用 index.ts 统一导出 -// components/Button/index.tsx -export { default } from "./Button"; -export type { ButtonProps } from "./types"; - -// 使用 -import Button from "@/components/Button"; -import type { ButtonProps } from "@/components/Button"; -``` - -### 4. 类型定义规范 - -```tsx -// ✅ 类型定义文件 -// types/user.ts -export interface User { - id: number; - name: string; - email: string; -} - -export type UserRole = "admin" | "user" | "guest"; - -// 使用 -import type { User, UserRole } from "@/types/user"; -``` - ---- - -## 📝 总结 - -### 核心原则 - -1. **一致性**:保持代码风格和命名规范的一致性 -2. **可读性**:代码应该易于理解和维护 -3. **可复用性**:组件和函数应该尽可能可复用 -4. **性能**:关注性能优化,但不要过度优化 -5. **类型安全**:充分利用 TypeScript 的类型检查 - -### 检查清单 - -在提交代码前,确保: - -- [ ] 组件命名符合 PascalCase -- [ ] 函数命名符合 camelCase -- [ ] 使用了 TypeScript 类型定义 -- [ ] 样式使用了 CSS Modules -- [ ] 避免了不必要的重新渲染 -- [ ] 使用了绝对路径导入 -- [ ] 代码通过了 ESLint 检查 -- [ ] 代码通过了 Prettier 格式化 - -### 参考资源 - -- [React 官方文档](https://react.dev/) -- [TypeScript 官方文档](https://www.typescriptlang.org/) -- [Zustand 文档](https://zustand-demo.pmnd.rs/) -- [CSS Modules 文档](https://github.com/css-modules/css-modules) - ---- - -**最后更新**: 2025-01-XX -**维护者**: 开发团队 diff --git a/Touchkebao/提示词/存客宝新架构.md b/Touchkebao/提示词/存客宝新架构.md new file mode 100644 index 00000000..8dea0938 --- /dev/null +++ b/Touchkebao/提示词/存客宝新架构.md @@ -0,0 +1,2110 @@ +# 存客宝新架构设计方案 + +## 一、业务模型与核心问题 + +### 1.1 业务模型 + +``` +系统用户账号 + └── 关联多个微信账号(左侧客服列表) + ├── 会话列表(Chat Sessions) + │ └── 一次性加载全部,切换账号时过滤 + └── 联系人列表(Contacts) + ├── 分组列表(Groups)- 一次性加载分组信息 + └── 联系人数据(Contacts) + ├── 按分组懒加载(点击展开时加载) + ├── 根据groupType判断:1=好友列表API,2=群列表API + └── 支持分页加载(limit + page参数) +``` + +### 1.2 核心问题 + +**重要说明**: + +- ✅ **会话列表加载方式**:一次性加载所有的会话数据到内存,切换账号时通过过滤 +- ✅ **联系人列表加载方式**:**按分组懒加载**,点击展开分组后才加载该分组的数据 +- ✅ **切换方式**:切换微信账号时,会话列表通过过滤已加载的数据;联系人列表需要重新加载展开的分组 +- ❌ **核心问题**:如何高效地处理大数据量渲染和切换性能问题 + +**具体问题**: + +1. **会话列表过滤性能**:从数万条会话中按 `wechatAccountId` 过滤,需要遍历大量数据 +2. **联系人列表懒加载**:点击展开分组时,需要调用API加载数据,如何优化加载和渲染 +3. **联系人列表分页加载**:分组内数据量大,需要分页加载和虚拟滚动 +4. **搜索功能问题**:搜索时需要同时请求好友列表和群列表API,不能依赖已加载的分组数据 +5. **切换账号性能**:切换账号时,会话列表需要重新过滤;联系人列表需要重新加载展开的分组 +6. **虚拟滚动问题**:过滤后的数据量仍然很大,需要虚拟滚动优化渲染 +7. **数据同步问题**:WebSocket收到新联系人时,需要更新对应分组的数据 +8. **状态更新问题**:切换账号时,如何避免不必要的重渲染和计算 +9. **本地数据库缓存问题**:按分组懒加载后,是否需要保留本地数据库缓存?如何优化缓存策略? + +--- + +## 二、架构设计原则 + +### 2.1 核心原则 + +1. **按需加载**:只加载可见区域的数据 +2. **虚拟渲染**:使用虚拟滚动,只渲染可见DOM节点 +3. **智能缓存**:按微信账号缓存数据,避免重复加载 +4. **渐进式渲染**:优先渲染关键数据,后台加载完整数据 +5. **状态分离**:数据状态与UI状态分离,减少不必要的重渲染 + +--- + +## 三、数据层架构 + +### 3.1 数据分层设计 + +``` +┌─────────────────────────────────────────┐ +│ Server API Layer (远程数据) │ +│ - 分页接口 │ +│ - 增量同步接口 │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Cache Layer (缓存层) │ +│ - IndexedDB (持久化缓存) │ +│ - Memory Cache (内存缓存) │ +│ - 按微信账号ID分组缓存 │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Store Layer (状态管理层) │ +│ - 当前可见数据(已筛选) │ +│ - 加载状态 │ +│ - 分页状态 │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ View Layer (视图层) │ +│ - 虚拟滚动组件 │ +│ - 只渲染可见项 │ +└─────────────────────────────────────────┘ +``` + +### 3.2 数据索引策略(核心优化) + +#### 3.2.1 数据索引架构 + +**关键优化**:使用Map索引,避免每次切换都遍历全部数据 + +``` +原始数据(一次性加载): +- allSessions: ChatSession[] (10000条) +- allContacts: Contact[] (50000条) + +索引结构(按账号ID分组): +- sessionIndex: Map (快速查找) +- contactIndex: Map (快速查找) + +过滤缓存(切换时使用): +- filteredSessionsCache: Map (缓存过滤结果) +- filteredContactsCache: Map (缓存过滤结果) +``` + +#### 3.2.2 索引构建策略 + +```typescript +// 数据加载时,立即构建索引 +interface DataIndexManager { + // 会话索引:accountId → sessions[] + sessionIndex: Map; + + // 好友索引:accountId → contacts[] + contactIndex: Map; + + // 构建索引(数据加载时调用) + buildIndexes: (sessions: ChatSession[], contacts: Contact[]) => void; + + // 快速获取(O(1)时间复杂度) + getSessionsByAccount: (accountId: number) => ChatSession[]; + getContactsByAccount: (accountId: number) => Contact[]; + + // 增量更新索引(新增数据时) + addSession: (session: ChatSession) => void; + addContact: (contact: Contact) => void; +} +``` + +#### 3.2.3 过滤缓存策略 + +```typescript +// 过滤结果缓存,避免重复计算 +interface FilterCache { + // 缓存过滤后的结果 + sessionsCache: Map; // accountId → filteredSessions + contactsCache: Map; // accountId → filteredContacts + + // 缓存失效标记(数据更新时失效) + sessionsCacheValid: Map; + contactsCacheValid: Map; + + // 获取过滤结果(优先使用缓存) + getFilteredSessions: (accountId: number) => ChatSession[]; + getFilteredContacts: (accountId: number) => Contact[]; + + // 失效缓存(数据更新时调用) + invalidateCache: (accountId: number) => void; +} +``` + +### 3.3 数据模型设计 + +#### 3.3.1 全局数据模型(一次性加载) + +```typescript +// 全局数据存储(所有账号的数据) +interface GlobalDataStore { + // 原始数据(一次性加载全部) + allSessions: ChatSession[]; // 所有会话(10000+条) + allContacts: Contact[]; // 所有好友(50000+条) + + // 索引结构(快速查找) + sessionIndex: Map; // accountId → sessions[] + contactIndex: Map; // accountId → contacts[] + + // 过滤缓存(切换账号时使用) + filteredSessionsCache: Map; // accountId → filteredSessions + filteredContactsCache: Map; // accountId → filteredContacts + + // 缓存有效性标记 + cacheValid: { + sessions: Map; + contacts: Map; + }; +} +``` + +#### 3.3.2 当前显示数据模型(过滤后的数据) + +```typescript +// 当前显示的数据(根据selectedAccountId过滤) +interface DisplayDataState { + // 当前账号ID(0表示"全部") + selectedAccountId: number; + + // 过滤后的数据(用于虚拟滚动) + filteredSessions: ChatSession[]; // 当前显示的会话列表 + filteredContacts: Contact[]; // 当前显示的好友列表 + + // 虚拟滚动可见区域 + visibleSessions: ChatSession[]; // 可见区域的会话(10-20条) + visibleContacts: Contact[]; // 可见区域的好友(10-20条) + + // 统计信息 + sessionsCount: number; // 当前显示的会话数量 + contactsCount: number; // 当前显示的好友数量 +} +``` + +--- + +## 四、状态管理架构 + +### 4.1 Store分层设计 + +``` +┌─────────────────────────────────────┐ +│ WeChatAccountStore │ +│ - 微信账号列表 │ +│ - 当前选中的账号ID (0=全部) │ +│ - 账号在线状态 │ +└──────────────┬──────────────────────┘ + │ + ┌──────────┴──────────┐ + │ │ +┌───▼──────────┐ ┌──────▼──────────┐ +│ SessionStore │ │ ContactStore │ +│ │ │ │ +│ - 会话数据 │ │ - 好友数据 │ +│ - 筛选逻辑 │ │ - 分组逻辑 │ +│ - 虚拟滚动 │ │ - 虚拟滚动 │ +│ 状态 │ │ 状态 │ +└──────────────┘ └─────────────────┘ +``` + +### 4.2 核心Store设计 + +#### 4.2.1 WeChatAccountStore + +```typescript +interface WeChatAccountState { + // 账号列表 + accountList: WeChatAccount[]; + selectedAccountId: number; // 0表示"全部" + + // 账号状态 + accountStatusMap: Map< + number, + { + isOnline: boolean; + lastSyncTime: number; + } + >; + + // 操作方法 + setAccountList: (accounts: WeChatAccount[]) => void; + setSelectedAccount: (accountId: number) => void; + updateAccountStatus: ( + accountId: number, + status: Partial, + ) => void; +} +``` + +#### 4.2.2 SessionStore(会话列表) + +```typescript +interface SessionStoreState { + // 全局数据(一次性加载全部) + allSessions: ChatSession[]; // 所有会话数据(10000+条) + + // 索引结构(快速查找) + sessionIndex: Map; // accountId → sessions[] + + // 当前显示的数据(过滤后的) + selectedAccountId: number; // 当前选中的账号ID(0=全部) + filteredSessions: ChatSession[]; // 过滤后的会话列表(用于虚拟滚动) + + // 虚拟滚动状态 + virtualScrollState: { + startIndex: number; // 可见区域起始索引 + endIndex: number; // 可见区域结束索引 + itemHeight: number; // 每项高度(固定72px) + containerHeight: number; // 容器高度 + totalHeight: number; // 总高度(filteredSessions.length * itemHeight) + }; + + // 筛选和排序 + searchKeyword: string; + sortBy: "time" | "unread" | "name"; + + // 过滤缓存(避免重复计算) + filteredSessionsCache: Map; // accountId → filteredSessions + cacheValid: Map; // accountId → 缓存是否有效 + + // 操作方法 + setAllSessions: (sessions: ChatSession[]) => void; // 设置全部数据并构建索引 + switchAccount: (accountId: number) => void; // 切换账号(使用索引过滤) + setVisibleRange: (start: number, end: number) => void; // 设置可见范围 + filter: (keyword: string) => void; // 搜索过滤 + sort: (by: "time" | "unread" | "name") => void; // 排序 + addSession: (session: ChatSession) => void; // 新增会话(更新索引) +} +``` + +#### 4.2.3 ContactStore(联系人列表 - 按分组懒加载) + +```typescript +interface ContactGroup { + id: number; // 分组ID(groupId) + groupName: string; // 分组名称 + groupType: 1 | 2; // 1=好友列表,2=群列表 + count?: number; // 分组内联系人数量(统计信息) +} + +interface GroupContactData { + contacts: Contact[]; // 已加载的联系人列表 + page: number; // 当前页码 + pageSize: number; // 每页数量 + hasMore: boolean; // 是否还有更多数据 + loading: boolean; // 是否正在加载 + loaded: boolean; // 是否已加载过(用于判断是否需要重新加载) +} + +interface ContactStoreState { + // ==================== 分组列表(一次性加载)==================== + groups: ContactGroup[]; // 所有分组信息 + + // ==================== 当前选中的账号ID(0=全部)==================== + selectedAccountId: number; + + // ==================== 展开的分组 ==================== + expandedGroups: Set; // groupKey集合(格式:`${groupId}_${groupType}_${accountId}`) + + // ==================== 分组数据(按分组懒加载)==================== + groupData: Map; // groupKey → GroupContactData + + // ==================== 搜索相关 ==================== + searchKeyword: string; + isSearchMode: boolean; + searchResults: Contact[]; // 搜索结果(调用API获取,不依赖分组数据) + searchLoading: boolean; + + // ==================== 虚拟滚动状态(每个分组独立)==================== + virtualScrollStates: Map; + + // ==================== 操作方法 ==================== + // 分组管理 + setGroups: (groups: ContactGroup[]) => void; // 设置分组列表 + toggleGroup: (groupId: number, groupType: 1 | 2) => Promise; // 切换分组展开/折叠 + + // 分组编辑操作 + addGroup: (group: Omit) => Promise; // 新增分组 + updateGroup: (group: ContactGroup) => Promise; // 更新分组 + deleteGroup: (groupId: number, groupType: 1 | 2) => Promise; // 删除分组 + + // 分组数据加载 + loadGroupContacts: ( + groupId: number, + groupType: 1 | 2, + page?: number, + limit?: number, + ) => Promise; // 加载分组联系人(懒加载) + loadMoreGroupContacts: (groupId: number, groupType: 1 | 2) => Promise; // 加载更多 + + // 搜索 + searchContacts: (keyword: string) => Promise; // 搜索(调用API,同时请求好友和群列表) + clearSearch: () => void; // 清除搜索 + + // 切换账号 + switchAccount: (accountId: number) => Promise; // 切换账号(重新加载展开的分组) + + // 联系人操作 + addContact: (contact: Contact) => void; // 新增联系人(更新对应分组) + updateContact: (contact: Contact) => void; // 更新联系人(更新对应分组) + updateContactRemark: ( + contactId: number, + groupId: number, + groupType: 1 | 2, + remark: string, + ) => Promise; // 修改联系人备注(右键菜单) + deleteContact: (contactId: number, groupId: number, groupType: 1 | 2) => void; // 删除联系人 + moveContactToGroup: ( + contactId: number, + fromGroupId: number, + toGroupId: number, + groupType: 1 | 2, + ) => Promise; // 移动联系人到其他分组(右键菜单) + + // 虚拟滚动 + setVisibleRange: (groupKey: string, start: number, end: number) => void; // 设置可见范围 +} +``` + +--- + +## 五、虚拟滚动架构 + +### 5.1 虚拟滚动原理 + +``` +┌─────────────────────────────────┐ +│ 容器 (固定高度) │ +│ ┌───────────────────────────┐ │ +│ │ 占位区域 (上方不可见项) │ │ ← 空白占位 +│ ├───────────────────────────┤ │ +│ │ 可见区域 (渲染的DOM) │ │ ← 只渲染这部分 +│ │ - Item 100 │ │ +│ │ - Item 101 │ │ +│ │ - Item 102 │ │ +│ │ - Item 103 │ │ +│ │ - Item 104 │ │ +│ ├───────────────────────────┤ │ +│ │ 占位区域 (下方不可见项) │ │ ← 空白占位 +│ └───────────────────────────┘ │ +└─────────────────────────────────┘ + +总数据:10000条 +可见区域:只渲染5-10条 +DOM节点:从10000个减少到10个 +``` + +### 5.2 虚拟滚动实现方案 + +#### 5.2.1 固定高度虚拟滚动 + +**适用场景**:会话列表(每项高度固定) + +```typescript +// 计算逻辑 +const ITEM_HEIGHT = 72; // 每项固定高度 +const CONTAINER_HEIGHT = 600; // 容器高度 +const VISIBLE_COUNT = Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + 2; // 可见项数 + 缓冲 + +// 滚动时计算 +const scrollTop = container.scrollTop; +const startIndex = Math.floor(scrollTop / ITEM_HEIGHT); +const endIndex = startIndex + VISIBLE_COUNT; + +// 渲染的数据 +const visibleItems = allItems.slice(startIndex, endIndex); + +// 占位高度 +const offsetTop = startIndex * ITEM_HEIGHT; +const totalHeight = allItems.length * ITEM_HEIGHT; +``` + +#### 5.2.2 动态高度虚拟滚动 + +**适用场景**:好友列表(分组折叠/展开,高度变化) + +```typescript +// 使用 Intersection Observer 或 动态测量 +// 方案1:预估高度 + 动态调整 +const ESTIMATED_HEIGHT = 50; +const measuredHeights = new Map(); // 实际测量的高度 + +// 方案2:使用 react-window 的 VariableSizeList +// 方案3:使用 react-virtualized 的 AutoSizer + List +``` + +### 5.3 虚拟滚动组件设计 + +#### 5.3.1 会话列表虚拟滚动 + +```typescript +// components/VirtualSessionList.tsx +interface VirtualSessionListProps { + sessions: ChatSession[]; + itemHeight: number; + containerHeight: number; + renderItem: (session: ChatSession, index: number) => React.ReactNode; + onScroll?: (scrollTop: number) => void; + onLoadMore?: () => void; +} + +// 实现要点: +// 1. 使用 react-window 或自实现虚拟滚动 +// 2. 监听滚动事件,计算可见范围 +// 3. 只渲染可见区域的项 +// 4. 使用占位div保持总高度 +// 5. 滚动到底部时触发加载更多 +``` + +#### 5.3.2 好友列表虚拟滚动 + +```typescript +// components/VirtualContactList.tsx +interface VirtualContactListProps { + groupedContacts: Map; + itemHeight: number; + groupHeaderHeight: number; + containerHeight: number; + renderGroupHeader: (groupKey: string) => React.ReactNode; + renderItem: (contact: Contact, index: number) => React.ReactNode; + onGroupToggle: (groupKey: string) => void; +} + +// 实现要点: +// 1. 每个分组独立计算可见范围 +// 2. 分组折叠时,该分组不渲染 +// 3. 动态计算总高度(考虑分组展开/折叠) +// 4. 支持分组内虚拟滚动 +``` + +--- + +## 六、数据过滤与切换策略(核心优化) + +### 6.1 数据索引构建(一次性加载时) + +```typescript +// 数据加载时,立即构建索引 +function buildIndexes(sessions: ChatSession[], contacts: Contact[]) { + // 1. 构建会话索引(O(n)时间复杂度,只执行一次) + const sessionIndex = new Map(); + sessions.forEach(session => { + const accountId = session.wechatAccountId; + if (!sessionIndex.has(accountId)) { + sessionIndex.set(accountId, []); + } + sessionIndex.get(accountId)!.push(session); + }); + + // 2. 构建好友索引 + const contactIndex = new Map(); + contacts.forEach(contact => { + const accountId = contact.wechatAccountId; + if (!contactIndex.has(accountId)) { + contactIndex.set(accountId, []); + } + contactIndex.get(accountId)!.push(contact); + }); + + return { sessionIndex, contactIndex }; +} +``` + +### 6.2 切换账号过滤策略(核心优化) + +#### 6.2.1 快速过滤(使用索引) + +```typescript +// 切换账号时的过滤逻辑(核心优化) +function switchAccount(accountId: number) { + // 1. 检查缓存(O(1) - 最快,< 1ms) + if ( + filteredSessionsCache.has(accountId) && + cacheValid.sessions.get(accountId) + ) { + return filteredSessionsCache.get(accountId)!; // 直接返回缓存 + } + + // 2. 使用索引快速获取(O(1) - 次快,< 1ms) + let filteredSessions: ChatSession[]; + if (accountId === 0) { + // "全部":返回所有会话(引用,不复制) + filteredSessions = allSessions; + } else { + // 特定账号:从索引获取(O(1),不遍历全部数据) + filteredSessions = sessionIndex.get(accountId) || []; + } + + // 3. 应用搜索和排序(如果需要) + // 注意:这里只对当前账号的数据进行过滤,不是全部数据 + // 例如:账号A有1000条,只对这1000条过滤,不是对全部10000条过滤 + if (searchKeyword) { + filteredSessions = filteredSessions.filter(s => + s.nickname.includes(searchKeyword), + ); + } + if (sortBy !== "time") { + filteredSessions = sortSessions(filteredSessions, sortBy); + } + + // 4. 缓存结果(避免下次切换时重复计算) + filteredSessionsCache.set(accountId, filteredSessions); + cacheValid.sessions.set(accountId, true); + + return filteredSessions; +} + +// 性能对比: +// ❌ 旧方案:allSessions.filter(s => s.wechatAccountId === accountId) +// 时间复杂度:O(n),n=10000,需要遍历全部数据,耗时:~50-100ms +// ✅ 新方案:sessionIndex.get(accountId) +// 时间复杂度:O(1),直接从索引获取,耗时:< 1ms +// 性能提升:50-100倍 +``` + +#### 6.2.2 过滤性能优化 + +```typescript +// 优化1:使用Map索引,避免每次遍历全部数据 +// 时间复杂度:O(1) 获取 + O(n) 过滤(n是当前账号的数据量,不是全部数据量) + +// 优化2:缓存过滤结果,避免重复计算 +// 切换回相同账号时,直接使用缓存 + +// 优化3:使用useMemo缓存计算结果 +const filteredSessions = useMemo(() => { + return switchAccount(selectedAccountId); +}, [selectedAccountId, searchKeyword, sortBy]); + +// 优化4:增量更新索引(新增数据时) +function addSession(session: ChatSession) { + allSessions.push(session); + const accountId = session.wechatAccountId; + + // 更新索引 + if (!sessionIndex.has(accountId)) { + sessionIndex.set(accountId, []); + } + sessionIndex.get(accountId)!.push(session); + + // 失效缓存 + cacheValid.sessions.set(accountId, false); +} +``` + +### 6.3 联系人列表:分组懒加载策略(核心变化) + +#### 6.3.1 分组列表加载(一次性) + +```typescript +// 加载分组列表(只加载分组信息,不加载联系人数据) +async function loadGroups() { + const result = await getLabelsListByGroup({}); + const groups = result?.list || []; + // groups包含:id, groupName, groupType, count等信息 + // 但不包含contacts数据 + return groups; +} +``` + +#### 6.3.2 分组展开时加载联系人(懒加载) + +```typescript +// 点击展开分组时,加载该分组的联系人 +async function loadGroupContacts( + groupId: number, + groupType: 1 | 2, + accountId: number, + page: number = 1, + limit: number = 50, +) { + const groupKey = `${groupId}_${groupType}_${accountId}`; + + // 1. 检查是否已加载 + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + return groupData.get(groupKey)!.contacts; // 直接返回已加载的数据 + } + + // 2. 设置加载状态 + setGroupLoading(groupKey, true); + + try { + // 3. 根据groupType调用不同的API + let contacts: Contact[] = []; + if (groupType === 1) { + // 好友列表API + const result = await getContactList({ + groupId, // 分组ID + page, + limit, + wechatAccountId: accountId === 0 ? undefined : accountId, + }); + contacts = result?.list || []; + } else if (groupType === 2) { + // 群列表API + const result = await getGroupList({ + groupId, // 分组ID + page, + limit, + wechatAccountId: accountId === 0 ? undefined : accountId, + }); + contacts = result?.list || []; + } + + // 4. 保存数据 + const existingData = groupData.get(groupKey); + const newData: GroupContactData = { + contacts: + page === 1 + ? contacts + : [...(existingData?.contacts || []), ...contacts], + page, + pageSize: limit, + hasMore: contacts.length === limit, + loading: false, + loaded: true, + lastLoadTime: Date.now(), + }; + groupData.set(groupKey, newData); + + return newData.contacts; + } catch (error) { + console.error("加载分组联系人失败:", error); + setGroupLoading(groupKey, false); + throw error; + } +} +``` + +#### 6.3.3 分组内分页加载 + +```typescript +// 分组内滚动到底部时,加载更多 +async function loadMoreGroupContacts( + groupId: number, + groupType: 1 | 2, + accountId: number, +) { + const groupKey = `${groupId}_${groupType}_${accountId}`; + const currentData = groupData.get(groupKey); + + if (!currentData || !currentData.hasMore || currentData.loading) { + return; // 没有更多数据或正在加载 + } + + // 加载下一页 + const nextPage = currentData.page + 1; + await loadGroupContacts( + groupId, + groupType, + accountId, + nextPage, + currentData.pageSize, + ); +} +``` + +### 6.4 搜索功能处理(核心问题) + +#### 6.4.1 搜索策略 + +**问题**:按分组懒加载后,搜索不能只搜索已加载的数据,需要搜索全部数据 + +**解决方案**:搜索时调用API,不依赖本地数据 + +```typescript +// 搜索联系人(需要同时请求好友列表和群列表) +async function searchContacts(keyword: string, accountId: number) { + // 1. 并行请求好友列表和群列表(都传入keyword参数) + const [friendsResult, groupsResult] = await Promise.all([ + // 好友列表API(groupType=1) + getContactList({ + keyword, // 搜索关键词 + wechatAccountId: accountId === 0 ? undefined : accountId, + page: 1, + limit: 100, // 搜索时可能需要加载更多结果 + }), + // 群列表API(groupType=2) + getGroupList({ + keyword, // 搜索关键词 + wechatAccountId: accountId === 0 ? undefined : accountId, + page: 1, + limit: 100, + }), + ]); + + // 2. 合并结果 + const friends = friendsResult?.list || []; + const groups = groupsResult?.list || []; + const allResults = [...friends, ...groups]; + + // 3. 转换为统一的Contact格式 + const contacts = convertToContacts(allResults); + + return contacts; +} +``` + +#### 6.4.2 搜索流程 + +``` +用户输入搜索关键词 + ↓ +1. 设置searchKeyword,进入搜索模式(0ms) + ↓ +2. 显示Loading状态(0-10ms) + ↓ +3. 并行调用API(10-200ms) + - getContactList({ keyword, wechatAccountId, page: 1, limit: 100 }) + - getGroupList({ keyword, wechatAccountId, page: 1, limit: 100 }) + ↓ +4. 合并结果并转换格式(200-220ms) + ↓ +5. 显示搜索结果(220-250ms) +``` + +### 6.5 虚拟滚动数据准备 + +#### 6.5.1 会话列表虚拟滚动 + +```typescript +// 会话列表:虚拟滚动只需要可见区域的数据 +function getVisibleSessions( + filteredSessions: ChatSession[], + scrollTop: number, +) { + const ITEM_HEIGHT = 72; + const CONTAINER_HEIGHT = 600; + const VISIBLE_COUNT = Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + 2; + + const startIndex = Math.floor(scrollTop / ITEM_HEIGHT); + const endIndex = startIndex + VISIBLE_COUNT; + + // 只返回可见区域的10-20条数据 + return filteredSessions.slice(startIndex, endIndex); +} +``` + +#### 6.5.2 联系人列表虚拟滚动(分组内) + +```typescript +// 联系人列表:每个分组独立虚拟滚动 +function getVisibleContacts( + groupData: GroupContactData, + scrollTop: number, + groupHeaderHeight: number = 40, +) { + const ITEM_HEIGHT = 60; + const CONTAINER_HEIGHT = 600; + const VISIBLE_COUNT = Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + 2; + + // 减去分组头部高度 + const adjustedScrollTop = Math.max(0, scrollTop - groupHeaderHeight); + const startIndex = Math.floor(adjustedScrollTop / ITEM_HEIGHT); + const endIndex = startIndex + VISIBLE_COUNT; + + // 只返回可见区域的联系人 + return groupData.contacts.slice(startIndex, endIndex); +} + +// 多个分组时,需要计算每个分组的可见范围 +function getVisibleContactsForAllGroups( + expandedGroups: ContactGroup[], + scrollTop: number, +) { + let currentOffset = 0; + const visibleItems: Array<{ type: "header" | "contact"; data: any }> = []; + + for (const group of expandedGroups) { + const groupKey = getGroupKey(group.id, group.groupType, selectedAccountId); + const groupData = groupDataMap.get(groupKey); + + if (!groupData) continue; + + const groupHeaderHeight = 40; + const groupContentHeight = groupData.contacts.length * 60; + const groupTotalHeight = groupHeaderHeight + groupContentHeight; + + // 判断分组是否在可见区域 + if ( + scrollTop <= currentOffset + groupTotalHeight && + scrollTop + CONTAINER_HEIGHT >= currentOffset + ) { + // 分组头部可见 + if (scrollTop <= currentOffset + groupHeaderHeight) { + visibleItems.push({ type: "header", data: group }); + } + + // 分组内容可见 + const groupScrollTop = scrollTop - currentOffset - groupHeaderHeight; + const visibleContacts = getVisibleContacts( + groupData, + groupScrollTop, + groupHeaderHeight, + ); + visibleItems.push( + ...visibleContacts.map(c => ({ type: "contact", data: c })), + ); + } + + currentOffset += groupTotalHeight; + } + + return visibleItems; +} +``` + +--- + +## 七、渲染优化技术 + +### 7.1 React渲染优化 + +#### 7.1.1 组件优化 + +```typescript +// 1. 使用 React.memo 避免不必要的重渲染 +const SessionItem = React.memo( + ({ session }) => { + // ... + }, + (prev, next) => prev.session.id === next.session.id, +); + +// 2. 使用 useMemo 缓存计算结果 +const filteredSessions = useMemo(() => { + return sessions.filter(s => s.nickname.includes(keyword)); +}, [sessions, keyword]); + +// 3. 使用 useCallback 缓存函数 +const handleClick = useCallback((id: number) => { + // ... +}, []); +``` + +#### 7.1.2 状态优化 + +```typescript +// 1. 细粒度状态订阅 +// ❌ 不推荐:订阅整个store +const { sessions, loading, searchKeyword } = useSessionStore(); + +// ✅ 推荐:只订阅需要的字段 +const sessions = useSessionStore(state => state.sessions); +const loading = useSessionStore(state => state.loading); + +// 2. 合并多个selector +const sessionSelectors = useSessionSelectors(); // 自定义hook合并 +``` + +### 7.2 DOM优化 + +#### 7.2.1 减少DOM节点 + +```typescript +// 虚拟滚动:从10000个DOM节点减少到10-20个 +// 分组折叠:折叠的分组不渲染DOM +// 条件渲染:不在可见区域的不渲染 +``` + +#### 7.2.2 使用CSS优化 + +```css +/* 1. 使用 will-change 提示浏览器优化 */ +.virtual-list-item { + will-change: transform; +} + +/* 2. 使用 contain 限制重绘范围 */ +.virtual-list-container { + contain: layout style paint; +} + +/* 3. 使用 transform 代替 top/left(GPU加速) */ +.virtual-list-item { + transform: translateY(var(--offset)); +} +``` + +### 7.3 事件处理优化 + +```typescript +// 1. 防抖滚动事件 +const handleScroll = useMemo( + () => + debounce((e: Event) => { + // 计算可见范围 + updateVisibleRange(); + }, 16), // 约60fps + [], +); + +// 2. 使用 requestAnimationFrame +const updateVisibleRange = () => { + requestAnimationFrame(() => { + // 更新可见范围 + }); +}; + +// 3. 使用 Intersection Observer(替代滚动事件) +const observer = new IntersectionObserver(entries => { + // 检测可见性变化 +}); +``` + +--- + +## 八、切换账号优化(核心优化) + +### 8.1 切换流程优化 + +``` +用户点击切换账号 + ↓ +1. 立即显示Loading状态(0ms) + ↓ +2. 检查过滤缓存(0-5ms) + - 有缓存且有效 → 直接使用缓存 + - 无缓存或失效 → 继续下一步 + ↓ +3. 使用索引快速过滤(5-20ms) + - accountId === 0 → 返回allSessions + - accountId !== 0 → 从sessionIndex获取(O(1)) + ↓ +4. 应用搜索和排序(如果需要)(20-50ms) + ↓ +5. 缓存过滤结果(50-60ms) + ↓ +6. 计算可见区域(虚拟滚动)(60-70ms) + ↓ +7. 渲染可见区域(70-100ms) +``` + +**性能目标**:切换账号 < 100ms(从点击到显示) + +### 8.2 过滤性能优化策略 + +#### 8.2.1 索引优化 + +```typescript +// 关键优化:使用Map索引,避免遍历全部数据 +// 时间复杂度对比: +// ❌ 旧方案:O(n) - 遍历全部10000条数据 +// ✅ 新方案:O(1) - 直接从索引获取 + +// 性能提升:10000条数据,从100ms降低到1ms +``` + +#### 8.2.2 缓存优化 + +```typescript +// 缓存过滤结果,避免重复计算 +// 场景:用户频繁切换账号 +// 优化:第一次过滤后缓存,后续直接使用缓存 + +// 缓存失效策略: +// 1. 数据更新时:失效对应账号的缓存 +// 2. 搜索/排序变化时:失效缓存,重新计算 +``` + +#### 8.2.3 React渲染优化 + +```typescript +// 使用useMemo缓存过滤结果 +const filteredSessions = useMemo(() => { + return switchAccount(selectedAccountId); +}, [selectedAccountId, searchKeyword, sortBy]); + +// 使用useMemo缓存可见区域数据 +const visibleSessions = useMemo(() => { + return getVisibleSessions(filteredSessions, scrollTop); +}, [filteredSessions, scrollTop]); + +// 避免不必要的重渲染 +const SessionList = React.memo( + ({ sessions }) => { + // ... + }, + (prev, next) => { + return ( + prev.sessions.length === next.sessions.length && + prev.sessions[0]?.id === next.sessions[0]?.id + ); + }, +); +``` + +### 8.3 增量更新优化 + +#### 8.3.1 会话列表增量更新 + +```typescript +// 新增会话时的优化 +function addSession(session: ChatSession) { + // 1. 添加到全部数据 + allSessions.push(session); + + // 2. 更新索引(O(1)) + const accountId = session.wechatAccountId; + if (!sessionIndex.has(accountId)) { + sessionIndex.set(accountId, []); + } + sessionIndex.get(accountId)!.push(session); + + // 3. 失效缓存(如果当前显示的是该账号) + if (selectedAccountId === accountId || selectedAccountId === 0) { + cacheValid.sessions.set(accountId, false); + // 触发重新过滤和渲染 + } +} +``` + +#### 8.3.2 联系人列表增量更新 + +```typescript +// WebSocket收到新联系人时的处理 +function handleNewContact(contact: Contact) { + const { groupId, groupType, wechatAccountId } = contact; + const groupKey = `${groupId}_${groupType}_${wechatAccountId}`; + + // 1. 如果该分组已加载,直接添加到列表 + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + const groupDataItem = groupData.get(groupKey)!; + // 检查是否已存在(避免重复) + const exists = groupDataItem.contacts.some(c => c.id === contact.id); + if (!exists) { + groupDataItem.contacts.push(contact); + // 更新分组数量 + updateGroupCount(groupId, groupType, wechatAccountId, 1); + } + } else { + // 2. 如果该分组未加载,只更新分组数量统计 + updateGroupCount(groupId, groupType, wechatAccountId, 1); + } + + // 3. 如果当前在搜索模式,且匹配搜索关键词,添加到搜索结果 + if (isSearchMode && matchesSearchKeyword(contact, searchKeyword)) { + searchResults.push(contact); + } +} + +// 更新联系人 +function updateContact(contact: Contact) { + const { groupId, groupType, wechatAccountId } = contact; + const groupKey = `${groupId}_${groupType}_${wechatAccountId}`; + + // 1. 如果该分组已加载,更新列表中的数据 + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + const groupDataItem = groupData.get(groupKey)!; + const index = groupDataItem.contacts.findIndex(c => c.id === contact.id); + if (index !== -1) { + groupDataItem.contacts[index] = contact; + } + } + + // 2. 如果当前在搜索模式,更新搜索结果 + if (isSearchMode) { + const searchIndex = searchResults.findIndex(c => c.id === contact.id); + if (searchIndex !== -1) { + searchResults[searchIndex] = contact; + } + } +} + +// 删除联系人 +function deleteContact( + contactId: number, + groupId: number, + groupType: 1 | 2, + accountId: number, +) { + const groupKey = `${groupId}_${groupType}_${accountId}`; + + // 1. 如果该分组已加载,从列表中删除 + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + const groupDataItem = groupData.get(groupKey)!; + groupDataItem.contacts = groupDataItem.contacts.filter( + c => c.id !== contactId, + ); + // 更新分组数量 + updateGroupCount(groupId, groupType, accountId, -1); + } + + // 2. 如果当前在搜索模式,从搜索结果中删除 + if (isSearchMode) { + searchResults = searchResults.filter(c => c.id !== contactId); + } +} + +// 移动联系人到其他分组 +async function moveContactToGroup( + contactId: number, + fromGroupId: number, + toGroupId: number, + groupType: 1 | 2, + accountId: number, +) { + // 1. 调用API移动分组 + await moveGroup({ + type: groupType === 1 ? "friend" : "group", + groupId: toGroupId, + id: contactId, + }); + + // 2. 从原分组移除 + const fromGroupKey = `${fromGroupId}_${groupType}_${accountId}`; + if (groupData.has(fromGroupKey) && groupData.get(fromGroupKey)!.loaded) { + const fromGroupData = groupData.get(fromGroupKey)!; + const contact = fromGroupData.contacts.find(c => c.id === contactId); + if (contact) { + fromGroupData.contacts = fromGroupData.contacts.filter( + c => c.id !== contactId, + ); + updateGroupCount(fromGroupId, groupType, accountId, -1); + } + } + + // 3. 添加到新分组 + const toGroupKey = `${toGroupId}_${groupType}_${accountId}`; + if (groupData.has(toGroupKey) && groupData.get(toGroupKey)!.loaded) { + const toGroupData = groupData.get(toGroupKey)!; + // 需要重新加载该联系人数据(因为groupId已变化) + const updatedContact = await getContactDetail(contactId); + if (updatedContact) { + toGroupData.contacts.push(updatedContact); + updateGroupCount(toGroupId, groupType, accountId, 1); + } + } else { + // 如果新分组未加载,只更新数量统计 + updateGroupCount(toGroupId, groupType, accountId, 1); + } +} + +// 更新分组数量统计 +function updateGroupCount( + groupId: number, + groupType: 1 | 2, + accountId: number, + delta: number, // +1 或 -1 +) { + // 更新groups数组中的count + const group = groups.find(g => g.id === groupId && g.groupType === groupType); + if (group) { + group.count = (group.count || 0) + delta; + } +} +``` + +#### 8.3.3 分组编辑操作处理 + +```typescript +// 新增分组 +async function addGroup(groupData: { + groupName: string; + groupMemo: string; + groupType: 1 | 2; + sort: number; +}) { + // 1. 调用API新增分组 + const result = await addGroupAPI(groupData); + const newGroup: ContactGroup = { + id: result.id, + groupName: result.groupName, + groupType: result.groupType, + count: 0, // 新分组初始数量为0 + }; + + // 2. 更新内存中的分组列表 + groups.push(newGroup); + // 按sort排序 + groups.sort((a, b) => (a.sort || 0) - (b.sort || 0)); + + // 3. 更新本地数据库缓存 + await updateGroupListCache(selectedAccountId, groups); + + // 4. 触发UI更新 + triggerUIUpdate(); +} + +// 更新分组 +async function updateGroup(groupData: { + id: number; + groupName: string; + groupMemo: string; + groupType: 1 | 2; + sort: number; +}) { + // 1. 调用API更新分组 + await updateGroupAPI(groupData); + + // 2. 更新内存中的分组列表 + const index = groups.findIndex(g => g.id === groupData.id); + if (index !== -1) { + groups[index] = { + ...groups[index], + groupName: groupData.groupName, + groupMemo: groupData.groupMemo, + sort: groupData.sort, + }; + // 按sort重新排序 + groups.sort((a, b) => (a.sort || 0) - (b.sort || 0)); + } + + // 3. 更新本地数据库缓存 + await updateGroupListCache(selectedAccountId, groups); + + // 4. 触发UI更新 + triggerUIUpdate(); +} + +// 删除分组 +async function deleteGroup(groupId: number, groupType: 1 | 2) { + // 1. 调用API删除分组 + await deleteGroupAPI(groupId); + + // 2. 从内存中的分组列表删除 + const index = groups.findIndex(g => g.id === groupId && g.groupType === groupType); + if (index !== -1) { + groups.splice(index, 1); + } + + // 3. 清理该分组的所有缓存数据 + // 3.1 清理内存中的分组数据 + const groupKeysToDelete: string[] = []; + groupData.forEach((value, key) => { + if (key.startsWith(`${groupId}_${groupType}_`)) { + groupKeysToDelete.push(key); + } + }); + groupKeysToDelete.forEach(key => { + groupData.delete(key); + }); + + // 3.2 清理本地数据库缓存 + for (const accountId of [0, ...getAllAccountIds()]) { + const groupKey = `${groupId}_${groupType}_${accountId}`; + await clearGroupContactsCache(groupKey); + } + + // 3.3 清理展开状态 + const expandedKeysToDelete: string[] = []; + expandedGroups.forEach(key => { + if (key.startsWith(`${groupId}_${groupType}_`)) { + expandedKeysToDelete.push(key); + } + }); + expandedKeysToDelete.forEach(key => { + expandedGroups.delete(key); + }); + + // 4. 更新本地数据库缓存(分组列表) + await updateGroupListCache(selectedAccountId, groups); + + // 5. 触发UI更新 + triggerUIUpdate(); +} +``` + +#### 8.3.4 联系人右键菜单操作处理 + +```typescript +// 修改联系人备注(联系人右键菜单) +async function updateContactRemark( + contactId: number, + groupId: number, + groupType: 1 | 2, + remark: string, +) { + // 1. 调用API更新备注 + await updateContactAPI({ + id: contactId, + conRemark: remark, + }); + + // 2. 更新内存中的数据 + const groupKey = `${groupId}_${groupType}_${selectedAccountId}`; + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + const groupDataItem = groupData.get(groupKey)!; + const contact = groupDataItem.contacts.find(c => c.id === contactId); + if (contact) { + contact.conRemark = remark; + } + } + + // 3. 更新本地数据库缓存(如果该分组已缓存) + const cacheKey = `groupContacts_${groupKey}_1`; + const cached = await getCache(cacheKey); + if (cached) { + const cachedContact = cached.contacts.find((c: Contact) => c.id === contactId); + if (cachedContact) { + cachedContact.conRemark = remark; + cached.lastUpdate = Date.now(); + await setCache(cacheKey, cached); + } + } + + // 4. 如果当前在搜索模式,更新搜索结果 + if (isSearchMode) { + const searchIndex = searchResults.findIndex(c => c.id === contactId); + if (searchIndex !== -1) { + searchResults[searchIndex].conRemark = remark; + } + } + + // 5. 触发UI更新 + triggerUIUpdate(); +} + +// 移动联系人到其他分组(联系人右键菜单) +async function moveContactToGroup( + contactId: number, + fromGroupId: number, + toGroupId: number, + groupType: 1 | 2, +) { + // 1. 调用API移动分组 + await moveGroup({ + type: groupType === 1 ? "friend" : "group", + groupId: toGroupId, + id: contactId, + }); + + // 2. 从原分组移除 + const fromGroupKey = `${fromGroupId}_${groupType}_${selectedAccountId}`; + if (groupData.has(fromGroupKey) && groupData.get(fromGroupKey)!.loaded) { + const fromGroupData = groupData.get(fromGroupKey)!; + const contact = fromGroupData.contacts.find(c => c.id === contactId); + if (contact) { + // 从原分组删除 + fromGroupData.contacts = fromGroupData.contacts.filter( + c => c.id !== contactId, + ); + // 更新分组数量 + updateGroupCount(fromGroupId, groupType, selectedAccountId, -1); + + // 更新原分组的本地缓存 + const fromCacheKey = `groupContacts_${fromGroupKey}_1`; + const fromCached = await getCache(fromCacheKey); + if (fromCached) { + fromCached.contacts = fromCached.contacts.filter( + (c: Contact) => c.id !== contactId, + ); + fromCached.lastUpdate = Date.now(); + await setCache(fromCacheKey, fromCached); + } + } + } + + // 3. 添加到新分组 + const toGroupKey = `${toGroupId}_${groupType}_${selectedAccountId}`; + if (groupData.has(toGroupKey) && groupData.get(toGroupKey)!.loaded) { + const toGroupData = groupData.get(toGroupKey)!; + // 需要重新加载该联系人数据(因为groupId已变化) + const updatedContact = await getContactDetail(contactId); + if (updatedContact) { + toGroupData.contacts.push(updatedContact); + updateGroupCount(toGroupId, groupType, selectedAccountId, 1); + + // 更新新分组的本地缓存 + const toCacheKey = `groupContacts_${toGroupKey}_1`; + const toCached = await getCache(toCacheKey); + if (toCached) { + toCached.contacts.push(updatedContact); + toCached.lastUpdate = Date.now(); + await setCache(toCacheKey, toCached); + } + } + } else { + // 如果新分组未加载,只更新数量统计 + updateGroupCount(toGroupId, groupType, selectedAccountId, 1); + } + + // 4. 如果当前在搜索模式,更新搜索结果 + if (isSearchMode) { + const searchIndex = searchResults.findIndex(c => c.id === contactId); + if (searchIndex !== -1) { + // 更新联系人的groupId + searchResults[searchIndex].groupId = toGroupId; + } + } + + // 5. 触发UI更新 + triggerUIUpdate(); +} +``` + +--- + +## 九、右键菜单操作处理 + +### 9.1 分组右键菜单操作 + +**场景**:在联系人列表的分组上右键 + +**操作**: +1. **新增分组**:调用 `addGroup` API +2. **编辑分组**:调用 `updateGroup` API +3. **删除分组**:调用 `deleteGroup` API + +**处理流程**: +```typescript +// 分组右键菜单操作 +interface GroupContextMenu { + // 新增分组 + onAddGroup: (groupType: 1 | 2) => Promise; + + // 编辑分组 + onEditGroup: (group: ContactGroup) => Promise; + + // 删除分组 + onDeleteGroup: (groupId: number, groupType: 1 | 2) => Promise; +} +``` + +### 9.2 联系人右键菜单操作 + +**场景**:在具体某个联系人上右键 + +**操作**: +1. **修改备注**:调用更新联系人API,更新 `conRemark` 字段 +2. **移动分组**:调用 `moveGroup` API,将联系人移动到其他分组 + +**处理流程**: +```typescript +// 联系人右键菜单操作 +interface ContactContextMenu { + // 修改备注 + onUpdateRemark: ( + contactId: number, + groupId: number, + groupType: 1 | 2, + remark: string, + ) => Promise; + + // 移动分组 + onMoveToGroup: ( + contactId: number, + fromGroupId: number, + toGroupId: number, + groupType: 1 | 2, + ) => Promise; +} +``` + +### 9.3 右键菜单操作对缓存的影响 + +#### 9.3.1 分组操作对缓存的影响 + +| 操作 | 影响范围 | 缓存处理 | +|------|---------|---------| +| 新增分组 | 分组列表 | 更新分组列表缓存 | +| 编辑分组 | 分组列表 | 更新分组列表缓存 | +| 删除分组 | 分组列表 + 该分组的所有数据 | 清理该分组的所有缓存(内存+本地数据库) | + +#### 9.3.2 联系人操作对缓存的影响 + +| 操作 | 影响范围 | 缓存处理 | +|------|---------|---------| +| 修改备注 | 当前分组 + 搜索结果 | 更新对应分组的缓存,更新搜索结果 | +| 移动分组 | 原分组 + 新分组 | 从原分组缓存删除,添加到新分组缓存(如果已加载) | + +--- + +## 十、技术选型 + +### 9.1 虚拟滚动库 + +#### 方案1:react-window(推荐) + +**优点**: + +- 轻量级(2KB) +- 性能优秀 +- API简单 +- 支持固定高度和动态高度 + +**适用场景**:会话列表、好友列表 + +```typescript +import { FixedSizeList, VariableSizeList } from "react-window"; +``` + +#### 方案2:react-virtualized + +**优点**: + +- 功能丰富 +- 支持AutoSizer +- 支持Grid布局 + +**缺点**: + +- 体积较大(~50KB) +- 维护较少 + +#### 方案3:自实现虚拟滚动 + +**优点**: + +- 完全可控 +- 无依赖 +- 可定制化 + +**缺点**: + +- 开发成本高 +- 需要处理各种边界情况 + +### 9.2 状态管理 + +**继续使用Zustand**: + +- 轻量级 +- 性能优秀 +- 支持selector优化 +- 已集成到项目 + +### 9.3 数据缓存 + +**IndexedDB (Dexie)**: + +- 已集成 +- 支持复杂查询 +- 支持事务 +- 性能优秀 + +--- + +## 十、实施步骤 + +### 阶段1:基础架构搭建(1-2周) + +1. **创建新的Store结构** + - WeChatAccountStore + - SessionStore(新) + - ContactStore(优化) + +2. **实现缓存层** + - Memory Cache + - IndexedDB缓存优化 + - 缓存更新策略 + +3. **数据迁移** + - 迁移现有数据到新结构 + - 保持向后兼容 + +### 阶段2:虚拟滚动实现(2-3周) + +1. **会话列表虚拟滚动** + - 使用react-window实现 + - 固定高度虚拟滚动 + - 滚动加载更多 + +2. **好友列表虚拟滚动** + - 分组虚拟滚动 + - 动态高度处理 + - 分组展开/折叠优化 + +3. **性能测试和优化** + - 大数据量测试(10000+条) + - 滚动性能测试 + - 内存占用测试 + +### 阶段3:切换优化(1周) + +1. **切换流程优化** + - 预加载策略 + - 缓存管理 + - Loading状态优化 + +2. **用户体验优化** + - 平滑过渡动画 + - 骨架屏加载 + - 错误处理 + +### 阶段4:测试和优化(1-2周) + +1. **性能测试** + - 大数据量场景测试 + - 切换性能测试 + - 内存泄漏测试 + +2. **用户体验测试** + - 交互流畅度 + - 加载速度 + - 错误处理 + +--- + +## 十一、性能指标 + +### 11.1 目标性能指标 + +``` +会话列表: +- 初始渲染:< 200ms(10000条数据,一次性加载) +- 滚动帧率:≥ 60fps(虚拟滚动,只渲染10-20条) +- 切换账号:< 100ms(使用索引过滤,O(1)获取) +- 过滤性能:< 50ms(10000条数据,使用索引) +- 内存占用:< 50MB(10000条数据) + +好友列表: +- 初始渲染:< 300ms(50000条数据,一次性加载) +- 分组展开:< 50ms(使用索引,O(1)获取) +- 滚动帧率:≥ 60fps(虚拟滚动,只渲染10-20条) +- 切换账号:< 150ms(使用索引过滤,O(1)获取) +- 过滤性能:< 100ms(50000条数据,使用索引) +- 内存占用:< 100MB(50000条数据) +``` + +### 11.2 监控指标 + +```typescript +// 性能监控 +1. 渲染时间:使用Performance API +2. 内存占用:使用performance.memory +3. 帧率:使用requestAnimationFrame +4. 网络请求:监控API调用次数和耗时 +``` + +--- + +## 十二、关键优化点总结 + +### 12.1 数据层优化 + +✅ **索引结构**:使用Map索引,O(1)时间复杂度获取数据 +✅ **过滤缓存**:缓存过滤结果,避免重复计算 +✅ **增量更新索引**:新增数据时只更新索引,不重新构建 +✅ **数据一次性加载**:全部数据加载到内存,切换时只过滤不重新加载 + +### 12.2 渲染层优化 + +✅ **虚拟滚动**:只渲染可见区域,减少DOM节点 +✅ **固定高度**:会话列表使用固定高度,性能最优 +✅ **动态高度优化**:好友列表使用预估高度+动态调整 +✅ **分组懒加载**:未展开的分组不加载数据 + +### 12.3 状态管理优化 + +✅ **细粒度订阅**:只订阅需要的字段 +✅ **状态分离**:数据状态与UI状态分离 +✅ **计算缓存**:使用useMemo缓存计算结果 +✅ **函数缓存**:使用useCallback缓存函数 + +### 12.4 切换优化 + +✅ **索引过滤**:使用Map索引,O(1)获取,避免遍历全部数据 +✅ **结果缓存**:缓存过滤结果,切换回相同账号时直接使用 +✅ **Memo优化**:使用useMemo缓存过滤和可见区域计算结果 +✅ **增量更新**:新增数据时只更新索引,不重新构建全部索引 + +### 12.5 搜索优化 + +✅ **API搜索**:搜索时调用API,不依赖已加载的分组数据 +✅ **并行请求**:同时请求好友列表和群列表API,提高搜索速度 +✅ **搜索结果独立**:搜索结果与分组数据分离,互不影响 +✅ **实时更新**:WebSocket收到新联系人时,自动更新搜索结果(如果匹配) + +### 12.6 数据同步优化 + +✅ **分组数据更新**:WebSocket收到新联系人时,实时更新对应分组的数据 +✅ **分组数量统计**:联系人变化时,实时更新分组数量统计 +✅ **联系人操作**:添加、删除、修改联系人时,同步更新分组数据和搜索结果 +✅ **分组编辑操作**:新增、删除、编辑分组时,同步更新分组列表、清理相关缓存、更新UI +✅ **联系人右键操作**:修改备注、移动分组时,同步更新对应分组的缓存和搜索结果 + +--- + +## 十三、风险与应对 + +### 13.1 技术风险 + +**风险1:虚拟滚动兼容性问题** + +- **应对**:充分测试各种浏览器和设备 +- **降级方案**:大数据量时提示用户,或使用分页加载 + +**风险2:动态高度计算不准确** + +- **应对**:使用Intersection Observer动态测量 +- **降级方案**:使用固定高度或预估高度 + +**风险3:内存占用过高** + +- **应对**:限制Memory Cache大小,使用LRU策略 +- **监控**:实时监控内存占用,超过阈值时清理 + +### 13.2 业务风险 + +**风险1:数据不一致** + +- **应对**:版本号机制,定期全量同步 +- **监控**:数据校验,异常时提示用户 + +**风险2:切换体验差** + +- **应对**:预加载策略,骨架屏加载 +- **优化**:优化缓存策略,减少加载时间 + +--- + +## 十四、本地数据库缓存策略详解 + +### 14.1 缓存策略的两个场景 + +**重要说明**:缓存策略包含**两个场景**,不是单一场景: + +#### 场景1:初始化加载时的缓存策略 + +**时机**:页面首次打开、刷新页面、切换账号时 + +**目的**:快速显示数据,减少loading时间 + +**流程**: + +``` +页面打开/刷新 + ↓ +1. 检查本地数据库是否有缓存 + ├─ 有缓存且有效(TTL内) → 立即显示缓存数据,后台调用API更新 + └─ 无缓存或失效 → 显示loading,调用API加载,加载完成后缓存 + ↓ +2. 后台更新(如果有缓存) + - 调用API获取最新数据 + - 对比缓存数据,更新差异 + - 更新缓存 + - 静默更新UI(不显示loading) +``` + +**示例代码**: + +```typescript +// 初始化加载分组列表 +async function initGroupList(accountId: number) { + // 1. 先检查本地缓存 + const cacheKey = `groupList_${accountId}`; + const cached = await getCache(cacheKey); + + if (cached && Date.now() - cached.lastUpdate < 30 * 60 * 1000) { + // 缓存有效,立即显示(不显示loading) + setGroups(cached.groups); + // 后台更新(不阻塞UI) + updateGroupListInBackground(accountId); + return; + } + + // 2. 缓存无效,调用API(显示loading) + setLoading(true); + const groups = await getLabelsListByGroup({ accountId }); + setGroups(groups); + // 保存到缓存 + await setCache(cacheKey, { groups, lastUpdate: Date.now() }); + setLoading(false); +} +``` + +#### 场景2:WebSocket实时更新时的缓存策略 + +**时机**:聊天过程中,WebSocket收到新数据时(新联系人、新消息等) + +**目的**:实时更新数据,保证数据一致性 + +**流程**: + +``` +WebSocket收到新数据 + ↓ +1. 更新内存中的数据(Store状态) + - 更新对应分组的数据 + - 更新分组统计 + - 更新搜索结果(如果匹配) + ↓ +2. 同步更新本地数据库缓存 + - 更新对应分组的缓存 + - 更新分组统计缓存 + - 不更新搜索缓存(搜索不缓存) + ↓ +3. 触发UI更新 + - React自动检测状态变化 + - 重新渲染相关组件 +``` + +**示例代码**: + +```typescript +// WebSocket收到新联系人时 +function handleWebSocketNewContact(contact: Contact) { + const { groupId, groupType, wechatAccountId } = contact; + const groupKey = `${groupId}_${groupType}_${wechatAccountId}`; + + // 1. 更新内存中的数据(Store) + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + const groupDataItem = groupData.get(groupKey)!; + const exists = groupDataItem.contacts.some(c => c.id === contact.id); + if (!exists) { + groupDataItem.contacts.push(contact); + } + } + + // 2. 更新本地数据库缓存(如果该分组已缓存) + const cacheKey = `groupContacts_${groupKey}_1`; // 第一页 + const cached = await getCache(cacheKey); + if (cached) { + // 如果第一页已缓存,添加到缓存中 + cached.contacts.push(contact); + cached.lastUpdate = Date.now(); + await setCache(cacheKey, cached); + } + + // 3. 更新分组统计缓存 + await updateGroupStatsCache(wechatAccountId, groupId, groupType, 1); + + // 4. 如果当前在搜索模式,且匹配搜索关键词,添加到搜索结果 + if (isSearchMode && matchesSearchKeyword(contact, searchKeyword)) { + searchResults.push(contact); + } + + // 5. 触发UI更新(React自动检测状态变化) +} +``` + +### 14.2 两种场景的配合 + +**完整的缓存策略**: + +```typescript +interface CacheStrategy { + // 场景1:初始化加载 - 先读缓存,后台更新 + initLoad: () => Promise; + + // 场景2:WebSocket更新 - 实时更新内存和缓存 + websocketUpdate: (data: any) => void; + + // 场景3:定期清理 - 清理过期缓存 + periodicUpdate: () => void; +} +``` + +**配合效果**: + +- ✅ **初始化时**:快速显示缓存数据,用户体验好 +- ✅ **运行时**:WebSocket实时更新,数据始终最新 +- ✅ **持久化**:缓存更新后,刷新页面也能看到最新数据 + +### 14.3 缓存策略总结表 + +| 数据类型 | 初始化策略 | WebSocket更新策略 | TTL | 价值 | +| ---------- | ------------------ | -------------------------------- | ------ | ---------- | +| 会话列表 | 先读缓存,后台更新 | 实时更新内存和缓存 | 无限制 | ⭐⭐⭐⭐⭐ | +| 分组列表 | 先读缓存,后台更新 | 实时更新内存和缓存(如果有推送) | 30分钟 | ⭐⭐⭐ | +| 分组联系人 | 先读缓存,后台更新 | 实时更新内存和缓存 | 1小时 | ⭐⭐⭐⭐⭐ | +| 分组统计 | 先读缓存,后台更新 | 实时更新内存和缓存 | 30分钟 | ⭐⭐⭐ | +| 搜索结果 | 直接调用API | 实时更新内存(不缓存) | - | - | + +**关键点**: + +- **初始化策略**:页面打开时,先检查缓存,有缓存立即显示,后台更新 +- **WebSocket更新策略**:收到新数据时,同时更新内存(Store)和本地数据库缓存 +- **分组编辑策略**:新增/编辑分组时更新缓存,删除分组时清理相关缓存 +- **两种策略配合**:初始化时快速显示,运行时实时更新,保证数据既快速又最新 + +### 14.4 分组编辑操作的缓存处理 + +#### 14.4.1 新增分组 + +```typescript +// 新增分组后的处理 +async function handleAddGroup(groupData: { + groupName: string; + groupMemo: string; + groupType: 1 | 2; + sort: number; +}) { + // 1. 调用API + const result = await addGroup(groupData); + const newGroup: ContactGroup = { + id: result.id, + groupName: result.groupName, + groupType: result.groupType, + count: 0, // 新分组初始数量为0 + }; + + // 2. 更新内存中的分组列表 + groups.push(newGroup); + groups.sort((a, b) => (a.sort || 0) - (b.sort || 0)); + + // 3. 更新本地数据库缓存(分组列表) + await updateGroupListCache(selectedAccountId, groups); + + // 4. 触发UI更新 + triggerUIUpdate(); +} +``` + +#### 14.4.2 编辑分组 + +```typescript +// 编辑分组后的处理 +async function handleUpdateGroup(groupData: { + id: number; + groupName: string; + groupMemo: string; + groupType: 1 | 2; + sort: number; +}) { + // 1. 调用API + await updateGroup(groupData); + + // 2. 更新内存中的分组列表 + const index = groups.findIndex(g => g.id === groupData.id); + if (index !== -1) { + groups[index] = { + ...groups[index], + groupName: groupData.groupName, + groupMemo: groupData.groupMemo, + sort: groupData.sort, + }; + groups.sort((a, b) => (a.sort || 0) - (b.sort || 0)); + } + + // 3. 更新本地数据库缓存(分组列表) + await updateGroupListCache(selectedAccountId, groups); + + // 4. 触发UI更新 + triggerUIUpdate(); +} +``` + +#### 14.4.3 删除分组(重要:需要清理相关缓存) + +```typescript +// 删除分组后的处理(重要:需要清理相关缓存) +async function handleDeleteGroup(groupId: number, groupType: 1 | 2) { + // 1. 调用API + await deleteGroup(groupId); + + // 2. 从内存中的分组列表删除 + const index = groups.findIndex(g => g.id === groupId && g.groupType === groupType); + if (index !== -1) { + groups.splice(index, 1); + } + + // 3. 清理该分组的所有缓存数据(重要) + // 3.1 清理内存中的分组联系人数据 + const groupKeysToDelete: string[] = []; + groupData.forEach((value, key) => { + if (key.startsWith(`${groupId}_${groupType}_`)) { + groupKeysToDelete.push(key); + } + }); + groupKeysToDelete.forEach(key => { + groupData.delete(key); + }); + + // 3.2 清理本地数据库缓存(所有账号的该分组数据) + for (const accountId of [0, ...getAllAccountIds()]) { + const groupKey = `${groupId}_${groupType}_${accountId}`; + await clearGroupContactsCache(groupKey); + } + + // 3.3 清理展开状态 + const expandedKeysToDelete: string[] = []; + expandedGroups.forEach(key => { + if (key.startsWith(`${groupId}_${groupType}_`)) { + expandedKeysToDelete.push(key); + } + }); + expandedKeysToDelete.forEach(key => { + expandedGroups.delete(key); + }); + + // 4. 更新本地数据库缓存(分组列表) + await updateGroupListCache(selectedAccountId, groups); + + // 5. 触发UI更新 + triggerUIUpdate(); +} +``` + +**关键点**: +- ✅ **新增分组**:只需更新分组列表缓存,新分组初始数量为0 +- ✅ **编辑分组**:只需更新分组列表缓存,按sort重新排序 +- ✅ **删除分组**:需要清理该分组的所有相关缓存(内存数据、本地数据库缓存、展开状态),避免内存泄漏和数据不一致 + +--- + +## 十五、总结 + +新架构通过以下核心设计解决大数据量渲染和切换问题: + +### 会话列表优化 + +1. **索引结构**:使用Map按账号ID索引,O(1)时间复杂度获取数据,避免遍历全部数据 +2. **过滤缓存**:缓存过滤结果,切换回相同账号时直接使用,避免重复计算 +3. **虚拟滚动**:只渲染可见区域的10-20条数据,大幅减少DOM节点(从10000个减少到20个) +4. **一次性加载**:全部数据加载到内存,切换时只过滤不重新加载,响应速度快 +5. **增量更新**:新增数据时只更新索引,不重新构建全部索引 + +### 联系人列表优化 + +1. **分组懒加载**:点击展开分组时才加载数据,不一次性加载全部 +2. **分页加载**:分组内支持分页,避免一次性加载大量数据 +3. **按账号缓存**:切换账号时,保留已加载的数据,按账号ID组织 +4. **虚拟滚动**:每个分组独立虚拟滚动,只渲染可见区域 +5. **搜索功能**:搜索时调用API,同时请求好友和群列表,不依赖已加载的分组数据 +6. **数据同步**:WebSocket收到新联系人时,实时更新对应分组的数据和搜索结果 +7. **分组编辑**:新增、删除、编辑分组时,同步更新分组列表、清理相关缓存、更新UI + +### 性能提升 + +**会话列表**: + +- 切换账号:从300-500ms降低到100ms(使用索引) +- 过滤性能:从100-200ms降低到50ms(使用索引+缓存) +- 渲染性能:从10000个DOM节点减少到20个(虚拟滚动) + +**联系人列表**: + +- 分组展开:< 200ms(首次加载第一页50条数据) +- 搜索功能:< 250ms(并行请求好友和群列表API) +- 切换账号(无展开分组):< 20ms(只显示分组列表) +- 切换账号(有缓存):< 50ms(直接显示缓存数据) +- 内存占用:按需占用(只加载展开的分组数据) + +通过这些优化,即使面对数万条数据,切换账号、搜索和渲染都能保持流畅的交互体验。 + +### 本地数据库缓存策略 + +**保留本地数据库,但改变使用方式**: + +- ✅ **会话列表**:全量缓存,WebSocket实时更新 +- ✅ **分组列表**:缓存30分钟,快速显示分组信息 +- ✅ **分组联系人**:按需缓存1小时,切换账号时快速显示 +- ✅ **分组统计**:缓存30分钟,快速显示统计数量 +- ❌ **搜索结果**:不缓存,直接调用API保证实时性 + +**优势**: + +- 提升加载速度:缓存可以大幅减少API调用 +- 优化用户体验:减少loading时间,支持离线访问 +- 切换账号优化:切换账号时快速显示已缓存的数据 +- 存储空间优化:只缓存用户实际查看的数据,不缓存全部数据 diff --git a/Touchkebao/提示词/存客宝架构改造计划.md b/Touchkebao/提示词/存客宝架构改造计划.md new file mode 100644 index 00000000..84c2d514 --- /dev/null +++ b/Touchkebao/提示词/存客宝架构改造计划.md @@ -0,0 +1,780 @@ +# 存客宝新架构改造计划 + +## 一、改造概述 + +### 1.1 改造目标 + +根据新架构设计,对现有代码进行系统性改造,解决大数据量渲染和切换性能问题。 + +**核心目标**: +- ✅ 会话列表切换账号 < 100ms(使用索引,O(1)获取) +- ✅ 联系人列表按分组懒加载,首次展开 < 200ms +- ✅ 虚拟滚动,只渲染可见区域(10-20条) +- ✅ 搜索功能改为API驱动,不依赖本地数据 +- ✅ 支持分组和联系人的右键菜单操作 + +### 1.2 改造范围 + +| 模块 | 当前状态 | 改造后状态 | 优先级 | +|------|---------|-----------|--------| +| SessionStore | 基础状态管理 | 索引+过滤缓存+虚拟滚动 | P0 | +| ContactStore | 全量加载 | 分组懒加载+分页+虚拟滚动 | P0 | +| MessageList组件 | 普通列表 | 虚拟滚动列表 | P0 | +| WechatFriends组件 | 全量加载 | 分组懒加载+虚拟滚动 | P0 | +| 搜索功能 | 本地搜索 | API搜索(并行请求) | P0 | +| 右键菜单 | 无 | 分组+联系人右键菜单 | P1 | +| 缓存策略 | 全量同步 | 按需缓存+TTL | P1 | +| WeChatAccountStore | 无独立Store | 新增独立Store | P1 | + +--- + +## 二、改造阶段规划 + +### 阶段1:基础架构搭建(2-3周) + +**目标**:搭建新的Store结构和数据索引系统 + +#### 1.1 创建WeChatAccountStore(1-2天) + +**文件**:`src/store/module/weChat/account.ts` + +**任务清单**: +- [ ] 创建WeChatAccountStore,管理微信账号列表 +- [ ] 实现selectedAccountId状态(0表示"全部") +- [ ] 实现账号状态管理(在线状态、最后同步时间) +- [ ] 实现账号切换方法 + +**代码示例**: +```typescript +interface WeChatAccountState { + accountList: WeChatAccount[]; + selectedAccountId: number; // 0表示"全部" + accountStatusMap: Map; + + setAccountList: (accounts: WeChatAccount[]) => void; + setSelectedAccount: (accountId: number) => void; + updateAccountStatus: (accountId: number, status: Partial) => void; +} +``` + +**依赖关系**: +- 依赖:无 +- 被依赖:SessionStore, ContactStore + +--- + +#### 1.2 改造SessionStore(3-4天) + +**文件**:`src/store/module/weChat/message.ts` + +**任务清单**: +- [ ] 添加allSessions字段(一次性加载全部数据) +- [ ] 实现sessionIndex(Map) +- [ ] 实现filteredSessionsCache(过滤结果缓存) +- [ ] 实现buildIndexes方法(构建索引) +- [ ] 实现switchAccount方法(使用索引快速过滤) +- [ ] 实现addSession方法(增量更新索引) +- [ ] 保留原有接口,向后兼容 + +**关键改造点**: +```typescript +// 1. 添加索引结构 +sessionIndex: Map; +filteredSessionsCache: Map; +cacheValid: Map; + +// 2. 构建索引方法 +buildIndexes: (sessions: ChatSession[]) => void; + +// 3. 快速切换账号 +switchAccount: (accountId: number) => ChatSession[]; + +// 4. 增量更新 +addSession: (session: ChatSession) => void; +``` + +**测试要点**: +- [ ] 索引构建正确性(10000条数据) +- [ ] 切换账号性能(< 100ms) +- [ ] 增量更新索引正确性 +- [ ] 缓存失效机制正确性 + +**依赖关系**: +- 依赖:WeChatAccountStore +- 被依赖:MessageList组件 + +--- + +#### 1.3 改造ContactStore(5-7天) + +**文件**:`src/store/module/weChat/contacts.ts` + +**任务清单**: +- [ ] 重构Store结构,支持分组懒加载 +- [ ] 实现groups字段(分组列表,一次性加载) +- [ ] 实现expandedGroups(展开的分组) +- [ ] 实现groupData(Map) +- [ ] 实现loadGroups方法(加载分组列表) +- [ ] 实现loadGroupContacts方法(懒加载分组联系人) +- [ ] 实现loadMoreGroupContacts方法(分页加载) +- [ ] 实现searchContacts方法(API搜索,并行请求) +- [ ] 实现switchAccount方法(切换账号,重新加载展开的分组) +- [ ] 实现分组编辑方法(addGroup, updateGroup, deleteGroup) +- [ ] 实现联系人操作方法(updateContactRemark, moveContactToGroup) + +**关键改造点**: +```typescript +// 1. 新的Store结构 +interface ContactStoreState { + groups: ContactGroup[]; // 分组列表 + expandedGroups: Set; // 展开的分组 + groupData: Map; // 分组数据(懒加载) + searchKeyword: string; + isSearchMode: boolean; + searchResults: Contact[]; // API搜索结果 + // ... +} + +// 2. 懒加载方法 +loadGroupContacts: (groupId, groupType, page, limit) => Promise; + +// 3. 搜索方法(API驱动) +searchContacts: (keyword: string) => Promise; + +// 4. 切换账号方法 +switchAccount: (accountId: number) => Promise; +``` + +**测试要点**: +- [ ] 分组列表加载正确性 +- [ ] 分组懒加载正确性(首次展开 < 200ms) +- [ ] 分页加载正确性 +- [ ] 搜索功能正确性(并行请求) +- [ ] 切换账号正确性(重新加载展开的分组) +- [ ] 分组编辑操作正确性 +- [ ] 联系人操作正确性 + +**依赖关系**: +- 依赖:WeChatAccountStore, API模块(getContactList, getGroupList, group.ts) +- 被依赖:WechatFriends组件 + +--- + +#### 1.4 实现数据索引工具(2-3天) + +**文件**:`src/utils/dataIndex.ts` + +**任务清单**: +- [ ] 实现DataIndexManager类 +- [ ] 实现buildIndexes方法(构建会话和联系人索引) +- [ ] 实现getSessionsByAccount方法(O(1)获取) +- [ ] 实现getContactsByAccount方法(O(1)获取) +- [ ] 实现增量更新索引方法 + +**代码示例**: +```typescript +class DataIndexManager { + sessionIndex: Map; + contactIndex: Map; + + buildIndexes(sessions: ChatSession[], contacts: Contact[]): void; + getSessionsByAccount(accountId: number): ChatSession[]; + getContactsByAccount(accountId: number): Contact[]; + addSession(session: ChatSession): void; + addContact(contact: Contact): void; +} +``` + +**测试要点**: +- [ ] 索引构建性能(10000条数据 < 100ms) +- [ ] 索引查询性能(O(1)时间复杂度) +- [ ] 增量更新正确性 + +**依赖关系**: +- 依赖:无 +- 被依赖:SessionStore, ContactStore + +--- + +### 阶段2:虚拟滚动实现(2-3周) + +**目标**:实现会话列表和联系人列表的虚拟滚动 + +#### 2.1 会话列表虚拟滚动(4-5天) + +**文件**: +- `src/components/VirtualSessionList/index.tsx`(新建) +- `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx`(改造) + +**任务清单**: +- [ ] 安装react-window依赖 +- [ ] 创建VirtualSessionList组件 +- [ ] 实现固定高度虚拟滚动(ITEM_HEIGHT = 72px) +- [ ] 实现可见区域计算逻辑 +- [ ] 实现滚动事件处理(防抖) +- [ ] 改造MessageList组件,使用VirtualSessionList +- [ ] 实现滚动加载更多(如果需要) +- [ ] 优化SessionItem组件(React.memo) + +**关键实现**: +```typescript +// 使用react-window的FixedSizeList +import { FixedSizeList } from 'react-window'; + + + {SessionItem} + +``` + +**测试要点**: +- [ ] 虚拟滚动性能(10000条数据,60fps) +- [ ] 滚动流畅度 +- [ ] 可见区域计算正确性 +- [ ] 内存占用(< 50MB) + +**依赖关系**: +- 依赖:SessionStore, react-window +- 被依赖:无 + +--- + +#### 2.2 联系人列表虚拟滚动(5-7天) + +**文件**: +- `src/components/VirtualContactList/index.tsx`(新建) +- `src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx`(改造) + +**任务清单**: +- [ ] 创建VirtualContactList组件 +- [ ] 实现分组虚拟滚动(每个分组独立) +- [ ] 实现动态高度处理(分组头部+联系人列表) +- [ ] 实现分组展开/折叠时的虚拟滚动调整 +- [ ] 实现分组内分页加载(滚动到底部) +- [ ] 改造WechatFriends组件,使用VirtualContactList +- [ ] 优化ContactItem组件(React.memo) + +**关键实现**: +```typescript +// 使用react-window的VariableSizeList(动态高度) +import { VariableSizeList } from 'react-window'; + +// 计算每个分组的总高度 +const getGroupHeight = (group: ContactGroup) => { + const headerHeight = 40; + const contactHeight = 60; + const contactCount = groupData.get(groupKey)?.contacts.length || 0; + return headerHeight + contactCount * contactHeight; +}; +``` + +**测试要点**: +- [ ] 分组虚拟滚动性能(多个分组,60fps) +- [ ] 动态高度计算正确性 +- [ ] 分组展开/折叠流畅度 +- [ ] 分页加载正确性 +- [ ] 内存占用(按需占用) + +**依赖关系**: +- 依赖:ContactStore, react-window +- 被依赖:无 + +--- + +### 阶段3:搜索和懒加载功能(1-2周) + +**目标**:实现API驱动的搜索和分组懒加载 + +#### 3.1 搜索功能改造(3-4天) + +**文件**: +- `src/store/module/weChat/contacts.ts`(改造) +- `src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx`(改造) + +**任务清单**: +- [ ] 改造searchContacts方法,改为API调用 +- [ ] 实现并行请求(getContactList + getGroupList) +- [ ] 实现搜索结果合并和格式转换 +- [ ] 实现搜索Loading状态 +- [ ] 实现搜索模式切换(isSearchMode) +- [ ] 改造UI,显示搜索结果 +- [ ] 实现清空搜索功能 + +**关键实现**: +```typescript +// 并行请求好友和群列表 +const [friendsResult, groupsResult] = await Promise.all([ + getContactList({ keyword, wechatAccountId, page: 1, limit: 100 }), + getGroupList({ keyword, wechatAccountId, page: 1, limit: 100 }), +]); + +// 合并结果 +const allResults = [...friendsResult.list, ...groupsResult.list]; +``` + +**测试要点**: +- [ ] 搜索API调用正确性(并行请求) +- [ ] 搜索结果合并正确性 +- [ ] 搜索性能(< 250ms) +- [ ] 搜索模式切换正确性 + +**依赖关系**: +- 依赖:API模块(getContactList, getGroupList) +- 被依赖:WechatFriends组件 + +--- + +#### 3.2 分组懒加载实现(4-5天) + +**文件**: +- `src/store/module/weChat/contacts.ts`(改造) +- `src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx`(改造) + +**任务清单**: +- [ ] 实现loadGroups方法(加载分组列表) +- [ ] 实现toggleGroup方法(切换分组展开/折叠) +- [ ] 实现loadGroupContacts方法(懒加载分组联系人) +- [ ] 根据groupType调用不同API(1=好友,2=群) +- [ ] 实现分页加载(limit + page参数) +- [ ] 实现加载状态管理 +- [ ] 实现缓存策略(已加载的分组不重复加载) +- [ ] 改造UI,点击展开时加载数据 + +**关键实现**: +```typescript +// 点击展开分组时 +async function toggleGroup(groupId: number, groupType: 1 | 2) { + const groupKey = `${groupId}_${groupType}_${selectedAccountId}`; + + if (expandedGroups.has(groupKey)) { + // 折叠 + expandedGroups.delete(groupKey); + } else { + // 展开 - 懒加载 + expandedGroups.add(groupKey); + await loadGroupContacts(groupId, groupType, 1, 50); + } +} +``` + +**测试要点**: +- [ ] 分组列表加载正确性 +- [ ] 懒加载正确性(首次展开 < 200ms) +- [ ] 分页加载正确性 +- [ ] 缓存机制正确性(不重复加载) +- [ ] 切换账号正确性(重新加载展开的分组) + +**依赖关系**: +- 依赖:API模块(getContactList, getGroupList) +- 被依赖:WechatFriends组件 + +--- + +### 阶段4:右键菜单和操作功能(1-2周) + +**目标**:实现分组和联系人的右键菜单操作 + +#### 4.1 分组右键菜单(3-4天) + +**文件**: +- `src/components/GroupContextMenu/index.tsx`(新建) +- `src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx`(改造) + +**任务清单**: +- [ ] 创建GroupContextMenu组件 +- [ ] 实现新增分组功能(调用addGroup API) +- [ ] 实现编辑分组功能(调用updateGroup API) +- [ ] 实现删除分组功能(调用deleteGroup API) +- [ ] 实现右键菜单显示逻辑 +- [ ] 实现分组编辑表单(Modal) +- [ ] 更新ContactStore,同步更新分组列表和缓存 + +**关键实现**: +```typescript +// 右键菜单操作 +const menuItems = [ + { key: 'add', label: '新增分组', onClick: handleAddGroup }, + { key: 'edit', label: '编辑分组', onClick: handleEditGroup }, + { key: 'delete', label: '删除分组', onClick: handleDeleteGroup }, +]; +``` + +**测试要点**: +- [ ] 新增分组正确性(更新分组列表和缓存) +- [ ] 编辑分组正确性(更新分组列表和缓存) +- [ ] 删除分组正确性(清理相关缓存) +- [ ] UI更新正确性 + +**依赖关系**: +- 依赖:API模块(group.ts), ContactStore +- 被依赖:WechatFriends组件 + +--- + +#### 4.2 联系人右键菜单(3-4天) + +**文件**: +- `src/components/ContactContextMenu/index.tsx`(新建) +- `src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx`(改造) + +**任务清单**: +- [ ] 创建ContactContextMenu组件 +- [ ] 实现修改备注功能(调用updateContact API) +- [ ] 实现移动分组功能(调用moveGroup API) +- [ ] 实现右键菜单显示逻辑 +- [ ] 实现修改备注表单(Modal) +- [ ] 实现移动分组选择器(Modal) +- [ ] 更新ContactStore,同步更新分组数据和缓存 + +**关键实现**: +```typescript +// 右键菜单操作 +const menuItems = [ + { key: 'remark', label: '修改备注', onClick: handleUpdateRemark }, + { key: 'move', label: '移动分组', onClick: handleMoveToGroup }, +]; +``` + +**测试要点**: +- [ ] 修改备注正确性(更新分组数据和搜索结果) +- [ ] 移动分组正确性(从原分组移除,添加到新分组) +- [ ] 缓存更新正确性 +- [ ] UI更新正确性 + +**依赖关系**: +- 依赖:API模块(updateContact, moveGroup), ContactStore +- 被依赖:WechatFriends组件 + +--- + +### 阶段5:缓存策略优化(1周) + +**目标**:优化IndexedDB缓存策略,支持按需缓存和TTL + +#### 5.1 缓存工具改造(3-4天) + +**文件**: +- `src/utils/cache/index.ts`(新建或改造) + +**任务清单**: +- [ ] 实现缓存工具类(支持TTL) +- [ ] 实现分组列表缓存(TTL: 30分钟) +- [ ] 实现分组联系人缓存(TTL: 1小时) +- [ ] 实现分组统计缓存(TTL: 30分钟) +- [ ] 实现缓存失效机制 +- [ ] 实现缓存清理机制(定期清理过期缓存) + +**关键实现**: +```typescript +interface CacheItem { + data: T; + lastUpdate: number; + ttl: number; // 毫秒 +} + +// 检查缓存是否有效 +function isCacheValid(item: CacheItem): boolean { + return Date.now() - item.lastUpdate < item.ttl; +} +``` + +**测试要点**: +- [ ] 缓存读写正确性 +- [ ] TTL机制正确性 +- [ ] 缓存失效正确性 +- [ ] 缓存清理正确性 + +**依赖关系**: +- 依赖:IndexedDB (Dexie) +- 被依赖:ContactStore, SessionStore + +--- + +#### 5.2 初始化加载优化(2-3天) + +**文件**: +- `src/store/module/weChat/contacts.ts`(改造) +- `src/store/module/weChat/message.ts`(改造) + +**任务清单**: +- [ ] 实现初始化加载策略(先读缓存,后台更新) +- [ ] 实现分组列表初始化(检查缓存) +- [ ] 实现会话列表初始化(检查缓存) +- [ ] 实现后台更新逻辑(静默更新) +- [ ] 实现Loading状态优化(有缓存时不显示Loading) + +**关键实现**: +```typescript +// 初始化加载 +async function initLoad() { + // 1. 检查缓存 + const cached = await getCache(cacheKey); + if (cached && isCacheValid(cached)) { + // 立即显示缓存数据 + setData(cached.data); + // 后台更新 + updateInBackground(); + return; + } + + // 2. 无缓存,调用API + setLoading(true); + const data = await fetchFromAPI(); + setData(data); + await setCache(cacheKey, data); + setLoading(false); +} +``` + +**测试要点**: +- [ ] 缓存读取正确性 +- [ ] 后台更新正确性 +- [ ] Loading状态正确性 + +**依赖关系**: +- 依赖:缓存工具 +- 被依赖:组件初始化 + +--- + +### 阶段6:WebSocket实时更新优化(1周) + +**目标**:优化WebSocket实时更新逻辑,同步更新内存和缓存 + +#### 6.1 WebSocket更新逻辑改造(3-4天) + +**文件**: +- `src/store/module/websocket/msgManage.ts`(改造) +- `src/store/module/weChat/contacts.ts`(改造) +- `src/store/module/weChat/message.ts`(改造) + +**任务清单**: +- [ ] 改造WebSocket消息处理,支持新联系人更新 +- [ ] 实现新联系人添加到对应分组(如果已加载) +- [ ] 实现新联系人更新分组统计 +- [ ] 实现新联系人更新搜索结果(如果匹配) +- [ ] 实现新联系人同步更新缓存 +- [ ] 实现新会话更新索引和缓存 + +**关键实现**: +```typescript +// WebSocket收到新联系人 +function handleWebSocketNewContact(contact: Contact) { + const groupKey = `${contact.groupId}_${contact.groupType}_${contact.wechatAccountId}`; + + // 1. 更新内存(如果分组已加载) + if (groupData.has(groupKey) && groupData.get(groupKey)!.loaded) { + groupData.get(groupKey)!.contacts.push(contact); + } + + // 2. 更新缓存 + await updateCache(groupKey, contact); + + // 3. 更新搜索结果(如果匹配) + if (isSearchMode && matchesSearchKeyword(contact, searchKeyword)) { + searchResults.push(contact); + } +} +``` + +**测试要点**: +- [ ] WebSocket更新正确性 +- [ ] 分组数据更新正确性 +- [ ] 缓存同步正确性 +- [ ] 搜索结果更新正确性 + +**依赖关系**: +- 依赖:WebSocket Store, ContactStore, SessionStore +- 被依赖:无 + +--- + +### 阶段7:测试和优化(2-3周) + +**目标**:全面测试和性能优化 + +#### 7.1 功能测试(1周) + +**任务清单**: +- [ ] 会话列表功能测试(切换账号、搜索、排序) +- [ ] 联系人列表功能测试(分组懒加载、分页、搜索) +- [ ] 右键菜单功能测试(分组操作、联系人操作) +- [ ] 缓存功能测试(初始化加载、后台更新) +- [ ] WebSocket更新测试(实时更新) +- [ ] 边界情况测试(大数据量、网络异常、缓存失效) + +**测试场景**: +- [ ] 10000条会话数据,切换账号性能 +- [ ] 50000条联系人数据,分组懒加载性能 +- [ ] 搜索功能(并行请求) +- [ ] 分组编辑操作(新增、编辑、删除) +- [ ] 联系人操作(修改备注、移动分组) +- [ ] 缓存策略(初始化、后台更新、TTL) + +--- + +#### 7.2 性能测试和优化(1周) + +**任务清单**: +- [ ] 会话列表性能测试(切换账号 < 100ms) +- [ ] 联系人列表性能测试(首次展开 < 200ms) +- [ ] 虚拟滚动性能测试(60fps) +- [ ] 内存占用测试(< 100MB) +- [ ] 网络请求优化(减少不必要的请求) +- [ ] 渲染优化(减少不必要的重渲染) + +**性能指标**: +- [ ] 会话列表切换账号:< 100ms +- [ ] 联系人分组展开:< 200ms +- [ ] 虚拟滚动帧率:≥ 60fps +- [ ] 内存占用:< 100MB +- [ ] 搜索响应时间:< 250ms + +--- + +#### 7.3 兼容性测试(3-5天) + +**任务清单**: +- [ ] 浏览器兼容性测试(Chrome, Firefox, Edge, Safari) +- [ ] 不同屏幕尺寸测试(1920x1080, 1366x768, 移动端) +- [ ] 不同数据量测试(1000条、10000条、50000条) +- [ ] 网络环境测试(正常、慢速、离线) + +--- + +## 三、依赖关系图 + +``` +WeChatAccountStore (新建) + ↓ +SessionStore (改造) ──→ MessageList组件 (改造) + ↓ +ContactStore (改造) ──→ WechatFriends组件 (改造) + ↓ +DataIndexManager (新建) + ↓ +VirtualSessionList (新建) +VirtualContactList (新建) + ↓ +GroupContextMenu (新建) +ContactContextMenu (新建) + ↓ +缓存工具 (新建/改造) + ↓ +WebSocket更新 (改造) +``` + +--- + +## 四、风险评估和应对 + +### 4.1 技术风险 + +| 风险 | 影响 | 应对措施 | +|------|------|---------| +| 虚拟滚动兼容性问题 | 高 | 充分测试各种浏览器,准备降级方案 | +| 动态高度计算不准确 | 中 | 使用Intersection Observer动态测量 | +| 内存占用过高 | 中 | 限制Memory Cache大小,使用LRU策略 | +| 索引构建性能问题 | 低 | 使用增量更新,避免全量重建 | + +### 4.2 业务风险 + +| 风险 | 影响 | 应对措施 | +|------|------|---------| +| 数据不一致 | 高 | 版本号机制,定期全量同步 | +| 切换体验差 | 中 | 预加载策略,骨架屏加载 | +| API调用失败 | 中 | 错误重试,降级到缓存数据 | + +--- + +## 五、改造时间表 + +| 阶段 | 任务 | 预计时间 | 负责人 | +|------|------|---------|--------| +| 阶段1 | 基础架构搭建 | 2-3周 | 后端+前端 | +| 阶段2 | 虚拟滚动实现 | 2-3周 | 前端 | +| 阶段3 | 搜索和懒加载 | 1-2周 | 前端 | +| 阶段4 | 右键菜单功能 | 1-2周 | 前端 | +| 阶段5 | 缓存策略优化 | 1周 | 前端 | +| 阶段6 | WebSocket优化 | 1周 | 前端 | +| 阶段7 | 测试和优化 | 2-3周 | 全栈 | +| **总计** | | **10-15周** | | + +--- + +## 六、关键里程碑 + +### 里程碑1:基础架构完成(3周后) +- ✅ WeChatAccountStore创建完成 +- ✅ SessionStore改造完成(索引+缓存) +- ✅ ContactStore改造完成(懒加载结构) +- ✅ 数据索引工具完成 + +### 里程碑2:虚拟滚动完成(6周后) +- ✅ 会话列表虚拟滚动完成 +- ✅ 联系人列表虚拟滚动完成 +- ✅ 性能测试通过(60fps) + +### 里程碑3:核心功能完成(9周后) +- ✅ 搜索功能改造完成(API驱动) +- ✅ 分组懒加载完成 +- ✅ 右键菜单功能完成 + +### 里程碑4:优化完成(12周后) +- ✅ 缓存策略优化完成 +- ✅ WebSocket更新优化完成 +- ✅ 性能优化完成 + +### 里程碑5:上线准备(15周后) +- ✅ 全面测试通过 +- ✅ 性能指标达标 +- ✅ 文档完善 +- ✅ 上线部署 + +--- + +## 七、注意事项 + +### 7.1 向后兼容 + +- ✅ 保留原有Store接口,逐步迁移 +- ✅ 新旧代码并存,逐步替换 +- ✅ 数据迁移脚本,平滑过渡 + +### 7.2 代码质量 + +- ✅ 每个阶段完成后进行Code Review +- ✅ 编写单元测试和集成测试 +- ✅ 遵循现有代码规范 + +### 7.3 文档更新 + +- ✅ 更新API文档 +- ✅ 更新组件文档 +- ✅ 更新架构文档 + +--- + +## 八、总结 + +本改造计划按照新架构设计,分7个阶段逐步实施: + +1. **基础架构搭建**:创建新的Store结构和数据索引系统 +2. **虚拟滚动实现**:解决大数据量渲染问题 +3. **搜索和懒加载**:优化数据加载策略 +4. **右键菜单功能**:完善交互功能 +5. **缓存策略优化**:提升加载速度 +6. **WebSocket优化**:保证数据实时性 +7. **测试和优化**:确保质量和性能 + +**预计总时间**:10-15周 + +**关键成功因素**: +- ✅ 严格按照阶段执行,每个阶段完成后进行测试 +- ✅ 保持向后兼容,平滑过渡 +- ✅ 充分测试,确保质量 +- ✅ 持续优化,提升性能 diff --git a/Touchkebao/提示词/性能优化快速参考.md b/Touchkebao/提示词/性能优化快速参考.md deleted file mode 100644 index e1f93a30..00000000 --- a/Touchkebao/提示词/性能优化快速参考.md +++ /dev/null @@ -1,193 +0,0 @@ -# 性能优化快速参考指南 - -> 快速查看关键优化点和代码示例 - -## 🚀 快速开始 - -### 1. 安装依赖(如果需要) - -```bash -# 检查 zustand 版本(需要 >= 5.0) -npm list zustand - -# 确认 react-window 已安装 -npm list react-window -``` - -### 2. 创建目录结构 - -```bash -mkdir -p src/hooks/weChat -mkdir -p src/components/MessageRenderer -mkdir -p src/components/AiLoadingIndicator -``` - ---- - -## 📝 核心优化代码模板 - -### 1. Zustand Selector 优化模板 - -```typescript -// ❌ 错误写法(会导致多次重渲染) -const currentMessages = useWeChatStore(state => state.currentMessages); -const messagesLoading = useWeChatStore(state => state.messagesLoading); - -// ✅ 正确写法(合并 selector,使用 shallow) -import { shallow } from "zustand/shallow"; - -const { currentMessages, messagesLoading } = useWeChatStore( - state => ({ - currentMessages: state.currentMessages, - messagesLoading: state.messagesLoading, - }), - shallow, // 关键:使用 shallow 比较 -); -``` - -### 2. React.memo 优化模板 - -```typescript -// ✅ 使用 React.memo 包装组件 -const MyComponent: React.FC = React.memo( - ({ prop1, prop2 }) => { - // 组件逻辑 - }, - (prev, next) => { - // 自定义比较函数 - return prev.prop1.id === next.prop1.id; - }, -); - -MyComponent.displayName = "MyComponent"; -``` - -### 3. useCallback 优化模板 - -```typescript -// ✅ 使用 useCallback 缓存函数 -const handleClick = useCallback( - (id: number) => { - // 处理逻辑 - }, - [dependency1, dependency2], // 依赖项 -); - -// ✅ 使用 useRef 存储不变的引用 -const contractRef = useRef(contract); -contractRef.current = contract; // 更新引用 - -const handleSend = useCallback(() => { - const currentContract = contractRef.current; // 使用引用 - // 处理逻辑 -}, []); // 不需要 contract 作为依赖 -``` - -### 4. useMemo 优化模板 - -```typescript -// ✅ 使用 useMemo 缓存计算结果 -const expensiveValue = useMemo( - () => { - // 复杂计算 - return computeExpensiveValue(data); - }, - [data], // 依赖项 -); -``` - -### 5. 虚拟滚动模板 - -```typescript -import { FixedSizeList } from "react-window"; - - - {({ index, style, data }) => ( -
- -
- )} -
-``` - ---- - -## 🔍 常见问题排查 - -### 问题 1:组件仍然频繁重渲染 - -**检查**: - -1. ✅ 是否使用了 `shallow` 比较? -2. ✅ `useCallback` 依赖项是否正确? -3. ✅ `React.memo` 比较函数是否正确? - -### 问题 2:虚拟滚动不工作 - -**检查**: - -1. ✅ 容器高度是否设置? -2. ✅ `itemSize` 是否正确? -3. ✅ `itemCount` 是否正确? - -### 问题 3:性能没有提升 - -**检查**: - -1. ✅ 是否使用了 React DevTools Profiler 测量? -2. ✅ 是否在开发模式下测试?(开发模式性能较差) -3. ✅ 是否有其他性能瓶颈? - ---- - -## 📊 性能指标参考 - -### 优化前(基准) - -- 100 条消息滚动:卡顿明显 -- 组件重渲染:每次状态变化都重渲染 -- 内存占用:1000 条消息 > 200MB - -### 优化后(目标) - -- 100 条消息滚动:流畅(FPS > 30) -- 组件重渲染:减少 60-80% -- 内存占用:1000 条消息 < 100MB - ---- - -## 🛠️ 调试工具 - -### React DevTools Profiler - -1. 安装 React DevTools 浏览器扩展 -2. 打开 Profiler 标签 -3. 点击录制按钮 -4. 执行操作 -5. 停止录制,查看性能数据 - -### Chrome Performance - -1. 打开 Chrome DevTools -2. 切换到 Performance 标签 -3. 点击录制按钮 -4. 执行操作 -5. 停止录制,分析性能数据 - ---- - -## 📚 相关文档 - -- [详细改造计划](./性能优化详细改造计划.md) -- [优化清单](./性能优化改造清单.md) -- [React 代码规范](../提示词/React代码编写规范.md) - ---- - -**最后更新**: 2025-01-XX diff --git a/Touchkebao/提示词/性能优化改造清单.md b/Touchkebao/提示词/性能优化改造清单.md deleted file mode 100644 index b5efba2e..00000000 --- a/Touchkebao/提示词/性能优化改造清单.md +++ /dev/null @@ -1,406 +0,0 @@ -# Touchkebao 性能优化改造清单 - -> 基于 React 代码编写规范(L5 - 性能优化指南)和实际代码分析 - -## 📊 性能问题分析总结 - -### 核心问题 - -1. **组件过大**:MessageRecord (1040行)、MessageEnter (608行) 超出规范建议的300行 -2. **缺少 React.memo 优化**:主要组件未使用 memo,导致不必要的重渲染 -3. **Zustand Selector 问题**:多个组件使用多个 selector,触发频繁重渲染 -4. **消息列表未使用虚拟滚动**:已安装 react-window 但未使用,长列表性能差 -5. **函数重复创建**:parseMessageContent 等函数每次渲染都重新创建 -6. **缺少 useMemo 优化**:groupedMessages 等计算值未缓存 - ---- - -## 🎯 优化改造清单 - -### 一、MessageRecord 组件优化(优先级:🔥 高) - -#### 1.1 组件拆分(必须) - -- [ ] **拆分 MessageRecord 组件** - - 文件:`src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - - 问题:1040行,严重超出300行规范 - - 改造: - - 提取 `parseMessageContent` 逻辑到独立 Hook:`useMessageParser.ts` - - 提取消息渲染逻辑到 `MessageRenderer.tsx` - - 提取右键菜单逻辑到 `MessageContextMenu.tsx` - - 提取消息分组逻辑到 `useMessageGrouping.ts` - -#### 1.2 React.memo 优化(必须) - -- [ ] **使用 React.memo 包装 MessageRecord** - ```tsx - const MessageRecord: React.FC = React.memo( - ({ contract }) => { - // ... - }, - (prev, next) => prev.contract.id === next.contract.id, - ); - ``` - -#### 1.3 Zustand Selector 优化(必须) - -- [ ] **合并多个 selector,使用 shallow 比较** - - ```tsx - // ❌ 当前写法(会导致多次重渲染) - const currentMessages = useWeChatStore(state => state.currentMessages); - const messagesLoading = useWeChatStore(state => state.messagesLoading); - const showCheckbox = useWeChatStore(state => state.showCheckbox); - - // ✅ 优化后 - const { currentMessages, messagesLoading, showCheckbox } = useWeChatStore( - state => ({ - currentMessages: state.currentMessages, - messagesLoading: state.messagesLoading, - showCheckbox: state.showCheckbox, - }), - shallow, - ); - ``` - - - 需要安装:`npm install zustand shallow-compare` - -#### 1.4 虚拟滚动优化(必须) - -- [ ] **实现消息列表虚拟滚动** - - 文件:`MessageRecord/index.tsx` - - 使用已安装的 `react-window` - - 改造: - - ```tsx - import { FixedSizeList } from "react-window"; - - - {({ index, style }) => ( -
- -
- )} -
; - ``` - -#### 1.5 useMemo 优化(必须) - -- [ ] **缓存 groupedMessages** - ```tsx - const groupedMessages = useMemo( - () => groupMessagesByTime(currentMessages), - [currentMessages], - ); - ``` - -#### 1.6 useCallback 优化(必须) - -- [ ] **优化 parseMessageContent 函数** - - 使用 `useCallback` 包装,避免每次渲染重新创建 - - 提取到独立 Hook 中管理 - ---- - -### 二、MessageEnter 组件优化(优先级:🔥 高) - -#### 2.1 组件拆分(必须) - -- [ ] **拆分 MessageEnter 组件** - - 文件:`src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx` - - 问题:608行,超出300行规范 - - 改造: - - 提取 AI 加载状态到 `AiLoadingIndicator.tsx` - - 提取文件上传逻辑到 `useFileUpload.ts` - - 提取消息发送逻辑到 `useMessageSend.ts` - -#### 2.2 React.memo 优化(必须) - -- [ ] **使用 React.memo 包装 MessageEnter** - ```tsx - const MessageEnter: React.FC = React.memo( - ({ contract }) => { - // ... - }, - (prev, next) => prev.contract.id === next.contract.id, - ); - ``` - -#### 2.3 Zustand Selector 优化(必须) - -- [ ] **合并多个 selector** - - ```tsx - // ❌ 当前写法(12个独立的 selector) - const EnterModule = useWeChatStore(state => state.EnterModule); - const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox); - // ... 还有10个 - - // ✅ 优化后 - const { - EnterModule, - updateShowCheckbox, - updateEnterModule, - // ... 其他 - } = useWeChatStore( - state => ({ - EnterModule: state.EnterModule, - updateShowCheckbox: state.updateShowCheckbox, - updateEnterModule: state.updateEnterModule, - // ... - }), - shallow, - ); - ``` - -#### 2.4 useCallback 依赖优化(重要) - -- [ ] **优化 handleSend 的依赖项** - - 当前依赖项过多,可能导致频繁重新创建 - - 使用 `useRef` 存储不变的引用 - ---- - -### 三、ChatWindow 组件优化(优先级:⚠️ 中) - -#### 3.1 React.memo 优化(必须) - -- [ ] **使用 React.memo 包装 ChatWindow** - ```tsx - const ChatWindow: React.FC = React.memo( - ({ contract }) => { - // ... - }, - (prev, next) => prev.contract.id === next.contract.id, - ); - ``` - -#### 3.2 Zustand Selector 优化(必须) - -- [ ] **合并 selector** - ```tsx - const { aiQuoteMessageContent, showChatRecordModel } = useWeChatStore( - state => ({ - aiQuoteMessageContent: state.aiQuoteMessageContent, - showChatRecordModel: state.showChatRecordModel, - }), - shallow, - ); - ``` - ---- - -### 四、状态管理优化(优先级:🔥 高) - -#### 4.1 Zustand Store 优化(必须) - -- [ ] **优化 useWeChatStore 的 selector 使用** - - 文件:`src/store/module/weChat/weChat.ts` - - 创建自定义 Hook 封装常用 selector - ```tsx - // 创建 useWeChatSelectors.ts - export const useWeChatSelectors = () => { - return useWeChatStore( - state => ({ - currentMessages: state.currentMessages, - currentContract: state.currentContract, - messagesLoading: state.messagesLoading, - showCheckbox: state.showCheckbox, - }), - shallow, - ); - }; - ``` - -#### 4.2 状态更新优化(重要) - -- [ ] **优化 addMessage 方法** - - 使用批量更新,避免频繁触发重渲染 - - 考虑使用 `unstable_batchedUpdates` 或 Zustand 的批量更新 - ---- - -### 五、消息渲染优化(优先级:🔥 高) - -#### 5.1 MessageItem 优化(已完成,需检查) - -- [x] **MessageItem 已使用 React.memo** - - 文件:`MessageRecord/index.tsx` (第176行) - - 需要检查比较函数是否正确 - -#### 5.2 消息内容解析优化(必须) - -- [ ] **提取 parseMessageContent 到独立 Hook** - - 创建:`src/hooks/useMessageParser.ts` - - 使用 `useMemo` 缓存解析结果 - - 避免每次渲染都重新解析 - -#### 5.3 表情解析优化(重要) - -- [ ] **优化 parseEmojiText 函数** - - 使用 `useMemo` 缓存解析结果 - - 考虑使用 Web Worker 处理大量表情解析 - ---- - -### 六、列表渲染优化(优先级:🔥 高) - -#### 6.1 虚拟滚动实现(必须) - -- [ ] **实现消息列表虚拟滚动** - - 使用 `react-window` 或 `react-virtualized` - - 只渲染可见区域的消息 - - 预计性能提升:50-80% - -#### 6.2 消息分组优化(重要) - -- [ ] **优化 groupMessagesByTime** - - 使用 `useMemo` 缓存分组结果 - - 考虑使用 `useMemo` 的依赖项优化 - ---- - -### 七、图片和媒体优化(优先级:⚠️ 中) - -#### 7.1 图片懒加载(重要) - -- [ ] **实现图片懒加载** - - 使用 `IntersectionObserver` API - - 或使用 `react-lazyload` 库 - - 只加载可见区域的图片 - -#### 7.2 视频加载优化(重要) - -- [ ] **优化视频消息加载** - - 延迟加载视频预览图 - - 使用缩略图替代完整视频 - ---- - -### 八、WebSocket 消息处理优化(优先级:⚠️ 中) - -#### 8.1 消息批量处理(重要) - -- [ ] **优化消息接收处理** - - 文件:`src/store/module/websocket/msgManage.ts` - - 批量处理多条消息,减少更新频率 - - 使用防抖或节流 - -#### 8.2 消息更新优化(重要) - -- [ ] **优化消息状态更新** - - 避免频繁更新整个消息列表 - - 使用局部更新策略 - ---- - -### 九、代码分割和懒加载(优先级:⚠️ 中) - -#### 9.1 路由懒加载(重要) - -- [ ] **实现路由级别的代码分割** - ```tsx - const ChatWindow = lazy(() => import("./components/ChatWindow")); - ``` - -#### 9.2 组件懒加载(可选) - -- [ ] **懒加载非关键组件** - - ProfileCard - - FollowupReminderModal - - TodoListModal - ---- - -### 十、依赖项优化(优先级:⚠️ 中) - -#### 10.1 安装必要依赖(必须) - -- [ ] **安装 shallow-compare** - ```bash - npm install shallow-compare - # 或使用 zustand 内置的 shallow - ``` - -#### 10.2 检查依赖版本(重要) - -- [ ] **确保 react-window 版本正确** - - 当前版本:^1.8.11(已安装但未使用) - ---- - -## 📈 预期性能提升 - -### 优化前问题 - -- 消息列表滚动卡顿(>100条消息时) -- 输入框输入延迟 -- 频繁的组件重渲染 -- 内存占用过高 - -### 优化后预期 - -- ✅ 消息列表流畅滚动(支持1000+条消息) -- ✅ 输入响应时间 < 50ms -- ✅ 组件重渲染减少 60-80% -- ✅ 内存占用降低 30-50% - ---- - -## 🚀 实施优先级 - -### 第一阶段(立即执行) - -1. MessageRecord 组件拆分 -2. MessageEnter 组件拆分 -3. Zustand Selector 优化 -4. React.memo 包装主要组件 - -### 第二阶段(本周内) - -1. 虚拟滚动实现 -2. useMemo/useCallback 优化 -3. 消息解析 Hook 提取 - -### 第三阶段(下周) - -1. 图片懒加载 -2. 代码分割 -3. WebSocket 消息批量处理 - ---- - -## 📝 注意事项 - -1. **渐进式优化**:不要一次性改动太多,逐步验证效果 -2. **性能监控**:使用 React DevTools Profiler 监控优化效果 -3. **测试覆盖**:确保优化后功能正常 -4. **代码审查**:每个优化点都要经过代码审查 - ---- - -## 🔍 性能监控工具 - -### 推荐使用 - -1. **React DevTools Profiler** - - 监控组件渲染时间 - - 识别性能瓶颈 - -2. **Chrome Performance** - - 分析运行时性能 - - 识别长任务 - -3. **Lighthouse** - - 整体性能评分 - - 优化建议 - ---- - -**最后更新**: 2025-01-XX -**维护者**: 开发团队 diff --git a/Touchkebao/提示词/性能优化详细改造计划.md b/Touchkebao/提示词/性能优化详细改造计划.md deleted file mode 100644 index 153629f1..00000000 --- a/Touchkebao/提示词/性能优化详细改造计划.md +++ /dev/null @@ -1,2985 +0,0 @@ -# Touchkebao 性能优化详细改造计划 - -> 基于性能优化改造清单,提供可执行的详细实施步骤 - -**文档版本**: v2.0 -**创建日期**: 2025-01-XX -**最后更新**: 2025-01-XX(新增 Sentry 和 TanStack Query) -**预计总工期**: 4-5周(包含新增优化工具) -**负责人**: 开发团队 - ---- - -## 📋 目录 - -- [准备工作](#准备工作) -- [第一阶段:核心组件优化(第1周)](#第一阶段核心组件优化第1周) -- [第二阶段:性能优化(第2周)](#第二阶段性能优化第2周) -- [第三阶段:高级优化(第3-4周)](#第三阶段高级优化第3-4周) -- [测试与验证](#测试与验证) -- [回滚方案](#回滚方案) - -**新增优化工具**: - -- **Sentry**:错误监控和性能追踪(第1周) -- **TanStack Query**:数据缓存和请求优化(第2周) - ---- - -## 准备工作 - -### 1. 环境准备 - -#### 1.1 安装依赖 - -```bash -# 安装 shallow 比较工具(Zustand 5.x 已内置,无需额外安装) -# 检查 zustand 版本 -npm list zustand - -# 如果版本 < 5.0,需要升级 -npm install zustand@^5.0.6 - -# 确认 react-window 已安装 -npm list react-window -# 当前版本:^1.8.11 ✅ - -# ========== 新增优化工具 ========== -# 1. 安装 Sentry(错误监控和性能追踪) -npm install @sentry/react - -# 2. 安装 TanStack Query(数据缓存和请求优化) -npm install @tanstack/react-query -``` - -#### 1.2 创建目录结构 - -```bash -# 创建新的目录结构 -mkdir -p src/hooks/weChat -mkdir -p src/components/MessageRenderer -mkdir -p src/components/AiLoadingIndicator -mkdir -p src/utils/messageParser -mkdir -p src/providers -mkdir -p src/utils/sentry -``` - -#### 1.3 配置 Sentry 和 TanStack Query(必须先完成) - -> **重要**:在开始性能优化前,必须先配置好这两个工具,以便在优化过程中实时监控效果 - -##### 步骤 1.3.1:配置 Sentry - -**文件**: `src/utils/sentry/index.ts` - -```typescript -import * as Sentry from "@sentry/react"; - -/** - * 初始化 Sentry - * 用于错误监控和性能追踪 - */ -export const initSentry = () => { - if (!import.meta.env.VITE_SENTRY_DSN) { - console.warn("Sentry DSN 未配置,跳过初始化"); - return; - } - - Sentry.init({ - dsn: import.meta.env.VITE_SENTRY_DSN, - environment: import.meta.env.MODE, - integrations: [ - new Sentry.BrowserTracing({ - tracingOrigins: [ - "localhost", - import.meta.env.VITE_API_BASE_URL || "/api", - ], - }), - new Sentry.Replay({ - maskAllText: false, - blockAllMedia: false, - }), - ], - tracesSampleRate: import.meta.env.MODE === "development" ? 1.0 : 0.1, - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - ignoreErrors: [ - "NetworkError", - "Failed to fetch", - "chrome-extension://", - "moz-extension://", - ], - }); -}; - -export const captureError = ( - error: Error, - context?: { - tags?: Record; - extra?: Record; - }, -) => { - Sentry.captureException(error, { - tags: context?.tags, - extra: context?.extra, - }); -}; - -export const addPerformanceBreadcrumb = ( - message: string, - data?: Record, -) => { - Sentry.addBreadcrumb({ - category: "performance", - message, - level: "info", - data, - }); -}; -``` - -**文件**: `src/main.tsx` - -```typescript -import { initSentry } from "@/utils/sentry"; - -// 最先初始化 Sentry -initSentry(); - -// ... 其他代码 -``` - -**文件**: `src/App.tsx` - -```typescript -import { SentryErrorBoundary } from "@sentry/react"; - -const ErrorFallback = () => ( -
-

出现了一些问题

-

我们已经记录了这个问题,正在修复中...

- -
-); - -function App() { - return ( - - {/* 应用内容 */} - - ); -} -``` - -##### 步骤 1.3.2:配置 TanStack Query - -**文件**: `src/providers/QueryProvider.tsx` - -```typescript -import React from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, - cacheTime: 10 * 60 * 1000, - retry: 1, - refetchOnWindowFocus: true, - refetchOnReconnect: true, - }, - mutations: { - retry: 1, - }, - }, -}); - -interface QueryProviderProps { - children: React.ReactNode; -} - -export const QueryProvider: React.FC = ({ children }) => { - return ( - - {children} - {import.meta.env.MODE === "development" && ( - - )} - - ); -}; -``` - -**文件**: `src/main.tsx` 或 `src/App.tsx` - -```typescript -import { QueryProvider } from "@/providers/QueryProvider"; - -function App() { - return ( - - {/* 应用内容 */} - - ); -} -``` - -#### 1.4 性能基准测试(使用 Sentry 记录) - -**在开始优化前,记录当前性能指标:** - -1. **使用 React DevTools Profiler** - - 记录 MessageRecord 组件渲染时间 - - 记录 MessageEnter 组件渲染时间 - - 记录重渲染次数 - -2. **使用 Sentry 记录性能基准** - - ```typescript - import { addPerformanceBreadcrumb } from "@/utils/sentry"; - - // 在关键组件中添加性能记录 - addPerformanceBreadcrumb("性能优化前 - MessageRecord 渲染", { - renderTime: 150, // ms - messageCount: 100, - }); - ``` - -3. **使用 Chrome Performance** - - 记录页面加载时间 - - 记录长任务(Long Tasks) - - 记录内存占用 - -4. **手动测试** - - 测试 100 条消息滚动性能 - - 测试 500 条消息滚动性能 - - 测试输入框响应时间 - -**保存基准数据到**: `docs/performance-baseline.md` - -**验收标准**: - -- [ ] Sentry 初始化成功,能够看到性能数据 -- [ ] TanStack Query 配置成功,DevTools 可用 -- [ ] 基准性能数据已记录 - ---- - -## 第一阶段:核心组件优化(第1周) - -> **重要提示**:本阶段所有优化任务都会同时使用 Sentry 进行性能监控和错误追踪,使用 TanStack Query 优化数据获取 - -### 任务 0.1:Sentry 和 TanStack Query 基础配置(已完成) - -> ✅ **已完成**:如果依赖已安装,此任务已完成。如果未完成,请先完成准备工作中的配置步骤。 - -#### 步骤 0.1.1:安装和配置 Sentry - -**文件**: `src/utils/sentry/index.ts` - -```typescript -import * as Sentry from "@sentry/react"; - -/** - * 初始化 Sentry - * 用于错误监控和性能追踪 - */ -export const initSentry = () => { - if (!import.meta.env.VITE_SENTRY_DSN) { - console.warn("Sentry DSN 未配置,跳过初始化"); - return; - } - - Sentry.init({ - dsn: import.meta.env.VITE_SENTRY_DSN, - environment: import.meta.env.MODE, - integrations: [ - new Sentry.BrowserTracing({ - // 追踪 API 请求性能 - tracingOrigins: [ - "localhost", - import.meta.env.VITE_API_BASE_URL || "/api", - ], - }), - new Sentry.Replay({ - // 错误回放,帮助调试 - maskAllText: false, - blockAllMedia: false, - }), - ], - // 性能追踪采样率(开发环境 100%,生产环境可降低) - tracesSampleRate: import.meta.env.MODE === "development" ? 1.0 : 0.1, - // 会话回放采样率 - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - // 忽略某些错误 - ignoreErrors: [ - // 忽略网络错误(可能是用户网络问题) - "NetworkError", - "Failed to fetch", - // 忽略浏览器扩展错误 - "chrome-extension://", - "moz-extension://", - ], - }); -}; - -/** - * 手动捕获错误 - */ -export const captureError = ( - error: Error, - context?: { - tags?: Record; - extra?: Record; - }, -) => { - Sentry.captureException(error, { - tags: context?.tags, - extra: context?.extra, - }); -}; - -/** - * 手动捕获消息 - */ -export const captureMessage = ( - message: string, - level: "info" | "warning" | "error" = "info", - context?: { - tags?: Record; - extra?: Record; - }, -) => { - Sentry.captureMessage(message, { - level, - tags: context?.tags, - extra: context?.extra, - }); -}; - -/** - * 添加性能面包屑 - */ -export const addPerformanceBreadcrumb = ( - message: string, - data?: Record, -) => { - Sentry.addBreadcrumb({ - category: "performance", - message, - level: "info", - data, - }); -}; -``` - -#### 步骤 0.1.2:在应用入口初始化 - -**文件**: `src/main.tsx` 或 `src/App.tsx` - -```typescript -import { initSentry } from "@/utils/sentry"; - -// 在应用启动时初始化 Sentry -initSentry(); - -// ... 其他代码 -``` - -#### 步骤 0.1.3:包装根组件 - -**文件**: `src/App.tsx` - -```typescript -import { SentryErrorBoundary } from "@sentry/react"; - -const ErrorFallback = () => ( -
-

出现了一些问题

-

我们已经记录了这个问题,正在修复中...

- -
-); - -function App() { - return ( - - {/* 应用内容 */} - - ); -} -``` - -#### 步骤 0.1.4:添加关键错误监控 - -**文件**: `src/store/module/websocket/msgManage.ts` - -```typescript -import { captureError, captureMessage } from "@/utils/sentry"; - -// 在关键错误处添加监控 -CmdNotify: async (message: WebSocketMessage) => { - if (["Auth failed", "Kicked out"].includes(message.notify)) { - captureMessage("WebSocket 认证失败", "error", { - tags: { source: "websocket", type: message.notify }, - extra: { message }, - }); - // ... 原有逻辑 - } -}, -``` - -**文件**: `src/store/module/weChat/weChat.ts` - -```typescript -import { captureError } from "@/utils/sentry"; - -loadChatMessages: async (Init: boolean, pageOverride?: number) => { - try { - // ... 原有逻辑 - } catch (error) { - captureError(error as Error, { - tags: { action: "loadChatMessages" }, - extra: { contactId: contact?.id, page: nextPage }, - }); - console.error("获取聊天消息失败:", error); - } -}, -``` - -#### 步骤 0.1.5:添加性能监控 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -```typescript -import { Profiler } from "@sentry/react"; -import { addPerformanceBreadcrumb } from "@/utils/sentry"; - -const MessageRecord: React.FC = ({ contract }) => { - return ( - { - // 记录组件渲染开始时间 - addPerformanceBreadcrumb("MessageRecord render start", { - contractId: contract.id, - }); - }} - onRender={(id, phase, actualDuration) => { - // 记录组件渲染性能 - if (actualDuration > 100) { - // 如果渲染时间超过 100ms,记录警告 - addPerformanceBreadcrumb("MessageRecord slow render", { - duration: actualDuration, - phase, - contractId: contract.id, - }); - } - }} - > - {/* 组件内容 */} - - ); -}; -``` - -**验收标准**: - -- [ ] Sentry 初始化成功 -- [ ] 错误能够正确上报 -- [ ] 性能数据能够正确记录 -- [ ] 不影响现有功能 -- [ ] 生产环境配置正确 - -**预期收益**: - -- ✅ **实时错误监控** -- ✅ **性能问题主动发现** -- ✅ **为后续优化提供基准数据** - ---- - -### 任务 1.1:优化 Zustand Selector 使用(优先级:🔥🔥🔥) - -**预计时间**: 2小时 -**影响范围**: 所有使用 useWeChatStore 的组件 - -> **同时使用 Sentry 监控**:在优化过程中使用 Sentry Profiler 监控组件渲染性能变化 - -#### 步骤 1.1.1:创建自定义 Selector Hook - -**文件**: `src/hooks/weChat/useWeChatSelectors.ts` - -```typescript -import { useWeChatStore } from "@/store/module/weChat/weChat"; -import { shallow } from "zustand/shallow"; - -/** - * 合并多个 selector,减少重渲染 - * 使用 shallow 比较,只有对象属性变化时才触发更新 - */ -export const useWeChatSelectors = () => { - return useWeChatStore( - state => ({ - // 消息相关 - currentMessages: state.currentMessages, - currentMessagesHasMore: state.currentMessagesHasMore, - messagesLoading: state.messagesLoading, - isLoadingData: state.isLoadingData, - - // 联系人相关 - currentContract: state.currentContract, - - // UI 状态 - showCheckbox: state.showCheckbox, - EnterModule: state.EnterModule, - showChatRecordModel: state.showChatRecordModel, - - // AI 相关 - isLoadingAiChat: state.isLoadingAiChat, - quoteMessageContent: state.quoteMessageContent, - aiQuoteMessageContent: state.aiQuoteMessageContent, - - // 选中记录 - selectedChatRecords: state.selectedChatRecords, - }), - shallow, // 使用 shallow 比较 - ); -}; - -/** - * 消息相关的 selector - */ -export const useMessageSelectors = () => { - return useWeChatStore( - state => ({ - currentMessages: state.currentMessages, - currentMessagesHasMore: state.currentMessagesHasMore, - messagesLoading: state.messagesLoading, - isLoadingData: state.isLoadingData, - }), - shallow, - ); -}; - -/** - * UI 状态相关的 selector - */ -export const useUIStateSelectors = () => { - return useWeChatStore( - state => ({ - showCheckbox: state.showCheckbox, - EnterModule: state.EnterModule, - showChatRecordModel: state.showChatRecordModel, - }), - shallow, - ); -}; - -/** - * AI 相关的 selector - */ -export const useAISelectors = () => { - return useWeChatStore( - state => ({ - isLoadingAiChat: state.isLoadingAiChat, - quoteMessageContent: state.quoteMessageContent, - aiQuoteMessageContent: state.aiQuoteMessageContent, - }), - shallow, - ); -}; - -/** - * 操作方法 selector(这些方法引用稳定,不需要 shallow) - */ -export const useWeChatActions = () => { - return useWeChatStore(state => ({ - addMessage: state.addMessage, - updateMessage: state.updateMessage, - recallMessage: state.recallMessage, - loadChatMessages: state.loadChatMessages, - updateShowCheckbox: state.updateShowCheckbox, - updateEnterModule: state.updateEnterModule, - updateQuoteMessageContent: state.updateQuoteMessageContent, - updateIsLoadingAiChat: state.updateIsLoadingAiChat, - updateSelectedChatRecords: state.updateSelectedChatRecords, - updateShowChatRecordModel: state.updateShowChatRecordModel, - setCurrentContact: state.setCurrentContact, - })); -}; -``` - -#### 步骤 1.1.2:更新 MessageRecord 组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -**修改前**: - -```typescript -const currentMessages = useWeChatStore(state => state.currentMessages); -const currentMessagesHasMore = useWeChatStore( - state => state.currentMessagesHasMore, -); -const loadChatMessages = useWeChatStore(state => state.loadChatMessages); -const messagesLoading = useWeChatStore(state => state.messagesLoading); -const isLoadingData = useWeChatStore(state => state.isLoadingData); -const showCheckbox = useWeChatStore(state => state.showCheckbox); -// ... 更多 selector -``` - -**修改后**: - -```typescript -import { - useMessageSelectors, - useUIStateSelectors, -} from "@/hooks/weChat/useWeChatSelectors"; -import { useWeChatActions } from "@/hooks/weChat/useWeChatSelectors"; - -const MessageRecord: React.FC = ({ contract }) => { - // 合并 selector - const { - currentMessages, - currentMessagesHasMore, - messagesLoading, - isLoadingData, - } = useMessageSelectors(); - - const { showCheckbox } = useUIStateSelectors(); - - const { - loadChatMessages, - updateShowCheckbox, - updateEnterModule, - updateSelectedChatRecords, - } = useWeChatActions(); - - // ... 其他代码 -}; -``` - -**测试要点**: - -- [ ] 确认组件功能正常 -- [ ] 使用 React DevTools 确认重渲染次数减少 -- [ ] 使用 Sentry Profiler 监控渲染性能 -- [ ] 测试消息列表滚动性能 - -**性能监控**: - -```typescript -import { Profiler } from "@sentry/react"; -import { addPerformanceBreadcrumb } from "@/utils/sentry"; - -// 在 MessageRecord 组件外包装 Profiler - { - addPerformanceBreadcrumb("MessageRecord 渲染性能", { - phase, - duration: actualDuration, - optimization: "selector-optimized", - }); - }} -> - - -``` - -#### 步骤 1.1.3:更新 MessageEnter 组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx` - -**修改前**: - -```typescript -const EnterModule = useWeChatStore(state => state.EnterModule); -const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox); -const updateEnterModule = useWeChatStore(state => state.updateEnterModule); -// ... 12个独立的 selector -``` - -**修改后**: - -```typescript -import { - useUIStateSelectors, - useAISelectors, -} from "@/hooks/weChat/useWeChatSelectors"; -import { useWeChatActions } from "@/hooks/weChat/useWeChatSelectors"; - -const MessageEnter: React.FC = ({ contract }) => { - // 合并 selector - const { EnterModule, showChatRecordModel } = useUIStateSelectors(); - const { isLoadingAiChat, quoteMessageContent, aiQuoteMessageContent } = - useAISelectors(); - - const { - updateShowCheckbox, - updateEnterModule, - addMessage, - updateQuoteMessageContent, - updateIsLoadingAiChat, - updateShowChatRecordModel, - } = useWeChatActions(); - - // ... 其他代码 -}; -``` - -**测试要点**: - -- [ ] 确认输入框功能正常 -- [ ] 确认 AI 功能正常 -- [ ] 确认文件上传功能正常 -- [ ] 使用 React DevTools 确认重渲染次数减少 - -#### 步骤 1.1.4:更新 ChatWindow 组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx` - -**修改**: - -```typescript -import { - useAISelectors, - useUIStateSelectors, -} from "@/hooks/weChat/useWeChatSelectors"; -import { useWeChatActions } from "@/hooks/weChat/useWeChatSelectors"; - -const ChatWindow: React.FC = ({ contract }) => { - const { aiQuoteMessageContent, showChatRecordModel } = useAISelectors(); - const { updateAiQuoteMessageContent, setCurrentContact } = useWeChatActions(); - - // ... 其他代码 -}; -``` - -**验收标准**: - -- [ ] 所有组件功能正常 -- [ ] React DevTools 显示重渲染次数减少 50%+ -- [ ] 无 TypeScript 错误 -- [ ] 无 ESLint 错误 - ---- - -### 任务 1.2:MessageRecord 组件拆分(优先级:🔥🔥🔥) - -**预计时间**: 1天 -**影响范围**: MessageRecord 组件及其子组件 - -> **同时使用 TanStack Query**:在拆分过程中,将消息数据获取迁移到 TanStack Query,实现数据缓存和自动刷新 -> **同时使用 Sentry**:监控组件拆分后的性能变化 - -#### 步骤 1.2.1:提取消息解析 Hook - -**文件**: `src/hooks/weChat/useMessageParser.ts` - -```typescript -import { useCallback, useMemo } from "react"; -import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji"; -import AudioMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage"; -import SmallProgramMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage"; -import VideoMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage"; -import LocationMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage"; -import SystemRecommendRemarkMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage"; -import RedPacketMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage"; -import TransferMessage from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage"; -import styles from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/com.module.scss"; - -const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i; -const FILE_EXT_REGEX = /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i; - -/** - * 消息解析 Hook - * 提取 parseMessageContent 逻辑,使用 useCallback 优化 - */ -export const useMessageParser = ( - contract: ContractData | weChatGroup, -) => { - // 解析表情文本 - const parseEmojiText = useCallback((text: string): React.ReactNode[] => { - const emojiRegex = /\[([^\]]+)\]/g; - const parts: React.ReactNode[] = []; - let lastIndex = 0; - let match; - - while ((match = emojiRegex.exec(text)) !== null) { - if (match.index > lastIndex) { - parts.push(text.slice(lastIndex, match.index)); - } - - const emojiName = match[1]; - const emojiPath = getEmojiPath(emojiName as any); - - if (emojiPath) { - parts.push( - {emojiName}, - ); - } else { - parts.push(match[0]); - } - - lastIndex = emojiRegex.lastIndex; - } - - if (lastIndex < text.length) { - parts.push(text.slice(lastIndex)); - } - - return parts; - }, []); - - // 解析消息内容 - const parseMessageContent = useCallback( - ( - content: string | null | undefined, - msg: ChatRecord, - msgType?: number, - ): React.ReactNode => { - if (content === null || content === undefined) { - return
消息内容不可用
; - } - - const isStringValue = typeof content === "string"; - const rawContent = isStringValue ? content : ""; - const trimmedContent = rawContent.trim(); - - switch (msgType) { - case 1: // 文本消息 - return ( -
-
- {parseEmojiText(rawContent)} -
-
- ); - - case 3: // 图片消息 - if (!isStringValue || !trimmedContent) { - return
[图片消息 - 无效链接]
; - } - return ( -
- 图片消息 window.open(rawContent, "_blank")} - /> -
- ); - - case 34: // 语音消息 - return ; - - case 43: // 视频消息 - return ( - - ); - - case 47: // 动图表情包 - return ( -
- 表情包 -
- ); - - case 48: // 定位消息 - return ; - - case 49: // 小程序/文件 - return ( - - ); - - case 10002: // 系统推荐备注消息 - return ( - - ); - - default: { - // 处理 JSON 格式的复杂消息 - try { - const jsonData = JSON.parse(trimmedContent); - if (jsonData && typeof jsonData === "object") { - // 红包消息 - if ( - jsonData.nativeurl && - jsonData.nativeurl.includes("wxpay://c2cbizmessagehandler/hongbao/receivehongbao") - ) { - return ( - - ); - } - - // 转账消息 - if ( - jsonData.title === "微信转账" || - (jsonData.transferid && jsonData.feedesc) - ) { - return ( - - ); - } - } - } catch (e) { - // 不是 JSON,继续处理 - } - - // 默认文本消息 - return ( -
-
- {parseEmojiText(rawContent)} -
-
- ); - } - } - }, - [contract, parseEmojiText], - ); - - return { - parseMessageContent, - parseEmojiText, - }; -}; -``` - -#### 步骤 1.2.2:提取消息分组 Hook - -**文件**: `src/hooks/weChat/useMessageGrouping.ts` - -```typescript -import { useMemo } from "react"; -import { ChatRecord } from "@/pages/pc/ckbox/data"; -import { formatWechatTime } from "@/utils/common"; - -export interface MessageGroup { - time: string; - messages: ChatRecord[]; -} - -/** - * 消息分组 Hook - * 使用 useMemo 缓存分组结果 - */ -export const useMessageGrouping = ( - messages: ChatRecord[] | null | undefined, -): MessageGroup[] => { - return useMemo(() => { - 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) - .map(msg => ({ - time: formatWechatTime(String(msg?.wechatTime)), - messages: [msg], - })); - }, [messages]); -}; -``` - -#### 步骤 1.2.3:提取右键菜单组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/MessageContextMenu/index.tsx` - -```typescript -import React, { useCallback, useState } from "react"; -import { ChatRecord } from "@/pages/pc/ckbox/data"; -import ClickMenu from "../ClickMeau"; -import { useWeChatActions } from "@/hooks/weChat/useWeChatSelectors"; -import { useContactStore } from "@/store/module/weChat/contacts"; -import { fetchReCallApi, fetchVoiceToTextApi } from "../../api"; - -interface MessageContextMenuProps { - onQuote?: (messageData: ChatRecord) => void; -} - -export const MessageContextMenu: React.FC = ({ - onQuote, -}) => { - const [contextMenu, setContextMenu] = useState({ - visible: false, - x: 0, - y: 0, - messageData: null as ChatRecord | null, - }); - const [nowIsOwn, setNowIsOwn] = useState(false); - - const { - updateEnterModule, - updateShowCheckbox, - updateSelectedChatRecords, - updateQuoteMessageContent, - } = useWeChatActions(); - const { showCheckbox } = useUIStateSelectors(); - const setTransmitModal = useContactStore(state => state.setTransmitModal); - - const handleContextMenu = useCallback( - (e: React.MouseEvent, msg: ChatRecord, isOwn: boolean) => { - e.preventDefault(); - setContextMenu({ - visible: true, - x: e.clientX, - y: e.clientY, - messageData: msg, - }); - setNowIsOwn(isOwn); - }, - [], - ); - - const handleCloseContextMenu = useCallback(() => { - setContextMenu({ - visible: false, - x: 0, - y: 0, - messageData: null, - }); - }, []); - - const handleForwardMessage = useCallback( - (messageData: ChatRecord) => { - updateSelectedChatRecords([messageData]); - setTransmitModal(true); - }, - [updateSelectedChatRecords, setTransmitModal], - ); - - const handRecall = useCallback((messageData: ChatRecord) => { - fetchReCallApi({ - friendMessageId: messageData?.wechatFriendId ? messageData.id : 0, - chatroomMessageId: messageData?.wechatFriendId ? 0 : messageData.id, - seq: +new Date(), - }); - }, []); - - const handVoiceToText = useCallback((messageData: ChatRecord) => { - fetchVoiceToTextApi({ - friendMessageId: messageData?.wechatFriendId ? messageData.id : 0, - chatroomMessageId: messageData?.wechatFriendId ? 0 : messageData.id, - seq: +new Date(), - }); - }, []); - - const handCommad = useCallback( - (action: string) => { - if (!contextMenu.messageData) return; - - switch (action) { - case "transmit": - handleForwardMessage(contextMenu.messageData); - break; - case "multipleForwarding": - updateEnterModule(!showCheckbox ? "multipleForwarding" : "common"); - updateShowCheckbox(!showCheckbox); - break; - case "quote": - onQuote?.(contextMenu.messageData); - break; - case "recall": - handRecall(contextMenu.messageData); - break; - case "voiceToText": - handVoiceToText(contextMenu.messageData); - break; - default: - break; - } - handleCloseContextMenu(); - }, - [ - contextMenu.messageData, - showCheckbox, - updateEnterModule, - updateShowCheckbox, - handleForwardMessage, - handRecall, - handVoiceToText, - onQuote, - handleCloseContextMenu, - ], - ); - - return { - handleContextMenu, - contextMenu: { - ...contextMenu, - isOwn: nowIsOwn, - onClose: handleCloseContextMenu, - onCommad: handCommad, - }, - }; -}; -``` - -#### 步骤 1.2.4:重构 MessageRecord 主组件(整合 TanStack Query) - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -**重构后结构**(整合 TanStack Query 和 Sentry): - -```typescript -import React, { useRef, useEffect, useCallback, useMemo } from "react"; -import { LoadingOutlined } from "@ant-design/icons"; -import { Profiler } from "@sentry/react"; -import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { useMessageSelectors, useUIStateSelectors } from "@/hooks/weChat/useWeChatSelectors"; -import { useWeChatActions } from "@/hooks/weChat/useWeChatSelectors"; -import { useMessageParser } from "@/hooks/weChat/useMessageParser"; -import { useMessageGrouping } from "@/hooks/weChat/useMessageGrouping"; -import { useChatMessages } from "@/hooks/weChat/useChatMessages"; // 使用 TanStack Query -import { MessageContextMenu } from "./components/MessageContextMenu"; -import { MessageItem } from "./components/MessageItem"; -import { parseSystemMessage } from "@/utils/filter"; -import { addPerformanceBreadcrumb } from "@/utils/sentry"; -import styles from "./com.module.scss"; - -interface MessageRecordProps { - contract: ContractData | weChatGroup; -} - -const MessageRecord: React.FC = React.memo( - ({ contract }) => { - const messagesEndRef = useRef(null); - const messagesContainerRef = useRef(null); - const [selectedRecords, setSelectedRecords] = React.useState([]); - - // ✅ 使用 TanStack Query 获取消息数据(自动缓存) - const { - data: messagesData, - isLoading: messagesLoading, - isFetching: isFetchingMessages, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useChatMessages(contract); - - // 扁平化分页数据 - const currentMessages = useMemo(() => { - if (!messagesData?.pages) return []; - return messagesData.pages.flatMap(page => page.list || []); - }, [messagesData]); - - // 使用优化的 selector(UI 状态) - const { showCheckbox } = useUIStateSelectors(); - const { updateSelectedChatRecords } = useWeChatActions(); - - // 使用提取的 Hook - const { parseMessageContent } = useMessageParser(contract); - const groupedMessages = useMessageGrouping(currentMessages); - const { handleContextMenu, contextMenu } = MessageContextMenu({ - onQuote: handleQuote, - }); - - // 加载更多消息 - const loadMoreMessages = useCallback(() => { - if (hasNextPage && !isFetchingNextPage) { - addPerformanceBreadcrumb("加载更多消息", { - contactId: contract.id, - currentCount: currentMessages.length, - }); - fetchNextPage(); - } - }, [hasNextPage, isFetchingNextPage, fetchNextPage, contract.id, currentMessages.length]); - - // 性能监控:记录消息数量变化 - useEffect(() => { - addPerformanceBreadcrumb("消息列表更新", { - contactId: contract.id, - messageCount: currentMessages.length, - isLoading: messagesLoading, - }); - }, [currentMessages.length, contract.id, messagesLoading]); - - return ( - { - if (actualDuration > 100) { - addPerformanceBreadcrumb("MessageRecord 慢渲染", { - duration: actualDuration, - phase, - messageCount: currentMessages.length, - contractId: contract.id, - }); - } - }} - > -
- {/* 加载更多按钮 */} - {hasNextPage && ( -
- {isFetchingNextPage ? ( - - ) : ( - "点击加载更早的信息" - )} -
- )} - - {/* 消息列表渲染 */} - {groupedMessages.map((group, groupIndex) => ( - - {/* 消息渲染逻辑 */} - - ))} - -
-
- - ); - }, - (prev, next) => prev.contract.id === next.contract.id, -); - -MessageRecord.displayName = "MessageRecord"; - -export default MessageRecord; -``` - -**验收标准**: - -- [ ] MessageRecord 组件代码行数 < 300 行 -- [ ] 所有功能正常 -- [ ] 无 TypeScript 错误 -- [ ] 性能测试通过 -- [ ] TanStack Query 缓存正常工作(切换联系人时立即显示缓存数据) -- [ ] Sentry 性能数据正常记录 -- [ ] 重复请求减少(通过 React Query DevTools 验证) - ---- - -### 任务 1.3:MessageEnter 组件拆分(优先级:🔥🔥🔥) - -**预计时间**: 1天 -**影响范围**: MessageEnter 组件 - -> **同时使用 TanStack Query**:消息发送使用 TanStack Query Mutation,实现乐观更新 -> **同时使用 Sentry**:监控消息发送性能和错误 - -#### 步骤 1.3.1:提取 AI 加载指示器组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/AiLoadingIndicator.tsx` - -```typescript -import React from "react"; -import { Button } from "antd"; -import { CloseOutlined } from "@ant-design/icons"; -import styles from "../MessageEnter.module.scss"; - -interface AiLoadingIndicatorProps { - onCancel: () => void; -} - -export const AiLoadingIndicator: React.FC = React.memo( - ({ onCancel }) => { - return ( -
-
-
-
-
-
-
-
-
🧠
-
-
-
-
-
-
-
-
- AI 正在思考 - - . - . - . - -
- -
-
- 正在分析消息内容,为您生成智能回复 -
-
-
- ); - }, -); - -AiLoadingIndicator.displayName = "AiLoadingIndicator"; -``` - -#### 步骤 1.3.2:提取消息发送 Hook(使用 TanStack Query + Sentry) - -**文件**: `src/hooks/weChat/useMessageSend.ts` - -```typescript -import { useCallback, useRef } from "react"; -import { message } from "antd"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; -import { useWebSocketStore } from "@/store/module/websocket/websocket"; -import { useWeChatActions } from "./useWeChatSelectors"; -import { clearAiRequestQueue } from "@/store/module/weChat/weChat"; -import { captureError, addPerformanceBreadcrumb } from "@/utils/sentry"; - -interface UseMessageSendProps { - contract: ContractData | weChatGroup; - isLoadingAiChat: boolean; - updateIsLoadingAiChat: (loading: boolean) => void; - updateQuoteMessageContent: (content: string) => void; -} - -/** - * 消息发送 Hook - * 使用 TanStack Query Mutation 实现乐观更新和错误处理 - * 使用 Sentry 监控发送性能和错误 - */ -export const useMessageSend = ({ - contract, - isLoadingAiChat, - updateIsLoadingAiChat, - updateQuoteMessageContent, -}: UseMessageSendProps) => { - const queryClient = useQueryClient(); - const { sendCommand } = useWebSocketStore.getState(); - const contractRef = useRef(contract); - contractRef.current = contract; - - // ✅ 使用 TanStack Query Mutation - const sendMessageMutation = useMutation({ - mutationFn: async (content: string) => { - const startTime = performance.now(); - const messageId = +Date.now(); - const currentContract = contractRef.current; - - const params = { - wechatAccountId: currentContract.wechatAccountId, - wechatChatroomId: currentContract?.chatroomId ? currentContract.id : 0, - wechatFriendId: currentContract?.chatroomId ? 0 : currentContract.id, - msgSubType: 0, - msgType: 1, - content, - seq: messageId, - }; - - sendCommand("CmdSendMessage", params); - - const duration = performance.now() - startTime; - addPerformanceBreadcrumb("消息发送", { - duration, - contactId: currentContract.id, - messageLength: content.length, - }); - - return { messageId, content, params }; - }, - // ✅ 乐观更新:立即更新 UI - onMutate: async content => { - // 取消正在进行的查询 - await queryClient.cancelQueries({ - queryKey: ["chatMessages", contract.id], - }); - - // 保存当前数据快照 - const previousMessages = queryClient.getQueryData([ - "chatMessages", - contract.id, - ]); - - // 乐观更新:立即添加消息 - const messageId = +Date.now(); - const newMessage: ChatRecord = { - id: messageId, - wechatAccountId: contract.wechatAccountId, - wechatFriendId: contract?.chatroomId ? 0 : contract.id, - wechatChatroomId: contract?.chatroomId ? contract.id : 0, - tenantId: 0, - accountId: 0, - synergyAccountId: 0, - content, - msgType: 1, - msgSubType: 0, - msgSvrId: "", - isSend: true, - createTime: new Date().toISOString(), - isDeleted: false, - deleteTime: "", - sendStatus: 1, // 发送中 - wechatTime: Date.now(), - origin: 0, - msgId: 0, - recalled: false, - seq: messageId, - }; - - queryClient.setQueryData(["chatMessages", contract.id], (old: any) => { - if (!old?.pages) return old; - return { - ...old, - pages: [ - { - ...old.pages[0], - list: [newMessage, ...(old.pages[0]?.list || [])], - }, - ...old.pages.slice(1), - ], - }; - }); - - return { previousMessages }; - }, - // ✅ 错误处理:回滚乐观更新并上报错误 - onError: (error, variables, context) => { - captureError(error as Error, { - tags: { action: "sendMessage" }, - extra: { - contactId: contract.id, - content: variables, - }, - }); - - // 回滚乐观更新 - if (context?.previousMessages) { - queryClient.setQueryData( - ["chatMessages", contract.id], - context.previousMessages, - ); - } - - message.error("消息发送失败,请重试"); - }, - // ✅ 成功后使缓存失效,重新获取最新数据 - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["chatMessages", contract.id], - }); - }, - }); - - const handleSend = useCallback( - async (content?: string) => { - const messageContent = content || ""; - - if (!messageContent || !messageContent.trim()) { - console.warn("消息内容为空,取消发送"); - return; - } - - // 用户主动发送消息时,取消AI请求 - if (!content && isLoadingAiChat) { - console.log("👤 用户主动发送消息,取消AI生成"); - clearAiRequestQueue("用户主动发送"); - updateIsLoadingAiChat(false); - } - - // ✅ 使用 TanStack Query Mutation - sendMessageMutation.mutate(messageContent, { - onSuccess: () => { - updateQuoteMessageContent(""); - }, - }); - }, - [ - isLoadingAiChat, - updateIsLoadingAiChat, - updateQuoteMessageContent, - sendMessageMutation, - ], - ); - - return { - handleSend, - isSending: sendMessageMutation.isLoading, - }; -}; -``` - -#### 步骤 1.3.3:提取文件上传 Hook - -**文件**: `src/hooks/weChat/useFileUpload.ts` - -```typescript -import { useCallback, useRef } from "react"; -import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; -import { useWebSocketStore } from "@/store/module/websocket/websocket"; -import { useWeChatActions } from "./useWeChatSelectors"; - -const FileType = { - TEXT: 1, - IMAGE: 2, - VIDEO: 3, - AUDIO: 4, - FILE: 5, -}; - -const IMAGE_FORMATS = [ - "jpg", - "jpeg", - "png", - "gif", - "bmp", - "webp", - "svg", - "ico", -]; -const VIDEO_FORMATS = [ - "mp4", - "avi", - "mov", - "wmv", - "flv", - "mkv", - "webm", - "3gp", - "rmvb", -]; - -const getMsgTypeByFileFormat = (filePath: string): number => { - const extension = filePath.toLowerCase().split(".").pop() || ""; - if (IMAGE_FORMATS.includes(extension)) return 3; - if (VIDEO_FORMATS.includes(extension)) return 43; - return 49; -}; - -interface UseFileUploadProps { - contract: ContractData | weChatGroup; -} - -export const useFileUpload = ({ contract }: UseFileUploadProps) => { - const { addMessage } = useWeChatActions(); - const { sendCommand } = useWebSocketStore.getState(); - const contractRef = useRef(contract); - contractRef.current = contract; - - const handleFileUploaded = useCallback( - ( - filePath: { url: string; name: string; durationMs?: number }, - fileType: number, - ) => { - const currentContract = contractRef.current; - let msgType = 1; - let content: any = ""; - - if ([FileType.TEXT].includes(fileType)) { - msgType = getMsgTypeByFileFormat(filePath.url); - } else if ([FileType.IMAGE].includes(fileType)) { - msgType = 3; - content = filePath.url; - } else if ([FileType.AUDIO].includes(fileType)) { - msgType = 34; - content = JSON.stringify({ - url: filePath.url, - durationMs: filePath.durationMs, - }); - } else if ([FileType.FILE].includes(fileType)) { - msgType = getMsgTypeByFileFormat(filePath.url); - if (msgType === 3) { - content = filePath.url; - } - if (msgType === 43) { - content = filePath.url; - } - if (msgType === 49) { - content = JSON.stringify({ - type: "file", - title: filePath.name, - url: filePath.url, - }); - } - } - - const messageId = +Date.now(); - const params = { - wechatAccountId: currentContract.wechatAccountId, - wechatChatroomId: currentContract?.chatroomId ? currentContract.id : 0, - wechatFriendId: currentContract?.chatroomId ? 0 : currentContract.id, - msgSubType: 0, - msgType, - content: content, - seq: messageId, - }; - - const localMessage: ChatRecord = { - id: messageId, - wechatAccountId: currentContract.wechatAccountId, - wechatFriendId: currentContract?.chatroomId ? 0 : currentContract.id, - wechatChatroomId: currentContract?.chatroomId ? currentContract.id : 0, - tenantId: 0, - accountId: 0, - synergyAccountId: 0, - content: params.content, - msgType: msgType, - msgSubType: 0, - 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); - }, - [addMessage], - ); - - return { - handleFileUploaded, - }; -}; -``` - -#### 步骤 1.3.4:重构 MessageEnter 主组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx` - -**重构后结构**(简化版): - -```typescript -import React, { useState, useEffect, useCallback } from "react"; -import { Layout, Button } from "antd"; -import { SendOutlined } from "@ant-design/icons"; -import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { useUIStateSelectors, useAISelectors } from "@/hooks/weChat/useWeChatSelectors"; -import { useWeChatActions } from "@/hooks/weChat/useWeChatSelectors"; -import { useMessageSend } from "@/hooks/weChat/useMessageSend"; -import { useFileUpload } from "@/hooks/weChat/useFileUpload"; -import { AiLoadingIndicator } from "./components/AiLoadingIndicator"; -import { InputToolbar } from "./components/InputToolbar"; -import styles from "./MessageEnter.module.scss"; - -const { Footer } = Layout; - -interface MessageEnterProps { - contract: ContractData | weChatGroup; -} - -const MessageEnter: React.FC = React.memo( - ({ contract }) => { - const [inputValue, setInputValue] = useState(""); - const { EnterModule } = useUIStateSelectors(); - const { isLoadingAiChat, quoteMessageContent, aiQuoteMessageContent } = useAISelectors(); - const { - updateIsLoadingAiChat, - updateQuoteMessageContent, - } = useWeChatActions(); - - const { handleSend } = useMessageSend({ - contract, - isLoadingAiChat, - updateIsLoadingAiChat, - updateQuoteMessageContent, - }); - - const { handleFileUploaded } = useFileUpload({ contract }); - - // ... 其他逻辑 - - return ( -
- {isLoadingAiChat ? ( - - ) : ( - // 输入区域 - )} -
- ); - }, - (prev, next) => prev.contract.id === next.contract.id, -); - -MessageEnter.displayName = "MessageEnter"; - -export default MessageEnter; -``` - -**验收标准**: - -- [ ] MessageEnter 组件代码行数 < 300 行 -- [ ] 所有功能正常 -- [ ] 无 TypeScript 错误 -- [ ] 性能测试通过 -- [ ] 消息发送使用乐观更新(立即显示在列表中) -- [ ] Sentry 记录发送性能和错误 -- [ ] 发送失败时自动回滚 - ---- - -### 任务 1.4:添加 React.memo 优化(优先级:🔥🔥) - -**预计时间**: 2小时 - -#### 步骤 1.4.1:包装 ChatWindow 组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx` - -```typescript -const ChatWindow: React.FC = React.memo( - ({ contract }) => { - // ... 现有代码 - }, - (prev, next) => { - // 只有 contract.id 变化时才重新渲染 - return prev.contract.id === next.contract.id; - }, -); - -ChatWindow.displayName = "ChatWindow"; -``` - -#### 步骤 1.4.2:检查 MessageItem 的 memo 比较函数 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -**当前代码**(第302-309行): - -```typescript -(prev, next) => - prev.msg === next.msg && - prev.isGroup === next.isGroup && - prev.showCheckbox === next.showCheckbox && - prev.isSelected === next.isSelected && - prev.currentCustomerAvatar === next.currentCustomerAvatar && - prev.contract === next.contract, -``` - -**优化建议**: - -```typescript -(prev, next) => { - // 使用深度比较或 ID 比较 - return ( - prev.msg.id === next.msg.id && - prev.msg.content === next.msg.content && - prev.msg.sendStatus === next.msg.sendStatus && - prev.isGroup === next.isGroup && - prev.showCheckbox === next.showCheckbox && - prev.isSelected === next.isSelected && - prev.currentCustomerAvatar === next.currentCustomerAvatar && - prev.contract.id === next.contract.id - ); -}; -``` - -**验收标准**: - -- [ ] 所有主要组件都使用 React.memo -- [ ] 比较函数正确 -- [ ] 功能正常 - ---- - -## 第二阶段:性能优化(第2周) - -### 任务 2.0:全面迁移到 TanStack Query(优先级:🔥🔥🔥) - -**预计时间**: 2-3天(部分已在任务 1.2 和 1.3 中完成) -**影响范围**: 所有 API 数据获取 - -> **说明**:在任务 1.2 和 1.3 中已经部分使用了 TanStack Query,本任务完成剩余的数据获取迁移 -> **同时使用 Sentry**:监控 API 请求性能和错误 - -#### 步骤 2.0.1:配置 QueryClient - -**文件**: `src/providers/QueryProvider.tsx` - -```typescript -import React from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -// 创建 QueryClient 实例 -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // 数据在 5 分钟内视为新鲜,不会重新请求 - staleTime: 5 * 60 * 1000, - // 缓存时间 10 分钟 - cacheTime: 10 * 60 * 1000, - // 失败重试 1 次 - retry: 1, - // 窗口聚焦时自动刷新 - refetchOnWindowFocus: true, - // 网络重连时自动刷新 - refetchOnReconnect: true, - }, - mutations: { - // 失败重试 1 次 - retry: 1, - }, - }, -}); - -interface QueryProviderProps { - children: React.ReactNode; -} - -export const QueryProvider: React.FC = ({ children }) => { - return ( - - {children} - {/* 开发环境显示 React Query DevTools */} - {import.meta.env.MODE === "development" && ( - - )} - - ); -}; -``` - -#### 步骤 2.0.2:在应用入口使用 QueryProvider - -**文件**: `src/main.tsx` 或 `src/App.tsx` - -```typescript -import { QueryProvider } from "@/providers/QueryProvider"; - -function App() { - return ( - - {/* 应用内容 */} - - ); -} -``` - -#### 步骤 2.0.3:创建消息列表 Hook - -**文件**: `src/hooks/weChat/useChatMessages.ts` - -```typescript -import { useInfiniteQuery } from "@tanstack/react-query"; -import { getChatMessages, getChatroomMessages } from "@/pages/pc/ckbox/api"; -import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { captureError, addPerformanceBreadcrumb } from "@/utils/sentry"; - -const DEFAULT_MESSAGE_PAGE_SIZE = 20; - -/** - * 消息列表 Hook - * 使用 TanStack Query 管理消息数据,自动缓存和分页 - */ -export const useChatMessages = (contact: ContractData | weChatGroup | null) => { - return useInfiniteQuery({ - queryKey: ["chatMessages", contact?.id, contact?.wechatAccountId], - queryFn: async ({ pageParam = 1 }) => { - if (!contact) { - throw new Error("联系人信息缺失"); - } - - const startTime = performance.now(); - const params: any = { - wechatAccountId: contact.wechatAccountId, - page: pageParam, - limit: DEFAULT_MESSAGE_PAGE_SIZE, - }; - - const isGroup = "chatroomId" in contact && Boolean(contact.chatroomId); - - try { - const response = isGroup - ? await getChatroomMessages(params) - : await getChatMessages(params); - - const duration = performance.now() - startTime; - - // ✅ 使用 Sentry 记录请求性能 - addPerformanceBreadcrumb("获取消息列表", { - duration, - contactId: contact.id, - page: pageParam, - messageCount: response?.list?.length || 0, - isGroup, - }); - - // 如果请求时间超过 1 秒,记录警告 - if (duration > 1000) { - addPerformanceBreadcrumb("慢请求警告", { - duration, - contactId: contact.id, - threshold: 1000, - }); - } - - return response; - } catch (error) { - // ✅ 使用 Sentry 捕获错误 - captureError(error as Error, { - tags: { - action: "getChatMessages", - isGroup: String(isGroup), - }, - extra: { - contactId: contact.id, - page: pageParam, - params, - }, - }); - throw error; - } - }, - getNextPageParam: (lastPage, allPages) => { - const hasMore = - lastPage?.hasNext || - lastPage?.hasNextPage || - (lastPage?.list?.length || 0) >= DEFAULT_MESSAGE_PAGE_SIZE; - return hasMore ? allPages.length + 1 : undefined; - }, - enabled: !!contact, - staleTime: 5 * 60 * 1000, - cacheTime: 10 * 60 * 1000, - // ✅ 使用 Sentry 监控查询状态变化 - onError: error => { - captureError(error as Error, { - tags: { query: "useChatMessages" }, - extra: { contactId: contact?.id }, - }); - }, - }); -}; -``` - -#### 步骤 2.0.4:创建联系人列表 Hook - -**文件**: `src/hooks/weChat/useContacts.ts` - -```typescript -import { useQuery } from "@tanstack/react-query"; -import { getContactList } from "@/pages/pc/ckbox/api"; - -/** - * 联系人列表 Hook - * 使用 TanStack Query 管理联系人数据 - */ -export const useContacts = () => { - return useQuery({ - queryKey: ["contacts"], - queryFn: getContactList, - staleTime: 10 * 60 * 1000, // 10 分钟缓存 - cacheTime: 30 * 60 * 1000, // 30 分钟缓存 - refetchOnWindowFocus: true, // 窗口聚焦时刷新 - }); -}; -``` - -#### 步骤 2.0.5:迁移 MessageRecord 组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -**修改前**: - -```typescript -const { currentMessages, messagesLoading } = useMessageSelectors(); -const { loadChatMessages } = useWeChatActions(); - -// 手动调用加载 -useEffect(() => { - loadChatMessages(true); -}, [contact.id]); -``` - -**修改后**: - -```typescript -import { useChatMessages } from "@/hooks/weChat/useChatMessages"; - -const MessageRecord: React.FC = ({ contract }) => { - // 使用 TanStack Query 管理消息数据 - const { - data: messagesData, - isLoading: messagesLoading, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useChatMessages(contract); - - // 扁平化分页数据 - const currentMessages = useMemo(() => { - if (!messagesData?.pages) return []; - return messagesData.pages.flatMap(page => page.list || []); - }, [messagesData]); - - // 加载更多消息 - const loadMoreMessages = () => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - - // ... 其他逻辑 -}; -``` - -#### 步骤 2.0.6:优化消息发送(乐观更新) - -**文件**: `src/hooks/weChat/useSendMessage.ts` - -```typescript -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useWebSocketStore } from "@/store/module/websocket/websocket"; -import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; - -/** - * 消息发送 Hook(使用乐观更新) - */ -export const useSendMessage = (contact: ContractData | weChatGroup) => { - const queryClient = useQueryClient(); - const { sendCommand } = useWebSocketStore.getState(); - - return useMutation({ - mutationFn: async (content: string) => { - const messageId = +Date.now(); - const params = { - wechatAccountId: contact.wechatAccountId, - wechatChatroomId: contact?.chatroomId ? contact.id : 0, - wechatFriendId: contact?.chatroomId ? 0 : contact.id, - msgSubType: 0, - msgType: 1, - content, - seq: messageId, - }; - - sendCommand("CmdSendMessage", params); - return { messageId, content }; - }, - // 乐观更新:立即更新 UI,无需等待服务器响应 - onMutate: async content => { - // 取消正在进行的查询,避免覆盖乐观更新 - await queryClient.cancelQueries({ - queryKey: ["chatMessages", contact.id], - }); - - // 保存当前数据快照 - const previousMessages = queryClient.getQueryData([ - "chatMessages", - contact.id, - ]); - - // 乐观更新:立即添加消息到列表 - queryClient.setQueryData(["chatMessages", contact.id], (old: any) => { - const newMessage: ChatRecord = { - id: +Date.now(), - content, - isSend: true, - sendStatus: 1, // 发送中 - // ... 其他字段 - }; - return { - ...old, - pages: [ - { - ...old.pages[0], - list: [newMessage, ...(old.pages[0]?.list || [])], - }, - ...old.pages.slice(1), - ], - }; - }); - - return { previousMessages }; - }, - // 如果失败,回滚乐观更新 - onError: (err, variables, context) => { - if (context?.previousMessages) { - queryClient.setQueryData( - ["chatMessages", contact.id], - context.previousMessages, - ); - } - }, - // 成功后,使缓存失效,重新获取最新数据 - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["chatMessages", contact.id], - }); - }, - }); -}; -``` - -#### 步骤 2.0.7:更新 MessageEnter 组件使用新 Hook - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx` - -```typescript -import { useSendMessage } from "@/hooks/weChat/useSendMessage"; - -const MessageEnter: React.FC = ({ contract }) => { - const { mutate: sendMessage, isLoading: isSending } = - useSendMessage(contract); - - const handleSend = useCallback( - async (content?: string) => { - const messageContent = content || inputValue; - if (!messageContent?.trim()) return; - - sendMessage(messageContent, { - onSuccess: () => { - setInputValue(""); - }, - onError: error => { - message.error("消息发送失败"); - // Sentry 会自动捕获错误 - }, - }); - }, - [inputValue, sendMessage], - ); - - // ... 其他逻辑 -}; -``` - -**验收标准**: - -- [ ] TanStack Query 配置正确 -- [ ] 消息列表使用新 Hook -- [ ] 联系人列表使用新 Hook -- [ ] 重复请求减少 50%+(通过 React Query DevTools 验证) -- [ ] 缓存功能正常(切换联系人时立即显示缓存) -- [ ] 乐观更新正常工作 -- [ ] Sentry 性能数据正常记录 -- [ ] 功能正常,无回归 - -**预期收益**: - -- ✅ **减少 50-70% 的重复请求** -- ✅ **代码减少 30-40%** -- ✅ **用户体验显著提升**(立即显示缓存数据) -- ✅ **更好的错误处理**(Sentry 自动捕获) -- ✅ **性能监控**(Sentry 自动追踪) - ---- - -### 任务 2.1:实现虚拟滚动(优先级:🔥🔥🔥) - -**预计时间**: 2天 -**影响范围**: MessageRecord 组件 - -> **同时使用 Sentry**:监控虚拟滚动性能,记录滚动流畅度 -> **配合 TanStack Query**:虚拟滚动配合 TanStack Query 的分页加载 - -#### 步骤 2.1.1:安装依赖(如果需要) - -```bash -# react-window 已安装,无需额外安装 -npm list react-window -``` - -#### 步骤 2.1.2:创建虚拟滚动组件 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VirtualizedMessageList.tsx` - -```typescript -import React, { useRef, useEffect, useCallback } from "react"; -import { FixedSizeList, ListChildComponentProps } from "react-window"; -import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { MessageGroup } from "@/hooks/weChat/useMessageGrouping"; -import { MessageItem } from "./MessageItem"; -import { parseSystemMessage } from "@/utils/filter"; -import styles from "../com.module.scss"; - -interface VirtualizedMessageListProps { - groupedMessages: MessageGroup[]; - contract: ContractData | weChatGroup; - isGroupChat: boolean; - showCheckbox: boolean; - currentCustomerAvatar?: string; - renderGroupUser: (msg: ChatRecord) => { avatar: string; nickname: string }; - clearWechatidInContent: (sender: any, content: string) => string; - parseMessageContent: ( - content: string | null | undefined, - msg: ChatRecord, - msgType?: number, - ) => React.ReactNode; - isMessageSelected: (msg: ChatRecord) => boolean; - onCheckboxChange: (checked: boolean, msg: ChatRecord) => void; - onContextMenu: (e: React.MouseEvent, msg: ChatRecord, isOwn: boolean) => void; - containerRef: React.RefObject; -} - -/** - * 虚拟滚动消息列表项 - */ -const VirtualizedMessageItem: React.FC< - ListChildComponentProps<{ - groups: MessageGroup[]; - props: Omit; - }> -> = ({ index, style, data }) => { - const { groups, props } = data; - const group = groups[index]; - - return ( -
- {/* 时间分隔符 */} - {group.messages - .filter(v => [10000, -10001].includes(v.msgType)) - .map(msg => { - const parsedText = parseSystemMessage(msg.content); - return ( -
- {parsedText} -
- ); - })} - - {/* 其他系统消息 */} - {group.messages - .filter(v => [570425393, 90000].includes(v.msgType)) - .map(msg => { - let displayContent = msg.content; - try { - const parsedContent = JSON.parse(msg.content); - if ( - parsedContent && - typeof parsedContent === "object" && - parsedContent.content - ) { - displayContent = parsedContent.content; - } - } catch (error) { - // 忽略解析错误 - } - return ( -
- {displayContent} -
- ); - })} - - {/* 时间标签 */} -
{group.time}
- - {/* 消息项 */} - {group.messages - .filter(v => ![10000, 570425393, 90000, -10001].includes(v.msgType)) - .map(msg => { - if (!msg) return null; - return ( - - ); - })} -
- ); -}; - -/** - * 虚拟滚动消息列表 - */ -export const VirtualizedMessageList: React.FC = ({ - groupedMessages, - containerRef, - ...props -}) => { - const listRef = useRef(null); - const [listHeight, setListHeight] = React.useState(600); - - // 监听容器高度变化 - useEffect(() => { - const updateHeight = () => { - if (containerRef.current) { - const height = containerRef.current.clientHeight; - setListHeight(height); - } - }; - - updateHeight(); - const resizeObserver = new ResizeObserver(updateHeight); - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [containerRef]); - - // 滚动到底部 - const scrollToBottom = useCallback(() => { - if (listRef.current && groupedMessages.length > 0) { - listRef.current.scrollToItem(groupedMessages.length - 1, "end"); - } - }, [groupedMessages.length]); - - // 当新消息到达时,滚动到底部 - useEffect(() => { - scrollToBottom(); - }, [groupedMessages.length, scrollToBottom]); - - // 估算每个消息组的高度(可以根据实际情况调整) - const estimateItemSize = useCallback((index: number) => { - const group = groupedMessages[index]; - // 基础高度:时间标签 + 消息项 - const baseHeight = 40; // 时间标签高度 - const messageHeight = 60; // 每条消息平均高度 - return baseHeight + group.messages.length * messageHeight; - }, [groupedMessages]); - - // 使用平均高度(简化实现,实际可以使用 VariableSizeList) - const averageItemSize = useMemo(() => { - if (groupedMessages.length === 0) return 100; - const totalSize = groupedMessages.reduce( - (sum, group) => sum + 40 + group.messages.length * 60, - 0, - ); - return Math.ceil(totalSize / groupedMessages.length); - }, [groupedMessages]); - - return ( - - {VirtualizedMessageItem} - - ); -}; -``` - -#### 步骤 2.1.3:集成到 MessageRecord - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -```typescript -import { VirtualizedMessageList } from "./components/VirtualizedMessageList"; - -// 在 MessageRecord 组件中使用 -const MessageRecord: React.FC = ({ contract }) => { - // ... 现有代码 - - // 如果消息数量 > 50,使用虚拟滚动 - const shouldUseVirtualization = groupedMessages.length > 50; - - return ( -
- {shouldUseVirtualization ? ( - - ) : ( - // 原有的渲染逻辑(小列表不使用虚拟滚动) - )} -
- ); -}; -``` - -**验收标准**: - -- [ ] 100+ 条消息滚动流畅(FPS > 30) -- [ ] 1000+ 条消息可以正常加载 -- [ ] 内存占用明显降低(< 100MB for 1000 messages) -- [ ] Sentry 记录滚动性能数据 -- [ ] 配合 TanStack Query 分页加载正常 -- [ ] 功能正常 - -**性能监控**: - -```typescript -// 在虚拟滚动组件中添加性能监控 -useEffect(() => { - const observer = new PerformanceObserver(list => { - for (const entry of list.getEntries()) { - if (entry.entryType === "measure" && entry.name === "scroll") { - addPerformanceBreadcrumb("虚拟滚动性能", { - duration: entry.duration, - messageCount: groupedMessages.length, - }); - } - } - }); - observer.observe({ entryTypes: ["measure"] }); - return () => observer.disconnect(); -}, [groupedMessages.length]); -``` - ---- - -### 任务 2.2:useMemo/useCallback 优化(优先级:🔥🔥) - -**预计时间**: 1天 - -#### 步骤 2.2.1:优化 groupedMessages - -已在 `useMessageGrouping.ts` 中使用 `useMemo`,无需额外优化。 - -#### 步骤 2.2.2:优化 renderGroupUser - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` - -```typescript -const renderGroupUser = useCallback( - (msg: ChatRecord) => { - if (!msg) { - return { avatar: "", nickname: "" }; - } - - const member = msg.senderWechatId - ? groupMemberMap.get(msg.senderWechatId) - : undefined; - - return { - avatar: member?.avatar || msg?.avatar, - nickname: member?.nickname || msg?.senderNickname, - }; - }, - [groupMemberMap], // 依赖 groupMemberMap -); -``` - -#### 步骤 2.2.3:优化其他回调函数 - -检查所有 `useCallback` 的依赖项,确保正确。 - -**验收标准**: - -- [ ] 所有回调函数都使用 useCallback -- [ ] 依赖项正确 -- [ ] 无性能警告 - ---- - -## 第三阶段:高级优化(第3-4周) - -### 任务 3.1:图片懒加载(优先级:⚠️) - -**预计时间**: 1天 - -#### 步骤 3.1.1:创建图片懒加载组件 - -**文件**: `src/components/LazyImage/index.tsx` - -```typescript -import React, { useState, useEffect, useRef } from "react"; - -interface LazyImageProps { - src: string; - alt: string; - style?: React.CSSProperties; - className?: string; - onClick?: () => void; - fallbackText?: string; -} - -export const LazyImage: React.FC = ({ - src, - alt, - style, - className, - onClick, - fallbackText, -}) => { - const [isLoaded, setIsLoaded] = useState(false); - const [isInView, setIsInView] = useState(false); - const imgRef = useRef(null); - - useEffect(() => { - const observer = new IntersectionObserver( - entries => { - entries.forEach(entry => { - if (entry.isIntersecting) { - setIsInView(true); - observer.unobserve(entry.target); - } - }); - }, - { - rootMargin: "50px", // 提前50px开始加载 - }, - ); - - if (imgRef.current) { - observer.observe(imgRef.current); - } - - return () => { - if (imgRef.current) { - observer.unobserve(imgRef.current); - } - }; - }, []); - - return ( -
- {isInView ? ( - {alt} setIsLoaded(true)} - onError={() => { - if (fallbackText) { - // 显示错误文本 - } - }} - /> - ) : ( -
加载中...
- )} -
- ); -}; -``` - -#### 步骤 3.1.2:在消息渲染中使用 - -更新 `useMessageParser.ts` 中的图片渲染逻辑。 - -**验收标准**: - -- [ ] 图片按需加载 -- [ ] 滚动性能提升 -- [ ] 内存占用降低 - ---- - -### 任务 3.2:代码分割(优先级:⚠️) - -**预计时间**: 0.5天 - -#### 步骤 3.2.1:路由懒加载 - -**文件**: `src/router/index.tsx` - -```typescript -import { lazy, Suspense } from "react"; -import { Spin } from "antd"; - -const ChatWindow = lazy( - () => import("@/pages/pc/ckbox/weChat/components/ChatWindow"), -); - -const LoadingFallback = () => ( -
- -
-); - -// 在路由中使用 -}> - - -``` - -#### 步骤 3.2.2:组件懒加载 - -**文件**: `src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx` - -```typescript -import { lazy, Suspense } from "react"; - -const ProfileCard = lazy(() => import("./components/ProfileCard")); -const FollowupReminderModal = lazy( - () => import("./components/FollowupReminderModal"), -); -const TodoListModal = lazy(() => import("./components/TodoListModal")); -``` - -**验收标准**: - -- [ ] 首屏加载时间减少 -- [ ] 代码分割正确 -- [ ] 功能正常 - ---- - -## 测试与验证 - -### 性能测试清单 - -#### 1. 功能测试 - -- [ ] 消息发送功能正常 -- [ ] 消息接收功能正常 -- [ ] 文件上传功能正常 -- [ ] AI 功能正常 -- [ ] 右键菜单功能正常 -- [ ] 消息转发功能正常 - -#### 2. 性能测试 - -- [ ] 100 条消息滚动流畅(FPS > 30) -- [ ] 500 条消息滚动流畅(FPS > 30) -- [ ] 1000 条消息可以正常加载 -- [ ] 输入框响应时间 < 50ms -- [ ] 组件重渲染次数减少 50%+ - -#### 3. 内存测试 - -- [ ] 1000 条消息内存占用 < 100MB -- [ ] 长时间使用无内存泄漏 - -#### 4. 兼容性测试 - -- [ ] Chrome 浏览器正常 -- [ ] Firefox 浏览器正常 -- [ ] Edge 浏览器正常 - ---- - -## 回滚方案 - -### 如果优化导致问题 - -1. **立即回滚** - - ```bash - git revert - ``` - -2. **分阶段回滚** - - 如果某个任务有问题,只回滚该任务 - - 保留其他已完成的优化 - -3. **回滚检查清单** - - [ ] 功能恢复正常 - - [ ] 性能恢复到优化前水平 - - [ ] 无错误日志 - ---- - -## 进度跟踪 - -### 第一周进度 - -- [ ] ✅ 任务 0.1:Sentry 和 TanStack Query 基础配置(已完成) -- [ ] 任务 1.1:Zustand Selector 优化(2小时,使用 Sentry 监控) -- [ ] 任务 1.2:MessageRecord 组件拆分(1天,整合 TanStack Query + Sentry) -- [ ] 任务 1.3:MessageEnter 组件拆分(1天,整合 TanStack Query + Sentry) -- [ ] 任务 1.4:React.memo 优化(2小时,使用 Sentry 监控) - -### 第二周进度 - -- [ ] 任务 2.0:全面迁移到 TanStack Query(2-3天,部分已在第一周完成) -- [ ] 任务 2.1:虚拟滚动实现(2天,配合 TanStack Query + Sentry 监控) -- [ ] 任务 2.2:useMemo/useCallback 优化(1天,使用 Sentry 监控) - -### 第三周进度 - -- [ ] 任务 3.1:图片懒加载 -- [ ] 任务 3.2:代码分割 - -### 第四周进度 - -- [ ] 性能测试与优化 -- [ ] 文档更新 -- [ ] 代码审查 - ---- - -## 注意事项 - -1. **每次任务完成后** - - 提交代码 - - 更新进度 - - 进行功能测试 - -2. **遇到问题** - - 记录问题 - - 分析原因 - - 寻求帮助 - -3. **性能监控**(贯穿整个优化过程) - - ✅ **Sentry**:自动监控组件渲染、API 请求、错误 - - ✅ **TanStack Query DevTools**:监控数据缓存和请求状态(开发环境) - - ✅ **React DevTools Profiler**:手动分析组件性能 - - ✅ **Chrome Performance**:分析运行时性能 - - 记录性能指标 - - 对比优化前后 - -**性能监控最佳实践**: - -```typescript -// 1. 在关键组件使用 Sentry Profiler - - - - -// 2. 在 API 请求中使用性能监控 -const startTime = performance.now(); -const data = await apiCall(); -addPerformanceBreadcrumb("API请求", { - duration: performance.now() - startTime, -}); - -// 3. 使用 TanStack Query DevTools 查看缓存状态 -// 开发环境自动显示,无需额外配置 -``` - ---- - -## 📊 优化工具使用指南 - -### Sentry 使用 - -1. **查看错误报告** - - 访问 Sentry 后台 - - 查看错误趋势和详情 - - 分析错误上下文 - -2. **查看性能数据** - - 查看 Transactions 页面 - - 分析慢请求和慢组件 - - 识别性能瓶颈 - -3. **错误回放** - - 查看用户操作回放 - - 重现错误场景 - - 快速定位问题 - -### TanStack Query 使用 - -1. **开发环境 DevTools** - - 打开 React Query DevTools - - 查看查询状态和缓存 - - 调试查询问题 - -2. **缓存管理** - - 使用 `queryClient.invalidateQueries` 使缓存失效 - - 使用 `queryClient.setQueryData` 手动更新缓存 - - 使用 `queryClient.getQueryData` 读取缓存 - -3. **性能优化** - - 调整 `staleTime` 和 `cacheTime` - - 使用 `enabled` 控制查询时机 - - 使用 `refetchOnWindowFocus` 控制自动刷新 - ---- - -**文档维护**: 每次任务完成后更新进度和遇到的问题 - ---- - -## 🎯 优化工具协同使用指南 - -### 在性能优化过程中同时使用 Sentry 和 TanStack Query - -#### 1. 组件优化时 - -**使用 Sentry Profiler 监控渲染性能**: - -```typescript -import { Profiler } from "@sentry/react"; -import { addPerformanceBreadcrumb } from "@/utils/sentry"; - - { - addPerformanceBreadcrumb("组件渲染", { - component: "ComponentName", - phase, - duration: actualDuration, - optimization: "selector-optimized", // 标记优化类型 - }); - }} -> - - -``` - -#### 2. 数据获取时 - -**使用 TanStack Query + Sentry 监控**: - -```typescript -import { useQuery } from "@tanstack/react-query"; -import { captureError, addPerformanceBreadcrumb } from "@/utils/sentry"; - -const { data, isLoading, error } = useQuery({ - queryKey: ["data"], - queryFn: async () => { - const startTime = performance.now(); - try { - const result = await fetchData(); - addPerformanceBreadcrumb("数据获取", { - duration: performance.now() - startTime, - dataSize: result.length, - }); - return result; - } catch (err) { - captureError(err as Error, { - tags: { query: "fetchData" }, - }); - throw err; - } - }, -}); -``` - -#### 3. 消息发送时 - -**使用 TanStack Query Mutation + Sentry**: - -```typescript -import { useMutation } from "@tanstack/react-query"; -import { captureError } from "@/utils/sentry"; - -const mutation = useMutation({ - mutationFn: sendMessage, - onError: error => { - captureError(error, { - tags: { action: "sendMessage" }, - }); - }, -}); -``` - -#### 4. 性能对比 - -**优化前后对比**: - -```typescript -// 优化前 -addPerformanceBreadcrumb("优化前 - MessageRecord", { - renderTime: 150, - messageCount: 100, -}); - -// 优化后 -addPerformanceBreadcrumb("优化后 - MessageRecord", { - renderTime: 50, // 提升 66% - messageCount: 100, - optimization: "memo + query", -}); -``` - -### 工具分工 - -| 工具 | 职责 | 使用场景 | -| ---------------------- | ------------------ | --------------------------------------- | -| **Sentry** | 错误监控、性能追踪 | 所有错误、慢请求、组件渲染性能 | -| **TanStack Query** | 数据缓存、请求管理 | 所有 API 数据获取、消息列表、联系人列表 | -| **React DevTools** | 组件分析 | 手动分析组件性能 | -| **Chrome Performance** | 运行时分析 | 分析长任务、内存占用 | - -### 最佳实践 - -1. **每个优化任务都添加 Sentry 监控** - - 记录优化前的性能 - - 记录优化后的性能 - - 对比优化效果 - -2. **所有 API 请求使用 TanStack Query** - - 自动缓存 - - 自动重试 - - 自动错误处理 - -3. **关键操作添加错误监控** - - 消息发送 - - 文件上传 - - WebSocket 连接 - -4. **定期查看 Sentry 数据** - - 发现性能瓶颈 - - 追踪优化效果 - - 快速定位问题 - ---- - -**最后更新**: 2025-01-XX -**版本**: v2.0(整合 Sentry 和 TanStack Query 到所有优化任务) diff --git a/Touchkebao/提示词/本地数据库缓存分析.md b/Touchkebao/提示词/本地数据库缓存分析.md new file mode 100644 index 00000000..ecdee839 --- /dev/null +++ b/Touchkebao/提示词/本地数据库缓存分析.md @@ -0,0 +1,287 @@ +# 本地数据库(IndexedDB)缓存必要性分析 + +## 一、当前使用情况 + +### 1.1 会话列表(Chat Sessions) + +**使用方式**: +- ✅ 一次性加载全部会话数据 +- ✅ 同步到本地数据库 +- ✅ 切换账号时从本地数据库过滤 + +**本地数据库的作用**: +- 快速显示:首次加载时先显示本地数据,后台同步服务器 +- 离线访问:网络断开时仍可查看历史会话 +- 减少API调用:切换账号时不需要重新请求 + +### 1.2 联系人列表(Contacts)- 旧架构 + +**使用方式**: +- ✅ 一次性加载所有好友和群(`getAllFriends` + `getAllGroups`) +- ✅ 同步到本地数据库(`syncContactsFromServer`) +- ✅ 分组统计从本地数据库查询(`getGroupStatistics`) +- ✅ 分组联系人从本地数据库分页获取(`getContactsByGroupPaginated`) + +**本地数据库的作用**: +- 快速显示:先显示本地数据,后台同步 +- 分组统计:快速统计分组数量 +- 分页加载:从本地数据库分页获取 + +## 二、新架构下的变化 + +### 2.1 会话列表(保持不变) + +**仍然需要本地数据库**: +- ✅ 一次性加载全部,本地缓存价值高 +- ✅ 切换账号时快速过滤 +- ✅ 离线访问 + +### 2.2 联系人列表(重大变化) + +**新架构**: +- ❌ **不再一次性加载全部**:改为按分组懒加载 +- ✅ **分组列表**:一次性加载分组信息(数量少,10-50个) +- ✅ **分组联系人**:按需加载(点击展开时加载) +- ✅ **搜索**:直接调用API,不依赖本地数据 +- ✅ **分组统计**:从API获取(`getGroupStatistics` 改为调用API) + +## 三、本地数据库价值分析 + +### 3.1 联系人列表 - 是否需要本地数据库? + +#### 方案A:完全移除本地数据库(不推荐) + +**优点**: +- ✅ 架构简单,不需要维护数据同步 +- ✅ 数据始终最新,不会出现不一致 +- ✅ 减少存储空间占用 + +**缺点**: +- ❌ **每次打开都需要加载**:分组列表、分组统计都需要调用API +- ❌ **切换账号慢**:需要重新加载所有展开的分组 +- ❌ **离线不可用**:网络断开时无法查看联系人 +- ❌ **用户体验差**:每次操作都有loading,没有缓存加速 + +#### 方案B:保留本地数据库,但改变使用方式(推荐) + +**保留的原因**: + +1. **分组列表缓存**(价值:⭐⭐⭐) + ```typescript + // 分组列表数量少(10-50个),变化不频繁 + // 缓存后可以快速显示,减少API调用 + // 但需要定期更新(例如:每小时或每次打开时检查) + ``` + +2. **分组联系人缓存**(价值:⭐⭐⭐⭐⭐) + ```typescript + // 按分组缓存已加载的联系人数据 + // 切换账号时,如果之前加载过,可以直接显示 + // 大幅提升切换账号的速度 + // 缓存策略:按 groupKey 缓存,TTL 1小时 + ``` + +3. **分组统计缓存**(价值:⭐⭐⭐) + ```typescript + // 分组数量统计可以缓存 + // 快速显示分组列表,后台同步最新数据 + // 缓存策略:TTL 30分钟 + ``` + +4. **离线访问**(价值:⭐⭐⭐⭐) + ```typescript + // 网络断开时,仍可查看已缓存的联系人 + // 提升用户体验 + ``` + +**改变的使用方式**: + +1. **不再一次性同步全部数据** + ```typescript + // ❌ 旧方式:syncContactsFromServer(一次性加载全部) + // ✅ 新方式:按分组懒加载,只缓存已加载的分组 + ``` + +2. **搜索不依赖本地数据** + ```typescript + // ❌ 旧方式:从本地数据库搜索 + // ✅ 新方式:直接调用API搜索 + ``` + +3. **分组统计改为API优先** + ```typescript + // ❌ 旧方式:从本地数据库统计 + // ✅ 新方式:调用API获取,本地数据库作为缓存 + ``` + +## 四、推荐的缓存策略 + +### 4.1 会话列表缓存(保持不变) + +```typescript +// 缓存策略:全量缓存 +// 更新策略:WebSocket实时更新 + 定期全量同步 +// TTL:无限制(通过版本号控制) +``` + +### 4.2 联系人列表缓存(新策略) + +#### 4.2.1 分组列表缓存 + +```typescript +interface GroupListCache { + groups: ContactGroup[]; // 分组列表 + lastUpdateTime: number; // 最后更新时间 + ttl: number; // 缓存有效期(30分钟) +} + +// 使用策略: +// 1. 首次加载:调用API获取分组列表 +// 2. 后续加载:检查缓存是否有效 +// - 有效 → 直接使用缓存,后台更新 +// - 无效 → 调用API更新 +``` + +#### 4.2.2 分组联系人缓存 + +```typescript +interface GroupContactsCache { + groupKey: string; // `${groupId}_${groupType}_${accountId}` + contacts: Contact[]; // 已加载的联系人 + page: number; // 当前页码 + hasMore: boolean; // 是否还有更多 + lastUpdateTime: number; // 最后更新时间 + ttl: number; // 缓存有效期(1小时) +} + +// 使用策略: +// 1. 展开分组时:检查缓存 +// - 有缓存且有效 → 直接显示,后台更新 +// - 无缓存或失效 → 调用API加载 +// 2. 切换账号时:检查缓存 +// - 有缓存 → 直接显示(快速切换) +// - 无缓存 → 调用API加载 +``` + +#### 4.2.3 分组统计缓存 + +```typescript +interface GroupStatsCache { + accountId: number; + stats: Map; // groupKey → count + lastUpdateTime: number; + ttl: number; // 缓存有效期(30分钟) +} + +// 使用策略: +// 1. 显示分组列表时:检查缓存 +// - 有缓存 → 显示缓存数据,后台更新 +// - 无缓存 → 调用API获取 +``` + +### 4.3 搜索功能(不使用缓存) + +```typescript +// 搜索不缓存,原因: +// 1. 搜索结果需要实时性 +// 2. 搜索关键词变化频繁 +// 3. 缓存命中率低 +``` + +## 五、缓存更新策略 + +### 5.1 实时更新(WebSocket) + +```typescript +// WebSocket收到新联系人时: +// 1. 更新对应分组的缓存(如果已缓存) +// 2. 更新分组统计缓存 +// 3. 不更新搜索缓存(搜索不缓存) +``` + +### 5.2 定期更新 + +```typescript +// 1. 分组列表:每次打开时检查,超过30分钟则更新 +// 2. 分组联系人:每次展开时检查,超过1小时则更新 +// 3. 分组统计:每次显示时检查,超过30分钟则更新 +``` + +### 5.3 手动刷新 + +```typescript +// 用户点击刷新时: +// 1. 清空所有联系人缓存 +// 2. 重新加载分组列表和统计 +// 3. 重新加载已展开的分组 +``` + +## 六、实施建议 + +### 6.1 保留本地数据库,但优化使用方式 + +**理由**: +1. ✅ **提升用户体验**:缓存可以快速显示,减少loading时间 +2. ✅ **减少API调用**:已加载的数据不需要重复请求 +3. ✅ **离线访问**:网络断开时仍可查看已缓存的数据 +4. ✅ **切换账号优化**:切换账号时,如果之前加载过,可以直接显示 + +**优化点**: +1. ✅ **改变同步策略**:不再一次性同步全部,改为按需缓存 +2. ✅ **搜索不缓存**:搜索直接调用API,保证实时性 +3. ✅ **缓存TTL**:设置合理的缓存有效期,避免数据过期 +4. ✅ **缓存清理**:定期清理过期缓存,控制存储空间 + +### 6.2 缓存架构设计 + +```typescript +interface ContactCacheManager { + // 分组列表缓存 + getGroupList: (accountId: number) => Promise; + setGroupList: (accountId: number, groups: ContactGroup[]) => void; + + // 分组联系人缓存 + getGroupContacts: (groupKey: string) => Promise; + setGroupContacts: (groupKey: string, data: GroupContactData) => void; + + // 分组统计缓存 + getGroupStats: (accountId: number) => Promise | null>; + setGroupStats: (accountId: number, stats: Map) => void; + + // 缓存清理 + clearExpiredCache: () => void; + clearAllCache: () => void; +} +``` + +## 七、总结 + +### 7.1 是否需要本地数据库? + +**答案:需要,但使用方式需要改变** + +### 7.2 保留的原因 + +1. ✅ **性能优化**:缓存可以大幅提升加载速度 +2. ✅ **用户体验**:减少loading时间,支持离线访问 +3. ✅ **减少API调用**:已加载的数据不需要重复请求 +4. ✅ **切换账号优化**:切换账号时快速显示已缓存的数据 + +### 7.3 改变的使用方式 + +1. ✅ **不再一次性同步全部**:改为按分组懒加载和缓存 +2. ✅ **搜索不缓存**:搜索直接调用API,保证实时性 +3. ✅ **设置缓存TTL**:避免数据过期,定期更新 +4. ✅ **按需缓存**:只缓存已加载的分组,不缓存未加载的数据 + +### 7.4 推荐的缓存策略 + +| 数据类型 | 是否缓存 | TTL | 更新策略 | +|---------|---------|-----|---------| +| 会话列表 | ✅ 是 | 无限制 | WebSocket实时更新 | +| 分组列表 | ✅ 是 | 30分钟 | 每次打开时检查 | +| 分组联系人 | ✅ 是 | 1小时 | 展开时检查,WebSocket更新 | +| 分组统计 | ✅ 是 | 30分钟 | 显示时检查,WebSocket更新 | +| 搜索结果 | ❌ 否 | - | 直接调用API | + +**结论**:保留本地数据库,但改变使用方式,从"全量同步"改为"按需缓存",既能享受缓存带来的性能提升,又能避免全量同步的缺点。 diff --git a/Touchkebao/提示词/聊天功能逻辑.md b/Touchkebao/提示词/聊天功能逻辑.md new file mode 100644 index 00000000..2325f426 --- /dev/null +++ b/Touchkebao/提示词/聊天功能逻辑.md @@ -0,0 +1,661 @@ +# 聊天功能逻辑文档 + +## 一、整体架构 + +### 1.1 核心组件 + +``` +ChatWindow (聊天窗口容器) +├── MessageList (会话列表 - 左侧) +├── MessageRecord (消息记录 - 中间) +└── MessageEnter (消息输入 - 底部) +``` + +### 1.2 状态管理模块 + +- **`weChat/weChat.ts`**: 聊天核心状态(当前联系人、消息列表、AI状态等) +- **`weChat/contacts.ts`**: 联系人管理 +- **`weChat/message.ts`**: 会话列表状态 +- **`websocket/websocket.ts`**: WebSocket连接管理 +- **`websocket/msgManage.ts`**: WebSocket消息处理 + +### 1.3 数据存储 + +- **IndexedDB (Dexie)**: + - `chatSessions`: 会话列表 + - `contactsUnified`: 联系人数据 + - `messages`: 消息记录(按需存储) + +--- + +## 二、会话列表管理 (MessageList) + +### 2.1 初始化流程 + +``` +1. 组件挂载 + ↓ +2. 从 IndexedDB 加载缓存会话列表(立即显示) + ↓ +3. 后台同步服务器数据(分页,每页500条) + ↓ +4. 同步完成后异步补充未知联系人详情 + ↓ +5. 订阅数据库变更,自动更新UI +``` + +### 2.2 数据同步机制 + +**同步策略**: + +- **首次加载**: 先显示缓存,后台同步服务器数据 +- **分页同步**: 每页500条,逐页同步,立即更新UI +- **增量更新**: 通过WebSocket实时更新 +- **未知联系人补充**: 同步完成后异步拉取缺失的头像、昵称等信息 + +**同步状态管理**: + +```typescript +// 同步状态栏显示 +- 同步中: 显示"同步中..." + Loading图标 +- 同步完成: 显示"同步完成" + 手动同步按钮 +``` + +### 2.3 会话列表操作 + +**置顶/取消置顶**: + +1. 立即更新UI(乐观更新) +2. 调用API更新服务器 +3. 更新数据库 +4. 失败时回滚UI + +**删除会话**: + +1. 立即从UI移除 +2. 调用API更新配置(`chat: false`) +3. 从数据库删除 +4. 失败时恢复UI + +**修改备注**: + +1. 立即更新UI +2. 通过WebSocket发送命令(`CmdModifyFriendRemark` / `CmdModifyGroupRemark`) +3. 调用API更新 +4. 更新数据库 +5. 失败时回滚 + +### 2.4 WebSocket消息更新 + +**事件流程**: + +``` +WebSocket收到新消息 + ↓ +msgManage.ts 处理 CmdNewMessage + ↓ +触发 chatMessageReceived 自定义事件 + ↓ +MessageList 监听事件 + ↓ +更新会话列表(内容、未读数、时间) +``` + +**处理逻辑**: + +- 已存在会话:更新消息内容、未读数、最后更新时间 +- 新会话:从联系人表构建会话,或从接口获取详情 + +--- + +## 三、消息发送流程 + +### 3.1 发送流程 + +``` +用户输入/选择文件 + ↓ +MessageEnter.handleSend() + ↓ +1. 构造本地消息对象(临时ID = 时间戳) +2. 立即添加到消息列表(乐观更新) +3. 通过WebSocket发送命令(CmdSendMessage) + ↓ +WebSocket响应(CmdSendMessageResp) + ↓ +更新消息状态(sendStatus: 0, 真实ID) +``` + +### 3.2 消息类型 + +| 类型 | msgType | 说明 | +| -------- | ---------------- | ------------ | +| 文本 | 1 | 普通文本消息 | +| 图片 | 3 | 图片消息 | +| 音频 | 34 | 语音消息 | +| 视频 | 43 | 视频消息 | +| 文件 | 49 | 文件消息 | +| 系统消息 | 10000, -10001 | 时间分隔线 | +| 特殊消息 | 570425393, 90000 | 其他系统消息 | + +### 3.3 文件上传处理 + +**图片/文件上传**: + +1. 用户选择文件 +2. 上传到服务器获取URL +3. 构造消息对象(msgType根据文件格式判断) +4. 发送消息 + +**音频录制**: + +1. 用户录制音频 +2. 上传音频文件获取URL +3. 构造消息对象(msgType: 34,包含durationMs) +4. 发送消息 + +**位置消息**: + +1. 用户选择位置 +2. 构造位置消息(JSON格式) +3. 发送消息 + +--- + +## 四、消息接收流程 + +### 4.1 WebSocket消息接收 + +``` +WebSocket.onmessage + ↓ +websocket.ts._handleMessage() + ↓ +msgManage.ts.msgManageCore() + ↓ +根据 cmdType 路由到对应处理器 +``` + +### 4.2 新消息处理 (CmdNewMessage) + +**处理流程**: + +```typescript +1. 调用 weChatStore.receivedMsg() 处理消息 +2. 异步同步到服务器(dataProcessing) +3. 触发 chatMessageReceived 事件 +4. MessageList 更新会话列表 +``` + +**receivedMsg 核心逻辑**: + +```typescript +1. 判断是否为当前聊天 + - 是:批量更新消息列表(16ms延迟,减少重渲染) + - 否:仅更新会话列表 + +2. AI处理(如果是文字消息且对方发送) + - 检查AI模式(aiType: 0=人工, 1=AI辅助, 2=AI接管) + - 防抖处理(3秒延迟,避免频繁请求) + - 消息队列(pendingMessages) + - 生成AI回复 +``` + +### 4.3 消息批量更新机制 + +**优化策略**: + +- **批量队列**: 16ms内收到的消息加入队列 +- **批量更新**: 16ms后一次性更新,减少重渲染 +- **性能提升**: 高频消息场景下显著减少渲染次数 + +```typescript +// 批量更新逻辑 +messageBatchQueue.push(message); +messageBatchTimer = setTimeout(() => { + const messagesToAdd = [...messageBatchQueue]; + messageBatchQueue = []; + set(state => ({ + currentMessages: [...state.currentMessages, ...messagesToAdd], + })); +}, 16); // 约一帧时间 +``` + +### 4.4 消息状态更新 + +**发送状态更新**: + +- `sendStatus: 1` - 发送中(显示Loading图标) +- `sendStatus: 0` - 发送成功(收到CmdSendMessageResp后更新) + +**文件下载状态**: + +- `isDownloading: true` - 下载中 +- `url` - 下载完成,设置文件URL + +--- + +## 五、AI处理逻辑 + +### 5.1 AI模式 + +| 模式 | aiType | 说明 | +| -------- | ------ | ---------------------------------------- | +| 人工接待 | 0 | 完全人工处理 | +| AI辅助 | 1 | AI生成回复,填充到输入框,人工确认后发送 | +| AI接管 | 2 | AI自动生成并发送回复 | + +### 5.2 AI触发机制 + +**自动触发**(收到新消息时): + +``` +收到文字消息(msgType === 1) + ↓ +检查AI模式(aiType === 1 或 2) + ↓ +防抖处理(3秒延迟) + ↓ +消息队列(pendingMessages) + ↓ +3秒内无新消息 → 开始处理 +``` + +**手动触发**(用户点击重新生成): + +``` +用户点击"重新生成"按钮 + ↓ +清除之前的AI请求 + ↓ +获取最近5条对方消息作为上下文 + ↓ +直接调用AI接口(不经过dataProcessing) +``` + +### 5.3 AI处理流程 + +**自动触发流程**: + +``` +1. 收到新消息,加入队列 +2. 3秒延迟(防抖) +3. 调用 dataProcessing(批量处理消息) +4. 调用 aiChat(生成回复) +5. 根据AI模式处理回复: + - AI辅助(aiType=1): 填充到输入框(quoteMessageContent) + - AI接管(aiType=2): 直接发送消息 +``` + +**手动触发流程**: + +``` +1. 清除之前的AI请求 +2. 获取最近5条对方消息 +3. 直接调用 aiChat(不调用dataProcessing) +4. 根据AI模式处理回复 +``` + +### 5.4 AI请求取消机制 + +**取消场景**: + +- 用户开始输入 +- 用户主动发送消息 +- 切换联系人 +- 用户手动取消 + +**取消逻辑**: + +```typescript +1. 清除定时器(aiRequestTimer) +2. 清空消息队列(pendingMessages) +3. 清除生成ID(currentAiGenerationId) +4. 更新加载状态(isLoadingAiChat: false) +``` + +### 5.5 AI配置更新 + +**配置更新流程**(ChatWindow组件): + +``` +1. 用户选择AI模式(人工/AI辅助/AI接管) +2. 调用API保存配置(setFriendInjectConfig) +3. 更新Store中的AI配置(aiQuoteMessageContent) +4. 更新会话数据库的aiType +5. 更新联系人数据库的aiType +6. 更新Store中的currentContract +``` + +--- + +## 六、消息显示 (MessageRecord) + +### 6.1 消息加载 + +**分页加载**: + +- **初始加载**: 加载第一页(20条) +- **滚动加载**: 滚动到顶部时加载更早的消息 +- **滚动位置保持**: 加载更多时保持滚动位置 + +**加载状态管理**: + +- `messagesLoading`: 消息加载中 +- `isLoadingData`: 数据初始化加载中 +- `currentMessagesHasMore`: 是否还有更多消息 + +### 6.2 消息分组 + +**分组规则**: + +- 按时间分组(同一天的消息归为一组) +- 显示时间分隔线 +- 系统消息单独处理 + +**虚拟滚动**: + +- 消息数量 > 50 时启用虚拟滚动 +- 使用 `react-window` 优化性能 +- 减少DOM节点数量 + +### 6.3 消息渲染 + +**消息类型渲染**: + +- **文本消息**: 解析表情、链接、@提及 +- **图片消息**: 显示图片预览,点击查看大图 +- **视频消息**: 显示预览图,点击播放 +- **音频消息**: 显示播放器,支持播放/暂停 +- **文件消息**: 显示文件卡片,点击下载 +- **位置消息**: 显示地图预览 +- **系统消息**: 显示时间分隔线、特殊提示 + +**群聊特殊处理**: + +- 显示发送者头像和昵称 +- 清理微信ID前缀(`wechatId:\n`) +- 群成员信息映射 + +### 6.4 消息操作 + +**右键菜单**: + +- **转发**: 单条转发 +- **多条转发**: 进入多选模式 +- **引用**: 引用消息内容到输入框 +- **撤回**: 撤回已发送的消息 +- **音频转文字**: 语音消息转文字 + +**消息选择**: + +- 多选模式:显示复选框 +- 选中消息:高亮显示 +- 批量操作:转发、删除等 + +--- + +## 七、数据同步与持久化 + +### 7.1 数据同步策略 + +**会话列表同步**: + +- **首次加载**: 从IndexedDB加载缓存 → 后台同步服务器 +- **增量更新**: WebSocket实时更新 +- **手动同步**: 用户点击同步按钮 + +**消息同步**: + +- **当前聊天**: 实时加载,分页获取 +- **历史消息**: 按需加载,不全部同步 +- **新消息**: WebSocket实时接收 + +### 7.2 持久化策略 + +**Store持久化**: + +- `user-store`: 用户信息、token +- `wechat-storage`: 不持久化currentContract(切换时清空) +- `message-storage`: 只持久化加载状态,不持久化数据 +- `contacts-storage`: 持久化联系人列表 + +**IndexedDB存储**: + +- `chatSessions`: 会话列表(完整数据) +- `contactsUnified`: 联系人数据(完整数据) +- `messages`: 消息记录(按需存储) + +### 7.3 数据一致性 + +**更新顺序**: + +1. 立即更新UI(乐观更新) +2. 调用API更新服务器 +3. 更新IndexedDB +4. 失败时回滚UI + +**冲突处理**: + +- 服务器数据优先 +- 本地缓存作为兜底 +- 同步时合并数据 + +--- + +## 八、性能优化 + +### 8.1 渲染优化 + +**组件优化**: + +- 使用 `React.memo` 避免不必要的重渲染 +- 使用选择器模式(selector)细粒度订阅 +- 合并多个selector减少重渲染 + +**消息列表优化**: + +- 虚拟滚动(消息数量 > 50) +- 消息分组减少DOM节点 +- 批量更新机制(16ms延迟) + +### 8.2 请求优化 + +**防抖机制**: + +- AI请求防抖(3秒) +- 搜索防抖(300ms) +- 消息批量更新(16ms) + +**请求优化**: + +- 分页加载,不一次性加载所有数据 +- 按需加载,只加载当前聊天消息 +- 缓存策略,优先使用本地数据 + +### 8.3 状态管理优化 + +**选择器模式**: + +```typescript +// ❌ 不推荐:解构整个store +const { currentContract, currentMessages } = useWeChatStore(); + +// ✅ 推荐:使用选择器 +const currentContract = useWeChatStore(state => state.currentContract); +const currentMessages = useWeChatStore(state => state.currentMessages); +``` + +**自定义Hook**: + +```typescript +// 合并多个selector +const { currentContract, currentMessages } = useMessageSelectors(); +``` + +--- + +## 九、错误处理 + +### 9.1 网络错误 + +**WebSocket断开**: + +- 自动重连(最多5次) +- 重连间隔:3秒 +- 页面刷新后自动恢复连接 + +**API请求失败**: + +- 显示错误提示 +- 失败时回滚UI(乐观更新) +- 重试机制(部分接口) + +### 9.2 数据错误 + +**消息解析失败**: + +- 容错处理,显示原始内容 +- 记录错误日志 + +**数据同步失败**: + +- 保留本地缓存 +- 显示同步状态 +- 支持手动重试 + +--- + +## 十、关键代码位置 + +### 10.1 核心文件 + +- **会话列表**: `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx` +- **聊天窗口**: `src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx` +- **消息输入**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx` +- **消息显示**: `src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx` + +### 10.2 状态管理 + +- **聊天状态**: `src/store/module/weChat/weChat.ts` +- **WebSocket**: `src/store/module/websocket/websocket.ts` +- **消息处理**: `src/store/module/websocket/msgManage.ts` + +### 10.3 数据库操作 + +- **会话管理**: `src/utils/dbAction/message.ts` +- **联系人管理**: `src/utils/dbAction/contact.ts` + +--- + +## 十一、流程图 + +### 11.1 消息发送流程 + +``` +用户输入/选择文件 + ↓ +MessageEnter.handleSend() + ↓ +构造本地消息(临时ID) + ↓ +添加到消息列表(乐观更新) + ↓ +WebSocket发送(CmdSendMessage) + ↓ +收到响应(CmdSendMessageResp) + ↓ +更新消息状态(真实ID,sendStatus: 0) +``` + +### 11.2 消息接收流程 + +``` +WebSocket收到消息 + ↓ +msgManage.ts处理(CmdNewMessage) + ↓ +weChatStore.receivedMsg() + ↓ +判断是否为当前聊天 + ├─ 是 → 批量更新消息列表 + │ ↓ + │ AI处理(如果是文字消息) + │ ├─ AI辅助 → 填充输入框 + │ └─ AI接管 → 直接发送 + │ + └─ 否 → 触发chatMessageReceived事件 + ↓ + MessageList更新会话列表 +``` + +### 11.3 AI处理流程 + +``` +收到新消息(文字,对方发送) + ↓ +检查AI模式(aiType) + ↓ +防抖处理(3秒延迟) + ↓ +消息队列(pendingMessages) + ↓ +3秒内无新消息 + ↓ +调用dataProcessing(批量处理) + ↓ +调用aiChat(生成回复) + ↓ +根据AI模式处理: + ├─ AI辅助(aiType=1)→ 填充输入框 + └─ AI接管(aiType=2)→ 直接发送 +``` + +--- + +## 十二、注意事项 + +### 12.1 性能注意事项 + +1. **避免解构整个store**,使用选择器模式 +2. **大量消息时启用虚拟滚动** +3. **使用批量更新机制**减少重渲染 +4. **合理使用防抖**避免频繁请求 + +### 12.2 数据一致性 + +1. **乐观更新**:先更新UI,再同步服务器 +2. **失败回滚**:API失败时回滚UI状态 +3. **数据同步**:定期同步服务器数据 + +### 12.3 AI处理注意事项 + +1. **取消机制**:用户操作时及时取消AI请求 +2. **防抖处理**:避免频繁触发AI请求 +3. **模式切换**:切换联系人时清除AI状态 + +--- + +## 十三、待优化项 + +1. **消息搜索功能**:支持全文搜索 +2. **消息撤回优化**:撤回后更新UI状态 +3. **文件下载进度**:显示下载进度 +4. **消息已读状态**:显示消息已读/未读 +5. **消息编辑功能**:支持编辑已发送消息 +6. **消息转发优化**:支持批量转发 +7. **AI回复优化**:支持多轮对话上下文 + +--- + +## 十四、总结 + +聊天功能采用**模块化设计**,通过**状态管理**、**WebSocket实时通信**、**IndexedDB持久化**实现完整的聊天体验。核心特点: + +1. **实时性**:WebSocket实时接收消息 +2. **性能优化**:批量更新、虚拟滚动、防抖处理 +3. **AI集成**:支持AI辅助和AI接管模式 +4. **数据一致性**:乐观更新 + 失败回滚 +5. **用户体验**:流畅的交互、及时的状态反馈 + +通过合理的架构设计和性能优化,实现了高效、稳定的聊天功能。