feat(web): add login page, auth store, API client, and route guard

- API client with axios interceptors: JWT attach + 401 auto-refresh
- Auth store (Zustand): login/logout/loadFromStorage with localStorage
- Login page: gradient background, Ant Design form, error handling
- Home page: dashboard with statistics cards
- App.tsx: PrivateRoute guard, /login route, auth state restoration
- MainLayout: dynamic user display, logout dropdown, menu navigation
- Users API service: CRUD with pagination support
This commit is contained in:
iven
2026-04-11 03:38:29 +08:00
parent a7cdf67d17
commit 4a03a639a6
9 changed files with 421 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
import { Layout, Menu, theme, Button, Avatar, Badge, Space } from 'antd';
import { Layout, Menu, theme, Avatar, Space, Dropdown, Button } from 'antd';
import {
HomeOutlined,
UserOutlined,
@@ -7,8 +7,11 @@ import {
SettingOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
LogoutOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useAppStore } from '../stores/app';
import { useAuthStore } from '../stores/auth';
const { Header, Sider, Content, Footer } = Layout;
@@ -20,8 +23,22 @@ const menuItems = [
];
export default function MainLayout({ children }: { children: React.ReactNode }) {
const { sidebarCollapsed, toggleSidebar, tenantName } = useAppStore();
const { sidebarCollapsed, toggleSidebar } = useAppStore();
const { user, logout } = useAuthStore();
const { token } = theme.useToken();
const navigate = useNavigate();
const userMenuItems = [
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: async () => {
await logout();
navigate('/login');
},
},
];
return (
<Layout style={{ minHeight: '100vh' }}>
@@ -39,7 +56,13 @@ export default function MainLayout({ children }: { children: React.ReactNode })
>
{sidebarCollapsed ? 'E' : 'ERP Platform'}
</div>
<Menu theme="dark" mode="inline" items={menuItems} defaultSelectedKeys={['/']} />
<Menu
theme="dark"
mode="inline"
items={menuItems}
defaultSelectedKeys={['/']}
onClick={({ key }) => navigate(key)}
/>
</Sider>
<Layout>
<Header
@@ -60,11 +83,13 @@ export default function MainLayout({ children }: { children: React.ReactNode })
/>
</Space>
<Space size="middle">
<Badge count={5}>
<BellOutlined style={{ fontSize: 18 }} />
</Badge>
<Avatar icon={<UserOutlined />} />
<span>Admin</span>
<BellOutlined style={{ fontSize: 18, cursor: 'pointer' }} />
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<Space style={{ cursor: 'pointer' }}>
<Avatar icon={<UserOutlined />} />
<span>{user?.display_name || user?.username || 'User'}</span>
</Space>
</Dropdown>
</Space>
</Header>
<Content
@@ -79,7 +104,7 @@ export default function MainLayout({ children }: { children: React.ReactNode })
{children}
</Content>
<Footer style={{ textAlign: 'center', padding: '8px 16px' }}>
{tenantName || 'ERP Platform'} · v0.1.0
ERP Platform · v0.1.0
</Footer>
</Layout>
</Layout>