Files
short-drama-agent-front/src/features/production/components/EpisodeAssemblyPanel.vue
T

142 lines
5.7 KiB
Vue
Raw 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.
<script setup lang="ts">
import { onScopeDispose, ref, watch } from 'vue'
import { NAlert, NButton, NCollapse, NCollapseItem, NProgress, NSpin } from 'naive-ui'
import { mediaAssetUrl } from '../../../lib/assets'
import { errorMessage } from '../../../lib/http'
import { productionApi } from '../api'
import type { EpisodeAssemblyResult, ProjectAssemblyReadiness } from '../types'
const props = defineProps<{ projectId: string }>()
const readiness = ref<ProjectAssemblyReadiness | null>(null)
const loading = ref(false)
const assembling = ref('')
const error = ref('')
const results = ref<Record<string, EpisodeAssemblyResult>>({})
let controller: AbortController | null = null
async function refresh() {
controller?.abort()
controller = new AbortController()
loading.value = true
error.value = ''
try {
readiness.value = await productionApi.assemblyReadiness(props.projectId, controller.signal)
} catch (cause) {
if (!controller.signal.aborted) error.value = errorMessage(cause)
} finally {
if (!controller.signal.aborted) loading.value = false
}
}
async function assemble(episodeId: string) {
if (assembling.value) return
assembling.value = episodeId
error.value = ''
try {
const result = await productionApi.assembleEpisode(episodeId)
results.value = { ...results.value, [episodeId]: result }
await refresh()
} catch (cause) {
error.value = errorMessage(cause)
} finally {
assembling.value = ''
}
}
function resultUrl(episodeId: string) {
const result = results.value[episodeId]
return result ? mediaAssetUrl(result.videoUrl) : null
}
watch(
() => props.projectId,
() => {
results.value = {}
void refresh()
},
{ immediate: true }
)
onScopeDispose(() => controller?.abort())
</script>
<template>
<NCollapse class="production-tool-section">
<NCollapseItem name="episode-assembly" title="单集成片拼接">
<NSpin :show="loading">
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="text-xs text-muted">
Beat Shot 顺序拼接当前有效主视频V1 只输出无声视频不包含对白配乐字幕和转场
</p>
<NButton size="small" :disabled="loading" @click="refresh">刷新就绪状态</NButton>
</div>
<NAlert v-if="error" type="error" :show-icon="false" class="mt-3">{{ error }}</NAlert>
<p v-if="readiness" class="mt-3 text-xs">
已就绪 {{ readiness.ready }}/{{ readiness.total }} · 阻塞 {{ readiness.blocked }}
</p>
<div v-if="readiness?.items.length" class="assembly-list mt-3">
<article v-for="item in readiness.items" :key="item.episodeId" class="assembly-row">
<div class="min-w-0">
<h4 class="truncate text-sm font-semibold">
{{ item.episodeNo }} · {{ item.title }}
</h4>
<p class="mt-1 text-xs text-muted">
主视频 {{ item.readyShots }}/{{ item.totalShots }} · {{ item.totalDurationSeconds }}
</p>
<NProgress
class="mt-2"
type="line"
:show-indicator="false"
:percentage="
item.totalShots ? Math.round((item.readyShots / item.totalShots) * 100) : 0
"
:status="item.ready ? 'success' : 'default'"
/>
<p
v-for="(issue, index) in item.issues.slice(0, 3)"
:key="index"
class="mt-1 text-xs text-muted"
>
<span v-if="issue.shotNo">镜头 {{ issue.shotNo }}</span>{{ issue.reason }}
</p>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<NButton
size="small"
:disabled="!item.ready || !!assembling"
:loading="assembling === item.episodeId"
@click="assemble(item.episodeId)"
>生成无声成片</NButton
>
<NButton
v-if="resultUrl(item.episodeId)"
tag="a"
size="small"
:href="resultUrl(item.episodeId) || undefined"
target="_blank"
rel="noopener"
>查看成片</NButton
>
</div>
</article>
</div>
<p v-else-if="readiness && !loading" class="mt-3 text-xs text-muted">当前项目还没有可拼接的剧集</p>
</NSpin>
</NCollapseItem>
</NCollapse>
</template>
<style scoped>
@reference "../../../styles/styles.css";
.assembly-list {
@apply grid gap-2;
}
.assembly-row {
@apply grid grid-cols-[minmax(0,_1fr)_auto] items-center gap-4 p-4 bg-(--app-subtle);
}
@media (max-width: 760px) {
.assembly-row {
@apply grid-cols-[minmax(0,_1fr)];
}
}
</style>