vue3_templates.md 11 KB

Vben Admin 5.x — 标准模板(可直接复制再改)

占位符 YourItemYourDto 等请替换为真实类型。新增/编辑支持 ModalDrawer 两种承载(与 system/deptsystem/menu 一致):同一功能只选一种,列表与子组件 form.vue 必须使用同一套(useVbenModalModaluseVbenDrawerDrawer)。web-ele 用 Element Plus,playground 用 Ant Design Vue,勿混用


1. 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 一致。


2a. index.vueModaluseVbenModal

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>

2b. index.vueDraweruseVbenDrawer

system/menu/list.vue / system/role/list.vue 同类;仅将 useVbenModaluseVbenDrawerFormModalFormDrawerformModalApiformDrawerApi

<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


3a. modules/form.vueModaluseVbenModal + <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.tsmodules/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 改为 ElButtonfrom 'element-plus');modalApi.unlock()modalApi.lock(false) 等价,与邻近文件择一即可。


3b. modules/form.vueDraweruseVbenDrawer + <Drawer>

system/menu/modules/form.vue 同类:子组件内 useVbenDrawerdrawerApi.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 等业务文件对齐。


4. 模板使用顺序

  1. 选定 Modal 或 Drawer(§6 对照表 + 仓库内同类页)。
  2. 复制 config.ts
  3. 复制 data.ts 中的 useYourFormSchema
  4. 复制 modules/form.vue§3a§3b(须与列表 §2a§2b 一致)。
  5. 复制 index.vue§2a§2b