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

@@ -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}` }