feat(crm): 创建 CRM 插件 crate + 前端 tabs/tree 页面类型 + 动态菜单

- CRM WASM 插件:Cargo.toml + src/lib.rs + plugin.toml(5 实体 + 9 权限 + 6 页面)
- 注册 erp-plugin-crm 到 workspace members
- PluginTabsPage: 通用标签页容器,递归渲染子页面
- PluginTreePage: 通用树形页面,前端构建树结构
- App.tsx: 新增 /tabs/:pageLabel 和 /tree/:entityName 路由
- plugin store: 从 manifest pages 生成菜单(支持 tabs 聚合)
- MainLayout: 动态图标映射(team/user/message/tags/apartment)
This commit is contained in:
iven
2026-04-16 12:41:17 +08:00
parent e68fe8c1b1
commit 92789e6713
9 changed files with 760 additions and 26 deletions

View File

@@ -1,13 +1,14 @@
import { create } from 'zustand';
import type { PluginInfo, PluginStatus } from '../api/plugins';
import { listPlugins } from '../api/plugins';
import type { PluginInfo, PluginStatus, PluginPageSchema, PluginSchemaResponse } from '../api/plugins';
import { listPlugins, getPluginSchema } from '../api/plugins';
export interface PluginMenuItem {
key: string;
icon: string;
label: string;
pluginId: string;
entity: string;
entity?: string;
pageType: 'crud' | 'tree' | 'tabs' | 'detail';
menuGroup?: string;
}
@@ -15,6 +16,7 @@ interface PluginStore {
plugins: PluginInfo[];
loading: boolean;
pluginMenuItems: PluginMenuItem[];
schemaCache: Record<string, PluginSchemaResponse>;
fetchPlugins: (page?: number, status?: PluginStatus) => Promise<void>;
refreshMenuItems: () => void;
}
@@ -23,12 +25,25 @@ export const usePluginStore = create<PluginStore>((set, get) => ({
plugins: [],
loading: false,
pluginMenuItems: [],
schemaCache: {},
fetchPlugins: async (page = 1, status?: PluginStatus) => {
set({ loading: true });
try {
const result = await listPlugins(page, 100, status);
set({ plugins: result.data });
// 预加载所有运行中插件的 schema
const schemas: Record<string, PluginSchemaResponse> = {};
for (const plugin of result.data) {
if (plugin.status !== 'running' && plugin.status !== 'enabled') continue;
try {
schemas[plugin.id] = await getPluginSchema(plugin.id) as PluginSchemaResponse;
} catch {
// schema 加载失败跳过
}
}
set({ schemaCache: schemas });
get().refreshMenuItems();
} finally {
set({ loading: false });
@@ -36,21 +51,59 @@ export const usePluginStore = create<PluginStore>((set, get) => ({
},
refreshMenuItems: () => {
const { plugins } = get();
const { plugins, schemaCache } = get();
const items: PluginMenuItem[] = [];
for (const plugin of plugins) {
if (plugin.status !== 'running' && plugin.status !== 'enabled') continue;
for (const entity of plugin.entities) {
items.push({
key: `/plugins/${plugin.id}/${entity.name}`,
icon: 'AppstoreOutlined',
label: entity.display_name || entity.name,
pluginId: plugin.id,
entity: entity.name,
menuGroup: undefined,
});
const schema = schemaCache[plugin.id];
const pages = (schema as { ui?: { pages: PluginPageSchema[] } })?.ui?.pages;
if (pages && pages.length > 0) {
for (const page of pages) {
if (page.type === 'tabs') {
// tabs 类型聚合为一个菜单项
items.push({
key: `/plugins/${plugin.id}/tabs/${encodeURIComponent('label' in page ? page.label : '')}`,
icon: ('icon' in page ? page.icon : 'AppstoreOutlined') || 'AppstoreOutlined',
label: ('label' in page ? page.label : plugin.name) as string,
pluginId: plugin.id,
pageType: 'tabs',
});
} else if (page.type === 'tree') {
items.push({
key: `/plugins/${plugin.id}/tree/${page.entity}`,
icon: ('icon' in page ? page.icon : 'ApartmentOutlined') || 'ApartmentOutlined',
label: ('label' in page ? page.label : page.entity) as string,
pluginId: plugin.id,
entity: page.entity,
pageType: 'tree',
});
} else if (page.type === 'crud') {
items.push({
key: `/plugins/${plugin.id}/${page.entity}`,
icon: ('icon' in page ? page.icon : 'TableOutlined') || 'TableOutlined',
label: ('label' in page ? page.label : page.entity) as string,
pluginId: plugin.id,
entity: page.entity,
pageType: 'crud',
});
}
// detail 类型不生成菜单项
}
} else {
// 回退:从 entities 生成菜单
for (const entity of plugin.entities) {
items.push({
key: `/plugins/${plugin.id}/${entity.name}`,
icon: 'AppstoreOutlined',
label: entity.display_name || entity.name,
pluginId: plugin.id,
entity: entity.name,
pageType: 'crud',
});
}
}
}