- 后端
- 前端
- 认证鉴权
- 应用安全
【免费下载链接】Snowy
💖国内首个国密前后分离快速开发平台💖《免费商用》,基于开源技术栈精心打造,融合Vue3+AntDesignVue4+Vite5+SpringBoot3+Mp+HuTool+Sa-Token。平台内置国密加解密功能,保障前后端数据传输安全;全面支持国产化环境,适配多种机型、中间件及数据库。特别推荐:插件提供工作流、多租户、多数据源、即时通讯等高级插件,灵活接入,让您的项目开发如虎添翼。
STable 是 Snowy 前后端分离快速开发平台(snowy-admin-web)中对 Ant Design Vue Table 的重封装组件,其核心价值在于:页面开发者只需向组件传递一个返回Promise的数据加载函数,即可自动完成分页请求、loading 控制、数据渲染与分页器状态管理,彻底告别在每一个列表页重复编写分页逻辑。本文将以 Table 组件 README 为骨架,结合组件源码与真实业务页面,完整讲解其设计思想、内置属性/方法、插槽用法以及后端返回结构适配方案,读完即可在 Snowy 中快速落地一个标准列表页。
一、封装说明:一次封装,全项目受益
基础的使用方式与 API 与官方版 Ant Design Vue Table 基本一致,在其基础上封装了加载数据的方法。 你无需在用到表格的页面进行分页逻辑处理,仅需向 Table 组件传递绑定
:data="Promise"对象即可。
这就是 STable 设计的出发点:把"查数据、管分页、控加载"这三件高频重复的事收进组件内部。组件最初由 @Saraka 完成封装,后由小诺技术团队完成 Vue3 升级并二次封装改进,最终成为 Snowy 后台所有列表页的统一基础设施。
在 Snowy 中,STable 通过 snowy.js 全局注册,因此页面模板中无需显式 import,直接使用<s-table>标签即可:
// snowy-admin-web/src/snowy.js import STable from './components/Table/index.vue' // ... install(app) { // 注册常用组件 app.component('STable', STable) // ... }组件本体位于 src/components/Table/index.vue(共 700+ 行),通过defineExpose对外暴露refresh、clearSelected、clearRefreshSelected三个内置方法,配合 columnSetting.vue 实现列显隐与拖拽排序,构成了一个开箱即用的"表格工作台"。
二、基础使用:一个 Promise 驱动整张表
2.1 最小可用示例
原文档给出的第一个例子是标准用法(Options API 风格),完整复现如下:
<template> <s-table ref="tableRef" :rowKey="(record) => record.data.id" :columns="columns" :data="loadData" :rowSelection="{ selectedRowKeys: selectedRowKeys, onChange: onSelectChange }" > </s-table> </template> <script> import STable from '@/components' export default { components: { STable, }, data() { return { columns: [ { title: '规则编号', dataIndex: 'no', }, { title: '描述', dataIndex: 'description', }, { title: '服务调用次数', dataIndex: 'callNo', sorter: true, needTotal: true, customRender: text => `${text } 次`, }, { title: '状态', dataIndex: 'status', needTotal: true, }, { title: '更新时间', dataIndex: 'updatedAt', sorter: true, }, ], // 查询条件参数 queryParam: {}, // 加载数据方法 必须为 Promise 对象 loadData: (parameter) => { return this.$http.get('/service', { params: Object.assign(parameter, this.queryParam), }).then((res) => { return res.result }) }, selectedRowKeys: [], selectedRows: [], } }, methods: { onSelectChange (selectedRowKeys, selectedRows) { this.selectedRowKeys = selectedRowKeys this.selectedRows = selectedRows }, }, } </script>要点拆解:
:data="loadData"是唯一必须绑定的属性,它接收一个函数,函数入参parameter由组件内部自动注入(含current页码、size每页条数、sortField/sortOrder排序字段、以及列过滤filters),函数必须返回Promise;queryParam与parameter通过Object.assign合并,用于把页面搜索表单的条件带给后端,搜索按钮只需触发刷新即可带参重查;needTotal: true的列会被组件收集进needTotalList,在开启alert时展示选中行的数值合计;sorter: true的列可触发后端排序,组件会将sortField/sortOrder一并传给接口;rowSelection用于多选,selectedRowKeys与selectedRows由页面持有,勾选变化时通过onSelectChange回写。
2.2 在 Snowy 中的现代写法(<script setup>)
原文档例子 1 中的this.$http属于早期示例写法。在当前 Snowy 项目中,实际页面统一走 request.js 封装的 axios 实例与按模块划分的 API 文件。以 文件管理页 为例,其loadData如下:
// 表格查询 返回 Promise 对象 const loadData = (parameter) => { return fileApi.filePage(Object.assign(parameter, searchFormState.value)).then((data) => { return data }) }其中searchFormState.value就是页面搜索表单的状态对象,与组件注入的parameter合并后作为请求参数;fileApi.filePage返回的正是后端分页结果(含records、current、total字段),直接透传给 STable 即可。
三、操作列与自定义插槽:bodyCell 的灵活定制
原文档第二个例子展示了列表页最常见的形态——"表格 + 右上角操作按钮 + 每行操作列",采用<script setup>写法,完整复现如下:
<template> <s-table ref="tableRef" :columns="columns" :data="loadData" :alert="false" bordered :row-key="(record) => record.id"> <!-- #operator 插槽可以放入一些关于表格的操作,比如新增数据。 --> <template #operator> <a-space> <a-button type="primary" @click=""> <template #icon><plus-outlined /></template> 新增 </a-button> </a-space> </template> <!-- #bodyCell 放入column表格列需要显示的数据,可以通过判断进行一个自定义显示 --> <template #bodyCell="{ column, record }"> <template > <a-avatar class="xn-wh25" /> </template> <template v-if="column.dataIndex === 'status'"> <!-- 进行自定义显示内容 --> </template> <!-- column.dataIndex === 'action' 时,可以进行编辑删除等关于这行数据的一个操作,操作内容可进行自定义 --> <template v-if="column.dataIndex === 'action'"> <a @click="">编辑</a> </template> </template> </s-table> </template> <script setup> import STable from '@/components/table/' const tableRef = ref() //一定要进行一个表格的ref绑定 const columns = ref([ { title: '规则编号', dataIndex: 'no', }, { title: '描述', dataIndex: 'description', }, { title: '服务调用次数', dataIndex: 'callNo', }, { title: '状态', dataIndex: 'status', }, { title: '更新时间', dataIndex: 'updatedAt', }, { table: '操作', dataIndex: 'action', scopedSlots: { customRender: 'action' }, }, ]) const queryParam = ref({}) // 加载按钮数据 const loadData = (parameter) => { return this.$http.get('/service', { params: Object.assign(parameter, queryParam.value), }).then((res) => { return res.result }) } const edit = (row) => { // axios 发送数据到后端 修改数据成功后 // 调用 refresh() 重新加载列表数据 // 这里 setTimeout 模拟发起请求的网络延迟.. setTimeout(() => { tableRef.value.refresh() // refresh() 不传参默认值 false 不刷新到分页第一页 }, 1500) } </script>两个核心插槽说明:
#operator插槽:渲染在表格工具栏左侧(源码中对应 index.vue 的s-table-tool-left),用于放置"新增""批量删除"等列表级操作按钮。组件会智能判断插槽是否包含有效内容(过滤空白文本、注释与空 Fragment),没有内容时整个工具栏区域自动隐藏;#bodyCell="{ column, record }"插槽:Ant Design Vue 4 的通用单元格插槽,通过column.dataIndex判断当前列,可实现状态标签、缩略图、富文本渲染;dataIndex === 'action'时则放置该行的"编辑 / 删除 / 预览"等行内操作。
在 Snowy 的真实业务中,这一模式被大量复用。例如 字典管理页 在bodyCell中根据dictColor、level渲染彩色标签,并根据权限码hasPerm('bizDictEdit')动态决定是否渲染"编辑"入口;文件管理页 则在bodyCell中按文件后缀切换缩略图、渲染存储引擎标签,并组合"预览 / 详情 / 下载 / 删除"多个行操作。
四、内置方法:通过 ref 驱动的表格控制
原文档指出:通过声明的ref调用,即tableRef.value。三个对外暴露方法在 index.vue 的 defineExpose 中定义:
| 方法 | 说明 |
|---|---|
tableRef.value.refresh(bool) | 刷新列表。用户新增/修改/删除数据后重载列表数据;传true或任意真值时强制回到第一页再刷新(常用于搜索按钮),不传参默认false,停留在当前页刷新 |
tableRef.value.clearSelected() | 清空表格当前选中项(selectedRowKeys、selectedRows置空,并触发onChange([], [])通知父组件) |
tableRef.value.clearRefreshSelected(bool) | 先按参数执行refresh(bool),随后清空选中项,适合"批量操作完成后刷新并复位勾选"的场景 |
注意:要调用
refresh(bool)需要给表格组件设定ref值。
从源码看,refresh(true)的实现是先把localPagination.current重置为 1、清空选中,再重新执行loadData()与getTableProps()(index.vue#L430-L443),这正是"搜索从第一页开始分页"语义的由来。
在真实页面中,这套方法形成了固定搭配:
- 搜索按钮:
tableRef.value.refresh(true)(见 文件管理页); - 新增/编辑表单保存回调:
tableRef.value.refresh()保持当前页不动刷新(见 字典管理页 的formSuccessful); - 删除单行后:
tableRef.value.refresh()(见 文件管理页); - 批量删除后:
tableRef.value.clearRefreshSelected()刷新并复位勾选(见 文件管理页)。
五、内置属性:在 a-table 之上扩展的配置项
除a-table自带属性(columns、rowKey、bordered、scroll、rowSelection、expand-row-by-click等均可直接透传)外,STable 额外提供以下属性,完整定义见 index.vue#L167-L235:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
alert | 设置是否显示表格信息栏(选中统计条) | [object, boolean] | null |
showPagination | 显示分页选择器,可传'auto'| boolean | [string, boolean] | 'auto' |
data | 加载数据方法,必须为Promise对象,必须绑定 | Function | -(必填) |
lineSelection | 是否开启点击行高亮显示并选中 | Boolean | false |
pageNum | 初始页码 | Number | 1 |
size | 初始每页条数 | Number | 10 |
showSizeChanger | 是否显示每页条数切换器 | Boolean | true |
defaultPageSize | 默认每页条数 | Number | 10 |
pageSizeOptions | 每页条数可选项 | Array | ['10', '20', '50', '100'] |
compSize | 表格密度初始值(default/middle/small) | String | 'middle' |
toolConfig | 工具栏开关集合 | Object | { refresh: false, height: false, columnSetting: false, striped: false } |
extraTool | 扩展工具栏按钮 | Array | [] |
5.1 工具栏(toolConfig)与密度/列设置
toolConfig的四个开关分别控制右上角工具栏的「刷新」「密度」「列设置」「斑马纹」四个能力。从 源码 toolbar 计算逻辑 看,只要任一开关为true或存在operator插槽内容,工具栏就会渲染。Snowy 业务页面的典型配置:
// 字典管理页:开启刷新、密度、列设置,关闭斑马纹 const toolConfig = { refresh: true, height: true, columnSetting: true, striped: false }- 密度切换:内置「默认 / 中等 / 紧凑」三档,切换结果写入
data.customSize并透传给a-table的size属性; - 列设置:由 columnSetting.vue 实现,支持复选框显隐列、拖拽排序(基于
vuedraggable-es)、全选/重置,变更后通过columnChange事件回写列配置并即时生效。列定义中若提供checked字段可控制默认显隐,未提供则默认显示; - 斑马纹:通过
rowClassNameSwitch控制,为偶数行追加odd样式类(index.vue#L102-L104); - 列宽拖拽:组件还监听
resizeColumn事件(handleResize),配合a-table的resizable能力可手动调整列宽。
5.2 lineSelection:点击行即选中
lineSelection开启后(须同时配置rowSelection),点击表格行即可高亮并切换选中状态,而无需点击复选框(源码见 index.vue#L344-L403)。实现上通过customRow注入onClick:
- 若
rowSelection.type === 'radio',行点击直接替换选中项(单选语义); - 多选模式下,行点击按当前 key 切换选中/取消;
- 自动过滤按钮、链接、复选框等可交互元素上的点击,避免误触;
- 若
getCheckboxProps标记了某行禁用,则该行点击不生效。
5.3 alert 属性对象
alert可传布尔值或对象。传对象时结构如下(原文档定义):
alert: { show: Boolean, clear: [Function, Boolean] }show控制是否渲染信息栏;组件内部以props.alert整体是否为真来决定显示;clear用于自定义"清空"行为:当alert为布尔值true时,点击清空走组件内置的clearSelected();当为对象且clear是函数时,执行自定义清空逻辑(见 index.vue#L71-L86)。
信息栏位于表格上方(对应模板中的a-alert,见 index.vue#L56-L89),展示内容包含三部分:
已选择: N(rowSelection.selectedRowKeys的长度);needTotalList中每个needTotal: true列的合计值(支持customRender自定义展示格式);- 存在选中项时的"清空"链接。
选中统计的数值累加采用防浮点误差的addNumbers辅助函数处理(index.vue#L671-L691),避免0.1 + 0.2类的精度问题。
在 文件管理页 中,alert与rowSelection的搭配写法如下:
const options = { alert: { show: false, clear: () => { selectedRowKeys.value = [] } }, rowSelection: { onChange: (selectedRowKey, selectedRows) => { selectedRowKeys.value = selectedRowKey } } }六、数据契约:loadData 与后端返回结构对齐
这是接入 STable 最关键的一步。原文档提醒:你可能需要为了与后端提供的接口返回结果一致而去修改组件内部的取数逻辑——且这种修改是全局性的,整个项目所有使用该表格组件的页面都必须遵守同一套返回字段约定。
6.1 组件如何构造请求参数
从 loadData 源码 可见,组件每次渲染分页变化、排序、过滤时,都会自动组装请求参数:
const parameter = Object.assign( { current: (pagination && pagination.current) || (props.showPagination && data.localPagination.current) || props.pageNum, size: (pagination && pagination.pageSize) || (props.showPagination && data.localPagination.pageSize) || props.pageSize || data.localPagination.pageSize }, (sorter && sorter.field && { sortField: sorter.field }) || {}, (sorter && sorter.order && { sortOrder: sorter.order }) || {}, { ...filters } )即:分页参数固定为current(页码)与size(每页条数),排序参数为sortField/sortOrder,列过滤以filters原样透传。这与 Snowy 后端的通用分页请求 CommonPageRequest.java 完全对应——该后端工具类正是从请求参数中读取current与size(并将size上限钳制为 100)来构建 MyBatis-Plus 分页对象的:
private static final String PAGE_SIZE_PARAM_NAME = "size"; private static final String PAGE_PARAM_NAME = "current";6.2 组件如何消费返回结果
loadData拿到Promise后,在.then回调中(index.vue#L548-L597)对返回结果r做如下约定:
result.then((r) => { if (r == null) { data.localLoading = false return } // 获取分页数据及分页的显示内容 data.localPagination = (props.showPagination && Object.assign({}, data.localPagination, { current: r.current, // 返回结果中的当前分页数 total: r.total, // 返回结果中的总记录数 showSizeChanger: props.showSizeChanger, pageSizeOptions: props.pageSizeOptions, showTotal: (total, range) => { return `${range[0]}-${range[1]} 共 ${total} 条 ` }, pageSize: (pagination && pagination.pageSize) || data.localPagination.pageSize })) || false // 后端数据records为null保存修复 if (r.records == null) { r.records = [] } // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页 if (r.records.length === 0 && props.showPagination && data.localPagination.current > 1) { data.localPagination.current-- loadData() return } // 返回结果中的数组数据 data.localDataSource = r.records data.localLoading = false getTableProps() })由此可知后端分页结果必须包含三个关键字段:
| 返回字段 | 含义 | 组件消费位置 |
|---|---|---|
r.records | 当前页数据数组 | 赋值给localDataSource渲染表格 |
r.current | 当前页码 | 同步到分页器current |
r.total | 总记录数 | 同步到分页器total(用于计算总页数) |
此外组件还内置了两层健壮性兜底:
records为null时自动置为[],避免渲染报错;- 当前页数据为空且页码大于 1 时(典型场景:删除最后一页最后一条数据),自动回退到上一页并重新加载,防止出现"空页卡死"。
6.3 参考返回 JSON 结构
原文档给出了符合该约定的接口返回示例:
{ "message": "", "result": { "data": [ { "id": 1, "cover": "https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png", "title": "Alipay", "description": "那是一种内在的东西, 他们到达不了,也无法触及的", "status": 1, "updatedAt": "2018-07-26 00:00:00" } ], "pageSize": 10, "pageNo": 0, "totalPage": 6, "totalCount": 57 }, "status": 200, "timestamp": 1534955098193 }需要说明的是:上面是早期示例的返回格式,与当前组件的字段约定并不完全一致——当前组件读取的是r.records、r.current、r.total(对应 MyBatis-PlusPage序列化后的字段),而不是示例中的data/pageNo/totalCount。如果你接入的接口返回结构与默认约定不同,就需要在组件内部适配(详见下节)。
6.4 适配自定义返回结构的方法
原文档的提示是修改@/components/table/index.js第 348 行起的取数逻辑,并强调两点:
需要注意的是,这里的修改是全局性的,意味着整个项目所有使用该 table 组件都需要遵守这个返回结果定义的字段。 文档中的结构有可能由于组件 bug 进行修正而改动。实际修改请以当时最新版本为准。
在当前仓库中,该组件已是 Vue3 的index.vue形态,对应的取数逻辑位于 index.vue 的 loadData.then 回调(约第 548-597 行)。若后端返回结构不同,可在此处将字段映射为records/current/total后再赋值。更推荐的做法是在页面loadData函数内完成适配——只影响当前页面,不影响全局,例如:
const loadData = (parameter) => { return someApi.page(Object.assign(parameter, queryParam.value)).then((data) => { // 将自定义结构映射为组件约定的 records / current / total return { records: data.data, current: data.pageNo, total: data.totalCount } }) }七、源码机制补充:初始化与状态同步
理解组件内部机制,有助于排查列表页的疑难问题。几个关键点:
- 初始化(
init):组件onMounted时执行(index.vue#L720-L722),支持从路由参数route.params.current恢复页码(多标签页场景下保持分页状态),并初始化分页配置、needTotalList以及外部传入的rowSelection.selectedRowKeys / selectedRows; - 响应式同步:
pageNum、size、showSizeChanger、showPagination、rowSelection、columns均有watch监听(index.vue#L261-L319),外部动态改列、切分页配置、清空勾选都会即时同步到表格;columns的checked字段用于列设置组件的默认显隐控制; - Props 透传:组件通过
getTableProps(index.vue#L606-L643)把经过本地化处理后的属性合并进a-table,并将undefined/null的属性过滤掉,保证透传属性干净合法; - 选中状态提升:
updateSelect会把选中项同步回props.rowSelection(selectedRows/selectedRowKeys),并通过onSelectionChange事件通知父组件,使"翻页后选中状态不丢失、alert 统计即时更新"。
八、结语
STable 是 Snowy 后台列表开发的统一底座:data属性接收 Promise、自动接管分页与 loading、bodyCell插槽自定义单元格、operator插槽放置列表操作、refresh(true)驱动搜索、toolConfig一键开启密度/列设置/斑马纹,再配合后端current/size分页参数与records/current/total返回约定,构成了完整的前后端表格闭环。掌握本文内容后,你可以直接在 Snowy 任意模块中复刻 文件管理页 或 字典管理页 的成熟范式,把更多精力留给业务本身。
- 后端
- 前端
- 认证鉴权
- 应用安全
【免费下载链接】Snowy
💖国内首个国密前后分离快速开发平台💖《免费商用》,基于开源技术栈精心打造,融合Vue3+AntDesignVue4+Vite5+SpringBoot3+Mp+HuTool+Sa-Token。平台内置国密加解密功能,保障前后端数据传输安全;全面支持国产化环境,适配多种机型、中间件及数据库。特别推荐:插件提供工作流、多租户、多数据源、即时通讯等高级插件,灵活接入,让您的项目开发如虎添翼。
相关推荐
Snowy 前端组件 XnDataList:基于 Ant Design Vue 的垂直数据列表组件全解析
Snowy 前端组件 XnDataList:基于 Ant Design Vue 的垂直数据列表组件全解析 导读 XnDataList 是 Snowy 快速开发平
后端前端认证鉴权应用安全Gyroflow视频稳定实操指南:3类设备5分钟出稳定成片
Gyroflow视频稳定实操指南:3类设备5分钟出稳定成片 航拍素材到手,地平线歪了,画面抖得没法看。Gyroflow这款开源视频稳定工具,用相机内置的陀螺仪数
ant-design-vue-pro组件库二次封装:打造符合业务需求的组件体系
ant design vue pro组件库二次封装:打造符合业务需求的组件体系 在企业级前端开发中,直接使用基础组件库往往难以满足复杂的业务场景。本文将以ant
前端企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考