1. 安装与目录结构

1.1 安装

Bash

npm install --save-dev plop

1.2 推荐目录结构

将所有生成器相关文件收纳在 plop-templates 文件夹中:

my-project/
├── plop-templates/
│   ├── api/
│   │   └── api.hbs           # API 接口模板
│   ├── view/
│   │   ├── index.hbs         # 列表主页模板
│   │   └── drawer.hbs        # 弹窗组件模板
│   └── plopfile.js           # 核心配置文件 (放在这里)
├── jsconfig.json             # (或 tsconfig.json) 用于路径别名支持
├── .prettierignore           # 用于防止模板格式化错乱
└── package.json

2. 核心配置 (plopfile.js)

由于项目开启了 "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;
    }
  });
};

3. 模板文件 (.hbs)

3.1 API 模板 (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);
};

3.2 列表页模板 (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>

3.3 弹窗模板 (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>

4. 环境与运行配置

4.1 防止格式化错乱

.hbs 文件中的 Handlebars 语法容易被 Prettier 误伤(例如把 {{name}} 格式化为 { { name } } 导致解析失败)。

在根目录创建 .prettierignore

Plaintext

# 忽略模板文件夹
plop-templates/

4.2 配置运行脚本

修改 package.json,指定 plopfile 的位置:

JSON

"scripts": {
  // 指定配置文件路径
  "new": "plop --plopfile ./plop-templates/plopfile.js"
}

4.3 兼容 TS Hooks (宽容模式)

为了让 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"]
}

5. 使用方法

  1. 运行命令

    Bash

    npm run new
    
  2. 按提示输入

    • 目录名:system
    • 模块名:User
    • 中文名:用户
  3. 结果

    自动生成 src/api/user.js, src/views/system/user/index.vue 等文件。


End of Guide

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com