feat: 支持按导出媒体文件名精确搜索

离线 HTML 搜索支持使用不含扩展名的完整图片或视频哈希文件名进行精确匹配,并保持其他消息字段的模糊搜索行为。\n\n补充图片和视频命中、大小写兼容、哈希片段及带扩展名不命中的 DOM 回归测试。
This commit is contained in:
Nanin
2026-08-12 22:02:43 +08:00
parent 34b86af0be
commit 8fac752e2b
2 changed files with 73 additions and 3 deletions
+17 -2
View File
@@ -995,6 +995,19 @@ const renderExportScript = (name: string): string => `
message.contentData && forwardedSearchText(message.contentData.items), message.contentData && forwardedSearchText(message.contentData.items),
message.exportMediaName message.exportMediaName
].filter(Boolean).join(' ').toLowerCase() ].filter(Boolean).join(' ').toLowerCase()
const exportedImageOrVideoFileStem = (message) => {
const mediaType = message.exportMediaType || (message.contentData && message.contentData.type)
if (mediaType !== 'image' && mediaType !== 'video') return ''
const mediaUrl = String(message.exportMediaUrl || '').split(/[?#]/, 1)[0]
const fileName = mediaUrl.slice(mediaUrl.lastIndexOf('/') + 1)
if (!fileName) return ''
let decodedFileName = fileName
try {
decodedFileName = decodeURIComponent(fileName)
} catch {}
const extensionIndex = decodedFileName.lastIndexOf('.')
return (extensionIndex > 0 ? decodedFileName.slice(0, extensionIndex) : decodedFileName).toLowerCase()
}
const shareLabel = (typeVal) => { const shareLabel = (typeVal) => {
if (String(typeVal) === '5') return '公众号链接' if (String(typeVal) === '5') return '公众号链接'
@@ -1516,7 +1529,9 @@ const renderExportScript = (name: string): string => `
return allMessages.filter((message) => return allMessages.filter((message) =>
(activeConversation === 'all' || message.exportConversationId === activeConversation) && (activeConversation === 'all' || message.exportConversationId === activeConversation) &&
(activeKind === 'all' || kindOf(message) === activeKind) && (activeKind === 'all' || kindOf(message) === activeKind) &&
(!term || searchText(message).includes(term)) (!term ||
searchText(message).includes(term) ||
exportedImageOrVideoFileStem(message) === term)
) )
} }
const applyFilters = (restorePosition = false) => { const applyFilters = (restorePosition = false) => {
@@ -1779,7 +1794,7 @@ export function renderExportPage(name: string): string {
</div> </div>
</div> </div>
<div class="controls"> <div class="controls">
<input id="query" type="search" placeholder="搜索发送者消息内容…" aria-label="搜索消息"> <input id="query" type="search" placeholder="搜索发送者消息内容或媒体文件名(不含后缀)…" aria-label="搜索消息">
</div> </div>
<div class="filters" id="filters"> <div class="filters" id="filters">
<button class="filter-button active" type="button" data-kind="all">全部</button> <button class="filter-button active" type="button" data-kind="all">全部</button>
+56 -1
View File
@@ -33,7 +33,7 @@ describe('export media', () => {
expect(html).toContain('aria-expanded="') expect(html).toContain('aria-expanded="')
expect(html).toContain('setExpandedTimelineYear') expect(html).toContain('setExpandedTimelineYear')
expect(html).toContain('data-kind="media"') expect(html).toContain('data-kind="media"')
expect(html).toContain('placeholder="搜索发送者消息内容…"') expect(html).toContain('placeholder="搜索发送者消息内容或媒体文件名(不含后缀)…"')
expect(html).toContain('font-size: 16px;') expect(html).toContain('font-size: 16px;')
expect(html).toContain('filtered.slice(windowStart, windowEnd)') expect(html).toContain('filtered.slice(windowStart, windowEnd)')
expect(html).toContain('windowStart = Math.max(0, windowEnd - PAGE_SIZE)') expect(html).toContain('windowStart = Math.max(0, windowEnd - PAGE_SIZE)')
@@ -156,6 +156,61 @@ describe('export media', () => {
dom.window.close() dom.window.close()
}) })
it('matches exported image and video filenames exactly without their extension', () => {
const html = renderExportPage('媒体文件名搜索')
const dom = new JSDOM(html, { runScripts: 'outside-only' })
const imageFileName = 'image_0123456789abcdef.jpg'
const videoFileName = 'video_fedcba9876543210.mp4'
const messages: Message[] = [
{
...messageForArchive('image-name', 'fixture', '媒体文件名搜索', '', 1),
type: '图片',
exportMediaType: 'image',
exportMediaUrl: `media/${imageFileName}`,
contentData: { type: 'image' }
},
{
...messageForArchive('video-name', 'fixture', '媒体文件名搜索', '', 2),
type: '视频',
exportMediaType: 'video',
exportMediaUrl: `media/${videoFileName}`,
contentData: { type: 'video' }
}
]
Object.assign(dom.window, {
__WECHAT_EXPORT__: {
version: 1,
sourceId: 'fixture',
name: '媒体文件名搜索',
messages
}
})
dom.window.eval(inlineScriptOf(html))
const search = dom.window.document.querySelector('#query') as HTMLInputElement
search.value = 'IMAGE_0123456789ABCDEF'
search.dispatchEvent(new dom.window.Event('input'))
expect(dom.window.document.querySelectorAll('.message')).toHaveLength(1)
expect(dom.window.document.querySelectorAll('img.media-image')).toHaveLength(1)
expect(dom.window.document.querySelectorAll('video.media-image')).toHaveLength(0)
search.value = '0123456789abcdef'
search.dispatchEvent(new dom.window.Event('input'))
expect(dom.window.document.querySelectorAll('.message')).toHaveLength(0)
search.value = 'video_fedcba9876543210'
search.dispatchEvent(new dom.window.Event('input'))
expect(dom.window.document.querySelectorAll('.message')).toHaveLength(1)
expect(dom.window.document.querySelectorAll('img.media-image')).toHaveLength(0)
expect(dom.window.document.querySelectorAll('video.media-image')).toHaveLength(1)
search.value = videoFileName
search.dispatchEvent(new dom.window.Event('input'))
expect(dom.window.document.querySelectorAll('.message')).toHaveLength(0)
dom.window.close()
})
it('filters a v2 merged archive by conversation before search and month counts', () => { it('filters a v2 merged archive by conversation before search and month counts', () => {
const html = renderExportPage('合并档案') const html = renderExportPage('合并档案')
const dom = new JSDOM(html, { runScripts: 'outside-only' }) const dom = new JSDOM(html, { runScripts: 'outside-only' })