chore: 初始化 monorepo 项目脚手架
- 配置 pnpm workspace + Turborepo - 创建 Next.js 15 前端应用 (apps/web) - 创建 NestJS 10 后端应用 (apps/api) - 集成 Prisma ORM + Swagger + JWT 认证 - 添加共享包 (packages/shared, eslint-config, typescript-config) - 添加项目文档 (README, CLAUDE.md, docs/design.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
.next
|
||||||
|
.turbo
|
||||||
|
out
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Prisma
|
||||||
|
prisma/*.db
|
||||||
|
prisma/*.db-journal
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.tsbuildinfo
|
||||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
auto-install-peers=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
dist
|
||||||
|
.turbo
|
||||||
|
coverage
|
||||||
|
pnpm-lock.yaml
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
65
CLAUDE.md
Normal file
65
CLAUDE.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Seclusion 是一个基于 Next.js + NestJS 的 Monorepo 项目模板,使用 pnpm workspace + Turborepo 管理。
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发(同时启动前后端)
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
pnpm lint
|
||||||
|
pnpm format # 格式化
|
||||||
|
pnpm format:check # 检查格式
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
pnpm test
|
||||||
|
cd apps/api && pnpm test:watch # 单个测试文件监听
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
pnpm db:generate # 生成 Prisma Client
|
||||||
|
pnpm db:push # 推送 schema 到数据库
|
||||||
|
pnpm db:migrate # 运行迁移
|
||||||
|
cd apps/api && pnpm db:studio # 打开 Prisma Studio
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Monorepo 结构
|
||||||
|
|
||||||
|
- **apps/web** - Next.js 15 前端 (端口 3000)
|
||||||
|
- **apps/api** - NestJS 10 后端 (端口 4000,API 文档: /api/docs)
|
||||||
|
- **packages/shared** - 共享类型定义和工具函数
|
||||||
|
- **packages/eslint-config** - 共享 ESLint 配置
|
||||||
|
- **packages/typescript-config** - 共享 TypeScript 配置
|
||||||
|
|
||||||
|
### 后端模块 (apps/api)
|
||||||
|
|
||||||
|
NestJS 采用模块化架构:
|
||||||
|
- **PrismaModule** - 全局数据库服务,其他模块通过依赖注入使用
|
||||||
|
- **AuthModule** - JWT 认证(注册、登录、token 验证)
|
||||||
|
- **UserModule** - 用户 CRUD
|
||||||
|
|
||||||
|
认证流程:使用 `@Public()` 装饰器标记公开接口,其他接口需要 JWT Bearer Token。
|
||||||
|
|
||||||
|
### 共享包使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { User, ApiResponse } from '@seclusion/shared';
|
||||||
|
import { formatDate, generateId } from '@seclusion/shared';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `apps/api/prisma/schema.prisma` - 数据库模型定义
|
||||||
|
- `apps/api/.env` - 后端环境变量 (DATABASE_URL, JWT_SECRET)
|
||||||
|
- `apps/web/.env.local` - 前端环境变量 (NEXT_PUBLIC_API_URL)
|
||||||
|
- `turbo.json` - Turborepo 任务依赖配置
|
||||||
116
README.md
Normal file
116
README.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Seclusion
|
||||||
|
|
||||||
|
基于 Next.js + NestJS 的 Monorepo 项目模板,使用 pnpm workspace + Turborepo 管理。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端**: Next.js 15 + React 19 + TypeScript
|
||||||
|
- **后端**: NestJS 10 + Prisma + Swagger + JWT
|
||||||
|
- **工具链**: pnpm + Turborepo + ESLint + Prettier
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
seclusion/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js 前端应用
|
||||||
|
│ └── api/ # NestJS 后端应用
|
||||||
|
├── packages/
|
||||||
|
│ ├── shared/ # 共享代码(类型、工具函数)
|
||||||
|
│ ├── eslint-config/ # ESLint 配置
|
||||||
|
│ └── typescript-config/ # TypeScript 配置
|
||||||
|
├── package.json
|
||||||
|
├── pnpm-workspace.yaml
|
||||||
|
└── turbo.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js >= 20.0.0
|
||||||
|
- pnpm >= 9.0.0
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 后端
|
||||||
|
cp apps/api/.env.example apps/api/.env
|
||||||
|
|
||||||
|
# 前端
|
||||||
|
cp apps/web/.env.example apps/web/.env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### 初始化数据库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成 Prisma Client
|
||||||
|
pnpm db:generate
|
||||||
|
|
||||||
|
# 推送数据库 schema
|
||||||
|
pnpm db:push
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 同时启动前端和后端
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- 前端: http://localhost:3000
|
||||||
|
- 后端: http://localhost:4000
|
||||||
|
- API 文档: http://localhost:4000/api/docs
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发
|
||||||
|
pnpm dev # 启动所有应用的开发服务器
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
pnpm build # 构建所有应用
|
||||||
|
|
||||||
|
# 代码质量
|
||||||
|
pnpm lint # 运行 ESLint
|
||||||
|
pnpm format # 格式化代码
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
pnpm db:generate # 生成 Prisma Client
|
||||||
|
pnpm db:push # 推送 schema 到数据库
|
||||||
|
pnpm db:migrate # 运行数据库迁移
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
pnpm clean # 清理所有构建产物和 node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 认证相关
|
||||||
|
|
||||||
|
- `POST /auth/register` - 用户注册
|
||||||
|
- `POST /auth/login` - 用户登录
|
||||||
|
- `GET /auth/me` - 获取当前用户信息(需认证)
|
||||||
|
|
||||||
|
### 用户管理
|
||||||
|
|
||||||
|
- `GET /users` - 获取所有用户(需认证)
|
||||||
|
- `GET /users/:id` - 获取指定用户(需认证)
|
||||||
|
- `PATCH /users/:id` - 更新用户信息(需认证)
|
||||||
|
- `DELETE /users/:id` - 删除用户(需认证)
|
||||||
|
|
||||||
|
## 共享包使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 导入类型
|
||||||
|
import type { User, ApiResponse } from '@seclusion/shared';
|
||||||
|
|
||||||
|
// 导入工具函数
|
||||||
|
import { formatDate, generateId } from '@seclusion/shared';
|
||||||
|
```
|
||||||
10
apps/api/.env.example
Normal file
10
apps/api/.env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 数据库配置
|
||||||
|
DATABASE_URL="file:./dev.db"
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET="your-super-secret-key-change-in-production"
|
||||||
|
JWT_EXPIRES_IN="7d"
|
||||||
|
|
||||||
|
# 应用配置
|
||||||
|
PORT=4000
|
||||||
|
NODE_ENV=development
|
||||||
8
apps/api/.eslintrc.js
Normal file
8
apps/api/.eslintrc.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['@seclusion/eslint-config/nest'],
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
};
|
||||||
8
apps/api/nest-cli.json
Normal file
8
apps/api/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
58
apps/api/package.json
Normal file
58
apps/api/package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "@seclusion/api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nest start --watch",
|
||||||
|
"build": "nest build",
|
||||||
|
"start": "node dist/main",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"clean": "rm -rf dist .turbo node_modules",
|
||||||
|
"db:generate": "prisma generate",
|
||||||
|
"db:push": "prisma db push",
|
||||||
|
"db:migrate": "prisma migrate dev",
|
||||||
|
"db:studio": "prisma studio"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.4.15",
|
||||||
|
"@nestjs/config": "^3.3.0",
|
||||||
|
"@nestjs/core": "^10.4.15",
|
||||||
|
"@nestjs/jwt": "^10.2.0",
|
||||||
|
"@nestjs/passport": "^10.0.3",
|
||||||
|
"@nestjs/platform-express": "^10.4.15",
|
||||||
|
"@nestjs/swagger": "^8.1.0",
|
||||||
|
"@prisma/client": "^6.1.0",
|
||||||
|
"@seclusion/shared": "workspace:*",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.4.9",
|
||||||
|
"@nestjs/schematics": "^10.2.3",
|
||||||
|
"@nestjs/testing": "^10.4.15",
|
||||||
|
"@seclusion/eslint-config": "workspace:*",
|
||||||
|
"@seclusion/typescript-config": "workspace:*",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prisma": "^6.1.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/api/prisma/dev.db
Normal file
BIN
apps/api/prisma/dev.db
Normal file
Binary file not shown.
19
apps/api/prisma/schema.prisma
Normal file
19
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
name String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
16
apps/api/src/app.controller.ts
Normal file
16
apps/api/src/app.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@ApiTags('健康检查')
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get('health')
|
||||||
|
@ApiOperation({ summary: '健康检查' })
|
||||||
|
healthCheck() {
|
||||||
|
return this.appService.healthCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
apps/api/src/app.module.ts
Normal file
23
apps/api/src/app.module.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { UserModule } from './user/user.module';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath: '.env',
|
||||||
|
}),
|
||||||
|
PrismaModule,
|
||||||
|
AuthModule,
|
||||||
|
UserModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
11
apps/api/src/app.service.ts
Normal file
11
apps/api/src/app.service.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
healthCheck() {
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
38
apps/api/src/auth/auth.controller.ts
Normal file
38
apps/api/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Post, Body, Get, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
||||||
|
import { Public } from './decorators/public.decorator';
|
||||||
|
import { CurrentUser } from './decorators/current-user.decorator';
|
||||||
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@ApiTags('认证')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '用户注册' })
|
||||||
|
@ApiBody({ type: RegisterDto })
|
||||||
|
register(@Body() dto: RegisterDto) {
|
||||||
|
return this.authService.register(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('login')
|
||||||
|
@Public()
|
||||||
|
@ApiOperation({ summary: '用户登录' })
|
||||||
|
@ApiBody({ type: LoginDto })
|
||||||
|
login(@Body() dto: LoginDto) {
|
||||||
|
return this.authService.login(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('me')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: '获取当前用户信息' })
|
||||||
|
getProfile(@CurrentUser() user: { id: string; email: string; name: string }) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
apps/api/src/auth/auth.module.ts
Normal file
27
apps/api/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
secret: configService.get<string>('JWT_SECRET', 'secret'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '7d'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
94
apps/api/src/auth/auth.service.ts
Normal file
94
apps/api/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async register(dto: RegisterDto) {
|
||||||
|
// 检查邮箱是否已存在
|
||||||
|
const existingUser = await this.prisma.user.findUnique({
|
||||||
|
where: { email: dto.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
throw new ConflictException('该邮箱已被注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
const user = await this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: dto.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
name: dto.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成 token
|
||||||
|
const tokens = await this.generateTokens(user.id, user.email);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
...tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(dto: LoginDto) {
|
||||||
|
// 查找用户
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { email: dto.email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('邮箱或密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
const isPasswordValid = await bcrypt.compare(dto.password, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
throw new UnauthorizedException('邮箱或密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 token
|
||||||
|
const tokens = await this.generateTokens(user.id, user.email);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
...tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateTokens(userId: string, email: string) {
|
||||||
|
const payload = { sub: userId, email };
|
||||||
|
|
||||||
|
const accessToken = await this.jwtService.signAsync(payload);
|
||||||
|
|
||||||
|
const refreshToken = await this.jwtService.signAsync(payload, {
|
||||||
|
expiresIn: '30d',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/auth/decorators/current-user.decorator.ts
Normal file
10
apps/api/src/auth/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: string | undefined, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
return data ? user?.[data] : user;
|
||||||
|
},
|
||||||
|
);
|
||||||
4
apps/api/src/auth/decorators/public.decorator.ts
Normal file
4
apps/api/src/auth/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
28
apps/api/src/auth/dto/auth.dto.ts
Normal file
28
apps/api/src/auth/dto/auth.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@ApiProperty({ example: 'user@example.com', description: '用户邮箱' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'password123', description: '密码(至少6位)' })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '张三', description: '用户名称' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@ApiProperty({ example: 'user@example.com', description: '用户邮箱' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'password123', description: '密码' })
|
||||||
|
@IsString()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
25
apps/api/src/auth/guards/jwt-auth.guard.ts
Normal file
25
apps/api/src/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
apps/api/src/auth/strategies/jwt.strategy.ts
Normal file
37
apps/api/src/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
configService: ConfigService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.get<string>('JWT_SECRET', 'secret'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id: payload.sub },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: user.id, email: user.email, name: user.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
44
apps/api/src/main.ts
Normal file
44
apps/api/src/main.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
const configService = app.get(ConfigService);
|
||||||
|
const port = configService.get<number>('PORT', 4000);
|
||||||
|
|
||||||
|
// 全局验证管道
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// CORS 配置
|
||||||
|
app.enableCors({
|
||||||
|
origin: ['http://localhost:3000'],
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Swagger 配置
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Seclusion API')
|
||||||
|
.setDescription('Seclusion 项目 API 文档')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api/docs', app, document);
|
||||||
|
|
||||||
|
await app.listen(port);
|
||||||
|
console.log(`🚀 Application is running on: http://localhost:${port}`);
|
||||||
|
console.log(`📚 Swagger docs: http://localhost:${port}/api/docs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
10
apps/api/src/prisma/prisma.module.ts
Normal file
10
apps/api/src/prisma/prisma.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module, Global } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
13
apps/api/src/prisma/prisma.service.ts
Normal file
13
apps/api/src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/api/src/user/dto/user.dto.ts
Normal file
9
apps/api/src/user/dto/user.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { IsString, IsOptional } from 'class-validator';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class UpdateUserDto {
|
||||||
|
@ApiPropertyOptional({ example: '张三', description: '用户名称' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
38
apps/api/src/user/user.controller.ts
Normal file
38
apps/api/src/user/user.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Patch, Delete, Param, Body, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { UpdateUserDto } from './dto/user.dto';
|
||||||
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@ApiTags('用户')
|
||||||
|
@Controller('users')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取所有用户' })
|
||||||
|
findAll() {
|
||||||
|
return this.userService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '根据 ID 获取用户' })
|
||||||
|
findById(@Param('id') id: string) {
|
||||||
|
return this.userService.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: '更新用户信息' })
|
||||||
|
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
||||||
|
return this.userService.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除用户' })
|
||||||
|
delete(@Param('id') id: string) {
|
||||||
|
return this.userService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/api/src/user/user.module.ts
Normal file
11
apps/api/src/user/user.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { UserController } from './user.controller';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [UserController],
|
||||||
|
providers: [UserService],
|
||||||
|
exports: [UserService],
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
||||||
72
apps/api/src/user/user.service.ts
Normal file
72
apps/api/src/user/user.service.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { UpdateUserDto } from './dto/user.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string) {
|
||||||
|
const user = await this.prisma.user.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateUserDto) {
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: dto,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.user.delete({ where: { id } });
|
||||||
|
|
||||||
|
return { message: '用户已删除' };
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/api/tsconfig.json
Normal file
13
apps/api/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "@seclusion/typescript-config/nestjs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
||||||
2
apps/web/.env.example
Normal file
2
apps/web/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# API 配置
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||||
8
apps/web/.eslintrc.js
Normal file
8
apps/web/.eslintrc.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['@seclusion/eslint-config/next'],
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
};
|
||||||
6
apps/web/next-env.d.ts
vendored
Normal file
6
apps/web/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference path="./.next/types/routes.d.ts" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
8
apps/web/next.config.ts
Normal file
8
apps/web/next.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
transpilePackages: ['@seclusion/shared'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
28
apps/web/package.json
Normal file
28
apps/web/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@seclusion/web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --port 3000",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"clean": "rm -rf .next .turbo node_modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@seclusion/shared": "workspace:*",
|
||||||
|
"next": "^15.1.3",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@seclusion/eslint-config": "workspace:*",
|
||||||
|
"@seclusion/typescript-config": "workspace:*",
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"@types/react": "^19.0.2",
|
||||||
|
"@types/react-dom": "^19.0.2",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "^15.1.3",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
apps/web/src/app/globals.css
Normal file
27
apps/web/src/app/globals.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-start-rgb: 255, 255, 255;
|
||||||
|
--background-end-rgb: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 255, 255, 255;
|
||||||
|
--background-start-rgb: 0, 0, 0;
|
||||||
|
--background-end-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground-rgb));
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
rgb(var(--background-end-rgb))
|
||||||
|
)
|
||||||
|
rgb(var(--background-start-rgb));
|
||||||
|
}
|
||||||
20
apps/web/src/app/layout.tsx
Normal file
20
apps/web/src/app/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Seclusion',
|
||||||
|
description: 'A monorepo template with Next.js and NestJS',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
apps/web/src/app/page.tsx
Normal file
33
apps/web/src/app/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { formatDate } from '@seclusion/shared';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Seclusion</h1>
|
||||||
|
<p className="text-lg text-gray-600 mb-8">
|
||||||
|
A monorepo template with Next.js + NestJS
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 justify-center">
|
||||||
|
<a
|
||||||
|
href="/api/health"
|
||||||
|
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
API Health Check
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="http://localhost:4000/api/docs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="px-6 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
API Docs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="mt-8 text-sm text-gray-400">
|
||||||
|
Generated at: {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
apps/web/src/lib/api.ts
Normal file
25
apps/web/src/lib/api.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
||||||
|
|
||||||
|
interface FetchOptions extends RequestInit {
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T> {
|
||||||
|
const { token, headers, ...rest } = options;
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(token && { Authorization: `Bearer ${token}` }),
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
...rest,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(error.message || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
11
apps/web/tsconfig.json
Normal file
11
apps/web/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "@seclusion/typescript-config/nextjs.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
189
docs/design.md
Normal file
189
docs/design.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Seclusion 项目设计文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
**Seclusion** 是一个基于 Next.js + NestJS 的全栈 Monorepo 项目模板,旨在为后续项目开发提供统一的脚手架。
|
||||||
|
|
||||||
|
## 2. 技术选型
|
||||||
|
|
||||||
|
| 层级 | 技术栈 | 版本 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 前端 | Next.js + React + TypeScript | 15 / 19 / 5.7 |
|
||||||
|
| 后端 | NestJS + Prisma + Swagger | 10 / 6 / 8 |
|
||||||
|
| 认证 | Passport + JWT | - |
|
||||||
|
| 数据库 | SQLite (可替换) | - |
|
||||||
|
| 包管理 | pnpm workspace | 9.x |
|
||||||
|
| 构建工具 | Turborepo | 2.x |
|
||||||
|
| 代码规范 | ESLint + Prettier | - |
|
||||||
|
|
||||||
|
## 3. 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
seclusion/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js 前端应用
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── app/ # App Router 页面
|
||||||
|
│ │ │ ├── components/ # React 组件
|
||||||
|
│ │ │ └── lib/ # 工具库(API 客户端等)
|
||||||
|
│ │ └── public/ # 静态资源
|
||||||
|
│ │
|
||||||
|
│ └── api/ # NestJS 后端应用
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── auth/ # 认证模块
|
||||||
|
│ │ ├── user/ # 用户模块
|
||||||
|
│ │ ├── prisma/ # 数据库服务
|
||||||
|
│ │ └── common/ # 公共装饰器/守卫
|
||||||
|
│ └── prisma/ # Prisma schema 和迁移
|
||||||
|
│
|
||||||
|
├── packages/
|
||||||
|
│ ├── shared/ # 共享代码
|
||||||
|
│ │ └── src/
|
||||||
|
│ │ ├── types/ # 类型定义
|
||||||
|
│ │ └── utils/ # 工具函数
|
||||||
|
│ ├── eslint-config/ # ESLint 配置包
|
||||||
|
│ └── typescript-config/ # TypeScript 配置包
|
||||||
|
│
|
||||||
|
├── package.json # 根配置
|
||||||
|
├── pnpm-workspace.yaml # 工作区定义
|
||||||
|
└── turbo.json # Turborepo 任务配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 架构设计
|
||||||
|
|
||||||
|
### 4.1 Monorepo 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Turborepo │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ apps/web │ ◄─────► │ apps/api │ │
|
||||||
|
│ │ (Next.js) │ HTTP │ (NestJS) │ │
|
||||||
|
│ └────────┬────────┘ └────────┬────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └───────────┬───────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ packages/shared │ │
|
||||||
|
│ │ (类型 + 工具函数) │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 后端模块架构
|
||||||
|
|
||||||
|
```
|
||||||
|
AppModule
|
||||||
|
│
|
||||||
|
├── ConfigModule (全局) # 环境变量管理
|
||||||
|
├── PrismaModule (全局) # 数据库连接
|
||||||
|
├── AuthModule # 认证模块
|
||||||
|
│ ├── AuthController
|
||||||
|
│ ├── AuthService
|
||||||
|
│ ├── JwtStrategy
|
||||||
|
│ └── JwtAuthGuard
|
||||||
|
└── UserModule # 用户模块
|
||||||
|
├── UserController
|
||||||
|
└── UserService
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 认证流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐ POST /auth/login ┌──────────────┐
|
||||||
|
│ Client │ ──────────────────────► │ AuthModule │
|
||||||
|
└──────────┘ └──────┬───────┘
|
||||||
|
▲ │
|
||||||
|
│ { accessToken } ▼
|
||||||
|
└───────────────────────────── 验证密码,生成 JWT
|
||||||
|
|
||||||
|
┌──────────┐ GET /users (Bearer) ┌──────────────┐
|
||||||
|
│ Client │ ──────────────────────► │ JwtAuthGuard │
|
||||||
|
└──────────┘ └──────┬───────┘
|
||||||
|
▲ │
|
||||||
|
│ { users } ▼
|
||||||
|
└───────────────────────────── 验证 Token,放行请求
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 数据模型
|
||||||
|
|
||||||
|
### 5.1 User 模型
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
name String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("users")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. API 接口设计
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 | 认证 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| POST | /auth/register | 用户注册 | 否 |
|
||||||
|
| POST | /auth/login | 用户登录 | 否 |
|
||||||
|
| GET | /auth/me | 获取当前用户 | 是 |
|
||||||
|
| GET | /users | 获取所有用户 | 是 |
|
||||||
|
| GET | /users/:id | 获取指定用户 | 是 |
|
||||||
|
| PATCH | /users/:id | 更新用户信息 | 是 |
|
||||||
|
| DELETE | /users/:id | 删除用户 | 是 |
|
||||||
|
| GET | /health | 健康检查 | 否 |
|
||||||
|
|
||||||
|
## 7. 共享包设计
|
||||||
|
|
||||||
|
### 7.1 类型定义 (@seclusion/shared/types)
|
||||||
|
|
||||||
|
- `ApiResponse<T>` - 通用 API 响应
|
||||||
|
- `PaginationParams` - 分页请求参数
|
||||||
|
- `PaginatedResponse<T>` - 分页响应
|
||||||
|
- `User` - 用户类型
|
||||||
|
- `LoginDto` / `CreateUserDto` - 请求 DTO
|
||||||
|
- `AuthResponse` / `TokenPayload` - 认证相关类型
|
||||||
|
|
||||||
|
### 7.2 工具函数 (@seclusion/shared/utils)
|
||||||
|
|
||||||
|
- `formatDate()` - 日期格式化
|
||||||
|
- `generateId()` - 随机 ID 生成
|
||||||
|
- `deepClone()` - 深拷贝
|
||||||
|
- `isEmpty()` - 空值判断
|
||||||
|
- `safeJsonParse()` - 安全 JSON 解析
|
||||||
|
|
||||||
|
## 8. 开发规范
|
||||||
|
|
||||||
|
### 8.1 端口分配
|
||||||
|
|
||||||
|
- 前端: 3000
|
||||||
|
- 后端: 4000
|
||||||
|
- Swagger 文档: 4000/api/docs
|
||||||
|
|
||||||
|
### 8.2 环境变量
|
||||||
|
|
||||||
|
**后端 (apps/api/.env)**
|
||||||
|
```env
|
||||||
|
DATABASE_URL="file:./dev.db"
|
||||||
|
JWT_SECRET="your-secret-key"
|
||||||
|
JWT_EXPIRES_IN="7d"
|
||||||
|
PORT=4000
|
||||||
|
```
|
||||||
|
|
||||||
|
**前端 (apps/web/.env.local)**
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 构建依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
build
|
||||||
|
├── packages/shared (先构建)
|
||||||
|
├── apps/web (依赖 shared)
|
||||||
|
└── apps/api (依赖 shared)
|
||||||
|
```
|
||||||
|
|
||||||
|
Turborepo 通过 `^build` 依赖确保构建顺序正确。
|
||||||
14
docs/index.md
Normal file
14
docs/index.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 文档索引
|
||||||
|
|
||||||
|
本目录包含 Seclusion 项目的所有文档。
|
||||||
|
|
||||||
|
## 文档列表
|
||||||
|
|
||||||
|
| 文档 | 说明 | 适用场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| [design.md](./design.md) | 项目设计文档 | 了解整体架构、技术选型、模块设计 |
|
||||||
|
|
||||||
|
## 快速链接
|
||||||
|
|
||||||
|
- [README.md](../README.md) - 项目入门指南
|
||||||
|
- [CLAUDE.md](../CLAUDE.md) - Claude Code 开发指引
|
||||||
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "seclusion",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.15.2",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "turbo run dev",
|
||||||
|
"build": "turbo run build",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"test": "turbo run test",
|
||||||
|
"clean": "turbo run clean && rm -rf node_modules",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||||
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||||
|
"db:generate": "turbo run db:generate",
|
||||||
|
"db:push": "turbo run db:push",
|
||||||
|
"db:migrate": "turbo run db:migrate"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.2",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"turbo": "^2.3.3",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0",
|
||||||
|
"pnpm": ">=9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
packages/eslint-config/index.js
Normal file
42
packages/eslint-config/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint', 'import'],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignorePatterns: ['node_modules/', 'dist/', '.next/', '.turbo/'],
|
||||||
|
};
|
||||||
13
packages/eslint-config/nest.js
Normal file
13
packages/eslint-config/nest.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
extends: ['./index.js'],
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
8
packages/eslint-config/next.js
Normal file
8
packages/eslint-config/next.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
extends: ['./index.js', 'next/core-web-vitals'],
|
||||||
|
rules: {
|
||||||
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
|
'react/jsx-key': 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
23
packages/eslint-config/package.json
Normal file
23
packages/eslint-config/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@seclusion/eslint-config",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.js",
|
||||||
|
"files": [
|
||||||
|
"index.js",
|
||||||
|
"next.js",
|
||||||
|
"nest.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||||
|
"@typescript-eslint/parser": "^8.18.2",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.31.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.0.0 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.57.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/shared/package.json
Normal file
37
packages/shared/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@seclusion/shared",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./types": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"import": "./dist/types/index.mjs",
|
||||||
|
"require": "./dist/types/index.js"
|
||||||
|
},
|
||||||
|
"./utils": {
|
||||||
|
"types": "./dist/utils/index.d.ts",
|
||||||
|
"import": "./dist/utils/index.mjs",
|
||||||
|
"require": "./dist/utils/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"lint": "eslint src --ext .ts,.tsx"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@seclusion/eslint-config": "workspace:*",
|
||||||
|
"@seclusion/typescript-config": "workspace:*",
|
||||||
|
"tsup": "^8.3.5",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/shared/src/index.ts
Normal file
5
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// 导出所有类型
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
// 导出所有工具函数
|
||||||
|
export * from './utils';
|
||||||
61
packages/shared/src/types/index.ts
Normal file
61
packages/shared/src/types/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// 通用 API 响应类型
|
||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页请求参数
|
||||||
|
export interface PaginationParams {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户基础类型
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户创建请求
|
||||||
|
export interface CreateUserDto {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登录请求
|
||||||
|
export interface LoginDto {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证响应
|
||||||
|
export interface AuthResponse {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
user: Omit<User, 'createdAt' | 'updatedAt'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token 载荷
|
||||||
|
export interface TokenPayload {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
}
|
||||||
75
packages/shared/src/utils/index.ts
Normal file
75
packages/shared/src/utils/index.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 日期格式化
|
||||||
|
export function formatDate(date: Date | string, format = 'YYYY-MM-DD'): string {
|
||||||
|
const d = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('YYYY', String(year))
|
||||||
|
.replace('MM', month)
|
||||||
|
.replace('DD', day)
|
||||||
|
.replace('HH', hours)
|
||||||
|
.replace('mm', minutes)
|
||||||
|
.replace('ss', seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟函数
|
||||||
|
export function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机 ID
|
||||||
|
export function generateId(length = 12): string {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 深拷贝
|
||||||
|
export function deepClone<T>(obj: T): T {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为空对象
|
||||||
|
export function isEmpty(obj: unknown): boolean {
|
||||||
|
if (obj === null || obj === undefined) return true;
|
||||||
|
if (typeof obj === 'string') return obj.trim().length === 0;
|
||||||
|
if (Array.isArray(obj)) return obj.length === 0;
|
||||||
|
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全解析 JSON
|
||||||
|
export function safeJsonParse<T>(json: string, fallback: T): T {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json) as T;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首字母大写
|
||||||
|
export function capitalize(str: string): string {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驼峰转短横线
|
||||||
|
export function camelToKebab(str: string): string {
|
||||||
|
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短横线转驼峰
|
||||||
|
export function kebabToCamel(str: string): string {
|
||||||
|
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
9
packages/shared/tsconfig.json
Normal file
9
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@seclusion/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
13
packages/shared/tsup.config.ts
Normal file
13
packages/shared/tsup.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: {
|
||||||
|
index: 'src/index.ts',
|
||||||
|
'types/index': 'src/types/index.ts',
|
||||||
|
'utils/index': 'src/utils/index.ts',
|
||||||
|
},
|
||||||
|
format: ['cjs', 'esm'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
sourcemap: true,
|
||||||
|
});
|
||||||
26
packages/typescript-config/base.json
Normal file
26
packages/typescript-config/base.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", ".next", ".turbo"]
|
||||||
|
}
|
||||||
14
packages/typescript-config/nestjs.json
Normal file
14
packages/typescript-config/nestjs.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/typescript-config/nextjs.json
Normal file
18
packages/typescript-config/nextjs.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "./base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"jsx": "preserve",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/typescript-config/package.json
Normal file
14
packages/typescript-config/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "@seclusion/typescript-config",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"base.json",
|
||||||
|
"nextjs.json",
|
||||||
|
"nestjs.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
9494
pnpm-lock.yaml
generated
Normal file
9494
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
34
turbo.json
Normal file
34
turbo.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||||
|
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["build"],
|
||||||
|
"cache": true
|
||||||
|
},
|
||||||
|
"clean": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"db:generate": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"db:push": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"db:migrate": {
|
||||||
|
"cache": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user