重构联系人管理逻辑,新增联系人状态管理和数据同步功能,优化联系人列表组件以提升用户体验和代码可读性。
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 { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||||
import { generateAiText } from "@/api/ai";
|
import { generateAiText } from "@/api/ai";
|
||||||
import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection";
|
import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection";
|
||||||
import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection";
|
import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection";
|
||||||
@@ -212,9 +213,7 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
|
|||||||
state.getKfUserInfo(contract.wechatAccountId || 0),
|
state.getKfUserInfo(contract.wechatAccountId || 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getSomeContractList = useCkChatStore(
|
const { getContactsByCustomer } = useContactStore();
|
||||||
state => state.getSomeContractList,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { sendCommand } = useWebSocketStore();
|
const { sendCommand } = useWebSocketStore();
|
||||||
|
|
||||||
@@ -931,10 +930,10 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
|
|||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const contractData = await getSomeContractList(
|
const contractData = getContactsByCustomer(
|
||||||
contract.wechatAccountId,
|
contract.wechatAccountId,
|
||||||
);
|
);
|
||||||
// 转换 ContractData[] 为 FriendSelectionItem[]
|
// 转换 Contact[] 为 FriendSelectionItem[]
|
||||||
const friendSelectionData = (contractData || []).map(
|
const friendSelectionData = (contractData || []).map(
|
||||||
item => ({
|
item => ({
|
||||||
id: item.id || item.serverId,
|
id: item.id || item.serverId,
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@weChatStore/weChat";
|
||||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
import { useMessageStore } from "@weChatStore/message";
|
||||||
|
import { useCustomerStore } from "@weChatStore/customer";
|
||||||
|
import { useContactStore } from "@weChatStore/contacts";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
import { useCustomerStore } from "@/store/module/weChat/customer";
|
|
||||||
import { updateConfig } from "@/pages/pc/ckbox/api";
|
import { updateConfig } from "@/pages/pc/ckbox/api";
|
||||||
import { getMessageList } from "./api";
|
import { getMessageList } from "./api";
|
||||||
import { dataProcessing } from "./api";
|
import { dataProcessing } from "./api";
|
||||||
@@ -18,13 +20,13 @@ import styles from "./MessageList.module.scss";
|
|||||||
import { formatWechatTime } from "@/utils/common";
|
import { formatWechatTime } from "@/utils/common";
|
||||||
import { MessageManager } from "@/utils/dbAction/message";
|
import { MessageManager } from "@/utils/dbAction/message";
|
||||||
import { ChatSession } from "@/utils/db";
|
import { ChatSession } from "@/utils/db";
|
||||||
import { useMessageStore } from "@/store/module/weChat/message";
|
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
interface MessageListProps {}
|
interface MessageListProps {}
|
||||||
|
|
||||||
const MessageList: React.FC<MessageListProps> = () => {
|
const MessageList: React.FC<MessageListProps> = () => {
|
||||||
|
const searchKeyword = useContactStore(state => state.searchKeyword);
|
||||||
const { setCurrentContact, currentContract } = useWeChatStore();
|
const { setCurrentContact, currentContract } = useWeChatStore();
|
||||||
const searchKeyword = useCkChatStore(state => state.searchKeyword);
|
|
||||||
const { currentCustomer } = useCustomerStore();
|
const { currentCustomer } = useCustomerStore();
|
||||||
const { sendCommand } = useWebSocketStore();
|
const { sendCommand } = useWebSocketStore();
|
||||||
const { user } = useUserStore();
|
const { user } = useUserStore();
|
||||||
|
|||||||
@@ -44,28 +44,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.groupPanel {
|
.groupPanel {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMoreContainer {
|
.loadMoreContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noMoreText {
|
.noMoreText {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noResults {
|
.noResults {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -119,4 +119,36 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 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 type { CollapseProps } from "antd";
|
||||||
import styles from "./WechatFriends.module.scss";
|
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 {
|
import {
|
||||||
useCkChatStore,
|
syncContactsFromServer,
|
||||||
searchContactsAndGroups,
|
filterContactsByCustomer,
|
||||||
} from "@/store/module/ckchat/ckchat";
|
getCountLables,
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
groupContactsByLabels,
|
||||||
import { addChatSession } from "@/store/module/ckchat/ckchat";
|
} from "./extend";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
|
||||||
|
|
||||||
interface WechatFriendsProps {
|
interface WechatFriendsProps {
|
||||||
selectedContactId?: ContractData | weChatGroup;
|
selectedContactId?: Contact;
|
||||||
}
|
}
|
||||||
const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
||||||
selectedContactId,
|
selectedContactId,
|
||||||
}) => {
|
}) => {
|
||||||
const [newContractList, setNewContractList] = useState<any[]>([]);
|
// 本地状态
|
||||||
const [searchResults, setSearchResults] = useState<
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||||
(ContractData | weChatGroup)[]
|
const [contactGroups, setContactGroups] = useState<ContactGroupByLabel[]>([]);
|
||||||
>([]);
|
const [labels, setLabels] = useState<ContactGroupByLabel[]>([]);
|
||||||
const getNewContractListFn = useCkChatStore(
|
const [loading, setLoading] = useState(false);
|
||||||
state => state.getNewContractList,
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
);
|
const [activeKey, setActiveKey] = useState<string[]>([]);
|
||||||
const kfSelected = useCkChatStore(state => state.kfSelected);
|
|
||||||
const countLables = useCkChatStore(state => state.countLables);
|
|
||||||
const searchKeyword = useCkChatStore(state => state.searchKeyword);
|
|
||||||
|
|
||||||
// 使用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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const loadLabels = async () => {
|
||||||
try {
|
try {
|
||||||
if (searchKeyword.trim()) {
|
const labelList = await getCountLables();
|
||||||
// 有搜索关键词时,获取搜索结果
|
setLabels(labelList);
|
||||||
const searchResult = await searchContactsAndGroups();
|
|
||||||
setSearchResults(searchResult || []);
|
|
||||||
setNewContractList([]);
|
|
||||||
} else {
|
|
||||||
// 无搜索关键词时,获取分组列表
|
|
||||||
const result = await getNewContractListFn();
|
|
||||||
setNewContractList(result || []);
|
|
||||||
setSearchResults([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取联系人数据失败:", error);
|
console.error("获取标签列表失败:", error);
|
||||||
setNewContractList([]);
|
|
||||||
setSearchResults([]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
loadLabels();
|
||||||
}, [getNewContractListFn, kfSelected, countLables, searchKeyword]);
|
}, []);
|
||||||
|
|
||||||
const [activeKey, setActiveKey] = useState<string[]>([]); // 默认展开第一个分组
|
// 初始化数据加载:先读取本地数据库,再静默同步
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
if (!currentUser?.id) return;
|
||||||
|
|
||||||
// 分页加载相关状态
|
setLoading(true);
|
||||||
const [visibleContacts, setVisibleContacts] = useState<{
|
|
||||||
[key: string]: ContractData[];
|
try {
|
||||||
}>({});
|
// 1. 先从本地数据库加载
|
||||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
const localContacts = await ContactManager.getUserContacts(
|
||||||
const [hasMore, setHasMore] = useState<{ [key: string]: boolean }>({});
|
currentUser.id,
|
||||||
const [page, setPage] = useState<{ [key: string]: number }>({});
|
);
|
||||||
const { setCurrentContact } = useWeChatStore();
|
|
||||||
const onContactClick = (contact: ContractData | weChatGroup) => {
|
if (localContacts && localContacts.length > 0) {
|
||||||
addChatSession(contact);
|
// 有本地数据,立即显示
|
||||||
|
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);
|
setCurrentContact(contact);
|
||||||
|
// 这里需要将 Contact 转换为 weChat 需要的格式
|
||||||
|
// 暂时使用 any 类型,后续需要完善转换逻辑
|
||||||
|
setWeChatCurrentContact(contact as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染联系人项
|
// 渲染联系人项
|
||||||
const renderContactItem = (contact: ContractData | weChatGroup) => {
|
const renderContactItem = (contact: Contact) => {
|
||||||
// 判断是否为群组
|
// 判断是否为群组
|
||||||
const isGroup = "chatroomId" in contact;
|
const isGroup = contact.type === "group";
|
||||||
const avatar = contact.avatar || contact.chatroomAvatar;
|
const avatar = contact.avatar;
|
||||||
const name = contact.conRemark || contact.nickname;
|
const name = contact.conRemark || contact.nickname;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -83,7 +140,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
|||||||
<div className={styles.avatarContainer}>
|
<div className={styles.avatarContainer}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={avatar}
|
src={avatar}
|
||||||
icon={!avatar && <span>{contact.nickname.charAt(0)}</span>}
|
icon={!avatar && <span>{contact.nickname?.charAt(0) || ""}</span>}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,120 +152,57 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化分页数据
|
// 渲染骨架屏
|
||||||
useEffect(() => {
|
const renderSkeleton = () => (
|
||||||
if (newContractList && newContractList.length > 0) {
|
<div className={styles.skeletonContainer}>
|
||||||
const initialVisibleContacts: { [key: string]: ContractData[] } = {};
|
{Array(10)
|
||||||
const initialLoading: { [key: string]: boolean } = {};
|
.fill(null)
|
||||||
const initialHasMore: { [key: string]: boolean } = {};
|
.map((_, index) => (
|
||||||
const initialPage: { [key: string]: number } = {};
|
<div key={`skeleton-${index}`} className={styles.skeletonItem}>
|
||||||
|
<Skeleton.Avatar active size="large" shape="circle" />
|
||||||
newContractList.forEach((group, index) => {
|
<div className={styles.skeletonInfo}>
|
||||||
const groupKey = index.toString();
|
<Skeleton.Input active size="small" style={{ width: "60%" }} />
|
||||||
// 每个分组初始加载20条数据
|
</div>
|
||||||
const pageSize = 20;
|
</div>
|
||||||
initialVisibleContacts[groupKey] = group.contacts.slice(0, pageSize);
|
))}
|
||||||
initialLoading[groupKey] = false;
|
</div>
|
||||||
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],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 渲染加载更多按钮
|
// 构建 Collapse 的 items
|
||||||
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属性
|
|
||||||
const getCollapseItems = (): CollapseProps["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 groupKey = index.toString();
|
||||||
const isActive = activeKey.includes(groupKey);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: groupKey,
|
key: groupKey,
|
||||||
label: (
|
label: (
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
<span>{group.groupName}</span>
|
<span>{group.groupName}</span>
|
||||||
<span className={styles.contactCount}>{group.contacts.length}</span>
|
<span className={styles.contactCount}>
|
||||||
|
{group.contacts?.length || 0}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
className: styles.groupPanel,
|
className: styles.groupPanel,
|
||||||
children: isActive ? (
|
children: (
|
||||||
<>
|
<List
|
||||||
<List
|
className={styles.list}
|
||||||
className={styles.list}
|
dataSource={group.contacts || []}
|
||||||
dataSource={visibleContacts[groupKey] || []}
|
renderItem={renderContactItem}
|
||||||
renderItem={renderContactItem}
|
/>
|
||||||
/>
|
),
|
||||||
{renderLoadMoreButton(groupKey)}
|
|
||||||
</>
|
|
||||||
) : null,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.contractListSimple}>
|
<div className={styles.contractListSimple}>
|
||||||
{searchKeyword.trim() ? (
|
{loading ? (
|
||||||
|
// 加载状态:显示骨架屏
|
||||||
|
renderSkeleton()
|
||||||
|
) : isSearchMode ? (
|
||||||
// 搜索模式:直接显示搜索结果列表
|
// 搜索模式:直接显示搜索结果列表
|
||||||
<>
|
<>
|
||||||
<div className={styles.header}>搜索结果</div>
|
<div className={styles.header}>搜索结果</div>
|
||||||
@@ -222,13 +216,21 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// 正常模式:显示分组
|
// 正常模式:显示分组列表
|
||||||
<Collapse
|
<>
|
||||||
className={styles.groupCollapse}
|
{refreshing && (
|
||||||
activeKey={activeKey}
|
<div className={styles.refreshingTip}>正在同步联系人...</div>
|
||||||
onChange={keys => setActiveKey(keys as string[])}
|
)}
|
||||||
items={getCollapseItems()}
|
<Collapse
|
||||||
/>
|
className={styles.groupCollapse}
|
||||||
|
activeKey={activeKey}
|
||||||
|
onChange={keys => setActiveKey(keys as string[])}
|
||||||
|
items={getCollapseItems()}
|
||||||
|
/>
|
||||||
|
{contactGroups.length === 0 && (
|
||||||
|
<div className={styles.noResults}>暂无联系人</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import WechatFriends from "./WechatFriends";
|
|||||||
import MessageList from "./MessageList/index";
|
import MessageList from "./MessageList/index";
|
||||||
import FriendsCircle from "./FriendsCicle";
|
import FriendsCircle from "./FriendsCicle";
|
||||||
import styles from "./SidebarMenu.module.scss";
|
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 {
|
interface SidebarMenuProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
||||||
const searchKeyword = useCkChatStore(state => state.searchKeyword);
|
const { searchKeyword, setSearchKeyword, clearSearchKeyword } =
|
||||||
const setSearchKeyword = useCkChatStore(state => state.setSearchKeyword);
|
useContactStore();
|
||||||
const clearSearchKeyword = useCkChatStore(state => state.clearSearchKeyword);
|
const currentCustomer = useCustomerStore(state => state.currentCustomer);
|
||||||
const kfSelected = useCkChatStore(state => state.kfSelected);
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState("chats");
|
const [activeTab, setActiveTab] = useState("chats");
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTab("chats");
|
setActiveTab("contracts");
|
||||||
}, [kfSelected]);
|
}, [currentCustomer]);
|
||||||
|
|
||||||
// 渲染骨架屏
|
// 渲染骨架屏
|
||||||
const renderSkeleton = () => (
|
const renderSkeleton = () => (
|
||||||
@@ -104,7 +104,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
|
|||||||
>
|
>
|
||||||
<span>联系人</span>
|
<span>联系人</span>
|
||||||
</div>
|
</div>
|
||||||
{kfSelected != 0 && (
|
{currentCustomer && currentCustomer.id !== 0 && (
|
||||||
<div
|
<div
|
||||||
className={`${styles.tabItem} ${activeTab === "friendsCicle" ? styles.active : ""}`}
|
className={`${styles.tabItem} ${activeTab === "friendsCicle" ? styles.active : ""}`}
|
||||||
onClick={() => setActiveTab("friendsCicle")}
|
onClick={() => setActiveTab("friendsCicle")}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
asyncContractList,
|
|
||||||
asyncChatSessions,
|
asyncChatSessions,
|
||||||
asyncWeChatGroup,
|
asyncWeChatGroup,
|
||||||
asyncCountLables,
|
asyncCountLables,
|
||||||
@@ -7,6 +6,7 @@ import {
|
|||||||
updateIsLoadWeChat,
|
updateIsLoadWeChat,
|
||||||
getIsLoadWeChat,
|
getIsLoadWeChat,
|
||||||
} from "@/store/module/ckchat/ckchat";
|
} from "@/store/module/ckchat/ckchat";
|
||||||
|
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { weChatGroupService, contractService } from "@/utils/db";
|
import { weChatGroupService, contractService } from "@/utils/db";
|
||||||
@@ -47,9 +47,10 @@ export const chatInitAPIdata = async () => {
|
|||||||
|
|
||||||
updateIsLoadWeChat(true);
|
updateIsLoadWeChat(true);
|
||||||
}
|
}
|
||||||
//获取联系人列表
|
//获取联系人列表 - 使用新的 contacts store
|
||||||
await asyncContractList(contractList);
|
// 注意:这里需要将 contractList 和 groupList 转换为统一的 Contact 格式
|
||||||
|
// 暂时保留旧的数据库写入逻辑,后续需要统一到 ContactManager
|
||||||
|
await contractService.createManyWithServerId(contractList);
|
||||||
await asyncWeChatGroup(groupList);
|
await asyncWeChatGroup(groupList);
|
||||||
|
|
||||||
//获取标签列表
|
//获取标签列表
|
||||||
@@ -247,36 +248,6 @@ export const getAllContactList = async () => {
|
|||||||
return [];
|
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 () => {
|
export const getAllGroupList = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -318,3 +289,33 @@ export const getAllGroupList = async () => {
|
|||||||
return [];
|
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 {
|
export interface CkChatState {
|
||||||
userInfo: CkUserInfo | null;
|
userInfo: CkUserInfo | null;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
searchKeyword: string;
|
|
||||||
isLoadWeChat: boolean;
|
isLoadWeChat: boolean;
|
||||||
getIsLoadWeChat: () => boolean;
|
getIsLoadWeChat: () => boolean;
|
||||||
updateIsLoadWeChat: (isLoadWeChat: boolean) => void;
|
updateIsLoadWeChat: (isLoadWeChat: boolean) => void;
|
||||||
contractList: ContractData[];
|
|
||||||
chatSessions: any[];
|
chatSessions: any[];
|
||||||
kfUserList: KfUserListData[];
|
kfUserList: KfUserListData[];
|
||||||
kfSelected: number;
|
kfSelected: number;
|
||||||
getKfSelectedUser: () => KfUserListData | undefined;
|
getKfSelectedUser: () => KfUserListData | undefined;
|
||||||
countLables: ContactGroupByLabel[];
|
countLables: ContactGroupByLabel[];
|
||||||
newContractList: ContactGroupByLabel[];
|
|
||||||
getContractList: () => ContractData[];
|
|
||||||
getSomeContractList: (kfSelected: number) => ContractData[];
|
|
||||||
getNewContractList: () => Promise<ContactGroupByLabel[]>;
|
|
||||||
setSearchKeyword: (keyword: string) => void;
|
|
||||||
clearSearchKeyword: () => void;
|
|
||||||
asyncKfSelected: (data: number) => void;
|
asyncKfSelected: (data: number) => void;
|
||||||
asyncWeChatGroup: (data: weChatGroup[]) => void;
|
asyncWeChatGroup: (data: weChatGroup[]) => void;
|
||||||
asyncCountLables: (data: ContactGroupByLabel[]) => void;
|
asyncCountLables: (data: ContactGroupByLabel[]) => void;
|
||||||
getkfUserList: () => KfUserListData[];
|
getkfUserList: () => KfUserListData[];
|
||||||
asyncKfUserList: (data: KfUserListData[]) => void;
|
asyncKfUserList: (data: KfUserListData[]) => void;
|
||||||
getKfUserInfo: (wechatAccountId: number) => KfUserListData | undefined;
|
getKfUserInfo: (wechatAccountId: number) => KfUserListData | undefined;
|
||||||
asyncContractList: (data: ContractData[]) => void;
|
|
||||||
getChatSessions: () => any[];
|
getChatSessions: () => any[];
|
||||||
asyncChatSessions: (data: any[]) => void;
|
asyncChatSessions: (data: any[]) => void;
|
||||||
updateChatSession: (session: ContractData | weChatGroup) => void;
|
updateChatSession: (session: ContractData | weChatGroup) => void;
|
||||||
|
|||||||
@@ -17,13 +17,10 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
set => ({
|
set => ({
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
contractList: [], //联系人列表
|
|
||||||
chatSessions: [], //聊天会话
|
chatSessions: [], //聊天会话
|
||||||
kfUserList: [], //客服列表
|
kfUserList: [], //客服列表
|
||||||
countLables: [], //标签列表
|
countLables: [], //标签列表
|
||||||
newContractList: [], //联系人分组
|
|
||||||
kfSelected: 0, //选中的客服
|
kfSelected: 0, //选中的客服
|
||||||
searchKeyword: "", //搜索关键词
|
|
||||||
isLoadWeChat: false, //是否加载微信
|
isLoadWeChat: false, //是否加载微信
|
||||||
getIsLoadWeChat: () => {
|
getIsLoadWeChat: () => {
|
||||||
return useCkChatStore.getState().isLoadWeChat;
|
return useCkChatStore.getState().isLoadWeChat;
|
||||||
@@ -44,27 +41,10 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
// 异步设置标签列表
|
// 异步设置标签列表
|
||||||
asyncCountLables: async (data: ContactGroupByLabel[]) => {
|
asyncCountLables: async (data: ContactGroupByLabel[]) => {
|
||||||
set({ countLables: data });
|
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) => {
|
asyncKfSelected: async (data: number) => {
|
||||||
set({ kfSelected: data });
|
set({ kfSelected: data });
|
||||||
// 清除getChatSessions、getContractList和getNewContractList缓存
|
// 清除getChatSessions缓存
|
||||||
const state = useCkChatStore.getState();
|
const state = useCkChatStore.getState();
|
||||||
if (
|
if (
|
||||||
state.getChatSessions &&
|
state.getChatSessions &&
|
||||||
@@ -73,151 +53,8 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
// 触发缓存重新计算
|
// 触发缓存重新计算
|
||||||
state.getChatSessions();
|
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 => {
|
asyncChatSessions: data => {
|
||||||
set({ chatSessions: data });
|
set({ chatSessions: data });
|
||||||
@@ -231,73 +68,6 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
state.getChatSessions();
|
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[]) => {
|
asyncWeChatGroup: async (data: weChatGroup[]) => {
|
||||||
await weChatGroupService.createManyWithServerId(data);
|
await weChatGroupService.createManyWithServerId(data);
|
||||||
@@ -341,7 +111,6 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
let cachedResult: any = null;
|
let cachedResult: any = null;
|
||||||
let lastKfSelected: number | null = null;
|
let lastKfSelected: number | null = null;
|
||||||
let lastChatSessionsLength: number = 0;
|
let lastChatSessionsLength: number = 0;
|
||||||
let lastSearchKeyword: string = "";
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const state = useCkChatStore.getState();
|
const state = useCkChatStore.getState();
|
||||||
@@ -350,8 +119,7 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
const shouldRecalculate =
|
const shouldRecalculate =
|
||||||
cachedResult === null ||
|
cachedResult === null ||
|
||||||
lastKfSelected !== state.kfSelected ||
|
lastKfSelected !== state.kfSelected ||
|
||||||
lastChatSessionsLength !== state.chatSessions.length ||
|
lastChatSessionsLength !== state.chatSessions.length;
|
||||||
lastSearchKeyword !== state.searchKeyword;
|
|
||||||
|
|
||||||
if (shouldRecalculate) {
|
if (shouldRecalculate) {
|
||||||
let filteredSessions = state.chatSessions;
|
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;
|
cachedResult = filteredSessions;
|
||||||
lastKfSelected = state.kfSelected;
|
lastKfSelected = state.kfSelected;
|
||||||
lastChatSessionsLength = state.chatSessions.length;
|
lastChatSessionsLength = state.chatSessions.length;
|
||||||
lastSearchKeyword = state.searchKeyword;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedResult;
|
return cachedResult;
|
||||||
@@ -621,21 +378,9 @@ export const getKfSelectedUser = () =>
|
|||||||
useCkChatStore.getState().getKfSelectedUser();
|
useCkChatStore.getState().getKfSelectedUser();
|
||||||
export const getKfUserInfo = (wechatAccountId: number) =>
|
export const getKfUserInfo = (wechatAccountId: number) =>
|
||||||
useCkChatStore.getState().getKfUserInfo(wechatAccountId);
|
useCkChatStore.getState().getKfUserInfo(wechatAccountId);
|
||||||
export const getContractList = () =>
|
|
||||||
useCkChatStore.getState().getContractList();
|
|
||||||
export const getNewContractList = () =>
|
|
||||||
useCkChatStore.getState().getNewContractList();
|
|
||||||
export const asyncCountLables = (data: ContactGroupByLabel[]) =>
|
export const asyncCountLables = (data: ContactGroupByLabel[]) =>
|
||||||
useCkChatStore.getState().asyncCountLables(data);
|
useCkChatStore.getState().asyncCountLables(data);
|
||||||
export const asyncNewContractList = (data: any[]) =>
|
|
||||||
useCkChatStore.getState().asyncNewContractList(data);
|
|
||||||
export const getCountLables = () => useCkChatStore.getState().countLables;
|
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) =>
|
export const pinChatSessionToTop = (sessionId: number) =>
|
||||||
useCkChatStore.getState().pinChatSessionToTop(sessionId);
|
useCkChatStore.getState().pinChatSessionToTop(sessionId);
|
||||||
useCkChatStore.getState().getKfSelectedUser();
|
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;
|
updateSelectedChatRecords: (message: ChatRecord[]) => void;
|
||||||
|
|
||||||
/** 选中的联系人或群组列表 */
|
|
||||||
selectedTransmitContact: ContractData[] | weChatGroup[];
|
|
||||||
/** 更新选中的联系人或群组 */
|
|
||||||
updateSelectedTransmitContact: (
|
|
||||||
contact: ContractData[] | weChatGroup[],
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
/** 转发弹窗开启状态 */
|
|
||||||
openTransmitModal: boolean;
|
|
||||||
/** 更新转发弹窗状态 */
|
|
||||||
updateTransmitModal: (open: boolean) => void;
|
|
||||||
// ==================== Transmit Module =========END===========
|
// ==================== Transmit Module =========END===========
|
||||||
|
|
||||||
// ==================== 当前联系人管理 ====================
|
// ==================== 当前联系人管理 ====================
|
||||||
|
|||||||
@@ -59,21 +59,6 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
set({ selectedChatRecords: message });
|
set({ selectedChatRecords: message });
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 选中的联系人或群组列表 */
|
|
||||||
selectedTransmitContact: [],
|
|
||||||
/** 更新选中的联系人或群组 */
|
|
||||||
updateSelectedTransmitContact: (
|
|
||||||
contact: ContractData[] | weChatGroup[],
|
|
||||||
) => {
|
|
||||||
set({ selectedTransmitContact: contact });
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 转发弹窗开启状态 */
|
|
||||||
openTransmitModal: false,
|
|
||||||
/** 更新转发弹窗状态 */
|
|
||||||
updateTransmitModal: (open: boolean) => {
|
|
||||||
set({ openTransmitModal: open });
|
|
||||||
},
|
|
||||||
// ==================== Transmit Module =========END===========
|
// ==================== Transmit Module =========END===========
|
||||||
|
|
||||||
// ==================== 当前联系人管理状态 ====================
|
// ==================== 当前联系人管理状态 ====================
|
||||||
@@ -159,7 +144,7 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
) => {
|
) => {
|
||||||
const state = useWeChatStore.getState();
|
const state = useWeChatStore.getState();
|
||||||
// 切换联系人时清空当前消息,等待重新加载
|
// 切换联系人时清空当前消息,等待重新加载
|
||||||
set({ currentMessages: [], openTransmitModal: false });
|
set({ currentMessages: [] });
|
||||||
|
|
||||||
const params: any = {};
|
const params: any = {};
|
||||||
|
|
||||||
@@ -495,7 +480,7 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "wechat-storage",
|
name: "wechat-storage",
|
||||||
partialize: state => ({
|
partialize: () => ({
|
||||||
// currentContract 不做持久化,登录和页面刷新时直接清空
|
// currentContract 不做持久化,登录和页面刷新时直接清空
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export interface Contact {
|
|||||||
wechatId?: string; // 微信号
|
wechatId?: string; // 微信号
|
||||||
alias?: string; // 别名
|
alias?: string; // 别名
|
||||||
gender?: number; // 性别
|
gender?: number; // 性别
|
||||||
|
groupId?: number; // 标签ID(分组)
|
||||||
region?: string; // 地区
|
region?: string; // 地区
|
||||||
signature?: string; // 个性签名
|
signature?: string; // 个性签名
|
||||||
phone?: 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