初始化
This commit is contained in:
		
						commit
						f1cf65e7a7
					
				
							
								
								
									
										19
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| # dependencies | ||||
| node_modules/ | ||||
| 
 | ||||
| # production | ||||
| /dist | ||||
| 
 | ||||
| # misc | ||||
| .DS_Store | ||||
| .env* | ||||
| 
 | ||||
| # logs | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| 
 | ||||
| # editor | ||||
| .vscode/ | ||||
| .idea/ | ||||
| *.swp | ||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| # TodoList 全栈项目 | ||||
| 
 | ||||
| 这是一个使用现代技术栈构建的 TodoList 全栈应用。 | ||||
| 
 | ||||
| ## 技术栈 | ||||
| 
 | ||||
| - 前端:React + TypeScript | ||||
| - 后端:Node.js + Express + TypeScript | ||||
| - 数据库:MongoDB | ||||
| - 部署:Docker + Docker Compose | ||||
| 
 | ||||
| ## 项目结构 | ||||
| 
 | ||||
| ``` | ||||
| full-stack/ | ||||
| ├── client/          # 前端代码 | ||||
| ├── server/          # 后端代码 | ||||
| ├── docker/          # Docker配置文件 | ||||
| └── README.md        # 项目说明文档 | ||||
| ``` | ||||
| 
 | ||||
| ## 开发环境要求 | ||||
| 
 | ||||
| - Node.js >= 16 | ||||
| - Docker & Docker Compose | ||||
| - MongoDB | ||||
| 
 | ||||
| ## 如何运行 | ||||
| 
 | ||||
| 1. 克隆项目 | ||||
| 2. 安装依赖 | ||||
|    ```bash | ||||
|    # 安装前端依赖 | ||||
|    cd client | ||||
|    npm install | ||||
| 
 | ||||
|    # 安装后端依赖 | ||||
|    cd ../server | ||||
|    npm install | ||||
|    ``` | ||||
| 3. 启动开发环境 | ||||
|    ```bash | ||||
|    # 使用 Docker Compose 启动所有服务 | ||||
|    docker-compose up | ||||
|    ``` | ||||
| 
 | ||||
| ## 功能特性 | ||||
| 
 | ||||
| - 创建、读取、更新、删除待办事项 | ||||
| - 标记待办事项为已完成 | ||||
| - 按状态筛选待办事项 | ||||
| - 响应式设计,支持移动端  | ||||
							
								
								
									
										19
									
								
								client-temp/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								client-temp/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| # dependencies | ||||
| node_modules/ | ||||
| 
 | ||||
| # production | ||||
| /dist | ||||
| 
 | ||||
| # misc | ||||
| .DS_Store | ||||
| .env* | ||||
| 
 | ||||
| # logs | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| 
 | ||||
| # editor | ||||
| .vscode/ | ||||
| .idea/ | ||||
| *.swp | ||||
							
								
								
									
										54
									
								
								client-temp/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								client-temp/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| # React + TypeScript + Vite | ||||
| 
 | ||||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||||
| 
 | ||||
| Currently, two official plugins are available: | ||||
| 
 | ||||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh | ||||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh | ||||
| 
 | ||||
| ## Expanding the ESLint configuration | ||||
| 
 | ||||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||||
| 
 | ||||
| ```js | ||||
| export default tseslint.config({ | ||||
|   extends: [ | ||||
|     // Remove ...tseslint.configs.recommended and replace with this | ||||
|     ...tseslint.configs.recommendedTypeChecked, | ||||
|     // Alternatively, use this for stricter rules | ||||
|     ...tseslint.configs.strictTypeChecked, | ||||
|     // Optionally, add this for stylistic rules | ||||
|     ...tseslint.configs.stylisticTypeChecked, | ||||
|   ], | ||||
|   languageOptions: { | ||||
|     // other options... | ||||
|     parserOptions: { | ||||
|       project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||||
|       tsconfigRootDir: import.meta.dirname, | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| ``` | ||||
| 
 | ||||
| You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: | ||||
| 
 | ||||
| ```js | ||||
| // eslint.config.js | ||||
| import reactX from 'eslint-plugin-react-x' | ||||
| import reactDom from 'eslint-plugin-react-dom' | ||||
| 
 | ||||
| export default tseslint.config({ | ||||
|   plugins: { | ||||
|     // Add the react-x and react-dom plugins | ||||
|     'react-x': reactX, | ||||
|     'react-dom': reactDom, | ||||
|   }, | ||||
|   rules: { | ||||
|     // other rules... | ||||
|     // Enable its recommended typescript rules | ||||
|     ...reactX.configs['recommended-typescript'].rules, | ||||
|     ...reactDom.configs.recommended.rules, | ||||
|   }, | ||||
| }) | ||||
| ``` | ||||
							
								
								
									
										28
									
								
								client-temp/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								client-temp/eslint.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| import js from '@eslint/js' | ||||
| import globals from 'globals' | ||||
| import reactHooks from 'eslint-plugin-react-hooks' | ||||
| import reactRefresh from 'eslint-plugin-react-refresh' | ||||
| import tseslint from 'typescript-eslint' | ||||
| 
 | ||||
| export default tseslint.config( | ||||
|   { ignores: ['dist'] }, | ||||
|   { | ||||
|     extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||||
|     files: ['**/*.{ts,tsx}'], | ||||
|     languageOptions: { | ||||
|       ecmaVersion: 2020, | ||||
|       globals: globals.browser, | ||||
|     }, | ||||
|     plugins: { | ||||
|       'react-hooks': reactHooks, | ||||
|       'react-refresh': reactRefresh, | ||||
|     }, | ||||
|     rules: { | ||||
|       ...reactHooks.configs.recommended.rules, | ||||
|       'react-refresh/only-export-components': [ | ||||
|         'warn', | ||||
|         { allowConstantExport: true }, | ||||
|       ], | ||||
|     }, | ||||
|   }, | ||||
| ) | ||||
							
								
								
									
										13
									
								
								client-temp/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								client-temp/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Vite + React + TS</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="root"></div> | ||||
|     <script type="module" src="/src/main.tsx"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										37
									
								
								client-temp/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								client-temp/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| { | ||||
|   "name": "client-temp", | ||||
|   "private": true, | ||||
|   "version": "0.0.0", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "tsc -b && vite build", | ||||
|     "build:dev": "tsc -b && vite build --mode development", | ||||
|     "build:prod": "tsc -b && vite build --mode production", | ||||
|     "lint": "eslint .", | ||||
|     "preview": "vite preview" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@types/react-router-dom": "^5.3.3", | ||||
|     "axios": "^1.10.0", | ||||
|     "react": "^19.1.0", | ||||
|     "react-dom": "^19.1.0", | ||||
|     "react-router-dom": "^7.6.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@eslint/js": "^9.25.0", | ||||
|     "@types/react": "^19.1.2", | ||||
|     "@types/react-dom": "^19.1.2", | ||||
|     "@vitejs/plugin-react-swc": "^3.9.0", | ||||
|     "autoprefixer": "^10.4.17", | ||||
|     "eslint": "^9.25.0", | ||||
|     "eslint-plugin-react-hooks": "^5.2.0", | ||||
|     "eslint-plugin-react-refresh": "^0.4.19", | ||||
|     "globals": "^16.0.0", | ||||
|     "postcss": "^8.4.35", | ||||
|     "tailwindcss": "^3.4.1", | ||||
|     "typescript": "~5.8.3", | ||||
|     "typescript-eslint": "^8.30.1", | ||||
|     "vite": "^6.3.5" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								client-temp/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								client-temp/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| export default { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
| } | ||||
							
								
								
									
										1
									
								
								client-temp/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client-temp/public/vite.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||||
| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										123
									
								
								client-temp/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								client-temp/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| import { useState, useEffect } from 'react'; | ||||
| import type { Todo } from './types/todo'; | ||||
| import { getTodos, addTodo, toggleTodo, deleteTodo } from './api/todo'; | ||||
| import { priorityColors } from './types/todo'; | ||||
| 
 | ||||
| function App() { | ||||
|   const [todos, setTodos] = useState<Todo[]>([]); | ||||
|   const [title, setTitle] = useState(''); | ||||
|   const [priority, setPriority] = useState<Todo['priority']>('medium'); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     loadTodos(); | ||||
|   }, []); | ||||
| 
 | ||||
|   const loadTodos = async () => { | ||||
|     try { | ||||
|       const data = await getTodos(); | ||||
|       setTodos(data); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load todos:', error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     if (!title.trim()) return; | ||||
| 
 | ||||
|     try { | ||||
|       const newTodo = await addTodo(title, priority); | ||||
|       setTodos([...todos, newTodo]); | ||||
|       setTitle(''); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to add todo:', error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleToggle = async (id: number) => { | ||||
|     try { | ||||
|       const updatedTodo = await toggleTodo(id); | ||||
|       setTodos(todos.map(todo => todo.id === id ? updatedTodo : todo)); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to toggle todo:', error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleDelete = async (id: number) => { | ||||
|     try { | ||||
|       await deleteTodo(id); | ||||
|       setTodos(todos.filter(todo => todo.id !== id)); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to delete todo:', error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="container mx-auto px-4 py-8"> | ||||
|       <h1 className="text-3xl font-bold mb-8">Todo List</h1> | ||||
|        | ||||
|       <form onSubmit={handleSubmit} className="mb-8"> | ||||
|         <div className="flex gap-4"> | ||||
|           <input | ||||
|             type="text" | ||||
|             value={title} | ||||
|             onChange={(e) => setTitle(e.target.value)} | ||||
|             placeholder="Add a new todo..." | ||||
|             className="flex-1 px-4 py-2 border rounded" | ||||
|           /> | ||||
|           <select | ||||
|             value={priority} | ||||
|             onChange={(e) => setPriority(e.target.value as Todo['priority'])} | ||||
|             className="px-4 py-2 border rounded" | ||||
|           > | ||||
|             <option value="low">Low</option> | ||||
|             <option value="medium">Medium</option> | ||||
|             <option value="high">High</option> | ||||
|           </select> | ||||
|           <button | ||||
|             type="submit" | ||||
|             className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" | ||||
|           > | ||||
|             Add | ||||
|           </button> | ||||
|         </div> | ||||
|       </form> | ||||
| 
 | ||||
|       <ul className="space-y-4"> | ||||
|         {todos.map((todo) => ( | ||||
|           <li | ||||
|             key={todo.id} | ||||
|             className="flex items-center justify-between p-4 border rounded" | ||||
|           > | ||||
|             <div className="flex items-center gap-4"> | ||||
|               <input | ||||
|                 type="checkbox" | ||||
|                 checked={todo.completed} | ||||
|                 onChange={() => handleToggle(todo.id)} | ||||
|                 className="w-5 h-5" | ||||
|               /> | ||||
|               <span | ||||
|                 className={`text-lg ${todo.completed ? 'line-through text-gray-500' : ''}`} | ||||
|               > | ||||
|                 {todo.title} | ||||
|               </span> | ||||
|               <span | ||||
|                 className={`px-2 py-1 rounded text-sm bg-${priorityColors[todo.priority]}-100 text-${priorityColors[todo.priority]}-800`} | ||||
|               > | ||||
|                 {todo.priority} | ||||
|               </span> | ||||
|             </div> | ||||
|             <button | ||||
|               onClick={() => handleDelete(todo.id)} | ||||
|               className="px-3 py-1 text-red-500 hover:text-red-600" | ||||
|             > | ||||
|               Delete | ||||
|             </button> | ||||
|           </li> | ||||
|         ))} | ||||
|       </ul> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export default App; | ||||
							
								
								
									
										13
									
								
								client-temp/src/api/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								client-temp/src/api/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5050/api/auth'; | ||||
| 
 | ||||
| export const register = async (username: string, password: string) => { | ||||
|   const res = await axios.post(`${API_URL}/register`, { username, password }); | ||||
|   return res.data; | ||||
| }; | ||||
| 
 | ||||
| export const login = async (username: string, password: string) => { | ||||
|   const res = await axios.post(`${API_URL}/login`, { username, password }); | ||||
|   return res.data; | ||||
| };  | ||||
							
								
								
									
										15
									
								
								client-temp/src/api/axios.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								client-temp/src/api/axios.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| export const api = axios.create({ | ||||
|   baseURL: import.meta.env.VITE_API_URL, | ||||
| }); | ||||
| 
 | ||||
| api.interceptors.response.use( | ||||
|   response => response, | ||||
|   error => { | ||||
|     if (error.response && error.response.status === 401) { | ||||
|       window.location.href = '/login'; | ||||
|     } | ||||
|     return Promise.reject(error); | ||||
|   } | ||||
| );  | ||||
							
								
								
									
										21
									
								
								client-temp/src/api/todo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								client-temp/src/api/todo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| import type { Todo } from '../types/todo'; | ||||
| import { api } from './axios'; | ||||
| 
 | ||||
| export const getTodos = async (): Promise<Todo[]> => { | ||||
|   const response = await api.get('/api/todos'); | ||||
|   return response.data; | ||||
| }; | ||||
| 
 | ||||
| export const addTodo = async (title: string, priority: Todo['priority']): Promise<Todo> => { | ||||
|   const response = await api.post('/api/todos', { title, priority }); | ||||
|   return response.data; | ||||
| }; | ||||
| 
 | ||||
| export const toggleTodo = async (id: number): Promise<Todo> => { | ||||
|   const response = await api.patch(`/api/todos/${id}/toggle`); | ||||
|   return response.data; | ||||
| }; | ||||
| 
 | ||||
| export const deleteTodo = async (id: number): Promise<void> => { | ||||
|   await api.delete(`/api/todos/${id}`); | ||||
| };  | ||||
							
								
								
									
										1
									
								
								client-temp/src/assets/react.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client-temp/src/assets/react.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> | ||||
| After Width: | Height: | Size: 4.0 KiB | 
							
								
								
									
										1
									
								
								client-temp/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client-temp/src/index.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
|   | ||||
							
								
								
									
										10
									
								
								client-temp/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								client-temp/src/main.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import { StrictMode } from 'react' | ||||
| import { createRoot } from 'react-dom/client' | ||||
| import './index.css' | ||||
| import App from './App.tsx' | ||||
| 
 | ||||
| createRoot(document.getElementById('root')!).render( | ||||
|   <StrictMode> | ||||
|     <App /> | ||||
|   </StrictMode>, | ||||
| ) | ||||
							
								
								
									
										64
									
								
								client-temp/src/pages/Login.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								client-temp/src/pages/Login.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { login } from '../api/auth'; | ||||
| import { useNavigate, Link } from 'react-router-dom'; | ||||
| 
 | ||||
| const Login: React.FC = () => { | ||||
|   const [username, setUsername] = useState(''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [error, setError] = useState(''); | ||||
|   const navigate = useNavigate(); | ||||
|   const [isLogin, setIsLogin] = useState(!!localStorage.getItem('token')); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const onStorage = () => setIsLogin(!!localStorage.getItem('token')); | ||||
|     window.addEventListener('storage', onStorage); | ||||
|     return () => window.removeEventListener('storage', onStorage); | ||||
|   }, []); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setError(''); | ||||
|     try { | ||||
|       const res = await login(username, password); | ||||
|       localStorage.setItem('token', res.token); | ||||
|       localStorage.setItem('username', res.username); | ||||
|       window.dispatchEvent(new Event('tokenChange')); | ||||
|       navigate('/'); | ||||
|     } catch (err: any) { | ||||
|       setError(err.response?.data?.message || '登录失败'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center py-4 sm:py-10 px-1 sm:px-2"> | ||||
|       <div className="w-full max-w-xs sm:max-w-md bg-white rounded-lg sm:rounded-xl shadow-lg p-4 sm:p-8"> | ||||
|         <h2 className="text-2xl sm:text-3xl font-bold text-center mb-4 sm:mb-6 text-blue-600 tracking-tight">登录</h2> | ||||
|         <form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:gap-4 mb-2"> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="text" | ||||
|             placeholder="用户名" | ||||
|             value={username} | ||||
|             onChange={e => setUsername(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="password" | ||||
|             placeholder="密码" | ||||
|             value={password} | ||||
|             onChange={e => setPassword(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           {error && <div className="text-red-500 text-xs sm:text-sm text-center">{error}</div>} | ||||
|           <button className="bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition text-sm sm:text-base" type="submit">登录</button> | ||||
|         </form> | ||||
|         <div className="text-xs sm:text-sm text-center mt-3 sm:mt-4"> | ||||
|           没有账号?<Link to="/register" className="text-blue-500 hover:underline">注册</Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Login;  | ||||
							
								
								
									
										58
									
								
								client-temp/src/pages/Register.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								client-temp/src/pages/Register.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import { register } from '../api/auth'; | ||||
| import { useNavigate, Link } from 'react-router-dom'; | ||||
| 
 | ||||
| const Register: React.FC = () => { | ||||
|   const [username, setUsername] = useState(''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [error, setError] = useState(''); | ||||
|   const [success, setSuccess] = useState(''); | ||||
|   const navigate = useNavigate(); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setError(''); | ||||
|     setSuccess(''); | ||||
|     try { | ||||
|       await register(username, password); | ||||
|       setSuccess('注册成功,请登录'); | ||||
|       setTimeout(() => navigate('/login'), 1000); | ||||
|     } catch (err: any) { | ||||
|       setError(err.response?.data?.message || '注册失败'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center py-4 sm:py-10 px-1 sm:px-2"> | ||||
|       <div className="w-full max-w-xs sm:max-w-md bg-white rounded-lg sm:rounded-xl shadow-lg p-4 sm:p-8"> | ||||
|         <h2 className="text-2xl sm:text-3xl font-bold text-center mb-4 sm:mb-6 text-blue-600 tracking-tight">注册</h2> | ||||
|         <form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:gap-4 mb-2"> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="text" | ||||
|             placeholder="用户名" | ||||
|             value={username} | ||||
|             onChange={e => setUsername(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="password" | ||||
|             placeholder="密码" | ||||
|             value={password} | ||||
|             onChange={e => setPassword(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           {error && <div className="text-red-500 text-xs sm:text-sm text-center">{error}</div>} | ||||
|           {success && <div className="text-green-500 text-xs sm:text-sm text-center">{success}</div>} | ||||
|           <button className="bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition text-sm sm:text-base" type="submit">注册</button> | ||||
|         </form> | ||||
|         <div className="text-xs sm:text-sm text-center mt-3 sm:mt-4"> | ||||
|           已有账号?<Link to="/login" className="text-blue-500 hover:underline">登录</Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Register;  | ||||
							
								
								
									
										20
									
								
								client-temp/src/types/todo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								client-temp/src/types/todo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| export type Priority = 'low' | 'medium' | 'high'; | ||||
| 
 | ||||
| export interface Todo { | ||||
|   id: number; | ||||
|   title: string; | ||||
|   completed: boolean; | ||||
|   priority: Priority; | ||||
| } | ||||
| 
 | ||||
| export const priorityColors: Record<Priority, string> = { | ||||
|   low: 'blue', | ||||
|   medium: 'yellow', | ||||
|   high: 'red', | ||||
| }; | ||||
| 
 | ||||
| export const priorityLabels = { | ||||
|   low: '低', | ||||
|   medium: '中', | ||||
|   high: '高' | ||||
| } as const;  | ||||
							
								
								
									
										1
									
								
								client-temp/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client-temp/src/vite-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" />
 | ||||
							
								
								
									
										12
									
								
								client-temp/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								client-temp/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| /** @type {import('tailwindcss').Config} */ | ||||
| export default { | ||||
|   content: [ | ||||
|     "./index.html", | ||||
|     "./src/**/*.{js,ts,jsx,tsx}", | ||||
|   ], | ||||
|   theme: { | ||||
|     extend: {}, | ||||
|   }, | ||||
|   plugins: [], | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										27
									
								
								client-temp/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								client-temp/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||
|     "target": "ES2020", | ||||
|     "useDefineForClassFields": true, | ||||
|     "lib": ["ES2020", "DOM", "DOM.Iterable"], | ||||
|     "module": "ESNext", | ||||
|     "skipLibCheck": true, | ||||
| 
 | ||||
|     /* Bundler mode */ | ||||
|     "moduleResolution": "bundler", | ||||
|     "allowImportingTsExtensions": true, | ||||
|     "verbatimModuleSyntax": true, | ||||
|     "moduleDetection": "force", | ||||
|     "noEmit": true, | ||||
|     "jsx": "react-jsx", | ||||
| 
 | ||||
|     /* Linting */ | ||||
|     "strict": true, | ||||
|     "noUnusedLocals": true, | ||||
|     "noUnusedParameters": true, | ||||
|     "erasableSyntaxOnly": true, | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     "noUncheckedSideEffectImports": true | ||||
|   }, | ||||
|   "include": ["src"] | ||||
| } | ||||
							
								
								
									
										7
									
								
								client-temp/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								client-temp/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| { | ||||
|   "files": [], | ||||
|   "references": [ | ||||
|     { "path": "./tsconfig.app.json" }, | ||||
|     { "path": "./tsconfig.node.json" } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										25
									
								
								client-temp/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								client-temp/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||||
|     "target": "ES2022", | ||||
|     "lib": ["ES2023"], | ||||
|     "module": "ESNext", | ||||
|     "skipLibCheck": true, | ||||
| 
 | ||||
|     /* Bundler mode */ | ||||
|     "moduleResolution": "bundler", | ||||
|     "allowImportingTsExtensions": true, | ||||
|     "verbatimModuleSyntax": true, | ||||
|     "moduleDetection": "force", | ||||
|     "noEmit": true, | ||||
| 
 | ||||
|     /* Linting */ | ||||
|     "strict": true, | ||||
|     "noUnusedLocals": true, | ||||
|     "noUnusedParameters": true, | ||||
|     "erasableSyntaxOnly": true, | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     "noUncheckedSideEffectImports": true | ||||
|   }, | ||||
|   "include": ["vite.config.ts"] | ||||
| } | ||||
							
								
								
									
										22
									
								
								client-temp/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								client-temp/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| import { defineConfig } from 'vite' | ||||
| import react from '@vitejs/plugin-react-swc' | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| export default defineConfig({ | ||||
|   plugins: [react()], | ||||
|   server: { | ||||
|     port: 5173, | ||||
|     proxy: { | ||||
|       '/api': { | ||||
|         target: process.env.VITE_API_URL, | ||||
|         changeOrigin: true, | ||||
|         rewrite: (path) => path.replace(/^\/api/, ''), | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   build: { | ||||
|     outDir: 'dist', | ||||
|     sourcemap: process.env.VITE_NODE_ENV === 'development', | ||||
|     minify: process.env.VITE_NODE_ENV === 'production', | ||||
|   }, | ||||
| }) | ||||
							
								
								
									
										23
									
								
								client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								client/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||
| 
 | ||||
| # dependencies | ||||
| /node_modules | ||||
| /.pnp | ||||
| .pnp.js | ||||
| 
 | ||||
| # testing | ||||
| /coverage | ||||
| 
 | ||||
| # production | ||||
| /build | ||||
| 
 | ||||
| # misc | ||||
| .DS_Store | ||||
| .env.local | ||||
| .env.development.local | ||||
| .env.test.local | ||||
| .env.production.local | ||||
| 
 | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
							
								
								
									
										46
									
								
								client/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								client/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| # Getting Started with Create React App | ||||
| 
 | ||||
| This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). | ||||
| 
 | ||||
| ## Available Scripts | ||||
| 
 | ||||
| In the project directory, you can run: | ||||
| 
 | ||||
| ### `npm start` | ||||
| 
 | ||||
| Runs the app in the development mode.\ | ||||
| Open [http://localhost:3000](http://localhost:3000) to view it in the browser. | ||||
| 
 | ||||
| The page will reload if you make edits.\ | ||||
| You will also see any lint errors in the console. | ||||
| 
 | ||||
| ### `npm test` | ||||
| 
 | ||||
| Launches the test runner in the interactive watch mode.\ | ||||
| See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. | ||||
| 
 | ||||
| ### `npm run build` | ||||
| 
 | ||||
| Builds the app for production to the `build` folder.\ | ||||
| It correctly bundles React in production mode and optimizes the build for the best performance. | ||||
| 
 | ||||
| The build is minified and the filenames include the hashes.\ | ||||
| Your app is ready to be deployed! | ||||
| 
 | ||||
| See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. | ||||
| 
 | ||||
| ### `npm run eject` | ||||
| 
 | ||||
| **Note: this is a one-way operation. Once you `eject`, you can’t go back!** | ||||
| 
 | ||||
| If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. | ||||
| 
 | ||||
| Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. | ||||
| 
 | ||||
| You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. | ||||
| 
 | ||||
| ## Learn More | ||||
| 
 | ||||
| You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). | ||||
| 
 | ||||
| To learn React, check out the [React documentation](https://reactjs.org/). | ||||
							
								
								
									
										56
									
								
								client/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								client/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| { | ||||
|   "name": "client", | ||||
|   "version": "0.1.0", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@testing-library/dom": "^10.4.0", | ||||
|     "@testing-library/jest-dom": "^6.6.3", | ||||
|     "@testing-library/react": "^16.3.0", | ||||
|     "@testing-library/user-event": "^13.5.0", | ||||
|     "@types/bcryptjs": "^3.0.0", | ||||
|     "@types/jest": "^27.5.2", | ||||
|     "@types/jsonwebtoken": "^9.0.9", | ||||
|     "@types/node": "^16.18.126", | ||||
|     "@types/react": "^19.1.8", | ||||
|     "@types/react-dom": "^19.1.6", | ||||
|     "@types/react-router-dom": "^5.3.3", | ||||
|     "axios": "^1.9.0", | ||||
|     "bcryptjs": "^3.0.2", | ||||
|     "jsonwebtoken": "^9.0.2", | ||||
|     "react": "^19.1.0", | ||||
|     "react-dom": "^19.1.0", | ||||
|     "react-router-dom": "^7.6.2", | ||||
|     "react-scripts": "5.0.1", | ||||
|     "typescript": "^4.9.5", | ||||
|     "web-vitals": "^2.1.4" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "react-scripts start", | ||||
|     "build": "react-scripts build", | ||||
|     "test": "react-scripts test", | ||||
|     "eject": "react-scripts eject" | ||||
|   }, | ||||
|   "eslintConfig": { | ||||
|     "extends": [ | ||||
|       "react-app", | ||||
|       "react-app/jest" | ||||
|     ] | ||||
|   }, | ||||
|   "browserslist": { | ||||
|     "production": [ | ||||
|       ">0.2%", | ||||
|       "not dead", | ||||
|       "not op_mini all" | ||||
|     ], | ||||
|     "development": [ | ||||
|       "last 1 chrome version", | ||||
|       "last 1 firefox version", | ||||
|       "last 1 safari version" | ||||
|     ] | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "autoprefixer": "^10.4.19", | ||||
|     "postcss": "^8.4.38", | ||||
|     "tailwindcss": "^3.3.3" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								client/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								client/postcss.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| module.exports = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								client/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								client/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										43
									
								
								client/public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								client/public/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <meta name="theme-color" content="#000000" /> | ||||
|     <meta | ||||
|       name="description" | ||||
|       content="Web site created using create-react-app" | ||||
|     /> | ||||
|     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | ||||
|     <!-- | ||||
|       manifest.json provides metadata used when your web app is installed on a | ||||
|       user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | ||||
|     --> | ||||
|     <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | ||||
|     <!-- | ||||
|       Notice the use of %PUBLIC_URL% in the tags above. | ||||
|       It will be replaced with the URL of the `public` folder during the build. | ||||
|       Only files inside the `public` folder can be referenced from the HTML. | ||||
| 
 | ||||
|       Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | ||||
|       work correctly both with client-side routing and a non-root public URL. | ||||
|       Learn how to configure a non-root public URL by running `npm run build`. | ||||
|     --> | ||||
|     <title>React App</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <noscript>You need to enable JavaScript to run this app.</noscript> | ||||
|     <div id="root"></div> | ||||
|     <!-- | ||||
|       This HTML file is a template. | ||||
|       If you open it directly in the browser, you will see an empty page. | ||||
| 
 | ||||
|       You can add webfonts, meta tags, or analytics to this file. | ||||
|       The build step will place the bundled scripts into the <body> tag. | ||||
| 
 | ||||
|       To begin the development, run `npm start` or `yarn start`. | ||||
|       To create a production bundle, use `npm run build` or `yarn build`. | ||||
|     --> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										
											BIN
										
									
								
								client/public/logo192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								client/public/logo192.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								client/public/logo512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								client/public/logo512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 9.4 KiB | 
							
								
								
									
										25
									
								
								client/public/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								client/public/manifest.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| { | ||||
|   "short_name": "React App", | ||||
|   "name": "Create React App Sample", | ||||
|   "icons": [ | ||||
|     { | ||||
|       "src": "favicon.ico", | ||||
|       "sizes": "64x64 32x32 24x24 16x16", | ||||
|       "type": "image/x-icon" | ||||
|     }, | ||||
|     { | ||||
|       "src": "logo192.png", | ||||
|       "type": "image/png", | ||||
|       "sizes": "192x192" | ||||
|     }, | ||||
|     { | ||||
|       "src": "logo512.png", | ||||
|       "type": "image/png", | ||||
|       "sizes": "512x512" | ||||
|     } | ||||
|   ], | ||||
|   "start_url": ".", | ||||
|   "display": "standalone", | ||||
|   "theme_color": "#000000", | ||||
|   "background_color": "#ffffff" | ||||
| } | ||||
							
								
								
									
										3
									
								
								client/public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/public/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| # https://www.robotstxt.org/robotstxt.html | ||||
| User-agent: * | ||||
| Disallow: | ||||
							
								
								
									
										38
									
								
								client/src/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								client/src/App.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| .App { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .App-logo { | ||||
|   height: 40vmin; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| @media (prefers-reduced-motion: no-preference) { | ||||
|   .App-logo { | ||||
|     animation: App-logo-spin infinite 20s linear; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .App-header { | ||||
|   background-color: #282c34; | ||||
|   min-height: 100vh; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   font-size: calc(10px + 2vmin); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .App-link { | ||||
|   color: #61dafb; | ||||
| } | ||||
| 
 | ||||
| @keyframes App-logo-spin { | ||||
|   from { | ||||
|     transform: rotate(0deg); | ||||
|   } | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										287
									
								
								client/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								client/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,287 @@ | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom'; | ||||
| import { Todo, Priority, priorityColors, priorityLabels } from './types/todo'; | ||||
| import * as todoApi from './api/todo'; | ||||
| import Login from './pages/Login'; | ||||
| import Register from './pages/Register'; | ||||
| 
 | ||||
| // 弹窗组件
 | ||||
| const Modal: React.FC<{ open: boolean; onClose: () => void; onConfirm?: () => void; title: string; content: string; confirmText?: string; cancelText?: string; }> = ({ open, onClose, onConfirm, title, content, confirmText = '确定', cancelText = '取消' }) => { | ||||
|   if (!open) return null; | ||||
|   return ( | ||||
|     <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-30 px-2"> | ||||
|       <div className="bg-white rounded-lg sm:rounded-xl shadow-lg p-4 sm:p-6 w-full max-w-xs sm:max-w-sm"> | ||||
|         <h3 className="text-base sm:text-lg font-bold mb-2 text-gray-800">{title}</h3> | ||||
|         <div className="mb-4 text-gray-600 text-sm sm:text-base">{content}</div> | ||||
|         <div className="flex justify-end gap-2"> | ||||
|           <button className="px-3 sm:px-4 py-1.5 sm:py-1 rounded bg-gray-200 text-gray-700 hover:bg-gray-300 text-xs sm:text-sm" onClick={onClose}>{cancelText}</button> | ||||
|           {onConfirm && <button className="px-3 sm:px-4 py-1.5 sm:py-1 rounded bg-blue-500 text-white hover:bg-blue-600 text-xs sm:text-sm" onClick={onConfirm}>{confirmText}</button>} | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| // 随机头像颜色生成
 | ||||
| function stringToColor(str: string) { | ||||
|   let hash = 0; | ||||
|   for (let i = 0; i < str.length; i++) { | ||||
|     hash = str.charCodeAt(i) + ((hash << 5) - hash); | ||||
|   } | ||||
|   const color = `hsl(${hash % 360}, 70%, 60%)`; | ||||
|   return color; | ||||
| } | ||||
| 
 | ||||
| // 状态栏组件
 | ||||
| const StatusBar: React.FC<{ username: string; avatarColor: string; avatarText: string; onLogout: () => void }> = ({ username, avatarColor, avatarText, onLogout }) => ( | ||||
|   <div className="fixed top-0 left-0 w-full h-14 sm:h-16 bg-white shadow flex items-center justify-end pr-4 sm:pr-8 z-50"> | ||||
|     <div className="flex items-center gap-2 sm:gap-3"> | ||||
|       <span | ||||
|         className="w-9 h-9 sm:w-10 sm:h-10 rounded-full flex items-center justify-center font-bold text-white text-lg sm:text-xl shadow" | ||||
|         style={{ background: avatarColor }} | ||||
|       > | ||||
|         {avatarText} | ||||
|       </span> | ||||
|       <span className="text-sm sm:text-base font-medium text-gray-700 mr-1 sm:mr-2">{username}</span> | ||||
|       <button | ||||
|         className="text-xs sm:text-sm text-gray-500 bg-gray-200 px-3 sm:px-4 py-1.5 sm:py-2 rounded hover:bg-gray-300 transition font-semibold" | ||||
|         onClick={onLogout} | ||||
|       > | ||||
|         退出登录 | ||||
|       </button> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| const TodoApp: React.FC = () => { | ||||
|   const [todos, setTodos] = useState<Todo[]>([]); | ||||
|   const [title, setTitle] = useState(''); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [editingId, setEditingId] = useState<string | null>(null); | ||||
|   const [editTitle, setEditTitle] = useState(''); | ||||
|   const [modalOpen, setModalOpen] = useState(false); | ||||
|   const [modalAction, setModalAction] = useState<null | (() => void)>(null); | ||||
|   const [modalContent, setModalContent] = useState(''); | ||||
|   const [modalTitle, setModalTitle] = useState(''); | ||||
|   const [editModalOpen, setEditModalOpen] = useState(false); | ||||
|   const [editTodoId, setEditTodoId] = useState<string | null>(null); | ||||
|   const [newTodoTitle, setNewTodoTitle] = useState(''); | ||||
|   const [newTodoPriority, setNewTodoPriority] = useState<Priority>('medium'); | ||||
|   const [editingTodo, setEditingTodo] = useState<Todo | null>(null); | ||||
|   const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); | ||||
|   const navigate = useNavigate(); | ||||
| 
 | ||||
|   // 获取所有 Todo
 | ||||
|   const fetchTodos = async () => { | ||||
|     setLoading(true); | ||||
|     try { | ||||
|       const data = await todoApi.getTodos(); | ||||
|       setTodos(data); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to fetch todos:', error); | ||||
|       if ((error as any)?.response?.status === 401) { | ||||
|         navigate('/login'); | ||||
|       } | ||||
|     } | ||||
|     setLoading(false); | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     fetchTodos(); | ||||
|   }, []); | ||||
| 
 | ||||
|   // 新增 Todo
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     if (!newTodoTitle.trim()) return; | ||||
| 
 | ||||
|     try { | ||||
|       const todo = await todoApi.createTodo(newTodoTitle.trim(), newTodoPriority); | ||||
|       setTodos([...todos, todo]); | ||||
|       setNewTodoTitle(''); | ||||
|       setNewTodoPriority('medium'); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to create todo:', error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // 删除 Todo
 | ||||
|   const handleDelete = async (id: string) => { | ||||
|     if (!window.confirm('确定要删除吗?')) return; | ||||
|     try { | ||||
|       await todoApi.deleteTodo(id); | ||||
|       setTodos(todos.filter(todo => todo._id !== id)); | ||||
|     } catch { | ||||
|       alert('删除失败'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // 标记完成/未完成
 | ||||
|   const handleToggle = async (todo: Todo) => { | ||||
|     try { | ||||
|       const updatedTodo = await todoApi.updateTodo(todo._id!, { completed: !todo.completed }); | ||||
|       setTodos(todos.map(t => t._id === todo._id ? updatedTodo : t)); | ||||
|     } catch { | ||||
|       alert('更新失败'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // 开始编辑
 | ||||
|   const handleEdit = (todo: Todo) => { | ||||
|     setEditingId(todo._id!); | ||||
|     setEditTitle(todo.title); | ||||
|     setEditTodoId(todo._id!); | ||||
|     setEditingTodo(todo); | ||||
|     setEditModalOpen(true); | ||||
|   }; | ||||
| 
 | ||||
|   // 保存编辑
 | ||||
|   const handleEditSave = async (id: string) => { | ||||
|     try { | ||||
|       const updatedTodo = await todoApi.updateTodo(id, { | ||||
|         title: editingTodo?.title || '', | ||||
|         priority: editingTodo?.priority || 'medium' | ||||
|       }); | ||||
|       setTodos(todos.map(t => t._id === id ? updatedTodo : t)); | ||||
|       setEditingTodo(null); | ||||
|       setEditModalOpen(false); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to update todo:', error); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // 取消编辑
 | ||||
|   const handleEditCancel = () => { | ||||
|     setEditingId(null); | ||||
|     setEditModalOpen(false); | ||||
|     setEditTodoId(null); | ||||
|   }; | ||||
| 
 | ||||
|   // 退出登录
 | ||||
|   const handleLogout = () => { | ||||
|     setModalTitle('退出登录'); | ||||
|     setModalContent('确定要退出登录吗?'); | ||||
|     setModalAction(() => () => { | ||||
|       localStorage.removeItem('token'); | ||||
|       localStorage.removeItem('username'); | ||||
|       window.dispatchEvent(new Event('tokenChange')); | ||||
|       navigate('/login'); | ||||
|     }); | ||||
|     setModalOpen(true); | ||||
|   }; | ||||
| 
 | ||||
|   const username = localStorage.getItem('username') || ''; | ||||
|   const avatarColor = stringToColor(username); | ||||
|   const avatarText = username ? username[0].toUpperCase() : '?'; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-gray-100 flex flex-col items-center py-4 sm:py-10 px-1 sm:px-2"> | ||||
|       {/* 状态栏 */} | ||||
|       <StatusBar username={username} avatarColor={avatarColor} avatarText={avatarText} onLogout={handleLogout} /> | ||||
|       <Modal | ||||
|         open={modalOpen} | ||||
|         onClose={() => setModalOpen(false)} | ||||
|         onConfirm={modalAction ? () => { setModalOpen(false); modalAction(); } : undefined} | ||||
|         title={modalTitle} | ||||
|         content={modalContent} | ||||
|       /> | ||||
|       {/* 编辑弹窗 */} | ||||
|       {editModalOpen && ( | ||||
|         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-30 px-2"> | ||||
|           <div className="bg-white rounded-lg sm:rounded-xl shadow-lg p-4 sm:p-8 w-full max-w-xs sm:max-w-md"> | ||||
|             <h3 className="text-base sm:text-xl font-bold mb-4 text-gray-800">编辑待办事项</h3> | ||||
|             <div className="flex flex-col gap-2 sm:gap-3 mb-4"> | ||||
|               <input | ||||
|                 value={editTitle} | ||||
|                 onChange={e => setEditTitle(e.target.value)} | ||||
|                 className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|                 placeholder="标题" | ||||
|               /> | ||||
|             </div> | ||||
|             <div className="flex justify-end gap-2"> | ||||
|               <button className="bg-green-500 text-white px-4 sm:px-5 py-2 rounded hover:bg-green-600 text-xs sm:text-base" onClick={() => handleEditSave(editTodoId!)}>保存</button> | ||||
|               <button className="bg-gray-300 text-gray-700 px-4 sm:px-5 py-2 rounded hover:bg-gray-400 text-xs sm:text-base" onClick={handleEditCancel}>取消</button> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|       {/* 主内容整体下移,宽度自适应 */} | ||||
|       <div className="w-full max-w-xs sm:max-w-2xl bg-white rounded-lg sm:rounded-xl shadow-lg p-3 sm:p-6 relative mt-16 sm:mt-24"> | ||||
|         <h2 className="text-2xl sm:text-3xl font-bold text-center mb-4 sm:mb-6 text-blue-600 tracking-tight">Todo List</h2> | ||||
|         <form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:gap-3 mb-6 sm:mb-8"> | ||||
|           <div className="flex space-x-4"> | ||||
|             <input | ||||
|               className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|               type="text" | ||||
|               placeholder="标题" | ||||
|               value={newTodoTitle} | ||||
|               onChange={e => setNewTodoTitle(e.target.value)} | ||||
|               required | ||||
|             /> | ||||
|             <select | ||||
|               value={newTodoPriority} | ||||
|               onChange={(e) => setNewTodoPriority(e.target.value as Priority)} | ||||
|               className="p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             > | ||||
|               {Object.entries(priorityLabels).map(([value, label]) => ( | ||||
|                 <option key={value} value={value}>{label}优先级</option> | ||||
|               ))} | ||||
|             </select> | ||||
|           </div> | ||||
|           <button className="bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition text-sm sm:text-base" type="submit">添加</button> | ||||
|         </form> | ||||
|         {loading ? ( | ||||
|           <div className="text-center text-gray-500 text-sm sm:text-base">加载中...</div> | ||||
|         ) : ( | ||||
|           <ul className="space-y-2 sm:space-y-3"> | ||||
|             {todos.map(todo => ( | ||||
|               <li key={todo._id} className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3 bg-gray-50 rounded-lg shadow p-2 sm:p-3"> | ||||
|                 <div className="flex items-center gap-2 sm:gap-3"> | ||||
|                   <input | ||||
|                     type="checkbox" | ||||
|                     checked={todo.completed} | ||||
|                     onChange={() => handleToggle(todo)} | ||||
|                     className="accent-blue-500 w-5 h-5" | ||||
|                   /> | ||||
|                   <span className={`flex-1 ${todo.completed ? 'line-through text-gray-400' : 'text-gray-900'} font-medium text-sm sm:text-base`}>{todo.title}</span> | ||||
|                 </div> | ||||
|                 <div className="flex gap-2 mt-1 sm:mt-0"> | ||||
|                   <button className="bg-yellow-400 text-white px-2 sm:px-3 py-1 rounded hover:bg-yellow-500 text-xs sm:text-base" onClick={() => handleEdit(todo)}>编辑</button> | ||||
|                   <button className="bg-red-500 text-white px-2 sm:px-3 py-1 rounded hover:bg-red-600 text-xs sm:text-base" onClick={() => handleDelete(todo._id!)}>删除</button> | ||||
|                 </div> | ||||
|               </li> | ||||
|             ))} | ||||
|           </ul> | ||||
|         )} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const App: React.FC = () => { | ||||
|   const [isLogin, setIsLogin] = useState(!!localStorage.getItem('token')); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const onStorage = () => setIsLogin(!!localStorage.getItem('token')); | ||||
|     window.addEventListener('storage', onStorage); | ||||
|     // 兼容同窗口内的 token 变化
 | ||||
|     const onTokenChange = () => setIsLogin(!!localStorage.getItem('token')); | ||||
|     window.addEventListener('tokenChange', onTokenChange); | ||||
|     return () => { | ||||
|       window.removeEventListener('storage', onStorage); | ||||
|       window.removeEventListener('tokenChange', onTokenChange); | ||||
|     }; | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <Router> | ||||
|       <Routes> | ||||
|         <Route path="/login" element={<Login />} /> | ||||
|         <Route path="/register" element={<Register />} /> | ||||
|         <Route path="/" element={isLogin ? <TodoApp /> : <Navigate to="/login" />} /> | ||||
|       </Routes> | ||||
|     </Router> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default App;  | ||||
							
								
								
									
										13
									
								
								client/src/api/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								client/src/api/auth.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5050/api/auth'; | ||||
| 
 | ||||
| export const register = async (username: string, password: string) => { | ||||
|   const res = await axios.post(`${API_URL}/register`, { username, password }); | ||||
|   return res.data; | ||||
| }; | ||||
| 
 | ||||
| export const login = async (username: string, password: string) => { | ||||
|   const res = await axios.post(`${API_URL}/login`, { username, password }); | ||||
|   return res.data; | ||||
| };  | ||||
							
								
								
									
										37
									
								
								client/src/api/todo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								client/src/api/todo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| import axios from 'axios'; | ||||
| import { Todo, Priority } from '../types/todo'; | ||||
| 
 | ||||
| const API_URL = 'http://localhost:5050/api'; | ||||
| 
 | ||||
| // 获取 token 辅助函数
 | ||||
| function getAuthHeader() { | ||||
|   const token = localStorage.getItem('token'); | ||||
|   return token ? { Authorization: `Bearer ${token}` } : {}; | ||||
| } | ||||
| 
 | ||||
| export const getTodos = async (): Promise<Todo[]> => { | ||||
|   const res = await axios.get<Todo[]>(`${API_URL}/todos`, { headers: getAuthHeader() }); | ||||
|   return res.data; | ||||
| }; | ||||
| 
 | ||||
| export const createTodo = async (title: string, priority: Priority): Promise<Todo> => { | ||||
|   const res = await axios.post( | ||||
|     `${API_URL}/todos`, | ||||
|     { title, priority }, | ||||
|     { headers: getAuthHeader() } | ||||
|   ); | ||||
|   return res.data; | ||||
| }; | ||||
| 
 | ||||
| export const updateTodo = async (id: string, updates: Partial<Todo>): Promise<Todo> => { | ||||
|   const res = await axios.put( | ||||
|     `${API_URL}/todos/${id}`, | ||||
|     updates, | ||||
|     { headers: getAuthHeader() } | ||||
|   ); | ||||
|   return res.data; | ||||
| }; | ||||
| 
 | ||||
| export const deleteTodo = async (id: string): Promise<void> => { | ||||
|   await axios.delete(`${API_URL}/todos/${id}`, { headers: getAuthHeader() }); | ||||
| };  | ||||
							
								
								
									
										3
									
								
								client/src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/src/index.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
							
								
								
									
										13
									
								
								client/src/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								client/src/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom/client'; | ||||
| import './index.css'; | ||||
| import App from './App'; | ||||
| 
 | ||||
| const root = ReactDOM.createRoot( | ||||
|   document.getElementById('root') as HTMLElement | ||||
| ); | ||||
| root.render( | ||||
|   <React.StrictMode> | ||||
|     <App /> | ||||
|   </React.StrictMode> | ||||
| ); | ||||
							
								
								
									
										64
									
								
								client/src/pages/Login.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								client/src/pages/Login.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| import React, { useState, useEffect } from 'react'; | ||||
| import { login } from '../api/auth'; | ||||
| import { useNavigate, Link } from 'react-router-dom'; | ||||
| 
 | ||||
| const Login: React.FC = () => { | ||||
|   const [username, setUsername] = useState(''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [error, setError] = useState(''); | ||||
|   const navigate = useNavigate(); | ||||
|   const [isLogin, setIsLogin] = useState(!!localStorage.getItem('token')); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const onStorage = () => setIsLogin(!!localStorage.getItem('token')); | ||||
|     window.addEventListener('storage', onStorage); | ||||
|     return () => window.removeEventListener('storage', onStorage); | ||||
|   }, []); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setError(''); | ||||
|     try { | ||||
|       const res = await login(username, password); | ||||
|       localStorage.setItem('token', res.token); | ||||
|       localStorage.setItem('username', res.username); | ||||
|       window.dispatchEvent(new Event('tokenChange')); | ||||
|       navigate('/'); | ||||
|     } catch (err: any) { | ||||
|       setError(err.response?.data?.message || '登录失败'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center py-4 sm:py-10 px-1 sm:px-2"> | ||||
|       <div className="w-full max-w-xs sm:max-w-md bg-white rounded-lg sm:rounded-xl shadow-lg p-4 sm:p-8"> | ||||
|         <h2 className="text-2xl sm:text-3xl font-bold text-center mb-4 sm:mb-6 text-blue-600 tracking-tight">登录</h2> | ||||
|         <form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:gap-4 mb-2"> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="text" | ||||
|             placeholder="用户名" | ||||
|             value={username} | ||||
|             onChange={e => setUsername(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="password" | ||||
|             placeholder="密码" | ||||
|             value={password} | ||||
|             onChange={e => setPassword(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           {error && <div className="text-red-500 text-xs sm:text-sm text-center">{error}</div>} | ||||
|           <button className="bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition text-sm sm:text-base" type="submit">登录</button> | ||||
|         </form> | ||||
|         <div className="text-xs sm:text-sm text-center mt-3 sm:mt-4"> | ||||
|           没有账号?<Link to="/register" className="text-blue-500 hover:underline">注册</Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Login;  | ||||
							
								
								
									
										58
									
								
								client/src/pages/Register.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								client/src/pages/Register.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| import React, { useState } from 'react'; | ||||
| import { register } from '../api/auth'; | ||||
| import { useNavigate, Link } from 'react-router-dom'; | ||||
| 
 | ||||
| const Register: React.FC = () => { | ||||
|   const [username, setUsername] = useState(''); | ||||
|   const [password, setPassword] = useState(''); | ||||
|   const [error, setError] = useState(''); | ||||
|   const [success, setSuccess] = useState(''); | ||||
|   const navigate = useNavigate(); | ||||
| 
 | ||||
|   const handleSubmit = async (e: React.FormEvent) => { | ||||
|     e.preventDefault(); | ||||
|     setError(''); | ||||
|     setSuccess(''); | ||||
|     try { | ||||
|       await register(username, password); | ||||
|       setSuccess('注册成功,请登录'); | ||||
|       setTimeout(() => navigate('/login'), 1000); | ||||
|     } catch (err: any) { | ||||
|       setError(err.response?.data?.message || '注册失败'); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center py-4 sm:py-10 px-1 sm:px-2"> | ||||
|       <div className="w-full max-w-xs sm:max-w-md bg-white rounded-lg sm:rounded-xl shadow-lg p-4 sm:p-8"> | ||||
|         <h2 className="text-2xl sm:text-3xl font-bold text-center mb-4 sm:mb-6 text-blue-600 tracking-tight">注册</h2> | ||||
|         <form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:gap-4 mb-2"> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="text" | ||||
|             placeholder="用户名" | ||||
|             value={username} | ||||
|             onChange={e => setUsername(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           <input | ||||
|             className="border rounded px-2 sm:px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 text-sm sm:text-base" | ||||
|             type="password" | ||||
|             placeholder="密码" | ||||
|             value={password} | ||||
|             onChange={e => setPassword(e.target.value)} | ||||
|             required | ||||
|           /> | ||||
|           {error && <div className="text-red-500 text-xs sm:text-sm text-center">{error}</div>} | ||||
|           {success && <div className="text-green-500 text-xs sm:text-sm text-center">{success}</div>} | ||||
|           <button className="bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition text-sm sm:text-base" type="submit">注册</button> | ||||
|         </form> | ||||
|         <div className="text-xs sm:text-sm text-center mt-3 sm:mt-4"> | ||||
|           已有账号?<Link to="/login" className="text-blue-500 hover:underline">登录</Link> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Register;  | ||||
							
								
								
									
										1
									
								
								client/src/react-app-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								client/src/react-app-env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| /// <reference types="react-scripts" />
 | ||||
							
								
								
									
										23
									
								
								client/src/types/todo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								client/src/types/todo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| export type Priority = 'low' | 'medium' | 'high'; | ||||
| 
 | ||||
| export interface Todo { | ||||
|   _id: string; | ||||
|   title: string; | ||||
|   completed: boolean; | ||||
|   priority: Priority; | ||||
|   userId: string; | ||||
|   createdAt: string; | ||||
|   updatedAt: string; | ||||
| } | ||||
| 
 | ||||
| export const priorityColors = { | ||||
|   low: 'bg-blue-100 text-blue-800', | ||||
|   medium: 'bg-yellow-100 text-yellow-800', | ||||
|   high: 'bg-red-100 text-red-800' | ||||
| } as const; | ||||
| 
 | ||||
| export const priorityLabels = { | ||||
|   low: '低', | ||||
|   medium: '中', | ||||
|   high: '高' | ||||
| } as const;  | ||||
							
								
								
									
										9
									
								
								client/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								client/tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| module.exports = { | ||||
|   content: [ | ||||
|     "./src/**/*.{js,jsx,ts,tsx}", | ||||
|   ], | ||||
|   theme: { | ||||
|     extend: {}, | ||||
|   }, | ||||
|   plugins: [], | ||||
| } | ||||
							
								
								
									
										26
									
								
								client/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								client/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es5", | ||||
|     "lib": [ | ||||
|       "dom", | ||||
|       "dom.iterable", | ||||
|       "esnext" | ||||
|     ], | ||||
|     "allowJs": true, | ||||
|     "skipLibCheck": true, | ||||
|     "esModuleInterop": true, | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "strict": true, | ||||
|     "forceConsistentCasingInFileNames": true, | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     "module": "esnext", | ||||
|     "moduleResolution": "node", | ||||
|     "resolveJsonModule": true, | ||||
|     "isolatedModules": true, | ||||
|     "noEmit": true, | ||||
|     "jsx": "react-jsx" | ||||
|   }, | ||||
|   "include": [ | ||||
|     "src" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										42
									
								
								server/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| { | ||||
|   "name": "server", | ||||
|   "version": "1.0.0", | ||||
|   "description": "TodoList 后端服务", | ||||
|   "main": "dist/app.js", | ||||
|   "scripts": { | ||||
|     "start": "node dist/app.js", | ||||
|     "dev": "ts-node-dev --respawn --transpile-only src/app.ts", | ||||
|     "build": "tsc", | ||||
|     "test": "echo \"Error: no test specified\" && exit 1" | ||||
|   }, | ||||
|   "keywords": [], | ||||
|   "author": "", | ||||
|   "license": "ISC", | ||||
|   "dependencies": { | ||||
|     "@types/bcryptjs": "^3.0.0", | ||||
|     "@types/cors": "^2.8.19", | ||||
|     "@types/express": "^5.0.3", | ||||
|     "@types/jsonwebtoken": "^9.0.9", | ||||
|     "@types/mongoose": "^5.11.97", | ||||
|     "@types/node": "^24.0.1", | ||||
|     "bcryptjs": "^3.0.2", | ||||
|     "cors": "^2.8.5", | ||||
|     "dotenv": "^16.5.0", | ||||
|     "express": "^5.1.0", | ||||
|     "jsonwebtoken": "^9.0.2", | ||||
|     "mongoose": "^8.15.1", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "typescript": "^5.8.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/cors": "^2.8.19", | ||||
|     "@types/express": "^5.0.3", | ||||
|     "@types/node": "^24.0.1", | ||||
|     "autoprefixer": "^10.4.21", | ||||
|     "postcss": "^8.5.5", | ||||
|     "tailwindcss": "^4.1.10", | ||||
|     "ts-node": "^10.9.2", | ||||
|     "ts-node-dev": "^2.0.0", | ||||
|     "typescript": "^5.8.3" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								server/src/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/src/app.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| import express from 'express'; | ||||
| import cors from 'cors'; | ||||
| import dotenv from 'dotenv'; | ||||
| import connectDB from './config/db'; | ||||
| import todoRoutes from './routes/todoRoutes'; | ||||
| import authRoutes from './routes/authRoutes'; | ||||
| 
 | ||||
| dotenv.config(); | ||||
| 
 | ||||
| const app = express(); | ||||
| 
 | ||||
| // 连接数据库
 | ||||
| connectDB(); | ||||
| 
 | ||||
| // 中间件
 | ||||
| app.use(cors({ | ||||
|   origin: 'http://localhost:5173', | ||||
|   credentials: true | ||||
| })); | ||||
| app.use(express.json()); | ||||
| 
 | ||||
| // 路由
 | ||||
| app.use('/api/todos', todoRoutes); | ||||
| app.use('/api/auth', authRoutes); | ||||
| 
 | ||||
| // 错误处理中间件
 | ||||
| app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { | ||||
|   console.error(err.stack); | ||||
|   res.status(500).json({ message: '服务器内部错误' }); | ||||
| }); | ||||
| 
 | ||||
| const PORT = process.env.PORT || 5050; | ||||
| 
 | ||||
| app.listen(PORT, () => { | ||||
|   console.log(`服务器运行在端口 ${PORT}`); | ||||
| });  | ||||
							
								
								
									
										16
									
								
								server/src/config/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/src/config/db.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| import mongoose from 'mongoose'; | ||||
| import dotenv from 'dotenv'; | ||||
| 
 | ||||
| dotenv.config(); | ||||
| 
 | ||||
| const connectDB = async () => { | ||||
|   try { | ||||
|     const conn = await mongoose.connect(process.env.MONGODB_URI as string); | ||||
|     console.log(`MongoDB 连接成功: ${conn.connection.host}`); | ||||
|   } catch (error) { | ||||
|     console.error('MongoDB 连接失败:', error); | ||||
|     process.exit(1); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export default connectDB;  | ||||
							
								
								
									
										42
									
								
								server/src/controllers/authController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								server/src/controllers/authController.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| import { Request, Response } from 'express'; | ||||
| import User, { IUser } from '../models/User'; | ||||
| import bcrypt from 'bcryptjs'; | ||||
| import jwt from 'jsonwebtoken'; | ||||
| 
 | ||||
| const JWT_SECRET = process.env.JWT_SECRET || 'secret_key'; | ||||
| 
 | ||||
| // 用户注册
 | ||||
| export const register = async (req: Request, res: Response) => { | ||||
|   const { username, password } = req.body; | ||||
|   try { | ||||
|     const existingUser = await User.findOne({ username }); | ||||
|     if (existingUser) { | ||||
|       return res.status(400).json({ message: '用户名已存在' }); | ||||
|     } | ||||
|     const hashedPassword = await bcrypt.hash(password, 10); | ||||
|     const user = new User({ username, password: hashedPassword }); | ||||
|     await user.save(); | ||||
|     res.status(201).json({ message: '注册成功' }); | ||||
|   } catch (error) { | ||||
|     res.status(500).json({ message: '注册失败', error }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 用户登录
 | ||||
| export const login = async (req: Request, res: Response) => { | ||||
|   const { username, password } = req.body; | ||||
|   try { | ||||
|     const user = await User.findOne({ username }); | ||||
|     if (!user) { | ||||
|       return res.status(400).json({ message: '用户不存在' }); | ||||
|     } | ||||
|     const isMatch = await bcrypt.compare(password, user.password); | ||||
|     if (!isMatch) { | ||||
|       return res.status(400).json({ message: '密码错误' }); | ||||
|     } | ||||
|     const token = jwt.sign({ userId: user._id, username: user.username }, JWT_SECRET, { expiresIn: '7d' }); | ||||
|     res.status(200).json({ token, username: user.username }); | ||||
|   } catch (error) { | ||||
|     res.status(500).json({ message: '登录失败', error }); | ||||
|   } | ||||
| };  | ||||
							
								
								
									
										83
									
								
								server/src/controllers/todoController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								server/src/controllers/todoController.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import { Request, Response, NextFunction } from 'express'; | ||||
| import Todo, { ITodo } from '../models/Todo'; | ||||
| import jwt from 'jsonwebtoken'; | ||||
| 
 | ||||
| const JWT_SECRET = process.env.JWT_SECRET || 'secret_key'; | ||||
| 
 | ||||
| // 鉴权中间件,解析 token 并注入 req.user
 | ||||
| export const authMiddleware = (req: any, res: Response, next: NextFunction): void => { | ||||
|   const authHeader = req.headers.authorization; | ||||
|   if (!authHeader || !authHeader.startsWith('Bearer ')) { | ||||
|     res.status(401).json({ message: '未授权' }); | ||||
|     return; | ||||
|   } | ||||
|   const token = authHeader.split(' ')[1]; | ||||
|   try { | ||||
|     const decoded = jwt.verify(token, JWT_SECRET) as any; | ||||
|     req.user = decoded; | ||||
|     next(); | ||||
|   } catch (err) { | ||||
|     res.status(401).json({ message: '无效的 token' }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 获取所有待办事项(只返回当前用户的)
 | ||||
| export const getAllTodos = async (req: Request & { user?: any }, res: Response) => { | ||||
|   try { | ||||
|     const todos = await Todo.find({ user: req.user.userId }).sort({ createdAt: -1 }); | ||||
|     res.status(200).json(todos); | ||||
|   } catch (error) { | ||||
|     res.status(500).json({ message: '获取待办事项失败', error }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 创建新的待办事项(自动绑定 user)
 | ||||
| export const createTodo = async (req: Request & { user?: any }, res: Response) => { | ||||
|   try { | ||||
|     const { title, priority = 'medium' } = req.body; | ||||
|     const todo = new Todo({ | ||||
|       title, | ||||
|       priority, | ||||
|       user: req.user.userId | ||||
|     }); | ||||
|     const savedTodo = await todo.save(); | ||||
|     res.status(201).json(savedTodo); | ||||
|   } catch (error) { | ||||
|     res.status(400).json({ message: '创建待办事项失败', error }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 更新待办事项
 | ||||
| export const updateTodo = async (req: Request & { user?: any }, res: Response) => { | ||||
|   try { | ||||
|     const todo = await Todo.findOne({ _id: req.params.id, user: req.user.userId }); | ||||
|     if (!todo) { | ||||
|       return res.status(404).json({ message: '待办事项不存在' }); | ||||
|     } | ||||
| 
 | ||||
|     const updatedTodo = await Todo.findByIdAndUpdate( | ||||
|       req.params.id, | ||||
|       { ...req.body }, | ||||
|       { new: true, runValidators: true } | ||||
|     ); | ||||
|      | ||||
|     res.status(200).json(updatedTodo); | ||||
|   } catch (error) { | ||||
|     res.status(400).json({ message: '更新待办事项失败', error }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 删除待办事项
 | ||||
| export const deleteTodo = async (req: Request & { user?: any }, res: Response) => { | ||||
|   try { | ||||
|     const todo = await Todo.findOne({ _id: req.params.id, user: req.user.userId }); | ||||
|     if (!todo) { | ||||
|       return res.status(404).json({ message: '待办事项不存在' }); | ||||
|     } | ||||
| 
 | ||||
|     await Todo.findByIdAndDelete(req.params.id); | ||||
|     res.status(200).json({ message: '待办事项已删除' }); | ||||
|   } catch (error) { | ||||
|     res.status(400).json({ message: '删除待办事项失败', error }); | ||||
|   } | ||||
| };  | ||||
							
								
								
									
										44
									
								
								server/src/models/Todo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								server/src/models/Todo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| import mongoose, { Schema, Document } from 'mongoose'; | ||||
| 
 | ||||
| export interface ITodo extends Document { | ||||
|   title: string; | ||||
|   description: string; | ||||
|   completed: boolean; | ||||
|   priority: 'low' | 'medium' | 'high'; | ||||
|   createdAt: Date; | ||||
|   updatedAt: Date; | ||||
|   user: mongoose.Types.ObjectId; | ||||
| } | ||||
| 
 | ||||
| const TodoSchema: Schema = new Schema({ | ||||
|   title: { | ||||
|     type: String, | ||||
|     required: [true, '标题是必需的'], | ||||
|     trim: true, | ||||
|     maxlength: [100, '标题不能超过100个字符'] | ||||
|   }, | ||||
|   description: { | ||||
|     type: String, | ||||
|     trim: true, | ||||
|     maxlength: [500, '描述不能超过500个字符'] | ||||
|   }, | ||||
|   completed: { | ||||
|     type: Boolean, | ||||
|     default: false | ||||
|   }, | ||||
|   priority: { | ||||
|     type: String, | ||||
|     enum: ['low', 'medium', 'high'], | ||||
|     default: 'medium', | ||||
|     required: true | ||||
|   }, | ||||
|   user: { | ||||
|     type: mongoose.Schema.Types.ObjectId, | ||||
|     ref: 'User', | ||||
|     required: true | ||||
|   } | ||||
| }, { | ||||
|   timestamps: true | ||||
| }); | ||||
| 
 | ||||
| export default mongoose.model<ITodo>('Todo', TodoSchema);  | ||||
							
								
								
									
										13
									
								
								server/src/models/User.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/src/models/User.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import mongoose, { Schema, Document } from 'mongoose'; | ||||
| 
 | ||||
| export interface IUser extends Document { | ||||
|   username: string; | ||||
|   password: string; | ||||
| } | ||||
| 
 | ||||
| const UserSchema: Schema = new Schema({ | ||||
|   username: { type: String, required: true, unique: true }, | ||||
|   password: { type: String, required: true } | ||||
| }); | ||||
| 
 | ||||
| export default mongoose.model<IUser>('User', UserSchema);  | ||||
							
								
								
									
										9
									
								
								server/src/routes/authRoutes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/src/routes/authRoutes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| import { Router } from 'express'; | ||||
| import { register, login } from '../controllers/authController'; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.post('/register', register); | ||||
| router.post('/login', login); | ||||
| 
 | ||||
| export default router;  | ||||
							
								
								
									
										17
									
								
								server/src/routes/todoRoutes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/src/routes/todoRoutes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| import { Router } from 'express'; | ||||
| import { | ||||
|   getAllTodos, | ||||
|   createTodo, | ||||
|   updateTodo, | ||||
|   deleteTodo, | ||||
|   authMiddleware | ||||
| } from '../controllers/todoController'; | ||||
| 
 | ||||
| const router = Router(); | ||||
| 
 | ||||
| router.get('/', authMiddleware, getAllTodos); | ||||
| router.post('/', authMiddleware, createTodo); | ||||
| router.put('/:id', authMiddleware, updateTodo); | ||||
| router.delete('/:id', authMiddleware, deleteTodo); | ||||
| 
 | ||||
| export default router;  | ||||
							
								
								
									
										14
									
								
								server/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es6", | ||||
|     "module": "commonjs", | ||||
|     "outDir": "./dist", | ||||
|     "rootDir": "./src", | ||||
|     "strict": true, | ||||
|     "esModuleInterop": true, | ||||
|     "skipLibCheck": true, | ||||
|     "forceConsistentCasingInFileNames": true | ||||
|   }, | ||||
|   "include": ["src/**/*"], | ||||
|   "exclude": ["node_modules"] | ||||
| }  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 梁泽军
						梁泽军