新增同步状态提示栏,优化消息列表组件的同步逻辑,支持逐页同步并即时更新UI,提升用户体验和代码可读性。

This commit is contained in:
超级老白兔
2025-11-28 16:32:23 +08:00
parent 9e76d39707
commit 23de3cac7e
3 changed files with 276 additions and 216 deletions

View File

@@ -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);
}
}

View File

@@ -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,19 +298,23 @@ 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) {
try {
const result: any = await getMessageList({
page,
limit,
@@ -329,47 +325,77 @@ const MessageList: React.FC<MessageListProps> = () => {
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 || "",
}));
// 立即同步这一页到数据库会触发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;
}
// 分离好友和群聊数据
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]);
// 继续下一页
page++;
if (page > 100) {
// 防止无限循环
hasMore = false;
}
}
}
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;
}
// 不等待同步完成让它在后台进行第一页数据同步后会立即更新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,46 +821,36 @@ 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%" }} />
// 渲染同步状态提示栏
const renderSyncStatusBar = () => (
<div className={styles.syncStatusBar}>
{syncing ? (
<div className={styles.syncStatusContent}>
<span className={styles.syncStatusText}>
<LoadingOutlined style={{ marginRight: "10px" }} /> ...
</span>
</div>
<Skeleton.Input
active
size="small"
style={{ width: "70%", marginTop: "8px" }}
) : (
<div className={styles.syncStatusContent}>
<span className={styles.syncStatusText}>
<CheckCircleOutlined
style={{ color: "green", marginRight: "10px" }}
/>
</span>
<span className={styles.syncButton} onClick={handleManualSync}>
</span>
</div>
</div>
))}
</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 ? (
// 加载状态:显示加载动画和骨架屏
renderLoading()
) : (
<>
{/* 同步状态提示栏 */}
{renderSyncStatusBar()}
<List
dataSource={filteredSessions as any[]}
renderItem={session => (
@@ -864,9 +879,7 @@ const MessageList: React.FC<MessageListProps> = () => {
<div className={styles.messageDetails}>
<div className={styles.messageHeader}>
<div className={styles.messageName}>
{session.conRemark ||
session.nickname ||
session.wechatId}
{session.conRemark || session.nickname || session.wechatId}
</div>
<div className={styles.messageTime}>
{formatWechatTime(session?.lastUpdateTime)}
@@ -879,6 +892,10 @@ const MessageList: React.FC<MessageListProps> = () => {
</div>
</List.Item>
)}
locale={{
emptyText:
filteredSessions.length === 0 && !syncing ? "暂无会话" : null,
}}
/>
{/* 右键菜单 */}
@@ -944,8 +961,6 @@ const MessageList: React.FC<MessageListProps> = () => {
maxLength={20}
/>
</Modal>
</>
)}
</div>
);
};

View File

@@ -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,12 +328,14 @@ export class MessageManager {
}
}
// 检查删除
// 检查删除(仅在非增量同步模式下执行)
if (!options?.skipDelete) {
for (const localSession of localSessions) {
if (!serverSessionMap.has(localSession.serverId)) {
toDelete.push(localSession.serverId);
}
}
}
// 4. 执行同步操作
let added = 0,