重构联系人分组逻辑,新增获取分组统计信息和分页获取联系人功能,优化联系人列表组件以提升用户体验和代码可读性。

This commit is contained in:
超级老白兔
2025-10-23 20:15:59 +08:00
parent dc58109829
commit 1f081cdddc
4 changed files with 446 additions and 45 deletions

View File

@@ -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;
}
}
}

View File

@@ -199,13 +199,13 @@ export const getCountLables = async (): Promise<ContactGroupByLabel[]> => {
};
/**
* 根据标签对联系人进行分组
* 获取分组统计信息(只获取数量,不获取详细数据)
*/
export const groupContactsByLabels = (
contacts: Contact[],
export const getGroupStatistics = async (
userId: number,
labels: ContactGroupByLabel[],
): ContactGroupByLabel[] => {
// 获取所有非默认标签的ID
customerId?: number,
): Promise<ContactGroupByLabel[]> => {
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<Contact[]> => {
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 [];
};

View File

@@ -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<WechatFriendsProps> = ({
selectedContactId,
}) => {
// 本地状态
// 本地状态(用于数据同步,不直接用于显示)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [contacts, setContacts] = useState<Contact[]>([]);
const [contactGroups, setContactGroups] = useState<ContactGroupByLabel[]>([]);
const [labels, setLabels] = useState<ContactGroupByLabel[]>([]);
@@ -30,6 +31,20 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
const [refreshing, setRefreshing] = useState(false);
const [activeKey, setActiveKey] = useState<string[]>([]);
// 分页相关状态
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<WechatFriendsProps> = ({
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<WechatFriendsProps> = ({
</div>
);
// 监听分组展开/折叠事件
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 <div className={styles.noMoreText}></div>;
}
return (
<div className={styles.loadMoreContainer}>
<button
className={styles.loadMoreBtn}
onClick={() => handleLoadMore(groupKey)}
disabled={groupLoading[groupKey]}
>
{groupLoading[groupKey] ? "加载中..." : "加载更多"}
</button>
</div>
);
};
// 构建 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: (
<div className={styles.groupHeader}>
<span>{group.groupName}</span>
<span className={styles.contactCount}>
{group.contacts?.length || 0}
</span>
<span className={styles.contactCount}>{group.count || 0}</span>
</div>
),
className: styles.groupPanel,
children: (
<List
className={styles.list}
dataSource={group.contacts || []}
renderItem={renderContactItem}
/>
),
children: isActive ? (
<>
{groupLoading[groupKey] && !groupContacts[groupKey] ? (
// 首次加载显示骨架屏
<div className={styles.groupSkeleton}>
{Array(3)
.fill(null)
.map((_, i) => (
<div key={`skeleton-${i}`} className={styles.skeletonItem}>
<Skeleton.Avatar active size="large" shape="circle" />
<div className={styles.skeletonInfo}>
<Skeleton.Input
active
size="small"
style={{ width: "60%" }}
/>
</div>
</div>
))}
</div>
) : (
<>
<List
className={styles.list}
dataSource={groupContacts[groupKey] || []}
renderItem={renderContactItem}
/>
{(groupContacts[groupKey]?.length || 0) > 0 &&
renderLoadMoreButton(groupKey)}
</>
)}
</>
) : null,
};
});
};
@@ -224,7 +405,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
<Collapse
className={styles.groupCollapse}
activeKey={activeKey}
onChange={keys => setActiveKey(keys as string[])}
onChange={handleCollapseChange}
items={getCollapseItems()}
/>
{contactGroups.length === 0 && (

View File

@@ -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<number> {
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<Contact[]> {
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 [];
}
}
}