diff --git a/cyynote-backend/target/classes/com/cyynote/WebApp.class b/cyynote-backend/target/classes/com/cyynote/WebApp.class index 9a95435..106080d 100644 Binary files a/cyynote-backend/target/classes/com/cyynote/WebApp.class and b/cyynote-backend/target/classes/com/cyynote/WebApp.class differ diff --git a/cyynote-backend/target/classes/com/cyynote/controller/JwtInterceptor.class b/cyynote-backend/target/classes/com/cyynote/controller/JwtInterceptor.class index 86a47f7..0434ad9 100644 Binary files a/cyynote-backend/target/classes/com/cyynote/controller/JwtInterceptor.class and b/cyynote-backend/target/classes/com/cyynote/controller/JwtInterceptor.class differ diff --git a/cyynote-backend/target/classes/com/cyynote/controller/NoteController.class b/cyynote-backend/target/classes/com/cyynote/controller/NoteController.class index 4b0b115..8264966 100644 Binary files a/cyynote-backend/target/classes/com/cyynote/controller/NoteController.class and b/cyynote-backend/target/classes/com/cyynote/controller/NoteController.class differ 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 e5f645a..6826818 100644 Binary files a/cyynote-backend/target/cyynote-0.2.jar and b/cyynote-backend/target/cyynote.jar differ diff --git a/cyynote-backend/target/cyynote.jar.original b/cyynote-backend/target/cyynote.jar.original index 88f79e4..b3f9b7e 100644 Binary files a/cyynote-backend/target/cyynote.jar.original and b/cyynote-backend/target/cyynote.jar.original differ diff --git a/cyynote-backend/target/maven-archiver/pom.properties b/cyynote-backend/target/maven-archiver/pom.properties index 7d28d87..ec3e122 100644 --- a/cyynote-backend/target/maven-archiver/pom.properties +++ b/cyynote-backend/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ -#Generated by Maven -#Wed Feb 05 19:29:06 CST 2025 -groupId=com.cyynote -artifactId=cyynote -version=1.0.0 +#Generated by Maven +#Wed Apr 29 15:30:28 CST 2026 +groupId=com.cyynote +artifactId=cyynote +version=1.0.0 diff --git a/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 7c4a5f5..207e70e 100644 --- a/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,27 +1,27 @@ -com\cyynote\payload\request\UpdatePasswordRequest.class -com\cyynote\controller\BaseController.class -com\cyynote\dso\DsHelper.class -com\cyynote\model\HistoryModel.class -com\cyynote\model\Appx.class -com\cyynote\payload\request\SignupRequest.class -com\cyynote\dso\AuthProcessorImpl.class -com\cyynote\controller\Test2Controller.class -com\cyynote\payload\response\MessageResponse.class -com\cyynote\WebApp.class -com\cyynote\dso\AuthSqlAnnotation.class -com\cyynote\controller\JwtInterceptor.class -com\cyynote\model\AppxModel.class -com\cyynote\controller\DemoController.class -com\cyynote\Config.class -com\cyynote\util\TreeBuild.class -com\cyynote\dso\SqlMapper.class -com\cyynote\model\UserModel.class -com\cyynote\payload\response\JwtResponse.class -com\cyynote\dso\NoteSqlAnnotation.class -com\cyynote\payload\request\LoginRequest.class -com\cyynote\controller\NoteController.class -com\cyynote\payload\request\NoteRequest.class -com\cyynote\util\TreeNode.class -com\cyynote\model\NoteModel.class -com\cyynote\dso\SqlAnnotation.class -com\cyynote\controller\AuthController.class +com/cyynote/payload/request/LoginRequest.class +com/cyynote/dso/DsHelper.class +com/cyynote/controller/JwtInterceptor.class +com/cyynote/util/TreeNode.class +com/cyynote/model/Appx.class +com/cyynote/model/UserModel.class +com/cyynote/payload/request/SignupRequest.class +com/cyynote/dso/AuthProcessorImpl.class +com/cyynote/payload/response/MessageResponse.class +com/cyynote/controller/NoteController.class +com/cyynote/controller/DemoController.class +com/cyynote/model/NoteModel.class +com/cyynote/Config.class +com/cyynote/payload/request/UpdatePasswordRequest.class +com/cyynote/payload/request/NoteRequest.class +com/cyynote/WebApp.class +com/cyynote/dso/AuthSqlAnnotation.class +com/cyynote/dso/SqlMapper.class +com/cyynote/payload/response/JwtResponse.class +com/cyynote/controller/AuthController.class +com/cyynote/dso/NoteSqlAnnotation.class +com/cyynote/model/AppxModel.class +com/cyynote/dso/SqlAnnotation.class +com/cyynote/controller/BaseController.class +com/cyynote/util/TreeBuild.class +com/cyynote/controller/Test2Controller.class +com/cyynote/model/HistoryModel.class diff --git a/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 97c77ce..bad85bb 100644 --- a/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/cyynote-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -1,27 +1,27 @@ -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\Config.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\AuthController.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\BaseController.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\DemoController.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\JwtInterceptor.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\NoteController.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\Test2Controller.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\AuthProcessorImpl.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\AuthSqlAnnotation.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\DsHelper.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\NoteSqlAnnotation.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\SqlAnnotation.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\SqlMapper.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\Appx.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\AppxModel.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\HistoryModel.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\NoteModel.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\UserModel.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\request\LoginRequest.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\request\NoteRequest.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\request\SignupRequest.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\request\UpdatePasswordRequest.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\response\JwtResponse.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\response\MessageResponse.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\util\TreeBuild.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\util\TreeNode.java -D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\WebApp.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/Config.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/WebApp.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/AuthController.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/BaseController.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/DemoController.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/JwtInterceptor.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/NoteController.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/Test2Controller.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/AuthProcessorImpl.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/AuthSqlAnnotation.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/DsHelper.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/NoteSqlAnnotation.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/SqlAnnotation.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/SqlMapper.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/Appx.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/AppxModel.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/HistoryModel.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/NoteModel.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/UserModel.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/request/LoginRequest.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/request/NoteRequest.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/request/SignupRequest.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/request/UpdatePasswordRequest.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/response/JwtResponse.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/response/MessageResponse.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/util/TreeBuild.java +/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/util/TreeNode.java diff --git a/cyynote-frontend/.env.example b/cyynote-frontend/.env.example new file mode 100644 index 0000000..f82fc04 --- /dev/null +++ b/cyynote-frontend/.env.example @@ -0,0 +1,3 @@ +# Copy to .env and fill in your values +VITE_API_BASE_URL=/api +VITE_WS_URL=ws://localhost:1234 diff --git a/cyynote-frontend/.github/workflows/ci.yml b/cyynote-frontend/.github/workflows/ci.yml new file mode 100644 index 0000000..8098c69 --- /dev/null +++ b/cyynote-frontend/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm typecheck + + - name: Lint + run: pnpm lint + + - name: Unit tests + run: pnpm test + + - name: Build + run: pnpm build diff --git a/cyynote-frontend/.npmrc b/cyynote-frontend/.npmrc new file mode 100644 index 0000000..bf2e764 --- /dev/null +++ b/cyynote-frontend/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/cyynote-frontend/.nvmrc b/cyynote-frontend/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/cyynote-frontend/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/cyynote-frontend/eslint.config.js b/cyynote-frontend/eslint.config.js new file mode 100644 index 0000000..4d1fdc5 --- /dev/null +++ b/cyynote-frontend/eslint.config.js @@ -0,0 +1,31 @@ +// @ts-check +import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; + +export default tseslint.config( + { ignores: ['dist', 'node_modules'] }, + { + extends: [...tseslint.configs.recommendedTypeChecked], + files: ['**/*.{ts,tsx}'], + languageOptions: { + globals: globals.browser, + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +); diff --git a/cyynote-frontend/package.json b/cyynote-frontend/package.json index ebe6a27..21d61c4 100644 --- a/cyynote-frontend/package.json +++ b/cyynote-frontend/package.json @@ -4,77 +4,69 @@ "private": true, "version": "0.0.0", "type": "module", + "engines": { + "node": ">=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) : [], + }, +}));