From 8fac752e2b7a65f6ee3b635b07dbf3bd5652c1f2 Mon Sep 17 00:00:00 2001
From: Nanin <7017924+nanin@users.noreply.github.com>
Date: Wed, 12 Aug 2026 22:02:43 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=8C=89=E5=AF=BC?=
=?UTF-8?q?=E5=87=BA=E5=AA=92=E4=BD=93=E6=96=87=E4=BB=B6=E5=90=8D=E7=B2=BE?=
=?UTF-8?q?=E7=A1=AE=E6=90=9C=E7=B4=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
离线 HTML 搜索支持使用不含扩展名的完整图片或视频哈希文件名进行精确匹配,并保持其他消息字段的模糊搜索行为。\n\n补充图片和视频命中、大小写兼容、哈希片段及带扩展名不命中的 DOM 回归测试。
---
src/main/export-html-template.ts | 19 +++++++++--
tests/unit/export-media.test.ts | 57 +++++++++++++++++++++++++++++++-
2 files changed, 73 insertions(+), 3 deletions(-)
diff --git a/src/main/export-html-template.ts b/src/main/export-html-template.ts
index ff91e10..d489142 100644
--- a/src/main/export-html-template.ts
+++ b/src/main/export-html-template.ts
@@ -995,6 +995,19 @@ const renderExportScript = (name: string): string => `
message.contentData && forwardedSearchText(message.contentData.items),
message.exportMediaName
].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) => {
if (String(typeVal) === '5') return '公众号链接'
@@ -1516,7 +1529,9 @@ const renderExportScript = (name: string): string => `
return allMessages.filter((message) =>
(activeConversation === 'all' || message.exportConversationId === activeConversation) &&
(activeKind === 'all' || kindOf(message) === activeKind) &&
- (!term || searchText(message).includes(term))
+ (!term ||
+ searchText(message).includes(term) ||
+ exportedImageOrVideoFileStem(message) === term)
)
}
const applyFilters = (restorePosition = false) => {
@@ -1779,7 +1794,7 @@ export function renderExportPage(name: string): string {
-
+
diff --git a/tests/unit/export-media.test.ts b/tests/unit/export-media.test.ts
index 7e0f810..876bf43 100644
--- a/tests/unit/export-media.test.ts
+++ b/tests/unit/export-media.test.ts
@@ -33,7 +33,7 @@ describe('export media', () => {
expect(html).toContain('aria-expanded="')
expect(html).toContain('setExpandedTimelineYear')
expect(html).toContain('data-kind="media"')
- expect(html).toContain('placeholder="搜索发送者或消息内容…"')
+ expect(html).toContain('placeholder="搜索发送者、消息内容或媒体文件名(不含后缀)…"')
expect(html).toContain('font-size: 16px;')
expect(html).toContain('filtered.slice(windowStart, windowEnd)')
expect(html).toContain('windowStart = Math.max(0, windowEnd - PAGE_SIZE)')
@@ -156,6 +156,61 @@ describe('export media', () => {
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', () => {
const html = renderExportPage('合并档案')
const dom = new JSDOM(html, { runScripts: 'outside-only' })