- 暂时移除未使用的变量

- 配置项目依赖
- 组织项目基础结构
- 完成auth页面书写

Change-Id: I132c32f131111121619eb69240ece0a39e295c36
diff --git a/src/AppLayout.tsx b/src/AppLayout.tsx
new file mode 100644
index 0000000..47dc382
--- /dev/null
+++ b/src/AppLayout.tsx
@@ -0,0 +1,50 @@
+import { Outlet, useLocation, useNavigate } from 'react-router';
+import { Layout, Menu } from 'antd';
+import { HomeOutlined, AppstoreOutlined } from '@ant-design/icons';
+
+const { Header } = Layout;
+
+const AppLayout = () => {
+    const location = useLocation();
+    const navigate = useNavigate();
+    // 导航项配置
+    const menuItems = [
+        {
+            key: 'home',
+            label: '主页',
+            icon: <HomeOutlined />,
+            path: '/',
+        },
+        {
+            key: 'tasks',
+            label: '任务清单',
+            icon: <AppstoreOutlined />,
+            path: '/tasks',
+        },
+    ];
+
+    return (
+        <Layout style={{ minHeight: '100vh', width: '100%' }}>
+            <Header className="header" style={{ display: 'flex', alignItems: 'center' }}>
+                <div className="logo" color='white'>创驿</div>
+                <Menu
+                    mode="horizontal"
+                    theme='dark'
+                    selectedKeys={[location.pathname === '/' ? 'home' : location.pathname.slice(1)]}
+                    items={menuItems.map(item => ({
+                        ...item,
+                        onClick: () => navigate(item.path),
+                    }))}
+                />
+            </Header>
+            <Layout.Content style={{ padding: '24px' }}>
+                <Outlet />
+            </Layout.Content>
+            <Layout.Footer>
+                © 2025 创驿 - 创作路上的同行者
+            </Layout.Footer>
+        </Layout>
+    );
+};
+
+export default AppLayout;    
\ No newline at end of file
diff --git a/src/api/authApi.ts b/src/api/authApi.ts
new file mode 100644
index 0000000..b28b04f
--- /dev/null
+++ b/src/api/authApi.ts
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/api/client.ts b/src/api/client.ts
new file mode 100644
index 0000000..623ee78
--- /dev/null
+++ b/src/api/client.ts
@@ -0,0 +1,148 @@
+type RequestOptions = {
+    method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
+    headers?: Record<string, string>;
+    body?: unknown;
+    queryParams?: Record<string, string | number>;
+};
+  
+type Interceptor = {
+    request?: (config: RequestInit) => RequestInit;
+    requestError?: (error: unknown) => Promise<never>; // 错误拦截器应始终抛出
+    response?: (response: Response) => Response | Promise<Response>;
+    // 错误拦截器必须抛出或返回与请求相同类型的结果
+    responseError?: <T>(error: unknown) => Promise<T>;
+};
+  
+export class HttpClient {
+    private static instance: HttpClient;
+    private baseUrl: string;
+    private interceptors: Interceptor[] = [];
+  
+    private constructor(baseUrl: string) {
+      this.baseUrl = baseUrl;
+    }
+  
+    public static getInstance(baseUrl: string): HttpClient {
+      if (!HttpClient.instance) {
+        HttpClient.instance = new HttpClient(baseUrl);
+      }
+      return HttpClient.instance;
+    }
+  
+    // 添加拦截器
+    addInterceptor(interceptor: Interceptor) {
+      this.interceptors.push(interceptor);
+      return this; // 支持链式调用
+    }
+  
+    async request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
+      const { 
+        method = 'GET', 
+        headers = {}, 
+        body, 
+        queryParams = {} 
+      } = options;
+  
+      const url = new URL(`${this.baseUrl}${endpoint}`);
+      Object.entries(queryParams).forEach(([key, value]) => {
+        url.searchParams.append(key, String(value));
+      });
+  
+      let config: RequestInit = {
+        method,
+        headers: {
+          'Content-Type': 'application/json',
+          ...headers
+        }
+      };
+  
+      if (body) {
+        config.body = JSON.stringify(body);
+      }
+  
+      // 请求拦截器处理
+      for (const interceptor of this.interceptors) {
+        if (interceptor.request) {
+          config = interceptor.request(config);
+        }
+      }
+  
+      try {
+        let response = await fetch(url.toString(), config);
+        
+        // 响应拦截器处理
+        for (const interceptor of this.interceptors) {
+          if (interceptor.response) {
+            response = await interceptor.response(response);
+          }
+        }
+  
+        if (!response.ok) {
+          const errorData = await response.json();
+          throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorData.message || 'Unknown error'}`);
+        }
+  
+        return await response.json();
+      } catch (error) {
+        // 改进的错误拦截器处理
+        let lastError = error;
+        
+        for (const interceptor of this.interceptors) {
+          if (interceptor.responseError) {
+            try {
+              // 错误拦截器必须返回Promise<T>或抛出
+              return await interceptor.responseError<T>(lastError);
+            } catch (e) {
+              // 保存最新错误并继续下一个拦截器
+              lastError = e;
+              console.error('Interceptor error:', e);
+            }
+          }
+        }
+        
+        // 如果所有拦截器都未能处理错误,则抛出最后一个错误
+        throw lastError;
+      }
+    }
+    get<T>(endpoint: string, queryParams?: Record<string, string | number>) {
+      return this.request<T>(endpoint, { queryParams });
+    }
+  
+    post<T>(endpoint: string, body?: unknown, headers?: Record<string, string>) {
+      return this.request<T>(endpoint, { method: 'POST', body, headers });
+    }
+  
+    put<T>(endpoint: string, body?: unknown, headers?: Record<string, string>) {
+      return this.request<T>(endpoint, { method: 'PUT', body, headers });
+    }
+  
+    delete<T>(endpoint: string, headers?: Record<string, string>) {
+      return this.request<T>(endpoint, { method: 'DELETE', headers });
+    }
+}
+  
+const client = HttpClient.getInstance('http://localhost:8080/');
+// 添加Token注入拦截器
+client.addInterceptor({
+    request: (config) => {
+      const token = localStorage.getItem('token'); // 从存储中获取token
+      if (token) {
+        config.headers = {
+          ...config.headers,
+          'Authorization': `Bearer ${token}`
+        };
+      }
+      return config;
+    },
+    responseError: async (error) => {
+        if (error instanceof Error && error.message.includes('401')) {
+          console.log('Token expired, redirecting to login...');
+          // 示例:重定向到登录页
+          window.location.href = '/login';
+          // 必须返回Promise<never>或抛出
+          return new Promise(() => {}); // 永远不解决的Promise
+        }
+        throw error;
+      }
+});
+export default client as HttpClient;
\ No newline at end of file
diff --git a/src/api/type.ts b/src/api/type.ts
new file mode 100644
index 0000000..a5dd1ca
--- /dev/null
+++ b/src/api/type.ts
@@ -0,0 +1,10 @@
+export interface LoginRequest {
+    email: string;
+    password: string;
+}
+  
+export interface LoginResponse {
+    user: string;
+    token: string;
+    refreshToken: string;
+}
\ No newline at end of file
diff --git a/src/assets/auth_background.png b/src/assets/auth_background.png
new file mode 100644
index 0000000..3ee84f0
--- /dev/null
+++ b/src/assets/auth_background.png
Binary files differ
diff --git a/src/assets/slogan.png b/src/assets/slogan.png
new file mode 100644
index 0000000..672fe1c
--- /dev/null
+++ b/src/assets/slogan.png
Binary files differ
diff --git a/src/feature/Home.tsx b/src/feature/Home.tsx
new file mode 100644
index 0000000..fc2c982
--- /dev/null
+++ b/src/feature/Home.tsx
@@ -0,0 +1,12 @@
+function Home() {
+
+  return (
+    <>
+      主页
+    </>
+  )
+}
+
+
+
+export default Home
diff --git a/src/feature/auth/AuthLayout.tsx b/src/feature/auth/AuthLayout.tsx
new file mode 100644
index 0000000..cceaf2d
--- /dev/null
+++ b/src/feature/auth/AuthLayout.tsx
@@ -0,0 +1,44 @@
+import { Card, Flex } from "antd";
+import { Outlet } from "react-router";
+import auth_background from "../../assets/auth_background.png"
+import slogan from "../../assets/slogan.png"
+function AuthLayout() {
+
+    return (
+        <Flex gap="20px" justify="center">
+            <div
+                style={{
+                    width: '45%',
+                    height: '80vh',
+                    backgroundImage: `url(${auth_background})`,
+                    backgroundSize: 'cover',
+                    backgroundPosition: 'center',
+                    borderRadius: '12px',
+                    position: 'relative',
+                }}
+            >
+                {/* <h1>
+                    登录创驿
+                </h1>
+                <p>
+                    与众多用户和创作者一起交流
+                </p> */}
+            </div>
+            <Flex
+                style={{ width: 400, height: '80vh' }}
+                vertical
+                gap="20px"
+                justify="space-between"
+            >
+                <Card>
+                    <Outlet></Outlet>
+                </Card>
+                <Card style={{ padding: 0, margin: 0 }}>
+                    <img src={slogan} width="100%" />
+                </Card>
+            </Flex>
+        </Flex>
+    );
+}
+
+export default AuthLayout;    
\ No newline at end of file
diff --git a/src/feature/auth/Forget.tsx b/src/feature/auth/Forget.tsx
new file mode 100644
index 0000000..25bf6dc
--- /dev/null
+++ b/src/feature/auth/Forget.tsx
@@ -0,0 +1,118 @@
+import { MailOutlined, LockOutlined } from '@ant-design/icons';
+import { Button, Form, Input, message, Row, Col } from 'antd';
+import { NavLink } from 'react-router';
+import { useState, useEffect } from 'react';
+
+function Forget() {
+    const [countdown, setCountdown] = useState(0);
+    const [emailSent,] = useState(false);
+
+    const onFinish = async () => {
+
+    };
+
+    useEffect(() => {
+        let countdownTimer = null;
+
+        if (countdown > 0) {
+            countdownTimer = setTimeout(() => {
+                setCountdown(prev => prev - 1);
+            }, 1000);
+        }
+
+        return () => {
+            if (countdownTimer) {
+                clearTimeout(countdownTimer);
+            }
+        };
+    }, [countdown]);
+
+    const resendCode = () => {
+        if (countdown > 0) return;
+        setCountdown(60);
+        message.info('验证码已重新发送');
+    };
+
+    return (
+        <Form
+            name="forget"
+            initialValues={{ remember: true }}
+            style={{ maxWidth: 360 }}
+            onFinish={onFinish}
+        >
+            <h2>重置密码</h2>
+            <p>请输入您注册时使用的邮箱地址</p>
+
+            <Form.Item
+                name="email"
+                rules={[
+                    { required: true, message: '请输入您的邮箱!' },
+                    { type: 'email', message: '请输入正确的邮箱格式' }
+                ]}
+            >
+                <Input prefix={<MailOutlined />} placeholder="注册邮箱" />
+            </Form.Item>
+
+            {emailSent && (
+                <>
+                    <Form.Item
+                        name="code"
+                        rules={[{ required: true, message: '请输入验证码!' }]}
+                    >
+                        <Row gutter={8}>
+                            <Col span={16}>
+                                <Input placeholder="验证码" />
+                            </Col>
+                            <Col span={8}>
+                                <Button
+                                    disabled={countdown > 0}
+                                    onClick={resendCode}
+                                    style={{ width: '100%' }}
+                                >
+                                    {countdown > 0 ? `${countdown}s后重试` : '重新发送'}
+                                </Button>
+                            </Col>
+                        </Row>
+                    </Form.Item>
+
+                    <Form.Item
+                        name="password"
+                        rules={[
+                            { required: true, message: '请设置新密码!' },
+                            { min: 6, message: '密码长度至少为6位' }
+                        ]}
+                    >
+                        <Input.Password prefix={<LockOutlined />} placeholder="新密码" />
+                    </Form.Item>
+
+                    <Form.Item
+                        name="confirmPassword"
+                        dependencies={['password']}
+                        rules={[
+                            { required: true, message: '请确认新密码!' },
+                            ({ getFieldValue }) => ({
+                                validator(_, value) {
+                                    if (!value || getFieldValue('password') === value) {
+                                        return Promise.resolve();
+                                    }
+                                    return Promise.reject(new Error('两次输入的密码不一致!'));
+                                },
+                            }),
+                        ]}
+                    >
+                        <Input.Password prefix={<LockOutlined />} placeholder="确认新密码" />
+                    </Form.Item>
+                </>
+            )}
+
+            <Form.Item>
+                <Button block type="primary" htmlType="submit">
+                    {emailSent ? '确认重置' : '获取验证码'}
+                </Button>
+                或 <NavLink to='/login'>返回登录</NavLink>
+            </Form.Item>
+        </Form>
+    );
+}
+
+export default Forget;    
\ No newline at end of file
diff --git a/src/feature/auth/Login.tsx b/src/feature/auth/Login.tsx
new file mode 100644
index 0000000..14bf6c8
--- /dev/null
+++ b/src/feature/auth/Login.tsx
@@ -0,0 +1,50 @@
+
+import { LockOutlined, MailOutlined } from '@ant-design/icons';
+import { Button, Checkbox, Form, Input, Flex } from 'antd';
+import { NavLink } from 'react-router';
+function Login() {
+    const onFinish = (values: unknown) => {
+        console.log('Received values of form: ', values);
+    };
+
+    return (
+        <Form
+            name="login"
+            initialValues={{ remember: true }}
+            style={{ maxWidth: 360 }}
+            onFinish={onFinish}
+        >
+            <h2>登录</h2>
+            <Form.Item
+                name="email"
+                rules={[{ required: true, message: '请输入你的账号(注册邮箱)!' }, { type: 'email', message: '请输入正确的邮箱' }]}
+            >
+                <Input prefix={<MailOutlined />} placeholder="账号(注册邮箱)" />
+            </Form.Item>
+            <Form.Item
+                name="password"
+                rules={[{ required: true, message: '请输入你的密码!' }]}
+            >
+                <Input prefix={<LockOutlined />} type="password" placeholder="密码" />
+            </Form.Item>
+            <Form.Item>
+                <Flex justify="space-between" align="center">
+                    <Form.Item name="remember" valuePropName="checked" noStyle>
+                        <Checkbox>自动登录</Checkbox>
+                    </Form.Item>
+                    <NavLink to='/forget'> 忘记密码 </NavLink>
+
+                </Flex>
+            </Form.Item>
+
+            <Form.Item>
+                <Button block type="primary" htmlType="submit">
+                    登录
+                </Button>
+                或 <NavLink to='/register'>注册</NavLink>
+            </Form.Item>
+        </Form>
+    );
+};
+
+export default Login;
\ No newline at end of file
diff --git a/src/feature/auth/Register.tsx b/src/feature/auth/Register.tsx
new file mode 100644
index 0000000..5b90c59
--- /dev/null
+++ b/src/feature/auth/Register.tsx
@@ -0,0 +1,149 @@
+import { useEffect, useState } from 'react';
+import { LockOutlined, MailOutlined, NumberOutlined, UserOutlined } from '@ant-design/icons';
+import { Button, Checkbox, Form, Input, Space } from 'antd';
+import { NavLink } from 'react-router';
+
+interface FormValues {
+    name: string;
+    email: string;
+    verifyCode: string;
+    password: string;
+    confirmPassword: string;
+    agreement: boolean;
+}
+
+function Register() {
+    const [countdown, setCountdown] = useState(0);
+    const [form] = Form.useForm<FormValues>();
+    const [emailStatues] = useState(false);
+
+    const sendVerificationCode = () => {
+        setCountdown(60)
+    }
+
+    // 发送表单倒计时
+    useEffect(() => {
+        if (countdown > 0) {
+            const timer = setTimeout(() => {
+                setCountdown(prev => prev - 1);
+            }, 1000);
+
+            return () => clearTimeout(timer);
+        }
+    }, [countdown]);
+
+
+    // 表单提交
+    const onFinish = (values: FormValues) => {
+        console.log('注册成功:', values);
+    };
+
+    return (
+        <Form
+            form={form}
+            style={{ maxWidth: 360 }}
+            onFinish={onFinish}
+            scrollToFirstError
+        >
+            <h2>注册</h2>
+
+            <Form.Item
+                name="name"
+                rules={[{ required: true, message: '请输入用户名' }]}
+            >
+                <Input prefix={<UserOutlined />} placeholder="请输入用户名" />
+            </Form.Item>
+
+            <Form.Item
+                name="email"
+
+                rules={[{ required: true, message: '请输入邮箱' }, { type: 'email', message: '邮箱格式错误' }]}
+            >
+                <Input
+                    prefix={<MailOutlined />}
+                    placeholder="请输入邮箱"
+                />
+            </Form.Item>
+
+            <Form.Item
+                name="verifyCode"
+                rules={[{ required: true, message: '请填写验证码' }]}
+            >
+                <Space direction="horizontal" size="small">
+                    <Input
+                        prefix={<NumberOutlined />}
+                        placeholder="请输入验证码"
+                        style={{ flex: 1 }}
+                    />
+                    <Button
+                        disabled={countdown > 0 || !emailStatues}
+                        onClick={sendVerificationCode}
+                        style={{ width: 120 }}
+                    >
+                        {countdown > 0 ? `${countdown}s后重试` : '发送验证码'}
+                    </Button>
+                </Space>
+            </Form.Item>
+
+            <Form.Item
+                name="password"
+                rules={[
+                    { required: true, message: '请输入密码' },
+                    { min: 6, message: '密码长度至少为6位' },
+                ]}
+            >
+                <Input.Password
+                    prefix={<LockOutlined />}
+                    placeholder="请输入密码"
+                />
+            </Form.Item>
+
+            <Form.Item
+                name="confirmPassword"
+                dependencies={['password']}
+                rules={[
+                    { required: true, message: '请确认密码' },
+                    ({ getFieldValue }) => ({
+                        validator(_, value) {
+                            if (!value || getFieldValue('password') === value) {
+                                return Promise.resolve();
+                            }
+                            return Promise.reject('两次输入的密码不一致');
+                        },
+                    }),
+                ]}
+            >
+                <Input.Password
+                    prefix={<LockOutlined />}
+                    placeholder="请确认密码"
+                />
+            </Form.Item>
+
+            <Form.Item
+                name="agreement"
+                valuePropName="checked"
+                rules={[
+                    {
+                        validator: (_, value) =>
+                            value ? Promise.resolve() : Promise.reject('请阅读并同意用户协议'),
+                    },
+                ]}
+            >
+                <Checkbox>我已阅读并同意用户协议:<a>《创驿平台用户协议》</a></Checkbox>
+            </Form.Item>
+
+            <Form.Item>
+                <Button
+                    type="primary"
+                    htmlType="submit"
+                    style={{ width: '100%' }}
+                >
+                    注册
+                </Button>
+                或 <NavLink to='/login'>返回登录</NavLink>
+            </Form.Item>
+        </Form>
+    );
+}
+
+export default Register;    
\ No newline at end of file
diff --git a/src/feature/auth/authSlice.ts b/src/feature/auth/authSlice.ts
new file mode 100644
index 0000000..01cd4e5
--- /dev/null
+++ b/src/feature/auth/authSlice.ts
@@ -0,0 +1,17 @@
+import { createSlice } from "@reduxjs/toolkit";
+import type { AuthState } from "../../store/types";
+
+
+const initialState: AuthState = {
+  token: '',
+  loading: false,
+  isAuth: false,
+}
+
+const authSlice = createSlice({
+  name: 'auth',
+  initialState,
+  reducers: {},
+});
+  
+export default authSlice.reducer;
\ No newline at end of file
diff --git a/src/feature/user/userSlice.ts b/src/feature/user/userSlice.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/feature/user/userSlice.ts
diff --git a/src/main.css b/src/main.css
new file mode 100644
index 0000000..923e71c
--- /dev/null
+++ b/src/main.css
@@ -0,0 +1,5 @@
+body, #root{
+    padding: 0;
+    margin: 0;
+    user-select: none;
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..e659723
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+
+// 状态管理 redux
+import { store } from '../src/store/store.ts';
+import { Provider } from 'react-redux';
+// 路由 react-router
+import routes from './routes.ts';
+import { RouterProvider } from 'react-router';
+// 组件库 ant
+import { ConfigProvider } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+
+import "./main.css"
+ReactDOM.createRoot(document.getElementById('root')!).render(
+  <React.StrictMode>
+    <Provider store={store}>
+      <ConfigProvider locale={zhCN}>
+        <RouterProvider router={routes} />
+      </ConfigProvider>
+    </Provider>
+  </React.StrictMode>
+);
\ No newline at end of file
diff --git a/src/routes.ts b/src/routes.ts
new file mode 100644
index 0000000..1356b1e
--- /dev/null
+++ b/src/routes.ts
@@ -0,0 +1,25 @@
+import { createBrowserRouter } from "react-router";
+import Home from "./feature/Home";
+import AuthLayout from "./feature/auth/AuthLayout";
+import Login from "./feature/auth/Login";
+import Register from "./feature/auth/Register";
+import AppLayout from "./AppLayout";
+import Forget from "./feature/auth/Forget";
+export default createBrowserRouter([{
+    Component: AppLayout,
+    children: [
+      {
+        path: "/",
+        Component: Home,
+      },
+      {
+          Component: AuthLayout,
+          children: [
+            { path: "login", Component: Login },
+            { path: "register", Component: Register },
+            { path: "forget", Component: Forget },
+          ],
+      }
+    ]
+}]);
+  
\ No newline at end of file
diff --git a/src/store/hooks.ts b/src/store/hooks.ts
new file mode 100644
index 0000000..25019ec
--- /dev/null
+++ b/src/store/hooks.ts
@@ -0,0 +1,6 @@
+import { useDispatch, useSelector, type TypedUseSelectorHook } from 'react-redux'
+import type { RootState, AppDispatch } from './store'
+
+// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
+export const useAppDispatch: () => AppDispatch = useDispatch
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
\ No newline at end of file
diff --git a/src/store/store.ts b/src/store/store.ts
new file mode 100644
index 0000000..ebf1408
--- /dev/null
+++ b/src/store/store.ts
@@ -0,0 +1,12 @@
+import { configureStore } from '@reduxjs/toolkit'
+
+export const store = configureStore({
+  reducer: {
+      
+  }
+})
+
+// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
+export type RootState = ReturnType<typeof store.getState>
+// 推断出类型: {posts: PostsState, comments: CommentsState, users: UsersState}
+export type AppDispatch = typeof store.dispatch
\ No newline at end of file
diff --git a/src/store/types.ts b/src/store/types.ts
new file mode 100644
index 0000000..372c089
--- /dev/null
+++ b/src/store/types.ts
@@ -0,0 +1,15 @@
+// 定义用户基本信息
+export interface User {
+    id: number;
+    username: string;
+    email: string;
+    role: string;
+    token: string;
+}
+
+// 定义认证状态
+export interface AuthState {
+    token: string;
+    loading: boolean;
+    isAuth: boolean;
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />