Files
2026-11-13 11:46:25 +08:00

68 lines
5.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<section v-if="task?.status === 'processing'" class="task-progress" aria-label="视频生成进度">
<div class="progress-head"><strong>{{ phaseLabel }}</strong><span>{{ elapsedLabel }}</span></div>
<template v-if="fresh && progress?.totalSteps && progress?.step != null">
<div class="progress-steps">当前阶段 {{ progress.step }} / {{ progress.totalSteps }} 步 · {{ percent }}%</div>
<progress :value="progress.step" :max="progress.totalSteps" aria-label="当前阶段进度" />
</template>
<p v-if="fresh && progress?.phase === 'queued'">前面还有 {{ progress.queueAhead ?? '未知数量的' }} 个任务;排队时间取决于前方任务。</p>
<p v-if="samplingFresh && progress?.samplingMaxSeconds">采样预计还需约 {{ timeRange(progress.samplingMinSeconds, progress.samplingMaxSeconds) }}<small>仅指当前采样阶段,后续解码、编码和保存另计。</small></p>
<p v-else-if="fresh && progress?.phase === 'sampling' && progress?.step === progress?.totalSteps && progress?.totalSteps">采样已结束,正在进入后续处理。</p>
<p v-else-if="fresh && progress?.phase === 'sampling'">正在收集采样速度,暂无法估算剩余时间。</p>
<p v-if="fresh && progress?.historyMaxSeconds">同显卡、同规格通常耗时约 {{ timeRange(progress.historyMinSeconds, progress.historyMaxSeconds) }}<small>参考 {{ progress.historySamples }} 次成功执行,不含排队与下载;属于估算。</small></p>
<p v-else-if="!waiting && !finishing">整段视频预计耗时:暂无足够的同规格记录。</p>
<p v-if="finishing">视频生成已结束,正在回传并保存到本地。</p>
<p v-else-if="task?.stage === 'preparing_first_frame'">正在融合全部绑定素材生成完整首帧,完成后自动提交视频。</p>
<p v-else-if="task?.stage === 'first_frame_ready'">首帧已保存,正在准备视频请求。</p>
<p v-else-if="task?.stage === 'cancelling'">已请求停止此任务,正在等待显卡确认。</p>
<p v-else-if="!fresh && !waiting" class="progress-note">{{ progress ? '进度更新暂时延迟,仍在跟踪原任务。' : '正在读取显卡进度…' }}</p>
</section>
</template>
<script setup lang="ts">
const props = defineProps<{ task?: Record<string, any> | null }>()
const clock = ref(Date.now())
let timer: ReturnType<typeof setInterval> | undefined
onMounted(() => { timer = setInterval(() => { clock.value = Date.now() }, 1000) })
onBeforeUnmount(() => { if (timer) clearInterval(timer) })
const progress = computed(() => {
if (props.task?.progress) return props.task.progress
try { return JSON.parse(props.task?.params || '{}').progress || null } catch { return null }
})
const finishing = computed(() => ['transferring', 'downloading', 'saving'].includes(props.task?.stage))
const fresh = computed(() => !finishing.value && props.task?.stage !== 'cancelling' && progress.value && clock.value - progress.value.observedAt < 90000)
const samplingFresh = computed(() => fresh.value && clock.value - progress.value.observedAt < 30000)
const waiting = computed(() => ['waiting_predecessor', 'blocked_predecessor', 'cancelling', 'preparing_first_frame', 'first_frame_ready'].includes(props.task?.stage))
const phases: Record<string, string> = { queued:'等待显卡队列', running:'显卡正在处理', sampling:'视频采样中', decoding:'视频 / 音频解码中',
encoding:'编码与保存中', loading:'模型 / 素材加载中', conditioning:'参考素材与提示词处理', postprocessing:'声音 / 字幕后处理',
preparing_first_frame:'第 1 步 · 生成完整分镜首帧', first_frame_ready:'第 2 步 · 提交视频', waiting_predecessor:'等待前镜头完成', blocked_predecessor:'等待处理前镜头', cancelling:'正在确认取消', transferring:'视频回传中', downloading:'视频下载中', saving:'保存视频中', submitting:'提交任务中' }
const phaseLabel = computed(() => {
const stage = props.task?.stage
return phases[waiting.value || ['transferring','downloading','saving','submitting'].includes(stage) ? stage : fresh.value ? progress.value.phase : stage] || '生成任务处理中'
})
const elapsedLabel = computed(() => {
const start = Date.parse(props.task?.created_at || props.task?.createdAt || '')
return Number.isFinite(start) ? `已提交 ${formatTime(Math.max(0, (clock.value - start) / 1000))}` : ''
})
const percent = computed(() => Math.min(100, Math.round(progress.value.step / progress.value.totalSteps * 100)))
function formatTime(seconds: number) {
const value = Math.max(0, Math.floor(seconds))
if (value < 60) return `${value} 秒`
if (value < 3600) return `${Math.floor(value / 60)} 分 ${value % 60} 秒`
return `${Math.floor(value / 3600)} 小时 ${Math.floor(value % 3600 / 60)} 分`
}
function timeRange(low: number, high: number) {
// Keep the server's estimated range, rather than an invented local countdown.
return high < 60 ? `${Math.max(1, Math.floor(low))}–${Math.ceil(high)} 秒` : `${Math.max(1, Math.floor(low / 60))}–${Math.max(1, Math.ceil(high / 60))} 分钟`
}
</script>
<style scoped>
.task-progress { margin: 12px 16px; padding: 12px 14px; background: var(--bg-secondary, #f7f8fa); border: 1px solid var(--border-color, #e5e7eb); border-radius: 12px; font-size: 13px; line-height: 1.6; }
.progress-head { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.progress-head span, small, .progress-note { color: var(--text-secondary, #737b8b); }
.progress-steps { margin-top: 8px; font-variant-numeric: tabular-nums; }
progress { display: block; width: 100%; height: 8px; margin: 6px 0; accent-color: #ff7714; }
p { margin: 6px 0 0; } small { display: block; font-size: 12px; }
</style>