mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 05:27:48 +08:00
refactor: 重构一下档案搜索功能 支持拼音微信号等
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user