diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06c11e9b..4eae0f7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,7 @@ name: Frontend Tests on: + workflow_dispatch: pull_request: branches: - v3 diff --git a/src/api/types.ts b/src/api/types.ts index 59b47df8..b7a9ba76 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -24,6 +24,40 @@ export interface MediaSourceInfo { media_types: string[] } +/** 订阅跨搜索与下载链路的当前业务状态。 */ +export interface SubscriptionExecutionStatus { + state: string + phase: string + updated_at: string + source?: string + batch_id?: string + task_id?: string + current_site_id?: number + error?: string + can_cancel: boolean + can_retry: boolean + requires_reconciliation: boolean +} + +/** 订阅搜索批次的聚合进度与当前工作。 */ +export interface SubscriptionBatchStatus { + batch_id: string + source: string + state: string + phase: string + total_count: number + processed_count: number + finished_count: number + failed_count: number + cancelled_count: number + created_at: string + updated_at: string + current_subscription_id?: number + current_site_id?: number + error?: string + can_cancel: boolean +} + // 手动刮削选项 export interface ManualScrapeOptions { // 媒体数据源 @@ -142,6 +176,8 @@ export interface Subscribe { downloader?: string // 自定义剧集组 episode_group?: string + // 当前搜索或下载执行状态 + execution_status?: SubscriptionExecutionStatus } /** 订阅删除成功后的机器可判断结果。 */ diff --git a/src/components/cards/SubscribeCard.vue b/src/components/cards/SubscribeCard.vue index 99f04b39..619d2986 100644 --- a/src/components/cards/SubscribeCard.vue +++ b/src/components/cards/SubscribeCard.vue @@ -68,6 +68,34 @@ const subscribeState = ref(props.media?.state ?? 'P') // 上一次更新时间 const lastUpdateText = computed(() => (props.media?.last_update ? formatDateDifference(props.media.last_update) : '')) +// 将后端稳定业务状态映射为紧凑、可本地化的卡片展示。 +const executionStateDisplay = computed(() => { + const execution = props.media?.execution_status + if (!execution) return null + const displays: Record = { + queued: { color: 'info', icon: 'mdi-clock-outline' }, + running: { color: 'info', icon: 'mdi-progress-clock' }, + matching: { color: 'info', icon: 'mdi-filter-search-outline' }, + searching: { color: 'primary', icon: 'mdi-magnify-scan' }, + waiting_site_budget: { color: 'warning', icon: 'mdi-timer-sand' }, + preparing: { color: 'primary', icon: 'mdi-package-variant-closed' }, + submitting: { color: 'primary', icon: 'mdi-download-network-outline' }, + accepted: { color: 'success', icon: 'mdi-download-check-outline' }, + retryable: { color: 'warning', icon: 'mdi-refresh-circle' }, + reconcile_required: { color: 'warning', icon: 'mdi-alert-circle-outline' }, + failed: { color: 'error', icon: 'mdi-alert-outline' }, + cancelling: { color: 'warning', icon: 'mdi-cancel' }, + cancelled: { color: 'secondary', icon: 'mdi-cancel' }, + completed: { color: 'success', icon: 'mdi-check-circle-outline' }, + } + const display = displays[execution.state] || displays[execution.phase] || displays.running + return { + ...display, + label: t(`subscribe.execution.state.${execution.state}`), + error: execution.error, + } +}) + // 判断后端数字/布尔开关是否启用 function isEnabledFlag(value: any) { return value === true || value === 1 || value === '1' @@ -92,6 +120,9 @@ const hasBestVersion = computed(() => isEnabledFlag(props.media?.best_version)) const isBestVersion = computed(() => hasBestVersion.value && isTvSubscribe(props.media)) const rightBottomStateDisplay = computed(() => { + if (executionStateDisplay.value) { + return executionStateDisplay.value + } if (subscribeState.value === 'S') { return { icon: 'mdi-pause-circle', label: t('subscribe.cardStatePaused') } } @@ -103,6 +134,9 @@ const rightBottomStateDisplay = computed(() => { // 移动端紧凑卡片的状态展示,颜色统一映射到 Vuetify 全局主题 token。 const compactStateDisplay = computed(() => { + if (executionStateDisplay.value) { + return executionStateDisplay.value + } if (subscribeState.value === 'S') { return { color: 'secondary', icon: 'mdi-pause-circle-outline', label: t('subscribe.cardStatePaused') } } @@ -175,6 +209,10 @@ const musicSubscribeMeta = computed(() => { } }) +const compactStateText = computed( + () => executionStateDisplay.value?.label || subscribeProgressText.value || musicSubscribeMeta.value?.text || '', +) + // 订阅卡片 hover 文案: // - 普通订阅:「已下载 X · 共 Y 集」 // - 洗版订阅:「已下载 X · 已洗版 N · 共 Y 集」 @@ -240,7 +278,8 @@ async function removeSubscribe() { async function searchSubscribe() { try { await api.get(`subscribe/search/${props.media?.id}`, { feedback: 'silent' }) - $toast.success(`${props.media?.name} 提交搜索请求成功!`) + $toast.success(t('subscribe.execution.searchSubmitted', { name: props.media?.name })) + emit('save') } catch (e) { $toast.error(t('subscribe.requestFailed')) console.log(e) @@ -651,7 +690,7 @@ function handleCardClick() {