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,77 +1,90 @@
import { useEffect, useState } from 'react';
import { Card, Row, Col, Statistic, Spin, Empty, Select, Tag } from 'antd';
import { useParams } from 'react-router-dom';
import { Card, Row, Col, Statistic, Spin, Empty, Select, Tag, message } from 'antd';
import {
TeamOutlined,
RiseOutlined,
PhoneOutlined,
TagsOutlined,
} from '@ant-design/icons';
import { listPluginData } from '../api/pluginData';
import { PluginFieldSchema, PluginEntitySchema } from '../api/plugins';
interface PluginDashboardPageProps {
pluginId: string;
entities: PluginEntitySchema[];
}
interface AggregationResult {
key: string;
count: number;
}
import { countPluginData, aggregatePluginData, type AggregateItem } from '../api/pluginData';
import { getPluginSchema, type PluginEntitySchema, type PluginSchemaResponse } from '../api/plugins';
/**
* 插件统计概览页面
* 使用 listPluginData 加载全量数据前端聚合
* 插件统计概览页面 — 通过路由参数自加载 schema使用后端 aggregate API
* 路由: /plugins/:pluginId/dashboard
*/
export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageProps) {
export function PluginDashboardPage() {
const { pluginId } = useParams<{ pluginId: string }>();
const [loading, setLoading] = useState(false);
const [selectedEntity, setSelectedEntity] = useState<string>(
entities[0]?.name || '',
);
const [entities, setEntities] = useState<PluginEntitySchema[]>([]);
const [selectedEntity, setSelectedEntity] = useState<string>('');
const [totalCount, setTotalCount] = useState(0);
const [aggregations, setAggregations] = useState<AggregationResult[]>([]);
const [aggregations, setAggregations] = useState<AggregateItem[]>([]);
// 加载 schema 获取 entities
useEffect(() => {
if (!pluginId) return;
const abortController = new AbortController();
async function loadSchema() {
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);
}
} catch {
message.warning('Schema 加载失败,部分功能不可用');
}
}
loadSchema();
return () => abortController.abort();
}, [pluginId]);
const currentEntity = entities.find((e) => e.name === selectedEntity);
const filterableFields = currentEntity?.fields.filter((f) => f.filterable) || [];
// 使用后端 count/aggregate API
useEffect(() => {
if (!pluginId || !selectedEntity) return;
setLoading(true);
const abortController = new AbortController();
async function loadData() {
setLoading(true);
try {
let allData: Record<string, unknown>[] = [];
let page = 1;
let hasMore = true;
let total = 0;
while (hasMore) {
const result = await listPluginData(pluginId, selectedEntity!, page, 200);
allData = [...allData, ...result.data.map((r) => r.data)];
total = result.total;
hasMore = result.data.length === 200 && allData.length < result.total;
page++;
}
const total = await countPluginData(pluginId!, selectedEntity!);
if (abortController.signal.aborted) return;
setTotalCount(total);
const aggs: AggregationResult[] = [];
const aggs: AggregateItem[] = [];
for (const field of filterableFields) {
const grouped = new Map<string, number>();
for (const item of allData) {
const val = String(item[field.name] ?? '(空)');
grouped.set(val, (grouped.get(val) || 0) + 1);
}
for (const [key, count] of grouped) {
aggs.push({ key: `${field.display_name || field.name}: ${key}`, count });
if (abortController.signal.aborted) return;
try {
const items = await aggregatePluginData(pluginId!, selectedEntity!, field.name);
for (const item of items) {
aggs.push({
key: `${field.display_name || field.name}: ${item.key || '(空)'}`,
count: item.count,
});
}
} catch {
// 单个字段聚合失败不影响其他字段
}
}
if (abortController.signal.aborted) return;
setAggregations(aggs);
} catch {
// 加载失败
message.warning('统计数据加载失败');
}
setLoading(false);
if (!abortController.signal.aborted) setLoading(false);
}
loadData();
return () => abortController.abort();
}, [pluginId, selectedEntity, filterableFields.length]);
const iconMap: Record<string, React.ReactNode> = {
@@ -83,11 +96,7 @@ export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageP
};
if (loading) {
return (
<div style={{ padding: 24, textAlign: 'center' }}>
<Spin />
</div>
);
return <div style={{ padding: 24, textAlign: 'center' }}><Spin /></div>;
}
return (
@@ -97,7 +106,7 @@ export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageP
size="small"
extra={
<Select
value={selectedEntity}
value={selectedEntity || undefined}
style={{ width: 150 }}
options={entities.map((e) => ({
label: e.display_name || e.name,
@@ -111,9 +120,9 @@ export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageP
<Col span={24}>
<Card>
<Statistic
title={currentEntity?.display_name || selectedEntity + ' 总数'}
title={currentEntity?.display_name || (selectedEntity ? selectedEntity + ' 总数' : '总数')}
value={totalCount}
prefix={iconMap[selectedEntity] || <TeamOutlined />}
prefix={selectedEntity ? (iconMap[selectedEntity] || <TeamOutlined />) : <TeamOutlined />}
valueStyle={{ color: '#4F46E5' }}
/>
</Card>