From 08d7cb470cc6c8da392ad1862c00df280acef35f Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 11:01:06 +0800 Subject: [PATCH] auto-save 2026-05-13 11:00 (~2) --- .memory/worklog.json | 13 ++++ web/components/lightbox.tsx | 119 +++++++++++++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 10 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 74f90cb..6175d02 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1277,6 +1277,19 @@ "message": "auto-save 2026-05-13 10:49 (~2)", "hash": "99bcb80", "files_changed": 2 + }, + { + "ts": "2026-05-13T10:55:33+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 10:55 (~4)", + "hash": "40deb81", + "files_changed": 4 + }, + { + "ts": "2026-05-13T02:57:37Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-13 10:55 (~4)", + "files_changed": 2 } ] } diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index d7f97ab..901140a 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -85,12 +85,13 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } } - const handleCleanup = async () => { + const handleCleanup = async (withRegion = false) => { setCleaning(true) try { - const updated = await cleanupFrame(jobId, f.index) + const updated = await cleanupFrame(jobId, f.index, withRegion ? region : null) onJobUpdate?.(updated) - toast.success(`分镜 ${f.index + 1} 清洗完成 · 下方查看`) + toast.success(`分镜 ${f.index + 1} 清洗完成 · 下方查看${withRegion ? "(框内)" : ""}`) + if (withRegion) { setCropMode(false); setRegion(null) } } catch (e) { toast.error("清洗失败:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -98,6 +99,37 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } } + // 画框 mouse handlers — 坐标基于 img wrapper 相对位置 + const getRelXY = (clientX: number, clientY: number) => { + const el = imgWrapRef.current + if (!el) return null + const r = el.getBoundingClientRect() + return { + x: Math.max(0, Math.min(1, (clientX - r.left) / r.width)), + y: Math.max(0, Math.min(1, (clientY - r.top) / r.height)), + } + } + const onCropMouseDown = (e: React.MouseEvent) => { + if (!cropMode) return + e.preventDefault() + const p = getRelXY(e.clientX, e.clientY) + if (!p) return + setDragStart(p) + setRegion({ 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({ + 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 handleApplyCleaned = async () => { setApplying(true) try { @@ -211,19 +243,86 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
{/* 左侧大图区 */}
- {/* 上方:主图(已应用清洗 → 显示 "已替换"角标;否则显示原图) */} -
+ {/* 上方:主图 + 画框 overlay */} +
{`frame -
+
{f.cleaned_applied ? "✨ 已替换为清洗版" : "原图"}
+ + {/* 画框 overlay */} + {cropMode && region && region.w > 0 && region.h > 0 && ( + <> + {/* 选区外暗化 — 用 4 个半透 div 围出 */} +
+
+
+
+ {/* 选区高亮边框 */} +
+ + )} + + {/* 画框模式提示 */} + {cropMode && ( +
+ {region && region.w > 0 ? `选区:${(region.w * 100).toFixed(0)}% × ${(region.h * 100).toFixed(0)}%` : "在图上拖动鼠标 → 框选清洗范围"} +
+ )}
+ {/* 画框工具栏 */} + {cropMode ? ( +
+ + +
+ ) : ( + + )} + {/* 下方:清洗版(有待应用版本时显示) */} {hasCleaned && cleanedSrc && (
@@ -252,10 +351,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
)} - {/* 清洗按钮 */} + {/* 清洗按钮(全图) */}