auto-save 2026-05-13 11:00 (~2)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user