fix: 使用 NScrollbar 统一内容区滚动条

This commit is contained in:
GouJ
2026-09-01 16:44:30 +08:00
parent db8c176b55
commit 3fe1b3f5c3
17 changed files with 967 additions and 737 deletions
+147 -136
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import WorkspacePage from '../../components/ui/WorkspacePage.vue'
import {
NScrollbar,
NAlert,
NButton,
NCheckbox,
@@ -155,95 +156,99 @@ watch(
</div>
</section></template
>
<NCollapse v-model:expanded-names="overview" class="workspace-overview"
><NCollapseItem name="generation" title="生成配置、批量操作与回执"
><div class="overview-content">
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">01</span>导演摄影设计
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
景别机位运镜与构图单集操作会重新生成并覆盖批量默认跳过完整剧集
</p>
<NScrollbar class="workspace-overview" content-class="overview-scroll-content"
><NCollapse v-model:expanded-names="overview"
><NCollapseItem name="generation" title="生成配置、批量操作与回执"
><div class="overview-content">
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">01</span>导演摄影设计
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
景别机位运镜与构图单集操作会重新生成并覆盖批量默认跳过完整剧集
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集导演设计"
:disabled="!allowed['direction-one']"
acknowledgement
description="调用模型生成当前整集的导演设计,校验通过后保存。如果此集已有设计,将覆盖原结果。"
@confirm="run('direction-one')"
/><ConfirmAction
:label="force ? '重生成全部导演设计' : '补齐项目导演设计'"
:disabled="!allowed['direction-all']"
acknowledgement
:description="
force
? '重新生成项目全部剧集的导演设计,覆盖已有结果。'
: '对整个项目生成导演设计,跳过已完整保存的剧集,可用于重试失败或未完成的剧集。'
"
@confirm="run('direction-all')"
/>
</div>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集导演设计"
:disabled="!allowed['direction-one']"
acknowledgement
description="调用模型生成当前整集的导演设计,校验通过后保存。如果此集已有设计,将覆盖原结果。"
@confirm="run('direction-one')"
/><ConfirmAction
:label="force ? '重生成全部导演设计' : '补齐项目导演设计'"
:disabled="!allowed['direction-all']"
acknowledgement
:description="
force
? '重新生成项目全部剧集的导演设计,覆盖已有结果。'
: '对整个项目生成导演设计,跳过已完整保存的剧集,可用于重试失败或未完成的剧集。'
"
@confirm="run('direction-all')"
/>
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">02</span>即时视觉状态
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
表情姿态位置环境与连续性每集需要完整导演设计批量中缺少前置设计的剧集会单独报错
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集即时状态"
:disabled="!allowed['visual-one']"
acknowledgement
description="依据本集导演设计和主体形态生成即时视觉状态,按配置尝试自动修复。校验通过才保存,已有状态将被覆盖。"
@confirm="run('visual-one')"
/><ConfirmAction
:label="force ? '重生成全部即时状态' : '补齐项目即时状态'"
:disabled="!allowed['visual-all']"
acknowledgement
:description="
force
? '重新生成整个项目的即时状态,覆盖已有结果。每集必须先有完整导演设计。'
: '补齐项目即时状态,跳过完整剧集;未完成导演设计的剧集会返回失败原因。'
"
@confirm="run('visual-all')"
/>
</div>
</div>
</div>
<div class="generation-row">
<div>
<h3 class="text-sm font-semibold">
<span class="mr-2 font-mono text-xs text-muted">02</span>即时视觉状态
</h3>
<p class="mt-2 text-xs leading-6 text-muted">
表情姿态位置环境与连续性每集需要完整导演设计批量中缺少前置设计的剧集会单独报错
</p>
</div>
<div class="flex flex-wrap gap-2">
<ConfirmAction
label="生成本集即时状态"
:disabled="!allowed['visual-one']"
acknowledgement
description="依据本集导演设计和主体形态生成即时视觉状态,按配置尝试自动修复。校验通过才保存,已有状态将被覆盖。"
@confirm="run('visual-one')"
/><ConfirmAction
:label="force ? '重生成全部即时状态' : '补齐项目即时状态'"
:disabled="!allowed['visual-all']"
acknowledgement
:description="
force
? '重新生成整个项目的即时状态,覆盖已有结果。每集必须先有完整导演设计。'
: '补齐项目即时状态,跳过完整剧集;未完成导演设计的剧集会返回失败原因。'
"
@confirm="run('visual-all')"
/>
</div>
</div>
<p class="mt-4 text-[11px] leading-6 text-muted">
修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于
VisualState
</p>
<NCollapse class="mt-3 border-t border-line pt-3"
><NCollapseItem name="details"
><template #header>视频提示词批量操作</template>
<p class="my-3 text-xs leading-6 text-muted">
后端会先校验 GenerationSpec
与主体参考图这里只生成提示词首帧视频任务就绪诊断与失败重试已整理到
<RouterLink :to="`/projects/${id}/production`" class="text-button mx-1"
>镜头生产</RouterLink
>
</p>
<ConfirmAction
:label="force ? '重生成全部视频提示词' : '补齐项目视频提示词'"
:disabled="!allowed.prompts"
acknowledgement
:description="
force
? '调用模型重新生成全项目镜头的视频提示词,覆盖已有正文。'
: '调用模型生成缺少视频提示词的镜头,已有提示词直接跳过。参考图缺失会返回失败。'
"
@confirm="run('prompts')" /></NCollapseItem
></NCollapse>
<GenerationReceipt v-if="session.receipt" :receipt="session.receipt" /></div></NCollapseItem
></NCollapse>
<div class="workspace-feedback">
<p class="mt-4 text-[11px] leading-6 text-muted">
修改或覆盖上游设计后后端不会自动使旧状态旧提示词失效请按顺序重生成下游内容自动修复次数仅用于
VisualState
</p>
<NCollapse class="mt-3 border-t border-line pt-3"
><NCollapseItem name="details"
><template #header>视频提示词批量操作</template>
<p class="my-3 text-xs leading-6 text-muted">
后端会先校验 GenerationSpec
与主体参考图这里只生成提示词首帧视频任务就绪诊断与失败重试已整理到
<RouterLink :to="`/projects/${id}/production`" class="text-button mx-1"
>镜头生产</RouterLink
>
</p>
<ConfirmAction
:label="force ? '重生成全部视频提示词' : '补齐项目视频提示词'"
:disabled="!allowed.prompts"
acknowledgement
:description="
force
? '调用模型重新生成全项目镜头的视频提示词,覆盖已有正文。'
: '调用模型生成缺少视频提示词的镜头,已有提示词直接跳过。参考图缺失会返回失败。'
"
@confirm="run('prompts')" /></NCollapseItem
></NCollapse>
<GenerationReceipt
v-if="session.receipt"
:receipt="session.receipt"
/></div></NCollapseItem></NCollapse
></NScrollbar>
<NScrollbar class="workspace-feedback">
<NAlert v-if="query.error.value" role="alert" type="error" :show-icon="false" class="mt-4"
>{{ query.error.value }} 当前保留上次成功读取的数据暂停生成操作<NButton
@click="query.refresh"
@@ -269,7 +274,7 @@ watch(
>
本集导演设计尚未补齐请先完成第 01 再生成本集即时状态
</NAlert>
</div>
</NScrollbar>
<div class="my-6 flex flex-wrap items-center justify-between gap-4">
<div>
<h3 class="text-sm font-semibold"> {{ episodeNo || '—' }} · 镜头检查</h3>
@@ -306,57 +311,63 @@ watch(
</div>
<div v-else-if="shot" class="panel storyboard-workspace">
<nav class="shot-list" aria-label="本集镜头">
<p class="px-4 pb-3 pt-5 text-[10px] text-muted">{{ shots.length }} 个镜头 · Beat 排列</p>
<template v-for="(item, index) in shots" :key="item.shotId"
><h4
v-if="index === 0 || shots[index - 1]?.beatNo !== item.beatNo"
class="px-4 pb-2 pt-4 font-mono text-[10px] text-muted"
<NScrollbar class="panel-scroll" content-class="shot-list-content">
<p class="px-4 pb-3 pt-5 text-[10px] text-muted">{{ shots.length }} 个镜头 · Beat 排列</p>
<template v-for="(item, index) in shots" :key="item.shotId"
><h4
v-if="index === 0 || shots[index - 1]?.beatNo !== item.beatNo"
class="px-4 pb-2 pt-4 font-mono text-[10px] text-muted"
>
BEAT {{ String(item.beatNo).padStart(2, '0') }}
</h4>
<NButton
:aria-current="shot.shotId === item.shotId ? 'true' : undefined"
@click="selectedShot = item.shotId"
class="shot-link"
:class="{ selected: shot.shotId === item.shotId }"
><span class="font-mono text-[10px] text-muted">{{
String(item.shotNo).padStart(2, '0')
}}</span
><span class="min-w-0"
><span class="block truncate text-xs font-medium">{{ item.title }}</span
><span class="mt-1 block text-[10px] text-muted"
>{{ item.direction ? '设计已保存' : '待设计' }} ·
{{ item.visualState ? '状态已保存' : '待状态' }}</span
></span
></NButton
></template
>
BEAT {{ String(item.beatNo).padStart(2, '0') }}
</h4>
<NButton
:aria-current="shot.shotId === item.shotId ? 'true' : undefined"
@click="selectedShot = item.shotId"
class="shot-link"
:class="{ selected: shot.shotId === item.shotId }"
><span class="font-mono text-[10px] text-muted">{{ String(item.shotNo).padStart(2, '0') }}</span
><span class="min-w-0"
><span class="block truncate text-xs font-medium">{{ item.title }}</span
><span class="mt-1 block text-[10px] text-muted"
>{{ item.direction ? '设计已保存' : '待设计' }} ·
{{ item.visualState ? '状态已保存' : '待状态' }}</span
></span
></NButton
></template
>
</NScrollbar>
</nav>
<article class="min-w-0 p-5 lg:p-7">
<header class="mb-6">
<p class="eyebrow">
BEAT {{ shot.beatNo }} / SHOT {{ shot.shotNo }}
<span v-if="shot.durationSeconds" class="ml-3">{{ shot.durationSeconds }}s</span>
</p>
<h3 class="mt-2 text-lg font-semibold">{{ shot.title }}</h3>
<p v-if="shot.description" class="mt-3 whitespace-pre-wrap text-sm leading-7">
{{ shot.description }}
</p>
<p v-if="shot.visualFocus" class="mt-2 text-xs leading-6 text-muted">
视觉重点{{ shot.visualFocus }}
</p>
<p class="mt-3 break-all font-mono text-[10px] text-muted">Shot ID · {{ shot.shotId }}</p>
<p class="mt-1 text-[10px] text-muted">
标题与剧情描述取自最近可用 Breakdown 快照以下设计与状态取自数据库
</p>
</header>
<div>
<NTabs v-model:value="detailTab" type="line" size="small" aria-label="镜头详情"
><NTab name="design">设计与状态</NTab><NTab name="tools">参考图与生成规格</NTab></NTabs
>
<div v-if="detailTab === 'design'" class="pt-6"><ShotDesign :shot="shot" /></div>
<div v-if="detailTab === 'tools'" class="pt-6">
<ShotTools :key="shot.shotId" :project-id="id" :shot="shot" :disabled="blocked" />
<article class="storyboard-detail min-w-0">
<NScrollbar class="panel-scroll" content-class="p-5 lg:p-7">
<header class="mb-6">
<p class="eyebrow">
BEAT {{ shot.beatNo }} / SHOT {{ shot.shotNo }}
<span v-if="shot.durationSeconds" class="ml-3">{{ shot.durationSeconds }}s</span>
</p>
<h3 class="mt-2 text-lg font-semibold">{{ shot.title }}</h3>
<p v-if="shot.description" class="mt-3 whitespace-pre-wrap text-sm leading-7">
{{ shot.description }}
</p>
<p v-if="shot.visualFocus" class="mt-2 text-xs leading-6 text-muted">
视觉重点{{ shot.visualFocus }}
</p>
<p class="mt-3 break-all font-mono text-[10px] text-muted">Shot ID · {{ shot.shotId }}</p>
<p class="mt-1 text-[10px] text-muted">
标题与剧情描述取自最近可用 Breakdown 快照以下设计与状态取自数据库
</p>
</header>
<div>
<NTabs v-model:value="detailTab" type="line" size="small" aria-label="镜头详情"
><NTab name="design">设计与状态</NTab><NTab name="tools">参考图与生成规格</NTab></NTabs
>
<div v-if="detailTab === 'design'" class="pt-6"><ShotDesign :shot="shot" /></div>
<div v-if="detailTab === 'tools'" class="pt-6">
<ShotTools :key="shot.shotId" :project-id="id" :shot="shot" :disabled="blocked" />
</div>
</div>
</div>
</NScrollbar>
</article>
</div>
<EmptyState
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NTable } from 'naive-ui'
import { NScrollbar, NAlert, NButton, NTable } from 'naive-ui'
import { computed } from 'vue'
import { Download } from '@lucide/vue'
import { downloadText } from '../../../lib/format'
@@ -42,7 +42,7 @@ function exportReceipt() {
{{ receipt.result.episodeCount }} · 完成 {{ receipt.result.completedEpisodes }} · 跳过
{{ receipt.result.skippedEpisodes }} · 失败 {{ receipt.result.failedEpisodes }}
</p>
<div class="table-scroll">
<NScrollbar x-scrollable class="table-scroll">
<NTable :single-line="false" class="project-table"
><thead>
<tr>
@@ -74,7 +74,7 @@ function exportReceipt() {
</tr>
</tbody></NTable
>
</div>
</NScrollbar>
</template>
<template v-else-if="receipt.kind === 'prompts'">
<p class="mt-4 text-sm">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NAlert, NButton, NCheckbox } from 'naive-ui'
import { NScrollbar, NAlert, NButton, NCheckbox } from 'naive-ui'
import { computed, onScopeDispose, ref, watch } from 'vue'
import { Download, ExternalLink, RefreshCw } from '@lucide/vue'
import { storyboardApi } from '../api'
@@ -183,7 +183,9 @@ function exportResult(kind: 'spec' | 'prompt') {
>{{ spec.subjects.length }} 个主体 · {{ spec.durationSeconds }} </span
><NButton @click="exportResult('spec')" text size="small"><Download :size="13" />导出规格</NButton>
</div>
<pre class="storyboard-json mt-3">{{ JSON.stringify(spec, null, 2) }}</pre>
<NScrollbar class="code-scroll mt-3" x-scrollable>
<pre class="storyboard-json">{{ JSON.stringify(spec, null, 2) }}</pre>
</NScrollbar>
</template>
</section>
<section class="border-t border-line pt-6">