From b3e0d399da4a321c65efe8c78518707bde60f0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Wed, 9 Apr 2025 17:21:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/superadmin/config/route.php | 11 +- .../superadmin/controller/Menu.php | 151 ++++++++++++++ Server/application/superadmin/model/Menu.php | 144 ++++++++++++++ Server/database/create_menu_table.sql | 35 ++++ Server/scripts/init-menu.php | 173 ++++++++++++++++ SuperAdmin/app/dashboard/layout.tsx | 74 +------ SuperAdmin/components/layout/header.tsx | 77 ++++++++ SuperAdmin/components/layout/sidebar.tsx | 186 ++++++++++++++++++ SuperAdmin/lib/menu-api.ts | 154 +++++++++++++++ 9 files changed, 938 insertions(+), 67 deletions(-) create mode 100644 Server/application/superadmin/controller/Menu.php create mode 100644 Server/application/superadmin/model/Menu.php create mode 100644 Server/database/create_menu_table.sql create mode 100644 Server/scripts/init-menu.php create mode 100644 SuperAdmin/components/layout/header.tsx create mode 100644 SuperAdmin/components/layout/sidebar.tsx create mode 100644 SuperAdmin/lib/menu-api.ts diff --git a/Server/application/superadmin/config/route.php b/Server/application/superadmin/config/route.php index 5413a593..dfec2b8d 100644 --- a/Server/application/superadmin/config/route.php +++ b/Server/application/superadmin/config/route.php @@ -2,4 +2,13 @@ use think\facade\Route; // 超级管理员认证相关路由 -Route::post('auth/login', 'app\\superadmin\\controller\\Auth@login'); \ No newline at end of file +Route::post('auth/login', 'app\\superadmin\\controller\\Auth@login'); + +// 菜单管理相关路由 +Route::group('menu', function () { + Route::get('tree', 'app\\superadmin\\controller\\Menu@getMenuTree'); + Route::get('list', 'app\\superadmin\\controller\\Menu@getMenuList'); + Route::post('save', 'app\\superadmin\\controller\\Menu@saveMenu'); + Route::delete('delete/:id', 'app\\superadmin\\controller\\Menu@deleteMenu'); + Route::post('status', 'app\\superadmin\\controller\\Menu@updateStatus'); +}); \ No newline at end of file diff --git a/Server/application/superadmin/controller/Menu.php b/Server/application/superadmin/controller/Menu.php new file mode 100644 index 00000000..d27fdd2e --- /dev/null +++ b/Server/application/superadmin/controller/Menu.php @@ -0,0 +1,151 @@ +request->param('only_enabled', 1); + $useCache = $this->request->param('use_cache', 1); + + // 调用模型获取菜单树 + $menuTree = MenuModel::getMenuTree($onlyEnabled, $useCache); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $menuTree + ]); + } + + /** + * 获取所有菜单(平铺结构,便于后台管理) + * @return \think\response\Json + */ + public function getMenuList() + { + // 查询条件 + $where = []; + $status = $this->request->param('status'); + if ($status !== null && $status !== '') { + $where[] = ['status', '=', intval($status)]; + } + + // 获取所有菜单 + $menus = MenuModel::where($where) + ->order('sort', 'asc') + ->select(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $menus + ]); + } + + /** + * 添加或更新菜单 + * @return \think\response\Json + */ + public function saveMenu() + { + if (!$this->request->isPost()) { + return json(['code' => 405, 'msg' => '请求方法不允许']); + } + + // 获取参数 + $data = $this->request->post(); + + // 验证参数 + $validate = $this->validate($data, [ + 'title|菜单名称' => 'require|max:50', + 'path|路由路径' => 'require|max:100', + 'parent_id|父菜单ID' => 'require|number', + 'status|状态' => 'require|in:0,1', + 'sort|排序' => 'require|number', + ]); + + if ($validate !== true) { + return json(['code' => 400, 'msg' => $validate]); + } + + // 保存菜单 + $result = MenuModel::saveMenu($data); + + if ($result) { + return json(['code' => 200, 'msg' => '保存成功']); + } else { + return json(['code' => 500, 'msg' => '保存失败']); + } + } + + /** + * 删除菜单 + * @param int $id 菜单ID + * @return \think\response\Json + */ + public function deleteMenu($id) + { + if (!$this->request->isDelete()) { + return json(['code' => 405, 'msg' => '请求方法不允许']); + } + + if (empty($id) || !is_numeric($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $result = MenuModel::deleteMenu($id); + + if ($result) { + return json(['code' => 200, 'msg' => '删除成功']); + } else { + return json(['code' => 500, 'msg' => '删除失败,可能存在子菜单']); + } + } + + /** + * 更新菜单状态 + * @return \think\response\Json + */ + public function updateStatus() + { + if (!$this->request->isPost()) { + return json(['code' => 405, 'msg' => '请求方法不允许']); + } + + $id = $this->request->post('id'); + $status = $this->request->post('status'); + + if (empty($id) || !is_numeric($id) || !in_array($status, [0, 1])) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $menu = MenuModel::find($id); + if (!$menu) { + return json(['code' => 404, 'msg' => '菜单不存在']); + } + + $menu->status = $status; + $result = $menu->save(); + + // 清除缓存 + MenuModel::clearMenuCache(); + + if ($result) { + return json(['code' => 200, 'msg' => '状态更新成功']); + } else { + return json(['code' => 500, 'msg' => '状态更新失败']); + } + } +} \ No newline at end of file diff --git a/Server/application/superadmin/model/Menu.php b/Server/application/superadmin/model/Menu.php new file mode 100644 index 00000000..8e116a15 --- /dev/null +++ b/Server/application/superadmin/model/Menu.php @@ -0,0 +1,144 @@ +order('sort', 'asc') + ->select() + ->toArray(); + + // 组织成树状结构 + $menuTree = self::buildMenuTree($allMenus); + + // 缓存结果 + if ($useCache) { + Cache::set($cacheKey, $menuTree, 3600); // 缓存1小时 + } + + return $menuTree; + } + + /** + * 构建菜单树 + * @param array $menus 所有菜单 + * @param int $parentId 父菜单ID + * @return array + */ + private static function buildMenuTree($menus, $parentId = 0) + { + $tree = []; + + foreach ($menus as $menu) { + if ($menu['parent_id'] == $parentId) { + $children = self::buildMenuTree($menus, $menu['id']); + if (!empty($children)) { + $menu['children'] = $children; + } + $tree[] = $menu; + } + } + + return $tree; + } + + /** + * 清除菜单缓存 + */ + public static function clearMenuCache() + { + Cache::delete('superadmin_menu_tree_enabled'); + Cache::delete('superadmin_menu_tree_all'); + } + + /** + * 添加或更新菜单 + * @param array $data 菜单数据 + * @return bool + */ + public static function saveMenu($data) + { + if (isset($data['id']) && $data['id'] > 0) { + // 更新 + $menu = self::find($data['id']); + if (!$menu) { + return false; + } + $result = $menu->save($data); + } else { + // 新增 + $menu = new self(); + $result = $menu->save($data); + } + + // 清除缓存 + self::clearMenuCache(); + + return $result !== false; + } + + /** + * 删除菜单 + * @param int $id 菜单ID + * @return bool + */ + public static function deleteMenu($id) + { + // 查找子菜单 + $childCount = self::where('parent_id', $id)->count(); + if ($childCount > 0) { + return false; // 有子菜单不能删除 + } + + $result = self::destroy($id); + + // 清除缓存 + self::clearMenuCache(); + + return $result !== false; + } +} \ No newline at end of file diff --git a/Server/database/create_menu_table.sql b/Server/database/create_menu_table.sql new file mode 100644 index 00000000..106bf7af --- /dev/null +++ b/Server/database/create_menu_table.sql @@ -0,0 +1,35 @@ +-- 创建菜单表 +CREATE TABLE IF NOT EXISTS `tk_menus` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `title` varchar(50) NOT NULL COMMENT '菜单名称', + `path` varchar(100) NOT NULL COMMENT '路由路径', + `icon` varchar(50) DEFAULT NULL COMMENT '图标名称', + `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单ID,0表示顶级菜单', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用', + `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序,数值越小越靠前', + `create_time` int(11) DEFAULT NULL COMMENT '创建时间', + `update_time` int(11) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表'; + +-- 插入超级管理员顶级菜单 +INSERT INTO `tk_menus` (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`) VALUES +('仪表盘', '/dashboard', 'LayoutDashboard', 0, 1, 10, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('项目管理', '/dashboard/projects', 'FolderKanban', 0, 1, 20, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('客户池', '/dashboard/customers', 'Users', 0, 1, 30, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('管理员权限', '/dashboard/admins', 'Settings', 0, 1, 40, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('系统设置', '/settings', 'Cog', 0, 1, 50, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); + +-- 插入子菜单 +INSERT INTO `tk_menus` (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`) VALUES +('项目列表', '/dashboard/projects', 'List', 2, 1, 21, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('新建项目', '/dashboard/projects/new', 'PlusCircle', 2, 1, 22, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('客户管理', '/dashboard/customers', 'Users', 3, 1, 31, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('客户分析', '/dashboard/customers/analytics', 'BarChart', 3, 1, 32, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('管理员列表', '/dashboard/admins', 'UserCog', 4, 1, 41, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('角色管理', '/dashboard/admins/roles', 'ShieldCheck', 4, 1, 42, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('权限设置', '/dashboard/admins/permissions', 'Lock', 4, 1, 43, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('基本设置', '/settings/general', 'Settings', 5, 1, 51, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), +('安全设置', '/settings/security', 'Shield', 5, 1, 52, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); \ No newline at end of file diff --git a/Server/scripts/init-menu.php b/Server/scripts/init-menu.php new file mode 100644 index 00000000..d1ed8ad5 --- /dev/null +++ b/Server/scripts/init-menu.php @@ -0,0 +1,173 @@ + Env::get('database.type', 'mysql'), + 'hostname' => Env::get('database.hostname', '127.0.0.1'), + 'database' => Env::get('database.database', 'database'), + 'username' => Env::get('database.username', 'root'), + 'password' => Env::get('database.password', 'root'), + 'hostport' => Env::get('database.hostport', '3306'), + 'charset' => Env::get('database.charset', 'utf8mb4'), + 'prefix' => Env::get('database.prefix', 'tk_'), +]; + +// 连接数据库 +try { + $dsn = "{$dbConfig['type']}:host={$dbConfig['hostname']};port={$dbConfig['hostport']};dbname={$dbConfig['database']};charset={$dbConfig['charset']}"; + $pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password']); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + echo "数据库连接成功!\n"; +} catch (PDOException $e) { + die("数据库连接失败: " . $e->getMessage() . "\n"); +} + +// 创建菜单表SQL +$createTableSql = " +CREATE TABLE IF NOT EXISTS `{$dbConfig['prefix']}menus` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `title` varchar(50) NOT NULL COMMENT '菜单名称', + `path` varchar(100) NOT NULL COMMENT '路由路径', + `icon` varchar(50) DEFAULT NULL COMMENT '图标名称', + `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单ID,0表示顶级菜单', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用', + `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序,数值越小越靠前', + `create_time` int(11) DEFAULT NULL COMMENT '创建时间', + `update_time` int(11) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表'; +"; + +// 执行创建表SQL +try { + $pdo->exec($createTableSql); + echo "菜单表创建成功!\n"; +} catch (PDOException $e) { + echo "菜单表创建失败: " . $e->getMessage() . "\n"; +} + +// 检查表中是否已有数据 +$checkSql = "SELECT COUNT(*) FROM `{$dbConfig['prefix']}menus`"; +try { + $count = $pdo->query($checkSql)->fetchColumn(); + if ($count > 0) { + echo "菜单表中已有 {$count} 条数据,跳过数据初始化\n"; + exit(0); + } +} catch (PDOException $e) { + echo "检查数据失败: " . $e->getMessage() . "\n"; + exit(1); +} + +// 插入顶级菜单数据 +$topMenus = [ + ['仪表盘', '/dashboard', 'LayoutDashboard', 0, 1, 10], + ['项目管理', '/dashboard/projects', 'FolderKanban', 0, 1, 20], + ['客户池', '/dashboard/customers', 'Users', 0, 1, 30], + ['管理员权限', '/dashboard/admins', 'Settings', 0, 1, 40], + ['系统设置', '/settings', 'Cog', 0, 1, 50], +]; + +$insertTopMenuSql = "INSERT INTO `{$dbConfig['prefix']}menus` + (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + +$timestamp = time(); +$insertStmt = $pdo->prepare($insertTopMenuSql); + +$pdo->beginTransaction(); +try { + foreach ($topMenus as $index => $menu) { + $insertStmt->execute([ + $menu[0], // title + $menu[1], // path + $menu[2], // icon + $menu[3], // parent_id + $menu[4], // status + $menu[5], // sort + $timestamp, + $timestamp + ]); + } + $pdo->commit(); + echo "顶级菜单数据插入成功!\n"; +} catch (PDOException $e) { + $pdo->rollBack(); + echo "顶级菜单数据插入失败: " . $e->getMessage() . "\n"; + exit(1); +} + +// 查询刚插入的顶级菜单ID +$menuIds = []; +$queryTopMenuSql = "SELECT id, title FROM `{$dbConfig['prefix']}menus` WHERE parent_id = 0"; +try { + $topMenusResult = $pdo->query($queryTopMenuSql)->fetchAll(PDO::FETCH_ASSOC); + foreach ($topMenusResult as $menu) { + $menuIds[$menu['title']] = $menu['id']; + } +} catch (PDOException $e) { + echo "查询顶级菜单失败: " . $e->getMessage() . "\n"; + exit(1); +} + +// 插入子菜单数据 +$subMenus = [ + ['项目列表', '/dashboard/projects', 'List', $menuIds['项目管理'], 1, 21], + ['新建项目', '/dashboard/projects/new', 'PlusCircle', $menuIds['项目管理'], 1, 22], + ['客户管理', '/dashboard/customers', 'Users', $menuIds['客户池'], 1, 31], + ['客户分析', '/dashboard/customers/analytics', 'BarChart', $menuIds['客户池'], 1, 32], + ['管理员列表', '/dashboard/admins', 'UserCog', $menuIds['管理员权限'], 1, 41], + ['角色管理', '/dashboard/admins/roles', 'ShieldCheck', $menuIds['管理员权限'], 1, 42], + ['权限设置', '/dashboard/admins/permissions', 'Lock', $menuIds['管理员权限'], 1, 43], + ['基本设置', '/settings/general', 'Settings', $menuIds['系统设置'], 1, 51], + ['安全设置', '/settings/security', 'Shield', $menuIds['系统设置'], 1, 52], +]; + +$insertSubMenuSql = "INSERT INTO `{$dbConfig['prefix']}menus` + (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + +$pdo->beginTransaction(); +try { + $insertStmt = $pdo->prepare($insertSubMenuSql); + foreach ($subMenus as $menu) { + $insertStmt->execute([ + $menu[0], // title + $menu[1], // path + $menu[2], // icon + $menu[3], // parent_id + $menu[4], // status + $menu[5], // sort + $timestamp, + $timestamp + ]); + } + $pdo->commit(); + echo "子菜单数据插入成功!\n"; +} catch (PDOException $e) { + $pdo->rollBack(); + echo "子菜单数据插入失败: " . $e->getMessage() . "\n"; + exit(1); +} + +echo "菜单初始化完成!\n"; \ No newline at end of file diff --git a/SuperAdmin/app/dashboard/layout.tsx b/SuperAdmin/app/dashboard/layout.tsx index b44afd4e..557729fe 100644 --- a/SuperAdmin/app/dashboard/layout.tsx +++ b/SuperAdmin/app/dashboard/layout.tsx @@ -1,12 +1,11 @@ "use client" import type React from "react" - import { useState } from "react" -import Link from "next/link" -import { usePathname } from "next/navigation" import { Button } from "@/components/ui/button" -import { LayoutDashboard, Users, Settings, LogOut, Menu, X } from "lucide-react" +import { Menu, X, LogOut } from "lucide-react" +import { Sidebar } from "@/components/layout/sidebar" +import { Header } from "@/components/layout/header" export default function DashboardLayout({ children, @@ -14,25 +13,6 @@ export default function DashboardLayout({ children: React.ReactNode }) { const [sidebarOpen, setSidebarOpen] = useState(true) - const pathname = usePathname() - - const navItems = [ - { - title: "项目管理", - href: "/dashboard/projects", - icon: , - }, - { - title: "客户池", - href: "/dashboard/customers", - icon: , - }, - { - title: "管理员权限", - href: "/dashboard/admins", - icon: , - }, - ] return (
@@ -45,55 +25,17 @@ export default function DashboardLayout({ {/* Sidebar */}
-
-
-

超级管理员后台

-
- -
- -
-
+
{/* Main content */} -
-
-

- {navItems.find((item) => pathname.startsWith(item.href))?.title || "仪表盘"} -

-
-
{children}
+
+
+
{children}
) diff --git a/SuperAdmin/components/layout/header.tsx b/SuperAdmin/components/layout/header.tsx new file mode 100644 index 00000000..4b8c5321 --- /dev/null +++ b/SuperAdmin/components/layout/header.tsx @@ -0,0 +1,77 @@ +"use client" + +import { useEffect, useState } from "react" +import { LogOut, Settings, User } from "lucide-react" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +interface AdminInfo { + id: number; + name: string; + account: string; +} + +export function Header() { + const [adminInfo, setAdminInfo] = useState(null) + + useEffect(() => { + // 从本地存储获取管理员信息 + const info = localStorage.getItem("admin_info") + if (info) { + try { + setAdminInfo(JSON.parse(info)) + } catch (e) { + console.error("解析管理员信息失败", e) + } + } + }, []) + + const handleLogout = () => { + localStorage.removeItem("admin_token") + localStorage.removeItem("admin_info") + window.location.href = "/login" + } + + return ( +
+
+ +
+ + + + + +
+ {adminInfo?.name || "管理员"} +
+
+ {adminInfo?.account || ""} +
+ + + + + 设置 + + + + + + 退出登录 + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/SuperAdmin/components/layout/sidebar.tsx b/SuperAdmin/components/layout/sidebar.tsx new file mode 100644 index 00000000..211b1607 --- /dev/null +++ b/SuperAdmin/components/layout/sidebar.tsx @@ -0,0 +1,186 @@ +"use client" + +import { useEffect, useState } from "react" +import Link from "next/link" +import { usePathname } from "next/navigation" +import { getMenus, type MenuItem } from "@/lib/menu-api" +import * as LucideIcons from "lucide-react" +import { ChevronDown, ChevronRight } from "lucide-react" + +export function Sidebar() { + const pathname = usePathname() + const [menus, setMenus] = useState([]) + const [loading, setLoading] = useState(true) + // 使用Set来存储已展开的菜单ID + const [expandedMenus, setExpandedMenus] = useState>(new Set()) + + useEffect(() => { + const fetchMenus = async () => { + setLoading(true) + try { + const data = await getMenus() + setMenus(data || []) + + // 自动展开当前活动菜单的父菜单 + autoExpandActiveMenuParent(data || []); + } catch (error) { + console.error("获取菜单失败:", error) + } finally { + setLoading(false) + } + } + + fetchMenus() + }, []) + + // 自动展开当前活动菜单的父菜单 + const autoExpandActiveMenuParent = (menuItems: MenuItem[]) => { + const newExpandedMenus = new Set(); + + // 递归查找当前路径匹配的菜单项 + const findActiveMenu = (items: MenuItem[], parentIds: number[] = []) => { + for (const item of items) { + const currentPath = pathname === "/" ? "/dashboard" : pathname; + const itemPath = item.path; + + if (currentPath === itemPath || currentPath.startsWith(itemPath + "/")) { + // 将所有父菜单ID添加到展开集合 + parentIds.forEach(id => newExpandedMenus.add(id)); + return true; + } + + if (item.children && item.children.length > 0) { + const found = findActiveMenu(item.children, [...parentIds, item.id]); + if (found) { + return true; + } + } + } + return false; + }; + + findActiveMenu(menuItems); + setExpandedMenus(newExpandedMenus); + }; + + // 切换菜单展开状态 + const toggleMenu = (menuId: number) => { + setExpandedMenus(prev => { + const newExpanded = new Set(prev); + if (newExpanded.has(menuId)) { + newExpanded.delete(menuId); + } else { + newExpanded.add(menuId); + } + return newExpanded; + }); + }; + + // 获取Lucide图标组件 + const getLucideIcon = (iconName: string) => { + if (!iconName) return null; + const Icon = (LucideIcons as any)[iconName]; + return Icon ? : null; + }; + + // 递归渲染菜单项 + const renderMenuItem = (item: MenuItem) => { + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedMenus.has(item.id); + const isActive = pathname === item.path; + const isChildActive = hasChildren && item.children!.some(child => + pathname === child.path || pathname.startsWith(child.path + "/") + ); + + return ( +
  • + {hasChildren ? ( +
    + + + {isExpanded && hasChildren && ( +
      + {item.children!.map(child => { + const isChildItemActive = pathname === child.path; + return ( +
    • + + {child.icon && getLucideIcon(child.icon)} + {child.title} + +
    • + ); + })} +
    + )} +
    + ) : ( + + {item.icon && getLucideIcon(item.icon)} + {item.title} + + )} +
  • + ); + }; + + return ( +
    +
    +

    超级管理员

    +
    + + +
    + ); +} \ No newline at end of file diff --git a/SuperAdmin/lib/menu-api.ts b/SuperAdmin/lib/menu-api.ts new file mode 100644 index 00000000..8d804429 --- /dev/null +++ b/SuperAdmin/lib/menu-api.ts @@ -0,0 +1,154 @@ +/** + * 菜单项接口 + */ +export interface MenuItem { + id: number; + title: string; + path: string; + icon?: string; + parent_id: number; + status: number; + sort: number; + children?: MenuItem[]; +} + +/** + * 从服务器获取菜单数据 + * @param onlyEnabled 是否只获取启用的菜单 + * @returns Promise + */ +export async function getMenus(onlyEnabled: boolean = true): Promise { + try { + // API基础路径从环境变量获取 + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; + + // 构建API URL + const url = `${apiBaseUrl}/menu/tree?only_enabled=${onlyEnabled ? 1 : 0}`; + + // 获取存储的token + const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null; + + // 发起请求 + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }, + credentials: 'include' + }); + + // 处理响应 + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + if (result.code === 200) { + return result.data; + } else { + console.error('获取菜单失败:', result.msg); + return []; + } + } catch (error) { + console.error('获取菜单出错:', error); + return []; + } +} + +/** + * 保存菜单 + * @param menuData 菜单数据 + * @returns Promise + */ +export async function saveMenu(menuData: Partial): Promise { + try { + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; + const url = `${apiBaseUrl}/menu/save`; + const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }, + body: JSON.stringify(menuData) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result.code === 200; + } catch (error) { + console.error('保存菜单出错:', error); + return false; + } +} + +/** + * 删除菜单 + * @param id 菜单ID + * @returns Promise + */ +export async function deleteMenu(id: number): Promise { + try { + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; + const url = `${apiBaseUrl}/menu/delete/${id}`; + const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null; + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result.code === 200; + } catch (error) { + console.error('删除菜单出错:', error); + return false; + } +} + +/** + * 更新菜单状态 + * @param id 菜单ID + * @param status 状态 (0-禁用, 1-启用) + * @returns Promise + */ +export async function updateMenuStatus(id: number, status: 0 | 1): Promise { + try { + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; + const url = `${apiBaseUrl}/menu/status`; + const token = typeof window !== 'undefined' ? localStorage.getItem('admin_token') : null; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }, + body: JSON.stringify({ id, status }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result.code === 200; + } catch (error) { + console.error('更新菜单状态出错:', error); + return false; + } +} \ No newline at end of file