diff --git a/src/api/__tests__/mediaClassification.spec.ts b/src/api/__tests__/mediaClassification.spec.ts index cf4b7afa..e5f933e3 100644 --- a/src/api/__tests__/mediaClassification.spec.ts +++ b/src/api/__tests__/mediaClassification.spec.ts @@ -50,7 +50,6 @@ function createPolicy(revision = 1): ClassificationPolicy { }, ], fallbacks: { 电影: 'movie' }, - source_fallbacks: {}, field_aliases: {}, updated_at: '2026-09-02T00:00:00Z', } diff --git a/src/api/mediaClassificationTypes.ts b/src/api/mediaClassificationTypes.ts index 61822819..8c93c154 100644 --- a/src/api/mediaClassificationTypes.ts +++ b/src/api/mediaClassificationTypes.ts @@ -102,7 +102,6 @@ export interface ClassificationPolicy { categories: ClassificationCategory[] rules: ClassificationRule[] fallbacks: Partial> - source_fallbacks: Record>> field_aliases: Record> updated_at?: string | null } @@ -221,6 +220,12 @@ export interface ClassificationFieldOption { label: string } +/** 数据源选择器使用的稳定值和人类可读名称。 */ +export interface ClassificationSourceOption { + value: string + title: string +} + /** 动态条件编辑器使用的字段能力目录项。 */ export interface ClassificationFieldDefinition { id: string diff --git a/src/components/classification/ClassificationCategoryEditor.vue b/src/components/classification/ClassificationCategoryEditor.vue index e3e918e3..86ec2328 100644 --- a/src/components/classification/ClassificationCategoryEditor.vue +++ b/src/components/classification/ClassificationCategoryEditor.vue @@ -90,7 +90,7 @@ function parseCategoryPath(pathText: string): ParsedCategoryPath { return { path, error: null } } -/** 返回分类被规则、来源兜底和各媒体类型全局兜底引用的具体原因。 */ +/** 返回分类被规则和各媒体类型全局兜底引用的具体原因。 */ function categoryReferenceReasons(categoryId: string): string[] { const reasons: string[] = [] if (referencedIds.value.has(categoryId)) reasons.push(t('setting.classification.category.ruleReference')) @@ -641,6 +641,18 @@ function updateFallback(mediaType: ClassificationMediaType, categoryId: string | gap: 14px; } +.classification-category-form-grid { + align-items: start; +} + +.classification-category-form-grid > :deep(.v-input) { + align-self: start; +} + +.classification-category-form-grid > :deep(.v-input .v-field) { + min-block-size: var(--v-input-control-height, 56px); +} + .classification-category-error { margin: 0; color: rgb(var(--v-theme-error)); @@ -777,10 +789,7 @@ function updateFallback(mediaType: ClassificationMediaType, categoryId: string | --v-field-border-opacity: 0.72; } -:global(html[data-theme='glass'] .classification-category-dialog .classification-category-form .v-field__overlay), -:global( - html[data-theme='glass'] .classification-category-dialog .classification-category-form .v-selection-control__wrapper -) { +:global(html[data-theme='glass'] .classification-category-dialog .classification-category-form .v-field__overlay) { background-color: var(--glass-control) !important; } @@ -790,13 +799,6 @@ function updateFallback(mediaType: ClassificationMediaType, categoryId: string | .classification-category-form .v-field--focused .v-field__overlay -), -:global( - html[data-theme='glass'] - .classification-category-dialog - .classification-category-form - .v-selection-control--dirty - .v-selection-control__wrapper ) { background-color: var(--glass-control-prominent) !important; } @@ -826,12 +828,8 @@ function updateFallback(mediaType: ClassificationMediaType, categoryId: string | --v-field-border-opacity: 0.72; } -:global(html[data-theme='transparent'] .classification-category-dialog .classification-category-form .v-field__overlay), :global( - html[data-theme='transparent'] - .classification-category-dialog - .classification-category-form - .v-selection-control__wrapper + html[data-theme='transparent'] .classification-category-dialog .classification-category-form .v-field__overlay ) { background-color: rgba(var(--v-theme-surface), var(--transparent-opacity)) !important; } @@ -842,13 +840,6 @@ function updateFallback(mediaType: ClassificationMediaType, categoryId: string | .classification-category-form .v-field--focused .v-field__overlay -), -:global( - html[data-theme='transparent'] - .classification-category-dialog - .classification-category-form - .v-selection-control--dirty - .v-selection-control__wrapper ) { background-color: rgba(var(--v-theme-primary), 0.14) !important; } diff --git a/src/components/classification/ClassificationConditionBuilder.vue b/src/components/classification/ClassificationConditionBuilder.vue index cf782cab..44b7b82c 100644 --- a/src/components/classification/ClassificationConditionBuilder.vue +++ b/src/components/classification/ClassificationConditionBuilder.vue @@ -7,6 +7,7 @@ import type { ClassificationFieldDefinition, ClassificationMediaType, ClassificationOperator, + ClassificationSourceOption, ClassificationSourceSupport, } from '@/api/mediaClassificationTypes' @@ -18,12 +19,14 @@ const props = withDefaults( fields: readonly ClassificationFieldDefinition[] mediaTypes: readonly ClassificationMediaType[] sources: readonly string[] + sourceOptions?: readonly ClassificationSourceOption[] depth?: number maxDepth?: number }>(), { depth: 0, maxDepth: 3, + sourceOptions: () => [], }, ) @@ -55,9 +58,16 @@ const NODE_KIND_ITEMS: ReadonlyArray<{ title: string; value: ConditionNodeKind } { title: '条件', value: 'condition' }, { title: '全部', value: 'all' }, { title: '任一', value: 'any' }, - { title: '非', value: 'not' }, + { title: '排除', value: 'not' }, ] +const NODE_KIND_HINTS: Record = { + condition: '单个条件只判断一个字段。选择“全部”或“任一”后,可以添加同级条件。', + all: '全部满足:所有条件都满足时,规则才会命中。', + any: '任一满足:只要有一个条件满足,规则就会命中。', + not: '排除:括号内条件满足时,规则不会命中。', +} + const OPERATOR_LABELS: Record = { equals: '等于', not_equals: '不等于', @@ -144,6 +154,7 @@ const fieldItems = computed(() => const nodeKind = computed(() => getNodeKind(props.modelValue)) const groupChildren = computed(() => getGroupChildren(props.modelValue)) const canUseGroup = computed(() => props.depth < props.maxDepth) +const nodeKindHint = computed(() => NODE_KIND_HINTS[nodeKind.value]) const canAddChild = computed( () => nodeKind.value !== 'condition' && @@ -204,6 +215,16 @@ const sourceSupportHints = computed(() => { return hints }) +/** 把规则来源限制解释成用户可理解的匹配范围。 */ +const sourceScopeNote = computed(() => { + if (props.sources.length === 0) return '未限定数据来源:这条规则适用于所有来源。' + const names = [...new Set(props.sources)].map(source => { + const option = props.sourceOptions.find(item => item.value === source) + return option?.title ?? source + }) + return `已选数据来源:${names.join('、')}。多个来源表示任意一个来源,不会合并多条媒体信息;字段按当前媒体的实际来源读取。` +}) + const valueControlKind = computed(() => { const condition = selectedCondition.value const definition = selectedDefinition.value @@ -438,6 +459,10 @@ function removeChild(index: number): void { +

+ {{ nodeKindHint }} +

+ @@ -654,6 +683,7 @@ function removeChild(index: number): void { :fields="props.fields" :media-types="props.mediaTypes" :sources="props.sources" + :source-options="props.sourceOptions" :depth="props.depth + 1" :max-depth="props.maxDepth" @update:model-value="updateChild(index, $event)" @@ -680,18 +710,20 @@ function removeChild(index: number): void {
- +
@@ -703,15 +735,15 @@ function removeChild(index: number): void { .classification-condition-builder { min-inline-size: 0; padding: 12px; - border: 1px solid var(--classification-border, rgba(var(--v-border-color), var(--v-border-opacity))); + border: 1px solid var(--classification-border, rgba(var(--v-theme-on-surface), 0.16)); border-radius: 8px; - background: var(--classification-panel, rgba(var(--v-theme-surface-variant), 0.12)); + background: var(--classification-panel, rgba(var(--v-theme-on-surface), 0.04)); } .classification-condition-builder:not([data-depth='0']) { padding: 8px 0 8px 10px; border: 0; - border-inline-start: 2px solid var(--classification-border, rgba(var(--v-border-color), var(--v-border-opacity))); + border-inline-start: 2px solid var(--classification-border, rgba(var(--v-theme-on-surface), 0.16)); border-radius: 0; background: transparent; } @@ -740,6 +772,14 @@ function removeChild(index: number): void { letter-spacing: 0; } +.classification-condition-builder__node-hint, +.classification-condition-builder__source-scope-note { + margin: 8px 0 0; + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + font-size: 0.75rem; + line-height: 1.5; +} + .classification-condition-builder__leaf { display: grid; grid-template-columns: minmax(180px, 1.15fr) minmax(150px, 0.85fr) minmax(220px, 1.4fr); @@ -794,7 +834,7 @@ function removeChild(index: number): void { } .classification-condition-builder__group-actions { - justify-content: flex-end; + justify-content: flex-start; } :global(.classification-field-menu .v-list-item-title), diff --git a/src/components/classification/ClassificationHelpDialog.vue b/src/components/classification/ClassificationHelpDialog.vue index c489a117..9d85a611 100644 --- a/src/components/classification/ClassificationHelpDialog.vue +++ b/src/components/classification/ClassificationHelpDialog.vue @@ -21,7 +21,6 @@ const visible = computed({ const sections = computed(() => [ { icon: 'mdi-file-tree-outline', key: 'categories' }, { icon: 'mdi-filter-cog-outline', key: 'rules' }, - { icon: 'mdi-database-sync-outline', key: 'sources' }, { icon: 'mdi-play-box-outline', key: 'preview' }, { icon: 'mdi-chart-box-outline', key: 'impact' }, { icon: 'mdi-check-decagram-outline', key: 'publish' }, diff --git a/src/components/classification/ClassificationImpactPanel.vue b/src/components/classification/ClassificationImpactPanel.vue index c106455f..b9532912 100644 --- a/src/components/classification/ClassificationImpactPanel.vue +++ b/src/components/classification/ClassificationImpactPanel.vue @@ -217,7 +217,6 @@ function resultSource(result: ClassificationResult): string { const source = resultSelection(result)?.source const labels: Record = { automatic: t('setting.classification.preview.selectionSource.automatic'), - source_fallback: t('setting.classification.preview.selectionSource.sourceFallback'), fallback: t('setting.classification.preview.selectionSource.fallback'), } return source ? (labels[source] ?? source) : t('setting.classification.impact.none') diff --git a/src/components/classification/ClassificationPolicyControlPanel.vue b/src/components/classification/ClassificationPolicyControlPanel.vue index 3abff2da..63b84d82 100644 --- a/src/components/classification/ClassificationPolicyControlPanel.vue +++ b/src/components/classification/ClassificationPolicyControlPanel.vue @@ -561,7 +561,7 @@ watch( gap: 8px; padding: 12px; border-radius: 8px; - background: rgba(var(--v-theme-surface-variant), 0.24); + background: var(--classification-control, rgba(var(--v-theme-on-surface), 0.08)); } .classification-policy-control__metrics { diff --git a/src/components/classification/ClassificationPreviewPanel.vue b/src/components/classification/ClassificationPreviewPanel.vue index e2cf449f..ada0f749 100644 --- a/src/components/classification/ClassificationPreviewPanel.vue +++ b/src/components/classification/ClassificationPreviewPanel.vue @@ -196,7 +196,6 @@ function selectionTitle(selection: ClassificationSelection | null | undefined): function selectionSourceLabel(source: string | null | undefined): string { const labels: Record = { automatic: t('setting.classification.preview.selectionSource.automatic'), - source_fallback: t('setting.classification.preview.selectionSource.sourceFallback'), fallback: t('setting.classification.preview.selectionSource.fallback'), } return source ? (labels[source] ?? source) : t('setting.classification.preview.missing') @@ -757,7 +756,7 @@ function factSourceLabel(source: ClassificationFactSource | null | undefined): s overflow-y: auto; border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); border-radius: 8px; - background: rgba(var(--v-theme-surface-variant), 0.12); + background: var(--classification-panel, rgba(var(--v-theme-on-surface), 0.04)); } .classification-preview__search-results :deep(.v-list-item) { diff --git a/src/components/classification/ClassificationRuleEditor.vue b/src/components/classification/ClassificationRuleEditor.vue index de77b2f1..7c69e024 100644 --- a/src/components/classification/ClassificationRuleEditor.vue +++ b/src/components/classification/ClassificationRuleEditor.vue @@ -1,16 +1,18 @@