优化消息列表组件,增加加载状态的视觉反馈,支持根据微信号进行会话筛选,并重构相关逻辑以提升用户体验和代码可读性。
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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("转回成功,已清理本地数据");
|
||||
|
||||
@@ -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: "公司快捷语",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -231,6 +231,8 @@ export class MessageManager {
|
||||
"phone",
|
||||
"region",
|
||||
"extendFields",
|
||||
"wechatId", // 添加wechatId比较
|
||||
"alias", // 添加alias比较
|
||||
];
|
||||
|
||||
for (const field of fieldsToCompare) {
|
||||
|
||||
Reference in New Issue
Block a user