auto-save 2026-05-12 18:46 (~3)

This commit is contained in:
2026-05-12 18:46:46 +08:00
parent 864781d5d4
commit 5a914b96df
3 changed files with 157 additions and 35 deletions

View File

@@ -214,6 +214,73 @@
.glass-node__body { padding: 0.85rem 1rem 1rem; }
/* ============================================================
Kanban 卡片Storyflow/参考图风格)
============================================================ */
.kanban-card {
position: relative;
border-radius: 14px;
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
.kanban-card:hover {
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.16);
box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.5);
}
/* dark 模式下:低明度彩色 */
.dark .kanban-violet { background: linear-gradient(160deg, rgba(167, 139, 250, 0.18), rgba(139, 92, 246, 0.08)); }
.dark .kanban-pink { background: linear-gradient(160deg, rgba(244, 114, 182, 0.18), rgba(236, 72, 153, 0.08)); }
.dark .kanban-orange { background: linear-gradient(160deg, rgba(251, 146, 60, 0.18), rgba(249, 115, 22, 0.08)); }
.dark .kanban-blue { background: linear-gradient(160deg, rgba(96, 165, 250, 0.18), rgba(59, 130, 246, 0.08)); }
.dark .kanban-green { background: linear-gradient(160deg, rgba(74, 222, 128, 0.18), rgba(34, 197, 94, 0.08)); }
.dark .kanban-cyan { background: linear-gradient(160deg, rgba(34, 211, 238, 0.18), rgba(6, 182, 212, 0.08)); }
.dark .kanban-rose { background: linear-gradient(160deg, rgba(251, 113, 133, 0.18), rgba(244, 63, 94, 0.08)); }
.dark .kanban-amber { background: linear-gradient(160deg, rgba(252, 211, 77, 0.18), rgba(245, 158, 11, 0.08)); }
/* light 模式pastel 暖底 */
:root:not(.dark) .kanban-violet { background: #ede9fe; border-color: rgba(139, 92, 246, 0.18); }
:root:not(.dark) .kanban-pink { background: #fce7f3; border-color: rgba(236, 72, 153, 0.18); }
:root:not(.dark) .kanban-orange { background: #ffedd5; border-color: rgba(249, 115, 22, 0.18); }
:root:not(.dark) .kanban-blue { background: #dbeafe; border-color: rgba(59, 130, 246, 0.18); }
:root:not(.dark) .kanban-green { background: #dcfce7; border-color: rgba(34, 197, 94, 0.18); }
:root:not(.dark) .kanban-cyan { background: #cffafe; border-color: rgba(6, 182, 212, 0.18); }
:root:not(.dark) .kanban-rose { background: #ffe4e6; border-color: rgba(244, 63, 94, 0.18); }
:root:not(.dark) .kanban-amber { background: #fef3c7; border-color: rgba(245, 158, 11, 0.18); }
.kanban-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 10.5px;
font-weight: 500;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-soft);
}
:root:not(.dark) .kanban-tag {
background: rgba(255, 255, 255, 0.7);
border-color: rgba(0, 0, 0, 0.06);
color: var(--text-soft);
}
.kanban-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
font-size: 10.5px;
color: var(--text-faint);
}
:root:not(.dark) .kanban-meta {
border-top-color: rgba(0, 0, 0, 0.06);
}
.glass-node__row {
display: flex; align-items: center; gap: 0.5rem;
font-size: 12px; color: var(--text-soft);

View File

@@ -35,6 +35,41 @@ function MiniCard({ children, className = "", onClick }: { children: ReactNode;
)
}
type KanbanTone = "violet" | "pink" | "orange" | "blue" | "green" | "cyan" | "rose" | "amber"
function KanbanCard({
tone = "violet",
tags,
title,
className = "",
onClick,
meta,
children,
}: {
tone?: KanbanTone
tags?: string[]
title?: ReactNode
className?: string
onClick?: (e: React.MouseEvent) => void
meta?: ReactNode
children?: ReactNode
}) {
return (
<div className={`kanban-card kanban-${tone} ${onClick ? "cursor-pointer" : ""} ${className}`} onClick={onClick}>
{tags && tags.length > 0 && (
<div className="flex items-center gap-1.5 mb-2 flex-wrap">
{tags.map((t) => (
<span key={t} className="kanban-tag">#{t}</span>
))}
</div>
)}
{title && <h3 className="text-[13.5px] font-semibold text-[var(--text-strong)] leading-snug mb-1.5">{title}</h3>}
{children}
{meta && <div className="kanban-meta">{meta}</div>}
</div>
)
}
interface Props {
data: NodeData
}
@@ -307,11 +342,11 @@ export function Dashboard({ data }: Props) {
</div>
)}
{/* ---- Keyframe ---- */}
{/* ---- Keyframe — Kanban 卡片 ---- */}
{key === "keyframe" && (
<div className="space-y-3">
{hasVideo && job && (
<MiniCard>
<KanbanCard tone="green" tags={["手动加帧"]} title="从视频任意时间点抽 1 张">
<button
type="button"
disabled={addingFrame}
@@ -320,42 +355,50 @@ export function Dashboard({ data }: Props) {
setAddingFrame(true)
try { await data.onAddManualFrame(t) } finally { setAddingFrame(false) }
}}
className="w-full text-[12px] py-2 rounded-md border border-dashed border-emerald-400/40 bg-emerald-400/5 hover:bg-emerald-400/10 text-emerald-300 disabled:opacity-50 inline-flex items-center justify-center gap-1.5"
className="mt-1 w-full text-[12px] py-1.5 rounded-md bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-50 inline-flex items-center justify-center gap-1.5"
>
{addingFrame ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
+ {videoT.toFixed(1)}s
{addingFrame ? "抽帧中…" : `${videoT.toFixed(1)}s 加为关键帧`}
</button>
</MiniCard>
</KanbanCard>
)}
{!hasFrames ? (
<div className="text-[11.5px] text-[var(--text-faint)] py-4 text-center"> 5 </div>
<KanbanCard tone="pink" tags={["分镜"]} title="等待解析后抽取">
<div className="text-[11.5px] text-[var(--text-soft)]"> 30 pHash + 5 </div>
</KanbanCard>
) : (
<div className="grid grid-cols-5 gap-3">
{job!.frames.map((f) => {
const isSel = data.selectedFrames.has(f.index)
return (
<MiniCard key={f.index} className={`p-0 overflow-hidden ${isSel ? "ring-2 ring-emerald-400 border-emerald-400" : ""}`}>
<button
type="button"
onClick={(e) => { e.stopPropagation(); data.onExpandFrame(f.index) }}
className="block w-full aspect-video bg-black relative overflow-hidden"
>
<img src={frameUrl(job!.id, f.index)} alt={`frame ${f.index}`} className="absolute inset-0 w-full h-full object-cover" />
<div className="absolute top-1 left-1 bg-black/70 text-white text-[9px] font-mono px-1.5 py-0.5 rounded">#{f.index + 1}</div>
<div className="absolute bottom-1 right-1 bg-black/70 text-white text-[9px] font-mono px-1.5 py-0.5 rounded">{f.timestamp.toFixed(1)}s</div>
</button>
<div className="px-2 py-1.5 flex items-center justify-between">
<span className="text-[10.5px] text-[var(--text-soft)]"> {f.index + 1}</span>
<KanbanCard
key={f.index}
tone={isSel ? "green" : "pink"}
tags={[`分镜 ${f.index + 1}`, `${f.timestamp.toFixed(1)}s`]}
className={isSel ? "ring-2 ring-emerald-400/60" : ""}
meta={
<button
type="button"
onClick={(e) => { e.stopPropagation(); data.onToggleFrame(f.index) }}
className={`text-[10px] px-1.5 py-0.5 rounded inline-flex items-center gap-0.5 ${isSel ? "bg-emerald-500 text-white" : "bg-white/10 text-[var(--text-soft)] border border-white/10"}`}
className={`ml-auto text-[10.5px] px-2 py-0.5 rounded-full inline-flex items-center gap-1 ${
isSel
? "bg-emerald-500 text-white"
: "bg-white/10 text-[var(--text-soft)] border border-white/15 hover:bg-white/20"
}`}
>
<Check className="h-2.5 w-2.5" />
{isSel ? "已选" : "选用"}
{isSel ? "已选" : "选用此帧"}
</button>
</div>
</MiniCard>
}
>
<button
type="button"
onClick={(e) => { e.stopPropagation(); data.onExpandFrame(f.index) }}
className="block w-full aspect-video rounded-md overflow-hidden bg-black relative"
>
<img src={frameUrl(job!.id, f.index)} alt={`frame ${f.index}`} className="absolute inset-0 w-full h-full object-cover" />
</button>
</KanbanCard>
)
})}
</div>
@@ -363,26 +406,31 @@ export function Dashboard({ data }: Props) {
</div>
)}
{/* ---- ASR / Translate ---- */}
{/* ---- ASR / Translate — Kanban 段落卡 ---- */}
{(key === "asr" || key === "translate") && (
!hasTranscript ? (
<div className="text-[11.5px] text-[var(--text-faint)] py-4 text-center">
{colState.asr === "running" ? "Gemini 转录中…" : "等待关键帧抽取"}
</div>
<KanbanCard tone={key === "asr" ? "blue" : "cyan"} tags={[key === "asr" ? "ASR" : "Translate"]} title="等待数据">
<div className="text-[11.5px] text-[var(--text-soft)]">
{colState.asr === "running" ? "Gemini 转录中…" : "需要先完成关键帧抽取"}
</div>
</KanbanCard>
) : (
<div className="grid grid-cols-2 gap-3">
{job!.transcript.map((s) => (
<MiniCard key={s.index}>
<div className="text-[9.5px] font-mono text-[var(--text-faint)] mb-1.5">
{s.start.toFixed(1)}s {s.end.toFixed(1)}s
<KanbanCard
key={s.index}
tone={key === "asr" ? "blue" : "cyan"}
tags={[`段落 ${s.index + 1}`, `${s.start.toFixed(1)}s → ${s.end.toFixed(1)}s`]}
>
<div className="text-[12.5px] text-[var(--text-strong)] leading-snug mb-1.5">
<span className="kanban-tag mr-1.5" style={{ padding: "1px 6px", fontSize: 9.5 }}>EN</span>
{s.en}
</div>
<div className="text-[12px] text-[var(--text-strong)] leading-snug mb-1">
<span className="text-[var(--text-faint)] mr-1 text-[10px]">EN</span>{s.en}
<div className="text-[12.5px] text-[var(--text-strong)] leading-snug">
<span className="kanban-tag mr-1.5" style={{ padding: "1px 6px", fontSize: 9.5 }}>ZH</span>
{s.zh || <span className="text-[var(--text-faint)] italic"></span>}
</div>
<div className="text-[12px] text-[var(--text-strong)] leading-snug">
<span className="text-[var(--text-faint)] mr-1 text-[10px]">ZH</span>{s.zh || <span className="text-[var(--text-faint)]"></span>}
</div>
</MiniCard>
</KanbanCard>
))}
</div>
)