This commit is contained in:
2026-04-29 19:05:47 +08:00
parent b98c66ca21
commit 88c2fa5d03
40 changed files with 1781 additions and 2068 deletions
Binary file not shown.
@@ -1,5 +1,5 @@
#Generated by Maven
#Wed Feb 05 19:29:06 CST 2025
#Wed Apr 29 15:30:28 CST 2026
groupId=com.cyynote
artifactId=cyynote
version=1.0.0
@@ -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
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# Copy to .env and fill in your values
VITE_API_BASE_URL=/api
VITE_WS_URL=ws://localhost:1234
+38
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
shamefully-hoist=true
+1
View File
@@ -0,0 +1 @@
20
+31
View File
@@ -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 },
],
},
},
);
+58 -66
View File
@@ -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"
}
}
+24 -150
View File
@@ -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';
// 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";
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;
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'));
export default function App() {
return (
<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="">
<Suspense fallback={<div style={{ padding: 24 }}>Loading</div>}>
<Routes>
<Route path="/" element={<NotePage />} />
<Route path="/home" element={<NotePage />} />
{/* Public routes */}
<Route path="/login" element={<SignIn />} />
<Route path="/test" element={<Test />} />
<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="/user" element={<BoardUser />} />
<Route path="/mod" element={<BoardModerator />} />
<Route path="/admin" element={<BoardAdmin />} />
</Routes>
</div>
</Route>
{ /*<AuthVerify logOut={this.logOut}/> */}
</div>
{/* Fallback */}
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
</Suspense>
);
}
}
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
* 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 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<string, Doc>,
@@ -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;
}
@@ -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<string | null>(null);
type Props = {};
const user = useAuthStore((s) => s.user);
if (user || redirect) return <Navigate to={redirect ?? '/home'} />;
type State = {
redirect: string | null,
username: string,
password: string,
loading: boolean,
message: string
};
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: ""
};
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 : '登录失败');
}
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 (
<div className="col-md-12">
<div className="card card-container">
<img
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>
<form onSubmit={(e) => void handleLogin(e)}>
<div className="form-group">
<label htmlFor="username">Username</label>
<Field name="username" type="text" className="form-control" />
<ErrorMessage
name="username"
component="div"
className="alert alert-danger"
<input
id="username"
type="text"
className="form-control"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Field name="password" type="password" className="form-control" />
<ErrorMessage
name="password"
component="div"
className="alert alert-danger"
<input
id="password"
type="password"
className="form-control"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="form-group">
<button type="submit" className="btn btn-primary btn-block" disabled={loading}>
{loading && (
<span className="spinner-border spinner-border-sm"></span>
)}
<span>Login</span>
<button type="submit" className="btn btn-primary btn-block">
Login
</button>
</div>
{message && (
<div className="form-group">
<div className="alert alert-danger" role="alert">
{message}
</div>
</div>
)}
</Form>
</Formik>
</form>
</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 { 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 = {};
@@ -1,103 +1,49 @@
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";
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({
const schema = 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!"),
.min(3, '用户名最少 3 个字符')
.max(20, '用户名最多 20 个字符')
.required('请输入用户名'),
email: Yup.string().email('邮箱格式不正确').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!"),
.min(6, '密码最少 6 个字符')
.max(40, '密码最多 40 个字符')
.required('请输入密码'),
});
interface RegisterForm {
username: string;
email: string;
password: string;
}
handleRegister(formValue: { username: string; email: string; password: string }) {
const { username, email, password } = formValue;
export default function Register() {
const [message, setMessage] = useState('');
const [successful, setSuccessful] = useState(false);
this.setState({
message: "",
successful: false
});
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegisterForm>({ resolver: yupResolver(schema) });
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
});
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);
}
);
}
render() {
const { successful, message } = this.state;
const initialValues = {
username: "",
email: "",
password: "",
};
return (
@@ -109,50 +55,56 @@ export default class Register extends Component<Props, State> {
className="profile-img-card"
/>
<Formik
initialValues={initialValues}
validationSchema={this.validationSchema}
onSubmit={this.handleRegister}
>
<Form>
<form onSubmit={(e) => void handleSubmit(onSubmit)(e)}>
{!successful && (
<div>
<div className="form-group">
<label htmlFor="username">Username</label>
<Field name="username" type="text" className="form-control" />
<ErrorMessage
name="username"
component="div"
className="alert alert-danger"
<input
{...register('username')}
type="text"
id="username"
className="form-control"
/>
{errors.username && (
<div className="alert alert-danger">{errors.username.message}</div>
)}
</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"
<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>
<Field
name="password"
<input
{...register('password')}
type="password"
id="password"
className="form-control"
/>
<ErrorMessage
name="password"
component="div"
className="alert alert-danger"
/>
{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">Sign Up</button>
<button
type="submit"
className="btn btn-primary btn-block"
disabled={isSubmitting}
>
{isSubmitting ? '注册中…' : 'Sign Up'}
</button>
</div>
</div>
)}
@@ -160,19 +112,15 @@ export default class Register extends Component<Props, State> {
{message && (
<div className="form-group">
<div
className={
successful ? "alert alert-success" : "alert alert-danger"
}
className={successful ? 'alert alert-success' : 'alert alert-danger'}
role="alert"
>
{message}
</div>
</div>
)}
</Form>
</Formik>
</form>
</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>
&nbsp;&nbsp;
<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)}
/>
</>
);
}
+30
View File
@@ -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;
+6 -19
View File
@@ -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(
// <React.StrictMode>
// <App />
// </React.StrictMode>,
// )
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
);
root.render(
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>
<App />
</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
+53 -97
View File
@@ -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 { 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';
import { Navigate } from "react-router-dom";
// import { Formik, Field, Form, ErrorMessage } from "formik";
// import * as Yup from "yup";
interface SignInForm {
username: string;
password: string;
}
import AuthService from "../services/auth.service";
import {IconDescriptions} from "@douyinfe/semi-icons-lab";
export default function SignIn() {
const navigate = useNavigate();
const user = useAuthStore((s) => s.user);
type Props = {};
useEffect(() => {
if (user) navigate('/home', { replace: true });
}, [user, navigate]);
type State = {
redirect: string | null,
username: string,
password: string,
loading: boolean,
message: string
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);
}
};
export default class SignIn 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: "/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 <Navigate to={this.state.redirect} />
}
const { Header, Footer, Content } = Layout;
const { Header, Content } = Layout;
const commonStyle = {
height: 64,
lineHeight: '64px',
background: 'var(--semi-color-fill-0)'
background: 'var(--semi-color-fill-0)',
};
return (
<Layout className="components-layout-demo">
<Header style={commonStyle}>
<div style={{ paddingTop: '10px' }}>
<IconDescriptions style={{ height: '36px', fontSize: 36 }} />
</div>
{/*Header*/}
</Header>
<Content style={{ textAlign: 'center' }}>
<p style={{ marginTop: 32, marginBottom: 16, fontSize: 20, fontWeight: 600 }}>
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>
<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>
</>
)}
</Form>
</Content>
<Footer style={commonStyle}>@版权无限所有</Footer>
</Layout>
);
}
}
+12 -12
View File
@@ -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<string, string> {
const { token } = useAuthStore.getState();
if (token) {
return { Authorization: `Bearer ${token}` };
}
return { Authorization: '' };
}
+41 -57
View File
@@ -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));
const API_URL = 'auth/';
const authService = {
async login(username: string, password: string): Promise<IAuthResponse> {
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;
},
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) {
return axios
.post(API_URL + "updatePassword", {
async updatePassword(
username: string,
oldpassword: string,
newpassword: string,
): Promise<void> {
await axiosInstance.post(`${API_URL}updatePassword`, {
username,
oldpassword,
newpassword
}, { headers: authHeader() })
.then(response => {
console.log(response)
return response.data;
newpassword,
});
}
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() {
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();
+57 -43
View File
@@ -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<ApiResponse<ITreeNode[]>> {
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<ApiResponse<INote>> {
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) {
return axios.get(API_URL + 'historyQueryOrDelete?flag='+flag, { headers: authHeader() });
}
delete(id: number): Promise<ApiResponse<void>> {
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<ApiResponse<INote>> {
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<ApiResponse<INote>> {
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", {
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<ApiResponse<INote[]>> {
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();
+19 -18
View File
@@ -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<ApiResponse<unknown>> {
return axiosInstance.get(API_URL + 'all').then((r) => r.data);
},
getUserBoard() {
return axios.get(API_URL + 'user', { headers: authHeader() });
}
getUserBoard(): Promise<ApiResponse<unknown>> {
return axiosInstance.get(API_URL + 'user').then((r) => r.data);
},
getModeratorBoard() {
return axios.get(API_URL + 'mod', { headers: authHeader() });
}
getModeratorBoard(): Promise<ApiResponse<unknown>> {
return axiosInstance.get(API_URL + 'mod').then((r) => r.data);
},
getAdminBoard() {
return axios.get(API_URL + 'admin', { headers: authHeader() });
}
}
getAdminBoard(): Promise<ApiResponse<unknown>> {
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 }),
}));
+50
View File
@@ -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 }),
}));
+36
View File
@@ -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;
}
+9 -2
View File
@@ -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,
+45 -13
View File
@@ -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()],
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",
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite:path=>{
return path.replace(/\/api/,'api/');
}
}
}
}
})
// 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) : [],
},
}));