feat(plop): 优化生成器支持 Prisma 关联关系
- 支持一对多/多对一关系定义并生成到 Prisma schema - 简化流程:查询关联配置根据关系自动预填 - 修复 Handlebars 模板 HTML 转义导致的乱码问题 - 修复 controller 模板缺少 Prisma 导入的问题 - 新增页面模板 (page.hbs) 生成前端页面 - 添加 FindAllParams/PaginationQueryDto 索引签名修复类型兼容 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,9 +116,11 @@ export const DEFAULT_CRUD_OPTIONS: Required<CrudServiceOptions> = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询参数(扩展 shared 包的类型,添加过滤条件)
|
* 分页查询参数(扩展 shared 包的类型,添加过滤条件)
|
||||||
|
* 允许额外的查询参数用于 filterableFields 过滤
|
||||||
*/
|
*/
|
||||||
export interface FindAllParams<WhereInput = Record<string, unknown>> extends PaginationParams {
|
export interface FindAllParams<WhereInput = Record<string, unknown>> extends PaginationParams {
|
||||||
where?: WhereInput;
|
where?: WhereInput;
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { IsInt, IsOptional, IsString, Matches } from 'class-validator';
|
|||||||
* - pageSize=-1 或 pageSize=0 返回所有数据
|
* - pageSize=-1 或 pageSize=0 返回所有数据
|
||||||
*/
|
*/
|
||||||
export class PaginationQueryDto implements PaginationParams {
|
export class PaginationQueryDto implements PaginationParams {
|
||||||
|
// 索引签名:允许额外的查询参数(用于 filterableFields)
|
||||||
|
[key: string]: unknown;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: '页码', default: 1, minimum: 1 })
|
@ApiPropertyOptional({ description: '页码', default: 1, minimum: 1 })
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
|
"@types/pluralize": "^0.0.33",
|
||||||
"plop": "^4.0.4",
|
"plop": "^4.0.4",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"turbo": "^2.3.3",
|
"turbo": "^2.3.3",
|
||||||
|
|||||||
@@ -9,13 +9,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NodePlopAPI, ActionType } from 'plop';
|
import type { NodePlopAPI, ActionType } from 'plop';
|
||||||
|
import pluralize from 'pluralize';
|
||||||
import { parseFields, type FieldDefinition } from '../utils/field-parser';
|
import { parseFields, type FieldDefinition } from '../utils/field-parser';
|
||||||
import {
|
import {
|
||||||
parseRelations,
|
parseRelations,
|
||||||
parseManyToMany,
|
parseManyToMany,
|
||||||
parseCountRelations,
|
parseCountRelations,
|
||||||
|
parseOneToMany,
|
||||||
|
parseManyToOne,
|
||||||
type RelationConfig,
|
type RelationConfig,
|
||||||
type ManyToManyConfig,
|
type ManyToManyConfig,
|
||||||
|
type OneToManyConfig,
|
||||||
|
type ManyToOneConfig,
|
||||||
} from '../utils/relation-parser';
|
} from '../utils/relation-parser';
|
||||||
import {
|
import {
|
||||||
parseSchema,
|
parseSchema,
|
||||||
@@ -56,11 +61,15 @@ interface TemplateData {
|
|||||||
serviceType: ServiceType;
|
serviceType: ServiceType;
|
||||||
relations: RelationConfig[];
|
relations: RelationConfig[];
|
||||||
manyToMany: ManyToManyConfig[];
|
manyToMany: ManyToManyConfig[];
|
||||||
|
oneToMany: OneToManyConfig[];
|
||||||
|
manyToOne: ManyToOneConfig[];
|
||||||
countRelations: string[];
|
countRelations: string[];
|
||||||
|
|
||||||
// 派生标志
|
// 派生标志
|
||||||
hasRelations: boolean;
|
hasRelations: boolean;
|
||||||
hasManyToMany: boolean;
|
hasManyToMany: boolean;
|
||||||
|
hasOneToMany: boolean;
|
||||||
|
hasManyToOne: boolean;
|
||||||
hasCountRelations: boolean;
|
hasCountRelations: boolean;
|
||||||
needsResponseDto: boolean;
|
needsResponseDto: boolean;
|
||||||
needsDetailDto: boolean;
|
needsDetailDto: boolean;
|
||||||
@@ -90,12 +99,6 @@ export function crudGenerator(plop: NodePlopAPI) {
|
|||||||
message: '模块中文名(如 产品):',
|
message: '模块中文名(如 产品):',
|
||||||
validate: (input: string) => !!input || '请输入中文名',
|
validate: (input: string) => !!input || '请输入中文名',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'pluralName',
|
|
||||||
message: '复数名称(如 products):',
|
|
||||||
default: (answers: { name: string }) => `${answers.name}s`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
name: 'generateTargets',
|
name: 'generateTargets',
|
||||||
@@ -146,59 +149,6 @@ export function crudGenerator(plop: NodePlopAPI) {
|
|||||||
name:string 名称 "示例名称" min:2 max:100
|
name:string 名称 "示例名称" min:2 max:100
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
// 关联关系配置(RelationCrudService 或 ManyToManyCrudService 时显示)
|
|
||||||
{
|
|
||||||
type: 'checkbox',
|
|
||||||
name: 'relationModels',
|
|
||||||
message: '选择要关联的模型(可多选,之后会配置字段):',
|
|
||||||
when: (answers: { serviceType: ServiceType }) =>
|
|
||||||
answers.serviceType === 'RelationCrudService' ||
|
|
||||||
answers.serviceType === 'ManyToManyCrudService',
|
|
||||||
choices: () => {
|
|
||||||
try {
|
|
||||||
const models = parseSchema();
|
|
||||||
return getAvailableModels(models).map((m) => ({
|
|
||||||
name: m,
|
|
||||||
value: m,
|
|
||||||
}));
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'editor',
|
|
||||||
name: 'relationsRaw',
|
|
||||||
message: '配置关联关系(已预填选中的模型,可调整字段和选项):',
|
|
||||||
when: (answers: { serviceType: ServiceType; relationModels?: string[] }) =>
|
|
||||||
(answers.serviceType === 'RelationCrudService' ||
|
|
||||||
answers.serviceType === 'ManyToManyCrudService') &&
|
|
||||||
answers.relationModels &&
|
|
||||||
answers.relationModels.length > 0,
|
|
||||||
default: (answers: { relationModels?: string[] }) => {
|
|
||||||
const models = parseSchema();
|
|
||||||
const lines: string[] = [
|
|
||||||
'# 关联关系配置(每行一个)',
|
|
||||||
'# 格式: 关联名:目标模型 字段1,字段2,... [noList]',
|
|
||||||
'# noList: 不在列表中显示',
|
|
||||||
'#',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const modelName of answers.relationModels || []) {
|
|
||||||
const model = getModelByName(models, modelName);
|
|
||||||
if (model) {
|
|
||||||
const fields = getSelectableFields(model);
|
|
||||||
// 默认选择 id 和前几个重要字段
|
|
||||||
const defaultFields = fields.slice(0, 4).join(',');
|
|
||||||
const relationName = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
||||||
lines.push(`${relationName}:${modelName} ${defaultFields}`);
|
|
||||||
lines.push(`# 可用字段: ${fields.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join('\n') + '\n';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// 多对多关系配置(ManyToManyCrudService 时显示)
|
// 多对多关系配置(ManyToManyCrudService 时显示)
|
||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
@@ -260,6 +210,137 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
return lines.join('\n') + '\n';
|
return lines.join('\n') + '\n';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 一对多关系配置(新模型包含多个目标模型)
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'oneToManyModels',
|
||||||
|
message: '选择一对多关联的目标模型(新模型包含多个目标,如:宿舍包含多个学生):',
|
||||||
|
choices: () => {
|
||||||
|
try {
|
||||||
|
const models = parseSchema();
|
||||||
|
return getAvailableModels(models).map((m) => ({
|
||||||
|
name: m,
|
||||||
|
value: m,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'oneToManyRaw',
|
||||||
|
message: '配置一对多关系:',
|
||||||
|
when: (answers: { oneToManyModels?: string[] }) =>
|
||||||
|
answers.oneToManyModels && answers.oneToManyModels.length > 0,
|
||||||
|
default: (answers: { name: string; oneToManyModels?: string[] }) => {
|
||||||
|
const lines: string[] = [
|
||||||
|
'# 一对多关系配置(每行一个)',
|
||||||
|
'# 格式: 关系名:目标模型 [optional]',
|
||||||
|
'# optional: 表示外键可为空',
|
||||||
|
'#',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const targetModelName of answers.oneToManyModels || []) {
|
||||||
|
const relationName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1) + 's';
|
||||||
|
lines.push(`${relationName}:${targetModelName} optional`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n') + '\n';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 多对一关系配置(新模型属于一个目标模型)
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'manyToOneModels',
|
||||||
|
message: '选择多对一关联的目标模型(新模型属于目标,如:成绩属于学生):',
|
||||||
|
choices: () => {
|
||||||
|
try {
|
||||||
|
const models = parseSchema();
|
||||||
|
return getAvailableModels(models).map((m) => ({
|
||||||
|
name: m,
|
||||||
|
value: m,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'manyToOneRaw',
|
||||||
|
message: '配置多对一关系:',
|
||||||
|
when: (answers: { manyToOneModels?: string[] }) =>
|
||||||
|
answers.manyToOneModels && answers.manyToOneModels.length > 0,
|
||||||
|
default: (answers: { name: string; manyToOneModels?: string[] }) => {
|
||||||
|
const lines: string[] = [
|
||||||
|
'# 多对一关系配置(每行一个)',
|
||||||
|
'# 格式: 关联名:目标模型 [optional]',
|
||||||
|
'# optional: 表示外键可为空',
|
||||||
|
'#',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const targetModelName of answers.manyToOneModels || []) {
|
||||||
|
const relationName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);
|
||||||
|
lines.push(`${relationName}:${targetModelName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n') + '\n';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 查询关联配置(根据一对多/多对一自动预填,用于配置 API 响应返回哪些关联字段)
|
||||||
|
{
|
||||||
|
type: 'editor',
|
||||||
|
name: 'relationsRaw',
|
||||||
|
message: '配置查询时要返回的关联字段(已根据关系自动预填):',
|
||||||
|
when: (answers: {
|
||||||
|
serviceType: ServiceType;
|
||||||
|
oneToManyModels?: string[];
|
||||||
|
manyToOneModels?: string[];
|
||||||
|
}) =>
|
||||||
|
(answers.serviceType === 'RelationCrudService' ||
|
||||||
|
answers.serviceType === 'ManyToManyCrudService') &&
|
||||||
|
((answers.oneToManyModels && answers.oneToManyModels.length > 0) ||
|
||||||
|
(answers.manyToOneModels && answers.manyToOneModels.length > 0)),
|
||||||
|
default: (answers: {
|
||||||
|
oneToManyModels?: string[];
|
||||||
|
manyToOneModels?: string[];
|
||||||
|
}) => {
|
||||||
|
const models = parseSchema();
|
||||||
|
const lines: string[] = [
|
||||||
|
'# 查询关联配置(每行一个)',
|
||||||
|
'# 格式: 关联名:目标模型 字段1,字段2,... [noList]',
|
||||||
|
'# noList: 不在列表中显示(仅详情显示)',
|
||||||
|
'#',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据多对一关系生成(查询时 include 关联对象)
|
||||||
|
for (const modelName of answers.manyToOneModels || []) {
|
||||||
|
const model = getModelByName(models, modelName);
|
||||||
|
if (model) {
|
||||||
|
const fields = getSelectableFields(model);
|
||||||
|
const defaultFields = fields.slice(0, 4).join(',');
|
||||||
|
const relationName = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
||||||
|
lines.push(`${relationName}:${modelName} ${defaultFields}`);
|
||||||
|
lines.push(`# 可用字段: ${fields.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据一对多关系生成(查询时 include 关联数组,默认 noList)
|
||||||
|
for (const modelName of answers.oneToManyModels || []) {
|
||||||
|
const model = getModelByName(models, modelName);
|
||||||
|
if (model) {
|
||||||
|
const fields = getSelectableFields(model);
|
||||||
|
const defaultFields = fields.slice(0, 4).join(',');
|
||||||
|
const relationName = modelName.charAt(0).toLowerCase() + modelName.slice(1) + 's';
|
||||||
|
lines.push(`${relationName}:${modelName} ${defaultFields} noList`);
|
||||||
|
lines.push(`# 可用字段: ${fields.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n') + '\n';
|
||||||
|
},
|
||||||
|
},
|
||||||
// 统计关系配置(RelationCrudService 或 ManyToManyCrudService 时显示)
|
// 统计关系配置(RelationCrudService 或 ManyToManyCrudService 时显示)
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
@@ -346,6 +427,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
|
|
||||||
// 解析关联配置
|
// 解析关联配置
|
||||||
const serviceType = data.serviceType as ServiceType;
|
const serviceType = data.serviceType as ServiceType;
|
||||||
|
const pascalName = data.name.charAt(0).toUpperCase() + data.name.slice(1);
|
||||||
const relations =
|
const relations =
|
||||||
serviceType !== 'CrudService' && data.relationsRaw
|
serviceType !== 'CrudService' && data.relationsRaw
|
||||||
? parseRelations(data.relationsRaw)
|
? parseRelations(data.relationsRaw)
|
||||||
@@ -354,6 +436,12 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
serviceType === 'ManyToManyCrudService' && data.manyToManyRaw
|
serviceType === 'ManyToManyCrudService' && data.manyToManyRaw
|
||||||
? parseManyToMany(data.manyToManyRaw)
|
? parseManyToMany(data.manyToManyRaw)
|
||||||
: [];
|
: [];
|
||||||
|
const oneToMany = data.oneToManyRaw
|
||||||
|
? parseOneToMany(data.oneToManyRaw, pascalName)
|
||||||
|
: [];
|
||||||
|
const manyToOne = data.manyToOneRaw
|
||||||
|
? parseManyToOne(data.manyToOneRaw, pluralize(data.name))
|
||||||
|
: [];
|
||||||
const countRelations =
|
const countRelations =
|
||||||
serviceType !== 'CrudService' && data.countRelationsRaw
|
serviceType !== 'CrudService' && data.countRelationsRaw
|
||||||
? parseCountRelations(data.countRelationsRaw)
|
? parseCountRelations(data.countRelationsRaw)
|
||||||
@@ -362,6 +450,8 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
// 派生标志
|
// 派生标志
|
||||||
const hasRelations = relations.length > 0;
|
const hasRelations = relations.length > 0;
|
||||||
const hasManyToMany = manyToMany.length > 0;
|
const hasManyToMany = manyToMany.length > 0;
|
||||||
|
const hasOneToMany = oneToMany.length > 0;
|
||||||
|
const hasManyToOne = manyToOne.length > 0;
|
||||||
const hasCountRelations = countRelations.length > 0;
|
const hasCountRelations = countRelations.length > 0;
|
||||||
const needsResponseDto = serviceType !== 'CrudService';
|
const needsResponseDto = serviceType !== 'CrudService';
|
||||||
const needsDetailDto = serviceType === 'ManyToManyCrudService' && (hasManyToMany || hasCountRelations);
|
const needsDetailDto = serviceType === 'ManyToManyCrudService' && (hasManyToMany || hasCountRelations);
|
||||||
@@ -370,7 +460,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
const templateData: TemplateData = {
|
const templateData: TemplateData = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
chineseName: data.chineseName,
|
chineseName: data.chineseName,
|
||||||
pluralName: data.pluralName,
|
pluralName: pluralize(data.name),
|
||||||
generateTargets: data.generateTargets,
|
generateTargets: data.generateTargets,
|
||||||
softDelete: data.softDelete,
|
softDelete: data.softDelete,
|
||||||
fields,
|
fields,
|
||||||
@@ -399,9 +489,13 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
serviceType,
|
serviceType,
|
||||||
relations,
|
relations,
|
||||||
manyToMany,
|
manyToMany,
|
||||||
|
oneToMany,
|
||||||
|
manyToOne,
|
||||||
countRelations,
|
countRelations,
|
||||||
hasRelations,
|
hasRelations,
|
||||||
hasManyToMany,
|
hasManyToMany,
|
||||||
|
hasOneToMany,
|
||||||
|
hasManyToOne,
|
||||||
hasCountRelations,
|
hasCountRelations,
|
||||||
needsResponseDto,
|
needsResponseDto,
|
||||||
needsDetailDto,
|
needsDetailDto,
|
||||||
@@ -417,6 +511,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/api/src/{{kebabCase name}}/dto/{{kebabCase name}}.dto.ts',
|
path: 'apps/api/src/{{kebabCase name}}/dto/{{kebabCase name}}.dto.ts',
|
||||||
templateFile: 'templates/api/dto.hbs',
|
templateFile: 'templates/api/dto.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Service
|
// Service
|
||||||
@@ -425,6 +520,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.service.ts',
|
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.service.ts',
|
||||||
templateFile: 'templates/api/service.hbs',
|
templateFile: 'templates/api/service.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Controller
|
// Controller
|
||||||
@@ -433,6 +529,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.controller.ts',
|
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.controller.ts',
|
||||||
templateFile: 'templates/api/controller.hbs',
|
templateFile: 'templates/api/controller.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Module
|
// Module
|
||||||
@@ -441,6 +538,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.module.ts',
|
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.module.ts',
|
||||||
templateFile: 'templates/api/module.hbs',
|
templateFile: 'templates/api/module.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修改 app.module.ts - 添加导入
|
// 修改 app.module.ts - 添加导入
|
||||||
@@ -450,6 +548,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
pattern: /(import \{ \w+Module \} from '\.\/\w+\/\w+\.module';)\n(\n@Module)/,
|
pattern: /(import \{ \w+Module \} from '\.\/\w+\/\w+\.module';)\n(\n@Module)/,
|
||||||
template: `$1\nimport { {{pascalCase name}}Module } from './{{kebabCase name}}/{{kebabCase name}}.module';\n$2`,
|
template: `$1\nimport { {{pascalCase name}}Module } from './{{kebabCase name}}/{{kebabCase name}}.module';\n$2`,
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修改 app.module.ts - 添加到 imports 数组
|
// 修改 app.module.ts - 添加到 imports 数组
|
||||||
@@ -459,6 +558,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
pattern: /(\s+)(StudentModule,?\s*\n)(\s*\],)/,
|
pattern: /(\s+)(StudentModule,?\s*\n)(\s*\],)/,
|
||||||
template: `$1$2$1{{pascalCase name}}Module,\n$3`,
|
template: `$1$2$1{{pascalCase name}}Module,\n$3`,
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,6 +570,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/web/src/services/{{kebabCase name}}.service.ts',
|
path: 'apps/web/src/services/{{kebabCase name}}.service.ts',
|
||||||
templateFile: 'templates/web/service.hbs',
|
templateFile: 'templates/web/service.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
@@ -478,6 +579,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/web/src/hooks/use{{pascalCase pluralName}}.ts',
|
path: 'apps/web/src/hooks/use{{pascalCase pluralName}}.ts',
|
||||||
templateFile: 'templates/web/hooks.hbs',
|
templateFile: 'templates/web/hooks.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件目录
|
// 组件目录
|
||||||
@@ -486,6 +588,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase pluralName}}Table.tsx',
|
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase pluralName}}Table.tsx',
|
||||||
templateFile: 'templates/web/table.hbs',
|
templateFile: 'templates/web/table.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
@@ -493,6 +596,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase name}}CreateDialog.tsx',
|
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase name}}CreateDialog.tsx',
|
||||||
templateFile: 'templates/web/create-dialog.hbs',
|
templateFile: 'templates/web/create-dialog.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
@@ -500,6 +604,16 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase name}}EditDialog.tsx',
|
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase name}}EditDialog.tsx',
|
||||||
templateFile: 'templates/web/edit-dialog.hbs',
|
templateFile: 'templates/web/edit-dialog.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面
|
||||||
|
actions.push({
|
||||||
|
type: 'add',
|
||||||
|
path: 'apps/web/src/app/(dashboard)/{{kebabCase pluralName}}/page.tsx',
|
||||||
|
templateFile: 'templates/web/page.hbs',
|
||||||
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修改 constants.ts - 添加 API 端点
|
// 修改 constants.ts - 添加 API 端点
|
||||||
@@ -509,6 +623,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
pattern: /(USERS:\s*['"]\/users['"],?)/,
|
pattern: /(USERS:\s*['"]\/users['"],?)/,
|
||||||
template: `$1\n {{constantCase pluralName}}: '/{{kebabCase pluralName}}',`,
|
template: `$1\n {{constantCase pluralName}}: '/{{kebabCase pluralName}}',`,
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,6 +634,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'packages/shared/src/types/{{kebabCase name}}.ts',
|
path: 'packages/shared/src/types/{{kebabCase name}}.ts',
|
||||||
templateFile: 'templates/shared/types.hbs',
|
templateFile: 'templates/shared/types.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修改 types/index.ts - 添加导出
|
// 修改 types/index.ts - 添加导出
|
||||||
@@ -527,6 +643,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'packages/shared/src/types/index.ts',
|
path: 'packages/shared/src/types/index.ts',
|
||||||
template: `\n// {{chineseName}}\nexport * from './{{kebabCase name}}';\n`,
|
template: `\n// {{chineseName}}\nexport * from './{{kebabCase name}}';\n`,
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,6 +654,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
path: 'apps/api/prisma/schema.prisma',
|
path: 'apps/api/prisma/schema.prisma',
|
||||||
templateFile: 'templates/prisma/model.hbs',
|
templateFile: 'templates/prisma/model.hbs',
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果启用软删除,修改 prisma.service.ts
|
// 如果启用软删除,修改 prisma.service.ts
|
||||||
@@ -547,6 +665,7 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
pattern: /(const SOFT_DELETE_MODELS:\s*Prisma\.ModelName\[\]\s*=\s*\[[\s\S]*?)(\];)/,
|
pattern: /(const SOFT_DELETE_MODELS:\s*Prisma\.ModelName\[\]\s*=\s*\[[\s\S]*?)(\];)/,
|
||||||
template: `$1, '{{pascalCase name}}'$2`,
|
template: `$1, '{{pascalCase name}}'$2`,
|
||||||
data: templateData,
|
data: templateData,
|
||||||
|
abortOnFail: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,14 +680,44 @@ name:string 名称 "示例名称" min:2 max:100
|
|||||||
if (hasManyToMany) {
|
if (hasManyToMany) {
|
||||||
console.log(`多对多关系: ${manyToMany.map((m) => m.name).join(', ')}`);
|
console.log(`多对多关系: ${manyToMany.map((m) => m.name).join(', ')}`);
|
||||||
}
|
}
|
||||||
|
if (hasOneToMany) {
|
||||||
|
console.log(`一对多关系: ${oneToMany.map((o) => `${o.name} → ${o.targetModel}`).join(', ')}`);
|
||||||
|
}
|
||||||
|
if (hasManyToOne) {
|
||||||
|
console.log(`多对一关系: ${manyToOne.map((m) => `${m.name} → ${m.targetModel}`).join(', ')}`);
|
||||||
|
}
|
||||||
if (hasCountRelations) {
|
if (hasCountRelations) {
|
||||||
console.log(`统计关系: ${countRelations.join(', ')}`);
|
console.log(`统计关系: ${countRelations.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打印需要手动修改目标模型的提示
|
||||||
|
if (hasOneToMany || hasManyToOne) {
|
||||||
|
console.log('\n⚠️ 需要手动修改目标模型:');
|
||||||
|
if (hasOneToMany) {
|
||||||
|
console.log('\n--- 一对多关系(请在目标模型中添加外键字段)---');
|
||||||
|
for (const rel of oneToMany) {
|
||||||
|
console.log(`\n 在 model ${rel.targetModel} 中添加:`);
|
||||||
|
console.log(` ${rel.foreignKey} String${rel.optional ? '?' : ''}`);
|
||||||
|
console.log(` ${rel.backRelation} ${pascalName}${rel.optional ? '?' : ''} @relation(fields: [${rel.foreignKey}], references: [id])`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasManyToOne) {
|
||||||
|
console.log('\n--- 多对一关系(请在目标模型中添加反向关联)---');
|
||||||
|
for (const rel of manyToOne) {
|
||||||
|
console.log(`\n 在 model ${rel.targetModel} 中添加:`);
|
||||||
|
console.log(` ${rel.backRelation} ${pascalName}[]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('\n后续步骤:');
|
console.log('\n后续步骤:');
|
||||||
if (data.generateTargets.includes('prisma')) {
|
if (hasOneToMany || hasManyToOne) {
|
||||||
|
console.log('1. 按照上述提示修改目标模型');
|
||||||
|
console.log('2. 运行 pnpm db:generate && pnpm db:push');
|
||||||
|
} else if (data.generateTargets.includes('prisma')) {
|
||||||
console.log('1. 运行 pnpm db:generate && pnpm db:push');
|
console.log('1. 运行 pnpm db:generate && pnpm db:push');
|
||||||
}
|
}
|
||||||
console.log('2. 重启开发服务器 pnpm dev\n');
|
console.log(`${hasOneToMany || hasManyToOne ? '3' : '2'}. 重启开发服务器 pnpm dev\n`);
|
||||||
return '完成';
|
return '完成';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
getFormControl,
|
getFormControl,
|
||||||
getCellRenderer,
|
getCellRenderer,
|
||||||
getWhereCondition,
|
getWhereCondition,
|
||||||
|
getFormattedExample,
|
||||||
} from '../utils/field-parser';
|
} from '../utils/field-parser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +93,10 @@ export function registerHelpers(plop: NodePlopAPI) {
|
|||||||
|
|
||||||
plop.setHelper('cellRenderer', (field: FieldDefinition) => getCellRenderer(field));
|
plop.setHelper('cellRenderer', (field: FieldDefinition) => getCellRenderer(field));
|
||||||
|
|
||||||
|
// ===== Example 格式化 helpers =====
|
||||||
|
|
||||||
|
plop.setHelper('formattedExample', (field: FieldDefinition) => getFormattedExample(field));
|
||||||
|
|
||||||
// ===== 查询条件 helpers =====
|
// ===== 查询条件 helpers =====
|
||||||
|
|
||||||
plop.setHelper('whereCondition', (field: FieldDefinition) => getWhereCondition(field));
|
plop.setHelper('whereCondition', (field: FieldDefinition) => getWhereCondition(field));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiCreatedResponse,
|
ApiCreatedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
{{#if (eq serviceType 'CrudService')}}
|
{{#if hasQueryDto}}
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
|
|||||||
|
|
||||||
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
|
||||||
{{#unless hasQueryDto}}
|
{{#unless hasQueryDto}}
|
||||||
import { PaginationQueryDto } from '@/common/crud';
|
import { PaginationQueryDto } from '@/common/crud/dto/pagination.dto';
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
@ApiTags('{{chineseName}}')
|
@ApiTags('{{chineseName}}')
|
||||||
|
|||||||
@@ -13,15 +13,16 @@ import type {
|
|||||||
{{pascalCase name}}Response,
|
{{pascalCase name}}Response,
|
||||||
} from '@seclusion/shared';
|
} from '@seclusion/shared';
|
||||||
|
|
||||||
import { createPaginatedResponseDto, PaginationQueryDto } from '@/common/crud';
|
import { createPaginatedResponseDto } from '@/common/crud/dto/paginated-response.dto';
|
||||||
|
import { PaginationQueryDto } from '@/common/crud/dto/pagination.dto';
|
||||||
|
|
||||||
/** 创建{{chineseName}}请求 DTO */
|
/** 创建{{chineseName}}请求 DTO */
|
||||||
export class Create{{pascalCase name}}Dto implements ICreate{{pascalCase name}}Dto {
|
export class Create{{pascalCase name}}Dto implements ICreate{{pascalCase name}}Dto {
|
||||||
{{#each createFields}}
|
{{#each createFields}}
|
||||||
{{#if nullable}}
|
{{#if nullable}}
|
||||||
@ApiPropertyOptional({ example: {{{example}}}, description: '{{label}}' })
|
@ApiPropertyOptional({ example: {{{formattedExample this}}}, description: '{{label}}' })
|
||||||
{{else}}
|
{{else}}
|
||||||
@ApiProperty({ example: {{{example}}}, description: '{{label}}' })
|
@ApiProperty({ example: {{{formattedExample this}}}, description: '{{label}}' })
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#each (validationDecorators this)}}
|
{{#each (validationDecorators this)}}
|
||||||
{{{this}}}
|
{{{this}}}
|
||||||
@@ -37,7 +38,7 @@ export class Create{{pascalCase name}}Dto implements ICreate{{pascalCase name}}D
|
|||||||
/** 更新{{chineseName}}请求 DTO */
|
/** 更新{{chineseName}}请求 DTO */
|
||||||
export class Update{{pascalCase name}}Dto implements IUpdate{{pascalCase name}}Dto {
|
export class Update{{pascalCase name}}Dto implements IUpdate{{pascalCase name}}Dto {
|
||||||
{{#each updateFields}}
|
{{#each updateFields}}
|
||||||
@ApiPropertyOptional({ example: {{{example}}}, description: '{{label}}' })
|
@ApiPropertyOptional({ example: {{{formattedExample this}}}, description: '{{label}}' })
|
||||||
{{#each (validationDecorators this)}}
|
{{#each (validationDecorators this)}}
|
||||||
{{{this}}}
|
{{{this}}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
@@ -99,10 +100,10 @@ export class {{pascalCase name}}ResponseDto implements {{pascalCase name}}Respon
|
|||||||
|
|
||||||
{{#each responseFields}}
|
{{#each responseFields}}
|
||||||
{{#if nullable}}
|
{{#if nullable}}
|
||||||
@ApiProperty({ example: {{{example}}}, description: '{{label}}', nullable: true })
|
@ApiProperty({ example: {{{formattedExample this}}}, description: '{{label}}', nullable: true })
|
||||||
{{name}}: {{tsResponseType this}} | null;
|
{{name}}: {{tsResponseType this}} | null;
|
||||||
{{else}}
|
{{else}}
|
||||||
@ApiProperty({ example: {{{example}}}, description: '{{label}}' })
|
@ApiProperty({ example: {{{formattedExample this}}}, description: '{{label}}' })
|
||||||
{{name}}: {{tsResponseType this}};
|
{{name}}: {{tsResponseType this}};
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,42 @@ model {{pascalCase name}} {
|
|||||||
id String @id @default(cuid(2))
|
id String @id @default(cuid(2))
|
||||||
{{#each fields}}
|
{{#each fields}}
|
||||||
{{name}} {{prismaType this}}{{#if unique}} @unique{{/if}}
|
{{name}} {{prismaType this}}{{#if unique}} @unique{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
{{#each manyToOne}}
|
||||||
|
{{foreignKey}} String{{#if optional}}?{{/if}}
|
||||||
|
{{name}} {{targetModel}}{{#if optional}}?{{/if}} @relation(fields: [{{foreignKey}}], references: [id])
|
||||||
{{/each}}
|
{{/each}}
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
{{#if softDelete}}
|
{{#if softDelete}}
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#each oneToMany}}
|
||||||
|
|
||||||
|
// 一对多:一个{{../chineseName}}有多个{{targetModel}}
|
||||||
|
{{name}} {{targetModel}}[]
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
@@map("{{snakeCase pluralName}}")
|
@@map("{{snakeCase pluralName}}")
|
||||||
}
|
}
|
||||||
|
{{#if hasOneToMany}}
|
||||||
|
|
||||||
|
// ⚠️ 请在以下模型中添加外键字段:
|
||||||
|
{{#each oneToMany}}
|
||||||
|
// model {{targetModel}} {
|
||||||
|
// ...
|
||||||
|
// {{foreignKey}} String{{#if optional}}?{{/if}}
|
||||||
|
// {{backRelation}} {{pascalCase ../name}}{{#if optional}}?{{/if}} @relation(fields: [{{foreignKey}}], references: [id])
|
||||||
|
// }
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if hasManyToOne}}
|
||||||
|
|
||||||
|
// ⚠️ 请在以下模型中添加反向关联:
|
||||||
|
{{#each manyToOne}}
|
||||||
|
// model {{targetModel}} {
|
||||||
|
// ...
|
||||||
|
// {{backRelation}} {{pascalCase ../name}}[]
|
||||||
|
// }
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { useCreate{{pascalCase name}} } from '@/hooks/use{{pascalCase pluralName
|
|||||||
|
|
||||||
const create{{pascalCase name}}Schema = z.object({
|
const create{{pascalCase name}}Schema = z.object({
|
||||||
{{#each createFields}}
|
{{#each createFields}}
|
||||||
{{name}}: {{zodValidation this}},
|
{{name}}: {{{zodValidation this}}},
|
||||||
{{/each}}
|
{{/each}}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function {{pascalCase name}}CreateDialog({
|
|||||||
resolver: zodResolver(create{{pascalCase name}}Schema),
|
resolver: zodResolver(create{{pascalCase name}}Schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
{{#each createFields}}
|
{{#each createFields}}
|
||||||
{{name}}: {{defaultValue this}},
|
{{name}}: {{{defaultValue this}}},
|
||||||
{{/each}}
|
{{/each}}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { useUpdate{{pascalCase name}} } from '@/hooks/use{{pascalCase pluralName
|
|||||||
|
|
||||||
const edit{{pascalCase name}}Schema = z.object({
|
const edit{{pascalCase name}}Schema = z.object({
|
||||||
{{#each updateFields}}
|
{{#each updateFields}}
|
||||||
{{name}}: {{zodValidation this}}.optional(),
|
{{name}}: {{{zodValidation this}}}.optional(),
|
||||||
{{/each}}
|
{{/each}}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export function {{pascalCase name}}EditDialog({
|
|||||||
resolver: zodResolver(edit{{pascalCase name}}Schema),
|
resolver: zodResolver(edit{{pascalCase name}}Schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
{{#each updateFields}}
|
{{#each updateFields}}
|
||||||
{{name}}: {{defaultValue this}},
|
{{name}}: {{{defaultValue this}}},
|
||||||
{{/each}}
|
{{/each}}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -77,7 +77,7 @@ export function {{pascalCase name}}EditDialog({
|
|||||||
if ({{camelCase name}}) {
|
if ({{camelCase name}}) {
|
||||||
form.reset({
|
form.reset({
|
||||||
{{#each updateFields}}
|
{{#each updateFields}}
|
||||||
{{name}}: {{camelCase ../name}}.{{name}}{{#if nullable}} ?? {{defaultValue this}}{{/if}},
|
{{name}}: {{camelCase ../name}}.{{name}}{{#if nullable}} ?? {{{defaultValue this}}}{{/if}},
|
||||||
{{/each}}
|
{{/each}}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
33
plop/templates/web/page.hbs
Normal file
33
plop/templates/web/page.hbs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { {{pascalCase pluralName}}Table } from '@/components/{{kebabCase pluralName}}/{{pascalCase pluralName}}Table';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
|
||||||
|
export default function {{pascalCase pluralName}}Page() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">{{chineseName}}管理</h1>
|
||||||
|
<p className="text-muted-foreground">管理系统中的所有{{chineseName}}信息。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{{chineseName}}列表</CardTitle>
|
||||||
|
<CardDescription>查看和管理所有{{chineseName}},支持新建、编辑和删除操作。</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<{{pascalCase pluralName}}Table />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,15 +16,14 @@ export interface Get{{pascalCase pluralName}}Params {
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BASE_URL = API_ENDPOINTS.{{constantCase pluralName}};
|
||||||
|
|
||||||
export const {{camelCase name}}Service = {
|
export const {{camelCase name}}Service = {
|
||||||
// 获取{{chineseName}}列表
|
// 获取{{chineseName}}列表
|
||||||
get{{pascalCase pluralName}}: (
|
get{{pascalCase pluralName}}: (
|
||||||
params: Get{{pascalCase pluralName}}Params = {},
|
params: Get{{pascalCase pluralName}}Params = {},
|
||||||
): Promise<PaginatedResponse<{{pascalCase name}}Response>> => {
|
): Promise<PaginatedResponse<{{pascalCase name}}Response>> => {
|
||||||
return http.get<PaginatedResponse<{{pascalCase name}}Response>>(
|
return http.get<PaginatedResponse<{{pascalCase name}}Response>>(BASE_URL, { params });
|
||||||
API_ENDPOINTS.{{constantCase pluralName}},
|
|
||||||
{ params },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{{#if softDelete}}
|
{{#if softDelete}}
|
||||||
@@ -33,7 +32,7 @@ export const {{camelCase name}}Service = {
|
|||||||
params: Get{{pascalCase pluralName}}Params = {},
|
params: Get{{pascalCase pluralName}}Params = {},
|
||||||
): Promise<PaginatedResponse<{{pascalCase name}}Response>> => {
|
): Promise<PaginatedResponse<{{pascalCase name}}Response>> => {
|
||||||
return http.get<PaginatedResponse<{{pascalCase name}}Response>>(
|
return http.get<PaginatedResponse<{{pascalCase name}}Response>>(
|
||||||
`${API_ENDPOINTS.{{constantCase pluralName}}}/deleted`,
|
`${BASE_URL}/deleted`,
|
||||||
{ params },
|
{ params },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -41,16 +40,14 @@ export const {{camelCase name}}Service = {
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
// 获取单个{{chineseName}}
|
// 获取单个{{chineseName}}
|
||||||
get{{pascalCase name}}: (id: string): Promise<{{pascalCase name}}Response> => {
|
get{{pascalCase name}}: (id: string): Promise<{{pascalCase name}}Response> => {
|
||||||
return http.get<{{pascalCase name}}Response>(
|
return http.get<{{pascalCase name}}Response>(`${BASE_URL}/${id}`);
|
||||||
`${API_ENDPOINTS.{{constantCase pluralName}}}/${id}`,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 创建{{chineseName}}
|
// 创建{{chineseName}}
|
||||||
create{{pascalCase name}}: (
|
create{{pascalCase name}}: (
|
||||||
data: Create{{pascalCase name}}Dto,
|
data: Create{{pascalCase name}}Dto,
|
||||||
): Promise<{{pascalCase name}}Response> => {
|
): Promise<{{pascalCase name}}Response> => {
|
||||||
return http.post<{{pascalCase name}}Response>(API_ENDPOINTS.{{constantCase pluralName}}, data);
|
return http.post<{{pascalCase name}}Response>(BASE_URL, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新{{chineseName}}
|
// 更新{{chineseName}}
|
||||||
@@ -58,23 +55,18 @@ export const {{camelCase name}}Service = {
|
|||||||
id: string,
|
id: string,
|
||||||
data: Update{{pascalCase name}}Dto,
|
data: Update{{pascalCase name}}Dto,
|
||||||
): Promise<{{pascalCase name}}Response> => {
|
): Promise<{{pascalCase name}}Response> => {
|
||||||
return http.patch<{{pascalCase name}}Response>(
|
return http.patch<{{pascalCase name}}Response>(`${BASE_URL}/${id}`, data);
|
||||||
`${API_ENDPOINTS.{{constantCase pluralName}}}/${id}`,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除{{chineseName}}
|
// 删除{{chineseName}}
|
||||||
delete{{pascalCase name}}: (id: string): Promise<void> => {
|
delete{{pascalCase name}}: (id: string): Promise<void> => {
|
||||||
return http.delete<void>(`${API_ENDPOINTS.{{constantCase pluralName}}}/${id}`);
|
return http.delete<void>(`${BASE_URL}/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
{{#if softDelete}}
|
{{#if softDelete}}
|
||||||
// 恢复{{chineseName}}
|
// 恢复{{chineseName}}
|
||||||
restore{{pascalCase name}}: (id: string): Promise<{{pascalCase name}}Response> => {
|
restore{{pascalCase name}}: (id: string): Promise<{{pascalCase name}}Response> => {
|
||||||
return http.patch<{{pascalCase name}}Response>(
|
return http.patch<{{pascalCase name}}Response>(`${BASE_URL}/${id}/restore`);
|
||||||
`${API_ENDPOINTS.{{constantCase pluralName}}}/${id}/restore`,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{{/if}}
|
{{/if}}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -315,9 +315,9 @@ export function {{pascalCase pluralName}}Table() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{chineseName}}列表
|
{{chineseName}}列表
|
||||||
{{{camelCase pluralName}}Data && (
|
{ {{camelCase pluralName}}Data && (
|
||||||
<Badge variant="secondary" className="ml-2">
|
<Badge variant="secondary" className="ml-2">
|
||||||
{{{camelCase pluralName}}Data.total}
|
{ {{camelCase pluralName}}Data.total}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -411,7 +411,7 @@ export function {{pascalCase pluralName}}Table() {
|
|||||||
|
|
||||||
{/* 编辑弹窗 */}
|
{/* 编辑弹窗 */}
|
||||||
<{{pascalCase name}}EditDialog
|
<{{pascalCase name}}EditDialog
|
||||||
{{camelCase name}}={{{camelCase name}}ToEdit}
|
{{camelCase name}}={ {{camelCase name}}ToEdit}
|
||||||
open={editDialogOpen}
|
open={editDialogOpen}
|
||||||
onOpenChange={setEditDialogOpen}
|
onOpenChange={setEditDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -379,3 +379,23 @@ export function getWhereCondition(field: FieldDefinition): string {
|
|||||||
}
|
}
|
||||||
return `query.${field.name}`;
|
return `query.${field.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取格式化后的 example 值(用于代码生成)
|
||||||
|
* 字符串类型加引号,数字/布尔值直接输出
|
||||||
|
*/
|
||||||
|
export function getFormattedExample(field: FieldDefinition): string {
|
||||||
|
switch (field.type) {
|
||||||
|
case 'string':
|
||||||
|
case 'enum':
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
return `'${field.example}'`;
|
||||||
|
case 'number':
|
||||||
|
return field.example;
|
||||||
|
case 'boolean':
|
||||||
|
return field.example.toLowerCase() === 'true' ? 'true' : 'false';
|
||||||
|
default:
|
||||||
|
return `'${field.example}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
* DSL 格式:
|
* DSL 格式:
|
||||||
* - 关联关系: 关联名:目标模型 字段1,字段2,... [noList]
|
* - 关联关系: 关联名:目标模型 字段1,字段2,... [noList]
|
||||||
* - 多对多: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,...
|
* - 多对多: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,...
|
||||||
|
* - 一对多: 关系名:目标模型 [optional]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关联关系配置
|
* 关联关系配置(用于查询时 include)
|
||||||
*/
|
*/
|
||||||
export interface RelationConfig {
|
export interface RelationConfig {
|
||||||
/** 关联名,如 'class' */
|
/** 关联名,如 'class' */
|
||||||
@@ -22,6 +23,48 @@ export interface RelationConfig {
|
|||||||
includeInList: boolean;
|
includeInList: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一对多关系配置(新模型包含多个目标模型)
|
||||||
|
* 例如:Dormitory 包含多个 Student
|
||||||
|
* - 新模型:students Student[]
|
||||||
|
* - 目标模型:dormitoryId String?, dormitory Dormitory?
|
||||||
|
*/
|
||||||
|
export interface OneToManyConfig {
|
||||||
|
/** 关系名(复数),如 'students' */
|
||||||
|
name: string;
|
||||||
|
/** 目标模型 PascalCase,如 'Student' */
|
||||||
|
targetModel: string;
|
||||||
|
/** 目标模型 camelCase,如 'student' */
|
||||||
|
target: string;
|
||||||
|
/** 外键字段名(在目标模型中),如 'dormitoryId' */
|
||||||
|
foreignKey: string;
|
||||||
|
/** 反向关联名(在目标模型中),如 'dormitory' */
|
||||||
|
backRelation: string;
|
||||||
|
/** 是否可选(外键是否可为 null),默认 true */
|
||||||
|
optional: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多对一关系配置(新模型属于一个目标模型)
|
||||||
|
* 例如:Grade(成绩) 属于 Student
|
||||||
|
* - 新模型:studentId String, student Student
|
||||||
|
* - 目标模型:grades Grade[]
|
||||||
|
*/
|
||||||
|
export interface ManyToOneConfig {
|
||||||
|
/** 外键字段名,如 'studentId' */
|
||||||
|
foreignKey: string;
|
||||||
|
/** 关联名,如 'student' */
|
||||||
|
name: string;
|
||||||
|
/** 目标模型 PascalCase,如 'Student' */
|
||||||
|
targetModel: string;
|
||||||
|
/** 目标模型 camelCase,如 'student' */
|
||||||
|
target: string;
|
||||||
|
/** 反向关联名(在目标模型中,复数),如 'grades' */
|
||||||
|
backRelation: string;
|
||||||
|
/** 是否可选,默认 false */
|
||||||
|
optional: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多对多关系<E585B3><E7B3BB><EFBFBD>置
|
* 多对多关系<E585B3><E7B3BB><EFBFBD>置
|
||||||
*/
|
*/
|
||||||
@@ -214,3 +257,84 @@ export function generateManyToManyTypeField(config: ManyToManyConfig): string {
|
|||||||
const fieldTypes = config.selectFields.map((f) => `${f}: ${getFieldType(f)}`);
|
const fieldTypes = config.selectFields.map((f) => `${f}: ${getFieldType(f)}`);
|
||||||
return `${config.name}?: Array<{ ${config.target}: { ${fieldTypes.join('; ')} } }>`;
|
return `${config.name}?: Array<{ ${config.target}: { ${fieldTypes.join('; ')} } }>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析一对多关系配置
|
||||||
|
* 格式: 关系名:目标模型 [optional]
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 输入: "students:Student optional"
|
||||||
|
* 输出: { name: 'students', targetModel: 'Student', target: 'student', foreignKey: 'dormitoryId', backRelation: 'dormitory', optional: true }
|
||||||
|
*/
|
||||||
|
export function parseOneToMany(dsl: string, currentModelName: string): OneToManyConfig[] {
|
||||||
|
const lines = dsl
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line && !line.startsWith('#'));
|
||||||
|
|
||||||
|
const configs: OneToManyConfig[] = [];
|
||||||
|
const currentModelCamel = currentModelName.charAt(0).toLowerCase() + currentModelName.slice(1);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 匹配格式: 关系名:目标模型 [optional]
|
||||||
|
const match = line.match(/^(\w+):(\w+)(?:\s+(optional))?$/);
|
||||||
|
if (!match) {
|
||||||
|
console.warn(`无法解析一对多配置行: ${line}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, name, targetModel, optionalFlag] = match;
|
||||||
|
const target = targetModel.charAt(0).toLowerCase() + targetModel.slice(1);
|
||||||
|
|
||||||
|
configs.push({
|
||||||
|
name,
|
||||||
|
targetModel,
|
||||||
|
target,
|
||||||
|
foreignKey: `${currentModelCamel}Id`,
|
||||||
|
backRelation: currentModelCamel,
|
||||||
|
optional: !!optionalFlag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析多对一关系配置
|
||||||
|
* 格式: 关联名:目标模型 [optional]
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* 输入: "student:Student"
|
||||||
|
* 输出: { foreignKey: 'studentId', name: 'student', targetModel: 'Student', target: 'student', backRelation: 'grades', optional: false }
|
||||||
|
*/
|
||||||
|
export function parseManyToOne(dsl: string, currentModelPluralName: string): ManyToOneConfig[] {
|
||||||
|
const lines = dsl
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line && !line.startsWith('#'));
|
||||||
|
|
||||||
|
const configs: ManyToOneConfig[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 匹配格式: 关联名:目标模型 [optional]
|
||||||
|
const match = line.match(/^(\w+):(\w+)(?:\s+(optional))?$/);
|
||||||
|
if (!match) {
|
||||||
|
console.warn(`无法解析多对一配置行: ${line}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, name, targetModel, optionalFlag] = match;
|
||||||
|
const target = targetModel.charAt(0).toLowerCase() + targetModel.slice(1);
|
||||||
|
|
||||||
|
configs.push({
|
||||||
|
foreignKey: `${target}Id`,
|
||||||
|
name,
|
||||||
|
targetModel,
|
||||||
|
target,
|
||||||
|
backRelation: currentModelPluralName,
|
||||||
|
optional: !!optionalFlag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@@ -11,9 +11,15 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.2
|
specifier: ^22.10.2
|
||||||
version: 22.19.3
|
version: 22.19.3
|
||||||
|
'@types/pluralize':
|
||||||
|
specifier: ^0.0.33
|
||||||
|
version: 0.0.33
|
||||||
plop:
|
plop:
|
||||||
specifier: ^4.0.4
|
specifier: ^4.0.4
|
||||||
version: 4.0.4(@types/node@22.19.3)
|
version: 4.0.4(@types/node@22.19.3)
|
||||||
|
pluralize:
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.4.2
|
specifier: ^3.4.2
|
||||||
version: 3.7.4
|
version: 3.7.4
|
||||||
@@ -2472,6 +2478,9 @@ packages:
|
|||||||
'@types/picomatch@4.0.2':
|
'@types/picomatch@4.0.2':
|
||||||
resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==}
|
resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==}
|
||||||
|
|
||||||
|
'@types/pluralize@0.0.33':
|
||||||
|
resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==}
|
||||||
|
|
||||||
'@types/qs@6.14.0':
|
'@types/qs@6.14.0':
|
||||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||||
|
|
||||||
@@ -8784,6 +8793,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/picomatch@4.0.2': {}
|
'@types/picomatch@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/pluralize@0.0.33': {}
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
|
|||||||
Reference in New Issue
Block a user