feat(canvas): 新增画布设置入口、区域选择滚轮平移与提交流程快捷键
Some checks failed
Publish container images / Quality checks (push) Has been cancelled
Publish container images / Resolve build version (push) Has been cancelled
Publish container images / Build open-ai-canvas-backend (amd64) (push) Has been cancelled
Publish container images / Build open-ai-canvas-backend (arm64) (push) Has been cancelled
Publish container images / Build open-ai-canvas-web (multi) (push) Has been cancelled
Publish container images / Publish open-ai-canvas-backend manifest (push) Has been cancelled
Publish container images / Publish open-ai-canvas-web manifest (push) Has been cancelled
Publish container images / Publish release assets (push) Has been cancelled

This commit is contained in:
2026-09-22 13:17:56 +08:00
parent 3c25511f47
commit c8cde54c41
6 changed files with 40 additions and 5 deletions

View File

@@ -2589,3 +2589,7 @@ SSE 现在按 revision 变化发送紧凑快照,无变化只 heartbeat;客
入口精简与工具草稿复测:移除左侧工具菜单及图片节点提示词模板入口,保留图片风格 / 预设和其他节点模板入口。工具引用列表读取草稿改用空值判断,明确清空时不再从原始生成提示词回读旧标签。风格和预设的选择、替换、连续编辑、标签移除、全文清空及保存重载覆盖有图/空节点两种情况;5 个专项文件共 58 项通过,TypeScript 类型检查通过。实际页面仍需确认普通/放大编辑器均无图片提示词模板入口,以及风格和预设清空后重新聚焦不回填。
主分支提交前复测:前端 `bun run test` 全量 1925 项通过,生产构建、类型检查、UI 退场 lint 通过;构建仍有大分块提示。同步了技能/工具标签结构与多行连接菜单的源码守护断言。后端 `go test ./...` 全量通过,工具、迁移、资源引用、handler 及工具预览权限/项目封面专项通过。本轮未执行真实模型调用、PostgreSQL 实库迁移或登录态浏览器验收,这些项目继续保留待验收。
画布页本轮改动:区域选择(底部工具栏胶囊切到「区域选择」)下滚轮改为上下平移画布,缩放保留给 Command/Ctrl + 滚轮(含触控板捏合);带 `data-canvas-wheel-scroll` 的节点内部区域仍保持自身滚动。顶栏分享按钮右侧新增「账户与设置」按钮,点击复用画布内「去设置」的 `navigateToSettings({ section: "channels", continueCreation: true })`,进入 `/settings?section=channels&continue=1`(顶部有「返回创作 / 保存并返回」,与模型缺失时的跳转一致,不新增全局配置弹窗)。曾尝试在画布内用抽屉内嵌设置页,因嵌套 Router 报错已移除。类型检查通过;待登录态浏览器验收:区域选择的滚轮方向与实际滚动手感,设置按钮跳转后返回画布时画布内容与滚动位置是否保持,以及设置项保存后画布生成默认值立即生效。
节点面板快捷键:`[data-canvas-node-panel]` 面板内任意位置按 ⌘/Ctrl + Enter 触发面板主按钮(目前只有创作面板的生成按钮带 `data-canvas-panel-submit`),生成中或提示词为空时按钮禁用,快捷键不拦截按键。待验收:文本框内按下不插入换行、输入法组合中不触发、面板内其他输入框与配置面板不被误触发。

View File

@@ -387,6 +387,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
<Button
type="text"
className={`canvas-node-composer-submit canvas-node-composer-submit-canvas ${showCost ? "has-cost" : ""}`}
data-canvas-panel-submit
disabled={isRunning || isSubmitDisabled}
style={
{

View File

@@ -1,5 +1,5 @@
import { motion, useReducedMotion } from "motion/react";
import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from "react";
import { useLayoutEffect, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type ReactNode, type RefObject } from "react";
import { Clapperboard, Image as ImageIcon, List, Music2, Pencil, Table2, Video, WandSparkles, Workflow as WorkflowIcon } from "lucide-react";
import { useCanvasOverlayLayer } from "@/components/canvas/canvas-overlay-layer";
@@ -160,6 +160,19 @@ export function CanvasNodePanelOverlay({
};
}, [containerRef, dragOffset?.x, dragOffset?.y, isDragging, node.height, node.id, node.position.x, node.position.y, node.width, panelHeight, panelWidth, viewport]);
// 面板内任意位置按下 ⌘/Ctrl + Enter 时直接提交,避免先把手移回输入框或生成按钮。
// 只有面板自己声明了主按钮([data-canvas-panel-submit])才生效,且按钮禁用时不拦截按键。
const handleSubmitShortcut = (event: ReactKeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Enter" || event.shiftKey || event.altKey) return;
if (!(event.ctrlKey || event.metaKey)) return;
if (event.nativeEvent.isComposing) return;
const submit = panelRef.current?.querySelector<HTMLButtonElement>("[data-canvas-panel-submit]");
if (!submit || submit.disabled || submit.getAttribute("aria-disabled") === "true") return;
event.preventDefault();
event.stopPropagation();
submit.click();
};
return (
<div
ref={panelRef}
@@ -167,6 +180,7 @@ export function CanvasNodePanelOverlay({
data-canvas-node-panel
className={`thin-scrollbar absolute max-w-[calc(100%_-_24px)] ${allowOverflow ? "overflow-visible" : "overflow-y-auto"}`}
style={{ left: 0, top: 0, transform: `translate3d(${initialPosition.left}px, ${initialPosition.top}px, 0)`, width: initialWidth, maxHeight: allowOverflow ? "none" : "calc(100% - 84px)", zIndex }}
onKeyDownCapture={handleSubmitShortcut}
onMouseDown={(event) => event.stopPropagation()}
onPointerDownCapture={bringToFront}
onFocusCapture={bringToFront}

View File

@@ -198,6 +198,18 @@ export function InfiniteCanvas({ interactive = true, containerRef, viewport, app
event.preventDefault();
interactingRef.current = true;
const current = viewportRef.current;
// 区域选择模式下滚轮用于上下浏览画布,缩放保留给 Ctrl/Meta + 滚轮(触控板捏合)。
if (boxSelectEnabled && !isPinchZoom) {
const shiftHorizontal = event.shiftKey && absX < 1;
scheduleViewportChange({
x: current.x - (shiftHorizontal ? deltaY : deltaX),
y: current.y - (shiftHorizontal ? 0 : deltaY),
k: current.k,
}, true);
return;
}
const rawAbsY = Math.abs(event.deltaY);
const looksLikeMouseWheel = event.deltaMode !== 0 || (rawAbsY >= 80 && Math.abs(rawAbsY - Math.round(rawAbsY / 100) * 100) < 1);
const looksLikeTrackpadPan = !isPinchZoom && (event.shiftKey || absX > 0 || (!looksLikeMouseWheel && absY > 0));
@@ -228,7 +240,7 @@ export function InfiniteCanvas({ interactive = true, containerRef, viewport, app
k: newScale,
}, true);
},
[containerRef, scheduleViewportChange],
[boxSelectEnabled, containerRef, scheduleViewportChange],
);
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {

View File

@@ -109,8 +109,8 @@ export const CANVAS_SHORTCUTS: CanvasShortcutItem[] = [
id: "box-select-tool",
category: "selection",
title: "使用区域选择工具",
description: "底部工具栏的胶囊开关切到区域选择后,空白处拖动可连续框选",
keys: [["区域选择", "拖动"]],
description: "底部工具栏的胶囊开关切到区域选择后,空白处拖动可连续框选;滚轮上下移动画布,缩放请用 Command/Ctrl + 滚轮",
keys: [["区域选择", "拖动"], ["区域选择", "滚轮"]],
keywords: ["工具栏", "多选", "selection"],
},
{

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { Link } from "react-router";
import { Clapperboard, CloudDownload, CloudUpload, CopyPlus, Focus, FolderKanban, Gauge, History, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Share2, Trash2, Undo2, Upload } from "lucide-react";
import { Clapperboard, CloudDownload, CloudUpload, CopyPlus, Focus, FolderKanban, Gauge, History, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Settings, Share2, Trash2, Undo2, Upload } from "lucide-react";
import { Button, Dropdown, Tooltip } from "antd";
import { WorkspaceCreditGiftMark } from "@/components/layout/workspace-credit-gift-mark";
@@ -10,6 +10,7 @@ import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import type { CanvasContextSummary } from "@/lib/canvas/canvas-context-summary";
import type { CanvasShortDramaProgress } from "@/lib/canvas/canvas-short-drama";
import { canvasThemes } from "@/lib/canvas-theme";
import { navigateToSettings } from "@/lib/settings-navigation";
import { useCanvasThemeStore } from "@/stores/canvas/use-canvas-theme-store";
import { useUserStore } from "@/stores/use-user-store";
import type { CanvasMediaPerformanceMode } from "@/types/canvas";
@@ -288,6 +289,9 @@ export function CanvasTopBar({
<CanvasTopBarTooltip label="分享画布">
<Button type="text" className="canvas-topbar-action !h-10 !w-10 !min-w-10 !rounded-xl !p-0" style={{ color: theme.node.text }} icon={<Share2 className="size-4" />} onClick={onShare} aria-label="分享画布" />
</CanvasTopBarTooltip>
<CanvasTopBarTooltip label="账户与设置">
<Button type="text" className="canvas-topbar-action !h-10 !w-10 !min-w-10 !rounded-xl !p-0" style={{ color: theme.node.text }} icon={<Settings className="size-4" />} onClick={() => navigateToSettings({ section: "channels", continueCreation: true })} aria-label="账户与设置" />
</CanvasTopBarTooltip>
</div>
</div>
<CanvasShortcutsModal open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />