refactor(ckbox): 重构客服列表和联系人管理逻辑

- 将ctrlUserList重命名为kfUserList并优化相关接口
- 新增异步更新客服列表、联系人列表和会话列表的方法
- 重构联系人分组逻辑,支持按标签分组
- 优化会话列表排序规则,按最后更新时间降序排列
- 移除无用代码并简化组件逻辑
This commit is contained in:
超级老白兔
2025-08-28 15:18:18 +08:00
parent 3c1337b213
commit 640daf2431
10 changed files with 226 additions and 182 deletions

View File

@@ -23,7 +23,7 @@ export interface FormData {
name: string;
startTime: string; // 允许推送的开始时间
endTime: string; // 允许推送的结束时间
maxPerDay: number;
dailyPushCount: number;
pushOrder: number; // 1: 按最早, 2: 按最新
isLoop: number; // 0: 否, 1: 是
pushType: number; // 0: 定时推送, 1: 立即推送

View File

@@ -14,6 +14,10 @@ import {
//读取聊天信息
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
export function WechatGroup(params) {
return request("/api/WechatGroup/list", params, "GET");
}
//获取聊天记录-1 清除未读
export function clearUnreadCount(params) {
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
@@ -30,7 +34,7 @@ export function getMessages(params: {
return request("/api/FriendMessage/SearchMessage", params, "GET");
}
//获取群列表
export function getChatRoomList(params: { prevId: number; count: number }) {
export function getGroupList(params: { prevId: number; count: number }) {
return request(
"/api/wechatChatroom/listExcludeMembersByPage?",
params,

View File

@@ -4,7 +4,7 @@ import { UserOutlined, TeamOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
import styles from "./MessageList.module.scss";
import { formatWechatTime } from "@/utils/common";
interface MessageListProps {
chatSessions: ContractData[];
currentChat: ContractData;
@@ -16,22 +16,6 @@ const MessageList: React.FC<MessageListProps> = ({
currentChat,
onChatSelect,
}) => {
const formatTime = (timestamp: string) => {
const now = dayjs();
const messageTime = dayjs(timestamp);
const diffDays = now.diff(messageTime, "day");
if (diffDays === 0) {
return messageTime.format("HH:mm");
} else if (diffDays === 1) {
return "昨天";
} else if (diffDays < 7) {
return messageTime.format("ddd");
} else {
return messageTime.format("MM-DD");
}
};
return (
<div className={styles.messageList}>
<List
@@ -47,7 +31,7 @@ const MessageList: React.FC<MessageListProps> = ({
<Badge count={session.unreadCount} size="small">
<Avatar
size={48}
src={session.avatar}
src={session.avatar || session.chatroomAvatar}
icon={
session?.type === "group" ? (
<TeamOutlined />
@@ -61,7 +45,7 @@ const MessageList: React.FC<MessageListProps> = ({
<div className={styles.messageHeader}>
<div className={styles.messageName}>{session.nickname}</div>
<div className={styles.messageTime}>
{formatTime(session?.lastTime || "")}
{formatWechatTime(session?.lastUpdateTime)}
</div>
</div>
<div className={styles.messageContent}>

View File

@@ -31,7 +31,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({
const chatSessions = getChatSessions();
const [searchText, setSearchText] = useState("");
const [activeTab, setActiveTab] = useState("contracts");
const [activeTab, setActiveTab] = useState("chats");
const handleSearch = (value: string) => {
setSearchText(value);

View File

@@ -1,29 +1,17 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { Avatar, Badge, Tooltip } from "antd";
import styles from "./VerticalUserList.module.scss";
import { CkChatCtrlUserData } from "@/store/module/ckchat.data";
interface UserItem {
id: string;
name: string;
avatar: string;
messageCount?: number;
isOnline?: boolean;
}
import { useCkChatStore } from "@/store/module/ckchat";
interface VerticalUserListProps {
activeKfUserId: number;
onUserSelect: (userId: string) => void;
}
import { getCtrlUserList, useCkChatStore } from "@/store/module/ckchat";
const VerticalUserList: React.FC<VerticalUserListProps> = ({
activeKfUserId,
onUserSelect,
}) => {
// 直接从store获取ctrlUserList这样当store中的数据更新时组件会自动重新渲染
const ctrlUserList = useCkChatStore(state => state.ctrlUserList);
const [activeUserId, setActiveUserId] = useState<number>();
const [activeUserId, setActiveUserId] = useState<number>(activeKfUserId);
useEffect(() => {
console.log("控制终端用户列表更新:", ctrlUserList);
}, [ctrlUserList]);
// 格式化消息数量显示
const formatMessageCount = (count: number) => {
if (count > 99) return "99+";
@@ -34,6 +22,7 @@ const VerticalUserList: React.FC<VerticalUserListProps> = ({
setActiveUserId(userId);
onUserSelect(userId.toString());
};
const kfUserList = useCkChatStore(state => state.kfUserList);
return (
<div className={styles.verticalUserList}>
@@ -41,7 +30,7 @@ const VerticalUserList: React.FC<VerticalUserListProps> = ({
<div className={styles.allFriends}></div>
</div>
<div className={styles.userList}>
{ctrlUserList.map(user => (
{kfUserList.map(user => (
<Tooltip key={user.id} title={user.name} placement="right">
<div
className={`${styles.userItem} ${activeUserId === user.id ? styles.active : ""}`}

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Layout, Button, Space, message, Tooltip } from "antd";
import { Layout, Button, Space, message, Tooltip, Spin } from "antd";
import { InfoCircleOutlined, MessageOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import { ContractData } from "./data";
@@ -10,63 +10,12 @@ import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat";
const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main";
// 垂直侧边栏用户数据
const verticalUsers = [
{
id: "all",
name: "全部好友",
avatar: "",
messageCount: 99,
},
{
id: "game",
name: "游戏好友",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
messageCount: 99,
},
{
id: "card",
name: "卡片好友",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
messageCount: 99,
},
{
id: "dh",
name: "DH好友",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
id: "yp",
name: "用户多",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
id: "dq",
name: "当前",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
id: "rq",
name: "人气榜",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
id: "ai",
name: "AI助手",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
{
id: "hf",
name: "回复号码",
avatar: "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png",
},
];
const CkboxPage: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage();
const [contracts, setContacts] = useState<any[]>([]);
const [currentChat, setCurrentChat] = useState<ContractData | null>(null);
const [activeVerticalUserId, setActiveVerticalUserId] = useState("all");
const [activeVerticalUserId, setActiveVerticalUserId] = useState(0);
const [loading, setLoading] = useState(false);
const [showProfile, setShowProfile] = useState(true);
@@ -75,8 +24,8 @@ const CkboxPage: React.FC = () => {
// 方法一:使用 Promise 链式调用处理异步函数
setLoading(true);
chatInitAPIdata()
.then((response: { contractList: any[]; chatRoomList: any[] }) => {
const { contractList, chatRoomList } = response;
.then(response => {
const { contractList, chatRoomList, kfUserList } = response;
//找出已经在聊天的
const isChatList = contractList.filter(
v => (v?.config && v.config?.chat) || false,
@@ -133,10 +82,10 @@ const CkboxPage: React.FC = () => {
<Header className={styles.header}></Header>
<Layout>
{/* 垂直侧边栏 */}
<Sider width={60} className={styles.verticalSider}>
<VerticalUserList
users={verticalUsers}
activeUserId={activeVerticalUserId}
activeKfUserId={activeVerticalUserId}
onUserSelect={handleVerticalUserSelect}
/>
</Sider>
@@ -148,7 +97,6 @@ const CkboxPage: React.FC = () => {
currentChat={currentChat}
onContactClick={handleContactClick}
onChatSelect={setCurrentChat}
loading={loading}
/>
</Sider>

View File

@@ -1,62 +1,182 @@
import { addCtrlUser, useCkChatStore } from "@/store/module/ckchat";
import {
useCkChatStore,
asyncKfUserList,
asyncContractList,
asyncChatSessions,
} from "@/store/module/ckchat";
import { useWebSocketStore } from "@/store/module/websocket";
import {
loginWithToken,
getControlTerminalList,
getContactList,
getChatRoomList,
getGroupList,
WechatGroup,
} from "./api";
const { sendCommand } = useWebSocketStore.getState();
import { useUserStore } from "@/store/module/user";
import { CkChatCtrlUserData } from "@/store/module/ckchat.data";
import { KfUserListData } from "@/store/module/ckchat.data";
const { login2 } = useUserStore.getState();
const { connect } = useWebSocketStore.getState();
const { setUserInfo, getAccountId } = useCkChatStore.getState();
//获取触客宝基础信息
export const chatInitAPIdata = async () => {
try {
// //发起链接
// if (Token && accountId) {
// connect({
// url: "wss://kf.quwanzhi.com:9993", // 显式指定WebSocket URL确保使用正确的服务器地址
// accessToken: String(Token),
// accountId: accountId,
// client: "kefu-client",
// cmdType: "CmdSignIn",
// seq: +new Date(),
// });
// console.log("WebSocket连接已初始化");
// } else {
// console.error("WebSocket连接初始化失败缺少Token或accountId");
// }
//获取联系人列表
const contractList = await getAllContactList();
//构建联系人列表标签
const newContractList = await createContractList(contractList);
//获取联系人列表
asyncContractList(contractList);
// 提取不重复的wechatAccountId组
const uniqueWechatAccountIds: number[] =
getUniqueWechatAccountIds(contractList);
//获取控制终端列表
const controlTerminalList: CkChatCtrlUserData[] =
const kfUserList: KfUserListData[] =
await getControlTerminalListByWechatAccountIds(uniqueWechatAccountIds);
//添加控制终端用户
controlTerminalList.forEach(item => {
addCtrlUser(item);
});
//获取用户列表
asyncKfUserList(kfUserList);
//获取群列表
const chatRoomList = await getAllChatRoomList();
const groupList = await getAllGroupList();
//获取消息会话列表并按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) => {
// 如果lastUpdateTime不存在则将其排在最后
if (!a.lastUpdateTime) return 1;
if (!b.lastUpdateTime) return -1;
// 首先按时间降序排列(最新的在前面)
const timeCompare =
new Date(b.lastUpdateTime).getTime() -
new Date(a.lastUpdateTime).getTime();
// 如果时间相同,则按未读消息数量降序排列
if (timeCompare === 0) {
// 如果unreadCount不存在则将其排在后面
const aUnread = a.unreadCount || 0;
const bUnread = b.unreadCount || 0;
return bUnread - aUnread; // 未读消息多的排在前面
}
return timeCompare;
},
);
//会话数据同步
asyncChatSessions(sortedSessions);
return {
contractList,
chatRoomList,
groupList,
kfUserList,
newContractList,
};
} catch (error) {
console.error("获取联系人列表失败:", error);
return [];
}
};
//构建联系人列表标签
export const createContractList = async (contractList: any[]) => {
const LablesRes = await Promise.all(
[1, 2].map(item =>
WechatGroup({
groupType: item,
}),
),
);
const [friend, group] = LablesRes;
const countLables = [...friend, ...group];
// 根据countLables中的groupName整理contractList数据
// 返回按标签分组的联系人数组,包括未分组标签(在数组最后)
return organizeContactsByLabels(contractList, countLables);
};
/**
* 根据标签组织联系人
* @param contractList 联系人列表
* @param countLables 标签列表
* @returns 按标签分组的联系人
*/
export const organizeContactsByLabels = (
contractList: any[],
countLables: any[],
) => {
// 创建结果对象,用于存储按标签分组的联系人
const result: { [key: string]: any[] } = {};
// 初始化结果对象,为每个标签创建一个空数组
countLables.forEach(label => {
if (label && label.groupName) {
result[label.groupName] = [];
}
});
// 创建未分组标签,用于存放没有匹配到任何标签的联系人
const ungroupedLabel = "未分组";
result[ungroupedLabel] = [];
// 遍历联系人列表
contractList.forEach(contact => {
// 确保联系人有labels字段且是数组
if (contact && Array.isArray(contact.labels)) {
// 标记联系人是否已被分配到某个组
let isAssigned = false;
// 遍历标签列表
countLables.forEach(label => {
if (label && label.groupName) {
// 检查联系人的labels是否包含当前标签的groupName
if (contact.labels.includes(label.groupName)) {
// 将联系人添加到对应标签的数组中
result[label.groupName].push(contact);
isAssigned = true;
}
}
});
// 如果联系人没有被分配到任何组,则添加到未分组
if (!isAssigned) {
result[ungroupedLabel].push(contact);
}
} else {
// 如果联系人没有labels字段或不是数组也添加到未分组
result[ungroupedLabel].push(contact);
}
});
// 将结果转换为数组格式,确保未分组在最后
const resultArray = Object.entries(result).map(([groupName, contacts]) => ({
groupName,
contacts,
}));
// 将未分组移到数组末尾
const ungroupedIndex = resultArray.findIndex(
item => item.groupName === ungroupedLabel,
);
if (ungroupedIndex !== -1) {
const ungrouped = resultArray.splice(ungroupedIndex, 1)[0];
resultArray.push(ungrouped);
}
return resultArray;
};
//获取控制终端列表
export const getControlTerminalListByWechatAccountIds = (
WechatAccountIds: number[],
@@ -126,7 +246,7 @@ export const getUniqueWechatAccountIds = contacts => {
return Array.from(uniqueAccountIdsSet);
};
// 递归获取所有群列表
export const getAllChatRoomList = async () => {
export const getAllGroupList = async () => {
try {
let allContacts = [];
let prevId = 0;
@@ -134,7 +254,7 @@ export const getAllChatRoomList = async () => {
let hasMore = true;
while (hasMore) {
const contractList = await getChatRoomList({
const contractList = await getGroupList({
prevId,
count,
});

View File

@@ -1,61 +1,27 @@
import { ContractData } from "../../pages/pc/ckbox/data";
//终端用户数据接口
export interface CkChatCtrlUserData {
/** 用户唯一标识ID */
export interface KfUserListData {
id: number;
/** 租户ID多租户系统中用于区分租户 */
tenantId: number;
/** 微信ID用户微信账号唯一标识 */
wechatId: string;
/** 用户昵称 */
nickname: string;
/** 用户别名/备注名(自定义标识) */
alias: string;
/** 头像图片URL阿里云OSS存储地址 */
avatar: string;
/**
* 性别标识0未知/未设置12符合微信性别字段定义
* 原始数据中为0此处标注可能的枚举值以便后续扩展
*/
gender: 0 | 1 | 2;
/** 地区信息(原始数据为空字符串,未填写) */
gender: number;
region: string;
/** 个性签名(原始数据为空字符串,未填写) */
signature: string;
/** 绑定的QQ号0表示未绑定 */
bindQQ: string;
/** 绑定的邮箱(原始数据为空字符串,未绑定) */
bindEmail: string;
/** 绑定的手机号(原始数据为空字符串,未绑定) */
bindMobile: string;
/**
* 用户创建时间(注册时间)
* 格式ISO 8601标准时间字符串YYYY-MM-DDTHH:mm:ss.fffffff
*/
createTime: string;
/** 当前登录设备ID关联用户使用的设备唯一标识 */
currentDeviceId: number;
/** 是否删除逻辑删除标识false未删除true已删除 */
isDeleted: boolean;
/**
* 删除时间(逻辑删除时记录,未删除时为默认初始时间"0001-01-01T00:00:00"
* 格式ISO 8601标准时间字符串
*/
deleteTime: string;
/** 用户所属群组ID用于群组分类管理 */
groupId: number;
/** 系统内对用户的备注信息(补充标识) */
memo: string;
/** 用户当前使用的微信版本号 */
wechatVersion: string;
/**
* 用户标签列表(用于分类、筛选,涵盖:业务类型、互动记录、身份标识等维度)
* 例如:"团队"(身份)、"未成交"(业务状态)、"抖音"(渠道)等
*/
labels: string[];
/**
* 最后更新时间(用户信息最近修改时间)
* 格式ISO 8601标准时间字符串
*/
lastUpdateTime: string;
[key: string]: any;
}
@@ -102,6 +68,20 @@ export interface CkUserInfo {
export interface CkChatState {
userInfo: CkUserInfo | null;
isLoggedIn: boolean;
contractList: ContractData[];
chatSessions: any[];
kfUserList: KfUserListData[];
getkfUserList: () => KfUserListData[];
asyncKfUserList: (data: KfUserListData[]) => void;
asyncContractList: (data: ContractData[]) => void;
asyncChatSessions: (data: any[]) => void;
deleteCtrlUser: (userId: number) => void;
updateCtrlUser: (user: KfUserListData) => void;
clearkfUserList: () => void;
getChatSessions: () => any[];
addChatSession: (session: any) => void;
updateChatSession: (session: any) => void;
deleteChatSession: (sessionId: string) => void;
setUserInfo: (userInfo: CkUserInfo) => void;
clearUserInfo: () => void;
updateAccount: (account: Partial<CkAccount>) => void;

View File

@@ -5,40 +5,50 @@ import {
CkUserInfo,
CkAccount,
CkTenant,
CkChatCtrlUserData,
KfUserListData,
} from "./ckchat.data";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
export const useCkChatStore = createPersistStore<CkChatState>(
set => ({
userInfo: null,
isLoggedIn: false,
chatSessions: [],
ctrlUserList: [],
// 控制终端用户列表
getCtrlUserList: () => {
const state = useCkChatStore.getState();
return state.ctrlUserList;
contractList: [], //联系人列表
chatSessions: [], //聊天会话
kfUserList: [], //客服列表
// 异步设置会话列表
asyncChatSessions: data => {
set({ chatSessions: data });
},
// 添加控制终端用户
addCtrlUser: (user: CkChatCtrlUserData) => {
set(state => ({
ctrlUserList: [...state.ctrlUserList, user],
}));
// 异步设置联系人列表
asyncContractList: data => {
set({ contractList: data });
},
// 控制终端用户列表
getkfUserList: () => {
const state = useCkChatStore.getState();
return state.kfUserList;
},
asyncKfUserList: data => {
set({ kfUserList: data });
},
// 删除控制终端用户
deleteCtrlUser: (userId: number) => {
set(state => ({
ctrlUserList: state.ctrlUserList.filter(item => item.id !== userId),
kfUserList: state.kfUserList.filter(item => item.id !== userId),
}));
},
// 更新控制终端用户
updateCtrlUser: (user: CkChatCtrlUserData) => {
updateCtrlUser: (user: KfUserListData) => {
set(state => ({
ctrlUserList: state.ctrlUserList.map(item =>
kfUserList: state.kfUserList.map(item =>
item.id === user.id ? user : item,
),
}));
},
// 清空控制终端用户列表
clearkfUserList: () => {
set({ kfUserList: [] });
},
// 获取聊天会话
getChatSessions: () => {
const state = useCkChatStore.getState();
@@ -159,10 +169,16 @@ export const updateChatSession = (session: ContractData | GroupData) =>
useCkChatStore.getState().updateChatSession(session);
export const deleteChatSession = (sessionId: string) =>
useCkChatStore.getState().deleteChatSession(sessionId);
export const getCtrlUserList = () => useCkChatStore.getState().ctrlUserList;
export const addCtrlUser = (user: CkChatCtrlUserData) =>
export const getkfUserList = () => useCkChatStore.getState().kfUserList;
export const addCtrlUser = (user: KfUserListData) =>
useCkChatStore.getState().addCtrlUser(user);
export const deleteCtrlUser = (userId: number) =>
useCkChatStore.getState().deleteCtrlUser(userId);
export const updateCtrlUser = (user: CkChatCtrlUserData) =>
export const updateCtrlUser = (user: KfUserListData) =>
useCkChatStore.getState().updateCtrlUser(user);
export const asyncKfUserList = (data: KfUserListData[]) =>
useCkChatStore.getState().asyncKfUserList(data);
export const asyncContractList = (data: ContractData[]) =>
useCkChatStore.getState().asyncContractList(data);
export const asyncChatSessions = (data: ContractData[]) =>
useCkChatStore.getState().asyncChatSessions(data);

View File

@@ -1,6 +1,9 @@
import { Modal } from "antd-mobile";
import { getSetting } from "@/store/module/settings";
export function formatWechatTime(timestamp) {
if (!timestamp) {
return "";
}
// 处理时间戳(兼容秒级/毫秒级)
const date = new Date(
timestamp.toString().length === 10 ? timestamp * 1000 : timestamp,