import { useState, useEffect, useCallback, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown, MessageCircle, UserX } from 'lucide-react' import { useChatStore } from '../stores/chatStore' import './ContactsPage.scss' interface ContactInfo { username: string displayName: string remark?: string nickname?: string avatarUrl?: string type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } function ContactsPage() { const [contacts, setContacts] = useState([]) const [filteredContacts, setFilteredContacts] = useState([]) const [selectedUsernames, setSelectedUsernames] = useState>(new Set()) const [isLoading, setIsLoading] = useState(true) const [searchKeyword, setSearchKeyword] = useState('') const [contactTypes, setContactTypes] = useState({ friends: true, groups: false, officials: false, deletedFriends: false }) // 导出模式与查看详情 const [exportMode, setExportMode] = useState(false) const [selectedContact, setSelectedContact] = useState(null) const navigate = useNavigate() const { setCurrentSession } = useChatStore() // 导出相关状态 const [exportFormat, setExportFormat] = useState<'json' | 'csv' | 'vcf'>('json') const [exportAvatars, setExportAvatars] = useState(true) const [exportFolder, setExportFolder] = useState('') const [isExporting, setIsExporting] = useState(false) const [showFormatSelect, setShowFormatSelect] = useState(false) const formatDropdownRef = useRef(null) // 加载通讯录 const loadContacts = useCallback(async () => { setIsLoading(true) try { const result = await window.electronAPI.chat.connect() if (!result.success) { console.error('连接失败:', result.error) setIsLoading(false) return } const contactsResult = await window.electronAPI.chat.getContacts() if (contactsResult.success && contactsResult.contacts) { // 获取头像URL const usernames = contactsResult.contacts.map((c: ContactInfo) => c.username) if (usernames.length > 0) { const avatarResult = await window.electronAPI.chat.enrichSessionsContactInfo(usernames) if (avatarResult.success && avatarResult.contacts) { contactsResult.contacts.forEach((contact: ContactInfo) => { const enriched = avatarResult.contacts?.[contact.username] if (enriched?.avatarUrl) { contact.avatarUrl = enriched.avatarUrl } }) } } setContacts(contactsResult.contacts) setFilteredContacts(contactsResult.contacts) setSelectedUsernames(new Set()) } } catch (e) { console.error('加载通讯录失败:', e) } finally { setIsLoading(false) } }, []) useEffect(() => { loadContacts() }, [loadContacts]) // 搜索和类型过滤 useEffect(() => { let filtered = contacts // 类型过滤 filtered = filtered.filter(c => { if (c.type === 'friend' && !contactTypes.friends) return false if (c.type === 'group' && !contactTypes.groups) return false if (c.type === 'official' && !contactTypes.officials) return false if (c.type === 'former_friend' && !contactTypes.deletedFriends) return false return true }) // 关键词过滤 if (searchKeyword.trim()) { const lower = searchKeyword.toLowerCase() filtered = filtered.filter(c => c.displayName?.toLowerCase().includes(lower) || c.remark?.toLowerCase().includes(lower) || c.username.toLowerCase().includes(lower) ) } setFilteredContacts(filtered) }, [searchKeyword, contacts, contactTypes]) // 点击外部关闭下拉菜单 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node if (showFormatSelect && formatDropdownRef.current && !formatDropdownRef.current.contains(target)) { setShowFormatSelect(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [showFormatSelect]) const selectedInFilteredCount = filteredContacts.reduce((count, contact) => { return selectedUsernames.has(contact.username) ? count + 1 : count }, 0) const allFilteredSelected = filteredContacts.length > 0 && selectedInFilteredCount === filteredContacts.length const toggleContactSelected = (username: string, checked: boolean) => { setSelectedUsernames(prev => { const next = new Set(prev) if (checked) { next.add(username) } else { next.delete(username) } return next }) } const toggleAllFilteredSelected = (checked: boolean) => { setSelectedUsernames(prev => { const next = new Set(prev) filteredContacts.forEach(contact => { if (checked) { next.add(contact.username) } else { next.delete(contact.username) } }) return next }) } const getAvatarLetter = (name: string) => { if (!name) return '?' return [...name][0] || '?' } const getContactTypeIcon = (type: string) => { switch (type) { case 'friend': return case 'group': return case 'official': return case 'former_friend': return default: return } } const getContactTypeName = (type: string) => { switch (type) { case 'friend': return '好友' case 'group': return '群聊' case 'official': return '公众号' case 'former_friend': return '曾经的好友' default: return '其他' } } // 选择导出文件夹 const selectExportFolder = async () => { try { const result = await window.electronAPI.dialog.openDirectory({ title: '选择导出位置' }) if (result && !result.canceled && result.filePaths && result.filePaths.length > 0) { setExportFolder(result.filePaths[0]) } } catch (e) { console.error('选择文件夹失败:', e) } } // 开始导出 const startExport = async () => { if (!exportFolder) { alert('请先选择导出位置') return } if (selectedUsernames.size === 0) { alert('请至少选择一个联系人') return } setIsExporting(true) try { const exportOptions = { format: exportFormat, exportAvatars, contactTypes: { friends: contactTypes.friends, groups: contactTypes.groups, officials: contactTypes.officials }, selectedUsernames: Array.from(selectedUsernames) } const result = await window.electronAPI.export.exportContacts(exportFolder, exportOptions) if (result.success) { alert(`导出成功!共导出 ${result.successCount} 个联系人`) } else { alert(`导出失败:${result.error}`) } } catch (e) { console.error('导出失败:', e) alert(`导出失败:${String(e)}`) } finally { setIsExporting(false) } } const exportFormatOptions = [ { value: 'json', label: 'JSON', desc: '详细格式,包含完整联系人信息' }, { value: 'csv', label: 'CSV (Excel)', desc: '电子表格格式,适合Excel查看' }, { value: 'vcf', label: 'VCF (vCard)', desc: '标准名片格式,支持导入手机' } ] const getOptionLabel = (value: string) => { return exportFormatOptions.find(opt => opt.value === value)?.label || value } return (
{/* 左侧:联系人列表 */}

通讯录

setSearchKeyword(e.target.value)} /> {searchKeyword && ( )}
共 {filteredContacts.length} 个联系人
{exportMode && (
已选 {selectedUsernames.size}(当前筛选 {selectedInFilteredCount} / {filteredContacts.length})
)} {isLoading ? (
加载中...
) : filteredContacts.length === 0 ? (
暂无联系人
) : (
{filteredContacts.map(contact => { const isChecked = selectedUsernames.has(contact.username) const isActive = !exportMode && selectedContact?.username === contact.username return (
{ if (exportMode) { toggleContactSelected(contact.username, !isChecked) } else { setSelectedContact(isActive ? null : contact) } }} > {exportMode && ( )}
{contact.avatarUrl ? ( ) : ( {getAvatarLetter(contact.displayName)} )}
{contact.displayName}
{contact.remark && contact.remark !== contact.displayName && (
备注: {contact.remark}
)}
{getContactTypeIcon(contact.type)} {getContactTypeName(contact.type)}
) })}
)}
{/* 右侧面板 */} {exportMode ? (

导出设置

导出格式

{showFormatSelect && (
{exportFormatOptions.map(option => ( ))}
)}

导出选项

导出位置

{exportFolder || '未设置'}
) : selectedContact ? (

联系人详情

{selectedContact.avatarUrl ? ( ) : ( {getAvatarLetter(selectedContact.displayName)} )}
{selectedContact.displayName}
{getContactTypeIcon(selectedContact.type)} {getContactTypeName(selectedContact.type)}
用户名{selectedContact.username}
昵称{selectedContact.nickname || selectedContact.displayName}
{selectedContact.remark &&
备注{selectedContact.remark}
}
类型{getContactTypeName(selectedContact.type)}
) : (
点击左侧联系人查看详情
)}
) } export default ContactsPage