From 1f081cdddc50a22957765ad0762e02fa6bd67d5e 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 20:15:59 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=81=94=E7=B3=BB=E4=BA=BA?= =?UTF-8?q?=E5=88=86=E7=BB=84=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=88=86=E7=BB=84=E7=BB=9F=E8=AE=A1=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=92=8C=E5=88=86=E9=A1=B5=E8=8E=B7=E5=8F=96=E8=81=94?= =?UTF-8?q?=E7=B3=BB=E4=BA=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=81=94=E7=B3=BB=E4=BA=BA=E5=88=97=E8=A1=A8=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E5=92=8C=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WechatFriends/WechatFriends.module.scss | 26 ++ .../SidebarMenu/WechatFriends/extend.ts | 119 +++++++-- .../SidebarMenu/WechatFriends/index.tsx | 235 ++++++++++++++++-- Touchkebao/src/utils/dbAction/contact.ts | 111 +++++++++ 4 files changed, 446 insertions(+), 45 deletions(-) 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 7ea6903a..75a67032 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 @@ -151,4 +151,30 @@ font-size: 12px; text-align: center; } + + // 分组骨架屏 + .groupSkeleton { + padding: 10px; + } + + // 加载更多按钮 + .loadMoreBtn { + background: #fff; + border: 1px solid #d9d9d9; + border-radius: 4px; + padding: 6px 15px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s; + + &:hover:not(:disabled) { + color: #1890ff; + border-color: #1890ff; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + } } diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts index 88cf1a25..f9a3b3c8 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts @@ -199,13 +199,13 @@ export const getCountLables = async (): Promise => { }; /** - * 根据标签对联系人进行分组 + * 获取分组统计信息(只获取数量,不获取详细数据) */ -export const groupContactsByLabels = ( - contacts: Contact[], +export const getGroupStatistics = async ( + userId: number, labels: ContactGroupByLabel[], -): ContactGroupByLabel[] => { - // 获取所有非默认标签的ID + customerId?: number, +): Promise => { const realGroupIds = labels .filter(item => item.id !== 0) .map(item => item.id); @@ -213,43 +213,126 @@ export const groupContactsByLabels = ( const groupedData: ContactGroupByLabel[] = []; for (const label of labels) { - let filteredContacts: Contact[] = []; + let count = 0; 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), + count = await ContactManager.getContactCount( + userId, + "friend", + customerId, + realGroupIds, + true, // 排除已分组的 ); } else { // 指定标签的好友 - filteredContacts = friends.filter( - friend => friend.groupId === label.id, + count = await ContactManager.getContactCount( + userId, + "friend", + customerId, + [label.id], + false, // 包含指定分组 ); } } 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), + count = await ContactManager.getContactCount( + userId, + "group", + customerId, + realGroupIds, + true, // 排除已分组的 ); } else { // 指定标签的群 - filteredContacts = groups.filter(group => group.groupId === label.id); + count = await ContactManager.getContactCount( + userId, + "group", + customerId, + [label.id], + false, // 包含指定分组 + ); } } groupedData.push({ ...label, - contacts: filteredContacts, + contacts: [], // 不加载数据,只返回统计信息 + count, // 添加统计数量 }); } return groupedData; }; + +/** + * 分页获取指定分组的联系人 + */ +export const getContactsByGroup = async ( + userId: number, + label: ContactGroupByLabel, + realGroupIds: number[], + customerId?: number, + page: number = 1, + pageSize: number = 20, +): Promise => { + const offset = (page - 1) * pageSize; + + if (Number(label.groupType) === 1) { + // 好友分组 + if (label.id === 0) { + // 未分组的好友 + return await ContactManager.getContactsByGroupPaginated( + userId, + "friend", + customerId, + realGroupIds, + true, // 排除已分组的 + offset, + pageSize, + ); + } else { + // 指定标签的好友 + return await ContactManager.getContactsByGroupPaginated( + userId, + "friend", + customerId, + [label.id], + false, // 包含指定分组 + offset, + pageSize, + ); + } + } else if (Number(label.groupType) === 2) { + // 群组分组 + if (label.id === 0) { + // 默认群分组 + return await ContactManager.getContactsByGroupPaginated( + userId, + "group", + customerId, + realGroupIds, + true, // 排除已分组的 + offset, + pageSize, + ); + } else { + // 指定标签的群 + return await ContactManager.getContactsByGroupPaginated( + userId, + "group", + customerId, + [label.id], + false, // 包含指定分组 + offset, + pageSize, + ); + } + } + + return []; +}; 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 1bfddfe0..3c0b6b36 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 @@ -11,9 +11,9 @@ import { useCustomerStore } from "@weChatStore/customer"; import { useUserStore } from "@storeModule/user"; import { syncContactsFromServer, - filterContactsByCustomer, getCountLables, - groupContactsByLabels, + getGroupStatistics, + getContactsByGroup, } from "./extend"; interface WechatFriendsProps { @@ -22,7 +22,8 @@ interface WechatFriendsProps { const ContactListSimple: React.FC = ({ selectedContactId, }) => { - // 本地状态 + // 本地状态(用于数据同步,不直接用于显示) + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [contacts, setContacts] = useState([]); const [contactGroups, setContactGroups] = useState([]); const [labels, setLabels] = useState([]); @@ -30,6 +31,20 @@ const ContactListSimple: React.FC = ({ const [refreshing, setRefreshing] = useState(false); const [activeKey, setActiveKey] = useState([]); + // 分页相关状态 + const [groupContacts, setGroupContacts] = useState<{ + [groupKey: string]: Contact[]; + }>({}); + const [groupPages, setGroupPages] = useState<{ [groupKey: string]: number }>( + {}, + ); + const [groupLoading, setGroupLoading] = useState<{ + [groupKey: string]: boolean; + }>({}); + const [groupHasMore, setGroupHasMore] = useState<{ + [groupKey: string]: boolean; + }>({}); + // 使用新的 contacts store const { searchResults, isSearchMode, setCurrentContact } = useContactStore(); @@ -100,21 +115,130 @@ const ContactListSimple: React.FC = ({ loadData(); }, [currentUser?.id, syncWithServer]); - // 根据客服筛选联系人 - const filteredContacts = filterContactsByCustomer( - contacts, - currentCustomer?.id, + // 获取分组统计信息(只获取数量,不加载联系人数据) + useEffect(() => { + const loadGroupStats = async () => { + if (!currentUser?.id || labels.length === 0) return; + + try { + const groupStats = await getGroupStatistics( + currentUser.id, + labels, + currentCustomer?.id, + ); + setContactGroups(groupStats); + } catch (error) { + console.error("获取分组统计失败:", error); + } + }; + + loadGroupStats(); + }, [currentUser?.id, labels, currentCustomer?.id]); + + // 当分组展开时,加载该分组的第一页数据 + const handleGroupExpand = useCallback( + async (groupKey: string) => { + const groupIndex = parseInt(groupKey); + const label = contactGroups[groupIndex]; + + if (!label || !currentUser?.id) return; + + // 如果已经加载过数据,不重复加载 + if (groupContacts[groupKey] && groupContacts[groupKey].length > 0) { + return; + } + + setGroupLoading(prev => ({ ...prev, [groupKey]: true })); + + try { + const realGroupIds = labels + .filter(item => item.id !== 0) + .map(item => item.id); + + const contacts = await getContactsByGroup( + currentUser.id, + label, + realGroupIds, + currentCustomer?.id, + 1, + 20, + ); + + setGroupContacts(prev => ({ ...prev, [groupKey]: contacts })); + setGroupPages(prev => ({ ...prev, [groupKey]: 1 })); + setGroupHasMore(prev => ({ + ...prev, + [groupKey]: contacts.length === 20, + })); + } catch (error) { + console.error("加载分组数据失败:", error); + } finally { + setGroupLoading(prev => ({ ...prev, [groupKey]: false })); + } + }, + [ + contactGroups, + labels, + currentUser?.id, + currentCustomer?.id, + groupContacts, + ], ); - // 根据标签对联系人进行分组 - useEffect(() => { - if (labels.length > 0 && filteredContacts.length > 0) { - const grouped = groupContactsByLabels(filteredContacts, labels); - setContactGroups(grouped); - } else { - setContactGroups([]); - } - }, [filteredContacts, labels]); + // 加载更多联系人 + const handleLoadMore = useCallback( + async (groupKey: string) => { + if (groupLoading[groupKey] || !groupHasMore[groupKey]) return; + + const groupIndex = parseInt(groupKey); + const label = contactGroups[groupIndex]; + + if (!label || !currentUser?.id) return; + + setGroupLoading(prev => ({ ...prev, [groupKey]: true })); + + try { + const currentPage = groupPages[groupKey] || 1; + const nextPage = currentPage + 1; + + const realGroupIds = labels + .filter(item => item.id !== 0) + .map(item => item.id); + + const newContacts = await getContactsByGroup( + currentUser.id, + label, + realGroupIds, + currentCustomer?.id, + nextPage, + 20, + ); + + setGroupContacts(prev => ({ + ...prev, + [groupKey]: [...(prev[groupKey] || []), ...newContacts], + })); + setGroupPages(prev => ({ ...prev, [groupKey]: nextPage })); + setGroupHasMore(prev => ({ + ...prev, + [groupKey]: newContacts.length === 20, + })); + } catch (error) { + console.error("加载更多联系人失败:", error); + } finally { + setGroupLoading(prev => ({ ...prev, [groupKey]: false })); + } + }, + [ + contactGroups, + labels, + currentUser?.id, + currentCustomer?.id, + groupLoading, + groupHasMore, + groupPages, + ], + ); // 联系人点击处理 const onContactClick = (contact: Contact) => { @@ -168,31 +292,88 @@ const ContactListSimple: React.FC = ({ ); + // 监听分组展开/折叠事件 + const handleCollapseChange = (keys: string | string[]) => { + const newKeys = Array.isArray(keys) ? keys : [keys]; + const expandedKeys = newKeys.filter(key => !activeKey.includes(key)); + + // 加载新展开的分组数据 + expandedKeys.forEach(key => { + handleGroupExpand(key); + }); + + setActiveKey(newKeys); + }; + + // 渲染加载更多按钮 + const renderLoadMoreButton = (groupKey: string) => { + if (!groupHasMore[groupKey]) { + return
没有更多了
; + } + + return ( +
+ +
+ ); + }; + // 构建 Collapse 的 items const getCollapseItems = (): CollapseProps["items"] => { if (!contactGroups || contactGroups.length === 0) return []; return contactGroups.map((group, index) => { const groupKey = index.toString(); + const isActive = activeKey.includes(groupKey); return { key: groupKey, label: (
{group.groupName} - - {group.contacts?.length || 0} - + {group.count || 0}
), className: styles.groupPanel, - children: ( - - ), + children: isActive ? ( + <> + {groupLoading[groupKey] && !groupContacts[groupKey] ? ( + // 首次加载显示骨架屏 +
+ {Array(3) + .fill(null) + .map((_, i) => ( +
+ +
+ +
+
+ ))} +
+ ) : ( + <> + + {(groupContacts[groupKey]?.length || 0) > 0 && + renderLoadMoreButton(groupKey)} + + )} + + ) : null, }; }); }; @@ -224,7 +405,7 @@ const ContactListSimple: React.FC = ({ setActiveKey(keys as string[])} + onChange={handleCollapseChange} items={getCollapseItems()} /> {contactGroups.length === 0 && ( diff --git a/Touchkebao/src/utils/dbAction/contact.ts b/Touchkebao/src/utils/dbAction/contact.ts index d015da45..501c6289 100644 --- a/Touchkebao/src/utils/dbAction/contact.ts +++ b/Touchkebao/src/utils/dbAction/contact.ts @@ -314,4 +314,115 @@ export class ContactManager { return { total: 0, friends: 0, groups: 0, byCustomer: {} }; } } + + /** + * 获取指定分组的联系人数量(支持数据库级别的统计) + */ + static async getContactCount( + userId: number, + type: "friend" | "group", + customerId?: number, + groupIds?: number[], + exclude: boolean = false, + ): Promise { + try { + const conditions: any[] = [ + { field: "userId", operator: "equals", value: userId }, + { field: "type", operator: "equals", value: type }, + ]; + + // 客服筛选 + if (customerId && customerId !== 0) { + conditions.push({ + field: "wechatAccountId", + operator: "equals", + value: customerId, + }); + } + + // 分组筛选 + if (groupIds && groupIds.length > 0) { + if (exclude) { + // 排除指定分组(未分组) + conditions.push({ + field: "groupId", + operator: "notIn", + value: groupIds, + }); + } else { + // 包含指定分组 + conditions.push({ + field: "groupId", + operator: "in", + value: groupIds, + }); + } + } + + const contacts = + await contactUnifiedService.findWhereMultiple(conditions); + return contacts.length; + } catch (error) { + console.error("获取联系人数量失败:", error); + return 0; + } + } + + /** + * 分页获取指定分组的联系人 + */ + static async getContactsByGroupPaginated( + userId: number, + type: "friend" | "group", + customerId?: number, + groupIds?: number[], + exclude: boolean = false, + offset: number = 0, + limit: number = 20, + ): Promise { + try { + const conditions: any[] = [ + { field: "userId", operator: "equals", value: userId }, + { field: "type", operator: "equals", value: type }, + ]; + + // 客服筛选 + if (customerId && customerId !== 0) { + conditions.push({ + field: "wechatAccountId", + operator: "equals", + value: customerId, + }); + } + + // 分组筛选 + if (groupIds && groupIds.length > 0) { + if (exclude) { + // 排除指定分组(未分组) + conditions.push({ + field: "groupId", + operator: "notIn", + value: groupIds, + }); + } else { + // 包含指定分组 + conditions.push({ + field: "groupId", + operator: "in", + value: groupIds, + }); + } + } + + // 查询数据 + const allContacts = + await contactUnifiedService.findWhereMultiple(conditions); + + // 手动分页(IndexedDB 不支持原生的 offset/limit) + return allContacts.slice(offset, offset + limit); + } catch (error) { + console.error("分页获取联系人失败:", error); + return []; + } + } }