refactor: 前端登录/权限/界面状态迁移至 zustand
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown, Drawer, Grid, Tooltip } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Layout, Menu, Button, Avatar, Badge, Dropdown, Drawer, Grid, Tooltip } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
TeamOutlined,
|
||||
@@ -28,16 +28,16 @@ import {
|
||||
TrophyOutlined,
|
||||
ApiOutlined,
|
||||
RobotOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { usePermission } from '../hooks/usePermission';
|
||||
import api from '../api';
|
||||
import {
|
||||
beginPermissionVerification,
|
||||
clearPermissions,
|
||||
writePermissions,
|
||||
} from '../auth/permission-store';
|
||||
import { useAppStore } from '../store/app/appStore';
|
||||
import { usePermissionStore } from '../store/permission/permissionStore';
|
||||
import { useUserStore } from '../store/user/userStore';
|
||||
import NotificationBell from '../components/NotificationBell';
|
||||
import RouteDock from '../components/RouteDock';
|
||||
import RouteKeeper from '../components/RouteKeeper';
|
||||
import { buildMenu, type AppMenuItem } from '../auth/menu-policy';
|
||||
|
||||
const AiChatDrawer = React.lazy(() => import('../components/AiChat/AiChatDrawer'));
|
||||
@@ -75,16 +75,22 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
};
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [aiChatOpen, setAiChatOpen] = useState(false);
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
const prevPathname = useRef('');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [user, setUser] = useState<{ name?: string; username?: string; roles?: string[] }>(() =>
|
||||
JSON.parse(localStorage.getItem('user') || '{}'),
|
||||
);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const updateUser = useUserStore((state) => state.updateUser);
|
||||
const logoutUser = useUserStore((state) => state.logout);
|
||||
const collapsed = useAppStore((state) => state.sidebarCollapsed);
|
||||
const drawerOpen = useAppStore((state) => state.mobileDrawerOpen);
|
||||
const aiChatOpen = useAppStore((state) => state.aiChatOpen);
|
||||
const aiWorking = useAppStore((state) => state.aiWorking);
|
||||
const openKeys = useAppStore((state) => state.menuOpenKeys);
|
||||
const toggleSidebar = useAppStore((state) => state.toggleSidebar);
|
||||
const setDrawerOpen = useAppStore((state) => state.setMobileDrawerOpen);
|
||||
const setAiChatOpen = useAppStore((state) => state.setAiChatOpen);
|
||||
const setAiWorking = useAppStore((state) => state.setAiWorking);
|
||||
const setOpenKeys = useAppStore((state) => state.setMenuOpenKeys);
|
||||
const { permissions, hasPermission } = usePermission();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,13 +99,13 @@ const MainLayout: React.FC = () => {
|
||||
let verificationInFlight = false;
|
||||
|
||||
const verifyPermissions = () => {
|
||||
if (cancelled || verificationInFlight || !localStorage.getItem('token')) return;
|
||||
if (cancelled || verificationInFlight || !useUserStore.getState().token) return;
|
||||
if (retryTimer !== undefined) {
|
||||
window.clearTimeout(retryTimer);
|
||||
retryTimer = undefined;
|
||||
}
|
||||
verificationInFlight = true;
|
||||
beginPermissionVerification();
|
||||
usePermissionStore.getState().beginPermissionVerification();
|
||||
api
|
||||
.get<{ id: number; username: string; permissions: string[]; roles?: string[] }>(
|
||||
'/auth/profile',
|
||||
@@ -107,22 +113,19 @@ const MainLayout: React.FC = () => {
|
||||
.then((profile) => {
|
||||
if (cancelled) return;
|
||||
verificationInFlight = false;
|
||||
writePermissions(profile.permissions || []);
|
||||
const cachedUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const nextUser = { ...cachedUser, ...profile };
|
||||
localStorage.setItem('user', JSON.stringify(nextUser));
|
||||
setUser(nextUser);
|
||||
usePermissionStore.getState().writePermissions(profile.permissions || []);
|
||||
updateUser(profile);
|
||||
})
|
||||
.catch(() => {
|
||||
verificationInFlight = false;
|
||||
if (cancelled || !localStorage.getItem('token')) return;
|
||||
if (cancelled || !useUserStore.getState().token) return;
|
||||
retryTimer = window.setTimeout(verifyPermissions, 5_000);
|
||||
});
|
||||
};
|
||||
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key !== 'token' && event.key !== 'permissions') return;
|
||||
beginPermissionVerification();
|
||||
usePermissionStore.getState().beginPermissionVerification();
|
||||
window.location.reload();
|
||||
};
|
||||
const handleOnline = () => verifyPermissions();
|
||||
@@ -141,7 +144,7 @@ const MainLayout: React.FC = () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
}, [updateUser]);
|
||||
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.sm; // < 576px (仅 xs)
|
||||
@@ -150,16 +153,15 @@ const MainLayout: React.FC = () => {
|
||||
const usesDrawer = !isDesktop;
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => buildMenu(user.roles ?? [], permissions),
|
||||
[user.roles, permissions],
|
||||
() => buildMenu(user?.roles ?? [], permissions),
|
||||
[user, permissions],
|
||||
);
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
clearPermissions();
|
||||
logoutUser();
|
||||
usePermissionStore.getState().clearPermissions();
|
||||
navigate('/login');
|
||||
}, [navigate]);
|
||||
}, [logoutUser, navigate]);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(key: string) => {
|
||||
@@ -303,17 +305,25 @@ const MainLayout: React.FC = () => {
|
||||
<MenuFoldOutlined />
|
||||
)
|
||||
}
|
||||
onClick={() => (usesDrawer ? setDrawerOpen(true) : setCollapsed(!collapsed))}
|
||||
onClick={() => (usesDrawer ? setDrawerOpen(true) : toggleSidebar())}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
{hasPermission('ai:chat:use') && (
|
||||
<Tooltip title="AI 助理">
|
||||
<Button
|
||||
type="text"
|
||||
aria-label="打开 AI 助理"
|
||||
icon={<RobotOutlined style={{ fontSize: 17 }} />}
|
||||
onClick={() => setAiChatOpen(true)}
|
||||
/>
|
||||
<Tooltip title={aiWorking ? 'AI 处理中' : 'AI 助理'}>
|
||||
<Badge dot={aiWorking} color="#007AFF" offset={[-3, 5]}>
|
||||
<Button
|
||||
type="text"
|
||||
aria-label={aiWorking ? 'AI 助理处理中' : '打开 AI 助理'}
|
||||
icon={
|
||||
aiWorking ? (
|
||||
<LoadingOutlined style={{ fontSize: 17, color: '#007AFF' }} spin />
|
||||
) : (
|
||||
<RobotOutlined style={{ fontSize: 17 }} />
|
||||
)
|
||||
}
|
||||
onClick={() => setAiChatOpen(true)}
|
||||
/>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasPermission('notification:view') && <NotificationBell />}
|
||||
@@ -336,7 +346,7 @@ const MainLayout: React.FC = () => {
|
||||
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}
|
||||
>
|
||||
<Avatar icon={<UserOutlined />} />
|
||||
{isDesktop && <span>{user.name || user.username || '用户'}</span>}
|
||||
{isDesktop && <span>{user?.name || user?.username || '用户'}</span>}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@@ -356,12 +366,16 @@ const MainLayout: React.FC = () => {
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
<RouteKeeper />
|
||||
</Content>
|
||||
</Layout>
|
||||
{hasPermission('ai:chat:use') && aiChatOpen && (
|
||||
{hasPermission('ai:chat:use') && (
|
||||
<React.Suspense fallback={null}>
|
||||
<AiChatDrawer open onClose={() => setAiChatOpen(false)} />
|
||||
<AiChatDrawer
|
||||
open={aiChatOpen}
|
||||
onClose={() => setAiChatOpen(false)}
|
||||
onRequestingChange={setAiWorking}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)}
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user