经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » React » 查看文章
react 高效高质量搭建后台系统 系列 —— 表格的封装
来源:cnblogs  作者:彭加李  时间:2023/2/13 8:45:54  对本文有异议

其他章节请看:

react 高效高质量搭建后台系统 系列

表格

有一种页面在后台系统中比较常见:页面分上下两部分,上部分是 input、select、时间等查询项,下部分是查询项对应的表格数据。包含增删改查,例如点击新建进行新增操作。就像这样:

本篇将对 ant 的表格进行封装。效果如下:

spug 中 Table 封装的分析

入口

我们选择 spug 比较简单的模块(角色管理)进行分析。

进入角色管理模块入口,发现表格区封装到模块当前目录的 Table.js 中:

  1. // spug\src\pages\system\role\index.js
  2. import ComTable from './Table';
  3. export default observer(function () {
  4. return (
  5. <AuthDiv auth="system.role.view">
  6. <Breadcrumb>
  7. <Breadcrumb.Item>首页</Breadcrumb.Item>
  8. <Breadcrumb.Item>系统管理</Breadcrumb.Item>
  9. <Breadcrumb.Item>角色管理</Breadcrumb.Item>
  10. </Breadcrumb>
  11. {/* 查询区域 */}
  12. <SearchForm>
  13. <SearchForm.Item span={8} title="角色名称">
  14. <Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
  15. </SearchForm.Item>
  16. </SearchForm>
  17. {/* 将表格区域封装到了 Table.js 中 */}
  18. <ComTable/>
  19. </AuthDiv>
  20. );
  21. })

查阅 Table.js 发现表格使用的是 components 中的 TableCard

  1. // spug\src\pages\system\role\Table.js
  2. import { TableCard, ... } from 'components';
  3. @observer
  4. class ComTable extends React.Component {
  5. ...
  6. render() {
  7. return (
  8. <TableCard
  9. rowKey="id"
  10. title="角色列表"
  11. loading={store.isFetching}
  12. dataSource={store.dataSource}
  13. onReload={store.fetchRecords}
  14. actions={[
  15. <AuthButton type="primary" icon={<PlusOutlined/>} onClick={() => store.showForm()}>新建</AuthButton>
  16. ]}
  17. pagination={{
  18. showSizeChanger: true,
  19. showLessItems: true,
  20. showTotal: total => `共 ${total} 条`,
  21. pageSizeOptions: ['10', '20', '50', '100']
  22. }}
  23. columns={this.columns}/>
  24. )
  25. }
  26. }
  27. export default ComTable

进一步跟进不难发现 TableCard.js 就是 spug 中 封装好的 Table 组件。

Tip: vscode 搜索 TableCard, 发现有 17 处,推测至少有 16 个模块使用的这个封装好的 Table 组件

表格封装的组件

下面我们来分析spug 中表格分装组件:TableCard。

TableCard 从界面上分三部分:头部表格主体(包含分页器)、Footer。请看代码:

  1. // spug\src\components\TableCard.js
  2. return (
  3. <div ref={rootRef} className={styles.tableCard} style={{ ...props.customStyles }}>
  4. {/* 头部。例如表格标题 */}
  5. <Header
  6. title={props.title}
  7. columns={columns}
  8. actions={props.actions}
  9. fields={fields}
  10. rootRef={rootRef}
  11. defaultFields={defaultFields}
  12. onFieldsChange={handleFieldsChange}
  13. onReload={props.onReload} />
  14. {/* 表格主体,包含分页。如果没数据分页器页不会显示 */}
  15. <Table
  16. tableLayout={props.tableLayout}
  17. scroll={props.scroll}
  18. rowKey={props.rowKey}
  19. loading={props.loading}
  20. columns={columns.filter((_, index) => fields.includes(index))}
  21. dataSource={props.dataSource}
  22. rowSelection={props.rowSelection}
  23. expandable={props.expandable}
  24. size={props.size}
  25. onChange={props.onChange}
  26. // 分页器
  27. pagination={props.pagination} />
  28. {/* Footer 根据props.selected 来显示,里面显示`选择了几项...` */}
  29. {selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
  30. </div>
  31. )

头部

头部分三部分,左侧是表格的标题,中间是是一些操作,例如新增、批量删除等,右侧是表格的操作。如下图所示:

右侧表格操作也有三部分:刷新表格、列展示、表格全屏。

Tip:表格刷新很简单,就是调用父组件的 reload 重新发请求。

全屏

表格全屏也很简单,利用的是浏览器原生支持的功能。

  1. // 全屏操作。使用浏览器自带全屏功能
  2. function handleFullscreen() {
  3. // props.rootRef.current 是表格组件的原始 Element
  4. // fullscreenEnabled 属性提供了启用全屏模式的可能性。当它的值是 false 的时候,表示全屏模式不可用(可能的原因有 "fullscreen" 特性不被允许,或全屏模式不被支持等)。
  5. if (props.rootRef.current && document.fullscreenEnabled) {
  6. // 如果处在全屏。
  7. // fullscreenElement 返回当前文档中正在以全屏模式显示的Element节点,如果没有使用全屏模式,则返回null.
  8. if (document.fullscreenElement) {
  9. document.exitFullscreen()
  10. } else {
  11. props.rootRef.current.requestFullscreen()
  12. }
  13. }
  14. }

列展示

比如取消描述信息,表格中将不会显示该列。效果如下图所示:

这个过程不会发送请求。

整个逻辑如下:

  • 父组件会给 <Header> 组件传入 columns、fields、onFieldsChange、defaultFields等属性方法。
  1. <Header
  2. title={props.title}
  3. columns={columns}
  4. actions={props.actions}
  5. fields={fields}
  6. rootRef={rootRef}
  7. defaultFields={defaultFields}
  8. onFieldsChange={handleFieldsChange}
  9. onReload={props.onReload} />
  • 绿框的 checkbox 由传入的 columns 决定
  • 列展示由传入的 columns 和 fields 决定,当选中的个数(fields)等于 columns 的个数,则全选
  • 重置主要针对 fields,页面一进来就会取到默认选中字段。

表格主体

表格主体就是调用 antd 中的 Table 组件:

: antd 中的 Table 有许多属性,这里只对外暴露有限个 antd 表格属性,这种做法不是很好。

  1. <Table
  2. // 表格元素的 table-layout 属性,例如可以实现`固定表头/列`
  3. tableLayout={props.tableLayout}
  4. // 表格是否可滚动
  5. scroll={props.scroll}
  6. // 表格行 key 的取值,可以是字符串或一个函数。spug 中 `rowKey="id"` 重现出现在 29 个文件中。
  7. rowKey={props.rowKey}
  8. // 加载中的 loading 效果
  9. loading={props.loading}
  10. // 表格的列。用户可以选择哪些列不显示
  11. columns={columns.filter((_, index) => fields.includes(index))}
  12. // 数据源
  13. dataSource={props.dataSource}
  14. // 表格行是否可选择,配置项(object)。可以不传
  15. rowSelection={props.rowSelection}
  16. // 展开功能的配置。可以不传
  17. expandable={props.expandable}
  18. // 表格大小 default | middle | small
  19. size={props.size}
  20. // 分页、排序、筛选变化时触发
  21. onChange={props.onChange}
  22. // 分页器,参考配置项或 pagination 文档,设为 false 时不展示和进行分页
  23. pagination={props.pagination} />

尾部

根据父组件的 selected 决定是否显示 Footer:

  1. {/* selected 来自 props,在 Footer 组件中显示选中了多少项等信息,spug 中没有使用到 */}
  2. {selected.length ? <Footer selected={selected} actions={batchActions} /> : null}

Footer 主要显示已选择...,spug 中出现得很少:

  1. function Footer(props) {
  2. const actions = props.actions || [];
  3. const length = props.selected.length;
  4. return length > 0 ? (
  5. <div className={styles.tableFooter}>
  6. <div className={styles.left}>已选择 <span>{length}</span> 项</div>
  7. <Space size="middle">
  8. {actions.map((item, index) => (
  9. <React.Fragment key={index}>{item}</React.Fragment>
  10. ))}
  11. </Space>
  12. </div>
  13. ) : null
  14. }

TableCard.js

spug 中表格封装的完整代码如下:

  1. // spug\src\components\TableCard.js
  2. import React, { useState, useEffect, useRef } from 'react';
  3. import { Table, Space, Divider, Popover, Checkbox, Button, Input, Select } from 'antd';
  4. import { ReloadOutlined, SettingOutlined, FullscreenOutlined, SearchOutlined } from '@ant-design/icons';
  5. import styles from './index.module.less';
  6. // 从缓存中取得之前设置的列。记录要隐藏的字段。比如之前将 `状态` 这列隐藏
  7. let TableFields = localStorage.getItem('TableFields')
  8. TableFields = TableFields ? JSON.parse(TableFields) : {}
  9. function Search(props) {
  10. // ...
  11. }
  12. // 已选择多少项。
  13. function Footer(props) {
  14. const actions = props.actions || [];
  15. const length = props.selected.length;
  16. return length > 0 ? (
  17. <div className={styles.tableFooter}>
  18. <div className={styles.left}>已选择 <span>{length}</span> 项</div>
  19. <Space size="middle">
  20. {actions.map((item, index) => (
  21. <React.Fragment key={index}>{item}</React.Fragment>
  22. ))}
  23. </Space>
  24. </div>
  25. ) : null
  26. }
  27. function Header(props) {
  28. // 表格所有的列
  29. const columns = props.columns || [];
  30. // 例如创建、批量删除等操作
  31. const actions = props.actions || [];
  32. // 选中列,也就是表格要显示的列
  33. const fields = props.fields || [];
  34. // 取消或选中某列时触发
  35. const onFieldsChange = props.onFieldsChange;
  36. // 列展示组件
  37. const Fields = () => {
  38. return (
  39. // value - 指定选中的选项 string[]
  40. // onChange- 变化时的回调函数 function(checkedValue)。
  41. // 例如取消`状态`这列的选中
  42. <Checkbox.Group value={fields} onChange={onFieldsChange}>
  43. {/* 展示所有的列 */}
  44. {columns.map((item, index) => (
  45. // 注:值的选中是根据索引来的,因为 columns 是数组,是有顺序的。
  46. <Checkbox value={index} key={index}>{item.title}</Checkbox>
  47. ))}
  48. </Checkbox.Group>
  49. )
  50. }
  51. // 列展示 - 全选或取消全部
  52. function handleCheckAll(e) {
  53. if (e.target.checked) {
  54. // 例如:[0, 1, 2, 3]
  55. // console.log('columns', columns.map((_, index) => index))
  56. onFieldsChange(columns.map((_, index) => index))
  57. } else {
  58. onFieldsChange([])
  59. }
  60. }
  61. // 全屏操作。使用浏览器自带全屏功能
  62. function handleFullscreen() {
  63. // props.rootRef.current 是表格组件的原始 Element
  64. // fullscreenEnabled 属性提供了启用全屏模式的可能性。当它的值是 false 的时候,表示全屏模式不可用(可能的原因有 "fullscreen" 特性不被允许,或全屏模式不被支持等)。
  65. if (props.rootRef.current && document.fullscreenEnabled) {
  66. // 如果处在全屏。
  67. // fullscreenElement 返回当前文档中正在以全屏模式显示的Element节点,如果没有使用全屏模式,则返回null.
  68. if (document.fullscreenElement) {
  69. // console.log('退出全屏')
  70. document.exitFullscreen()
  71. } else {
  72. // console.log('全屏该元素')
  73. props.rootRef.current.requestFullscreen()
  74. }
  75. }
  76. }
  77. // 头部分左右两部分:表格标题 和 options。options 又分两部分:操作项(例如新建、批量删除)、表格操作(刷新表格、表格列显隐控制、表格全屏控制)
  78. return (
  79. <div className={styles.toolbar}>
  80. <div className={styles.title}>{props.title}</div>
  81. <div className={styles.option}>
  82. {/* 新建、删除等项 */}
  83. <Space size="middle" style={{ marginRight: 10 }}>
  84. {actions.map((item, index) => (
  85. // 这种用法有意思
  86. <React.Fragment key={index}>{item}</React.Fragment>
  87. ))}
  88. </Space>
  89. {/* 如果有新建等按钮就得加一个分隔符 | */}
  90. {actions.length ? <Divider type="vertical" /> : null}
  91. {/* 表格操作:刷新表格、表格列显隐控制、表格全屏控制 */}
  92. <Space className={styles.icons}>
  93. {/* 刷新表格 */}
  94. <ReloadOutlined onClick={props.onReload} />
  95. {/* 控制表格列的显示,比如让`状态`这列隐藏 */}
  96. <Popover
  97. arrowPointAtCenter
  98. destroyTooltipOnHide={{ keepParent: false }}
  99. // 头部:列展示、重置
  100. title={[
  101. <Checkbox
  102. key="1"
  103. // 全选状态。选中的列数 === 表格中定义的列数
  104. checked={fields.length === columns.length}
  105. // 在实现全选效果时,你可能会用到 indeterminate 属性。
  106. // 设置 indeterminate 状态,只负责样式控制
  107. indeterminate={![0, columns.length].includes(fields.length)}
  108. onChange={handleCheckAll}>列展示</Checkbox>,
  109. // 重置展示最初的列,也就是页面刚进来时列展示的状态。localStorage 会记录对表格列展示的状态。
  110. <Button
  111. key="2"
  112. type="link"
  113. style={{ padding: 0 }}
  114. onClick={() => onFieldsChange(props.defaultFields)}>重置</Button>
  115. ]}
  116. overlayClassName={styles.tableFields}
  117. // 触发方式是 click
  118. trigger="click"
  119. placement="bottomRight"
  120. // 卡片内容
  121. content={<Fields />}>
  122. <SettingOutlined />
  123. </Popover>
  124. {/* 表格全屏控制 */}
  125. <FullscreenOutlined onClick={handleFullscreen} />
  126. </Space>
  127. </div>
  128. </div>
  129. )
  130. }
  131. function TableCard(props) {
  132. // 定义一个 ref,用于表格的全屏控制
  133. const rootRef = useRef();
  134. // Footer 组件中使用
  135. const batchActions = props.batchActions || [];
  136. // Footer 组件中使用
  137. const selected = props.selected || [];
  138. // 记录要展示的列
  139. // 例如全选则是 [0, 1, 2, 3 ...],空数组表示不展示任何列
  140. const [fields, setFields] = useState([]);
  141. // 用于列展示中的重置功能。页面一进来就会将选中的列进行保存
  142. const [defaultFields, setDefaultFields] = useState([]);
  143. // 用于保存传入的表格的列数据
  144. const [columns, setColumns] = useState([]);
  145. useEffect(() => {
  146. // _columns - 传入的列数据
  147. let [_columns, _fields] = [props.columns, []];
  148. // `角色名称`这种功能 props.children 是空。
  149. if (props.children) {
  150. if (Array.isArray(props.children)) {
  151. _columns = props.children.filter(x => x.props).map(x => x.props)
  152. } else {
  153. _columns = [props.children.props]
  154. }
  155. }
  156. // 隐藏字段。有 hide 属性的是要隐藏的字段。如果有 tKey 字段,隐藏字段则以缓存的为准
  157. let hideFields = _columns.filter(x => x.hide).map(x => x.title)
  158. // tKey 是表格标识,比如这个表要隐藏 `状态` 字段,另一个表格要隐藏 `地址` 字段,与表格初始列展示对应。
  159. // 如果表格有唯一标识(tKey),再看TableFields(来自localStorage)中是否有数据,如果没有则更新缓存
  160. if (props.tKey) {
  161. if (TableFields[props.tKey]) {
  162. hideFields = TableFields[props.tKey]
  163. } else {
  164. TableFields[props.tKey] = hideFields
  165. localStorage.setItem('TableFields', JSON.stringify(TableFields))
  166. }
  167. }
  168. // Array.prototype.entries() 方法返回一个新的数组迭代器对象,该对象包含数组中每个索引的键/值对。
  169. for (let [index, item] of _columns.entries()) {
  170. // 比如之前将 `状态` 这列隐藏,输出:hideFields ['状态']
  171. // console.log('hideFields', hideFields)
  172. if (!hideFields.includes(item.title)) _fields.push(index)
  173. }
  174. //
  175. setFields(_fields);
  176. // 将传入的列数据保存在 state 中
  177. setColumns(_columns);
  178. // 记录初始展示的列
  179. setDefaultFields(_fields);
  180. // eslint-disable-next-line react-hooks/exhaustive-deps
  181. }, [])
  182. // 列展示的操作。
  183. function handleFieldsChange(fields) {
  184. // 更新选中的 fields
  185. setFields(fields)
  186. // tKey 就是一个标识,可以将未选中的fields存入 localStorage。比如用户取消了 `状态` 这列的展示,只要没有清空缓存,下次查看表格中仍旧不会显示`状态`这列
  187. // 将列展示状态保存到缓存
  188. if (props.tKey) {
  189. TableFields[props.tKey] = columns.filter((_, index) => !fields.includes(index)).map(x => x.title)
  190. localStorage.setItem('TableFields', JSON.stringify(TableFields))
  191. // 隐藏三列("频率","描述","操作"),输入: {"hi":["备注信息"],"cb":[],"cg":[],"cc":[],"sa":[],"mi":["频率","描述","操作"]}
  192. // console.log(localStorage.getItem('TableFields'))
  193. }
  194. }
  195. // 分为三部分:Header、Table和 Footer。
  196. return (
  197. <div ref={rootRef} className={styles.tableCard}>
  198. {/* 头部。 */}
  199. <Header
  200. // 表格标题。例如`角色列表`
  201. title={props.title}
  202. // 表格的列
  203. columns={columns}
  204. // 操作。例如新增、批量删除等操作
  205. actions={props.actions}
  206. // 不隐藏的列
  207. fields={fields}
  208. rootRef={rootRef}
  209. defaultFields={defaultFields}
  210. // 所选列变化时触发
  211. onFieldsChange={handleFieldsChange}
  212. onReload={props.onReload} />
  213. {/* antd 的 Table 组件 */}
  214. <Table
  215. // 表格元素的 table-layout 属性,例如可以实现`固定表头/列`
  216. tableLayout={props.tableLayout}
  217. // 表格是否可滚动
  218. scroll={props.scroll}
  219. // 表格行 key 的取值,可以是字符串或一个函数。spug 中 `rowKey="id"` 重现出现在 29 个文件中。
  220. rowKey={props.rowKey}
  221. // 加载中的 loading 效果
  222. loading={props.loading}
  223. // 表格的列。用户可以选择哪些列不显示
  224. columns={columns.filter((_, index) => fields.includes(index))}
  225. // 数据源
  226. dataSource={props.dataSource}
  227. // 表格行是否可选择,配置项(object)。可以不传
  228. rowSelection={props.rowSelection}
  229. // 展开功能的配置。可以不传
  230. expandable={props.expandable}
  231. // 表格大小 default | middle | small
  232. size={props.size}
  233. // 分页、排序、筛选变化时触发
  234. onChange={props.onChange}
  235. // 分页器,参考配置项或 pagination 文档,设为 false 时不展示和进行分页
  236. pagination={props.pagination} />
  237. {/* selected 来自 props,在 Footer 组件中显示选中了多少项等信息,spug 中没有使用到 */}
  238. {selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
  239. </div>
  240. )
  241. }
  242. // spug 没有用到
  243. TableCard.Search = Search;
  244. export default TableCard

myspug 中 Table 封装的实现

配置 mobx

笔者这里验证效果时需要使用状态管理器 mobx,目前项目会报如下 2 种错误:

  1. Support for the experimental syntax 'decorators' isn't currently enabled (10:1):
  1. src\pages\system\role\Table.js
  2. Line 10: Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (10:0)

这里需要两处修改即可:

  • config-overrides.js 中增加 addDecoratorsLegacy 的支持
  • 项目根目录新建 .babelrc 文件

Tip: 具体细节请看 这里

至此 mobx 仍有问题,经过一番折腾,最终才验证表格成功。

笔者在表格中使用一个变量(store.isFetching)控制 loading 效果,但页面一直显示加载效果。而加载完毕将 isFetching 置为 false 的语句也执行了,怀疑是 store.isFetching 变量没有同步到组件。折腾了一番...,最后将 mobx和 mobx-react 包版本改成和 spug 中相同:

  1. - "mobx": "^6.7.0",
  2. - "mobx-react": "^7.6.0",
  3. + "mobx": "^5.15.7",
  4. + "mobx-react": "^6.3.1",

期间无意发现我的组件加载完毕后输出两次

  1. componentDidMount(){
  2. // 执行2次
  3. console.log('hi')
  4. }

删除 <React.StrictMode>

效果

笔者在新建页面(角色管理)中验证封装的表格组件,效果如下:

代码

有关导航的配置,路由、mock数据、样式都无需讲解,这里主要说一下表格模块的封装(TableCard.js)和表格的使用(store.jsTable.js)。

TableCard.js

前面我们已经分析过了 spug 中表格的封装,这里与之类似,不在冗余。

  1. // myspug\src\components\TableCard.js
  2. import React, { useState, useEffect, useRef } from 'react';
  3. import { Table, Space, Divider, Popover, Checkbox, Button, Input, Select } from 'antd';
  4. import { ReloadOutlined, SettingOutlined, FullscreenOutlined, SearchOutlined } from '@ant-design/icons';
  5. import styles from './index.module.less';
  6. // 从缓存中取得之前设置的列。记录要隐藏的字段。比如之前将 `状态` 这列隐藏
  7. let TableFields = localStorage.getItem('TableFields')
  8. TableFields = TableFields ? JSON.parse(TableFields) : {}
  9. // 已选择多少项。
  10. function Footer(props) {
  11. const actions = props.actions || [];
  12. const length = props.selected.length;
  13. return length > 0 ? (
  14. <div className={styles.tableFooter}>
  15. <div className={styles.left}>已选择 <span>{length}</span> 项</div>
  16. <Space size="middle">
  17. {actions.map((item, index) => (
  18. <React.Fragment key={index}>{item}</React.Fragment>
  19. ))}
  20. </Space>
  21. </div>
  22. ) : null
  23. }
  24. function Header(props) {
  25. const columns = props.columns || [];
  26. const actions = props.actions || [];
  27. // 选中列,也就是表格要显示的列
  28. const fields = props.fields || [];
  29. const onFieldsChange = props.onFieldsChange;
  30. // 列展示组件
  31. const Fields = () => {
  32. return (
  33. // value - 指定选中的选项 string[]
  34. // onChange- 变化时的回调函数 function(checkedValue)。
  35. // 例如取消`状态`这列的选中
  36. <Checkbox.Group value={fields} onChange={onFieldsChange}>
  37. {/* 展示所有的列 */}
  38. {columns.map((item, index) => (
  39. // 注:值的选中是根据索引来的,因为 columns 是数组,是有顺序的。
  40. <Checkbox value={index} key={index}>{item.title}</Checkbox>
  41. ))}
  42. </Checkbox.Group>
  43. )
  44. }
  45. // 列展示 - 全选或取消全部
  46. function handleCheckAll(e) {
  47. if (e.target.checked) {
  48. // 例如:[0, 1, 2, 3]
  49. // console.log('columns', columns.map((_, index) => index))
  50. onFieldsChange(columns.map((_, index) => index))
  51. } else {
  52. onFieldsChange([])
  53. }
  54. }
  55. // 全屏操作。使用浏览器自带全屏功能
  56. function handleFullscreen() {
  57. // props.rootRef.current 是表格组件的原始 Element
  58. // fullscreenEnabled 属性提供了启用全屏模式的可能性。当它的值是 false 的时候,表示全屏模式不可用(可能的原因有 "fullscreen" 特性不被允许,或全屏模式不被支持等)。
  59. if (props.rootRef.current && document.fullscreenEnabled) {
  60. // 如果处在全屏。
  61. // fullscreenElement 返回当前文档中正在以全屏模式显示的Element节点,如果没有使用全屏模式,则返回null.
  62. if (document.fullscreenElement) {
  63. // console.log('退出全屏')
  64. document.exitFullscreen()
  65. } else {
  66. // console.log('全屏该元素')
  67. props.rootRef.current.requestFullscreen()
  68. }
  69. }
  70. }
  71. // 头部分左右两部分:表格标题 和 options。options 又分两部分:操作项(例如新建、批量删除)、表格操作(刷新表格、表格列显隐控制、表格全屏控制)
  72. return (
  73. <div className={styles.toolbar}>
  74. <div className={styles.title}>{props.title}</div>
  75. <div className={styles.option}>
  76. {/* 新建、删除等项 */}
  77. <Space size="middle" style={{ marginRight: 10 }}>
  78. {actions.map((item, index) => (
  79. // 这种用法有意思
  80. <React.Fragment key={index}>{item}</React.Fragment>
  81. ))}
  82. </Space>
  83. {/* 如果有新建等按钮就得加一个分隔符 | */}
  84. {actions.length ? <Divider type="vertical" /> : null}
  85. {/* 表格操作:刷新表格、表格列显隐控制、表格全屏控制 */}
  86. <Space className={styles.icons}>
  87. {/* 刷新表格 */}
  88. <ReloadOutlined onClick={props.onReload} />
  89. {/* 控制表格列的显示,比如让`状态`这列隐藏 */}
  90. <Popover
  91. arrowPointAtCenter
  92. destroyTooltipOnHide={{ keepParent: false }}
  93. // 头部:列展示、重置
  94. title={[
  95. <Checkbox
  96. key="1"
  97. // 全选状态。选中的列数 === 表格中定义的列数
  98. checked={fields.length === columns.length}
  99. // 在实现全选效果时,你可能会用到 indeterminate 属性。
  100. // 设置 indeterminate 状态,只负责样式控制
  101. indeterminate={![0, columns.length].includes(fields.length)}
  102. onChange={handleCheckAll}>列展示</Checkbox>,
  103. // 重置展示最初的列,也就是页面刚进来时列展示的状态。localStorage 会记录对表格列展示的状态。
  104. <Button
  105. key="2"
  106. type="link"
  107. style={{ padding: 0 }}
  108. onClick={() => onFieldsChange(props.defaultFields)}>重置</Button>
  109. ]}
  110. overlayClassName={styles.tableFields}
  111. // 触发方式是 click
  112. trigger="click"
  113. placement="bottomRight"
  114. // 卡片内容
  115. content={<Fields />}>
  116. <SettingOutlined />
  117. </Popover>
  118. {/* 表格全屏控制 */}
  119. <FullscreenOutlined onClick={handleFullscreen} />
  120. </Space>
  121. </div>
  122. </div>
  123. )
  124. }
  125. function TableCard(props) {
  126. // 定义一个 ref,用于表格的全屏控制
  127. const rootRef = useRef();
  128. // Footer 组件中使用
  129. const batchActions = props.batchActions || [];
  130. // Footer 组件中使用
  131. const selected = props.selected || [];
  132. // 记录要展示的列
  133. // 例如全选则是 [0, 1, 2, 3 ...],空数组表示不展示任何列
  134. const [fields, setFields] = useState([]);
  135. const [defaultFields, setDefaultFields] = useState([]);
  136. // 用于保存传入的表格的列数据
  137. const [columns, setColumns] = useState([]);
  138. useEffect(() => {
  139. // _columns - 传入的列数据
  140. let [_columns, _fields] = [props.columns, []];
  141. if (props.children) {
  142. if (Array.isArray(props.children)) {
  143. _columns = props.children.filter(x => x.props).map(x => x.props)
  144. } else {
  145. _columns = [props.children.props]
  146. }
  147. }
  148. // 隐藏字段。有 hide 属性的是要隐藏的字段。如果有 tKey 字段,隐藏字段则以缓存的为准
  149. let hideFields = _columns.filter(x => x.hide).map(x => x.title)
  150. // tKey 是表格标识,比如这个表要隐藏 `状态` 字段,另一个表格要隐藏 `地址` 字段,与表格初始列展示对应。
  151. // 如果表格有唯一标识(tKey),再看TableFields(来自localStorage)中是否有数据,如果没有则更新缓存
  152. if (props.tKey) {
  153. if (TableFields[props.tKey]) {
  154. hideFields = TableFields[props.tKey]
  155. } else {
  156. TableFields[props.tKey] = hideFields
  157. localStorage.setItem('TableFields', JSON.stringify(TableFields))
  158. }
  159. }
  160. // Array.prototype.entries() 方法返回一个新的数组迭代器对象,该对象包含数组中每个索引的键/值对。
  161. for (let [index, item] of _columns.entries()) {
  162. // 比如之前将 `状态` 这列隐藏,输出:hideFields ['状态']
  163. // console.log('hideFields', hideFields)
  164. if (!hideFields.includes(item.title)) _fields.push(index)
  165. }
  166. //
  167. setFields(_fields);
  168. // 将传入的列数据保存在 state 中
  169. setColumns(_columns);
  170. // 记录初始展示的列
  171. setDefaultFields(_fields);
  172. // eslint-disable-next-line react-hooks/exhaustive-deps
  173. }, [])
  174. // 列展示的操作。
  175. function handleFieldsChange(fields) {
  176. // 更新选中的 fields
  177. setFields(fields)
  178. // tKey 就是一个标识,可以将未选中的fields存入 localStorage。比如用户取消了 `状态` 这列的展示,只要没有清空缓存,下次查看表格中仍旧不会显示`状态`这列
  179. // 将列展示状态保存到缓存
  180. if (props.tKey) {
  181. TableFields[props.tKey] = columns.filter((_, index) => !fields.includes(index)).map(x => x.title)
  182. localStorage.setItem('TableFields', JSON.stringify(TableFields))
  183. // 隐藏三列("频率","描述","操作"),输入: {"hi":["备注信息"],"cb":[],"cg":[],"cc":[],"sa":[],"mi":["频率","描述","操作"]}
  184. // console.log(localStorage.getItem('TableFields'))
  185. }
  186. }
  187. // 分为三部分:Header、Table和 Footer。
  188. return (
  189. <div ref={rootRef} className={styles.tableCard}>
  190. {/* 头部。 */}
  191. <Header
  192. // 表格标题。例如`角色列表`
  193. title={props.title}
  194. // 表格的列
  195. columns={columns}
  196. // 操作。例如新增、批量删除等操作
  197. actions={props.actions}
  198. // 不隐藏的列
  199. fields={fields}
  200. rootRef={rootRef}
  201. defaultFields={defaultFields}
  202. // 所选列变化时触发
  203. onFieldsChange={handleFieldsChange}
  204. onReload={props.onReload} />
  205. {/* antd 的 Table 组件 */}
  206. <Table
  207. // 表格元素的 table-layout 属性,例如可以实现`固定表头/列`
  208. tableLayout={props.tableLayout}
  209. // 表格是否可滚动
  210. scroll={props.scroll}
  211. // 表格行 key 的取值,可以是字符串或一个函数。spug 中 `rowKey="id"` 重现出现在 29 个文件中。
  212. rowKey={props.rowKey}
  213. // 加载中的 loading 效果
  214. loading={props.loading}
  215. // 表格的列。用户可以选择哪些列不显示
  216. columns={columns.filter((_, index) => fields.includes(index))}
  217. // 数据源
  218. dataSource={props.dataSource}
  219. // 表格行是否可选择,配置项(object)。可以不传
  220. rowSelection={props.rowSelection}
  221. // 展开功能的配置。可以不传
  222. expandable={props.expandable}
  223. // 表格大小 default | middle | small
  224. size={props.size}
  225. // 分页、排序、筛选变化时触发
  226. onChange={props.onChange}
  227. // 分页器,参考配置项或 pagination 文档,设为 false 时不展示和进行分页
  228. pagination={props.pagination} />
  229. {/* selected 来自 props,在 Footer 组件中显示选中了多少项等信息,spug 中没有使用到 */}
  230. {selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
  231. </div>
  232. )
  233. }
  234. // spug 没有用到,我们也删除
  235. // TableCard.Search = Search;
  236. export default TableCard
Table.js

这里是表格的使用,与 antd Table 类似,主要是 columns(列) 和 dataSource(数据源):

  1. // myspug\src\pages\system\role\Table.js
  2. import React from 'react';
  3. import { observer } from 'mobx-react';
  4. import { Modal, Popover, Button, message } from 'antd';
  5. // PlusOutlined:antd 2.2.8 找到
  6. import { PlusOutlined } from '@ant-design/icons';
  7. import { TableCard, } from '@/components';
  8. import store from './store';
  9. @observer
  10. class ComTable extends React.Component {
  11. componentDidMount() {
  12. store.fetchRecords()
  13. }
  14. columns = [{
  15. title: '角色名称',
  16. dataIndex: 'name',
  17. }, {
  18. title: '关联账户',
  19. render: info => 0
  20. }, {
  21. title: '描述信息',
  22. dataIndex: 'desc',
  23. ellipsis: true
  24. }, {
  25. title: '操作',
  26. width: 400,
  27. render: info => (
  28. '编辑按钮'
  29. )
  30. }];
  31. render() {
  32. return (
  33. <TableCard
  34. rowKey="id"
  35. title="角色列表"
  36. loading={store.isFetching}
  37. dataSource={store.dataSource}
  38. // 刷新表格
  39. onReload={store.fetchRecords}
  40. actions={[
  41. <Button type="primary" icon={<PlusOutlined />}>新增</Button>
  42. ]}
  43. pagination={{
  44. showSizeChanger: true,
  45. showLessItems: true,
  46. showTotal: total => `共 ${total} 条`,
  47. pageSizeOptions: ['10', '20', '50', '100']
  48. }}
  49. columns={this.columns} />
  50. )
  51. }
  52. }
  53. export default ComTable
store.js

状态管理。例如表格的数据的请求,控制表格 loading 效果的 isFetching:

  1. // myspug\src\pages\system\role\store.js
  2. import { observable, computed, } from 'mobx';
  3. import http from '@/libs/http';
  4. class Store {
  5. @observable records = [];
  6. @observable isFetching = false;
  7. @computed get dataSource() {
  8. let records = this.records;
  9. return records
  10. }
  11. fetchRecords = () => {
  12. // 加载中
  13. this.isFetching = true;
  14. http.get('/api/account/role/')
  15. .then(res => {
  16. this.records = res
  17. })
  18. .finally(() => this.isFetching = false)
  19. };
  20. }
  21. export default new Store()

分页请求数据

spug 中的表格数据是一次性加载出来的,点击上下翻页不会发请求给后端。配合表格上方的过滤条件,体验不错,因为无需请求,数据都在前端。就像这样:

但是如果数据量很大,按照常规做法,翻页、查询等操作都需要从后端重新请求数据。

要实现表格翻页时重新请求数据也很简单,使用 antd Table 的 onChange 属性(分页、排序、筛选变化时触发)即可。

前面我们已经在 TableCard.js 中增加了该属性(即onChange={props.onChange}

下面我们将角色管理页面的表格改为分页请求数据:

首先我们回顾下目前这种一次请求表格所有数据,纯前端分页效果。请看代码:

  1. render() {
  2. return (
  3. <TableCard
  4. rowKey="id"
  5. title="角色列表"
  6. loading={store.isFetching}
  7. // 后端的数据源
  8. dataSource={store.dataSource}
  9. onReload={store.fetchRecords}
  10. actions={[
  11. <Button type="primary" icon={<PlusOutlined />}>新增</Button>
  12. ]}
  13. // 分页器
  14. pagination={{
  15. showSizeChanger: true,
  16. showLessItems: true,
  17. showTotal: total => `共 ${total} 条`,
  18. pageSizeOptions: ['10', '20', '50', '100']
  19. }}
  20. columns={this.columns} />
  21. )
  22. }

只需要给表格传入数据源(dataSource),antd Table 自动完成前端分页效果。

接着我们修改代码如下:

  • Table.js - 给表格增加了 onChange 对应的回调以及给分页器增加 total 属性
  • store.js - 定义新状态 current、total
  1. // myspug\src\pages\system\role\Table.js
  2. ...
  3. import { TableCard, } from '@/components';
  4. import store from './store';
  5. @observer
  6. class ComTable extends React.Component {
  7. componentDidMount() {
  8. store.fetchRecords()
  9. }
  10. columns = [...];
  11. handleTableChange = ({current}, filters, sorter) => {
  12. store.current = current
  13. store.tableOptions = {
  14. // 排序:好像只支持单个排序
  15. sortField: sorter.field,
  16. sortOrder: sorter.order,
  17. ...filters
  18. }
  19. store.fetchRecords();
  20. };
  21. render() {
  22. return (
  23. <TableCard
  24. rowKey="id"
  25. title="角色列表"
  26. loading={store.isFetching}
  27. // 后端的数据源
  28. dataSource={store.dataSource}
  29. onReload={store.fetchRecords}
  30. onChange={this.handleTableChange}
  31. // 分页器
  32. pagination={{
  33. showSizeChanger: true,
  34. showLessItems: true,
  35. showTotal: total => `共 ${total} 条`,
  36. pageSizeOptions: ['10', '20', '50', '100'],
  37. // 如果不传 total,则以后端返回数据条数作为 total 的值
  38. total: store.total,
  39. // 如果不传,则默认是第一条,如果需要默认显示第3条,则必须传
  40. current: store.current,
  41. }}
  42. columns={this.columns} />
  43. )
  44. }
  45. }
  46. export default ComTable
  1. // myspug\src\pages\system\role\store.js
  2. class Store {
  3. ...
  4. // 默认第1页
  5. @observable current = 1;
  6. // 总共多少页
  7. @observable total = '';
  8. // 其他参数,例如排序、过滤等等
  9. @observable tableOptions = {}
  10. fetchRecords = () => {
  11. const realParams = {current: this.current, ...this.tableOptions}
  12. this.isFetching = true;
  13. http.get('/api/account/role/', {params: realParams})
  14. .then(res => {
  15. // 可以这么赋值
  16. // ({data: this.records, total: this.pagination.total} = res)
  17. this.total = res.total
  18. this.records = res.data
  19. })
  20. .finally(() => this.isFetching = false)
  21. }
  22. }
  23. export default new Store()

最终效果如下图所示:

Tip:本地 mock 模拟数据如下

  1. const getNum = () => String(+new Date()).slice(-3)
  2. // 注:第三个参数必须不能是对象,否则 getNum 不会重新执行
  3. Mock.mock(/\/api\/account\/role\/.*/, 'get', function () {
  4. return {
  5. "data": {
  6. data: new Array(10).fill(0).map((item, index) => ({
  7. "id": index + getNum(), "name": 'name' + index + getNum(), "desc": null,
  8. })),
  9. total: 10000,
  10. }
  11. , "error": ""
  12. }
  13. })

扩展

create-react-app 组件为什么加载两次

试试删除 <React.StrictMode>(官网说:这仅适用于开发模式。生产模式下生命周期不会被调用两次)

疑惑:笔者验证表格时使用了 mobx,表格没渲染出来,删除 <React.StrictMode> 后表格正常,不知是否是 <React.StrictMode> 的副作用。

spug 中表格的不足

  • antd Table 组件某些属性无法使用:spug 中表格是对 antd Table 组件的封装,但是现在封装的组件对外的接口只提供了 antd Table 中有限的几个属性。例如上文提到的翻页请求后端数据需要使用 antd Table 中的 onChange 属性就没有提供出来

  • 头部一定会有:不需要都不行

其他章节请看:

react 高效高质量搭建后台系统 系列

原文链接:https://www.cnblogs.com/pengjiali/p/17111020.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号