前端重构

This commit is contained in:
Yvan 2026-01-08 15:20:38 +08:00
parent 5f66394e7c
commit fad3767b79
2 changed files with 205 additions and 87 deletions

View File

@ -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({

View File

@ -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}>