diff --git a/web/src/components/RightContent/AvatarDropdown.tsx b/web/src/components/RightContent/AvatarDropdown.tsx index 1ecbd14..f4ffaca 100644 --- a/web/src/components/RightContent/AvatarDropdown.tsx +++ b/web/src/components/RightContent/AvatarDropdown.tsx @@ -1,5 +1,5 @@ -import { createAdminService } from "@/services/index"; import { setUserAuthority } from "@/services/kratos/user"; +import { jsonInBlacklist } from "@/services/kratos/jwt"; import { LogoutOutlined, SettingOutlined, @@ -14,8 +14,6 @@ import React from "react"; import { flushSync } from "react-dom"; import HeaderDropdown from "../HeaderDropdown"; -const adminService = createAdminService(); - export type GlobalHeaderRightProps = { menu?: boolean; children?: React.ReactNode; @@ -56,7 +54,10 @@ export const AvatarDropdown: React.FC = ({ * 退出登录 */ const loginOut = async () => { - await adminService.Logout({}); + const res = await jsonInBlacklist(); + if (res.code !== 0) { + return; + } const { search, pathname } = window.location; const urlParams = new URL(window.location.href).searchParams; const searchParams = new URLSearchParams({ diff --git a/web/src/pages/user/login/index.tsx b/web/src/pages/user/login/index.tsx index f7081f5..1815237 100644 --- a/web/src/pages/user/login/index.tsx +++ b/web/src/pages/user/login/index.tsx @@ -1,9 +1,9 @@ /** * KRA - Login Page - * User authentication with captcha support + * Modern login page with gradient background and card design */ import React, { useState, useEffect, useCallback } from 'react'; -import { Form, Input, Button, message, App } from 'antd'; +import { Form, Input, Button, App } from 'antd'; import { UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons'; import { history, Helmet, useModel } from '@umijs/max'; import { flushSync } from 'react-dom'; @@ -13,7 +13,7 @@ import { useUserStore } from '@/models/user'; import { convertToMenuData } from '@/utils/dynamicRouter'; import Logo from '@/components/Logo/Logo'; import BottomInfo from '@/components/BottomInfo/BottomInfo'; -import { createStyles } from 'antd-style'; +import { createStyles, keyframes } from 'antd-style'; import Settings from '../../../../config/defaultSettings'; interface LoginFormData { @@ -29,61 +29,140 @@ interface CaptchaData { openCaptcha: boolean; } +// 浮动动画 +const float = keyframes` + 0%, 100% { transform: translateY(0px) rotate(0deg); } + 50% { transform: translateY(-20px) rotate(5deg); } +`; + +const float2 = keyframes` + 0%, 100% { transform: translateY(0px) rotate(0deg); } + 50% { transform: translateY(-15px) rotate(-5deg); } +`; + +const pulse = keyframes` + 0%, 100% { opacity: 0.4; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(1.05); } +`; + const useStyles = createStyles(({ token }) => ({ container: { width: '100%', - height: '100vh', + minHeight: '100vh', display: 'flex', position: 'relative', overflow: 'hidden', }, + // 左侧品牌区域 - 深色渐变背景 leftSection: { flex: 1, + display: 'none', + alignItems: 'center', + justifyContent: 'center', + background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)', + position: 'relative', + overflow: 'hidden', + '@media (min-width: 992px)': { + display: 'flex', + }, + }, + // 浮动装饰圆圈 + floatingCircle1: { + position: 'absolute', + width: '300px', + height: '300px', + borderRadius: '50%', + background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.1) 100%)', + top: '-50px', + left: '-50px', + animation: `${float} 8s ease-in-out infinite`, + }, + floatingCircle2: { + position: 'absolute', + width: '200px', + height: '200px', + borderRadius: '50%', + background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(99, 102, 241, 0.1) 100%)', + bottom: '100px', + left: '20%', + animation: `${float2} 10s ease-in-out infinite`, + }, + floatingCircle3: { + position: 'absolute', + width: '150px', + height: '150px', + borderRadius: '50%', + background: 'linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(59, 130, 246, 0.1) 100%)', + top: '30%', + right: '10%', + animation: `${pulse} 6s ease-in-out infinite`, + }, + floatingCircle4: { + position: 'absolute', + width: '100px', + height: '100px', + borderRadius: '50%', + background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.3) 0%, rgba(99, 102, 241, 0.15) 100%)', + bottom: '-20px', + right: '25%', + animation: `${float} 12s ease-in-out infinite`, + }, + // 品牌内容 + brandContent: { + position: 'relative', + zIndex: 10, + textAlign: 'center' as const, + color: '#fff', + padding: '0 48px', + }, + brandTitle: { + fontSize: '42px', + fontWeight: 700, + marginBottom: '16px', + background: 'linear-gradient(135deg, #fff 0%, #a5b4fc 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text', + }, + brandSubtitle: { + fontSize: '16px', + color: 'rgba(255, 255, 255, 0.7)', + lineHeight: 1.6, + maxWidth: '400px', + margin: '0 auto', + }, + // 右侧表单区域 + rightSection: { + width: '100%', + minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', - background: '#fff', - position: 'relative', - zIndex: 10, - }, - rightSection: { - width: '50%', - height: '100%', - background: '#194bfb', - display: 'none', - '@media (min-width: 768px)': { - display: 'block', + background: '#f8fafc', + padding: '24px', + '@media (min-width: 992px)': { + width: '480px', + minWidth: '480px', }, }, - rightBanner: { + // 登录卡片 + loginCard: { width: '100%', - height: '100%', - objectFit: 'cover', - }, - oblique: { - position: 'absolute', - height: '130%', - width: '60%', + maxWidth: '400px', background: '#fff', - transform: 'rotate(-12deg)', - marginLeft: '-200px', - zIndex: 5, - }, - formWrapper: { - width: '100%', - maxWidth: '384px', - padding: '48px 24px', - zIndex: 999, + borderRadius: '16px', + padding: '40px 32px', + boxShadow: '0 4px 24px rgba(0, 0, 0, 0.08)', }, logoWrapper: { display: 'flex', alignItems: 'center', justifyContent: 'center', - marginBottom: '16px', + marginBottom: '24px', }, title: { textAlign: 'center' as const, - fontSize: '32px', + fontSize: '28px', fontWeight: 700, marginBottom: '8px', color: token.colorText, @@ -92,10 +171,29 @@ const useStyles = createStyles(({ token }) => ({ textAlign: 'center' as const, fontSize: '14px', color: token.colorTextSecondary, - marginBottom: '36px', + marginBottom: '32px', }, + // 表单样式 formItem: { - marginBottom: '24px', + marginBottom: '20px', + }, + inputWrapper: { + '& .ant-input-affix-wrapper': { + borderRadius: '8px', + padding: '12px 16px', + border: '1px solid #e2e8f0', + transition: 'all 0.3s ease', + '&:hover': { + borderColor: '#6366f1', + }, + '&:focus, &.ant-input-affix-wrapper-focused': { + borderColor: '#6366f1', + boxShadow: '0 0 0 3px rgba(99, 102, 241, 0.1)', + }, + }, + '& .ant-input': { + fontSize: '15px', + }, }, captchaWrapper: { display: 'flex', @@ -106,21 +204,44 @@ const useStyles = createStyles(({ token }) => ({ }, captchaImage: { width: '120px', - height: '40px', - borderRadius: '6px', + height: '48px', + borderRadius: '8px', cursor: 'pointer', - background: '#c3d4f2', + background: 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)', + transition: 'transform 0.2s ease', + '&:hover': { + transform: 'scale(1.02)', + }, }, submitBtn: { width: '100%', - height: '44px', + height: '48px', fontSize: '16px', + fontWeight: 600, + borderRadius: '8px', + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + border: 'none', + transition: 'all 0.3s ease', + '&:hover': { + background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)', + transform: 'translateY(-1px)', + boxShadow: '0 4px 12px rgba(99, 102, 241, 0.4)', + }, }, initBtn: { width: '100%', - height: '44px', + height: '48px', fontSize: '16px', + fontWeight: 500, + borderRadius: '8px', marginTop: '12px', + border: '1px solid #e2e8f0', + color: token.colorTextSecondary, + transition: 'all 0.3s ease', + '&:hover': { + borderColor: '#6366f1', + color: '#6366f1', + }, }, bottomInfo: { position: 'absolute' as const, @@ -133,19 +254,22 @@ const useStyles = createStyles(({ token }) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', - gap: '12px', + gap: '16px', }, linkIcon: { - width: '32px', - height: '32px', + width: '36px', + height: '36px', cursor: 'pointer', - transition: 'transform 0.2s', + transition: 'all 0.3s ease', + opacity: 0.8, '&:hover': { - transform: 'scale(1.1)', + transform: 'scale(1.15)', + opacity: 1, }, }, })); + const Login: React.FC = () => { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); @@ -164,7 +288,6 @@ const Login: React.FC = () => { const fetchCaptcha = useCallback(async () => { try { const res = await captcha(); - // request.ts 的 responseInterceptors 已经解包,直接使用 res if (res.code === 0 && res.data) { setCaptchaData({ captchaId: res.data.captchaId || '', @@ -196,17 +319,14 @@ const Login: React.FC = () => { if (res.code === 0 && res.data) { const { token, user } = res.data; - // 保存 token 和用户信息 setToken(token); setUserInfo(user); - // 更新全局状态,重新获取用户信息和菜单 const [userInfo, menus] = await Promise.all([ initialState?.fetchUserInfo?.(), initialState?.fetchMenus?.(), ]); - // 转换菜单数据 const menuData = menus ? convertToMenuData(menus) : []; flushSync(() => { @@ -220,14 +340,12 @@ const Login: React.FC = () => { messageApi.success('登录成功!'); - // 跳转到用户默认首页或重定向页面 const urlParams = new URL(window.location.href).searchParams; const redirect = urlParams.get('redirect'); if (redirect) { history.push(redirect); } else { - // 使用用户角色的默认路由 const defaultRouter = user?.authority?.defaultRouter || 'dashboard'; history.push(`/${defaultRouter}`); } @@ -247,7 +365,6 @@ const Login: React.FC = () => { const handleCheckInit = async () => { try { const res = await checkDB(); - // request.ts 的 responseInterceptors 已经解包,直接使用 res if (res.code === 0 && res.data) { const checkResult = res.data as { needInit?: boolean }; if (checkResult.needInit) { @@ -267,18 +384,31 @@ const Login: React.FC = () => { 登录 - {Settings.title || 'Kratos Admin'} + {/* 左侧品牌区域 */}
-
+
+
+
+
-
+
+

Kratos Admin

+

+ 基于 React 19 + Go Kratos 微服务框架构建的现代化全栈管理平台, + 提供高效、安全、可扩展的企业级解决方案 +

+
+
+ + {/* 右侧登录表单 */} +
+
-

Kratos Admin

-

- 基于 React 19 + Go Kratos 的全栈管理平台 -

+

欢迎回来

+

请输入您的账号信息登录系统

{ > } + prefix={} placeholder="请输入用户名" /> } + prefix={} placeholder="请输入密码" /> @@ -320,21 +450,15 @@ const Login: React.FC = () => { className={styles.formItem} rules={[ { required: true, message: '请输入验证码' }, - { - pattern: /^\d+$/, - message: '验证码须为数字', - }, - { - min: captchaData.captchaLength, - message: `请输入${captchaData.captchaLength}位验证码`, - }, + { pattern: /^\d+$/, message: '验证码须为数字' }, + { min: captchaData.captchaLength, message: `请输入${captchaData.captchaLength}位验证码` }, ]} >
} + prefix={} placeholder="请输入验证码" - className={styles.captchaInput} + className={`${styles.captchaInput} ${styles.inputWrapper}`} /> {captchaData.picPath && ( { )} - + - +
-
- banner -
- + {/* 底部信息 */}