From d07cdce7bc2632a90c504ef8f3486d45420c8810 Mon Sep 17 00:00:00 2001 From: suzmii Date: Thu, 6 Aug 2026 10:26:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=B8=E6=88=8F=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E7=94=9F=E6=88=90=E5=A4=8D=E5=AE=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐触屏画风预览点击开关 转发场景生成告警并保留权威项目写回 限制场景参考图并禁用自动重试 补充场景生成回归测试与技术说明 --- ...方案】图片画布游戏场景生成链路-2026-08-04.md | 7 +- ...ImageCanvasGenerationComposerView.test.tsx | 35 ++++++++ .../ImageCanvasSceneStyleControl.tsx | 18 +++- ...anvasGenerationSubmissionWorkflow.test.tsx | 90 +++++++++++++++++++ ...ImageCanvasGenerationSubmissionWorkflow.ts | 19 ++++ src/index.css | 9 +- .../image-editor/editorProjectClient.test.ts | 23 ++++- .../image-editor/editorProjectClient.ts | 7 +- 8 files changed, 192 insertions(+), 16 deletions(-) diff --git a/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md b/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md index e5fba1d12..3e3d67936 100644 --- a/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md +++ b/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md @@ -56,7 +56,7 @@ - FileReader Data URL 正式请求。 - 复制出的整套画布和缩减版类型。 -四张固定图片只用于前端预览,不进入 `generationReferences`,不随生成请求提交。保留原分辨率,只做轻量无损压缩;画风菜单打开时不同时挂载全部预览图,只在用户悬停或键盘聚焦对应预览入口时按需挂载当前图片。 +四张固定图片只用于前端预览,不进入 `generationReferences`,不随生成请求提交。保留原分辨率,只做轻量无损压缩;画风菜单打开时不同时挂载全部预览图,只在用户悬停、键盘聚焦或点击对应预览入口时按需挂载当前图片。点击同一入口可关闭预览,保证无 hover 的触屏设备可用。 ## 4. API 契约 @@ -85,7 +85,9 @@ canvasCompletion? 请求不接受前端组装后的完整 `prompt`。本期不修改 `/api/external/v1`。 -`generationInputs` 只承载结构化输入快照。后端不信任或透传前端提交的 `fields`,而是根据标准化后的场景请求重新构建画面内容、视觉风格、比例、清晰度和模型字段;前端提交的参考图元数据只使用既有项目资源或账号素材的行引用,不保存媒体 URL。完整 Provider Prompt 仍只能由后端生成。 +完整 Provider Prompt 仍只能由后端生成。 + +场景参考图沿用普通图片生成的客户端前置门禁,最多 5 张;超限时不得发送 HTTP 请求。场景生成 POST 使用生成专用零重试策略,避免 inline 响应丢失后重复调用 Provider。 后端对模型、比例和清晰度先沿用 `normalize_editor_generation_options` 标准化,再使用标准化比例决定画幅描述和入队价格。 @@ -127,6 +129,7 @@ prompt = 后端完整 Prompt - `scene` 明确按普通图片模型档位计价。 - Provider 失败、执行取消和 lease 耗尽沿用现有扣退费语义。 - Provider 成功后的持久化或画布写回失败语义不在本期调整。 +- inline 响应中的通用生成告警必须在队列终态处理前转发;有项目画布占位但响应缺少权威 `project` 快照时,不得追加本地图层或把占位标记为已完成。 ## 7. 数据落点 diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx index bb2100335..880f84140 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx @@ -339,6 +339,41 @@ describe('ImageCanvasGenerationComposerView', () => { expect(within(panel).queryByText('像素艺术')).toBeNull(); }); + it('触屏点击可以打开并关闭当前画风预览', () => { + renderComposer({ + mode: 'scene', + prompt: '', + status: 'idle', + composerOpen: true, + generationReferences: [], + sceneStylePreset: 'anime', + sceneCustomStyle: '', + imageModel: 'gemini-3.1-flash-image-preview', + aspectRatio: '16:9', + imageSize: '1K', + }); + + fireEvent.click(screen.getByRole('button', { name: '画风预设 日系动画' })); + const previewButton = screen.getByRole('button', { + name: '预览日系动画画风', + }); + + fireEvent.pointerDown(previewButton, { pointerType: 'touch' }); + fireEvent.click(previewButton); + fireEvent.blur(previewButton); + expect(previewButton.getAttribute('aria-expanded')).toBe('true'); + expect(screen.getByAltText('日系动画固定画风预览')).toBeTruthy(); + + fireEvent.pointerDown(previewButton, { pointerType: 'touch' }); + fireEvent.click(previewButton); + expect(previewButton.getAttribute('aria-expanded')).toBe('false'); + expect(screen.queryByAltText('日系动画固定画风预览')).toBeNull(); + + fireEvent.focus(previewButton); + expect(previewButton.getAttribute('aria-expanded')).toBe('true'); + expect(screen.getByAltText('日系动画固定画风预览')).toBeTruthy(); + }); + it('让场景自定义画风使用可自增长的多行字段', () => { renderComposer({ mode: 'scene', diff --git a/src/components/image-editor/ImageCanvasSceneStyleControl.tsx b/src/components/image-editor/ImageCanvasSceneStyleControl.tsx index 86d5875ba..b87c88589 100644 --- a/src/components/image-editor/ImageCanvasSceneStyleControl.tsx +++ b/src/components/image-editor/ImageCanvasSceneStyleControl.tsx @@ -53,16 +53,19 @@ export function ImageCanvasSceneStyleControl({ useState(null); const [focusedPreview, setFocusedPreview] = useState(null); + const [clickedPreview, setClickedPreview] = + useState(null); const triggerRef = useRef(null); const dismiss = useCallback(() => { setIsOpen(false); setHoveredPreview(null); setFocusedPreview(null); + setClickedPreview(null); }, []); const selectedValue = dialog.sceneStylePreset ?? DEFAULT_EDITOR_SCENE_STYLE_PRESET; const selectedLabel = getEditorSceneStylePresetLabel(selectedValue); - const activePreview = hoveredPreview ?? focusedPreview; + const activePreview = hoveredPreview ?? focusedPreview ?? clickedPreview; useImageCanvasFloatingOptionDismiss({ isOpen, @@ -174,8 +177,17 @@ export function ImageCanvasSceneStyleControl({ className="image-canvas-editor__scene-style-preview-button" aria-label={`预览${option.label}画风`} aria-expanded={previewActive} - onPointerDown={(event) => event.preventDefault()} - onClick={(event) => event.stopPropagation()} + onClick={(event) => { + event.stopPropagation(); + setClickedPreview((currentPreview) => { + if (currentPreview === option.value) { + setHoveredPreview(null); + setFocusedPreview(null); + return null; + } + return option.value; + }); + }} >