refactor: 重构一下档案搜索功能 支持拼音微信号等

This commit is contained in:
电摇小子
2026-09-03 23:38:12 +08:00
parent bde3bc537f
commit 21e299942b
17 changed files with 365 additions and 44 deletions
+13 -3
View File
@@ -45,7 +45,11 @@ import { AppUpdatePrompt } from './features/app-update/AppUpdatePrompt'
import { GroupExitMonitorWorkspace } from './features/group-exit-monitor/GroupExitMonitorWorkspace'
import { LogsWorkspace } from './features/logs/LogsWorkspace'
import { selectContactAvatarRefreshUsernames } from './utils/contact-avatar'
import { filterContacts } from './utils/contact-search'
import {
buildContactSearchIndex,
filterContactSearchIndex,
type ContactSearchIndex
} from '../../shared/contact-search'
const SIDEBAR_MIN_WIDTH = 260
const SIDEBAR_MAX_WIDTH = 380
@@ -140,6 +144,9 @@ const areMessagesEquivalent = (left: Message[], right: Message[]): boolean => {
return true
}
const filterContactList = (index: ContactSearchIndex, keyword: string): Contact[] =>
filterContactSearchIndex(index, keyword)
type GroupSnapshot = {
roomId: string
memberCount: number
@@ -218,6 +225,7 @@ function App(): React.ReactElement {
const [isDatabaseConnecting, setIsDatabaseConnecting] = useState(false)
const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey)
const [contacts, setContacts] = useState<Contact[]>([])
const contactSearchIndex = React.useMemo(() => buildContactSearchIndex(contacts), [contacts])
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
const [messages, setMessages] = useState<Message[]>([])
const [isMessagesLoading, setIsMessagesLoading] = useState(false)
@@ -591,7 +599,9 @@ function App(): React.ReactElement {
options?.onProgress?.('正在加载联系人...', 35)
const list = await window.api.getContacts()
setContacts(list)
setFilteredContacts(filterContacts(list, options?.filterKeyword || ''))
setFilteredContacts(
filterContactSearchIndex(buildContactSearchIndex(list), options?.filterKeyword || '')
)
const runId = ++contactAvatarHydrationRunRef.current
const hydrate = (): Promise<void> => hydrateContactAvatars(list, runId, options?.onProgress)
if (options?.waitForAvatars) {
@@ -1436,7 +1446,7 @@ function App(): React.ReactElement {
])
const handleSearchContacts = (keyword: string): void => {
setFilteredContacts(filterContacts(contacts, keyword))
setFilteredContacts(filterContactList(contactSearchIndex, keyword))
}
const handleRefreshContacts = async (filterKeyword: string): Promise<void> => {
@@ -13,9 +13,10 @@ export function ConversationItem({
onSelect
}: ConversationItemProps): React.ReactElement {
const nickname = contact.m_nsNickName?.trim()
const wxid = contact.m_nsUsrName
const displayName = nickname || wxid || '未命名会话'
const initial = (displayName || wxid || '?').charAt(0)
const avatarUsername = contact.m_nsUsrName
const internalWxid = contact.wxid || avatarUsername
const displayName = nickname || internalWxid || '未命名会话'
const initial = (displayName || internalWxid || '?').charAt(0)
const [repairedAvatar, setRepairedAvatar] = useState<{
username: string
source: string
@@ -23,22 +24,22 @@ export function ConversationItem({
}>()
const [failedAvatar, setFailedAvatar] = useState<{ username: string; source: string }>()
const repairedSource =
repairedAvatar?.username === wxid && repairedAvatar.replaces === contact.avatar
repairedAvatar?.username === avatarUsername && repairedAvatar.replaces === contact.avatar
? repairedAvatar.source
: undefined
const avatar = repairedSource || contact.avatar
const avatarFailed = failedAvatar?.username === wxid && failedAvatar.source === avatar
const avatarFailed = failedAvatar?.username === avatarUsername && failedAvatar.source === avatar
const handleAvatarError = (): void => {
if (!avatar || avatarFailed) return
setFailedAvatar({ username: wxid, source: avatar })
setFailedAvatar({ username: avatarUsername, source: avatar })
if (avatar.startsWith('data:')) return
void window.api
.getContactAvatars([wxid], { refresh: true })
.getContactAvatars([avatarUsername], { refresh: true })
.then((avatars) => {
const fallback = avatars[wxid]
const fallback = avatars[avatarUsername]
if (!fallback || fallback === avatar) return
setRepairedAvatar({ username: wxid, source: fallback, replaces: avatar })
setRepairedAvatar({ username: avatarUsername, source: fallback, replaces: avatar })
setFailedAvatar(undefined)
})
.catch(() => undefined)
@@ -49,7 +50,11 @@ export function ConversationItem({
type="button"
className={`conversation-item ${active ? 'active' : ''}`}
onClick={() => onSelect(contact)}
title={wxid && wxid !== displayName ? wxid : displayName}
title={
contact.wechatId ||
contact.alias ||
(internalWxid !== displayName ? internalWxid : displayName)
}
>
<span className="conversation-item-active-mark" aria-hidden />
<span className="conversation-item-avatar">
@@ -68,6 +73,9 @@ export function ConversationItem({
</span>
<span className="conversation-item-body">
<span className="conversation-item-name">{displayName}</span>
{contact.wechatId && (
<span className="conversation-item-meta">微信号: {contact.wechatId}</span>
)}
</span>
</button>
)
@@ -151,6 +151,15 @@ export function ConversationSidebar({
const handleSearchChange = (term: string): void => {
setSearchTerm(term)
onSearch(term)
if (term.trim()) {
setExpandedSections((current) => ({
...current,
groups: true,
folded: true,
officialAccounts: true,
contacts: true
}))
}
}
const handleRefresh = async (): Promise<void> => {
@@ -75,7 +75,11 @@ function ExportContactRow({
{name}
</strong>
<small className="text-[11px] text-muted-foreground">
{contact.type === 'group' ? '群聊' : '联系人'}
{contact.type === 'group'
? '群聊'
: contact.wechatId
? `联系人 · 微信号: ${contact.wechatId}`
: '联系人'}
</small>
</span>
{showSelection && (
@@ -23,6 +23,10 @@ import type {
import { displayName } from './exportUtils'
import type { VoiceModelStatus } from '../../../../shared/voice-recognition'
import { resolveMemberName } from '../../../../shared/member-names'
import {
buildContactSearchIndex,
filterContactSearchIndex
} from '../../../../shared/contact-search'
const ALL_CONTACT_TYPES: ExportContactType[] = ['group', 'user']
const contactTypeKey = (types: ExportContactType[] | undefined): string =>
@@ -57,6 +61,7 @@ export function ExportWorkspace({
const [selectedContacts, setSelectedContacts] = useState<Contact[]>(() =>
initialSelection ? [initialSelection] : []
)
const contactSearchIndex = useMemo(() => buildContactSearchIndex(contacts), [contacts])
const [activeContactId, setActiveContactId] = useState(initialSelection?.md5 || '')
const [previewByContact, setPreviewByContact] = useState<Record<string, Message[]>>({})
const [range, setRange] = useState<ExportRange>(() => (runningAllTask ? 'all' : 'today'))
@@ -109,15 +114,8 @@ export function ExportWorkspace({
}, [exportAll, loadPreviewMessages, previewByContact, selectedContacts])
const filteredContacts = useMemo(() => {
const keyword = contactFilter.trim().toLowerCase()
return contacts.filter((contact) => {
if (contactType !== 'all' && contact.type !== contactType) return false
if (!keyword) return true
return [contact.m_nsNickName, contact.m_nsUsrName].some((value) =>
value.toLowerCase().includes(keyword)
)
})
}, [contactFilter, contactType, contacts])
return filterContactSearchIndex(contactSearchIndex, contactFilter, contactType)
}, [contactFilter, contactSearchIndex, contactType])
const activeContact =
selectedContacts.find((contact) => contact.md5 === activeContactId) ||
@@ -287,6 +287,15 @@
white-space: nowrap;
}
.conversation-item-meta {
overflow: hidden;
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-sidebar-account {
box-sizing: border-box;
flex: 0 0 auto;
+3 -13
View File
@@ -1,17 +1,7 @@
import type { Contact } from '../../../shared/types'
import { buildContactSearchIndex, filterContactSearchIndex } from '../../../shared/contact-search'
export function filterContacts(list: Contact[], keyword: string): Contact[] {
const query = keyword.trim().toLocaleLowerCase()
if (!query) return list
return list.filter((contact) =>
[
contact.remark,
contact.wechatNickname,
contact.m_nsNickName,
contact.alias,
contact.wechatId,
contact.m_nsUsrName
].some((value) => value?.trim().toLocaleLowerCase().includes(query))
)
if (!keyword.trim()) return list
return filterContactSearchIndex(buildContactSearchIndex(list), keyword)
}