feat: 对接项目生成配置与单集成片
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<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="mt-4">
|
||||
<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>
|
||||
Reference in New Issue
Block a user