&*j;=u%25ay-%>3@81tGe^_z*C7pb9y*Ed^H3t$BIKH2o+olp#$q;)_ zfpjCb_^VFg5fU~K)nf*d*r@BCC>UZ!0&b?AGk_jTPXaSnCuW110wjHPPe^9R^;jo3 zwvzTl)C`Zl5}O2}3lec=hZ*$JnkW#7enKKc)(pM${_$9Hc=Sr_A9Biwe*Y=T?~1CK z6eZ9uPICjy-sMGbZl$yQmpB&`ouS8v{58__t0$JP%i3R&%QR3ianbZqDs<2#5FdN@n5bCn^ZtH992~5k(eA|8|@G9u`wdn7bnpg|@{m z^d6Y`*$Zf2Xr&|g%sai#5}Syvv(>Jnx&EM7-|Jr7!M~zdAyjt*xl;OLhvW-a%H1m0 z*x5*nb=R5u><7lyVpN AR?q@1U59 zO+)QW wL8t zyip?u_nI+K$uh{ y)~}qj?(w0&=SE^8`_WMM zTybjG=999h38Yes7}-4*LJ7H)UE8{mE(6;8voE+TYY%33A>S6`G_95^5QHNTo_;Ao ztIQIZ_}49%{8|=O;isBZ?=7kfdF8_@azfoTd+hEJKWE!)$)N%HIe2cplaK`ry#=pV z0q{9w-`i0h@!R8K3GC{ivt{70IWG`EP |(1g7i_Q<>aEAT{5( yD z=!O?kq61VegV+st@XCw475j6vS)_z@efuqQgHQR1T4;|-#OLZNQJPV4k$AX1Uk8Lm z{N*b*ia=I+MB}kWpupJ~>!C@xEN#Wa7V+7{m4j8c?)ChV=D?o~sjT?0C_AQ 7B-vxqX30s0I_`2$in86#`mAsT-w?j{&AL@B3$;P z31G4(lV|b}uSD CIrjk+M1R!X7s 4Aabn<)zpgT}#gE|mIvV38^ODy@<&yflpCwS#fRf9ZX3lPV_?8@C5)A;T zqmouFLFk;qIs4rA=hh=GL~sCFsXHsqO6_y~*AFt93 9UYVBSx1s(=Kb&5;j7cSowdE;7()CC2|-i9Zz+_BIw8#ll~-tyH?F3{%`QCsYa*b#s*9iCc`1P1oC26?`g<9))EJ3%xz+O!B3 zZ7$j~To)C@PquR>a1+Dh>-a%IvH_Y7^ys|4o?E%3`I&ADXfC8++hAdZfzIT#%C+Jz z1lU~K_vAm0m8Qk}K$F>| >RPK%<1SI0(G+8q~H zAsjezyP+u!Se4q3GW)`h`NPSRlMoBjCzNPesWJwVTY!o@G8=(6I%4XHGaSiS3MEBK zhgGFv6Jc>L$4jVE!I?TQuwvz_%CyO!bLh94nqK11C2W$*aa2ueGopG8DnBICVUORP zgytv#)49fVXDaR$SukloYC3u7#5H)}1K21=?DKj^U)8G;MS)&Op)g^zR2($<>C*zW z;X7`hLxiIO#J`ANdyAOJle4V%ppa*(+0i3w;8i*BA_;u8gOO6)MY`ueq7stBMJTB; z-a0R>hT*}>z|Gg}@^zDL1MrH+2hsR8 zHc}*9IvuQC^Ju)^#Y{fOr(96rQNPNhxc;mH@W*m206>Lo<*SaaH?~8zg&f&%YiOEG zGiz?*CP>Bci}!WiS=zj#K5I}>DtpregpP_tfZtPa(N<%vo^#WCQ5BTv0vr%Z{)0q+ z)RbfHktUm|lg&U3YM%lMUM(f u}i#kjX9h>GYctkx9Mt_8{@s%!K_EI zScgwy6%_fR?CG JQtmgNAj^h9B#zma MDWgH55pGuY1Gv7D z;8Psm(vEPiwn#MgJYu4Ty9D|h!?Rj0ddE|&L3S{IP%H4^N!m`60ZwZw^;eg4sk6K{ ziA^`Sbl_4~f&Oo%n;8Ye(tiAdlZKI!Z=|j$5hS|D$bDJ}p{gh$KN&JZYLUjv4h{NY zBJ>X9z !xfDGY z+oh_Z&_e#Q(-}>ssZfm=j$D&4W4FNy&-kAO1~#3Im;F)Nwe{(*75(p=P^VI?X 0GFakfh+X-px4a%Uw@fSbmp9hM1_~R>?Z8+ ziy|e9>8V*`OP}4x5JjdWp}7eX;lVxp5qS} 0YZek;SNmm7tEeSF*-dI)6U-A%m6YvCgM(}_=k#a6o^%-K4{`B1+}O4x zztDT%hVb;v#?j`lTvlFQ3aV#zkX=7 ;YFLS$uIzb0E3lozs5`Xy zi~vF+%{z9uLjKvKPhP%x5f ~7-Gj+%5N`%^=yk*Qn{`> z;xj&ROY6g`iy2a@{O)V(jk&8#hHACVDXey5a+KDod_Z&}kHM}xt7}Md@pil{2x7E~ zL$k^d2@Ec2XskjrN+IILw;#7((abu;OJii&v3?60x>d_Ma(onIPtcVnX@ELF0aL?T zSmWiL3(dOFkt!x=1O!_0n(cAzZW+3nHJ{2S>tgSK?~cF ha^y(l@-Mr2W$%MN{#af8J;V*>hdq!gx=d0h$T7l}>91Wh07)9CTX zh2_ZdQCyFOQ)l(}gft0UZ G`Sh2`x-w`5vC2UD}lZs*5 zG76$akzn}Xi))L3oGJ75#pcN=cX3!=57$Ha=hQ2^lwdyU#a}4JJOz6ddR%zae%#4& za)bFj)z=YQela(F#Y|Q#dp}PJghITwXouVaMq$BM?K%cXn9^Y@g43$=O)F&ZlOUom zJiad#dea;-eywBA@e&D6Pdso1?2^(pXiN91?jvcaUyYoKUmvl5G9e$W!okWe*@a<^ z8cQQ6cNSf+UPDx%?_G4a IiybZHHagF{ ;IcD(dPO!#=u zWfqLcPc^+7Uu#l(B pxft{*4lv#*u7X9AOzDO z1D9?^jIo}?%iz(_dwLa{ex#T}76ZfN_Z-hwpus9y+4xaUu9cX}&P{XrZVWE{1^0yw zO;YhLEW!pJcbCt3L8~a7>jsaN{V3>tz6_7`&pi%GxZ=V3?3K^ U+*ryLSb)8^IblJ0 zSRLNDvIxt)S}g30?s_3NX>F?NKIGrG_zB9@Z>uSW3k2es_H2kU;Rnn%j5qP)!XHKE zPB2mHP~tLCg4K_vH$xv`HbRsJwbZMUV(t=ez;Ec(vyHH)FbfLg`c61I$W_uBB>i^r z&{_P;369-&>23R%qNIULe=1~T$(DA`ev*EWZ6j(B$(te}x1WvmIll21zvygkS%vwG zzkR6Z#RKA2!z!C%M!O>!=Gr0(J0FP=-MN=5t-Ir)of50y10W}j`GtRCsXBakrKtG& zazmITDJMA0C51&BnLY)SY9r)NVTMs);1<=oosS9g31l{4ztjD3#+2H7u_|66b|_*O z;Qk6nalpqdHOjx|K&vUS_6ITgGll;TdaN*ta=M_YtyC)I9Tmr~VaPrH2q b6sd~=AcIxV+%z{E&0@y=DPArw zdV7z(G1hBx7hd{>(cr43^WF%4Y@PXZ?wPpj{OQ#tvc$pABJbvPGvdR`cAtHn)cSEV zrpu}1tJwQ3y!mSmH*uz*x0o|CS<^w%&KJzsj~DU0cLQUxk5B!hWE>aBkjJle8z~;s z-!A=($+}Jq_BTK5^B!`R>!MulZN)F=iXXeUd0w5lUsE5VP*H*oCy( ;?S$p*TVvTxwAeWFB$jHyb0593)$zqalVlDX=GcCN1gU0 zlgU)I$LcXZ8Oyc2TZYTPu@-;7<4YYB-``Qa;IDcvydIA$%kHhJKV^m*- zxcvU4viy &Kr5GVM{IT>WRywKQ9;>SEiQD*NqplK-KK4YR`p0@JW)n_{TU3bt0 zim%;(m1=#v2}zTps=?fU5w^(*y)xT%1vtQH&}50ZF!9YxW=&7*W($2kgKyz1mUgfs zfV<*XVVIFnohW=|j+@Kfo!#liQR^x>2yQdrG;2o8WZR+XzU_nG=Ed2rK?ntA;K5B{ z>M8+*A4 !Jm^Bg}aW?R?6;@QG@uQ8&oJ{hFixcfEnJ4QH?A4>P=q29oDGW;L;= z9-a0;g%c`C+Ai!UmK$NC*4#;Jp<1=TioL=t^YM)<<%u#hnnfSS`nq63QKGO1L8RzX z@MFDq s1z ztYmxDl@LU)5acvHk)~Z`RW7=aJ_nGD!mOSYD>5Odjn@TK#LY{jf?+piB5AM-CAoT_ z?S-*q7}wyLJzK>N%eMPuFgN)Q_otKP;aqy=D5f! 7<=n(lNkYRXVpkB{TAYLYg{|(jtRqYmg$xH zjmq ?B(RE4 zQx^~Pt}gxC2~l=K$$-sYy_r$CO(d=+b3H1MB*y_5g6WLaWTXn+TKQ|hNY^>Mp6k*$ zwkovomhu776vQATqT4blf~g;TY(MWCrf^^yfWJvSAB$p5l;jm@o#=!lqw+Lqfq>X= z$6~kxfm7`3q4zUEB;u4qa#BdJxO!;xGm)wwuisj{0y2x{R(IGMrsIzDY9LW>m!Y`= z04sx3IjnYvL<4JqxQ8f7qYd0s2Ig%`ytYPEMKI)s(LD}D@EY>x`VFtqvnADNBdeao zC96X+MxnwKmjpg{U&gP3HE}1=s!lv&D{6(g_lzyF3A`7Jn*&d_kL<;dAFx!UZ>hB8 z5A*%LsAn;VLp>3${0>M?PSQ)9s3}|h2e?TG4_F{}{Cs>#3Q*t$(CUc}M)I}8cPF6% z=+h(Kh^8)}gj(0}#e7O^FQ6`~fd1#8#!}LMuo3A0bN`o}PYsm!Y}sdOz$+Tegc=qT z8x`PH$7lvnhJp{kHWb22l;@7B7|4yL4UOOVM0MP_>P%S1Lnid)+k9{+3D+JFa#Pyf zhVc#&df87APl4W9X)F3pGS>@etfl=_E5tBcVoOfrD4hmVeTY-cj((pkn%n@EgN{0f zwb_^Rk0I#i ZuHK!l*lN`ceJn(sI{$Fq6nN& zE<-=0_2WN}m+*ivmIOxB@#~Q-cZ>l136w{#TIJe478`KE7@=a{>SzPHsKLzYAyBQO zAtuuF$-JSDy_S@6GW0MOE~R)b;+0f%_NMrW(+V#c_d&U8Z9+ec4=HmOHw?gdjF(Lu zzra 83M_BoO-1b3;9`%&DHfuUY)6YDV21P$C!Rc?mv&{lx#f8oc6?0?x zK08{WP65?#>(vPfA-c=MCY| %*1_<3D4NX zeVTi-JGl2uP_2@0F{G({pxQOXt_d{g_CV6b?jNpfUG9;8yle-^4KHRvZs-_2siata zt+d_T@U$&t*xaD22(fH(W1r$Mo?3dc%Tncm=C6{V9y{v&VT#^1L04vDrLM9qBoZ4@ z6DBN#m57hX7$C(=#$Y5$bJmwA$T8jKD8+6A!-IJwA{WOfs%s}yxUw^?MRZjF$n_KN z6`_bGXcmE#5e4Ym)aQJ)xg3Pg0@k`iGuHe?f(5LtuzSq=nS^5z>vqU0EuZ&75V%Z{ zYyhRLN^)$c6Ds{f7*FBpE;n5iglx5PkHfWrj3`x^j^t z7ntuV`g!9Xg#^3!x)l*}IW=(Tz3>Y5l4uGaB&lz{GDjm2D5S$CExLT`I1#n^lBH7Y zDgpMag@`iETKAI=p<5E#LTkw zVR@=yY|uBVI1HG|8h+d;G-qfuj}-ZR6fN>EfCCW z9~wRQoAPEa#aO?3h?x{YvV*d+NtPkf&4V0k4|L=uj!U{L+oLa(z#&iuhJr3-PjO3R z5s?=nn_5^*^Rawr>>Nr@K(jwkB#JK-=+HqwfdO<+P5byeim)wvqGlP-P|~Nse8=XF zz`?RYB|D6SwS}C+YQv+;}k6$-%D(@+t14BL@vM z2q%q?f6D-A5s$_WY3{^G0F131bbh|g!}#BKw=HQ7mx;Dzg4Z*bTLQSfo{ed{4}NZW zfrRm^Ca$rlE{Ue~uYv>R9{3s mwATcdM_6+yWIO z*ZRH~uXE@#p$XTbCt5j7j2=86e{9>HIB6xDzV+vAo&B?KUiMP|ttOElepnl%|DPqL b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN 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 ( - --- ); -}; -// 随机头像颜色生成 -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; +{title}
-{content}-- - {onConfirm && } +-+ + + {/* Title */} + + {/* Search and button */} +-+setSearchTerm(e.target.value)} + style={{ width: 300 }} + /> + + + + {isModalOpen && ( + + )} + + + ); } -// 状态栏组件 -const StatusBar: React.FC<{ username: string; avatarColor: string; avatarText: string; onLogout: () => void }> = ({ username, avatarColor, avatarText, onLogout }) => ( - --); - -const TodoApp: React.FC = () => { - const [todos, setTodos] = useState- - {avatarText} - - {username} - --([]); - 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 ( - - {/* 状态栏 */} -- ); -}; - -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 ( -- 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 ? ( -加载中...- ) : ( -- {todos.map(todo => ( -
- )} -- -
- ))} -- handleToggle(todo)} - className="accent-blue-500 w-5 h-5" - /> - {todo.title} --- - --- - ); -}; - -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 ( - --+登录
- -- 没有账号?注册 --+); -}; +} 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+ + +++ + {error && ( ++ Todo List +轻松管理您的任务 +setError(null)} + style={{ marginBottom: 24 }} + /> + )} + + + } + placeholder="账户" + size="large" + /> + + ++ + +} + placeholder="密码" + size="large" + /> + + + +++自动登录 ++ 忘记密码 + 注册新账号 + ++ + + +(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 ( - --+注册
- -- 已有账号?登录 --+); -}; +} 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 @@ -///+ + +++ + {error && ( ++ Todo List +轻松管理您的任务 +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" + /> + + + ++ 已有账号?去登录 ++ +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