用于成堪数智平台快速生成标准 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 校验器
apps/micro-apps/{appName}/src/
├── api/{module}.ts # API接口
├── views/{module}/
│ ├── index.vue # 列表页
│ ├── config.ts # 表格+表单配置
│ └── drawer.vue # 抽屉(新增/编辑/详情)
├── utils/request.ts # 请求客户端
└── router/ # 路由
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 |
index.vue (列表页) → 引用 tableConfig + Drawer组件
config.ts (配置中心) → tableConfig(表格/搜索) + drawerConfig(表单) + setGlobParams() + getFormInfo()
drawer.vue (抽屉) → 引用 drawerConfig, 处理add/edit/copy/detail四模式
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')
TreeWrapper (ElRow 5:19)
├── tree.vue (BussinessTree) → 加载树,存session("menuNode"), emit('nodeClick')
└── index.vue右侧
├── EditTable → 按menuId过滤
├── Drawer → 新增时预设menusArr为当前树节点
└── DrawerAuth → 角色授权(可选)
关键变量: activeMenuId(当前树节点) | menuId(表格过滤参数) | menusArr(新增预设) | session("menuNode")(树数据缓存)
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);
}
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 };
}
<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>
左侧树节点点击 → 右侧表格按
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),{模块名称}替换为中文(如用户)。
<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>
<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>
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' } } } };
<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>
不需要可跳过,同时删除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>
<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>
<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>
<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'] } // 条件显示
{
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' },
}
{
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
{ 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/*
src/api/{module}.ts (分页+列表+ID查询+新增+修改+删除)src/views/{module}/