前端重构
This commit is contained in:
parent
5f66394e7c
commit
fad3767b79
|
|
@ -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<GlobalHeaderRightProps> = ({
|
|||
* 退出登录
|
||||
*/
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<title>登录 - {Settings.title || 'Kratos Admin'}</title>
|
||||
</Helmet>
|
||||
|
||||
{/* 左侧品牌区域 */}
|
||||
<div className={styles.leftSection}>
|
||||
<div className={styles.oblique} />
|
||||
<div className={styles.floatingCircle1} />
|
||||
<div className={styles.floatingCircle2} />
|
||||
<div className={styles.floatingCircle3} />
|
||||
<div className={styles.floatingCircle4} />
|
||||
|
||||
<div className={styles.formWrapper}>
|
||||
<div className={styles.brandContent}>
|
||||
<h1 className={styles.brandTitle}>Kratos Admin</h1>
|
||||
<p className={styles.brandSubtitle}>
|
||||
基于 React 19 + Go Kratos 微服务框架构建的现代化全栈管理平台,
|
||||
提供高效、安全、可扩展的企业级解决方案
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧登录表单 */}
|
||||
<div className={styles.rightSection}>
|
||||
<div className={styles.loginCard}>
|
||||
<div className={styles.logoWrapper}>
|
||||
<Logo size="large" />
|
||||
</div>
|
||||
|
||||
<h1 className={styles.title}>Kratos Admin</h1>
|
||||
<p className={styles.subtitle}>
|
||||
基于 React 19 + Go Kratos 的全栈管理平台
|
||||
</p>
|
||||
<h2 className={styles.title}>欢迎回来</h2>
|
||||
<p className={styles.subtitle}>请输入您的账号信息登录系统</p>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
|
|
@ -288,28 +418,28 @@ const Login: React.FC = () => {
|
|||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
className={styles.formItem}
|
||||
className={`${styles.formItem} ${styles.inputWrapper}`}
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 5, message: '用户名至少5个字符' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
prefix={<UserOutlined style={{ color: '#94a3b8' }} />}
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
className={styles.formItem}
|
||||
className={`${styles.formItem} ${styles.inputWrapper}`}
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
prefix={<LockOutlined style={{ color: '#94a3b8' }} />}
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
@ -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}位验证码` },
|
||||
]}
|
||||
>
|
||||
<div className={styles.captchaWrapper}>
|
||||
<Input
|
||||
prefix={<SafetyOutlined />}
|
||||
prefix={<SafetyOutlined style={{ color: '#94a3b8' }} />}
|
||||
placeholder="请输入验证码"
|
||||
className={styles.captchaInput}
|
||||
className={`${styles.captchaInput} ${styles.inputWrapper}`}
|
||||
/>
|
||||
{captchaData.picPath && (
|
||||
<img
|
||||
|
|
@ -349,7 +473,7 @@ const Login: React.FC = () => {
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item style={{ marginBottom: '12px' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
|
|
@ -360,7 +484,7 @@ const Login: React.FC = () => {
|
|||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleCheckInit}
|
||||
|
|
@ -373,14 +497,7 @@ const Login: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightSection}>
|
||||
<img
|
||||
src="/login_right_banner.jpg"
|
||||
alt="banner"
|
||||
className={styles.rightBanner}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className={styles.bottomInfo}>
|
||||
<BottomInfo>
|
||||
<div className={styles.links}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue