From 20f6f20c5510cae7eb055600880ab9b83dd4f6f5 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 27 Jun 2026 17:15:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=BB=E5=B8=83=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=BF=A1=E6=81=AF=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为图片信息弹窗增加复制信息按钮 复制内容仅包含当前可见元数据字段 补充图片信息复制回归测试和文档约定 --- .../shared-memory/decision-log.md | 2 +- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 +- .../ImageCanvasMetadataModalView.test.tsx | 50 ++++++++++++ .../ImageCanvasMetadataModalView.tsx | 77 ++++++++++++++++++- src/index.css | 11 +++ 5 files changed, 136 insertions(+), 6 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 7210ddce7..c23510bf6 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2835,7 +2835,7 @@ ## 2026-06-16 图片画布图片信息页不展示生图 Prompt - 背景:图片画布中每张生成图片的信息页原来展示 `Prompt` 和复制 Prompt,但该字段可能是后端组装后的生图提示词,不适合作为用户可见的图片输入信息。 -- 决策:图片信息页删除生图 Prompt 展示和复制入口,改为展示生成时的用户面板输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、修改要求,以及角色规范、常规参考图、图标规范和修改参考图等参考图卡片。旧数据或上传图片没有输入快照时显示 `-`,不得回退展示内部 Prompt。 +- 决策:图片信息页删除生图 Prompt 展示和复制入口,改为展示生成时的用户面板输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、修改要求,以及角色规范、常规参考图、图标规范和修改参考图等参考图卡片,并只允许“复制信息”复制当前可见字段。旧数据或上传图片没有输入快照时显示 `-`,不得回退展示内部 Prompt。 - 影响范围:`src/components/image-editor/ImageCanvasEditorView.tsx`、图片画布 layout snapshot、图片画布技术方案。 - 验证方式:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx` 应覆盖图片信息页无 `Prompt`、无 `复制Prompt`,并展示普通生成、角色生成、图标素材和修改结果的输入快照。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index b4f865dfc..7040aba55 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -18,7 +18,7 @@ - 默认工具为选择模式;底部工具栏采用 AI 画布工作流工具组:选择、抓手、上传、生成图片、生成视频、生成音乐、生成规范、生成角色形象、生成图标素材、生成 UI 设计图。底部栏不再展示文字工具、形状标注工具和导出工具;上传与生成图片之间、生成音乐与生成规范之间各有一个半图标高度分割线。 - 鼠标中键拖拽始终平移画布;长按 Space 临时进入抓手模式,松开后恢复原工具。 - 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层、生成占位框或画板的边缘与中心线;当移动元素接近两个同轴元素形成的等距位置时,支持横向或纵向等距吸附。 -- 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 +- 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中的后端拼接 Prompt、固定生成模板或模型默认提示词。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 - 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑默认从原图模型和分辨率初始化,但提交使用面板当前选择的 `model/aspectRatio/imageSize`;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后走同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片源读取为图片 Data URL;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作快速编辑固定使用 `seedance2.0-fast` 动作 / 视频模型。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 - 快速编辑面板对齐其它生成类面板:首行支持额外参考图,最多 8 张;原图 / 原素材自动作为最后一张隐式参考提交,但不在参考图条里固定展示 `图x`。打开快速编辑时画布必须自动平移缩放,让原素材完整落在可视区上半部分,底部面板固定出现在素材下方且不遮挡内容,竖屏 UI 素材也必须完整展示。快速编辑右侧显示矩形、椭圆、画笔框选工具,但进入时不默认启用;点击工具后显示选中态,再点同一工具取消启用。尺寸和模型默认从原素材配置 / 分辨率推断,底部参数胶囊可点击修改;图片快速编辑左下角统一显示 `x:y·xK`,右下角模型胶囊紧贴生成按钮。提交前将提示词里对原素材的 `原图`、`当前图片`、`当前图` 或 `图1` 引用改写为最后一张源素材编号,例如额外 1 张参考图时写成 `图2`。点击快速编辑生成后立即创建独立 `Quick Edit Generator` 画布占位播放生成中动画,不再在原图图层上播放生成中遮罩;该生成中占位可通过键盘 `Delete` / `Backspace` 删除。 diff --git a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx index cdabe319f..ade7a0bf0 100644 --- a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx +++ b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx @@ -3,9 +3,14 @@ import { fireEvent, render, screen, within } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; +import * as clipboardService from '../../services/clipboard'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView'; +vi.mock('../../services/clipboard', () => ({ + copyTextToClipboard: vi.fn(), +})); + function createLayer(overrides: Partial = {}): CanvasLayer { return { id: 'layer-1', @@ -67,6 +72,51 @@ describe('ImageCanvasMetadataModalView', () => { expect(within(dialog).getByText('generated/object.png')).toBeTruthy(); }); + it('copies the visible metadata snapshot without restoring internal prompt fields', async () => { + vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValueOnce(true); + + render( + , + ); + + const dialog = screen.getByRole('dialog', { name: '图片信息' }); + fireEvent.click(within(dialog).getByRole('button', { name: '复制图片信息' })); + + expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith( + [ + '图片类型:生成图片', + '生成输入:\n生成提示词:清爽游戏按钮\n参考图:角色立绘 (项目资源 · resource-reference)', + 'Model:gpt-image-2', + 'Resolution:1024 x 768 px', + 'Task:123', + 'Object:generated/object.png', + ].join('\n'), + ); + expect(clipboardService.copyTextToClipboard).not.toHaveBeenCalledWith( + expect.stringContaining('内部组装 Prompt 不应复制'), + ); + expect(await within(dialog).findByText('已复制')).toBeTruthy(); + }); + it('renders nanobanana2 instead of the raw Gemini image model id', () => { render( `${field.title}:${field.value}`), + ...references.map( + (reference) => + `${reference.title}:${reference.label} (${formatReferenceType( + reference.refType, + )} · ${reference.refId})`, + ), + ].join('\n'); +} + +function buildLayerMetadataCopyText(layer: CanvasLayer, mediaLabel: string) { + return [ + `${mediaLabel}类型:${formatLayerImageType(layer)}`, + `生成输入:\n${formatGenerationInputsForCopy(layer)}`, + `Model:${layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}`, + layer.mediaType === 'audio' + ? `时长:${formatLayerDurationForDisplay(layer)}` + : `Resolution:${layer.originalWidth} x ${layer.originalHeight} px`, + `Task:${formatTaskIdForDisplay(layer.taskId)}`, + `Object:${layer.objectKey ?? layer.assetObjectId ?? '-'}`, + ].join('\n'); +} + export function ImageCanvasMetadataModalView({ layer, onClose, }: ImageCanvasMetadataModalViewProps) { + const { copyState, copyText, resetCopyState } = useCopyFeedback(); const mediaLabel = layer?.mediaType === 'audio' ? '音频' @@ -28,6 +72,20 @@ export function ImageCanvasMetadataModalView({ : layer?.mediaType === 'image-sequence' ? '序列帧' : '图片'; + const metadataText = layer ? buildLayerMetadataCopyText(layer, mediaLabel) : ''; + + useEffect(() => { + resetCopyState(); + }, [metadataText, resetCopyState]); + + const copyMetadata = () => { + if (!metadataText) { + return; + } + + void copyText(metadataText); + }; + return ( + ) : null + } > {layer ? (
@@ -90,10 +162,7 @@ export function ImageCanvasMetadataModalView({ <>
时长
- {formatCanvasDurationMetric(layer.durationSeconds).replace( - /^时长\s*/u, - '', - )} + {formatLayerDurationForDisplay(layer)}
) : ( diff --git a/src/index.css b/src/index.css index af2729776..015a05fd1 100644 --- a/src/index.css +++ b/src/index.css @@ -8418,6 +8418,9 @@ button.image-canvas-editor__reference-chip:disabled { .image-canvas-editor__metadata-body { padding: 0; + -webkit-touch-callout: default; + -webkit-user-select: text; + user-select: text; } .image-canvas-editor__generate-dialog-body { @@ -8549,6 +8552,14 @@ button.image-canvas-editor__reference-chip:disabled { overflow-wrap: anywhere; } +.image-canvas-editor__metadata-footer { + padding: 0.72rem 0.85rem; +} + +.image-canvas-editor__metadata-copy-button { + justify-content: center; +} + @media (max-width: 760px) { .image-canvas-editor { flex-direction: column;