重构SidebarMenu组件,移除不必要的useEffect,优化消息列表和联系人组件的加载逻辑,合并样式文件以提升代码可读性和维护性。

This commit is contained in:
超级老白兔
2025-10-23 20:42:29 +08:00
parent 8941b87e03
commit 3b82908e8a
8 changed files with 296 additions and 203 deletions

View File

@@ -187,3 +187,27 @@
font-size: 12px;
text-align: center;
}
// 骨架屏样式
.skeletonContainer {
padding: 10px;
}
.skeletonItem {
display: flex;
align-items: flex-start;
padding: 12px 16px;
gap: 12px;
border-bottom: 1px solid #f0f0f0;
}
.skeletonContent {
flex: 1;
}
.skeletonHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from "react";
import { List, Avatar, Badge, Modal, Input, message, Spin } from "antd";
import { List, Avatar, Badge, Modal, Input, message, Skeleton } from "antd";
import {
UserOutlined,
TeamOutlined,
@@ -7,21 +7,19 @@ import {
DeleteOutlined,
EditOutlined,
} from "@ant-design/icons";
import styles from "./com.module.scss";
import { useWeChatStore } from "@weChatStore/weChat";
import { useMessageStore } from "@weChatStore/message";
import { useCustomerStore } from "@weChatStore/customer";
import { useContactStore } from "@weChatStore/contacts";
import { useWebSocketStore } from "@/store/module/websocket/websocket";
import { updateConfig } from "@/pages/pc/ckbox/api";
import { getMessageList } from "./api";
import { dataProcessing } from "./api";
import styles from "./MessageList.module.scss";
import { getMessageList, dataProcessing } from "./api";
import { formatWechatTime } from "@/utils/common";
import { MessageManager } from "@/utils/dbAction/message";
import { ChatSession } from "@/utils/db";
import { useUserStore } from "@/store/module/user";
interface MessageListProps {}
const MessageList: React.FC<MessageListProps> = () => {
@@ -33,8 +31,13 @@ const MessageList: React.FC<MessageListProps> = () => {
const currentUserId = user?.id || 0;
// Store状态
const { loading, refreshing, refreshTrigger, setLoading, setRefreshing } =
useMessageStore();
const {
loading,
refreshTrigger,
hasLoadedOnce,
setLoading,
setHasLoadedOnce,
} = useMessageStore();
// 组件内部状态:会话列表数据
const [sessions, setSessions] = useState<ChatSession[]>([]);
@@ -346,6 +349,18 @@ const MessageList: React.FC<MessageListProps> = () => {
const initializeSessions = async () => {
if (!currentUserId) return;
// 如果已经加载过一次,只从本地数据库读取,不请求接口
if (hasLoadedOnce) {
try {
const cachedSessions =
await MessageManager.getUserSessions(currentUserId);
setSessions(cachedSessions);
} catch (error) {
console.error("从本地加载会话列表失败:", error);
}
return;
}
setLoading(true);
try {
@@ -358,9 +373,9 @@ const MessageList: React.FC<MessageListProps> = () => {
setSessions(cachedSessions);
setLoading(false);
// 2. 后台静默同步
setRefreshing(true);
// 2. 后台静默同步(不显示同步提示)
await syncWithServer();
setHasLoadedOnce(true); // 标记已加载过
} else {
// 无缓存直接API加载
await syncWithServer();
@@ -368,12 +383,11 @@ const MessageList: React.FC<MessageListProps> = () => {
await MessageManager.getUserSessions(currentUserId);
setSessions(newSessions);
setLoading(false);
setHasLoadedOnce(true); // 标记已加载过
}
} catch (error) {
console.error("初始化会话列表失败:", error);
setLoading(false);
} finally {
setRefreshing(false);
}
};
@@ -522,117 +536,147 @@ 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>
);
return (
<div className={styles.messageList}>
{loading && <div className={styles.loading}>...</div>}
{refreshing && !loading && (
<div className={styles.refreshingTip}>...</div>
)}
<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 || session.chatroomAvatar}
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)}
{loading ? (
// 加载状态:显示骨架屏
renderSkeleton()
) : (
<>
<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 || session.chatroomAvatar}
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}>
{session.content}
</div>
</div>
</div>
<div className={styles.messageContent}>{session.content}</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>
</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!)}
{/* 修改备注Modal */}
<Modal
title="修改备注"
open={editRemarkModal.visible}
onOk={handleSaveRemark}
onCancel={() =>
setEditRemarkModal({
visible: false,
session: null,
remark: "",
})
}
okText="保存"
cancelText="取消"
>
<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>
<Input
value={editRemarkModal.remark}
onChange={e =>
setEditRemarkModal(prev => ({
...prev,
remark: e.target.value,
}))
}
placeholder="请输入备注"
maxLength={20}
/>
</Modal>
</>
)}
{/* 修改备注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>
);
};

View File

@@ -1,7 +1,7 @@
import React, { useState, useCallback, useEffect } from "react";
import { List, Avatar, Skeleton, Collapse } from "antd";
import type { CollapseProps } from "antd";
import styles from "./WechatFriends.module.scss";
import styles from "./com.module.scss";
import { Contact } from "@/utils/db";
import { ContactManager } from "@/utils/dbAction";
import { ContactGroupByLabel } from "@/pages/pc/ckbox/data";
@@ -22,28 +22,24 @@ interface WechatFriendsProps {
const ContactListSimple: React.FC<WechatFriendsProps> = ({
selectedContactId,
}) => {
// 本地状态(用于数据同步,不直接用于显示)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [contacts, setContacts] = useState<Contact[]>([]);
// 基础状态
const [contactGroups, setContactGroups] = useState<ContactGroupByLabel[]>([]);
const [labels, setLabels] = useState<ContactGroupByLabel[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [activeKey, setActiveKey] = useState<string[]>([]);
// 分页相关状态
const [groupContacts, setGroupContacts] = useState<{
[groupKey: string]: Contact[];
}>({});
const [groupPages, setGroupPages] = useState<{ [groupKey: string]: number }>(
{},
);
const [groupLoading, setGroupLoading] = useState<{
[groupKey: string]: boolean;
}>({});
const [groupHasMore, setGroupHasMore] = useState<{
[groupKey: string]: boolean;
}>({});
// 分页相关状态(合并为对象,减少状态数量)
const [groupData, setGroupData] = useState<{
contacts: { [groupKey: string]: Contact[] };
pages: { [groupKey: string]: number };
loading: { [groupKey: string]: boolean };
hasMore: { [groupKey: string]: boolean };
}>({
contacts: {},
pages: {},
loading: {},
hasMore: {},
});
// 使用新的 contacts store
const { searchResults, isSearchMode, setCurrentContact } = useContactStore();
@@ -53,19 +49,26 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
const currentCustomer = useCustomerStore(state => state.currentCustomer);
const { setCurrentContact: setWeChatCurrentContact } = useWeChatStore();
// 从服务器同步数据
const syncWithServer = useCallback(async (userId: number) => {
setRefreshing(true);
try {
const updatedContacts = await syncContactsFromServer(userId);
setContacts(updatedContacts);
} catch (error) {
console.error("同步联系人失败:", error);
} finally {
setRefreshing(false);
}
}, []);
// 从服务器同步数据(静默同步,不显示提示)
const syncWithServer = useCallback(
async (userId: number) => {
try {
await syncContactsFromServer(userId);
// 同步完成后,重新加载分组统计
if (labels.length > 0) {
const groupStats = await getGroupStatistics(
userId,
labels,
currentCustomer?.id,
);
setContactGroups(groupStats);
}
} catch (error) {
console.error("同步联系人失败:", error);
}
},
[labels, currentCustomer?.id],
);
// 获取标签列表
useEffect(() => {
@@ -86,23 +89,19 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
const loadData = async () => {
if (!currentUser?.id) return;
setLoading(true);
try {
// 1. 先从本地数据库加载
// 检查本地数据库是否有数据
const localContacts = await ContactManager.getUserContacts(
currentUser.id,
);
if (localContacts && localContacts.length > 0) {
// 有本地数据,立即显示
setContacts(localContacts);
setLoading(false);
// 有本地数据,直接显示(不显示 loading
// 后台静默同步
syncWithServer(currentUser.id);
} else {
// 没有本地数据,从服务器获取
// 没有本地数据,显示骨架屏并从服务器获取
setLoading(true);
await syncWithServer(currentUser.id);
setLoading(false);
}
@@ -144,11 +143,13 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
if (!label || !currentUser?.id) return;
// 如果已经加载过数据,不重复加载
if (groupContacts[groupKey] && groupContacts[groupKey].length > 0) {
return;
}
if (groupData.contacts[groupKey]?.length > 0) return;
setGroupLoading(prev => ({ ...prev, [groupKey]: true }));
// 设置加载状态
setGroupData(prev => ({
...prev,
loading: { ...prev.loading, [groupKey]: true },
}));
try {
const realGroupIds = labels
@@ -164,16 +165,19 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
20,
);
setGroupContacts(prev => ({ ...prev, [groupKey]: contacts }));
setGroupPages(prev => ({ ...prev, [groupKey]: 1 }));
setGroupHasMore(prev => ({
...prev,
[groupKey]: contacts.length === 20,
// 更新分组数据
setGroupData(prev => ({
contacts: { ...prev.contacts, [groupKey]: contacts },
pages: { ...prev.pages, [groupKey]: 1 },
loading: { ...prev.loading, [groupKey]: false },
hasMore: { ...prev.hasMore, [groupKey]: contacts.length === 20 },
}));
} catch (error) {
console.error("加载分组数据失败:", error);
} finally {
setGroupLoading(prev => ({ ...prev, [groupKey]: false }));
setGroupData(prev => ({
...prev,
loading: { ...prev.loading, [groupKey]: false },
}));
}
},
[
@@ -181,24 +185,28 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
labels,
currentUser?.id,
currentCustomer?.id,
groupContacts,
groupData.contacts,
],
);
// 加载更多联系人
const handleLoadMore = useCallback(
async (groupKey: string) => {
if (groupLoading[groupKey] || !groupHasMore[groupKey]) return;
if (groupData.loading[groupKey] || !groupData.hasMore[groupKey]) return;
const groupIndex = parseInt(groupKey);
const label = contactGroups[groupIndex];
if (!label || !currentUser?.id) return;
setGroupLoading(prev => ({ ...prev, [groupKey]: true }));
// 设置加载状态
setGroupData(prev => ({
...prev,
loading: { ...prev.loading, [groupKey]: true },
}));
try {
const currentPage = groupPages[groupKey] || 1;
const currentPage = groupData.pages[groupKey] || 1;
const nextPage = currentPage + 1;
const realGroupIds = labels
@@ -214,30 +222,25 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
20,
);
setGroupContacts(prev => ({
...prev,
[groupKey]: [...(prev[groupKey] || []), ...newContacts],
}));
setGroupPages(prev => ({ ...prev, [groupKey]: nextPage }));
setGroupHasMore(prev => ({
...prev,
[groupKey]: newContacts.length === 20,
// 更新分组数据
setGroupData(prev => ({
contacts: {
...prev.contacts,
[groupKey]: [...(prev.contacts[groupKey] || []), ...newContacts],
},
pages: { ...prev.pages, [groupKey]: nextPage },
loading: { ...prev.loading, [groupKey]: false },
hasMore: { ...prev.hasMore, [groupKey]: newContacts.length === 20 },
}));
} catch (error) {
console.error("加载更多联系人失败:", error);
} finally {
setGroupLoading(prev => ({ ...prev, [groupKey]: false }));
setGroupData(prev => ({
...prev,
loading: { ...prev.loading, [groupKey]: false },
}));
}
},
[
contactGroups,
labels,
currentUser?.id,
currentCustomer?.id,
groupLoading,
groupHasMore,
groupPages,
],
[contactGroups, labels, currentUser?.id, currentCustomer?.id, groupData],
);
// 联系人点击处理
@@ -307,7 +310,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
// 渲染加载更多按钮
const renderLoadMoreButton = (groupKey: string) => {
if (!groupHasMore[groupKey]) {
if (!groupData.hasMore[groupKey]) {
return <div className={styles.noMoreText}></div>;
}
@@ -316,9 +319,9 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
<button
className={styles.loadMoreBtn}
onClick={() => handleLoadMore(groupKey)}
disabled={groupLoading[groupKey]}
disabled={groupData.loading[groupKey]}
>
{groupLoading[groupKey] ? "加载中..." : "加载更多"}
{groupData.loading[groupKey] ? "加载中..." : "加载更多"}
</button>
</div>
);
@@ -343,7 +346,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
className: styles.groupPanel,
children: isActive ? (
<>
{groupLoading[groupKey] && !groupContacts[groupKey] ? (
{groupData.loading[groupKey] && !groupData.contacts[groupKey] ? (
// 首次加载显示骨架屏
<div className={styles.groupSkeleton}>
{Array(3)
@@ -365,10 +368,10 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
<>
<List
className={styles.list}
dataSource={groupContacts[groupKey] || []}
dataSource={groupData.contacts[groupKey] || []}
renderItem={renderContactItem}
/>
{(groupContacts[groupKey]?.length || 0) > 0 &&
{(groupData.contacts[groupKey]?.length || 0) > 0 &&
renderLoadMoreButton(groupKey)}
</>
)}
@@ -381,7 +384,7 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
return (
<div className={styles.contractListSimple}>
{loading ? (
// 加载状态:显示骨架屏
// 加载状态:显示骨架屏(只在首次无本地数据时显示)
renderSkeleton()
) : isSearchMode ? (
// 搜索模式:直接显示搜索结果列表
@@ -399,9 +402,6 @@ const ContactListSimple: React.FC<WechatFriendsProps> = ({
) : (
// 正常模式:显示分组列表
<>
{refreshing && (
<div className={styles.refreshingTip}>...</div>
)}
<Collapse
className={styles.groupCollapse}
activeKey={activeKey}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { Input, Skeleton } from "antd";
import { SearchOutlined, ChromeOutlined } from "@ant-design/icons";
import { SearchOutlined } from "@ant-design/icons";
import WechatFriends from "./WechatFriends";
import MessageList from "./MessageList/index";
import FriendsCircle from "./FriendsCicle";
@@ -26,10 +26,6 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
clearSearchKeyword();
};
useEffect(() => {
setActiveTab("contracts");
}, [currentCustomer]);
// 渲染骨架屏
const renderSkeleton = () => (
<div className={styles.skeletonContainer}>

View File

@@ -34,6 +34,8 @@ export interface MessageState {
refreshTrigger: number;
//最后刷新时间
lastRefreshTime: string | null;
//是否已经加载过一次(避免重复请求)
hasLoadedOnce: boolean;
//设置加载状态
setLoading: (loading: boolean) => void;
@@ -41,6 +43,10 @@ export interface MessageState {
setRefreshing: (refreshing: boolean) => void;
//触发刷新(通知组件重新查询)
triggerRefresh: () => void;
//设置已加载标识
setHasLoadedOnce: (loaded: boolean) => void;
//重置加载状态(用于登出或切换用户)
resetLoadState: () => void;
// ==================== 保留原有接口(向后兼容) ====================
//消息列表(废弃,保留兼容)

View File

@@ -15,6 +15,7 @@ export const useMessageStore = create<MessageState>()(
refreshing: false,
refreshTrigger: 0,
lastRefreshTime: null,
hasLoadedOnce: false,
setLoading: (loading: boolean) => set({ loading }),
setRefreshing: (refreshing: boolean) => set({ refreshing }),
@@ -23,6 +24,14 @@ export const useMessageStore = create<MessageState>()(
refreshTrigger: get().refreshTrigger + 1,
lastRefreshTime: new Date().toISOString(),
}),
setHasLoadedOnce: (loaded: boolean) => set({ hasLoadedOnce: loaded }),
resetLoadState: () =>
set({
hasLoadedOnce: false,
loading: false,
refreshing: false,
refreshTrigger: 0,
}),
// ==================== 保留原有接口(向后兼容) ====================
messageList: [],
@@ -42,6 +51,7 @@ export const useMessageStore = create<MessageState>()(
partialize: state => ({
// 只持久化必要的状态,不持久化数据
lastRefreshTime: state.lastRefreshTime,
hasLoadedOnce: state.hasLoadedOnce,
// 保留原有持久化字段(向后兼容)
messageList: [],
currentMessage: null,
@@ -99,3 +109,15 @@ export const setRefreshing = (refreshing: boolean) =>
* 触发刷新(通知组件重新查询数据库)
*/
export const triggerRefresh = () => useMessageStore.getState().triggerRefresh();
/**
* 设置已加载标识
* @param loaded 是否已加载
*/
export const setHasLoadedOnce = (loaded: boolean) =>
useMessageStore.getState().setHasLoadedOnce(loaded);
/**
* 重置加载状态(用于登出或切换用户)
*/
export const resetLoadState = () => useMessageStore.getState().resetLoadState();

View File

@@ -0,0 +1 @@
//消息过滤器