diff --git a/TouchVueThree/.env.development b/.env.development similarity index 100% rename from TouchVueThree/.env.development rename to .env.development diff --git a/TouchVueThree/.env.production b/.env.production similarity index 100% rename from TouchVueThree/.env.production rename to .env.production diff --git a/old/.eslintrc.js b/.eslintrc.js similarity index 100% rename from old/.eslintrc.js rename to .eslintrc.js diff --git a/old/.gitattributes b/.gitattributes similarity index 100% rename from old/.gitattributes rename to .gitattributes diff --git a/old/.gitignore b/.gitignore similarity index 100% rename from old/.gitignore rename to .gitignore diff --git a/old/.prettierrc b/.prettierrc similarity index 100% rename from old/.prettierrc rename to .prettierrc diff --git a/.vscode/settings.json b/.vscode/settings.json index d1c0d93..7241ea9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,6 +41,5 @@ "typescript.suggest.autoImports": true, "editor.tabSize": 2, "editor.insertSpaces": true, - "editor.detectIndentation": false, - "git.ignoreLimitWarning": true + "editor.detectIndentation": false } diff --git a/TouchVueThree/.editorconfig b/TouchVueThree/.editorconfig deleted file mode 100644 index e6a42e8..0000000 --- a/TouchVueThree/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# EditorConfig 帮助不同编辑器和 IDE 维护一致的编码风格 -# https://editorconfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false - -[*.{json,yml,yaml}] -indent_size = 2 - -[*.vue] -indent_size = 2 diff --git a/TouchVueThree/.eslintignore b/TouchVueThree/.eslintignore deleted file mode 100644 index 0a4935f..0000000 --- a/TouchVueThree/.eslintignore +++ /dev/null @@ -1,19 +0,0 @@ -node_modules -dist -dist-ssr -*.local - -# Build outputs -*.min.js -*.min.css - -# Config files -*.config.js -*.config.cjs -*.config.mjs - -# Static assets -public - -# Logs -*.log diff --git a/TouchVueThree/.eslintrc-auto-import.json b/TouchVueThree/.eslintrc-auto-import.json deleted file mode 100644 index bc66116..0000000 --- a/TouchVueThree/.eslintrc-auto-import.json +++ /dev/null @@ -1,316 +0,0 @@ -{ - "globals": { - "Component": true, - "ComponentPublicInstance": true, - "ComputedRef": true, - "DirectiveBinding": true, - "EffectScope": true, - "ExtractDefaultPropTypes": true, - "ExtractPropTypes": true, - "ExtractPublicPropTypes": true, - "InjectionKey": true, - "MaybeRef": true, - "MaybeRefOrGetter": true, - "PropType": true, - "Ref": true, - "VNode": true, - "WritableComputedRef": true, - "acceptHMRUpdate": true, - "asyncComputed": true, - "autoResetRef": true, - "computed": true, - "computedAsync": true, - "computedEager": true, - "computedInject": true, - "computedWithControl": true, - "controlledComputed": true, - "controlledRef": true, - "createApp": true, - "createEventHook": true, - "createGlobalState": true, - "createInjectionState": true, - "createPinia": true, - "createReactiveFn": true, - "createReusableTemplate": true, - "createSharedComposable": true, - "createTemplatePromise": true, - "createUnrefFn": true, - "customRef": true, - "debouncedRef": true, - "debouncedWatch": true, - "defineAsyncComponent": true, - "defineComponent": true, - "defineStore": true, - "eagerComputed": true, - "effectScope": true, - "extendRef": true, - "getActivePinia": true, - "getCurrentInstance": true, - "getCurrentScope": true, - "h": true, - "ignorableWatch": true, - "inject": true, - "injectLocal": true, - "isDefined": true, - "isProxy": true, - "isReactive": true, - "isReadonly": true, - "isRef": true, - "makeDestructurable": true, - "mapActions": true, - "mapGetters": true, - "mapState": true, - "mapStores": true, - "mapWritableState": true, - "markRaw": true, - "nextTick": true, - "onActivated": true, - "onBeforeMount": true, - "onBeforeRouteLeave": true, - "onBeforeRouteUpdate": true, - "onBeforeUnmount": true, - "onBeforeUpdate": true, - "onClickOutside": true, - "onDeactivated": true, - "onErrorCaptured": true, - "onKeyStroke": true, - "onLongPress": true, - "onMounted": true, - "onRenderTracked": true, - "onRenderTriggered": true, - "onScopeDispose": true, - "onServerPrefetch": true, - "onStartTyping": true, - "onUnmounted": true, - "onUpdated": true, - "onWatcherCleanup": true, - "pausableWatch": true, - "provide": true, - "provideLocal": true, - "reactify": true, - "reactifyObject": true, - "reactive": true, - "reactiveComputed": true, - "reactiveOmit": true, - "reactivePick": true, - "readonly": true, - "ref": true, - "refAutoReset": true, - "refDebounced": true, - "refDefault": true, - "refThrottled": true, - "refWithControl": true, - "resolveComponent": true, - "resolveRef": true, - "resolveUnref": true, - "setActivePinia": true, - "setMapStoreSuffix": true, - "shallowReactive": true, - "shallowReadonly": true, - "shallowRef": true, - "storeToRefs": true, - "syncRef": true, - "syncRefs": true, - "templateRef": true, - "throttledRef": true, - "throttledWatch": true, - "toRaw": true, - "toReactive": true, - "toRef": true, - "toRefs": true, - "toValue": true, - "triggerRef": true, - "tryOnBeforeMount": true, - "tryOnBeforeUnmount": true, - "tryOnMounted": true, - "tryOnScopeDispose": true, - "tryOnUnmounted": true, - "unref": true, - "unrefElement": true, - "until": true, - "useActiveElement": true, - "useAnimate": true, - "useArrayDifference": true, - "useArrayEvery": true, - "useArrayFilter": true, - "useArrayFind": true, - "useArrayFindIndex": true, - "useArrayFindLast": true, - "useArrayIncludes": true, - "useArrayJoin": true, - "useArrayMap": true, - "useArrayReduce": true, - "useArraySome": true, - "useArrayUnique": true, - "useAsyncQueue": true, - "useAsyncState": true, - "useAttrs": true, - "useBase64": true, - "useBattery": true, - "useBluetooth": true, - "useBreakpoints": true, - "useBroadcastChannel": true, - "useBrowserLocation": true, - "useCached": true, - "useClipboard": true, - "useClipboardItems": true, - "useCloned": true, - "useColorMode": true, - "useConfirmDialog": true, - "useCounter": true, - "useCssModule": true, - "useCssVar": true, - "useCssVars": true, - "useCurrentElement": true, - "useCycleList": true, - "useDark": true, - "useDateFormat": true, - "useDebounce": true, - "useDebounceFn": true, - "useDebouncedRefHistory": true, - "useDeviceMotion": true, - "useDeviceOrientation": true, - "useDevicePixelRatio": true, - "useDevicesList": true, - "useDisplayMedia": true, - "useDocumentVisibility": true, - "useDraggable": true, - "useDropZone": true, - "useElementBounding": true, - "useElementByPoint": true, - "useElementHover": true, - "useElementSize": true, - "useElementVisibility": true, - "useEventBus": true, - "useEventListener": true, - "useEventSource": true, - "useEyeDropper": true, - "useFavicon": true, - "useFetch": true, - "useFileDialog": true, - "useFileSystemAccess": true, - "useFocus": true, - "useFocusWithin": true, - "useFps": true, - "useFullscreen": true, - "useGamepad": true, - "useGeolocation": true, - "useId": true, - "useIdle": true, - "useImage": true, - "useInfiniteQuery": true, - "useInfiniteScroll": true, - "useIntersectionObserver": true, - "useInterval": true, - "useIntervalFn": true, - "useKeyModifier": true, - "useLastChanged": true, - "useLink": true, - "useLocalStorage": true, - "useMagicKeys": true, - "useManualRefHistory": true, - "useMediaControls": true, - "useMediaQuery": true, - "useMemoize": true, - "useMemory": true, - "useModel": true, - "useMounted": true, - "useMouse": true, - "useMouseInElement": true, - "useMousePressed": true, - "useMutation": true, - "useMutationObserver": true, - "useNavigatorLanguage": true, - "useNetwork": true, - "useNow": true, - "useObjectUrl": true, - "useOffsetPagination": true, - "useOnline": true, - "usePageLeave": true, - "useParallax": true, - "useParentElement": true, - "usePerformanceObserver": true, - "usePermission": true, - "usePointer": true, - "usePointerLock": true, - "usePointerSwipe": true, - "usePreferredColorScheme": true, - "usePreferredContrast": true, - "usePreferredDark": true, - "usePreferredLanguages": true, - "usePreferredReducedMotion": true, - "usePrevious": true, - "useQuery": true, - "useQueryClient": true, - "useRafFn": true, - "useRefHistory": true, - "useResizeObserver": true, - "useRoute": true, - "useRouter": true, - "useScreenOrientation": true, - "useScreenSafeArea": true, - "useScriptTag": true, - "useScroll": true, - "useScrollLock": true, - "useSessionStorage": true, - "useShare": true, - "useSlots": true, - "useSorted": true, - "useSpeechRecognition": true, - "useSpeechSynthesis": true, - "useStepper": true, - "useStorage": true, - "useStorageAsync": true, - "useStyleTag": true, - "useSupported": true, - "useSwipe": true, - "useTemplateRef": true, - "useTemplateRefsList": true, - "useTextDirection": true, - "useTextSelection": true, - "useTextareaAutosize": true, - "useThrottle": true, - "useThrottleFn": true, - "useThrottledRefHistory": true, - "useTimeAgo": true, - "useTimeout": true, - "useTimeoutFn": true, - "useTimeoutPoll": true, - "useTimestamp": true, - "useTitle": true, - "useToNumber": true, - "useToString": true, - "useToggle": true, - "useTransition": true, - "useUrlSearchParams": true, - "useUserMedia": true, - "useVModel": true, - "useVModels": true, - "useVibrate": true, - "useVirtualList": true, - "useWakeLock": true, - "useWebNotification": true, - "useWebSocket": true, - "useWebWorker": true, - "useWebWorkerFn": true, - "useWindowFocus": true, - "useWindowScroll": true, - "useWindowSize": true, - "watch": true, - "watchArray": true, - "watchAtMost": true, - "watchDebounced": true, - "watchDeep": true, - "watchEffect": true, - "watchIgnorable": true, - "watchImmediate": true, - "watchOnce": true, - "watchPausable": true, - "watchPostEffect": true, - "watchSyncEffect": true, - "watchThrottled": true, - "watchTriggerable": true, - "watchWithFilter": true, - "whenever": true - } -} diff --git a/TouchVueThree/.gitignore b/TouchVueThree/.gitignore deleted file mode 100644 index c960e23..0000000 --- a/TouchVueThree/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# Environment variables -.env -.env.local -.env.production.local -.env.development.local - -# Build outputs -*.tsbuildinfo diff --git a/TouchVueThree/.npmrc b/TouchVueThree/.npmrc deleted file mode 100644 index ae2dc2a..0000000 --- a/TouchVueThree/.npmrc +++ /dev/null @@ -1,4 +0,0 @@ -# npm 配置 -registry=https://registry.npmmirror.com -save-exact=false -engine-strict=false diff --git a/TouchVueThree/.prettierignore b/TouchVueThree/.prettierignore deleted file mode 100644 index 83f97b7..0000000 --- a/TouchVueThree/.prettierignore +++ /dev/null @@ -1,30 +0,0 @@ -# Dependencies -node_modules -package-lock.json -pnpm-lock.yaml -yarn.lock - -# Build outputs -dist -dist-ssr -*.local - -# Logs -*.log - -# Editor -.vscode -.idea - -# Cache -.cache -.parcel-cache - -# Environment -.env -.env.local -.env.production.local - -# Other -*.min.js -*.min.css diff --git a/TouchVueThree/.prettierrc b/TouchVueThree/.prettierrc deleted file mode 100644 index c1892d7..0000000 --- a/TouchVueThree/.prettierrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "es5", - "arrowParens": "always", - "endOfLine": "lf" -} diff --git a/TouchVueThree/.vscode/extensions.json b/TouchVueThree/.vscode/extensions.json deleted file mode 100644 index a241802..0000000 --- a/TouchVueThree/.vscode/extensions.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "recommendations": [ - "Vue.volar", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "bradlc.vscode-tailwindcss" - ] -} diff --git a/TouchVueThree/API接口修正说明.md b/TouchVueThree/API接口修正说明.md deleted file mode 100644 index 2ae120a..0000000 --- a/TouchVueThree/API接口修正说明.md +++ /dev/null @@ -1,183 +0,0 @@ -# API接口修正说明 - -## 🔧 修正内容 - -根据旧项目的实际API调用,已修正以下接口: - -### 1. 好友列表接口 - -**旧项目实现**: -```typescript -// old/src/pages/pc/ckbox/weChat/api.ts -export function getContactList(params, options?: ListRequestOptions) { - return request( - "/v1/kefu/wechatFriend/list", - params, - "GET", - undefined, - options?.debounceGap, - ); -} -``` - -**新项目修正**: -```typescript -// TouchVueThree/src/api/modules/wechat.ts -export function getContactList(params: { - wechatAccountId?: number - groupId?: number - keyword?: string - page?: number - limit?: number - pageNum?: number - pageSize?: number -}) { - // 统一参数格式:page/limit 或 pageNum/pageSize - const requestParams: any = {} - if (params.page !== undefined) requestParams.page = params.page - if (params.limit !== undefined) requestParams.limit = params.limit - if (params.pageNum !== undefined) requestParams.page = params.pageNum - if (params.pageSize !== undefined) requestParams.limit = params.pageSize - if (params.wechatAccountId !== undefined) requestParams.wechatAccountId = params.wechatAccountId - if (params.groupId !== undefined) requestParams.groupId = params.groupId - if (params.keyword !== undefined) requestParams.keyword = params.keyword - - return request('/v1/kefu/wechatFriend/list', requestParams, 'GET') -} -``` - -**关键修正**: -- ✅ 使用 **GET** 方法(不是POST) -- ✅ 参数使用 `page` 和 `limit`(旧项目标准) -- ✅ 支持 `pageNum/pageSize` 兼容格式 - -### 2. 群列表接口 - -**旧项目实现**: -```typescript -// old/src/pages/pc/ckbox/weChat/api.ts -export function getGroupList(params, options?: ListRequestOptions) { - return request( - "/v1/kefu/wechatChatroom/list", - params, - "GET", - undefined, - options?.debounceGap, - ); -} -``` - -**新项目修正**: -```typescript -export function getGroupList(params: { - wechatAccountId?: number - groupId?: number - keyword?: string - page?: number - limit?: number - pageNum?: number - pageSize?: number -}) { - // 统一参数格式 - const requestParams: any = {} - if (params.page !== undefined) requestParams.page = params.page - if (params.limit !== undefined) requestParams.limit = params.limit - // ... 其他参数处理 - return request('/v1/kefu/wechatChatroom/list', requestParams, 'GET') -} -``` - -**关键修正**: -- ✅ 使用 **GET** 方法(不是POST) -- ✅ 路径正确:`/v1/kefu/wechatChatroom/list` - -### 3. 分组列表接口 - -**旧项目实现**: -```typescript -// old/src/pages/pc/ckbox/weChat/api.ts -export function getLabelsListByGroup(params) { - return request("/v1/kefu/wechatGroup/list", params, "GET"); -} -``` - -**新项目修正**: -```typescript -export function getContactGroups(accountId?: number) { - const params: any = {} - if (accountId !== undefined && accountId !== 0) { - params.wechatAccountId = accountId - } - return request('/v1/kefu/wechatGroup/list', params, 'GET') -} - -export function getLabelsListByGroup(params?: { wechatAccountId?: number }) { - return request('/v1/kefu/wechatGroup/list', params || {}, 'GET') -} -``` - -**关键修正**: -- ✅ 使用 **GET** 方法 -- ✅ 添加 `getLabelsListByGroup` 别名函数 - -### 4. 删除分组接口 - -**旧项目实现**: -```typescript -// old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/api.ts -export function deleteGroup(id) { - return request("/v1/kefu/wechatGroup/delete?id=" + id, "DELETE"); -} -``` - -**新项目修正**: -```typescript -export function deleteGroup(id: number) { - return request(`/v1/kefu/wechatGroup/delete?id=${id}`, null, 'DELETE') -} -``` - -**关键修正**: -- ✅ 使用查询参数 `?id=${id}`(不是路径参数) - -## 📋 API接口对照表 - -| 功能 | 旧项目路径 | 新项目路径 | 方法 | 状态 | -|------|-----------|-----------|------|------| -| 好友列表 | `/v1/kefu/wechatFriend/list` | `/v1/kefu/wechatFriend/list` | GET | ✅ 已修正 | -| 群列表 | `/v1/kefu/wechatChatroom/list` | `/v1/kefu/wechatChatroom/list` | GET | ✅ 已修正 | -| 分组列表 | `/v1/kefu/wechatGroup/list` | `/v1/kefu/wechatGroup/list` | GET | ✅ 已修正 | -| 新增分组 | `/v1/kefu/wechatGroup/add` | `/v1/kefu/wechatGroup/add` | POST | ✅ 正确 | -| 更新分组 | `/v1/kefu/wechatGroup/update` | `/v1/kefu/wechatGroup/update` | POST | ✅ 正确 | -| 删除分组 | `/v1/kefu/wechatGroup/delete?id=${id}` | `/v1/kefu/wechatGroup/delete?id=${id}` | DELETE | ✅ 已修正 | -| 移动分组 | `/v1/kefu/wechatGroup/move` | `/v1/kefu/wechatGroup/move` | POST | ✅ 正确 | - -## 🔍 参数格式说明 - -### 好友/群列表参数 -```typescript -{ - page?: number // 页码(旧项目标准) - limit?: number // 每页数量(旧项目标准) - wechatAccountId?: number // 账号ID(0表示全部) - groupId?: number // 分组ID(0表示未分组/默认分组) - keyword?: string // 搜索关键词 -} -``` - -### 分组列表参数 -```typescript -{ - wechatAccountId?: number // 账号ID(可选) - page?: number // 页码(可选) - limit?: number // 每页数量(可选) -} -``` - -## ✅ 修正完成 - -所有API接口已按照旧项目的实际实现进行修正,确保: -1. ✅ HTTP方法正确(GET/POST/DELETE) -2. ✅ 路径正确 -3. ✅ 参数格式正确 -4. ✅ 参数名称正确(page/limit vs pageNum/pageSize) diff --git a/TouchVueThree/API接口说明.md b/TouchVueThree/API接口说明.md deleted file mode 100644 index 860de60..0000000 --- a/TouchVueThree/API接口说明.md +++ /dev/null @@ -1,280 +0,0 @@ -# API 接口说明 - -## ⚠️ 重要:必需的 API 接口 - -改造后的聊天系统需要以下 API 接口支持。如果后端接口路径不同,请修改 `src/api/modules/wechat.ts` 中的对应函数。 - ---- - -## 🔴 必需接口(高优先级) - -### 1. 获取好友详情 - -**用途**: 收到陌生好友消息时,自动获取好友完整信息并创建会话 - -**当前实现**: -```typescript -// src/api/modules/wechat.ts -export function getFriendDetail(params: { friendId: number }) { - return request(`/v1/kefu/wechatFriend/detail/${params.friendId}`, {}, 'GET') -} -``` - -**如果接口路径不同,请修改为**: -```typescript -// 示例1: 使用 POST 请求 -export function getFriendDetail(params: { friendId: number }) { - return request('/v1/kefu/wechatFriend/detail', params, 'POST') -} - -// 示例2: 使用 request2 -export function getFriendDetail(params: { friendId: number }) { - return request2('/api/wechatFriend/detail', params, 'GET') -} - -// 示例3: 使用不同的路径 -export function getFriendDetail(params: { friendId: number }) { - return request(`/api/friend/${params.friendId}`, {}, 'GET') -} -``` - -**返回数据格式要求**: -```typescript -{ - id: number // 好友ID - nickname: string // 昵称 - conRemark?: string // 备注名(可选) - avatar: string // 头像URL - wxid?: string // 微信ID(可选) - wechatAccountId: number // 所属客服账号 - // ... 其他字段 -} -``` - ---- - -### 2. 获取群聊详情 - -**用途**: 收到陌生群聊消息时,自动获取群聊完整信息并创建会话 - -**当前实现**: -```typescript -// src/api/modules/wechat.ts -export function getGroupDetail(params: { groupId: number }) { - return request(`/v1/kefu/wechatChatroom/detail/${params.groupId}`, {}, 'GET') -} -``` - -**如果接口路径不同,请修改为**: -```typescript -// 示例1: 使用 POST 请求 -export function getGroupDetail(params: { groupId: number }) { - return request('/v1/kefu/wechatChatroom/detail', params, 'POST') -} - -// 示例2: 使用 request2 -export function getGroupDetail(params: { groupId: number }) { - return request2('/api/wechatChatroom/detail', params, 'GET') -} - -// 示例3: 使用不同的路径 -export function getGroupDetail(params: { groupId: number }) { - return request(`/api/group/${params.groupId}`, {}, 'GET') -} -``` - -**返回数据格式要求**: -```typescript -{ - id: number // 群聊ID - nickname: string // 群名称 - avatar: string // 群头像 - chatroomId?: string // 群聊ID(可选) - memberCount?: number // 成员数(可选) - wechatAccountId: number // 所属客服账号 - // ... 其他字段 -} -``` - ---- - -## 🟡 可选接口(中优先级) - -### 3. 增量同步消息 - -**用途**: WebSocket 断线重连后,同步断线期间遗漏的消息 - -**当前未实现**,如果后端提供此接口,请添加: - -```typescript -// src/api/modules/wechat.ts - -/** - * 获取指定时间之后的消息(增量同步) - */ -export function getMessagesSince(params: { - wechatAccountId: number - since: number // 时间戳(毫秒) - limit?: number // 数量限制(可选) -}) { - return request('/v1/kefu/message/since', params, 'GET') -} -``` - -**返回数据格式**: -```typescript -{ - list: Message[] // 消息列表 - total: number // 总数 -} -``` - -**使用位置**: `src/composables/business/wechat/useWebSocket.ts` 中的 `syncMissedMessages()` 函数 - ---- - -## 📝 接口调用时机 - -### getFriendDetail - -**调用时机**: -1. WebSocket 收到陌生好友的新消息 -2. 会话不存在时自动调用 -3. 创建临时会话后,后台重试获取详情 - -**调用位置**: `src/utils/dbManagers/SessionManager.ts` - -```typescript -// 自动调用,无需手动处理 -const contactInfo = await getFriendDetail({ friendId: sessionId }) -``` - -### getGroupDetail - -**调用时机**: -1. WebSocket 收到陌生群聊的新消息 -2. 会话不存在时自动调用 -3. 创建临时会话后,后台重试获取详情 - -**调用位置**: `src/utils/dbManagers/SessionManager.ts` - -```typescript -// 自动调用,无需手动处理 -const contactInfo = await getGroupDetail({ groupId: sessionId }) -``` - ---- - -## 🔧 如何修改接口路径 - -### 步骤1: 找到函数定义 - -打开 `src/api/modules/wechat.ts`,找到对应的函数: - -```typescript -export function getFriendDetail(params: { friendId: number }) { - return request(`/v1/kefu/wechatFriend/detail/${params.friendId}`, {}, 'GET') -} -``` - -### 步骤2: 修改路径和请求方式 - -根据后端实际接口修改: - -```typescript -// 如果后端接口是 POST 请求 -export function getFriendDetail(params: { friendId: number }) { - return request('/v1/kefu/wechatFriend/detail', params, 'POST') -} - -// 如果使用 request2 -export function getFriendDetail(params: { friendId: number }) { - return request2('/api/wechatFriend/detail', params, 'GET') -} -``` - -### 步骤3: 确保返回数据格式匹配 - -确保后端返回的数据包含以下字段(至少): -- `id`: 好友/群聊ID -- `nickname`: 昵称 -- `avatar`: 头像URL -- `wechatAccountId`: 所属客服账号 - -如果字段名不同,需要修改 `SessionManager.ts` 中的数据映射。 - ---- - -## 🐛 故障排查 - -### 问题1: 接口返回 404 - -**原因**: 接口路径不正确 - -**解决**: 修改 `src/api/modules/wechat.ts` 中的接口路径 - -### 问题2: 接口返回数据格式不匹配 - -**原因**: 后端返回的字段名与预期不同 - -**解决**: 修改 `SessionManager.ts` 中的数据映射: - -```typescript -// 在 createSessionFromMessage() 函数中 -const newSession: ChatSession = { - // 如果后端返回的是 name 而不是 nickname - nickname: contactInfo.name || contactInfo.nickname, - // 如果后端返回的是 remark 而不是 conRemark - conRemark: contactInfo.remark || contactInfo.conRemark, - // ... -} -``` - -### 问题3: 接口需要额外参数 - -**原因**: 后端接口需要更多参数(如 wechatAccountId) - -**解决**: 修改函数签名和调用: - -```typescript -// 修改函数签名 -export function getFriendDetail(params: { - friendId: number - wechatAccountId?: number // 添加可选参数 -}) { - return request('/v1/kefu/wechatFriend/detail', params, 'POST') -} - -// 在 SessionManager.ts 中调用时传入 -const contactInfo = await getFriendDetail({ - friendId: sessionId, - wechatAccountId: wechatAccountId -}) -``` - ---- - -## ✅ 测试清单 - -修改接口后,请测试以下场景: - -- [ ] 收到陌生好友消息时,能自动获取详情并创建会话 -- [ ] 收到陌生群聊消息时,能自动获取详情并创建会话 -- [ ] 接口失败时,能创建临时会话(降级方案) -- [ ] 后台重试能成功更新会话详情 -- [ ] 返回的数据能正确映射到会话对象 - ---- - -## 📞 需要帮助? - -如果遇到问题: - -1. 检查浏览器控制台的错误信息 -2. 检查网络请求的 URL 和参数 -3. 检查后端返回的数据格式 -4. 参考 [聊天系统改造方案.md](./聊天系统改造方案.md) 中的接口说明 - ---- - -**最后更新**: 2026-01-13 diff --git a/TouchVueThree/CHANGELOG.md b/TouchVueThree/CHANGELOG.md deleted file mode 100644 index 73de2cb..0000000 --- a/TouchVueThree/CHANGELOG.md +++ /dev/null @@ -1,27 +0,0 @@ -# 更新日志 - -所有重要的项目变更都会记录在此文件中。 - -格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), -版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。 - -## [1.0.0] - 2024-01-XX - -### 新增 -- 初始化项目 -- 集成 Vite + Vue 3 + Tailwind CSS -- 创建基础项目结构 -- 添加聊天模块(ChatList, ChatWindow, ChatDetail) -- 添加账号管理页面(Accounts) -- 添加数据看板页面(Analytics) -- 创建全局导航栏组件(Sidebar) -- 创建 AI 算力展示组件(AICreditPill) -- 配置 ESLint 和 Prettier -- 添加环境变量支持 - -### 配置 -- Vite 构建配置 -- Tailwind CSS 主题配置 -- PostCSS 配置 -- 路径别名配置(@ → src) -- 编辑器配置文件 diff --git a/TouchVueThree/LICENSE b/TouchVueThree/LICENSE deleted file mode 100644 index 8d55588..0000000 --- a/TouchVueThree/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 触客宝 Pro - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/TouchVueThree/PROJECT_STRUCTURE.md b/TouchVueThree/PROJECT_STRUCTURE.md deleted file mode 100644 index cd4f9b2..0000000 --- a/TouchVueThree/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,323 +0,0 @@ -# TouchVueThree 项目目录结构说明 - -> **PC 端微信客服系统 - Vue3 + TypeScript + Element Plus** - -## 📁 目录结构 - -``` -TouchVueThree/ -├── public/ # 静态资源(不会被编译) -│ ├── favicon.ico -│ └── assets/ # 图片、表情包等静态资源 -│ -├── src/ -│ ├── api/ # API 接口层 -│ │ ├── request.ts # Axios 封装(拦截器、错误处理) -│ │ ├── interceptors.ts # 请求/响应拦截器 -│ │ └── modules/ # 按业务模块划分的接口 -│ │ ├── user.ts # 用户相关接口 -│ │ ├── wechat.ts # 微信相关接口 -│ │ ├── ai.ts # AI 相关接口 -│ │ └── common.ts # 通用接口(文件上传等) -│ │ -│ ├── assets/ # 资源文件 -│ │ ├── images/ # 图片资源 -│ │ ├── icons/ # 图标资源 -│ │ └── styles/ # 样式文件 -│ │ ├── variables.scss # 全局变量(颜色、字体) -│ │ ├── mixins.scss # SCSS 混入 -│ │ ├── reset.scss # 样式重置 -│ │ └── global.scss # 全局样式 -│ │ -│ ├── components/ # 公共组件 -│ │ ├── common/ # 通用组件(与业务无关) -│ │ │ ├── Button/ # 自定义按钮 -│ │ │ ├── Dialog/ # 自定义对话框 -│ │ │ ├── Loading/ # 加载组件 -│ │ │ ├── Empty/ # 空状态 -│ │ │ └── ErrorBoundary/ # 错误边界 -│ │ │ -│ │ └── business/ # 业务组件(微信客服相关) -│ │ ├── ChatWindow/ # 聊天窗口 -│ │ ├── ContactList/ # 联系人列表 -│ │ ├── SessionList/ # 会话列表 -│ │ ├── CustomerList/ # 客服账号列表 -│ │ ├── ProfileCard/ # 客户画像卡片 -│ │ ├── EmojiPicker/ # 表情选择器 -│ │ ├── FileUpload/ # 文件上传 -│ │ └── QuickReply/ # 快捷回复 -│ │ -│ ├── composables/ # 组合式函数(Composition API) -│ │ ├── core/ # 核心功能 -│ │ │ ├── useAuth.ts # 认证登录 -│ │ │ ├── useWebSocket.ts # WebSocket 管理 -│ │ │ ├── useEventBus.ts # 事件总线 -│ │ │ └── useRequest.ts # 请求封装 -│ │ │ -│ │ └── business/ # 业务功能 -│ │ ├── useMessages.ts # 消息管理 -│ │ ├── useContacts.ts # 联系人管理 -│ │ ├── useSessions.ts # 会话管理 -│ │ ├── useAi.ts # AI 功能 -│ │ └── useUpload.ts # 文件上传 -│ │ -│ ├── directives/ # 自定义指令 -│ │ ├── vLoading.ts # 加载指令 -│ │ ├── vLazyLoad.ts # 懒加载指令 -│ │ └── vPermission.ts # 权限指令 -│ │ -│ ├── layouts/ # 页面布局 -│ │ ├── DefaultLayout.vue # 默认布局 -│ │ └── ChatLayout.vue # 聊天布局(三栏) -│ │ -│ ├── router/ # 路由配置 -│ │ ├── index.ts # 路由主文件 -│ │ ├── guards.ts # 路由守卫(权限控制) -│ │ └── routes.ts # 路由配置 -│ │ -│ ├── stores/ # Pinia 状态管理 -│ │ ├── index.ts # Store 统一出口 -│ │ └── modules/ # Store 模块 -│ │ ├── user.ts # 用户状态 -│ │ ├── app.ts # 应用全局状态 -│ │ ├── websocket.ts # WebSocket 状态 -│ │ └── wechat/ # 微信模块 -│ │ ├── contacts.ts # 联系人 -│ │ ├── messages.ts # 消息 -│ │ ├── sessions.ts # 会话 -│ │ └── ai.ts # AI -│ │ -│ ├── types/ # TypeScript 类型定义 -│ │ ├── global.d.ts # 全局类型 -│ │ ├── api.d.ts # API 类型 -│ │ ├── user.ts # 用户类型 -│ │ ├── wechat.ts # 微信类型 -│ │ └── websocket.ts # WebSocket 类型 -│ │ -│ ├── utils/ # 工具函数 -│ │ ├── common.ts # 通用工具 -│ │ ├── date.ts # 日期处理 -│ │ ├── format.ts # 格式化 -│ │ ├── validator.ts # 验证 -│ │ ├── storage.ts # 本地存储 -│ │ ├── event-bus.ts # 事件总线 -│ │ └── sentry/ # 错误监控 -│ │ └── index.ts -│ │ -│ ├── views/ # 页面组件 -│ │ ├── Login/ # 登录页 -│ │ │ └── index.vue -│ │ │ -│ │ ├── Chat/ # 聊天主页(核心功能) -│ │ │ ├── index.vue -│ │ │ └── components/ # 聊天页专用组件 -│ │ │ ├── Sidebar.vue # 左侧边栏 -│ │ │ ├── Main.vue # 中间聊天区 -│ │ │ └── Panel.vue # 右侧面板 -│ │ │ -│ │ ├── Dashboard/ # 数据看板 -│ │ │ └── index.vue -│ │ │ -│ │ ├── Settings/ # 系统设置 -│ │ │ └── index.vue -│ │ │ -│ │ ├── PowerCenter/ # 能力中心 -│ │ │ ├── CustomerManagement/ # 客户管理 -│ │ │ ├── ContentManagement/ # 内容管理 -│ │ │ ├── DataStatistics/ # 数据统计 -│ │ │ └── AiTraining/ # AI 训练 -│ │ │ -│ │ └── 404/ # 404 页面 -│ │ └── index.vue -│ │ -│ ├── App.vue # 根组件 -│ └── main.ts # 入口文件 -│ -├── .env.development # 开发环境变量 -├── .env.production # 生产环境变量 -├── .eslintrc.cjs # ESLint 配置 -├── .prettierrc # Prettier 配置 -├── tsconfig.json # TypeScript 配置 -├── vite.config.ts # Vite 配置 -└── package.json # 项目配置 -``` - -## 🎯 命名规范 - -### 文件命名 - -- **组件文件**:PascalCase(大驼峰) - - `ChatWindow.vue`, `MessageList.vue` - -- **工具文件**:camelCase(小驼峰) - - `common.ts`, `useAuth.ts` - -- **类型文件**:camelCase 或 kebab-case - - `user.ts`, `api.d.ts` - -### 目录命名 - -- **kebab-case**(短横线)或 **camelCase** - - `components/business`, `composables/core` - -### 组件命名规则 - -```typescript -// ✅ 推荐 - - -// ❌ 避免 - -``` - -## 🔧 开发规范 - -### 1. Composables 使用规范 - -```typescript -// composables/core/useAuth.ts -import { ref, computed } from 'vue' -import { useUserStore } from '@/stores' - -export function useAuth() { - const userStore = useUserStore() - - const isLoggedIn = computed(() => userStore.isLoggedIn) - - const login = async (credentials) => { - // 登录逻辑 - } - - return { - isLoggedIn, - login - } -} -``` - -### 2. Store 使用规范 - -```typescript -// stores/modules/user.ts -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' - -export const useUserStore = defineStore('user', () => { - // State - const user = ref(null) - const token = ref('') - - // Getters - const isLoggedIn = computed(() => !!token.value) - - // Actions - const setUser = (userData) => { - user.value = userData - } - - return { user, token, isLoggedIn, setUser } -}, { - persist: { - key: 'user-store', - paths: ['user', 'token'] - } -}) -``` - -### 3. API 调用规范 - -```typescript -// api/modules/user.ts -import request from '../request' - -export const loginApi = (data: LoginParams) => { - return request('/auth/login', data, 'POST') -} - -export const getUserInfoApi = () => { - return request('/user/info', {}, 'GET') -} -``` - -### 4. 组件引入规范 - -```vue - -``` - -## 📝 注释规范 - -```typescript -/** - * 用户登录 - * @param credentials 登录凭证 - * @returns Promise - */ -export async function login(credentials: LoginParams): Promise { - // 实现逻辑 -} -``` - -## 🚀 启动命令 - -```bash -# 安装依赖 -pnpm install - -# 启动开发服务器 -pnpm dev - -# 类型检查 -pnpm type-check - -# 代码检查 -pnpm lint - -# 格式化代码 -pnpm format - -# 构建生产版本 -pnpm build - -# 预览生产版本 -pnpm preview - -# 打包分析 -pnpm analyze -``` - -## 📌 下一步计划 - -1. ✅ 目录结构已创建 -2. ✅ package.json 已优化 -3. ⏳ 安装依赖:`pnpm install` -4. ⏳ 创建基础配置文件 -5. ⏳ 开发核心功能 - ---- - -**更新时间**: 2026-01-12 -**版本**: 2.0.0 -**技术栈**: Vue 3.4 + TypeScript 5.4 + Element Plus 2.5 + Pinia 2.1 diff --git a/TouchVueThree/QUICK_START.md b/TouchVueThree/QUICK_START.md deleted file mode 100644 index ab7c53f..0000000 --- a/TouchVueThree/QUICK_START.md +++ /dev/null @@ -1,402 +0,0 @@ -# TouchVueThree 快速开始指南 - -## 🚀 项目已完成配置 - -### ✅ 已完成的工作 - -1. **目录结构创建** ✅ - - 完整的 PC 端目录结构 - - 模块化的代码组织 - -2. **package.json 优化** ✅ - - 移除移动端相关依赖 - - 添加 PC 端必需依赖 - - 优化打包配置 - -3. **Vite 配置优化** ✅ - - 自动导入配置 - - 路径别名配置 - - 打包优化配置 - - Gzip 压缩 - -4. **样式系统** ✅ - - variables.scss (全局变量) - - mixins.scss (混入工具) - - reset.scss (样式重置) - - global.scss (全局样式) - ---- - -## 📦 第一步:安装依赖 - -```bash -# 进入项目目录 -cd TouchVueThree - -# 删除旧的 node_modules 和 lock 文件(可选,但推荐) -rm -rf node_modules pnpm-lock.yaml - -# 安装所有依赖 -pnpm install -``` - -### 依赖说明 - -**核心框架**: -- `vue@^3.4.21` - Vue 3 框架 -- `vue-router@^4.2.5` - 路由管理 -- `pinia@^2.1.7` - 状态管理 -- `pinia-plugin-persistedstate@^3.2.1` - Pinia 持久化 - -**UI 组件库**: -- `element-plus@^2.5.6` - PC 端 UI 组件 -- `@element-plus/icons-vue@^2.3.1` - Element Plus 图标 - -**数据请求**: -- `axios@^1.6.7` - HTTP 客户端 -- `@tanstack/vue-query@^5.20.0` - 数据请求管理(自动缓存、重试) - -**工具库**: -- `@vueuse/core@^10.7.2` - Vue 组合式工具集 -- `dayjs@^1.11.13` - 日期处理 -- `lodash-es@^4.17.21` - 工具函数库 -- `mitt@^3.0.1` - 事件总线 -- `nanoid@^5.0.4` - ID 生成器 - -**图表**: -- `echarts@^5.6.0` - 图表库 -- `vue-echarts@^6.6.8` - Vue ECharts 封装 - -**监控**: -- `@sentry/vue@^7.100.0` - 错误监控 - ---- - -## 🔧 第二步:创建环境变量文件 - -创建 `.env.development` 文件: - -```env -# API 基础地址 -VITE_API_BASE_URL=http://localhost:3000/api - -# WebSocket 地址 -VITE_API_WS_URL=ws://localhost:3000/ws - -# 应用标题 -VITE_APP_TITLE=触客宝 - 开发环境 - -# Sentry DSN -VITE_SENTRY_DSN= - -# 是否启用 Mock 数据 -VITE_USE_MOCK=false -``` - -创建 `.env.production` 文件: - -```env -# API 基础地址 -VITE_API_BASE_URL=https://api.touchkebao.com/api - -# WebSocket 地址 -VITE_API_WS_URL=wss://api.touchkebao.com/ws - -# 应用标题 -VITE_APP_TITLE=触客宝 - -# Sentry DSN -VITE_SENTRY_DSN=your_sentry_dsn_here - -# 是否启用 Mock 数据 -VITE_USE_MOCK=false -``` - ---- - -## 🎯 第三步:启动开发服务器 - -```bash -# 启动开发服务器 -pnpm dev - -# 项目将在 http://localhost:8888 启动 -``` - ---- - -## 📂 第四步:理解项目结构 - -``` -src/ -├── api/ # API 接口(按模块划分) -├── assets/ # 静态资源和样式 -├── components/ # 公共组件 -│ ├── common/ # 通用组件 -│ └── business/ # 业务组件 -├── composables/ # 组合式函数 -│ ├── core/ # 核心功能 -│ └── business/ # 业务功能 -├── directives/ # 自定义指令 -├── layouts/ # 布局组件 -├── router/ # 路由配置 -├── stores/ # Pinia 状态管理 -│ └── modules/ # Store 模块 -├── types/ # TypeScript 类型 -├── utils/ # 工具函数 -└── views/ # 页面组件 -``` - ---- - -## 🔨 开发指南 - -### 1. 创建新页面 - -```vue - - - - - - {{ message }} - - - - -``` - -### 2. 创建 Store - -```typescript -// src/stores/modules/example.ts -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' - -export const useExampleStore = defineStore('example', () => { - // State - const count = ref(0) - - // Getters - const doubleCount = computed(() => count.value * 2) - - // Actions - const increment = () => { - count.value++ - } - - return { count, doubleCount, increment } -}, { - persist: { - key: 'example-store', - paths: ['count'] - } -}) -``` - -### 3. 创建 API 接口 - -```typescript -// src/api/modules/example.ts -import request from '../request' - -export const getListApi = (params: any) => { - return request('/list', params, 'GET') -} - -export const createItemApi = (data: any) => { - return request('/item', data, 'POST') -} -``` - -### 4. 创建 Composable - -```typescript -// src/composables/business/useExample.ts -import { ref } from 'vue' -import { getListApi } from '@/api/modules/example' - -export function useExample() { - const loading = ref(false) - const list = ref([]) - - const fetchList = async () => { - loading.value = true - try { - list.value = await getListApi({}) - } finally { - loading.value = false - } - } - - return { - loading, - list, - fetchList - } -} -``` - ---- - -## 📝 常用命令 - -```bash -# 开发服务器 -pnpm dev - -# 类型检查 -pnpm type-check - -# 代码检查 -pnpm lint - -# 代码格式化 -pnpm format - -# 构建生产版本 -pnpm build - -# 预览生产版本 -pnpm preview - -# 打包分析 -pnpm analyze -``` - ---- - -## 🎨 样式使用示例 - -```vue - - - - - 标题 - 副标题 - - - - 内容 - - - - -``` - ---- - -## 🔍 调试技巧 - -### 1. Vue DevTools - -安装 Vue DevTools 浏览器扩展,用于调试 Vue 组件和 Pinia Store。 - -### 2. 自动导入类型提示 - -项目已配置自动导入,会自动生成类型文件: -- `src/auto-imports.d.ts` - Vue API 类型 -- `src/components.d.ts` - 组件类型 - -如果类型提示不生效,重启 VSCode 或运行: -```bash -pnpm dev -``` - -### 3. ESLint 自动修复 - -```bash -pnpm lint -``` - ---- - -## ⚠️ 注意事项 - -1. **不要手动修改自动生成的文件**: - - `src/auto-imports.d.ts` - - `src/components.d.ts` - - `.eslintrc-auto-import.json` - -2. **使用 SCSS 变量**: - 全局变量已自动导入,可直接使用 `$primary-color` 等 - -3. **路径别名**: - 项目已配置路径别名,推荐使用: - ```typescript - import { xxx } from '@/xxx' // src/ - import { xxx } from '@api/xxx' // src/api/ - import { xxx } from '@components/xxx' // src/components/ - import { xxx } from '@stores/xxx' // src/stores/ - ``` - -4. **TypeScript 严格模式**: - 项目启用了 TypeScript 严格模式,请注意类型定义 - ---- - -## 🆘 常见问题 - -### Q: pnpm install 失败? - -A: 尝试: -```bash -# 清理缓存 -pnpm store prune - -# 重新安装 -pnpm install -``` - -### Q: 类型提示不生效? - -A: -1. 重启 VSCode -2. 运行 `pnpm dev` 生成类型文件 -3. 检查 `tsconfig.json` 配置 - -### Q: 自动导入不生效? - -A: -1. 检查 `vite.config.ts` 中的 AutoImport 配置 -2. 重启开发服务器 -3. 检查 `.eslintrc-auto-import.json` 是否生成 - ---- - -## 📚 相关文档 - -- [Vue 3 官方文档](https://cn.vuejs.org/) -- [Element Plus 官方文档](https://element-plus.org/zh-CN/) -- [Pinia 官方文档](https://pinia.vuejs.org/zh/) -- [VueUse 官方文档](https://vueuse.org/) -- [TanStack Query 官方文档](https://tanstack.com/query/latest) - ---- - -## 🎉 开始开发 - -现在您可以开始开发了!建议按以下顺序: - -1. ✅ 安装依赖 -2. ✅ 创建环境变量文件 -3. ✅ 启动开发服务器 -4. 🔨 开始编写代码 - -祝您开发愉快! 🚀 diff --git a/TouchVueThree/QUICK_START_改造版.md b/TouchVueThree/QUICK_START_改造版.md deleted file mode 100644 index 64182d3..0000000 --- a/TouchVueThree/QUICK_START_改造版.md +++ /dev/null @@ -1,311 +0,0 @@ -# 🚀 聊天系统改造版 - 快速开始 - -> 5分钟快速了解如何使用新架构 - ---- - -## 📦 改造内容 - -### 核心变化 - -| 改造点 | 旧方式 | 新方式 | 优势 | -|--------|--------|--------|------| -| 数据加载 | 定时轮询 | 订阅机制 | 网络请求减少95% | -| 首屏显示 | 等待API | 缓存优先 | 加载速度提升85% | -| 陌生消息 | 无法显示 | 自动创建 | 不丢消息 | -| 数据隔离 | 单库混存 | 一号一库 | 彻底隔离 | - ---- - -## 🎯 使用方法 - -### 1️⃣ 登录时(自动初始化数据库) - -```typescript -// src/stores/modules/user.ts -// ✅ 已自动集成,无需修改 - -const login = async (params) => { - const response = await loginAPI(params) - - // ⭐ 自动初始化数据库(一号一库) - await databaseManager.ensureDatabase(response.member.id) - - setUser(response.member) - router.push('/chat') -} -``` - -### 2️⃣ 聊天页面(初始化会话列表) - -```vue - - - - - - - {{ session.nickname }}: {{ session.content }} - - -``` - -### 3️⃣ WebSocket(自动更新会话) - -```typescript -// src/composables/business/wechat/useWebSocket.ts -// ✅ 已自动集成,无需修改 - -// WebSocket 收到新消息时: -// 1. 自动更新 IndexedDB -// 2. 自动创建会话(如果不存在) -// 3. 自动触发 UI 更新 -// 4. 自动保存消息记录 - -// 你只需要:连接 WebSocket -const { connect } = useWebSocket() -connect({ accountId, accessToken }) -``` - -### 4️⃣ 切换账户 - -```typescript -// 切换账户时,自动切换数据库 -await sessionStore.switchAccount(newAccountId) - -// 内部自动完成: -// 1. 切换数据库 -// 2. 读取缓存 -// 3. 后台同步 -``` - -### 5️⃣ 退出登录 - -```typescript -// src/stores/modules/user.ts -// ✅ 已自动集成,无需修改 - -const logout = async () => { - // ⭐ 自动关闭数据库 - await databaseManager.closeCurrentDatabase() - - clearUser() - router.push('/login') -} -``` - ---- - -## 🎨 UI 自动更新 - -### 订阅机制(替代轮询) - -```typescript -// ❌ 旧方式:定时器轮询 -setInterval(() => { - loadSessions() // 每3秒请求一次,浪费资源 -}, 3000) - -// ✅ 新方式:订阅机制 -SessionManager.onUpdate((sessions) => { - // 数据变更时自动调用,无需轮询 - this.sessions = sessions -}) -``` - -### 数据流向 - -``` -WebSocket 收到消息 - ↓ -更新 IndexedDB - ↓ -SessionManager 触发回调 - ↓ -Store 自动更新 - ↓ -UI 自动刷新 -``` - ---- - -## 🔧 必需的 API 接口 - -### 1. 获取好友详情(重要!) - -```typescript -// 接口:GET /api/friend/detail?friendId=123 -// 调用时机:收到陌生好友消息时 - -interface FriendDetail { - id: number - nickname: string - conRemark?: string - avatar: string - wxid: string - wechatAccountId: number -} -``` - -### 2. 获取群聊详情(重要!) - -```typescript -// 接口:GET /api/group/detail?groupId=456 -// 调用时机:收到陌生群聊消息时 - -interface GroupDetail { - id: number - nickname: string - avatar: string - chatroomId: string - memberCount: number - wechatAccountId: number -} -``` - -### 3. 获取会话列表 - -```typescript -// 接口:GET /api/session/list?page=1&limit=200&wechatAccountId=1 -// 调用时机:登录、切换账户 - -interface SessionListResponse { - list: Session[] - total: number -} -``` - ---- - -## ⚠️ 重要规则 - -### ✅ 必须遵守 - -```typescript -// 1. 使用 db() 函数(带括号) -await db().sessions.toArray() // ✅ 正确 -await db.sessions.toArray() // ❌ 错误 - -// 2. 不要使用定时器轮询 -setInterval(() => loadSessions(), 3000) // ❌ 错误 - -// 3. 使用订阅机制 -SessionManager.onUpdate(() => {}) // ✅ 正确 - -// 4. 组件卸载时清理 -onUnmounted(() => { - sessionStore.cleanup() -}) -``` - ---- - -## 📊 性能提升 - -| 指标 | 改造前 | 改造后 | 提升 | -|------|--------|--------|------| -| 首屏加载 | 1-3s | <200ms | **85%** ⚡ | -| 网络请求 | 1200次/小时 | <50次/小时 | **95%** 🔽 | -| 服务器负载 | 高 | 低 | **95%** 🔽 | -| 离线能力 | 无 | 完整缓存 | **100%** 📱 | - ---- - -## 🐛 常见问题 - -### Q1: 数据库初始化失败? - -```typescript -// 错误:Database not initialized - -// 原因:登录时未初始化 -// 解决:已自动集成到 user.ts,无需修改 -``` - -### Q2: 会话列表不更新? - -```typescript -// 原因:未调用 init() -// 解决:在聊天页面 onMounted 中调用 -await sessionStore.init(accountId) -``` - -### Q3: 陌生好友消息不显示? - -```typescript -// 原因:后端未提供 getFriendDetail 接口 -// 解决:实现接口(参考上方接口说明) -``` - -### Q4: 切换账户数据混乱? - -```typescript -// 原因:未调用 switchAccount() -// 解决:切换时调用 -await sessionStore.switchAccount(newAccountId) -``` - ---- - -## 🎯 验收清单 - -### 功能验收 - -- [x] 登录后会话列表秒开(<200ms) -- [x] WebSocket 消息自动更新会话 -- [x] 陌生好友消息自动显示 -- [x] 切换账户数据正确隔离 -- [x] 退出登录数据清空 -- [x] 离线可查看缓存 - -### 性能验收 - -- [x] 首屏加载 < 200ms -- [x] 切换会话 < 100ms -- [x] 网络请求减少 95%+ -- [x] 无定时器轮询 - ---- - -## 📚 详细文档 - -- [聊天系统改造方案.md](./聊天系统改造方案.md) - 完整技术方案(3900行) -- [聊天系统改造实施说明.md](./聊天系统改造实施说明.md) - 实施说明 - ---- - -## 🎉 完成! - -改造已完成,核心功能已集成到以下文件: - -``` -TouchVueThree/ -├── src/ -│ ├── utils/ -│ │ ├── db.ts ← 数据库管理器 -│ │ └── dbManagers/ -│ │ ├── SessionManager.ts ← 会话管理器 -│ │ └── MessageManager.ts ← 消息管理器 -│ ├── stores/modules/ -│ │ ├── user.ts ← 用户 Store(已集成) -│ │ └── wechat/ -│ │ └── useSessionStore.ts ← 会话 Store(已重构) -│ └── composables/business/wechat/ -│ └── useWebSocket.ts ← WebSocket(已重构) -``` - -**开始使用吧!** 🚀 diff --git a/TouchVueThree/README.md b/TouchVueThree/README.md deleted file mode 100644 index 11d3ec0..0000000 --- a/TouchVueThree/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# 触客宝 Pro - Vue 3 版本 - -基于 Vite + Vue 3 + Tailwind CSS 构建的现代化工作台应用。 - -## 技术栈 - -- **Vue 3** - 渐进式 JavaScript 框架 -- **Vite** - 下一代前端构建工具 -- **Tailwind CSS** - 实用优先的 CSS 框架 -- **ESLint** - 代码质量检查 -- **Prettier** - 代码格式化 - -## 项目结构 - -``` -TouchVueThree/ -├── src/ -│ ├── assets/ # 静态资源 -│ │ └── style.css # 全局样式(Tailwind 指令) -│ ├── components/ # 公共组件 -│ │ ├── Sidebar.vue # 全局左侧导航栏 -│ │ └── AICreditPill.vue # AI算力展示胶囊 -│ ├── views/ # 页面视图 -│ │ ├── Chat/ # 聊天模块 -│ │ ├── Accounts.vue # 账号管理 -│ │ └── Analytics.vue # 数据看板 -│ ├── App.vue # 根组件 -│ └── main.js # 应用入口 -├── public/ # 公共静态资源 -├── index.html # HTML 入口 -└── vite.config.js # Vite 配置 -``` - -## 开发指南 - -### 安装依赖 - -```bash -npm install -# 或 -pnpm install -# 或 -yarn install -``` - -### 启动开发服务器 - -```bash -npm run dev -# 或 -pnpm dev -# 或 -yarn dev -``` - -开发服务器将在 `http://localhost:5173` 启动。 - -### 构建生产版本 - -```bash -npm run build -# 或 -pnpm build -# 或 -yarn build -``` - -构建产物将输出到 `dist/` 目录。 - -### 预览生产构建 - -```bash -npm run preview -# 或 -pnpm preview -# 或 -yarn preview -``` - -### 代码检查 - -```bash -# ESLint 检查并自动修复 -npm run lint - -# Prettier 格式化代码 -npm run format -``` - -## 环境变量 - -复制 `.env.example` 为 `.env` 并根据需要配置环境变量。 - -```bash -cp .env.example .env -``` - -## 功能模块 - -### 1. 聊天模块 (Chat) -- **ChatList** - 消息列表 -- **ChatWindow** - 聊天窗口 -- **ChatDetail** - CRM 资料详情 - -### 2. 账号管理 (Accounts) -- 微信账号管理 -- 账号状态监控 - -### 3. 数据看板 (Analytics) -- 业务数据统计 -- 可视化图表展示 - -## 开发规范 - -- 使用 Vue 3 Composition API -- 组件采用 ` -``` - -**就这么简单!** 不需要关心 WebSocket 连接、断开、重连等细节。 - ---- - -### 场景 2:发送消息 - -```typescript -import { useWebSocket } from '@/composables/business/wechat/useWebSocket' - -const { sendCommand } = useWebSocket() - -// 发送文本消息 -sendCommand('CmdSendTextMsg', { - content: '你好', - targetId: 123, - targetType: 'friend', -}) -``` - ---- - -### 场景 3:在 Store 中使用 - -```typescript -// stores/modules/wechat/useMessageStore.ts -import { defineStore } from 'pinia' -import { useMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - -export const useMessageStore = defineStore('message', () => { - const messages = ref([]) - - // 订阅消息(只需要写一次) - const { onNewMessage } = useMessageSubscription() - - onNewMessage((message) => { - messages.value.push(message) - }) - - return { - messages, - } -}) -``` - ---- - -## 📚 常见问题 - -### Q1: 如何知道有哪些消息类型可以订阅? - -**A:** 查看 `useMessageSubscription.ts` 中的方法: - -```typescript -// 可用的订阅方法 -onNewMessage // 新消息 -onMessageUpdate // 消息更新 -onMessageRecall // 消息撤回 -onSessionUpdate // 会话更新 -onAccountStatus // 账号状态 -onSystemNotification // 系统通知 -``` - ---- - -### Q2: 如何取消订阅? - -**A:** 订阅方法返回取消函数 - -```typescript -const unsubscribe = onNewMessage((msg) => { - console.log(msg) -}) - -// 不需要时取消 -unsubscribe() - -// 或者在组件卸载时自动取消(推荐) -onUnmounted(() => { - unsubscribe() -}) -``` - ---- - -### Q3: 消息没有收到怎么办? - -**A:** 开发环境会自动打印调试日志 - -```typescript -// 控制台会显示: -[MessageSubscription] subscribe: message:new, 当前订阅数: 1 -[MessageSubscription] emit: message:new, 订阅者数: 1 - -// 检查: -1. 订阅数是否 > 0 -2. emit 是否被调用 -3. WebSocket 是否已连接 -``` - ---- - -### Q4: 为什么不直接调用 Store 方法? - -**A:** 对比一下: - -```typescript -// ❌ 旧方式:强耦合 -import { useMessageStore } from '@/stores/modules/wechat' -const messageStore = useMessageStore() -messageStore.addMessage(msg) // 组件依赖具体的 Store - -// ✅ 新方式:解耦 -const { onNewMessage } = useMessageSubscription() -onNewMessage((msg) => { - // 组件不关心谁处理消息 - // Store 自己会订阅并处理 -}) -``` - -**好处:** - -- 组件不需要导入 Store -- Store 可以随时更换,不影响组件 -- 测试更简单 - ---- - -## 🎯 最佳实践 - -### ✅ DO(推荐做法) - -```typescript -// 1. 在 Store 中统一订阅 -// stores/modules/wechat/useMessageStore.ts -setupMessageSubscription({ - onNewMessage: (msg) => { - /* 处理 */ - }, - onMessageUpdate: ({ id, changes }) => { - /* 处理 */ - }, -}) - -// 2. 组件中只读取 Store 数据 -const messageStore = useMessageStore() -const messages = computed(() => messageStore.messages) -``` - -### ❌ DON'T(不推荐) - -```typescript -// 1. 不要在多个组件中重复订阅 -// 每个消息类型应该只在一个地方订阅(Store) - -// 2. 不要在订阅回调中做耗时操作 -onNewMessage(async (msg) => { - await heavyComputation() // ❌ 会阻塞后续消息 -}) - -// 改为异步处理 -onNewMessage((msg) => { - heavyComputation().catch(console.error) // ✅ 不阻塞 -}) -``` - ---- - -## 🔧 调试技巧 - -### 1. 查看订阅统计 - -```typescript -const { getStats } = useMessageSubscription() -console.log(getStats()) - -// 输出: -// { -// 'message:new': 2, // 2 个订阅者 -// 'message:update': 1, // 1 个订阅者 -// 'session:update': 3 // 3 个订阅者 -// } -``` - -### 2. 使用 Vue Devtools - -- 安装 Vue Devtools 扩展 -- 查看 Pinia Store 状态 -- 实时观察消息变化 - ---- - -## 📖 进阶阅读 - -- [WebSocket架构优化方案.md](./WebSocket架构优化方案.md) - 了解架构设计思路 -- [发布订阅模式](https://refactoring.guru/design-patterns/observer) - 理解设计模式 -- [Vue Composables](https://vuejs.org/guide/reusability/composables.html) - 学习 Composable 用法 - ---- - -## 💡 记住这些要点 - -1. **组件订阅消息** → `onNewMessage((msg) => {})` -2. **发送消息** → `sendCommand('CmdXxx', data)` -3. **Store 统一管理** → 用 `setupMessageSubscription` -4. **开发环境有日志** → 查看控制台 -5. **有问题看文档** → 本文档 + 源码注释 - ---- - -就这么简单!你已经学会了 90% 的 WebSocket 使用方法 🎉 diff --git a/TouchVueThree/WebSocket架构优化方案.md b/TouchVueThree/WebSocket架构优化方案.md deleted file mode 100644 index eb62f86..0000000 --- a/TouchVueThree/WebSocket架构优化方案.md +++ /dev/null @@ -1,562 +0,0 @@ -# WebSocket 架构优化方案 - -## 📊 旧项目问题分析 - -### 1. **代码组织混乱** - -#### 问题表现: -```typescript -// ❌ 旧项目:websocket.ts (615行) -// - WebSocket 连接管理 -// - 消息发送/接收 -// - 心跳/重连逻辑 -// - 客服状态查询 -// - 持久化存储逻辑 -// 所有功能杂糅在一起,职责不清晰 - -// ❌ 旧项目:msgManage.ts (469行+) -// - 40+ 个消息类型处理函数 -// - 直接调用多个 Store(weChat, customer, contact等) -// - 业务逻辑和消息处理混合 -// - 循环依赖严重 -``` - -**核心问题:** -1. **单一文件过大**:websocket.ts 615行,msgManage.ts 469行+ -2. **职责不清**:连接管理、消息处理、业务逻辑混在一起 -3. **循环依赖**:Store 之间相互调用,msgManage 调用多个 Store -4. **难以维护**:新增消息类型需要修改核心文件 -5. **测试困难**:耦合严重,无法单独测试 - -### 2. **消息处理机制复杂** - -```typescript -// ❌ 旧项目消息处理流程 -WebSocket.onmessage - → msgManageCore(data) - → messageHandlers[cmdType](message) - → 直接调用 useWeChatStore.addMessage() - → 直接调用 useCustomerStore.updateCustomerList() - → 直接调用 ContactManager.updateContact() - → 直接调用 db.xxx.put() - → 手动触发 window.dispatchEvent() -``` - -**核心问题:** -1. **强耦合**:消息处理直接依赖具体 Store -2. **难以扩展**:新增消息处理逻辑需要修改核心代码 -3. **状态管理混乱**:同时操作 Store、IndexedDB、DOM 事件 -4. **缺少中间层**:没有统一的消息分发和状态同步机制 - -### 3. **性能问题** - -```typescript -// ❌ 旧项目性能瓶颈 -CmdNewMessage: async (message) => { - // 1. 同步写入 Store(阻塞主线程) - receivedMsg(msgData) - - // 2. 同步写入 IndexedDB(阻塞主线程) - await MessageManager.saveMessage(msgData) - await ContactManager.updateContact(...) - - // 3. 同步更新缓存(阻塞主线程) - await sessionListCache.set(...) - - // 4. 触发 UI 更新(可能导致多次渲染) - window.dispatchEvent(new CustomEvent(...)) -} -``` - -**核心问题:** -1. **阻塞式写入**:所有操作都是同步的 -2. **重复更新**:多次触发 UI 重新渲染 -3. **缺少批处理**:高频消息会导致性能问题 -4. **缺少优先级**:所有消息同等处理 - ---- - -## ✅ 新项目优化方案 - -### 架构设计原则 - -1. **关注点分离(Separation of Concerns)** -2. **单一职责(Single Responsibility)** -3. **依赖倒置(Dependency Inversion)** -4. **发布订阅(Pub/Sub Pattern)** - ---- - -## 📁 文件组织结构 - -### 1. **分层架构** - -``` -TouchVueThree/src/composables/business/wechat/ -├── useWebSocket.ts # WebSocket 连接管理(核心) -├── useMessageSubscription.ts # 消息订阅中心(核心) -├── handlers/ # 消息处理器(模块化) -│ ├── index.ts # 处理器注册中心 -│ ├── messageHandlers.ts # 聊天消息处理 -│ ├── accountHandlers.ts # 账号状态处理 -│ ├── sessionHandlers.ts # 会话相关处理 -│ └── systemHandlers.ts # 系统通知处理 -├── middleware/ # 中间件(可选) -│ ├── messageQueue.ts # 消息队列(批处理) -│ ├── messageDedup.ts # 消息去重 -│ └── rateLimiter.ts # 流量控制 -└── utils/ # 工具函数 - ├── reconnection.ts # 重连策略 - ├── heartbeat.ts # 心跳管理 - └── serialization.ts # 序列化/反序列化 -``` - ---- - -## 🎯 核心模块职责 - -### 1. **useWebSocket.ts - 连接管理器** - -**职责:** 只负责 WebSocket 生命周期管理 - -```typescript -// ✅ 新项目优化后 -export function useWebSocket() { - return { - // 连接管理 - connect, - disconnect, - reconnect, - - // 状态 - status, - isConnected, - - // 基础通信 - send, - sendCommand, - - // 高级特性 - syncMissedMessages, // 增量同步 - } -} -``` - -**优点:** -- 职责单一,只管连接 -- 不依赖具体业务逻辑 -- 易于测试和复用 - ---- - -### 2. **useMessageSubscription.ts - 消息订阅中心** - -**职责:** 消息事件的发布/订阅 - -```typescript -// ✅ 发布订阅模式 -export function useMessageSubscription() { - const messageEmitter = mitt<{ - 'message:new': Message - 'message:update': { id: number; changes: Partial } - 'session:update': Session - 'account:status': AccountStatus - }>() - - return { - // 订阅(组件层面) - onNewMessage: (handler) => messageEmitter.on('message:new', handler), - onMessageUpdate: (handler) => messageEmitter.on('message:update', handler), - - // 发布(WebSocket 层面) - emitNewMessage: (msg) => messageEmitter.emit('message:new', msg), - emitMessageUpdate: (id, changes) => messageEmitter.emit('message:update', { id, changes }), - } -} -``` - -**优点:** -- 解耦发送方和接收方 -- 组件可按需订阅 -- 支持多个订阅者 -- 避免循环依赖 - ---- - -### 3. **handlers/ - 模块化处理器** - -**职责:** 按业务域划分消息处理逻辑 - -```typescript -// ✅ handlers/messageHandlers.ts -export const messageHandlers = { - // 新消息 - [WS_CMD_TYPE.RECEIVE_MESSAGE]: async (wsMessage) => { - // 1. 数据验证 - if (!validate(wsMessage.data)) return - - // 2. 更新 IndexedDB(异步,不阻塞) - SessionManager.updateOnNewMessage(...).catch(console.error) - - // 3. 发布事件(通知订阅者) - emitNewMessage(wsMessage.data) - }, - - // 消息状态 - [WS_CMD_TYPE.MESSAGE_STATUS]: (wsMessage) => { - emitMessageUpdate(wsMessage.data.id, { status: wsMessage.data.status }) - }, -} - -// ✅ handlers/accountHandlers.ts -export const accountHandlers = { - // 账号状态 - [WS_CMD_TYPE.ACCOUNT_STATUS]: (wsMessage) => { - // 处理账号相关逻辑 - }, -} - -// ✅ handlers/index.ts - 注册中心 -import { messageHandlers } from './messageHandlers' -import { accountHandlers } from './accountHandlers' - -export const createMessageHandler = () => { - const handlers = { - ...messageHandlers, - ...accountHandlers, - // 易于扩展 - } - - return (cmdType: string, message: WebSocketMessage) => { - const handler = handlers[cmdType] - if (handler) { - handler(message).catch(console.error) - } else { - console.warn('未注册的消息类型:', cmdType) - } - } -} -``` - -**优点:** -- **模块化**:按业务域拆分,单个文件 < 200 行 -- **可扩展**:新增消息类型只需新增文件 -- **可测试**:每个 handler 独立测试 -- **可维护**:修改不影响其他模块 - ---- - -## 🔧 具体优化点 - -### 1. **异步非阻塞处理** - -```typescript -// ❌ 旧项目:阻塞式 -CmdNewMessage: async (message) => { - await db.messages.put(message) // 阻塞 - await updateSession(...) // 阻塞 - window.dispatchEvent(...) // 同步 -} - -// ✅ 新项目:异步非阻塞 -handleNewMessage: async (wsMessage) => { - // 1. 立即发布事件(UI 优先) - emitNewMessage(wsMessage.data) - - // 2. 异步更新数据库(不阻塞 UI) - SessionManager.updateOnNewMessage(...) - .catch(console.error) // 静默失败 - - // 3. UI 自动订阅并响应 - // Store 中:onNewMessage((msg) => { /* 更新 UI */ }) -} -``` - ---- - -### 2. **消息去重与批处理** - -```typescript -// ✅ middleware/messageQueue.ts -export class MessageQueue { - private queue: WebSocketMessage[] = [] - private dedup = new Set() - private timer: NodeJS.Timeout | null = null - - // 添加消息(去重 + 批处理) - enqueue(message: WebSocketMessage) { - const key = this.getDeduKey(message) - if (this.dedup.has(key)) return // 去重 - - this.dedup.add(key) - this.queue.push(message) - - // 批处理:100ms 内的消息一起处理 - this.scheduleFlush() - } - - private scheduleFlush() { - if (this.timer) return - this.timer = setTimeout(() => { - this.flush() - }, 100) - } - - private flush() { - const batch = this.queue.splice(0, this.queue.length) - this.dedup.clear() - this.timer = null - - // 批量处理 - batch.forEach(msg => this.handler(msg)) - } -} -``` - ---- - -### 3. **智能重连策略** - -```typescript -// ✅ utils/reconnection.ts -export class ReconnectionManager { - private attempts = 0 - private maxAttempts = 5 - private baseDelay = 1000 - - // 指数退避 + 抖动 - getNextDelay(): number { - const exponential = this.baseDelay * Math.pow(2, this.attempts) - const jitter = Math.random() * 1000 // 0-1秒随机抖动 - return Math.min(exponential + jitter, 30000) // 最大30秒 - } - - // 重置策略 - reset() { - this.attempts = 0 - } - - // 增加计数 - increment() { - this.attempts++ - } - - // 是否应该重连 - shouldReconnect(): boolean { - return this.attempts < this.maxAttempts - } -} -``` - ---- - -### 4. **心跳超时检测** - -```typescript -// ✅ utils/heartbeat.ts -export class HeartbeatManager { - private timer: NodeJS.Timeout | null = null - private timeoutTimer: NodeJS.Timeout | null = null - - start(onTimeout: () => void) { - this.stop() - - // 每 30 秒发送心跳 - this.timer = setInterval(() => { - sendCommand(WS_CMD_TYPE.HEARTBEAT) - - // 5 秒内未收到响应 → 触发超时 - this.timeoutTimer = setTimeout(() => { - console.warn('心跳超时,触发重连') - onTimeout() - }, 5000) - }, 30000) - } - - // 收到心跳响应 - onResponse() { - if (this.timeoutTimer) { - clearTimeout(this.timeoutTimer) - this.timeoutTimer = null - } - } - - stop() { - if (this.timer) clearInterval(this.timer) - if (this.timeoutTimer) clearTimeout(this.timeoutTimer) - } -} -``` - ---- - -## 🎨 使用示例 - -### 1. **在组件中使用** - -```vue - -``` - ---- - -### 2. **在 Store 中使用** - -```typescript -// ✅ stores/modules/wechat/useMessageStore.ts -import { defineStore } from 'pinia' -import { useMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - -export const useMessageStore = defineStore('message', () => { - const messages = ref([]) - - // 订阅消息事件 - const { onNewMessage, onMessageUpdate } = useMessageSubscription() - - onNewMessage((message) => { - // 添加到消息列表 - messages.value.push(message) - - // 更新未读数 - increaseUnreadCount(message.sessionId) - }) - - onMessageUpdate(({ id, changes }) => { - const msg = messages.value.find(m => m.id === id) - if (msg) { - Object.assign(msg, changes) - } - }) - - return { - messages, - // ... - } -}) -``` - ---- - -## 📊 对比总结 - -| 维度 | 旧项目 | 新项目 | -|------|--------|--------| -| **文件组织** | 单文件 600+ 行 | 模块化,单文件 < 200 行 | -| **职责划分** | 混乱,多职责耦合 | 清晰,单一职责 | -| **消息处理** | 直接调用 Store | 发布订阅解耦 | -| **性能** | 阻塞式同步处理 | 异步非阻塞 + 批处理 | -| **扩展性** | 难,需修改核心代码 | 易,新增文件即可 | -| **测试性** | 难,强耦合 | 易,模块独立 | -| **维护性** | 低,牵一发动全身 | 高,修改影响范围小 | -| **重连策略** | 简单固定延迟 | 指数退避 + 抖动 | -| **心跳机制** | 基础心跳 | 心跳 + 超时检测 | -| **增量同步** | 无 | 支持断线重连后同步 | - ---- - -## 🚀 迁移建议 - -### 阶段 1:核心基础(已完成) -- ✅ 创建 `useWebSocket.ts` - 连接管理 -- ✅ 创建 `useMessageSubscription.ts` - 订阅中心 -- ✅ 基本消息处理逻辑 - -### 阶段 2:模块化拆分(进行中) -- [ ] 创建 `handlers/` 目录 -- [ ] 拆分消息处理器(按业务域) -- [ ] 实现消息队列和批处理 - -### 阶段 3:高级特性(规划中) -- [ ] 消息去重中间件 -- [ ] 智能重连策略 -- [ ] 增量同步接口对接 -- [ ] 性能监控和日志 - -### 阶段 4:测试和优化 -- [ ] 单元测试覆盖 -- [ ] 集成测试 -- [ ] 性能优化 -- [ ] 文档完善 - ---- - -## 💡 最佳实践 - -### 1. **组件使用 WebSocket** -```typescript -// ✅ 推荐:使用订阅模式 -const { onNewMessage } = useMessageSubscription() -onNewMessage((msg) => { /* 处理 */ }) - -// ❌ 不推荐:直接调用 WebSocket -useWebSocket().sendCommand(...) -``` - -### 2. **Store 使用订阅** -```typescript -// ✅ 推荐:在 Store 中统一订阅 -defineStore('message', () => { - const { onNewMessage } = useMessageSubscription() - onNewMessage((msg) => { /* 更新 Store */ }) -}) - -// ❌ 不推荐:组件分散订阅 -``` - -### 3. **错误处理** -```typescript -// ✅ 推荐:优雅降级 -handleNewMessage(msg).catch((error) => { - console.error('消息处理失败:', error) - // 不阻断后续消息 -}) - -// ❌ 不推荐:抛出错误 -await handleNewMessage(msg) // 可能阻塞 -``` - ---- - -## 📚 参考资料 - -- WebSocket 规范:https://datatracker.ietf.org/doc/html/rfc6455 -- 发布订阅模式:https://refactoring.guru/design-patterns/observer -- 消息队列最佳实践:https://martinfowler.com/articles/201701-event-driven.html diff --git a/TouchVueThree/WebSocket架构实施完成报告.md b/TouchVueThree/WebSocket架构实施完成报告.md deleted file mode 100644 index 688ed4b..0000000 --- a/TouchVueThree/WebSocket架构实施完成报告.md +++ /dev/null @@ -1,452 +0,0 @@ -# WebSocket 架构实施完成报告 - -> 📅 完成时间:2026-01-14 -> -> 🎯 目标:基于新架构重构 WebSocket 消息处理系统 - ---- - -## ✅ 完成内容 - -### 1. 核心架构文件 - -#### 📁 handlers/ 目录(模块化处理器) - -``` -src/composables/business/wechat/handlers/ -├── index.ts # 处理器注册中心 -├── messageHandlers.ts # 聊天消息处理 -├── accountHandlers.ts # 账号状态处理 -├── sessionHandlers.ts # 会话相关处理 -└── systemHandlers.ts # 系统通知处理 -``` - -**主要功能:** -- ✅ 消息处理器的统一注册和管理 -- ✅ 支持动态注册/注销处理器 -- ✅ 完善的错误处理和日志 -- ✅ 开发环境调试工具 - ---- - -### 2. 订阅系统增强 - -#### 📄 useMessageSubscription.ts - -**新增功能:** -- ✅ 完整的事件类型定义(`MessageEvents`) -- ✅ 调试工具:订阅统计、日志追踪 -- ✅ 全局单例模式(`setupMessageSubscription`) -- ✅ 自动生命周期管理 - -**改进点:** -```typescript -// 之前:简单的发布订阅 -emitNewMessage(message) - -// 现在:带调试信息的智能订阅 -[MessageSubscription] emit: message:new, 订阅者数: 2 - preview: {"id":123,"content":"你好"}... -``` - ---- - -### 3. WebSocket 核心简化 - -#### 📄 useWebSocket.ts - -**重构内容:** -- ✅ 移除 200+ 行的消息处理逻辑 -- ✅ 集成 `MessageHandlerManager` -- ✅ 统一的消息分发机制 - -**代码对比:** - -```typescript -// ❌ 旧版:switch-case 处理(100+ 行) -switch (message.cmdType) { - case 'CmdReceiveMessage': - // 50 行处理逻辑 - break - case 'CmdMessageStatus': - // 30 行处理逻辑 - break - // ... 10+ 个 case -} - -// ✅ 新版:一行搞定 -await handlerManager.handle(cmdType, message) -``` - ---- - -### 4. Store 集成 - -#### 📄 useMessageStore.ts - -**新增功能:** -- ✅ 使用 `setupMessageSubscription` 统一订阅 -- ✅ 自动处理新消息、状态更新、撤回 -- ✅ 完善的日志和错误处理 - -**代码示例:** - -```typescript -setupMessageSubscription({ - onNewMessage: (message) => { - addMessage(message) - }, - onMessageUpdate: ({ id, changes }) => { - updateMessageStatus(id, changes.status) - }, - onMessageRecall: ({ messageId, sessionId }) => { - // 处理撤回 - }, -}) -``` - ---- - -### 5. 常量扩展 - -#### 📄 constants/wechat.ts - -**扩展内容:** -- ✅ 40+ 个 WebSocket 命令类型 -- ✅ 按功能分组(连接、消息、会话、账号、系统) -- ✅ 完整的注释说明 - ---- - -### 6. 文档体系 - -#### 📚 文档列表 - -1. **WebSocket架构优化方案.md** - 架构设计思路 -2. **WebSocket使用指南.md** - 新人快速上手 -3. **WebSocket架构实施完成报告.md** - 本文档 - ---- - -## 📊 架构对比 - -### 文件结构对比 - -``` -旧架构(单体) 新架构(分层) -───────────── ───────────── -useWebSocket.ts (600行) useWebSocket.ts (150行) - ├─ 连接管理 ├─ 连接管理 - ├─ 消息处理 (400行) └─ 消息分发 (20行) - └─ 状态管理 - handlers/ (4文件, 500行) - ├─ messageHandlers.ts - ├─ accountHandlers.ts - ├─ sessionHandlers.ts - └─ systemHandlers.ts - - useMessageSubscription.ts (300行) - ├─ 事件定义 - ├─ 调试工具 - └─ 订阅管理 -``` - -### 代码复杂度对比 - -| 指标 | 旧架构 | 新架构 | 改善 | -|-----|--------|--------|------| -| **单文件行数** | 600+ | 150 | -75% | -| **循环复杂度** | 高(大量switch) | 低(表驱动) | ↓↓↓ | -| **耦合度** | 强耦合 | 松耦合 | ↑↑↑ | -| **可测试性** | 困难 | 容易 | ↑↑↑ | -| **可维护性** | 困难 | 容易 | ↑↑↑ | - ---- - -## 🎯 已实现的功能 - -### ✅ 核心功能 - -- [x] 消息接收和处理 -- [x] 消息状态更新 -- [x] 消息撤回 -- [x] 会话更新 -- [x] 账号状态管理 -- [x] 系统通知 -- [x] 心跳机制 -- [x] 错误处理 - -### ✅ 辅助功能 - -- [x] 消息去重 -- [x] 日志追踪 -- [x] 调试工具 -- [x] 类型安全 -- [x] 自动订阅管理 - ---- - -## 🚀 新架构优势 - -### 1. **开发效率** ↑ 30% - -```typescript -// 旧:修改消息处理,需要理解 600 行代码 -// 新:只需修改 messageHandlers.ts 中的一个函数 - -// 新增功能:只需添加一个 handler -export const messageHandlers: HandlerRegistry = { - [WS_CMD_TYPE.NEW_FEATURE]: handleNewFeature, - // 其他 handler 不受影响 -} -``` - -### 2. **Bug 率** ↓ 50% - -- 模块独立,影响范围可控 -- 类型安全,编译期检查 -- 统一错误处理,不会中断其他消息 - -### 3. **协作冲突** ↓ 80% - -``` -旧:多人修改同一文件 → Git 冲突 -新:每人修改自己的 handler → 零冲突 -``` - -### 4. **调试时间** ↓ 60% - -```typescript -// 开发环境自动打印: -[MessageHandler] 处理消息: CmdReceiveMessage - preview: {"sessionId":123,"content":"你好"}... -[MessageHandler] ✅ 处理完成: CmdReceiveMessage - -[MessageSubscription] emit: message:new, 订阅者数: 2 -``` - ---- - -## 📈 性能测试 - -### 消息处理性能 - -``` -测试场景:1000 条消息,3 个订阅者 - -旧架构: -- 平均处理时间:50ms -- 内存占用:8.2MB - -新架构: -- 平均处理时间:55ms (+10%) -- 内存占用:8.5MB (+4%) - -结论:性能损失可接受,换来的是: -✅ 可维护性 ↑↑↑ -✅ 可扩展性 ↑↑↑ -✅ 代码质量 ↑↑↑ -``` - ---- - -## 📋 使用示例 - -### 场景 1:新增消息类型 - -```typescript -// 1. 在 constants/wechat.ts 添加命令类型 -export const WS_CMD_TYPE = { - NEW_FEATURE: 'CmdNewFeature', -} - -// 2. 在 messageHandlers.ts 添加处理器 -function handleNewFeature(message: WebSocketMessage): void { - console.log('处理新功能:', message.data) - // 你的处理逻辑 -} - -// 3. 注册处理器 -export const messageHandlers: HandlerRegistry = { - [WS_CMD_TYPE.NEW_FEATURE]: handleNewFeature, -} - -// 完成!自动生效 -``` - -### 场景 2:组件订阅消息 - -```vue - -``` - -### 场景 3:调试消息流 - -```typescript -// 1. 查看订阅统计 -const { getStats } = useMessageSubscription() -console.log(getStats()) -// → { 'message:new': 2, 'session:update': 1 } - -// 2. 查看已注册的处理器 -const manager = getMessageHandlerManager() -console.log(manager.getRegisteredTypes()) -// → ['CmdReceiveMessage', 'CmdMessageStatus', ...] - -// 3. 动态注册处理器(调试用) -manager.register('CmdTest', (msg) => { - console.log('测试消息:', msg) -}) -``` - ---- - -## 🔧 后续优化建议 - -### 阶段 3:高级特性(可选) - -#### 1. 消息队列中间件 - -```typescript -// middleware/messageQueue.ts -export class MessageQueue { - private queue: Message[] = [] - - async process() { - // 批量处理,减少渲染次数 - const batch = this.queue.splice(0, 100) - await Promise.all(batch.map(msg => handleMessage(msg))) - } -} -``` - -#### 2. 智能重连策略 - -```typescript -// utils/reconnection.ts -export class SmartReconnection { - private attempts = 0 - private backoff = [1s, 2s, 5s, 10s, 30s] - - async reconnect() { - const delay = this.backoff[this.attempts] - await sleep(delay) - // 重连逻辑 - } -} -``` - -#### 3. 消息去重增强 - -```typescript -// utils/deduplication.ts -export class MessageDeduplicator { - private cache = new LRU(1000) - - isDuplicate(messageId: string): boolean { - return this.cache.has(messageId) - } -} -``` - -#### 4. 性能监控 - -```typescript -// utils/performance.ts -export class PerformanceMonitor { - track(cmdType: string, duration: number) { - // 上报性能数据 - console.log(`[Performance] ${cmdType}: ${duration}ms`) - } -} -``` - ---- - -## 📝 团队协作指南 - -### 新增消息处理器 - -1. **创建分支**:`feature/handler-xxx` -2. **修改文件**:`handlers/xxxHandlers.ts` -3. **添加测试**:确保功能正常 -4. **提交 PR**:代码审查 - -### 代码规范 - -```typescript -// ✅ 好的实践 -function handleXxx(message: WebSocketMessage): void { - // 1. 数据验证 - if (!message.data) return - - // 2. 业务处理 - const result = process(message.data) - - // 3. 发布事件 - emitXxx(result) - - // 4. 日志记录 - console.log('[XxxHandler] 处理完成') -} - -// ❌ 避免 -function handleXxx(message: any) { - // 没有类型 - // 没有验证 - // 没有日志 - doSomething(message) -} -``` - ---- - -## 🎉 总结 - -### 成就解锁 - -- ✅ 代码行数减少 75% -- ✅ 模块化程度提升 100% -- ✅ 可维护性提升 200% -- ✅ 团队协作效率提升 80% -- ✅ Bug 率降低 50% - -### 最终评分 - -| 维度 | 分数 | 评价 | -|-----|------|------| -| 代码质量 | 9.5/10 | 优秀 ⭐⭐⭐⭐⭐ | -| 架构设计 | 9/10 | 优秀 ⭐⭐⭐⭐⭐ | -| 可维护性 | 10/10 | 完美 ⭐⭐⭐⭐⭐ | -| 性能表现 | 8/10 | 良好 ⭐⭐⭐⭐ | -| 文档完善度 | 10/10 | 完美 ⭐⭐⭐⭐⭐ | -| **总分** | **9.3/10** | **优秀** | - ---- - -## 📞 支持 - -如有问题,请参考: -- [WebSocket使用指南.md](./WebSocket使用指南.md) - 快速上手 -- [WebSocket架构优化方案.md](./WebSocket架构优化方案.md) - 设计思路 - ---- - -**🎊 恭喜!WebSocket 架构重构完成!** - -现在你拥有了一个: -- ✅ 模块化的 -- ✅ 可维护的 -- ✅ 可扩展的 -- ✅ 高质量的 - -WebSocket 消息处理系统!🚀 diff --git a/TouchVueThree/content数据实例.md b/TouchVueThree/content数据实例.md deleted file mode 100644 index 9c71edf..0000000 --- a/TouchVueThree/content数据实例.md +++ /dev/null @@ -1,19 +0,0 @@ -以下为content字段里各种消息类型: - -1、纯字符串或数字: -先生上的飞机啊立刻搭街坊拉萨 - -2、连接字符串 -https://uniapp.dcloud.net.cn/component/ - -3、json字符串,以下是包含xml的,这种你返回: 文本过长,或者从底部的title标签获取文字展示,一般这类型的json字符很长都会被截断,因此数据不完整,你要做好兼容处理。 -{"contentXml":"wxid_znvuzsl6ktqz22:\n\n\n\t\n\t\t1kg/瓶【美味可口】海天上等蚝油 鲜蚝鲜熬QH\n\t\t\n\t\t\n\t\tview\n\t\t33\n\t\t0\n\t\t\n\t\thttps://mp.weixin.qq.com/mp/waerrpage?appid=wxd9813e0a0d4d4156&type=upgrade&upgradetype=3#wechat_redirect\n\t\t\n\t\t0\n\t\t\n\t\t\n\t\t0\n\t\t\n\t\t\t\n\t\t\t0\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t3057020100044b30490201000204d9542c9002032f9ea10204207928b602046965ccc4042466333434303231392d303461302d343233312d613363652d3531373433363563663361610204051408030201000405004c537500\n\t\t\t2e91bcab4cb75e118f61e1fafafa1a4c\n\t\t\t39933\n\t\t\t504\n\t\t\t630\n\t\t\t3c943a9a1f0ffcfcec8113dd013f2f84\n\t\t\t3c943a9a1f0ffcfcec8113dd013f2f84\n\t\t\t1\n\t\t\t\n\t\t\t0\n\t\t\n\t\t\n\t\t0\n\t\tgh_e3e0d867dd82@app\n\t\t多多买菜\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\tnull\n\t\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\tnull\n\t\t\tnull\n\t\t\t\n\t\t\tnull\n\t\t\t0\n\t\t\tnull\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\twxapp_wxd9813e0a0d4d4156pages/vgt_s_p_s/vgt_s_p_s.html?refer_share_channel=message&refer_share_id=1145014a-6fdd-4dd6-8302-94272647957c&refer_share_uin=V7BXFCIT7Y2GRLBORYN4KGDWF4_GEXDA&refer_share_btn=share_button&should_forward_type=1&store_id=8975643072730&recall_goods_sn=%5BEAJqjJpVv0jQtltEiWBBRtwF%2CEAImDcMTw-tH6-3d6tvE0arZ%5D&arugula_token=1768278971845-a45418bd1f739bc7&\_s_source=wechat_group_chat_bee&refer_share_exp=default&title_type=-1&goods_sn_list=EAIX_pmCEnEt6rIwRpQ5v7wg%2CEAJYO_wmfANFKssSp721TMmq&grab_group_grey=true&goods_card_share_type=20&title_scene=robot&top_goods_num=2&sku_sn_list=EAJ3lYdt3g9hFkzz1FEw_Bv2%2CEALIvbT2evwnXSCQKLB3zSwt&card_price_use_opt_show_price=d_1&\_oc_bee_msg_id=4724030717-0-9521219-c-1&\_oc_user_role=cap&\_x_cid=xcx_double_goods_card_158390_share_bee\n\t\t\t\n\t\t\n\t\t\n\t\t2e91bcab4cb75e118f61e1fafafa1a4c\n\t\t\n\t\t\t0\n\t\t\t0\n\t\t\n\t\t\n\t\t\t\n\t\t\tgh_e3e0d867dd82@app\n\t\t\twxd9813e0a0d4d4156\n\t\t\t2254\n\t\t\t2\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t0\n\t\t\t0\n\t\t\t\n\t\t\t\t630\n\t\t\t\t504\n\t\t\t\t0\n\t\t\t\n\t\t\t103967\n\t\t\n\t\t\n\t\t\n\t\t\t0\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\tfalse\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n \n \n \n \n 33\n 0\n 0\n \n \n \n \n 0\n https://mp.weixin.qq.com/mp/waerrpage?appid=wx9d58f7af796b898c&type=upgrade&upgradetype=3#wechat_redirect\n \n \n \n \n 0\n \n \n \n \n \n \n \n \n \n \n 0\n\n \n \n \n \n \n \n \n\n \n \n gh_2e76e3c5b02b@app\n \n \n \n \n \n \n \n 2\n 0\n \n \n \n 0\n 0\n \n \n 25984982650643454@openim\n 0\n \n 1\n \n \n \n","previewImage":"https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/weremote/chat-logs/5E2C38F5A275450D935F3ECEC076124E/aa6d4c2f7b1fe24d04d34f4f409883e6/3/654d0d7b2aaa65b48a96c1110e225029a9b7a0d9","type":"miniprogram"} diff --git a/TouchVueThree/env.d.ts b/TouchVueThree/env.d.ts deleted file mode 100644 index aafde74..0000000 --- a/TouchVueThree/env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/// - -declare module '*.vue' { - import type { DefineComponent } from 'vue' - const component: DefineComponent<{}, {}, any> - export default component -} - -interface ImportMetaEnv { - readonly VITE_APP_TITLE: string - readonly VITE_APP_VERSION: string - readonly VITE_API_BASE_URL: string - readonly VITE_API_TIMEOUT: string - readonly VITE_ENABLE_DEBUG: string - readonly VITE_ENABLE_MOCK: string - // 更多环境变量... -} - -interface ImportMeta { - readonly env: ImportMetaEnv -} diff --git a/TouchVueThree/env.example b/TouchVueThree/env.example deleted file mode 100644 index 47125b9..0000000 --- a/TouchVueThree/env.example +++ /dev/null @@ -1,11 +0,0 @@ -# 应用配置 -VITE_APP_TITLE=触客宝 Pro -VITE_APP_VERSION=1.0.0 - -# API 配置 -VITE_API_BASE_URL=http://localhost:3000/api -VITE_API_TIMEOUT=10000 - -# 功能开关 -VITE_ENABLE_DEBUG=false -VITE_ENABLE_MOCK=false diff --git a/TouchVueThree/eslint.config.js b/TouchVueThree/eslint.config.js deleted file mode 100644 index f5f1c0c..0000000 --- a/TouchVueThree/eslint.config.js +++ /dev/null @@ -1,144 +0,0 @@ -import js from '@eslint/js' -import vue from 'eslint-plugin-vue' -import tseslint from 'typescript-eslint' -import vueParser from 'vue-eslint-parser' -import prettier from 'eslint-plugin-prettier' -import prettierConfig from 'eslint-config-prettier' - -export default tseslint.config( - // 基础 JavaScript 推荐规则 - js.configs.recommended, - - // 全局忽略 - { - ignores: [ - 'dist/**', - 'node_modules/**', - '*.config.js', - '*.config.mjs', - '*.config.ts', - '.vite/**', - 'coverage/**', - '*.d.ts', - 'auto-imports.d.ts', - 'components.d.ts', - '.eslintrc-auto-import.json', - ], - }, - - // TypeScript 推荐配置 - ...tseslint.configs.recommended, - - // Vue 文件配置 - { - files: ['**/*.vue'], - languageOptions: { - parser: vueParser, - parserOptions: { - parser: tseslint.parser, - ecmaVersion: 'latest', - sourceType: 'module', - }, - }, - plugins: { - vue, - prettier, - }, - rules: { - // Vue 推荐规则 - ...vue.configs['vue3-recommended'].rules, - - // Vue 自定义规则 - 'vue/multi-word-component-names': 'off', - 'vue/require-default-prop': 'off', - 'vue/no-v-html': 'off', - 'vue/html-self-closing': [ - 'error', - { - html: { - void: 'always', - normal: 'never', - component: 'always', - }, - svg: 'always', - math: 'always', - }, - ], - - // Prettier 规则 - 'prettier/prettier': [ - 'error', - { - endOfLine: 'auto', - }, - ], - - // 通用规则 - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - }, - }, - - // TypeScript 文件配置 - { - files: ['**/*.ts', '**/*.tsx'], - plugins: { - prettier, - }, - rules: { - // 自定义 TypeScript 规则 - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/ban-ts-comment': [ - 'error', - { - 'ts-ignore': 'allow-with-description', - }, - ], - - // Prettier 规则 - 'prettier/prettier': [ - 'error', - { - endOfLine: 'auto', - }, - ], - - // 通用规则 - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-unused-vars': 'off', // 使用 TypeScript 的规则 - }, - }, - - // JavaScript 文件配置 - { - files: ['**/*.js', '**/*.jsx'], - languageOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - plugins: { - prettier, - }, - rules: { - 'prettier/prettier': [ - 'error', - { - endOfLine: 'auto', - }, - ], - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - }, - }, - - // Prettier 配置(禁用冲突规则) - prettierConfig, -) diff --git a/TouchVueThree/index.html b/TouchVueThree/index.html deleted file mode 100644 index fd3eab0..0000000 --- a/TouchVueThree/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - 触客宝 Pro - 全局工作台 - - - - - - - - - - - - diff --git a/TouchVueThree/package.json b/TouchVueThree/package.json deleted file mode 100644 index 8eaea9d..0000000 --- a/TouchVueThree/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "touchvue-three", - "version": "2.0.0", - "type": "module", - "private": true, - "license": "MIT", - "scripts": { - "dev": "vite", - "build": "vue-tsc && vite build", - "preview": "vite preview", - "type-check": "vue-tsc --noEmit", - "lint": "eslint . --fix", - "lint:check": "eslint .", - "format": "prettier --write \"src/**/*.{vue,ts,tsx,js,json,scss,css}\"", - "format:check": "prettier --check \"src/**/*.{vue,ts,tsx,js,json,scss,css}\"", - "analyze": "vite build --mode analyze" - }, - "dependencies": { - "@element-plus/icons-vue": "^2.3.2", - "@sentry/vue": "^7.120.4", - "@tanstack/vue-query": "^5.92.5", - "@vueuse/core": "^10.11.1", - "axios": "^1.13.2", - "dayjs": "^1.11.19", - "dexie": "^4.2.1", - "echarts": "^5.6.0", - "element-plus": "^2.13.1", - "lodash-es": "^4.17.21", - "mitt": "^3.0.1", - "nanoid": "^5.0.4", - "pinia": "^2.3.1", - "pinia-plugin-persistedstate": "^3.2.3", - "vue": "^3.5.26", - "vue-echarts": "^6.7.3", - "vue-router": "^4.6.4" - }, - "devDependencies": { - "@eslint/js": "^9.18.0", - "@types/lodash-es": "^4.17.12", - "@types/node": "^20.11.5", - "@vitejs/plugin-vue": "^5.0.4", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-vue": "^10.6.2", - "prettier": "^3.7.4", - "rollup-plugin-visualizer": "^5.14.0", - "sass": "^1.75.0", - "typescript": "^5.4.5", - "typescript-eslint": "^8.18.2", - "unplugin-auto-import": "^0.17.5", - "unplugin-vue-components": "^0.26.0", - "vite": "^5.1.4", - "vite-plugin-compression": "^0.5.1", - "vue-tsc": "^1.8.27" - } -} diff --git a/TouchVueThree/pnpm-lock.yaml b/TouchVueThree/pnpm-lock.yaml deleted file mode 100644 index 354f47c..0000000 --- a/TouchVueThree/pnpm-lock.yaml +++ /dev/null @@ -1,3469 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@element-plus/icons-vue': - specifier: ^2.3.2 - version: 2.3.2(vue@3.5.26(typescript@5.9.3)) - '@sentry/vue': - specifier: ^7.120.4 - version: 7.120.4(vue@3.5.26(typescript@5.9.3)) - '@tanstack/vue-query': - specifier: ^5.92.5 - version: 5.92.5(vue@3.5.26(typescript@5.9.3)) - '@vueuse/core': - specifier: ^10.11.1 - version: 10.11.1(vue@3.5.26(typescript@5.9.3)) - axios: - specifier: ^1.13.2 - version: 1.13.2 - dayjs: - specifier: ^1.11.19 - version: 1.11.19 - dexie: - specifier: ^4.2.1 - version: 4.2.1 - echarts: - specifier: ^5.6.0 - version: 5.6.0 - element-plus: - specifier: ^2.13.1 - version: 2.13.1(vue@3.5.26(typescript@5.9.3)) - lodash-es: - specifier: ^4.17.21 - version: 4.17.22 - mitt: - specifier: ^3.0.1 - version: 3.0.1 - nanoid: - specifier: ^5.0.4 - version: 5.1.6 - pinia: - specifier: ^2.3.1 - version: 2.3.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) - pinia-plugin-persistedstate: - specifier: ^3.2.3 - version: 3.2.3(pinia@2.3.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))) - vue: - specifier: ^3.5.26 - version: 3.5.26(typescript@5.9.3) - vue-echarts: - specifier: ^6.7.3 - version: 6.7.3(@vue/runtime-core@3.5.26)(echarts@5.6.0)(vue@3.5.26(typescript@5.9.3)) - vue-router: - specifier: ^4.6.4 - version: 4.6.4(vue@3.5.26(typescript@5.9.3)) - devDependencies: - '@eslint/js': - specifier: ^9.18.0 - version: 9.39.2 - '@types/lodash-es': - specifier: ^4.17.12 - version: 4.17.12 - '@types/node': - specifier: ^20.11.5 - version: 20.19.28 - '@vitejs/plugin-vue': - specifier: ^5.0.4 - version: 5.2.4(vite@5.4.21(@types/node@20.19.28)(sass@1.97.2))(vue@3.5.26(typescript@5.9.3)) - eslint: - specifier: ^9.18.0 - version: 9.39.2 - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2) - eslint-plugin-prettier: - specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2))(eslint@9.39.2)(prettier@3.7.4) - eslint-plugin-vue: - specifier: ^10.6.2 - version: 10.6.2(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@10.2.0(eslint@9.39.2)) - prettier: - specifier: ^3.7.4 - version: 3.7.4 - rollup-plugin-visualizer: - specifier: ^5.14.0 - version: 5.14.0(rollup@4.55.1) - sass: - specifier: ^1.75.0 - version: 1.97.2 - typescript: - specifier: ^5.4.5 - version: 5.9.3 - typescript-eslint: - specifier: ^8.18.2 - version: 8.52.0(eslint@9.39.2)(typescript@5.9.3) - unplugin-auto-import: - specifier: ^0.17.5 - version: 0.17.8(@vueuse/core@10.11.1(vue@3.5.26(typescript@5.9.3)))(rollup@4.55.1) - unplugin-vue-components: - specifier: ^0.26.0 - version: 0.26.0(@babel/parser@7.28.5)(rollup@4.55.1)(vue@3.5.26(typescript@5.9.3)) - vite: - specifier: ^5.1.4 - version: 5.4.21(@types/node@20.19.28)(sass@1.97.2) - vite-plugin-compression: - specifier: ^0.5.1 - version: 0.5.1(vite@5.4.21(@types/node@20.19.28)(sass@1.97.2)) - vue-tsc: - specifier: ^1.8.27 - version: 1.8.27(typescript@5.9.3) - -packages: - - '@antfu/utils@0.7.10': - resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} - engines: {node: '>=6.9.0'} - - '@ctrl/tinycolor@3.6.1': - resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} - engines: {node: '>=10'} - - '@element-plus/icons-vue@2.3.2': - resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} - peerDependencies: - vue: ^3.2.0 - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@floating-ui/core@1.7.3': - resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - - '@floating-ui/dom@1.7.4': - resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@parcel/watcher-android-arm64@2.5.4': - resolution: {integrity: sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.4': - resolution: {integrity: sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.4': - resolution: {integrity: sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.4': - resolution: {integrity: sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.4': - resolution: {integrity: sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.4': - resolution: {integrity: sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.4': - resolution: {integrity: sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.4': - resolution: {integrity: sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.4': - resolution: {integrity: sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.4': - resolution: {integrity: sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.4': - resolution: {integrity: sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.4': - resolution: {integrity: sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.4': - resolution: {integrity: sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.4': - resolution: {integrity: sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==} - engines: {node: '>= 10.0.0'} - - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.55.1': - resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.55.1': - resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.55.1': - resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.55.1': - resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.55.1': - resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.55.1': - resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.55.1': - resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.55.1': - resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.55.1': - resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.55.1': - resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.55.1': - resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.55.1': - resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.55.1': - resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.55.1': - resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.55.1': - resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.55.1': - resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.55.1': - resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.55.1': - resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.55.1': - resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.55.1': - resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.55.1': - resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.55.1': - resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.55.1': - resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.55.1': - resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.55.1': - resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} - cpu: [x64] - os: [win32] - - '@sentry-internal/feedback@7.120.4': - resolution: {integrity: sha512-eSwgvTdrh03zYYaI6UVOjI9p4VmKg6+c2+CBQfRZX++6wwnCVsNv7XF7WUIpVGBAkJ0N2oapjQmCzJKGKBRWQg==} - engines: {node: '>=12'} - - '@sentry-internal/replay-canvas@7.120.4': - resolution: {integrity: sha512-2+W4CgUL1VzrPjArbTid4WhKh7HH21vREVilZdvffQPVwOEpgNTPAb69loQuTlhJVveh9hWTj2nE5UXLbLP+AA==} - engines: {node: '>=12'} - - '@sentry-internal/tracing@7.120.4': - resolution: {integrity: sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==} - engines: {node: '>=8'} - - '@sentry/browser@7.120.4': - resolution: {integrity: sha512-ymlNtIPG6HAKzM/JXpWVGCzCNufZNADfy+O/olZuVJW5Be1DtOFyRnBvz0LeKbmxJbXb2lX/XMhuen6PXPdoQw==} - engines: {node: '>=8'} - - '@sentry/core@7.120.4': - resolution: {integrity: sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==} - engines: {node: '>=8'} - - '@sentry/integrations@7.120.4': - resolution: {integrity: sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==} - engines: {node: '>=8'} - - '@sentry/replay@7.120.4': - resolution: {integrity: sha512-FW8sPenNFfnO/K7sncsSTX4rIVak9j7VUiLIagJrcqZIC7d1dInFNjy8CdVJUlyz3Y3TOgIl3L3+ZpjfyMnaZg==} - engines: {node: '>=12'} - - '@sentry/types@7.120.4': - resolution: {integrity: sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==} - engines: {node: '>=8'} - - '@sentry/utils@7.120.4': - resolution: {integrity: sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==} - engines: {node: '>=8'} - - '@sentry/vue@7.120.4': - resolution: {integrity: sha512-dPWNzS0kA8z9IDhOLuCpj6UC1Kw26WnTnjDy5m9qISm8fObalmpG6c+iL4Yj5qSBR0e6Jwak8V1gES2wTQGSLQ==} - engines: {node: '>=8'} - peerDependencies: - vue: 2.x || 3.x - - '@sxzz/popperjs-es@2.11.7': - resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} - - '@tanstack/match-sorter-utils@8.19.4': - resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} - engines: {node: '>=12'} - - '@tanstack/query-core@5.90.16': - resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==} - - '@tanstack/vue-query@5.92.5': - resolution: {integrity: sha512-wbJ820OdPbmyKqrs/FW3n/4fScji63d2aAQqTl54ex9UolJwJdrEqDmSGyc5R5orgs3OlWVlF5UsK+/7wLxgzA==} - peerDependencies: - '@vue/composition-api': ^1.1.2 - vue: ^2.6.0 || ^3.3.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/lodash-es@4.17.12': - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - - '@types/lodash@4.17.23': - resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} - - '@types/node@20.19.28': - resolution: {integrity: sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==} - - '@types/web-bluetooth@0.0.20': - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - - '@typescript-eslint/eslint-plugin@8.52.0': - resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.52.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/parser@8.52.0': - resolution: {integrity: sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.52.0': - resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/scope-manager@8.52.0': - resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.52.0': - resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.52.0': - resolution: {integrity: sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/types@8.52.0': - resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.52.0': - resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.52.0': - resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.52.0': - resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@vitejs/plugin-vue@5.2.4': - resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 || ^6.0.0 - vue: ^3.2.25 - - '@volar/language-core@1.11.1': - resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} - - '@volar/source-map@1.11.1': - resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} - - '@volar/typescript@1.11.1': - resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} - - '@vue/compiler-core@3.5.26': - resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} - - '@vue/compiler-dom@3.5.26': - resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} - - '@vue/compiler-sfc@3.5.26': - resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} - - '@vue/compiler-ssr@3.5.26': - resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} - - '@vue/devtools-api@6.6.4': - resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - - '@vue/language-core@1.8.27': - resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@vue/reactivity@3.5.26': - resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} - - '@vue/runtime-core@3.5.26': - resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} - - '@vue/runtime-dom@3.5.26': - resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} - - '@vue/server-renderer@3.5.26': - resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} - peerDependencies: - vue: 3.5.26 - - '@vue/shared@3.5.26': - resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} - - '@vueuse/core@10.11.1': - resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} - - '@vueuse/metadata@10.11.1': - resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} - - '@vueuse/shared@10.11.1': - resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - async-validator@4.2.5: - resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - axios@1.13.2: - resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} - - de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - dexie@4.2.1: - resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - echarts@5.6.0: - resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} - - element-plus@2.13.1: - resolution: {integrity: sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==} - peerDependencies: - vue: ^3.3.0 - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - entities@7.0.0: - resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} - engines: {node: '>=0.12'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-prettier@5.5.4: - resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - - eslint-plugin-vue@10.6.2: - resolution: {integrity: sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 - eslint: ^8.57.0 || ^9.0.0 - vue-eslint-parser: ^10.0.0 - peerDependenciesMeta: - '@stylistic/eslint-plugin': - optional: true - '@typescript-eslint/parser': - optional: true - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - - immutable@5.1.4: - resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - jsonfile@6.2.0: - resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lie@3.1.1: - resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} - - local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} - engines: {node: '>=14'} - - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} - - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} - - localforage@1.10.0: - resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash-es@4.17.22: - resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} - - lodash-unified@1.0.3: - resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} - peerDependencies: - '@types/lodash-es': '*' - lodash: '*' - lodash-es: '*' - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - memoize-one@6.0.0: - resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - muggle-string@0.3.1: - resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@5.1.6: - resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} - engines: {node: ^18 || >=20} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-wheel-es@1.2.0: - resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - pinia-plugin-persistedstate@3.2.3: - resolution: {integrity: sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==} - peerDependencies: - pinia: ^2.0.0 - - pinia@2.3.1: - resolution: {integrity: sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==} - peerDependencies: - typescript: '>=4.4.4' - vue: ^2.7.0 || ^3.5.11 - peerDependenciesMeta: - typescript: - optional: true - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} - engines: {node: '>=4'} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-linter-helpers@1.0.1: - resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} - engines: {node: '>=6.0.0'} - - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} - engines: {node: '>=14'} - hasBin: true - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - remove-accents@0.5.0: - resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resize-detector@0.3.0: - resolution: {integrity: sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup-plugin-visualizer@5.14.0: - resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - rolldown: 1.x - rollup: 2.x || 3.x || 4.x - peerDependenciesMeta: - rolldown: - optional: true - rollup: - optional: true - - rollup@4.55.1: - resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - sass@1.97.2: - resolution: {integrity: sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==} - engines: {node: '>=14.0.0'} - hasBin: true - - scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - strip-literal@2.1.1: - resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - synckit@0.11.11: - resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} - engines: {node: ^14.18.0 || >=16.0.0} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - tslib@2.3.0: - resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript-eslint@8.52.0: - resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - ufo@1.6.2: - resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - unimport@3.14.6: - resolution: {integrity: sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unplugin-auto-import@0.17.8: - resolution: {integrity: sha512-CHryj6HzJ+n4ASjzwHruD8arhbdl+UXvhuAIlHDs15Y/IMecG3wrf7FVg4pVH/DIysbq/n0phIjNHAjl7TG7Iw==} - engines: {node: '>=14'} - peerDependencies: - '@nuxt/kit': ^3.2.2 - '@vueuse/core': '*' - peerDependenciesMeta: - '@nuxt/kit': - optional: true - '@vueuse/core': - optional: true - - unplugin-vue-components@0.26.0: - resolution: {integrity: sha512-s7IdPDlnOvPamjunVxw8kNgKNK8A5KM1YpK5j/p97jEKTjlPNrA0nZBiSfAKKlK1gWZuyWXlKL5dk3EDw874LQ==} - engines: {node: '>=14'} - peerDependencies: - '@babel/parser': ^7.15.8 - '@nuxt/kit': ^3.2.2 - vue: 2 || 3 - peerDependenciesMeta: - '@babel/parser': - optional: true - '@nuxt/kit': - optional: true - - unplugin@1.16.1: - resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} - engines: {node: '>=14.0.0'} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vite-plugin-compression@0.5.1: - resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} - peerDependencies: - vite: '>=2.0.0' - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vue-demi@0.13.11: - resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - - vue-demi@0.14.10: - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - - vue-echarts@6.7.3: - resolution: {integrity: sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==} - peerDependencies: - '@vue/composition-api': ^1.0.5 - '@vue/runtime-core': ^3.0.0 - echarts: ^5.4.1 - vue: ^2.6.12 || ^3.1.1 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - '@vue/runtime-core': - optional: true - - vue-eslint-parser@10.2.0: - resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - - vue-router@4.6.4: - resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} - peerDependencies: - vue: ^3.5.0 - - vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - - vue-tsc@1.8.27: - resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} - hasBin: true - peerDependencies: - typescript: '*' - - vue@3.5.26: - resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zrender@5.6.1: - resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} - -snapshots: - - '@antfu/utils@0.7.10': {} - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/parser@7.28.5': - dependencies: - '@babel/types': 7.28.5 - - '@babel/types@7.28.5': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@ctrl/tinycolor@3.6.1': {} - - '@element-plus/icons-vue@2.3.2(vue@3.5.26(typescript@5.9.3))': - dependencies: - vue: 3.5.26(typescript@5.9.3) - - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': - dependencies: - eslint: 9.39.2 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.1': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.3': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@floating-ui/core@1.7.3': - dependencies: - '@floating-ui/utils': 0.2.10 - - '@floating-ui/dom@1.7.4': - dependencies: - '@floating-ui/core': 1.7.3 - '@floating-ui/utils': 0.2.10 - - '@floating-ui/utils@0.2.10': {} - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@parcel/watcher-android-arm64@2.5.4': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.4': - optional: true - - '@parcel/watcher-darwin-x64@2.5.4': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.4': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.4': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.4': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.4': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.4': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.4': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.4': - optional: true - - '@parcel/watcher-win32-arm64@2.5.4': - optional: true - - '@parcel/watcher-win32-ia32@2.5.4': - optional: true - - '@parcel/watcher-win32-x64@2.5.4': - optional: true - - '@parcel/watcher@2.5.4': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.4 - '@parcel/watcher-darwin-arm64': 2.5.4 - '@parcel/watcher-darwin-x64': 2.5.4 - '@parcel/watcher-freebsd-x64': 2.5.4 - '@parcel/watcher-linux-arm-glibc': 2.5.4 - '@parcel/watcher-linux-arm-musl': 2.5.4 - '@parcel/watcher-linux-arm64-glibc': 2.5.4 - '@parcel/watcher-linux-arm64-musl': 2.5.4 - '@parcel/watcher-linux-x64-glibc': 2.5.4 - '@parcel/watcher-linux-x64-musl': 2.5.4 - '@parcel/watcher-win32-arm64': 2.5.4 - '@parcel/watcher-win32-ia32': 2.5.4 - '@parcel/watcher-win32-x64': 2.5.4 - optional: true - - '@pkgr/core@0.2.9': {} - - '@rollup/pluginutils@5.3.0(rollup@4.55.1)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.55.1 - - '@rollup/rollup-android-arm-eabi@4.55.1': - optional: true - - '@rollup/rollup-android-arm64@4.55.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.55.1': - optional: true - - '@rollup/rollup-darwin-x64@4.55.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.55.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.55.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.55.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.55.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.55.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.55.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.55.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.55.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.55.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.55.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.55.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.55.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.55.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.55.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.55.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.55.1': - optional: true - - '@sentry-internal/feedback@7.120.4': - dependencies: - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry-internal/replay-canvas@7.120.4': - dependencies: - '@sentry/core': 7.120.4 - '@sentry/replay': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry-internal/tracing@7.120.4': - dependencies: - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry/browser@7.120.4': - dependencies: - '@sentry-internal/feedback': 7.120.4 - '@sentry-internal/replay-canvas': 7.120.4 - '@sentry-internal/tracing': 7.120.4 - '@sentry/core': 7.120.4 - '@sentry/integrations': 7.120.4 - '@sentry/replay': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry/core@7.120.4': - dependencies: - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry/integrations@7.120.4': - dependencies: - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - localforage: 1.10.0 - - '@sentry/replay@7.120.4': - dependencies: - '@sentry-internal/tracing': 7.120.4 - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - - '@sentry/types@7.120.4': {} - - '@sentry/utils@7.120.4': - dependencies: - '@sentry/types': 7.120.4 - - '@sentry/vue@7.120.4(vue@3.5.26(typescript@5.9.3))': - dependencies: - '@sentry/browser': 7.120.4 - '@sentry/core': 7.120.4 - '@sentry/types': 7.120.4 - '@sentry/utils': 7.120.4 - vue: 3.5.26(typescript@5.9.3) - - '@sxzz/popperjs-es@2.11.7': {} - - '@tanstack/match-sorter-utils@8.19.4': - dependencies: - remove-accents: 0.5.0 - - '@tanstack/query-core@5.90.16': {} - - '@tanstack/vue-query@5.92.5(vue@3.5.26(typescript@5.9.3))': - dependencies: - '@tanstack/match-sorter-utils': 8.19.4 - '@tanstack/query-core': 5.90.16 - '@vue/devtools-api': 6.6.4 - vue: 3.5.26(typescript@5.9.3) - vue-demi: 0.14.10(vue@3.5.26(typescript@5.9.3)) - - '@types/estree@1.0.8': {} - - '@types/json-schema@7.0.15': {} - - '@types/lodash-es@4.17.12': - dependencies: - '@types/lodash': 4.17.23 - - '@types/lodash@4.17.23': {} - - '@types/node@20.19.28': - dependencies: - undici-types: 6.21.0 - - '@types/web-bluetooth@0.0.20': {} - - '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/type-utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.52.0 - eslint: 9.39.2 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.52.0 - debug: 4.4.3 - eslint: 9.39.2 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) - '@typescript-eslint/types': 8.52.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.52.0': - dependencies: - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/visitor-keys': 8.52.0 - - '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/type-utils@8.52.0(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.39.2 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.52.0': {} - - '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/visitor-keys': 8.52.0 - debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.3 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.52.0(eslint@9.39.2)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) - '@typescript-eslint/scope-manager': 8.52.0 - '@typescript-eslint/types': 8.52.0 - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - eslint: 9.39.2 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.52.0': - dependencies: - '@typescript-eslint/types': 8.52.0 - eslint-visitor-keys: 4.2.1 - - '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.28)(sass@1.97.2))(vue@3.5.26(typescript@5.9.3))': - dependencies: - vite: 5.4.21(@types/node@20.19.28)(sass@1.97.2) - vue: 3.5.26(typescript@5.9.3) - - '@volar/language-core@1.11.1': - dependencies: - '@volar/source-map': 1.11.1 - - '@volar/source-map@1.11.1': - dependencies: - muggle-string: 0.3.1 - - '@volar/typescript@1.11.1': - dependencies: - '@volar/language-core': 1.11.1 - path-browserify: 1.0.1 - - '@vue/compiler-core@3.5.26': - dependencies: - '@babel/parser': 7.28.5 - '@vue/shared': 3.5.26 - entities: 7.0.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-dom@3.5.26': - dependencies: - '@vue/compiler-core': 3.5.26 - '@vue/shared': 3.5.26 - - '@vue/compiler-sfc@3.5.26': - dependencies: - '@babel/parser': 7.28.5 - '@vue/compiler-core': 3.5.26 - '@vue/compiler-dom': 3.5.26 - '@vue/compiler-ssr': 3.5.26 - '@vue/shared': 3.5.26 - estree-walker: 2.0.2 - magic-string: 0.30.21 - postcss: 8.5.6 - source-map-js: 1.2.1 - - '@vue/compiler-ssr@3.5.26': - dependencies: - '@vue/compiler-dom': 3.5.26 - '@vue/shared': 3.5.26 - - '@vue/devtools-api@6.6.4': {} - - '@vue/language-core@1.8.27(typescript@5.9.3)': - dependencies: - '@volar/language-core': 1.11.1 - '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.5.26 - '@vue/shared': 3.5.26 - computeds: 0.0.1 - minimatch: 9.0.5 - muggle-string: 0.3.1 - path-browserify: 1.0.1 - vue-template-compiler: 2.7.16 - optionalDependencies: - typescript: 5.9.3 - - '@vue/reactivity@3.5.26': - dependencies: - '@vue/shared': 3.5.26 - - '@vue/runtime-core@3.5.26': - dependencies: - '@vue/reactivity': 3.5.26 - '@vue/shared': 3.5.26 - - '@vue/runtime-dom@3.5.26': - dependencies: - '@vue/reactivity': 3.5.26 - '@vue/runtime-core': 3.5.26 - '@vue/shared': 3.5.26 - csstype: 3.2.3 - - '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': - dependencies: - '@vue/compiler-ssr': 3.5.26 - '@vue/shared': 3.5.26 - vue: 3.5.26(typescript@5.9.3) - - '@vue/shared@3.5.26': {} - - '@vueuse/core@10.11.1(vue@3.5.26(typescript@5.9.3))': - dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 10.11.1 - '@vueuse/shared': 10.11.1(vue@3.5.26(typescript@5.9.3)) - vue-demi: 0.14.10(vue@3.5.26(typescript@5.9.3)) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - - '@vueuse/metadata@10.11.1': {} - - '@vueuse/shared@10.11.1(vue@3.5.26(typescript@5.9.3))': - dependencies: - vue-demi: 0.14.10(vue@3.5.26(typescript@5.9.3)) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - argparse@2.0.1: {} - - async-validator@4.2.5: {} - - asynckit@0.4.0: {} - - axios@1.13.2: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - balanced-match@1.0.2: {} - - binary-extensions@2.3.0: {} - - boolbase@1.0.0: {} - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - callsites@3.1.0: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - computeds@0.0.1: {} - - concat-map@0.0.1: {} - - confbox@0.1.8: {} - - confbox@0.2.2: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - cssesc@3.0.0: {} - - csstype@3.2.3: {} - - dayjs@1.11.19: {} - - de-indent@1.0.2: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - define-lazy-prop@2.0.0: {} - - delayed-stream@1.0.0: {} - - detect-libc@2.1.2: - optional: true - - dexie@4.2.1: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - echarts@5.6.0: - dependencies: - tslib: 2.3.0 - zrender: 5.6.1 - - element-plus@2.13.1(vue@3.5.26(typescript@5.9.3)): - dependencies: - '@ctrl/tinycolor': 3.6.1 - '@element-plus/icons-vue': 2.3.2(vue@3.5.26(typescript@5.9.3)) - '@floating-ui/dom': 1.7.4 - '@popperjs/core': '@sxzz/popperjs-es@2.11.7' - '@types/lodash': 4.17.23 - '@types/lodash-es': 4.17.12 - '@vueuse/core': 10.11.1(vue@3.5.26(typescript@5.9.3)) - async-validator: 4.2.5 - dayjs: 1.11.19 - lodash: 4.17.21 - lodash-es: 4.17.22 - lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.22)(lodash@4.17.21) - memoize-one: 6.0.0 - normalize-wheel-es: 1.2.0 - vue: 3.5.26(typescript@5.9.3) - transitivePeerDependencies: - - '@vue/composition-api' - - emoji-regex@8.0.0: {} - - entities@7.0.0: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - escape-string-regexp@5.0.0: {} - - eslint-config-prettier@10.1.8(eslint@9.39.2): - dependencies: - eslint: 9.39.2 - - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.2))(eslint@9.39.2)(prettier@3.7.4): - dependencies: - eslint: 9.39.2 - prettier: 3.7.4 - prettier-linter-helpers: 1.0.1 - synckit: 0.11.11 - optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@9.39.2) - - eslint-plugin-vue@10.6.2(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(vue-eslint-parser@10.2.0(eslint@9.39.2)): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) - eslint: 9.39.2 - natural-compare: 1.4.0 - nth-check: 2.1.1 - postcss-selector-parser: 7.1.1 - semver: 7.7.3 - vue-eslint-parser: 10.2.0(eslint@9.39.2) - xml-name-validator: 4.0.0 - optionalDependencies: - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.2: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - esutils@2.0.3: {} - - exsolve@1.0.8: {} - - fast-deep-equal@3.1.3: {} - - fast-diff@1.3.0: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - - follow-redirects@1.15.11: {} - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.0 - universalify: 2.0.1 - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - globals@14.0.0: {} - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - has-flag@4.0.0: {} - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - he@1.2.0: {} - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - immediate@3.0.6: {} - - immutable@5.1.4: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-docker@2.2.1: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - - isexe@2.0.0: {} - - js-tokens@9.0.1: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - jsonfile@6.2.0: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lie@3.1.1: - dependencies: - immediate: 3.0.6 - - local-pkg@0.4.3: {} - - local-pkg@0.5.1: - dependencies: - mlly: 1.8.0 - pkg-types: 1.3.1 - - local-pkg@1.1.2: - dependencies: - mlly: 1.8.0 - pkg-types: 2.3.0 - quansync: 0.2.11 - - localforage@1.10.0: - dependencies: - lie: 3.1.1 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash-es@4.17.22: {} - - lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.22)(lodash@4.17.21): - dependencies: - '@types/lodash-es': 4.17.12 - lodash: 4.17.21 - lodash-es: 4.17.22 - - lodash.merge@4.6.2: {} - - lodash@4.17.21: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - math-intrinsics@1.1.0: {} - - memoize-one@6.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - mitt@3.0.1: {} - - mlly@1.8.0: - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.2 - - ms@2.1.3: {} - - muggle-string@0.3.1: {} - - nanoid@3.3.11: {} - - nanoid@5.1.6: {} - - natural-compare@1.4.0: {} - - node-addon-api@7.1.1: - optional: true - - normalize-path@3.0.0: {} - - normalize-wheel-es@1.2.0: {} - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-browserify@1.0.1: {} - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - pinia-plugin-persistedstate@3.2.3(pinia@2.3.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))): - dependencies: - pinia: 2.3.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) - - pinia@2.3.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): - dependencies: - '@vue/devtools-api': 6.6.4 - vue: 3.5.26(typescript@5.9.3) - vue-demi: 0.14.10(vue@3.5.26(typescript@5.9.3)) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - '@vue/composition-api' - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - - pkg-types@2.3.0: - dependencies: - confbox: 0.2.2 - exsolve: 1.0.8 - pathe: 2.0.3 - - postcss-selector-parser@7.1.1: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier-linter-helpers@1.0.1: - dependencies: - fast-diff: 1.3.0 - - prettier@3.7.4: {} - - proxy-from-env@1.1.0: {} - - punycode@2.3.1: {} - - quansync@0.2.11: {} - - queue-microtask@1.2.3: {} - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - readdirp@4.1.2: {} - - remove-accents@0.5.0: {} - - require-directory@2.1.1: {} - - resize-detector@0.3.0: {} - - resolve-from@4.0.0: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rollup-plugin-visualizer@5.14.0(rollup@4.55.1): - dependencies: - open: 8.4.2 - picomatch: 4.0.3 - source-map: 0.7.6 - yargs: 17.7.2 - optionalDependencies: - rollup: 4.55.1 - - rollup@4.55.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.55.1 - '@rollup/rollup-android-arm64': 4.55.1 - '@rollup/rollup-darwin-arm64': 4.55.1 - '@rollup/rollup-darwin-x64': 4.55.1 - '@rollup/rollup-freebsd-arm64': 4.55.1 - '@rollup/rollup-freebsd-x64': 4.55.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 - '@rollup/rollup-linux-arm-musleabihf': 4.55.1 - '@rollup/rollup-linux-arm64-gnu': 4.55.1 - '@rollup/rollup-linux-arm64-musl': 4.55.1 - '@rollup/rollup-linux-loong64-gnu': 4.55.1 - '@rollup/rollup-linux-loong64-musl': 4.55.1 - '@rollup/rollup-linux-ppc64-gnu': 4.55.1 - '@rollup/rollup-linux-ppc64-musl': 4.55.1 - '@rollup/rollup-linux-riscv64-gnu': 4.55.1 - '@rollup/rollup-linux-riscv64-musl': 4.55.1 - '@rollup/rollup-linux-s390x-gnu': 4.55.1 - '@rollup/rollup-linux-x64-gnu': 4.55.1 - '@rollup/rollup-linux-x64-musl': 4.55.1 - '@rollup/rollup-openbsd-x64': 4.55.1 - '@rollup/rollup-openharmony-arm64': 4.55.1 - '@rollup/rollup-win32-arm64-msvc': 4.55.1 - '@rollup/rollup-win32-ia32-msvc': 4.55.1 - '@rollup/rollup-win32-x64-gnu': 4.55.1 - '@rollup/rollup-win32-x64-msvc': 4.55.1 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - sass@1.97.2: - dependencies: - chokidar: 4.0.3 - immutable: 5.1.4 - source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.5.4 - - scule@1.3.0: {} - - semver@7.7.3: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - source-map-js@1.2.1: {} - - source-map@0.7.6: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-json-comments@3.1.1: {} - - strip-literal@2.1.1: - dependencies: - js-tokens: 9.0.1 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - synckit@0.11.11: - dependencies: - '@pkgr/core': 0.2.9 - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - ts-api-utils@2.4.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - - tslib@2.3.0: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript-eslint@8.52.0(eslint@9.39.2)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/parser': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - eslint: 9.39.2 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - typescript@5.9.3: {} - - ufo@1.6.2: {} - - undici-types@6.21.0: {} - - unimport@3.14.6(rollup@4.55.1): - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - acorn: 8.15.0 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - fast-glob: 3.3.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.0 - pathe: 2.0.3 - picomatch: 4.0.3 - pkg-types: 1.3.1 - scule: 1.3.0 - strip-literal: 2.1.1 - unplugin: 1.16.1 - transitivePeerDependencies: - - rollup - - universalify@2.0.1: {} - - unplugin-auto-import@0.17.8(@vueuse/core@10.11.1(vue@3.5.26(typescript@5.9.3)))(rollup@4.55.1): - dependencies: - '@antfu/utils': 0.7.10 - '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - fast-glob: 3.3.3 - local-pkg: 0.5.1 - magic-string: 0.30.21 - minimatch: 9.0.5 - unimport: 3.14.6(rollup@4.55.1) - unplugin: 1.16.1 - optionalDependencies: - '@vueuse/core': 10.11.1(vue@3.5.26(typescript@5.9.3)) - transitivePeerDependencies: - - rollup - - unplugin-vue-components@0.26.0(@babel/parser@7.28.5)(rollup@4.55.1)(vue@3.5.26(typescript@5.9.3)): - dependencies: - '@antfu/utils': 0.7.10 - '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - chokidar: 3.6.0 - debug: 4.4.3 - fast-glob: 3.3.3 - local-pkg: 0.4.3 - magic-string: 0.30.21 - minimatch: 9.0.5 - resolve: 1.22.11 - unplugin: 1.16.1 - vue: 3.5.26(typescript@5.9.3) - optionalDependencies: - '@babel/parser': 7.28.5 - transitivePeerDependencies: - - rollup - - supports-color - - unplugin@1.16.1: - dependencies: - acorn: 8.15.0 - webpack-virtual-modules: 0.6.2 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - util-deprecate@1.0.2: {} - - vite-plugin-compression@0.5.1(vite@5.4.21(@types/node@20.19.28)(sass@1.97.2)): - dependencies: - chalk: 4.1.2 - debug: 4.4.3 - fs-extra: 10.1.0 - vite: 5.4.21(@types/node@20.19.28)(sass@1.97.2) - transitivePeerDependencies: - - supports-color - - vite@5.4.21(@types/node@20.19.28)(sass@1.97.2): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.55.1 - optionalDependencies: - '@types/node': 20.19.28 - fsevents: 2.3.3 - sass: 1.97.2 - - vue-demi@0.13.11(vue@3.5.26(typescript@5.9.3)): - dependencies: - vue: 3.5.26(typescript@5.9.3) - - vue-demi@0.14.10(vue@3.5.26(typescript@5.9.3)): - dependencies: - vue: 3.5.26(typescript@5.9.3) - - vue-echarts@6.7.3(@vue/runtime-core@3.5.26)(echarts@5.6.0)(vue@3.5.26(typescript@5.9.3)): - dependencies: - echarts: 5.6.0 - resize-detector: 0.3.0 - vue: 3.5.26(typescript@5.9.3) - vue-demi: 0.13.11(vue@3.5.26(typescript@5.9.3)) - optionalDependencies: - '@vue/runtime-core': 3.5.26 - - vue-eslint-parser@10.2.0(eslint@9.39.2): - dependencies: - debug: 4.4.3 - eslint: 9.39.2 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)): - dependencies: - '@vue/devtools-api': 6.6.4 - vue: 3.5.26(typescript@5.9.3) - - vue-template-compiler@2.7.16: - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - - vue-tsc@1.8.27(typescript@5.9.3): - dependencies: - '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.27(typescript@5.9.3) - semver: 7.7.3 - typescript: 5.9.3 - - vue@3.5.26(typescript@5.9.3): - dependencies: - '@vue/compiler-dom': 3.5.26 - '@vue/compiler-sfc': 3.5.26 - '@vue/runtime-dom': 3.5.26 - '@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.9.3)) - '@vue/shared': 3.5.26 - optionalDependencies: - typescript: 5.9.3 - - webpack-virtual-modules@0.6.2: {} - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - xml-name-validator@4.0.0: {} - - y18n@5.0.8: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} - - zrender@5.6.1: - dependencies: - tslib: 2.3.0 diff --git a/TouchVueThree/public/.gitkeep b/TouchVueThree/public/.gitkeep deleted file mode 100644 index 6f37e73..0000000 --- a/TouchVueThree/public/.gitkeep +++ /dev/null @@ -1,3 +0,0 @@ -# Public 目录 -# 此目录用于存放静态资源文件,如 favicon.ico、图片等 -# 目录中的文件会直接被复制到构建输出的根目录 diff --git a/TouchVueThree/src/App.vue b/TouchVueThree/src/App.vue deleted file mode 100644 index eb3410a..0000000 --- a/TouchVueThree/src/App.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - diff --git a/TouchVueThree/src/api/index.ts b/TouchVueThree/src/api/index.ts deleted file mode 100644 index ff01fb6..0000000 --- a/TouchVueThree/src/api/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * API 统一导出 - */ - -// 导出 request 实例 -export { default as request } from './request' -export { default as request2 } from './request2' - -// 导出所有 API 模块 -export * from './modules/user' -export * from './modules/wechat' -export * from './modules/ai' -export * from './modules/content' -export * from './modules/common' - -// 类型导出 -export type * from './modules/user' -export type * from './modules/wechat' -export type * from './modules/ai' -export type * from './modules/content' -export type * from './modules/common' diff --git a/TouchVueThree/src/api/modules/ai.ts b/TouchVueThree/src/api/modules/ai.ts deleted file mode 100644 index deb715d..0000000 --- a/TouchVueThree/src/api/modules/ai.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * AI 相关 API - */ - -import axios from 'axios' -import request from '../request' -import { useUserStore } from '@/stores' - -/** - * AI 对话接口 - */ -export interface AiChatParams { - friendId: number - wechatAccountId: number - [property: string]: any -} - -export function aiChat(params: AiChatParams) { - return request('/v1/kefu/ai/chat', params, 'POST') -} - -/** - * 数据处理接口(Socket消息传入数据中心) - */ -export interface DataProcessingParams { - /** - * 是否置顶("0" 或 "1") - */ - isTop?: string - /** - * 类型固定值(如 "CmdPinToTop") - */ - type?: string - /** - * 公共参数 - */ - wechatAccountId?: string | number - wechatChatroomId?: string | number - wechatFriendId?: string | number - /** - * 其他参数 - */ - chatroomMessage?: any[] - friendMessage?: any[] - [property: string]: any -} - -export function dataProcessing(params: DataProcessingParams) { - return request('/v1/kefu/dataProcessing', params, 'POST') -} - -/** - * 获取消息状态 - */ -export function asyncMessageStatus(params: { - messageId: number - wechatFriendId?: number - wechatChatroomId?: number - wechatAccountId: number -}) { - return request('/v1/kefu/message/getMessageStatus', params, 'GET') -} - -/** - * AI文本生成接口(群公告等) - * @param {string} content - 提示词内容 - * @returns {Promise} - AI生成的文本内容 - */ -export async function generateAiText( - content: string, - params?: { wechatAccountId: number | string; groupId: number | string }, -): Promise { - try { - // 获取用户token - const userStore = useUserStore() - const token = userStore.token - - // 获取AI接口基础URL - const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || '/api' - const fullUrl = `${apiBaseUrl}/v1/kefu/wechatChatroom/aiAnnouncement` - - // 发送POST请求 - const response = await axios.post( - fullUrl, - { - wechatAccountId: params?.wechatAccountId, - groupId: params?.groupId, - content, - }, - { - headers: { - 'Content-Type': 'application/json', - Authorization: token ? `Bearer ${token}` : undefined, - }, - timeout: 30000, // AI生成可能需要更长时间 - }, - ) - - // 新接口返回:{ code: 200, msg: 'success', data: '...公告内容...' } - if (response?.data?.code === 200) { - return response?.data?.data || '' - } - return '' - } catch (error: any) { - const errorMessage = error.response?.data?.message || error.message || 'AI生成失败' - throw new Error(errorMessage) - } -} diff --git a/TouchVueThree/src/api/modules/common.ts b/TouchVueThree/src/api/modules/common.ts deleted file mode 100644 index 47f40f7..0000000 --- a/TouchVueThree/src/api/modules/common.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 通用 API(文件上传、流量池等) - */ - -import axios from 'axios' -import { useUserStore } from '@/stores' - -/** - * 通用文件上传方法(支持图片、文件) - * @param {File} file - 要上传的文件对象 - * @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址 - * @returns {Promise} - 上传成功后返回文件url - */ -export async function uploadFile( - file: File, - uploadUrl: string = '/v1/attachment/upload', -): Promise { - try { - // 创建 FormData 对象用于文件上传 - const formData = new FormData() - formData.append('file', file) - - // 获取用户token - const userStore = useUserStore() - const token = userStore.token - - const fullUrl = `${import.meta.env.VITE_API_BASE_URL || '/api'}${uploadUrl}` - - // 直接使用 axios 上传文件 - const response = await axios.post(fullUrl, formData, { - headers: { - Authorization: token ? `Bearer ${token}` : undefined, - }, - timeout: 20000, - }) - return response?.data?.data?.url || '' - } catch (e: any) { - const errorMessage = e.response?.data?.message || e.message || '文件上传失败' - throw new Error(errorMessage) - } -} - -/** - * 获取流量池列表 - */ -export function getTrafficPoolList() { - return axios.get('/v1/traffic/pool/getPackage', { - params: { - page: 1, - limit: 9999, - }, - }) -} diff --git a/TouchVueThree/src/api/modules/content.ts b/TouchVueThree/src/api/modules/content.ts deleted file mode 100644 index 3647a9f..0000000 --- a/TouchVueThree/src/api/modules/content.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * 内容管理相关 API(素材、违禁词、关键词回复) - */ - -import request from '../request' - -// ==================== 素材管理 ==================== - -export interface MaterialListParams { - keyword?: string - limit?: string - page?: string -} - -export interface ContentItem { - type: 'text' | 'image' | 'video' | 'file' | 'audio' | 'link' - data: string | LinkData -} - -export interface LinkData { - title: string - url: string - cover: string -} - -export interface MaterialAddRequest { - title: string - cover?: string - status: number - content: ContentItem[] -} - -export interface MaterialUpdateRequest extends MaterialAddRequest { - id?: string -} - -export interface MaterialSetStatusRequest { - id: string -} - -/** - * 获取素材列表 - */ -export function getMaterialList(params: MaterialListParams) { - return request('/v1/kefu/content/material/list', params, 'GET') -} - -/** - * 添加素材 - */ -export function addMaterial(data: MaterialAddRequest) { - return request('/v1/kefu/content/material/add', data, 'POST') -} - -/** - * 获取素材详情 - */ -export function getMaterialDetails(id: string) { - return request('/v1/kefu/content/material/details', { id }, 'GET') -} - -/** - * 删除素材 - */ -export function deleteMaterial(id: string) { - return request('/v1/kefu/content/material/del', { id }, 'DELETE') -} - -/** - * 更新素材 - */ -export function updateMaterial(data: MaterialUpdateRequest) { - return request('/v1/kefu/content/material/update', data, 'POST') -} - -/** - * 修改素材状态 - */ -export function setMaterialStatus(data: MaterialSetStatusRequest) { - return request('/v1/kefu/content/material/setStatus', data, 'POST') -} - -// ==================== 违禁词管理 ==================== - -export interface SensitiveWordListParams { - keyword?: string - limit?: string - page?: string -} - -export interface SensitiveWordAddRequest { - content: string - keywords: string - /** - * 操作 0不操作 1替换 2删除 3警告 4禁止发送 - */ - operation: string - status: string - title: string -} - -export interface SensitiveWordUpdateRequest extends SensitiveWordAddRequest { - id?: string -} - -export interface SensitiveWordSetStatusRequest { - id: string -} - -/** - * 获取违禁词列表 - */ -export function getSensitiveWordList(params: SensitiveWordListParams) { - return request('/v1/kefu/content/sensitiveWord/list', params, 'GET') -} - -/** - * 添加违禁词 - */ -export function addSensitiveWord(data: SensitiveWordAddRequest) { - return request('/v1/kefu/content/sensitiveWord/add', data, 'POST') -} - -/** - * 获取违禁词详情 - */ -export function getSensitiveWordDetails(id: string) { - return request('/v1/kefu/content/sensitiveWord/details', { id }, 'GET') -} - -/** - * 删除违禁词 - */ -export function deleteSensitiveWord(id: string) { - return request('/v1/kefu/content/sensitiveWord/del', { id }, 'DELETE') -} - -/** - * 更新违禁词 - */ -export function updateSensitiveWord(data: SensitiveWordUpdateRequest) { - return request('/v1/kefu/content/sensitiveWord/update', data, 'POST') -} - -/** - * 修改违禁词状态 - */ -export function setSensitiveWordStatus(data: SensitiveWordSetStatusRequest) { - return request('/v1/kefu/content/sensitiveWord/setStatus', data, 'GET') -} - -// ==================== 关键词回复管理 ==================== - -export interface KeywordListParams { - keyword?: string - limit?: string - page?: string -} - -export interface KeywordAddRequest { - title: string - keywords: string - content: string - type: number // 匹配类型:模糊匹配、精确匹配 - level: number // 优先级 - replyType: number // 回复类型:文本回复、模板回复 - status: string - metailGroups: any[] -} - -export interface KeywordUpdateRequest extends KeywordAddRequest { - id?: number -} - -export interface KeywordSetStatusRequest { - id: number -} - -/** - * 获取关键词回复列表 - */ -export function getKeywordList(params: KeywordListParams) { - return request('/v1/kefu/content/keywords/list', params, 'GET') -} - -/** - * 添加关键词回复 - */ -export function addKeyword(data: KeywordAddRequest) { - return request('/v1/kefu/content/keywords/add', data, 'POST') -} - -/** - * 获取关键词回复详情 - */ -export function getKeywordDetails(id: number) { - return request('/v1/kefu/content/keywords/details', { id }, 'GET') -} - -/** - * 删除关键词回复 - */ -export function deleteKeyword(id: number) { - return request('/v1/kefu/content/keywords/del', { id }, 'DELETE') -} - -/** - * 更新关键词回复 - */ -export function updateKeyword(data: KeywordUpdateRequest) { - return request('/v1/kefu/content/keywords/update', data, 'POST') -} - -/** - * 修改关键词回复状态 - */ -export function setKeywordStatus(data: KeywordSetStatusRequest) { - return request('/v1/kefu/content/keywords/setStatus', data, 'POST') -} diff --git a/TouchVueThree/src/api/modules/user.ts b/TouchVueThree/src/api/modules/user.ts deleted file mode 100644 index 30d9bde..0000000 --- a/TouchVueThree/src/api/modules/user.ts +++ /dev/null @@ -1,92 +0,0 @@ -import request from '../request' -import request2 from '../request2' - -// ==================== 登录相关接口 ==================== - -/** - * 密码登录 - */ -export function loginWithPassword(params: { - account: string - password: string - verifyCode?: string - verifySessionId?: string - typeId?: number -}) { - return request('/v1/kefu/login', params, 'POST') -} - -/** - * 验证码登录 - */ -export function loginWithCode(params: { - account: string - verificationCode: string - verifySessionId?: string - typeId?: number -}) { - return request('/v1/auth/login-code', params, 'POST') -} - -/** - * 发送验证码 - */ -export function sendVerificationCode(params: { mobile: string; type: string }) { - return request('/v1/auth/code', params, 'POST') -} - -/** - * 退出登录 - */ -export function logout() { - return request('/v1/auth/logout', {}, 'POST') -} - -/** - * 获取用户信息 - */ -export function getUserInfo() { - return request('/v1/auth/user-info', {}, 'GET') -} - -/** - * 获取验证码图片 - */ -export function getVerifyCode() { - return request<{ - verifyCodeImage: string - verifySessionId: string - }>('/v1/api/user/verify-code', {}, 'GET') -} - -// ==================== 触客宝接口 ==================== - -/** - * 触客宝登录(使用 token) - */ -export function loginWithToken(params: { - grant_type: string - client_id: string - client_secret: string - username: string - password: string -}) { - return request2( - '/token', - params, - 'POST', - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }, - 1000 - ) -} - -/** - * 获取触客宝用户信息 - */ -export function getChuKeBaoUserInfo() { - return request2('/api/account/self', {}, 'GET') -} diff --git a/TouchVueThree/src/api/modules/wechat.ts b/TouchVueThree/src/api/modules/wechat.ts deleted file mode 100644 index 5abaa9a..0000000 --- a/TouchVueThree/src/api/modules/wechat.ts +++ /dev/null @@ -1,674 +0,0 @@ -/** - * 微信相关 API(从旧项目完整迁移) - */ - -import request from '../request' -import request2 from '../request2' - -// ==================== 客服账号管理 ==================== - -/** - * 获取客服列表(微信账号列表) - */ -export function getCustomerList() { - return request('/v1/kefu/customerService/list', {}, 'GET') -} - -/** - * 获取控制终端列表 - */ -export function getControlTerminalList(params: any) { - return request2('/api/wechataccount', params, 'GET') -} - -// ==================== 好友管理 ==================== - -/** - * 获取联系人列表(好友列表)- 旧接口兼容 - */ -export function getContactListOld(params: { prevId: number; count: number }) { - return request2('/api/wechatFriend/list', params, 'GET') -} - -/** - * 获取好友列表(分页,支持分组和搜索) - * ⭐ 使用 GET 方法,与旧项目保持一致 - */ -export function getContactList(params: { - wechatAccountId?: number - groupId?: number - keyword?: string - page?: number - limit?: number - pageNum?: number - pageSize?: number -}) { - // 统一参数格式:page/limit 或 pageNum/pageSize - const requestParams: any = {} - if (params.page !== undefined) requestParams.page = params.page - if (params.limit !== undefined) requestParams.limit = params.limit - if (params.pageNum !== undefined) requestParams.page = params.pageNum - if (params.pageSize !== undefined) requestParams.limit = params.pageSize - if (params.wechatAccountId !== undefined) - requestParams.wechatAccountId = params.wechatAccountId - if (params.groupId !== undefined) requestParams.groupId = params.groupId - if (params.keyword !== undefined) requestParams.keyword = params.keyword - - return request('/v1/kefu/wechatFriend/list', requestParams, 'GET') -} - -/** - * 获取好友列表(别名,兼容旧代码) - */ -export function getFriendList(params: { - wechatAccountId?: number - groupId?: number - keyword?: string - pageNum?: number - pageSize?: number -}) { - return getContactList(params) -} - -/** - * 获取好友详情 - * ⭐ 用于收到陌生好友消息时获取完整信息 - * 兼容旧项目接口:/v1/kefu/wechatFriend/detail?id=xxx - */ -export function getFriendDetail(params: { friendId?: number; id?: number }) { - const friendId = params.friendId || params.id - return request('/v1/kefu/wechatFriend/detail', { id: friendId }, 'GET') -} - -/** - * 清除好友未读数 - */ -export function clearFriendUnread(params: any) { - return request2('/api/WechatFriend/clearUnreadCount', params, 'PUT') -} - -/** - * 更新好友配置 - * 用于删除会话(设置 chat: false)等操作 - * ⭐ 使用 request2(与旧项目保持一致) - */ -export function updateFriendConfig(params: { - id: number - config?: { chat?: boolean } - [key: string]: any -}) { - return request2('/api/WechatFriend/updateConfig', params, 'PUT') -} - -/** - * 更新好友信息(包含备注等) - * ⭐ 使用 /v1/kefu/wechatFriend/updateInfo,与旧项目保持一致 - */ -export function updateFriendInfo(params: { - id: number - conRemark?: string - phone?: string - company?: string - name?: string - position?: string - email?: string - address?: string - qq?: string - remark?: string - [key: string]: any -}) { - return request('/v1/kefu/wechatFriend/updateInfo', params, 'POST') -} - -// ==================== 群聊管理 ==================== - -/** - * 获取群列表(旧接口,兼容) - */ -export function getGroupListOld(params: { prevId: number; count: number }) { - return request2('/api/wechatChatroom/listExcludeMembersByPage', params, 'GET') -} - -/** - * 获取群列表(分页,支持分组和搜索) - * ⭐ 使用 GET 方法,与旧项目保持一致 - */ -export function getGroupList(params: { - wechatAccountId?: number - groupId?: number - keyword?: string - page?: number - limit?: number - pageNum?: number - pageSize?: number -}) { - // 统一参数格式:page/limit 或 pageNum/pageSize - const requestParams: any = {} - if (params.page !== undefined) requestParams.page = params.page - if (params.limit !== undefined) requestParams.limit = params.limit - if (params.pageNum !== undefined) requestParams.page = params.pageNum - if (params.pageSize !== undefined) requestParams.limit = params.pageSize - if (params.wechatAccountId !== undefined) - requestParams.wechatAccountId = params.wechatAccountId - if (params.groupId !== undefined) requestParams.groupId = params.groupId - if (params.keyword !== undefined) requestParams.keyword = params.keyword - - return request('/v1/kefu/wechatChatroom/list', requestParams, 'GET') -} - -/** - * 获取群聊列表 - */ -export function getWechatGroupList(params: any) { - return request2('/api/WechatGroup/list', params, 'GET') -} - -/** - * 获取群聊详情 - * ⭐ 用于收到陌生群聊消息时获取完整信息 - */ -export function getGroupDetail(params: { groupId: number }) { - return request(`/v1/kefu/wechatChatroom/detail/${params.groupId}`, {}, 'GET') -} - -/** - * 获取群成员列表 - */ -export function getGroupMembers(params: { id: number }) { - return request2( - '/api/WechatChatroom/listMembersByWechatChatroomId', - params, - 'GET' - ) -} - -/** - * 添加群组成员 - */ -export function addGroupMembers(groupId: string, memberIds: string[]) { - return request2(`/v1/groups/${groupId}/members`, { memberIds }, 'POST') -} - -/** - * 移除群组成员 - */ -export function removeGroupMembers(groupId: string, memberIds: string[]) { - return request2(`/v1/groups/${groupId}/members`, { memberIds }, 'DELETE') -} - -// ==================== 群组分组管理 ==================== - -/** - * 添加分组 - */ -export function addGroup(data: { - groupName: string - groupMemo: string - groupType: number - sort: number -}) { - return request('/v1/kefu/wechatGroup/add', data, 'POST') -} - -/** - * 更新分组 - * ⭐ 使用 update 路径,与旧项目保持一致 - */ -export function updateGroup(data: { - id: number - groupName: string - groupMemo: string - groupType: number - sort: number -}) { - return request('/v1/kefu/wechatGroup/update', data, 'POST') -} - -/** - * 删除分组 - * ⭐ 使用查询参数,与旧项目 WechatFriends/api.ts 保持一致 - */ -export function deleteGroup(id: number) { - return request(`/v1/kefu/wechatGroup/delete?id=${id}`, null, 'DELETE') -} - -/** - * 获取分组列表(支持账号筛选) - * ⭐ 使用 GET 方法,与旧项目保持一致 - */ -export function getContactGroups(accountId?: number) { - const params: any = {} - if (accountId !== undefined && accountId !== 0) { - params.wechatAccountId = accountId - } - // 即使 accountId 为 0 也要传参数,由后端决定含义 - return request('/v1/kefu/wechatGroup/list', params, 'GET') -} - -/** - * 获取分组列表(别名,与旧项目保持一致) - */ -export function getLabelsListByGroup(params?: { wechatAccountId?: number }) { - return request('/v1/kefu/wechatGroup/list', params || {}, 'GET') -} - -/** - * 移动分组 - */ -export function moveGroup(data: { - type: 'friend' | 'chatroom' - groupId: number - id: number -}) { - return request('/v1/kefu/wechatGroup/move', data, 'POST') -} - -// ==================== 消息管理 ==================== - -/** - * 获取聊天消息(好友/群聊通用) - */ -export interface MessageParams { - From?: number | string - To?: number | string - page?: number - limit?: number - wechatChatroomId?: number | string - wechatFriendId?: number | string - wechatAccountId?: number | string - [property: string]: any -} - -export function getChatMessages(params: MessageParams) { - return request('/v1/kefu/message/details', params, 'GET', { debounce: false }) -} - -export function getChatroomMessages(params: MessageParams) { - return request('/v1/kefu/message/details', params, 'GET', { debounce: false }) -} - -/** - * 获取会话列表(消息列表) - */ -export function getSessionList(params: { page: number; limit: number }) { - return request('/v1/kefu/message/list', params, 'GET') -} - -/** - * 清除未读消息 - */ -export function clearUnreadCount(params: any) { - return request('/v1/kefu/message/readMessage', params, 'GET') -} - -/** - * 清除未读消息(别名) - */ -export function clearUnread(params: any) { - return request('/v1/kefu/message/readMessage', params, 'GET') -} - -/** - * 获取消息状态 - */ -export function asyncMessageStatus(params: { - messageId: number - wechatFriendId?: number - wechatChatroomId?: number - wechatAccountId: number -}) { - return request('/v1/kefu/message/getMessageStatus', params, 'GET') -} - -/** - * 获取消息状态(单个) - */ -export function getMessageStatus(messageId: string) { - return request2(`/v1/messages/${messageId}/status`, {}, 'GET') -} - -/** - * 标记消息为已读 - */ -export function markMessageAsRead(messageId: string) { - return request2(`/v1/messages/${messageId}/read`, {}, 'PUT') -} - -/** - * 标记聊天为已读 - */ -export function markChatAsRead(chatId: string) { - return request2(`/v1/chats/${chatId}/read`, {}, 'PUT') -} - -/** - * 转发消息 - */ -export function forwardMessage(messageId: string, targetChatIds: string[]) { - return request2('/v1/messages/forward', { messageId, targetChatIds }, 'POST') -} - -/** - * 撤回消息 - */ -export function recallMessage(messageId: string) { - return request2(`/v1/messages/${messageId}/recall`, {}, 'PUT') -} - -/** - * 发送消息 - */ -export function sendMessage(chatId: string, content: string, type: number = 1) { - return request2(`/v1/chats/${chatId}/messages`, { content, type }, 'POST') -} - -/** - * 发送文件消息 - */ -export function sendFileMessage(chatId: string, file: File, type: number) { - const formData = new FormData() - formData.append('file', file) - formData.append('type', String(type)) - return request2(`/v1/chats/${chatId}/messages/file`, formData, 'POST') -} - -// ==================== 聊天会话管理 ==================== - -/** - * 获取聊天历史 - */ -export function getChatHistory( - chatId: string, - page: number = 1, - pageSize: number = 50 -) { - return request2(`/v1/chats/${chatId}/messages`, { page, pageSize }, 'GET') -} - -/** - * 删除聊天会话 - */ -export function deleteChatSession(chatId: string) { - return request2(`/v1/chats/${chatId}`, {}, 'DELETE') -} - -/** - * 静音聊天会话 - */ -export function muteChatSession(chatId: string) { - return request2(`/v1/chats/${chatId}/mute`, {}, 'PUT') -} - -/** - * 取消静音聊天会话 - */ -export function unmuteChatSession(chatId: string) { - return request2(`/v1/chats/${chatId}/unmute`, {}, 'PUT') -} - -// ==================== 好友接待配置 ==================== - -/** - * 获取好友接待配置 - */ -export function getFriendInjectConfig(params: any) { - return request('/v1/kefu/ai/friend/get', params, 'GET') -} - -/** - * 设置好友接待配置(AI类型) - */ -export function setFriendInjectConfig(params: { - type: number - wechatAccountId: number - friendId: number -}) { - return request('/v1/kefu/ai/friend/set', params, 'POST') -} - -// ==================== 在线状态 ==================== - -/** - * 获取在线状态 - */ -export function getOnlineStatus(userId: string) { - return request2(`/v1/users/${userId}/status`, {}, 'GET') -} - -// ==================== 快捷回复 ==================== - -/** - * 获取快捷回复列表(分组树形结构) - * @param params { replyType: 0-公共 1-个人 2-部门 } - */ -export function getQuickReplyList(params: { replyType?: number }) { - return request('/v1/kefu/reply/list', params, 'GET') -} - -/** - * 添加快捷回复 - */ -export function addQuickReply(params: { - content?: string - groupId?: string - msgType?: string[] - sortIndex?: string - title?: string - replyType?: string[] - [key: string]: any -}) { - return request('/v1/kefu/reply/addReply', params, 'POST') -} - -/** - * 更新快捷回复 - */ -export function updateQuickReply(params: { - id: string - content?: string - groupId?: string - msgType?: string[] - sortIndex?: string - title?: string - [key: string]: any -}) { - return request('/v1/kefu/reply/updateReply', params, 'POST') -} - -/** - * 删除快捷回复 - */ -export function deleteQuickReply(params: { id: string }) { - return request('/v1/kefu/reply/deleteReply', params, 'DELETE') -} - -/** - * 添加快捷回复分组 - */ -export function addQuickReplyGroup(params: { - groupName?: string - parentId?: string - replyType?: string[] - sortIndex?: string - [key: string]: any -}) { - return request('/v1/kefu/reply/addGroup', params, 'POST') -} - -/** - * 更新快捷回复分组 - */ -export function updateQuickReplyGroup(params: { - id: string - groupName?: string - parentId?: string - replyType?: string[] - sortIndex?: string - [key: string]: any -}) { - return request('/v1/kefu/reply/updateGroup', params, 'POST') -} - -/** - * 删除快捷回复分组 - */ -export function deleteQuickReplyGroup(params: { id: string }) { - return request('/v1/kefu/reply/deleteGroup', params, 'DELETE') -} - -// ==================== 聊天设置 ==================== - -/** - * 获取聊天设置 - */ -export function getChatSettings() { - return request2('/v1/chat/settings', {}, 'GET') -} - -/** - * 更新聊天设置 - */ -export function updateChatSettings(settings: any) { - return request2('/v1/chat/settings', settings, 'PUT') -} - -// ==================== 表情包 ==================== - -/** - * 获取表情包列表 - */ -export function getEmojiList() { - return request2('/v1/emojis', {}, 'GET') -} - -// ==================== 朋友圈 ==================== - -/** - * 获取朋友圈列表 - */ -export function getMomentsList(params: { - wechatAccountId: number - pageNum?: number -}) { - return request('/v1/wechat/moments/list', params, 'POST') -} - -/** - * 点赞朋友圈 - */ -export function likeMoment(params: { - wechatAccountId: number - momentId: string -}) { - return request('/v1/wechat/moments/like', params, 'POST') -} - -/** - * 评论朋友圈 - */ -export function commentMoment(params: { - wechatAccountId: number - momentId: string - content: string -}) { - return request('/v1/wechat/moments/comment', params, 'POST') -} - -// ==================== 语音转文字 ==================== - -/** - * 语音转文字 - */ -export function voiceToText(params: { audioUrl: string }) { - return request('/v1/wechat/voice/to/text', params, 'POST') -} - -// ==================== 搜索 ==================== - -/** - * 搜索聊天记录 - */ -export function searchChatRecords(params: { - wechatAccountId: number - contactId: number - type: 'friend' | 'group' - keyword: string - pageNum?: number - pageSize?: number -}) { - return request('/v1/wechat/message/search', params, 'POST') -} - -// ==================== 跟进提醒相关接口 ==================== - -/** - * 获取跟进提醒列表 - */ -export function getFollowUpList(params: { - isProcess?: string - isRemind?: string - keyword?: string - level?: string - limit?: string - page?: string - friendId?: string -}) { - return request('/v1/kefu/followUp/list', params, 'GET') -} - -/** - * 添加跟进提醒 - */ -export function addFollowUp(params: { - description?: string - friendId: string - reminderTime?: string - title?: string - type?: string // 0其他 1电话回访 2发送消息 3安排会议 4发送邮件 -}) { - return request('/v1/kefu/followUp/add', params, 'POST') -} - -/** - * 处理跟进提醒 - */ -export function processFollowUp(params: { ids?: string }) { - return request('/v1/kefu/followUp/process', params, 'GET') -} - -// ==================== 待办事项相关接口 ==================== - -/** - * 获取待办事项列表 - */ -export function getTodoList(params: { - isProcess?: string - isRemind?: string - keyword?: string - level?: string - limit?: string - page?: string - friendId?: string -}) { - return request('/v1/kefu/todo/list', params, 'GET') -} - -/** - * 添加待办事项 - */ -export function addTodo(params: { - description?: string - friendId: string - level?: string // 0低优先级 1中优先级 2高优先级 3紧急 - reminderTime?: string - title?: string -}) { - return request('/v1/kefu/todo/add', params, 'POST') -} - -/** - * 处理待办事项 - */ -export function processTodo(params: { ids: string }) { - return request('/v1/kefu/todo/process', params, 'GET') -} diff --git a/TouchVueThree/src/api/request.ts b/TouchVueThree/src/api/request.ts deleted file mode 100644 index b0b9998..0000000 --- a/TouchVueThree/src/api/request.ts +++ /dev/null @@ -1,210 +0,0 @@ -import axios, { - AxiosInstance, - AxiosRequestConfig, - Method, - AxiosResponse, - AxiosError, -} from 'axios' -import { ElMessage } from 'element-plus' -import { useUserStore } from '@/stores/modules/user' -import { handleLoginExpired } from '@/utils/auth' - -// ==================== 类型定义 ==================== -interface ApiResponse { - code: number - success?: boolean - msg?: string - message?: string - data: T -} - -interface RequestConfig extends AxiosRequestConfig { - showError?: boolean - showLoading?: boolean - customErrorHandler?: (error: AxiosError) => void -} - -// ==================== 常量配置 ==================== -const BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api' -const TIMEOUT = 20000 - -// 不显示错误提示的 URL 白名单 -const ERROR_SILENT_URLS = [ - '/v1/kefu/wechatFriend/list', - '/v1/kefu/message/details', -] - -// ==================== 创建 Axios 实例 ==================== -const instance: AxiosInstance = axios.create({ - baseURL: BASE_URL, - timeout: TIMEOUT, - headers: { - 'Content-Type': 'application/json', - }, -}) - -// ==================== 请求拦截器 ==================== -instance.interceptors.request.use( - (config) => { - // 自动注入 Token - const userStore = useUserStore() - if (userStore.token) { - config.headers.Authorization = `Bearer ${userStore.token}` - } - - // 如果是 FormData,移除 Content-Type 让浏览器自动设置 - if (config.data instanceof FormData) { - delete config.headers['Content-Type'] - } - - // 添加时间戳防止缓存(GET 请求) - if (config.method?.toLowerCase() === 'get') { - config.params = { - ...config.params, - _t: Date.now(), - } - } - - return config - }, - (error) => { - return Promise.reject(error) - } -) - -// ==================== 响应拦截器 ==================== -instance.interceptors.response.use( - (response: AxiosResponse) => { - const { data } = response - const { code, success, msg, message } = data - - // 判断是否业务成功 - const isSuccess = code === 200 || success === true - - // 如果没有标准格式,直接返回(透传) - if (code === undefined && success === undefined) { - return data - } - - // 业务成功,返回数据 - if (isSuccess) { - return data.data ?? data - } - - // ==================== 业务失败处理 ==================== - - // 401 未授权 - if (code === 401) { - handleLoginExpired() - return Promise.reject(new Error(msg || message || '登录已过期')) - } - - // 403 无权限 - if (code === 403) { - ElMessage.error('暂无权限') - return Promise.reject(new Error('暂无权限')) - } - - // 其他错误 - const errorMsg = msg || message || '请求失败' - - // 检查是否在错误白名单中 - const isInSilentList = ERROR_SILENT_URLS.some((url) => - response.config.url?.includes(url) - ) - - // 显示错误提示(除非在白名单或禁用) - const customConfig = response.config as RequestConfig - if (!isInSilentList && customConfig.showError !== false) { - ElMessage.error(errorMsg) - } - - return Promise.reject(new Error(errorMsg)) - }, - (error: AxiosError) => { - const customConfig = error.config as RequestConfig - - // ==================== 错误处理 ==================== - - // 自定义错误处理 - if (customConfig?.customErrorHandler) { - customConfig.customErrorHandler(error) - return Promise.reject(error) - } - - // 网络错误 - if (!error.response) { - if (error.code === 'ECONNABORTED') { - ElMessage.error('请求超时,请稍后重试') - } else { - ElMessage.error('网络异常,请检查网络连接') - } - return Promise.reject(error) - } - - // HTTP 状态码错误 - const { status } = error.response - - switch (status) { - case 401: - handleLoginExpired() - break - case 403: - ElMessage.error('暂无权限') - break - case 404: - ElMessage.error('请求的资源不存在') - break - case 500: - ElMessage.error('服务器错误') - break - case 502: - ElMessage.error('网关错误') - break - case 503: - ElMessage.error('服务不可用') - break - default: - ElMessage.error(error.message || '请求失败') - } - - return Promise.reject(error) - } -) - -// ==================== 导出请求函数 ==================== - -/** - * 通用请求函数 - * @param url 请求地址 - * @param data 请求数据 - * @param method 请求方法 - * @param config 额外配置 - */ -export function request( - url: string, - data?: any, - method: Method = 'GET', - config?: RequestConfig -): Promise { - const axiosConfig: AxiosRequestConfig = { - url, - method, - ...config, - } - - // 如果是FormData,不设置Content-Type,让浏览器自动设置 - if (data instanceof FormData) { - delete axiosConfig.headers?.['Content-Type'] - } - - if (method.toUpperCase() === 'GET') { - axiosConfig.params = data - } else { - axiosConfig.data = data - } - - return instance(axiosConfig) -} - -export default request diff --git a/TouchVueThree/src/api/request2.ts b/TouchVueThree/src/api/request2.ts deleted file mode 100644 index acd86bb..0000000 --- a/TouchVueThree/src/api/request2.ts +++ /dev/null @@ -1,132 +0,0 @@ -import axios, { - AxiosInstance, - AxiosRequestConfig, - Method, - AxiosResponse, - AxiosError, -} from 'axios' -import { ElMessage } from 'element-plus' -import { useUserStore } from '@/stores/modules/user' -import { handleLoginExpired } from '@/utils/auth' - -// ==================== 类型定义 ==================== -interface RequestConfig extends AxiosRequestConfig { - showError?: boolean -} - -// ==================== 常量配置 ==================== -const BASE_URL = import.meta.env.VITE_API_BASE_URL2 || '/api' -const TIMEOUT = 20000 - -// 接口错误白名单:这些接口失败时不显示错误提示 -const ERROR_SILENT_URLS: string[] = [] - -// ==================== 创建 Axios 实例 ==================== -const instance: AxiosInstance = axios.create({ - baseURL: BASE_URL, - timeout: TIMEOUT, - headers: { - 'Content-Type': 'application/json', - Client: 'kefu-client', - }, -}) - -// ==================== 请求拦截器 ==================== -instance.interceptors.request.use( - (config) => { - // ⭐ 在每次请求时动态获取最新的 token2(参考旧项目逻辑) - // 旧项目:const { token2 } = useUserStore.getState(); - // 新项目:使用 Pinia,在拦截器中动态获取 - const userStore = useUserStore() - const token2 = userStore.token2 - - if (token2) { - config.headers = config.headers || {} - // ⭐ 使用小写 bearer(与旧项目保持一致) - config.headers.Authorization = `bearer ${token2}` - } else { - // ⭐ 如果没有 token2,尝试从 localStorage 读取(兼容性处理) - const savedToken2 = localStorage.getItem('token2') - if (savedToken2) { - config.headers = config.headers || {} - config.headers.Authorization = `bearer ${savedToken2}` - // 同步到 store - userStore.setToken2(savedToken2) - } - } - - // 如果是 FormData,移除 Content-Type 让浏览器自动设置 - if (config.data instanceof FormData) { - delete config.headers['Content-Type'] - } - - return config - }, - (error) => { - return Promise.reject(error) - } -) - -// ==================== 响应拦截器 ==================== -instance.interceptors.response.use( - (res: AxiosResponse) => { - // request2 直接返回 data,不做业务判断 - return res.data - }, - (err: AxiosError) => { - // 检查是否在错误白名单中 - const url = err.config?.url || '' - const isInErrorSilentList = ERROR_SILENT_URLS.some((pattern) => - url.includes(pattern) - ) - - // 处理401错误,统一的登录过期处理 - if (err.response && err.response.status === 401) { - handleLoginExpired() - return Promise.reject(err) - } - - // 如果不在白名单中,显示错误提示 - if (!isInErrorSilentList) { - ElMessage.error(err.message || '网络异常') - } - return Promise.reject(err) - } -) - -// ==================== 导出请求函数 ==================== - -/** - * 触客宝接口请求函数 - * @param url 请求地址 - * @param data 请求数据 - * @param method 请求方法 - * @param config 额外配置 - */ -export function request2( - url: string, - data?: any, - method: Method = 'GET', - config?: RequestConfig -): Promise { - const axiosConfig: AxiosRequestConfig = { - url, - method, - ...config, - } - - // 如果是FormData,不设置Content-Type,让浏览器自动设置 - if (data instanceof FormData) { - delete axiosConfig.headers?.['Content-Type'] - } - - if (method.toUpperCase() === 'GET') { - axiosConfig.params = data - } else { - axiosConfig.data = data - } - - return instance(axiosConfig) -} - -export default request2 diff --git a/TouchVueThree/src/assets/element-variables.scss b/TouchVueThree/src/assets/element-variables.scss deleted file mode 100644 index d7b83b5..0000000 --- a/TouchVueThree/src/assets/element-variables.scss +++ /dev/null @@ -1,11 +0,0 @@ -// Element Plus 主题变量覆盖(可选) -// 更多变量请参考: https://element-plus.org/zh-CN/guide/theming.html - -// 主题色 -// $--el-color-primary: #409eff; - -// 边框圆角 -// $--el-border-radius-base: 4px; - -// 字体 -// $--el-font-size-base: 14px; diff --git a/TouchVueThree/src/assets/style.css b/TouchVueThree/src/assets/style.css deleted file mode 100644 index 858dd77..0000000 --- a/TouchVueThree/src/assets/style.css +++ /dev/null @@ -1,87 +0,0 @@ -/* 全局样式 - Vue 3 + TypeScript + Element Plus */ - -/* CSS 重置 */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -/* 全局字体 */ -body { - font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", - "Microsoft YaHei", "微软雅黑", Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-tap-highlight-color: transparent; -} - -/* 隐藏滚动条 */ -.no-scrollbar::-webkit-scrollbar { - display: none; -} - -.no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; -} - -/* 自定义滚动条 */ -.custom-scroll::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -.custom-scroll::-webkit-scrollbar-thumb { - background: #dcdfe6; - border-radius: 3px; -} - -.custom-scroll::-webkit-scrollbar-thumb:hover { - background: #c0c4cc; -} - -.custom-scroll::-webkit-scrollbar-track { - background: #f5f7fa; -} - -/* 页面切换动画 */ -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.3s ease; -} - -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -/* 移动端适配 */ -.mobile-h-screen { - height: 100vh; - height: 100dvh; /* 解决移动端地址栏遮挡问题 */ -} - -/* 安全区域适配 */ -.pb-safe { - padding-bottom: env(safe-area-inset-bottom); -} - -/* Element Plus 全局样式覆盖 */ -:root { - --el-color-primary: #409eff; - --el-border-radius-base: 4px; -} - -/* 响应式工具类 */ -@media (max-width: 768px) { - .mobile-hidden { - display: none !important; - } -} - -@media (min-width: 769px) { - .desktop-hidden { - display: none !important; - } -} diff --git a/TouchVueThree/src/assets/styles/global.scss b/TouchVueThree/src/assets/styles/global.scss deleted file mode 100644 index 8954bc5..0000000 --- a/TouchVueThree/src/assets/styles/global.scss +++ /dev/null @@ -1,163 +0,0 @@ -// 全局样式 - -@use './reset.scss'; -@use './variables.scss' as *; -@use './mixins.scss' as *; - -// ==================== 全局工具类 ==================== - -// 文本对齐 -.text-left { text-align: left; } -.text-center { text-align: center; } -.text-right { text-align: right; } - -// 文本省略 -.ellipsis { @include ellipsis; } -.ellipsis-2 { @include multi-ellipsis(2); } -.ellipsis-3 { @include multi-ellipsis(3); } - -// Flex 布局 -.flex { display: flex; } -.flex-center { @include flex-center; } -.flex-between { @include flex-between; } -.flex-align-center { @include flex-align-center; } -.flex-justify-center { @include flex-justify-center; } -.flex-column { flex-direction: column; } -.flex-wrap { flex-wrap: wrap; } -.flex-1 { flex: 1; } - -// 间距 -.mt-xs { margin-top: $spacing-xs; } -.mt-sm { margin-top: $spacing-sm; } -.mt-md { margin-top: $spacing-md; } -.mt-lg { margin-top: $spacing-lg; } -.mt-xl { margin-top: $spacing-xl; } - -.mb-xs { margin-bottom: $spacing-xs; } -.mb-sm { margin-bottom: $spacing-sm; } -.mb-md { margin-bottom: $spacing-md; } -.mb-lg { margin-bottom: $spacing-lg; } -.mb-xl { margin-bottom: $spacing-xl; } - -.ml-xs { margin-left: $spacing-xs; } -.ml-sm { margin-left: $spacing-sm; } -.ml-md { margin-left: $spacing-md; } -.ml-lg { margin-left: $spacing-lg; } -.ml-xl { margin-left: $spacing-xl; } - -.mr-xs { margin-right: $spacing-xs; } -.mr-sm { margin-right: $spacing-sm; } -.mr-md { margin-right: $spacing-md; } -.mr-lg { margin-right: $spacing-lg; } -.mr-xl { margin-right: $spacing-xl; } - -.p-xs { padding: $spacing-xs; } -.p-sm { padding: $spacing-sm; } -.p-md { padding: $spacing-md; } -.p-lg { padding: $spacing-lg; } -.p-xl { padding: $spacing-xl; } - -// 字体大小 -.text-xs { font-size: $font-size-xs; } -.text-sm { font-size: $font-size-sm; } -.text-base { font-size: $font-size-base; } -.text-lg { font-size: $font-size-lg; } -.text-xl { font-size: $font-size-xl; } - -// 字体粗细 -.font-light { font-weight: $font-weight-light; } -.font-normal { font-weight: $font-weight-normal; } -.font-medium { font-weight: $font-weight-medium; } -.font-bold { font-weight: $font-weight-bold; } - -// 颜色 -.text-primary { color: $text-color-primary; } -.text-regular { color: $text-color-regular; } -.text-secondary { color: $text-color-secondary; } -.text-placeholder { color: $text-color-placeholder; } - -// 圆角 -.rounded { border-radius: $border-radius-base; } -.rounded-sm { border-radius: $border-radius-small; } -.rounded-lg { border-radius: $border-radius-large; } -.rounded-full { border-radius: $border-radius-circle; } - -// ==================== 自定义滚动条 ==================== - -.custom-scrollbar { - @include scrollbar(8px, rgba(0, 0, 0, 0.2)); -} - -// ==================== 卡片 ==================== - -.card { - @include card; -} - -// ==================== 页面容器 ==================== - -.page-container { - height: 100%; - background-color: $bg-color-page; - overflow: hidden; -} - -.page-content { - height: 100%; - padding: $spacing-lg; - overflow-y: auto; - @include scrollbar; -} - -// ==================== 加载状态 ==================== - -.loading-container { - @include flex-center; - height: 100%; - color: $text-color-secondary; -} - -// ==================== 空状态 ==================== - -.empty-container { - @include flex-center; - flex-direction: column; - height: 100%; - color: $text-color-secondary; - - .empty-icon { - font-size: 64px; - margin-bottom: $spacing-md; - opacity: 0.3; - } - - .empty-text { - font-size: $font-size-base; - } -} - -// ==================== 动画 ==================== - -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.3s; -} - -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.slide-fade-enter-active { - transition: all 0.3s ease-out; -} - -.slide-fade-leave-active { - transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1); -} - -.slide-fade-enter-from, -.slide-fade-leave-to { - transform: translateX(20px); - opacity: 0; -} diff --git a/TouchVueThree/src/assets/styles/mixins.scss b/TouchVueThree/src/assets/styles/mixins.scss deleted file mode 100644 index 5d9480b..0000000 --- a/TouchVueThree/src/assets/styles/mixins.scss +++ /dev/null @@ -1,178 +0,0 @@ -// SCSS 混入(Mixins) -@use 'sass:math'; - -// ==================== 文本省略 ==================== - -// 单行省略 -@mixin ellipsis { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -// 多行省略 -@mixin multi-ellipsis($lines: 2) { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: $lines; - overflow: hidden; - text-overflow: ellipsis; -} - -// ==================== Flex 布局 ==================== - -// Flex 居中 -@mixin flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -// Flex 垂直居中 -@mixin flex-align-center { - display: flex; - align-items: center; -} - -// Flex 水平居中 -@mixin flex-justify-center { - display: flex; - justify-content: center; -} - -// Flex 两端对齐 -@mixin flex-between { - display: flex; - align-items: center; - justify-content: space-between; -} - -// ==================== 清除浮动 ==================== - -@mixin clearfix { - &::after { - content: ''; - display: table; - clear: both; - } -} - -// ==================== 滚动条样式 ==================== - -@mixin scrollbar($width: 6px, $thumb-color: rgba(0, 0, 0, 0.2)) { - &::-webkit-scrollbar { - width: $width; - height: $width; - } - - &::-webkit-scrollbar-thumb { - background-color: $thumb-color; - border-radius: math.div($width, 2); - - &:hover { - background-color: rgba(0, 0, 0, 0.3); - } - } - - &::-webkit-scrollbar-track { - background-color: rgba(0, 0, 0, 0.05); - } -} - -// ==================== 响应式 ==================== - -// 媒体查询 -@mixin respond-to($breakpoint) { - @if $breakpoint == xs { - @media (max-width: 768px) { @content; } - } - @else if $breakpoint == sm { - @media (min-width: 768px) and (max-width: 992px) { @content; } - } - @else if $breakpoint == md { - @media (min-width: 992px) and (max-width: 1200px) { @content; } - } - @else if $breakpoint == lg { - @media (min-width: 1200px) and (max-width: 1920px) { @content; } - } - @else if $breakpoint == xl { - @media (min-width: 1920px) { @content; } - } -} - -// ==================== 动画 ==================== - -// 淡入淡出 -@mixin fade-in($duration: 0.3s) { - animation: fadeIn $duration ease-in; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -// 滑入 -@mixin slide-in($direction: 'bottom', $duration: 0.3s) { - @if $direction == 'bottom' { - animation: slideInBottom $duration ease-out; - } @else if $direction == 'top' { - animation: slideInTop $duration ease-out; - } @else if $direction == 'left' { - animation: slideInLeft $duration ease-out; - } @else if $direction == 'right' { - animation: slideInRight $duration ease-out; - } -} - -@keyframes slideInBottom { - from { - transform: translateY(20px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -// ==================== 卡片样式 ==================== - -@mixin card($padding: 16px) { - background: #fff; - border-radius: 8px; - padding: $padding; - box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); -} - -// ==================== 绝对定位居中 ==================== - -@mixin absolute-center { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -// ==================== 圆形头像 ==================== - -@mixin avatar($size: 40px) { - width: $size; - height: $size; - border-radius: 50%; - object-fit: cover; -} - -// ==================== 状态点 ==================== - -@mixin status-dot($size: 8px, $color: #67c23a) { - width: $size; - height: $size; - border-radius: 50%; - background-color: $color; - border: 2px solid #fff; -} diff --git a/TouchVueThree/src/assets/styles/reset.scss b/TouchVueThree/src/assets/styles/reset.scss deleted file mode 100644 index cc480b1..0000000 --- a/TouchVueThree/src/assets/styles/reset.scss +++ /dev/null @@ -1,83 +0,0 @@ -// CSS 重置样式 - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, -body { - height: 100%; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', - 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.5; - color: #303133; - background-color: #f2f3f5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -#app { - height: 100%; -} - -a { - color: #409eff; - text-decoration: none; - transition: color 0.3s; - - &:hover { - color: #66b1ff; - } -} - -ul, -ol { - list-style: none; -} - -button { - border: none; - outline: none; - cursor: pointer; - background: transparent; -} - -input, -textarea { - font-family: inherit; - font-size: inherit; - outline: none; -} - -img { - display: block; - max-width: 100%; -} - -// 滚动条美化 -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.2); - border-radius: 4px; - - &:hover { - background-color: rgba(0, 0, 0, 0.3); - } -} - -::-webkit-scrollbar-track { - background-color: rgba(0, 0, 0, 0.05); -} - -// 选中文本颜色 -::selection { - background-color: #409eff; - color: #fff; -} diff --git a/TouchVueThree/src/assets/styles/variables.scss b/TouchVueThree/src/assets/styles/variables.scss deleted file mode 100644 index 051ca56..0000000 --- a/TouchVueThree/src/assets/styles/variables.scss +++ /dev/null @@ -1,136 +0,0 @@ -// 全局 SCSS 变量 - -// ==================== CSS 变量(全局主题) ==================== -:root { - --primary-color: #188eee; - --primary-color-light: #40a9ff; - --primary-color-dark: #096dd9; - --primary-gradient: linear-gradient(135deg, #188eee 0%, #096dd9 100%); - --primary-shadow: rgba(24, 142, 238, 0.3); - --primary-shadow-light: rgba(24, 142, 238, 0.1); - --primary-shadow-dark: rgba(24, 142, 238, 0.4); -} - -// ==================== 颜色系统 ==================== - -// 主题色(与 Element Plus 保持一致,但使用项目主色) -$primary-color: #188eee; -$primary-color-light: #40a9ff; -$primary-color-dark: #096dd9; -$success-color: #67c23a; -$warning-color: #e6a23c; -$danger-color: #f56c6c; -$info-color: #909399; - -// 文字颜色 -$text-color-primary: #303133; -$text-color-regular: #606266; -$text-color-secondary: #909399; -$text-color-placeholder: #c0c4cc; - -// 背景颜色 -$bg-color: #ffffff; -$bg-color-page: #f2f3f5; -$bg-color-overlay: #ffffff; - -// 边框颜色 -$border-color: #dcdfe6; -$border-color-light: #e4e7ed; -$border-color-lighter: #ebeef5; -$border-color-extra-light: #f2f6fc; - -// ==================== 尺寸系统 ==================== - -// 间距 -$spacing-xs: 4px; -$spacing-sm: 8px; -$spacing-md: 12px; -$spacing-lg: 16px; -$spacing-xl: 20px; -$spacing-xxl: 24px; - -// 圆角 -$border-radius-base: 4px; -$border-radius-small: 2px; -$border-radius-large: 8px; -$border-radius-round: 20px; -$border-radius-circle: 50%; - -// 字体大小 -$font-size-xs: 12px; -$font-size-sm: 13px; -$font-size-base: 14px; -$font-size-md: 15px; -$font-size-lg: 16px; -$font-size-xl: 18px; -$font-size-xxl: 20px; - -// 字体粗细 -$font-weight-light: 300; -$font-weight-normal: 400; -$font-weight-medium: 500; -$font-weight-bold: 700; - -// 行高 -$line-height-base: 1.5; -$line-height-sm: 1.2; -$line-height-lg: 2; - -// ==================== 阴影 ==================== - -$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04); -$box-shadow-dark: 0 2px 8px rgba(0, 0, 0, 0.15); -$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1); - -// ==================== 过渡动画 ==================== - -$transition-base: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); -$transition-fade: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1); -$transition-border: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); -$transition-color: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1); - -// ==================== Z-Index ==================== - -$z-index-normal: 1; -$z-index-top: 1000; -$z-index-popper: 2000; -$z-index-dialog: 3000; -$z-index-message: 4000; - -// ==================== 布局 ==================== - -// 头部高度 -$header-height: 60px; - -// 侧边栏宽度 -$sidebar-width: 240px; -$sidebar-collapsed-width: 64px; - -// 聊天布局 -$chat-sidebar-width: 280px; -$chat-panel-width: 320px; - -// 容器最大宽度 -$container-max-width: 1400px; - -// ==================== 微信客服专用 ==================== - -// 消息气泡 -$message-bubble-bg-self: #95ec69; -$message-bubble-bg-other: #ffffff; -$message-bubble-border-color: #e4e7ed; -$message-bubble-radius: 8px; - -// 会话列表 -$session-item-height: 72px; -$session-item-hover-bg: #f5f7fa; -$session-item-active-bg: #ecf5ff; - -// 联系人列表 -$contact-item-height: 60px; -$contact-avatar-size: 40px; - -// 在线状态 -$status-online: #67c23a; -$status-offline: #909399; -$status-busy: #e6a23c; diff --git a/TouchVueThree/src/auto-imports.d.ts b/TouchVueThree/src/auto-imports.d.ts deleted file mode 100644 index c17773b..0000000 --- a/TouchVueThree/src/auto-imports.d.ts +++ /dev/null @@ -1,312 +0,0 @@ -/* eslint-disable */ -/* prettier-ignore */ -// @ts-nocheck -// noinspection JSUnusedGlobalSymbols -// Generated by unplugin-auto-import -export {} -declare global { - const EffectScope: typeof import('vue')['EffectScope'] - const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] - const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] - const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] - const computed: typeof import('vue')['computed'] - const computedAsync: typeof import('@vueuse/core')['computedAsync'] - const computedEager: typeof import('@vueuse/core')['computedEager'] - const computedInject: typeof import('@vueuse/core')['computedInject'] - const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] - const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] - const controlledRef: typeof import('@vueuse/core')['controlledRef'] - const createApp: typeof import('vue')['createApp'] - const createEventHook: typeof import('@vueuse/core')['createEventHook'] - const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] - const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] - const createPinia: typeof import('pinia')['createPinia'] - const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] - const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] - const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] - const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] - const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] - const customRef: typeof import('vue')['customRef'] - const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] - const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] - const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] - const defineComponent: typeof import('vue')['defineComponent'] - const defineStore: typeof import('pinia')['defineStore'] - const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] - const effectScope: typeof import('vue')['effectScope'] - const extendRef: typeof import('@vueuse/core')['extendRef'] - const getActivePinia: typeof import('pinia')['getActivePinia'] - const getCurrentInstance: typeof import('vue')['getCurrentInstance'] - const getCurrentScope: typeof import('vue')['getCurrentScope'] - const h: typeof import('vue')['h'] - const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] - const inject: typeof import('vue')['inject'] - const injectLocal: typeof import('@vueuse/core')['injectLocal'] - const isDefined: typeof import('@vueuse/core')['isDefined'] - const isProxy: typeof import('vue')['isProxy'] - const isReactive: typeof import('vue')['isReactive'] - const isReadonly: typeof import('vue')['isReadonly'] - const isRef: typeof import('vue')['isRef'] - const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] - const mapActions: typeof import('pinia')['mapActions'] - const mapGetters: typeof import('pinia')['mapGetters'] - const mapState: typeof import('pinia')['mapState'] - const mapStores: typeof import('pinia')['mapStores'] - const mapWritableState: typeof import('pinia')['mapWritableState'] - const markRaw: typeof import('vue')['markRaw'] - const nextTick: typeof import('vue')['nextTick'] - const onActivated: typeof import('vue')['onActivated'] - const onBeforeMount: typeof import('vue')['onBeforeMount'] - const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] - const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] - const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] - const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] - const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] - const onDeactivated: typeof import('vue')['onDeactivated'] - const onErrorCaptured: typeof import('vue')['onErrorCaptured'] - const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] - const onLongPress: typeof import('@vueuse/core')['onLongPress'] - const onMounted: typeof import('vue')['onMounted'] - const onRenderTracked: typeof import('vue')['onRenderTracked'] - const onRenderTriggered: typeof import('vue')['onRenderTriggered'] - const onScopeDispose: typeof import('vue')['onScopeDispose'] - const onServerPrefetch: typeof import('vue')['onServerPrefetch'] - const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] - const onUnmounted: typeof import('vue')['onUnmounted'] - const onUpdated: typeof import('vue')['onUpdated'] - const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] - const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] - const provide: typeof import('vue')['provide'] - const provideLocal: typeof import('@vueuse/core')['provideLocal'] - const reactify: typeof import('@vueuse/core')['reactify'] - const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] - const reactive: typeof import('vue')['reactive'] - const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] - const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] - const reactivePick: typeof import('@vueuse/core')['reactivePick'] - const readonly: typeof import('vue')['readonly'] - const ref: typeof import('vue')['ref'] - const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] - const refDebounced: typeof import('@vueuse/core')['refDebounced'] - const refDefault: typeof import('@vueuse/core')['refDefault'] - const refThrottled: typeof import('@vueuse/core')['refThrottled'] - const refWithControl: typeof import('@vueuse/core')['refWithControl'] - const resolveComponent: typeof import('vue')['resolveComponent'] - const resolveRef: typeof import('@vueuse/core')['resolveRef'] - const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] - const setActivePinia: typeof import('pinia')['setActivePinia'] - const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] - const shallowReactive: typeof import('vue')['shallowReactive'] - const shallowReadonly: typeof import('vue')['shallowReadonly'] - const shallowRef: typeof import('vue')['shallowRef'] - const storeToRefs: typeof import('pinia')['storeToRefs'] - const syncRef: typeof import('@vueuse/core')['syncRef'] - const syncRefs: typeof import('@vueuse/core')['syncRefs'] - const templateRef: typeof import('@vueuse/core')['templateRef'] - const throttledRef: typeof import('@vueuse/core')['throttledRef'] - const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] - const toRaw: typeof import('vue')['toRaw'] - const toReactive: typeof import('@vueuse/core')['toReactive'] - const toRef: typeof import('vue')['toRef'] - const toRefs: typeof import('vue')['toRefs'] - const toValue: typeof import('vue')['toValue'] - const triggerRef: typeof import('vue')['triggerRef'] - const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] - const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] - const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] - const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] - const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] - const unref: typeof import('vue')['unref'] - const unrefElement: typeof import('@vueuse/core')['unrefElement'] - const until: typeof import('@vueuse/core')['until'] - const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] - const useAnimate: typeof import('@vueuse/core')['useAnimate'] - const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] - const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] - const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] - const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] - const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] - const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] - const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] - const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] - const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] - const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] - const useArraySome: typeof import('@vueuse/core')['useArraySome'] - const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] - const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] - const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] - const useAttrs: typeof import('vue')['useAttrs'] - const useBase64: typeof import('@vueuse/core')['useBase64'] - const useBattery: typeof import('@vueuse/core')['useBattery'] - const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] - const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] - const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] - const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] - const useCached: typeof import('@vueuse/core')['useCached'] - const useClipboard: typeof import('@vueuse/core')['useClipboard'] - const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] - const useCloned: typeof import('@vueuse/core')['useCloned'] - const useColorMode: typeof import('@vueuse/core')['useColorMode'] - const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] - const useCounter: typeof import('@vueuse/core')['useCounter'] - const useCssModule: typeof import('vue')['useCssModule'] - const useCssVar: typeof import('@vueuse/core')['useCssVar'] - const useCssVars: typeof import('vue')['useCssVars'] - const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] - const useCycleList: typeof import('@vueuse/core')['useCycleList'] - const useDark: typeof import('@vueuse/core')['useDark'] - const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] - const useDebounce: typeof import('@vueuse/core')['useDebounce'] - const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] - const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] - const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] - const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] - const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] - const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] - const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] - const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] - const useDraggable: typeof import('@vueuse/core')['useDraggable'] - const useDropZone: typeof import('@vueuse/core')['useDropZone'] - const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] - const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] - const useElementHover: typeof import('@vueuse/core')['useElementHover'] - const useElementSize: typeof import('@vueuse/core')['useElementSize'] - const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] - const useEventBus: typeof import('@vueuse/core')['useEventBus'] - const useEventListener: typeof import('@vueuse/core')['useEventListener'] - const useEventSource: typeof import('@vueuse/core')['useEventSource'] - const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] - const useFavicon: typeof import('@vueuse/core')['useFavicon'] - const useFetch: typeof import('@vueuse/core')['useFetch'] - const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] - const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] - const useFocus: typeof import('@vueuse/core')['useFocus'] - const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] - const useFps: typeof import('@vueuse/core')['useFps'] - const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] - const useGamepad: typeof import('@vueuse/core')['useGamepad'] - const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] - const useId: typeof import('vue')['useId'] - const useIdle: typeof import('@vueuse/core')['useIdle'] - const useImage: typeof import('@vueuse/core')['useImage'] - const useInfiniteQuery: typeof import('@tanstack/vue-query')['useInfiniteQuery'] - const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] - const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] - const useInterval: typeof import('@vueuse/core')['useInterval'] - const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] - const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] - const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] - const useLink: typeof import('vue-router')['useLink'] - const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] - const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] - const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] - const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] - const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] - const useMemoize: typeof import('@vueuse/core')['useMemoize'] - const useMemory: typeof import('@vueuse/core')['useMemory'] - const useModel: typeof import('vue')['useModel'] - const useMounted: typeof import('@vueuse/core')['useMounted'] - const useMouse: typeof import('@vueuse/core')['useMouse'] - const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] - const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] - const useMutation: typeof import('@tanstack/vue-query')['useMutation'] - const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] - const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] - const useNetwork: typeof import('@vueuse/core')['useNetwork'] - const useNow: typeof import('@vueuse/core')['useNow'] - const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] - const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] - const useOnline: typeof import('@vueuse/core')['useOnline'] - const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] - const useParallax: typeof import('@vueuse/core')['useParallax'] - const useParentElement: typeof import('@vueuse/core')['useParentElement'] - const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] - const usePermission: typeof import('@vueuse/core')['usePermission'] - const usePointer: typeof import('@vueuse/core')['usePointer'] - const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] - const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] - const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] - const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] - const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] - const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] - const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] - const usePrevious: typeof import('@vueuse/core')['usePrevious'] - const useQuery: typeof import('@tanstack/vue-query')['useQuery'] - const useQueryClient: typeof import('@tanstack/vue-query')['useQueryClient'] - const useRafFn: typeof import('@vueuse/core')['useRafFn'] - const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] - const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] - const useRoute: typeof import('vue-router')['useRoute'] - const useRouter: typeof import('vue-router')['useRouter'] - const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] - const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] - const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] - const useScroll: typeof import('@vueuse/core')['useScroll'] - const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] - const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] - const useShare: typeof import('@vueuse/core')['useShare'] - const useSlots: typeof import('vue')['useSlots'] - const useSorted: typeof import('@vueuse/core')['useSorted'] - const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] - const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] - const useStepper: typeof import('@vueuse/core')['useStepper'] - const useStorage: typeof import('@vueuse/core')['useStorage'] - const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] - const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] - const useSupported: typeof import('@vueuse/core')['useSupported'] - const useSwipe: typeof import('@vueuse/core')['useSwipe'] - const useTemplateRef: typeof import('vue')['useTemplateRef'] - const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] - const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] - const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] - const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] - const useThrottle: typeof import('@vueuse/core')['useThrottle'] - const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] - const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] - const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] - const useTimeout: typeof import('@vueuse/core')['useTimeout'] - const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] - const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] - const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] - const useTitle: typeof import('@vueuse/core')['useTitle'] - const useToNumber: typeof import('@vueuse/core')['useToNumber'] - const useToString: typeof import('@vueuse/core')['useToString'] - const useToggle: typeof import('@vueuse/core')['useToggle'] - const useTransition: typeof import('@vueuse/core')['useTransition'] - const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] - const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] - const useVModel: typeof import('@vueuse/core')['useVModel'] - const useVModels: typeof import('@vueuse/core')['useVModels'] - const useVibrate: typeof import('@vueuse/core')['useVibrate'] - const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] - const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] - const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] - const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] - const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] - const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] - const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] - const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] - const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] - const watch: typeof import('vue')['watch'] - const watchArray: typeof import('@vueuse/core')['watchArray'] - const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] - const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] - const watchDeep: typeof import('@vueuse/core')['watchDeep'] - const watchEffect: typeof import('vue')['watchEffect'] - const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] - const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] - const watchOnce: typeof import('@vueuse/core')['watchOnce'] - const watchPausable: typeof import('@vueuse/core')['watchPausable'] - const watchPostEffect: typeof import('vue')['watchPostEffect'] - const watchSyncEffect: typeof import('vue')['watchSyncEffect'] - const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] - const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] - const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] - const whenever: typeof import('@vueuse/core')['whenever'] -} -// for type re-export -declare global { - // @ts-ignore - export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' - import('vue') -} diff --git a/TouchVueThree/src/components.d.ts b/TouchVueThree/src/components.d.ts deleted file mode 100644 index 20184a6..0000000 --- a/TouchVueThree/src/components.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint-disable */ -/* prettier-ignore */ -// @ts-nocheck -// Generated by unplugin-vue-components -// Read more: https://github.com/vuejs/core/pull/3399 -export {} - -declare module 'vue' { - export interface GlobalComponents { - ElAvatar: typeof import('element-plus/es')['ElAvatar'] - ElBadge: typeof import('element-plus/es')['ElBadge'] - ElButton: typeof import('element-plus/es')['ElButton'] - ElCard: typeof import('element-plus/es')['ElCard'] - ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] - ElCollapse: typeof import('element-plus/es')['ElCollapse'] - ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] - ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition'] - ElContainer: typeof import('element-plus/es')['ElContainer'] - ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] - ElDialog: typeof import('element-plus/es')['ElDialog'] - ElDropdown: typeof import('element-plus/es')['ElDropdown'] - ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] - ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] - ElEmpty: typeof import('element-plus/es')['ElEmpty'] - ElForm: typeof import('element-plus/es')['ElForm'] - ElFormItem: typeof import('element-plus/es')['ElFormItem'] - ElHeader: typeof import('element-plus/es')['ElHeader'] - ElIcon: typeof import('element-plus/es')['ElIcon'] - ElImage: typeof import('element-plus/es')['ElImage'] - ElInput: typeof import('element-plus/es')['ElInput'] - ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElMain: typeof import('element-plus/es')['ElMain'] - ElMenu: typeof import('element-plus/es')['ElMenu'] - ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] - ElOption: typeof import('element-plus/es')['ElOption'] - ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] - ElSelect: typeof import('element-plus/es')['ElSelect'] - ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] - ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem'] - ElSpace: typeof import('element-plus/es')['ElSpace'] - ElTabPane: typeof import('element-plus/es')['ElTabPane'] - ElTabs: typeof import('element-plus/es')['ElTabs'] - ElTag: typeof import('element-plus/es')['ElTag'] - ElTooltip: typeof import('element-plus/es')['ElTooltip'] - ElTree: typeof import('element-plus/es')['ElTree'] - EmojiPicker: typeof import('./components/common/EmojiPicker/index.vue')['default'] - FriendSelection: typeof import('./components/business/FriendSelection/index.vue')['default'] - RouterLink: typeof import('vue-router')['RouterLink'] - RouterView: typeof import('vue-router')['RouterView'] - SessionContextMenu: typeof import('./components/business/SessionContextMenu/index.vue')['default'] - } -} diff --git a/TouchVueThree/src/components/business/FriendSelection/index.vue b/TouchVueThree/src/components/business/FriendSelection/index.vue deleted file mode 100644 index 11cb96d..0000000 --- a/TouchVueThree/src/components/business/FriendSelection/index.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - 好友选择功能开发中... - 需要从联系人列表选择好友 - - - - - - - - diff --git a/TouchVueThree/src/components/business/SessionContextMenu/index.vue b/TouchVueThree/src/components/business/SessionContextMenu/index.vue deleted file mode 100644 index eb62fee..0000000 --- a/TouchVueThree/src/components/business/SessionContextMenu/index.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - {{ isPinned ? '取消置顶' : '置顶' }} - - - - 修改备注 - - - - 删除 - - - - - - - - - diff --git a/TouchVueThree/src/components/common/EmojiPicker/index.vue b/TouchVueThree/src/components/common/EmojiPicker/index.vue deleted file mode 100644 index 7fc65ea..0000000 --- a/TouchVueThree/src/components/common/EmojiPicker/index.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - {{ category.label }} - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/composables/business/wechat/handlers/accountHandlers.ts b/TouchVueThree/src/composables/business/wechat/handlers/accountHandlers.ts deleted file mode 100644 index 6adcfaf..0000000 --- a/TouchVueThree/src/composables/business/wechat/handlers/accountHandlers.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * 账号状态处理器 - * - * 负责处理: - * - 账号在线状态 - * - 账号登录响应 - * - 账号列表更新 - */ - -import type { WebSocketMessage } from '@/types/wechat' -import type { HandlerRegistry } from './index' -import { useMessageSubscription } from '../useMessageSubscription' -import { WS_CMD_TYPE } from '@/constants/wechat' -import { useAccountStore } from '@/stores/modules/wechat' - -const { emitAccountStatus } = useMessageSubscription() - -// ==================== 账号状态处理器 ==================== - -/** - * 处理账号在线状态响应 - * CmdRequestWechatAccountsAliveStatusResp - */ -function handleAccountAliveStatus(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.wechatAccountsAliveStatus) { - console.warn('[AccountHandler] 账号状态数据为空') - return - } - - const aliveStatus = data.wechatAccountsAliveStatus as Record - - console.log('[AccountHandler] 📡 账号状态更新:', { - count: Object.keys(aliveStatus).length, - online: Object.values(aliveStatus).filter(Boolean).length, - }) - - // 发布每个账号的状态 - Object.entries(aliveStatus).forEach(([accountId, isOnline]) => { - emitAccountStatus(Number(accountId), isOnline) - }) - - // 更新 Store - try { - const accountStore = useAccountStore() - accountStore.updateAccountsOnlineStatus(aliveStatus) - } catch (error) { - console.error('[AccountHandler] 更新 Store 失败:', error) - } -} - -/** - * 处理登录响应 - * CmdSignInResp - */ -function handleSignInResp(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data) { - console.warn('[AccountHandler] 登录响应数据为空') - return - } - - console.log('[AccountHandler] 🔐 登录响应:', { - code: data.code, - message: data.message, - }) - - if (data.code === 0) { - console.log('[AccountHandler] ✅ 登录成功') - } else { - console.error('[AccountHandler] ❌ 登录失败:', data.message) - } -} - -/** - * 处理账号列表更新 - * CmdAccountListUpdate - */ -function handleAccountListUpdate(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.accounts) { - console.warn('[AccountHandler] 账号列表数据为空') - return - } - - console.log('[AccountHandler] 📋 账号列表更新:', { - count: data.accounts.length, - }) - - // TODO: 更新账号列表 Store -} - -// ==================== 导出处理器映射 ==================== - -export const accountHandlers: HandlerRegistry = { - // 账号在线状态 - [WS_CMD_TYPE.ACCOUNT_ALIVE_STATUS_RESP]: handleAccountAliveStatus, - - // 登录响应 - [WS_CMD_TYPE.SIGN_IN_RESP]: handleSignInResp, - - // 账号列表更新 - [WS_CMD_TYPE.ACCOUNT_LIST_UPDATE]: handleAccountListUpdate, -} diff --git a/TouchVueThree/src/composables/business/wechat/handlers/index.ts b/TouchVueThree/src/composables/business/wechat/handlers/index.ts deleted file mode 100644 index 2a25be0..0000000 --- a/TouchVueThree/src/composables/business/wechat/handlers/index.ts +++ /dev/null @@ -1,210 +0,0 @@ -/** - * WebSocket 消息处理器注册中心 - * - * 职责: - * - 统一注册所有消息处理器 - * - 提供处理器调用入口 - * - 支持动态扩展 - */ - -import type { WebSocketMessage } from '@/types/wechat' -import { messageHandlers } from './messageHandlers' -import { accountHandlers } from './accountHandlers' -import { sessionHandlers } from './sessionHandlers' -import { systemHandlers } from './systemHandlers' - -// ==================== 类型定义 ==================== - -export type MessageHandler = (message: WebSocketMessage) => void | Promise - -export type HandlerRegistry = Record - -// ==================== 处理器注册表 ==================== - -/** - * 默认处理器映射 - * 包含所有模块的处理器 - */ -const defaultHandlers: HandlerRegistry = { - ...messageHandlers, - ...accountHandlers, - ...sessionHandlers, - ...systemHandlers, -} - -// ==================== 处理器管理器 ==================== - -/** - * 消息处理器管理器 - * 支持动态注册和注销处理器 - */ -export class MessageHandlerManager { - private handlers: HandlerRegistry - private readonly isDev = import.meta.env.DEV - - constructor(initialHandlers: HandlerRegistry = {}) { - this.handlers = { ...defaultHandlers, ...initialHandlers } - - if (this.isDev) { - console.log('[MessageHandler] 已注册的处理器:', Object.keys(this.handlers)) - } - } - - /** - * 处理消息 - * @param cmdType 命令类型 - * @param message 消息数据 - */ - async handle(cmdType: string, message: WebSocketMessage): Promise { - const handler = this.handlers[cmdType] - - if (!handler) { - if (this.isDev) { - console.warn(`[MessageHandler] 未注册的消息类型: ${cmdType}`) - } - return - } - - try { - if (this.isDev) { - console.log(`[MessageHandler] 处理消息: ${cmdType}`, { - preview: JSON.stringify(message).substring(0, 100) + '...', - }) - } - - await handler(message) - - if (this.isDev) { - console.log(`[MessageHandler] ✅ 处理完成: ${cmdType}`) - } - } catch (error) { - console.error(`[MessageHandler] ❌ 处理失败: ${cmdType}`, error) - // 不抛出错误,避免影响后续消息处理 - } - } - - /** - * 注册新的处理器 - * @param cmdType 命令类型 - * @param handler 处理函数 - */ - register(cmdType: string, handler: MessageHandler): void { - if (this.handlers[cmdType] && this.isDev) { - console.warn(`[MessageHandler] 覆盖已存在的处理器: ${cmdType}`) - } - - this.handlers[cmdType] = handler - - if (this.isDev) { - console.log(`[MessageHandler] 注册处理器: ${cmdType}`) - } - } - - /** - * 批量注册处理器 - * @param handlers 处理器映射 - */ - registerBatch(handlers: HandlerRegistry): void { - Object.entries(handlers).forEach(([cmdType, handler]) => { - this.register(cmdType, handler) - }) - } - - /** - * 注销处理器 - * @param cmdType 命令类型 - */ - unregister(cmdType: string): void { - if (!this.handlers[cmdType]) { - if (this.isDev) { - console.warn(`[MessageHandler] 处理器不存在: ${cmdType}`) - } - return - } - - delete this.handlers[cmdType] - - if (this.isDev) { - console.log(`[MessageHandler] 注销处理器: ${cmdType}`) - } - } - - /** - * 获取已注册的处理器列表 - */ - getRegisteredTypes(): string[] { - return Object.keys(this.handlers) - } - - /** - * 检查是否已注册 - * @param cmdType 命令类型 - */ - has(cmdType: string): boolean { - return !!this.handlers[cmdType] - } - - /** - * 清空所有处理器(用于测试) - */ - clear(): void { - this.handlers = {} - if (this.isDev) { - console.log('[MessageHandler] 已清空所有处理器') - } - } - - /** - * 重置为默认处理器 - */ - reset(): void { - this.handlers = { ...defaultHandlers } - if (this.isDev) { - console.log('[MessageHandler] 已重置为默认处理器') - } - } -} - -// ==================== 单例实例 ==================== - -let managerInstance: MessageHandlerManager | null = null - -/** - * 获取消息处理器管理器实例(单例) - */ -export function getMessageHandlerManager(): MessageHandlerManager { - if (!managerInstance) { - managerInstance = new MessageHandlerManager() - } - return managerInstance -} - -/** - * 创建消息处理函数(简化版) - * - * @example - * ```typescript - * const handleMessage = createMessageHandler() - * - * ws.onmessage = (event) => { - * const message = JSON.parse(event.data) - * handleMessage(message.cmdType, message) - * } - * ``` - */ -export function createMessageHandler() { - const manager = getMessageHandlerManager() - - return (cmdType: string, message: WebSocketMessage) => { - return manager.handle(cmdType, message) - } -} - -// ==================== 导出 ==================== - -export { - messageHandlers, - accountHandlers, - sessionHandlers, - systemHandlers, -} diff --git a/TouchVueThree/src/composables/business/wechat/handlers/messageHandlers.ts b/TouchVueThree/src/composables/business/wechat/handlers/messageHandlers.ts deleted file mode 100644 index dc2742d..0000000 --- a/TouchVueThree/src/composables/business/wechat/handlers/messageHandlers.ts +++ /dev/null @@ -1,331 +0,0 @@ -/** - * 聊天消息处理器 - * - * 负责处理: - * - 新消息接收 - * - 消息状态更新 - * - 消息撤回 - */ - -import type { WebSocketMessage } from '@/types/wechat' -import type { HandlerRegistry } from './index' -import { useMessageSubscription } from '../useMessageSubscription' -import { SessionManager } from '@/utils/dbManagers/SessionManager' -import { MessageManager } from '@/utils/dbManagers/MessageManager' -import { WS_CMD_TYPE } from '@/constants/wechat' -import { useMessageStore } from '@/stores/modules/wechat/useMessageStore' - -const { emitNewMessage, emitMessageUpdate, emitMessageRecall } = useMessageSubscription() - -// ==================== 辅助函数 ==================== - -/** - * 验证消息数据有效性 - */ -function validateMessageData(data: any): boolean { - if (!data) { - console.warn('[MessageHandler] 消息数据为空') - return false - } - - if (!data.sessionId || !data.sessionType) { - console.warn('[MessageHandler] 缺少会话信息:', data) - return false - } - - return true -} - -/** - * 构建标准 Message 对象 - */ -function buildStandardMessage(data: any, currentAccountId?: number): Message { - return { - id: data.id || Date.now(), - clientId: data.clientId || `msg_${Date.now()}`, - serverId: data.serverId, - sessionId: data.sessionId, - wechatAccountId: data.wechatAccountId || currentAccountId || 0, - msgType: data.msgType || 1, - content: data.content || '', - direction: data.isSend ? 'send' : 'receive', - isSend: data.isSend || false, - createTime: data.createTime || new Date().toISOString(), - timestamp: data.wechatTime || Date.now(), - status: data.status || 'success', - sender: data.sender, - } -} - -// ==================== 消息处理器 ==================== - -/** - * 处理新消息 - * CmdNewMessage / CmdReceiveMessage - */ -async function handleNewMessage(wsMessage: WebSocketMessage): Promise { - const data = wsMessage.data - - // 1. 验证数据 - if (!validateMessageData(data)) { - return - } - - // 2. 消息去重(如果启用) - if (data.clientId && (await MessageManager.checkDuplicate(data.clientId))) { - console.log('[MessageHandler] 重复消息,跳过:', data.clientId) - return - } - - console.log('[MessageHandler] 📨 收到新消息:', { - sessionId: data.sessionId, - type: data.sessionType, - content: data.content?.substring(0, 20) + '...', - }) - - try { - // 3. 更新会话(异步,不阻塞) - SessionManager.updateOnNewMessage( - data.sessionId, - data.sessionType, - data.content, - data.wechatAccountId - ).catch((error) => { - console.error('[MessageHandler] 更新会话失败:', error) - }) - - // 4. 保存消息记录(如果启用缓存) - if (MessageManager.shouldCacheMessage(data.sessionId)) { - const chatMessage = buildStandardMessage(data) - MessageManager.addMessage(chatMessage).catch((error) => { - console.error('[MessageHandler] 保存消息失败:', error) - }) - } - - // 5. 发布新消息事件(立即通知订阅者) - const message = buildStandardMessage(data) - emitNewMessage(message) - - console.log('[MessageHandler] ✅ 新消息处理完成') - } catch (error) { - console.error('[MessageHandler] ❌ 新消息处理失败:', error) - } -} - -/** - * 处理发送消息响应 - * CmdSendMessageResp - 只有发送消息才会有这个响应,包含 seq - */ -function handleSendMessageResp(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data) { - console.warn('[MessageHandler] 发送消息响应数据为空') - return - } - - // ⭐ CmdSendMessageResp 包含 seq(用于匹配)和服务器返回的消息ID - const { seq, friendMessage, chatroomMessage } = data - - if (!seq) { - console.warn('[MessageHandler] CmdSendMessageResp 缺少 seq:', data) - return - } - - // ⭐ 确定会话ID(从 friendMessage 或 chatroomMessage 中提取) - // 旧项目数据结构:{ seq, friendMessage: { wechatFriendId, id }, chatroomMessage: { wechatChatroomId, id } } - let targetSessionId: number | undefined - let isGroupChat = false - - if (friendMessage?.wechatFriendId) { - // 好友消息:使用 wechatFriendId 作为 sessionId - targetSessionId = - typeof friendMessage.wechatFriendId === 'string' - ? Number(friendMessage.wechatFriendId) - : friendMessage.wechatFriendId - isGroupChat = false - } else if (chatroomMessage?.wechatChatroomId) { - // 群聊消息:使用 wechatChatroomId 作为 sessionId(可能是 string,需要转换) - const chatroomId = - typeof chatroomMessage.wechatChatroomId === 'string' - ? Number(chatroomMessage.wechatChatroomId) - : chatroomMessage.wechatChatroomId - if (!isNaN(chatroomId)) { - targetSessionId = chatroomId - } - isGroupChat = true - } - - if (!targetSessionId) { - console.warn('[MessageHandler] CmdSendMessageResp 无法确定 sessionId:', { - seq, - friendMessage, - chatroomMessage, - fullData: data, - }) - return - } - - // ⭐ 通过 seq 找到之前发送的临时消息 - // ⚠️ 注意:发送消息时使用的 sessionId 可能是 session.id(对于群聊可能是 session.chatroomId) - // 但响应中的 sessionId 是 wechatFriendId 或 wechatChatroomId - // 我们需要尝试两种可能的 sessionId - const messageStore = useMessageStore() - let tempMessage = messageStore.findMessageBySeq(targetSessionId, seq) - - // ⭐ 如果没找到,尝试使用 session.id(因为发送时可能使用的是 session.id) - // 对于群聊,session.id 可能等于 chatroomId,也可能不等于 - if (!tempMessage) { - // 遍历所有会话,查找包含该 seq 的消息 - const allSessions = Array.from(messageStore.messages.keys()) - for (const sessionId of allSessions) { - tempMessage = messageStore.findMessageBySeq(sessionId, seq) - if (tempMessage) { - targetSessionId = sessionId // ⭐ 更新为实际找到的 sessionId - break - } - } - } - - if (tempMessage) { - // ⭐ 获取服务器返回的真实消息ID - const serverMessageId = friendMessage?.id || chatroomMessage?.id - - // ⭐ 通过事件机制更新消息状态(包含 sessionId) - emitMessageUpdate(seq, { - status: 'success', // 发送成功 - id: serverMessageId || tempMessage.id, // 使用服务器返回的真实ID - serverId: serverMessageId ? String(serverMessageId) : undefined, - sessionId: targetSessionId, // ⭐ 传递 sessionId,供订阅者使用 - }) - - console.log('[MessageHandler] ✅ 发送消息响应处理完成:', { - seq, - sessionId: targetSessionId, - serverMessageId, - tempMessageId: tempMessage.id, - }) - } else { - console.warn('[MessageHandler] ⚠️ 未找到对应的临时消息:', { - seq, - sessionId: targetSessionId, - friendMessage, - chatroomMessage, - }) - } -} - -/** - * 处理消息状态更新 - * CmdMessageStatus - 通用消息状态更新(不包含 seq) - */ -function handleMessageStatus(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data) { - console.warn('[MessageHandler] 消息状态数据为空') - return - } - - const { messageId, clientId, status, sessionId } = data - - // ⭐ CmdMessageStatus 不包含 seq,使用 messageId 或 clientId - const id = messageId || clientId - - if (!id) { - console.warn('[MessageHandler] 缺少消息标识:', data) - return - } - - // console.log('[MessageHandler] 📝 消息状态更新:', { - // id, - // status, - // sessionId, - // }) - - // 发布消息更新事件 - emitMessageUpdate(id, { - status: status || 'success', - serverId: data.serverId, - }) - - // 更新数据库(异步) - if (sessionId && MessageManager.shouldCacheMessage(sessionId)) { - MessageManager.updateMessageStatus(id, status || 'success').catch((error) => { - console.error('[MessageHandler] 更新消息状态失败:', error) - }) - } -} - -/** - * 处理消息撤回 - * CmdRecallMessage - */ -function handleMessageRecall(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.messageId || !data.sessionId) { - console.warn('[MessageHandler] 撤回消息数据不完整:', data) - return - } - - console.log('[MessageHandler] 🗑️ 消息撤回:', { - messageId: data.messageId, - sessionId: data.sessionId, - }) - - // 发布撤回事件 - emitMessageRecall(data.messageId, data.sessionId) - - // 更新数据库(异步) - if (MessageManager.shouldCacheMessage(data.sessionId)) { - MessageManager.markAsRecalled(data.messageId).catch((error) => { - console.error('[MessageHandler] 标记撤回失败:', error) - }) - } -} - -/** - * 处理发送消息结果 - * CmdSendMessageResult - */ -function handleSendMessageResult(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data) { - console.warn('[MessageHandler] 发送结果数据为空') - return - } - - console.log('[MessageHandler] 📤 发送消息结果:', { - messageId: data.friendMessageId || data.chatroomMessageId, - status: data.sendStatus, - }) - - // 更新消息状态 - const messageId = data.friendMessageId || data.chatroomMessageId - if (messageId) { - const status = data.sendStatus === 0 ? 'success' : 'failed' - emitMessageUpdate(messageId, { status }) - } -} - -// ==================== 导出处理器映射 ==================== - -export const messageHandlers: HandlerRegistry = { - // 接收新消息(不包含 seq) - [WS_CMD_TYPE.RECEIVE_MESSAGE]: handleNewMessage, - [WS_CMD_TYPE.NEW_MESSAGE]: handleNewMessage, // 别名 - - // ⭐ 发送消息响应(包含 seq,需要特殊处理) - [WS_CMD_TYPE.SEND_MESSAGE_RESP]: handleSendMessageResp, - - // 通用消息状态更新(不包含 seq) - [WS_CMD_TYPE.MESSAGE_STATUS]: handleMessageStatus, - - // 消息撤回 - [WS_CMD_TYPE.RECALL_MESSAGE]: handleMessageRecall, - - // 发送结果 - [WS_CMD_TYPE.SEND_MESSAGE_RESULT]: handleSendMessageResult, -} diff --git a/TouchVueThree/src/composables/business/wechat/handlers/sessionHandlers.ts b/TouchVueThree/src/composables/business/wechat/handlers/sessionHandlers.ts deleted file mode 100644 index ad6a271..0000000 --- a/TouchVueThree/src/composables/business/wechat/handlers/sessionHandlers.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 会话相关处理器 - * - * 负责处理: - * - 会话列表更新 - * - 会话置顶 - * - 未读数变化 - */ - -import type { WebSocketMessage } from '@/types/wechat' -import type { HandlerRegistry } from './index' -import { useMessageSubscription } from '../useMessageSubscription' -import { WS_CMD_TYPE } from '@/constants/wechat' -import { useSessionStore } from '@/stores/modules/wechat' - -const { emitSessionUpdate } = useMessageSubscription() - -// ==================== 会话处理器 ==================== - -/** - * 处理会话更新 - * CmdSessionUpdate - */ -function handleSessionUpdate(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.sessionId) { - console.warn('[SessionHandler] 会话更新数据不完整') - return - } - - console.log('[SessionHandler] 🔄 会话更新:', { - sessionId: data.sessionId, - type: data.type, - }) - - // 发布会话更新事件 - emitSessionUpdate(data.sessionId, data) - - // 更新 Store - try { - const sessionStore = useSessionStore() - if (data.type === 'pin') { - sessionStore.togglePin(data.sessionId) - } else if (data.type === 'mute') { - sessionStore.toggleMute(data.sessionId) - } - } catch (error) { - console.error('[SessionHandler] 更新 Store 失败:', error) - } -} - -/** - * 处理未读数清零 - * CmdClearUnread - */ -function handleClearUnread(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.sessionId) { - console.warn('[SessionHandler] 清除未读数据不完整') - return - } - - console.log('[SessionHandler] 🔔 清除未读:', { - sessionId: data.sessionId, - }) - - // 更新 Store - try { - const sessionStore = useSessionStore() - sessionStore.clearUnreadCount(data.sessionId) - } catch (error) { - console.error('[SessionHandler] 清除未读失败:', error) - } -} - -/** - * 处理会话删除 - * CmdSessionDelete - */ -function handleSessionDelete(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.sessionId) { - console.warn('[SessionHandler] 删除会话数据不完整') - return - } - - console.log('[SessionHandler] 🗑️ 删除会话:', { - sessionId: data.sessionId, - }) - - // 更新 Store - try { - const sessionStore = useSessionStore() - sessionStore.removeSession(data.sessionId) - } catch (error) { - console.error('[SessionHandler] 删除会话失败:', error) - } -} - -// ==================== 导出处理器映射 ==================== - -export const sessionHandlers: HandlerRegistry = { - // 会话更新 - [WS_CMD_TYPE.SESSION_UPDATE]: handleSessionUpdate, - - // 清除未读 - [WS_CMD_TYPE.CLEAR_UNREAD]: handleClearUnread, - - // 删除会话 - [WS_CMD_TYPE.SESSION_DELETE]: handleSessionDelete, -} diff --git a/TouchVueThree/src/composables/business/wechat/handlers/systemHandlers.ts b/TouchVueThree/src/composables/business/wechat/handlers/systemHandlers.ts deleted file mode 100644 index 2e9b101..0000000 --- a/TouchVueThree/src/composables/business/wechat/handlers/systemHandlers.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 系统通知处理器 - * - * 负责处理: - * - 系统通知 - * - 心跳响应 - * - 错误消息 - */ - -import type { WebSocketMessage } from '@/types/wechat' -import type { HandlerRegistry } from './index' -import { useMessageSubscription } from '../useMessageSubscription' -import { WS_CMD_TYPE } from '@/constants/wechat' -import { ElNotification } from 'element-plus' - -const { emitSystemNotification } = useMessageSubscription() - -// ==================== 系统处理器 ==================== - -/** - * 处理心跳响应 - * CmdHeartbeat / CmdPing - */ -function handleHeartbeat(wsMessage: WebSocketMessage): void { - console.log('[SystemHandler] 💓 心跳响应') - - // 更新最后活跃时间 - const lastActiveTime = Date.now() - localStorage.setItem('lastSyncTime', lastActiveTime.toString()) -} - -/** - * 处理系统通知 - * CmdSystemNotification - */ -function handleSystemNotification(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.type || !data.content) { - console.warn('[SystemHandler] 系统通知数据不完整') - return - } - - console.log('[SystemHandler] 📢 系统通知:', { - type: data.type, - content: data.content, - }) - - // 发布通知事件 - emitSystemNotification(data.type, data.content) - - // 显示桌面通知 - if (data.showNotification !== false) { - ElNotification({ - title: '系统通知', - message: data.content, - type: data.level || 'info', - duration: 5000, - }) - } -} - -/** - * 处理错误消息 - * CmdError - */ -function handleError(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data) { - console.warn('[SystemHandler] 错误消息数据为空') - return - } - - console.error('[SystemHandler] ❌ 错误消息:', { - code: data.code, - message: data.message, - }) - - // 显示错误通知 - ElNotification({ - title: '错误', - message: data.message || '发生未知错误', - type: 'error', - duration: 5000, - }) -} - -/** - * 处理服务器时间同步 - * CmdServerTime - */ -function handleServerTime(wsMessage: WebSocketMessage): void { - const data = wsMessage.data - - if (!data || !data.serverTime) { - return - } - - const serverTime = data.serverTime - const localTime = Date.now() - const timeDiff = serverTime - localTime - - console.log('[SystemHandler] 🕐 服务器时间:', { - serverTime: new Date(serverTime).toLocaleString(), - localTime: new Date(localTime).toLocaleString(), - diff: `${timeDiff}ms`, - }) - - // 保存时间差(用于时间校准) - localStorage.setItem('serverTimeDiff', timeDiff.toString()) -} - -// ==================== 导出处理器映射 ==================== - -export const systemHandlers: HandlerRegistry = { - // 心跳 - [WS_CMD_TYPE.HEARTBEAT]: handleHeartbeat, - [WS_CMD_TYPE.PING]: handleHeartbeat, // 别名 - - // 系统通知 - [WS_CMD_TYPE.SYSTEM_NOTIFICATION]: handleSystemNotification, - - // 错误消息 - [WS_CMD_TYPE.ERROR]: handleError, - - // 服务器时间 - [WS_CMD_TYPE.SERVER_TIME]: handleServerTime, -} diff --git a/TouchVueThree/src/composables/business/wechat/index.ts b/TouchVueThree/src/composables/business/wechat/index.ts deleted file mode 100644 index a0b692d..0000000 --- a/TouchVueThree/src/composables/business/wechat/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 微信业务 Composables 统一导出 - */ - -export { useWebSocket } from './useWebSocket' -export { useMessageSubscription, setupMessageSubscription } from './useMessageSubscription' -export { useAIRequestQueue } from './useAIRequestQueue' -export { useMessageParser } from './useMessageParser' - -// 导出 handlers -export { getMessageHandlerManager, createMessageHandler } from './handlers' -export type { MessageHandler, HandlerRegistry } from './handlers' diff --git a/TouchVueThree/src/composables/business/wechat/useAIRequestQueue.ts b/TouchVueThree/src/composables/business/wechat/useAIRequestQueue.ts deleted file mode 100644 index f6d34f2..0000000 --- a/TouchVueThree/src/composables/business/wechat/useAIRequestQueue.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * AI 请求队列管理 Composable - */ - -import { ref, onUnmounted } from 'vue' -import { debounce } from 'lodash-es' -import type { Message } from '@/types/wechat' -import { AI_REQUEST_DEBOUNCE } from '@/constants/wechat' -import { useAIStore } from '@/stores/modules/wechat' - -export function useAIRequestQueue(delay: number = AI_REQUEST_DEBOUNCE) { - const aiStore = useAIStore() - - // ==================== 状态 ==================== - - /** 消息队列 */ - const queue = ref([]) - - /** 是否正在处理 */ - const isProcessing = ref(false) - - /** 当前生成ID */ - const currentGenerationId = ref(null) - - // ==================== 防抖处理 ==================== - - /** - * 处理队列(防抖) - */ - const processQueue = debounce( - async (accountId: number, contactId: string) => { - if (queue.value.length === 0 || isProcessing.value) { - return - } - - isProcessing.value = true - - try { - const messages = [...queue.value] - queue.value = [] - - console.log('开始处理AI请求队列:', messages.length, '条消息') - - // 调用AI生成 - const response = await aiStore.generateReply({ - messages, - contactId, - accountId, - }) - - console.log('AI生成完成:', response.content) - - return response - } catch (error) { - console.error('AI生成失败:', error) - throw error - } finally { - isProcessing.value = false - currentGenerationId.value = null - } - }, - delay, - ) - - // ==================== 队列操作 ==================== - - /** - * 添加消息到队列 - */ - const addToQueue = (message: Message, accountId: number, contactId: string) => { - queue.value.push(message) - console.log('添加消息到AI队列:', message.id, '队列长度:', queue.value.length) - - // 触发防抖处理 - processQueue(accountId, contactId) - } - - /** - * 清空队列 - */ - const clearQueue = () => { - queue.value = [] - processQueue.cancel() - isProcessing.value = false - currentGenerationId.value = null - console.log('AI请求队列已清空') - } - - /** - * 立即处理队列(取消防抖) - */ - const processImmediately = async (accountId: number, contactId: string) => { - processQueue.cancel() - - if (queue.value.length === 0) { - return - } - - isProcessing.value = true - - try { - const messages = [...queue.value] - queue.value = [] - - const response = await aiStore.generateReply({ - messages, - contactId, - accountId, - }) - - return response - } catch (error) { - console.error('AI生成失败:', error) - throw error - } finally { - isProcessing.value = false - } - } - - /** - * 获取队列长度 - */ - const getQueueLength = () => queue.value.length - - /** - * 是否有待处理的消息 - */ - const hasPendingMessages = () => queue.value.length > 0 - - // ==================== 生命周期 ==================== - - onUnmounted(() => { - clearQueue() - }) - - // ==================== 返回 ==================== - - return { - // 状态 - queue, - isProcessing, - currentGenerationId, - - // 方法 - addToQueue, - clearQueue, - processQueue: processImmediately, - getQueueLength, - hasPendingMessages, - } -} diff --git a/TouchVueThree/src/composables/business/wechat/useMessageParser.ts b/TouchVueThree/src/composables/business/wechat/useMessageParser.ts deleted file mode 100644 index 5b9b9dc..0000000 --- a/TouchVueThree/src/composables/business/wechat/useMessageParser.ts +++ /dev/null @@ -1,300 +0,0 @@ -/** - * 消息解析 Composable - */ - -import { computed, type Ref } from 'vue' -import type { Message } from '@/types/wechat' -import { MESSAGE_TYPE, IMAGE_URL_REGEX, VIDEO_URL_REGEX, AUDIO_URL_REGEX } from '@/constants/wechat' - -export function useMessageParser(message: Ref) { - // ==================== 消息类型判断 ==================== - - /** - * 消息类型 - */ - const messageType = computed(() => { - const type = message.value.msgType - switch (type) { - case MESSAGE_TYPE.TEXT: - return 'text' - case MESSAGE_TYPE.IMAGE: - return 'image' - case MESSAGE_TYPE.VIDEO: - return 'video' - case MESSAGE_TYPE.AUDIO: - return 'audio' - case MESSAGE_TYPE.FILE: - return 'file' - case MESSAGE_TYPE.LOCATION: - return 'location' - case MESSAGE_TYPE.EMOJI: - return 'emoji' - case MESSAGE_TYPE.MINI_PROGRAM: - return 'miniProgram' - case MESSAGE_TYPE.RED_PACKET: - return 'redPacket' - case MESSAGE_TYPE.TRANSFER: - return 'transfer' - case MESSAGE_TYPE.SYSTEM: - case MESSAGE_TYPE.TIME_DIVIDER: - case MESSAGE_TYPE.RECOMMEND_REMARK: - case MESSAGE_TYPE.GROUP_INVITE: - return 'system' - default: - return 'unknown' - } - }) - - /** - * 是否是文本消息 - */ - const isTextMessage = computed(() => messageType.value === 'text') - - /** - * 是否是图片消息 - */ - const isImageMessage = computed(() => messageType.value === 'image') - - /** - * 是否是视频消息 - */ - const isVideoMessage = computed(() => messageType.value === 'video') - - /** - * 是否是语音消息 - */ - const isAudioMessage = computed(() => messageType.value === 'audio') - - /** - * 是否是文件消息 - */ - const isFileMessage = computed(() => messageType.value === 'file') - - /** - * 是否是位置消息 - */ - const isLocationMessage = computed(() => messageType.value === 'location') - - /** - * 是否是表情消息 - */ - const isEmojiMessage = computed(() => messageType.value === 'emoji') - - /** - * 是否是系统消息 - */ - const isSystemMessage = computed(() => messageType.value === 'system') - - /** - * 是否是自己发送的消息 - */ - const isOwnMessage = computed(() => message.value.isSend) - - /** - * 是否已撤回 - */ - const isRecalled = computed(() => message.value.isRecalled) - - // ==================== 消息内容解析 ==================== - - /** - * 解析的消息内容 - */ - const parsedContent = computed(() => { - try { - const content = message.value.content - - // 文件消息 - if (isFileMessage.value) { - const fileData = tryParseJSON(content) - if (fileData) { - return { - type: 'file', - url: fileData.url || content, - name: fileData.title || fileData.name || '文件', - size: fileData.size || 0, - ext: fileData.fileext || '', - } - } - } - - // 图片消息 - if (isImageMessage.value) { - return { - type: 'image', - url: content, - } - } - - // 视频消息 - if (isVideoMessage.value) { - const videoData = tryParseJSON(content) - if (videoData) { - return { - type: 'video', - url: videoData.url || content, - thumbUrl: videoData.thumbUrl, - duration: videoData.duration, - } - } - return { - type: 'video', - url: content, - } - } - - // 语音消息 - if (isAudioMessage.value) { - const audioData = tryParseJSON(content) - if (audioData) { - return { - type: 'audio', - url: audioData.url || content, - duration: audioData.duration || 0, - text: audioData.text, - } - } - return { - type: 'audio', - url: content, - duration: 0, - } - } - - // 位置消息 - if (isLocationMessage.value) { - const locationData = tryParseJSON(content) - if (locationData) { - return { - type: 'location', - label: locationData.label || '', - lat: locationData.lat || 0, - lng: locationData.lng || 0, - poiName: locationData.poiName, - } - } - } - - // 文本消息(默认) - return { - type: 'text', - text: content || '', - } - } catch (error) { - console.error('消息解析失败:', error) - return { - type: 'text', - text: message.value.content || '[消息解析失败]', - } - } - }) - - /** - * 消息预览文本(用于会话列表) - */ - const previewText = computed(() => { - if (isRecalled.value) { - return '[已撤回]' - } - - switch (messageType.value) { - case 'text': - return message.value.content - case 'image': - return '[图片]' - case 'video': - return '[视频]' - case 'audio': - return '[语音]' - case 'file': - return '[文件]' - case 'location': - return '[位置]' - case 'emoji': - return '[表情]' - case 'miniProgram': - return '[小程序]' - case 'redPacket': - return '[红包]' - case 'transfer': - return '[转账]' - case 'system': - return message.value.content - default: - return '[未知消息]' - } - }) - - // ==================== 辅助方法 ==================== - - /** - * 尝试解析JSON - */ - function tryParseJSON(str: string) { - try { - return JSON.parse(str) - } catch { - return null - } - } - - /** - * 判断是否是URL - */ - function isURL(str: string) { - try { - new URL(str) - return true - } catch { - return false - } - } - - /** - * 判断是否是图片URL - */ - function isImageURL(url: string) { - return IMAGE_URL_REGEX.test(url) - } - - /** - * 判断是否是视频URL - */ - function isVideoURL(url: string) { - return VIDEO_URL_REGEX.test(url) - } - - /** - * 判断是否是音频URL - */ - function isAudioURL(url: string) { - return AUDIO_URL_REGEX.test(url) - } - - // ==================== 返回 ==================== - - return { - // 消息类型 - messageType, - isTextMessage, - isImageMessage, - isVideoMessage, - isAudioMessage, - isFileMessage, - isLocationMessage, - isEmojiMessage, - isSystemMessage, - isOwnMessage, - isRecalled, - - // 解析内容 - parsedContent, - previewText, - - // 工具方法 - isURL, - isImageURL, - isVideoURL, - isAudioURL, - } -} diff --git a/TouchVueThree/src/composables/business/wechat/useMessageSubscription.ts b/TouchVueThree/src/composables/business/wechat/useMessageSubscription.ts deleted file mode 100644 index 72a5fd3..0000000 --- a/TouchVueThree/src/composables/business/wechat/useMessageSubscription.ts +++ /dev/null @@ -1,354 +0,0 @@ -/** - * WebSocket 消息订阅中心(发布订阅模式) - * - * 核心职责: - * - 解耦消息发送方(WebSocket)和接收方(组件/Store) - * - 支持多个订阅者 - * - 提供类型安全的事件系统 - * - * 使用场景: - * 1. 组件订阅:实时更新 UI - * 2. Store 订阅:更新全局状态 - * 3. Service 订阅:触发副作用(通知、音效等) - */ - -import mitt, { type Emitter } from 'mitt' -import type { Message } from '@/types/wechat' - -// ==================== 事件类型定义 ==================== - -type MessageEvents = { - // 新消息 - 'message:new': Message - - // 消息更新 - 'message:update': { - id: number | string - changes: Partial - } - - // 消息撤回 - 'message:recall': { - messageId: number - sessionId: number - } - - // 会话更新 - 'session:update': { - sessionId: number - changes: any - } - - // 账号状态 - 'account:status': { - accountId: number - isOnline: boolean - } - - // 系统通知 - 'system:notification': { - type: string - content: string - } -} - -// ==================== 调试工具 ==================== - -let eventEmitter: Emitter | null = null -let subscriptionCount = new Map() - -// 开发环境:记录订阅和发布 -const isDev = import.meta.env.DEV - -function logSubscription(event: keyof MessageEvents, action: 'subscribe' | 'unsubscribe') { - if (!isDev) return - - const count = subscriptionCount.get(event) || 0 - const newCount = action === 'subscribe' ? count + 1 : Math.max(0, count - 1) - subscriptionCount.set(event, newCount) - - console.log(`[MessageSubscription] ${action}: ${event}, 当前订阅数: ${newCount}`) -} - -function logEmit(event: keyof MessageEvents, data: any) { - if (!isDev) return - - const count = subscriptionCount.get(event) || 0 - console.log(`[MessageSubscription] emit: ${event}, 订阅者数: ${count}`, { - preview: JSON.stringify(data).substring(0, 100) + '...', - }) -} - -// ==================== 核心实现 ==================== - -/** - * 获取或创建事件发射器(单例) - */ -function getEmitter(): Emitter { - if (!eventEmitter) { - eventEmitter = mitt() - - // 开发环境:显示订阅信息 - if (isDev) { - console.log('[MessageSubscription] 事件中心已初始化') - } - } - return eventEmitter -} - -/** - * 消息订阅 Hook - * - * @example - * ```typescript - * // 在组件中使用 - * const { onNewMessage, emitNewMessage } = useMessageSubscription() - * - * // 订阅新消息 - * onNewMessage((message) => { - * console.log('收到新消息:', message) - * }) - * - * // 发布新消息(通常由 WebSocket 调用) - * emitNewMessage(message) - * ``` - */ -export function useMessageSubscription() { - const emitter = getEmitter() - - // ==================== 订阅方法(组件/Store 使用)==================== - - /** - * 订阅新消息 - */ - const onNewMessage = (handler: (message: Message) => void) => { - logSubscription('message:new', 'subscribe') - emitter.on('message:new', handler) - - // 返回取消订阅函数 - return () => { - logSubscription('message:new', 'unsubscribe') - emitter.off('message:new', handler) - } - } - - /** - * 订阅消息更新 - */ - const onMessageUpdate = ( - handler: (update: { id: number | string; changes: Partial }) => void - ) => { - logSubscription('message:update', 'subscribe') - emitter.on('message:update', handler) - - return () => { - logSubscription('message:update', 'unsubscribe') - emitter.off('message:update', handler) - } - } - - /** - * 订阅消息撤回 - */ - const onMessageRecall = (handler: (data: { messageId: number; sessionId: number }) => void) => { - logSubscription('message:recall', 'subscribe') - emitter.on('message:recall', handler) - - return () => { - logSubscription('message:recall', 'unsubscribe') - emitter.off('message:recall', handler) - } - } - - /** - * 订阅会话更新 - */ - const onSessionUpdate = (handler: (data: { sessionId: number; changes: any }) => void) => { - logSubscription('session:update', 'subscribe') - emitter.on('session:update', handler) - - return () => { - logSubscription('session:update', 'unsubscribe') - emitter.off('session:update', handler) - } - } - - /** - * 订阅账号状态 - */ - const onAccountStatus = (handler: (data: { accountId: number; isOnline: boolean }) => void) => { - logSubscription('account:status', 'subscribe') - emitter.on('account:status', handler) - - return () => { - logSubscription('account:status', 'unsubscribe') - emitter.off('account:status', handler) - } - } - - /** - * 订阅系统通知 - */ - const onSystemNotification = (handler: (data: { type: string; content: string }) => void) => { - logSubscription('system:notification', 'subscribe') - emitter.on('system:notification', handler) - - return () => { - logSubscription('system:notification', 'unsubscribe') - emitter.off('system:notification', handler) - } - } - - // ==================== 发布方法(WebSocket 使用)==================== - - /** - * 发布新消息 - */ - const emitNewMessage = (message: Message) => { - logEmit('message:new', message) - emitter.emit('message:new', message) - } - - /** - * 发布消息更新 - */ - const emitMessageUpdate = (id: number | string, changes: Partial) => { - logEmit('message:update', { id, changes }) - emitter.emit('message:update', { id, changes }) - } - - /** - * 发布消息撤回 - */ - const emitMessageRecall = (messageId: number, sessionId: number) => { - logEmit('message:recall', { messageId, sessionId }) - emitter.emit('message:recall', { messageId, sessionId }) - } - - /** - * 发布会话更新 - */ - const emitSessionUpdate = (sessionId: number, changes: any) => { - logEmit('session:update', { sessionId, changes }) - emitter.emit('session:update', { sessionId, changes }) - } - - /** - * 发布账号状态 - */ - const emitAccountStatus = (accountId: number, isOnline: boolean) => { - logEmit('account:status', { accountId, isOnline }) - emitter.emit('account:status', { accountId, isOnline }) - } - - /** - * 发布系统通知 - */ - const emitSystemNotification = (type: string, content: string) => { - logEmit('system:notification', { type, content }) - emitter.emit('system:notification', { type, content }) - } - - // ==================== 工具方法 ==================== - - /** - * 清除所有订阅(用于测试) - */ - const clearAll = () => { - emitter.all.clear() - subscriptionCount.clear() - console.log('[MessageSubscription] 已清除所有订阅') - } - - /** - * 获取订阅统计(用于调试) - */ - const getStats = () => { - return Object.fromEntries(subscriptionCount) - } - - return { - // 订阅 - onNewMessage, - onMessageUpdate, - onMessageRecall, - onSessionUpdate, - onAccountStatus, - onSystemNotification, - - // 发布 - emitNewMessage, - emitMessageUpdate, - emitMessageRecall, - emitSessionUpdate, - emitAccountStatus, - emitSystemNotification, - - // 工具 - clearAll, - getStats, - } -} - -/** - * 全局单例:在 Store 中统一订阅 - * - * @example - * ```typescript - * // stores/modules/wechat/useMessageStore.ts - * import { setupMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - * - * export const useMessageStore = defineStore('message', () => { - * const messages = ref([]) - * - * // 设置订阅 - * setupMessageSubscription({ - * onNewMessage: (msg) => { - * messages.value.push(msg) - * }, - * onMessageUpdate: ({ id, changes }) => { - * // 更新消息 - * } - * }) - * - * return { messages } - * }) - * ``` - */ -export function setupMessageSubscription(handlers: { - onNewMessage?: (message: Message) => void - onMessageUpdate?: (update: { id: number | string; changes: Partial }) => void - onMessageRecall?: (data: { messageId: number; sessionId: number }) => void - onSessionUpdate?: (data: { sessionId: number; changes: any }) => void - onAccountStatus?: (data: { accountId: number; isOnline: boolean }) => void - onSystemNotification?: (data: { type: string; content: string }) => void -}) { - const subscription = useMessageSubscription() - - // 自动设置所有订阅 - const unsubscribers: (() => void)[] = [] - - if (handlers.onNewMessage) { - unsubscribers.push(subscription.onNewMessage(handlers.onNewMessage)) - } - if (handlers.onMessageUpdate) { - unsubscribers.push(subscription.onMessageUpdate(handlers.onMessageUpdate)) - } - if (handlers.onMessageRecall) { - unsubscribers.push(subscription.onMessageRecall(handlers.onMessageRecall)) - } - if (handlers.onSessionUpdate) { - unsubscribers.push(subscription.onSessionUpdate(handlers.onSessionUpdate)) - } - if (handlers.onAccountStatus) { - unsubscribers.push(subscription.onAccountStatus(handlers.onAccountStatus)) - } - if (handlers.onSystemNotification) { - unsubscribers.push(subscription.onSystemNotification(handlers.onSystemNotification)) - } - - // 返回统一的清理函数 - return () => { - unsubscribers.forEach(unsub => unsub()) - } -} diff --git a/TouchVueThree/src/composables/business/wechat/useWebSocket.ts b/TouchVueThree/src/composables/business/wechat/useWebSocket.ts deleted file mode 100644 index c1092e9..0000000 --- a/TouchVueThree/src/composables/business/wechat/useWebSocket.ts +++ /dev/null @@ -1,537 +0,0 @@ -/** - * WebSocket 连接管理 Composable(重构版 - 集成 IndexedDB) - * - * 核心改造: - * ✅ 收到新消息 → 更新 IndexedDB(SessionManager.updateOnNewMessage) - * ✅ 会话不存在 → 自动获取好友详情并创建 - * ✅ 断线重连 → 增量同步遗漏消息 - * ✅ 心跳检测 → 记录最后同步时间 - * - * @author TouchVueThree Team - * @date 2026-01-13 - */ - -import { ref, onUnmounted } from 'vue' -import type { WebSocketConfig, WebSocketMessage, WebSocketStatus } from '@/types/wechat' -import { - WS_HEARTBEAT_INTERVAL, - WS_RECONNECT_INTERVAL, - WS_MAX_RECONNECT_ATTEMPTS, - WS_RECONNECT_BACKOFF_BASE, - WS_CMD_TYPE, -} from '@/constants/wechat' -import { useMessageSubscription } from './useMessageSubscription' -import { SessionManager } from '@/utils/dbManagers/SessionManager' -import { MessageManager } from '@/utils/dbManagers/MessageManager' -import { getMessageHandlerManager } from './handlers' -import { ElMessage } from 'element-plus' -import { useAccountStore } from '@/stores/modules/wechat' // ⭐ 导入账号 Store - -// 默认配置 -const DEFAULT_CONFIG: Partial = { - url: import.meta.env.VITE_API_WS_URL || 'ws://localhost:8080/ws', - client: 'kefu-client', - autoReconnect: true, - reconnectInterval: WS_RECONNECT_INTERVAL, - maxReconnectAttempts: WS_MAX_RECONNECT_ATTEMPTS, - heartbeatInterval: WS_HEARTBEAT_INTERVAL, -} - -// ⭐⭐⭐ 单例模式:全局共享 WebSocket 状态 ⭐⭐⭐ -const ws = ref(null) -const status = ref('disconnected') -const reconnectAttempts = ref(0) -const config = ref(null) - -// 定时器(全局) -let heartbeatTimer: NodeJS.Timeout | null = null -let reconnectTimer: NodeJS.Timeout | null = null -let heartbeatTimeoutTimer: NodeJS.Timeout | null = null -let aliveStatusTimer: NodeJS.Timeout | null = null // ⭐ 在线状态查询定时器 - -// 消息订阅(全局) -const { emitNewMessage, emitMessageUpdate } = useMessageSubscription() - -// ⭐ 消息处理器管理器(全局) -const handlerManager = getMessageHandlerManager() - -// ⭐ 最后同步时间(全局,用于断线重连后增量同步) -let lastSyncTime = Date.now() - -// ⭐ 在线状态查询相关(全局) -let lastAliveStatusRequest = 0 -const ALIVE_STATUS_REQUEST_INTERVAL = 5000 // 每 5 秒查询一次 - -/** - * WebSocket 管理 Composable(单例模式) - * - * ⚠️ 重要:此 composable 采用单例模式 - * - 所有组件共享同一个 WebSocket 连接 - * - 多次调用 useWebSocket() 返回的是同一个实例 - * - 只需在根组件(Chat/index.vue)调用 connect() 一次 - */ -export function useWebSocket() { - // ==================== 状态(单例共享) ==================== - // ws, status, reconnectAttempts, config 已在外部定义 - - // ==================== 计算属性 ==================== - - const isConnected = () => status.value === 'connected' - const isConnecting = () => status.value === 'connecting' - const isReconnecting = () => status.value === 'reconnecting' - - // ==================== 连接管理 ==================== - - /** - * 连接 WebSocket - */ - const connect = (userConfig: Partial) => { - // 如果已连接,先断开 - if (ws.value) { - disconnect() - } - - // 合并配置 - config.value = { - ...DEFAULT_CONFIG, - ...userConfig, - } as WebSocketConfig - - // 创建连接 - status.value = 'connecting' - const wsUrl = `${config.value.url}?token=${config.value.accessToken}&accountId=${config.value.accountId}` - - try { - ws.value = new WebSocket(wsUrl) - - // 绑定事件 - ws.value.onopen = handleOpen - ws.value.onmessage = handleMessage - ws.value.onclose = handleClose - ws.value.onerror = handleError - } catch (error) { - console.error('WebSocket 连接失败:', error) - status.value = 'error' - handleReconnect() - } - } - - /** - * 断开连接 - */ - const disconnect = () => { - stopHeartbeat() - stopReconnect() - stopAliveStatusQuery() // ⭐ 停止在线状态查询 - - if (ws.value) { - try { - ws.value.close() - } catch (error) { - console.error('WebSocket 关闭失败:', error) - } - ws.value = null - } - - status.value = 'disconnected' - reconnectAttempts.value = 0 - } - - /** - * 重连 - */ - const reconnect = () => { - if (!config.value || !config.value.autoReconnect) { - return - } - - if (reconnectAttempts.value >= config.value.maxReconnectAttempts) { - console.error('WebSocket 重连次数超限') - status.value = 'error' - ElMessage.error('连接失败,请刷新页面重试') - return - } - - status.value = 'reconnecting' - reconnectAttempts.value++ - - // 指数退避 - const delay = - config.value.reconnectInterval * Math.pow(WS_RECONNECT_BACKOFF_BASE, reconnectAttempts.value - 1) - - console.log(`WebSocket 将在 ${delay}ms 后重连(第 ${reconnectAttempts.value} 次)`) - - reconnectTimer = setTimeout(() => { - connect(config.value!) - }, delay) - } - - // ==================== 事件处理 ==================== - - /** - * 连接打开 - */ - const handleOpen = async () => { - console.log('✅ WebSocket 连接成功') - status.value = 'connected' - reconnectAttempts.value = 0 - - // 发送登录命令 - if (config.value) { - send({ - cmdType: WS_CMD_TYPE.SIGN_IN, - seq: Date.now(), - client: config.value.client, - accountId: config.value.accountId, - }) - } - - // ⭐ 重连后增量同步遗漏消息 - const now = Date.now() - if (now - lastSyncTime > 5000) { - // 断线超过 5 秒 - console.log('🔄 检测到连接中断,开始增量同步...') - await syncMissedMessages() - } - - // 更新最后同步时间 - lastSyncTime = now - localStorage.setItem('lastSyncTime', now.toString()) - - // 启动心跳 - startHeartbeat() - - // ⭐ 启动在线状态查询(延迟启动,确保账号列表已加载) - // 延迟 2 秒,给账号列表加载时间 - setTimeout(() => { - startAliveStatusQuery() - }, 2000) - } - - /** - * 接收消息(使用新的 Handler 架构) - */ - const handleMessage = async (event: MessageEvent) => { - try { - const message: WebSocketMessage = JSON.parse(event.data) - console.log('WebSocket 收到消息:', message) - - const { cmdType } = message - - // ⭐ 使用统一的 Handler 管理器处理消息 - await handlerManager.handle(cmdType, message) - - // 特殊处理:心跳响应(需要清除超时定时器) - if (cmdType === WS_CMD_TYPE.HEARTBEAT || cmdType === WS_CMD_TYPE.PING) { - if (heartbeatTimeoutTimer) { - clearTimeout(heartbeatTimeoutTimer) - heartbeatTimeoutTimer = null - } - lastSyncTime = Date.now() - localStorage.setItem('lastSyncTime', lastSyncTime.toString()) - } - } catch (error) { - console.error('WebSocket 消息解析失败:', error) - } - } - - /** - * 连接关闭 - */ - const handleClose = (event: CloseEvent) => { - console.log('WebSocket 连接关闭:', event.code, event.reason) - status.value = 'disconnected' - stopHeartbeat() - - // 非正常关闭,尝试重连 - if (event.code !== 1000 && config.value?.autoReconnect) { - handleReconnect() - } - } - - /** - * 连接错误 - */ - const handleError = (event: Event) => { - console.error('WebSocket 错误:', event) - status.value = 'error' - } - - /** - * 处理重连 - */ - const handleReconnect = () => { - reconnect() - } - - // ==================== 消息处理 ==================== - // 注:消息处理逻辑已移至 handlers/ 模块 - // 具体实现见: - // - messageHandlers.ts: 聊天消息处理 - // - accountHandlers.ts: 账号状态处理 - // - sessionHandlers.ts: 会话相关处理 - // - systemHandlers.ts: 系统通知处理 - - // ==================== 发送消息 ==================== - - /** - * 发送消息 - */ - const send = (message: WebSocketMessage) => { - if (!ws.value || ws.value.readyState !== WebSocket.OPEN) { - console.error('WebSocket 未连接') - return false - } - - try { - ws.value.send(JSON.stringify(message)) - return true - } catch (error) { - console.error('WebSocket 发送消息失败:', error) - return false - } - } - - /** - * 发送命令 - */ - const sendCommand = (cmdType: string, data?: any) => { - // ⭐ 使用旧项目的格式:直接展开 data 到顶层,不要包裹 - const message = { - cmdType, - ...data, // ✅ 展开 data,而不是 { data: {...} } - seq: Date.now(), - } - - console.log('[WebSocket] 📤 发送命令:', cmdType, message) - - const success = send(message) - - if (!success) { - console.error('[WebSocket] ❌ 发送命令失败:', cmdType) - } - - return success - } - - // ==================== 心跳 ==================== - - /** - * 启动心跳 - */ - const startHeartbeat = () => { - stopHeartbeat() - - heartbeatTimer = setInterval(() => { - if (isConnected()) { - sendCommand(WS_CMD_TYPE.HEARTBEAT) - - // ⭐ 设置心跳超时检测(5秒内未收到响应则重连) - heartbeatTimeoutTimer = setTimeout(() => { - console.warn('⚠️ 心跳超时,准备重连...') - ws.value?.close() - handleReconnect() - }, 5000) - } - }, config.value?.heartbeatInterval || WS_HEARTBEAT_INTERVAL) - } - - /** - * 停止心跳 - */ - const stopHeartbeat = () => { - if (heartbeatTimer) { - clearInterval(heartbeatTimer) - heartbeatTimer = null - } - if (heartbeatTimeoutTimer) { - clearTimeout(heartbeatTimeoutTimer) - heartbeatTimeoutTimer = null - } - } - - /** - * 停止重连 - */ - const stopReconnect = () => { - if (reconnectTimer) { - clearTimeout(reconnectTimer) - reconnectTimer = null - } - } - - // ==================== 在线状态查询 ==================== - - /** - * 请求账号在线状态 - */ - const requestAliveStatus = () => { - if (!isConnected()) { - console.warn('[WebSocket] ⚠️ WebSocket 未连接,跳过在线状态查询') - return - } - - const accountStore = useAccountStore() - const now = Date.now() - - // 限流:避免频繁请求 - if (now - lastAliveStatusRequest < ALIVE_STATUS_REQUEST_INTERVAL) { - console.log('[WebSocket] ⏸️ 在线状态查询限流中,跳过本次请求') - return - } - - const accountIds = accountStore.accountList.map((acc) => acc.id) - - if (accountIds.length === 0) { - console.log('[WebSocket] ⚠️ 账号列表为空,跳过在线状态查询') - return - } - - // 发送查询命令 - const success = sendCommand('CmdRequestWechatAccountsAliveStatus', { - wechatAccountIds: accountIds, - }) - - if (success) { - lastAliveStatusRequest = now - console.log('[WebSocket] 📡 请求账号在线状态:', accountIds) - } else { - console.error('[WebSocket] ❌ 发送在线状态查询命令失败') - } - } - - /** - * 启动在线状态查询定时器 - */ - const startAliveStatusQuery = () => { - stopAliveStatusQuery() - - if (!isConnected()) { - console.warn('[WebSocket] ⚠️ WebSocket 未连接,无法启动在线状态查询') - return - } - - const accountStore = useAccountStore() - const accountIds = accountStore.accountList.map((acc) => acc.id) - - if (accountIds.length === 0) { - console.warn('[WebSocket] ⚠️ 账号列表为空,延迟启动在线状态查询') - // 如果账号列表为空,延迟 3 秒再试 - setTimeout(() => { - startAliveStatusQuery() - }, 3000) - return - } - - console.log('[WebSocket] ✅ 启动在线状态查询定时器,账号数量:', accountIds.length) - - // 立即请求一次 - requestAliveStatus() - - // 启动定时器,每5秒查询一次 - aliveStatusTimer = setInterval(() => { - if (isConnected()) { - requestAliveStatus() - } else { - console.warn('[WebSocket] ⚠️ WebSocket 已断开,停止在线状态查询') - stopAliveStatusQuery() - } - }, ALIVE_STATUS_REQUEST_INTERVAL) - } - - /** - * 停止在线状态查询定时器 - */ - const stopAliveStatusQuery = () => { - if (aliveStatusTimer) { - clearInterval(aliveStatusTimer) - aliveStatusTimer = null - console.log('[WebSocket] ⏹ 停止在线状态查询定时器') - } - } - - // ==================== 增量同步(防数据丢失)==================== - - /** - * 增量同步遗漏消息 - * 断线重连后调用,拉取断线期间的消息 - */ - const syncMissedMessages = async () => { - try { - const savedLastSyncTime = localStorage.getItem('lastSyncTime') - const syncFrom = savedLastSyncTime ? parseInt(savedLastSyncTime) : lastSyncTime - - console.log(`📥 开始增量同步,从 ${new Date(syncFrom).toLocaleString()} 开始`) - - // TODO: 调用增量同步接口 - // const missedMessages = await getMessagesSince({ - // wechatAccountId: config.value?.accountId, - // since: syncFrom, - // }) - - // 模拟:这里需要后端提供增量同步接口 - // 格式:GET /api/messages/since?timestamp=xxx&accountId=xxx - // 返回:{ list: Message[], total: number } - - // 处理遗漏的消息 - // for (const message of missedMessages) { - // await handleNewMessage({ cmdType: WS_CMD_TYPE.RECEIVE_MESSAGE, data: message }) - // } - - console.log('✅ 增量同步完成') - } catch (error) { - console.error('❌ 增量同步失败:', error) - // 不阻断连接流程 - } - } - - // ==================== 生命周期 ==================== - - onUnmounted(() => { - disconnect() - }) - - // ==================== 初始化 ==================== - - /** - * 初始化:恢复最后同步时间 - */ - const initLastSyncTime = () => { - const saved = localStorage.getItem('lastSyncTime') - if (saved) { - lastSyncTime = parseInt(saved) - console.log(`📅 恢复最后同步时间: ${new Date(lastSyncTime).toLocaleString()}`) - } - } - - // 自动初始化 - initLastSyncTime() - - // ==================== 返回 ==================== - - return { - // 状态 - ws, - status, - reconnectAttempts, - config, - - // 计算属性 - isConnected, - isConnecting, - isReconnecting, - - // 方法 - connect, - disconnect, - reconnect, - send, - sendCommand, - syncMissedMessages, // 暴露增量同步方法 - - // ⭐ 在线状态查询 - requestAliveStatus, - startAliveStatusQuery, - stopAliveStatusQuery, - } -} diff --git a/TouchVueThree/src/constants/wechat.ts b/TouchVueThree/src/constants/wechat.ts deleted file mode 100644 index 8fdb880..0000000 --- a/TouchVueThree/src/constants/wechat.ts +++ /dev/null @@ -1,302 +0,0 @@ -/** - * 微信相关常量定义 - */ - -import { MessageType, AIType as AITypeEnum } from '@/types/wechat' - -// ==================== 消息类型 ==================== - -/** 消息类型常量 */ -export const MESSAGE_TYPE = { - TEXT: 1, // 文本 - IMAGE: 3, // 图片 - AUDIO: 34, // 语音 - VIDEO: 43, // 视频 - EMOJI: 47, // 表情 - LOCATION: 48, // 位置 - FILE: 49, // 文件 - LINK: 49, // 链接 - MINI_PROGRAM: 4901, // 小程序 - RED_PACKET: 4902, // 红包 - TRANSFER: 4903, // 转账 - SYSTEM: 10000, // 系统消息 - TIME_DIVIDER: -10001, // 时间分隔 - RECALL: 10002, // 撤回消息 - RECOMMEND_REMARK: 570425393, // 推荐备注 - GROUP_INVITE: 90000, // 群邀请 -} as const - -/** 系统消息类型列表 */ -export const SYSTEM_MESSAGE_TYPES = [ - MESSAGE_TYPE.SYSTEM, - MESSAGE_TYPE.TIME_DIVIDER, - MESSAGE_TYPE.RECALL, - MESSAGE_TYPE.RECOMMEND_REMARK, - MESSAGE_TYPE.GROUP_INVITE, -] - -/** 媒体消息类型列表 */ -export const MEDIA_MESSAGE_TYPES = [ - MESSAGE_TYPE.IMAGE, - MESSAGE_TYPE.VIDEO, - MESSAGE_TYPE.AUDIO, - MESSAGE_TYPE.FILE, -] - -// ==================== AI 类型 ==================== - -/** AI 模式常量 */ -export const AI_TYPE = { - MANUAL: 0, // 人工接待 - ASSIST: 1, // AI辅助 - TAKEOVER: 2, // AI接管 -} as const - -/** AI 模式选项 */ -export const AI_TYPE_OPTIONS = [ - { value: AI_TYPE.MANUAL, label: '人工接待', icon: 'User' }, - { value: AI_TYPE.ASSIST, label: 'AI辅助', icon: 'MagicStick' }, - { value: AI_TYPE.TAKEOVER, label: 'AI接管', icon: 'Cpu' }, -] as const - -// ==================== 文件类型 ==================== - -/** 图片文件扩展名 */ -export const IMAGE_EXTENSIONS = [ - 'jpg', - 'jpeg', - 'png', - 'gif', - 'webp', - 'bmp', - 'svg', - 'ico', -] as const - -/** 视频文件扩展名 */ -export const VIDEO_EXTENSIONS = [ - 'mp4', - 'avi', - 'mov', - 'wmv', - 'flv', - 'mkv', - 'webm', - '3gp', - 'rmvb', - 'mpeg', - 'mpg', -] as const - -/** 音频文件扩展名 */ -export const AUDIO_EXTENSIONS = [ - 'mp3', - 'wav', - 'ogg', - 'aac', - 'm4a', - 'flac', - 'wma', - 'amr', - 'silk', -] as const - -/** 文档文件扩展名 */ -export const DOCUMENT_EXTENSIONS = [ - 'pdf', - 'doc', - 'docx', - 'xls', - 'xlsx', - 'ppt', - 'pptx', - 'txt', - 'md', - 'csv', -] as const - -/** 压缩文件扩展名 */ -export const ARCHIVE_EXTENSIONS = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'] as const - -/** 所有支持的文件扩展名 */ -export const ALL_FILE_EXTENSIONS = [ - ...IMAGE_EXTENSIONS, - ...VIDEO_EXTENSIONS, - ...AUDIO_EXTENSIONS, - ...DOCUMENT_EXTENSIONS, - ...ARCHIVE_EXTENSIONS, -] as const - -// ==================== 文件大小限制 ==================== - -/** 文件大小限制(MB) */ -export const FILE_SIZE_LIMITS = { - IMAGE: 10, // 图片 10MB - VIDEO: 100, // 视频 100MB - AUDIO: 10, // 音频 10MB - FILE: 100, // 文件 100MB -} as const - -// ==================== WebSocket 命令类型 ==================== - -/** WebSocket 命令类型 */ -export const WS_CMD_TYPE = { - // ==================== 连接相关 ==================== - SIGN_IN: 'CmdSignIn', // 登录 - SIGN_IN_RESP: 'CmdSignInResp', // 登录响应 - HEARTBEAT: 'CmdHeartbeat', // 心跳 - PING: 'CmdPing', // Ping(心跳别名) - - // ==================== 消息发送 ==================== - // ⚠️ 注意:实际发送消息时使用 'CmdSendMessage' 命令(旧项目格式) - // 通过 msgType 字段区分消息类型(1=文本, 3=图片, 34=语音, 48=位置, 49=文件等) - SEND_MESSAGE: 'CmdSendMessage', // 发送消息(统一命令) - SEND_TEXT: 'CmdSendTextMsg', // 发送文本消息(未使用) - SEND_IMAGE: 'CmdSendImageMsg', // 发送图片消息(未使用) - SEND_VIDEO: 'CmdSendVideoMsg', // 发送视频消息(未使用) - SEND_AUDIO: 'CmdSendAudioMsg', // 发送语音消息(未使用) - SEND_FILE: 'CmdSendFileMsg', // 发送文件消息(未使用) - SEND_LOCATION: 'CmdSendLocationMsg', // 发送位置消息(未使用) - SEND_MESSAGE_RESP: 'CmdSendMessageResp', // 发送消息响应 - SEND_MESSAGE_RESULT: 'CmdSendMessageResult', // 发送消息结果 - - // ==================== 消息接收 ==================== - RECEIVE_MESSAGE: 'CmdReceiveMessage', // 接收消息 - NEW_MESSAGE: 'CmdNewMessage', // 新消息(别名) - MESSAGE_STATUS: 'CmdMessageStatus', // 消息状态 - RECALL_MESSAGE: 'CmdRecallMsg', // 撤回消息 - - // ==================== 会话管理 ==================== - SESSION_UPDATE: 'CmdSessionUpdate', // 会话更新 - SESSION_DELETE: 'CmdSessionDelete', // 会话删除 - CLEAR_UNREAD: 'CmdClearUnread', // 清除未读 - SYNC_MESSAGE: 'CmdSyncMessage', // 同步消息 - - // ==================== 账号状态 ==================== - ACCOUNT_STATUS: 'CmdAccountStatus', // 账号状态 - ACCOUNT_ALIVE_STATUS_RESP: 'CmdRequestWechatAccountsAliveStatusResp', // 账号在线状态响应 - ACCOUNT_LIST_UPDATE: 'CmdAccountListUpdate', // 账号列表更新 - - // ==================== 系统通知 ==================== - SYSTEM_NOTIFICATION: 'CmdSystemNotification', // 系统通知 - ERROR: 'CmdError', // 错误消息 - SERVER_TIME: 'CmdServerTime', // 服务器时间同步 -} as const - -// ==================== 时间格式 ==================== - -/** 时间格式常量 */ -export const TIME_FORMAT = { - FULL: 'YYYY-MM-DD HH:mm:ss', - DATE: 'YYYY-MM-DD', - TIME: 'HH:mm:ss', - SHORT: 'MM-DD HH:mm', - MINUTE: 'HH:mm', -} as const - -// ==================== 分页 ==================== - -/** 默认分页参数 */ -export const DEFAULT_PAGE_SIZE = 20 - -/** 消息加载页大小 */ -export const MESSAGE_PAGE_SIZE = 50 - -/** 联系人加载页大小 */ -export const CONTACT_PAGE_SIZE = 100 - -// ==================== 防抖/节流时间 ==================== - -/** AI 请求防抖时间(毫秒) */ -export const AI_REQUEST_DEBOUNCE = 3000 - -/** 消息批量处理延迟(毫秒) */ -export const MESSAGE_BATCH_DELAY = 16 - -/** 搜索防抖时间(毫秒) */ -export const SEARCH_DEBOUNCE = 300 - -/** 滚动节流时间(毫秒) */ -export const SCROLL_THROTTLE = 100 - -// ==================== WebSocket 配置 ==================== - -/** 心跳间隔(毫秒) */ -export const WS_HEARTBEAT_INTERVAL = 30000 - -/** 重连间隔(毫秒) */ -export const WS_RECONNECT_INTERVAL = 3000 - -/** 最大重连次数 */ -export const WS_MAX_RECONNECT_ATTEMPTS = 5 - -/** 重连指数退避基数 */ -export const WS_RECONNECT_BACKOFF_BASE = 1.5 - -// ==================== 虚拟滚动 ==================== - -/** 虚拟滚动预渲染数量 */ -export const VIRTUAL_SCROLL_OVERSCAN = 5 - -/** 预估消息项高度 */ -export const ESTIMATED_MESSAGE_HEIGHT = 80 - -/** 预估系统消息高度 */ -export const ESTIMATED_SYSTEM_MESSAGE_HEIGHT = 30 - -/** 预估时间分隔高度 */ -export const ESTIMATED_TIME_DIVIDER_HEIGHT = 40 - -// ==================== 缓存 ==================== - -/** 消息缓存最大数量 */ -export const MAX_CACHED_MESSAGES = 1000 - -/** 会话缓存最大数量 */ -export const MAX_CACHED_SESSIONS = 100 - -/** 联系人缓存最大数量 */ -export const MAX_CACHED_CONTACTS = 500 - -// ==================== 正则表达式 ==================== - -/** URL 正则 */ -export const URL_REGEX = /^https?:\/\//i - -/** 图片 URL 正则 */ -export const IMAGE_URL_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i - -/** 视频 URL 正则 */ -export const VIDEO_URL_REGEX = /\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i - -/** 音频 URL 正则 */ -export const AUDIO_URL_REGEX = /\.(mp3|wav|ogg|aac|m4a)$/i - -/** 文件 URL 正则 */ -export const FILE_URL_REGEX = /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i - -/** 手机号正则 */ -export const PHONE_REGEX = /^1[3-9]\d{9}$/ - -/** 微信号正则 */ -export const WECHAT_ID_REGEX = /^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/ - -// ==================== 其他 ==================== - -/** 表情包路径前缀 */ -export const EMOJI_PATH_PREFIX = '/assets/face/' - -/** 表情包扩展名 */ -export const EMOJI_EXTENSION = '.png' - -/** 默认头像 */ -export const DEFAULT_AVATAR = '/assets/default-avatar.png' - -/** 默认群聊头像 */ -export const DEFAULT_GROUP_AVATAR = '/assets/default-group-avatar.png' - -/** 本地存储键前缀 */ -export const STORAGE_KEY_PREFIX = 'wechat_' - -/** 消息批量上传数量 */ -export const MESSAGE_BATCH_UPLOAD_SIZE = 50 diff --git a/TouchVueThree/src/env.d.ts b/TouchVueThree/src/env.d.ts deleted file mode 100644 index 9efb2db..0000000 --- a/TouchVueThree/src/env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/// - -declare module '*.vue' { - import type { DefineComponent } from 'vue' - const component: DefineComponent<{}, {}, any> - export default component -} - -// Element Plus 全局类型 -declare module 'element-plus/global' - -// 环境变量类型声明 -interface ImportMetaEnv { - readonly VITE_APP_TITLE: string - readonly VITE_API_BASE_URL: string - // 更多环境变量... -} - -interface ImportMeta { - readonly env: ImportMetaEnv -} diff --git a/TouchVueThree/src/layouts/MainLayout.vue b/TouchVueThree/src/layouts/MainLayout.vue deleted file mode 100644 index 799e824..0000000 --- a/TouchVueThree/src/layouts/MainLayout.vue +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - - - - - - - - - 发朋友圈 - - - - {{ pageTitle }} - - - - - - - - - - {{ user?.tokens || 0 }} - - - - - - - - - - - - - - - {{ user?.username || '用户' }} - 高级客服专员 - - - - - - - {{ user?.account }} - - - - 系统设置 - - - 清除缓存 - - - 退出登录 - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/layouts/PowerLayout.vue b/TouchVueThree/src/layouts/PowerLayout.vue deleted file mode 100644 index d75d209..0000000 --- a/TouchVueThree/src/layouts/PowerLayout.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - {{ backButtonText }} - - - {{ title }} - {{ subtitle }} - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/layouts/index.ts b/TouchVueThree/src/layouts/index.ts deleted file mode 100644 index 147c48b..0000000 --- a/TouchVueThree/src/layouts/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 布局组件统一导出 - */ - -export { default as MainLayout } from './MainLayout.vue' -export { default as PowerLayout } from './PowerLayout.vue' diff --git a/TouchVueThree/src/main.ts b/TouchVueThree/src/main.ts deleted file mode 100644 index c1ee226..0000000 --- a/TouchVueThree/src/main.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createApp } from 'vue' -import { createPinia } from 'pinia' -import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' -import ElementPlus from 'element-plus' -import zhCn from 'element-plus/es/locale/lang/zh-cn' -import App from './App.vue' -import router from './router' -import { useUserStore } from './stores/modules/user' - -// 样式导入 -import 'element-plus/dist/index.css' -import './assets/styles/global.scss' - -// ==================== 创建应用实例 ==================== -const app = createApp(App) - -// ==================== 初始化 Pinia ==================== -const pinia = createPinia() -pinia.use(piniaPluginPersistedstate) -app.use(pinia) - -// ==================== 初始化用户信息 ==================== -const userStore = useUserStore() -// 异步初始化用户信息和数据库(不阻塞应用启动) -userStore.initUser().catch((error) => { - console.error('初始化用户信息失败:', error) -}) - -// ==================== 配置 Element Plus ==================== -app.use(ElementPlus, { - locale: zhCn, - size: 'default', -}) - -// ==================== 使用路由 ==================== -app.use(router) - -// ==================== 挂载应用 ==================== -app.mount('#app') diff --git a/TouchVueThree/src/router/guards.ts b/TouchVueThree/src/router/guards.ts deleted file mode 100644 index d795993..0000000 --- a/TouchVueThree/src/router/guards.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Router } from 'vue-router' -import { useUserStore } from '@/stores/modules/user' -import { ElMessage } from 'element-plus' - -/** - * 设置路由守卫 - */ -export function setupRouterGuard(router: Router) { - router.beforeEach((to, from, next) => { - const userStore = useUserStore() - - // 设置页面标题 - if (to.meta.title) { - document.title = `${to.meta.title} - 触客宝` - } - - // 检查是否需要登录 - if (to.meta.requiresAuth) { - if (!userStore.isLoggedIn) { - ElMessage.warning('请先登录') - next({ - path: '/login', - query: { redirect: to.fullPath }, - }) - return - } - } - - // 如果已登录,访问登录页则跳转到首页 - if (to.path === '/login' && userStore.isLoggedIn) { - next('/chat') - return - } - - next() - }) - - router.afterEach(() => { - // 路由切换后的处理 - }) -} diff --git a/TouchVueThree/src/router/index.ts b/TouchVueThree/src/router/index.ts deleted file mode 100644 index be7c647..0000000 --- a/TouchVueThree/src/router/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' -import { setupRouterGuard } from './guards' - -// ==================== 路由配置 ==================== -const routes: RouteRecordRaw[] = [ - { - path: '/', - name: 'Index', - redirect: '/chat', - }, - { - path: '/login', - name: 'Login', - component: () => import('@views/Login/index.vue'), - meta: { - requiresAuth: false, - title: '登录', - layout: 'blank', - }, - }, - { - path: '/chat', - name: 'Chat', - component: () => import('@views/Chat/index.vue'), - meta: { - requiresAuth: true, - title: '聊天', - layout: 'main', - }, - }, - { - path: '/dashboard', - name: 'Dashboard', - component: () => import('@views/Dashboard/index.vue'), - meta: { - requiresAuth: true, - title: '数据看板', - layout: 'main', - }, - }, - { - path: '/settings', - name: 'Settings', - component: () => import('@views/Settings/index.vue'), - meta: { - requiresAuth: true, - title: '系统设置', - layout: 'main', - }, - }, - { - path: '/power-center', - name: 'PowerCenter', - redirect: '/power-center/customer-management', - meta: { - requiresAuth: true, - title: '能力中心', - layout: 'main', - }, - children: [ - { - path: 'customer-management', - name: 'CustomerManagement', - component: () => import('@views/PowerCenter/CustomerManagement/index.vue'), - meta: { - requiresAuth: true, - title: '客户管理', - layout: 'power', - }, - }, - { - path: 'content-management', - name: 'ContentManagement', - component: () => import('@views/PowerCenter/ContentManagement/index.vue'), - meta: { - requiresAuth: true, - title: '内容管理', - layout: 'power', - }, - }, - { - path: 'data-statistics', - name: 'DataStatistics', - component: () => import('@views/PowerCenter/DataStatistics/index.vue'), - meta: { - requiresAuth: true, - title: '数据统计', - layout: 'power', - }, - }, - { - path: 'ai-training', - name: 'AiTraining', - component: () => import('@views/PowerCenter/AiTraining/index.vue'), - meta: { - requiresAuth: true, - title: 'AI 训练', - layout: 'power', - }, - }, - ], - }, - { - path: '/:pathMatch(.*)*', - name: 'NotFound', - component: () => import('@views/404/index.vue'), - meta: { - title: '404', - }, - }, -] - -// ==================== 创建路由实例 ==================== -const router = createRouter({ - history: createWebHistory(), - routes, - scrollBehavior(to, from, savedPosition) { - if (savedPosition) { - return savedPosition - } else { - return { top: 0 } - } - }, -}) - -// ==================== 设置路由守卫 ==================== -setupRouterGuard(router) - -export default router diff --git a/TouchVueThree/src/stores/index.ts b/TouchVueThree/src/stores/index.ts deleted file mode 100644 index 0304588..0000000 --- a/TouchVueThree/src/stores/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Store 统一导出 - -export { useUserStore } from './modules/user' -export type { User } from './modules/user' - -// 微信模块 Stores -export { - useAccountStore, - useContactStore, - useSessionStore, - useMessageStore, - useAIStore, - useUIStore, -} from './modules/wechat' - -// 后续添加其他 Store 时在这里导出 -// export { useAppStore } from './modules/app' -// export { useWebSocketStore } from './modules/websocket' diff --git a/TouchVueThree/src/stores/modules/user.ts b/TouchVueThree/src/stores/modules/user.ts deleted file mode 100644 index e14019a..0000000 --- a/TouchVueThree/src/stores/modules/user.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import { loginWithPassword, loginWithCode } from '@/api' -import router from '@/router' -import { ElMessage } from 'element-plus' -import { databaseManager, checkBrowserSupport } from '@/utils/db' - -// ==================== 类型定义 ==================== -export interface User { - id: number - account: string - username: string - phone: string - avatar: string - isAdmin: number - companyId: number - typeId: number - status: number - s2_accountId: string - createTime: string - updateTime: string | null - tokens: number - lastLoginIp: string - lastLoginTime: number -} - -interface LoginResponse { - token: string - member: User - kefuData?: { - self: any - token: { - access_token: string - } - } -} - -// ==================== Store 定义 ==================== -export const useUserStore = defineStore('user', () => { - // ==================== State ==================== - const user = ref(null) - const token = ref('') - const token2 = ref('') // WebSocket token - - // ==================== Getters ==================== - const isLoggedIn = computed(() => !!token.value && !!user.value) - const isAdmin = computed(() => user.value?.isAdmin === 1) - const userId = computed(() => user.value?.id || 0) - const accountId = computed(() => user.value?.s2_accountId || '') - - // ==================== Actions ==================== - - /** - * 设置用户信息 - */ - const setUser = (userData: User) => { - user.value = userData - } - - /** - * 设置 Token - */ - const setToken = (tokenValue: string) => { - token.value = tokenValue - if (tokenValue) { - localStorage.setItem('token', tokenValue) - } else { - localStorage.removeItem('token') - } - } - - /** - * 设置 Token2(WebSocket token) - */ - const setToken2 = (token2Value: string) => { - token2.value = token2Value - if (token2Value) { - localStorage.setItem('token2', token2Value) - } else { - localStorage.removeItem('token2') - } - } - - /** - * 登录(密码登录或验证码登录) - */ - const login = async ( - params: { - account: string - password?: string - verificationCode?: string - verifyCode?: string - verifySessionId?: string - typeId?: number - }, - isPasswordLogin: boolean = true - ) => { - try { - // ⚠️ 登录前:检查浏览器兼容性 - const browserSupport = checkBrowserSupport() - if (!browserSupport.supported) { - ElMessage.warning(browserSupport.message) - // 降级:继续登录,但提示用户某些功能可能不可用 - } - - const loginParams = { - ...params, - verifySessionId: params.verifySessionId || '', - typeId: params.typeId || 1, - } - - const response = (isPasswordLogin - ? await loginWithPassword(loginParams) - : await loginWithCode(loginParams)) as LoginResponse - - const { member, kefuData } = response - - // 清空 WebSocket 连接状态(如果需要,后续添加 WebSocket Store 时启用) - // useWebSocketStore.getState().clearConnectionState() - - // 设置用户信息和 token - setToken(response.token) - setUser(member) - - // 设置 token2(触客宝 token) - if (kefuData?.token?.access_token) { - setToken2(kefuData.token.access_token) - } - - // 设置触客宝用户信息(如果需要,后续添加 CkChat Store 时启用) - // if (kefuData?.self) { - // useCkChatStore.getState().setUserInfo(kefuData.self) - // } - - // ⭐ 关键:初始化数据库(一号一库) - try { - await databaseManager.ensureDatabase(member.id) - console.log('✅ 数据库初始化成功') - } catch (dbError) { - console.error('❌ 数据库初始化失败:', dbError) - ElMessage.warning('本地数据库初始化失败,部分功能可能不可用') - // 不阻断登录流程,降级到纯API模式 - } - - ElMessage.success('登录成功') - - // 跳转到首页 - const redirect = router.currentRoute.value.query.redirect as string - router.push(redirect || '/chat') - } catch (error) { - console.error('登录失败:', error) - throw error - } - } - - /** - * 退出登录 - * - * @param skipRouterPush 是否跳过路由跳转(用于统一的 handleLoginExpired 中) - */ - const logout = async (skipRouterPush = false) => { - console.log('[UserStore] 🚪 开始退出登录') - - try { - // ⭐ 1. 关闭 WebSocket 连接 - try { - // 动态导入避免循环依赖 - const { useWebSocket } = await import('@/composables/business/wechat') - const { disconnect } = useWebSocket() - disconnect() - console.log('[UserStore] ✅ WebSocket 已关闭') - } catch (error) { - console.error('[UserStore] ❌ 关闭 WebSocket 失败:', error) - } - - // ⭐ 2. 关闭数据库连接 - try { - await databaseManager.closeCurrentDatabase() - console.log('[UserStore] ✅ 数据库已关闭') - } catch (error) { - console.error('[UserStore] ❌ 关闭数据库失败:', error) - } - } catch (error) { - console.error('[UserStore] ❌ 关闭资源失败:', error) - } - - // ⭐ 3. 清除本地存储 - localStorage.removeItem('token') - localStorage.removeItem('token2') - localStorage.removeItem('user-store') - - // ⭐ 4. 清除状态 - user.value = null - token.value = '' - token2.value = '' - - // ⭐ 5. 跳转到登录页(可选) - if (!skipRouterPush) { - router.push('/login') - } - - console.log('[UserStore] ✅ 退出登录完成') - } - - /** - * 初始化用户信息(从本地存储恢复) - */ - const initUser = async () => { - const savedToken = localStorage.getItem('token') - const savedToken2 = localStorage.getItem('token2') - const savedUser = localStorage.getItem('user-store') - - if (savedToken) { - token.value = savedToken - } - - if (savedToken2) { - token2.value = savedToken2 - } - - if (savedUser) { - try { - const parsed = JSON.parse(savedUser) - if (parsed.state?.user) { - user.value = parsed.state.user - - // ⭐ 如果用户已登录,初始化数据库 - if (user.value?.id) { - try { - await databaseManager.ensureDatabase(user.value.id) - console.log('✅ 数据库已初始化(从本地存储恢复)') - } catch (error) { - console.error('初始化数据库失败:', error) - // 不阻断用户使用,降级到纯API模式 - } - } - } - } catch (error) { - console.warn('恢复用户信息失败:', error) - } - } - } - - return { - // State - user, - token, - token2, - - // Getters - isLoggedIn, - isAdmin, - userId, - accountId, - - // Actions - setUser, - setToken, - setToken2, - login, - logout, - initUser, - } -}, { - // 持久化配置 - persist: { - key: 'user-store', - storage: localStorage, - paths: ['user', 'token', 'token2'], - }, -}) diff --git a/TouchVueThree/src/stores/modules/wechat/index.ts b/TouchVueThree/src/stores/modules/wechat/index.ts deleted file mode 100644 index 0e56ebd..0000000 --- a/TouchVueThree/src/stores/modules/wechat/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 微信模块 Store 统一导出 - */ - -export { useAccountStore } from './useAccountStore' -export { useContactStore } from './useContactStore' -export { useSessionStore } from './useSessionStore' -export { useMessageStore } from './useMessageStore' -export { useAIStore } from './useAIStore' -export { useUIStore } from './useUIStore' -export { useChatWindowStore } from './useChatWindowStore' - -// 导出类型 -export type * from '@/types/wechat' diff --git a/TouchVueThree/src/stores/modules/wechat/useAIStore.ts b/TouchVueThree/src/stores/modules/wechat/useAIStore.ts deleted file mode 100644 index 50d807e..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useAIStore.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * AI功能管理 Store - */ - -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import type { AIConfig, AIGenerateRequest, AIGenerateResponse, Message } from '@/types/wechat' -import { AI_TYPE } from '@/constants/wechat' -import { aiChat, dataProcessing } from '@/api' -import { nanoid } from 'nanoid' - -export const useAIStore = defineStore('wechat-ai', () => { - // ==================== 状态 ==================== - - /** AI配置(按联系人ID) */ - const aiConfigs = ref>(new Map()) - - /** 是否正在生成AI回复 */ - const isGenerating = ref(false) - - /** 当前生成ID(用于取消) */ - const currentGenerationId = ref(null) - - /** 生成的内容(流式传输时) */ - const generatedContent = ref('') - - /** 生成进度 */ - const generationProgress = ref(0) - - // ==================== 计算属性 ==================== - - /** - * 获取联系人的AI配置 - */ - const getAIConfig = computed(() => (contactId: string) => { - return aiConfigs.value.get(contactId) || createDefaultConfig(contactId) - }) - - /** - * 是否启用AI - */ - const isAIEnabled = computed(() => (contactId: string) => { - const config = aiConfigs.value.get(contactId) - return config?.enabled ?? false - }) - - /** - * 获取AI类型 - */ - const getAIType = computed(() => (contactId: string) => { - const config = aiConfigs.value.get(contactId) - return config?.type ?? AI_TYPE.MANUAL - }) - - // ==================== Actions ==================== - - /** - * 更新AI配置 - */ - const updateAIConfig = async (contactId: string, updates: Partial) => { - const currentConfig = aiConfigs.value.get(contactId) || createDefaultConfig(contactId) - const newConfig = { ...currentConfig, ...updates } - aiConfigs.value.set(contactId, newConfig) - - return newConfig - } - - /** - * 设置AI类型 - */ - const setAIType = async (contactId: string, type: number) => { - await updateAIConfig(contactId, { type }) - } - - /** - * 启用/禁用AI - */ - const toggleAI = async (contactId: string) => { - const currentConfig = aiConfigs.value.get(contactId) || createDefaultConfig(contactId) - await updateAIConfig(contactId, { enabled: !currentConfig.enabled }) - } - - /** - * 生成AI回复 - */ - const generateReply = async (request: AIGenerateRequest): Promise => { - // 生成唯一ID - const generationId = nanoid() - currentGenerationId.value = generationId - isGenerating.value = true - generatedContent.value = '' - generationProgress.value = 0 - - try { - // 准备消息上下文(最近10条消息) - const context = request.messages.slice(-10).map((msg) => ({ - role: msg.isSend ? 'assistant' : 'user', - content: msg.content, - })) - - // 调用AI接口 - const response = await aiChat({ - messages: context, - accountId: request.accountId, - contactId: request.contactId, - customPrompt: request.customPrompt, - }) - - // 检查是否被取消 - if (currentGenerationId.value !== generationId) { - throw new Error('生成已取消') - } - - generatedContent.value = response.content - generationProgress.value = 100 - - return response - } catch (error) { - console.error('AI生成失败:', error) - throw error - } finally { - // 只有当前生成ID匹配时才清理 - if (currentGenerationId.value === generationId) { - isGenerating.value = false - currentGenerationId.value = null - } - } - } - - /** - * 手动触发AI生成 - */ - const manualTriggerAI = async ( - contactId: string, - accountId: number, - messages: Message[], - ): Promise => { - try { - const response = await generateReply({ - messages, - contactId, - accountId, - }) - - return response.content - } catch (error) { - console.error('手动触发AI失败:', error) - throw error - } - } - - /** - * 停止生成 - */ - const stopGeneration = () => { - currentGenerationId.value = null - isGenerating.value = false - generatedContent.value = '' - generationProgress.value = 0 - } - - /** - * 数据处理(用于AI学习) - */ - const processData = async (data: { - accountId: number - contactId: string - messages: Message[] - }) => { - try { - await dataProcessing({ - accountId: data.accountId, - contactId: data.contactId, - messages: data.messages.map((msg) => ({ - content: msg.content, - msgType: msg.msgType, - isSend: msg.isSend, - timestamp: msg.timestamp, - })), - }) - } catch (error) { - console.error('数据处理失败:', error) - throw error - } - } - - /** - * 批量加载AI配置 - */ - const loadAIConfigs = async (contactIds: string[]) => { - // TODO: 从后端批量加载AI配置 - console.log('加载AI配置:', contactIds) - } - - /** - * 重置状态 - */ - const reset = () => { - aiConfigs.value.clear() - isGenerating.value = false - currentGenerationId.value = null - generatedContent.value = '' - generationProgress.value = 0 - } - - // ==================== 返回 ==================== - - return { - // 状态 - aiConfigs, - isGenerating, - currentGenerationId, - generatedContent, - generationProgress, - - // 计算属性 - getAIConfig, - isAIEnabled, - getAIType, - - // Actions - updateAIConfig, - setAIType, - toggleAI, - generateReply, - manualTriggerAI, - stopGeneration, - processData, - loadAIConfigs, - reset, - } -}) - -/** - * 创建默认AI配置 - */ -function createDefaultConfig(contactId: string): AIConfig { - return { - contactId, - type: AI_TYPE.MANUAL, - enabled: false, - autoReply: false, - replyDelay: 3000, - } -} diff --git a/TouchVueThree/src/stores/modules/wechat/useAccountStore.ts b/TouchVueThree/src/stores/modules/wechat/useAccountStore.ts deleted file mode 100644 index 740012b..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useAccountStore.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * 微信账号管理 Store - */ - -import { defineStore } from 'pinia' -import { ref, computed, onMounted } from 'vue' -import type { WeChatAccount } from '@/types/wechat' -import { getCustomerList } from '@/api' -import { useMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - -export const useAccountStore = defineStore( - 'wechat-account', - () => { - // ==================== 状态 ==================== - - /** 账号列表 */ - const accountList = ref([]) - - /** 当前选中的账号 */ - const currentAccount = ref(null) - - /** 未读消息数(按账号ID) */ - const unreadCounts = ref>(new Map()) - - /** 加载状态 */ - const loading = ref(false) - - // ==================== 订阅 WebSocket 事件 ==================== - - const { onAccountStatus } = useMessageSubscription() - - // 订阅账号在线状态变化 - onAccountStatus(({ accountId, isOnline }) => { - console.log('[AccountStore] 📡 收到在线状态变化:', { accountId, isOnline }) - updateOnlineStatus(accountId, isOnline) - }) - - // ==================== 计算属性 ==================== - - /** 在线账号列表 */ - const onlineAccounts = computed(() => accountList.value.filter((acc) => acc.isOnline)) - - /** 离线账号列表 */ - const offlineAccounts = computed(() => - accountList.value.filter((acc) => !acc.isOnline), - ) - - /** 总未读数 */ - const totalUnreadCount = computed(() => { - return Array.from(unreadCounts.value.values()).reduce((sum, count) => sum + count, 0) - }) - - /** 当前账号未读数 */ - const currentUnreadCount = computed(() => { - if (!currentAccount.value) return 0 - return unreadCounts.value.get(currentAccount.value.id) || 0 - }) - - // ==================== Actions ==================== - - /** - * 加载账号列表 - */ - const loadAccounts = async () => { - loading.value = true - try { - const response = await getCustomerList() - accountList.value = response - - // 如果还没有选中账号,选中第一个 - if (!currentAccount.value && accountList.value.length > 0) { - currentAccount.value = accountList.value[0] - } - - return accountList.value - } catch (error) { - console.error('加载账号列表失败:', error) - throw error - } finally { - loading.value = false - } - } - - /** - * 切换账号 - */ - const switchAccount = (accountId: number) => { - const account = accountList.value.find((acc) => acc.id === accountId) - if (account) { - currentAccount.value = account - return true - } - return false - } - - /** - * 切换到全部账号(显示所有账号的消息) - */ - const switchToAllAccounts = () => { - currentAccount.value = { - id: 0, - name: '全部', - avatar: '', - isOnline: true, - } - } - - /** - * 更新账号信息 - */ - const updateAccount = (accountId: number, updates: Partial) => { - const index = accountList.value.findIndex((acc) => acc.id === accountId) - if (index !== -1) { - accountList.value[index] = { ...accountList.value[index], ...updates } - - // 如果更新的是当前账号,也更新当前账号 - if (currentAccount.value?.id === accountId) { - currentAccount.value = accountList.value[index] - } - } - } - - /** - * 设置账号未读数 - */ - const setUnreadCount = (accountId: number, count: number) => { - unreadCounts.value.set(accountId, count) - } - - /** - * 增加账号未读数 - */ - const increaseUnreadCount = (accountId: number, delta = 1) => { - const current = unreadCounts.value.get(accountId) || 0 - unreadCounts.value.set(accountId, current + delta) - } - - /** - * 清除账号未读数 - */ - const clearUnreadCount = (accountId: number) => { - unreadCounts.value.set(accountId, 0) - } - - /** - * 获取账号未读数 - */ - const getUnreadCount = (accountId: number) => { - return unreadCounts.value.get(accountId) || 0 - } - - /** - * 更新账号在线状态 - */ - const updateOnlineStatus = (accountId: number, isOnline: boolean) => { - updateAccount(accountId, { isOnline }) - } - - /** - * 批量更新账号在线状态 - * @param aliveStatus 在线状态对象 { accountId: isOnline } - */ - const updateAccountsOnlineStatus = (aliveStatus: Record) => { - Object.entries(aliveStatus).forEach(([accountId, isOnline]) => { - updateOnlineStatus(Number(accountId), isOnline) - }) - - // 按在线状态排序:在线的排在前面 - accountList.value.sort((a, b) => { - if (a.isOnline && !b.isOnline) return -1 - if (!a.isOnline && b.isOnline) return 1 - return 0 - }) - - console.log('[AccountStore] ✅ 批量更新在线状态完成:', { - total: Object.keys(aliveStatus).length, - online: Object.values(aliveStatus).filter(Boolean).length, - }) - } - - /** - * 重置状态 - */ - const reset = () => { - accountList.value = [] - currentAccount.value = null - unreadCounts.value.clear() - loading.value = false - } - - // ==================== 返回 ==================== - - return { - // 状态 - accountList, - currentAccount, - unreadCounts, - loading, - - // 计算属性 - onlineAccounts, - offlineAccounts, - totalUnreadCount, - currentUnreadCount, - - // Actions - loadAccounts, - switchAccount, - switchToAllAccounts, - updateAccount, - setUnreadCount, - increaseUnreadCount, - clearUnreadCount, - getUnreadCount, - updateOnlineStatus, - updateAccountsOnlineStatus, - reset, - } - }, - { - persist: { - paths: ['currentAccount'], - }, - }, -) diff --git a/TouchVueThree/src/stores/modules/wechat/useChatWindowStore.ts b/TouchVueThree/src/stores/modules/wechat/useChatWindowStore.ts deleted file mode 100644 index 6f90d98..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useChatWindowStore.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * 聊天窗口 Store - * 管理聊天窗口的 UI 状态 - */ - -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import { ElMessage } from 'element-plus' -import { useSessionStore } from './useSessionStore' - -export const useChatWindowStore = defineStore('chat-window', () => { - // ==================== UI 状态 ==================== - - /** 是否显示个人资料卡 */ - const showProfileCard = ref(true) - - /** 是否显示聊天记录搜索 */ - const showChatHistory = ref(false) - - /** 是否显示跟进提醒弹窗 */ - const showFollowUpReminder = ref(false) - - /** 是否显示待办事项弹窗 */ - const showTodoList = ref(false) - - // ==================== 输入框状态 ==================== - - /** 当前输入内容 */ - const inputContent = ref('') - - /** 待插入的内容(用于快捷语等功能) */ - const pendingInsertContent = ref('') - - /** 草稿箱(按会话ID存储) */ - const drafts = ref>(new Map()) - - // ==================== AI 配置 ==================== - - /** AI 模式类型(按会话ID存储) */ - const aiTypes = ref>(new Map()) - - // ==================== 计算属性 ==================== - - /** - * 当前会话的 AI 类型 - */ - const currentAIType = computed(() => { - const sessionStore = useSessionStore() - const sessionId = sessionStore.currentSession?.id - if (!sessionId) return 0 - - return aiTypes.value.get(sessionId) || 0 - }) - - /** - * 当前会话的草稿 - */ - const currentDraft = computed({ - get: () => { - const sessionStore = useSessionStore() - const sessionId = sessionStore.currentSession?.id - if (!sessionId) return '' - - return drafts.value.get(sessionId) || '' - }, - set: (value: string) => { - const sessionStore = useSessionStore() - const sessionId = sessionStore.currentSession?.id - if (!sessionId) return - - if (value) { - drafts.value.set(sessionId, value) - } else { - drafts.value.delete(sessionId) - } - }, - }) - - // ==================== UI 操作 ==================== - - /** - * 切换个人资料卡显示状态 - */ - const toggleProfileCard = () => { - showProfileCard.value = !showProfileCard.value - console.log(`👤 个人资料卡: ${showProfileCard.value ? '显示' : '隐藏'}`) - } - - /** - * 打开聊天记录搜索 - */ - const openChatHistory = () => { - showChatHistory.value = true - console.log('🔍 打开聊天记录搜索') - } - - /** - * 关闭聊天记录搜索 - */ - const closeChatHistory = () => { - showChatHistory.value = false - console.log('❌ 关闭聊天记录搜索') - } - - /** - * 打开跟进提醒弹窗 - */ - const openFollowUpReminder = () => { - showFollowUpReminder.value = true - console.log('🔔 打开跟进提醒') - } - - /** - * 关闭跟进提醒弹窗 - */ - const closeFollowUpReminder = () => { - showFollowUpReminder.value = false - console.log('❌ 关闭跟进提醒') - } - - /** - * 打开待办事项弹窗 - */ - const openTodoList = () => { - showTodoList.value = true - console.log('✅ 打开待办事项') - } - - /** - * 关闭待办事项弹窗 - */ - const closeTodoList = () => { - showTodoList.value = false - console.log('❌ 关闭待办事项') - } - - // ==================== 输入框操作 ==================== - - /** - * 插入文本到输入框 - * - * @param content 要插入的内容 - */ - const insertText = (content: string) => { - pendingInsertContent.value = content - console.log(`📝 插入文本到输入框: ${content.substring(0, 50)}...`) - } - - /** - * 清空待插入内容 - */ - const clearPendingInsert = () => { - pendingInsertContent.value = '' - } - - /** - * 保存草稿 - * - * @param sessionId 会话ID - * @param content 草稿内容 - */ - const saveDraft = (sessionId: number, content: string) => { - if (content) { - drafts.value.set(sessionId, content) - console.log(`💾 保存草稿: sessionId=${sessionId}`) - } else { - drafts.value.delete(sessionId) - } - } - - /** - * 加载草稿 - * - * @param sessionId 会话ID - * @returns 草稿内容 - */ - const loadDraft = (sessionId: number): string => { - return drafts.value.get(sessionId) || '' - } - - /** - * 清空草稿 - * - * @param sessionId 会话ID - */ - const clearDraft = (sessionId: number) => { - drafts.value.delete(sessionId) - console.log(`🧹 清空草稿: sessionId=${sessionId}`) - } - - // ==================== AI 配置 ==================== - - /** - * 更新 AI 类型 - * - * @param sessionId 会话ID - * @param type AI 类型 - */ - const updateAIType = async (sessionId: number, type: 0 | 1 | 2) => { - try { - // TODO: 调用 API 更新 AI 配置 - // await setFriendInjectConfig({ - // type, - // wechatAccountId: session.wechatAccountId, - // friendId: sessionId, - // }) - - aiTypes.value.set(sessionId, type) - - const labels = ['人工接待', 'AI辅助', 'AI接管'] - ElMessage.success(`已切换为${labels[type]}`) - console.log(`🤖 更新 AI 类型: sessionId=${sessionId}, type=${type}`) - } catch (error) { - console.error('更新 AI 配置失败:', error) - ElMessage.error('配置更新失败') - throw error - } - } - - /** - * 获取 AI 类型 - * - * @param sessionId 会话ID - * @returns AI 类型 - */ - const getAIType = (sessionId: number): 0 | 1 | 2 => { - return aiTypes.value.get(sessionId) || 0 - } - - /** - * 清空所有状态(用于登出) - */ - const clearAll = () => { - showProfileCard.value = true - showChatHistory.value = false - showFollowUpReminder.value = false - showTodoList.value = false - inputContent.value = '' - drafts.value.clear() - aiTypes.value.clear() - console.log('🧹 清空聊天窗口状态') - } - - // ==================== 返回 ==================== - - return { - // UI 状态 - showProfileCard, - showChatHistory, - showFollowUpReminder, - showTodoList, - - // 输入框状态 - inputContent, - pendingInsertContent, - currentDraft, - - // AI 配置 - currentAIType, - - // UI 操作 - toggleProfileCard, - openChatHistory, - closeChatHistory, - openFollowUpReminder, - closeFollowUpReminder, - openTodoList, - closeTodoList, - - // 输入框操作 - insertText, - clearPendingInsert, - saveDraft, - loadDraft, - clearDraft, - - // AI 配置 - updateAIType, - getAIType, - - // 其他 - clearAll, - } -}) diff --git a/TouchVueThree/src/stores/modules/wechat/useContactStore.ts b/TouchVueThree/src/stores/modules/wechat/useContactStore.ts deleted file mode 100644 index 9e6c860..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useContactStore.ts +++ /dev/null @@ -1,908 +0,0 @@ -/** - * 联系人管理 Store(重构版 - 分组懒加载架构) - * - * 核心特性: - * ✅ 分组懒加载 - 点击展开时才加载联系人 - * ✅ API搜索 - 并行请求好友和群列表 - * ✅ 缓存策略 - 本地缓存提升体验 - * ✅ 账号切换 - 支持多账号切换 - * - * @author TouchVueThree Team - */ - -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import type { Contact, ContactGroup, ContactType, GroupContactData } from '@/types/wechat' -import { - getContactList, - getGroupList, - getContactGroups, - getLabelsListByGroup, - updateFriendInfo, - moveGroup, - addGroup, - updateGroup, - deleteGroup, - setFriendInjectConfig as updateContactAiTypeAPI, - getFriendDetail, - getGroupDetail, -} from '@/api' -import { useAccountStore } from './useAccountStore' -import { useSessionStore } from './useSessionStore' -import { SessionManager } from '@/utils/dbManagers/SessionManager' -import { ElMessage } from 'element-plus' - -// ==================== 缓存工具 ==================== - -const CACHE_TTL = 60 * 60 * 1000 // 1小时 - -/** - * 从缓存读取数据 - */ -async function getFromCache(key: string): Promise { - try { - const cached = localStorage.getItem(`contact_cache_${key}`) - if (!cached) return null - - const { data, timestamp } = JSON.parse(cached) - if (Date.now() - timestamp > CACHE_TTL) { - localStorage.removeItem(`contact_cache_${key}`) - return null - } - - return data - } catch { - return null - } -} - -/** - * 保存数据到缓存 - */ -async function setToCache(key: string, data: T): Promise { - try { - localStorage.setItem( - `contact_cache_${key}`, - JSON.stringify({ - data, - timestamp: Date.now(), - }) - ) - } catch (error) { - console.warn('缓存保存失败:', error) - } -} - -/** - * 生成分组Key - */ -function getGroupKey(groupId: number, groupType: 1 | 2, accountId: number): string { - return `${groupId}_${groupType}_${accountId}` -} - -/** - * 排序分组列表 - */ -function sortGroups(groups: ContactGroup[]): ContactGroup[] { - return [...groups].sort((a, b) => { - // 默认群分组(groupType=2 且 id=0)始终在最前 - const isDefaultGroupA = a.groupType === 2 && a.id === 0 - const isDefaultGroupB = b.groupType === 2 && b.id === 0 - if (isDefaultGroupA && !isDefaultGroupB) return -1 - if (!isDefaultGroupA && isDefaultGroupB) return 1 - - // 未分组(groupType=1 且 id=0)始终在最后 - const isUngroupedA = a.groupType === 1 && a.id === 0 - const isUngroupedB = b.groupType === 1 && b.id === 0 - if (isUngroupedA && !isUngroupedB) return 1 - if (!isUngroupedA && isUngroupedB) return -1 - - // 其他按 sort 排序 - return (a.sort || 0) - (b.sort || 0) - }) -} - -export const useContactStore = defineStore('wechat-contact', () => { - // ==================== 状态 ==================== - - /** 分组列表 */ - const groups = ref([]) - - /** 展开的分组(Set) */ - const expandedGroups = ref>(new Set()) - - /** 分组数据(Map) */ - const groupData = ref>(new Map()) - - /** 当前选中的账号ID(0表示全部) */ - const selectedAccountId = ref(0) - - /** 搜索关键词 */ - const searchKeyword = ref('') - - /** 是否搜索模式 */ - const isSearchMode = ref(false) - - /** 搜索结果 */ - const searchResults = ref([]) - - /** 搜索加载状态 */ - const searchLoading = ref(false) - - /** 加载状态 */ - const loading = ref(false) - - // ==================== 计算属性 ==================== - - /** 所有联系人(从分组数据中提取) */ - const allContacts = computed(() => { - const contacts: Contact[] = [] - groupData.value.forEach((data) => { - contacts.push(...data.contacts) - }) - return contacts - }) - - /** 筛选后的联系人(搜索模式) */ - const filteredContacts = computed(() => { - if (isSearchMode.value) { - return searchResults.value - } - return allContacts.value - }) - - // ==================== Actions ==================== - - /** - * 加载分组列表(带缓存) - */ - const loadGroups = async (accountId?: number) => { - const targetAccountId = accountId ?? selectedAccountId.value - - // 1. 先尝试从缓存读取 - const cacheKey = `groups_${targetAccountId}` - const cached = await getFromCache(cacheKey) - if (cached && cached.length > 0) { - groups.value = cached - // 后台更新 - loadGroupsFromAPI(targetAccountId).catch(console.error) - return cached - } - - // 2. 无缓存,调用API - return await loadGroupsFromAPI(targetAccountId) - } - - /** - * 从API加载分组列表 - */ - const loadGroupsFromAPI = async (accountId: number) => { - try { - loading.value = true - const params: any = { - page: 1, - limit: 1000, // 分组列表通常不会太多 - } - // 即便为 0 也要把 wechatAccountId 传给接口,由后端决定含义 - if (accountId !== undefined) { - params.wechatAccountId = accountId - } - const response: any = await getLabelsListByGroup(params) - const list = response?.list || response?.data || response || [] - - // 转换为ContactGroup格式 - const friendGroups: ContactGroup[] = list - .filter((item: any) => Number(item.groupType) === 1) - .map((item: any) => ({ - id: item.id, - groupName: item.groupName, - groupType: 1, - count: item.count || -1, // -1表示未加载 - sort: item.sort || 0, - groupMemo: item.groupMemo || '', - })) - - const groupGroups: ContactGroup[] = list - .filter((item: any) => Number(item.groupType) === 2) - .map((item: any) => ({ - id: item.id, - groupName: item.groupName, - groupType: 2, - count: item.count || -1, - sort: item.sort || 0, - groupMemo: item.groupMemo || '', - })) - - // 补充默认分组 - const extraGroups: ContactGroup[] = [] - - const hasDefaultGroup = groupGroups.some((g) => g.id === 0 && g.groupType === 2) - if (!hasDefaultGroup) { - extraGroups.push({ - id: 0, - groupName: '默认群分组', - groupType: 2, - count: 0, - sort: -9999, - groupMemo: '', - }) - } - - const hasUngrouped = friendGroups.some((g) => g.id === 0 && g.groupType === 1) - if (!hasUngrouped) { - extraGroups.push({ - id: 0, - groupName: '未分组', - groupType: 1, - count: 0, - sort: 999999, - groupMemo: '', - }) - } - - const allGroups = [...extraGroups, ...friendGroups, ...groupGroups] - const sortedGroups = sortGroups(allGroups) - groups.value = sortedGroups - - // 缓存 - await setToCache(`groups_${accountId}`, sortedGroups) - return sortedGroups - } catch (error) { - console.error('加载分组列表失败:', error) - ElMessage.error('加载分组列表失败') - throw error - } finally { - loading.value = false - } - } - - /** - * 切换分组展开/折叠(懒加载) - */ - const toggleGroup = async (groupId: number, groupType: 1 | 2) => { - const accountId = selectedAccountId.value - const groupKey = getGroupKey(groupId, groupType, accountId) - const expanded = new Set(expandedGroups.value) - - if (expanded.has(groupKey)) { - // 折叠 - expanded.delete(groupKey) - expandedGroups.value = expanded - } else { - // 展开 - 懒加载 - expanded.add(groupKey) - expandedGroups.value = expanded - await loadGroupContacts(groupId, groupType, 1, 50) - } - } - - /** - * 加载分组联系人(懒加载,带缓存) - */ - const loadGroupContacts = async ( - groupId: number, - groupType: 1 | 2, - page: number = 1, - limit: number = 50 - ) => { - const accountId = selectedAccountId.value - const groupKey = getGroupKey(groupId, groupType, accountId) - - // 第一页时,先尝试从缓存读取 - if (page === 1) { - const cacheKey = `groupContacts_${groupKey}` - const cached = await getFromCache(cacheKey) - if (cached && cached.contacts.length > 0) { - const data = new Map(groupData.value) - data.set(groupKey, { ...cached, loading: false }) - groupData.value = data - - // 后台更新 - loadGroupContactsFromAPI(groupId, groupType, page, limit).catch(console.error) - return - } - } - - // 设置loading状态(保留现有联系人数据,只更新loading状态) - const data = new Map(groupData.value) - const existing = data.get(groupKey) - data.set(groupKey, { - contacts: existing?.contacts || [], // ⭐ 保留现有联系人,不重置 - page: existing?.page || page, - pageSize: limit, - hasMore: existing?.hasMore ?? true, - loading: true, // 只更新loading状态 - loaded: existing?.loaded || false, - }) - groupData.value = data - - // 调用API - await loadGroupContactsFromAPI(groupId, groupType, page, limit) - } - - /** - * 从API加载分组联系人 - */ - const loadGroupContactsFromAPI = async ( - groupId: number, - groupType: 1 | 2, - page: number, - limit: number - ) => { - const accountId = selectedAccountId.value - const groupKey = getGroupKey(groupId, groupType, accountId) - - try { - let contacts: Contact[] = [] - let total = 0 - - const params: any = { - page, - limit, - } - - // 账号ID:即便为 0 也要传 - if (accountId !== undefined) { - params.wechatAccountId = accountId - } - - // 分组ID:即便为 0(未分组/默认群分组)也要传给接口,由后端决定语义 - if (groupId !== undefined) { - params.groupId = groupId - } - - if (groupType === 1) { - // 好友列表API - const result: any = await getContactList(params) - const list = result?.list || result?.data || result || [] - contacts = list.map((item: any) => ({ - id: item.id, - type: 'friend' as ContactType, - wechatAccountId: item.wechatAccountId || accountId, - avatar: item.avatar || '', - nickname: item.nickname || '', - remark: item.conRemark || item.remark, - wxid: item.wechatId || item.wxid, - aiType: item.aiType ?? 0, - groupId: item.groupId, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - })) - total = result?.total || list.length - } else { - // 群列表 - const result: any = await getGroupList(params) - const list = result?.list || result?.data || result || [] - contacts = list.map((item: any) => ({ - id: item.id, - type: 'group' as ContactType, - wechatAccountId: item.wechatAccountId || accountId, - avatar: item.chatroomAvatar || item.avatar || '', - nickname: item.nickname || '', - remark: item.conRemark || item.remark, - chatroomId: item.chatroomId || '', - chatroomAvatar: item.chatroomAvatar || item.avatar || '', - aiType: item.aiType ?? 0, - groupId: item.groupId, - createdAt: item.createdAt, - updatedAt: item.updatedAt, - })) - total = result?.total || list.length - } - - // 更新数据(追加模式:保留现有联系人,追加新联系人) - const data = new Map(groupData.value) - const existing = data.get(groupKey) - - // ⭐ 关键:如果是第一页,替换数据;否则追加数据(确保不丢失现有数据) - let updatedContacts: Contact[] - if (page === 1) { - // 第一页:替换数据 - updatedContacts = contacts - } else { - // 后续页:追加数据(保留现有联系人,追加新联系人) - const existingContacts = existing?.contacts || [] - // 去重:避免重复添加(基于id和type) - const existingIds = new Set( - existingContacts.map((c) => `${c.type}_${c.id}`) - ) - const newContacts = contacts.filter( - (c) => !existingIds.has(`${c.type}_${c.id}`) - ) - updatedContacts = [...existingContacts, ...newContacts] - } - - const updatedData: GroupContactData = { - contacts: updatedContacts, - page, - pageSize: limit, - hasMore: contacts.length === limit, // 如果返回的数据量等于limit,说明可能还有更多 - loading: false, - loaded: true, - lastLoadTime: Date.now(), - } - data.set(groupKey, updatedData) - groupData.value = data - - // 更新分组人数 - const updatedGroups = groups.value.map((g) => - g.id === groupId && g.groupType === groupType ? { ...g, count: total } : g - ) - groups.value = updatedGroups - - // 缓存第一页数据 - if (page === 1) { - await setToCache(`groupContacts_${groupKey}`, updatedData) - } - } catch (error) { - console.error('加载分组联系人失败:', error) - // 恢复loading状态 - const data = new Map(groupData.value) - const existing = data.get(groupKey) - if (existing) { - data.set(groupKey, { ...existing, loading: false }) - groupData.value = data - } - throw error - } - } - - /** - * 加载更多分组联系人 - */ - const loadMoreGroupContacts = async (groupId: number, groupType: 1 | 2) => { - const accountId = selectedAccountId.value - const groupKey = getGroupKey(groupId, groupType, accountId) - const currentData = groupData.value.get(groupKey) - - if (!currentData || !currentData.hasMore || currentData.loading) { - return // 没有更多数据或正在加载 - } - - // 加载下一页 - const nextPage = currentData.page + 1 - await loadGroupContacts(groupId, groupType, nextPage, currentData.pageSize) - } - - /** - * 搜索联系人(API搜索,并行请求) - */ - const searchContacts = async (keyword: string) => { - if (!keyword.trim()) { - isSearchMode.value = false - searchResults.value = [] - searchKeyword.value = '' - return - } - - searchKeyword.value = keyword - isSearchMode.value = true - searchLoading.value = true - - try { - const accountId = selectedAccountId.value - const params: any = { - keyword, - page: 1, - limit: 100, - } - - // 账号ID:即便为 0 也要传 - if (accountId !== undefined) { - params.wechatAccountId = accountId - } - - // 并行请求好友和群列表 - const [friendsResult, groupsResult] = await Promise.all([ - getContactList(params), - getGroupList(params), - ]) - - const friendsList = friendsResult?.list || friendsResult?.data || friendsResult || [] - const groupsList = groupsResult?.list || groupsResult?.data || groupsResult || [] - - const allResults: Contact[] = [ - ...friendsList.map((item: any) => ({ - id: item.id, - type: 'friend' as ContactType, - wechatAccountId: item.wechatAccountId || accountId, - avatar: item.avatar || '', - nickname: item.nickname || '', - remark: item.conRemark || item.remark, - wxid: item.wechatId || item.wxid, - aiType: item.aiType ?? 0, - groupId: item.groupId, - })), - ...groupsList.map((item: any) => ({ - id: item.id, - type: 'group' as ContactType, - wechatAccountId: item.wechatAccountId || accountId, - avatar: item.chatroomAvatar || item.avatar || '', - nickname: item.nickname || '', - remark: item.conRemark || item.remark, - chatroomId: item.chatroomId || '', - chatroomAvatar: item.chatroomAvatar || item.avatar || '', - aiType: item.aiType ?? 0, - groupId: item.groupId, - })), - ] - - searchResults.value = allResults - } catch (error) { - console.error('搜索联系人失败:', error) - ElMessage.error('搜索失败') - searchResults.value = [] - } finally { - searchLoading.value = false - } - } - - /** - * 清除搜索 - */ - const clearSearch = () => { - searchKeyword.value = '' - isSearchMode.value = false - searchResults.value = [] - searchLoading.value = false - } - - /** - * 切换账号 - * 清空所有旧账号的状态,重新加载新账号的数据 - */ - const switchAccount = async (accountId: number) => { - const oldAccountId = selectedAccountId.value - - // 如果切换到相同账号,不需要重置 - if (oldAccountId === accountId) { - return - } - - // 1. 清空所有展开状态(切换账号时应该折叠所有分组) - expandedGroups.value = new Set() - - // 2. 清空所有已加载的联系人数据(切换账号时应该清空所有数据,重新加载) - groupData.value = new Map() - - // 3. 清空分组列表(避免显示旧账号的分组) - groups.value = [] - - // 4. 清空搜索状态 - searchKeyword.value = '' - isSearchMode.value = false - searchResults.value = [] - searchLoading.value = false - - // 5. 更新选中的账号ID - selectedAccountId.value = accountId - - // 6. 重新加载新账号的分组列表 - await loadGroups(accountId) - - // 注意:不自动展开任何分组,让用户手动展开 - } - - /** - * 根据ID和类型获取联系人 - */ - const getContact = (contactId: number, type: ContactType): Contact | undefined => { - for (const data of groupData.value.values()) { - const contact = data.contacts.find((c) => c.id === contactId && c.type === type) - if (contact) return contact - } - return undefined - } - - /** - * 更新联系人信息 - */ - const updateContact = (contactId: number, type: ContactType, updates: Partial) => { - const accountId = selectedAccountId.value - const data = new Map(groupData.value) - - for (const [groupKey, groupDataItem] of data.entries()) { - const contactIndex = groupDataItem.contacts.findIndex( - (c) => c.id === contactId && c.type === type - ) - if (contactIndex !== -1) { - groupDataItem.contacts[contactIndex] = { - ...groupDataItem.contacts[contactIndex], - ...updates, - } - data.set(groupKey, groupDataItem) - break - } - } - - groupData.value = data - - // 如果当前在搜索模式,更新搜索结果 - if (isSearchMode.value) { - const index = searchResults.value.findIndex((c) => c.id === contactId && c.type === type) - if (index !== -1) { - searchResults.value[index] = { ...searchResults.value[index], ...updates } - } - } - } - - /** - * 更新联系人AI类型 - */ - const updateContactAiType = async ( - contactId: number, - type: ContactType, - accountId: number, - aiType: number - ) => { - try { - await updateContactAiTypeAPI({ - type: aiType, - wechatAccountId: accountId, - friendId: contactId, - }) - - // 更新本地数据 - updateContact(contactId, type, { aiType }) - - return true - } catch (error) { - console.error('更新联系人AI类型失败:', error) - throw error - } - } - - /** - * 修改联系人备注 - */ - const updateContactRemark = async ( - contactId: number, - groupId: number, - groupType: 1 | 2, - remark: string - ) => { - try { - // 调用API更新备注 - await updateFriendInfo({ - id: contactId, - conRemark: remark, - }) - - // 更新内存中的数据 - const accountId = selectedAccountId.value - const contactType: ContactType = groupType === 1 ? 'friend' : 'group' - updateContact(contactId, contactType, { remark }) - - // ⭐ 重新获取详情并同步到本地数据库(替代手动更新会话列表) - if (contactType === 'friend') { - const detail = await getFriendDetail({ id: contactId }) - await SessionManager.syncSessionFromFriendDetail(contactId, detail.detail || detail) - } else { - const detail = await getGroupDetail({ groupId: contactId }) - await SessionManager.syncSessionFromGroupDetail(contactId, detail) - } - - ElMessage.success('修改备注成功') - } catch (error) { - console.error('修改备注失败:', error) - ElMessage.error('修改备注失败') - throw error - } - } - - /** - * 移动联系人到其他分组 - */ - const moveContactToGroup = async ( - contactId: number, - fromGroupId: number, - toGroupId: number, - groupType: 1 | 2 - ) => { - try { - // 调用API移动分组 - await moveGroup({ - type: groupType === 1 ? 'friend' : 'chatroom', - groupId: toGroupId, - id: contactId, - }) - - const accountId = selectedAccountId.value - - // 从原分组移除 - const fromGroupKey = getGroupKey(fromGroupId, groupType, accountId) - const data = new Map(groupData.value) - const fromGroupData = data.get(fromGroupKey) - - if (fromGroupData && fromGroupData.loaded) { - fromGroupData.contacts = fromGroupData.contacts.filter((c) => c.id !== contactId) - data.set(fromGroupKey, fromGroupData) - } - - // 添加到新分组(如果已加载) - const toGroupKey = getGroupKey(toGroupId, groupType, accountId) - const toGroupData = data.get(toGroupKey) - - if (toGroupData && toGroupData.loaded) { - // 重新加载该联系人数据 - const contact = getContact(contactId, groupType === 1 ? 'friend' : 'group') - if (contact) { - toGroupData.contacts.push({ ...contact }) - data.set(toGroupKey, toGroupData) - } - } - - groupData.value = data - - // 更新分组人数 - const updatedGroups = groups.value.map((g) => { - if (g.id === fromGroupId && g.groupType === groupType) { - return { ...g, count: Math.max(0, g.count - 1) } - } - if (g.id === toGroupId && g.groupType === groupType) { - return { ...g, count: g.count + 1 } - } - return g - }) - groups.value = updatedGroups - - ElMessage.success('移动分组成功') - } catch (error) { - console.error('移动分组失败:', error) - ElMessage.error('移动分组失败') - throw error - } - } - - /** - * 新增分组 - */ - const addContactGroup = async (group: Omit) => { - try { - const result: any = await addGroup({ - groupName: group.groupName, - groupMemo: group.groupMemo || '', - groupType: group.groupType, - sort: group.sort || 0, - }) - - const newGroup: ContactGroup = { - id: result?.id || result?.data?.id || 0, - groupName: result?.groupName || result?.data?.groupName || group.groupName, - groupType: (result?.groupType || result?.data?.groupType || group.groupType) as 1 | 2, - count: 0, - sort: result?.sort || result?.data?.sort || group.sort || 0, - groupMemo: result?.groupMemo || result?.data?.groupMemo || group.groupMemo, - } - - const updatedGroups = [...groups.value, newGroup] - groups.value = sortGroups(updatedGroups) - - ElMessage.success('新增分组成功') - return newGroup - } catch (error) { - console.error('新增分组失败:', error) - ElMessage.error('新增分组失败') - throw error - } - } - - /** - * 更新分组 - */ - const updateContactGroup = async (group: ContactGroup) => { - try { - await updateGroup({ - id: group.id, - groupName: group.groupName, - groupMemo: group.groupMemo || '', - groupType: group.groupType, - sort: group.sort || 0, - }) - - const updatedGroups = groups.value.map((g) => (g.id === group.id ? group : g)) - groups.value = sortGroups(updatedGroups) - - ElMessage.success('编辑分组成功') - } catch (error) { - console.error('编辑分组失败:', error) - ElMessage.error('编辑分组失败') - throw error - } - } - - /** - * 删除分组 - */ - const deleteContactGroup = async (groupId: number, groupType: 1 | 2) => { - try { - await deleteGroup(groupId) - - // 从分组列表删除 - const updatedGroups = groups.value.filter( - (g) => !(g.id === groupId && g.groupType === groupType) - ) - groups.value = updatedGroups - - // 清理该分组的所有缓存数据 - const accountId = selectedAccountId.value - const groupDataNew = new Map(groupData.value) - const expandedGroupsNew = new Set(expandedGroups.value) - - // 清理所有账号的该分组数据 - groupDataNew.forEach((value, key) => { - if (key.startsWith(`${groupId}_${groupType}_`)) { - groupDataNew.delete(key) - } - }) - - // 清理展开状态 - expandedGroupsNew.forEach((key) => { - if (key.startsWith(`${groupId}_${groupType}_`)) { - expandedGroupsNew.delete(key) - } - }) - - groupData.value = groupDataNew - expandedGroups.value = expandedGroupsNew - - ElMessage.success('删除分组成功') - } catch (error) { - console.error('删除分组失败:', error) - ElMessage.error('删除分组失败') - throw error - } - } - - /** - * 重置状态 - */ - const reset = () => { - groups.value = [] - expandedGroups.value = new Set() - groupData.value = new Map() - selectedAccountId.value = 0 - searchKeyword.value = '' - isSearchMode.value = false - searchResults.value = [] - searchLoading.value = false - loading.value = false - } - - // ==================== 返回 ==================== - - return { - // 状态 - groups, - expandedGroups, - groupData, - selectedAccountId, - searchKeyword, - isSearchMode, - searchResults, - searchLoading, - loading, - - // 计算属性 - allContacts, - filteredContacts, - - // Actions - loadGroups, - toggleGroup, - loadGroupContacts, - loadMoreGroupContacts, - searchContacts, - clearSearch, - switchAccount, - getContact, - updateContact, - updateContactAiType, - updateContactRemark, - moveContactToGroup, - addContactGroup, - updateContactGroup, - deleteContactGroup, - reset, - } -}) diff --git a/TouchVueThree/src/stores/modules/wechat/useMessageStore.ts b/TouchVueThree/src/stores/modules/wechat/useMessageStore.ts deleted file mode 100644 index aca2613..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useMessageStore.ts +++ /dev/null @@ -1,602 +0,0 @@ -/** - * 消息 Store - * 管理聊天消息的状态和操作 - */ - -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import { ElMessage } from 'element-plus' -import type { Message } from '@/types/wechat' -import { useSessionStore } from './useSessionStore' -import { getChatMessages, getChatroomMessages } from '@/api/modules/wechat' -import { setupMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - -export const useMessageStore = defineStore('wechat-message', () => { - // ==================== 状态 ==================== - - /** 消息列表(按会话ID存储) */ - const messages = ref>(new Map()) - - // ==================== WebSocket 订阅 ==================== - - /** - * ⭐ 设置 WebSocket 消息订阅 - * 统一在 Store 中处理 WebSocket 消息事件 - */ - setupMessageSubscription({ - // 新消息 - onNewMessage: (message: Message) => { - // console.log('[MessageStore] 收到新消息:', message) - if (message.sessionId) { - addMessage(message.sessionId, message) - } - }, - - // 消息更新 - onMessageUpdate: ({ id, changes }) => { - console.log('[MessageStore] 消息状态更新:', { id, changes }) - - // ⭐ 如果 changes 中包含 sessionId,直接使用 - if (changes.sessionId && typeof changes.sessionId === 'number') { - const sessionId = changes.sessionId - // ⭐ 从 changes 中移除 sessionId,避免写入消息对象 - const { sessionId: _, ...updateData } = changes - updateMessageStatus(sessionId, id, updateData) - return - } - - // ⭐ 否则遍历所有会话查找消息(兼容旧逻辑) - for (const [sessionId, sessionMessages] of messages.value.entries()) { - const message = sessionMessages.find( - (m) => - m.id === id || - m.clientId === id || - (typeof id === 'number' && m.seq === id) - ) - if (message) { - updateMessageStatus(sessionId, id, changes) - break - } - } - }, - - // 消息撤回 - onMessageRecall: ({ messageId, sessionId }) => { - // console.log('[MessageStore] 消息撤回:', { messageId, sessionId }) - const sessionMessages = messages.value.get(sessionId) - if (sessionMessages) { - const index = sessionMessages.findIndex((m) => m.id === messageId) - if (index !== -1) { - // 更新消息为撤回状态 - sessionMessages[index] = { - ...sessionMessages[index], - content: '消息已被撤回', - msgType: 10002, // 撤回消息类型 - } - } - } - }, - }) - - /** 加载状态 */ - const loading = ref(false) - - /** 是否还有更多消息 */ - const hasMore = ref(true) - - /** 当前页码 */ - const currentPage = ref(1) - - // ==================== AI 相关 ==================== - - /** AI 加载状态 */ - const aiLoading = ref(false) - - /** AI 回复队列 */ - const aiReplyQueue = ref([]) - - // ==================== 计算属性 ==================== - - /** - * 当前会话的消息列表 - */ - const currentMessages = computed(() => { - const sessionStore = useSessionStore() - const sessionId = sessionStore.currentSession?.id - if (!sessionId) return [] - - return messages.value.get(sessionId) || [] - }) - - /** - * 当前消息数量 - */ - const currentMessageCount = computed(() => currentMessages.value.length) - - // ==================== 消息操作 ==================== - - /** - * 加载消息列表 - * - * @param sessionId 会话ID - * @param page 页码 - * @param pageSize 每页数量 - */ - const loadMessages = async ( - sessionId: number, - page: number = 1, - pageSize: number = 50 - ) => { - const sessionStore = useSessionStore() - const session = sessionStore.sessions.find((s) => s.id === sessionId) - - if (!session) { - // console.warn(`会话不存在: ${sessionId}`) - return - } - - loading.value = true - - try { - // 1. 从 IndexedDB 加载缓存(如果存在) - // TODO: 实现 MessageManager.getMessages - // const cached = await MessageManager.getMessages(sessionId, page, pageSize) - // if (cached.length > 0 && page === 1) { - // messages.value.set(sessionId, cached) - // } - - // 2. 从 API 加载消息 - const params: any = { - wechatAccountId: session.wechatAccountId, - page: page, - limit: pageSize, - } - - // 根据会话类型设置参数 - if (session.type === 'group') { - params.wechatChatroomId = sessionId - } else { - params.wechatFriendId = sessionId - } - - // 调用 API - const apiCall = - session.type === 'group' ? getChatroomMessages : getChatMessages - const response = await apiCall(params) - - // 3. 转换消息格式 - let messageList: Message[] = [] - - if (Array.isArray(response)) { - messageList = response.map((item: any) => - convertToMessage(item, session) - ) - } else if (response && typeof response === 'object') { - const list = response.list || response.data || response.messages || [] - if (Array.isArray(list)) { - messageList = list.map((item: any) => convertToMessage(item, session)) - } - } - - // ⭐ API 返回的数据可能是倒序(最新的在前),需要反转成升序(最早的在前) - // 参考旧项目:sortMessagesByTime 按时间升序排序 - // 如果 API 返回的是倒序,先反转数组 - if (messageList.length > 1) { - const firstTime = new Date(messageList[0].createTime).getTime() - const lastTime = new Date( - messageList[messageList.length - 1].createTime - ).getTime() - // 如果第一条消息的时间晚于最后一条,说明是倒序,需要反转 - if (firstTime > lastTime) { - messageList.reverse() - } - } - - // 4. 合并消息(如果是加载更多,追加;否则替换) - // ⭐ 聊天记录需要按时间升序排列(最早的在上,最新的在下) - // 参考旧项目:sortMessagesByTime 按时间升序排序 - if (page === 1) { - // 第一页:按时间升序排序(最早的在前) - messageList.sort((a, b) => { - const timeA = new Date(a.createTime).getTime() - const timeB = new Date(b.createTime).getTime() - return timeA - timeB // 升序:最早的在前 - }) - messages.value.set(sessionId, messageList) - } else { - // 加载更多:合并后按时间升序排序 - const existing = messages.value.get(sessionId) || [] - // 去重合并 - const merged = [...existing] - messageList.forEach((newMsg) => { - const exists = merged.some( - (m) => - m.id === newMsg.id || - (m.clientId && newMsg.clientId && m.clientId === newMsg.clientId) - ) - if (!exists) { - merged.push(newMsg) - } - }) - // ⭐ 按时间升序排序(最早的在前,最新的在后) - merged.sort((a, b) => { - const timeA = new Date(a.createTime).getTime() - const timeB = new Date(b.createTime).getTime() - return timeA - timeB // 升序:最早的在前 - }) - messages.value.set(sessionId, merged) - } - - // 5. 更新 hasMore - hasMore.value = messageList.length === pageSize - currentPage.value = page - - // 6. 保存到 IndexedDB(如果启用) - // TODO: 实现 MessageManager.saveMessages - // await MessageManager.saveMessages(messageList) - - // console.log(`📥 加载消息完成: sessionId=${sessionId}, page=${page}, count=${messageList.length}`) - } catch (error) { - console.error('加载消息失败:', error) - ElMessage.error('加载消息失败') - } finally { - loading.value = false - } - } - - /** - * 转换 API 消息格式为标准 Message 格式 - */ - const convertToMessage = (item: any, session: any): Message => { - return { - id: item.id || item.msgId || Date.now(), - clientId: item.clientId || item.clientMsgId, - sessionId: session.id, - wechatAccountId: session.wechatAccountId, - msgType: item.msgType || item.type || 1, - content: item.content || item.text || '', - direction: item.isSend ? 'send' : 'receive', - isSend: item.isSend || false, - createTime: - item.createTime || - item.time || - item.wechatTime || - new Date().toISOString(), - timestamp: - item.timestamp || - item.wechatTime || - new Date(item.createTime || item.time).getTime(), - status: item.status || 'success', - sender: item.sender - ? { - id: item.sender.id, - wxid: item.sender.wxid || item.senderWechatId, - nickname: item.sender.nickname || item.senderNickname, - avatar: item.sender.avatar, - } - : undefined, - } - } - - /** - * 发送消息 - * - * @param content 消息内容 - * @param msgType 消息类型 - * @param seq 消息序列号(用于匹配 CmdSendMessageResp 响应) - */ - const sendMessage = async ( - content: string, - msgType: number = 1, - seq?: number - ) => { - const sessionStore = useSessionStore() - const session = sessionStore.currentSession - if (!session) { - ElMessage.warning('请先选择会话') - return - } - - // ⭐ 如果没有传入 seq,生成一个(与 MessageInput 中的保持一致) - const messageSeq = seq || Date.now() - - // ⭐ 确定用于存储消息的 sessionId - // 对于群聊,使用 chatroomId(转换为数字,与 WebSocket 发送时一致) - // 对于好友,使用 session.id - // ⚠️ 注意:chatroomId 是 string,需要转换为 number - let messageSessionId: number = session.id - if (session.type === 'group' && session.chatroomId) { - // 尝试将 chatroomId 转换为数字,如果失败则使用 session.id - const chatroomIdNum = Number(session.chatroomId) - if (!isNaN(chatroomIdNum)) { - messageSessionId = chatroomIdNum - } - } - - // 1. 生成临时消息 - const tempMessage: Message = { - id: Date.now(), - clientId: `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - sessionId: messageSessionId, // ⭐ 使用正确的 sessionId - wechatAccountId: session.wechatAccountId, - msgType, - content, - direction: 'send', - isSend: true, - createTime: new Date().toISOString(), - timestamp: Date.now(), - status: 'sending', - seq: messageSeq, // ⭐ 添加 seq 字段,用于匹配 CmdSendMessageResp - sender: { - nickname: '我', - }, - } - - // 2. 添加到消息列表(乐观更新) - const sessionMessages = messages.value.get(messageSessionId) || [] - messages.value.set(messageSessionId, [...sessionMessages, tempMessage]) - - // 3. 消息已通过 WebSocket 发送(在 MessageInput 中调用) - // WebSocket 收到 CmdSendMessageResp 后会通过 seq 匹配并更新状态 - - // console.log(`📤 发送消息: ${content}`) - - // ⭐ 返回 seq,供调用方使用 - return messageSeq - } - - /** - * 添加消息到列表 - * 用于接收新消息或发送消息后添加 - * - * @param sessionId 会话ID - * @param message 消息对象 - */ - const addMessage = (sessionId: number | string, message: Message) => { - const id = typeof sessionId === 'string' ? Number(sessionId) : sessionId - if (!id) return - - const sessionMessages = messages.value.get(id) || [] - - // 去重检查 - const exists = sessionMessages.some( - (m) => - m.id === message.id || - (m.clientId && message.clientId && m.clientId === message.clientId) || - (m.id && message.id && String(m.id) === String(message.id)) - ) - - if (!exists) { - messages.value.set(id, [...sessionMessages, message]) - // console.log(`📨 添加消息到列表:`, { - // sessionId: id, - // content: message.content?.substring(0, 20), - // }) - } - } - - /** - * 接收消息 - * WebSocket 推送时调用(兼容旧方法名) - * - * @param message 消息对象 - */ - const receiveMessage = (message: Message) => { - const sessionId = message.sessionId - if (!sessionId) return - - addMessage(sessionId, message) - } - - /** - * 根据 seq 查找消息(仅用于发送消息的状态更新) - * 用于匹配 CmdSendMessageResp 响应 - * - * @param sessionId 会话ID - * @param seq 消息序列号 - * @returns 找到的消息,如果不存在返回 undefined - */ - const findMessageBySeq = ( - sessionId: number, - seq: number - ): Message | undefined => { - const sessionMessages = messages.value.get(sessionId) - if (!sessionMessages) return undefined - - return sessionMessages.find((m) => m.seq === seq && m.direction === 'send') - } - - /** - * 更新消息状态(支持通过 seq 更新) - * 用于更新发送中消息的状态(成功/失败) - * - * @param sessionId 会话ID - * @param messageId 消息ID(或 clientId 或 seq) - * @param updates 更新内容 - */ - const updateMessageStatus = ( - sessionId: number, - messageId: number | string, - updates: Partial - ) => { - const sessionMessages = messages.value.get(sessionId) - if (!sessionMessages) return - - const index = sessionMessages.findIndex((m) => { - if (typeof messageId === 'number') { - // ⭐ 优先匹配 seq(用于发送消息响应),然后匹配 id - return ( - m.seq === messageId || - m.id === messageId || - String(m.id) === String(messageId) - ) - } else { - return m.clientId === messageId || m.id === messageId - } - }) - - if (index !== -1) { - // ⭐ 创建新数组和新消息对象,确保 Vue 响应式系统检测到变化 - const newMessages = sessionMessages.map((msg, i) => { - if (i === index) { - // ⭐ 创建新对象,确保响应式更新 - return { - ...msg, - ...updates, - } - } - return msg - }) - - // ⭐ 重新设置整个数组,触发 Map 的响应式更新 - messages.value.set(sessionId, newMessages) - - // ⭐ 强制触发响应式更新(确保 computed 重新计算) - // 通过重新创建 Map 来触发响应式 - messages.value = new Map(messages.value) - - // console.log(`✅ 更新消息状态:`, { - // sessionId, - // messageId, - // oldStatus: sessionMessages[index].status, - // newStatus: updates.status, - // updates - // }) - } - } - - /** - * 删除消息 - * - * @param sessionId 会话ID - * @param messageId 消息ID - */ - const deleteMessage = (sessionId: number, messageId: number) => { - const sessionMessages = messages.value.get(sessionId) - if (!sessionMessages) return - - const filtered = sessionMessages.filter((m) => m.id !== messageId) - messages.value.set(sessionId, filtered) - - // console.log(`🗑️ 删除消息: ${messageId}`) - } - - /** - * 撤回消息 - * - * @param sessionId 会话ID - * @param messageId 消息ID - */ - const recallMessage = async (sessionId: number, messageId: number) => { - try { - // TODO: 调用 API 撤回消息 - // await recallMessageAPI({ messageId }) - - // 更新消息状态 - const sessionMessages = messages.value.get(sessionId) - if (sessionMessages) { - const message = sessionMessages.find((m) => m.id === messageId) - if (message) { - message.recalled = true - message.content = '你撤回了一条消息' - } - } - - ElMessage.success('撤回成功') - // console.log(`↩️ 撤回消息: ${messageId}`) - } catch (error) { - console.error('撤回失败:', error) - ElMessage.error('撤回失败') - } - } - - /** - * 转发消息 - * - * @param messageId 消息ID - * @param targetIds 目标会话ID列表 - */ - const forwardMessage = async (messageId: number, targetIds: number[]) => { - try { - // TODO: 实现转发逻辑 - // console.log(`📮 转发消息: ${messageId} → ${targetIds}`) - ElMessage.success('转发成功') - } catch (error) { - console.error('转发失败:', error) - ElMessage.error('转发失败') - } - } - - // ==================== AI 功能 ==================== - - /** - * 请求 AI 回复 - * - * @param content 用户输入内容 - */ - const requestAIReply = async (content: string) => { - aiLoading.value = true - - try { - // TODO: 调用 AI API - // const res = await getAIReply({ content, sessionId }) - // aiReplyQueue.value.push(res.reply) - // console.log(`🤖 AI 回复请求: ${content}`) - } catch (error) { - console.error('AI 回复失败:', error) - ElMessage.error('AI 回复失败') - } finally { - aiLoading.value = false - } - } - - /** - * 清空 AI 回复队列 - */ - const clearAIQueue = () => { - aiReplyQueue.value = [] - // console.log('🧹 清空 AI 队列') - } - - /** - * 清空指定会话的消息 - * - * @param sessionId 会话ID - */ - const clearMessages = (sessionId: number) => { - messages.value.delete(sessionId) - // console.log(`🧹 清空会话消息: ${sessionId}`) - } - - // ==================== 返回 ==================== - - return { - // 状态 - messages, - currentMessages, - currentMessageCount, - loading, - hasMore, - currentPage, - aiLoading, - aiReplyQueue, - - // 消息操作 - loadMessages, - sendMessage, - addMessage, - receiveMessage, - findMessageBySeq, - updateMessageStatus, - deleteMessage, - recallMessage, - forwardMessage, - - // AI 功能 - requestAIReply, - clearAIQueue, - - // 其他 - clearMessages, - } -}) diff --git a/TouchVueThree/src/stores/modules/wechat/useSessionStore.ts b/TouchVueThree/src/stores/modules/wechat/useSessionStore.ts deleted file mode 100644 index f5685bb..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useSessionStore.ts +++ /dev/null @@ -1,983 +0,0 @@ -/** - * 会话管理 Store(重构版 - 基于 IndexedDB + 订阅机制) - * - * 核心改造: - * ✅ 替换轮询 → 订阅机制(SessionManager.onUpdate) - * ✅ 缓存优先 → 从 IndexedDB 读取,秒开体验 - * ✅ 后台同步 → 分页加载所有会话到本地 - * ✅ 自动更新 → WebSocket 推送时自动刷新 UI - * - * @author TouchVueThree Team - * @date 2026-01-13 - */ - -import { defineStore } from 'pinia' -import { ref, computed, onUnmounted } from 'vue' -import type { ChatSession } from '@/utils/db' -import { SessionManager } from '@/utils/dbManagers/SessionManager' -import { databaseManager } from '@/utils/db' -import { - getSessionList, - clearUnread as clearUnreadAPI, - updateFriendConfig, - getFriendDetail, - getGroupDetail, -} from '@/api' -import { dataProcessing } from '@/api/modules/ai' -import { useUserStore } from '@/stores/modules/user' -import { ElMessage } from 'element-plus' -import type { Contact } from '@/types/wechat' - -// ==================== 辅助类型 ==================== - -/** 会话接口(兼容旧类型) */ -interface Session extends ChatSession { - // 兼容字段 - latestMessage?: { - content: string - wechatTime: string - } - wechatTime?: number -} - -export const useSessionStore = defineStore('wechat-session', () => { - // ==================== 状态 ==================== - - /** 会话列表 */ - const sessions = ref([]) - - /** 当前选中的会话 */ - const currentSession = ref(null) - - /** 加载状态 */ - const loading = ref(false) - - /** 首次加载状态 */ - const initialLoading = ref(true) - - /** 后台同步状态(用于显示顶部同步提示) */ - const syncing = ref(false) - - /** 是否还有更多数据(用于滚动加载) */ - const hasMore = ref(true) - - /** 当前账号ID(0表示全部) */ - const currentAccountId = ref(0) - - /** 订阅取消函数 */ - let unsubscribe: (() => void) | null = null - - // ==================== 性能监控 ==================== - - /** 订阅回调触发次数(用于调试) */ - const updateCallbackCount = ref(0) - - /** 最后一次更新时间(用于调试) */ - const lastUpdateTime = ref(0) - - // ==================== 计算属性 ==================== - - /** - * 排序后的会话列表 - * ⭐ 数据库已经按 sortKey 降序排序,这里使用 sortKey 字符串比较(与数据库排序一致) - * 如果数据库未排序,这里作为备用排序 - */ - const sortedSessions = computed(() => { - // 使用 sortKey 字符串比较(与数据库排序一致) - // 降序排序:置顶在前,时间新的在前,名称小的在前 - return [...sessions.value].sort((a, b) => { - // 如果 sortKey 存在,使用 sortKey 排序(与数据库一致) - if (a.sortKey && b.sortKey) { - return b.sortKey.localeCompare(a.sortKey) - } - - // 备用排序逻辑(如果 sortKey 不存在) - if (a.config.top && !b.config.top) return -1 - if (!a.config.top && b.config.top) return 1 - return (b.config.msgTime || 0) - (a.config.msgTime || 0) - }) - }) - - /** - * 总未读数 - */ - const totalUnreadCount = computed(() => { - return sessions.value.reduce((sum, session) => { - return sum + (session.config?.unreadCount || 0) - }, 0) - }) - - // ==================== Actions ==================== - - /** - * 自动选择第一个会话(如果当前没有选中的会话) - * 用于首次加载时自动打开第一个聊天 - */ - const autoSelectFirstSession = () => { - // 如果已经有选中的会话,不自动选择 - if (currentSession.value) { - return - } - - // 获取排序后的会话列表 - const sorted = sortedSessions.value - - // 如果有会话,自动选择第一个 - if (sorted.length > 0) { - const firstSession = sorted[0] - // console.log( - // '🎯 自动选择第一个会话:', - // firstSession.nickname || firstSession.id - // ) - selectSession(firstSession) - } - } - - /** - * 初始化会话 Store - * - * @param accountId 账号ID(可选,0表示全部) - * - * 执行流程: - * 1. 确保数据库已初始化 - * 2. 从 IndexedDB 读取缓存(立即显示) - * 3. 订阅数据库变更(自动更新 UI) - * 4. 后台同步服务器数据(分页加载) - */ - const init = async (accountId: number = 0) => { - try { - // console.log(`📦 初始化会话 Store (账号: ${accountId || '全部'})`) - - currentAccountId.value = accountId - - // ⭐ 步骤0:确保数据库已初始化 - const userStore = useUserStore() - const userId = userStore.userId - - if (!userId) { - // console.warn('⚠️ 用户未登录,无法初始化数据库') - ElMessage.warning('请先登录') - return - } - - // 检查数据库是否已初始化 - if (!databaseManager.isInitialized()) { - // console.log('📗 数据库未初始化,正在初始化...') - await databaseManager.ensureDatabase(userId) - // console.log('✅ 数据库初始化完成') - } - - // ⭐ 步骤1:从 IndexedDB 读取缓存(立即显示) - sessions.value = await SessionManager.getUserSessions(accountId) - // console.log(`✅ 缓存读取完成,共 ${sessions.value.length} 个会话`) - - // ⭐ 步骤1.5:如果缓存中有数据且没有选中的会话,自动选择第一个 - autoSelectFirstSession() - - // ⭐ 步骤2:订阅数据库变更(自动更新 UI) - if (unsubscribe) { - unsubscribe() // 清除旧订阅 - } - - // ⭐ 优化订阅回调:防止频繁覆盖导致UI闪烁 - let updateTimer: ReturnType | null = null - unsubscribe = SessionManager.onUpdate( - (updatedSessions, updatedAccountId) => { - // ⭐ 使用 currentAccountId.value 而不是闭包的 accountId,确保切换账号后订阅正确 - const currentId = currentAccountId.value - // 只更新当前账号的会话 - if ( - currentId === 0 || - updatedAccountId === undefined || - updatedAccountId === currentId - ) { - // 性能监控 - updateCallbackCount.value++ - const now = Date.now() - const timeSinceLastUpdate = lastUpdateTime.value - ? now - lastUpdateTime.value - : 0 - lastUpdateTime.value = now - - // 防抖:避免在用户操作期间频繁覆盖本地状态 - if (updateTimer) { - clearTimeout(updateTimer) - // console.log( - // `⏸️ 订阅回调被防抖 (${timeSinceLastUpdate}ms since last update)` - // ) - } - - updateTimer = setTimeout(async () => { - // console.log( - // `🔄 会话更新 [#${updateCallbackCount.value}]:`, - // updatedSessions.length, - // 'sessions,', - // timeSinceLastUpdate > 0 - // ? `${timeSinceLastUpdate}ms ago` - // : 'first update' - // ) - // ⭐ 重新从数据库获取当前账号的会话,确保切换账号后订阅正确 - const currentId = currentAccountId.value - sessions.value = await SessionManager.getUserSessions(currentId) - // 如果更新后还没有选中的会话,自动选择第一个 - autoSelectFirstSession() - updateTimer = null - }, 100) // 100ms 防抖 - } - } - ) - - // console.log('✅ 订阅机制已启动') - - // ⭐ 步骤3:后台同步服务器数据(不阻塞 UI) - syncFromServer(accountId) - } catch (error) { - console.error('初始化会话失败:', error) - ElMessage.error('加载会话失败,请刷新重试') - } - } - - /** - * 从服务器同步会话数据 - * 分页加载所有会话,保存到 IndexedDB - * - * @param accountId 账号ID - */ - const syncFromServer = async (accountId: number = 0) => { - if (loading.value) return - - try { - loading.value = true - initialLoading.value = true - syncing.value = true - - // console.log('📥 开始从服务器同步会话...') - - // 通知 SessionManager 开始同步(防止竞态条件) - SessionManager.beginSync() - - let page = 1 - const limit = 200 - let hasMore = true - const syncedSessions: Session[] = [] - let isFirstPage = true - - // 分页加载所有会话 - while (hasMore) { - try { - const params: any = { - page, - limit, - } - - if (accountId && accountId !== 0) { - params.wechatAccountId = accountId - } - - // console.log(`📄 请求第 ${page} 页...`) - - const res = await getSessionList(params) - - if (!res || !res.list) { - // console.log('接口返回数据为空,停止同步') - break - } - - const list: any[] = res.list || [] - const total = res.total || 0 - - // console.log(`✅ 第 ${page} 页:${list.length} 条数据,总数: ${total}`) - - // 如果返回空数据,停止同步 - if (list.length === 0) { - break - } - - // 转换为标准格式并保存 - // ⭐ 关键:正确判断群和好友类型 - const formattedSessions: Session[] = list.map((item: any) => { - // 判断类型:优先使用 type,如果没有则通过 chatroomId 判断 - const sessionType = - item.type || (item.chatroomId ? 'group' : 'friend') - - return { - id: item.id, - serverId: `${sessionType}_${item.id}`, - type: sessionType, - wechatAccountId: item.wechatAccountId, - nickname: item.nickname, - conRemark: item.conRemark, - avatar: item.avatar, - wxid: item.wxid, - chatroomId: item.chatroomId, - content: item.content || item.latestMessage?.content || '', - lastUpdateTime: item.lastUpdateTime || new Date().toISOString(), - config: { - // ⭐ 确保服务器返回的 boolean 值正确映射 - unreadCount: item.config?.unreadCount || 0, - top: Boolean(item.config?.top), // 确保是 boolean 类型 - msgTime: item.config?.msgTime || Date.now(), - chat: item.config?.chat !== false, - mute: item.config?.mute || false, - }, - sortKey: '', // 临时占位,稍后使用 SessionManager.generateSortKey 生成 - } - }) - - // ⭐ 为每个会话生成正确的 sortKey - formattedSessions.forEach((session) => { - session.sortKey = SessionManager.generateSortKey(session) - }) - - // 批量保存到 IndexedDB - await SessionManager.syncSessions(formattedSessions) - syncedSessions.push(...formattedSessions) - - // ⭐ 第一页加载完成后,立即取消 initialLoading,允许渲染 - if (isFirstPage && formattedSessions.length > 0) { - initialLoading.value = false - isFirstPage = false - // console.log('✅ 第一页加载完成,开始渲染列表') - // 第一页加载完成后,自动选择第一个会话 - autoSelectFirstSession() - } - - // 判断是否还有下一页 - if (total > 0 && page * limit >= total) { - // console.log(`✅ 已加载完所有数据 (${page * limit} >= ${total})`) - hasMore = false - break - } - - page++ - } catch (error) { - console.error(`第 ${page} 页同步失败:`, error) - // 单页失败不影响整体,停止同步 - hasMore = false - break - } - } - - // console.log(`✅ 同步完成,共同步 ${syncedSessions.length} 个会话`) - - // 结束同步,应用待处理更新 - await SessionManager.endSync(accountId) - - // ⭐ 同步完成后,如果还没有选中的会话,自动选择第一个 - // 这确保首次加载时(缓存为空)也能自动选择第一个会话 - autoSelectFirstSession() - } catch (error) { - console.error('同步会话失败:', error) - ElMessage.error('同步失败,但不影响使用') - } finally { - loading.value = false - initialLoading.value = false - syncing.value = false - } - } - - /** - * 手动刷新会话列表 - * 用于下拉刷新等场景 - */ - const refresh = async () => { - await syncFromServer(currentAccountId.value) - } - - /** - * 加载更多会话(滚动加载) - * ⚠️ 注意:新架构在初始化时已加载所有会话,此方法主要用于兼容 - */ - const loadMore = async () => { - // 新架构在 init() 时已分页加载所有会话 - // 如果确实需要加载更多,可以调用 refresh() - // console.warn('loadMore 已废弃,新架构在初始化时已加载所有会话') - hasMore.value = false - } - - /** - * 从本地加载会话列表(不请求API) - * 用于切换账号时快速过滤显示 - * - * @param accountId 账号ID(0表示全部) - */ - const loadFromLocal = async (accountId: number = 0) => { - try { - // console.log(`🔄 从本地加载会话列表 (账号: ${accountId || '全部'})`) - - currentAccountId.value = accountId - - // ⭐ 直接从 IndexedDB 读取并过滤 - sessions.value = await SessionManager.getUserSessions(accountId) - // console.log(`✅ 本地加载完成,共 ${sessions.value.length} 个会话`) - - // 如果还没有选中的会话,自动选择第一个 - autoSelectFirstSession() - } catch (error) { - console.error('从本地加载会话失败:', error) - ElMessage.error('加载会话失败') - } - } - - /** - * 切换账号 - * 只从本地过滤数据,不重新请求API - * - * @param accountId 账号ID(0表示全部) - */ - const switchAccount = async (accountId: number) => { - // console.log(`🔄 切换账号: ${accountId}`) - // 清空当前选中的会话,让新账号自动选择第一个 - currentSession.value = null - // ⭐ 只从本地加载,不请求API - await loadFromLocal(accountId) - } - - /** - * 选择会话 - * - * @param session 会话对象 - */ - const selectSession = async (session: Session) => { - currentSession.value = session - - // 清除未读数 - if (session.config?.unreadCount && session.config.unreadCount > 0) { - await clearUnread(session.id) - } - } - - /** - * 根据联系人ID选择会话 - * - * @param contactId 联系人ID - */ - const selectSessionByContact = (contactId: string) => { - const session = sessions.value.find((s) => s.id.toString() === contactId) - if (session) { - selectSession(session) - } - } - - /** - * 处理搜索结果点击 - * - * 流程: - * 1. 检查会话是否存在 - * 2. 如果存在:移动到对应区域顶部(置顶/非置顶) - * 3. 如果不存在:获取详情并创建会话,插入到对应位置 - * 4. 选中会话 - * - * @param contact 搜索结果中的联系人 - */ - const handleSearchResultClick = async (contact: Contact) => { - try { - const sessionId = contact.id - const sessionType = contact.type - const accountId = contact.wechatAccountId || currentAccountId.value - - // 1. 检查会话是否存在 - const existingSession = sessions.value.find( - (s) => s.id === sessionId && s.type === sessionType - ) - - if (existingSession) { - // 场景A:会话已存在 - 移动到顶部 - const updatedSession = await moveSessionToTop(existingSession) - // ⭐ 使用更新后的会话对象 - await selectSession(updatedSession) - } else { - // 场景B:会话不存在 - 获取详情并创建 - const newSession = await createSessionFromContact(contact, accountId) - await selectSession(newSession) - } - } catch (error) { - console.error('处理搜索结果点击失败:', error) - ElMessage.error('打开会话失败') - throw error - } - } - - /** - * 移动会话到顶部 - * 根据置顶状态,移动到对应区域的第一个位置 - * - * @param session 要移动的会话 - * @returns 更新后的会话对象 - */ - const moveSessionToTop = async (session: Session): Promise => { - try { - const now = Date.now() - - // 更新会话的 msgTime 为当前时间,使其排到对应区域的顶部 - const updatedSession: Session = { - ...session, - config: { - ...session.config, - msgTime: now, // 更新时间戳,使其排到顶部 - }, - sortKey: '', // 临时占位,稍后生成 - lastUpdateTime: new Date().toISOString(), - } - - // ⭐ 使用 SessionManager.generateSortKey 生成正确的 sortKey - updatedSession.sortKey = SessionManager.generateSortKey(updatedSession) - - // 更新数据库(会触发订阅回调,自动更新 sessions.value) - await SessionManager.upsertSession(updatedSession) - - // ⭐ 立即更新本地状态,确保 UI 立即响应 - // 注意:直接替换对象引用,确保 Vue 响应式系统检测到变化 - const index = sessions.value.findIndex((s) => s.id === session.id) - if (index !== -1) { - // 直接替换对象,触发响应式更新 - sessions.value[index] = updatedSession - // 强制触发数组更新(Vue 3 通常会自动检测,但为了保险) - sessions.value = [...sessions.value] - } else { - // 如果没找到,直接添加(理论上不应该发生) - sessions.value.push(updatedSession) - } - - // console.log(`✅ 会话 ${session.id} 已移动到顶部,msgTime: ${now}`) - - // 返回更新后的会话对象 - return updatedSession - } catch (error) { - console.error('移动会话到顶部失败:', error) - throw error - } - } - - /** - * 从联系人创建新会话 - * - * @param contact 联系人信息 - * @param accountId 账号ID - * @returns 新创建的会话 - */ - const createSessionFromContact = async ( - contact: Contact, - accountId: number - ): Promise => { - try { - const sessionId = contact.id - const sessionType = contact.type - - // 1. 获取联系人详情(确保数据完整) - let contactInfo: any - if (sessionType === 'friend') { - contactInfo = await getFriendDetail({ friendId: sessionId }) - } else { - contactInfo = await getGroupDetail({ groupId: sessionId }) - } - - // 2. 构建新会话数据 - const now = Date.now() - const newSession: Session = { - id: sessionId, // number 类型 - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: accountId, - nickname: - contactInfo.nickname || - contactInfo.name || - contact.nickname || - '未知用户', - conRemark: - contactInfo.conRemark || contactInfo.remark || contact.remark, - avatar: - contactInfo.avatar || - contact.avatar || - contact.chatroomAvatar || - '/default-avatar.png', - wxid: contactInfo.wxid || contact.wxid, - chatroomId: - sessionType === 'group' - ? contactInfo.chatroomId || contact.chatroomId - : undefined, - content: '', // 新会话暂无消息 - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 0, - top: Boolean( - contactInfo.isTop || contactInfo.config?.top || contact.isTop - ), // 从详情中获取置顶状态 - msgTime: now, // 当前时间,使其排到对应区域的顶部 - chat: true, - mute: false, - }, - sortKey: '', // 临时占位,稍后生成 - } - - // ⭐ 使用 SessionManager.generateSortKey 生成正确的 sortKey - newSession.sortKey = SessionManager.generateSortKey(newSession) - - // 3. 保存到数据库 - await SessionManager.upsertSession(newSession) - - // console.log(`✅ 新会话已创建: ${newSession.nickname || sessionId}`) - - return newSession - } catch (error) { - console.error('创建会话失败:', error) - throw error - } - } - - /** - * 清除未读数 - * - * @param sessionId 会话ID - */ - /** - * 增加未读数 - * - * @param sessionId 会话ID - * @param count 增加的未读数(默认1) - */ - const increaseUnreadCount = (sessionId: number, count: number = 1) => { - const session = sessions.value.find((s) => s.id === sessionId) - if (session) { - const currentUnread = session.config?.unreadCount || 0 - session.config.unreadCount = currentUnread + count - - // 更新数据库 - SessionManager.upsertSession(session).catch((err) => { - console.error('更新未读数失败:', err) - }) - - // console.log(`📬 增加未读数: sessionId=${sessionId}, count=${count}, total=${session.config.unreadCount}`) - } - } - - const clearUnread = async (sessionId: number) => { - try { - const session = sessions.value.find((s) => s.id === sessionId) - if (!session) return - - // 调用 API 清除未读 - // ⭐ 根据会话类型传递相应的参数(好友ID或群ID) - const params: any = { - wechatAccountId: session.wechatAccountId, - } - - if (session.type === 'group' && session.chatroomId) { - // 群聊:传递 wechatChatroomId - params.wechatChatroomId = session.chatroomId - } else if (session.type === 'friend') { - // 好友:传递 wechatFriendId - params.wechatFriendId = session.id - } - - await clearUnreadAPI(params) - - // 更新本地数据库 - await SessionManager.clearUnread(sessionId, currentAccountId.value) - - // console.log(`✅ 已清除会话 ${sessionId} 的未读数`) - } catch (error) { - console.error('清除未读失败:', error) - } - } - - /** - * 添加新消息到会话 - * ⚠️ 不再需要手动调用,WebSocket 会自动调用 SessionManager.updateOnNewMessage - * - * @deprecated 使用 SessionManager.updateOnNewMessage 代替 - */ - const addMessage = (sessionId: number, content: string) => { - // console.warn('addMessage 已废弃,请使用 SessionManager.updateOnNewMessage') - const session = sessions.value.find((s) => s.id === sessionId) - if (session) { - session.content = content - session.config.msgTime = Date.now() - } - } - - /** - * 置顶/取消置顶会话 - * - * @param sessionId 会话ID - */ - const togglePin = async (sessionId: number) => { - try { - const session = sessions.value.find((s) => s.id === sessionId) - if (!session) { - ElMessage.error('会话不存在') - return - } - - const currentPinned = session.config?.top ? 1 : 0 - const newPinned = currentPinned === 1 ? 0 : 1 - const now = Date.now() - - // 1. 立即更新本地状态(乐观更新) - // ⭐ 关键:更新 msgTime,使其移动到对应区域的顶部 - const updatedSession: Session = { - ...session, - config: { - ...session.config, - top: newPinned === 1, - msgTime: now, // ⭐ 更新时间戳,使其移动到对应区域顶部 - }, - sortKey: '', // 临时占位,稍后生成 - lastUpdateTime: new Date().toISOString(), - } - - // ⭐ 使用 SessionManager.generateSortKey 生成正确的 sortKey - updatedSession.sortKey = SessionManager.generateSortKey(updatedSession) - - // 立即更新本地状态(在数据库更新之前,确保UI立即响应) - const index = sessions.value.findIndex((s) => s.id === sessionId) - if (index !== -1) { - sessions.value[index] = updatedSession - // 强制触发响应式更新 - sessions.value = [...sessions.value] - } - - // 2. 调用 dataProcessing API 更新服务器(先调用API) - const isGroup = - session.type === 'group' || - ('chatroomId' in session && session.chatroomId) - - const params: any = { - type: 'CmdPinToTop', - wechatAccountId: String(session.wechatAccountId), - isTop: String(newPinned), - } - - if (isGroup) { - params.wechatChatroomId = String(session.id) - } else { - params.wechatFriendId = String(session.id) - } - - await dataProcessing(params) - - // 3. 更新数据库(在API调用成功后,确保持久化) - // ⭐ 延迟更新数据库,避免立即触发订阅回调覆盖UI - setTimeout(async () => { - await SessionManager.upsertSession(updatedSession) - }, 200) - - ElMessage.success(newPinned === 1 ? '置顶成功' : '取消置顶成功') - // console.log(`✅ 已切换会话 ${sessionId} 的置顶状态,msgTime: ${now}`) - } catch (error) { - console.error('切换置顶失败:', error) - - // 失败时回滚UI - 重新从数据库加载 - const session = await SessionManager.getSession(sessionId) - if (session) { - const index = sessions.value.findIndex((s) => s.id === sessionId) - if (index !== -1) { - sessions.value[index] = session - sessions.value = [...sessions.value] - } - } - - ElMessage.error('操作失败') - } - } - - /** - * 更新会话备注 - * - * @param sessionId 会话ID - * @param remark 备注名 - */ - const updateSessionRemark = async (sessionId: number, remark: string) => { - try { - const session = sessions.value.find((s) => s.id === sessionId) - if (!session) { - throw new Error('会话不存在') - } - - // TODO: 调用 API 更新备注 - // if (session.type === 'friend') { - // await updateFriendConfig({ - // wechatAccountId: session.wechatAccountId, - // friendId: sessionId, - // conRemark: remark, - // }) - // } else if (session.type === 'group') { - // // 群聊备注更新 API(如果存在) - // // await updateGroupConfig({ groupId: sessionId, conRemark: remark }) - // } - - // 更新本地数据库 - const dbSession = await SessionManager.getSession(sessionId) - if (dbSession) { - await SessionManager.upsertSession({ - ...dbSession, - conRemark: remark, - }) - } - - // console.log(`✅ 已更新会话 ${sessionId} 的备注`) - } catch (error) { - console.error('更新备注失败:', error) - throw error - } - } - - /** - * 删除会话 - * ⭐ 参考旧项目逻辑:立即UI更新 → API调用 → 数据库删除 → 失败时恢复UI - * - * @param sessionId 会话ID - */ - const deleteSession = async (sessionId: number) => { - // ⭐ 先保存完整的 session 对象,用于失败时恢复UI - const session = sessions.value.find((s) => s.id === sessionId) - if (!session) { - ElMessage.error('会话不存在') - return - } - - // ⭐ 保存 session 的索引位置,用于失败时恢复到原位置 - const sessionIndex = sessions.value.findIndex((s) => s.id === sessionId) - - try { - // 1. 立即从UI移除(乐观更新) - if (sessionIndex !== -1) { - sessions.value.splice(sessionIndex, 1) - } - - // 如果是当前会话,清空选中 - if (currentSession.value?.id === sessionId) { - currentSession.value = null - } - - // 2. 后台调用API更新服务器(设置 chat: false) - // ⭐ 参考旧项目:使用 updateFriendConfig,传递 id 和 config - await updateFriendConfig({ - id: sessionId, - config: { chat: false }, - }) - - // 3. 后台从数据库删除 - // ⭐ 参考旧项目:调用 SessionManager.deleteSession - await SessionManager.deleteSession(sessionId, currentAccountId.value) - - // console.log(`✅ 已删除会话 ${sessionId}`) - ElMessage.success('删除成功') - } catch (error) { - console.error('删除会话失败:', error) - - // 4. 失败时恢复UI(参考旧项目逻辑) - // ⭐ 恢复到原来的位置,而不是简单 push - if (sessionIndex !== -1) { - sessions.value.splice(sessionIndex, 0, session) - } else { - // 如果找不到原位置,则追加到末尾 - sessions.value.push(session) - } - - ElMessage.error('删除失败') - } - } - - /** - * 清空会话列表 - * 用于登出或切换用户 - */ - const clearSessions = () => { - sessions.value = [] - currentSession.value = null - currentAccountId.value = 0 - - // 取消订阅 - if (unsubscribe) { - unsubscribe() - unsubscribe = null - } - - // console.log('✅ 会话列表已清空') - } - - /** - * 获取会话统计信息 - */ - const getStatistics = async () => { - return await SessionManager.getStatistics(currentAccountId.value) - } - - // ==================== 生命周期 ==================== - - /** - * 组件卸载时清理 - */ - const cleanup = () => { - if (unsubscribe) { - unsubscribe() - unsubscribe = null - } - // console.log('✅ 会话 Store 已清理') - } - - // 自动清理(在组件使用 onUnmounted 时触发) - onUnmounted(cleanup) - - // ==================== 返回 ==================== - - /** - * 获取性能统计信息(调试用) - */ - const getPerformanceStats = () => { - return { - updateCallbackCount: updateCallbackCount.value, - lastUpdateTime: lastUpdateTime.value, - sessionCount: sessions.value.length, - currentAccountId: currentAccountId.value, - } - } - - /** - * 重置性能统计(调试用) - */ - const resetPerformanceStats = () => { - updateCallbackCount.value = 0 - lastUpdateTime.value = 0 - // console.log('✅ 性能统计已重置') - } - - return { - // State - sessions, - sortedSessions, - currentSession, - loading, - initialLoading, - syncing, - hasMore, - totalUnreadCount, - currentAccountId, - - // Actions - increaseUnreadCount, - init, - loadFromLocal, - refresh, - loadMore, // 兼容旧代码 - switchAccount, - selectSession, - selectSessionByContact, - handleSearchResultClick, - moveSessionToTop, - createSessionFromContact, - clearUnread, - addMessage, // 已废弃,保留兼容性 - togglePin, - updateSessionRemark, - deleteSession, - clearSessions, - getStatistics, - cleanup, - - // Performance Monitoring (Debug) - getPerformanceStats, - resetPerformanceStats, - } -}) diff --git a/TouchVueThree/src/stores/modules/wechat/useUIStore.ts b/TouchVueThree/src/stores/modules/wechat/useUIStore.ts deleted file mode 100644 index 03d5826..0000000 --- a/TouchVueThree/src/stores/modules/wechat/useUIStore.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * UI状态管理 Store - */ - -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import type { SidebarTab, ModalType } from '@/types/wechat' - -export const useUIStore = defineStore('wechat-ui', () => { - // ==================== 状态 ==================== - - /** 是否显示资料卡 */ - const showProfileCard = ref(true) - - /** 是否显示聊天记录搜索 */ - const showChatRecordSearch = ref(false) - - /** 当前活动的侧边栏标签页 */ - const activeTab = ref('chats') - - /** 选中的消息ID集合 */ - const selectedMessages = ref>(new Set()) - - /** 是否显示消息复选框 */ - const showCheckbox = ref(false) - - /** 当前显示的模态框 */ - const currentModal = ref(null) - - /** 模态框数据 */ - const modalData = ref(null) - - /** 是否正在加载 */ - const loading = ref(false) - - /** 加载提示文本 */ - const loadingText = ref('') - - // ==================== 计算属性 ==================== - - /** 是否有选中的消息 */ - const hasSelectedMessages = computed(() => selectedMessages.value.size > 0) - - /** 选中的消息数量 */ - const selectedMessageCount = computed(() => selectedMessages.value.size) - - /** 是否显示模态框 */ - const hasModal = computed(() => currentModal.value !== null) - - // ==================== Actions ==================== - - /** - * 切换资料卡显示状态 - */ - const toggleProfileCard = () => { - showProfileCard.value = !showProfileCard.value - } - - /** - * 显示资料卡 - */ - const openProfileCard = () => { - showProfileCard.value = true - } - - /** - * 隐藏资料卡 - */ - const closeProfileCard = () => { - showProfileCard.value = false - } - - /** - * 打开聊天记录搜索 - */ - const openChatRecordSearch = () => { - showChatRecordSearch.value = true - } - - /** - * 关闭聊天记录搜索 - */ - const closeChatRecordSearch = () => { - showChatRecordSearch.value = false - } - - /** - * 切换标签页 - */ - const switchTab = (tab: SidebarTab) => { - activeTab.value = tab - } - - /** - * 切换消息选择 - */ - const toggleMessageSelection = (messageId: string) => { - if (selectedMessages.value.has(messageId)) { - selectedMessages.value.delete(messageId) - } else { - selectedMessages.value.add(messageId) - } - } - - /** - * 选中所有消息 - */ - const selectAllMessages = (messageIds: string[]) => { - messageIds.forEach((id) => selectedMessages.value.add(id)) - } - - /** - * 取消选中所有消息 - */ - const deselectAllMessages = () => { - selectedMessages.value.clear() - } - - /** - * 清除选择 - */ - const clearSelection = () => { - selectedMessages.value.clear() - showCheckbox.value = false - } - - /** - * 切换复选框显示 - */ - const toggleCheckbox = () => { - showCheckbox.value = !showCheckbox.value - if (!showCheckbox.value) { - selectedMessages.value.clear() - } - } - - /** - * 显示复选框 - */ - const showMessageCheckbox = () => { - showCheckbox.value = true - } - - /** - * 隐藏复选框 - */ - const hideMessageCheckbox = () => { - showCheckbox.value = false - selectedMessages.value.clear() - } - - /** - * 打开模态框 - */ - const openModal = (modalType: Exclude, data?: any) => { - currentModal.value = modalType - modalData.value = data - } - - /** - * 关闭模态框 - */ - const closeModal = () => { - currentModal.value = null - modalData.value = null - } - - /** - * 显示加载状态 - */ - const showLoading = (text = '加载中...') => { - loading.value = true - loadingText.value = text - } - - /** - * 隐藏加载状态 - */ - const hideLoading = () => { - loading.value = false - loadingText.value = '' - } - - /** - * 重置状态 - */ - const reset = () => { - showProfileCard.value = true - showChatRecordSearch.value = false - activeTab.value = 'chats' - selectedMessages.value.clear() - showCheckbox.value = false - currentModal.value = null - modalData.value = null - loading.value = false - loadingText.value = '' - } - - // ==================== 返回 ==================== - - return { - // 状态 - showProfileCard, - showChatRecordSearch, - activeTab, - selectedMessages, - showCheckbox, - currentModal, - modalData, - loading, - loadingText, - - // 计算属性 - hasSelectedMessages, - selectedMessageCount, - hasModal, - - // Actions - toggleProfileCard, - openProfileCard, - closeProfileCard, - openChatRecordSearch, - closeChatRecordSearch, - switchTab, - toggleMessageSelection, - selectAllMessages, - deselectAllMessages, - clearSelection, - toggleCheckbox, - showMessageCheckbox, - hideMessageCheckbox, - openModal, - closeModal, - showLoading, - hideLoading, - reset, - } -}) diff --git a/TouchVueThree/src/types/wechat.ts b/TouchVueThree/src/types/wechat.ts deleted file mode 100644 index dd56510..0000000 --- a/TouchVueThree/src/types/wechat.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * 微信相关类型定义 - */ - -// ==================== 基础类型 ==================== - -/** 微信账号 */ -export interface WeChatAccount { - id: number - name: string - avatar: string - isOnline: boolean - loginStatus?: number - deviceType?: string - wechatId?: string - nickname?: string -} - -/** 联系人类型 */ -export type ContactType = 'friend' | 'group' - -/** AI 模式类型 */ -export type AIType = 0 | 1 | 2 // 0-人工 1-AI辅助 2-AI接管 - -// ==================== 联系人 ==================== - -/** 联系人基础信息 */ -export interface Contact { - id: number - type: ContactType - wechatAccountId: number - avatar: string - nickname: string - conRemark?: string // ⭐ 微信备注,remark 字段已废弃 - wxid?: string - chatroomId?: string - chatroomAvatar?: string - aiType?: AIType - labels?: string[] - isTop?: boolean - groupId?: number // 所属分组ID - createdAt?: number - updatedAt?: number -} - -/** 群聊信息 */ -export interface Group extends Contact { - type: 'group' - chatroomId: string - chatroomAvatar: string - memberCount?: number - members?: GroupMember[] -} - -/** 好友信息 */ -export interface Friend extends Contact { - type: 'friend' - wxid: string -} - -/** 群成员 */ -export interface GroupMember { - id: number - chatroomId: string - wxid: string - nickname: string - avatar?: string - displayName?: string -} - -/** 联系人分组 */ -export interface ContactGroup { - id: number - groupName: string - groupType: 1 | 2 // 1-好友分组, 2-群分组 - count: number - sort: number - groupMemo?: string - contacts?: Contact[] -} - -/** 分组联系人数据 */ -export interface GroupContactData { - contacts: Contact[] - page: number - pageSize: number - hasMore: boolean - loading: boolean - loaded: boolean - lastLoadTime?: number -} - -// ==================== 会话 ==================== - -/** 会话信息 */ -export interface Session { - id: string // 联系人ID - type: ContactType - wechatAccountId: number - contact: Contact - lastMessage?: Message - lastMessageTime?: number - unreadCount: number - isTop: boolean - isMuted: boolean - aiType?: AIType - createdAt: number - updatedAt: number -} - -// ==================== 消息 ==================== - -/** 消息类型枚举 */ -export enum MessageType { - TEXT = 1, // 文本 - IMAGE = 3, // 图片 - AUDIO = 34, // 语音 - VIDEO = 43, // 视频 - EMOJI = 47, // 表情 - LOCATION = 48, // 位置 - FILE = 49, // 文件 - LINK = 49, // 链接(也是49) - MINI_PROGRAM = 4901, // 小程序 - RED_PACKET = 4902, // 红包 - TRANSFER = 4903, // 转账 - SYSTEM = 10000, // 系统消息 - TIME_DIVIDER = -10001, // 时间分隔 - RECALL = 10002, // 撤回消息 - RECOMMEND_REMARK = 570425393, // 推荐备注 - GROUP_INVITE = 90000, // 群邀请 -} - -/** 消息发送状态 */ -export enum MessageStatus { - SENDING = 'sending', // 发送中 - SUCCESS = 'success', // 发送成功 - FAILED = 'failed', // 发送失败 - RECALLED = 'recalled', // 已撤回 -} - -/** 消息基础信息 */ -export interface Message { - id: number | string - clientId?: string // 客户端临时ID - seq?: number // ⭐ 消息序列号(仅发送消息有,用于匹配 CmdSendMessageResp) - sessionId?: number - wechatAccountId?: number - msgType: number - content: string - direction: 'send' | 'receive' // 消息方向 - isSend?: boolean // 是否是自己发送(兼容旧字段) - sender?: { - id?: string - wxid?: string - nickname: string - avatar?: string - } - createTime: string // 创建时间 - timestamp?: number // 时间戳(兼容) - status?: 'sending' | 'success' | 'failed' // 发送状态 - serverId?: string // 服务器返回的真实ID - recalled?: boolean // 是否已撤回 - recalledBy?: string - replyTo?: string // 回复的消息ID - extra?: Record -} - -/** 文件消息内容 */ -export interface FileMessageContent { - type: 'file' - url: string - name: string - size: number - ext?: string - isDownloading?: boolean -} - -/** 图片消息内容 */ -export interface ImageMessageContent { - type: 'image' - url: string - thumbUrl?: string - width?: number - height?: number -} - -/** 视频消息内容 */ -export interface VideoMessageContent { - type: 'video' - url: string - thumbUrl?: string - duration?: number - width?: number - height?: number -} - -/** 语音消息内容 */ -export interface AudioMessageContent { - type: 'audio' - url: string - duration: number - isPlaying?: boolean - text?: string // 语音转文字 -} - -/** 位置消息内容 */ -export interface LocationMessageContent { - type: 'location' - label: string - lat: number - lng: number - poiName?: string -} - -/** 消息分组(按时间) */ -export interface MessageGroup { - time: string - messages: Message[] -} - -// ==================== AI ==================== - -/** AI 配置 */ -export interface AIConfig { - contactId: string - type: AIType - enabled: boolean - autoReply?: boolean - replyDelay?: number - customPrompt?: string -} - -/** AI 生成请求 */ -export interface AIGenerateRequest { - messages: Message[] - contactId: string - accountId: number - customPrompt?: string -} - -/** AI 生成响应 */ -export interface AIGenerateResponse { - content: string - confidence?: number - suggestions?: string[] -} - -// ==================== WebSocket ==================== - -/** WebSocket 消息类型 */ -export interface WebSocketMessage { - cmdType: string - seq?: number - wechatAccountIds?: number[] - content?: any - data?: any - [key: string]: any -} - -/** WebSocket 连接状态 */ -export enum WebSocketStatus { - DISCONNECTED = 'disconnected', - CONNECTING = 'connecting', - CONNECTED = 'connected', - RECONNECTING = 'reconnecting', - ERROR = 'error', -} - -/** WebSocket 配置 */ -export interface WebSocketConfig { - url: string - client: string - accountId: number - accessToken: string - autoReconnect: boolean - cmdType: string - seq: number - reconnectInterval: number - maxReconnectAttempts: number - heartbeatInterval: number -} - -// ==================== UI 相关 ==================== - -/** 侧边栏标签页 */ -export type SidebarTab = 'chats' | 'contacts' | 'moments' - -/** 模态框类型 */ -export type ModalType = - | 'add-friend' - | 'create-group' - | 'followup-reminder' - | 'todo-list' - | 'chat-record-search' - | 'profile-card' - | 'forward-message' - | null - -// ==================== API 响应 ==================== - -/** 通用API响应 */ -export interface ApiResponse { - code: number - message: string - data: T -} - -/** 分页参数 */ -export interface PageParams { - pageNum: number - pageSize: number -} - -/** 分页响应 */ -export interface PageResponse { - list: T[] - total: number - pageNum: number - pageSize: number - hasMore: boolean -} diff --git a/TouchVueThree/src/utils/auth.ts b/TouchVueThree/src/utils/auth.ts deleted file mode 100644 index 9776ee5..0000000 --- a/TouchVueThree/src/utils/auth.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * 认证工具函数 - * - * 提供统一的登录过期处理和退出登录逻辑 - * - * @author TouchVueThree Team - * @date 2026-01-16 - */ - -import { ElMessage } from 'element-plus' -import router from '@/router' -import { useUserStore } from '@/stores/modules/user' - -// 防止重复处理标志位 -let isHandlingLogout = false - -/** - * 统一的登录过期处理(401 错误) - * - * 执行流程: - * 1. 关闭 WebSocket 连接 - * 2. 关闭 IndexedDB 数据库 - * 3. 清除登录缓存(token、用户信息等) - * 4. 显示提示消息 - * 5. 跳转到登录页(保存当前路径用于重新登录后跳转) - * - * ⚠️ 注意:使用防重复处理机制,避免多个 401 错误同时触发 - */ -export async function handleLoginExpired() { - // 防止重复处理 - if (isHandlingLogout) { - console.warn('[Auth] 正在处理登录过期,跳过重复调用') - return - } - - isHandlingLogout = true - console.log('[Auth] 🔐 开始处理登录过期') - - try { - const userStore = useUserStore() - - // 保存当前路径(用于重新登录后跳转) - const currentPath = window.location.pathname + window.location.search - - // 调用 logout 方法(会关闭 WebSocket 和数据库) - // skipRouterPush = true,避免重复跳转 - await userStore.logout(true) - - // 显示提示 - ElMessage.warning('登录已过期,请重新登录') - - // 延迟跳转(避免与其他操作冲突) - setTimeout(() => { - // 只有当前不在登录页时才跳转 - if (router.currentRoute.value.path !== '/login') { - router.push({ - path: '/login', - query: { redirect: currentPath }, - }) - } - - // 重置标志位 - isHandlingLogout = false - }, 500) - } catch (error) { - console.error('[Auth] ❌ 处理登录过期失败:', error) - - // 确保重置标志位 - isHandlingLogout = false - - // 确保跳转到登录页 - if (router.currentRoute.value.path !== '/login') { - router.push('/login') - } - } -} - -/** - * 手动退出登录 - * - * 用于用户主动点击"退出登录"按钮 - */ -export async function handleManualLogout() { - try { - const userStore = useUserStore() - - // 调用 logout 方法(会关闭 WebSocket 和数据库,并跳转到登录页) - await userStore.logout() - - ElMessage.success('已退出登录') - } catch (error) { - console.error('[Auth] ❌ 退出登录失败:', error) - ElMessage.error('退出登录失败,请稍后重试') - } -} - -/** - * 检查登录状态 - * - * @returns 是否已登录 - */ -export function isLoggedIn(): boolean { - const userStore = useUserStore() - return userStore.isLoggedIn -} - -/** - * 获取当前用户信息 - */ -export function getCurrentUser() { - const userStore = useUserStore() - return userStore.user -} diff --git a/TouchVueThree/src/utils/db.ts b/TouchVueThree/src/utils/db.ts deleted file mode 100644 index 74a4e85..0000000 --- a/TouchVueThree/src/utils/db.ts +++ /dev/null @@ -1,503 +0,0 @@ -/** - * 数据库管理器 - 多账户隔离的 IndexedDB 管理 - * - * 核心特性: - * - 一号一库:每个账户独立数据库,彻底隔离 - * - 自动切换:账户切换时自动切换数据库 - * - 防泄漏:旧数据库自动关闭 - * - * @author TouchVueThree Team - * @date 2026-01-13 - */ - -import Dexie, { Table } from 'dexie' - -const DB_NAME_PREFIX = 'TouchChatDB' -const DB_VERSION = 1 - -// ==================== 数据表接口 ==================== - -/** - * 会话表 - * 存储:会话列表、最新消息、未读数等 - */ -export interface ChatSession { - // 主键和标识 - id: number // 主键(好友ID或群ID) - serverId: string // 唯一标识: friend_123 / group_456 - type: 'friend' | 'group' // 类型 - wechatAccountId: number // 所属客服账号 - - // 联系人信息 - nickname: string - conRemark?: string // 备注名 - avatar: string - wxid?: string // 微信ID - chatroomId?: string // 群聊ID - - // 消息信息 - content: string // 最新消息内容 - lastUpdateTime: string // 最后更新时间 ISO8601 - - // 配置 - config: { - unreadCount: number // 未读数 - top: boolean // 置顶 - msgTime: number // 消息时间戳 - chat?: boolean // 是否开启聊天 - mute?: boolean // 是否免打扰 - } - - // 索引字段 - sortKey: string // 排序键 (msgTime_id) -} - -/** - * 消息表(可选) - * 存储:聊天记录,限制数量和时间 - */ -export interface ChatMessage { - // 主键和标识 - id: number // 主键(消息ID) - clientId: string // 客户端临时ID (nanoid) - serverId?: string // 服务器消息ID - sessionId: number // 所属会话 - sessionType: 'friend' | 'group' - wechatAccountId: number - - // 消息内容 - content: string - msgType: number // 1=文本, 3=图片, 34=语音... - direction: 'send' | 'receive' - - // 发送者信息 - sender?: { - id: string - wxid: string - nickname: string - avatar?: string - } - - // 时间戳 - createTime: string // ISO8601 - wechatTime: number // 微信时间戳(毫秒) - - // 状态 - status: 'sending' | 'success' | 'failed' - isRead?: boolean - isRecalled?: boolean - - // 序列号(保证消息顺序) - sequence?: number - - // 扩展信息 - extra?: string // JSON 字符串 -} - -/** - * 联系人表 - * 存储:好友和群聊列表 - */ -export interface Contact { - // 主键和标识 - id: number - serverId: string - type: 'friend' | 'group' - wechatAccountId: number - - // 基本信息 - nickname: string - conRemark?: string - avatar: string - wxid?: string - chatroomId?: string - - // 搜索字段 - searchKey: string // 拼音首字母等,用于搜索 - - // 时间戳 - lastUpdateTime: string -} - -// ==================== 数据库类 ==================== - -/** - * 聊天数据库类 - * 使用 Dexie 封装 IndexedDB - */ -class ChatDatabase extends Dexie { - sessions!: Table - messages!: Table - contacts!: Table - - constructor(dbName: string) { - super(dbName) - - // 定义数据表和索引 - this.version(DB_VERSION).stores({ - // 会话表索引 - 添加 sortKey 索引用于数据库层面排序 - sessions: 'id, serverId, wechatAccountId, type, lastUpdateTime, sortKey, [wechatAccountId+sortKey]', - - // 消息表索引 - messages: 'id, clientId, serverId, sessionId, [sessionId+createTime], [sessionId+sequence], createTime, wechatAccountId', - - // 联系人表索引 - contacts: 'id, serverId, wechatAccountId, type, searchKey', - }) - } -} - -// ==================== 数据库管理器 ==================== - -/** - * 数据库管理器类 - * 负责管理多个数据库实例(一号一库) - */ -class DatabaseManager { - /** 当前数据库实例 */ - private currentDb: ChatDatabase | null = null - - /** 当前用户ID */ - private currentUserId: number | null = null - - /** - * 获取数据库名称 - */ - private getDatabaseName(userId: number): string { - return `${DB_NAME_PREFIX}_${userId}` - } - - /** - * 打开数据库 - */ - private async openDatabase(dbName: string): Promise { - try { - const instance = new ChatDatabase(dbName) - await instance.open() - console.log(`📗 数据库已打开: ${dbName}`) - return instance - } catch (error: any) { - // 数据库损坏检测 - if ( - error.name === 'DatabaseClosedError' || - error.name === 'InvalidStateError' || - error.message?.includes('corrupt') - ) { - console.error('⚠️ 数据库已损坏,尝试修复...', error) - - try { - // 删除损坏的数据库 - await Dexie.delete(dbName) - console.log('已删除损坏的数据库') - - // 重新创建 - const instance = new ChatDatabase(dbName) - await instance.open() - - console.log('✅ 数据库已修复') - - // TODO: 这里可以添加 Sentry 上报 - // Sentry.captureException(new Error('IndexedDB 损坏'), { - // extra: { dbName, originalError: error } - // }) - - return instance - } catch (retryError) { - console.error('❌ 数据库修复失败:', retryError) - throw retryError - } - } - - throw error - } - } - - /** - * 确保数据库已初始化 - * 如果数据库已存在且用户ID匹配,直接返回 - * 否则关闭旧数据库,打开新数据库 - * - * @param userId 用户ID - * @returns 数据库实例 - */ - async ensureDatabase(userId: number): Promise { - if (!userId) { - throw new Error('Invalid userId: userId cannot be empty or 0') - } - - // 如果当前数据库已打开且用户ID匹配,直接返回 - if ( - this.currentDb && - this.currentUserId === userId && - this.currentDb.isOpen() - ) { - return this.currentDb - } - - // 关闭旧数据库 - await this.closeCurrentDatabase() - - // 打开新数据库 - const dbName = this.getDatabaseName(userId) - this.currentDb = await this.openDatabase(dbName) - this.currentUserId = userId - - console.log(`✅ 已切换到用户 ${userId} 的数据库: ${dbName}`) - - return this.currentDb - } - - /** - * 获取当前数据库 - * 如果数据库未初始化,抛出异常 - * - * @returns 数据库实例 - */ - getCurrentDatabase(): ChatDatabase { - if (!this.currentDb || !this.currentDb.isOpen()) { - throw new Error('Database not initialized. Please call ensureDatabase() first.') - } - return this.currentDb - } - - /** - * 获取当前用户ID - */ - getCurrentUserId(): number | null { - return this.currentUserId - } - - /** - * 检查数据库是否已初始化 - */ - isInitialized(): boolean { - return !!this.currentDb && this.currentDb.isOpen() - } - - /** - * 关闭当前数据库 - */ - async closeCurrentDatabase(): Promise { - if (this.currentDb) { - try { - this.currentDb.close() - console.log(`📕 已关闭用户 ${this.currentUserId} 的数据库`) - } catch (error) { - console.warn('关闭数据库失败:', error) - } - this.currentDb = null - this.currentUserId = null - } - } - - /** - * 删除用户数据库 - * 用于登出或清除数据 - * - * @param userId 用户ID - */ - async deleteUserDatabase(userId: number): Promise { - const dbName = this.getDatabaseName(userId) - - // 如果是当前数据库,先关闭 - if (this.currentUserId === userId) { - await this.closeCurrentDatabase() - } - - try { - await Dexie.delete(dbName) - console.log(`🗑️ 已删除用户 ${userId} 的数据库: ${dbName}`) - } catch (error) { - console.error('删除数据库失败:', error) - throw error - } - } - - /** - * 列出所有用户数据库 - * - * @returns 用户ID列表 - */ - async listUserDatabases(): Promise { - try { - const databases = await Dexie.getDatabaseNames() - const userIds: number[] = [] - - databases.forEach((dbName) => { - if (dbName.startsWith(DB_NAME_PREFIX)) { - const userId = parseInt(dbName.replace(`${DB_NAME_PREFIX}_`, '')) - if (!isNaN(userId)) { - userIds.push(userId) - } - } - }) - - return userIds - } catch (error) { - console.error('列出数据库失败:', error) - return [] - } - } - - /** - * 检查存储配额 - * 返回配额使用情况和是否可以继续写入 - */ - async checkStorageQuota(): Promise<{ - usage: number - quota: number - usagePercent: number - canWrite: boolean - }> { - if ('storage' in navigator && 'estimate' in navigator.storage) { - try { - const estimate = await navigator.storage.estimate() - const usage = estimate.usage || 0 - const quota = estimate.quota || 0 - const usagePercent = quota > 0 ? (usage / quota) * 100 : 0 - - console.log( - `💾 存储使用情况: ${(usage / 1024 / 1024).toFixed(2)} MB / ${(quota / 1024 / 1024).toFixed(2)} MB (${usagePercent.toFixed(1)}%)` - ) - - // 超过 95% 禁止写入 - const canWrite = usagePercent < 95 - - return { - usage, - quota, - usagePercent, - canWrite, - } - } catch (error) { - console.error('检查存储配额失败:', error) - } - } - - // 不支持配额检测的浏览器,默认允许写入 - return { - usage: 0, - quota: 0, - usagePercent: 0, - canWrite: true, - } - } - - /** - * 清理旧数据 - * 删除 30 天前的消息和孤儿消息 - */ - async cleanOldData(): Promise { - if (!this.isInitialized()) { - console.warn('数据库未初始化,无法清理数据') - return - } - - try { - const db = this.getCurrentDatabase() - const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000 - - console.log('开始清理旧数据...') - - // 1. 删除 30 天前的消息 - const deletedCount = await db.messages - .where('wechatTime') - .below(thirtyDaysAgo) - .delete() - - console.log(`删除了 ${deletedCount} 条旧消息`) - - // 2. 删除孤儿消息(没有对应会话的消息) - const sessionIds = await db.sessions.toCollection().primaryKeys() - const orphanCount = await db.messages - .where('sessionId') - .noneOf(sessionIds as number[]) - .delete() - - console.log(`删除了 ${orphanCount} 条孤儿消息`) - - console.log('✅ 旧数据清理完成') - } catch (error) { - console.error('清理旧数据失败:', error) - throw error - } - } -} - -// ==================== 导出 ==================== - -/** - * 数据库管理器单例 - */ -export const databaseManager = new DatabaseManager() - -/** - * 获取当前数据库的快捷函数 - * - * @example - * const sessions = await db().sessions.toArray() - * await db().sessions.put(session) - */ -export const db = () => databaseManager.getCurrentDatabase() - -/** - * 检查浏览器兼容性 - */ -export const checkBrowserSupport = (): { - supported: boolean - message?: string -} => { - // 1. 检查 IndexedDB - if (!('indexedDB' in window)) { - return { - supported: false, - message: '您的浏览器不支持本地存储功能,请升级到最新版本', - } - } - - // 2. 检查 WebSocket - if (!('WebSocket' in window)) { - return { - supported: false, - message: '您的浏览器不支持实时通信功能,请升级到最新版本', - } - } - - // 3. 检查隐私模式(Safari) - try { - const testDb = indexedDB.open('test') - testDb.onerror = () => { - return { - supported: false, - message: '检测到您正在使用隐私模式,某些功能可能无法使用', - } - } - } catch (error) { - return { - supported: false, - message: '浏览器存储功能已被禁用', - } - } - - return { supported: true } -} - -/** - * 安全写入(带配额检查) - */ -export const safeWrite = async (operation: () => Promise) => { - const quota = await databaseManager.checkStorageQuota() - - if (!quota.canWrite) { - console.error('存储空间不足,无法写入') - throw new Error('存储空间不足') - } - - // 如果超过 80% 开始清理 - if (quota.usagePercent > 80) { - console.warn('存储空间不足,开始清理旧数据...') - await databaseManager.cleanOldData() - } - - await operation() -} diff --git a/TouchVueThree/src/utils/dbManagers/MessageManager.ts b/TouchVueThree/src/utils/dbManagers/MessageManager.ts deleted file mode 100644 index 8e1f679..0000000 --- a/TouchVueThree/src/utils/dbManagers/MessageManager.ts +++ /dev/null @@ -1,537 +0,0 @@ -/** - * 消息管理器 - 负责消息数据的存储和查询(可选功能) - * - * 核心特性: - * - 混合策略:缓存 + 实时加载 - * - 自动清理:限制数量和时间 - * - 消息去重:防止重复消息 - * - 消息排序:按序列号和时间戳 - * - * 存储限制: - * - 每个会话最多 500 条消息 - * - 保留最近 30 天 - * - 超过限制自动清理 - * - * @author TouchVueThree Team - * @date 2026-01-13 - */ - -import { db } from '../db' -import type { ChatMessage } from '../db' - -// ==================== 配置常量 ==================== - -/** 每个会话最多缓存的消息数量 */ -const MAX_MESSAGES_PER_SESSION = 500 - -/** 消息保留时间(天) */ -const MESSAGE_RETENTION_DAYS = 30 - -/** 内存去重缓存大小 */ -const DEDUP_CACHE_SIZE = 1000 - -// ==================== 类型定义 ==================== - -/** 消息更新回调函数 */ -type MessageUpdateCallback = (messages: ChatMessage[], sessionId: number) => void - -// ==================== 消息管理器类 ==================== - -export class MessageManager { - /** 回调函数集合(按会话ID分组) */ - private static updateCallbacks = new Map>() - - /** 消息去重表(内存级别,快速检查) */ - private static messageClientIdSet = new Set() - - // ==================== 回调管理 ==================== - - /** - * 订阅指定会话的消息变更 - * - * @param sessionId 会话ID - * @param callback 回调函数 - * @returns 取消订阅函数 - * - * @example - * const unsubscribe = MessageManager.onUpdate(123, (messages) => { - * console.log('消息更新:', messages) - * }) - * // 取消订阅 - * unsubscribe() - */ - static onUpdate(sessionId: number, callback: MessageUpdateCallback) { - if (!this.updateCallbacks.has(sessionId)) { - this.updateCallbacks.set(sessionId, new Set()) - } - this.updateCallbacks.get(sessionId)!.add(callback) - - return () => { - const callbacks = this.updateCallbacks.get(sessionId) - if (callbacks) { - callbacks.delete(callback) - if (callbacks.size === 0) { - this.updateCallbacks.delete(sessionId) - } - } - } - } - - /** - * 触发指定会话的回调 - * - * @param sessionId 会话ID - */ - private static async triggerCallbacks(sessionId: number) { - const callbacks = this.updateCallbacks.get(sessionId) - if (!callbacks || callbacks.size === 0) return - - try { - const messages = await this.getMessages(sessionId) - - callbacks.forEach((callback) => { - try { - callback(messages, sessionId) - } catch (error) { - console.error('消息更新回调执行失败:', error) - } - }) - } catch (error) { - console.error('触发回调失败:', error) - } - } - - // ==================== 消息去重 ==================== - - /** - * 检查消息是否重复 - * - * @param clientId 客户端消息ID - * @returns 是否重复 - */ - static async checkDuplicate(clientId: string): Promise { - // 1. 内存级别快速检查 - if (this.messageClientIdSet.has(clientId)) { - return true - } - - // 2. IndexedDB 检查(防止刷新后重复) - try { - const existing = await db() - .messages.where('clientId') - .equals(clientId) - .first() - - if (existing) { - // 补充到内存缓存 - this.messageClientIdSet.add(clientId) - return true - } - } catch (error) { - console.error('检查重复消息失败:', error) - } - - return false - } - - // ==================== 消息查询 ==================== - - /** - * 获取会话的消息列表 - * - * @param sessionId 会话ID - * @param offset 偏移量(分页) - * @param limit 数量限制 - * @returns 消息列表(已排序) - */ - static async getMessages( - sessionId: number, - offset = 0, - limit = 50 - ): Promise { - try { - const messages = await db() - .messages.where('sessionId') - .equals(sessionId) - .offset(offset) - .limit(limit) - .toArray() - - // 排序:优先按序列号,其次按时间戳 - return messages.sort((a, b) => { - // 1. 按序列号排序 - if (a.sequence !== undefined && b.sequence !== undefined) { - if (a.sequence !== b.sequence) { - return a.sequence - b.sequence - } - } - - // 2. 按时间戳排序 - if (a.wechatTime !== b.wechatTime) { - return a.wechatTime - b.wechatTime - } - - // 3. 按ID排序(兜底) - return a.id - b.id - }) - } catch (error) { - console.error('获取消息列表失败:', error) - return [] - } - } - - /** - * 获取会话的最新消息 - * - * @param sessionId 会话ID - * @param limit 数量限制 - * @returns 消息列表 - */ - static async getLatestMessages( - sessionId: number, - limit = 50 - ): Promise { - try { - const messages = await db() - .messages.where('sessionId') - .equals(sessionId) - .reverse() - .limit(limit) - .toArray() - - // 反转顺序(从旧到新) - return messages.reverse() - } catch (error) { - console.error('获取最新消息失败:', error) - return [] - } - } - - /** - * 获取消息总数 - * - * @param sessionId 会话ID - * @returns 消息总数 - */ - static async getMessageCount(sessionId: number): Promise { - try { - return await db().messages.where('sessionId').equals(sessionId).count() - } catch (error) { - console.error('获取消息数量失败:', error) - return 0 - } - } - - // ==================== 消息操作 ==================== - - /** - * 添加新消息 - * 自动检查重复、限制数量、触发回调 - * - * @param message 消息对象 - */ - static async addMessage(message: ChatMessage) { - try { - // 1. 检查重复 - if (await this.checkDuplicate(message.clientId)) { - console.log('⚠️ 重复消息,跳过:', message.clientId) - return - } - - // 2. 保存到数据库 - await db().messages.put(message) - - // 3. 添加到内存缓存 - this.messageClientIdSet.add(message.clientId) - - // 4. 限制内存缓存大小 - if (this.messageClientIdSet.size > DEDUP_CACHE_SIZE) { - const toRemove = Array.from(this.messageClientIdSet).slice( - 0, - this.messageClientIdSet.size - DEDUP_CACHE_SIZE - ) - toRemove.forEach((id) => this.messageClientIdSet.delete(id)) - } - - // 5. 检查是否超过数量限制 - const count = await this.getMessageCount(message.sessionId) - if (count > MAX_MESSAGES_PER_SESSION) { - await this.cleanOldMessages(message.sessionId) - } - - // 6. 触发回调 - await this.triggerCallbacks(message.sessionId) - } catch (error) { - console.error('添加消息失败:', error) - throw error - } - } - - /** - * 批量缓存消息 - * 用于从服务器加载历史消息 - * - * @param sessionId 会话ID - * @param messages 消息列表 - */ - static async cacheMessages(sessionId: number, messages: ChatMessage[]) { - try { - await db().transaction('rw', db().messages, async () => { - for (const message of messages) { - // 检查重复 - if (!(await this.checkDuplicate(message.clientId))) { - await db().messages.put(message) - this.messageClientIdSet.add(message.clientId) - } - } - }) - - // 检查是否超过限制 - const count = await this.getMessageCount(sessionId) - if (count > MAX_MESSAGES_PER_SESSION) { - await this.cleanOldMessages(sessionId) - } - - await this.triggerCallbacks(sessionId) - } catch (error) { - console.error('批量缓存消息失败:', error) - throw error - } - } - - /** - * 更新消息状态 - * - * @param messageId 消息ID - * @param updates 更新字段 - */ - static async updateMessage( - messageId: number, - updates: Partial - ) { - try { - const message = await db().messages.get(messageId) - if (!message) { - console.warn('消息不存在:', messageId) - return - } - - await db().messages.update(messageId, updates) - await this.triggerCallbacks(message.sessionId) - } catch (error) { - console.error('更新消息失败:', error) - throw error - } - } - - /** - * 撤回消息 - * - * @param messageId 消息ID - */ - static async recallMessage(messageId: number) { - try { - await this.updateMessage(messageId, { - content: '[消息已撤回]', - isRecalled: true, - status: 'success', - }) - } catch (error) { - console.error('撤回消息失败:', error) - throw error - } - } - - /** - * 标记消息已读 - * - * @param messageIds 消息ID列表 - */ - static async markAsRead(messageIds: number[]) { - try { - await db().transaction('rw', db().messages, async () => { - for (const id of messageIds) { - await db().messages.update(id, { isRead: true }) - } - }) - } catch (error) { - console.error('标记已读失败:', error) - throw error - } - } - - // ==================== 清理操作 ==================== - - /** - * 清理会话的旧消息 - * 保留最新的 MAX_MESSAGES_PER_SESSION 条 - * - * @param sessionId 会话ID - */ - static async cleanOldMessages(sessionId: number) { - try { - const messages = await db() - .messages.where('sessionId') - .equals(sessionId) - .toArray() - - if (messages.length <= MAX_MESSAGES_PER_SESSION) { - return - } - - // 按时间排序 - messages.sort((a, b) => a.wechatTime - b.wechatTime) - - // 删除旧消息 - const toDelete = messages - .slice(0, messages.length - MAX_MESSAGES_PER_SESSION) - .map((m) => m.id) - - await db().messages.bulkDelete(toDelete) - - console.log(`🗑️ 清理了会话 ${sessionId} 的 ${toDelete.length} 条旧消息`) - } catch (error) { - console.error('清理旧消息失败:', error) - } - } - - /** - * 清理过期消息 - * 删除超过 MESSAGE_RETENTION_DAYS 天的消息 - */ - static async cleanExpiredMessages() { - try { - const expireTime = Date.now() - MESSAGE_RETENTION_DAYS * 24 * 60 * 60 * 1000 - - const deletedCount = await db() - .messages.where('wechatTime') - .below(expireTime) - .delete() - - console.log(`🗑️ 清理了 ${deletedCount} 条过期消息`) - } catch (error) { - console.error('清理过期消息失败:', error) - } - } - - /** - * 清空会话的所有消息 - * - * @param sessionId 会话ID - */ - static async clearSessionMessages(sessionId: number) { - try { - await db().messages.where('sessionId').equals(sessionId).delete() - await this.triggerCallbacks(sessionId) - } catch (error) { - console.error('清空会话消息失败:', error) - throw error - } - } - - /** - * 清空所有消息(慎用) - */ - static async clearAll() { - try { - await db().messages.clear() - this.messageClientIdSet.clear() - this.updateCallbacks.clear() - } catch (error) { - console.error('清空所有消息失败:', error) - throw error - } - } - - // ==================== 搜索功能 ==================== - - /** - * 搜索消息 - * - * @param sessionId 会话ID - * @param keyword 关键词 - * @param limit 数量限制 - * @returns 消息列表 - */ - static async searchMessages( - sessionId: number, - keyword: string, - limit = 50 - ): Promise { - try { - const messages = await db() - .messages.where('sessionId') - .equals(sessionId) - .toArray() - - // 过滤包含关键词的消息 - const results = messages.filter((m) => - m.content.toLowerCase().includes(keyword.toLowerCase()) - ) - - // 按时间倒序 - results.sort((a, b) => b.wechatTime - a.wechatTime) - - return results.slice(0, limit) - } catch (error) { - console.error('搜索消息失败:', error) - return [] - } - } - - // ==================== 统计功能 ==================== - - /** - * 获取会话的消息统计 - * - * @param sessionId 会话ID - * @returns 统计信息 - */ - static async getStatistics(sessionId: number): Promise<{ - total: number - unreadCount: number - sendCount: number - receiveCount: number - todayCount: number - }> { - try { - const messages = await db() - .messages.where('sessionId') - .equals(sessionId) - .toArray() - - const todayStart = new Date().setHours(0, 0, 0, 0) - - return { - total: messages.length, - unreadCount: messages.filter((m) => !m.isRead).length, - sendCount: messages.filter((m) => m.direction === 'send').length, - receiveCount: messages.filter((m) => m.direction === 'receive').length, - todayCount: messages.filter((m) => m.wechatTime >= todayStart).length, - } - } catch (error) { - console.error('获取统计信息失败:', error) - return { - total: 0, - unreadCount: 0, - sendCount: 0, - receiveCount: 0, - todayCount: 0, - } - } - } - - /** - * 是否应该缓存消息 - * 根据会话类型和配置决定 - * - * @param sessionId 会话ID - * @returns 是否缓存 - */ - static shouldCacheMessage(sessionId: number): boolean { - // 默认缓存所有消息 - // 可以根据业务需求添加更多判断逻辑 - // 例如:只缓存重要会话、最近活跃会话等 - return true - } -} diff --git a/TouchVueThree/src/utils/dbManagers/SessionManager.ts b/TouchVueThree/src/utils/dbManagers/SessionManager.ts deleted file mode 100644 index f6b141b..0000000 --- a/TouchVueThree/src/utils/dbManagers/SessionManager.ts +++ /dev/null @@ -1,715 +0,0 @@ -/** - * 会话管理器 - 负责会话数据的 CRUD 和订阅机制 - * - * 核心特性: - * - 订阅机制:替代轮询,数据变更时自动通知 - * - 同步控制:防止竞态条件 - * - 动态创建:陌生好友消息自动获取详情并创建会话 - * - 批量同步:从服务器批量同步会话数据 - * - * @author TouchVueThree Team - * @date 2026-01-13 - */ - -import { db } from '../db' -import type { ChatSession } from '../db' -import { getFriendDetail, getGroupDetail } from '@/api/modules/wechat' - -// ==================== 类型定义 ==================== - -/** 会话更新回调函数 */ -type SessionUpdateCallback = (sessions: ChatSession[], accountId?: number) => void - -// ==================== 会话管理器类 ==================== - -export class SessionManager { - /** 回调函数集合 */ - private static updateCallbacks = new Set() - - /** 是否正在同步(防止竞态条件) */ - private static isSyncing = false - - /** 同步期间的待处理更新 */ - private static pendingUpdates = new Map>() - - // ==================== 排序键生成 ==================== - - /** - * 生成会话排序键(与旧项目一致) - * 格式: ${isTop}|${timestamp}|${displayName} - * - * @param session 会话数据 - * @returns 排序键字符串 - */ - static generateSortKey(session: ChatSession): string { - // 1. 置顶标识:置顶为1,普通为0(降序时置顶在前) - const isTop = session.config?.top ? 1 : 0 - - // 2. 时间戳:使用 msgTime 或 lastUpdateTime - const timestamp = session.config?.msgTime || - new Date(session.lastUpdateTime || new Date()).getTime() - - // 3. 显示名称:用于稳定排序 - const displayName = ( - session.conRemark || - session.nickname || - '' - ).toLowerCase() - - // 格式:置顶标识|时间戳|显示名称 - // 降序排序:置顶(1)在前,时间大的在前,名称小的在前 - return `${isTop}|${timestamp}|${displayName}` - } - - // ==================== 回调管理 ==================== - - /** - * 订阅会话变更 - * 当会话数据发生变化时,会调用回调函数 - * - * @param callback 回调函数 - * @returns 取消订阅函数 - * - * @example - * const unsubscribe = SessionManager.onUpdate((sessions) => { - * console.log('会话更新:', sessions) - * }) - * // 取消订阅 - * unsubscribe() - */ - static onUpdate(callback: SessionUpdateCallback) { - this.updateCallbacks.add(callback) - return () => this.updateCallbacks.delete(callback) - } - - /** - * 触发所有回调 - * - * @param accountId 账号ID(可选,用于过滤) - */ - private static async triggerCallbacks(accountId?: number) { - try { - const sessions = await this.getUserSessions(accountId) - - this.updateCallbacks.forEach((callback) => { - try { - callback(sessions, accountId) - } catch (error) { - console.error('会话更新回调执行失败:', error) - } - }) - } catch (error) { - console.error('触发回调失败:', error) - } - } - - // ==================== 同步控制 ==================== - - /** - * 开始同步 - * 同步期间的更新会暂存,同步结束后统一应用 - */ - static beginSync() { - this.isSyncing = true - this.pendingUpdates.clear() - console.log('📥 开始同步会话...') - } - - /** - * 结束同步 - * 应用同步期间的待处理更新 - * - * @param accountId 账号ID - */ - static async endSync(accountId?: number) { - this.isSyncing = false - - if (this.pendingUpdates.size > 0) { - console.log(`📤 应用 ${this.pendingUpdates.size} 个待处理更新`) - - for (const [sessionId, update] of this.pendingUpdates) { - try { - await db().sessions.update(sessionId, update) - } catch (error) { - console.error(`更新会话 ${sessionId} 失败:`, error) - } - } - - this.pendingUpdates.clear() - await this.triggerCallbacks(accountId) - } - - console.log('✅ 同步完成') - } - - // ==================== 数据查询 ==================== - - /** - * 获取用户的所有会话 - * 支持按账号ID过滤 - * - * @param accountId 账号ID(可选,0或undefined表示全部) - * @returns 会话列表(已排序) - */ - static async getUserSessions(accountId?: number): Promise { - try { - let query = db().sessions.toCollection() - - // 如果指定了账号ID,进行过滤 - if (accountId && accountId !== 0) { - query = db().sessions.where('wechatAccountId').equals(accountId) - } - - const sessions = await query.toArray() - - // 排序:置顶在前,时间倒序 - return sessions.sort((a, b) => { - // 置顶优先 - if (a.config.top && !b.config.top) return -1 - if (!a.config.top && b.config.top) return 1 - - // 按消息时间倒序 - return b.config.msgTime - a.config.msgTime - }) - } catch (error) { - console.error('获取会话列表失败:', error) - throw error - } - } - - /** - * 获取单个会话 - * - * @param sessionId 会话ID - * @returns 会话信息或 undefined - */ - static async getSession(sessionId: number): Promise { - try { - return await db().sessions.get(sessionId) - } catch (error) { - console.error('获取会话失败:', error) - return undefined - } - } - - // ==================== 数据操作 ==================== - - /** - * 添加或更新会话(自动更新 sortKey) - * - * @param session 会话信息 - */ - static async upsertSession(session: ChatSession) { - try { - // ⭐ 自动生成并更新 sortKey - const sessionWithSortKey = { - ...session, - sortKey: this.generateSortKey(session), - } - - await db().sessions.put(sessionWithSortKey) - await this.triggerCallbacks(session.wechatAccountId) - } catch (error) { - console.error('保存会话失败:', error) - throw error - } - } - - /** - * 批量同步会话 - * 用于从服务器批量同步数据 - * - * @param sessions 会话列表 - */ - static async syncSessions(sessions: ChatSession[]) { - try { - await db().transaction('rw', db().sessions, async () => { - for (const session of sessions) { - // ⭐ 确保每个会话都有正确的 sortKey - const sessionWithSortKey = { - ...session, - sortKey: session.sortKey || this.generateSortKey(session), - } - await db().sessions.put(sessionWithSortKey) - } - }) - await this.triggerCallbacks() - } catch (error) { - console.error('批量同步会话失败:', error) - throw error - } - } - - /** - * 收到新消息时更新会话 - * - * ⭐ 关键功能:如果会话不存在,自动获取好友详情并创建 - * - * @param sessionId 会话ID(好友ID或群ID) - * @param sessionType 会话类型 - * @param content 消息内容 - * @param wechatAccountId 账号ID - */ - static async updateOnNewMessage( - sessionId: number, - sessionType: 'friend' | 'group', - content: string, - wechatAccountId?: number - ) { - try { - const update: Partial = { - content, - lastUpdateTime: new Date().toISOString(), - sortKey: `${Date.now()}_${sessionId}`, - } - - // 如果正在同步,暂存更新 - if (this.isSyncing) { - console.log('⏸️ 同步进行中,暂存更新:', sessionId) - const existing = this.pendingUpdates.get(sessionId) - this.pendingUpdates.set(sessionId, { ...existing, ...update }) - return - } - - // 检查会话是否存在 - const existing = await db().sessions.get(sessionId) - - if (existing) { - // ✅ 会话已存在,直接更新 - const updatedSession = { - ...existing, - ...update, - config: { - ...existing.config, - msgTime: Date.now(), - unreadCount: (existing.config.unreadCount || 0) + 1, - }, - } - - // ⭐ 自动更新 sortKey - updatedSession.sortKey = this.generateSortKey(updatedSession) - - await db().sessions.update(sessionId, updatedSession) - await this.triggerCallbacks(wechatAccountId) - return - } - - // ⭐ 会话不存在 - 动态创建 - console.warn(`⚠️ 会话 ${sessionId} 不存在,动态创建...`) - await this.createSessionFromMessage( - sessionId, - sessionType, - content, - wechatAccountId - ) - } catch (error) { - console.error('更新会话失败:', error) - throw error - } - } - - /** - * 根据消息创建新会话(陌生好友/群聊) - * - * @param sessionId 会话ID - * @param sessionType 会话类型 - * @param content 消息内容 - * @param wechatAccountId 账号ID - */ - private static async createSessionFromMessage( - sessionId: number, - sessionType: 'friend' | 'group', - content: string, - wechatAccountId?: number - ) { - try { - // 1. 根据类型调用不同接口获取详情 - console.log(`🔍 获取${sessionType === 'friend' ? '好友' : '群聊'}详情...`) - - const contactInfo = - sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 2. 创建新会话 - const newSession: ChatSession = { - id: sessionId, - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: wechatAccountId || 0, - nickname: contactInfo.nickname || contactInfo.name || '未知用户', - conRemark: contactInfo.conRemark || contactInfo.remark, - avatar: contactInfo.avatar || '/default-avatar.png', - wxid: contactInfo.wxid, - chatroomId: sessionType === 'group' ? contactInfo.chatroomId : undefined, - content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: Date.now(), - chat: true, - }, - sortKey: '', // 临时占位,稍后生成 - } - - // ⭐ 使用 generateSortKey 生成正确的 sortKey - newSession.sortKey = this.generateSortKey(newSession) - - // 3. 保存到数据库 - await db().sessions.put(newSession) - - console.log('✅ 新会话已创建:', newSession.nickname || sessionId) - - // 4. 触发回调,更新 UI - await this.triggerCallbacks(wechatAccountId) - } catch (error) { - console.error('获取好友详情失败,使用降级方案:', error) - - // ⚠️ 降级方案:创建临时会话 - await this.createFallbackSession( - sessionId, - sessionType, - content, - wechatAccountId - ) - } - } - - /** - * 创建临时会话(降级方案) - * 当获取好友详情失败时使用 - * - * @param sessionId 会话ID - * @param sessionType 会话类型 - * @param content 消息内容 - * @param wechatAccountId 账号ID - */ - private static async createFallbackSession( - sessionId: number, - sessionType: 'friend' | 'group', - content: string, - wechatAccountId?: number - ) { - const tempSession: ChatSession = { - id: sessionId, - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: wechatAccountId || 0, - nickname: '未知用户', - avatar: '/default-avatar.png', - content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: Date.now(), - chat: true, - }, - sortKey: '', // 临时占位,稍后生成 - } - - // ⭐ 使用 generateSortKey 生成正确的 sortKey - tempSession.sortKey = this.generateSortKey(tempSession) - - await db().sessions.put(tempSession) - await this.triggerCallbacks(wechatAccountId) - - console.log('⚠️ 已创建临时会话,后台继续重试获取详情') - - // 后台继续重试 - this.retryGetContactInfo(sessionId, sessionType, wechatAccountId) - } - - /** - * 重试获取联系人信息 - * 3次重试,指数退避:5s、10s、15s - * - * @param sessionId 会话ID - * @param sessionType 会话类型 - * @param wechatAccountId 账号ID - */ - private static async retryGetContactInfo( - sessionId: number, - sessionType: 'friend' | 'group', - wechatAccountId?: number - ) { - let retryCount = 0 - const maxRetries = 3 - - while (retryCount < maxRetries) { - try { - // 指数退避:5秒、10秒、15秒 - await new Promise((resolve) => - setTimeout(resolve, 5000 * (retryCount + 1)) - ) - - console.log(`🔄 重试获取联系人信息 (${retryCount + 1}/${maxRetries})`) - - const contactInfo = - sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 更新会话详情 - await db().sessions.update(sessionId, { - nickname: contactInfo.nickname || contactInfo.name, - conRemark: contactInfo.conRemark || contactInfo.remark, - avatar: contactInfo.avatar, - wxid: contactInfo.wxid, - chatroomId: - sessionType === 'group' ? contactInfo.chatroomId : undefined, - }) - - console.log(`✅ 重试成功,已更新会话 ${sessionId} 的详细信息`) - await this.triggerCallbacks(wechatAccountId) - break // 成功则退出循环 - } catch (error) { - retryCount++ - console.warn( - `重试获取联系人信息失败 (${retryCount}/${maxRetries})`, - error - ) - - if (retryCount >= maxRetries) { - // 达到最大重试次数,记录错误 - console.error(`❌ 无法获取会话${sessionId}的详细信息`) - // TODO: 上报到 Sentry - // Sentry.captureException( - // new Error(`无法获取会话${sessionId}的详细信息`), - // { extra: { sessionId, sessionType, retryCount } } - // ) - } - } - } - } - - /** - * 清除未读数 - * - * @param sessionId 会话ID - * @param accountId 账号ID - */ - static async clearUnread(sessionId: number, accountId?: number) { - try { - await db().sessions.update(sessionId, { - 'config.unreadCount': 0, - }) - await this.triggerCallbacks(accountId) - } catch (error) { - console.error('清除未读失败:', error) - } - } - - /** - * 删除会话 - * - * @param sessionId 会话ID - * @param accountId 账号ID - */ - static async deleteSession(sessionId: number, accountId?: number) { - try { - await db().sessions.delete(sessionId) - await this.triggerCallbacks(accountId) - } catch (error) { - console.error('删除会话失败:', error) - throw error - } - } - - /** - * 置顶/取消置顶会话(自动更新 sortKey) - * - * @param sessionId 会话ID - * @param accountId 账号ID - */ - static async togglePin(sessionId: number, accountId?: number) { - try { - const session = await db().sessions.get(sessionId) - if (session) { - const updatedSession = { - ...session, - config: { - ...session.config, - top: !session.config.top, - }, - } - - // ⭐ 自动更新 sortKey(置顶状态变化会影响排序) - updatedSession.sortKey = this.generateSortKey(updatedSession) - - await db().sessions.update(sessionId, updatedSession) - await this.triggerCallbacks(accountId) - } - } catch (error) { - console.error('切换置顶状态失败:', error) - throw error - } - } - - /** - * 清空所有会话(慎用) - */ - static async clearAll() { - try { - await db().sessions.clear() - await this.triggerCallbacks() - } catch (error) { - console.error('清空会话失败:', error) - throw error - } - } - - /** - * 获取会话统计信息 - */ - static async getStatistics(accountId?: number): Promise<{ - total: number - unreadCount: number - topCount: number - }> { - try { - const sessions = await this.getUserSessions(accountId) - - return { - total: sessions.length, - unreadCount: sessions.reduce( - (sum, s) => sum + (s.config.unreadCount || 0), - 0 - ), - topCount: sessions.filter((s) => s.config.top).length, - } - } catch (error) { - console.error('获取统计信息失败:', error) - return { total: 0, unreadCount: 0, topCount: 0 } - } - } - - // ==================== 数据联动同步 ==================== - - /** - * 从详情数据同步到本地数据库 - * ⭐ 核心方法:确保 API 返回的最新数据同步到本地 - * - * 使用场景: - * - 修改好友备注后,重新获取详情并同步到本地 - * - 修改扩展字段后,重新获取详情并同步到本地 - * - 修改标签后,重新获取详情并同步到本地 - * - 修改群聊名称后,重新获取详情并同步到本地 - * - * @param detail API 返回的详情数据 - * @param sessionId 会话 ID - * @param sessionType 会话类型 ('friend' | 'group') - */ - static async syncSessionFromDetail( - detail: any, - sessionId: number, - sessionType: 'friend' | 'group' - ): Promise { - try { - // 1. 从数据库获取现有会话 - const existingSession = await db().sessions.get(sessionId) - - if (!existingSession) { - console.warn(`[SessionManager] 会话 ${sessionId} 不存在,跳过同步`) - return - } - - // 2. 构建更新数据 - const updates: Partial = {} - let hasChanges = false - - if (sessionType === 'friend') { - // 好友类型:同步备注、扩展字段等 - if (detail.conRemark !== undefined && detail.conRemark !== existingSession.conRemark) { - updates.conRemark = detail.conRemark - hasChanges = true - } - if (detail.nickname !== undefined && detail.nickname !== existingSession.nickname) { - updates.nickname = detail.nickname - hasChanges = true - } - if (detail.avatar !== undefined && detail.avatar !== existingSession.avatar) { - updates.avatar = detail.avatar - hasChanges = true - } - if (detail.wxid !== undefined && detail.wxid !== existingSession.wxid) { - updates.wxid = detail.wxid - hasChanges = true - } - - // 同步扩展字段 - if (detail.extendFields !== undefined && detail.extendFields !== existingSession.extendFields) { - updates.extendFields = detail.extendFields - hasChanges = true - } - - // 同步标签 - if (detail.labels !== undefined) { - const detailLabels = Array.isArray(detail.labels) ? detail.labels : (detail.labels || '').split(',').filter(Boolean) - const existingLabels = existingSession.labels || [] - if (JSON.stringify(detailLabels) !== JSON.stringify(existingLabels)) { - updates.labels = detailLabels - hasChanges = true - } - } - } else { - // 群聊类型:同步群名、群主等 - if (detail.nickname !== undefined && detail.nickname !== existingSession.nickname) { - updates.nickname = detail.nickname - hasChanges = true - } - if (detail.chatroomName !== undefined && detail.chatroomName !== existingSession.nickname) { - updates.nickname = detail.chatroomName - hasChanges = true - } - if (detail.avatar !== undefined && detail.avatar !== existingSession.avatar) { - updates.avatar = detail.avatar - hasChanges = true - } - if (detail.chatroomId !== undefined && detail.chatroomId !== existingSession.chatroomId) { - updates.chatroomId = detail.chatroomId - hasChanges = true - } - } - - // 3. 如果有更新,重新生成 sortKey - if (hasChanges) { - const updatedSession = { ...existingSession, ...updates } - updates.sortKey = this.generateSortKey(updatedSession) - updates.lastUpdateTime = new Date().toISOString() - - // 4. 更新到数据库 - await db().sessions.update(sessionId, updates) - - console.log(`[SessionManager] ✅ 从详情同步会话数据: ${sessionId}`, updates) - - // 5. 触发订阅回调(自动更新 UI) - await this.triggerCallbacks(existingSession.wechatAccountId) - } else { - console.log(`[SessionManager] ℹ️ 会话 ${sessionId} 数据无变化,跳过更新`) - } - } catch (error) { - console.error('[SessionManager] ❌ 从详情同步会话失败:', error) - throw error - } - } - - /** - * 快捷方法:从好友详情同步 - * @param friendId 好友ID - * @param detail API 返回的好友详情数据 - */ - static async syncSessionFromFriendDetail(friendId: number, detail: any): Promise { - return this.syncSessionFromDetail(detail, friendId, 'friend') - } - - /** - * 快捷方法:从群聊详情同步 - * @param groupId 群聊ID - * @param detail API 返回的群聊详情数据 - */ - static async syncSessionFromGroupDetail(groupId: number, detail: any): Promise { - return this.syncSessionFromDetail(detail, groupId, 'group') - } -} diff --git a/TouchVueThree/src/utils/emoji.ts b/TouchVueThree/src/utils/emoji.ts deleted file mode 100644 index f021d81..0000000 --- a/TouchVueThree/src/utils/emoji.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * 微信表情包工具函数 - * 提供表情解析、路径获取等功能 - */ - -// 导入表情映射数据(从 wechatEmoji 模块) -import { getEmojiPath, getEmojiNameByPath } from '@/utils/wechatEmoji' -import type { EmojiName } from '@/utils/wechatEmoji' - -/** - * 解析文本中的表情标记 [表情名称] 并替换为 HTML img 标签 - * @param text 原始文本 - * @returns 解析后的 HTML 字符串 - * - * @example - * ```typescript - * const html = parseEmojiText('你好[微笑]') - * // 返回: '你好' - * ``` - */ -export function parseEmojiText(text: string): string { - if (!text || typeof text !== 'string') { - return text || '' - } - - // 匹配 [表情名称] 格式 - const emojiRegex = /\[([^\]]+)\]/g - let result = text - let match: RegExpExecArray | null - - while ((match = emojiRegex.exec(text)) !== null) { - const emojiName = match[1] as EmojiName - const emojiPath = getEmojiPath(emojiName) - - if (emojiPath) { - // 转义 HTML 特殊字符(避免 XSS) - const escapedName = escapeHtml(emojiName) - const escapedPath = escapeHtml(emojiPath) - - // 替换为 img 标签,添加 data-emoji-name 标识属性 - const imgTag = `` - result = result.replace(match[0], imgTag) - } - } - - return result -} - -/** - * HTML 转义 - */ -function escapeHtml(text: string): string { - const map: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - } - return text.replace(/[&<>"']/g, m => map[m]) -} - -/** - * 判断是否为表情包 URL - * @param content 内容字符串 - * @returns 是否为表情包 URL - */ -export function isEmojiUrl(content: string): boolean { - if (!content || typeof content !== 'string') { - return false - } - - return ( - content.includes('ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com') || - /\.(gif|webp|png|jpg|jpeg)$/i.test(content) || - content.includes('emoji') || - content.includes('sticker') || - content.includes('expression') - ) -} - -/** - * 将 HTML 内容转换为文本格式(包含 [表情名称]) - * @param html HTML 内容 - * @returns 文本格式(表情用 [表情名称] 表示) - */ -export function htmlToText(html: string): string { - if (!html) return '' - - // 创建一个临时 DOM 元素来解析 HTML - const tempDiv = document.createElement('div') - tempDiv.innerHTML = html - - // 遍历所有 img 标签,替换为 [表情名称] - const images = tempDiv.querySelectorAll('img.emoji-image') - images.forEach((img) => { - // 优先使用 data-emoji-name 属性 - let emojiName = img.getAttribute('data-emoji-name') - - // 如果没有 data-emoji-name,使用 alt 属性 - if (!emojiName) { - emojiName = img.getAttribute('alt') - } - - // 如果还是没有,根据图片路径查找表情名称 - if (!emojiName) { - const src = img.getAttribute('src') - if (src) { - emojiName = getEmojiNameByPath(src) - } - } - - if (emojiName) { - const textNode = document.createTextNode(`[${emojiName}]`) - img.parentNode?.replaceChild(textNode, img) - } - }) - - // 获取纯文本内容 - return tempDiv.textContent || tempDiv.innerText || '' -} - -/** - * 将文本格式转换为 HTML(将 [表情名称] 转换为 标签) - * @param text 文本内容 - * @returns HTML 内容 - */ -export function textToHtml(text: string): string { - if (!text) return '' - - // 使用 parseEmojiText 函数将 [表情名称] 转换为 标签 - return parseEmojiText(text) -} - -/** - * 获取 HTML 内容的纯文本长度(不包括 HTML 标签) - * @param html HTML 内容 - * @returns 纯文本长度 - */ -export function getHtmlTextLength(html: string): number { - return htmlToText(html).length -} diff --git a/TouchVueThree/src/utils/wechatEmoji.ts b/TouchVueThree/src/utils/wechatEmoji.ts deleted file mode 100644 index fde152f..0000000 --- a/TouchVueThree/src/utils/wechatEmoji.ts +++ /dev/null @@ -1,931 +0,0 @@ -/** - * 微信表情包 TypeScript 模块 - * 提供类型安全的表情访问和图片路径获取功能 - */ - -/** - * 表情类别枚举 - */ -export enum EmojiCategory { - /** 人脸表情 */ - FACE = 'face', - /** 手势表情 */ - GESTURE = 'gesture', - /** 动物表情 */ - ANIMAL = 'animal', - /** 祝福表情 */ - BLESSING = 'blessing', - /** 其他表情 */ - OTHER = 'other', -} - -/** - * 表情信息接口 - */ -export interface EmojiInfo { - /** 表情名称 */ - name: string - /** 表情类别 */ - category: EmojiCategory - /** 图片文件路径 */ - path: string - /** 英文名称(可选) */ - englishName?: string -} - -/** - * 表情名称类型 - */ -export type EmojiName = - // 人脸表情 - | '微笑' - | '撇嘴' - | '色' - | '发呆' - | '得意' - | '流泪' - | '害羞' - | '闭嘴' - | '睡' - | '大哭' - | '尴尬' - | '发怒' - | '调皮' - | '呲牙' - | '惊讶' - | '难过' - | '囧' - | '抓狂' - | '吐' - | '偷笑' - | '愉快' - | '白眼' - | '傲慢' - | '困' - | '惊恐' - | '憨笑' - | '悠闲' - | '咒骂' - | '疑问' - | '嘘' - | '晕' - | '衰' - | '骷髅' - | '敲打' - | '再见' - | '擦汗' - | '抠鼻' - | '鼓掌' - | '坏笑' - | '右哼哼' - | '鄙视' - | '委屈' - | '快哭了' - | '阴险' - | '亲亲' - | '可怜' - | '笑脸' - | '生病' - | '脸红' - | '破涕为笑' - | '恐惧' - | '失望' - | '无语' - | '嘿哈' - | '捂脸' - | '机智' - | '皱眉' - | '耶' - | '吃瓜' - | '加油' - | '汗' - | '天啊' - | 'Emm' - | '社会社会' - | '旺柴' - | '好的' - | '打脸' - | '哇' - | '翻白眼' - | '666' - | '让我看看' - | '叹气' - | '苦涩' - | '裂开' - | '奸笑' - // 手势表情 - | '握手' - | '胜利' - | '抱拳' - | '勾引' - | '拳头' - | 'OK' - | '合十' - | '强' - | '拥抱' - | '弱' - // 动物表情 - | '猪头' - | '跳跳' - | '发抖' - | '转圈' - // 祝福表情 - | '庆祝' - | '礼物' - | '红包' - | '發' - | '福' - | '烟花' - | '爆竹' - // 其他表情 - | '嘴唇' - | '爱心' - | '心碎' - | '啤酒' - | '咖啡' - | '蛋糕' - | '凋谢' - | '菜刀' - | '炸弹' - | '便便' - | '太阳' - | '月亮' - | '玫瑰' - -/** - * 表情数据映射 - * 将表情名称映射到完整的表情信息 - */ -const EMOJI_DATA: Record = { - // 人脸表情 - 微笑: { - name: '微笑', - category: EmojiCategory.FACE, - path: '/assets/face/smile.png', - }, - 撇嘴: { - name: '撇嘴', - category: EmojiCategory.FACE, - path: '/assets/face/pout.png', - }, - 色: { - name: '色', - category: EmojiCategory.FACE, - path: '/assets/face/lustful.png', - }, - 发呆: { - name: '发呆', - category: EmojiCategory.FACE, - path: '/assets/face/daze.png', - }, - 得意: { - name: '得意', - category: EmojiCategory.FACE, - path: '/assets/face/smug.png', - }, - 流泪: { - name: '流泪', - category: EmojiCategory.FACE, - path: '/assets/face/crying.png', - }, - 害羞: { - name: '害羞', - category: EmojiCategory.FACE, - path: '/assets/face/shy.png', - }, - 闭嘴: { - name: '闭嘴', - category: EmojiCategory.FACE, - path: '/assets/face/shut-up.png', - }, - 睡: { - name: '睡', - category: EmojiCategory.FACE, - path: '/assets/face/sleep.png', - }, - 大哭: { - name: '大哭', - category: EmojiCategory.FACE, - path: '/assets/face/wail.png', - }, - 尴尬: { - name: '尴尬', - category: EmojiCategory.FACE, - path: '/assets/face/awkward.png', - }, - 发怒: { - name: '发怒', - category: EmojiCategory.FACE, - path: '/assets/face/angry.png', - }, - 调皮: { - name: '调皮', - category: EmojiCategory.FACE, - path: '/assets/face/naughty.png', - }, - 呲牙: { - name: '呲牙', - category: EmojiCategory.FACE, - path: '/assets/face/grin.png', - }, - 惊讶: { - name: '惊讶', - category: EmojiCategory.FACE, - path: '/assets/face/surprised.png', - }, - 难过: { - name: '难过', - category: EmojiCategory.FACE, - path: '/assets/face/sad.png', - }, - 囧: { - name: '囧', - category: EmojiCategory.FACE, - path: '/assets/face/embarrassed.png', - }, - 抓狂: { - name: '抓狂', - category: EmojiCategory.FACE, - path: '/assets/face/crazy.png', - }, - 吐: { - name: '吐', - category: EmojiCategory.FACE, - path: '/assets/face/vomit.png', - }, - 偷笑: { - name: '偷笑', - category: EmojiCategory.FACE, - path: '/assets/face/snicker.png', - }, - 愉快: { - name: '愉快', - category: EmojiCategory.FACE, - path: '/assets/face/happy.png', - }, - 白眼: { - name: '白眼', - category: EmojiCategory.FACE, - path: '/assets/face/roll-eyes.png', - }, - 傲慢: { - name: '傲慢', - category: EmojiCategory.FACE, - path: '/assets/face/arrogant.png', - }, - 困: { - name: '困', - category: EmojiCategory.FACE, - path: '/assets/face/sleepy.png', - }, - 惊恐: { - name: '惊恐', - category: EmojiCategory.FACE, - path: '/assets/face/panic.png', - }, - 憨笑: { - name: '憨笑', - category: EmojiCategory.FACE, - path: '/assets/face/silly-smile.png', - }, - 悠闲: { - name: '悠闲', - category: EmojiCategory.FACE, - path: '/assets/face/leisurely.png', - }, - 咒骂: { - name: '咒骂', - category: EmojiCategory.FACE, - path: '/assets/face/curse.png', - }, - 疑问: { - name: '疑问', - category: EmojiCategory.FACE, - path: '/assets/face/question.png', - }, - 嘘: { - name: '嘘', - category: EmojiCategory.FACE, - path: '/assets/face/shush.png', - }, - 晕: { - name: '晕', - category: EmojiCategory.FACE, - path: '/assets/face/dizzy.png', - }, - 衰: { - name: '衰', - category: EmojiCategory.FACE, - path: '/assets/face/unlucky.png', - }, - 骷髅: { - name: '骷髅', - category: EmojiCategory.FACE, - path: '/assets/face/skull.png', - }, - 敲打: { - name: '敲打', - category: EmojiCategory.FACE, - path: '/assets/face/knock.png', - }, - 再见: { - name: '再见', - category: EmojiCategory.FACE, - path: '/assets/face/goodbye.png', - }, - 擦汗: { - name: '擦汗', - category: EmojiCategory.FACE, - path: '/assets/face/wipe-sweat.png', - }, - 抠鼻: { - name: '抠鼻', - category: EmojiCategory.FACE, - path: '/assets/face/pick-nose.png', - }, - 鼓掌: { - name: '鼓掌', - category: EmojiCategory.FACE, - path: '/assets/face/clap.png', - }, - 坏笑: { - name: '坏笑', - category: EmojiCategory.FACE, - path: '/assets/face/evil-smile.png', - }, - 右哼哼: { - name: '右哼哼', - category: EmojiCategory.FACE, - path: '/assets/face/right-hum.png', - }, - 鄙视: { - name: '鄙视', - category: EmojiCategory.FACE, - path: '/assets/face/despise.png', - }, - 委屈: { - name: '委屈', - category: EmojiCategory.FACE, - path: '/assets/face/wronged.png', - }, - 快哭了: { - name: '快哭了', - category: EmojiCategory.FACE, - path: '/assets/face/about-to-cry.png', - }, - 阴险: { - name: '阴险', - category: EmojiCategory.FACE, - path: '/assets/face/sinister.png', - }, - 亲亲: { - name: '亲亲', - category: EmojiCategory.FACE, - path: '/assets/face/kiss.png', - }, - 可怜: { - name: '可怜', - category: EmojiCategory.FACE, - path: '/assets/face/pitiful.png', - }, - 笑脸: { - name: '笑脸', - category: EmojiCategory.FACE, - path: '/assets/face/smiley.png', - }, - 生病: { - name: '生病', - category: EmojiCategory.FACE, - path: '/assets/face/sick.png', - }, - 脸红: { - name: '脸红', - category: EmojiCategory.FACE, - path: '/assets/face/blush.png', - }, - 破涕为笑: { - name: '破涕为笑', - category: EmojiCategory.FACE, - path: '/assets/face/tears-to-smile.png', - }, - 恐惧: { - name: '恐惧', - category: EmojiCategory.FACE, - path: '/assets/face/fear.png', - }, - 失望: { - name: '失望', - category: EmojiCategory.FACE, - path: '/assets/face/disappointed.png', - }, - 无语: { - name: '无语', - category: EmojiCategory.FACE, - path: '/assets/face/speechless.png', - }, - 嘿哈: { - name: '嘿哈', - category: EmojiCategory.FACE, - path: '/assets/face/hey-ha.png', - }, - 捂脸: { - name: '捂脸', - category: EmojiCategory.FACE, - path: '/assets/face/facepalm.png', - }, - 机智: { - name: '机智', - category: EmojiCategory.FACE, - path: '/assets/face/smart.png', - }, - 皱眉: { - name: '皱眉', - category: EmojiCategory.FACE, - path: '/assets/face/frown.png', - }, - 耶: { - name: '耶', - category: EmojiCategory.FACE, - path: '/assets/face/yeah.png', - }, - 吃瓜: { - name: '吃瓜', - category: EmojiCategory.FACE, - path: '/assets/face/eat-melon.png', - }, - 加油: { - name: '加油', - category: EmojiCategory.FACE, - path: '/assets/face/cheer-up.png', - }, - - 汗: { - name: '汗', - category: EmojiCategory.FACE, - path: '/assets/face/sweat.png', - }, - 天啊: { - name: '天啊', - category: EmojiCategory.FACE, - path: '/assets/face/oh-my.png', - }, - Emm: { - name: 'Emm', - category: EmojiCategory.FACE, - path: '/assets/face/Emm.png', - }, - 社会社会: { - name: '社会社会', - category: EmojiCategory.FACE, - path: '/assets/face/social.png', - }, - 旺柴: { - name: '旺柴', - category: EmojiCategory.FACE, - path: '/assets/face/doge.png', - }, - 好的: { - name: '好的', - category: EmojiCategory.FACE, - path: '/assets/face/good.png', - }, - 打脸: { - name: '打脸', - category: EmojiCategory.FACE, - path: '/assets/face/slap-face.png', - }, - 哇: { - name: '哇', - category: EmojiCategory.FACE, - path: '/assets/face/wow.png', - }, - 翻白眼: { - name: '翻白眼', - category: EmojiCategory.FACE, - path: '/assets/face/eye-roll.png', - }, - '666': { - name: '666', - category: EmojiCategory.FACE, - path: '/assets/face/666.png', - }, - 让我看看: { - name: '让我看看', - category: EmojiCategory.FACE, - path: '/assets/face/let-me-see.png', - }, - 叹气: { - name: '叹气', - category: EmojiCategory.FACE, - path: '/assets/face/sigh.png', - }, - 苦涩: { - name: '苦涩', - category: EmojiCategory.FACE, - path: '/assets/face/bitter.png', - }, - 裂开: { - name: '裂开', - category: EmojiCategory.FACE, - path: '/assets/face/crack.png', - }, - 奸笑: { - name: '奸笑', - category: EmojiCategory.FACE, - path: '/assets/face/sly-smile.png', - }, - - // 手势表情 - 握手: { - name: '握手', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/handshake.png', - }, - 胜利: { - name: '胜利', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/victory.png', - }, - 抱拳: { - name: '抱拳', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/fist-salute.png', - }, - 勾引: { - name: '勾引', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/beckon.png', - }, - 拳头: { - name: '拳头', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/fist.png', - }, - OK: { - name: 'OK', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/OK.png', - }, - 合十: { - name: '合十', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/pray.png', - }, - 强: { - name: '强', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/strong.png', - }, - 拥抱: { - name: '拥抱', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/hug.png', - }, - 弱: { - name: '弱', - category: EmojiCategory.GESTURE, - path: '/assets/gesture/weak.png', - }, - - // 动物表情 - 猪头: { - name: '猪头', - category: EmojiCategory.ANIMAL, - path: '/assets/animal/pig-head.png', - }, - 跳跳: { - name: '跳跳', - category: EmojiCategory.ANIMAL, - path: '/assets/animal/jump.png', - }, - 发抖: { - name: '发抖', - category: EmojiCategory.ANIMAL, - path: '/assets/animal/shiver.png', - }, - 转圈: { - name: '转圈', - category: EmojiCategory.ANIMAL, - path: '/assets/animal/spin.png', - }, - - // 祝福表情 - 庆祝: { - name: '庆祝', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/celebrate.png', - }, - 礼物: { - name: '礼物', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/gift.png', - }, - 红包: { - name: '红包', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/red-envelope.png', - }, - 發: { - name: '發', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/fortune.png', - }, - 福: { - name: '福', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/blessing.png', - }, - 烟花: { - name: '烟花', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/fireworks.png', - }, - 爆竹: { - name: '爆竹', - category: EmojiCategory.BLESSING, - path: '/assets/blessing/firecrackers.png', - }, - - // 其他表情 - 嘴唇: { - name: '嘴唇', - category: EmojiCategory.OTHER, - path: '/assets/other/lips.png', - }, - 爱心: { - name: '爱心', - category: EmojiCategory.OTHER, - path: '/assets/other/heart.png', - }, - 心碎: { - name: '心碎', - category: EmojiCategory.OTHER, - path: '/assets/other/broken-heart.png', - }, - 啤酒: { - name: '啤酒', - category: EmojiCategory.OTHER, - path: '/assets/other/beer.png', - }, - 咖啡: { - name: '咖啡', - category: EmojiCategory.OTHER, - path: '/assets/other/coffee.png', - }, - 蛋糕: { - name: '蛋糕', - category: EmojiCategory.OTHER, - path: '/assets/other/cake.png', - }, - 凋谢: { - name: '凋谢', - category: EmojiCategory.OTHER, - path: '/assets/other/wither.png', - }, - 菜刀: { - name: '菜刀', - category: EmojiCategory.OTHER, - path: '/assets/other/knife.png', - }, - 炸弹: { - name: '炸弹', - category: EmojiCategory.OTHER, - path: '/assets/other/bomb.png', - }, - 便便: { - name: '便便', - category: EmojiCategory.OTHER, - path: '/assets/other/poop.png', - }, - 太阳: { - name: '太阳', - category: EmojiCategory.OTHER, - path: '/assets/other/sun.png', - }, - 月亮: { - name: '月亮', - category: EmojiCategory.OTHER, - path: '/assets/other/moon.png', - }, - 玫瑰: { - name: '玫瑰', - category: EmojiCategory.OTHER, - path: '/assets/other/rose.png', - }, -} - -/** - * 获取所有表情数据的辅助函数 - */ -function getAllEmojiData(): EmojiInfo[] { - const result: EmojiInfo[] = [] - for (const key in EMOJI_DATA) { - if (Object.prototype.hasOwnProperty.call(EMOJI_DATA, key)) { - result.push(EMOJI_DATA[key as EmojiName]) - } - } - return result -} - -/** - * 按类别分组的表情数据 - */ -export const EMOJI_CATEGORIES = { - [EmojiCategory.FACE]: getAllEmojiData().filter( - (emoji) => emoji.category === EmojiCategory.FACE - ), - [EmojiCategory.GESTURE]: getAllEmojiData().filter( - (emoji) => emoji.category === EmojiCategory.GESTURE - ), - [EmojiCategory.ANIMAL]: getAllEmojiData().filter( - (emoji) => emoji.category === EmojiCategory.ANIMAL - ), - [EmojiCategory.BLESSING]: getAllEmojiData().filter( - (emoji) => emoji.category === EmojiCategory.BLESSING - ), - [EmojiCategory.OTHER]: getAllEmojiData().filter( - (emoji) => emoji.category === EmojiCategory.OTHER - ), -} as const - -/** - * 获取表情图片路径 - * @param name 表情名称 - * @returns 图片路径,如果表情不存在则返回 null - * - * @example - * ```typescript - * const path = getEmojiPath('微笑'); // 'assets/face/微笑.png' - * const invalidPath = getEmojiPath('不存在'); // null - * ``` - */ -export function getEmojiPath(name: EmojiName): string | null { - const emoji = EMOJI_DATA[name] - return emoji ? emoji.path : null -} - -/** - * 获取表情信息 - * @param name 表情名称 - * @returns 表情信息对象,如果表情不存在则返回 null - * - * @example - * ```typescript - * const emoji = getEmojiInfo('微笑'); - * // { name: '微笑', category: EmojiCategory.FACE, path: 'assets/face/微笑.png' } - * ``` - */ -export function getEmojiInfo(name: EmojiName): EmojiInfo | null { - return EMOJI_DATA[name] || null -} - -/** - * 根据类别获取表情列表 - * @param category 表情类别 - * @returns 该类别下的所有表情信息 - * - * @example - * ```typescript - * const faceEmojis = getEmojisByCategory(EmojiCategory.FACE); - * ``` - */ -export function getEmojisByCategory(category: EmojiCategory): EmojiInfo[] { - return EMOJI_CATEGORIES[category] -} - -/** - * 获取所有表情信息 - * @returns 所有表情的信息数组 - * - * @example - * ```typescript - * const allEmojis = getAllEmojis(); - * console.log(`总共有 ${allEmojis.length} 个表情`); - * ``` - */ -export function getAllEmojis(): EmojiInfo[] { - return getAllEmojiData() -} - -/** - * 搜索表情 - * @param keyword 搜索关键词 - * @returns 匹配的表情信息数组 - * - * @example - * ```typescript - * const results = searchEmojis('笑'); - * // 返回包含 '微笑', '偷笑', '坏笑' 等的表情 - * ``` - */ -export function searchEmojis(keyword: string): EmojiInfo[] { - return getAllEmojiData().filter((emoji) => emoji.name.indexOf(keyword) !== -1) -} - -/** - * 检查表情是否存在 - * @param name 表情名称 - * @returns 是否存在该表情 - * - * @example - * ```typescript - * const exists = hasEmoji('微笑'); // true - * const notExists = hasEmoji('不存在的表情'); // false - * ``` - */ -export function hasEmoji(name: EmojiName): boolean { - return name in EMOJI_DATA -} - -/** - * 获取表情名称列表 - * @param category 可选的类别筛选 - * @returns 表情名称数组 - * - * @example - * ```typescript - * const allNames = getEmojiNames(); - * const faceNames = getEmojiNames(EmojiCategory.FACE); - * ``` - */ -export function getEmojiNames(category?: EmojiCategory): string[] { - if (category) { - return getEmojisByCategory(category).map((emoji) => emoji.name) - } - const names: string[] = [] - for (const key in EMOJI_DATA) { - if (Object.prototype.hasOwnProperty.call(EMOJI_DATA, key)) { - names.push(key) - } - } - return names -} - -/** - * 随机获取表情 - * @param category 可选的类别筛选 - * @returns 随机表情信息 - * - * @example - * ```typescript - * const randomEmoji = getRandomEmoji(); - * const randomFaceEmoji = getRandomEmoji(EmojiCategory.FACE); - * ``` - */ -export function getRandomEmoji(category?: EmojiCategory): EmojiInfo { - const emojis = category ? getEmojisByCategory(category) : getAllEmojis() - const randomIndex = Math.floor(Math.random() * emojis.length) - return emojis[randomIndex] -} - -/** - * 根据路径查找表情名称 - * @param path 表情图片路径 - * @returns 表情名称,如果未找到则返回 null - * - * @example - * ```typescript - * const name = getEmojiNameByPath('/face/smile.png'); // '微笑' - * ``` - */ -export function getEmojiNameByPath(path: string): string | null { - if (!path) return null - - // 规范化路径(移除开头的 / 和 /assets/) - const normalizedPath = path.replace(/^\/assets?\//, '/') - - // 遍历所有表情数据查找匹配的路径 - for (const key in EMOJI_DATA) { - if (Object.prototype.hasOwnProperty.call(EMOJI_DATA, key)) { - const emoji = EMOJI_DATA[key as EmojiName] - if (emoji.path === normalizedPath || emoji.path === path) { - return emoji.name - } - } - } - - return null -} - -/** - * 默认导出对象,包含所有主要功能 - */ -const WeChatEmojis = { - // 枚举和类型 - EmojiCategory, - - // 数据 - EMOJI_CATEGORIES, - - // 工具函数 - getEmojiPath, - getEmojiInfo, - getEmojisByCategory, - getAllEmojis, - searchEmojis, - hasEmoji, - getEmojiNames, - getRandomEmoji, -} as const - -export default WeChatEmojis diff --git a/TouchVueThree/src/views/404/index.vue b/TouchVueThree/src/views/404/index.vue deleted file mode 100644 index 6855054..0000000 --- a/TouchVueThree/src/views/404/index.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - 404 - 页面不存在 - 返回首页 - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/AccountList/index.vue b/TouchVueThree/src/views/Chat/components/AccountList/index.vue deleted file mode 100644 index 0faa7a0..0000000 --- a/TouchVueThree/src/views/Chat/components/AccountList/index.vue +++ /dev/null @@ -1,168 +0,0 @@ - - - - 微信号 - - - - - - - 全部 - - - - - - - - - {{ account?.name?.charAt(0) || '?' }} - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ChatHeader/components/AITypeSelector.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ChatHeader/components/AITypeSelector.vue deleted file mode 100644 index 205a627..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ChatHeader/components/AITypeSelector.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - - {{ currentLabel }} - - - - - - - - - {{ option.label }} - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ChatHeader/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ChatHeader/index.vue deleted file mode 100644 index 463101d..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ChatHeader/index.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - - {{ - currentSession?.nickname?.charAt(0) || - currentSession?.conRemark?.charAt(0) || - '?' - }} - - - - - - {{ displayName }} - - - 群聊 - - - 在线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/FollowUpReminder/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/FollowUpReminder/index.vue deleted file mode 100644 index e51721f..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/FollowUpReminder/index.vue +++ /dev/null @@ -1,357 +0,0 @@ - - - - - 跟进提醒设置 - 设置客户跟进时间和方式 - - - - - - - - - - - - - - - - - - - - - - - - - 添加提醒 - - - - - - - - - - - - - - - {{ reminder.type }} - - - {{ reminder.status }} - - - - - - {{ reminder.content }} - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageInput/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageInput/index.vue deleted file mode 100644 index 6e120c2..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageInput/index.vue +++ /dev/null @@ -1,1167 +0,0 @@ - - - - - - - - - 😊 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 正在获取位置... - - - - - {{ currentLocation.name || '当前位置' }} - - - {{ currentLocation.address || '地址获取中...' }} - - - 经度: {{ currentLocation.lng?.toFixed(6) }}, 纬度: - {{ currentLocation.lat?.toFixed(6) }} - - - - - 无法获取位置信息,请检查浏览器权限设置 - - - - 取消 - - 发送位置 - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/MessageContent.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/MessageContent.vue deleted file mode 100644 index deac919..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/MessageContent.vue +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - - - - - 语音消息 - - - - - - 视频消息 - - - - - - - - {{ fileName }} - - - 下载 - - - - - - - - 位置消息 - - - - - {{ message.direction === 'send' ? '你' : '对方' }}撤回了一条消息 - - - - - {{ message.content }} - - - - - - 暂不支持的消息类型 - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/MessageItem.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/MessageItem.vue deleted file mode 100644 index d556803..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/MessageItem.vue +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - - - - - {{ senderName }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/TimeMessage.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/TimeMessage.vue deleted file mode 100644 index d00afa8..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/components/TimeMessage.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - {{ time }} - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/index.vue deleted file mode 100644 index 8d5be4f..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/MessageList/index.vue +++ /dev/null @@ -1,429 +0,0 @@ - - - - - - - - - - - - - - 加载更多 - - - - - - - - - - - - - - - - - - - - - - - - - - - 有新消息 - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/BasicInfo.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/BasicInfo.vue deleted file mode 100644 index 781e0b4..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/BasicInfo.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - {{ contact.nickname || '未知' }} - {{ contact.conRemark }} - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/CustomerInfo.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/CustomerInfo.vue deleted file mode 100644 index 4ce58f4..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/CustomerInfo.vue +++ /dev/null @@ -1,572 +0,0 @@ - - - - - - - - - 详细信息 - - - - - - - - - - - - - - - - - 标签 - - - - - - - - 群公告 - - - - - - - - 群成员 ({{ groupMembers.length }}) - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/DetailFields.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/DetailFields.vue deleted file mode 100644 index f6644f3..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/DetailFields.vue +++ /dev/null @@ -1,234 +0,0 @@ - - - - {{ field.label }} - - - - - - - - 保存 - - - 取消 - - - - - - - {{ values[field.key] || field.placeholder || '未设置' }} - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/GroupMembers.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/GroupMembers.vue deleted file mode 100644 index fc2e39c..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/GroupMembers.vue +++ /dev/null @@ -1,506 +0,0 @@ - - - - - - - - - - - - - - - - - {{ member.displayName || member.nickname || '未知' }} - - 群主 - - - 管理员 - - - - {{ member.wxid }} - - - - - - 移除 - - - - - - - - - - - - - 添加成员 - - - 删除成员 - - - - - 添加管理 - - - 删除管理 - - - 转让群主 - - - - 退出群聊 - - - - - - - - - - - - - - - - - - - - - - - - - - handleConfirmTransferOwner(ids[0])" - @cancel="transferOwnerDialogVisible = false" - /> - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/GroupNotice.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/GroupNotice.vue deleted file mode 100644 index 2db10f8..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/GroupNotice.vue +++ /dev/null @@ -1,281 +0,0 @@ - - - - - {{ notice || '点击添加群公告' }} - - - - - - - - - - - 取消 - - 🤖 AI编写 - - 确定 - - - - - - - - 📝 AI提示词 - - - 💡 提示:详细的提示词能帮助AI生成更符合您需求的群公告内容 - - - {{ aiGenerating ? '生成中...' : '生成内容' }} - - - - - 🤖 AI生成的群公告 - - - - - 取消 - - 使用此内容 - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/MomentsPreview.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/MomentsPreview.vue deleted file mode 100644 index 872f7db..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/MomentsPreview.vue +++ /dev/null @@ -1,448 +0,0 @@ - - - - - - - - 好友朋友圈 - - - - - - - - - - - - - - - - - - - - {{ moment.nickname || '未知' }} - {{ formatTime(moment.createTime) }} - - - - - {{ moment.content }} - - - - - - - - - - - - - - - - - - {{ moment.isLiked ? '已赞' : '点赞' }} - - - - 评论 - - - - - - - - {{ like.nickname }} - 、 - - - - - - {{ comment.nickname }}: - {{ comment.content }} - - - - - - - - 加载更多 - - - - - - - - - - 取消 - 确定 - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/QuickReply.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/QuickReply.vue deleted file mode 100644 index 2298c3a..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/QuickReply.vue +++ /dev/null @@ -1,1028 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - 添加新分组 - 新增快捷语 - - - - - - - - - - - - - - - - {{ data.label }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 取消 - 确定 - - - - - - - - - - - - 取消 - 确定 - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/TagManager.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/TagManager.vue deleted file mode 100644 index 6323372..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/TagManager.vue +++ /dev/null @@ -1,157 +0,0 @@ - - - - - {{ tag }} - - - - - - 新增标签 - - - - - - - - - - - - - - - - 暂无可用标签 - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/index.vue deleted file mode 100644 index cdf0769..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/index.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/components/TodoList/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/components/TodoList/index.vue deleted file mode 100644 index 1cc7516..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/components/TodoList/index.vue +++ /dev/null @@ -1,383 +0,0 @@ - - - - - 待办事项清单 - 管理日常工作任务 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 添加任务 - - - - - - - - - - - - - - - {{ todo.title }} - - - - - {{ todo.description }} - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/ChatWindow/index.vue b/TouchVueThree/src/views/Chat/components/ChatWindow/index.vue deleted file mode 100644 index f9cd43c..0000000 --- a/TouchVueThree/src/views/Chat/components/ChatWindow/index.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/EmptyState.vue b/TouchVueThree/src/views/Chat/components/EmptyState.vue deleted file mode 100644 index 7137729..0000000 --- a/TouchVueThree/src/views/Chat/components/EmptyState.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - 选择一个聊天开始对话 - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/ContactContextMenu.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/ContactContextMenu.vue deleted file mode 100644 index 945d412..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/ContactContextMenu.vue +++ /dev/null @@ -1,280 +0,0 @@ - - - - - - - - - - {{ isPinned ? '取消置顶' : '置顶' }} - - - - 修改备注 - - - - 移动分组 - - - - - - - - - - - - 取消 - - 确定 - - - - - - - - - - - - - - - 取消 - - 确定 - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/DeleteGroupDialog.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/DeleteGroupDialog.vue deleted file mode 100644 index deb10dd..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/DeleteGroupDialog.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - 确定要删除分组 "{{ group?.groupName }}" 吗? - 删除后,该分组下的联系人将移动到默认分组 - - - 取消 - 确定 - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/GroupContextMenu.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/GroupContextMenu.vue deleted file mode 100644 index 2bb0078..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/GroupContextMenu.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - 新增分组 - - - - 编辑分组 - - - - 删除分组 - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/GroupFormDialog.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/GroupFormDialog.vue deleted file mode 100644 index afed53e..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/components/GroupFormDialog.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 取消 - 确定 - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/index.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/index.vue deleted file mode 100644 index bcd3cc1..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/ContactList/index.vue +++ /dev/null @@ -1,602 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - {{ contact.nickname?.charAt(0) || '?' }} - - - - - {{ contact.remark || contact.nickname || '未知' }} - - - - 群聊 - - - {{ getAITypeLabel(contact.aiType) }} - - - - - - - - - - - - - - - - - - - - {{ group.groupName }} - - {{ group.count >= 0 ? group.count : '-' }} - - - - - - - - - - - - - - - - - - - - {{ contact.nickname?.charAt(0) || '?' }} - - - - - {{ contact.remark || contact.nickname || '未知' }} - - - - 群聊 - - - {{ getAITypeLabel(contact.aiType) }} - - - - - - - - - 加载更多 - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/SearchResultList/index.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/SearchResultList/index.vue deleted file mode 100644 index 262ff82..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/SearchResultList/index.vue +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - {{ contact.nickname?.charAt(0) || '?' }} - - - - - {{ contact.remark || contact.nickname || '未知' }} - - - - 群聊 - - - {{ getAITypeLabel(contact.aiType) }} - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/SessionContextMenu.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/SessionContextMenu.vue deleted file mode 100644 index 95f7fa4..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/SessionContextMenu.vue +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - {{ session?.config?.top ? '取消置顶' : '置顶' }} - - - - 修改备注 - - - - 删除 - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/index.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/index.vue deleted file mode 100644 index 06227c0..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/index.vue +++ /dev/null @@ -1,398 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 正在同步数据... - - - - - - - - {{ - session?.nickname?.charAt(0) || - session?.conRemark?.charAt(0) || - '?' - }} - - - - - - - {{ session?.conRemark || session?.nickname || '未知' }} - - - {{ - formatTime(session?.config?.msgTime || session?.wechatTime) - }} - - - - - {{ - formatMessagePreview( - session?.latestMessage?.content || session?.content - ) - }} - - - - - - - - - - - - 加载中... - - - - - 没有更多了 - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/components/SidebarMenu/index.vue b/TouchVueThree/src/views/Chat/components/SidebarMenu/index.vue deleted file mode 100644 index b54f46c..0000000 --- a/TouchVueThree/src/views/Chat/components/SidebarMenu/index.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - 聊天 - - - 联系人 - - - - - - - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Chat/index.vue b/TouchVueThree/src/views/Chat/index.vue deleted file mode 100644 index 5a00d07..0000000 --- a/TouchVueThree/src/views/Chat/index.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/TouchVueThree/src/views/Dashboard/index.vue b/TouchVueThree/src/views/Dashboard/index.vue deleted file mode 100644 index 0dd4b17..0000000 --- a/TouchVueThree/src/views/Dashboard/index.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - 数据看板 - 数据看板功能开发中... - - - - - - diff --git a/TouchVueThree/src/views/Login/index.vue b/TouchVueThree/src/views/Login/index.vue deleted file mode 100644 index 67eea8b..0000000 --- a/TouchVueThree/src/views/Login/index.vue +++ /dev/null @@ -1,659 +0,0 @@ - - - - - - - - - - - - - - - - - 触客宝 - - 登录您的账户继续使用 - - - - - - - - 密码登录 - - - 验证码登录 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ countdown > 0 ? `${countdown}s` : '获取验证码' }} - - - - - - - - - 我已阅读并同意 - 《触客宝用户协议》 - 和 - 《隐私政策》 - - - - - - - - {{ loading ? '登录中...' : '登录' }} - - - - - - - - - - - diff --git a/TouchVueThree/src/views/PowerCenter/AiTraining/index.vue b/TouchVueThree/src/views/PowerCenter/AiTraining/index.vue deleted file mode 100644 index 878309e..0000000 --- a/TouchVueThree/src/views/PowerCenter/AiTraining/index.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - AI 训练 - AI 训练功能开发中... - - - - - - diff --git a/TouchVueThree/src/views/PowerCenter/ContentManagement/index.vue b/TouchVueThree/src/views/PowerCenter/ContentManagement/index.vue deleted file mode 100644 index d20fe4b..0000000 --- a/TouchVueThree/src/views/PowerCenter/ContentManagement/index.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - 内容管理 - 内容管理功能开发中... - - - - - - diff --git a/TouchVueThree/src/views/PowerCenter/CustomerManagement/index.vue b/TouchVueThree/src/views/PowerCenter/CustomerManagement/index.vue deleted file mode 100644 index 7eb1065..0000000 --- a/TouchVueThree/src/views/PowerCenter/CustomerManagement/index.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - 客户管理 - 客户管理功能开发中... - - - - - - diff --git a/TouchVueThree/src/views/PowerCenter/DataStatistics/index.vue b/TouchVueThree/src/views/PowerCenter/DataStatistics/index.vue deleted file mode 100644 index 8be9daf..0000000 --- a/TouchVueThree/src/views/PowerCenter/DataStatistics/index.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - 数据统计 - 数据统计功能开发中... - - - - - - diff --git a/TouchVueThree/src/views/Settings/index.vue b/TouchVueThree/src/views/Settings/index.vue deleted file mode 100644 index 62ea0a9..0000000 --- a/TouchVueThree/src/views/Settings/index.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - 系统设置 - 系统设置功能开发中... - - - - - - diff --git a/TouchVueThree/tsconfig.json b/TouchVueThree/tsconfig.json deleted file mode 100644 index b47803f..0000000 --- a/TouchVueThree/tsconfig.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - - /* Path Alias */ - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "@api/*": ["./src/api/*"], - "@components/*": ["./src/components/*"], - "@composables/*": ["./src/composables/*"], - "@stores/*": ["./src/stores/*"], - "@utils/*": ["./src/utils/*"], - "@types/*": ["./src/types/*"], - "@views/*": ["./src/views/*"], - "@assets/*": ["./src/assets/*"], - "@layouts/*": ["./src/layouts/*"], - "@directives/*": ["./src/directives/*"] - }, - - /* Types */ - "types": [ - "vite/client", - "element-plus/global", - "node" - ] - }, - "include": [ - "src/**/*.ts", - "src/**/*.d.ts", - "src/**/*.tsx", - "src/**/*.vue" - ], - "exclude": [ - "node_modules", - "dist" - ], - "references": [ - { - "path": "./tsconfig.node.json" - } - ] -} diff --git a/TouchVueThree/tsconfig.node.json b/TouchVueThree/tsconfig.node.json deleted file mode 100644 index 97ede7e..0000000 --- a/TouchVueThree/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] -} diff --git a/TouchVueThree/vite.config.ts b/TouchVueThree/vite.config.ts deleted file mode 100644 index 76a1bfc..0000000 --- a/TouchVueThree/vite.config.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import path from 'path' -import { fileURLToPath } from 'url' - -// Element Plus 自动导入 -import AutoImport from 'unplugin-auto-import/vite' -import Components from 'unplugin-vue-components/vite' -import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' - -// 性能优化插件 -import compression from 'vite-plugin-compression' -import { visualizer } from 'rollup-plugin-visualizer' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -// https://vitejs.dev/config/ -export default defineConfig(({ mode }) => { - return { - plugins: [ - vue(), - - // 自动导入 Vue 3 API、Vue Router、Pinia、VueUse、TanStack Query - AutoImport({ - imports: [ - 'vue', - 'vue-router', - 'pinia', - '@vueuse/core', - { - '@tanstack/vue-query': [ - 'useQuery', - 'useMutation', - 'useQueryClient', - 'useInfiniteQuery', - ], - }, - ], - resolvers: [ElementPlusResolver()], - dts: 'src/auto-imports.d.ts', - eslintrc: { - enabled: true, - filepath: './.eslintrc-auto-import.json', - globalsPropValue: true, - }, - }), - - // 自动导入 Element Plus 组件 - Components({ - resolvers: [ - ElementPlusResolver({ - importStyle: 'sass', // 使用 SCSS 样式 - }), - ], - dts: 'src/components.d.ts', - }), - - // Gzip 压缩 - compression({ - algorithm: 'gzip', - ext: '.gz', - threshold: 10240, // 大于 10KB 的文件才压缩 - }), - - // 打包分析(只在 analyze 模式下启用) - mode === 'analyze' && - visualizer({ - open: true, - gzipSize: true, - brotliSize: true, - filename: 'dist/stats.html', - }), - ].filter(Boolean), - - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@api': path.resolve(__dirname, './src/api'), - '@components': path.resolve(__dirname, './src/components'), - '@composables': path.resolve(__dirname, './src/composables'), - '@stores': path.resolve(__dirname, './src/stores'), - '@utils': path.resolve(__dirname, './src/utils'), - '@types': path.resolve(__dirname, './src/types'), - '@views': path.resolve(__dirname, './src/views'), - '@assets': path.resolve(__dirname, './src/assets'), - }, - }, - - css: { - preprocessorOptions: { - scss: { - additionalData: `@use "@/assets/styles/variables.scss" as *;`, - api: 'modern-compiler', - }, - }, - }, - - server: { - open: true, - port: 8080, - host: '0.0.0.0', - proxy: { - '/api': { - target: process.env.VITE_API_BASE_URL || 'http://localhost:3000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), - }, - }, - }, - - build: { - outDir: 'dist', - assetsDir: 'assets', - sourcemap: false, - chunkSizeWarningLimit: 2000, - - rollupOptions: { - output: { - manualChunks: { - // 核心框架 - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - - // UI 组件库 - 'element-plus': ['element-plus', '@element-plus/icons-vue'], - - // 工具库 - utils: [ - 'axios', - 'dayjs', - '@vueuse/core', - 'lodash-es', - 'mitt', - 'nanoid', - ], - - // 数据管理 - query: ['@tanstack/vue-query'], - - // 图表库 - echarts: ['echarts', 'vue-echarts'], - }, - - chunkFileNames: 'assets/js/[name]-[hash].js', - entryFileNames: 'assets/js/[name]-[hash].js', - assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', - }, - }, - - minify: 'esbuild', - target: 'es2015', - }, - - // 依赖预构建优化 - optimizeDeps: { - include: [ - 'vue', - 'vue-router', - 'pinia', - 'pinia-plugin-persistedstate', - 'element-plus', - '@element-plus/icons-vue', - '@vueuse/core', - 'axios', - 'dayjs', - 'echarts', - 'lodash-es', - 'mitt', - 'nanoid', - ], - }, - } -}) diff --git a/TouchVueThree/个人资料卡片功能分析.md b/TouchVueThree/个人资料卡片功能分析.md deleted file mode 100644 index ea8b2aa..0000000 --- a/TouchVueThree/个人资料卡片功能分析.md +++ /dev/null @@ -1,461 +0,0 @@ -# 个人资料卡片功能分析 - -## 📋 概述 - -个人资料卡片(ProfileCard)是聊天窗口右侧的侧边栏组件,用于显示和编辑联系人/群聊的详细信息。支持好友和群聊两种模式,包含多个功能标签页。 - -## 🏗️ 组件结构 - -### 主组件层级 - -``` -ProfileCard (index.tsx) -├── 标签页切换 (Tab切换) -│ ├── 快捷语录 (QuickWords) -│ ├── 个人资料/群资料 (ProfileModules) -│ └── 朋友圈 (FriendsCircle) - 仅好友显示 -└── LayoutFiexd (固定布局组件) -``` - -### 核心组件 - -1. **ProfileCard/index.tsx** - 主容器组件 - - 管理标签页切换(快捷语录、个人资料、朋友圈) - - 懒加载渲染,只渲染当前激活的标签页 - - 宽度:330px - -2. **ProfileModules/index.tsx** - 资料详情组件 - - 显示和编辑联系人/群聊的详细信息 - - 支持好友和群聊两种模式 - - 包含多个功能模块 - -## ✨ 核心功能 - -### 1. 基本信息展示 - -#### 好友模式 -- **头像**:显示好友头像(80px) -- **昵称**:显示微信昵称 -- **状态**:在线状态(固定显示"在线") -- **微信号**:显示微信ID(只读) -- **地区**:显示地区信息(只读) - -#### 群聊模式 -- **头像**:显示群头像 -- **群名称**:显示群名称(可编辑,需群主权限) -- **群ID**:显示群ID(只读) -- **群主**:显示群主信息(只读) -- **群昵称**:显示我在群中的昵称(可编辑) - -### 2. 详细信息编辑 - -#### 好友扩展字段(可编辑) -- **微信备注** (`conRemark`):可编辑,同步到后端和会话列表 -- **电话** (`phone`):存储在 `extendFields` JSON字段中 -- **QQ** (`qq`):存储在 `extendFields` JSON字段中 -- **邮箱** (`email`):存储在 `extendFields` JSON字段中 -- **公司** (`company`):存储在 `extendFields` JSON字段中 -- **职位** (`position`):存储在 `extendFields` JSON字段中 -- **地址** (`address`):存储在 `extendFields` JSON字段中 -- **描述** (`remark`):存储在 `extendFields` JSON字段中 - -#### 数据同步机制 -1. **WebSocket命令**: - - 好友:`CmdModifyFriendRemark` - 修改备注和扩展字段 - - 群聊:`CmdModifyGroupRemark` - 修改群备注和扩展字段 - -2. **HTTP API**: - - `updateFriendInfo` - 更新好友信息到后端 - - 包含所有扩展字段的更新 - -3. **本地数据库同步**: - - 更新会话列表数据库(`MessageManager.updateRemark`) - - 更新联系人数据库(`ContactManager.updateContact`) - - 更新Store中的状态 - -### 3. 标签管理 - -#### 功能特性 -- **标签列表**:显示所有可用标签(客服标签 + 联系人标签) -- **标签切换**:点击标签可选中/取消选中 -- **新增标签**:支持动态添加新标签 -- **标签同步**: - - 好友:`CmdModifyFriendLabel` WebSocket命令 - - 群聊:`CmdModifyGroupLabel` WebSocket命令 - -#### 标签数据来源 -- 客服标签:从 `useCustomerStore` 获取当前客服的 `labels` -- 联系人标签:从 `contract.labels` 获取 -- 合并去重后显示 - -### 4. 群聊管理功能 - -#### 群公告 -- **查看**:显示群公告内容 -- **编辑**:群主可编辑群公告 -- **AI编写**:支持AI生成群公告 - - 使用 `generateAiText` API - - 可自定义提示词 - - 生成后可编辑再应用 -- **WebSocket命令**:`CmdChatroomOperate` (chatroomOperateType: 5) - -#### 群成员管理 -- **添加成员**: - - 从联系人列表选择(支持分页加载) - - WebSocket命令:`CmdChatroomInvite` - -- **删除成员**: - - 从群成员列表选择 - - WebSocket命令:`CmdChatroomOperate` (chatroomOperateType: 2) - -- **添加管理员**: - - 从群成员列表选择 - - WebSocket命令:`CmdChatroomOperate` (chatroomOperateType: 12) - -- **删除管理员**: - - 从管理员列表选择 - - WebSocket命令:`CmdChatroomOperate` (chatroomOperateType: 8) - -- **转让群主**: - - 从群成员列表选择(只能选一个) - - WebSocket命令:`CmdChatroomOperate` (chatroomOperateType: 10) - -#### 群成员列表 -- 显示所有群成员(从 `useWeChatStore.currentGroupMembers` 获取) -- 支持hover显示"添加好友"按钮 -- 点击可发送好友请求(`CmdChatroomOperate` chatroomOperateType: 1) - -#### 退出群聊 -- 确认对话框 -- WebSocket命令:`CmdChatroomOperate` (chatroomOperateType: 4) - -### 5. 好友详情获取 - -#### API调用 -- **接口**:`getFriendInfo({ id: contract.id })` -- **路径**:`/v1/kefu/wechatFriend/detail` -- **时机**:组件挂载后静默请求(不显示loading) -- **数据使用**:优先使用API返回的详情数据,补充扩展字段 - -#### 返回数据结构 -```typescript -{ - detail: { - id: number; - wechatAccountId: number; - alias: string; - wechatId: string; - conRemark: string; - nickname: string; - avatar: string; - region: string; - labels: any[]; - signature: string; - extendFields: string; // JSON字符串 - phone: string; - company: string; - position: string; - createTime: string; - aiType: number; - // ... 更多字段 - } -} -``` - -### 6. 快捷语录功能 - -#### 功能特性 -- **分类管理**:个人、公共、部门三种类型 -- **分组管理**:支持创建分组,树形结构展示 -- **快捷回复**:支持文本、图片、视频、链接等类型 -- **快速发送**:点击快捷回复可直接发送消息 -- **CRUD操作**:支持增删改查快捷回复和分组 - -#### 数据操作 -- `addReply` - 添加快捷回复 -- `updateReply` - 更新快捷回复 -- `deleteReply` - 删除快捷回复 -- `addGroup` - 添加分组 -- `updateGroup` - 更新分组 -- `deleteGroup` - 删除分组 - -### 7. 朋友圈功能 - -#### 功能特性 -- 显示好友的朋友圈动态 -- 需要传入 `wechatFriendId` 参数 -- 具体实现需要查看 `FriendsCircle` 组件 - -## 🔧 技术实现细节 - -### 1. 状态管理 - -#### 使用的Store -- `useCustomerStore` - 获取客服信息和标签 -- `useMessageStore` - 更新会话列表 -- `useWeChatStore` - 获取群成员列表、当前联系人 -- `useWebSocketStore` - 发送WebSocket命令 -- `useUserStore` - 获取当前用户ID - -#### 本地状态 -- `remarkValue` - 备注值 -- `selectedTags` - 选中的标签 -- `allAvailableTags` - 所有可用标签 -- `friendDetail` - 好友详情(API返回) -- `extendFields` - 扩展字段对象 -- `groupNameValue` - 群名称 -- `groupNoticeValue` - 群公告 -- `selfDisplayNameValue` - 群昵称 - -### 2. 数据同步流程 - -#### 备注修改流程 -``` -用户编辑备注 - ↓ -更新本地状态 (乐观更新) - ↓ -发送WebSocket命令 (CmdModifyFriendRemark/CmdModifyGroupRemark) - ↓ -调用HTTP API (updateFriendInfo) - 仅好友 - ↓ -更新会话列表数据库 (MessageManager.updateRemark) - ↓ -更新联系人数据库 (ContactManager.updateContact) - ↓ -更新Store状态 (setCurrentContact) -``` - -### 3. 组件优化 - -#### 懒加载 -- 使用 `Suspense` 和 `lazy` 实现组件懒加载 -- 标签页使用 `renderedKeys` 控制,只渲染激活的标签页 - -#### 防抖优化 -- 订阅回调使用100ms防抖,避免频繁更新UI - -#### 深度比较 -- `DetailValue` 组件使用深度比较,避免不必要的状态重置 - -### 4. 权限控制 - -#### 群管理权限 -- `hasGroupManagePermission()` - 检查是否有群管理权限 -- 目前所有用户都有完整权限(返回true) -- 影响的功能: - - 修改群名称 - - 修改群公告 - - 删除成员 - - 添加/删除管理员 - - 转让群主 - -## 📡 WebSocket命令列表 - -| 命令 | 用途 | 参数 | -|------|------|------| -| `CmdModifyFriendRemark` | 修改好友备注 | `wechatAccountId`, `wechatFriendId`, `newRemark`, `extendFields` | -| `CmdModifyGroupRemark` | 修改群备注 | `wechatAccountId`, `chatroomId`, `newRemark`, `extendFields` | -| `CmdModifyFriendLabel` | 修改好友标签 | `wechatAccountId`, `wechatFriendId`, `labels` | -| `CmdModifyGroupLabel` | 修改群标签 | `wechatAccountId`, `chatroomId`, `labels` | -| `CmdChatroomOperate` | 群操作 | `wechatAccountId`, `wechatChatroomId`, `chatroomOperateType`, `extra` | -| `CmdChatroomInvite` | 邀请成员 | `wechatChatroomId`, `wechatFriendIds` | - -### chatroomOperateType 枚举 -- `1` - 发送好友请求 -- `2` - 删除成员 -- `4` - 退出群聊 -- `5` - 修改群公告 -- `6` - 修改群名称 -- `8` - 删除管理员 / 修改群昵称 -- `10` - 转让群主 -- `12` - 添加管理员 - -## 🔌 API接口 - -### 1. 获取好友详情 -```typescript -GET /v1/kefu/wechatFriend/detail -参数: { id: number } -返回: FriendDetailResponse -``` - -### 2. 更新好友信息 -```typescript -POST /v1/kefu/wechatFriend/updateInfo -参数: UpdateFriendInfoParams { - id: number; - conRemark: string; - phone: string; - company: string; - name: string; - position: string; - email: string; - address: string; - qq: string; - remark: string; -} -``` - -### 3. AI生成群公告 -```typescript -POST /v1/kefu/ai/generateText -参数: { - prompt: string; - wechatAccountId: number; - groupId: number; -} -返回: string (生成的文本) -``` - -## 📦 数据结构 - -### ContractData (好友) -```typescript -{ - id: number; - wechatAccountId: number; - nickname: string; - conRemark: string; - avatar: string; - wechatId: string; - labels: string[]; - extendFields: string; // JSON字符串 - aiType: number; - // ... -} -``` - -### weChatGroup (群聊) -```typescript -{ - id: number; - wechatAccountId: number; - chatroomId: string; - name: string; - notice: string; - selfDisplyName: string; - chatroomOwner: string; - chatroomAvatar: string; - labels: string[]; - extendFields: string; // JSON字符串 - // ... -} -``` - -## 🎨 UI组件 - -### DetailValue 组件 -- **功能**:可编辑的字段展示组件 -- **特性**: - - 点击字段进入编辑模式 - - 支持文本和文本域两种类型 - - 自动检测字段变化 - - 支持保存和取消 - - 深度比较避免不必要的重置 - -### 标签组件 -- 使用 Ant Design 的 `Tag` 组件 -- 选中状态:蓝色背景 `#e6f7ff`,蓝色边框 -- 未选中状态:灰色背景 `#fafafa`,灰色边框 -- 支持点击切换选中状态 - -## 🔄 数据流 - -### 初始化流程 -``` -组件挂载 - ↓ -判断是好友还是群聊 - ↓ -如果是好友,静默请求 getFriendInfo - ↓ -解析 extendFields JSON - ↓ -合并数据源(API数据优先) - ↓ -渲染UI -``` - -### 编辑保存流程 -``` -用户点击编辑 - ↓ -进入编辑模式 - ↓ -用户修改值 - ↓ -检测变化 (changedKeys) - ↓ -用户点击保存 - ↓ -调用 saveHandler - ↓ -发送WebSocket命令 - ↓ -调用HTTP API (仅好友) - ↓ -更新本地数据库 - ↓ -更新Store状态 - ↓ -调用 onSaveSuccess - ↓ -退出编辑模式 -``` - -## 💡 优化建议 - -### 1. 性能优化 -- ✅ 已实现懒加载 -- ✅ 已实现防抖 -- ✅ 已实现深度比较 -- ⚠️ 可考虑虚拟滚动(群成员列表) - -### 2. 用户体验 -- ✅ 乐观更新(立即更新UI) -- ✅ 静默获取详情(不阻塞UI) -- ⚠️ 可添加保存成功/失败的明确提示 -- ⚠️ 可添加字段验证 - -### 3. 错误处理 -- ⚠️ API请求失败时的降级方案 -- ⚠️ WebSocket命令失败时的重试机制 -- ⚠️ 数据同步冲突的处理 - -### 4. 代码优化 -- ⚠️ 可提取公共的WebSocket命令发送逻辑 -- ⚠️ 可统一扩展字段的处理逻辑 -- ⚠️ 可优化状态管理,减少重复代码 - -## 📝 迁移注意事项 - -### 1. 组件拆分 -- ProfileCard 主容器 -- ProfileModules 资料详情 -- QuickWords 快捷语录 -- FriendsCircle 朋友圈 - -### 2. Store迁移 -- 需要迁移相关的Store逻辑 -- 需要迁移WebSocket命令发送逻辑 -- 需要迁移本地数据库操作 - -### 3. API迁移 -- 需要确认新项目的API接口是否一致 -- 需要确认数据结构是否兼容 - -### 4. 样式迁移 -- 需要迁移SCSS样式文件 -- 需要适配新的UI框架(Element Plus) - -## 🎯 核心功能总结 - -1. ✅ **基本信息展示** - 头像、昵称、状态等 -2. ✅ **详细信息编辑** - 备注、扩展字段等 -3. ✅ **标签管理** - 标签的增删改查 -4. ✅ **群聊管理** - 群公告、成员管理、权限控制 -5. ✅ **好友详情获取** - API静默获取 -6. ✅ **快捷语录** - 快捷回复管理 -7. ✅ **朋友圈** - 好友动态展示 -8. ✅ **数据同步** - WebSocket + HTTP API + 本地数据库 diff --git a/TouchVueThree/会话列表同步清理策略.md b/TouchVueThree/会话列表同步清理策略.md deleted file mode 100644 index acb187d..0000000 --- a/TouchVueThree/会话列表同步清理策略.md +++ /dev/null @@ -1,597 +0,0 @@ -# 会话列表同步清理策略 - -## 📋 需求分析 - -### 核心需求 - -1. ✅ **会话列表不能自动清除** - 用户的重要数据 -2. ✅ **缓存优先显示** - 首次打开先显示本地缓存 -3. ✅ **后台全量同步** - 以服务器数据为准 -4. ✅ **识别已删除会话** - 追踪在其他端(手机)删除的会话 -5. ✅ **安全清理机制** - 确保不误删 - -### 同步场景 - -| 场景 | 本地状态 | 服务器状态 | 操作 | -|------|---------|-----------|------| -| **新增会话** | ❌ 不存在 | ✅ 存在 | ➕ 添加到本地 | -| **更新会话** | ✅ 存在 | ✅ 存在 | 🔄 更新本地数据 | -| **删除会话** | ✅ 存在 | ❌ 不存在 | ❌ 从本地删除 | -| **孤儿会话** | ✅ 存在 | ❌ 长期不存在 | 🗑️ 标记+清理 | - -## 🔧 技术方案 - -### 1. 数据库结构扩展 - -**文件**: `TouchVueThree/src/utils/db.ts` - -```typescript -export interface ChatSession { - // ... 现有字段 ... - - // ⭐ 新增同步相关字段 - lastSyncTime?: string // 最后同步时间(ISO8601) - serverExists?: boolean // 服务器是否存在该会话 - syncVersion?: number // 同步版本号 - deletedAt?: string // 删除时间(软删除) - isLocalOnly?: boolean // 是否仅本地(临时会话) -} -``` - -### 2. 同步策略流程 - -```typescript -/** - * 会话列表同步流程(完整版) - */ -const syncSessions = async (accountId: number) => { - try { - syncing.value = true - - // ===================== 阶段 1: 显示本地缓存 ===================== - // 1.1 立即从本地数据库读取并显示 - const cachedSessions = await SessionManager.getUserSessions(accountId) - sessions.value = cachedSessions - initialLoading.value = false // 立即显示,不阻塞 UI - - console.log(`✅ 显示本地缓存: ${cachedSessions.length} 个会话`) - - // ===================== 阶段 2: 后台全量同步 ===================== - // 2.1 标记同步开始(用于识别哪些会话在服务器上存在) - await SessionManager.markSyncStart(accountId) - - // 2.2 分页获取服务器所有会话(以服务器为准) - let page = 1 - const limit = 500 - const serverSessionIds = new Set() // 记录服务器上的会话 ID - - while (true) { - const result = await getSessionList({ page, limit, accountId }) - if (!result || result.length === 0) break - - // 2.3 格式化并保存每一页数据 - const formattedSessions = result.map((item) => ({ - id: item.id, - serverId: `${item.type}_${item.id}`, - type: item.type, - wechatAccountId: item.wechatAccountId, - // ... 其他字段 ... - - // ⭐ 同步标记 - lastSyncTime: new Date().toISOString(), - serverExists: true, // 标记服务器存在 - syncVersion: Date.now(), - })) - - // 记录服务器上的会话 ID - formattedSessions.forEach((s) => serverSessionIds.add(s.id)) - - // 批量保存/更新到本地 - await SessionManager.upsertBatch(formattedSessions) - - console.log(`📥 同步第 ${page} 页: ${formattedSessions.length} 个会话`) - - if (result.length < limit) break - page++ - } - - // ===================== 阶段 3: 识别并清理已删除会话 ===================== - // 3.1 找出本地有但服务器没有的会话(可能在其他端被删除) - const localSessions = await SessionManager.getUserSessions(accountId) - const deletedSessions = localSessions.filter( - (session) => !serverSessionIds.has(session.id) - ) - - console.log( - `🔍 发现 ${deletedSessions.length} 个会话在服务器上不存在,准备清理` - ) - - // 3.2 清理策略:安全删除(软删除 + 延迟硬删除) - for (const session of deletedSessions) { - await SessionManager.markAsDeleted(session.id, accountId) - } - - // 3.3 刷新列表(排除已删除的会话) - const updatedSessions = await SessionManager.getUserSessions(accountId, { - excludeDeleted: true, - }) - sessions.value = updatedSessions - - console.log(`✅ 同步完成,当前会话数: ${updatedSessions.length}`) - - syncing.value = false - } catch (error) { - console.error('同步会话失败:', error) - syncing.value = false - throw error - } -} -``` - -### 3. SessionManager 扩展方法 - -**文件**: `TouchVueThree/src/utils/dbManagers/SessionManager.ts` - -```typescript -export class SessionManager { - // ==================== 同步标记管理 ==================== - - /** - * 标记同步开始(重置所有会话的 serverExists 标记) - * 用于识别哪些会话在本次同步中出现 - */ - static async markSyncStart(accountId?: number) { - try { - let query = db().sessions.toCollection() - - if (accountId && accountId !== 0) { - query = db().sessions.where('wechatAccountId').equals(accountId) - } - - // 重置 serverExists 标记为 false - await query.modify((session) => { - session.serverExists = false - }) - - console.log(`🔄 开始同步,重置 serverExists 标记`) - } catch (error) { - console.error('标记同步开始失败:', error) - throw error - } - } - - /** - * 批量插入或更新会话 - * ⭐ 标记 serverExists = true(表示服务器存在) - */ - static async upsertBatch(sessions: ChatSession[]) { - try { - await db().transaction('rw', db().sessions, async () => { - for (const session of sessions) { - // 自动生成 sortKey - const sessionWithMeta = { - ...session, - sortKey: session.sortKey || this.generateSortKey(session), - lastSyncTime: new Date().toISOString(), - serverExists: true, // ⭐ 标记服务器存在 - syncVersion: Date.now(), - } - - await db().sessions.put(sessionWithMeta) - } - }) - - await this.triggerCallbacks() - } catch (error) { - console.error('批量更新会话失败:', error) - throw error - } - } - - /** - * 标记会话为已删除(软删除) - * 不立即从数据库删除,而是标记 deletedAt - * - * @param sessionId 会话ID - * @param accountId 账号ID - */ - static async markAsDeleted(sessionId: number, accountId?: number) { - try { - const session = await db().sessions.get(sessionId) - - if (session) { - await db().sessions.update(sessionId, { - serverExists: false, - deletedAt: new Date().toISOString(), - }) - - console.log(`🗑️ 标记会话为已删除: ${session.nickname || sessionId}`) - } - - await this.triggerCallbacks(accountId) - } catch (error) { - console.error('标记会话为已删除失败:', error) - throw error - } - } - - /** - * 获取用户的所有会话(已排序) - * ⭐ 支持排除已删除的会话 - * - * @param accountId 账号ID - * @param options 选项 - * @returns 会话列表 - */ - static async getUserSessions( - accountId?: number, - options?: { - excludeDeleted?: boolean // 是否排除已删除的会话 - includeLocalOnly?: boolean // 是否包含仅本地的会话 - } - ): Promise { - try { - let query = db().sessions.toCollection() - - // 按账号过滤 - if (accountId && accountId !== 0) { - query = db().sessions.where('wechatAccountId').equals(accountId) - } - - // 获取所有会话 - let sessions = await query.toArray() - - // 排除已删除的会话 - if (options?.excludeDeleted !== false) { - sessions = sessions.filter((s) => !s.deletedAt) - } - - // 排除仅本地的会话(如果需要) - if (!options?.includeLocalOnly) { - sessions = sessions.filter((s) => !s.isLocalOnly) - } - - // 按 sortKey 降序排序 - return sessions.sort((a, b) => { - if (a.sortKey && b.sortKey) { - return b.sortKey.localeCompare(a.sortKey) - } - return 0 - }) - } catch (error) { - console.error('获取会话列表失败:', error) - throw error - } - } - - /** - * 硬删除已标记的会话(定期清理) - * 删除标记超过 7 天的会话 - */ - static async cleanupDeletedSessions() { - try { - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() - - const deletedCount = await db() - .sessions.where('deletedAt') - .below(sevenDaysAgo) - .delete() - - console.log(`🗑️ 清理了 ${deletedCount} 个已删除的会话`) - - return deletedCount - } catch (error) { - console.error('清理已删除会话失败:', error) - throw error - } - } - - /** - * 恢复误删的会话 - * 如果用户发现某个会话被误删,可以恢复 - * - * @param sessionId 会话ID - */ - static async restoreDeletedSession(sessionId: number) { - try { - await db().sessions.update(sessionId, { - deletedAt: undefined, - serverExists: true, - }) - - console.log(`✅ 恢复会话: ${sessionId}`) - - await this.triggerCallbacks() - } catch (error) { - console.error('恢复会话失败:', error) - throw error - } - } -} -``` - -### 4. Store 层面集成 - -**文件**: `TouchVueThree/src/stores/modules/wechat/useSessionStore.ts` - -```typescript -/** - * 初始化会话 Store(优化版) - * - * 执行流程: - * 1. 立即显示本地缓存(不阻塞 UI) - * 2. 后台全量同步服务器数据 - * 3. 识别并清理已删除的会话 - */ -const init = async (accountId: number = 0) => { - try { - console.log(`📦 初始化会话 Store (账号: ${accountId || '全部'})`) - - currentAccountId.value = accountId - - // ⭐ 步骤 0:确保数据库已初始化 - const userStore = useUserStore() - const userId = userStore.userId - - if (!userId) { - console.warn('用户未登录,跳过会话初始化') - return - } - - await databaseManager.ensureDatabase(userId) - - // ⭐ 步骤 1:立即从本地缓存读取并显示(不阻塞 UI) - const cachedSessions = await SessionManager.getUserSessions(accountId, { - excludeDeleted: true, // 排除已删除的会话 - }) - - sessions.value = cachedSessions - initialLoading.value = false // 立即显示 - - console.log(`✅ 显示本地缓存: ${cachedSessions.length} 个会话`) - - // 自动选择第一个会话 - autoSelectFirstSession() - - // ⭐ 步骤 2:订阅数据库变更(自动更新 UI) - if (!unsubscribe) { - unsubscribe = SessionManager.onUpdate((updatedSessions, updatedAccountId) => { - if (updatedAccountId === accountId || !updatedAccountId) { - // 排除已删除的会话 - sessions.value = updatedSessions.filter((s) => !s.deletedAt) - updateCallbackCount.value++ - lastUpdateTime.value = Date.now() - } - }) - } - - // ⭐ 步骤 3:后台同步服务器数据(不阻塞 UI) - syncSessionsWithCleanup(accountId) - } catch (error) { - console.error('初始化会话失败:', error) - initialLoading.value = false - } -} - -/** - * 后台同步会话列表(带清理机制) - * - * @param accountId 账号ID - */ -const syncSessionsWithCleanup = async (accountId: number) => { - try { - syncing.value = true - - // 1. 标记同步开始 - await SessionManager.markSyncStart(accountId) - - // 2. 分页获取服务器数据 - let page = 1 - const limit = 500 - const serverSessionIds = new Set() - - while (true) { - const result = await getSessionList({ page, limit, accountId }) - if (!result || result.length === 0) break - - // 格式化数据 - const formattedSessions = result.map((item) => { - const sessionType = item.chatroomId ? 'group' : 'friend' - const session: ChatSession = { - id: item.id, - serverId: `${sessionType}_${item.id}`, - type: sessionType, - wechatAccountId: item.wechatAccountId, - nickname: item.nickname, - conRemark: item.conRemark, - avatar: item.avatar, - wxid: item.wxid, - chatroomId: item.chatroomId, - content: item.content || item.latestMessage?.content || '', - lastUpdateTime: item.lastUpdateTime || new Date().toISOString(), - config: { - unreadCount: item.config?.unreadCount || 0, - top: Boolean(item.config?.top), - msgTime: item.config?.msgTime || Date.now(), - chat: item.config?.chat !== false, - mute: item.config?.mute || false, - }, - sortKey: '', // 临时占位 - - // ⭐ 同步标记 - lastSyncTime: new Date().toISOString(), - serverExists: true, - syncVersion: Date.now(), - } - - // 生成 sortKey - session.sortKey = SessionManager.generateSortKey(session) - - // 记录服务器上的会话 ID - serverSessionIds.add(session.id) - - return session - }) - - // 批量保存到数据库 - await SessionManager.upsertBatch(formattedSessions) - - console.log(`📥 同步第 ${page} 页: ${formattedSessions.length} 个会话`) - - if (result.length < limit) break - page++ - } - - // 3. 识别并清理已删除的会话 - const localSessions = await SessionManager.getUserSessions(accountId, { - excludeDeleted: false, // 包含所有会话 - }) - - const deletedSessions = localSessions.filter( - (session) => !serverSessionIds.has(session.id) && !session.deletedAt - ) - - if (deletedSessions.length > 0) { - console.log( - `🔍 发现 ${deletedSessions.length} 个会话在服务器上不存在,标记为已删除` - ) - - for (const session of deletedSessions) { - await SessionManager.markAsDeleted(session.id, accountId) - } - } - - // 4. 刷新列表 - const updatedSessions = await SessionManager.getUserSessions(accountId, { - excludeDeleted: true, - }) - sessions.value = updatedSessions - - console.log(`✅ 同步完成,当前会话数: ${updatedSessions.length}`) - - syncing.value = false - } catch (error) { - console.error('同步会话失败:', error) - syncing.value = false - } -} - -/** - * 定期清理已删除的会话(7 天后硬删除) - */ -const scheduleCleanup = () => { - // 每天清理一次 - setInterval(async () => { - try { - await SessionManager.cleanupDeletedSessions() - } catch (error) { - console.error('定期清理失败:', error) - } - }, 24 * 60 * 60 * 1000) // 24 小时 - - // 启动时立即清理一次 - setTimeout(async () => { - try { - await SessionManager.cleanupDeletedSessions() - } catch (error) { - console.error('启动清理失败:', error) - } - }, 60 * 1000) // 1 分钟后 -} - -// 在 Store 初始化时启动定期清理 -onMounted(() => { - scheduleCleanup() -}) -``` - -## 🔍 工作流程示意 - -``` -用户打开应用 - ↓ -┌─────────────────────────────────────────┐ -│ 阶段 1: 立即显示本地缓存(< 500ms) │ -├─────────────────────────────────────────┤ -│ 1. 从 IndexedDB 读取会话列表 │ -│ 2. 排除已删除的会话(deletedAt 存在) │ -│ 3. 立即显示在 UI 上 │ -│ 4. 用户可以立即操作 │ -└─────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────┐ -│ 阶段 2: 后台全量同步(不阻塞 UI) │ -├─────────────────────────────────────────┤ -│ 1. 标记所有会话 serverExists = false │ -│ 2. 分页获取服务器所有会话 │ -│ 3. 每页数据: │ -│ - 标记 serverExists = true │ -│ - 更新到本地数据库 │ -│ - 记录 serverSessionIds │ -│ 4. 继续直到获取所有数据 │ -└─────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────┐ -│ 阶段 3: 识别并清理已删除会话 │ -├─────────────────────────────────────────┤ -│ 1. 对比本地和服务器会话 ID │ -│ 2. 找出本地有但服务器没有的会话 │ -│ 3. 软删除: 标记 deletedAt │ -│ 4. 刷新 UI,排除已删除的会话 │ -└─────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────┐ -│ 阶段 4: 定期硬删除(7 天后) │ -├─────────────────────────────────────────┤ -│ 1. 每天检查一次 │ -│ 2. 删除 deletedAt 超过 7 天的会话 │ -│ 3. 释放存储空间 │ -└─────────────────────────────────────────┘ -``` - -## ⚠️ 安全机制 - -### 1. 软删除(7 天缓冲期) - -- ✅ 不立即硬删除,标记 `deletedAt` -- ✅ 7 天内可以恢复(`restoreDeletedSession`) -- ✅ 防止误删重要会话 - -### 2. 同步版本号 - -- ✅ 每次同步分配新的 `syncVersion` -- ✅ 用于追踪数据更新 -- ✅ 便于排查同步问题 - -### 3. 服务器存在标记 - -- ✅ `serverExists` 字段标记服务器状态 -- ✅ 每次同步重置并重新标记 -- ✅ 准确识别已删除会话 - -## 📊 性能影响 - -| 操作 | 耗时 | 影响 | -|------|------|------| -| **显示本地缓存** | < 500ms | ✅ 不阻塞 UI | -| **后台同步 1000 会话** | ~5s | ✅ 后台进行 | -| **识别已删除会话** | < 100ms | ✅ 几乎无感知 | -| **定期硬删除** | < 1s | ✅ 后台执行 | - -## 📝 总结 - -### 核心优势 - -1. ✅ **秒开体验**: 立即显示本地缓存 -2. ✅ **安全清理**: 软删除 + 7 天缓冲期 -3. ✅ **准确同步**: 以服务器数据为准 -4. ✅ **跨端同步**: 自动识别在手机端删除的会话 -5. ✅ **性能优化**: 不阻塞 UI,后台同步 - -### 用户体验 - -- ✅ 启动即可使用(< 500ms) -- ✅ 同步过程无感知(后台进行) -- ✅ 数据始终最新(自动同步) -- ✅ 误删可恢复(7 天内) -- ✅ 跨端一致(自动清理) diff --git a/TouchVueThree/会话列表排序优化实施总结.md b/TouchVueThree/会话列表排序优化实施总结.md deleted file mode 100644 index 066e106..0000000 --- a/TouchVueThree/会话列表排序优化实施总结.md +++ /dev/null @@ -1,181 +0,0 @@ -# 会话列表排序优化实施总结 - -## ✅ 已完成的改造 - -### 1. 数据库结构优化 - -**文件**: `TouchVueThree/src/utils/db.ts` - -- ✅ 添加 `sortKey` 索引:`'id, serverId, wechatAccountId, type, lastUpdateTime, sortKey, [wechatAccountId+sortKey]'` -- ✅ 添加复合索引 `[wechatAccountId+sortKey]`,用于按账号过滤并排序 - -### 2. sortKey 生成函数 - -**文件**: `TouchVueThree/src/utils/dbManagers/SessionManager.ts` - -- ✅ 实现 `generateSortKey` 方法 -- ✅ 格式与旧项目一致:`${isTop}|${timestamp}|${displayName}` -- ✅ 包含置顶、时间、显示名称三个维度 - -### 3. 数据库层面排序 - -**文件**: `TouchVueThree/src/utils/dbManagers/SessionManager.ts` - -- ✅ 修改 `getUserSessions`,使用数据库层面排序 -- ✅ 使用 `orderBy('sortKey').reverse()` 实现降序排序 -- ✅ 利用数据库索引,提升性能 - -### 4. 统一更新机制 - -**文件**: `TouchVueThree/src/utils/dbManagers/SessionManager.ts` - -- ✅ `upsertSession`: 自动生成 sortKey -- ✅ `updateOnNewMessage`: 自动更新 sortKey -- ✅ `togglePin`: 自动更新 sortKey -- ✅ `syncSessions`: 批量生成 sortKey -- ✅ `createSessionFromMessage`: 自动生成 sortKey - -### 5. Store 层面优化 - -**文件**: `TouchVueThree/src/stores/modules/wechat/useSessionStore.ts` - -- ✅ 修改 `sortedSessions`,使用 `sortKey` 字符串比较 -- ✅ 更新所有创建/更新会话的地方,使用 `SessionManager.generateSortKey` -- ✅ 保持 Vue 3 响应式特性 - -## 📊 核心改进 - -### 排序机制 - -**之前**: -```typescript -// 内存排序,分步判断 -return sessions.sort((a, b) => { - if (a.config.top && !b.config.top) return -1 - if (!a.config.top && b.config.top) return 1 - return (b.config.msgTime || 0) - (a.config.msgTime || 0) -}) -``` - -**现在**: -```typescript -// 数据库层面排序 -const sessions = await query - .orderBy('sortKey') - .reverse() - .toArray() - -// Store 层面使用 sortKey 字符串比较(与数据库一致) -return [...sessions.value].sort((a, b) => { - if (a.sortKey && b.sortKey) { - return b.sortKey.localeCompare(a.sortKey) - } - // 备用排序逻辑 -}) -``` - -### sortKey 格式 - -**格式**: `${isTop}|${timestamp}|${displayName}` - -**示例**: -- 置顶会话: `"1|1705123456789|张三"` -- 普通会话: `"0|1705123456789|李四"` - -**优势**: -- ✅ 一次比较完成所有排序 -- ✅ 包含稳定排序(显示名称) -- ✅ 易于扩展新维度 - -## 🎯 性能提升 - -| 场景 | 之前 | 现在 | 提升 | -|------|------|------|------| -| **1000 个会话首次加载** | ~100ms | ~50ms | 2x | -| **排序更新** | ~50ms | ~10ms | 5x | -| **大数据量(5000+)** | 可能卡顿 | 流畅 | 显著 | - -## ⚠️ 注意事项 - -### 1. 数据库版本升级 - -如果现有数据库已有数据,需要: -- 升级数据库版本(如果需要) -- 为现有数据生成 `sortKey` - -**建议**: 在首次加载时,检查并更新现有数据的 `sortKey`: - -```typescript -// 在 getUserSessions 中添加 -const sessions = await query.orderBy('sortKey').reverse().toArray() - -// 检查并更新缺失的 sortKey -for (const session of sessions) { - if (!session.sortKey) { - session.sortKey = SessionManager.generateSortKey(session) - await db().sessions.update(session.id, { sortKey: session.sortKey }) - } -} -``` - -### 2. 保持响应式 - -- ✅ 数据库更新后,通过订阅机制通知 Store -- ✅ Store 自动更新,触发 Vue 响应式更新 -- ✅ 无需手动触发更新 - -### 3. 兼容性 - -- ✅ `sortKey` 格式与旧项目一致 -- ✅ 如果 `sortKey` 不存在,使用备用排序逻辑 -- ✅ 向后兼容 - -## 🔄 后续优化建议 - -### 1. 数据库版本升级脚本 - -如果需要为现有数据生成 `sortKey`,可以添加升级脚本: - -```typescript -this.version(2).stores({ - sessions: 'id, serverId, wechatAccountId, type, lastUpdateTime, sortKey, [wechatAccountId+sortKey]', -}).upgrade(async (tx) => { - // 为现有数据生成 sortKey - await tx.table('sessions').toCollection().modify((session) => { - if (!session.sortKey) { - session.sortKey = SessionManager.generateSortKey(session) - } - }) -}) -``` - -### 2. 性能监控 - -可以添加性能监控,跟踪排序性能: - -```typescript -const startTime = performance.now() -const sessions = await SessionManager.getUserSessions(accountId) -const endTime = performance.now() -console.log(`排序耗时: ${endTime - startTime}ms`) -``` - -### 3. 缓存策略 - -- ✅ 首次加载:从数据库读取(已排序) -- ✅ 后台同步:增量更新,自动更新 `sortKey` -- ✅ 实时更新:WebSocket 推送时,自动更新 `sortKey` - -## 📝 总结 - -本次优化成功实现了: - -1. ✅ **sortKey 机制**: 统一排序逻辑,包含置顶、时间、名称三个维度 -2. ✅ **数据库层面排序**: 利用 Dexie 索引,提升性能 -3. ✅ **稳定排序**: 确保相同条件的会话顺序稳定 -4. ✅ **响应式保持**: 利用 Vue 3 + Pinia 的响应式特性 -5. ✅ **易于维护**: 统一更新机制,代码清晰 - -**性能提升**: 2-5 倍 -**代码质量**: 更统一、更易维护 -**用户体验**: 排序更稳定、加载更快 diff --git a/TouchVueThree/会话列表预览消息规则.md b/TouchVueThree/会话列表预览消息规则.md deleted file mode 100644 index 7d35de9..0000000 --- a/TouchVueThree/会话列表预览消息规则.md +++ /dev/null @@ -1,789 +0,0 @@ -# 会话列表预览消息规则 - -> **文档说明**:详细记录会话列表中消息预览的格式化规则和处理逻辑(框架无关,适用于 React/Vue 项目改造) - -## 📋 目录 - -- [核心概述](#核心概述) -- [数据来源](#数据来源) -- [处理流程](#处理流程) -- [规则详解](#规则详解) -- [与旧项目对比](#与旧项目对比) -- [代码实现](#代码实现) -- [测试用例](#测试用例) - ---- - -## 核心概述 - -### 基本信息 - -| 项目 | 说明 | -| ------------ | ----------------------------------------------------------------- | -| **工具函数** | `formatMessagePreview()` / `messageFilter()` | -| **文件位置** | `utils/messagePreview.ts`(新项目)或 `utils/filter.ts`(旧项目) | -| **使用场景** | 会话列表消息预览、通知预览、消息摘要 | -| **数据来源** | `session.latestMessage.content` 或 `session.content` | -| **返回类型** | `string`(永远不会返回空值) | -| **框架支持** | ✅ React、Vue、Angular、原生 JS 等 | - -### 设计原则 - -1. **兜底处理**:所有异常情况都有友好提示,不会显示原始错误 -2. **优先级明确**:按照消息类型的匹配优先级依次判断 -3. **长度限制**:文本消息最多显示50个字符,超出部分显示省略号 -4. **特殊符号**:富媒体消息使用中括号包裹,如 `[图片]`、`[视频]` -5. **兼容性强**:处理 JSON 不完整、XML 截断等边界情况 - ---- - -## 数据来源 - -### 会话列表数据字段 - -在会话列表中,预览消息的数据来源字段: - -| 字段 | 说明 | 优先级 | -| ------------------------------- | ------------ | -------- | -| `session.latestMessage.content` | 最新消息内容 | 优先使用 | -| `session.content` | 会话缓存内容 | 兜底字段 | - -### 调用示例 - -**新项目(Vue)**: - -```typescript -const previewText = formatMessagePreview( - session?.latestMessage?.content || session?.content -) -``` - -**旧项目(React)**: - -```typescript -const previewText = messageFilter(session.content) -``` - -### 函数签名 - -```typescript -/** - * 格式化消息预览内容 - * @param content 原始消息内容 - * @returns 格式化后的预览文本 - */ -function formatMessagePreview(content: string | null | undefined): string -``` - -### 使用说明 - -- ✅ **框架无关**:可用于 React、Vue、Angular 等任何框架 -- ✅ **输入类型**:`string | null | undefined` -- ✅ **输出类型**:`string`(永远不会返回空值) -- ✅ **使用场景**:会话列表预览、通知预览、消息摘要等 - ---- - -## 处理流程 - -### 流程图 - -``` -输入 content - ↓ -① 空值检查 → null/undefined/空字符串 → "暂无消息" - ↓ -② 阿里云OSS链接检查 → 匹配到 → 根据扩展名返回 [图片]/[视频]/[音频] - ↓ -③ JSON解析尝试 - ├─ 成功 - │ ├─ 小程序消息 → "[小程序消息]" - │ ├─ JSON中包含OSS链接 → 根据扩展名返回 - │ ├─ contentXml提取title → 显示title(最多50字符) - │ ├─ JSON过长(>500字符) → "[文本过长]" - │ ├─ JSON.title字段 → 显示title(最多50字符) - │ ├─ JSON.content字段 → 显示content(最多50字符) - │ └─ 无法识别 → "[消息]" - └─ 失败(非JSON) - ↓ -④ 普通HTTP链接检查 - ├─ 图片扩展名 → "[图片]" - ├─ 视频扩展名 → "[视频]" - ├─ 音频扩展名 → "[音频]" - └─ 其他链接 → "[链接]" - ↓ -⑤ XML字符串检查 → 提取title或返回"[文本过长]" - ↓ -⑥ 普通文本 → 显示文本(最多50字符) -``` - ---- - -## 规则详解 - -### 1️⃣ 空值处理 - -```typescript -if (!content || typeof content !== 'string') { - return '暂无消息' -} - -const trimmed = content.trim() -if (!trimmed) { - return '暂无消息' -} -``` - -**处理情况**: - -- `null`、`undefined` -- 非字符串类型 -- 空字符串或纯空白字符 - -**返回结果**:`"暂无消息"` - ---- - -### 2️⃣ 阿里云 OSS 链接识别 - -#### OSS 前缀 - -```typescript -const ALIYUN_OSS_PREFIX = 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com' -``` - -#### 文件类型判断 - -| 类型 | 扩展名 | 返回值 | -| -------- | --------------------------------------------------------------- | -------- | -| **图片** | `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`, `.svg` | `[图片]` | -| **视频** | `.mp4`, `.avi`, `.mov`, `.wmv`, `.flv`, `.mkv`, `.webm`, `.m4v` | `[视频]` | -| **音频** | `.mp3`, `.wav`, `.wma`, `.flac`, `.aac`, `.ogg`, `.m4a` | `[音频]` | - -#### 示例 - -**输入**: - -``` -https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/weremote/chat-logs/xxx/9160773596410687940.jpg -``` - -**输出**:`[图片]` - ---- - -### 3️⃣ JSON 格式消息 - -#### 3.1 小程序消息 - -**识别特征**(满足任一条件): - -1. `contentXml` 包含 `...", - "type": "miniprogram" -} -``` - ---- - -#### 3.2 JSON 中包含 OSS 链接 - -递归遍历 JSON 所有字段,查找包含 OSS 前缀的链接: - -```typescript -const findAliyunOssLink = (obj: any): string | null => { - if (typeof obj === 'string' && obj.includes(ALIYUN_OSS_PREFIX)) { - return obj - } - if (typeof obj === 'object' && obj !== null) { - for (const value of Object.values(obj)) { - const link = findAliyunOssLink(value) - if (link) return link - } - } - return null -} -``` - -**示例**: - -```json -{ - "previewImage": "https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/.../image.png", - "type": "image" -} -``` - -**返回**:`[图片]` - ---- - -#### 3.3 从 contentXml 提取 title - -**匹配规则**: - -```typescript -const titleMatch = xmlString.match( - /([^<]*(?:[^<]*)*)<\/title>/i -) -``` - -**处理步骤**: - -1. 匹配 `...` 标签 -2. 处理 CDATA:`` → `文本` -3. 去除首尾空白 -4. 限制长度为 50 字符 - -**示例输入**: - -```xml - -``` - -**返回**:`超值预售!抢26年经济师《蓝宝典4.0》` - ---- - -#### 3.4 JSON 过长处理 - -**触发条件**:JSON 字符串长度 > 500 字符 - -**处理逻辑**: - -1. 尝试从 XML 中提取 title -2. 提取成功 → 显示 title(最多50字符) -3. 提取失败 → 返回 `[文本过长]` - ---- - -#### 3.5 提取 JSON 字段 - -**字段优先级**: - -| 优先级 | 字段名 | 处理 | -| ------ | --------- | ------------------------------- | -| 1 | `title` | 显示 title 内容(最多50字符) | -| 2 | `content` | 显示 content 内容(最多50字符) | -| 3 | 无匹配 | 返回 `[消息]` | - ---- - -### 4️⃣ 普通 HTTP 链接 - -**匹配规则**:`/^https?:\/\//i` - -| 链接类型 | 扩展名匹配 | 返回值 | -| -------- | --------------- | -------- | -| 图片链接 | IMAGE_EXT_REGEX | `[图片]` | -| 视频链接 | VIDEO_EXT_REGEX | `[视频]` | -| 音频链接 | AUDIO_EXT_REGEX | `[音频]` | -| 其他链接 | - | `[链接]` | - -**示例**: - -``` -https://example.com/video.mp4 → [视频] -https://example.com/page.html → [链接] -``` - ---- - -### 5️⃣ XML 字符串 - -**识别特征**(满足任一条件): - -- 包含 `` -- 包含 `` 标签内容 -2. 成功 → 显示 title(最多50字符) -3. 失败 → 返回 `[文本过长]` - ---- - -### 6️⃣ 普通文本消息 - -**处理规则**: - -- 最大长度:50 字符 -- 超出部分:截断并添加 `...` -- 不做任何格式转换 - -**示例**: - -```typescript -输入: '先生上的飞机啊立刻搭街坊拉萨,这是一条很长的消息内容,超过了五十个字符的限制' -输出: '先生上的飞机啊立刻搭街坊拉萨,这是一条很长的消息内容,超过了五...' -``` - ---- - -## 与旧项目对比 - -### 旧项目实现(messageFilter) - -旧项目使用 `messageFilter()` 函数(位于 `old/src/utils/filter.ts`): - -```typescript -export const messageFilter = (message: string) => { - if (!message) return '' - - try { - const parsed = JSON.parse(message) - - switch (true) { - case !!(parsed.previewImage || parsed.tencentUrl): - return '[图片]' - case !!(parsed.videoUrl || parsed.video): - return '[视频]' - case !!( - parsed.voiceUrl || - parsed.voice || - (parsed.url && parsed.durationMs) - ): - return parsed.text ? `[语音] ${parsed.text}` : '[语音]' - // ... 其他判断 - } - } catch { - return message.length > 30 ? message.substring(0, 30) + '...' : message - } -} -``` - -### 核心差异 - -| 对比项 | 旧项目 | 新项目 | 优势对比 | -| ---------------- | --------------------------------------------- | ------------------------------------------ | ------------------------ | -| **JSON字段判断** | 硬编码字段名(如 `previewImage`, `videoUrl`) | 动态查找 OSS 链接 + 字段提取 | 新项目更灵活,兼容性更好 | -| **小程序识别** | 无专门处理 | 多维度识别(`appid`、`type`、`weappinfo`) | 新项目识别更准确 | -| **XML处理** | 无专门处理 | 提取 `` 标签显示有意义内容 | 新项目用户体验更好 | -| **长度限制** | 30 字符 | 50 字符 | 新项目显示更多信息 | -| **截断处理** | JSON 被截断时显示原始 JSON | 尝试提取 title 或标记 `[文本过长]` | 新项目更优雅 | -| **OSS 链接** | 无专门处理 | 递归查找 JSON 中的 OSS 链接 | 新项目支持嵌套结构 | - -### 新项目优势 - -✅ **更强大的 XML 解析**:能从复杂的 `contentXml` 中提取 title -✅ **递归查找 OSS 链接**:支持深层嵌套的 JSON 结构 -✅ **小程序消息识别**:多维度判断,更准确 -✅ **优雅的边界处理**:JSON 不完整、XML 截断都有友好提示 -✅ **更长的文本预览**:50字符 vs 30字符 - ---- - -## 代码实现 - -### 核心函数(完整实现) - -```typescript -/** - * 消息预览格式化工具 - * 用于会话列表中显示消息预览,参考 content数据实例.md - */ - -// 图片扩展名正则 -const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i - -// 视频扩展名正则 -const VIDEO_EXT_REGEX = /\.(mp4|avi|mov|wmv|flv|mkv|webm|m4v)$/i - -// 音频扩展名正则 -const AUDIO_EXT_REGEX = /\.(mp3|wav|wma|flac|aac|ogg|m4a)$/i - -// 阿里云 OSS 前缀 -const ALIYUN_OSS_PREFIX = 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com' - -/** - * 尝试解析 JSON - */ -const tryParseJson = (content: string): Record | null => { - try { - return JSON.parse(content) - } catch { - return null - } -} - -/** - * 从 XML 字符串中提取 title - */ -const extractTitleFromXml = (xmlString: string): string | null => { - try { - // 尝试提取 标签内容 - const titleMatch = xmlString.match( - /([^<]*(?:[^<]*)*)<\/title>/i - ) - if (titleMatch && titleMatch[1]) { - let title = titleMatch[1] - // 处理 CDATA - title = title.replace(//gi, '$1') - // 去除首尾空白 - title = title.trim() - if (title) { - return title - } - } - } catch { - // 解析失败,返回 null - } - return null -} - -/** - * 检查是否为阿里云 OSS 链接,并判断类型 - */ -const checkAliyunOssLink = (url: string): '图片' | '视频' | '音频' | null => { - if (!url.includes(ALIYUN_OSS_PREFIX)) { - return null - } - - // 根据文件扩展名判断类型 - if (IMAGE_EXT_REGEX.test(url)) { - return '图片' - } - if (VIDEO_EXT_REGEX.test(url)) { - return '视频' - } - if (AUDIO_EXT_REGEX.test(url)) { - return '音频' - } - - return null -} - -/** - * 检查是否为小程序消息 - */ -const isMiniProgramMessage = (jsonData: Record): boolean => { - // 检查是否有 contentXml 且包含 appid - if (jsonData.contentXml && typeof jsonData.contentXml === 'string') { - const xmlContent = jsonData.contentXml - // 检查是否包含 - if (xmlContent.includes(' { - if (typeof obj === 'string' && obj.includes(ALIYUN_OSS_PREFIX)) { - return obj - } - if (typeof obj === 'object' && obj !== null) { - for (const value of Object.values(obj)) { - const link = findAliyunOssLink(value) - if (link) { - return link - } - } - } - return null - } - - const ossLink = findAliyunOssLink(jsonData) - if (ossLink) { - const ossType = checkAliyunOssLink(ossLink) - if (ossType) { - return `[${ossType}]` - } - } - - // 2.3 尝试从 contentXml 中提取 title - if (jsonData.contentXml && typeof jsonData.contentXml === 'string') { - const title = extractTitleFromXml(jsonData.contentXml) - if (title) { - // 限制长度 - const maxLength = 50 - return title.length > maxLength - ? title.substring(0, maxLength) + '...' - : title - } - } - - // 2.4 检查 JSON 是否过长或被截断 - // 如果 JSON 字符串很长(超过 500 字符),可能被截断 - if (trimmed.length > 500) { - // 尝试提取 title - const title = extractTitleFromXml(trimmed) - if (title) { - const maxLength = 50 - return title.length > maxLength - ? title.substring(0, maxLength) + '...' - : title - } - return '[文本过长]' - } - - // 2.5 尝试从 JSON 中提取有意义的信息 - if (jsonData.title) { - const title = String(jsonData.title) - const maxLength = 50 - return title.length > maxLength - ? title.substring(0, maxLength) + '...' - : title - } - - if (jsonData.content) { - const content = String(jsonData.content) - const maxLength = 50 - return content.length > maxLength - ? content.substring(0, maxLength) + '...' - : content - } - - // 2.6 无法识别的 JSON,返回通用提示 - return '[消息]' - } - - // 3. 检查是否为普通 HTTP 链接 - if (/^https?:\/\//i.test(trimmed)) { - // 检查是否为图片链接 - if (IMAGE_EXT_REGEX.test(trimmed)) { - return '[图片]' - } - // 检查是否为视频链接 - if (VIDEO_EXT_REGEX.test(trimmed)) { - return '[视频]' - } - // 检查是否为音频链接 - if (AUDIO_EXT_REGEX.test(trimmed)) { - return '[音频]' - } - // 普通链接 - return '[链接]' - } - - // 4. 检查是否为 XML 字符串(但没有被 JSON 包裹) - if ( - trimmed.includes('') || - trimmed.includes(' maxLength - ? title.substring(0, maxLength) + '...' - : title - } - return '[文本过长]' - } - - // 5. 普通文本消息 - // 限制长度,避免过长文本影响显示 - const maxLength = 50 - if (trimmed.length > maxLength) { - return trimmed.substring(0, maxLength) + '...' - } - - return trimmed -} -``` - ---- - -## 测试用例 - -### 1. 空值测试 - -| 输入 | 输出 | -| ----------- | ---------- | -| `null` | `暂无消息` | -| `undefined` | `暂无消息` | -| `""` | `暂无消息` | -| `" "` | `暂无消息` | - ---- - -### 2. 阿里云 OSS 链接 - -| 输入 | 输出 | -| ------------------------------------------------------------------ | -------- | -| `https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/.../file.jpg` | `[图片]` | -| `https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/.../file.mp4` | `[视频]` | -| `https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/.../file.mp3` | `[音频]` | - ---- - -### 3. 小程序消息 - -**输入**: - -```json -{ - "contentXml": "...", - "type": "miniprogram" -} -``` - -**输出**:`[小程序消息]` - ---- - -### 4. JSON 嵌套 OSS 链接 - -**输入**: - -```json -{ - "data": { - "media": { - "url": "https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/.../image.png" - } - } -} -``` - -**输出**:`[图片]` - ---- - -### 5. XML 提取 title - -**输入**: - -```json -{ - "contentXml": "" -} -``` - -**输出**:`1kg/瓶【美味可口】海天上等蚝油` - ---- - -### 6. JSON 过长 - -**输入**:长度 > 500 字符的 JSON,且无 title - -**输出**:`[文本过长]` - ---- - -### 7. 普通 HTTP 链接 - -| 输入 | 输出 | -| ------------------------------- | -------- | -| `https://example.com/image.jpg` | `[图片]` | -| `https://example.com/video.mp4` | `[视频]` | -| `https://example.com/page.html` | `[链接]` | - ---- - -### 8. 纯文本 - -| 输入 | 输出 | -| ----------------------------------------------------------------- | --------------------------------------------------------------- | -| `"你好"` | `你好` | -| `"这是一条很长的消息,超过了五十个字符的限制,需要被截断处理..."` | `这是一条很长的消息,超过了五十个字符的限制,需要被截断处理...` | - ---- - -## 📌 注意事项 - -### 1. 性能优化 - -- ✅ **正则表达式**:所有正则都定义在模块顶层,避免重复编译 -- ✅ **递归查找**:`findAliyunOssLink` 找到第一个匹配后立即返回 -- ✅ **提前返回**:每个判断成功后立即返回,减少不必要的计算 - -### 2. 数据兼容性 - -- ✅ **JSON 不完整**:解析失败时走 XML 或文本处理流程 -- ✅ **XML 截断**:无法提取 title 时返回 `[文本过长]` -- ✅ **嵌套结构**:递归查找支持任意深度的 JSON 嵌套 - -### 3. 用户体验 - -- ✅ **友好提示**:所有异常情况都有清晰的中文提示 -- ✅ **信息优先**:优先显示有意义的 title/content,而非 `[消息]` -- ✅ **长度控制**:50字符刚好能显示完整语义,又不会过长 - -### 4. 扩展性 - -如需添加新的消息类型识别: - -1. 在 `formatMessagePreview` 函数中添加新的判断分支 -2. 遵循现有的优先级顺序(从特殊到一般) -3. 确保有兜底的返回值 - ---- - -## 📝 变更记录 - -| 日期 | 版本 | 变更内容 | -| ---------- | ---- | -------------------------------- | -| 2026-01-16 | v1.0 | 创建文档,记录新项目消息预览规则 | - ---- - -## 🔗 相关文档 - -- [content数据实例.md](./content数据实例.md) - 消息内容格式说明 -- [开发日志.md](./开发日志.md) - 项目开发记录 -- [会话列表排序优化实施总结.md](./会话列表排序优化实施总结.md) - 会话列表优化说明 - ---- - -**📌 提示**:本文档基于 `src/utils/messagePreview.ts` 实现编写,与实际代码保持同步。 diff --git a/TouchVueThree/客服在线状态功能说明.md b/TouchVueThree/客服在线状态功能说明.md deleted file mode 100644 index 9552873..0000000 --- a/TouchVueThree/客服在线状态功能说明.md +++ /dev/null @@ -1,364 +0,0 @@ -# 客服在线状态功能说明 - -## 📋 功能概述 - -客服在线状态功能实现了实时监控微信账号的在线/离线状态,并在界面上进行可视化展示。 - -## 🎯 核心特性 - -### 1. **实时状态查询** -- 每 5 秒通过 WebSocket 查询一次所有账号的在线状态 -- 使用 `CmdRequestWechatAccountsAliveStatus` 命令 -- 服务器返回 `CmdRequestWechatAccountsAliveStatusResp` 响应 - -### 2. **自动排序** -- 在线账号自动排在前面 -- 离线账号显示为半透明(opacity: 0.5) - -### 3. **可视化展示** -- 在线状态:绿色圆点指示器(右下角) -- 离线状态:无指示器 + 半透明头像 - -## 🏗️ 架构设计 - -### 架构流程 - -``` -WebSocket 连接成功 - ↓ -startAliveStatusQuery() - ↓ -每 5 秒发送 CmdRequestWechatAccountsAliveStatus - ↓ -服务器返回 CmdRequestWechatAccountsAliveStatusResp - ↓ -accountHandlers.handleAccountAliveStatus() - ↓ -emitAccountStatus (发布订阅模式) - ↓ -useAccountStore 更新状态 - ↓ -UI 自动刷新 -``` - -### 核心组件 - -#### 1. **WebSocket 层** (`useWebSocket.ts`) - -```typescript -// 定时器管理 -let aliveStatusTimer: NodeJS.Timeout | null = null -const ALIVE_STATUS_REQUEST_INTERVAL = 5000 // 5 秒 - -// 请求在线状态 -const requestAliveStatus = () => { - const accountIds = accountStore.accountList.map(acc => acc.id) - sendCommand('CmdRequestWechatAccountsAliveStatus', { - wechatAccountIds: accountIds, - }) -} - -// 启动定时查询 -const startAliveStatusQuery = () => { - requestAliveStatus() // 立即查询一次 - aliveStatusTimer = setInterval(requestAliveStatus, 5000) -} -``` - -#### 2. **消息处理层** (`accountHandlers.ts`) - -```typescript -function handleAccountAliveStatus(wsMessage: WebSocketMessage): void { - const aliveStatus = wsMessage.data.wechatAccountsAliveStatus - - // 发布每个账号的状态(发布订阅模式) - Object.entries(aliveStatus).forEach(([accountId, isOnline]) => { - emitAccountStatus(Number(accountId), isOnline) - }) - - // 批量更新 Store - accountStore.updateAccountsOnlineStatus(aliveStatus) -} -``` - -#### 3. **状态管理层** (`useAccountStore.ts`) - -```typescript -// 订阅在线状态事件 -onAccountStatus(({ accountId, isOnline }) => { - updateOnlineStatus(accountId, isOnline) -}) - -// 批量更新在线状态 -const updateAccountsOnlineStatus = (aliveStatus: Record) => { - Object.entries(aliveStatus).forEach(([accountId, isOnline]) => { - updateOnlineStatus(Number(accountId), isOnline) - }) - - // 按在线状态排序 - accountList.value.sort((a, b) => { - if (a.isOnline && !b.isOnline) return -1 - if (!a.isOnline && b.isOnline) return 1 - return 0 - }) -} -``` - -#### 4. **UI 层** (`AccountList/index.vue`) - -```vue - - - - - - {{ account?.name?.charAt(0) }} - - - - - - - - - -``` - -## 🔄 数据流 - -### 1. **WebSocket → Handler** - -```javascript -// WebSocket 收到消息 -{ - cmdType: "CmdRequestWechatAccountsAliveStatusResp", - data: { - wechatAccountsAliveStatus: { - "300745": true, // 在线 - "300746": false, // 离线 - "300747": true // 在线 - } - } -} -``` - -### 2. **Handler → 发布订阅** - -```javascript -// 遍历每个账号,发布状态变化 -emitAccountStatus(300745, true) -emitAccountStatus(300746, false) -emitAccountStatus(300747, true) -``` - -### 3. **Store 订阅 → 更新状态** - -```javascript -// Store 订阅到事件,更新本地状态 -onAccountStatus(({ accountId, isOnline }) => { - updateOnlineStatus(accountId, isOnline) -}) -``` - -### 4. **响应式更新 → UI 自动刷新** - -```javascript -// accountList 是 ref,Vue 自动检测变化并刷新 UI -accountList.value.sort((a, b) => { - if (a.isOnline && !b.isOnline) return -1 - if (!a.isOnline && b.isOnline) return 1 - return 0 -}) -``` - -## 🎨 UI 效果 - -### 在线账号 -``` -┌─────────────┐ -│ [头像] │ ← 正常显示 -│ ● │ ← 绿色圆点(右下角) -└─────────────┘ -``` - -### 离线账号 -``` -┌─────────────┐ -│ [头像] │ ← 半透明(opacity: 0.5) -│ │ ← 无绿色圆点 -└─────────────┘ -``` - -## 🔧 优化特性 - -### 1. **限流机制** -```typescript -let lastAliveStatusRequest = 0 -const ALIVE_STATUS_REQUEST_INTERVAL = 5000 - -// 避免频繁请求 -if (now - lastAliveStatusRequest < ALIVE_STATUS_REQUEST_INTERVAL) { - return -} -``` - -### 2. **生命周期管理** -```typescript -// WebSocket 连接成功 → 启动定时器 -handleOpen() → startAliveStatusQuery() - -// WebSocket 断开 → 停止定时器 -disconnect() → stopAliveStatusQuery() -``` - -### 3. **错误处理** -```typescript -try { - accountStore.updateAccountsOnlineStatus(aliveStatus) -} catch (error) { - console.error('[AccountHandler] 更新 Store 失败:', error) - // 不阻断其他流程 -} -``` - -## 📊 与旧项目的对比 - -### 旧项目实现方式 - -```typescript -// 1. 在 websocket.ts 中定时请求 -const timer = setInterval(() => { - sendCommand("CmdRequestWechatAccountsAliveStatus", { - wechatAccountIds: customerList.map(v => v.id) - }) -}, 5000) - -// 2. 在 msgManage.ts 中直接更新 Store -const updatedCustomerList = customerList.map(customer => ({ - ...customer, - isOnline: wechatAccountsAliveStatus[customer.id] || false, -})) - -// 3. 手动排序 -updatedCustomerList.sort((a, b) => { - if (a.isOnline && !b.isOnline) return -1 - if (!a.isOnline && b.isOnline) return 1 - return 0 -}) - -updateCustomerList(updatedCustomerList) -``` - -### 新项目改进点 - -| 对比项 | 旧项目 | 新项目 | 优势 | -|--------|--------|--------|------| -| **架构** | 直接操作 Store | 发布订阅模式 | 解耦,易维护 | -| **类型安全** | 无 | TypeScript 完整类型 | 减少 Bug | -| **状态管理** | Zustand | Pinia + Composable | Vue 3 最佳实践 | -| **订阅机制** | 手动监听 | mitt 事件总线 | 更高效,支持多订阅者 | -| **生命周期** | 手动清理 | 自动清理 | 避免内存泄漏 | -| **限流** | 无 | 5 秒限流 | 减少服务器压力 | - -## 🚀 使用示例 - -### 在组件中使用 - -```vue - - - - - 在线: {{ onlineCount }} / {{ accountList.length }} - - - {{ account.name }} - 🟢 在线 - ⚫ 离线 - - - -``` - -## 🐛 调试 - -### 开启调试日志 - -在浏览器控制台查看: - -``` -[WebSocket] 📡 请求账号在线状态: [300745, 300746, 300747] -[AccountHandler] 📡 账号状态更新: { count: 3, online: 2 } -[MessageSubscription] emit: account:status, 订阅者数: 1 -[AccountStore] 📡 收到在线状态变化: { accountId: 300745, isOnline: true } -[AccountStore] ✅ 批量更新在线状态完成: { total: 3, online: 2 } -``` - -## ✅ 测试清单 - -- [x] WebSocket 连接成功后自动开始查询 -- [x] 每 5 秒查询一次在线状态 -- [x] 在线账号显示绿色圆点 -- [x] 离线账号半透明显示 -- [x] 在线账号自动排在前面 -- [x] 断开连接时停止查询 -- [x] 重连后恢复查询 -- [x] 切换账号时状态正确 -- [x] 多个账号同时在线/离线切换正常 -- [x] 控制台无错误日志 - -## 📝 注意事项 - -1. **服务器支持**:确保后端支持 `CmdRequestWechatAccountsAliveStatus` 命令 -2. **性能优化**:5 秒间隔是平衡实时性和性能的折中方案 -3. **限流机制**:避免同时发送多个请求 -4. **错误容忍**:单个账号状态更新失败不影响其他账号 -5. **生命周期**:组件卸载时自动清理定时器 - -## 🔮 未来优化 - -1. **WebSocket 推送**:改为服务器主动推送状态变化(减少轮询) -2. **批量优化**:支持一次查询返回所有账号状态 -3. **离线缓存**:本地缓存最后已知状态 -4. **状态过渡**:添加淡入淡出动画效果 -5. **通知提醒**:账号上线/下线时桌面通知 - ---- - -**文档版本**: v1.0 -**最后更新**: 2026-01-15 -**负责人**: TouchVueThree Team diff --git a/TouchVueThree/开发日志.md b/TouchVueThree/开发日志.md deleted file mode 100644 index 41f5218..0000000 --- a/TouchVueThree/开发日志.md +++ /dev/null @@ -1,1517 +0,0 @@ -# 触客宝 Vue3 版本开发日志 - -> 项目开始时间:2026-01 -> -> 最后更新:2026-01-14 - ---- - -## 📋 项目概况 - -### 技术栈 - -- **前端框架**:Vue 3 + TypeScript -- **UI 库**:Element Plus 2.13.1 -- **状态管理**:Pinia -- **路由**:Vue Router -- **构建工具**:Vite -- **本地存储**:IndexedDB (Dexie) -- **实时通信**:WebSocket -- **其他**:mitt (事件总线)、echarts、dayjs - ---- - -## 🎯 项目总体进度:75% - -### ✅ 已完成模块 - -#### 1. 基础架构(100%) - -- [x] 项目初始化和构建配置 -- [x] 路由系统配置 -- [x] 状态管理(Pinia Store) -- [x] API 请求封装 -- [x] IndexedDB 数据库封装 -- [x] 全局样式和主题 -- [x] Sentry 错误监控集成 - -#### 2. 登录和身份验证(100%) - -- [x] 登录页面 -- [x] Token 管理 -- [x] 路由守卫 -- [x] 权限控制 - -#### 3. 布局系统(100%) - -- [x] MainLayout - 主布局 -- [x] PowerLayout - 功能中心布局 -- [x] 响应式适配 - -#### 4. 联系人系统(95%) - -- [x] 联系人列表 -- [x] 好友管理 -- [x] 群组管理 -- [x] 联系人搜索 -- [x] 联系人右键菜单(含置顶功能) -- [x] 虚拟滚动优化 -- [ ] 联系人标签管理(待完善) - -#### 5. 会话系统(90%) - -- [x] 会话列表 -- [x] 会话排序(置顶、时间、未读) -- [x] 会话搜索 -- [x] 未读数管理 -- [x] 会话自动创建 -- [ ] 会话归档功能(待开发) - -#### 6. 聊天窗口(85%) - -- [x] 聊天窗口基础布局 -- [x] 消息列表(虚拟滚动) -- [x] 消息渲染(文本、图片、表情等) -- [x] 消息输入框(contentEditable) - - [x] 文本输入 - - [x] 腾讯表情包集成(100+ 表情) - - [x] 图片上传 - - [x] 视频上传 - - [x] 文件上传 - - [x] 语音录制(支持 MP3/WebM/WAV/AMR) - - [x] 位置选择 - - [x] 消息草稿保存 -- [x] 消息发送状态管理 -- [x] 消息撤回 -- [x] 表情解析和渲染 -- [x] 聊天头部(AI 模式切换) -- [x] 跟进提醒功能 -- [x] 待办事项功能 -- [ ] 消息转发(待开发) -- [ ] 聊天记录导出(待开发) - -#### 7. 个人资料卡片(90%) - -- [x] 基本信息展示 -- [x] 详细字段编辑 -- [x] 快捷回复 -- [x] 标签管理 -- [x] 群公告(含 AI 生成) -- [x] 朋友圈预览 -- [ ] 更多扩展功能(待规划) - -#### 8. WebSocket 架构(100%)✨ 新架构 - -- [x] WebSocket 连接管理 -- [x] 心跳机制 -- [x] 断线重连 -- [x] 消息处理器模块化 - - [x] messageHandlers - 聊天消息处理 - - [x] accountHandlers - 账号状态处理 - - [x] sessionHandlers - 会话相关处理 - - [x] systemHandlers - 系统通知处理 -- [x] 发布订阅模式(useMessageSubscription) -- [x] 消息去重机制 -- [x] Store 统一订阅 -- [x] 完整的调试工具和日志 -- [x] 架构文档和使用指南 - ---- - -## ⚠️ 开发注意事项 - -### 1. Element Plus 相关 - -#### 按钮类型限制 - -```typescript -// ❌ 错误:type="link" 在 2.13.1 中不支持 -按钮 - -// ✅ 正确:使用空字符串获得文本样式 -按钮 - -// ⚠️ 已弃用但仍可用(会有警告) -按钮 -``` - -**有效的 type 值**:`["default", "primary", "success", "warning", "info", "danger", "text", ""]` - -#### 其他注意事项 - -- `el-button` 的 `type="text"` 在 3.0.0 中将被弃用,推荐使用 `type=""` -- `` 不能在 `` 内部使用,应该用普通 div 替代 - -### 2. 表情包集成 - -#### 路径配置 - -```typescript -// ✅ 正确:public 目录下的资源路径 -const emojiPath = '/face/smile.png' - -// ❌ 错误:不要加 /assets 前缀 -const emojiPath = '/assets/face/smile.png' -``` - -#### 表情数据结构 - -```typescript -interface EmojiInfo { - name: string // 中文名称,如:'微笑' - path: string // 图片路径,如:'/face/smile.png' - category: EmojiCategory -} -``` - -#### 消息格式转换 - -```typescript -// 发送时:HTML → 纯文本 -contentEditable.innerHTML → htmlToText() → '[微笑]你好' - -// 接收时:纯文本 → HTML -'[微笑]你好' → parseEmojiText() → '你好' -``` - -### 3. contentEditable 输入框 - -#### 关键点 - -- 使用 `div[contenteditable="true"]` 而非 `textarea` -- 必须手动管理光标位置(insertHtmlAtCursor) -- 需要显式跟踪 `innerHTML` 变化以触发 Vue 响应式 -- 粘贴事件需要过滤和清理 HTML - -#### 响应式处理 - -```typescript -// ✅ 必须同时更新 inputContent 才能触发 computed -const inputRef = ref() -const inputContent = ref('') - -function handleInput() { - inputContent.value = inputRef.value.innerHTML - // 现在 canSend 会正确更新 -} -``` - -### 4. 语音录制 - -#### 音频格式优先级 - -```typescript -// 优先级:MP3 > WebM > WAV > AMR -const mimeTypes = [ - 'audio/mpeg', // .mp3 - 'audio/webm', // .webm - 'audio/wav', // .wav - 'audio/amr', // .amr -] -``` - -#### 文件后缀匹配 - -- 必须根据实际的 `mimeType` 设置正确的文件扩展名 -- 上传接口:`/v1/attachment/upload` -- 消息类型:`msgType: 34` - -### 5. WebSocket 消息处理 - -#### 新架构使用方式 - -**在组件中订阅消息:** - -```typescript -import { useMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - -const { onNewMessage } = useMessageSubscription() - -onNewMessage((message) => { - // 处理新消息 -}) -``` - -**在 Store 中统一订阅:** - -```typescript -import { setupMessageSubscription } from '@/composables/business/wechat/useMessageSubscription' - -setupMessageSubscription({ - onNewMessage: (msg) => addMessage(msg), - onMessageUpdate: ({ id, changes }) => updateStatus(id, changes), -}) -``` - -**新增消息类型处理:** - -```typescript -// 1. 在 handlers/messageHandlers.ts 中添加处理函数 -function handleNewFeature(message: WebSocketMessage) { - // 处理逻辑 -} - -// 2. 注册到 messageHandlers -export const messageHandlers: HandlerRegistry = { - [WS_CMD_TYPE.NEW_FEATURE]: handleNewFeature, -} - -// 3. 完成!自动生效 -``` - -### 6. IndexedDB 使用 - -#### 数据管理器 - -- `SessionManager` - 会话数据管理 -- `MessageManager` - 消息数据管理 -- `ContactManager` - 联系人数据管理(如果有) - -#### 注意事项 - -- 所有数据库操作都是异步的(返回 Promise) -- 使用 `.catch()` 处理错误,不要阻塞主流程 -- 消息去重:使用 `checkDuplicate(clientId)` - -### 7. 消息头像匹配 - -#### 发送消息的头像 - -```typescript -// ✅ 根据 wechatAccountId 从客服账号列表获取 -const senderAvatar = computed(() => { - if (props.message.isSend) { - const account = accountStore.accountList.find( - (acc) => acc.id === props.message.wechatAccountId - ) - return account?.avatar || DEFAULT_AVATAR - } - return props.message.sender?.avatar || DEFAULT_AVATAR -}) -``` - -### 8. 样式布局注意 - -#### 消息布局(类似微信) - -```scss -// 发送的消息:右对齐,头像在右 -.message-send { - justify-content: flex-end; - // HTML 顺序:[状态图标] [消息气泡] [头像] -} - -// 接收的消息:左对齐,头像在左 -.message-receive { - justify-content: flex-start; - // HTML 顺序:[头像] [消息气泡] -} -``` - -### 9. SCSS 编译 - -#### @keyframes 位置 - -```scss -// ✅ 正确:@keyframes 在根级别 - - -// ❌ 错误:@keyframes 嵌套在选择器内 - -``` - ---- - -## 🐛 已修复的问题 - -### 1. Element Plus 相关 - -- ✅ 修复 `type="link"` 不被支持的问题(改为 `type=""`) -- ✅ 修复 `` 在 div 内部使用的错误 -- ✅ 修复 placeholder 中未转义引号的语法错误 - -### 2. WebSocket 相关 - -- ✅ 修复 `messageEmitter` 导出不存在的错误 -- ✅ 优化消息处理架构,解决代码混乱问题 - -### 3. 表情包相关 - -- ✅ 修复表情包路径引用错误(从 `/assets/face/` 改为 `/face/`) -- ✅ 修复表情选择器"其他"分类显示空白的问题 -- ✅ 优化表情选择器尺寸和布局 - -### 4. 消息输入相关 - -- ✅ 修复 contentEditable 输入框发送按钮无法点击的问题 -- ✅ 移除文本长度限制和计数显示 -- ✅ 移除输入框边框 - -### 5. 语音录制相关 - -- ✅ 修复音频文件后缀格式不正确的问题 -- ✅ 添加录音动画效果 - -### 6. 样式布局相关 - -- ✅ 修复自发送消息头像位置错误(现在在右侧) -- ✅ 优化消息状态图标位置 - -### 7. 搜索功能相关 - -- ✅ 修复搜索结果右键菜单无效的问题 -- ✅ 添加搜索结果置顶功能 - -### 8. SCSS 编译相关 - -- ✅ 修复 @keyframes 位置导致的编译错误 -- ✅ 修复多余的大括号导致的语法错误 - ---- - -## 📊 代码质量指标 - -### 架构改进(WebSocket) - -| 指标 | 旧架构 | 新架构 | 改善 | -| ---------- | ------ | ---------- | ----- | -| 单文件行数 | 600+ | 150 | -75% | -| 模块化程度 | ⭐⭐ | ⭐⭐⭐⭐⭐ | +150% | -| 可维护性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | +150% | -| Git 冲突率 | 高 | 低 | -80% | -| 调试效率 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +60% | - -### 测试覆盖率 - -- 单元测试:待开发(0%) -- 集成测试:待开发(0%) -- E2E 测试:待开发(0%) - ---- - -## 🔄 待开发功能 - -### 近期计划(1-2周) - -#### 1. 消息功能完善 - -- [ ] 消息转发给他人 -- [ ] 聊天记录查看和搜索 -- [ ] 消息引用/回复 -- [ ] 消息多选操作 - -#### 2. 文件管理 - -- [ ] 文件消息预览 -- [ ] 文件下载管理 -- [ ] 图片/视频查看器 - -#### 3. 群组功能 - -- [ ] 群成员管理 -- [ ] 群设置 -- [ ] @提醒功能 - -### 中期计划(3-4周) - -#### 1. AI 功能 - -- [ ] AI 辅助回复 -- [ ] AI 内容生成 -- [ ] 智能推荐 - -#### 2. 数据统计 - -- [ ] 消息统计 -- [ ] 客户分析 -- [ ] 数据报表 - -#### 3. 高级功能 - -- [ ] 消息同步优化 -- [ ] 离线消息处理 -- [ ] 消息加密 - -### 长期计划(1-2月) - -#### 1. 性能优化 - -- [ ] 虚拟滚动优化 -- [ ] 大文件上传优化 -- [ ] 内存管理优化 - -#### 2. 测试体系 - -- [ ] 单元测试(目标 >80%) -- [ ] 集成测试 -- [ ] E2E 测试 - -#### 3. 文档完善 - -- [ ] API 文档 -- [ ] 组件文档 -- [ ] 部署文档 - ---- - -## 📚 相关文档 - -### 架构文档 - -- [WebSocket架构优化方案.md](./WebSocket架构优化方案.md) - WebSocket 新架构设计思路 -- [WebSocket使用指南.md](./WebSocket使用指南.md) - WebSocket 快速上手指南(5分钟) -- [WebSocket架构实施完成报告.md](./WebSocket架构实施完成报告.md) - 架构实施总结 - -### 功能文档 - -- [会话列表优化实施总结.md](./会话列表优化实施总结.md) -- [联系人列表迁移完成报告.md](./联系人列表迁移完成报告.md) -- [个人资料功能修复总结.md](./个人资料功能修复总结.md) -- [聊天系统改造实施说明.md](./聊天系统改造实施说明.md) - -### API 文档 - -- [API接口说明.md](./API接口说明.md) -- [API接口修正说明.md](./API接口修正说明.md) - ---- - -## 👥 团队协作 - -### Git 工作流 - -```bash -# 功能开发 -git checkout -b feature/功能名称 -git commit -m "feat: 描述" -git push origin feature/功能名称 - -# Bug 修复 -git checkout -b fix/问题描述 -git commit -m "fix: 描述" -git push origin fix/问题描述 -``` - -### Commit 规范 - -- `feat:` - 新功能 -- `fix:` - Bug 修复 -- `docs:` - 文档更新 -- `style:` - 代码格式调整 -- `refactor:` - 重构 -- `perf:` - 性能优化 -- `test:` - 测试相关 -- `chore:` - 构建/工具相关 - ---- - -## 🎯 下一步行动 - -### 本周重点(Week of 2026-01-14) - -1. ✅ 完成 WebSocket 架构优化和文档 -2. ✅ 完成聊天窗口核心功能 -3. ✅ 修复所有已知 UI 问题 -4. 🔄 开始实现消息转发功能 -5. 🔄 开始实现聊天记录查看功能 - -### 下周计划(Week of 2026-01-21) - -1. 完成消息转发和聊天记录功能 -2. 实现文件预览和下载 -3. 开始群组功能开发 -4. 编写单元测试(目标:核心功能 >50%) - ---- - -## 📝 更新日志 - -### 2026-01-14 - -- ✅ 完成 WebSocket 架构重构(模块化 + 发布订阅模式) -- ✅ 完成聊天输入框迁移(contentEditable + 表情包 + 语音 + 位置) -- ✅ 完成跟进提醒和待办事项迁移 -- ✅ 修复 Element Plus type="link" 兼容性问题 -- ✅ 修复搜索结果右键菜单和置顶功能 -- ✅ 修复消息布局和头像显示问题 -- ✅ 创建完整的架构文档(3 个 MD 文件) - -### 待更新... - ---- - -## 🕳️ 踩坑记录 - -### 1. Element Plus 按钮类型混乱 ⚠️ 高频坑 - -**问题描述:** -Element Plus 2.13.1 在按钮 `type` 属性上存在混乱的兼容性问题: - -- 使用 `type="text"` 会触发弃用警告:"type.text is about to be deprecated in version 3.0.0, please use link instead" -- 按照警告修改为 `type="link"` 后,却触发验证错误:"Invalid prop: validation failed for prop type. Expected one of [...], got value 'link'" -- 文档推荐使用 `type="link"`,但运行时却不接受这个值 - -**错误示例:** - -```vue - -按钮 - - - -按钮 - -``` - -**解决方案:** - -```vue - -按钮 -``` - -**根本原因:** - -- Element Plus 2.x 过渡版本的 API 不稳定 -- TypeScript 类型定义与运行时验证不一致 -- 建议升级到 Element Plus 3.x(但需评估兼容性) - -**影响范围:** - -- `DetailFields.vue` - 详情字段编辑按钮 -- `MomentsPreview.vue` - 朋友圈加载更多按钮 -- `GroupNotice.vue` - 群公告生成按钮 -- `TagManager.vue` - 标签管理按钮 -- `PowerLayout.vue` - 权限中心布局按钮 - -**修复时间:** 2026-01-14 (耗时约 2 小时,反复尝试多种方案) - ---- - -### 2. contentEditable 输入框发送按钮失效 - -**问题描述:** -将 `textarea` 改为 `contentEditable` div 后,发送按钮突然无法点击(`disabled` 状态)。原因是 Vue 无法自动监听 `contentEditable` 元素的内容变化。 - -**错误代码:** - -```vue - - - 发送 - - - -``` - -**解决方案:** - -```typescript -// ✅ 引入显式的响应式变量跟踪内容 -const inputRef = ref() -const inputContent = ref('') // 显式跟踪 innerHTML - -// 每次内容变化时同步更新 -function handleInput() { - inputContent.value = inputRef.value!.innerHTML - saveDraft() // 保存草稿 -} - -// computed 依赖显式变量 -const canSend = computed(() => { - const text = htmlToText(inputContent.value) - return text.trim().length > 0 -}) -``` - -**关键点:** - -1. `contentEditable` 不是表单元素,Vue 的 `v-model` 无效 -2. 必须手动监听 `input` 事件 -3. 必须使用额外的 `ref` 显式跟踪 `innerHTML` -4. 所有修改 `innerHTML` 的操作都要同步更新这个 `ref` - -**影响范围:** - -- `MessageInput/index.vue` - 消息输入框 -- 所有涉及发送按钮状态、草稿保存、内容清空的逻辑 - -**修复时间:** 2026-01-14 (耗时约 30 分钟) - ---- - -### 3. 表情包路径错误 404 - -**问题描述:** -从旧项目迁移表情包数据时,直接复制了路径配置 `/assets/face/smile.png`,导致运行时所有表情图片 404。 - -**错误原因:** - -```typescript -// ❌ 旧项目路径(React + Webpack) -const emojiPath = '/assets/face/smile.png' - -// ✅ 新项目路径(Vue + Vite) -const emojiPath = '/face/smile.png' -``` - -**根本原因:** - -- Vite 的 `public` 目录下的资源路径是 `/资源名`,不需要 `/assets` 前缀 -- Webpack 的 `public` 配置可能不同 -- 旧项目可能有 `base` 配置或别名 - -**解决方案:** - -```typescript -// wechatEmoji.ts 批量修改所有路径 -export const EMOJI_DATA: Record = { - 微笑: { - name: '微笑', - path: '/face/smile.png', // 去掉 /assets 前缀 - category: EmojiCategory.FACE, - }, - // ... 100+ 条数据 -} -``` - -**预防措施:** - -- 迁移资源时注意不同构建工具的差异 -- 使用相对路径或配置 `@/assets` 别名 -- 添加资源加载失败的兜底逻辑 - -**影响范围:** - -- `wechatEmoji.ts` - 所有表情数据(100+ 条) -- `EmojiPicker/index.vue` - 表情选择器 - -**修复时间:** 2026-01-14 (耗时约 10 分钟) - ---- - -### 4. 语音录制文件格式不匹配 - -**问题描述:** -实现语音录制功能后,上传接口报错:"文件后缀格式不正确"。原因是浏览器录制的 `Blob` 没有设置正确的文件扩展名。 - -**错误代码:** - -```typescript -// ❌ 直接使用 Blob,没有文件名 -const blob = new Blob(audioChunks, { type: 'audio/webm' }) -await uploadFile(blob) -// 后端无法识别文件类型 -``` - -**解决方案:** - -```typescript -// ✅ 1. 优先使用 MP3 格式 -const mimeTypes = [ - 'audio/mpeg', // .mp3 (最佳兼容性) - 'audio/webm', // .webm (Chrome) - 'audio/wav', // .wav (通用) - 'audio/amr', // .amr (WeChat) -] - -// 2. 检测浏览器支持的格式 -const supportedType = mimeTypes.find((type) => - MediaRecorder.isTypeSupported(type) -) - -// 3. 转换为 File 对象并设置正确的扩展名 -const extension = supportedType.split('/')[1] // 'mpeg' -> 'mp3' -const file = new File([blob], `voice_${Date.now()}.${extension}`, { - type: supportedType, -}) - -// 4. 上传 -await uploadFile(file) -``` - -**扩展名映射表:** -| MIME Type | 扩展名 | 兼容性 | -|-----------|--------|--------| -| `audio/mpeg` | `.mp3` | ⭐⭐⭐⭐⭐ 最佳 | -| `audio/webm` | `.webm` | ⭐⭐⭐⭐ Chrome/Edge | -| `audio/wav` | `.wav` | ⭐⭐⭐ 通用但文件大 | -| `audio/amr` | `.amr` | ⭐⭐ 仅部分浏览器 | - -**影响范围:** - -- `MessageInput/index.vue` - 语音录制逻辑 -- 后端上传接口验证 - -**修复时间:** 2026-01-14 (耗时约 1 小时,包括查阅旧项目代码) - ---- - -### 5. SCSS @keyframes 编译错误 - -**问题描述:** -为录音按钮添加动画时,SCSS 报错:"expected '}',unmatched '}'"。 - -**错误代码:** - -```scss - -``` - -**解决方案:** - -```scss - -``` - -**原因:** - -- SCSS 的 `@keyframes` 必须在根级别或 `@media` 内 -- 不能嵌套在普通选择器内部 -- 即使在 `scoped` 样式中也要遵循这个规则 - -**影响范围:** - -- `MessageInput/index.vue` - 录音动画 -- `QuickReply.vue` - 也有类似的语法错误(多余的大括号) - -**修复时间:** 2026-01-14 (耗时约 15 分钟) - ---- - -### 6. 搜索结果右键菜单无效 - -**问题描述:** -为搜索结果添加右键菜单,但点击右键完全没反应,浏览器默认菜单依然弹出。 - -**错误代码:** - -```vue - - - {{ contact.name }} - - - - -``` - -**解决方案:** - -```typescript -function handleContextMenu(e: MouseEvent, contact: Contact) { - // ✅ 显式阻止默认行为和冒泡 - e.preventDefault() - e.stopPropagation() - - contextMenu.value = { - visible: true, - x: e.clientX, - y: e.clientY, - contact, - } -} -``` - -**原因:** - -- `@contextmenu.prevent` 修饰符有时不够可靠 -- 需要在函数内部显式调用 `preventDefault()` 和 `stopPropagation()` -- 特别是在有多层 DOM 嵌套时 - -**影响范围:** - -- `SearchResultList/index.vue` - 搜索结果右键菜单 - -**修复时间:** 2026-01-14 (耗时约 20 分钟) - ---- - -### 7. 消息头像位置错乱 - -**问题描述:** -自己发送的消息头像显示在左侧,而不是微信那样的右侧布局。 - -**问题演变过程:** - -```scss -// ❌ 尝试1:使用 row-reverse,但没效果 -.message-send { - flex-direction: row-reverse; - justify-content: flex-start; // 还是左对齐 -} - -// ❌ 尝试2:justify-content 不对 -.message-send { - flex-direction: row; - justify-content: flex-end; // 但元素顺序不对 -} - -// ✅ 最终方案:同时调整 HTML 顺序 + CSS -``` - -**解决方案:** - -```vue - - - - ✓ - 内容 - - - - - -``` - -**关键点:** - -1. HTML 元素顺序要符合视觉顺序 -2. 使用 `justify-content: flex-end` 实现右对齐 -3. 不要过度依赖 `flex-direction: row-reverse`(会导致元素顺序混乱) - -**影响范围:** - -- `MessageItem.vue` - 消息项组件 - -**修复时间:** 2026-01-14 (耗时约 40 分钟,经历多次尝试) - ---- - -### 8. 表情选择器空白过大 - -**问题描述:** -点击"其他"分类的表情时,下方出现巨大空白区域,体验很差。 - -**原因:** - -```scss -// ❌ 使用 max-height,内容少时会有大量空白 -.emoji-picker { - max-height: 350px; -} - -.emoji-grid { - display: grid; - grid-template-columns: repeat(10, 1fr); - // 没有设置 align-content -} -``` - -**解决方案:** - -```scss -// ✅ 1. 固定容器高度 -.emoji-picker { - height: 350px; // 固定高度,不是 max-height -} - -// 2. 内容顶部对齐 -.emoji-grid { - display: grid; - grid-template-columns: repeat(10, 1fr); - align-content: start; // 内容从顶部开始 - grid-auto-rows: 36px; // 固定行高 -} -``` - -**对比效果:** - -- **修复前**:8 个表情显示,下方空白 250px -- **修复后**:8 个表情紧凑显示,下方空白 100px(可滚动区域) - -**影响范围:** - -- `EmojiPicker/index.vue` - 表情选择器 - -**修复时间:** 2026-01-14 (耗时约 15 分钟) - ---- - -### 9. WebSocket 消息处理混乱(架构级问题) - -**问题描述:** -旧项目的 WebSocket 代码组织混乱,存在严重的可维护性问题: - -- 单文件 600+ 行,巨大的 `switch-case` -- 消息处理逻辑与网络层耦合 -- 难以测试和扩展 -- 多人协作容易冲突 - -**旧架构示例:** - -```typescript -// ❌ useWebSocket.ts (600+ 行) -function handleMessage(event: MessageEvent) { - const message = JSON.parse(event.data) - - switch (message.cmd) { - case 'CmdReceiveMessage': - // 100 行处理逻辑 - messageStore.addMessage(...) - sessionStore.updateSession(...) - break - case 'CmdMessageStatus': - // 50 行处理逻辑 - break - case 'CmdAccountStatus': - // 50 行处理逻辑 - break - // ... 20+ cases - } -} -``` - -**新架构解决方案:** - -**1. 分层设计:** - -``` -composables/business/wechat/ -├── useWebSocket.ts # 连接层(网络) -├── useMessageSubscription.ts # 订阅层(事件总线) -└── handlers/ # 处理层(业务逻辑) - ├── index.ts # 处理器注册 - ├── messageHandlers.ts # 消息相关 - ├── accountHandlers.ts # 账号相关 - ├── sessionHandlers.ts # 会话相关 - └── systemHandlers.ts # 系统相关 -``` - -**2. 发布订阅模式:** - -```typescript -// ✅ 处理器只关注业务逻辑 -export const messageHandlers = createMessageHandler({ - [WS_CMD_TYPE.RECEIVE_MESSAGE]: (message) => { - // 纯粹的业务逻辑,无网络层耦合 - emitNewMessage(transformMessage(message)) - }, -}) - -// Store 统一订阅 -setupMessageSubscription({ - onNewMessage: (msg) => messageStore.addMessage(msg), - onMessageUpdate: (update) => messageStore.updateStatus(update), -}) -``` - -**改进效果:** - -- 单文件行数:600+ → 150 行 ⬇️ 75% -- Git 冲突率:高 → 低 ⬇️ 80% -- 新增处理器:修改 1 个文件 → 新建 1 个文件 ✅ 无冲突 -- 可测试性:⭐⭐ → ⭐⭐⭐⭐⭐ - -**详细文档:** - -- [WebSocket架构优化方案.md](./WebSocket架构优化方案.md) -- [WebSocket使用指南.md](./WebSocket使用指南.md) - -**修复时间:** 2026-01-14 (重构耗时约 4 小时,含文档编写) - ---- - -### 10. WebSocket 发送消息格式不匹配 ⚠️ 严重问题 - -**问题描述:** -新项目发送消息时,WebSocket 请求没有被正确发送,导致消息无法送达服务器。经排查发现,新项目使用的消息格式与旧项目(后端要求)不一致。 - -**错误实现(新项目初版):** - -```typescript -// ❌ 错误:使用了自定义的格式,后端无法识别 -sendCommand('CmdSendTextMsg', { - wechatAccountId: session.wechatAccountId, - targetId: session.id, - targetType: session.type === 'group' ? 'group' : 'friend', - content: text, - msgType: 1, -}) -``` - -**正确实现(旧项目格式):** - -```typescript -// ✅ 正确:必须使用旧项目的格式(后端要求) -sendCommand('CmdSendMessage', { - wechatAccountId: session.wechatAccountId, - wechatChatroomId: session.type === 'group' ? session.id : 0, - wechatFriendId: session.type === 'friend' ? session.id : 0, - msgSubType: 0, - msgType: 1, - content: text, - seq: Date.now(), -}) -``` - -**关键区别:** - -| 字段 | 新项目(错误) | 旧项目(正确) | 说明 | -| -------------- | ------------------------- | ------------------------------------- | ------------------------- | -| **命令名称** | `CmdSendTextMsg` | `CmdSendMessage` | 后端识别的命令 | -| **目标标识** | `targetId` + `targetType` | `wechatChatroomId` + `wechatFriendId` | 使用两个字段区分好友/群聊 | -| **消息子类型** | ❌ 缺少 | `msgSubType: 0` | 必须字段 | -| **序列号** | ❌ 缺少 | `seq: Date.now()` | 用于消息追踪 | - -**根本原因:** - -- 后端 WebSocket 接口是基于旧项目设计的 -- 不同业务场景(好友消息、群消息、不同消息类型)使用 **相同的命令** `CmdSendMessage` -- 通过 `wechatChatroomId` 和 `wechatFriendId` 字段区分好友或群聊(互斥,一个为 0) -- 新项目在迁移时没有参考旧项目的实际格式,自行设计了新格式 - -**影响范围:** - -- `MessageInput/index.vue` - 文本、图片、文件、语音、位置消息发送 -- `QuickReply.vue` - 快捷回复发送 -- **所有** 需要发送消息的功能模块 - -**预防措施:** - -1. ⭐ **迁移前必须仔细对比旧项目的实际实现**,不能凭猜测 -2. WebSocket 协议格式必须与后端保持严格一致 -3. 不同业务场景(好友/群聊)可能使用相同的命令,通过参数区分 -4. 测试时要验证 WebSocket 消息是否真的被发送(查看网络面板) -5. 添加 WebSocket 消息发送日志,方便调试 - -**调试技巧:** - -```javascript -// 1. 在 sendCommand 中添加日志 -console.log('📤 发送 WebSocket 消息:', cmdType, data) - -// 2. 在浏览器开发者工具查看 WS 请求 -// Network → WS → 选择连接 → Messages → 查看发送的消息 - -// 3. 对比旧项目的实际发送格式 -// 旧项目运行时抓包,查看实际的 JSON 格式 -``` - -**修复时间:** 2026-01-14 (排查耗时约 1.5 小时,修复约 30 分钟) - -**教训:** -这是一个典型的"假设错误"问题。开发时认为 `CmdSendTextMsg`、`targetId`、`targetType` 这些命名更"合理",但后端实际使用的是 `CmdSendMessage`、`wechatChatroomId`、`wechatFriendId`。**永远不要假设,要实际验证!** - ---- - -### 11. WebSocket 非单例导致"未连接"错误 ⚠️⚠️ 严重架构问题 - -**问题描述:** -修复了发送消息格式后,发送消息时仍然提示"WebSocket 未连接"。经排查发现,**`useWebSocket` composable 不是单例模式**,每次调用都创建新实例,导致: - -- `Chat/index.vue` 中 `connect()` 建立的连接 -- `MessageInput/index.vue` 中 `sendCommand()` 使用的是**另一个实例**,其 `ws.value` 为 `null` - -**错误架构(非单例):** - -```typescript -// ❌ 错误:每次调用创建新实例 -export function useWebSocket() { - const ws = ref(null) // ❌ 函数内部的局部变量 - const status = ref('disconnected') - // ... - - return { ws, status, connect, sendCommand } -} - -// 在组件 A 中 -const { connect } = useWebSocket() // 实例 A -connect({ token: 'xxx' }) // 实例 A 建立连接 - -// 在组件 B 中 -const { sendCommand } = useWebSocket() // 实例 B(新的!) -sendCommand('CmdSendMessage', {}) // ❌ 实例 B 的 ws = null -``` - -**正确架构(单例模式):** - -```typescript -// ✅ 正确:状态在函数外部,全局共享 -const ws = ref(null) // ⭐ 全局单例 -const status = ref('disconnected') -let heartbeatTimer: NodeJS.Timeout | null = null -// ... - -/** - * WebSocket 管理 Composable(单例模式) - * - * ⚠️ 重要:此 composable 采用单例模式 - * - 所有组件共享同一个 WebSocket 连接 - * - 多次调用 useWebSocket() 返回的是同一个实例 - * - 只需在根组件(Chat/index.vue)调用 connect() 一次 - */ -export function useWebSocket() { - // 返回全局共享的状态和方法 - return { ws, status, connect, sendCommand } -} - -// 在组件 A 中 -const { connect } = useWebSocket() // 访问全局实例 -connect({ token: 'xxx' }) // 全局实例建立连接 - -// 在组件 B 中 -const { sendCommand } = useWebSocket() // 访问全局实例(同一个) -sendCommand('CmdSendMessage', {}) // ✅ 使用全局实例的连接 -``` - -**Vue 3 Composable 单例模式关键点:** - -1. **状态定义位置:** - - ❌ 错误:`function useXxx() { const state = ref() }`(函数内部 = 局部) - - ✅ 正确:`const state = ref(); function useXxx() {}`(函数外部 = 全局) - -2. **与 React Hooks 的区别:** - - React Hooks:每次调用都创建新状态(设计如此) - - Vue Composable:可以共享状态(将状态放在函数外部) - -3. **适用场景:** - - 全局单例:WebSocket、事件总线、配置管理 - - 局部实例:表单状态、组件内部逻辑 - -**影响范围:** - -- `useWebSocket.ts` - WebSocket 核心逻辑 -- 所有使用 WebSocket 的组件(6+ 个组件) - -**对比 Pinia Store:** -| 方案 | 优点 | 缺点 | 适用场景 | -|------|------|------|---------| -| **Composable 单例** | 轻量、灵活、无需注册 | 需要手动管理状态作用域 | WebSocket、工具函数 | -| **Pinia Store** | 自动单例、devtools 支持、TypeScript 完美 | 需要注册、略重 | 业务状态管理 | - -**修复时间:** 2026-01-14 (排查耗时约 20 分钟,修复约 5 分钟) - -**教训:** - -- Vue 3 Composable 默认**不是**单例,需要显式将状态放在函数外部 -- WebSocket、事件总线等全局资源必须使用单例模式 -- 如果逻辑复杂或需要 devtools 支持,优先考虑 Pinia Store -- 调试 "未连接" 类问题时,先检查是否是实例不一致导致的 - ---- - -### 12. WebSocket 发送格式多了 `data` 包裹层 ⚠️ 格式错误 - -**问题描述:** -修复了非单例问题后,发送消息仍然失败。用户指出新项目的 WebSocket 发送格式多了一层 `data` 包裹,与旧项目不一致。 - -**错误格式(新项目):** - -```typescript -// ❌ 错误:sendCommand 将参数包裹在 data 中 -const sendCommand = (cmdType: string, data?: any) => { - return send({ - cmdType, - seq: Date.now(), - data, // ❌ 错误:作为一个属性 - }) -} - -// 发送结果: -{ - "cmdType": "CmdSendMessage", - "seq": 1234567890, - "data": { // ❌ 多了一层包裹 - "wechatAccountId": 1, - "wechatFriendId": 100, - "content": "hello" - } -} -``` - -**正确格式(旧项目):** - -```typescript -// ✅ 正确:直接展开参数到顶层 -const sendCommand = (cmdType: string, data?: any) => { - return send({ - cmdType, - ...data, // ✅ 展开运算符,直接合并到顶层 - seq: Date.now(), - }) -} - -// 发送结果: -{ - "cmdType": "CmdSendMessage", - "seq": 1234567890, - "wechatAccountId": 1, // ✅ 在顶层 - "wechatFriendId": 100, - "content": "hello" -} -``` - -**根本原因:** - -- 对象属性 `{ data }` 与对象展开 `{ ...data }` 的区别 -- `{ cmdType, data }` → 创建了一个新对象,`data` 作为其属性 -- `{ cmdType, ...data }` → 将 `data` 的所有属性展开到当前对象 - -**JavaScript 知识点:** - -```javascript -const data = { a: 1, b: 2 } - -// ❌ 包裹(错误) -const obj1 = { type: 'test', data } -// 结果:{ type: 'test', data: { a: 1, b: 2 } } - -// ✅ 展开(正确) -const obj2 = { type: 'test', ...data } -// 结果:{ type: 'test', a: 1, b: 2 } -``` - -**影响范围:** - -- `useWebSocket.ts` - `sendCommand` 方法 -- 所有通过 WebSocket 发送的消息 - -**修复时间:** 2026-01-14 (用户指出问题,修复约 2 分钟) - -**教训:** - -- 迁移代码时,必须**逐字对比**发送格式,不能想当然 -- `{ data }` 和 `{ ...data }` 完全不同,一个字符的差异导致完全不同的结果 -- 使用浏览器开发者工具查看 WebSocket 实际发送的 JSON 数据 -- 感谢用户细心指出问题!👍 - ---- - -### 13. 群聊消息发送字段取值错误 ⚠️ 逻辑错误 - -**问题描述:** -用户再次指出,发送群聊消息时 `wechatChatroomId` 的取值不正确,使用了 `session.id` 而不是 `session.chatroomId`。 - -**错误实现:** - -```typescript -// ❌ 错误:群聊时使用 session.id -wechatChatroomId: session.type === 'group' ? session.id : 0 // ❌ -wechatFriendId: session.type === 'friend' ? session.id : 0 -``` - -**正确实现:** - -```typescript -// ✅ 正确:群聊时使用 session.chatroomId -wechatChatroomId: session.type === 'group' - ? session.chatroomId || session.id - : 0 // ✅ -wechatFriendId: session.type === 'friend' ? session.id : 0 -``` - -**数据结构对比:** - -| 联系人类型 | session.id | session.chatroomId | wechatFriendId | wechatChatroomId | -| ---------- | -------------- | ------------------ | -------------- | ---------------- | -| **好友** | 123 (好友ID) | undefined | 123 | 0 | -| **群聊** | 456 (联系人ID) | '789' (群聊ID) | 0 | '789' | - -**旧项目的判断逻辑:** - -```javascript -// 旧项目使用 chatroomId 字段判断是否为群聊 -wechatChatroomId: contract?.chatroomId ? contract.id : 0, -wechatFriendId: contract?.chatroomId ? 0 : contract.id, -``` - -**根本原因:** - -- 新项目的 `Session` 类型定义中没有明确 `chatroomId` 字段(虽然运行时存在) -- 混淆了 `session.id`(联系人ID)和 `session.chatroomId`(群聊ID) -- 对于群聊,这两个 ID 可能不同,必须使用 `chatroomId` - -**影响范围:** - -- `MessageInput/index.vue` - 所有消息类型(5处) -- `QuickReply.vue` - 快捷回复(1处) - -**修复时间:** 2026-01-14 (用户指出问题,修复约 10 分钟) - -**教训:** - -- 数据结构迁移时,必须完全理解字段含义和关系 -- `session.id` 和 `session.chatroomId` 是不同的概念 -- TypeScript 类型不完整时,运行时可能有额外字段 -- 再次感谢用户的细心审查!🙏 - ---- - -### 14. 表情转换标识丢失 - -**问题描述:** -`contentEditable` 输入框中插入表情图片后,发送时无法正确转换为 `[表情名称]` 格式,服务器无法识别。 - -**错误实现:** - -```typescript -// ❌ 仅靠 alt 属性,容易丢失或被修改 -function insertEmoji(emoji: EmojiInfo) { - const img = `` - insertHtmlAtCursor(img) -} - -function htmlToText(html: string): string { - // ❌ alt 可能被用户编辑或粘贴时丢失 - return html.replace(/]*alt="([^"]*)"[^>]*>/g, '[$1]') -} -``` - -**解决方案:** - -```typescript -// ✅ 使用 data-emoji-name 属性作为主标识 -function insertEmoji(emoji: EmojiInfo) { - const img = `` - insertHtmlAtCursor(img) -} - -function htmlToText(html: string): string { - return html.replace(/]*>/g, (match) => { - // 优先级:data-emoji-name > alt > src 反查 - const dataName = match.match(/data-emoji-name="([^"]*)"/) - if (dataName) return `[${dataName[1]}]` - - const alt = match.match(/alt="([^"]*)"/) - if (alt) return `[${alt[1]}]` - - const src = match.match(/src="([^"]*)"/) - if (src) { - const name = getEmojiNameByPath(src[1]) - return name ? `[${name}]` : '[表情]' - } - - return '[表情]' - }) -} -``` - -**关键点:** - -1. 使用 `data-*` 属性存储语义化标识 -2. 提供多级降级方案(data > alt > src 反查) -3. 处理边界情况(未知表情) - -**影响范围:** - -- `MessageInput/index.vue` - 表情插入 -- `emoji.ts` - HTML/文本转换 - -**修复时间:** 2026-01-14 (耗时约 30 分钟) - ---- - -## 💡 技术债务 - -### 高优先级 - -- 需要添加单元测试 -- 需要优化 IndexedDB 查询性能 -- 需要处理大量消息时的内存占用 - -### 中优先级 - -- 代码分割和懒加载 -- 图片压缩和优化 -- 错误边界和容错处理 - -### 低优先级 - -- 代码注释完善 -- 类型定义优化 -- ESLint 规则完善 - ---- - -## 🎉 里程碑 - -- ✅ 2026-01-01: 项目启动 -- ✅ 2026-01-07: 基础架构完成 -- ✅ 2026-01-10: 联系人和会话列表完成 -- ✅ 2026-01-14: **WebSocket 架构优化完成** -- ✅ 2026-01-14: **聊天核心功能完成** -- 🎯 2026-01-20: 消息高级功能完成(预计) -- 🎯 2026-01-28: 群组功能完成(预计) -- 🎯 2026-02-10: Beta 版本发布(预计) -- 🎯 2026-02-28: 正式版本 1.0(预计) - ---- - -**当前版本**: v0.75.0 (Beta) - -**最后更新者**: AI Assistant - -**下次审查**: 2026-01-21 diff --git a/TouchVueThree/改造完成报告.md b/TouchVueThree/改造完成报告.md deleted file mode 100644 index eab4d53..0000000 --- a/TouchVueThree/改造完成报告.md +++ /dev/null @@ -1,402 +0,0 @@ -# 📊 TouchVueThree 聊天系统改造完成报告 - -> **改造日期**: 2026-01-13 -> **改造版本**: v1.0 -> **改造状态**: ✅ 核心功能已完成 - ---- - -## 🎯 改造目标 - -基于 [聊天系统改造方案.md](./聊天系统改造方案.md) 的完整技术方案,实现以下核心目标: - -1. ✅ **替换轮询为订阅机制** - 减少网络请求 95%+ -2. ✅ **实现缓存优先策略** - 首屏加载 < 200ms -3. ✅ **多账户数据隔离** - 一号一库,彻底隔离 -4. ✅ **自动创建会话** - 陌生好友消息无缝显示 -5. ✅ **防数据丢失机制** - 心跳检测 + 增量同步 -6. ✅ **离线缓存能力** - 支持离线查看 - ---- - -## 📦 改造内容 - -### 1. 核心模块(已完成) - -| 模块 | 文件路径 | 代码行数 | 状态 | -|------|---------|---------|------| -| **数据库管理器** | `src/utils/db.ts` | ~450 行 | ✅ 完成 | -| **会话管理器** | `src/utils/dbManagers/SessionManager.ts` | ~550 行 | ✅ 完成 | -| **消息管理器** | `src/utils/dbManagers/MessageManager.ts` | ~450 行 | ✅ 完成 | -| **用户 Store** | `src/stores/modules/user.ts` | 已集成 | ✅ 完成 | -| **会话 Store** | `src/stores/modules/wechat/useSessionStore.ts` | ~350 行 | ✅ 完成 | -| **WebSocket** | `src/composables/business/wechat/useWebSocket.ts` | 已集成 | ✅ 完成 | - -**总计新增/修改代码**: ~2000 行 - ---- - -## 🚀 核心改进 - -### 改进1: 替换轮询为订阅机制 - -**改造前**: -```typescript -// ❌ 定时器轮询(每3秒请求一次) -setInterval(() => { - loadSessions() // 1小时 = 1200次请求 -}, 3000) -``` - -**改造后**: -```typescript -// ✅ 订阅机制(按需更新) -SessionManager.onUpdate((sessions) => { - this.sessions = sessions // 1小时 = 实际消息数(可能只有几次) -}) -``` - -**效果**: -- 🔽 网络请求减少 **95%+** (1200次 → <50次) -- 🔽 服务器负载降低 **95%+** -- ⚡ 响应速度更快 (<50ms vs 0-3000ms) - ---- - -### 改进2: 缓存优先策略 - -**改造前**: -```typescript -// ❌ 每次都从服务器加载(1-3秒) -const sessions = await getSessionList() -``` - -**改造后**: -```typescript -// ✅ 缓存优先(<200ms) -// 步骤1: 从 IndexedDB 读取(立即显示) -sessions.value = await SessionManager.getUserSessions() - -// 步骤2: 后台同步服务器数据(不阻塞UI) -syncFromServer() -``` - -**效果**: -- ⚡ 首屏加载提升 **85%** (1-3s → <200ms) -- 📱 支持离线查看缓存 -- 🎯 用户体验提升显著 - ---- - -### 改进3: 多账户数据隔离 - -**改造前**: -``` -单一数据库,所有账户混存 -├── 账户A的数据 -├── 账户B的数据 ← 可能混乱 -└── 账户C的数据 -``` - -**改造后**: -``` -一号一库,物理隔离 -├── ChatDatabase_123 (账户A) -├── ChatDatabase_456 (账户B) -└── ChatDatabase_789 (账户C) -``` - -**效果**: -- ✅ 彻底隔离,防止数据混乱 -- ✅ 切换账户秒开(<500ms) -- ✅ 数据安全可靠 - ---- - -### 改进4: 自动创建会话 - -**改造前**: -``` -WebSocket 收到消息 - → 会话不存在 - → 消息丢失 ❌ -``` - -**改造后**: -``` -WebSocket 收到消息 - → 检查会话是否存在 - → 不存在?调用 getFriendDetail() - → 创建新会话 - → 显示在列表顶部 ✅ -``` - -**效果**: -- ✅ 不会丢消息 -- ✅ 新好友消息无缝显示 -- ✅ 降级方案保证可用(显示"未知用户") - ---- - -### 改进5: 防数据丢失机制 - -**改造前**: -``` -WebSocket 断线 - → 重连 - → 期间消息丢失 ❌ -``` - -**改造后**: -``` -WebSocket 断线 - → 记录最后同步时间 - → 重连 - → 增量同步遗漏消息 - → 数据完整 ✅ -``` - -**机制**: -- ✅ 心跳检测(30秒) -- ✅ 心跳超时重连(5秒) -- ✅ 指数退避重连(1s、2s、4s...) -- ✅ 增量同步(需后端支持) - ---- - -## 📊 性能对比 - -| 指标 | 改造前 | 改造后 | 提升 | -|------|--------|--------|------| -| **首屏加载** | 1-3s | <200ms | **85%** ⚡ | -| **切换会话** | 200ms | <100ms | **50%** ⚡ | -| **切换账户** | 1-2s | <500ms | **70%** ⚡ | -| **网络请求** | 1200次/小时 | <50次/小时 | **95%** 🔽 | -| **服务器负载** | 高 | 低 | **95%** 🔽 | -| **离线能力** | 无 | 完整缓存 | **100%** 📱 | -| **数据隔离** | 单库混存 | 一号一库 | **100%** 🔒 | -| **消息丢失率** | 1-5% | <0.01% | **99%** ✅ | - ---- - -## 🏗️ 架构对比 - -### 数据流向对比 - -**改造前**: -``` -定时器轮询 - ↓ -请求 API - ↓ -更新 Store - ↓ -刷新 UI -``` - -**改造后**: -``` -WebSocket 推送 - ↓ -更新 IndexedDB - ↓ -SessionManager 触发回调 - ↓ -Store 自动更新 - ↓ -UI 自动刷新 -``` - ---- - -## 🔧 技术栈 - -| 模块 | 技术选型 | 说明 | -|------|---------|------| -| **数据库** | Dexie (IndexedDB) | 一号一库,物理隔离 | -| **状态管理** | Pinia | 响应式,订阅数据库变更 | -| **实时通信** | WebSocket | 消息推送,心跳检测 | -| **UI 框架** | Vue 3 + Element Plus | 组件化 | - ---- - -## 📝 代码质量 - -### 代码规范 - -- ✅ **TypeScript**: 100% 类型安全 -- ✅ **ESLint**: 0 错误 -- ✅ **代码注释**: 完整的 JSDoc 注释 -- ✅ **命名规范**: 统一的命名风格 -- ✅ **模块化**: 职责清晰,易于维护 - -### 测试覆盖 - -- ✅ **浏览器兼容性**: 已检查 -- ✅ **数据库损坏恢复**: 已实现 -- ✅ **存储配额检测**: 已实现 -- ✅ **错误降级方案**: 已实现 - ---- - -## 📚 文档 - -### 已完成的文档 - -1. ✅ [聊天系统改造方案.md](./聊天系统改造方案.md) - 完整技术方案(3900行) -2. ✅ [聊天系统改造实施说明.md](./聊天系统改造实施说明.md) - 实施说明 -3. ✅ [QUICK_START_改造版.md](./QUICK_START_改造版.md) - 快速开始指南 -4. ✅ [改造完成报告.md](./改造完成报告.md) - 本文档 - ---- - -## ⚠️ 注意事项 - -### 必需的 API 接口 - -改造后需要后端提供以下接口: - -| 接口 | 路径 | 优先级 | 说明 | -|------|------|--------|------| -| `getFriendDetail` | `/api/friend/detail` | 🔴 高 | 获取好友详情(陌生好友消息时调用) | -| `getGroupDetail` | `/api/group/detail` | 🔴 高 | 获取群聊详情(陌生群聊消息时调用) | -| `getSessionList` | `/api/session/list` | 🔴 高 | 获取会话列表(登录、切换账户) | -| `getMessagesSince` | `/api/messages/since` | 🟡 中 | 增量同步消息(断线重连后) | - -### 使用规则 - -```typescript -// ✅ 必须遵守的规则 - -// 1. 使用 db() 函数(带括号) -await db().sessions.toArray() // ✅ 正确 - -// 2. 不要使用定时器轮询 -// ❌ setInterval(() => loadSessions(), 3000) - -// 3. 使用订阅机制 -SessionManager.onUpdate(() => {}) // ✅ 正确 - -// 4. 组件卸载时清理 -onUnmounted(() => sessionStore.cleanup()) -``` - ---- - -## 🔄 后续优化建议 - -### 短期(1-2周) - -- [ ] 实现增量同步接口 `getMessagesSince()` -- [ ] 添加消息搜索功能 -- [ ] 优化虚拟滚动性能 -- [ ] 添加骨架屏加载 -- [ ] 完善错误监控(Sentry) - -### 中期(1-2月) - -- [ ] 实现消息离线队列 -- [ ] 添加全文搜索索引 -- [ ] 优化大文件传输 -- [ ] 实现多标签页同步(BroadcastChannel) -- [ ] 添加性能监控 - -### 长期(3-6月) - -- [ ] 考虑 Electron 混合方案 -- [ ] 实现 WebAssembly 加速 -- [ ] 完善离线能力 -- [ ] 添加本地全文索引 - ---- - -## ✅ 验收清单 - -### 功能验收 - -- [x] 登录时初始化数据库 -- [x] 会话列表缓存优先显示 -- [x] WebSocket 消息自动更新会话 -- [x] 陌生好友消息自动创建会话 -- [x] 切换账户数据正确隔离 -- [x] 退出登录关闭数据库 -- [x] 订阅机制替代轮询 - -### 性能验收 - -- [x] 首屏加载 < 200ms -- [x] 切换会话 < 100ms -- [x] 网络请求减少 95%+ -- [x] 支持离线查看缓存 - -### 稳定性验收 - -- [x] 无 TypeScript 错误 -- [x] 无 ESLint 错误 -- [x] 浏览器兼容性检查 -- [x] 数据库损坏恢复机制 -- [x] 存储配额检测 - -### 代码质量验收 - -- [x] 完整的 TypeScript 类型定义 -- [x] 完整的 JSDoc 注释 -- [x] 统一的代码风格 -- [x] 模块化设计 -- [x] 错误处理完善 - ---- - -## 🎉 总结 - -### 改造成果 - -本次改造成功实现了以下核心目标: - -1. ✅ **性能提升 80%+** - 首屏加载、网络请求、服务器负载 -2. ✅ **用户体验提升** - 秒开、离线缓存、无缝显示 -3. ✅ **数据安全可靠** - 多账户隔离、防丢失、错误恢复 -4. ✅ **架构现代化** - 订阅机制、缓存优先、模块化设计 -5. ✅ **易于维护** - 类型安全、文档完善、职责清晰 - -### 架构优势 - -- 🚀 **性能优秀**: 首屏 <200ms,网络请求减少 95% -- 💾 **存储可控**: 每用户 <50MB,自动清理 -- 🔒 **数据安全**: 一号一库,物理隔离 -- 🛠️ **易维护**: 模块化,类型安全 -- 📱 **离线能力**: 支持离线查看缓存 -- 🎯 **用户体验**: 秒开,无缝,流畅 - -### 技术亮点 - -1. **订阅机制** - 替代轮询,减少 95% 网络请求 -2. **缓存优先** - 秒开体验,提升 85% 加载速度 -3. **一号一库** - 多账户物理隔离,彻底防混乱 -4. **自动创建** - 陌生好友消息无缝显示 -5. **防丢失** - 心跳检测 + 增量同步 + 错误恢复 - ---- - -## 📞 联系方式 - -如有问题或建议,请: - -1. 查看 [聊天系统改造方案.md](./聊天系统改造方案.md) -2. 查看 [QUICK_START_改造版.md](./QUICK_START_改造版.md) -3. 联系开发团队 - ---- - -**改造完成!** 🎊 - -**开始时间**: 2026-01-13 -**完成时间**: 2026-01-13 -**改造状态**: ✅ 核心功能已完成 -**代码质量**: ✅ 无错误,可上线 - ---- - -> **致谢** -> 感谢所有参与项目讨论和代码贡献的团队成员。 -> 本改造参考了微信、Telegram 等优秀产品的设计理念。 diff --git a/TouchVueThree/数据联动更新实施总结.md b/TouchVueThree/数据联动更新实施总结.md deleted file mode 100644 index cb04f9a..0000000 --- a/TouchVueThree/数据联动更新实施总结.md +++ /dev/null @@ -1,350 +0,0 @@ -# 数据联动更新实施总结 - -## ✅ 已完成的工作 - -### 1. 扩展 SessionManager(核心方法) - -**文件**: `TouchVueThree/src/utils/dbManagers/SessionManager.ts` - -#### 新增方法 - -```typescript -// ⭐ 核心方法:从详情数据同步到本地数据库 -static async syncSessionFromDetail( - detail: any, - sessionId: number, - sessionType: 'friend' | 'group' -): Promise - -// 快捷方法:从好友详情同步 -static async syncSessionFromFriendDetail(friendId: number, detail: any): Promise - -// 快捷方法:从群聊详情同步 -static async syncSessionFromGroupDetail(groupId: number, detail: any): Promise -``` - -#### 功能说明 - -- ✅ 自动对比本地和服务器数据,只更新有变化的字段 -- ✅ 支持好友和群聊两种类型 -- ✅ 自动重新生成 `sortKey`,确保排序正确 -- ✅ 更新后自动触发订阅回调,UI 自动刷新 -- ✅ 支持扩展字段(`extendFields`)同步 -- ✅ 支持标签(`labels`)同步 - -### 2. 修改 CustomerInfo.vue(个人资料卡) - -**文件**: `TouchVueThree/src/views/Chat/components/ChatWindow/components/ProfileCard/components/CustomerInfo.vue` - -#### 修改点 - -1. **导入依赖** - ```typescript - import { getGroupDetail } from '@/api/modules/wechat' - import { SessionManager } from '@/utils/dbManagers/SessionManager' - ``` - -2. **`handleSaveFriendInfo`(保存好友信息)** - - ✅ 调用 `updateFriendInfo` API - - ✅ 重新获取好友详情 `getFriendDetail` - - ✅ 同步到本地数据库 `SessionManager.syncSessionFromFriendDetail` - - ✅ 移除了手动更新会话列表的代码(由数据库订阅自动处理) - -3. **`handleSaveGroupInfo`(保存群聊信息)** - - ✅ 发送 WebSocket 命令 - - ✅ 重新获取群聊详情 `getGroupDetail` - - ✅ 同步到本地数据库 `SessionManager.syncSessionFromGroupDetail` - -4. **`handleUpdateTags`(更新标签)** - - ✅ 发送 WebSocket 命令 - - ✅ 调用 `updateFriendInfo` API - - ✅ 重新获取好友详情 - - ✅ 同步到本地数据库 - -### 3. 修改 SessionContextMenu.vue(会话列表右键菜单) - -**文件**: `TouchVueThree/src/views/Chat/components/SidebarMenu/SessionList/SessionContextMenu.vue` - -#### 修改点 - -1. **导入依赖** - ```typescript - import { updateFriendConfig, getFriendDetail } from '@/api/modules/wechat' - import { SessionManager } from '@/utils/dbManagers/SessionManager' - ``` - -2. **`handleEditRemark`(修改备注)** - - ✅ 调用 `updateFriendConfig` API - - ✅ 重新获取好友详情 `getFriendDetail` - - ✅ 同步到本地数据库 `SessionManager.syncSessionFromFriendDetail` - - ✅ 移除了手动调用 `sessionStore.updateSessionRemark` 的代码 - -### 4. 修改 useContactStore(联系人 Store) - -**文件**: `TouchVueThree/src/stores/modules/wechat/useContactStore.ts` - -#### 修改点 - -1. **导入依赖** - ```typescript - import { getFriendDetail, getGroupDetail } from '@/api' - import { SessionManager } from '@/utils/dbManagers/SessionManager' - ``` - -2. **`updateContactRemark`(修改联系人备注)** - - ✅ 调用 `updateFriendInfo` API - - ✅ 更新内存中的联系人数据 - - ✅ 重新获取详情(好友或群聊) - - ✅ 同步到本地数据库 - - ✅ 移除了手动更新会话列表的代码 - -## 📊 数据流对比 - -### 修改前(❌ 数据不一致) - -``` -用户修改备注 - ↓ -调用 API (updateFriendInfo) - ↓ -⚠️ 只更新了部分 UI - ↓ -❌ 会话列表显示旧备注 -❌ 本地数据库还是旧数据 -❌ sortKey 没有更新 -``` - -### 修改后(✅ 数据一致) - -``` -用户修改备注 - ↓ -调用 API (updateFriendInfo) - ↓ -重新获取详情 (getFriendDetail) ← ⭐ 获取服务器最新数据 - ↓ -同步到本地数据库 (SessionManager.syncSessionFromDetail) ← ⭐ 关键步骤 - ↓ -触发数据库订阅回调 (triggerCallbacks) - ↓ -✅ 会话列表自动更新(显示新备注) -✅ 聊天窗口头部自动更新 -✅ 个人资料卡自动更新 -✅ sortKey 自动重新生成 -✅ 排序自动调整 -``` - -## 🔄 支持的数据同步场景 - -| 场景 | 操作入口 | 同步字段 | 状态 | -|------|---------|---------|------| -| **修改好友备注** | 会话列表右键菜单 | `conRemark`, `sortKey` | ✅ | -| **修改好友备注** | 个人资料卡 | `conRemark`, `sortKey` | ✅ | -| **修改好友备注** | 联系人列表右键菜单 | `conRemark`, `sortKey` | ✅ | -| **修改扩展字段** | 个人资料卡 | `extendFields`, `phone`, `email` 等 | ✅ | -| **修改标签** | 个人资料卡 | `labels` | ✅ | -| **修改群聊名称** | 个人资料卡 | `nickname` (群名), `sortKey` | ✅ | -| **置顶会话** | 会话列表右键菜单 | `config.top`, `sortKey` | ✅ (已实现) | -| **删除会话** | 会话列表右键菜单 | `deletedAt` | ✅ (已实现) | - -## 🎯 核心优势 - -### 1. 数据一致性 - -- ✅ 所有 UI 位置显示的数据保持同步 -- ✅ 本地数据库和服务器数据保持一致 -- ✅ `sortKey` 自动更新,排序始终正确 - -### 2. 代码简洁 - -- ✅ 不需要在每个地方手动更新 UI -- ✅ 统一的数据同步逻辑 -- ✅ 移除了冗余的手动更新代码 - -### 3. 易于维护 - -- ✅ 新增修改入口时,只需调用 `SessionManager.syncSessionFromDetail` -- ✅ 统一的错误处理 -- ✅ 清晰的数据流 - -### 4. 自动化 - -- ✅ 数据库变更自动触发 UI 更新 -- ✅ `sortKey` 自动重新生成 -- ✅ 排序自动调整 - -## 📝 使用示例 - -### 示例 1:修改好友备注 - -```typescript -// 1. 调用 API -await updateFriendInfo({ - id: friendId, - conRemark: newRemark, -}) - -// 2. 重新获取详情 -const detail = await getFriendDetail({ id: friendId }) - -// 3. 同步到本地数据库(一行代码搞定) -await SessionManager.syncSessionFromFriendDetail(friendId, detail.detail || detail) - -// ✅ 会话列表、聊天窗口头部、个人资料卡会自动更新 -// ✅ sortKey 自动重新生成,排序自动调整 -``` - -### 示例 2:修改扩展字段 - -```typescript -// 1. 调用 API -await updateFriendInfo({ - id: friendId, - phone: '123456', - email: 'test@example.com', -}) - -// 2. 重新获取详情 -const detail = await getFriendDetail({ id: friendId }) - -// 3. 同步到本地数据库 -await SessionManager.syncSessionFromFriendDetail(friendId, detail.detail || detail) - -// ✅ extendFields 字段会自动更新到数据库 -// ✅ 个人资料卡会自动显示最新数据 -``` - -### 示例 3:修改群聊名称 - -```typescript -// 1. 发送 WebSocket 命令 -sendCommand('CmdChatroomOperate', { - wechatAccountId: accountId, - wechatChatroomId: groupId, - chatroomOperateType: 6, - extra: JSON.stringify({ chatroomName: newName }), -}) - -// 2. 重新获取群聊详情 -const detail = await getGroupDetail({ groupId: groupId }) - -// 3. 同步到本地数据库 -await SessionManager.syncSessionFromGroupDetail(groupId, detail) - -// ✅ 会话列表、聊天窗口头部会自动更新 -// ✅ sortKey 自动重新生成(群名是排序的一部分) -``` - -## ⚠️ 注意事项 - -### 1. API 返回格式 - -不同 API 的返回格式可能不同,需要适配: - -```typescript -// getFriendDetail 可能返回 -{ detail: { id: 123, conRemark: '新备注', ... } } -// 或 -{ id: 123, conRemark: '新备注', ... } - -// 统一处理 -const detail = response.detail || response -await SessionManager.syncSessionFromFriendDetail(friendId, detail) -``` - -### 2. 扩展字段格式 - -扩展字段在 API 中是 JSON 字符串: - -```typescript -// API 返回 -{ - extendFields: '{"phone":"123456","email":"test@example.com"}' -} - -// syncSessionFromDetail 会直接保存字符串格式到数据库 -// UI 层需要自己解析 JSON -const extFields = JSON.parse(session.extendFields || '{}') -``` - -### 3. 标签字段格式 - -标签可能是数组或逗号分隔的字符串: - -```typescript -// API 可能返回数组 -{ labels: ['标签1', '标签2'] } -// 或字符串 -{ labels: '标签1,标签2' } - -// syncSessionFromDetail 会统一转换为数组保存到数据库 -``` - -### 4. 移除旧的手动更新代码 - -在实施新方案后,可以移除以下旧代码: - -```typescript -// ❌ 旧代码(手动更新 Store) -await sessionStore.updateSessionRemark(contactId, newRemark) - -// ❌ 旧代码(手动查找会话并更新) -const session = sessions.find((s) => s.id === contactId) -if (session) { - sessionStore.updateSession(contactId, { conRemark: newRemark }) -} - -// ✅ 新代码(只需同步到数据库,自动触发更新) -const detail = await getFriendDetail({ id: contactId }) -await SessionManager.syncSessionFromFriendDetail(contactId, detail.detail || detail) -``` - -## 🚀 未来优化方向 - -### 1. 批量同步 - -如果需要批量修改多个联系人的信息,可以扩展 `SessionManager`: - -```typescript -static async syncBatchFromDetails( - details: Array<{ id: number; type: 'friend' | 'group'; data: any }> -): Promise -``` - -### 2. 冲突检测 - -如果本地修改和服务器修改冲突,可以添加冲突检测: - -```typescript -static async syncSessionFromDetail( - detail: any, - sessionId: number, - sessionType: 'friend' | 'group', - options?: { conflictResolution: 'server' | 'local' | 'merge' } -): Promise -``` - -### 3. 差异日志 - -记录每次同步的差异,便于调试: - -```typescript -console.log(`[SessionManager] 同步差异:`, { - sessionId, - changes: updates, - timestamp: new Date().toISOString(), -}) -``` - -## ✅ 总结 - -通过实施统一的数据联动更新机制,我们实现了: - -1. ✅ **数据一致性**:所有 UI 位置显示的数据保持同步 -2. ✅ **代码简洁**:统一的数据同步逻辑,易于维护 -3. ✅ **自动化**:数据库变更自动触发 UI 更新 -4. ✅ **可扩展**:新增修改入口只需调用统一方法 - -**核心思路**:API + 详情 + 数据库 → 订阅回调 → UI 自动更新 - -这与旧项目的实现思路一致,但利用了新项目的技术特性(IndexedDB + 订阅机制),实现了更优雅的方案。 diff --git a/TouchVueThree/文档清理建议.md b/TouchVueThree/文档清理建议.md deleted file mode 100644 index 53bfeef..0000000 --- a/TouchVueThree/文档清理建议.md +++ /dev/null @@ -1,247 +0,0 @@ -# 文档清理建议 - -## 📋 文档状态分析 - -### ✅ 建议保留的核心文档(15个) - -#### 1. 项目总览类 -- **`改造完成报告.md`** - 项目整体改造总结,包含性能对比和验收清单 -- **`开发日志.md`** - 持续更新的开发日志和踩坑记录 -- **`CHANGELOG.md`** - 变更日志,记录版本变化 - -#### 2. 系统架构类 -- **`聊天系统改造方案.md`** - 完整的技术方案,3900行,作为架构参考 -- **`本地缓存策略与微信对比分析.md`** - 缓存策略对比,有参考价值 -- **`会话列表同步清理策略.md`** - 重要的同步策略文档 - -#### 3. 功能说明类 -- **`个人资料卡片功能分析.md`** - 详细的功能分析,430行 -- **`客服在线状态功能说明.md`** - 功能说明 -- **`content数据实例.md`** - 数据结构示例 - -#### 4. API接口类 -- **`API接口说明.md`** - API接口文档 -- **`API接口修正说明.md`** - 接口修正记录 - -#### 5. 实施总结类(保留最终版本) -- **`登录过期自动退出实施总结.md`** - ✅ 已完成,保留 -- **`数据联动更新实施总结.md`** - ✅ 已完成,保留 -- **`联系人列表迁移完成报告.md`** - ✅ 已完成,保留 -- **`会话列表排序优化实施总结.md`** - ✅ 已完成,保留(二选一) - ---- - -### 🗑️ 建议删除的文档(14个) - -#### 1. 过程文档(开发中的临时文档) - -**聊天窗口迁移相关(5个)**: -- ❌ `聊天窗口迁移规划.md` - 规划阶段文档,已完成 -- ❌ `聊天窗口迁移进度.md` - 进度跟踪文档,已完成 -- ❌ `聊天窗口迁移快速指南.md` - 临时指南,已被其他文档覆盖 -- ❌ `聊天窗口完善进度.md` - 进度文档,已完成 -- ❌ `聊天窗口架构优化方案.md` - 方案文档,已被聊天系统改造方案覆盖 - -**原因**:这些都是开发过程中的临时文档,功能已经完成并有最终的总结文档 - ---- - -#### 2. 策略/方案文档(已有实施总结) - -**登录过期相关(1个)**: -- ❌ `登录过期自动退出机制.md` - 方案文档,已有实施总结 - -**数据联动相关(1个)**: -- ❌ `数据联动更新策略.md` - 策略文档,已有实施总结 - -**原因**:策略和方案文档在开发完成后,实施总结已经包含了关键信息 - ---- - -#### 3. 检查/分析文档(已修复完成) - -**个人资料相关(2个)**: -- ❌ `个人资料功能迁移检查报告.md` - 检查报告,问题已修复 -- ❌ `个人资料功能修复总结.md` - 修复总结,功能已稳定 - -**原因**:这些是发现问题和修复问题的过程文档,现在功能已经稳定 - ---- - -#### 4. 会话列表排序相关(重复和过程文档,5个) - -**分析类(可删除3个)**: -- ❌ `会话列表排序规则分析.md` - 旧项目分析,已完成对比 -- ❌ `新项目会话列表排序规则.md` - 新项目分析,已完成对比 -- ❌ `会话列表排序规则对比分析.md` - 对比分析,已完成优化 - -**方案/说明类(可删除2个)**: -- ❌ `会话列表排序优化方案.md` - 优化方案,已实施 -- ❌ `会话列表排序优化说明.md` - 优化说明,已有实施总结 - -**测试/索引类(可删除2个)**: -- ❌ `会话列表优化测试指南.md` - 测试指南,功能已稳定 -- ❌ `会话列表优化文档索引.md` - 文档索引,文档清理后不需要 - -**实施总结类(二选一删除1个)**: -- ⚠️ `会话列表优化实施总结.md` - 与 `会话列表排序优化实施总结.md` 重复,删除其中一个 - -**原因**:会话列表排序优化已经完成,这些都是开发过程中的分析、方案、测试文档 - ---- - -### 🔄 建议合并的文档(2组) - -#### 组1:聊天系统改造 -- 保留:`聊天系统改造方案.md`(3900行,最完整) -- 删除:`聊天系统改造实施说明.md`(内容已被改造方案覆盖) - -#### 组2:会话列表优化实施总结 -- 保留:`会话列表排序优化实施总结.md`(更聚焦排序优化) -- 删除:`会话列表优化实施总结.md`(内容重复) - ---- - -## 📊 删除前后对比 - -| 类型 | 删除前 | 删除后 | 说明 | -|------|--------|--------|------| -| **总文档数** | 33个 | 15个 | 减少54% | -| **过程文档** | 14个 | 0个 | 已完成的临时文档 | -| **重复文档** | 4个 | 0个 | 合并或删除 | -| **核心文档** | 15个 | 15个 | 保留所有核心文档 | - ---- - -## 🎯 删除原则 - -### 1. 删除过程文档 -✅ 规划、进度、检查报告等开发过程中的文档 -✅ 功能已经稳定,不再需要追踪 - -### 2. 保留结果文档 -✅ 实施总结、完成报告 -✅ 功能说明、API文档 -✅ 架构方案、开发日志 - -### 3. 合并重复文档 -✅ 多个实施总结合并为一个 -✅ 方案和说明合并 - -### 4. 保留参考价值文档 -✅ 架构设计、对比分析 -✅ 技术方案、策略文档 -✅ API接口、数据结构 - ---- - -## 📝 执行步骤 - -### 步骤1:备份(可选) -```bash -# 创建备份文件夹 -mkdir TouchVueThree/docs_archive - -# 移动要删除的文档到备份文件夹 -mv TouchVueThree/聊天窗口*.md TouchVueThree/docs_archive/ -mv TouchVueThree/会话列表排序*.md TouchVueThree/docs_archive/ -# ... 其他文档 -``` - -### 步骤2:删除文档 -直接删除以下14个文档: - -```bash -# 聊天窗口相关(5个) -rm TouchVueThree/聊天窗口迁移规划.md -rm TouchVueThree/聊天窗口迁移进度.md -rm TouchVueThree/聊天窗口迁移快速指南.md -rm TouchVueThree/聊天窗口完善进度.md -rm TouchVueThree/聊天窗口架构优化方案.md - -# 策略/方案文档(2个) -rm TouchVueThree/登录过期自动退出机制.md -rm TouchVueThree/数据联动更新策略.md - -# 个人资料相关(2个) -rm TouchVueThree/个人资料功能迁移检查报告.md -rm TouchVueThree/个人资料功能修复总结.md - -# 会话列表排序相关(7个) -rm TouchVueThree/会话列表排序规则分析.md -rm TouchVueThree/新项目会话列表排序规则.md -rm TouchVueThree/会话列表排序规则对比分析.md -rm TouchVueThree/会话列表排序优化方案.md -rm TouchVueThree/会话列表排序优化说明.md -rm TouchVueThree/会话列表优化测试指南.md -rm TouchVueThree/会话列表优化文档索引.md - -# 重复文档(2个) -rm TouchVueThree/聊天系统改造实施说明.md -rm TouchVueThree/会话列表优化实施总结.md -``` - -### 步骤3:更新文档索引 -在 `README.md` 中更新文档列表,只保留15个核心文档 - ---- - -## ✅ 清理后的文档结构 - -``` -TouchVueThree/ -├── 📘 项目总览 -│ ├── 改造完成报告.md -│ ├── 开发日志.md -│ └── CHANGELOG.md -│ -├── 📐 架构设计 -│ ├── 聊天系统改造方案.md -│ ├── 本地缓存策略与微信对比分析.md -│ └── 会话列表同步清理策略.md -│ -├── 📋 功能说明 -│ ├── 个人资料卡片功能分析.md -│ ├── 客服在线状态功能说明.md -│ └── content数据实例.md -│ -├── 🔌 API接口 -│ ├── API接口说明.md -│ └── API接口修正说明.md -│ -└── 📊 实施总结 - ├── 登录过期自动退出实施总结.md - ├── 数据联动更新实施总结.md - ├── 联系人列表迁移完成报告.md - └── 会话列表排序优化实施总结.md -``` - ---- - -## 🎉 清理后的优势 - -1. ✅ **文档更清晰**:只保留核心文档,易于查找 -2. ✅ **维护成本低**:减少54%的文档,降低维护负担 -3. ✅ **信息聚焦**:保留最重要的架构、API和实施总结 -4. ✅ **新人友好**:文档少而精,新人更容易上手 -5. ✅ **版本管理**:减少git仓库大小,提升clone速度 - ---- - -## ⚠️ 注意事项 - -1. **删除前备份**:如果担心丢失信息,先备份到 `docs_archive` 文件夹 -2. **保留git历史**:即使删除文件,git历史中仍然可以找回 -3. **更新README**:删除后记得更新项目README中的文档链接 -4. **团队沟通**:如果是团队项目,先和团队成员确认 - ---- - -## 📌 总结 - -**删除文档**:19个(包括重复和过程文档) -**保留文档**:15个(核心和参考文档) -**文档减少**:54% -**信息损失**:0%(所有关键信息已在保留的文档中) - -**建议**:立即执行删除,保持项目文档整洁高效! diff --git a/TouchVueThree/本地缓存策略与微信对比分析.md b/TouchVueThree/本地缓存策略与微信对比分析.md deleted file mode 100644 index 64c4234..0000000 --- a/TouchVueThree/本地缓存策略与微信对比分析.md +++ /dev/null @@ -1,379 +0,0 @@ -# 本地缓存策略与微信 PC 端对比分析 - -## 📋 概述 - -本文档对比分析我们的本地缓存实现与微信 PC 端的缓存策略,并从用户体验角度提供改进建议。 - -## 🎯 微信 PC 端的缓存特点 - -### 1. 缓存策略 - -**会话列表缓存**: -- ✅ **秒开体验**: 启动时立即显示本地缓存的会话列表 -- ✅ **后台同步**: 启动后在后台同步最新数据 -- ✅ **增量更新**: 只同步变更的会话,减少网络请求 -- ✅ **持久化存储**: 使用本地文件系统(SQLite)存储 - -**消息缓存**: -- ✅ **分页加载**: 按需加载历史消息 -- ✅ **本地优先**: 优先从本地读取,找不到再请求服务器 -- ✅ **智能清理**: 自动清理超过 30 天的消息 -- ✅ **媒体文件缓存**: 图片、视频等媒体文件单独缓存 - -### 2. 用户体验特点 - -**启动速度**: -- ⚡ 启动即可看到会话列表(< 500ms) -- ⚡ 消息记录立即可用 -- ⚡ 无明显的"加载中"状态 - -**响应速度**: -- ⚡ 点击会话立即显示消息 -- ⚡ 发送消息立即显示(乐观更新) -- ⚡ 收到新消息立即更新 - -**数据一致性**: -- ✅ 后台静默同步,用户无感知 -- ✅ 冲突自动解决(以服务器为准) -- ✅ 网络恢复时自动补齐缺失数据 - -## 🔍 我们的实现对比 - -### 1. 已实现的功能 - -| 功能 | 微信 PC 端 | 我们的实现 | 状态 | -|------|-----------|-----------|------| -| **会话列表本地缓存** | ✅ SQLite | ✅ IndexedDB | ✅ 已实现 | -| **启动时优先读取缓存** | ✅ | ✅ | ✅ 已实现 | -| **后台同步服务器数据** | ✅ | ✅ | ✅ 已实现 | -| **数据库层面排序** | ✅ | ✅ | ✅ 已实现 | -| **sortKey 机制** | ✅ | ✅ | ✅ 已实现 | -| **订阅机制** | ✅ | ✅ | ✅ 已实现 | -| **一号一库隔离** | ✅ | ✅ | ✅ 已实现 | -| **消息本地缓存** | ✅ | ✅ | ✅ 已实现 | -| **分页加载历史消息** | ✅ | ✅ | ✅ 已实现 | -| **乐观更新** | ✅ | ✅ | ✅ 已实现 | - -### 2. 存在的差距 - -| 功能 | 微信 PC 端 | 我们的实现 | 状态 | -|------|-----------|-----------|------| -| **增量同步** | ✅ 只同步变更 | ⚠️ 全量同步 | ⚠️ 待优化 | -| **智能清理** | ✅ 自动清理旧数据 | ⚠️ 手动清理 | ⚠️ 待优化 | -| **离线模式** | ✅ 完整离线可用 | ⚠️ 部分可用 | ⚠️ 待优化 | -| **媒体文件缓存** | ✅ 单独缓存 | ⚠️ 未实现 | ❌ 未实现 | -| **搜索索引** | ✅ 全文搜索 | ⚠️ 基础搜索 | ⚠️ 待优化 | -| **数据压缩** | ✅ 压缩存储 | ❌ 未压缩 | ❌ 未实现 | -| **配额管理** | ✅ 动态调整 | ⚠️ 基础检查 | ⚠️ 待优化 | - -## 🎨 用户体验对比 - -### 1. 启动速度 - -**微信 PC 端**: -``` -启动 → 立即显示会话列表(缓存)→ 后台同步 → 无缝更新 -耗时: < 500ms -``` - -**我们的实现**: -``` -启动 → 初始化数据库 → 读取缓存 → 显示会话列表 → 后台同步 → 更新 -耗时: ~1000ms -``` - -**差距分析**: -- ⚠️ 初始化时间稍长(IndexedDB 打开需要时间) -- ⚠️ 首次加载需要从服务器获取数据 -- ✅ 后续启动速度接近微信 - -### 2. 响应速度 - -**微信 PC 端**: -``` -点击会话 → 立即显示本地消息 → 后台加载最新消息 → 无缝插入 -耗时: < 100ms -``` - -**我们的实现**: -``` -点击会话 → 查询本地消息 → 显示 → 后台加载最新消息 → 更新 -耗时: ~300ms -``` - -**差距分析**: -- ⚠️ 查询速度略慢(IndexedDB 查询性能) -- ⚠️ 渲染优化空间(虚拟滚动) -- ✅ 整体体验可接受 - -### 3. 发送消息 - -**微信 PC 端**: -``` -发送 → 立即显示(乐观更新)→ 后台发送 → 更新状态 -耗时: < 50ms -``` - -**我们的实现**: -``` -发送 → 立即显示(乐观更新)→ WebSocket 发送 → 更新状态 -耗时: ~100ms -``` - -**差距分析**: -- ✅ 乐观更新已实现 -- ✅ 状态更新及时 -- ✅ 整体体验接近微信 - -### 4. 数据同步 - -**微信 PC 端**: -``` -增量同步 → 只获取变更数据 → 本地合并 → 无感知更新 -网络流量: 很小(只同步变更) -``` - -**我们的实现**: -``` -全量同步 → 分页获取所有数据 → 本地更新 → 显示同步状态 -网络流量: 较大(全量同步) -``` - -**差距分析**: -- ⚠️ 网络流量较大 -- ⚠️ 同步时间较长 -- ⚠️ 用户可感知(同步提示) - -## 📊 性能对比 - -### 1. 启动性能 - -| 指标 | 微信 PC 端 | 我们的实现 | 差距 | -|------|-----------|-----------|------| -| **首次启动** | ~500ms | ~1000ms | 2x | -| **后续启动** | ~200ms | ~500ms | 2.5x | -| **会话列表显示** | < 100ms | ~300ms | 3x | -| **消息列表显示** | < 50ms | ~200ms | 4x | - -### 2. 内存占用 - -| 指标 | 微信 PC 端 | 我们的实现 | 对比 | -|------|-----------|-----------|------| -| **基础内存** | ~150MB | ~80MB | ✅ 更低 | -| **1000 会话** | ~200MB | ~120MB | ✅ 更低 | -| **10000 消息** | ~250MB | ~150MB | ✅ 更低 | - -**说明**: 浏览器环境内存占用通常低于桌面应用 - -### 3. 存储空间 - -| 指标 | 微信 PC 端 | 我们的实现 | 对比 | -|------|-----------|-----------|------| -| **会话数据** | ~5MB / 1000 会话 | ~3MB / 1000 会话 | ✅ 更优 | -| **消息数据** | ~50MB / 10000 消息 | ~40MB / 10000 消息 | ✅ 更优 | -| **媒体文件** | 独立缓存 | 未缓存 | ❌ 缺失 | - -## 🚀 改进建议 - -### 1. 高优先级优化 - -#### 1.1 增量同步机制 - -**目标**: 减少网络流量和同步时间 - -**实现方案**: -```typescript -/** - * 增量同步会话列表 - * 只同步最后一次同步后的变更数据 - */ -const syncSessions = async (accountId: number) => { - // 1. 获取最后同步时间 - const lastSyncTime = await getLastSyncTime(accountId) - - // 2. 请求增量数据(只获取变更) - const changes = await getSessionChanges({ - accountId, - since: lastSyncTime, - }) - - // 3. 合并本地数据 - await mergeSessionChanges(changes) - - // 4. 更新最后同步时间 - await updateLastSyncTime(accountId, Date.now()) -} -``` - -**预期效果**: -- ✅ 网络流量减少 90%+ -- ✅ 同步时间减少 80%+ -- ✅ 用户体验提升明显 - -#### 1.2 启动性能优化 - -**目标**: 减少启动时间,接近微信体验 - -**实现方案**: -```typescript -/** - * 优化启动流程 - */ -const optimizedInit = async (accountId: number) => { - // 1. 并行初始化数据库和加载缓存 - const [db, cachedSessions] = await Promise.all([ - databaseManager.ensureDatabase(userId), - SessionManager.getUserSessions(accountId), - ]) - - // 2. 立即显示缓存数据 - sessions.value = cachedSessions - initialLoading.value = false - - // 3. 后台同步(不阻塞 UI) - syncSessionsInBackground(accountId) -} -``` - -**预期效果**: -- ✅ 启动时间减少 50% -- ✅ 首屏显示更快 -- ✅ 用户体验显著提升 - -#### 1.3 智能清理机制 - -**目标**: 自动清理旧数据,减少存储占用 - -**实现方案**: -```typescript -/** - * 自动清理策略 - */ -const autoCleanup = async () => { - // 1. 清理 30 天前的消息 - await cleanOldMessages(30) - - // 2. 清理孤儿数据 - await cleanOrphanData() - - // 3. 压缩数据库(如果需要) - await compactDatabase() -} - -// 定时执行(每天一次) -setInterval(autoCleanup, 24 * 60 * 60 * 1000) -``` - -**预期效果**: -- ✅ 存储占用减少 60%+ -- ✅ 查询性能提升 30%+ -- ✅ 用户无感知 - -### 2. 中优先级优化 - -#### 2.1 搜索性能优化 - -**目标**: 提升搜索速度和准确性 - -**实现方案**: -- 建立全文搜索索引 -- 使用拼音首字母搜索 -- 支持模糊匹配 - -#### 2.2 离线模式增强 - -**目标**: 完整的离线功能 - -**实现方案**: -- 离线状态检测 -- 离线队列管理 -- 网络恢复时自动重试 - -#### 2.3 媒体文件缓存 - -**目标**: 减少重复加载媒体文件 - -**实现方案**: -- 使用 Cache API 缓存媒体文件 -- LRU 淘汰策略 -- 配额管理 - -### 3. 低优先级优化 - -#### 3.1 数据压缩 - -**目标**: 减少存储空间占用 - -**实现方案**: -- 压缩消息内容 -- 压缩扩展字段 - -#### 3.2 预加载优化 - -**目标**: 提前加载可能需要的数据 - -**实现方案**: -- 预加载附近会话的消息 -- 预加载常用联系人 - -## 📝 用户体验提升总结 - -### 当前实现优势 - -1. ✅ **核心功能完整**: 会话列表缓存、消息缓存、排序机制等核心功能完整 -2. ✅ **响应式更新**: 利用 Vue 3 + Pinia,UI 更新及时 -3. ✅ **数据隔离**: 一号一库,数据安全 -4. ✅ **性能可接受**: 对于中小规模数据(< 1000 会话),性能良好 - -### 与微信的差距 - -1. ⚠️ **启动速度**: 比微信慢 2-3 倍(但仍在可接受范围内) -2. ⚠️ **网络流量**: 全量同步导致流量较大 -3. ⚠️ **离线体验**: 离线模式不够完善 -4. ❌ **媒体缓存**: 缺少媒体文件缓存 - -### 改进优先级 - -**立即改进** (2-3 天): -1. 增量同步机制 -2. 启动性能优化 -3. 智能清理机制 - -**短期改进** (1-2 周): -1. 搜索性能优化 -2. 离线模式增强 -3. 错误处理优化 - -**长期规划** (1-2 月): -1. 媒体文件缓存 -2. 数据压缩 -3. 预加载优化 - -## 🎯 结论 - -### 整体评价 - -我们的实现 **已经达到了可用的水平**,核心功能与微信 PC 端相似: - -- ✅ 本地缓存机制完整 -- ✅ 数据库层面排序 -- ✅ 响应式更新及时 -- ✅ 乐观更新流畅 - -### 用户体验评分 - -| 维度 | 微信 PC 端 | 我们的实现 | 评分 | -|------|-----------|-----------|------| -| **启动速度** | 10/10 | 7/10 | 70% | -| **响应速度** | 10/10 | 8/10 | 80% | -| **流畅度** | 10/10 | 9/10 | 90% | -| **稳定性** | 10/10 | 8/10 | 80% | -| **整体体验** | 10/10 | 8/10 | 80% | - -### 推荐行动 - -1. **立即实施**: 增量同步、启动优化、智能清理 -2. **持续改进**: 根据用户反馈持续优化 -3. **监控指标**: 建立性能监控,跟踪关键指标 - -**总结**: 我们的实现已经非常接近微信 PC 端的用户体验,通过上述优化可以进一步缩小差距,达到企业级应用的标准。 diff --git a/TouchVueThree/登录过期自动退出实施总结.md b/TouchVueThree/登录过期自动退出实施总结.md deleted file mode 100644 index 657c141..0000000 --- a/TouchVueThree/登录过期自动退出实施总结.md +++ /dev/null @@ -1,489 +0,0 @@ -# 登录过期自动退出实施总结 - -## ✅ 已完成的工作 - -### 1. 创建统一的认证工具函数 - -**文件**: `TouchVueThree/src/utils/auth.ts`(新建) - -#### 核心函数 - -```typescript -/** - * 统一的登录过期处理(401 错误) - */ -export async function handleLoginExpired() - -/** - * 手动退出登录 - */ -export async function handleManualLogout() - -/** - * 检查登录状态 - */ -export function isLoggedIn(): boolean - -/** - * 获取当前用户信息 - */ -export function getCurrentUser() -``` - -#### 关键特性 - -- ✅ **防重复处理**:使用 `isHandlingLogout` 标志位避免多个 401 错误同时触发 -- ✅ **完整清理**:调用 `userStore.logout()` 关闭所有资源 -- ✅ **友好提示**:显示"登录已过期,请重新登录" -- ✅ **路径保存**:保存当前路径到 `redirect` 参数,重新登录后跳转回原页面 - -### 2. 修改 useUserStore.logout() - -**文件**: `TouchVueThree/src/stores/modules/user.ts` - -#### 修改内容 - -```typescript -/** - * 退出登录 - * - * @param skipRouterPush 是否跳过路由跳转(用于统一的 handleLoginExpired 中) - */ -const logout = async (skipRouterPush = false) => { - console.log('[UserStore] 🚪 开始退出登录') - - try { - // ⭐ 1. 关闭 WebSocket 连接 - try { - const { useWebSocket } = await import('@/composables/business/wechat') - const { disconnect } = useWebSocket() - disconnect() - console.log('[UserStore] ✅ WebSocket 已关闭') - } catch (error) { - console.error('[UserStore] ❌ 关闭 WebSocket 失败:', error) - } - - // ⭐ 2. 关闭数据库连接 - try { - await databaseManager.closeCurrentDatabase() - console.log('[UserStore] ✅ 数据库已关闭') - } catch (error) { - console.error('[UserStore] ❌ 关闭数据库失败:', error) - } - } catch (error) { - console.error('[UserStore] ❌ 关闭资源失败:', error) - } - - // ⭐ 3. 清除本地存储 - localStorage.removeItem('token') - localStorage.removeItem('token2') - localStorage.removeItem('user-store') - - // ⭐ 4. 清除状态 - user.value = null - token.value = '' - token2.value = '' - - // ⭐ 5. 跳转到登录页(可选) - if (!skipRouterPush) { - router.push('/login') - } - - console.log('[UserStore] ✅ 退出登录完成') -} -``` - -#### 新增功能 - -1. ✅ **关闭 WebSocket**:调用 `useWebSocket().disconnect()` -2. ✅ **关闭数据库**:调用 `databaseManager.closeCurrentDatabase()` -3. ✅ **清除缓存**:移除 `token`、`token2`、`user-store` -4. ✅ **清除状态**:重置 Pinia Store 状态 -5. ✅ **可选跳转**:支持 `skipRouterPush` 参数,避免重复跳转 - -#### 动态导入避免循环依赖 - -```typescript -// ⭐ 使用动态导入避免循环依赖 -const { useWebSocket } = await import('@/composables/business/wechat') -``` - -### 3. 修改 request.ts - -**文件**: `TouchVueThree/src/api/request.ts` - -#### 修改内容 - -```typescript -// 导入统一的认证工具 -import { handleLoginExpired } from '@/utils/auth' - -// 业务失败处理 -if (code === 401) { - handleLoginExpired() // ⭐ 使用统一的处理函数 - return Promise.reject(new Error(msg || message || '登录已过期')) -} - -// HTTP 状态码错误 -switch (status) { - case 401: - handleLoginExpired() // ⭐ 使用统一的处理函数 - break - // ... -} -``` - -#### 移除的代码 - -```typescript -// ❌ 删除了原有的 handleUnauthorized 函数 -// ❌ 删除了 isUnauthorizedHandling 标志位 -// ❌ 删除了 router 导入(不再需要) -``` - -### 4. 修改 request2.ts - -**文件**: `TouchVueThree/src/api/request2.ts` - -#### 修改内容 - -```typescript -// 导入统一的认证工具 -import { handleLoginExpired } from '@/utils/auth' - -// 处理401错误,统一的登录过期处理 -if (err.response && err.response.status === 401) { - handleLoginExpired() // ⭐ 使用统一的处理函数 - return Promise.reject(err) -} -``` - -#### 移除的代码 - -```typescript -// ❌ 删除了原有的手动跳转逻辑 -// ❌ 删除了 router 导入(不再需要) -``` - -## 📊 完整流程图 - -``` -用户请求 API - ↓ -服务器返回 401 - ↓ -Axios 拦截器捕获 401 - ↓ -调用 handleLoginExpired() - ↓ -┌─────────────────────────────────────┐ -│ 防重复处理检查 │ -│ if (isHandlingLogout) return │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 调用 userStore.logout(true) │ -│ - skipRouterPush = true │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 1. 关闭 WebSocket 连接 │ -│ - disconnect() │ -│ - 清理心跳定时器 │ -│ - 清理重连定时器 │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 2. 关闭 IndexedDB 数据库 │ -│ - closeCurrentDatabase() │ -│ - 关闭数据库连接 │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 3. 清除登录缓存 │ -│ - localStorage: token │ -│ - localStorage: token2 │ -│ - localStorage: user-store │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 4. 清除 Pinia Store 状态 │ -│ - user.value = null │ -│ - token.value = '' │ -│ - token2.value = '' │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 5. 显示提示消息 │ -│ "登录已过期,请重新登录" │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 6. 跳转到登录页 │ -│ - 保存当前路径到 redirect │ -│ - router.push('/login') │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 7. 重置防重复标志位 │ -│ isHandlingLogout = false │ -└─────────────────────────────────────┘ -``` - -## 🎯 核心优势 - -### 1. 代码统一 - -- ✅ `request.ts` 和 `request2.ts` 使用同一套逻辑 -- ✅ 所有 401 错误都通过 `handleLoginExpired()` 处理 -- ✅ 易于维护和扩展 - -### 2. 资源清理完整 - -- ✅ WebSocket 连接关闭 -- ✅ IndexedDB 数据库关闭 -- ✅ localStorage 缓存清除 -- ✅ Pinia Store 状态重置 - -### 3. 防止重复处理 - -- ✅ 全局标志位 `isHandlingLogout` -- ✅ 避免多个 401 错误同时触发 -- ✅ 确保只执行一次清理逻辑 - -### 4. 用户体验好 - -- ✅ 自动退出,无需用户操作 -- ✅ 友好提示:"登录已过期,请重新登录" -- ✅ 重新登录后跳转回原页面 -- ✅ 无残留连接或数据库锁 - -## 📝 使用示例 - -### 示例 1:API 请求触发 401 - -```typescript -// 用户发起 API 请求 -const response = await request('/api/user/info') - -// 服务器返回 401 -// ↓ -// Axios 拦截器自动捕获 -// ↓ -// 调用 handleLoginExpired() -// ↓ -// 自动退出登录,清理所有资源 -// ↓ -// 跳转到登录页 -``` - -### 示例 2:用户手动退出登录 - -```typescript -// 用户点击"退出登录"按钮 -import { handleManualLogout } from '@/utils/auth' - -const handleLogout = async () => { - await handleManualLogout() - // ✅ 显示"已退出登录" - // ✅ 自动跳转到登录页 -} -``` - -### 示例 3:MainLayout 中的退出登录 - -```typescript -// TouchVueThree/src/layouts/MainLayout.vue - -import { handleManualLogout } from '@/utils/auth' - -const handleUserCommand = async (command: string) => { - switch (command) { - case 'logout': - await handleManualLogout() - break - // ... - } -} -``` - -## ⚠️ 注意事项 - -### 1. 动态导入避免循环依赖 - -由于 `useUserStore` 和 `useWebSocket` 可能存在循环依赖,使用动态导入: - -```typescript -// ✅ 正确:动态导入 -const { useWebSocket } = await import('@/composables/business/wechat') - -// ❌ 错误:直接导入可能导致循环依赖 -import { useWebSocket } from '@/composables/business/wechat' -``` - -### 2. skipRouterPush 参数 - -`logout()` 方法支持 `skipRouterPush` 参数,避免重复跳转: - -```typescript -// handleLoginExpired 中: -await userStore.logout(true) // 跳过路由跳转 -router.push({ path: '/login', query: { redirect: currentPath } }) - -// 手动退出登录: -await userStore.logout() // 自动跳转到登录页 -``` - -### 3. 防重复处理 - -使用全局标志位 `isHandlingLogout` 防止重复处理: - -```typescript -let isHandlingLogout = false - -export async function handleLoginExpired() { - if (isHandlingLogout) { - console.warn('[Auth] 正在处理登录过期,跳过重复调用') - return - } - - isHandlingLogout = true - - try { - // ...处理逻辑 - } finally { - setTimeout(() => { - isHandlingLogout = false - }, 500) - } -} -``` - -### 4. 错误处理 - -所有资源关闭操作都有独立的 try-catch,确保一个失败不影响其他: - -```typescript -try { - // 关闭 WebSocket -} catch (error) { - console.error('关闭 WebSocket 失败:', error) -} - -try { - // 关闭数据库 -} catch (error) { - console.error('关闭数据库失败:', error) -} -``` - -## 🚀 扩展优化(未来) - -### 1. Token 自动刷新 - -```typescript -// 在 token 即将过期前自动刷新 -export async function refreshTokenIfNeeded() { - const userStore = useUserStore() - const tokenExpireTime = localStorage.getItem('tokenExpireTime') - - if (tokenExpireTime) { - const expireTime = parseInt(tokenExpireTime) - const now = Date.now() - - // 提前 5 分钟刷新 - if (expireTime - now < 5 * 60 * 1000) { - try { - await userStore.refreshToken() - } catch (error) { - await handleLoginExpired() - } - } - } -} -``` - -### 2. 多标签页同步 - -```typescript -// 监听 localStorage 变化,同步多标签页登录状态 -window.addEventListener('storage', (e) => { - if (e.key === 'token' && !e.newValue) { - // token 被清除,其他标签页也退出 - handleLoginExpired() - } -}) -``` - -### 3. 退出前确认 - -```typescript -export async function handleManualLogoutWithConfirm() { - await ElMessageBox.confirm( - '确定要退出登录吗?', - '退出登录', - { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - } - ) - - await handleManualLogout() -} -``` - -## ✅ 测试验证 - -### 测试场景 - -1. ✅ **模拟 token 过期** - - 修改 localStorage 中的 token 为无效值 - - 发起 API 请求 - - 验证是否自动退出登录 - -2. ✅ **手动退出登录** - - 点击"退出登录"按钮 - - 验证是否正确清理所有资源 - -3. ✅ **重新登录后跳转** - - 在非登录页触发 401 - - 重新登录 - - 验证是否跳转回原页面 - -4. ✅ **多个 401 错误** - - 同时发起多个 API 请求 - - 服务器返回多个 401 - - 验证是否只处理一次 - -5. ✅ **WebSocket 关闭** - - 触发 401 错误 - - 验证 WebSocket 连接是否关闭 - - 验证心跳定时器是否清理 - -6. ✅ **数据库关闭** - - 触发 401 错误 - - 验证 IndexedDB 连接是否关闭 - - 验证是否无数据库锁残留 - -## 📊 预期效果 - -实施后,当用户登录过期时: - -1. ✅ **自动退出**:无需用户操作,系统自动处理 -2. ✅ **清理干净**:WebSocket + 数据库 + 缓存 + Store 全部清除 -3. ✅ **提示友好**:显示"登录已过期,请重新登录" -4. ✅ **体验流畅**:重新登录后跳转回原页面 -5. ✅ **无副作用**:不会残留连接或数据库锁 -6. ✅ **防重复**:多个 401 错误只处理一次 - -## 📝 总结 - -通过实施统一的登录过期自动退出机制,我们实现了: - -- ✅ **代码统一**:`request.ts` 和 `request2.ts` 使用同一套逻辑 -- ✅ **资源清理完整**:WebSocket + 数据库 + 缓存 + Store -- ✅ **防止重复处理**:全局标志位避免多次触发 -- ✅ **用户体验好**:自动退出 + 友好提示 + 重新登录后跳转回原页面 - -**核心原则**:API 401 → handleLoginExpired() → logout() → 清理所有资源 → 跳转登录页 diff --git a/TouchVueThree/聊天系统改造方案.md b/TouchVueThree/聊天系统改造方案.md deleted file mode 100644 index 0425add..0000000 --- a/TouchVueThree/聊天系统改造方案.md +++ /dev/null @@ -1,3963 +0,0 @@ -# 📋 TouchVueThree 聊天系统架构方案 - -> **方案版本**: v1.0 -> **创建日期**: 2026-01-13 -> **技术架构**: Vue 3 + Dexie + WebSocket + Pinia -> **项目定位**: 企业级客服聊天系统(Web版) - ---- - -## 目录 - -- [一、整体架构设计](#一整体架构设计) -- [二、数据库设计](#二数据库设计) -- [三、核心模块设计](#三核心模块设计) -- [四、数据同步策略](#四数据同步策略) -- [五、边缘场景处理](#五边缘场景处理) -- [六、消息存储策略](#六消息存储策略) -- [七、实施步骤](#七实施步骤) -- [七、关键代码清单](#七关键代码清单) -- [八、技术栈总结](#八技术栈总结) -- [九、注意事项](#九注意事项) -- [十、性能指标](#十性能指标) -- [十一、核心代码示例](#十一核心代码示例) -- [十二、架构对比分析](#十二架构对比分析) -- [十三、FAQ](#十三faq) -- [十四、参考资料](#十四参考资料) - ---- - -## 一、整体架构设计 - -### 1.1 数据流向 - -``` -┌─────────────────┐ -│ WebSocket │ ← 实时消息推送 -└────────┬────────┘ - ↓ -┌─────────────────┐ -│ Message Handler│ ← 消息处理中心 -└────────┬────────┘ - ↓ -┌─────────────────┐ -│ IndexedDB │ ← 本地持久化存储(多账户隔离) -│ (Dexie) │ -└────────┬────────┘ - ↓ -┌─────────────────┐ -│ Pinia Store │ ← 响应式状态管理 -└────────┬────────┘ - ↓ -┌─────────────────┐ -│ Vue Components │ ← UI 展示层 -└─────────────────┘ -``` - -### 1.2 核心特性 - -| 特性 | 说明 | 优势 | -| ------------------ | ---------------------- | ---------------------- | -| **多账户数据隔离** | 一号一数据库 | 彻底隔离,防止数据混乱 | -| **实时消息推送** | WebSocket + 事件订阅 | 毫秒级响应,无轮询开销 | -| **离线缓存** | IndexedDB 本地存储 | 秒开体验,支持离线查看 | -| **缓存优先策略** | 先显示缓存,后台同步 | 首屏加载 < 200ms | -| **防数据丢失** | 心跳 + 增量同步 + 事务 | 99.99% 消息不丢失 | -| **按需存储消息** | 限制数量和时间 | 控制存储空间 < 50MB | -| **虚拟滚动** | 只渲染可见消息 | 支持万级消息流畅滚动 | -| **智能预加载** | 预测用户行为 | 减少等待时间 | -| **动态会话创建** | 自动获取陌生好友详情 | 新好友消息无缝显示 | - -### 1.3 设计原则 - -``` -性能优先: -- 缓存优先,后台同步 -- 虚拟滚动,按需渲染 -- 懒加载,代码分割 - -用户体验: -- 骨架屏,消除白屏 -- 乐观更新,即时反馈 -- 错误降级,友好提示 - -数据安全: -- 多重备份,防止丢失 -- 增量同步,减少开销 -- 事务保证,数据一致 - -可维护性: -- 模块化设计,职责清晰 -- 类型安全,减少错误 -- 文档完善,易于交接 -``` - ---- - -## 二、数据库设计 - -### 2.1 多账户隔离方案(一号一库) - -```typescript -浏览器 IndexedDB: -├── ChatDatabase_123 (用户123) -│ ├── sessions → 会话列表 -│ ├── messages → 聊天记录(可选) -│ └── contacts → 联系人列表 -│ -├── ChatDatabase_456 (用户456) -│ ├── sessions -│ ├── messages -│ └── contacts -│ -└── ChatDatabase_789 (用户789) - └── ... -``` - -### 2.2 数据表结构 - -#### **sessions 表(会话列表)** - -```typescript -interface ChatSession { - id: number // 主键(好友ID或群ID) - serverId: string // 唯一标识: friend_123 / group_456 - type: 'friend' | 'group' // 类型 - wechatAccountId: number // 所属客服账号 - - // 联系人信息 - nickname: string - conRemark?: string // 备注名 - avatar: string - - // 消息信息 - content: string // 最新消息内容 - lastUpdateTime: string // 最后更新时间 - - // 配置 - config: { - unreadCount: number // 未读数 - top: boolean // 置顶 - msgTime: number // 消息时间戳 - } - - // 索引字段 - sortKey: string // 排序键 -} - -// 索引设计 -indexes: 'id, serverId, wechatAccountId, type, lastUpdateTime, sortKey' -``` - -#### **messages 表(聊天记录,可选)** - -```typescript -interface ChatMessage { - id: number // 主键(消息ID) - serverId: string // 服务器消息ID - sessionId: number // 所属会话 - sessionType: 'friend' | 'group' - wechatAccountId: number - - // 消息内容 - content: string - msgType: number // 1=文本, 3=图片, 34=语音... - direction: 'send' | 'receive' - - // 时间戳 - createTime: string - wechatTime: number - - // 状态 - status: 'sending' | 'success' | 'failed' - - // 扩展信息 - extra?: string // JSON 字符串 -} - -// 索引设计 -indexes: 'id, serverId, sessionId, [sessionId+createTime], createTime' - -// 存储限制 -- 每个会话最多 500 条消息 -- 保留最近 30 天消息 -- 超过限制自动清理 -``` - -#### **contacts 表(联系人列表)** - -```typescript -interface Contact { - id: number - serverId: string - type: 'friend' | 'group' - wechatAccountId: number - nickname: string - conRemark?: string - avatar: string - // ... 其他字段 -} - -// 索引设计 -indexes: 'id, serverId, wechatAccountId, type' -``` - ---- - -## 三、核心模块设计 - -### 3.1 数据库管理器 (`src/utils/db.ts`) - -```typescript -核心职责: -1. 管理多个数据库实例(一号一库) -2. 切换账户时自动切换数据库 -3. 提供统一的数据库访问接口 - -主要方法: -- ensureDatabase(userId) → 初始化/切换数据库 -- getCurrentDatabase() → 获取当前数据库 -- closeCurrentDatabase() → 关闭数据库 -- deleteUserDatabase(userId) → 删除用户数据库 -- listUserDatabases() → 列出所有数据库 -``` - -### 3.2 会话管理器 (`src/utils/dbManagers/SessionManager.ts`) - -```typescript -核心职责: -1. 会话数据的 CRUD 操作 -2. 数据变更的订阅/通知机制 -3. 批量同步服务器数据 - -主要方法: -- getUserSessions(accountId?) → 获取会话列表 -- upsertSession(session) → 添加/更新会话 -- syncSessions(sessions[]) → 批量同步 -- updateOnNewMessage(...) → WebSocket 消息更新 -- clearUnread(sessionId) → 清除未读 -- onUpdate(callback) → 订阅数据变更 -``` - -### 3.3 消息管理器 (`src/utils/dbManagers/MessageManager.ts`,可选) - -```typescript -核心职责: -1. 消息数据的存储和查询 -2. 自动清理旧消息 -3. 混合策略:缓存 + 实时加载 - -主要方法: -- getMessages(sessionId) → 获取消息列表 -- cacheMessages(messages[]) → 缓存消息 -- cleanOldMessages(sessionId) → 清理旧消息 -- addMessage(message) → 添加新消息 -``` - -### 3.4 WebSocket 管理 (`src/composables/business/wechat/useWebSocket.ts`) - -```typescript -核心职责: -1. WebSocket 连接管理 -2. 消息接收和分发 -3. 心跳检测和自动重连 - -主要方法: -- connect(url) → 建立连接 -- disconnect() → 断开连接 -- send(data) → 发送消息 -- onMessage(handler) → 消息监听 -- handleNewMessage(msg) → 处理新消息 -``` - -### 3.5 Pinia Store - -#### **useUserStore** (`src/stores/modules/user.ts`) - -```typescript -职责:用户认证和数据库初始化 - -关键逻辑: -- login() - → 调用登录接口 - → 初始化数据库: databaseManager.ensureDatabase(userId) - -- logout() - → 关闭数据库: databaseManager.closeCurrentDatabase() - → 清空状态 -``` - -#### **useSessionStore** (`src/stores/modules/wechat/useSessionStore.ts`) - -```typescript -职责:会话列表管理 - -关键逻辑: -- init(userId, accountId) - → 从 IndexedDB 读取缓存(立即显示) - → 订阅数据库变更(自动更新 UI) - → 后台同步服务器数据(分页加载) - -- switchAccount(accountId) - → 从 IndexedDB 重新读取(按账号过滤) - -- selectSession(session) - → 设置当前会话 - → 清除未读数 - -注意:❌ 不使用定时器轮询! -``` - -#### **useMessageStore** (`src/stores/modules/wechat/useMessageStore.ts`,可选) - -```typescript -职责:当前会话的消息列表管理 - -关键逻辑: -- loadMessages(sessionId) - → 从缓存读取(秒开) - → 后台从服务器刷新 - → 更新缓存 - -- loadMoreMessages(oldestId) - → 向上滚动加载更多 - -- addNewMessage(message) - → 插入到 UI - → 保存到缓存 - → 更新会话列表 -``` - ---- - -## 四、数据同步策略 - -### 4.1 初始加载(首次登录) - -``` -1. 用户登录 - ↓ -2. 初始化数据库: databaseManager.ensureDatabase(userId) - ↓ -3. 从 IndexedDB 读取缓存 → 立即显示(可能为空) - ↓ -4. 后台从服务器分页加载所有会话 - ├─ 第1页 → 保存到 IndexedDB → UI 更新 - ├─ 第2页 → 保存到 IndexedDB → UI 更新 - └─ 第N页 → 加载完成 - ↓ -5. 建立 WebSocket 连接 - ↓ -6. 订阅数据库变更(SessionManager.onUpdate) -``` - -### 4.2 账户切换 - -``` -1. 用户点击切换账户 - ↓ -2. 切换数据库: databaseManager.ensureDatabase(newUserId) - ↓ -3. 从 IndexedDB 读取缓存 → 立即显示 - ↓ -4. 后台增量同步(可选) -``` - -### 4.3 WebSocket 消息更新 - -``` -WebSocket 实时消息处理流程: - -1. WebSocket 收到新消息 - ↓ -2. 验证消息格式 - ├─ 有效 → 继续处理 - └─ 无效 → 记录错误,丢弃 - ↓ -3. 消息去重 - ├─ clientId 已存在 → 跳过(重复消息) - └─ clientId 不存在 → 继续 - ↓ -4. 检查会话是否存在 ⭐ 重要 - ├─ 会话存在 → 直接更新 - └─ 会话不存在 → 请求好友详情接口 - ↓ - 获取完整好友/群聊信息 - ├─ 昵称、头像、备注 - ├─ 群聊成员数 - └─ 其他必要信息 - ↓ - 创建新会话并保存到数据库 - ↓ -5. 更新 IndexedDB(事务) - ├─ 更新会话列表 - │ ├─ 最新消息内容 - │ ├─ 未读数 +1 - │ ├─ 时间戳 - │ └─ 排序键 - └─ 保存消息记录(可选) - ↓ -6. SessionManager 触发回调 - ├─ 通知所有订阅者 - └─ BroadcastChannel 同步多标签页 - ↓ -7. Store 自动更新 - ├─ 更新会话列表 UI - ├─ 更新未读数徽章 - └─ 播放提示音(如果需要) - ↓ -8. UI 自动刷新 - ├─ 会话列表重新排序 - ├─ 新会话出现在列表顶部 - ├─ 当前会话追加新消息 - └─ 显示桌面通知 -``` - -**代码实现**: - -```typescript -// WebSocket 消息处理 -ws.onmessage = async (event) => { - const rawData = JSON.parse(event.data) - - // 1. 验证消息格式 - if (!isValidMessage(rawData)) { - console.error('无效消息:', rawData) - Sentry.captureException(new Error('Invalid WebSocket message')) - return - } - - const message = parseMessage(rawData) - - // 2. 消息去重 - const isDuplicate = await MessageManager.checkDuplicate(message.clientId) - - if (isDuplicate) { - console.log('重复消息,跳过:', message.clientId) - return - } - - // 3. ⭐ 检查会话是否存在(关键步骤) - let session = await db().sessions.get(message.sessionId) - - if (!session) { - console.log('会话不存在,获取好友详情:', message.sessionId) - - try { - // 根据消息类型调用不同接口 - const contactInfo = - message.sessionType === 'friend' - ? await getFriendDetail({ friendId: message.sessionId }) - : await getGroupDetail({ groupId: message.sessionId }) - - // 创建新会话 - session = { - id: message.sessionId, - serverId: `${message.sessionType}_${message.sessionId}`, - type: message.sessionType, - wechatAccountId: message.wechatAccountId, - nickname: contactInfo.nickname, - conRemark: contactInfo.conRemark, - avatar: contactInfo.avatar, - content: message.content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: message.wechatTime, - }, - sortKey: `${message.wechatTime}_${message.sessionId}`, - } - - // 保存新会话到数据库 - await db().sessions.put(session) - - console.log('✅ 新会话已创建:', session.nickname) - } catch (error) { - console.error('获取好友详情失败:', error) - Sentry.captureException(error) - - // 降级方案:创建一个临时会话(包含基本信息) - session = { - id: message.sessionId, - serverId: `${message.sessionType}_${message.sessionId}`, - type: message.sessionType, - wechatAccountId: message.wechatAccountId, - nickname: message.nickname || '未知用户', - avatar: message.avatar || '/default-avatar.png', - content: message.content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: message.wechatTime, - }, - sortKey: `${message.wechatTime}_${message.sessionId}`, - } - - await db().sessions.put(session) - } - } - - // 4. 更新 IndexedDB(事务保证) - await db().transaction('rw', [db().sessions, db().messages], async () => { - // 更新会话 - await SessionManager.updateOnNewMessage( - message.sessionId, - message.sessionType, - message.content, - message.wechatAccountId - ) - - // 保存消息(可选) - if (shouldCacheMessage(message.sessionId)) { - await MessageManager.addMessage(message) - } - }) - - // 5. 触发回调(自动更新 UI) - // SessionManager 内部会触发 - - // 6. 多标签页同步 - broadcastChannel.postMessage({ - type: 'new_message', - sessionId: message.sessionId, - isNewSession: !session, // 标记是否为新会话 - }) - - // 7. 播放提示音 - if (message.sessionId !== currentSessionId.value) { - playNotificationSound() - } - - // 8. 桌面通知 - if (Notification.permission === 'granted') { - new Notification('新消息', { - body: message.content, - icon: session.avatar, - tag: `session_${message.sessionId}`, // 防止重复通知 - }) - } -} -``` - -### 4.4 ⭐ 关键场景:收到陌生好友/群聊消息 - -**问题描述**: -当 WebSocket 收到新消息时,如果发送者(好友或群聊)不在本地会话列表中(可能是新好友、新群聊、或还未同步),会导致消息无法正常显示。 - -**解决方案**:动态获取好友/群聊详情并创建新会话。 - -#### **完整处理流程** - -``` -收到 WebSocket 新消息 - ↓ -检查会话是否存在 - ├─ 存在 → 直接更新消息和未读数 - └─ 不存在 ⚠️ - ↓ - 1. 判断消息类型(好友 or 群聊) - ↓ - 2. 调用对应接口获取详情 - ├─ 好友消息 → getFriendDetail({ friendId }) - └─ 群聊消息 → getGroupDetail({ groupId }) - ↓ - 3. 获取完整信息 - ├─ 昵称、备注、头像 - ├─ 群聊成员数(群聊) - ├─ 微信账号ID - └─ 其他必要字段 - ↓ - 4. 创建新会话记录 - ├─ 填充完整联系人信息 - ├─ 设置初始未读数为 1 - ├─ 设置最新消息内容 - └─ 计算排序键 - ↓ - 5. 保存到 IndexedDB - await db().sessions.put(newSession) - ↓ - 6. 触发回调,更新 UI - ├─ 新会话出现在列表顶部 - ├─ 显示未读徽章 - └─ 播放提示音 -``` - -#### **代码实现(SessionManager)** - -```typescript -// src/utils/dbManagers/SessionManager.ts - -static async updateOnNewMessage( - sessionId: number, - sessionType: 'friend' | 'group', - content: string, - wechatAccountId?: number -) { - // 1. 检查会话是否存在 - const existing = await db().sessions.get(sessionId) - - if (existing) { - // 会话已存在,直接更新 - await db().sessions.update(sessionId, { - content, - lastUpdateTime: new Date().toISOString(), - config: { - ...existing.config, - msgTime: Date.now(), - unreadCount: (existing.config.unreadCount || 0) + 1, - }, - }) - await this.triggerCallbacks(wechatAccountId) - return - } - - // ⭐ 会话不存在 - 关键处理 - console.log(`会话 ${sessionId} 不存在,动态创建...`) - - try { - // 2. 根据类型调用不同接口 - const contactInfo = - sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 3. 创建新会话 - const newSession: ChatSession = { - id: sessionId, - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: wechatAccountId || 0, - nickname: contactInfo.nickname, - conRemark: contactInfo.conRemark, - avatar: contactInfo.avatar, - chatroomId: sessionType === 'group' ? contactInfo.chatroomId : undefined, - content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: Date.now(), - chat: true, - }, - sortKey: `${Date.now()}_${sessionId}`, - } - - // 4. 保存到数据库 - await db().sessions.put(newSession) - - console.log('✅ 新会话已创建:', contactInfo.nickname || sessionId) - - // 5. 触发回调,更新 UI - await this.triggerCallbacks(wechatAccountId) - } catch (error) { - // ⚠️ 接口失败 - 降级方案 - console.error('获取好友详情失败,使用降级方案:', error) - await this.createFallbackSession(sessionId, sessionType, content, wechatAccountId) - } -} - -// 降级方案:创建临时会话 -private static async createFallbackSession( - sessionId: number, - sessionType: 'friend' | 'group', - content: string, - wechatAccountId?: number -) { - const tempSession: ChatSession = { - id: sessionId, - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: wechatAccountId || 0, - nickname: '未知用户', - avatar: '/default-avatar.png', - content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: Date.now(), - chat: true, - }, - sortKey: `${Date.now()}_${sessionId}`, - } - - await db().sessions.put(tempSession) - await this.triggerCallbacks(wechatAccountId) - - // 后台继续重试(3次,指数退避:5s、10s、15s) - this.retryGetContactInfo(sessionId, sessionType, wechatAccountId) -} - -// 重试获取联系人信息 -private static async retryGetContactInfo( - sessionId: number, - sessionType: 'friend' | 'group', - wechatAccountId?: number -) { - let retryCount = 0 - const maxRetries = 3 - - while (retryCount < maxRetries) { - try { - // 指数退避:5秒、10秒、15秒 - await new Promise((resolve) => - setTimeout(resolve, 5000 * (retryCount + 1)) - ) - - const contactInfo = - sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 更新会话详情 - await db().sessions.update(sessionId, { - nickname: contactInfo.nickname, - conRemark: contactInfo.conRemark, - avatar: contactInfo.avatar, - }) - - console.log(`✅ 重试成功,已更新会话 ${sessionId} 的详细信息`) - await this.triggerCallbacks(wechatAccountId) - break // 成功则退出循环 - } catch (error) { - retryCount++ - console.warn( - `重试获取联系人信息失败 (${retryCount}/${maxRetries})`, - error - ) - - if (retryCount >= maxRetries) { - // 达到最大重试次数,记录到 Sentry - Sentry.captureException( - new Error(`无法获取会话${sessionId}的详细信息`), - { - extra: { sessionId, sessionType, retryCount }, - } - ) - } - } - } -} -``` - -#### **WebSocket 层面的处理** - -```typescript -// src/composables/business/wechat/useWebSocket.ts - -ws.onmessage = async (event) => { - const message = parseWebSocketMessage(event.data) - - // 1. 检查会话是否存在 - const session = await db().sessions.get(message.sessionId) - - if (!session) { - console.log('⚠️ 收到陌生好友/群聊消息,动态创建会话') - - // 2. 调用 SessionManager 处理(自动获取详情并创建) - await SessionManager.updateOnNewMessage( - message.sessionId, - message.sessionType, - message.content, - message.wechatAccountId - ) - - // 3. 保存消息记录(可选) - if (shouldCacheMessage(message.sessionId)) { - await MessageManager.addMessage(message) - } - - // 4. 通知其他标签页 - broadcastChannel.postMessage({ - type: 'new_session_created', - sessionId: message.sessionId, - }) - - // 5. 桌面通知 - if (Notification.permission === 'granted') { - new Notification('新消息', { - body: message.content, - icon: '/default-avatar.png', - tag: `new_session_${message.sessionId}`, - }) - } - } else { - // 会话已存在,正常更新 - await SessionManager.updateOnNewMessage( - message.sessionId, - message.sessionType, - message.content, - message.wechatAccountId - ) - } -} -``` - -#### **必需的 API 接口** - -#### **必需接口**: - -| 接口 | 路径 | 说明 | 调用时机 | -| ----------------- | -------------------- | -------------- | --------------------- | -| `getFriendDetail` | `/api/friend/detail` | 获取好友详情 | ⭐ 收到陌生好友消息时 | -| `getGroupDetail` | `/api/group/detail` | 获取群聊详情 | ⭐ 收到陌生群聊消息时 | -| `getSessionList` | `/api/session/list` | 获取会话列表 | 登录、切换账户 | -| `getContactList` | `/api/contact/list` | 获取联系人列表 | 联系人页面 | -| `getChatMessages` | `/api/message/list` | 获取聊天记录 | 打开会话、加载更多 | -| `sendMessage` | `/api/message/send` | 发送消息 | 用户发送 | -| `markAsRead` | `/api/message/read` | 标记已读 | 消息可见时 | - -**接口参数示例**: - -```typescript -// 获取好友详情(会话不存在时调用) -const friendInfo = await getFriendDetail({ - friendId: sessionId, -}) - -// 获取群聊详情(会话不存在时调用) -const groupInfo = await getGroupDetail({ - groupId: sessionId, -}) - -// 获取会话列表(初始加载) -const sessions = await getSessionList({ - wechatAccountId, - page: 1, - limit: 200, -}) - -// 获取消息 -const messages = await MessageManager.getMessages(sessionId, offset, limit) - -// 清理旧消息 -await MessageManager.cleanOldMessages(sessionId) -``` - -**返回数据格式**: - -```typescript -// getFriendDetail 返回 -interface FriendDetail { - id: number // 好友ID - wxId: string // 微信ID - nickname: string // 昵称 - conRemark?: string // 备注名 - avatar: string // 头像URL - wechatAccountId: number // 所属客服账号 - // ... 其他字段 -} - -// getGroupDetail 返回 -interface GroupDetail { - id: number // 群聊ID - chatroomId: string // 群聊ID - nickname: string // 群名称 - avatar: string // 群头像 - memberCount: number // 成员数 - wechatAccountId: number // 所属客服账号 - // ... 其他字段 -} -``` - -#### **用户体验时间线** - -**场景1:接口快速成功** - -``` -0ms 用户A收到陌生好友"张三"的消息 - ↓ - 检测会话不存在 - ↓ - 请求好友详情接口 - -100ms 接口返回成功 - ↓ - 创建新会话 - ↓ - 显示在列表顶部 - ↓ - 用户看到 "张三: 你好" -``` - -**场景2:接口失败 + 降级** - -``` -0ms 用户A收到陌生好友"张三"的消息 - ↓ - 检测会话不存在 - ↓ - 请求好友详情接口 - -3000ms 接口超时/失败 - ↓ - 创建临时会话 - ↓ - 显示在列表顶部 - ↓ - 用户看到 "未知用户: 你好" - -8000ms 后台重试成功 - ↓ - 更新会话信息 - ↓ - 用户看到 "张三: 你好" -``` - -#### **关键点总结** - -✅ **不会丢消息**:即使接口失败,也会创建临时会话显示消息 -✅ **用户无感知**:正常情况下 100-200ms 完成,用户无感知延迟 -✅ **自动恢复**:接口失败后自动重试(3次,指数退避) -✅ **错误监控**:重试失败后上报 Sentry,便于排查问题 -✅ **降级方案**:临时会话保证基本可用,后台继续获取详情 - ---- - -### 4.5 防数据丢失策略 - -#### **场景1:WebSocket 断线重连** - -```typescript -// 记录最后同步时间 -localStorage.setItem('lastSyncTime', Date.now()) - -// 重连后拉取遗漏消息 -const handleReconnected = async () => { - const lastSync = localStorage.getItem('lastSyncTime') - const missed = await getMessagesSince({ timestamp: lastSync }) - - // 更新到 IndexedDB - for (const msg of missed) { - await SessionManager.updateOnNewMessage(...) - } -} -``` - -#### **场景2:初始同步中断** - -```typescript -// 记录失败的页 -const failedPages: number[] = [] - -// 重试机制 -while (hasMore) { - try { - await loadPage(page) - } catch (error) { - failedPages.push(page) - if (consecutiveFailures >= 3) break - } -} - -// 重试失败的页 -await retryFailedPages(failedPages) -``` - -#### **场景3:竞态条件(同步期间收到新消息)** - -```typescript -class SessionManager { - private static isSyncing = false - private static pendingUpdates = new Map() - - static beginSync() { - this.isSyncing = true - } - - static async endSync() { - this.isSyncing = false - // 应用暂存的更新 - for (const [id, update] of this.pendingUpdates) { - await db().sessions.update(id, update) - } - } - - static async updateOnNewMessage(...) { - if (this.isSyncing) { - // 暂存更新 - this.pendingUpdates.set(sessionId, update) - return - } - // 立即更新 - await db().sessions.update(sessionId, update) - } -} -``` - -#### **场景4:IndexedDB 写入失败** - -```typescript -try { - await db().sessions.put(session) -} catch (error) { - if (error.name === 'QuotaExceededError') { - // 清理旧数据 - await cleanOldMessages() - // 重试 - await db().sessions.put(session) - } else { - // 降级到内存存储 - window.dispatchEvent( - new CustomEvent('sessionUpdateFallback', { - detail: session, - }) - ) - } -} -``` - ---- - -## 五、消息存储策略 - -### 5.1 推荐:混合策略 - -```typescript -阶段1:基础版(不存储消息) -- 只存储会话列表 -- 消息实时从服务器加载 -- 快速上线 - -阶段2:优化版(可选存储消息) -- 缓存最近 500 条消息/30 天 -- 混合策略:缓存优先 + 后台刷新 -- 提升用户体验 -``` - -### 5.2 存储限制(避免数据过大) - -```typescript -每个会话: -- 最多 500 条消息 -- 保留 30 天 - -清理策略: -- 定期清理超过 30 天的消息 -- 超过 500 条时删除最旧的 - -总体限制: -- 估计 10-50MB/用户 -- 浏览器默认配额:50MB-1GB -``` - ---- - -## 六、实施步骤 - -### 阶段1:数据库基础架构 ✅ - -``` -文件: -- src/utils/db.ts -- src/utils/dbManagers/SessionManager.ts - -任务: -✅ 实现 DatabaseManager(多账户隔离) -✅ 实现 SessionManager(会话管理) -✅ 定义数据表结构 -``` - -### 阶段2:Store 集成 ✅ - -``` -文件: -- src/stores/modules/user.ts -- src/stores/modules/wechat/useSessionStore.ts - -任务: -✅ 登录时初始化数据库 -✅ Store 订阅数据库变更 -✅ 实现缓存优先策略 -✅ 去除定时器轮询 -``` - -### 阶段3:WebSocket 集成 🔄 - -``` -文件: -- src/composables/business/wechat/useWebSocket.ts -- src/composables/business/wechat/useMessageHandler.ts - -任务: -🔄 WebSocket 连接管理 -🔄 消息处理和分发 -🔄 更新 IndexedDB -🔄 心跳检测和重连 -``` - -### 阶段4:防数据丢失 ⏳ - -``` -任务: -⏳ 断线重连后增量同步 -⏳ 同步失败重试机制 -⏳ 竞态条件处理 -⏳ 错误降级方案 -``` - -### 阶段5:消息存储(可选)⏳ - -``` -文件: -- src/utils/dbManagers/MessageManager.ts -- src/stores/modules/wechat/useMessageStore.ts - -任务: -⏳ 实现消息缓存 -⏳ 自动清理机制 -⏳ 混合加载策略 -``` - ---- - -## 七、关键代码清单 - -### 7.1 必须实现的文件 - -``` -src/utils/ -├── db.ts ← 数据库管理器 -└── dbManagers/ - ├── SessionManager.ts ← 会话管理器 - └── MessageManager.ts (可选) ← 消息管理器 - -src/stores/modules/ -├── user.ts ← 用户 Store(数据库初始化) -└── wechat/ - ├── useSessionStore.ts ← 会话 Store - └── useMessageStore.ts (可选) ← 消息 Store - -src/composables/business/wechat/ -├── useWebSocket.ts ← WebSocket 管理 -└── useMessageHandler.ts ← 消息处理 -``` - -### 7.2 核心 API 调用 - -```typescript -// ========== 数据库管理 ========== - -// 登录时初始化数据库 -await databaseManager.ensureDatabase(userId) - -// 获取当前数据库 -const db = databaseManager.getCurrentDatabase() - -// 查询会话列表 -const sessions = await db.sessions.toArray() - -// 订阅变更 -SessionManager.onUpdate((sessions) => { - // 更新 UI -}) - -// ========== 会话管理 ========== - -// WebSocket 收到新消息 -await SessionManager.updateOnNewMessage( - sessionId, - sessionType, - content, - wechatAccountId -) - -// 清除未读 -await SessionManager.clearUnread(sessionId, accountId) - -// 置顶/取消置顶 -await SessionManager.togglePin(sessionId, accountId) - -// 删除会话 -await SessionManager.deleteSession(sessionId, accountId) - -// ========== 联系人信息 ========== - -// 获取好友详情(会话不存在时调用) -const friendInfo = await getFriendDetail({ - friendId: sessionId, -}) - -// 获取群聊详情(会话不存在时调用) -const groupInfo = await getGroupDetail({ - groupId: sessionId, -}) - -// 获取联系人列表 -const contacts = await getContactList({ - wechatAccountId, - page: 1, - limit: 200, -}) - -// ========== 消息管理(可选)========== - -// 缓存消息 -await MessageManager.cacheMessages(sessionId, messages) - -// 获取消息 -const messages = await MessageManager.getMessages(sessionId, offset, limit) - -// 清理旧消息 -await MessageManager.cleanOldMessages(sessionId) -``` - -### 7.3 API 接口清单 - -#### **必需接口**: - -| 接口 | 路径 | 说明 | 调用时机 | -| ----------------- | -------------------- | -------------- | --------------------- | -| `getFriendDetail` | `/api/friend/detail` | 获取好友详情 | ⭐ 收到陌生好友消息时 | -| `getGroupDetail` | `/api/group/detail` | 获取群聊详情 | ⭐ 收到陌生群聊消息时 | -| `getSessionList` | `/api/session/list` | 获取会话列表 | 登录、切换账户 | -| `getContactList` | `/api/contact/list` | 获取联系人列表 | 联系人页面 | -| `getChatMessages` | `/api/message/list` | 获取聊天记录 | 打开会话、加载更多 | -| `sendMessage` | `/api/message/send` | 发送消息 | 用户发送 | -| `markAsRead` | `/api/message/read` | 标记已读 | 消息可见时 | - -#### **可选接口**: - -| 接口 | 路径 | 说明 | -| ------------------ | --------------------- | ------------ | -| `getMessagesSince` | `/api/message/since` | 增量同步消息 | -| `searchMessages` | `/api/message/search` | 搜索历史消息 | -| `getAIConfig` | `/api/ai/config` | 获取AI配置 | -| `uploadFile` | `/api/file/upload` | 上传文件 | - ---- - -## 八、技术栈总结 - -| 模块 | 技术选型 | 说明 | -| ------------ | -------------------- | ---------------------- | -| **数据库** | Dexie (IndexedDB) | 一号一库,物理隔离 | -| **状态管理** | Pinia | 响应式,订阅数据库变更 | -| **实时通信** | WebSocket | 消息推送,心跳检测 | -| **UI 框架** | Vue 3 + Element Plus | 组件化 | -| **错误监控** | Sentry | 数据同步异常追踪 | - ---- - -## 九、注意事项 - -### ⚠️ 必须遵守的规则 - -1. **登录时必须初始化数据库** - - ```typescript - await databaseManager.ensureDatabase(userId) - ``` - -2. **切换账户时必须切换数据库** - - ```typescript - await databaseManager.ensureDatabase(newUserId) - ``` - -3. **登出时必须关闭数据库** - - ```typescript - await databaseManager.closeCurrentDatabase() - ``` - -4. **使用 `db()` 函数获取当前数据库** - - ```typescript - await db().sessions.toArray() // ✅ - await db.sessions.toArray() // ❌ 错误 - ``` - -5. **不要使用定时器轮询会话列表** - ```typescript - setInterval(() => loadSessions(), 3000) // ❌ 错误 - ``` - 应该使用订阅机制: - ```typescript - SessionManager.onUpdate(() => {}) // ✅ 正确 - ``` - ---- - -## 十、性能指标 - -### 10.1 核心性能指标 - -| 指标 | 目标值 | 实测值 | 测试场景 | 备注 | -| -------------- | ------- | --------- | ------------- | --------------- | -| **首屏加载** | < 200ms | 120-180ms | 1000个会话 | 从缓存读取 | -| **切换会话** | < 100ms | 50-80ms | 相邻会话切换 | IndexedDB 查询 | -| **切换账户** | < 500ms | 300-450ms | 1000个会话 | 切换数据库+读取 | -| **消息到UI** | < 50ms | 20-40ms | WebSocket推送 | 更新DB+回调 | -| **发送消息** | < 150ms | 80-120ms | 文本消息 | 到服务器确认 | -| **滚动流畅度** | 60fps | 55-60fps | 1万条消息 | 虚拟滚动 | -| **搜索响应** | < 300ms | 150-250ms | 本地搜索 | IndexedDB查询 | -| **数据库大小** | < 50MB | 10-30MB | 正常使用 | 定期清理 | - -### 10.2 Web Vitals 指标 - -| 指标 | 目标 | 描述 | -| -------- | ------- | ------------ | -| **FCP** | < 1.8s | 首次内容绘制 | -| **LCP** | < 2.5s | 最大内容绘制 | -| **FID** | < 100ms | 首次输入延迟 | -| **CLS** | < 0.1 | 累积布局偏移 | -| **TTFB** | < 600ms | 首字节时间 | -| **TTI** | < 3.8s | 可交互时间 | - -### 10.3 性能优化清单 - -```typescript -// ========== 前端性能优化 ========== - -1. 代码优化 - ✅ 路由懒加载 - ✅ 组件按需导入 - ✅ Tree Shaking - ✅ 代码分割 - ✅ Gzip 压缩 - -2. 资源优化 - ✅ 图片懒加载 - ✅ WebP 格式 - ✅ CDN 加速 - ✅ 资源预加载 - ✅ Service Worker 缓存 - -3. 渲染优化 - ✅ 虚拟滚动 - ✅ 骨架屏 - ✅ 防抖节流 - ✅ 计算属性缓存 - ✅ Keep-Alive - -4. 数据库优化 - ✅ 索引优化 - ✅ 批量操作 - ✅ 事务控制 - ✅ 定期清理 - ✅ 查询优化 - -5. 网络优化 - ✅ HTTP/2 - ✅ WebSocket 长连接 - ✅ 请求合并 - ✅ 响应缓存 - ✅ 增量同步 - -// ========== 监控与分析 ========== - -6. 性能监控 - ✅ Sentry 错误追踪 - ✅ 自定义埋点 - ✅ Performance API - ✅ Lighthouse 测试 - ✅ Bundle Analyzer -``` - -### 10.4 性能测试结果 - -#### **场景1:冷启动(首次访问)** - -``` -测试条件: -- 网络:4G(15Mbps) -- 设备:MacBook Pro M1 -- 浏览器:Chrome 120 -- 数据:0个缓存会话 - -结果: -├─ 加载时间:1.2s -├─ 白屏时间:0.3s -├─ 首屏渲染:0.8s -├─ 可交互时间:1.5s -└─ 总包体积:850KB(Gzip后) -``` - -#### **场景2:热启动(已缓存)** - -``` -测试条件: -- 网络:离线 -- 设备:MacBook Pro M1 -- 浏览器:Chrome 120 -- 数据:1000个会话 - -结果: -├─ 加载时间:< 0.5s -├─ 白屏时间:< 0.1s -├─ 首屏渲染:< 0.2s -├─ 可交互时间:< 0.8s -└─ 缓存命中率:98% -``` - -#### **场景3:高并发(100人同时在线)** - -``` -测试条件: -- 服务器:2核4G -- 并发用户:100人 -- 消息频率:10条/秒 - -结果: -├─ 平均响应时间:< 100ms -├─ 99分位响应时间:< 200ms -├─ WebSocket 连接数:100 -├─ CPU 占用:< 30% -└─ 内存占用:< 1.5GB -``` - -#### **场景4:大数据量(10000条消息)** - -``` -测试条件: -- 会话消息数:10000条 -- 虚拟滚动:开启 -- 设备:MacBook Pro M1 - -结果: -├─ 初始加载:< 0.3s -├─ 滚动帧率:58-60fps -├─ 内存占用:< 150MB -├─ DOM 节点数:< 100(虚拟滚动) -└─ 滚动无卡顿 -``` - -### 10.5 性能问题排查 - -#### **问题1:首屏加载慢** - -```typescript -// 排查步骤 -1. 打开 Chrome DevTools → Network -2. 查看 Waterfall 图 -3. 定位慢请求 - -// 常见原因 -- 资源过大 → 压缩/分割 -- 请求太多 → 合并/懒加载 -- 无缓存 → Service Worker -- 白屏时间长 → 骨架屏 - -// 优化方案 -import { defineAsyncComponent } from 'vue' - -const HeavyComponent = defineAsyncComponent({ - loader: () => import('./HeavyComponent.vue'), - loadingComponent: LoadingSpinner, - delay: 200 -}) -``` - -#### **问题2:滚动卡顿** - -```typescript -// 排查步骤 -1. 打开 Chrome DevTools → Performance -2. 录制滚动操作 -3. 查看 FPS 和 Main Thread - -// 常见原因 -- 渲染节点太多 → 虚拟滚动 -- 重排重绘 → 优化样式 -- 同步操作阻塞 → 异步处理 - -// 优化方案 -// 使用 requestAnimationFrame -const smoothScroll = () => { - requestAnimationFrame(() => { - // 滚动逻辑 - }) -} - -// 使用 will-change 优化 -.message-list { - will-change: transform; - transform: translateZ(0); -} -``` - -#### **问题3:内存泄漏** - -```typescript -// 排查步骤 -1. 打开 Chrome DevTools → Memory -2. 多次操作后拍摄堆快照 -3. 对比增长的对象 - -// 常见原因 -- 事件监听未清理 -- 定时器未清除 -- 闭包持有引用 - -// 优化方案 -// 使用 onUnmounted 清理 -import { onUnmounted } from 'vue' - -const cleanup = () => { - // 清理事件监听 - ws.close() - channel.close() - clearInterval(timer) -} - -onUnmounted(cleanup) -``` - ---- - -## 十一、边界问题和异常处理清单 - -### 11.1 边界问题分类 - -#### **类别1:账户和权限相关** - -| 边界问题 | 场景描述 | 解决方案 | 优先级 | -| ------------------------ | ----------------------------------------------- | ------------------------------------------------------------ | ------ | -| **账户切换时收到消息** | 用户正在从账户A切换到账户B,此时收到账户A的消息 | 检查消息的 `wechatAccountId`,如果不匹配当前账户则丢弃或缓存 | 🔴 高 | -| **账户被删除后收到消息** | 账户在其他设备被删除,但 WebSocket 还在接收消息 | 定期验证账户有效性,无效则断开 WebSocket | 🟡 中 | -| **多账户同时登录** | 同一浏览器登录多个账户(不同标签页) | 每个账户独立数据库,通过 `wechatAccountId` 隔离 | 🔴 高 | -| **无权限访问会话** | 用户尝试访问已被移除的群聊 | 接口返回 403,前端提示"无权限"并移除会话 | 🟡 中 | - -**代码示例(账户切换时的消息处理)**: - -```typescript -// src/composables/business/wechat/useWebSocket.ts - -ws.onmessage = async (event) => { - const message = parseWebSocketMessage(event.data) - - // ⭐ 检查消息是否属于当前账户 - const currentAccountId = useAccountStore().currentAccount?.id - - if (message.wechatAccountId !== currentAccountId) { - console.warn('收到其他账户的消息,已忽略:', { - messageAccountId: message.wechatAccountId, - currentAccountId, - }) - - // 可选:缓存到待处理队列,等待切换回该账户时处理 - pendingMessagesQueue.set(message.wechatAccountId, message) - - return // 直接忽略 - } - - // 继续正常处理 - await SessionManager.updateOnNewMessage(/* ... */) -} - -// 切换账户时处理待处理消息 -const switchAccount = async (newAccountId: number) => { - // 1. 切换数据库 - await databaseManager.ensureDatabase(newAccountId) - - // 2. 处理该账户的待处理消息 - const pendingMessages = pendingMessagesQueue.get(newAccountId) || [] - for (const msg of pendingMessages) { - await SessionManager.updateOnNewMessage(/* ... */) - } - - // 3. 清空待处理队列 - pendingMessagesQueue.delete(newAccountId) -} -``` - ---- - -#### **类别2:网络和连接相关** - -| 边界问题 | 场景描述 | 解决方案 | 优先级 | -| ---------------------- | ------------------------------------------------- | ------------------------------------------------------- | ------ | -| **WebSocket 断线重连** | 网络波动导致 WebSocket 断开 | 指数退避重连 + 增量同步断线期间的消息 | 🔴 高 | -| **断线期间的消息丢失** | WebSocket 断开 5 分钟后重连,期间收到 10 条消息 | 重连后调用 `getMessagesSince(lastMessageTime)` 增量同步 | 🔴 高 | -| **心跳超时** | 60 秒未收到服务器 pong,但连接未断开 | 主动断开并重连,避免"僵尸连接" | 🟡 中 | -| **并发连接限制** | 同一账户在多个标签页打开,服务器限制只能 1 个连接 | 主标签页持有连接,其他标签页通过 BroadcastChannel 同步 | 🟡 中 | -| **弱网环境** | 3G/4G 弱网,消息发送超时 | 显示"发送中"状态,超时后提示重试,支持离线队列 | 🟡 中 | - -**代码示例(断线重连 + 增量同步)**: - -```typescript -// src/composables/business/wechat/useWebSocket.ts - -let reconnectAttempts = 0 -const maxReconnectAttempts = 10 -let lastSyncTime = Date.now() - -const reconnect = async () => { - if (reconnectAttempts >= maxReconnectAttempts) { - console.error('达到最大重连次数,停止重连') - ElMessage.error('连接已断开,请刷新页面') - return - } - - // 指数退避:1s、2s、4s、8s...最多 30s - const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000) - await new Promise((resolve) => setTimeout(resolve, delay)) - - reconnectAttempts++ - - try { - // 1. 重新建立 WebSocket 连接 - await connectWebSocket() - - // 2. ⭐ 增量同步断线期间的消息 - await syncMissedMessages() - - // 3. 重置重连计数 - reconnectAttempts = 0 - lastSyncTime = Date.now() - - ElMessage.success('连接已恢复') - } catch (error) { - console.error('重连失败:', error) - reconnect() // 继续重试 - } -} - -// 增量同步断线期间的消息 -const syncMissedMessages = async () => { - const currentAccountId = useAccountStore().currentAccount?.id - if (!currentAccountId) return - - try { - // 调用增量同步接口(假设后端提供) - const missedMessages = await getMessagesSince({ - wechatAccountId: currentAccountId, - since: lastSyncTime, - }) - - console.log(`同步到 ${missedMessages.length} 条断线期间的消息`) - - // 逐条处理 - for (const message of missedMessages) { - await SessionManager.updateOnNewMessage( - message.sessionId, - message.sessionType, - message.content, - message.wechatAccountId - ) - - // 如果会话已打开,保存消息记录 - if (shouldCacheMessage(message.sessionId)) { - await MessageManager.addMessage(message) - } - } - } catch (error) { - console.error('增量同步失败:', error) - Sentry.captureException(error) - } -} - -ws.onclose = () => { - console.warn('WebSocket 连接已断开,准备重连...') - reconnect() -} -``` - ---- - -#### **类别3:数据一致性相关** - -| 边界问题 | 场景描述 | 解决方案 | 优先级 | -| ------------------------ | ------------------------------------ | ------------------------------------------------- | ------ | -| **消息重复** | WebSocket 重复发送同一条消息 | 使用 `clientId` 去重,IndexedDB 中检查是否已存在 | 🔴 高 | -| **消息乱序** | 网络延迟导致后发的消息先到达 | 使用 `sequence`(序列号)和 `wechatTime` 双重排序 | 🔴 高 | -| **会话被删除后收到消息** | 用户删除会话,但又收到该会话的新消息 | 重新创建会话(参考 4.4 章节的陌生好友逻辑) | 🟡 中 | -| **未读数不一致** | 多标签页同时标记已读,导致未读数错乱 | 使用 BroadcastChannel 同步,以最新值为准 | 🟡 中 | -| **时间戳异常** | 服务器时间比本地时间快 1 小时 | 统一使用服务器时间,本地仅用于 UI 显示 | 🟡 中 | -| **会话列表顺序错乱** | 并发更新导致会话排序混乱 | 使用 `sortKey`(时间戳+ID)保证唯一性和顺序 | 🟡 中 | - -**代码示例(消息去重)**: - -```typescript -// src/utils/dbManagers/MessageManager.ts - -// 消息去重表(内存级别) -const messageClientIdSet = new Set() - -export class MessageManager { - // 检查消息是否重复 - static async checkDuplicate(clientId: string): Promise { - // 1. 内存级别快速检查 - if (messageClientIdSet.has(clientId)) { - return true - } - - // 2. IndexedDB 检查(防止刷新后重复) - const existing = await db() - .messages.where('clientId') - .equals(clientId) - .first() - - if (existing) { - messageClientIdSet.add(clientId) // 补充到内存缓存 - return true - } - - return false - } - - // 添加消息(带去重) - static async addMessage(message: ChatMessage) { - // 1. 检查重复 - if (await this.checkDuplicate(message.clientId)) { - console.log('重复消息,跳过:', message.clientId) - return - } - - // 2. 保存到数据库 - await db().messages.put(message) - - // 3. 添加到内存缓存 - messageClientIdSet.add(message.clientId) - - // 4. 限制内存缓存大小(保留最近 1000 条) - if (messageClientIdSet.size > 1000) { - const toRemove = Array.from(messageClientIdSet).slice(0, 100) - toRemove.forEach((id) => messageClientIdSet.delete(id)) - } - } -} -``` - -**代码示例(多标签页未读数同步)**: - -```typescript -// src/composables/business/wechat/useBroadcastSync.ts - -const channel = new BroadcastChannel('chat_sync') - -// 发送同步消息 -const syncUnreadCount = (sessionId: number, unreadCount: number) => { - channel.postMessage({ - type: 'unread_count_update', - sessionId, - unreadCount, - timestamp: Date.now(), - }) -} - -// 接收同步消息 -channel.onmessage = async (event) => { - const { type, sessionId, unreadCount, timestamp } = event.data - - if (type === 'unread_count_update') { - // ⭐ 只接受更新时间更晚的数据 - const session = await db().sessions.get(sessionId) - const lastUpdateTime = new Date(session?.lastUpdateTime || 0).getTime() - - if (timestamp > lastUpdateTime) { - // 更新本地数据 - await db().sessions.update(sessionId, { - config: { - ...session!.config, - unreadCount, - }, - lastUpdateTime: new Date(timestamp).toISOString(), - }) - - // 更新 Store(触发 UI 刷新) - await SessionManager.triggerCallbacks() - } - } -} -``` - ---- - -#### **类别4:存储和性能相关** - -| 边界问题 | 场景描述 | 解决方案 | 优先级 | -| ---------------------- | ------------------------------ | -------------------------------------------- | ------ | -| **IndexedDB 配额限制** | 存储空间不足(通常 50MB-1GB) | 定期清理旧消息,限制每个会话最多 500 条 | 🟡 中 | -| **数据库损坏** | IndexedDB 文件损坏导致无法打开 | 捕获异常,删除损坏的数据库,重新初始化 | 🟡 中 | -| **大消息/大文件** | 接收 10MB 的视频文件 | 文件不存储在 IndexedDB,仅存储 URL 引用 | 🟡 中 | -| **会话列表过长** | 账户有 5000+ 个会话 | 虚拟滚动 + 分页加载 + 索引优化 | 🟡 中 | -| **并发写入冲突** | 多个操作同时修改同一会话 | 使用 Dexie 事务确保原子性 | 🔴 高 | -| **内存泄漏** | 长时间运行导致内存占用持续增长 | 定期清理未使用的数据,`onUnmounted` 清理监听 | 🟡 中 | - -**代码示例(IndexedDB 配额检测)**: - -```typescript -// src/utils/db.ts - -export class DatabaseManager { - // 检查存储配额 - static async checkStorageQuota() { - if ('storage' in navigator && 'estimate' in navigator.storage) { - const estimate = await navigator.storage.estimate() - const usage = estimate.usage || 0 - const quota = estimate.quota || 0 - const usagePercent = (usage / quota) * 100 - - console.log( - `存储使用情况: ${(usage / 1024 / 1024).toFixed(2)} MB / ${(quota / 1024 / 1024).toFixed(2)} MB (${usagePercent.toFixed(1)}%)` - ) - - // ⚠️ 超过 80% 开始清理 - if (usagePercent > 80) { - console.warn('存储空间不足,开始清理旧数据...') - await this.cleanOldData() - - ElMessage.warning('存储空间不足,已自动清理旧数据') - } - - // 🔴 超过 95% 禁止写入 - if (usagePercent > 95) { - ElMessage.error('存储空间严重不足,请手动清理数据') - return false - } - - return true - } - - return true // 不支持配额检测的浏览器,允许写入 - } - - // 清理旧数据 - static async cleanOldData() { - const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000 - - try { - // 1. 删除 30 天前的消息 - await db().messages.where('wechatTime').below(thirtyDaysAgo).delete() - - // 2. 删除没有会话关联的消息(孤儿消息) - const sessionIds = await db().sessions.toCollection().primaryKeys() - await db().messages.where('sessionId').noneOf(sessionIds).delete() - - console.log('✅ 旧数据清理完成') - } catch (error) { - console.error('清理旧数据失败:', error) - Sentry.captureException(error) - } - } -} - -// 写入前检查配额 -export const safeWrite = async (operation: () => Promise) => { - const hasSpace = await DatabaseManager.checkStorageQuota() - - if (!hasSpace) { - throw new Error('存储空间不足') - } - - await operation() -} -``` - -**代码示例(数据库损坏恢复)**: - -```typescript -// src/utils/db.ts - -export const initDatabase = async (wechatAccountId: number) => { - const dbName = `${DB_NAME_PREFIX}_${wechatAccountId}` - - try { - // 尝试打开数据库 - const database = new ChatDatabase(dbName) - await database.open() - - currentDb = database - console.log(`✅ 数据库 ${dbName} 初始化成功`) - } catch (error: any) { - // ⚠️ 数据库损坏 - if ( - error.name === 'DatabaseClosedError' || - error.name === 'InvalidStateError' || - error.message.includes('corrupt') - ) { - console.error('数据库已损坏,尝试修复...', error) - - try { - // 1. 删除损坏的数据库 - await Dexie.delete(dbName) - console.log('已删除损坏的数据库') - - // 2. 重新创建 - const database = new ChatDatabase(dbName) - await database.open() - - currentDb = database - - // 3. 提示用户 - ElMessage.warning('数据库已损坏并已修复,历史数据已清空') - - // 4. 上报 Sentry - Sentry.captureException(new Error('IndexedDB 损坏'), { - extra: { dbName, originalError: error }, - }) - } catch (retryError) { - console.error('数据库修复失败:', retryError) - ElMessage.error('数据库初始化失败,请清除浏览器缓存后重试') - throw retryError - } - } else { - throw error - } - } -} -``` - ---- - -#### **类别5:业务逻辑相关** - -| 边界问题 | 场景描述 | 解决方案 | 优先级 | -| ---------------- | -------------------------- | ------------------------------------------ | ------ | -| **撤回消息** | 消息发送后被撤回 | WebSocket 接收撤回事件,更新消息状态或删除 | 🟡 中 | -| **未读数溢出** | 某个会话未读数超过 999 | 显示为 "999+",实际值仍然准确记录 | 🟢 低 | -| **特殊字符处理** | 消息包含 emoji、零宽字符等 | 使用 DOMPurify 清理,Vue 自动转义 | 🟡 中 | -| **空会话列表** | 新账户没有任何会话 | 显示空状态插图,引导用户添加好友 | 🟢 低 | -| **群聊成员变化** | 用户被移出群聊 | 接收事件后标记会话为"已退出",禁止发送 | 🟡 中 | -| **好友被删除** | 好友删除当前用户 | 接收事件后标记会话,提示"对方已删除你" | 🟡 中 | - -**代码示例(撤回消息)**: - -```typescript -// src/composables/business/wechat/useWebSocket.ts - -ws.onmessage = async (event) => { - const data = JSON.parse(event.data) - - // 处理撤回消息事件 - if (data.type === 'message_recall') { - const { clientId, sessionId } = data - - // 1. 更新 IndexedDB - await db().messages.where('clientId').equals(clientId).modify({ - content: '[消息已撤回]', - msgType: 10001, // 特殊类型 - recalled: true, - }) - - // 2. 更新会话列表(如果是最新消息) - const session = await db().sessions.get(sessionId) - if (session?.content && session.content === data.originalContent) { - await db().sessions.update(sessionId, { - content: '[消息已撤回]', - }) - } - - // 3. 触发 UI 更新 - await SessionManager.triggerCallbacks() - await MessageManager.triggerCallbacks(sessionId) - - console.log('消息已撤回:', clientId) - } -} -``` - -**代码示例(未读数显示)**: - -```vue - - - - - - - - - -``` - ---- - -#### **类别6:浏览器兼容性相关** - -| 边界问题 | 场景描述 | 解决方案 | 优先级 | -| --------------------------- | ---------------------------------- | ------------------------------------ | ------ | -| **不支持 IndexedDB** | IE 10 或隐私模式禁用 IndexedDB | 降级到内存存储 + 提示用户 | 🟡 中 | -| **不支持 WebSocket** | 老旧浏览器不支持 WebSocket | 降级到轮询 + 提示升级浏览器 | 🟡 中 | -| **不支持 BroadcastChannel** | Safari 不完全支持 BroadcastChannel | 使用 localStorage + storage 事件替代 | 🟢 低 | -| **Safari 隐私模式** | IndexedDB 在隐私模式下被禁用 | 检测并提示用户切换到普通模式 | 🟡 中 | - -**代码示例(IndexedDB 兼容性检测)**: - -```typescript -// src/utils/db.ts - -export const checkBrowserSupport = (): { - supported: boolean - message?: string -} => { - // 1. 检查 IndexedDB - if (!('indexedDB' in window)) { - return { - supported: false, - message: '您的浏览器不支持本地存储功能,请升级到最新版本', - } - } - - // 2. 检查 WebSocket - if (!('WebSocket' in window)) { - return { - supported: false, - message: '您的浏览器不支持实时通信功能,请升级到最新版本', - } - } - - // 3. 检查隐私模式(Safari) - try { - const testDb = indexedDB.open('test') - testDb.onerror = () => { - return { - supported: false, - message: '检测到您正在使用隐私模式,某些功能可能无法使用', - } - } - } catch (error) { - return { - supported: false, - message: '浏览器存储功能已被禁用', - } - } - - return { supported: true } -} - -// 在应用启动时检查 -const supportCheck = checkBrowserSupport() -if (!supportCheck.supported) { - ElMessageBox.alert(supportCheck.message, '兼容性警告', { - type: 'warning', - confirmButtonText: '我知道了', - }) -} -``` - ---- - -### 11.2 边界问题优先级矩阵 - -| 问题类别 | 🔴 高优先级(必须处理) | 🟡 中优先级(建议处理) | 🟢 低优先级(可选) | -| -------------- | -------------------------------------- | ----------------------------------------------------------------------------- | ------------------------ | -| **账户和权限** | 账户切换时收到消息多账户同时登录 | 账户被删除后收到消息无权限访问会话 | - | -| **网络和连接** | WebSocket 断线重连断线期间消息丢失 | 心跳超时并发连接限制弱网环境 | - | -| **数据一致性** | 消息重复消息乱序并发写入冲突 | 会话被删除后收到消息未读数不一致时间戳异常 | - | -| **存储和性能** | - | IndexedDB 配额限制数据库损坏大消息/大文件会话列表过长内存泄漏 | - | -| **业务逻辑** | - | 撤回消息特殊字符处理群聊成员变化好友被删除 | 未读数溢出空会话列表 | -| **浏览器兼容** | - | 不支持 IndexedDB不支持 WebSocketSafari 隐私模式 | 不支持 BroadcastChannel | - ---- - -### 11.3 关键边界问题处理清单 - -以下是开发时必须考虑的关键边界问题清单: - -#### **✅ 阶段1:MVP 必须处理(上线前)** - -- [x] 消息去重(防止 WebSocket 重复发送) -- [x] 消息乱序(使用序列号排序) -- [x] 陌生好友/群聊消息(动态获取详情) -- [x] WebSocket 断线重连(指数退避) -- [x] 账户切换时消息隔离 -- [x] 并发写入冲突(Dexie 事务) -- [x] IndexedDB 配额检测 - -#### **⏳ 阶段2:优化阶段(上线后迭代)** - -- [ ] 断线期间消息增量同步(需后端支持 `getMessagesSince` 接口) -- [ ] 多标签页未读数同步(BroadcastChannel) -- [ ] 数据库损坏恢复 -- [ ] 会话被删除后收到消息 -- [ ] 撤回消息处理 -- [ ] 弱网环境优化(离线队列) -- [ ] 内存泄漏排查和优化 - -#### **🔄 阶段3:体验优化(长期维护)** - -- [ ] Safari 隐私模式兼容 -- [ ] 特殊字符和 emoji 处理 -- [ ] 未读数溢出显示(999+) -- [ ] 大文件/大消息优化 -- [ ] 会话列表虚拟滚动(5000+ 会话) -- [ ] 群聊成员变化提示 -- [ ] 好友删除提示 - ---- - -### 11.4 边界问题测试建议 - -#### **测试场景清单** - -| 测试场景 | 测试步骤 | 预期结果 | -| ------------------ | ----------------------------------------------------------------- | ---------------------------------- | -| **消息重复** | 1. WebSocket 手动发送同一条消息 2 次2. 观察会话列表和消息列表 | 只显示 1 条消息 | -| **账户切换** | 1. 登录账户 A2. 切换到账户 B3. 账户 A 收到新消息 | 账户 A 的消息不显示在账户 B 中 | -| **断线重连** | 1. 登录后断开网络2. 等待 10 秒3. 恢复网络 | 自动重连成功,显示提示"连接已恢复" | -| **陌生好友消息** | 1. 删除某个好友的会话2. 该好友发送新消息 | 会话列表自动创建新会话并显示 | -| **IndexedDB 配额** | 1. 填充大量数据至接近配额2. 继续发送消息 | 自动清理旧数据,弹出提示 | -| **并发写入** | 1. 多个标签页同时标记已读2. 观察未读数 | 未读数一致,无冲突 | -| **消息乱序** | 1. 手动调整消息时间戳2. 观察消息列表 | 消息按序列号正确排序 | -| **撤回消息** | 1. 发送消息2. WebSocket 发送撤回事件 | 消息显示为"[消息已撤回]" | - ---- - -## 十二、核心代码示例 - -### 11.1 数据库管理器完整实现 - -```typescript -// src/utils/db.ts - -import Dexie, { Table } from 'dexie' - -const DB_NAME_PREFIX = 'ChatDatabase' - -// ==================== 数据表接口 ==================== -export interface ChatSession { - id: number - serverId: string - type: 'friend' | 'group' - wechatAccountId: number - nickname: string - conRemark?: string - avatar: string - content: string - lastUpdateTime: string - config: { - unreadCount: number - top: boolean - msgTime: number - } - sortKey: string -} - -export interface ChatMessage { - id: number - serverId: string - sessionId: number - sessionType: 'friend' | 'group' - wechatAccountId: number - content: string - msgType: number - direction: 'send' | 'receive' - createTime: string - wechatTime: number - status: 'sending' | 'success' | 'failed' - extra?: string -} - -export interface Contact { - id: number - serverId: string - type: 'friend' | 'group' - wechatAccountId: number - nickname: string - conRemark?: string - avatar: string - lastUpdateTime: string - searchKey: string -} - -// ==================== 数据库类 ==================== -class ChatDatabase extends Dexie { - sessions!: Table - messages!: Table - contacts!: Table - - constructor(dbName: string) { - super(dbName) - - this.version(1).stores({ - sessions: 'id, serverId, wechatAccountId, type, lastUpdateTime, sortKey', - messages: 'id, serverId, sessionId, [sessionId+createTime], createTime', - contacts: 'id, serverId, wechatAccountId, type, searchKey', - }) - } -} - -// ==================== 数据库管理器 ==================== -class DatabaseManager { - private currentDb: ChatDatabase | null = null - private currentUserId: number | null = null - - private getDatabaseName(userId: number): string { - return `${DB_NAME_PREFIX}_${userId}` - } - - private async openDatabase(dbName: string): Promise { - const instance = new ChatDatabase(dbName) - await instance.open() - return instance - } - - async ensureDatabase(userId: number): Promise { - if (!userId) { - throw new Error('Invalid userId') - } - - if ( - this.currentDb && - this.currentUserId === userId && - this.currentDb.isOpen() - ) { - return this.currentDb - } - - await this.closeCurrentDatabase() - - const dbName = this.getDatabaseName(userId) - this.currentDb = await this.openDatabase(dbName) - this.currentUserId = userId - - console.log(`✅ 已切换到用户 ${userId} 的数据库: ${dbName}`) - - return this.currentDb - } - - getCurrentDatabase(): ChatDatabase { - if (!this.currentDb) { - throw new Error('Database not initialized') - } - return this.currentDb - } - - getCurrentUserId(): number | null { - return this.currentUserId - } - - isInitialized(): boolean { - return !!this.currentDb && this.currentDb.isOpen() - } - - async closeCurrentDatabase(): Promise { - if (this.currentDb) { - try { - this.currentDb.close() - console.log(`📕 已关闭用户 ${this.currentUserId} 的数据库`) - } catch (error) { - console.warn('关闭数据库失败:', error) - } - this.currentDb = null - this.currentUserId = null - } - } - - async deleteUserDatabase(userId: number): Promise { - const dbName = this.getDatabaseName(userId) - - if (this.currentUserId === userId) { - await this.closeCurrentDatabase() - } - - await Dexie.delete(dbName) - console.log(`🗑️ 已删除用户 ${userId} 的数据库`) - } - - async listUserDatabases(): Promise { - const databases = await Dexie.getDatabaseNames() - const userIds: number[] = [] - - databases.forEach((dbName) => { - if (dbName.startsWith(DB_NAME_PREFIX)) { - const userId = parseInt(dbName.replace(`${DB_NAME_PREFIX}_`, '')) - if (!isNaN(userId)) { - userIds.push(userId) - } - } - }) - - return userIds - } -} - -export const databaseManager = new DatabaseManager() -export const db = () => databaseManager.getCurrentDatabase() -``` - -### 11.2 会话管理器完整实现 - -```typescript -// src/utils/dbManagers/SessionManager.ts - -import { db } from '../db' -import type { ChatSession } from '../db' - -export class SessionManager { - private static updateCallbacks = new Set<(sessions: ChatSession[]) => void>() - private static isSyncing = false - private static pendingUpdates = new Map>() - - // ==================== 回调管理 ==================== - - static onUpdate(callback: (sessions: ChatSession[]) => void) { - this.updateCallbacks.add(callback) - return () => this.updateCallbacks.delete(callback) - } - - private static async triggerCallbacks(accountId?: number) { - try { - const sessions = await this.getUserSessions(accountId) - this.updateCallbacks.forEach((cb) => { - try { - cb(sessions) - } catch (error) { - console.error('会话更新回调执行失败:', error) - } - }) - } catch (error) { - console.error('触发回调失败:', error) - } - } - - // ==================== 同步控制 ==================== - - static beginSync() { - this.isSyncing = true - this.pendingUpdates.clear() - } - - static async endSync(accountId?: number) { - this.isSyncing = false - - if (this.pendingUpdates.size > 0) { - console.log(`应用 ${this.pendingUpdates.size} 个待处理更新`) - - for (const [sessionId, update] of this.pendingUpdates) { - await db().sessions.update(sessionId, update) - } - - this.pendingUpdates.clear() - await this.triggerCallbacks(accountId) - } - } - - // ==================== 数据操作 ==================== - - static async getUserSessions(accountId?: number): Promise { - let query = db().sessions.toCollection() - - if (accountId && accountId !== 0) { - query = db().sessions.where('wechatAccountId').equals(accountId) - } - - const sessions = await query.toArray() - - return sessions.sort((a, b) => { - if (a.config.top && !b.config.top) return -1 - if (!a.config.top && b.config.top) return 1 - return b.config.msgTime - a.config.msgTime - }) - } - - static async upsertSession(session: ChatSession) { - try { - await db().sessions.put(session) - await this.triggerCallbacks() - } catch (error) { - console.error('保存会话失败:', error) - throw error - } - } - - static async syncSessions(sessions: ChatSession[]) { - await db().transaction('rw', db().sessions, async () => { - for (const session of sessions) { - await db().sessions.put(session) - } - }) - await this.triggerCallbacks() - } - - static async updateOnNewMessage( - sessionId: number, - sessionType: 'friend' | 'group', - content: string, - wechatAccountId?: number - ) { - const update: Partial = { - content, - lastUpdateTime: new Date().toISOString(), - config: { - msgTime: Date.now(), - unreadCount: 0, // 会在下面更新 - top: false, - }, - } - - if (this.isSyncing) { - console.log('同步进行中,暂存更新:', sessionId) - this.pendingUpdates.set(sessionId, update) - return - } - - const existing = await db().sessions.get(sessionId) - - if (existing) { - // 会话已存在,直接更新 - await db().sessions.update(sessionId, { - ...update, - config: { - ...existing.config, - msgTime: Date.now(), - unreadCount: (existing.config.unreadCount || 0) + 1, - }, - }) - await this.triggerCallbacks(wechatAccountId) - } else { - // ⭐ 会话不存在,需要先获取好友详情再创建 - console.warn('会话不存在,获取好友详情并创建:', sessionId) - - try { - // 根据类型调用不同接口 - const contactInfo = - sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 创建新会话 - const newSession: ChatSession = { - id: sessionId, - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: wechatAccountId || 0, - nickname: contactInfo.nickname, - conRemark: contactInfo.conRemark, - avatar: contactInfo.avatar, - chatroomId: - sessionType === 'group' ? contactInfo.chatroomId : undefined, - content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: Date.now(), - chat: true, - }, - sortKey: `${Date.now()}_${sessionId}`, - } - - // 保存到数据库 - await db().sessions.put(newSession) - - console.log('✅ 新会话已创建:', newSession.nickname) - - // 触发回调,更新 UI - await this.triggerCallbacks(wechatAccountId) - } catch (error) { - console.error('获取好友详情失败,使用降级方案:', error) - - // 降级方案:创建临时会话(只包含基本信息) - const tempSession: ChatSession = { - id: sessionId, - serverId: `${sessionType}_${sessionId}`, - type: sessionType, - wechatAccountId: wechatAccountId || 0, - nickname: '未知用户', - avatar: '/default-avatar.png', - content, - lastUpdateTime: new Date().toISOString(), - config: { - unreadCount: 1, - top: false, - msgTime: Date.now(), - chat: true, - }, - sortKey: `${Date.now()}_${sessionId}`, - } - - await db().sessions.put(tempSession) - - // 后台继续尝试获取完整信息 - this.retryGetContactInfo(sessionId, sessionType, wechatAccountId) - } - } - } - - // ⭐ 后台重试获取联系人信息 - private static async retryGetContactInfo( - sessionId: number, - sessionType: 'friend' | 'group', - wechatAccountId?: number - ) { - let retryCount = 0 - const maxRetries = 3 - - while (retryCount < maxRetries) { - try { - // 指数退避:5秒、10秒、15秒 - await new Promise((resolve) => - setTimeout(resolve, 5000 * (retryCount + 1)) - ) - - const contactInfo = - sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 更新会话详情 - await db().sessions.update(sessionId, { - nickname: contactInfo.nickname, - conRemark: contactInfo.conRemark, - avatar: contactInfo.avatar, - }) - - console.log('✅ 已更新会话详情:', contactInfo.nickname) - await this.triggerCallbacks(wechatAccountId) - break - } catch (error) { - retryCount++ - console.warn( - `重试获取联系人信息失败 (${retryCount}/${maxRetries})`, - error - ) - - if (retryCount >= maxRetries) { - // 达到最大重试次数,记录到 Sentry - Sentry.captureException( - new Error(`无法获取会话${sessionId}的详细信息`), - { - extra: { sessionId, sessionType, retryCount }, - } - ) - } - } - } - } - - static async clearUnread(sessionId: number, accountId?: number) { - await db().sessions.update(sessionId, { - 'config.unreadCount': 0, - }) - await this.triggerCallbacks(accountId) - } - - static async deleteSession(sessionId: number, accountId?: number) { - await db().sessions.delete(sessionId) - await this.triggerCallbacks(accountId) - } - - static async togglePin(sessionId: number, accountId?: number) { - const session = await db().sessions.get(sessionId) - if (session) { - await db().sessions.update(sessionId, { - 'config.top': !session.config.top, - }) - await this.triggerCallbacks(accountId) - } - } -} -``` - -### 11.3 Store 集成示例 - -```typescript -// src/stores/modules/wechat/useSessionStore.ts - -import { defineStore } from 'pinia' -import { ref, onUnmounted } from 'vue' -import { SessionManager } from '@/utils/dbManagers/SessionManager' -import { getSessionList } from '@/api' -import type { Session } from '@/types/wechat' - -export const useSessionStore = defineStore('wechat-session', () => { - const sessions = ref([]) - const currentSession = ref(null) - const loading = ref(false) - const currentAccountId = ref(0) - - let unsubscribe: (() => void) | null = null - - const init = async (accountId: number = 0) => { - currentAccountId.value = accountId - - // 1. 从 IndexedDB 读取缓存 - sessions.value = await SessionManager.getUserSessions(accountId) - - // 2. 订阅数据库变更 - unsubscribe = SessionManager.onUpdate((updatedSessions) => { - if (accountId === 0) { - sessions.value = updatedSessions - } else { - sessions.value = updatedSessions.filter( - (s) => s.wechatAccountId === accountId - ) - } - }) - - // 3. 后台同步服务器数据 - syncFromServer(accountId) - } - - const syncFromServer = async (accountId: number = 0) => { - loading.value = true - SessionManager.beginSync() - - try { - let page = 1 - const limit = 200 - let hasMore = true - - while (hasMore) { - const params: any = { page, limit } - if (accountId !== 0) params.wechatAccountId = accountId - - const res = await getSessionList(params) - const list = res?.list || [] - const total = res?.total || 0 - - if (list.length === 0) break - - await SessionManager.syncSessions(list) - - if (total > 0 && page * limit >= total) break - page++ - } - } catch (error) { - console.error('同步会话失败:', error) - } finally { - await SessionManager.endSync(accountId) - loading.value = false - } - } - - const switchAccount = async (accountId: number) => { - await init(accountId) - } - - const selectSession = async (session: Session) => { - currentSession.value = session - if (session.config?.unreadCount > 0) { - await SessionManager.clearUnread(session.id, currentAccountId.value) - } - } - - const cleanup = () => { - if (unsubscribe) { - unsubscribe() - unsubscribe = null - } - } - - onUnmounted(cleanup) - - return { - sessions, - currentSession, - loading, - init, - switchAccount, - selectSession, - cleanup, - } -}) -``` - ---- - -## 十二、架构对比分析 - -### 12.1 与PC端微信架构对比 - -#### **PC微信的架构特点** - -``` -PC微信技术架构(推测): - -┌─────────────────────┐ -│ Native GUI (C++) │ ← Electron/Qt 本地应用 -└──────────┬──────────┘ - ↓ -┌─────────────────────┐ -│ SQLite 本地数据库 │ ← 完整的离线存储 -│ - 消息历史 │ -│ - 联系人列表 │ -│ - 文件缓存 │ -└──────────┬──────────┘ - ↓ -┌─────────────────────┐ -│ 长连接协议 │ ← 自定义协议 -│ - 心跳保活 │ -│ - 断线重连 │ -│ - 消息确认机制 │ -└─────────────────────┘ -``` - -#### **架构层面对比** - -| 维度 | PC微信 | TouchVueThree | 对比结果 | -| -------------- | ---------------- | ------------------- | --------- | -| **应用类型** | Native客户端 | Web应用 | ❌ 微信胜 | -| **启动速度** | 500ms-1s | 1-2s | ❌ 微信胜 | -| **内存占用** | 100-300MB | 50-150MB | ✅ 我们胜 | -| **安装部署** | 需要下载安装 | 打开即用 | ✅ 我们胜 | -| **跨平台** | 需要多版本 | 一套代码全平台 | ✅ 我们胜 | -| **数据库** | SQLite(无限制) | IndexedDB(有限制) | ❌ 微信胜 | -| **离线能力** | 完全离线可用 | 部分离线 | ❌ 微信胜 | -| **实时性** | 自定义协议 | WebSocket | 🟡 接近 | -| **消息可靠性** | 100%保证 | 99.9%+ | 🟡 接近 | -| **更新维护** | 需要用户更新 | 自动更新 | ✅ 我们胜 | - -#### **用户体验对比** - -| 场景 | PC微信 | TouchVueThree | 评分 | -| -------------- | ---------------- | --------------- | --------- | -| **首次打开** | 5-10s | <1s(缓存优先) | ✅ 我们胜 | -| **切换会话** | 瞬间 | 瞬间 | 🟡 平手 | -| **发送消息** | 即时 | 即时 | 🟡 平手 | -| **接收消息** | 即时 | 即时 | 🟡 平手 | -| **搜索历史** | 快速(本地索引) | 需要API | ❌ 微信胜 | -| **查看图片** | 本地缓存秒开 | 需要加载 | ❌ 微信胜 | -| **离线使用** | 完全可用 | 只能查看缓存 | ❌ 微信胜 | -| **多设备同步** | 需手动 | 自动同步 | ✅ 我们胜 | -| **占用空间** | 可能数GB | <100MB | ✅ 我们胜 | - -### 12.2 性能对比实测数据(预估) - -| 操作 | PC微信 | TouchVueThree | 差距 | -| ---------- | ------ | ------------- | ----- | -| 应用启动 | 800ms | 1500ms | +87% | -| 切换会话 | 50ms | 80ms | +60% | -| 发送消息 | 100ms | 120ms | +20% | -| 接收消息 | 50ms | 60ms | +20% | -| 搜索消息 | 200ms | 1500ms | +650% | -| 加载图片 | 50ms | 300ms | +500% | -| 滚动流畅度 | 60fps | 50-60fps | 略差 | - -### 12.3 我们的优势场景 - -#### ✅ **企业客服系统** - -``` -适合场景: -- 多人协作,需要统一管理 -- 需要快速部署和更新 -- 需要权限控制和数据统计 -- 跨平台一致性体验 -``` - -#### ✅ **轻量级沟通** - -``` -适合场景: -- 主要处理实时消息 -- 不需要查询大量历史 -- 在线使用为主 -- 对安装包大小敏感 -``` - -#### ✅ **快速迭代产品** - -``` -适合场景: -- 需要频繁功能更新 -- 需要A/B测试 -- 需要灰度发布 -- 需要快速响应需求 -``` - -### 12.4 优化路线图 - -#### **短期优化(1-2周)** - -```typescript -优化项: -1. ✅ 增加消息缓存量(500→2000条) -2. ✅ 使用 Service Worker 缓存图片 -3. ✅ 优化虚拟滚动性能 -4. ✅ 添加消息预加载机制 -5. ✅ 实现骨架屏加载 - -预期提升: -- 首屏加载速度提升 30% -- 滚动流畅度提升至 60fps -- 图片加载速度提升 50% -``` - -#### **中期优化(1-2月)** - -```typescript -优化项: -1. ⏳ 实现增量同步机制 -2. ⏳ 添加全文搜索索引 -3. ⏳ 优化 WebSocket 重连策略 -4. ⏳ 实现消息离线队列 -5. ⏳ 优化大文件传输 - -预期提升: -- 消息到达可靠性 > 99.99% -- 搜索速度提升 70% -- 离线能力增强 -``` - -#### **长期优化(3-6月)** - -```typescript -优化项: -1. ⏳ 考虑 Electron 混合方案 -2. ⏳ 实现 WebAssembly 加速 -3. ⏳ 优化大文件传输协议 -4. ⏳ 完善离线能力 -5. ⏳ 添加本地全文索引 - -预期提升: -- 性能接近原生应用水平(90%+) -- 离线能力媲美PC微信 -- 存储空间可达 1GB+ -``` - -### 12.5 混合方案建议(最优解) - -``` -┌─────────────────────────────────────┐ -│ 阶段1:Web版(当前) │ -├─────────────────────────────────────┤ -│ 技术栈:Vue 3 + Vite │ -│ 优势:快速上线,验证需求 │ -│ 部署:Web服务器 │ -│ 时间:1-2月 │ -└─────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ 阶段2:Electron版(可选) │ -├─────────────────────────────────────┤ -│ 技术栈:Vue 3 + Electron │ -│ 优势:原生能力,更大存储 │ -│ 部署:打包为桌面应用 │ -│ 代码复用:>90% │ -│ 时间:1月(基于Web版) │ -└─────────────────────────────────────┘ - -技术实现: -1. Web版和Desktop版共享核心代码 -2. 使用条件编译区分平台差异 -3. Desktop版获得原生文件访问能力 -4. Desktop版可使用 SQLite 替代 IndexedDB -5. 两版本同步更新,维护成本低 -``` - -### 12.6 综合评分 - -| 维度 | PC微信 | TouchVueThree | 备注 | -| -------------- | ------- | ------------- | ------------------- | -| **性能** | 9.5分 | 7.5分 | 后期可优化至8.5分 | -| **可靠性** | 10分 | 8.5分 | 防丢失策略后可达9分 | -| **用户体验** | 9分 | 8分 | 企业场景足够 | -| **开发成本** | 8分 | 9分 | Web开发更简单 | -| **维护成本** | 7分 | 9分 | 自动更新优势明显 | -| **跨平台能力** | 6分 | 10分 | 一套代码全平台 | -| **部署便捷性** | 5分 | 10分 | 无需安装 | -| **总分** | 54.5/70 | 62/70 | **我们领先** | - -### 12.7 结论 - -**TouchVueThree 方案评价**: - -✅ **核心体验**: 达到微信 80-85% 水平,企业客服场景完全够用 -✅ **技术架构**: 现代化,易维护,扩展性强 -✅ **开发效率**: 高(单一技术栈,快速迭代) -✅ **部署成本**: 低(无需安装,自动更新) -✅ **适用场景**: 企业客服、轻量沟通、SaaS产品 - -**最终定位**: - -> 这不是微信的替代品,而是一个专为企业客服场景优化的现代化聊天系统。在开发效率、部署成本和用户体验之间找到了**最佳平衡点**。 - ---- - -## 十三、FAQ - -### Q1: 为什么不使用定时器轮询? - -**A**: 轮询会造成不必要的网络请求和服务器压力。 - -```typescript -// ❌ 错误做法:定时轮询 -setInterval(() => { - loadSessions() // 每3秒请求一次,浪费资源 -}, 3000) - -// ✅ 正确做法:订阅机制 -SessionManager.onUpdate((sessions) => { - // 数据变更时自动更新,按需响应 - updateUI(sessions) -}) -``` - -**优势对比**: - -- **轮询方式**: 1小时 = 1200次请求(每3秒一次) -- **订阅方式**: 1小时 = 实际消息数量(可能只有几次) - -**节省资源**: - -- 🔽 网络请求减少 95%+ -- 🔽 服务器负载降低 95%+ -- 🔽 客户端CPU占用降低 80%+ -- ✅ 响应速度更快(<50ms vs 0-3000ms) - ---- - -### Q2: 切换账户时数据会混乱吗? - -**A**: 不会。每个账户都有独立的数据库,数据完全物理隔离。 - -```typescript -// 用户A登录 -await databaseManager.ensureDatabase(123) -// 使用 ChatDatabase_123 - -// 切换到用户B -await databaseManager.ensureDatabase(456) -// 自动切换到 ChatDatabase_456 -// 同时关闭用户A的数据库,不会相互干扰 -``` - -**安全保障**: - -- ✅ **物理隔离**: 不同数据库文件,彻底隔离 -- ✅ **自动切换**: 无需手动管理,防止出错 -- ✅ **防泄漏**: 旧数据库自动关闭 -- ✅ **性能优化**: 只打开当前用户的数据库 - -**实际效果**: - -``` -浏览器 IndexedDB: -├── ChatDatabase_123 (用户A的数据) -├── ChatDatabase_456 (用户B的数据) -└── ChatDatabase_789 (用户C的数据) -``` - ---- - -### Q3: IndexedDB 存储空间有限制吗? - -**A**: 有限制,但对客服系统来说完全够用。 - -**浏览器配额**: - -| 浏览器 | 默认配额 | 可申请 | 实际可用 | -| ------- | ------------ | ------ | -------- | -| Chrome | ~60%可用空间 | 无上限 | 数十GB | -| Firefox | ~50%可用空间 | 可申请 | 数十GB | -| Safari | ~1GB | 固定 | 1GB | -| Edge | ~60%可用空间 | 无上限 | 数十GB | - -**我们的使用量**: - -```typescript -// 数据量预估 -const STORAGE_ESTIMATION = { - // 单条数据大小 - perSession: 500, // 500 bytes/会话 - perMessage: 500, // 500 bytes/消息 - perContact: 300, // 300 bytes/联系人 - - // 数量限制 - maxSessions: 1000, // 最多1000个会话 - maxMessagesPerSession: 500, // 每会话500条消息 - maxContacts: 5000, // 最多5000个联系人 - - // 总量计算 - sessions: 1000 × 500 = 500KB, - messages: 1000 × 500 × 500 = 250MB (极端情况), - contacts: 5000 × 300 = 1.5MB, - - // 实际使用(正常情况) - typical: 10-50MB/用户, - maximum: 100MB/用户 (极端场景) -} -``` - -**控制策略**: - -```typescript -// 自动清理机制 -const cleanup = { - // 消息清理 - messageRetention: 30, // 保留30天 - maxMessagesPerSession: 500, // 每会话最多500条 - - // 会话清理 - inactiveSessionDays: 90, // 90天未活跃的会话 - - // 文件清理 - fileCache: 7, // 文件缓存7天 - - // 触发条件 - quotaThreshold: 0.8, // 使用率超过80%时清理 -} -``` - ---- - -### Q4: 如果用户清除浏览器数据会怎样? - -**A**: IndexedDB 会被清空,但有完善的容错机制。 - -**容错流程**: - -``` -1. 用户清除数据 → IndexedDB 被删除 - ↓ -2. 下次登录 → 检测到数据库为空 - ↓ -3. 显示加载状态 → "正在同步数据..." - ↓ -4. 自动从服务器同步 → 重建本地数据库 - ↓ -5. 恢复正常使用 → 用户无感知 -``` - -**防范措施**: - -```typescript -// 1. 关键状态备份到 localStorage -const backupCriticalState = () => { - const critical = { - lastSyncTime: Date.now(), - currentSessionId: sessionStore.currentSession?.id, - unreadCount: sessionStore.totalUnread, - recentSessions: sessionStore.sessions.slice(0, 10), // 最近10个会话 - } - localStorage.setItem('chat_backup', JSON.stringify(critical)) -} - -// 2. 检测数据丢失 -const detectDataLoss = async () => { - const sessionCount = await db().sessions.count() - const hasBackup = localStorage.getItem('chat_backup') - - if (sessionCount === 0 && hasBackup) { - // 数据丢失,提示用户 - ElMessage.warning('检测到本地数据已清空,正在重新同步...') - await resyncAllData() - } -} - -// 3. 快速恢复 -const quickRestore = () => { - const backup = localStorage.getItem('chat_backup') - if (backup) { - const state = JSON.parse(backup) - // 恢复关键状态,让用户快速看到内容 - sessionStore.sessions = state.recentSessions - // 后台继续完整同步 - syncAllData() - } -} -``` - ---- - -### Q5: 多标签页会数据不同步吗? - -**A**: IndexedDB 本身支持多标签页共享,但需要主动通知UI更新。 - -**推荐方案:BroadcastChannel** - -```typescript -// 创建广播频道 -const channel = new BroadcastChannel('chat_sync') - -// 标签页A:数据变更后广播 -const updateSession = async (session: Session) => { - // 1. 更新 IndexedDB - await SessionManager.upsertSession(session) - - // 2. 广播给其他标签页 - channel.postMessage({ - type: 'session_updated', - data: { - sessionId: session.id, - timestamp: Date.now(), - }, - }) -} - -// 标签页B/C/D:监听并更新UI -channel.onmessage = async (event) => { - const { type, data } = event.data - - switch (type) { - case 'session_updated': - // 重新从 IndexedDB 读取(已被标签页A更新) - const session = await SessionManager.getSession(data.sessionId) - sessionStore.updateSessionInUI(session) - break - - case 'session_deleted': - sessionStore.removeSessionFromUI(data.sessionId) - break - - case 'new_message': - // 播放提示音 - playNotificationSound() - // 更新未读数 - sessionStore.incrementUnread(data.sessionId) - break - } -} -``` - -**同步场景覆盖**: - -- ✅ 会话列表更新 -- ✅ 未读数变化 -- ✅ 新消息到达 -- ✅ 会话删除/置顶 -- ✅ 消息已读状态 -- ✅ 用户在线状态 - ---- - -### Q6: WebSocket 断线会丢消息吗? - -**A**: 不会。有多重防丢失机制。 - -**保障措施**: - -```typescript -// 1. 记录最后同步时间 -const recordSyncTime = () => { - localStorage.setItem('lastSyncTime', Date.now().toString()) -} - -// 2. 重连后增量同步 -ws.onopen = async () => { - const lastSync = parseInt(localStorage.getItem('lastSyncTime') || '0') - const now = Date.now() - - if (now - lastSync > 5000) { - // 断线超过5秒 - console.log('检测到连接中断,开始增量同步...') - - // 拉取遗漏的消息 - const missed = await getMessagesSince({ - timestamp: lastSync, - accountId: currentAccountId.value, - }) - - console.log(`获取到 ${missed.length} 条遗漏消息`) - - // 更新到本地 - for (const msg of missed) { - await handleNewMessage(msg) - } - } - - // 更新同步时间 - recordSyncTime() -} - -// 3. 心跳检测(30秒) -let heartbeatTimer: NodeJS.Timeout -let heartbeatTimeout: NodeJS.Timeout - -const startHeartbeat = () => { - heartbeatTimer = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'ping' })) - - // 5秒内未收到 pong 则认为连接异常 - heartbeatTimeout = setTimeout(() => { - console.warn('心跳超时,重连...') - ws.close() - reconnect() - }, 5000) - } - }, 30000) -} - -ws.onmessage = (event) => { - const data = JSON.parse(event.data) - - if (data.type === 'pong') { - // 收到心跳响应,清除超时 - clearTimeout(heartbeatTimeout) - recordSyncTime() - } -} - -// 4. 指数退避重连 -let reconnectAttempts = 0 -const maxReconnectDelay = 60000 // 最大60秒 - -const reconnect = () => { - const delay = Math.min( - 1000 * Math.pow(2, reconnectAttempts), - maxReconnectDelay - ) - - console.log(`将在 ${delay}ms 后重连 (尝试 ${reconnectAttempts + 1})`) - - setTimeout(() => { - reconnectAttempts++ - connect() - }, delay) -} - -// 5. 连接成功后重置 -ws.onopen = () => { - reconnectAttempts = 0 - startHeartbeat() - incrementalSync() -} -``` - -**可靠性保证**: - -- ✅ 99.99% 消息不丢失 -- ✅ 断线自动重连 -- ✅ 增量同步遗漏消息 -- ✅ 心跳检测连接状态 - ---- - -### Q7: 如何处理大量历史消息? - -**A**: 采用分页加载 + 虚拟滚动 + 智能缓存。 - -**完整方案**: - -```typescript -// 1. 分页加载策略 -const loadMessages = async (sessionId: number, page = 1) => { - const LIMIT = 50 - - // ① 优先从缓存读取 - const cached = await MessageManager.getMessages( - sessionId, - (page - 1) * LIMIT, - LIMIT - ) - - if (cached.length > 0) { - return cached // 秒开 - } - - // ② 从服务器加载 - const messages = await getChatMessages({ - sessionId, - from: (page - 1) * LIMIT, - count: LIMIT - }) - - // ③ 缓存到本地 - await MessageManager.cacheMessages(sessionId, messages) - - return messages -} - -// 2. 虚拟滚动(只渲染可见区域) - - - - - - - - -// 3. 滚动到顶部加载更多 -const onScroll = (e: Event) => { - const { scrollTop } = e.target as HTMLElement - - if (scrollTop < 100 && !loading.value && hasMore.value) { - loadMoreMessages() - } -} - -// 4. 智能预加载 -const preloadNearbyMessages = async () => { - const currentIndex = messages.value.findIndex( - m => m.id === currentMessageId.value - ) - - if (currentIndex > messages.value.length - 20) { - // 快滚动到底部了,预加载下一页 - loadMoreMessages() - } -} - -// 5. 内存管理 -const MAX_MESSAGES_IN_MEMORY = 500 - -const manageMemory = () => { - if (messages.value.length > MAX_MESSAGES_IN_MEMORY) { - // 移除最旧的消息(保留在缓存中) - messages.value = messages.value.slice(-MAX_MESSAGES_IN_MEMORY) - } -} -``` - -**性能效果**: - -- ✅ 首屏加载 < 200ms -- ✅ 滚动流畅 60fps -- ✅ 内存占用 < 50MB -- ✅ 支持万级消息 - ---- - -### Q8: 如何优化首屏加载速度? - -**A**: 多管齐下,全面优化。 - -**优化清单**: - -```typescript -// ========== 1. 代码分割 ========== -// 路由懒加载 -const routes = [ - { - path: '/chat', - component: () => import('@/views/Chat/index.vue') - }, - { - path: '/settings', - component: () => import('@/views/Settings/index.vue') - } -] - -// ========== 2. 组件按需导入 ========== -// vite.config.ts -import Components from 'unplugin-vue-components/vite' -import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' - -plugins: [ - Components({ - resolvers: [ElementPlusResolver()] - }) -] - -// ========== 3. 图片优化 ========== -// 懒加载 - - -// WebP格式 - - - - - -// ========== 4. 预加载关键资源 ========== -// index.html - - - - -// ========== 5. Service Worker 缓存 ========== -// sw.js -self.addEventListener('install', (event) => { - event.waitUntil( - caches.open('chat-v1').then((cache) => { - return cache.addAll([ - '/', - '/css/main.css', - '/js/app.js', - '/fonts/main.woff2' - ]) - }) - ) -}) - -// ========== 6. 骨架屏 ========== - - - - - - - - - -// ========== 7. 资源压缩 ========== -// vite.config.ts -import viteCompression from 'vite-plugin-compression' - -plugins: [ - viteCompression({ - algorithm: 'gzip', - threshold: 10240, // 10KB以上才压缩 - deleteOriginFile: false - }) -] - -// ========== 8. CDN加速 ========== -// vite.config.ts -build: { - rollupOptions: { - output: { - manualChunks: { - 'element-plus': ['element-plus'], - 'echarts': ['echarts'], - vendor: ['vue', 'vue-router', 'pinia'] - } - } - } -} -``` - -**优化效果对比**: - -| 指标 | 优化前 | 优化后 | 提升 | -| -------- | ------ | ------ | ---------- | -| 首屏时间 | 3-5s | <1s | **70-80%** | -| 白屏时间 | 2s | <300ms | **85%** | -| 包体积 | 2MB | 800KB | **60%** | -| 首次加载 | 5s | 1.5s | **70%** | -| 二次加载 | 2s | <500ms | **75%** | - ---- - -### Q9: 如何保证消息顺序? - -**A**: 使用时间戳 + 序列号双重保证。 - -```typescript -interface Message { - id: number // 服务器消息ID - clientId: string // 客户端临时ID (nanoid) - sessionId: number - content: string - wechatTime: number // 微信时间戳(毫秒) - createTime: string // 服务器时间 ISO8601 - sequence: number // 序列号(会话内递增) - status: 'sending' | 'success' | 'failed' -} - -// 排序逻辑 -const sortMessages = (messages: Message[]) => { - return messages.sort((a, b) => { - // 1. 优先按序列号排序(同一会话内严格递增) - if (a.sequence !== b.sequence) { - return a.sequence - b.sequence - } - - // 2. 序列号相同(罕见)则按时间戳 - if (a.wechatTime !== b.wechatTime) { - return a.wechatTime - b.wechatTime - } - - // 3. 时间戳也相同(极罕见)则按ID - return a.id - b.id - }) -} - -// 插入新消息 -const insertMessage = async (newMessage: Message) => { - // 1. 计算序列号 - const lastMessage = messages.value[messages.value.length - 1] - newMessage.sequence = (lastMessage?.sequence || 0) + 1 - - // 2. 插入到正确位置 - const insertIndex = messages.value.findIndex( - (m) => m.sequence > newMessage.sequence - ) - - if (insertIndex === -1) { - messages.value.push(newMessage) - } else { - messages.value.splice(insertIndex, 0, newMessage) - } - - // 3. 保存到数据库 - await MessageManager.addMessage(newMessage) -} -``` - ---- - -### Q10: WebSocket收到新好友的消息怎么处理? - -**A**: 先检查会话是否存在,不存在则先获取好友详情,再创建会话。 - -**完整流程**: - -```typescript -// 场景:收到一个陌生好友的首条消息 -WebSocket 消息: -{ - sessionId: 999, // 本地没有这个会话 - sessionType: 'friend', - content: '你好', - wechatTime: 1736758800000 -} - -处理步骤: -1. 检查会话是否存在 - await db().sessions.get(999) // 返回 undefined - -2. 会话不存在 → 请求好友详情接口 - const friendInfo = await getFriendDetail({ friendId: 999 }) - -3. 获取到完整信息 - { - id: 999, - nickname: '张三', - avatar: 'https://cdn.example.com/avatar/999.jpg', - conRemark: '张总', - wechatId: 'wxid_xxx', - // ... 其他信息 - } - -4. 创建新会话并保存 - const newSession = { - id: 999, - nickname: '张三', - conRemark: '张总', - avatar: 'https://...', - content: '你好', - config: { unreadCount: 1, ... } - } - await db().sessions.put(newSession) - -5. 更新 UI - SessionManager.triggerCallbacks() - → 会话列表自动显示新会话 -``` - -**降级方案(接口失败时)**: - -```typescript -// 如果获取好友详情失败,创建临时会话 -try { - const friendInfo = await getFriendDetail({ friendId: 999 }) - // 正常创建会话... -} catch (error) { - // 降级方案:创建临时会话 - const tempSession = { - id: 999, - nickname: '未知用户', - avatar: '/default-avatar.png', - content: '你好', - config: { unreadCount: 1, ... } - } - await db().sessions.put(tempSession) - - // 后台继续重试(3次,5s/10s/15s间隔) - retryGetContactInfo(999, 'friend') -} -``` - -**重试机制**: - -```typescript -// 指数退避重试 -private static async retryGetContactInfo( - sessionId: number, - sessionType: 'friend' | 'group', - wechatAccountId?: number -) { - let retryCount = 0 - const maxRetries = 3 - - while (retryCount < maxRetries) { - try { - // 5秒、10秒、15秒 - await new Promise(resolve => - setTimeout(resolve, 5000 * (retryCount + 1)) - ) - - const contactInfo = sessionType === 'friend' - ? await getFriendDetail({ friendId: sessionId }) - : await getGroupDetail({ groupId: sessionId }) - - // 更新会话详情 - await db().sessions.update(sessionId, { - nickname: contactInfo.nickname, - conRemark: contactInfo.conRemark, - avatar: contactInfo.avatar - }) - - console.log('✅ 已更新会话详情') - break // 成功则退出 - } catch (error) { - retryCount++ - if (retryCount >= maxRetries) { - // 记录到 Sentry - Sentry.captureException(error) - } - } - } -} -``` - -**用户体验**: - -``` -时间轴: -0ms 收到消息 - → 检测会话不存在 - → 请求好友详情接口 - -100ms 接口返回成功 - → 创建新会话 - → 显示在列表顶部 - → 用户看到 "张三: 你好" - -或 - -0ms 收到消息 - → 检测会话不存在 - → 请求好友详情接口 - -3000ms 接口超时/失败 - → 创建临时会话 - → 显示在列表顶部 - → 用户看到 "未知用户: 你好" - -8000ms 后台重试成功 - → 更新会话信息 - → 用户看到 "张三: 你好" -``` - -**关键点**: - -- ✅ 不会因为接口失败而丢消息 -- ✅ 降级方案保证基本可用 -- ✅ 后台重试提升用户体验 -- ✅ 错误监控快速定位问题 - ---- - -### Q11: 如何实现已读回执? - -**A**: 使用可见性检测 + WebSocket通知。 - -```typescript -// 1. 监听消息可见性 -import { useIntersectionObserver } from '@vueuse/core' - -const { stop } = useIntersectionObserver( - messageRef, - ([{ isIntersecting }]) => { - if (isIntersecting && message.value.status === 'unread') { - // 消息进入可见区域,标记为已读 - markAsRead(message.value.id) - } - }, - { - threshold: 0.5, // 50%可见时触发 - } -) - -// 2. 批量标记已读(防抖) -import { debounce } from 'lodash-es' - -const markAsRead = debounce(async (messageIds: number[]) => { - // ① 更新本地状态 - messages.value.forEach(m => { - if (messageIds.includes(m.id)) { - m.status = 'read' - } - }) - - // ② 更新数据库 - await db().messages.bulkUpdate( - messageIds.map(id => ({ id, status: 'read' })) - ) - - // ③ 通知服务器 - await markMessagesAsRead({ messageIds }) - - // ④ WebSocket 通知发送方 - ws.send(JSON.stringify({ - type: 'message_read', - data: { messageIds, sessionId } - })) -}, 500) // 500ms防抖 - -// 3. 接收已读回执 -ws.onmessage = (event) => { - const { type, data } = JSON.parse(event.data) - - if (type === 'message_read') { - // 更新消息状态 - messages.value.forEach(m => { - if (data.messageIds.includes(m.id)) { - m.readBy = data.readBy // ['user1', 'user2'] - m.readAt = data.readAt - } - }) - } -} - -// 4. 显示已读状态 - - - - - 发送中 - - - - 发送失败 - - - - 已读 - - - - 已送达 - - - -``` - ---- - -## 十四、开发工具与最佳实践 - -### 14.1 推荐的开发工具 - -```typescript -// VSCode 插件推荐 -{ - "recommendations": [ - "Vue.volar", // Vue 3 官方支持 - "dbaeumer.vscode-eslint", // ESLint - "esbenp.prettier-vscode", // 代码格式化 - "lokalise.i18n-ally", // 国际化 - "antfu.iconify", // 图标预览 - "Vue.vscode-typescript-vue-plugin", // TS 支持 - "Perkovec.emoji", // Emoji 支持 - "christian-kohler.path-intellisense" // 路径智能提示 - ] -} -``` - -### 14.2 代码规范 - -```typescript -// .eslintrc.js -module.exports = { - extends: [ - 'plugin:vue/vue3-recommended', - '@vue/typescript/recommended', - 'prettier', - ], - rules: { - 'vue/multi-word-component-names': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - }, -} -``` - -### 14.3 Git 工作流 - -```bash -# 分支策略 -main # 生产环境 - ↑ -develop # 开发环境 - ↑ -feature/* # 功能分支 -bugfix/* # 修复分支 -hotfix/* # 紧急修复 - -# 提交规范 -feat: 新功能 -fix: 修复bug -docs: 文档更新 -style: 代码格式 -refactor: 重构 -perf: 性能优化 -test: 测试 -chore: 构建/工具 - -# 示例 -git commit -m "feat(chat): 添加消息加密功能" -git commit -m "fix(db): 修复 IndexedDB 写入失败问题" -``` - -### 14.4 性能监控最佳实践 - -```typescript -// 关键指标监控 -const monitorPerformance = () => { - // 1. FCP - 首次内容绘制 - const paint = performance.getEntriesByType('paint') - const fcp = paint.find((p) => p.name === 'first-contentful-paint') - - // 2. LCP - 最大内容绘制 - new PerformanceObserver((list) => { - const entries = list.getEntries() - const lcp = entries[entries.length - 1] - console.log('LCP:', lcp.renderTime || lcp.loadTime) - }).observe({ entryTypes: ['largest-contentful-paint'] }) - - // 3. FID - 首次输入延迟 - new PerformanceObserver((list) => { - const entries = list.getEntries() - entries.forEach((entry) => { - console.log('FID:', entry.processingStart - entry.startTime) - }) - }).observe({ entryTypes: ['first-input'] }) - - // 4. CLS - 累积布局偏移 - new PerformanceObserver((list) => { - let cls = 0 - list.getEntries().forEach((entry) => { - if (!entry.hadRecentInput) { - cls += entry.value - } - }) - console.log('CLS:', cls) - }).observe({ entryTypes: ['layout-shift'] }) -} -``` - ---- - -## 十五、参考资料 - -### 官方文档 - -- [Dexie.js 官方文档](https://dexie.org/) - IndexedDB 封装库 -- [IndexedDB API - MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API) - 浏览器数据库 -- [Pinia 官方文档](https://pinia.vuejs.org/) - Vue 状态管理 -- [WebSocket API - MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket) - 实时通信 -- [Vue 3 官方文档](https://cn.vuejs.org/) - 框架文档 -- [Element Plus](https://element-plus.org/) - UI 组件库 - -### 技术文章 - -- [IndexedDB 最佳实践](https://web.dev/indexeddb-best-practices/) -- [WebSocket 心跳机制](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications) -- [Service Worker 缓存策略](https://web.dev/service-worker-caching-and-http-caching/) -- [Web 性能优化](https://web.dev/fast/) - -### 开源项目参考 - -- [微信 Web 版](https://wx.qq.com/) - 界面参考 -- [Telegram Web](https://web.telegram.org/) - 架构参考 -- [Element Plus Admin](https://github.com/element-plus/element-plus-admin) - 后台模板 - -### 相关工具 - -- [Chrome DevTools](https://developer.chrome.com/docs/devtools/) - 调试工具 -- [Vue DevTools](https://devtools.vuejs.org/) - Vue 专用调试 -- [Sentry](https://sentry.io/) - 错误监控 -- [Lighthouse](https://developers.google.com/web/tools/lighthouse) - 性能测试 - ---- - -## 十六、附录 - -### A. 术语表 - -| 术语 | 说明 | -| ---------------- | ------------------------------------------ | -| IndexedDB | 浏览器提供的客户端存储数据库 | -| Dexie | IndexedDB 的封装库,提供更简洁的 API | -| WebSocket | 全双工通信协议,用于实时消息推送 | -| Pinia | Vue 3 官方推荐的状态管理库 | -| Service Worker | 浏览器后台线程,用于离线缓存和推送 | -| BroadcastChannel | 浏览器 API,用于同域多标签页通信 | -| LCP | Largest Contentful Paint,最大内容绘制时间 | -| FCP | First Contentful Paint,首次内容绘制时间 | - -### B. 项目检查清单 - -```markdown -## 开发阶段 - -- [ ] 数据库管理器实现 -- [ ] 会话管理器实现 -- [ ] WebSocket 连接管理 -- [ ] Pinia Store 集成 -- [ ] UI 组件开发 -- [ ] 消息处理逻辑 -- [ ] 防数据丢失机制 - -## 测试阶段 - -- [ ] 单元测试覆盖率 > 80% -- [ ] E2E 测试通过 -- [ ] 性能测试达标 -- [ ] 兼容性测试通过 -- [ ] 安全测试通过 -- [ ] 压力测试通过 - -## 上线阶段 - -- [ ] 代码审查完成 -- [ ] 文档更新完成 -- [ ] 部署脚本准备 -- [ ] 监控系统配置 -- [ ] 灰度发布计划 -- [ ] 回滚方案准备 -``` - -### C. 版本更新日志 - -```markdown -## v1.0.0 (2026-01-13) - -### 新功能 - -- ✨ 完成基础架构设计 -- ✨ 实现多账户数据隔离 -- ✨ 集成 WebSocket 实时通信 -- ✨ 实现会话列表缓存 - -### 优化 - -- ⚡ 优化首屏加载速度 -- ⚡ 实现骨架屏加载 -- ⚡ 添加虚拟滚动 - -### 修复 - -- 🐛 修复数据同步问题 -- 🐛 修复内存泄漏 - -## v0.9.0 (2026-01-01) - -### 新功能 - -- ✨ 初始化项目 -- ✨ 完成技术选型 -``` - ---- - -**文档维护者**: 开发团队 -**最后更新时间**: 2026-01-13 -**文档版本**: v1.0 -**项目状态**: 🚧 开发中 - ---- - -> **致谢** -> 感谢所有参与项目讨论和代码贡献的团队成员。 -> 本方案参考了微信、Telegram 等优秀产品的设计理念。 - ---- - -**联系方式** -如有问题或建议,请提交 Issue 或联系开发团队。 diff --git a/TouchVueThree/联系人列表迁移完成报告.md b/TouchVueThree/联系人列表迁移完成报告.md deleted file mode 100644 index 742737b..0000000 --- a/TouchVueThree/联系人列表迁移完成报告.md +++ /dev/null @@ -1,237 +0,0 @@ -# 联系人列表迁移完成报告 - -## 📋 迁移概述 - -本次迁移将旧项目(React + Zustand)的联系人列表功能完整迁移到新项目(Vue3 + Pinia),实现了分组懒加载、API搜索、右键菜单等核心功能。 - -## ✅ 已完成功能 - -### 1. 类型定义完善 ✅ -- ✅ 补充 `ContactGroup` 类型(包含 groupName, groupType, count, sort, groupMemo) -- ✅ 新增 `GroupContactData` 类型(分组联系人数据结构) -- ✅ 在 `Contact` 类型中添加 `groupId` 字段 - -### 2. API 接口完善 ✅ -- ✅ `updateFriendInfo` - 更新好友信息(包含备注) -- ✅ `getContactGroups` - 获取分组列表(支持账号筛选) -- ✅ `getFriendList` - 支持 groupId 和 keyword 参数 -- ✅ `getGroupList` - 新增支持分组和搜索的接口 - -### 3. Store 重构 ✅ -- ✅ **分组懒加载架构** - - 分组列表缓存机制 - - 分组联系人懒加载(点击展开时加载) - - 分组联系人分页加载 - - 缓存策略(1小时TTL) - -- ✅ **搜索功能** - - API搜索(并行请求好友和群列表) - - 搜索防抖处理 - - 搜索结果管理 - -- ✅ **账号切换** - - 支持多账号切换 - - 自动重新加载展开的分组 - -- ✅ **联系人操作** - - 修改备注 - - 移动分组 - - 更新联系人信息 - - 更新AI类型 - -- ✅ **分组管理** - - 新增分组 - - 编辑分组 - - 删除分组 - - 分组排序 - -### 4. 组件实现 ✅ - -#### ContactList 主组件 -- ✅ 分组列表展示 -- ✅ 分组展开/折叠 -- ✅ 联系人列表展示 -- ✅ 搜索模式切换 -- ✅ 加载状态管理 -- ✅ 空状态处理 - -#### ContactContextMenu 联系人右键菜单 -- ✅ 修改备注功能 -- ✅ 移动分组功能 -- ✅ 弹窗表单验证 - -#### GroupContextMenu 分组右键菜单 -- ✅ 新增分组 -- ✅ 编辑分组(默认分组不可编辑) -- ✅ 删除分组(默认分组不可删除) - -#### GroupFormDialog 分组表单弹窗 -- ✅ 新增分组表单 -- ✅ 编辑分组表单 -- ✅ 表单验证 - -#### DeleteGroupDialog 删除确认弹窗 -- ✅ 删除确认提示 -- ✅ 危险操作标识 - -## 📁 文件结构 - -``` -TouchVueThree/src/ -├── types/ -│ └── wechat.ts # 类型定义(已更新) -├── api/ -│ └── modules/ -│ └── wechat.ts # API接口(已更新) -├── stores/ -│ └── modules/ -│ └── wechat/ -│ └── useContactStore.ts # 联系人Store(已重构) -└── views/ - └── Chat/ - └── components/ - └── SidebarMenu/ - └── ContactList/ - ├── index.vue # 主组件 - └── components/ - ├── ContactContextMenu.vue # 联系人右键菜单 - ├── GroupContextMenu.vue # 分组右键菜单 - ├── GroupFormDialog.vue # 分组表单弹窗 - └── DeleteGroupDialog.vue # 删除确认弹窗 -``` - -## 🎯 核心特性 - -### 1. 分组懒加载 -- **性能优化**:只加载展开的分组联系人,减少初始加载时间 -- **缓存机制**:本地缓存分组列表和第一页联系人数据 -- **分页加载**:支持滚动加载更多联系人 - -### 2. API搜索 -- **并行请求**:同时请求好友和群列表,提升搜索速度 -- **实时搜索**:输入关键词后自动搜索 -- **搜索结果管理**:独立的搜索结果状态管理 - -### 3. 右键菜单 -- **联系人菜单**:修改备注、移动分组 -- **分组菜单**:新增、编辑、删除分组 -- **权限控制**:默认分组不可编辑/删除 - -### 4. 账号切换 -- **自动同步**:切换账号时自动重新加载数据 -- **状态保持**:保持展开状态,重新加载数据 - -## 🔧 技术实现 - -### 缓存策略 -```typescript -// 缓存键格式 -groups_{accountId} // 分组列表 -groupContacts_{groupId}_{groupType}_{accountId} // 分组联系人 - -// 缓存TTL -CACHE_TTL = 60 * 60 * 1000 // 1小时 -``` - -### 分组Key生成 -```typescript -function getGroupKey(groupId: number, groupType: 1 | 2, accountId: number): string { - return `${groupId}_${groupType}_${accountId}` -} -``` - -### 数据流 -``` -用户操作 - ↓ -组件事件 - ↓ -Store Actions - ↓ -API调用 - ↓ -更新Store状态 - ↓ -组件响应式更新 -``` - -## 📝 使用示例 - -### 加载分组列表 -```typescript -const contactStore = useContactStore() -await contactStore.loadGroups(accountId) -``` - -### 切换分组展开/折叠 -```typescript -await contactStore.toggleGroup(groupId, groupType) -``` - -### 搜索联系人 -```typescript -await contactStore.searchContacts(keyword) -``` - -### 修改备注 -```typescript -await contactStore.updateContactRemark(contactId, groupId, groupType, remark) -``` - -### 移动分组 -```typescript -await contactStore.moveContactToGroup(contactId, fromGroupId, toGroupId, groupType) -``` - -## ⚠️ 注意事项 - -1. **groupId 字段**:联系人数据需要包含 `groupId` 字段,用于移动分组功能 -2. **API响应格式**:确保API返回的数据格式符合预期(list/data字段) -3. **默认分组**:id为0的分组为默认分组,不可编辑/删除 -4. **账号切换**:切换账号时会清空当前展开状态,需要重新展开分组 - -## 🐛 已知问题 - -1. **联系人groupId获取**:部分API可能不返回groupId,需要后端支持 -2. **缓存失效**:WebSocket更新联系人时,需要手动清除相关缓存 -3. **虚拟滚动**:当前未实现虚拟滚动,大数据量时可能有性能问题 - -## 🚀 后续优化建议 - -1. **虚拟滚动**:实现虚拟滚动以支持大量联系人 -2. **WebSocket同步**:监听WebSocket消息,实时更新联系人数据 -3. **IndexedDB缓存**:使用IndexedDB替代localStorage,支持更大数据量 -4. **性能监控**:添加性能监控,优化加载速度 -5. **错误处理**:完善错误处理和重试机制 - -## ✅ 测试清单 - -- [ ] 分组列表加载正常 -- [ ] 分组展开/折叠正常 -- [ ] 联系人懒加载正常 -- [ ] 分页加载更多正常 -- [ ] 搜索功能正常 -- [ ] 修改备注功能正常 -- [ ] 移动分组功能正常 -- [ ] 新增分组功能正常 -- [ ] 编辑分组功能正常 -- [ ] 删除分组功能正常 -- [ ] 账号切换正常 -- [ ] 缓存机制正常 - -## 📅 迁移时间 - -- **开始时间**:2026-01-13 -- **完成时间**:2026-01-13 -- **总耗时**:约2小时 - -## 👥 参与人员 - -- 开发:AI Assistant -- 审核:待定 - ---- - -**迁移状态**:✅ 已完成 -**代码质量**:✅ 通过Linter检查 -**功能完整性**:✅ 核心功能已实现 diff --git a/TouchVueThree/静态/index.html b/TouchVueThree/静态/index.html deleted file mode 100644 index a722a92..0000000 --- a/TouchVueThree/静态/index.html +++ /dev/null @@ -1,803 +0,0 @@ - - - - - - 触客宝 Pro - AI聚合工作台 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 商务号01 (在线) - - - - - - - - 销售小王 - - - - - - - - - - - - - - - - - - - 消息 - - - - - - - - - 全部 - - - 未读 (12) - - - AI托管 - - - - - - - - - - - - - - - - 大野(朋友圈业务) - - 11:45 - - - AI - - 已帮您生成3条回复建议... - - - - - - - - - - - - - 5 - - - - - - 京东内购群 - - 10:20 - - - 张三: 谁有新的优惠券链接? - - - - - - - - - - - - - VIP客户-李总 - - 昨天 - - - 好的,合同我让财务看一下。 - - - - - - - - - - - - - - - 大野(朋友圈业务) - - - - 微信在线 - - - - - - - - - - 发朋友圈 - - - - 同步 - - - - - - - - - - AI 算力 - 12,450 - - - - - - - 预计可用: 3天 - - 立即充值 - - - - - - - - - - - - - - 今天 10:23 - - - - - - 大野 - - 老板,上次说的那个魔兽代练套餐,如果是老客户的话,还能再送一次幻化吗? - - - - - - - - - - - - AI 建议回复 (消耗 5 算力) - - - 一键发送 - - - - - "亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。" - - - - - - - - - - - - - - - - - - 客户资料 - - - 话术/快捷 - - - - - - - - - - - - - - - 大野 - ID: ys666wow - - - - - - 用户标签 - - - 高意向 - 魔兽玩家 - - + - - - - - - - - 备注名 - - - - 手机号 - - 138****8888 - - - - - 跟进记录 - - 上次提到需要发票,记得确认公司抬头。 - - - - - - - - - - - - - - - - 开场白 - 通用 - - - 您好,很高兴为您服务!请问有什么可以帮您的吗?我们目前正在进行... - - - - - 价格异议处理 - - - 亲,这个价格已经是我们的底价了,包含了一年的售后服务,性价比非常高... - - - - - - - - - - - diff --git a/TouchVueThree/静态/index2.html b/TouchVueThree/静态/index2.html deleted file mode 100644 index c49668f..0000000 --- a/TouchVueThree/静态/index2.html +++ /dev/null @@ -1,640 +0,0 @@ - - - - - - 触客宝 Pro - 全端自适应版 - - - - - - - - - - - - - - - - - - - - - 消息 - - - - - - - - - - - - - 全部 - 未读 (12) - - AI托管 - - - - - - - - - - - - AI - - - - - - 大野(朋友圈业务) - - 11:45 - - - 已为您生成 3 条回复建议... - - - - - - - - - 99 - - - - - - 京东内购福利群 - - 10:20 - - - 张三: [图片] 谁有这个券? - - - - - - - - - - - - - - - - - - - 大野 - 在线 - - 微信在线 - - - - - - - - 算力 - 12,450 - - - - - - - - - - - - - - - - 今天 10:23 - - - - - - - 大野 - - 老板,上次说的那个魔兽代练套餐,如果是老客户的话,还能再送一次幻化吗? - - - - - - - - - - AI 建议回复 - - -5 算力 - - - “亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。” - - - - 忽略 - - - 发送 - - - - - - - - - - - - - - - - - - AI - - - - - - - - - - 发送 - - - - - - - - - - - - - - - - 消息 - - - - 通讯录 - - - - 数据 - - - - 我的 - - - - - - - diff --git a/old/devlop.py b/devlop.py similarity index 100% rename from old/devlop.py rename to devlop.py diff --git a/old/emoji-mapping.json b/emoji-mapping.json similarity index 100% rename from old/emoji-mapping.json rename to emoji-mapping.json diff --git a/old/favicon.ico b/favicon.ico similarity index 100% rename from old/favicon.ico rename to favicon.ico diff --git a/old/index.html b/index.html similarity index 100% rename from old/index.html rename to index.html diff --git a/old/.env.development b/old/.env.development deleted file mode 100644 index 7a2a571..0000000 --- a/old/.env.development +++ /dev/null @@ -1,6 +0,0 @@ -# 基础环境变量示例 -VITE_API_BASE_URL=http://www.yishi.com -VITE_API_BASE_URL2=https://s2.siyuguanli.com:9991 -VITE_API_WS_URL=wss://s2.siyuguanli.com:9993 -# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com -VITE_APP_TITLE=存客宝 diff --git a/old/.env.production b/old/.env.production deleted file mode 100644 index 430f2f3..0000000 --- a/old/.env.production +++ /dev/null @@ -1,6 +0,0 @@ -# 基础环境变量示例 -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com -VITE_API_BASE_URL2=https://s2.siyuguanli.com:9991 -VITE_API_WS_URL=wss://s2.siyuguanli.com:9993 -# VITE_API_BASE_URL=http://www.yishi.com -VITE_APP_TITLE=存客宝 diff --git a/old/public/assets/animal/jump.png b/old/public/assets/animal/jump.png deleted file mode 100644 index ae1307a..0000000 Binary files a/old/public/assets/animal/jump.png and /dev/null differ diff --git a/old/public/assets/animal/pig-head.png b/old/public/assets/animal/pig-head.png deleted file mode 100644 index c14749e..0000000 Binary files a/old/public/assets/animal/pig-head.png and /dev/null differ diff --git a/old/public/assets/animal/shiver.png b/old/public/assets/animal/shiver.png deleted file mode 100644 index 2bc6cf2..0000000 Binary files a/old/public/assets/animal/shiver.png and /dev/null differ diff --git a/old/public/assets/animal/spin.png b/old/public/assets/animal/spin.png deleted file mode 100644 index 7aa3b5a..0000000 Binary files a/old/public/assets/animal/spin.png and /dev/null differ diff --git a/old/public/assets/blessing/blessing.png b/old/public/assets/blessing/blessing.png deleted file mode 100644 index d4a5b17..0000000 Binary files a/old/public/assets/blessing/blessing.png and /dev/null differ diff --git a/old/public/assets/blessing/celebrate.png b/old/public/assets/blessing/celebrate.png deleted file mode 100644 index 6b3c639..0000000 Binary files a/old/public/assets/blessing/celebrate.png and /dev/null differ diff --git a/old/public/assets/blessing/firecrackers.png b/old/public/assets/blessing/firecrackers.png deleted file mode 100644 index 52ac8fb..0000000 Binary files a/old/public/assets/blessing/firecrackers.png and /dev/null differ diff --git a/old/public/assets/blessing/fireworks.png b/old/public/assets/blessing/fireworks.png deleted file mode 100644 index 58d2848..0000000 Binary files a/old/public/assets/blessing/fireworks.png and /dev/null differ diff --git a/old/public/assets/blessing/fortune.png b/old/public/assets/blessing/fortune.png deleted file mode 100644 index 2af454c..0000000 Binary files a/old/public/assets/blessing/fortune.png and /dev/null differ diff --git a/old/public/assets/blessing/gift.png b/old/public/assets/blessing/gift.png deleted file mode 100644 index 6082fba..0000000 Binary files a/old/public/assets/blessing/gift.png and /dev/null differ diff --git a/old/public/assets/blessing/red-envelope.png b/old/public/assets/blessing/red-envelope.png deleted file mode 100644 index 28b9698..0000000 Binary files a/old/public/assets/blessing/red-envelope.png and /dev/null differ diff --git a/old/public/assets/face/666.png b/old/public/assets/face/666.png deleted file mode 100644 index f947dc6..0000000 Binary files a/old/public/assets/face/666.png and /dev/null differ diff --git a/old/public/assets/face/Emm.png b/old/public/assets/face/Emm.png deleted file mode 100644 index 8fb9370..0000000 Binary files a/old/public/assets/face/Emm.png and /dev/null differ diff --git a/old/public/assets/face/about-to-cry.png b/old/public/assets/face/about-to-cry.png deleted file mode 100644 index f0558ba..0000000 Binary files a/old/public/assets/face/about-to-cry.png and /dev/null differ diff --git a/old/public/assets/face/angry.png b/old/public/assets/face/angry.png deleted file mode 100644 index c8cba00..0000000 Binary files a/old/public/assets/face/angry.png and /dev/null differ diff --git a/old/public/assets/face/arrogant.png b/old/public/assets/face/arrogant.png deleted file mode 100644 index cfd4cf5..0000000 Binary files a/old/public/assets/face/arrogant.png and /dev/null differ diff --git a/old/public/assets/face/awkward.png b/old/public/assets/face/awkward.png deleted file mode 100644 index 6d4900e..0000000 Binary files a/old/public/assets/face/awkward.png and /dev/null differ diff --git a/old/public/assets/face/bitter.png b/old/public/assets/face/bitter.png deleted file mode 100644 index bf0dd64..0000000 Binary files a/old/public/assets/face/bitter.png and /dev/null differ diff --git a/old/public/assets/face/blush.png b/old/public/assets/face/blush.png deleted file mode 100644 index 92ba41d..0000000 Binary files a/old/public/assets/face/blush.png and /dev/null differ diff --git a/old/public/assets/face/cheer-up.png b/old/public/assets/face/cheer-up.png deleted file mode 100644 index 911ca55..0000000 Binary files a/old/public/assets/face/cheer-up.png and /dev/null differ diff --git a/old/public/assets/face/clap.png b/old/public/assets/face/clap.png deleted file mode 100644 index c6963bc..0000000 Binary files a/old/public/assets/face/clap.png and /dev/null differ diff --git a/old/public/assets/face/crack.png b/old/public/assets/face/crack.png deleted file mode 100644 index 626d479..0000000 Binary files a/old/public/assets/face/crack.png and /dev/null differ diff --git a/old/public/assets/face/crazy.png b/old/public/assets/face/crazy.png deleted file mode 100644 index b440837..0000000 Binary files a/old/public/assets/face/crazy.png and /dev/null differ diff --git a/old/public/assets/face/crying.png b/old/public/assets/face/crying.png deleted file mode 100644 index bdfe6fc..0000000 Binary files a/old/public/assets/face/crying.png and /dev/null differ diff --git a/old/public/assets/face/curse.png b/old/public/assets/face/curse.png deleted file mode 100644 index 0a8c0bf..0000000 Binary files a/old/public/assets/face/curse.png and /dev/null differ diff --git a/old/public/assets/face/daze.png b/old/public/assets/face/daze.png deleted file mode 100644 index e6e388f..0000000 Binary files a/old/public/assets/face/daze.png and /dev/null differ diff --git a/old/public/assets/face/despise.png b/old/public/assets/face/despise.png deleted file mode 100644 index 3d32430..0000000 Binary files a/old/public/assets/face/despise.png and /dev/null differ diff --git a/old/public/assets/face/disappointed.png b/old/public/assets/face/disappointed.png deleted file mode 100644 index d38c888..0000000 Binary files a/old/public/assets/face/disappointed.png and /dev/null differ diff --git a/old/public/assets/face/dizzy.png b/old/public/assets/face/dizzy.png deleted file mode 100644 index 8b0a5a2..0000000 Binary files a/old/public/assets/face/dizzy.png and /dev/null differ diff --git a/old/public/assets/face/doge.png b/old/public/assets/face/doge.png deleted file mode 100644 index 02000fb..0000000 Binary files a/old/public/assets/face/doge.png and /dev/null differ diff --git a/old/public/assets/face/eat-melon.png b/old/public/assets/face/eat-melon.png deleted file mode 100644 index 64f72ad..0000000 Binary files a/old/public/assets/face/eat-melon.png and /dev/null differ diff --git a/old/public/assets/face/embarrassed.png b/old/public/assets/face/embarrassed.png deleted file mode 100644 index fc7fbfc..0000000 Binary files a/old/public/assets/face/embarrassed.png and /dev/null differ diff --git a/old/public/assets/face/evil-smile.png b/old/public/assets/face/evil-smile.png deleted file mode 100644 index 8585d82..0000000 Binary files a/old/public/assets/face/evil-smile.png and /dev/null differ diff --git a/old/public/assets/face/eye-roll.png b/old/public/assets/face/eye-roll.png deleted file mode 100644 index 4ebf05d..0000000 Binary files a/old/public/assets/face/eye-roll.png and /dev/null differ diff --git a/old/public/assets/face/facepalm.png b/old/public/assets/face/facepalm.png deleted file mode 100644 index ea8a13c..0000000 Binary files a/old/public/assets/face/facepalm.png and /dev/null differ diff --git a/old/public/assets/face/fear.png b/old/public/assets/face/fear.png deleted file mode 100644 index 6e935b2..0000000 Binary files a/old/public/assets/face/fear.png and /dev/null differ diff --git a/old/public/assets/face/frown.png b/old/public/assets/face/frown.png deleted file mode 100644 index 123bf08..0000000 Binary files a/old/public/assets/face/frown.png and /dev/null differ diff --git a/old/public/assets/face/good.png b/old/public/assets/face/good.png deleted file mode 100644 index 7005ce9..0000000 Binary files a/old/public/assets/face/good.png and /dev/null differ diff --git a/old/public/assets/face/goodbye.png b/old/public/assets/face/goodbye.png deleted file mode 100644 index 8ef994b..0000000 Binary files a/old/public/assets/face/goodbye.png and /dev/null differ diff --git a/old/public/assets/face/grin.png b/old/public/assets/face/grin.png deleted file mode 100644 index abce5e0..0000000 Binary files a/old/public/assets/face/grin.png and /dev/null differ diff --git a/old/public/assets/face/happy.png b/old/public/assets/face/happy.png deleted file mode 100644 index a78c8e6..0000000 Binary files a/old/public/assets/face/happy.png and /dev/null differ diff --git a/old/public/assets/face/hey-ha.png b/old/public/assets/face/hey-ha.png deleted file mode 100644 index d424a3f..0000000 Binary files a/old/public/assets/face/hey-ha.png and /dev/null differ diff --git a/old/public/assets/face/kiss.png b/old/public/assets/face/kiss.png deleted file mode 100644 index 3190fa3..0000000 Binary files a/old/public/assets/face/kiss.png and /dev/null differ diff --git a/old/public/assets/face/knock.png b/old/public/assets/face/knock.png deleted file mode 100644 index 5eb4480..0000000 Binary files a/old/public/assets/face/knock.png and /dev/null differ diff --git a/old/public/assets/face/leisurely.png b/old/public/assets/face/leisurely.png deleted file mode 100644 index dd19c44..0000000 Binary files a/old/public/assets/face/leisurely.png and /dev/null differ diff --git a/old/public/assets/face/let-me-see.png b/old/public/assets/face/let-me-see.png deleted file mode 100644 index c3fafee..0000000 Binary files a/old/public/assets/face/let-me-see.png and /dev/null differ diff --git a/old/public/assets/face/lustful.png b/old/public/assets/face/lustful.png deleted file mode 100644 index 630ae0e..0000000 Binary files a/old/public/assets/face/lustful.png and /dev/null differ diff --git a/old/public/assets/face/naughty.png b/old/public/assets/face/naughty.png deleted file mode 100644 index 5aba419..0000000 Binary files a/old/public/assets/face/naughty.png and /dev/null differ diff --git a/old/public/assets/face/oh-my.png b/old/public/assets/face/oh-my.png deleted file mode 100644 index 67d1b97..0000000 Binary files a/old/public/assets/face/oh-my.png and /dev/null differ diff --git a/old/public/assets/face/panic.png b/old/public/assets/face/panic.png deleted file mode 100644 index 8bfda02..0000000 Binary files a/old/public/assets/face/panic.png and /dev/null differ diff --git a/old/public/assets/face/pick-nose.png b/old/public/assets/face/pick-nose.png deleted file mode 100644 index e44adf6..0000000 Binary files a/old/public/assets/face/pick-nose.png and /dev/null differ diff --git a/old/public/assets/face/pitiful.png b/old/public/assets/face/pitiful.png deleted file mode 100644 index 1e75cbb..0000000 Binary files a/old/public/assets/face/pitiful.png and /dev/null differ diff --git a/old/public/assets/face/pout.png b/old/public/assets/face/pout.png deleted file mode 100644 index 937ae74..0000000 Binary files a/old/public/assets/face/pout.png and /dev/null differ diff --git a/old/public/assets/face/question.png b/old/public/assets/face/question.png deleted file mode 100644 index c2bb9c9..0000000 Binary files a/old/public/assets/face/question.png and /dev/null differ diff --git a/old/public/assets/face/right-hum.png b/old/public/assets/face/right-hum.png deleted file mode 100644 index e466e2a..0000000 Binary files a/old/public/assets/face/right-hum.png and /dev/null differ diff --git a/old/public/assets/face/roll-eyes.png b/old/public/assets/face/roll-eyes.png deleted file mode 100644 index fa261a4..0000000 Binary files a/old/public/assets/face/roll-eyes.png and /dev/null differ diff --git a/old/public/assets/face/sad.png b/old/public/assets/face/sad.png deleted file mode 100644 index 636ae7b..0000000 Binary files a/old/public/assets/face/sad.png and /dev/null differ diff --git a/old/public/assets/face/shush.png b/old/public/assets/face/shush.png deleted file mode 100644 index a670056..0000000 Binary files a/old/public/assets/face/shush.png and /dev/null differ diff --git a/old/public/assets/face/shut-up.png b/old/public/assets/face/shut-up.png deleted file mode 100644 index d89f350..0000000 Binary files a/old/public/assets/face/shut-up.png and /dev/null differ diff --git a/old/public/assets/face/shy.png b/old/public/assets/face/shy.png deleted file mode 100644 index 50ec786..0000000 Binary files a/old/public/assets/face/shy.png and /dev/null differ diff --git a/old/public/assets/face/sick.png b/old/public/assets/face/sick.png deleted file mode 100644 index 39fdd91..0000000 Binary files a/old/public/assets/face/sick.png and /dev/null differ diff --git a/old/public/assets/face/sigh.png b/old/public/assets/face/sigh.png deleted file mode 100644 index 2840584..0000000 Binary files a/old/public/assets/face/sigh.png and /dev/null differ diff --git a/old/public/assets/face/silly-smile.png b/old/public/assets/face/silly-smile.png deleted file mode 100644 index b554894..0000000 Binary files a/old/public/assets/face/silly-smile.png and /dev/null differ diff --git a/old/public/assets/face/sinister.png b/old/public/assets/face/sinister.png deleted file mode 100644 index d37f39c..0000000 Binary files a/old/public/assets/face/sinister.png and /dev/null differ diff --git a/old/public/assets/face/skull.png b/old/public/assets/face/skull.png deleted file mode 100644 index f883579..0000000 Binary files a/old/public/assets/face/skull.png and /dev/null differ diff --git a/old/public/assets/face/slap-face.png b/old/public/assets/face/slap-face.png deleted file mode 100644 index d12031a..0000000 Binary files a/old/public/assets/face/slap-face.png and /dev/null differ diff --git a/old/public/assets/face/sleep.png b/old/public/assets/face/sleep.png deleted file mode 100644 index 8b37848..0000000 Binary files a/old/public/assets/face/sleep.png and /dev/null differ diff --git a/old/public/assets/face/sleepy.png b/old/public/assets/face/sleepy.png deleted file mode 100644 index 148c66f..0000000 Binary files a/old/public/assets/face/sleepy.png and /dev/null differ diff --git a/old/public/assets/face/sly-smile.png b/old/public/assets/face/sly-smile.png deleted file mode 100644 index 895c591..0000000 Binary files a/old/public/assets/face/sly-smile.png and /dev/null differ diff --git a/old/public/assets/face/smart.png b/old/public/assets/face/smart.png deleted file mode 100644 index 999d4b5..0000000 Binary files a/old/public/assets/face/smart.png and /dev/null differ diff --git a/old/public/assets/face/smile.png b/old/public/assets/face/smile.png deleted file mode 100644 index a3195d6..0000000 Binary files a/old/public/assets/face/smile.png and /dev/null differ diff --git a/old/public/assets/face/smiley.png b/old/public/assets/face/smiley.png deleted file mode 100644 index e3ac78d..0000000 Binary files a/old/public/assets/face/smiley.png and /dev/null differ diff --git a/old/public/assets/face/smug.png b/old/public/assets/face/smug.png deleted file mode 100644 index 0bac94b..0000000 Binary files a/old/public/assets/face/smug.png and /dev/null differ diff --git a/old/public/assets/face/snicker.png b/old/public/assets/face/snicker.png deleted file mode 100644 index 51836d1..0000000 Binary files a/old/public/assets/face/snicker.png and /dev/null differ diff --git a/old/public/assets/face/social.png b/old/public/assets/face/social.png deleted file mode 100644 index e4a311b..0000000 Binary files a/old/public/assets/face/social.png and /dev/null differ diff --git a/old/public/assets/face/speechless.png b/old/public/assets/face/speechless.png deleted file mode 100644 index 443f9d6..0000000 Binary files a/old/public/assets/face/speechless.png and /dev/null differ diff --git a/old/public/assets/face/surprised.png b/old/public/assets/face/surprised.png deleted file mode 100644 index 33feac5..0000000 Binary files a/old/public/assets/face/surprised.png and /dev/null differ diff --git a/old/public/assets/face/sweat.png b/old/public/assets/face/sweat.png deleted file mode 100644 index 3b940c5..0000000 Binary files a/old/public/assets/face/sweat.png and /dev/null differ diff --git a/old/public/assets/face/tears-to-smile.png b/old/public/assets/face/tears-to-smile.png deleted file mode 100644 index 447dcb6..0000000 Binary files a/old/public/assets/face/tears-to-smile.png and /dev/null differ diff --git a/old/public/assets/face/unlucky.png b/old/public/assets/face/unlucky.png deleted file mode 100644 index 47ed471..0000000 Binary files a/old/public/assets/face/unlucky.png and /dev/null differ diff --git a/old/public/assets/face/vomit.png b/old/public/assets/face/vomit.png deleted file mode 100644 index b3e072e..0000000 Binary files a/old/public/assets/face/vomit.png and /dev/null differ diff --git a/old/public/assets/face/wail.png b/old/public/assets/face/wail.png deleted file mode 100644 index 3c64886..0000000 Binary files a/old/public/assets/face/wail.png and /dev/null differ diff --git a/old/public/assets/face/wipe-sweat.png b/old/public/assets/face/wipe-sweat.png deleted file mode 100644 index b9256ad..0000000 Binary files a/old/public/assets/face/wipe-sweat.png and /dev/null differ diff --git a/old/public/assets/face/wow.png b/old/public/assets/face/wow.png deleted file mode 100644 index 5d1c179..0000000 Binary files a/old/public/assets/face/wow.png and /dev/null differ diff --git a/old/public/assets/face/wronged.png b/old/public/assets/face/wronged.png deleted file mode 100644 index b6491fd..0000000 Binary files a/old/public/assets/face/wronged.png and /dev/null differ diff --git a/old/public/assets/face/yeah.png b/old/public/assets/face/yeah.png deleted file mode 100644 index 969fd94..0000000 Binary files a/old/public/assets/face/yeah.png and /dev/null differ diff --git a/old/public/assets/gesture/OK.png b/old/public/assets/gesture/OK.png deleted file mode 100644 index 2d0f6e5..0000000 Binary files a/old/public/assets/gesture/OK.png and /dev/null differ diff --git a/old/public/assets/gesture/beckon.png b/old/public/assets/gesture/beckon.png deleted file mode 100644 index 28e3733..0000000 Binary files a/old/public/assets/gesture/beckon.png and /dev/null differ diff --git a/old/public/assets/gesture/fist-salute.png b/old/public/assets/gesture/fist-salute.png deleted file mode 100644 index 51d17db..0000000 Binary files a/old/public/assets/gesture/fist-salute.png and /dev/null differ diff --git a/old/public/assets/gesture/fist.png b/old/public/assets/gesture/fist.png deleted file mode 100644 index 91c10e0..0000000 Binary files a/old/public/assets/gesture/fist.png and /dev/null differ diff --git a/old/public/assets/gesture/handshake.png b/old/public/assets/gesture/handshake.png deleted file mode 100644 index 9e6be93..0000000 Binary files a/old/public/assets/gesture/handshake.png and /dev/null differ diff --git a/old/public/assets/gesture/hug.png b/old/public/assets/gesture/hug.png deleted file mode 100644 index 0bbcdb9..0000000 Binary files a/old/public/assets/gesture/hug.png and /dev/null differ diff --git a/old/public/assets/gesture/pray.png b/old/public/assets/gesture/pray.png deleted file mode 100644 index eca2b73..0000000 Binary files a/old/public/assets/gesture/pray.png and /dev/null differ diff --git a/old/public/assets/gesture/strong.png b/old/public/assets/gesture/strong.png deleted file mode 100644 index f81c624..0000000 Binary files a/old/public/assets/gesture/strong.png and /dev/null differ diff --git a/old/public/assets/gesture/victory.png b/old/public/assets/gesture/victory.png deleted file mode 100644 index 2a54535..0000000 Binary files a/old/public/assets/gesture/victory.png and /dev/null differ diff --git a/old/public/assets/gesture/weak.png b/old/public/assets/gesture/weak.png deleted file mode 100644 index be8b1a8..0000000 Binary files a/old/public/assets/gesture/weak.png and /dev/null differ diff --git a/old/public/assets/other/beer.png b/old/public/assets/other/beer.png deleted file mode 100644 index 81d40ba..0000000 Binary files a/old/public/assets/other/beer.png and /dev/null differ diff --git a/old/public/assets/other/bomb.png b/old/public/assets/other/bomb.png deleted file mode 100644 index 9ece24c..0000000 Binary files a/old/public/assets/other/bomb.png and /dev/null differ diff --git a/old/public/assets/other/broken-heart.png b/old/public/assets/other/broken-heart.png deleted file mode 100644 index dc23ec8..0000000 Binary files a/old/public/assets/other/broken-heart.png and /dev/null differ diff --git a/old/public/assets/other/cake.png b/old/public/assets/other/cake.png deleted file mode 100644 index 9a62501..0000000 Binary files a/old/public/assets/other/cake.png and /dev/null differ diff --git a/old/public/assets/other/coffee.png b/old/public/assets/other/coffee.png deleted file mode 100644 index 91b7c79..0000000 Binary files a/old/public/assets/other/coffee.png and /dev/null differ diff --git a/old/public/assets/other/heart.png b/old/public/assets/other/heart.png deleted file mode 100644 index aa9b744..0000000 Binary files a/old/public/assets/other/heart.png and /dev/null differ diff --git a/old/public/assets/other/knife.png b/old/public/assets/other/knife.png deleted file mode 100644 index 77c5a13..0000000 Binary files a/old/public/assets/other/knife.png and /dev/null differ diff --git a/old/public/assets/other/lips.png b/old/public/assets/other/lips.png deleted file mode 100644 index 858e854..0000000 Binary files a/old/public/assets/other/lips.png and /dev/null differ diff --git a/old/public/assets/other/moon.png b/old/public/assets/other/moon.png deleted file mode 100644 index 20ed34e..0000000 Binary files a/old/public/assets/other/moon.png and /dev/null differ diff --git a/old/public/assets/other/poop.png b/old/public/assets/other/poop.png deleted file mode 100644 index 9ee508f..0000000 Binary files a/old/public/assets/other/poop.png and /dev/null differ diff --git a/old/public/assets/other/rose.png b/old/public/assets/other/rose.png deleted file mode 100644 index 83cc3b2..0000000 Binary files a/old/public/assets/other/rose.png and /dev/null differ diff --git a/old/public/assets/other/sun.png b/old/public/assets/other/sun.png deleted file mode 100644 index 04578c4..0000000 Binary files a/old/public/assets/other/sun.png and /dev/null differ diff --git a/old/public/assets/other/wither.png b/old/public/assets/other/wither.png deleted file mode 100644 index b189bb9..0000000 Binary files a/old/public/assets/other/wither.png and /dev/null differ diff --git a/old/src/api/module/wechat.ts b/old/src/api/module/wechat.ts deleted file mode 100644 index c70d6a5..0000000 --- a/old/src/api/module/wechat.ts +++ /dev/null @@ -1,30 +0,0 @@ -import request from "@/api/request"; -//获取客服列表 -export function getCustomerList() { - return request("/v1/kefu/customerService/list", {}, "GET"); -} - -/** - * 数据处理接口(Socket消息传入数据中心) - */ -export interface DataProcessingParams { - /** - * 是否置顶 - */ - isTop: string; - /** - * 类型固定值 - */ - type?: string; - /** - * 公共 - */ - wechatAccountId?: string; - wechatChatroomId?: string; - wechatFriendId?: string; - [property: string]: any; -} - -export function dataProcessing(params: DataProcessingParams) { - return request("/v1/kefu/dataProcessing", params, "POST"); -} diff --git a/old/提示词/功能架构.md b/old/提示词/功能架构.md deleted file mode 100644 index 5d12750..0000000 --- a/old/提示词/功能架构.md +++ /dev/null @@ -1,495 +0,0 @@ -# 触客宝(Touchkebao)功能架构 - -> 本文档描述当前 Touchkebao 前端工程在 **页面路由、状态管理、数据访问、本地缓存和核心业务模块** 维度的功能架构,并附带 ASCII 线框图,便于沟通与扩展设计。 - -## 1. 技术栈与整体结构 - -- **基础框架** - - **React 18 + TypeScript**:单页应用(SPA) - - **Vite**:构建与开发服务器(入口 `src/main.tsx`) - - **UI 组件** - - PC 端:`antd` - - 移动端:`antd-mobile` - - **状态管理**:`zustand`(含自定义持久化封装) - - **请求与数据** - - HTTP:`axios` + 统一封装 `api/request.ts` - - 本地缓存:`Dexie`(IndexedDB,统一封装在 `utils/db.ts`) - - **监控与稳定性** - - `@sentry/react`:初始化于 `main.tsx`,并通过 `App.tsx` 中的 `ErrorBoundary` 做兜底 - - 自定义错误提示与 Toast(`antd-mobile`) - -- **入口初始化流程** - - `src/main.tsx` - - 初始化 Sentry(必须在其他逻辑前) - - 设置 `dayjs` 中文 - - 调用 `initializeDatabaseFromPersistedUser()`: - - 从持久化的 user-store 中恢复上次登录用户 - - 以 `userId` 为维度初始化 Dexie 数据库 - - 渲染根组件: - - `ConfigProvider`(antd 中文配置) - - `QueryProvider`(React Query 统一数据请求层) - - `App` - - `src/App.tsx` - - 使用 `Sentry.ErrorBoundary` 包裹整个应用 - - 内部渲染: - - `AppRouter`(统一路由系统) - - `UpdateNotification`(版本更新提醒,支持自动刷新) - ---- - -## 2. 路由 & 页面模块架构 - -路由集中于 `src/router` 目录,通过 `import.meta.glob` 动态加载模块。 - -### 2.1 路由整体 - -- **核心文件** - - `router/index.tsx` - - 使用 `import.meta.glob("./module/*.{ts,tsx}", { eager: true })` 自动导入所有路由模块 - - 聚合得到 `routes: RouteObject[]` - - 对 `auth: true` 的路由包装 `PermissionRoute` - - 追加通配符 `*` → `404` 页 - - 使用 `BrowserRouter + useRoutes` 渲染 - - `router/permissionRoute.tsx` - - 依赖 `useUserStore` 提供的 `user` 与 `isLoggedIn` - - 未登录时记录当前 `pathname + search`,跳转至 `/login?returnUrl=...` - - 当路由声明 `requiredRole` 时,校验 `user.isAdmin === 1`,否则跳转首页 `/` - -- **路由模块划分** - - `router/module/common.tsx` - - `/` → `Index` 首页:根据终端类型跳转: - - 移动端 → `/mobile/dashboard` - - PC 端 → `/pc/weChat` - - `/login` → 登录页(免登录) - - `/guide` → 产品引导页(需要登录) - - `/init` → iframe 初始化页(嵌入其他系统对接) - - `router/module/pc.tsx` - - `/pc` → `CkboxPage`(PC 端总布局,内含顶部导航) - - 子路由(均需要登录): - - `/pc/commonConfig` → 通用配置中心 - - `/pc/dashboard` → 数据看板 - - `/pc/weChat` → 微信客服工作台(核心聊天场景) - - `/pc/powerCenter` → 能力中心(导航页) - - `/pc/powerCenter/customer-management` → 客户管理 - - `/pc/powerCenter/communication-record` → 沟通记录 - - `/pc/powerCenter/content-management` → 内容管理 & 朋友圈发布 - - `/pc/powerCenter/ai-training` → AI 训练/话术配置 - - `/pc/powerCenter/auto-greeting` → 自动欢迎与自动打招呼 - - `/pc/powerCenter/message-push-assistant` → 消息推送助手 - - `/pc/powerCenter/message-push-assistant/create-push-task/:pushType` → 新建推送任务多步骤向导 - - `/pc/powerCenter/data-statistics` → 数据统计 - - `/pc/powerCenter/push-history` → 推送历史 - - `router/module/mobile.tsx` - - `/profile` → 移动端个人中心/设置页(需要登录) - -### 2.2 页面布局与导航 - -- **PC 布局(`components/Layout`)** - - `Layout.tsx / LayoutFiexd.tsx` - - 支持传入自定义 header(如 `NavCommon`) - - 标准结构: - - 顶部:产品标题、用户信息、全局操作区 - - 中间:左右结构(侧边栏 + 主内容) - -- **PC 触客宝主页面(`pages/pc/ckbox/index.tsx` 及子模块)** - - `CkboxPage`: - - 使用 `Layout` 作为整体页面容器 - - header 为 `NavCommon`(标题“触客宝”等) - - 中间区域通过 `` 渲染子路由页面 - ---- - -## 3. 状态管理架构(Zustand) - -### 3.1 Store 组织方式 - -- **统一出口**:`src/store/index.ts` - - 导出: - - `useUserStore`(用户) - - `useAppStore`(应用级状态) - - `useSettingsStore`(个性化设置) - - `useWebSocketStore`(WebSocket 连接与命令) - - 各种 `createPersistStore`、`persistUtils` 工具 - - 提供: - - `getStores()`:一次性获取所有 store 状态 - - `subscribeToAllStores()`:统一订阅所有 store 变更 - -- **业务模块 Store(节选)** - - `store/module/user.ts` - - 用户信息(id、名字、角色、isAdmin 等) - - 登录状态 `isLoggedIn` 与 token - - 使用 `persist` 中间件,存入 localStorage - - `store/module/app.ts` - - 全局 loading、布局属性、全局配置开关等 - - `store/module/settings.ts` - - 主题、表格密度等个性化设置 - - `store/module/websocket/websocket.ts` - - WebSocket 连接状态(已连接/重连中等) - - `sendCommand` 封装(如 `CmdSendMessage`) - - `store/module/weChat/*` - - 好友/群列表、客户、当前选中会话 - - 聊天消息、分页、群成员 - - AI 对话相关状态与逻辑(见 6 章节) - -### 3.2 权限与登录态 - -- `PermissionRoute` 通过 `useUserStore` 读取: - - `isLoggedIn`:控制是否自动跳转登录页 - - `user.isAdmin`:用于路由级别的角色校验 -- 登录成功后: - - 持久化 user store,以便刷新后仍可恢复 - - `initializeDatabaseFromPersistedUser` 读取该信息并初始化 IndexedDB - ---- - -## 4. 数据访问层(API 封装) - -### 4.1 通用请求封装(`api/request.ts`) - -- **Axios 实例配置** - - `baseURL`:优先取环境变量 `VITE_API_BASE_URL`,否则 `/api` - - 默认 `timeout`:20 秒 - - 默认请求头:`Content-Type: application/json` - - 请求拦截器: - - 从 `useUserStore.getState()` 中读取 `token` - - 自动附加 `Authorization: Bearer ` - -- **响应拦截器逻辑** - - 统一假设服务端返回结构:`{ code, success, msg, data }` - - 判断业务成功: - - 优先看 `code === 200` - - 或 `success === true` - - 若都无,则认为是“透明透传”结构,直接返回原数据 - - 业务失败处理: - - 如果 `code === 401`: - - 清理本地 `token` - - 计算当前路径 `pathname + search` - - 重定向到 `/login?redirect=当前路径` - - 其它错误: - - 根据 `ERROR_SILENT_URLS` 白名单控制是否显示错误 Toast - - 使用 `Toast.show({content: msg || "接口错误"})` 提示 - - 网络错误: - - 仍带白名单控制 - - 默认展示 `"网络异常"` 提示 - -- **请求防抖机制** - - 使用 `debounceMap`(Map)记录请求 - - key 由 `method + url + data` 组成 - - 默认间隔 `DEFAULT_DEBOUNCE_GAP = 1000ms` - - 可通过 `config.debounce = false` 关闭 - - 高频接口白名单 `NO_DEBOUNCE_URLS`(好友/群列表、消息列表)不走防抖 - -- **统一导出函数** - - `request(url, data?, method = "GET", config?, debounceGap?)` - - 自动区分 GET(用 `params`)与非 GET(用 `data`) - - 自动处理 FormData(移除默认 `Content-Type`) - -### 4.2 文件上传封装(`api/common.ts`) - -- `uploadFile(file, uploadUrl = "/v1/attachment/upload")` - - 使用原生 `FormData` 封装文件 - - 根据用户 token 设置 `Authorization` 头 - - 返回后端 `data.url` 作为上传结果 - -### 4.3 业务 API 模块 - -- `api/ai.ts` - - `dataProcessing(params)`:新消息到达后进行预处理(发送到存客宝后台) - - `aiChat(params)`:根据上下文消息生成 AI 回复内容 -- `api/module/wechat.ts` - - 聊天消息列表、群成员、好友/群列表等接口 -- `api/module/group.ts` - - 分组/标签等管理类接口 - ---- - -## 5. 本地数据库架构(IndexedDB + Dexie) - -### 5.1 数据库设计(`utils/db.ts`) - -- **数据库命名** - - 前缀:`CunkebaoDatabase` - - 最终库名:`CunkebaoDatabase_${userId}` - - 每个登录用户一个独立数据库,实现物理隔离 - -- **核心表结构(统一 friend / group 模型)** - - `ChatSession`(会话表) - - `serverId`:主键(使用服务端的 id) - - `userId`:本地用户 ID,用于多用户隔离 - - `type`:`"friend" | "group"` - - 通用字段:`wechatAccountId, nickname, avatar, content, lastUpdateTime` 等 - - 配置字段:`config.unreadCount, config.top` - - 排序字段:`sortKey` - - 扩展字段:`extendFields`(字符串 JSON) - - `Contact`(统一联系人表) - - `serverId`:主键 - - `userId`:用户 ID - - `type`:`"friend" | "group"` - - 通用信息:昵称、备注、头像、地区、签名、搜索 key 等 - - 标签/分组:`groupId` - - 扩展结构:`extendFields` - - `ContactLabelMap`(联系人与标签映射表) - - `serverId`:`${contactId}_${labelId}` - - `labelId`、`contactId`、`contactType` - - 用于按标签筛选联系人、统计 - - `UserLoginRecord`(登录记录表) - - `serverId`: `user_${userId}` - - `lastLoginTime`、`loginCount`、`lastActiveTime` 等 - -- **索引设计** - - 常用组合索引: - - `[userId+type]` - - `[userId+wechatAccountId]` - - `[userId+lastUpdateTime]` - - `sortKey / searchKey` 等 - - 方便实现: - - 会话按更新时间与置顶排序 - - 联系人/会话按关键字搜索 - - 用户/标签多维度筛选 - -### 5.2 数据库管理与代理 - -- `DatabaseManager` - - 负责: - - 根据 `userId` 打开/切换数据库实例 - - 维护 `currentDb` 与 `currentUserId` - - 提供 `ensureDatabase(userId)`、`getCurrentDatabase()` 等方法 - - 支持检测是否已初始化(`isInitialized`)和关闭数据库(`closeCurrentDatabase`) - -- 启动恢复逻辑 - - `initializeDatabaseFromPersistedUser()` - - 读取持久化 user-store(`PERSIST_KEYS.USER_STORE`) - - 解析得到 `user.id` 作为当前 userId - - 调用 `databaseManager.ensureDatabase(userId)` 初始化数据库 - -- 数据库代理对象 - - 使用 `Proxy` 将 `db` 暴露为“当前数据库”的代理 - - 调用侧无需感知内部的实例切换逻辑 - -- 通用 `DatabaseService` - - 对 Dexie 的 `Table` 封装: - - 增删改查、批量插入/更新、条件查询、分页、排序、统计 - - 支持 `createWithServerId` / `createManyWithServerId`,与接口 ID 对齐 - - 基于该服务构建: - - `chatSessionService` - - `contactUnifiedService` - - `contactLabelMapService` - - `userLoginRecordService` - ---- - -## 6. 核心业务:微信客服工作台 - -### 6.1 页面结构(/pc/weChat) - -- 路由:`/pc/weChat` → `pages/pc/ckbox/weChat` -- 典型三栏布局: - 1. 左侧侧边栏 `SidebarMenu` - - 会话列表 MessageList(虚拟列表 + 未读数) - - 好友/群/客户切换 - - 朋友圈入口 - - 添加好友/新建会话入口 - 2. 中间聊天窗口 `ChatWindow` - - 消息列表(`VirtualizedMessageList`) - - 文本、图片、语音、视频、小程序、红包、转账等多种消息类型组件 - - 输入区 `MessageEnter` - - 文本输入框 - - Emoji 表情(`EmojiSeclection`) - - 文件/图片/语音/视频上传(复用 `components/Upload` 系列) - - 支持 AI 建议文案自动填充(quoteMessageContent) - - 辅助弹窗: - - 聊天记录搜索 `ChatRecordSearch` - - 转发消息 `TransmitModal` - - 待办/提醒 `TodoListModal` - 3. 右侧信息栏 `ProfileCard` - - 客户画像与标签 - - 朋友圈动态(`FriendsCicle`) - - 快捷话术(`QuickWords`) - - 与当前会话相关的附加信息 - -### 6.2 微信 Store(`store/module/weChat/weChat.ts`) - -> 使用 `zustand + persist` 管理微信聊天域内的复杂状态。 - -- **状态主体** - - 当前对象与消息: - - `currentContract`: 当前选中的联系人/群(`ContractData | weChatGroup`) - - `currentMessages`: 当前聊天消息数组 - - `currentMessagesPage` / `currentMessagesPageSize` / `currentMessagesHasMore` - - `currentGroupMembers`: 当前群成员列表 - - AI 相关: - - `aiQuoteMessageContent`: AI 接管配置值 - - `quoteMessageContent`: AI 生成的文案草稿,用于填充输入框 - - `isLoadingAiChat`: 是否正在生成 AI 回复 - - 加载与 UI: - - `messagesLoading` / `isLoadingData` - - `showCheckbox`:是否显示多选框 - - `EnterModule`: `"common" | "multipleForwarding"` 等 - - 朋友圈: - - `MomentCommon`: 朋友圈列表 - - `MomentCommonLoading`: 加载标记 - -- **关键行为** - - `setCurrentContact(contract)` - - 切换当前会话: - - 清除 AI 请求队列与定时器 - - 重置当前消息与分页状态 - - 清除未读数(调用 `clearUnreadCount1/2`) - - 拉取 AI 配置(`getFriendInjectConfig`) - - 更新当前会话并调用 `loadChatMessages(true)` 拉取首屏消息 - - `loadChatMessages(Init, pageOverride?)` - - 根据当前联系人是好友/群决定调用 `getChatMessages` 或 `getChatroomMessages` - - 统一消息列表结构,并按时间排序 - - 处理分页信息(page/limit/hasMore) - - 首次加载群聊时同步拉取群成员列表 - - `SearchMessage({From, To, keyword, Count})` - - 在当前会话内按时间 + 关键字搜索消息 - - `receivedMsg(message)` - - 当 WebSocket 收到新消息时调用 - - 若消息属于当前会话: - - 使用 **16ms 批量更新队列**,减少 re-render 次数 - - 针对“对方发送的文字消息”,根据当前会话 `aiType` 触发 AI 逻辑: - - 先将消息加入 `pendingMessages` 队列 - - 3 秒内无更多新消息,则批量发送到 `dataProcessing` - - 再调用 `aiChat` 获取 AI 返回内容 - - 若 `aiType === 2`(AI 接管): - - 构造本地“发送中”消息并追加到 `currentMessages` - - 同时通过 WebSocket `CmdSendMessage` 实际发送 - - 若 `aiType === 1`(AI 辅助): - - 将 AI 文案写入 `quoteMessageContent`,MessageEnter 自动填充输入框 - - `manualTriggerAi()` - - 用户手动点击“智能回复”等按钮,主动触发 AI 回复流程 - ---- - -## 7. 通用组件与能力模块 - -- **选择器组件** - - `AccountSelection / DeviceSelection / ContentSelection / FriendSelection / GroupSelection / PoolSelection / MemberSelection / TwoColumnSelection` - - 特点: - - 支持弹窗选择、多选、搜索 - - 复用同一 API 与数据结构 - - 应用于内容管理、推送任务、新建沟通等多处业务场景 - -- **上传组件** - - 位于 `components/Upload/*` - - 包含: - - `ImageUpload, FileUpload, VideoUpload, AudioUpload, AvatarUpload, ChatFileUpload, SimpleFileUpload, MainImgUpload` - - 与 `uploadFile`(或独立 axios 调用)结合,支持多文件类型与进度回调 - -- **虚拟列表组件** - - `VirtualContactList, VirtualSessionList, VirtualMessageList, InfiniteList` - - 依托 `react-window` 等,实现上万级数据的高性能渲染 - -- **图表组件** - - `LineChart, LineChart2` - - 基于 `echarts-for-react`,主要用于看板与数据统计模块 - ---- - -## 8. ASCII 架构 & 页面线框图 - -### 8.1 系统高层功能架构(模块关系) - -```text -+-------------------------------------------------------------+ -| Touchkebao SPA | -| (React + TS + Vite, Antd, Antd-Mobile, Zustand, Dexie) | -+--------------------------+----------------------------------+ - | - v -+-------------------------------------------------------------+ -| App Shell | -| - App.tsx | -| - Sentry.ErrorBoundary | -| - AppRouter (BrowserRouter + useRoutes) | -+--------------------------+----------------------------------+ - | - v -+-------------------------------------------------------------+ -| Router 层 | -| - / -> Index (设备判断, 跳 /pc/weChat or /mobile) | -| - /login -> Login | -| - /guide -> Guide (auth) | -| - /init -> Iframe Init | -| - /pc/... -> PC 工作台模块 | -| - /profile -> Mobile Profile (auth) | -| - * -> 404 | -| 权限: PermissionRoute + useUserStore | -+--------------------------+----------------------------------+ - | - v -+-------------------------------------------------------------+ -| 业务模块(PC) | -| /pc (CkboxPage: Layout + NavCommon + ) | -| |- /pc/weChat -> 微信客服工作台 | -| |- /pc/dashboard -> 数据看板 | -| |- /pc/commonConfig -> 通用配置 | -| |- /pc/powerCenter/... -> 能力中心子模块 | -+--------------------------+----------------------------------+ - | - v -+-------------------------------------------------------------+ -| 状态与数据层 | -| - zustand stores | -| * user/app/settings/websocket | -| * weChat store (聊天状态 + AI 队列) | -| - api/request.ts (Axios 封装 + Token + 防抖 + Toast) | -| - api/ai.ts, api/module/wechat.ts, group.ts 等 | -| - Dexie IndexedDB (db.ts) | -| * chatSessions / contactsUnified / contactLabelMap | -| * userLoginRecords | -+-------------------------------------------------------------+ -``` - -### 8.2 PC 微信客服工作台页面线框(/pc/weChat) - -```text -+-----------------------------------------------------------------------------------+ -| 顶部导航 NavCommon | -| [ 触客宝 | 用户信息 | 其他入口 ] | -+-----------------------------------------------------------------------------------+ -| | -| +------------------------+------------------------------------+----------------+ | -| | 左侧侧边栏 SidebarMenu | 中间聊天窗口 ChatWindow | 右侧信息栏 | | -| | | | ProfileCard 等 | | -| | - 账号/设备切换 | +------------------------------+ | | | -| | - 会话列表 MessageList| | 聊天消息列表 MessageList | | - 客户画像 | | -| | * 虚拟滚动 | | (VirtualizedMessageList) | | - 朋友圈动态 | | -| | * 未读数 | | - 文本/图片/语音/视频/... | | - 快捷话术 | | -| | - 好友/群/客户列表 | | - 各种系统消息类型组件 | | - AI 配置 | | -| | - 朋友圈入口 | +------------------------------+ | | | -| | - 添加好友/发起会话 | | 输入区 MessageEnter | | | | -| | | | - 文本输入 | | | | -| | | | - 表情 EmojiPicker | | | | -| | | | - 文件/图片/语音/视频上传 | | | | -| | | | - AI 建议文案 (quote…) | | | | -| +------------------------+------------------------------------+----------------+ | -| | -+-----------------------------------------------------------------------------------+ -| 底部(可选):全局提示 / 更新提示 UpdateNotification | -+-----------------------------------------------------------------------------------+ -``` - -### 8.3 PowerCenter 能力中心线框(简版) - -```text -+----------------------------------------------------------------------------+ -| /pc/powerCenter | -+----------------------------------------------------------------------------+ -| 顶部:Tab / 菜单导航 | -| [ 客户管理 ] [ 沟通记录 ] [ 内容管理 ] [ AI 训练 ] [ 自动欢迎 ] [ 推送助手 ] ... | -+----------------------------------------------------------------------------+ -| 内容区:根据当前子路由渲染 | -| - 客户管理: 列表 + 筛选 + 详情侧滑抽屉 | -| - 沟通记录: 时间线/列表 + 搜索 | -| - 内容管理: 素材列表 + 编辑弹窗 + 预览 + 定时发布配置 | -| - AI 训练: 话术库配置、模型参数设置 | -| - 自动欢迎: 规则列表 + 开关 + 编辑 | -| - 推送助手: 创建推送任务多步骤向导(选择账号 -> 选人群 -> 配置内容 -> 发送) | -+----------------------------------------------------------------------------+ -``` - ---- - -以上内容即为当前 Touchkebao 前端工程的高层功能架构说明,可作为后续架构演进、文档输出、分享 PPT 的基础材料。 diff --git a/old/提示词/存客宝新架构.md b/old/提示词/存客宝新架构.md deleted file mode 100644 index 8dea093..0000000 --- a/old/提示词/存客宝新架构.md +++ /dev/null @@ -1,2110 +0,0 @@ -# 存客宝新架构设计方案 - -## 一、业务模型与核心问题 - -### 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/old/提示词/存客宝架构改造日志.md b/old/提示词/存客宝架构改造日志.md deleted file mode 100644 index 7f4f6a2..0000000 --- a/old/提示词/存客宝架构改造日志.md +++ /dev/null @@ -1,912 +0,0 @@ -# 存客宝新架构改造日志 - -## 改造进度总览 - -**开始时间**:2024-12-19 -**当前阶段**:阶段7 - 测试和优化(进行中) -**整体进度**:93% (阶段1完成 100%,阶段2完成 100%,阶段3完成 100%,阶段4完成 100%,阶段5完成 100%,阶段6完成 100%,阶段7进度 60%) - ---- - -## 阶段1:基础架构搭建(2-3周) - -**开始时间**:2024-12-19 -**预计完成时间**:2025-01-02 -**当前进度**:100% (4/4 任务完成) - -### 1.1 创建WeChatAccountStore(1-2天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 创建WeChatAccountStore,管理微信账号列表 -- [x] 实现selectedAccountId状态(0表示"全部") -- [x] 实现账号状态管理(在线状态、最后同步时间) -- [x] 实现账号切换方法 - -**完成情况**: - -- ✅ 已创建 `src/store/module/weChat/account.ts` - 微信账号管理Store -- ✅ 实现了账号列表管理(accountList) -- ✅ 实现了选中账号状态(selectedAccountId,0表示"全部") -- ✅ 实现了账号状态映射(accountStatusMap: Map) -- ✅ 实现了账号操作方法(setAccountList, setSelectedAccount, updateAccountStatus等) -- ✅ 实现了账号CRUD操作(addAccount, updateAccount, removeAccount) -- ✅ 实现了Map类型的持久化处理(转换为数组存储,恢复时转换回Map) -- ✅ 通过lint检查,无错误 - -**文件路径**:`src/store/module/weChat/account.ts` - ---- - -### 1.2 改造SessionStore(3-4天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 添加allSessions字段(一次性加载全部数据) -- [x] 实现sessionIndex(Map) -- [x] 实现filteredSessionsCache(过滤结果缓存) -- [x] 实现buildIndexes方法(构建索引) -- [x] 实现switchAccount方法(使用索引快速过滤) -- [x] 实现addSession方法(增量更新索引) -- [x] 保留原有接口,向后兼容 - -**完成情况**: - -- ✅ 已更新 `src/store/module/weChat/message.data.ts` - 添加新架构接口定义 -- ✅ 已更新 `src/store/module/weChat/message.ts` - 实现索引和缓存功能 -- ✅ 实现了allSessions字段(存储全部会话数据) -- ✅ 实现了sessionIndex(Map索引,O(1)快速查找) -- ✅ 实现了filteredSessionsCache(过滤结果缓存,避免重复计算) -- ✅ 实现了buildIndexes方法(构建索引,O(n)时间复杂度,只执行一次) -- ✅ 实现了switchAccount方法(使用索引快速过滤,O(1)获取+O(n)过滤) -- ✅ 实现了addSession方法(增量更新索引,O(1)更新) -- ✅ 实现了搜索和排序功能(setSearchKeyword, setSortBy) -- ✅ 实现了缓存失效机制(invalidateCache) -- ✅ 实现了Map类型的持久化处理(转换为数组存储,恢复时转换回Map) -- ✅ 保留原有接口,完全向后兼容 -- ✅ 通过lint检查,无错误 - -**文件路径**: - -- `src/store/module/weChat/message.data.ts` - 接口定义 -- `src/store/module/weChat/message.ts` - 实现 - -**性能优化**: - -- 切换账号:从O(n)遍历全部数据 → O(1)索引获取,性能提升50-100倍 -- 过滤缓存:避免重复计算,切换回相同账号时直接使用缓存 -- 增量更新:新增会话时只更新索引,不重新构建全部索引 - ---- - -### 1.3 改造ContactStore(5-7天) - -**状态**:✅ 核心功能已完成(90%) -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 -**预计完成时间**:2024-12-26(细节完善) - -**任务清单**: - -- [x] 创建数据结构定义文件(contacts.data.ts) -- [x] 重构Store结构,支持分组懒加载 -- [x] 实现groups字段(分组列表,一次性加载) -- [x] 实现expandedGroups(展开的分组) -- [x] 实现groupData(Map) -- [x] 实现loadGroupContacts方法(懒加载分组联系人) -- [x] 实现loadMoreGroupContacts方法(分页加载) -- [x] 实现searchContacts方法(API搜索,并行请求) -- [x] 实现switchAccount方法(切换账号,重新加载展开的分组) -- [x] 实现分组编辑方法(addGroup, updateGroup, deleteGroup) -- [x] 实现联系人操作方法(updateContactRemark, moveContactToGroup) -- [x] 实现虚拟滚动(setVisibleRange) -- [ ] 完善细节(API调用、错误处理、loadGroups方法) - -**完成情况**: - -- ✅ 已创建 `src/store/module/weChat/contacts.data.ts` - 新架构数据结构定义 -- ✅ 定义了ContactGroup、GroupContactData、VirtualScrollState接口 -- ✅ 定义了ContactStoreState接口(包含新架构和向后兼容字段) -- ✅ 已创建 `src/store/module/weChat/contacts.new.ts` - 新架构实现文件 -- ✅ 实现了分组管理(setGroups, toggleGroup) -- ✅ 实现了分组数据加载(loadGroupContacts, loadMoreGroupContacts) -- ✅ 实现了搜索功能(searchContacts - API并行请求) -- ✅ 实现了切换账号(switchAccount - 重新加载展开的分组) -- ✅ 实现了分组编辑(addGroup, updateGroup, deleteGroup) -- ✅ 实现了联系人操作(addContact, updateContact, updateContactRemark, deleteContact, moveContactToGroup) -- ✅ 实现了虚拟滚动(setVisibleRange) -- ✅ 实现了Map和Set类型的持久化处理 -- ✅ 保留原有接口,向后兼容 -- ✅ 通过lint检查,无错误 - -**当前进度**: - -- [x] 创建数据结构定义文件 -- [x] 实现ContactStore核心功能(分组懒加载、API搜索、分组编辑等) -- [ ] 完善细节(API调用、错误处理等) -- [ ] 测试和优化 - -**文件路径**: - -- `src/store/module/weChat/contacts.data.ts` - 数据结构定义 -- `src/store/module/weChat/contacts.new.ts` - 新架构实现(待迁移) - -**注意事项**: - -- 新文件命名为 `contacts.new.ts`,用于逐步迁移 -- 最终需要替换原有的 `contacts.ts` 文件 -- 部分API调用需要根据实际接口完善(如updateContactRemark, moveContactToGroup) - ---- - -### 1.4 实现数据索引工具(2-3天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 实现DataIndexManager类 -- [x] 实现buildIndexes方法(构建会话和联系人索引) -- [x] 实现getSessionsByAccount方法(O(1)获取) -- [x] 实现getContactsByAccount方法(O(1)获取) -- [x] 实现增量更新索引方法(addSession, addContact, updateSession, updateContact) -- [x] 实现删除方法(removeSession, removeContact) -- [x] 实现统计和工具方法(getStats, getAllAccountIds, isEmpty) - -**完成情况**: - -- ✅ 已创建 `src/utils/dataIndex.ts` - 数据索引工具类 -- ✅ 实现了DataIndexManager类,支持会话和联系人索引 -- ✅ 实现了buildIndexes方法(O(n)时间复杂度,只执行一次) -- ✅ 实现了getSessionsByAccount和getContactsByAccount方法(O(1)时间复杂度) -- ✅ 实现了增量更新方法(addSession, addContact, updateSession, updateContact) -- ✅ 实现了删除方法(removeSession, removeContact) -- ✅ 实现了统计和工具方法(getStats, getAllAccountIds, isEmpty, clear) -- ✅ 支持全局单例模式(可选) -- ✅ 通过lint检查,无错误 - -**文件路径**:`src/utils/dataIndex.ts` - -**性能特点**: - -- 索引构建:O(n)时间复杂度,只执行一次 -- 索引查询:O(1)时间复杂度,直接从Map获取 -- 增量更新:O(1)时间复杂度,只更新对应账号的索引 -- 支持"全部"账号(accountId=0),自动合并所有账号数据 - ---- - -## 阶段2:虚拟滚动实现(2-3周) - -**开始时间**:2024-12-19 -**预计完成时间**:2025-01-09 -**当前进度**:100% (组件创建完成,MessageList和WechatFriends集成完成,代码优化完成,阶段2完成) - -### 2.1 会话列表虚拟滚动(4-5天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 创建VirtualSessionList组件 -- [x] 实现固定高度虚拟滚动(ITEM_HEIGHT = 72px) -- [x] 实现可见区域计算逻辑 -- [x] 实现滚动事件处理(防抖) -- [x] 实现滚动加载更多支持 -- [x] 优化SessionItem组件(React.memo) -- [x] 创建MessageList虚拟滚动集成示例(index.virtual.tsx) -- [x] 实际集成到MessageList组件(已完成) - -**完成情况**: - -- ✅ 已创建 `src/components/VirtualSessionList/index.tsx` - 会话列表虚拟滚动组件 -- ✅ 已创建 `src/components/VirtualSessionList/index.module.scss` - 样式文件 -- ✅ 实现了固定高度虚拟滚动(ITEM_HEIGHT = 72px) -- ✅ 实现了可见区域计算(使用react-window的FixedSizeList) -- ✅ 实现了滚动事件处理 -- ✅ 实现了滚动加载更多支持(可配置阈值) -- ✅ 实现了选中状态高亮 -- ✅ 支持右键菜单和点击事件 -- ✅ 支持滚动到指定会话 -- ✅ 实现了空状态显示 -- ✅ 通过lint检查,无错误 - -**文件路径**: - -- `src/components/VirtualSessionList/index.tsx` - 组件实现 -- `src/components/VirtualSessionList/index.module.scss` - 样式文件 - -**性能特点**: - -- 固定高度:72px,性能最优 -- 只渲染可见区域:10-20条数据 -- 支持缓冲渲染:上下各多渲染2项,提升滚动流畅度 - ---- - -### 2.2 联系人列表虚拟滚动(5-7天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 创建VirtualContactList组件 -- [x] 实现分组虚拟滚动(每个分组独立) -- [x] 实现动态高度处理(分组头部+联系人列表) -- [x] 实现分组展开/折叠时的虚拟滚动调整 -- [x] 实现分组内分页加载支持(滚动到底部) -- [x] 实际集成到WechatFriends组件(已完成) -- [x] 支持分组和联系人的右键菜单 - -**完成情况**: - -- ✅ 已创建 `src/components/VirtualContactList/index.tsx` - 联系人列表虚拟滚动组件 -- ✅ 已创建 `src/components/VirtualContactList/index.module.scss` - 样式文件 -- ✅ 实现了分组虚拟滚动(使用react-window的VariableSizeList) -- ✅ 实现了动态高度处理(分组头部40px + 联系人项60px) -- ✅ 实现了分组展开/折叠时的虚拟滚动调整 -- ✅ 支持分组头部和联系人项的独立渲染 -- ✅ 支持分组头部和联系人项的右键菜单 -- ✅ 实现了空状态显示 -- ✅ 通过lint检查,无错误 - -**文件路径**: - -- `src/components/VirtualContactList/index.tsx` - 组件实现 -- `src/components/VirtualContactList/index.module.scss` - 样式文件 - -**性能特点**: - -- 动态高度:分组头部40px,联系人项60px -- 只渲染可见区域:根据展开的分组动态计算 -- 支持分组展开/折叠:自动调整虚拟滚动高度 -- 支持分组内分页加载:滚动到底部触发加载更多 - -**注意事项**: - -- 使用VariableSizeList处理动态高度 -- 需要缓存每项的高度,提升性能 -- 分组展开/折叠时需要重置高度缓存 - ---- - -## 阶段3:搜索和懒加载功能(1-2周) - -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 -**当前进度**:100% (搜索功能已集成,懒加载已实现) - -**任务清单**: - -- [x] 集成新架构的搜索功能到SidebarMenu组件 -- [x] 实现搜索防抖(300ms) -- [x] 使用新架构的searchContacts方法(API驱动,并行请求好友和群列表) -- [x] 更新WechatFriends组件,使用新架构的searchResults和isSearchMode -- [x] 保持向后兼容(同时更新旧架构的searchKeyword) -- [x] 实现懒加载功能(loadMoreGroupContacts已在ContactStore中实现) - -**完成情况**: - -- ✅ 已集成搜索功能到 `src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx` -- ✅ 实现了搜索防抖(300ms延迟) -- ✅ 使用新架构的searchContacts方法(API并行请求好友和群列表) -- ✅ WechatFriends组件已使用新架构的searchResults和isSearchMode -- ✅ 保持向后兼容(同时更新旧架构的searchKeyword) -- ✅ 懒加载功能已在ContactStore中实现(loadMoreGroupContacts) -- ✅ 虚拟滚动组件已支持滚动加载更多 - -**文件路径**: - -- `src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx` - 搜索功能集成 -- `src/store/module/weChat/contacts.new.ts` - 搜索和懒加载实现 - ---- - -## 阶段4:右键菜单和操作功能(1-2周) - -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 -**当前进度**:100% (右键菜单组件已创建并集成) - -**任务清单**: - -- [x] 创建GroupContextMenu组件(分组右键菜单:新增、编辑、删除) -- [x] 创建ContactContextMenu组件(联系人右键菜单:修改备注、移动分组) -- [x] 集成右键菜单到VirtualContactList组件 -- [x] 集成右键菜单到WechatFriends组件 -- [x] 实现分组操作回调(通过Store方法) -- [x] 实现联系人操作回调(修改备注、移动分组) - -**完成情况**: - -- ✅ 已创建 `src/components/GroupContextMenu/index.tsx` - 分组右键菜单组件 -- ✅ 已创建 `src/components/ContactContextMenu/index.tsx` - 联系人右键菜单组件 -- ✅ 实现了分组操作(新增、编辑、删除分组) -- ✅ 实现了联系人操作(修改备注、移动分组) -- ✅ 集成右键菜单到VirtualContactList组件(支持分组和联系人右键) -- ✅ 集成右键菜单到WechatFriends组件(完整的状态管理和回调) -- ✅ 实现了分组操作回调(addGroup, updateGroup, deleteGroup) -- ✅ 实现了联系人操作回调(updateContactRemark, moveContactToGroup) -- ✅ 修复了updateContactRemark方法,支持根据contactId查找分组信息 -- ✅ 修复了addGroup方法,处理API返回数据结构 -- ✅ 通过lint检查,无错误 - -**文件路径**: - -- `src/components/GroupContextMenu/index.tsx` - 分组右键菜单 -- `src/components/GroupContextMenu/index.module.scss` - 分组菜单样式 -- `src/components/ContactContextMenu/index.tsx` - 联系人右键菜单 -- `src/components/ContactContextMenu/index.module.scss` - 联系人菜单样式 -- `src/components/VirtualContactList/index.tsx` - 虚拟滚动组件(已集成右键菜单) -- `src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx` - 联系人列表(已集成右键菜单) - ---- - -## 阶段5:缓存策略优化(1周) - -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 -**当前进度**:100% (缓存工具类已创建,ContactStore和SessionStore已集成缓存) - -### 5.1 缓存工具改造(3-4天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 实现缓存工具类(支持TTL) -- [x] 实现分组列表缓存(TTL: 30分钟) -- [x] 实现分组联系人缓存(TTL: 1小时) -- [x] 实现分组统计缓存(TTL: 30分钟) -- [x] 实现缓存失效机制 -- [x] 实现缓存清理机制(定期清理过期缓存) - -**完成情况**: - -- ✅ 已创建 `src/utils/cache/index.ts` - 缓存工具类 -- ✅ 实现了CacheManager类,支持TTL和IndexedDB存储 -- ✅ 实现了分组列表缓存管理器(groupListCache,TTL: 30分钟) -- ✅ 实现了分组联系人缓存管理器(groupContactsCache,TTL: 1小时) -- ✅ 实现了分组统计缓存管理器(groupStatsCache,TTL: 30分钟) -- ✅ 实现了会话列表缓存管理器(sessionListCache,TTL: 1小时) -- ✅ 实现了缓存失效机制(自动检查TTL) -- ✅ 实现了定期清理过期缓存(每小时执行一次) -- ✅ 支持IndexedDB和localStorage两种存储方式 -- ✅ 通过lint检查,无错误 - -**文件路径**:`src/utils/cache/index.ts` - -**性能特点**: - -- TTL机制:自动检查缓存是否过期 -- IndexedDB存储:支持大容量数据缓存 -- 定期清理:每小时自动清理过期缓存 -- 后台更新:有缓存时立即显示,后台静默更新 - ---- - -### 5.2 初始化加载优化(2-3天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 实现初始化加载策略(先读缓存,后台更新) -- [x] 实现分组列表初始化(检查缓存) -- [x] 实现会话列表初始化(检查缓存) -- [x] 实现后台更新逻辑(静默更新) -- [x] 实现Loading状态优化(有缓存时不显示Loading) - -**完成情况**: - -- ✅ ContactStore已集成缓存(loadGroups、loadGroupContacts方法) -- ✅ 实现了分组列表缓存加载(先读缓存,后台更新) -- ✅ 实现了分组联系人缓存加载(先读缓存,后台更新) -- ✅ SessionStore已集成缓存(setAllSessions、loadSessionsFromCache方法) -- ✅ 实现了会话列表缓存加载(先读缓存,后台更新) -- ✅ 有缓存时立即显示数据,不显示Loading状态 -- ✅ 后台静默更新,不阻塞UI -- ✅ 通过lint检查,无错误 - -**文件路径**: - -- `src/store/module/weChat/contacts.new.ts` - ContactStore缓存集成 -- `src/store/module/weChat/message.ts` - SessionStore缓存集成 - -**优化效果**: - -- 首次加载:有缓存时 < 50ms(从IndexedDB读取) -- 无缓存时:正常API调用,然后缓存结果 -- 后台更新:不阻塞UI,静默更新缓存 - ---- - -## 阶段6:WebSocket实时更新优化(1周) - -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 -**当前进度**:100% (WebSocket消息处理已优化,同步更新新架构Store和缓存) - -### 6.1 WebSocket更新逻辑改造(3-4天) - -**状态**:✅ 已完成 -**开始时间**:2024-12-19 -**完成时间**:2024-12-19 - -**任务清单**: - -- [x] 改造WebSocket消息处理,支持新联系人更新 -- [x] 实现新联系人添加到对应分组(如果已加载) -- [x] 实现新联系人更新分组统计 -- [x] 实现新联系人更新搜索结果(如果匹配) -- [x] 实现新联系人同步更新缓存 -- [x] 实现新会话更新索引和缓存 - -**完成情况**: - -- ✅ 已优化 `src/store/module/websocket/msgManage.ts` - WebSocket消息处理 -- ✅ 优化了CmdNewMessage处理器,同步更新SessionStore索引和缓存 -- ✅ 实现了新会话增量更新索引(addSession方法) -- ✅ 实现了新会话缓存更新(更新sessionListCache) -- ✅ 实现了缓存失效机制(invalidateCache) -- ✅ 优化了CmdFriendInfoChanged处理器,同步更新ContactStore和缓存 -- ✅ 实现了联系人信息更新(updateContact方法自动更新分组数据和搜索结果) -- ✅ 实现了联系人缓存更新(更新groupContactsCache) -- ✅ 通过lint检查,无错误 - -**文件路径**: - -- `src/store/module/websocket/msgManage.ts` - WebSocket消息处理优化 - -**优化效果**: - -- 新消息到达时:自动更新会话列表索引和缓存(< 10ms) -- 联系人信息变更时:自动更新分组数据和搜索结果(< 10ms) -- 缓存同步:实时更新IndexedDB缓存,保证数据一致性 - ---- - -## 阶段7:测试和优化(2-3周) - -**开始时间**:2024-12-19 -**当前进度**:50% (测试工具已创建,性能监控已添加,代码优化已完成,WebSocket优化已完成,测试用例文档已创建,待实际测试) - -### 7.1 功能测试(1周) - -**状态**:🟡 进行中 -**开始时间**:2024-12-19 - -**任务清单**: - -- [x] 创建测试和优化指南文档 -- [x] 创建性能测试工具(performanceTest.ts) -- [x] 在关键操作中添加性能监控(switchAccount, loadGroupContacts, searchContacts) -- [x] 优化代码,减少不必要的重渲染(React.memo优化) -- [x] 添加错误处理和边界情况处理(addSession方法) -- [ ] 会话列表功能测试(切换账号、搜索、排序) -- [ ] 联系人列表功能测试(分组懒加载、分页、搜索) -- [ ] 右键菜单功能测试(分组操作、联系人操作) -- [ ] 缓存功能测试(初始化加载、后台更新) -- [ ] WebSocket更新测试(实时更新) -- [ ] 边界情况测试(大数据量、网络异常、缓存失效) - -**完成情况**: - -- ✅ 已创建 `提示词/测试和优化指南.md` - 详细的测试指南文档 -- ✅ 已创建 `src/utils/test/performanceTest.ts` - 性能测试工具类 -- ✅ 在SessionStore.switchAccount方法中添加了性能监控 -- ✅ 在ContactStore.loadGroupContacts方法中添加了性能监控 -- ✅ 在ContactStore.searchContacts方法中添加了性能监控 -- ✅ 在ContactStore.switchAccount方法中添加了性能监控 -- ✅ 优化VirtualSessionList组件,使用React.memo减少重渲染 -- ✅ 优化addSession方法,添加边界检查和错误处理 -- ✅ 包含功能测试清单、性能测试指标、兼容性测试要求 -- ✅ 包含测试工具和方法、已知问题和解决方案 -- ✅ 包含优化建议和后续优化计划 - -**文件路径**: - -- `提示词/测试和优化指南.md` - 测试指南文档 -- `src/utils/test/performanceTest.ts` - 性能测试工具 - ---- - -### 7.2 性能测试和优化(1周) - -**状态**:🟡 进行中 -**开始时间**:2024-12-20 - -**任务清单**: - -- [x] 创建性能测试工具(performanceTest.ts) -- [x] 在关键操作中添加性能监控 -- [x] 渲染优化(减少不必要的重渲染 - React.memo) -- [ ] 会话列表性能测试(切换账号 < 100ms) -- [ ] 联系人列表性能测试(首次展开 < 200ms) -- [ ] 虚拟滚动性能测试(60fps) -- [ ] 内存占用测试(< 100MB) -- [ ] 网络请求优化(减少不必要的请求) - -**完成情况**: - -- ✅ 已创建 `src/utils/test/performanceTest.ts` - 性能测试工具类 -- ✅ 在SessionStore.switchAccount方法中添加了性能监控 -- ✅ 在ContactStore.loadGroupContacts方法中添加了性能监控 -- ✅ 在ContactStore.searchContacts方法中添加了性能监控 -- ✅ 在ContactStore.switchAccount方法中添加了性能监控 -- ✅ VirtualSessionList组件使用React.memo优化,减少重渲染 -- ✅ 添加了自定义比较函数,优化渲染性能 - -**文件路径**:`src/utils/test/performanceTest.ts` - -**性能指标**: - -- 会话列表切换账号:< 100ms -- 联系人分组展开:< 200ms -- 虚拟滚动帧率:≥ 60fps -- 内存占用:< 100MB -- 搜索响应时间:< 250ms - ---- - -### 7.3 兼容性测试(3-5天) - -**状态**:⚪ 未开始 - -**任务清单**: - -- [ ] 浏览器兼容性测试(Chrome, Firefox, Edge, Safari) -- [ ] 不同屏幕尺寸测试(1920x1080, 1366x768, 移动端) -- [ ] 不同数据量测试(1000条、10000条、50000条) -- [ ] 网络环境测试(正常、慢速、离线) - ---- - -## 改造记录 - -### 2024-12-19 - -**时间**:14:00 -**操作**:创建改造日志文件,开始阶段1.1 - 创建WeChatAccountStore - -**时间**:14:30 -**操作**:完成WeChatAccountStore创建 - -- 创建了 `src/store/module/weChat/account.ts` 文件 -- 实现了完整的账号管理功能 -- 支持账号列表、选中状态、账号状态管理 -- 实现了Map类型的持久化处理 -- 通过lint检查,无错误 - -**时间**:15:00 -**操作**:完成SessionStore改造 - -- 更新了 `src/store/module/weChat/message.data.ts` - 添加新架构接口 -- 更新了 `src/store/module/weChat/message.ts` - 实现索引和缓存功能 -- 实现了allSessions、sessionIndex、filteredSessionsCache等核心功能 -- 实现了buildIndexes、switchAccount、addSession等关键方法 -- 实现了搜索和排序功能 -- 实现了Map类型的持久化处理 -- 保留原有接口,完全向后兼容 -- 通过lint检查,无错误 - -**时间**:15:30 -**操作**:开始ContactStore改造 - -- 创建了 `src/store/module/weChat/contacts.data.ts` - 新架构数据结构定义 -- 定义了ContactGroup、GroupContactData、VirtualScrollState接口 -- 定义了ContactStoreState接口(包含新架构和向后兼容字段) - -**时间**:16:00 -**操作**:完成ContactStore核心功能实现 - -- 创建了 `src/store/module/weChat/contacts.new.ts` - 新架构实现文件 -- 实现了分组管理、分组数据加载、搜索、切换账号等核心功能 -- 实现了分组编辑和联系人操作功能 -- 实现了Map和Set类型的持久化处理 -- 保留原有接口,向后兼容 -- 通过lint检查,无错误 - -**时间**:16:30 -**操作**:完成数据索引工具实现 - -- 创建了 `src/utils/dataIndex.ts` - 数据索引工具类 -- 实现了DataIndexManager类,支持会话和联系人索引 -- 实现了buildIndexes、getSessionsByAccount、getContactsByAccount等方法 -- 实现了增量更新和删除方法 -- 实现了统计和工具方法 -- 支持全局单例模式 -- 通过lint检查,无错误 - -**阶段1总结**: - -- ✅ 阶段1.1:创建WeChatAccountStore - 已完成 -- ✅ 阶段1.2:改造SessionStore - 已完成 -- ✅ 阶段1.3:改造ContactStore - 核心功能已完成 -- ✅ 阶段1.4:实现数据索引工具 - 已完成 - -**阶段1完成时间**:2024-12-19(预计2-3周,实际1天完成核心功能) - -**时间**:17:00 -**操作**:完成虚拟滚动组件创建 - -- 创建了 `src/components/VirtualSessionList` - 会话列表虚拟滚动组件 -- 创建了 `src/components/VirtualContactList` - 联系人列表虚拟滚动组件 -- 实现了固定高度和动态高度的虚拟滚动 -- 支持滚动加载更多、右键菜单、选中状态等功能 -- 通过lint检查,无错误 - -**时间**:17:30 -**操作**:创建虚拟滚动集成示例 - -- 创建了 `src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx` - 虚拟滚动集成示例 -- 展示了如何将VirtualSessionList集成到MessageList组件 -- 展示了如何与新架构的SessionStore集成 -- 保留了原有功能(右键菜单、修改备注等) - -**时间**:18:00 -**操作**:创建虚拟滚动集成指南 - -- 创建了 `提示词/虚拟滚动集成指南.md` - 详细的集成指南文档 -- 包含MessageList和WechatFriends的集成步骤 -- 包含性能优化建议、测试要点、常见问题等 -- 提供了回滚方案和注意事项 - -**时间**:18:30 -**操作**:实际集成VirtualSessionList到MessageList组件 - -- 导入VirtualSessionList组件 -- 集成新架构的SessionStore(switchAccount, setSearchKeyword, setAllSessions, buildIndexes) -- 替换List组件为VirtualSessionList -- 调整SessionItem组件(从List.Item改为div,适配虚拟滚动) -- 调整样式(固定高度72px,flex布局) -- 修复数据源引用(使用displaySessions,优先使用新架构的sessions) -- 保留所有原有功能(右键菜单、修改备注、删除等) -- 修复enrichUnknownContacts、自动点击第一个会话等逻辑 - -**时间**:19:00 -**操作**:实际集成VirtualContactList到WechatFriends组件 - -- 导入VirtualContactList组件和useContactStoreNew -- 集成新架构的ContactStore(setGroups, toggleGroup, loadGroupContacts, loadMoreGroupContacts, switchAccount) -- 将ContactGroupByLabel转换为ContactGroup格式并同步到新架构 -- 替换Collapse组件为VirtualContactList -- 调整renderContactItem和renderGroupHeader(适配虚拟滚动) -- 调整样式(flex布局,overflow: hidden) -- 保留搜索模式(使用原有List组件) -- 保留所有原有功能(联系人点击、分组展开/折叠、分页加载等) - -**时间**:19:30 -**操作**:修复和优化代码 - -- 修复MessageList组件中的linter错误(sessions变量引用问题,useCallback导入) -- 清理WechatFriends组件中未使用的旧代码(添加废弃注释) -- 所有linter错误已修复 -- 代码已优化,保持向后兼容 - -**时间**:20:00 -**操作**:开始阶段3 - 搜索和懒加载功能 - -- 集成新架构的搜索功能到SidebarMenu组件 -- 实现搜索防抖(300ms) -- 使用新架构的searchContacts方法(API驱动,并行请求好友和群列表) -- 更新WechatFriends组件,使用新架构的searchResults和isSearchMode -- 保持向后兼容(同时更新旧架构的searchKeyword) - -**时间**:20:15 -**操作**:修复VirtualSessionList组件的滚动事件处理错误 - -- 修复react-window的onScroll回调参数格式问题 -- react-window的onScroll接收的是对象参数(scrollOffset等),不是标准React事件 -- 更新handleScroll函数以适配react-window的API -- 修复scrollTop读取undefined的错误 - -**时间**:20:20 -**操作**:修复VirtualContactList组件的滚动事件处理错误 - -- 同样的问题,VariableSizeList的onScroll也是对象参数格式 -- 更新handleScroll函数以适配react-window的API -- 修复加载更多逻辑,使用totalHeight计算距离底部距离 -- 修复变量名冲突(totalHeight重复定义) - -**时间**:20:30 -**操作**:完成阶段4 - 右键菜单和操作功能 - -- 创建GroupContextMenu组件(分组右键菜单:新增、编辑、删除) -- 创建ContactContextMenu组件(联系人右键菜单:修改备注、移动分组) -- 集成右键菜单到VirtualContactList组件 -- 集成右键菜单到WechatFriends组件 -- 实现分组操作回调(通过Store方法) -- 实现联系人操作回调(修改备注、移动分组) -- 修复updateContactRemark方法,支持根据contactId查找分组信息 -- 修复addGroup方法,处理API返回数据结构 - -**阶段1-4总结**: - -- ✅ 阶段1:基础架构搭建 - 已完成(WeChatAccountStore、SessionStore、ContactStore、数据索引工具) -- ✅ 阶段2:虚拟滚动实现 - 已完成(VirtualSessionList、VirtualContactList,已集成到MessageList和WechatFriends) -- ✅ 阶段3:搜索和懒加载功能 - 已完成(搜索功能已集成,懒加载已实现) -- ✅ 阶段4:右键菜单和操作功能 - 已完成(GroupContextMenu、ContactContextMenu已创建并集成) - -**时间**:21:00 -**操作**:完成阶段5 - 缓存策略优化 - -- 创建缓存工具类(CacheManager,支持TTL和IndexedDB) -- 实现分组列表缓存(TTL: 30分钟) -- 实现分组联系人缓存(TTL: 1小时) -- 实现分组统计缓存(TTL: 30分钟) -- 实现会话列表缓存(TTL: 1小时) -- 实现缓存失效和清理机制(定期清理过期缓存) -- 集成缓存到ContactStore(loadGroups、loadGroupContacts方法) -- 集成缓存到SessionStore(setAllSessions、loadSessionsFromCache方法) -- 实现初始化加载优化(先读缓存,后台更新) -- 优化Loading状态(有缓存时不显示Loading) - -**阶段1-5总结**: - -- ✅ 阶段1:基础架构搭建 - 已完成 -- ✅ 阶段2:虚拟滚动实现 - 已完成 -- ✅ 阶段3:搜索和懒加载功能 - 已完成 -- ✅ 阶段4:右键菜单和操作功能 - 已完成 -- ✅ 阶段5:缓存策略优化 - 已完成 - -**时间**:21:00 -**操作**:完成阶段5 - 缓存策略优化 - -- 创建缓存工具类(CacheManager,支持TTL和IndexedDB) -- 实现分组列表缓存(TTL: 30分钟) -- 实现分组联系人缓存(TTL: 1小时) -- 实现分组统计缓存(TTL: 30分钟) -- 实现会话列表缓存(TTL: 1小时) -- 实现缓存失效和清理机制(定期清理过期缓存) -- 集成缓存到ContactStore(loadGroups、loadGroupContacts方法) -- 集成缓存到SessionStore(setAllSessions、loadSessionsFromCache方法) -- 实现初始化加载优化(先读缓存,后台更新) -- 优化Loading状态(有缓存时不显示Loading) - -**时间**:22:00 -**操作**:完成阶段6 - WebSocket实时更新优化 - -- 优化CmdNewMessage处理器,同步更新SessionStore索引和缓存 -- 实现新会话增量更新索引(addSession方法) -- 实现新会话缓存更新(更新sessionListCache) -- 实现缓存失效机制(invalidateCache) -- 优化CmdFriendInfoChanged处理器,同步更新ContactStore和缓存 -- 实现联系人信息更新(updateContact方法自动更新分组数据和搜索结果) -- 实现联系人缓存更新(更新groupContactsCache) - -**时间**:22:30 -**操作**:完善代码细节和修复BUG - -- 修复contacts.new.ts中的语法错误(删除重复代码片段) -- 修复WechatFriends组件中updateContactRemark未定义的问题 -- 修复displayGroups初始化顺序问题(使用newGroups代替) -- 完善updateContactRemark方法,实现API调用(updateFriendInfo) -- 完善moveContactToGroup方法,实现联系人数据重新加载 -- 添加ContactManager导入,完善数据库操作 -- 所有TODO项已完成,代码已完善 - -**阶段1-6总结**: - -- ✅ 阶段1:基础架构搭建 - 已完成 -- ✅ 阶段2:虚拟滚动实现 - 已完成 -- ✅ 阶段3:搜索和懒加载功能 - 已完成 -- ✅ 阶段4:右键菜单和操作功能 - 已完成 -- ✅ 阶段5:缓存策略优化 - 已完成 -- ✅ 阶段6:WebSocket实时更新优化 - 已完成 - -**代码完善情况**: - -- ✅ 所有TODO项已完成 -- ✅ 所有语法错误已修复 -- ✅ 所有lint错误已修复 -- ✅ API调用已完善 -- ✅ 缓存同步已完善 - -**时间**:23:00 -**操作**:开始阶段7 - 测试和优化 - -- 创建测试和优化指南文档(`提示词/测试和优化指南.md`) -- 包含功能测试清单、性能测试指标、兼容性测试要求 -- 包含测试工具和方法、已知问题和解决方案 -- 包含优化建议和后续优化计划 - -**下一步**:执行实际测试,收集性能数据,进行优化 - -**时间**:2024-12-20(继续改造) -**操作**:继续阶段7 - 测试和优化 - -- 创建性能测试工具(`src/utils/test/performanceTest.ts`) - - 实现了PerformanceTestSuite类,支持测试切换账号、展开分组、搜索等操作 - - 支持在浏览器控制台运行性能测试(window.runPerformanceTests) - - 提供测试结果统计和导出功能 - -- 在关键操作中添加性能监控 - - SessionStore.switchAccount方法已添加性能监控 - - ContactStore.loadGroupContacts方法已添加性能监控 - - ContactStore.searchContacts方法已添加性能监控 - - ContactStore.switchAccount方法已添加性能监控 - -- 优化代码,减少不必要的重渲染 - - VirtualSessionList组件:使用React.memo优化SessionRow组件 - - 添加自定义比较函数,只在会话数据或选中状态变化时重渲染 - -- 添加错误处理和边界情况处理 - - addSession方法:添加边界检查(确保session有效) - - addSession方法:检查是否已存在,避免重复添加 - - addSession方法:添加try-catch错误处理 - - WebSocket消息处理:添加边界检查和错误处理 - - WebSocket消息处理:添加超时保护(5秒) - -- 优化WebSocket消息处理 - - CmdNewMessage处理器:添加性能监控 - - CmdFriendInfoChanged处理器:添加性能监控 - - msgManageCore核心函数:添加性能监控和错误处理 - - 统一使用performanceMonitor进行性能监控 - -- 创建测试用例文档(`src/utils/test/testCases.md`) - - 包含会话列表功能测试用例(切换账号、搜索、排序) - - 包含联系人列表功能测试用例(分组懒加载、分页、搜索) - - 包含右键菜单功能测试用例(分组操作、联系人操作) - - 包含缓存功能测试用例(初始化加载、缓存失效) - - 包含WebSocket实时更新测试用例(新消息更新、联系人信息更新) - - 包含边界情况测试用例(大数据量、网络异常、缓存失效) - - 包含性能测试用例和测试报告模板 - -- 优化ErrorBoundary组件 - - 集成性能监控(使用performanceMonitor) - - 集成Sentry错误上报(使用captureError) - - 添加错误统计和记录 - - 优化错误处理逻辑 - -- 创建错误处理工具类(`src/utils/errorHandler.ts`) - - 实现了ErrorHandler类,统一处理应用错误 - - 支持错误类型分类(网络、API、验证、权限等) - - 支持错误严重程度分级(低、中、高、严重) - - 实现错误频率限制(防止错误风暴) - - 提供错误处理Hook(useErrorHandler) - - 提供错误处理装饰器(handleErrors) - - 支持Promise错误处理(handlePromiseError) - - 支持异步函数包装(wrapAsync) - - 提供错误统计功能(getErrorStats) - -**阶段7当前进度**:60% - -- ✅ 测试工具已创建 -- ✅ 性能监控已添加(包括WebSocket) -- ✅ 代码优化已完成 -- ✅ 错误处理已添加(包括WebSocket) -- ✅ 测试用例文档已创建 -- ✅ ErrorBoundary组件已优化(集成性能监控和Sentry) -- ✅ 错误处理工具类已创建(errorHandler.ts) -- ⏳ 待执行实际测试 - ---- - -## 问题记录 - -(暂无问题) - ---- - -## 备注 - -(暂无备注) diff --git a/old/提示词/存客宝架构改造计划.md b/old/提示词/存客宝架构改造计划.md deleted file mode 100644 index 84c2d51..0000000 --- a/old/提示词/存客宝架构改造计划.md +++ /dev/null @@ -1,780 +0,0 @@ -# 存客宝新架构改造计划 - -## 一、改造概述 - -### 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/old/提示词/技术栈.md b/old/提示词/技术栈.md deleted file mode 100644 index 86f0657..0000000 --- a/old/提示词/技术栈.md +++ /dev/null @@ -1,34 +0,0 @@ -## 使用技术栈 - -- React 18 -- TypeScript -- Vite(新一代前端构建工具) -- axios -- sass (scss) -- React Router v6 -- antd-mobile -- antd(已设置基础单位为 rem,配合 postcss-pxtorem) -- postcss-pxtorem(px 转 rem,移动端适配) -- ESLint + Prettier(代码规范与自动格式化) -- 路径别名 @ 指向 src 目录 - -## 关于兼容与工程化 - -- 自动化脚本(yarn lint、yarn dev 等) -- 移动端 rem 适配(html 根字体 + pxtorem) -- iOS 浏览器滚动回弹兼容问题已通过全局样式处理 -- 支持 VS Code 编辑器自动格式化(推荐配合 ESLint/Prettier 插件) - -## 目录结构简要 - -- src/ 业务源码(pages、api、styles、App.tsx、main.tsx 等) -- public/ 静态资源目录 -- index.html 项目入口(根目录) -- vite.config.ts 构建与路径别名配置 -- tsconfig.json TypeScript 配置 -- .eslintrc.js 代码规范配置 - -## 新增优化组件 - -TanStack Query -Sentry diff --git a/old/提示词/本地数据库缓存分析.md b/old/提示词/本地数据库缓存分析.md deleted file mode 100644 index ecdee83..0000000 --- a/old/提示词/本地数据库缓存分析.md +++ /dev/null @@ -1,287 +0,0 @@ -# 本地数据库(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/old/提示词/测试和优化指南.md b/old/提示词/测试和优化指南.md deleted file mode 100644 index cd98e7a..0000000 --- a/old/提示词/测试和优化指南.md +++ /dev/null @@ -1,459 +0,0 @@ -# 存客宝新架构测试和优化指南 - -## 一、功能测试清单 - -### 1.1 会话列表功能测试 - -#### 测试场景1:切换账号 -- [ ] 测试切换到"全部"账号(accountId=0) -- [ ] 测试切换到特定账号 -- [ ] 测试切换账号的性能(应 < 100ms) -- [ ] 测试切换账号后会话列表正确显示 -- [ ] 测试切换账号后缓存是否正确使用 - -**测试步骤**: -1. 打开会话列表 -2. 切换到不同账号 -3. 观察切换速度和数据正确性 - -**预期结果**: -- 切换速度 < 100ms -- 会话列表正确显示对应账号的数据 -- 切换回之前账号时使用缓存(更快) - ---- - -#### 测试场景2:搜索功能 -- [ ] 测试搜索关键词输入 -- [ ] 测试搜索防抖(300ms延迟) -- [ ] 测试搜索结果正确性 -- [ ] 测试清空搜索 -- [ ] 测试搜索性能(应 < 250ms) - -**测试步骤**: -1. 在搜索框输入关键词 -2. 观察搜索延迟和结果 -3. 清空搜索框 - -**预期结果**: -- 搜索有300ms防抖延迟 -- 搜索结果正确匹配 -- 清空后恢复原列表 - ---- - -#### 测试场景3:排序功能 -- [ ] 测试按时间排序(默认) -- [ ] 测试按未读数排序 -- [ ] 测试按名称排序 -- [ ] 测试置顶会话始终在最前 - -**测试步骤**: -1. 切换不同的排序方式 -2. 观察会话列表顺序 - -**预期结果**: -- 排序正确 -- 置顶会话始终在最前 - ---- - -### 1.2 联系人列表功能测试 - -#### 测试场景1:分组懒加载 -- [ ] 测试分组列表加载 -- [ ] 测试分组展开/折叠 -- [ ] 测试分组首次展开性能(应 < 200ms) -- [ ] 测试分组内分页加载 -- [ ] 测试分组数据缓存 - -**测试步骤**: -1. 打开联系人列表 -2. 展开不同分组 -3. 滚动到分组底部触发加载更多 - -**预期结果**: -- 分组列表快速加载 -- 首次展开 < 200ms -- 分页加载正常工作 -- 切换账号后重新展开分组时使用缓存 - ---- - -#### 测试场景2:搜索功能 -- [ ] 测试搜索关键词输入 -- [ ] 测试搜索防抖(300ms延迟) -- [ ] 测试API并行请求(好友和群列表) -- [ ] 测试搜索结果正确性 -- [ ] 测试搜索性能(应 < 250ms) - -**测试步骤**: -1. 在搜索框输入关键词 -2. 观察搜索延迟和结果 -3. 检查网络请求(应并行请求好友和群列表) - -**预期结果**: -- 搜索有300ms防抖延迟 -- 并行请求好友和群列表 -- 搜索结果正确匹配 - ---- - -### 1.3 右键菜单功能测试 - -#### 测试场景1:分组右键菜单 -- [ ] 测试新增分组 -- [ ] 测试编辑分组 -- [ ] 测试删除分组 -- [ ] 测试分组操作后列表更新 - -**测试步骤**: -1. 右键点击分组 -2. 执行新增/编辑/删除操作 -3. 观察分组列表更新 - -**预期结果**: -- 操作成功 -- 分组列表立即更新 -- 缓存同步更新 - ---- - -#### 测试场景2:联系人右键菜单 -- [ ] 测试修改备注 -- [ ] 测试移动分组 -- [ ] 测试操作后数据更新 -- [ ] 测试操作后缓存更新 - -**测试步骤**: -1. 右键点击联系人 -2. 执行修改备注/移动分组操作 -3. 观察数据更新 - -**预期结果**: -- 操作成功 -- 联系人数据立即更新 -- 分组数据同步更新 -- 缓存同步更新 - ---- - -### 1.4 缓存功能测试 - -#### 测试场景1:初始化加载 -- [ ] 测试有缓存时的加载速度(应 < 50ms) -- [ ] 测试无缓存时的加载速度 -- [ ] 测试后台更新机制 -- [ ] 测试Loading状态(有缓存时不显示) - -**测试步骤**: -1. 清除缓存后首次加载 -2. 再次加载(有缓存) -3. 观察加载速度和Loading状态 - -**预期结果**: -- 有缓存时 < 50ms,不显示Loading -- 无缓存时正常API调用 -- 后台静默更新缓存 - ---- - -#### 测试场景2:缓存失效和清理 -- [ ] 测试TTL机制(30分钟/1小时) -- [ ] 测试定期清理过期缓存 -- [ ] 测试手动失效缓存 - -**测试步骤**: -1. 等待缓存过期 -2. 观察缓存是否自动清理 -3. 手动失效缓存 - -**预期结果**: -- TTL机制正常工作 -- 过期缓存自动清理 -- 手动失效正常工作 - ---- - -### 1.5 WebSocket实时更新测试 - -#### 测试场景1:新消息更新 -- [ ] 测试收到新消息时会话列表更新 -- [ ] 测试会话索引增量更新 -- [ ] 测试会话缓存更新 -- [ ] 测试更新性能(应 < 10ms) - -**测试步骤**: -1. 打开会话列表 -2. 接收新消息 -3. 观察会话列表更新 - -**预期结果**: -- 会话列表立即更新 -- 索引增量更新 -- 缓存同步更新 -- 更新速度 < 10ms - ---- - -#### 测试场景2:联系人信息更新 -- [ ] 测试联系人信息变更时分组数据更新 -- [ ] 测试搜索结果更新 -- [ ] 测试缓存更新 - -**测试步骤**: -1. 打开联系人列表 -2. 接收联系人信息变更 -3. 观察数据更新 - -**预期结果**: -- 分组数据立即更新 -- 搜索结果同步更新 -- 缓存同步更新 - ---- - -### 1.6 边界情况测试 - -#### 测试场景1:大数据量 -- [ ] 测试10000条会话数据 -- [ ] 测试50000条联系人数据 -- [ ] 测试切换账号性能 -- [ ] 测试内存占用 - -**测试步骤**: -1. 准备大量测试数据 -2. 执行各种操作 -3. 观察性能和内存 - -**预期结果**: -- 切换账号 < 100ms -- 内存占用 < 100MB -- 虚拟滚动正常工作 - ---- - -#### 测试场景2:网络异常 -- [ ] 测试网络断开时的降级处理 -- [ ] 测试API失败时的错误处理 -- [ ] 测试缓存降级 - -**测试步骤**: -1. 断开网络 -2. 执行各种操作 -3. 观察错误处理 - -**预期结果**: -- 使用缓存数据降级 -- 错误提示友好 -- 网络恢复后自动同步 - ---- - -## 二、性能测试和优化 - -### 2.1 性能指标 - -| 指标 | 目标值 | 测试方法 | -|------|--------|---------| -| 会话列表切换账号 | < 100ms | 使用Performance API测量 | -| 联系人分组展开 | < 200ms | 使用Performance API测量 | -| 虚拟滚动帧率 | ≥ 60fps | 使用Chrome DevTools | -| 内存占用 | < 100MB | 使用Chrome DevTools Memory | -| 搜索响应时间 | < 250ms | 使用Performance API测量 | -| 缓存读取速度 | < 50ms | 使用Performance API测量 | - ---- - -### 2.2 性能优化建议 - -#### 优化1:减少不必要的重渲染 -- 使用 `React.memo` 优化组件 -- 使用 `useMemo` 和 `useCallback` 优化计算 -- 避免在render中创建新对象 - -#### 优化2:减少网络请求 -- 使用缓存减少API调用 -- 合并多个请求 -- 使用防抖和节流 - -#### 优化3:优化虚拟滚动 -- 调整 `OVERSCAN_COUNT` 参数 -- 优化 `ITEM_HEIGHT` 计算 -- 使用 `React.memo` 优化列表项 - -#### 优化4:优化内存使用 -- 及时清理不需要的数据 -- 限制缓存大小 -- 使用LRU策略 - ---- - -## 三、兼容性测试 - -### 3.1 浏览器兼容性 - -| 浏览器 | 版本 | 测试状态 | -|--------|------|---------| -| Chrome | 最新版 | 待测试 | -| Firefox | 最新版 | 待测试 | -| Edge | 最新版 | 待测试 | -| Safari | 最新版 | 待测试 | - -**测试要点**: -- IndexedDB支持 -- WebSocket支持 -- ES6+语法支持 -- CSS Grid/Flexbox支持 - ---- - -### 3.2 屏幕尺寸测试 - -| 分辨率 | 测试状态 | -|--------|---------| -| 1920x1080 | 待测试 | -| 1366x768 | 待测试 | -| 移动端 | 待测试 | - -**测试要点**: -- 布局适配 -- 虚拟滚动适配 -- 右键菜单适配 - ---- - -### 3.3 数据量测试 - -| 数据量 | 测试状态 | -|--------|---------| -| 1000条 | 待测试 | -| 10000条 | 待测试 | -| 50000条 | 待测试 | - -**测试要点**: -- 加载性能 -- 内存占用 -- 操作响应速度 - ---- - -## 四、测试工具和方法 - -### 4.1 性能测试工具 - -1. **Chrome DevTools** - - Performance面板:测量渲染性能 - - Memory面板:测量内存占用 - - Network面板:测量网络请求 - -2. **React DevTools** - - Profiler:分析组件渲染性能 - - Components:检查组件状态 - -3. **自定义性能监控** - - 使用Performance API - - 添加性能日志 - ---- - -### 4.2 测试脚本示例 - -```typescript -// 性能测试示例 -const measurePerformance = (name: string, fn: () => void) => { - const start = performance.now(); - fn(); - const end = performance.now(); - console.log(`${name}: ${end - start}ms`); -}; - -// 测试切换账号性能 -measurePerformance("切换账号", () => { - switchAccount(accountId); -}); -``` - ---- - -## 五、已知问题和解决方案 - -### 5.1 已知问题 - -1. **问题**:虚拟滚动在某些浏览器上可能不流畅 - - **解决方案**:使用 `will-change` CSS属性优化 - -2. **问题**:大量数据时内存占用较高 - - **解决方案**:限制缓存大小,使用LRU策略 - -3. **问题**:网络异常时用户体验不佳 - - **解决方案**:使用缓存降级,显示友好提示 - ---- - -## 六、测试报告模板 - -### 测试报告 - -**测试日期**:2024-XX-XX -**测试人员**:XXX -**测试环境**:Chrome 最新版 / Windows 10 - -#### 功能测试结果 -- [x] 会话列表功能:通过 -- [x] 联系人列表功能:通过 -- [x] 右键菜单功能:通过 -- [x] 缓存功能:通过 -- [x] WebSocket更新:通过 - -#### 性能测试结果 -- 会话列表切换账号:XXms(目标:< 100ms) -- 联系人分组展开:XXms(目标:< 200ms) -- 虚拟滚动帧率:XXfps(目标:≥ 60fps) -- 内存占用:XXMB(目标:< 100MB) - -#### 问题记录 -1. 问题描述 -2. 复现步骤 -3. 解决方案 - ---- - -## 七、优化建议 - -### 7.1 代码优化 -- [ ] 使用 `React.memo` 优化列表项组件 -- [ ] 使用 `useMemo` 优化计算 -- [ ] 使用 `useCallback` 优化回调函数 -- [ ] 减少不必要的状态更新 - -### 7.2 性能优化 -- [ ] 优化虚拟滚动参数 -- [ ] 优化缓存策略 -- [ ] 优化网络请求 -- [ ] 优化内存使用 - -### 7.3 用户体验优化 -- [ ] 添加加载骨架屏 -- [ ] 添加错误提示 -- [ ] 添加操作反馈 -- [ ] 优化动画效果 - ---- - -## 八、后续优化计划 - -1. **短期优化**(1周内) - - 完成功能测试 - - 修复发现的问题 - - 优化关键性能指标 - -2. **中期优化**(1个月内) - - 完成性能测试 - - 实施性能优化 - - 完成兼容性测试 - -3. **长期优化**(持续) - - 监控性能指标 - - 持续优化 - - 收集用户反馈 diff --git a/old/提示词/聊天功能逻辑.md b/old/提示词/聊天功能逻辑.md deleted file mode 100644 index 2325f42..0000000 --- a/old/提示词/聊天功能逻辑.md +++ /dev/null @@ -1,661 +0,0 @@ -# 聊天功能逻辑文档 - -## 一、整体架构 - -### 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. **用户体验**:流畅的交互、及时的状态反馈 - -通过合理的架构设计和性能优化,实现了高效、稳定的聊天功能。 diff --git a/old/提示词/虚拟滚动集成指南.md b/old/提示词/虚拟滚动集成指南.md deleted file mode 100644 index 5f0fed4..0000000 --- a/old/提示词/虚拟滚动集成指南.md +++ /dev/null @@ -1,328 +0,0 @@ -# 虚拟滚动组件集成指南 - -## 一、MessageList组件集成VirtualSessionList - -### 1.1 集成步骤 - -#### 步骤1:导入VirtualSessionList组件 - -```typescript -import { VirtualSessionList } from "@/components/VirtualSessionList"; -``` - -#### 步骤2:与新架构SessionStore集成 - -```typescript -// 使用新架构的SessionStore -const { - sessions, // 已经是过滤后的数据 - selectedAccountId, - switchAccount, - setSearchKeyword, - setAllSessions, - buildIndexes, -} = useMessageStore(); - -// 监听currentCustomer变化,同步到SessionStore -useEffect(() => { - const accountId = currentCustomer?.id || 0; - if (accountId !== selectedAccountId) { - switchAccount(accountId); - } -}, [currentCustomer, selectedAccountId, switchAccount]); - -// 监听搜索关键词变化 -useEffect(() => { - if (searchKeyword) { - setSearchKeyword(searchKeyword); - } -}, [searchKeyword, setSearchKeyword]); -``` - -#### 步骤3:数据加载时构建索引 - -```typescript -// 在数据加载完成后,构建索引 -useEffect(() => { - if (sessions.length > 0 && allSessions.length === 0) { - // 首次加载,构建索引 - setAllSessions(sessions); - } else if (sessions.length > 0) { - // 数据更新,重新构建索引 - buildIndexes(sessions); - } -}, [sessions]); -``` - -#### 步骤4:替换List组件为VirtualSessionList - -```typescript -// 原来的代码: - ( - - )} -/> - -// 替换为: - ( - - )} - onItemClick={onContactClick} - onItemContextMenu={handleContextMenu} - className={styles.virtualList} -/> -``` - -#### 步骤5:调整样式 - -```scss -.messageList { - height: 100%; - position: relative; - display: flex; - flex-direction: column; - - .virtualList { - flex: 1; - overflow: hidden; - } -} -``` - -### 1.2 注意事项 - -1. **保留原有功能**:右键菜单、修改备注、删除等功能都需要保留 -2. **数据同步**:确保新架构的SessionStore与现有数据同步 -3. **性能优化**:大数据量时,虚拟滚动会自动优化渲染 -4. **向后兼容**:保留原有的filteredSessions逻辑,逐步迁移 - ---- - -## 二、WechatFriends组件集成VirtualContactList - -### 2.1 集成步骤 - -#### 步骤1:导入VirtualContactList组件 - -```typescript -import { VirtualContactList } from "@/components/VirtualContactList"; -import { useContactStoreNew } from "@/store/module/weChat/contacts.new"; -``` - -#### 步骤2:使用新架构的ContactStore - -```typescript -// 使用新架构的ContactStore -const { - groups, - expandedGroups, - groupData, - selectedAccountId, - toggleGroup, - loadGroupContacts, - searchContacts, - clearSearch, - switchAccount, -} = useContactStoreNew(); - -// 生成分组Key的函数 -const getGroupKey = useCallback( - (groupId: number, groupType: 1 | 2, accountId: number) => { - return `${groupId}_${groupType}_${accountId}`; - }, - [], -); -``` - -#### 步骤3:加载分组列表 - -```typescript -// 初始化时加载分组列表 -useEffect(() => { - const loadGroups = async () => { - try { - const result = await getLabelsListByGroup({}); - const groups = result?.list || []; - // 转换为ContactGroup格式 - const contactGroups: ContactGroup[] = groups.map((g: any) => ({ - id: g.id, - groupName: g.groupName, - groupType: g.groupType, - count: g.count, - sort: g.sort, - groupMemo: g.groupMemo, - })); - setGroups(contactGroups); - } catch (error) { - console.error("加载分组列表失败:", error); - } - }; - loadGroups(); -}, []); -``` - -#### 步骤4:替换Collapse组件为VirtualContactList - -```typescript -// 原来的代码: - - -// 替换为: - ( - - {/* 分组头部内容 */} - - )} - renderContact={(contact, groupIndex, contactIndex) => ( - - {/* 联系人项内容 */} - - )} - onGroupToggle={toggleGroup} - onContactClick={handleContactClick} - onGroupContextMenu={handleGroupContextMenu} - onContactContextMenu={handleContactContextMenu} - onGroupLoadMore={loadMoreGroupContacts} - className={styles.virtualList} -/> -``` - -### 2.2 注意事项 - -1. **分组展开/折叠**:使用toggleGroup方法,会自动触发懒加载 -2. **搜索功能**:使用searchContacts方法,会调用API并行请求 -3. **切换账号**:使用switchAccount方法,会重新加载展开的分组 -4. **分页加载**:滚动到底部时,自动调用loadMoreGroupContacts - ---- - -## 三、性能优化建议 - -### 3.1 会话列表优化 - -1. **使用索引过滤**:切换账号时使用switchAccount方法,O(1)获取 -2. **缓存过滤结果**:相同账号切换时直接使用缓存 -3. **虚拟滚动**:只渲染可见区域,减少DOM节点 - -### 3.2 联系人列表优化 - -1. **分组懒加载**:只加载展开的分组数据 -2. **分页加载**:分组内支持分页,避免一次性加载大量数据 -3. **虚拟滚动**:支持动态高度,自动调整 - -### 3.3 通用优化 - -1. **React.memo**:优化SessionItem和ContactItem组件 -2. **useMemo**:缓存计算结果 -3. **useCallback**:缓存函数引用 - ---- - -## 四、测试要点 - -### 4.1 功能测试 - -- [ ] 会话列表正常显示 -- [ ] 切换账号功能正常 -- [ ] 搜索功能正常 -- [ ] 右键菜单正常 -- [ ] 修改备注功能正常 -- [ ] 删除会话功能正常 -- [ ] 联系人列表正常显示 -- [ ] 分组展开/折叠正常 -- [ ] 分组内分页加载正常 -- [ ] 联系人搜索正常 - -### 4.2 性能测试 - -- [ ] 10000条会话数据,切换账号 < 100ms -- [ ] 虚拟滚动帧率 ≥ 60fps -- [ ] 内存占用 < 100MB -- [ ] 分组展开 < 200ms - -### 4.3 兼容性测试 - -- [ ] Chrome浏览器 -- [ ] Firefox浏览器 -- [ ] Edge浏览器 -- [ ] Safari浏览器 - ---- - -## 五、回滚方案 - -如果集成后出现问题,可以: - -1. **保留原组件**:不删除原有的List和Collapse组件 -2. **条件渲染**:使用feature flag控制是否使用虚拟滚动 -3. **逐步迁移**:先在一个页面测试,确认无误后再全面推广 - -```typescript -// 使用feature flag控制 -const USE_VIRTUAL_SCROLL = true; // 从环境变量或配置读取 - -{USE_VIRTUAL_SCROLL ? ( - -) : ( - -)} -``` - ---- - -## 六、常见问题 - -### Q1: 虚拟滚动后,滚动位置丢失? - -**A**: 使用VirtualSessionList的scrollToSession方法,在数据更新后滚动到指定位置。 - -### Q2: 分组展开后,虚拟滚动高度不正确? - -**A**: 使用VariableSizeList的resetAfterIndex方法,在分组展开/折叠后重置高度缓存。 - -### Q3: 性能没有明显提升? - -**A**: 确保数据量足够大(> 1000条),虚拟滚动的优势在大数据量时更明显。 - ---- - -## 七、总结 - -虚拟滚动组件的集成需要: - -1. ✅ 导入组件 -2. ✅ 与新架构Store集成 -3. ✅ 替换原有List/Collapse组件 -4. ✅ 调整样式 -5. ✅ 测试和优化 - -**预计集成时间**:每个组件1-2天(包括测试和优化) diff --git a/old/提示词/触客宝功能逻辑流程图.md b/old/提示词/触客宝功能逻辑流程图.md deleted file mode 100644 index 94ca709..0000000 --- a/old/提示词/触客宝功能逻辑流程图.md +++ /dev/null @@ -1,523 +0,0 @@ -## 触客宝功能逻辑实现流程图 - -> 本文档基于《触客宝功能架构》整理关键业务链路的**实现流程**,以文本 + ASCII 流程图的形式展示前端在不同场景下的调用关系,便于开发/排查问题与架构沟通。 - ---- - -## 一、应用启动 & 初始化流程 - -### 1.1 启动总体流程 - -```text -浏览器访问页面 - | - v -加载 index.html & Vite 入口脚本 - | - v -执行 src/main.tsx - | - v -初始化 Sentry (initSentry) - | - v -initializeDatabaseFromPersistedUser() - | - +-- 读取 localStorage 中 USER_STORE - | | - | +-- 无用户信息 -> 不初始化 DB -> 返回 null - | | - | +-- 有用户信息 -> 解析 user.id -> ensureDatabase(userId) - | | - | +-- DatabaseManager 创建/打开 Dexie DB - | - v -创建 React Root - | - v - - - - - - | - v -App.tsx 内部: - Sentry.ErrorBoundary 包裹 AppRouter + UpdateNotification -``` - ---- - -## 二、路由跳转 & 权限控制流程 - -### 2.1 路由解析与渲染 - -```text -AppRouter 渲染 - | - v -import.meta.glob("./module/*.{ts,tsx}") - | - v -收集所有路由模块 default 导出路由数组 - | - v -组合得到 routes[] - | - v -对每个 route: - 如果 route.auth === true - -> 用 包裹 - 否则 - -> 直接使用 route.element - | - v -追加 404 路由 { path: "*", element: } - | - v -useRoutes(routes) 生成路由树并渲染 -``` - -### 2.2 权限路由(登录 & 角色校验) - -```text -用户访问受保护路由 (route.auth = true) - | - v -渲染 - | - v -从 useUserStore 读取 user, isLoggedIn - | - +-- 若 !isLoggedIn 或 !user - | | - | +-- 记录当前路径 currentPath = pathname + search - | | - | +-- navigate("/login?returnUrl=" + encodeURIComponent(currentPath)) - | | - | +-- 不渲染子组件 (return null) - | - +-- 若 requiredRole 存在 且 user.isAdmin !== 1 - | | - | +-- navigate("/") - | +-- return null - | - +-- 否则 (已登录 & 权限满足) - | - +-- 渲染 children (真正的业务页面) -``` - ---- - -## 三、首页访问 & 端类型分流流程 - -### 3.1 `/` 首页逻辑 - -```text -用户访问 "/" 路径 - | - v -渲染 IndexPage (pages/index.tsx) - | - v -useEffect(() => { - 判断是否移动端: - - UA 匹配移动端关键字 - - 或 window.innerWidth <= 768 -}) - | - +-- 若 isMobile === true - | | - | +-- navigate("/mobile/dashboard") - | - +-- 若 isMobile === false - | - +-- navigate("/pc/weChat") - | - v -页面本身只渲染简单 "首页" 占位,主要职责是分流 -``` - ---- - -## 四、登录流程 & 本地缓存初始化 - -> 下面为前端视角,后端返回细节略去,仅关注状态/存储/跳转。 - -```text -用户在 /login 输入账号密码 - | - v -点击登录按钮 -> 触发登录 API - | - v -调用 request("/login", formData, "POST") - | - v -Axios 请求拦截器: - - 若有 token 则附加 Authorization (首次通常无) - | - v -服务器返回登录结果 { code/success, data: { user, token } } - | - v -响应拦截器: - - 判断业务成功 - - 返回 data (user, token) - | - v -前端登录逻辑: - - 调用 useUserStore.setState(): - * 保存 user 信息 - * 保存 token - * 标记 isLoggedIn = true - - 按 returnUrl 或默认路径跳转: - * 若 URL 中有 ?returnUrl=xxx -> navigate(xxx) - * 否则 -> navigate("/") - | - v -后续刷新页面: - - main.tsx 中 initializeDatabaseFromPersistedUser() - - 根据持久化 user.id 初始化对应 Dexie 数据库 -``` - ---- - -## 五、PC 端微信客服工作台整体流程(/pc/weChat) - -### 5.1 页面三栏布局渲染流程 - -```text -用户访问 "/pc/weChat" - | - v -路由匹配 pc.tsx 中: - path: "/pc", element: , children: [...] - | - v -渲染 CkboxPage: - }> - // 当前为 WeChatPage - - | - v -WeChatPage 内部: - - 左侧 SidebarMenu (会话列表 / 联系人列表 / 朋友圈入口) - - 中间 ChatWindow (消息列表 + 输入框) - - 右侧 ProfileCard 等(客户画像/朋友圈/话术等) -``` - -### 5.2 选择会话 & 加载消息流程 - -```text -用户在 SidebarMenu 中点击某个好友/群聊 - | - v -onContactClick(contact) 触发 - | - v -useWeChatStore.setCurrentContact(contact) - | - v -setCurrentContact 内部逻辑: - 1) 清除 AI 请求定时器 & 队列 (防止跨会话 AI 干扰) - 2) 重置当前聊天状态: - - currentMessages = [] - - currentMessagesPage = 1 - - currentMessagesHasMore = true - - isLoadingAiChat = false - 3) 构造 params: - - 单聊: wechatFriendId = contact.id - - 群聊: wechatChatroomId = contact.id - 4) 调用 clearUnreadCount1/2 清空未读 - 5) 调用 getFriendInjectConfig 获取 AI 配置 - -> 更新 aiQuoteMessageContent - 6) 更新 currentContract = contact - 并更新 currentMessagesRequestId - 7) 调用 updateConfig({ id: contact.id, config: { chat: true } }) - 8) 调用 state.loadChatMessages(true) 拉取首屏消息 -``` - -### 5.3 拉取聊天消息流程(首屏 & 翻页) - -```text -调用 loadChatMessages(Init, pageOverride?) - | - v -从 store 读取: - - currentContract - - currentMessagesPage / PageSize / HasMore - - currentMessagesRequestId - | - +-- 若无 currentContract 或 id -> 直接返回 - +-- 若 !Init 且 !HasMore -> 不再请求 - +-- 若 messagesLoading && !Init -> 避免并发重复加载 - | - v -计算 nextPage: - - Init === true ? 1 : (pageOverride || currentPage + 1) - limit = currentMessagesPageSize (默认 20) - | - v -构造请求参数 params: - - wechatAccountId: contact.wechatAccountId - - page, limit - - 单聊: wechatFriendId = contact.id - - 群聊: wechatChatroomId = contact.id - | - v -根据是否群聊: - - 群聊: 调用 getChatroomMessages(params) - - 单聊: 调用 getChatMessages(params) - | - v -normalizeMessages(response) 标准化为数组 - | - v -sortMessagesByTime(messages) 统一按时间排序 - | - v -resolvePaginationState(response, page, limit, listLength) - | - v -若 Init 且为群聊: - - 额外调用 getGroupMembers({ id: contact.id }) - | - v -set(state => { - 若 currentMessagesRequestId 未变化 && currentContract.id 未变化 - - Init: currentMessages = sortedMessages - - 非 Init: currentMessages = [...sortedMessages, ...原有] - - 更新 currentGroupMembers (群聊首屏时) - - 更新 page / limit / hasMore -}) -``` - ---- - -## 六、新消息接收 & AI 自动回复流程 - -### 6.1 新消息接收(WebSocket → Store) - -```text -WebSocket 收到新消息 (CmdNewMessage) - | - v -WebSocket 层解析消息 -> 调用 useWeChatStore.receivedMsg(message) - | - v -receivedMsg(message) 内部: - 1) 判断消息归属: - - getMessageId = message.wechatChatroomId || message.wechatFriendId - - isWechatGroup = !!message.wechatChatroomId - 2) 判断是否为当前选中会话: - - 若 currentContract && currentContract.id == getMessageId - -> 进入“当前会话处理分支” - - 否则 - -> 当前函数不更新会话列表,MessageList 通过其他事件更新 -``` - -### 6.2 当前会话消息的批量渲染优化 - -```text -当前会话处理分支: - | - v -将 message 推入 messageBatchQueue - | - v -若已有 messageBatchTimer: - -> clearTimeout(messageBatchTimer) - | - v -设置新的 messageBatchTimer (延迟 ~16ms): - | - v -定时器触发: - - 拷贝 messageBatchQueue -> messagesToAdd - - 清空 messageBatchQueue - - set(state => { - currentMessages = [...state.currentMessages, ...messagesToAdd] - }) - | - v -实现一帧内多条消息合并更新,降低渲染压力 -``` - -### 6.3 AI 自动回复决策流程 - -```text -在 receivedMsg 中 (仍然是当前会话分支): - | - v -若 message.msgType === 1 (文字) 且 !message.isSend - 且 currentContract.aiType in [1, 2] - | - v -AI 触发逻辑: - 1) 若存在 aiRequestTimer 或 currentAiGenerationId: - - 调用 clearAiRequestQueue("收到新消息") - - 清除上一次 AI 请求状态 - 2) 将当前 message 加入 pendingMessages 队列 - 3) set({ isLoadingAiChat: true }) - 4) 启动 aiRequestTimer (延迟 AI_REQUEST_DELAY=3000ms): - | - v - 定时器回调: - - 复制 pendingMessages -> messagesToProcess - - 清空 pendingMessages, 清除 aiRequestTimer - - 生成 generationId = generateAiId() - - currentAiGenerationId = generationId - - 构造 dataProcessing 参数: - * type: "CmdNewMessage" - * wechatAccountId - * chatroomMessage 或 friendMessage = messagesToProcess - - 调用 dataProcessing(params) - - 若 dataProcessingResult 不为成功标志 (注意实际判断逻辑) - -> 取最后一条消息 lastMessage - -> 调用 aiChat({ friendId/getMessageId, wechatAccountId, message: lastMessage }) - -> 得到 aiResponseContent - -> 若 currentAiGenerationId 仍等于 generationId (确保未被取消) - | - v - 根据 aiType 分支: - - aiType === 2 (AI 接管模式) - * 构造本地发送消息 localMessage - * 将 localMessage append 到 currentMessages - * 调用 useWebSocketStore.sendCommand("CmdSendMessage", {...}) - * set({ isLoadingAiChat: false }) - * 清空 currentAiGenerationId - - aiType === 1 (AI 辅助模式) - * set({ - quoteMessageContent: aiResponseContent, - isLoadingAiChat: false - }) - * 清空 currentAiGenerationId - - 若中途任何错误: - -> 打印错误日志 - -> set({ isLoadingAiChat: false }) - -> currentAiGenerationId = null -``` - -### 6.4 手动触发 AI 回复流程 - -```text -用户在 UI 中点击“智能回复”按钮 - | - v -调用 manualTriggerAi() - | - v -manualTriggerAi 逻辑: - 1) 读取 currentContract, currentMessages - 2) 若无 currentContract 或 currentMessages 为空 -> 返回 false - 3) 校验 aiType in [1, 2],否则不触发 - 4) 若已有 aiRequestTimer 或 currentAiGenerationId: - -> clearAiRequestQueue("手动重新生成") - 5) 从 currentMessages 中过滤最近 5 条对方文本消息 recentMessages - 6) set({ isLoadingAiChat: true }) - 7) 生成 generationId, 记录到 currentAiGenerationId - 8) 调用 aiChat({ friendId, wechatAccountId, message: lastMessage }) - 9) 根据 aiType: - - aiType === 2: - * 构造本地消息 localMessage,append 到 currentMessages - * sendCommand("CmdSendMessage", {...}) - * set({ isLoadingAiChat: false }) - - aiType === 1: - * updateQuoteMessageContent(aiResponseContent) - * set({ isLoadingAiChat: false }) - 10) 清空 currentAiGenerationId -``` - ---- - -## 七、消息搜索 & 历史记录查看流程 - -```text -用户在聊天窗口打开“聊天记录搜索”面板 - | - v -选择时间范围 (From, To)、输入关键字 keyword、设置数量 Count - | - v -调用 useWeChatStore.SearchMessage({ From, To, keyword, Count }) - | - v -SearchMessage 内部: - 1) set({ messagesLoading: true }) - 2) 判断当前会话是群聊还是单聊 - 3) 构造 params: - - wechatAccountId - - keyword - - From, To - - page = 1 - - limit = Count - - 单聊: wechatFriendId - - 群聊: wechatChatroomId - 4) 调用对应 API: - - 群聊: getChatroomMessages(params) + getGroupMembers({ id }) - - 单聊: getChatMessages(params) - 5) 对返回结果: - - normalizeMessages -> sortMessagesByTime - - set({ - currentMessages, - currentGroupMembers (群聊), - currentMessagesPage = 1, - currentMessagesHasMore = false, - currentMessagesPageSize = Count - }) - 6) finally: set({ messagesLoading: false }) -``` - ---- - -## 八、PowerCenter 能力中心典型流程(示例:创建消息推送任务) - -> 这里只给出“消息推送助手”的高层流程,实际每个步骤内部还会调用共用选择组件和上传组件。 - -```text -用户访问 "/pc/powerCenter/message-push-assistant" - | - v -展示推送任务列表 + “新建推送任务”按钮 - | - v -点击“新建推送任务” - | - v -navigate("/pc/powerCenter/message-push-assistant/create-push-task/:pushType") - | - v -渲染 CreatePushTask 页面 (多步骤向导): - Step 1: StepSelectAccount - - 使用 AccountSelection / DeviceSelection 组件 - - 选择推送使用的微信/设备账号 - Step 2: StepSelectContacts - - 调用联系人/分组相关 API - - 使用 FriendSelection / GroupSelection / PoolSelection 等组件 - - 支持搜索/筛选与多选 - Step 3: StepPushParams - - 设置推送参数 (时间、频次、规则等) - Step 4: StepSendMessage - - 选择消息内容: - * 直接输入文本 - * 从内容库选择: ContentSelection + 内容管理 API - - 支持图片/文件/视频等资源上传 (复用 Upload 组件) - | - v -完成配置 -> 点击“创建任务” - | - v -调用创建任务 API - | - v -成功后: - - 弹出成功提示 - - 跳转回任务列表或详情页 - - 列表数据可能通过 React Query 或手动刷新获取最新状态 -``` - ---- - -以上流程图覆盖了应用启动、路由与权限、登录、PC 微信客服工作台、AI 自动回复、消息搜索以及能力中心中一个典型场景的端到端逻辑,可作为排查问题和设计新功能时的“全局参考图”。 -如需更详细的**时序图(Sequence Diagram)**或针对某个子模块(例如 IndexedDB 同步、WebSocket 重连策略等)的专门流程图,可以在此文档基础上再拆分子章节。 diff --git a/old/package-lock.json b/package-lock.json similarity index 100% rename from old/package-lock.json rename to package-lock.json diff --git a/old/package.json b/package.json similarity index 100% rename from old/package.json rename to package.json diff --git a/old/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 100% rename from old/pnpm-lock.yaml rename to pnpm-lock.yaml diff --git a/old/postcss.config.js b/postcss.config.js similarity index 100% rename from old/postcss.config.js rename to postcss.config.js diff --git a/TouchVueThree/public/assets/animal/jump.png b/public/assets/animal/jump.png similarity index 100% rename from TouchVueThree/public/assets/animal/jump.png rename to public/assets/animal/jump.png diff --git a/TouchVueThree/public/assets/animal/pig-head.png b/public/assets/animal/pig-head.png similarity index 100% rename from TouchVueThree/public/assets/animal/pig-head.png rename to public/assets/animal/pig-head.png diff --git a/TouchVueThree/public/assets/animal/shiver.png b/public/assets/animal/shiver.png similarity index 100% rename from TouchVueThree/public/assets/animal/shiver.png rename to public/assets/animal/shiver.png diff --git a/TouchVueThree/public/assets/animal/spin.png b/public/assets/animal/spin.png similarity index 100% rename from TouchVueThree/public/assets/animal/spin.png rename to public/assets/animal/spin.png diff --git a/TouchVueThree/public/assets/blessing/blessing.png b/public/assets/blessing/blessing.png similarity index 100% rename from TouchVueThree/public/assets/blessing/blessing.png rename to public/assets/blessing/blessing.png diff --git a/TouchVueThree/public/assets/blessing/celebrate.png b/public/assets/blessing/celebrate.png similarity index 100% rename from TouchVueThree/public/assets/blessing/celebrate.png rename to public/assets/blessing/celebrate.png diff --git a/TouchVueThree/public/assets/blessing/firecrackers.png b/public/assets/blessing/firecrackers.png similarity index 100% rename from TouchVueThree/public/assets/blessing/firecrackers.png rename to public/assets/blessing/firecrackers.png diff --git a/TouchVueThree/public/assets/blessing/fireworks.png b/public/assets/blessing/fireworks.png similarity index 100% rename from TouchVueThree/public/assets/blessing/fireworks.png rename to public/assets/blessing/fireworks.png diff --git a/TouchVueThree/public/assets/blessing/fortune.png b/public/assets/blessing/fortune.png similarity index 100% rename from TouchVueThree/public/assets/blessing/fortune.png rename to public/assets/blessing/fortune.png diff --git a/TouchVueThree/public/assets/blessing/gift.png b/public/assets/blessing/gift.png similarity index 100% rename from TouchVueThree/public/assets/blessing/gift.png rename to public/assets/blessing/gift.png diff --git a/TouchVueThree/public/assets/blessing/red-envelope.png b/public/assets/blessing/red-envelope.png similarity index 100% rename from TouchVueThree/public/assets/blessing/red-envelope.png rename to public/assets/blessing/red-envelope.png diff --git a/TouchVueThree/public/assets/face/666.png b/public/assets/face/666.png similarity index 100% rename from TouchVueThree/public/assets/face/666.png rename to public/assets/face/666.png diff --git a/TouchVueThree/public/assets/face/Emm.png b/public/assets/face/Emm.png similarity index 100% rename from TouchVueThree/public/assets/face/Emm.png rename to public/assets/face/Emm.png diff --git a/TouchVueThree/public/assets/face/about-to-cry.png b/public/assets/face/about-to-cry.png similarity index 100% rename from TouchVueThree/public/assets/face/about-to-cry.png rename to public/assets/face/about-to-cry.png diff --git a/TouchVueThree/public/assets/face/angry.png b/public/assets/face/angry.png similarity index 100% rename from TouchVueThree/public/assets/face/angry.png rename to public/assets/face/angry.png diff --git a/TouchVueThree/public/assets/face/arrogant.png b/public/assets/face/arrogant.png similarity index 100% rename from TouchVueThree/public/assets/face/arrogant.png rename to public/assets/face/arrogant.png diff --git a/TouchVueThree/public/assets/face/awkward.png b/public/assets/face/awkward.png similarity index 100% rename from TouchVueThree/public/assets/face/awkward.png rename to public/assets/face/awkward.png diff --git a/TouchVueThree/public/assets/face/bitter.png b/public/assets/face/bitter.png similarity index 100% rename from TouchVueThree/public/assets/face/bitter.png rename to public/assets/face/bitter.png diff --git a/TouchVueThree/public/assets/face/blush.png b/public/assets/face/blush.png similarity index 100% rename from TouchVueThree/public/assets/face/blush.png rename to public/assets/face/blush.png diff --git a/TouchVueThree/public/assets/face/cheer-up.png b/public/assets/face/cheer-up.png similarity index 100% rename from TouchVueThree/public/assets/face/cheer-up.png rename to public/assets/face/cheer-up.png diff --git a/TouchVueThree/public/assets/face/clap.png b/public/assets/face/clap.png similarity index 100% rename from TouchVueThree/public/assets/face/clap.png rename to public/assets/face/clap.png diff --git a/TouchVueThree/public/assets/face/crack.png b/public/assets/face/crack.png similarity index 100% rename from TouchVueThree/public/assets/face/crack.png rename to public/assets/face/crack.png diff --git a/TouchVueThree/public/assets/face/crazy.png b/public/assets/face/crazy.png similarity index 100% rename from TouchVueThree/public/assets/face/crazy.png rename to public/assets/face/crazy.png diff --git a/TouchVueThree/public/assets/face/crying.png b/public/assets/face/crying.png similarity index 100% rename from TouchVueThree/public/assets/face/crying.png rename to public/assets/face/crying.png diff --git a/TouchVueThree/public/assets/face/curse.png b/public/assets/face/curse.png similarity index 100% rename from TouchVueThree/public/assets/face/curse.png rename to public/assets/face/curse.png diff --git a/TouchVueThree/public/assets/face/daze.png b/public/assets/face/daze.png similarity index 100% rename from TouchVueThree/public/assets/face/daze.png rename to public/assets/face/daze.png diff --git a/TouchVueThree/public/assets/face/despise.png b/public/assets/face/despise.png similarity index 100% rename from TouchVueThree/public/assets/face/despise.png rename to public/assets/face/despise.png diff --git a/TouchVueThree/public/assets/face/disappointed.png b/public/assets/face/disappointed.png similarity index 100% rename from TouchVueThree/public/assets/face/disappointed.png rename to public/assets/face/disappointed.png diff --git a/TouchVueThree/public/assets/face/dizzy.png b/public/assets/face/dizzy.png similarity index 100% rename from TouchVueThree/public/assets/face/dizzy.png rename to public/assets/face/dizzy.png diff --git a/TouchVueThree/public/assets/face/doge.png b/public/assets/face/doge.png similarity index 100% rename from TouchVueThree/public/assets/face/doge.png rename to public/assets/face/doge.png diff --git a/TouchVueThree/public/assets/face/eat-melon.png b/public/assets/face/eat-melon.png similarity index 100% rename from TouchVueThree/public/assets/face/eat-melon.png rename to public/assets/face/eat-melon.png diff --git a/TouchVueThree/public/assets/face/embarrassed.png b/public/assets/face/embarrassed.png similarity index 100% rename from TouchVueThree/public/assets/face/embarrassed.png rename to public/assets/face/embarrassed.png diff --git a/TouchVueThree/public/assets/face/evil-smile.png b/public/assets/face/evil-smile.png similarity index 100% rename from TouchVueThree/public/assets/face/evil-smile.png rename to public/assets/face/evil-smile.png diff --git a/TouchVueThree/public/assets/face/eye-roll.png b/public/assets/face/eye-roll.png similarity index 100% rename from TouchVueThree/public/assets/face/eye-roll.png rename to public/assets/face/eye-roll.png diff --git a/TouchVueThree/public/assets/face/facepalm.png b/public/assets/face/facepalm.png similarity index 100% rename from TouchVueThree/public/assets/face/facepalm.png rename to public/assets/face/facepalm.png diff --git a/TouchVueThree/public/assets/face/fear.png b/public/assets/face/fear.png similarity index 100% rename from TouchVueThree/public/assets/face/fear.png rename to public/assets/face/fear.png diff --git a/TouchVueThree/public/assets/face/frown.png b/public/assets/face/frown.png similarity index 100% rename from TouchVueThree/public/assets/face/frown.png rename to public/assets/face/frown.png diff --git a/TouchVueThree/public/assets/face/good.png b/public/assets/face/good.png similarity index 100% rename from TouchVueThree/public/assets/face/good.png rename to public/assets/face/good.png diff --git a/TouchVueThree/public/assets/face/goodbye.png b/public/assets/face/goodbye.png similarity index 100% rename from TouchVueThree/public/assets/face/goodbye.png rename to public/assets/face/goodbye.png diff --git a/TouchVueThree/public/assets/face/grin.png b/public/assets/face/grin.png similarity index 100% rename from TouchVueThree/public/assets/face/grin.png rename to public/assets/face/grin.png diff --git a/TouchVueThree/public/assets/face/happy.png b/public/assets/face/happy.png similarity index 100% rename from TouchVueThree/public/assets/face/happy.png rename to public/assets/face/happy.png diff --git a/TouchVueThree/public/assets/face/hey-ha.png b/public/assets/face/hey-ha.png similarity index 100% rename from TouchVueThree/public/assets/face/hey-ha.png rename to public/assets/face/hey-ha.png diff --git a/TouchVueThree/public/assets/face/kiss.png b/public/assets/face/kiss.png similarity index 100% rename from TouchVueThree/public/assets/face/kiss.png rename to public/assets/face/kiss.png diff --git a/TouchVueThree/public/assets/face/knock.png b/public/assets/face/knock.png similarity index 100% rename from TouchVueThree/public/assets/face/knock.png rename to public/assets/face/knock.png diff --git a/TouchVueThree/public/assets/face/leisurely.png b/public/assets/face/leisurely.png similarity index 100% rename from TouchVueThree/public/assets/face/leisurely.png rename to public/assets/face/leisurely.png diff --git a/TouchVueThree/public/assets/face/let-me-see.png b/public/assets/face/let-me-see.png similarity index 100% rename from TouchVueThree/public/assets/face/let-me-see.png rename to public/assets/face/let-me-see.png diff --git a/TouchVueThree/public/assets/face/lustful.png b/public/assets/face/lustful.png similarity index 100% rename from TouchVueThree/public/assets/face/lustful.png rename to public/assets/face/lustful.png diff --git a/TouchVueThree/public/assets/face/naughty.png b/public/assets/face/naughty.png similarity index 100% rename from TouchVueThree/public/assets/face/naughty.png rename to public/assets/face/naughty.png diff --git a/TouchVueThree/public/assets/face/oh-my.png b/public/assets/face/oh-my.png similarity index 100% rename from TouchVueThree/public/assets/face/oh-my.png rename to public/assets/face/oh-my.png diff --git a/TouchVueThree/public/assets/face/panic.png b/public/assets/face/panic.png similarity index 100% rename from TouchVueThree/public/assets/face/panic.png rename to public/assets/face/panic.png diff --git a/TouchVueThree/public/assets/face/pick-nose.png b/public/assets/face/pick-nose.png similarity index 100% rename from TouchVueThree/public/assets/face/pick-nose.png rename to public/assets/face/pick-nose.png diff --git a/TouchVueThree/public/assets/face/pitiful.png b/public/assets/face/pitiful.png similarity index 100% rename from TouchVueThree/public/assets/face/pitiful.png rename to public/assets/face/pitiful.png diff --git a/TouchVueThree/public/assets/face/pout.png b/public/assets/face/pout.png similarity index 100% rename from TouchVueThree/public/assets/face/pout.png rename to public/assets/face/pout.png diff --git a/TouchVueThree/public/assets/face/question.png b/public/assets/face/question.png similarity index 100% rename from TouchVueThree/public/assets/face/question.png rename to public/assets/face/question.png diff --git a/TouchVueThree/public/assets/face/right-hum.png b/public/assets/face/right-hum.png similarity index 100% rename from TouchVueThree/public/assets/face/right-hum.png rename to public/assets/face/right-hum.png diff --git a/TouchVueThree/public/assets/face/roll-eyes.png b/public/assets/face/roll-eyes.png similarity index 100% rename from TouchVueThree/public/assets/face/roll-eyes.png rename to public/assets/face/roll-eyes.png diff --git a/TouchVueThree/public/assets/face/sad.png b/public/assets/face/sad.png similarity index 100% rename from TouchVueThree/public/assets/face/sad.png rename to public/assets/face/sad.png diff --git a/TouchVueThree/public/assets/face/shush.png b/public/assets/face/shush.png similarity index 100% rename from TouchVueThree/public/assets/face/shush.png rename to public/assets/face/shush.png diff --git a/TouchVueThree/public/assets/face/shut-up.png b/public/assets/face/shut-up.png similarity index 100% rename from TouchVueThree/public/assets/face/shut-up.png rename to public/assets/face/shut-up.png diff --git a/TouchVueThree/public/assets/face/shy.png b/public/assets/face/shy.png similarity index 100% rename from TouchVueThree/public/assets/face/shy.png rename to public/assets/face/shy.png diff --git a/TouchVueThree/public/assets/face/sick.png b/public/assets/face/sick.png similarity index 100% rename from TouchVueThree/public/assets/face/sick.png rename to public/assets/face/sick.png diff --git a/TouchVueThree/public/assets/face/sigh.png b/public/assets/face/sigh.png similarity index 100% rename from TouchVueThree/public/assets/face/sigh.png rename to public/assets/face/sigh.png diff --git a/TouchVueThree/public/assets/face/silly-smile.png b/public/assets/face/silly-smile.png similarity index 100% rename from TouchVueThree/public/assets/face/silly-smile.png rename to public/assets/face/silly-smile.png diff --git a/TouchVueThree/public/assets/face/sinister.png b/public/assets/face/sinister.png similarity index 100% rename from TouchVueThree/public/assets/face/sinister.png rename to public/assets/face/sinister.png diff --git a/TouchVueThree/public/assets/face/skull.png b/public/assets/face/skull.png similarity index 100% rename from TouchVueThree/public/assets/face/skull.png rename to public/assets/face/skull.png diff --git a/TouchVueThree/public/assets/face/slap-face.png b/public/assets/face/slap-face.png similarity index 100% rename from TouchVueThree/public/assets/face/slap-face.png rename to public/assets/face/slap-face.png diff --git a/TouchVueThree/public/assets/face/sleep.png b/public/assets/face/sleep.png similarity index 100% rename from TouchVueThree/public/assets/face/sleep.png rename to public/assets/face/sleep.png diff --git a/TouchVueThree/public/assets/face/sleepy.png b/public/assets/face/sleepy.png similarity index 100% rename from TouchVueThree/public/assets/face/sleepy.png rename to public/assets/face/sleepy.png diff --git a/TouchVueThree/public/assets/face/sly-smile.png b/public/assets/face/sly-smile.png similarity index 100% rename from TouchVueThree/public/assets/face/sly-smile.png rename to public/assets/face/sly-smile.png diff --git a/TouchVueThree/public/assets/face/smart.png b/public/assets/face/smart.png similarity index 100% rename from TouchVueThree/public/assets/face/smart.png rename to public/assets/face/smart.png diff --git a/TouchVueThree/public/assets/face/smile.png b/public/assets/face/smile.png similarity index 100% rename from TouchVueThree/public/assets/face/smile.png rename to public/assets/face/smile.png diff --git a/TouchVueThree/public/assets/face/smiley.png b/public/assets/face/smiley.png similarity index 100% rename from TouchVueThree/public/assets/face/smiley.png rename to public/assets/face/smiley.png diff --git a/TouchVueThree/public/assets/face/smug.png b/public/assets/face/smug.png similarity index 100% rename from TouchVueThree/public/assets/face/smug.png rename to public/assets/face/smug.png diff --git a/TouchVueThree/public/assets/face/snicker.png b/public/assets/face/snicker.png similarity index 100% rename from TouchVueThree/public/assets/face/snicker.png rename to public/assets/face/snicker.png diff --git a/TouchVueThree/public/assets/face/social.png b/public/assets/face/social.png similarity index 100% rename from TouchVueThree/public/assets/face/social.png rename to public/assets/face/social.png diff --git a/TouchVueThree/public/assets/face/speechless.png b/public/assets/face/speechless.png similarity index 100% rename from TouchVueThree/public/assets/face/speechless.png rename to public/assets/face/speechless.png diff --git a/TouchVueThree/public/assets/face/surprised.png b/public/assets/face/surprised.png similarity index 100% rename from TouchVueThree/public/assets/face/surprised.png rename to public/assets/face/surprised.png diff --git a/TouchVueThree/public/assets/face/sweat.png b/public/assets/face/sweat.png similarity index 100% rename from TouchVueThree/public/assets/face/sweat.png rename to public/assets/face/sweat.png diff --git a/TouchVueThree/public/assets/face/tears-to-smile.png b/public/assets/face/tears-to-smile.png similarity index 100% rename from TouchVueThree/public/assets/face/tears-to-smile.png rename to public/assets/face/tears-to-smile.png diff --git a/TouchVueThree/public/assets/face/unlucky.png b/public/assets/face/unlucky.png similarity index 100% rename from TouchVueThree/public/assets/face/unlucky.png rename to public/assets/face/unlucky.png diff --git a/TouchVueThree/public/assets/face/vomit.png b/public/assets/face/vomit.png similarity index 100% rename from TouchVueThree/public/assets/face/vomit.png rename to public/assets/face/vomit.png diff --git a/TouchVueThree/public/assets/face/wail.png b/public/assets/face/wail.png similarity index 100% rename from TouchVueThree/public/assets/face/wail.png rename to public/assets/face/wail.png diff --git a/TouchVueThree/public/assets/face/wipe-sweat.png b/public/assets/face/wipe-sweat.png similarity index 100% rename from TouchVueThree/public/assets/face/wipe-sweat.png rename to public/assets/face/wipe-sweat.png diff --git a/TouchVueThree/public/assets/face/wow.png b/public/assets/face/wow.png similarity index 100% rename from TouchVueThree/public/assets/face/wow.png rename to public/assets/face/wow.png diff --git a/TouchVueThree/public/assets/face/wronged.png b/public/assets/face/wronged.png similarity index 100% rename from TouchVueThree/public/assets/face/wronged.png rename to public/assets/face/wronged.png diff --git a/TouchVueThree/public/assets/face/yeah.png b/public/assets/face/yeah.png similarity index 100% rename from TouchVueThree/public/assets/face/yeah.png rename to public/assets/face/yeah.png diff --git a/TouchVueThree/public/assets/gesture/OK.png b/public/assets/gesture/OK.png similarity index 100% rename from TouchVueThree/public/assets/gesture/OK.png rename to public/assets/gesture/OK.png diff --git a/TouchVueThree/public/assets/gesture/beckon.png b/public/assets/gesture/beckon.png similarity index 100% rename from TouchVueThree/public/assets/gesture/beckon.png rename to public/assets/gesture/beckon.png diff --git a/TouchVueThree/public/assets/gesture/fist-salute.png b/public/assets/gesture/fist-salute.png similarity index 100% rename from TouchVueThree/public/assets/gesture/fist-salute.png rename to public/assets/gesture/fist-salute.png diff --git a/TouchVueThree/public/assets/gesture/fist.png b/public/assets/gesture/fist.png similarity index 100% rename from TouchVueThree/public/assets/gesture/fist.png rename to public/assets/gesture/fist.png diff --git a/TouchVueThree/public/assets/gesture/handshake.png b/public/assets/gesture/handshake.png similarity index 100% rename from TouchVueThree/public/assets/gesture/handshake.png rename to public/assets/gesture/handshake.png diff --git a/TouchVueThree/public/assets/gesture/hug.png b/public/assets/gesture/hug.png similarity index 100% rename from TouchVueThree/public/assets/gesture/hug.png rename to public/assets/gesture/hug.png diff --git a/TouchVueThree/public/assets/gesture/pray.png b/public/assets/gesture/pray.png similarity index 100% rename from TouchVueThree/public/assets/gesture/pray.png rename to public/assets/gesture/pray.png diff --git a/TouchVueThree/public/assets/gesture/strong.png b/public/assets/gesture/strong.png similarity index 100% rename from TouchVueThree/public/assets/gesture/strong.png rename to public/assets/gesture/strong.png diff --git a/TouchVueThree/public/assets/gesture/victory.png b/public/assets/gesture/victory.png similarity index 100% rename from TouchVueThree/public/assets/gesture/victory.png rename to public/assets/gesture/victory.png diff --git a/TouchVueThree/public/assets/gesture/weak.png b/public/assets/gesture/weak.png similarity index 100% rename from TouchVueThree/public/assets/gesture/weak.png rename to public/assets/gesture/weak.png diff --git a/TouchVueThree/public/assets/other/beer.png b/public/assets/other/beer.png similarity index 100% rename from TouchVueThree/public/assets/other/beer.png rename to public/assets/other/beer.png diff --git a/TouchVueThree/public/assets/other/bomb.png b/public/assets/other/bomb.png similarity index 100% rename from TouchVueThree/public/assets/other/bomb.png rename to public/assets/other/bomb.png diff --git a/TouchVueThree/public/assets/other/broken-heart.png b/public/assets/other/broken-heart.png similarity index 100% rename from TouchVueThree/public/assets/other/broken-heart.png rename to public/assets/other/broken-heart.png diff --git a/TouchVueThree/public/assets/other/cake.png b/public/assets/other/cake.png similarity index 100% rename from TouchVueThree/public/assets/other/cake.png rename to public/assets/other/cake.png diff --git a/TouchVueThree/public/assets/other/coffee.png b/public/assets/other/coffee.png similarity index 100% rename from TouchVueThree/public/assets/other/coffee.png rename to public/assets/other/coffee.png diff --git a/TouchVueThree/public/assets/other/heart.png b/public/assets/other/heart.png similarity index 100% rename from TouchVueThree/public/assets/other/heart.png rename to public/assets/other/heart.png diff --git a/TouchVueThree/public/assets/other/knife.png b/public/assets/other/knife.png similarity index 100% rename from TouchVueThree/public/assets/other/knife.png rename to public/assets/other/knife.png diff --git a/TouchVueThree/public/assets/other/lips.png b/public/assets/other/lips.png similarity index 100% rename from TouchVueThree/public/assets/other/lips.png rename to public/assets/other/lips.png diff --git a/TouchVueThree/public/assets/other/moon.png b/public/assets/other/moon.png similarity index 100% rename from TouchVueThree/public/assets/other/moon.png rename to public/assets/other/moon.png diff --git a/TouchVueThree/public/assets/other/poop.png b/public/assets/other/poop.png similarity index 100% rename from TouchVueThree/public/assets/other/poop.png rename to public/assets/other/poop.png diff --git a/TouchVueThree/public/assets/other/rose.png b/public/assets/other/rose.png similarity index 100% rename from TouchVueThree/public/assets/other/rose.png rename to public/assets/other/rose.png diff --git a/TouchVueThree/public/assets/other/sun.png b/public/assets/other/sun.png similarity index 100% rename from TouchVueThree/public/assets/other/sun.png rename to public/assets/other/sun.png diff --git a/TouchVueThree/public/assets/other/wither.png b/public/assets/other/wither.png similarity index 100% rename from TouchVueThree/public/assets/other/wither.png rename to public/assets/other/wither.png diff --git a/old/public/logo.png b/public/logo.png similarity index 100% rename from old/public/logo.png rename to public/logo.png diff --git a/old/public/manifest.json b/public/manifest.json similarity index 100% rename from old/public/manifest.json rename to public/manifest.json diff --git a/old/public/websdk.js b/public/websdk.js similarity index 100% rename from old/public/websdk.js rename to public/websdk.js diff --git a/old/src/App.tsx b/src/App.tsx similarity index 100% rename from old/src/App.tsx rename to src/App.tsx diff --git a/old/src/android-polyfills.ts b/src/android-polyfills.ts similarity index 100% rename from old/src/android-polyfills.ts rename to src/android-polyfills.ts diff --git a/old/src/api/ai.ts b/src/api/ai.ts similarity index 100% rename from old/src/api/ai.ts rename to src/api/ai.ts diff --git a/old/src/api/common.ts b/src/api/common.ts similarity index 100% rename from old/src/api/common.ts rename to src/api/common.ts diff --git a/old/src/api/module/group.ts b/src/api/module/group.ts similarity index 100% rename from old/src/api/module/group.ts rename to src/api/module/group.ts diff --git a/src/api/module/wechat.ts b/src/api/module/wechat.ts new file mode 100644 index 0000000..5e10f84 --- /dev/null +++ b/src/api/module/wechat.ts @@ -0,0 +1,5 @@ +import request from "@/api/request"; +//获取客服列表 +export function getCustomerList() { + return request("/v1/kefu/customerService/list", {}, "GET"); +} diff --git a/old/src/api/request.ts b/src/api/request.ts similarity index 100% rename from old/src/api/request.ts rename to src/api/request.ts diff --git a/old/src/api/request2.ts b/src/api/request2.ts similarity index 100% rename from old/src/api/request2.ts rename to src/api/request2.ts diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..fbb85ef --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,59 @@ +/** + * API 响应类型定义 + * + * 用于统一管理 API 响应数据结构,提供类型约束 + */ + +/** + * 标准 API 响应结构(带业务状态码) + * + * 常见格式: + * - { code: 200, success: true, msg: "成功", data: T } + * - { code: 200, success: true, msg: "成功", list: T[], total: number } + * - { detail: T } (详情接口) + */ +export interface ApiResponse { + code?: number; + success?: boolean; + msg?: string; + message?: string; + data?: T; + list?: T[]; // 列表接口常用字段 + total?: number; // 分页接口常用字段 + [key: string]: any; // 兼容其他字段 +} + +/** + * 详情接口响应结构(通常包含 detail 字段) + */ +export interface ApiDetailResponse { + code?: number; + success?: boolean; + msg?: string; + message?: string; + detail?: T; + data?: T; + [key: string]: any; +} + +/** + * 分页响应结构 + */ +export interface ApiPageResponse { + list: T[]; + total: number; + page?: number; + limit?: number; + [key: string]: any; +} + +/** + * 列表响应结构(兼容多种格式) + */ +export interface ApiListResponse { + list?: T[]; + data?: T[]; + items?: T[]; + records?: T[]; + [key: string]: any; +} diff --git a/old/src/components/AccountSelection/api.ts b/src/components/AccountSelection/api.ts similarity index 100% rename from old/src/components/AccountSelection/api.ts rename to src/components/AccountSelection/api.ts diff --git a/old/src/components/AccountSelection/data.ts b/src/components/AccountSelection/data.ts similarity index 100% rename from old/src/components/AccountSelection/data.ts rename to src/components/AccountSelection/data.ts diff --git a/old/src/components/AccountSelection/index.module.scss b/src/components/AccountSelection/index.module.scss similarity index 100% rename from old/src/components/AccountSelection/index.module.scss rename to src/components/AccountSelection/index.module.scss diff --git a/old/src/components/AccountSelection/index.tsx b/src/components/AccountSelection/index.tsx similarity index 100% rename from old/src/components/AccountSelection/index.tsx rename to src/components/AccountSelection/index.tsx diff --git a/old/src/components/AccountSelection/selectionPopup.tsx b/src/components/AccountSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/AccountSelection/selectionPopup.tsx rename to src/components/AccountSelection/selectionPopup.tsx diff --git a/old/src/components/AndroidCompatibilityCheck.tsx b/src/components/AndroidCompatibilityCheck.tsx similarity index 100% rename from old/src/components/AndroidCompatibilityCheck.tsx rename to src/components/AndroidCompatibilityCheck.tsx diff --git a/old/src/components/CompatibilityCheck.tsx b/src/components/CompatibilityCheck.tsx similarity index 100% rename from old/src/components/CompatibilityCheck.tsx rename to src/components/CompatibilityCheck.tsx diff --git a/old/src/components/ContactContextMenu/index.module.scss b/src/components/ContactContextMenu/index.module.scss similarity index 100% rename from old/src/components/ContactContextMenu/index.module.scss rename to src/components/ContactContextMenu/index.module.scss diff --git a/old/src/components/ContactContextMenu/index.tsx b/src/components/ContactContextMenu/index.tsx similarity index 100% rename from old/src/components/ContactContextMenu/index.tsx rename to src/components/ContactContextMenu/index.tsx diff --git a/old/src/components/ContentSelection/api.ts b/src/components/ContentSelection/api.ts similarity index 100% rename from old/src/components/ContentSelection/api.ts rename to src/components/ContentSelection/api.ts diff --git a/old/src/components/ContentSelection/data.ts b/src/components/ContentSelection/data.ts similarity index 100% rename from old/src/components/ContentSelection/data.ts rename to src/components/ContentSelection/data.ts diff --git a/old/src/components/ContentSelection/index.module.scss b/src/components/ContentSelection/index.module.scss similarity index 100% rename from old/src/components/ContentSelection/index.module.scss rename to src/components/ContentSelection/index.module.scss diff --git a/old/src/components/ContentSelection/index.tsx b/src/components/ContentSelection/index.tsx similarity index 100% rename from old/src/components/ContentSelection/index.tsx rename to src/components/ContentSelection/index.tsx diff --git a/old/src/components/ContentSelection/selectionPopup.tsx b/src/components/ContentSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/ContentSelection/selectionPopup.tsx rename to src/components/ContentSelection/selectionPopup.tsx diff --git a/old/src/components/DeviceSelection/api.ts b/src/components/DeviceSelection/api.ts similarity index 100% rename from old/src/components/DeviceSelection/api.ts rename to src/components/DeviceSelection/api.ts diff --git a/old/src/components/DeviceSelection/data.ts b/src/components/DeviceSelection/data.ts similarity index 100% rename from old/src/components/DeviceSelection/data.ts rename to src/components/DeviceSelection/data.ts diff --git a/old/src/components/DeviceSelection/index.module.scss b/src/components/DeviceSelection/index.module.scss similarity index 100% rename from old/src/components/DeviceSelection/index.module.scss rename to src/components/DeviceSelection/index.module.scss diff --git a/old/src/components/DeviceSelection/index.tsx b/src/components/DeviceSelection/index.tsx similarity index 100% rename from old/src/components/DeviceSelection/index.tsx rename to src/components/DeviceSelection/index.tsx diff --git a/old/src/components/DeviceSelection/selectionPopup.tsx b/src/components/DeviceSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/DeviceSelection/selectionPopup.tsx rename to src/components/DeviceSelection/selectionPopup.tsx diff --git a/old/src/components/EmojiSeclection/EmojiPicker.css b/src/components/EmojiSeclection/EmojiPicker.css similarity index 100% rename from old/src/components/EmojiSeclection/EmojiPicker.css rename to src/components/EmojiSeclection/EmojiPicker.css diff --git a/old/src/components/EmojiSeclection/EmojiPicker.tsx b/src/components/EmojiSeclection/EmojiPicker.tsx similarity index 100% rename from old/src/components/EmojiSeclection/EmojiPicker.tsx rename to src/components/EmojiSeclection/EmojiPicker.tsx diff --git a/old/src/components/EmojiSeclection/index.ts b/src/components/EmojiSeclection/index.ts similarity index 100% rename from old/src/components/EmojiSeclection/index.ts rename to src/components/EmojiSeclection/index.ts diff --git a/old/src/components/EmojiSeclection/wechatEmoji.ts b/src/components/EmojiSeclection/wechatEmoji.ts similarity index 100% rename from old/src/components/EmojiSeclection/wechatEmoji.ts rename to src/components/EmojiSeclection/wechatEmoji.ts diff --git a/old/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx similarity index 100% rename from old/src/components/ErrorBoundary/index.tsx rename to src/components/ErrorBoundary/index.tsx diff --git a/old/src/components/FriendSelection/api.ts b/src/components/FriendSelection/api.ts similarity index 100% rename from old/src/components/FriendSelection/api.ts rename to src/components/FriendSelection/api.ts diff --git a/old/src/components/FriendSelection/data.ts b/src/components/FriendSelection/data.ts similarity index 100% rename from old/src/components/FriendSelection/data.ts rename to src/components/FriendSelection/data.ts diff --git a/old/src/components/FriendSelection/index.module.scss b/src/components/FriendSelection/index.module.scss similarity index 100% rename from old/src/components/FriendSelection/index.module.scss rename to src/components/FriendSelection/index.module.scss diff --git a/old/src/components/FriendSelection/index.tsx b/src/components/FriendSelection/index.tsx similarity index 100% rename from old/src/components/FriendSelection/index.tsx rename to src/components/FriendSelection/index.tsx diff --git a/old/src/components/FriendSelection/selectionPopup.tsx b/src/components/FriendSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/FriendSelection/selectionPopup.tsx rename to src/components/FriendSelection/selectionPopup.tsx diff --git a/old/src/components/GroupContextMenu/index.module.scss b/src/components/GroupContextMenu/index.module.scss similarity index 100% rename from old/src/components/GroupContextMenu/index.module.scss rename to src/components/GroupContextMenu/index.module.scss diff --git a/old/src/components/GroupContextMenu/index.tsx b/src/components/GroupContextMenu/index.tsx similarity index 100% rename from old/src/components/GroupContextMenu/index.tsx rename to src/components/GroupContextMenu/index.tsx diff --git a/old/src/components/GroupSelection/api.ts b/src/components/GroupSelection/api.ts similarity index 100% rename from old/src/components/GroupSelection/api.ts rename to src/components/GroupSelection/api.ts diff --git a/old/src/components/GroupSelection/data.ts b/src/components/GroupSelection/data.ts similarity index 100% rename from old/src/components/GroupSelection/data.ts rename to src/components/GroupSelection/data.ts diff --git a/old/src/components/GroupSelection/index.module.scss b/src/components/GroupSelection/index.module.scss similarity index 100% rename from old/src/components/GroupSelection/index.module.scss rename to src/components/GroupSelection/index.module.scss diff --git a/old/src/components/GroupSelection/index.tsx b/src/components/GroupSelection/index.tsx similarity index 100% rename from old/src/components/GroupSelection/index.tsx rename to src/components/GroupSelection/index.tsx diff --git a/old/src/components/GroupSelection/selectionPopup.tsx b/src/components/GroupSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/GroupSelection/selectionPopup.tsx rename to src/components/GroupSelection/selectionPopup.tsx diff --git a/old/src/components/InfiniteList/InfiniteList.module.scss b/src/components/InfiniteList/InfiniteList.module.scss similarity index 100% rename from old/src/components/InfiniteList/InfiniteList.module.scss rename to src/components/InfiniteList/InfiniteList.module.scss diff --git a/old/src/components/InfiniteList/InfiniteList.tsx b/src/components/InfiniteList/InfiniteList.tsx similarity index 100% rename from old/src/components/InfiniteList/InfiniteList.tsx rename to src/components/InfiniteList/InfiniteList.tsx diff --git a/old/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx similarity index 100% rename from old/src/components/Layout/Layout.tsx rename to src/components/Layout/Layout.tsx diff --git a/old/src/components/Layout/LayoutFiexd.tsx b/src/components/Layout/LayoutFiexd.tsx similarity index 100% rename from old/src/components/Layout/LayoutFiexd.tsx rename to src/components/Layout/LayoutFiexd.tsx diff --git a/old/src/components/Layout/layout.module.scss b/src/components/Layout/layout.module.scss similarity index 100% rename from old/src/components/Layout/layout.module.scss rename to src/components/Layout/layout.module.scss diff --git a/old/src/components/LineChart.tsx b/src/components/LineChart.tsx similarity index 100% rename from old/src/components/LineChart.tsx rename to src/components/LineChart.tsx diff --git a/old/src/components/LineChart2.tsx b/src/components/LineChart2.tsx similarity index 100% rename from old/src/components/LineChart2.tsx rename to src/components/LineChart2.tsx diff --git a/old/src/components/MeauMobile/MeauMoible.tsx b/src/components/MeauMobile/MeauMoible.tsx similarity index 100% rename from old/src/components/MeauMobile/MeauMoible.tsx rename to src/components/MeauMobile/MeauMoible.tsx diff --git a/old/src/components/MemberSelection/TwoColumnMemberSelection.module.scss b/src/components/MemberSelection/TwoColumnMemberSelection.module.scss similarity index 100% rename from old/src/components/MemberSelection/TwoColumnMemberSelection.module.scss rename to src/components/MemberSelection/TwoColumnMemberSelection.module.scss diff --git a/old/src/components/MemberSelection/TwoColumnMemberSelection.tsx b/src/components/MemberSelection/TwoColumnMemberSelection.tsx similarity index 100% rename from old/src/components/MemberSelection/TwoColumnMemberSelection.tsx rename to src/components/MemberSelection/TwoColumnMemberSelection.tsx diff --git a/old/src/components/MemberSelection/index.tsx b/src/components/MemberSelection/index.tsx similarity index 100% rename from old/src/components/MemberSelection/index.tsx rename to src/components/MemberSelection/index.tsx diff --git a/old/src/components/MetailSelection/api.ts b/src/components/MetailSelection/api.ts similarity index 100% rename from old/src/components/MetailSelection/api.ts rename to src/components/MetailSelection/api.ts diff --git a/old/src/components/MetailSelection/data.ts b/src/components/MetailSelection/data.ts similarity index 100% rename from old/src/components/MetailSelection/data.ts rename to src/components/MetailSelection/data.ts diff --git a/old/src/components/MetailSelection/index.module.scss b/src/components/MetailSelection/index.module.scss similarity index 100% rename from old/src/components/MetailSelection/index.module.scss rename to src/components/MetailSelection/index.module.scss diff --git a/old/src/components/MetailSelection/index.tsx b/src/components/MetailSelection/index.tsx similarity index 100% rename from old/src/components/MetailSelection/index.tsx rename to src/components/MetailSelection/index.tsx diff --git a/old/src/components/MetailSelection/selectionPopup.tsx b/src/components/MetailSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/MetailSelection/selectionPopup.tsx rename to src/components/MetailSelection/selectionPopup.tsx diff --git a/old/src/components/NavCommon/index.tsx b/src/components/NavCommon/index.tsx similarity index 100% rename from old/src/components/NavCommon/index.tsx rename to src/components/NavCommon/index.tsx diff --git a/old/src/components/PlaceholderPage.tsx b/src/components/PlaceholderPage.tsx similarity index 100% rename from old/src/components/PlaceholderPage.tsx rename to src/components/PlaceholderPage.tsx diff --git a/old/src/components/PoolSelection/api.ts b/src/components/PoolSelection/api.ts similarity index 100% rename from old/src/components/PoolSelection/api.ts rename to src/components/PoolSelection/api.ts diff --git a/old/src/components/PoolSelection/data.ts b/src/components/PoolSelection/data.ts similarity index 100% rename from old/src/components/PoolSelection/data.ts rename to src/components/PoolSelection/data.ts diff --git a/old/src/components/PoolSelection/index.module.scss b/src/components/PoolSelection/index.module.scss similarity index 100% rename from old/src/components/PoolSelection/index.module.scss rename to src/components/PoolSelection/index.module.scss diff --git a/old/src/components/PoolSelection/index.tsx b/src/components/PoolSelection/index.tsx similarity index 100% rename from old/src/components/PoolSelection/index.tsx rename to src/components/PoolSelection/index.tsx diff --git a/old/src/components/PoolSelection/selectionPopup.tsx b/src/components/PoolSelection/selectionPopup.tsx similarity index 100% rename from old/src/components/PoolSelection/selectionPopup.tsx rename to src/components/PoolSelection/selectionPopup.tsx diff --git a/old/src/components/PopuLayout/footer.module.scss b/src/components/PopuLayout/footer.module.scss similarity index 100% rename from old/src/components/PopuLayout/footer.module.scss rename to src/components/PopuLayout/footer.module.scss diff --git a/old/src/components/PopuLayout/footer.tsx b/src/components/PopuLayout/footer.tsx similarity index 100% rename from old/src/components/PopuLayout/footer.tsx rename to src/components/PopuLayout/footer.tsx diff --git a/old/src/components/PopuLayout/header.module.scss b/src/components/PopuLayout/header.module.scss similarity index 100% rename from old/src/components/PopuLayout/header.module.scss rename to src/components/PopuLayout/header.module.scss diff --git a/old/src/components/PopuLayout/header.tsx b/src/components/PopuLayout/header.tsx similarity index 100% rename from old/src/components/PopuLayout/header.tsx rename to src/components/PopuLayout/header.tsx diff --git a/old/src/components/PowerNavtion/index.module.scss b/src/components/PowerNavtion/index.module.scss similarity index 100% rename from old/src/components/PowerNavtion/index.module.scss rename to src/components/PowerNavtion/index.module.scss diff --git a/old/src/components/PowerNavtion/index.tsx b/src/components/PowerNavtion/index.tsx similarity index 100% rename from old/src/components/PowerNavtion/index.tsx rename to src/components/PowerNavtion/index.tsx diff --git a/old/src/components/PowerNavtion/types.ts b/src/components/PowerNavtion/types.ts similarity index 100% rename from old/src/components/PowerNavtion/types.ts rename to src/components/PowerNavtion/types.ts diff --git a/old/src/components/StepIndicator/index.tsx b/src/components/StepIndicator/index.tsx similarity index 100% rename from old/src/components/StepIndicator/index.tsx rename to src/components/StepIndicator/index.tsx diff --git a/old/src/components/TwoColumnSelection/TwoColumnSelection.module.scss b/src/components/TwoColumnSelection/TwoColumnSelection.module.scss similarity index 100% rename from old/src/components/TwoColumnSelection/TwoColumnSelection.module.scss rename to src/components/TwoColumnSelection/TwoColumnSelection.module.scss diff --git a/old/src/components/TwoColumnSelection/TwoColumnSelection.tsx b/src/components/TwoColumnSelection/TwoColumnSelection.tsx similarity index 100% rename from old/src/components/TwoColumnSelection/TwoColumnSelection.tsx rename to src/components/TwoColumnSelection/TwoColumnSelection.tsx diff --git a/old/src/components/UpdateNotification/index.tsx b/src/components/UpdateNotification/index.tsx similarity index 100% rename from old/src/components/UpdateNotification/index.tsx rename to src/components/UpdateNotification/index.tsx diff --git a/old/src/components/Upload/AudioRecorder/index.tsx b/src/components/Upload/AudioRecorder/index.tsx similarity index 100% rename from old/src/components/Upload/AudioRecorder/index.tsx rename to src/components/Upload/AudioRecorder/index.tsx diff --git a/old/src/components/Upload/AudioUpload/index.module.scss b/src/components/Upload/AudioUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/AudioUpload/index.module.scss rename to src/components/Upload/AudioUpload/index.module.scss diff --git a/old/src/components/Upload/AudioUpload/index.tsx b/src/components/Upload/AudioUpload/index.tsx similarity index 100% rename from old/src/components/Upload/AudioUpload/index.tsx rename to src/components/Upload/AudioUpload/index.tsx diff --git a/old/src/components/Upload/AvatarUpload/index.module.scss b/src/components/Upload/AvatarUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/AvatarUpload/index.module.scss rename to src/components/Upload/AvatarUpload/index.module.scss diff --git a/old/src/components/Upload/AvatarUpload/index.tsx b/src/components/Upload/AvatarUpload/index.tsx similarity index 100% rename from old/src/components/Upload/AvatarUpload/index.tsx rename to src/components/Upload/AvatarUpload/index.tsx diff --git a/old/src/components/Upload/ChatFileUpload/example.tsx b/src/components/Upload/ChatFileUpload/example.tsx similarity index 100% rename from old/src/components/Upload/ChatFileUpload/example.tsx rename to src/components/Upload/ChatFileUpload/example.tsx diff --git a/old/src/components/Upload/ChatFileUpload/index.module.scss b/src/components/Upload/ChatFileUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/ChatFileUpload/index.module.scss rename to src/components/Upload/ChatFileUpload/index.module.scss diff --git a/old/src/components/Upload/ChatFileUpload/index.tsx b/src/components/Upload/ChatFileUpload/index.tsx similarity index 100% rename from old/src/components/Upload/ChatFileUpload/index.tsx rename to src/components/Upload/ChatFileUpload/index.tsx diff --git a/old/src/components/Upload/FileUpload/index.module.scss b/src/components/Upload/FileUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/FileUpload/index.module.scss rename to src/components/Upload/FileUpload/index.module.scss diff --git a/old/src/components/Upload/FileUpload/index.tsx b/src/components/Upload/FileUpload/index.tsx similarity index 100% rename from old/src/components/Upload/FileUpload/index.tsx rename to src/components/Upload/FileUpload/index.tsx diff --git a/old/src/components/Upload/ImageUpload/ImageUpload.tsx b/src/components/Upload/ImageUpload/ImageUpload.tsx similarity index 100% rename from old/src/components/Upload/ImageUpload/ImageUpload.tsx rename to src/components/Upload/ImageUpload/ImageUpload.tsx diff --git a/old/src/components/Upload/ImageUpload/index.module.scss b/src/components/Upload/ImageUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/ImageUpload/index.module.scss rename to src/components/Upload/ImageUpload/index.module.scss diff --git a/old/src/components/Upload/MainImgUpload/index.module.scss b/src/components/Upload/MainImgUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/MainImgUpload/index.module.scss rename to src/components/Upload/MainImgUpload/index.module.scss diff --git a/old/src/components/Upload/MainImgUpload/index.tsx b/src/components/Upload/MainImgUpload/index.tsx similarity index 100% rename from old/src/components/Upload/MainImgUpload/index.tsx rename to src/components/Upload/MainImgUpload/index.tsx diff --git a/old/src/components/Upload/README.md b/src/components/Upload/README.md similarity index 100% rename from old/src/components/Upload/README.md rename to src/components/Upload/README.md diff --git a/old/src/components/Upload/SimpleFileUpload/index.tsx b/src/components/Upload/SimpleFileUpload/index.tsx similarity index 100% rename from old/src/components/Upload/SimpleFileUpload/index.tsx rename to src/components/Upload/SimpleFileUpload/index.tsx diff --git a/old/src/components/Upload/VideoUpload/index.module.scss b/src/components/Upload/VideoUpload/index.module.scss similarity index 100% rename from old/src/components/Upload/VideoUpload/index.module.scss rename to src/components/Upload/VideoUpload/index.module.scss diff --git a/old/src/components/Upload/VideoUpload/index.tsx b/src/components/Upload/VideoUpload/index.tsx similarity index 100% rename from old/src/components/Upload/VideoUpload/index.tsx rename to src/components/Upload/VideoUpload/index.tsx diff --git a/old/src/components/VirtualContactList/index.module.scss b/src/components/VirtualContactList/index.module.scss similarity index 100% rename from old/src/components/VirtualContactList/index.module.scss rename to src/components/VirtualContactList/index.module.scss diff --git a/old/src/components/VirtualContactList/index.tsx b/src/components/VirtualContactList/index.tsx similarity index 100% rename from old/src/components/VirtualContactList/index.tsx rename to src/components/VirtualContactList/index.tsx diff --git a/old/src/components/VirtualMessageList/VirtualMessageList.tsx b/src/components/VirtualMessageList/VirtualMessageList.tsx similarity index 100% rename from old/src/components/VirtualMessageList/VirtualMessageList.tsx rename to src/components/VirtualMessageList/VirtualMessageList.tsx diff --git a/old/src/components/VirtualSessionList/index.module.scss b/src/components/VirtualSessionList/index.module.scss similarity index 100% rename from old/src/components/VirtualSessionList/index.module.scss rename to src/components/VirtualSessionList/index.module.scss diff --git a/old/src/components/VirtualSessionList/index.tsx b/src/components/VirtualSessionList/index.tsx similarity index 100% rename from old/src/components/VirtualSessionList/index.tsx rename to src/components/VirtualSessionList/index.tsx diff --git a/old/src/hooks/weChat/useChatMessages.ts b/src/hooks/weChat/useChatMessages.ts similarity index 100% rename from old/src/hooks/weChat/useChatMessages.ts rename to src/hooks/weChat/useChatMessages.ts diff --git a/old/src/hooks/weChat/useMessageGrouping.ts b/src/hooks/weChat/useMessageGrouping.ts similarity index 100% rename from old/src/hooks/weChat/useMessageGrouping.ts rename to src/hooks/weChat/useMessageGrouping.ts diff --git a/old/src/hooks/weChat/useMessageParser.tsx b/src/hooks/weChat/useMessageParser.tsx similarity index 100% rename from old/src/hooks/weChat/useMessageParser.tsx rename to src/hooks/weChat/useMessageParser.tsx diff --git a/src/hooks/weChat/useMessageTypeParser.tsx b/src/hooks/weChat/useMessageTypeParser.tsx new file mode 100644 index 0000000..c5d75fc --- /dev/null +++ b/src/hooks/weChat/useMessageTypeParser.tsx @@ -0,0 +1,170 @@ +import React, { useCallback } from "react"; +import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; +import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji"; +import { + MESSAGE_TYPE_MAP, + SPECIAL_TYPE_DETECTORS, + UNKNOWN_MESSAGE_CONFIG, + MessageTypeNodeProps, +} from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/messageTypes/messageTypeConfig"; +import styles from "@/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/com.module.scss"; + +/** + * 尝试解析 JSON + */ +const tryParseJson = (content: string): any => { + try { + return JSON.parse(content); + } catch { + return null; + } +}; + +/** + * 消息类型解析 Hook(重构版 - 使用对象映射) + */ +export const useMessageTypeParser = (contract: ContractData | weChatGroup) => { + // 判断是否为表情包URL的工具函数 + const isEmojiUrl = useCallback((content: string): boolean => { + return ( + content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") || + /\.(gif|webp|png|jpg|jpeg)$/i.test(content) || + content.includes("emoji") || + content.includes("sticker") || + content.includes("expression") + ); + }, []); + + // 解析表情包文字格式[表情名称]并替换为img标签 + 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( + , + ); + } 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 || content === "") { + return [消息内容不可用]; + } + + const rawContent = String(content); + const trimmedContent = rawContent.trim(); + + // 尝试解析 JSON(缓存结果) + const parsedJson = tryParseJson(trimmedContent); + + // 构建渲染属性 + const nodeProps: MessageTypeNodeProps = { + content: rawContent, + msg, + contract, + parsedJson, + parseEmojiText, + isEmojiUrl, + }; + + try { + // 1. 如果有明确的 msgType,优先查找配置 + if (msgType !== undefined && MESSAGE_TYPE_MAP[msgType]) { + const config = MESSAGE_TYPE_MAP[msgType]; + return config.nodeFunc(nodeProps); + } + + // 2. 尝试通过内容特征推导类型 + for (const detector of SPECIAL_TYPE_DETECTORS) { + try { + if (detector.detector(trimmedContent, parsedJson)) { + console.log(`🔍 推导出类型: ${detector.name} (msgType=${msgType || '未知'})`); + return detector.nodeFunc(nodeProps); + } + } catch (error) { + console.warn(`检测器 ${detector.name} 执行失败:`, error); + } + } + + // 3. 如果 msgType 存在但没有配置,尝试用 detector 推导 + if (msgType !== undefined) { + // 遍历所有配置,找到有 detector 且能匹配的 + for (const [type, config] of Object.entries(MESSAGE_TYPE_MAP)) { + if (config.detector) { + try { + if (config.detector(trimmedContent, parsedJson)) { + console.log(`🔍 通过 detector 推导: msgType=${type} (${config.type})`); + return config.nodeFunc(nodeProps); + } + } catch (error) { + console.warn(`msgType=${type} detector 执行失败:`, error); + } + } + } + } + + // 4. 使用未知类型兜底处理 + console.log(`⚠️ 未识别的消息类型,使用兜底处理 (msgType=${msgType || '未知'})`); + return UNKNOWN_MESSAGE_CONFIG.nodeFunc(nodeProps); + } catch (error) { + console.error("消息渲染失败:", error, { msg, content }); + return ( + + [消息渲染失败{msgType ? `: 类型${msgType}` : ""}] + + ); + } + }, + [contract, parseEmojiText, isEmojiUrl], + ); + + return { + parseMessageContent, + parseEmojiText, + isEmojiUrl, + }; +}; diff --git a/old/src/hooks/weChat/useWeChatSelectors.ts b/src/hooks/weChat/useWeChatSelectors.ts similarity index 100% rename from old/src/hooks/weChat/useWeChatSelectors.ts rename to src/hooks/weChat/useWeChatSelectors.ts diff --git a/old/src/main.tsx b/src/main.tsx similarity index 100% rename from old/src/main.tsx rename to src/main.tsx diff --git a/old/src/pages/404/index.module.scss b/src/pages/404/index.module.scss similarity index 100% rename from old/src/pages/404/index.module.scss rename to src/pages/404/index.module.scss diff --git a/old/src/pages/404/index.tsx b/src/pages/404/index.tsx similarity index 100% rename from old/src/pages/404/index.tsx rename to src/pages/404/index.tsx diff --git a/old/src/pages/guide/api.ts b/src/pages/guide/api.ts similarity index 100% rename from old/src/pages/guide/api.ts rename to src/pages/guide/api.ts diff --git a/old/src/pages/guide/index.module.scss b/src/pages/guide/index.module.scss similarity index 100% rename from old/src/pages/guide/index.module.scss rename to src/pages/guide/index.module.scss diff --git a/old/src/pages/guide/index.tsx b/src/pages/guide/index.tsx similarity index 100% rename from old/src/pages/guide/index.tsx rename to src/pages/guide/index.tsx diff --git a/old/src/pages/iframe/index.module.scss b/src/pages/iframe/index.module.scss similarity index 100% rename from old/src/pages/iframe/index.module.scss rename to src/pages/iframe/index.module.scss diff --git a/old/src/pages/iframe/index.tsx b/src/pages/iframe/index.tsx similarity index 100% rename from old/src/pages/iframe/index.tsx rename to src/pages/iframe/index.tsx diff --git a/old/src/pages/iframe/init.tsx b/src/pages/iframe/init.tsx similarity index 100% rename from old/src/pages/iframe/init.tsx rename to src/pages/iframe/init.tsx diff --git a/old/src/pages/index.tsx b/src/pages/index.tsx similarity index 100% rename from old/src/pages/index.tsx rename to src/pages/index.tsx diff --git a/old/src/pages/login/Login.tsx b/src/pages/login/Login.tsx similarity index 100% rename from old/src/pages/login/Login.tsx rename to src/pages/login/Login.tsx diff --git a/old/src/pages/login/api.ts b/src/pages/login/api.ts similarity index 100% rename from old/src/pages/login/api.ts rename to src/pages/login/api.ts diff --git a/old/src/pages/login/login.module.scss b/src/pages/login/login.module.scss similarity index 100% rename from old/src/pages/login/login.module.scss rename to src/pages/login/login.module.scss diff --git a/old/src/pages/mobile/Profile/Profile.tsx b/src/pages/mobile/Profile/Profile.tsx similarity index 100% rename from old/src/pages/mobile/Profile/Profile.tsx rename to src/pages/mobile/Profile/Profile.tsx diff --git a/old/src/pages/mobile/Profile/api.ts b/src/pages/mobile/Profile/api.ts similarity index 100% rename from old/src/pages/mobile/Profile/api.ts rename to src/pages/mobile/Profile/api.ts diff --git a/old/src/pages/mobile/Profile/index.module.scss b/src/pages/mobile/Profile/index.module.scss similarity index 100% rename from old/src/pages/mobile/Profile/index.module.scss rename to src/pages/mobile/Profile/index.module.scss diff --git a/old/src/pages/pc/ckbox/api.ts b/src/pages/pc/ckbox/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/api.ts rename to src/pages/pc/ckbox/api.ts diff --git a/old/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts b/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts rename to src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/api.ts diff --git a/old/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss b/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss rename to src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.module.scss diff --git a/old/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx b/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx rename to src/pages/pc/ckbox/commonConfig/components/ReceptionSettings/index.tsx diff --git a/old/src/pages/pc/ckbox/commonConfig/index.module.scss b/src/pages/pc/ckbox/commonConfig/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/commonConfig/index.module.scss rename to src/pages/pc/ckbox/commonConfig/index.module.scss diff --git a/old/src/pages/pc/ckbox/commonConfig/index.tsx b/src/pages/pc/ckbox/commonConfig/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/commonConfig/index.tsx rename to src/pages/pc/ckbox/commonConfig/index.tsx diff --git a/old/src/pages/pc/ckbox/components/NavCommon/Notice.tsx b/src/pages/pc/ckbox/components/NavCommon/Notice.tsx similarity index 100% rename from old/src/pages/pc/ckbox/components/NavCommon/Notice.tsx rename to src/pages/pc/ckbox/components/NavCommon/Notice.tsx diff --git a/old/src/pages/pc/ckbox/components/NavCommon/api.ts b/src/pages/pc/ckbox/components/NavCommon/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/components/NavCommon/api.ts rename to src/pages/pc/ckbox/components/NavCommon/api.ts diff --git a/old/src/pages/pc/ckbox/components/NavCommon/index.module.scss b/src/pages/pc/ckbox/components/NavCommon/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/components/NavCommon/index.module.scss rename to src/pages/pc/ckbox/components/NavCommon/index.module.scss diff --git a/old/src/pages/pc/ckbox/components/NavCommon/index.tsx b/src/pages/pc/ckbox/components/NavCommon/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/components/NavCommon/index.tsx rename to src/pages/pc/ckbox/components/NavCommon/index.tsx diff --git a/old/src/pages/pc/ckbox/dashboard/index.module.scss b/src/pages/pc/ckbox/dashboard/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/dashboard/index.module.scss rename to src/pages/pc/ckbox/dashboard/index.module.scss diff --git a/old/src/pages/pc/ckbox/dashboard/index.tsx b/src/pages/pc/ckbox/dashboard/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/dashboard/index.tsx rename to src/pages/pc/ckbox/dashboard/index.tsx diff --git a/old/src/pages/pc/ckbox/data.ts b/src/pages/pc/ckbox/data.ts similarity index 100% rename from old/src/pages/pc/ckbox/data.ts rename to src/pages/pc/ckbox/data.ts diff --git a/old/src/pages/pc/ckbox/index.tsx b/src/pages/pc/ckbox/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/index.tsx rename to src/pages/pc/ckbox/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss b/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss rename to src/pages/pc/ckbox/powerCenter/ai-training/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx b/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/ai-training/index.tsx rename to src/pages/pc/ckbox/powerCenter/ai-training/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/auto-greeting/index.module.scss b/src/pages/pc/ckbox/powerCenter/auto-greeting/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/auto-greeting/index.module.scss rename to src/pages/pc/ckbox/powerCenter/auto-greeting/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/auto-greeting/index.tsx b/src/pages/pc/ckbox/powerCenter/auto-greeting/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/auto-greeting/index.tsx rename to src/pages/pc/ckbox/powerCenter/auto-greeting/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/communication-record/index.module.scss b/src/pages/pc/ckbox/powerCenter/communication-record/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/communication-record/index.module.scss rename to src/pages/pc/ckbox/powerCenter/communication-record/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/communication-record/index.tsx b/src/pages/pc/ckbox/powerCenter/communication-record/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/communication-record/index.tsx rename to src/pages/pc/ckbox/powerCenter/communication-record/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/api.ts b/src/pages/pc/ckbox/powerCenter/content-management/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/api.ts rename to src/pages/pc/ckbox/powerCenter/content-management/api.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx b/src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx rename to src/pages/pc/ckbox/powerCenter/content-management/components/EditMomentModal.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss b/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss rename to src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx b/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx rename to src/pages/pc/ckbox/powerCenter/content-management/components/MomentPublish.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx b/src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx rename to src/pages/pc/ckbox/powerCenter/content-management/components/PreviewMomentModal.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss b/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss rename to src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx b/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx rename to src/pages/pc/ckbox/powerCenter/content-management/components/PublishSchedule.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/api.ts b/src/pages/pc/ckbox/powerCenter/content-management/components/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/api.ts rename to src/pages/pc/ckbox/powerCenter/content-management/components/api.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts b/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/components/index.ts rename to src/pages/pc/ckbox/powerCenter/content-management/components/index.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/index.module.scss b/src/pages/pc/ckbox/powerCenter/content-management/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/index.module.scss rename to src/pages/pc/ckbox/powerCenter/content-management/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/content-management/index.tsx b/src/pages/pc/ckbox/powerCenter/content-management/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/content-management/index.tsx rename to src/pages/pc/ckbox/powerCenter/content-management/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/customer-management/index.module.scss b/src/pages/pc/ckbox/powerCenter/customer-management/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/customer-management/index.module.scss rename to src/pages/pc/ckbox/powerCenter/customer-management/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/customer-management/index.tsx b/src/pages/pc/ckbox/powerCenter/customer-management/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/customer-management/index.tsx rename to src/pages/pc/ckbox/powerCenter/customer-management/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx b/src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx rename to src/pages/pc/ckbox/powerCenter/data-statistics/index.data.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss b/src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss rename to src/pages/pc/ckbox/powerCenter/data-statistics/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx b/src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx rename to src/pages/pc/ckbox/powerCenter/data-statistics/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/index.data.tsx b/src/pages/pc/ckbox/powerCenter/index.data.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/index.data.tsx rename to src/pages/pc/ckbox/powerCenter/index.data.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/index.module.scss b/src/pages/pc/ckbox/powerCenter/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/index.module.scss rename to src/pages/pc/ckbox/powerCenter/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/index.tsx b/src/pages/pc/ckbox/powerCenter/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/index.tsx rename to src/pages/pc/ckbox/powerCenter/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss b/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/components/PushTaskModal.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectAccount/index.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectAccount/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectAccount/index.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectAccount/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectContacts/index.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectContacts/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectContacts/index.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSelectContacts/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/ContentLibrarySelector.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/ContentLibrarySelector.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/ContentLibrarySelector.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/ContentLibrarySelector.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/api.ts b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/api.ts rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/api.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts b/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss b/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx b/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx rename to src/pages/pc/ckbox/powerCenter/message-push-assistant/index.tsx diff --git a/old/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/src/pages/pc/ckbox/powerCenter/push-history/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/push-history/api.ts rename to src/pages/pc/ckbox/powerCenter/push-history/api.ts diff --git a/old/src/pages/pc/ckbox/powerCenter/push-history/index.module.scss b/src/pages/pc/ckbox/powerCenter/push-history/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/push-history/index.module.scss rename to src/pages/pc/ckbox/powerCenter/push-history/index.module.scss diff --git a/old/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/src/pages/pc/ckbox/powerCenter/push-history/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/powerCenter/push-history/index.tsx rename to src/pages/pc/ckbox/powerCenter/push-history/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/api.ts b/src/pages/pc/ckbox/weChat/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/api.ts rename to src/pages/pc/ckbox/weChat/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/ChatWindow.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ChatRecordSearch/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/FollowupReminderModal/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/MessageEnter.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/MessageEnter.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/MessageEnter.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/MessageEnter.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/toContract/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/api.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/api.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/com.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/com.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/com.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/com.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/AudioMessage/AudioMessage.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/ClickMenu.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/ClickMenu.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/ClickMenu.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/ClickMenu.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/ClickMeau/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/RedPacketMessage.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/RedPacketMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/SmallProgramMessage.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SmallProgramMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/SystemRecommendRemarkMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/TransferMessage.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/TransferMessage.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/TransferMessage.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/TransferMessage.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransferMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/TransmitModal.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/TransmitModal.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/TransmitModal.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/TransmitModal.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/TransmitModal/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/VideoMessage.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VideoMessage/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VirtualizedMessageList.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VirtualizedMessageList.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VirtualizedMessageList.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/VirtualizedMessageList.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/Person.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/api.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/api.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/api.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/api.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/friendCard.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/friendCard.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/friendCard.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/components/friendCard.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.data.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.data.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.data.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.data.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.func.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.func.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.func.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.func.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/FriendsCicle/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/Person.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/api.ts b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/api.ts rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/GroupModal.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/QuickReplyModal.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/QuickReplyModal.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/QuickReplyModal.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/components/QuickReplyModal.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/QuickWords/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.module.scss b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.module.scss rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/components/TodoListModal/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx b/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx rename to src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss b/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss rename to src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx b/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx rename to src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/api.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/api.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.module.scss rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/AddFriends/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/api.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/api.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/api.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/api.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/components/friendCard.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.func.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.func.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.func.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.func.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.module.scss rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/com.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/data.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.virtual.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/PopChatRoom/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/SidebarMenu.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/SidebarMenu.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/SidebarMenu.module.scss rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/SidebarMenu.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/api.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/api.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/api.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/api.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts b/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx rename to src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/components/Skeleton/index.module.scss b/src/pages/pc/ckbox/weChat/components/Skeleton/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/Skeleton/index.module.scss rename to src/pages/pc/ckbox/weChat/components/Skeleton/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/components/Skeleton/index.tsx b/src/pages/pc/ckbox/weChat/components/Skeleton/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/components/Skeleton/index.tsx rename to src/pages/pc/ckbox/weChat/components/Skeleton/index.tsx diff --git a/old/src/pages/pc/ckbox/weChat/data.ts b/src/pages/pc/ckbox/weChat/data.ts similarity index 100% rename from old/src/pages/pc/ckbox/weChat/data.ts rename to src/pages/pc/ckbox/weChat/data.ts diff --git a/old/src/pages/pc/ckbox/weChat/index.module.scss b/src/pages/pc/ckbox/weChat/index.module.scss similarity index 100% rename from old/src/pages/pc/ckbox/weChat/index.module.scss rename to src/pages/pc/ckbox/weChat/index.module.scss diff --git a/old/src/pages/pc/ckbox/weChat/index.tsx b/src/pages/pc/ckbox/weChat/index.tsx similarity index 100% rename from old/src/pages/pc/ckbox/weChat/index.tsx rename to src/pages/pc/ckbox/weChat/index.tsx diff --git a/old/src/polyfills.ts b/src/polyfills.ts similarity index 100% rename from old/src/polyfills.ts rename to src/polyfills.ts diff --git a/old/src/providers/QueryProvider.tsx b/src/providers/QueryProvider.tsx similarity index 100% rename from old/src/providers/QueryProvider.tsx rename to src/providers/QueryProvider.tsx diff --git a/old/src/react-app-env.d.ts b/src/react-app-env.d.ts similarity index 100% rename from old/src/react-app-env.d.ts rename to src/react-app-env.d.ts diff --git a/old/src/router/index.tsx b/src/router/index.tsx similarity index 100% rename from old/src/router/index.tsx rename to src/router/index.tsx diff --git a/old/src/router/module/common.tsx b/src/router/module/common.tsx similarity index 100% rename from old/src/router/module/common.tsx rename to src/router/module/common.tsx diff --git a/old/src/router/module/mobile.tsx b/src/router/module/mobile.tsx similarity index 100% rename from old/src/router/module/mobile.tsx rename to src/router/module/mobile.tsx diff --git a/old/src/router/module/pc.tsx b/src/router/module/pc.tsx similarity index 100% rename from old/src/router/module/pc.tsx rename to src/router/module/pc.tsx diff --git a/old/src/router/permissionRoute.tsx b/src/router/permissionRoute.tsx similarity index 100% rename from old/src/router/permissionRoute.tsx rename to src/router/permissionRoute.tsx diff --git a/old/src/store/createPersistStore.ts b/src/store/createPersistStore.ts similarity index 100% rename from old/src/store/createPersistStore.ts rename to src/store/createPersistStore.ts diff --git a/old/src/store/index.ts b/src/store/index.ts similarity index 100% rename from old/src/store/index.ts rename to src/store/index.ts diff --git a/old/src/store/module/app.ts b/src/store/module/app.ts similarity index 100% rename from old/src/store/module/app.ts rename to src/store/module/app.ts diff --git a/old/src/store/module/ckchat/api.ts b/src/store/module/ckchat/api.ts similarity index 100% rename from old/src/store/module/ckchat/api.ts rename to src/store/module/ckchat/api.ts diff --git a/old/src/store/module/ckchat/ckchat.data.ts b/src/store/module/ckchat/ckchat.data.ts similarity index 100% rename from old/src/store/module/ckchat/ckchat.data.ts rename to src/store/module/ckchat/ckchat.data.ts diff --git a/old/src/store/module/ckchat/ckchat.ts b/src/store/module/ckchat/ckchat.ts similarity index 100% rename from old/src/store/module/ckchat/ckchat.ts rename to src/store/module/ckchat/ckchat.ts diff --git a/old/src/store/module/dataCenter/index.data.ts b/src/store/module/dataCenter/index.data.ts similarity index 100% rename from old/src/store/module/dataCenter/index.data.ts rename to src/store/module/dataCenter/index.data.ts diff --git a/old/src/store/module/dataCenter/index.ts b/src/store/module/dataCenter/index.ts similarity index 100% rename from old/src/store/module/dataCenter/index.ts rename to src/store/module/dataCenter/index.ts diff --git a/old/src/store/module/settings.ts b/src/store/module/settings.ts similarity index 100% rename from old/src/store/module/settings.ts rename to src/store/module/settings.ts diff --git a/old/src/store/module/user.ts b/src/store/module/user.ts similarity index 100% rename from old/src/store/module/user.ts rename to src/store/module/user.ts diff --git a/old/src/store/module/weChat/account.ts b/src/store/module/weChat/account.ts similarity index 100% rename from old/src/store/module/weChat/account.ts rename to src/store/module/weChat/account.ts diff --git a/old/src/store/module/weChat/chatRecord.ts b/src/store/module/weChat/chatRecord.ts similarity index 100% rename from old/src/store/module/weChat/chatRecord.ts rename to src/store/module/weChat/chatRecord.ts diff --git a/old/src/store/module/weChat/contacts.data.ts b/src/store/module/weChat/contacts.data.ts similarity index 100% rename from old/src/store/module/weChat/contacts.data.ts rename to src/store/module/weChat/contacts.data.ts diff --git a/old/src/store/module/weChat/contacts.new.ts b/src/store/module/weChat/contacts.new.ts similarity index 100% rename from old/src/store/module/weChat/contacts.new.ts rename to src/store/module/weChat/contacts.new.ts diff --git a/old/src/store/module/weChat/contacts.ts b/src/store/module/weChat/contacts.ts similarity index 100% rename from old/src/store/module/weChat/contacts.ts rename to src/store/module/weChat/contacts.ts diff --git a/old/src/store/module/weChat/currentChat.ts b/src/store/module/weChat/currentChat.ts similarity index 100% rename from old/src/store/module/weChat/currentChat.ts rename to src/store/module/weChat/currentChat.ts diff --git a/old/src/store/module/weChat/customer.data.ts b/src/store/module/weChat/customer.data.ts similarity index 100% rename from old/src/store/module/weChat/customer.data.ts rename to src/store/module/weChat/customer.data.ts diff --git a/old/src/store/module/weChat/customer.ts b/src/store/module/weChat/customer.ts similarity index 100% rename from old/src/store/module/weChat/customer.ts rename to src/store/module/weChat/customer.ts diff --git a/old/src/store/module/weChat/message.data.ts b/src/store/module/weChat/message.data.ts similarity index 100% rename from old/src/store/module/weChat/message.data.ts rename to src/store/module/weChat/message.data.ts diff --git a/old/src/store/module/weChat/message.ts b/src/store/module/weChat/message.ts similarity index 100% rename from old/src/store/module/weChat/message.ts rename to src/store/module/weChat/message.ts diff --git a/old/src/store/module/weChat/weChat.data.ts b/src/store/module/weChat/weChat.data.ts similarity index 100% rename from old/src/store/module/weChat/weChat.data.ts rename to src/store/module/weChat/weChat.data.ts diff --git a/old/src/store/module/weChat/weChat.ts b/src/store/module/weChat/weChat.ts similarity index 100% rename from old/src/store/module/weChat/weChat.ts rename to src/store/module/weChat/weChat.ts diff --git a/old/src/store/module/websocket/msg.data.ts b/src/store/module/websocket/msg.data.ts similarity index 100% rename from old/src/store/module/websocket/msg.data.ts rename to src/store/module/websocket/msg.data.ts diff --git a/old/src/store/module/websocket/msgManage.ts b/src/store/module/websocket/msgManage.ts similarity index 100% rename from old/src/store/module/websocket/msgManage.ts rename to src/store/module/websocket/msgManage.ts diff --git a/old/src/store/module/websocket/websocket.ts b/src/store/module/websocket/websocket.ts similarity index 100% rename from old/src/store/module/websocket/websocket.ts rename to src/store/module/websocket/websocket.ts diff --git a/old/src/store/persistUtils.ts b/src/store/persistUtils.ts similarity index 100% rename from old/src/store/persistUtils.ts rename to src/store/persistUtils.ts diff --git a/old/src/styles/global.scss b/src/styles/global.scss similarity index 100% rename from old/src/styles/global.scss rename to src/styles/global.scss diff --git a/old/src/types/device.ts b/src/types/device.ts similarity index 100% rename from old/src/types/device.ts rename to src/types/device.ts diff --git a/old/src/types/weChat.ts b/src/types/weChat.ts similarity index 100% rename from old/src/types/weChat.ts rename to src/types/weChat.ts diff --git a/old/src/utils/apiUrl.ts b/src/utils/apiUrl.ts similarity index 100% rename from old/src/utils/apiUrl.ts rename to src/utils/apiUrl.ts diff --git a/old/src/utils/cache/index.ts b/src/utils/cache/index.ts similarity index 100% rename from old/src/utils/cache/index.ts rename to src/utils/cache/index.ts diff --git a/old/src/utils/chartColors.ts b/src/utils/chartColors.ts similarity index 100% rename from old/src/utils/chartColors.ts rename to src/utils/chartColors.ts diff --git a/old/src/utils/common.ts b/src/utils/common.ts similarity index 100% rename from old/src/utils/common.ts rename to src/utils/common.ts diff --git a/old/src/utils/dataIndex.ts b/src/utils/dataIndex.ts similarity index 100% rename from old/src/utils/dataIndex.ts rename to src/utils/dataIndex.ts diff --git a/old/src/utils/db.ts b/src/utils/db.ts similarity index 100% rename from old/src/utils/db.ts rename to src/utils/db.ts diff --git a/old/src/utils/dbAction/contact.ts b/src/utils/dbAction/contact.ts similarity index 100% rename from old/src/utils/dbAction/contact.ts rename to src/utils/dbAction/contact.ts diff --git a/old/src/utils/dbAction/index.ts b/src/utils/dbAction/index.ts similarity index 100% rename from old/src/utils/dbAction/index.ts rename to src/utils/dbAction/index.ts diff --git a/old/src/utils/dbAction/loginManager.ts b/src/utils/dbAction/loginManager.ts similarity index 100% rename from old/src/utils/dbAction/loginManager.ts rename to src/utils/dbAction/loginManager.ts diff --git a/old/src/utils/dbAction/message.ts b/src/utils/dbAction/message.ts similarity index 100% rename from old/src/utils/dbAction/message.ts rename to src/utils/dbAction/message.ts diff --git a/old/src/utils/env.ts b/src/utils/env.ts similarity index 100% rename from old/src/utils/env.ts rename to src/utils/env.ts diff --git a/old/src/utils/errorHandler.ts b/src/utils/errorHandler.ts similarity index 100% rename from old/src/utils/errorHandler.ts rename to src/utils/errorHandler.ts diff --git a/old/src/utils/filter.ts b/src/utils/filter.ts similarity index 100% rename from old/src/utils/filter.ts rename to src/utils/filter.ts diff --git a/old/src/utils/icon.tsx b/src/utils/icon.tsx similarity index 100% rename from old/src/utils/icon.tsx rename to src/utils/icon.tsx diff --git a/TouchVueThree/src/utils/messagePreview.ts b/src/utils/messagePreview.ts similarity index 59% rename from TouchVueThree/src/utils/messagePreview.ts rename to src/utils/messagePreview.ts index 6648794..627d65d 100644 --- a/TouchVueThree/src/utils/messagePreview.ts +++ b/src/utils/messagePreview.ts @@ -1,30 +1,30 @@ /** * 消息预览格式化工具 - * 用于会话列表中显示消息预览,参考 content数据实例.md + * 用于会话列表中显示消息预览,参考 会话列表预览消息规则.md */ // 图片扩展名正则 -const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i +const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i; // 视频扩展名正则 -const VIDEO_EXT_REGEX = /\.(mp4|avi|mov|wmv|flv|mkv|webm|m4v)$/i +const VIDEO_EXT_REGEX = /\.(mp4|avi|mov|wmv|flv|mkv|webm|m4v)$/i; // 音频扩展名正则 -const AUDIO_EXT_REGEX = /\.(mp3|wav|wma|flac|aac|ogg|m4a)$/i +const AUDIO_EXT_REGEX = /\.(mp3|wav|wma|flac|aac|ogg|m4a)$/i; // 阿里云 OSS 前缀 -const ALIYUN_OSS_PREFIX = 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com' +const ALIYUN_OSS_PREFIX = 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com'; /** * 尝试解析 JSON */ const tryParseJson = (content: string): Record | null => { try { - return JSON.parse(content) + return JSON.parse(content); } catch { - return null + return null; } -} +}; /** * 从 XML 字符串中提取 title @@ -32,44 +32,46 @@ const tryParseJson = (content: string): Record | null => { const extractTitleFromXml = (xmlString: string): string | null => { try { // 尝试提取 标签内容 - const titleMatch = xmlString.match(/([^<]*(?:[^<]*)*)<\/title>/i) + const titleMatch = xmlString.match( + /([^<]*(?:[^<]*)*)<\/title>/i + ); if (titleMatch && titleMatch[1]) { - let title = titleMatch[1] + let title = titleMatch[1]; // 处理 CDATA - title = title.replace(//gi, '$1') + title = title.replace(//gi, '$1'); // 去除首尾空白 - title = title.trim() + title = title.trim(); if (title) { - return title + return title; } } } catch { // 解析失败,返回 null } - return null -} + return null; +}; /** * 检查是否为阿里云 OSS 链接,并判断类型 */ const checkAliyunOssLink = (url: string): '图片' | '视频' | '音频' | null => { if (!url.includes(ALIYUN_OSS_PREFIX)) { - return null + return null; } // 根据文件扩展名判断类型 if (IMAGE_EXT_REGEX.test(url)) { - return '图片' + return '图片'; } if (VIDEO_EXT_REGEX.test(url)) { - return '视频' + return '视频'; } if (AUDIO_EXT_REGEX.test(url)) { - return '音频' + return '音频'; } - return null -} + return null; +}; /** * 检查是否为小程序消息 @@ -77,25 +79,25 @@ const checkAliyunOssLink = (url: string): '图片' | '视频' | '音频' | null const isMiniProgramMessage = (jsonData: Record): boolean => { // 检查是否有 contentXml 且包含 appid if (jsonData.contentXml && typeof jsonData.contentXml === 'string') { - const xmlContent = jsonData.contentXml + const xmlContent = jsonData.contentXml; // 检查是否包含 if (xmlContent.includes(' { if (typeof obj === 'string' && obj.includes(ALIYUN_OSS_PREFIX)) { - return obj + return obj; } if (typeof obj === 'object' && obj !== null) { for (const value of Object.values(obj)) { - const link = findAliyunOssLink(value) + const link = findAliyunOssLink(value); if (link) { - return link + return link; } } } - return null - } + return null; + }; - const ossLink = findAliyunOssLink(jsonData) + const ossLink = findAliyunOssLink(jsonData); if (ossLink) { - const ossType = checkAliyunOssLink(ossLink) + const ossType = checkAliyunOssLink(ossLink); if (ossType) { - return `[${ossType}]` + return `[${ossType}]`; } } // 2.3 尝试从 contentXml 中提取 title if (jsonData.contentXml && typeof jsonData.contentXml === 'string') { - const title = extractTitleFromXml(jsonData.contentXml) + const title = extractTitleFromXml(jsonData.contentXml); if (title) { // 限制长度 - const maxLength = 50 - return title.length > maxLength ? title.substring(0, maxLength) + '...' : title + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; } } @@ -170,65 +174,77 @@ export function formatMessagePreview( // 如果 JSON 字符串很长(超过 500 字符),可能被截断 if (trimmed.length > 500) { // 尝试提取 title - const title = extractTitleFromXml(trimmed) + const title = extractTitleFromXml(trimmed); if (title) { - const maxLength = 50 - return title.length > maxLength ? title.substring(0, maxLength) + '...' : title + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; } - return '[文本过长]' + return '[文本过长]'; } // 2.5 尝试从 JSON 中提取有意义的信息 if (jsonData.title) { - const title = String(jsonData.title) - const maxLength = 50 - return title.length > maxLength ? title.substring(0, maxLength) + '...' : title + const title = String(jsonData.title); + const maxLength = 50; + return title.length > maxLength + ? title.substring(0, maxLength) + '...' + : title; } if (jsonData.content) { - const content = String(jsonData.content) - const maxLength = 50 - return content.length > maxLength ? content.substring(0, maxLength) + '...' : content + const content = String(jsonData.content); + const maxLength = 50; + return content.length > maxLength + ? content.substring(0, maxLength) + '...' + : content; } // 2.6 无法识别的 JSON,返回通用提示 - return '[消息]' + return '[消息]'; } // 3. 检查是否为普通 HTTP 链接 if (/^https?:\/\//i.test(trimmed)) { // 检查是否为图片链接 if (IMAGE_EXT_REGEX.test(trimmed)) { - return '[图片]' + return '[图片]'; } // 检查是否为视频链接 if (VIDEO_EXT_REGEX.test(trimmed)) { - return '[视频]' + return '[视频]'; } // 检查是否为音频链接 if (AUDIO_EXT_REGEX.test(trimmed)) { - return '[音频]' + return '[音频]'; } // 普通链接 - return '[链接]' + return '[链接]'; } // 4. 检查是否为 XML 字符串(但没有被 JSON 包裹) - if (trimmed.includes('') || trimmed.includes('') || + trimmed.includes('
好友选择功能开发中...
需要从联系人列表选择好友
页面不存在
{{ moment.content }}
选择一个聊天开始对话
确定要删除分组 "{{ group?.groupName }}" 吗?
删除后,该分组下的联系人将移动到默认分组
数据看板功能开发中...
登录您的账户继续使用
AI 训练功能开发中...
内容管理功能开发中...
客户管理功能开发中...
数据统计功能开发中...
系统设置功能开发中...
在线: {{ onlineCount }} / {{ accountList.length }}
- 已帮您生成3条回复建议... -
- 张三: 谁有新的优惠券链接? -
- 好的,合同我让财务看一下。 -
- "亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。" -
ID: ys666wow
- 您好,很高兴为您服务!请问有什么可以帮您的吗?我们目前正在进行... -
- 亲,这个价格已经是我们的底价了,包含了一年的售后服务,性价比非常高... -
- 已为您生成 3 条回复建议... -
- 张三: [图片] 谁有这个券? -
微信在线
- “亲,没问题的!既然是老客户,这次直接帮您申请赠送幻化服务。现在下单的话,我这就去安排打手准备。” -