fix: 🐛 修复玩家和管理员登录后刷新自动退出的bug优

This commit is contained in:
Stev_Wang
2025-12-27 21:12:05 +08:00
parent 598cd3026b
commit cb5088115a
11 changed files with 195 additions and 86 deletions

View File

@@ -3,8 +3,9 @@ import { useAuthStore } from '../stores/authStore';
const AdminAuthRoute = () => { const AdminAuthRoute = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const authType = useAuthStore((state) => state.authType);
if (!isAuthenticated) { if (!isAuthenticated || authType !== 'admin') {
return <Navigate to="/admin/login" replace />; return <Navigate to="/admin/login" replace />;
} }

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Layout, Menu, Button, theme } from 'antd'; import { Layout, Menu, Button, theme, App } from 'antd';
import { import {
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
@@ -10,7 +10,6 @@ import {
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore'; import { useAuthStore } from '../stores/authStore';
import { adminAuthService } from '../services/adminAuthService'; import { adminAuthService } from '../services/adminAuthService';
import { message } from 'antd';
const { Header, Sider, Content, Footer } = Layout; const { Header, Sider, Content, Footer } = Layout;
@@ -23,11 +22,11 @@ const AdminLayout = () => {
const location = useLocation(); const location = useLocation();
const adminUser = useAuthStore((state) => state.adminUser); const adminUser = useAuthStore((state) => state.adminUser);
const logout = useAuthStore((state) => state.logout); const logout = useAuthStore((state) => state.logout);
const { message } = App.useApp();
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await adminAuthService.logout(); await adminAuthService.logout();
localStorage.removeItem('adminToken');
logout(); logout();
message.success('登出成功'); message.success('登出成功');
navigate('/admin/login'); navigate('/admin/login');

View File

@@ -3,8 +3,25 @@ import { useAuthStore } from '../stores/authStore';
const PlayerAuthRoute = () => { const PlayerAuthRoute = () => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const authType = useAuthStore((state) => state.authType);
const setAuth = useAuthStore((state) => state.setAuth);
if (!isAuthenticated) { // 检查是否有playerToken
const playerToken = localStorage.getItem('playerToken');
const playerUserStr = localStorage.getItem('playerUser');
// 如果有playerToken但authType不是player说明需要重新加载玩家认证
if (playerToken && playerUserStr && authType !== 'player') {
try {
const playerUser = JSON.parse(playerUserStr);
setAuth(playerUser, playerToken, 'player');
} catch (e) {
console.error('解析playerUser失败:', e);
}
}
// 如果没有playerToken重定向到登录页
if (!playerToken) {
return <Navigate to="/player/login" replace />; return <Navigate to="/player/login" replace />;
} }

View File

@@ -1,4 +1,4 @@
import { Layout, Menu, Button, Dropdown, theme } from 'antd'; import { Layout, Menu, Button, Dropdown, theme, App } from 'antd';
import { import {
HomeOutlined, HomeOutlined,
UserOutlined, UserOutlined,
@@ -6,7 +6,6 @@ import {
DownOutlined, DownOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { message } from 'antd';
import { playerAuthService } from '../services/playerAuthService'; import { playerAuthService } from '../services/playerAuthService';
const { Header, Content, Footer } = Layout; const { Header, Content, Footer } = Layout;
@@ -17,11 +16,11 @@ const PlayerLayout = () => {
} = theme.useToken(); } = theme.useToken();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { message } = App.useApp();
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await playerAuthService.logout(); await playerAuthService.logout();
localStorage.removeItem('playerToken');
message.success('登出成功'); message.success('登出成功');
navigate('/player/login'); navigate('/player/login');
} catch (error) { } catch (error) {
@@ -57,40 +56,67 @@ const PlayerLayout = () => {
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'center',
background: colorBgLayout, background: '#001529',
padding: '0 24px', padding: '0',
height: 64,
}} }}
> >
{/* 导航条内容容器 */}
<div <div
style={{ style={{
color: 'white',
fontSize: '20px',
fontWeight: 'bold',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 12, width: '100%',
maxWidth: 1200,
padding: '0 24px',
}} }}
> >
<span>西</span> {/* LOGO标题位 */}
</div> <div
<Menu style={{
theme="dark" color: '#ffffff',
mode="horizontal" fontSize: '20px',
selectedKeys={[location.pathname]} fontWeight: 'bold',
items={menuItems} display: 'flex',
style={{ flex: 1, minWidth: 0, justifyContent: 'center' }} alignItems: 'center',
onClick={({ key }) => navigate(key)} gap: 12,
/> flexShrink: 0,
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight"> }}
<Button
type="text"
style={{ color: 'white' }}
icon={<DownOutlined />}
> >
<span>西</span>
</Button> </div>
</Dropdown>
{/* 导航菜单位 */}
<div
style={{
flex: 1,
display: 'flex',
justifyContent: 'center',
margin: '0 24px',
}}
>
<Menu
theme="dark"
mode="horizontal"
selectedKeys={[location.pathname]}
items={menuItems}
style={{ flex: 1, minWidth: 0, justifyContent: 'center', background: 'transparent' }}
onClick={({ key }) => navigate(key)}
/>
</div>
{/* 个人中心菜单位 */}
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<Button
type="text"
style={{ color: '#ffffff', flexShrink: 0 }}
icon={<DownOutlined />}
>
</Button>
</Dropdown>
</div>
</Header> </Header>
<Content <Content
style={{ style={{

View File

@@ -1,7 +1,7 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { ConfigProvider, theme } from 'antd'; import { ConfigProvider, App, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN'; import zhCN from 'antd/locale/zh_CN';
import './index.css'; import './index.css';
import router from './router'; import router from './router';
@@ -16,7 +16,9 @@ createRoot(document.getElementById('root')!).render(
...appTheme, ...appTheme,
}} }}
> >
<RouterProvider router={router} /> <App>
<RouterProvider router={router} />
</App>
</ConfigProvider> </ConfigProvider>
</StrictMode>, </StrictMode>,
); );

View File

@@ -17,7 +17,7 @@ const AdminDashboard = () => {
title="总用户数" title="总用户数"
value={11280} value={11280}
prefix={<UserOutlined />} prefix={<UserOutlined />}
valueStyle={{ color: '#3f8600' }} styles={{ content: { color: '#3f8600' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -27,7 +27,7 @@ const AdminDashboard = () => {
title="今日订单" title="今日订单"
value={93} value={93}
prefix={<ShoppingOutlined />} prefix={<ShoppingOutlined />}
valueStyle={{ color: '#cf1322' }} styles={{ content: { color: '#cf1322' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -38,7 +38,7 @@ const AdminDashboard = () => {
value={11280} value={11280}
prefix={<DollarOutlined />} prefix={<DollarOutlined />}
precision={2} precision={2}
valueStyle={{ color: '#1890ff' }} styles={{ content: { color: '#1890ff' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -48,7 +48,7 @@ const AdminDashboard = () => {
title="活动参与" title="活动参与"
value={93} value={93}
prefix={<TrophyOutlined />} prefix={<TrophyOutlined />}
valueStyle={{ color: '#722ed1' }} styles={{ content: { color: '#722ed1' } }}
/> />
</Card> </Card>
</Col> </Col>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Form, Input, Button, Card, message } from 'antd'; import { Form, Input, Button, Card, App } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { adminAuthService, type LoginRequest } from '../../services/adminAuthService'; import { adminAuthService, type LoginRequest } from '../../services/adminAuthService';
@@ -9,6 +9,7 @@ const AdminLogin = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const setAuth = useAuthStore((state) => state.setAuth); const setAuth = useAuthStore((state) => state.setAuth);
const { message } = App.useApp();
const onFinish = async (values: LoginRequest) => { const onFinish = async (values: LoginRequest) => {
setLoading(true); setLoading(true);
@@ -20,9 +21,9 @@ const AdminLogin = () => {
username: response.username, username: response.username,
role: response.role, role: response.role,
}, },
response.accessToken response.accessToken,
'admin'
); );
localStorage.setItem('adminToken', response.accessToken);
message.success('登录成功'); message.success('登录成功');
navigate('/admin/dashboard'); navigate('/admin/dashboard');
} catch (error) { } catch (error) {

View File

@@ -1,4 +1,4 @@
import { Card, Row, Col, Statistic, List, Avatar, Button } from 'antd'; import { Card, Row, Col, Statistic, Avatar, Button } from 'antd';
import { import {
TrophyOutlined, TrophyOutlined,
GiftOutlined, GiftOutlined,
@@ -36,7 +36,7 @@ const PlayerDashboard = () => {
value={100} value={100}
suffix="级" suffix="级"
prefix={<StarOutlined />} prefix={<StarOutlined />}
valueStyle={{ color: '#3f8600' }} styles={{ content: { color: '#3f8600' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -47,7 +47,7 @@ const PlayerDashboard = () => {
value={12800} value={12800}
prefix={<GiftOutlined />} prefix={<GiftOutlined />}
precision={2} precision={2}
valueStyle={{ color: '#cf1322' }} styles={{ content: { color: '#cf1322' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -58,7 +58,7 @@ const PlayerDashboard = () => {
value={365} value={365}
suffix="小时" suffix="小时"
prefix={<ClockCircleOutlined />} prefix={<ClockCircleOutlined />}
valueStyle={{ color: '#1890ff' }} styles={{ content: { color: '#1890ff' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -68,7 +68,7 @@ const PlayerDashboard = () => {
title="成就点数" title="成就点数"
value={2560} value={2560}
prefix={<TrophyOutlined />} prefix={<TrophyOutlined />}
valueStyle={{ color: '#722ed1' }} styles={{ content: { color: '#722ed1' } }}
/> />
</Card> </Card>
</Col> </Col>
@@ -76,24 +76,33 @@ const PlayerDashboard = () => {
<Row gutter={16} style={{ marginTop: 24 }}> <Row gutter={16} style={{ marginTop: 24 }}>
<Col span={12}> <Col span={12}>
<Card title="系统公告"> <Card title="系统公告">
<List <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
itemLayout="horizontal" {notices.map((item, index) => (
dataSource={notices} <div
renderItem={(item) => ( key={index}
<List.Item> style={{
<List.Item.Meta display: 'flex',
avatar={<Avatar icon={<TrophyOutlined />} />} alignItems: 'flex-start',
title={item.title} padding: '12px 0',
description={ borderBottom: index !== notices.length - 1 ? '1px solid #f0f0f0' : 'none',
<div> }}
<p>{item.description}</p> >
<small style={{ color: '#999' }}>{item.time}</small> <Avatar
</div> icon={<TrophyOutlined />}
} style={{ marginRight: 12, flexShrink: 0 }}
/> />
</List.Item> <div style={{ flex: 1 }}>
)} <div style={{ fontSize: 16, fontWeight: 500, marginBottom: 4 }}>
/> {item.title}
</div>
<div style={{ color: '#666', marginBottom: 4 }}>
{item.description}
</div>
<small style={{ color: '#999' }}>{item.time}</small>
</div>
</div>
))}
</div>
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col span={12}>

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Form, Input, Button, Card, message } from 'antd'; import { Form, Input, Button, Card, App } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { playerAuthService, type LoginRequest } from '../../services/playerAuthService'; import { playerAuthService, type LoginRequest } from '../../services/playerAuthService';
@@ -9,17 +9,17 @@ const PlayerLogin = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const setAuth = useAuthStore((state) => state.setAuth); const setAuth = useAuthStore((state) => state.setAuth);
const { message } = App.useApp();
const onFinish = async (values: LoginRequest) => { const onFinish = async (values: LoginRequest) => {
setLoading(true); setLoading(true);
try { try {
const response = await playerAuthService.login(values); const response = await playerAuthService.login(values);
localStorage.setItem('playerToken', response.accessToken);
setAuth({ setAuth({
id: response.userId, id: response.userId,
username: response.username, username: response.username,
role: response.role, role: response.role,
}, response.accessToken); }, response.accessToken, 'player');
message.success('登录成功'); message.success('登录成功');
navigate('/player/dashboard'); navigate('/player/dashboard');
} catch (error) { } catch (error) {

View File

@@ -6,28 +6,82 @@ interface AdminUser {
role: string; role: string;
} }
type AuthType = 'admin' | 'player';
interface AuthState { interface AuthState {
isAuthenticated: boolean; isAuthenticated: boolean;
adminUser: AdminUser | null; adminUser: AdminUser | null;
token: string | null; token: string | null;
setAuth: (user: AdminUser, token: string) => void; authType: AuthType | null;
setAuth: (user: AdminUser, token: string, type: AuthType) => void;
logout: () => void; logout: () => void;
} }
export const useAuthStore = create<AuthState>((set) => ({ export const useAuthStore = create<AuthState>((set) => {
isAuthenticated: false, // 初始化时从localStorage加载
adminUser: null, const savedAdminToken = localStorage.getItem('adminToken');
token: null, const savedPlayerToken = localStorage.getItem('playerToken');
setAuth: (user, token) => const savedAdminUserStr = localStorage.getItem('adminUser');
set({ const savedPlayerUserStr = localStorage.getItem('playerUser');
isAuthenticated: true,
adminUser: user, let savedUser: AdminUser | null = null;
token, let savedToken: string | null = null;
}), let savedAuthType: AuthType | null = null;
logout: () =>
set({ // 优先加载管理员认证
isAuthenticated: false, if (savedAdminToken && savedAdminUserStr) {
adminUser: null, try {
token: null, savedUser = JSON.parse(savedAdminUserStr);
}), savedToken = savedAdminToken;
})); savedAuthType = 'admin';
} catch (e) {
console.error('解析adminUser失败:', e);
}
}
// 如果没有管理员认证,则加载玩家认证
if (!savedToken && savedPlayerToken && savedPlayerUserStr) {
try {
savedUser = JSON.parse(savedPlayerUserStr);
savedToken = savedPlayerToken;
savedAuthType = 'player';
} catch (e) {
console.error('解析playerUser失败:', e);
}
}
return {
isAuthenticated: !!savedToken,
adminUser: savedUser,
token: savedToken,
authType: savedAuthType,
setAuth: (user, token, type) => {
// 根据类型保存到不同的localStorage key
if (type === 'admin') {
localStorage.setItem('adminToken', token);
localStorage.setItem('adminUser', JSON.stringify(user));
} else {
localStorage.setItem('playerToken', token);
localStorage.setItem('playerUser', JSON.stringify(user));
}
set({
isAuthenticated: true,
adminUser: user,
token,
authType: type,
});
},
logout: () => {
// 清除所有认证数据
localStorage.removeItem('adminToken');
localStorage.removeItem('adminUser');
localStorage.removeItem('playerToken');
localStorage.removeItem('playerUser');
set({
isAuthenticated: false,
adminUser: null,
token: null,
authType: null,
});
},
};
});

View File

@@ -16,7 +16,7 @@ export const appTheme: ThemeConfig = {
fontSize: 14, fontSize: 14,
// 圆角 // 圆角
borderRadius: 6, borderRadius: 3,
}, },
components: { components: {