diff --git a/src/pages/pc/ckbox/weChat/components/SidebarMenu/SearchAnyone/index.module.scss b/src/pages/pc/ckbox/weChat/components/SidebarMenu/SearchAnyone/index.module.scss new file mode 100644 index 0000000..786b3f4 --- /dev/null +++ b/src/pages/pc/ckbox/weChat/components/SidebarMenu/SearchAnyone/index.module.scss @@ -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; +} diff --git a/src/pages/pc/ckbox/weChat/components/SidebarMenu/SearchAnyone/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/SearchAnyone/index.tsx new file mode 100644 index 0000000..49131b3 --- /dev/null +++ b/src/pages/pc/ckbox/weChat/components/SidebarMenu/SearchAnyone/index.tsx @@ -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 = ({ onContactClick }) => { + const [searchValue, setSearchValue] = useState(""); + const [searchResults, setSearchResults] = useState([]); + 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>(); + const containerRef = useRef(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 ( +
handleResultClick(contact)} + > +
+ + {contact.nickname?.charAt(0) || (isGroup ? "群" : "联")} + + ) + } + className={styles.avatar} + /> +
+
+
{name}
+ {isGroup &&
群聊
} +
+
+ ); + }; + + return ( +
+ } + value={searchValue} + onChange={e => handleSearchChange(e.target.value)} + onFocus={() => { + if (searchValue.trim()) { + setShowResults(true); + } + }} + allowClear + /> + + {/* 搜索结果列表 */} + {showResults && ( +
+ {loading ? ( +
+ + 搜索中... +
+ ) : searchResults.length > 0 ? ( +
+ {searchResults.map(contact => renderResultItem(contact))} +
+ ) : searchValue.trim() ? ( +
未找到匹配的联系人
+ ) : null} +
+ )} +
+ ); +}; + +export default SearchAnyone; diff --git a/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx b/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx index 717de6f..6fde844 100644 --- a/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx +++ b/src/pages/pc/ckbox/weChat/components/SidebarMenu/index.tsx @@ -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 = ({ 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 = ({ loading = false }) => { handleContactSelection(); }, [currentContact, currentUserId, setCurrentContact]); - // 搜索防抖处理 - const searchDebounceRef = useRef>(); - - 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 = ({ loading = false }) => {
{/* 搜索栏 */}
- } - value={searchKeyword || oldSearchKeyword} - onChange={e => handleSearch(e.target.value)} - onClear={handleClearSearch} - allowClear - /> + {currentCustomer && ( = ({ loading = false }) => {
{ - setActiveTab("contracts"); - try { - const accountId = currentCustomer?.id || 0; - // 每次切到联系人标签时,强制从接口刷新一次分组列表(通过全局 store 调用,避免 hook 实例问题) - await useContactStoreNew - .getState() - .loadGroupsFromAPI(accountId); - } catch (error) { - console.error("刷新联系人分组失败:", error); - } - }} + onClick={() => setActiveTab("contracts")} > 联系人