diff --git a/client-temp/.gitignore b/client-temp/.gitignore deleted file mode 100644 index fa39f5a..0000000 --- a/client-temp/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# dependencies -node_modules/ - -# production -/dist - -# misc -.DS_Store -.env* - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# editor -.vscode/ -.idea/ -*.swp diff --git a/client-temp/README.md b/client-temp/README.md deleted file mode 100644 index da98444..0000000 --- a/client-temp/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# 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, - }, -}) -``` diff --git a/client-temp/package.json b/client-temp/package.json deleted file mode 100644 index 60c0cc2..0000000 --- a/client-temp/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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": { - "@ant-design/icons": "^6.0.0", - "@types/react-router-dom": "^5.3.3", - "antd": "^5.26.1", - "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", - "unplugin-auto-import": "^19.3.0", - "unplugin-auto-import-antd": "0.0.2", - "vite": "^6.3.5" - } -} diff --git a/client-temp/postcss.config.js b/client-temp/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/client-temp/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/client-temp/src/App.tsx b/client-temp/src/App.tsx deleted file mode 100644 index b02e96d..0000000 --- a/client-temp/src/App.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Layout } from 'antd'; -import type { Priority, Todo } from './types/todo'; -import TodoModalForm from './components/TodoModalForm'; -import TodoHeader from './components/TodoHeader'; -import TodoListContent from './components/TodoListContent'; -import { getPriorityTagColor, getPriorityIcon, priorityLabels } from './utils/priorityUtils'; -import { useTodos } from './hooks/useTodos'; -import { Typography, Input, Button, Space } from 'antd'; - -const { Content } = Layout; -const { Text } = Typography; -const { Search } = Input; - -function App() { - const { - todos, - isModalOpen, - currentTodo, - modalTitle, - isEditing, - activeTab, - searchTerm, - setSearchTerm, - openAddModal, - closeModal, - handleEditClick, - handleModalSubmit, - handleToggle, - handleDelete, - handleTabChange, - filteredTodos, - } = useTodos(); - - return ( - - - - {/* Title */} -
- {/* Search and button */} -
- setSearchTerm(e.target.value)} - style={{ width: 300 }} - /> - -
-
- - - - {isModalOpen && ( - - )} -
-
- ); -} - -export default App; diff --git a/client-temp/src/api/auth.ts b/client-temp/src/api/auth.ts deleted file mode 100644 index 7586d12..0000000 --- a/client-temp/src/api/auth.ts +++ /dev/null @@ -1,13 +0,0 @@ -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; -}; \ No newline at end of file diff --git a/client-temp/src/api/todo.ts b/client-temp/src/api/todo.ts deleted file mode 100644 index 0c69457..0000000 --- a/client-temp/src/api/todo.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Todo } from '../types/todo'; -import { api } from './axios'; - -export const getTodos = async (): Promise => { - const response = await api.get('/todos'); - return response.data; -}; - -export const addTodo = async (title: string, description: string, priority: Todo['priority']): Promise => { - const response = await api.post('/todos', { title, description, priority }); - return response.data; -}; - -export const updateTodo = async (_id: string, title: string, description: string, priority: Todo['priority'], completed: boolean): Promise => { - const response = await api.put(`/todos/${_id}`, { title, description, priority, completed }); - return response.data; -}; - -export const toggleTodo = async (_id: string): Promise => { - const response = await api.patch(`/todos/${_id}/toggle`); - return response.data; -}; - -export const deleteTodo = async (_id: string): Promise => { - await api.delete(`/todos/${_id}`); -}; \ No newline at end of file diff --git a/client-temp/src/index.css b/client-temp/src/index.css deleted file mode 100644 index 5f82d2e..0000000 --- a/client-temp/src/index.css +++ /dev/null @@ -1,9 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - body { - @apply bg-gray-50; - } -} \ No newline at end of file diff --git a/client-temp/src/pages/Login.tsx b/client-temp/src/pages/Login.tsx deleted file mode 100644 index 22410e4..0000000 --- a/client-temp/src/pages/Login.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Form, Input, Button, Card, Space, Typography, Alert, Checkbox, Divider } from 'antd'; -import { UserOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons'; -import { api } from '../api/axios'; - -const { Title, Text, Link } = Typography; - -function Login() { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const navigate = useNavigate(); - - const onFinish = async (values: any) => { - setLoading(true); - setError(null); - try { - const response = await api.post('/auth/login', { - username: values.username, - password: values.password, - }); - localStorage.setItem('token', response.data.token); - localStorage.setItem('username', values.username); - navigate('/'); - } catch (err: any) { - const errorMessage = err.response?.data?.message || '登录失败,请检查用户名或密码。'; - setError(errorMessage); - } finally { - setLoading(false); - } - }; - - return ( -
- - -
- - Todo List - 轻松管理您的任务 -
- - {error && ( - setError(null)} - style={{ marginBottom: 24 }} - /> - )} - -
- - } - placeholder="账户" - size="large" - /> - - - - } - placeholder="密码" - size="large" - /> - - - -
- 自动登录 - - 忘记密码 - 注册新账号 - -
-
- - - - -
-
-
-
- ); -} - -export default Login; \ No newline at end of file diff --git a/client-temp/src/pages/Register.tsx b/client-temp/src/pages/Register.tsx deleted file mode 100644 index 749e846..0000000 --- a/client-temp/src/pages/Register.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Form, Input, Button, Card, Space, Typography, Alert } from 'antd'; -import { UserOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons'; -import { api } from '../api/axios'; - -const { Title, Text, Link } = Typography; - -function Register() { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const navigate = useNavigate(); - - const onFinish = async (values: any) => { - setLoading(true); - setError(null); - try { - await api.post('/auth/register', { - username: values.username, - password: values.password, - }); - navigate('/login'); - } catch (err: any) { - const errorMessage = err.response?.data?.message || '注册失败,用户名可能已被占用或服务器错误。'; - setError(errorMessage); - } finally { - setLoading(false); - } - }; - - return ( -
- - -
- - Todo List - 轻松管理您的任务 -
- - {error && ( - setError(null)} - style={{ marginBottom: 24 }} - /> - )} - -
- - } - placeholder="用户名" - size="large" - /> - - - - } - placeholder="密码" - size="large" - /> - - - ({ - validator(_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致!')); - }, - }), - ]} - > - } - placeholder="确认密码" - size="large" - /> - - - - - -
- 已有账号?去登录 -
-
-
-
-
- ); -} - -export default Register; \ No newline at end of file diff --git a/client-temp/src/types/todo.ts b/client-temp/src/types/todo.ts deleted file mode 100644 index 56f949e..0000000 --- a/client-temp/src/types/todo.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type Priority = 'low' | 'medium' | 'high'; - -export interface Todo { - _id: string; - title: string; - description: string; - completed: boolean; - priority: Priority; -} - -export const priorityColors: Record = { - low: 'green', - medium: 'orange', - high: 'red', -}; - -export const priorityLabels = { - low: '低', - medium: '中', - high: '高' -} as const; \ No newline at end of file diff --git a/client-temp/tailwind.config.js b/client-temp/tailwind.config.js deleted file mode 100644 index d37737f..0000000 --- a/client-temp/tailwind.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: {}, - }, - plugins: [], -} - diff --git a/client-temp/tsconfig.json b/client-temp/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/client-temp/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/client/.gitignore b/client/.gitignore index 4d29575..fa39f5a 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,23 +1,19 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage +node_modules/ # production -/build +/dist # misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +.env* +# logs npm-debug.log* yarn-debug.log* yarn-error.log* + +# editor +.vscode/ +.idea/ +*.swp diff --git a/client/README.md b/client/README.md index b87cb00..da98444 100644 --- a/client/README.md +++ b/client/README.md @@ -1,46 +1,54 @@ -# Getting Started with Create React App +# React + TypeScript + Vite -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. -## Available Scripts +Currently, two official plugins are available: -In the project directory, you can run: +- [@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 -### `npm start` +## Expanding the ESLint configuration -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: -The page will reload if you make edits.\ -You will also see any lint errors in the console. +```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, + }, + }, +}) +``` -### `npm test` +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: -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. +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' -### `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/). +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, + }, +}) +``` diff --git a/client-temp/auto-imports.d.ts b/client/auto-imports.d.ts similarity index 100% rename from client-temp/auto-imports.d.ts rename to client/auto-imports.d.ts diff --git a/client-temp/eslint.config.js b/client/eslint.config.js similarity index 100% rename from client-temp/eslint.config.js rename to client/eslint.config.js diff --git a/client-temp/index.html b/client/index.html similarity index 100% rename from client-temp/index.html rename to client/index.html diff --git a/client/package.json b/client/package.json index ac35a34..60c0cc2 100644 --- a/client/package.json +++ b/client/package.json @@ -1,56 +1,41 @@ { - "name": "client", - "version": "0.1.0", + "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": { - "@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", + "@ant-design/icons": "^6.0.0", "@types/react-router-dom": "^5.3.3", - "axios": "^1.9.0", - "bcryptjs": "^3.0.2", - "jsonwebtoken": "^9.0.2", + "antd": "^5.26.1", + "axios": "^1.10.0", "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" - ] + "react-router-dom": "^7.6.2" }, "devDependencies": { - "autoprefixer": "^10.4.19", - "postcss": "^8.4.38", - "tailwindcss": "^3.3.3" + "@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", + "unplugin-auto-import": "^19.3.0", + "unplugin-auto-import-antd": "0.0.2", + "vite": "^6.3.5" } } diff --git a/client/postcss.config.js b/client/postcss.config.js index 33ad091..2e7af2b 100644 --- a/client/postcss.config.js +++ b/client/postcss.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { plugins: { tailwindcss: {}, autoprefixer: {}, diff --git a/client/public/favicon.ico b/client/public/favicon.ico deleted file mode 100644 index a11777c..0000000 Binary files a/client/public/favicon.ico and /dev/null differ diff --git a/client/public/index.html b/client/public/index.html deleted file mode 100644 index aa069f2..0000000 --- a/client/public/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - React App - - - -
- - - diff --git a/client/public/logo192.png b/client/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/client/public/logo192.png and /dev/null differ diff --git a/client/public/logo512.png b/client/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/client/public/logo512.png and /dev/null differ diff --git a/client/public/manifest.json b/client/public/manifest.json deleted file mode 100644 index 080d6c7..0000000 --- a/client/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "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" -} diff --git a/client/public/robots.txt b/client/public/robots.txt deleted file mode 100644 index e9e57dc..0000000 --- a/client/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/client-temp/public/vite.svg b/client/public/vite.svg similarity index 100% rename from client-temp/public/vite.svg rename to client/public/vite.svg diff --git a/client-temp/src/App.App.tsx b/client/src/App.App.tsx similarity index 100% rename from client-temp/src/App.App.tsx rename to client/src/App.App.tsx diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/client/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.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); - } -} diff --git a/client/src/App.tsx b/client/src/App.tsx index c7e5a3b..b02e96d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,287 +1,88 @@ -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'; +import { Layout } from 'antd'; +import type { Priority, Todo } from './types/todo'; +import TodoModalForm from './components/TodoModalForm'; +import TodoHeader from './components/TodoHeader'; +import TodoListContent from './components/TodoListContent'; +import { getPriorityTagColor, getPriorityIcon, priorityLabels } from './utils/priorityUtils'; +import { useTodos } from './hooks/useTodos'; +import { Typography, Input, Button, Space } from 'antd'; + +const { Content } = Layout; +const { Text } = Typography; +const { Search } = Input; + +function App() { + const { + todos, + isModalOpen, + currentTodo, + modalTitle, + isEditing, + activeTab, + searchTerm, + setSearchTerm, + openAddModal, + closeModal, + handleEditClick, + handleModalSubmit, + handleToggle, + handleDelete, + handleTabChange, + filteredTodos, + } = useTodos(); -// 弹窗组件 -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 ( -
-
-

{title}

-
{content}
-
- - {onConfirm && } + + + + {/* Title */} +
+ {/* Search and button */} +
+ setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> + +
-
-
- ); -}; -// 随机头像颜色生成 -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; + + + {isModalOpen && ( + + )} + + + ); } -// 状态栏组件 -const StatusBar: React.FC<{ username: string; avatarColor: string; avatarText: string; onLogout: () => void }> = ({ username, avatarColor, avatarText, onLogout }) => ( -
-
- - {avatarText} - - {username} - -
-
-); - -const TodoApp: React.FC = () => { - const [todos, setTodos] = useState([]); - const [title, setTitle] = useState(''); - const [loading, setLoading] = useState(false); - const [editingId, setEditingId] = useState(null); - const [editTitle, setEditTitle] = useState(''); - const [modalOpen, setModalOpen] = useState(false); - const [modalAction, setModalAction] = useState void)>(null); - const [modalContent, setModalContent] = useState(''); - const [modalTitle, setModalTitle] = useState(''); - const [editModalOpen, setEditModalOpen] = useState(false); - const [editTodoId, setEditTodoId] = useState(null); - const [newTodoTitle, setNewTodoTitle] = useState(''); - const [newTodoPriority, setNewTodoPriority] = useState('medium'); - const [editingTodo, setEditingTodo] = useState(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 ( -
- {/* 状态栏 */} - - setModalOpen(false)} - onConfirm={modalAction ? () => { setModalOpen(false); modalAction(); } : undefined} - title={modalTitle} - content={modalContent} - /> - {/* 编辑弹窗 */} - {editModalOpen && ( -
-
-

编辑待办事项

-
- 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="标题" - /> -
-
- - -
-
-
- )} - {/* 主内容整体下移,宽度自适应 */} -
-

Todo List

-
-
- setNewTodoTitle(e.target.value)} - required - /> - -
- -
- {loading ? ( -
加载中...
- ) : ( -
    - {todos.map(todo => ( -
  • -
    - handleToggle(todo)} - className="accent-blue-500 w-5 h-5" - /> - {todo.title} -
    -
    - - -
    -
  • - ))} -
- )} -
-
- ); -}; - -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 ( - - - } /> - } /> - : } /> - - - ); -}; - -export default App; \ No newline at end of file +export default App; diff --git a/client-temp/src/api/axios.ts b/client/src/api/axios.ts similarity index 100% rename from client-temp/src/api/axios.ts rename to client/src/api/axios.ts diff --git a/client/src/api/todo.ts b/client/src/api/todo.ts index dabbfdf..0c69457 100644 --- a/client/src/api/todo.ts +++ b/client/src/api/todo.ts @@ -1,37 +1,26 @@ -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}` } : {}; -} +import type { Todo } from '../types/todo'; +import { api } from './axios'; export const getTodos = async (): Promise => { - const res = await axios.get(`${API_URL}/todos`, { headers: getAuthHeader() }); - return res.data; + const response = await api.get('/todos'); + return response.data; }; -export const createTodo = async (title: string, priority: Priority): Promise => { - const res = await axios.post( - `${API_URL}/todos`, - { title, priority }, - { headers: getAuthHeader() } - ); - return res.data; +export const addTodo = async (title: string, description: string, priority: Todo['priority']): Promise => { + const response = await api.post('/todos', { title, description, priority }); + return response.data; }; -export const updateTodo = async (id: string, updates: Partial): Promise => { - const res = await axios.put( - `${API_URL}/todos/${id}`, - updates, - { headers: getAuthHeader() } - ); - return res.data; +export const updateTodo = async (_id: string, title: string, description: string, priority: Todo['priority'], completed: boolean): Promise => { + const response = await api.put(`/todos/${_id}`, { title, description, priority, completed }); + return response.data; }; -export const deleteTodo = async (id: string): Promise => { - await axios.delete(`${API_URL}/todos/${id}`, { headers: getAuthHeader() }); +export const toggleTodo = async (_id: string): Promise => { + const response = await api.patch(`/todos/${_id}/toggle`); + return response.data; +}; + +export const deleteTodo = async (_id: string): Promise => { + await api.delete(`/todos/${_id}`); }; \ No newline at end of file diff --git a/client-temp/src/assets/react.svg b/client/src/assets/react.svg similarity index 100% rename from client-temp/src/assets/react.svg rename to client/src/assets/react.svg diff --git a/client-temp/src/components/AntDesignIcon.tsx b/client/src/components/AntDesignIcon.tsx similarity index 100% rename from client-temp/src/components/AntDesignIcon.tsx rename to client/src/components/AntDesignIcon.tsx diff --git a/client-temp/src/components/TodoHeader.tsx b/client/src/components/TodoHeader.tsx similarity index 100% rename from client-temp/src/components/TodoHeader.tsx rename to client/src/components/TodoHeader.tsx diff --git a/client-temp/src/components/TodoListContent.tsx b/client/src/components/TodoListContent.tsx similarity index 100% rename from client-temp/src/components/TodoListContent.tsx rename to client/src/components/TodoListContent.tsx diff --git a/client-temp/src/components/TodoModalForm.tsx b/client/src/components/TodoModalForm.tsx similarity index 100% rename from client-temp/src/components/TodoModalForm.tsx rename to client/src/components/TodoModalForm.tsx diff --git a/client-temp/src/hooks/useTodos.ts b/client/src/hooks/useTodos.ts similarity index 100% rename from client-temp/src/hooks/useTodos.ts rename to client/src/hooks/useTodos.ts diff --git a/client/src/index.css b/client/src/index.css index bd6213e..5f82d2e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,9 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +@layer base { + body { + @apply bg-gray-50; + } +} \ No newline at end of file diff --git a/client/src/index.tsx b/client/src/index.tsx deleted file mode 100644 index 1fd12b7..0000000 --- a/client/src/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -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( - - - -); diff --git a/client-temp/src/main.tsx b/client/src/main.tsx similarity index 100% rename from client-temp/src/main.tsx rename to client/src/main.tsx diff --git a/client-temp/src/pages/ForgotPassword.tsx b/client/src/pages/ForgotPassword.tsx similarity index 100% rename from client-temp/src/pages/ForgotPassword.tsx rename to client/src/pages/ForgotPassword.tsx diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 8613791..22410e4 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -1,64 +1,117 @@ -import React, { useState, useEffect } from 'react'; -import { login } from '../api/auth'; -import { useNavigate, Link } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Form, Input, Button, Card, Space, Typography, Alert, Checkbox, Divider } from 'antd'; +import { UserOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons'; +import { api } from '../api/axios'; -const Login: React.FC = () => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); +const { Title, Text, Link } = Typography; + +function Login() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); 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(''); + const onFinish = async (values: any) => { + setLoading(true); + setError(null); try { - const res = await login(username, password); - localStorage.setItem('token', res.token); - localStorage.setItem('username', res.username); - window.dispatchEvent(new Event('tokenChange')); + const response = await api.post('/auth/login', { + username: values.username, + password: values.password, + }); + localStorage.setItem('token', response.data.token); + localStorage.setItem('username', values.username); navigate('/'); } catch (err: any) { - setError(err.response?.data?.message || '登录失败'); + const errorMessage = err.response?.data?.message || '登录失败,请检查用户名或密码。'; + setError(errorMessage); + } finally { + setLoading(false); } }; return ( -
-
-

登录

-
- setUsername(e.target.value)} - required - /> - setPassword(e.target.value)} - required - /> - {error &&
{error}
} - -
-
- 没有账号?注册 -
-
+
+ + +
+ + Todo List + 轻松管理您的任务 +
+ + {error && ( + setError(null)} + style={{ marginBottom: 24 }} + /> + )} + +
+ + } + placeholder="账户" + size="large" + /> + + + + } + placeholder="密码" + size="large" + /> + + + +
+ 自动登录 + + 忘记密码 + 注册新账号 + +
+
+ + + + +
+
+
); -}; +} export default Login; \ No newline at end of file diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx index f75a28f..749e846 100644 --- a/client/src/pages/Register.tsx +++ b/client/src/pages/Register.tsx @@ -1,58 +1,131 @@ import React, { useState } from 'react'; -import { register } from '../api/auth'; -import { useNavigate, Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { Form, Input, Button, Card, Space, Typography, Alert } from 'antd'; +import { UserOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons'; +import { api } from '../api/axios'; -const Register: React.FC = () => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); +const { Title, Text, Link } = Typography; + +function Register() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const navigate = useNavigate(); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setSuccess(''); + const onFinish = async (values: any) => { + setLoading(true); + setError(null); try { - await register(username, password); - setSuccess('注册成功,请登录'); - setTimeout(() => navigate('/login'), 1000); + await api.post('/auth/register', { + username: values.username, + password: values.password, + }); + navigate('/login'); } catch (err: any) { - setError(err.response?.data?.message || '注册失败'); + const errorMessage = err.response?.data?.message || '注册失败,用户名可能已被占用或服务器错误。'; + setError(errorMessage); + } finally { + setLoading(false); } }; return ( -
-
-

注册

-
- setUsername(e.target.value)} - required - /> - setPassword(e.target.value)} - required - /> - {error &&
{error}
} - {success &&
{success}
} - -
-
- 已有账号?登录 -
-
+
+ + +
+ + Todo List + 轻松管理您的任务 +
+ + {error && ( + setError(null)} + style={{ marginBottom: 24 }} + /> + )} + +
+ + } + placeholder="用户名" + size="large" + /> + + + + } + placeholder="密码" + size="large" + /> + + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致!')); + }, + }), + ]} + > + } + placeholder="确认密码" + size="large" + /> + + + + + +
+ 已有账号?去登录 +
+
+
+
); -}; +} export default Register; \ No newline at end of file diff --git a/client/src/react-app-env.d.ts b/client/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5..0000000 --- a/client/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/client/src/types/todo.ts b/client/src/types/todo.ts index 081360a..56f949e 100644 --- a/client/src/types/todo.ts +++ b/client/src/types/todo.ts @@ -3,18 +3,16 @@ export type Priority = 'low' | 'medium' | 'high'; export interface Todo { _id: string; title: string; + description: 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 priorityColors: Record = { + low: 'green', + medium: 'orange', + high: 'red', +}; export const priorityLabels = { low: '低', diff --git a/client-temp/src/utils/priorityUtils.ts b/client/src/utils/priorityUtils.ts similarity index 100% rename from client-temp/src/utils/priorityUtils.ts rename to client/src/utils/priorityUtils.ts diff --git a/client-temp/src/vite-env.d.ts b/client/src/vite-env.d.ts similarity index 100% rename from client-temp/src/vite-env.d.ts rename to client/src/vite-env.d.ts diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 115da8e..d37737f 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -1,9 +1,12 @@ -module.exports = { +/** @type {import('tailwindcss').Config} */ +export default { content: [ - "./src/**/*.{js,jsx,ts,tsx}", + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], } + diff --git a/client-temp/tsconfig.app.json b/client/tsconfig.app.json similarity index 100% rename from client-temp/tsconfig.app.json rename to client/tsconfig.app.json diff --git a/client/tsconfig.json b/client/tsconfig.json index a273b0c..1ffef60 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,26 +1,7 @@ { - "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" + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } ] } diff --git a/client-temp/tsconfig.node.json b/client/tsconfig.node.json similarity index 100% rename from client-temp/tsconfig.node.json rename to client/tsconfig.node.json diff --git a/client-temp/vite.config.ts b/client/vite.config.ts similarity index 100% rename from client-temp/vite.config.ts rename to client/vite.config.ts