update #1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
#Generated by Maven
|
#Generated by Maven
|
||||||
#Wed Feb 05 19:29:06 CST 2025
|
#Wed Apr 29 15:30:28 CST 2026
|
||||||
groupId=com.cyynote
|
groupId=com.cyynote
|
||||||
artifactId=cyynote
|
artifactId=cyynote
|
||||||
version=1.0.0
|
version=1.0.0
|
||||||
|
|||||||
+27
-27
@@ -1,27 +1,27 @@
|
|||||||
com\cyynote\payload\request\UpdatePasswordRequest.class
|
com/cyynote/payload/request/LoginRequest.class
|
||||||
com\cyynote\controller\BaseController.class
|
com/cyynote/dso/DsHelper.class
|
||||||
com\cyynote\dso\DsHelper.class
|
com/cyynote/controller/JwtInterceptor.class
|
||||||
com\cyynote\model\HistoryModel.class
|
com/cyynote/util/TreeNode.class
|
||||||
com\cyynote\model\Appx.class
|
com/cyynote/model/Appx.class
|
||||||
com\cyynote\payload\request\SignupRequest.class
|
com/cyynote/model/UserModel.class
|
||||||
com\cyynote\dso\AuthProcessorImpl.class
|
com/cyynote/payload/request/SignupRequest.class
|
||||||
com\cyynote\controller\Test2Controller.class
|
com/cyynote/dso/AuthProcessorImpl.class
|
||||||
com\cyynote\payload\response\MessageResponse.class
|
com/cyynote/payload/response/MessageResponse.class
|
||||||
com\cyynote\WebApp.class
|
com/cyynote/controller/NoteController.class
|
||||||
com\cyynote\dso\AuthSqlAnnotation.class
|
com/cyynote/controller/DemoController.class
|
||||||
com\cyynote\controller\JwtInterceptor.class
|
com/cyynote/model/NoteModel.class
|
||||||
com\cyynote\model\AppxModel.class
|
com/cyynote/Config.class
|
||||||
com\cyynote\controller\DemoController.class
|
com/cyynote/payload/request/UpdatePasswordRequest.class
|
||||||
com\cyynote\Config.class
|
com/cyynote/payload/request/NoteRequest.class
|
||||||
com\cyynote\util\TreeBuild.class
|
com/cyynote/WebApp.class
|
||||||
com\cyynote\dso\SqlMapper.class
|
com/cyynote/dso/AuthSqlAnnotation.class
|
||||||
com\cyynote\model\UserModel.class
|
com/cyynote/dso/SqlMapper.class
|
||||||
com\cyynote\payload\response\JwtResponse.class
|
com/cyynote/payload/response/JwtResponse.class
|
||||||
com\cyynote\dso\NoteSqlAnnotation.class
|
com/cyynote/controller/AuthController.class
|
||||||
com\cyynote\payload\request\LoginRequest.class
|
com/cyynote/dso/NoteSqlAnnotation.class
|
||||||
com\cyynote\controller\NoteController.class
|
com/cyynote/model/AppxModel.class
|
||||||
com\cyynote\payload\request\NoteRequest.class
|
com/cyynote/dso/SqlAnnotation.class
|
||||||
com\cyynote\util\TreeNode.class
|
com/cyynote/controller/BaseController.class
|
||||||
com\cyynote\model\NoteModel.class
|
com/cyynote/util/TreeBuild.class
|
||||||
com\cyynote\dso\SqlAnnotation.class
|
com/cyynote/controller/Test2Controller.class
|
||||||
com\cyynote\controller\AuthController.class
|
com/cyynote/model/HistoryModel.class
|
||||||
|
|||||||
+27
-27
@@ -1,27 +1,27 @@
|
|||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\Config.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/Config.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\AuthController.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/WebApp.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\BaseController.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/AuthController.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\DemoController.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/BaseController.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\JwtInterceptor.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/DemoController.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\NoteController.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/JwtInterceptor.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\controller\Test2Controller.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/NoteController.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\AuthProcessorImpl.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/controller/Test2Controller.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\AuthSqlAnnotation.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/AuthProcessorImpl.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\DsHelper.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/AuthSqlAnnotation.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\NoteSqlAnnotation.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/DsHelper.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\SqlAnnotation.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/NoteSqlAnnotation.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\dso\SqlMapper.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/SqlAnnotation.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\Appx.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/dso/SqlMapper.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\AppxModel.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/Appx.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\HistoryModel.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/AppxModel.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\NoteModel.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/HistoryModel.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\model\UserModel.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/NoteModel.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\request\LoginRequest.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/model/UserModel.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\request\NoteRequest.java
|
/opt/cyy/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\SignupRequest.java
|
/opt/cyy/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\UpdatePasswordRequest.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/request/SignupRequest.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\payload\response\JwtResponse.java
|
/opt/cyy/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\MessageResponse.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/response/JwtResponse.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\util\TreeBuild.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/payload/response/MessageResponse.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\util\TreeNode.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/util/TreeBuild.java
|
||||||
D:\chuyy_code\cyynote\cyynote-backend\src\main\java\com\cyynote\WebApp.java
|
/opt/cyy/CyyNote/cyynote-backend/src/main/java/com/cyynote/util/TreeNode.java
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Copy to .env and fill in your values
|
||||||
|
VITE_API_BASE_URL=/api
|
||||||
|
VITE_WS_URL=ws://localhost:1234
|
||||||
+38
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
shamefully-hoist=true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
20
|
||||||
@@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -4,77 +4,69 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0",
|
||||||
|
"pnpm": ">=9.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@9.15.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "pnpm typecheck && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"typecheck": "tsc --noEmit",
|
||||||
"preview": "vite preview"
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alkhipce/editorjs-react": "^0.1.4",
|
"@douyinfe/semi-icons": "2.96.0",
|
||||||
"@blocknote/core": "^0.10.0",
|
"@douyinfe/semi-icons-lab": "2.96.0",
|
||||||
"@blocknote/react": "^0.10.0",
|
"@douyinfe/semi-ui": "2.96.0",
|
||||||
"@douyinfe/semi-icons": "^2.48.0",
|
"@excalidraw/excalidraw": "^0.17.6",
|
||||||
"@douyinfe/semi-icons-lab": "^2.48.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@douyinfe/semi-ui": "^2.48.0",
|
"@lexical/clipboard": "0.44.0",
|
||||||
"@editorjs/checklist": "^1.6.0",
|
"@lexical/code": "0.44.0",
|
||||||
"@editorjs/code": "^2.9.0",
|
"@lexical/file": "0.44.0",
|
||||||
"@editorjs/delimiter": "^1.4.0",
|
"@lexical/hashtag": "0.44.0",
|
||||||
"@editorjs/editorjs": "^2.28.2",
|
"@lexical/link": "0.44.0",
|
||||||
"@editorjs/embed": "^2.7.0",
|
"@lexical/list": "0.44.0",
|
||||||
"@editorjs/header": "^2.8.1",
|
"@lexical/mark": "0.44.0",
|
||||||
"@editorjs/image": "^2.9.0",
|
"@lexical/overflow": "0.44.0",
|
||||||
"@editorjs/inline-code": "^1.5.0",
|
"@lexical/plain-text": "0.44.0",
|
||||||
"@editorjs/link": "^2.6.2",
|
"@lexical/react": "0.44.0",
|
||||||
"@editorjs/list": "^1.9.0",
|
"@lexical/rich-text": "0.44.0",
|
||||||
"@editorjs/marker": "^1.4.0",
|
"@lexical/selection": "0.44.0",
|
||||||
"@editorjs/paragraph": "^2.11.3",
|
"@lexical/table": "0.44.0",
|
||||||
"@editorjs/quote": "^2.6.0",
|
"@lexical/utils": "0.44.0",
|
||||||
"@editorjs/raw": "^2.5.0",
|
"@lexical/yjs": "0.44.0",
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
"@tanstack/react-query": "^5.74.0",
|
||||||
"@editorjs/table": "^2.3.0",
|
"axios": "^1.8.0",
|
||||||
"@editorjs/warning": "^1.4.0",
|
"html-react-parser": "^5.2.0",
|
||||||
"@excalidraw/excalidraw": "^0.17.2",
|
"katex": "^0.16.21",
|
||||||
"@lexical/clipboard": "^0.12.5",
|
"lexical": "0.44.0",
|
||||||
"@lexical/code": "^0.12.5",
|
"react": "^18.3.1",
|
||||||
"@lexical/file": "^0.12.5",
|
"react-dom": "^18.3.1",
|
||||||
"@lexical/hashtag": "^0.12.5",
|
"react-hook-form": "^7.55.0",
|
||||||
"@lexical/link": "^0.12.5",
|
"react-router": "^7.5.0",
|
||||||
"@lexical/list": "^0.12.5",
|
"y-websocket": "^3.0.0",
|
||||||
"@lexical/mark": "^0.12.5",
|
"yjs": "^13.6.22",
|
||||||
"@lexical/overflow": "^0.12.5",
|
"yup": "^1.6.0",
|
||||||
"@lexical/plain-text": "^0.12.5",
|
"zustand": "^5.0.3"
|
||||||
"@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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.3.20",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^8.30.0",
|
||||||
"@typescript-eslint/parser": "^6.10.0",
|
"@typescript-eslint/parser": "^8.30.0",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.4",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"typescript": "^5.2.2",
|
"globals": "^16.0.0",
|
||||||
"vite": "^5.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-150
@@ -1,163 +1,37 @@
|
|||||||
import { Component } from "react";
|
import { lazy, Suspense } from 'react';
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route, Navigate } from 'react-router';
|
||||||
import "./App.css";
|
import './App.css';
|
||||||
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
|
||||||
import AuthService from "./services/auth.service";
|
const NotePage = lazy(() => import('./pages/note-page'));
|
||||||
import IUser from './types/user.type';
|
const SignIn = lazy(() => import('./pages/sign-in'));
|
||||||
|
const Register = lazy(() => import('./components/register.component'));
|
||||||
// import Login from "./components/login.component";
|
const Profile = lazy(() => import('./components/profile.component'));
|
||||||
import Register from "./components/register.component";
|
const BoardUser = lazy(() => import('./components/board-user.component'));
|
||||||
// import Home from "./components/home.component";
|
const BoardModerator = lazy(() => import('./components/board-moderator.component'));
|
||||||
import Profile from "./components/profile.component";
|
const BoardAdmin = lazy(() => import('./components/board-admin.component'));
|
||||||
import BoardUser from "./components/board-user.component";
|
|
||||||
import BoardModerator from "./components/board-moderator.component";
|
|
||||||
import BoardAdmin from "./components/board-admin.component";
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
class App extends Component<Props, State> {
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Suspense fallback={<div style={{ padding: 24 }}>Loading…</div>}>
|
||||||
{/*<nav className="navbar navbar-expand navbar-dark bg-dark">*/}
|
|
||||||
{/* <Link to={"/"} className="navbar-brand">*/}
|
|
||||||
{/* bezKoder*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* <div className="navbar-nav mr-auto">*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/home"} className="nav-link">*/}
|
|
||||||
{/* Home*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
|
|
||||||
{/* {showModeratorBoard && (*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/mod"} className="nav-link">*/}
|
|
||||||
{/* Moderator Board*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
|
|
||||||
{/* {showAdminBoard && (*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/admin"} className="nav-link">*/}
|
|
||||||
{/* Admin Board*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
|
|
||||||
{/* {currentUser && (*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/user"} className="nav-link">*/}
|
|
||||||
{/* User*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
|
|
||||||
{/* {currentUser ? (*/}
|
|
||||||
{/* <div className="navbar-nav ml-auto">*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/profile"} className="nav-link">*/}
|
|
||||||
{/* {currentUser.username}*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <a href="/login" className="nav-link" onClick={this.logOut}>*/}
|
|
||||||
{/* LogOut*/}
|
|
||||||
{/* </a>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/* ) : (*/}
|
|
||||||
{/* <div className="navbar-nav ml-auto">*/}
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/login"} className="nav-link">*/}
|
|
||||||
{/* Login*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
|
|
||||||
{/* <li className="nav-item">*/}
|
|
||||||
{/* <Link to={"/register"} className="nav-link">*/}
|
|
||||||
{/* Sign Up*/}
|
|
||||||
{/* </Link>*/}
|
|
||||||
{/* </li>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/*</nav>*/}
|
|
||||||
|
|
||||||
<div className="">
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<NotePage />} />
|
{/* Public routes */}
|
||||||
<Route path="/home" element={<NotePage />} />
|
|
||||||
<Route path="/login" element={<SignIn />} />
|
<Route path="/login" element={<SignIn />} />
|
||||||
|
|
||||||
<Route path="/test" element={<Test />} />
|
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
||||||
|
{/* Protected routes */}
|
||||||
|
<Route element={<ProtectedRoute />}>
|
||||||
|
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||||
|
<Route path="/home" element={<NotePage />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
<Route path="/user" element={<BoardUser />} />
|
<Route path="/user" element={<BoardUser />} />
|
||||||
<Route path="/mod" element={<BoardModerator />} />
|
<Route path="/mod" element={<BoardModerator />} />
|
||||||
<Route path="/admin" element={<BoardAdmin />} />
|
<Route path="/admin" element={<BoardAdmin />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Fallback */}
|
||||||
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</Suspense>
|
||||||
|
|
||||||
{ /*<AuthVerify logOut={this.logOut}/> */}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|||||||
@@ -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 ? <Outlet /> : <Navigate to="/login" replace />;
|
||||||
|
}
|
||||||
@@ -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
|
* Configuration via environment variables:
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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 type { Provider } from '@lexical/yjs';
|
||||||
import {WebsocketProvider} from 'y-websocket';
|
import { WebsocketProvider } from 'y-websocket';
|
||||||
import {Doc} from 'yjs';
|
import { Doc } from 'yjs';
|
||||||
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const params = new URLSearchParams(url.search);
|
|
||||||
const WEBSOCKET_ENDPOINT =
|
const WEBSOCKET_ENDPOINT =
|
||||||
params.get('collabEndpoint') || 'ws://localhost:1234';
|
(import.meta.env.VITE_WS_URL as string | undefined) ?? 'ws://localhost:1234';
|
||||||
const WEBSOCKET_SLUG = 'playground';
|
|
||||||
const WEBSOCKET_ID = params.get('collabId') || '0';
|
// Use a project-specific slug — not the Meta playground default.
|
||||||
|
const WEBSOCKET_SLUG = 'cyynote';
|
||||||
|
|
||||||
// parent dom -> child doc
|
|
||||||
export function createWebsocketProvider(
|
export function createWebsocketProvider(
|
||||||
id: string,
|
id: string,
|
||||||
yjsDocMap: Map<string, Doc>,
|
yjsDocMap: Map<string, Doc>,
|
||||||
@@ -31,13 +30,11 @@ export function createWebsocketProvider(
|
|||||||
doc.load();
|
doc.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return new WebsocketProvider(
|
return new WebsocketProvider(
|
||||||
WEBSOCKET_ENDPOINT,
|
WEBSOCKET_ENDPOINT,
|
||||||
WEBSOCKET_SLUG + '/' + WEBSOCKET_ID + '/' + id,
|
`${WEBSOCKET_SLUG}/${id}`,
|
||||||
doc,
|
doc,
|
||||||
{
|
{ connect: false },
|
||||||
connect: false,
|
) as unknown as Provider;
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,151 +1,69 @@
|
|||||||
import { Component } from "react";
|
/**
|
||||||
import { Navigate } from "react-router-dom";
|
* @deprecated Use src/pages/sign-in.tsx instead.
|
||||||
import { Formik, Field, Form, ErrorMessage } from "formik";
|
* This file is kept for reference and is not used in the router.
|
||||||
import * as Yup from "yup";
|
*/
|
||||||
|
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<string | null>(null);
|
||||||
|
|
||||||
type Props = {};
|
const user = useAuthStore((s) => s.user);
|
||||||
|
if (user || redirect) return <Navigate to={redirect ?? '/home'} />;
|
||||||
|
|
||||||
type State = {
|
const handleLogin = async (e: FormEvent) => {
|
||||||
redirect: string | null,
|
e.preventDefault();
|
||||||
username: string,
|
setMessage('');
|
||||||
password: string,
|
try {
|
||||||
loading: boolean,
|
await authService.login(username, password);
|
||||||
message: string
|
setRedirect('/home');
|
||||||
};
|
} catch (error: unknown) {
|
||||||
|
setMessage(error instanceof Error ? error.message : '登录失败');
|
||||||
export default class Login extends Component<Props, State> {
|
|
||||||
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 <Navigate to={this.state.redirect} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const { loading, message } = this.state;
|
|
||||||
|
|
||||||
const initialValues = {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<div className="card card-container">
|
<div className="card card-container">
|
||||||
<img
|
<form onSubmit={(e) => void handleLogin(e)}>
|
||||||
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
|
|
||||||
alt="profile-img"
|
|
||||||
className="profile-img-card"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Formik
|
|
||||||
initialValues={initialValues}
|
|
||||||
validationSchema={this.validationSchema}
|
|
||||||
onSubmit={this.handleLogin}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="username">Username</label>
|
<label htmlFor="username">Username</label>
|
||||||
<Field name="username" type="text" className="form-control" />
|
<input
|
||||||
<ErrorMessage
|
id="username"
|
||||||
name="username"
|
type="text"
|
||||||
component="div"
|
className="form-control"
|
||||||
className="alert alert-danger"
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="password">Password</label>
|
<label htmlFor="password">Password</label>
|
||||||
<Field name="password" type="password" className="form-control" />
|
<input
|
||||||
<ErrorMessage
|
id="password"
|
||||||
name="password"
|
type="password"
|
||||||
component="div"
|
className="form-control"
|
||||||
className="alert alert-danger"
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-primary btn-block" disabled={loading}>
|
<button type="submit" className="btn btn-primary btn-block">
|
||||||
{loading && (
|
Login
|
||||||
<span className="spinner-border spinner-border-sm"></span>
|
|
||||||
)}
|
|
||||||
<span>Login</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div className="form-group">
|
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Form>
|
</form>
|
||||||
</Formik>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Modal
|
||||||
|
title="删除确认"
|
||||||
|
visible={deleteVisible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={closeDeleteConfirm}
|
||||||
|
afterClose={closeDeleteConfirm}
|
||||||
|
closeOnEsc
|
||||||
|
>
|
||||||
|
确认要删除「{deleteNoteName}」吗?删除后可在回收站恢复。
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Modal
|
||||||
|
title="修改密码"
|
||||||
|
visible={updatePasswordModelVisible}
|
||||||
|
size="large"
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setUpdatePasswordModelVisible(false)}
|
||||||
|
closeOnEsc
|
||||||
|
>
|
||||||
|
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
|
||||||
|
<Form
|
||||||
|
onSubmit={(values) =>
|
||||||
|
handleSubmit(
|
||||||
|
values as { username: string; oldpassword: string; newpassword: string },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{ textAlign: 'center', width: 400 }}
|
||||||
|
initValues={{ username: user?.username ?? '' }}
|
||||||
|
>
|
||||||
|
<Form.Input
|
||||||
|
field="username"
|
||||||
|
label="用户名"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
mode="password"
|
||||||
|
field="oldpassword"
|
||||||
|
label="旧密码"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
mode="password"
|
||||||
|
field="newpassword"
|
||||||
|
label="新密码"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
||||||
|
<Button htmlType="submit" type="tertiary">
|
||||||
|
确认修改
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Modal
|
||||||
|
title="回收站"
|
||||||
|
visible={deleteModelVisible}
|
||||||
|
size="large"
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setDeleteModelVisible(false)}
|
||||||
|
closeOnEsc
|
||||||
|
>
|
||||||
|
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
|
||||||
|
<List
|
||||||
|
dataSource={trashList}
|
||||||
|
renderItem={(item: INote) => (
|
||||||
|
<List.Item
|
||||||
|
header={<Avatar color="blue">NOTE</Avatar>}
|
||||||
|
main={
|
||||||
|
<div>
|
||||||
|
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ showTooltip: true }}
|
||||||
|
style={{ width: 'calc(100% - 68px)' }}
|
||||||
|
>
|
||||||
|
创建时间:{item.createtime},更新时间:{item.updatetime},
|
||||||
|
最后浏览时间:{item.viewtime}。
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<ButtonGroup theme="borderless">
|
||||||
|
<Button onClick={() => handleRestore(item.id)}>恢复</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Breadcrumb
|
||||||
|
style={{ marginBottom: '24px' }}
|
||||||
|
routes={['CyyNote', '笔记标题', currentTitle]}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: '1px solid var(--semi-color-border)',
|
||||||
|
padding: '15px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton
|
||||||
|
placeholder={<Skeleton.Paragraph rows={2} />}
|
||||||
|
loading={isNoteLoading}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="笔记标题"
|
||||||
|
size="large"
|
||||||
|
value={currentTitle}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{currentNoteId !== null && currentContent !== null ? (
|
||||||
|
<PlaygroundApp
|
||||||
|
noteId={currentNoteId}
|
||||||
|
noteTitle={currentTitle}
|
||||||
|
editData={currentContent}
|
||||||
|
handleLawClick={handleRefreshTree}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: 'var(--semi-color-text-2)' }}>请从左侧选择笔记</div>
|
||||||
|
)}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from "react";
|
import { Component } from "react";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from 'react-router';
|
||||||
import AuthService from "../services/auth.service";
|
import AuthService from "../services/auth.service";
|
||||||
import IUser from "../types/user.type";
|
import type { IUser } from '../types';
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,103 +1,49 @@
|
|||||||
import { Component } from "react";
|
import { useState } from 'react';
|
||||||
import { Formik, Field, Form, ErrorMessage } from "formik";
|
import { useForm } from 'react-hook-form';
|
||||||
import * as Yup from "yup";
|
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({
|
||||||
|
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
username: string,
|
|
||||||
email: string,
|
|
||||||
password: string,
|
|
||||||
successful: boolean,
|
|
||||||
message: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Register extends Component<Props, State> {
|
|
||||||
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()
|
username: Yup.string()
|
||||||
.test(
|
.min(3, '用户名最少 3 个字符')
|
||||||
"len",
|
.max(20, '用户名最多 20 个字符')
|
||||||
"The username must be between 3 and 20 characters.",
|
.required('请输入用户名'),
|
||||||
(val: any) =>
|
email: Yup.string().email('邮箱格式不正确').required('请输入邮箱'),
|
||||||
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()
|
password: Yup.string()
|
||||||
.test(
|
.min(6, '密码最少 6 个字符')
|
||||||
"len",
|
.max(40, '密码最多 40 个字符')
|
||||||
"The password must be between 6 and 40 characters.",
|
.required('请输入密码'),
|
||||||
(val: any) =>
|
});
|
||||||
val &&
|
|
||||||
val.toString().length >= 6 &&
|
interface RegisterForm {
|
||||||
val.toString().length <= 40
|
username: string;
|
||||||
)
|
email: string;
|
||||||
.required("This field is required!"),
|
password: string;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [successful, setSuccessful] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<RegisterForm>({ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
@@ -109,50 +55,56 @@ export default class Register extends Component<Props, State> {
|
|||||||
className="profile-img-card"
|
className="profile-img-card"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Formik
|
<form onSubmit={(e) => void handleSubmit(onSubmit)(e)}>
|
||||||
initialValues={initialValues}
|
|
||||||
validationSchema={this.validationSchema}
|
|
||||||
onSubmit={this.handleRegister}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
{!successful && (
|
{!successful && (
|
||||||
<div>
|
<div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="username"> Username </label>
|
<label htmlFor="username">Username</label>
|
||||||
<Field name="username" type="text" className="form-control" />
|
<input
|
||||||
<ErrorMessage
|
{...register('username')}
|
||||||
name="username"
|
type="text"
|
||||||
component="div"
|
id="username"
|
||||||
className="alert alert-danger"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="email"> Email </label>
|
|
||||||
<Field name="email" type="email" className="form-control" />
|
|
||||||
<ErrorMessage
|
|
||||||
name="email"
|
|
||||||
component="div"
|
|
||||||
className="alert alert-danger"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="password"> Password </label>
|
|
||||||
<Field
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
className="form-control"
|
className="form-control"
|
||||||
/>
|
/>
|
||||||
<ErrorMessage
|
{errors.username && (
|
||||||
name="password"
|
<div className="alert alert-danger">{errors.username.message}</div>
|
||||||
component="div"
|
)}
|
||||||
className="alert alert-danger"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-primary btn-block">Sign Up</button>
|
<label htmlFor="email">Email</label>
|
||||||
|
<input
|
||||||
|
{...register('email')}
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<div className="alert alert-danger">{errors.email.message}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="password">Password</label>
|
||||||
|
<input
|
||||||
|
{...register('password')}
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
className="form-control"
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<div className="alert alert-danger">{errors.password.message}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary btn-block"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? '注册中…' : 'Sign Up'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -160,19 +112,15 @@ export default class Register extends Component<Props, State> {
|
|||||||
{message && (
|
{message && (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div
|
<div
|
||||||
className={
|
className={successful ? 'alert alert-success' : 'alert alert-danger'}
|
||||||
successful ? "alert alert-success" : "alert alert-danger"
|
|
||||||
}
|
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</form>
|
||||||
</Formik>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ showTooltip: true }}
|
||||||
|
style={{ width: 'calc(100% - 68px)' }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography.Text>
|
||||||
|
<ButtonGroup size="small" theme="borderless">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<IconCopyAdd />}
|
||||||
|
onClick={(e) => {
|
||||||
|
addNote(item.key);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="warning"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
onClick={(e) => {
|
||||||
|
openDeleteConfirm(item.key, String(label));
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
showClear
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(v) => setSearchQuery(v)}
|
||||||
|
onEnterPress={onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
position: 'sticky',
|
||||||
|
top: '3px',
|
||||||
|
paddingLeft: '10px',
|
||||||
|
paddingTop: '3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
CYY Note
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<List
|
||||||
|
bordered
|
||||||
|
style={{
|
||||||
|
flexBasis: '100%',
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: '1px solid var(--semi-color-border)',
|
||||||
|
}}
|
||||||
|
dataSource={NAV_ITEMS as unknown as NavItem[]}
|
||||||
|
renderItem={(item: NavItem) => (
|
||||||
|
<div
|
||||||
|
style={{ margin: 5, border: '1px solid var(--semi-color-border)', cursor: 'pointer' }}
|
||||||
|
className="list-item"
|
||||||
|
onClick={() => onNavItemClick(item)}
|
||||||
|
>
|
||||||
|
{item === '首页' && <Button type="danger" theme="borderless" icon={<LabIconToken />} style={{ marginRight: 4 }} />}
|
||||||
|
{item === '设置' && <Button type="danger" theme="borderless" icon={<IconTreeSelect />} style={{ marginRight: 4 }} />}
|
||||||
|
{item === '已删除' && <Button type="danger" theme="borderless" icon={<IconModal />} style={{ marginRight: 4 }} />}
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
marginTop: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
position: 'sticky',
|
||||||
|
top: '40px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="secondary" onClick={() => addNote(0)}>
|
||||||
|
添加主菜单
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
trigger="click"
|
||||||
|
showTick
|
||||||
|
position="bottomLeft"
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<Dropdown.Title>最近浏览</Dropdown.Title>
|
||||||
|
<Dropdown.Divider />
|
||||||
|
{viewData.map((item: INote) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={item.id}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => loadNote(item.id)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button type="secondary">最近浏览</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tree
|
||||||
|
filterTreeNode
|
||||||
|
searchPlaceholder=" "
|
||||||
|
showFilteredOnly
|
||||||
|
directory
|
||||||
|
virtualize
|
||||||
|
value={currentNoteId}
|
||||||
|
treeData={treeData as unknown as Parameters<typeof Tree>[0]['treeData']}
|
||||||
|
renderLabel={renderLabel as Parameters<typeof Tree>[0]['renderLabel']}
|
||||||
|
style={treeStyle}
|
||||||
|
onSelect={(id) => loadNote(id as unknown as number)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -1,28 +1,15 @@
|
|||||||
// import React from 'react'
|
import React from 'react';
|
||||||
// import ReactDOM from 'react-dom/client'
|
|
||||||
// import App from './App.tsx'
|
|
||||||
// import './index.css'
|
|
||||||
//
|
|
||||||
// ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
// <React.StrictMode>
|
|
||||||
// <App />
|
|
||||||
// </React.StrictMode>,
|
|
||||||
// )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from 'react-router';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
document.getElementById('root') as HTMLElement
|
<React.StrictMode>
|
||||||
);
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Tabs type="button" onChange={handleTabChange} defaultActiveKey="1">
|
||||||
|
<TabPane tab="最近更新" itemKey="1" />
|
||||||
|
<TabPane tab="最新添加" itemKey="2" />
|
||||||
|
<TabPane tab="最近浏览" itemKey="3" />
|
||||||
|
</Tabs>
|
||||||
|
<div style={{ padding: 5, border: '1px solid var(--semi-color-border)', margin: 5 }}>
|
||||||
|
<List
|
||||||
|
dataSource={homeData}
|
||||||
|
renderItem={(item: INote) => (
|
||||||
|
<List.Item
|
||||||
|
header={<Avatar color="blue">NOTE</Avatar>}
|
||||||
|
main={
|
||||||
|
<div>
|
||||||
|
<span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ showTooltip: true }}
|
||||||
|
style={{ width: 'calc(100% - 68px)' }}
|
||||||
|
>
|
||||||
|
创建时间:{item.createtime},更新时间:{item.updatetime},
|
||||||
|
最后浏览时间:{item.viewtime}。
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<ButtonGroup theme="borderless">
|
||||||
|
<Button onClick={() => loadNote(item.id)}>查看</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,123 +1,79 @@
|
|||||||
import { Component } from "react";
|
import { useEffect } from 'react';
|
||||||
import { Form,Toast, Button } from '@douyinfe/semi-ui';
|
import { useNavigate } from 'react-router';
|
||||||
// import { IconSemiLogo, IconFeishuLogo, IconHelpCircle, IconBell } from '@douyinfe/semi-icons';
|
import { Form, Button, Toast, Layout } from '@douyinfe/semi-ui';
|
||||||
import { Layout } from '@douyinfe/semi-ui';
|
import { IconDescriptions } from '@douyinfe/semi-icons-lab';
|
||||||
|
import authService from '../services/auth.service';
|
||||||
|
import { useAuthStore } from '../stores/useAuthStore';
|
||||||
|
|
||||||
import { Navigate } from "react-router-dom";
|
interface SignInForm {
|
||||||
// import { Formik, Field, Form, ErrorMessage } from "formik";
|
username: string;
|
||||||
// import * as Yup from "yup";
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
import AuthService from "../services/auth.service";
|
export default function SignIn() {
|
||||||
import {IconDescriptions} from "@douyinfe/semi-icons-lab";
|
const navigate = useNavigate();
|
||||||
|
const user = useAuthStore((s) => s.user);
|
||||||
|
|
||||||
type Props = {};
|
useEffect(() => {
|
||||||
|
if (user) navigate('/home', { replace: true });
|
||||||
|
}, [user, navigate]);
|
||||||
|
|
||||||
type State = {
|
const handleSubmit = async (values: SignInForm) => {
|
||||||
redirect: string | null,
|
try {
|
||||||
username: string,
|
await authService.login(values.username, values.password);
|
||||||
password: string,
|
Toast.success('登录成功');
|
||||||
loading: boolean,
|
navigate('/home', { replace: true });
|
||||||
message: string
|
} catch (error: unknown) {
|
||||||
};
|
const msg =
|
||||||
|
error instanceof Error ? error.message : '登录失败,请重试';
|
||||||
export default class SignIn extends Component<Props, State> {
|
Toast.error(msg);
|
||||||
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() {
|
const { Header, Content } = Layout;
|
||||||
if (this.state.redirect) {
|
|
||||||
return <Navigate to={this.state.redirect} />
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const { Header, Footer, Content } = Layout;
|
|
||||||
|
|
||||||
const commonStyle = {
|
const commonStyle = {
|
||||||
height: 64,
|
height: 64,
|
||||||
lineHeight: '64px',
|
lineHeight: '64px',
|
||||||
background: 'var(--semi-color-fill-0)'
|
background: 'var(--semi-color-fill-0)',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="components-layout-demo">
|
<Layout className="components-layout-demo">
|
||||||
<Header style={commonStyle}>
|
<Header style={commonStyle}>
|
||||||
<div style={{ paddingTop: '10px' }} >
|
<div style={{ paddingTop: '10px' }}>
|
||||||
|
<IconDescriptions style={{ height: '36px', fontSize: 36 }} />
|
||||||
|
|
||||||
<IconDescriptions style={{height: '36px', fontSize: 36}}/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/*Header*/}
|
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{ textAlign: 'center' }}>
|
<Content style={{ textAlign: 'center' }}>
|
||||||
|
<p style={{ marginTop: 32, marginBottom: 16, fontSize: 20, fontWeight: 600 }}>
|
||||||
CyyNote 登录
|
CyyNote 登录
|
||||||
<Form onSubmit={values => this.handleSubmit(values)} style={{ textAlign: 'center' ,width: 400 }}>
|
|
||||||
{({ formState, values, formApi }) => (
|
|
||||||
<>
|
|
||||||
<Form.Input field='username' label='UserName' style={{ width: '100%' }} placeholder='Enter your user name'></Form.Input>
|
|
||||||
<Form.Input mode="password" field='password' label='Password' style={{ width: '100%' }} placeholder='Enter your password'></Form.Input>
|
|
||||||
<Form.Checkbox field='agree' noLabel>I have read and agree to the terms of service</Form.Checkbox>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<p>
|
|
||||||
<span>Or</span><Button theme='borderless' style={{ color: 'var(--semi-color-primary)', marginLeft: 10, cursor: 'pointer' }}>Sign up</Button>
|
|
||||||
</p>
|
</p>
|
||||||
<Button disabled={!values.agree} htmlType='submit' type="tertiary">Log in</Button>
|
<Form
|
||||||
|
onSubmit={(values) => void handleSubmit(values as SignInForm)}
|
||||||
|
style={{ textAlign: 'center', width: 400, margin: '0 auto' }}
|
||||||
|
>
|
||||||
|
<Form.Input
|
||||||
|
field="username"
|
||||||
|
label="用户名"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
mode="password"
|
||||||
|
field="password"
|
||||||
|
label="密码"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Button htmlType="submit" type="primary" theme="solid" block>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|
||||||
</Content>
|
</Content>
|
||||||
<Footer style={commonStyle}>@版权无限所有</Footer>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
export default function authHeader() {
|
/**
|
||||||
const userStr = localStorage.getItem("user");
|
* @deprecated Use src/lib/axios.ts instead — the unified Axios instance
|
||||||
console.log('userStr:'+userStr)
|
* automatically injects the Authorization header via a request interceptor.
|
||||||
let user = null;
|
* This file is kept temporarily for services not yet migrated.
|
||||||
if (userStr)
|
*/
|
||||||
user = JSON.parse(userStr);
|
import { useAuthStore } from '../stores/useAuthStore';
|
||||||
|
|
||||||
if (user && user.accessToken) {
|
export default function authHeader(): Record<string, string> {
|
||||||
return { Authorization: 'Bearer ' + user.accessToken }; // for Spring Boot back-end
|
const { token } = useAuthStore.getState();
|
||||||
// return { 'x-access-token': user.accessToken }; // for Node.js Express back-end
|
if (token) {
|
||||||
} else {
|
return { Authorization: `Bearer ${token}` };
|
||||||
return { Authorization: '' }; // for Spring Boot back-end
|
|
||||||
// return { 'x-access-token': null }; // for Node Express back-end
|
|
||||||
}
|
}
|
||||||
|
return { Authorization: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +1,52 @@
|
|||||||
import axios from "axios";
|
import axiosInstance from '../lib/axios';
|
||||||
import authHeader from "./auth-header.ts";
|
import { useAuthStore } from '../stores/useAuthStore';
|
||||||
|
import type { IAuthResponse } from '../types';
|
||||||
|
|
||||||
// const API_URL = "http://localhost:8080/api/auth/";
|
const API_URL = 'auth/';
|
||||||
const API_URL = "/api/auth/";
|
|
||||||
class AuthService {
|
const authService = {
|
||||||
login(username: string, password: string) {
|
async login(username: string, password: string): Promise<IAuthResponse> {
|
||||||
return axios
|
const response = await axiosInstance.post<{ data: IAuthResponse }>(
|
||||||
.post(API_URL + "signin", {
|
`${API_URL}signin`,
|
||||||
username,
|
{ username, password },
|
||||||
password
|
);
|
||||||
})
|
const { data } = response.data;
|
||||||
.then(response => {
|
useAuthStore.getState().setAuth(data.user, data.accessToken);
|
||||||
console.log(response)
|
return data;
|
||||||
if (response.data.data.accessToken) {
|
},
|
||||||
console.log(response.data.data)
|
|
||||||
localStorage.setItem("user", JSON.stringify(response.data.data));
|
async logout(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await axiosInstance.get(`${API_URL}logout`);
|
||||||
|
} finally {
|
||||||
|
useAuthStore.getState().logout();
|
||||||
}
|
}
|
||||||
debugger
|
},
|
||||||
|
|
||||||
return response.data.data;
|
async register(
|
||||||
});
|
username: string,
|
||||||
}
|
email: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await axiosInstance.post(`${API_URL}signup`, { username, email, password });
|
||||||
|
},
|
||||||
|
|
||||||
updatePassword(username: string, oldpassword: string, newpassword: string) {
|
async updatePassword(
|
||||||
return axios
|
username: string,
|
||||||
.post(API_URL + "updatePassword", {
|
oldpassword: string,
|
||||||
|
newpassword: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await axiosInstance.post(`${API_URL}updatePassword`, {
|
||||||
username,
|
username,
|
||||||
oldpassword,
|
oldpassword,
|
||||||
newpassword
|
newpassword,
|
||||||
}, { headers: authHeader() })
|
|
||||||
.then(response => {
|
|
||||||
console.log(response)
|
|
||||||
return response.data;
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
logout() {
|
|
||||||
localStorage.removeItem("user");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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", {
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentUser() {
|
getCurrentUser() {
|
||||||
const userStr = localStorage.getItem("user");
|
return useAuthStore.getState().user;
|
||||||
console.log(userStr)
|
},
|
||||||
if (userStr) return JSON.parse(userStr);
|
};
|
||||||
|
|
||||||
return null;
|
export default authService;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new AuthService();
|
|
||||||
|
|||||||
@@ -1,54 +1,68 @@
|
|||||||
import axios from 'axios';
|
import axiosInstance from '../lib/axios';
|
||||||
import authHeader from './auth-header';
|
import type { ApiResponse, INote, ITreeNode } from '../types';
|
||||||
|
|
||||||
// const API_URL = 'http://localhost:8080/api/note/';
|
const API_URL = 'note/';
|
||||||
const API_URL = '/api/note/';
|
|
||||||
class NoteService {
|
|
||||||
getAllContent() {
|
|
||||||
return axios.get(API_URL + 'all',{ headers: authHeader() });
|
|
||||||
}
|
|
||||||
|
|
||||||
get(id) {
|
const noteService = {
|
||||||
return axios.get(API_URL + 'get?id='+id, { headers: authHeader() });
|
getAllContent(): Promise<ApiResponse<ITreeNode[]>> {
|
||||||
}
|
return axiosInstance.get(API_URL + 'all').then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
deleteBack(id) {
|
get(id: number): Promise<ApiResponse<INote>> {
|
||||||
return axios.get(API_URL + 'deleteBack?id='+id, { headers: authHeader() });
|
return axiosInstance
|
||||||
}
|
.get(API_URL + 'get', { params: { id } })
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
update(id: number, title: string, context: string): Promise<ApiResponse<void>> {
|
||||||
|
return axiosInstance
|
||||||
|
.post(API_URL + 'update', { id, title, context })
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
historyQueryOrDelete(flag) {
|
delete(id: number): Promise<ApiResponse<void>> {
|
||||||
return axios.get(API_URL + 'historyQueryOrDelete?flag='+flag, { headers: authHeader() });
|
return axiosInstance
|
||||||
}
|
.post(API_URL + 'delete', { id })
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
dataHome(type,search) {
|
add(pid: number): Promise<ApiResponse<INote>> {
|
||||||
return axios.get(API_URL + 'home?type='+type+'&search='+search, { headers: authHeader() });
|
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", {
|
* Restore a note from trash.
|
||||||
id,
|
* @todo Backend: rename to DELETE /note/trash/:id for correct REST semantics.
|
||||||
title,
|
*/
|
||||||
context
|
restoreFromTrash(id: number): Promise<ApiResponse<INote>> {
|
||||||
},{ headers: authHeader() });
|
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<ApiResponse<number>> {
|
||||||
|
return axiosInstance
|
||||||
|
.get(API_URL + 'historyQueryOrDelete', { params: { flag } })
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
delete(id: bigint) {
|
/**
|
||||||
return axios.post(API_URL + "delete", {
|
* Fetch home list by type and optional search query.
|
||||||
id
|
* type: '0'=search, '1'=recently updated, '2'=newly added,
|
||||||
},{ headers: authHeader() });
|
* '3'=recently viewed, '4'=deleted/trash list
|
||||||
}
|
*/
|
||||||
|
getHomeData(type: string, search: string): Promise<ApiResponse<INote[]>> {
|
||||||
|
return axiosInstance
|
||||||
|
.get(API_URL + 'home', { params: { type, search } })
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
add(pid: bigint) {
|
export default noteService;
|
||||||
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 new NoteService();
|
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import axios from 'axios';
|
import axiosInstance from '../lib/axios';
|
||||||
import authHeader from './auth-header';
|
import type { ApiResponse } from '../types';
|
||||||
|
|
||||||
const API_URL = 'http://localhost:8080/api/test/';
|
const API_URL = 'test/';
|
||||||
|
|
||||||
class UserService {
|
const userService = {
|
||||||
getPublicContent() {
|
getPublicContent(): Promise<ApiResponse<unknown>> {
|
||||||
return axios.get(API_URL + 'all');
|
return axiosInstance.get(API_URL + 'all').then((r) => r.data);
|
||||||
}
|
},
|
||||||
|
|
||||||
getUserBoard() {
|
getUserBoard(): Promise<ApiResponse<unknown>> {
|
||||||
return axios.get(API_URL + 'user', { headers: authHeader() });
|
return axiosInstance.get(API_URL + 'user').then((r) => r.data);
|
||||||
}
|
},
|
||||||
|
|
||||||
getModeratorBoard() {
|
getModeratorBoard(): Promise<ApiResponse<unknown>> {
|
||||||
return axios.get(API_URL + 'mod', { headers: authHeader() });
|
return axiosInstance.get(API_URL + 'mod').then((r) => r.data);
|
||||||
}
|
},
|
||||||
|
|
||||||
getAdminBoard() {
|
getAdminBoard(): Promise<ApiResponse<unknown>> {
|
||||||
return axios.get(API_URL + 'admin', { headers: authHeader() });
|
return axiosInstance.get(API_URL + 'admin').then((r) => r.data);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default userService;
|
||||||
|
|
||||||
export default new UserService();
|
|
||||||
|
|||||||
@@ -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<AuthState>()(
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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<NoteState>()((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 }),
|
||||||
|
}));
|
||||||
@@ -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<UIState>()((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 }),
|
||||||
|
}));
|
||||||
@@ -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<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
@@ -11,9 +11,16 @@
|
|||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Path aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
|||||||
@@ -1,19 +1,51 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => ({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server:{
|
resolve: {
|
||||||
proxy:{
|
// new URL is pure ESM — no __dirname needed, no @types/node required
|
||||||
//此处编写代理规则
|
alias: { '@': new URL('./src', import.meta.url).pathname },
|
||||||
"/api":{
|
},
|
||||||
target:"http://localhost:8080",
|
css: {
|
||||||
changeOrigin:true,
|
preprocessorOptions: {
|
||||||
rewrite:path=>{
|
scss: {
|
||||||
return path.replace(/\/api/,'api/');
|
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) : [],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user