From 030541b99d2a3bbfca51a1bb5c4a974172515da7 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Mon, 25 May 2026 15:51:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(canvas):=20=E6=B7=BB=E5=8A=A0=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=9B=BE=E7=89=87=E4=BF=A1=E6=81=AF=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在画布项目状态中添加 showImageInfo 属性 - 实现图片信息栏组件显示图片尺寸和文件大小 - 添加画布工具栏开关控制图片信息显示 - 将图片信息状态集成到项目历史记录和持久化 - 更新画布节点组件支持图片信息显示功能 --- CHANGELOG.md | 17 ++++------------- docs/pending-test.md | 17 ----------------- .../(user)/canvas/[id]/canvas-client-page.tsx | 19 ++++++++++++++----- .../(user)/canvas/components/canvas-node.tsx | 19 +++++++++++++++++++ .../canvas/components/canvas-toolbar.tsx | 15 +++++++++++++-- .../(user)/canvas/stores/use-canvas-store.ts | 5 ++++- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e631c..42c4875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,10 @@ ## Unreleased -+ [新增] 新增用户账号与算力点体系,支持账号密码注册登录、Linux.do OAuth 登录、用户管理、算力点调整和算力点日志。 -+ [新增] 后端模型渠道调用支持按模型算力点计费,生图、图生图、文本对话和视频接口会在请求前预扣算力点,上游失败时自动返还。 -+ [新增] 管理后台公开配置支持设置模型算力点、默认图片模型、默认视频模型和默认文本模型。 -+ [新增] 画布右上角展示当前用户算力点余额,节点发送按钮、右侧助手和生成配置节点会展示本次预计消耗算力点。 -+ [新增] 新增视频生成节点,支持本地直连和云端渠道的视频生成、轮询、下载、保存到素材和参考图生成视频。 -+ [优化] 画布模型选择器统一为共享下拉组件,云端模式使用后台公开配置的可用模型,并按模型名称展示常见厂商图标。 -+ [优化] 画布图片和视频节点会按素材或生成结果的真实宽高比调整尺寸,空节点切换比例时也会同步更新外框。 -+ [优化] 画布生图设置弹层改为紧凑面板,模型选择保留在底部工具栏,尺寸、比例、质量和生成数量配置更集中。 -+ [优化] 管理后台渠道模型列表获取和模型测试改为走后端接口,不再由前端直接读取或转发 API Key。 -+ [优化] 管理后台渠道可用模型选择器支持单行省略展示,临时移除模型后仍保留候选,方便重新添加。 -+ [优化] 后台提示词定时同步默认开启并默认每 5 分钟执行一次。 -+ [修复] 云端默认模型为空时前端会回落到第一个系统可用模型,避免模型下拉为空或误弹配置弹窗。 -+ [修复] 模型算力点配置输入不再因失焦回跳为 0,系统可用模型临时删除时会保留历史算力点配置。 ++ [新增] 新增用户账号与算力点体系,支持账号密码注册登录、Linux.do OAuth。 ++ [新增] 管理后台公开配置支持设置模型算力点、支持计费查询。 ++ [新增] 画布右上角展示用户算力点余额,生成按钮会展示本次预计消耗算力点。 ++ [新增] 新增视频生成节点。 ## v0.0.7 - 2026-05-23 diff --git a/docs/pending-test.md b/docs/pending-test.md index e81670e..bcab4b0 100644 --- a/docs/pending-test.md +++ b/docs/pending-test.md @@ -1,18 +1 @@ # 待测试 - -- 用户模块新增 `users` 完整字段和 `credit_logs` 表;账号密码注册/登录已开放,后端 `/api/v1` 模型渠道接口需要登录后调用;Linux.do OAuth 登录参数移到后台系统设置,登录页按开关展示 Linux.do 按钮并使用 `public/icons/linuxdo.svg` 图标,Linux.do 原始资料写入 `users.extra.linuxDo`;后台 `/admin/users` 可新增、编辑、删除用户并在编辑弹窗内调整算力点,编辑用户不修改头像和算力点,算力点变化会写入 `credit_logs`;后台新增 `/admin/credit-logs` 算力点日志表格和增删改接口,需要确认注册、登录、Linux.do 配置保存和回调、后台筛选分页、用户保存删除、算力点调整、日志增删改查、登录态下生图/对话/视频和算力点流水都正常。 -- 管理后台 `/admin/settings` 公开配置新增模型算力点配置;后端 `/api/v1` 生图、图生图、对话和视频创建接口会在请求上游前按模型预扣用户算力点并写入 `credit_logs` 的 `ai_consume` 记录,上游失败时写入 `ai_refund` 返还记录,余额不足会拒绝调用,前端远程调用成功后会刷新当前用户信息;系统可用模型临时删除时会保留 JSON 里的历史模型算力点配置,渠道里临时移除模型时公开配置下拉仍保留已见过的模型候选,云端默认模型为空时前端会回落到第一个系统可用模型,云端模型下拉为空时不会弹出配置弹窗,需要确认不同模型扣费、余额不足拦截、失败返还、流水记录、顶部余额更新、算力点输入不回跳、临时删除模型后配置保留和可重新添加正常。 -- 画布详情页右上角用户头像左侧新增当前算力点余额展示,未登录或无用户信息时显示 0,需要确认登录、未登录和调用后刷新余额显示正常。 -- 画布节点下方对话框、右侧助手发送按钮和生成配置节点开始生成按钮新增本次预计消耗算力点展示,费用来自公开配置的模型算力点,未配置显示 0,生图按生成张数倍乘;画布里的模型下拉在云端模式下会使用后台公开配置返回的系统可用模型,需要确认文本、生图、图生图、生成配置节点和本地直连模式显示正常。 -- 配置弹窗和后台公开配置新增默认视频模型;视频节点通过设置弹层单独配置视频质量、尺寸和秒数,连接图片节点时会作为 `input_reference` 参考图生成视频,并按 OpenAI 视频接口格式提交 `multipart/form-data`、`size`、`seconds`、`vquality` 和参考图参数,需要确认本地直连和云端渠道下默认视频模型选择、节点视频参数配置、参考图生成、保存和重试都正常。 -- 画布新增视频节点:工具栏可新建视频节点,上传/拖拽本地视频后可在节点内播放、下载、保存到我的素材;节点下方对话框和生成配置节点支持视频生成,前端会调用 OpenAI 兼容 `videos` 接口并把生成结果保存为本地视频 Blob;后端远程渠道新增 `/api/v1/videos`、`/api/v1/videos/:id`、`/api/v1/videos/:id/content` 代理,需要确认本地渠道和远程渠道都能生成、轮询和下载视频。 -- 图片和视频节点会按素材或生成结果的真实宽高比调整节点尺寸;空图片/视频节点在尺寸设置切换到 16:9、9:16 等比例时也会同步调整节点外框,需要确认上传、生成、重试、素材插入和空节点尺寸切换后的黑边情况。 -- 模型选择器改为共享的 shadcn 风格下拉,按模型名称显示 OpenAI、Claude、Gemini 图标,并把画布节点下方、右侧助手和配置弹窗统一到同一套组件;右侧助手的模型和模式按钮会随工具栏宽度自动在文字态和图标态之间切换,需要确认生图页、画布助手、节点配置和配置弹窗中的选中态和下拉列表显示正常。 -- 画布图像设置里的尺寸输入去掉了原生数字步进箭头,需要确认宽高仍可直接输入、禁用态正常、设置值回写正常。 -- 后台提示词定时同步默认改为开启且默认每 5 分钟执行一次,需要确认新环境未保存过设置时开关默认打开、Cron 默认值正确,用户手动关闭或修改 Cron 后能保持配置。 -- 画布右侧助手和节点下方对话框的生图设置弹层改为紧凑图像设置面板,面板只包含质量、尺寸、宽高比和生成数量选择,模型选择保留在对话框底部工具栏;需要确认助手内设置面板不会被右侧面板裁切,节点内质量可正常选择并参与生成、尺寸可编辑、宽高比预览显示正常且为直角矩形、auto 不重复显示、张数每行 4 个且自定义输入框能显示当前值、实际生图参数正常;节点设置面板打开时会隐藏节点悬浮工具条,避免遮挡质量选择。 -- 管理后台 `/admin/prompts` 新增提示词批量删除,需要确认多选、确认弹窗、删除后列表刷新和筛选条件下删除行为。 -- 管理后台 `/admin/settings` 的私有配置新增提示词定时同步开关和 Cron 表达式;开启后后端会按配置同步内置 GitHub 远程提示词源,需要确认保存配置和到点同步行为。 -- 管理后台 `/admin/settings` 中的渠道模型列表获取和模型测试已改为走后端接口,前端不再直接读取或转发 API Key,需要确认保存后、编辑中和已有渠道三种场景都能正常拉取模型和测试连通性。 -- 管理后台 `/admin/settings` 编辑渠道抽屉里的渠道可用模型选择器改为单行响应式展示,模型过多时用省略标签收起;从当前渠道移除某个模型后,该模型仍会保留在抽屉下拉候选里,方便重新添加,需要确认不会撑高输入框、右侧获取模型列表按钮对齐和移除后可重新选择正常。 -- 提示词远程源新增 `davidwuw0811-boop/awesome-gpt-image2-prompts`,同步时会读取仓库 `prompts.json` 的结构化数据,需要确认后台同步后分类、封面图、标签和提示词内容展示正常。 diff --git a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx index ae66570..a58a7f3 100644 --- a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx +++ b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx @@ -66,6 +66,7 @@ type CanvasHistoryEntry = Pick & { chatSessions: CanvasAssistantSession[]; activeChatId: string | null; backgroundMode: CanvasBackgroundMode; + showImageInfo: boolean; }; const VIDEO_NODE_MAX_WIDTH = 420; @@ -246,6 +247,7 @@ function InfiniteCanvasPage() { const [runningNodeId, setRunningNodeId] = useState(null); const [isMiniMapOpen, setIsMiniMapOpen] = useState(false); const [backgroundMode, setBackgroundMode] = useState("lines"); + const [showImageInfo, setShowImageInfo] = useState(false); const [clearConfirmOpen, setClearConfirmOpen] = useState(false); const [assetPickerOpen, setAssetPickerOpen] = useState(false); const [assetPickerTab, setAssetPickerTab] = useState("my-assets"); @@ -284,8 +286,9 @@ function InfiniteCanvasPage() { chatSessions, activeChatId, backgroundMode, + showImageInfo, }), - [activeChatId, backgroundMode, chatSessions], + [activeChatId, backgroundMode, chatSessions, showImageInfo], ); useEffect(() => { @@ -305,6 +308,7 @@ function InfiniteCanvasPage() { setChatSessions(restoredSessions); setActiveChatId(project.activeChatId || null); setBackgroundMode(project.backgroundMode); + setShowImageInfo(project.showImageInfo || false); setViewport(project.viewport); historyRef.current = { past: [], future: [] }; if (historyCommitTimerRef.current) { @@ -317,6 +321,7 @@ function InfiniteCanvasPage() { chatSessions: restoredSessions, activeChatId: project.activeChatId || null, backgroundMode: project.backgroundMode, + showImageInfo: project.showImageInfo || false, }; setHistoryState({ canUndo: false, canRedo: false }); setProjectLoaded(true); @@ -328,7 +333,7 @@ function InfiniteCanvasPage() { if (!projectLoaded || applyingHistoryRef.current || historyPausedRef.current) return; const next = createHistoryEntry(); const previous = lastHistoryRef.current; - if (previous?.nodes === next.nodes && previous.connections === next.connections && previous.chatSessions === next.chatSessions && previous.activeChatId === next.activeChatId && previous.backgroundMode === next.backgroundMode) return; + if (previous?.nodes === next.nodes && previous.connections === next.connections && previous.chatSessions === next.chatSessions && previous.activeChatId === next.activeChatId && previous.backgroundMode === next.backgroundMode && previous.showImageInfo === next.showImageInfo) return; if (historyCommitTimerRef.current) clearTimeout(historyCommitTimerRef.current); historyCommitTimerRef.current = setTimeout(() => { @@ -348,12 +353,12 @@ function InfiniteCanvasPage() { historyCommitTimerRef.current = null; } }; - }, [activeChatId, backgroundMode, chatSessions, connections, createHistoryEntry, nodes, projectLoaded]); + }, [activeChatId, backgroundMode, chatSessions, connections, createHistoryEntry, nodes, projectLoaded, showImageInfo]); useEffect(() => { if (!projectLoaded || historyPausedRef.current) return; - updateProject(projectId, { nodes, connections, chatSessions, activeChatId, backgroundMode }); - }, [activeChatId, backgroundMode, chatSessions, connections, nodes, projectId, projectLoaded, updateProject]); + updateProject(projectId, { nodes, connections, chatSessions, activeChatId, backgroundMode, showImageInfo }); + }, [activeChatId, backgroundMode, chatSessions, connections, nodes, projectId, projectLoaded, showImageInfo, updateProject]); useEffect(() => { if (!dialogNodeId) setNodeImageSettingsOpen(false); @@ -795,6 +800,7 @@ function InfiniteCanvasPage() { setChatSessions(entry.chatSessions); setActiveChatId(entry.activeChatId); setBackgroundMode(entry.backgroundMode); + setShowImageInfo(entry.showImageInfo); setSelectedNodeIds(new Set()); setSelectedConnectionId(null); setContextMenu(null); @@ -2085,6 +2091,7 @@ function InfiniteCanvasPage() { batchOpening={openingBatchIds.has(node.id)} batchRecovering={collapsingBatchIds.has(node.id)} batchMotion={batchMotionById.get(node.id)} + showImageInfo={showImageInfo} renderPanel={(panelNode) => ( createNode(CanvasNodeType.Image)} onAddVideo={() => createNode(CanvasNodeType.Video)} onAddText={() => createNode(CanvasNodeType.Text)} @@ -2191,6 +2199,7 @@ function InfiniteCanvasPage() { onClear={() => setClearConfirmOpen(true)} onDeselect={deselectCanvas} onBackgroundModeChange={setBackgroundMode} + onShowImageInfoChange={setShowImageInfo} onOpenAssetLibrary={() => { setAssetPickerTab("library"); setAssetPickerOpen(true); diff --git a/web/src/app/(user)/canvas/components/canvas-node.tsx b/web/src/app/(user)/canvas/components/canvas-node.tsx index 2bd4f53..6f63b9d 100644 --- a/web/src/app/(user)/canvas/components/canvas-node.tsx +++ b/web/src/app/(user)/canvas/components/canvas-node.tsx @@ -5,6 +5,7 @@ import type { ReactNode } from "react"; import { ChevronRight, Image as ImageIcon, RefreshCw, Star, Video } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; +import { formatBytes } from "@/lib/image-utils"; import { useThemeStore } from "@/stores/use-theme-store"; import { CanvasNodeType, type CanvasNodeData, type Position } from "../types"; @@ -21,6 +22,7 @@ type CanvasNodeProps = { isConnecting: boolean; editRequestNonce?: number; showPanel: boolean; + showImageInfo: boolean; renderPanel?: (node: CanvasNodeData) => ReactNode; renderNodeContent?: (node: CanvasNodeData) => ReactNode; batchCount?: number; @@ -71,6 +73,7 @@ export const CanvasNode = React.memo(function CanvasNode({ isConnecting, editRequestNonce = 0, showPanel, + showImageInfo, renderPanel, renderNodeContent, batchCount = 0, @@ -296,6 +299,8 @@ export const CanvasNode = React.memo(function CanvasNode({ /> + {showImageInfo && hasImageContent ? : null} + {!hasImageContent && !hasVideoContent ?
: null} @@ -537,6 +542,20 @@ function ImageContent({ ); } +function ImageInfoBar({ node }: { node: CanvasNodeData }) { + const width = Math.round(node.metadata?.naturalWidth || node.width); + const height = Math.round(node.metadata?.naturalHeight || node.height); + const size = formatBytes(node.metadata?.bytes || 0); + return ( +
+ + {width} x {height} + {size ? ` · ${size}` : ""} + +
+ ); +} + function BatchFrame({ batchCount, batchExpanded, batchOpening, batchRecovering, onToggleBatch, children }: { batchCount: number; batchExpanded: boolean; batchOpening: boolean; batchRecovering: boolean; onToggleBatch?: () => void; children: ReactNode }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const isBatchRoot = batchCount > 1; diff --git a/web/src/app/(user)/canvas/components/canvas-toolbar.tsx b/web/src/app/(user)/canvas/components/canvas-toolbar.tsx index 4727d72..fe229d8 100644 --- a/web/src/app/(user)/canvas/components/canvas-toolbar.tsx +++ b/web/src/app/(user)/canvas/components/canvas-toolbar.tsx @@ -1,7 +1,7 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode, RefObject } from "react"; import { useRef, useState } from "react"; -import { Button, Segmented } from "antd"; -import { CircleDot, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Library, Moon, Palette, Redo2, Settings2, Square, Sun, Trash2, Type, Undo2, Upload, Video } from "lucide-react"; +import { Button, Segmented, Switch } from "antd"; +import { CircleDot, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Info, Library, Moon, Palette, Redo2, Settings2, Square, Sun, Trash2, Type, Undo2, Upload, Video } from "lucide-react"; import { canvasThemes, type CanvasBackgroundMode, type CanvasColorTheme, type CanvasTheme } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; @@ -12,6 +12,7 @@ export function CanvasToolbar({ canUndo, canRedo, backgroundMode, + showImageInfo, onAddImage, onAddVideo, onAddText, @@ -23,6 +24,7 @@ export function CanvasToolbar({ onClear, onDeselect, onBackgroundModeChange, + onShowImageInfoChange, onOpenAssetLibrary, onOpenMyAssets, }: { @@ -30,6 +32,7 @@ export function CanvasToolbar({ canUndo: boolean; canRedo: boolean; backgroundMode: CanvasBackgroundMode; + showImageInfo: boolean; onAddImage: () => void; onAddVideo: () => void; onAddText: () => void; @@ -41,6 +44,7 @@ export function CanvasToolbar({ onClear: () => void; onDeselect: () => void; onBackgroundModeChange: (mode: CanvasBackgroundMode) => void; + onShowImageInfoChange: (show: boolean) => void; onOpenAssetLibrary: () => void; onOpenMyAssets: () => void; }) { @@ -174,6 +178,13 @@ export function CanvasToolbar({ }, ]} /> +
+ + + 图片信息 + + +
) : null} diff --git a/web/src/app/(user)/canvas/stores/use-canvas-store.ts b/web/src/app/(user)/canvas/stores/use-canvas-store.ts index 1bcaf03..67e0e6a 100644 --- a/web/src/app/(user)/canvas/stores/use-canvas-store.ts +++ b/web/src/app/(user)/canvas/stores/use-canvas-store.ts @@ -16,6 +16,7 @@ export type CanvasProject = { chatSessions: CanvasAssistantSession[]; activeChatId: string | null; backgroundMode: CanvasBackgroundMode; + showImageInfo: boolean; viewport: ViewportTransform; }; @@ -27,7 +28,7 @@ type CanvasStore = { openProject: (id: string) => CanvasProject | null; renameProject: (id: string, title: string) => void; deleteProjects: (ids: string[]) => void; - updateProject: (id: string, patch: Partial>) => void; + updateProject: (id: string, patch: Partial>) => void; }; const initialViewport: ViewportTransform = { x: 0, y: 0, k: 1 }; @@ -75,6 +76,7 @@ export const useCanvasStore = create()( chatSessions: [], activeChatId: null, backgroundMode: "lines", + showImageInfo: false, viewport: initialViewport, }; set((state) => ({ projects: [project, ...state.projects] })); @@ -92,6 +94,7 @@ export const useCanvasStore = create()( chatSessions: source.chatSessions || [], activeChatId: source.activeChatId || null, backgroundMode: source.backgroundMode || "lines", + showImageInfo: source.showImageInfo || false, viewport: source.viewport || initialViewport, }; set((state) => ({ projects: [project, ...state.projects] }));