From 468bb5579dfe64c53e08f69220825fb1c86e7d13 Mon Sep 17 00:00:00 2001 From: suyx Date: Wed, 5 Aug 2026 18:01:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(POSTV1-07):=20=E8=87=AA=E5=8A=A8=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E7=BC=96=E8=BE=91=E5=99=A8=E6=93=8D=E4=BD=9C=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/editor-page.tsx | 72 +++++++++++++++++----------- tests/e2e/wp4-03-text-editor.spec.ts | 8 ++++ 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/apps/web/src/editor-page.tsx b/apps/web/src/editor-page.tsx index e5f4841..1ef8da0 100644 --- a/apps/web/src/editor-page.tsx +++ b/apps/web/src/editor-page.tsx @@ -160,6 +160,20 @@ export function EditorPage({ projectId }: { projectId: string }) { const conflictExportGuardRef = useRef(new ConflictExportGuard()); const candidateMenuRef = useRef(null); const candidateTriggerRef = useRef(null); + const noticeTimerRef = useRef | undefined>(undefined); + + function showNotice(message: string) { + if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current); + setNotice(message); + noticeTimerRef.current = setTimeout(() => { + setNotice(""); + noticeTimerRef.current = undefined; + }, 3_000); + } + + useEffect(() => () => { + if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current); + }, []); useEffect(() => { let active = true; @@ -175,7 +189,7 @@ export function EditorPage({ projectId }: { projectId: string }) { setDraftAdjustments(initial.background.adjustments); historyRef.current = new CanvasEditHistory(initial); elementControllerRef.current = new CanvasElementController(initial); - }).catch(() => { if (active) setNotice("编辑器暂时无法读取项目"); }); + }).catch(() => { if (active) showNotice("编辑器暂时无法读取项目"); }); return () => { active = false; }; }, [projectId]); @@ -291,7 +305,7 @@ export function EditorPage({ projectId }: { projectId: string }) { function applyPreview() { if (!canvasState) return; commitCanvas(updateBackgroundAdjustments(canvasState, draftAdjustments)); - setNotice("底图调整已提交"); + showNotice("底图调整已提交"); } function undo() { @@ -334,9 +348,9 @@ export function EditorPage({ projectId }: { projectId: string }) { const palette = await paletteForAsset(pendingBackground); commitCanvas(switchBackground(canvasState, pendingBackground, palette)); setPendingBackground(undefined); - setNotice("已更换底图,覆盖元素保留,底图处理已重置"); + showNotice("已更换底图,覆盖元素保留,底图处理已重置"); } catch { - setNotice("新底图无法读取,未更换底图或刷新色卡"); + showNotice("新底图无法读取,未更换底图或刷新色卡"); } } @@ -353,7 +367,7 @@ export function EditorPage({ projectId }: { projectId: string }) { commitCanvas(next); setGuides([]); setCandidateMenu(undefined); - setNotice(message); + showNotice(message); } function addSticker(sticker: StaticStickerCatalogItem) { @@ -369,8 +383,8 @@ export function EditorPage({ projectId }: { projectId: string }) { })); commitElementOperation(controller, "贴纸已加入画布"); } catch (error) { - if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。"); - else setNotice("贴纸未能加入画布"); + if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。"); + else showNotice("贴纸未能加入画布"); } } @@ -383,8 +397,8 @@ export function EditorPage({ projectId }: { projectId: string }) { controller.add(createColorCardElement(definition, palette, newElementIdentity(), canvasState.elements.length)); commitElementOperation(controller, "色卡已按原始底图加入画布"); } catch (error) { - if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。"); - else setNotice("无法从原始底图稳定提取五色,色卡未加入画布"); + if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。"); + else showNotice("无法从原始底图稳定提取五色,色卡未加入画布"); } } @@ -394,7 +408,7 @@ export function EditorPage({ projectId }: { projectId: string }) { if (templateId === "DYN012") { const font = fontOption("FONT081"); if (!font || await ensureFont(font.fontId, font.url) !== "ready") { - setNotice("DYN012 的 FONT081 替代字体不可用,未使用系统字体替代。"); + showNotice("DYN012 的 FONT081 替代字体不可用,未使用系统字体替代。"); return; } } @@ -407,8 +421,8 @@ export function EditorPage({ projectId }: { projectId: string }) { commitElementOperation(controller, "动态值已确认并加入画布"); setLocationDialog(undefined); } catch (error) { - if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。"); - else setNotice("动态贴纸未能加入画布"); + if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。"); + else showNotice("动态贴纸未能加入画布"); } } @@ -456,7 +470,7 @@ export function EditorPage({ projectId }: { projectId: string }) { controller.replaceElement(overrideDynamicStickerValue(element, value)); commitElementOperation(controller, "动态贴纸显示文字已更新"); } catch { - setNotice("动态贴纸显示文字不能为空"); + showNotice("动态贴纸显示文字不能为空"); } } @@ -480,7 +494,7 @@ export function EditorPage({ projectId }: { projectId: string }) { if (!template.fontUrl || !canvasState) return; const status = await ensureFont(template.defaultFontId, template.fontUrl); if (status !== "ready") { - setNotice("素材暂不可用,未使用系统字体替代。"); + showNotice("素材暂不可用,未使用系统字体替代。"); return; } const controller = controllerForCurrent(); @@ -490,8 +504,8 @@ export function EditorPage({ projectId }: { projectId: string }) { commitElementOperation(controller, "文字模板已加入画布"); void recordRecentTextTemplate(template.templateId, template.resourceVersion); } catch (error) { - if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。"); - else setNotice("文字模板未能加入画布"); + if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。"); + else showNotice("文字模板未能加入画布"); } } @@ -503,7 +517,7 @@ export function EditorPage({ projectId }: { projectId: string }) { action(edit); return { ...current, draft: edit.value }; } catch { - setNotice("文字参数不在允许范围内"); + showNotice("文字参数不在允许范围内"); return current; } }); @@ -514,7 +528,7 @@ export function EditorPage({ projectId }: { projectId: string }) { if (!template?.fontUrl) return; const status = await ensureFont(template.defaultFontId, template.fontUrl); if (status !== "ready") { - setNotice("素材暂不可用,未使用系统字体替代。"); + showNotice("素材暂不可用,未使用系统字体替代。"); return; } updateTextDraft((edit) => edit.switchTemplate(templateId)); @@ -527,7 +541,7 @@ export function EditorPage({ projectId }: { projectId: string }) { } const option = fontOption(fontId); if (!option || await ensureFont(option.fontId, option.url) !== "ready") { - setNotice("字体素材暂不可用,未使用系统字体替代。"); + showNotice("字体素材暂不可用,未使用系统字体替代。"); return; } updateTextDraft((edit) => edit.setStyle({ fontOverride: fontId })); @@ -546,8 +560,8 @@ export function EditorPage({ projectId }: { projectId: string }) { void recordRecentTextTemplate(complete.template_or_asset_id, complete.resource_version); } } catch (error) { - if (error instanceof Error && error.message === "text_content_required") setNotice("请输入文字内容或删除该元素。"); - else setNotice("文字编辑未能完成"); + if (error instanceof Error && error.message === "text_content_required") showNotice("请输入文字内容或删除该元素。"); + else showNotice("文字编辑未能完成"); } } @@ -556,7 +570,7 @@ export function EditorPage({ projectId }: { projectId: string }) { const current = canvasState.elements.find((element) => element.element_id === textEdit.elementId); if (current) setTextEdit({ draft: structuredClone(current), elementId: current.element_id, originalTemplateId: current.template_or_asset_id }); } - setNotice("已取消未提交的文字修改"); + showNotice("已取消未提交的文字修改"); } function pendingTextDraft() { @@ -624,7 +638,7 @@ export function EditorPage({ projectId }: { projectId: string }) { return; } const ran = await conflictExportGuardRef.current.run(() => executeExport(options)); - if (!ran) setNotice("版本冲突时仅允许导出本页版本一次"); + if (!ran) showNotice("版本冲突时仅允许导出本页版本一次"); } async function retryExportDownload() { @@ -639,8 +653,8 @@ export function EditorPage({ projectId }: { projectId: string }) { action(controller); commitElementOperation(controller, message); } catch (error) { - if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。"); - else setNotice("对象操作未完成"); + if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。"); + else showNotice("对象操作未完成"); } } @@ -668,7 +682,7 @@ export function EditorPage({ projectId }: { projectId: string }) { opacityGestureRef.current = undefined; if (gesture.last === gesture.base) return; commitCanvas(gesture.last); - setNotice("贴纸透明度已提交"); + showNotice("贴纸透明度已提交"); } function duplicateSelection() { @@ -688,7 +702,7 @@ export function EditorPage({ projectId }: { projectId: string }) { const controller = controllerForCurrent(); if (!controller || selectedIds.length === 0) return; clipboardRef.current = controller.copySelected(); - setNotice("已复制到画布剪贴板"); + showNotice("已复制到画布剪贴板"); } function pasteSelection() { @@ -698,7 +712,7 @@ export function EditorPage({ projectId }: { projectId: string }) { controller.pasteElements(clipboardRef.current, () => newElementIdentity()); commitElementOperation(controller, "已粘贴画布对象"); } catch (error) { - if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。"); + if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。"); } } @@ -728,7 +742,7 @@ export function EditorPage({ projectId }: { projectId: string }) { const drag = dragRef.current; if (!drag) return; commitCanvas(drag.last); - setNotice("对象位置已提交"); + showNotice("对象位置已提交"); setGuides([]); dragRef.current = undefined; } diff --git a/tests/e2e/wp4-03-text-editor.spec.ts b/tests/e2e/wp4-03-text-editor.spec.ts index 308cbf3..5372f0a 100644 --- a/tests/e2e/wp4-03-text-editor.spec.ts +++ b/tests/e2e/wp4-03-text-editor.spec.ts @@ -281,4 +281,12 @@ test("POSTV1-07 keeps text selection stable and dismisses move feedback", async await page.waitForTimeout(800); expect(backend.canvas.elements[0]?.position).toEqual(positionBefore); expect(backend.saves).toBe(savesBefore); + + await page.mouse.move(clientX, clientY); + await page.mouse.down(); + await page.mouse.move(clientX + 12, clientY); + await page.mouse.up(); + await expect(page.getByText("对象位置已提交", { exact: true })).toBeVisible(); + await expect.poll(() => backend.saves, { timeout: 5_000 }).toBeGreaterThan(savesBefore); + await expect(page.getByText("对象位置已提交", { exact: true })).toBeHidden({ timeout: 5_000 }); });