refactor: 使用 Naive UI 重构后台布局与黑白双主题

This commit is contained in:
GouJ
2026-09-01 16:31:21 +08:00
parent 55516537bf
commit db8c176b55
51 changed files with 3277 additions and 2442 deletions
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NCollapse, NCollapseItem, NTag } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { ExternalLink, FileSearch, ImagePlus, RefreshCw } from '@lucide/vue'
import { AssetImage, StatusBadge } from '../../../components/ui'
@@ -189,18 +190,18 @@ async function inspectSpecs() {
<p class="mt-1 text-[11px] text-muted">记录每次候选与失败主资产决定下一阶段实际输入</p>
</div>
<div class="flex gap-3">
<button class="text-button" :disabled="inspecting" @click="inspectSpecs">
<FileSearch :size="13" />{{ inspecting ? '检查中…' : '检查生成规格' }}
</button>
<button class="text-button" :disabled="query.loading.value" @click="refreshAll">
<RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新资产
</button>
<NButton :disabled="inspecting" @click="inspectSpecs" text size="small"
><FileSearch :size="13" />{{ inspecting ? '检查中…' : '检查生成规格' }}</NButton
>
<NButton :disabled="query.loading.value" @click="refreshAll" text size="small"
><RefreshCw :size="13" :class="{ 'animate-spin': query.loading.value }" />刷新资产
</NButton>
</div>
</div>
<p v-if="query.error.value" class="alert alert-error mt-4" role="alert">
{{ query.error.value }} 当前保留上次成功读取的资产操作已暂停
</p>
<p v-if="specError" class="alert alert-error mt-4" role="alert">{{ specError }}</p>
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
>{{ query.error.value }} 当前保留上次成功读取的资产操作已暂停
</NAlert>
<NAlert v-if="specError" role="alert" type="error" :show-icon="false" class="mt-4">{{ specError }}</NAlert>
<div class="production-stage mt-5">
<div class="production-stage-heading">
@@ -246,9 +247,9 @@ async function inspectSpecs() {
:label="keyframeStale ? '主首帧已过期' : productionStatusLabel(keyframeReadiness.status)"
/>
</div>
<p v-if="keyframeStale" class="alert mt-3 text-xs" role="status">
<NAlert v-if="keyframeStale" role="status" type="info" :show-icon="false" class="mt-3 text-xs">
当前主首帧使用的主体参考资产已经变化旧图仍保留但不会继续进入视频生成请重新生成主首帧
</p>
</NAlert>
<ul v-if="keyframeReadiness?.issues.length" class="mt-3 space-y-2 text-xs text-danger">
<li v-for="issue in keyframeReadiness.issues" :key="issue.code">
{{ issueLabel(issue.code) }}{{ issue.reason }}
@@ -264,34 +265,44 @@ async function inspectSpecs() {
<div class="p-3">
<div class="flex flex-wrap items-center gap-2">
<StatusBadge :status="item.status" :label="productionStatusLabel(item.status)" />
<span v-if="item.isPrimary" class="tag">主首帧</span>
<span v-if="item.isPrimary && keyframeStale" class="tag">已过期</span>
<span class="text-[10px] text-muted">{{ item.width || '—' }} × {{ item.height || '—' }}</span>
<NTag v-if="item.isPrimary" size="small" :bordered="false">主首帧</NTag>
<NTag v-if="item.isPrimary && keyframeStale" size="small" :bordered="false">已过期</NTag>
<span class="text-[10px] text-muted"
>{{ item.width || '—' }} × {{ item.height || '—' }}</span
>
</div>
<p v-if="item.error" class="mt-2 line-clamp-3 text-xs text-danger">{{ item.error }}</p>
<p class="mt-2 text-[10px] text-muted">{{ formatDate(item.createdAt) }}</p>
<button
<NButton
v-if="item.status === 'completed' && item.imageUrl && !item.isPrimary"
class="text-button mt-2"
:disabled="assetBlocked"
@click="confirmingKeyframe = item.id"
text
size="small"
class="mt-2"
>
设为主首帧
</button>
<div v-if="confirmingKeyframe === item.id" class="alert mt-2 text-xs">
<p>确认让后续视频使用此首帧已有视频不会自动重生成</p>
</NButton>
<NAlert
v-if="confirmingKeyframe === item.id"
type="info"
:show-icon="false"
class="mt-2 text-xs"
><p>确认让后续视频使用此首帧已有视频不会自动重生成</p>
<div class="mt-2 flex gap-3">
<button class="button button-primary" @click="setPrimaryKeyframe">确认切换</button>
<button class="text-button" @click="confirmingKeyframe = ''">取消</button>
</div>
</div>
<NButton @click="setPrimaryKeyframe" type="primary">确认切换</NButton>
<NButton @click="confirmingKeyframe = ''" text size="small">取消</NButton>
</div></NAlert
>
</div>
</article>
</div>
<p v-else class="mt-4 text-xs text-muted">尚无首帧记录</p>
<button class="button button-secondary mt-4" :disabled="!canGenerateKeyframe" @click="keyframeOpen = true">
<ImagePlus :size="14" />{{ keyframeStale ? '重新生成主首帧' : currentKeyframe ? '再生成一张候选' : '生成首帧' }}
</button>
<NButton :disabled="!canGenerateKeyframe" @click="keyframeOpen = true" class="mt-4"
><ImagePlus :size="14" />{{
keyframeStale ? '重新生成主首帧' : currentKeyframe ? '再生成一张候选' : '生成首帧'
}}</NButton
>
</div>
<div class="production-stage">
@@ -306,9 +317,9 @@ async function inspectSpecs() {
:label="videoStale ? '主视频已过期' : productionStatusLabel(videoReadiness.status)"
/>
</div>
<p v-if="videoStale" class="alert mt-3 text-xs" role="status">
<NAlert v-if="videoStale" role="status" type="info" :show-icon="false" class="mt-3 text-xs">
当前主视频使用的主首帧或主体参考图已经变化旧视频仍保留重新生成完成后新视频会自动接替过期主视频
</p>
</NAlert>
<ul v-if="videoReadiness?.issues.length" class="mt-3 space-y-2 text-xs text-danger">
<li v-for="issue in videoReadiness.issues" :key="issue.code">
{{ issueLabel(issue.code) }}{{ issue.reason }}
@@ -326,11 +337,15 @@ async function inspectSpecs() {
<div class="min-w-0 flex-1 p-4">
<div class="flex flex-wrap items-center gap-2">
<StatusBadge :status="item.status" :label="productionStatusLabel(item.status)" />
<span v-if="item.isPrimary" class="tag">主视频</span>
<span v-if="item.isPrimary && videoStale" class="tag">已过期</span>
<span class="text-[10px] text-muted">{{ item.durationSeconds || shot.durationSeconds || '—' }}s</span>
<NTag v-if="item.isPrimary" size="small" :bordered="false">主视频</NTag>
<NTag v-if="item.isPrimary && videoStale" size="small" :bordered="false">已过期</NTag>
<span class="text-[10px] text-muted"
>{{ item.durationSeconds || shot.durationSeconds || '—' }}s</span
>
</div>
<p v-if="item.error" class="alert alert-error mt-3" role="alert">{{ item.error }}</p>
<NAlert v-if="item.error" role="alert" type="error" :show-icon="false" class="mt-3">{{
item.error
}}</NAlert>
<p class="mt-3 break-all text-[10px] text-muted">
{{ item.provider }} · {{ item.model }} · {{ formatDate(item.createdAt) }}
</p>
@@ -341,24 +356,25 @@ async function inspectSpecs() {
target="_blank"
rel="noopener noreferrer"
class="text-button"
>打开视频<ExternalLink :size="12" /></a
>
<button
>打开视频<ExternalLink :size="12"
/></a>
<NButton
v-if="item.status === 'completed' && item.videoUrl && !item.isPrimary"
class="text-button"
:disabled="assetBlocked"
@click="confirmingVideo = item.id"
text
size="small"
>
设为主视频
</button>
</NButton>
</div>
<div v-if="confirmingVideo === item.id" class="alert mt-3 text-xs">
<p>确认把此候选设为当前镜头主视频旧成片仍会保留</p>
<NAlert v-if="confirmingVideo === item.id" type="info" :show-icon="false" class="mt-3 text-xs"
><p>确认把此候选设为当前镜头主视频旧成片仍会保留</p>
<div class="mt-2 flex gap-3">
<button class="button button-primary" @click="setPrimaryVideo">确认切换</button>
<button class="text-button" @click="confirmingVideo = ''">取消</button>
</div>
</div>
<NButton @click="setPrimaryVideo" type="primary">确认切换</NButton>
<NButton @click="confirmingVideo = ''" text size="small">取消</NButton>
</div></NAlert
>
</div>
</article>
</div>
@@ -378,46 +394,43 @@ async function inspectSpecs() {
primary
@confirm="generateVideo"
/>
<button
v-if="activeVideo"
class="button button-secondary"
:disabled="assetBlocked"
@click="refreshActiveVideo"
>
<RefreshCw :size="14" />刷新任务状态
</button>
<NButton v-if="activeVideo" :disabled="assetBlocked" @click="refreshActiveVideo"
><RefreshCw :size="14" />刷新任务状态
</NButton>
</div>
</div>
<details v-if="keyframeSpec || videoSpec" class="mt-5 border-t border-line pt-4 text-xs">
<summary class="cursor-pointer font-medium">本镜确定性生成规格</summary>
<div class="mt-4 grid gap-5 lg:grid-cols-2">
<div v-if="keyframeSpec">
<h5 class="font-medium">首帧 Prompt · {{ keyframeSpec.references.length }} 张参考图</h5>
<p class="mt-3 whitespace-pre-wrap leading-6 text-muted">{{ keyframeSpec.prompt }}</p>
<ul v-if="keyframeSpec.references.length" class="mt-3 space-y-1 text-[11px] text-muted">
<li
v-for="reference in keyframeSpec.references"
:key="`${reference.role}:${reference.imageId}`"
>
{{ reference.subjectRef }} ·
{{ reference.role === 'identity-anchor' ? '演员身份母版' : '形态主图' }} ·
<code>{{ reference.imageId }}</code>
</li>
</ul>
</div>
<div v-if="videoSpec">
<h5 class="font-medium">视频 Prompt · {{ videoSpec.durationSeconds }}s</h5>
<p class="mt-3 whitespace-pre-wrap leading-6 text-muted">{{ videoSpec.videoPrompt }}</p>
<p v-if="videoSpec.negativePrompt" class="mt-3 whitespace-pre-wrap leading-6 text-muted">
Negative prompt: {{ videoSpec.negativePrompt }}
</p>
<p class="mt-3 break-all font-mono text-[10px] text-muted">
Keyframe ID · {{ videoSpec.keyframe.id }}
</p>
</div>
</div>
</details>
<NCollapse v-if="keyframeSpec || videoSpec" class="mt-5 border-t border-line pt-4 text-xs"
><NCollapseItem name="details"
><template #header>本镜确定性生成规格</template>
<div class="mt-4 grid gap-5 lg:grid-cols-2">
<div v-if="keyframeSpec">
<h5 class="font-medium">首帧 Prompt · {{ keyframeSpec.references.length }} 张参考图</h5>
<p class="mt-3 whitespace-pre-wrap leading-6 text-muted">{{ keyframeSpec.prompt }}</p>
<ul v-if="keyframeSpec.references.length" class="mt-3 space-y-1 text-[11px] text-muted">
<li
v-for="reference in keyframeSpec.references"
:key="`${reference.role}:${reference.imageId}`"
>
{{ reference.subjectRef }} ·
{{ reference.role === 'identity-anchor' ? '演员身份母版' : '形态主图' }} ·
<code>{{ reference.imageId }}</code>
</li>
</ul>
</div>
<div v-if="videoSpec">
<h5 class="font-medium">视频 Prompt · {{ videoSpec.durationSeconds }}s</h5>
<p class="mt-3 whitespace-pre-wrap leading-6 text-muted">{{ videoSpec.videoPrompt }}</p>
<p v-if="videoSpec.negativePrompt" class="mt-3 whitespace-pre-wrap leading-6 text-muted">
Negative prompt: {{ videoSpec.negativePrompt }}
</p>
<p class="mt-3 break-all font-mono text-[10px] text-muted">
Keyframe ID · {{ videoSpec.keyframe.id }}
</p>
</div>
</div></NCollapseItem
></NCollapse
>
<KeyframeDialog
v-model:open="keyframeOpen"
:shot-id="shot.shotId"
@@ -428,4 +441,4 @@ async function inspectSpecs() {
@generate="generateKeyframe"
/>
</section>
</template>
</template>