auto-save 2026-05-13 15:11 (~3)

This commit is contained in:
2026-05-13 15:11:45 +08:00
parent 6d08857695
commit 02df0c5a60
3 changed files with 72 additions and 87 deletions

View File

@@ -299,18 +299,10 @@ export default function Home() {
<>
<div className="canvas-bg" />
<main className="relative h-screen w-screen overflow-hidden flex">
{/* 右上工具 */}
<header className="absolute top-3 right-6 z-30 flex items-center gap-2 pointer-events-auto">
{job && (
<div className="glass-node flex items-center gap-2 px-3 h-9" style={{ borderRadius: 12 }}>
<span className="text-[10px] uppercase tracking-widest text-[var(--text-faint)]">JOB</span>
<span className="text-[11.5px] font-mono text-[var(--text-strong)]">{job.id.slice(0, 8)}</span>
<span className="text-[10.5px] text-[var(--text-faint)]">·</span>
<span className="text-[11.5px] text-[var(--text-soft)]">{job.message || job.status}</span>
</div>
)}
{/* 主题切换 — 左下角 */}
<div className="absolute bottom-3 left-3 z-30 pointer-events-auto">
<ThemeToggle />
</header>
</div>
{/* 左侧:竖向 tile 看板(极窄) */}
<aside

View File

@@ -1,7 +1,7 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { LayoutGrid, ChevronDown, ChevronUp, Sparkle, X } from "lucide-react"
import { LayoutGrid, ChevronDown, ChevronUp, Sparkle } from "lucide-react"
import { type Job, type KeyFrame, cutoutUrl, effectiveFrameUrl, hasCutout } from "@/lib/api"
interface Props {
@@ -32,6 +32,14 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame
)
// focused 分镜数据
// focus 用来高亮分镜缩略图 + 顶部 indicator不再触发底部详情面板展开
const focusFrame = focusedFrame !== null
? job.frames.find((f) => f.index === focusedFrame) ?? null
: null
const focusSeq = focusFrame
? frames.findIndex((f) => f.index === focusFrame.index) + 1
: 0
// 所有"已进入分镜阶段"的提取图(按分镜时间序展平)
type Shot = { frameIdx: number; seq: number; elementId: string; elementName: string; cid: string; isLegacy: boolean }
const allShots: Shot[] = []
@@ -68,26 +76,14 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame
</span>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{focusFrame && (
<button
onClick={() => onFocusFrame(null)}
className="text-[10.5px] text-white/60 hover:text-white inline-flex items-center gap-1 px-2 py-0.5 rounded border border-white/15 hover:border-white/30"
title="收起详情,回到列表视图"
>
<X className="h-3 w-3" />
</button>
)}
<button
onClick={() => setCollapsed(!collapsed)}
className="text-white/50 hover:text-white text-[11px] inline-flex items-center gap-1"
title={collapsed ? "展开" : "折叠"}
>
{collapsed ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />}
{collapsed ? "展开" : "折叠"}
</button>
</div>
<button
onClick={() => setCollapsed(!collapsed)}
className="shrink-0 text-white/50 hover:text-white text-[11px] inline-flex items-center gap-1"
title={collapsed ? "展开" : "折叠"}
>
{collapsed ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />}
{collapsed ? "展开" : "折叠"}
</button>
</div>
{/* thumbnails row */}
@@ -150,62 +146,46 @@ export function StoryboardBar({ job, selectedFrames, focusedFrame, onFocusFrame
)
)}
{/* 编排详情面板 — 简化版:只展示该分镜的所有提取图 */}
{focusFrame && !collapsed && (() => {
type Shot = { e: typeof focusElements[number]; cid: string; isLegacy: boolean }
const allShots: Shot[] = []
focusElements.forEach((e) => {
if (e.cutouts && e.cutouts.length > 0) {
e.cutouts.forEach((cid) => allShots.push({ e, cid, isLegacy: false }))
} else if (e.cutout_id) {
allShots.push({ e, cid: e.cutout_id, isLegacy: true })
}
})
return (
<div
className="border-t border-white/10 bg-black/20 overflow-y-auto"
style={{ maxHeight: "55vh" }}
>
<div className="p-4">
<div className="text-[12px] font-semibold text-white mb-2 flex items-center gap-1.5">
<Sparkle className="h-3.5 w-3.5 text-violet-300" />
{focusSeq} ·
<span className="text-[10px] text-white/40 font-mono">· {allShots.length} </span>
</div>
{allShots.length === 0 ? (
<div className="rounded-md border border-dashed border-white/15 p-3 text-[11px] text-white/40">
· AI
</div>
) : (
<div className="grid grid-cols-6 gap-2">
{allShots.map(({ e, cid, isLegacy }) => {
const url = isLegacy
? cutoutUrl(job.id, focusFrame.index, e.id)
: cutoutUrl(job.id, focusFrame.index, e.id, cid)
return (
<a
key={`${e.id}_${cid}`}
href={url}
target="_blank"
rel="noreferrer"
className="relative block rounded-md overflow-hidden border border-white/15 hover:border-violet-300/60 bg-white"
style={{ aspectRatio: "1/1" }}
title={`${e.name_zh} · 点击查看原图`}
>
<img src={url} alt={e.name_zh} className="absolute inset-0 w-full h-full object-contain" />
<div className="absolute bottom-0 left-0 right-0 px-1.5 py-0.5 text-[9px] text-white bg-black/70 truncate">
{e.name_zh}
</div>
</a>
)
})}
</div>
)}
</div>
{/* 所有提取图(按分镜顺序展平) */}
{!collapsed && allShots.length > 0 && (
<div className="border-t border-white/10 px-4 py-2 bg-black/10">
<div className="text-[10px] text-white/45 mb-1.5 inline-flex items-center gap-1">
<Sparkle className="h-2.5 w-2.5 text-violet-300" />
· {allShots.length}
</div>
)
})()}
<div className="flex gap-1.5 overflow-x-auto pb-1">
{allShots.map((s) => {
const url = s.isLegacy
? cutoutUrl(job.id, s.frameIdx, s.elementId)
: cutoutUrl(job.id, s.frameIdx, s.elementId, s.cid)
const isFocusFrame = focusedFrame === s.frameIdx
return (
<button
key={`${s.frameIdx}_${s.elementId}_${s.cid}`}
onClick={() => onFocusFrame(s.frameIdx)}
title={`${s.elementName} · 来自分镜 #${s.seq} · 点击聚焦该分镜`}
className={`relative shrink-0 rounded-md overflow-hidden border bg-white transition hover:-translate-y-0.5 ${
isFocusFrame
? "border-violet-300 ring-2 ring-violet-300/60"
: "border-white/15 hover:border-violet-300/50"
}`}
style={{ width: 80, height: 80 }}
>
<img src={url} alt={s.elementName} className="absolute inset-0 w-full h-full object-contain" />
{/* 左上:来自分镜号 */}
<div className="absolute top-0.5 left-0.5 text-[8.5px] font-bold text-white bg-violet-500/85 backdrop-blur px-1 py-0.5 rounded leading-none">
#{s.seq}
</div>
{/* 底部:元素名 */}
<div className="absolute bottom-0 left-0 right-0 px-1 py-0.5 text-[8.5px] text-white bg-black/70 truncate leading-tight">
{s.elementName}
</div>
</button>
)
})}
</div>
</div>
)}
{/* Hover 大图预览 · 浮在缩略图下方(不挡其他界面) */}
{mounted && hover && (() => {