From 1ea6f0d8505da4eb974cfaf6bef35bdb9a2ad14f Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 19:34:17 +0800 Subject: [PATCH] auto-save 2026-05-13 19:34 (~4) --- .memory/worklog.json | 13 ++++++ docs/source-analysis.html | 11 +++++ web/app/page.tsx | 2 +- web/components/nodes/index.tsx | 74 +++++++++++++++++++++++++++++++--- 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index ab4c670..aa21d92 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -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 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index dbf5e69..2502d62 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -830,6 +830,17 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-13 · 关键帧详情支持右下角拖拽缩放和上层钉住

+ KeyframePanelNode +
+
+

问题:只有按钮缩放不够直观;钉住后仍作为画布节点,会继续随 ReactFlow 画布缩放。

+

改动:增加右下角拖拽缩放手柄;钉住时通过 portal 固定到浏览器上层,脱离 ReactFlow 画布缩放和平移。

+

影响:web/components/nodes/index.tsxweb/app/page.tsx;未钉住时仍是画布节点,钉住后保持屏幕固定位置。

+
+

2026-05-13 · 关键帧详情面板增加钉住按钮

diff --git a/web/app/page.tsx b/web/app/page.tsx index 12dabd0..ac7b62a 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -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) => { diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 29c267d..5b223cf 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -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(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 = (
@@ -543,7 +583,7 @@ export function KeyframePanelNode({ data }: any) {
+
) + + if (pinned && typeof document !== "undefined") { + return createPortal( +
+ {panel} +
, + document.body, + ) + } + + return panel } /* ============================================================