auto-save 2026-05-13 11:00 (~2)

This commit is contained in:
2026-05-13 11:01:06 +08:00
parent 40deb81b71
commit 08d7cb470c
2 changed files with 122 additions and 10 deletions

View File

@@ -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
}
]
}

View File

@@ -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
<div className="flex gap-3 p-3 overflow-hidden flex-1 min-h-0">
{/* 左侧大图区 */}
<div className="flex flex-col items-stretch gap-2 flex-shrink-0" style={{ width: 320 }}>
{/* 上方:主图(已应用清洗 → 显示 "已替换"角标;否则显示原图) */}
<div className="relative">
{/* 上方:主图 + 画框 overlay */}
<div
ref={imgWrapRef}
className={`relative ${cropMode ? "cursor-crosshair select-none" : ""}`}
onMouseDown={onCropMouseDown}
onMouseMove={onCropMouseMove}
onMouseUp={onCropMouseUp}
onMouseLeave={onCropMouseUp}
>
<img
src={mainSrc}
alt={`frame ${f.index}`}
className="rounded-lg object-contain w-full"
className="rounded-lg object-contain w-full pointer-events-none"
style={{ maxHeight: hasCleaned ? "38vh" : "62vh" }}
draggable={false}
/>
<div className="absolute top-2 left-2 text-[9.5px] px-1.5 py-0.5 rounded backdrop-blur bg-black/50 text-white/80">
<div className="absolute top-2 left-2 text-[9.5px] px-1.5 py-0.5 rounded backdrop-blur bg-black/50 text-white/80 pointer-events-none">
{f.cleaned_applied ? "✨ 已替换为清洗版" : "原图"}
</div>
{/* 画框 overlay */}
{cropMode && region && region.w > 0 && region.h > 0 && (
<>
{/* 选区外暗化 — 用 4 个半透 div 围出 */}
<div className="absolute pointer-events-none bg-black/55" style={{ left: 0, top: 0, right: 0, height: `${region.y * 100}%` }} />
<div className="absolute pointer-events-none bg-black/55" style={{ left: 0, top: `${(region.y + region.h) * 100}%`, right: 0, bottom: 0 }} />
<div className="absolute pointer-events-none bg-black/55" style={{ left: 0, top: `${region.y * 100}%`, width: `${region.x * 100}%`, height: `${region.h * 100}%` }} />
<div className="absolute pointer-events-none bg-black/55" style={{ left: `${(region.x + region.w) * 100}%`, top: `${region.y * 100}%`, right: 0, height: `${region.h * 100}%` }} />
{/* 选区高亮边框 */}
<div
className="absolute pointer-events-none border-2 border-cyan-300 shadow-[0_0_0_1px_rgba(0,0,0,0.4)]"
style={{
left: `${region.x * 100}%`,
top: `${region.y * 100}%`,
width: `${region.w * 100}%`,
height: `${region.h * 100}%`,
}}
/>
</>
)}
{/* 画框模式提示 */}
{cropMode && (
<div className="absolute bottom-2 left-2 right-2 text-[10px] px-2 py-1 rounded backdrop-blur bg-cyan-500/85 text-white text-center pointer-events-none font-medium">
{region && region.w > 0 ? `选区:${(region.w * 100).toFixed(0)}% × ${(region.h * 100).toFixed(0)}%` : "在图上拖动鼠标 → 框选清洗范围"}
</div>
)}
</div>
{/* 画框工具栏 */}
{cropMode ? (
<div className="flex items-center gap-1.5">
<button
onClick={() => handleCleanup(true)}
disabled={cleaning || !region || region.w < 0.03 || region.h < 0.03}
className="flex-1 px-2 py-1.5 rounded-md text-[11px] font-medium inline-flex items-center justify-center gap-1 transition bg-cyan-500 hover:bg-cyan-400 text-white disabled:opacity-40 disabled:cursor-not-allowed"
title="只清洗框内"
>
{cleaning ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkle className="h-3 w-3" />}
{cleaning ? "清洗框内…" : "✓ 清洗框内"}
</button>
<button
onClick={() => { setCropMode(false); setRegion(null); setDragStart(null) }}
className="px-2 py-1.5 rounded-md text-[11px] bg-white/10 hover:bg-white/20 text-white"
title="取消画框"
>
<X className="h-3 w-3" />
</button>
</div>
) : (
<button
onClick={() => { setCropMode(true); setRegion(null) }}
className="w-full px-3 py-1.5 rounded-md text-[10.5px] font-medium inline-flex items-center justify-center gap-1.5 transition bg-white/[0.06] hover:bg-cyan-500/30 border border-white/15 hover:border-cyan-300/50 text-white/80 hover:text-white"
title="拖框限定清洗范围(推荐用于精确去掉某个角落的水印)"
>
<Crop className="h-3 w-3" />
📐
</button>
)}
{/* 下方:清洗版(有待应用版本时显示) */}
{hasCleaned && cleanedSrc && (
<div className="rounded-lg border border-emerald-400/40 bg-emerald-500/5 p-2 space-y-1.5">
@@ -252,10 +351,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
</div>
)}
{/* 清洗按钮 */}
{/* 清洗按钮(全图) */}
<button
onClick={handleCleanup}
disabled={cleaning}
onClick={() => handleCleanup(false)}
disabled={cleaning || cropMode}
className="w-full px-3 py-1.5 rounded-md text-[11.5px] font-medium inline-flex items-center justify-center gap-1.5 transition bg-gradient-to-r from-cyan-500/80 to-emerald-500/80 hover:from-cyan-500 hover:to-emerald-500 text-white disabled:opacity-40 disabled:cursor-not-allowed"
title="清掉水印 / @用户名 / 字幕 / 平台 logo"
>