前端重构

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 { setUserAuthority } from "@/services/kratos/user";
import { jsonInBlacklist } from "@/services/kratos/jwt";
import { import {
LogoutOutlined, LogoutOutlined,
SettingOutlined, SettingOutlined,
@ -14,8 +14,6 @@ import React from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import HeaderDropdown from "../HeaderDropdown"; import HeaderDropdown from "../HeaderDropdown";
const adminService = createAdminService();
export type GlobalHeaderRightProps = { export type GlobalHeaderRightProps = {
menu?: boolean; menu?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
@ -56,7 +54,10 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
* 退 * 退
*/ */
const loginOut = async () => { const loginOut = async () => {
await adminService.Logout({}); const res = await jsonInBlacklist();
if (res.code !== 0) {
return;
}
const { search, pathname } = window.location; const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams; const urlParams = new URL(window.location.href).searchParams;
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({

View File

@ -1,9 +1,9 @@
/** /**
* KRA - Login Page * 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 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 { UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons';
import { history, Helmet, useModel } from '@umijs/max'; import { history, Helmet, useModel } from '@umijs/max';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
@ -13,7 +13,7 @@ import { useUserStore } from '@/models/user';
import { convertToMenuData } from '@/utils/dynamicRouter'; import { convertToMenuData } from '@/utils/dynamicRouter';
import Logo from '@/components/Logo/Logo'; import Logo from '@/components/Logo/Logo';
import BottomInfo from '@/components/BottomInfo/BottomInfo'; import BottomInfo from '@/components/BottomInfo/BottomInfo';
import { createStyles } from 'antd-style'; import { createStyles, keyframes } from 'antd-style';
import Settings from '../../../../config/defaultSettings'; import Settings from '../../../../config/defaultSettings';
interface LoginFormData { interface LoginFormData {
@ -29,61 +29,140 @@ interface CaptchaData {
openCaptcha: boolean; 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 }) => ({ const useStyles = createStyles(({ token }) => ({
container: { container: {
width: '100%', width: '100%',
height: '100vh', minHeight: '100vh',
display: 'flex', display: 'flex',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
}, },
// 左侧品牌区域 - 深色渐变背景
leftSection: { leftSection: {
flex: 1, 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', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: '#fff', background: '#f8fafc',
position: 'relative', padding: '24px',
zIndex: 10, '@media (min-width: 992px)': {
}, width: '480px',
rightSection: { minWidth: '480px',
width: '50%',
height: '100%',
background: '#194bfb',
display: 'none',
'@media (min-width: 768px)': {
display: 'block',
}, },
}, },
rightBanner: { // 登录卡片
loginCard: {
width: '100%', width: '100%',
height: '100%', maxWidth: '400px',
objectFit: 'cover',
},
oblique: {
position: 'absolute',
height: '130%',
width: '60%',
background: '#fff', background: '#fff',
transform: 'rotate(-12deg)', borderRadius: '16px',
marginLeft: '-200px', padding: '40px 32px',
zIndex: 5, boxShadow: '0 4px 24px rgba(0, 0, 0, 0.08)',
},
formWrapper: {
width: '100%',
maxWidth: '384px',
padding: '48px 24px',
zIndex: 999,
}, },
logoWrapper: { logoWrapper: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginBottom: '16px', marginBottom: '24px',
}, },
title: { title: {
textAlign: 'center' as const, textAlign: 'center' as const,
fontSize: '32px', fontSize: '28px',
fontWeight: 700, fontWeight: 700,
marginBottom: '8px', marginBottom: '8px',
color: token.colorText, color: token.colorText,
@ -92,10 +171,29 @@ const useStyles = createStyles(({ token }) => ({
textAlign: 'center' as const, textAlign: 'center' as const,
fontSize: '14px', fontSize: '14px',
color: token.colorTextSecondary, color: token.colorTextSecondary,
marginBottom: '36px', marginBottom: '32px',
}, },
// 表单样式
formItem: { 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: { captchaWrapper: {
display: 'flex', display: 'flex',
@ -106,21 +204,44 @@ const useStyles = createStyles(({ token }) => ({
}, },
captchaImage: { captchaImage: {
width: '120px', width: '120px',
height: '40px', height: '48px',
borderRadius: '6px', borderRadius: '8px',
cursor: 'pointer', cursor: 'pointer',
background: '#c3d4f2', background: 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
transition: 'transform 0.2s ease',
'&:hover': {
transform: 'scale(1.02)',
},
}, },
submitBtn: { submitBtn: {
width: '100%', width: '100%',
height: '44px', height: '48px',
fontSize: '16px', 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: { initBtn: {
width: '100%', width: '100%',
height: '44px', height: '48px',
fontSize: '16px', fontSize: '16px',
fontWeight: 500,
borderRadius: '8px',
marginTop: '12px', marginTop: '12px',
border: '1px solid #e2e8f0',
color: token.colorTextSecondary,
transition: 'all 0.3s ease',
'&:hover': {
borderColor: '#6366f1',
color: '#6366f1',
},
}, },
bottomInfo: { bottomInfo: {
position: 'absolute' as const, position: 'absolute' as const,
@ -133,19 +254,22 @@ const useStyles = createStyles(({ token }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: '12px', gap: '16px',
}, },
linkIcon: { linkIcon: {
width: '32px', width: '36px',
height: '32px', height: '36px',
cursor: 'pointer', cursor: 'pointer',
transition: 'transform 0.2s', transition: 'all 0.3s ease',
opacity: 0.8,
'&:hover': { '&:hover': {
transform: 'scale(1.1)', transform: 'scale(1.15)',
opacity: 1,
}, },
}, },
})); }));
const Login: React.FC = () => { const Login: React.FC = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -164,7 +288,6 @@ const Login: React.FC = () => {
const fetchCaptcha = useCallback(async () => { const fetchCaptcha = useCallback(async () => {
try { try {
const res = await captcha(); const res = await captcha();
// request.ts 的 responseInterceptors 已经解包,直接使用 res
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
setCaptchaData({ setCaptchaData({
captchaId: res.data.captchaId || '', captchaId: res.data.captchaId || '',
@ -196,17 +319,14 @@ const Login: React.FC = () => {
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
const { token, user } = res.data; const { token, user } = res.data;
// 保存 token 和用户信息
setToken(token); setToken(token);
setUserInfo(user); setUserInfo(user);
// 更新全局状态,重新获取用户信息和菜单
const [userInfo, menus] = await Promise.all([ const [userInfo, menus] = await Promise.all([
initialState?.fetchUserInfo?.(), initialState?.fetchUserInfo?.(),
initialState?.fetchMenus?.(), initialState?.fetchMenus?.(),
]); ]);
// 转换菜单数据
const menuData = menus ? convertToMenuData(menus) : []; const menuData = menus ? convertToMenuData(menus) : [];
flushSync(() => { flushSync(() => {
@ -220,14 +340,12 @@ const Login: React.FC = () => {
messageApi.success('登录成功!'); messageApi.success('登录成功!');
// 跳转到用户默认首页或重定向页面
const urlParams = new URL(window.location.href).searchParams; const urlParams = new URL(window.location.href).searchParams;
const redirect = urlParams.get('redirect'); const redirect = urlParams.get('redirect');
if (redirect) { if (redirect) {
history.push(redirect); history.push(redirect);
} else { } else {
// 使用用户角色的默认路由
const defaultRouter = user?.authority?.defaultRouter || 'dashboard'; const defaultRouter = user?.authority?.defaultRouter || 'dashboard';
history.push(`/${defaultRouter}`); history.push(`/${defaultRouter}`);
} }
@ -247,7 +365,6 @@ const Login: React.FC = () => {
const handleCheckInit = async () => { const handleCheckInit = async () => {
try { try {
const res = await checkDB(); const res = await checkDB();
// request.ts 的 responseInterceptors 已经解包,直接使用 res
if (res.code === 0 && res.data) { if (res.code === 0 && res.data) {
const checkResult = res.data as { needInit?: boolean }; const checkResult = res.data as { needInit?: boolean };
if (checkResult.needInit) { if (checkResult.needInit) {
@ -267,18 +384,31 @@ const Login: React.FC = () => {
<title> - {Settings.title || 'Kratos Admin'}</title> <title> - {Settings.title || 'Kratos Admin'}</title>
</Helmet> </Helmet>
{/* 左侧品牌区域 */}
<div className={styles.leftSection}> <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}> <div className={styles.logoWrapper}>
<Logo size="large" /> <Logo size="large" />
</div> </div>
<h1 className={styles.title}>Kratos Admin</h1> <h2 className={styles.title}></h2>
<p className={styles.subtitle}> <p className={styles.subtitle}></p>
React 19 + Go Kratos
</p>
<Form <Form
form={form} form={form}
@ -288,28 +418,28 @@ const Login: React.FC = () => {
> >
<Form.Item <Form.Item
name="username" name="username"
className={styles.formItem} className={`${styles.formItem} ${styles.inputWrapper}`}
rules={[ rules={[
{ required: true, message: '请输入用户名' }, { required: true, message: '请输入用户名' },
{ min: 5, message: '用户名至少5个字符' }, { min: 5, message: '用户名至少5个字符' },
]} ]}
> >
<Input <Input
prefix={<UserOutlined />} prefix={<UserOutlined style={{ color: '#94a3b8' }} />}
placeholder="请输入用户名" placeholder="请输入用户名"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="password" name="password"
className={styles.formItem} className={`${styles.formItem} ${styles.inputWrapper}`}
rules={[ rules={[
{ required: true, message: '请输入密码' }, { required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6个字符' }, { min: 6, message: '密码至少6个字符' },
]} ]}
> >
<Input.Password <Input.Password
prefix={<LockOutlined />} prefix={<LockOutlined style={{ color: '#94a3b8' }} />}
placeholder="请输入密码" placeholder="请输入密码"
/> />
</Form.Item> </Form.Item>
@ -320,21 +450,15 @@ const Login: React.FC = () => {
className={styles.formItem} className={styles.formItem}
rules={[ rules={[
{ required: true, message: '请输入验证码' }, { required: true, message: '请输入验证码' },
{ { pattern: /^\d+$/, message: '验证码须为数字' },
pattern: /^\d+$/, { min: captchaData.captchaLength, message: `请输入${captchaData.captchaLength}位验证码` },
message: '验证码须为数字',
},
{
min: captchaData.captchaLength,
message: `请输入${captchaData.captchaLength}位验证码`,
},
]} ]}
> >
<div className={styles.captchaWrapper}> <div className={styles.captchaWrapper}>
<Input <Input
prefix={<SafetyOutlined />} prefix={<SafetyOutlined style={{ color: '#94a3b8' }} />}
placeholder="请输入验证码" placeholder="请输入验证码"
className={styles.captchaInput} className={`${styles.captchaInput} ${styles.inputWrapper}`}
/> />
{captchaData.picPath && ( {captchaData.picPath && (
<img <img
@ -349,7 +473,7 @@ const Login: React.FC = () => {
</Form.Item> </Form.Item>
)} )}
<Form.Item> <Form.Item style={{ marginBottom: '12px' }}>
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
@ -360,7 +484,7 @@ const Login: React.FC = () => {
</Button> </Button>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item style={{ marginBottom: 0 }}>
<Button <Button
type="default" type="default"
onClick={handleCheckInit} onClick={handleCheckInit}
@ -373,14 +497,7 @@ const Login: React.FC = () => {
</div> </div>
</div> </div>
<div className={styles.rightSection}> {/* 底部信息 */}
<img
src="/login_right_banner.jpg"
alt="banner"
className={styles.rightBanner}
/>
</div>
<div className={styles.bottomInfo}> <div className={styles.bottomInfo}>
<BottomInfo> <BottomInfo>
<div className={styles.links}> <div className={styles.links}>