import { useEffect, useState, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { Tree, Descriptions, Card, Empty, Spin, message } from 'antd'; import type { TreeProps } from 'antd'; import { listPluginData, type PluginDataRecord } from '../api/pluginData'; import { getPluginSchema, type PluginFieldSchema, type PluginPageSchema, type PluginSchemaResponse, } from '../api/plugins'; interface TreeNode { key: string; title: string; children: TreeNode[]; raw: Record; } interface PluginTreePageProps { pluginIdOverride?: string; entityOverride?: string; } /** * 插件树形页面 — 通过路由参数自加载 schema * 路由: /plugins/:pluginId/tree/:entityName * 也支持通过 props 覆盖(用于 tabs 内嵌) */ export function PluginTreePage({ pluginIdOverride, entityOverride }: PluginTreePageProps = {}) { const routeParams = useParams<{ pluginId: string; entityName: string }>(); const pluginId = pluginIdOverride || routeParams.pluginId || ''; const entityName = entityOverride || routeParams.entityName || ''; const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); const [selectedNode, setSelectedNode] = useState(null); const [fields, setFields] = useState([]); const [treeConfig, setTreeConfig] = useState<{ idField: string; parentField: string; labelField: string; } | null>(null); // 加载 schema useEffect(() => { if (!pluginId || !entityName) return; const abortController = new AbortController(); async function loadSchema() { try { const schema: PluginSchemaResponse = await getPluginSchema(pluginId!); if (abortController.signal.aborted) return; const entity = schema.entities?.find((e) => e.name === entityName); if (entity) { setFields(entity.fields); } const pages = schema.ui?.pages || []; const treePage = pages.find( (p): p is PluginPageSchema & { type: 'tree'; entity: string; id_field: string; parent_field: string; label_field: string } => p.type === 'tree' && p.entity === entityName, ); if (treePage) { setTreeConfig({ idField: treePage.id_field, parentField: treePage.parent_field, labelField: treePage.label_field, }); } } catch { message.warning('Schema 加载失败,部分功能不可用'); } } loadSchema(); return () => abortController.abort(); }, [pluginId, entityName]); // 加载数据 useEffect(() => { if (!pluginId || !entityName) return; const abortController = new AbortController(); async function loadAll() { setLoading(true); try { let allRecords: PluginDataRecord[] = []; let page = 1; let hasMore = true; while (hasMore) { if (abortController.signal.aborted) return; const result = await listPluginData(pluginId!, entityName!, page, 100); allRecords = [...allRecords, ...result.data]; hasMore = result.data.length === 100 && allRecords.length < result.total; page++; } if (!abortController.signal.aborted) { setRecords(allRecords); } } catch { message.warning('数据加载失败'); } if (!abortController.signal.aborted) setLoading(false); } loadAll(); return () => abortController.abort(); }, [pluginId, entityName]); const idField = treeConfig?.idField || 'id'; const parentField = treeConfig?.parentField || 'parent_id'; const labelField = treeConfig?.labelField || fields[1]?.name || 'name'; // 构建树结构 const treeData = useMemo(() => { const nodeMap = new Map(); const rootNodes: TreeNode[] = []; for (const record of records) { const data = record.data; const key = String(data[idField] || record.id); const title = String(data[labelField] || '未命名'); nodeMap.set(key, { key, title, children: [], raw: { ...data, _id: record.id, _version: record.version }, }); } for (const record of records) { const data = record.data; const key = String(data[idField] || record.id); const parentKey = data[parentField] ? String(data[parentField]) : null; const node = nodeMap.get(key)!; if (parentKey && nodeMap.has(parentKey)) { nodeMap.get(parentKey)!.children.push(node); } else { rootNodes.push(node); } } return rootNodes; }, [records, idField, parentField, labelField]); const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => { if (selectedKeys.length > 0) { setSelectedNode(info.node as unknown as TreeNode); } else { setSelectedNode(null); } }; if (loading) { return
; } return (
{treeData.length === 0 ? ( ) : ( )}
{selectedNode ? ( {fields.map((field) => ( {String(selectedNode.raw[field.name] ?? '-')} ))} ) : ( )}
); }