优化消息列表组件,增加加载状态的视觉反馈,支持根据微信号进行会话筛选,并重构相关逻辑以提升用户体验和代码可读性。

This commit is contained in:
超级老白兔
2025-11-28 15:33:32 +08:00
parent 5c7940538d
commit ce47856c81
11 changed files with 226 additions and 80 deletions

View File

@@ -21,7 +21,11 @@ import {
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./detail.module.scss";
import { getWechatAccountDetail, getWechatFriends, transferWechatFriends } from "./api";
import {
getWechatAccountDetail,
getWechatFriends,
transferWechatFriends,
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
@@ -36,7 +40,9 @@ const WechatAccountDetail: React.FC = () => {
const [accountInfo, setAccountInfo] = useState<any>(null);
const [showRestrictions, setShowRestrictions] = useState(false);
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>([]);
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
[],
);
const [inheritInfo, setInheritInfo] = useState(true);
const [transferLoading, setTransferLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
@@ -216,7 +222,7 @@ const WechatAccountDetail: React.FC = () => {
await transferWechatFriends({
wechatId: id,
devices: selectedDevices.map(device => device.id),
inherit: inheritInfo
inherit: inheritInfo,
});
Toast.show({
@@ -608,10 +614,7 @@ const WechatAccountDetail: React.FC = () => {
<div className={style["form-item"]}>
<div className={style["form-label"]}></div>
<div className={style["form-control-switch"]}>
<Switch
checked={inheritInfo}
onChange={setInheritInfo}
/>
<Switch checked={inheritInfo} onChange={setInheritInfo} />
<span className={style["switch-label"]}>
{inheritInfo ? "是" : "否"}
</span>

View File

@@ -28,10 +28,20 @@ instance.interceptors.request.use((config: any) => {
instance.interceptors.response.use(
(res: AxiosResponse) => {
const { code, success, msg } = res.data || {};
if (code === 200 || success) {
return res.data.data ?? res.data;
const payload = res.data || {};
const { code, success, msg } = payload;
const hasBizCode = typeof code === "number";
const hasBizSuccess = typeof success === "boolean";
const bizSuccess = hasBizCode
? code === 200
: hasBizSuccess
? success
: undefined;
if (bizSuccess === true || (!hasBizCode && !hasBizSuccess)) {
return payload.data ?? payload;
}
Toast.show({ content: msg || "接口错误", position: "top" });
if (code === 401) {
localStorage.removeItem("token");

View File

@@ -6,11 +6,14 @@ import {
WechatFriendAllot,
WechatFriendRebackAllot,
} from "@/pages/pc/ckbox/weChat/api";
import { dataProcessing } from "@/api/ai";
import { useCurrentContact } from "@/store/module/weChat/weChat";
import { ContactManager } from "@/utils/dbAction/contact";
import { MessageManager } from "@/utils/dbAction/message";
import { useUserStore } from "@/store/module/user";
import { useWeChatStore } from "@/store/module/weChat/weChat";
import { useMessageStore } from "@weChatStore/message";
import { useContactStore } from "@weChatStore/contacts";
const { TextArea } = Input;
const { Option } = Select;
@@ -37,6 +40,8 @@ const ToContract: React.FC<ToContractProps> = ({
const clearCurrentContact = useWeChatStore(
state => state.clearCurrentContact,
);
const removeSessionById = useMessageStore(state => state.removeSessionById);
const deleteContact = useContactStore(state => state.deleteContact);
const [visible, setVisible] = useState(false);
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
const [comment, setComment] = useState<string>("");
@@ -79,6 +84,12 @@ const ToContract: React.FC<ToContractProps> = ({
notifyReceiver: true,
comment: comment.trim(),
});
dataProcessing({
type: "CmdAllotFriend",
wechatChatroomId: currentContact.id,
toAccountId: selectedTarget as number,
wechatAccountId: currentContact.wechatAccountId,
});
} else {
await WechatFriendAllot({
wechatFriendId: currentContact.id,
@@ -86,6 +97,12 @@ const ToContract: React.FC<ToContractProps> = ({
notifyReceiver: true,
comment: comment.trim(),
});
dataProcessing({
type: "CmdAllotFriend",
wechatFriendId: currentContact.id,
toAccountId: selectedTarget as number,
wechatAccountId: currentContact.wechatAccountId,
});
}
}
@@ -97,7 +114,10 @@ const ToContract: React.FC<ToContractProps> = ({
const currentUserId = useUserStore.getState().user?.id || 0;
const contactType = "chatroomId" in currentContact ? "group" : "friend";
// 1. 从会话列表数据库删除
// 1. 立即从Store中删除会话更新UI
removeSessionById(currentContact.id, contactType);
// 2. 从会话列表数据库删除
await MessageManager.deleteSession(
currentUserId,
currentContact.id,
@@ -105,11 +125,19 @@ const ToContract: React.FC<ToContractProps> = ({
);
console.log("✅ 已从会话列表删除");
// 2. 从联系人数据库删除
// 3. 从联系人数据库删除
await ContactManager.deleteContact(currentContact.id);
console.log("✅ 已从联系人数据库删除");
// 3. 清空当前选中的联系人(关闭聊天窗口
// 4. 从联系人Store中删除更新联系人列表UI
try {
await deleteContact(currentContact.id);
console.log("✅ 已从联系人列表Store删除");
} catch (error) {
console.error("从联系人Store删除失败:", error);
}
// 5. 清空当前选中的联系人(关闭聊天窗口)
clearCurrentContact();
message.success("转接成功,已清理本地数据");
@@ -151,7 +179,10 @@ const ToContract: React.FC<ToContractProps> = ({
const currentUserId = useUserStore.getState().user?.id || 0;
const contactType = "chatroomId" in currentContact ? "group" : "friend";
// 1. 从会话列表数据库删除
// 1. 立即从Store中删除会话更新UI
removeSessionById(currentContact.id, contactType);
// 2. 从会话列表数据库删除
await MessageManager.deleteSession(
currentUserId,
currentContact.id,
@@ -159,11 +190,19 @@ const ToContract: React.FC<ToContractProps> = ({
);
console.log("✅ 已从会话列表删除");
// 2. 从联系人数据库删除
// 3. 从联系人数据库删除
await ContactManager.deleteContact(currentContact.id);
console.log("✅ 已从联系人数据库删除");
// 3. 清空当前选中的联系人(关闭聊天窗口
// 4. 从联系人Store中删除更新联系人列表UI
try {
await deleteContact(currentContact.id);
console.log("✅ 已从联系人列表Store删除");
} catch (error) {
console.error("从联系人Store删除失败:", error);
}
// 5. 清空当前选中的联系人(关闭聊天窗口)
clearCurrentContact();
message.success("转回成功,已清理本地数据");

View File

@@ -502,13 +502,10 @@ const QuickWords: React.FC<QuickWordsProps> = ({ onInsert }) => {
key: QuickWordsType.PERSONAL.toString(),
label: "个人快捷语",
},
{
key: QuickWordsType.PUBLIC.toString(),
label: "公共快捷语",
},
{
key: QuickWordsType.DEPARTMENT.toString(),
label: "部门快捷语",
label: "公司快捷语",
},
]}
/>

View File

@@ -188,6 +188,37 @@
text-align: center;
}
// 加载容器样式
.loadingContainer {
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
min-height: 400px;
position: relative;
padding-top: 40px;
:global(.ant-spin-container) {
width: 100%;
}
:global(.ant-spin-spinning) {
position: relative;
}
:global(.ant-spin-text) {
color: #1890ff;
font-size: 14px;
margin-top: 12px;
}
}
.loadingContent {
width: 100%;
height: 100%;
opacity: 0.6;
}
// 骨架屏样式
.skeletonContainer {
padding: 10px;

View File

@@ -1,5 +1,14 @@
import React, { useEffect, useState, useRef } from "react";
import { List, Avatar, Badge, Modal, Input, message, Skeleton } from "antd";
import {
List,
Avatar,
Badge,
Modal,
Input,
message,
Skeleton,
Spin,
} from "antd";
import {
UserOutlined,
TeamOutlined,
@@ -74,6 +83,7 @@ const MessageList: React.FC<MessageListProps> = () => {
const contextMenuRef = useRef<HTMLDivElement>(null);
const previousUserIdRef = useRef<number | null>(null);
const loadRequestRef = useRef(0);
const autoClickRef = useRef(false);
// 右键菜单事件处理
const handleContextMenu = (e: React.MouseEvent, session: ChatSession) => {
@@ -370,6 +380,7 @@ const MessageList: React.FC<MessageListProps> = () => {
previousUserIdRef.current = currentUserId;
setHasLoadedOnce(false);
setSessionState([]);
autoClickRef.current = false; // 重置自动点击标记
}, [currentUserId, setHasLoadedOnce, setSessionState]);
// 初始化加载会话列表
@@ -383,7 +394,7 @@ const MessageList: React.FC<MessageListProps> = () => {
const requestId = ++loadRequestRef.current;
const initializeSessions = async () => {
// setLoading(true);
setLoading(true);
try {
const cachedSessions =
@@ -416,7 +427,7 @@ const MessageList: React.FC<MessageListProps> = () => {
}
} finally {
if (!isCancelled && loadRequestRef.current === requestId) {
// setLoading(false);
setLoading(false);
}
}
};
@@ -447,25 +458,99 @@ const MessageList: React.FC<MessageListProps> = () => {
// 根据客服和搜索关键词筛选会话
useEffect(() => {
let filtered = [...sessions];
const filterSessions = async () => {
let filtered = [...sessions];
// 根据当前选中的客服筛选
if (currentCustomer && currentCustomer.id !== 0) {
filtered = filtered.filter(v => v.wechatAccountId === currentCustomer.id);
// 根据当前选中的客服筛选
if (currentCustomer && currentCustomer.id !== 0) {
filtered = filtered.filter(
v => v.wechatAccountId === currentCustomer.id,
);
}
// 根据搜索关键词进行模糊匹配(支持搜索昵称、备注名、微信号)
if (searchKeyword.trim()) {
const keyword = searchKeyword.toLowerCase();
// 如果搜索关键词可能是微信号,需要从联系人表补充 wechatId
const sessionsNeedingWechatId = filtered.filter(
v => !v.wechatId && v.type === "friend",
);
// 批量从联系人表获取 wechatId
if (sessionsNeedingWechatId.length > 0) {
const contactPromises = sessionsNeedingWechatId.map(session =>
ContactManager.getContactByIdAndType(
currentUserId,
session.id,
session.type,
),
);
const contacts = await Promise.all(contactPromises);
// 补充 wechatId 到会话数据
contacts.forEach((contact, index) => {
if (contact && contact.wechatId) {
const session = sessionsNeedingWechatId[index];
const sessionIndex = filtered.findIndex(
s => s.id === session.id && s.type === session.type,
);
if (sessionIndex !== -1) {
filtered[sessionIndex] = {
...filtered[sessionIndex],
wechatId: contact.wechatId,
};
}
}
});
}
filtered = filtered.filter(v => {
const nickname = (v.nickname || "").toLowerCase();
const conRemark = (v.conRemark || "").toLowerCase();
const wechatId = (v.wechatId || "").toLowerCase();
return (
nickname.includes(keyword) ||
conRemark.includes(keyword) ||
wechatId.includes(keyword)
);
});
}
setFilteredSessions(filtered);
};
filterSessions();
}, [sessions, currentCustomer, searchKeyword, currentUserId]);
// 渲染完毕后自动点击第一个聊天记录
useEffect(() => {
// 只在以下条件满足时自动点击:
// 1. 不在加载状态
// 2. 有过滤后的会话列表
// 3. 当前没有选中的联系人
// 4. 还没有自动点击过
// 5. 不在搜索状态(避免搜索时自动切换)
if (
!loading &&
filteredSessions.length > 0 &&
!currentContract &&
!autoClickRef.current &&
!searchKeyword.trim()
) {
// 延迟一点时间确保DOM已渲染
const timer = setTimeout(() => {
const firstSession = filteredSessions[0];
if (firstSession) {
autoClickRef.current = true;
onContactClick(firstSession);
}
}, 100);
return () => clearTimeout(timer);
}
// 根据搜索关键词进行模糊匹配
if (searchKeyword.trim()) {
const keyword = searchKeyword.toLowerCase();
filtered = filtered.filter(v => {
const nickname = (v.nickname || "").toLowerCase();
const conRemark = (v.conRemark || "").toLowerCase();
return nickname.includes(keyword) || conRemark.includes(keyword);
});
}
setFilteredSessions(filtered);
}, [sessions, currentCustomer, searchKeyword]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, filteredSessions, currentContract, searchKeyword]);
// ==================== WebSocket消息处理 ====================
@@ -735,11 +820,20 @@ const MessageList: React.FC<MessageListProps> = () => {
</div>
);
// 渲染加载中状态(带旋转动画)
const renderLoading = () => (
<div className={styles.loadingContainer}>
<Spin size="large" tip="加载中...">
<div className={styles.loadingContent}>{renderSkeleton()}</div>
</Spin>
</div>
);
return (
<div className={styles.messageList}>
{loading ? (
// 加载状态:显示骨架屏
renderSkeleton()
// 加载状态:显示加载动画和骨架屏
renderLoading()
) : (
<>
<List

View File

@@ -54,11 +54,7 @@ const CkboxPage: React.FC = () => {
</div>
) : (
<div className={styles.welcomeScreen}>
<div className={styles.welcomeContent}>
<MessageOutlined style={{ fontSize: 64, color: "#1890ff" }} />
<h2>使</h2>
<p></p>
</div>
<div className={styles.welcomeContent}></div>
</div>
)}
</Content>

View File

@@ -157,7 +157,7 @@ const messageHandlers: Record<string, MessageHandler> = {
CmdNotify: async (message: WebSocketMessage) => {
console.log("通知消息", message);
// 在这里添加具体的处理逻辑
if (message.notify == "Auth failed") {
if (["Auth failed", "Kicked out"].includes(message.notify)) {
// 避免重复弹窗
if ((window as any).__CKB_AUTH_FAILED_SHOWN__) {
return;

View File

@@ -351,36 +351,6 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
_handleMessage: (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
// console.log("收到WebSocket消息:", data);
// 处理特定的通知消息
if (data.cmdType === "CmdNotify") {
// 处理Auth failed通知
if (data.notify === "Auth failed" || data.notify === "Kicked out") {
// console.error(`WebSocket ${data.notify},断开连接`);
// Toast.show({
// content: `WebSocket ${data.notify},断开连接`,
// position: "top",
// });
// 禁用自动重连
if (get().config) {
set({
config: {
...get().config!,
autoReconnect: false,
},
});
}
// 停止客服状态查询定时器
get()._stopAliveStatusTimer();
// 断开连接
get().disconnect();
return;
}
}
const currentState = get();
const newMessage: WebSocketMessage = {

View File

@@ -40,6 +40,7 @@ export class ContactManager {
/**
* 搜索联系人
* 支持搜索昵称、备注名、微信号
*/
static async searchContacts(
userId: number,
@@ -52,8 +53,11 @@ export class ContactManager {
return contacts.filter(contact => {
const nickname = (contact.nickname || "").toLowerCase();
const conRemark = (contact.conRemark || "").toLowerCase();
const wechatId = (contact.wechatId || "").toLowerCase();
return (
nickname.includes(lowerKeyword) || conRemark.includes(lowerKeyword)
nickname.includes(lowerKeyword) ||
conRemark.includes(lowerKeyword) ||
wechatId.includes(lowerKeyword)
);
});
} catch (error) {

View File

@@ -231,6 +231,8 @@ export class MessageManager {
"phone",
"region",
"extendFields",
"wechatId", // 添加wechatId比较
"alias", // 添加alias比较
];
for (const field of fieldsToCompare) {