From 10b83799654eec8a0e3f511252fdf0dd79ffa0b7 Mon Sep 17 00:00:00 2001 From: OmniAI Developer Date: Fri, 5 Jun 2026 00:37:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A4=E4=BA=92=E5=BC=8F=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86=E7=94=9F=E6=88=90=E5=99=A8=20+=20=E7=94=B5?= =?UTF-8?q?=E5=95=86=E5=8F=96=E6=B6=88=E7=94=9F=E6=88=90=E4=B8=8E=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - 交互式对话框生成器模块(路由、页面、样式、MorePage入口) - 电商模块取消生成功能(任务追踪/取消按钮/中止逻辑) - 视频服务图片上传支持 Blob/dataURL/远程URL 多种来源 优化: - 电商图片上传修复本地 blob 预览图缺少原始文件的问题 - 视频规划管线错误信息改进 - 生成流程中多处增加中止检查点 --- src/App.tsx | 6 +- src/components/AppShell.tsx | 1 + src/components/PageTransition.tsx | 3 +- .../dialog-generator/DialogGeneratorPage.tsx | 290 +++++++++ src/features/ecommerce/EcommercePage.tsx | 126 +++- .../ecommerce/EcommerceVideoWorkspace.tsx | 5 +- .../ecommerce/ecommerceVideoService.ts | 99 ++- .../ecommerce/panels/EcommerceClonePanel.tsx | 7 + .../ecommerce/panels/EcommerceDetailPanel.tsx | 7 + .../ecommerce/panels/EcommerceTryOnPanel.tsx | 7 + src/features/more/MorePage.tsx | 2 + src/styles/index.css | 1 + src/styles/pages/dialog-generator.css | 580 ++++++++++++++++++ src/styles/pages/ecommerce.css | 37 ++ src/styles/pages/toolbox.css | 4 +- src/types.ts | 1 + 16 files changed, 1125 insertions(+), 51 deletions(-) create mode 100644 src/features/dialog-generator/DialogGeneratorPage.tsx create mode 100644 src/styles/pages/dialog-generator.css diff --git a/src/App.tsx b/src/App.tsx index 2e1d5fd..91e878f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ const CommunityCaseAddPage = lazy(() => import("./features/community-review/Comm const CommunityReviewPage = lazy(() => import("./features/community-review/CommunityReviewPage")); const AvatarConsolePage = lazy(() => import("./features/digital-human/AvatarConsolePage")); const DigitalHumanPage = lazy(() => import("./features/digital-human/DigitalHumanPage")); +const DialogGeneratorPage = lazy(() => import("./features/dialog-generator/DialogGeneratorPage")); const EcommercePage = lazy(() => import("./features/ecommerce/EcommercePage")); const HomePage = lazy(() => import("./features/home/HomePage")); const ImageWorkbenchPage = lazy(() => import("./features/image-workbench/ImageWorkbenchPage")); @@ -110,6 +111,7 @@ const VIEW_KEYS = new Set([ "resolutionUpscale", "watermarkRemoval", "subtitleRemoval", + "dialogGenerator", "digitalHuman", "avatarConsole", "characterMix", @@ -123,7 +125,7 @@ const VIEW_KEYS = new Set([ "not-found", ]); -const PUBLIC_VIEW_SET = new Set(["home", "login", "community", "more", "userAgreement", "privacyPolicy", "not-found"]); +const PUBLIC_VIEW_SET = new Set(["home", "login", "community", "more", "dialogGenerator", "userAgreement", "privacyPolicy", "not-found"]); function normalizeViewKey(rawView: string): WebViewKey { const normalized = @@ -1159,6 +1161,8 @@ function App() { onSelectView={handleSetView} /> ); + case "dialogGenerator": + return ; case "report": return ; case "providerHealth": diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index a03cf8a..ce0a772 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -86,6 +86,7 @@ function AppShell({ "imageWorkbench", "resolutionUpscale", "digitalHuman", + "dialogGenerator", "avatarConsole", "characterMix", ] as WebViewKey[]; diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index a020281..058a5d9 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -23,6 +23,7 @@ const NAV_ORDER: string[] = [ "resolutionUpscale", "watermarkRemoval", "subtitleRemoval", + "dialogGenerator", "digitalHuman", "avatarConsole", "characterMix", @@ -87,4 +88,4 @@ export default function PageTransition({ viewKey, children }: PageTransitionProp {displayedChildren} ); -} \ No newline at end of file +} diff --git a/src/features/dialog-generator/DialogGeneratorPage.tsx b/src/features/dialog-generator/DialogGeneratorPage.tsx new file mode 100644 index 0000000..4d99386 --- /dev/null +++ b/src/features/dialog-generator/DialogGeneratorPage.tsx @@ -0,0 +1,290 @@ +import { useCallback, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type TouchEvent as ReactTouchEvent } from "react"; + +type DialogStyle = "style1" | "style2" | "style3" | "style4"; + +interface DialogItem { + id: number; + style: DialogStyle; + x: number; + y: number; + text: string; + color: string; + confirmed: boolean; +} + +interface DragState { + id: number; + offsetX: number; + offsetY: number; +} + +const dialogStyles: Array<{ + key: DialogStyle; + label: string; + description: string; + swatchClass: string; +}> = [ + { key: "style1", label: "白色圆角对话框", description: "适合浅色说明与标注", swatchClass: "is-white" }, + { key: "style2", label: "蓝色气泡对话框", description: "适合角色台词与重点提示", swatchClass: "is-blue" }, + { key: "style3", label: "黄色提示对话框", description: "适合醒目提醒与强调", swatchClass: "is-amber" }, + { key: "style4", label: "灰色简约对话框", description: "适合信息备注与辅助说明", swatchClass: "is-gray" }, +]; + +const textColorOptions = [ + { value: "#ffffff", label: "白色" }, + { value: "#111827", label: "黑色" }, + { value: "#ef4444", label: "红色" }, + { value: "#f59e0b", label: "黄色" }, + { value: "#165dff", label: "蓝色" }, + { value: "#00ff88", label: "绿色" }, +]; + +function DialogGeneratorPage() { + const fileInputRef = useRef(null); + const previewRef = useRef(null); + const dragRef = useRef(null); + const nextIdRef = useRef(0); + const [backgroundUrl, setBackgroundUrl] = useState(""); + const [dialogs, setDialogs] = useState([]); + const [selectedTextColor, setSelectedTextColor] = useState(textColorOptions[0].value); + const [activeDragId, setActiveDragId] = useState(null); + + const handleFile = useCallback((file?: File | null) => { + if (!file || !file.type.startsWith("image/")) return; + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result === "string") { + setBackgroundUrl(reader.result); + } + }; + reader.readAsDataURL(file); + }, []); + + const addDialog = useCallback((style: DialogStyle) => { + nextIdRef.current += 1; + const id = nextIdRef.current; + setDialogs((current) => [ + ...current, + { + id, + style, + x: 30 + (id * 25) % 200, + y: 30 + (id * 20) % 150, + text: "", + color: selectedTextColor, + confirmed: false, + }, + ]); + }, [selectedTextColor]); + + const updateDialog = useCallback((id: number, patch: Partial) => { + setDialogs((current) => current.map((item) => (item.id === id ? { ...item, ...patch } : item))); + }, []); + + const deleteDialog = useCallback((id: number) => { + setDialogs((current) => current.filter((item) => item.id !== id)); + }, []); + + const startDrag = useCallback((id: number, clientX: number, clientY: number) => { + const dialogEl = document.querySelector(`[data-dialog-id="${id}"]`); + if (!dialogEl) return; + const rect = dialogEl.getBoundingClientRect(); + dragRef.current = { + id, + offsetX: clientX - rect.left, + offsetY: clientY - rect.top, + }; + setActiveDragId(id); + }, []); + + const moveDrag = useCallback((clientX: number, clientY: number) => { + const drag = dragRef.current; + const preview = previewRef.current; + if (!drag || !preview) return; + const dialogEl = document.querySelector(`[data-dialog-id="${drag.id}"]`); + if (!dialogEl) return; + + const bounds = preview.getBoundingClientRect(); + const nextX = Math.max(0, Math.min(clientX - drag.offsetX - bounds.left, bounds.width - dialogEl.offsetWidth)); + const nextY = Math.max(0, Math.min(clientY - drag.offsetY - bounds.top, bounds.height - dialogEl.offsetHeight)); + updateDialog(drag.id, { x: nextX, y: nextY }); + }, [updateDialog]); + + const endDrag = useCallback(() => { + dragRef.current = null; + setActiveDragId(null); + }, []); + + const handleCanvasMouseMove = useCallback((event: ReactMouseEvent) => { + moveDrag(event.clientX, event.clientY); + }, [moveDrag]); + + const handleCanvasTouchMove = useCallback((event: ReactTouchEvent) => { + const touch = event.touches[0]; + if (!touch) return; + moveDrag(touch.clientX, touch.clientY); + }, [moveDrag]); + + return ( +
+
+ + +
+
+
+ Preview +

预览区域

+
+

拖动文字定位,输入文字后点击确认,确认后只保留文字图层,双击可重新编辑。

+
+ +
+ {backgroundUrl ?
: null} + {!backgroundUrl ? ( +
+ 🖼 +

上传图片后开始编辑

+
+ ) : null} + + {dialogs.map((dialog) => ( +
{ + const target = event.target as HTMLElement; + if (target.closest("textarea,button")) return; + startDrag(dialog.id, event.clientX, event.clientY); + event.preventDefault(); + }} + onTouchStart={(event) => { + const target = event.target as HTMLElement; + if (target.closest("textarea,button")) return; + const touch = event.touches[0]; + if (touch) startDrag(dialog.id, touch.clientX, touch.clientY); + }} + onDoubleClick={() => { + if (dialog.confirmed) updateDialog(dialog.id, { confirmed: false }); + }} + > + {!dialog.confirmed ? ( + + ) : null} + {dialog.confirmed ? ( +
{dialog.text}
+ ) : ( +