From c24f6541b6a0de1a2e0b53970b9895319691fde1 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 16:41:05 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../superadmin/data/administrator.sql | 2 -- SuperAdmin/app/dashboard/page.tsx | 16 +++++++--------- SuperAdmin/app/login/page.tsx | 12 ++++++------ 3 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 Server/application/superadmin/data/administrator.sql diff --git a/Server/application/superadmin/data/administrator.sql b/Server/application/superadmin/data/administrator.sql deleted file mode 100644 index 02073d56..00000000 --- a/Server/application/superadmin/data/administrator.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT INTO `tk_administrators` (`name`, `account`, `password`, `status`, `createTime`, `updateTime`) -VALUES ('超级管理员', 'admin', MD5('123456'), 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); \ No newline at end of file diff --git a/SuperAdmin/app/dashboard/page.tsx b/SuperAdmin/app/dashboard/page.tsx index 2daa9ee4..a183c5b7 100644 --- a/SuperAdmin/app/dashboard/page.tsx +++ b/SuperAdmin/app/dashboard/page.tsx @@ -17,28 +17,26 @@ export default function DashboardPage() { const adminInfo = localStorage.getItem("admin_info") if (adminInfo) { try { - const { name } = JSON.parse(adminInfo) - setUserName(name || "管理员") + const userData = JSON.parse(adminInfo) + setUserName(userData.name || "管理员") } catch (err) { console.error("解析用户信息失败:", err) + setUserName("管理员") } } // 获取当前时间 const hour = new Date().getHours() - let timeGreeting = "" if (hour >= 5 && hour < 12) { - timeGreeting = "上午好" + setGreeting("上午好") } else if (hour >= 12 && hour < 14) { - timeGreeting = "中午好" + setGreeting("中午好") } else if (hour >= 14 && hour < 18) { - timeGreeting = "下午好" + setGreeting("下午好") } else { - timeGreeting = "晚上好" + setGreeting("晚上好") } - - setGreeting(timeGreeting) }, []) return ( diff --git a/SuperAdmin/app/login/page.tsx b/SuperAdmin/app/login/page.tsx index 0397d814..5109a840 100644 --- a/SuperAdmin/app/login/page.tsx +++ b/SuperAdmin/app/login/page.tsx @@ -11,7 +11,7 @@ import { Label } from "@/components/ui/label" import { md5 } from "@/lib/utils" export default function LoginPage() { - const [username, setUsername] = useState("") + const [account, setAccount] = useState("") const [password, setPassword] = useState("") const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState("") @@ -33,7 +33,7 @@ export default function LoginPage() { "Content-Type": "application/json", }, body: JSON.stringify({ - account: username, + account, password: encryptedPassword }), credentials: "include" @@ -70,12 +70,12 @@ export default function LoginPage() {
- + setUsername(e.target.value)} + value={account} + onChange={(e) => setAccount(e.target.value)} required />
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 2/8] =?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 From c451d5b0eb82f66d461712b76cbbe7f181b6a88b 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 18:13:46 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/superadmin/config/route.php | 11 +- .../superadmin/controller/Administrator.php | 147 ++++++++++++++++++ 2 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 Server/application/superadmin/controller/Administrator.php diff --git a/Server/application/superadmin/config/route.php b/Server/application/superadmin/config/route.php index dfec2b8d..4a086ac1 100644 --- a/Server/application/superadmin/config/route.php +++ b/Server/application/superadmin/config/route.php @@ -8,7 +8,12 @@ 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'); +}); + +// 管理员相关路由 +Route::group('administrator', function () { + // 获取管理员列表 + Route::get('list', 'app\\superadmin\\controller\\Administrator@getList'); + // 获取管理员详情 + Route::get('detail/:id', 'app\\superadmin\\controller\\Administrator@getDetail'); }); \ No newline at end of file diff --git a/Server/application/superadmin/controller/Administrator.php b/Server/application/superadmin/controller/Administrator.php new file mode 100644 index 00000000..1509e487 --- /dev/null +++ b/Server/application/superadmin/controller/Administrator.php @@ -0,0 +1,147 @@ +request->param('page/d', 1); + $limit = $this->request->param('limit/d', 10); + $keyword = $this->request->param('keyword/s', ''); + + // 构建查询条件 + $where = [ + ['deleteTime', '=', 0] + ]; + + // 如果有搜索关键词 + if (!empty($keyword)) { + $where[] = ['account|name', 'like', "%{$keyword}%"]; + } + + // 查询管理员数据 + $total = AdminModel::where($where)->count(); + $list = AdminModel::where($where) + ->field('id, account, name, status, authId, createTime, lastLoginTime, lastLoginIp') + ->order('id', 'desc') + ->page($page, $limit) + ->select(); + + // 格式化数据 + $data = []; + foreach ($list as $item) { + $data[] = [ + 'id' => $item->id, + 'username' => $item->account, + 'name' => $item->name, + 'role' => $this->getRoleName($item->authId), + 'status' => $item->status, + 'createdAt' => $item->createTime, + 'lastLogin' => !empty($item->lastLoginTime) ? date('Y-m-d H:i', $item->lastLoginTime) : '从未登录', + 'permissions' => $this->getPermissions($item->authId) + ]; + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $data, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取详细信息 + * @param int $id 管理员ID + * @return \think\response\Json + */ + public function getDetail($id) + { + // 查询管理员信息 + $admin = AdminModel::where('id', $id) + ->where('deleteTime', 0) + ->field('id, account, name, status, authId, createTime, lastLoginTime') + ->find(); + + // 如果查不到记录 + if (!$admin) { + return json([ + 'code' => 404, + 'msg' => '管理员不存在', + 'data' => null + ]); + } + + // 格式化数据 + $data = [ + 'id' => $admin->id, + 'username' => $admin->account, + 'name' => $admin->name, + 'status' => $admin->status, + 'authId' => $admin->authId, + 'roleName' => $this->getRoleName($admin->authId), + 'createdAt' => date('Y-m-d', $admin->createTime), + 'lastLogin' => !empty($admin->lastLoginTime) ? date('Y-m-d H:i', $admin->lastLoginTime) : '从未登录', + 'permissions' => $this->getPermissions($admin->authId) + ]; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $data + ]); + } + + /** + * 根据权限ID获取角色名称 + * @param int $authId 权限ID + * @return string + */ + private function getRoleName($authId) + { + // 可以从权限表中查询,这里为演示简化处理 + switch($authId) { + case 1: + return '超级管理员'; + case 2: + return '项目管理员'; + case 3: + return '客户管理员'; + default: + return '普通管理员'; + } + } + + /** + * 根据权限ID获取权限列表 + * @param int $authId 权限ID + * @return array + */ + private function getPermissions($authId) + { + // 可以从权限表中查询,这里为演示简化处理 + $permissions = [ + 1 => ['项目管理', '客户池', '管理员权限', '系统设置'], // 超级管理员 + 2 => ['项目管理', '客户池'], // 项目管理员 + 3 => ['客户池'], // 客户管理员 + 4 => [] // 普通管理员 + ]; + + return isset($permissions[$authId]) ? $permissions[$authId] : []; + } +} \ No newline at end of file From d33988aa4e2f0c054c87a43241956b1e4edb41c7 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 18:35:36 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E8=8E=B7=E5=8F=96=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SuperAdmin/app/dashboard/admins/page.tsx | 128 ++++++++++++++++++++--- SuperAdmin/lib/admin-api.ts | 74 +++++++++++++ SuperAdmin/lib/config.ts | 12 +++ 3 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 SuperAdmin/lib/admin-api.ts create mode 100644 SuperAdmin/lib/config.ts diff --git a/SuperAdmin/app/dashboard/admins/page.tsx b/SuperAdmin/app/dashboard/admins/page.tsx index 1713c62a..2a8d73c5 100644 --- a/SuperAdmin/app/dashboard/admins/page.tsx +++ b/SuperAdmin/app/dashboard/admins/page.tsx @@ -1,15 +1,17 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import Link from "next/link" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { Search, MoreHorizontal, Edit, Trash, UserPlus } from "lucide-react" +import { Search, MoreHorizontal, Edit, Trash, UserPlus, Loader2 } from "lucide-react" import { Badge } from "@/components/ui/badge" +import { useToast } from "@/components/ui/use-toast" +import { getAdministrators, Administrator } from "@/lib/admin-api" -// Sample admin data +// 保留原始示例数据,作为加载失败时的备用数据 const adminsData = [ { id: "1", @@ -51,13 +53,75 @@ const adminsData = [ export default function AdminsPage() { const [searchTerm, setSearchTerm] = useState("") + const [isLoading, setIsLoading] = useState(true) + const [administrators, setAdministrators] = useState([]) + const [totalCount, setTotalCount] = useState(0) + const [currentPage, setCurrentPage] = useState(1) + const [pageSize] = useState(10) + const { toast } = useToast() - const filteredAdmins = adminsData.filter( - (admin) => - admin.username.toLowerCase().includes(searchTerm.toLowerCase()) || - admin.name.toLowerCase().includes(searchTerm.toLowerCase()) || - admin.role.toLowerCase().includes(searchTerm.toLowerCase()), - ) + // 加载管理员列表 + useEffect(() => { + fetchAdministrators() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPage]) + + // 获取管理员列表 + const fetchAdministrators = async (keyword: string = searchTerm) => { + setIsLoading(true) + try { + const response = await getAdministrators(currentPage, pageSize, keyword) + if (response.code === 200 && response.data) { + setAdministrators(response.data.list) + setTotalCount(response.data.total) + } else { + toast({ + title: "获取管理员列表失败", + description: response.msg || "请稍后重试", + variant: "destructive", + }) + // 加载失败时显示示例数据 + setAdministrators(adminsData.map(admin => ({ + ...admin, + id: Number(admin.id) + })) as Administrator[]) + setTotalCount(adminsData.length) + } + } catch (error) { + console.error("获取管理员列表出错:", error) + toast({ + title: "获取管理员列表失败", + description: "请检查网络连接后重试", + variant: "destructive", + }) + // 加载失败时显示示例数据 + setAdministrators(adminsData.map(admin => ({ + ...admin, + id: Number(admin.id) + })) as Administrator[]) + setTotalCount(adminsData.length) + } finally { + setIsLoading(false) + } + } + + // 处理搜索 + const handleSearch = () => { + setCurrentPage(1) // 重置为第一页 + fetchAdministrators() + } + + // Enter键搜索 + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSearch() + } + } + + // 检查是否为超级管理员(id为1) + const isSuperAdmin = (id: number) => { + return id === 1 + } return (
    @@ -79,8 +143,10 @@ export default function AdminsPage() { className="pl-8" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} + onKeyDown={handleSearchKeyDown} />
    +
    @@ -97,8 +163,16 @@ export default function AdminsPage() { - {filteredAdmins.length > 0 ? ( - filteredAdmins.map((admin) => ( + {isLoading ? ( + + +
    + +
    +
    +
    + ) : administrators.length > 0 ? ( + administrators.map((admin) => ( {admin.username} {admin.name} @@ -130,9 +204,11 @@ export default function AdminsPage() { 编辑管理员 - - 删除管理员 - + {!isSuperAdmin(admin.id) && ( + + 删除管理员 + + )} @@ -148,6 +224,30 @@ export default function AdminsPage() {
    + + {totalCount > pageSize && ( +
    + + + 第 {currentPage} 页 / 共 {Math.ceil(totalCount / pageSize)} 页 + + +
    + )} ) } diff --git a/SuperAdmin/lib/admin-api.ts b/SuperAdmin/lib/admin-api.ts new file mode 100644 index 00000000..4095a8f5 --- /dev/null +++ b/SuperAdmin/lib/admin-api.ts @@ -0,0 +1,74 @@ +import { getConfig } from './config'; + +// 管理员接口数据类型定义 +export interface Administrator { + id: number; + username: string; + name: string; + role: string; + status: number; + createdAt: string; + lastLogin: string; + permissions: string[]; +} + +// 分页响应数据类型 +export interface PaginatedResponse { + list: T[]; + total: number; + page: number; + limit: number; +} + +// API响应数据结构 +export interface ApiResponse { + code: number; + msg: string; + data: T | null; +} + +/** + * 获取管理员列表 + * @param page 页码 + * @param limit 每页数量 + * @param keyword 搜索关键词 + * @returns 管理员列表 + */ +export async function getAdministrators( + page: number = 1, + limit: number = 10, + keyword: string = '' +): Promise>> { + const { apiBaseUrl } = getConfig(); + + // 构建查询参数 + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('limit', limit.toString()); + if (keyword) { + params.append('keyword', keyword); + } + + try { + // 发送请求 + const response = await fetch(`${apiBaseUrl}/administrator/list?${params.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`请求失败,状态码: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('获取管理员列表失败:', error); + return { + code: 500, + msg: '获取管理员列表失败', + data: null + }; + } +} \ No newline at end of file diff --git a/SuperAdmin/lib/config.ts b/SuperAdmin/lib/config.ts new file mode 100644 index 00000000..421e1f54 --- /dev/null +++ b/SuperAdmin/lib/config.ts @@ -0,0 +1,12 @@ +/** + * 获取应用配置 + * @returns 应用配置 + */ +export function getConfig() { + // 优先获取环境变量中配置的API地址 + const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com'; + + return { + apiBaseUrl + }; +} \ No newline at end of file From 3cf0ee2bcb15a8f27603a160980a670f2da87cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 10 Apr 2025 09:30:16 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E7=AE=A1=E7=90=86=E5=91=98=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../superadmin/controller/Administrator.php | 2 +- .../app/dashboard/admins/[id]/edit/page.tsx | 75 ++++++-- SuperAdmin/app/layout.tsx | 5 +- SuperAdmin/components/ui/toast.tsx | 174 ++++++------------ SuperAdmin/components/ui/use-toast.tsx | 65 +++++++ SuperAdmin/lib/admin-api.ts | 45 +++++ 6 files changed, 231 insertions(+), 135 deletions(-) create mode 100644 SuperAdmin/components/ui/use-toast.tsx diff --git a/Server/application/superadmin/controller/Administrator.php b/Server/application/superadmin/controller/Administrator.php index 1509e487..cab67ad5 100644 --- a/Server/application/superadmin/controller/Administrator.php +++ b/Server/application/superadmin/controller/Administrator.php @@ -95,7 +95,7 @@ class Administrator extends Controller 'status' => $admin->status, 'authId' => $admin->authId, 'roleName' => $this->getRoleName($admin->authId), - 'createdAt' => date('Y-m-d', $admin->createTime), + 'createdAt' => $admin->createTime, 'lastLogin' => !empty($admin->lastLoginTime) ? date('Y-m-d H:i', $admin->lastLoginTime) : '从未登录', 'permissions' => $this->getPermissions($admin->authId) ]; diff --git a/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx b/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx index 7cc8de27..d112b107 100644 --- a/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx +++ b/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx @@ -2,29 +2,34 @@ import type React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft } from "lucide-react" +import { ArrowLeft, Loader2 } from "lucide-react" import Link from "next/link" import { Checkbox } from "@/components/ui/checkbox" +import { getAdministratorDetail, AdministratorDetail } from "@/lib/admin-api" +import { useToast } from "@/components/ui/use-toast" -// Sample admin data for editing -const adminData = { - id: "2", - username: "admin_li", - name: "李管理", - permissions: ["project_management", "customer_pool"], -} +// 权限 ID 到前端权限键的映射 +const permissionMapping: Record = { + 1: ["project_management", "customer_pool", "admin_management"], // 超级管理员 + 2: ["project_management", "customer_pool"], // 项目管理员 + 3: ["customer_pool"], // 客户管理员 + 4: [], // 普通管理员 +}; export default function EditAdminPage({ params }: { params: { id: string } }) { const router = useRouter() + const { toast } = useToast() + const [isLoading, setIsLoading] = useState(true) const [isSubmitting, setIsSubmitting] = useState(false) - const [username, setUsername] = useState(adminData.username) - const [name, setName] = useState(adminData.name) + const [adminInfo, setAdminInfo] = useState(null) + const [username, setUsername] = useState("") + const [name, setName] = useState("") const permissions = [ { id: "project_management", label: "项目管理" }, @@ -32,7 +37,42 @@ export default function EditAdminPage({ params }: { params: { id: string } }) { { id: "admin_management", label: "管理员权限" }, ] - const [selectedPermissions, setSelectedPermissions] = useState(adminData.permissions) + const [selectedPermissions, setSelectedPermissions] = useState([]) + + // 加载管理员详情 + useEffect(() => { + const fetchAdminDetail = async () => { + setIsLoading(true) + try { + const response = await getAdministratorDetail(params.id) + if (response.code === 200 && response.data) { + setAdminInfo(response.data) + // 设置表单数据 + setUsername(response.data.username) + setName(response.data.name) + // 根据 authId 设置权限 + setSelectedPermissions(permissionMapping[response.data.authId] || []) + } else { + toast({ + title: "获取管理员详情失败", + description: response.msg || "请稍后重试", + variant: "destructive", + }) + } + } catch (error) { + console.error("获取管理员详情出错:", error) + toast({ + title: "获取管理员详情失败", + description: "请检查网络连接后重试", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + fetchAdminDetail() + }, [params.id]) const togglePermission = (permissionId: string) => { setSelectedPermissions((prev) => @@ -51,6 +91,17 @@ export default function EditAdminPage({ params }: { params: { id: string } }) { }, 1500) } + if (isLoading) { + return ( +
    +
    + +

    加载管理员详情中...

    +
    +
    + ) + } + return (
    diff --git a/SuperAdmin/app/layout.tsx b/SuperAdmin/app/layout.tsx index 8e15d431..cf84e55d 100644 --- a/SuperAdmin/app/layout.tsx +++ b/SuperAdmin/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata } from "next" import { Inter } from "next/font/google" import "./globals.css" import { ThemeProvider } from "@/components/theme-provider" +import { ToastProvider } from "@/components/ui/use-toast" const inter = Inter({ subsets: ["latin"] }) @@ -21,7 +22,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/SuperAdmin/components/ui/toast.tsx b/SuperAdmin/components/ui/toast.tsx index 521b94b0..c6463b30 100644 --- a/SuperAdmin/components/ui/toast.tsx +++ b/SuperAdmin/components/ui/toast.tsx @@ -1,129 +1,61 @@ "use client" import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" import { X } from "lucide-react" import { cn } from "@/lib/utils" -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, +interface ToastProps extends React.HTMLAttributes { + variant?: "default" | "destructive" | "success" + onDismiss?: () => void + title?: string + description?: string + action?: React.ReactNode +} + +export const ToastProvider = React.Fragment + +export function Toast({ + className, + variant = "default", + onDismiss, + title, + description, + action, + ...props +}: ToastProps) { + const variantStyles = { + default: "bg-background text-foreground", + destructive: "bg-destructive text-destructive-foreground", + success: "bg-green-500 text-white" + } + + return ( +
    +
    + {title &&

    {title}

    } + {description &&

    {description}

    } +
    + {action} + +
    + ) +} + +export function ToastViewport() { + return ( +
    + ) } diff --git a/SuperAdmin/components/ui/use-toast.tsx b/SuperAdmin/components/ui/use-toast.tsx new file mode 100644 index 00000000..59446f99 --- /dev/null +++ b/SuperAdmin/components/ui/use-toast.tsx @@ -0,0 +1,65 @@ +"use client" + +import { createContext, useContext, useState, useEffect, ReactNode } from "react" +import { Toast, ToastProvider, ToastViewport } from "@/components/ui/toast" + +type ToastProps = { + id: string + title?: string + description?: string + variant?: "default" | "destructive" | "success" + action?: ReactNode +} + +interface ToastContextValue { + toast: (props: Omit) => void + dismiss: (id: string) => void + toasts: ToastProps[] +} + +const ToastContext = createContext(null) + +export function useToast() { + const context = useContext(ToastContext) + if (context === null) { + throw new Error("useToast must be used within a ToastProvider") + } + return context +} + +interface ToastProviderProps { + children: ReactNode +} + +export function ToastProvider({ children }: ToastProviderProps) { + const [toasts, setToasts] = useState([]) + + const toast = (props: Omit) => { + const id = Math.random().toString(36).substring(2, 9) + setToasts((prev) => [...prev, { id, ...props }]) + } + + const dismiss = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)) + } + + // 自动移除 + useEffect(() => { + const timer = setTimeout(() => { + if (toasts.length > 0) { + setToasts((prev) => prev.slice(1)) + } + }, 5000) + return () => clearTimeout(timer) + }, [toasts]) + + return ( + + {children} + + {toasts.map((props) => ( + dismiss(props.id)} /> + ))} + + ) +} \ No newline at end of file diff --git a/SuperAdmin/lib/admin-api.ts b/SuperAdmin/lib/admin-api.ts index 4095a8f5..4d8553b5 100644 --- a/SuperAdmin/lib/admin-api.ts +++ b/SuperAdmin/lib/admin-api.ts @@ -12,6 +12,19 @@ export interface Administrator { permissions: string[]; } +// 管理员详情接口 +export interface AdministratorDetail { + id: number; + username: string; + name: string; + authId: number; + roleName: string; + status: number; + createdAt: string; + lastLogin: string; + permissions: string[]; +} + // 分页响应数据类型 export interface PaginatedResponse { list: T[]; @@ -71,4 +84,36 @@ export async function getAdministrators( data: null }; } +} + +/** + * 获取管理员详情 + * @param id 管理员ID + * @returns 管理员详情 + */ +export async function getAdministratorDetail(id: number | string): Promise> { + const { apiBaseUrl } = getConfig(); + + try { + // 发送请求 + const response = await fetch(`${apiBaseUrl}/administrator/detail/${id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`请求失败,状态码: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('获取管理员详情失败:', error); + return { + code: 500, + msg: '获取管理员详情失败', + data: null + }; + } } \ No newline at end of file From c23d0433ef78035387dcaf6aca41cd62d5c5fe09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 10 Apr 2025 11:54:21 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/superadmin/config/route.php | 37 +++-- .../superadmin/controller/Auth.php | 2 +- .../superadmin/middleware/AdminAuth.php | 74 +++++++++ Server/public/index.php | 20 +-- Server/route/route.php | 10 +- SuperAdmin/app/dashboard/layout.tsx | 20 ++- SuperAdmin/app/login/page.tsx | 51 ++++--- SuperAdmin/components/ui/scroll-area.tsx | 2 +- SuperAdmin/lib/admin-api.ts | 76 +++------- SuperAdmin/lib/api-utils.ts | 83 ++++++++++ SuperAdmin/lib/menu-api.ts | 142 ++++++------------ SuperAdmin/lib/utils.ts | 61 ++++++++ 12 files changed, 375 insertions(+), 203 deletions(-) create mode 100644 Server/application/superadmin/middleware/AdminAuth.php create mode 100644 SuperAdmin/lib/api-utils.ts diff --git a/Server/application/superadmin/config/route.php b/Server/application/superadmin/config/route.php index 4a086ac1..7748ea26 100644 --- a/Server/application/superadmin/config/route.php +++ b/Server/application/superadmin/config/route.php @@ -1,19 +1,28 @@ middleware(['app\\superadmin\\middleware\\AdminAuth']); \ No newline at end of file diff --git a/Server/application/superadmin/controller/Auth.php b/Server/application/superadmin/controller/Auth.php index 47a9024d..bffa5b01 100644 --- a/Server/application/superadmin/controller/Auth.php +++ b/Server/application/superadmin/controller/Auth.php @@ -57,7 +57,7 @@ class Auth extends Controller */ private function createToken($admin) { - $data = $admin->id . '|' . $admin->account . '|' . time(); + $data = $admin->id . '|' . $admin->account; return md5($data . 'cunkebao_admin_secret'); } } \ No newline at end of file diff --git a/Server/application/superadmin/middleware/AdminAuth.php b/Server/application/superadmin/middleware/AdminAuth.php new file mode 100644 index 00000000..f4bb9897 --- /dev/null +++ b/Server/application/superadmin/middleware/AdminAuth.php @@ -0,0 +1,74 @@ + 401, + 'msg' => '请先登录', + 'data' => null + ]); + } + + // 获取管理员信息 + $admin = \app\superadmin\model\Administrator::where([ + ['id', '=', $adminId], + ['status', '=', 1], + ['deleteTime', '=', 0] + ])->find(); + + // 如果管理员不存在,返回401未授权 + if (!$admin) { + return json([ + 'code' => 401, + 'msg' => '管理员账号不存在或已被禁用', + 'data' => null + ]); + } + + // 验证Token是否有效 + $expectedToken = $this->createToken($admin); + + if ($adminToken !== $expectedToken) { + return json([ + 'code' => 401, + 'msg' => '登录已过期,请重新登录', + 'data' => null + ]); + } + + // 将管理员信息绑定到请求对象,方便后续控制器使用 + $request->adminInfo = $admin; + + // 继续执行后续操作 + return $next($request); + } + + /** + * 创建登录令牌 + * @param \app\superadmin\model\Administrator $admin + * @return string + */ + private function createToken($admin) + { + $data = $admin->id . '|' . $admin->account; + return md5($data . 'cunkebao_admin_secret'); + } +} \ No newline at end of file diff --git a/Server/public/index.php b/Server/public/index.php index ea854690..ed629339 100644 --- a/Server/public/index.php +++ b/Server/public/index.php @@ -12,16 +12,16 @@ // [ 应用入口文件 ] namespace think; -//处理跨域预检请求 -if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){ - //允许的源域名 - header("Access-Control-Allow-Origin: *"); - //允许的请求头信息 - header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization"); - //允许的请求类型 - header('Access-Control-Allow-Methods: GET, POST, PUT,DELETE,OPTIONS,PATCH'); - exit; -} +////处理跨域预检请求 +//if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){ +// //允许的源域名 +// header("Access-Control-Allow-Origin: *"); +// //允许的请求头信息 +// header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization"); +// //允许的请求类型 +// header('Access-Control-Allow-Methods: GET, POST, PUT,DELETE,OPTIONS,PATCH'); +// exit; +//} define('ROOT_PATH', dirname(__DIR__)); define('DS', DIRECTORY_SEPARATOR); diff --git a/Server/route/route.php b/Server/route/route.php index cd50328c..94f08633 100644 --- a/Server/route/route.php +++ b/Server/route/route.php @@ -12,11 +12,11 @@ use think\facade\Route; // 允许跨域 - header('Access-Control-Allow-Origin: *'); - header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); - header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token'); - header('Access-Control-Max-Age: 1728000'); - header('Access-Control-Allow-Credentials: true'); +// header('Access-Control-Allow-Origin: *'); +// header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); +// header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token'); +// header('Access-Control-Max-Age: 1728000'); +// header('Access-Control-Allow-Credentials: true'); // 加载Store模块路由配置 include __DIR__ . '/../application/api/config/route.php'; diff --git a/SuperAdmin/app/dashboard/layout.tsx b/SuperAdmin/app/dashboard/layout.tsx index 557729fe..aa73fed2 100644 --- a/SuperAdmin/app/dashboard/layout.tsx +++ b/SuperAdmin/app/dashboard/layout.tsx @@ -1,11 +1,13 @@ "use client" import type React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" -import { Menu, X, LogOut } from "lucide-react" +import { Menu, X } from "lucide-react" import { Sidebar } from "@/components/layout/sidebar" import { Header } from "@/components/layout/header" +import { getAdminInfo } from "@/lib/utils" export default function DashboardLayout({ children, @@ -13,6 +15,20 @@ export default function DashboardLayout({ children: React.ReactNode }) { const [sidebarOpen, setSidebarOpen] = useState(true) + const router = useRouter() + + // 认证检查 + useEffect(() => { + const checkAuth = () => { + const adminInfo = getAdminInfo() + if (!adminInfo) { + // 未登录时跳转到登录页 + router.push('/login') + } + } + + checkAuth() + }, [router]) return (
    diff --git a/SuperAdmin/app/login/page.tsx b/SuperAdmin/app/login/page.tsx index 5109a840..5188527b 100644 --- a/SuperAdmin/app/login/page.tsx +++ b/SuperAdmin/app/login/page.tsx @@ -8,52 +8,58 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Label } from "@/components/ui/label" -import { md5 } from "@/lib/utils" +import { md5, saveAdminInfo } from "@/lib/utils" +import { login } from "@/lib/admin-api" +import { useToast } from "@/components/ui/use-toast" export default function LoginPage() { const [account, setAccount] = useState("") const [password, setPassword] = useState("") const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState("") const router = useRouter() + const { toast } = useToast() const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setIsLoading(true) - setError("") try { // 对密码进行MD5加密 const encryptedPassword = md5(password) // 调用登录接口 - const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/login`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - account, - password: encryptedPassword - }), - credentials: "include" - }) + const result = await login(account, encryptedPassword) - const result = await response.json() - - if (result.code === 200) { - // 保存用户信息到本地存储 - localStorage.setItem("admin_info", JSON.stringify(result.data)) - localStorage.setItem("admin_token", result.data.token) + if (result.code === 200 && result.data) { + // 保存管理员信息 + saveAdminInfo(result.data) + + // 显示成功提示 + toast({ + title: "登录成功", + description: `欢迎回来,${result.data.name}`, + variant: "success", + }) // 跳转到仪表盘 router.push("/dashboard") } else { - setError(result.msg || "登录失败") + // 显示错误提示 + toast({ + title: "登录失败", + description: result.msg || "账号或密码错误", + variant: "destructive", + }) } } catch (err) { console.error("登录失败:", err) - setError("网络错误,请稍后再试") + + // 显示错误提示 + toast({ + title: "登录失败", + description: "网络错误,请稍后再试", + variant: "destructive", + }) } finally { setIsLoading(false) } @@ -65,7 +71,6 @@ export default function LoginPage() { 超级管理员后台 请输入您的账号和密码登录系统 - {error &&

    {error}

    }
    diff --git a/SuperAdmin/components/ui/scroll-area.tsx b/SuperAdmin/components/ui/scroll-area.tsx index 0b4a48d8..54b87cd7 100644 --- a/SuperAdmin/components/ui/scroll-area.tsx +++ b/SuperAdmin/components/ui/scroll-area.tsx @@ -35,7 +35,7 @@ const ScrollBar = React.forwardRef< orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]", orientation === "horizontal" && - "h-2.5 flex-col border-t border-t-transparent p-[1px]", + "h-2.5 border-t border-t-transparent p-[1px]", className )} {...props} diff --git a/SuperAdmin/lib/admin-api.ts b/SuperAdmin/lib/admin-api.ts index 4d8553b5..c4190492 100644 --- a/SuperAdmin/lib/admin-api.ts +++ b/SuperAdmin/lib/admin-api.ts @@ -1,4 +1,4 @@ -import { getConfig } from './config'; +import { apiRequest, ApiResponse } from './api-utils'; // 管理员接口数据类型定义 export interface Administrator { @@ -33,11 +33,25 @@ export interface PaginatedResponse { limit: number; } -// API响应数据结构 -export interface ApiResponse { - code: number; - msg: string; - data: T | null; +/** + * 管理员登录 + * @param account 账号 + * @param password 密码 + * @returns 登录结果 + */ +export async function login( + account: string, + password: string +): Promise> { + return apiRequest('/auth/login', 'POST', { + account, + password + }); } /** @@ -52,8 +66,6 @@ export async function getAdministrators( limit: number = 10, keyword: string = '' ): Promise>> { - const { apiBaseUrl } = getConfig(); - // 构建查询参数 const params = new URLSearchParams(); params.append('page', page.toString()); @@ -62,28 +74,7 @@ export async function getAdministrators( params.append('keyword', keyword); } - try { - // 发送请求 - const response = await fetch(`${apiBaseUrl}/administrator/list?${params.toString()}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error(`请求失败,状态码: ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error('获取管理员列表失败:', error); - return { - code: 500, - msg: '获取管理员列表失败', - data: null - }; - } + return apiRequest(`/administrator/list?${params.toString()}`); } /** @@ -92,28 +83,5 @@ export async function getAdministrators( * @returns 管理员详情 */ export async function getAdministratorDetail(id: number | string): Promise> { - const { apiBaseUrl } = getConfig(); - - try { - // 发送请求 - const response = await fetch(`${apiBaseUrl}/administrator/detail/${id}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error(`请求失败,状态码: ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error('获取管理员详情失败:', error); - return { - code: 500, - msg: '获取管理员详情失败', - data: null - }; - } + return apiRequest(`/administrator/detail/${id}`); } \ No newline at end of file diff --git a/SuperAdmin/lib/api-utils.ts b/SuperAdmin/lib/api-utils.ts new file mode 100644 index 00000000..8ddd25d9 --- /dev/null +++ b/SuperAdmin/lib/api-utils.ts @@ -0,0 +1,83 @@ +import { getConfig } from './config'; +import { getAdminInfo, clearAdminInfo } from './utils'; + +/** + * API响应数据结构 + */ +export interface ApiResponse { + code: number; + msg: string; + data: T | null; +} + +/** + * 通用API请求函数 + * @param endpoint API端点 + * @param method HTTP方法 + * @param data 请求数据 + * @returns API响应 + */ +export async function apiRequest( + endpoint: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', + data?: any +): Promise> { + const { apiBaseUrl } = getConfig(); + const url = `${apiBaseUrl}${endpoint}`; + + // 获取认证信息 + const adminInfo = getAdminInfo(); + + // 请求头 + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + // 如果有认证信息,添加Cookie头 + if (adminInfo?.token) { + // 添加认证令牌,作为Cookie发送 + document.cookie = `admin_id=${adminInfo.id}; path=/`; + document.cookie = `admin_token=${adminInfo.token}; path=/`; + } + + // 请求配置 + const config: RequestInit = { + method, + headers, + credentials: 'include', // 包含跨域请求的Cookie + }; + + // 如果有请求数据,转换为JSON + if (data && method !== 'GET') { + config.body = JSON.stringify(data); + } + + try { + const response = await fetch(url, config); + + if (!response.ok) { + throw new Error(`请求失败: ${response.status} ${response.statusText}`); + } + + const result = await response.json() as ApiResponse; + + // 如果返回未授权错误,清除登录信息 + if (result.code === 401) { + clearAdminInfo(); + // 如果在浏览器环境,跳转到登录页 + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + } + + return result; + } catch (error) { + console.error('API请求错误:', error); + + return { + code: 500, + msg: error instanceof Error ? error.message : '未知错误', + data: null + }; + } +} \ No newline at end of file diff --git a/SuperAdmin/lib/menu-api.ts b/SuperAdmin/lib/menu-api.ts index 8d804429..e284bd42 100644 --- a/SuperAdmin/lib/menu-api.ts +++ b/SuperAdmin/lib/menu-api.ts @@ -1,3 +1,5 @@ +import { apiRequest, ApiResponse } from './api-utils'; + /** * 菜单项接口 */ @@ -13,78 +15,60 @@ export interface MenuItem { } /** - * 从服务器获取菜单数据 + * 获取菜单树 * @param onlyEnabled 是否只获取启用的菜单 - * @returns Promise + * @returns 菜单树 */ export async function getMenus(onlyEnabled: boolean = true): Promise { try { - // API基础路径从环境变量获取 - const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; + // 构建查询参数 + const params = new URLSearchParams(); + params.append('only_enabled', onlyEnabled ? '1' : '0'); - // 构建API URL - const url = `${apiBaseUrl}/menu/tree?only_enabled=${onlyEnabled ? 1 : 0}`; + const response = await apiRequest(`/menu/tree?${params.toString()}`); - // 获取存储的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 []; - } + return response.data || []; } catch (error) { - console.error('获取菜单出错:', error); + console.error('获取菜单树失败:', error); return []; } } /** - * 保存菜单 + * 获取菜单列表 + * @param page 页码 + * @param limit 每页数量 + * @returns 菜单列表 + */ +export async function getMenuList( + page: number = 1, + limit: number = 20 +): Promise> { + // 构建查询参数 + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('limit', limit.toString()); + + return apiRequest(`/menu/list?${params.toString()}`); +} + +/** + * 保存菜单(新增或更新) * @param menuData 菜单数据 - * @returns Promise + * @returns 保存结果 */ 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 apiRequest('/menu/save', 'POST', menuData); - 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; + return response.code === 200; } catch (error) { - console.error('保存菜单出错:', error); + console.error('保存菜单失败:', error); return false; } } @@ -92,30 +76,15 @@ export async function saveMenu(menuData: Partial): Promise { /** * 删除菜单 * @param id 菜单ID - * @returns Promise + * @returns 删除结果 */ 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 apiRequest(`/menu/delete/${id}`, 'DELETE'); - 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; + return response.code === 200; } catch (error) { - console.error('删除菜单出错:', error); + console.error('删除菜单失败:', error); return false; } } @@ -123,32 +92,19 @@ export async function deleteMenu(id: number): Promise { /** * 更新菜单状态 * @param id 菜单ID - * @param status 状态 (0-禁用, 1-启用) - * @returns Promise + * @param status 状态:1启用,0禁用 + * @returns 更新结果 */ 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 }) + const response = await apiRequest('/menu/status', 'POST', { + id, + status }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - return result.code === 200; + return response.code === 200; } catch (error) { - console.error('更新菜单状态出错:', error); + console.error('更新菜单状态失败:', error); return false; } } \ No newline at end of file diff --git a/SuperAdmin/lib/utils.ts b/SuperAdmin/lib/utils.ts index ad400f14..3f401632 100644 --- a/SuperAdmin/lib/utils.ts +++ b/SuperAdmin/lib/utils.ts @@ -12,3 +12,64 @@ export function cn(...inputs: ClassValue[]) { export function md5(text: string): string { return crypto.createHash("md5").update(text).digest("hex") } + +/** + * 管理员信息 + */ +export interface AdminInfo { + id: number; + name: string; + account: string; + token: string; +} + +/** + * 保存管理员信息到本地存储 + * @param adminInfo 管理员信息 + */ +export function saveAdminInfo(adminInfo: AdminInfo): void { + if (typeof window !== 'undefined') { + localStorage.setItem('admin_id', adminInfo.id.toString()); + localStorage.setItem('admin_name', adminInfo.name); + localStorage.setItem('admin_account', adminInfo.account); + localStorage.setItem('admin_token', adminInfo.token); + } +} + +/** + * 获取管理员信息 + * @returns 管理员信息 + */ +export function getAdminInfo(): AdminInfo | null { + if (typeof window === 'undefined') { + return null; + } + + const id = localStorage.getItem('admin_id'); + const name = localStorage.getItem('admin_name'); + const account = localStorage.getItem('admin_account'); + const token = localStorage.getItem('admin_token'); + + if (!id || !name || !account || !token) { + return null; + } + + return { + id: parseInt(id, 10), + name, + account, + token + }; +} + +/** + * 清除管理员信息 + */ +export function clearAdminInfo(): void { + if (typeof window !== 'undefined') { + localStorage.removeItem('admin_id'); + localStorage.removeItem('admin_name'); + localStorage.removeItem('admin_account'); + localStorage.removeItem('admin_token'); + } +} From e81a75b67ed3cc4ddc8532060e843cccb53b4d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 10 Apr 2025 16:11:15 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E7=AE=A1=E7=90=86=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/superadmin/config/route.php | 5 + .../superadmin/controller/Administrator.php | 140 ++++++++++- .../superadmin/controller/Menu.php | 36 ++- .../superadmin/model/Administrator.php | 14 -- .../model/AdministratorPermissions.php | 90 +++++++ Server/application/superadmin/model/Menu.php | 123 +++++----- Server/public/index.php | 18 +- Server/route/route.php | 10 +- .../app/dashboard/admins/[id]/edit/page.tsx | 215 ++++++++++++----- SuperAdmin/app/dashboard/admins/new/page.tsx | 220 ++++++++++++++---- SuperAdmin/lib/admin-api.ts | 37 +++ SuperAdmin/lib/menu-api.ts | 20 ++ 12 files changed, 724 insertions(+), 204 deletions(-) create mode 100644 Server/application/superadmin/model/AdministratorPermissions.php diff --git a/Server/application/superadmin/config/route.php b/Server/application/superadmin/config/route.php index 7748ea26..caafefef 100644 --- a/Server/application/superadmin/config/route.php +++ b/Server/application/superadmin/config/route.php @@ -13,6 +13,7 @@ Route::group('', function () { 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'); + Route::get('toplevel', 'app\\superadmin\\controller\\Menu@getTopLevelMenus'); }); // 管理员相关路由 @@ -21,6 +22,10 @@ Route::group('', function () { Route::get('list', 'app\\superadmin\\controller\\Administrator@getList'); // 获取管理员详情 Route::get('detail/:id', 'app\\superadmin\\controller\\Administrator@getDetail'); + // 更新管理员信息 + Route::post('update', 'app\\superadmin\\controller\\Administrator@updateAdmin'); + // 添加管理员 + Route::post('add', 'app\\superadmin\\controller\\Administrator@addAdmin'); }); // 系统信息相关路由 diff --git a/Server/application/superadmin/controller/Administrator.php b/Server/application/superadmin/controller/Administrator.php index cab67ad5..2a35b061 100644 --- a/Server/application/superadmin/controller/Administrator.php +++ b/Server/application/superadmin/controller/Administrator.php @@ -1,6 +1,7 @@ $item->name, 'role' => $this->getRoleName($item->authId), 'status' => $item->status, - 'createdAt' => $item->createTime, - 'lastLogin' => !empty($item->lastLoginTime) ? date('Y-m-d H:i', $item->lastLoginTime) : '从未登录', - 'permissions' => $this->getPermissions($item->authId) + 'createdAt' => date('Y-m-d H:i:s', $item->createTime), + 'lastLogin' => !empty($item->lastLoginTime) ? date('Y-m-d H:i:s', $item->lastLoginTime) : '从未登录', + 'permissions' => $this->getPermissions($item->id) ]; } @@ -134,14 +135,133 @@ class Administrator extends Controller */ private function getPermissions($authId) { - // 可以从权限表中查询,这里为演示简化处理 - $permissions = [ - 1 => ['项目管理', '客户池', '管理员权限', '系统设置'], // 超级管理员 - 2 => ['项目管理', '客户池'], // 项目管理员 - 3 => ['客户池'], // 客户管理员 - 4 => [] // 普通管理员 + $ids = AdministratorPermissions::getPermissions($authId); + + if ($ids) { + return \app\superadmin\model\Menu::getMenusNameByIds($ids); + } + + return []; + } + + /** + * 更新管理员信息 + * @return \think\response\Json + */ + public function updateAdmin() + { + if (!$this->request->isPost()) { + return json(['code' => 405, 'msg' => '请求方法不允许']); + } + + // 获取当前登录的管理员信息 + $currentAdmin = $this->request->adminInfo; + + // 获取请求参数 + $id = $this->request->post('id/d'); + $username = $this->request->post('username/s'); + $name = $this->request->post('name/s'); + $password = $this->request->post('password/s'); + $permissionIds = $this->request->post('permissionIds/a'); + + // 参数验证 + if (empty($id) || empty($username) || empty($name)) { + return json(['code' => 400, 'msg' => '参数不完整']); + } + + // 判断是否有权限修改 + if ($currentAdmin->id != 1 && $currentAdmin->id != $id) { + return json(['code' => 403, 'msg' => '您没有权限修改其他管理员']); + } + + // 查询管理员 + $admin = AdminModel::where('id', $id)->where('deleteTime', 0)->find(); + if (!$admin) { + return json(['code' => 404, 'msg' => '管理员不存在']); + } + + // 准备更新数据 + $data = [ + 'account' => $username, + 'name' => $name, + 'updateTime' => time() ]; - return isset($permissions[$authId]) ? $permissions[$authId] : []; + // 如果提供了密码,则更新密码 + if (!empty($password)) { + $data['password'] = md5($password); + } + + // 更新管理员信息 + $result = $admin->save($data); + + // 如果当前是超级管理员(ID为1),并且修改的不是自己,则更新权限 + if ($currentAdmin->id == 1 && $currentAdmin->id != $id && !empty($permissionIds)) { + \app\superadmin\model\AdministratorPermissions::savePermissions($id, $permissionIds); + } + + return json([ + 'code' => 200, + 'msg' => '更新成功', + 'data' => null + ]); + } + + /** + * 添加管理员 + * @return \think\response\Json + */ + public function addAdmin() + { + if (!$this->request->isPost()) { + return json(['code' => 405, 'msg' => '请求方法不允许']); + } + + // 获取当前登录的管理员信息 + $currentAdmin = $this->request->adminInfo; + + // 只有超级管理员(ID为1)可以添加管理员 + if ($currentAdmin->id != 1) { + return json(['code' => 403, 'msg' => '您没有权限添加管理员']); + } + + // 获取请求参数 + $username = $this->request->post('username/s'); + $name = $this->request->post('name/s'); + $password = $this->request->post('password/s'); + $permissionIds = $this->request->post('permissionIds/a'); + + // 参数验证 + if (empty($username) || empty($name) || empty($password)) { + return json(['code' => 400, 'msg' => '参数不完整']); + } + + // 检查账号是否已存在 + $exists = AdminModel::where('account', $username)->where('deleteTime', 0)->find(); + if ($exists) { + return json(['code' => 400, 'msg' => '账号已存在']); + } + + // 创建管理员 + $admin = new AdminModel(); + $admin->account = $username; + $admin->name = $name; + $admin->password = md5($password); + $admin->status = 1; + $admin->createTime = time(); + $admin->updateTime = time(); + $admin->deleteTime = 0; + $admin->save(); + + // 保存权限 + if (!empty($permissionIds)) { + \app\superadmin\model\AdministratorPermissions::savePermissions($admin->id, $permissionIds); + } + + return json([ + 'code' => 200, + 'msg' => '添加成功', + 'data' => null + ]); } } \ No newline at end of file diff --git a/Server/application/superadmin/controller/Menu.php b/Server/application/superadmin/controller/Menu.php index d27fdd2e..a4b36601 100644 --- a/Server/application/superadmin/controller/Menu.php +++ b/Server/application/superadmin/controller/Menu.php @@ -17,10 +17,20 @@ class Menu extends Controller { // 参数处理 $onlyEnabled = $this->request->param('only_enabled', 1); - $useCache = $this->request->param('use_cache', 1); + $useCache = $this->request->param('use_cache', 0); // 由于要根据用户权限过滤,默认不使用缓存 + + // 获取当前登录的管理员信息 + $adminInfo = $this->request->adminInfo; // 调用模型获取菜单树 - $menuTree = MenuModel::getMenuTree($onlyEnabled, $useCache); + if ($adminInfo->id == 1) { + // 超级管理员获取所有菜单 + $menuTree = MenuModel::getMenuTree($onlyEnabled, $useCache); + } else { + // 非超级管理员根据权限获取菜单 + $permissionIds = \app\superadmin\model\AdministratorPermissions::getPermissions($adminInfo->id); + $menuTree = MenuModel::getMenuTreeByPermissions($permissionIds, $onlyEnabled); + } return json([ 'code' => 200, @@ -148,4 +158,26 @@ class Menu extends Controller return json(['code' => 500, 'msg' => '状态更新失败']); } } + + /** + * 获取一级菜单(供权限设置使用) + * @return \think\response\Json + */ + public function getTopLevelMenus() + { + // 获取所有启用的一级菜单 + $menus = \app\superadmin\model\Menu::where([ + ['parent_id', '=', 0], + ['status', '=', 1] + ]) + ->field('id, title') + ->order('sort', 'asc') + ->select(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $menus + ]); + } } \ No newline at end of file diff --git a/Server/application/superadmin/model/Administrator.php b/Server/application/superadmin/model/Administrator.php index be2ce650..0d1f56eb 100644 --- a/Server/application/superadmin/model/Administrator.php +++ b/Server/application/superadmin/model/Administrator.php @@ -11,20 +11,6 @@ class Administrator extends Model // 设置数据表名 protected $name = 'administrators'; - // 设置数据表前缀 - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = true; - - // 定义时间戳字段名 - protected $createTime = 'createTime'; - protected $updateTime = 'updateTime'; - protected $deleteTime = 'deleteTime'; - // 隐藏字段 protected $hidden = [ 'password' diff --git a/Server/application/superadmin/model/AdministratorPermissions.php b/Server/application/superadmin/model/AdministratorPermissions.php new file mode 100644 index 00000000..d0aa39e4 --- /dev/null +++ b/Server/application/superadmin/model/AdministratorPermissions.php @@ -0,0 +1,90 @@ + 'integer', + 'adminId' => 'integer', + 'permissions' => 'json', + 'createTime' => 'integer', + 'updateTime' => 'integer', + 'deleteTime' => 'integer' + ]; + + /** + * 保存管理员权限 + * @param int $adminId 管理员ID + * @param array $permissionIds 权限ID数组 + * @return bool + */ + public static function savePermissions($adminId, $permissionIds) + { + // 检查是否已有记录 + $record = self::where('adminId', $adminId)->find(); + + // 准备权限数据 + $permissionData = [ + 'ids' => is_array($permissionIds) ? implode(',', $permissionIds) : $permissionIds + ]; + + if ($record) { + // 更新已有记录 + return $record->save([ + 'permissions' => json_encode($permissionData), + 'updateTime' => time() + ]); + } else { + // 创建新记录 + return self::create([ + 'adminId' => $adminId, + 'permissions' => json_encode($permissionData), + 'createTime' => time(), + 'updateTime' => time(), + 'deleteTime' => 0 + ]); + } + } + + /** + * 获取管理员权限 + * @param int $adminId 管理员ID + * @return array 权限ID数组 + */ + public static function getPermissions($adminId) + { + $record = self::where('adminId', $adminId)->find(); + + if (!$record || empty($record->permissions)) { + return []; + } + + $permissions = $record->permissions ? json_decode($record->permissions, true) : []; + + if (isset($permissions['ids']) && !empty($permissions['ids'])) { + return is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids']; + } + + return []; + } +} \ No newline at end of file diff --git a/Server/application/superadmin/model/Menu.php b/Server/application/superadmin/model/Menu.php index 8e116a15..d3476fe9 100644 --- a/Server/application/superadmin/model/Menu.php +++ b/Server/application/superadmin/model/Menu.php @@ -12,19 +12,6 @@ class Menu extends Model // 设置数据表名 protected $name = 'menus'; - // 设置数据表前缀 - protected $prefix = 'tk_'; - - // 设置主键 - protected $pk = 'id'; - - // 自动写入时间戳 - protected $autoWriteTimestamp = true; - - // 定义时间戳字段名 - protected $createTime = 'create_time'; - protected $updateTime = 'update_time'; - /** * 获取所有菜单,并组织成树状结构 * @param bool $onlyEnabled 是否只获取启用的菜单 @@ -35,11 +22,6 @@ class Menu extends Model { $cacheKey = 'superadmin_menu_tree' . ($onlyEnabled ? '_enabled' : '_all'); - // 如果使用缓存并且缓存中有数据,则直接返回缓存数据 -// if ($useCache && Cache::has($cacheKey)) { -// return Cache::get($cacheKey); -// } - // 查询条件 $where = []; if ($onlyEnabled) { @@ -62,7 +44,12 @@ class Menu extends Model return $menuTree; } - + + public static function getMenusNameByIds($ids) + { + return self::whereIn('id', $ids)->column('title'); + } + /** * 构建菜单树 * @param array $menus 所有菜单 @@ -85,60 +72,60 @@ class Menu extends Model return $tree; } - + /** - * 清除菜单缓存 + * 根据权限ID获取相应的菜单树 + * @param array $permissionIds 权限ID数组 + * @param bool $onlyEnabled 是否只获取启用的菜单 + * @return array */ - public static function clearMenuCache() + public static function getMenuTreeByPermissions($permissionIds, $onlyEnabled = true) { - 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; + // 如果没有权限,返回空数组 + if (empty($permissionIds)) { + return []; + } + + // 查询条件 + $where = []; + if ($onlyEnabled) { + $where[] = ['status', '=', 1]; + } + + // 获取所有一级菜单(用户拥有权限的) + $topMenus = self::where($where) + ->where('parent_id', 0) + ->whereIn('id', $permissionIds) + ->order('sort', 'asc') + ->select() + ->toArray(); + + // 菜单ID集合,用于获取子菜单 + $menuIds = array_column($topMenus, 'id'); + + // 获取所有子菜单 + $childMenus = self::where($where) + ->where('parent_id', 'in', $menuIds) + ->order('sort', 'asc') + ->select() + ->toArray(); + + // 将子菜单按照父ID进行分组 + $childMenusGroup = []; + foreach ($childMenus as $menu) { + $childMenusGroup[$menu['parent_id']][] = $menu; + } + + // 构建菜单树 + $menuTree = []; + foreach ($topMenus as $topMenu) { + // 添加子菜单 + if (isset($childMenusGroup[$topMenu['id']])) { + $topMenu['children'] = $childMenusGroup[$topMenu['id']]; } - $result = $menu->save($data); - } else { - // 新增 - $menu = new self(); - $result = $menu->save($data); + $menuTree[] = $topMenu; } - // 清除缓存 - 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; + return $menuTree; } } \ No newline at end of file diff --git a/Server/public/index.php b/Server/public/index.php index ed629339..854db7e9 100644 --- a/Server/public/index.php +++ b/Server/public/index.php @@ -13,15 +13,15 @@ namespace think; ////处理跨域预检请求 -//if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){ -// //允许的源域名 -// header("Access-Control-Allow-Origin: *"); -// //允许的请求头信息 -// header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization"); -// //允许的请求类型 -// header('Access-Control-Allow-Methods: GET, POST, PUT,DELETE,OPTIONS,PATCH'); -// exit; -//} +if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){ + //允许的源域名 + header("Access-Control-Allow-Origin: *"); + //允许的请求头信息 + header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization"); + //允许的请求类型 + header('Access-Control-Allow-Methods: GET, POST, PUT,DELETE,OPTIONS,PATCH'); + exit; +} define('ROOT_PATH', dirname(__DIR__)); define('DS', DIRECTORY_SEPARATOR); diff --git a/Server/route/route.php b/Server/route/route.php index 94f08633..cd50328c 100644 --- a/Server/route/route.php +++ b/Server/route/route.php @@ -12,11 +12,11 @@ use think\facade\Route; // 允许跨域 -// header('Access-Control-Allow-Origin: *'); -// header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); -// header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token'); -// header('Access-Control-Max-Age: 1728000'); -// header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); + header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token'); + header('Access-Control-Max-Age: 1728000'); + header('Access-Control-Allow-Credentials: true'); // 加载Store模块路由配置 include __DIR__ . '/../application/api/config/route.php'; diff --git a/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx b/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx index d112b107..7c58788b 100644 --- a/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx +++ b/SuperAdmin/app/dashboard/admins/[id]/edit/page.tsx @@ -11,58 +11,84 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { ArrowLeft, Loader2 } from "lucide-react" import Link from "next/link" import { Checkbox } from "@/components/ui/checkbox" -import { getAdministratorDetail, AdministratorDetail } from "@/lib/admin-api" +import { getAdministratorDetail, updateAdministrator } from "@/lib/admin-api" import { useToast } from "@/components/ui/use-toast" +import { getTopLevelMenus } from "@/lib/menu-api" +import { getAdminInfo } from "@/lib/utils" -// 权限 ID 到前端权限键的映射 -const permissionMapping: Record = { - 1: ["project_management", "customer_pool", "admin_management"], // 超级管理员 - 2: ["project_management", "customer_pool"], // 项目管理员 - 3: ["customer_pool"], // 客户管理员 - 4: [], // 普通管理员 -}; +interface MenuPermission { + id: number; + title: string; +} export default function EditAdminPage({ params }: { params: { id: string } }) { const router = useRouter() const { toast } = useToast() const [isLoading, setIsLoading] = useState(true) const [isSubmitting, setIsSubmitting] = useState(false) - const [adminInfo, setAdminInfo] = useState(null) + const [adminInfo, setAdminInfo] = useState(null) const [username, setUsername] = useState("") const [name, setName] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [menuPermissions, setMenuPermissions] = useState([]) + const [selectedPermissions, setSelectedPermissions] = useState([]) + const [currentAdmin, setCurrentAdmin] = useState(null) + const [canEditPermissions, setCanEditPermissions] = useState(false) - const permissions = [ - { id: "project_management", label: "项目管理" }, - { id: "customer_pool", label: "客户池" }, - { id: "admin_management", label: "管理员权限" }, - ] - - const [selectedPermissions, setSelectedPermissions] = useState([]) - - // 加载管理员详情 + // 加载管理员详情和菜单权限 useEffect(() => { - const fetchAdminDetail = async () => { + const fetchData = async () => { setIsLoading(true) try { - const response = await getAdministratorDetail(params.id) - if (response.code === 200 && response.data) { - setAdminInfo(response.data) - // 设置表单数据 - setUsername(response.data.username) - setName(response.data.name) - // 根据 authId 设置权限 - setSelectedPermissions(permissionMapping[response.data.authId] || []) + // 获取当前登录的管理员信息 + const currentAdminInfo = getAdminInfo() + setCurrentAdmin(currentAdminInfo) + + // 获取管理员详情 + const adminResponse = await getAdministratorDetail(params.id) + + if (adminResponse.code === 200 && adminResponse.data) { + setAdminInfo(adminResponse.data) + setUsername(adminResponse.data.username) + setName(adminResponse.data.name) + + // 判断是否可以编辑权限 + // 只有超级管理员(ID为1)可以编辑其他人的权限 + // 编辑自己时不能修改权限 + const isEditingSelf = currentAdminInfo && parseInt(params.id) === currentAdminInfo.id + const isSuperAdmin = currentAdminInfo && currentAdminInfo.id === 1 + + setCanEditPermissions(!!(isSuperAdmin && !isEditingSelf)) + + // 如果可以编辑权限,则获取菜单权限 + if (isSuperAdmin && !isEditingSelf) { + const menuResponse = await getTopLevelMenus() + if (menuResponse.code === 200 && menuResponse.data) { + setMenuPermissions(menuResponse.data) + + // 获取管理员已有的权限 + const permissionsResponse = await getAdministratorDetail(params.id) + if (permissionsResponse.code === 200 && permissionsResponse.data) { + // 如果有权限数据,则设置选中的权限 + if (permissionsResponse.data.permissions) { + // 假设权限是存储为菜单ID的数组 + setSelectedPermissions(permissionsResponse.data.permissions.map((p: any) => p.id || p)) + } + } + } + } } else { toast({ title: "获取管理员详情失败", - description: response.msg || "请稍后重试", + description: adminResponse.msg || "请稍后重试", variant: "destructive", }) } } catch (error) { - console.error("获取管理员详情出错:", error) + console.error("获取数据出错:", error) toast({ - title: "获取管理员详情失败", + title: "获取数据失败", description: "请检查网络连接后重试", variant: "destructive", }) @@ -71,24 +97,78 @@ export default function EditAdminPage({ params }: { params: { id: string } }) { } } - fetchAdminDetail() + fetchData() }, [params.id]) - const togglePermission = (permissionId: string) => { + // 切换权限选择 + const togglePermission = (permissionId: number) => { setSelectedPermissions((prev) => prev.includes(permissionId) ? prev.filter((id) => id !== permissionId) : [...prev, permissionId], ) } - const handleSubmit = (e: React.FormEvent) => { + // 提交表单 + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + + // 验证密码 + if (password && password !== confirmPassword) { + toast({ + title: "密码不匹配", + description: "两次输入的密码不一致", + variant: "destructive", + }) + return + } + setIsSubmitting(true) - - // Simulate API call - setTimeout(() => { + + try { + // 准备提交的数据 + const updateData: any = { + username, + name, + } + + // 如果有设置密码,则添加密码字段 + if (password) { + updateData.password = password + } + + // 如果可以编辑权限,则添加权限字段 + if (canEditPermissions) { + updateData.permissionIds = selectedPermissions + } + + // 调用更新API + const response = await updateAdministrator(params.id, updateData) + + if (response.code === 200) { + toast({ + title: "更新成功", + description: "管理员信息已更新", + variant: "success", + }) + + // 更新成功后返回列表页 + router.push("/dashboard/admins") + } else { + toast({ + title: "更新失败", + description: response.msg || "请稍后重试", + variant: "destructive", + }) + } + } catch (error) { + console.error("更新管理员信息出错:", error) + toast({ + title: "更新失败", + description: "请检查网络连接后重试", + variant: "destructive", + }) + } finally { setIsSubmitting(false) - router.push("/dashboard/admins") - }, 1500) + } } if (isLoading) { @@ -145,39 +225,60 @@ export default function EditAdminPage({ params }: { params: { id: string } }) {
    - + setPassword(e.target.value)} + />
    - + setConfirmPassword(e.target.value)} + />
    -
    - -
    - {permissions.map((permission) => ( -
    - togglePermission(permission.id)} - /> - -
    - ))} + {canEditPermissions && ( +
    + +
    + {menuPermissions.map((menu) => ( +
    + togglePermission(menu.id)} + /> + +
    + ))} +
    -
    + )} diff --git a/SuperAdmin/app/dashboard/admins/new/page.tsx b/SuperAdmin/app/dashboard/admins/new/page.tsx index 4ef3fa3d..0d10a691 100644 --- a/SuperAdmin/app/dashboard/admins/new/page.tsx +++ b/SuperAdmin/app/dashboard/admins/new/page.tsx @@ -2,43 +2,143 @@ import type React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft } from "lucide-react" +import { ArrowLeft, Loader2 } from "lucide-react" import Link from "next/link" import { Checkbox } from "@/components/ui/checkbox" +import { addAdministrator } from "@/lib/admin-api" +import { useToast } from "@/components/ui/use-toast" +import { getTopLevelMenus } from "@/lib/menu-api" +import { getAdminInfo } from "@/lib/utils" + +interface MenuPermission { + id: number; + title: string; +} export default function NewAdminPage() { const router = useRouter() + const { toast } = useToast() + const [username, setUsername] = useState("") + const [name, setName] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(true) const [isSubmitting, setIsSubmitting] = useState(false) + const [menuPermissions, setMenuPermissions] = useState([]) + const [selectedPermissions, setSelectedPermissions] = useState([]) + const [canManagePermissions, setCanManagePermissions] = useState(false) - const permissions = [ - { id: "project_management", label: "项目管理" }, - { id: "customer_pool", label: "客户池" }, - { id: "admin_management", label: "管理员权限" }, - ] + // 加载权限数据 + useEffect(() => { + const loadPermissions = async () => { + setIsLoading(true) + try { + // 获取当前登录的管理员 + const currentAdmin = getAdminInfo() + + // 只有超级管理员(ID为1)可以管理权限 + if (currentAdmin && currentAdmin.id === 1) { + setCanManagePermissions(true) + + // 获取菜单权限 + const response = await getTopLevelMenus() + if (response.code === 200 && response.data) { + setMenuPermissions(response.data) + } + } + } catch (error) { + console.error("获取权限数据失败:", error) + toast({ + title: "获取权限数据失败", + description: "请检查网络连接后重试", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + loadPermissions() + }, []) - const [selectedPermissions, setSelectedPermissions] = useState([]) - - const togglePermission = (permissionId: string) => { + const togglePermission = (permissionId: number) => { setSelectedPermissions((prev) => prev.includes(permissionId) ? prev.filter((id) => id !== permissionId) : [...prev, permissionId], ) } - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + + // 验证密码 + if (!password) { + toast({ + title: "密码不能为空", + description: "添加管理员时必须设置密码", + variant: "destructive", + }) + return + } + + if (password !== confirmPassword) { + toast({ + title: "密码不匹配", + description: "两次输入的密码不一致", + variant: "destructive", + }) + return + } + setIsSubmitting(true) - - // Simulate API call - setTimeout(() => { + + try { + // 准备提交数据 + const data: any = { + username, + name, + password, + } + + // 如果可以管理权限,则添加权限设置 + if (canManagePermissions && selectedPermissions.length > 0) { + data.permissionIds = selectedPermissions + } + + // 调用添加API + const response = await addAdministrator(data) + + if (response.code === 200) { + toast({ + title: "添加成功", + description: "管理员账号已成功添加", + variant: "success", + }) + + // 返回管理员列表页 + router.push("/dashboard/admins") + } else { + toast({ + title: "添加失败", + description: response.msg || "请稍后重试", + variant: "destructive", + }) + } + } catch (error) { + console.error("添加管理员出错:", error) + toast({ + title: "添加失败", + description: "请检查网络连接后重试", + variant: "destructive", + }) + } finally { setIsSubmitting(false) - router.push("/dashboard/admins") - }, 1500) + } } return ( @@ -56,55 +156,97 @@ export default function NewAdminPage() { 管理员信息 - 创建新管理员账号并设置权限 + 创建新的管理员账号
    - + setUsername(e.target.value)} + placeholder="请输入账号" + required + />
    - + setName(e.target.value)} + placeholder="请输入姓名" + required + />
    - + setPassword(e.target.value)} + placeholder="请输入密码" + required + />
    - - + + setConfirmPassword(e.target.value)} + placeholder="请再次输入密码" + required + />
    -
    - -
    - {permissions.map((permission) => ( -
    - togglePermission(permission.id)} - /> - -
    - ))} + {canManagePermissions && ( +
    + +
    + {isLoading ? ( +
    + + 加载权限数据中... +
    + ) : ( + menuPermissions.map((menu) => ( +
    + togglePermission(menu.id)} + /> + +
    + )) + )} +
    -
    + )} diff --git a/SuperAdmin/lib/admin-api.ts b/SuperAdmin/lib/admin-api.ts index c4190492..ccf501f4 100644 --- a/SuperAdmin/lib/admin-api.ts +++ b/SuperAdmin/lib/admin-api.ts @@ -84,4 +84,41 @@ export async function getAdministrators( */ export async function getAdministratorDetail(id: number | string): Promise> { return apiRequest(`/administrator/detail/${id}`); +} + +/** + * 更新管理员信息 + * @param id 管理员ID + * @param data 更新的数据 + * @returns 更新结果 + */ +export async function updateAdministrator( + id: number | string, + data: { + username: string; + name: string; + password?: string; + permissionIds?: number[]; + } +): Promise> { + return apiRequest('/administrator/update', 'POST', { + id, + ...data + }); +} + +/** + * 添加管理员 + * @param data 管理员数据 + * @returns 添加结果 + */ +export async function addAdministrator( + data: { + username: string; + name: string; + password: string; + permissionIds?: number[]; + } +): Promise> { + return apiRequest('/administrator/add', 'POST', data); } \ No newline at end of file diff --git a/SuperAdmin/lib/menu-api.ts b/SuperAdmin/lib/menu-api.ts index e284bd42..ad0de545 100644 --- a/SuperAdmin/lib/menu-api.ts +++ b/SuperAdmin/lib/menu-api.ts @@ -25,6 +25,9 @@ export async function getMenus(onlyEnabled: boolean = true): Promise const params = new URLSearchParams(); params.append('only_enabled', onlyEnabled ? '1' : '0'); + // 禁用缓存,每次都获取最新的基于用户权限的菜单 + params.append('use_cache', '0'); + const response = await apiRequest(`/menu/tree?${params.toString()}`); return response.data || []; @@ -107,4 +110,21 @@ export async function updateMenuStatus(id: number, status: 0 | 1): Promise> { + try { + return await apiRequest('/menu/toplevel'); + } catch (error) { + console.error('获取一级菜单失败:', error); + return { + code: 500, + msg: '获取一级菜单失败', + data: [] + }; + } } \ No newline at end of file From 8e5a636893cf3a5d977945643fce042ea0e1d109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=B3=E6=B8=85=E7=88=BD?= Date: Thu, 10 Apr 2025 16:34:02 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E8=B6=85=E7=AE=A1=E5=90=8E=E5=8F=B0=20-=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=AE=A1=E7=90=86=E5=91=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/superadmin/config/route.php | 2 + .../superadmin/controller/Administrator.php | 61 ++++++++++++ SuperAdmin/app/dashboard/admins/page.tsx | 95 ++++++++++++++++++- SuperAdmin/components/ui/alert-dialog.tsx | 13 ++- SuperAdmin/lib/admin-api.ts | 9 ++ 5 files changed, 173 insertions(+), 7 deletions(-) diff --git a/Server/application/superadmin/config/route.php b/Server/application/superadmin/config/route.php index caafefef..084a24aa 100644 --- a/Server/application/superadmin/config/route.php +++ b/Server/application/superadmin/config/route.php @@ -26,6 +26,8 @@ Route::group('', function () { Route::post('update', 'app\\superadmin\\controller\\Administrator@updateAdmin'); // 添加管理员 Route::post('add', 'app\\superadmin\\controller\\Administrator@addAdmin'); + // 删除管理员 + Route::post('delete', 'app\\superadmin\\controller\\Administrator@deleteAdmin'); }); // 系统信息相关路由 diff --git a/Server/application/superadmin/controller/Administrator.php b/Server/application/superadmin/controller/Administrator.php index 2a35b061..14189dac 100644 --- a/Server/application/superadmin/controller/Administrator.php +++ b/Server/application/superadmin/controller/Administrator.php @@ -264,4 +264,65 @@ class Administrator extends Controller 'data' => null ]); } + + /** + * 删除管理员 + * @return \think\response\Json + */ + public function deleteAdmin() + { + if (!$this->request->isPost()) { + return json(['code' => 405, 'msg' => '请求方法不允许']); + } + + // 获取当前登录的管理员信息 + $currentAdmin = $this->request->adminInfo; + + // 获取请求参数 + $id = $this->request->post('id/d'); + + // 参数验证 + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数不完整']); + } + + // 不能删除自己的账号 + if ($currentAdmin->id == $id) { + return json(['code' => 403, 'msg' => '不能删除自己的账号']); + } + + // 只有超级管理员(ID为1)可以删除管理员 + if ($currentAdmin->id != 1) { + return json(['code' => 403, 'msg' => '您没有权限删除管理员']); + } + + // 不能删除超级管理员账号 + if ($id == 1) { + return json(['code' => 403, 'msg' => '不能删除超级管理员账号']); + } + + // 查询管理员 + $admin = AdminModel::where('id', $id)->where('deleteTime', 0)->find(); + if (!$admin) { + return json(['code' => 404, 'msg' => '管理员不存在']); + } + + // 执行软删除 + $admin->deleteTime = time(); + $result = $admin->save(); + + if ($result) { + return json([ + 'code' => 200, + 'msg' => '删除成功', + 'data' => null + ]); + } else { + return json([ + 'code' => 500, + 'msg' => '删除失败', + 'data' => null + ]); + } + } } \ No newline at end of file diff --git a/SuperAdmin/app/dashboard/admins/page.tsx b/SuperAdmin/app/dashboard/admins/page.tsx index 2a8d73c5..b2a642d8 100644 --- a/SuperAdmin/app/dashboard/admins/page.tsx +++ b/SuperAdmin/app/dashboard/admins/page.tsx @@ -9,7 +9,17 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { Search, MoreHorizontal, Edit, Trash, UserPlus, Loader2 } from "lucide-react" import { Badge } from "@/components/ui/badge" import { useToast } from "@/components/ui/use-toast" -import { getAdministrators, Administrator } from "@/lib/admin-api" +import { getAdministrators, deleteAdministrator, Administrator } from "@/lib/admin-api" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" // 保留原始示例数据,作为加载失败时的备用数据 const adminsData = [ @@ -54,11 +64,16 @@ const adminsData = [ export default function AdminsPage() { const [searchTerm, setSearchTerm] = useState("") const [isLoading, setIsLoading] = useState(true) + const [isDeleting, setIsDeleting] = useState(false) const [administrators, setAdministrators] = useState([]) const [totalCount, setTotalCount] = useState(0) const [currentPage, setCurrentPage] = useState(1) const [pageSize] = useState(10) const { toast } = useToast() + + // 删除对话框状态 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [adminToDelete, setAdminToDelete] = useState(null) // 加载管理员列表 useEffect(() => { @@ -122,6 +137,50 @@ export default function AdminsPage() { const isSuperAdmin = (id: number) => { return id === 1 } + + // 打开删除确认对话框 + const openDeleteDialog = (admin: Administrator) => { + setAdminToDelete(admin) + setDeleteDialogOpen(true) + } + + // 确认删除管理员 + const confirmDelete = async () => { + if (!adminToDelete) return + + setIsDeleting(true) + try { + const response = await deleteAdministrator(adminToDelete.id) + + if (response.code === 200) { + toast({ + title: "删除成功", + description: `管理员 ${adminToDelete.name} 已成功删除`, + variant: "success", + }) + + // 重新获取管理员列表 + fetchAdministrators() + } else { + toast({ + title: "删除失败", + description: response.msg || "请稍后重试", + variant: "destructive", + }) + } + } catch (error) { + console.error("删除管理员出错:", error) + toast({ + title: "删除失败", + description: "请检查网络连接后重试", + variant: "destructive", + }) + } finally { + setIsDeleting(false) + setDeleteDialogOpen(false) + setAdminToDelete(null) + } + } return (
    @@ -205,7 +264,10 @@ export default function AdminsPage() { {!isSuperAdmin(admin.id) && ( - + openDeleteDialog(admin)} + > 删除管理员 )} @@ -248,6 +310,35 @@ export default function AdminsPage() {
    )} + + {/* 删除确认对话框 */} + + + + 确认删除管理员 + + 您确定要删除管理员 "{adminToDelete?.name}" 吗?此操作无法撤销。 + + + + 取消 + + {isDeleting ? ( + <> + + 删除中... + + ) : ( + "确认删除" + )} + + + +
    ) } diff --git a/SuperAdmin/components/ui/alert-dialog.tsx b/SuperAdmin/components/ui/alert-dialog.tsx index 25e7b474..8ce30316 100644 --- a/SuperAdmin/components/ui/alert-dialog.tsx +++ b/SuperAdmin/components/ui/alert-dialog.tsx @@ -10,7 +10,12 @@ const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger -const AlertDialogPortal = AlertDialogPrimitive.Portal +const AlertDialogPortal = ({ + ...props +}: AlertDialogPrimitive.AlertDialogPortalProps) => ( + +) +AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName const AlertDialogOverlay = React.forwardRef< React.ElementRef, @@ -18,7 +23,7 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( > { return apiRequest('/administrator/add', 'POST', data); +} + +/** + * 删除管理员 + * @param id 管理员ID + * @returns 删除结果 + */ +export async function deleteAdministrator(id: number | string): Promise> { + return apiRequest('/administrator/delete', 'POST', { id }); } \ No newline at end of file