From dc58109829cf7d937fc89be08b21f5fa9d6ac129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 23 Oct 2025 19:56:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=81=94=E7=B3=BB=E4=BA=BA?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=81=94=E7=B3=BB=E4=BA=BA=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=92=8C=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E8=81=94=E7=B3=BB=E4=BA=BA=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=BB=84=E4=BB=B6=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C=E5=92=8C=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ProfileModules/index.tsx | 9 +- .../SidebarMenu/MessageList/index.tsx | 12 +- .../WechatFriends/WechatFriends.module.scss | 70 ++- .../SidebarMenu/WechatFriends/extend.ts | 255 +++++++++ .../SidebarMenu/WechatFriends/index.tsx | 306 +++++----- .../weChat/components/SidebarMenu/index.tsx | 16 +- Touchkebao/src/pages/pc/ckbox/weChat/main.ts | 69 +-- .../src/store/module/ckchat/ckchat.data.ts | 9 - Touchkebao/src/store/module/ckchat/ckchat.ts | 259 +-------- .../src/store/module/weChat/contacts.data.ts | 70 +++ .../src/store/module/weChat/contacts.ts | 539 ++++++++++++++++++ .../src/store/module/weChat/weChat.data.ts | 11 - Touchkebao/src/store/module/weChat/weChat.ts | 19 +- Touchkebao/src/utils/db.ts | 1 + Touchkebao/src/utils/dbAction/contact.ts | 317 ++++++++++ Touchkebao/src/utils/dbAction/loginManager.ts | 169 ++++++ 16 files changed, 1614 insertions(+), 517 deletions(-) create mode 100644 Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts create mode 100644 Touchkebao/src/store/module/weChat/contacts.data.ts diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index 58bb50fe..fa96bcb2 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -25,6 +25,7 @@ import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import { useCkChatStore } from "@/store/module/ckchat/ckchat"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; import { useWeChatStore } from "@/store/module/weChat/weChat"; +import { useContactStore } from "@/store/module/weChat/contacts"; import { generateAiText } from "@/api/ai"; import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection"; import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection"; @@ -212,9 +213,7 @@ const Person: React.FC = ({ contract }) => { state.getKfUserInfo(contract.wechatAccountId || 0), ); - const getSomeContractList = useCkChatStore( - state => state.getSomeContractList, - ); + const { getContactsByCustomer } = useContactStore(); const { sendCommand } = useWebSocketStore(); @@ -931,10 +930,10 @@ const Person: React.FC = ({ contract }) => { icon={} onClick={async () => { try { - const contractData = await getSomeContractList( + const contractData = getContactsByCustomer( contract.wechatAccountId, ); - // 转换 ContractData[] 为 FriendSelectionItem[] + // 转换 Contact[] 为 FriendSelectionItem[] const friendSelectionData = (contractData || []).map( item => ({ id: item.id || item.serverId, diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index 41743f87..ab344821 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -7,10 +7,12 @@ import { DeleteOutlined, EditOutlined, } from "@ant-design/icons"; -import { useWeChatStore } from "@/store/module/weChat/weChat"; -import { useCkChatStore } from "@/store/module/ckchat/ckchat"; +import { useWeChatStore } from "@weChatStore/weChat"; +import { useMessageStore } from "@weChatStore/message"; +import { useCustomerStore } from "@weChatStore/customer"; +import { useContactStore } from "@weChatStore/contacts"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; -import { useCustomerStore } from "@/store/module/weChat/customer"; + import { updateConfig } from "@/pages/pc/ckbox/api"; import { getMessageList } from "./api"; import { dataProcessing } from "./api"; @@ -18,13 +20,13 @@ import styles from "./MessageList.module.scss"; import { formatWechatTime } from "@/utils/common"; import { MessageManager } from "@/utils/dbAction/message"; import { ChatSession } from "@/utils/db"; -import { useMessageStore } from "@/store/module/weChat/message"; + import { useUserStore } from "@/store/module/user"; interface MessageListProps {} const MessageList: React.FC = () => { + const searchKeyword = useContactStore(state => state.searchKeyword); const { setCurrentContact, currentContract } = useWeChatStore(); - const searchKeyword = useCkChatStore(state => state.searchKeyword); const { currentCustomer } = useCustomerStore(); const { sendCommand } = useWebSocketStore(); const { user } = useUserStore(); diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss index 00633207..7ea6903a 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss @@ -44,28 +44,28 @@ } .groupPanel { - background-color: transparent; -} + background-color: transparent; + } -.loadMoreContainer { - display: flex; - justify-content: center; - padding: 10px 0; -} + .loadMoreContainer { + display: flex; + justify-content: center; + padding: 10px 0; + } -.noMoreText { - text-align: center; - color: #999; - font-size: 12px; - padding: 10px 0; -} + .noMoreText { + text-align: center; + color: #999; + font-size: 12px; + padding: 10px 0; + } -.noResults { - text-align: center; - color: #999; - padding: 20px; - font-size: 14px; -} + .noResults { + text-align: center; + color: #999; + padding: 20px; + font-size: 14px; + } .list { flex: 1; @@ -119,4 +119,36 @@ overflow: hidden; text-overflow: ellipsis; } + + .groupInfo { + font-size: 12px; + color: #999; + margin-top: 4px; + } + + // 骨架屏样式 + .skeletonContainer { + padding: 10px; + } + + .skeletonItem { + display: flex; + align-items: center; + padding: 10px 15px; + gap: 12px; + } + + .skeletonInfo { + flex: 1; + } + + // 刷新提示 + .refreshingTip { + padding: 8px 15px; + background-color: #e6f7ff; + border-bottom: 1px solid #91d5ff; + color: #1890ff; + font-size: 12px; + text-align: center; + } } diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts new file mode 100644 index 00000000..88cf1a25 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts @@ -0,0 +1,255 @@ +import { Contact } from "@/utils/db"; +import { ContactManager } from "@/utils/dbAction"; +import { + getContactList, + getGroupList, + getLabelsListByGroup, +} from "@/pages/pc/ckbox/weChat/api"; +import { ContactGroupByLabel } from "@/pages/pc/ckbox/data"; + +/** + * 递归获取所有好友列表 + */ +export const getAllFriends = async () => { + try { + let allFriends = []; + let page = 1; + const limit = 500; + let hasMore = true; + + while (hasMore) { + const result = await getContactList({ page, limit }); + const friendList = result?.list || []; + + if ( + !friendList || + !Array.isArray(friendList) || + friendList.length === 0 + ) { + hasMore = false; + break; + } + + allFriends = [...allFriends, ...friendList]; + + if (friendList.length === 0) { + hasMore = false; + } else { + page = page + 1; + } + } + return allFriends; + } catch (error) { + console.error("获取所有好友列表失败:", error); + return []; + } +}; + +/** + * 递归获取所有群组列表 + */ +export const getAllGroups = async () => { + try { + let allGroups = []; + let page = 1; + const limit = 500; + let hasMore = true; + + while (hasMore) { + const result = await getGroupList({ page, limit }); + const groupList = result?.list || []; + + if (!groupList || !Array.isArray(groupList) || groupList.length === 0) { + hasMore = false; + break; + } + + allGroups = [...allGroups, ...groupList]; + + if (groupList.length < limit) { + hasMore = false; + } else { + // 获取最后一条数据的id作为下一次请求的page + const lastGroup = groupList[groupList.length - 1]; + page = lastGroup.id; + } + } + + return allGroups; + } catch (error) { + console.error("获取所有群列表失败:", error); + return []; + } +}; + +/** + * 将好友数据转换为统一的 Contact 格式 + */ +export const convertFriendsToContacts = ( + friends: any[], + userId: number, +): Contact[] => { + return friends.map((friend: any) => ({ + serverId: `friend_${friend.id}`, + userId, + id: friend.id, + type: "friend" as const, + wechatAccountId: friend.wechatAccountId, + wechatId: friend.wechatId, + nickname: friend.nickname || "", + conRemark: friend.conRemark || "", + avatar: friend.avatar || "", + groupId: friend.groupId, // 保留标签ID + lastUpdateTime: new Date().toISOString(), + sortKey: "", + searchKey: "", + })); +}; + +/** + * 将群组数据转换为统一的 Contact 格式 + */ +export const convertGroupsToContacts = ( + groups: any[], + userId: number, +): Contact[] => { + return groups.map((group: any) => ({ + serverId: `group_${group.id}`, + userId, + id: group.id, + type: "group" as const, + wechatAccountId: group.wechatAccountId, + wechatId: group.chatroomId || "", + nickname: group.nickname || "", + conRemark: group.conRemark || "", + avatar: group.chatroomAvatar || group.avatar || "", + groupId: group.groupId, // 保留标签ID + lastUpdateTime: new Date().toISOString(), + sortKey: "", + searchKey: "", + })); +}; + +/** + * 从服务器同步联系人数据 + */ +export const syncContactsFromServer = async (userId: number) => { + try { + // 递归获取好友列表 + const friends = await getAllFriends(); + + // 递归获取群组列表 + const groups = await getAllGroups(); + + // 转换为统一的 Contact 格式 + const friendContacts = convertFriendsToContacts(friends, userId); + const groupContacts = convertGroupsToContacts(groups, userId); + const allContacts = [...friendContacts, ...groupContacts]; + + // 同步到数据库 + await ContactManager.syncContacts(userId, allContacts); + + // 重新从数据库读取 + const updatedContacts = await ContactManager.getUserContacts(userId); + return updatedContacts; + } catch (error) { + console.error("同步联系人失败:", error); + throw error; + } +}; + +/** + * 根据客服筛选联系人 + */ +export const filterContactsByCustomer = ( + contacts: Contact[], + customerId: number | undefined, +): Contact[] => { + if (!customerId || customerId === 0) { + return contacts; + } + return contacts.filter(contact => contact.wechatAccountId === customerId); +}; + +/** + * 获取标签列表(分组列表) + */ +export const getCountLables = async (): Promise => { + try { + const result = await getLabelsListByGroup({}); + const labelsRes = result?.list || []; + + return [ + { + id: 0, + groupName: "默认群分组", + groupType: 2, + }, + ...labelsRes, + { + id: 0, + groupName: "未分组", + groupType: 1, + }, + ]; + } catch (error) { + console.error("获取标签列表失败:", error); + return []; + } +}; + +/** + * 根据标签对联系人进行分组 + */ +export const groupContactsByLabels = ( + contacts: Contact[], + labels: ContactGroupByLabel[], +): ContactGroupByLabel[] => { + // 获取所有非默认标签的ID + const realGroupIds = labels + .filter(item => item.id !== 0) + .map(item => item.id); + + const groupedData: ContactGroupByLabel[] = []; + + for (const label of labels) { + let filteredContacts: Contact[] = []; + + if (Number(label.groupType) === 1) { + // 好友分组 + const friends = contacts.filter(c => c.type === "friend"); + + if (label.id === 0) { + // 未分组:不属于任何标签的好友 + filteredContacts = friends.filter( + friend => !friend.groupId || !realGroupIds.includes(friend.groupId), + ); + } else { + // 指定标签的好友 + filteredContacts = friends.filter( + friend => friend.groupId === label.id, + ); + } + } else if (Number(label.groupType) === 2) { + // 群组分组 + const groups = contacts.filter(c => c.type === "group"); + + if (label.id === 0) { + // 默认群分组:不属于任何标签的群 + filteredContacts = groups.filter( + group => !group.groupId || !realGroupIds.includes(group.groupId), + ); + } else { + // 指定标签的群 + filteredContacts = groups.filter(group => group.groupId === label.id); + } + } + + groupedData.push({ + ...label, + contacts: filteredContacts, + }); + } + + return groupedData; +}; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx index 76f5f981..1bfddfe0 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx @@ -1,77 +1,134 @@ import React, { useState, useCallback, useEffect } from "react"; -import { List, Avatar, Collapse, Button } from "antd"; +import { List, Avatar, Skeleton, Collapse } from "antd"; import type { CollapseProps } from "antd"; import styles from "./WechatFriends.module.scss"; +import { Contact } from "@/utils/db"; +import { ContactManager } from "@/utils/dbAction"; +import { ContactGroupByLabel } from "@/pages/pc/ckbox/data"; +import { useContactStore } from "@weChatStore/contacts"; +import { useWeChatStore } from "@weChatStore/weChat"; +import { useCustomerStore } from "@weChatStore/customer"; +import { useUserStore } from "@storeModule/user"; import { - useCkChatStore, - searchContactsAndGroups, -} from "@/store/module/ckchat/ckchat"; -import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; -import { addChatSession } from "@/store/module/ckchat/ckchat"; -import { useWeChatStore } from "@/store/module/weChat/weChat"; + syncContactsFromServer, + filterContactsByCustomer, + getCountLables, + groupContactsByLabels, +} from "./extend"; interface WechatFriendsProps { - selectedContactId?: ContractData | weChatGroup; + selectedContactId?: Contact; } const ContactListSimple: React.FC = ({ selectedContactId, }) => { - const [newContractList, setNewContractList] = useState([]); - const [searchResults, setSearchResults] = useState< - (ContractData | weChatGroup)[] - >([]); - const getNewContractListFn = useCkChatStore( - state => state.getNewContractList, - ); - const kfSelected = useCkChatStore(state => state.kfSelected); - const countLables = useCkChatStore(state => state.countLables); - const searchKeyword = useCkChatStore(state => state.searchKeyword); + // 本地状态 + const [contacts, setContacts] = useState([]); + const [contactGroups, setContactGroups] = useState([]); + const [labels, setLabels] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [activeKey, setActiveKey] = useState([]); - // 使用useEffect来处理异步的getNewContractList调用 + // 使用新的 contacts store + const { searchResults, isSearchMode, setCurrentContact } = useContactStore(); + + // 获取用户和客服信息 + const currentUser = useUserStore(state => state.user); + const currentCustomer = useCustomerStore(state => state.currentCustomer); + const { setCurrentContact: setWeChatCurrentContact } = useWeChatStore(); + + // 从服务器同步数据 + const syncWithServer = useCallback(async (userId: number) => { + setRefreshing(true); + + try { + const updatedContacts = await syncContactsFromServer(userId); + setContacts(updatedContacts); + } catch (error) { + console.error("同步联系人失败:", error); + } finally { + setRefreshing(false); + } + }, []); + + // 获取标签列表 useEffect(() => { - const fetchData = async () => { + const loadLabels = async () => { try { - if (searchKeyword.trim()) { - // 有搜索关键词时,获取搜索结果 - const searchResult = await searchContactsAndGroups(); - setSearchResults(searchResult || []); - setNewContractList([]); - } else { - // 无搜索关键词时,获取分组列表 - const result = await getNewContractListFn(); - setNewContractList(result || []); - setSearchResults([]); - } + const labelList = await getCountLables(); + setLabels(labelList); } catch (error) { - console.error("获取联系人数据失败:", error); - setNewContractList([]); - setSearchResults([]); + console.error("获取标签列表失败:", error); } }; - fetchData(); - }, [getNewContractListFn, kfSelected, countLables, searchKeyword]); + loadLabels(); + }, []); - const [activeKey, setActiveKey] = useState([]); // 默认展开第一个分组 + // 初始化数据加载:先读取本地数据库,再静默同步 + useEffect(() => { + const loadData = async () => { + if (!currentUser?.id) return; - // 分页加载相关状态 - const [visibleContacts, setVisibleContacts] = useState<{ - [key: string]: ContractData[]; - }>({}); - const [loading, setLoading] = useState<{ [key: string]: boolean }>({}); - const [hasMore, setHasMore] = useState<{ [key: string]: boolean }>({}); - const [page, setPage] = useState<{ [key: string]: number }>({}); - const { setCurrentContact } = useWeChatStore(); - const onContactClick = (contact: ContractData | weChatGroup) => { - addChatSession(contact); + setLoading(true); + + try { + // 1. 先从本地数据库加载 + const localContacts = await ContactManager.getUserContacts( + currentUser.id, + ); + + if (localContacts && localContacts.length > 0) { + // 有本地数据,立即显示 + setContacts(localContacts); + setLoading(false); + + // 后台静默同步 + syncWithServer(currentUser.id); + } else { + // 没有本地数据,从服务器获取 + await syncWithServer(currentUser.id); + setLoading(false); + } + } catch (error) { + console.error("加载联系人数据失败:", error); + setLoading(false); + } + }; + + loadData(); + }, [currentUser?.id, syncWithServer]); + + // 根据客服筛选联系人 + const filteredContacts = filterContactsByCustomer( + contacts, + currentCustomer?.id, + ); + + // 根据标签对联系人进行分组 + useEffect(() => { + if (labels.length > 0 && filteredContacts.length > 0) { + const grouped = groupContactsByLabels(filteredContacts, labels); + setContactGroups(grouped); + } else { + setContactGroups([]); + } + }, [filteredContacts, labels]); + + // 联系人点击处理 + const onContactClick = (contact: Contact) => { setCurrentContact(contact); + // 这里需要将 Contact 转换为 weChat 需要的格式 + // 暂时使用 any 类型,后续需要完善转换逻辑 + setWeChatCurrentContact(contact as any); }; // 渲染联系人项 - const renderContactItem = (contact: ContractData | weChatGroup) => { + const renderContactItem = (contact: Contact) => { // 判断是否为群组 - const isGroup = "chatroomId" in contact; - const avatar = contact.avatar || contact.chatroomAvatar; + const isGroup = contact.type === "group"; + const avatar = contact.avatar; const name = contact.conRemark || contact.nickname; return ( @@ -83,7 +140,7 @@ const ContactListSimple: React.FC = ({
{contact.nickname.charAt(0)}} + icon={!avatar && {contact.nickname?.charAt(0) || ""}} className={styles.avatar} />
@@ -95,120 +152,57 @@ const ContactListSimple: React.FC = ({ ); }; - // 初始化分页数据 - useEffect(() => { - if (newContractList && newContractList.length > 0) { - const initialVisibleContacts: { [key: string]: ContractData[] } = {}; - const initialLoading: { [key: string]: boolean } = {}; - const initialHasMore: { [key: string]: boolean } = {}; - const initialPage: { [key: string]: number } = {}; - - newContractList.forEach((group, index) => { - const groupKey = index.toString(); - // 每个分组初始加载20条数据 - const pageSize = 20; - initialVisibleContacts[groupKey] = group.contacts.slice(0, pageSize); - initialLoading[groupKey] = false; - initialHasMore[groupKey] = group.contacts.length > pageSize; - initialPage[groupKey] = 1; - }); - - setVisibleContacts(initialVisibleContacts); - setLoading(initialLoading); - setHasMore(initialHasMore); - setPage(initialPage); - } - }, [newContractList]); - - // 加载更多联系人 - const loadMoreContacts = useCallback( - (groupKey: string) => { - if (loading[groupKey] || !hasMore[groupKey] || !newContractList) return; - - setLoading(prev => ({ ...prev, [groupKey]: true })); - - // 模拟异步加载 - setTimeout(() => { - const groupIndex = parseInt(groupKey); - const group = newContractList[groupIndex]; - if (!group) return; - - const pageSize = 20; - const currentPage = page[groupKey] || 1; - const nextPage = currentPage + 1; - const startIndex = currentPage * pageSize; - const endIndex = nextPage * pageSize; - const newContacts = group.contacts.slice(startIndex, endIndex); - - setVisibleContacts(prev => ({ - ...prev, - [groupKey]: [...(prev[groupKey] || []), ...newContacts], - })); - - setPage(prev => ({ ...prev, [groupKey]: nextPage })); - setHasMore(prev => ({ - ...prev, - [groupKey]: endIndex < group.contacts.length, - })); - - setLoading(prev => ({ ...prev, [groupKey]: false })); - }, 300); - }, - [loading, hasMore, page, newContractList], + // 渲染骨架屏 + const renderSkeleton = () => ( +
+ {Array(10) + .fill(null) + .map((_, index) => ( +
+ +
+ +
+
+ ))} +
); - // 渲染加载更多按钮 - const renderLoadMoreButton = (groupKey: string) => { - if (!hasMore[groupKey]) - return
没有更多了
; - - return ( -
- -
- ); - }; - - // 构建Collapse的items属性 + // 构建 Collapse 的 items const getCollapseItems = (): CollapseProps["items"] => { - if (!newContractList || newContractList.length === 0) return []; + if (!contactGroups || contactGroups.length === 0) return []; - return newContractList.map((group, index) => { + return contactGroups.map((group, index) => { const groupKey = index.toString(); - const isActive = activeKey.includes(groupKey); return { key: groupKey, label: (
{group.groupName} - {group.contacts.length} + + {group.contacts?.length || 0} +
), className: styles.groupPanel, - children: isActive ? ( - <> - - {renderLoadMoreButton(groupKey)} - - ) : null, + children: ( + + ), }; }); }; return (
- {searchKeyword.trim() ? ( + {loading ? ( + // 加载状态:显示骨架屏 + renderSkeleton() + ) : isSearchMode ? ( // 搜索模式:直接显示搜索结果列表 <>
搜索结果
@@ -222,13 +216,21 @@ const ContactListSimple: React.FC = ({ )} ) : ( - // 正常模式:显示分组 - setActiveKey(keys as string[])} - items={getCollapseItems()} - /> + // 正常模式:显示分组列表 + <> + {refreshing && ( +
正在同步联系人...
+ )} + setActiveKey(keys as string[])} + items={getCollapseItems()} + /> + {contactGroups.length === 0 && ( +
暂无联系人
+ )} + )}
); diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx index 37516986..b9b5999f 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx @@ -5,16 +5,16 @@ import WechatFriends from "./WechatFriends"; import MessageList from "./MessageList/index"; import FriendsCircle from "./FriendsCicle"; import styles from "./SidebarMenu.module.scss"; -import { useCkChatStore } from "@/store/module/ckchat/ckchat"; +import { useContactStore } from "@/store/module/weChat/contacts"; +import { useCustomerStore } from "@/store/module/weChat/customer"; interface SidebarMenuProps { loading?: boolean; } const SidebarMenu: React.FC = ({ loading = false }) => { - const searchKeyword = useCkChatStore(state => state.searchKeyword); - const setSearchKeyword = useCkChatStore(state => state.setSearchKeyword); - const clearSearchKeyword = useCkChatStore(state => state.clearSearchKeyword); - const kfSelected = useCkChatStore(state => state.kfSelected); + const { searchKeyword, setSearchKeyword, clearSearchKeyword } = + useContactStore(); + const currentCustomer = useCustomerStore(state => state.currentCustomer); const [activeTab, setActiveTab] = useState("chats"); @@ -27,8 +27,8 @@ const SidebarMenu: React.FC = ({ loading = false }) => { }; useEffect(() => { - setActiveTab("chats"); - }, [kfSelected]); + setActiveTab("contracts"); + }, [currentCustomer]); // 渲染骨架屏 const renderSkeleton = () => ( @@ -104,7 +104,7 @@ const SidebarMenu: React.FC = ({ loading = false }) => { > 联系人 - {kfSelected != 0 && ( + {currentCustomer && currentCustomer.id !== 0 && (
setActiveTab("friendsCicle")} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/main.ts b/Touchkebao/src/pages/pc/ckbox/weChat/main.ts index a74e2d28..551d85d5 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/main.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/main.ts @@ -1,5 +1,4 @@ import { - asyncContractList, asyncChatSessions, asyncWeChatGroup, asyncCountLables, @@ -7,6 +6,7 @@ import { updateIsLoadWeChat, getIsLoadWeChat, } from "@/store/module/ckchat/ckchat"; +import { useContactStore } from "@/store/module/weChat/contacts"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; import { useUserStore } from "@/store/module/user"; import { weChatGroupService, contractService } from "@/utils/db"; @@ -47,9 +47,10 @@ export const chatInitAPIdata = async () => { updateIsLoadWeChat(true); } - //获取联系人列表 - await asyncContractList(contractList); - + //获取联系人列表 - 使用新的 contacts store + // 注意:这里需要将 contractList 和 groupList 转换为统一的 Contact 格式 + // 暂时保留旧的数据库写入逻辑,后续需要统一到 ContactManager + await contractService.createManyWithServerId(contractList); await asyncWeChatGroup(groupList); //获取标签列表 @@ -247,36 +248,6 @@ export const getAllContactList = async () => { return []; } }; - -// 提取不重复的wechatAccountId组 -export const getUniqueWechatAccountIds = ( - contacts: ContractData[], - groupList: weChatGroup[], -) => { - if (!contacts || !Array.isArray(contacts) || contacts.length === 0) { - return []; - } - - // 使用Set来存储不重复的wechatAccountId - const uniqueAccountIdsSet = new Set(); - - // 遍历联系人列表,将每个wechatAccountId添加到Set中 - contacts.forEach(contact => { - if (contact && contact.wechatAccountId) { - uniqueAccountIdsSet.add(contact.wechatAccountId); - } - }); - - // 遍历联系人列表,将每个wechatAccountId添加到Set中 - groupList.forEach(group => { - if (group && group.wechatAccountId) { - uniqueAccountIdsSet.add(group.wechatAccountId); - } - }); - - // 将Set转换为数组并返回 - return Array.from(uniqueAccountIdsSet); -}; // 递归获取所有群列表 export const getAllGroupList = async () => { try { @@ -318,3 +289,33 @@ export const getAllGroupList = async () => { return []; } }; + +// 提取不重复的wechatAccountId组 +export const getUniqueWechatAccountIds = ( + contacts: ContractData[], + groupList: weChatGroup[], +) => { + if (!contacts || !Array.isArray(contacts) || contacts.length === 0) { + return []; + } + + // 使用Set来存储不重复的wechatAccountId + const uniqueAccountIdsSet = new Set(); + + // 遍历联系人列表,将每个wechatAccountId添加到Set中 + contacts.forEach(contact => { + if (contact && contact.wechatAccountId) { + uniqueAccountIdsSet.add(contact.wechatAccountId); + } + }); + + // 遍历联系人列表,将每个wechatAccountId添加到Set中 + groupList.forEach(group => { + if (group && group.wechatAccountId) { + uniqueAccountIdsSet.add(group.wechatAccountId); + } + }); + + // 将Set转换为数组并返回 + return Array.from(uniqueAccountIdsSet); +}; diff --git a/Touchkebao/src/store/module/ckchat/ckchat.data.ts b/Touchkebao/src/store/module/ckchat/ckchat.data.ts index d406fbe7..6eb2d225 100644 --- a/Touchkebao/src/store/module/ckchat/ckchat.data.ts +++ b/Touchkebao/src/store/module/ckchat/ckchat.data.ts @@ -33,29 +33,20 @@ export interface CkUserInfo { export interface CkChatState { userInfo: CkUserInfo | null; isLoggedIn: boolean; - searchKeyword: string; isLoadWeChat: boolean; getIsLoadWeChat: () => boolean; updateIsLoadWeChat: (isLoadWeChat: boolean) => void; - contractList: ContractData[]; chatSessions: any[]; kfUserList: KfUserListData[]; kfSelected: number; getKfSelectedUser: () => KfUserListData | undefined; countLables: ContactGroupByLabel[]; - newContractList: ContactGroupByLabel[]; - getContractList: () => ContractData[]; - getSomeContractList: (kfSelected: number) => ContractData[]; - getNewContractList: () => Promise; - setSearchKeyword: (keyword: string) => void; - clearSearchKeyword: () => void; asyncKfSelected: (data: number) => void; asyncWeChatGroup: (data: weChatGroup[]) => void; asyncCountLables: (data: ContactGroupByLabel[]) => void; getkfUserList: () => KfUserListData[]; asyncKfUserList: (data: KfUserListData[]) => void; getKfUserInfo: (wechatAccountId: number) => KfUserListData | undefined; - asyncContractList: (data: ContractData[]) => void; getChatSessions: () => any[]; asyncChatSessions: (data: any[]) => void; updateChatSession: (session: ContractData | weChatGroup) => void; diff --git a/Touchkebao/src/store/module/ckchat/ckchat.ts b/Touchkebao/src/store/module/ckchat/ckchat.ts index 2bd1c391..558b1c8e 100644 --- a/Touchkebao/src/store/module/ckchat/ckchat.ts +++ b/Touchkebao/src/store/module/ckchat/ckchat.ts @@ -17,13 +17,10 @@ export const useCkChatStore = createPersistStore( set => ({ userInfo: null, isLoggedIn: false, - contractList: [], //联系人列表 chatSessions: [], //聊天会话 kfUserList: [], //客服列表 countLables: [], //标签列表 - newContractList: [], //联系人分组 kfSelected: 0, //选中的客服 - searchKeyword: "", //搜索关键词 isLoadWeChat: false, //是否加载微信 getIsLoadWeChat: () => { return useCkChatStore.getState().isLoadWeChat; @@ -44,27 +41,10 @@ export const useCkChatStore = createPersistStore( // 异步设置标签列表 asyncCountLables: async (data: ContactGroupByLabel[]) => { set({ countLables: data }); - // 清除getNewContractList缓存 - const state = useCkChatStore.getState(); - if ( - state.getNewContractList && - typeof state.getNewContractList === "function" - ) { - // 触发缓存重新计算 - await state.getNewContractList(); - } - }, - // 设置搜索关键词 - setSearchKeyword: (keyword: string) => { - set({ searchKeyword: keyword }); - }, - // 清除搜索关键词 - clearSearchKeyword: () => { - set({ searchKeyword: "" }); }, asyncKfSelected: async (data: number) => { set({ kfSelected: data }); - // 清除getChatSessions、getContractList和getNewContractList缓存 + // 清除getChatSessions缓存 const state = useCkChatStore.getState(); if ( state.getChatSessions && @@ -73,151 +53,8 @@ export const useCkChatStore = createPersistStore( // 触发缓存重新计算 state.getChatSessions(); } - if ( - state.getContractList && - typeof state.getContractList === "function" - ) { - // 触发缓存重新计算 - state.getContractList(); - } - if ( - state.getNewContractList && - typeof state.getNewContractList === "function" - ) { - // 触发缓存重新计算 - await state.getNewContractList(); - } }, - // 获取联系人分组列表 - 使用缓存避免无限循环 - getNewContractList: (() => { - let cachedResult: any = null; - let lastKfSelected: number | null = null; - let lastCountLablesLength: number = 0; - let lastSearchKeyword: string = ""; - - return async () => { - const state = useCkChatStore.getState(); - - // 检查是否需要重新计算缓存 - const shouldRecalculate = - cachedResult === null || - lastKfSelected !== state.kfSelected || - lastCountLablesLength !== (state.countLables?.length || 0) || - lastSearchKeyword !== state.searchKeyword; - - if (shouldRecalculate) { - // 使用createContractList构建联系人分组数据 - let contractList = await createContractList( - state.kfSelected, - state.countLables, - ); - - // 根据搜索关键词筛选联系人分组 - if (state.searchKeyword.trim()) { - const keyword = state.searchKeyword.toLowerCase(); - contractList = contractList - .map(group => ({ - ...group, - contracts: - group.contracts?.filter(item => { - const nickname = (item.nickname || "").toLowerCase(); - const conRemark = (item.conRemark || "").toLowerCase(); - return ( - nickname.includes(keyword) || conRemark.includes(keyword) - ); - }) || [], - })) - .filter(group => group.contracts.length > 0); - } - - cachedResult = contractList; - lastKfSelected = state.kfSelected; - lastCountLablesLength = state.countLables?.length || 0; - lastSearchKeyword = state.searchKeyword; - } - - return cachedResult; - }; - })(), - // 搜索好友和群组的新方法 - 从本地数据库查询并返回扁平化的搜索结果 - searchContactsAndGroups: (() => { - let cachedResult: (ContractData | weChatGroup)[] = []; - let lastKfSelected: number | null = null; - let lastSearchKeyword: string = ""; - - return async () => { - const state = useCkChatStore.getState(); - - // 检查是否需要重新计算缓存 - const shouldRecalculate = - lastKfSelected !== state.kfSelected || - lastSearchKeyword !== state.searchKeyword; - - if (shouldRecalculate) { - if (state.searchKeyword.trim()) { - const keyword = state.searchKeyword.toLowerCase(); - - // 从本地数据库查询联系人数据 - let allContacts: any[] = await contractService.findAll(); - - // 从本地数据库查询群组数据 - let allGroups: any[] = await weChatGroupService.findAll(); - - // 根据选中的客服筛选联系人 - if (state.kfSelected !== 0) { - allContacts = allContacts.filter( - item => item.wechatAccountId === state.kfSelected, - ); - } - - // 根据选中的客服筛选群组 - if (state.kfSelected !== 0) { - allGroups = allGroups.filter( - item => item.wechatAccountId === state.kfSelected, - ); - } - - // 搜索匹配的联系人 - const matchedContacts = allContacts.filter(item => { - const nickname = (item.nickname || "").toLowerCase(); - const conRemark = (item.conRemark || "").toLowerCase(); - return nickname.includes(keyword) || conRemark.includes(keyword); - }); - - // 搜索匹配的群组 - const matchedGroups = allGroups.filter(item => { - const nickname = (item.nickname || "").toLowerCase(); - const conRemark = (item.conRemark || "").toLowerCase(); - return nickname.includes(keyword) || conRemark.includes(keyword); - }); - - // 合并搜索结果 - cachedResult = [...matchedContacts, ...matchedGroups]; - } else { - cachedResult = []; - } - - lastKfSelected = state.kfSelected; - lastSearchKeyword = state.searchKeyword; - } - - return cachedResult; - }; - })(), - // 异步设置联系人分组列表 - asyncNewContractList: async (data: any[]) => { - set({ newContractList: data }); - // 清除getNewContractList缓存 - const state = useCkChatStore.getState(); - if ( - state.getNewContractList && - typeof state.getNewContractList === "function" - ) { - // 触发缓存重新计算 - await state.getNewContractList(); - } - }, // 异步设置会话列表 asyncChatSessions: data => { set({ chatSessions: data }); @@ -231,73 +68,6 @@ export const useCkChatStore = createPersistStore( state.getChatSessions(); } }, - // 异步设置联系人列表 - asyncContractList: async (data: ContractData[]) => { - set({ contractList: data }); - await contractService.createManyWithServerId(data); - // 清除getContractList缓存 - const state = useCkChatStore.getState(); - if ( - state.getContractList && - typeof state.getContractList === "function" - ) { - // 触发缓存重新计算 - state.getContractList(); - } - }, - //获取特定联系人 - getSomeContractList: (kfSelected: number) => { - const state = useCkChatStore.getState(); - return state.contractList.filter( - item => item.wechatAccountId === kfSelected, - ); - }, - // 获取联系人列表 - 使用缓存避免无限循环 - getContractList: (() => { - let cachedResult: any = null; - let lastKfSelected: number | null = null; - let lastContractListLength: number = 0; - let lastSearchKeyword: string = ""; - - return () => { - const state = useCkChatStore.getState(); - - // 检查是否需要重新计算缓存 - const shouldRecalculate = - cachedResult === null || - lastKfSelected !== state.kfSelected || - lastContractListLength !== state.contractList.length || - lastSearchKeyword !== state.searchKeyword; - - if (shouldRecalculate) { - let filteredContracts = state.contractList; - - // 根据客服筛选 - if (state.kfSelected !== 0) { - filteredContracts = filteredContracts.filter( - item => item.wechatAccountId === state.kfSelected, - ); - } - - // 根据搜索关键词筛选 - if (state.searchKeyword.trim()) { - const keyword = state.searchKeyword.toLowerCase(); - filteredContracts = filteredContracts.filter(item => { - const nickname = (item.nickname || "").toLowerCase(); - const conRemark = (item.conRemark || "").toLowerCase(); - return nickname.includes(keyword) || conRemark.includes(keyword); - }); - } - - cachedResult = filteredContracts; - lastKfSelected = state.kfSelected; - lastContractListLength = state.contractList.length; - lastSearchKeyword = state.searchKeyword; - } - - return cachedResult; - }; - })(), //异步设置联系人分组 asyncWeChatGroup: async (data: weChatGroup[]) => { await weChatGroupService.createManyWithServerId(data); @@ -341,7 +111,6 @@ export const useCkChatStore = createPersistStore( let cachedResult: any = null; let lastKfSelected: number | null = null; let lastChatSessionsLength: number = 0; - let lastSearchKeyword: string = ""; return () => { const state = useCkChatStore.getState(); @@ -350,8 +119,7 @@ export const useCkChatStore = createPersistStore( const shouldRecalculate = cachedResult === null || lastKfSelected !== state.kfSelected || - lastChatSessionsLength !== state.chatSessions.length || - lastSearchKeyword !== state.searchKeyword; + lastChatSessionsLength !== state.chatSessions.length; if (shouldRecalculate) { let filteredSessions = state.chatSessions; @@ -363,20 +131,9 @@ export const useCkChatStore = createPersistStore( ); } - // 根据搜索关键词筛选 - if (state.searchKeyword.trim()) { - const keyword = state.searchKeyword.toLowerCase(); - filteredSessions = filteredSessions.filter(item => { - const nickname = (item.nickname || "").toLowerCase(); - const conRemark = (item.conRemark || "").toLowerCase(); - return nickname.includes(keyword) || conRemark.includes(keyword); - }); - } - cachedResult = filteredSessions; lastKfSelected = state.kfSelected; lastChatSessionsLength = state.chatSessions.length; - lastSearchKeyword = state.searchKeyword; } return cachedResult; @@ -621,21 +378,9 @@ export const getKfSelectedUser = () => useCkChatStore.getState().getKfSelectedUser(); export const getKfUserInfo = (wechatAccountId: number) => useCkChatStore.getState().getKfUserInfo(wechatAccountId); -export const getContractList = () => - useCkChatStore.getState().getContractList(); -export const getNewContractList = () => - useCkChatStore.getState().getNewContractList(); export const asyncCountLables = (data: ContactGroupByLabel[]) => useCkChatStore.getState().asyncCountLables(data); -export const asyncNewContractList = (data: any[]) => - useCkChatStore.getState().asyncNewContractList(data); export const getCountLables = () => useCkChatStore.getState().countLables; -export const setSearchKeyword = (keyword: string) => - useCkChatStore.getState().setSearchKeyword(keyword); -export const clearSearchKeyword = () => - useCkChatStore.getState().clearSearchKeyword(); -export const searchContactsAndGroups = () => - useCkChatStore.getState().searchContactsAndGroups(); export const pinChatSessionToTop = (sessionId: number) => useCkChatStore.getState().pinChatSessionToTop(sessionId); useCkChatStore.getState().getKfSelectedUser(); diff --git a/Touchkebao/src/store/module/weChat/contacts.data.ts b/Touchkebao/src/store/module/weChat/contacts.data.ts new file mode 100644 index 00000000..0ef039e1 --- /dev/null +++ b/Touchkebao/src/store/module/weChat/contacts.data.ts @@ -0,0 +1,70 @@ +//联系人标签分组 +export interface ContactGroupByLabel { + id: number; + accountId?: number; + groupName: string; + tenantId?: number; + count: number; + [key: string]: any; +} +//群聊数据接口 +export interface weChatGroup { + id?: number; + wechatAccountId: number; + tenantId: number; + accountId: number; + chatroomId: string; + chatroomOwner: string; + conRemark: string; + nickname: string; + chatroomAvatar: string; + groupId: number; + config?: { + top?: false; + chat?: boolean; + unreadCount?: number; + }; + labels?: string[]; + notice: string; + selfDisplyName: string; + wechatChatroomId: number; + serverId?: number; + [key: string]: any; +} + +// 联系人数据接口 +export interface ContractData { + id?: number; + serverId?: number; + wechatAccountId: number; + wechatId: string; + alias: string; + conRemark: string; + nickname: string; + quanPin: string; + avatar?: string; + gender: number; + region: string; + addFrom: number; + phone: string; + labels: string[]; + signature: string; + accountId: number; + extendFields: null; + city?: string; + lastUpdateTime: string; + isPassed: boolean; + tenantId: number; + groupId: number; + thirdParty: null; + additionalPicture: string; + desc: string; + config?: { + chat?: boolean; + unreadCount: number; + }; + lastMessageTime: number; + + duplicate: boolean; + [key: string]: any; +} diff --git a/Touchkebao/src/store/module/weChat/contacts.ts b/Touchkebao/src/store/module/weChat/contacts.ts index e69de29b..ca875b97 100644 --- a/Touchkebao/src/store/module/weChat/contacts.ts +++ b/Touchkebao/src/store/module/weChat/contacts.ts @@ -0,0 +1,539 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { ContactGroupByLabel } from "@/pages/pc/ckbox/data"; +import { Contact } from "@/utils/db"; +import { ContactManager } from "@/utils/dbAction"; + +/** + * 联系人状态管理接口 + */ +export interface ContactState { + // ==================== 基础状态 ==================== + /** 联系人列表 */ + contactList: Contact[]; + /** 联系人分组列表 */ + contactGroups: ContactGroupByLabel[]; + /** 当前选中的联系人 */ + currentContact: Contact | null; + /** 搜索关键词 */ + searchKeyword: string; + /** 加载状态 */ + loading: boolean; + /** 刷新状态 */ + refreshing: boolean; + + // ==================== 搜索和筛选 ==================== + /** 搜索结果 */ + searchResults: Contact[]; + /** 是否在搜索模式 */ + isSearchMode: boolean; + + // ==================== 分页和性能 ==================== + /** 可见联系人(用于分页) */ + visibleContacts: { [key: string]: Contact[] }; + /** 加载状态(按分组) */ + loadingStates: { [key: string]: boolean }; + /** 是否有更多数据(按分组) */ + hasMore: { [key: string]: boolean }; + /** 当前页码(按分组) */ + currentPage: { [key: string]: number }; + + // ==================== 转发相关 ==================== + /** 选中的转发联系人 */ + selectedTransmitContacts: Contact[]; + /** 转发弹窗状态 */ + openTransmitModal: boolean; + + // ==================== 基础操作方法 ==================== + /** 设置联系人列表 */ + setContactList: (contacts: Contact[]) => void; + /** 设置联系人分组 */ + setContactGroups: (groups: ContactGroupByLabel[]) => void; + /** 设置当前联系人 */ + setCurrentContact: (contact: Contact | null) => void; + /** 清空当前联系人 */ + clearCurrentContact: () => void; + /** 设置搜索关键词 */ + setSearchKeyword: (keyword: string) => void; + /** 清空搜索关键词 */ + clearSearchKeyword: () => void; + /** 设置加载状态 */ + setLoading: (loading: boolean) => void; + /** 设置刷新状态 */ + setRefreshing: (refreshing: boolean) => void; + + // ==================== 搜索和筛选方法 ==================== + /** 搜索联系人 */ + searchContacts: (keyword: string) => Promise; + /** 根据客服筛选联系人 */ + filterByCustomer: (customerId: number) => Promise; + /** 清空筛选 */ + clearFilter: () => void; + + // ==================== 数据同步方法 ==================== + /** 从服务器同步联系人数据 */ + syncFromServer: (userId: number) => Promise; + /** 从本地数据库加载联系人数据 */ + loadFromLocal: (userId: number) => Promise; + /** 刷新联系人数据 */ + refreshContacts: (userId: number) => Promise; + + // ==================== 联系人操作方法 ==================== + /** 添加联系人 */ + addContact: (contact: Contact) => Promise; + /** 更新联系人 */ + updateContact: (contact: Contact) => Promise; + /** 删除联系人 */ + deleteContact: (contactId: number) => Promise; + /** 批量添加联系人 */ + addContacts: (contacts: Contact[]) => Promise; + + // ==================== 分组管理方法 ==================== + /** 获取联系人分组列表 */ + getContactGroups: ( + userId: number, + customerId?: number, + ) => Promise; + /** 创建联系人分组 */ + createContactGroup: (group: Omit) => Promise; + /** 更新联系人分组 */ + updateContactGroup: (group: ContactGroupByLabel) => Promise; + /** 删除联系人分组 */ + deleteContactGroup: (groupId: number) => Promise; + + // ==================== 分页方法 ==================== + /** 加载更多联系人 */ + loadMoreContacts: (groupId: string) => Promise; + /** 重置分页状态 */ + resetPagination: () => void; + + // ==================== 转发相关方法 ==================== + /** 设置选中的转发联系人 */ + setSelectedTransmitContacts: (contacts: Contact[]) => void; + /** 添加转发联系人 */ + addTransmitContact: (contact: Contact) => void; + /** 移除转发联系人 */ + removeTransmitContact: (contactId: number) => void; + /** 清空转发联系人 */ + clearTransmitContacts: () => void; + /** 设置转发弹窗状态 */ + setTransmitModal: (open: boolean) => void; + + // ==================== 便捷获取方法 ==================== + /** 根据ID获取联系人 */ + getContactById: (id: number) => Contact | undefined; + /** 根据客服ID获取联系人 */ + getContactsByCustomer: (customerId: number) => Contact[]; + /** 获取联系人总数 */ + getContactCount: () => number; + /** 获取搜索结果总数 */ + getSearchResultCount: () => number; +} + +/** + * 联系人状态管理 Store + */ +export const useContactStore = create()( + persist( + (set, get) => ({ + // ==================== 初始状态 ==================== + contactList: [], + contactGroups: [], + currentContact: null, + searchKeyword: "", + loading: false, + refreshing: false, + searchResults: [], + isSearchMode: false, + visibleContacts: {}, + loadingStates: {}, + hasMore: {}, + currentPage: {}, + selectedTransmitContacts: [], + openTransmitModal: false, + + // ==================== 基础操作方法 ==================== + setContactList: (contacts: Contact[]) => { + set({ contactList: contacts }); + }, + + setContactGroups: (groups: ContactGroupByLabel[]) => { + set({ contactGroups: groups }); + }, + + setCurrentContact: (contact: Contact | null) => { + set({ currentContact: contact }); + }, + + clearCurrentContact: () => { + set({ currentContact: null }); + }, + + setSearchKeyword: (keyword: string) => { + set({ searchKeyword: keyword }); + if (keyword.trim()) { + get().searchContacts(keyword); + } else { + set({ isSearchMode: false, searchResults: [] }); + } + }, + + clearSearchKeyword: () => { + set({ + searchKeyword: "", + isSearchMode: false, + searchResults: [], + }); + }, + + setLoading: (loading: boolean) => { + set({ loading }); + }, + + setRefreshing: (refreshing: boolean) => { + set({ refreshing }); + }, + + // ==================== 搜索和筛选方法 ==================== + searchContacts: async (keyword: string) => { + if (!keyword.trim()) { + set({ isSearchMode: false, searchResults: [] }); + return; + } + + set({ loading: true, isSearchMode: true }); + + try { + const results = await ContactManager.searchContacts( + get().currentContact?.userId || 0, + keyword, + ); + set({ searchResults: results }); + } catch (error) { + console.error("搜索联系人失败:", error); + set({ searchResults: [] }); + } finally { + set({ loading: false }); + } + }, + + filterByCustomer: async (customerId: number) => { + set({ loading: true }); + + try { + const contacts = await ContactManager.getContactsByCustomer( + get().currentContact?.userId || 0, + customerId, + ); + set({ contactList: contacts }); + } catch (error) { + console.error("按客服筛选联系人失败:", error); + } finally { + set({ loading: false }); + } + }, + + clearFilter: () => { + set({ + contactList: [], + searchResults: [], + isSearchMode: false, + searchKeyword: "", + }); + }, + + // ==================== 数据同步方法 ==================== + syncFromServer: async (userId: number) => { + set({ refreshing: true }); + + try { + // 这里应该调用实际的API + // const serverContacts = await getContactListFromServer(userId); + // await ContactManager.syncContacts(userId, serverContacts); + + // 临时使用本地数据 + const localContacts = await ContactManager.getUserContacts(userId); + set({ contactList: localContacts }); + } catch (error) { + console.error("从服务器同步联系人失败:", error); + } finally { + set({ refreshing: false }); + } + }, + + loadFromLocal: async (userId: number) => { + set({ loading: true }); + + try { + const contacts = await ContactManager.getUserContacts(userId); + set({ contactList: contacts }); + } catch (error) { + console.error("从本地加载联系人失败:", error); + } finally { + set({ loading: false }); + } + }, + + refreshContacts: async (userId: number) => { + await get().loadFromLocal(userId); + await get().syncFromServer(userId); + }, + + // ==================== 联系人操作方法 ==================== + addContact: async (contact: Contact) => { + try { + await ContactManager.addContact(contact); + set(state => ({ + contactList: [...state.contactList, contact], + })); + } catch (error) { + console.error("添加联系人失败:", error); + throw error; + } + }, + + updateContact: async (contact: Contact) => { + try { + await ContactManager.updateContact(contact); + set(state => ({ + contactList: state.contactList.map(c => + c.id === contact.id ? contact : c, + ), + })); + } catch (error) { + console.error("更新联系人失败:", error); + throw error; + } + }, + + deleteContact: async (contactId: number) => { + try { + await ContactManager.deleteContact(contactId); + set(state => ({ + contactList: state.contactList.filter(c => c.id !== contactId), + })); + } catch (error) { + console.error("删除联系人失败:", error); + throw error; + } + }, + + addContacts: async (contacts: Contact[]) => { + try { + await ContactManager.addContacts(contacts); + set(state => ({ + contactList: [...state.contactList, ...contacts], + })); + } catch (error) { + console.error("批量添加联系人失败:", error); + throw error; + } + }, + + // ==================== 分组管理方法 ==================== + getContactGroups: async (userId: number, customerId?: number) => { + try { + const groups = await ContactManager.getContactGroups( + userId, + customerId, + ); + set({ contactGroups: groups }); + return groups; + } catch (error) { + console.error("获取联系人分组失败:", error); + return []; + } + }, + + createContactGroup: async (group: Omit) => { + try { + await ContactManager.createContactGroup(group); + // 重新获取分组列表 + await get().getContactGroups(get().currentContact?.userId || 0); + } catch (error) { + console.error("创建联系人分组失败:", error); + throw error; + } + }, + + updateContactGroup: async (group: ContactGroupByLabel) => { + try { + await ContactManager.updateContactGroup(group); + set(state => ({ + contactGroups: state.contactGroups.map(g => + g.id === group.id ? group : g, + ), + })); + } catch (error) { + console.error("更新联系人分组失败:", error); + throw error; + } + }, + + deleteContactGroup: async (groupId: number) => { + try { + await ContactManager.deleteContactGroup(groupId); + set(state => ({ + contactGroups: state.contactGroups.filter(g => g.id !== groupId), + })); + } catch (error) { + console.error("删除联系人分组失败:", error); + throw error; + } + }, + + // ==================== 分页方法 ==================== + loadMoreContacts: async (groupId: string) => { + const state = get(); + if (state.loadingStates[groupId] || !state.hasMore[groupId]) { + return; + } + + set(state => ({ + loadingStates: { ...state.loadingStates, [groupId]: true }, + })); + + try { + const currentPage = state.currentPage[groupId] || 1; + const nextPage = currentPage + 1; + const pageSize = 20; + + // 这里应该调用实际的分页API + // const newContacts = await getContactsByGroup(groupId, nextPage, pageSize); + + // 临时使用本地数据模拟分页 + const allContacts = state.contactList; + const startIndex = currentPage * pageSize; + const endIndex = nextPage * pageSize; + const newContacts = allContacts.slice(startIndex, endIndex); + + set(state => ({ + visibleContacts: { + ...state.visibleContacts, + [groupId]: [ + ...(state.visibleContacts[groupId] || []), + ...newContacts, + ], + }, + currentPage: { ...state.currentPage, [groupId]: nextPage }, + hasMore: { + ...state.hasMore, + [groupId]: endIndex < allContacts.length, + }, + loadingStates: { ...state.loadingStates, [groupId]: false }, + })); + } catch (error) { + console.error("加载更多联系人失败:", error); + set(state => ({ + loadingStates: { ...state.loadingStates, [groupId]: false }, + })); + } + }, + + resetPagination: () => { + set({ + visibleContacts: {}, + loadingStates: {}, + hasMore: {}, + currentPage: {}, + }); + }, + + // ==================== 转发相关方法 ==================== + setSelectedTransmitContacts: (contacts: Contact[]) => { + set({ selectedTransmitContacts: contacts }); + }, + + addTransmitContact: (contact: Contact) => { + set(state => { + const exists = state.selectedTransmitContacts.some( + c => c.id === contact.id, + ); + if (exists) return state; + return { + selectedTransmitContacts: [ + ...state.selectedTransmitContacts, + contact, + ], + }; + }); + }, + + removeTransmitContact: (contactId: number) => { + set(state => ({ + selectedTransmitContacts: state.selectedTransmitContacts.filter( + c => c.id !== contactId, + ), + })); + }, + + clearTransmitContacts: () => { + set({ selectedTransmitContacts: [] }); + }, + + setTransmitModal: (open: boolean) => { + set({ openTransmitModal: open }); + }, + + // ==================== 便捷获取方法 ==================== + getContactById: (id: number) => { + const state = get(); + return state.contactList.find(contact => contact.id === id); + }, + + getContactsByCustomer: (customerId: number) => { + const state = get(); + return state.contactList.filter( + contact => contact.wechatAccountId === customerId, + ); + }, + + getContactCount: () => { + return get().contactList.length; + }, + + getSearchResultCount: () => { + return get().searchResults.length; + }, + }), + { + name: "contacts-storage", + partialize: () => ({ + // 只持久化必要的状态,不持久化临时状态 + contactList: [], + contactGroups: [], + currentContact: null, + searchKeyword: "", + selectedTransmitContacts: [], + openTransmitModal: false, + }), + }, + ), +); + +// ==================== 便捷选择器导出 ==================== +/** 获取联系人列表的 Hook */ +export const useContactList = () => useContactStore(state => state.contactList); +/** 获取当前联系人的 Hook */ +export const useCurrentContact = () => + useContactStore(state => state.currentContact); +/** 获取搜索关键词的 Hook */ +export const useSearchKeyword = () => + useContactStore(state => state.searchKeyword); +/** 获取加载状态的 Hook */ +export const useContactLoading = () => useContactStore(state => state.loading); +/** 获取刷新状态的 Hook */ +export const useContactRefreshing = () => + useContactStore(state => state.refreshing); +/** 获取搜索结果的 Hook */ +export const useSearchResults = () => + useContactStore(state => state.searchResults); +/** 获取是否在搜索模式的 Hook */ +export const useIsSearchMode = () => + useContactStore(state => state.isSearchMode); +/** 获取转发联系人的 Hook */ +export const useSelectedTransmitContacts = () => + useContactStore(state => state.selectedTransmitContacts); +/** 获取转发弹窗状态的 Hook */ +export const useTransmitModal = () => + useContactStore(state => state.openTransmitModal); diff --git a/Touchkebao/src/store/module/weChat/weChat.data.ts b/Touchkebao/src/store/module/weChat/weChat.data.ts index b3f1bf2c..3a7bb10e 100644 --- a/Touchkebao/src/store/module/weChat/weChat.data.ts +++ b/Touchkebao/src/store/module/weChat/weChat.data.ts @@ -24,17 +24,6 @@ export interface WeChatState { /** 更新选中的聊天记录 */ updateSelectedChatRecords: (message: ChatRecord[]) => void; - /** 选中的联系人或群组列表 */ - selectedTransmitContact: ContractData[] | weChatGroup[]; - /** 更新选中的联系人或群组 */ - updateSelectedTransmitContact: ( - contact: ContractData[] | weChatGroup[], - ) => void; - - /** 转发弹窗开启状态 */ - openTransmitModal: boolean; - /** 更新转发弹窗状态 */ - updateTransmitModal: (open: boolean) => void; // ==================== Transmit Module =========END=========== // ==================== 当前联系人管理 ==================== diff --git a/Touchkebao/src/store/module/weChat/weChat.ts b/Touchkebao/src/store/module/weChat/weChat.ts index c1746dfc..27a2348e 100644 --- a/Touchkebao/src/store/module/weChat/weChat.ts +++ b/Touchkebao/src/store/module/weChat/weChat.ts @@ -59,21 +59,6 @@ export const useWeChatStore = create()( set({ selectedChatRecords: message }); }, - /** 选中的联系人或群组列表 */ - selectedTransmitContact: [], - /** 更新选中的联系人或群组 */ - updateSelectedTransmitContact: ( - contact: ContractData[] | weChatGroup[], - ) => { - set({ selectedTransmitContact: contact }); - }, - - /** 转发弹窗开启状态 */ - openTransmitModal: false, - /** 更新转发弹窗状态 */ - updateTransmitModal: (open: boolean) => { - set({ openTransmitModal: open }); - }, // ==================== Transmit Module =========END=========== // ==================== 当前联系人管理状态 ==================== @@ -159,7 +144,7 @@ export const useWeChatStore = create()( ) => { const state = useWeChatStore.getState(); // 切换联系人时清空当前消息,等待重新加载 - set({ currentMessages: [], openTransmitModal: false }); + set({ currentMessages: [] }); const params: any = {}; @@ -495,7 +480,7 @@ export const useWeChatStore = create()( }), { name: "wechat-storage", - partialize: state => ({ + partialize: () => ({ // currentContract 不做持久化,登录和页面刷新时直接清空 }), }, diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts index 9a8559e4..407acf9b 100644 --- a/Touchkebao/src/utils/db.ts +++ b/Touchkebao/src/utils/db.ts @@ -87,6 +87,7 @@ export interface Contact { wechatId?: string; // 微信号 alias?: string; // 别名 gender?: number; // 性别 + groupId?: number; // 标签ID(分组) region?: string; // 地区 signature?: string; // 个性签名 phone?: string; // 手机号 diff --git a/Touchkebao/src/utils/dbAction/contact.ts b/Touchkebao/src/utils/dbAction/contact.ts index e69de29b..d015da45 100644 --- a/Touchkebao/src/utils/dbAction/contact.ts +++ b/Touchkebao/src/utils/dbAction/contact.ts @@ -0,0 +1,317 @@ +import { Contact, contactUnifiedService } from "@/utils/db"; +import { ContactGroupByLabel } from "@/pages/pc/ckbox/data"; + +/** + * 联系人数据管理器 + * 负责联系人相关的数据库操作和业务逻辑 + */ +export class ContactManager { + /** + * 获取用户的所有联系人 + */ + static async getUserContacts(userId: number): Promise { + try { + const contacts = await contactUnifiedService.findWhere("userId", userId); + return contacts; + } catch (error) { + console.error("获取用户联系人失败:", error); + return []; + } + } + + /** + * 根据客服ID获取联系人 + */ + static async getContactsByCustomer( + userId: number, + customerId: number, + ): Promise { + try { + const contacts = await contactUnifiedService.findWhereMultiple([ + { field: "userId", operator: "equals", value: userId }, + { field: "wechatAccountId", operator: "equals", value: customerId }, + ]); + return contacts; + } catch (error) { + console.error("根据客服获取联系人失败:", error); + return []; + } + } + + /** + * 搜索联系人 + */ + static async searchContacts( + userId: number, + keyword: string, + ): Promise { + try { + const contacts = await contactUnifiedService.findWhere("userId", userId); + const lowerKeyword = keyword.toLowerCase(); + + return contacts.filter(contact => { + const nickname = (contact.nickname || "").toLowerCase(); + const conRemark = (contact.conRemark || "").toLowerCase(); + return ( + nickname.includes(lowerKeyword) || conRemark.includes(lowerKeyword) + ); + }); + } catch (error) { + console.error("搜索联系人失败:", error); + return []; + } + } + + /** + * 添加联系人 + */ + static async addContact(contact: Contact): Promise { + try { + await contactUnifiedService.create(contact); + } catch (error) { + console.error("添加联系人失败:", error); + throw error; + } + } + + /** + * 批量添加联系人 + */ + static async addContacts(contacts: Contact[]): Promise { + try { + await contactUnifiedService.createMany(contacts); + } catch (error) { + console.error("批量添加联系人失败:", error); + throw error; + } + } + + /** + * 更新联系人 + */ + static async updateContact(contact: Contact): Promise { + try { + await contactUnifiedService.update(contact.serverId, contact); + } catch (error) { + console.error("更新联系人失败:", error); + throw error; + } + } + + /** + * 删除联系人 + */ + static async deleteContact(contactId: number): Promise { + try { + // 这里需要根据实际的ID字段来删除 + // 假设contactId对应的是id字段 + const contacts = await contactUnifiedService.findWhere("id", contactId); + if (contacts.length > 0) { + await contactUnifiedService.delete(contacts[0].serverId); + } + } catch (error) { + console.error("删除联系人失败:", error); + throw error; + } + } + + /** + * 同步联系人数据 + */ + static async syncContacts( + userId: number, + serverContacts: any[], + ): Promise { + try { + // 获取本地联系人 + const localContacts = await this.getUserContacts(userId); + const localContactMap = new Map(localContacts.map(c => [c.serverId, c])); + + // 处理服务器联系人 + const contactsToAdd: Contact[] = []; + const contactsToUpdate: Contact[] = []; + + for (const serverContact of serverContacts) { + const localContact = localContactMap.get(serverContact.serverId); + + if (!localContact) { + // 新增联系人 + contactsToAdd.push({ + ...serverContact, + userId, + serverId: serverContact.serverId, + lastUpdateTime: new Date().toISOString(), + }); + } else { + // 检查是否需要更新 + if (this.isContactChanged(localContact, serverContact)) { + contactsToUpdate.push({ + ...serverContact, + userId, + serverId: serverContact.serverId, + lastUpdateTime: new Date().toISOString(), + }); + } + } + } + + // 执行数据库操作 + if (contactsToAdd.length > 0) { + await this.addContacts(contactsToAdd); + } + + if (contactsToUpdate.length > 0) { + for (const contact of contactsToUpdate) { + await this.updateContact(contact); + } + } + + console.log( + `同步联系人完成: 新增${contactsToAdd.length}个, 更新${contactsToUpdate.length}个`, + ); + } catch (error) { + console.error("同步联系人失败:", error); + throw error; + } + } + + /** + * 检查联系人是否发生变化 + */ + private static isContactChanged(local: Contact, server: any): boolean { + return ( + local.nickname !== server.nickname || + local.conRemark !== server.conRemark || + local.avatar !== server.avatar || + local.wechatAccountId !== server.wechatAccountId + ); + } + + /** + * 获取联系人分组列表 + */ + static async getContactGroups( + userId: number, + customerId?: number, + ): Promise { + try { + // 这里应该根据实际的标签系统来实现 + // 暂时返回空数组,实际实现需要根据标签表来查询 + return []; + } catch (error) { + console.error("获取联系人分组失败:", error); + return []; + } + } + + /** + * 创建联系人分组 + */ + static async createContactGroup( + group: Omit, + ): Promise { + try { + // 这里应该调用标签相关的API + console.log("创建联系人分组:", group); + } catch (error) { + console.error("创建联系人分组失败:", error); + throw error; + } + } + + /** + * 更新联系人分组 + */ + static async updateContactGroup(group: ContactGroupByLabel): Promise { + try { + // 这里应该调用标签相关的API + console.log("更新联系人分组:", group); + } catch (error) { + console.error("更新联系人分组失败:", error); + throw error; + } + } + + /** + * 删除联系人分组 + */ + static async deleteContactGroup(groupId: number): Promise { + try { + // 这里应该调用标签相关的API + console.log("删除联系人分组:", groupId); + } catch (error) { + console.error("删除联系人分组失败:", error); + throw error; + } + } + + /** + * 根据ID获取联系人 + */ + static async getContactById( + userId: number, + contactId: number, + ): Promise { + try { + const contacts = await contactUnifiedService.findWhereMultiple([ + { field: "userId", operator: "equals", value: userId }, + { field: "id", operator: "equals", value: contactId }, + ]); + return contacts.length > 0 ? contacts[0] : null; + } catch (error) { + console.error("根据ID获取联系人失败:", error); + return null; + } + } + + /** + * 根据类型获取联系人 + */ + static async getContactsByType( + userId: number, + type: "friend" | "group", + ): Promise { + try { + const contacts = await contactUnifiedService.findWhereMultiple([ + { field: "userId", operator: "equals", value: userId }, + { field: "type", operator: "equals", value: type }, + ]); + return contacts; + } catch (error) { + console.error("根据类型获取联系人失败:", error); + return []; + } + } + + /** + * 获取联系人统计信息 + */ + static async getContactStats(userId: number): Promise<{ + total: number; + friends: number; + groups: number; + byCustomer: { [customerId: number]: number }; + }> { + try { + const contacts = await this.getUserContacts(userId); + + const stats = { + total: contacts.length, + friends: contacts.filter(c => c.type === "friend").length, + groups: contacts.filter(c => c.type === "group").length, + byCustomer: {} as { [customerId: number]: number }, + }; + + // 按客服统计 + contacts.forEach(contact => { + const customerId = contact.wechatAccountId; + stats.byCustomer[customerId] = (stats.byCustomer[customerId] || 0) + 1; + }); + + return stats; + } catch (error) { + console.error("获取联系人统计失败:", error); + return { total: 0, friends: 0, groups: 0, byCustomer: {} }; + } + } +} diff --git a/Touchkebao/src/utils/dbAction/loginManager.ts b/Touchkebao/src/utils/dbAction/loginManager.ts index e69de29b..fd3ed519 100644 --- a/Touchkebao/src/utils/dbAction/loginManager.ts +++ b/Touchkebao/src/utils/dbAction/loginManager.ts @@ -0,0 +1,169 @@ +import { UserLoginRecord, userLoginRecordService } from "@/utils/db"; + +/** + * 登录管理器 + * 负责用户登录记录相关的数据库操作和业务逻辑 + */ +export class LoginManager { + /** + * 记录用户登录 + */ + static async recordLogin(userId: number): Promise { + try { + const serverId = `user_${userId}`; + const existingRecord = + await userLoginRecordService.findByPrimaryKey(serverId); + + if (existingRecord) { + // 更新已存在的记录 + await userLoginRecordService.update(serverId, { + lastLoginTime: new Date().toISOString(), + loginCount: (existingRecord.loginCount || 0) + 1, + lastActiveTime: new Date().toISOString(), + }); + } else { + // 创建新记录 + const newRecord: UserLoginRecord = { + serverId, + userId, + lastLoginTime: new Date().toISOString(), + loginCount: 1, + createTime: new Date().toISOString(), + lastActiveTime: new Date().toISOString(), + }; + await userLoginRecordService.create(newRecord); + } + } catch (error) { + console.error("记录用户登录失败:", error); + throw error; + } + } + + /** + * 更新用户活跃时间 + */ + static async updateActiveTime(userId: number): Promise { + try { + const serverId = `user_${userId}`; + await userLoginRecordService.update(serverId, { + lastActiveTime: new Date().toISOString(), + }); + } catch (error) { + console.error("更新用户活跃时间失败:", error); + } + } + + /** + * 获取用户登录记录 + */ + static async getLoginRecord(userId: number): Promise { + try { + const serverId = `user_${userId}`; + const record = await userLoginRecordService.findByPrimaryKey(serverId); + return record as UserLoginRecord | null; + } catch (error) { + console.error("获取用户登录记录失败:", error); + return null; + } + } + + /** + * 检查用户是否是首次登录 + */ + static async isFirstLogin(userId: number): Promise { + try { + const record = await this.getLoginRecord(userId); + return !record || record.loginCount === 0; + } catch (error) { + console.error("检查首次登录失败:", error); + return true; + } + } + + /** + * 清理过期用户数据(未登录超过指定天数) + */ + static async cleanupInactiveUsers(inactiveDays: number = 7): Promise { + try { + const allRecords = await userLoginRecordService.findAll(); + const currentTime = new Date().getTime(); + const inactiveThreshold = inactiveDays * 24 * 60 * 60 * 1000; + + const inactiveUserIds: number[] = []; + + for (const record of allRecords) { + const lastActiveTime = new Date(record.lastActiveTime).getTime(); + if (currentTime - lastActiveTime > inactiveThreshold) { + inactiveUserIds.push(record.userId); + await userLoginRecordService.delete(record.serverId); + } + } + + console.log(`清理了 ${inactiveUserIds.length} 个不活跃用户的数据`); + } catch (error) { + console.error("清理不活跃用户数据失败:", error); + } + } + + /** + * 获取所有登录记录 + */ + static async getAllLoginRecords(): Promise { + try { + return (await userLoginRecordService.findAll()) as UserLoginRecord[]; + } catch (error) { + console.error("获取所有登录记录失败:", error); + return []; + } + } + + /** + * 删除用户登录记录 + */ + static async deleteLoginRecord(userId: number): Promise { + try { + const serverId = `user_${userId}`; + await userLoginRecordService.delete(serverId); + } catch (error) { + console.error("删除用户登录记录失败:", error); + throw error; + } + } + + /** + * 获取用户统计信息 + */ + static async getUserStats(): Promise<{ + totalUsers: number; + activeUsers: number; + inactiveUsers: number; + }> { + try { + const allRecords = await userLoginRecordService.findAll(); + const currentTime = new Date().getTime(); + const activeDays = 7; + const activeThreshold = activeDays * 24 * 60 * 60 * 1000; + + let activeCount = 0; + for (const record of allRecords) { + const lastActiveTime = new Date(record.lastActiveTime).getTime(); + if (currentTime - lastActiveTime <= activeThreshold) { + activeCount++; + } + } + + return { + totalUsers: allRecords.length, + activeUsers: activeCount, + inactiveUsers: allRecords.length - activeCount, + }; + } catch (error) { + console.error("获取用户统计信息失败:", error); + return { + totalUsers: 0, + activeUsers: 0, + inactiveUsers: 0, + }; + } + } +}