重构联系人管理逻辑,新增联系人状态管理和数据同步功能,优化联系人列表组件以提升用户体验和代码可读性。
This commit is contained in:
@@ -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<PersonProps> = ({ 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<PersonProps> = ({ contract }) => {
|
||||
icon={<PlusOutlined />}
|
||||
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,
|
||||
|
||||
@@ -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<MessageListProps> = () => {
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ContactGroupByLabel[]> => {
|
||||
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;
|
||||
};
|
||||
@@ -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<WechatFriendsProps> = ({
|
||||
selectedContactId,
|
||||
}) => {
|
||||
const [newContractList, setNewContractList] = useState<any[]>([]);
|
||||
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<Contact[]>([]);
|
||||
const [contactGroups, setContactGroups] = useState<ContactGroupByLabel[]>([]);
|
||||
const [labels, setLabels] = useState<ContactGroupByLabel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [activeKey, setActiveKey] = useState<string[]>([]);
|
||||
|
||||
// 使用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<string[]>([]); // 默认展开第一个分组
|
||||
// 初始化数据加载:先读取本地数据库,再静默同步
|
||||
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<WechatFriendsProps> = ({
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
src={avatar}
|
||||
icon={!avatar && <span>{contact.nickname.charAt(0)}</span>}
|
||||
icon={!avatar && <span>{contact.nickname?.charAt(0) || ""}</span>}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</div>
|
||||
@@ -95,120 +152,57 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化分页数据
|
||||
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 = () => (
|
||||
<div className={styles.skeletonContainer}>
|
||||
{Array(10)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<div key={`skeleton-${index}`} 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>
|
||||
);
|
||||
|
||||
// 渲染加载更多按钮
|
||||
const renderLoadMoreButton = (groupKey: string) => {
|
||||
if (!hasMore[groupKey])
|
||||
return <div className={styles.noMoreText}>没有更多了</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.loadMoreContainer}>
|
||||
<Button
|
||||
size="small"
|
||||
loading={loading[groupKey]}
|
||||
onClick={() => loadMoreContacts(groupKey)}
|
||||
>
|
||||
{loading[groupKey] ? "加载中..." : "加载更多"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 构建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: (
|
||||
<div className={styles.groupHeader}>
|
||||
<span>{group.groupName}</span>
|
||||
<span className={styles.contactCount}>{group.contacts.length}</span>
|
||||
<span className={styles.contactCount}>
|
||||
{group.contacts?.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
className: styles.groupPanel,
|
||||
children: isActive ? (
|
||||
<>
|
||||
<List
|
||||
className={styles.list}
|
||||
dataSource={visibleContacts[groupKey] || []}
|
||||
renderItem={renderContactItem}
|
||||
/>
|
||||
{renderLoadMoreButton(groupKey)}
|
||||
</>
|
||||
) : null,
|
||||
children: (
|
||||
<List
|
||||
className={styles.list}
|
||||
dataSource={group.contacts || []}
|
||||
renderItem={renderContactItem}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.contractListSimple}>
|
||||
{searchKeyword.trim() ? (
|
||||
{loading ? (
|
||||
// 加载状态:显示骨架屏
|
||||
renderSkeleton()
|
||||
) : isSearchMode ? (
|
||||
// 搜索模式:直接显示搜索结果列表
|
||||
<>
|
||||
<div className={styles.header}>搜索结果</div>
|
||||
@@ -222,13 +216,21 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 正常模式:显示分组
|
||||
<Collapse
|
||||
className={styles.groupCollapse}
|
||||
activeKey={activeKey}
|
||||
onChange={keys => setActiveKey(keys as string[])}
|
||||
items={getCollapseItems()}
|
||||
/>
|
||||
// 正常模式:显示分组列表
|
||||
<>
|
||||
{refreshing && (
|
||||
<div className={styles.refreshingTip}>正在同步联系人...</div>
|
||||
)}
|
||||
<Collapse
|
||||
className={styles.groupCollapse}
|
||||
activeKey={activeKey}
|
||||
onChange={keys => setActiveKey(keys as string[])}
|
||||
items={getCollapseItems()}
|
||||
/>
|
||||
{contactGroups.length === 0 && (
|
||||
<div className={styles.noResults}>暂无联系人</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<SidebarMenuProps> = ({ 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<SidebarMenuProps> = ({ loading = false }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab("chats");
|
||||
}, [kfSelected]);
|
||||
setActiveTab("contracts");
|
||||
}, [currentCustomer]);
|
||||
|
||||
// 渲染骨架屏
|
||||
const renderSkeleton = () => (
|
||||
@@ -104,7 +104,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
||||
>
|
||||
<span>联系人</span>
|
||||
</div>
|
||||
{kfSelected != 0 && (
|
||||
{currentCustomer && currentCustomer.id !== 0 && (
|
||||
<div
|
||||
className={`${styles.tabItem} ${activeTab === "friendsCicle" ? styles.active : ""}`}
|
||||
onClick={() => setActiveTab("friendsCicle")}
|
||||
|
||||
@@ -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<number>();
|
||||
|
||||
// 遍历联系人列表,将每个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<number>();
|
||||
|
||||
// 遍历联系人列表,将每个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);
|
||||
};
|
||||
|
||||
@@ -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<ContactGroupByLabel[]>;
|
||||
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;
|
||||
|
||||
@@ -17,13 +17,10 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
||||
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<CkChatState>(
|
||||
// 异步设置标签列表
|
||||
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<CkChatState>(
|
||||
// 触发缓存重新计算
|
||||
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<CkChatState>(
|
||||
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<CkChatState>(
|
||||
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<CkChatState>(
|
||||
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<CkChatState>(
|
||||
);
|
||||
}
|
||||
|
||||
// 根据搜索关键词筛选
|
||||
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();
|
||||
|
||||
70
Touchkebao/src/store/module/weChat/contacts.data.ts
Normal file
70
Touchkebao/src/store/module/weChat/contacts.data.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<void>;
|
||||
/** 根据客服筛选联系人 */
|
||||
filterByCustomer: (customerId: number) => Promise<void>;
|
||||
/** 清空筛选 */
|
||||
clearFilter: () => void;
|
||||
|
||||
// ==================== 数据同步方法 ====================
|
||||
/** 从服务器同步联系人数据 */
|
||||
syncFromServer: (userId: number) => Promise<void>;
|
||||
/** 从本地数据库加载联系人数据 */
|
||||
loadFromLocal: (userId: number) => Promise<void>;
|
||||
/** 刷新联系人数据 */
|
||||
refreshContacts: (userId: number) => Promise<void>;
|
||||
|
||||
// ==================== 联系人操作方法 ====================
|
||||
/** 添加联系人 */
|
||||
addContact: (contact: Contact) => Promise<void>;
|
||||
/** 更新联系人 */
|
||||
updateContact: (contact: Contact) => Promise<void>;
|
||||
/** 删除联系人 */
|
||||
deleteContact: (contactId: number) => Promise<void>;
|
||||
/** 批量添加联系人 */
|
||||
addContacts: (contacts: Contact[]) => Promise<void>;
|
||||
|
||||
// ==================== 分组管理方法 ====================
|
||||
/** 获取联系人分组列表 */
|
||||
getContactGroups: (
|
||||
userId: number,
|
||||
customerId?: number,
|
||||
) => Promise<ContactGroupByLabel[]>;
|
||||
/** 创建联系人分组 */
|
||||
createContactGroup: (group: Omit<ContactGroupByLabel, "id">) => Promise<void>;
|
||||
/** 更新联系人分组 */
|
||||
updateContactGroup: (group: ContactGroupByLabel) => Promise<void>;
|
||||
/** 删除联系人分组 */
|
||||
deleteContactGroup: (groupId: number) => Promise<void>;
|
||||
|
||||
// ==================== 分页方法 ====================
|
||||
/** 加载更多联系人 */
|
||||
loadMoreContacts: (groupId: string) => Promise<void>;
|
||||
/** 重置分页状态 */
|
||||
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<ContactState>()(
|
||||
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<ContactGroupByLabel, "id">) => {
|
||||
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);
|
||||
|
||||
@@ -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===========
|
||||
|
||||
// ==================== 当前联系人管理 ====================
|
||||
|
||||
@@ -59,21 +59,6 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
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<WeChatState>()(
|
||||
) => {
|
||||
const state = useWeChatStore.getState();
|
||||
// 切换联系人时清空当前消息,等待重新加载
|
||||
set({ currentMessages: [], openTransmitModal: false });
|
||||
set({ currentMessages: [] });
|
||||
|
||||
const params: any = {};
|
||||
|
||||
@@ -495,7 +480,7 @@ export const useWeChatStore = create<WeChatState>()(
|
||||
}),
|
||||
{
|
||||
name: "wechat-storage",
|
||||
partialize: state => ({
|
||||
partialize: () => ({
|
||||
// currentContract 不做持久化,登录和页面刷新时直接清空
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -87,6 +87,7 @@ export interface Contact {
|
||||
wechatId?: string; // 微信号
|
||||
alias?: string; // 别名
|
||||
gender?: number; // 性别
|
||||
groupId?: number; // 标签ID(分组)
|
||||
region?: string; // 地区
|
||||
signature?: string; // 个性签名
|
||||
phone?: string; // 手机号
|
||||
|
||||
@@ -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<Contact[]> {
|
||||
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<Contact[]> {
|
||||
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<Contact[]> {
|
||||
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<void> {
|
||||
try {
|
||||
await contactUnifiedService.create(contact);
|
||||
} catch (error) {
|
||||
console.error("添加联系人失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加联系人
|
||||
*/
|
||||
static async addContacts(contacts: Contact[]): Promise<void> {
|
||||
try {
|
||||
await contactUnifiedService.createMany(contacts);
|
||||
} catch (error) {
|
||||
console.error("批量添加联系人失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新联系人
|
||||
*/
|
||||
static async updateContact(contact: Contact): Promise<void> {
|
||||
try {
|
||||
await contactUnifiedService.update(contact.serverId, contact);
|
||||
} catch (error) {
|
||||
console.error("更新联系人失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除联系人
|
||||
*/
|
||||
static async deleteContact(contactId: number): Promise<void> {
|
||||
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<void> {
|
||||
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<ContactGroupByLabel[]> {
|
||||
try {
|
||||
// 这里应该根据实际的标签系统来实现
|
||||
// 暂时返回空数组,实际实现需要根据标签表来查询
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("获取联系人分组失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建联系人分组
|
||||
*/
|
||||
static async createContactGroup(
|
||||
group: Omit<ContactGroupByLabel, "id">,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 这里应该调用标签相关的API
|
||||
console.log("创建联系人分组:", group);
|
||||
} catch (error) {
|
||||
console.error("创建联系人分组失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新联系人分组
|
||||
*/
|
||||
static async updateContactGroup(group: ContactGroupByLabel): Promise<void> {
|
||||
try {
|
||||
// 这里应该调用标签相关的API
|
||||
console.log("更新联系人分组:", group);
|
||||
} catch (error) {
|
||||
console.error("更新联系人分组失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除联系人分组
|
||||
*/
|
||||
static async deleteContactGroup(groupId: number): Promise<void> {
|
||||
try {
|
||||
// 这里应该调用标签相关的API
|
||||
console.log("删除联系人分组:", groupId);
|
||||
} catch (error) {
|
||||
console.error("删除联系人分组失败:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取联系人
|
||||
*/
|
||||
static async getContactById(
|
||||
userId: number,
|
||||
contactId: number,
|
||||
): Promise<Contact | null> {
|
||||
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<Contact[]> {
|
||||
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: {} };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { UserLoginRecord, userLoginRecordService } from "@/utils/db";
|
||||
|
||||
/**
|
||||
* 登录管理器
|
||||
* 负责用户登录记录相关的数据库操作和业务逻辑
|
||||
*/
|
||||
export class LoginManager {
|
||||
/**
|
||||
* 记录用户登录
|
||||
*/
|
||||
static async recordLogin(userId: number): Promise<void> {
|
||||
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<void> {
|
||||
try {
|
||||
const serverId = `user_${userId}`;
|
||||
await userLoginRecordService.update(serverId, {
|
||||
lastActiveTime: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("更新用户活跃时间失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户登录记录
|
||||
*/
|
||||
static async getLoginRecord(userId: number): Promise<UserLoginRecord | null> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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<UserLoginRecord[]> {
|
||||
try {
|
||||
return (await userLoginRecordService.findAll()) as UserLoginRecord[];
|
||||
} catch (error) {
|
||||
console.error("获取所有登录记录失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户登录记录
|
||||
*/
|
||||
static async deleteLoginRecord(userId: number): Promise<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user