260601-前端页面各功能实现方式与代码规范.md 98 KB

前端页面实现模式分析

分析日期:2026-06-01\ 项目版本:JeecgBoot 3.9.2


一、概述

本文档深入分析湛江人社项目前端(jeecgboot-vue3/)中数据列表页面数据编辑页面的实现方式,涵盖查询表单、操作按钮、表单Input组件等核心 UI 模式的实现与使用规范。提炼出前端开发中的最大规范公约数,指导新的业务模块快速开发。


二、前端项目架构概览

2.1 技术栈

层次 技术 版本
框架 Vue 3 3.5.22
构建工具 Vite 6.3.6
UI框架 Ant Design Vue 4.2.6
状态管理 Pinia 2.1.7
HTTP Axios(封装为 defHttp) 1.12.2
表格 VXE-Table / BasicTable 4.13.31
语言 TypeScript 5.9.3

2.2 核心目录结构

jeecgboot-vue3/src/
├── api/                        # API 接口层
│   ├── sys/                    #   系统管理API
│   └── demo/                   #   示例API
├── components/                 # 公共组件层
│   ├── Table/                  #   BasicTable 核心表格组件
│   ├── Form/                   #   BasicForm 核心表单组件
│   ├── Modal/                  #   BasicModal 弹窗组件
│   ├── Drawer/                 #   BasicDrawer 抽屉组件
│   ├── jeecg/                  #   JeecgBoot 专用业务组件
│   └── Button/                 #   按钮封装
├── hooks/
│   ├── system/
│   │   ├── useListPage.ts      #   ★ 列表页面核心钩子
│   │   └── useMethods.ts       #   ★ 导入导出方法
│   └── web/                    #   通用钩子
├── views/                      # 页面级组件
│   ├── system/                 #   系统管理页面(标准模式)
│   │   ├── user/               #      用户管理
│   │   ├── role/               #      角色管理
│   │   ├── dict/               #      字典管理
│   │   └── notice/             #      通知公告
│   ├── spbb/                   #   ★ 自定义业务模块(代码生成模式)
│   │   ├── feedback/           #      视频帮办-反馈信息
│   │   └── video/              #      视频管理
│   └── super/online/           #   Online 低代码动态表单
│       └── cgform/auto/        #      自动生成的列表/表单页面
└── settings/                   # 全局配置

三、前端页面文件的组织规范

每个业务模块按功能分为四个文件(标准模式)或三个文件(代码生成模式):

3.1 标准模式(系统管理模块)

views/system/xxx/
├── index.vue           # 列表页面(模板 + 逻辑)
├── xxx.data.ts         # 数据定义(列定义 + 查询表单 + 编辑表单)
├── xxx.api.ts          # API 接口定义
├── components/         # 子组件
│   ├── XxxDrawer.vue   # 编辑抽屉(或 XxxModal.vue 编辑弹窗)
│   └── ...             # 其他子组件

3.2 代码生成模式(自定义业务模块)

views/spbb/xxx/
├── XxxList.vue         # 列表页面
├── Xxx.data.ts         # 数据定义
├── Xxx.api.ts          # API 接口定义
└── components/
    ├── XxxModal.vue    # 编辑弹窗(Modal容器层)
    └── XxxForm.vue     # 编辑表单(表单具体实现)

四、数据列表页面实现模式

4.1 标准模式(使用 useListPage + BasicTable)

以 用户管理列表页、角色管理列表页 为代表。

4.1.1 页面模板结构

<template>
  <div>
    <!-- 引用表格 -->
    <BasicTable @register="registerTable" :rowSelection="rowSelection">
      <!-- 插槽: table标题(操作按钮区域) -->
      <template #tableTitle>
        <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
        <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
        <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
        <!-- 批量操作下拉 -->
        <a-dropdown v-if="selectedRowKeys.length > 0">
          <template #overlay>
            <a-menu>
              <a-menu-item key="1" @click="batchHandleDelete">
                <Icon icon="ant-design:delete-outlined"></Icon>删除
              </a-menu-item>
            </a-menu>
          </template>
          <a-button>批量操作<Icon icon="mdi:chevron-down"></Icon></a-button>
        </a-dropdown>
      </template>
      <!-- 操作栏 -->
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
      </template>
    </BasicTable>
    <!-- 编辑抽屉/弹窗 -->
    <UserDrawer @register="registerDrawer" @success="handleSuccess" />
  </div>
</template>

4.1.2 Script 核心逻辑

import { BasicTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, searchFormSchema } from './user.data';
import { list, deleteUser, getExportUrl, getImportUrl } from './user.api';

// 注册编辑组件(Drawer 或 Modal)
const [registerDrawer, { openDrawer }] = useDrawer();

// 使用 useListPage 整合列表页公共方法
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
  designScope: 'user-list',
  tableProps: {
    title: '用户列表',
    api: list,              // 列表数据 API
    columns: columns,       // 列定义
    canResize: true,
    useSearchForm: true,    // 启用搜索表单
    formConfig: {
      schemas: searchFormSchema,  // 搜索条件配置
    },
    actionColumn: {
      width: 120,
    },
  },
  exportConfig: {
    name: '用户列表',
    url: getExportUrl,
  },
  importConfig: {
    url: getImportUrl,
  },
});

// 注册表格
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;

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

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

// 删除
async function handleDelete(record) {
  await deleteUser({ id: record.id }, reload);
}

// 操作栏(主操作 - 直接显示)
function getTableAction(record): ActionItem[] {
  return [
    { label: '编辑', onClick: handleEdit.bind(null, record) },
  ];
}

// 下拉操作栏(更多操作)
function getDropDownAction(record): ActionItem[] {
  return [
    { label: '详情', onClick: handleDetail.bind(null, record) },
    {
      label: '删除',
      popConfirm: {
        title: '是否确认删除',
        confirm: handleDelete.bind(null, record),
      },
    },
  ];
}

4.2 代码生成模式(使用 j-modal + 手动管理)

以 反馈信息列表页 为代表。

4.2.1 核心特点

  • 使用 super-query 组件提供高级查询能力
  • 使用 useSearchForm: false 关闭内置搜索表单
  • 编辑表单通过 defineExpose 暴露方法供 Modal 调用
  • 使用 v-auth 指令控制按钮权限
// 注册表格(不使用内置搜索表单)
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
  tableProps: {
    api: list,
    columns,
    useSearchForm: false,       // 不启用内置搜索
    actionColumn: { width: 120, fixed: 'right' },
    beforeFetch: async (params) => {
      return Object.assign(params, queryParam);
    },
  },
  exportConfig: { name: '视频帮办-反馈信息', url: getExportUrl },
  importConfig: { url: getImportUrl },
});

// 编辑表单通过 ref 调用
const registerModal = ref();
function handleAdd() {
  registerModal.value.disableSubmit = false;
  registerModal.value.add();
}
function handleEdit(record) {
  registerModal.value.disableSubmit = false;
  registerModal.value.edit(record);
}

4.3 useListPage 核心钩子分析

useListPage 是列表页面的核心封装,提供以下能力:

4.3.1 默认表格配置

配置项 默认值 说明
rowKey 'id' 主键字段
useSearchForm true 启用搜索表单
formConfig.compact true 紧凑模式
formConfig.autoSubmitOnEnter true 回车自动提交
formConfig.showAdvancedButton true 显示展开/收起按钮
formConfig.autoAdvancedCol 3 超过3列自动折叠
striped false 斑马纹(默认关闭)
canResize true 可自适应高度
minHeight 300 表格最小高度
bordered true 显示边框
showIndexColumn false 不显示序号列
showTableSetting true 显示表格设置
showActionColumn true 显示操作列
actionColumn.width 120 操作列宽度
默认排序 createTime DESC 无排序参数时默认

4.3.2 自适应列配置

const adaptiveColProps = {
  xs: 24,    // <576px
  sm: 12,    // ≥576px
  md: 12,    // ≥768px
  lg: 8,     // ≥992px
  xl: 8,     // ≥1200px
  xxl: 6,    // ≥1600px
};

4.3.3 提供的方法

方法 参数 说明
onExportXls() 导出 Excel(合并搜索条件 + 选中行)
onImportXls(file) file 导入 Excel
doRequest(api, options?) api + 配置 通用请求(自动显示确认框、加载、刷新、清空选择)
doDeleteRecord(api) api 快捷删除(无确认框)
tableContext - 表格上下文(解构出 registerTable, reload 等)

五、查询表单(Search Form)实现模式

5.1 标准模式 —— BasicTable 内置搜索

useListPage 默认提供了内置搜索表单。查询条件定义在 .data.ts 文件的 searchFormSchema 数组中:

// role.data.ts
export const searchFormSchema: FormSchema[] = [
  {
    field: 'roleName',
    label: '角色名称',
    component: 'Input',            // 使用的组件
    colProps: { span: 6 },         // 栅格占比
  },
  {
    field: 'roleCode',
    label: '角色编码',
    component: 'Input',
    colProps: { span: 6 },
  },
];

当超过 autoAdvancedCol(默认3列)时,多余的条件会自动折叠,显示"展开/收起"按钮。

5.2 搜索表单支持的组件类型

组件名 使用示例 说明
Input component: 'Input' 普通文本输入
JInput component: 'JInput' 支持模糊查询(传递 * 前缀)
JDictSelectTag { dictCode: 'sex' } 字典下拉选择
JSelectDept { placeholder: '请选择部门' } 部门树选择
ApiSelect { api: xxx } 异步数据下拉

5.3 代码生成模式 —— Online 动态查询

Online 低代码模式中,OnlineQueryForm 动态渲染查询条件:

  • 通过 /online/cgform/api/getQueryInfoVue3/{id} 接口获取字段配置
  • 使用 FormSchemaFactory.createFormSchema() 将字段配置转换为 FormSchema
  • 支持范围查询模板:groupDategroupDatetimegroupTimegroupNumber
  • 支持前3列为"常用查询",超出的自动折叠
  • 默认值优先级:config(配置) > cache(路由缓存) > param(地址栏参数)

六、编辑页面实现模式

6.1 模式一:Drawer 抽屉编辑(标准模式最常用)

以 用户编辑、角色编辑 为代表。

6.1.1 模板结构

<template>
  <BasicDrawer v-bind="$attrs" @register="registerDrawer" :title="getTitle" :width="500"
    @ok="handleSubmit" destroyOnClose>
    <BasicForm @register="registerForm" />
  </BasicDrawer>
</template>

6.1.2 Script 核心逻辑

import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { formSchema } from '../role.data';
import { saveOrUpdateRole } from '../role.api';

const isUpdate = ref(true);

// 注册表单(showActionButtonGroup: false 隐藏自带按钮,使用抽屉的 footer 按钮)
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
  labelWidth: 90,
  schemas: formSchema,
  showActionButtonGroup: false,
});

// 注册抽屉(data 从 openDrawer() 传入)
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  resetFields();
  isUpdate.value = !!data?.isUpdate;
  setDrawerProps({ confirmLoading: false });
  if (unref(isUpdate)) {
    setFieldsValue({ ...data.record });
  }
});

// 提交事件
async function handleSubmit() {
  const values = await validate();
  setDrawerProps({ confirmLoading: true });
  await saveOrUpdateRole(values, isUpdate.value);
  closeDrawer();
  emit('success');
}

6.2 模式二:Modal 弹窗编辑

以 字典编辑、通知公告编辑 为代表。

6.2.1 模板结构

<template>
  <BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" width="550px" @ok="handleSubmit">
    <BasicForm @register="registerForm" />
  </BasicModal>
</template>

6.2.2 与 Drawer 模式的对比

  • useDrawerInner -> useModalInner
  • BasicDrawer -> BasicModal
  • closeDrawer -> closeModal
  • 其他逻辑完全一致

6.3 模式三:代码生成模式(Modal + Form 分离)

以 反馈信息编辑 为代表。

6.3.1 特点

  • Modal 和 Form 分离为两个组件
  • Modal 使用 j-modal(非 BasicModal),通过 ref 控制
  • Form 使用传统的 a-form(非 BasicForm),通过 Form.useForm API
  • 通过 defineExpose 暴露 add()edit()submitForm() 方法

6.3.2 Modal 容器组件

<template>
  <j-modal :title="title" :width="800" :visible="visible" @ok="handleOk" @cancel="handleCancel">
    <SpbbFeedbackForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" />
    <template #footer>
      <a-button @click="handleCancel">取消</a-button>
      <a-button :class="{ 'jee-hidden': disableSubmit }" type="primary" @click="handleOk">确认</a-button>
    </template>
  </j-modal>
</template>
<script setup>
function add() {
  visible.value = true;
  nextTick(() => registerForm.value.add());
}
function edit(record) {
  visible.value = true;
  nextTick(() => registerForm.value.edit(record));
}
</script>

6.3.3 表单组件

<template>
  <JFormContainer :disabled="disabled">
    <a-form ref="formRef" :labelCol="labelCol" :wrapperCol="wrapperCol">
      <a-row>
        <a-col :span="24">
          <a-form-item label="视频id" v-bind="validateInfos.videoDataId">
            <a-input v-model:value="formData.videoDataId" placeholder="请输入视频id" allow-clear />
          </a-form-item>
        </a-col>
      </a-row>
    </a-form>
  </JFormContainer>
</template>
<script setup>
import { Form } from 'ant-design-vue';
const useForm = Form.useForm;
const formData = reactive({ id: '', videoDataId: '', feedbackType: '', ... });
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules);
defineExpose({ add, edit, submitForm });
</script>

6.4 三种编辑模式对比

特性 Drawer 模式 Modal 模式 代码生成模式
容器组件 BasicDrawer BasicModal j-modal
表单组件 BasicForm BasicForm a-form
数据绑定 setFieldsValue setFieldsValue 手动 Object.assign
验证方式 validate() validate() Form.useForm.validate()
组件通信 useDrawerInner useModalInner defineExpose + ref
适用场景 内容较多的编辑页 内容适中的编辑页 代码生成模块

七、Form Schema 配置详解

7.1 FormSchema 标准结构

interface FormSchema {
  field: string;                                    // 字段名
  label: string;                                    // 标签文本
  component: string;                                // 组件名
  required?: boolean;                               // 是否必填
  defaultValue?: any;                               // 默认值
  show?: boolean;                                   // 是否显示
  ifShow?: ({ values }) => boolean;                 // 条件显示
  dynamicDisabled?: ({ values }) => boolean;        // 条件禁用
  componentProps?: object | (({ formActionType, formModel }) => object); // 组件属性
  rules?: Rule[];                                   // 验证规则
  dynamicRules?: ({ model, schema, values }) => Rule[]; // 动态验证规则
  colProps?: { span: number };                      // 栅格配置
  slot?: string;                                    // 自定义插槽
}

7.2 表单组件使用大全

所有组件的配置方式,从 用户数据定义 和 角色数据定义 中提取:

组件名 引入/来源 关键配置属性 用途
Input Ant Design Vue placeholder, readOnly 普通文本输入
InputPassword Ant Design Vue autocomplete, visibilityToggle 密码输入
InputTextArea Ant Design Vue rows, placeholder 多行文本输入
InputNumber Ant Design Vue min, max, step, precision 数字输入
JDictSelectTag Jeecg 组件 dictCode, placeholder, type(radio/radioButton), stringToNumber, multiple 字典选择
JSelectDept Jeecg 组件 sync, checkStrictly, defaultExpandLevel, onSelect, onChange 部门树选择
JSelectDepartPost Jeecg 组件 multiple, izShowDepPath, params 部门岗位选择
JImageUpload Jeecg 组件 fileMax 图片上传
JSearchSelect Jeecg 组件 dict(格式:表名,显示字段,值字段), async, multiple 搜索选择(如租户)
JSwitch Jeecg 组件 options['1', '0']), query 开关
JInput Jeecg 组件 同 Input,增加模糊查询能力 支持模糊查询的输入
JRangeNumber Jeecg 组件 同 InputNumber 范围数值输入(Online)
StrengthMeter Jeecg 组件 autocomplete 密码强度检测
ApiSelect Ant Design Vue api, labelField, valueField, mode, immediate, params 异步下拉选择
Select Ant Design Vue mode, options, tagRender 普通下拉选择
RadioGroup Ant Design Vue options{label, value, key}[] 单选框组
DatePicker Ant Design Vue valueFormat, placeholder 日期选择
Switch Ant Design Vue checkedValue, uncheckedValue 开关
EditorWidget Jeecg 组件 ckeditor, ueditor 富文本编辑器
FileWidget Jeecg 组件 文件上传配置 文件上传
AreaLinkage Jeecg 组件 省市区联动配置 行政区划选择

7.3 动态控制功能

条件显示(ifShow)

{
  field: 'departIds',
  component: 'Select',
  ifShow: ({ values }) => values.userIdentity == 2,  // 当身份为"上级"时才显示
}

条件禁用(dynamicDisabled)

{
  field: 'username',
  component: 'Input',
  dynamicDisabled: ({ values }) => {
    return !!values.id;  // 编辑时禁用,新增时可编辑
  },
}

动态验证(dynamicRules)

// 唯一性校验
{
  field: 'roleCode',
  component: 'Input',
  dynamicRules: ({ values, model }) => {
    return [
      {
        required: true,
        validator: (_, value) => {
          return isRoleExist({ id: model.id, roleCode: value })
            .then(res => res.success ? resolve() : reject(res.message));
        },
      },
    ];
  },
}

// 重复校验工具函数
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_table', 'field_name', model, schema, true),

动态组件属性(componentProps 函数形式)

{
  field: 'selecteddeparts',
  component: 'JSelectDept',
  componentProps: ({ formActionType, formModel }) => {
    return {
      onSelect: (options, values) => {
        // 部门选择后更新岗位下拉数据
        formActionType.updateSchema([...]);
      },
    };
  },
}

7.4 列定义(BasicColumn)配置

export const columns: BasicColumn[] = [
  {
    title: '用户账号',      // 列标题
    dataIndex: 'username',  // 对应字段
    width: 120,             // 列宽
    resizable: true,        // 可调整列宽
    sorter: true,           // 可排序
    align: 'center',        // 对齐方式
    // 自定义渲染
    customRender: ({ text }) => {
      return render.renderDict(text, 'sex');  // 字典值翻译
    },
    // 或使用:
    customRender: ({ record, text }) => {
      return getDepartName(record.orgCodeTxt);
    },
  },
];

八、操作按钮与操作栏

8.1 表格标题按钮(tableTitle 插槽)

所有按钮都放在 #tableTitle 插槽中:

按钮 图标 使用的API/方法 条件显示
新增 ant-design:plus-outlined openDrawer(true, { isUpdate: false }) 始终显示
导出 ant-design:export-outlined onExportXls() 始终显示
导入 ant-design:import-outlined onImportXls() 始终显示
刷新缓存 ant-design:sync-outlined 自定义API 字典等特定页面
回收站 ant-design:hdd-outlined openModal(true, {}) 用户、字典等特定页面
批量操作 ant-design:down-outlined 下拉菜单 selectedRowKeys.length > 0

批量操作通过 a-dropdown 包裹,在选择了数据后才出现。

8.2 行操作栏(#action 插槽)

使用 TableAction 组件分为主操作和下拉操作:

// 主操作(直接显示为按钮)
function getTableAction(record): ActionItem[] {
  return [
    { label: '编辑', onClick: handleEdit.bind(null, record) },
  ];
}

// 下拉操作(点击"更多"展开)
function getDropDownAction(record): ActionItem[] {
  return [
    { label: '详情', onClick: handleDetail.bind(null, record) },
    {
      label: '删除',
      popConfirm: {  // 弹出确认框
        title: '是否确认删除',
        confirm: handleDelete.bind(null, record),
      },
    },
  ];
}

8.3 按钮权限控制

  • 指令方式v-auth="'system:user:add'"
  • 函数方式hasPermission('system:user:edit')
  • auth属性{ label: '编辑', auth: 'spbb:spbb_feedback:edit' }

8.4 操作按钮的典型实现

// 删除单个
async function handleDelete(record) {
  await deleteUser({ id: record.id }, reload);
}

// 批量删除
async function batchHandleDelete() {
  await batchDeleteUser({ ids: selectedRowKeys.value }, () => {
    selectedRowKeys.value = [];
    reload();
  });
}

// 批量删除(带确认框 - API 层实现)
// API 文件中的实现:
export const batchDelete = (params, handleSuccess) => {
  createConfirm({
    title: '确认删除',
    content: '是否删除选中数据',
    onOk: () => {
      return defHttp.delete({ url: Api.deleteBatch, data: params })
        .then(() => handleSuccess());
    },
  });
}

九、API 层实现模式

9.1 标准 API 定义

// xxx.api.ts
import { defHttp } from '/@/utils/http/axios';

enum Api {
  list = '/sys/xxx/list',
  add = '/sys/xxx/add',
  edit = '/sys/xxx/edit',
  deleteOne = '/sys/xxx/delete',
  deleteBatch = '/sys/xxx/deleteBatch',
  importExcel = '/sys/xxx/importExcel',
  exportXls = '/sys/xxx/exportXls',
}

export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;

export const list = (params) => defHttp.get({ url: Api.list, params });

export const saveOrUpdate = (params, isUpdate) => {
  let url = isUpdate ? Api.edit : Api.add;
  return defHttp.post({ url, params });
};

export const deleteOne = (params, handleSuccess) => {
  return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true })
    .then(() => handleSuccess());
};

export const batchDelete = (params, handleSuccess) => {
  // 带确认框的批量删除
  createConfirm({
    title: '确认删除',
    content: '是否删除选中数据',
    onOk: () => defHttp.delete({ url: Api.deleteBatch, data: params })
      .then(() => handleSuccess()),
  });
};

9.2 API 层规范

规范 说明
URL 定义 使用 enum Api {} 管理所有 URL
导入导出 导出 URL 用 getExportUrl 导出,导入 URL 用 getImportUrl 导出
导出/导入方法 useListPageuseMethods 统一处理
删除回调 删除方法接受 handleSuccess 回调,完成 API 后调用
HTTP 方法 list(GET), add(POST), edit(POST), delete(DELETE)
joinParamsToUrl DELETE 请求需要设置 { joinParamsToUrl: true }

9.3 defHttp 调用方式

场景 调用方式
GET 请求 defHttp.get({ url, params })
POST 请求 defHttp.post({ url, params })
DELETE 请求 defHttp.delete({ url, params }, { joinParamsToUrl: true })
文件上传 defHttp.uploadFile({ url }, { file: data.file }, { success })
文件下载 defHttp.get({ url, params, responseType: 'blob' }, { isTransformResponse: false, isReturnNativeResponse: true })

十、Online 动态表单的组件工厂模式

10.1 组件工厂架构

组件工厂 是 Online 低代码的核心,将后端配置的字段定义动态转化为前端表单组件。

FormSchemaFactory.createFormSchema(key, item)
    ↓ 根据 item.view 分发
各个 Widget 实现类
    ├── InputWidget          → text 文本输入
    ├── NumberWidget         → number 数值输入
    ├── SelectWidget         → select 下拉选择
    ├── RadioWidget          → radio 单选框
    ├── CheckboxWidget       → checkbox 多选框
    ├── SwitchWidget         → switch 开关
    ├── DateWidget           → date 日期选择
    ├── TimeWidget           → time 时间选择
    ├── TextAreaWidget       → textarea 多行文本
    ├── PasswordWidget       → password 密码输入
    ├── FileWidget           → file 文件上传
    ├── ImageWidget          → image 图片上传
    ├── EditorWidget         → editor 富文本编辑器
    ├── PopupWidget          → popup 弹出框选择
    ├── PopupDictWidget      → popup_dict 弹出字典
    ├── LinkTableWidget      → link_table 关联表
    ├── LinkDownWidget       → link_down 下拉联动
    ├── TreeSelectWidget     → tree_select 树选择
    ├── TreeCategoryWidget   → tree_category 树分类
    ├── SelectSearchWidget   → select_search 搜索选择
    ├── SelectDepartWidget   → depart 部门选择
    ├── SelectUserWidget     → user 用户选择(单选)
    ├── SelectUser2Widget    → user2 用户选择(多选)
    ├── SelectMultiWidget    → select_multi 多选
    ├── CascaderPcaForQueryWidget → pca 省市区级联
    ├── PcaWidget            → pca 省市区
    ├── AreaLinkage          → area_linkage 区划联动
    ├── SlotWidget           → 自定义插槽
    └── MarkdownWidget       → markdown 编辑器

10.2 Widget 实现接口

// IFormSchema.ts
interface IFormSchema {
  field: string;              // 字段名
  setFormRef(formRef);        // 设置表单 ref
  getFormItemSchema();        // 获取表单项配置
  asSearchForm();             // 转换为搜索表单配置
  isHidden();                 // 设置为隐藏
  noChange();                 // 不监听变化
}

10.3 组件注册机制

组件通过 index.ts 注册到全局:

// components/jeecg/ 下的组件
// 和 components/Form/src/jeecg/ 下的组件

// 在 registerGlobComp.ts 中统一注册
import JDictSelectTag from './components/jeecg/JDictSelectTag.vue';
app.component('JDictSelectTag', JDictSelectTag);

所有 Jeecg 专用组件以 J 开头的命名约定:

  • JInputJSwitchJImageUploadJSelectDeptJSelectDepartPostJSearchSelect
  • JDictSelectTagJRangeNumberJFormContainer

十一、最大规范公约数(前端)

11.1 列表页面公约数

标准模式模板(8步)

<template>
  <div>
    <BasicTable @register="registerTable" :rowSelection="rowSelection">
      <template #tableTitle>
        <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
        <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
        <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
        <a-dropdown v-if="selectedRowKeys.length > 0">
          <template #overlay>
            <a-menu>
              <a-menu-item key="1" @click="batchHandleDelete">
                <Icon icon="ant-design:delete-outlined"></Icon>删除
              </a-menu-item>
            </a-menu>
          </template>
          <a-button>批量操作<Icon icon="mdi:chevron-down"></Icon></a-button>
        </a-dropdown>
      </template>
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
      </template>
    </BasicTable>
    <XxxDrawer @register="registerDrawer" @success="reload" />
  </div>
</template>
// 第1步:导入基础设施
import { BasicTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, searchFormSchema } from './xxx.data';
import { list, deleteOne, getExportUrl, getImportUrl } from './xxx.api';

// 第2步:注册编辑组件
const [registerDrawer, { openDrawer }] = useDrawer();

// 第3步:调用 useListPage
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
  tableProps: {
    title: '模块名称',
    api: list,
    columns,
    formConfig: { schemas: searchFormSchema },
  },
  exportConfig: { name: '导出文件名', url: getExportUrl },
  importConfig: { url: getImportUrl },
});

// 第4步:解构表格上下文
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;

// 第5步:新增/编辑/删除函数
function handleCreate() { openDrawer(true, { isUpdate: false }); }
function handleEdit(record) { openDrawer(true, { record, isUpdate: true }); }
async function handleDelete(record) { await deleteOne({ id: record.id }, reload); }
async function batchHandleDelete() { await batchDelete({ ids: selectedRowKeys.value }, reload); }

// 第6步:主操作栏
function getTableAction(record): ActionItem[] {
  return [{ label: '编辑', onClick: handleEdit.bind(null, record) }];
}

// 第7步:下拉操作栏
function getDropDownAction(record): ActionItem[] {
  return [
    { label: '详情', onClick: handleDetail.bind(null, record) },
    { label: '删除', popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) }},
  ];
}

11.2 编辑页面公约数(Drawer + BasicForm 模式)

<template>
  <BasicDrawer v-bind="$attrs" @register="registerDrawer" :title="getTitle"
    :width="500" @ok="handleSubmit" destroyOnClose>
    <BasicForm @register="registerForm" />
  </BasicDrawer>
</template>
import { BasicForm, useForm } from '/@/components/Form';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { formSchema } from '../xxx.data';
import { saveOrUpdate } from '../xxx.api';

const isUpdate = ref(true);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
  labelWidth: 90,
  schemas: formSchema,
  showActionButtonGroup: false,
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  await resetFields();
  isUpdate.value = !!data?.isUpdate;
  setDrawerProps({ confirmLoading: false });
  if (unref(isUpdate)) await setFieldsValue({ ...data.record });
});
const getTitle = computed(() => !unref(isUpdate) ? '新增' : '编辑');
async function handleSubmit() {
  const values = await validate();
  setDrawerProps({ confirmLoading: true });
  await saveOrUpdate(values, isUpdate.value);
  closeDrawer();
  emit('success');
}

11.3 data.ts 文件公约数

import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';

// 1. 列定义
export const columns: BasicColumn[] = [
  { title: '名称', dataIndex: 'name', width: 120, resizable: true },
  { title: '创建时间', dataIndex: 'createTime', width: 100 },
];

// 2. 搜索表单
export const searchFormSchema: FormSchema[] = [
  { label: '名称', field: 'name', component: 'JInput', colProps: { span: 6 } },
];

// 3. 编辑表单
export const formSchema: FormSchema[] = [
  { label: '', field: 'id', component: 'Input', show: false },           // 隐藏ID
  { label: '名称', field: 'name', required: true, component: 'Input' },  // 基本信息
  { label: '类型', field: 'type', component: 'JDictSelectTag',           // 字典
    componentProps: { dictCode: 'xxx_type', stringToNumber: true }},
  { label: '部门', field: 'deptId', component: 'JSelectDept' },          // 部门
  { label: '日期', field: 'date', component: 'DatePicker' },             // 日期
  { label: '备注', field: 'remark', component: 'InputTextArea' },        // 多行
  { label: '排序', field: 'sort', component: 'InputNumber',              // 数字
    componentProps: { min: 1, precision: 0 }},
];

11.4 api.ts 文件公约数

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

enum Api {
  list = '/模块/实体/list',
  add = '/模块/实体/add',
  edit = '/模块/实体/edit',
  deleteOne = '/模块/实体/delete',
  deleteBatch = '/模块/实体/deleteBatch',
  importExcel = '/模块/实体/importExcel',
  exportXls = '/模块/实体/exportXls',
}

export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;
export const list = (params) => defHttp.get({ url: Api.list, params });
export const saveOrUpdate = (params, isUpdate) =>
  defHttp.post({ url: isUpdate ? Api.edit : Api.add, params });
export const deleteOne = (params, handleSuccess) =>
  defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
export const batchDelete = (params, handleSuccess) =>
  defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => handleSuccess());

11.5 开发一个前端新模块的最小步骤

  1. 创建目录views/模块名/
  2. 创建 api.ts:定义 7 个标准 API URL
  3. 创建 data.ts:定义 columns + searchFormSchema + formSchema
  4. 创建编辑组件components/XxxDrawer.vue(Drawer + BasicForm 模式)
  5. 创建列表页index.vue(BasicTable + useListPage 模式)
  6. 注册路由:在路由配置中添加新页面路由

十二、关键数据结构

12.1 FormSchema 完整类型

interface FormSchema {
  field: string;
  label: string;
  labelWidth?: number | string;
  component: string;
  componentProps?: object | ((opt: { formModel, formActionType }) => object);
  required?: boolean;
  rules?: Recordable[];
  dynamicRules?: (opt: { values, model, schema }) => Recordable[];
  ifShow?: (opt: { values, model, schema }) => boolean;
  show?: boolean;
  slot?: string;
  colProps?: { span: number; xs: number; sm: number; md: number; lg: number; xl: number; xxl: number };
  defaultValue?: any;
  dynamicDisabled?: (opt: { values, model, schema }) => boolean;
  helpMessage?: string | string[];
}

12.2 BasicColumn 标准类型

interface BasicColumn {
  title: string;
  dataIndex: string;
  width?: number;
  resizable?: boolean;
  sorter?: boolean;
  align?: 'left' | 'center' | 'right';
  fixed?: 'left' | 'right' | false;
  slots?: { customRender: string };
  customRender?: (opt: { text, record, index }) => any;
  defaultHidden?: boolean;     // 默认隐藏(表格设置中可配置显示)
}

十三、涉及的文件清单

文件 操作 原因
AIWork/260601-前端页面实现模式分析.md 新增 本次分析文档

十四、关键源码索引

组件/文件 路径
useListPage 钩子 jeecgboot-vue3/src/hooks/system/useListPage.ts
useMethods 钩子 jeecgboot-vue3/src/hooks/system/useMethods.ts
BasicTable props jeecgboot-vue3/src/components/Table/src/props.ts
BasicForm props jeecgboot-vue3/src/components/Form/src/props.ts
用户管理列表页 jeecgboot-vue3/src/views/system/user/index.vue
用户编辑抽屉 jeecgboot-vue3/src/views/system/user/UserDrawer.vue
用户数据定义 jeecgboot-vue3/src/views/system/user/user.data.ts
用户API定义 jeecgboot-vue3/src/views/system/user/user.api.ts
角色管理列表页 jeecgboot-vue3/src/views/system/role/index.vue
角色编辑抽屉 jeecgboot-vue3/src/views/system/role/components/RoleDrawer.vue
角色数据定义 jeecgboot-vue3/src/views/system/role/role.data.ts
字典管理列表页 jeecgboot-vue3/src/views/system/dict/index.vue
字典编辑弹窗 jeecgboot-vue3/src/views/system/dict/components/DictModal.vue
通知公告列表页 jeecgboot-vue3/src/views/system/notice/index.vue
通知公告编辑弹窗 jeecgboot-vue3/src/views/system/notice/NoticeModal.vue
反馈信息列表页 jeecgboot-vue3/src/views/spbb/feedback/SpbbFeedbackList.vue
反馈信息编辑弹窗 jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackModal.vue
反馈信息编辑表单 jeecgboot-vue3/src/views/spbb/feedback/components/SpbbFeedbackForm.vue
Online 查询表单 jeecgboot-vue3/src/views/super/online/cgform/auto/comp/OnlineQueryForm.vue
组件工厂入口 jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/FormSchemaFactory.ts
组件工厂接口 jeecgboot-vue3/src/views/super/online/cgform/auto/comp/factory/IFormSchema.ts

十五、文件上传组件实现模式

追加日期:2026-06-01

JeecgBoot 框架前端内置了完整的文件上传组件体系,与后端 /sys/common/upload 接口无缝对接。

15.1 核心上传组件概览

项目中主要有 3 种 文件上传组件,全部在 componentMap.ts 中注册:

组件名 注册键 源文件路径 用途
JImageUpload 'JImageUpload' JImageUpload.vue 专用图片上传
JUpload 'JUpload' JUpload.vue 通用文件/图片上传
Upload 'Upload' BasicUpload.vue 基础上传组件

辅助组件:

  • JUploadButton — 全局注册的导入按钮组件
  • JUploadModal — 弹窗式上传组件
  • JImportModal — Excel 导入弹窗

15.2 JImageUpload 组件详解

底层实现:基于 a-upload 组件封装,设置 accept="image/*"

核心 Props

Prop 类型 默认值 说明
value string \| array - 绑定值,逗号分隔的路径字符串
listType string 'picture-card' 列表样式:picture-card / picture
text string '上传' 按钮文本
bizPath string 'temp' 上传业务路径(对应后端 biz 参数)
disabled boolean false 是否禁用
fileMax number 1 最大上传数量
uploadUrl string 系统上传URL 可自定义上传地址
previewWidth number 520 预览宽度

值格式:逗号分隔的路径字符串(如 "temp/20260601/xxx.jpg,temp/20260601/yyy.jpg"

使用示例(在 FormSchema 中):

{
  label: '头像',
  field: 'avatar',
  component: 'JImageUpload',
  componentProps: {
    fileMax: 1,
    bizPath: 'avatar',
  },
}

使用示例(在原生表单中直接使用):

<JImageUpload v-model:value="formData.avatar" :fileMax="1" bizPath="avatar" />

15.3 JUpload 组件详解

底层实现:基于 a-upload,功能更丰富,支持文件和图片两种模式

上传类型枚举(upload.data.ts):

export enum UploadTypeEnum {
  all = 'all',      // 所有文件
  image = 'image',  // 仅图片
  file = 'file',    // 仅文件
}

核心 Props

Prop 类型 默认值 说明
value string \| array - 绑定值
text string '上传' 按钮文本
fileType string 'all' 文件类型:all/image/file
bizPath string 'temp' 上传业务路径
returnUrl boolean true true=仅返回url,false=返回fileName/filePath/fileSize
maxCount number 0 最大上传数量,0=不限制
multiple boolean true 是否允许多选
mover boolean true 是否显示左右移动按钮
download boolean true 是否显示下载按钮
removeConfirm boolean false 删除时是否确认
disabled boolean false 是否禁用
replaceLastOne boolean false 超出最大数量依然允许上传

值格式

  • returnUrl: true(默认):逗号分隔的路径字符串
  • returnUrl: false:JSON 字符串数组 [{fileName, filePath, fileSize}]

使用示例(在 FormSchema 中):

// 附件上传
{
  field: 'files',
  label: '通告附件',
  component: 'JUpload',
  componentProps: {
    text: '文件上传',
    maxCount: 20,
    download: true,
  },
}

// 图片上传(限制1张)
{
  field: 'uploadImageMax',
  component: 'JUpload',
  label: '上传图片(1)',
  componentProps: {
    fileType: UploadTypeEnum.image,
    maxCount: 1,
  },
}

15.4 上传 API 层

上传地址定义

api/common/api.ts:

const baseUploadUrl = globSetting.uploadUrl;
export const uploadUrl = `${baseUploadUrl}/sys/common/upload`;

这个 uploadUrlJImageUploadJUpload 组件直接引用作为默认上传地址。

defHttp.uploadFile 方法

Axios.ts 提供编程式上传方法:

uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams, callback?: UploadFileCallBack) {
  const formData = new window.FormData();
  formData.append(customFilename, params.file, params.filename);
  config.baseURL = glob.uploadUrl;
  return this.axiosInstance.request<T>({
    ...config,
    method: 'POST',
    data: formData,
    headers: { 'Content-type': ContentTypeEnum.FORM_DATA },
  });
}

API 文件中的使用

// 通用上传
export const uploadFile = (params, success) => {
  return defHttp.uploadFile({ url: uploadUrl }, params, { success });
};

15.5 文件 URL 构造机制

getFileAccessHttpUrl 是文件 URL 构造的核心函数:

export const getFileAccessHttpUrl = (fileUrl, prefix = 'http') => {
  let result = fileUrl;
  try {
    if (fileUrl && fileUrl.length > 0 && !fileUrl.startsWith(prefix)) {
      let isArray = fileUrl.indexOf('[') != -1;
      if (!isArray) {
        let prefix = `${baseApiUrl}/sys/common/static/`;
        if (!fileUrl.startsWith(prefix)) {
          result = `${prefix}${fileUrl}`;
        }
      }
    }
  } catch (e) {}
  return result;
};

关键行为:对于非 http 开头的相对路径,自动添加 ${baseApiUrl}/sys/common/static/ 前缀。这意味着:

  • 后端返回 "temp/20260601/xxx.jpg" → 前端自动构造为 http://localhost:8080/jeecg-boot/sys/common/static/temp/20260601/xxx.jpg
  • 如果路径已经是完整 URL(以 http 开头),则直接使用

15.6 上传组件的认证机制

JImageUploadJUpload 都通过 getHeaders() 函数自动附加 Token:

import { getHeaders } from '/@/utils/common/compUtils';
const headers = getHeaders();

getHeaders() 内部调用 getToken() 获取当前用户的 JWT Token,添加到请求头中。

15.7 上传流程总结

1. 组件层(JImageUpload / JUpload)
   ├── 使用 a-upload 的 action 属性直接上传到后端
   ├── 默认上传地址:${globSetting.uploadUrl}/sys/common/upload
   ├── 通过 bizPath 参数指定业务子路径
   └── 通过 getHeaders() 自动附加 Token

2. 后端处理
   ├── CommonController.upload() 接收文件
   ├── 安全校验:SsrfFileTypeFilter.checkUploadFileType()
   ├── 根据 uploadType 选择存储方式(local/minio/alioss)
   └── 返回 Result(success=true, message=相对路径)

3. 前端值存储
   ├── 上传成功后从 response.message 提取相对路径
   ├── 默认以逗号分隔的路径字符串存储(returnUrl: true)
   └── 编辑回显时通过 getFileAccessHttpUrl() 构造完整 URL

4. 文件访问
   ├── 本地存储:GET /sys/common/static/{相对路径}
   ├── 云存储:直接使用返回的完整 URL
   └── 静态文件服务接口免 Token 认证(Shiro anon 配置)

15.8 自定义上传场景的注意事项

当业务需要使用自定义上传接口(非 /sys/common/upload)时,需要注意以下限制:

组件 是否支持自定义上传地址 限制说明
JImageUpload ✅ 支持(uploadUrl prop) 但内部使用 getFileAccessHttpUrl 显示图片,该函数会自动添加 /sys/common/static/ 前缀,与自定义路径不兼容
JUpload ❌ 不支持 没有 uploadUrl prop,上传地址硬编码为系统默认地址
a-upload ✅ 支持(action prop) 完全自定义,需手动处理上传响应和 URL 构造

解决方案:在自定义上传场景中,使用 a-upload 直接控制,手动处理:

  1. 通过 action 属性指定自定义上传地址
  2. 通过 headers 属性传入认证 Token(使用 getHeaders()
  3. @change 回调中从 response.message 提取相对路径
  4. 自定义 getSpbbFileUrl() 函数构造文件访问 URL(而非使用 getFileAccessHttpUrl

15.9 关键源码索引(文件上传)

组件/文件 路径
JImageUpload 组件 jeecgboot-vue3/src/components/Form/src/jeecg/components/JImageUpload.vue
JUpload 组件 jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUpload.vue
上传类型枚举 jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/upload.data.ts
上传弹窗组件 jeecgboot-vue3/src/components/Form/src/jeecg/components/JUpload/JUploadModal.vue
导入按钮组件 jeecgboot-vue3/src/components/Button/src/JUploadButton.vue
通用 API 定义 jeecgboot-vue3/src/api/common/api.ts
上传 API 封装 jeecgboot-vue3/src/api/sys/upload.ts
Axios 上传方法 jeecgboot-vue3/src/utils/http/axios/Axios.ts
文件URL构造 jeecgboot-vue3/src/utils/common/compUtils.ts
用户头像上传示例 jeecgboot-vue3/src/views/system/user/user.data.ts
通知公告附件示例 jeecgboot-vue3/src/views/system/notice/notice.data.ts
JUpload Demo jeecgboot-vue3/src/views/demo/jeecg/JUploadDemo.vue

十六、表单校验实现方式与代码规范

追加日期:2026-06-01

16.1 表单校验体系总览

本项目前端存在 三大表单校验体系,分别服务于不同的业务场景:

体系 核心组件 校验引擎 适用场景
BasicForm components/Form Ant Design Vue Form(底层 async-validator) 主表单、一对一子表、通用表单
原生 a-form ant-design-vue Ant Design Vue Form(底层 async-validator) 登录/注册/忘记密码页面
JVxeTable components/jeecg/JVxeTable VXE-Table 内置 EditRules 一对多子表(行编辑表格)

Online 动态表单 组合使用 BasicForm + JVxeTable 两种引擎。

16.2 BasicForm 校验体系(主体系)

架构层次

Ant Design Vue Form (底层,内置 async-validator)
  └── BasicForm 组件 (封装层)
       ├── useForm.ts        — 外部调用 Hook
       ├── useFormEvents.ts  — 内部事件处理(validate/validateFields/clearValidate)
       ├── FormItem.vue      — 校验规则处理核心(handleRules)
       ├── helper.ts         — 校验辅助工具(占位提示/规则类型设置)
       └── validator.ts      — 通用校验规则库(email/phone/duplicateCheck等)

表单注册与校验调用

// 1. 在 xxx.data.ts 中定义 formSchema(含校验规则)
export const formSchema: FormSchema[] = [
  { field: 'username', label: '用户名', component: 'Input', required: true },
  { field: 'email', label: '邮箱', component: 'Input',
    dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'email', model, schema, false) },
];

// 2. 在 Vue 组件中注册表单
const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
  schemas: formSchema,
  showActionButtonGroup: false,
});

// 3. 提交时校验
async function handleSubmit() {
  try {
    const values = await validate();
    // ... 提交逻辑
  } catch (error) {
    // 校验失败,error 包含 errorFields
  }
}

校验规则的三种定义方式

方式一:required 属性(简单必填)

// 布尔值
{ field: 'username', required: true }

// 函数形式(动态判断)
{ field: 'accessed', required: ({ values }) => values.needAccess === true }

方式二:rules 数组(静态规则)

{ field: 'phone', rules: [
  { required: false, message: '请输入正确的手机号', pattern: /^1[3456789]\d{9}$/, trigger: 'blur' }
]}

{ field: 'password', rules: [
  { required: true, message: '请输入密码' },
  { pattern: /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[~!@#$%^&*()_+`\-={}:";'<>?,./]).{8,}$/,
    message: '密码需8位以上,包含数字、大小写字母和特殊符号' }
]}

方式三:dynamicRules 函数(动态规则 — 推荐)

// 唯一性校验(调后端接口)
{ field: 'username',
  dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'username', model, schema, true)
}

// 确认密码校验(依赖其他字段值)
{ field: 'confirmPassword',
  dynamicRules: ({ values }) => rules.confirmPassword(values, true)
}

// 邮箱格式 + 唯一性组合
{ field: 'email',
  dynamicRules: ({ model, schema }) => [
    { ...rules.duplicateCheckRule('sys_user', 'email', model, schema, false)[0], trigger: 'blur' },
    { ...rules.rule('email', false)[0], trigger: 'blur' },
  ]
}

// 自定义 validator(如角色编码唯一性)
{ field: 'roleCode',
  dynamicRules: ({ values, model }) => [{
    required: true,
    validator: (_, value) => {
      if (!value) return Promise.reject('请输入角色编码');
      return new Promise((resolve, reject) => {
        isRoleExist({ id: model.id, roleCode: value })
          .then(res => res.success ? resolve() : reject(res.message || '校验失败'))
          .catch(err => reject(err.message || '验证失败'));
      });
    },
  }]
}

handleRules() 核心逻辑

FormItem.vue 中的 handleRules() 函数是校验规则生成的核心:

handleRules() 执行流程:
  1. 禁用状态 → clearValidate + 返回空规则
  2. 无权限/隐藏 → 返回空规则
  3. 有 dynamicRules → 执行函数获取规则数组 + required 补充 + hijackValidator
  4. 有 rules → 深拷贝 + required 默认 validator 生成 + message 补充 + pattern 转正则 + hijackValidator
  5. 仅 required → 生成内置 validator(覆盖 undefined/null/空数组/空字符串/Tree未选中等场景)

关键特性

  • 禁用状态跳过校验:全局或单项禁用时直接 clearValidate 并返回空规则
  • 权限控制v-auth 指令控制显隐的字段,无权限时不校验
  • 防重复校验hijackValidator 通过 100ms 开关机制防止 validator 被重复调用
  • pattern 字符串转正则:支持将字符串类型的 pattern 转为 RegExp 对象
  • 自动补充 message:根据组件类型自动生成"请输入"/"请选择" + label 的提示信息

trigger 触发机制

trigger 值 触发时机 适用组件
'change' 值变化时 Select、Checkbox、Radio、Switch
'blur' 失焦时 Input、InputTextArea、InputNumber
['change', 'blur'] 值变化或失焦 通用
不设置 默认(Ant Design Vue 内部决定) 通用

FormItem.vue 中的特殊处理

// 如果规则中有 trigger: 'blur',则 change 事件时不自动校验
const findItem = getRules().find((item) => item?.trigger === 'blur');
if (!findItem) {
  // 没有blur触发器 → change时立即校验
  props.validateFields([field]).catch((_) => {});
}

校验方法 API

方法 说明 来源
validate() 校验整个表单,返回 Promise<表单值> useForm
validateFields(nameList) 校验指定字段 useForm
clearValidate(name?) 清除校验信息 useForm
scrollToField(name, options) 滚动到校验失败字段 useForm
setFieldsValue(values) 设置字段值后自动触发 validateFields useForm

16.3 通用校验规则库 — validator.ts

validator.ts 提供了项目级的通用校验方法:

方法 功能 默认 trigger
rules.rule(type, required) 根据 type 分发到 email/phone 'change'
rules.email(required) 邮箱格式校验 'change'
rules.phone(required) 手机号格式校验 /^1[3456789]\d{9}$/ 'change'
rules.startTime(endTime, required) 开始时间 < 结束时间 'change'
rules.endTime(startTime, required) 结束时间 > 开始时间 'change'
rules.confirmPassword(values, required) 确认密码一致性校验 无(默认)
rules.duplicateCheckRule(tableName, fieldName, model, schema, required?) 后端唯一性校验(调 /sys/duplicate/check 无(默认)
duplicateValidate(tableName, fieldName, fieldVal, dataId) 原生 <a-form> 用的唯一校验函数 -

使用规范

import { rules } from '/@/utils/helper/validator';

// 必填 + 唯一性校验
{ field: 'username', required: true,
  dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'username', model, schema, true)
}

// 非必填 + 格式校验 + 唯一性校验(组合使用)
{ field: 'email',
  dynamicRules: ({ model, schema }) => [
    { ...rules.duplicateCheckRule('sys_user', 'email', model, schema, false)[0], trigger: 'blur' },
    { ...rules.rule('email', false)[0], trigger: 'blur' },
  ]
}

// 确认密码
{ field: 'confirmPassword',
  dynamicRules: ({ values }) => rules.confirmPassword(values, true)
}

16.4 原生 a-form 校验体系(登录页面)

登录/注册/忘记密码页面使用原生 <a-form> 而非 BasicForm:

<template>
  <Form :model="formData" :rules="getFormRules" ref="formRef" @keypress.enter="handleLogin">
    <FormItem name="account" class="enter-x">
      <Input v-model:value="formData.account" placeholder="用户名" />
    </FormItem>
  </Form>
</template>

<script setup>
import { useFormRules, useFormValid } from './useLogin';

const formRef = ref();
const { getFormRules } = useFormRules();
const { validForm } = useFormValid(formRef);

async function handleLogin() {
  const data = await validForm();  // 内部调用 formRef.validate()
  if (!data) return;
  // ... 登录逻辑
}
</script>

useLogin.ts 提供两个 Hook:

  • useFormRules():根据当前登录状态(登录/注册/重置密码/手机号)返回不同的校验规则
  • useFormValid(formRef):封装 formRef.validate()validForm() 方法
登录状态 校验字段
LOGIN account 必填, password 必填
REGISTER account 后端唯一, password 必填, mobile 格式+后端唯一, sms 必填, confirmPassword 一致性, policy 勾选
RESET_PASSWORD username 必填, confirmPassword 一致性, sms 必填, mobile 必填
MOBILE sms 必填, mobile 必填

16.5 JVxeTable 校验体系(行编辑表格)

在列配置中通过 validateRules 数组定义:

const columns = [
  {
    key: 'name',
    title: '姓名',
    type: JVxeTypes.input,
    validateRules: [
      { required: true, message: '姓名不能为空' },
      { pattern: 'only', message: '姓名不能重复' },  // 唯一校验
    ],
  },
  {
    key: 'email',
    title: '邮箱',
    type: JVxeTypes.input,
    validateRules: [
      { pattern: 'e', message: '邮箱格式不正确' },  // 预置正则
    ],
  },
];

预置正则规则(Online 兼容,定义在 useValidateRules.ts):

说明 正则
* 非空 /^.+$/
n6-16 6到16位数字 /^\d{6,16}$/
*6-16 6到16位任意字符 /^.{6,16}$/
s6-18 6到18位字母 /^[a-z\|A-Z]{6,18}$/
url 网址 /^((ht\|f)tps?):\/\/...$/
e 电子邮件 /^[a-zA-Z0-9_-]+@...$/
m 手机号码 /^1[3456789]\d{9}$/
p 邮政编码 /^\d{6}$/
s 字母 /^[A-Z\|a-z]+$/
n 数字 /^-?\d+(\.?\d+\|\d?)$/
z 整数 /^-?\d+$/
money 金额 /^(([1-9][0-9]*)\|...)$/

校验执行方法

// 校验当前可见行(快速)
const errMap = await instance.validateTable();

// 校验所有行(含虚拟滚动隐藏的行)
const errMap = await instance.fullValidateTable();

唯一校验器(前端遍历表格数据,非后端接口):

function uniqueValidator({ methods }) {
  return function (event) {
    const { cellValue, column, rule } = event;
    if (cellValue == '') return Promise.resolve();
    let tableData = methods.getTableData();
    let findCount = 0;
    for (let rowData of tableData) {
      if (rowData[column.params.key] === cellValue) {
        if (++findCount >= 2) {
          return Promise.reject(new Error(rule.message));
        }
      }
    }
    return Promise.resolve();
  };
}

16.6 Online 动态表单校验

校验流程

handleSubmit()
  ├── 单表 → validate() [BasicForm]
  └── 主子表 → validateAll()
        ├── 第1步:validate() [主表 BasicForm]
        ├── 第2步:validateSubTableFields()
        │     ├── 一对一子表 → instance.getAll() → validate() [BasicForm]
        │     └── 一对多子表 → instance.fullValidateTable() [JVxeTable]
        └── 第3步:合并所有数据

校验失败自动定位

  • Tab 自动切换:校验失败时自动切换到出错子表
  • 滚动定位scrollToField(errorFields[0].name, { behavior: 'smooth', block: 'center' })
  • 一对多子表切换后重新校验sleep(300, () => instance?.validateTable())

Online 唯一校验(后端 API):

function checkOnlyFieldValue(rule, value) {
  return new Promise((resolve) => {
    if (!value) resolve('');
    let param = { tableName: realTableName, fieldName: rule.field, fieldVal: value };
    if (formData.id) param['dataId'] = formData.id;
    duplicateCheck(param)
      .then(res => res.success ? resolve('') : resolve(res.message))
      .catch(msg => resolve(msg));
  });
}

16.7 校验代码规范

校验规则定义规范

规范项 要求 示例
规则定义位置 统一放在 xxx.data.ts 文件中 user.data.tsrole.data.ts
简单必填 使用 required: true { field: 'name', required: true }
格式校验 使用 rules 数组 + trigger: 'blur' { rules: [{ pattern: /.../, trigger: 'blur' }] }
唯一性校验 使用 dynamicRules + rules.duplicateCheckRule() dynamicRules: ({ model, schema }) => rules.duplicateCheckRule(...)
依赖其他字段的校验 使用 dynamicRules dynamicRules: ({ values }) => rules.confirmPassword(values, true)
后端校验 使用 dynamicRules + 自定义 validator dynamicRules: ({ model }) => [{ validator: (_, value) => apiCheck(value) }]

trigger 使用规范

组件类型 推荐 trigger 原因
Input / InputTextArea / InputNumber 'blur' 避免输入过程中频繁触发校验
Select / Checkbox / Radio / Switch 'change' 选择即确定,可立即校验
唯一性校验 'blur' 减少后端请求频率
格式校验(邮箱/手机/正则) 'blur' 用户完成输入后再校验
确认密码 不设置(默认) 值变化时立即校验一致性

message 提示规范

场景 message 格式 示例
必填(输入类) 请输入${label} 请输入用户名
必填(选择类) 请选择${label} 请选择部门
格式错误 ${label}格式不正确 手机号码格式有误
唯一性失败 ${label}已存在 用户名已存在
长度限制 长度在 ${min} 到 ${max} 个字符 长度在 2 到 30 个字符
自定义错误 后端返回的 message duplicateCheck 接口返回值

自动 message 生成:当 required: true 但未指定 message 时,FormItem.vue 会根据组件类型自动生成"请输入"/"请选择" + label 的提示。

校验调用规范

// ✅ 推荐:使用 useForm 返回的 validate()
async function handleSubmit() {
  try {
    const values = await validate();
    await saveApi(values);
    emit('success');
  } catch (error) {
    // 校验失败,Ant Design Vue 自动显示错误提示
  } finally {
    setDrawerProps({ confirmLoading: false });
  }
}

// ❌ 不推荐:手动 try-catch validate 后不处理错误
async function handleSubmit() {
  const values = await validate();  // 可能抛出异常未捕获
  await saveApi(values);
}

16.8 校验流程全景图

┌─────────────────────────────────────────────────────────────────┐
│                     表单校验执行流程                               │
│                                                                  │
│  用户输入/选择                                                    │
│    ↓                                                             │
│  值变更事件 (change/blur)                                         │
│    ↓                                                             │
│  FormItem.renderComponent()                                      │
│    ├── 有 trigger:'blur' 规则?                                   │
│    │   ├── 是 → 等 blur 事件由 Ant Design Vue 内部触发校验         │
│    │   └── 否 → 立即 validateFields([field])                     │
│    ↓                                                             │
│  FormItem.handleRules() 生成规则                                  │
│    ├── 禁用/隐藏/无权限 → 返回空规则                               │
│    ├── dynamicRules → 执行函数获取规则                             │
│    ├── rules → 深拷贝 + 增强                                      │
│    └── required → 生成内置 validator                              │
│    ↓                                                             │
│  hijackValidator() 防重复执行                                     │
│    ↓                                                             │
│  Ant Design Vue Form.Item rules 绑定                              │
│    ↓                                                             │
│  async-validator 执行校验                                         │
│    ↓                                                             │
│  校验结果:通过 → 无提示 / 失败 → 红色提示                         │
│    ↓                                                             │
│  提交时:validate() 全量校验                                       │
│    ├── 成功 → 返回表单值                                          │
│    └── 失败 → 抛出异常(含 errorFields)                           │
│           └── scrollToField() 滚动到错误字段                      │
└─────────────────────────────────────────────────────────────────┘

16.9 关键源码索引(表单校验)

文件 作用
useForm.ts useForm Hook,外部调用入口
useFormEvents.ts 表单事件处理(validate/validateFields)
FormItem.vue 校验规则处理核心(handleRules)
form.ts FormSchema/Rule 类型定义
helper.ts 校验辅助(占位提示/规则类型)
validator.ts 通用校验规则库
useValidateRules.ts JVxeTable 校验规则
useLogin.ts 登录表单校验 Hook
user.data.ts 用户表单校验规则示例
IFormSchema.ts Online 表单 Schema 基类
useAutoForm.ts Online 表单唯一校验

十七、BasicTable 插槽(Slot)用法详解

追加日期:2026-06-02

17.1 插槽机制概述

BasicTable 的插槽机制由以下核心文件协同实现:

文件 作用
BasicTable.vue 主组件,模板中定义插槽透传逻辑
useTableHeader.ts 处理表头区域插槽(tableTitle, toolbar, headerTop, tableTop, alertAfter)
useTableForm.ts 处理搜索表单插槽(form-xxx)
TableHeader.vue 表头区域布局组件,渲染 headerTop/tableTitle/toolbar/tableTop/alertAfter

**插槽分类流程 **(BasicTable.vue 中 slotNamesGroup 计算属性):

用户在 <BasicTable> 中定义的插槽
        |
        v
slotNamesGroup 计算属性自动分类
        |
   +----+----+
   |         |
   v         v
native组   custom组   + 单独处理的插槽
   |         |              |
   v         v              v
直接透传   列级自定义    bodyCell: 优先匹配 slotsBak.customRender
给 antd     插槽渲染      form-xxx: 去掉前缀后传给 BasicForm
<Table>                  tableTitle/toolbar/headerTop/tableTop/alertAfter:
                         由 useTableHeader 传给 TableHeader 组件
                         expandedRowRender/summary:
                         直接在 <Table> 模板中使用

17.2 完整插槽列表

第一类:表头区域插槽

1. tableTitle — 表格标题/操作按钮区域
  • 位置:表格标题栏左侧
  • 用途:放置新增、导出、导入、批量操作等按钮,最常用的插槽
  • 作用域参数:无

<template #tableTitle>
  <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate">新增</a-button>
  <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
  <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
  <a-dropdown v-if="selectedRowKeys.length > 0">
    <template #overlay>
      <a-menu>
        <a-menu-item key="1" @click="batchHandleDelete">
          <Icon icon="ant-design:delete-outlined"></Icon>
          删除
        </a-menu-item>
      </a-menu>
    </template>
    <a-button>批量操作
      <Icon icon="mdi:chevron-down"></Icon>
    </a-button>
  </a-dropdown>
</template>

**源码实现 **:useTableHeader.ts 将 tableTitle 插槽通过 h() 函数传递给 TableHeader.vue ,TableHeader 中将其渲染在左侧 __tableTitle 区域。当未使用此插槽但配置了 title prop 时,会渲染默认的 TableTitle 组件。

2. toolbar — 工具栏区域
  • 位置:表格标题栏右侧,与 TableSetting(列配置按钮)同行
  • 用途:放置工具按钮,如"获取表单数据"、"切换自适应高度"等
  • 作用域参数:无

<template #toolbar>
  <a-button type="primary" @click="getFormValues">获取表单数据</a-button>
</template>

源码实现:TableHeader 中 toolbar 插槽渲染在右侧 __toolbar 区域,后面紧跟 TableSetting 组件。当 toolbar 插槽和 showTableSetting 同时存在时,中间会自动添加竖线分隔符 Divider

3. headerTop — 表头顶部区域
  • 位置:表格标题栏上方,独立一行
  • 用途:放置提示信息、自定义选中状态显示等
  • 作用域参数:无

<template #headerTop>
  <a-alert type="info" show-icon>
    <template #message>
      <template v-if="checkedKeys.length > 0">
        <span>已选中{{ checkedKeys.length }}条记录(可跨页)</span>
        <a-button type="link" @click="checkedKeys = []" size="small">清空</a-button>
      </template>
      <template v-else>
        <span>未选中任何项目</span>
      </template>
    </template>
  </a-alert>
</template>

源码实现:TableHeader 中 headerTop 插槽在最顶部渲染,外层包裹 margin: 5px 的 div。

4. tableTop — 表头下方区域(替代默认选中提示)
  • 位置:标题栏下方,默认是选中行数的 alert 提示
  • 用途:自定义选中提示区域,或用空内容隐藏默认提示
  • 作用域参数:无
<!-- 用法1:隐藏默认选中提示 -->
<template #tableTop><span></span></template>

<!-- 用法2:自定义选中提示内容 -->
<template #tableTop>
  <a-alert type="info" show-icon>
    <template #message>自定义提示内容</template>
  </a-alert>
</template>

源码实现:TableHeader 中 tableTop 插槽有默认内容——一个 <a-alert> 显示"已选中 X 条记录"和"清空"链接。使用此插槽会* *完全替换**默认提示。

5. alertAfter — 选中提示后方区域
  • 位置:紧跟在默认选中提示 alert 之后
  • 用途:在选中提示后追加自定义内容
  • 作用域参数:无

源码实现alertAfter 插槽嵌套在 tableTop 默认内容的 <a-alert> 内部,仅在选中行数 > 0 时显示。项目暂未发现实际使用案例。

第二类:表格内容区域插槽

6. bodyCell — 单元格内容(antdv3.x 兼容)
  • 位置:表格所有单元格
  • 用途:统一拦截所有单元格渲染,根据 column.key 判断是否自定义渲染
  • 作用域参数{ column, record, text, index, renderIndex }

<template #bodyCell="{ column, record }">
  <Avatar v-if="column.key === 'avatar'" :size="60" :src="record.avatar"/>
</template>

**重要机制 **:BasicTable.vue 中 bodyCell 插槽有特殊逻辑:


<template #bodyCell="data">
  <template v-if="data.column?.slotsBak?.customRender">
    <!-- 如果列配置了 slotsBak.customRender,优先使用列级自定义插槽 -->
    <slot :name="data.column.slotsBak.customRender" v-bind="data || {}"></slot>
  </template>
  <template v-else>
    <!-- 否则使用 bodyCell 插槽 -->
    <slot name="bodyCell" v-bind="data || {}"></slot>
  </template>
</template>

即:如果列配置了 slots.customRender,会优先使用列级命名插槽;否则走 bodyCell 全局插槽。

7. expandedRowRender — 展开行内容
  • 位置:点击行展开图标后显示的子内容区域
  • 用途:展示子表、详情信息、嵌套表格等
  • 作用域参数{ record, index, indent, expanded }
<!-- 简单用法 -->
<template #expandedRowRender="{ record }">
  <span>No: {{ record.no }}</span>
</template>

<!-- 复杂用法 - 嵌套子表 -->
<template #expandedRowRender="{ record }">
  <a-tabs v-model:activeKey="innerSubTable.tabIndex">
    <a-tab-pane v-for="(item, index) in innerSubTable.tabNav" :tab="item.tableTxt" :key="index + ''">
      <OnlCgformInnerSubTable :subTableId="item.id" :mTableSelectedRcordId="expandedRowKeys[0]"/>
    </a-tab-pane>
  </a-tabs>
</template>
8. summary — 表尾合计区域
  • 位置:表格底部合计行
  • 用途:自定义合计行渲染,覆盖默认的 TableSummary 组件
  • 作用域参数{ pageData }(当前页数据)

<template #summary="{ pageData }">
  <TableSummary fixed>
    <TableSummaryRow>
      <TableSummaryCell :index="0">合计</TableSummaryCell>
      <TableSummaryCell :index="1">{{ totalAmount }}</TableSummaryCell>
    </TableSummaryRow>
  </TableSummary>
</template>

生效条件:此插槽仅在 showSummaryRef 为 true(即配置了 summaryFuncsummaryData 且有数据)且 showSummary prop 为 false 时生效。默认情况下使用内置 TableSummary 组件渲染。

9. headerCell — 表头单元格
  • 位置:表头单元格
  • 用途:自定义表头单元格渲染
  • 作用域参数{ column, title }

特殊处理:当列是自定义选择列时,会渲染 CustomSelectHeader 而非用户自定义内容(解决全选框不显示的问题)。

第三类:列级自定义插槽

10. 列内容自定义插槽(slots.customRender
  • 定义方式:在列配置中设置 slots: { customRender: 'slotName' }
  • 作用域参数{ text, record, column, index, renderIndex }
// 列配置
const columns: BasicColumn[] = [
    {title: 'ID', dataIndex: 'id', slots: {customRender: 'id'}},
    {title: '编号', dataIndex: 'no', slots: {customRender: 'no'}},
    {title: '图片', dataIndex: 'imgArr', slots: {customRender: 'img'}},
];
<!-- 对应的插槽 -->
<template #id="{ record }">ID: {{ record.id }}</template>
<template #no="{ record }">
  <Tag color="green">{{ record.no }}</Tag>
</template>
<template #img="{ text }">
  <TableImg :size="60" :simpleShow="true" :imgList="text"/>
</template>

Online 表单中的典型用法

<!-- 文件下载插槽 -->
<template #fileSlot="{ text, record, column }">
  <span v-if="!text">无文件</span>
  <a-button v-else :ghost="true" type="primary" size="small" @click="downloadRowFile(text, record, column, ID)">下载
  </a-button>
</template>

<!-- 图片预览插槽 -->
<template #imgSlot="{ text }">
  <span v-if="!text">无图片</span>
  <img v-else :src="getImgView(text)" alt="图片不存在" class="online-cell-image"/>
</template>

<!-- 富文本插槽 -->
<template #htmlSlot="{ text, column, record }">
  <template v-if="column.fieldHref">
    <a v-html="text" @click="handleClickFieldHref(column.fieldHref, record)"></a>
  </template>
  <div v-else v-html="text"></div>
</template>

<!-- 省市区插槽 -->
<template #pcaSlot="{ text, column }">
  <div :title="getPcaText(text, column)">{{ getPcaText(text, column) }}</div>
</template>

<!-- 日期格式化插槽 -->
<template #dateSlot="{ text, column }">
  <span>{{ getFormatDate(text, column) }}</span>
</template>
11. 列标题自定义插槽(slots.title
  • 定义方式:在列配置中设置 slots: { title: 'slotName' }
  • 作用域参数:无
// 列配置
{
    dataIndex: 'name', slots
:
    {
        title: 'customTitle'
    }
}
{
    dataIndex: 'address', slots
:
    {
        title: 'customAddress'
    }
}

<template #customTitle>
  <span>姓名 <BasicHelp class="ml-2" text="姓名"/></span>
</template>
<template #customAddress>
  地址
  <FormOutlined class="ml-2"/>
</template>
12. action — 操作列插槽(最常用的列级插槽)
  • 定义方式:通过 actionColumn 配置自动生成,内部设置 slots: { customRender: 'action' }
  • 作用域参数{ record, index }

<template #action="{ record }">
  <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>

第四类:搜索表单插槽

13. form-xxx — 搜索表单字段自定义插槽
  • 定义方式:在 FormSchema 中配置 slot: 'slotName',使用时插槽名为 form-slotName
  • 作用域参数{ model, field, values }(model 是表单数据对象,field 是字段名)
// FormSchema 配置
{
    field: 'email', component
:
    'JInput', slot
:
    'email'
}
{
    field: 'age', component
:
    'JInput', slot
:
    'age'
}
<!-- 邮箱自定义搜索 -->
<template #form-email="{ model, field }">
  <a-input placeholder="请输入邮箱" v-model:value="model[field]" addon-before="邮箱:" addon-after=".com"></a-input>
</template>

<!-- 年龄范围搜索 -->
<template #form-age="{ model, field }">
  <a-input placeholder="最小年龄" type="ge" v-model:value="min" style="width: calc(50% - 15px)"
           @change="ageChange(model, field)"></a-input>
  <span>~</span>
  <a-input placeholder="最大年龄" type="le" v-model:value="max" style="width: calc(50% - 15px)"
           @change="ageChange(model, field)"></a-input>
</template>

**机制说明 **:useTableForm.ts 中,getFormSlotKeys 计算属性会自动识别所有以 form- 开头的插槽,replaceFormSlotKey 函数将 form- 前缀去掉后映射到 BasicForm 的对应插槽。

第五类:Ant Design 原生插槽透传

slotNamesGroup 计算属性会将非 bodyCell 且非列级自定义的插槽归入 native 组,直接透传给底层 <Table> 组件。因此 Ant Design Vue Table 支持的所有原生插槽都可以直接使用,包括但不限于:

原生插槽 作用域参数 用途
headerCell { column, title } 自定义表头单元格
customFilterIcon { filtered } 自定义筛选图标
expandIcon { expanded, onExpand, record } 自定义展开图标
emptyText 空数据提示

17.3 插槽分类汇总表

插槽名 类别 作用域参数 典型放置组件 使用频率
tableTitle 表头区域 a-button, a-dropdown, j-upload-button, Icon 极高
toolbar 表头区域 a-button
headerTop 表头区域 a-alert
tableTop 表头区域 自定义内容/空span(隐藏默认提示)
alertAfter 表头区域 自定义内容 极低
bodyCell 表格内容 { column, record, text, index } Avatar, Tag, 任意组件
expandedRowRender 表格内容 { record, index, indent, expanded } a-tabs, 子表格, 详情面板
summary 表格内容 { pageData } TableSummary
headerCell 表格内容 { column, title } 自定义表头
action 列级自定义 { record, index } TableAction 极高
自定义列插槽 列级自定义 { text, record, column, index } Tag, TableImg, a-button, img
自定义标题插槽 列级自定义 BasicHelp, Icon, 自定义标题
form-xxx 搜索表单 { model, field, values } a-input, 自定义表单控件
Ant原生插槽 原生透传 视具体插槽而定 视具体插槽而定

17.4 插槽中可放置的前端组件

BasicTable 的插槽本质上是 Vue 的标准插槽机制,可以放置任何合法的 Vue 组件或 HTML 内容。项目中实际使用的组件包括:

Ant Design Vue 组件

组件 常用插槽 用途
a-button tableTitle, toolbar, action 操作按钮(最常见)
a-dropdown / a-menu / a-menu-item tableTitle 下拉菜单/批量操作
a-alert headerTop, tableTop 提示信息
a-input form-xxx 输入框(表单插槽)
a-tabs / a-tab-pane expandedRowRender 标签页(展开行)
Tag 列自定义插槽 标签展示
Avatar bodyCell 头像展示
a-divider tableTitle 分隔线

项目自定义组件

组件 常用插槽 用途
TableAction action 操作列组件(配合 action 插槽使用,最核心)
TableImg 列自定义插槽 图片展示组件
BasicHelp 列标题插槽 帮助提示组件
Icon tableTitle, toolbar 图标组件
j-upload-button tableTitle 上传按钮
OnlineSuperQuery tableTitle 高级查询组件
OnlCgformInnerSubTable expandedRowRender 子表组件
import-excel-progress tableTitle 导入进度组件

原生 HTML

  • span, div, img, a 等基础标签
  • v-html 渲染富文本内容

任意自定义 Vue 组件

只要是合法的 Vue 组件,都可以放入插槽中使用。

17.5 插槽使用代码规范

规范一:操作按钮统一放在 tableTitle 插槽

<!-- ✅ 推荐 -->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
  <template #tableTitle>
    <a-button type="primary" v-auth="'xxx:add'" preIcon="ant-design:plus-outlined" @click="handleAdd">新增</a-button>
    <a-button type="primary" v-auth="'xxx:export'" preIcon="ant-design:export-outlined" @click="onExportXls">导出
    </a-button>
    <j-upload-button type="primary" v-auth="'xxx:import'" preIcon="ant-design:import-outlined" @click="onImportXls">
      导入
    </j-upload-button>
  </template>
</BasicTable>

<!-- ❌ 不推荐:操作按钮放在其他位置 -->

规范二:操作列统一使用 action 插槽 + TableAction 组件

<!-- ✅ 推荐 -->
<template #action="{ record }">
  <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>

<!-- ❌ 不推荐:在 action 插槽中直接写按钮 -->
<template #action="{ record }">
  <a-button @click="handleEdit(record)">编辑</a-button>
  <a-button @click="handleDelete(record)">删除</a-button>
</template>

规范三:列自定义渲染优先使用 slots.customRender 而非 bodyCell

// ✅ 推荐:使用列级插槽,语义清晰
{
    title: '状态', dataIndex
:
    'status', slots
:
    {
        customRender: 'status'
    }
}
// 对应模板: <template #status="{ record }">...</template>

// ❌ 不推荐:使用 bodyCell 全局拦截,所有列都在一个插槽中判断
// <template #bodyCell="{ column, record }">
//   <template v-if="column.key === 'status'">...</template>
//   <template v-if="column.key === 'type'">...</template>
// </template>

规范四:搜索表单自定义控件使用 form-xxx 插槽

// data.ts 中定义
{
    field: 'age', component
:
    'JInput', slot
:
    'age'
}
<!-- 列表页中使用 -->
<template #form-age="{ model, field }">
  <a-input v-model:value="model[field]" placeholder="请输入年龄"/>
</template>

规范五:隐藏默认选中提示时使用空 tableTop 插槽

<!-- 隐藏默认的"已选中X条记录"提示 -->
<template #tableTop><span></span></template>

规范六:批量操作按钮使用 a-dropdown 并条件显示


<template #tableTitle>
  <!-- 其他按钮... -->
  <a-dropdown v-if="selectedRowKeys.length > 0">
    <template #overlay>
      <a-menu>
        <a-menu-item key="1" @click="batchHandleDelete">
          <Icon icon="ant-design:delete-outlined"></Icon>
          删除
        </a-menu-item>
      </a-menu>
    </template>
    <a-button>批量操作
      <Icon icon="mdi:chevron-down"></Icon>
    </a-button>
  </a-dropdown>
</template>

规范七:按钮权限控制

<!-- 使用 v-auth 指令 -->
<a-button type="primary" v-auth="'spbb:sppb_video:add'" @click="handleAdd">新增</a-button>

<!-- 在 TableAction 中使用 auth 属性 -->
function getTableAction(record): ActionItem[] {
return [
{ label: '编辑', onClick: handleEdit.bind(null, record), auth: 'spbb:sppb_video:edit' },
];
}

17.6 关键源码索引(BasicTable 插槽)

文件 作用
BasicTable.vue 主组件,定义插槽透传逻辑和 slotNamesGroup 分类
useTableHeader.ts 处理表头区域插槽传递给 TableHeader
useTableForm.ts 处理 form-xxx 插槽的识别和前缀替换
TableHeader.vue 表头布局组件,渲染 headerTop/tableTitle/toolbar/tableTop/alertAfter
CustomerCell.vue 列自定义插槽 + bodyCell 使用示例
ExpandTable.vue expandedRowRender 使用示例
FormTable.vue headerTop/toolbar/form-xxx 使用示例
BasicTableDemoAjax.vue form-email 自定义搜索示例
OnlCgformInnerTableList.vue expandedRowRender + Online 列插槽综合示例