import { useMemo, useEffect } from 'react'; import { Responsive, WidthProvider } from 'react-grid-layout/legacy'; import { useDashboardStore } from '../store/dashboardStore'; import { useProjectStore } from '../store/projectStore'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { X } from "lucide-react"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line } from 'recharts'; import { VegaChart } from "@/components/VegaChart"; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; const CHART_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']; const TABLE_PREVIEW_LIMIT = 20; function isNumericValue(value: unknown) { if (typeof value === 'number') return Number.isFinite(value); if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) return false; const parsed = Number(trimmed); return Number.isFinite(parsed); } return false; } function inferChartKeys(data: Record[]) { if (data.length === 0) { return { xKey: null as string | null, yKeys: [] as string[] }; } const allKeys = Object.keys(data[0] || {}); if (allKeys.length === 0) { return { xKey: null as string | null, yKeys: [] as string[] }; } const preferredX = ['name', 'date', 'time', 'category', 'label']; const xKey = preferredX.find((k) => allKeys.includes(k)) || allKeys[0]; const candidateY = allKeys.filter((k) => k !== xKey); const numericY = candidateY.filter((key) => data.some((row) => isNumericValue(row[key]))); const yKeys = (numericY.length > 0 ? numericY : candidateY).slice(0, 3); return { xKey, yKeys }; } export function Dashboard() { const { charts, removeChart, updateLayout, loadCharts } = useDashboardStore(); const { currentProject } = useProjectStore(); useEffect(() => { if (currentProject) { loadCharts(currentProject.id); } }, [currentProject, loadCharts]); const ResponsiveGridLayout = useMemo( () => WidthProvider(Responsive as any) as any, [] ); const layouts = useMemo(() => ({ lg: charts.map((c) => c.layout) }), [charts]); const onLayoutChange = (currentLayout: any[]) => { if (currentProject) { updateLayout( currentLayout.map((item) => ({ i: item.i, x: item.x, y: item.y, w: item.w, h: item.h, })), currentProject.id ); } }; if (!currentProject) { return (

请选择一个项目以查看仪表板。

); } if (charts.length === 0) { return (

当前项目暂无图表。

前往对话页并添加可视化结果!

); } return (

Dashboard

{charts.map((chart) => { const rows = chart.data as Record[]; const columns = Object.keys(rows[0] || {}); const previewRows = chart.type === "table" ? rows.slice(0, TABLE_PREVIEW_LIMIT) : rows; const isTableTruncated = chart.type === "table" && rows.length > TABLE_PREVIEW_LIMIT; return (
{chart.title} {chart.type === "table" ? `TABLE · ${rows.length} 行 · ${columns.length} 列` : `${chart.type.toUpperCase()} Chart`}
{(() => { if (chart.type === "table") { if (rows.length === 0) { return (
当前表格没有可展示数据
); } if (columns.length === 0) { return (
当前表格数据缺少可展示字段
); } return (
{isTableTruncated ? `预览前 ${TABLE_PREVIEW_LIMIT} 行 / 共 ${rows.length} 行,${columns.length} 列` : `共 ${rows.length} 行,${columns.length} 列`}
{columns.map((col) => {col})} {previewRows.map((row, i) => ( {columns.map((col) => ( {String(row[col] ?? "")} ))} ))}
); } if (chart.chartSpec && rows.length > 0) { return (
); } const { xKey, yKeys } = inferChartKeys(rows); if (!xKey || yKeys.length === 0) { return (
当前图表数据缺少可绘制字段
); } return ( {chart.type === 'bar' ? ( {yKeys.map((key, index) => ( ))} ) : ( {yKeys.map((key, index) => ( ))} )} ); })()}
)})}
); }