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,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>
|
||||
|
||||
Reference in New Issue
Block a user