菜单权限

This commit is contained in:
liamzi
2025-01-14 18:52:03 +08:00
parent 2edb91cbea
commit 34550d0517
10 changed files with 790 additions and 409 deletions

View File

@@ -20,11 +20,11 @@ const Header = ({ collapsed, setCollapsed }) => {
};
const userMenuItems = [
{
key: "profile",
icon: <UserOutlined />,
label: "个人信息",
},
// {
// key: "profile",
// icon: <UserOutlined />,
// label: "个人信息",
// },
{
key: "logout",
icon: <LogoutOutlined />,

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useMemo,useEffect } from "react";
import { Layout, Menu } from "antd";
import { useNavigate, useLocation } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
@@ -14,12 +14,13 @@ const Sidebar = ({ collapsed }) => {
const { isDarkMode } = useTheme();
const { user } = useAuth();
const menuItems = useMemo(() => {
if (!user?.id) return [];
const { adminRole: role } = user;
return getMenuItems(role);
const menuClient = useMemo(() => {
if (!user?.id||user.menukeys?.length===0) return [];
return getMenuItems(user?.menukeys || []);
}, [user]);
useEffect(()=>{
console.log(menuClient,'menuClient');
},[menuClient])
const defaultOpenKeys = useMemo(() => {
const pathSegments = location.pathname.split("/").filter(Boolean);
return pathSegments.reduce((acc, _, index) => {
@@ -29,7 +30,6 @@ const Sidebar = ({ collapsed }) => {
}, []);
}, [location.pathname]);
// Handle menu item click
const handleMenuClick = ({ key }) => {
navigate(key);
};
@@ -41,8 +41,8 @@ const Sidebar = ({ collapsed }) => {
collapsed={collapsed}
theme={isDarkMode ? "dark" : "light"}
width={256}
collapsedWidth={80} // 添加这个属性
className={`app-sidebar ${collapsed ? "collapsed" : ""}`} // 添加collapsed类名
collapsedWidth={80}
className={`app-sidebar ${collapsed ? "collapsed" : ""}`}
>
<Logo collapsed={collapsed} isDarkMode={isDarkMode} />
<Menu
@@ -50,7 +50,7 @@ const Sidebar = ({ collapsed }) => {
mode="inline"
selectedKeys={[location.pathname]}
defaultOpenKeys={defaultOpenKeys}
items={menuItems}
items={menuClient}
onClick={handleMenuClick}
/>
</Sider>

View File

@@ -1,24 +1,27 @@
import React from "react";
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext";
import { Spin } from "antd";
const PUBLIC_PATHS = ['login', '404'];
export const ProtectedRoute = ({ children }) => {
const { user, loading } = useAuth();
const { user } = useAuth();
const location = useLocation();
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Spin size="large" />
</div>
);
const currentPath = location.pathname.replace(/^\//, '');
if (PUBLIC_PATHS.includes(currentPath) || currentPath === '*') {
return children;
}
if (user?.id) {
const hasPermission = user.menukeys?.some(key => {
return currentPath === key || currentPath.startsWith(`${key}/`);
});
if (!user?.id) {
return (
<Navigate to={`/login?redirectTo=${location.pathname || ""}`} replace />
);
if (!hasPermission ) {
return <Navigate to="/404" replace />;
}
return children;
}
// 如果用户未登录,重定向到登录页
return <Navigate to="/login" state={{ from: location }} replace />;
};

View File

@@ -40,7 +40,8 @@ export const AuthProvider = ({ children }) => {
error,
} = await supabase.auth.getSession();
const role = await checkInTeam(session?.user ?? null);
setUser({ ...session?.user, adminRole: role });
const menukey = await fetchMenuList(role);
setUser({ ...session?.user, adminRole: role, menukeys: menukey });
} catch (error) {
console.error("Error getting session:", error);
} finally {
@@ -55,7 +56,8 @@ export const AuthProvider = ({ children }) => {
if (user?.id && !user?.adminRole) {
(async () => {
const role = await checkInTeam(user);
setUser({ ...user, adminRole: role });
const menukey = await fetchMenuList(role);
setUser({ ...user, adminRole: role, menukeys: menukey });
})();
}
}, [user]);
@@ -179,7 +181,26 @@ export const AuthProvider = ({ children }) => {
setLoading(false);
}
};
const fetchMenuList = async (role) => {
console.log(role,'role');
try {
const { data, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'menuKey')
.eq('attributes->>roleName', role) // 添加这行来筛选 OWNER 角色
.single();
if (error) throw error;
if(data?.attributes?.menuKeys){
return data.attributes.menuKeys;
}else{
return [];
}
} catch (error) {
message.error('获取菜单失败:' + error.message);
return [];
}
};
// 登出
const logout = async () => {
try {

View File

@@ -1,40 +0,0 @@
import { Form, Input, Button, Card, message } from 'antd';
const Settings = () => {
const [form] = Form.useForm();
const onFinish = (values) => {
console.log('Success:', values);
message.success('Settings updated successfully');
};
return (
<Card title="Settings">
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
email: 'john@example.com',
notifications: true,
}}
>
<Form.Item
label="Email"
name="email"
rules={[{ required: true, type: 'email' }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Save Changes
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default Settings;

View File

@@ -0,0 +1,283 @@
import React, { useState, useEffect } from 'react';
import {
Table,
Button,
Modal,
Form,
Select,
Input,
message,
Card,
Typography,
Space,
TreeSelect
} from 'antd';
import { getAllRouteOptions, allRoutes } from '@/routes/routes';
import { supabase } from '@/config/supabase';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
const { Title } = Typography;
const TYPE = 'menuKey';
export default function MenuManagement() {
const [loading, setLoading] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [menuList, setMenuList] = useState([]);
const [form] = Form.useForm();
const [roles, setRoles] = useState([]);
// 获取所有路由选项
const routeOptions = getAllRouteOptions();
// 表格列配置
const columns = [
{
title: '角色名称',
dataIndex: ['attributes', 'roleName'],
key: 'roleName',
render: (roleName) => {
const role = roles.find(r => r.name === roleName);
return role ? role.name : roleName;
}
},
{
title: '可访问菜单',
dataIndex: ['attributes', 'menuKeys'],
key: 'menuKeys',
render: (menuKeys) => menuKeys?.map(key => {
const route = routeOptions.find(r => r.value === key);
return route ? route.label : key;
}).join(', '),
},
{
title: '描述',
dataIndex: ['attributes', 'description'],
key: 'description',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
编辑
</Button>
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)}
>
删除
</Button>
</Space>
),
},
];
// 获取菜单数据
const fetchMenuList = async () => {
try {
setLoading(true);
const { data, error } = await supabase
.from('resources')
.select('*')
.eq('type', TYPE)
.order('created_at', { ascending: false });
if (error) throw error;
setMenuList(data);
} catch (error) {
message.error('获取数据失败:' + error.message);
} finally {
setLoading(false);
}
};
// 获取角色列表
const fetchRoles = async () => {
try {
const { data, error } = await supabase
.from('roles')
.select('*')
.order('name');
if (error) throw error;
setRoles(data || []);
} catch (error) {
message.error('获取角色数据失败');
console.error(error);
}
};
// 处理表单提交
const handleSubmit = async (values) => {
try {
setLoading(true);
const menuData = {
type: TYPE,
attributes: {
roleName: values.roleName,
menuKeys: values.menuKeys,
description: values.description
}
};
let result;
if (form.getFieldValue('id')) {
result = await supabase
.from('resources')
.update(menuData)
.eq('id', form.getFieldValue('id'))
.select();
} else {
result = await supabase
.from('resources')
.insert([menuData])
.select();
}
if (result.error) throw result.error;
message.success('保存成功');
setIsModalVisible(false);
fetchMenuList();
} catch (error) {
message.error('保存失败:' + error.message);
} finally {
setLoading(false);
}
};
// 编辑菜单
const handleEdit = (record) => {
form.setFieldsValue({
id: record.id,
roleName: record.attributes.roleName,
menuKeys: record.attributes.menuKeys,
description: record.attributes.description
});
setIsModalVisible(true);
};
// 删除菜单
const handleDelete = async (id) => {
try {
setLoading(true);
const {data, error } = await supabase
.from('resources')
.delete()
.eq('id', id);
if (error) throw error;
if(data.length>0){
message.success('删除成功');
fetchMenuList();
}else{
message.error('删除失败');
}
} catch (error) {
message.error('删除失败:' + error.message);
} finally {
setLoading(false);
}
};
// 将路由配置转换为 TreeSelect 所需的格式
const getTreeData = (routes) => {
return routes.map(route => ({
title: route.name,
value: route.key,
children: route.children ? getTreeData(route.children) : undefined,
}));
};
useEffect(() => {
fetchMenuList();
fetchRoles();
}, []);
return (
<div className="bg-gradient-to-b from-gray-50 to-white min-h-screen p-2">
<Card className="shadow-lg rounded-lg">
<div className="flex justify-between items-center mb-4">
<Title level={4} className="mb-0">菜单权限管理</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
form.resetFields();
setIsModalVisible(true);
}}
>
新增角色菜单
</Button>
</div>
<Table
columns={columns}
dataSource={menuList}
loading={loading}
rowKey="id"
/>
<Modal
title={form.getFieldValue('id') ? '编辑角色菜单' : '新增角色菜单'}
open={isModalVisible}
onOk={() => form.submit()}
onCancel={() => setIsModalVisible(false)}
confirmLoading={loading}
>
<Form
form={form}
onFinish={handleSubmit}
layout="vertical"
>
<Form.Item name="id" hidden>
<Input />
</Form.Item>
<Form.Item
name="roleName"
label="角色名称"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select
placeholder="请选择角色"
options={roles.map(role => ({
label: role.name,
value: role.name
}))}
/>
</Form.Item>
<Form.Item
name="menuKeys"
label="可访问菜单"
rules={[{ required: true, message: '请选择可访问菜单' }]}
>
<TreeSelect
treeData={getTreeData(allRoutes)}
treeCheckable
showCheckedStrategy={TreeSelect.SHOW_ALL}
checkStrictly={true}
placeholder="请选择可访问菜单"
style={{ width: '100%' }}
maxTagCount={3}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
/>
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea placeholder="请输入描述信息" />
</Form.Item>
</Form>
</Modal>
</Card>
</div>
);
}

View File

@@ -4,12 +4,8 @@ import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { supabase } from '@/config/supabase';
const RoleHeader = ({ onAdd, onSearch }) => (
<div className="flex justify-between mb-4">
<Input.Search
placeholder="搜索权限"
onSearch={onSearch}
style={{ width: 300 }}
/>
<div className="flex justify-end mb-4">
<Button
type="primary"
icon={<PlusOutlined />}
@@ -43,8 +39,8 @@ export default function PermissionManagement() {
const [resourceModalVisible, setResourceModalVisible] = useState(false);
const [roleForm] = Form.useForm();
const [resourceForm] = Form.useForm();
const [editingResource, setEditingResource] = useState(null);
// 获取所有权限数据(包括关联数据)
const fetchPermissions = async (params = {}) => {
try {
setLoading(true);
@@ -56,15 +52,6 @@ export default function PermissionManagement() {
resources:resource_id(*)
`, { count: 'exact' });
// 添加搜索条件
if (params.search) {
query = query.or(`
roles.name.ilike.%${params.search}%,
resources.resource_name.ilike.%${params.search}%,
resources.description.ilike.%${params.search}%
`);
}
// 添加排序
if (params.field && params.order) {
const ascending = params.order === 'ascend';
@@ -240,6 +227,47 @@ export default function PermissionManagement() {
}
};
// 添加删除资源的函数
const handleDeleteResource = async (resourceId) => {
try {
const { error } = await supabase
.from('permission_resources')
.delete()
.eq('id', resourceId);
if (error) throw error;
message.success('删除资源成功');
fetchResources();
} catch (error) {
message.error('删除资源失败');
console.error(error);
}
};
// 添加更新资源的函数
const handleUpdateResource = async (values) => {
try {
const { error } = await supabase
.from('permission_resources')
.update({
resource_name: values.resource_name,
resource_type: values.resource_type,
attributes_type: values.attributes_type,
description: values.description
})
.eq('id', editingResource.id);
if (error) throw error;
message.success('更新资源成功');
setResourceModalVisible(false);
resourceForm.resetFields();
fetchResources();
} catch (error) {
message.error('更新资源失败');
console.error(error);
}
};
// 初始化加载
useEffect(() => {
fetchPermissions();
@@ -274,7 +302,6 @@ export default function PermissionManagement() {
title: '角色',
dataIndex: ['roles', 'name'],
key: 'role_name',
sorter: true,
render: (name) => (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
{name}
@@ -285,10 +312,9 @@ export default function PermissionManagement() {
title: '资源',
dataIndex: ['resources', 'resource_name'],
key: 'resource_name',
sorter: true,
},
{
title: '资源类型',
title: '控制颗粒度',
dataIndex: ['resources', 'resource_type'],
key: 'resource_type',
render: (type) => (
@@ -306,10 +332,8 @@ export default function PermissionManagement() {
key: 'attributes_type',
render: (attributes_type) => (
<span>
{!attributes_type ? '' : attributes_type}
{!attributes_type ? '-' : attributes_type}
</span>
)
},
{
@@ -333,12 +357,12 @@ export default function PermissionManagement() {
);
}
},
{
title: '描述',
dataIndex: ['resources', 'description'],
key: 'description',
ellipsis: true,
},
// {
// title: '描述',
// dataIndex: ['resources', 'description'],
// key: 'description',
// ellipsis: true,
// },
{
title: '操作',
key: 'operation',
@@ -365,7 +389,11 @@ export default function PermissionManagement() {
];
// Modal 表单内容
const renderFormItems = () => (
const renderFormItems = () => {
const [roleSelectOpen, setRoleSelectOpen] = useState(false);
const [resourceSelectOpen, setResourceSelectOpen] = useState(false);
return (
<>
<Form.Item
name="role_id"
@@ -373,7 +401,10 @@ export default function PermissionManagement() {
rules={[{ required: true, message: '请选择角色' }]}
>
<Select
allowClear
placeholder="请选择角色"
open={roleSelectOpen}
onDropdownVisibleChange={setRoleSelectOpen}
dropdownRender={(menu) => (
<>
{menu}
@@ -381,7 +412,10 @@ export default function PermissionManagement() {
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => setRoleModalVisible(true)}
onClick={() => {
setRoleSelectOpen(false); // 关闭下拉框
setRoleModalVisible(true);
}}
style={{ paddingLeft: 8 }}
>
添加角色
@@ -404,24 +438,60 @@ export default function PermissionManagement() {
>
<Select
placeholder="请选择资源"
open={resourceSelectOpen}
onDropdownVisibleChange={setResourceSelectOpen}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: '8px 0' }} />
<div style={{ padding: '8px', display: 'flex', justifyContent: 'space-between' }}>
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => setResourceModalVisible(true)}
style={{ paddingLeft: 8 }}
onClick={() => {
setResourceSelectOpen(false);
setEditingResource(null); // 清空编辑状态
resourceForm.resetFields(); // 清空表单
setResourceModalVisible(true);
}}
>
添加资源
</Button>
</div>
</>
)}
>
{resources.map(resource => (
<Select.Option key={resource.id} value={resource.id}>
{`${resource.resource_name} ${resource.attributes_type?`(${resource.attributes_type})`:''}`}
<div className="flex justify-between items-center">
<span>{`${resource.resource_name} ${resource.attributes_type?`(${resource.attributes_type})`:''}`}</span>
<div className="flex space-x-2" onClick={e => e.stopPropagation()}>
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => {
setResourceSelectOpen(false);
setEditingResource(resource);
resourceForm.setFieldsValue(resource);
setResourceModalVisible(true);
}}
/>
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这个资源吗?',
onOk: () => handleDeleteResource(resource.id),
});
}}
/>
</div>
</div>
</Select.Option>
))}
</Select>
@@ -432,7 +502,7 @@ export default function PermissionManagement() {
label="操作类型"
rules={[{ required: true, message: '请选择操作类型' }]}
>
<Select placeholder="请选择操作类型">
<Select placeholder="请选择操作类型" allowClear>
<Select.Option value="create">创建</Select.Option>
<Select.Option value="read">读取</Select.Option>
<Select.Option value="update">更新</Select.Option>
@@ -441,6 +511,7 @@ export default function PermissionManagement() {
</Form.Item>
</>
);
};
return (
<App>
@@ -535,18 +606,26 @@ export default function PermissionManagement() {
{/* 添加资源 Modal */}
<Modal
title="添加资源"
title={editingResource ? "编辑资源" : "添加资源"}
open={resourceModalVisible}
onCancel={() => {
setResourceModalVisible(false);
setEditingResource(null);
resourceForm.resetFields();
}}
onOk={() => resourceForm.submit()}
zIndex={1100}
>
<Form
form={resourceForm}
layout="vertical"
onFinish={handleAddResource}
onFinish={(values) => {
if (editingResource) {
handleUpdateResource(values);
} else {
handleAddResource(values);
}
}}
>
<Form.Item
name="resource_name"

View File

@@ -2,10 +2,9 @@ import React, { Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Spin } from 'antd';
import MainLayout from '@/components/Layout/MainLayout';
import { generateRoutes } from '@/routes/routes';
import { allRoutes } from '@/routes/routes';
import Login from '@/pages/auth/Login';
import NotFound from '@/pages/notFound';
import Dashboard from '@/pages/Dashboard';
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
import { useAuth } from '@/contexts/AuthContext';
@@ -14,19 +13,26 @@ const LoadingComponent = () => (
<Spin size="large" />
</div>
);
// 这里做premeision 路由限制
// 渲染路由
const renderRoutes = (routes) => {
return routes
// .filter(route => !route.hidden)
.map(route => {
if (!Array.isArray(routes)) {
console.warn('routes is not an array:', routes);
return [];
}
return routes.map(route => {
const Component = route.component;
if (route.children) {
return (
<Route key={route.path} path={route.path} element={
<Route
key={route.path}
path={route.path}
element={
<Suspense fallback={<LoadingComponent />}>
<Component />
</Suspense>
}>
}
>
{renderRoutes(route.children)}
</Route>
);
@@ -47,15 +53,26 @@ const renderRoutes = (routes) => {
};
const AppRoutes = () => {
const { user } = useAuth();
const { adminRole: role } = user;
const { user, loading } = useAuth();
if (loading) {
return (
<div className="flex justify-center items-center min-h-screen">
<Spin size="large" />
</div>
);
}
return (
<Routes>
<Route
path="/login"
element={
user?.id ? <Navigate to="/company/serviceTemplate" replace /> : <Login />
user?.id && user.menukeys?.includes('company/serviceTemplate') ? (
<Navigate to="/company/serviceTemplate" replace />
) : (
<Login />
)
}
/>
@@ -67,11 +84,15 @@ const AppRoutes = () => {
</ProtectedRoute>
}
>
{/* 默认重定向到服务管理页面 */}
<Route
index
element={<Navigate to="/company/serviceTemplate" replace />}
/>
{renderRoutes(generateRoutes(role))}
{/* 渲染所有路由 */}
{renderRoutes(allRoutes)}
<Route path="*" element={<NotFound />} />
</Route>
</Routes>

View File

@@ -1,210 +1,180 @@
import { lazy } from "react";
// Resource Management routes
const resourceRoutes = [
{
path: "team",
component: lazy(() => import("@/pages/resource/team")),
name: "团队管理",
icon: "team",
roles: ["OWNER"],
},
{
path: "role",
component: lazy(() => import("@/pages/resource/role")),
name: "角色管理",
icon: "setting",
roles: ["OWNER"],
},
{
path: "bucket",
component: lazy(() => import("@/pages/resource/bucket")),
name: "对象存储",
icon: "shop",
roles: ["OWNER"],
},
{
path: "task/edit/:id?",
component: lazy(() => import("@/pages/resource/resourceTask/edit")),
hidden: true,
name: "新增/编辑任务",
roles: ["OWNER"],
},
];
// Company routes
const companyRoutes = [
{
path: "quotation",
component: lazy(() => import("@/pages/company/quotation")),
name: "报价单",
icon: "file",
roles: ["ADMIN", "OWNER"],
},
{
path: "quotaInfo/:id?", // 添加可选的 id 参数
hidden: true,
component: lazy(() => import("@/pages/company/quotation/detail")),
name: "报价单详情",
icon: "file",
roles: ["ADMIN", "OWNER"],
},
{
path: "project",
component: lazy(() => import("@/pages/company/project")),
name: "专案管理",
icon: "appstore",
roles: ["ADMIN", "OWNER"],
},
{
path: "task",
component: lazy(() => import("@/pages/company/task")),
name: "任务管理",
icon: "appstore",
roles: ["OWNER"],
},
{
path: "taskInfo/:id?",
hidden:true,
component: lazy(() => import("@/pages/company/task/detail")),
name: "任务管理详情",
icon: "appstore",
roles: ["OWNER"],
},
{
path: "serviceTemplate",
component: lazy(() => import("@/pages/company/service")),
name: "服务管理",
icon: "container",
roles: ["ADMIN", "OWNER"],
},
{
path: "templateItemManage",
component: lazy(() => import("@/pages/company/service/itemsManange")),
name: "资源类型",
icon: "container",
roles: ["ADMIN", "OWNER"],
},
{
path: "serviceTemplateInfo/:id?",
hidden: true,
component: lazy(() => import("@/pages/company/service/detail")),
name: "服务模版详情",
icon: "container",
roles: ["ADMIN", "OWNER"],
},
{
path: "quotaInfo/preview/:id?", // 添加可选的 id 参数
hidden: true,
component: lazy(() => import("@/pages/company/quotation/view")),
name: "报价单预览",
icon: "file",
roles: ["ADMIN", "OWNER"],
},
{
path: "customer",
component: lazy(() => import("@/pages/company/customer")),
name: "客户管理",
icon: "user",
roles: ["ADMIN", "OWNER"],
},
{
path: "customerInfo/:id?",
hidden: true,
component: lazy(() => import("@/pages/company/customer/detail")),
name: "客户详情",
icon: "user",
roles: ["ADMIN", "OWNER"],
},
{
path: "supplier",
component: lazy(() => import("@/pages/company/supplier")),
name: "供应商管理",
icon: "branches",
roles: ["ADMIN", "OWNER"],
},
{
path: "supplierInfo/:id?",
hidden: true,
component: lazy(() => import("@/pages/company/supplier/detail")),
name: "供应商详情",
icon: "branches",
roles: ["ADMIN", "OWNER"],
},
{
path: "projectInfo/:id?",
hidden: true,
component: lazy(() => import("@/pages/company/project/detail")),
name: "专案管理详情",
icon: "appstore",
roles: ["ADMIN", "OWNER"],
},
{
path: "projectView/:id?",
hidden: true,
component: lazy(() => import("@/pages/company/project/info")),
name: "专案详情",
icon: "appstore",
roles: ["ADMIN", "OWNER"],
}
];
const marketingRoutes = [];
// const roleRoutes = [
// {
// path: "role",
// component: lazy(() => import("@/pages/role")),
// name: "角色管理",
// icon: "setting",
// roles: ["ADMIN", "OWNER"],
// },
// ];
export const generateRoutes = (role) => {
return [
// 所有可用的路由配置
export const allRoutes = [
{
path: "dashboard",
component: lazy(() => import("@/pages/Dashboard")),
name: "仪表盘",
icon: "dashboard",
roles: ["ADMIN", "OWNER", "MEMBER"],
key: "dashboard",
},
{
path: "resource",
component: lazy(() => import("@/pages/resource")),
name: "资源管理",
icon: "appstore",
children: resourceRoutes.filter((route) => route.roles.includes(role)),
roles: ["OWNER"],
key: "resource",
children: [
{
path: "team",
component: lazy(() => import("@/pages/resource/team")),
name: "团队管理",
icon: "team",
key: "resource/team",
},
{
path: "role",
component: lazy(() => import("@/pages/resource/role")),
name: "角色管理",
icon: "setting",
key: "resource/role",
},
{
path: "menu",
component: lazy(() => import("@/pages/resource/menu")),
name: "菜单管理",
icon: "menu",
key: "resource/menu",
},
{
path: "bucket",
component: lazy(() => import("@/pages/resource/bucket")),
name: "对象存储",
icon: "shop",
key: "resource/bucket",
},
{
path: "task/edit/:id?",
component: lazy(() => import("@/pages/resource/resourceTask/edit")),
name: "新增/编辑任务",
hidden: true,
key: "resource/task/edit",
}
]
},
{
path: "company",
component: lazy(() => import("@/pages/company")),
name: "公司管理",
icon: "bank",
children: companyRoutes.filter((route) => route.roles.includes(role)),
roles: ["ADMIN", "OWNER"],
key: "company",
children: [
{
path: "quotation",
component: lazy(() => import("@/pages/company/quotation")),
name: "报价单",
icon: "file",
key: "company/quotation",
},
// {
// path: "marketing",
// component: lazy(() => import("@/pages/marketing")),
// name: "行销中心",
// icon: "shopping",
// children: marketingRoutes.filter((route) => route.roles.includes(role)),
// roles: ["ADMIN", "OWNER"],
// },
{
path: "quotaInfo/:id?",
component: lazy(() => import("@/pages/company/quotation/detail")),
name: "报价单详情",
icon: "file",
hidden: true,
key: "company/quotaInfo",
},
{
path: "project",
component: lazy(() => import("@/pages/company/project")),
name: "专案管理",
icon: "appstore",
key: "company/project",
},
{
path: "task",
component: lazy(() => import("@/pages/company/task")),
name: "任务管理",
icon: "appstore",
key: "company/task",
},
{
path: "taskInfo/:id?",
component: lazy(() => import("@/pages/company/task/detail")),
name: "任务管理详情",
icon: "appstore",
hidden: true,
key: "company/taskInfo",
},
{
path: "serviceTemplate",
component: lazy(() => import("@/pages/company/service")),
name: "服务管理",
icon: "appstore",
key: "company/serviceTemplate",
},
{
path: "supplier",
component: lazy(() => import("@/pages/company/supplier")),
name: "供应商管理",
icon: "branches",
key: "company/supplier",
},
{
path: "supplierInfo/:id?",
component: lazy(() => import("@/pages/company/supplier/detail")),
name: "供应商详情",
icon: "branches",
hidden: true,
key: "company/supplierInfo",
},
{
path: "projectInfo/:id?",
component: lazy(() => import("@/pages/company/project/detail")),
name: "专案管理详情",
icon: "appstore",
hidden: true,
key: "company/projectInfo",
},
{
path: "projectView/:id?",
component: lazy(() => import("@/pages/company/project/info")),
name: "专案详情",
icon: "appstore",
hidden: true,
key: "company/projectView",
}
]
}
];
// {
// path: "role",
// component: lazy(() => import("@/pages/role")),
// name: "权限管理",
// icon: "setting",
// children: roleRoutes.filter((route) => route.roles.includes(role)),
// roles: ["ADMIN", "OWNER"],
// },
].filter((route) => route.roles.includes(role));
export const getRouteByKey = (key) => {
const keys = key.split('/').filter(Boolean);
let current = allRoutes;
let result = null;
keys.forEach(k => {
const found = current.find(r => r.path.split('/')[0] === k);
if (found) {
result = found;
current = found.children || [];
}
});
return result;
};
export const flattenRoutes = (routes, parentPath = '') => {
return routes.reduce((acc, route) => {
const path = parentPath ? `${parentPath}/${route.path}` : route.path;
acc.push({ ...route, path });
if (route.children) {
acc.push(...flattenRoutes(route.children, path));
}
return acc;
}, []);
};
// 获取所有可选的路由选项(用于菜单管理)
export const getAllRouteOptions = () => {
return flattenRoutes(allRoutes)
.filter(route => !route.hidden)
.map(route => ({
label: route.name,
value: route.key,
isLeaf: !route.children
}));
};

View File

@@ -1,7 +1,7 @@
import React from "react";
import { generateRoutes } from "@/routes/routes";
import * as AntIcons from "@ant-design/icons";
import { ColorIcon } from "@/components/Layout/ColorIcon";
import { allRoutes } from "@/routes/routes";
const getAntIcon = (iconName) => {
const iconKey = `${iconName.charAt(0).toUpperCase()}${iconName.slice(
@@ -10,25 +10,69 @@ const getAntIcon = (iconName) => {
return AntIcons[iconKey] ? React.createElement(AntIcons[iconKey]) : null;
};
const generateMenuItems = (routes, parentPath = "") => {
const generateMenuItems = (routes, menuKeys = [], parentPath = "") => {
return routes
.filter((route) => !route.hidden)
.filter((route) => {
if (!menuKeys.length) return !route.hidden;
const isRouteAllowed = menuKeys.includes(route.key);
// 如果有子路由,只要子路由中有被授权的,父路由就应该显示
if (route.children) {
const hasAllowedChildren = route.children.some(child =>
menuKeys.includes(child.key) ||
(child.children && child.children.some(grandChild => menuKeys.includes(grandChild.key)))
);
return hasAllowedChildren || isRouteAllowed;
}
return isRouteAllowed;
})
.map((route) => {
const fullPath = `${parentPath}/${route.path}`.replace(/\/+/g, "/");
const icon = route.icon && <ColorIcon icon={getAntIcon(route.icon)} />;
const menuItem = {
key: fullPath,
key: route.key, // 使用 key 而不是 fullPath
icon,
label: route.name,
};
if (route.children) {
menuItem.children = generateMenuItems(route.children, fullPath);
const filteredChildren = generateMenuItems(route.children, menuKeys, fullPath);
if (filteredChildren.length > 0) {
menuItem.children = filteredChildren;
}
}
return menuItem;
});
};
export const getMenuItems = (role) => generateMenuItems(generateRoutes(role));
export const getMenuItems = (menuKeys = []) => generateMenuItems(allRoutes, menuKeys);
export const filterRoutesByMenuKeys = (routes, menuKeys) => {
if (!Array.isArray(routes) || !Array.isArray(menuKeys)) {
return [];
}
return routes.reduce((acc, route) => {
// 检查当前路由的 key 是否在 menuKeys 中
const isRouteAllowed = menuKeys.includes(route.key);
// 递归处理子路由
let filteredChildren = [];
if (route.children) {
filteredChildren = filterRoutesByMenuKeys(route.children, menuKeys);
}
// 如果当前路由被允许或者有被允许的子路由,则保留该路由
if (isRouteAllowed || filteredChildren.length > 0) {
acc.push({
...route,
children: filteredChildren.length > 0 ? filteredChildren : undefined
});
}
return acc;
}, []);
};
export { generateMenuItems, getAntIcon };