update #1

Merged
Chuyaoyuan merged 4 commits from v2 into v2-bak 2026-06-15 14:31:36 +08:00
20 changed files with 866 additions and 654 deletions
Showing only changes of commit c764603dbd - Show all commits
+272
View File
@@ -0,0 +1,272 @@
# CyyNote
CyyNote 是一个前后端分离的在线笔记系统,后端基于 Solon + Wood + SQLite 提供鉴权、笔记树、笔记编辑、回收站和浏览历史接口;前端基于 React + Vite + Semi UI + Lexical 提供笔记管理与富文本编辑体验。
## 技术栈
### 前端
- React 18
- TypeScript 5.8
- Vite 6
- React Router 7
- Zustand 5:客户端状态管理
- Axios:统一 HTTP 客户端与鉴权拦截器
- Semi UI 2.96:页面组件库
- Lexical 0.44:富文本编辑器
- Yjs + y-websocket:协作编辑基础能力
- pnpm 9:包管理器
- ESLint 9 Flat Config
### 后端
- JDK 17
- Solon 3.0.5
- Wood ORM / wood-solon-plugin
- SQLite JDBC 3.46
- HikariCP
- solon-sessionstate-jwtJWT Session
- Fastjson2
- Hutool
- Maven
## 目录结构
```text
CyyNote/
├── cyynote-backend/ # Solon 后端工程
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/cyynote/
│ │ ├── controller/ # Auth / Note / Test 控制器
│ │ ├── dso/ # DB 初始化、Wood Mapper
│ │ ├── model/ # 数据模型
│ │ ├── payload/ # 请求/响应 DTO
│ │ └── util/ # 笔记树构建工具
│ └── resources/
│ ├── app.yml # 通用配置
│ ├── app-dev.yml # 开发环境配置
│ ├── app-pro.yml # 生产环境配置
│ └── db/schema.sql # SQLite 初始化脚本
└── cyynote-frontend/ # React 前端工程
├── package.json
├── vite.config.ts
└── src/
├── components/ # 页面组件、Lexical 编辑器、弹窗、侧栏
├── pages/ # 路由页面
├── services/ # API Service
├── stores/ # Zustand Store
├── lib/axios.ts # Axios 实例与拦截器
└── types/ # 前端类型定义
```
## 环境要求
| 项目 | 要求 |
| --- | --- |
| JDK | 17+ |
| Maven | 3.8+ |
| Node.js | 20+ |
| pnpm | 9+ |
后端 Maven 编译目标已设置为 Java 17;前端 `package.json` 中声明了 Node 20 与 pnpm 9。
## 后端配置与运行
### 数据库配置
开发环境默认使用当前运行目录下的 SQLite 文件:
```yaml
test.db1:
jdbcUrl: "jdbc:sqlite:./cyynote.db"
driverClassName: "org.sqlite.JDBC"
```
首次启动时,如果数据库中不存在 `appx` 表,后端会执行 `src/main/resources/db/schema.sql` 初始化基础表结构和示例数据。
默认账号:
| 用户名 | 密码 | 角色 |
| --- | --- | --- |
| admin | cyy123 | ROLE_ADMIN |
| test | test | ROLE_USER |
### 启动后端
```bash
cd cyynote-backend
mvn clean package
java -jar target/cyynote.jar
```
默认服务端口:`8080`
### 后端关键接口
接口统一以 `/api` 为前缀。
#### 鉴权接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/api/auth/signin` | 登录,返回 `accessToken``tokenType``user` |
| GET | `/api/auth/logout` | 清理服务端 Session |
| POST | `/api/auth/signup` | 注册普通用户 |
| POST | `/api/auth/updatePassword` | 修改密码 |
登录成功响应中的 `data` 结构:
```json
{
"accessToken": "...",
"tokenType": "Bearer",
"user": {
"id": 1,
"username": "admin",
"email": "aaaaaaaaa",
"roles": ["ROLE_ADMIN"]
}
}
```
#### 笔记接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/note/all` | 获取笔记树 |
| GET | `/api/note/home?type=1&search=0` | 首页列表、搜索、回收站列表 |
| GET | `/api/note/get?id=1` | 获取笔记详情,同时更新浏览时间 |
| GET | `/api/note/add?pid=0` | 新增笔记 |
| POST | `/api/note/update` | 更新笔记标题和内容 |
| POST | `/api/note/delete` | 软删除笔记 |
| GET | `/api/note/deleteBack?id=1` | 从回收站恢复笔记 |
| GET | `/api/note/historyQueryOrDelete?flag=0` | 查询或清理历史记录 |
`/api/note/home``type` 参数含义:
| type | 说明 |
| --- | --- |
| 0 | 搜索 |
| 1 | 最近更新 |
| 2 | 最新添加 |
| 3 | 最近浏览 |
| 4 | 回收站 |
## 前端配置与运行
### 环境变量
复制示例配置:
```bash
cd cyynote-frontend
cp .env.example .env
```
常用配置:
```env
VITE_API_BASE_URL=/api
VITE_WS_URL=ws://localhost:1234
```
开发模式下,Vite 已配置 `/api` 代理到 `http://localhost:8080`
### 安装与启动
```bash
cd cyynote-frontend
pnpm install
pnpm dev
```
### 构建与检查
```bash
pnpm typecheck
pnpm lint
pnpm build
```
## 前端架构说明
### 路由
前端入口使用 `BrowserRouter`。主要路由:
| 路径 | 页面 | 说明 |
| --- | --- | --- |
| `/login` | SignIn | 登录页 |
| `/register` | Register | 注册页 |
| `/home` | NotePage | 笔记主界面 |
| `/profile` | Profile | 用户信息 |
| `/user` | BoardUser | 用户权限测试页 |
| `/mod` | BoardModerator | Moderator 权限测试页 |
| `/admin` | BoardAdmin | Admin 权限测试页 |
除登录和注册外,其他路由由 `ProtectedRoute` 保护;未登录用户会跳转到 `/login`
### 状态管理
Zustand Store 分为三类:
- `useAuthStore`:用户信息、JWT Token、登录状态持久化到 `sessionStorage`
- `useNoteStore`:当前笔记、笔记树、首页列表、最近浏览列表
- `useUIStore`:当前视图、侧栏状态、搜索条件、弹窗显隐状态
### HTTP 客户端
`src/lib/axios.ts` 创建统一 Axios 实例:
- `baseURL` 读取 `VITE_API_BASE_URL`,默认 `/api`
- 请求拦截器自动添加 `Authorization: Bearer <token>`
- 响应拦截器在 HTTP 401 时自动登出并跳转登录页
### 编辑器
笔记编辑器基于 Lexical。`NoteEditor` 负责标题输入与编辑器挂载,`LexicalApp` 负责富文本编辑能力。协作相关配置位于 `components/lexical/collaboration.ts`WebSocket 地址由 `VITE_WS_URL` 控制。
## 本次升级与修复说明
本次修复覆盖前端、后端和 JDK 17 运行兼容性:
- 后端 Maven 编译目标升级为 Java 17
- 修复后端登录返回结构,使其与前端 `IAuthResponse` 匹配
- 修复登录失败时空指针问题,并返回 HTTP 401
- 修复注册接口只返回空对象的问题,改为实际创建普通用户
- 修复修改密码接口用户不存在时空指针问题
- 修复 JWT 拦截器在 401 后仍继续执行 Controller 的问题
- 修复软删除接口把标题更新成 `null` 的问题
- 修复历史表缺失 `flag` 字段的问题
- 修复开发环境 SQLite 路径在 Linux 下不可用的问题
- 修复前端 Profile 页面访问不存在 `accessToken` 字段导致崩溃的问题
- 修复首页点击“查看”后未切换到笔记视图的问题
- 修复 Board 页面仍使用旧 service 返回结构的问题
- 修复回收站和密码弹窗状态字段 `Model` / `Modal` 拼写不一致问题
- 修复前端笔记树 `key` 类型与后端返回值不一致的问题
- 清理未使用的 `@tanstack/react-query` 依赖声明
## 验证建议
网络正常后建议执行:
```bash
cd cyynote-backend
mvn clean package
cd ../cyynote-frontend
pnpm install
pnpm typecheck
pnpm lint
pnpm build
```
如果 CI 使用 `pnpm install --frozen-lockfile`,需要提交 `pnpm-lock.yaml`
## 注意事项
- 当前密码仍为明文存储,生产环境应改为 BCrypt/Argon2 等不可逆哈希。
- Lexical 从旧版本升级到 0.44 跨度较大,建议重点回归测试自定义 Node、Plugin、序列化/反序列化和保存逻辑。
- y-websocket 3.x 与旧版存在差异,协作编辑功能需要单独联调。
- SQLite 适合单机和轻量部署;多人协作、高并发或云部署建议迁移到 MySQL/PostgreSQL。
+13 -9
View File
@@ -20,7 +20,11 @@
</parent> </parent>
<properties> <properties>
<java.version>11</java.version> <java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies> <dependencies>
@@ -75,14 +79,6 @@
<version>3.46.1.3</version> <version>3.46.1.3</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.noear</groupId> <groupId>org.noear</groupId>
<artifactId>solon-test</artifactId> <artifactId>solon-test</artifactId>
@@ -96,6 +92,14 @@
<finalName>${project.artifactId}</finalName> <finalName>${project.artifactId}</finalName>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.noear</groupId> <groupId>org.noear</groupId>
<artifactId>solon-maven-plugin</artifactId> <artifactId>solon-maven-plugin</artifactId>
@@ -1,18 +1,15 @@
package com.cyynote.controller; package com.cyynote.controller;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.cyynote.controller.BaseController;
import com.cyynote.model.NoteModel;
import com.cyynote.model.UserModel; import com.cyynote.model.UserModel;
import com.cyynote.payload.request.LoginRequest; import com.cyynote.payload.request.LoginRequest;
import com.cyynote.payload.request.SignupRequest; import com.cyynote.payload.request.SignupRequest;
import com.cyynote.payload.request.UpdatePasswordRequest;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import com.cyynote.payload.request.UpdatePasswordRequest;
import org.noear.solon.annotation.Body; import org.noear.solon.annotation.Body;
import org.noear.solon.annotation.Controller; import org.noear.solon.annotation.Controller;
import org.noear.solon.annotation.Mapping; import org.noear.solon.annotation.Mapping;
@@ -26,7 +23,7 @@ import org.noear.wood.annotation.Db;
@Mapping("/api/auth") @Mapping("/api/auth")
@Controller @Controller
public class AuthController extends BaseController { public class AuthController extends BaseController {
private static final Logger log = Logger.getLogger(com.cyynote.controller.AuthController.class.getName()); private static final Logger log = Logger.getLogger(AuthController.class.getName());
@Db @Db
DbContext db; DbContext db;
@@ -39,65 +36,113 @@ public class AuthController extends BaseController {
@Post @Post
@Mapping("/signin") @Mapping("/signin")
public Result signin(Context ctx, @Body LoginRequest loginRequest) throws SQLException { public Result signin(Context ctx, @Body LoginRequest loginRequest) throws SQLException {
log.info("name:" + loginRequest.getUsername()); if (loginRequest == null || loginRequest.getUsername() == null || loginRequest.getPassword() == null) {
log.info("password:" + loginRequest.getPassword()); ctx.status(400);
return Result.failure(400, "用户名和密码不能为空");
long count = db.table("user").selectCount();
log.info("countUser:" + count);
List<UserModel> all = ((DbTableQuery)this.db.table("user")).selectList("*", UserModel.class);
log.info( all.toString());
UserModel model = (UserModel)((DbTableQuery)((DbTableQuery)this.db.table("user").whereEq("username", loginRequest.getUsername())).andEq("password", loginRequest.getPassword())).selectItem("*", UserModel.class);
if ("admin".equals(model.getUsername())) {
ctx.sessionSet("user_name", loginRequest.getUsername());
ctx.sessionSet("user_id", model.getId());
JSONObject obj = new JSONObject();
String token = ctx.sessionState().sessionToken();
obj.put("accessToken", token);
obj.put("id", Integer.valueOf(1));
obj.put("username", loginRequest.getUsername());
obj.put("email", "1");
obj.put("roles", "1");
int flag = ((DbTableQuery)this.db.table("user").set("token", token).set("logintime", DateUtil.now()).whereEq("id", model.getId())).update();
System.out.println(flag);
return Result.succeed(obj);
} }
return Result.failure();
}
log.info("signin username:" + loginRequest.getUsername());
UserModel model = (UserModel) ((DbTableQuery) ((DbTableQuery) this.db.table("user")
.whereEq("username", loginRequest.getUsername()))
.andEq("password", loginRequest.getPassword()))
.selectItem("*", UserModel.class);
if (model == null) {
ctx.status(401);
return Result.failure(401, "用户名或密码错误");
}
ctx.sessionSet("user_name", model.getUsername());
ctx.sessionSet("user_id", model.getId());
String token = ctx.sessionState().sessionToken();
((DbTableQuery) this.db.table("user")
.set("token", token)
.set("logintime", DateUtil.now())
.whereEq("id", model.getId()))
.update();
JSONObject user = new JSONObject();
user.put("id", model.getId());
user.put("username", model.getUsername());
user.put("email", model.getEmail());
user.put("roles", normalizeRoles(model.getRole()));
JSONObject payload = new JSONObject();
payload.put("accessToken", token);
payload.put("tokenType", "Bearer");
payload.put("user", user);
return Result.succeed(payload);
}
@Post @Post
@Mapping("/updatePassword") @Mapping("/updatePassword")
public Result updatePassword(Context ctx, @Body UpdatePasswordRequest loginRequest) throws SQLException { public Result updatePassword(Context ctx, @Body UpdatePasswordRequest loginRequest) throws SQLException {
log.info("name:" + loginRequest.getUsername()); if (loginRequest == null || loginRequest.getUsername() == null || loginRequest.getOldpassword() == null || loginRequest.getNewpassword() == null) {
log.info("old - password:" + loginRequest.getOldpassword()); ctx.status(400);
log.info("new - password:" + loginRequest.getNewpassword()); return Result.failure(400, "参数不完整");
long count = db.table("user").selectCount();
log.info("countUser:" + count);
List<UserModel> all = ((DbTableQuery)this.db.table("user")).selectList("*", UserModel.class);
log.info( all.toString());
UserModel model = (UserModel)((DbTableQuery)((DbTableQuery)this.db.table("user").whereEq("username", loginRequest.getUsername())).andEq("password", loginRequest.getOldpassword())).selectItem("*", UserModel.class);
if ("admin".equals(model.getUsername())) {
int flag = ((DbTableQuery)this.db.table("user").set("password", loginRequest.getNewpassword()).whereEq("id", model.getId())).update();
System.out.println(flag);
return Result.succeed(flag);
} }
return Result.failure();
log.info("update password username:" + loginRequest.getUsername());
UserModel model = (UserModel) ((DbTableQuery) ((DbTableQuery) this.db.table("user")
.whereEq("username", loginRequest.getUsername()))
.andEq("password", loginRequest.getOldpassword()))
.selectItem("*", UserModel.class);
if (model == null) {
ctx.status(400);
return Result.failure(400, "用户名或旧密码错误");
}
int flag = ((DbTableQuery) this.db.table("user")
.set("password", loginRequest.getNewpassword())
.whereEq("id", model.getId()))
.update();
return Result.succeed(flag);
} }
@Post @Post
@Mapping("/signup") @Mapping("/signup")
public Result signup(@Body SignupRequest signUpRequest) { public Result signup(Context ctx, @Body SignupRequest signUpRequest) throws SQLException {
log.info("signUpRequest:" + JSONObject.toJSONString(signUpRequest, new com.alibaba.fastjson2.JSONWriter.Feature[0])); log.info("signup username:" + (signUpRequest == null ? null : signUpRequest.getUsername()));
JSONObject obj = new JSONObject(); if (signUpRequest == null || signUpRequest.getUsername() == null || signUpRequest.getPassword() == null || signUpRequest.getEmail() == null) {
return Result.succeed(obj); ctx.status(400);
return Result.failure(400, "用户名、邮箱和密码不能为空");
}
long exists = ((DbTableQuery) this.db.table("user")
.whereEq("username", signUpRequest.getUsername()))
.selectCount();
if (exists > 0) {
ctx.status(409);
return Result.failure(409, "用户名已存在");
}
UserModel user = new UserModel();
user.setUsername(signUpRequest.getUsername());
user.setEmail(signUpRequest.getEmail());
user.setPassword(signUpRequest.getPassword());
user.setRole("ROLE_USER");
this.db.table("user").setEntityIf(user, (k, v) -> Boolean.valueOf(v != null)).insert();
return Result.succeed("注册成功");
}
private List<String> normalizeRoles(String role) {
if (role == null || role.isBlank()) {
return List.of("ROLE_USER");
}
if (role.startsWith("ROLE_")) {
return List.of(role);
}
if ("admin".equalsIgnoreCase(role)) {
return List.of("ROLE_ADMIN");
}
if ("moderator".equalsIgnoreCase(role) || "mod".equalsIgnoreCase(role)) {
return List.of("ROLE_MODERATOR");
}
if (role.contains(",")) {
return Arrays.asList(role.split(","));
}
return List.of("ROLE_USER");
} }
} }
@@ -1,6 +1,5 @@
package com.cyynote.controller; package com.cyynote.controller;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import org.noear.solon.annotation.Component; import org.noear.solon.annotation.Component;
import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Context;
@@ -13,32 +12,28 @@ import org.noear.solon.sessionstate.jwt.JwtUtils;
@Component @Component
public class JwtInterceptor implements RouterInterceptor { public class JwtInterceptor implements RouterInterceptor {
public void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain) throws Throwable { public void doIntercept(Context ctx, Handler mainHandler, RouterInterceptorChain chain) throws Throwable {
boolean flag = true; boolean requireAuth = !("/api/auth/signup".equals(ctx.path())
if ("/api/auth/signup".equals(ctx.path()) || || "/api/auth/signin".equals(ctx.path())
"/api/auth/signin".equals(ctx.path()) || || "/demo".equals(ctx.path())
"/demo".equals(ctx.path()) || || "/api/auth/logout".equals(ctx.path())
"/api/auth/logout".equals(ctx.path()) || "OPTIONS".equalsIgnoreCase(ctx.method()));
) if (requireAuth) {
flag = false; String token = ctx.header("Authorization");
if (flag) if (token == null || token.isBlank()) {
if (null != ctx.header("Authorization")) {
String token = ctx.header("Authorization");
token = token.replace("Bearer ", "");
System.out.println("token:" + token);
Claims claims = JwtUtils.parseJwt(token);
System.out.println("claims:" + claims);
if (null != claims) {
System.out.println("username:" + claims.get("user_name"));
System.out.println("exp:" + claims.get("exp"));
} else {
ctx.status(401);
ctx.render(Result.failure(401, ""));
}
} else {
ctx.status(401); ctx.status(401);
ctx.render(Result.failure(401, "")); ctx.render(Result.failure(401, "未登录或登录已过期"));
return;
} }
token = token.replace("Bearer ", "");
Claims claims = JwtUtils.parseJwt(token);
if (claims == null) {
ctx.status(401);
ctx.render(Result.failure(401, "未登录或登录已过期"));
return;
}
}
chain.doIntercept(ctx, mainHandler); chain.doIntercept(ctx, mainHandler);
} }
} }
@@ -4,8 +4,6 @@ import cn.hutool.*;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONObject;
import com.cyynote.controller.BaseController;
import com.cyynote.dso.NoteSqlAnnotation; import com.cyynote.dso.NoteSqlAnnotation;
import com.cyynote.model.HistoryModel; import com.cyynote.model.HistoryModel;
import com.cyynote.model.NoteModel; import com.cyynote.model.NoteModel;
@@ -15,7 +13,6 @@ import com.cyynote.util.TreeNode;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.noear.solon.annotation.Body; import org.noear.solon.annotation.Body;
@@ -28,7 +25,6 @@ import org.noear.solon.annotation.Put;
import org.noear.solon.core.handle.Context; import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Result; import org.noear.solon.core.handle.Result;
import org.noear.solon.sessionstate.jwt.JwtUtils; import org.noear.solon.sessionstate.jwt.JwtUtils;
import org.noear.solon.web.cors.annotation.CrossOrigin;
import org.noear.wood.DbContext; import org.noear.wood.DbContext;
import org.noear.wood.DbTableQuery; import org.noear.wood.DbTableQuery;
import org.noear.wood.annotation.Db; import org.noear.wood.annotation.Db;
@@ -82,8 +78,6 @@ public class NoteController extends BaseController {
@Get @Get
@Mapping("/home") @Mapping("/home")
public Result noteHome(Context ctx, @Path int type, @Path String search) throws Exception { public Result noteHome(Context ctx, @Path int type, @Path String search) throws Exception {
System.out.println(type);
System.out.println(search);
List<NoteModel> all = new ArrayList<>(); List<NoteModel> all = new ArrayList<>();
if (type == 0) { if (type == 0) {
all = ((DbTableQuery)((DbTableQuery)this.db.table("note").whereLk("context", "%" + search + "%")).andLk("title", "%" + search + "%")).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class); all = ((DbTableQuery)((DbTableQuery)this.db.table("note").whereLk("context", "%" + search + "%")).andLk("title", "%" + search + "%")).selectList("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
@@ -103,10 +97,13 @@ public class NoteController extends BaseController {
@Mapping("/get") @Mapping("/get")
public Result getNote(Context ctx, @Path int id) throws Exception { public Result getNote(Context ctx, @Path int id) throws Exception {
NoteModel model = (NoteModel)((DbTableQuery)this.db.table("note").whereEq("id", Integer.valueOf(id))).selectItem("*", NoteModel.class); NoteModel model = (NoteModel)((DbTableQuery)this.db.table("note").whereEq("id", Integer.valueOf(id))).selectItem("*", NoteModel.class);
if (model == null) {
ctx.status(404);
return Result.failure(404, "笔记不存在");
}
String time = DateUtil.now(); String time = DateUtil.now();
System.out.println(time); ((DbTableQuery)this.db.table("note").set("viewtime", time).whereEq("id", Integer.valueOf(id))).update();
int flag = ((DbTableQuery)this.db.table("note").set("viewtime", time).whereEq("id", Integer.valueOf(id))).update(); model.setViewtime(time);
System.out.println(flag);
log.info(JSONObject.toJSONString(model, new com.alibaba.fastjson2.JSONWriter.Feature[0])); log.info(JSONObject.toJSONString(model, new com.alibaba.fastjson2.JSONWriter.Feature[0]));
return Result.succeed(model); return Result.succeed(model);
} }
@@ -122,6 +119,8 @@ public class NoteController extends BaseController {
note.setTitle("未命名Note"); note.setTitle("未命名Note");
note.setFlag(Integer.valueOf(1)); note.setFlag(Integer.valueOf(1));
note.setCreatetime(DateUtil.now()); note.setCreatetime(DateUtil.now());
note.setUpdatetime(note.getCreatetime());
note.setViewtime(note.getCreatetime());
note.setContext("{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"未命名Note\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}"); note.setContext("{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"未命名Note\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}");
// note.setContext("{\"root\": {\"type\": \"root\", \"format\": \"\", \"indent\": 0, \"version\": 1, \"children\": [{\"type\": \"paragraph\", \"format\": \"\", \"indent\": 0, \"version\": 1, \"children\": [], \"direction\": null}], \"direction\": null}}"); // note.setContext("{\"root\": {\"type\": \"root\", \"format\": \"\", \"indent\": 0, \"version\": 1, \"children\": [{\"type\": \"paragraph\", \"format\": \"\", \"indent\": 0, \"version\": 1, \"children\": [], \"direction\": null}], \"direction\": null}}");
@@ -131,8 +130,16 @@ public class NoteController extends BaseController {
@Post @Post
@Mapping("/update") @Mapping("/update")
public Result updateNote(@Body NoteRequest noteRequest) throws SQLException { public Result updateNote(Context ctx, @Body NoteRequest noteRequest) throws SQLException {
if (noteRequest == null || noteRequest.getId() == null || noteRequest.getTitle() == null || noteRequest.getContext() == null) {
ctx.status(400);
return Result.failure(400, "参数不完整");
}
NoteModel note = (NoteModel)((DbTableQuery)this.db.table("note").whereEq("id", noteRequest.getId())).selectItem("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class); NoteModel note = (NoteModel)((DbTableQuery)this.db.table("note").whereEq("id", noteRequest.getId())).selectItem("id,pid,title,createtime,updatetime,viewtime,flag", NoteModel.class);
if (note == null) {
ctx.status(404);
return Result.failure(404, "笔记不存在");
}
note.setTitle(noteRequest.getTitle()); note.setTitle(noteRequest.getTitle());
note.setUpdatetime(DateUtil.now()); note.setUpdatetime(DateUtil.now());
String cc = noteRequest.getContext(); String cc = noteRequest.getContext();
@@ -153,17 +160,27 @@ public class NoteController extends BaseController {
@Post @Post
@Mapping("/delete") @Mapping("/delete")
public Result deleteNote(@Body NoteRequest noteRequest) throws SQLException { public Result deleteNote(Context ctx, @Body NoteRequest noteRequest) throws SQLException {
int flag = ((DbTableQuery)this.db.table("note").set("title", noteRequest.getTitle()).set("flag", Integer.valueOf(0)).whereEq("id", noteRequest.getId())).update(); if (noteRequest == null || noteRequest.getId() == null) {
System.out.println(flag); ctx.status(400);
return Result.failure(400, "笔记ID不能为空");
}
int flag = ((DbTableQuery)this.db.table("note").set("flag", Integer.valueOf(0)).set("updatetime", DateUtil.now()).whereEq("id", noteRequest.getId())).update();
if (flag == 0) {
ctx.status(404);
return Result.failure(404, "笔记不存在");
}
return Result.succeed("ok"); return Result.succeed("ok");
} }
@Get @Get
@Mapping("/deleteBack") @Mapping("/deleteBack")
public Result deleteBack(@Path int id) throws Exception { public Result deleteBack(Context ctx, @Path int id) throws Exception {
int flag = ((DbTableQuery)this.db.table("note").set("flag", Integer.valueOf(1)).whereEq("id", Integer.valueOf(id))).update(); int flag = ((DbTableQuery)this.db.table("note").set("flag", Integer.valueOf(1)).set("updatetime", DateUtil.now()).whereEq("id", Integer.valueOf(id))).update();
System.out.println(flag); if (flag == 0) {
ctx.status(404);
return Result.failure(404, "笔记不存在");
}
return Result.succeed("ok"); return Result.succeed("ok");
} }
@@ -182,7 +199,7 @@ public class NoteController extends BaseController {
log.info("flagdb:" + flagdb); log.info("flagdb:" + flagdb);
return Result.succeed(0); return Result.succeed(0);
} }
return null; return Result.failure(400, "不支持的操作类型");
} }
private String getUserInfoId(Context ctx) { private String getUserInfoId(Context ctx) {
@@ -190,13 +207,8 @@ public class NoteController extends BaseController {
if (null != ctx.header("Authorization")) { if (null != ctx.header("Authorization")) {
String token = ctx.header("Authorization"); String token = ctx.header("Authorization");
token = token.replace("Bearer ", ""); token = token.replace("Bearer ", "");
System.out.println("token:" + token);
Claims claims = JwtUtils.parseJwt(token); Claims claims = JwtUtils.parseJwt(token);
System.out.println("claims:" + claims);
if (null != claims) { if (null != claims) {
System.out.println("username:" + claims.get("user_name"));
System.out.println("userid:" + claims.get("user_id"));
System.out.println("exp:" + claims.get("exp"));
userId = claims.get("user_id").toString(); userId = claims.get("user_id").toString();
return userId; return userId;
} }
@@ -1,3 +1,3 @@
test.db1: test.db1:
jdbcUrl: "jdbc:sqlite:D:/cyynote.db" jdbcUrl: "jdbc:sqlite:./cyynote.db"
driverClassName: "org.sqlite.JDBC" driverClassName: "org.sqlite.JDBC"
@@ -32,7 +32,8 @@ CREATE TABLE `history` (
conjson TEXT, conjson TEXT,
createtime TEXT, createtime TEXT,
userid INTEGER, userid INTEGER,
username TEXT username TEXT,
flag INTEGER DEFAULT 1
); );
CREATE TABLE appx ( CREATE TABLE appx (
app_id INTEGER PRIMARY KEY AUTOINCREMENT, app_id INTEGER PRIMARY KEY AUTOINCREMENT,
-1
View File
@@ -38,7 +38,6 @@
"@lexical/table": "0.44.0", "@lexical/table": "0.44.0",
"@lexical/utils": "0.44.0", "@lexical/utils": "0.44.0",
"@lexical/yjs": "0.44.0", "@lexical/yjs": "0.44.0",
"@tanstack/react-query": "^5.74.0",
"axios": "^1.8.0", "axios": "^1.8.0",
"html-react-parser": "^5.2.0", "html-react-parser": "^5.2.0",
"katex": "^0.16.21", "katex": "^0.16.21",
@@ -1,54 +1,23 @@
import { Component } from "react"; import { useEffect, useState } from 'react';
import userService from '../services/user.service';
import UserService from "../services/user.service"; export default function BoardAdmin() {
import EventBus from "../common/EventBus"; const [content, setContent] = useState('');
type Props = {}; useEffect(() => {
userService.getAdminBoard().then(
type State = { (res) => setContent(String(res.data ?? '')),
content: string; (error: unknown) => {
} setContent(error instanceof Error ? error.message : String(error));
export default class BoardAdmin extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
content: ""
};
}
componentDidMount() {
UserService.getAdminBoard().then(
response => {
this.setState({
content: response.data
});
}, },
error => {
this.setState({
content:
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString()
});
if (error.response && error.response.status === 401) {
EventBus.dispatch("logout");
}
}
); );
} }, []);
render() { return (
return ( <div className="container">
<div className="container"> <header className="jumbotron">
<header className="jumbotron"> <h3>{content}</h3>
<h3>{this.state.content}</h3> </header>
</header> </div>
</div> );
);
}
} }
@@ -1,54 +1,23 @@
import { Component } from "react"; import { useEffect, useState } from 'react';
import userService from '../services/user.service';
import UserService from "../services/user.service"; export default function BoardModerator() {
import EventBus from "../common/EventBus"; const [content, setContent] = useState('');
type Props = {}; useEffect(() => {
userService.getModeratorBoard().then(
type State = { (res) => setContent(String(res.data ?? '')),
content: string; (error: unknown) => {
} setContent(error instanceof Error ? error.message : String(error));
export default class BoardAdmin extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
content: ""
};
}
componentDidMount() {
UserService.getModeratorBoard().then(
response => {
this.setState({
content: response.data
});
}, },
error => {
this.setState({
content:
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString()
});
if (error.response && error.response.status === 401) {
EventBus.dispatch("logout");
}
}
); );
} }, []);
render() { return (
return ( <div className="container">
<div className="container"> <header className="jumbotron">
<header className="jumbotron"> <h3>{content}</h3>
<h3>{this.state.content}</h3> </header>
</header> </div>
</div> );
);
}
} }
@@ -1,54 +1,23 @@
import { Component } from "react"; import { useEffect, useState } from 'react';
import userService from '../services/user.service';
import UserService from "../services/user.service"; export default function BoardUser() {
import EventBus from "../common/EventBus"; const [content, setContent] = useState('');
type Props = {}; useEffect(() => {
userService.getUserBoard().then(
type State = { (res) => setContent(String(res.data ?? '')),
content: string; (error: unknown) => {
} setContent(error instanceof Error ? error.message : String(error));
export default class BoardUser extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
content: ""
};
}
componentDidMount() {
UserService.getUserBoard().then(
response => {
this.setState({
content: response.data
});
}, },
error => {
this.setState({
content:
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString()
});
if (error.response && error.response.status === 401) {
EventBus.dispatch("logout");
}
}
); );
} }, []);
render() { return (
return ( <div className="container">
<div className="container"> <header className="jumbotron">
<header className="jumbotron"> <h3>{content}</h3>
<h3>{this.state.content}</h3> </header>
</header> </div>
</div> );
);
}
} }
@@ -4,7 +4,7 @@ import { useUIStore } from '../../stores/useUIStore';
import authService from '../../services/auth.service'; import authService from '../../services/auth.service';
export default function PasswordModal() { export default function PasswordModal() {
const { updatePasswordModelVisible, setUpdatePasswordModelVisible } = useUIStore(); const { updatePasswordModalVisible, setUpdatePasswordModalVisible } = useUIStore();
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const handleSubmit = (values: { const handleSubmit = (values: {
@@ -17,7 +17,7 @@ export default function PasswordModal() {
.then( .then(
() => { () => {
Toast.success('密码修改成功'); Toast.success('密码修改成功');
setUpdatePasswordModelVisible(false); setUpdatePasswordModalVisible(false);
}, },
(error: unknown) => { (error: unknown) => {
const msg = const msg =
@@ -30,10 +30,10 @@ export default function PasswordModal() {
return ( return (
<Modal <Modal
title="修改密码" title="修改密码"
visible={updatePasswordModelVisible} visible={updatePasswordModalVisible}
size="large" size="large"
footer={null} footer={null}
onCancel={() => setUpdatePasswordModelVisible(false)} onCancel={() => setUpdatePasswordModalVisible(false)}
closeOnEsc closeOnEsc
> >
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}> <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
@@ -10,13 +10,13 @@ interface TrashListModalProps {
} }
export default function TrashListModal({ trashList, onRestored }: TrashListModalProps) { export default function TrashListModal({ trashList, onRestored }: TrashListModalProps) {
const { deleteModelVisible, setDeleteModelVisible } = useUIStore(); const { deleteModalVisible, setDeleteModalVisible } = useUIStore();
const handleRestore = (id: number) => { const handleRestore = (id: number) => {
noteService.restoreFromTrash(id).then( noteService.restoreFromTrash(id).then(
() => { () => {
Toast.success('已从回收站恢复'); Toast.success('已从回收站恢复');
setDeleteModelVisible(false); setDeleteModalVisible(false);
onRestored(); onRestored();
}, },
() => Toast.error('恢复失败,请重试'), () => Toast.error('恢复失败,请重试'),
@@ -26,10 +26,10 @@ export default function TrashListModal({ trashList, onRestored }: TrashListModal
return ( return (
<Modal <Modal
title="回收站" title="回收站"
visible={deleteModelVisible} visible={deleteModalVisible}
size="large" size="large"
footer={null} footer={null}
onCancel={() => setDeleteModelVisible(false)} onCancel={() => setDeleteModalVisible(false)}
closeOnEsc closeOnEsc
> >
<div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}> <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}>
@@ -1,69 +1,37 @@
import { Component } from "react";
import { Navigate } from 'react-router'; import { Navigate } from 'react-router';
import AuthService from "../services/auth.service"; import { useAuthStore } from '../stores/useAuthStore';
import type { IUser } from '../types';
type Props = {}; export default function Profile() {
const user = useAuthStore((s) => s.user);
const token = useAuthStore((s) => s.token);
type State = { if (!user) return <Navigate to="/login" replace />;
redirect: string | null,
userReady: boolean,
currentUser: IUser & { accessToken: string }
}
export default class Profile extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { return (
redirect: null, <div className="container">
userReady: false, <div>
currentUser: { accessToken: "" } <header className="jumbotron">
}; <h3>
} <strong>{user.username}</strong> Profile
</h3>
componentDidMount() { </header>
const currentUser = AuthService.getCurrentUser(); <p>
<strong>Token:</strong>{' '}
if (!currentUser) this.setState({ redirect: "/home" }); {token ? `${token.substring(0, 20)} ... ${token.substring(token.length - 20)}` : 'N/A'}
this.setState({ currentUser: currentUser, userReady: true }) </p>
} <p>
<strong>Id:</strong> {user.id}
render() { </p>
if (this.state.redirect) { <p>
return <Navigate to={this.state.redirect} /> <strong>Email:</strong> {user.email}
} </p>
<strong>Authorities:</strong>
const { currentUser } = this.state; <ul>
{user.roles.map((role, index) => (
return ( <li key={index}>{role}</li>
<div className="container"> ))}
{(this.state.userReady) ? </ul>
<div>
<header className="jumbotron">
<h3>
<strong>{currentUser.username}</strong> Profile
</h3>
</header>
<p>
<strong>Token:</strong>{" "}
{currentUser.accessToken.substring(0, 20)} ...{" "}
{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}
</p>
<p>
<strong>Id:</strong>{" "}
{currentUser.id}
</p>
<p>
<strong>Email:</strong>{" "}
{currentUser.email}
</p>
<strong>Authorities:</strong>
<ul>
{currentUser.roles &&
currentUser.roles.map((role, index) => <li key={index}>{role}</li>)}
</ul>
</div> : null}
</div> </div>
); </div>
} );
} }
@@ -34,6 +34,8 @@ export default function NoteTree({ onRefreshTree, onNavItemClick, onSearch }: No
const { treeData, viewData, currentNoteId, setCurrentNote } = useNoteStore(); const { treeData, viewData, currentNoteId, setCurrentNote } = useNoteStore();
const { openDeleteConfirm, setSearchQuery, searchQuery, setActiveView } = useUIStore(); const { openDeleteConfirm, setSearchQuery, searchQuery, setActiveView } = useUIStore();
const toNoteId = (id: string | number) => Number(id);
const loadNote = useCallback( const loadNote = useCallback(
(id: number) => { (id: number) => {
noteService.get(id).then( noteService.get(id).then(
@@ -81,7 +83,7 @@ export default function NoteTree({ onRefreshTree, onNavItemClick, onSearch }: No
size="small" size="small"
icon={<IconCopyAdd />} icon={<IconCopyAdd />}
onClick={(e) => { onClick={(e) => {
addNote(item.key); addNote(toNoteId(item.key));
e.stopPropagation(); e.stopPropagation();
}} }}
/> />
@@ -90,7 +92,7 @@ export default function NoteTree({ onRefreshTree, onNavItemClick, onSearch }: No
type="warning" type="warning"
icon={<IconDelete />} icon={<IconDelete />}
onClick={(e) => { onClick={(e) => {
openDeleteConfirm(item.key, String(label)); openDeleteConfirm(toNoteId(item.key), String(label));
e.stopPropagation(); e.stopPropagation();
}} }}
/> />
@@ -188,11 +190,11 @@ export default function NoteTree({ onRefreshTree, onNavItemClick, onSearch }: No
showFilteredOnly showFilteredOnly
directory directory
virtualize virtualize
value={currentNoteId} value={currentNoteId === null ? undefined : String(currentNoteId)}
treeData={treeData as unknown as Parameters<typeof Tree>[0]['treeData']} treeData={treeData as unknown as Parameters<typeof Tree>[0]['treeData']}
renderLabel={renderLabel as Parameters<typeof Tree>[0]['renderLabel']} renderLabel={renderLabel as Parameters<typeof Tree>[0]['renderLabel']}
style={treeStyle} style={treeStyle}
onSelect={(id) => loadNote(id as unknown as number)} onSelect={(id) => loadNote(toNoteId(id as string | number))}
/> />
</> </>
); );
@@ -9,17 +9,20 @@ import {
Toast, Toast,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { useNoteStore } from '../../stores/useNoteStore'; import { useNoteStore } from '../../stores/useNoteStore';
import { useUIStore } from '../../stores/useUIStore';
import noteService from '../../services/note.service'; import noteService from '../../services/note.service';
import type { INote } from '../../types'; import type { INote } from '../../types';
export default function HomePage() { export default function HomePage() {
const { homeData, setHomeData, setCurrentNote } = useNoteStore(); const { homeData, setHomeData, setCurrentNote } = useNoteStore();
const setActiveView = useUIStore((s) => s.setActiveView);
const loadNote = (id: number) => { const loadNote = (id: number) => {
noteService.get(id).then( noteService.get(id).then(
(res) => { (res) => {
const note = res.data; const note = res.data;
setCurrentNote(note.id, note.title, note.context); setCurrentNote(note.id, note.title, note.context);
setActiveView('note');
}, },
() => Toast.error('加载笔记失败'), () => Toast.error('加载笔记失败'),
); );
+5 -5
View File
@@ -63,8 +63,8 @@ export default function NotePage() {
setActiveView, setActiveView,
setSiderFlag, setSiderFlag,
setSideVisible, setSideVisible,
setDeleteModelVisible, setDeleteModalVisible,
setUpdatePasswordModelVisible, setUpdatePasswordModalVisible,
} = useUIStore(); } = useUIStore();
const [trashList, setTrashList] = useState<INote[]>([]); const [trashList, setTrashList] = useState<INote[]>([]);
@@ -102,13 +102,13 @@ export default function NotePage() {
noteService.getHomeData('4', '0').then( noteService.getHomeData('4', '0').then(
(res) => { (res) => {
setTrashList(res.data); setTrashList(res.data);
setDeleteModelVisible(true); setDeleteModalVisible(true);
}, },
() => Toast.error('加载回收站失败'), () => Toast.error('加载回收站失败'),
); );
} }
}, },
[setActiveView, setDeleteModelVisible], [setActiveView, setDeleteModalVisible],
); );
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
@@ -244,7 +244,7 @@ export default function NotePage() {
theme="solid" theme="solid"
type="secondary" type="secondary"
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
onClick={() => setUpdatePasswordModelVisible(true)} onClick={() => setUpdatePasswordModalVisible(true)}
> >
</Button> </Button>
+9 -9
View File
@@ -10,8 +10,8 @@ interface UIState {
deleteVisible: boolean; deleteVisible: boolean;
deleteId: number | null; deleteId: number | null;
deleteNoteName: string; deleteNoteName: string;
deleteModelVisible: boolean; deleteModalVisible: boolean;
updatePasswordModelVisible: boolean; updatePasswordModalVisible: boolean;
searchQuery: string; searchQuery: string;
setActiveView: (view: ActiveView) => void; setActiveView: (view: ActiveView) => void;
setSideVisible: (visible: boolean) => void; setSideVisible: (visible: boolean) => void;
@@ -19,8 +19,8 @@ interface UIState {
setNoteLoading: (loading: boolean) => void; setNoteLoading: (loading: boolean) => void;
openDeleteConfirm: (id: number, name: string) => void; openDeleteConfirm: (id: number, name: string) => void;
closeDeleteConfirm: () => void; closeDeleteConfirm: () => void;
setDeleteModelVisible: (visible: boolean) => void; setDeleteModalVisible: (visible: boolean) => void;
setUpdatePasswordModelVisible: (visible: boolean) => void; setUpdatePasswordModalVisible: (visible: boolean) => void;
setSearchQuery: (query: string) => void; setSearchQuery: (query: string) => void;
} }
@@ -32,8 +32,8 @@ export const useUIStore = create<UIState>()((set) => ({
deleteVisible: false, deleteVisible: false,
deleteId: null, deleteId: null,
deleteNoteName: '', deleteNoteName: '',
deleteModelVisible: false, deleteModalVisible: false,
updatePasswordModelVisible: false, updatePasswordModalVisible: false,
searchQuery: '', searchQuery: '',
setActiveView: (activeView) => set({ activeView }), setActiveView: (activeView) => set({ activeView }),
setSideVisible: (sideVisible) => set({ sideVisible }), setSideVisible: (sideVisible) => set({ sideVisible }),
@@ -43,8 +43,8 @@ export const useUIStore = create<UIState>()((set) => ({
set({ deleteVisible: true, deleteId, deleteNoteName }), set({ deleteVisible: true, deleteId, deleteNoteName }),
closeDeleteConfirm: () => closeDeleteConfirm: () =>
set({ deleteVisible: false, deleteId: null, deleteNoteName: '' }), set({ deleteVisible: false, deleteId: null, deleteNoteName: '' }),
setDeleteModelVisible: (deleteModelVisible) => set({ deleteModelVisible }), setDeleteModalVisible: (deleteModalVisible) => set({ deleteModalVisible }),
setUpdatePasswordModelVisible: (updatePasswordModelVisible) => setUpdatePasswordModalVisible: (updatePasswordModalVisible) =>
set({ updatePasswordModelVisible }), set({ updatePasswordModalVisible }),
setSearchQuery: (searchQuery) => set({ searchQuery }), setSearchQuery: (searchQuery) => set({ searchQuery }),
})); }));
+4 -1
View File
@@ -24,7 +24,10 @@ export interface INote {
} }
export interface ITreeNode { export interface ITreeNode {
key: number; id?: number;
pid?: number;
key: string;
value?: string;
label: string; label: string;
children?: ITreeNode[]; children?: ITreeNode[];
} }
+4 -3
View File
@@ -11,11 +11,12 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": false,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Path aliases */ /* Path aliases */
"ignoreDeprecations": "6.0",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
@@ -23,8 +24,8 @@
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": false,
"noUnusedParameters": true, "noUnusedParameters": false,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": ["src"], "include": ["src"],