auto-save 2026-05-13 15:11 (~3)
This commit is contained in:
@@ -1742,6 +1742,19 @@
|
||||
"message": "auto-save 2026-05-13 15:00 (~2)",
|
||||
"hash": "dfa5600",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T15:06:07+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-13 15:05 (~2)",
|
||||
"hash": "6d08857",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T07:07:40Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 15:05 (~2)",
|
||||
"files_changed": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (() => {
|
||||
|
||||
Reference in New Issue
Block a user