From 2559ef127f76463982acab79ecf4bead459e9acd Mon Sep 17 00:00:00 2001 From: Zain Date: Wed, 25 Mar 2026 19:54:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=89=A7=E9=9B=86?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=A4=B1=E8=B4=A5=E9=A1=B9=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E4=B8=8E=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- findings.md | 2 + progress.md | 2 + src/content/components/file-selector-panel.ts | 9 +++- .../components/virtual-preview-list.ts | 45 ++++++++++++++++++- src/locales/en.json | 3 +- src/locales/zh_CN.json | 3 +- src/locales/zh_TW.json | 3 +- .../unit/preview-panel-episode-assist.test.ts | 5 +++ tests/unit/virtual-preview-list.test.ts | 45 +++++++++++++++++++ 9 files changed, 110 insertions(+), 7 deletions(-) diff --git a/findings.md b/findings.md index d353077..bbaf5df 100644 --- a/findings.md +++ b/findings.md @@ -12,3 +12,5 @@ - 2026-03-25:`config-panel` 将 `activeRuleConfig` 的同步逻辑从 `updated()` 调整到 `willUpdate()`,避免 Lit 在测试与运行时发出“update completed 后又触发 update”的低效更新警告。 - 2026-03-25:多语言文案已补齐到 `zh_CN` / `zh_TW` / `en`,并确认占位符必须使用 Chrome i18n 兼容的 `$1` 形式,不能写成 `{0}`。 - 2026-03-25:完整验证结果:`npm run typecheck` 通过,`npx vitest run` 通过(36 files / 263 tests);`npm run lint` 通过但仓库内仍存在大量历史 warning,本次未扩散为新的 lint error。 +- 2026-03-25:继续完成“剧集提取失败态增强”:`suggestedFailureFileId` 现在真正用于右侧预览的失败项高亮与自动滚动定位,避免“点了首个失败项但用户还要自己找”的断层体验。 +- 2026-03-25:为失败项增加内联引导文案 `episode_assist_focus_hint`,明确告诉用户可以直接点整段/片段继续修正,再次预览。 diff --git a/progress.md b/progress.md index 949c563..4025efc 100644 --- a/progress.md +++ b/progress.md @@ -11,3 +11,5 @@ - 2026-03-25:完成 `preview-panel` / `virtual-preview-list` / `file-selector-panel` 的辅助回填接线,实现从预览项整段回填、片段点选、失败项快捷修正、执行成功后自动记录 recent preset。 - 2026-03-25:补齐 `zh_CN` / `zh_TW` / `en` 新文案,并新增 4 个单测文件覆盖 config-panel、preview-panel、file-selector-panel 的新交互。 - 2026-03-25:完成验证:`npm run typecheck` ✅,`npx vitest run` ✅,`npm run lint` ✅(仅剩历史 warning)。 +- 2026-03-25:完成下一批失败态增强:右侧预览支持根据 `suggestedFailureFileId` 自动高亮并滚动定位失败项,且在整段/片段回填成功后清除聚焦态,避免提示残留。 +- 2026-03-25:补充失败态引导文案与测试,验证通过:`typecheck` ✅,`vitest` 全量 266 tests ✅,`lint` ✅(仅历史 warning)。 diff --git a/src/content/components/file-selector-panel.ts b/src/content/components/file-selector-panel.ts index f502ca1..7582403 100644 --- a/src/content/components/file-selector-panel.ts +++ b/src/content/components/file-selector-panel.ts @@ -570,12 +570,16 @@ export class FileSelectorPanel extends LitElement { private handleAssistApplyFullName(e: CustomEvent<{ fileId: string; fileName: string }>): void { this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.fileName); - this.setAssistState({ sampleFileId: e.detail.fileId, sampleFileName: e.detail.fileName }); + this.setAssistState({ + sampleFileId: e.detail.fileId, + sampleFileName: e.detail.fileName, + suggestedFailureFileId: null, + }); } private handleAssistApplySegment(e: CustomEvent<{ fileId: string; segment: string }>): void { this.applyAssistValue(this.episodeExtractAssistState.fillTarget, e.detail.segment); - this.setAssistState({ sampleFileId: e.detail.fileId }); + this.setAssistState({ sampleFileId: e.detail.fileId, suggestedFailureFileId: null }); } private handleAssistUseItemAsSample(e: CustomEvent<{ fileId: string; fileName: string }>): void { @@ -605,6 +609,7 @@ export class FileSelectorPanel extends LitElement { sampleFileId: e.detail.fileId, sampleFileName: raw, fillTarget: 'prefix', + suggestedFailureFileId: null, }); } diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index 49ec55a..fb765c6 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -44,6 +44,7 @@ export class VirtualPreviewList extends LitElement { */ private _virtualizerInitialized = false; private _itemsKey: string | null = null; + private _lastFocusedFailureId: string | null = null; private shouldVirtualize(): boolean { return this.items.length > VirtualPreviewList.VIRTUALIZE_MIN_ITEMS; @@ -124,11 +125,16 @@ export class VirtualPreviewList extends LitElement { const conflictHint = item.conflict ? html`${I18nService.t('error_api_conflict')}` : ''; - const previewItemClasses = ['preview-item', statusClass].filter(Boolean).join(' '); + const isSuggestedFailure = + this.episodeExtractAssistEnabled && + this.episodeExtractAssistState.suggestedFailureFileId === item.file.id; + const previewItemClasses = ['preview-item', statusClass, isSuggestedFailure ? 'assist-focus' : ''] + .filter(Boolean) + .join(' '); const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' '); return html` -
+
@@ -145,6 +151,9 @@ export class VirtualPreviewList extends LitElement { ? html`
${errorText}
` : '' } + ${isSuggestedFailure + ? html`
${I18nService.t('episode_assist_focus_hint')}
` + : ''} ${this.renderAssistActions(item)}
@@ -251,6 +260,24 @@ export class VirtualPreviewList extends LitElement { protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); + const suggestedFailureFileId = this.episodeExtractAssistState.suggestedFailureFileId; + if ( + this.episodeExtractAssistEnabled && + suggestedFailureFileId && + suggestedFailureFileId !== this._lastFocusedFailureId && + (changedProperties.has('episodeExtractAssistState') || changedProperties.has('items')) + ) { + this._lastFocusedFailureId = suggestedFailureFileId; + requestAnimationFrame(() => { + const target = this.renderRoot.querySelector(`[data-file-id="${suggestedFailureFileId}"]`); + target?.scrollIntoView({ block: 'center', behavior: 'smooth' }); + }); + } + + if (!suggestedFailureFileId) { + this._lastFocusedFailureId = null; + } + if (changedProperties.has('items') && this.items.length > 0) { const nextKey = this.computeItemsKey(this.items); if (nextKey !== this._itemsKey) { @@ -356,6 +383,10 @@ export class VirtualPreviewList extends LitElement { border-left: 3px solid var(--cdr-border-strong, #d9d9d9); } + .preview-item.assist-focus { + box-shadow: inset 0 0 0 1px var(--cdr-primary, #1890ff), 0 0 0 2px rgba(24, 144, 255, 0.12); + } + .preview-content { flex: 1; min-width: 0; @@ -434,6 +465,16 @@ export class VirtualPreviewList extends LitElement { word-break: break-word; } + .assist-focus-hint { + margin-top: 6px; + font-size: 12px; + color: var(--cdr-info-text, #0958d9); + background: var(--cdr-info-bg, #e6f4ff); + border: 1px solid var(--cdr-info-border, #91caff); + border-radius: 8px; + padding: 6px 8px; + } + .assist-actions { display: flex; flex-wrap: wrap; diff --git a/src/locales/en.json b/src/locales/en.json index 6cad55a..a9ba247 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -240,5 +240,6 @@ "episode_assist_focus_segment_mode": "Switch to segment mode", "rule_preset_invalid": "This template is incompatible with the current version. Please delete and save it again.", "rule_preset_invalid_badge": "Invalid", - "rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied." + "rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied.", + "episode_assist_focus_hint": "The failed item is highlighted. Fill from the full text or a segment, then preview again." } diff --git a/src/locales/zh_CN.json b/src/locales/zh_CN.json index c984e0d..9735670 100644 --- a/src/locales/zh_CN.json +++ b/src/locales/zh_CN.json @@ -240,5 +240,6 @@ "episode_assist_focus_segment_mode": "切换到片段点选模式", "rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。", "rule_preset_invalid_badge": "已失效", - "rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。" + "rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。", + "episode_assist_focus_hint": "已为你定位失败项,可直接点整段或片段回填后继续预览。" } diff --git a/src/locales/zh_TW.json b/src/locales/zh_TW.json index 91f04b2..c0abdd2 100644 --- a/src/locales/zh_TW.json +++ b/src/locales/zh_TW.json @@ -240,5 +240,6 @@ "episode_assist_focus_segment_mode": "切換到片段點選模式", "rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。", "rule_preset_invalid_badge": "已失效", - "rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。" + "rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。", + "episode_assist_focus_hint": "已為你定位失敗項,可直接點整段或片段回填後繼續預覽。" } diff --git a/tests/unit/preview-panel-episode-assist.test.ts b/tests/unit/preview-panel-episode-assist.test.ts index 9b1f792..ed729d0 100644 --- a/tests/unit/preview-panel-episode-assist.test.ts +++ b/tests/unit/preview-panel-episode-assist.test.ts @@ -71,6 +71,11 @@ describe('PreviewPanel episode assist bridge', () => { expect(prefixEvents).toHaveLength(1); expect(modeEvents).toHaveLength(1); + const focusedItem = previewList?.shadowRoot?.querySelector('[data-file-id="file-1"]'); + const focusHint = previewList?.shadowRoot?.querySelector('[data-role="episode-assist-focus-hint"]'); + expect(focusedItem?.className).toContain('assist-focus'); + expect(focusHint?.textContent).toContain('定位'); + panel.remove(); }); }); diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 2c55b6b..9a27c5c 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -151,6 +151,51 @@ describe('VirtualPreviewList pending preview mapping', () => { element.remove(); }); + it('highlights and scrolls to the suggested failure item in assist mode', async () => { + const originalScrollIntoView = HTMLElement.prototype.scrollIntoView; + const scrollSpy = vi.fn(); + HTMLElement.prototype.scrollIntoView = scrollSpy; + + const element = new VirtualPreviewList(); + element.episodeExtractAssistEnabled = true; + element.episodeExtractAssistState = { + sampleFileId: 'file-error', + sampleFileName: 'Show.S01E01', + fillTarget: 'prefix', + fillMode: 'segment', + suggestedFailureFileId: 'file-error', + }; + element.items = [ + { + file: { + id: 'file-error', + name: 'Show.S01E01.mkv', + ext: '.mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'Show.S01E01.mkv', + conflict: false, + error: 'extract_episode_not_found', + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined))); + + const focusedItem = element.shadowRoot?.querySelector('[data-file-id="file-error"]'); + const hint = element.shadowRoot?.querySelector('[data-role="episode-assist-focus-hint"]'); + + expect(focusedItem?.className).toContain('assist-focus'); + expect(hint?.textContent).toContain(I18nService.t('episode_assist_focus_hint')); + expect(scrollSpy).toHaveBeenCalled(); + + HTMLElement.prototype.scrollIntoView = originalScrollIntoView; + element.remove(); + }); + it('locks preview-item structure with semantic classes and target-specific truncation rules', async () => { const element = new VirtualPreviewList(); element.items = [