- 新增种子脚本模板 module-seed.hbs - 生成器新增「菜单/权限种子脚本」选项 - 添加菜单图标和排序的提示问题 - 生成的脚本包含 4 个 CRUD 权限和 1 个菜单项 - 支持独立运行或被主 seed.ts 导入调用 - 修复 ESM 模块导入路径(添加 .ts 扩展名) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
776 lines
27 KiB
TypeScript
776 lines
27 KiB
TypeScript
/**
|
||
* CRUD 生成器
|
||
* 交互式提问和文件生成逻辑
|
||
*
|
||
* 支持三种服务类型:
|
||
* - CrudService: 单表 CRUD
|
||
* - RelationCrudService: 带关联查询
|
||
* - ManyToManyCrudService: 多对多关系
|
||
*/
|
||
|
||
import type { NodePlopAPI, ActionType } from 'plop';
|
||
import pluralize from 'pluralize';
|
||
import { parseFields, type FieldDefinition } from '../utils/field-parser.ts';
|
||
import {
|
||
parseRelations,
|
||
parseManyToMany,
|
||
parseCountRelations,
|
||
parseOneToMany,
|
||
parseManyToOne,
|
||
type RelationConfig,
|
||
type ManyToManyConfig,
|
||
type OneToManyConfig,
|
||
type ManyToOneConfig,
|
||
} from '../utils/relation-parser.ts';
|
||
import {
|
||
parseSchema,
|
||
getAvailableModels,
|
||
getSelectableFields,
|
||
getModelByName,
|
||
inferManyToManyConfig,
|
||
type SchemaModel,
|
||
} from '../utils/schema-parser.ts';
|
||
|
||
// 服务类型
|
||
type ServiceType = 'CrudService' | 'RelationCrudService' | 'ManyToManyCrudService';
|
||
|
||
// 模板数据类型
|
||
interface TemplateData {
|
||
name: string;
|
||
chineseName: string;
|
||
pluralName: string;
|
||
generateTargets: string[];
|
||
softDelete: boolean;
|
||
fields: FieldDefinition[];
|
||
createFields: FieldDefinition[];
|
||
updateFields: FieldDefinition[];
|
||
responseFields: FieldDefinition[];
|
||
tableColumns: FieldDefinition[];
|
||
queryFields: FieldDefinition[];
|
||
selectFields: string[];
|
||
defaultPageSize: number;
|
||
maxPageSize: number;
|
||
defaultSortBy: string;
|
||
defaultSortOrder: string;
|
||
hasQueryDto: boolean;
|
||
hasTextarea: boolean;
|
||
hasSelect: boolean;
|
||
hasSwitch: boolean;
|
||
|
||
// 服务类型相关
|
||
serviceType: ServiceType;
|
||
relations: RelationConfig[];
|
||
manyToMany: ManyToManyConfig[];
|
||
oneToMany: OneToManyConfig[];
|
||
manyToOne: ManyToOneConfig[];
|
||
countRelations: string[];
|
||
|
||
// 派生标志
|
||
hasRelations: boolean;
|
||
hasManyToMany: boolean;
|
||
hasOneToMany: boolean;
|
||
hasManyToOne: boolean;
|
||
hasCountRelations: boolean;
|
||
needsResponseDto: boolean;
|
||
needsDetailDto: boolean;
|
||
|
||
// 种子脚本相关
|
||
menuIcon: string;
|
||
menuSort: number;
|
||
menuParentCode: string;
|
||
}
|
||
|
||
/**
|
||
* 注册 CRUD 生成器
|
||
*/
|
||
export function crudGenerator(plop: NodePlopAPI) {
|
||
plop.setGenerator('crud', {
|
||
description: '生成 CRUD 模块(后端 + 前端 + 共享类型 + Prisma)',
|
||
prompts: [
|
||
{
|
||
type: 'input',
|
||
name: 'name',
|
||
message: '模块名称(英文,如 product):',
|
||
validate: (input: string) => {
|
||
if (!/^[a-z][a-zA-Z0-9]*$/.test(input)) {
|
||
return '模块名必须以小写字母开头,只能包含字母和数字';
|
||
}
|
||
return true;
|
||
},
|
||
},
|
||
{
|
||
type: 'input',
|
||
name: 'chineseName',
|
||
message: '模块中文名(如 产品):',
|
||
validate: (input: string) => !!input || '请输入中文名',
|
||
},
|
||
{
|
||
type: 'checkbox',
|
||
name: 'generateTargets',
|
||
message: '选择要生成的模块:',
|
||
choices: [
|
||
{ name: '后端 (NestJS)', value: 'api', checked: true },
|
||
{ name: '前端 (Next.js)', value: 'web', checked: true },
|
||
{ name: '共享类型', value: 'shared', checked: true },
|
||
{ name: 'Prisma Model', value: 'prisma', checked: true },
|
||
{ name: '菜单/权限种子脚本', value: 'seed', checked: true },
|
||
],
|
||
},
|
||
// 服务类型选择
|
||
{
|
||
type: 'list',
|
||
name: 'serviceType',
|
||
message: '选择服务类型:',
|
||
choices: [
|
||
{ name: 'CrudService(单表 CRUD)', value: 'CrudService' },
|
||
{ name: 'RelationCrudService(带关联查询)', value: 'RelationCrudService' },
|
||
{ name: 'ManyToManyCrudService(多对多关系)', value: 'ManyToManyCrudService' },
|
||
],
|
||
default: 'CrudService',
|
||
},
|
||
{
|
||
type: 'confirm',
|
||
name: 'softDelete',
|
||
message: '是否启用软删除?',
|
||
default: true,
|
||
},
|
||
{
|
||
type: 'editor',
|
||
name: 'fieldsRaw',
|
||
message: '定义字段(使用 DSL 语法,见下方示例):',
|
||
default: `# 字段定义示例(每行一个字段)
|
||
# 格式: 字段名:类型[?可选][!唯一] 标签 "示例值" [验证规则]
|
||
#
|
||
# 类型: string, number, boolean, date, datetime, enum(v1,v2,...)
|
||
# 修饰符: ? 可选, ! 唯一
|
||
# 验证: min:n, max:n, email, url
|
||
# 标志: noCreate, noUpdate, noTable
|
||
#
|
||
# 示例:
|
||
# title:string 标题 "示例标题" min:2 max:100
|
||
# description:string? 描述 "描述内容" max:500
|
||
# price:number 价格 "99.99" min:0
|
||
# status:enum(draft,published,archived) 状态 "draft"
|
||
|
||
name:string 名称 "示例名称" min:2 max:100
|
||
`,
|
||
},
|
||
// 多对多关系配置(ManyToManyCrudService 时显示)
|
||
{
|
||
type: 'checkbox',
|
||
name: 'manyToManyModels',
|
||
message: '选择多对多关联的模型(可多选):',
|
||
when: (answers: { serviceType: ServiceType }) =>
|
||
answers.serviceType === 'ManyToManyCrudService',
|
||
choices: () => {
|
||
try {
|
||
const models = parseSchema();
|
||
return getAvailableModels(models).map((m) => ({
|
||
name: m,
|
||
value: m,
|
||
}));
|
||
} catch {
|
||
return [];
|
||
}
|
||
},
|
||
},
|
||
{
|
||
type: 'editor',
|
||
name: 'manyToManyRaw',
|
||
message: '配置多对多关系(已预填选中的模型,请确认中间表配置):',
|
||
when: (answers: { serviceType: ServiceType; manyToManyModels?: string[] }) =>
|
||
answers.serviceType === 'ManyToManyCrudService' &&
|
||
answers.manyToManyModels &&
|
||
answers.manyToManyModels.length > 0,
|
||
default: (answers: { name: string; manyToManyModels?: string[] }) => {
|
||
const models = parseSchema();
|
||
const currentModelName = answers.name.charAt(0).toUpperCase() + answers.name.slice(1);
|
||
const lines: string[] = [
|
||
'# 多对多关系配置(每行一个)',
|
||
'# 格式: 关系名:中间表:外键:目标键:目标模型 字段1,字段2,...',
|
||
'#',
|
||
];
|
||
|
||
for (const targetModelName of answers.manyToManyModels || []) {
|
||
const targetModel = getModelByName(models, targetModelName);
|
||
if (targetModel) {
|
||
const config = inferManyToManyConfig(models, currentModelName, targetModelName);
|
||
const fields = getSelectableFields(targetModel);
|
||
const defaultFields = fields.slice(0, 4).join(',');
|
||
const relationName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1) + 's';
|
||
|
||
if (config) {
|
||
lines.push(`${relationName}:${config.through}:${config.foreignKey}:${config.targetKey}:${targetModelName} ${defaultFields}`);
|
||
} else {
|
||
// 如果无法推断,给出模板
|
||
const through = `${answers.name}${targetModelName}`;
|
||
const foreignKey = `${answers.name}Id`;
|
||
const targetKey = `${targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1)}Id`;
|
||
lines.push(`${relationName}:${through}:${foreignKey}:${targetKey}:${targetModelName} ${defaultFields}`);
|
||
lines.push(`# ⚠ 请确认中间表配置是否正确`);
|
||
}
|
||
lines.push(`# 可用字段: ${fields.join(', ')}`);
|
||
}
|
||
}
|
||
|
||
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 时显示)
|
||
{
|
||
type: 'input',
|
||
name: 'countRelationsRaw',
|
||
message: '统计关系(输入将来会有的关系名,逗号分隔,如 students,orders):',
|
||
when: (answers: { serviceType: ServiceType }) =>
|
||
answers.serviceType === 'RelationCrudService' ||
|
||
answers.serviceType === 'ManyToManyCrudService',
|
||
default: '',
|
||
},
|
||
{
|
||
type: 'checkbox',
|
||
name: 'searchFieldNames',
|
||
message: '选择支持搜索的字段:',
|
||
choices: (answers: { fieldsRaw: string }) => {
|
||
try {
|
||
const fields = parseFields(answers.fieldsRaw);
|
||
return fields
|
||
.filter((f) => ['string', 'enum'].includes(f.type))
|
||
.map((f) => ({
|
||
name: `${f.name} (${f.label})`,
|
||
value: f.name,
|
||
}));
|
||
} catch {
|
||
return [];
|
||
}
|
||
},
|
||
},
|
||
{
|
||
type: 'number',
|
||
name: 'defaultPageSize',
|
||
message: '默认分页大小:',
|
||
default: 20,
|
||
},
|
||
{
|
||
type: 'number',
|
||
name: 'maxPageSize',
|
||
message: '最大分页大小:',
|
||
default: 100,
|
||
},
|
||
{
|
||
type: 'list',
|
||
name: 'defaultSortBy',
|
||
message: '默认排序字段:',
|
||
choices: (answers: { fieldsRaw: string }) => {
|
||
try {
|
||
const fields = parseFields(answers.fieldsRaw);
|
||
return [
|
||
{ name: 'createdAt (创建时间)', value: 'createdAt' },
|
||
{ name: 'updatedAt (更新时间)', value: 'updatedAt' },
|
||
...fields.map((f) => ({
|
||
name: `${f.name} (${f.label})`,
|
||
value: f.name,
|
||
})),
|
||
];
|
||
} catch {
|
||
return [{ name: 'createdAt (创建时间)', value: 'createdAt' }];
|
||
}
|
||
},
|
||
default: 'createdAt',
|
||
},
|
||
{
|
||
type: 'list',
|
||
name: 'defaultSortOrder',
|
||
message: '默认排序方向:',
|
||
choices: [
|
||
{ name: '降序 (desc)', value: 'desc' },
|
||
{ name: '升序 (asc)', value: 'asc' },
|
||
],
|
||
default: 'desc',
|
||
},
|
||
// 菜单图标(生成种子脚本时使用)
|
||
{
|
||
type: 'input',
|
||
name: 'menuIcon',
|
||
message: '菜单图标名称(Lucide 图标,如 Users、FileText、Settings):',
|
||
when: (answers: { generateTargets: string[] }) =>
|
||
answers.generateTargets.includes('seed'),
|
||
default: 'FileText',
|
||
},
|
||
// 菜单排序
|
||
{
|
||
type: 'number',
|
||
name: 'menuSort',
|
||
message: '菜单排序值(数字越小越靠前):',
|
||
when: (answers: { generateTargets: string[] }) =>
|
||
answers.generateTargets.includes('seed'),
|
||
default: 50,
|
||
},
|
||
],
|
||
actions: (data) => {
|
||
if (!data) return [];
|
||
|
||
// 解析字段
|
||
let fields: FieldDefinition[];
|
||
try {
|
||
fields = parseFields(data.fieldsRaw);
|
||
} catch (error) {
|
||
console.error('字段解析错误:', error);
|
||
return [];
|
||
}
|
||
|
||
// 解析关联配置
|
||
const serviceType = data.serviceType as ServiceType;
|
||
const pascalName = data.name.charAt(0).toUpperCase() + data.name.slice(1);
|
||
const relations =
|
||
serviceType !== 'CrudService' && data.relationsRaw
|
||
? parseRelations(data.relationsRaw)
|
||
: [];
|
||
const manyToMany =
|
||
serviceType === 'ManyToManyCrudService' && 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 =
|
||
serviceType !== 'CrudService' && data.countRelationsRaw
|
||
? parseCountRelations(data.countRelationsRaw)
|
||
: [];
|
||
|
||
// 派生标志
|
||
const hasRelations = relations.length > 0;
|
||
const hasManyToMany = manyToMany.length > 0;
|
||
const hasOneToMany = oneToMany.length > 0;
|
||
const hasManyToOne = manyToOne.length > 0;
|
||
const hasCountRelations = countRelations.length > 0;
|
||
const needsResponseDto = serviceType !== 'CrudService';
|
||
const needsDetailDto = serviceType === 'ManyToManyCrudService' && (hasManyToMany || hasCountRelations);
|
||
|
||
// 准备模板数据
|
||
const templateData: TemplateData = {
|
||
name: data.name,
|
||
chineseName: data.chineseName,
|
||
pluralName: pluralize(data.name),
|
||
generateTargets: data.generateTargets,
|
||
softDelete: data.softDelete,
|
||
fields,
|
||
createFields: fields.filter((f) => !f.flags.noCreate),
|
||
updateFields: fields.filter((f) => !f.flags.noUpdate),
|
||
responseFields: fields,
|
||
tableColumns: fields.filter((f) => !f.flags.noTable),
|
||
queryFields: fields.filter((f) =>
|
||
data.searchFieldNames?.includes(f.name),
|
||
),
|
||
selectFields: fields.map((f) => f.name),
|
||
defaultPageSize: data.defaultPageSize,
|
||
maxPageSize: data.maxPageSize,
|
||
defaultSortBy: data.defaultSortBy,
|
||
defaultSortOrder: data.defaultSortOrder,
|
||
hasQueryDto: data.searchFieldNames?.length > 0,
|
||
hasTextarea: fields.some((f) => {
|
||
if (f.type !== 'string') return false;
|
||
const maxLen = f.validations.find((v) => v.type === 'max')?.value;
|
||
return maxLen && Number(maxLen) > 100;
|
||
}),
|
||
hasSelect: fields.some((f) => f.type === 'enum'),
|
||
hasSwitch: fields.some((f) => f.type === 'boolean'),
|
||
|
||
// 服务类型相关
|
||
serviceType,
|
||
relations,
|
||
manyToMany,
|
||
oneToMany,
|
||
manyToOne,
|
||
countRelations,
|
||
hasRelations,
|
||
hasManyToMany,
|
||
hasOneToMany,
|
||
hasManyToOne,
|
||
hasCountRelations,
|
||
needsResponseDto,
|
||
needsDetailDto,
|
||
|
||
// 种子脚本相关
|
||
menuIcon: data.menuIcon || 'FileText',
|
||
menuSort: data.menuSort || 50,
|
||
menuParentCode: '', // 默认不设父目录
|
||
};
|
||
|
||
const actions: ActionType[] = [];
|
||
|
||
// ===== 后端文件 =====
|
||
if (data.generateTargets.includes('api')) {
|
||
// DTO
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/api/src/{{kebabCase name}}/dto/{{kebabCase name}}.dto.ts',
|
||
templateFile: 'templates/api/dto.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// Service
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.service.ts',
|
||
templateFile: 'templates/api/service.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// Controller
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.controller.ts',
|
||
templateFile: 'templates/api/controller.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// Module
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/api/src/{{kebabCase name}}/{{kebabCase name}}.module.ts',
|
||
templateFile: 'templates/api/module.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// 修改 app.module.ts - 添加导入
|
||
actions.push({
|
||
type: 'modify',
|
||
path: 'apps/api/src/app.module.ts',
|
||
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`,
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// 修改 app.module.ts - 添加到 imports 数组
|
||
actions.push({
|
||
type: 'modify',
|
||
path: 'apps/api/src/app.module.ts',
|
||
pattern: /(\s+)(StudentModule,?\s*\n)(\s*\],)/,
|
||
template: `$1$2$1{{pascalCase name}}Module,\n$3`,
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
}
|
||
|
||
// ===== 前端文件 =====
|
||
if (data.generateTargets.includes('web')) {
|
||
// Service
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/web/src/services/{{kebabCase name}}.service.ts',
|
||
templateFile: 'templates/web/service.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// Hooks
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/web/src/hooks/use{{pascalCase pluralName}}.ts',
|
||
templateFile: 'templates/web/hooks.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// 组件目录
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase pluralName}}Table.tsx',
|
||
templateFile: 'templates/web/table.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase name}}CreateDialog.tsx',
|
||
templateFile: 'templates/web/create-dialog.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/web/src/components/{{kebabCase pluralName}}/{{pascalCase name}}EditDialog.tsx',
|
||
templateFile: 'templates/web/edit-dialog.hbs',
|
||
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 端点
|
||
actions.push({
|
||
type: 'modify',
|
||
path: 'apps/web/src/config/constants.ts',
|
||
pattern: /(USERS:\s*['"]\/users['"],?)/,
|
||
template: `$1\n {{constantCase pluralName}}: '/{{kebabCase pluralName}}',`,
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
}
|
||
|
||
// ===== 共享类型 =====
|
||
if (data.generateTargets.includes('shared')) {
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'packages/shared/src/types/{{kebabCase name}}.ts',
|
||
templateFile: 'templates/shared/types.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// 修改 types/index.ts - 添加导出
|
||
actions.push({
|
||
type: 'append',
|
||
path: 'packages/shared/src/types/index.ts',
|
||
template: `\n// {{chineseName}}\nexport * from './{{kebabCase name}}';\n`,
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
}
|
||
|
||
// ===== Prisma Model =====
|
||
if (data.generateTargets.includes('prisma')) {
|
||
actions.push({
|
||
type: 'append',
|
||
path: 'apps/api/prisma/schema.prisma',
|
||
templateFile: 'templates/prisma/model.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
|
||
// 如果启用软删除,修改 prisma.service.ts
|
||
if (data.softDelete) {
|
||
actions.push({
|
||
type: 'modify',
|
||
path: 'apps/api/src/prisma/prisma.service.ts',
|
||
pattern: /(const SOFT_DELETE_MODELS:\s*Prisma\.ModelName\[\]\s*=\s*\[[\s\S]*?)(\];)/,
|
||
template: `$1, '{{pascalCase name}}'$2`,
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ===== 种子脚本 =====
|
||
if (data.generateTargets.includes('seed')) {
|
||
actions.push({
|
||
type: 'add',
|
||
path: 'apps/api/prisma/seeds/{{kebabCase name}}.seed.ts',
|
||
templateFile: 'templates/seed/module-seed.hbs',
|
||
data: templateData,
|
||
abortOnFail: false,
|
||
});
|
||
}
|
||
|
||
// 打印生成信息
|
||
actions.push(() => {
|
||
console.log('\n✨ 生成完成!\n');
|
||
console.log(`服务类型: ${serviceType}`);
|
||
if (hasRelations) {
|
||
console.log(`关联关系: ${relations.map((r) => r.name).join(', ')}`);
|
||
}
|
||
if (hasManyToMany) {
|
||
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) {
|
||
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后续步骤:');
|
||
let stepNum = 1;
|
||
if (hasOneToMany || hasManyToOne) {
|
||
console.log(`${stepNum}. 按照上述提示修改目标模型`);
|
||
stepNum++;
|
||
}
|
||
if (data.generateTargets.includes('prisma')) {
|
||
console.log(`${stepNum}. 运行 pnpm db:generate && pnpm db:push`);
|
||
stepNum++;
|
||
}
|
||
if (data.generateTargets.includes('seed')) {
|
||
console.log(`${stepNum}. 查看并执行种子脚本:`);
|
||
console.log(` cd apps/api && npx ts-node prisma/seeds/${data.name}.seed.ts`);
|
||
stepNum++;
|
||
}
|
||
console.log(`${stepNum}. 重启开发服务器 pnpm dev\n`);
|
||
return '完成';
|
||
});
|
||
|
||
return actions;
|
||
},
|
||
});
|
||
}
|