feat: 增加失效规则模板保护

这个提交包含在:
Zain 2026-03-25 19:42:50 +08:00
父节点 653cd3fc1b
当前提交 4df5375b92
共有 7 个文件被更改,包括 114 次插入 和 24 次删除

查看文件

@ -11,6 +11,7 @@ import {
type RulePresetRecord,
} from '../../types/rule-preset';
import { I18nService } from '../../utils/i18n';
import { buildRulePresetSummary, isRulePresetConfigValid } from '../../utils/rule-presets';
/**
* Config Panel Component
@ -225,6 +226,10 @@ export class ConfigPanel extends LitElement {
}
private handleApplyPreset(record: RulePresetRecord): void {
if (!isRulePresetConfigValid(record.config)) {
return;
}
this.dispatchEvent(
new CustomEvent('apply-rule-preset', {
detail: { presetId: record.id, source: record.source },
@ -818,16 +823,26 @@ export class ConfigPanel extends LitElement {
private renderPresetItem(record: RulePresetRecord, deletable: boolean) {
const summary = this.buildPresetSummary(record);
const isValid = isRulePresetConfigValid(record.config);
return html`
<div class="preset-item" data-preset-id=${record.id}>
<div class="preset-item ${isValid ? '' : 'is-invalid'}" data-preset-id=${record.id}>
<div class="preset-content">
<div class="preset-name">${record.name || summary}</div>
<div class="preset-name">
${record.name || summary}
${isValid
? ''
: html`<span class="preset-invalid-badge">${I18nService.t('rule_preset_invalid_badge')}</span>`}
</div>
<div class="preset-summary">${summary}</div>
${isValid
? ''
: html`<div class="preset-invalid-hint">${I18nService.t('rule_preset_invalid_hint')}</div>`}
</div>
<div class="preset-actions">
<button
class="text-action-button"
data-role="apply-rule-preset"
?disabled=${!isValid}
@click=${() => this.handleApplyPreset(record)}
>
${I18nService.t('rule_preset_apply_button')}
@ -849,24 +864,7 @@ export class ConfigPanel extends LitElement {
}
private buildPresetSummary(record: RulePresetRecord): string {
switch (record.config.type) {
case 'replace':
return `${I18nService.t('rule_replace')}: ${String(record.config.params.search || '')} → ${String(record.config.params.replace || '')}`;
case 'regex':
return `${I18nService.t('rule_regex')}: ${String(record.config.params.pattern || '')}`;
case 'prefix':
return `${I18nService.t('rule_prefix')}: ${String(record.config.params.prefix || '')}`;
case 'suffix':
return `${I18nService.t('rule_suffix')}: ${String(record.config.params.suffix || '')}`;
case 'numbering':
return `${I18nService.t('rule_numbering')}: ${String(record.config.params.format || '{num}')}`;
case 'sanitize':
return `${I18nService.t('rule_sanitize')}: ${String(record.config.params.removeChars || I18nService.t('param_remove_illegal'))}`;
case 'episodeExtract':
return `${I18nService.t('rule_episode_extract')}: ${String(record.config.params.template || '{prefix}.S{season}E{episode}{ext}')}`;
default:
return record.config.type;
}
return buildRulePresetSummary(record.config);
}
private renderEpisodeExtractAssist() {
@ -1697,14 +1695,32 @@ export class ConfigPanel extends LitElement {
border-top: 1px solid var(--cdr-border, #f0f0f0);
}
.preset-item.is-invalid {
opacity: 0.88;
}
.preset-summary,
.preset-empty,
.assist-sample-name,
.assist-warning {
.assist-warning,
.preset-invalid-hint {
font-size: 12px;
color: var(--cdr-text-secondary, #595959);
}
.preset-invalid-badge {
display: inline-flex;
align-items: center;
margin-left: 6px;
padding: 2px 6px;
border-radius: 999px;
background: var(--cdr-warning-bg, #fff7e6);
border: 1px solid var(--cdr-warning-border, #ffd591);
color: var(--cdr-warning-text, #fa8c16);
font-size: 11px;
font-weight: 500;
}
.assist-sample-name {
margin: 6px 0 10px;
word-break: break-word;

查看文件

@ -51,6 +51,7 @@ import {
getRulePresetById,
getRecentRulePresets,
getTemplateRulePresets,
isRulePresetConfigValid,
recordRecentRulePreset,
saveTemplateRulePreset,
} from '../../utils/rule-presets';
@ -623,6 +624,11 @@ export class FileSelectorPanel extends LitElement {
return;
}
if (!isRulePresetConfigValid(record.config)) {
alert(I18nService.t('rule_preset_invalid'));
return;
}
this.ruleConfig = record.config;
this.episodeExtractAssistState = {
...DEFAULT_EPISODE_EXTRACT_ASSIST_STATE,

查看文件

@ -237,5 +237,8 @@
"episode_assist_apply_full_name": "Fill current field with full text",
"episode_assist_use_item_as_sample": "Use this item as sample",
"episode_assist_use_prefix_from_item": "Try using this as prefix",
"episode_assist_focus_segment_mode": "Switch to segment mode"
"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."
}

查看文件

@ -237,5 +237,8 @@
"episode_assist_apply_full_name": "整段回填到当前字段",
"episode_assist_use_item_as_sample": "将该项设为样本",
"episode_assist_use_prefix_from_item": "尝试提取剧名到 prefix",
"episode_assist_focus_segment_mode": "切换到片段点选模式"
"episode_assist_focus_segment_mode": "切换到片段点选模式",
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
"rule_preset_invalid_badge": "已失效",
"rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。"
}

查看文件

@ -237,5 +237,8 @@
"episode_assist_apply_full_name": "整段回填到目前欄位",
"episode_assist_use_item_as_sample": "將此項設為樣本",
"episode_assist_use_prefix_from_item": "嘗試提取劇名到 prefix",
"episode_assist_focus_segment_mode": "切換到片段點選模式"
"episode_assist_focus_segment_mode": "切換到片段點選模式",
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
"rule_preset_invalid_badge": "已失效",
"rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。"
}

查看文件

@ -21,6 +21,37 @@ describe('ConfigPanel rule preset interactions', () => {
vi.stubGlobal('prompt', vi.fn(() => '剧集模板'));
});
it('marks invalid presets as disabled and does not dispatch apply events', async () => {
const panel = new ConfigPanel();
panel.recentRulePresets = [
createPreset({
id: 'invalid-1',
source: 'recent',
name: '失效模板',
config: { type: 'replace', params: { search: '', replace: '' } },
}),
];
const applyEvents: Array<Event> = [];
panel.addEventListener('apply-rule-preset', (event) => applyEvents.push(event));
document.body.appendChild(panel);
await panel.updateComplete;
const applyButton = panel.shadowRoot?.querySelector<HTMLButtonElement>(
'[data-preset-id="invalid-1"] [data-role="apply-rule-preset"]'
);
const badgeText = panel.shadowRoot?.querySelector('[data-preset-id="invalid-1"] .preset-invalid-badge')?.textContent ?? '';
expect(applyButton?.disabled).toBe(true);
expect(badgeText).toContain('失效');
applyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
expect(applyEvents).toHaveLength(0);
panel.remove();
});
it('renders recent/templates and dispatches apply, save, delete events', async () => {
const panel = new ConfigPanel();
panel.activeRuleConfig = {

查看文件

@ -132,6 +132,34 @@ describe('FileSelectorPanel rule preset integration', () => {
expect(panel.episodeExtractAssistState.fillMode).toBe('full-name');
});
it('blocks invalid preset application and shows alert', async () => {
const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness;
const alertSpy = vi.spyOn(window, 'alert');
vi.spyOn(rulePresets, 'getRulePresetById').mockResolvedValue({
id: 'invalid-template',
name: '失效模板',
source: 'template',
createdAt: 1,
updatedAt: 1,
config: {
type: 'replace',
params: { search: '', replace: '' },
},
});
panel.ruleConfig = {
type: 'prefix',
params: { prefix: 'Keep', separator: '.' },
};
await panel.handleApplyRulePreset(new CustomEvent('apply-rule-preset', { detail: { presetId: 'invalid-template' } }));
expect(alertSpy).toHaveBeenCalledTimes(1);
expect(panel.ruleConfig.type).toBe('prefix');
expect(panel.ruleConfig.params.prefix).toBe('Keep');
});
it('records recent presets after a successful execution', async () => {
const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness;
panel.adapter = new PresetTestAdapter();