auto-save 2026-05-12 18:46 (~3)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user