Refactor SidebarMenu component by removing old search functionality and integrating SearchAnyone component for improved search experience. Simplified state management by utilizing the new contact store.

This commit is contained in:
乘风
2026-01-15 11:33:34 +08:00
parent 88a6555302
commit b65bbca307
3 changed files with 361 additions and 74 deletions

View File

@@ -0,0 +1,90 @@
.searchContainer {
position: relative;
width: 100%;
}
.resultsContainer {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 4px;
box-shadow: 0 0px 10px rgba(0, 0, 0, 0.3);
z-index: 1000;
height: 400px;
width: 260px;
}
.resultsList {
max-height: 400px;
overflow-y: auto;
}
.resultItem {
display: flex;
align-items: center;
padding: 8px 15px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
&:active {
background-color: #e8e8e8;
}
}
.avatarContainer {
margin-right: 10px;
flex-shrink: 0;
}
.avatar {
background-color: #1890ff;
}
.contractInfo {
flex: 1;
overflow: hidden;
min-width: 0;
}
.name {
font-size: 14px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
.groupInfo {
font-size: 12px;
color: #999;
margin-top: 2px;
}
.loadingContainer {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
gap: 8px;
}
.loadingText {
font-size: 14px;
color: #999;
}
.noResults {
text-align: center;
color: #999;
padding: 20px;
font-size: 14px;
}

View File

@@ -0,0 +1,267 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import { Input, Avatar, Spin } from "antd";
import { SearchOutlined } from "@ant-design/icons";
import { Contact } from "@/utils/db";
import { getFriendList } from "@/components/FriendSelection/api";
import { getGroupList } from "@/components/GroupSelection/api";
import { useContactStore } from "@/store/module/weChat/contacts";
import { useCustomerStore } from "@/store/module/weChat/customer";
import { MessageManager } from "@/utils/dbAction/message";
import { useUserStore } from "@/store/module/user";
import styles from "./index.module.scss";
interface SearchAnyoneProps {
onContactClick?: (contact: Contact) => void;
}
const SearchAnyone: React.FC<SearchAnyoneProps> = ({ onContactClick }) => {
const [searchValue, setSearchValue] = useState("");
const [searchResults, setSearchResults] = useState<Contact[]>([]);
const [loading, setLoading] = useState(false);
const [showResults, setShowResults] = useState(false);
const { setCurrentContact } = useContactStore();
const currentCustomer = useCustomerStore(state => state.currentCustomer);
const { user } = useUserStore();
const currentUserId = user?.id || 0;
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const containerRef = useRef<HTMLDivElement>(null);
// 搜索防抖处理
const performSearch = useCallback(
async (keyword: string) => {
if (!keyword.trim()) {
setSearchResults([]);
setShowResults(false);
return;
}
setLoading(true);
setShowResults(true);
try {
// 同时请求好友列表和群列表
const [friendsResult, groupsResult] = await Promise.all([
getFriendList({
page: 1,
limit: 50,
keyword: keyword.trim(),
}),
getGroupList({
page: 1,
limit: 50,
keyword: keyword.trim(),
}),
]);
const friends = (friendsResult?.list || []).map((item: any) => ({
serverId: `friend_${item.id}`,
userId: currentUserId,
id: item.id,
type: "friend" as const,
wechatAccountId: item.wechatAccountId || currentCustomer?.id || 0,
nickname: item.nickname || "",
conRemark: item.conRemark || "",
avatar: item.avatar || "",
lastUpdateTime: new Date().toISOString(),
sortKey: "",
searchKey: (item.conRemark || item.nickname || "").toLowerCase(),
wechatFriendId: item.id,
wechatId: item.wechatId || "",
alias: item.alias || "",
gender: item.gender,
groupId: item.groupId,
region: item.region || "",
signature: item.signature || "",
phone: item.phone || "",
quanPin: item.quanPin || "",
}));
const groups = (groupsResult?.list || []).map((item: any) => ({
serverId: `group_${item.id}`,
userId: currentUserId,
id: item.id,
type: "group" as const,
wechatAccountId: item.wechatAccountId || currentCustomer?.id || 0,
nickname: item.name || item.chatroomName || "",
conRemark: item.conRemark || "",
avatar: item.chatroomAvatar || item.avatar || "",
lastUpdateTime: new Date().toISOString(),
sortKey: "",
searchKey: (
item.conRemark ||
item.nickname ||
item.chatroomName ||
""
).toLowerCase(),
chatroomId: item.chatroomId || "",
chatroomOwner: item.chatroomOwner || "",
selfDisplayName: item.selfDisplayName || "",
notice: item.notice || "",
memberCount: item.memberCount || 0,
}));
// 合并结果并去重
const allResults = [...friends, ...groups];
const uniqueResults = allResults.filter(
(contact, index, self) =>
index ===
self.findIndex(c => c.id === contact.id && c.type === contact.type),
);
setSearchResults(uniqueResults);
} catch (error) {
console.error("搜索失败:", error);
setSearchResults([]);
} finally {
setLoading(false);
}
},
[currentUserId, currentCustomer],
);
// 处理搜索输入
const handleSearchChange = (value: string) => {
setSearchValue(value);
// 清除之前的定时器
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
// 如果输入为空,立即隐藏结果
if (!value.trim()) {
setSearchResults([]);
setShowResults(false);
return;
}
// 防抖300ms 后执行搜索
searchTimeoutRef.current = setTimeout(() => {
performSearch(value);
}, 300);
};
// 处理点击搜索结果
const handleResultClick = useCallback(
async (contact: Contact) => {
// 设置当前联系人(这会触发 SidebarMenu 中的 useEffect自动切换到聊天tab并选中会话
setCurrentContact(contact);
// 如果有自定义点击处理,调用它
if (onContactClick) {
onContactClick(contact);
}
// 清空搜索并隐藏结果
setSearchValue("");
setSearchResults([]);
setShowResults(false);
},
[setCurrentContact, onContactClick],
);
// 点击外部区域关闭搜索结果
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setShowResults(false);
}
};
if (showResults) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [showResults]);
// 组件卸载时清除定时器
useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, []);
// 渲染搜索结果项
const renderResultItem = (contact: Contact) => {
const isGroup = contact.type === "group";
// 参考会话列表的显示逻辑优先显示备注名其次昵称最后微信号好友或群ID群聊
const name =
contact.conRemark ||
contact.nickname ||
(isGroup ? `群聊${contact.id}` : (contact as any).wechatId || "");
return (
<div
key={`${contact.type}_${contact.id}`}
className={styles.resultItem}
onClick={() => handleResultClick(contact)}
>
<div className={styles.avatarContainer}>
<Avatar
size={48}
src={contact.avatar}
icon={
!contact.avatar && (
<span>
{contact.nickname?.charAt(0) || (isGroup ? "群" : "联")}
</span>
)
}
className={styles.avatar}
/>
</div>
<div className={styles.contractInfo}>
<div className={styles.name}>{name}</div>
{isGroup && <div className={styles.groupInfo}></div>}
</div>
</div>
);
};
return (
<div className={styles.searchContainer} ref={containerRef}>
<Input
placeholder="搜索客户..."
prefix={<SearchOutlined />}
value={searchValue}
onChange={e => handleSearchChange(e.target.value)}
onFocus={() => {
if (searchValue.trim()) {
setShowResults(true);
}
}}
allowClear
/>
{/* 搜索结果列表 */}
{showResults && (
<div className={styles.resultsContainer}>
{loading ? (
<div className={styles.loadingContainer}>
<Spin size="small" />
<span className={styles.loadingText}>...</span>
</div>
) : searchResults.length > 0 ? (
<div className={styles.resultsList}>
{searchResults.map(contact => renderResultItem(contact))}
</div>
) : searchValue.trim() ? (
<div className={styles.noResults}></div>
) : null}
</div>
)}
</div>
);
};
export default SearchAnyone;

View File

@@ -11,6 +11,7 @@ import MessageList from "./MessageList/index";
import FriendsCircle from "./FriendsCicle";
import AddFriends from "./AddFriends";
import PopChatRoom from "./PopChatRoom";
import SearchAnyone from "./SearchAnyone";
import styles from "./SidebarMenu.module.scss";
import { useContactStore } from "@/store/module/weChat/contacts";
import { useContactStoreNew } from "@/store/module/weChat/contacts.new";
@@ -23,16 +24,7 @@ interface SidebarMenuProps {
}
const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
const {
searchKeyword: oldSearchKeyword,
setSearchKeyword: setOldSearchKeyword,
clearSearchKeyword,
currentContact,
} = useContactStore();
// 使用新架构的ContactStore进行搜索
const contactStoreNew = useContactStoreNew();
const { searchKeyword, searchContacts, clearSearch } = contactStoreNew;
const { currentContact } = useContactStore();
const currentCustomer = useCustomerStore(state => state.currentCustomer);
const { setCurrentContact } = useWeChatStore();
@@ -76,50 +68,6 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
handleContactSelection();
}, [currentContact, currentUserId, setCurrentContact]);
// 搜索防抖处理
const searchDebounceRef = useRef<ReturnType<typeof setTimeout>>();
const handleSearch = (value: string) => {
// 同时更新旧架构(向后兼容)
setOldSearchKeyword(value);
// 清除之前的防抖定时器
if (searchDebounceRef.current) {
clearTimeout(searchDebounceRef.current);
}
// 如果关键词为空,立即清除搜索
if (!value.trim()) {
clearSearch();
return;
}
// 防抖300ms后执行搜索
searchDebounceRef.current = setTimeout(() => {
searchContacts(value);
}, 300);
};
const handleClearSearch = () => {
// 清除防抖定时器
if (searchDebounceRef.current) {
clearTimeout(searchDebounceRef.current);
}
// 清除旧架构的搜索
clearSearchKeyword();
// 清除新架构的搜索
clearSearch();
};
// 组件卸载时清除防抖定时器
useEffect(() => {
return () => {
if (searchDebounceRef.current) {
clearTimeout(searchDebounceRef.current);
}
};
}, []);
// 下拉菜单项
const menuItems: MenuProps["items"] = [
{
@@ -190,14 +138,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
<div className={styles.headerContainer}>
{/* 搜索栏 */}
<div className={styles.searchBar}>
<Input
placeholder="搜索客户..."
prefix={<SearchOutlined />}
value={searchKeyword || oldSearchKeyword}
onChange={e => handleSearch(e.target.value)}
onClear={handleClearSearch}
allowClear
/>
<SearchAnyone />
{currentCustomer && (
<Dropdown
menu={{ items: menuItems }}
@@ -219,18 +160,7 @@ const SidebarMenu: React.FC<SidebarMenuProps> = ({ loading = false }) => {
</div>
<div
className={`${styles.tabItem} ${activeTab === "contracts" ? styles.active : ""}`}
onClick={async () => {
setActiveTab("contracts");
try {
const accountId = currentCustomer?.id || 0;
// 每次切到联系人标签时,强制从接口刷新一次分组列表(通过全局 store 调用,避免 hook 实例问题)
await useContactStoreNew
.getState()
.loadGroupsFromAPI(accountId);
} catch (error) {
console.error("刷新联系人分组失败:", error);
}
}}
onClick={() => setActiveTab("contracts")}
>
<span></span>
</div>