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

@@ -0,0 +1,37 @@
import { Typography, Card, Row, Col, Statistic } from 'antd';
import {
UserOutlined,
TeamOutlined,
FileTextOutlined,
BellOutlined,
} from '@ant-design/icons';
export default function Home() {
return (
<div>
<Typography.Title level={4}></Typography.Title>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="用户总数" value={0} prefix={<UserOutlined />} />
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="角色数量" value={0} prefix={<TeamOutlined />} />
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="流程实例" value={0} prefix={<FileTextOutlined />} />
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card>
<Statistic title="未读消息" value={0} prefix={<BellOutlined />} />
</Card>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,68 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Form, Input, Button, Card, message, Typography } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useAuthStore } from '../stores/auth';
const { Title } = Typography;
export default function Login() {
const navigate = useNavigate();
const login = useAuthStore((s) => s.login);
const loading = useAuthStore((s) => s.loading);
const [messageApi, contextHolder] = message.useMessage();
const onFinish = async (values: { username: string; password: string }) => {
try {
await login(values.username, values.password);
messageApi.success('登录成功');
navigate('/');
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ||
'登录失败,请检查用户名和密码';
messageApi.error(errorMsg);
}
};
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
{contextHolder}
<Card style={{ width: 400, borderRadius: 8 }} variant="borderless">
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<Title level={3} style={{ marginBottom: 4 }}>
ERP Platform
</Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
<Form name="login" onFinish={onFinish} autoComplete="off" size="large">
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input prefix={<UserOutlined />} placeholder="用户名" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
</Button>
</Form.Item>
</Form>
</Card>
</div>
);
}