From 9e3ce9df6b86376bdc5f82f1a1bd1dd569ef8de7 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 11:28:56 +0800 Subject: [PATCH] auto-save 2026-05-13 11:28 (~4) --- .memory/worklog.json | 13 +++ web/components/lightbox.tsx | 148 +++++++++++++++++++-------------- web/components/nodes/index.tsx | 2 +- web/lib/api.ts | 6 ++ 4 files changed, 107 insertions(+), 62 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index e9679f1..95ce866 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1330,6 +1330,19 @@ "message": "auto-save 2026-05-13 11:17 (~2)", "hash": "f4ce533", "files_changed": 2 + }, + { + "ts": "2026-05-13T11:23:24+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 11:23 (~4)", + "hash": "647b05a", + "files_changed": 4 + }, + { + "ts": "2026-05-13T03:27:37Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 11:23 (~4)", + "files_changed": 2 } ] } diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 801ed32..deb6788 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -109,17 +109,17 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } const handleExtractRegion = async () => { - if (!region || !extractName.trim()) return + // 提取语义只在恰好 1 个框时支持 + if (regions.length !== 1 || !extractName.trim()) return + const r = regions[0] setExtracting(true) try { - // 先加 element 拿到 id const added = await addElement(jobId, f.index, { name_zh: extractName.trim(), source: "region", - region, + region: r, }) onJobUpdate?.(added) - // 找到新加的 element id(按 created_at desc 取最新一条) const fr = added.frames.find((x) => x.index === f.index) const newEl = fr?.elements?.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))[0] if (newEl) { @@ -129,7 +129,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } else { toast.success(`「${extractName.trim()}」已加入元素清单 · 但抠图未触发`) } - setCropMode(false); setRegion(null); setExtractNamePrompt(false); setExtractName("") + setCropMode(false); setRegions([]); setExtractNamePrompt(false); setExtractName("") } catch (e) { toast.error("提取失败:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -153,20 +153,26 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const p = getRelXY(e.clientX, e.clientY) if (!p) return setDragStart(p) - setRegion({ x: p.x, y: p.y, w: 0, h: 0 }) + setDraftRegion({ x: p.x, y: p.y, w: 0, h: 0 }) } const onCropMouseMove = (e: React.MouseEvent) => { if (!cropMode || !dragStart) return const p = getRelXY(e.clientX, e.clientY) if (!p) return - setRegion({ + setDraftRegion({ x: Math.min(dragStart.x, p.x), y: Math.min(dragStart.y, p.y), w: Math.abs(p.x - dragStart.x), h: Math.abs(p.y - dragStart.y), }) } - const onCropMouseUp = () => setDragStart(null) + const onCropMouseUp = () => { + if (draftRegion && draftRegion.w >= 0.02 && draftRegion.h >= 0.02) { + setRegions((prev) => [...prev, draftRegion]) + } + setDraftRegion(null) + setDragStart(null) + } const handleApplyCleaned = async () => { setApplying(true) @@ -301,31 +307,36 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o {f.cleaned_applied ? "✨ 已替换为清洗版" : "原图"} - {/* 画框 overlay */} - {cropMode && region && region.w > 0 && region.h > 0 && ( - <> - {/* 选区外暗化 — 用 4 个半透 div 围出 */} -
-
-
-
- {/* 选区高亮边框 */} -
- + {/* 已确认的多个选区 */} + {cropMode && regions.map((r, i) => ( +
+ #{i + 1} +
+ ))} + + {/* 当前正在拖的草稿框 */} + {cropMode && draftRegion && draftRegion.w > 0 && draftRegion.h > 0 && ( +
)} - {/* 画框模式提示 */} + {/* 画框模式角标(小,左上) — 不再遮挡画面 */} {cropMode && ( -
- {region && region.w > 0 ? `选区:${(region.w * 100).toFixed(0)}% × ${(region.h * 100).toFixed(0)}%` : "在图上拖动鼠标 → 框选清洗范围"} +
+ 画框 · 已选 {regions.length}
)}
@@ -333,7 +344,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o {/* 画框工具栏 */} {cropMode ? ( extractNamePrompt ? ( - // 提取模式:要用户填名字 + // 提取模式:要用户填名字(仅 1 框时进入此模式)
) : ( - // 画框完成 → 选操作 -
- - - + // 画框模式 — 可连续画多框 +
+
+ {regions.length === 0 + ? "在图上拖动鼠标 → 框选要处理的区域(可连续画多个)" + : `已框 ${regions.length} 个 · 继续拖鼠标加框 · 「去掉」批量清洗 · 单框可「提取」为元素`} +
+
+ + + + +
) ) : ( )} diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 272f18c..1d320b5 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -6,7 +6,7 @@ import { Mic, Languages, FileEdit, Sparkles, Film, FileVideo, Loader2, Plus, X, } from "lucide-react" import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell" -import { type Job, frameUrl, videoUrl, generatedImageUrl } from "@/lib/api" +import { type Job, frameUrl, effectiveFrameUrl, videoUrl, generatedImageUrl } from "@/lib/api" export interface NodeData { job: Job | null // 当前 active job diff --git a/web/lib/api.ts b/web/lib/api.ts index 130d018..04fecd1 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -196,6 +196,12 @@ export function frameUrl(jobId: string, frameIndex: number): string { return `${API_BASE}/jobs/${jobId}/frames/${frameIndex}.jpg` } +// 接 frame 对象时返回正确版本 URL(已应用清洗版时加 cache-bust) +export function effectiveFrameUrl(jobId: string, frame: { index: number; cleaned_applied?: boolean | null }): string { + const base = `${API_BASE}/jobs/${jobId}/frames/${frame.index}.jpg` + return frame.cleaned_applied ? `${base}?v=applied` : base +} + export function videoUrl(jobId: string): string { return `${API_BASE}/jobs/${jobId}/video.mp4` }