fix: stabilize workbench layout frame
This commit is contained in:
2
RULES.md
2
RULES.md
@@ -11,7 +11,7 @@
|
||||
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
||||
- 风格:`04-Dark-Gallery-Ambient`(路径:`~/Projects/research/20260305-网页风格库/04-Dark-Gallery-Ambient.md`)
|
||||
- 第一冲刺:步骤 1-4(下载 / 拆轨 / 关键帧 / ASR+翻译)
|
||||
- 当前产品方向(2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方消息输入区发送复刻/创新/卡通和画面要求,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后必须弹窗确认,用户点确认才生成对应数量的统一多角度套图。右侧主体元素区的套图输出、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
|
||||
- 当前产品方向(2026-05-20 再确认):信息流广告快速复刻默认进入“三字段候选生成”工作流。主界面为“左侧素材输入列 + 右侧信息流复刻工作表”;工作台使用固定 1800x1000 操作画布,不同显示器或浏览器宽度下保持同一框架,窗口变小时只滚动查看,不通过 `xl/2xl` 断点重排核心操作区。用户粘贴 TK 链接或上传视频后点击“开始分析”,系统自动下载源视频;下载完成后并行启动两条路:音频文案路提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效;视频视觉路自动抽取参考帧。源视频工作区右侧主体链路是“参考帧池 → 转换层 → 主体元素”:参考帧池竖向排列;转换层是轻量对话式生图确认区,参考图可通过左侧缩略图 `+`、参考帧拖拽、胶片拖拽或本地图片拖入进入转换层,用户选择 GPT/Gemini 套件后先分析参考图;识别结果里的特征 chip 只作为“保留元素”本地选择,点亮=保留、再点取消,点击不立即请求模型,随下一条发送消息提交;用户再在下方消息输入区发送复刻/创新/卡通和画面要求,生成数量通过发送区旁边的张数控件控制;后端返回英文出图 prompt 后必须弹窗确认,用户点确认才生成对应数量的统一多角度套图。右侧主体元素区的套图输出、文件夹分组、单张重生、删除和 hover 预览逻辑保持不变。旧下方“相似主体 / 主体模板库”不再作为主路径。波形下方的画面胶片只是临时预览,点击只跳转原视频时间点,双击或拖进参考帧池才正式加入关键帧,已加入的胶片直接显示“已添加”。产品图上传后独立形成产品资产包,自动识别视角/结构/比例并补缺角度。分镜工作台按逐句时间轴默认只露“文案 / 场景一句话 / 人物+产品+动作”,产品素材池、批量控制、三字段、视频候选和高级区都必须可折叠;视频候选无内容时默认不占大面积,有候选时默认只显示迷你缩略条,展开后才显示 4-grid。单条默认生成 4 个视频候选,顶部支持整片批量生成候选;首尾帧、视觉规划、产品出现方式和旧 6 字段保留在“高级”抽屉与后端 quick-plan 自动展开中,不能再作为客户默认闸门。
|
||||
|
||||
## 部署事实
|
||||
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -496,7 +496,7 @@ nextjs-portal {
|
||||
|
||||
.skg-board-theme::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
@@ -509,7 +509,7 @@ nextjs-portal {
|
||||
|
||||
.skg-board-theme::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -2468,9 +2468,9 @@ export function AdRecreationBoard({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 h-screen w-screen overflow-hidden bg-black text-white`}>
|
||||
<div className="skg-board-ambient pointer-events-none absolute inset-0" />
|
||||
<div className="relative z-10 flex h-full flex-col px-4 py-4">
|
||||
<section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 h-screen w-screen overflow-auto bg-black text-white`}>
|
||||
<div className="skg-board-ambient pointer-events-none fixed inset-0" />
|
||||
<div className="relative z-10 mx-auto flex h-[1000px] w-[1800px] max-w-none flex-col px-4 py-4">
|
||||
<header className="skg-board-topbar mb-3 flex items-center justify-between gap-4 rounded-lg border border-white/10 bg-white/[0.04] px-4 py-3">
|
||||
<div className="skg-board-brand">
|
||||
<div className="skg-board-brand__logo-chip" aria-hidden="true">
|
||||
@@ -2707,6 +2707,7 @@ function AudioIntakePanel({
|
||||
const [filmstripDragTime, setFilmstripDragTime] = useState<number | null>(null)
|
||||
const [filmstripBusyTime, setFilmstripBusyTime] = useState<number | null>(null)
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const transcriptScrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const rowRefs = useRef<Record<number, HTMLDivElement | null>>({})
|
||||
const syncFrameRef = useRef<number | null>(null)
|
||||
const audioSrcUrl = job ? apiAssetUrl(job.source_audio_url) || sourceAudioUrl(job.id) : ""
|
||||
@@ -2758,7 +2759,16 @@ function AudioIntakePanel({
|
||||
}, [audioSrcUrl, job?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSegment) rowRefs.current[activeSegment.index]?.scrollIntoView({ block: "nearest" })
|
||||
const container = transcriptScrollRef.current
|
||||
const row = activeSegment ? rowRefs.current[activeSegment.index] : null
|
||||
if (!container || !row) return
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const rowRect = row.getBoundingClientRect()
|
||||
if (rowRect.top < containerRect.top) {
|
||||
container.scrollTop -= containerRect.top - rowRect.top
|
||||
} else if (rowRect.bottom > containerRect.bottom) {
|
||||
container.scrollTop += rowRect.bottom - containerRect.bottom
|
||||
}
|
||||
}, [activeSegment?.index])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2904,13 +2914,13 @@ function AudioIntakePanel({
|
||||
|
||||
<div className="grid gap-2 border-t border-white/8 pt-2">
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-3 xl:grid-cols-[430px_minmax(0,1fr)] 2xl:grid-cols-[460px_minmax(0,1fr)]">
|
||||
<div className="grid grid-cols-[460px_minmax(0,1fr)] gap-3">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<SectionTitle icon={<Play className="h-4 w-4" />} title="原版视频" />
|
||||
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s</span>
|
||||
</div>
|
||||
<div className="relative mx-auto aspect-[9/16] h-[450px] overflow-hidden rounded-md border border-white/10 bg-black 2xl:h-[510px]">
|
||||
<div className="relative mx-auto aspect-[9/16] h-[510px] overflow-hidden rounded-md border border-white/10 bg-black">
|
||||
{job.video_url ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
@@ -2947,6 +2957,7 @@ function AudioIntakePanel({
|
||||
job={job}
|
||||
processing={processing}
|
||||
activeSegmentIndex={activeSegment?.index ?? null}
|
||||
scrollRef={transcriptScrollRef}
|
||||
rowRefs={rowRefs}
|
||||
onSeek={seekTo}
|
||||
/>
|
||||
@@ -3019,12 +3030,14 @@ function TranscriptTimelinePanel({
|
||||
job,
|
||||
processing,
|
||||
activeSegmentIndex,
|
||||
scrollRef,
|
||||
rowRefs,
|
||||
onSeek,
|
||||
}: {
|
||||
job: Job
|
||||
processing: boolean
|
||||
activeSegmentIndex: number | null
|
||||
scrollRef: RefObject<HTMLDivElement | null>
|
||||
rowRefs: { current: Record<number, HTMLDivElement | null> }
|
||||
onSeek: (time: number) => void
|
||||
}) {
|
||||
@@ -3040,7 +3053,7 @@ function TranscriptTimelinePanel({
|
||||
<div>时间</div>
|
||||
<div>原文 / 中文</div>
|
||||
</div>
|
||||
<div className="max-h-[252px] overflow-y-auto 2xl:max-h-[306px]">
|
||||
<div ref={scrollRef} className="max-h-[306px] overflow-y-auto">
|
||||
{job.transcript.map((segment) => {
|
||||
const active = activeSegmentIndex === segment.index
|
||||
return (
|
||||
@@ -3869,7 +3882,7 @@ function SourceSubjectPipeline({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-2 xl:grid-cols-[150px_minmax(260px,0.9fr)_minmax(0,1.1fr)] 2xl:grid-cols-[170px_minmax(285px,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div className="grid grid-cols-[170px_minmax(285px,0.95fr)_minmax(0,1.05fr)] gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<SectionTitle icon={<ImageIcon className="h-4 w-4" />} title="参考帧池" />
|
||||
@@ -3920,7 +3933,7 @@ function SourceSubjectPipeline({
|
||||
<span>{frames.length} 张</span>
|
||||
<span>{filmstripDragging ? "松手加入" : "点击选择"}</span>
|
||||
</div>
|
||||
<div className="flex max-h-[410px] flex-col gap-1 overflow-y-auto pr-0.5 2xl:max-h-[500px]">
|
||||
<div className="flex max-h-[500px] flex-col gap-1 overflow-y-auto pr-0.5">
|
||||
{frames.map((frame, index) => {
|
||||
const selected = selectedFrames.has(frame.index)
|
||||
return (
|
||||
@@ -3941,7 +3954,7 @@ function SourceSubjectPipeline({
|
||||
alt={`参考帧 ${index + 1}`}
|
||||
label={`参考帧 ${String(index + 1).padStart(2, "0")}`}
|
||||
meta={`${frame.timestamp.toFixed(1)}s`}
|
||||
className="mx-auto aspect-[9/16] w-[72px] 2xl:w-[80px]"
|
||||
className="mx-auto aspect-[9/16] w-[80px]"
|
||||
objectFit="contain"
|
||||
previewObjectFit="contain"
|
||||
previewPlacement="left"
|
||||
@@ -3985,7 +3998,7 @@ function SourceSubjectPipeline({
|
||||
{agentReferenceFrames.length ? `${agentReferenceFrames.length}/${RECONSTRUCTION_FRAME_LIMIT} 图` : "待选图"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex min-h-[410px] flex-col rounded-md border border-white/10 bg-black/24 p-2 2xl:min-h-[500px]">
|
||||
<div className="flex min-h-[500px] flex-col rounded-md border border-white/10 bg-black/24 p-2">
|
||||
<div className="mb-2 grid grid-cols-2 gap-1.5">
|
||||
{SUBJECT_MODEL_BUNDLE_OPTIONS.map((option) => (
|
||||
<button
|
||||
@@ -4029,7 +4042,7 @@ function SourceSubjectPipeline({
|
||||
alt={`转换参考 ${index + 1}`}
|
||||
label={`参考 ${index + 1}`}
|
||||
meta={`${frame.timestamp.toFixed(1)}s`}
|
||||
className="aspect-[9/16] w-[44px] shrink-0 bg-black 2xl:w-[50px]"
|
||||
className="aspect-[9/16] w-[50px] shrink-0 bg-black"
|
||||
objectFit="contain"
|
||||
previewObjectFit="contain"
|
||||
previewPlacement="left"
|
||||
@@ -4230,7 +4243,7 @@ function SourceSubjectPipeline({
|
||||
{subjectAssetPacks.length ? `${subjectAssetPacks.length} 套` : "待生成"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-h-[410px] rounded-md border border-white/10 bg-black/32 p-2 2xl:min-h-[500px]">
|
||||
<div className="min-h-[500px] rounded-md border border-white/10 bg-black/32 p-2">
|
||||
{subjectBusyFor ? (
|
||||
<div className="mb-2 rounded-md border border-cyan-200/20 bg-cyan-300/[0.07] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/70">
|
||||
正在生成{reconstructionModeConfig(subjectBusyFor.mode).label} {subjectBusyFor.viewCount} 张;参考 {subjectBusyFor.sourceCount || "自主描述"}。
|
||||
@@ -4253,7 +4266,7 @@ function SourceSubjectPipeline({
|
||||
{activeSubjectPack.assets.length} 张
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid max-h-[300px] grid-cols-[repeat(auto-fill,minmax(76px,1fr))] gap-2 overflow-y-auto pr-0.5 2xl:grid-cols-[repeat(auto-fill,minmax(88px,1fr))]">
|
||||
<div className="grid max-h-[300px] grid-cols-[repeat(auto-fill,minmax(88px,1fr))] gap-2 overflow-y-auto pr-0.5">
|
||||
{activeSubjectPack.assets.map((asset) => {
|
||||
const busyMode = subjectAssetBusy?.endsWith(asset.id) ? subjectAssetBusy.split(":")[0] : ""
|
||||
const status = subjectAssetStatus(asset)
|
||||
@@ -4437,7 +4450,7 @@ function SourceKeyframePicker({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`min-h-[205px] rounded-md border p-1.5 transition 2xl:min-h-[260px] ${
|
||||
className={`min-h-[260px] rounded-md border p-1.5 transition ${
|
||||
filmstripDragging
|
||||
? dropActive
|
||||
? "border-[#d6b36a]/80 bg-[#d6b36a]/12 ring-1 ring-[#d6b36a]/45"
|
||||
@@ -4472,7 +4485,7 @@ function SourceKeyframePicker({
|
||||
<span className="text-[10px] text-white/34">参考帧池</span>
|
||||
<span className="text-[9.5px] text-white/28">{filmstripDragging ? "松手加入关键帧" : "拖入胶片选帧,悬停放大"}</span>
|
||||
</div>
|
||||
<div className="grid max-h-[178px] grid-cols-[repeat(auto-fill,minmax(38px,1fr))] gap-1 overflow-y-auto pr-0.5 2xl:max-h-[232px]">
|
||||
<div className="grid max-h-[232px] grid-cols-[repeat(auto-fill,minmax(38px,1fr))] gap-1 overflow-y-auto pr-0.5">
|
||||
{frames.map((frame, index) => {
|
||||
const selected = selectedFrames.has(frame.index)
|
||||
return (
|
||||
@@ -4863,7 +4876,7 @@ function SourceReferenceBuildPanel({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 grid gap-1.5 sm:grid-cols-2">
|
||||
<div className="mb-2 grid grid-cols-2 gap-1.5">
|
||||
{[
|
||||
{ value: "template" as const, label: "用模板生成", desc: "从内置形象或数据库模板延展新主体" },
|
||||
{ value: "source_similar" as const, label: "不用模板(从源视频关键帧创新)", desc: "只读取源视频角色文字特征,不上传参考图做复制" },
|
||||
@@ -4965,7 +4978,7 @@ function SourceReferenceBuildPanel({
|
||||
) : null}
|
||||
|
||||
<div className="my-2 h-px bg-white/10" />
|
||||
<div className="grid gap-2 lg:grid-cols-[1fr_1.6fr_auto]">
|
||||
<div className="grid grid-cols-[1fr_1.6fr_auto] gap-2">
|
||||
<input
|
||||
value={templateDraftName}
|
||||
onChange={(event) => setTemplateDraftName(event.target.value)}
|
||||
@@ -5046,7 +5059,7 @@ function SourceReferenceBuildPanel({
|
||||
alt={asset.label || asset.view}
|
||||
label={asset.label || asset.view || "主体视图预览"}
|
||||
meta={subjectAssetStatusLabel(asset)}
|
||||
className={`aspect-[9/16] w-20 2xl:w-24 ${running ? "bg-black/40" : failed ? "bg-rose-950/30" : "bg-white"}`}
|
||||
className={`aspect-[9/16] w-24 ${running ? "bg-black/40" : failed ? "bg-rose-950/30" : "bg-white"}`}
|
||||
objectFit="contain"
|
||||
busy={running}
|
||||
emptyText={failed ? "失败" : running ? "生成中" : undefined}
|
||||
@@ -5117,7 +5130,7 @@ function SourceReferenceBuildPanel({
|
||||
</div>
|
||||
|
||||
{subjectProfileMode === "manual" ? (
|
||||
<div className="mt-2 grid gap-1.5 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="mt-2 grid grid-cols-4 gap-1.5">
|
||||
{SUBJECT_PROFILE_CATEGORIES.map((category) => (
|
||||
<label key={category.key} className="min-w-0">
|
||||
<span className="mb-1 block text-[9px] font-semibold text-white/40">{category.label}</span>
|
||||
@@ -5139,7 +5152,7 @@ function SourceReferenceBuildPanel({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 xl:grid-cols-[auto_auto_minmax(220px,1fr)_auto] xl:items-start">
|
||||
<div className="grid grid-cols-[auto_auto_minmax(220px,1fr)_auto] items-start gap-2">
|
||||
<div className="flex rounded-md border border-white/10 bg-black/28 p-0.5">
|
||||
{[
|
||||
{ value: "transparent_human" as const, label: "透明骨架" },
|
||||
@@ -6159,7 +6172,7 @@ function AudioStoryboardPlanPanel({
|
||||
</div>
|
||||
|
||||
{panelOpen.product ? (
|
||||
<div className="grid max-h-[360px] gap-2 overflow-y-auto pr-1 md:grid-cols-2 2xl:grid-cols-3">
|
||||
<div className="grid max-h-[360px] grid-cols-3 gap-2 overflow-y-auto pr-1">
|
||||
{productItems.map((item) => (
|
||||
<ProductReferenceCard
|
||||
key={item.id}
|
||||
@@ -6202,7 +6215,7 @@ function AudioStoryboardPlanPanel({
|
||||
</button>
|
||||
</div>
|
||||
{panelOpen.batch ? (
|
||||
<div className="mt-2 grid gap-2 xl:grid-cols-[minmax(0,1fr)_auto]">
|
||||
<div className="mt-2 grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||
<textarea
|
||||
value={authorIntent}
|
||||
onChange={(event) => setAuthorIntent(event.target.value)}
|
||||
@@ -6391,7 +6404,7 @@ function AudioStoryboardPlanPanel({
|
||||
</div>
|
||||
|
||||
{fieldsOpen ? (
|
||||
<div className="mt-2 grid gap-3 xl:grid-cols-[minmax(280px,0.82fr)_minmax(420px,1.18fr)]">
|
||||
<div className="mt-2 grid grid-cols-[minmax(280px,0.82fr)_minmax(420px,1.18fr)] gap-3">
|
||||
<div className="grid min-w-0 gap-1.5">
|
||||
<CompactStoryboardField
|
||||
label="文案"
|
||||
@@ -6450,7 +6463,7 @@ function AudioStoryboardPlanPanel({
|
||||
</div>
|
||||
|
||||
{advancedRows.has(row.index) ? (
|
||||
<div className="grid xl:grid-cols-[54px_120px_minmax(170px,0.48fr)_minmax(420px,1.2fr)_360px] 2xl:grid-cols-[56px_140px_280px_minmax(560px,1fr)_420px]">
|
||||
<div className="grid grid-cols-[56px_140px_280px_minmax(560px,1fr)_420px]">
|
||||
<StoryboardPlanCell label="分镜">
|
||||
<div className="font-mono text-[11px] text-white/40">{row.start.toFixed(1)}-{row.end.toFixed(1)}s</div>
|
||||
<div className="mt-1.5 inline-flex max-w-full rounded-md border border-emerald-300/15 bg-emerald-300/[0.08] px-1.5 py-0.5 text-[10px] leading-tight text-emerald-100/80">
|
||||
@@ -6548,7 +6561,7 @@ function AudioStoryboardPlanPanel({
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<div className="grid gap-1 md:grid-cols-2">
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<textarea
|
||||
value={plannedRow.firstFramePlan}
|
||||
onChange={(event) => patchRowPlan(row.index, { firstFramePlan: event.target.value })}
|
||||
@@ -6563,7 +6576,7 @@ function AudioStoryboardPlanPanel({
|
||||
/>
|
||||
</div>
|
||||
{showChineseMirror && (plannedRow.firstFramePlanZh || plannedRow.lastFramePlanZh) ? (
|
||||
<div className="-mt-1 grid gap-1 md:grid-cols-2">
|
||||
<div className="-mt-1 grid grid-cols-2 gap-1">
|
||||
<p className="line-clamp-2 text-[10px] leading-snug text-emerald-100/34" title={plannedRow.firstFramePlanZh}>中:{plannedRow.firstFramePlanZh}</p>
|
||||
<p className="line-clamp-2 text-[10px] leading-snug text-cyan-100/34" title={plannedRow.lastFramePlanZh}>中:{plannedRow.lastFramePlanZh}</p>
|
||||
</div>
|
||||
@@ -6602,7 +6615,7 @@ function AudioStoryboardPlanPanel({
|
||||
产品入库
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-1.5 md:grid-cols-[minmax(0,1fr)_88px_88px]">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_88px_88px] gap-1.5">
|
||||
<div className="rounded border border-white/10 bg-black/24 px-2 py-1.5 text-[10px] leading-snug text-white/42">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="text-white/54">{sceneStep.no} 首尾帧闸门</span>
|
||||
@@ -6657,7 +6670,7 @@ function AudioStoryboardPlanPanel({
|
||||
</div>
|
||||
</StoryboardPlanCell>
|
||||
|
||||
<StoryboardPlanCell label={`${videoStep.no} 视频候选 / 待生成`} className="xl:border-r-0">
|
||||
<StoryboardPlanCell label={`${videoStep.no} 视频候选 / 待生成`} className="border-r-0">
|
||||
<StoryboardVideoSlots
|
||||
job={job}
|
||||
videos={rowVideos}
|
||||
@@ -6736,7 +6749,7 @@ function AudioStoryboardPlanPanel({
|
||||
</div>
|
||||
|
||||
{refinePreview ? (
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-2">
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<div className="rounded-md border border-white/10 bg-black/24 p-2">
|
||||
<div className="mb-1 text-[11px] font-semibold text-white/58">改前</div>
|
||||
<p className="text-[11px] leading-snug text-white/60">文案:{plannedDialogRow.skgCopy}</p>
|
||||
@@ -6949,7 +6962,7 @@ function MissingProductViewSlot({
|
||||
|
||||
function StoryboardPlanCell({ label, children, className = "" }: { label: string; children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={`min-w-0 border-b border-white/8 p-2 xl:border-b-0 xl:border-r ${className}`}>
|
||||
<div className={`min-w-0 border-r border-white/8 p-2 ${className}`}>
|
||||
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-white/32">{label}</div>
|
||||
{children}
|
||||
</div>
|
||||
@@ -7611,7 +7624,7 @@ function StoryboardSegmentCard({
|
||||
|
||||
<div className="grid gap-3">
|
||||
<SegmentBand icon={<FileText className="h-4 w-4" />} title="音频分镜文案">
|
||||
<div className="grid gap-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<textarea
|
||||
value={scene.scene ?? ""}
|
||||
onChange={(e) => patch({ scene: e.target.value })}
|
||||
@@ -7640,7 +7653,7 @@ function StoryboardSegmentCard({
|
||||
</SegmentBand>
|
||||
|
||||
<SegmentBand icon={<Package className="h-4 w-4" />} title="每个分镜需要的关键元素">
|
||||
<div className="grid gap-3 lg:grid-cols-[170px_minmax(0,1fr)_auto]">
|
||||
<div className="grid grid-cols-[170px_minmax(0,1fr)_auto] gap-3">
|
||||
<img src={effectiveFrameUrl(job.id, frame)} alt={frameLabel(frame, order)} className="aspect-video w-full rounded-md border border-white/10 bg-black object-cover" />
|
||||
<div className="min-w-0">
|
||||
<div className="mb-2 flex items-center gap-2 text-[11px] text-white/44">
|
||||
@@ -7703,7 +7716,7 @@ function StoryboardSegmentCard({
|
||||
</SegmentBand>
|
||||
|
||||
<SegmentBand icon={<Film className="h-4 w-4" />} title="视频生成">
|
||||
<div className="grid gap-3 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<div className="grid grid-cols-[220px_minmax(0,1fr)] gap-3">
|
||||
<div className="grid gap-2">
|
||||
<select value={model} onChange={(e) => setModel(e.target.value as VideoModel)} className="h-10 rounded-md border border-white/10 bg-black/55 px-2 text-[12px] text-white outline-none">
|
||||
{VIDEO_MODELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
@@ -7713,7 +7726,7 @@ function StoryboardSegmentCard({
|
||||
生成本分镜视频
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{videos.length > 0 ? videos.map((video) => (
|
||||
<VideoCandidate
|
||||
key={video.id}
|
||||
@@ -7806,7 +7819,7 @@ function DraftSegmentCard({
|
||||
|
||||
<div className="grid gap-3">
|
||||
<SegmentBand icon={<FileText className="h-4 w-4" />} title="音频分镜文案">
|
||||
<div className="grid gap-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<textarea value={draft.scene.scene ?? ""} onChange={(e) => patchScene({ scene: e.target.value })} placeholder="新剧情" className={`${fieldClass} min-h-[72px]`} />
|
||||
<textarea value={draft.scene.product ?? ""} onChange={(e) => patchScene({ product: e.target.value })} placeholder="产品融入" className={`${fieldClass} min-h-[72px]`} />
|
||||
<textarea value={draft.scene.action ?? ""} onChange={(e) => patchScene({ action: e.target.value })} placeholder="动作 / 镜头" className={`${fieldClass} min-h-[72px]`} />
|
||||
@@ -7824,7 +7837,7 @@ function DraftSegmentCard({
|
||||
</SegmentBand>
|
||||
|
||||
<SegmentBand icon={<Package className="h-4 w-4" />} title="每个分镜需要的关键元素">
|
||||
<div className="grid gap-3 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<div className="grid grid-cols-[220px_minmax(0,1fr)] gap-3">
|
||||
<select
|
||||
value={draft.frameIndex ?? ""}
|
||||
onChange={(e) => onPatch({ frameIndex: e.target.value ? Number(e.target.value) : null })}
|
||||
@@ -7846,7 +7859,7 @@ function DraftSegmentCard({
|
||||
</SegmentBand>
|
||||
|
||||
<SegmentBand icon={<Film className="h-4 w-4" />} title="视频生成">
|
||||
<div className="grid gap-2 lg:grid-cols-[220px_220px]">
|
||||
<div className="grid grid-cols-[220px_220px] gap-2">
|
||||
<select value={model} onChange={(e) => setModel(e.target.value as VideoModel)} className="h-10 rounded-md border border-white/10 bg-black/55 px-2 text-[12px] text-white outline-none">
|
||||
{VIDEO_MODELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user