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] =?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'); + } +}