import { useEffect, useState, useMemo, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { Row, Col, Empty, Select, theme } from 'antd'; import { DashboardOutlined } from '@ant-design/icons'; import { countPluginData, aggregatePluginData, listPluginData } from '../api/pluginData'; import { getPluginSchema, type PluginEntitySchema, type PluginSchemaResponse, type PluginPageSchema, type DashboardWidget, } from '../api/plugins'; import type { EntityStat, FieldBreakdown, WidgetData } from './dashboard/dashboardTypes'; import { getEntityPalette, getEntityIcon, getDelayClass } from './dashboard/dashboardConstants'; import { StatCard, SkeletonStatCard, BreakdownCard, SkeletonBreakdownCard, WidgetRenderer, } from './dashboard/DashboardWidgets'; // ── 主组件 ── export function PluginDashboardPage() { const { pluginId } = useParams<{ pluginId: string }>(); const { token: themeToken } = theme.useToken(); const [loading, setLoading] = useState(false); const [schemaLoading, setSchemaLoading] = useState(false); const [entities, setEntities] = useState([]); const [selectedEntity, setSelectedEntity] = useState(''); const [entityStats, setEntityStats] = useState([]); const [breakdowns, setBreakdowns] = useState([]); const [error, setError] = useState(null); // Widget-based dashboard state const [widgets, setWidgets] = useState([]); const [widgetData, setWidgetData] = useState([]); const [widgetsLoading, setWidgetsLoading] = useState(false); const isDark = themeToken.colorBgContainer === '#111827' || themeToken.colorBgContainer === 'rgb(17, 24, 39)'; // 加载 schema useEffect(() => { if (!pluginId) return; const abortController = new AbortController(); async function loadSchema() { setSchemaLoading(true); setError(null); try { const schema: PluginSchemaResponse = await getPluginSchema(pluginId!); if (abortController.signal.aborted) return; const entityList = schema.entities || []; setEntities(entityList); if (entityList.length > 0) { setSelectedEntity(entityList[0].name); } // 提取 dashboard widgets const pages = schema.ui?.pages || []; const dashboardPage = pages.find( (p): p is PluginPageSchema & { type: 'dashboard'; widgets?: DashboardWidget[] } => p.type === 'dashboard', ); if (dashboardPage?.widgets && dashboardPage.widgets.length > 0) { setWidgets(dashboardPage.widgets); } } catch { setError('Schema 加载失败,部分功能不可用'); } finally { if (!abortController.signal.aborted) setSchemaLoading(false); } } loadSchema(); return () => abortController.abort(); }, [pluginId]); const currentEntity = useMemo( () => entities.find((e) => e.name === selectedEntity), [entities, selectedEntity], ); const filterableFields = useMemo( () => currentEntity?.fields.filter((f) => f.filterable) || [], [currentEntity], ); // 加载所有实体的计数 useEffect(() => { if (!pluginId || entities.length === 0) return; const abortController = new AbortController(); async function loadAllCounts() { const results: EntityStat[] = []; for (let i = 0; i < entities.length; i++) { const entity = entities[i]; if (abortController.signal.aborted) return; const palette = getEntityPalette(entity.name, i); const icon = getEntityIcon(entity.name); try { const count = await countPluginData(pluginId!, entity.name); if (abortController.signal.aborted) return; results.push({ name: entity.name, displayName: entity.display_name || entity.name, count, icon, gradient: palette.gradient, iconBg: palette.iconBg, }); } catch { results.push({ name: entity.name, displayName: entity.display_name || entity.name, count: 0, icon, gradient: palette.gradient, iconBg: palette.iconBg, }); } } if (!abortController.signal.aborted) { setEntityStats(results); } } loadAllCounts(); return () => abortController.abort(); }, [pluginId, entities]); // Widget 数据并行加载 useEffect(() => { if (!pluginId || widgets.length === 0) return; const abortController = new AbortController(); async function loadWidgetData() { setWidgetsLoading(true); try { const results = await Promise.all( widgets.map(async (widget) => { try { // 旧类型 if (widget.type === 'stat_card') { const count = await countPluginData(pluginId!, widget.entity); return { widget, data: [], count }; } // stat_cards — 多个统计卡片 if (widget.type === 'stat_cards' && widget.cards) { const cardResults = await Promise.all( widget.cards.map(async (card) => { try { const count = await countPluginData(pluginId!, card.entity, { filter: card.filter ? JSON.parse(card.filter) : undefined, }); return { card, value: count }; } catch { return { card, value: 0 }; } }), ); return { widget, data: [], statCards: cardResults }; } // action_list — 待办列表 if (widget.type === 'action_list' && widget.queries) { const actionResults = await Promise.all( widget.queries.map(async (query) => { try { const filterObj = query.filter ? JSON.parse(query.filter) : undefined; const sortParts = query.sort?.split(' ') ?? []; const result = await listPluginData(pluginId!, query.entity, 1, widget.max_items ?? 10, { filter: filterObj, sort_by: sortParts[0] || undefined, sort_order: (sortParts[1] as 'asc' | 'desc') || undefined, }); return { query, records: result.data }; } catch { return { query, records: [] }; } }), ); return { widget, data: [], actionItems: actionResults }; } // funnel — 阶段漏斗 if (widget.type === 'funnel' && widget.lane_field) { const data = await aggregatePluginData( pluginId!, widget.entity, widget.lane_field, ); return { widget, data }; } // card_list — 卡片列表 if (widget.type === 'card_list') { const filterObj = widget.filter ? JSON.parse(widget.filter) : undefined; const result = await listPluginData(pluginId!, widget.entity, 1, widget.max_items ?? 10, { filter: filterObj, }); return { widget, data: [], records: result.data }; } // 旧类型图表 if (widget.dimension_field) { const data = await aggregatePluginData( pluginId!, widget.entity, widget.dimension_field, ); return { widget, data }; } // fallback — 仅返回计数 const count = await countPluginData(pluginId!, widget.entity); return { widget, data: [], count }; } catch { return { widget, data: [], count: 0 }; } }), ); if (!abortController.signal.aborted) { setWidgetData(results); } } finally { if (!abortController.signal.aborted) setWidgetsLoading(false); } } loadWidgetData(); return () => abortController.abort(); }, [pluginId, widgets]); // 当前实体的聚合数据 const loadData = useCallback(async () => { if (!pluginId || !selectedEntity || filterableFields.length === 0) return; const abortController = new AbortController(); setLoading(true); setError(null); try { const fieldResults: FieldBreakdown[] = []; for (const field of filterableFields) { if (abortController.signal.aborted) return; try { const items = await aggregatePluginData(pluginId!, selectedEntity!, field.name); if (abortController.signal.aborted) return; fieldResults.push({ fieldName: field.name, displayName: field.display_name || field.name, items, }); } catch { // 单个字段聚合失败不影响其他字段 } } if (!abortController.signal.aborted) setBreakdowns(fieldResults); } catch { setError('统计数据加载失败'); } finally { if (!abortController.signal.aborted) setLoading(false); } return () => abortController.abort(); }, [pluginId, selectedEntity, filterableFields, entityStats]); useEffect(() => { const cleanup = loadData(); return () => { cleanup?.then((fn) => fn?.()).catch(() => {}); }; }, [loadData]); // 当前选中实体的总数 const currentTotal = useMemo( () => entityStats.find((s) => s.name === selectedEntity)?.count ?? 0, [entityStats, selectedEntity], ); // 当前实体的色板 const currentPalette = useMemo( () => getEntityPalette(selectedEntity, entities.findIndex((e) => e.name === selectedEntity)), [selectedEntity, entities], ); // ── 渲染 ── if (schemaLoading) { return (
{Array.from({ length: 5 }).map((_, i) => ( ))} {Array.from({ length: 3 }).map((_, i) => ( ))}
); } return (
{/* 页面标题 */}

统计概览

{pluginId ? `${pluginId.toUpperCase()} 数据统计` : '数据统计'}