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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user