分析日期:2026-06-01\ 项目版本:JeecgBoot 3.9.2
本文档深入分析湛江人社项目前端(jeecgboot-vue3/)中数据列表页面和数据编辑页面的实现方式,涵盖查询表单、操作按钮、表单Input组件等核心 UI 模式的实现与使用规范。提炼出前端开发中的最大规范公约数,指导新的业务模块快速开发。
| 层次 | 技术 | 版本 |
|---|---|---|
| 框架 | 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 |
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/ # 全局配置
每个业务模块按功能分为四个文件(标准模式)或三个文件(代码生成模式):
views/system/xxx/
├── index.vue # 列表页面(模板 + 逻辑)
├── xxx.data.ts # 数据定义(列定义 + 查询表单 + 编辑表单)
├── xxx.api.ts # API 接口定义
├── components/ # 子组件
│ ├── XxxDrawer.vue # 编辑抽屉(或 XxxModal.vue 编辑弹窗)
│ └── ... # 其他子组件
views/spbb/xxx/
├── XxxList.vue # 列表页面
├── Xxx.data.ts # 数据定义
├── Xxx.api.ts # API 接口定义
└── components/
├── XxxModal.vue # 编辑弹窗(Modal容器层)
└── XxxForm.vue # 编辑表单(表单具体实现)
以 用户管理列表页、角色管理列表页 为代表。
<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>
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),
},
},
];
}
以 反馈信息列表页 为代表。
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);
}
useListPage 是列表页面的核心封装,提供以下能力:
| 配置项 | 默认值 | 说明 |
|---|---|---|
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 |
无排序参数时默认 |
const adaptiveColProps = {
xs: 24, // <576px
sm: 12, // ≥576px
md: 12, // ≥768px
lg: 8, // ≥992px
xl: 8, // ≥1200px
xxl: 6, // ≥1600px
};
| 方法 | 参数 | 说明 |
|---|---|---|
onExportXls() |
无 | 导出 Excel(合并搜索条件 + 选中行) |
onImportXls(file) |
file | 导入 Excel |
doRequest(api, options?) |
api + 配置 | 通用请求(自动显示确认框、加载、刷新、清空选择) |
doDeleteRecord(api) |
api | 快捷删除(无确认框) |
tableContext |
- | 表格上下文(解构出 registerTable, reload 等) |
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列)时,多余的条件会自动折叠,显示"展开/收起"按钮。
| 组件名 | 使用示例 | 说明 |
|---|---|---|
Input |
component: 'Input' |
普通文本输入 |
JInput |
component: 'JInput' |
支持模糊查询(传递 * 前缀) |
JDictSelectTag |
{ dictCode: 'sex' } |
字典下拉选择 |
JSelectDept |
{ placeholder: '请选择部门' } |
部门树选择 |
ApiSelect |
{ api: xxx } |
异步数据下拉 |
Online 低代码模式中,OnlineQueryForm 动态渲染查询条件:
/online/cgform/api/getQueryInfoVue3/{id} 接口获取字段配置FormSchemaFactory.createFormSchema() 将字段配置转换为 FormSchemagroupDate、groupDatetime、groupTime、groupNumberconfig(配置) > cache(路由缓存) > param(地址栏参数)以 用户编辑、角色编辑 为代表。
<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/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');
}
以 字典编辑、通知公告编辑 为代表。
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" width="550px" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
useDrawerInner -> useModalInnerBasicDrawer -> BasicModalcloseDrawer -> closeModal以 反馈信息编辑 为代表。
j-modal(非 BasicModal),通过 ref 控制a-form(非 BasicForm),通过 Form.useForm APIdefineExpose 暴露 add()、edit()、submitForm() 方法<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>
<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>
| 特性 | Drawer 模式 | Modal 模式 | 代码生成模式 |
|---|---|---|---|
| 容器组件 | BasicDrawer |
BasicModal |
j-modal |
| 表单组件 | BasicForm |
BasicForm |
a-form |
| 数据绑定 | setFieldsValue |
setFieldsValue |
手动 Object.assign |
| 验证方式 | validate() |
validate() |
Form.useForm.validate() |
| 组件通信 | useDrawerInner |
useModalInner |
defineExpose + ref |
| 适用场景 | 内容较多的编辑页 | 内容适中的编辑页 | 代码生成模块 |
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; // 自定义插槽
}
所有组件的配置方式,从 用户数据定义 和 角色数据定义 中提取:
| 组件名 | 引入/来源 | 关键配置属性 | 用途 |
|---|---|---|---|
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 组件 | 省市区联动配置 | 行政区划选择 |
{
field: 'departIds',
component: 'Select',
ifShow: ({ values }) => values.userIdentity == 2, // 当身份为"上级"时才显示
}
{
field: 'username',
component: 'Input',
dynamicDisabled: ({ values }) => {
return !!values.id; // 编辑时禁用,新增时可编辑
},
}
// 唯一性校验
{
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),
{
field: 'selecteddeparts',
component: 'JSelectDept',
componentProps: ({ formActionType, formModel }) => {
return {
onSelect: (options, values) => {
// 部门选择后更新岗位下拉数据
formActionType.updateSchema([...]);
},
};
},
}
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);
},
},
];
所有按钮都放在 #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 包裹,在选择了数据后才出现。
使用 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),
},
},
];
}
v-auth="'system:user:add'"hasPermission('system:user:edit'){ label: '编辑', auth: 'spbb:spbb_feedback:edit' }// 删除单个
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());
},
});
}
// 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()),
});
};
| 规范 | 说明 |
|---|---|
| URL 定义 | 使用 enum Api {} 管理所有 URL |
| 导入导出 | 导出 URL 用 getExportUrl 导出,导入 URL 用 getImportUrl 导出 |
| 导出/导入方法 | 由 useListPage 和 useMethods 统一处理 |
| 删除回调 | 删除方法接受 handleSuccess 回调,完成 API 后调用 |
| HTTP 方法 | list(GET), add(POST), edit(POST), delete(DELETE) |
joinParamsToUrl |
DELETE 请求需要设置 { joinParamsToUrl: true } |
| 场景 | 调用方式 |
|---|---|
| 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 低代码的核心,将后端配置的字段定义动态转化为前端表单组件。
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 编辑器
// IFormSchema.ts
interface IFormSchema {
field: string; // 字段名
setFormRef(formRef); // 设置表单 ref
getFormItemSchema(); // 获取表单项配置
asSearchForm(); // 转换为搜索表单配置
isHidden(); // 设置为隐藏
noChange(); // 不监听变化
}
组件通过 index.ts 注册到全局:
// components/jeecg/ 下的组件
// 和 components/Form/src/jeecg/ 下的组件
// 在 registerGlobComp.ts 中统一注册
import JDictSelectTag from './components/jeecg/JDictSelectTag.vue';
app.component('JDictSelectTag', JDictSelectTag);
所有 Jeecg 专用组件以 J 开头的命名约定:
JInput、JSwitch、JImageUpload、JSelectDept、JSelectDepartPost、JSearchSelectJDictSelectTag、JRangeNumber、JFormContainer<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) }},
];
}
<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');
}
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 }},
];
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());
views/模块名/components/XxxDrawer.vue(Drawer + BasicForm 模式)index.vue(BasicTable + useListPage 模式)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[];
}
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 接口无缝对接。
项目中主要有 3 种 文件上传组件,全部在 componentMap.ts 中注册:
| 组件名 | 注册键 | 源文件路径 | 用途 |
|---|---|---|---|
JImageUpload |
'JImageUpload' |
JImageUpload.vue | 专用图片上传 |
JUpload |
'JUpload' |
JUpload.vue | 通用文件/图片上传 |
Upload |
'Upload' |
BasicUpload.vue | 基础上传组件 |
辅助组件:
JUploadButton — 全局注册的导入按钮组件JUploadModal — 弹窗式上传组件JImportModal — Excel 导入弹窗底层实现:基于 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" />
底层实现:基于 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,
},
}
api/common/api.ts:
const baseUploadUrl = globSetting.uploadUrl;
export const uploadUrl = `${baseUploadUrl}/sys/common/upload`;
这个 uploadUrl 被 JImageUpload 和 JUpload 组件直接引用作为默认上传地址。
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 },
});
}
// 通用上传
export const uploadFile = (params, success) => {
return defHttp.uploadFile({ url: uploadUrl }, params, { success });
};
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.jpghttp 开头),则直接使用JImageUpload 和 JUpload 都通过 getHeaders() 函数自动附加 Token:
import { getHeaders } from '/@/utils/common/compUtils';
const headers = getHeaders();
getHeaders() 内部调用 getToken() 获取当前用户的 JWT Token,添加到请求头中。
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 配置)
当业务需要使用自定义上传接口(非 /sys/common/upload)时,需要注意以下限制:
| 组件 | 是否支持自定义上传地址 | 限制说明 |
|---|---|---|
JImageUpload |
✅ 支持(uploadUrl prop) |
但内部使用 getFileAccessHttpUrl 显示图片,该函数会自动添加 /sys/common/static/ 前缀,与自定义路径不兼容 |
JUpload |
❌ 不支持 | 没有 uploadUrl prop,上传地址硬编码为系统默认地址 |
a-upload |
✅ 支持(action prop) |
完全自定义,需手动处理上传响应和 URL 构造 |
解决方案:在自定义上传场景中,使用 a-upload 直接控制,手动处理:
action 属性指定自定义上传地址headers 属性传入认证 Token(使用 getHeaders())@change 回调中从 response.message 提取相对路径getSpbbFileUrl() 函数构造文件访问 URL(而非使用 getFileAccessHttpUrl)| 组件/文件 | 路径 |
|---|---|
| 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
本项目前端存在 三大表单校验体系,分别服务于不同的业务场景:
| 体系 | 核心组件 | 校验引擎 | 适用场景 |
|---|---|---|---|
| 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 两种引擎。
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 被重复调用| 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((_) => {});
}
| 方法 | 说明 | 来源 |
|---|---|---|
validate() |
校验整个表单,返回 Promise<表单值> | useForm |
validateFields(nameList) |
校验指定字段 | useForm |
clearValidate(name?) |
清除校验信息 | useForm |
scrollToField(name, options) |
滚动到校验失败字段 | useForm |
setFieldsValue(values) |
设置字段值后自动触发 validateFields |
useForm |
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)
}
登录/注册/忘记密码页面使用原生 <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 必填 |
在列配置中通过 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();
};
}
校验流程:
handleSubmit()
├── 单表 → validate() [BasicForm]
└── 主子表 → validateAll()
├── 第1步:validate() [主表 BasicForm]
├── 第2步:validateSubTableFields()
│ ├── 一对一子表 → instance.getAll() → validate() [BasicForm]
│ └── 一对多子表 → instance.fullValidateTable() [JVxeTable]
└── 第3步:合并所有数据
校验失败自动定位:
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));
});
}
| 规范项 | 要求 | 示例 |
|---|---|---|
| 规则定义位置 | 统一放在 xxx.data.ts 文件中 |
user.data.ts、role.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 | 原因 |
|---|---|---|
| Input / InputTextArea / InputNumber | 'blur' |
避免输入过程中频繁触发校验 |
| Select / Checkbox / Radio / Switch | 'change' |
选择即确定,可立即校验 |
| 唯一性校验 | 'blur' |
减少后端请求频率 |
| 格式校验(邮箱/手机/正则) | 'blur' |
用户完成输入后再校验 |
| 确认密码 | 不设置(默认) | 值变化时立即校验一致性 |
| 场景 | 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);
}
┌─────────────────────────────────────────────────────────────────┐
│ 表单校验执行流程 │
│ │
│ 用户输入/选择 │
│ ↓ │
│ 值变更事件 (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() 滚动到错误字段 │
└─────────────────────────────────────────────────────────────────┘
| 文件 | 作用 |
|---|---|
| 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 表单唯一校验 |
追加日期:2026-06-02
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> 模板中使用
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 组件。
toolbar — 工具栏区域
<template #toolbar>
<a-button type="primary" @click="getFormValues">获取表单数据</a-button>
</template>
源码实现:TableHeader 中 toolbar 插槽渲染在右侧 __toolbar 区域,后面紧跟 TableSetting 组件。当 toolbar 插槽和
showTableSetting 同时存在时,中间会自动添加竖线分隔符 Divider。
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。
tableTop — 表头下方区域(替代默认选中提示)<!-- 用法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 条记录"和"清空"链接。使用此插槽会*
*完全替换**默认提示。
alertAfter — 选中提示后方区域源码实现:alertAfter 插槽嵌套在 tableTop 默认内容的 <a-alert> 内部,仅在选中行数 > 0 时显示。项目暂未发现实际使用案例。
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 全局插槽。
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>
summary — 表尾合计区域{ pageData }(当前页数据)
<template #summary="{ pageData }">
<TableSummary fixed>
<TableSummaryRow>
<TableSummaryCell :index="0">合计</TableSummaryCell>
<TableSummaryCell :index="1">{{ totalAmount }}</TableSummaryCell>
</TableSummaryRow>
</TableSummary>
</template>
生效条件:此插槽仅在 showSummaryRef 为 true(即配置了 summaryFunc 或 summaryData 且有数据)且 showSummary prop
为 false 时生效。默认情况下使用内置 TableSummary 组件渲染。
headerCell — 表头单元格{ column, title }特殊处理:当列是自定义选择列时,会渲染 CustomSelectHeader 而非用户自定义内容(解决全选框不显示的问题)。
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>
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>
action — 操作列插槽(最常用的列级插槽)actionColumn 配置自动生成,内部设置 slots: { customRender: 'action' }{ record, index }
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
form-xxx — 搜索表单字段自定义插槽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 的对应插槽。
slotNamesGroup 计算属性会将非 bodyCell 且非列级自定义的插槽归入 native 组,直接透传给底层 <Table> 组件。因此 Ant
Design Vue Table 支持的所有原生插槽都可以直接使用,包括但不限于:
| 原生插槽 | 作用域参数 | 用途 |
|---|---|---|
headerCell |
{ column, title } |
自定义表头单元格 |
customFilterIcon |
{ filtered } |
自定义筛选图标 |
expandIcon |
{ expanded, onExpand, record } |
自定义展开图标 |
emptyText |
无 | 空数据提示 |
| 插槽名 | 类别 | 作用域参数 | 典型放置组件 | 使用频率 |
|---|---|---|---|---|
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原生插槽 | 原生透传 | 视具体插槽而定 | 视具体插槽而定 | 低 |
BasicTable 的插槽本质上是 Vue 的标准插槽机制,可以放置任何合法的 Vue 组件或 HTML 内容。项目中实际使用的组件包括:
| 组件 | 常用插槽 | 用途 |
|---|---|---|
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 | 导入进度组件 |
span, div, img, a 等基础标签v-html 渲染富文本内容只要是合法的 Vue 组件,都可以放入插槽中使用。
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' },
];
}
| 文件 | 作用 |
|---|---|
| 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 列插槽综合示例 |