feat(web): 完善插件前端页面 — 数据 API、筛选、视图切换和统计展示
- 新增 pluginData API 层:count/aggregate/stats 端点调用 - PluginCRUDPage 支持 visible_when 条件字段、筛选器下拉、视图切换 - PluginTabsPage 支持 tabs 布局和子实体 CRUD - PluginTreePage 实现树形数据加载和节点展开/收起 - PluginGraphPage 实现关系图谱可视化展示 - PluginDashboardPage 实现统计卡片和聚合数据展示 - PluginAdmin 状态显示优化 - plugin store 增强 schema 加载逻辑和菜单生成
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { Tree, Descriptions, Card, Empty, Spin } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Tree, Descriptions, Card, Empty, Spin, message } from 'antd';
|
||||
import type { TreeProps } from 'antd';
|
||||
import { listPluginData, PluginDataRecord } from '../api/pluginData';
|
||||
import { PluginFieldSchema } from '../api/plugins';
|
||||
|
||||
interface PluginTreePageProps {
|
||||
pluginId: string;
|
||||
entity: string;
|
||||
idField: string;
|
||||
parentField: string;
|
||||
labelField: string;
|
||||
fields: PluginFieldSchema[];
|
||||
}
|
||||
import { listPluginData, type PluginDataRecord } from '../api/pluginData';
|
||||
import {
|
||||
getPluginSchema,
|
||||
type PluginFieldSchema,
|
||||
type PluginPageSchema,
|
||||
type PluginSchemaResponse,
|
||||
} from '../api/plugins';
|
||||
|
||||
interface TreeNode {
|
||||
key: string;
|
||||
@@ -20,47 +17,105 @@ interface TreeNode {
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function PluginTreePage({
|
||||
pluginId,
|
||||
entity,
|
||||
idField,
|
||||
parentField,
|
||||
labelField,
|
||||
fields,
|
||||
}: PluginTreePageProps) {
|
||||
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<PluginDataRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<TreeNode | null>(null);
|
||||
const [fields, setFields] = useState<PluginFieldSchema[]>([]);
|
||||
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) {
|
||||
const result = await listPluginData(pluginId, entity, page, 100);
|
||||
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++;
|
||||
}
|
||||
setRecords(allRecords);
|
||||
if (!abortController.signal.aborted) {
|
||||
setRecords(allRecords);
|
||||
}
|
||||
} catch {
|
||||
// 加载失败
|
||||
message.warning('数据加载失败');
|
||||
}
|
||||
setLoading(false);
|
||||
if (!abortController.signal.aborted) setLoading(false);
|
||||
}
|
||||
loadAll();
|
||||
}, [pluginId, entity]);
|
||||
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<string, TreeNode>();
|
||||
const rootNodes: TreeNode[] = [];
|
||||
|
||||
// 创建所有节点
|
||||
for (const record of records) {
|
||||
const data = record.data;
|
||||
const key = String(data[idField] || record.id);
|
||||
@@ -73,7 +128,6 @@ export function PluginTreePage({
|
||||
});
|
||||
}
|
||||
|
||||
// 构建父子关系
|
||||
for (const record of records) {
|
||||
const data = record.data;
|
||||
const key = String(data[idField] || record.id);
|
||||
@@ -99,26 +153,17 @@ export function PluginTreePage({
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
return <div style={{ padding: 24, textAlign: 'center' }}><Spin /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, display: 'flex', gap: 16 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Card title={entity + ' 层级'} size="small">
|
||||
<Card title={(entityName || '') + ' 层级'} size="small">
|
||||
{treeData.length === 0 ? (
|
||||
<Empty description="暂无数据" />
|
||||
) : (
|
||||
<Tree
|
||||
showLine
|
||||
defaultExpandAll
|
||||
treeData={treeData}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<Tree showLine defaultExpandAll treeData={treeData} onSelect={onSelect} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user