auto-save 2026-05-13 19:34 (~4)

This commit is contained in:
2026-05-13 19:34:17 +08:00
parent 4da7fa8f2f
commit 1ea6f0d850
4 changed files with 93 additions and 7 deletions

View File

@@ -2202,6 +2202,19 @@
"message": "auto-save 2026-05-13 19:23 (~4)",
"hash": "1f9c094",
"files_changed": 4
},
{
"ts": "2026-05-13T19:28:47+08:00",
"type": "commit",
"message": "auto-save 2026-05-13 19:28 (~4)",
"hash": "4da7fa8",
"files_changed": 4
},
{
"ts": "2026-05-13T11:29:29Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-13 19:28 (~4)",
"files_changed": 1
}
]
}

View File

@@ -830,6 +830,17 @@ api/main.py
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-13 · 关键帧详情支持右下角拖拽缩放和上层钉住</h3>
<span class="tag orange">KeyframePanelNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>只有按钮缩放不够直观;钉住后仍作为画布节点,会继续随 ReactFlow 画布缩放。</p>
<p><strong>改动:</strong>增加右下角拖拽缩放手柄;钉住时通过 portal 固定到浏览器上层,脱离 ReactFlow 画布缩放和平移。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code><code>web/app/page.tsx</code>;未钉住时仍是画布节点,钉住后保持屏幕固定位置。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 关键帧详情面板增加钉住按钮</h3>

View File

@@ -174,7 +174,7 @@ export default function Home() {
}, [])
const handleFramePanelScaleChange = useCallback((scale: number) => {
setFramePanelScale(Math.max(0.75, Math.min(1.35, Number(scale.toFixed(2)))))
setFramePanelScale(Math.max(0.65, Math.min(1.6, Number(scale.toFixed(2)))))
}, [])
const handleDeleteFrame = useCallback(async (idx: number) => {

View File

@@ -1,9 +1,10 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { type NodeProps } from "@xyflow/react"
import { createPortal } from "react-dom"
import { type NodeProps, useReactFlow } from "@xyflow/react"
import {
Link2, Upload, Download, Scissors, Image as ImageIcon,
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin,
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin, Maximize2,
} from "lucide-react"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
import { type Job, type ImageRef, effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl } from "@/lib/api"
@@ -513,6 +514,9 @@ export function KeyframeNode({ data, selected }: any) {
============================================================ */
export function KeyframePanelNode({ data }: any) {
const d: NodeData = data
const { getZoom } = useReactFlow()
const panelRef = useRef<HTMLDivElement>(null)
const [pinRect, setPinRect] = useState<{ left: number; top: number }>({ left: 24, top: 72 })
if (!d.job || d.expandedFrame === null) return null
const active = d.job.frames.find((f) => f.index === d.expandedFrame)
const scale = d.framePanelScale ?? 1
@@ -521,12 +525,48 @@ export function KeyframePanelNode({ data }: any) {
const panelHeight = Math.round(746 * scale)
const bodyHeight = Math.max(520, panelHeight - 27)
const setScale = (next: number) => {
const clamped = Math.max(0.75, Math.min(1.35, Number(next.toFixed(2))))
const clamped = Math.max(0.65, Math.min(1.6, Number(next.toFixed(2))))
d.onFramePanelScaleChange?.(clamped)
}
return (
const togglePinned = () => {
if (!pinned) {
const rect = panelRef.current?.getBoundingClientRect()
if (rect) {
setPinRect({
left: Math.max(12, Math.min(window.innerWidth - Math.min(rect.width, window.innerWidth - 24), rect.left)),
top: Math.max(12, Math.min(window.innerHeight - 80, rect.top)),
})
}
}
d.onFramePanelPinnedChange?.(!pinned)
}
const startResize = (e: React.PointerEvent) => {
e.preventDefault()
e.stopPropagation()
const startX = e.clientX
const startY = e.clientY
const startScale = scale
const zoom = pinned ? 1 : getZoom()
const onMove = (ev: PointerEvent) => {
const dx = (ev.clientX - startX) / zoom
const dy = (ev.clientY - startY) / zoom
const delta = Math.abs(dx) > Math.abs(dy) ? dx / 760 : dy / 746
setScale(startScale + delta)
}
const onUp = () => {
window.removeEventListener("pointermove", onMove)
window.removeEventListener("pointerup", onUp)
}
window.addEventListener("pointermove", onMove)
window.addEventListener("pointerup", onUp)
}
const panel = (
<div
className="rounded-2xl border border-white/15 bg-black/70 shadow-2xl overflow-hidden"
ref={panelRef}
className="relative rounded-2xl border border-white/15 bg-black/70 shadow-2xl overflow-hidden"
style={{ width: panelWidth, height: panelHeight, boxShadow: "0 30px 80px -20px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.05)" }}
>
<div className={`keyframe-panel-drag flex h-7 items-center justify-between bg-gradient-to-r from-orange-500 to-red-500 px-3 text-white ${pinned ? "cursor-default" : "cursor-move"}`}>
@@ -543,7 +583,7 @@ export function KeyframePanelNode({ data }: any) {
</span>
<button
type="button"
onClick={(e) => { e.stopPropagation(); d.onFramePanelPinnedChange?.(!pinned) }}
onClick={(e) => { e.stopPropagation(); togglePinned() }}
className={`nodrag h-5 w-5 rounded inline-flex items-center justify-center transition ${
pinned
? "bg-white text-orange-600 shadow"
@@ -601,8 +641,30 @@ export function KeyframePanelNode({ data }: any) {
onCopyImage={d.onCopyImage}
/>
</div>
<button
type="button"
onPointerDown={startResize}
className="nodrag absolute bottom-0 right-0 z-[5] h-7 w-7 cursor-nwse-resize rounded-tl-md bg-white/10 text-white/65 hover:bg-orange-400/35 hover:text-white inline-flex items-center justify-center"
title="拖动右下角缩放面板"
>
<Maximize2 className="h-3.5 w-3.5" />
</button>
</div>
)
if (pinned && typeof document !== "undefined") {
return createPortal(
<div
className="fixed z-[240]"
style={{ left: pinRect.left, top: pinRect.top }}
>
{panel}
</div>,
document.body,
)
}
return panel
}
/* ============================================================