vben2.md 32 KB

Vben2 (Vue3) CRUD 页面生成技能

概述

本技能用于在 Vben Admin v2 (Vue3 + Vite + TypeScript) 项目中快速生成标准的 CRUD 管理页面。基于对项目的深度分析,提炼出了一套完整的开发规范和代码模板。

公共规范(格式、组件命名、API命名、API层组织、权限模式、CRUD架构、Lint流程)已在公司统一配置 §六 中定义,本文件不再重复。


一、项目技术栈

技术 版本/说明
Vue 3.x (Composition API + Options API 混用,项目使用 defineComponent + setup())
Vite 构建工具
TypeScript 主要开发语言
Ant Design Vue UI 组件库
Vben Admin v2 后台管理框架
Windi CSS 原子化 CSS 框架
Pinia / Vuex 状态管理
axios HTTP 请求
i18n 国际化 (vue-i18n)
ESLint + Prettier 代码规范

路径别名

/@/* → src/*
/#/* → types/*

二、目录结构规范

2.1 功能模块目录结构(标准三件套)

每个业务模块页面放在 src/views/{module}/{feature}/ 下,通常包含三个核心文件:

src/views/{module}/{feature}/
├── index.vue              # 主页面(列表页)
├── {Feature}Drawer.vue    # 侧边抽屉表单(新增/编辑)
└── {feature}.data.ts      # 数据配置(列定义、表单Schema、搜索Schema)

命名规则:

  • 目录名:kebab-case(如 alarm/contacts/
  • 主页面:固定为 index.vue
  • Drawer 组件:PascalCase + Drawer 后缀(如 ContactDrawer.vueDictDrawer.vue
  • 数据配置:kebab-case + .data.ts 后缀(如 config.data.tsdict.data.ts
  • Modal 组件(替代 Drawer 时):PascalCase + Modal 后缀(如 AccountModal.vue

2.2 API 层目录结构

公共原则见公司统一配置 §6.4(一功能一文件、禁止拆散、禁止混合、公共接口单独文件、接口只写一次)。以下仅列出 Vben2 专属结构。

src/api/{module}/
├── {feature}.ts           # 单个功能的全部 API 方法(增删改查等)
├── {feature2}.ts          # 另一个功能的全部 API 方法
└── model/
    └── {feature}Model.ts  # 对应功能的 TypeScript 类型/接口定义

Vben2 专属规则:

  • 不再使用 index.ts 统一导出,各页面直接按功能路径导入
  • API 文件与 Model 文件一一对应
  • API 文件:camelCase(如 alarmContact.tsdataBoard.ts
  • Model 文件:camelCase + Model 后缀(如 alarmContactModel.tsmenuModel.ts

导入示例:

// 直接按功能路径导入,不经过 index.ts 中转
import { getContactList, addContact, deleteContact } from '/@/api/alarm/alarmContact';

2.3 枚举目录

src/enums/
└── {feature}Enum.ts       # camelCase + Enum 后缀

三、Vben2 专属命名规范

通用命名规范见公司统一配置 §6.2(组件PascalCase、目录kebab-case、页面index.vue)和 §6.3(API方法命名)。以下仅列出 Vben2 专属差异。

3.1 文件命名(Vben2 专属类型)

类型 规则 示例
Drawer/Modal 组件 PascalCase + 后缀 ContactDrawer.vue, AccountModal.vue
数据配置 kebab-case + .data.ts config.data.ts, role.data.ts
Model 文件 camelCase + Model.ts menuModel.ts
枚举文件 camelCase + Enum.ts roleEnum.ts
Hook 文件 camelCase, use 前缀 useBatchDelete.ts
工具文件 camelCase menuUtil.ts

3.2 Vben2 变量/函数命名

类型 规则 示例
导出常量(列/表单配置) camelCase columns, searchFormSchema, formSchema
导出接口/类型 PascalCase ContactModal, MenuOperationParams
导出枚举 PascalCase RoleEnum, KeysTypeEnum
setup 内函数 camelCase handleCreate, handleEdit, handleDelete, handleSuccess
setup 内 ref camelCase isUpdate, treeData, roleMenus
事件处理函数 handle 前缀 handleSubmit, handleCreate, handleEdit
计算属性 get 前缀 getTitle, getCanBatchDelete

3.4 API 枚举命名

API 路径使用 PascalCase 枚举:

enum Api {
  AccountList = '/user/page',
  BaseUserUrl = '/user',
}

enum API {
  alarmContact = '/alarm_contact',
}

四、代码模板

4.1 {feature}.data.ts — 数据配置文件模板

import { BasicColumn, FormSchema } from '/@/components/Table';
// 按需导入:
// import { h } from 'vue';
// import { Tag } from 'ant-design-vue';
// import { Icon } from '/@/components/Icon';
// import { useI18n } from '/@/hooks/web/useI18n';
// import { emailRule, phoneRule } from '/@/utils/rules';

// const { t } = useI18n();

// ==================== 表格列配置 ====================
export const columns: BasicColumn[] = [
  {
    title: '字段名称',        // 或 t('i18n.key')
    dataIndex: 'fieldName',
    width: 120,
    // align: 'left',         // 默认居中,需要时指定
    // slots: { customRender: 'slotName' },  // 自定义渲染插槽
    // customRender: ({ record }) => {       // 行内自定义渲染
    //   return h(Tag, { color: 'green' }, () => '文本');
    // },
  },
  {
    title: '状态',
    dataIndex: 'status',
    width: 100,
    customRender: ({ record }) => {
      const enable = record.status === 1;
      const color = enable ? 'green' : 'red';
      const text = enable ? '启用' : '停用';
      return h(Tag, { color }, () => text);
    },
  },
  {
    title: '创建时间',
    dataIndex: 'createTime',
    width: 180,
  },
];

// ==================== 搜索表单配置 ====================
export const searchFormSchema: FormSchema[] = [
  {
    field: 'keyword',
    label: '关键字',
    component: 'Input',
    colProps: { span: 8 },          // 搜索表单常用 span: 6 或 8
    componentProps: {
      maxLength: 255,
      placeholder: '请输入关键字',
    },
  },
  {
    field: 'status',
    label: '状态',
    component: 'Select',
    componentProps: {
      options: [
        { label: '启用', value: 1 },
        { label: '停用', value: 0 },
      ],
    },
    colProps: { span: 8 },
  },
];

// ==================== 新增/编辑表单配置 ====================
export const formSchema: FormSchema[] = [
  {
    field: 'name',
    label: '名称',
    required: true,
    component: 'Input',
    componentProps: {
      maxLength: 255,
      placeholder: '请输入名称',
    },
  },
  {
    field: 'status',
    label: '状态',
    component: 'RadioButtonGroup',
    defaultValue: 1,
    componentProps: {
      options: [
        { label: '启用', value: 1 },
        { label: '停用', value: 0 },
      ],
    },
  },
  {
    field: 'remark',
    label: '备注',
    component: 'InputTextArea',
    componentProps: {
      maxLength: 255,
      placeholder: '请输入备注',
    },
    dynamicRules: () => {
      return [
        {
          required: false,
          validator: (_, value) => {
            if (String(value).length > 255) {
              return Promise.reject('字数不超过255个字');
            }
            return Promise.resolve();
          },
        },
      ];
    },
  },
  {
    field: 'id',
    label: '',
    component: 'Input',
    show: false,                    // 隐藏字段,用于传递id
  },
];

FormSchema 常用 component 类型:

  • Input / InputTextArea — 文本输入
  • InputNumber — 数字输入
  • Select — 下拉选择
  • RadioButtonGroup — 单选按钮组
  • Switch — 开关
  • DatePicker — 日期选择
  • TreeSelect / ApiTreeSelect — 树形选择
  • IconPicker — 图标选择
  • Checkbox — 复选框

FormSchema 常用属性:

  • ifShow: ({ values }) => boolean — 条件显示(根据表单其他值)
  • dynamicRules — 动态校验规则
  • rules — 静态校验规则(如 phoneRule, emailRule
  • slot — 自定义插槽
  • show: false — 隐藏字段(用于传递隐藏值如 id)
  • colProps: { lg: 12, md: 24 } — 栅格布局
  • defaultValue — 默认值

4.2 index.vue — 主列表页模板(标准 CRUD)

<template>
  <div>
    <BasicTable
      :clickToRowSelect="false"
      @register="registerTable"
    >
      <template #toolbar>
        <Authority value="api:yt:{module}:post">
          <a-button type="primary" @click="handleCreate"> 新增{业务名} </a-button>
        </Authority>
        <Authority value="api:yt:{module}:delete">
          <Popconfirm
            title="您确定要批量删除数据"
            ok-text="确定"
            cancel-text="取消"
            @confirm="handleDeleteOrBatchDelete(null)"
          >
            <a-button type="primary" color="error" :disabled="hasBatchDelete">
              批量删除
            </a-button>
          </Popconfirm>
        </Authority>
      </template>
      <template #action="{ record }">
        <TableAction
          :actions="[
            {
              label: '编辑',
              auth: 'api:yt:{module}:update',
              icon: 'clarity:note-edit-line',
              onClick: handleEdit.bind(null, record),
            },
            {
              label: '删除',
              auth: 'api:yt:{module}:delete',
              icon: 'ant-design:delete-outlined',
              color: 'error',
              popConfirm: {
                title: '是否确认删除',
                confirm: handleDeleteOrBatchDelete.bind(null, record),
              },
            },
          ]"
        />
      </template>
    </BasicTable>
    <{Feature}Drawer @register="registerDrawer" @success="handleSuccess" />
  </div>
</template>
<script lang="ts">
  import { defineComponent, nextTick } from 'vue';
  import { BasicTable, useTable, TableAction } from '/@/components/Table';
  import { useDrawer } from '/@/components/Drawer';
  import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
  import { Authority } from '/@/components/Authority';
  import { Popconfirm } from 'ant-design-vue';
  import {Feature}Drawer from './{Feature}Drawer.vue';
  import { columns, searchFormSchema } from './{feature}.data';
  import { get{Feature}List, delete{Feature} } from '/@/api/{module}/{feature}';

  export default defineComponent({
    name: '{Feature}Management',
    components: {
      BasicTable,
      {Feature}Drawer,
      TableAction,
      Authority,
      Popconfirm,
    },
    setup() {
      const [registerDrawer, { openDrawer }] = useDrawer();

      function handleSuccess() {
        reload();
      }

      const [registerTable, { reload, setProps }] = useTable({
        title: '{业务名}列表',
        api: get{Feature}List,
        columns,
        formConfig: {
          labelWidth: 120,
          schemas: searchFormSchema,
        },
        useSearchForm: true,
        showTableSetting: true,
        bordered: true,
        showIndexColumn: false,
        rowKey: 'id',
        actionColumn: {
          width: 200,
          title: '操作',
          dataIndex: 'action',
          slots: { customRender: 'action' },
          fixed: 'right',
        },
      });

      const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
        delete{Feature},
        handleSuccess,
        setProps
      );
      nextTick(() => {
        setProps(selectionOptions);
      });

      function handleCreate() {
        openDrawer(true, {
          isUpdate: false,
        });
      }

      function handleEdit(record: Recordable) {
        openDrawer(true, {
          record,
          isUpdate: true,
        });
      }

      return {
        registerTable,
        registerDrawer,
        handleCreate,
        handleEdit,
        handleSuccess,
        hasBatchDelete,
        handleDeleteOrBatchDelete,
      };
    },
  });
</script>

4.3 {Feature}Drawer.vue — Drawer 表单模板(标准新增/编辑)

<template>
  <BasicDrawer
    v-bind="$attrs"
    @register="registerDrawer"
    showFooter
    :title="getTitle"
    width="500px"
    @ok="handleSubmit"
  >
    <BasicForm @register="registerForm" />
  </BasicDrawer>
</template>
<script lang="ts">
  import { defineComponent, ref, computed, unref } from 'vue';
  import { BasicForm, useForm } from '/@/components/Form';
  import { formSchema } from './{feature}.data';
  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  import { saveOrEdit{Feature} } from '/@/api/{module}/{feature}';
  import { useMessage } from '/@/hooks/web/useMessage';

  export default defineComponent({
    name: '{Feature}Drawer',
    components: { BasicDrawer, BasicForm },
    emits: ['success', 'register'],
    setup(_, { emit }) {
      const isUpdate = ref(true);
      let id: string | undefined;

      const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
        labelWidth: 100,
        schemas: formSchema,
        showActionButtonGroup: false,
      });

      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
        await resetFields();
        setDrawerProps({ confirmLoading: false });
        isUpdate.value = !!data?.isUpdate;
        if (unref(isUpdate)) {
          id = data.record.id;
          await setFieldsValue({
            ...data.record,
          });
        }
      });

      const getTitle = computed(() => (!unref(isUpdate) ? '新增{业务名}' : '编辑{业务名}'));

      async function handleSubmit() {
        setDrawerProps({ confirmLoading: true });
        try {
          const { createMessage } = useMessage();
          const values = await validate();
          let successMessage = '添加成功';
          if (unref(isUpdate)) {
            successMessage = '修改成功';
            Reflect.set(values, 'id', id);
          }
          await saveOrEdit{Feature}(values, unref(isUpdate)).then(() => {
            closeDrawer();
            emit('success');
            createMessage.success(successMessage);
          });
        } finally {
          setTimeout(() => {
            setDrawerProps({ confirmLoading: false });
          }, 300);
        }
      }

      return {
        registerDrawer,
        registerForm,
        getTitle,
        handleSubmit,
      };
    },
  });
</script>

4.4 {Feature}Modal.vue — Modal 表单模板(弹窗模式,替代 Drawer)

<template>
  <BasicModal
    v-bind="$attrs"
    @register="registerModal"
    :title="getTitle"
    @ok="handleSubmit"
  >
    <BasicForm @register="registerForm" />
  </BasicModal>
</template>
<script lang="ts">
  import { defineComponent, ref, computed, unref } from 'vue';
  import { BasicModal, useModalInner } from '/@/components/Modal';
  import { BasicForm, useForm } from '/@/components/Form';
  import { formSchema } from './{feature}.data';
  import { saveOrEdit{Feature} } from '/@/api/{module}/{feature}';
  import { useMessage } from '/@/hooks/web/useMessage';

  export default defineComponent({
    name: '{Feature}Modal',
    components: { BasicModal, BasicForm },
    emits: ['success', 'register'],
    setup(_, { emit }) {
      const isUpdate = ref(true);

      const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
        labelWidth: 100,
        schemas: formSchema,
        showActionButtonGroup: false,
      });

      const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
        await resetFields();
        setModalProps({ confirmLoading: false });
        isUpdate.value = !!data?.isUpdate;
        if (unref(isUpdate)) {
          await setFieldsValue({
            ...data.record,
          });
        }
      });

      const getTitle = computed(() => (!unref(isUpdate) ? '新增{业务名}' : '编辑{业务名}'));

      async function handleSubmit() {
        setModalProps({ confirmLoading: true });
        try {
          const { createMessage } = useMessage();
          const values = await validate();
          await saveOrEdit{Feature}(values, unref(isUpdate));
          closeModal();
          emit('success');
          createMessage.success(unref(isUpdate) ? '编辑成功' : '新增成功');
        } finally {
          setTimeout(() => {
            setModalProps({ confirmLoading: false });
          }, 300);
        }
      }

      return {
        registerModal,
        registerForm,
        getTitle,
        handleSubmit,
      };
    },
  });
</script>

4.5 API 文件模板

import { defHttp } from '/@/utils/http/axios';
import type {
  {Feature}PageParams,
  {Feature}Modal,
  {Feature}Params,
  {Feature}Info,
} from './model/{feature}Model';
import { getPageData } from '../../base';    // 分页查询使用 getPageData

enum Api {
  {FEATURE}_URL = '/{feature_path}',
  UPDATE_URL = '/{feature_path}/update',
}

/**
 * 分页查询{业务名}列表
 * @param params 查询参数(含分页)
 * @returns 分页数据
 */
export const get{Feature}List = (params: {Feature}PageParams) => {
  return getPageData<{Feature}Modal>(params, Api.{FEATURE}_URL);
};

/**
 * 新增{业务名}
 * @param params 新增参数
 */
export const add{Feature} = (params: {Feature}Params) => {
  return defHttp.post({
    url: Api.{FEATURE}_URL,
    data: params,
  });
};

/**
 * 更新{业务名}
 * @param params 更新参数
 */
export const update{Feature} = (params: {Feature}Params) => {
  return defHttp.post({
    url: Api.UPDATE_URL,
    data: params,
  });
};

/**
 * 保存或编辑{业务名}
 * @param params 表单数据
 * @param isUpdate 是否为编辑模式
 */
export const saveOrEdit{Feature} = (params: {Feature}Info, isUpdate: boolean) => {
  if (isUpdate) return update{Feature}(params);
  return add{Feature}(params);
};

/**
 * 删除{业务名}
 * @param ids 待删除的ID数组
 */
export const delete{Feature} = (ids: string[]) => {
  return defHttp.delete({
    url: Api.{FEATURE}_URL,
    data: ids,           // 方式一:直接传数组
    // data: { ids },    // 方式二:包装为对象
  });
};

/**
 * 更新{业务名}状态
 * @param id 记录ID
 * @param status 目标状态值
 */
export const set{Feature}Status = (id: string, status: number) => {
  return defHttp.put({ url: Api.{FEATURE}_URL + '/update_status/' + id + '/' + status });
};

API 设计模式说明:

  • 分页查询:统一使用 getPageData 工具函数(基于 defHttp.get
  • 新增:defHttp.post
  • 更新:defHttp.putdefHttp.post(取决于后端设计)
  • 删除:defHttp.delete,参数为 string[](ID数组)
  • 保存/编辑合并方法:saveOrEdit 模式,通过 isUpdate 参数区分

4.6 Model 文件模板

import { BasicPageParams } from '/@/api/model/baseModel';

// 分页查询参数
export type {Feature}PageParams = BasicPageParams & {
  keyword?: string;
  status?: number;
};

// 列表项数据模型
export interface {Feature}Modal {
  id: string;
  name: string;
  status: number;
  createTime: string;
  updateTime: string;
  remark?: string;
}

// 新增/编辑参数
export interface {Feature}Params {
  name: string;
  status: number;
  remark?: string;
}

export type {Feature}Info = {Feature}Params;

// 通用分页返回模型(如使用 getPageData)
// 返回类型为 PageData<{Feature}Modal>,已包含 items + total

基础模型说明(src/api/model/baseModel.ts):

export interface BasicPageParams {
  page: number;
  pageSize: number;
}

export interface BasicFetchResult<T> {
  items: T[];
  total: number;
}

分页基础请求类(src/api/base.ts):

export class BaseQueryRequest implements BaseQueryParams {
  constructor(page = 1, pageSize = 10) { ... }
}

export function getPageData<T>(params: BaseQueryParams, url: string): Promise<PageData<T>> { ... }

五、核心组件和 Hooks 速查

5.1 表格 — BasicTable + useTable

const [registerTable, { reload, setProps, getRowSelection, getSelectRowKeys, setSelectedRowKeys }] = useTable({
  title: '列表标题',
  api: getListApi,              // 必须是返回 Promise 的函数
  columns,                       // 从 .data.ts 导入
  formConfig: {                  // 搜索表单配置(与 useSearchForm 配合)
    labelWidth: 120,
    schemas: searchFormSchema,
    resetFunc: resetFn,          // 重置回调
  },
  useSearchForm: true,           // 是否显示搜索表单
  showTableSetting: true,        // 表格设置按钮
  bordered: true,                // 边框
  showIndexColumn: false,        // 序号列
  rowKey: 'id',                  // 行唯一标识
  canResize: false,              // 是否可以自适应高度
  pagination: true,              // 分页(树表设为 false)
  isTreeTable: false,            // 是否树形表格
  striped: false,                // 斑马纹
  clickToRowSelect: false,       // 点击行是否触发选择
  actionColumn: {                // 操作列
    width: 200,
    title: '操作',
    dataIndex: 'action',
    slots: { customRender: 'action' },
    fixed: 'right',
  },
  rowSelection: {                // 行选择(也可通过 useBatchDelete 注入)
    type: 'checkbox',
  },
});

常用表格方法:

  • reload() — 重新加载数据
  • setProps(props) — 动态设置表格属性
  • getRowSelection() — 获取当前选中行
  • getSelectRowKeys() — 获取选中行 key
  • setSelectedRowKeys(keys) — 设置选中行
  • collapseAll() — 折叠所有(树表用)

5.2 抽屉 — BasicDrawer + useDrawer / useDrawerInner

父组件:

const [registerDrawer, { openDrawer, closeDrawer }] = useDrawer();

// 打开抽屉
openDrawer(true, {
  isUpdate: false,     // 新增模式
  // isUpdate: true,   // 编辑模式
  record: data,        // 编辑时传入行数据
});

子组件(Drawer 内部):

const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  await resetFields();
  setDrawerProps({ confirmLoading: false });
  isUpdate.value = !!data?.isUpdate;
  if (unref(isUpdate)) {
    await setFieldsValue({ ...data.record });
  }
});

5.3 弹窗 — BasicModal + useModal / useModalInner

父组件:

const [registerModal, { openModal, closeModal }] = useModal();

// 打开弹窗
openModal(true, {
  isUpdate: false,
  record: data,
});

子组件(Modal 内部):

const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
  await resetFields();
  setModalProps({ confirmLoading: false });
  isUpdate.value = !!data?.isUpdate;
  if (unref(isUpdate)) {
    await setFieldsValue({ ...data.record });
  }
});

5.4 表单 — BasicForm + useForm

const [registerForm, {
  resetFields,         // 重置表单
  setFieldsValue,      // 设置表单值
  validate,            // 校验表单
  getFieldsValue,      // 获取表单值
  updateSchema,        // 动态更新 Schema(如禁用、隐藏字段)
  clearValidate,       // 清除校验
}] = useForm({
  labelWidth: 100,
  schemas: formSchema,
  showActionButtonGroup: false,
  baseColProps: { lg: 12, md: 24 },   // 默认栅格
});

5.5 权限控制 — Authority

<!-- 按钮级权限控制 -->
<Authority value="api:yt:{module}:post">
  <a-button type="primary">新增</a-button>
</Authority>

<!-- TableAction 行内权限 -->
{
  label: '编辑',
  auth: 'api:yt:{module}:update',
  icon: 'clarity:note-edit-line',
  onClick: handleEdit.bind(null, record),
}

5.6 批量删除 — useBatchDelete

import { useBatchDelete } from '/@/hooks/web/useBatchDelete';

const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions, resetSelectedRowKeys } =
  useBatchDelete(deleteFn, handleSuccess, setProps);

// 注入选择配置到表格
nextTick(() => {
  setProps(selectionOptions);
});

// 自定义 checkbox 禁用
selectionOptions.rowSelection.getCheckboxProps = (record: Recordable) => {
  if (record.status === 1) {
    return { disabled: true };
  }
  return { disabled: false };
};

5.7 消息提示 — useMessage

import { useMessage } from '/@/hooks/web/useMessage';

const { createMessage } = useMessage();
createMessage.success('操作成功');
createMessage.error('操作失败');
createMessage.warning('警告信息');
createMessage.info('提示信息');

5.8 确认弹框 — useSyncConfirm

import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm';

const { createSyncConfirm } = useSyncConfirm();

// 返回 Promise,支持 async/await
await createSyncConfirm({
  iconType: 'warning',
  content: '确认后数据将被删除',
});

5.9 国际化 — useI18n

import { useI18n } from '/@/hooks/web/useI18n';

const { t } = useI18n();

// 在 .data.ts 中使用
title: t('routes.common.system.tableTitleSystemMenuName'),

// 在组件中使用
const getI18nCreateMenu = computed(() => t('routes.common.system.pageSystemTitleCreateMenu'));

5.10 树形组件 — BasicTree

<BasicTree
  v-if="treeData.length"
  checkable
  toolbar
  ref="treeRef"
  :treeData="treeData"
  :replace-fields="{ title: 'name', key: 'id' }"
  :checkedKeys="checkedKeys"
  @check="handleCheckClick"
  title="菜单分配"
/>

5.11 树形选择 — TreeSelect / ApiTreeSelect

// 静态树形选择
{
  field: 'parentId',
  label: '上级菜单',
  component: 'TreeSelect',
  componentProps: {
    replaceFields: { title: 'menuName', key: 'id', value: 'id' },
    getPopupContainer: () => document.body,
  },
}

// 动态 API 树形选择
{
  field: 'organizationId',
  label: '所属组织',
  required: true,
  component: 'ApiTreeSelect',
  componentProps: {
    api: async () => {
      const data = await getOrganizationList();
      copyTransFun(data as any as any[]);
      return data;
    },
  },
}

六、编码规范

6.1 组件风格

  • 使用 Options API + defineComponent + setup() 函数(项目统一风格)
  • 不使用 <script setup> 语法糖
  • 组件必须声明 name 属性(PascalCase)
  • 组件必须显式注册 components

    export default defineComponent({
    name: 'FeatureDrawer',
    components: { BasicDrawer, BasicForm },
    emits: ['success', 'register'],
    setup(_, { emit }) {
    // ...
    return { /* 模板需要用到的变量和方法 */ };
    },
    });
    

6.2 数据操作

  • 使用 Reflect.get(obj, key)Reflect.set(obj, key, value) 操作对象属性(项目惯例)
  • 使用 Reflect.deleteProperty(obj, key) 删除属性
  • 使用 unref(isUpdate) 而非 isUpdate.value 读取 ref(在计算/判断中)

    // 推荐
    Reflect.set(values, 'id', id);
    const name = Reflect.get(data.record, 'name');
    
    // 不推荐
    values.id = id;  // 项目不使用此风格
    

6.3 Drawer/Modal 提交流程规范

async function handleSubmit() {
  setDrawerProps({ confirmLoading: true });  // 或 setModalProps
  try {
    const values = await validate();
    // 处理数据...
    await saveApi(values);
    closeDrawer();                            // 或 closeModal
    emit('success');
    createMessage.success('操作成功');
  } finally {
    setTimeout(() => {
      setDrawerProps({ confirmLoading: false });  // 延迟关闭loading,防止闪烁
    }, 300);
  }
}

6.4 表单校验规范

  • 必填字段:required: true
  • 长度限制:使用 dynamicRules 返回 validator
  • 特殊格式(手机/邮箱):使用 rules: phoneRule / rules: emailRule(来自 /@/utils/rules

    dynamicRules: () => {
    return [
    {
      required: false,
      validator: (_, value) => {
        if (String(value).length > 255) {
          return Promise.reject('字数不超过255个字');
        }
        return Promise.resolve();
      },
    },
    ];
    },
    

6.5 表格操作列规范

actionColumn: {
  width: 200,                          // 一般200,3个操作时300
  title: '操作',
  dataIndex: 'action',
  slots: { customRender: 'action' },
  fixed: 'right',
}

操作按钮配置:

{
  label: '编辑',
  auth: 'api:yt:{module}:update',     // 权限标识
  icon: 'clarity:note-edit-line',     // 编辑图标
  onClick: handleEdit.bind(null, record),
},
{
  label: '删除',
  auth: 'api:yt:{module}:delete',
  icon: 'ant-design:delete-outlined', // 删除图标
  color: 'error',                     // 红色
  popConfirm: {                       // 二次确认
    title: '是否确认删除',
    confirm: handleDelete.bind(null, record),
  },
},

6.6 权限控制规范

公共权限模式见公司统一配置 §6.5。以下为 Vben2 专属实现。

  • 工具栏按钮:使用 <Authority value="api:yt:{module}:{action}"> 包裹
  • 表格行操作:使用 auth: 'api:yt:{module}:{action}' 属性
  • 权限值格式:api:yt:{模块}:{操作}:{细化}

6.7 样式规范

  • 使用 <style scoped lang="less">
  • 使用 Windi CSS 原子类(如 p-4, w-3/4
  • 深度选择器使用 :deep()
  • Vben 组件样式覆盖示例:

    :deep(.vben-basic-tree) {
    width: 100% !important;
    }
    

七、树形表格页面模式

当需要树形表格(如菜单管理、组织管理)时,与标准 CRUD 的差异:

// useTable 配置差异
const [registerTable, { reload, collapseAll }] = useTable({
  isTreeTable: true,
  pagination: false,           // 树表不分页
  striped: false,
  canResize: false,
  rowKey: (record) => record.id,
  // 不使用 useSearchForm + formConfig
  // 而是使用外部搜索组件或 searchFormSchema
});

// 数据加载后默认展开
function onFetchSuccess() {
  nextTick(collapseAll);       // 或 expandAll
}

树形数据的上级选择:

// 在 useDrawerInner 中加载树形数据
let treeData = await getMenuList(1);
treeData = listToTree(treeData);
updateSchema({
  field: 'parentId',
  componentProps: { treeData },
});

八、左右分栏页面模式

当需要左侧树 + 右侧表格布局时:

<template>
  <div>
    <PageWrapper dense contentFullHeight contentClass="flex">
      <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
      <BasicTable style="flex: auto" @register="registerTable" :searchInfo="searchInfo">
        <!-- toolbar, action slots... -->
      </BasicTable>
    </PageWrapper>
  </div>
</template>

九、常见图标

操作 图标
编辑 clarity:note-edit-line
删除 ant-design:delete-outlined
新增 ant-design:plus-outlined
查看 ant-design:eye-outlined
详情 ant-design:profile-outlined

十、生成流程

当用户请求生成一个新的 CRUD 页面时,按以下步骤执行:

  1. 确认需求:模块名、业务名、字段列表、是否树形、Drawer/Modal 选择
  2. 创建 Model 文件src/api/{module}/model/{feature}Model.ts
  3. 创建 API 文件src/api/{module}/{feature}.ts
  4. 创建数据配置src/views/{module}/{feature}/{feature}.data.ts
  5. 创建 Drawer/Modal 组件src/views/{module}/{feature}/{Feature}Drawer.vue
  6. 创建主页面src/views/{module}/{feature}/index.vue
  7. 添加路由配置(如需要)

十一、关键导入路径速查

// 组件
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { BasicForm, useForm } from '/@/components/Form';
import { BasicDrawer, useDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicModal, useModal, useModalInner } from '/@/components/Modal';
import { BasicTree, TreeItem } from '/@/components/Tree';
import { Icon } from '/@/components/Icon';
import { Authority } from '/@/components/Authority';
import { PageWrapper } from '/@/components/Page';

// Hooks
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm';

// HTTP
import { defHttp } from '/@/utils/http/axios';

// 工具
import { listToTree } from '/@/utils/menuUtil';
import { copyTransFun, copyTransTreeFun } from '/@/utils/fnUtils';
import { emailRule, phoneRule } from '/@/utils/rules';
import { isArray } from '/@/utils/is';
import { getPageData, BaseQueryRequest } from '/@/api/base';

// 基础模型
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';

// 枚举
import { RoleEnum } from '/@/enums/roleEnum';

// Ant Design Vue
import { Tag, Switch, Popconfirm, Spin, notification } from 'ant-design-vue';