异步(远程)菜单
DVHA 框架支持从远程服务器动态获取菜单数据,实现菜单的动态配置和权限控制。
功能特点
- 🌐 远程获取 - 从服务器动态加载菜单配置
- 🔄 实时更新 - 支持菜单的实时更新和变更
- 🔐 权限控制 - 基于用户权限动态显示菜单
- 🔧 灵活配置 - 支持复杂的菜单结构和元数据
- 📱 本地合并 - 远程菜单与本地菜单自动合并
- ⚡ 缓存机制 - 避免重复请求,提升性能
- 🛡️ 错误处理 - 远程菜单加载失败时的降级处理
基础配置
启用远程菜单
js
const adminManage = {
name: 'admin',
title: '管理后台',
// 远程菜单API路径
apiRoutePath: '/api/admin/menus',
// 数据提供者(必需)
dataProvider: {
custom: async (options, manage, auth) => {
const response = await fetch(options.path, {
method: 'GET',
headers: {
'Authorization': `Bearer ${auth.token}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
}
},
// 本地菜单(可选,会与远程菜单合并)
menus: [
{
name: 'dashboard',
label: '仪表盘',
path: 'dashboard',
icon: 'dashboard',
sort: 0, // 确保在远程菜单之前显示
component: () => import('./pages/Dashboard.vue')
}
]
}
API 响应格式
远程菜单API需要返回符合以下格式的数据:
json
{
"data": [
{
"name": "users",
"label": "用户管理",
"path": "users",
"icon": "users",
"sort": 1,
"meta": {
"permissions": ["user.read"]
}
},
{
"name": "system",
"label": "系统管理",
"icon": "system",
"sort": 2
},
{
"name": "system.settings",
"label": "系统设置",
"path": "system/settings",
"parent": "system",
"sort": 1,
"meta": {
"permissions": ["system.admin"]
}
}
]
}
数据提供者配置
基础数据提供者
js
const dataProvider = {
custom: async (options, manage, auth) => {
const { path, method = 'GET', headers = {}, ...config } = options
// 构建完整的请求URL
const baseUrl = manage.config?.apiUrl || 'http://localhost:3000'
const url = path.startsWith('http') ? path : `${baseUrl}${path}`
// 默认headers
const defaultHeaders = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth?.token || ''}`,
...headers
}
const response = await fetch(url, {
method,
headers: defaultHeaders,
...config
})
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`)
}
return await response.json()
}
}
菜单结构定义
完整菜单项
json
{
"name": "user.management",
"label": "用户管理",
"path": "users",
"icon": "users",
"sort": 10,
"parent": null,
"hidden": false,
"loader": null,
"meta": {
"permissions": ["user.read", "user.write"],
"roles": ["admin", "manager"],
"title": "用户管理 - 系统后台",
"keepAlive": true,
"breadcrumb": ["首页", "用户管理"],
"badge": {
"text": "New",
"color": "red"
}
}
}
多级菜单
json
{
"data": [
{
"name": "system",
"label": "系统管理",
"icon": "system",
"sort": 1
},
{
"name": "system.users",
"label": "用户管理",
"path": "system/users",
"parent": "system",
"icon": "users",
"sort": 1
},
{
"name": "system.roles",
"label": "角色管理",
"path": "system/roles",
"parent": "system",
"icon": "roles",
"sort": 2
},
{
"name": "system.permissions",
"label": "权限管理",
"path": "system/permissions",
"parent": "system",
"icon": "permissions",
"sort": 3
}
]
}
外部链接菜单
json
{
"name": "external.docs",
"label": "帮助文档",
"path": "https://docs.example.com",
"icon": "help",
"loader": "iframe",
"sort": 100,
"meta": {
"external": true,
"target": "_blank"
}
}
权限控制
基于权限的菜单
json
{
"data": [
{
"name": "users",
"label": "用户管理",
"path": "users",
"icon": "users",
"meta": {
"permissions": ["user.read"] // 需要用户读取权限
}
},
{
"name": "admin",
"label": "系统管理",
"icon": "admin",
"meta": {
"roles": ["admin"] // 仅管理员可见
}
},
{
"name": "reports",
"label": "报表中心",
"path": "reports",
"icon": "chart",
"meta": {
"permissions": ["report.read"],
"roles": ["admin", "manager"] // 需要权限且角色匹配
}
}
]
}
条件显示菜单
json
{
"name": "vip.features",
"label": "VIP功能",
"path": "vip",
"icon": "vip",
"meta": {
"conditions": {
"userLevel": "vip",
"subscriptionActive": true
}
}
}
服务端实现示例
Node.js + Express
js
// 菜单路由处理
app.get('/api/admin/menus', authenticateToken, async (req, res) => {
try {
const user = req.user
const userPermissions = await getUserPermissions(user.id)
const userRoles = await getUserRoles(user.id)
// 获取所有菜单
const allMenus = await getMenusFromDatabase()
// 根据用户权限过滤菜单
const filteredMenus = allMenus.filter(menu => {
// 检查权限
if (menu.meta?.permissions) {
const hasPermission = menu.meta.permissions.some(
permission => userPermissions.includes(permission)
)
if (!hasPermission) return false
}
// 检查角色
if (menu.meta?.roles) {
const hasRole = menu.meta.roles.some(
role => userRoles.includes(role)
)
if (!hasRole) return false
}
return true
})
res.json({
data: filteredMenus.sort((a, b) => (a.sort || 0) - (b.sort || 0))
})
} catch (error) {
res.status(500).json({ error: 'Failed to load menus' })
}
})
Laravel PHP
php
<?php
class MenuController extends Controller
{
public function getMenus(Request $request)
{
$user = $request->user();
$permissions = $user->permissions->pluck('name')->toArray();
$roles = $user->roles->pluck('name')->toArray();
$menus = Menu::where('active', true)
->orderBy('sort')
->get()
->filter(function ($menu) use ($permissions, $roles) {
// 检查权限
if ($menu->permissions) {
$menuPermissions = json_decode($menu->permissions, true);
if (!array_intersect($menuPermissions, $permissions)) {
return false;
}
}
// 检查角色
if ($menu->roles) {
$menuRoles = json_decode($menu->roles, true);
if (!array_intersect($menuRoles, $roles)) {
return false;
}
}
return true;
});
return response()->json([
'data' => $menus->values()
]);
}
}
错误处理
客户端错误处理
框架会自动处理远程菜单加载失败的情况:
js
// 在 DuxAppProvider 中的错误处理
if (manage.config?.apiRoutePath) {
try {
await manage.config.dataProvider?.custom({
path: manage.config.apiRoutePath,
meta: {
timeout: 5000,
}
}, manage, authStore.getUser(manageName)).then((res) => {
routeStore.appendRoutes(formatMenus(res.data || []))
})
} catch (error) {
console.error('Failed to load remote menus:', error)
// 继续使用本地菜单
}
}
最佳实践
性能优化
- 使用适当的缓存策略
- 设置合理的请求超时时间
- 避免频繁的菜单更新
安全考虑
- 在服务端进行权限验证
- 不要在前端存储敏感的权限信息
- 使用HTTPS传输菜单数据
用户体验
- 提供菜单加载状态提示
- 设计合理的降级方案
- 避免菜单频繁闪烁变化
错误处理
- 实现完善的错误处理机制
- 提供有意义的错误提示
- 记录详细的错误日志
注意事项
- 数据格式: 确保远程菜单数据符合
IMenu
接口规范 - 权限验证: 在服务端进行权限验证,前端验证仅用于显示
- 性能考虑: 合理使用缓存,避免过度请求
- 错误处理: 实现降级方案,确保系统在网络异常时可用
- 安全性: 使用HTTPS和token认证保护菜单接口
- 兼容性: 远程菜单与本地菜单的合并要考虑名称冲突