vben3.md 38 KB

Vben3 CRUD 页面模板生成 Skill

用于成堪数智平台快速生成标准 CRUD 页面。 核心产出:index.vue + config.ts + drawer.vue + api/xxx.ts 四件套。


一、技术栈

层级 技术 层级 技术
框架 Vue 3 Composition API + <script setup> UI库 Element Plus
构建 Vite + .env.* 表格 VXE Table (EditTable封装)
表单 @vben/wflow (AddForm) 状态 Pinia (@vben/stores)
请求 @vben/request (createRequestClient) 校验 Zod 链式校验
样式 Tailwind CSS + SCSS 类型 TypeScript 严格模式
微前端 qiankun (vite-plugin-qiankun) 图标 @vben/icons (getIconFont)

核心依赖

@vben/adapter      → EditTable, Drawer, BussinessTree, BussinessUpload
@vben/common-ui    → Page, simpleTitle, useVbenForm
@vben/wflow        → AddForm (抽屉内表单+表格组合)
@vben/utils        → $EventBus, cloneDeep, regexp, session, findTreeNodePath, commWay
@vben/request-api  → getDictByCode
@vben/stores       → useGlobdata, useAccessStore, useAuthStore
@vben-core/form-ui → z 校验器

二、目录与API规范

2.1 微应用目录

apps/micro-apps/{appName}/src/
├── api/{module}.ts          # API接口
├── views/{module}/
│   ├── index.vue            # 列表页
│   ├── config.ts            # 表格+表单配置
│   └── drawer.vue           # 抽屉(新增/编辑/详情)
├── utils/request.ts         # 请求客户端
└── router/                  # 路由

2.2 API命名规则 (src/api/{module}.ts)

操作 函数名 方法/路径
分页查询 get{Module}Page GET /jhxt/{module}/page
列表查询 get{Module}List GET /jhxt/{module}/list
按ID查询 get{Module}ById(id) GET /jhxt/{module}/{id}
新增 post{Module}X(data) POST /jhxt/{module}
修改 put{Module}X(data) PUT /jhxt/{module}
删除 delete{Module}X(params) DELETE /jhxt/{module}/removeByIds (用deleteCommon)
导入/导出 importData/exportData POST /jhxt/{module}/importEvents/export
模板下载 downloadTemplate GET /jhxt/{module}/template

三、命名规则

类型 规则 示例
页面目录 小写单词连写 user/, ruleGroup/
文件 index.vue / config.ts / drawer.vue views/user/drawer.vue
模板引用 {name}Ref + useTemplateRef editTableRef, drawerRef
配置对象 camelCase + Config tableConfig, drawerConfig
权限标识 @ums:{module}:{action} @ums:user:add, @ums:role:edit

四、模板架构

4.1 组件关系

index.vue (列表页) → 引用 tableConfig + Drawer组件
config.ts (配置中心) → tableConfig(表格/搜索) + drawerConfig(表单) + setGlobParams() + getFormInfo()
drawer.vue (抽屉) → 引用 drawerConfig, 处理add/edit/copy/detail四模式

4.2 数据流

index.vue                      drawer.vue
  │                               │
  ├─ operationClick() ──────────→ openModal(data)
  │   params.code:                  ├─ 'add'    → 新增(空表单)
  │     'add' / 'edit' / 'copy'     ├─ 'edit'   → 编辑(填充)
  │     'detail' / 'delete'         ├─ 'copy'   → 复制新增
  │                                 └─ 'detail' → 详情(禁用表单)
  │     'delete' → 直接删除
  │                                     
  └─ getData() ←────────────── onConfirm() → post/put → close → emit('onConfirm')

4.3 TreeWrapper架构(左树右表)

TreeWrapper (ElRow 5:19)
├── tree.vue (BussinessTree) → 加载树,存session("menuNode"), emit('nodeClick')
└── index.vue右侧
    ├── EditTable → 按menuId过滤
    ├── Drawer → 新增时预设menusArr为当前树节点
    └── DrawerAuth → 角色授权(可选)

关键变量: activeMenuId(当前树节点) | menuId(表格过滤参数) | menusArr(新增预设) | session("menuNode")(树数据缓存)


五、模板代码

5.1 API模板 (src/api/{module}.ts)

import { requestClient } from '#/utils/request';
import { deleteCommon, downLoadData } from './common';

export async function get{Module}Page(params?: any) {
  return requestClient.get('/jhxt/{module}/page', { params });
}
export async function get{Module}List(params?: any) {
  return requestClient.get('/jhxt/{module}/list', { params });
}
export async function get{Module}ById(id: number) {
  return requestClient.get(`/jhxt/{module}/${id}`);
}
export async function post{Module}X(data: any) {
  return requestClient.post('/jhxt/{module}', data);
}
export async function put{Module}X(data: any) {
  return requestClient.put('/jhxt/{module}', data);
}
export async function delete{Module}X(params: any) {
  return deleteCommon(`/jhxt/{module}/removeByIds`, params);
}

5.2 config.ts 模板

import { cloneDeep, session } from '@vben/utils';
import { get{Module}Page } from '#/api/{module}';

let globParams: Record<string, any> = {};
export const setGlobParams = (params: any) => { globParams = { ...globParams, ...params }; };

export const tableConfig = {
  options: {
    tableTitle: '{模块名称}列表',
    gridOptions: {
      checkboxConfig: { reserve: true },
      columns: [
        // { field: 'name', minWidth: 200, title: '名称' },
        // { field: 'isEnable', width: 90, title: '状态', slots: { default: "isEnable" } },
      ],
      proxyConfig: {
        ajax: {
          query: async (params: any, formValues: any) => {
            const res: any = await get{Module}Page({ ...params.page, ...formValues, ...globParams });
            if (res.code !== 1) return { items: [], total: 0 };
            return { items: cloneDeep(res.result.records), total: +res.result.total || 0 };
          },
        },
      },
    },
    formOptions: {
      commonConfig: { labelWidth: 60 },
      showCollapseButton: false, collapsed: false,
      schema: [
        // { component: 'Input', fieldName: 'keyword', label: '关键字', componentProps: { clearable: true, placeholder: '请输入' } },
      ],
      wrapperClass: 'grid-cols-1 md:grid-cols-4',
      submitButtonOptions: { auth: '@ums:{module}:search' },
      resetButtonOptions: { auth: '@ums:{module}:reset' },
    },
  },
  formObj: {
    operateOptions: [
      { code: 'edit', text: '编辑', type: 'primary', directives: [{ name: 'auth', value: '@ums:{module}:edit' }] },
      { code: 'delete', text: '删除', type: 'danger', directives: [{ name: 'auth', value: '@ums:{module}:delete' }] },
    ],
  },
  isView: false, showOperate: true, showSeq: true,
  buttonAuth: { addBtn: '@ums:{module}:add' },
};

export const drawerConfig = {
  formConfig: {
    wrapperClass: 'grid-cols-1 md:grid-cols-3',
    layout: 'horizontal',
    schemaConfig: [{
      schema: [
        { formItemClass: 'form-item-col3 hidden', fieldName: 'id', label: '', dependencies: { show: false } },
        // { component: 'Input', componentProps: { placeholder: '请输入名称', clearable: true }, fieldName: 'name', label: '名称', rules: 'required' },
        // { formItemClass: 'form-item-col3', component: 'Input', componentProps: { rows: 4, type: 'textarea' }, fieldName: 'remark', label: '备注' },
      ],
    }],
  },
  tableConfig: { show: false },
  mapConfig: { show: false },
  layoutConfig: { column: 1 },
};

export function getFormInfo() {
  const [Form, formApi] = useVbenForm({
    layout: 'horizontal',
    commonConfig: { componentProps: { class: 'w-full' } },
    schema: [
      { formItemClass: 'form-item-col3', component: 'Input', componentProps: { disabled: true }, fieldName: 'createInfo', label: '创建信息' },
      { formItemClass: 'form-item-col3', component: 'Input', componentProps: { disabled: true }, fieldName: 'updateInfo', label: '编辑信息' },
    ],
    wrapperClass: 'grid-cols-1 md:grid-cols-3',
    showDefaultActions: false,
  });
  return { Form, formApi };
}

5.3 index.vue 模板(基础版)

<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { EditTable } from '@vben/adapter';
import { Page } from '@vben/common-ui';
import { getIconFont } from '@vben/icons';
import { ElButton, ElMessage, ElPopconfirm } from 'element-plus';
import { delete{Module}X } from '#/api/{module}';
import { tableConfig } from './config';
import Drawer from './drawer.vue';

const editTableRef = useTemplateRef('editTable');
const drawerRef = useTemplateRef('drawer');

const events = {
  operationClick(data: { gridApi: any; params: any }) {
    const { params } = data;
    if (params.code === 'delete') { events.deleteClick(params.row); return; }
    if (['add', 'copy', 'detail', 'edit'].includes(params.code)) {
      drawerRef.value?.openModal(data);
    }
  },
  deleteClick(row?: any) {
    const instance = editTableRef.value?.getInstance();
    if (!instance) return;
    const gridApi: any = instance.gridApi;
    const ids = row ? row.id : gridApi.grid.getCheckboxRecords().map((item: any) => item.id).join(',');
    if (!ids) { ElMessage.warning('请选择要删除的数据!'); return; }
    delete{Module}X({ id: ids }).then((res: any) => {
      if (res.code === 1) { ElMessage.success('删除成功'); events.getData(); }
      else { ElMessage.error(res.message || '删除失败'); }
    });
  },
  async getData() {
    const instance = editTableRef.value?.getInstance();
    if (!instance) return;
    const gridApi: any = instance.gridApi;
    const values = (await gridApi.formApi.getValues()) || {};
    editTableRef.value?.getData({ ...values });
  },
};
</script>

<template>
  <Page auto-content-height>
    <EditTable ref="editTable" v-bind="tableConfig" @operation-click="events.operationClick">
      <template #toolbar-tools-after>
        <ElPopconfirm v-auth="'@ums:{module}:delete'" title="是否确认删除选中记录?" @confirm="events.deleteClick()">
          <template #reference>
            <ElButton v-auth="'@ums:{module}:delete'" type="danger" :icon="getIconFont('icon-shanchu')" plain>删除</ElButton>
          </template>
        </ElPopconfirm>
      </template>
    </EditTable>
    <Drawer ref="drawer" @on-confirm="events.getData" />
  </Page>
</template>

5.4 TreeWrapper模板(左树右表)

左侧树节点点击 → 右侧表格按menuId过滤;新增时预设当前树节点。

生成步骤: | # | 文件 | 操作 | 必改处 | |---|------|------|--------| | ① | comp/treeWrapper.vue | 直接复制,零修改 | 0 | | ② | comp/tree.vue | 复制后替换树API | 1 | | ③ | config.ts | 复制后填充表格/搜索/表单 | 3 | | ④ | drawer.vue | 复制后替换CRUD API | 1 | | ⑤ | index.vue | 复制后替换CRUD API | 1 |

生成时先将{module}替换为模块名(如user),{模块名称}替换为中文(如用户)。

5.4.1 treeWrapper.vue(复制即用)

<script lang="ts" setup>
import { Page } from "@vben/common-ui";
import { ElCol, ElRow } from "element-plus";
import { useTemplateRef } from "vue";
import Tree from "./tree.vue";

const props = defineProps({ showDeptment: { type: Boolean, default: true }, showTree: { type: Boolean, default: true }, defaultData: { type: Array, default: () => [] } });
const emits = defineEmits(["nodeClick", "getTreeData"]);
const treeRef = useTemplateRef("tree");
const events = {
  handleNodeClick(node: any) { emits("nodeClick", node); },
  getTreeData(data: any) { emits("getTreeData", data); },
};
defineExpose({ refreshTree() { treeRef.value?.refreshTree(); } });
</script>
<template>
  <Page auto-content-height>
    <ElRow class="h-full" :gutter="10">
      <ElCol :span="5" class="h-full"><Tree ref="tree" :show-deptment="props.showDeptment" :default-data="props.defaultData" :show-tree="props.showTree" @node-click="events.handleNodeClick" @get-tree-data="events.getTreeData" /></ElCol>
      <ElCol :span="19" class="h-full"><slot name="right-content"></slot></ElCol>
    </ElRow>
  </Page>
</template>

5.4.2 tree.vue(改1处:树API)

<script lang="ts" setup>
import { ref, useTemplateRef, computed } from "vue";
import { BussinessTree } from "@vben/adapter";
import { session } from "@vben/utils";
import { get{Module}Tree } from "#/api/{module}";  // ⓐ 替换为你的树API

const props = defineProps({ showDeptment: { type: Boolean, default: true }, showTree: { type: Boolean, default: true }, defaultData: { type: Array, default: () => [] } });
const emits = defineEmits(["nodeClick", "getTreeData"]);
const treeRef = useTemplateRef("tree");

function buildTreeData(result: any[]) { return [{ id: "-1", name: "全部", value: "-1", children: result }]; }

const treeOptions = ref({
  searchOptions: { show: true, placeholder: "请输入关键字" },
  treeOptions: {
    options: { expandOnClickNode: false, data: [], props: { children: "children", label: "name", value: "id" } },
    api: {
      query() {
        return new Promise((resolve) => {
          get{Module}Tree({}).then((res) => {
            if (res.code == 1 && res.result) {
              const list = buildTreeData(res.result);
              session.setItem("menuNode", list);
              resolve(list); emits("getTreeData", list);
            }
          });
        });
      }, isUseApi: true,
    },
  },
});
const treeOptionsComputed = computed(() => treeOptions.value);
const events = { handleNodeClick(node: any) { emits("nodeClick", node); } };

defineExpose({
  refreshTree() {
    get{Module}Tree({}).then((res) => {
      if (res.code == 1 && res.result) {
        const list = buildTreeData(res.result);
        session.setItem("menuNode", list);
        treeOptions.value = { ...treeOptions.value, treeOptions: { ...treeOptions.value.treeOptions, options: { ...treeOptions.value.treeOptions.options, data: list } } };
        emits("getTreeData", list);
      }
    });
  },
});
</script>
<template>
  <BussinessTree ref="tree" v-bind="treeOptionsComputed" @node-click="events.handleNodeClick" />
</template>

5.4.3 config.ts(改3处:表格列/搜索/表单)

import { cloneDeep, session } from '@vben/utils';
import { get{Module}Page } from '#/api/{module}';  // ⓐ 共用API

let globParams: Record<string, any> = {};
export const setGlobParams = (params: any) => { globParams = { ...globParams, ...params }; };

export const tableConfig = {
  options: {
    tableTitle: '{模块名称}列表',
    gridOptions: {
      checkboxConfig: { reserve: true },
      columns: [  // ⓑ 替换表格列
        // { field: 'name', minWidth: 200, title: '名称' },
        // { field: 'isEnable', width: 90, title: '状态', slots: { default: "isEnable" } },
      ],
      proxyConfig: {
        ajax: {
          query: async (params: any, formValues: any) => {
            const res: any = await get{Module}Page({ ...params.page, ...formValues, ...globParams });
            if (res.code !== 1) return { items: [], total: 0 };
            return { items: cloneDeep(res.result.records), total: +res.result.total || 0 };
          },
        },
      },
    },
    formOptions: {
      commonConfig: { labelWidth: 60 }, showCollapseButton: false, collapsed: false,
      schema: [  // ⓒ 替换搜索字段
        // { component: 'Input', fieldName: 'keyword', label: '关键字', componentProps: { clearable: true, placeholder: '请输入' } },
      ],
      wrapperClass: 'grid-cols-1 md:grid-cols-4',
      submitButtonOptions: { auth: '@ums:{module}:search' }, resetButtonOptions: { auth: '@ums:{module}:reset' },
    },
  },
  formObj: {
    operateOptions: [
      { code: 'edit', text: '编辑', type: 'primary', directives: [{ name: 'auth', value: '@ums:{module}:edit' }] },
      { code: 'delete', text: '删除', type: 'danger', directives: [{ name: 'auth', value: '@ums:{module}:delete' }] },
      { code: 'menuAuth', text: '权限配置', type: 'primary', directives: [{ name: 'auth', value: '@ums:{module}:auth' }] },
    ],
  },
  isView: false, showOperate: true, showSeq: true, buttonAuth: { addBtn: '@ums:{module}:add' },
};

export const drawerConfig = {
  formConfig: {
    wrapperClass: 'grid-cols-1', layout: 'horizontal',
    schemaConfig: [{
      schema: [  // ⓓ 替换表单字段
        { formItemClass: 'form-item-col3 hidden', fieldName: 'id', label: '', dependencies: { show: false } },
        // { component: 'Input', componentProps: { placeholder: '请输入名称', clearable: true }, fieldName: 'name', label: '名称', rules: 'required' },
        // { component: 'ApiTreeSelect', componentProps: { labelField: 'name', valueField: 'id', childrenField: 'children', api: () => session.getItem("menuNode"), multiple: true }, fieldName: 'menus', label: '上级菜单', rules: 'required' },
      ],
    }],
  },
  tableConfig: { show: false }, mapConfig: { show: false }, layoutConfig: { column: 1 },
};

// 权限树配置(供drawerAuth使用)
const treeConfig = { searchOptions: { show: true, placeholder: '搜索' }, treeOptions: { options: { expandOnClickNode: false, data: [] as any, nodeKey: 'id', checkStrictly: false } } };
export const authTreeOptions = { title: '未授权角色', ...treeConfig, treeOptions: { ...treeConfig.treeOptions, options: { ...treeConfig.treeOptions.options, showCheckbox: true, props: { children: 'children', label: 'name', value: 'id' } } } };
export const authSelectTreeOptions = { title: '已授权角色', ...treeConfig, treeOptions: { ...treeConfig.treeOptions, options: { ...treeConfig.treeOptions.options, showCheckbox: true, props: { children: 'children', label: 'name', value: 'id' } } } };

5.4.4 drawer.vue(改1处:CRUD API)

<template>
  <Drawer ref="drawer" :title="title" class-name="w-[600px]" @on-confirm="events.onConfirm">
    <template #default><AddForm ref="addForm" v-bind="drawerConfig" :is-auto-map-height="true" /></template>
  </Drawer>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from "vue";
import { Drawer } from "@vben/adapter"; import { AddForm } from "@vben/wflow";
import { ElMessage } from "element-plus"; import { commWay } from "@vben/utils";
import { post{Module}X, put{Module}X, get{Module}ById } from "#/api/{module}";  // ⓓ 替换CRUD API
import { drawerConfig } from "./config";

const emits = defineEmits(["onConfirm"]);
const drawerRef = useTemplateRef("drawer"); const addFormRef = useTemplateRef("addForm");
const title = ref(""); const selectedRows: any = ref({});

const events = {
  operationClick(data: { gridApi: any; params: any }) {
    const { params } = data; selectedRows.value = params.row || {}; title.value = params.code === "add" ? "新增" : "编辑";
    drawerRef.value?.openModal(() => {
      if (params.row) {
        get{Module}ById(params.row.id).then((r: any) => {
          if (r.code == 1 && r.result?.menus) { addFormRef.value?.getFormInstance()?.formApi?.setValues({ menus: r.result.menus.map((i: any) => i.menuId) }); }
        });
        addFormRef.value?.getFormInstance()?.formApi?.setValues({ ...params.row });
      }
      if (params.code === "add") { addFormRef.value?.getFormInstance()?.formApi?.setValues({ menus: data.menusArr }); }
    });
  },
  async onConfirm() {
    const values = await addFormRef.value?.getValues(); if (!values) return;
    values.file = commWay.getFileRes(values); if (values.menus) { values.menus = values.menus.map((i: any) => ({ menuId: i })); }
    const res: any = await (selectedRows.value.id ? put{Module}X : post{Module}X)({ ...selectedRows.value, ...values });
    if (res.code === 1) { ElMessage.success("保存成功"); drawerRef.value?.closeModal(); emits("onConfirm"); }
    else { ElMessage.error(res.message || "保存失败"); }
  },
};
defineExpose({ openModal(data: any) { events.operationClick(data); }, closeModal() { drawerRef.value?.closeModal(); } });
</script>

5.4.5 drawerAuth.vue(可选,角色授权)

不需要可跳过,同时删除index.vue中的<DrawerAuth>和config.ts中的authTreeOptions/authSelectTreeOptions。依赖#/components/treeTransfer/fileOuth.vue

<template>
  <Drawer ref="drawer" :title="title" class-name="w-[800px]" @on-confirm="events.onConfirm">
    <template #default>
      <TreeTransfer ref="treeTransfer" :left-options="authTreeOptionsComputed" :right-options="authSelectTreeOptionsComputed" :all-menu-list="allRoleList" :current-node="{ id: '0' }" @update-data="events.onUpdateData" />
    </template>
  </Drawer>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, computed } from 'vue';
import { Drawer } from '@vben/adapter'; import { cloneDeep } from '@vben/utils';
import { ElMessage } from 'element-plus';
import { getRolePage } from '#/api/role';  // ⓔ 角色列表API
import { get{Module}ById, auth{Module} } from "#/api/{module}";  // ⓕ 详情+授权API
import TreeTransfer from '#/components/treeTransfer/fileOuth.vue';
import { authSelectTreeOptions as _authSelectTreeOptions, authTreeOptions as _authTreeOptions } from './config';

const emits = defineEmits(['onConfirm']); const drawerRef = useTemplateRef('drawer'); const title = ref('');
const selectedRows: any = ref({}); const allRoleList = ref<any[]>([]);
const authSelectTreeOptions = ref(_authSelectTreeOptions); const authTreeOptions = ref(_authTreeOptions);
const authTreeOptionsComputed = computed(() => authTreeOptions.value); const authSelectTreeOptionsComputed = computed(() => authSelectTreeOptions.value);

const events = {
  init() { getRolePage({ current: 1, size: 100000 }).then((res: any) => { if (res.code === 1) { allRoleList.value = cloneDeep(res.result.records || []).map((item: any) => ({ ...item, parentId: '0' })); } }); },
  onUpdateData(data: { left: any[]; right: any[] }) { authTreeOptions.value.treeOptions.options.data = data.left.length ? data.left[0].children : []; authSelectTreeOptions.value.treeOptions.options.data = data.right.length ? data.right[0].children : []; },
  openModal(data: any) { selectedRows.value = data.params.row || {}; title.value = `角色授权(${selectedRows.value.name})`; drawerRef.value?.openModal(() => { if (data.params.row) { get{Module}ById(data.params.row.id).then((r: any) => { if (r.code === 1 && r.result) { const roleIdList = r.result.roles?.map((item: any) => item.roleId) || []; authSelectTreeOptions.value.treeOptions.options.data = allRoleList.value.filter((i: any) => roleIdList.includes(i.id)); authTreeOptions.value.treeOptions.options.data = allRoleList.value.filter((i: any) => !roleIdList.includes(i.id)); } }); } }); },
  async onConfirm() { const ids = authSelectTreeOptions.value.treeOptions.options.data.map((item: any) => item.id); if (ids.length === 0) { ElMessage.error('请选择要绑定的角色'); return; } auth{Module}({ roleIds: ids, ids: [selectedRows.value.id] }).then((r: any) => { if (r.code === 1) { drawerRef.value?.closeModal(); ElMessage.success('角色绑定成功'); emits('onConfirm'); } }); },
};
events.init();
defineExpose({ openModal(data: any) { events.openModal(data); }, closeModal() { drawerRef.value?.closeModal(); } });
</script>

5.4.6 index.vue(改1处:CRUD API)

<template>
  <TreeWrapper ref="treeWrapper" :show-deptment="true" @node-click="events.handleNodeClick">
    <template #right-content>
      <EditTable ref="editTable" v-bind="tableConfig" @operation-click="events.operationClick">
        <template #toolbar-tools-after>
          <ElPopconfirm v-auth="'@ums:{module}:delete'" title="是否确认删除选中记录?" @confirm="events.deleteClick()">
            <template #reference><ElButton v-auth="'@ums:{module}:delete'" type="danger" :icon="getIconFont('icon-shanchu')" plain>删除</ElButton></template>
          </ElPopconfirm>
        </template>
        <template #isEnable="{ row }">
          <ElSwitch inline-prompt active-text="启用" inactive-text="禁用" v-model="row.isEnable" :active-value="true" :inactive-value="false" @change="events.changeStatus(row)" />
        </template>
      </EditTable>
      <Drawer ref="drawer" @on-confirm="events.getData" />
      <DrawerAuth ref="drawerAuth" :is-show-tab="false" @on-confirm="events.getData" />
    </template>
  </TreeWrapper>
</template>
<script lang="ts" setup>
import { useTemplateRef } from "vue";
import { EditTable } from "@vben/adapter"; import { getIconFont } from "@vben/icons";
import { ElButton, ElMessage, ElPopconfirm, ElSwitch } from "element-plus";
import { delete{Module}X, put{Module}X } from "#/api/{module}";  // ⓖ 替换CRUD API
import TreeWrapper from "./comp/treeWrapper.vue"; import { tableConfig } from "./config";
import Drawer from "./drawer.vue"; import DrawerAuth from "./drawerAuth.vue";

const editTableRef = useTemplateRef("editTable"); const drawerRef = useTemplateRef("drawer");
const drawerAuthRef = useTemplateRef("drawerAuth"); const treeWrapperRef = useTemplateRef("treeWrapper");
let activeMenuId = "";

const events = {
  operationClick(data: { gridApi: any; params: any }) {
    const { params } = data;
    if (params.code === "delete") { events.deleteClick(params.row); return; }
    if (["add", "edit"].includes(params.code)) { data.menusArr = activeMenuId ? [activeMenuId] : []; drawerRef.value?.openModal(data); }
    if (params.code === "menuAuth") { drawerAuthRef.value?.openModal(data); }
  },
  changeStatus(row: any) { put{Module}X({ ...row }).then((r: any) => { if (r.code === 1) { events.getData(); ElMessage.success("操作成功"); } }); },
  deleteClick(row?: any) {
    const instance = editTableRef.value?.getInstance(); if (!instance) return;
    const ids = row ? row.id : instance.gridApi.grid.getCheckboxRecords().map((item: any) => item.id).join(",");
    if (!ids) { ElMessage.warning("请选择要删除的数据!"); return; }
    delete{Module}X({ id: ids }).then((r: any) => { if (r.code === 1) { ElMessage.success("删除成功"); events.getData(); } });
  },
  async getData() {
    const instance = editTableRef.value?.getInstance(); if (!instance) return;
    const values = (await instance.gridApi.formApi.getValues()) || {};
    editTableRef.value?.getData({ ...values }); treeWrapperRef.value?.refreshTree();
  },
  async handleNodeClick(node: any) {
    activeMenuId = node.id; const instance = editTableRef.value?.getInstance(); if (!instance) return;
    const values = (await instance.gridApi.formApi.getValues()) || {};
    editTableRef.value?.getData({ ...values, menuId: node.id });
  },
};
</script>

新增时预设 menusArr 为当前树节点;编辑时通过 get{Module}ById 回填菜单权限;提交时用 commWay.getFileRes 处理文件。不需要文件上传或菜单关联可删除对应逻辑。

<template>
  <Drawer ref="drawer" :title="title" class-name="w-[600px]" @on-confirm="events.onConfirm">
    <template #default>
      <AddForm ref="addForm" v-bind="drawerConfig" :is-auto-map-height="true" />
    </template>
  </Drawer>
</template>

<script lang="ts" setup>
import { ref, useTemplateRef } from "vue";
import { Drawer } from "@vben/adapter";
import { AddForm } from "@vben/wflow";
import { ElMessage } from "element-plus";
import { commWay } from "@vben/utils";

// ⓓ 替换为你的 CRUD API
import { post{Module}X, put{Module}X, get{Module}ById } from "#/api/{module}";

import { drawerConfig } from "./config";

const emits = defineEmits(["onConfirm"]);
const drawerRef = useTemplateRef("drawer");
const addFormRef = useTemplateRef("addForm");
const title = ref("");
const selectedRows: any = ref({});

const events = {
  operationClick(data: { gridApi: any; params: any }) {
    const { params } = data;
    selectedRows.value = params.row || {};
    title.value = params.code === "add" ? "新增" : "编辑";
    drawerRef.value?.openModal(() => {
      const { row } = params;
      if (row) {
        // 编辑:回填详情(如菜单权限)
        get{Module}ById(row.id).then((r: any) => {
          if (r.code == 1 && r.result?.menus) {
            const instance = addFormRef.value?.getFormInstance();
            instance?.formApi?.setValues({ menus: r.result.menus.map((i: any) => i.menuId) });
          }
        });
        addFormRef.value?.getFormInstance()?.formApi?.setValues({ ...row });
      }
      if (params.code === "add") {
        // 新增:预设树节点为上级菜单
        addFormRef.value?.getFormInstance()?.formApi?.setValues({ menus: data.menusArr });
      }
    });
  },

  async onConfirm() {
    const values = await addFormRef.value?.getValues();
    if (!values) return;
    values.file = commWay.getFileRes(values);  // 处理上传文件
    if (values.menus) {
      values.menus = values.menus.map((i: any) => ({ menuId: i }));
    }
    const Api = selectedRows.value.id ? put{Module}X : post{Module}X;
    const res: any = await Api({ ...selectedRows.value, ...values });
    if (res.code === 1) {
      ElMessage.success("保存成功");
      drawerRef.value?.closeModal();
      emits("onConfirm");
    } else {
      ElMessage.error(res.message || "保存失败");
    }
  },
};

defineExpose({
  openModal(data: any) { events.operationClick(data); },
  closeModal() { drawerRef.value?.closeModal(); },
});
</script>

<style scoped lang="scss">
.add-form { height: 100%; }
:deep(.add-form-item4) {
  height: calc(100% - 362px);
  & > div { height: 100% !important; }
}
</style>

5.5 drawer.vue 基础版(仅表单)

<template>
  <Drawer ref="drawer" :title="title" class-name="w-[600px]" @on-confirm="events.onConfirm">
    <template #default><AddForm ref="addForm" v-bind="drawerConfig" :is-auto-map-height="true" /></template>
  </Drawer>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { Drawer } from '@vben/adapter'; import { AddForm } from '@vben/wflow';
import { ElMessage } from 'element-plus';
import { post{Module}X, put{Module}X } from '#/api/{module}';
import { drawerConfig } from './config';

const emits = defineEmits(['onConfirm']); const drawerRef = useTemplateRef('drawer'); const addFormRef = useTemplateRef('addForm');
const title = ref(''); const selectedRows: any = ref({}); const editType = ref<'add' | 'copy' | 'detail' | 'edit'>('add');

const events = {
  operationClick(data: { gridApi: any; params: any }) {
    const { params } = data; selectedRows.value = params.row || {};
    title.value = { add: '新增{模块名称}', edit: `编辑{模块名称} (ID:${selectedRows.value.id})`, copy: '复制新增', detail: `{模块名称}详情 (ID:${selectedRows.value.id})` }[params.code] || '';
    drawerRef.value?.openModal(); editType.value = params.code as 'add' | 'copy' | 'detail' | 'edit';
    if (['copy', 'detail', 'edit'].includes(params.code)) {
      setTimeout(() => {
        const instance = addFormRef.value?.getFormInstance();
        instance?.formApi?.setState({ commonConfig: { disabled: editType.value === 'detail' } });
        instance?.formApi?.setValues({ ...selectedRows.value, id: params.code === 'copy' ? undefined : selectedRows.value.id });
      }, 600);
    }
  },
  async onConfirm() {
    if (editType.value === 'detail') { drawerRef.value?.closeModal(); return; }
    const values = await addFormRef.value?.getValues(); if (!values) return;
    const res: any = await (selectedRows.value.id ? put{Module}X : post{Module}X)(values);
    if (res.code === 1) { ElMessage.success('保存成功'); drawerRef.value?.closeModal(); emits('onConfirm'); }
    else { ElMessage.error(res.message || '保存失败'); }
  },
};
defineExpose({ openModal(data: any) { events.operationClick(data); }, closeModal() { drawerRef.value?.closeModal(); } });
</script>

5.6 drawer.vue 内嵌表格版

<template>
  <Drawer ref="drawer" :title="title" @on-confirm="events.onConfirm">
    <template #default>
      <AddForm ref="addForm" v-bind="drawerConfig" :is-auto-map-height="true" @operation-click="events.tableOperationClick">
        <template #table-title><simpleTitle title="{子表标题}" /></template>
        <template v-if="editType !== 'detail'" #toolbar-tools><ElButton type="primary" link @click="events.addRow">添加行</ElButton></template>
        <template v-if="editType === 'detail'" #footer-content><simpleTitle title="操作信息" style="margin-bottom: 16px" /><FormComponent /></template>
      </AddForm>
    </template>
  </Drawer>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { Drawer } from '@vben/adapter'; import { simpleTitle } from '@vben/common-ui'; import { AddForm } from '@vben/wflow';
import { ElButton, ElMessage } from 'element-plus';
import { get{Module}ById, post{Module}X, put{Module}X } from '#/api/{module}';
import { drawerConfig, getFormInfo } from './config';

const emits = defineEmits(['onConfirm']); const drawerRef = useTemplateRef('drawer'); const addFormRef = useTemplateRef('addForm');
const title = ref(''); const selectedRows: any = ref({}); const editType = ref<'add' | 'copy' | 'detail' | 'edit'>('add');
const { Form: FormComponent, formApi: formComponentApi } = getFormInfo();

const events = {
  operationClick(data: { gridApi: any; params: any }) {
    const { params } = data; selectedRows.value = params.row || {};
    title.value = { add: '新增{模块名称}', edit: `编辑{模块名称} (ID:${selectedRows.value.id})`, copy: '复制新增', detail: `{模块名称}详情 (ID:${selectedRows.value.id})` }[params.code] || '';
    drawerRef.value?.openModal(); editType.value = params.code as 'add' | 'copy' | 'detail' | 'edit';
    drawerConfig.tableConfig.showOperation = params.code !== 'detail';
    if (['copy', 'detail', 'edit'].includes(params.code)) {
      drawerRef.value?.DrawerApi.setState({ loading: true });
      get{Module}ById(params.row.id).then((res: any) => {
        setTimeout(() => {
          selectedRows.value = { ...params.row, ...res.result }; const instance = addFormRef.value?.getFormInstance();
          instance?.formApi?.setState({ commonConfig: { disabled: editType.value === 'detail' } });
          instance?.formApi?.setValues({ ...selectedRows.value, id: params.code === 'copy' ? undefined : selectedRows.value.id });
          if (params.code === 'detail') { formComponentApi.setValues({ createInfo: `${res.result.createUserName || ''} ${res.result.createTime || ''}`, updateInfo: `${res.result.updateUserName || ''} ${res.result.updateTime || ''}` }); }
          drawerRef.value?.DrawerApi.setState({ loading: false });
        }, 600);
      });
    }
  },
  tableOperationClick(data: { gridApi: any; params: any }) { if (data.params.code === 'deleteRow') { const tableInstance = addFormRef.value?.getTableInstance(); tableInstance?.gridApi?.grid?.remove(data.params.row); } },
  async onConfirm() { if (editType.value === 'detail') { drawerRef.value?.closeModal(); return; } const values = await addFormRef.value?.getValues(); if (!values) return; const res: any = await (selectedRows.value.id ? put{Module}X : post{Module}X)(values); if (res.code === 1) { ElMessage.success('保存成功'); drawerRef.value?.closeModal(); emits('onConfirm'); } },
  addRow() { const tableInstance = addFormRef.value?.getTableInstance(); const grid = tableInstance?.gridApi?.grid; grid?.insertAt({}, -1); const { fullData } = events.getTableData(); grid?.setEditRow(fullData[fullData.length - 1]); },
  getTableData() { const tableInstance = addFormRef.value?.getTableInstance(); const grid = tableInstance?.gridApi?.grid; return { fullData: grid?.getTableData().fullData, grid }; },
};
defineExpose({ openModal(data: any) { events.operationClick(data); }, closeModal() { drawerRef.value?.closeModal(); } });
</script>

六、表单组件速查

component 用途 关键componentProps
Input 文本输入 placeholder, clearable, type: 'textarea', rows, maxLength
Select / ApiSelect 下拉/远程下拉 options/api, labelField, valueField, multiple
ApiTreeSelect 远程树选择 api, labelField, valueField, childrenField, multiple
RadioGroup / ApiRadioGroup 单选组 options: [{ label, value }]/api
DatePicker 日期选择 format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss'
Switch 开关 activeValue, inactiveValue

校验规则

rules: 'required'  // 必填
rules: 'selectRequired'  // 必选
rules: z.string().email({ message: '请输入正确的电子邮箱' }).nullable().optional()  // 邮箱
rules: z.string().refine((value) => regexp.phone.test(String(value)), { message: '请输入正确的联系电话' }).nullable().optional()  // 正则
dependencies: { if(values: any) { return values.type === 'temporary' }, triggerFields: ['type'] }  // 条件显示

七、配置对象核心字段

7.1 tableConfig

{
  options: { tableTitle: '列表标题', gridOptions: { checkboxConfig: { reserve: true }, columns: [], proxyConfig: { ajax: { query: async (params, formValues) => ({ items, total }) } } }, formOptions: { schema: [], wrapperClass: 'grid-cols-1 md:grid-cols-4' } },
  formObj: { operateOptions: [{ code, text, type, directives: [{ name: 'auth', value }] }] },
  isView: false, showOperate: true, showSeq: true, buttonAuth: { addBtn: '@ums:{module}:add' },
}

7.2 drawerConfig

{
  formConfig: { wrapperClass: 'grid-cols-1 md:grid-cols-3', layout: 'horizontal', schemaConfig: [{ schema: [] }] },
  tableConfig: { show: false },  // show: true时配columns/editConfig
  mapConfig: { show: false }, layoutConfig: { column: 1 },
}

八、常用样式类

类名 用途
form-item-col3 表单项占满整行(3列布局)
form-item-col3 hidden 隐藏表单字段(如id)
grid-cols-1 md:grid-cols-3 表单3列响应式
grid-cols-1 md:grid-cols-4 搜索表单4列响应式
w-[600px] 抽屉宽度(class-name)

九、权限标识

格式: @ums:{module}:{action}
常见action: add/edit/delete/search/reset/detail/import/export/auth
使用: v-auth指令 / buttonAuth / directives / submitButtonOptions

十、API响应

{ code: 1, result: { records: [...], total: 100 } }  // 分页
{ items: [...], total: 100 }  // 前端代理返回

十一、导入速查

// index.vue: EditTable, Page, getIconFont, ElButton/ElMessage/ElPopconfirm/ElSwitch
// drawer.vue: Drawer, AddForm, simpleTitle, ElButton/ElMessage
// config.ts: useVbenForm, cloneDeep, session, z (from @vben-core/form-ui)
// 路径别名: #/* → ./src/*

十二、生成步骤

  1. API文件: src/api/{module}.ts (分页+列表+ID查询+新增+修改+删除)
  2. 页面目录: src/views/{module}/
  3. config.ts: tableConfig(表格/搜索) + drawerConfig(表单)
  4. drawer.vue: 根据是否有子表选模板
  5. index.vue: 根据是否有左树选模板
  6. 路由: 添加路由配置
  7. 验证: 启动检查