first commit

This commit is contained in:
sunlei 2026-05-08 11:21:51 +08:00
commit 73c4429a52
35 changed files with 9504 additions and 0 deletions

11
.env Normal file
View File

@ -0,0 +1,11 @@
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=qwqvqaqeq2333KT
DB_DATABASE=shy_template
DB_SYNC=true
MINIO_ENDPOINT=192.168.1.206
MINIO_PORT=9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin

11
.env.prod Normal file
View File

@ -0,0 +1,11 @@
DB_HOST=192.168.1.206
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=3h1@admin
DB_DATABASE=shy_template
DB_SYNC=false
MINIO_ENDPOINT=192.168.1.206
MINIO_PORT=9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin

26
.eslintrc.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin',"typeorm"],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'prettier/prettier': 'off',
},
};

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# env
/.env.dev

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "auto"
}

35
README.md Normal file
View File

@ -0,0 +1,35 @@
## 技术总览
`Node`、`Ts`、`Nest.js`、`TypeORM`、`MySQL`、`Express`
## 项目简介
此项目为`kt-template-online`服务端,基于`Node`、`Ts`开发
使用服务端框架`Nest.js`构建项目以及`ORM`框架`TypeORM`快速生成`SQL`语句以及映射`SQL`库表字段关系
## 运行项目
```bash
# 运行
$ pnpm start
# 开发环境
$ pnpm start:dev
# 生产环境
$ pnpm start:prod
```
## 测试
```bash
# unit tests
$ pnpm test
# e2e tests
$ pnpm test:e2e
# test coverage
$ pnpm test:cov
```

19
dockerfile Normal file
View File

@ -0,0 +1,19 @@
# 引用基础镜像
FROM node:22.14.0-release
# 指定工作目录
WORKDIR /app
# 拷贝文件
COPY . .
# 安装依赖
RUN npm install
RUN npm install pm2 -g
# # 声明暴露端口号
EXPOSE 48085
CMD npm run start:prod

14
ecosystem.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
apps: [
{
name: 'shy-template-server',
script: './dist/main.js',
env_prod: {
NODE_ENV: 'prod',
},
env_dev: {
NODE_ENV: 'dev',
},
},
],
};

8
nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

98
package.json Normal file
View File

@ -0,0 +1,98 @@
{
"name": "kt-template-online-api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "nest build && NODE_ENV=prod node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^9.4.3",
"@nestjs/config": "^2.3.4",
"@nestjs/core": "^9.4.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.1.1",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-express": "^9.4.3",
"@nestjs/swagger": "^7.4.2",
"@nestjs/typeorm": "^9.0.1",
"@types/dotenv": "^8.2.3",
"@types/lodash": "^4.17.24",
"@types/lodash-es": "^4.17.12",
"@types/multer": "^1.4.13",
"cross-env": "^7.0.3",
"dotenv": "^16.6.1",
"express-session": "^1.19.0",
"lodash": "^4.18.1",
"minio": "^8.0.7",
"mssql": "^9.3.2",
"multer": "1.4.5-lts.1",
"mysql2": "^3.22.3",
"nestjs-knife4j-plus": "^1.0.7",
"nestjs-minio-client": "^2.2.0",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.14",
"rxjs": "^7.8.2",
"svg-captcha": "^1.4.0",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@nestjs/cli": "^9.5.0",
"@nestjs/schematics": "^9.2.0",
"@nestjs/testing": "^9.4.3",
"@types/express": "^4.17.25",
"@types/express-session": "^1.19.0",
"@types/jest": "29.2.4",
"@types/node": "18.11.18",
"@types/passport-jwt": "^3.0.13",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^2.0.16",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.2",
"eslint-plugin-prettier": "^4.2.5",
"eslint-plugin-typeorm": "0.0.19",
"jest": "29.3.1",
"prettier": "^2.8.8",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"ts-jest": "29.0.3",
"ts-loader": "^9.5.7",
"ts-node": "^10.9.2",
"tsconfig-paths": "4.1.1",
"typescript": "^4.9.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

8241
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

13
src/app.controller.ts Normal file
View File

@ -0,0 +1,13 @@
import { Controller, Get, Redirect } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@Redirect('/api#/', 301)
getHome() {
return { url: '/api#/' };
}
}

57
src/app.module.ts Normal file
View File

@ -0,0 +1,57 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MinioModule } from 'nestjs-minio-client';
import { ComponentModule } from './component/component.module';
import { DictModule } from './dict/dict.module';
import { SaveMiddleware } from './middleware/save.middleware';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: `.env${
process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''
}`,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
synchronize: configService.get('DB_SYNC'),
entities: [__dirname + '/**/*.entity.js'],
};
},
inject: [ConfigService],
}),
MinioModule.registerAsync({
isGlobal: true,
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
return {
endPoint: configService.get('MINIO_ENDPOINT'),
port: parseInt(configService.get('MINIO_PORT')),
useSSL: false,
accessKey: configService.get('MINIO_ACCESS_KEY'),
secretKey: configService.get('MINIO_SECRET_KEY'),
};
},
inject: [ConfigService],
}),
ComponentModule,
DictModule,
],
providers: [AppService, ConfigService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(SaveMiddleware).forRoutes('*/save');
}
}

8
src/app.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

9
src/common/index.ts Normal file
View File

@ -0,0 +1,9 @@
export function DecodeDictKey(dict: Dict[]): PropertyDecorator {
return (target, key: string | symbol) => {
Reflect.defineProperty(target, `_${key.toString()}`, {
set(newVal) {
this[key] = dict.find((i) => i.value == newVal).label;
},
});
};
}

View File

@ -0,0 +1,150 @@
import {
Controller,
Get,
Post,
Res,
Query,
Body,
HttpStatus,
HttpCode,
} from '@nestjs/common';
import {
ApiExtraModels,
ApiOkResponse,
ApiOperation,
ApiProperty,
ApiQuery,
ApiTags,
PartialType,
} from '@nestjs/swagger';
import { ToolsService } from 'src/utils/tool.service';
import { ComponentService } from './component.service';
import { Component } from './component.entity';
import { PaginatedDto } from '@/utils/constant';
import { ComponentDto } from './component.dto';
class CompPageDto
extends PartialType(Component)
implements PageParams<Component>
{
@ApiProperty({
type: Number,
default: 1,
})
pageNo: number;
@ApiProperty({
type: Number,
default: 10,
})
pageSize: number;
}
class CompPageResDto extends PaginatedDto {
@ApiProperty({
type: [ComponentDto],
})
list: ComponentDto[];
}
@Controller('component')
@ApiTags('component')
@ApiExtraModels(PaginatedDto, ComponentDto)
export class ComponentController {
constructor(
private readonly toolsService: ToolsService,
private readonly componentService: ComponentService,
) {} //注入服务
@Get('allList')
@ApiOperation({ summary: '获取组件列表' })
@ApiOkResponse({ type: [ComponentDto] })
async getAllList(@Res() res) {
const list = await this.componentService.all();
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
}
@Get('list')
@ApiOperation({ summary: '获取组件列表分页' })
@ApiQuery({ type: [CompPageDto] })
@ApiOkResponse({
type: CompPageResDto,
})
async getList(
@Res() res,
@Query() { pageNo, pageSize, ...args }: PageParams<ComponentDto>,
): Promise<CompPageResDto> {
const list = await this.componentService.page({
pageNo,
pageSize,
...args,
});
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
return;
}
@Post('save')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '保存组件' })
async save(@Res() res, @Body() component: Component) {
const save = await this.componentService.save(component);
if (!save) {
res.send(this.toolsService.res(HttpStatus.BAD_REQUEST, '操作失败', null));
return;
}
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', save.id));
return;
}
@Post('remove')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '删除组件' })
@ApiQuery({ name: 'id', type: String })
async remove(@Res() res, @Query('id') id) {
const remove = await this.componentService.remove(id);
if (!remove) {
res.send(
this.toolsService.res(HttpStatus.BAD_REQUEST, '操作失败', remove),
);
return;
}
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', remove));
return;
}
@Post('update')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '编辑组件' })
async update(@Res() res, @Body() component: Component) {
const update = await this.componentService.update(component);
if (!update) {
res.send(
this.toolsService.res(HttpStatus.BAD_REQUEST, '操作失败', update),
);
return;
}
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', update));
return;
}
@Get('detail')
@ApiOperation({ summary: '组件详情' })
@ApiQuery({ name: 'id', type: String })
@ApiOkResponse({ type: ComponentDto })
async detail(@Res() res, @Query('id') id) {
const detail = await this.componentService.find(id);
if (!detail) {
res.send(this.toolsService.res(HttpStatus.BAD_REQUEST, '操作失败', null));
return;
}
res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail));
return;
}
}

View File

@ -0,0 +1,23 @@
import { DecodeDictKey } from '@/common';
import { Component } from './component.entity';
import { DictKeyEnum, DictKeyMap } from '@/utils/constant';
import { ApiProperty } from '@nestjs/swagger';
export class ComponentDto extends Component {
[x: string]: any;
@ApiProperty()
@DecodeDictKey(DictKeyMap.get(DictKeyEnum.COMPONENT_TYPE))
typeMsg: string;
@ApiProperty()
@DecodeDictKey([
...DictKeyMap.get(DictKeyEnum.CHART),
...DictKeyMap.get(DictKeyEnum.COMPONENT),
])
componentTypeMsg: string;
constructor(component: Component) {
super(component);
this._typeMsg = component.type;
this._componentTypeMsg = component.componentType;
}
}

View File

@ -0,0 +1,77 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ComponentTypeEnum, ComponentEnum } from '@/utils/constant';
@Entity()
export class Component {
constructor(component?: Component) {
Object.assign(this, component);
}
@ApiPropertyOptional()
@PrimaryGeneratedColumn('uuid')
id: string;
@ApiProperty()
@Column({
default: '',
})
name: string;
@ApiProperty({
type: 'enum',
enum: ComponentTypeEnum,
})
@Column({
type: 'enum',
enum: ComponentTypeEnum,
})
type: number;
@ApiProperty({
type: 'enum',
enum: ComponentEnum,
})
@Column({
name: 'component_type',
type: 'enum',
enum: ComponentEnum,
})
componentType: number;
@ApiProperty()
@Column({
type: 'mediumtext',
nullable: false,
})
image: string;
@ApiProperty()
@Column({
type: 'mediumtext',
nullable: false,
})
template: string;
@CreateDateColumn({
name: 'create_time',
})
createTime: Date;
@UpdateDateColumn({
name: 'update_time',
})
updateTime: Date;
@Column({
default: 0,
})
is_deleted: boolean;
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ComponentController } from './component.controller';
import { ComponentService } from './component.service';
import { Component } from './component.entity';
import { ToolsService } from 'src/utils/tool.service';
@Module({
imports: [TypeOrmModule.forFeature([Component])],
controllers: [ComponentController],
providers: [ComponentService, ToolsService],
exports: [ComponentService],
})
export class ComponentModule {}

View File

@ -0,0 +1,124 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Component } from './component.entity';
import { ToolsService } from 'src/utils/tool.service';
import { isNumber, omit, pick } from 'lodash';
import { ComponentDto } from './component.dto';
@Injectable()
export class ComponentService {
constructor(
@InjectRepository(Component)
private readonly userRepository: Repository<Component>,
private readonly toolsService: ToolsService,
) {}
async all(): Promise<Component[]> {
const components = await this.userRepository
.createQueryBuilder('component')
.getMany();
return components;
}
async page({
pageNo,
pageSize,
...args
}: PageParams<Component>): Promise<Page<Component>> {
const hasOwnEntity = new Component();
const [wheres, likes] = [['is_deleted'], ['name']] as Array<
Array<keyof Component>
>;
const [likeWhereSql, likeWhereValue] =
this.toolsService.getLikeWhere<Component>(
'component',
wheres,
likes,
pick({ ...args, is_deleted: false }, ...wheres, ...likes),
);
const [list, total] = await this.userRepository
.createQueryBuilder('component')
.select([
'component.id',
'component.name',
'component.type',
'component.componentType',
'component.image',
'component.createTime',
])
.where(likeWhereSql, likeWhereValue)
.andWhere(
omit(
pick(
args,
Object.keys(args).filter(
(key) =>
Object.hasOwn(hasOwnEntity, key) &&
(isNumber(args[key]) ? true : !!args[key]),
),
),
...wheres,
...likes,
),
)
.skip((pageNo - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
return this.toolsService.page<Component>(
list.map((component) => new ComponentDto(component)),
total,
);
}
async save(component: Component): Promise<Component> {
const link = this.userRepository.create(component);
const save = await this.userRepository.save(link);
return save;
}
async remove(id: string): Promise<boolean> {
const link = await this.userRepository
.createQueryBuilder('component')
.update()
.set({ is_deleted: true } as any)
.where('id = :id', { id })
.execute();
return link.affected > 0;
}
async update(component: Component): Promise<boolean> {
const link = await this.userRepository
.createQueryBuilder('component')
.update()
.set(component)
.where('id = :id', { id: component.id })
.execute();
return link.affected > 0;
}
async find(id: number): Promise<Component> {
const component = await this.userRepository
.createQueryBuilder('component')
.select([
'component.id',
'component.name',
'component.type',
'component.componentType',
'component.image',
'component.template',
'component.createTime',
])
.where('component.id = :id', {
id,
})
.getOne();
return component;
}
}

View File

@ -0,0 +1,44 @@
import { Controller, Get, HttpStatus, ParseIntPipe, Query, Res } from '@nestjs/common';
import { ToolsService } from 'src/utils/tool.service';
import { DictService } from './dict.service';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ComponentTypeEnum, DictKeyEnum, DictKeyType } from '@/utils/constant';
@ApiTags('dict')
@Controller('dict')
export class DictController {
constructor(
private readonly toolsService: ToolsService,
private readonly dictService: DictService,
) {} //注入服务
@ApiOperation({ summary: '根据key获取字典' })
@ApiQuery({ name: 'dictKey', enum: DictKeyEnum })
@Get('getDictByKey')
async getDictByKey(@Res() res, @Query('dictKey') dictKey: DictKeyType) {
const dict = this.toolsService.getDictByKey(dictKey)
return res.send(
this.toolsService.res(
HttpStatus.OK,
'操作成功',
dict,
),
);
}
@ApiOperation({ summary: '根据组件类型获取组件字典' })
@ApiQuery({ name: 'type', enum: ComponentTypeEnum })
@Get('getComponentDictByType')
async getComponentDictByType(@Res() res, @Query('type', ParseIntPipe) type) {
const dict = await this.dictService.getComponentDictByType(type)
return res.send(
this.toolsService.res(
HttpStatus.OK,
'操作成功',
dict,
),
);
}
}

11
src/dict/dict.module.ts Normal file
View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { DictController } from './dict.controller';
import { DictService } from './dict.service';
import { ToolsService } from 'src/utils/tool.service';
@Module({
controllers: [DictController],
providers: [DictService, ToolsService],
exports: [DictService],
})
export class DictModule {}

21
src/dict/dict.service.ts Normal file
View File

@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { ToolsService } from '@/utils/tool.service';
import { ComponentTypeEnum, DictKeyEnum } from '@/utils/constant';
@Injectable()
export class DictService {
constructor(private readonly toolsService: ToolsService) {}
async getComponentDictByType(type: ComponentTypeEnum): Promise<Dict[]> {
switch (type) {
case ComponentTypeEnum.CHART:
return this.toolsService.getDictByKey(DictKeyEnum.CHART);
case ComponentTypeEnum.COMPONENT:
return this.toolsService.getDictByKey(DictKeyEnum.COMPONENT);
default:
return this.toolsService.getDictByKey(DictKeyEnum.CHART);
}
}
}

29
src/main.ts Normal file
View File

@ -0,0 +1,29 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { urlencoded, json } from 'express';
import { knife4jSetup } from 'nestjs-knife4j-plus';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ extended: true, limit: '50mb' }));
const options = new DocumentBuilder()
.setTitle('KT-Template API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
// 启用knife4j增强关键代码
knife4jSetup(app, [
{
name: '1.0', // 文档版本名称
url: `/api-json`, // Swagger openapi JSON地址
},
]);
await app.listen(48085);
}
bootstrap();

View File

@ -0,0 +1,10 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class SaveMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: VoidFunction) {
Reflect.deleteProperty(req.body, 'id');
next();
}
}

View File

@ -0,0 +1,16 @@
import {
Controller,
} from '@nestjs/common';
import { ToolsService } from 'src/utils/tool.service';
import { MinioClientService } from './minio.service';
@Controller('minio')
export class MinioClientController {
constructor(
private readonly toolsService: ToolsService,
private readonly minioClientService: MinioClientService,
) {} //注入服务
}

13
src/minio/minio.module.ts Normal file
View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { MinioClientController } from './minio.controller';
import { MinioClientService } from './minio.service';
import { ToolsService } from 'src/utils/tool.service';
import { MinioModule } from 'nestjs-minio-client';
@Module({
imports: [MinioModule],
controllers: [MinioClientController],
providers: [MinioClientService, ToolsService],
exports: [MinioClientService],
})
export class MinioClientModule {}

View File

@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
import { ToolsService } from 'src/utils/tool.service';
@Injectable()
export class MinioClientService {
constructor(
private readonly toolsService: ToolsService,
) {}
}

22
src/types/res.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
type Res = {
code: number;
msg: string;
data: any;
};
type Page<T = any> = {
list: T[];
total: number;
};
type Dict<T = object> = {
label: string;
value: any;
} & Partial<T>;
type PageParams<T> = {
pageSize: number;
pageNo: number;
} & Partial<T>;
type Many<T> = T | readonly T[];

179
src/utils/constant.ts Normal file
View File

@ -0,0 +1,179 @@
import { ApiProperty } from '@nestjs/swagger';
export class PaginatedDto {
@ApiProperty()
total: number;
}
export enum ComponentTypeEnum {
CHART = 1,
COMPONENT = 2,
}
export enum ComponentEnum {
NOT_CATEGORY = -1,
LINE = 1,
BAR = 2,
PIE = 3,
SCATTER = 4,
MAP = 5,
CANDLESTICK = 6,
RADAR = 7,
BOX_PLOT = 8,
HEATMAP = 9,
GRAPH = 10,
LINES = 11,
TREE = 12,
TREE_MAP = 13,
SUNBURST = 14,
PARALLEL = 15,
SAN_KEY = 16,
FUNNEL = 17,
GAUGE = 18,
PICTORIAL_BAR = 19,
THEME_RIVER = 20,
LIQUID_FILL = 21,
WORD_CLOUD = 22,
TABLE = 23,
FORM = 24,
CONTAINER = 25,
}
export enum DictKeyEnum {
COMPONENT_TYPE = 'COMPONENT_TYPE',
CHART = 'CHART',
COMPONENT = 'COMPONENT',
}
export type DictKeyType = keyof typeof DictKeyEnum;
export const DictKeyMap: Map<DictKeyType, Dict[]> = new Map();
const ComponentTypeDict = [
{
label: '图表',
value: 1,
},
{
label: '组件',
value: 2,
},
];
const ChartDict = [
{
label: '未分类',
value: -1,
},
{
label: '折线图',
value: 1,
},
{
label: '柱状图',
value: 2,
},
{
label: '饼图',
value: 3,
},
{
label: '散点图',
value: 4,
},
{
label: '地图',
value: 5,
},
{
label: 'K线图',
value: 6,
},
{
label: '雷达图',
value: 7,
},
{
label: '盒须图',
value: 8,
},
{
label: '热力图',
value: 9,
},
{
label: '关系图',
value: 10,
},
{
label: '路径图',
value: 11,
},
{
label: '树图',
value: 12,
},
{
label: '矩树图',
value: 13,
},
{
label: '旭日图',
value: 14,
},
{
label: '平行坐标系',
value: 15,
},
{
label: '桑基图',
value: 16,
},
{
label: '漏斗图',
value: 17,
},
{
label: '仪表盘',
value: 18,
},
{
label: '象形图',
value: 19,
},
{
label: '河流图',
value: 20,
},
{
label: '水球',
value: 21,
},
{
label: '词云',
value: 22,
},
];
const ComponentDict = [
{
label: '未分类',
value: -1,
},
{
label: '表格',
value: 23,
},
{
label: '表单',
value: 24,
},
{
label: '容器',
value: 25,
},
];
DictKeyMap.set(DictKeyEnum.COMPONENT_TYPE, ComponentTypeDict);
DictKeyMap.set(DictKeyEnum.CHART, ChartDict);
DictKeyMap.set(DictKeyEnum.COMPONENT, ComponentDict);

105
src/utils/tool.service.ts Normal file
View File

@ -0,0 +1,105 @@
import { Injectable } from '@nestjs/common';
import * as svgCaptcha from 'svg-captcha';
import { DictKeyMap, DictKeyEnum } from './constant';
import type { DictKeyType } from './constant';
import { isBoolean } from 'lodash';
@Injectable()
export class ToolsService {
async captche(size = 4) {
const captcha = svgCaptcha.create({
//可配置返回的图片信息
size, //生成几个验证码
fontSize: 50, //文字大小
width: 100, //宽度
height: 34, //高度
background: '#ffffff', //背景颜色
});
return captcha;
}
res(code: number, msg: string, data: any): Res {
const retn: Res = {
code,
msg,
data,
};
return retn;
}
page<T = any>(list: T[], total: number): Page<T> {
const retn = {
list,
total,
};
return retn;
}
getWhereStr(alias: string, key) {
return `${alias}.${key.toString()} = :${key.toString()}`;
}
getLikeStr(alias: string, key) {
return `${alias}.${key.toString()} like :${key.toString()}`;
}
getLikeWhere<T = object>(
alias: string,
wheres: Array<keyof T>,
likes: Array<keyof T>,
values: Partial<T>,
operator = 'AND',
): [string, Record<keyof T, string>] {
const linkOperator = ` ${operator} `;
const wheresEndIndex = wheres.length;
return [
[...wheres, ...likes].reduce((pre, cur, index, source) => {
const isLink = !!source
.slice(0, index)
.some((key) => isBoolean(values[key]) || !!values[key]);
const { getLikeStr, getWhereStr } = this;
if (!isBoolean(values[cur]) && !values[cur]) return pre;
if (!index) getWhereStr(alias, cur);
const matchSqlFn = index >= wheresEndIndex ? getLikeStr : getWhereStr;
const beforeSql = `${pre}${isLink ? linkOperator : ' '}`;
return `${beforeSql}${matchSqlFn(alias, cur)}`;
}, ''),
Object.entries(values).reduce((pre, [key, value]) => {
if (!isBoolean(value) && !value) return pre;
if (likes.includes(key as keyof T))
return { ...pre, ...{ [key]: `%${value}%` } };
return { ...pre, ...{ [key]: value } };
}, {} as Record<keyof T, string>),
];
}
getDictByKey(key: DictKeyType): Dict[] {
if (!DictKeyEnum[key]) {
return [];
}
return DictKeyMap.get(key);
}
dictFormat<T = object>(
label: string,
value: any,
other: Partial<T>,
): Dict<T> {
const options = {
label,
value,
...other,
};
return options;
}
}

24
test/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ESNext",
"sourceMap": true,
"outDir": "./dist",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"@/*": [
"./src/*"
],
}
}
}