feat(web): 新增 PluginGraphPage 关系图谱和 PluginDashboardPage 统计概览
- PluginGraphPage: Canvas 2D 绘制客户关系图谱,支持中心节点选择和关系类型筛选 - PluginDashboardPage: 全量数据前端聚合统计,支持按 filterable 字段分组计数 - App.tsx: 注册 /graph/:entityName 和 /dashboard 路由
This commit is contained in:
147
apps/web/src/pages/PluginDashboardPage.tsx
Normal file
147
apps/web/src/pages/PluginDashboardPage.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Row, Col, Statistic, Spin, Empty, Select, Tag } 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件统计概览页面
|
||||
* 使用 listPluginData 加载全量数据前端聚合
|
||||
*/
|
||||
export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedEntity, setSelectedEntity] = useState<string>(
|
||||
entities[0]?.name || '',
|
||||
);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [aggregations, setAggregations] = useState<AggregationResult[]>([]);
|
||||
|
||||
const currentEntity = entities.find((e) => e.name === selectedEntity);
|
||||
const filterableFields = currentEntity?.fields.filter((f) => f.filterable) || [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!pluginId || !selectedEntity) return;
|
||||
setLoading(true);
|
||||
|
||||
async function loadData() {
|
||||
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++;
|
||||
}
|
||||
setTotalCount(total);
|
||||
|
||||
const aggs: AggregationResult[] = [];
|
||||
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 });
|
||||
}
|
||||
}
|
||||
setAggregations(aggs);
|
||||
} catch {
|
||||
// 加载失败
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [pluginId, selectedEntity, filterableFields.length]);
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
customer: <TeamOutlined />,
|
||||
contact: <TeamOutlined />,
|
||||
communication: <PhoneOutlined />,
|
||||
customer_tag: <TagsOutlined />,
|
||||
customer_relationship: <RiseOutlined />,
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 24, textAlign: 'center' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card
|
||||
title="统计概览"
|
||||
size="small"
|
||||
extra={
|
||||
<Select
|
||||
value={selectedEntity}
|
||||
style={{ width: 150 }}
|
||||
options={entities.map((e) => ({
|
||||
label: e.display_name || e.name,
|
||||
value: e.name,
|
||||
}))}
|
||||
onChange={setSelectedEntity}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={currentEntity?.display_name || selectedEntity + ' 总数'}
|
||||
value={totalCount}
|
||||
prefix={iconMap[selectedEntity] || <TeamOutlined />}
|
||||
valueStyle={{ color: '#4F46E5' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{aggregations.length > 0 && (
|
||||
<Col span={24}>
|
||||
<Card title="分组统计" size="small">
|
||||
<Row gutter={[8, 8]}>
|
||||
{aggregations.slice(0, 20).map((agg, idx) => (
|
||||
<Col span={6} key={idx}>
|
||||
<Tag color="blue" style={{ width: '100%', textAlign: 'center' }}>
|
||||
{agg.key}: {agg.count}
|
||||
</Tag>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{aggregations.length === 0 && totalCount === 0 && (
|
||||
<Col span={24}>
|
||||
<Empty description="暂无数据" />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user