占位符 YourItem、YourDto 等请替换为真实类型。新增/编辑支持 Modal 与 Drawer 两种承载(与 system/dept、system/menu 一致):同一功能只选一种,列表与子组件 form.vue 必须使用同一套(useVbenModal↔Modal 或 useVbenDrawer↔Drawer)。web-ele 用 Element Plus,playground 用 Ant Design Vue,勿混用。
config.ts(查询 + 列 + 组装 gridOptions)import type { VbenFormSchema } from '#/adapter/form';
import type {
OnActionClickFn,
VxeTableGridColumns,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { YourItem } from '#/api/your-module';
import { yourPageListApi } from '#/api/your-module';
export const YOUR_TABLE_TITLE_KEY = 'page.yourModule.list';
export const listFormOptions = {
collapsed: false,
schema: [
{
component: 'Input',
componentProps: { clearable: true, placeholder: '关键词' },
fieldName: 'keyword',
label: '关键词',
},
] as VbenFormSchema[],
showCollapseButton: false,
submitOnChange: true,
wrapperClass: 'grid-cols-1 md:grid-cols-4',
};
export function buildColumns(
onActionClick: OnActionClickFn<YourItem>,
): VxeTableGridColumns<YourItem> {
return [
{ type: 'seq', title: '序号', width: 56, fixed: 'left' },
{ field: 'Id', title: 'Id', minWidth: 80 },
{
align: 'center',
cellRender: {
attrs: {
nameField: 'Name',
onClick: onActionClick,
},
name: 'CellOperation',
},
field: 'operation',
fixed: 'right',
title: '操作',
width: 160,
},
];
}
export function buildGridOptions(
onRowAction: OnActionClickFn<YourItem>,
): VxeTableGridOptions<YourItem> {
return {
columns: buildColumns(onRowAction),
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const res = await yourPageListApi({
page: page?.currentPage,
pageSize: page?.pageSize,
...formValues,
});
return res;
},
},
},
toolbarConfig: {
custom: true,
export: false,
refresh: true,
zoom: true,
},
};
}
说明:proxyConfig.ajax.query 的入参/返回值必须与项目现有 API 一致。
index.vue — Modal(useVbenModal)与 system/dept/list.vue 同类。
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { YourItem } from '#/api/your-module';
import { Page, useVbenModal } from '@vben/common-ui';
import { ElButton } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { buildGridOptions, listFormOptions, YOUR_TABLE_TITLE_KEY } from './config';
import Form from './modules/form.vue';
defineOptions({ name: 'YourFeatureIndex' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: listFormOptions,
gridOptions: buildGridOptions(onRowAction) as VxeTableGridOptions<YourItem>,
});
function refreshGrid() {
gridApi.query();
}
function onCreate() {
formModalApi.setData(null).open();
}
function onRowAction(e: OnActionClickParams<YourItem>) {
switch (e.code) {
case 'edit': {
formModalApi.setData(e.row).open();
break;
}
case 'delete': {
void onDelete(e.row);
break;
}
default: {
break;
}
}
}
async function onDelete(_row: YourItem) {
/* ElMessageBox.confirm + 删除 API + refreshGrid */
}
</script>
<template>
<Page auto-content-height>
<FormModal @success="refreshGrid" />
<Grid :table-title="$t(YOUR_TABLE_TITLE_KEY)">
<template #toolbar-tools>
<ElButton type="primary" @click="onCreate">+ 新增</ElButton>
</template>
</Grid>
</Page>
</template>
index.vue — Drawer(useVbenDrawer)与 system/menu/list.vue / system/role/list.vue 同类;仅将 useVbenModal → useVbenDrawer,FormModal → FormDrawer,formModalApi → formDrawerApi。
<script lang="ts" setup>
import type {
OnActionClickParams,
VxeTableGridOptions,
} from '#/adapter/vxe-table';
import type { YourItem } from '#/api/your-module';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { ElButton } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { $t } from '#/locales';
import { buildGridOptions, listFormOptions, YOUR_TABLE_TITLE_KEY } from './config';
import Form from './modules/form.vue';
defineOptions({ name: 'YourFeatureIndex' });
const [FormDrawer, formDrawerApi] = useVbenDrawer({
connectedComponent: Form,
destroyOnClose: true,
});
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: listFormOptions,
gridOptions: buildGridOptions(onRowAction) as VxeTableGridOptions<YourItem>,
});
function refreshGrid() {
gridApi.query();
}
function onCreate() {
formDrawerApi.setData(null).open();
}
function onRowAction(e: OnActionClickParams<YourItem>) {
switch (e.code) {
case 'edit': {
formDrawerApi.setData(e.row).open();
break;
}
case 'delete': {
void onDelete(e.row);
break;
}
default: {
break;
}
}
}
async function onDelete(_row: YourItem) {
/* 删除 + refreshGrid */
}
</script>
<template>
<Page auto-content-height>
<FormDrawer @success="refreshGrid" />
<Grid :table-title="$t(YOUR_TABLE_TITLE_KEY)">
<template #toolbar-tools>
<ElButton type="primary" @click="onCreate">+ 新增</ElButton>
</template>
</Grid>
</Page>
</template>
说明:playground 可将 ElButton 换成 Ant Design Vue 的 Button。
modules/form.vue — Modal(useVbenModal + <Modal>)与 system/dept/modules/form.vue 同类。
data.ts 中导出 schema:
import type { VbenFormSchema } from '#/adapter/form';
export function useYourFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'name',
label: '名称',
rules: 'required',
},
];
}
建议(类似 carGps/config.ts):把表单 schema/默认值外置到 config.ts / data.ts,modules/form.vue 只负责流程。
<script lang="ts" setup>
import type { YourModuleApi } from '#/api/your-module';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createYour, updateYour } from '#/api/your-module';
import { $t } from '#/locales';
import { useYourFormSchema } from '../data';
defineOptions({ name: 'YourFeatureForm' });
const emit = defineEmits<{
success: [];
}>();
const formData = ref<YourModuleApi.YourDto>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['名称'])
: $t('ui.actionTitle.create', ['名称']);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useYourFormSchema(),
showDefaultActions: false,
});
function resetForm() {
formApi.resetForm();
formApi.setValues(formData.value || {});
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
modalApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateYour(formData.value.id, data)
: createYour(data));
modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = modalApi.getData<YourModuleApi.YourDto>();
if (data) {
if (data.pid === 0) {
data.pid = undefined;
}
formData.value = data;
formApi.setValues(formData.value);
}
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
<template #prepend-footer>
<div class="flex-auto">
<Button type="primary" danger @click="resetForm">
{{ $t('common.reset') }}
</Button>
</div>
</template>
</Modal>
</template>
web-ele:将 Button 改为 ElButton(from 'element-plus');modalApi.unlock() 与 modalApi.lock(false) 等价,与邻近文件择一即可。
modules/form.vue — Drawer(useVbenDrawer + <Drawer>)与 system/menu/modules/form.vue 同类:子组件内 useVbenDrawer,drawerApi.getData / drawerApi.lock / drawerApi.unlock。
<script lang="ts" setup>
import type { YourModuleApi } from '#/api/your-module';
import { computed, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { createYour, updateYour } from '#/api/your-module';
import { $t } from '#/locales';
import { useYourFormSchema } from '../data';
defineOptions({ name: 'YourFeatureForm' });
const emit = defineEmits<{
success: [];
}>();
const formData = ref<YourModuleApi.YourDto>();
const getDrawerTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['名称'])
: $t('ui.actionTitle.create', ['名称']);
});
const [Form, formApi] = useVbenForm({
layout: 'vertical',
schema: useYourFormSchema(),
showDefaultActions: false,
});
const [Drawer, drawerApi] = useVbenDrawer({
async onConfirm() {
const { valid } = await formApi.validate();
if (valid) {
drawerApi.lock();
const data = await formApi.getValues();
try {
await (formData.value?.id
? updateYour(formData.value.id, data)
: createYour(data));
drawerApi.close();
emit('success');
} finally {
drawerApi.unlock();
}
}
},
onOpenChange(isOpen) {
if (isOpen) {
const data = drawerApi.getData<YourModuleApi.YourDto>();
if (data) {
formData.value = data;
formApi.setValues(formData.value);
} else {
formData.value = undefined;
formApi.resetForm();
}
}
},
});
</script>
<template>
<Drawer class="w-full max-w-200" :title="getDrawerTitle">
<Form class="mx-4" />
</Drawer>
</template>
说明:复杂表单可在 Drawer 上增加 class 控制宽度(如 max-w-200);onOpenChange 内数据清洗逻辑按 menu/modules/form.vue 等业务文件对齐。
config.ts。data.ts 中的 useYourFormSchema。modules/form.vue:§3a 或 §3b(须与列表 §2a 或 §2b 一致)。index.vue:§2a 或 §2b。