diff --git a/apps/web/src/api/pluginData.ts b/apps/web/src/api/pluginData.ts index b1b0e39..bc85bc6 100644 --- a/apps/web/src/api/pluginData.ts +++ b/apps/web/src/api/pluginData.ts @@ -86,3 +86,40 @@ export async function deletePluginData( ) { await client.delete(`/plugins/${pluginId}/${entity}/${id}`); } + +export async function countPluginData( + pluginId: string, + entity: string, + options?: { filter?: Record; search?: string }, +) { + const params: Record = {}; + if (options?.filter) params.filter = JSON.stringify(options.filter); + if (options?.search) params.search = options.search; + + const { data } = await client.get<{ success: boolean; data: number }>( + `/plugins/${pluginId}/${entity}/count`, + { params }, + ); + return data.data; +} + +export interface AggregateItem { + key: string; + count: number; +} + +export async function aggregatePluginData( + pluginId: string, + entity: string, + groupBy: string, + filter?: Record, +) { + const params: Record = { group_by: groupBy }; + if (filter) params.filter = JSON.stringify(filter); + + const { data } = await client.get<{ success: boolean; data: AggregateItem[] }>( + `/plugins/${pluginId}/${entity}/aggregate`, + { params }, + ); + return data.data; +} diff --git a/apps/web/src/api/plugins.ts b/apps/web/src/api/plugins.ts index 1e4b19f..3386f41 100644 --- a/apps/web/src/api/plugins.ts +++ b/apps/web/src/api/plugins.ts @@ -113,8 +113,8 @@ export async function updatePluginConfig(id: string, config: Record }>( +export async function getPluginSchema(id: string): Promise { + const { data } = await client.get<{ success: boolean; data: PluginSchemaResponse }>( `/admin/plugins/${id}/schema`, ); return data.data; @@ -155,7 +155,9 @@ export type PluginPageSchema = | { type: 'crud'; entity: string; label: string; icon?: string; enable_search?: boolean; enable_views?: string[] } | { type: 'tree'; entity: string; label: string; icon?: string; id_field: string; parent_field: string; label_field: string } | { type: 'detail'; entity: string; label: string; sections: PluginSectionSchema[] } - | { type: 'tabs'; label: string; icon?: string; tabs: PluginPageSchema[] }; + | { type: 'tabs'; label: string; icon?: string; tabs: PluginPageSchema[] } + | { type: 'graph'; entity: string; label: string; relationship_entity: string; source_field: string; target_field: string; edge_label_field: string; node_label_field: string } + | { type: 'dashboard'; label: string }; export type PluginSectionSchema = | { type: 'fields'; label: string; fields: string[] } diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index 141ab3b..01f165c 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -18,8 +18,6 @@ import { TeamOutlined, TableOutlined, TagsOutlined, - UserAddOutlined, - ApartmentOutlined as RelationshipIcon, } from '@ant-design/icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAppStore } from '../stores/app'; diff --git a/apps/web/src/pages/PluginAdmin.tsx b/apps/web/src/pages/PluginAdmin.tsx index 0e5e4ff..a08bf30 100644 --- a/apps/web/src/pages/PluginAdmin.tsx +++ b/apps/web/src/pages/PluginAdmin.tsx @@ -21,7 +21,6 @@ import { CloudDownloadOutlined, DeleteOutlined, ReloadOutlined, - AppstoreOutlined, HeartOutlined, } from '@ant-design/icons'; import type { PluginInfo, PluginStatus } from '../api/plugins'; @@ -267,7 +266,7 @@ export default function PluginAdmin() { }} maxCount={1} accept=".wasm" - fileList={wasmFile ? [wasmFile as unknown as Parameters[0]] : []} + fileList={[]} onRemove={() => setWasmFile(null)} > diff --git a/apps/web/src/pages/PluginCRUDPage.tsx b/apps/web/src/pages/PluginCRUDPage.tsx index e34acf5..8180123 100644 --- a/apps/web/src/pages/PluginCRUDPage.tsx +++ b/apps/web/src/pages/PluginCRUDPage.tsx @@ -31,14 +31,14 @@ import { createPluginData, updatePluginData, deletePluginData, - PluginDataListOptions, + type PluginDataListOptions, } from '../api/pluginData'; import { getPluginSchema, - PluginFieldSchema, - PluginEntitySchema, - PluginPageSchema, - PluginSectionSchema, + type PluginFieldSchema, + type PluginEntitySchema, + type PluginPageSchema, + type PluginSectionSchema, } from '../api/plugins'; const { Search } = Input; @@ -133,8 +133,12 @@ export default function PluginCRUDPage({ // 加载 schema useEffect(() => { if (!pluginId) return; - getPluginSchema(pluginId) - .then((schema) => { + const abortController = new AbortController(); + + async function loadSchema() { + try { + const schema = await getPluginSchema(pluginId!); + if (abortController.signal.aborted) return; const entities: PluginEntitySchema[] = (schema as { entities?: PluginEntitySchema[] }).entities || []; setAllEntities(entities); const entity = entities.find((e) => e.name === entityName); @@ -145,7 +149,6 @@ export default function PluginCRUDPage({ const ui = (schema as { ui?: { pages: PluginPageSchema[] } }).ui; if (ui?.pages) { setAllPages(ui.pages); - // 找到 detail 页面的 sections const detailPage = ui.pages.find( (p) => p.type === 'detail' && 'entity' in p && p.entity === entityName, ); @@ -153,19 +156,21 @@ export default function PluginCRUDPage({ setDetailSections(detailPage.sections); } } - }) - .catch(() => { - // schema 加载失败时仍可使用 - }); + } catch { + message.warning('Schema 加载失败,部分功能不可用'); + } + } + + loadSchema(); + return () => abortController.abort(); }, [pluginId, entityName]); const fetchData = useCallback( - async (p = page) => { + async (p = page, overrides?: { search?: string; sort_by?: string; sort_order?: 'asc' | 'desc' }) => { if (!pluginId || !entityName) return; setLoading(true); try { const options: PluginDataListOptions = {}; - // 自动添加 filterField 过滤(detail 页面内嵌 CRUD) const mergedFilters = { ...filters }; if (filterField && filterValue) { mergedFilters[filterField] = filterValue; @@ -173,10 +178,13 @@ export default function PluginCRUDPage({ if (Object.keys(mergedFilters).length > 0) { options.filter = mergedFilters; } - if (searchText) options.search = searchText; - if (sortBy) { - options.sort_by = sortBy; - options.sort_order = sortOrder; + const effectiveSearch = overrides?.search ?? searchText; + if (effectiveSearch) options.search = effectiveSearch; + const effectiveSortBy = overrides?.sort_by ?? sortBy; + const effectiveSortOrder = overrides?.sort_order ?? sortOrder; + if (effectiveSortBy) { + options.sort_by = effectiveSortBy; + options.sort_order = effectiveSortOrder; } const result = await listPluginData(pluginId, entityName, p, 20, options); setRecords( @@ -505,7 +513,7 @@ export default function PluginCRUDPage({ onSearch={(value) => { setSearchText(value); setPage(1); - fetchData(1); + fetchData(1, { search: value }); }} /> )} @@ -529,6 +537,21 @@ export default function PluginCRUDPage({ rowKey="_id" loading={loading} size={compact ? 'small' : undefined} + onChange={(_pagination, _filters, sorter) => { + if (!Array.isArray(sorter) && sorter.field) { + const newSortBy = String(sorter.field); + const newSortOrder = sorter.order === 'ascend' ? 'asc' as const : 'desc' as const; + setSortBy(newSortBy); + setSortOrder(newSortOrder); + setPage(1); + fetchData(1, { sort_by: newSortBy, sort_order: newSortOrder }); + } else if (!sorter || (Array.isArray(sorter) && sorter.length === 0)) { + setSortBy(undefined); + setSortOrder('desc'); + setPage(1); + fetchData(1, { sort_by: undefined, sort_order: undefined }); + } + }} pagination={ compact ? { pageSize: 5, showTotal: (t) => `共 ${t} 条` } diff --git a/apps/web/src/pages/PluginDashboardPage.tsx b/apps/web/src/pages/PluginDashboardPage.tsx index 5aa7e77..108a095 100644 --- a/apps/web/src/pages/PluginDashboardPage.tsx +++ b/apps/web/src/pages/PluginDashboardPage.tsx @@ -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( - entities[0]?.name || '', - ); + const [entities, setEntities] = useState([]); + const [selectedEntity, setSelectedEntity] = useState(''); const [totalCount, setTotalCount] = useState(0); - const [aggregations, setAggregations] = useState([]); + const [aggregations, setAggregations] = useState([]); + + // 加载 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[] = []; - 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(); - 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 = { @@ -83,11 +96,7 @@ export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageP }; if (loading) { - return ( -
- -
- ); + return
; } return ( @@ -97,7 +106,7 @@ export function PluginDashboardPage({ pluginId, entities }: PluginDashboardPageP size="small" extra={