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}
) : (