洛雪音乐
72.63M · 2026-02-05
Bash
npm install --save-dev plop
将所有生成器相关文件收纳在 plop-templates 文件夹中:
my-project/
├── plop-templates/
│ ├── api/
│ │ └── api.hbs # API 接口模板
│ ├── view/
│ │ ├── index.hbs # 列表主页模板
│ │ └── drawer.hbs # 弹窗组件模板
│ └── plopfile.js # 核心配置文件 (放在这里)
├── jsconfig.json # (或 tsconfig.json) 用于路径别名支持
├── .prettierignore # 用于防止模板格式化错乱
└── package.json
由于项目开启了 "type": "module",配置文件使用 ESM 语法 (export default)。
文件位置:plop-templates/plopfile.js
JavaScript
export default function (plop) {
// 设置生成器
plop.setGenerator('crud', {
description: '自动生成列表+弹窗+API (JS版)',
// 1. 交互提示
prompts: [
{
type: 'input',
name: 'folder',
message: '请输入目录名称 (例如 system):',
validate: (v) => !v ? '目录不能为空' : true
},
{
type: 'input',
name: 'name',
message: '请输入模块名称 (英文, 大驼峰, 如 User):',
validate: (v) => !v ? '模块名不能为空' : true
},
{
type: 'input',
name: 'cnName',
message: '请输入模块中文名称 (如 用户):',
default: '数据'
}
],
// 2. 执行动作
actions: (data) => {
// 注意:如果 plopfile 在子目录下,path 需要根据你的实际情况调整
// 通常 plop 执行时的根目录是项目根目录,所以路径一般仍从 src 开始写
const actions = [
// 生成 API 文件
{
type: 'add',
path: 'src/api/{{dashCase name}}.js',
templateFile: 'plop-templates/api/api.hbs',
},
// 生成列表页
{
type: 'add',
path: 'src/views/{{dashCase folder}}/{{dashCase name}}/index.vue',
templateFile: 'plop-templates/view/index.hbs',
},
// 生成弹窗组件
{
type: 'add',
path: 'src/views/{{dashCase folder}}/{{dashCase name}}/components/{{properCase name}}Drawer.vue',
templateFile: 'plop-templates/view/drawer.hbs',
}
];
return actions;
}
});
};
api.hbs)JavaScript
import http from "@/utils/http";
// 模块基础路径
const PORT = "/{{ dashCase name }}";
/**
* 获取{{ cnName }}列表
* @param {Object} params - { pageNum, pageSize, keyword ... }
*/
export const get{{ properCase name }}List = (params) => {
return http.get(`${PORT}/list`, { params });
};
/**
* 新增{{ cnName }}
*/
export const add{{ properCase name }} = (params) => {
return http.post(`${PORT}/add`, params);
};
/**
* 编辑{{ cnName }}
*/
export const edit{{ properCase name }} = (params) => {
return http.put(`${PORT}/edit`, params);
};
/**
* 删除{{ cnName }}
*/
export const delete{{ properCase name }} = (params) => {
return http.post(`${PORT}/delete`, params);
};
index.hbs)HTML
<template>
<div class="table-box">
<el-card>
<div class="table-header">
<el-form :inline="true" :model="searchParams">
<el-form-item label="名称">
<el-input v-model="searchParams.keyword" placeholder="搜索{{ cnName }}" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getTableData">搜索</el-button>
<el-button icon="Refresh" @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="header-button">
<el-button type="primary" icon="Plus" @click="openDrawer('新增')">新增{{ cnName }}</el-button>
</div>
</div>
<el-table :data="tableData" border stripe style="width: 100%">
<el-table-column type="index" label="#" width="80" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button type="primary" link icon="Edit" @click="openDrawer('编辑', scope.row)">编辑</el-button>
<el-button type="danger" link icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pageParams.pageNum"
v-model:page-size="pageParams.pageSize"
:total="total"
@current-change="getTableData"
@size-change="getTableData"
layout="total, sizes, prev, pager, next, jumper"
/>
</el-card>
<{{ properCase name }}Drawer ref="drawerRef" />
</div>
</template>
<script setup name="{{ properCase name }}">
import { ref, reactive } from "vue";
import { get{{ properCase name }}List, delete{{ properCase name }}, add{{ properCase name }}, edit{{ properCase name }} } from "@/api/{{ dashCase name }}";
import {{ properCase name }}Drawer from "./components/{{ properCase name }}Drawer.vue";
import { ElMessage, ElMessageBox } from "element-plus";
const drawerRef = ref();
const tableData = ref([]);
const total = ref(0);
const searchParams = reactive({ keyword: "" });
const pageParams = reactive({ pageNum: 1, pageSize: 10 });
const getTableData = async () => {
const params = { ...pageParams, ...searchParams };
try {
const { data } = await get{{ properCase name }}List(params);
tableData.value = data.list || [];
total.value = data.total || 0;
} catch (error) {
console.error(error);
}
};
const resetSearch = () => {
searchParams.keyword = "";
getTableData();
};
const openDrawer = (title, row = {}) => {
const params = {
title,
rowData: { ...row },
api: title === "新增" ? add{{ properCase name }} : edit{{ properCase name }},
getTableList: getTableData
};
drawerRef.value.acceptParams(params);
};
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(`是否确认删除该{{ cnName }}?`, "提示", { type: "warning" });
await delete{{ properCase name }}({ id: row.id });
ElMessage.success("删除成功");
getTableData();
} catch (error) {
// Cancelled
}
};
getTableData();
</script>
<style scoped>
.table-header { display: flex; justify-content: space-between; margin-bottom: 20px; }
</style>
drawer.hbs)HTML
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
size="500px"
:title="`${drawerProps.title}{{ cnName }}`"
>
<el-form
ref="ruleFormRef"
label-width="100px"
label-suffix=" :"
:rules="rules"
:model="drawerProps.rowData"
>
<el-form-item label="{{ cnName }}名称" prop="name">
<el-input v-model="drawerProps.rowData.name" placeholder="请输入名称" clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-drawer>
</template>
<script setup name="{{ properCase name }}Drawer">
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
const rules = reactive({
name: [{ required: true, message: "请填写名称", trigger: "blur" }]
});
const drawerVisible = ref(false);
const drawerProps = ref({
title: "",
rowData: {},
api: null,
getTableList: null
});
const acceptParams = (params) => {
drawerProps.value = params;
drawerVisible.value = true;
};
const ruleFormRef = ref();
const handleSubmit = () => {
ruleFormRef.value.validate(async (valid) => {
if (!valid) return;
try {
await drawerProps.value.api(drawerProps.value.rowData);
ElMessage.success(`${drawerProps.value.title}{{ cnName }}成功!`);
drawerProps.value.getTableList && drawerProps.value.getTableList();
drawerVisible.value = false;
} catch (error) {
console.error(error);
}
});
};
defineExpose({
acceptParams
});
</script>
.hbs 文件中的 Handlebars 语法容易被 Prettier 误伤(例如把 {{name}} 格式化为 { { name } } 导致解析失败)。
在根目录创建 .prettierignore:
Plaintext
# 忽略模板文件夹
plop-templates/
修改 package.json,指定 plopfile 的位置:
JSON
"scripts": {
// 指定配置文件路径
"new": "plop --plopfile ./plop-templates/plopfile.js"
}
为了让 JS 项目能顺滑引用旧的 TS Hooks 且 VS Code 能识别路径别名 @,在根目录创建 tsconfig.json:
JSON
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"strict": false,
"jsx": "preserve",
"allowJs": true, // 允许 JS
"checkJs": false, // 不检查 JS 类型
"baseUrl": ".",
"paths": {
"@/*": ["src/*"] // 路径别名映射
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", "src/**/*.js"]
}
运行命令:
Bash
npm run new
按提示输入:
systemUser用户结果:
自动生成 src/api/user.js, src/views/system/user/index.vue 等文件。
End of Guide