import { useState, useMemo, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; import { X, Type, AlignLeft, AlignCenter, AlignRight, Bold, Italic, Underline } 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 { t } = useTranslation(); const { dashboards, activeDashboardId, removeChart, updateLayout, loadDashboards, renameDashboard, updateDashboardTitleStyle } = useDashboardStore(); const { currentProject } = useProjectStore(); const [isEditingTitle, setIsEditingTitle] = useState(false); const [editTitle, setEditTitle] = useState(""); const titleInputRef = useRef(null); useEffect(() => { if (currentProject) { loadDashboards(currentProject.id); } }, [currentProject, loadDashboards]); const activeDashboard = useMemo(() => { return dashboards.find((d) => d.id === activeDashboardId) || dashboards[0] || null; }, [dashboards, activeDashboardId]); const charts = activeDashboard?.charts || []; 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 && activeDashboard) { updateLayout( currentLayout.map((item) => ({ i: item.i, x: item.x, y: item.y, w: item.w, h: item.h, })), activeDashboard.id, currentProject.id ); } }; const handleTitleSubmit = () => { if (activeDashboard && currentProject && editTitle.trim()) { renameDashboard(activeDashboard.id, editTitle.trim(), currentProject.id); } setIsEditingTitle(false); }; const handleStyleChange = (key: string, value: string) => { if (activeDashboard && currentProject) { updateDashboardTitleStyle(activeDashboard.id, { [key]: value }, currentProject.id); } }; if (!currentProject) { return (

{t('selectProjectToViewDashboard')}

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

{t('noDashboardsInCurrentProject')}

{t('createDashboardToGetStarted')}

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

{t('noChartsInCurrentProject')}

{t('goToChatToAddCharts')}

); } return (
{isEditingTitle ? ( setEditTitle(e.target.value)} onBlur={handleTitleSubmit} onKeyDown={(e) => { if (e.key === 'Enter') handleTitleSubmit(); if (e.key === 'Escape') setIsEditingTitle(false); }} className="text-2xl font-bold h-auto py-1 px-2 -ml-2 bg-transparent border-transparent hover:border-zinc-200 focus:border-indigo-500 focus:ring-indigo-500 max-w-md" /> ) : (

{ setEditTitle(activeDashboard.name || t('dashboardMenu')); setIsEditingTitle(true); setTimeout(() => titleInputRef.current?.focus(), 0); }} > {activeDashboard.name || t('dashboardMenu')}

{['inherit', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6'].map(color => (
)}
{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" ? t('tableRowColDesc', { rowCount: rows.length, colCount: columns.length }) : `${chart.type.toUpperCase()} Chart`}
{(() => { if (chart.type === "table") { if (rows.length === 0) { return (
当前表格没有可展示数据
); } if (columns.length === 0) { return (
当前表格数据缺少可展示字段
); } return (
{isTableTruncated ? t('previewTableRows', { previewLimit: TABLE_PREVIEW_LIMIT, rowCount: rows.length, colCount: columns.length }) : t('totalTableRows', { rowCount: rows.length, colCount: 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) => ( ))} )} ); })()}
)})}
); }