'use client'; import React, { useState, useRef, useCallback, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { TrendingUp, Eye, EyeOff } from 'lucide-react'; interface DataPoint { x: number; y: number; label?: string; } interface DramaLineChartProps { data?: DataPoint[]; width?: number; height?: number; onDataChange?: (data: DataPoint[]) => void; className?: string; showLabels?: boolean; title?: string; showToggleButton?: boolean; } export function DramaLineChart({ data: initialData, width, height = 120, onDataChange, className = '', showLabels = true, title = '戏剧张力线', showToggleButton = true }: DramaLineChartProps) { // Mock 数据 - 模拟一个经典的戏剧结构,x轴表示时间进度 const mockData: DataPoint[] = [ { x: 0, y: 20, label: '0s' }, { x: 15, y: 35, label: '1' }, { x: 30, y: 45, label: '2s' }, { x: 45, y: 65, label: '3s' }, { x: 60, y: 85, label: '4s' }, { x: 75, y: 70, label: '5s' }, { x: 90, y: 40, label: '6s' }, { x: 100, y: 25, label: '7s' }, { x: 100, y: 25, label: '8s' }, ]; const [data, setData] = useState(initialData || mockData); const [isDragging, setIsDragging] = useState(false); const [dragIndex, setDragIndex] = useState(null); const [isVisible, setIsVisible] = useState(!showToggleButton); const [hoveredPoint, setHoveredPoint] = useState(null); const [containerWidth, setContainerWidth] = useState(width || 320); const svgRef = useRef(null); const containerRef = useRef(null); // 动态计算容器宽度 useEffect(() => { const updateWidth = () => { if (containerRef.current && !width) { setContainerWidth(containerRef.current.offsetWidth - 8); // 减去padding } }; updateWidth(); window.addEventListener('resize', updateWidth); return () => window.removeEventListener('resize', updateWidth); }, [width]); // 计算SVG坐标 const padding = 20; let chartWidth = (width || containerWidth) - padding * 4; chartWidth = showToggleButton ? chartWidth : (width || containerWidth) - padding * 2; const chartHeight = height - padding * 2; const getPointPosition = useCallback((point: DataPoint) => { return { x: padding + (point.x / 100) * chartWidth, y: padding + (1 - point.y / 100) * chartHeight }; }, [chartWidth, chartHeight, padding]); // 根据鼠标位置计算数据点 const getDataPointFromMouse = useCallback((clientX: number, clientY: number) => { if (!svgRef.current) return null; const rect = svgRef.current.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; const dataX = Math.max(0, Math.min(100, ((x - padding) / chartWidth) * 100)); const dataY = Math.max(0, Math.min(100, (1 - (y - padding) / chartHeight) * 100)); return { x: dataX, y: dataY }; }, [chartWidth, chartHeight, padding]); // 处理鼠标按下 const handleMouseDown = useCallback((e: React.MouseEvent, index: number) => { e.preventDefault(); setIsDragging(true); setDragIndex(index); }, []); // 处理鼠标移动 const handleMouseMove = useCallback((e: MouseEvent) => { if (!isDragging || dragIndex === null) return; const newPoint = getDataPointFromMouse(e.clientX, e.clientY); if (!newPoint) return; const newData = [...data]; // 保持x坐标不变,只允许改变y坐标 newData[dragIndex] = { ...newData[dragIndex], y: newPoint.y }; setData(newData); onDataChange?.(newData); }, [isDragging, dragIndex, data, getDataPointFromMouse, onDataChange]); // 处理鼠标抬起 const handleMouseUp = useCallback(() => { setIsDragging(false); setDragIndex(null); }, []); // 添加全局事件监听 useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; } }, [isDragging, handleMouseMove, handleMouseUp]); // 生成路径字符串 const pathData = data.map((point, index) => { const pos = getPointPosition(point); return index === 0 ? `M ${pos.x} ${pos.y}` : `L ${pos.x} ${pos.y}`; }).join(' '); // 生成平滑曲线路径 const smoothPathData = React.useMemo(() => { if (data.length < 2) return pathData; const points = data.map(getPointPosition); let path = `M ${points[0].x} ${points[0].y}`; for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; const next = points[i + 1]; if (i === 1) { // 第二个点 const cp1x = prev.x + (curr.x - prev.x) * 0.3; const cp1y = prev.y; const cp2x = curr.x - (curr.x - prev.x) * 0.3; const cp2y = curr.y; path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`; } else if (i === points.length - 1) { // 最后一个点 const cp1x = prev.x + (curr.x - prev.x) * 0.3; const cp1y = prev.y; const cp2x = curr.x - (curr.x - prev.x) * 0.3; const cp2y = curr.y; path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`; } else { // 中间的点 const prevPrev = points[i - 2]; const cp1x = prev.x + (curr.x - prevPrev.x) * 0.15; const cp1y = prev.y + (curr.y - prevPrev.y) * 0.15; const cp2x = curr.x - (next.x - prev.x) * 0.15; const cp2y = curr.y - (next.y - prev.y) * 0.15; path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`; } } return path; }, [data, getPointPosition, pathData]); // 切换显示状态 const toggleVisibility = () => { setIsVisible(!isVisible); }; return (
{/* 切换按钮 */} {showToggleButton && ( {isVisible ? ( ) : ( )} )} {/* 折线图容器 */} {isVisible && (
{/* 标题 */} {title} (Drag to adjust the tension value) {/* SVG 图表 */} {/* 网格线 */} {/* 渐变 */} {/* 区域渐变 */} {/* 主线条 */} {/* 数据点 */} {data.map((point, index) => { const pos = getPointPosition(point); const isHovered = hoveredPoint === index; const isDraggingThis = dragIndex === index; return ( {/* 点的光环效果 */} {(isHovered || isDraggingThis) && ( )} {/* 主要的点 */} handleMouseDown(e, index)} onMouseEnter={() => setHoveredPoint(index)} onMouseLeave={() => setHoveredPoint(null)} whileHover={{ scale: 1.3 }} whileTap={{ scale: 0.9 }} initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ delay: 0.5 + index * 0.1, duration: 0.3, type: "spring", stiffness: 300 }} /> {/* 数值标签 */} {(isHovered || isDraggingThis) && showLabels && ( {Math.round(point.y)} )} ); })} {/* 时间轴标签 - 替代原来的底部标签 */} {showLabels && data.map((point, index) => { const pos = getPointPosition(point); // 只显示部分标签避免重叠 const shouldShow = index === 0 || index === data.length - 1 || index % 2 === 0; return shouldShow ? ( {point.label} ) : null; })}
)}
); }