import { useCallback, useEffect, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type TouchEvent as ReactTouchEvent } from "react"; import { ApartmentOutlined, DownOutlined, RobotOutlined, ThunderboltOutlined } from "@ant-design/icons"; type DialogStyle = "style1" | "style2" | "style3" | "style4"; type GenerationMode = "dialog" | "video"; 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: "绿色" }, ]; const dialogModelOptions = [ { id: "gemini", label: "Gemini" }, { id: "wanxian", label: "万相" }, { id: "deepseek", label: "DeepSeek" }, ]; const thinkingSpeedOptions = [ { id: "default", label: "默认" }, { id: "high", label: "高" }, { id: "ultra", label: "急速" }, ]; const thinkingDepthOptions = [ { id: "default", label: "默认" }, { id: "strong", label: "强" }, { id: "extreme", label: "极限" }, ]; const videoDurationOptions = [ { value: "5", label: "5s" }, { value: "6", label: "6s" }, { value: "7", label: "7s" }, { value: "8", label: "8s" }, { value: "9", label: "9s" }, { value: "10", label: "10s" }, { value: "11", label: "11s" }, { value: "12", label: "12s" }, { value: "13", label: "13s" }, { value: "14", label: "14s" }, { value: "15", label: "15s" }, ]; function DialogGeneratorPage() { const fileInputRef = useRef(null); const previewRef = useRef(null); const dragRef = useRef(null); const nextIdRef = useRef(0); const controlsRef = useRef(null); const [backgroundUrl, setBackgroundUrl] = useState(""); const [dialogs, setDialogs] = useState([]); const [selectedTextColor, setSelectedTextColor] = useState(textColorOptions[0].value); const [activeDragId, setActiveDragId] = useState(null); // ── Generation state ── const [generationMode, setGenerationMode] = useState("dialog"); const [dialogModel, setDialogModel] = useState(dialogModelOptions[0].id); const [thinkingSpeed, setThinkingSpeed] = useState(thinkingSpeedOptions[0].id); const [thinkingDepth, setThinkingDepth] = useState(thinkingDepthOptions[0].id); const [videoDuration, setVideoDuration] = useState(videoDurationOptions[0].value); const [activeDropdown, setActiveDropdown] = useState(null); const [isGenerating, setIsGenerating] = useState(false); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (controlsRef.current && !controlsRef.current.contains(event.target as Node)) { setActiveDropdown(null); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); 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}
) : (