commit f1cf65e7a773430f2db25c90a79fd80a4bf20364
Author: 梁泽军 <5654792+tcubic21@user.noreply.gitee.com>
Date: Mon Jun 16 18:12:26 2025 +0800
初始化
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fa39f5a
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a930d44
--- /dev/null
+++ b/README.md
@@ -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
+ ```
+
+## 功能特性
+
+- 创建、读取、更新、删除待办事项
+- 标记待办事项为已完成
+- 按状态筛选待办事项
+- 响应式设计,支持移动端
\ No newline at end of file
diff --git a/client-temp/.gitignore b/client-temp/.gitignore
new file mode 100644
index 0000000..fa39f5a
--- /dev/null
+++ b/client-temp/.gitignore
@@ -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
diff --git a/client-temp/README.md b/client-temp/README.md
new file mode 100644
index 0000000..da98444
--- /dev/null
+++ b/client-temp/README.md
@@ -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,
+ },
+})
+```
diff --git a/client-temp/eslint.config.js b/client-temp/eslint.config.js
new file mode 100644
index 0000000..092408a
--- /dev/null
+++ b/client-temp/eslint.config.js
@@ -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 },
+ ],
+ },
+ },
+)
diff --git a/client-temp/index.html b/client-temp/index.html
new file mode 100644
index 0000000..e4b78ea
--- /dev/null
+++ b/client-temp/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React + TS
+
+
+
+
+
+
diff --git a/client-temp/package.json b/client-temp/package.json
new file mode 100644
index 0000000..126a608
--- /dev/null
+++ b/client-temp/package.json
@@ -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"
+ }
+}
diff --git a/client-temp/postcss.config.js b/client-temp/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/client-temp/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/client-temp/public/vite.svg b/client-temp/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/client-temp/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client-temp/src/App.tsx b/client-temp/src/App.tsx
new file mode 100644
index 0000000..6b795f5
--- /dev/null
+++ b/client-temp/src/App.tsx
@@ -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([]);
+ const [title, setTitle] = useState('');
+ const [priority, setPriority] = useState('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 (
+
+ );
+}
+
+export default App;
diff --git a/client-temp/src/api/auth.ts b/client-temp/src/api/auth.ts
new file mode 100644
index 0000000..7586d12
--- /dev/null
+++ b/client-temp/src/api/auth.ts
@@ -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;
+};
\ No newline at end of file
diff --git a/client-temp/src/api/axios.ts b/client-temp/src/api/axios.ts
new file mode 100644
index 0000000..0429f47
--- /dev/null
+++ b/client-temp/src/api/axios.ts
@@ -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);
+ }
+);
\ No newline at end of file
diff --git a/client-temp/src/api/todo.ts b/client-temp/src/api/todo.ts
new file mode 100644
index 0000000..6d3a88f
--- /dev/null
+++ b/client-temp/src/api/todo.ts
@@ -0,0 +1,21 @@
+import type { Todo } from '../types/todo';
+import { api } from './axios';
+
+export const getTodos = async (): Promise => {
+ const response = await api.get('/api/todos');
+ return response.data;
+};
+
+export const addTodo = async (title: string, priority: Todo['priority']): Promise => {
+ const response = await api.post('/api/todos', { title, priority });
+ return response.data;
+};
+
+export const toggleTodo = async (id: number): Promise => {
+ const response = await api.patch(`/api/todos/${id}/toggle`);
+ return response.data;
+};
+
+export const deleteTodo = async (id: number): Promise => {
+ await api.delete(`/api/todos/${id}`);
+};
\ No newline at end of file
diff --git a/client-temp/src/assets/react.svg b/client-temp/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/client-temp/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client-temp/src/index.css b/client-temp/src/index.css
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/client-temp/src/index.css
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client-temp/src/main.tsx b/client-temp/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/client-temp/src/main.tsx
@@ -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(
+
+
+ ,
+)
diff --git a/client-temp/src/pages/Login.tsx b/client-temp/src/pages/Login.tsx
new file mode 100644
index 0000000..8613791
--- /dev/null
+++ b/client-temp/src/pages/Login.tsx
@@ -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 (
+
+ );
+};
+
+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
new file mode 100644
index 0000000..f75a28f
--- /dev/null
+++ b/client-temp/src/pages/Register.tsx
@@ -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 (
+
+ );
+};
+
+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
new file mode 100644
index 0000000..5bed21c
--- /dev/null
+++ b/client-temp/src/types/todo.ts
@@ -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 = {
+ low: 'blue',
+ medium: 'yellow',
+ high: 'red',
+};
+
+export const priorityLabels = {
+ low: '低',
+ medium: '中',
+ high: '高'
+} as const;
\ No newline at end of file
diff --git a/client-temp/src/vite-env.d.ts b/client-temp/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/client-temp/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/client-temp/tailwind.config.js b/client-temp/tailwind.config.js
new file mode 100644
index 0000000..d37737f
--- /dev/null
+++ b/client-temp/tailwind.config.js
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
+
diff --git a/client-temp/tsconfig.app.json b/client-temp/tsconfig.app.json
new file mode 100644
index 0000000..c9ccbd4
--- /dev/null
+++ b/client-temp/tsconfig.app.json
@@ -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"]
+}
diff --git a/client-temp/tsconfig.json b/client-temp/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/client-temp/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/client-temp/tsconfig.node.json b/client-temp/tsconfig.node.json
new file mode 100644
index 0000000..9728af2
--- /dev/null
+++ b/client-temp/tsconfig.node.json
@@ -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"]
+}
diff --git a/client-temp/vite.config.ts b/client-temp/vite.config.ts
new file mode 100644
index 0000000..fd3e974
--- /dev/null
+++ b/client-temp/vite.config.ts
@@ -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',
+ },
+})
diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 0000000..4d29575
--- /dev/null
+++ b/client/.gitignore
@@ -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*
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 0000000..b87cb00
--- /dev/null
+++ b/client/README.md
@@ -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/).
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..ac35a34
--- /dev/null
+++ b/client/package.json
@@ -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"
+ }
+}
diff --git a/client/postcss.config.js b/client/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/client/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/client/public/favicon.ico b/client/public/favicon.ico
new file mode 100644
index 0000000..a11777c
Binary files /dev/null and b/client/public/favicon.ico differ
diff --git a/client/public/index.html b/client/public/index.html
new file mode 100644
index 0000000..aa069f2
--- /dev/null
+++ b/client/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/client/public/logo192.png b/client/public/logo192.png
new file mode 100644
index 0000000..fc44b0a
Binary files /dev/null and b/client/public/logo192.png differ
diff --git a/client/public/logo512.png b/client/public/logo512.png
new file mode 100644
index 0000000..a4e47a6
Binary files /dev/null and b/client/public/logo512.png differ
diff --git a/client/public/manifest.json b/client/public/manifest.json
new file mode 100644
index 0000000..080d6c7
--- /dev/null
+++ b/client/public/manifest.json
@@ -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"
+}
diff --git a/client/public/robots.txt b/client/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/client/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/client/src/App.css b/client/src/App.css
new file mode 100644
index 0000000..74b5e05
--- /dev/null
+++ b/client/src/App.css
@@ -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);
+ }
+}
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..c7e5a3b
--- /dev/null
+++ b/client/src/App.tsx
@@ -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 (
+
+
+
{title}
+
{content}
+
+
+ {onConfirm && }
+
+
+
+ );
+};
+
+// 随机头像颜色生成
+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 }) => (
+
+
+
+ {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
+
+ {loading ? (
+
加载中...
+ ) : (
+
+ )}
+
+
+ );
+};
+
+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
diff --git a/client/src/api/auth.ts b/client/src/api/auth.ts
new file mode 100644
index 0000000..7586d12
--- /dev/null
+++ b/client/src/api/auth.ts
@@ -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;
+};
\ No newline at end of file
diff --git a/client/src/api/todo.ts b/client/src/api/todo.ts
new file mode 100644
index 0000000..dabbfdf
--- /dev/null
+++ b/client/src/api/todo.ts
@@ -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 => {
+ const res = await axios.get(`${API_URL}/todos`, { headers: getAuthHeader() });
+ return res.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 updateTodo = async (id: string, updates: Partial): Promise => {
+ const res = await axios.put(
+ `${API_URL}/todos/${id}`,
+ updates,
+ { headers: getAuthHeader() }
+ );
+ return res.data;
+};
+
+export const deleteTodo = async (id: string): Promise => {
+ await axios.delete(`${API_URL}/todos/${id}`, { headers: getAuthHeader() });
+};
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000..bd6213e
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/client/src/index.tsx b/client/src/index.tsx
new file mode 100644
index 0000000..1fd12b7
--- /dev/null
+++ b/client/src/index.tsx
@@ -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(
+
+
+
+);
diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx
new file mode 100644
index 0000000..8613791
--- /dev/null
+++ b/client/src/pages/Login.tsx
@@ -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 (
+
+ );
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx
new file mode 100644
index 0000000..f75a28f
--- /dev/null
+++ b/client/src/pages/Register.tsx
@@ -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 (
+
+ );
+};
+
+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
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/client/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/client/src/types/todo.ts b/client/src/types/todo.ts
new file mode 100644
index 0000000..081360a
--- /dev/null
+++ b/client/src/types/todo.ts
@@ -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;
\ No newline at end of file
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
new file mode 100644
index 0000000..115da8e
--- /dev/null
+++ b/client/tailwind.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ content: [
+ "./src/**/*.{js,jsx,ts,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..a273b0c
--- /dev/null
+++ b/client/tsconfig.json
@@ -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"
+ ]
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..fa37cee
--- /dev/null
+++ b/server/package.json
@@ -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"
+ }
+}
diff --git a/server/src/app.ts b/server/src/app.ts
new file mode 100644
index 0000000..3eb0280
--- /dev/null
+++ b/server/src/app.ts
@@ -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}`);
+});
\ No newline at end of file
diff --git a/server/src/config/db.ts b/server/src/config/db.ts
new file mode 100644
index 0000000..137e95e
--- /dev/null
+++ b/server/src/config/db.ts
@@ -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;
\ No newline at end of file
diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts
new file mode 100644
index 0000000..1df9729
--- /dev/null
+++ b/server/src/controllers/authController.ts
@@ -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 });
+ }
+};
\ No newline at end of file
diff --git a/server/src/controllers/todoController.ts b/server/src/controllers/todoController.ts
new file mode 100644
index 0000000..37edb94
--- /dev/null
+++ b/server/src/controllers/todoController.ts
@@ -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 });
+ }
+};
\ No newline at end of file
diff --git a/server/src/models/Todo.ts b/server/src/models/Todo.ts
new file mode 100644
index 0000000..49df800
--- /dev/null
+++ b/server/src/models/Todo.ts
@@ -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('Todo', TodoSchema);
\ No newline at end of file
diff --git a/server/src/models/User.ts b/server/src/models/User.ts
new file mode 100644
index 0000000..1e4abb8
--- /dev/null
+++ b/server/src/models/User.ts
@@ -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('User', UserSchema);
\ No newline at end of file
diff --git a/server/src/routes/authRoutes.ts b/server/src/routes/authRoutes.ts
new file mode 100644
index 0000000..47bcf62
--- /dev/null
+++ b/server/src/routes/authRoutes.ts
@@ -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;
\ No newline at end of file
diff --git a/server/src/routes/todoRoutes.ts b/server/src/routes/todoRoutes.ts
new file mode 100644
index 0000000..04aa88d
--- /dev/null
+++ b/server/src/routes/todoRoutes.ts
@@ -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;
\ No newline at end of file
diff --git a/server/tsconfig.json b/server/tsconfig.json
new file mode 100644
index 0000000..904601c
--- /dev/null
+++ b/server/tsconfig.json
@@ -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"]
+}
\ No newline at end of file