From 902d8f72845b6ffb309f26eee563ee0265f570d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E6=B3=BD=E5=86=9B?= <5654792+tcubic21@user.noreply.gitee.com> Date: Tue, 17 Jun 2025 17:40:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client-temp/auto-imports.d.ts | 10 + client-temp/package.json | 4 + client-temp/src/App.App.tsx | 181 +++++++++++++++++ client-temp/src/App.tsx | 191 +++++++----------- client-temp/src/api/axios.ts | 14 +- client-temp/src/api/todo.ts | 19 +- client-temp/src/components/AntDesignIcon.tsx | 47 +++++ client-temp/src/components/TodoHeader.tsx | 72 +++++++ .../src/components/TodoListContent.tsx | 151 ++++++++++++++ client-temp/src/components/TodoModalForm.tsx | 103 ++++++++++ client-temp/src/hooks/useTodos.ts | 175 ++++++++++++++++ client-temp/src/index.css | 10 +- client-temp/src/main.tsx | 30 ++- client-temp/src/pages/ForgotPassword.tsx | 116 +++++++++++ client-temp/src/pages/Login.tsx | 153 +++++++++----- client-temp/src/pages/Register.tsx | 161 +++++++++++---- client-temp/src/types/todo.ts | 7 +- client-temp/src/utils/priorityUtils.ts | 25 +++ client-temp/vite.config.ts | 14 +- 19 files changed, 1253 insertions(+), 230 deletions(-) create mode 100644 client-temp/auto-imports.d.ts create mode 100644 client-temp/src/App.App.tsx create mode 100644 client-temp/src/components/AntDesignIcon.tsx create mode 100644 client-temp/src/components/TodoHeader.tsx create mode 100644 client-temp/src/components/TodoListContent.tsx create mode 100644 client-temp/src/components/TodoModalForm.tsx create mode 100644 client-temp/src/hooks/useTodos.ts create mode 100644 client-temp/src/pages/ForgotPassword.tsx create mode 100644 client-temp/src/utils/priorityUtils.ts diff --git a/client-temp/auto-imports.d.ts b/client-temp/auto-imports.d.ts new file mode 100644 index 0000000..9d24007 --- /dev/null +++ b/client-temp/auto-imports.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + +} diff --git a/client-temp/package.json b/client-temp/package.json index 126a608..60c0cc2 100644 --- a/client-temp/package.json +++ b/client-temp/package.json @@ -12,7 +12,9 @@ "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", @@ -32,6 +34,8 @@ "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/src/App.App.tsx b/client-temp/src/App.App.tsx new file mode 100644 index 0000000..1b9efeb --- /dev/null +++ b/client-temp/src/App.App.tsx @@ -0,0 +1,181 @@ +import { useState, useEffect } from 'react'; +import type { Todo, Priority } from './types/todo'; +import { getTodos, addTodo, toggleTodo, deleteTodo, updateTodo } from './api/todo'; +import TodoModalForm from './components/TodoModalForm'; +import TodoHeader from './components/TodoHeader'; +import TodoListContent from './components/TodoListContent'; +import { Layout, notification } from 'antd'; + +const { Content } = Layout; + +function App() { + const [todos, setTodos] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentTodo, setCurrentTodo] = useState(null); + const [modalTitle, setModalTitle] = useState(''); + const [isEditing, setIsEditing] = useState(false); // New state to indicate if we are editing or adding + const [activeTab, setActiveTab] = useState('pending'); // New state for active tab + const [searchTerm, setSearchTerm] = useState(''); // New state for search term + + useEffect(() => { + loadTodos(); + }, []); + + const loadTodos = async () => { + try { + const data = await getTodos(); + setTodos(data); + } catch (error) { + console.error('Failed to load todos:', error); + notification.error({ + message: '加载待办事项失败', + description: '无法从服务器获取待办事项列表。', + }); + } + }; + + const closeModal = () => { + setIsModalOpen(false); + setCurrentTodo(null); + setIsEditing(false); + }; + + const openAddModal = () => { + setCurrentTodo(null); + setModalTitle('新增待办事项'); + setIsEditing(false); + setIsModalOpen(true); + }; + + const handleEditClick = (todo: Todo) => { + setCurrentTodo(todo); + setModalTitle('编辑待办事项'); + setIsEditing(true); + setIsModalOpen(true); + }; + + const handleModalSubmit = async (values: { + title: string; + description: string; + priority: Priority; + completed?: boolean; + }) => { + try { + if (currentTodo) { + // Update existing todo + const updatedTodo = await updateTodo( + currentTodo._id, + values.title, + values.description, + values.priority, + values.completed || false // Ensure completed is boolean + ); + setTodos(todos.map(todo => (todo._id === updatedTodo._id ? updatedTodo : todo))); + notification.success({ + message: '待办事项更新成功', + }); + } else { + // Add new todo + const newTodo = await addTodo(values.title, values.description, values.priority); + setTodos([...todos, newTodo]); + notification.success({ + message: '待办事项添加成功', + }); + } + closeModal(); + } catch (error) { + console.error(currentTodo ? 'Failed to update todo:' : 'Failed to add todo:', error); + notification.error({ + message: currentTodo ? '更新待办事项失败' : '添加待办事项失败', + description: '请检查您的输入和网络连接。', + }); + } + }; + + const handleToggle = async (_id: string) => { + try { + const updatedTodo = await toggleTodo(_id); + setTodos(todos.map(todo => todo._id === _id ? updatedTodo : todo)); + notification.success({ + message: '待办事项状态切换成功', + }); + } catch (error) { + console.error('Failed to toggle todo:', error); + notification.error({ + message: '切换待办事项状态失败', + description: '请稍后再试。', + }); + } + }; + + const handleDelete = async (_id: string) => { + try { + await deleteTodo(_id); + setTodos(todos.filter(todo => todo._id !== _id)); + notification.success({ + message: '待办事项删除成功', + }); + } catch (error) { + console.error('Failed to delete todo:', error); + notification.error({ + message: '删除待办事项失败', + description: '请稍后再试。', + }); + } + }; + + const handleTabChange = (key: string) => { + setActiveTab(key); + }; + + const filteredTodos = todos.filter(todo => { + const matchesSearch = searchTerm === '' || + todo.title.toLowerCase().includes(searchTerm.toLowerCase()) || + (todo.description && todo.description.toLowerCase().includes(searchTerm.toLowerCase())); + + if (activeTab === 'pending') { + return !todo.completed && matchesSearch; + } else { + return todo.completed && matchesSearch; + } + }); + + return ( + + + + + + {isModalOpen && ( + + )} + + + ); +} + +export default App; \ No newline at end of file diff --git a/client-temp/src/App.tsx b/client-temp/src/App.tsx index 6b795f5..b02e96d 100644 --- a/client-temp/src/App.tsx +++ b/client-temp/src/App.tsx @@ -1,122 +1,87 @@ -import { useState, useEffect } from 'react'; -import type { Todo } from './types/todo'; -import { getTodos, addTodo, toggleTodo, deleteTodo } from './api/todo'; -import { priorityColors } from './types/todo'; +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, 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); - } - }; + const { + todos, + isModalOpen, + currentTodo, + modalTitle, + isEditing, + activeTab, + searchTerm, + setSearchTerm, + openAddModal, + closeModal, + handleEditClick, + handleModalSubmit, + handleToggle, + handleDelete, + handleTabChange, + filteredTodos, + } = useTodos(); return ( -
-

Todo List

- -
-
- setTitle(e.target.value)} - placeholder="Add a new todo..." - className="flex-1 px-4 py-2 border rounded" - /> - - + + + + {/* Title */} +
+ {/* Search and button */} +
+ setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> + +
- -
    - {todos.map((todo) => ( -
  • -
    - handleToggle(todo.id)} - className="w-5 h-5" - /> - - {todo.title} - - - {todo.priority} - -
    - -
  • - ))} -
-
+ + + {isModalOpen && ( + + )} + + ); } diff --git a/client-temp/src/api/axios.ts b/client-temp/src/api/axios.ts index 0429f47..3231215 100644 --- a/client-temp/src/api/axios.ts +++ b/client-temp/src/api/axios.ts @@ -1,13 +1,25 @@ import axios from 'axios'; export const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL, + baseURL: '/api', }); +api.interceptors.request.use( + config => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + error => Promise.reject(error) +); + api.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { + localStorage.removeItem('token'); // 清除无效 token window.location.href = '/login'; } return Promise.reject(error); diff --git a/client-temp/src/api/todo.ts b/client-temp/src/api/todo.ts index 6d3a88f..0c69457 100644 --- a/client-temp/src/api/todo.ts +++ b/client-temp/src/api/todo.ts @@ -2,20 +2,25 @@ import type { Todo } from '../types/todo'; import { api } from './axios'; export const getTodos = async (): Promise => { - const response = await api.get('/api/todos'); + const response = await api.get('/todos'); return response.data; }; -export const addTodo = async (title: string, priority: Todo['priority']): Promise => { - const response = await api.post('/api/todos', { title, priority }); +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 toggleTodo = async (id: number): Promise => { - const response = await api.patch(`/api/todos/${id}/toggle`); +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: number): Promise => { - await api.delete(`/api/todos/${id}`); +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/components/AntDesignIcon.tsx b/client-temp/src/components/AntDesignIcon.tsx new file mode 100644 index 0000000..3f289e7 --- /dev/null +++ b/client-temp/src/components/AntDesignIcon.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +const AntDesignIcon: React.FC> = (props) => ( + + Group 28 Copy 5 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export { AntDesignIcon }; \ No newline at end of file diff --git a/client-temp/src/components/TodoHeader.tsx b/client-temp/src/components/TodoHeader.tsx new file mode 100644 index 0000000..fc1793b --- /dev/null +++ b/client-temp/src/components/TodoHeader.tsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from 'react'; +import { Layout, Typography, Button, Modal, Space, Avatar } from 'antd'; +import { ExclamationCircleOutlined, LogoutOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; + +const { Header } = Layout; +const { Text } = Typography; + +interface TodoHeaderProps { +} + +const TodoHeader: React.FC = () => { + const navigate = useNavigate(); + const [userName, setUserName] = useState(''); + const [avatarBgColor, setAvatarBgColor] = useState(''); + + const generateRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; + }; + + useEffect(() => { + // In a real application, you would fetch the user's name from an authenticated API + // For now, let's assume the username is stored in localStorage after login + const storedUserName = localStorage.getItem('username'); // Assuming username is stored + setUserName(storedUserName || '用户'); + setAvatarBgColor(generateRandomColor()); + }, []); + + const showConfirmLogout = () => { + Modal.confirm({ + title: '确认退出', + icon: , + content: '您确定要退出登录吗?', + okText: '确认', + cancelText: '取消', + onOk() { + localStorage.removeItem('token'); + localStorage.removeItem('username'); // Also clear username on logout + navigate('/login'); + }, + onCancel() { + console.log('取消退出'); + }, + }); + }; + + return ( +
+
+
+ Todo List +
+ + + {userName.charAt(0).toUpperCase()} + + {userName} + + +
+
+ ); +}; + +export default TodoHeader; \ No newline at end of file diff --git a/client-temp/src/components/TodoListContent.tsx b/client-temp/src/components/TodoListContent.tsx new file mode 100644 index 0000000..2d99cb9 --- /dev/null +++ b/client-temp/src/components/TodoListContent.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { List, Button, Typography, Tag, Space, Popconfirm, Checkbox, Tabs } from 'antd'; +import { EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import type { Todo, Priority } from '../types/todo'; + +const { Text } = Typography; + +interface TodoListContentProps { + todos: Todo[]; + handleEditClick: (todo: Todo) => void; + handleToggle: (id: string) => void; + handleDelete: (id: string) => void; + getPriorityTagColor: (priority: Priority) => string; + getPriorityIcon: (priority: Priority) => string; + priorityLabels: { [key in Priority]: string }; + activeTab: string; + handleTabChange: (key: string) => void; + filteredTodos: Todo[]; +} + +const TodoListContent: React.FC = ({ + todos, + handleEditClick, + handleToggle, + handleDelete, + getPriorityTagColor, + getPriorityIcon, + priorityLabels, + activeTab, + handleTabChange, + filteredTodos, +}) => { + const items = [ + { + key: 'pending', + label: '待办', + children: ( + !todo.completed)} + renderItem={(todo) => ( + } onClick={() => handleEditClick(todo)}> + 编辑 + , + handleDelete(todo._id)} + okText="是" + cancelText="否" + > + + , + ]} + > +
+ handleToggle(todo._id)} + /> + } + title={ + + + {todo.title} + + + {getPriorityIcon(todo.priority)} {priorityLabels[todo.priority]} + + + } + description={ + todo.description && {todo.description} + } + /> +
+
+ )} + /> + ), + }, + { + key: 'completed', + label: '已完成', + children: ( + todo.completed)} + renderItem={(todo) => ( + } onClick={() => handleEditClick(todo)}> + 编辑 + , + handleDelete(todo._id)} + okText="是" + cancelText="否" + > + + , + ]} + > +
+ handleToggle(todo._id)} + /> + } + title={ + + + {todo.title} + + + {getPriorityIcon(todo.priority)} {priorityLabels[todo.priority]} + + + } + description={ + todo.description && {todo.description} + } + /> +
+
+ )} + /> + ), + }, + ]; + + return ( + + ); +}; + +export default TodoListContent; \ No newline at end of file diff --git a/client-temp/src/components/TodoModalForm.tsx b/client-temp/src/components/TodoModalForm.tsx new file mode 100644 index 0000000..ff1bde8 --- /dev/null +++ b/client-temp/src/components/TodoModalForm.tsx @@ -0,0 +1,103 @@ +import React, { useEffect } from 'react'; +import { Modal, Form, Input, Select, Button, Checkbox } from 'antd'; +import type { Priority } from '../types/todo'; +import { priorityLabels, getPriorityIcon } from '../utils/priorityUtils'; + +const { Option } = Select; + +interface TodoModalFormProps { + isVisible: boolean; + title: string; + initialValues: { + title: string; + description: string; + priority: Priority; + completed?: boolean; + }; + onCancel: () => void; + onSubmit: (values: { + title: string; + description: string; + priority: Priority; + completed?: boolean; + }) => void; + isEditing: boolean; +} + +const TodoModalForm: React.FC = ({ + isVisible, + title, + initialValues, + onCancel, + onSubmit, + isEditing, +}) => { + const [form] = Form.useForm(); + + useEffect(() => { + form.setFieldsValue(initialValues); + }, [initialValues, form]); + + const handleFinish = (values: any) => { + onSubmit(values); + }; + + return ( + +
+ + + + + + + + + + {isEditing && ( + + 已完成 + + )} + +
+ + +
+
+
+
+ ); +}; + +export default TodoModalForm; \ No newline at end of file diff --git a/client-temp/src/hooks/useTodos.ts b/client-temp/src/hooks/useTodos.ts new file mode 100644 index 0000000..dba39bb --- /dev/null +++ b/client-temp/src/hooks/useTodos.ts @@ -0,0 +1,175 @@ +import { useState, useEffect } from 'react'; +import { notification } from 'antd'; +import type { Todo, Priority } from '../types/todo'; +import { getTodos, addTodo, toggleTodo, deleteTodo, updateTodo } from '../api/todo'; + +interface UseTodosResult { + todos: Todo[]; + isModalOpen: boolean; + currentTodo: Todo | null; + modalTitle: string; + isEditing: boolean; + activeTab: string; + searchTerm: string; + setSearchTerm: (term: string) => void; + openAddModal: () => void; + closeModal: () => void; + handleEditClick: (todo: Todo) => void; + handleModalSubmit: (values: { title: string; description: string; priority: Priority; completed?: boolean; }) => Promise; + handleToggle: (_id: string) => Promise; + handleDelete: (_id: string) => Promise; + handleTabChange: (key: string) => void; + filteredTodos: Todo[]; + loadTodos: () => Promise; +} + +export const useTodos = (): UseTodosResult => { + const [todos, setTodos] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentTodo, setCurrentTodo] = useState(null); + const [modalTitle, setModalTitle] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const [activeTab, setActiveTab] = useState('pending'); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + loadTodos(); + }, []); + + const loadTodos = async () => { + try { + const data = await getTodos(); + setTodos(data); + } catch (error) { + console.error('Failed to load todos:', error); + notification.error({ + message: '加载待办事项失败', + description: '无法从服务器获取待办事项列表。', + }); + } + }; + + const closeModal = () => { + setIsModalOpen(false); + setCurrentTodo(null); + setIsEditing(false); + }; + + const openAddModal = () => { + setCurrentTodo(null); + setModalTitle('新增待办事项'); + setIsEditing(false); + setIsModalOpen(true); + }; + + const handleEditClick = (todo: Todo) => { + setCurrentTodo(todo); + setModalTitle('编辑待办事项'); + setIsEditing(true); + setIsModalOpen(true); + }; + + const handleModalSubmit = async (values: { + title: string; + description: string; + priority: Priority; + completed?: boolean; + }) => { + try { + if (currentTodo) { + const updatedTodo = await updateTodo( + currentTodo._id, + values.title, + values.description, + values.priority, + values.completed || false + ); + setTodos(todos.map(todo => (todo._id === updatedTodo._id ? updatedTodo : todo))); + notification.success({ + message: '待办事项更新成功', + }); + } else { + const newTodo = await addTodo(values.title, values.description, values.priority); + setTodos([...todos, newTodo]); + notification.success({ + message: '待办事项添加成功', + }); + } + closeModal(); + } catch (error) { + console.error(currentTodo ? 'Failed to update todo:' : 'Failed to add todo:', error); + notification.error({ + message: currentTodo ? '更新待办事项失败' : '添加待办事项失败', + description: '请检查您的输入和网络连接。', + }); + } + }; + + const handleToggle = async (_id: string) => { + try { + const updatedTodo = await toggleTodo(_id); + setTodos(todos.map(todo => todo._id === _id ? updatedTodo : todo)); + notification.success({ + message: '待办事项状态切换成功', + }); + } catch (error) { + console.error('Failed to toggle todo:', error); + notification.error({ + message: '切换待办事项状态失败', + description: '请稍后再试。', + }); + } + }; + + const handleDelete = async (_id: string) => { + try { + await deleteTodo(_id); + setTodos(todos.filter(todo => todo._id !== _id)); + notification.success({ + message: '待办事项删除成功', + }); + } catch (error) { + console.error('Failed to delete todo:', error); + notification.error({ + message: '删除待办事项失败', + description: '请稍后再试。', + }); + } + }; + + const handleTabChange = (key: string) => { + setActiveTab(key); + }; + + const filteredTodos = todos.filter(todo => { + const matchesSearch = searchTerm === '' || + todo.title.toLowerCase().includes(searchTerm.toLowerCase()) || + (todo.description && todo.description.toLowerCase().includes(searchTerm.toLowerCase())); + + if (activeTab === 'pending') { + return !todo.completed && matchesSearch; + } else { + return todo.completed && matchesSearch; + } + }); + + return { + todos, + isModalOpen, + currentTodo, + modalTitle, + isEditing, + activeTab, + searchTerm, + setSearchTerm, + openAddModal, + closeModal, + handleEditClick, + handleModalSubmit, + handleToggle, + handleDelete, + handleTabChange, + filteredTodos, + loadTodos, + }; +}; \ No newline at end of file diff --git a/client-temp/src/index.css b/client-temp/src/index.css index 0519ecb..5f82d2e 100644 --- a/client-temp/src/index.css +++ b/client-temp/src/index.css @@ -1 +1,9 @@ - \ No newline at end of file +@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/main.tsx b/client-temp/src/main.tsx index bef5202..6432203 100644 --- a/client-temp/src/main.tsx +++ b/client-temp/src/main.tsx @@ -1,10 +1,22 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import App from './App.tsx'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import ForgotPassword from './pages/ForgotPassword'; +import 'antd/dist/reset.css'; +import '@ant-design/v5-patch-for-react-19'; -createRoot(document.getElementById('root')!).render( - - - , -) +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + } /> + } /> + } /> + } /> + + + +); diff --git a/client-temp/src/pages/ForgotPassword.tsx b/client-temp/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..05d0bd8 --- /dev/null +++ b/client-temp/src/pages/ForgotPassword.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Form, Input, Button, Card, Space, Typography, Alert } from 'antd'; +import { MailOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons'; +import { api } from '../api/axios'; + +const { Title, Text, Link } = Typography; + +function ForgotPassword() { + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const onFinish = async (values: any) => { + setLoading(true); + setMessage(null); + setError(null); + try { + // 模拟API调用,实际情况会调用后端接口发送重置邮件 + console.log('Reset password request for:', values.email); + await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟 + + setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。'); + // 实际情况下,这里可能会调用后端接口,例如: + // await api.post('/auth/forgot-password', { email: values.email }); + + } catch (err: any) { + const errorMessage = err.response?.data?.message || '请求失败,请稍后再试。'; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +
+ + 忘记密码 + 请输入您的邮箱或用户名以重置密码 +
+ + {message && ( + setMessage(null)} + style={{ marginBottom: 24 }} + /> + )} + + {error && ( + setError(null)} + style={{ marginBottom: 24 }} + /> + )} + +
+ + } + placeholder="邮箱或用户名" + size="large" + /> + + + + + +
+ + 返回登录 + +
+
+
+ ); +} + +export default ForgotPassword; \ No newline at end of file diff --git a/client-temp/src/pages/Login.tsx b/client-temp/src/pages/Login.tsx index 8613791..22410e4 100644 --- a/client-temp/src/pages/Login.tsx +++ b/client-temp/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-temp/src/pages/Register.tsx b/client-temp/src/pages/Register.tsx index f75a28f..749e846 100644 --- a/client-temp/src/pages/Register.tsx +++ b/client-temp/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-temp/src/types/todo.ts b/client-temp/src/types/todo.ts index 5bed21c..56f949e 100644 --- a/client-temp/src/types/todo.ts +++ b/client-temp/src/types/todo.ts @@ -1,15 +1,16 @@ export type Priority = 'low' | 'medium' | 'high'; export interface Todo { - id: number; + _id: string; title: string; + description: string; completed: boolean; priority: Priority; } export const priorityColors: Record = { - low: 'blue', - medium: 'yellow', + low: 'green', + medium: 'orange', high: 'red', }; diff --git a/client-temp/src/utils/priorityUtils.ts b/client-temp/src/utils/priorityUtils.ts new file mode 100644 index 0000000..c5ecfb2 --- /dev/null +++ b/client-temp/src/utils/priorityUtils.ts @@ -0,0 +1,25 @@ +import type { Priority } from '../types/todo'; + +export const priorityLabels: { [key in Priority]: string } = { + low: '低', + medium: '中', + high: '高', +}; + +export const getPriorityTagColor = (priority: Priority): string => { + switch (priority) { + case 'low': return 'green'; + case 'medium': return 'orange'; + case 'high': return 'red'; + default: return 'default'; + } +}; + +export const getPriorityIcon = (priority: Priority): string => { + switch (priority) { + case 'low': return '🌱'; + case 'medium': return '🌟'; + case 'high': return '🔥'; + default: return ''; + } +}; \ No newline at end of file diff --git a/client-temp/vite.config.ts b/client-temp/vite.config.ts index fd3e974..985303e 100644 --- a/client-temp/vite.config.ts +++ b/client-temp/vite.config.ts @@ -1,14 +1,21 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' +import AutoImport from 'unplugin-auto-import/vite' +import AntdResolve from 'unplugin-auto-import-antd' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + AutoImport({ + resolvers: [AntdResolve()] + }) + ], server: { port: 5173, proxy: { '/api': { - target: process.env.VITE_API_URL, + target: 'http://localhost:5050', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, @@ -19,4 +26,7 @@ export default defineConfig({ sourcemap: process.env.VITE_NODE_ENV === 'development', minify: process.env.VITE_NODE_ENV === 'production', }, + optimizeDeps: { + exclude: ['@ant-design/v5-patch-for-react-19'], + }, })