mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-09 09:46:44 +08:00
fix(subscribe): restore card metadata after completion
This commit is contained in:
@@ -14,6 +14,8 @@ import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||
import { buildMusicDetailRoute, formatMusicAudioSpecs, formatMusicBitrate } from '@/utils/music'
|
||||
|
||||
const COMPLETED_EXECUTION_VISIBLE_MS = 5_000
|
||||
|
||||
const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue'))
|
||||
const SubscribeFilesDialog = defineAsyncComponent(() => import('../dialog/SubscribeFilesDialog.vue'))
|
||||
const SubscribeShareDialog = defineAsyncComponent(() => import('../dialog/SubscribeShareDialog.vue'))
|
||||
@@ -68,9 +70,40 @@ const subscribeState = ref<string>(props.media?.state ?? 'P')
|
||||
// 上一次更新时间
|
||||
const lastUpdateText = computed(() => (props.media?.last_update ? formatDateDifference(props.media.last_update) : ''))
|
||||
|
||||
// 成功终态只承担短暂反馈,持久账本仍由后端保留,卡片随后恢复订阅进度。
|
||||
const visibleExecutionStatus = ref<Subscribe['execution_status'] | null>(null)
|
||||
let completedExecutionTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
// 清理上一条成功终态的恢复计时器,避免卡片复用后由旧任务覆盖新状态。
|
||||
function clearCompletedExecutionTimer() {
|
||||
if (!completedExecutionTimer) return
|
||||
clearTimeout(completedExecutionTimer)
|
||||
completedExecutionTimer = undefined
|
||||
}
|
||||
|
||||
// 活动与异常状态持续展示;成功完成仅在后端更新时间后的短窗口内展示。
|
||||
function syncVisibleExecutionStatus(execution: Subscribe['execution_status']) {
|
||||
clearCompletedExecutionTimer()
|
||||
visibleExecutionStatus.value = execution
|
||||
if (!execution || (execution.state !== 'completed' && execution.phase !== 'completed')) return
|
||||
|
||||
const updatedAt = Date.parse(execution.updated_at)
|
||||
const elapsed = Number.isFinite(updatedAt) ? Math.max(0, Date.now() - updatedAt) : 0
|
||||
const remaining = COMPLETED_EXECUTION_VISIBLE_MS - elapsed
|
||||
if (remaining <= 0) {
|
||||
visibleExecutionStatus.value = null
|
||||
return
|
||||
}
|
||||
|
||||
completedExecutionTimer = setTimeout(() => {
|
||||
visibleExecutionStatus.value = null
|
||||
completedExecutionTimer = undefined
|
||||
}, remaining)
|
||||
}
|
||||
|
||||
// 将后端稳定业务状态映射为紧凑、可本地化的卡片展示。
|
||||
const executionStateDisplay = computed(() => {
|
||||
const execution = props.media?.execution_status
|
||||
const execution = visibleExecutionStatus.value
|
||||
if (!execution) return null
|
||||
const displays: Record<string, { color: string; icon: string }> = {
|
||||
queued: { color: 'info', icon: 'mdi-clock-outline' },
|
||||
@@ -474,6 +507,14 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.media?.execution_status,
|
||||
execution => syncVisibleExecutionStatus(execution),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => clearCompletedExecutionTimer())
|
||||
|
||||
// 切换订阅记录时重新尝试加载图片,避免复用卡片组件后沿用旧的失败状态。
|
||||
watch(
|
||||
() => [props.media?.id, props.media?.backdrop, props.media?.poster],
|
||||
@@ -701,11 +742,7 @@ function handleCardClick() {
|
||||
<span v-if="compactStateText" class="subscribe-card-mobile-progress-text">
|
||||
{{ compactStateText }}
|
||||
</span>
|
||||
<VTooltip
|
||||
v-if="executionStateDisplay?.error"
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
<VTooltip v-if="executionStateDisplay?.error" activator="parent" location="top">
|
||||
{{ executionStateDisplay.error }}
|
||||
</VTooltip>
|
||||
</div>
|
||||
@@ -830,16 +867,14 @@ function handleCardClick() {
|
||||
<VCardText
|
||||
v-if="rightBottomStateDisplay"
|
||||
class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300 text-xs"
|
||||
:style="executionStateDisplay ? { color: `rgb(var(--v-theme-${executionStateDisplay.color}))` } : undefined"
|
||||
:style="
|
||||
executionStateDisplay ? { color: `rgb(var(--v-theme-${executionStateDisplay.color}))` } : undefined
|
||||
"
|
||||
:title="executionStateDisplay?.error || rightBottomStateDisplay.label"
|
||||
>
|
||||
<VIcon :icon="rightBottomStateDisplay.icon" class="me-1" />
|
||||
{{ rightBottomStateDisplay.label }}
|
||||
<VTooltip
|
||||
v-if="executionStateDisplay?.error"
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
<VTooltip v-if="executionStateDisplay?.error" activator="parent" location="top">
|
||||
{{ executionStateDisplay.error }}
|
||||
</VTooltip>
|
||||
</VCardText>
|
||||
|
||||
@@ -368,6 +368,67 @@ describe('SubscribeCard display and progress', () => {
|
||||
expect(screen.getByText('等待站点额度')).toBeInTheDocument()
|
||||
expect(screen.getByTitle('站点 9 冷却中')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([480, 1024])('briefly shows a fresh completion then restores normal metadata at %ipx', async width => {
|
||||
setViewport(width)
|
||||
const { media, rerender, unmount } = await renderCard({
|
||||
lack_episode: 4,
|
||||
state: 'R',
|
||||
total_episode: 10,
|
||||
type: '电视剧',
|
||||
})
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-09-02T12:00:00+08:00'))
|
||||
|
||||
try {
|
||||
await rerender({
|
||||
media: {
|
||||
...media,
|
||||
execution_status: {
|
||||
can_cancel: false,
|
||||
can_retry: false,
|
||||
phase: 'completed',
|
||||
requires_reconciliation: false,
|
||||
state: 'completed',
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(screen.getByText('执行完成')).toBeInTheDocument()
|
||||
if (width < 600) expect(screen.queryByText('6 / 10')).not.toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5_000)
|
||||
|
||||
expect(screen.queryByText('执行完成')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('6 / 10')).toBeInTheDocument()
|
||||
if (width >= 600) expect(screen.getByText(formatDateDifference(media.last_update))).toBeInTheDocument()
|
||||
} finally {
|
||||
unmount()
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('does not revive an expired completion after the card is reloaded', async () => {
|
||||
setViewport(480)
|
||||
await renderCard({
|
||||
execution_status: {
|
||||
can_cancel: false,
|
||||
can_retry: false,
|
||||
phase: 'completed',
|
||||
requires_reconciliation: false,
|
||||
state: 'completed',
|
||||
updated_at: new Date(Date.now() - 6_000).toISOString(),
|
||||
},
|
||||
lack_episode: 4,
|
||||
state: 'R',
|
||||
total_episode: 10,
|
||||
type: '电视剧',
|
||||
})
|
||||
|
||||
expect(screen.queryByText('执行完成')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('6 / 10')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SubscribeCard interaction boundaries', () => {
|
||||
|
||||
Reference in New Issue
Block a user