重构联系人分组逻辑,新增获取分组统计信息和分页获取联系人功能,优化联系人列表组件以提升用户体验和代码可读性。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user