新增同步状态提示栏,优化消息列表组件的同步逻辑,支持逐页同步并即时更新UI,提升用户体验和代码可读性。
This commit is contained in:
@@ -242,3 +242,41 @@
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
// 同步状态提示栏样式
|
||||
.syncStatusBar {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background-color: #fafafa;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.syncStatusContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.syncStatusText {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.syncButton {
|
||||
color: green;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
color: green;
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import {
|
||||
List,
|
||||
Avatar,
|
||||
Badge,
|
||||
Modal,
|
||||
Input,
|
||||
message,
|
||||
Skeleton,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import { List, Avatar, Badge, Modal, Input, message } from "antd";
|
||||
import {
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
PushpinOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LoadingOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import styles from "./com.module.scss";
|
||||
import {
|
||||
@@ -47,14 +40,13 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
|
||||
// Store状态
|
||||
const {
|
||||
loading,
|
||||
hasLoadedOnce,
|
||||
setLoading,
|
||||
setHasLoadedOnce,
|
||||
sessions,
|
||||
setSessions: setSessionState,
|
||||
} = useMessageStore();
|
||||
const [filteredSessions, setFilteredSessions] = useState<ChatSession[]>([]);
|
||||
const [syncing, setSyncing] = useState(false); // 同步状态
|
||||
|
||||
// 右键菜单相关状态
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
@@ -306,70 +298,104 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
|
||||
// 与服务器同步数据
|
||||
// 与服务器同步数据(优化版:逐页同步,立即更新UI)
|
||||
const syncWithServer = async () => {
|
||||
if (!currentUserId) return;
|
||||
|
||||
setSyncing(true); // 开始同步,显示同步状态栏
|
||||
|
||||
try {
|
||||
// 获取会话列表数据(分页获取所有数据)
|
||||
let allMessages: any[] = [];
|
||||
let page = 1;
|
||||
const limit = 500;
|
||||
let hasMore = true;
|
||||
let totalProcessed = 0;
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
// 分页获取会话列表
|
||||
// 分页获取会话列表,每页成功后立即同步
|
||||
while (hasMore) {
|
||||
const result: any = await getMessageList({
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
try {
|
||||
const result: any = await getMessageList({
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
if (!result || !Array.isArray(result) || result.length === 0) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
if (!result || !Array.isArray(result) || result.length === 0) {
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
allMessages = [...allMessages, ...result];
|
||||
// 立即处理这一页的数据
|
||||
const friends = result.filter(
|
||||
(msg: any) => msg.dataType === "friend" || !msg.chatroomId,
|
||||
);
|
||||
const groups = result
|
||||
.filter((msg: any) => msg.dataType === "group" || msg.chatroomId)
|
||||
.map((msg: any) => ({
|
||||
...msg,
|
||||
chatroomAvatar: msg.chatroomAvatar || msg.avatar || "",
|
||||
}));
|
||||
|
||||
if (result.length < limit) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
// 立即同步这一页到数据库(会触发UI更新)
|
||||
// 分页同步时跳过删除检查,避免误删其他页的会话
|
||||
await MessageManager.syncSessions(
|
||||
currentUserId,
|
||||
{
|
||||
friends,
|
||||
groups,
|
||||
},
|
||||
{ skipDelete: true },
|
||||
);
|
||||
|
||||
totalProcessed += result.length;
|
||||
successCount++;
|
||||
|
||||
// 判断是否还有下一页
|
||||
if (result.length < limit) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略单页失败,继续处理下一页
|
||||
console.error(`第${page}页同步失败:`, error);
|
||||
failCount++;
|
||||
|
||||
// 如果连续失败太多,停止同步
|
||||
if (failCount >= 3) {
|
||||
console.warn("连续失败次数过多,停止同步");
|
||||
break;
|
||||
}
|
||||
|
||||
// 继续下一页
|
||||
page++;
|
||||
if (page > 100) {
|
||||
// 防止无限循环
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分离好友和群聊数据
|
||||
const friends = allMessages.filter(
|
||||
(msg: any) => msg.dataType === "friend" || !msg.chatroomId,
|
||||
);
|
||||
const groups = allMessages
|
||||
.filter((msg: any) => msg.dataType === "group" || msg.chatroomId)
|
||||
.map((msg: any) => {
|
||||
// 确保群聊数据包含正确的头像字段
|
||||
// 如果接口返回的是 avatar 字段,需要映射到 chatroomAvatar
|
||||
return {
|
||||
...msg,
|
||||
chatroomAvatar: msg.chatroomAvatar || msg.avatar || "",
|
||||
};
|
||||
});
|
||||
|
||||
// 执行增量同步
|
||||
const syncResult = await MessageManager.syncSessions(currentUserId, {
|
||||
friends,
|
||||
groups,
|
||||
});
|
||||
|
||||
// 同步后验证数据
|
||||
const verifySession = await MessageManager.getUserSessions(currentUserId);
|
||||
console.log("同步后的会话数据示例:", verifySession[0]);
|
||||
|
||||
console.log(
|
||||
`会话同步完成: 新增${syncResult.added}, 更新${syncResult.updated}, 删除${syncResult.deleted}`,
|
||||
`会话同步完成: 成功${successCount}页, 失败${failCount}页, 共处理${totalProcessed}条数据`,
|
||||
);
|
||||
|
||||
// 会话管理器会在有变更时触发订阅回调
|
||||
} catch (error) {
|
||||
console.error("同步服务器数据失败:", error);
|
||||
} finally {
|
||||
setSyncing(false); // 同步完成,更新状态栏
|
||||
}
|
||||
};
|
||||
|
||||
// 手动触发同步的函数
|
||||
const handleManualSync = async () => {
|
||||
if (syncing) return; // 如果正在同步,不重复触发
|
||||
setSyncing(true);
|
||||
try {
|
||||
await syncWithServer();
|
||||
} catch (error) {
|
||||
console.error("手动同步失败:", error);
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -394,8 +420,6 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
const requestId = ++loadRequestRef.current;
|
||||
|
||||
const initializeSessions = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const cachedSessions =
|
||||
await MessageManager.getUserSessions(currentUserId);
|
||||
@@ -404,6 +428,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 有缓存数据立即显示
|
||||
if (cachedSessions.length > 0) {
|
||||
setSessionState(cachedSessions);
|
||||
}
|
||||
@@ -411,12 +436,18 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
const needsFullSync = cachedSessions.length === 0 || !hasLoadedOnce;
|
||||
|
||||
if (needsFullSync) {
|
||||
await syncWithServer();
|
||||
if (isCancelled || loadRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
setHasLoadedOnce(true);
|
||||
// 不等待同步完成,让它在后台进行,第一页数据同步后会立即更新UI
|
||||
syncWithServer()
|
||||
.then(() => {
|
||||
if (!isCancelled && loadRequestRef.current === requestId) {
|
||||
setHasLoadedOnce(true);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("同步失败:", error);
|
||||
});
|
||||
} else {
|
||||
// 后台同步
|
||||
syncWithServer().catch(error => {
|
||||
console.error("后台同步失败:", error);
|
||||
});
|
||||
@@ -425,10 +456,6 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
if (!isCancelled) {
|
||||
console.error("初始化会话列表失败:", error);
|
||||
}
|
||||
} finally {
|
||||
if (!isCancelled && loadRequestRef.current === requestId) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -526,13 +553,11 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
// 渲染完毕后自动点击第一个聊天记录
|
||||
useEffect(() => {
|
||||
// 只在以下条件满足时自动点击:
|
||||
// 1. 不在加载状态
|
||||
// 2. 有过滤后的会话列表
|
||||
// 3. 当前没有选中的联系人
|
||||
// 4. 还没有自动点击过
|
||||
// 5. 不在搜索状态(避免搜索时自动切换)
|
||||
// 1. 有过滤后的会话列表
|
||||
// 2. 当前没有选中的联系人
|
||||
// 3. 还没有自动点击过
|
||||
// 4. 不在搜索状态(避免搜索时自动切换)
|
||||
if (
|
||||
!loading &&
|
||||
filteredSessions.length > 0 &&
|
||||
!currentContract &&
|
||||
!autoClickRef.current &&
|
||||
@@ -550,7 +575,7 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loading, filteredSessions, currentContract, searchKeyword]);
|
||||
}, [filteredSessions, currentContract, searchKeyword]);
|
||||
|
||||
// ==================== WebSocket消息处理 ====================
|
||||
|
||||
@@ -796,156 +821,146 @@ const MessageList: React.FC<MessageListProps> = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染骨架屏
|
||||
const renderSkeleton = () => (
|
||||
<div className={styles.skeletonContainer}>
|
||||
{Array(8)
|
||||
.fill(null)
|
||||
.map((_, index) => (
|
||||
<div key={`skeleton-${index}`} className={styles.skeletonItem}>
|
||||
<Skeleton.Avatar active size={48} shape="circle" />
|
||||
<div className={styles.skeletonContent}>
|
||||
<div className={styles.skeletonHeader}>
|
||||
<Skeleton.Input active size="small" style={{ width: "40%" }} />
|
||||
<Skeleton.Input active size="small" style={{ width: "20%" }} />
|
||||
</div>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
style={{ width: "70%", marginTop: "8px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染加载中状态(带旋转动画)
|
||||
const renderLoading = () => (
|
||||
<div className={styles.loadingContainer}>
|
||||
<Spin size="large" tip="加载中...">
|
||||
<div className={styles.loadingContent}>{renderSkeleton()}</div>
|
||||
</Spin>
|
||||
// 渲染同步状态提示栏
|
||||
const renderSyncStatusBar = () => (
|
||||
<div className={styles.syncStatusBar}>
|
||||
{syncing ? (
|
||||
<div className={styles.syncStatusContent}>
|
||||
<span className={styles.syncStatusText}>
|
||||
<LoadingOutlined style={{ marginRight: "10px" }} /> 同步中...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.syncStatusContent}>
|
||||
<span className={styles.syncStatusText}>
|
||||
<CheckCircleOutlined
|
||||
style={{ color: "green", marginRight: "10px" }}
|
||||
/>
|
||||
同步完成
|
||||
</span>
|
||||
<span className={styles.syncButton} onClick={handleManualSync}>
|
||||
同步
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.messageList}>
|
||||
{loading ? (
|
||||
// 加载状态:显示加载动画和骨架屏
|
||||
renderLoading()
|
||||
) : (
|
||||
<>
|
||||
<List
|
||||
dataSource={filteredSessions as any[]}
|
||||
renderItem={session => (
|
||||
<List.Item
|
||||
key={session.id}
|
||||
className={`${styles.messageItem} ${
|
||||
currentContract?.id === session.id ? styles.active : ""
|
||||
} ${(session.config as any)?.top ? styles.pinned : ""}`}
|
||||
onClick={() => onContactClick(session)}
|
||||
onContextMenu={e => handleContextMenu(e, session)}
|
||||
>
|
||||
<div className={styles.messageInfo}>
|
||||
<Badge count={session.config.unreadCount || 0} size="small">
|
||||
<Avatar
|
||||
size={48}
|
||||
src={session.avatar}
|
||||
icon={
|
||||
session?.type === "group" ? (
|
||||
<TeamOutlined />
|
||||
) : (
|
||||
<UserOutlined />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Badge>
|
||||
<div className={styles.messageDetails}>
|
||||
<div className={styles.messageHeader}>
|
||||
<div className={styles.messageName}>
|
||||
{session.conRemark ||
|
||||
session.nickname ||
|
||||
session.wechatId}
|
||||
</div>
|
||||
<div className={styles.messageTime}>
|
||||
{formatWechatTime(session?.lastUpdateTime)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.messageContent}>
|
||||
{messageFilter(session.content)}
|
||||
</div>
|
||||
{/* 同步状态提示栏 */}
|
||||
{renderSyncStatusBar()}
|
||||
|
||||
<List
|
||||
dataSource={filteredSessions as any[]}
|
||||
renderItem={session => (
|
||||
<List.Item
|
||||
key={session.id}
|
||||
className={`${styles.messageItem} ${
|
||||
currentContract?.id === session.id ? styles.active : ""
|
||||
} ${(session.config as any)?.top ? styles.pinned : ""}`}
|
||||
onClick={() => onContactClick(session)}
|
||||
onContextMenu={e => handleContextMenu(e, session)}
|
||||
>
|
||||
<div className={styles.messageInfo}>
|
||||
<Badge count={session.config.unreadCount || 0} size="small">
|
||||
<Avatar
|
||||
size={48}
|
||||
src={session.avatar}
|
||||
icon={
|
||||
session?.type === "group" ? (
|
||||
<TeamOutlined />
|
||||
) : (
|
||||
<UserOutlined />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Badge>
|
||||
<div className={styles.messageDetails}>
|
||||
<div className={styles.messageHeader}>
|
||||
<div className={styles.messageName}>
|
||||
{session.conRemark || session.nickname || session.wechatId}
|
||||
</div>
|
||||
<div className={styles.messageTime}>
|
||||
{formatWechatTime(session?.lastUpdateTime)}
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{contextMenu.visible && contextMenu.session && (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className={styles.contextMenu}
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: contextMenu.x,
|
||||
top: contextMenu.y,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleTogglePin(contextMenu.session!)}
|
||||
>
|
||||
<PushpinOutlined />
|
||||
{(contextMenu.session.config as any)?.top ? "取消置顶" : "置顶"}
|
||||
</div>
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleEditRemark(contextMenu.session!)}
|
||||
>
|
||||
<EditOutlined />
|
||||
修改备注
|
||||
</div>
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleDelete(contextMenu.session!)}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
<div className={styles.messageContent}>
|
||||
{messageFilter(session.content)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List.Item>
|
||||
)}
|
||||
locale={{
|
||||
emptyText:
|
||||
filteredSessions.length === 0 && !syncing ? "暂无会话" : null,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 修改备注Modal */}
|
||||
<Modal
|
||||
title="修改备注"
|
||||
open={editRemarkModal.visible}
|
||||
onOk={handleSaveRemark}
|
||||
onCancel={() =>
|
||||
setEditRemarkModal({
|
||||
visible: false,
|
||||
session: null,
|
||||
remark: "",
|
||||
})
|
||||
}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
{/* 右键菜单 */}
|
||||
{contextMenu.visible && contextMenu.session && (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className={styles.contextMenu}
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: contextMenu.x,
|
||||
top: contextMenu.y,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleTogglePin(contextMenu.session!)}
|
||||
>
|
||||
<Input
|
||||
value={editRemarkModal.remark}
|
||||
onChange={e =>
|
||||
setEditRemarkModal(prev => ({
|
||||
...prev,
|
||||
remark: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="请输入备注"
|
||||
maxLength={20}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
<PushpinOutlined />
|
||||
{(contextMenu.session.config as any)?.top ? "取消置顶" : "置顶"}
|
||||
</div>
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleEditRemark(contextMenu.session!)}
|
||||
>
|
||||
<EditOutlined />
|
||||
修改备注
|
||||
</div>
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
onClick={() => handleDelete(contextMenu.session!)}
|
||||
>
|
||||
<DeleteOutlined />
|
||||
删除
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 修改备注Modal */}
|
||||
<Modal
|
||||
title="修改备注"
|
||||
open={editRemarkModal.visible}
|
||||
onOk={handleSaveRemark}
|
||||
onCancel={() =>
|
||||
setEditRemarkModal({
|
||||
visible: false,
|
||||
session: null,
|
||||
remark: "",
|
||||
})
|
||||
}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Input
|
||||
value={editRemarkModal.remark}
|
||||
onChange={e =>
|
||||
setEditRemarkModal(prev => ({
|
||||
...prev,
|
||||
remark: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="请输入备注"
|
||||
maxLength={20}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -259,6 +259,8 @@ export class MessageManager {
|
||||
* 增量同步会话数据
|
||||
* @param userId 用户ID
|
||||
* @param serverData 服务器数据
|
||||
* @param options 同步选项
|
||||
* @param options.skipDelete 是否跳过删除检查(用于分页增量同步)
|
||||
* @returns 同步结果统计
|
||||
*/
|
||||
static async syncSessions(
|
||||
@@ -267,6 +269,9 @@ export class MessageManager {
|
||||
friends?: ContractData[];
|
||||
groups?: weChatGroup[];
|
||||
},
|
||||
options?: {
|
||||
skipDelete?: boolean; // 是否跳过删除检查(用于分页增量同步)
|
||||
},
|
||||
): Promise<{
|
||||
added: number;
|
||||
updated: number;
|
||||
@@ -323,10 +328,12 @@ export class MessageManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查删除
|
||||
for (const localSession of localSessions) {
|
||||
if (!serverSessionMap.has(localSession.serverId)) {
|
||||
toDelete.push(localSession.serverId);
|
||||
// 检查删除(仅在非增量同步模式下执行)
|
||||
if (!options?.skipDelete) {
|
||||
for (const localSession of localSessions) {
|
||||
if (!serverSessionMap.has(localSession.serverId)) {
|
||||
toDelete.push(localSession.serverId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user