diff --git a/.gitignore b/.gitignore index 8475459..96f653b 100644 --- a/.gitignore +++ b/.gitignore @@ -191,9 +191,6 @@ web/apps/web-ele/src/views/online-dev/* !web/apps/web-ele/src/views/online-dev/page-render/ !web/apps/web-ele/src/views/online-dev/page-render/** web/apps/web-ele/src/views/dashboard/ -!web/apps/web-ele/src/views/dashboard/ -web/apps/web-ele/src/views/dashboard/* -!web/apps/web-ele/src/views/dashboard/index.vue web/apps/web-ele/src/views/demos/ web/apps/web-ele/src/views/zq-smart-table/ web/apps/web-ele/src/api/online-dev/ diff --git a/backend-fastapi/core/menu/service.py b/backend-fastapi/core/menu/service.py index ea90760..39c0904 100644 --- a/backend-fastapi/core/menu/service.py +++ b/backend-fastapi/core/menu/service.py @@ -17,7 +17,7 @@ from core.menu.schema import MenuCreate, MenuUpdate menu_cache = CacheManager(prefix="menu:") # 缓存key -AI_AGENT_ADMIN_MENU_CACHE_VERSION = "v7" +AI_AGENT_ADMIN_MENU_CACHE_VERSION = "v8" MENU_TREE_CACHE_KEY = f"tree:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}" USER_ROUTE_CACHE_PREFIX = f"user_route:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}:" @@ -52,10 +52,10 @@ def _filter_ai_agent_admin_menus(menus: List[Menu]) -> List[Menu]: def _normalize_ai_agent_admin_menu(menu: Menu) -> None: - """Route the retained control center to the lightweight native dashboard.""" + """Route the retained control center to the original rendered dashboard.""" if menu.name == "ControlCenter": - menu.path = "/dashboard" - menu.component = "dashboard/index" + menu.path = "/page-render/main_home" + menu.component = "online-dev/page-render/index" menu.query = None diff --git a/backend-fastapi/core/role/service.py b/backend-fastapi/core/role/service.py index 2a77b39..47dfbb5 100644 --- a/backend-fastapi/core/role/service.py +++ b/backend-fastapi/core/role/service.py @@ -14,6 +14,18 @@ from core.role.model import Role from core.role.schema import RoleCreate, RoleUpdate +def _filter_ai_agent_admin_role_menus(menus: List[Any]) -> List[Any]: + from core.menu.service import ( + AI_AGENT_ADMIN_MENU_NAMES, + _normalize_ai_agent_admin_menu, + ) + + filtered = [menu for menu in menus if menu.name in AI_AGENT_ADMIN_MENU_NAMES] + for menu in filtered: + _normalize_ai_agent_admin_menu(menu) + return filtered + + class RoleService(BaseService[Role, RoleCreate, RoleUpdate]): """ 角色服务层 @@ -430,26 +442,30 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]): # 菜单始终全量替换 from core.menu.model import Menu result = await db.execute(select(Menu).where(Menu.id.in_(menu_ids))) - menus = list(result.scalars().all()) + menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all())) + allowed_menu_ids = {menu.id for menu in menus} role.menus = menus from core.permission.model import Permission if loaded_menu_ids: # 增量更新:只替换已加载菜单下的权限 - loaded_menu_set = set(loaded_menu_ids) + loaded_menu_set = set(loaded_menu_ids) & allowed_menu_ids # 保留未加载菜单下的原有权限 kept_permissions = [ p for p in (role.permissions or []) - if p.menu_id not in loaded_menu_set + if p.menu_id in allowed_menu_ids and p.menu_id not in loaded_menu_set ] # 获取前端提交的权限(已加载菜单下用户选择的权限) new_permissions = [] if permission_ids: result = await db.execute( - select(Permission).where(Permission.id.in_(permission_ids)) + select(Permission).where( + Permission.id.in_(permission_ids), + Permission.menu_id.in_(allowed_menu_ids), + ) ) new_permissions = list(result.scalars().all()) @@ -459,7 +475,10 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]): # 兼容旧逻辑:全量替换 if permission_ids: result = await db.execute( - select(Permission).where(Permission.id.in_(permission_ids)) + select(Permission).where( + Permission.id.in_(permission_ids), + Permission.menu_id.in_(allowed_menu_ids), + ) ) permissions = list(result.scalars().all()) else: @@ -543,17 +562,20 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]): # 获取所有菜单 result = await db.execute(select(Menu).where(Menu.is_deleted == False)) # noqa: E712 - all_menus = list(result.scalars().all()) + all_menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all())) + allowed_menu_ids = {menu.id for menu in all_menus} # 获取该角色已分配的权限ID和菜单ID role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set() - role_menu_ids = set(m.id for m in role.menus) if role.menus else set() + role_menu_ids = {m.id for m in (role.menus or []) if m.id in allowed_menu_ids} # 获取所有启用的权限 result = await db.execute( select(Permission).where(Permission.is_active == True, Permission.is_deleted == False) # noqa: E712 ) - all_permissions = list(result.scalars().all()) + all_permissions = [ + perm for perm in list(result.scalars().all()) if perm.menu_id in allowed_menu_ids + ] # 权限类型映射 PERMISSION_TYPE_MAP = { @@ -637,7 +659,9 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]): if application_id: query = query.where(Menu.application_id == application_id) result = await db.execute(query) - all_menus = list(result.scalars().all()) + all_menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all())) + allowed_menu_ids = {menu.id for menu in all_menus} + role_menu_ids = {menu_id for menu_id in role_menu_ids if menu_id in allowed_menu_ids} # 统计每个菜单的权限数量 permission_counts = {} @@ -685,10 +709,20 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]): @classmethod async def get_menu_permissions(cls, db: AsyncSession, role: Role, menu_id: str) -> Dict[str, Any]: """获取指定菜单的权限列表""" + from core.menu.model import Menu from core.permission.model import Permission # 获取该角色已选中的权限ID role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set() + menu_result = await db.execute( + select(Menu).where(Menu.id == menu_id, Menu.is_deleted == False) # noqa: E712 + ) + menu = menu_result.scalar_one_or_none() + if not menu or not _filter_ai_agent_admin_role_menus([menu]): + return { + 'menu_id': menu_id, + 'permissions': [], + } # 权限类型映射 PERMISSION_TYPE_MAP = { diff --git a/web/apps/web-ele/src/locales/index.ts b/web/apps/web-ele/src/locales/index.ts index 4e0005b..a15ddec 100644 --- a/web/apps/web-ele/src/locales/index.ts +++ b/web/apps/web-ele/src/locales/index.ts @@ -32,6 +32,7 @@ const baseModules = import.meta.glob([ const businessModules = import.meta.glob([ './langs/zh-CN/ai-platform.json', './langs/zh-CN/announcement.json', + './langs/zh-CN/dashboard-design.json', './langs/zh-CN/menu.json', './langs/zh-CN/message.json', './langs/zh-CN/permission.json', diff --git a/web/apps/web-ele/src/preferences.ts b/web/apps/web-ele/src/preferences.ts index dcbd846..f85eb34 100644 --- a/web/apps/web-ele/src/preferences.ts +++ b/web/apps/web-ele/src/preferences.ts @@ -16,7 +16,7 @@ export const overridesPreferences = defineOverridesPreferences({ enablePreferences: false, accessMode: 'mixed', authPageLayout: 'panel-center', - defaultHomePath: '/dashboard', + defaultHomePath: '/page-render/main_home', layout: 'header-sidebar-nav', }, shortcutKeys: { diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts index c0d4d06..01ea745 100644 --- a/web/apps/web-ele/src/router/access.ts +++ b/web/apps/web-ele/src/router/access.ts @@ -83,8 +83,8 @@ function normalizeBackendRoute< if (route.name !== 'ControlCenter') return route; const normalized = { ...route }; - normalized.path = '/dashboard'; - normalized.component = 'dashboard/index'; + normalized.path = '/page-render/main_home'; + normalized.component = 'online-dev/page-render/index'; normalized.query = undefined; return normalized; } @@ -159,7 +159,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { '../views/ai-platform/workflow/editor/index.vue', '../views/ai-platform/workflow/index.vue', '../views/ai-platform/workflow-runs/index.vue', - '../views/dashboard/index.vue', + '../views/online-dev/page-render/index.vue', ]); const layoutMap: ComponentRecordType = { diff --git a/web/apps/web-ele/src/router/guard.ts b/web/apps/web-ele/src/router/guard.ts index ce88899..34f2f17 100644 --- a/web/apps/web-ele/src/router/guard.ts +++ b/web/apps/web-ele/src/router/guard.ts @@ -191,7 +191,7 @@ function setupAccessGuard(router: Router) { if (to.path === subAppRootPath) { const defaultHome = - preferences.app.defaultHomePath || '/dashboard'; + preferences.app.defaultHomePath || '/page-render/main_home'; // 确保路径包含子应用前缀 const subAppTargetPath = defaultHome.startsWith(subAppRootPath) ? defaultHome diff --git a/web/apps/web-ele/src/router/routes/modules/ai-platform.ts b/web/apps/web-ele/src/router/routes/modules/ai-platform.ts index 0781a2c..36eff05 100644 --- a/web/apps/web-ele/src/router/routes/modules/ai-platform.ts +++ b/web/apps/web-ele/src/router/routes/modules/ai-platform.ts @@ -1,12 +1,6 @@ import type { RouteRecordRaw } from 'vue-router'; const routes: RouteRecordRaw[] = [ - { - meta: { hideInMenu: true }, - name: 'LegacyMainHomeRedirect', - path: '/page-render/main_home', - redirect: '/dashboard', - }, { meta: { hideInMenu: true }, name: 'AIKnowledgeLegacyRedirect', diff --git a/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue b/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue index dc34703..8b167d2 100644 --- a/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue +++ b/web/apps/web-ele/src/views/_core/menu/modules/menu-form.vue @@ -109,10 +109,11 @@ const componentKeys: string[] = Object.keys( '../../../ai-platform/agent/index.vue', '../../../ai-platform/knowledge/detail/index.vue', '../../../ai-platform/knowledge/index.vue', + '../../../ai-platform/model/index.vue', '../../../ai-platform/workflow/editor/index.vue', '../../../ai-platform/workflow/index.vue', '../../../ai-platform/workflow-runs/index.vue', - '../../../dashboard/index.vue', + '../../../online-dev/page-render/index.vue', ]), ) .filter((item) => !item.includes('/modules/')) @@ -120,7 +121,7 @@ const componentKeys: string[] = Object.keys( const path = v .replace('../../../_core/', '/_core/') .replace('../../../ai-platform/', '/ai-platform/') - .replace('../../../dashboard/', '/dashboard/'); + .replace('../../../online-dev/', '/online-dev/'); return path.endsWith('.vue') ? path.slice(0, -4) : path; }); diff --git a/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue b/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue index e6a37d4..d23acac 100644 --- a/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue +++ b/web/apps/web-ele/src/views/_core/role/modules/permission-assign-panel.vue @@ -95,6 +95,26 @@ function filterTreeByApp(nodes: MenuNode[], appId?: string): MenuNode[] { })); } +function filterAiAgentAdminTree(nodes: MenuNode[]): MenuNode[] { + return nodes + .filter((node) => AI_AGENT_ADMIN_MENU_NAMES.has(node.name)) + .map((node) => ({ + ...node, + children: node.children + ? filterAiAgentAdminTree(node.children as MenuNode[]) + : [], + })); +} + +function collectMenuIds(nodes: MenuNode[], ids: Set) { + nodes.forEach((node) => { + ids.add(node.id); + if (node.children?.length) { + collectMenuIds(node.children as MenuNode[], ids); + } + }); +} + const treeData = computed(() => filterTreeByApp(allTreeData.value, selectedAppId.value), ); @@ -110,6 +130,29 @@ const appList = ref([]); const selectedAppId = ref(undefined); const loadingApps = ref(false); +const AI_AGENT_ADMIN_MENU_NAMES = new Set([ + 'ControlCenter', + 'AIPlatform', + 'AIAgent', + 'AIModelConfig', + 'AIWorkflow', + 'AIWorkflowRuns', + 'KnowledgeBase', + 'Codex', + 'SystemConfigManager', + 'SystemManagement', + 'SystemPermission', + 'UserManagement', + 'SystemMenu', + 'SystemRole', + 'Message', + 'MessageList', + 'AnnouncementList', + 'AnnouncementManage', +]); + +const LEGACY_APP_KEYWORDS = ['审批中心', '车辆管理']; + const resourceScopeConfigRef = ref>(); const layoutContainerRef = ref(null); const scrollAreaHeight = ref(400); @@ -134,7 +177,12 @@ async function loadApps() { try { loadingApps.value = true; const res = await getApplicationListApi({ pageSize: 100 }); - appList.value = res.items || []; + appList.value = (res.items || []).filter( + (app) => + !LEGACY_APP_KEYWORDS.some( + (keyword) => app.name.includes(keyword) || app.code.includes(keyword), + ), + ); } catch (error) { console.error($t('role.permissions.loadAppsFailed'), error); ElMessage.error($t('role.permissions.loadAppsFailed')); @@ -223,11 +271,15 @@ async function loadMenuTree() { const data = await getRoleMenusApi(props.role.id); // 使用后端返回的菜单树结构 - allTreeData.value = data.menu_tree || []; + allTreeData.value = filterAiAgentAdminTree(data.menu_tree || []); // 初始化已选菜单 const selectedMenuIdsList = data.selected_menu_ids || []; - selectedMenuIds.value = new Set(selectedMenuIdsList); + const visibleMenuIds = new Set(); + collectMenuIds(allTreeData.value, visibleMenuIds); + selectedMenuIds.value = new Set( + selectedMenuIdsList.filter((id: string) => visibleMenuIds.has(id)), + ); // 清空权限缓存和选中状态 menuPermissionsCache.value = {}; diff --git a/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue b/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue index 82ed1ff..1486fdb 100644 --- a/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue +++ b/web/apps/web-ele/src/views/_core/ui-config/modules/app-settings-form.vue @@ -53,7 +53,7 @@ const appDynamicTitle = ref(true); const appWatermark = ref(false); const appWatermarkContent = ref(''); const appEnableCheckUpdates = ref(true); -const appDefaultHomePath = ref('/dashboard'); +const appDefaultHomePath = ref('/page-render/main_home'); const appEnablePreferences = ref(true); const footerEnable = ref(true); @@ -106,7 +106,7 @@ function getDefaultConfig() { defaultHomePath: overrides.app?.defaultHomePath || preferences.app.defaultHomePath || - '/dashboard', + '/page-render/main_home', enablePreferences: overrides.app?.enablePreferences ?? true, }, footer: { @@ -233,7 +233,7 @@ async function loadConfig() { // 获取带子应用前缀的首页路径 function getDefaultHomePathWithPrefix(): string { - const path = appDefaultHomePath.value || '/dashboard'; + const path = appDefaultHomePath.value || '/page-render/main_home'; const appCode = appContextStore.appCode; // 如果是子应用模式且路径不包含子应用前缀,自动添加 if (appCode && !path.startsWith(`/app/${appCode}`)) { diff --git a/web/apps/web-ele/src/views/dashboard/index.vue b/web/apps/web-ele/src/views/dashboard/index.vue deleted file mode 100644 index 22b5578..0000000 --- a/web/apps/web-ele/src/views/dashboard/index.vue +++ /dev/null @@ -1,802 +0,0 @@ - - - - - diff --git a/web/apps/web-ele/src/views/online-dev/page-render/index.vue b/web/apps/web-ele/src/views/online-dev/page-render/index.vue index d10a60d..e894949 100644 --- a/web/apps/web-ele/src/views/online-dev/page-render/index.vue +++ b/web/apps/web-ele/src/views/online-dev/page-render/index.vue @@ -15,7 +15,15 @@ const pageConfig = ref(''); const pageName = ref(''); const pageCode = ref(''); -const MAIN_HOME_AI_LINKS = [ +type QuickLink = null | { + bgColor: string; + icon: string; + id: string; + path: string; + title: string; +}; + +const MAIN_HOME_AI_LINKS: QuickLink[] = [ { bgColor: '', icon: 'lucide:square-user', @@ -37,6 +45,13 @@ const MAIN_HOME_AI_LINKS = [ path: '/ai-platform/knowledge-base', title: '知识库', }, + { + bgColor: '', + icon: 'lucide:settings', + id: 'ai-model', + path: '/ai-platform/model', + title: '模型配置', + }, { bgColor: '', icon: 'lucide:code', @@ -45,11 +60,72 @@ const MAIN_HOME_AI_LINKS = [ title: 'Codex', }, null, - null, - null, - null, ]; +const MAIN_HOME_ADMIN_LINKS: QuickLink[] = [ + { + bgColor: '', + icon: 'lucide:users', + id: 'system-user', + path: '/system/user', + title: '用户管理', + }, + { + bgColor: '', + icon: 'lucide:shield-check', + id: 'system-role', + path: '/system/role', + title: '角色权限', + }, + { + bgColor: '', + icon: 'lucide:key-round', + id: 'system-permission', + path: '/system/new-permission', + title: '权限管理', + }, + { + bgColor: '', + icon: 'lucide:menu', + id: 'system-menu', + path: '/system/menu', + title: '菜单管理', + }, + { + bgColor: '', + icon: 'lucide:megaphone', + id: 'announcement', + path: '/message/announcement', + title: '公告管理', + }, + { + bgColor: '', + icon: 'lucide:message-square-text', + id: 'message-list', + path: '/message/list', + title: '消息中心', + }, +]; + +function toQuickLinksWidget( + widget: Record, + title: string, + menus: QuickLink[], +) { + return { + ...widget, + title, + type: 'quick-links', + props: { + ...widget.props, + columns: 3, + menus, + rows: 2, + title, + }, + }; +} + function normalizeMainHomeConfig(config: Record) { if (pageCode.value !== 'main_home' || !Array.isArray(config.widgets)) { return config; @@ -58,32 +134,29 @@ function normalizeMainHomeConfig(config: Record) { return { ...config, widgets: config.widgets.map((widget: Record) => { - if (widget.type !== 'quick-links') return widget; - - return { - ...widget, - props: { - ...widget.props, - menus: MAIN_HOME_AI_LINKS, - }, - }; + if (widget.type === 'approval-center') { + return toQuickLinksWidget(widget, 'AI 协作', MAIN_HOME_AI_LINKS); + } + if (widget.type === 'my-apps') { + return toQuickLinksWidget(widget, '基础管理', MAIN_HOME_ADMIN_LINKS); + } + if (widget.type === 'quick-links') { + return toQuickLinksWidget(widget, '快捷入口', MAIN_HOME_AI_LINKS); + } + return widget; }), }; } -// 获取页面编码 function getPageCode(): string { - // 优先从 query 获取 if (route.query.pageCode) { return route.query.pageCode as string; } - // 其次从 params 获取 if (route.params.code) { return route.params.code as string; } - // 最后从路径中提取(路径格式:/page-render/xxx) const pathParts = route.path.split('/').filter(Boolean); const length = pathParts.length; if (length >= 2 && pathParts[length - 2] === 'page-render') { @@ -93,7 +166,6 @@ function getPageCode(): string { return ''; } -// 加载页面数据 async function loadPageData() { const code = getPageCode(); if (!code) { @@ -123,7 +195,6 @@ onMounted(() => { loadPageData(); }); -// 监听路由变化 watch( () => route.fullPath, () => {