feat: surface conflict and error info

这个提交包含在:
Zain 2026-03-20 17:38:22 +08:00
父节点 733978c786
当前提交 add84da7b4
共有 2 个文件被更改,包括 135 次插入 和 8 次删除

查看文件

@ -74,6 +74,15 @@ export class VirtualPreviewList extends LitElement {
* Render a single preview item * Render a single preview item
* @private * @private
*/ */
private resolveErrorText(error?: string): string | undefined {
if (!error) return undefined;
const translationKey = `error_${error}`;
const translated = I18nService.t(translationKey);
return translated === translationKey ? error : translated;
}
private renderPreviewItem(item: PreviewItem): any { private renderPreviewItem(item: PreviewItem): any {
const statusClass = item.conflict const statusClass = item.conflict
? 'conflict' ? 'conflict'
@ -95,19 +104,28 @@ export class VirtualPreviewList extends LitElement {
? html`<span class="status-badge pending">${I18nService.t('status_pending')}</span>` ? html`<span class="status-badge pending">${I18nService.t('status_pending')}</span>`
: ''; : '';
const errorText = this.resolveErrorText(item.error);
const errorTitle = item.error ?? errorText ?? '';
const conflictHint = item.conflict
? html`<span class="conflict-hint">${I18nService.t('error_api_conflict')}</span>`
: '';
return html` return html`
<div class="preview-item ${statusClass}"> <div class="preview-item ${statusClass}">
<div class="preview-content"> <div class="preview-content">
<div class="old-name-row">
<div class="old-name" title=${item.file.name}> <div class="old-name" title=${item.file.name}>
${item.file.name} ${item.file.name}
</div> </div>
<div class="new-name ${statusClass}" title=${item.newName}>
${item.newName}
${statusBadge} ${statusBadge}
</div> </div>
<div class="new-name ${statusClass}" title=${item.newName}>
<span class="new-name-text">${item.newName}</span>
${conflictHint}
</div>
${ ${
item.error errorText
? html`<div class="error-message" title=${item.error}>${item.error}</div>` ? html`<div class="error-message" title=${errorTitle}>${errorText}</div>`
: '' : ''
} }
</div> </div>
@ -232,10 +250,16 @@ export class VirtualPreviewList extends LitElement {
min-width: 0; min-width: 0;
} }
.old-name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.old-name { .old-name {
font-size: 12px; font-size: 12px;
color: var(--cdr-text-secondary, #595959); color: var(--cdr-text-secondary, #595959);
margin-bottom: 4px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -253,6 +277,14 @@ export class VirtualPreviewList extends LitElement {
gap: 8px; gap: 8px;
} }
.new-name-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.new-name.conflict { .new-name.conflict {
color: var(--cdr-warning-text, #fa8c16); color: var(--cdr-warning-text, #fa8c16);
} }
@ -269,6 +301,12 @@ export class VirtualPreviewList extends LitElement {
color: var(--cdr-text-secondary, #595959); color: var(--cdr-text-secondary, #595959);
} }
.conflict-hint {
font-size: 12px;
color: var(--cdr-warning-text, #fa8c16);
flex-shrink: 0;
}
.error-message { .error-message {
margin-top: 4px; margin-top: 4px;
font-size: 12px; font-size: 12px;

查看文件

@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list'; import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list';
import { I18nService } from '../../src/utils/i18n';
describe('VirtualPreviewList pending preview mapping', () => { describe('VirtualPreviewList pending preview mapping', () => {
it('renders original name and new name for a pending preview item', async () => { it('renders original name and new name for a pending preview item', async () => {
@ -29,4 +30,92 @@ describe('VirtualPreviewList pending preview mapping', () => {
element.remove(); element.remove();
}); });
it('renders conflict items with status badge and helper text', async () => {
const element = new VirtualPreviewList();
element.items = [
{
file: {
id: 'file-conflict',
name: 'old-conflict.mkv',
ext: 'mkv',
size: 1,
mtime: Date.now(),
isDir: false,
},
newName: 'new-conflict.mkv',
conflict: true,
done: false,
},
];
document.body.appendChild(element);
await element.updateComplete;
const textContent = element.shadowRoot?.textContent ?? '';
expect(textContent).toContain('old-conflict.mkv');
expect(textContent).toContain('new-conflict.mkv');
expect(textContent).toContain(I18nService.t('preview_badge_conflict'));
expect(textContent).toContain(I18nService.t('error_api_conflict'));
const badge = element.shadowRoot?.querySelector('.old-name-row .status-badge.conflict');
expect(badge).toBeTruthy();
element.remove();
});
it('shows localized extract_episode_not_found message instead of raw error key', async () => {
const element = new VirtualPreviewList();
element.items = [
{
file: {
id: 'file-error',
name: 'old-error.mkv',
ext: 'mkv',
size: 1,
mtime: Date.now(),
isDir: false,
},
newName: 'new-error.mkv',
conflict: false,
error: 'extract_episode_not_found',
},
];
document.body.appendChild(element);
await element.updateComplete;
const textContent = element.shadowRoot?.textContent ?? '';
expect(textContent).toContain(I18nService.t('error_extract_episode_not_found'));
element.remove();
});
it('keeps pending badge next to old name row when showStatus is true', async () => {
const element = new VirtualPreviewList();
element.showStatus = true;
element.items = [
{
file: {
id: 'file-pending',
name: 'old-pending.mkv',
ext: 'mkv',
size: 1,
mtime: Date.now(),
isDir: false,
},
newName: 'new-pending.mkv',
conflict: false,
done: false,
},
];
document.body.appendChild(element);
await element.updateComplete;
const badge = element.shadowRoot?.querySelector('.old-name-row .status-badge.pending');
expect(badge).toBeTruthy();
element.remove();
});
}); });