- PluginGraphPage: Canvas 2D 绘制客户关系图谱,支持中心节点选择和关系类型筛选 - PluginDashboardPage: 全量数据前端聚合统计,支持按 filterable 字段分组计数 - App.tsx: 注册 /graph/:entityName 和 /dashboard 路由
148 lines
4.3 KiB
TypeScript
148 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|