From 88c2fa5d034d168a337dc7562e5c7a7f44fe284b Mon Sep 17 00:00:00 2001 From: Chuyaoyuan Date: Wed, 29 Apr 2026 19:05:47 +0800 Subject: [PATCH] update --- .../target/classes/com/cyynote/WebApp.class | Bin 1876 -> 1965 bytes .../cyynote/controller/JwtInterceptor.class | Bin 3067 -> 3039 bytes .../cyynote/controller/NoteController.class | Bin 11795 -> 11750 bytes .../target/{cyynote-0.2.jar => cyynote.jar} | Bin 20933192 -> 20933212 bytes cyynote-backend/target/cyynote.jar.original | Bin 40394 -> 40414 bytes .../target/maven-archiver/pom.properties | 10 +- .../compile/default-compile/createdFiles.lst | 54 +- .../compile/default-compile/inputFiles.lst | 54 +- cyynote-frontend/.env.example | 3 + cyynote-frontend/.github/workflows/ci.yml | 38 + cyynote-frontend/.npmrc | 1 + cyynote-frontend/.nvmrc | 1 + cyynote-frontend/eslint.config.js | 31 + cyynote-frontend/package.json | 124 +- cyynote-frontend/src/App.tsx | 190 +- .../src/components/ProtectedRoute.tsx | 8 + .../src/components/lexical/collaboration.ts | 33 +- .../src/components/login.component.tsx | 206 +-- .../src/components/modals/DeleteModal.tsx | 42 + .../src/components/modals/PasswordModal.tsx | 78 + .../src/components/modals/TrashListModal.tsx | 66 + .../src/components/note/NoteEditor.tsx | 64 + .../src/components/profile.component.tsx | 4 +- .../src/components/register.component.tsx | 298 ++-- .../src/components/sidebar/NoteTree.tsx | 199 +++ cyynote-frontend/src/lib/axios.ts | 30 + cyynote-frontend/src/main.tsx | 29 +- cyynote-frontend/src/pages/home/HomePage.tsx | 73 + cyynote-frontend/src/pages/note-page.tsx | 1528 ++++------------- cyynote-frontend/src/pages/sign-in.tsx | 202 +-- cyynote-frontend/src/services/auth-header.ts | 24 +- cyynote-frontend/src/services/auth.service.ts | 98 +- cyynote-frontend/src/services/note.service.ts | 100 +- cyynote-frontend/src/services/user.service.ts | 37 +- cyynote-frontend/src/stores/useAuthStore.ts | 29 + cyynote-frontend/src/stores/useNoteStore.ts | 34 + cyynote-frontend/src/stores/useUIStore.ts | 50 + cyynote-frontend/src/types/index.ts | 36 + cyynote-frontend/tsconfig.json | 11 +- cyynote-frontend/vite.config.ts | 64 +- 40 files changed, 1781 insertions(+), 2068 deletions(-) rename cyynote-backend/target/{cyynote-0.2.jar => cyynote.jar} (99%) create mode 100644 cyynote-frontend/.env.example create mode 100644 cyynote-frontend/.github/workflows/ci.yml create mode 100644 cyynote-frontend/.npmrc create mode 100644 cyynote-frontend/.nvmrc create mode 100644 cyynote-frontend/eslint.config.js create mode 100644 cyynote-frontend/src/components/ProtectedRoute.tsx create mode 100644 cyynote-frontend/src/components/modals/DeleteModal.tsx create mode 100644 cyynote-frontend/src/components/modals/PasswordModal.tsx create mode 100644 cyynote-frontend/src/components/modals/TrashListModal.tsx create mode 100644 cyynote-frontend/src/components/note/NoteEditor.tsx create mode 100644 cyynote-frontend/src/components/sidebar/NoteTree.tsx create mode 100644 cyynote-frontend/src/lib/axios.ts create mode 100644 cyynote-frontend/src/pages/home/HomePage.tsx create mode 100644 cyynote-frontend/src/stores/useAuthStore.ts create mode 100644 cyynote-frontend/src/stores/useNoteStore.ts create mode 100644 cyynote-frontend/src/stores/useUIStore.ts create mode 100644 cyynote-frontend/src/types/index.ts diff --git a/cyynote-backend/target/classes/com/cyynote/WebApp.class b/cyynote-backend/target/classes/com/cyynote/WebApp.class index 9a954359465e193ac4e660d2250493c46a2d6777..106080d39b810a45cd472509b23dffd0b435b5d9 100644 GIT binary patch delta 477 zcmZ9Hy;BoW6vclp?Cu+%JIO+m$*9X%AjyhEQ85@rqKF>^P>CA9T{JQ`yLHsjW6arwjhwbrUaZ^X@yjiq;rQ+fNv4F`LT@(_x%KIfRQU3ENF%G-JO z#OVKKp?X-TY`DNh&7?2hrLB6Zrqc6wPEX1Iijq#r>qb9Nn*vf?mfJfZ zHD$qmZ-6UYmFfu7T$8CO``J7BDVa?(aKLK}8IJbt1k6y8-gP;yl7he_QAIHEuE=8I zo&82G`p$6FX5=S@j)bDk`NL^UpK_L)%)x{x+!BQpx49$lkf6K5-DjCcJQnpAyb|6B Kdd&)N$^8SRK1}8S delta 456 zcmYk1J5L)y6otQ8>|JG8Y>N;iBm_c$h|Ob@nD>HthX4kXKp+W$XcH_1ST@3j7XHKb z`~#49h=PKOiVlg6I)4Lk?FJV!bLZT9&OPTe{20>r&o{R~AAipEf2FHY#BAE>@RLTp z>hT|pxBhX!j2;xWGtRuX=SGt|-l6AYyzG{H+9E%S&ANh27W-P1Fl;$(j z7GG>;n7xby0?JJ)-n}nlr~dAw{}tTmO4wvUW0A5MCJ`Yaz&Fhj%NE~@W>)-E@UHoX zLa(jna+~X_N-Kpulg+00Co*0>pUzvXDO!EcY|t&qTyFPZ&tl!*Fh^9}KQU|Dx+SD3 z;gCH&a@(akNUubPH}o@rLEL{bn~sXJAu39^g(gg887IOZuJnfF1eJV<=%Q~Cj0=kzVDe M;99C*{APo?7efh1_y7O^ diff --git a/cyynote-backend/target/classes/com/cyynote/controller/JwtInterceptor.class b/cyynote-backend/target/classes/com/cyynote/controller/JwtInterceptor.class index 86a47f7abeb25e0074380e62704298a470ad802d..0434ad96b2b3876a004e1b7d3dabfb7f7ad05794 100644 GIT binary patch delta 341 zcmXw!J4-@w7{-79bLt##kQ`8x=tg3e4U$37&^@z@v? zUYn=`Z}QzbVm}i>qpeDNnPVBU4Gdp0T4NHb3<|GSX6S-GuVzeLF{98h7aHcDy!bE6 z6WhUIiB68_;*@SG^iadcEq?Ck<&gkS^z%xP4fA`{%n)Or6lt1CvdlIaw0bT!*)nIH qcF3B2>y%>`r9pWLW&@QM_9)V1-T)Wc7mlz{zA5QAbs|#Q*M9-B^+;#{ delta 332 zcmXAl%}N4c6o#K~#%3HxDh5rYqM|TM5vpB_eyz+D>^!NM>9qy=iWTCh+_-khm^&A7Eu z2R`JJF>9WRh}GX>ut{B-o@lJi9JH7gL9I#bnu(xhE#9%L&}O}J`PWh%P3ArIMvWmF z3?mugoKdbA77{~wrH#0mK1`xCmwCDIEtgKd6}d7DwVScQ>#=>%0h~g zj3XTWnbWf1#xgh0=LIG&^zb69Ju^3TJSUjE#DhM()beE&q?EGEWVweEt%|JNq)8t7 zd3k6-R-==*hcdH$21B9Kat<;56*u>BzsXlUSkBk1!mhhI9}qKn%*#mPn{K|vw@tp| z;k$e<)S;V`eBU~z`=Evo-RQ)RJbacrJ8-9{eyRkYdANf+llcXaKDjDbS5rI7=e>Mt zc_ov1NZhHYsSnf*l)=|TvXW1ls7Bw)R9(#!e?Sesm#K=WC_o1Y zihd&?k^&h~p04~Y;AW-58fj%gX?t^;v>sc5(^)GcGRQhO89)OtM5Y`fbop9gZjw&? z+eP|qudNV%TU24OKD^?U0%mZAyq99YlC`gtf8|exh-XTFmYACzWv)3ZPirGURFcveH_Sk%&>OF^;uWq8O%)p^dw!jYkZNUlGG|#nL=+R}5Llc{`WJ zLNhqdIWuup1%vbi=a4SzMr>RgvvHl+xSowFq}Fn8h&9&8<9!V(@Z*q-=}6%XyipP| z1Os>zZ2DvxWTIUOhGWAV~sH6(N^i zm>gBusL8!3;XRTfha_Pro5Tkv#-b~7W!9CzmtrVBYz(s{pxyiLiceL9Yr|A4^QH@x zr+5EYJW&>tS7WVsRWpGjh>_6R|Fip&-gopo@#5lgJR+ zh5uJXkHr}5y51ILI0&mKKPb6cpI@MJl`oO+A*aOS5@iGDNbd=IGoms|QmJD=z4e?h z`t^%OBTAARmyNjdVEeyG4D6EA(6eLoY*;=vCO*&xoF(1F28hCfSAR#}R)>y{Ze*MRh!MaC1izcZw{qq;K|+ zi(`oP1-(ogx9_$z?vh2-UmACFkG!LfNEeLcGbroG_fL44hW{)x_b8V^GM`hQ2D*%U z=}TM=;jB)rylg8gLfD}`Amjv)*5>OYN6qsxGWbID>YFu3ROC{BkuQnzUYQ+@RK6_2 z*ZGDd;2=NX$Kl>5(o-{^>-J0Cex=)Qbo-rdf6(oZy8W5I%8FfNZ`bfQ{&7YQX7Eq- L;F39Fq24;Jy>91&( zYq_+T3xVv0*yl9}y`m1VYRt7X=GZx*J>&T-Dn``^3ozI(s>-TUr8$A?%$T?fw9 zy$IlP!`$Rbi;d(pZdCJHqGJc^kSAeYL$lUSUPnY!logbYn3y-AAd) zuCn-^a4LEJD7E@gw#tjg6--o%)3P;sd|uInGPU}da4Aw0g=+n)?8@3%T9mJrzgsI? zdHAQbqg5u)StnbyH4Ip>$?0~(Ad;<>En8UKk{s5s(mPLL>0M3N~h%F~{)72Kp$SSq6!l(8MlWOVx!Sk4Mrk&`(ECQDEW z^pq`|2<_flI5x^8^mJ!^I>o2pKP{@TIMr`)ngBC6UCu>FkYan9{8T(KBEDVbcZj0ReGF>1`{@xSTkBDR5l{c);{AUP{0xN>G1i~p6t|~0$%QF^fF6~-z~9; zRXKf)*ifN4ym3*rhzSp_M@Vov|M8^GAv!ENM2AFoPov*^#qWK*UvX*;OQ~V06?>7l zQZYXWPL#;Sd4LZ}Mtb6MKE#KGL10xdwpE<1`D}%IIEQlubsA>y5t-R=ACfqa^I>9+ zl(Hou--g(4UJ%Yzp2-{akW(NK7Fa zdrWj5=Tfm$Qe&p}q6C%O=1Lt@mI{EE1p<(nF90Q5#^ow?GFu_!+0cTmbx@{;tqRd6 zStw*J5-4ui&mvdgb?)5x(lBsQHVtl>m|NionHg4BD|KDPs5A%as!GpS@d*Xpx}xoL zLB6GJ8;!i1BmpQ2AeWgRIf}4BBTqpNpOh5YBnek@wZveeACizNyY?`A5r)Et4SsB4 z(ANCBKkzA)yHNjv$rTI*fvQ89yKK7j8S;RRo zbA#e--uZUh~#89Ov*crs|5v;T6Ws<8&8JRj)8Nz(Bbcq** zWdj3c_LA>1qA-Z0P;)?i^-LJ@rVnHzh$J;68zKGR<$ohFa7aqPP;4+8@iyJlmNerf zed%q+PPNQhjfbr)Z$@DquLf!{mmYI925rVHkJ*g9wll{-9sBDozi^;mczvkw2EXto zEmUWHYPGz%Z^w;NaH_A{goXxho5~*8jb7M;p{SP<+>3j$4-2qg{>!|E#W<*wqqw)drfnxTli! zB2xvDD|bZNAfmkWaF}ah+O~11AE}6aFXx3CkMC6%#l7N3eWJLJ`^Ajvj&>N%?iADV zm*xU2t9Pu=bkzl#hLf+UBN}vD0j)mnxaPIY+r}|9*tJR4(hy|y8E!9~=XF^bJQ#e> z``__zu;NWoJ|w%jh-8DDJIePYH=pttzx2>zYQ8Tc>V$P%;6mX diff --git a/cyynote-backend/target/cyynote-0.2.jar b/cyynote-backend/target/cyynote.jar similarity index 99% rename from cyynote-backend/target/cyynote-0.2.jar rename to cyynote-backend/target/cyynote.jar index e5f645a6e067e4a6d83fbbba93f2797d3539d3e3..68268185d62c3bf2a6867d778fad77b85bb5262e 100644 GIT binary patch delta 12420 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 J6~Kq}{{cGKcwhhk delta 12401 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 NBKUjh6LJ6B{{bntb#edz delta 9856 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 +QqpNXoV@ z^&uy4geY+h2#f6+pxFb)t{?u&BvJXTztr=8!7F0u5}3Z2LGE&y2lf2cRK_2tEB__Y z%c{R;f7my99^)Xj>(%X1wzLwC{Rw4><|XKtxNx5DVS)HeHDXp_#E!)7z(uIyM`dd2 z9>#3cT7gl^xh0Kdt4|8!jGBQWKTy`F&a>7mp*MSZwJb9{p|9Ked0XqnXq{F zbv^BYPnwfYOW-fC_!boo?P3cDb8SJt%W3y636;?4gPfE|^MHtWrTHfN&UQr5iV-6u zh!#(XOQj<}fYRk!&C|)Z&+@~zfzheLfFITf-Y9!@PA?$07<-%S2_A^rd861v~)$6DJZXu&fvC)8z*^Dd|&5t z&W9v-@Htqtww%W7ZB)?)b+!rk+*p&}OH03rKif~WcQ)&aR4oeCSh z4l~1eR^B9Q{BoB7@4;K1Vvh_huB+heLlDt5;!T2EZmFHIK!k%+N4-sOpnp`g0sC;V zf0e8$V402pXwJl{UJK8JC;4X;{2zsFIPeWEt`$Ab_b#QxWrp$fljJ2P;sy}tmnYb~ zBj5Bw@d|~u=(XsqyHZX+%*!TZcKXl;b@3KV^yw+WV*@FtE4R9dv-u0~z z%pe=8*r-m+ssI6|JRv4)X%tH-<`6+fkR(AAW|X{V<3!|<97ssb1~_&;jb~;)K9W=Q zY{ZL{<(k9;b-D2cM1b|J-JuaLKMi|1o#oc6Y@mt~7)0m`BVG-z}!NCdrC9FnpOD<1( zbW8kaLF{kh*+Upn1Yk|Ro6|pE(ti^%C~g&ZlONqJ(gRqir<}Lgf&t-8sQ;QP)n6g; z^8hNTG%U(;e{W;3nGl!=wmOHfdGP;b5PoAIeXA;6H7t$=BQ5-kgvol4>>=g`?mAu6=(3OU)qx~X+-bwZbbhGcVwapa|F-!I^F5@0J5HHE`mc4T{VVV|Qy?ym9$|lFK?js}ppEtp`V1!wF7|sazzs0{5 zuml6&xnY~54?FUre+fKsxF@0lP@D(AT*FZUr>TceP zu*ba26}GaJ08#9NJB&FNi~#3$i+GMefWK1$#P^nbo=AKr zDw_$*Cjmf5I1%9UpS%+V!vb%xNB`dP(MT|Yc2Jxl*}op3f2RAtcRu%q4=6?5XGNG$G6d{@ z*%`9p=#9em%K=uuzj-D6?~)F^_XYSNq5{JI$=20.0.0", + "pnpm": ">=9.0.0" + }, + "packageManager": "pnpm@9.15.0", "scripts": { "dev": "vite", - "build": "vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "build": "pnpm typecheck && vite build", + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { - "@alkhipce/editorjs-react": "^0.1.4", - "@blocknote/core": "^0.10.0", - "@blocknote/react": "^0.10.0", - "@douyinfe/semi-icons": "^2.48.0", - "@douyinfe/semi-icons-lab": "^2.48.0", - "@douyinfe/semi-ui": "^2.48.0", - "@editorjs/checklist": "^1.6.0", - "@editorjs/code": "^2.9.0", - "@editorjs/delimiter": "^1.4.0", - "@editorjs/editorjs": "^2.28.2", - "@editorjs/embed": "^2.7.0", - "@editorjs/header": "^2.8.1", - "@editorjs/image": "^2.9.0", - "@editorjs/inline-code": "^1.5.0", - "@editorjs/link": "^2.6.2", - "@editorjs/list": "^1.9.0", - "@editorjs/marker": "^1.4.0", - "@editorjs/paragraph": "^2.11.3", - "@editorjs/quote": "^2.6.0", - "@editorjs/raw": "^2.5.0", - "@editorjs/simple-image": "^1.6.0", - "@editorjs/table": "^2.3.0", - "@editorjs/warning": "^1.4.0", - "@excalidraw/excalidraw": "^0.17.2", - "@lexical/clipboard": "^0.12.5", - "@lexical/code": "^0.12.5", - "@lexical/file": "^0.12.5", - "@lexical/hashtag": "^0.12.5", - "@lexical/link": "^0.12.5", - "@lexical/list": "^0.12.5", - "@lexical/mark": "^0.12.5", - "@lexical/overflow": "^0.12.5", - "@lexical/plain-text": "^0.12.5", - "@lexical/react": "^0.12.5", - "@lexical/rich-text": "^0.12.5", - "@lexical/selection": "^0.12.5", - "@lexical/table": "^0.12.5", - "@lexical/utils": "^0.12.5", - "axios": "^1.6.2", - "editorjs-html": "^3.4.3", - "formik": "^2.4.5", - "html-react-parser": "^5.0.7", - "katex": "^0.16.9", - "lexical": "^0.12.5", - "prettier": "^3.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-editor-js": "^2.1.0", - "react-router-dom": "^6.20.1", - "sass": "^1.69.5", - "y-websocket": "^1.5.0", - "yjs": "^13.6.10", - "yup": "^1.3.2" + "@douyinfe/semi-icons": "2.96.0", + "@douyinfe/semi-icons-lab": "2.96.0", + "@douyinfe/semi-ui": "2.96.0", + "@excalidraw/excalidraw": "^0.17.6", + "@hookform/resolvers": "^3.10.0", + "@lexical/clipboard": "0.44.0", + "@lexical/code": "0.44.0", + "@lexical/file": "0.44.0", + "@lexical/hashtag": "0.44.0", + "@lexical/link": "0.44.0", + "@lexical/list": "0.44.0", + "@lexical/mark": "0.44.0", + "@lexical/overflow": "0.44.0", + "@lexical/plain-text": "0.44.0", + "@lexical/react": "0.44.0", + "@lexical/rich-text": "0.44.0", + "@lexical/selection": "0.44.0", + "@lexical/table": "0.44.0", + "@lexical/utils": "0.44.0", + "@lexical/yjs": "0.44.0", + "@tanstack/react-query": "^5.74.0", + "axios": "^1.8.0", + "html-react-parser": "^5.2.0", + "katex": "^0.16.21", + "lexical": "0.44.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.55.0", + "react-router": "^7.5.0", + "y-websocket": "^3.0.0", + "yjs": "^13.6.22", + "yup": "^1.6.0", + "zustand": "^5.0.3" }, "devDependencies": { - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", - "@vitejs/plugin-react": "^4.2.0", - "eslint": "^8.53.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.4", - "typescript": "^5.2.2", - "vite": "^5.0.0" + "@types/react": "^18.3.20", + "@types/react-dom": "^18.3.5", + "@typescript-eslint/eslint-plugin": "^8.30.0", + "@typescript-eslint/parser": "^8.30.0", + "@vitejs/plugin-react": "^4.4.0", + "eslint": "^9.24.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.0.0", + "prettier": "^3.5.0", + "sass": "^1.86.0", + "typescript": "5.8.3", + "typescript-eslint": "^8.30.0", + "vite": "^6.3.0", + "vitest": "^3.1.0" } } diff --git a/cyynote-frontend/src/App.tsx b/cyynote-frontend/src/App.tsx index ed88f7d..b302946 100644 --- a/cyynote-frontend/src/App.tsx +++ b/cyynote-frontend/src/App.tsx @@ -1,163 +1,37 @@ -import { Component } from "react"; -import { Routes, Route } from "react-router-dom"; -import "./App.css"; +import { lazy, Suspense } from 'react'; +import { Routes, Route, Navigate } from 'react-router'; +import './App.css'; +import ProtectedRoute from './components/ProtectedRoute'; -import AuthService from "./services/auth.service"; -import IUser from './types/user.type'; +const NotePage = lazy(() => import('./pages/note-page')); +const SignIn = lazy(() => import('./pages/sign-in')); +const Register = lazy(() => import('./components/register.component')); +const Profile = lazy(() => import('./components/profile.component')); +const BoardUser = lazy(() => import('./components/board-user.component')); +const BoardModerator = lazy(() => import('./components/board-moderator.component')); +const BoardAdmin = lazy(() => import('./components/board-admin.component')); -// import Login from "./components/login.component"; -import Register from "./components/register.component"; -// import Home from "./components/home.component"; -import Profile from "./components/profile.component"; -import BoardUser from "./components/board-user.component"; -import BoardModerator from "./components/board-moderator.component"; -import BoardAdmin from "./components/board-admin.component"; +export default function App() { + return ( + Loading…}> + + {/* Public routes */} + } /> + } /> + {/* Protected routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + -import NotePage from "./pages/note-page.tsx"; -import EventBus from "./common/EventBus"; -import SignIn from "./pages/sign-in.tsx"; -import Test from "./pages/test.tsx"; -// "build": "tsc && vite build", - - -type Props = {}; - -type State = { - showModeratorBoard: boolean, - showAdminBoard: boolean, - currentUser: IUser | undefined + {/* Fallback */} + } /> + + + ); } - -class App extends Component { - constructor(props: Props) { - super(props); - this.logOut = this.logOut.bind(this); - - this.state = { - showModeratorBoard: false, - showAdminBoard: false, - currentUser: undefined, - }; - } - - componentDidMount() { - const user = AuthService.getCurrentUser(); - - if (user) { - this.setState({ - currentUser: user, - showModeratorBoard: user.roles.includes("ROLE_MODERATOR"), - showAdminBoard: user.roles.includes("ROLE_ADMIN"), - }); - } - - EventBus.on("logout", this.logOut); - } - - componentWillUnmount() { - EventBus.remove("logout", this.logOut); - } - - logOut() { - AuthService.logout(); - this.setState({ - showModeratorBoard: false, - showAdminBoard: false, - currentUser: undefined, - }); - } - - render() { - // const { currentUser, showModeratorBoard, showAdminBoard } = this.state; - - return ( -
- {/**/} - -
- - } /> - } /> - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - -
- - { /* */} -
- ); - } -} - -export default App; diff --git a/cyynote-frontend/src/components/ProtectedRoute.tsx b/cyynote-frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..cdbbd0d --- /dev/null +++ b/cyynote-frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,8 @@ +import { Navigate, Outlet } from 'react-router'; +import { useAuthStore } from '../stores/useAuthStore'; + +/** Wraps routes that require authentication. Redirects to /login if not signed in. */ +export default function ProtectedRoute() { + const user = useAuthStore((s) => s.user); + return user ? : ; +} diff --git a/cyynote-frontend/src/components/lexical/collaboration.ts b/cyynote-frontend/src/components/lexical/collaboration.ts index 618ebd5..b79a7ad 100644 --- a/cyynote-frontend/src/components/lexical/collaboration.ts +++ b/cyynote-frontend/src/components/lexical/collaboration.ts @@ -1,23 +1,22 @@ /** - * Copyright (c) Meta Platforms, Inc. and affiliates. + * WebSocket collaboration provider factory for Lexical. * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. + * Configuration via environment variables: + * VITE_WS_URL – WebSocket server URL (default: ws://localhost:1234) * + * See .env.example for a full list of supported variables. */ -import {Provider} from '@lexical/yjs'; -import {WebsocketProvider} from 'y-websocket'; -import {Doc} from 'yjs'; +import type { Provider } from '@lexical/yjs'; +import { WebsocketProvider } from 'y-websocket'; +import { Doc } from 'yjs'; -const url = new URL(window.location.href); -const params = new URLSearchParams(url.search); const WEBSOCKET_ENDPOINT = - params.get('collabEndpoint') || 'ws://localhost:1234'; -const WEBSOCKET_SLUG = 'playground'; -const WEBSOCKET_ID = params.get('collabId') || '0'; + (import.meta.env.VITE_WS_URL as string | undefined) ?? 'ws://localhost:1234'; + +// Use a project-specific slug — not the Meta playground default. +const WEBSOCKET_SLUG = 'cyynote'; -// parent dom -> child doc export function createWebsocketProvider( id: string, yjsDocMap: Map, @@ -31,13 +30,11 @@ export function createWebsocketProvider( doc.load(); } - // @ts-ignore return new WebsocketProvider( WEBSOCKET_ENDPOINT, - WEBSOCKET_SLUG + '/' + WEBSOCKET_ID + '/' + id, + `${WEBSOCKET_SLUG}/${id}`, doc, - { - connect: false, - }, - ); + { connect: false }, + ) as unknown as Provider; } + diff --git a/cyynote-frontend/src/components/login.component.tsx b/cyynote-frontend/src/components/login.component.tsx index 32de014..8abfb94 100644 --- a/cyynote-frontend/src/components/login.component.tsx +++ b/cyynote-frontend/src/components/login.component.tsx @@ -1,151 +1,69 @@ -import { Component } from "react"; -import { Navigate } from "react-router-dom"; -import { Formik, Field, Form, ErrorMessage } from "formik"; -import * as Yup from "yup"; +/** + * @deprecated Use src/pages/sign-in.tsx instead. + * This file is kept for reference and is not used in the router. + */ +import { useState } from 'react'; +import type { FormEvent } from 'react'; +import { Navigate } from 'react-router'; +import authService from '../services/auth.service'; +import { useAuthStore } from '../stores/useAuthStore'; -import AuthService from "../services/auth.service"; +export default function Login() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [message, setMessage] = useState(''); + const [redirect, setRedirect] = useState(null); -type Props = {}; + const user = useAuthStore((s) => s.user); + if (user || redirect) return ; -type State = { - redirect: string | null, - username: string, - password: string, - loading: boolean, - message: string -}; - -export default class Login extends Component { - constructor(props: Props) { - super(props); - this.handleLogin = this.handleLogin.bind(this); - - this.state = { - redirect: null, - username: "", - password: "", - loading: false, - message: "" - }; - } - - componentDidMount() { - const currentUser = AuthService.getCurrentUser(); - - if (currentUser) { - this.setState({ redirect: "/profile" }); - }; - } - - componentWillUnmount() { - window.location.reload(); - } - - validationSchema() { - return Yup.object().shape({ - username: Yup.string().required("This field is required!"), - password: Yup.string().required("This field is required!"), - }); - } - - handleLogin(formValue: { username: string; password: string }) { - const { username, password } = formValue; - - this.setState({ - message: "", - loading: true - }); - - - AuthService.login(username, password).then( - () => { - this.setState({ - redirect: "/profile" - }); - }, - error => { - const resMessage = - (error.response && - error.response.data && - error.response.data.message) || - error.message || - error.toString(); - - this.setState({ - loading: false, - message: resMessage - }); - } - ); - } - - render() { - if (this.state.redirect) { - return + const handleLogin = async (e: FormEvent) => { + e.preventDefault(); + setMessage(''); + try { + await authService.login(username, password); + setRedirect('/home'); + } catch (error: unknown) { + setMessage(error instanceof Error ? error.message : '登录失败'); } + }; - const { loading, message } = this.state; - - const initialValues = { - username: "", - password: "", - }; - - return ( -
-
- profile-img - - -
-
- - - -
- -
- - - -
- -
- -
- - {message && ( -
-
- {message} -
-
- )} -
-
-
+ return ( +
+
+
void handleLogin(e)}> +
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ {message && ( +
+ {message} +
+ )} +
- ); - } +
+ ); } diff --git a/cyynote-frontend/src/components/modals/DeleteModal.tsx b/cyynote-frontend/src/components/modals/DeleteModal.tsx new file mode 100644 index 0000000..31cbf7e --- /dev/null +++ b/cyynote-frontend/src/components/modals/DeleteModal.tsx @@ -0,0 +1,42 @@ +import { Modal, Toast } from '@douyinfe/semi-ui'; +import { useUIStore } from '../../stores/useUIStore'; +import { useNoteStore } from '../../stores/useNoteStore'; +import noteService from '../../services/note.service'; + +interface DeleteModalProps { + onDeleted: () => void; +} + +export default function DeleteModal({ onDeleted }: DeleteModalProps) { + const { deleteVisible, deleteId, deleteNoteName, closeDeleteConfirm } = useUIStore(); + const clearCurrentNote = useNoteStore((s) => s.clearCurrentNote); + + const handleOk = () => { + if (deleteId === null) return; + noteService.delete(deleteId).then( + () => { + Toast.info(`已删除:${deleteNoteName}`); + closeDeleteConfirm(); + clearCurrentNote(); + onDeleted(); + }, + () => { + Toast.error('删除失败,请重试'); + closeDeleteConfirm(); + }, + ); + }; + + return ( + + 确认要删除「{deleteNoteName}」吗?删除后可在回收站恢复。 + + ); +} diff --git a/cyynote-frontend/src/components/modals/PasswordModal.tsx b/cyynote-frontend/src/components/modals/PasswordModal.tsx new file mode 100644 index 0000000..f3f6b3a --- /dev/null +++ b/cyynote-frontend/src/components/modals/PasswordModal.tsx @@ -0,0 +1,78 @@ +import { Modal, Form, Button, Toast } from '@douyinfe/semi-ui'; +import { useAuthStore } from '../../stores/useAuthStore'; +import { useUIStore } from '../../stores/useUIStore'; +import authService from '../../services/auth.service'; + +export default function PasswordModal() { + const { updatePasswordModelVisible, setUpdatePasswordModelVisible } = useUIStore(); + const user = useAuthStore((s) => s.user); + + const handleSubmit = (values: { + username: string; + oldpassword: string; + newpassword: string; + }) => { + authService + .updatePassword(values.username, values.oldpassword, values.newpassword) + .then( + () => { + Toast.success('密码修改成功'); + setUpdatePasswordModelVisible(false); + }, + (error: unknown) => { + const msg = + error instanceof Error ? error.message : '修改失败,请重试'; + Toast.error(msg); + }, + ); + }; + + return ( + setUpdatePasswordModelVisible(false)} + closeOnEsc + > +
+
+ handleSubmit( + values as { username: string; oldpassword: string; newpassword: string }, + ) + } + style={{ textAlign: 'center', width: 400 }} + initValues={{ username: user?.username ?? '' }} + > + + + +
+ +
+ +
+
+ ); +} diff --git a/cyynote-frontend/src/components/modals/TrashListModal.tsx b/cyynote-frontend/src/components/modals/TrashListModal.tsx new file mode 100644 index 0000000..93ea6d3 --- /dev/null +++ b/cyynote-frontend/src/components/modals/TrashListModal.tsx @@ -0,0 +1,66 @@ +import { Modal, List, Avatar, Button, ButtonGroup, Toast, Typography } from '@douyinfe/semi-ui'; +import { useUIStore } from '../../stores/useUIStore'; +import { useNoteStore } from '../../stores/useNoteStore'; +import noteService from '../../services/note.service'; +import type { INote } from '../../types'; + +interface TrashListModalProps { + trashList: INote[]; + onRestored: () => void; +} + +export default function TrashListModal({ trashList, onRestored }: TrashListModalProps) { + const { deleteModelVisible, setDeleteModelVisible } = useUIStore(); + + const handleRestore = (id: number) => { + noteService.restoreFromTrash(id).then( + () => { + Toast.success('已从回收站恢复'); + setDeleteModelVisible(false); + onRestored(); + }, + () => Toast.error('恢复失败,请重试'), + ); + }; + + return ( + setDeleteModelVisible(false)} + closeOnEsc + > +
+ ( + NOTE} + main={ +
+ + {item.title} + + + 创建时间:{item.createtime},更新时间:{item.updatetime}, + 最后浏览时间:{item.viewtime}。 + +
+ } + extra={ + + + + } + /> + )} + /> +
+
+ ); +} diff --git a/cyynote-frontend/src/components/note/NoteEditor.tsx b/cyynote-frontend/src/components/note/NoteEditor.tsx new file mode 100644 index 0000000..0bb827c --- /dev/null +++ b/cyynote-frontend/src/components/note/NoteEditor.tsx @@ -0,0 +1,64 @@ +import { useCallback } from 'react'; +import { Input, Skeleton, Breadcrumb } from '@douyinfe/semi-ui'; +import { useNoteStore } from '../../stores/useNoteStore'; +import { useUIStore } from '../../stores/useUIStore'; +import PlaygroundApp from '../lexical/LexicalApp'; + +export default function NoteEditor() { + const { currentNoteId, currentTitle, currentContent, setTitle } = useNoteStore(); + const { isNoteLoading } = useUIStore(); + + const handleTitleChange = useCallback( + (value: string) => { + setTitle(value); + }, + [setTitle], + ); + + // handleRefreshTree is intentionally a no-op here: + // LexicalApp calls it after structural changes, but tree refresh + // is driven by the NoteStore and handled at the NotePage level. + const handleRefreshTree = useCallback(() => { + // no-op: tree is refreshed by NotePage via store subscription + }, []); + + return ( + <> + +
+ } + loading={isNoteLoading} + > + +
+
+ {currentNoteId !== null && currentContent !== null ? ( + + ) : ( +
请从左侧选择笔记
+ )} +
+
+ + ); +} diff --git a/cyynote-frontend/src/components/profile.component.tsx b/cyynote-frontend/src/components/profile.component.tsx index 9043fb5..cda8bea 100644 --- a/cyynote-frontend/src/components/profile.component.tsx +++ b/cyynote-frontend/src/components/profile.component.tsx @@ -1,7 +1,7 @@ import { Component } from "react"; -import { Navigate } from "react-router-dom"; +import { Navigate } from 'react-router'; import AuthService from "../services/auth.service"; -import IUser from "../types/user.type"; +import type { IUser } from '../types'; type Props = {}; diff --git a/cyynote-frontend/src/components/register.component.tsx b/cyynote-frontend/src/components/register.component.tsx index e0919eb..85369ef 100644 --- a/cyynote-frontend/src/components/register.component.tsx +++ b/cyynote-frontend/src/components/register.component.tsx @@ -1,178 +1,126 @@ -import { Component } from "react"; -import { Formik, Field, Form, ErrorMessage } from "formik"; -import * as Yup from "yup"; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as Yup from 'yup'; +import authService from '../services/auth.service'; -import AuthService from "../services/auth.service"; +const schema = Yup.object().shape({ + username: Yup.string() + .min(3, '用户名最少 3 个字符') + .max(20, '用户名最多 20 个字符') + .required('请输入用户名'), + email: Yup.string().email('邮箱格式不正确').required('请输入邮箱'), + password: Yup.string() + .min(6, '密码最少 6 个字符') + .max(40, '密码最多 40 个字符') + .required('请输入密码'), +}); -type Props = {}; - -type State = { - username: string, - email: string, - password: string, - successful: boolean, - message: string -}; - -export default class Register extends Component { - constructor(props: Props) { - super(props); - this.handleRegister = this.handleRegister.bind(this); - - this.state = { - username: "", - email: "", - password: "", - successful: false, - message: "" - }; - } - - validationSchema() { - return Yup.object().shape({ - username: Yup.string() - .test( - "len", - "The username must be between 3 and 20 characters.", - (val: any) => - val && - val.toString().length >= 3 && - val.toString().length <= 20 - ) - .required("This field is required!"), - email: Yup.string() - .email("This is not a valid email.") - .required("This field is required!"), - password: Yup.string() - .test( - "len", - "The password must be between 6 and 40 characters.", - (val: any) => - val && - val.toString().length >= 6 && - val.toString().length <= 40 - ) - .required("This field is required!"), - }); - } - - handleRegister(formValue: { username: string; email: string; password: string }) { - const { username, email, password } = formValue; - - this.setState({ - message: "", - successful: false - }); - - AuthService.register( - username, - email, - password - ).then( - response => { - this.setState({ - message: response.data.message, - successful: true - }); - }, - error => { - const resMessage = - (error.response && - error.response.data && - error.response.data.message) || - error.message || - error.toString(); - - this.setState({ - successful: false, - message: resMessage - }); - } - ); - } - - render() { - const { successful, message } = this.state; - - const initialValues = { - username: "", - email: "", - password: "", - }; - - return ( -
-
- profile-img - - -
- {!successful && ( -
-
- - - -
- -
- - - -
- -
- - - -
- -
- -
-
- )} - - {message && ( -
-
- {message} -
-
- )} -
-
-
-
- ); - } +interface RegisterForm { + username: string; + email: string; + password: string; +} + +export default function Register() { + const [message, setMessage] = useState(''); + const [successful, setSuccessful] = useState(false); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ resolver: yupResolver(schema) }); + + const onSubmit = async (data: RegisterForm) => { + setMessage(''); + setSuccessful(false); + try { + await authService.register(data.username, data.email, data.password); + setSuccessful(true); + setMessage('注册成功!请登录。'); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : '注册失败,请重试'; + setSuccessful(false); + setMessage(msg); + } + }; + + return ( +
+
+ profile-img + +
void handleSubmit(onSubmit)(e)}> + {!successful && ( +
+
+ + + {errors.username && ( +
{errors.username.message}
+ )} +
+ +
+ + + {errors.email && ( +
{errors.email.message}
+ )} +
+ +
+ + + {errors.password && ( +
{errors.password.message}
+ )} +
+ +
+ +
+
+ )} + + {message && ( +
+
+ {message} +
+
+ )} +
+
+
+ ); } diff --git a/cyynote-frontend/src/components/sidebar/NoteTree.tsx b/cyynote-frontend/src/components/sidebar/NoteTree.tsx new file mode 100644 index 0000000..35db517 --- /dev/null +++ b/cyynote-frontend/src/components/sidebar/NoteTree.tsx @@ -0,0 +1,199 @@ +import { useCallback } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import { + Tree, + List, + Input, + Button, + ButtonGroup, + Dropdown, + Typography, + Toast, +} from '@douyinfe/semi-ui'; +import { + IconCopyAdd, + IconDelete, + IconSearch, +} from '@douyinfe/semi-icons'; +import { IconModal, IconToken as LabIconToken, IconTreeSelect } from '@douyinfe/semi-icons-lab'; +import { useNoteStore } from '../../stores/useNoteStore'; +import { useUIStore } from '../../stores/useUIStore'; +import noteService from '../../services/note.service'; +import type { INote, ITreeNode } from '../../types'; + +const NAV_ITEMS = ['首页', '设置', '已删除'] as const; +type NavItem = (typeof NAV_ITEMS)[number]; + +interface NoteTreeProps { + onRefreshTree: () => void; + onNavItemClick: (item: NavItem) => void; + onSearch: () => void; +} + +export default function NoteTree({ onRefreshTree, onNavItemClick, onSearch }: NoteTreeProps) { + const { treeData, viewData, currentNoteId, setCurrentNote } = useNoteStore(); + const { openDeleteConfirm, setSearchQuery, searchQuery, setActiveView } = useUIStore(); + + const loadNote = useCallback( + (id: number) => { + noteService.get(id).then( + (res) => { + const note = res.data; + setCurrentNote(note.id, note.title, note.context); + setActiveView('note'); + }, + () => Toast.error('加载笔记失败'), + ); + }, + [setCurrentNote, setActiveView], + ); + + const addNote = useCallback( + (pid: number) => { + noteService.add(pid).then( + () => { + Toast.success('已添加'); + onRefreshTree(); + }, + () => Toast.error('添加失败'), + ); + }, + [onRefreshTree], + ); + + const treeStyle: CSSProperties = { + height: 550, + border: '1px solid var(--semi-color-border)', + position: 'sticky', + top: '85px', + }; + + const renderLabel = (label: ReactNode, item: ITreeNode) => ( +
+ + {label} + + +
+ ); + + return ( + <> +
+
+ } + showClear + value={searchQuery} + onChange={(v) => setSearchQuery(v)} + onEnterPress={onSearch} + /> +
+ +

+ CYY Note +

+ + ( +
onNavItemClick(item)} + > + {item === '首页' &&
+ )} + /> + +
+ +    + + 最近浏览 + + {viewData.map((item: INote) => ( + loadNote(item.id)} + > + {item.title} + + ))} + + } + > + + +
+ + [0]['treeData']} + renderLabel={renderLabel as Parameters[0]['renderLabel']} + style={treeStyle} + onSelect={(id) => loadNote(id as unknown as number)} + /> + + ); +} diff --git a/cyynote-frontend/src/lib/axios.ts b/cyynote-frontend/src/lib/axios.ts new file mode 100644 index 0000000..2b1f064 --- /dev/null +++ b/cyynote-frontend/src/lib/axios.ts @@ -0,0 +1,30 @@ +import axios from 'axios'; +import { useAuthStore } from '../stores/useAuthStore'; + +const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL ?? '/api', + timeout: 30_000, +}); + +axiosInstance.interceptors.request.use((config) => { + const { token } = useAuthStore.getState(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +axiosInstance.interceptors.response.use( + (response) => response, + (error: unknown) => { + if ( + axios.isAxiosError(error) && + error.response?.status === 401 + ) { + useAuthStore.getState().logout(); + } + return Promise.reject(error); + }, +); + +export default axiosInstance; diff --git a/cyynote-frontend/src/main.tsx b/cyynote-frontend/src/main.tsx index 9f1aab0..64c49c5 100644 --- a/cyynote-frontend/src/main.tsx +++ b/cyynote-frontend/src/main.tsx @@ -1,28 +1,15 @@ -// import React from 'react' -// import ReactDOM from 'react-dom/client' -// import App from './App.tsx' -// import './index.css' -// -// ReactDOM.createRoot(document.getElementById('root')!).render( -// -// -// , -// ) - - - +import React from 'react'; import ReactDOM from 'react-dom/client'; -import { BrowserRouter } from "react-router-dom"; +import { BrowserRouter } from 'react-router'; import './index.css'; import App from './App'; -const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + , ); -root.render( - - - -); diff --git a/cyynote-frontend/src/pages/home/HomePage.tsx b/cyynote-frontend/src/pages/home/HomePage.tsx new file mode 100644 index 0000000..a8ab501 --- /dev/null +++ b/cyynote-frontend/src/pages/home/HomePage.tsx @@ -0,0 +1,73 @@ +import { + List, + Avatar, + Button, + ButtonGroup, + Typography, + Tabs, + TabPane, + Toast, +} from '@douyinfe/semi-ui'; +import { useNoteStore } from '../../stores/useNoteStore'; +import noteService from '../../services/note.service'; +import type { INote } from '../../types'; + +export default function HomePage() { + const { homeData, setHomeData, setCurrentNote } = useNoteStore(); + + const loadNote = (id: number) => { + noteService.get(id).then( + (res) => { + const note = res.data; + setCurrentNote(note.id, note.title, note.context); + }, + () => Toast.error('加载笔记失败'), + ); + }; + + const handleTabChange = (key: string) => { + noteService.getHomeData(key, '0').then( + (res) => setHomeData(res.data), + () => Toast.error('加载列表失败'), + ); + }; + + return ( + <> + + + + + +
+ ( + NOTE} + main={ +
+ + {item.title} + + + 创建时间:{item.createtime},更新时间:{item.updatetime}, + 最后浏览时间:{item.viewtime}。 + +
+ } + extra={ + + + + } + /> + )} + /> +
+ + ); +} diff --git a/cyynote-frontend/src/pages/note-page.tsx b/cyynote-frontend/src/pages/note-page.tsx index 6eaeac9..c465fa1 100644 --- a/cyynote-frontend/src/pages/note-page.tsx +++ b/cyynote-frontend/src/pages/note-page.tsx @@ -1,1154 +1,374 @@ -import { Component } from "react"; -import { Navigate } from "react-router-dom"; -import AuthService from "../services/auth.service"; -import IUser from "../types/user.type"; -import { - Layout, - Nav, - Button, - Breadcrumb, - Skeleton, - Avatar, - List, - Tree, - Input, - Typography, - Toast, - ButtonGroup, Form, - SideSheet, Tabs, TabPane, BackTop, Modal, Dropdown, Tag, - Row -} from '@douyinfe/semi-ui'; - -import { - IconBell, - IconBytedanceLogo, - IconSearch, - IconExit, IconArrowUp, IconMenu, IconCopyAdd, IconDelete, IconMinusCircle, IconCode -} from '@douyinfe/semi-icons'; -import { IconDescriptions, IconModal, IconToken, IconTreeSelect } from "@douyinfe/semi-icons-lab"; - -import "./styles.css"; - - -import NoteService from "../services/note.service"; - -import PlaygroundApp from "../components/lexical/LexicalApp.tsx"; - - -type Props = NonNullable; - -type State = { - redirect: string | null, - showSettings: boolean, - showNote: boolean, - showHome: boolean, - showSearch: boolean, - searchData: string, - userReady: boolean, - sideVisible: boolean, - siderFlag: boolean, - siderWidth: string, - skeletonLoad: boolean, - noteTitle: string, - noteId: bigint, - editData: null, - isEditMode: boolean, - isNoteShow: boolean, - treeNodeData: object, - dataHome: object, - deleteListData: object, - deleteVisible: boolean, - deleteId: bigint, - treeValue: bigint, - dataView: object, - deleteModelVisible: boolean, - updatePasswordModelVisible: boolean, - currentUser: IUser & { accessToken: string } -} - -export default class NotePage extends Component { - - constructor(props: Props) { - super(props); - this.handleLawClick = this.handleLawClick.bind(this) - this.state = { - showSettings: false, - showNote: false, - showHome: true, - showSearch: false, - searchData: '', - redirect: null, - userReady: false, - sideVisible: false, - siderFlag: true, - siderWidth: '265px', - skeletonLoad: false, - isEditMode: false, - noteTitle: '', - noteId: null, - editData: null, - isNoteShow: true, - treeNodeData: [], - dataHome: [], - dataView: [], - deleteListData: [], - deleteVisible: false, - deleteId: '', - treeValue: null, - deleteModelVisible: false, - updatePasswordModelVisible: false, - currentUser: { accessToken: "" } - }; - } - - componentDidMount() { - const currentUser = AuthService.getCurrentUser(); - - console.log(currentUser) - if (!currentUser) { - this.setState({ redirect: "/login" }); - return; - } - this.setState({ currentUser: currentUser, userReady: true }) - - - NoteService.getAllContent().then( - response => { - // console.log(response.data.data) - - const listData = response.data.data - this.setState({ - treeNodeData: listData - }); - }, - error => { - console.log(error) - Toast.info('鉴权失败'); - this.logOut() - // return; - } - ); - - NoteService.dataHome('1', '0').then( - response => { - // console.log(response.data.data); - const data = response.data.data - console.log(data); - this.setState({ - dataHome: data, - }) - }, - error => { - console.log(error) - } - ); - NoteService.dataHome('3', '0').then( - response => { - const data = response.data.data - console.log(data); - const viewdata = data.slice(0, -10); - this.setState({ - dataView: viewdata, - }) - }, - error => { - console.log(error) - } - ); - - } - - logOut = () => { - console.log('logout') - AuthService.logout(); - - AuthService.logoutApi().then( - response => { - - console.log(response) - this.setState({ - currentUser: undefined - }); - console.log('logout2') - this.setState({ redirect: "/login" }) - }, - error => { - console.log(error) - this.setState({ redirect: "/login" }) - } - ); - - } - - - handleLawClick = (val) => { - console.log('handleLawClick') - console.log(val) - NoteService.getAllContent().then( - response => { - // console.log(response.data.data) - const listData = response.data.data - this.setState({ - treeNodeData: listData - }); - }, - error => { - console.log(error) - } - ); - } - - - handleUpdateSubmit = (values: { username: string; oldpassword: string; newpassword: string }) => { - console.log(values); - // Toast.info('表单已提交'); - - AuthService.updatePassword(values.username,values.oldpassword, values.newpassword).then( - () => { - Toast.info('修改成功'); - this.setState({ - updatePasswordModelVisible:false - }); - }, - error => { - const resMessage = - (error.response && - error.response.data && - error.response.data.message) || - error.message || - error.toString(); - Toast.info(resMessage); - } - ); - }; - - - render() { - const { Header, Footer, Sider, Content } = Layout; - - if (this.state.redirect) { - return - } - - const { - currentUser, siderWidth, siderFlag, isNoteShow, - sideVisible, skeletonLoad, treeNodeData, noteTitle, noteId, - showHome, showNote, showSettings, showSearch, searchData, editData, - dataHome, dataView, deleteVisible, deleteId, deleteModelVisible, deleteListData, updatePasswordModelVisible - } = this.state; - - const data = [ - '首页', - '设置', - '已删除', - ]; - - const sidechange = () => { - console.log(this.state.noteId) - this.setState({ - sideVisible: !sideVisible, - treeValue: this.state.noteId - }); - }; - - const treeButton = (value) => ( - - - - - {/**/} - {/* Menu Item 1*/} - {/* Menu Item 2*/} - {/* Menu Item 3*/} - {/* */} - {/* }*/} - {/*>*/} - {/* 更多*/} - {/**/} - - - ); - - const handleDeleteOk = () => { - console.log('handleOk'); - deleteNote(this.state.deleteId); - console.log(this.state.deleteId) - this.setState({ deleteVisible: false, }); - } - - const handleDeleteAfterClose = () => { - console.log('handleAfterClose'); - this.setState({ deleteVisible: false, }); - } - - const handleDeleteCancel = () => { - console.log('onCancel'); - this.setState({ deleteVisible: false, }); - } - - const handleDeleteModelOk = () => { - console.log('handleDeleteModelOk'); - this.setState({ deleteModelVisible: false, }); - } - - const handleDeleteModelAfterClose = () => { - console.log('handleDeleteModelAfterClose'); - this.setState({ deleteModelVisible: false, }); - } - - const handleDeleteModelCancel = () => { - console.log('handleDeleteModelCancel'); - this.setState({ deleteModelVisible: false, }); - } - - const handleUpdatePasswordModelOk = () => { - console.log('handleupdatePasswordModelOk'); - this.setState({ updatePasswordModelVisible: false, }); - } - - const handleUpdatePasswordModelAfterClose = () => { - console.log('handleupdatePasswordModelAfterClose'); - this.setState({ updatePasswordModelVisible: false, }); - } - - const handleUpdatePasswordModelCancel = () => { - console.log('handleupdatePasswordModelCancel'); - this.setState({ updatePasswordModelVisible: false, }); - } - - - - const topStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: 30, - width: 30, - borderRadius: '100%', - backgroundColor: '#0077fa', - color: '#fff', - bottom: 65, - right: 20, - }; - - - const treeDataStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center' - }; - - const renderLabel = (label, item) => ( -
- - {label} - - {/*{label}*/} - {treeButton(item.key)} -
- ); - const onTreeSelect = (e) => { - console.log('当前选项: ', e) - this.setState({ - isNoteShow: false - }); - NoteService.get(e).then( - response => { - // console.log(response.data.data) - const data = response.data.data - // console.log(data) - this.setState({ - showSettings: false, - showNote: true, - showHome: false, - showSearch: false, - noteId: data.id, - editData: data.context, - noteTitle: data.title, - isNoteShow: true, - treeValue: data.id, - }); - }, - error => { - console.log(error) - } - ); - console.log(e); - }; - - - const treeDataWithNode = treeNodeData; - - console.log(treeDataWithNode) - - const treeStyle = { - // width: 260, - height: 550, - border: '1px solid var(--semi-color-border)', - position: 'sticky', - top: '85px', - }; - - const onbreakpoint = (screen, bool) => { - console.log(screen, bool); - - if (!bool) { - this.setState({ - siderFlag: false, - siderWidth: '0px' - }); - } else { - this.setState({ - siderFlag: true, - siderWidth: '265px' - }); - } - - }; - - const inputOnChange = (value, e) => { - console.log(value); - this.setState({ - noteTitle: value, - }); - } - - const itemClick = (e, item) => { - console.log(e); - console.log(e.item); - console.log(item); - console.log('itemClick'); - Toast.info('菜单选择:' + item); - if (item === '首页') { - this.setState({ - showSettings: false, - showNote: false, - showHome: true, - showSearch: false, - }); - } else if (item === '设置') { - this.setState({ - showSettings: true, - showNote: false, - showHome: false, - showSearch: false, - }); - - } else if (item === '已删除') { - console.log('已删除'); - this.setState({ - deleteModelVisible: true, - }); - - NoteService.dataHome('4', '0').then( - response => { - // console.log(response.data.data); - const data = response.data.data - // console.log(data); - this.setState({ - deleteListData: data, - }) - }, - error => { - console.log(error) - } - ); - } - }; - const homeDataClick = (e, item) => { - console.log(e); - console.log(e.item); - console.log(item); - console.log('homeDataClick'); - // Toast.info('查看:' + item); - onTreeSelect(item); - }; - - const deleteNote = (id) => { - console.log('deleteNote'); - NoteService.delete(id).then( - response => { - console.log(response.data.data); - const data = response.data.data - console.log(data); - Toast.info('删除:' + data); - this.handleLawClick(); - }, - error => { - console.log(error) - } - ); - } - - const addNote = (pid) => { - console.log('addNote'); - console.log(pid); - NoteService.add(pid).then( - response => { - console.log(response.data.data); - const data = response.data.data - console.log(data); - Toast.info('添加:' + data); - this.handleLawClick(); - }, - error => { - console.log(error) - } - ); - } - - - const updatePassword = () => { - console.log('updatePassword'); - this.setState({updatePasswordModelVisible: true,}); - } - - - const historyClick = (flag) => { - console.log('historyClick'); - console.log(flag); - - NoteService.historyQueryOrDelete(flag).then( - response => { - console.log("data:"+response.data.data) - Toast.info("historyCount:"+response.data.data); - }, - error => { - console.log(error) - } - ); - - } - - - const dropdownClick = (id) => { - console.log('dropdownClick'); - console.log(id); - console.log('dropdownClick============='); - onTreeSelect(id); - } - const search = () => { - console.log('search'); - console.log(this.state.searchData); - - this.setState({ - showSettings: false, - showNote: false, - showHome: false, - showSearch: true, - }); - - NoteService.dataHome('0', this.state.searchData).then( - response => { - console.log(response.data.data); - const data = response.data.data - console.log(data); - this.setState({ - dataHome: data, - }) - }, - error => { - console.log(error) - } - ); - - } - - const onTabSelect = (e) => { - console.log(e); - console.log('onTabSelect'); - - NoteService.dataHome(e, '0').then( - response => { - console.log(response.data.data); - const data = response.data.data - console.log(data); - this.setState({ - dataHome: data, - }) - }, - error => { - console.log(error) - } - ); - - }; - const deleteListDataClick = (e, item) => { - console.log(e); - console.log(e.item); - console.log(item); - console.log('deleteListDataClick'); - - NoteService.deleteBack(item).then( - response => { - console.log(response.data.data) - Toast.info('已恢复:' + response.data.data); - this.handleLawClick(0) - this.setState({ deleteModelVisible: false, }); - }, - error => { - console.log(error) - } - ); - }; - - - return ( - -
-
- -
-
- - - - {siderFlag ? ( - <> -

-
- } showClear onEnterPress={() => search()} > -
- -

CYY Note

- -
itemClick(e, item)}> - {/*
- } - - // renderItem={item => itemClick(e, item)}>{item}} - /> - -
- -    - - 最近浏览 - - { - this.state.dataView.map((item, key) => { - return ( - dropdownClick(item.id)}>{item.title} - ) - }) - } - - } - > - - -
- ( - - )} - showFilteredOnly={true} - directory - value={this.state.treeValue} - treeData={treeDataWithNode} - renderLabel={renderLabel} - style={treeStyle} - onChange={e => console.log('当前所有选中项: ', e)} - onSelect={e => onTreeSelect(e)} - /> - - ) : null - } -
- =1.16.0 - onCancel={handleDeleteCancel} - closeOnEsc={true} - > - 确认要删除 {noteTitle} 吗?. -
- More content... {noteId} -
- - =1.16.0 - onCancel={handleDeleteModelCancel} - closeOnEsc={true} - > - -
- -
-
this.handleUpdateSubmit(values)} style={{ textAlign: 'center' ,width: 400 }}> - {({ formState, values, formApi }) => ( - <> - - - - -
-
- -
- - )} -
-
-
-
- - =1.16.0 - onCancel={handleUpdatePasswordModelCancel} - closeOnEsc={true} - > - 已经删除列表 -
-
- ( - NOTE} - main={ -
- {item.title} - - - 创建时间:{item.createtime},更新时间:{item.updatetime},最后浏览时间:{item.viewtime}。 - - -
- } - extra={ - - - - } - /> - )} - /> -
-
- {showNote && - - - -
- } loading={skeletonLoad}> - -

- - {isNoteShow ? ( -
- -
- ) : ( -
- 空 -
- )} -

.

-
-
-
} - {showHome && - -
- onTabSelect(e)}> - - - - - - - - -
- ( - NOTE} - main={ -
- {item.title} - - 创建时间:{item.createtime},更新时间:{item.updatetime},最后浏览时间:{item.viewtime}。 - - -
- } - extra={ - - - - - } - /> - )} - /> -
-
-
} - - {showSettings && - -
- Settings -
- - - -
- - - -
- - - -
-
} - - {showSearch && - -
- Search: {this.state.searchData}

- {this.state.searchData} - -
- ( - NOTE} - main={ -
- {item.title}, - - 创建时间:{item.createtime},更新时间:{item.updatetime},最后浏览时间:{item.viewtime}。 - -
- } - extra={ - - - - - } - /> - )} - /> -
- -
-
} - -
-
- - - Copyright © 2023 Chuyaoyuan. CYY.IM. - - - 客服 - 反馈建议 - -
- - - -
- - ); - } -} +import { useEffect, useCallback, useState } from 'react'; +import type { CSSProperties } from 'react'; +import { + Layout, + Nav, + Button, + Breadcrumb, + BackTop, + Avatar, + Toast, + SideSheet, + Row, + List, + ButtonGroup, + Typography, +} from '@douyinfe/semi-ui'; +import { + IconBell, + IconExit, + IconArrowUp, + IconMenu, + IconCode, +} from '@douyinfe/semi-icons'; +import { IconDescriptions } from '@douyinfe/semi-icons-lab'; + +import { useAuthStore } from '../stores/useAuthStore'; +import { useNoteStore } from '../stores/useNoteStore'; +import { useUIStore } from '../stores/useUIStore'; +import noteService from '../services/note.service'; +import authService from '../services/auth.service'; + +import NoteTree from '../components/sidebar/NoteTree'; +import NoteEditor from '../components/note/NoteEditor'; +import HomePage from './home/HomePage'; +import DeleteModal from '../components/modals/DeleteModal'; +import PasswordModal from '../components/modals/PasswordModal'; +import TrashListModal from '../components/modals/TrashListModal'; + +import type { INote } from '../types'; + +import './styles.css'; + +const topStyle: CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: 30, + width: 30, + borderRadius: '100%', + backgroundColor: 'var(--semi-color-primary)', + color: '#fff', + bottom: 65, + right: 20, +}; + +export default function NotePage() { + const user = useAuthStore((s) => s.user); + const { setTreeData, setHomeData, setViewData } = useNoteStore(); + const { + activeView, + siderFlag, + sideVisible, + setActiveView, + setSiderFlag, + setSideVisible, + setDeleteModelVisible, + setUpdatePasswordModelVisible, + } = useUIStore(); + + const [trashList, setTrashList] = useState([]); + + const loadTree = useCallback(() => { + noteService.getAllContent().then( + (res) => setTreeData(res.data), + () => Toast.error('加载笔记树失败'), + ); + }, [setTreeData]); + + useEffect(() => { + loadTree(); + noteService.getHomeData('1', '0').then( + (res) => setHomeData(res.data), + () => {}, + ); + noteService.getHomeData('3', '0').then( + (res) => setViewData(res.data.slice(0, 10)), + () => {}, + ); + }, [loadTree, setHomeData, setViewData]); + + const handleLogout = useCallback(() => { + void authService.logout(); + }, []); + + const handleNavItemClick = useCallback( + (item: string) => { + if (item === '首页') { + setActiveView('home'); + } else if (item === '设置') { + setActiveView('settings'); + } else if (item === '已删除') { + noteService.getHomeData('4', '0').then( + (res) => { + setTrashList(res.data); + setDeleteModelVisible(true); + }, + () => Toast.error('加载回收站失败'), + ); + } + }, + [setActiveView, setDeleteModelVisible], + ); + + const handleSearch = useCallback(() => { + const query = useUIStore.getState().searchQuery; + setActiveView('search'); + noteService.getHomeData('0', query).then( + (res) => setHomeData(res.data), + () => Toast.error('搜索失败'), + ); + }, [setActiveView, setHomeData]); + + const handleBreakpoint = useCallback( + (_screen: string, matched: boolean) => { + setSiderFlag(matched); + }, + [setSiderFlag], + ); + + const { Header, Footer, Sider, Content } = Layout; + const siderWidth = siderFlag ? '265px' : '0px'; + + return ( + +
+ +
+ + + + {siderFlag && ( + + )} + + + {/* Delete confirmation modal */} + + + {/* Change password modal */} + + + {/* Trash / deleted notes modal */} + + + {/* Main content area */} + {activeView === 'note' && ( + + + + )} + + {activeView === 'home' && ( + + +
+ +
+
+ )} + + {activeView === 'settings' && ( + + +
+ Settings +
+ + + +
+ + + +
+ + + +
+
+ )} + + {activeView === 'search' && ( + + +
+ { + setActiveView('note'); + noteService.get(id).then( + (res) => { + const note = res.data; + useNoteStore.getState().setCurrentNote(note.id, note.title, note.context); + }, + () => Toast.error('加载失败'), + ); + }} /> +
+
+ )} +
+ +
+ + + Copyright © 2023 Chuyaoyuan. CYY.IM. + + + 客服 + 反馈建议 + +
+ + + + +
+ ); +} + +/** Inline search results — reads homeData (set by search) from store */ +function SearchResults({ onViewNote }: { onViewNote: (id: number) => void }) { + const homeData = useNoteStore((s) => s.homeData); + const searchQuery = useUIStore((s) => s.searchQuery); + return ( + <> +

搜索:{searchQuery}

+
+ ( + NOTE} + main={ +
+ + {item.title} + + + 创建时间:{item.createtime},更新时间:{item.updatetime}。 + +
+ } + extra={ + + + + } + /> + )} + /> +
+ + ); +} diff --git a/cyynote-frontend/src/pages/sign-in.tsx b/cyynote-frontend/src/pages/sign-in.tsx index 9599a03..b91aa12 100644 --- a/cyynote-frontend/src/pages/sign-in.tsx +++ b/cyynote-frontend/src/pages/sign-in.tsx @@ -1,123 +1,79 @@ -import { Component } from "react"; -import { Form,Toast, Button } from '@douyinfe/semi-ui'; -// import { IconSemiLogo, IconFeishuLogo, IconHelpCircle, IconBell } from '@douyinfe/semi-icons'; -import { Layout } from '@douyinfe/semi-ui'; - -import { Navigate } from "react-router-dom"; -// import { Formik, Field, Form, ErrorMessage } from "formik"; -// import * as Yup from "yup"; - -import AuthService from "../services/auth.service"; -import {IconDescriptions} from "@douyinfe/semi-icons-lab"; - -type Props = {}; - -type State = { - redirect: string | null, - username: string, - password: string, - loading: boolean, - message: string -}; - -export default class SignIn extends Component { - constructor(props: Props) { - super(props); - // this.handleLogin = this.handleLogin.bind(this); - - this.state = { - redirect: null, - username: "", - password: "", - loading: false, - message: "" - }; - } - - componentDidMount() { - const currentUser = AuthService.getCurrentUser(); - - if (currentUser) { - this.setState({ redirect: "/home" }); - } - } - - // componentWillUnmount() { - // window.location.reload(); - // } - - - handleSubmit = (values: { username: string; password: string }) => { - console.log(values); - // Toast.info('表单已提交'); - - AuthService.login(values.username, values.password).then( - () => { - Toast.info('登录成功'); - this.setState({ - redirect: "/home" - }); - }, - error => { - const resMessage = - (error.response && - error.response.data && - error.response.data.message) || - error.message || - error.toString(); - Toast.info(resMessage); - } - ); - }; - - render() { - if (this.state.redirect) { - return - } - - - const { Header, Footer, Content } = Layout; - - const commonStyle = { - height: 64, - lineHeight: '64px', - background: 'var(--semi-color-fill-0)' - }; - return ( - -
-
- - - -
- - - {/*Header*/} -
- - CyyNote 登录 -
this.handleSubmit(values)} style={{ textAlign: 'center' ,width: 400 }}> - {({ formState, values, formApi }) => ( - <> - - - I have read and agree to the terms of service -
-

- Or -

- -
- - )} -
- - -
-
@版权无限所有
-
- - ); - } -} +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { Form, Button, Toast, Layout } from '@douyinfe/semi-ui'; +import { IconDescriptions } from '@douyinfe/semi-icons-lab'; +import authService from '../services/auth.service'; +import { useAuthStore } from '../stores/useAuthStore'; + +interface SignInForm { + username: string; + password: string; +} + +export default function SignIn() { + const navigate = useNavigate(); + const user = useAuthStore((s) => s.user); + + useEffect(() => { + if (user) navigate('/home', { replace: true }); + }, [user, navigate]); + + const handleSubmit = async (values: SignInForm) => { + try { + await authService.login(values.username, values.password); + Toast.success('登录成功'); + navigate('/home', { replace: true }); + } catch (error: unknown) { + const msg = + error instanceof Error ? error.message : '登录失败,请重试'; + Toast.error(msg); + } + }; + + const { Header, Content } = Layout; + const commonStyle = { + height: 64, + lineHeight: '64px', + background: 'var(--semi-color-fill-0)', + }; + + return ( + +
+
+ +
+
+ +

+ CyyNote 登录 +

+
void handleSubmit(values as SignInForm)} + style={{ textAlign: 'center', width: 400, margin: '0 auto' }} + > + + +
+ +
+ +
+
+ ); +} diff --git a/cyynote-frontend/src/services/auth-header.ts b/cyynote-frontend/src/services/auth-header.ts index a2db2bb..5d703e4 100644 --- a/cyynote-frontend/src/services/auth-header.ts +++ b/cyynote-frontend/src/services/auth-header.ts @@ -1,15 +1,15 @@ -export default function authHeader() { - const userStr = localStorage.getItem("user"); - console.log('userStr:'+userStr) - let user = null; - if (userStr) - user = JSON.parse(userStr); +/** + * @deprecated Use src/lib/axios.ts instead — the unified Axios instance + * automatically injects the Authorization header via a request interceptor. + * This file is kept temporarily for services not yet migrated. + */ +import { useAuthStore } from '../stores/useAuthStore'; - if (user && user.accessToken) { - return { Authorization: 'Bearer ' + user.accessToken }; // for Spring Boot back-end - // return { 'x-access-token': user.accessToken }; // for Node.js Express back-end - } else { - return { Authorization: '' }; // for Spring Boot back-end - // return { 'x-access-token': null }; // for Node Express back-end +export default function authHeader(): Record { + const { token } = useAuthStore.getState(); + if (token) { + return { Authorization: `Bearer ${token}` }; } + return { Authorization: '' }; } + diff --git a/cyynote-frontend/src/services/auth.service.ts b/cyynote-frontend/src/services/auth.service.ts index a82973a..c390282 100644 --- a/cyynote-frontend/src/services/auth.service.ts +++ b/cyynote-frontend/src/services/auth.service.ts @@ -1,68 +1,52 @@ -import axios from "axios"; -import authHeader from "./auth-header.ts"; +import axiosInstance from '../lib/axios'; +import { useAuthStore } from '../stores/useAuthStore'; +import type { IAuthResponse } from '../types'; -// const API_URL = "http://localhost:8080/api/auth/"; -const API_URL = "/api/auth/"; -class AuthService { - login(username: string, password: string) { - return axios - .post(API_URL + "signin", { - username, - password - }) - .then(response => { - console.log(response) - if (response.data.data.accessToken) { - console.log(response.data.data) - localStorage.setItem("user", JSON.stringify(response.data.data)); - } - debugger +const API_URL = 'auth/'; - return response.data.data; - }); - } +const authService = { + async login(username: string, password: string): Promise { + const response = await axiosInstance.post<{ data: IAuthResponse }>( + `${API_URL}signin`, + { username, password }, + ); + const { data } = response.data; + useAuthStore.getState().setAuth(data.user, data.accessToken); + return data; + }, - updatePassword(username: string, oldpassword: string, newpassword: string) { - return axios - .post(API_URL + "updatePassword", { - username, - oldpassword, - newpassword - }, { headers: authHeader() }) - .then(response => { - console.log(response) - return response.data; - }); - } + async logout(): Promise { + try { + await axiosInstance.get(`${API_URL}logout`); + } finally { + useAuthStore.getState().logout(); + } + }, - logout() { - localStorage.removeItem("user"); - } + async register( + username: string, + email: string, + password: string, + ): Promise { + await axiosInstance.post(`${API_URL}signup`, { username, email, password }); + }, - - logoutApi() { - return axios - .get(API_URL + "logout",{ headers: authHeader() }) - .then(response => { - console.log(response) - }); - } - - register(username: string, email: string, password: string) { - return axios.post(API_URL + "signup", { + async updatePassword( + username: string, + oldpassword: string, + newpassword: string, + ): Promise { + await axiosInstance.post(`${API_URL}updatePassword`, { username, - email, - password + oldpassword, + newpassword, }); - } + }, getCurrentUser() { - const userStr = localStorage.getItem("user"); - console.log(userStr) - if (userStr) return JSON.parse(userStr); + return useAuthStore.getState().user; + }, +}; - return null; - } -} +export default authService; -export default new AuthService(); diff --git a/cyynote-frontend/src/services/note.service.ts b/cyynote-frontend/src/services/note.service.ts index ff920f9..7d654c5 100644 --- a/cyynote-frontend/src/services/note.service.ts +++ b/cyynote-frontend/src/services/note.service.ts @@ -1,54 +1,68 @@ -import axios from 'axios'; -import authHeader from './auth-header'; +import axiosInstance from '../lib/axios'; +import type { ApiResponse, INote, ITreeNode } from '../types'; -// const API_URL = 'http://localhost:8080/api/note/'; -const API_URL = '/api/note/'; -class NoteService { - getAllContent() { - return axios.get(API_URL + 'all',{ headers: authHeader() }); - } +const API_URL = 'note/'; - get(id) { - return axios.get(API_URL + 'get?id='+id, { headers: authHeader() }); - } +const noteService = { + getAllContent(): Promise> { + return axiosInstance.get(API_URL + 'all').then((r) => r.data); + }, - deleteBack(id) { - return axios.get(API_URL + 'deleteBack?id='+id, { headers: authHeader() }); - } + get(id: number): Promise> { + return axiosInstance + .get(API_URL + 'get', { params: { id } }) + .then((r) => r.data); + }, + update(id: number, title: string, context: string): Promise> { + return axiosInstance + .post(API_URL + 'update', { id, title, context }) + .then((r) => r.data); + }, - historyQueryOrDelete(flag) { - return axios.get(API_URL + 'historyQueryOrDelete?flag='+flag, { headers: authHeader() }); - } + delete(id: number): Promise> { + return axiosInstance + .post(API_URL + 'delete', { id }) + .then((r) => r.data); + }, - dataHome(type,search) { - return axios.get(API_URL + 'home?type='+type+'&search='+search, { headers: authHeader() }); - } + add(pid: number): Promise> { + return axiosInstance + .get(API_URL + 'add', { params: { pid } }) + .then((r) => r.data); + }, - update(id: bigint, title: string, context: string) { - return axios.post(API_URL + "update", { - id, - title, - context - },{ headers: authHeader() }); - } + /** + * Restore a note from trash. + * @todo Backend: rename to DELETE /note/trash/:id for correct REST semantics. + */ + restoreFromTrash(id: number): Promise> { + return axiosInstance + .get(API_URL + 'deleteBack', { params: { id } }) + .then((r) => r.data); + }, + /** + * Query view history count or delete all history. + * @todo Backend: split into GET /note/history/count and DELETE /note/history. + */ + historyQueryOrDelete(flag: number): Promise> { + return axiosInstance + .get(API_URL + 'historyQueryOrDelete', { params: { flag } }) + .then((r) => r.data); + }, - delete(id: bigint) { - return axios.post(API_URL + "delete", { - id - },{ headers: authHeader() }); - } + /** + * Fetch home list by type and optional search query. + * type: '0'=search, '1'=recently updated, '2'=newly added, + * '3'=recently viewed, '4'=deleted/trash list + */ + getHomeData(type: string, search: string): Promise> { + return axiosInstance + .get(API_URL + 'home', { params: { type, search } }) + .then((r) => r.data); + }, +}; - add(pid: bigint) { - if(pid){ - return axios.get(API_URL + 'add?pid='+pid, { headers: authHeader() }); - }else{ - return axios.get(API_URL + 'add?pid=0', { headers: authHeader() }); - } +export default noteService; - } - -} - -export default new NoteService(); diff --git a/cyynote-frontend/src/services/user.service.ts b/cyynote-frontend/src/services/user.service.ts index 4e7a1c7..f3f996c 100644 --- a/cyynote-frontend/src/services/user.service.ts +++ b/cyynote-frontend/src/services/user.service.ts @@ -1,24 +1,25 @@ -import axios from 'axios'; -import authHeader from './auth-header'; +import axiosInstance from '../lib/axios'; +import type { ApiResponse } from '../types'; -const API_URL = 'http://localhost:8080/api/test/'; +const API_URL = 'test/'; -class UserService { - getPublicContent() { - return axios.get(API_URL + 'all'); - } +const userService = { + getPublicContent(): Promise> { + return axiosInstance.get(API_URL + 'all').then((r) => r.data); + }, - getUserBoard() { - return axios.get(API_URL + 'user', { headers: authHeader() }); - } + getUserBoard(): Promise> { + return axiosInstance.get(API_URL + 'user').then((r) => r.data); + }, - getModeratorBoard() { - return axios.get(API_URL + 'mod', { headers: authHeader() }); - } + getModeratorBoard(): Promise> { + return axiosInstance.get(API_URL + 'mod').then((r) => r.data); + }, - getAdminBoard() { - return axios.get(API_URL + 'admin', { headers: authHeader() }); - } -} + getAdminBoard(): Promise> { + return axiosInstance.get(API_URL + 'admin').then((r) => r.data); + }, +}; + +export default userService; -export default new UserService(); diff --git a/cyynote-frontend/src/stores/useAuthStore.ts b/cyynote-frontend/src/stores/useAuthStore.ts new file mode 100644 index 0000000..36ab683 --- /dev/null +++ b/cyynote-frontend/src/stores/useAuthStore.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import type { IUser } from '../types'; + +interface AuthState { + user: IUser | null; + token: string | null; + setAuth: (user: IUser, token: string) => void; + logout: () => void; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + token: null, + setAuth: (user, token) => set({ user, token }), + logout: () => { + set({ user: null, token: null }); + window.location.href = '/login'; + }, + }), + { + name: 'cyynote-auth', + // sessionStorage: token cleared when browser tab closes + storage: createJSONStorage(() => sessionStorage), + }, + ), +); diff --git a/cyynote-frontend/src/stores/useNoteStore.ts b/cyynote-frontend/src/stores/useNoteStore.ts new file mode 100644 index 0000000..8273588 --- /dev/null +++ b/cyynote-frontend/src/stores/useNoteStore.ts @@ -0,0 +1,34 @@ +import { create } from 'zustand'; +import type { INote, ITreeNode } from '../types'; + +interface NoteState { + currentNoteId: number | null; + currentTitle: string; + currentContent: string | null; + treeData: ITreeNode[]; + homeData: INote[]; + viewData: INote[]; + setCurrentNote: (id: number, title: string, content: string) => void; + setTreeData: (data: ITreeNode[]) => void; + setTitle: (title: string) => void; + setHomeData: (data: INote[]) => void; + setViewData: (data: INote[]) => void; + clearCurrentNote: () => void; +} + +export const useNoteStore = create()((set) => ({ + currentNoteId: null, + currentTitle: '', + currentContent: null, + treeData: [], + homeData: [], + viewData: [], + setCurrentNote: (id, title, content) => + set({ currentNoteId: id, currentTitle: title, currentContent: content }), + setTreeData: (treeData) => set({ treeData }), + setTitle: (title) => set({ currentTitle: title }), + setHomeData: (homeData) => set({ homeData }), + setViewData: (viewData) => set({ viewData }), + clearCurrentNote: () => + set({ currentNoteId: null, currentTitle: '', currentContent: null }), +})); diff --git a/cyynote-frontend/src/stores/useUIStore.ts b/cyynote-frontend/src/stores/useUIStore.ts new file mode 100644 index 0000000..64ced32 --- /dev/null +++ b/cyynote-frontend/src/stores/useUIStore.ts @@ -0,0 +1,50 @@ +import { create } from 'zustand'; + +export type ActiveView = 'home' | 'note' | 'settings' | 'search'; + +interface UIState { + activeView: ActiveView; + sideVisible: boolean; + siderFlag: boolean; + isNoteLoading: boolean; + deleteVisible: boolean; + deleteId: number | null; + deleteNoteName: string; + deleteModelVisible: boolean; + updatePasswordModelVisible: boolean; + searchQuery: string; + setActiveView: (view: ActiveView) => void; + setSideVisible: (visible: boolean) => void; + setSiderFlag: (flag: boolean) => void; + setNoteLoading: (loading: boolean) => void; + openDeleteConfirm: (id: number, name: string) => void; + closeDeleteConfirm: () => void; + setDeleteModelVisible: (visible: boolean) => void; + setUpdatePasswordModelVisible: (visible: boolean) => void; + setSearchQuery: (query: string) => void; +} + +export const useUIStore = create()((set) => ({ + activeView: 'home', + sideVisible: false, + siderFlag: true, + isNoteLoading: false, + deleteVisible: false, + deleteId: null, + deleteNoteName: '', + deleteModelVisible: false, + updatePasswordModelVisible: false, + searchQuery: '', + setActiveView: (activeView) => set({ activeView }), + setSideVisible: (sideVisible) => set({ sideVisible }), + setSiderFlag: (siderFlag) => set({ siderFlag }), + setNoteLoading: (isNoteLoading) => set({ isNoteLoading }), + openDeleteConfirm: (deleteId, deleteNoteName) => + set({ deleteVisible: true, deleteId, deleteNoteName }), + closeDeleteConfirm: () => + set({ deleteVisible: false, deleteId: null, deleteNoteName: '' }), + setDeleteModelVisible: (deleteModelVisible) => set({ deleteModelVisible }), + setUpdatePasswordModelVisible: (updatePasswordModelVisible) => + set({ updatePasswordModelVisible }), + setSearchQuery: (searchQuery) => set({ searchQuery }), +})); diff --git a/cyynote-frontend/src/types/index.ts b/cyynote-frontend/src/types/index.ts new file mode 100644 index 0000000..987c97b --- /dev/null +++ b/cyynote-frontend/src/types/index.ts @@ -0,0 +1,36 @@ +export type UserRole = 'ROLE_USER' | 'ROLE_MODERATOR' | 'ROLE_ADMIN'; + +export interface IUser { + id: number; + username: string; + email: string; + roles: UserRole[]; +} + +export interface IAuthResponse { + accessToken: string; + tokenType: string; + user: IUser; +} + +export interface INote { + id: number; + pid: number; + title: string; + context: string; + createtime: string; + updatetime: string; + viewtime: string; +} + +export interface ITreeNode { + key: number; + label: string; + children?: ITreeNode[]; +} + +export interface ApiResponse { + code: number; + message: string; + data: T; +} diff --git a/cyynote-frontend/tsconfig.json b/cyynote-frontend/tsconfig.json index a7fc6fb..8c7ac16 100644 --- a/cyynote-frontend/tsconfig.json +++ b/cyynote-frontend/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, @@ -11,9 +11,16 @@ "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, + "verbatimModuleSyntax": true, "noEmit": true, "jsx": "react-jsx", + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + /* Linting */ "strict": true, "noUnusedLocals": true, diff --git a/cyynote-frontend/vite.config.ts b/cyynote-frontend/vite.config.ts index 3324658..4a46b6f 100644 --- a/cyynote-frontend/vite.config.ts +++ b/cyynote-frontend/vite.config.ts @@ -1,19 +1,51 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ command }) => ({ plugins: [react()], - server:{ - proxy:{ - //此处编写代理规则 - "/api":{ - target:"http://localhost:8080", - changeOrigin:true, - rewrite:path=>{ - return path.replace(/\/api/,'api/'); - } - } - } - } -}) + resolve: { + // new URL is pure ESM — no __dirname needed, no @types/node required + alias: { '@': new URL('./src', import.meta.url).pathname }, + }, + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + // no rewrite: /api/... proxied as-is to http://localhost:8080/api/... + }, + }, + }, + build: { + rollupOptions: { + output: { + manualChunks: { + lexical: [ + 'lexical', + '@lexical/react', + '@lexical/rich-text', + '@lexical/list', + '@lexical/code', + '@lexical/table', + ], + semi: ['@douyinfe/semi-ui', '@douyinfe/semi-icons'], + excalidraw: ['@excalidraw/excalidraw'], + vendor: ['react', 'react-dom', 'react-router', 'axios'], + yjs: ['yjs', 'y-websocket'], + }, + }, + }, + }, + esbuild: { + // strip console.* and debugger in production builds + drop: command === 'build' ? (['console', 'debugger'] as const) : [], + }, +}));