镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat: 增强剧集提取失败项定位与引导
这个提交包含在:
父节点
4df5375b92
当前提交
2559ef127f
@ -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`,明确告诉用户可以直接点整段/片段继续修正,再次预览。
|
||||
|
||||
@ -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)。
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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`<span class="conflict-hint">${I18nService.t('error_api_conflict')}</span>`
|
||||
: '';
|
||||
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`
|
||||
<div class="${previewItemClasses}">
|
||||
<div class="${previewItemClasses}" data-file-id=${item.file.id}>
|
||||
<div class="preview-content">
|
||||
<div class="old-name-row">
|
||||
<div class="old-name old-name-secondary" title=${item.file.name}>
|
||||
@ -145,6 +151,9 @@ export class VirtualPreviewList extends LitElement {
|
||||
? html`<div class="error-message" title=${errorTitle}>${errorText}</div>`
|
||||
: ''
|
||||
}
|
||||
${isSuggestedFailure
|
||||
? html`<div class="assist-focus-hint" data-role="episode-assist-focus-hint">${I18nService.t('episode_assist_focus_hint')}</div>`
|
||||
: ''}
|
||||
${this.renderAssistActions(item)}
|
||||
</div>
|
||||
</div>
|
||||
@ -251,6 +260,24 @@ export class VirtualPreviewList extends LitElement {
|
||||
protected updated(changedProperties: PropertyValues<this>): 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<HTMLElement>(`[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;
|
||||
|
||||
@ -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."
|
||||
}
|
||||
|
||||
@ -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": "已为你定位失败项,可直接点整段或片段回填后继续预览。"
|
||||
}
|
||||
|
||||
@ -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": "已為你定位失敗項,可直接點整段或片段回填後繼續預覽。"
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 = [
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户