import React, { useState, useRef, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { Separator } from '@/components/ui/separator'; import { Type, Crop, Save, Loader2, Check, Plus, X } from 'lucide-react'; import { FlowerTextTemplate } from '@/api/entities'; const loadImage = (url) => new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => resolve(img); img.onerror = reject; img.src = url; }); export default function ImageCompareModal({ isOpen, onClose, generatedImage, onSave, initialFlowerText = '', initialFlowerStyle = 'classic' }) { const [currentImageDataUrl, setCurrentImageDataUrl] = useState(generatedImage); const [mode, setMode] = useState('crop'); const [textElements, setTextElements] = useState([]); const [selectedTextId, setSelectedTextId] = useState(null); const [cropArea, setCropArea] = useState({ x: 10, y: 10, width: 80, height: 80 }); const [isDragging, setIsDragging] = useState(false); const [dragType, setDragType] = useState(null); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [resizeHandle, setResizeHandle] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isApplyingCrop, setIsApplyingCrop] = useState(false); const [templateName, setTemplateName] = useState(''); const imageContainerRef = useRef(null); const isDraggingRef = useRef(false); useEffect(() => { if (isOpen) { setCurrentImageDataUrl(generatedImage); setTextElements([ { id: 1, text: initialFlowerText, x: 50, y: 50, fontSize: 32, color: '#FFFFFF', style: initialFlowerStyle, width: 300 } ]); setSelectedTextId(1); setCropArea({ x: 10, y: 10, width: 80, height: 80 }); setMode('crop'); } }, [isOpen, generatedImage, initialFlowerText, initialFlowerStyle]); const textStyleOptions = [ { value: 'classic', label: '经典样式' }, { value: 'modern', label: '现代简约' }, { value: 'cute', label: '可爱风格' }, { value: 'elegant', label: '优雅文艺' }, { value: 'bold', label: '粗体醒目' }, { value: 'gradient', label: '渐变炫彩' }, { value: 'neon', label: '霓虹发光' }, { value: 'vintage', label: '复古怀旧' } ]; const getTextStyle = (style) => { const styles = { classic: { fontFamily: 'serif', textShadow: '2px 2px 4px rgba(0,0,0,0.5)', fontWeight: 'bold' }, modern: { fontFamily: 'sans-serif', textShadow: '1px 1px 2px rgba(0,0,0,0.3)', fontWeight: '600', letterSpacing: '1px' }, cute: { fontFamily: 'cursive', textShadow: '2px 2px 0px #fff, 4px 4px 6px rgba(0,0,0,0.3)', fontWeight: 'bold' }, elegant: { fontFamily: 'serif', textShadow: '1px 1px 3px rgba(0,0,0,0.4)', fontWeight: '300', fontStyle: 'italic' }, bold: { fontFamily: 'sans-serif', textShadow: '3px 3px 0px #000, 6px 6px 8px rgba(0,0,0,0.4)', fontWeight: '900', textTransform: 'uppercase' }, gradient: { fontFamily: 'sans-serif', background: 'linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', textShadow: 'none', fontWeight: 'bold' }, neon: { fontFamily: 'sans-serif', textShadow: '0 0 10px currentColor, 0 0 20px currentColor, 0 0 30px currentColor', fontWeight: 'bold', color: '#00ffff' }, vintage: { fontFamily: 'serif', textShadow: '2px 2px 0px #8B4513, 4px 4px 6px rgba(0,0,0,0.5)', fontWeight: 'bold', color: '#D2691E' } }; return styles[style] || styles.classic; }; const getRelativePosition = (e) => { if (!imageContainerRef.current) return { x: 0, y: 0 }; const rect = imageContainerRef.current.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width) * 100; const y = ((e.clientY - rect.top) / rect.height) * 100; return { x: Math.max(0, Math.min(100, x)), y: Math.max(0, Math.min(100, y)) }; }; const handleMouseDown = (e, type, id = null, handle = null) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); setDragType(type); isDraggingRef.current = true; const pos = getRelativePosition(e); setDragStart(pos); if (type === 'text' && id) { setSelectedTextId(id); const text = textElements.find(t => t.id === id); if (text) setDragOffset({ x: pos.x - text.x, y: pos.y - text.y }); } else if (type === 'crop' && !handle) { setDragOffset({ x: pos.x - cropArea.x, y: pos.y - cropArea.y }); } else if (type === 'crop' && handle) { setResizeHandle(handle); } }; const handleMouseMove = (e) => { if (!isDraggingRef.current) return; const pos = getRelativePosition(e); if (dragType === 'text') { setTextElements(prev => prev.map(t => t.id === selectedTextId ? { ...t, x: pos.x - dragOffset.x, y: pos.y - dragOffset.y } : t)); } else if (dragType === 'crop') { if (resizeHandle) { setCropArea(prev => { const newArea = { ...prev }; const dx = pos.x - dragStart.x; const dy = pos.y - dragStart.y; if (resizeHandle.includes('e')) newArea.width += dx; if (resizeHandle.includes('s')) newArea.height += dy; if (resizeHandle.includes('w')) { newArea.x += dx; newArea.width -= dx; } if (resizeHandle.includes('n')) { newArea.y += dy; newArea.height -= dy; } setDragStart(pos); return newArea; }); } else { setCropArea(prev => ({ ...prev, x: pos.x - dragOffset.x, y: pos.y - dragOffset.y })); } } }; const handleMouseUp = () => { setIsDragging(false); setDragType(null); isDraggingRef.current = false; setResizeHandle(null); }; useEffect(() => { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [handleMouseMove, handleMouseUp]); const handleApplyCrop = async () => { setIsApplyingCrop(true); try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const baseImage = await loadImage(currentImageDataUrl); const cropX = (cropArea.x / 100) * baseImage.naturalWidth; const cropY = (cropArea.y / 100) * baseImage.naturalHeight; const cropWidth = (cropArea.width / 100) * baseImage.naturalWidth; const cropHeight = (cropArea.height / 100) * baseImage.naturalHeight; canvas.width = cropWidth; canvas.height = cropHeight; ctx.drawImage(baseImage, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); const newImageDataUrl = canvas.toDataURL('image/png'); setCurrentImageDataUrl(newImageDataUrl); setMode('text'); } catch (error) { console.error("应用裁剪失败:", error); } finally { setIsApplyingCrop(false); } }; const generateEditedImage = async () => { setIsSaving(true); try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const baseImage = await loadImage(currentImageDataUrl); canvas.width = baseImage.naturalWidth; canvas.height = baseImage.naturalHeight; ctx.drawImage(baseImage, 0, 0); textElements.forEach(textEl => { const style = getTextStyle(textEl.style); ctx.font = `${style.fontWeight || 'normal'} ${style.fontStyle || ''} ${textEl.fontSize}px ${style.fontFamily || 'sans-serif'}`; ctx.fillStyle = textEl.color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (style.textShadow) { ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 5; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; } const xPos = (textEl.x / 100) * canvas.width; const yPos = (textEl.y / 100) * canvas.height; const lines = textEl.text.split('\n'); lines.forEach((line, index) => { ctx.fillText(line, xPos, yPos + (index * textEl.fontSize * 1.2)); }); ctx.shadowColor = 'transparent'; }); const finalImageDataUrl = canvas.toDataURL('image/png'); onSave({ originalUrl: generatedImage, newImageDataUrl: finalImageDataUrl }); onClose(); } catch (error) { console.error("生成最终图片失败:", error); } finally { setIsSaving(false); } }; const handleSliderChange = (field, value) => { setCropArea(prev => { const newArea = { ...prev }; if (field === 'x') newArea.x = Math.min(value, 100 - prev.width); else if (field === 'y') newArea.y = Math.min(value, 100 - prev.height); else if (field === 'width') newArea.width = Math.min(value, 100 - prev.x); else if (field === 'height') newArea.height = Math.min(value, 100 - prev.y); if (newArea.width < 10) newArea.width = 10; if (newArea.height < 10) newArea.height = 10; return newArea; }); }; const handleTextChange = (textId, field, value) => { setTextElements(prev => prev.map(t => t.id === textId ? { ...t, [field]: value } : t)); }; const handleAddText = () => { const newText = { id: Date.now(), text: '新文字', x: 50, y: 70, fontSize: 20, color: '#FFFFFF', style: 'classic', width: 150 }; setTextElements(prev => [...prev, newText]); setSelectedTextId(newText.id); }; const handleDeleteText = (textId) => { setTextElements(prev => prev.filter(t => t.id !== textId)); if (selectedTextId === textId) setSelectedTextId(null); }; const handleSaveTemplate = async () => { if (!templateName.trim()) { alert('请输入模板名称'); return; } await FlowerTextTemplate.create({ name: templateName, textElements: textElements }); alert('模板保存成功!'); setTemplateName(''); }; const renderMultilineText = (textEl) => { const lineHeight = textEl.fontSize * 1.2; return (