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 } 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 { ENTITY_PALETTE, DEFAULT_PALETTE, ENTITY_ICONS, 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 (const entity of entities) { if (abortController.signal.aborted) return; try { const count = await countPluginData(pluginId!, entity.name); if (abortController.signal.aborted) return; const palette = ENTITY_PALETTE[entity.name] || DEFAULT_PALETTE; results.push({ name: entity.name, displayName: entity.display_name || entity.name, count, icon: ENTITY_ICONS[entity.name] || , gradient: palette.gradient, iconBg: palette.iconBg, }); } catch { const palette = ENTITY_PALETTE[entity.name] || DEFAULT_PALETTE; results.push({ name: entity.name, displayName: entity.display_name || entity.name, count: 0, icon: ENTITY_ICONS[entity.name] || , 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 }; } if (widget.dimension_field) { const data = await aggregatePluginData( pluginId!, widget.entity, widget.dimension_field, ); return { widget, data }; } // 没有 dimension_field 时仅返回计数 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( () => ENTITY_PALETTE[selectedEntity] || DEFAULT_PALETTE, [selectedEntity], ); // ── 渲染 ── if (schemaLoading) { return (
{Array.from({ length: 5 }).map((_, i) => ( ))} {Array.from({ length: 3 }).map((_, i) => ( ))}
); } return (
{/* 页面标题 */}

统计概览

CRM 数据全景视图,实时掌握业务动态