feat(plop): 添加菜单/权限种子脚本生成功能

- 新增种子脚本模板 module-seed.hbs
- 生成器新增「菜单/权限种子脚本」选项
- 添加菜单图标和排序的提示问题
- 生成的脚本包含 4 个 CRUD 权限和 1 个菜单项
- 支持独立运行或被主 seed.ts 导入调用
- 修复 ESM 模块导入路径(添加 .ts 扩展名)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
charilezhou
2026-01-19 19:42:11 +08:00
parent 331142d87a
commit ed901250bd
4 changed files with 156 additions and 11 deletions

View File

@@ -10,7 +10,7 @@
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.ts';
import {
parseRelations,
parseManyToMany,
@@ -21,7 +21,7 @@ import {
type ManyToManyConfig,
type OneToManyConfig,
type ManyToOneConfig,
} from '../utils/relation-parser';
} from '../utils/relation-parser.ts';
import {
parseSchema,
getAvailableModels,
@@ -29,7 +29,7 @@ import {
getModelByName,
inferManyToManyConfig,
type SchemaModel,
} from '../utils/schema-parser';
} from '../utils/schema-parser.ts';
// 服务类型
type ServiceType = 'CrudService' | 'RelationCrudService' | 'ManyToManyCrudService';
@@ -73,6 +73,11 @@ interface TemplateData {
hasCountRelations: boolean;
needsResponseDto: boolean;
needsDetailDto: boolean;
// 种子脚本相关
menuIcon: string;
menuSort: number;
menuParentCode: string;
}
/**
@@ -108,6 +113,7 @@ export function crudGenerator(plop: NodePlopAPI) {
{ name: '前端 (Next.js)', value: 'web', checked: true },
{ name: '共享类型', value: 'shared', checked: true },
{ name: 'Prisma Model', value: 'prisma', checked: true },
{ name: '菜单/权限种子脚本', value: 'seed', checked: true },
],
},
// 服务类型选择
@@ -412,6 +418,24 @@ name:string 名称 "示例名称" min:2 max:100
],
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 [];
@@ -499,6 +523,11 @@ name:string 名称 "示例名称" min:2 max:100
hasCountRelations,
needsResponseDto,
needsDetailDto,
// 种子脚本相关
menuIcon: data.menuIcon || 'FileText',
menuSort: data.menuSort || 50,
menuParentCode: '', // 默认不设父目录
};
const actions: ActionType[] = [];
@@ -670,6 +699,17 @@ name:string 名称 "示例名称" min:2 max:100
}
}
// ===== 种子脚本 =====
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');
@@ -711,13 +751,21 @@ name:string 名称 "示例名称" min:2 max:100
}
console.log('\n后续步骤:');
let stepNum = 1;
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(`${stepNum}. 按照上述提示修改目标模型`);
stepNum++;
}
console.log(`${hasOneToMany || hasManyToOne ? '3' : '2'}. 重启开发服务器 pnpm dev\n`);
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 '完成';
});

View File

@@ -17,7 +17,7 @@ import {
getCellRenderer,
getWhereCondition,
getFormattedExample,
} from '../utils/field-parser';
} from '../utils/field-parser.ts';
/**
* 命名转换工具函数

View File

@@ -1,7 +1,7 @@
import type { NodePlopAPI } from 'plop';
import { registerHelpers } from './helpers';
import { crudGenerator } from './generators/crud';
import { registerHelpers } from './helpers/index.ts';
import { crudGenerator } from './generators/crud.ts';
export default function (plop: NodePlopAPI) {
// 注册自定义 Handlebars helpers

View File

@@ -0,0 +1,97 @@
/**
* {{chineseName}}模块种子数据
*
* 此脚本可独立运行npx ts-node prisma/seeds/{{kebabCase name}}.seed.ts
* 或作为模块被主 seed.ts 导入调用
*/
import { PrismaClient } from '@prisma/client';
// {{chineseName}}管理权限
const {{camelCase name}}Permissions = [
{ code: '{{kebabCase name}}:create', name: '创建{{chineseName}}', resource: '{{kebabCase name}}', action: 'create' },
{ code: '{{kebabCase name}}:read', name: '查看{{chineseName}}', resource: '{{kebabCase name}}', action: 'read' },
{ code: '{{kebabCase name}}:update', name: '更新{{chineseName}}', resource: '{{kebabCase name}}', action: 'update' },
{ code: '{{kebabCase name}}:delete', name: '删除{{chineseName}}', resource: '{{kebabCase name}}', action: 'delete' },
];
// {{chineseName}}管理菜单
const {{camelCase name}}Menu = {
code: '{{kebabCase name}}-management',
name: '{{chineseName}}管理',
type: 'menu',
path: '/{{kebabCase pluralName}}',
icon: '{{menuIcon}}',
sort: {{menuSort}},
isStatic: false,
};
/**
* 创建{{chineseName}}模块的权限和菜单
* @param prisma PrismaClient 实例(可选,不传则创建新实例)
*/
export async function seed{{pascalCase name}}Module(prisma?: PrismaClient) {
const db = prisma || new PrismaClient();
const isOwnClient = !prisma;
try {
console.log('\n开始创建{{chineseName}}模块种子数据...');
// 1. 创建权限
console.log(' 创建权限...');
for (const permission of {{camelCase name}}Permissions) {
await db.permission.upsert({
where: { code: permission.code },
update: permission,
create: permission,
});
}
console.log(' ✓ 已创建 ' + {{camelCase name}}Permissions.length + ' 个权限');
// 2. 创建菜单
console.log(' 创建菜单...');
{{#if menuParentCode}}
// 获取父菜单
const parentMenu = await db.menu.findUnique({
where: { code: '{{menuParentCode}}' },
});
if (parentMenu) {
await db.menu.upsert({
where: { code: {{camelCase name}}Menu.code },
update: { ...{{camelCase name}}Menu, parentId: parentMenu.id },
create: { ...{{camelCase name}}Menu, parentId: parentMenu.id },
});
} else {
console.warn(' ⚠ 父菜单 {{menuParentCode}} 不存在,将创建为顶级菜单');
await db.menu.upsert({
where: { code: {{camelCase name}}Menu.code },
update: {{camelCase name}}Menu,
create: {{camelCase name}}Menu,
});
}
{{else}}
await db.menu.upsert({
where: { code: {{camelCase name}}Menu.code },
update: {{camelCase name}}Menu,
create: {{camelCase name}}Menu,
});
{{/if}}
console.log(' ✓ 已创建菜单: {{chineseName}}管理');
console.log('{{chineseName}}模块种子数据创建完成!\n');
} finally {
if (isOwnClient) {
await db.$disconnect();
}
}
}
// 直接运行时执行
if (require.main === module) {
seed{{pascalCase name}}Module()
.catch((e) => {
console.error('{{chineseName}}模块种子数据创建失败:', e);
process.exit(1);
});
}