import { useEffect, useState, useRef } from 'react'; import { Col, Spin, Empty, Tag, Progress, Skeleton, Tooltip, Card, Typography } from 'antd'; import { InfoCircleOutlined, DashboardOutlined, } from '@ant-design/icons'; import { Column, Pie, Funnel, Line } from '@ant-design/charts'; import type { EntityStat, FieldBreakdown, WidgetData } from './dashboardTypes'; import { TAG_COLORS, WIDGET_ICON_MAP } from './dashboardConstants'; // ── 计数动画 Hook ── function useCountUp(end: number, duration = 800) { const [count, setCount] = useState(0); const prevEnd = useRef(end); useEffect(() => { if (end === prevEnd.current && count > 0) return; prevEnd.current = end; if (end === 0) { setCount(0); return; } const startTime = performance.now(); function tick(now: number) { const elapsed = now - startTime; const progress = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); setCount(Math.round(end * eased)); if (progress < 1) requestAnimationFrame(tick); } requestAnimationFrame(tick); }, [end, duration]); return count; } // ── 共享工具 ── function prepareChartData(data: WidgetData['data'], dimensionOrder?: string[]) { return dimensionOrder ? dimensionOrder .map((key) => data.find((d) => d.key === key)) .filter(Boolean) .map((d) => ({ key: d!.key, count: d!.count })) : data.map((d) => ({ key: d.key, count: d.count })); } const TAG_COLOR_MAP: Record = { blue: '#3B82F6', green: '#10B981', orange: '#F59E0B', red: '#EF4444', purple: '#8B5CF6', cyan: '#06B6D4', magenta: '#EC4899', gold: '#EAB308', lime: '#84CC16', geekblue: '#6366F1', volcano: '#F97316', }; function tagStrokeColor(color: string): string { return TAG_COLOR_MAP[color] || '#3B82F6'; } function WidgetCardShell({ title, widgetType, children, }: { title: string; widgetType: string; children: React.ReactNode; }) { return ( {WIDGET_ICON_MAP[widgetType]} {title}} className="erp-fade-in" > {children} ); } function ChartEmpty() { return ; } // ── 基础子组件 ── function StatValue({ value, loading }: { value: number; loading: boolean }) { const animatedValue = useCountUp(value); if (loading) return ; return {animatedValue.toLocaleString()}; } /** 顶部统计卡片 */ export function StatCard({ stat, loading, delay }: { stat: EntityStat; loading: boolean; delay: string }) { return (
{stat.displayName}
{stat.icon}
); } /** 骨架屏卡片 */ export function SkeletonStatCard({ delay }: { delay: string }) { return (
); } /** 字段分布卡片 */ export function BreakdownCard({ breakdown, totalCount, index, }: { breakdown: FieldBreakdown; totalCount: number; index: number }) { const maxCount = Math.max(...breakdown.items.map((i) => i.count), 1); return (
{breakdown.displayName} {breakdown.items.length} 项
{breakdown.items.map((item, idx) => { const percent = totalCount > 0 ? Math.round((item.count / totalCount) * 100) : 0; const barPercent = maxCount > 0 ? Math.round((item.count / maxCount) * 100) : 0; const color = TAG_COLORS[idx % TAG_COLORS.length]; return (
{item.key || '(空)'} {item.count} {percent}%
); })}
{breakdown.items.length === 0 && ( )}
); } /** 骨架屏分布卡片 */ export function SkeletonBreakdownCard({ index }: { index: number }) { return (
); } // ── Widget 图表子组件 ── /** 统计卡片 widget */ function StatWidgetCard({ widgetData }: { widgetData: WidgetData }) { const { widget, count } = widgetData; const animatedValue = useCountUp(count ?? 0); const color = widget.color || '#4F46E5'; return (
{WIDGET_ICON_MAP[widget.type] || }
{widget.title}
{animatedValue.toLocaleString()}
); } /** 柱状图 widget */ function BarWidgetCard({ widgetData, isDark }: { widgetData: WidgetData; isDark: boolean }) { const { widget, data } = widgetData; const chartData = prepareChartData(data, widget.dimension_order); const axisLabelStyle = { fill: isDark ? '#94A3B8' : '#475569' }; return ( {chartData.length > 0 ? ( ) : } ); } /** 饼图 widget */ function PieWidgetCard({ widgetData }: { widgetData: WidgetData }) { const { widget, data } = widgetData; const chartData = data.map((d) => ({ key: d.key, count: d.count })); return ( {chartData.length > 0 ? ( ) : } ); } /** 漏斗图 widget */ function FunnelWidgetCard({ widgetData }: { widgetData: WidgetData }) { const { widget, data } = widgetData; const chartData = prepareChartData(data, widget.dimension_order); return ( {chartData.length > 0 ? ( ) : } ); } /** 折线图 widget */ function LineWidgetCard({ widgetData, isDark }: { widgetData: WidgetData; isDark: boolean }) { const { widget, data } = widgetData; const chartData = prepareChartData(data, widget.dimension_order); const axisLabelStyle = { fill: isDark ? '#94A3B8' : '#475569' }; return ( {chartData.length > 0 ? ( ) : } ); } /** 渲染单个 widget */ export function WidgetRenderer({ widgetData, isDark }: { widgetData: WidgetData; isDark: boolean }) { switch (widgetData.widget.type) { case 'stat_card': return ; case 'bar_chart': return ; case 'pie_chart': return ; case 'funnel_chart': return ; case 'line_chart': return ; default: return null; } }