소스 검색

fix: 调整目录结构,增加路由鉴权白名单

周玉环 1 일 전
부모
커밋
34bd5d4c36

+ 3 - 8
src/contexts/AuthContext.tsx

@@ -3,7 +3,6 @@ import type { ReactNode } from "react";
 import type { JeecgMenu } from "@/types/menu";
 import { getUserPermissions } from "@/services/api";
 import { isPublicRoute } from "@/router/publicRoutes.tsx";
-// import { systemMenus } from "@/config/systemMenus";
 
 interface AuthContextType {
   menus: JeecgMenu[];
@@ -28,15 +27,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
       const [menusData] = await Promise.all([
         getUserPermissions(),
       ]);
-      setMenus(menusData.menu);
-
-      // 合并系统菜单和用户菜单
-      // const allMenus = [...(menusData.menu || []), ...systemMenus];
-      // setMenus(allMenus);
+      // 只设置用户菜单,系统管理菜单通过设置菜单访问
+      setMenus(menusData.menu || []);
     } catch (error) {
       console.error("加载权限数据失败:", error);
-      // 如果获取用户菜单失败,至少显示系统管理菜单
-      // setMenus(systemMenus);
+      setMenus([]);
     } finally {
       setLoading(false);
     }

+ 75 - 0
src/layouts/Header/SystemMenuItems.tsx

@@ -0,0 +1,75 @@
+/**
+ * 系统管理菜单项组件
+ * 只在设置菜单中显示,不在主导航中显示
+ */
+import { Navbar } from "@contentful/f36-navbar";
+import { useNavigate } from "react-router-dom";
+
+interface SystemMenuItem {
+  title: string;
+  path: string;
+  description: string;
+  permission?: string; // 权限标识
+}
+
+export default function SystemMenuItems() {
+  const navigate = useNavigate();
+
+  const systemMenuItems: SystemMenuItem[] = [
+    {
+      title: "菜单管理",
+      path: "/system/menu",
+      description: "管理系统菜单结构和权限",
+      permission: "system:menu:view"
+    },
+    {
+      title: "用户管理", 
+      path: "/system/user",
+      description: "管理系统用户账号",
+      permission: "system:user:view"
+    },
+    {
+      title: "角色管理",
+      path: "/system/role", 
+      description: "管理用户角色和权限分配",
+      permission: "system:role:view"
+    },
+    {
+      title: "日志管理",
+      path: "/system/log",
+      description: "查看系统操作日志",
+      permission: "system:log:view"
+    }
+  ];
+
+  const handleMenuClick = (path: string) => {
+    navigate(path);
+  };
+
+  // TODO: 这里可以添加权限检查逻辑
+  // const hasPermission = (permission?: string) => {
+  //   if (!permission) return true;
+  //   // 检查用户是否有对应权限
+  //   return true;
+  // };
+
+  // 过滤有权限的菜单项
+  const visibleMenuItems = systemMenuItems.filter(() => {
+    // TODO: 添加权限检查
+    // return hasPermission(item.permission);
+    return true; // 暂时显示所有菜单
+  });
+
+  return (
+    <>
+      <Navbar.MenuSectionTitle>系统管理</Navbar.MenuSectionTitle>
+      {visibleMenuItems.map((item) => (
+        <Navbar.MenuItem
+          key={item.path}
+          title={item.title}
+          onClick={() => handleMenuClick(item.path)}
+        />
+      ))}
+    </>
+  );
+}

+ 13 - 6
src/layouts/Header/index.tsx

@@ -9,6 +9,7 @@ import { useMenu } from "@/contexts/MenuContext";
 import { useNavigate, useLocation } from "react-router-dom";
 import { useEffect, useState } from "react";
 import SearchModal from "@/components/SearchModal";
+// import SystemMenuItems from "./SystemMenuItems";
 import { navigateToJeecgByKey } from "@/utils/jeecgNavigation";
 
 import type { JeecgMenu } from "@/types/menu";
@@ -191,16 +192,22 @@ export default function Header() {
         />
       </Navbar.Item>
       <Navbar.Item label="设置菜单" icon={<GearSixIcon />}>
+        {/* <SystemMenuItems /> */}
         <Navbar.MenuSectionTitle>通用</Navbar.MenuSectionTitle>
         <Navbar.MenuItem
-          title="菜单设置"
+          title="系统管理"
           onClick={() => navigateToJeecgByKey("menu")}
         />
-        <Navbar.MenuItem title="用户管理" />
-        <Navbar.MenuItem title="角色管理" />
-        <Navbar.MenuSectionTitle>空间</Navbar.MenuSectionTitle>
-        <Navbar.MenuItem title="应用" />
-        <Navbar.MenuItem title="权限" onClick={toPermissionPage} />
+        <Navbar.MenuDivider />
+        <Navbar.MenuSectionTitle>空间设置</Navbar.MenuSectionTitle>
+        <Navbar.MenuItem title="应用管理" />
+        <Navbar.MenuItem title="权限设置" onClick={toPermissionPage} />
+        <Navbar.MenuDivider />
+        <Navbar.MenuSectionTitle>其他</Navbar.MenuSectionTitle>
+        <Navbar.MenuItem
+          title="系统信息"
+          onClick={() => navigate("/system/info")}
+        />
       </Navbar.Item>
     </>
   );

+ 0 - 63
src/pages/system-test/index.tsx

@@ -1,63 +0,0 @@
-/**
- * 系统管理测试页面
- */
-import { Card, Stack, Text, Button, Flex } from "@contentful/f36-components";
-import { Link } from "react-router-dom";
-
-export default function SystemTest() {
-  return (
-    <Card>
-      <Stack spacing="spacingM">
-        <Text fontSize="fontSizeL" fontWeight="fontWeightMedium">
-          系统管理功能测试
-        </Text>
-        
-        <Text>
-          这是一个测试页面,用于验证系统管理功能是否正常工作。
-        </Text>
-
-        <Stack spacing="spacingS">
-          <Text fontWeight="fontWeightMedium">可用的系统管理功能:</Text>
-          
-          <Flex gap="spacingS" flexWrap="wrap">
-            <Button as={Link} to="/system/menu" variant="primary">
-              菜单管理
-            </Button>
-            <Button as={Link} to="/system/user" variant="primary">
-              用户管理
-            </Button>
-            <Button as={Link} to="/system/role" variant="primary">
-              角色管理
-            </Button>
-            <Button as={Link} to="/system/log" variant="primary">
-              日志管理
-            </Button>
-          </Flex>
-        </Stack>
-
-        <Stack spacing="spacingS">
-          <Text fontWeight="fontWeightMedium">功能特点:</Text>
-          <ul>
-            <li>使用 Contentful F36 组件库,保持一致的设计风格</li>
-            <li>完整的 CRUD 操作支持</li>
-            <li>分页、搜索、批量操作</li>
-            <li>权限管理和角色分配</li>
-            <li>操作日志记录和查看</li>
-            <li>响应式设计,适配不同屏幕尺寸</li>
-          </ul>
-        </Stack>
-
-        <Stack spacing="spacingS">
-          <Text fontWeight="fontWeightMedium">技术栈:</Text>
-          <ul>
-            <li>React 19 + TypeScript</li>
-            <li>Contentful F36 Components</li>
-            <li>React Router v7</li>
-            <li>Axios HTTP 客户端</li>
-            <li>Vite 构建工具</li>
-          </ul>
-        </Stack>
-      </Stack>
-    </Card>
-  );
-}

+ 51 - 2
src/pages/system/menu/MenuForm.tsx → src/pages/system/menu/components/MenuForm.tsx

@@ -12,18 +12,51 @@ import {
   Flex,
 } from "@contentful/f36-components";
 import type { SysMenu } from "@/types/system";
-import { MENU_TYPE_OPTIONS, FORM_FIELDS, SWITCH_FIELDS } from "./constants";
+import { MENU_TYPE_OPTIONS, FORM_FIELDS, SWITCH_FIELDS } from "../constants/config";
 
 interface MenuFormProps {
   form: Partial<SysMenu>;
   onChange: (form: Partial<SysMenu>) => void;
+  menuList?: SysMenu[]; // 用于选择父菜单
 }
 
-export default function MenuForm({ form, onChange }: MenuFormProps) {
+export default function MenuForm({ form, onChange, menuList = [] }: MenuFormProps) {
   const handleInputChange = (field: keyof SysMenu, value: any) => {
     onChange({ ...form, [field]: value });
   };
 
+  // 获取扁平化的菜单列表用于父菜单选择
+  const getFlatMenuList = (menus: SysMenu[], level = 0): Array<{ id: string; name: string; level: number }> => {
+    const result: Array<{ id: string; name: string; level: number }> = [];
+    menus.forEach((menu) => {
+      // 排除当前编辑的菜单及其子菜单
+      if (form.id && (menu.id === form.id || isDescendant(menu, form.id))) {
+        return;
+      }
+      result.push({
+        id: menu.id,
+        name: menu.name,
+        level,
+      });
+      if (menu.children && menu.children.length > 0) {
+        result.push(...getFlatMenuList(menu.children, level + 1));
+      }
+    });
+    return result;
+  };
+
+  // 检查是否是子菜单
+  const isDescendant = (menu: SysMenu, targetId: string): boolean => {
+    if (menu.children) {
+      return menu.children.some(child => 
+        child.id === targetId || isDescendant(child, targetId)
+      );
+    }
+    return false;
+  };
+
+  const flatMenuList = getFlatMenuList(menuList);
+
   return (
     <Form>
       <Stack spacing="spacingM">
@@ -37,6 +70,22 @@ export default function MenuForm({ form, onChange }: MenuFormProps) {
           />
         </FormControl>
 
+        {/* 父菜单 */}
+        <FormControl>
+          <FormControl.Label>父菜单</FormControl.Label>
+          <Select
+            value={form.parentId || ""}
+            onChange={(e) => handleInputChange("parentId", e.target.value || undefined)}
+          >
+            <Select.Option value="">顶级菜单</Select.Option>
+            {flatMenuList.map((menu) => (
+              <Select.Option key={menu.id} value={menu.id}>
+                {" ".repeat(menu.level)}{menu.name}
+              </Select.Option>
+            ))}
+          </Select>
+        </FormControl>
+
         {/* 菜单类型 */}
         <FormControl>
           <FormControl.Label>{FORM_FIELDS.menuType.label}</FormControl.Label>

+ 183 - 0
src/pages/system/menu/components/MenuTreeTable.tsx

@@ -0,0 +1,183 @@
+/**
+ * 菜单树形表格组件
+ */
+import { useState } from "react";
+import {
+  Table,
+  Flex,
+  Text,
+  Button,
+} from "@contentful/f36-components";
+import type { SysMenu } from "@/types/system";
+import { createColumns } from "../constants/columns";
+
+interface MenuTreeTableProps {
+  menuList: SysMenu[];
+  selectedRows: string[];
+  onSelectAll: (checked: boolean) => void;
+  onSelectRow: (id: string, checked: boolean) => void;
+  onEdit: (record: SysMenu) => void;
+  onDelete: (id: string) => void;
+}
+
+interface MenuTreeRowProps {
+  menu: SysMenu;
+  level: number;
+  selectedRows: string[];
+  onSelectRow: (id: string, checked: boolean) => void;
+  onEdit: (record: SysMenu) => void;
+  onDelete: (id: string) => void;
+  columns: any[];
+}
+
+// 单个菜单行组件
+function MenuTreeRow({
+  menu,
+  level,
+  selectedRows,
+  onSelectRow,
+  onEdit,
+  onDelete,
+  columns,
+}: MenuTreeRowProps) {
+  const [expanded, setExpanded] = useState(true);
+  const hasChildren = menu.children && menu.children.length > 0;
+
+  // 缩进样式
+  const indentStyle = {
+    paddingLeft: `${level * 20}px`,
+  };
+
+  return (
+    <>
+      <Table.Row>
+        <Table.Cell>
+          <input
+            type="checkbox"
+            checked={selectedRows.includes(menu.id)}
+            onChange={(e) => onSelectRow(menu.id, e.target.checked)}
+          />
+        </Table.Cell>
+        {columns.map((col, index) => (
+          <Table.Cell key={col.accessor}>
+            {index === 0 ? (
+              // 第一列显示层级和展开/收起按钮
+              <Flex alignItems="center" style={indentStyle}>
+                {hasChildren && (
+                  <Button
+                    variant="transparent"
+                    size="small"
+                    onClick={() => setExpanded(!expanded)}
+                    style={{
+                      minWidth: "20px",
+                      padding: "0 4px",
+                      marginRight: "8px",
+                    }}
+                  >
+                    {expanded ? "−" : "+"}
+                  </Button>
+                )}
+                {!hasChildren && (
+                  <span
+                    style={{
+                      display: "inline-block",
+                      width: "20px",
+                      marginRight: "8px",
+                    }}
+                  />
+                )}
+                {col.render ? (
+                  col.render(menu[col.accessor as keyof SysMenu], menu)
+                ) : (
+                  <Text>{String(menu[col.accessor as keyof SysMenu] || "")}</Text>
+                )}
+              </Flex>
+            ) : col.render ? (
+              col.render(menu[col.accessor as keyof SysMenu], menu)
+            ) : (
+              String(menu[col.accessor as keyof SysMenu] || "")
+            )}
+          </Table.Cell>
+        ))}
+      </Table.Row>
+      {/* 递归渲染子菜单 */}
+      {hasChildren &&
+        expanded &&
+        menu.children!.map((child) => (
+          <MenuTreeRow
+            key={child.id}
+            menu={child}
+            level={level + 1}
+            selectedRows={selectedRows}
+            onSelectRow={onSelectRow}
+            onEdit={onEdit}
+            onDelete={onDelete}
+            columns={columns}
+          />
+        ))}
+    </>
+  );
+}
+
+export default function MenuTreeTable({
+  menuList,
+  selectedRows,
+  onSelectAll,
+  onSelectRow,
+  onEdit,
+  onDelete,
+}: MenuTreeTableProps) {
+  // 创建表格列配置
+  const columns = createColumns(onEdit, onDelete);
+
+  // 获取所有菜单ID(包括子菜单)
+  const getAllMenuIds = (menus: SysMenu[]): string[] => {
+    const ids: string[] = [];
+    const traverse = (menuItems: SysMenu[]) => {
+      menuItems.forEach((menu) => {
+        ids.push(menu.id);
+        if (menu.children && menu.children.length > 0) {
+          traverse(menu.children);
+        }
+      });
+    };
+    traverse(menus);
+    return ids;
+  };
+
+  const allMenuIds = getAllMenuIds(menuList);
+  const isAllSelected = allMenuIds.length > 0 && allMenuIds.every(id => selectedRows.includes(id));
+
+  return (
+    <Table>
+      <Table.Head>
+        <Table.Row>
+          <Table.Cell>
+            <input
+              type="checkbox"
+              checked={isAllSelected}
+              onChange={(e) => onSelectAll(e.target.checked)}
+            />
+          </Table.Cell>
+          {columns.map((col) => (
+            <Table.Cell key={col.accessor}>{col.header}</Table.Cell>
+          ))}
+        </Table.Row>
+      </Table.Head>
+      <Table.Body>
+        {menuList.map((menu) => (
+          <MenuTreeRow
+            key={menu.id}
+            menu={menu}
+            level={0}
+            selectedRows={selectedRows}
+            onSelectRow={onSelectRow}
+            onEdit={onEdit}
+            onDelete={onDelete}
+            columns={columns}
+          />
+        ))}
+      </Table.Body>
+    </Table>
+  );
+}

+ 5 - 0
src/pages/system/menu/components/index.ts

@@ -0,0 +1,5 @@
+/**
+ * 菜单管理组件统一导出
+ */
+export { default as MenuForm } from './MenuForm';
+export { default as MenuTreeTable } from './MenuTreeTable';

+ 1 - 1
src/pages/system/menu/columns.tsx → src/pages/system/menu/constants/columns.tsx

@@ -3,7 +3,7 @@
  */
 import { Text, Badge, Flex, Button } from "@contentful/f36-components";
 import type { SysMenu } from "@/types/system";
-import { MENU_TYPE_CONFIG, MENU_STATUS_CONFIG } from "./constants";
+import { MENU_TYPE_CONFIG, MENU_STATUS_CONFIG } from "./config";
 
 // 渲染菜单类型
 export const renderMenuType = (type: number) => {

+ 0 - 0
src/pages/system/menu/constants.ts → src/pages/system/menu/constants/config.ts


+ 5 - 0
src/pages/system/menu/constants/index.ts

@@ -0,0 +1,5 @@
+/**
+ * 菜单管理常量统一导出
+ */
+export { createColumns } from './columns';
+export * from './config';

+ 4 - 0
src/pages/system/menu/hooks/index.ts

@@ -0,0 +1,4 @@
+/**
+ * 菜单管理hooks统一导出
+ */
+export { useMenuManagement } from './useMenuManagement';

+ 18 - 2
src/pages/system/menu/useMenuManagement.ts → src/pages/system/menu/hooks/useMenuManagement.ts

@@ -4,7 +4,7 @@
 import { useState, useEffect } from "react";
 import { menuApi } from "@/services";
 import type { SysMenu } from "@/types/system";
-import { DEFAULT_FORM_DATA } from "./constants";
+import { DEFAULT_FORM_DATA } from "../constants/config";
 
 export function useMenuManagement() {
   const [loading, setLoading] = useState(false);
@@ -90,10 +90,26 @@ export function useMenuManagement() {
     setEditingMenu(null);
   };
 
+  // 获取所有菜单ID(包括子菜单)
+  const getAllMenuIds = (menus: SysMenu[]): string[] => {
+    const ids: string[] = [];
+    const traverse = (menuItems: SysMenu[]) => {
+      menuItems.forEach((menu) => {
+        ids.push(menu.id);
+        if (menu.children && menu.children.length > 0) {
+          traverse(menu.children);
+        }
+      });
+    };
+    traverse(menus);
+    return ids;
+  };
+
   // 处理全选
   const handleSelectAll = (checked: boolean) => {
     if (checked) {
-      setSelectedRows(menuList.map((item) => item.id));
+      const allIds = getAllMenuIds(menuList);
+      setSelectedRows(allIds);
     } else {
       setSelectedRows([]);
     }

+ 14 - 45
src/pages/system/menu/index.tsx

@@ -3,16 +3,15 @@
  */
 import {
   Card,
-  Table,
   Button,
   Stack,
   Flex,
-  Text,
   Modal,
 } from "@contentful/f36-components";
-import { useMenuManagement } from "./useMenuManagement";
-import { createColumns } from "./columns";
-import MenuForm from "./MenuForm";
+import { useMenuManagement } from "./hooks/useMenuManagement";
+import MenuForm from "./components/MenuForm";
+import MenuTreeTable from "./components/MenuTreeTable";
+
 
 export default function MenuManagement() {
   const {
@@ -34,9 +33,6 @@ export default function MenuManagement() {
     handleSelectRow,
   } = useMenuManagement();
 
-  // 创建表格列配置
-  const columns = createColumns(handleEdit, handleDelete);
-
   return (
     <Card>
       <Stack spacing="spacingM" flexDirection="column">
@@ -67,42 +63,15 @@ export default function MenuManagement() {
           </Flex>
         </Flex>
 
-        {/* 表格 */}
-        <Table>
-          <Table.Head>
-            <Table.Row>
-              <Table.Cell>
-                <input
-                  type="checkbox"
-                  onChange={(e) => handleSelectAll(e.target.checked)}
-                />
-              </Table.Cell>
-              {columns.map((col) => (
-                <Table.Cell key={col.accessor}>{col.header}</Table.Cell>
-              ))}
-            </Table.Row>
-          </Table.Head>
-          <Table.Body>
-            {menuList.map((item) => (
-              <Table.Row key={item.id}>
-                <Table.Cell>
-                  <input
-                    type="checkbox"
-                    checked={selectedRows.includes(item.id)}
-                    onChange={(e) => handleSelectRow(item.id, e.target.checked)}
-                  />
-                </Table.Cell>
-                {columns.map((col) => (
-                  <Table.Cell key={col.accessor}>
-                    {col.render
-                      ? col.render(item[col.accessor], item)
-                      : String(item[col.accessor] || "")}
-                  </Table.Cell>
-                ))}
-              </Table.Row>
-            ))}
-          </Table.Body>
-        </Table>
+        {/* 树形表格 */}
+        <MenuTreeTable
+          menuList={menuList}
+          selectedRows={selectedRows}
+          onSelectAll={handleSelectAll}
+          onSelectRow={handleSelectRow}
+          onEdit={handleEdit}
+          onDelete={handleDelete}
+        />
       </Stack>
 
       {/* 添加/编辑弹窗 */}
@@ -112,7 +81,7 @@ export default function MenuManagement() {
           onClose={handleCancel}
         />
         <Modal.Content>
-          <MenuForm form={form} onChange={setForm} />
+          <MenuForm form={form} onChange={setForm} menuList={menuList} />
         </Modal.Content>
         <Modal.Controls>
           <Button variant="secondary" onClick={handleCancel}>

+ 45 - 0
src/pages/system/menu/styles/MenuTreeTable.module.css

@@ -0,0 +1,45 @@
+/* 菜单树形表格样式 */
+.treeRow {
+  transition: background-color 0.2s ease;
+}
+
+.treeRow:hover {
+  background-color: #f8f9fa;
+}
+
+.expandButton {
+  cursor: pointer;
+  user-select: none;
+  font-family: monospace;
+  font-weight: bold;
+  color: #666;
+  border: none;
+  background: none;
+  padding: 2px 4px;
+  border-radius: 2px;
+}
+
+.expandButton:hover {
+  background-color: #e9ecef;
+  color: #333;
+}
+
+.indentLine {
+  border-left: 1px dashed #ddd;
+  margin-left: 10px;
+  padding-left: 10px;
+}
+
+.menuLevel0 {
+  font-weight: 600;
+}
+
+.menuLevel1 {
+  font-weight: 500;
+  color: #666;
+}
+
+.menuLevel2 {
+  font-weight: normal;
+  color: #888;
+}

+ 4 - 0
src/pages/system/menu/styles/index.ts

@@ -0,0 +1,4 @@
+/**
+ * 菜单管理样式统一导出
+ */
+export { default as menuTreeTableStyles } from "./MenuTreeTable.module.css";

+ 2 - 2
src/router/index.tsx

@@ -11,7 +11,7 @@ import {
 } from "react-router-dom";
 import MainLayout from "@/layouts/MainLayout/index";
 import { publicRoutes } from "./publicRoutes.tsx";
-// import { systemRoutes } from "./systemRoutes.tsx";
+import { systemRoutes } from "./systemRoutes.tsx";
 
 import type { JeecgMenu } from "@/types/menu";
 
@@ -185,7 +185,7 @@ function generateProtectedRoutes(menus: JeecgMenu[]): RouteObject {
       },
       ...routes,
       // 添加系统管理路由
-      // systemRoutes,
+      systemRoutes,
     ],
   };
 }