-
0
- ? session.config.unreadCount
- : ""
- }
- >
- {session?.lastMessage}
+ {loading ? (
+ // 加载状态:显示骨架屏
+ renderSkeleton()
+ ) : (
+ <>
+
(
+ onContactClick(session)}
+ onContextMenu={e => handleContextMenu(e, session)}
+ >
+
+
+
+ ) : (
+
+ )
+ }
+ />
+
+
+
+
+ {session.conRemark ||
+ session.nickname ||
+ session.wechatId}
+
+
+ {formatWechatTime(session?.lastUpdateTime)}
+
+
+
+ {messageFilter(session.content)}
+
+
+ )}
+ />
+
+ {/* 右键菜单 */}
+ {contextMenu.visible && contextMenu.session && (
+
+
handleTogglePin(contextMenu.session!)}
+ >
+
+ {(contextMenu.session.config as any)?.top ? "取消置顶" : "置顶"}
+
+
handleEditRemark(contextMenu.session!)}
+ >
+
+ 修改备注
+
+
handleDelete(contextMenu.session!)}
+ >
+
+ 删除
-
- )}
- />
+ )}
+
+ {/* 修改备注Modal */}
+
+ setEditRemarkModal({
+ visible: false,
+ session: null,
+ remark: "",
+ })
+ }
+ okText="保存"
+ cancelText="取消"
+ >
+
+ setEditRemarkModal(prev => ({
+ ...prev,
+ remark: e.target.value,
+ }))
+ }
+ placeholder="请输入备注"
+ maxLength={20}
+ />
+
+ >
+ )}
);
};
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss
similarity index 57%
rename from Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss
rename to Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss
index 00633207..70630da1 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/WechatFriends.module.scss
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/com.module.scss
@@ -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,56 @@
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;
+ }
+
+ // 分组骨架屏
+ .groupSkeleton {
+ padding: 10px;
+ }
+
+ // 加载更多文字
+ .loadMoreText {
+ text-align: center;
+ padding: 10px 15px;
+ font-size: 14px;
+ color: #1890ff;
+ cursor: pointer;
+ transition: all 0.3s;
+
+ &:hover {
+ color: #40a9ff;
+ background-color: #f5f5f5;
+ }
+ }
}
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts
new file mode 100644
index 00000000..220c4d56
--- /dev/null
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/extend.ts
@@ -0,0 +1,343 @@
+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
=> {
+ 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 getGroupStatistics = async (
+ userId: number,
+ labels: ContactGroupByLabel[],
+ customerId?: number,
+): Promise => {
+ // 分别提取好友标签和群标签的ID
+ const friendGroupIds = labels
+ .filter(item => item.id !== 0 && Number(item.groupType) === 1)
+ .map(item => item.id);
+
+ const chatGroupIds = labels
+ .filter(item => item.id !== 0 && Number(item.groupType) === 2)
+ .map(item => item.id);
+
+ const groupedData: ContactGroupByLabel[] = [];
+
+ for (const label of labels) {
+ let count = 0;
+
+ if (Number(label.groupType) === 1) {
+ // 好友分组
+ if (label.id === 0) {
+ // 未分组:不属于任何好友标签的好友
+ count = await ContactManager.getContactCount(
+ userId,
+ "friend",
+ customerId,
+ friendGroupIds, // 只排除好友标签
+ true, // 排除已分组的
+ );
+ } else {
+ // 指定标签的好友
+ count = await ContactManager.getContactCount(
+ userId,
+ "friend",
+ customerId,
+ [label.id],
+ false, // 包含指定分组
+ );
+ }
+ } else if (Number(label.groupType) === 2) {
+ // 群组分组
+ if (label.id === 0) {
+ // 默认群分组:不属于任何群标签的群
+ count = await ContactManager.getContactCount(
+ userId,
+ "group",
+ customerId,
+ chatGroupIds, // 只排除群标签
+ true, // 排除已分组的
+ );
+ } else {
+ // 指定标签的群
+ count = await ContactManager.getContactCount(
+ userId,
+ "group",
+ customerId,
+ [label.id],
+ false, // 包含指定分组
+ );
+ }
+ }
+
+ groupedData.push({
+ ...label,
+ contacts: [], // 不加载数据,只返回统计信息
+ count, // 添加统计数量
+ });
+ }
+
+ return groupedData;
+};
+
+/**
+ * 分页获取指定分组的联系人
+ */
+export const getContactsByGroup = async (
+ userId: number,
+ label: ContactGroupByLabel,
+ realGroupIds: number[],
+ customerId?: number,
+ page: number = 1,
+ pageSize: number = 20,
+): Promise => {
+ const offset = (page - 1) * pageSize;
+
+ if (Number(label.groupType) === 1) {
+ // 好友分组
+ if (label.id === 0) {
+ // 未分组的好友
+ return await ContactManager.getContactsByGroupPaginated(
+ userId,
+ "friend",
+ customerId,
+ realGroupIds,
+ true, // 排除已分组的
+ offset,
+ pageSize,
+ );
+ } else {
+ // 指定标签的好友
+ return await ContactManager.getContactsByGroupPaginated(
+ userId,
+ "friend",
+ customerId,
+ [label.id],
+ false, // 包含指定分组
+ offset,
+ pageSize,
+ );
+ }
+ } else if (Number(label.groupType) === 2) {
+ // 群组分组
+ if (label.id === 0) {
+ // 默认群分组
+ return await ContactManager.getContactsByGroupPaginated(
+ userId,
+ "group",
+ customerId,
+ realGroupIds,
+ true, // 排除已分组的
+ offset,
+ pageSize,
+ );
+ } else {
+ // 指定标签的群
+ return await ContactManager.getContactsByGroupPaginated(
+ userId,
+ "group",
+ customerId,
+ [label.id],
+ false, // 包含指定分组
+ offset,
+ pageSize,
+ );
+ }
+ }
+
+ return [];
+};
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx
index 76f5f981..deed4a02 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/WechatFriends/index.tsx
@@ -1,77 +1,344 @@
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 dayjs from "dayjs";
+import styles from "./com.module.scss";
+import { Contact, ChatSession } from "@/utils/db";
+import { ContactManager, MessageManager } from "@/utils/dbAction";
+import { ContactGroupByLabel } from "@/pages/pc/ckbox/data";
+import { useContactStore } from "@weChatStore/contacts";
+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,
+ getCountLables,
+ getGroupStatistics,
+ getContactsByGroup,
+} from "./extend";
interface WechatFriendsProps {
- selectedContactId?: ContractData | weChatGroup;
+ selectedContactId?: Contact;
}
const ContactListSimple: React.FC = ({
selectedContactId,
}) => {
- const [newContractList, setNewContractList] = useState([]);
- 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 [contactGroups, setContactGroups] = useState([]);
+ const [labels, setLabels] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [initializing, setInitializing] = useState(true); // 初始化状态
+ const [activeKey, setActiveKey] = useState([]);
- // 使用useEffect来处理异步的getNewContractList调用
- useEffect(() => {
- const fetchData = async () => {
+ // 分页相关状态(合并为对象,减少状态数量)
+ const [groupData, setGroupData] = useState<{
+ contacts: { [groupKey: string]: Contact[] };
+ pages: { [groupKey: string]: number };
+ loading: { [groupKey: string]: boolean };
+ hasMore: { [groupKey: string]: boolean };
+ }>({
+ contacts: {},
+ pages: {},
+ loading: {},
+ hasMore: {},
+ });
+
+ // 使用新的 contacts store
+ const { searchResults, isSearchMode, setCurrentContact } = useContactStore();
+
+ // 获取用户和客服信息
+ const currentUser = useUserStore(state => state.user);
+ const currentCustomer = useCustomerStore(state => state.currentCustomer);
+
+ // 从服务器同步数据(静默同步,不显示提示)
+ const syncWithServer = useCallback(
+ async (userId: number) => {
try {
- if (searchKeyword.trim()) {
- // 有搜索关键词时,获取搜索结果
- const searchResult = await searchContactsAndGroups();
- setSearchResults(searchResult || []);
- setNewContractList([]);
- } else {
- // 无搜索关键词时,获取分组列表
- const result = await getNewContractListFn();
- setNewContractList(result || []);
- setSearchResults([]);
+ await syncContactsFromServer(userId);
+ // 同步完成后,重新加载分组统计
+ if (labels.length > 0) {
+ const groupStats = await getGroupStatistics(
+ userId,
+ labels,
+ currentCustomer?.id,
+ );
+ setContactGroups(groupStats);
}
} catch (error) {
- console.error("获取联系人数据失败:", error);
- setNewContractList([]);
- setSearchResults([]);
+ console.error("同步联系人失败:", error);
+ }
+ },
+ [labels, currentCustomer?.id],
+ );
+
+ // 获取标签列表
+ useEffect(() => {
+ const loadLabels = async () => {
+ try {
+ const labelList = await getCountLables();
+ setLabels(labelList);
+ } catch (error) {
+ console.error("获取标签列表失败:", error);
}
};
- fetchData();
- }, [getNewContractListFn, kfSelected, countLables, searchKeyword]);
+ loadLabels();
+ }, []);
- const [activeKey, setActiveKey] = useState([]); // 默认展开第一个分组
+ // 初始化数据加载:先读取本地数据库,再静默同步
+ 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);
- setCurrentContact(contact);
+ try {
+ // 检查本地数据库是否有数据
+ const localContacts = await ContactManager.getUserContacts(
+ currentUser.id,
+ );
+
+ if (localContacts && localContacts.length > 0) {
+ // 有本地数据,直接显示(不显示 loading)
+ // 后台静默同步
+ syncWithServer(currentUser.id);
+ } else {
+ // 没有本地数据,显示骨架屏并从服务器获取
+ setLoading(true);
+ await syncWithServer(currentUser.id);
+ setLoading(false);
+ }
+ } catch (error) {
+ console.error("加载联系人数据失败:", error);
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, [currentUser?.id, syncWithServer]);
+
+ // 获取分组统计信息(只获取数量,不加载联系人数据)
+ useEffect(() => {
+ const loadGroupStats = async () => {
+ if (!currentUser?.id || labels.length === 0) return;
+
+ try {
+ // 先检查本地数据库是否有数据
+ const localContacts = await ContactManager.getUserContacts(
+ currentUser.id,
+ );
+
+ if (localContacts && localContacts.length > 0) {
+ // 有本地数据,直接加载分组统计(不显示骨架屏)
+ const groupStats = await getGroupStatistics(
+ currentUser.id,
+ labels,
+ currentCustomer?.id,
+ );
+ setContactGroups(groupStats);
+ setInitializing(false); // 初始化完成
+ } else {
+ // 没有本地数据,显示骨架屏
+ setLoading(true);
+ const groupStats = await getGroupStatistics(
+ currentUser.id,
+ labels,
+ currentCustomer?.id,
+ );
+ setContactGroups(groupStats);
+ setLoading(false);
+ setInitializing(false); // 初始化完成
+ }
+ } catch (error) {
+ console.error("获取分组统计失败:", error);
+ setLoading(false);
+ setInitializing(false); // 即使失败也标记为完成
+ }
+ };
+
+ loadGroupStats();
+ }, [currentUser?.id, labels, currentCustomer?.id]);
+
+ // 当分组展开时,加载该分组的第一页数据
+ const handleGroupExpand = useCallback(
+ async (groupKey: string) => {
+ const groupIndex = parseInt(groupKey);
+ const label = contactGroups[groupIndex];
+
+ if (!label || !currentUser?.id) return;
+
+ // 如果已经加载过数据,不重复加载
+ if (groupData.contacts[groupKey]?.length > 0) return;
+
+ // 设置加载状态
+ setGroupData(prev => ({
+ ...prev,
+ loading: { ...prev.loading, [groupKey]: true },
+ }));
+
+ try {
+ // 根据当前标签的 groupType 计算正确的 realGroupIds
+ const realGroupIds = labels
+ .filter(
+ item =>
+ item.id !== 0 &&
+ Number(item.groupType) === Number(label.groupType),
+ )
+ .map(item => item.id);
+
+ const contacts = await getContactsByGroup(
+ currentUser.id,
+ label,
+ realGroupIds,
+ currentCustomer?.id,
+ 1,
+ 20,
+ );
+
+ // 更新分组数据
+ setGroupData(prev => ({
+ contacts: { ...prev.contacts, [groupKey]: contacts },
+ pages: { ...prev.pages, [groupKey]: 1 },
+ loading: { ...prev.loading, [groupKey]: false },
+ hasMore: { ...prev.hasMore, [groupKey]: contacts.length === 20 },
+ }));
+ } catch (error) {
+ console.error("加载分组数据失败:", error);
+ setGroupData(prev => ({
+ ...prev,
+ loading: { ...prev.loading, [groupKey]: false },
+ }));
+ }
+ },
+ [
+ contactGroups,
+ labels,
+ currentUser?.id,
+ currentCustomer?.id,
+ groupData.contacts,
+ ],
+ );
+
+ // 加载更多联系人
+ const handleLoadMore = useCallback(
+ async (groupKey: string) => {
+ if (groupData.loading[groupKey] || !groupData.hasMore[groupKey]) return;
+
+ const groupIndex = parseInt(groupKey);
+ const label = contactGroups[groupIndex];
+
+ if (!label || !currentUser?.id) return;
+
+ // 设置加载状态
+ setGroupData(prev => ({
+ ...prev,
+ loading: { ...prev.loading, [groupKey]: true },
+ }));
+
+ try {
+ const currentPage = groupData.pages[groupKey] || 1;
+ const nextPage = currentPage + 1;
+
+ // 根据当前标签的 groupType 计算正确的 realGroupIds
+ const realGroupIds = labels
+ .filter(
+ item =>
+ item.id !== 0 &&
+ Number(item.groupType) === Number(label.groupType),
+ )
+ .map(item => item.id);
+
+ const newContacts = await getContactsByGroup(
+ currentUser.id,
+ label,
+ realGroupIds,
+ currentCustomer?.id,
+ nextPage,
+ 20,
+ );
+
+ // 更新分组数据
+ setGroupData(prev => ({
+ contacts: {
+ ...prev.contacts,
+ [groupKey]: [...(prev.contacts[groupKey] || []), ...newContacts],
+ },
+ pages: { ...prev.pages, [groupKey]: nextPage },
+ loading: { ...prev.loading, [groupKey]: false },
+ hasMore: { ...prev.hasMore, [groupKey]: newContacts.length === 20 },
+ }));
+ } catch (error) {
+ console.error("加载更多联系人失败:", error);
+ setGroupData(prev => ({
+ ...prev,
+ loading: { ...prev.loading, [groupKey]: false },
+ }));
+ }
+ },
+ [contactGroups, labels, currentUser?.id, currentCustomer?.id, groupData],
+ );
+
+ // 联系人点击处理
+ const onContactClick = async (contact: Contact) => {
+ if (!currentUser?.id) return;
+
+ try {
+ // 1. 查询数据库是否存在该联系人的会话
+ const existingSession = await MessageManager.getSessionByContactId(
+ currentUser.id,
+ contact.id,
+ contact.type,
+ );
+
+ const currentTime = dayjs().format(); // 当前时间
+
+ if (!existingSession) {
+ // === 场景1:会话不存在,创建新会话并插入数据库 ===
+
+ const newSession: ChatSession = {
+ serverId: `${contact.type}_${contact.id}`,
+ userId: currentUser.id,
+ id: contact.id,
+ type: contact.type,
+ wechatAccountId: contact.wechatAccountId,
+ nickname: contact.nickname,
+ conRemark: contact.conRemark || "",
+ avatar: contact.avatar,
+ content: "",
+ lastUpdateTime: currentTime, // 使用当前时间
+ config: {
+ unreadCount: 0,
+ top: contact.config?.top === true ? 1 : 0, // boolean → number
+ },
+ sortKey: "",
+ wechatId: contact.wechatId,
+ };
+
+ // 插入数据库(等待完成)
+ await MessageManager.createSession(currentUser.id, newSession);
+ console.log(`创建新会话: ${contact.nickname || contact.wechatId}`);
+ } else {
+ // === 场景2:会话已存在,更新 lastUpdateTime ===
+
+ await MessageManager.updateSessionTime(
+ currentUser.id,
+ contact.id,
+ contact.type,
+ currentTime, // 更新为当前时间
+ );
+ console.log(
+ `更新会话时间: ${contact.nickname || contact.wechatId} -> ${currentTime}`,
+ );
+ }
+
+ // 3. 数据库操作完成后,触发 UI 更新
+ setCurrentContact(contact);
+ } catch (error) {
+ console.error("处理联系人点击失败:", error);
+ }
};
-
// 渲染联系人项
- 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 +350,7 @@ const ContactListSimple: React.FC = ({
{contact.nickname.charAt(0)}}
+ icon={!avatar && {contact.nickname?.charAt(0) || ""}}
className={styles.avatar}
/>
@@ -95,91 +362,56 @@ const ContactListSimple: React.FC = ({
);
};
- // 初始化分页数据
- 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 = () => (
+
+ {Array(10)
+ .fill(null)
+ .map((_, index) => (
+
+ ))}
+
);
- // 渲染加载更多按钮
+ // 监听分组展开/折叠事件
+ const handleCollapseChange = (keys: string | string[]) => {
+ const newKeys = Array.isArray(keys) ? keys : [keys];
+ const expandedKeys = newKeys.filter(key => !activeKey.includes(key));
+
+ // 加载新展开的分组数据
+ expandedKeys.forEach(key => {
+ handleGroupExpand(key);
+ });
+
+ setActiveKey(newKeys);
+ };
+
+ // 渲染加载更多文字
const renderLoadMoreButton = (groupKey: string) => {
- if (!hasMore[groupKey])
+ if (!groupData.hasMore[groupKey]) {
return 没有更多了
;
+ }
return (
-
-
+
!groupData.loading[groupKey] && handleLoadMore(groupKey)}
+ >
+ {groupData.loading[groupKey] ? "加载中..." : "加载更多"}
);
};
- // 构建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);
@@ -188,18 +420,41 @@ const ContactListSimple: React.FC
= ({
label: (
{group.groupName}
- {group.contacts.length}
+ {group.count || 0}
),
className: styles.groupPanel,
children: isActive ? (
<>
-
- {renderLoadMoreButton(groupKey)}
+ {groupData.loading[groupKey] && !groupData.contacts[groupKey] ? (
+ // 首次加载显示骨架屏
+
+ {Array(3)
+ .fill(null)
+ .map((_, i) => (
+
+ ))}
+
+ ) : (
+ <>
+
+ {(groupData.contacts[groupKey]?.length || 0) > 0 &&
+ renderLoadMoreButton(groupKey)}
+ >
+ )}
>
) : null,
};
@@ -208,7 +463,10 @@ const ContactListSimple: React.FC = ({
return (
- {searchKeyword.trim() ? (
+ {loading || initializing ? (
+ // 加载状态:显示骨架屏(初始化或首次无本地数据时显示)
+ renderSkeleton()
+ ) : isSearchMode ? (
// 搜索模式:直接显示搜索结果列表
<>
搜索结果
@@ -222,13 +480,18 @@ const ContactListSimple: React.FC
= ({
)}
>
) : (
- // 正常模式:显示分组
- setActiveKey(keys as string[])}
- items={getCollapseItems()}
- />
+ // 正常模式:显示分组列表
+ <>
+
+ {contactGroups.length === 0 && (
+ 暂无联系人
+ )}
+ >
)}
);
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx
index 37516986..f596b9c9 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx
@@ -1,22 +1,64 @@
-import React, { useEffect, useState } from "react";
+import React, { useState, useEffect } from "react";
import { Input, Skeleton } from "antd";
-import { SearchOutlined, ChromeOutlined } from "@ant-design/icons";
+import { SearchOutlined } from "@ant-design/icons";
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";
+import { useWeChatStore } from "@/store/module/weChat/weChat";
+import { useUserStore } from "@/store/module/user";
+import { MessageManager } from "@/utils/dbAction/message";
interface SidebarMenuProps {
loading?: boolean;
}
const SidebarMenu: React.FC = ({ 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,
+ currentContact,
+ } = useContactStore();
+ const currentCustomer = useCustomerStore(state => state.currentCustomer);
+ const { setCurrentContact } = useWeChatStore();
+ const { user } = useUserStore();
+ const currentUserId = user?.id || 0;
const [activeTab, setActiveTab] = useState("chats");
+ const [switchingTab, setSwitchingTab] = useState(false); // tab切换加载状态
+
+ // 监听 currentContact 变化,自动切换到聊天tab并选中会话
+ useEffect(() => {
+ if (!currentContact || !currentUserId) return;
+
+ const handleContactSelection = async () => {
+ try {
+ setSwitchingTab(true);
+
+ // 2. 从数据库中查找该联系人对应的会话
+ const session = await MessageManager.getSessionByContactId(
+ currentUserId,
+ currentContact.id,
+ currentContact.type,
+ );
+
+ if (session) {
+ // 3. 直接选中该会话
+ setCurrentContact(session as any, true);
+ }
+
+ // 4. 关闭加载状态
+ setSwitchingTab(false);
+ } catch (error) {
+ console.error("处理联系人选中失败:", error);
+ setSwitchingTab(false);
+ }
+ };
+
+ handleContactSelection();
+ }, [currentContact, currentUserId, setCurrentContact]);
const handleSearch = (value: string) => {
setSearchKeyword(value);
@@ -26,10 +68,6 @@ const SidebarMenu: React.FC = ({ loading = false }) => {
clearSearchKeyword();
};
- useEffect(() => {
- setActiveTab("chats");
- }, [kfSelected]);
-
// 渲染骨架屏
const renderSkeleton = () => (
@@ -104,7 +142,7 @@ const SidebarMenu: React.FC = ({ loading = false }) => {
>
联系人
- {kfSelected != 0 && (
+ {currentCustomer && currentCustomer.id !== 0 && (
setActiveTab("friendsCicle")}
@@ -118,6 +156,11 @@ const SidebarMenu: React.FC
= ({ loading = false }) => {
// 渲染内容部分
const renderContent = () => {
+ // 如果正在切换tab到聊天,显示骨架屏
+ if (switchingTab && activeTab === "chats") {
+ return renderSkeleton();
+ }
+
switch (activeTab) {
case "chats":
return ;
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/VerticalUserList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/VerticalUserList/index.tsx
deleted file mode 100644
index e75e7d08..00000000
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/VerticalUserList/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from "react";
-import { Avatar, Badge } from "antd";
-import styles from "./VerticalUserList.module.scss";
-import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat/ckchat";
-
-const VerticalUserList: React.FC = () => {
- const handleUserSelect = (userId: number) => {
- asyncKfSelected(userId);
- };
- const kfUserList = useCkChatStore(state => state.kfUserList);
- const kfSelected = useCkChatStore(state => state.kfSelected);
- const chatSessions = useCkChatStore(state => state.chatSessions);
- const getUnreadCount = (wechatAccountId: number) => {
- if (wechatAccountId != 0) {
- const session = chatSessions.filter(
- v => v.wechatAccountId === wechatAccountId,
- );
- return session.reduce((pre, cur) => pre + cur.config.unreadCount, 0);
- } else {
- return chatSessions.reduce((pre, cur) => pre + cur.config.unreadCount, 0);
- }
- };
-
- return (
-
-
-
-
handleUserSelect(0)}>
-
- 全部
-
-
-
- {kfUserList.map(user => (
-
handleUserSelect(user.id)}
- >
-
-
- {!user.avatar && user.name.charAt(0)}
-
-
-
-
- ))}
-
-
- );
-};
-
-export default VerticalUserList;
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/data.ts b/Touchkebao/src/pages/pc/ckbox/weChat/data.ts
index 4fb95faa..52605792 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/data.ts
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/data.ts
@@ -113,11 +113,11 @@ export interface weChatGroup {
chatroomAvatar: string;
groupId: number;
config?: {
+ top?: false;
chat?: boolean;
unreadCount: number;
};
labels?: string[];
-
notice: string;
selfDisplyName: string;
wechatChatroomId: number;
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx
index 88c3e84a..0bb8510c 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/index.tsx
@@ -1,48 +1,49 @@
-import React, { useState, useEffect } from "react";
+import React, { useEffect } from "react";
import { Layout } from "antd";
import { MessageOutlined } from "@ant-design/icons";
import ChatWindow from "./components/ChatWindow/index";
import SidebarMenu from "./components/SidebarMenu/index";
-import VerticalUserList from "./components/VerticalUserList";
+import CustomerList from "./components/CustomerList";
import PageSkeleton from "./components/Skeleton";
import styles from "./index.module.scss";
-const { Content, Sider } = Layout;
-import { chatInitAPIdata, initSocket } from "./main";
+import { useWebSocketStore } from "@/store/module/websocket/websocket";
+import { useUserStore } from "@/store/module/user";
+import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { useWeChatStore } from "@/store/module/weChat/weChat";
+const { Content, Sider } = Layout;
+
const CkboxPage: React.FC = () => {
- // 不要在组件初始化时获取sendCommand,而是在需要时动态获取
- const [loading, setLoading] = useState(false);
const currentContract = useWeChatStore(state => state.currentContract);
+
+ // 初始化 WebSocket 连接
useEffect(() => {
- // 方法一:使用 Promise 链式调用处理异步函数
- setLoading(true);
- chatInitAPIdata()
- .then(() => {
- // 数据加载完成后初始化WebSocket连接
- initSocket();
- })
- .catch(error => {
- console.error("获取联系人列表失败:", error);
- })
- .finally(() => {
- setLoading(false);
- });
+ const { token2 } = useUserStore.getState();
+ const { getAccountId } = useCkChatStore.getState();
+ const { connect } = useWebSocketStore.getState();
+
+ connect({
+ accessToken: token2,
+ accountId: Number(getAccountId()),
+ client: "kefu-client",
+ cmdType: "CmdSignIn",
+ seq: +new Date(),
+ });
}, []);
return (
-
+
{/* 垂直侧边栏 */}
-
+
{/* 左侧联系人边栏 */}
-
+
{/* 主内容区 */}
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/main.ts b/Touchkebao/src/pages/pc/ckbox/weChat/main.ts
deleted file mode 100644
index 93bee0a9..00000000
--- a/Touchkebao/src/pages/pc/ckbox/weChat/main.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-import {
- asyncKfUserList,
- asyncContractList,
- asyncChatSessions,
- asyncWeChatGroup,
- asyncCountLables,
- useCkChatStore,
- updateIsLoadWeChat,
- getIsLoadWeChat,
-} from "@/store/module/ckchat/ckchat";
-import { useWebSocketStore } from "@/store/module/websocket/websocket";
-import { useUserStore } from "@/store/module/user";
-import { weChatGroupService, contractService } from "@/utils/db";
-
-import {
- loginWithToken,
- getControlTerminalList,
- getContactList,
- getGroupList,
- getAgentList,
- getLabelsListByGroup,
-} from "./api";
-
-import {
- KfUserListData,
- ContractData,
- weChatGroup,
-} from "@/pages/pc/ckbox/data";
-
-const { login2 } = useUserStore.getState();
-//获取触客宝基础信息
-export const chatInitAPIdata = async () => {
- try {
- let contractList = [];
- let groupList = [];
-
- if (getIsLoadWeChat()) {
- //获取联系人列表
- contractList = await contractService.findAll();
- //获取群列表
- groupList = await weChatGroupService.findAll();
- } else {
- //获取联系人列表
- contractList = await getAllContactList();
-
- //获取群列表
- groupList = await getAllGroupList();
-
- updateIsLoadWeChat(true);
- }
- //获取联系人列表
- await asyncContractList(contractList);
-
- await asyncWeChatGroup(groupList);
-
- //获取控制终端列表
- const kfUserList: KfUserListData[] = await getAgentList();
-
- //获取用户列表
- await asyncKfUserList(kfUserList);
-
- //获取标签列表
- const countLables = await getCountLables();
- await asyncCountLables(countLables);
-
- //获取消息会话列表并按lastUpdateTime排序
- const filterUserSessions = contractList?.filter(
- v => v?.config && v.config?.chat,
- );
- const filterGroupSessions = groupList?.filter(
- v => v?.config && v.config?.chat,
- );
- //排序功能
- const sortedSessions = [...filterUserSessions, ...filterGroupSessions].sort(
- (a, b) => {
- // 获取未读消息数量
- const aUnread = a.config?.unreadCount || 0;
- const bUnread = b.config?.unreadCount || 0;
-
- // 首先按未读消息数量降序排列(未读消息多的排在前面)
- if (aUnread !== bUnread) {
- return bUnread - aUnread;
- }
-
- // 如果未读消息数量相同,则按时间降序排列(最新的在前面)
- // 如果lastUpdateTime不存在,则将其排在最后
- if (!a.lastUpdateTime) return 1;
- if (!b.lastUpdateTime) return -1;
-
- const timeCompare =
- new Date(b.lastUpdateTime).getTime() -
- new Date(a.lastUpdateTime).getTime();
-
- return timeCompare;
- },
- );
- //会话数据同步
- asyncChatSessions(sortedSessions);
-
- return {
- contractList,
- groupList,
- kfUserList,
- };
- } catch (error) {
- console.error("获取联系人列表失败:", error);
- return [];
- }
-};
-//发起soket连接
-export const initSocket = () => {
- // 从store获取token和accountId
- const { token2 } = useUserStore.getState();
- const { getAccountId } = useCkChatStore.getState();
- const Token = token2;
- const accountId = getAccountId();
- // 使用WebSocket store初始化连接
- const { connect } = useWebSocketStore.getState();
-
- // 连接WebSocket
- connect({
- accessToken: Token,
- accountId: Number(accountId),
- client: "kefu-client",
- cmdType: "CmdSignIn",
- seq: +new Date(),
- });
-};
-
-export const getCountLables = async () => {
- const Result = await getLabelsListByGroup({});
- const LablesRes = Result.list;
- return [
- ...[
- {
- id: 0,
- groupName: "默认群分组",
- groupType: 2,
- },
- ],
- ...LablesRes,
- ...[
- {
- id: 0,
- groupName: "未分组",
- groupType: 1,
- },
- ],
- ];
-};
-/**
- * 根据标签组织联系人
- * @param contractList 联系人列表
- * @param countLables 标签列表
- * @returns 按标签分组的联系人
- */
-
-//获取控制终端列表
-export const getControlTerminalListByWechatAccountIds = (
- WechatAccountIds: number[],
-) => {
- return Promise.all(
- WechatAccountIds.map(id => getControlTerminalList({ id: id })),
- );
-};
-// 递归获取所有联系人列表
-export const getAllContactList = async () => {
- try {
- let allContacts = [];
- let page = 1;
- const limit = 1000;
- let hasMore = true;
-
- while (hasMore) {
- const Result = await getContactList({
- page,
- limit,
- });
- const contractList = Result.list;
- if (
- !contractList ||
- !Array.isArray(contractList) ||
- contractList.length === 0
- ) {
- hasMore = false;
- break;
- }
-
- allContacts = [...allContacts, ...contractList];
-
- // 如果返回的数据少于请求的数量,说明已经没有更多数据了
- if (contractList.length == 0) {
- hasMore = false;
- } else {
- page = page + 1;
- }
- }
- return allContacts;
- } catch (error) {
- console.error("获取所有联系人列表失败:", error);
- return [];
- }
-};
-
-// 提取不重复的wechatAccountId组
-export const getUniqueWechatAccountIds = (
- contacts: ContractData[],
- groupList: weChatGroup[],
-) => {
- if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
- return [];
- }
-
- // 使用Set来存储不重复的wechatAccountId
- const uniqueAccountIdsSet = new Set();
-
- // 遍历联系人列表,将每个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 {
- let allContacts = [];
- let page = 1;
- const limit = 1000;
- let hasMore = true;
-
- while (hasMore) {
- const Result = await getGroupList({
- page,
- limit,
- });
- const contractList = Result.list;
- if (
- !contractList ||
- !Array.isArray(contractList) ||
- contractList.length === 0
- ) {
- hasMore = false;
- break;
- }
-
- allContacts = [...allContacts, ...contractList];
-
- // 如果返回的数据少于请求的数量,说明已经没有更多数据了
- if (contractList.length < limit) {
- hasMore = false;
- } else {
- // 获取最后一条数据的id作为下一次请求的page
- const lastContact = contractList[contractList.length - 1];
- page = lastContact.id;
- }
- }
-
- return allContacts;
- } catch (error) {
- console.error("获取所有群列表失败:", error);
- return [];
- }
-};
-
-//获取token
-const getToken = () => {
- return new Promise((resolve, reject) => {
- const params = {
- grant_type: "password",
- password: "kr123456",
- username: "kr_xf3",
- // username: "karuo",
- // password: "zhiqun1984",
- };
- loginWithToken(params)
- .then(res => {
- login2(res.access_token);
- resolve(res.access_token);
- })
- .catch(err => {
- reject(err);
- });
- });
-};
diff --git a/Touchkebao/src/store/module/ckchat/api.ts b/Touchkebao/src/store/module/ckchat/api.ts
index b921e605..c98a02fa 100644
--- a/Touchkebao/src/store/module/ckchat/api.ts
+++ b/Touchkebao/src/store/module/ckchat/api.ts
@@ -1,5 +1,5 @@
//构建联系人列表标签
-import { weChatGroupService, contractService } from "@/utils/db";
+import { contactUnifiedService } from "@/utils/db";
import { request } from "@/api/request2";
import { ContactGroupByLabel } from "@/pages/pc/ckbox/data";
@@ -11,7 +11,6 @@ export const createContractList = async (
kfSelected: number,
countLables: ContactGroupByLabel[],
) => {
- // 根据 groupType 决定查询不同的服务
const realGroup = countLables
.filter(item => item.id !== 0)
.map(item => item.id);
@@ -19,44 +18,27 @@ export const createContractList = async (
const dataByLabels = [];
for (const label of countLables) {
let data;
- if (label.groupType === 1) {
- if (label.id == 0) {
- data = await contractService.findWhereMultiple([
- {
- field: "groupId",
- operator: "notIn",
- value: realGroup,
- },
- ]);
- } else {
- data = await contractService.findWhere("groupId", label.id);
- }
- // 过滤出 kfSelected 对应的联系人
- if (kfSelected && kfSelected != 0) {
- data = data.filter(contact => contact.wechatAccountId === kfSelected);
- }
- // console.log(`标签 ${label.groupName} 对应的联系人数据:`, data);
- } else if (label.groupType === 2) {
- // groupType: 2, 查询 weChatGroupService
- if (label.id == 0) {
- data = await weChatGroupService.findWhereMultiple([
- {
- field: "groupId",
- operator: "notIn",
- value: realGroup,
- },
- ]);
- } else {
- data = await weChatGroupService.findWhere("groupId", label.id);
- // 过滤出 kfSelected 对应的联系人
- }
- if (kfSelected && kfSelected != 0) {
- data = data.filter(contact => contact.wechatAccountId === kfSelected);
- }
+
+ // 使用统一的联系人服务查询
+ if (label.id == 0) {
+ // 未分组:排除所有已分组的联系人
+ data = await contactUnifiedService.findWhereMultiple([
+ {
+ field: "groupId",
+ operator: "notIn",
+ value: realGroup,
+ },
+ ]);
} else {
- console.warn(`未知的 groupType: ${label.groupType}`);
- data = [];
+ // 指定分组:查询该分组的联系人
+ data = await contactUnifiedService.findWhere("groupId", label.id);
}
+
+ // 过滤出 kfSelected 对应的联系人
+ if (kfSelected && kfSelected != 0) {
+ data = data.filter(contact => contact.wechatAccountId === kfSelected);
+ }
+
dataByLabels.push({
...label,
contacts: data,
diff --git a/Touchkebao/src/store/module/ckchat/ckchat.data.ts b/Touchkebao/src/store/module/ckchat/ckchat.data.ts
index 9eeebffa..6eb2d225 100644
--- a/Touchkebao/src/store/module/ckchat/ckchat.data.ts
+++ b/Touchkebao/src/store/module/ckchat/ckchat.data.ts
@@ -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: () => 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;
diff --git a/Touchkebao/src/store/module/ckchat/ckchat.ts b/Touchkebao/src/store/module/ckchat/ckchat.ts
index 73e9c2ad..63924f3b 100644
--- a/Touchkebao/src/store/module/ckchat/ckchat.ts
+++ b/Touchkebao/src/store/module/ckchat/ckchat.ts
@@ -7,9 +7,10 @@ import {
KfUserListData,
ContactGroupByLabel,
} from "@/pages/pc/ckbox/data";
-import { weChatGroupService, contractService } from "@/utils/db";
+import { ContactManager } from "@/utils/dbAction";
import { createContractList } from "@/store/module/ckchat/api";
import { useWeChatStore } from "@/store/module/weChat/weChat";
+import { useUserStore } from "@/store/module/user";
// 从weChat store获取clearCurrentContact方法
const getClearCurrentContact = () =>
useWeChatStore.getState().clearCurrentContact;
@@ -17,19 +18,15 @@ export const useCkChatStore = createPersistStore(
set => ({
userInfo: null,
isLoggedIn: false,
- contractList: [], //联系人列表
chatSessions: [], //聊天会话
kfUserList: [], //客服列表
countLables: [], //标签列表
- newContractList: [], //联系人分组
kfSelected: 0, //选中的客服
- searchKeyword: "", //搜索关键词
isLoadWeChat: false, //是否加载微信
getIsLoadWeChat: () => {
return useCkChatStore.getState().isLoadWeChat;
},
updateIsLoadWeChat: (isLoadWeChat: boolean) => {
- console.log("updateIsLoadWeChat", isLoadWeChat);
set({ isLoadWeChat });
},
//客服列表
@@ -37,34 +34,17 @@ export const useCkChatStore = createPersistStore(
set({ kfUserList: data });
},
// 获取客服列表
- getkfUserList: async () => {
+ getkfUserList: () => {
const state = useCkChatStore.getState();
return state.kfUserList;
},
// 异步设置标签列表
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(
// 触发缓存重新计算
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,76 +68,29 @@ export const useCkChatStore = createPersistStore(
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);
+ const currentUserId = useUserStore.getState().user?.id || 0;
+ // 转换为统一的 Contact 格式并保存
+ await ContactManager.addContacts(
+ data.map(g => ({
+ serverId: `group_${g.id}`,
+ userId: currentUserId,
+ id: g.id,
+ type: "group" as const,
+ wechatAccountId: g.wechatAccountId,
+ nickname: g.nickname,
+ conRemark: g.conRemark,
+ avatar: g.chatroomAvatar || "",
+ lastUpdateTime: new Date().toISOString(),
+ sortKey: "",
+ searchKey: (g.conRemark || g.nickname || "").toLowerCase(),
+ chatroomId: g.chatroomId,
+ chatroomOwner: g.chatroomOwner,
+ selfDisplayName: g.selfDisplyName,
+ notice: g.notice,
+ })),
+ );
},
//获取选中的客服信息
getKfSelectedUser: () => {
@@ -330,12 +120,17 @@ export const useCkChatStore = createPersistStore(
clearkfUserList: () => {
set({ kfUserList: [] });
},
+ // 添加控制终端用户
+ addCtrlUser: (user: KfUserListData) => {
+ set(state => ({
+ kfUserList: [...state.kfUserList, user],
+ }));
+ },
// 获取聊天会话 - 使用缓存避免无限循环
getChatSessions: (() => {
let cachedResult: any = null;
let lastKfSelected: number | null = null;
let lastChatSessionsLength: number = 0;
- let lastSearchKeyword: string = "";
return () => {
const state = useCkChatStore.getState();
@@ -344,8 +139,7 @@ export const useCkChatStore = createPersistStore(
const shouldRecalculate =
cachedResult === null ||
lastKfSelected !== state.kfSelected ||
- lastChatSessionsLength !== state.chatSessions.length ||
- lastSearchKeyword !== state.searchKeyword;
+ lastChatSessionsLength !== state.chatSessions.length;
if (shouldRecalculate) {
let filteredSessions = state.chatSessions;
@@ -357,20 +151,9 @@ export const useCkChatStore = createPersistStore(
);
}
- // 根据搜索关键词筛选
- 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;
@@ -423,6 +206,88 @@ export const useCkChatStore = createPersistStore(
};
});
},
+ // 切换会话置顶状态
+ toggleChatSessionPin: (sessionId: number, isPinned: boolean) => {
+ set(state => {
+ const updatedSessions = state.chatSessions.map(item => {
+ if (item.id === sessionId) {
+ return {
+ ...item,
+ config: {
+ ...item.config,
+ top: isPinned,
+ },
+ };
+ }
+ return item;
+ });
+
+ // 如果置顶,将会话移到顶部
+ if (isPinned) {
+ const sessionIndex = updatedSessions.findIndex(
+ item => item.id === sessionId,
+ );
+ if (sessionIndex !== -1) {
+ const session = updatedSessions[sessionIndex];
+ const otherSessions = updatedSessions.filter(
+ item => item.id !== sessionId,
+ );
+ return {
+ chatSessions: [session, ...otherSessions],
+ };
+ }
+ } else {
+ // 如果取消置顶,重新排序(置顶>未读>名称>时间)
+ const sortedSessions = updatedSessions.sort((a, b) => {
+ // 获取置顶状态
+ const aTop = (a.config as any)?.top || false;
+ const bTop = (b.config as any)?.top || false;
+
+ // 首先按置顶状态排序(置顶的排在前面)
+ if (aTop !== bTop) {
+ return aTop ? -1 : 1;
+ }
+
+ // 如果都是置顶或都不是置顶,则按未读消息数量降序排列
+ const aUnread = a.config?.unreadCount || 0;
+ const bUnread = b.config?.unreadCount || 0;
+
+ if (aUnread !== bUnread) {
+ return bUnread - aUnread;
+ }
+
+ // 如果未读消息数量相同,则按显示名称排序
+ const getDisplayName = (session: any) => {
+ return session.conRemark || session.nickname || "";
+ };
+ const aName = getDisplayName(a).toLowerCase();
+ const bName = getDisplayName(b).toLowerCase();
+
+ if (aName !== bName) {
+ return aName.localeCompare(bName, "zh-CN");
+ }
+
+ // 如果名称也相同,则按时间降序排列
+ if (!a.lastUpdateTime) return 1;
+ if (!b.lastUpdateTime) return -1;
+
+ const timeCompare =
+ new Date(b.lastUpdateTime).getTime() -
+ new Date(a.lastUpdateTime).getTime();
+
+ return timeCompare;
+ });
+
+ return {
+ chatSessions: sortedSessions,
+ };
+ }
+
+ return {
+ chatSessions: updatedSessions,
+ };
+ });
+ },
// 设置用户信息
setUserInfo: (userInfo: CkUserInfo) => {
set({ userInfo, isLoggedIn: true });
@@ -492,8 +357,8 @@ export const useCkChatStore = createPersistStore(
isLoggedIn: state.isLoggedIn,
kfUserList: state.kfUserList,
}),
- onRehydrateStorage: () => state => {
- // console.log("CkChat store hydrated:", state);
+ onRehydrateStorage: () => () => {
+ // console.log("CkChat store hydrated");
},
},
);
@@ -510,7 +375,7 @@ export const addChatSession = (session: ContractData | weChatGroup) =>
useCkChatStore.getState().addChatSession(session);
export const updateChatSession = (session: ContractData | weChatGroup) =>
useCkChatStore.getState().updateChatSession(session);
-export const deleteChatSession = (sessionId: string) =>
+export const deleteChatSession = (sessionId: number) =>
useCkChatStore.getState().deleteChatSession(sessionId);
export const getkfUserList = () => useCkChatStore.getState().kfUserList;
export const addCtrlUser = (user: KfUserListData) =>
@@ -533,21 +398,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();
@@ -555,3 +408,5 @@ export const updateIsLoadWeChat = (isLoadWeChat: boolean) =>
useCkChatStore.getState().updateIsLoadWeChat(isLoadWeChat);
export const getIsLoadWeChat = () =>
useCkChatStore.getState().getIsLoadWeChat();
+export const toggleChatSessionPin = (sessionId: number, isPinned: boolean) =>
+ useCkChatStore.getState().toggleChatSessionPin(sessionId, isPinned);
diff --git a/Touchkebao/src/store/module/dataCenter/index.data.ts b/Touchkebao/src/store/module/dataCenter/index.data.ts
new file mode 100644
index 00000000..29a1f2d6
--- /dev/null
+++ b/Touchkebao/src/store/module/dataCenter/index.data.ts
@@ -0,0 +1,113 @@
+import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
+import {
+ CommentItem,
+ likeListItem,
+ FriendsCircleItem,
+} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
+
+/**
+ * 微信聊天状态管理接口定义
+ * 包含聊天消息、联系人管理、朋友圈等功能的状态和方法
+ */
+export interface WeChatState {
+ showChatRecordModel: boolean;
+ updateShowChatRecordModel: (show: boolean) => void;
+ aiQuoteMessageContent: number;
+ updateAiQuoteMessageContent: (message: number) => void;
+ quoteMessageContent: string;
+ updateQuoteMessageContent: (value: string) => void;
+ // ==================== Transmit Module =========Start===========
+ /** 选中的聊天记录列表 */
+ selectedChatRecords: ChatRecord[];
+ /** 更新选中的聊天记录 */
+ updateSelectedChatRecords: (message: ChatRecord[]) => void;
+
+ /** 选中的联系人或群组列表 */
+ selectedTransmitContact: ContractData[] | weChatGroup[];
+ /** 更新选中的联系人或群组 */
+ updateSelectedTransmitContact: (
+ contact: ContractData[] | weChatGroup[],
+ ) => void;
+
+ /** 转发弹窗开启状态 */
+ openTransmitModal: boolean;
+ /** 更新转发弹窗状态 */
+ updateTransmitModal: (open: boolean) => void;
+ // ==================== Transmit Module =========END===========
+
+ // ==================== 当前联系人管理 ====================
+ /** 当前选中的联系人/群组 */
+ currentContract: ContractData | weChatGroup | null;
+ /** 清空当前联系人 */
+ clearCurrentContact: () => void;
+ /** 设置当前联系人 */
+ setCurrentContact: (
+ contract: ContractData | weChatGroup,
+ isExist?: boolean,
+ ) => void;
+
+ // ==================== 聊天消息管理 ====================
+ /** 当前聊天的消息列表 */
+ currentMessages: ChatRecord[];
+ /** 添加新消息 */
+ addMessage: (message: ChatRecord) => void;
+ /** 更新指定消息 */
+ updateMessage: (messageId: number, updates: Partial) => void;
+ /** 撤回指定消息 */
+ recallMessage: (messageId: number) => void;
+ /** 消息加载状态 */
+ messagesLoading: boolean;
+ /** 数据初始化加载状态 */
+ isLoadingData: boolean;
+ /** 当前群组成员列表 */
+ currentGroupMembers: any[];
+
+ // ==================== 界面状态管理 ====================
+ /** 是否显示复选框 */
+ showCheckbox: boolean;
+ /** 更新复选框显示状态 */
+ updateShowCheckbox: (show: boolean) => void;
+ /** 进入模块类型 (common | multipleForwarding) */
+ EnterModule: string;
+ /** 更新进入模块类型 */
+ updateEnterModule: (module: string) => void;
+
+ // ==================== 朋友圈相关 ====================
+ /** 朋友圈数据列表 */
+ MomentCommon: FriendsCircleItem[];
+ /** 朋友圈数据加载状态 */
+ MomentCommonLoading: boolean;
+ /** 清空朋友圈数据 */
+ clearMomentCommon: () => void;
+ /** 添加朋友圈数据 */
+ addMomentCommon: (moment: FriendsCircleItem[]) => void;
+ /** 更新朋友圈数据 */
+ updateMomentCommon: (moments: FriendsCircleItem[]) => void;
+ /** 更新朋友圈加载状态 */
+ updateMomentCommonLoading: (loading: boolean) => void;
+ /** 更新朋友圈点赞 */
+ updateLikeMoment: (snsId: string, likeList: likeListItem[]) => void;
+ /** 更新朋友圈评论 */
+ updateComment: (snsId: string, commentList: CommentItem[]) => void;
+
+ // ==================== 消息加载方法 ====================
+ /** 加载聊天消息 */
+ loadChatMessages: (Init: boolean, To?: number) => Promise;
+ /** 搜索消息 */
+ SearchMessage: (params: {
+ From: number;
+ To: number;
+ keyword: string;
+ Count?: number;
+ }) => Promise;
+
+ // ==================== 视频消息处理 ====================
+ /** 设置视频消息加载状态 */
+ setVideoLoading: (messageId: number, isLoading: boolean) => void;
+ /** 设置视频消息URL */
+ setVideoUrl: (messageId: number, videoUrl: string) => void;
+
+ // ==================== 消息接收处理 ====================
+ /** 接收新消息处理 */
+ receivedMsg: (message: ChatRecord) => void;
+}
diff --git a/Touchkebao/src/store/module/dataCenter/index.ts b/Touchkebao/src/store/module/dataCenter/index.ts
new file mode 100644
index 00000000..fe83e150
--- /dev/null
+++ b/Touchkebao/src/store/module/dataCenter/index.ts
@@ -0,0 +1,480 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import {
+ getChatMessages,
+ getChatroomMessages,
+ getGroupMembers,
+} from "@/pages/pc/ckbox/api";
+import { WeChatState } from "./index.data";
+
+import {
+ likeListItem,
+ CommentItem,
+} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
+import {
+ clearUnreadCount1,
+ clearUnreadCount2,
+ updateConfig,
+ getFriendInjectConfig,
+} from "@/pages/pc/ckbox/api";
+import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
+import { ContactManager } from "@/utils/dbAction";
+import { useUserStore } from "@/store/module/user";
+import {
+ addChatSession,
+ updateChatSession,
+ useCkChatStore,
+ pinChatSessionToTop,
+} from "@/store/module/ckchat/ckchat";
+
+/**
+ * 微信聊天状态管理 Store
+ * 使用 Zustand 管理微信聊天相关的状态和操作
+ */
+export const useWeChatStore = create()(
+ persist(
+ (set, get) => ({
+ showChatRecordModel: false,
+ updateShowChatRecordModel: (show: boolean) => {
+ set({ showChatRecordModel: show });
+ },
+ //当前用户的ai接管状态
+ aiQuoteMessageContent: 0,
+ updateAiQuoteMessageContent: (message: number) => {
+ set({ aiQuoteMessageContent: message });
+ },
+ quoteMessageContent: "",
+ updateQuoteMessageContent: (message: string) => {
+ set({ quoteMessageContent: message });
+ },
+ // ==================== Transmit Module =========Start===========
+ /** 选中的聊天记录列表 */
+ selectedChatRecords: [],
+ /** 更新选中的聊天记录 */
+ updateSelectedChatRecords: (message: ChatRecord[]) => {
+ set({ selectedChatRecords: message });
+ },
+
+ /** 选中的联系人或群组列表 */
+ selectedTransmitContact: [],
+ /** 更新选中的联系人或群组 */
+ updateSelectedTransmitContact: (
+ contact: ContractData[] | weChatGroup[],
+ ) => {
+ set({ selectedTransmitContact: contact });
+ },
+
+ /** 转发弹窗开启状态 */
+ openTransmitModal: false,
+ /** 更新转发弹窗状态 */
+ updateTransmitModal: (open: boolean) => {
+ set({ openTransmitModal: open });
+ },
+ // ==================== Transmit Module =========END===========
+
+ // ==================== 当前联系人管理状态 ====================
+ /** 当前选中的联系人/群组 */
+ currentContract: null,
+ /** 当前聊天的消息列表 */
+ currentMessages: [],
+
+ // ==================== 聊天消息管理方法 ====================
+ /** 添加新消息到当前聊天 */
+ addMessage: message => {
+ const { wechatChatroomId, wechatFriendId } = message;
+ const currentContract = get().currentContract;
+ if (currentContract) {
+ if (
+ currentContract.id === wechatFriendId ||
+ currentContract.id === wechatChatroomId
+ ) {
+ set(state => ({
+ currentMessages: [...state.currentMessages, message],
+ }));
+ }
+ }
+ },
+ /** 更新指定消息内容 */
+ updateMessage: (messageId, updates) => {
+ set(state => ({
+ currentMessages: state.currentMessages.map(msg =>
+ msg.id === messageId ? { ...msg, ...updates } : msg,
+ ),
+ }));
+ },
+ /** 撤回指定消息 */
+ recallMessage: (messageId: number) => {
+ set(state => ({
+ currentMessages: state.currentMessages.filter(
+ msg => msg.id !== messageId,
+ ),
+ }));
+ },
+
+ // ==================== 加载状态管理 ====================
+ /** 消息加载状态 */
+ messagesLoading: false,
+ /** 数据初始化加载状态 */
+ isLoadingData: false,
+ /** 当前群组成员列表 */
+ currentGroupMembers: [],
+
+ // ==================== 界面状态管理 ====================
+ /** 是否显示复选框 */
+ showCheckbox: false,
+ /** 更新复选框显示状态 */
+ updateShowCheckbox: (show: boolean) => {
+ set({ showCheckbox: show });
+ },
+ /** 进入模块类型 (common | multipleForwarding) */
+ EnterModule: "common",
+ /** 更新进入模块类型 */
+ updateEnterModule: (module: string) => {
+ set({ EnterModule: module });
+ },
+
+ // ==================== 朋友圈相关状态 ====================
+ /** 朋友圈数据列表 */
+ MomentCommon: [],
+ /** 朋友圈数据加载状态 */
+ MomentCommonLoading: false,
+ /** 更新朋友圈加载状态 */
+ updateMomentCommonLoading: (loading: boolean) => {
+ set({ MomentCommonLoading: loading });
+ },
+
+ // ==================== 当前联系人管理方法 ====================
+ /** 清空当前联系人和消息 */
+ clearCurrentContact: () => {
+ set({ currentContract: null, currentMessages: [] });
+ },
+ /** 设置当前联系人并加载相关数据 */
+ setCurrentContact: (
+ contract: ContractData | weChatGroup,
+ isExist?: boolean,
+ ) => {
+ const state = useWeChatStore.getState();
+ // 切换联系人时清空当前消息,等待重新加载
+ set({ currentMessages: [], openTransmitModal: false });
+
+ const params: any = {};
+
+ if (!contract.chatroomId) {
+ params.wechatFriendId = contract.id;
+ } else {
+ params.wechatChatroomId = contract.id;
+ }
+ //重置动作
+ set({ showChatRecordModel: false });
+ clearUnreadCount1(params);
+ clearUnreadCount2([contract.id]);
+ getFriendInjectConfig({
+ friendId: contract.id,
+ wechatAccountId: contract.wechatAccountId,
+ }).then(result => {
+ set({ aiQuoteMessageContent: result });
+ });
+ if (isExist) {
+ updateChatSession({
+ ...contract,
+ config: { unreadCount: 0 },
+ });
+ } else {
+ addChatSession(contract);
+ }
+ set({ currentContract: contract });
+ updateConfig({
+ id: contract.id,
+ config: { chat: true },
+ });
+ state.loadChatMessages(true, 4704624000000);
+ },
+
+ // ==================== 消息加载方法 ====================
+ /** 加载聊天消息 */
+ loadChatMessages: async (Init: boolean, To?: number) => {
+ const state = useWeChatStore.getState();
+ const contact = state.currentContract;
+ set({ messagesLoading: true });
+ set({ isLoadingData: Init });
+ try {
+ const params: any = {
+ wechatAccountId: contact.wechatAccountId,
+ From: 1,
+ To: To || +new Date(),
+ Count: 20,
+ olderData: true,
+ };
+
+ if ("chatroomId" in contact && contact.chatroomId) {
+ // 群聊消息加载
+ params.wechatChatroomId = contact.id;
+ const messages = await getChatroomMessages(params);
+ const currentGroupMembers = await getGroupMembers({
+ id: contact.id,
+ });
+ if (Init) {
+ set({ currentMessages: messages || [], currentGroupMembers });
+ } else {
+ set({
+ currentMessages: [
+ ...(messages || []),
+ ...state.currentMessages,
+ ],
+ });
+ }
+ } else {
+ // 私聊消息加载
+ params.wechatFriendId = contact.id;
+ const messages = await getChatMessages(params);
+ if (Init) {
+ set({ currentMessages: messages || [] });
+ } else {
+ set({
+ currentMessages: [
+ ...(messages || []),
+ ...state.currentMessages,
+ ],
+ });
+ }
+ }
+ set({ messagesLoading: false });
+ } catch (error) {
+ console.error("获取聊天消息失败:", error);
+ } finally {
+ set({ messagesLoading: false });
+ }
+ },
+
+ /** 搜索消息 */
+ SearchMessage: async ({
+ From = 1,
+ To = 4704624000000,
+ keyword = "",
+ Count = 20,
+ }: {
+ From: number;
+ To: number;
+ keyword: string;
+ Count?: number;
+ }) => {
+ const state = useWeChatStore.getState();
+ const contact = state.currentContract;
+ set({ messagesLoading: true });
+
+ try {
+ const params: any = {
+ wechatAccountId: contact.wechatAccountId,
+ From,
+ To,
+ keyword,
+ Count,
+ olderData: true,
+ };
+
+ if ("chatroomId" in contact && contact.chatroomId) {
+ // 群聊消息搜索
+ params.wechatChatroomId = contact.id;
+ const messages = await getChatroomMessages(params);
+ const currentGroupMembers = await getGroupMembers({
+ id: contact.id,
+ });
+ set({ currentMessages: messages || [], currentGroupMembers });
+ } else {
+ // 私聊消息搜索
+ params.wechatFriendId = contact.id;
+ const messages = await getChatMessages(params);
+ set({ currentMessages: messages || [] });
+ }
+ set({ messagesLoading: false });
+ } catch (error) {
+ console.error("获取聊天消息失败:", error);
+ } finally {
+ set({ messagesLoading: false });
+ }
+ },
+
+ /** 设置消息加载状态 */
+ setMessageLoading: loading => {
+ set({ messagesLoading: Boolean(loading) });
+ },
+
+ // ==================== 消息接收处理 ====================
+ /** 接收新消息处理 */
+ receivedMsg: async message => {
+ const currentContract = useWeChatStore.getState().currentContract;
+ // 判断是群聊还是私聊
+ const getMessageId =
+ message?.wechatChatroomId || message.wechatFriendId;
+ const isWechatGroup = message?.wechatChatroomId;
+
+ // 如果是当前选中的聊天,直接添加到消息列表
+ if (currentContract && currentContract.id == getMessageId) {
+ set(state => ({
+ currentMessages: [...state.currentMessages, message],
+ }));
+ } else {
+ // 更新其他聊天的未读消息数
+ const chatSessions = useCkChatStore.getState().chatSessions;
+ const session = chatSessions.find(item => item.id == getMessageId);
+ if (session) {
+ session.config.unreadCount = Number(session.config.unreadCount) + 1;
+ updateChatSession(session);
+ // 将接收到新消息的会话置顶到列表顶部
+ pinChatSessionToTop(getMessageId);
+ } else {
+ // 如果会话不存在,创建新会话
+ const currentUserId = useUserStore.getState().user?.id || 0;
+ const contactType = isWechatGroup ? "group" : "friend";
+
+ // 从统一联系人表查询
+ const contacts =
+ await ContactManager.getUserContacts(currentUserId);
+ const contact = contacts.find(
+ c => c.id === getMessageId[0] && c.type === contactType,
+ );
+
+ if (contact) {
+ addChatSession({
+ ...contact,
+ config: { unreadCount: 1 },
+ } as any);
+ // 新创建的会话会自动添加到列表顶部,无需额外置顶
+ }
+ }
+ }
+ },
+
+ // ==================== 便捷选择器方法 ====================
+ /** 获取当前联系人 */
+ getCurrentContact: () => get().currentContract,
+ /** 获取当前消息列表 */
+ getCurrentMessages: () => get().currentMessages,
+ /** 获取消息加载状态 */
+ getMessagesLoading: () => get().messagesLoading,
+
+ // ==================== 视频消息处理方法 ====================
+ /** 设置视频消息加载状态 */
+ setVideoLoading: (messageId: number, isLoading: boolean) => {
+ set(state => ({
+ currentMessages: state.currentMessages.map(msg => {
+ if (msg.id === messageId) {
+ try {
+ const content = JSON.parse(msg.content);
+ // 更新加载状态
+ const updatedContent = { ...content, isLoading };
+ return {
+ ...msg,
+ content: JSON.stringify(updatedContent),
+ };
+ } catch (e) {
+ console.error("更新视频加载状态失败:", e);
+ return msg;
+ }
+ }
+ return msg;
+ }),
+ }));
+ },
+
+ /** 设置视频消息URL */
+ setVideoUrl: (messageId: number, videoUrl: string) => {
+ set(state => ({
+ currentMessages: state.currentMessages.map(msg => {
+ if (msg.id === messageId) {
+ try {
+ const content = JSON.parse(msg.content);
+ // 检查视频是否已经下载完毕,避免重复更新
+ if (content.videoUrl && content.videoUrl === videoUrl) {
+ console.log("视频已下载,跳过重复更新:", messageId);
+ return msg;
+ }
+
+ // 设置视频URL并清除加载状态
+ const updatedContent = {
+ ...content,
+ videoUrl,
+ isLoading: false,
+ };
+ return {
+ ...msg,
+ content: JSON.stringify(updatedContent),
+ };
+ } catch (e) {
+ console.error("更新视频URL失败:", e);
+ return msg;
+ }
+ }
+ return msg;
+ }),
+ }));
+ },
+
+ // ==================== 数据清理方法 ====================
+ /** 清空所有数据 */
+ clearAllData: () => {
+ set({
+ currentContract: null,
+ currentMessages: [],
+ messagesLoading: false,
+ });
+ },
+
+ // ==================== 朋友圈管理方法 ====================
+ /** 清空朋友圈数据 */
+ clearMomentCommon: () => {
+ set({ MomentCommon: [] });
+ },
+
+ /** 添加朋友圈数据 */
+ addMomentCommon: moment => {
+ set(state => ({
+ MomentCommon: [...state.MomentCommon, ...moment],
+ }));
+ },
+
+ /** 更新朋友圈数据 */
+ updateMomentCommon: moments => {
+ set({ MomentCommon: moments });
+ },
+
+ /** 更新朋友圈点赞 */
+ updateLikeMoment: (snsId: string, likeList: likeListItem[]) => {
+ set(state => ({
+ MomentCommon: state.MomentCommon.map(moment =>
+ moment.snsId === snsId ? { ...moment, likeList } : moment,
+ ),
+ }));
+ },
+
+ /** 更新朋友圈评论 */
+ updateComment: (snsId: string, commentList: CommentItem[]) => {
+ set(state => ({
+ MomentCommon: state.MomentCommon.map(moment =>
+ moment.snsId === snsId ? { ...moment, commentList } : moment,
+ ),
+ }));
+ },
+ }),
+ {
+ name: "wechat-storage",
+ partialize: state => ({
+ // currentContract 不做持久化,登录和页面刷新时直接清空
+ }),
+ },
+ ),
+);
+
+// ==================== 便捷选择器导出 ====================
+/** 获取当前联系人的 Hook */
+export const useCurrentContact = () =>
+ useWeChatStore(state => state.currentContract);
+/** 获取当前消息列表的 Hook */
+export const useCurrentMessages = () =>
+ useWeChatStore(state => state.currentMessages);
+/** 获取消息加载状态的 Hook */
+export const useMessagesLoading = () =>
+ useWeChatStore(state => state.messagesLoading);
+/** 获取复选框显示状态的 Hook */
+export const useShowCheckbox = () =>
+ useWeChatStore(state => state.showCheckbox);
diff --git a/Touchkebao/src/store/module/weChat/chatRecord.ts b/Touchkebao/src/store/module/weChat/chatRecord.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/Touchkebao/src/store/module/weChat/contacts.data.ts b/Touchkebao/src/store/module/weChat/contacts.data.ts
new file mode 100644
index 00000000..0ef039e1
--- /dev/null
+++ b/Touchkebao/src/store/module/weChat/contacts.data.ts
@@ -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;
+}
diff --git a/Touchkebao/src/store/module/weChat/contacts.ts b/Touchkebao/src/store/module/weChat/contacts.ts
new file mode 100644
index 00000000..ca875b97
--- /dev/null
+++ b/Touchkebao/src/store/module/weChat/contacts.ts
@@ -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;
+ /** 根据客服筛选联系人 */
+ filterByCustomer: (customerId: number) => Promise;
+ /** 清空筛选 */
+ clearFilter: () => void;
+
+ // ==================== 数据同步方法 ====================
+ /** 从服务器同步联系人数据 */
+ syncFromServer: (userId: number) => Promise;
+ /** 从本地数据库加载联系人数据 */
+ loadFromLocal: (userId: number) => Promise;
+ /** 刷新联系人数据 */
+ refreshContacts: (userId: number) => Promise;
+
+ // ==================== 联系人操作方法 ====================
+ /** 添加联系人 */
+ addContact: (contact: Contact) => Promise;
+ /** 更新联系人 */
+ updateContact: (contact: Contact) => Promise;
+ /** 删除联系人 */
+ deleteContact: (contactId: number) => Promise;
+ /** 批量添加联系人 */
+ addContacts: (contacts: Contact[]) => Promise;
+
+ // ==================== 分组管理方法 ====================
+ /** 获取联系人分组列表 */
+ getContactGroups: (
+ userId: number,
+ customerId?: number,
+ ) => Promise;
+ /** 创建联系人分组 */
+ createContactGroup: (group: Omit) => Promise;
+ /** 更新联系人分组 */
+ updateContactGroup: (group: ContactGroupByLabel) => Promise;
+ /** 删除联系人分组 */
+ deleteContactGroup: (groupId: number) => Promise;
+
+ // ==================== 分页方法 ====================
+ /** 加载更多联系人 */
+ loadMoreContacts: (groupId: string) => Promise;
+ /** 重置分页状态 */
+ 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()(
+ 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) => {
+ 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);
diff --git a/Touchkebao/src/store/module/weChat/currentChat.ts b/Touchkebao/src/store/module/weChat/currentChat.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/Touchkebao/src/store/module/weChat/customer.data.ts b/Touchkebao/src/store/module/weChat/customer.data.ts
new file mode 100644
index 00000000..50983be6
--- /dev/null
+++ b/Touchkebao/src/store/module/weChat/customer.data.ts
@@ -0,0 +1,41 @@
+export interface Customer {
+ id: number;
+ tenantId: number;
+ wechatId: string;
+ nickname: string;
+ alias: string;
+ avatar: string;
+ gender: number;
+ region: string;
+ signature: string;
+ bindQQ: string;
+ bindEmail: string;
+ bindMobile: string;
+ createTime: string;
+ currentDeviceId: number;
+ isDeleted: boolean;
+ deleteTime: string;
+ groupId: number;
+ memo: string;
+ wechatVersion: string;
+ labels: string[];
+ lastUpdateTime: string;
+ isOnline?: boolean;
+ momentsMax: number;
+ momentsNum: number;
+ [key: string]: any;
+}
+
+//Store State
+export interface CustomerState {
+ //客服列表
+ customerList: Customer[];
+ //当前选中的客服
+ currentCustomer: Customer | null;
+ //更新客服列表
+ updateCustomerList: (customerList: Customer[]) => void;
+ //更新客服状态
+ updateCustomerStatus: (customerId: number, status: string) => void;
+ //更新当前选中的客服
+ updateCurrentCustomer: (customer: Customer) => void;
+}
diff --git a/Touchkebao/src/store/module/weChat/customer.ts b/Touchkebao/src/store/module/weChat/customer.ts
new file mode 100644
index 00000000..9b85daf7
--- /dev/null
+++ b/Touchkebao/src/store/module/weChat/customer.ts
@@ -0,0 +1,56 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { Customer, CustomerState } from "./customer.data";
+export const useCustomerStore = create()(
+ persist(
+ (set, get) => ({
+ customerList: [], //客服列表
+ currentCustomer: null, //当前选中的客服
+ updateCustomerList: (customerList: Customer[]) => set({ customerList }), //更新客服列表
+ updateCurrentCustomer: (customer: Customer) =>
+ set({ currentCustomer: customer }), //更新当前选中的客服
+ updateCustomerStatus: (customerId: number, status: string) =>
+ set({
+ customerList: get().customerList.map(customer =>
+ customer.id === customerId ? { ...customer, status } : customer,
+ ),
+ }), //更新客服状态
+ }),
+ {
+ name: "customer-storage",
+ partialize: state => ({
+ customerList: [],
+ currentCustomer: null,
+ }),
+ },
+ ),
+);
+/**
+ * 更新当前选中的客服
+ * @param customer 客服
+ * @returns void
+ */
+export const updateCurrentCustomer = (customer: Customer) =>
+ useCustomerStore.getState().updateCurrentCustomer(customer);
+/**
+ * 更新客服列表
+ * @param customerList 客服列表
+ * @returns void
+ */
+export const updateCustomerList = (customerList: Customer[]) =>
+ useCustomerStore.getState().updateCustomerList(customerList);
+/**
+ * 获取当前选中的客服
+ * @returns Customer | null
+ */
+export const getCurrentCustomer = () =>
+ useCustomerStore.getState().currentCustomer;
+
+/**
+ * 更新客服状态
+ * @param customerId 客服ID
+ * @param status 状态
+ * @returns void
+ */
+export const updateCustomerStatus = (customerId: number, status: string) =>
+ useCustomerStore.getState().updateCustomerStatus(customerId, status);
diff --git a/Touchkebao/src/store/module/weChat/message.data.ts b/Touchkebao/src/store/module/weChat/message.data.ts
new file mode 100644
index 00000000..0925bbfd
--- /dev/null
+++ b/Touchkebao/src/store/module/weChat/message.data.ts
@@ -0,0 +1,63 @@
+export interface Message {
+ id: number;
+ wechatId: string;
+ nickname: string;
+ alias: string;
+ avatar: string;
+ region: string;
+ signature: string;
+ bindQQ: string;
+ bindEmail: string;
+ bindMobile: string;
+ createTime: string;
+ currentDeviceId: number;
+ isDeleted: boolean;
+ deleteTime: string;
+ groupId: number;
+ memo: string;
+ wechatVersion: string;
+ labels: string[];
+ lastUpdateTime: string;
+ isOnline?: boolean;
+ momentsMax: number;
+ momentsNum: number;
+ wechatAccountId: number;
+ [key: string]: any;
+}
+
+//Store State - 会话列表状态管理(不存储数据,只管理状态)
+export interface MessageState {
+ //加载状态
+ loading: boolean;
+ //后台同步状态
+ refreshing: boolean;
+ //刷新触发器(用于通知组件重新查询数据库)
+ refreshTrigger: number;
+ //最后刷新时间
+ lastRefreshTime: string | null;
+ //是否已经加载过一次(避免重复请求)
+ hasLoadedOnce: boolean;
+
+ //设置加载状态
+ setLoading: (loading: boolean) => void;
+ //设置同步状态
+ setRefreshing: (refreshing: boolean) => void;
+ //触发刷新(通知组件重新查询)
+ triggerRefresh: () => void;
+ //设置已加载标识
+ setHasLoadedOnce: (loaded: boolean) => void;
+ //重置加载状态(用于登出或切换用户)
+ resetLoadState: () => void;
+
+ // ==================== 保留原有接口(向后兼容) ====================
+ //消息列表(废弃,保留兼容)
+ messageList: Message[];
+ //当前选中的消息(废弃,保留兼容)
+ currentMessage: Message | null;
+ //更新消息列表(废弃,保留兼容)
+ updateMessageList: (messageList: Message[]) => void;
+ //更新消息状态(废弃,保留兼容)
+ updateMessageStatus: (messageId: number, status: string) => void;
+ //更新当前选中的消息(废弃,保留兼容)
+ updateCurrentMessage: (message: Message) => void;
+}
diff --git a/Touchkebao/src/store/module/weChat/message.ts b/Touchkebao/src/store/module/weChat/message.ts
new file mode 100644
index 00000000..51503f19
--- /dev/null
+++ b/Touchkebao/src/store/module/weChat/message.ts
@@ -0,0 +1,123 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+import { Message, MessageState } from "./message.data";
+
+/**
+ * 会话列表状态管理Store
+ * 职责:只管理状态,不存储会话列表数据
+ * 数据存储在:组件state + IndexedDB
+ */
+export const useMessageStore = create()(
+ persist(
+ (set, get) => ({
+ // ==================== 新增状态管理 ====================
+ loading: false,
+ refreshing: false,
+ refreshTrigger: 0,
+ lastRefreshTime: null,
+ hasLoadedOnce: false,
+
+ setLoading: (loading: boolean) => set({ loading }),
+ setRefreshing: (refreshing: boolean) => set({ refreshing }),
+ triggerRefresh: () =>
+ set({
+ refreshTrigger: get().refreshTrigger + 1,
+ lastRefreshTime: new Date().toISOString(),
+ }),
+ setHasLoadedOnce: (loaded: boolean) => set({ hasLoadedOnce: loaded }),
+ resetLoadState: () =>
+ set({
+ hasLoadedOnce: false,
+ loading: false,
+ refreshing: false,
+ refreshTrigger: 0,
+ }),
+
+ // ==================== 保留原有接口(向后兼容) ====================
+ messageList: [],
+ currentMessage: null,
+ updateMessageList: (messageList: Message[]) => set({ messageList }),
+ updateCurrentMessage: (message: Message) =>
+ set({ currentMessage: message }),
+ updateMessageStatus: (messageId: number, status: string) =>
+ set({
+ messageList: get().messageList.map(message =>
+ message.id === messageId ? { ...message, status } : message,
+ ),
+ }),
+ }),
+ {
+ name: "message-storage",
+ partialize: state => ({
+ // 只持久化必要的状态,不持久化数据
+ lastRefreshTime: state.lastRefreshTime,
+ hasLoadedOnce: state.hasLoadedOnce,
+ // 保留原有持久化字段(向后兼容)
+ messageList: [],
+ currentMessage: null,
+ }),
+ },
+ ),
+);
+/**
+ * 更新当前选中的消息
+ * @param message 消息
+ * @returns void
+ */
+export const updateCurrentMessage = (message: Message) =>
+ useMessageStore.getState().updateCurrentMessage(message);
+/**
+ * 更新消息列表
+ * @param messageList 消息列表
+ * @returns void
+ */
+export const updateMessageList = (messageList: Message[]) =>
+ useMessageStore.getState().updateMessageList(messageList);
+/**
+ * 获取当前选中的消息
+ * @returns Message | null
+ */
+export const getCurrentMessage = () =>
+ useMessageStore.getState().currentMessage;
+
+/**
+ * 更新消息状态
+ * @param messageId 消息ID
+ * @param status 状态
+ * @returns void
+ */
+export const updateMessageStatus = (messageId: number, status: string) =>
+ useMessageStore.getState().updateMessageStatus(messageId, status);
+
+// ==================== 新增导出函数 ====================
+
+/**
+ * 设置加载状态
+ * @param loading 加载状态
+ */
+export const setLoading = (loading: boolean) =>
+ useMessageStore.getState().setLoading(loading);
+
+/**
+ * 设置同步状态
+ * @param refreshing 同步状态
+ */
+export const setRefreshing = (refreshing: boolean) =>
+ useMessageStore.getState().setRefreshing(refreshing);
+
+/**
+ * 触发刷新(通知组件重新查询数据库)
+ */
+export const triggerRefresh = () => useMessageStore.getState().triggerRefresh();
+
+/**
+ * 设置已加载标识
+ * @param loaded 是否已加载
+ */
+export const setHasLoadedOnce = (loaded: boolean) =>
+ useMessageStore.getState().setHasLoadedOnce(loaded);
+
+/**
+ * 重置加载状态(用于登出或切换用户)
+ */
+export const resetLoadState = () => useMessageStore.getState().resetLoadState();
diff --git a/Touchkebao/src/store/module/weChat/weChat.data.ts b/Touchkebao/src/store/module/weChat/weChat.data.ts
index 29a1f2d6..00bc2c51 100644
--- a/Touchkebao/src/store/module/weChat/weChat.data.ts
+++ b/Touchkebao/src/store/module/weChat/weChat.data.ts
@@ -14,6 +14,8 @@ export interface WeChatState {
updateShowChatRecordModel: (show: boolean) => void;
aiQuoteMessageContent: number;
updateAiQuoteMessageContent: (message: number) => void;
+ isLoadingAiChat: boolean;
+ updateIsLoadingAiChat: (loading: boolean) => void;
quoteMessageContent: string;
updateQuoteMessageContent: (value: string) => void;
// ==================== Transmit Module =========Start===========
@@ -22,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===========
// ==================== 当前联系人管理 ====================
@@ -110,4 +101,7 @@ export interface WeChatState {
// ==================== 消息接收处理 ====================
/** 接收新消息处理 */
receivedMsg: (message: ChatRecord) => void;
+ /** 查找消息 */
+ findMessageBySeq: (seq: number) => ChatRecord | undefined;
+ findMessageById: (id: number) => ChatRecord | undefined;
}
diff --git a/Touchkebao/src/store/module/weChat/weChat.ts b/Touchkebao/src/store/module/weChat/weChat.ts
index 5290b5eb..abeb9c19 100644
--- a/Touchkebao/src/store/module/weChat/weChat.ts
+++ b/Touchkebao/src/store/module/weChat/weChat.ts
@@ -6,7 +6,7 @@ import {
getGroupMembers,
} from "@/pages/pc/ckbox/api";
import { WeChatState } from "./weChat.data";
-
+import { dataProcessing, aiChat } from "@/api/ai";
import {
likeListItem,
CommentItem,
@@ -18,13 +18,6 @@ import {
getFriendInjectConfig,
} from "@/pages/pc/ckbox/api";
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
-import { weChatGroupService, contractService } from "@/utils/db";
-import {
- addChatSession,
- updateChatSession,
- useCkChatStore,
- pinChatSessionToTop,
-} from "@/store/module/ckchat/ckchat";
/**
* 微信聊天状态管理 Store
@@ -46,6 +39,11 @@ export const useWeChatStore = create()(
updateQuoteMessageContent: (message: string) => {
set({ quoteMessageContent: message });
},
+ //正在加载ai对话
+ isLoadingAiChat: false,
+ updateIsLoadingAiChat: (loading: boolean) => {
+ set({ isLoadingAiChat: loading });
+ },
// ==================== Transmit Module =========Start===========
/** 选中的聊天记录列表 */
selectedChatRecords: [],
@@ -54,21 +52,6 @@ export const useWeChatStore = create()(
set({ selectedChatRecords: message });
},
- /** 选中的联系人或群组列表 */
- selectedTransmitContact: [],
- /** 更新选中的联系人或群组 */
- updateSelectedTransmitContact: (
- contact: ContractData[] | weChatGroup[],
- ) => {
- set({ selectedTransmitContact: contact });
- },
-
- /** 转发弹窗开启状态 */
- openTransmitModal: false,
- /** 更新转发弹窗状态 */
- updateTransmitModal: (open: boolean) => {
- set({ openTransmitModal: open });
- },
// ==================== Transmit Module =========END===========
// ==================== 当前联系人管理状态 ====================
@@ -93,6 +76,12 @@ export const useWeChatStore = create()(
}
}
},
+ findMessageBySeq: (seq: number) => {
+ return get().currentMessages.find(msg => msg.seq === seq);
+ },
+ findMessageById: (id: number) => {
+ return get().currentMessages.find(msg => msg.id === id);
+ },
/** 更新指定消息内容 */
updateMessage: (messageId, updates) => {
set(state => ({
@@ -148,13 +137,10 @@ export const useWeChatStore = create()(
set({ currentContract: null, currentMessages: [] });
},
/** 设置当前联系人并加载相关数据 */
- setCurrentContact: (
- contract: ContractData | weChatGroup,
- isExist?: boolean,
- ) => {
+ setCurrentContact: (contract: ContractData | weChatGroup) => {
const state = useWeChatStore.getState();
// 切换联系人时清空当前消息,等待重新加载
- set({ currentMessages: [], openTransmitModal: false });
+ set({ currentMessages: [] });
const params: any = {};
@@ -173,14 +159,7 @@ export const useWeChatStore = create()(
}).then(result => {
set({ aiQuoteMessageContent: result });
});
- if (isExist) {
- updateChatSession({
- ...contract,
- config: { unreadCount: 0 },
- });
- } else {
- addChatSession(contract);
- }
+ // 注意:会话列表的未读数清零在MessageList组件的onContactClick中处理
set({ currentContract: contract });
updateConfig({
id: contract.id,
@@ -201,7 +180,7 @@ export const useWeChatStore = create()(
wechatAccountId: contact.wechatAccountId,
From: 1,
To: To || +new Date(),
- Count: 5,
+ Count: 20,
olderData: true,
};
@@ -307,40 +286,51 @@ export const useWeChatStore = create()(
message?.wechatChatroomId || message.wechatFriendId;
const isWechatGroup = message?.wechatChatroomId;
- // 如果是当前选中的聊天,直接添加到消息列表
- if (currentContract && currentContract.id == getMessageId) {
- set(state => ({
- currentMessages: [...state.currentMessages, message],
- }));
- } else {
- // 更新其他聊天的未读消息数
- const chatSessions = useCkChatStore.getState().chatSessions;
- const session = chatSessions.find(item => item.id == getMessageId);
- if (session) {
- session.unreadCount = Number(session.unreadCount) + 1;
- updateChatSession(session);
- // 将接收到新消息的会话置顶到列表顶部
- pinChatSessionToTop(getMessageId);
- } else {
- // 如果会话不存在,创建新会话
- if (isWechatGroup) {
- const [group] = await weChatGroupService.findByIds(getMessageId);
- if (group) {
- addChatSession({
- ...group,
- unreadCount: 1,
- });
- // 新创建的会话会自动添加到列表顶部,无需额外置顶
+ try {
+ // 如果是当前选中的聊天,直接添加到消息列表
+ if (currentContract && currentContract.id == getMessageId) {
+ set(state => ({
+ currentMessages: [...state.currentMessages, message],
+ }));
+
+ // 只有文字消息才触发AI(msgType === 1)
+ if (message.msgType === 1) {
+ //把数据传到存客宝
+ const params: any = {
+ type: "CmdNewMessage",
+ wechatAccountId: currentContract.wechatAccountId,
+ };
+ if (isWechatGroup) {
+ params.chatroomMessage = [message];
+ } else {
+ params.friendMessage = [message];
+ }
+ const dataProcessingResult = await dataProcessing(params);
+ //如果成功,就请求ai对话接口
+ set(() => ({
+ isLoadingAiChat: true,
+ }));
+ if (!dataProcessingResult) {
+ const messageContent = await aiChat({
+ friendId: getMessageId,
+ wechatAccountId: currentContract.wechatAccountId,
+ message: message,
+ });
+
+ set(() => ({
+ quoteMessageContent: messageContent?.content || "",
+ isLoadingAiChat: false,
+ }));
}
- } else {
- const [user] = await contractService.findByIds(getMessageId);
- addChatSession({
- ...user,
- unreadCount: 1,
- });
- // 新创建的会话会自动添加到列表顶部,无需额外置顶
}
}
+ // 注意:非当前聊天的会话列表更新已通过 chatMessageReceived 事件
+ // 在 MessageList 组件中处理,无需在此重复操作
+ } catch (error) {
+ console.error("接收新消息失败:", error);
+ set(() => ({
+ isLoadingAiChat: false,
+ }));
}
},
@@ -457,7 +447,7 @@ export const useWeChatStore = create()(
}),
{
name: "wechat-storage",
- partialize: state => ({
+ partialize: () => ({
// currentContract 不做持久化,登录和页面刷新时直接清空
}),
},
diff --git a/Touchkebao/src/store/module/websocket/msgManage.ts b/Touchkebao/src/store/module/websocket/msgManage.ts
index beab2fbf..4148f5dc 100644
--- a/Touchkebao/src/store/module/websocket/msgManage.ts
+++ b/Touchkebao/src/store/module/websocket/msgManage.ts
@@ -1,15 +1,19 @@
//消息管理器
-import { getkfUserList, asyncKfUserList } from "@/store/module/ckchat/ckchat";
import { useWeChatStore } from "../weChat/weChat";
import { WebSocketMessage } from "./websocket";
import { deepCopy } from "@/utils/common";
import { Messages } from "./msg.data";
import { db } from "@/utils/db";
+import { Modal } from "antd";
+import { useCustomerStore, updateCustomerList } from "../weChat/customer";
// 消息处理器类型定义
type MessageHandler = (message: WebSocketMessage) => void;
const addMessage = useWeChatStore.getState().addMessage;
const recallMessage = useWeChatStore.getState().recallMessage;
const receivedMsg = useWeChatStore.getState().receivedMsg;
+const findMessageBySeq = useWeChatStore.getState().findMessageBySeq;
+const findMessageById = useWeChatStore.getState().findMessageById;
+const updateMessage = useWeChatStore.getState().updateMessage;
const updateMomentCommonLoading =
useWeChatStore.getState().updateMomentCommonLoading;
const addMomentCommon = useWeChatStore.getState().addMomentCommon;
@@ -19,44 +23,73 @@ const messageHandlers: Record = {
CmdRequestWechatAccountsAliveStatusResp: message => {
// console.log("微信账号存活状态响应", message);
// 获取客服列表
- const kfUserList = deepCopy(getkfUserList());
+ const customerList = deepCopy(useCustomerStore.getState().customerList);
const wechatAccountsAliveStatus = message.wechatAccountsAliveStatus || {};
- // 遍历客服列表,更新存活状态
- kfUserList.forEach(kfUser => {
- kfUser.isOnline = wechatAccountsAliveStatus[kfUser.id];
- });
+
+ // 遍历客服列表,更新在线状态
+ const updatedCustomerList = customerList.map(customer => ({
+ ...customer,
+ isOnline: wechatAccountsAliveStatus[customer.id] || false,
+ }));
// 按在线状态排序,在线的排在前面
- kfUserList.sort((a, b) => {
+ updatedCustomerList.sort((a, b) => {
if (a.isOnline && !b.isOnline) return -1;
if (!a.isOnline && b.isOnline) return 1;
return 0;
});
- asyncKfUserList(kfUserList);
+ // 更新客服列表
+ updateCustomerList(updatedCustomerList);
},
// 发送消息响应
CmdSendMessageResp: message => {
- console.log("发送消息响应", message);
- addMessage(message.friendMessage || message.chatroomMessage);
- // 在这里添加具体的处理逻辑
+ console.log("CmdSendMessageResp", message);
+ const msg = findMessageBySeq(message.seq);
+ if (msg) {
+ updateMessage(message.seq, {
+ sendStatus: 1,
+ id: message.friendMessage?.id || message.chatroomMessage?.id,
+ });
+ }
},
CmdSendMessageResult: message => {
- console.log("发送消息结果", message);
- // 在这里添加具体的处理逻辑
+ updateMessage(message.friendMessageId, {
+ sendStatus: 0,
+ });
},
// 接收消息响应
CmdReceiveMessageResp: message => {
- console.log("接收消息响应", message);
- addMessage(message.friendMessage || message.chatroomMessage);
+ console.log("CmdReceiveMessageResp 接收消息响应", message);
+ // addMessage(message.friendMessage || message.chatroomMessage);
// 在这里添加具体的处理逻辑
},
//收到消息
CmdNewMessage: (message: Messages) => {
- // 在这里添加具体的处理逻辑
+ // 处理消息本身
receivedMsg(message.friendMessage || message.chatroomMessage);
+
+ // 触发会话列表更新事件
+ const msgData = message.friendMessage || message.chatroomMessage;
+ if (msgData) {
+ const sessionId = message.friendMessage
+ ? message.friendMessage.wechatFriendId
+ : message.chatroomMessage?.wechatChatroomId;
+ const type = message.friendMessage ? "friend" : "group";
+
+ // 发送自定义事件通知MessageList组件
+ window.dispatchEvent(
+ new CustomEvent("chatMessageReceived", {
+ detail: {
+ message: msgData,
+ sessionId,
+ type,
+ },
+ }),
+ );
+ }
},
- CmdFriendInfoChanged: message => {
+ CmdFriendInfoChanged: () => {
// console.log("好友信息变更", message);
// 在这里添加具体的处理逻辑
},
@@ -82,14 +115,31 @@ const messageHandlers: Record = {
console.log("通知消息", message);
// 在这里添加具体的处理逻辑
if (message.notify == "Auth failed") {
- // 被踢出时删除所有缓存数据
- localStorage.clear();
- // 删除
- await db.kfUsers.clear();
- await db.weChatGroup.clear();
- await db.contracts.clear();
- await db.newContractList.clear();
- window.location.href = "/login";
+ // 避免重复弹窗
+ if ((window as any).__CKB_AUTH_FAILED_SHOWN__) {
+ return;
+ }
+ (window as any).__CKB_AUTH_FAILED_SHOWN__ = true;
+
+ Modal.warning({
+ title: "登录失效",
+ content: "认证已失效或账号在其他设备登录,请重新登录。",
+ okText: "重新登录",
+ onOk: async () => {
+ try {
+ // 被踢出时删除所有缓存数据
+ localStorage.clear();
+ await db.chatSessions.clear();
+ await db.contactsUnified.clear();
+ await db.contactLabelMap.clear();
+ await db.userLoginRecords.clear();
+ } finally {
+ (window as any).__CKB_AUTH_FAILED_SHOWN__ = false;
+ window.location.href = "/login";
+ }
+ },
+ });
+ return;
}
},
@@ -97,12 +147,21 @@ const messageHandlers: Record = {
CmdMessageRecalled: message => {
const MessageId = message.friendMessageId || message.chatroomMessageId;
recallMessage(MessageId);
- // {
- // "friendMessageId": 745007874,
- // "chatroomMessageId": 0,
- // "seq": 2076470,
- // "cmdType": "CmdMessageRecalled"
- // }
+ },
+
+ CmdVoiceToTextResult: message => {
+ const msg = findMessageById(
+ message.friendMessageId || message.chatroomMessageId,
+ );
+ const content = JSON.parse(msg.content);
+ if (msg) {
+ updateMessage(msg.id, {
+ content: JSON.stringify({
+ ...content,
+ text: message.text,
+ }),
+ });
+ }
},
// 可以继续添加更多处理器...
diff --git a/Touchkebao/src/utils/common.ts b/Touchkebao/src/utils/common.ts
index 5c195d3b..c2a879cb 100644
--- a/Touchkebao/src/utils/common.ts
+++ b/Touchkebao/src/utils/common.ts
@@ -1,55 +1,76 @@
import { Modal } from "antd-mobile";
import { getSetting } from "@/store/module/settings";
-export function formatWechatTime(timestamp) {
- if (!timestamp) {
+import dayjs from "dayjs";
+export function formatWechatTime(lastUpdateTime: string) {
+ if (!lastUpdateTime) {
return "";
}
- // 处理时间戳(兼容秒级/毫秒级)
- const date = new Date(
- timestamp.toString().length === 10 ? timestamp * 1000 : timestamp,
- );
- const now = new Date();
- // 获取消息时间的年月日时分
- const messageYear = date.getFullYear();
- const messageMonth = date.getMonth();
- const messageDate = date.getDate();
- const messageHour = date.getHours().toString().padStart(2, "0");
- const messageMinute = date.getMinutes().toString().padStart(2, "0");
+ // 使用dayjs解析时间字符串,处理"2025-10-22 13:21:13"格式
+ let messageTime;
+ try {
+ // 直接解析时间字符串,dayjs可以自动识别这种格式
+ messageTime = dayjs(lastUpdateTime);
- // 获取当前时间的年月日
- const nowYear = now.getFullYear();
- const nowMonth = now.getMonth();
- const nowDate = now.getDate();
-
- // 创建当天0点的时间对象,用于比较是否同一天
- const today = new Date(nowYear, nowMonth, nowDate, 0, 0, 0);
- const yesterday = new Date(today);
- yesterday.setDate(yesterday.getDate() - 1);
- const weekAgo = new Date(today);
- weekAgo.setDate(weekAgo.getDate() - 6); // 7天前(包括今天)
-
- // 消息日期(不含时间)
- const messageDay = new Date(messageYear, messageMonth, messageDate, 0, 0, 0);
-
- // 当天消息:只显示时分
- if (messageDay.getTime() === today.getTime()) {
- return `${messageHour}:${messageMinute}`;
+ // 检查日期是否有效
+ if (!messageTime.isValid()) {
+ console.warn("formatWechatTime: Invalid date string", lastUpdateTime);
+ return "";
+ }
+ } catch (error) {
+ console.error(
+ "formatWechatTime: Error processing date string",
+ lastUpdateTime,
+ error,
+ );
+ return "";
}
- // 昨天消息:显示"昨天 时分"
- if (messageDay.getTime() === yesterday.getTime()) {
- return `昨天 ${messageHour}:${messageMinute}`;
+ const now = dayjs();
+
+ // 获取消息时间和当前时间的年份
+ const messageYear = messageTime.year();
+ const nowYear = now.year();
+
+ // 如果是去年或更早的时间,直接返回空字符串
+ if (messageYear < nowYear) {
+ return "";
}
- // 一周内消息:显示"星期X 时分"
- if (messageDay.getTime() >= weekAgo.getTime()) {
+ // 创建时间比较对象
+ const today = now.startOf("day");
+ const yesterday = today.subtract(1, "day");
+ const messageDay = messageTime.startOf("day");
+
+ // 1. 时间是否今天,如果是就展示时和分,示例为: "13:00"
+ if (messageDay.isSame(today, "day")) {
+ return messageTime.format("HH:mm");
+ }
+
+ // 2. 时间是否为昨天,如果是就展示"昨天"和时与分,示例为: "昨天 13:00"
+ if (messageDay.isSame(yesterday, "day")) {
+ return `昨天 ${messageTime.format("HH:mm")}`;
+ }
+
+ // 3. 时间是否在本周,如果是就展示具体周几,示例为: "周一"
+ const startOfWeek = now.startOf("week");
+ const endOfWeek = now.endOf("week");
+ if (messageTime.isAfter(startOfWeek) && messageTime.isBefore(endOfWeek)) {
const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
- return `${weekdays[date.getDay()]} ${messageHour}:${messageMinute}`;
+ return weekdays[messageTime.day()];
}
- // 超过一周:显示"年月日 时分"
- return `${messageYear}年${messageMonth + 1}月${messageDate}日 ${messageHour}:${messageMinute}`;
+ // 4. 时间是否在本月,如果是就展示月和日,示例为: "12/07"
+ if (messageTime.isSame(now, "month")) {
+ return messageTime.format("MM/DD");
+ }
+
+ // 5. 时间是否在本年,如果在本年就展示月和日,示例为: "12/07"
+ if (messageTime.isSame(now, "year")) {
+ return messageTime.format("MM/DD");
+ }
+
+ return "";
}
/**
* 通用js调用弹窗,Promise风格
diff --git a/Touchkebao/src/utils/db.ts b/Touchkebao/src/utils/db.ts
index df673a94..d314eca9 100644
--- a/Touchkebao/src/utils/db.ts
+++ b/Touchkebao/src/utils/db.ts
@@ -4,72 +4,143 @@
* 架构设计:
* 1. 使用serverId作为数据库主键,直接对应接口返回的id字段
* 2. 保留原始的id字段,用于存储接口数据的完整性
- * 3. 简化数据处理逻辑,避免ID映射的复杂性
+ * 3. 添加userId字段实现多用户数据隔离
+ * 4. 统一会话表和联系人表,兼容好友和群聊
*
* 优势:
* - 直接使用服务器ID作为主键,避免ID冲突
- * - 保持数据的一致性和可追溯性
- * - 简化查询逻辑,提高性能
- * - 支持重复数据检测和去重
- *
- * 使用方法:
- * - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId()
- * - 查询数据:使用 findById(id) 根据原始ID查询,或 findByPrimaryKey(serverId) 根据主键查询
- * - 批量查询:使用 findByIds([id1, id2, ...]) 根据原始ID批量查询
- * - 内部操作:serverId作为主键用于数据库内部管理
- *
- * 示例:
- * const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据
- * const serverId = await service.createWithServerId(serverData); // 存储,返回serverId
- * const data = await service.findById(1001); // 根据原始ID查询(用户友好)
- * const dataByPK = await service.findByPrimaryKey(serverId); // 根据主键查询(内部使用)
+ * - 通过userId实现多用户数据隔离
+ * - 统一的会话和联系人表结构,兼容好友和群聊
+ * - 支持复合索引,提高查询性能
+ * - 支持用户登录记录和自动清理
*/
import Dexie, { Table } from "dexie";
-import {
- KfUserListData,
- weChatGroup,
- ContractData,
- MessageListData,
-} from "@/pages/pc/ckbox/data";
-// 数据类型定义,使用serverId作为主键
-export interface KfUserWithServerId extends Omit {
- serverId: number | string; // 服务器ID作为主键
- id?: number; // 接口数据的原始ID字段
+// ==================== 用户登录记录 ====================
+export interface UserLoginRecord {
+ serverId: string; // 主键: user_${userId}
+ userId: number; // 用户ID
+ lastLoginTime: string; // 最后登录时间
+ loginCount: number; // 登录次数
+ createTime: string; // 首次登录时间
+ lastActiveTime: string; // 最后活跃时间
}
-// 新联系人列表数据接口
-export interface NewContactListData {
- serverId: number | string; // 服务器ID作为主键
- id?: number; // 接口数据的原始ID字段
- groupName: string;
- contacts: ContractData[];
- weChatGroup: weChatGroup[];
+// ==================== 统一会话表(兼容好友和群聊) ====================
+export interface ChatSession {
+ serverId: string; // 主键
+ userId: number; // 用户ID(数据隔离)
+ id: number; // 原始ID
+ type: "friend" | "group"; // 类型:好友或群聊
+
+ // 通用字段
+ wechatAccountId: number; // 所属客服账号
+ nickname: string; // 显示名称
+ conRemark?: string; // 备注名
+ avatar: string; // 头像
+ content: string; // 最新消息内容
+ lastUpdateTime: string; // 最后更新时间
+ config: {
+ unreadCount: number; // 未读数量
+ top: number; // 是否置顶(1=置顶,0=非置顶)
+ };
+ sortKey: string; // 预计算排序键
+
+ // 好友特有字段(type='friend'时有效)
+ wechatFriendId?: number; // 好友ID
+ wechatId?: string; // 微信号
+ alias?: string; // 别名
+
+ // 群聊特有字段(type='group'时有效)
+ chatroomId?: string; // 群聊ID
+ chatroomOwner?: string; // 群主
+ selfDisplayName?: string; // 群内昵称
+ notice?: string; // 群公告
+}
+
+// ==================== 统一联系人表(兼容好友和群聊) ====================
+export interface Contact {
+ serverId: string; // 主键
+ userId: number; // 用户ID(数据隔离)
+ id: number; // 原始ID
+ type: "friend" | "group"; // 类型:好友或群聊
+
+ // 通用字段
+ wechatAccountId: number; // 所属客服账号
+ nickname: string; // 显示名称
+ conRemark?: string; // 备注名
+ avatar: string; // 头像
+ lastUpdateTime: string; // 最后更新时间
+ config?: any; // 配置信息
+ sortKey: string; // 预计算排序键
+ searchKey: string; // 预计算搜索键
+
+ // 好友特有字段(type='friend'时有效)
+ wechatFriendId?: number; // 好友ID
+ wechatId?: string; // 微信号
+ alias?: string; // 别名
+ gender?: number; // 性别
+ groupId?: number; // 标签ID(分组)
+ region?: string; // 地区
+ signature?: string; // 个性签名
+ phone?: string; // 手机号
+ quanPin?: string; // 全拼
+
+ // 群聊特有字段(type='group'时有效)
+ chatroomId?: string; // 群聊ID
+ chatroomOwner?: string; // 群主
+ selfDisplayName?: string; // 群内昵称
+ notice?: string; // 群公告
+ memberCount?: number; // 群成员数量
+}
+
+// ==================== 联系人标签映射表 ====================
+export interface ContactLabelMap {
+ serverId: string; // 主键: ${contactId}_${labelId}
+ userId: number; // 用户ID(数据隔离)
+ labelId: number; // 标签ID
+ contactId: number; // 联系人ID
+ contactType: "friend" | "group"; // 联系人类型
+ sortKey: string; // 预计算排序键
+ searchKey: string; // 预计算搜索键
+
+ // 列表展示必需字段(轻量)
+ avatar: string;
+ nickname: string;
+ conRemark?: string;
+ unreadCount: number;
+ lastUpdateTime: string;
}
// 数据库类
class CunkebaoDatabase extends Dexie {
- kfUsers!: Table;
- weChatGroup!: Table;
- contracts!: Table;
- newContractList!: Table;
- messageList!: Table;
+ // ==================== 统一表结构 ====================
+ chatSessions!: Table; // 统一会话表
+ contactsUnified!: Table; // 统一联系人表
+ contactLabelMap!: Table; // 联系人标签映射表
+ userLoginRecords!: Table; // 用户登录记录表
constructor() {
super("CunkebaoDatabase");
- // 版本1:使用serverId作为主键的架构
+ // 版本1:统一表结构
this.version(1).stores({
- kfUsers:
- "serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
- weChatGroup:
- "serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName",
- contracts:
- "serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, duplicate",
- newContractList: "serverId, id, groupName, contacts",
- messageList:
- "serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName",
+ // 会话表索引:支持按用户、类型、时间、置顶等查询
+ chatSessions:
+ "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], [userId+lastUpdateTime], sortKey, nickname, conRemark, avatar, content, lastUpdateTime",
+
+ // 联系人表索引:支持按用户、类型、标签、搜索等查询
+ contactsUnified:
+ "serverId, userId, id, type, wechatAccountId, [userId+type], [userId+wechatAccountId], sortKey, searchKey, nickname, conRemark, avatar, lastUpdateTime, groupId",
+
+ // 联系人标签映射表索引:支持按用户、标签、联系人、类型查询
+ contactLabelMap:
+ "serverId, userId, labelId, contactId, contactType, [userId+labelId], [userId+contactId], [userId+labelId+sortKey], sortKey, searchKey, avatar, nickname, conRemark, unreadCount, lastUpdateTime",
+
+ // 用户登录记录表索引:支持按用户ID、登录时间查询
+ userLoginRecords:
+ "serverId, userId, lastLoginTime, loginCount, createTime, lastActiveTime",
});
}
}
@@ -331,12 +402,11 @@ export class DatabaseService {
}
}
-// 创建各表的服务实例
-export const kfUserService = new DatabaseService(db.kfUsers);
-export const weChatGroupService = new DatabaseService(db.weChatGroup);
-export const contractService = new DatabaseService(db.contracts);
-export const newContactListService = new DatabaseService(db.newContractList);
-export const messageListService = new DatabaseService(db.messageList);
+// 创建统一表的服务实例
+export const chatSessionService = new DatabaseService(db.chatSessions);
+export const contactUnifiedService = new DatabaseService(db.contactsUnified);
+export const contactLabelMapService = new DatabaseService(db.contactLabelMap);
+export const userLoginRecordService = new DatabaseService(db.userLoginRecords);
// 默认导出数据库实例
export default db;
diff --git a/Touchkebao/src/utils/dbAction/contact.ts b/Touchkebao/src/utils/dbAction/contact.ts
new file mode 100644
index 00000000..675e0650
--- /dev/null
+++ b/Touchkebao/src/utils/dbAction/contact.ts
@@ -0,0 +1,464 @@
+import { Contact, contactUnifiedService } from "@/utils/db";
+import { ContactGroupByLabel } from "@/pages/pc/ckbox/data";
+
+/**
+ * 联系人数据管理器
+ * 负责联系人相关的数据库操作和业务逻辑
+ */
+export class ContactManager {
+ /**
+ * 获取用户的所有联系人
+ */
+ static async getUserContacts(userId: number): Promise {
+ 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 {
+ 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 {
+ 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 {
+ try {
+ await contactUnifiedService.create(contact);
+ } catch (error) {
+ console.error("添加联系人失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 批量添加联系人
+ */
+ static async addContacts(contacts: Contact[]): Promise {
+ try {
+ await contactUnifiedService.createMany(contacts);
+ } catch (error) {
+ console.error("批量添加联系人失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 更新联系人
+ */
+ static async updateContact(contact: Contact): Promise {
+ try {
+ await contactUnifiedService.update(contact.serverId, contact);
+ } catch (error) {
+ console.error("更新联系人失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 删除联系人
+ */
+ static async deleteContact(contactId: number): Promise {
+ 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 {
+ 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 {
+ try {
+ // 这里应该根据实际的标签系统来实现
+ // 暂时返回空数组,实际实现需要根据标签表来查询
+ return [];
+ } catch (error) {
+ console.error("获取联系人分组失败:", error);
+ return [];
+ }
+ }
+
+ /**
+ * 创建联系人分组
+ */
+ static async createContactGroup(
+ group: Omit,
+ ): Promise {
+ try {
+ // 这里应该调用标签相关的API
+ console.log("创建联系人分组:", group);
+ } catch (error) {
+ console.error("创建联系人分组失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 更新联系人分组
+ */
+ static async updateContactGroup(group: ContactGroupByLabel): Promise {
+ try {
+ // 这里应该调用标签相关的API
+ console.log("更新联系人分组:", group);
+ } catch (error) {
+ console.error("更新联系人分组失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 删除联系人分组
+ */
+ static async deleteContactGroup(groupId: number): Promise {
+ try {
+ // 这里应该调用标签相关的API
+ console.log("删除联系人分组:", groupId);
+ } catch (error) {
+ console.error("删除联系人分组失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 根据ID获取联系人
+ */
+ static async getContactById(
+ userId: number,
+ contactId: number,
+ ): Promise {
+ 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;
+ }
+ }
+
+ /**
+ * 根据ID和类型获取联系人(用于消息列表查询完整联系人信息)
+ */
+ static async getContactByIdAndType(
+ userId: number,
+ contactId: number,
+ type: "friend" | "group",
+ ): Promise {
+ try {
+ const contacts = await contactUnifiedService.findWhereMultiple([
+ { field: "userId", operator: "equals", value: userId },
+ { field: "id", operator: "equals", value: contactId },
+ { field: "type", operator: "equals", value: type },
+ ]);
+ return contacts.length > 0 ? contacts[0] : null;
+ } catch (error) {
+ console.error("根据ID和类型获取联系人失败:", error);
+ return null;
+ }
+ }
+
+ /**
+ * 根据类型获取联系人
+ */
+ static async getContactsByType(
+ userId: number,
+ type: "friend" | "group",
+ ): Promise {
+ 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: {} };
+ }
+ }
+
+ /**
+ * 获取指定分组的联系人数量(支持数据库级别的统计)
+ */
+ static async getContactCount(
+ userId: number,
+ type: "friend" | "group",
+ customerId?: number,
+ groupIds?: number[],
+ exclude: boolean = false,
+ ): Promise {
+ try {
+ console.log("getContactCount 调用参数:", {
+ userId,
+ type,
+ customerId,
+ groupIds,
+ exclude,
+ });
+
+ const conditions: any[] = [
+ { field: "userId", operator: "equals", value: userId },
+ { field: "type", operator: "equals", value: type },
+ ];
+
+ // 客服筛选
+ if (customerId && customerId !== 0) {
+ conditions.push({
+ field: "wechatAccountId",
+ operator: "equals",
+ value: customerId,
+ });
+ }
+
+ // 分组筛选
+ if (groupIds && groupIds.length > 0) {
+ if (exclude) {
+ // 排除指定分组(未分组)
+ conditions.push({
+ field: "groupId",
+ operator: "notIn",
+ value: groupIds,
+ });
+ } else {
+ // 包含指定分组
+ conditions.push({
+ field: "groupId",
+ operator: "anyOf",
+ value: groupIds,
+ });
+ }
+ }
+
+ console.log("查询条件:", conditions);
+
+ const contacts =
+ await contactUnifiedService.findWhereMultiple(conditions);
+
+ console.log(
+ `查询结果数量: ${contacts.length}, type: ${type}, groupIds: ${groupIds}`,
+ );
+
+ return contacts.length;
+ } catch (error) {
+ console.error("获取联系人数量失败:", error);
+ return 0;
+ }
+ }
+
+ /**
+ * 分页获取指定分组的联系人
+ */
+ static async getContactsByGroupPaginated(
+ userId: number,
+ type: "friend" | "group",
+ customerId?: number,
+ groupIds?: number[],
+ exclude: boolean = false,
+ offset: number = 0,
+ limit: number = 20,
+ ): Promise {
+ try {
+ const conditions: any[] = [
+ { field: "userId", operator: "equals", value: userId },
+ { field: "type", operator: "equals", value: type },
+ ];
+
+ // 客服筛选
+ if (customerId && customerId !== 0) {
+ conditions.push({
+ field: "wechatAccountId",
+ operator: "equals",
+ value: customerId,
+ });
+ }
+
+ // 分组筛选
+ if (groupIds && groupIds.length > 0) {
+ if (exclude) {
+ // 排除指定分组(未分组)
+ conditions.push({
+ field: "groupId",
+ operator: "notIn",
+ value: groupIds,
+ });
+ } else {
+ // 包含指定分组
+ conditions.push({
+ field: "groupId",
+ operator: "anyOf",
+ value: groupIds,
+ });
+ }
+ }
+
+ // 查询数据
+ const allContacts =
+ await contactUnifiedService.findWhereMultiple(conditions);
+
+ // 手动分页(IndexedDB 不支持原生的 offset/limit)
+ return allContacts.slice(offset, offset + limit);
+ } catch (error) {
+ console.error("分页获取联系人失败:", error);
+ return [];
+ }
+ }
+}
diff --git a/Touchkebao/src/utils/dbAction/index.ts b/Touchkebao/src/utils/dbAction/index.ts
new file mode 100644
index 00000000..3152e5db
--- /dev/null
+++ b/Touchkebao/src/utils/dbAction/index.ts
@@ -0,0 +1,7 @@
+/**
+ * 数据库操作层统一导出
+ */
+
+export { MessageManager } from "./message";
+export { ContactManager } from "./contact";
+export { LoginManager } from "./loginManager";
diff --git a/Touchkebao/src/utils/dbAction/loginManager.ts b/Touchkebao/src/utils/dbAction/loginManager.ts
new file mode 100644
index 00000000..fd3ed519
--- /dev/null
+++ b/Touchkebao/src/utils/dbAction/loginManager.ts
@@ -0,0 +1,169 @@
+import { UserLoginRecord, userLoginRecordService } from "@/utils/db";
+
+/**
+ * 登录管理器
+ * 负责用户登录记录相关的数据库操作和业务逻辑
+ */
+export class LoginManager {
+ /**
+ * 记录用户登录
+ */
+ static async recordLogin(userId: number): Promise {
+ 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 {
+ try {
+ const serverId = `user_${userId}`;
+ await userLoginRecordService.update(serverId, {
+ lastActiveTime: new Date().toISOString(),
+ });
+ } catch (error) {
+ console.error("更新用户活跃时间失败:", error);
+ }
+ }
+
+ /**
+ * 获取用户登录记录
+ */
+ static async getLoginRecord(userId: number): Promise {
+ 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 {
+ 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 {
+ 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 {
+ try {
+ return (await userLoginRecordService.findAll()) as UserLoginRecord[];
+ } catch (error) {
+ console.error("获取所有登录记录失败:", error);
+ return [];
+ }
+ }
+
+ /**
+ * 删除用户登录记录
+ */
+ static async deleteLoginRecord(userId: number): Promise {
+ 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,
+ };
+ }
+ }
+}
diff --git a/Touchkebao/src/utils/dbAction/message.ts b/Touchkebao/src/utils/dbAction/message.ts
new file mode 100644
index 00000000..ae0a0e20
--- /dev/null
+++ b/Touchkebao/src/utils/dbAction/message.ts
@@ -0,0 +1,776 @@
+/**
+ * 会话列表数据库操作管理器
+ * 职责:
+ * 1. 会话数据的增删改查
+ * 2. 增量同步逻辑(对比本地和服务器数据)
+ * 3. 好友/群聊数据转换为统一格式
+ * 4. 提供回调机制通知组件更新
+ */
+
+import { db, chatSessionService, ChatSession } from "../db";
+import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
+
+export class MessageManager {
+ private static updateCallbacks = new Set<(sessions: ChatSession[]) => void>();
+
+ // ==================== 回调管理 ====================
+
+ /**
+ * 注册会话更新回调
+ * @param callback 回调函数
+ * @returns 取消注册的函数
+ */
+ static onSessionsUpdate(callback: (sessions: ChatSession[]) => void) {
+ this.updateCallbacks.add(callback);
+ return () => this.updateCallbacks.delete(callback);
+ }
+
+ /**
+ * 触发所有注册的回调
+ * @param userId 用户ID
+ */
+ private static async triggerCallbacks(userId: number) {
+ try {
+ const sessions = await this.getUserSessions(userId);
+ this.updateCallbacks.forEach(callback => {
+ try {
+ callback(sessions);
+ } catch (error) {
+ console.error("会话更新回调执行失败:", error);
+ }
+ });
+ } catch (error) {
+ console.error("触发回调失败:", error);
+ }
+ }
+
+ // ==================== 数据转换 ====================
+
+ /**
+ * 生成会话排序键(微信排序方式)
+ * @param session 会话数据
+ * @returns 排序键
+ */
+ private static generateSortKey(session: any): string {
+ // 1. 置顶标识:置顶为1,普通为0(降序时置顶在前)
+ const isTop = session.config?.top ? 1 : 0;
+
+ // 2. 时间戳:直接使用时间戳,数值越大表示越新
+ const timestamp = new Date(session.lastUpdateTime || new Date()).getTime();
+
+ // 3. 显示名称:用于稳定排序
+ const displayName = (
+ session.conRemark ||
+ session.nickname ||
+ ""
+ ).toLowerCase();
+
+ // 格式:置顶标识|时间戳|显示名称
+ // 降序排序:置顶(1)在前,时间大的在前,名称小的在前
+ return `${isTop}|${timestamp}|${displayName}`;
+ }
+
+ /**
+ * 转换好友会话为统一格式
+ * @param friend 好友数据
+ * @param userId 用户ID
+ * @returns 统一会话格式
+ */
+ private static convertFriendToChatSession(
+ friend: ContractData,
+ userId: number,
+ ): ChatSession {
+ return {
+ serverId: `friend_${friend.id}`,
+ userId,
+ id: friend.id!,
+ type: "friend",
+ wechatAccountId: friend.wechatAccountId,
+ nickname: friend.nickname,
+ conRemark: friend.conRemark,
+ avatar: friend.avatar || "",
+ content: (friend as any).content || "",
+ lastUpdateTime: friend.lastUpdateTime || new Date().toISOString(),
+ config: {
+ unreadCount: friend.config?.unreadCount || 0,
+ top: (friend.config as any)?.top || false,
+ },
+ sortKey: this.generateSortKey(friend),
+ wechatFriendId: friend.id,
+ wechatId: friend.wechatId,
+ alias: friend.alias,
+ };
+ }
+
+ /**
+ * 转换群聊会话为统一格式
+ * @param group 群聊数据
+ * @param userId 用户ID
+ * @returns 统一会话格式
+ */
+ private static convertGroupToChatSession(
+ group: weChatGroup,
+ userId: number,
+ ): ChatSession {
+ return {
+ serverId: `group_${group.id}`,
+ userId,
+ id: group.id!,
+ type: "group",
+ wechatAccountId: group.wechatAccountId,
+ nickname: group.nickname,
+ conRemark: group.conRemark,
+ avatar: group.chatroomAvatar || "",
+ content: (group as any).content || "",
+ lastUpdateTime: (group as any).lastUpdateTime || new Date().toISOString(),
+ config: {
+ unreadCount: (group.config as any)?.unreadCount || 0,
+ top: (group.config as any)?.top || false,
+ },
+ sortKey: this.generateSortKey(group),
+ chatroomId: group.chatroomId,
+ chatroomOwner: group.chatroomOwner,
+ selfDisplayName: group.selfDisplyName,
+ notice: group.notice,
+ };
+ }
+
+ // ==================== 查询操作 ====================
+
+ /**
+ * 获取用户的所有会话(已排序)
+ * @param userId 用户ID
+ * @returns 会话列表
+ */
+ static async getUserSessions(userId: number): Promise {
+ try {
+ // 按sortKey降序排序查询(置顶在前,最新的在前)
+ const sessions = await db.chatSessions
+ .where("userId")
+ .equals(userId)
+ .reverse()
+ .sortBy("sortKey");
+
+ return sessions;
+ } catch (error) {
+ console.error("获取用户会话失败:", error);
+ return [];
+ }
+ }
+
+ /**
+ * 根据ID查找会话
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @returns 会话数据
+ */
+ static async getSessionById(
+ userId: number,
+ sessionId: number,
+ ): Promise {
+ try {
+ return await db.chatSessions
+ .where(["userId", "id"])
+ .equals([userId, sessionId])
+ .first();
+ } catch (error) {
+ console.error("查找会话失败:", error);
+ return undefined;
+ }
+ }
+
+ // ==================== 同步操作 ====================
+
+ /**
+ * 判断会话是否需要更新
+ * @param local 本地会话
+ * @param server 服务器会话
+ * @returns 是否需要更新
+ */
+ private static needsUpdate(local: ChatSession, server: ChatSession): boolean {
+ const fieldsToCompare = [
+ "content",
+ "lastUpdateTime",
+ "nickname",
+ "conRemark",
+ "avatar",
+ "wechatAccountId", // 添加wechatAccountId比较
+ ];
+
+ for (const field of fieldsToCompare) {
+ if (
+ JSON.stringify((local as any)[field]) !==
+ JSON.stringify((server as any)[field])
+ ) {
+ return true;
+ }
+ }
+
+ // 检查config对象
+ if (
+ local.config.unreadCount !== server.config.unreadCount ||
+ local.config.top !== server.config.top
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 增量同步会话数据
+ * @param userId 用户ID
+ * @param serverData 服务器数据
+ * @returns 同步结果统计
+ */
+ static async syncSessions(
+ userId: number,
+ serverData: {
+ friends?: ContractData[];
+ groups?: weChatGroup[];
+ },
+ ): Promise<{
+ added: number;
+ updated: number;
+ deleted: number;
+ }> {
+ return await db.transaction("rw", [db.chatSessions], async () => {
+ // 1. 获取本地现有会话
+ const localSessions = (await chatSessionService.findWhere(
+ "userId",
+ userId,
+ )) as ChatSession[];
+ const localSessionMap = new Map(localSessions.map(s => [s.id, s]));
+
+ // 2. 转换服务器数据为统一格式
+ const serverSessions: ChatSession[] = [];
+
+ // 处理好友会话
+ if (serverData.friends) {
+ const friends = serverData.friends
+ .filter(f => (f.config as any)?.chat === true) // 只要开启会话的
+ .map(friend => this.convertFriendToChatSession(friend, userId));
+ serverSessions.push(...friends);
+ }
+
+ // 处理群聊会话
+ if (serverData.groups) {
+ const groups = serverData.groups
+ .filter(g => (g.config as any)?.chat === true) // 只要开启会话的
+ .map(group => this.convertGroupToChatSession(group, userId));
+ serverSessions.push(...groups);
+ }
+
+ const serverSessionMap = new Map(serverSessions.map(s => [s.id, s]));
+
+ // 3. 计算差异
+ const toAdd: ChatSession[] = [];
+ const toUpdate: ChatSession[] = [];
+ const toDelete: number[] = [];
+
+ // 检查新增和更新
+ for (const serverSession of serverSessions) {
+ const localSession = localSessionMap.get(serverSession.id);
+
+ if (!localSession) {
+ toAdd.push(serverSession);
+ } else {
+ if (this.needsUpdate(localSession, serverSession)) {
+ toUpdate.push(serverSession);
+ }
+ }
+ }
+
+ // 检查删除
+ for (const localSession of localSessions) {
+ if (!serverSessionMap.has(localSession.id)) {
+ toDelete.push(localSession.id);
+ }
+ }
+
+ // 4. 执行同步操作
+ let added = 0,
+ updated = 0,
+ deleted = 0;
+
+ if (toAdd.length > 0) {
+ await this.batchAddSessions(toAdd);
+ added = toAdd.length;
+ }
+
+ if (toUpdate.length > 0) {
+ await this.batchUpdateSessions(toUpdate);
+ updated = toUpdate.length;
+ }
+
+ if (toDelete.length > 0) {
+ await this.batchDeleteSessions(userId, toDelete);
+ deleted = toDelete.length;
+ }
+
+ console.log(`会话同步完成: 新增${added}, 更新${updated}, 删除${deleted}`);
+
+ // 5. 触发回调通知组件
+ await this.triggerCallbacks(userId);
+
+ return { added, updated, deleted };
+ });
+ }
+
+ // ==================== 增删改操作 ====================
+
+ /**
+ * 批量新增会话
+ * @param sessions 会话列表
+ */
+ private static async batchAddSessions(sessions: ChatSession[]) {
+ if (sessions.length === 0) return;
+
+ const dataToInsert = sessions.map(session => ({
+ ...session,
+ serverId: `${session.type}_${session.id}`,
+ }));
+
+ await db.chatSessions.bulkAdd(dataToInsert);
+ }
+
+ /**
+ * 批量更新会话
+ * @param sessions 会话列表
+ */
+ private static async batchUpdateSessions(sessions: ChatSession[]) {
+ if (sessions.length === 0) return;
+
+ for (const session of sessions) {
+ const serverId = `${session.type}_${session.id}`;
+ await chatSessionService.update(serverId, session);
+ }
+ }
+
+ /**
+ * 批量删除会话
+ * @param userId 用户ID
+ * @param sessionIds 会话ID列表
+ */
+ private static async batchDeleteSessions(
+ userId: number,
+ sessionIds: number[],
+ ) {
+ if (sessionIds.length === 0) return;
+
+ await db.chatSessions
+ .where("userId")
+ .equals(userId)
+ .and(session => sessionIds.includes(session.id))
+ .delete();
+ }
+
+ /**
+ * 新增单个会话
+ * @param session 会话数据
+ */
+ static async addSession(session: ChatSession): Promise {
+ try {
+ const dataToInsert = {
+ ...session,
+ serverId: `${session.type}_${session.id}`,
+ sortKey: this.generateSortKey(session),
+ };
+
+ await db.chatSessions.add(dataToInsert);
+ await this.triggerCallbacks(session.userId);
+ } catch (error) {
+ console.error("新增会话失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 更新单个会话
+ * @param session 会话数据
+ */
+ static async updateSession(
+ session: Partial & {
+ userId: number;
+ id: number;
+ type: "friend" | "group";
+ },
+ ): Promise {
+ try {
+ const serverId = `${session.type}_${session.id}`;
+ const updateData = {
+ ...session,
+ sortKey: this.generateSortKey(session),
+ };
+
+ await chatSessionService.update(serverId, updateData);
+ await this.triggerCallbacks(session.userId);
+ } catch (error) {
+ console.error("更新会话失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 删除单个会话
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ */
+ static async deleteSession(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ ): Promise {
+ try {
+ const serverId = `${type}_${sessionId}`;
+ await chatSessionService.delete(serverId);
+ await this.triggerCallbacks(userId);
+ } catch (error) {
+ console.error("删除会话失败:", error);
+ throw error;
+ }
+ }
+
+ // ==================== 特殊操作 ====================
+
+ /**
+ * 从联系人数据构建会话(发起新会话时使用)
+ * @param contact 联系人数据(好友或群聊)
+ * @param userId 用户ID
+ * @returns 会话数据
+ */
+ static buildSessionFromContact(
+ contact: ContractData | weChatGroup,
+ userId: number,
+ ): ChatSession {
+ const isGroup = "chatroomId" in contact;
+
+ if (isGroup) {
+ // 群聊
+ return this.convertGroupToChatSession(contact as weChatGroup, userId);
+ } else {
+ // 好友
+ return this.convertFriendToChatSession(contact as ContractData, userId);
+ }
+ }
+
+ /**
+ * 更新会话的最新消息(WebSocket消息到达时使用)
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ * @param message 消息内容
+ */
+ static async updateSessionOnNewMessage(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ message: {
+ content: string;
+ },
+ ): Promise {
+ try {
+ const serverId = `${type}_${sessionId}`;
+ const session = (await chatSessionService.findByPrimaryKey(
+ serverId,
+ )) as ChatSession;
+
+ if (session) {
+ const updatedSession = {
+ ...session,
+ config: {
+ ...session.config,
+ unreadCount: (session.config?.unreadCount || 0) + 1,
+ },
+ content: message.content,
+ lastUpdateTime: new Date().toISOString(),
+ };
+
+ updatedSession.sortKey = this.generateSortKey(updatedSession);
+
+ await chatSessionService.update(serverId, updatedSession);
+ await this.triggerCallbacks(userId);
+ }
+ } catch (error) {
+ console.error("更新会话消息失败:", error);
+ }
+ }
+
+ /**
+ * 标记会话为已读
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ */
+ static async markAsRead(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ ): Promise {
+ try {
+ const serverId = `${type}_${sessionId}`;
+ const session = (await chatSessionService.findByPrimaryKey(
+ serverId,
+ )) as ChatSession;
+
+ if (session && session.config.unreadCount > 0) {
+ await chatSessionService.update(serverId, {
+ config: {
+ ...session.config,
+ unreadCount: 0,
+ },
+ });
+ // 不触发回调,因为只是已读状态变化,不需要重新排序
+ }
+ } catch (error) {
+ console.error("标记已读失败:", error);
+ }
+ }
+
+ /**
+ * 置顶/取消置顶会话
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ * @param isPinned 是否置顶
+ */
+ static async togglePin(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ isPinned: number,
+ ): Promise {
+ try {
+ const serverId = `${type}_${sessionId}`;
+ const session = (await chatSessionService.findByPrimaryKey(
+ serverId,
+ )) as ChatSession;
+
+ if (session) {
+ const updatedSession = {
+ ...session,
+ config: {
+ ...session.config,
+ top: isPinned,
+ },
+ };
+
+ updatedSession.sortKey = this.generateSortKey(updatedSession);
+
+ await chatSessionService.update(serverId, updatedSession);
+ await this.triggerCallbacks(userId);
+ }
+ } catch (error) {
+ console.error("置顶操作失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 更新会话时间(用于联系人点击时更新)
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ * @param newTime 新的时间
+ */
+ static async updateSessionTime(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ newTime: string,
+ ): Promise {
+ try {
+ const serverId = `${type}_${sessionId}`;
+ const session = (await chatSessionService.findByPrimaryKey(
+ serverId,
+ )) as ChatSession;
+
+ if (session) {
+ const updatedSession = {
+ ...session,
+ lastUpdateTime: newTime,
+ };
+
+ // 重新生成 sortKey(因为时间变了,排序会改变)
+ updatedSession.sortKey = this.generateSortKey(updatedSession);
+
+ await chatSessionService.update(serverId, updatedSession);
+ console.log(`会话时间已更新: ${serverId} -> ${newTime}`);
+ }
+ } catch (error) {
+ console.error("更新会话时间失败:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 更新会话备注
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ * @param remark 新备注
+ */
+ static async updateRemark(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ remark: string,
+ ): Promise {
+ try {
+ const serverId = `${type}_${sessionId}`;
+ const session = (await chatSessionService.findByPrimaryKey(
+ serverId,
+ )) as ChatSession;
+
+ if (session) {
+ const updatedSession = {
+ ...session,
+ conRemark: remark,
+ };
+
+ updatedSession.sortKey = this.generateSortKey(updatedSession);
+
+ await chatSessionService.update(serverId, updatedSession);
+ await this.triggerCallbacks(userId);
+ }
+ } catch (error) {
+ console.error("更新备注失败:", error);
+ throw error;
+ }
+ }
+
+ // ==================== 批量操作优化 ====================
+
+ private static updateBuffer: Array<{
+ userId: number;
+ sessionId: number;
+ type: "friend" | "group";
+ updates: Partial;
+ }> = [];
+ private static bufferTimer: NodeJS.Timeout | null = null;
+
+ /**
+ * 批量更新会话(用于WebSocket消息批处理)
+ * @param userId 用户ID
+ * @param sessionId 会话ID
+ * @param type 会话类型
+ * @param updates 更新内容
+ */
+ static batchUpdateSession(
+ userId: number,
+ sessionId: number,
+ type: "friend" | "group",
+ updates: Partial,
+ ): void {
+ this.updateBuffer.push({ userId, sessionId, type, updates });
+
+ if (this.bufferTimer) {
+ clearTimeout(this.bufferTimer);
+ }
+
+ this.bufferTimer = setTimeout(async () => {
+ await this.flushUpdateBuffer();
+ }, 100); // 100ms批量处理
+ }
+
+ /**
+ * 刷新更新缓冲区
+ */
+ private static async flushUpdateBuffer() {
+ if (this.updateBuffer.length === 0) return;
+
+ const buffer = [...this.updateBuffer];
+ this.updateBuffer = [];
+
+ try {
+ await db.transaction("rw", [db.chatSessions], async () => {
+ for (const item of buffer) {
+ const serverId = `${item.type}_${item.sessionId}`;
+ const session = (await chatSessionService.findByPrimaryKey(
+ serverId,
+ )) as ChatSession;
+
+ if (session) {
+ const updatedSession = {
+ ...session,
+ ...item.updates,
+ };
+
+ updatedSession.sortKey = this.generateSortKey(updatedSession);
+
+ await chatSessionService.update(serverId, updatedSession);
+ }
+ }
+ });
+
+ // 触发回调
+ const userIds = new Set(buffer.map(item => item.userId));
+ for (const userId of userIds) {
+ await this.triggerCallbacks(userId);
+ }
+ } catch (error) {
+ console.error("批量更新会话失败:", error);
+ }
+ }
+
+ // ==================== 清理操作 ====================
+
+ /**
+ * 清空指定用户的所有会话
+ * @param userId 用户ID
+ */
+ static async clearUserSessions(userId: number): Promise {
+ try {
+ await db.chatSessions.where("userId").equals(userId).delete();
+ console.log(`用户 ${userId} 的会话数据已清空`);
+ } catch (error) {
+ console.error("清空用户会话失败:", error);
+ }
+ }
+
+ /**
+ * 根据联系人ID获取会话
+ * @param userId 用户ID
+ * @param contactId 联系人ID
+ * @param type 类型(friend/group)
+ */
+ static async getSessionByContactId(
+ userId: number,
+ contactId: number,
+ type: "friend" | "group",
+ ): Promise {
+ try {
+ const serverId = `${type}_${contactId}`;
+ const session = await chatSessionService.findByPrimaryKey(serverId);
+ return session as ChatSession | null;
+ } catch (error) {
+ console.error("根据联系人ID获取会话失败:", error);
+ return null;
+ }
+ }
+
+ /**
+ * 创建新会话
+ * @param userId 用户ID
+ * @param session 会话数据
+ */
+ static async createSession(
+ userId: number,
+ session: ChatSession,
+ ): Promise {
+ try {
+ // 生成 sortKey
+ const sessionWithSortKey = {
+ ...session,
+ sortKey: this.generateSortKey(session),
+ };
+
+ await chatSessionService.create(sessionWithSortKey);
+ console.log(`创建新会话: ${session.nickname || session.wechatId}`);
+ } catch (error) {
+ console.error("创建会话失败:", error);
+ throw error;
+ }
+ }
+}
diff --git a/Touchkebao/src/utils/filter.ts b/Touchkebao/src/utils/filter.ts
new file mode 100644
index 00000000..5e2d3754
--- /dev/null
+++ b/Touchkebao/src/utils/filter.ts
@@ -0,0 +1,64 @@
+//消息过滤器
+export const messageFilter = (message: string) => {
+ if (!message) return "";
+
+ try {
+ // 尝试解析为 JSON
+ const parsed = JSON.parse(message);
+
+ // 根据消息类型返回对应的显示文本
+ switch (true) {
+ // 图片消息:包含 previewImage 或 tencentUrl
+ case !!(parsed.previewImage || parsed.tencentUrl):
+ return "[图片]";
+
+ // 视频消息:包含 videoUrl 或 video(需要在语音消息之前判断)
+ case !!(parsed.videoUrl || parsed.video):
+ return "[视频]";
+
+ // 语音消息:包含 voiceUrl、voice 或 (url + durationMs)
+ case !!(
+ parsed.voiceUrl ||
+ parsed.voice ||
+ (parsed.url && parsed.durationMs)
+ ):
+ return parsed.text ? `[语音] ${parsed.text}` : "[语音]";
+
+ // 文件消息:包含 fileUrl 或 file
+ case !!(parsed.fileUrl || parsed.file):
+ return "[文件]";
+
+ // 表情消息:包含 emoji 或 emojiUrl
+ case !!(parsed.emoji || parsed.emojiUrl):
+ return "[表情]";
+
+ // 位置消息:包含 latitude 和 longitude
+ case !!(parsed.latitude && parsed.longitude):
+ return "[位置]";
+
+ // 链接消息:包含 linkUrl
+ case !!parsed.linkUrl:
+ return "[链接]";
+
+ // 文本消息:包含 text 或 content
+ case !!(parsed.text || parsed.content):
+ return parsed.text || parsed.content;
+
+ // 其他未知 JSON 格式
+ default:
+ return message;
+ }
+ } catch (error) {
+ // 如果不是 JSON 格式,检查是否为特殊格式
+ // 以 @ 开头的图片URL,例如:@https://...jpg
+ if (
+ message.startsWith("@") &&
+ /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(message)
+ ) {
+ return "[图片]";
+ }
+
+ // 其他情况直接返回原始消息
+ return message;
+ }
+};
diff --git a/Touchkebao/tsconfig.json b/Touchkebao/tsconfig.json
index 9d100420..5d6d8515 100644
--- a/Touchkebao/tsconfig.json
+++ b/Touchkebao/tsconfig.json
@@ -17,7 +17,11 @@
"jsx": "react-jsx",
"baseUrl": "./",
"paths": {
- "@/*": ["src/*"]
+ "@/*": ["src/*"],
+ "@weChatStore/*": ["src/store/module/weChat/*"],
+ "@storeModule/*": ["src/store/module/*"],
+ "@apiModule/*": ["src/api/module/*"],
+ "@utils/*": ["src/utils/*"]
}
},
"include": ["src"]
diff --git a/Touchkebao/vite.config.ts b/Touchkebao/vite.config.ts
index 7ab397b4..1431a796 100644
--- a/Touchkebao/vite.config.ts
+++ b/Touchkebao/vite.config.ts
@@ -7,6 +7,10 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve("src"),
+ "@storeModule": path.resolve("src/store/module/"),
+ "@weChatStore": path.resolve("src/store/module/weChat"),
+ "@apiModule": path.resolve("src/api/module/"),
+ "@utils": path.resolve("src/utils/"),
},
},
server: {