mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
feat: enhance recognition and rule testing features
- Updated maxWidth for NameTestView and RuleTestView dialogs to improve layout. - Added new localization strings for recognition and rule testing in English, Simplified Chinese, and Traditional Chinese. - Refactored NameTestView to include detailed analysis steps and summary items for better user feedback. - Improved error handling and loading states in both NameTestView and RuleTestView. - Redesigned form layouts for NameTestView and RuleTestView to enhance user experience and accessibility. - Added computed properties for better data handling and display in the recognition and rule testing processes.
This commit is contained in:
@@ -1425,6 +1425,28 @@ export interface FilterRuleGroup {
|
||||
category?: string
|
||||
}
|
||||
|
||||
// 规则测试结果
|
||||
export interface RuleTestData {
|
||||
// 原始标题
|
||||
title?: string
|
||||
// 原始副标题
|
||||
subtitle?: string
|
||||
// 规则组名称
|
||||
rulegroup_name?: string
|
||||
// 规则组详情
|
||||
rulegroup?: FilterRuleGroup | null
|
||||
// 识别元数据
|
||||
meta_info?: MetaInfo | null
|
||||
// 媒体信息
|
||||
media_info?: MediaInfo | null
|
||||
// 种子信息
|
||||
torrent_info?: TorrentInfo | null
|
||||
// 优先级
|
||||
priority?: number | null
|
||||
// 是否命中过滤规则
|
||||
matched?: boolean
|
||||
}
|
||||
|
||||
// 订阅下载文件详情
|
||||
export interface SubscribeDownloadFileInfo {
|
||||
// 种子名称
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useShortcutTools() {
|
||||
icon: 'mdi-text-recognition',
|
||||
dialog: 'nameTest',
|
||||
component: NameTestView,
|
||||
maxWidth: '45rem',
|
||||
maxWidth: '65rem',
|
||||
titleText: t('shortcut.recognition.title'),
|
||||
},
|
||||
{
|
||||
@@ -58,6 +58,7 @@ export function useShortcutTools() {
|
||||
icon: 'mdi-filter-cog',
|
||||
dialog: 'ruleTest',
|
||||
component: RuleTestView,
|
||||
maxWidth: '65rem',
|
||||
titleText: t('shortcut.rule.subtitle'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1536,6 +1536,39 @@ export default {
|
||||
recognizeAgain: 'Recognize Again',
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
requestFailed: 'Recognition request failed',
|
||||
inputTitle: 'Test Input',
|
||||
inputSubtitle: 'Enter a torrent or file name to inspect the recognition breakdown',
|
||||
unrecognized: 'No media recognized',
|
||||
waitingResult: 'Waiting for recognition result',
|
||||
analysisTitle: 'Analysis Flow',
|
||||
analysisSubtitle: 'Title preprocessing, meta info, and media match',
|
||||
summary: {
|
||||
year: 'Year',
|
||||
episode: 'Episode',
|
||||
type: 'Type',
|
||||
source: 'Source',
|
||||
},
|
||||
steps: {
|
||||
original: {
|
||||
title: 'Original Title',
|
||||
caption: 'The raw string entering recognition',
|
||||
},
|
||||
words: {
|
||||
title: 'Custom Words',
|
||||
caption: 'Applied custom recognition words',
|
||||
value: '{count} matched',
|
||||
none: 'No custom words applied',
|
||||
},
|
||||
meta: {
|
||||
title: 'Meta Info',
|
||||
caption: 'Parsed name, episodes, and resource terms',
|
||||
},
|
||||
media: {
|
||||
title: 'Media Match',
|
||||
caption: 'Final matched media source',
|
||||
},
|
||||
},
|
||||
},
|
||||
netTest: {
|
||||
notTested: 'Not Tested',
|
||||
@@ -1549,8 +1582,46 @@ export default {
|
||||
title: 'Title',
|
||||
subtitle: 'Subtitle',
|
||||
ruleGroup: 'Rule Group',
|
||||
ruleGroupPlaceholder: 'Please select',
|
||||
priority: 'Priority: {value}',
|
||||
noPriorityRule: 'No priority rule matched!',
|
||||
requestFailed: 'Rule test request failed',
|
||||
inputTitle: 'Rule Test',
|
||||
inputSubtitle: 'Select a rule group to inspect filter matching and priority',
|
||||
waitingResult: 'Waiting for rule test result',
|
||||
matched: 'Filter rule matched',
|
||||
priorityLabel: 'Priority',
|
||||
analysisTitle: 'Filter Flow',
|
||||
analysisSubtitle: 'Rule group, media recognition, filter match, and priority order',
|
||||
ruleCount: '{count} rules',
|
||||
summary: {
|
||||
priority: 'Priority',
|
||||
ruleGroup: 'Rule Group',
|
||||
ruleCount: 'Rules',
|
||||
media: 'Media',
|
||||
},
|
||||
steps: {
|
||||
group: {
|
||||
title: 'Rule Group',
|
||||
caption: 'Current group contains {count} priority rules',
|
||||
empty: 'No rules in this group',
|
||||
},
|
||||
media: {
|
||||
title: 'Media Recognition',
|
||||
caption: 'Recognize media context before filtering',
|
||||
none: 'No media recognized',
|
||||
},
|
||||
filter: {
|
||||
title: 'Filter Match',
|
||||
matched: 'Resource passes the filter rules',
|
||||
pending: 'No match result returned',
|
||||
},
|
||||
priority: {
|
||||
title: 'Priority Result',
|
||||
caption: 'Converted priority for the matched resource',
|
||||
empty: 'No priority generated',
|
||||
},
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
about: {
|
||||
|
||||
@@ -1529,6 +1529,39 @@ export default {
|
||||
recognizeAgain: '重新识别',
|
||||
title: '标题',
|
||||
subtitle: '副标题',
|
||||
requestFailed: '识别请求失败',
|
||||
inputTitle: '测试输入',
|
||||
inputSubtitle: '输入种子名或文件名,查看媒体识别拆解结果',
|
||||
unrecognized: '未识别到媒体',
|
||||
waitingResult: '等待识别结果',
|
||||
analysisTitle: '解析链路',
|
||||
analysisSubtitle: '标题预处理、元信息和媒体匹配结果',
|
||||
summary: {
|
||||
year: '年份',
|
||||
episode: '季集',
|
||||
type: '类型',
|
||||
source: '数据源',
|
||||
},
|
||||
steps: {
|
||||
original: {
|
||||
title: '原始标题',
|
||||
caption: '进入识别流程的原始字符串',
|
||||
},
|
||||
words: {
|
||||
title: '识别词',
|
||||
caption: '自定义识别词应用情况',
|
||||
value: '命中 {count} 条',
|
||||
none: '未应用识别词',
|
||||
},
|
||||
meta: {
|
||||
title: '元信息',
|
||||
caption: '解析出的名称、季集和资源信息',
|
||||
},
|
||||
media: {
|
||||
title: '媒体匹配',
|
||||
caption: '最终匹配到的媒体数据源',
|
||||
},
|
||||
},
|
||||
},
|
||||
netTest: {
|
||||
notTested: '未测试',
|
||||
@@ -1542,8 +1575,46 @@ export default {
|
||||
title: '标题',
|
||||
subtitle: '副标题',
|
||||
ruleGroup: '规则组',
|
||||
ruleGroupPlaceholder: '请选择',
|
||||
priority: '优先级:{value}',
|
||||
noPriorityRule: '未命中任何优先级规则!',
|
||||
requestFailed: '规则测试请求失败',
|
||||
inputTitle: '规则测试',
|
||||
inputSubtitle: '选择规则组后,查看过滤命中和优先级结果',
|
||||
waitingResult: '等待规则测试结果',
|
||||
matched: '命中过滤规则',
|
||||
priorityLabel: '优先级',
|
||||
analysisTitle: '过滤链路',
|
||||
analysisSubtitle: '规则组、媒体识别、过滤命中和优先级排序',
|
||||
ruleCount: '{count} 条',
|
||||
summary: {
|
||||
priority: '优先级',
|
||||
ruleGroup: '规则组',
|
||||
ruleCount: '规则数',
|
||||
media: '媒体',
|
||||
},
|
||||
steps: {
|
||||
group: {
|
||||
title: '规则组',
|
||||
caption: '当前规则组包含 {count} 条优先级规则',
|
||||
empty: '规则组暂无规则',
|
||||
},
|
||||
media: {
|
||||
title: '媒体识别',
|
||||
caption: '过滤前先识别媒体上下文',
|
||||
none: '未识别到媒体',
|
||||
},
|
||||
filter: {
|
||||
title: '过滤命中',
|
||||
matched: '资源符合过滤规则',
|
||||
pending: '未得到命中结果',
|
||||
},
|
||||
priority: {
|
||||
title: '排序结果',
|
||||
caption: '命中资源换算后的优先级',
|
||||
empty: '未生成优先级',
|
||||
},
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
about: {
|
||||
|
||||
@@ -1528,6 +1528,39 @@ export default {
|
||||
recognizeAgain: '重新識別',
|
||||
title: '標題',
|
||||
subtitle: '副標題',
|
||||
requestFailed: '識別請求失敗',
|
||||
inputTitle: '測試輸入',
|
||||
inputSubtitle: '輸入種子名或檔案名,查看媒體識別拆解結果',
|
||||
unrecognized: '未識別到媒體',
|
||||
waitingResult: '等待識別結果',
|
||||
analysisTitle: '解析鏈路',
|
||||
analysisSubtitle: '標題預處理、元資訊和媒體匹配結果',
|
||||
summary: {
|
||||
year: '年份',
|
||||
episode: '季集',
|
||||
type: '類型',
|
||||
source: '資料源',
|
||||
},
|
||||
steps: {
|
||||
original: {
|
||||
title: '原始標題',
|
||||
caption: '進入識別流程的原始字串',
|
||||
},
|
||||
words: {
|
||||
title: '識別詞',
|
||||
caption: '自訂識別詞應用情況',
|
||||
value: '命中 {count} 條',
|
||||
none: '未套用識別詞',
|
||||
},
|
||||
meta: {
|
||||
title: '元資訊',
|
||||
caption: '解析出的名稱、季集和資源資訊',
|
||||
},
|
||||
media: {
|
||||
title: '媒體匹配',
|
||||
caption: '最終匹配到的媒體資料源',
|
||||
},
|
||||
},
|
||||
},
|
||||
netTest: {
|
||||
notTested: '未測試',
|
||||
@@ -1541,8 +1574,46 @@ export default {
|
||||
title: '標題',
|
||||
subtitle: '副標題',
|
||||
ruleGroup: '規則組',
|
||||
ruleGroupPlaceholder: '請選擇',
|
||||
priority: '優先級:{value}',
|
||||
noPriorityRule: '未命中任何優先級規則!',
|
||||
requestFailed: '規則測試請求失敗',
|
||||
inputTitle: '規則測試',
|
||||
inputSubtitle: '選擇規則組後,查看過濾命中和優先級結果',
|
||||
waitingResult: '等待規則測試結果',
|
||||
matched: '命中過濾規則',
|
||||
priorityLabel: '優先級',
|
||||
analysisTitle: '過濾鏈路',
|
||||
analysisSubtitle: '規則組、媒體識別、過濾命中和優先級排序',
|
||||
ruleCount: '{count} 條',
|
||||
summary: {
|
||||
priority: '優先級',
|
||||
ruleGroup: '規則組',
|
||||
ruleCount: '規則數',
|
||||
media: '媒體',
|
||||
},
|
||||
steps: {
|
||||
group: {
|
||||
title: '規則組',
|
||||
caption: '目前規則組包含 {count} 條優先級規則',
|
||||
empty: '規則組暫無規則',
|
||||
},
|
||||
media: {
|
||||
title: '媒體識別',
|
||||
caption: '過濾前先識別媒體上下文',
|
||||
none: '未識別到媒體',
|
||||
},
|
||||
filter: {
|
||||
title: '過濾命中',
|
||||
matched: '資源符合過濾規則',
|
||||
pending: '未取得命中結果',
|
||||
},
|
||||
priority: {
|
||||
title: '排序結果',
|
||||
caption: '命中資源換算後的優先級',
|
||||
empty: '未產生優先級',
|
||||
},
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
about: {
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import type { Context } from '@/api/types'
|
||||
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface SummaryItem {
|
||||
icon: string
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface AnalysisStep {
|
||||
icon: string
|
||||
title: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -27,64 +38,480 @@ const nameTestText = ref(t('nameTest.recognize'))
|
||||
// 是否显示结果
|
||||
const showResult = ref(false)
|
||||
|
||||
// 调用API识别
|
||||
// 请求错误提示
|
||||
const nameTestError = ref('')
|
||||
|
||||
const metaInfo = computed(() => nameTestResult.value?.meta_info)
|
||||
const mediaInfo = computed(() => nameTestResult.value?.media_info)
|
||||
const isRecognized = computed(() => Boolean(metaInfo.value?.name))
|
||||
const resultTitle = computed(() => mediaInfo.value?.title || metaInfo.value?.name || t('nameTest.unrecognized'))
|
||||
const resultSubtitle = computed(() => {
|
||||
const parts = [mediaInfo.value?.year || metaInfo.value?.year, mediaInfo.value?.type || metaInfo.value?.type]
|
||||
if (metaInfo.value?.season_episode) parts.push(metaInfo.value.season_episode)
|
||||
return parts.filter(Boolean).join(' · ') || t('nameTest.waitingResult')
|
||||
})
|
||||
const resourceChips = computed(() => {
|
||||
return [
|
||||
metaInfo.value?.web_source,
|
||||
metaInfo.value?.edition,
|
||||
metaInfo.value?.resource_pix,
|
||||
metaInfo.value?.video_encode,
|
||||
metaInfo.value?.audio_encode,
|
||||
metaInfo.value?.resource_team,
|
||||
].filter(Boolean) as string[]
|
||||
})
|
||||
const summaryItems = computed<SummaryItem[]>(() => [
|
||||
{
|
||||
icon: 'mdi-calendar-range',
|
||||
label: t('nameTest.summary.year'),
|
||||
value: mediaInfo.value?.year || metaInfo.value?.year || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-format-list-numbered',
|
||||
label: t('nameTest.summary.episode'),
|
||||
value: metaInfo.value?.season_episode || metaInfo.value?.episode || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-shape-outline',
|
||||
label: t('nameTest.summary.type'),
|
||||
value: mediaInfo.value?.type || metaInfo.value?.type || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-database-search',
|
||||
label: t('nameTest.summary.source'),
|
||||
value: mediaInfo.value?.source || mediaInfo.value?.tmdb_id?.toString() || mediaInfo.value?.douban_id || '-',
|
||||
},
|
||||
])
|
||||
const analysisSteps = computed<AnalysisStep[]>(() => [
|
||||
{
|
||||
icon: 'mdi-file-document-outline',
|
||||
title: t('nameTest.steps.original.title'),
|
||||
value: metaInfo.value?.org_string || nameTestForm.title || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-puzzle-check-outline',
|
||||
title: t('nameTest.steps.meta.title'),
|
||||
value: [
|
||||
metaInfo.value?.name,
|
||||
metaInfo.value?.season_episode,
|
||||
metaInfo.value?.resource_term,
|
||||
metaInfo.value?.release_group,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ') || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-movie-search-outline',
|
||||
title: t('nameTest.steps.media.title'),
|
||||
value: mediaInfo.value?.tmdb_id
|
||||
? `TMDB ${mediaInfo.value.tmdb_id}`
|
||||
: mediaInfo.value?.douban_id
|
||||
? `Douban ${mediaInfo.value.douban_id}`
|
||||
: mediaInfo.value?.title || '-',
|
||||
},
|
||||
])
|
||||
|
||||
/** 将 TMDB 原始图片地址转换为弹窗内更轻量的海报缩略图。 */
|
||||
function getPosterImage(url = '') {
|
||||
if (!url) return ''
|
||||
return url.replace('original', 'w500')
|
||||
}
|
||||
|
||||
/** 调用媒体识别接口并刷新解析工作台。 */
|
||||
async function nameTest() {
|
||||
if (!nameTestForm.title) return
|
||||
|
||||
try {
|
||||
nameTestLoading.value = true
|
||||
nameTestText.value = t('nameTest.recognizing')
|
||||
nameTestError.value = ''
|
||||
showResult.value = false
|
||||
nameTestResult.value = await api.get('media/recognize', {
|
||||
nameTestResult.value = await api.get<Context, Context>('media/recognize', {
|
||||
params: {
|
||||
title: nameTestForm.title,
|
||||
subtitle: nameTestForm.subtitle,
|
||||
},
|
||||
})
|
||||
nameTestLoading.value = false
|
||||
nameTestText.value = t('nameTest.recognizeAgain')
|
||||
showResult.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
nameTestError.value = error instanceof Error ? error.message : t('nameTest.requestFailed')
|
||||
} finally {
|
||||
nameTestLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow class="pt-2">
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="nameTestForm.title"
|
||||
:label="t('nameTest.title')"
|
||||
:rules="[requiredValidator]"
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="nameTestForm.subtitle"
|
||||
:label="t('nameTest.subtitle')"
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-subtitles"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" class="text-center">
|
||||
<VBtn :disabled="nameTestLoading" @click="nameTest">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-text-recognition" />
|
||||
</template>
|
||||
{{ nameTestText }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<VExpandTransition>
|
||||
<div v-show="showResult">
|
||||
<MediaInfoCard :context="nameTestResult" />
|
||||
<div class="shortcut-workbench">
|
||||
<section class="shortcut-panel shortcut-input-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('nameTest.inputTitle') }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ t('nameTest.inputSubtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
<VIcon icon="mdi-text-recognition" color="primary" />
|
||||
</div>
|
||||
|
||||
<VForm validate-on="submit lazy" @submit.prevent="nameTest">
|
||||
<VRow class="shortcut-form">
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VTextField
|
||||
v-model="nameTestForm.title"
|
||||
:label="t('nameTest.title')"
|
||||
:rules="[requiredValidator]"
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VTextarea
|
||||
v-model="nameTestForm.subtitle"
|
||||
:label="t('nameTest.subtitle')"
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-subtitles"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VBtn block type="submit" :disabled="nameTestLoading" :loading="nameTestLoading">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-movie-search-outline" />
|
||||
</template>
|
||||
{{ nameTestText }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
|
||||
<VAlert
|
||||
v-if="nameTestError"
|
||||
class="mt-4"
|
||||
density="comfortable"
|
||||
icon="mdi-alert-circle-outline"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ nameTestError }}
|
||||
</VAlert>
|
||||
</section>
|
||||
|
||||
<section class="shortcut-panel shortcut-result-panel">
|
||||
<div v-if="showResult" class="result-stack">
|
||||
<div class="media-result-card" :class="{ 'media-result-card--failed': !isRecognized }">
|
||||
<div v-if="mediaInfo?.poster_path" class="poster-frame">
|
||||
<VImg :src="getPosterImage(mediaInfo.poster_path)" aspect-ratio="2/3" cover>
|
||||
<template #placeholder>
|
||||
<VSkeletonLoader class="h-100 w-100" />
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div v-else class="poster-frame poster-frame--empty">
|
||||
<VIcon :icon="isRecognized ? 'mdi-movie-open-check' : 'mdi-movie-open-remove'" size="32" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<div class="result-heading">
|
||||
<VIcon :icon="isRecognized ? 'mdi-check-circle-outline' : 'mdi-alert-circle-outline'" color="primary" />
|
||||
<span class="result-title-text text-subtitle-1 font-weight-medium text-truncate">{{ resultTitle }}</span>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ resultSubtitle }}
|
||||
</div>
|
||||
<div class="chip-row mt-3">
|
||||
<VChip
|
||||
v-for="chip in resourceChips"
|
||||
:key="chip"
|
||||
class="result-chip"
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ chip }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div v-for="item in summaryItems" :key="item.label" class="summary-tile">
|
||||
<VIcon :icon="item.icon" size="18" class="summary-icon" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div class="text-body-2 font-weight-medium text-truncate">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<VIcon icon="mdi-movie-search-outline" size="36" />
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ t('nameTest.waitingResult') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section v-if="showResult" class="shortcut-panel analysis-panel mt-4">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('nameTest.analysisTitle') }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ t('nameTest.analysisSubtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
|
||||
<div class="analysis-flow">
|
||||
<div v-for="step in analysisSteps" :key="step.title" class="analysis-step">
|
||||
<VIcon :icon="step.icon" color="primary" size="20" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-body-2 font-weight-medium">
|
||||
{{ step.title }}
|
||||
</div>
|
||||
<div class="text-body-2 step-value">
|
||||
{{ step.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section v-if="showResult && metaInfo?.apply_words?.length" class="shortcut-panel applied-words-panel mt-4">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('nameTest.steps.words.title') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="words-list">
|
||||
<div v-if="metaInfo.org_string" class="word-row word-row--source">
|
||||
<span class="text-caption text-medium-emphasis">{{ t('nameTest.steps.original.title') }}</span>
|
||||
<span class="word-text">{{ metaInfo.org_string }}</span>
|
||||
</div>
|
||||
<div v-for="word in metaInfo.apply_words" :key="word" class="word-row">
|
||||
<span class="word-text">{{ word }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shortcut-workbench {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
gap: 1rem;
|
||||
padding-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
.shortcut-panel {
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
background: var(--app-grouped-list-background);
|
||||
box-shadow: var(--app-surface-shadow);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.shortcut-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shortcut-form-col {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.shortcut-form-col:first-child {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.shortcut-form-col:last-child {
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
.result-stack {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.media-result-card {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem minmax(0, 1fr);
|
||||
gap: 0.85rem;
|
||||
align-items: center;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.media-result-card--failed {
|
||||
background: rgba(var(--v-theme-error), 0.08);
|
||||
}
|
||||
|
||||
.poster-frame {
|
||||
overflow: hidden;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-control-radius);
|
||||
aspect-ratio: 2 / 3;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.35);
|
||||
}
|
||||
|
||||
.poster-frame--empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.result-chip {
|
||||
max-inline-size: 100%;
|
||||
}
|
||||
|
||||
.result-heading {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-title-text {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.summary-tile {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.22);
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
min-block-size: 14rem;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 0.75rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.analysis-flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.analysis-step {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.55rem;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.2);
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.step-value {
|
||||
overflow: hidden;
|
||||
margin-block-start: 0.35rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.words-list {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.word-row {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.2);
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.word-row--source {
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
.word-text {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shortcut-workbench,
|
||||
.analysis-flow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.result-heading {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.35rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.result-title-text {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.shortcut-panel {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.media-result-card {
|
||||
grid-template-columns: 4.25rem minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { requiredValidator } from '@/@validators'
|
||||
import api from '@/api'
|
||||
import { FilterRuleGroup } from '@/api/types'
|
||||
import type { ApiResponse, FilterRuleGroup, RuleTestData } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface SummaryItem {
|
||||
icon: string
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface AnalysisStep {
|
||||
icon: string
|
||||
title: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface FormValidationResult {
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
interface RuleTestFormRef {
|
||||
validate: () => Promise<FormValidationResult>
|
||||
}
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
// 规则测试表单引用
|
||||
const ruleTestFormRef = ref<RuleTestFormRef>()
|
||||
|
||||
// 识别结果
|
||||
const ruleTestResult = ref('')
|
||||
const ruleTestResponse = ref<ApiResponse<RuleTestData>>()
|
||||
|
||||
// 名称识别表单
|
||||
const ruleTestForm = reactive({
|
||||
title: null,
|
||||
subtitle: null,
|
||||
rulegroup: null,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
rulegroup: '',
|
||||
})
|
||||
|
||||
// 识别按钮状态
|
||||
@@ -27,47 +50,145 @@ const ruleTestText = ref(t('ruleTest.test'))
|
||||
// 是否显示结果
|
||||
const showResult = ref(false)
|
||||
|
||||
// 请求错误提示
|
||||
const ruleTestError = ref('')
|
||||
|
||||
// 所有规则组列表
|
||||
const filterRuleGroups = ref<FilterRuleGroup[]>([])
|
||||
|
||||
// 规则组加载状态
|
||||
const filterRuleGroupLoading = ref(false)
|
||||
|
||||
// 规则组选项
|
||||
const filterRuleGroupItems = computed(() => {
|
||||
return filterRuleGroups.value.map(item => ({ title: item.name, value: item.name }))
|
||||
return [
|
||||
{ title: t('ruleTest.ruleGroupPlaceholder'), value: '' },
|
||||
...filterRuleGroups.value.map(item => ({ title: item.name, value: item.name })),
|
||||
]
|
||||
})
|
||||
const selectedRuleGroup = computed(() => filterRuleGroups.value.find(item => item.name === ruleTestForm.rulegroup))
|
||||
const ruleTestData = computed(() => ruleTestResponse.value?.data)
|
||||
const metaInfo = computed(() => ruleTestData.value?.meta_info)
|
||||
const mediaInfo = computed(() => ruleTestData.value?.media_info)
|
||||
const torrentInfo = computed(() => ruleTestData.value?.torrent_info)
|
||||
const isMatched = computed(() => Boolean(ruleTestResponse.value?.success && ruleTestData.value?.matched))
|
||||
const resultIcon = computed(() => (isMatched.value ? 'mdi-filter-check-outline' : 'mdi-filter-remove-outline'))
|
||||
const resultColor = computed(() => (isMatched.value ? 'success' : 'warning'))
|
||||
const priorityText = computed(() => {
|
||||
const priority = ruleTestData.value?.priority
|
||||
return typeof priority === 'number' ? priority.toString() : '-'
|
||||
})
|
||||
const resultTitle = computed(() => {
|
||||
if (isMatched.value) return t('ruleTest.matched')
|
||||
return ruleTestResponse.value?.message || t('ruleTest.noPriorityRule')
|
||||
})
|
||||
const resultSubtitle = computed(() => {
|
||||
const parts = [
|
||||
mediaInfo.value?.title || metaInfo.value?.name || ruleTestForm.title,
|
||||
mediaInfo.value?.year || metaInfo.value?.year,
|
||||
metaInfo.value?.season_episode,
|
||||
]
|
||||
return parts.filter(Boolean).join(' · ') || t('ruleTest.waitingResult')
|
||||
})
|
||||
const ruleCount = computed(() => countRules(ruleTestData.value?.rulegroup?.rule_string || selectedRuleGroup.value?.rule_string))
|
||||
const resourceChips = computed(() => {
|
||||
return [
|
||||
mediaInfo.value?.type || metaInfo.value?.type,
|
||||
mediaInfo.value?.category,
|
||||
metaInfo.value?.resource_pix,
|
||||
metaInfo.value?.edition,
|
||||
metaInfo.value?.resource_team,
|
||||
].filter(Boolean) as string[]
|
||||
})
|
||||
const summaryItems = computed<SummaryItem[]>(() => [
|
||||
{
|
||||
icon: 'mdi-sort-numeric-descending',
|
||||
label: t('ruleTest.summary.priority'),
|
||||
value: priorityText.value,
|
||||
},
|
||||
{
|
||||
icon: 'mdi-filter-cog-outline',
|
||||
label: t('ruleTest.summary.ruleGroup'),
|
||||
value: ruleTestData.value?.rulegroup_name || ruleTestForm.rulegroup || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-format-list-numbered',
|
||||
label: t('ruleTest.summary.ruleCount'),
|
||||
value: ruleCount.value ? t('ruleTest.ruleCount', { count: ruleCount.value }) : '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-movie-search-outline',
|
||||
label: t('ruleTest.summary.media'),
|
||||
value: mediaInfo.value?.title || metaInfo.value?.name || '-',
|
||||
},
|
||||
])
|
||||
const analysisSteps = computed<AnalysisStep[]>(() => [
|
||||
{
|
||||
icon: 'mdi-filter-settings-outline',
|
||||
title: t('ruleTest.steps.group.title'),
|
||||
value: ruleTestData.value?.rulegroup_name || ruleTestForm.rulegroup || '-',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-movie-search-outline',
|
||||
title: t('ruleTest.steps.media.title'),
|
||||
value: mediaInfo.value?.title || metaInfo.value?.name || t('ruleTest.steps.media.none'),
|
||||
},
|
||||
{
|
||||
icon: resultIcon.value,
|
||||
title: t('ruleTest.steps.filter.title'),
|
||||
value:
|
||||
ruleTestResponse.value?.message ||
|
||||
(isMatched.value ? torrentInfo.value?.title || ruleTestForm.title : t('ruleTest.steps.filter.pending')),
|
||||
},
|
||||
{
|
||||
icon: 'mdi-sort-numeric-descending',
|
||||
title: t('ruleTest.steps.priority.title'),
|
||||
value: priorityText.value,
|
||||
},
|
||||
])
|
||||
|
||||
// 加载规则组
|
||||
/** 统计规则组串中的优先级规则数量。 */
|
||||
function countRules(ruleString = '') {
|
||||
return ruleString.split('>').filter(item => item.trim()).length
|
||||
}
|
||||
|
||||
/** 加载用户过滤规则组并填充规则组选择框。 */
|
||||
async function queryFilterRuleGroups() {
|
||||
try {
|
||||
filterRuleGroupLoading.value = true
|
||||
const result: { [key: string]: any } = await api.get('system/setting/UserFilterRuleGroups')
|
||||
filterRuleGroups.value = result.data?.value ?? []
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
filterRuleGroupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API识别
|
||||
/** 调用规则测试接口并刷新解析工作台。 */
|
||||
async function ruleTest() {
|
||||
if (!ruleTestForm.title || !ruleTestForm.rulegroup) return
|
||||
const validation = await ruleTestFormRef.value?.validate()
|
||||
if (!validation?.valid) return
|
||||
|
||||
try {
|
||||
ruleTestLoading.value = true
|
||||
ruleTestText.value = t('ruleTest.testing')
|
||||
ruleTestError.value = ''
|
||||
showResult.value = false
|
||||
const result: { [key: string]: any } = await api.get('system/ruletest', {
|
||||
ruleTestResponse.value = await api.get<ApiResponse<RuleTestData>, ApiResponse<RuleTestData>>('system/ruletest', {
|
||||
params: {
|
||||
title: ruleTestForm.title,
|
||||
subtitle: ruleTestForm.subtitle,
|
||||
rulegroup_name: ruleTestForm.rulegroup,
|
||||
},
|
||||
})
|
||||
if (result.success) ruleTestResult.value = t('ruleTest.priority', { value: result.data.priority })
|
||||
else ruleTestResult.value = t('ruleTest.noPriorityRule')
|
||||
|
||||
ruleTestLoading.value = false
|
||||
ruleTestText.value = t('ruleTest.testAgain')
|
||||
showResult.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ruleTestError.value = error instanceof Error ? error.message : t('ruleTest.requestFailed')
|
||||
} finally {
|
||||
ruleTestLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,52 +198,336 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow class="pt-2">
|
||||
<VCol cols="12" md="8">
|
||||
<VTextField
|
||||
v-model="ruleTestForm.title"
|
||||
:label="t('ruleTest.title')"
|
||||
:rules="[requiredValidator]"
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="ruleTestForm.rulegroup"
|
||||
:label="t('ruleTest.ruleGroup')"
|
||||
:items="filterRuleGroupItems"
|
||||
prepend-inner-icon="mdi-filter"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="ruleTestForm.subtitle"
|
||||
:label="t('ruleTest.subtitle')"
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-subtitles"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol cols="12" class="text-center">
|
||||
<VBtn :disabled="ruleTestLoading" @click="ruleTest">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-filter-check-outline" />
|
||||
</template>
|
||||
{{ ruleTestText }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<VExpandTransition>
|
||||
<div v-show="showResult">
|
||||
<VCol>
|
||||
<VAlert icon="mdi-alert-circle-outline">
|
||||
{{ ruleTestResult }}
|
||||
</VAlert>
|
||||
</VCol>
|
||||
<div class="shortcut-workbench">
|
||||
<section class="shortcut-panel shortcut-input-panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('ruleTest.inputTitle') }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ t('ruleTest.inputSubtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
<VIcon icon="mdi-filter-cog" color="primary" />
|
||||
</div>
|
||||
|
||||
<VForm ref="ruleTestFormRef" validate-on="submit lazy" @submit.prevent="ruleTest">
|
||||
<VRow class="shortcut-form">
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VTextField
|
||||
v-model="ruleTestForm.title"
|
||||
:label="t('ruleTest.title')"
|
||||
:rules="[requiredValidator]"
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-movie-open"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VSelect
|
||||
v-model="ruleTestForm.rulegroup"
|
||||
:items="filterRuleGroupItems"
|
||||
:label="t('ruleTest.ruleGroup')"
|
||||
:loading="filterRuleGroupLoading"
|
||||
:rules="[requiredValidator]"
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-filter"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VTextarea
|
||||
v-model="ruleTestForm.subtitle"
|
||||
:label="t('ruleTest.subtitle')"
|
||||
rows="2"
|
||||
auto-grow
|
||||
prepend-inner-icon="mdi-subtitles"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" class="shortcut-form-col">
|
||||
<VBtn block type="submit" :disabled="ruleTestLoading" :loading="ruleTestLoading">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-filter-check-outline" />
|
||||
</template>
|
||||
{{ ruleTestText }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
|
||||
<VAlert
|
||||
v-if="ruleTestError"
|
||||
class="mt-4"
|
||||
density="comfortable"
|
||||
icon="mdi-alert-circle-outline"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ ruleTestError }}
|
||||
</VAlert>
|
||||
</section>
|
||||
|
||||
<section class="shortcut-panel shortcut-result-panel">
|
||||
<div v-if="showResult" class="result-stack">
|
||||
<div class="rule-result-card" :class="{ 'rule-result-card--matched': isMatched }">
|
||||
<div class="priority-badge" :class="{ 'priority-badge--matched': isMatched }">
|
||||
<span class="text-caption text-medium-emphasis">{{ t('ruleTest.priorityLabel') }}</span>
|
||||
<span class="priority-value">{{ priorityText }}</span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="result-heading">
|
||||
<VIcon :icon="resultIcon" :color="resultColor" />
|
||||
<span class="result-title-text text-subtitle-1 font-weight-medium text-truncate">{{ resultTitle }}</span>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ resultSubtitle }}
|
||||
</div>
|
||||
<div class="chip-row mt-3">
|
||||
<VChip
|
||||
v-for="chip in resourceChips"
|
||||
:key="chip"
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ chip }}
|
||||
</VChip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div v-for="item in summaryItems" :key="item.label" class="summary-tile">
|
||||
<VIcon :icon="item.icon" size="18" class="summary-icon" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div class="text-body-2 font-weight-medium text-truncate">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<VIcon icon="mdi-filter-search-outline" size="36" />
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
{{ t('ruleTest.waitingResult') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section v-if="showResult" class="shortcut-panel analysis-panel mt-4">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('ruleTest.analysisTitle') }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ t('ruleTest.analysisSubtitle') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
|
||||
<div class="analysis-flow">
|
||||
<div v-for="step in analysisSteps" :key="step.title" class="analysis-step">
|
||||
<VIcon :icon="step.icon" color="primary" size="20" />
|
||||
<div class="min-w-0">
|
||||
<div class="text-body-2 font-weight-medium">
|
||||
{{ step.title }}
|
||||
</div>
|
||||
<div class="text-body-2 step-value">
|
||||
{{ step.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shortcut-workbench {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
gap: 1rem;
|
||||
padding-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
.shortcut-panel {
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
backdrop-filter: var(--app-grouped-list-backdrop-filter);
|
||||
background: var(--app-grouped-list-background);
|
||||
box-shadow: var(--app-surface-shadow);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.shortcut-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shortcut-form-col {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.shortcut-form-col:first-child {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.shortcut-form-col:last-child {
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
.result-stack {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.rule-result-card {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem minmax(0, 1fr);
|
||||
gap: 0.85rem;
|
||||
align-items: center;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-warning), 0.08);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.rule-result-card--matched {
|
||||
background: rgba(var(--v-theme-success), 0.08);
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
display: grid;
|
||||
min-block-size: 5rem;
|
||||
place-items: center;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.32);
|
||||
}
|
||||
|
||||
.priority-badge--matched {
|
||||
border-color: rgba(var(--v-theme-success), 0.42);
|
||||
background: rgba(var(--v-theme-success), 0.1);
|
||||
}
|
||||
|
||||
.priority-value {
|
||||
font-size: 1.65rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.result-heading {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-title-text {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.summary-tile {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.22);
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
min-block-size: 14rem;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 0.75rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.analysis-flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.analysis-step {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.55rem;
|
||||
border: var(--app-surface-border);
|
||||
border-radius: var(--app-surface-radius);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.2);
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.step-value {
|
||||
overflow: hidden;
|
||||
margin-block-start: 0.35rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shortcut-workbench,
|
||||
.analysis-flow {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.result-heading {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.35rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.result-title-text {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.shortcut-panel {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.rule-result-card {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user