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:
iven
2026-04-16 23:42:57 +08:00
parent 3483395f5e
commit ae62e2ecb2
10 changed files with 401 additions and 217 deletions

View File

@@ -1,46 +1,67 @@
import { useState } from 'react';
import { Tabs } from 'antd';
import {
PluginPageSchema,
PluginEntitySchema,
PluginFieldSchema,
} from '../api/plugins';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Tabs, Spin, message } from 'antd';
import { getPluginSchema, type PluginPageSchema, type PluginSchemaResponse } from '../api/plugins';
import PluginCRUDPage from './PluginCRUDPage';
import { PluginTreePage } from './PluginTreePage';
interface PluginTabsPageProps {
pluginId: string;
label: string;
icon?: string;
tabs: PluginPageSchema[];
entities: PluginEntitySchema[];
}
/**
* 插件 Tabs 页面 — 通过路由参数自加载 schema
* 路由: /plugins/:pluginId/tabs/:pageLabel
*/
export function PluginTabsPage() {
const { pluginId, pageLabel } = useParams<{ pluginId: string; pageLabel: string }>();
const [loading, setLoading] = useState(true);
const [tabs, setTabs] = useState<PluginPageSchema[]>([]);
const [activeKey, setActiveKey] = useState('');
export function PluginTabsPage({ pluginId, label, tabs, entities }: PluginTabsPageProps) {
const [activeKey, setActiveKey] = useState(tabs[0] && 'label' in tabs[0] ? tabs[0].label : '');
useEffect(() => {
if (!pluginId || !pageLabel) return;
const abortController = new AbortController();
async function loadSchema() {
try {
const schema: PluginSchemaResponse = await getPluginSchema(pluginId!);
const pages = schema.ui?.pages || [];
const tabsPage = pages.find(
(p): p is PluginPageSchema & { type: 'tabs' } =>
p.type === 'tabs' && p.label === pageLabel,
);
if (tabsPage && 'tabs' in tabsPage) {
setTabs(tabsPage.tabs);
const firstLabel = tabsPage.tabs.find((t) => 'label' in t)?.label || '';
setActiveKey(firstLabel);
}
} catch {
message.warning('Schema 加载失败,部分功能不可用');
} finally {
if (!abortController.signal.aborted) setLoading(false);
}
}
loadSchema();
return () => abortController.abort();
}, [pluginId, pageLabel]);
if (loading) {
return <div style={{ padding: 24, textAlign: 'center' }}><Spin /></div>;
}
const renderTabContent = (tab: PluginPageSchema) => {
if (tab.type === 'crud') {
// 懒加载 PluginCRUDPage 避免循环依赖
const PluginCRUDPage = require('./PluginCRUDPage').default;
return (
<PluginCRUDPage
pluginIdOverride={pluginId}
entityOverride={tab.entity}
enableSearch={tab.enable_search}
enableViews={tab.enable_views}
/>
);
}
if (tab.type === 'tree') {
const PluginTreePage = require('./PluginTreePage').PluginTreePage;
const entity = entities.find((e) => e.name === tab.entity);
return (
<PluginTreePage
pluginId={pluginId}
entity={tab.entity}
idField={tab.id_field}
parentField={tab.parent_field}
labelField={tab.label_field}
fields={entity?.fields || []}
pluginIdOverride={pluginId}
entityOverride={tab.entity}
/>
);
}