其他章節請看:
有一種頁面在後臺系統中比較常見:頁面分上下兩部分,上部分是 input、select、時間等查詢項,下部分是查詢項對應的表格資料。包含增刪改查
,例如點選新建
進行新增操作。就像這樣:
本篇將對 ant 的表格進行封裝。效果如下:
我們選擇 spug 比較簡單的模組(角色管理
)進行分析。
進入角色管理模組入口,發現表格區封裝到模組當前目錄的 Table.js
中:
// spug\src\pages\system\role\index.js
import ComTable from './Table';
export default observer(function () {
return (
<AuthDiv auth="system.role.view">
<Breadcrumb>
<Breadcrumb.Item>首頁</Breadcrumb.Item>
<Breadcrumb.Item>系統管理</Breadcrumb.Item>
<Breadcrumb.Item>角色管理</Breadcrumb.Item>
</Breadcrumb>
{/* 查詢區域 */}
<SearchForm>
<SearchForm.Item span={8} title="角色名稱">
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="請輸入"/>
</SearchForm.Item>
</SearchForm>
{/* 將表格區域封裝到了 Table.js 中 */}
<ComTable/>
</AuthDiv>
);
})
查閱 Table.js 發現表格使用的是 components 中的 TableCard
。
// spug\src\pages\system\role\Table.js
import { TableCard, ... } from 'components';
@observer
class ComTable extends React.Component {
...
render() {
return (
<TableCard
rowKey="id"
title="角色列表"
loading={store.isFetching}
dataSource={store.dataSource}
onReload={store.fetchRecords}
actions={[
<AuthButton type="primary" icon={<PlusOutlined/>} onClick={() => store.showForm()}>新建</AuthButton>
]}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `共 ${total} 條`,
pageSizeOptions: ['10', '20', '50', '100']
}}
columns={this.columns}/>
)
}
}
export default ComTable
進一步跟進不難發現 TableCard.js
就是 spug 中 封裝好的 Table 元件。
Tip: vscode 搜尋 TableCard,
發現有 17 處,推測至少有 16 個模組使用的這個封裝好的 Table 元件
下面我們來分析spug 中表格分裝元件:TableCard。
TableCard 從介面上分三部分:頭部
、表格主體
(包含分頁器)、Footer
。請看程式碼:
// spug\src\components\TableCard.js
return (
<div ref={rootRef} className={styles.tableCard} style={{ ...props.customStyles }}>
{/* 頭部。例如表格標題 */}
<Header
title={props.title}
columns={columns}
actions={props.actions}
fields={fields}
rootRef={rootRef}
defaultFields={defaultFields}
onFieldsChange={handleFieldsChange}
onReload={props.onReload} />
{/* 表格主體,包含分頁。如果沒資料分頁器頁不會顯示 */}
<Table
tableLayout={props.tableLayout}
scroll={props.scroll}
rowKey={props.rowKey}
loading={props.loading}
columns={columns.filter((_, index) => fields.includes(index))}
dataSource={props.dataSource}
rowSelection={props.rowSelection}
expandable={props.expandable}
size={props.size}
onChange={props.onChange}
// 分頁器
pagination={props.pagination} />
{/* Footer 根據props.selected 來顯示,裡面顯示`選擇了幾項...` */}
{selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
</div>
)
頭部分三部分,左側是表格的標題
,中間是是一些操作
,例如新增、批次刪除等,右側是表格的操作
。如下圖所示:
右側表格操作也有三部分:重新整理表格、列展示、表格全螢幕。
Tip:表格重新整理很簡單,就是呼叫父元件的 reload 重新發請求。
表格全螢幕也很簡單,利用的是瀏覽器原生支援的功能。
// 全螢幕操作。使用瀏覽器自帶全螢幕功能
function handleFullscreen() {
// props.rootRef.current 是表格元件的原始 Element
// fullscreenEnabled 屬性提供了啟用全螢幕模式的可能性。當它的值是 false 的時候,表示全螢幕模式不可用(可能的原因有 "fullscreen" 特性不被允許,或全螢幕模式不被支援等)。
if (props.rootRef.current && document.fullscreenEnabled) {
// 如果處在全螢幕。
// fullscreenElement 返回當前檔案中正在以全螢幕模式顯示的Element節點,如果沒有使用全螢幕模式,則返回null.
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
props.rootRef.current.requestFullscreen()
}
}
}
比如取消描述資訊
,表格中將不會顯示該列。效果如下圖所示:
這個過程不會傳送請求。
整個邏輯如下:
<Header>
元件傳入 columns、fields、onFieldsChange、defaultFields等屬性方法。<Header
title={props.title}
columns={columns}
actions={props.actions}
fields={fields}
rootRef={rootRef}
defaultFields={defaultFields}
onFieldsChange={handleFieldsChange}
onReload={props.onReload} />
綠框
的 checkbox 由傳入的 columns 決定列展示
由傳入的 columns 和 fields 決定,當選中的個數(fields)等於 columns 的個數,則全選重置
主要針對 fields,頁面一進來就會取到預設選中欄位。表格主體就是呼叫 antd 中的 Table 元件:
注: antd 中的 Table 有許多屬性,這裡只對外暴露有限個 antd 表格屬性,這種做法不是很好。
<Table
// 表格元素的 table-layout 屬性,例如可以實現`固定表頭/列`
tableLayout={props.tableLayout}
// 表格是否可捲動
scroll={props.scroll}
// 表格行 key 的取值,可以是字串或一個函數。spug 中 `rowKey="id"` 重現出現在 29 個檔案中。
rowKey={props.rowKey}
// 載入中的 loading 效果
loading={props.loading}
// 表格的列。使用者可以選擇哪些列不顯示
columns={columns.filter((_, index) => fields.includes(index))}
// 資料來源
dataSource={props.dataSource}
// 表格行是否可選擇,設定項(object)。可以不傳
rowSelection={props.rowSelection}
// 展開功能的設定。可以不傳
expandable={props.expandable}
// 表格大小 default | middle | small
size={props.size}
// 分頁、排序、篩選變化時觸發
onChange={props.onChange}
// 分頁器,參考設定項或 pagination 檔案,設為 false 時不展示和進行分頁
pagination={props.pagination} />
根據父元件的 selected 決定是否顯示 Footer:
{/* selected 來自 props,在 Footer 元件中顯示選中了多少項等資訊,spug 中沒有使用到 */}
{selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
Footer 主要顯示已選擇...
,spug 中出現得很少:
function Footer(props) {
const actions = props.actions || [];
const length = props.selected.length;
return length > 0 ? (
<div className={styles.tableFooter}>
<div className={styles.left}>已選擇 <span>{length}</span> 項</div>
<Space size="middle">
{actions.map((item, index) => (
<React.Fragment key={index}>{item}</React.Fragment>
))}
</Space>
</div>
) : null
}
spug 中表格封裝的完整程式碼如下:
// spug\src\components\TableCard.js
import React, { useState, useEffect, useRef } from 'react';
import { Table, Space, Divider, Popover, Checkbox, Button, Input, Select } from 'antd';
import { ReloadOutlined, SettingOutlined, FullscreenOutlined, SearchOutlined } from '@ant-design/icons';
import styles from './index.module.less';
// 從快取中取得之前設定的列。記錄要隱藏的欄位。比如之前將 `狀態` 這列隱藏
let TableFields = localStorage.getItem('TableFields')
TableFields = TableFields ? JSON.parse(TableFields) : {}
function Search(props) {
// ...
}
// 已選擇多少項。
function Footer(props) {
const actions = props.actions || [];
const length = props.selected.length;
return length > 0 ? (
<div className={styles.tableFooter}>
<div className={styles.left}>已選擇 <span>{length}</span> 項</div>
<Space size="middle">
{actions.map((item, index) => (
<React.Fragment key={index}>{item}</React.Fragment>
))}
</Space>
</div>
) : null
}
function Header(props) {
// 表格所有的列
const columns = props.columns || [];
// 例如建立、批次刪除等操作
const actions = props.actions || [];
// 選中列,也就是表格要顯示的列
const fields = props.fields || [];
// 取消或選中某列時觸發
const onFieldsChange = props.onFieldsChange;
// 列展示元件
const Fields = () => {
return (
// value - 指定選中的選項 string[]
// onChange- 變化時的回撥函數 function(checkedValue)。
// 例如取消`狀態`這列的選中
<Checkbox.Group value={fields} onChange={onFieldsChange}>
{/* 展示所有的列 */}
{columns.map((item, index) => (
// 注:值的選中是根據索引來的,因為 columns 是陣列,是有順序的。
<Checkbox value={index} key={index}>{item.title}</Checkbox>
))}
</Checkbox.Group>
)
}
// 列展示 - 全選或取消全部
function handleCheckAll(e) {
if (e.target.checked) {
// 例如:[0, 1, 2, 3]
// console.log('columns', columns.map((_, index) => index))
onFieldsChange(columns.map((_, index) => index))
} else {
onFieldsChange([])
}
}
// 全螢幕操作。使用瀏覽器自帶全螢幕功能
function handleFullscreen() {
// props.rootRef.current 是表格元件的原始 Element
// fullscreenEnabled 屬性提供了啟用全螢幕模式的可能性。當它的值是 false 的時候,表示全螢幕模式不可用(可能的原因有 "fullscreen" 特性不被允許,或全螢幕模式不被支援等)。
if (props.rootRef.current && document.fullscreenEnabled) {
// 如果處在全螢幕。
// fullscreenElement 返回當前檔案中正在以全螢幕模式顯示的Element節點,如果沒有使用全螢幕模式,則返回null.
if (document.fullscreenElement) {
// console.log('退出全螢幕')
document.exitFullscreen()
} else {
// console.log('全螢幕該元素')
props.rootRef.current.requestFullscreen()
}
}
}
// 頭部分左右兩部分:表格標題 和 options。options 又分兩部分:操作項(例如新建、批次刪除)、表格操作(重新整理表格、表格列顯隱控制、表格全螢幕控制)
return (
<div className={styles.toolbar}>
<div className={styles.title}>{props.title}</div>
<div className={styles.option}>
{/* 新建、刪除等項 */}
<Space size="middle" style={{ marginRight: 10 }}>
{actions.map((item, index) => (
// 這種用法有意思
<React.Fragment key={index}>{item}</React.Fragment>
))}
</Space>
{/* 如果有新建等按鈕就得加一個分隔符 | */}
{actions.length ? <Divider type="vertical" /> : null}
{/* 表格操作:重新整理表格、表格列顯隱控制、表格全螢幕控制 */}
<Space className={styles.icons}>
{/* 重新整理表格 */}
<ReloadOutlined onClick={props.onReload} />
{/* 控制表格列的顯示,比如讓`狀態`這列隱藏 */}
<Popover
arrowPointAtCenter
destroyTooltipOnHide={{ keepParent: false }}
// 頭部:列展示、重置
title={[
<Checkbox
key="1"
// 全選狀態。選中的列數 === 表格中定義的列數
checked={fields.length === columns.length}
// 在實現全選效果時,你可能會用到 indeterminate 屬性。
// 設定 indeterminate 狀態,只負責樣式控制
indeterminate={![0, columns.length].includes(fields.length)}
onChange={handleCheckAll}>列展示</Checkbox>,
// 重置展示最初的列,也就是頁面剛進來時列展示的狀態。localStorage 會記錄對錶格列展示的狀態。
<Button
key="2"
type="link"
style={{ padding: 0 }}
onClick={() => onFieldsChange(props.defaultFields)}>重置</Button>
]}
overlayClassName={styles.tableFields}
// 觸發方式是 click
trigger="click"
placement="bottomRight"
// 卡片內容
content={<Fields />}>
<SettingOutlined />
</Popover>
{/* 表格全螢幕控制 */}
<FullscreenOutlined onClick={handleFullscreen} />
</Space>
</div>
</div>
)
}
function TableCard(props) {
// 定義一個 ref,用於表格的全螢幕控制
const rootRef = useRef();
// Footer 元件中使用
const batchActions = props.batchActions || [];
// Footer 元件中使用
const selected = props.selected || [];
// 記錄要展示的列
// 例如全選則是 [0, 1, 2, 3 ...],空陣列表示不展示任何列
const [fields, setFields] = useState([]);
// 用於列展示中的重置功能。頁面一進來就會將選中的列進行儲存
const [defaultFields, setDefaultFields] = useState([]);
// 用於儲存傳入的表格的列資料
const [columns, setColumns] = useState([]);
useEffect(() => {
// _columns - 傳入的列資料
let [_columns, _fields] = [props.columns, []];
// `角色名稱`這種功能 props.children 是空。
if (props.children) {
if (Array.isArray(props.children)) {
_columns = props.children.filter(x => x.props).map(x => x.props)
} else {
_columns = [props.children.props]
}
}
// 隱藏欄位。有 hide 屬性的是要隱藏的欄位。如果有 tKey 欄位,隱藏欄位則以快取的為準
let hideFields = _columns.filter(x => x.hide).map(x => x.title)
// tKey 是表格標識,比如這個表要隱藏 `狀態` 欄位,另一個表格要隱藏 `地址` 欄位,與表格初始列展示對應。
// 如果表格有唯一標識(tKey),再看TableFields(來自localStorage)中是否有資料,如果沒有則更新快取
if (props.tKey) {
if (TableFields[props.tKey]) {
hideFields = TableFields[props.tKey]
} else {
TableFields[props.tKey] = hideFields
localStorage.setItem('TableFields', JSON.stringify(TableFields))
}
}
// Array.prototype.entries() 方法返回一個新的陣列迭代器物件,該物件包含陣列中每個索引的鍵/值對。
for (let [index, item] of _columns.entries()) {
// 比如之前將 `狀態` 這列隱藏,輸出:hideFields ['狀態']
// console.log('hideFields', hideFields)
if (!hideFields.includes(item.title)) _fields.push(index)
}
//
setFields(_fields);
// 將傳入的列資料儲存在 state 中
setColumns(_columns);
// 記錄初始展示的列
setDefaultFields(_fields);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 列展示的操作。
function handleFieldsChange(fields) {
// 更新選中的 fields
setFields(fields)
// tKey 就是一個標識,可以將未選中的fields存入 localStorage。比如使用者取消了 `狀態` 這列的展示,只要沒有清空快取,下次檢視表格中仍舊不會顯示`狀態`這列
// 將列展示狀態儲存到快取
if (props.tKey) {
TableFields[props.tKey] = columns.filter((_, index) => !fields.includes(index)).map(x => x.title)
localStorage.setItem('TableFields', JSON.stringify(TableFields))
// 隱藏三列("頻率","描述","操作"),輸入: {"hi":["備註資訊"],"cb":[],"cg":[],"cc":[],"sa":[],"mi":["頻率","描述","操作"]}
// console.log(localStorage.getItem('TableFields'))
}
}
// 分為三部分:Header、Table和 Footer。
return (
<div ref={rootRef} className={styles.tableCard}>
{/* 頭部。 */}
<Header
// 表格標題。例如`角色列表`
title={props.title}
// 表格的列
columns={columns}
// 操作。例如新增、批次刪除等操作
actions={props.actions}
// 不隱藏的列
fields={fields}
rootRef={rootRef}
defaultFields={defaultFields}
// 所選列變化時觸發
onFieldsChange={handleFieldsChange}
onReload={props.onReload} />
{/* antd 的 Table 元件 */}
<Table
// 表格元素的 table-layout 屬性,例如可以實現`固定表頭/列`
tableLayout={props.tableLayout}
// 表格是否可捲動
scroll={props.scroll}
// 表格行 key 的取值,可以是字串或一個函數。spug 中 `rowKey="id"` 重現出現在 29 個檔案中。
rowKey={props.rowKey}
// 載入中的 loading 效果
loading={props.loading}
// 表格的列。使用者可以選擇哪些列不顯示
columns={columns.filter((_, index) => fields.includes(index))}
// 資料來源
dataSource={props.dataSource}
// 表格行是否可選擇,設定項(object)。可以不傳
rowSelection={props.rowSelection}
// 展開功能的設定。可以不傳
expandable={props.expandable}
// 表格大小 default | middle | small
size={props.size}
// 分頁、排序、篩選變化時觸發
onChange={props.onChange}
// 分頁器,參考設定項或 pagination 檔案,設為 false 時不展示和進行分頁
pagination={props.pagination} />
{/* selected 來自 props,在 Footer 元件中顯示選中了多少項等資訊,spug 中沒有使用到 */}
{selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
</div>
)
}
// spug 沒有用到
TableCard.Search = Search;
export default TableCard
筆者這裡驗證效果時需要使用狀態管理器 mobx,目前專案會報如下 2 種錯誤:
Support for the experimental syntax 'decorators' isn't currently enabled (10:1):
src\pages\system\role\Table.js
Line 10: Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (10:0)
這裡需要兩處修改即可:
addDecoratorsLegacy
的支援.babelrc
檔案Tip: 具體細節請看 這裡。
至此 mobx 仍有問題,經過一番折騰,最終才驗證表格成功。
筆者在表格中使用一個變數(store.isFetching
)控制 loading 效果,但頁面一直顯示載入效果。而載入完畢將 isFetching 置為 false 的語句也執行了,懷疑是 store.isFetching 變數沒有同步到元件。折騰了一番...,最後將 mobx和 mobx-react 包版本改成和 spug 中相同:
- "mobx": "^6.7.0",
- "mobx-react": "^7.6.0",
+ "mobx": "^5.15.7",
+ "mobx-react": "^6.3.1",
期間無意發現我的元件載入完畢後輸出兩次
componentDidMount(){
// 執行2次
console.log('hi')
}
刪除 <React.StrictMode>
。
筆者在新建頁面(角色管理
)中驗證封裝的表格元件,效果如下:
有關導航的設定,路由、mock資料、樣式都無需講解,這裡主要說一下表格模組的封裝(TableCard.js
)和表格的使用(store.js
、Table.js
)。
前面我們已經分析過了 spug 中表格的封裝,這裡與之類似,不在冗餘。
// myspug\src\components\TableCard.js
import React, { useState, useEffect, useRef } from 'react';
import { Table, Space, Divider, Popover, Checkbox, Button, Input, Select } from 'antd';
import { ReloadOutlined, SettingOutlined, FullscreenOutlined, SearchOutlined } from '@ant-design/icons';
import styles from './index.module.less';
// 從快取中取得之前設定的列。記錄要隱藏的欄位。比如之前將 `狀態` 這列隱藏
let TableFields = localStorage.getItem('TableFields')
TableFields = TableFields ? JSON.parse(TableFields) : {}
// 已選擇多少項。
function Footer(props) {
const actions = props.actions || [];
const length = props.selected.length;
return length > 0 ? (
<div className={styles.tableFooter}>
<div className={styles.left}>已選擇 <span>{length}</span> 項</div>
<Space size="middle">
{actions.map((item, index) => (
<React.Fragment key={index}>{item}</React.Fragment>
))}
</Space>
</div>
) : null
}
function Header(props) {
const columns = props.columns || [];
const actions = props.actions || [];
// 選中列,也就是表格要顯示的列
const fields = props.fields || [];
const onFieldsChange = props.onFieldsChange;
// 列展示元件
const Fields = () => {
return (
// value - 指定選中的選項 string[]
// onChange- 變化時的回撥函數 function(checkedValue)。
// 例如取消`狀態`這列的選中
<Checkbox.Group value={fields} onChange={onFieldsChange}>
{/* 展示所有的列 */}
{columns.map((item, index) => (
// 注:值的選中是根據索引來的,因為 columns 是陣列,是有順序的。
<Checkbox value={index} key={index}>{item.title}</Checkbox>
))}
</Checkbox.Group>
)
}
// 列展示 - 全選或取消全部
function handleCheckAll(e) {
if (e.target.checked) {
// 例如:[0, 1, 2, 3]
// console.log('columns', columns.map((_, index) => index))
onFieldsChange(columns.map((_, index) => index))
} else {
onFieldsChange([])
}
}
// 全螢幕操作。使用瀏覽器自帶全螢幕功能
function handleFullscreen() {
// props.rootRef.current 是表格元件的原始 Element
// fullscreenEnabled 屬性提供了啟用全螢幕模式的可能性。當它的值是 false 的時候,表示全螢幕模式不可用(可能的原因有 "fullscreen" 特性不被允許,或全螢幕模式不被支援等)。
if (props.rootRef.current && document.fullscreenEnabled) {
// 如果處在全螢幕。
// fullscreenElement 返回當前檔案中正在以全螢幕模式顯示的Element節點,如果沒有使用全螢幕模式,則返回null.
if (document.fullscreenElement) {
// console.log('退出全螢幕')
document.exitFullscreen()
} else {
// console.log('全螢幕該元素')
props.rootRef.current.requestFullscreen()
}
}
}
// 頭部分左右兩部分:表格標題 和 options。options 又分兩部分:操作項(例如新建、批次刪除)、表格操作(重新整理表格、表格列顯隱控制、表格全螢幕控制)
return (
<div className={styles.toolbar}>
<div className={styles.title}>{props.title}</div>
<div className={styles.option}>
{/* 新建、刪除等項 */}
<Space size="middle" style={{ marginRight: 10 }}>
{actions.map((item, index) => (
// 這種用法有意思
<React.Fragment key={index}>{item}</React.Fragment>
))}
</Space>
{/* 如果有新建等按鈕就得加一個分隔符 | */}
{actions.length ? <Divider type="vertical" /> : null}
{/* 表格操作:重新整理表格、表格列顯隱控制、表格全螢幕控制 */}
<Space className={styles.icons}>
{/* 重新整理表格 */}
<ReloadOutlined onClick={props.onReload} />
{/* 控制表格列的顯示,比如讓`狀態`這列隱藏 */}
<Popover
arrowPointAtCenter
destroyTooltipOnHide={{ keepParent: false }}
// 頭部:列展示、重置
title={[
<Checkbox
key="1"
// 全選狀態。選中的列數 === 表格中定義的列數
checked={fields.length === columns.length}
// 在實現全選效果時,你可能會用到 indeterminate 屬性。
// 設定 indeterminate 狀態,只負責樣式控制
indeterminate={![0, columns.length].includes(fields.length)}
onChange={handleCheckAll}>列展示</Checkbox>,
// 重置展示最初的列,也就是頁面剛進來時列展示的狀態。localStorage 會記錄對錶格列展示的狀態。
<Button
key="2"
type="link"
style={{ padding: 0 }}
onClick={() => onFieldsChange(props.defaultFields)}>重置</Button>
]}
overlayClassName={styles.tableFields}
// 觸發方式是 click
trigger="click"
placement="bottomRight"
// 卡片內容
content={<Fields />}>
<SettingOutlined />
</Popover>
{/* 表格全螢幕控制 */}
<FullscreenOutlined onClick={handleFullscreen} />
</Space>
</div>
</div>
)
}
function TableCard(props) {
// 定義一個 ref,用於表格的全螢幕控制
const rootRef = useRef();
// Footer 元件中使用
const batchActions = props.batchActions || [];
// Footer 元件中使用
const selected = props.selected || [];
// 記錄要展示的列
// 例如全選則是 [0, 1, 2, 3 ...],空陣列表示不展示任何列
const [fields, setFields] = useState([]);
const [defaultFields, setDefaultFields] = useState([]);
// 用於儲存傳入的表格的列資料
const [columns, setColumns] = useState([]);
useEffect(() => {
// _columns - 傳入的列資料
let [_columns, _fields] = [props.columns, []];
if (props.children) {
if (Array.isArray(props.children)) {
_columns = props.children.filter(x => x.props).map(x => x.props)
} else {
_columns = [props.children.props]
}
}
// 隱藏欄位。有 hide 屬性的是要隱藏的欄位。如果有 tKey 欄位,隱藏欄位則以快取的為準
let hideFields = _columns.filter(x => x.hide).map(x => x.title)
// tKey 是表格標識,比如這個表要隱藏 `狀態` 欄位,另一個表格要隱藏 `地址` 欄位,與表格初始列展示對應。
// 如果表格有唯一標識(tKey),再看TableFields(來自localStorage)中是否有資料,如果沒有則更新快取
if (props.tKey) {
if (TableFields[props.tKey]) {
hideFields = TableFields[props.tKey]
} else {
TableFields[props.tKey] = hideFields
localStorage.setItem('TableFields', JSON.stringify(TableFields))
}
}
// Array.prototype.entries() 方法返回一個新的陣列迭代器物件,該物件包含陣列中每個索引的鍵/值對。
for (let [index, item] of _columns.entries()) {
// 比如之前將 `狀態` 這列隱藏,輸出:hideFields ['狀態']
// console.log('hideFields', hideFields)
if (!hideFields.includes(item.title)) _fields.push(index)
}
//
setFields(_fields);
// 將傳入的列資料儲存在 state 中
setColumns(_columns);
// 記錄初始展示的列
setDefaultFields(_fields);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 列展示的操作。
function handleFieldsChange(fields) {
// 更新選中的 fields
setFields(fields)
// tKey 就是一個標識,可以將未選中的fields存入 localStorage。比如使用者取消了 `狀態` 這列的展示,只要沒有清空快取,下次檢視表格中仍舊不會顯示`狀態`這列
// 將列展示狀態儲存到快取
if (props.tKey) {
TableFields[props.tKey] = columns.filter((_, index) => !fields.includes(index)).map(x => x.title)
localStorage.setItem('TableFields', JSON.stringify(TableFields))
// 隱藏三列("頻率","描述","操作"),輸入: {"hi":["備註資訊"],"cb":[],"cg":[],"cc":[],"sa":[],"mi":["頻率","描述","操作"]}
// console.log(localStorage.getItem('TableFields'))
}
}
// 分為三部分:Header、Table和 Footer。
return (
<div ref={rootRef} className={styles.tableCard}>
{/* 頭部。 */}
<Header
// 表格標題。例如`角色列表`
title={props.title}
// 表格的列
columns={columns}
// 操作。例如新增、批次刪除等操作
actions={props.actions}
// 不隱藏的列
fields={fields}
rootRef={rootRef}
defaultFields={defaultFields}
// 所選列變化時觸發
onFieldsChange={handleFieldsChange}
onReload={props.onReload} />
{/* antd 的 Table 元件 */}
<Table
// 表格元素的 table-layout 屬性,例如可以實現`固定表頭/列`
tableLayout={props.tableLayout}
// 表格是否可捲動
scroll={props.scroll}
// 表格行 key 的取值,可以是字串或一個函數。spug 中 `rowKey="id"` 重現出現在 29 個檔案中。
rowKey={props.rowKey}
// 載入中的 loading 效果
loading={props.loading}
// 表格的列。使用者可以選擇哪些列不顯示
columns={columns.filter((_, index) => fields.includes(index))}
// 資料來源
dataSource={props.dataSource}
// 表格行是否可選擇,設定項(object)。可以不傳
rowSelection={props.rowSelection}
// 展開功能的設定。可以不傳
expandable={props.expandable}
// 表格大小 default | middle | small
size={props.size}
// 分頁、排序、篩選變化時觸發
onChange={props.onChange}
// 分頁器,參考設定項或 pagination 檔案,設為 false 時不展示和進行分頁
pagination={props.pagination} />
{/* selected 來自 props,在 Footer 元件中顯示選中了多少項等資訊,spug 中沒有使用到 */}
{selected.length ? <Footer selected={selected} actions={batchActions} /> : null}
</div>
)
}
// spug 沒有用到,我們也刪除
// TableCard.Search = Search;
export default TableCard
這裡是表格的使用,與 antd Table 類似,主要是 columns(列) 和 dataSource(資料來源):
// myspug\src\pages\system\role\Table.js
import React from 'react';
import { observer } from 'mobx-react';
import { Modal, Popover, Button, message } from 'antd';
// PlusOutlined:antd 2.2.8 找到
import { PlusOutlined } from '@ant-design/icons';
import { TableCard, } from '@/components';
import store from './store';
@observer
class ComTable extends React.Component {
componentDidMount() {
store.fetchRecords()
}
columns = [{
title: '角色名稱',
dataIndex: 'name',
}, {
title: '關聯賬戶',
render: info => 0
}, {
title: '描述資訊',
dataIndex: 'desc',
ellipsis: true
}, {
title: '操作',
width: 400,
render: info => (
'編輯按鈕'
)
}];
render() {
return (
<TableCard
rowKey="id"
title="角色列表"
loading={store.isFetching}
dataSource={store.dataSource}
// 重新整理表格
onReload={store.fetchRecords}
actions={[
<Button type="primary" icon={<PlusOutlined />}>新增</Button>
]}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `共 ${total} 條`,
pageSizeOptions: ['10', '20', '50', '100']
}}
columns={this.columns} />
)
}
}
export default ComTable
狀態管理。例如表格的資料的請求,控制表格 loading 效果的 isFetching:
// myspug\src\pages\system\role\store.js
import { observable, computed, } from 'mobx';
import http from '@/libs/http';
class Store {
@observable records = [];
@observable isFetching = false;
@computed get dataSource() {
let records = this.records;
return records
}
fetchRecords = () => {
// 載入中
this.isFetching = true;
http.get('/api/account/role/')
.then(res => {
this.records = res
})
.finally(() => this.isFetching = false)
};
}
export default new Store()
spug 中的表格資料是一次性載入出來的,點選上下翻頁
不會發請求給後端。配合表格上方的過濾條件,體驗不錯,因為無需請求,資料都在前端。就像這樣:
但是如果資料量很大,按照常規做法,翻頁、查詢等操作都需要從後端重新請求資料。
要實現表格翻頁時重新請求資料也很簡單,使用 antd Table 的 onChange
屬性(分頁、排序、篩選變化時觸發)即可。
前面我們已經在 TableCard.js 中增加了該屬性(即onChange={props.onChange}
)
下面我們將角色管理
頁面的表格改為分頁請求資料:
首先我們回顧下目前這種一次請求表格所有資料,純前端分頁效果。請看程式碼:
render() {
return (
<TableCard
rowKey="id"
title="角色列表"
loading={store.isFetching}
// 後端的資料來源
dataSource={store.dataSource}
onReload={store.fetchRecords}
actions={[
<Button type="primary" icon={<PlusOutlined />}>新增</Button>
]}
// 分頁器
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `共 ${total} 條`,
pageSizeOptions: ['10', '20', '50', '100']
}}
columns={this.columns} />
)
}
只需要給表格傳入資料來源(dataSource),antd Table 自動完成前端分頁效果。
接著我們修改程式碼如下:
// myspug\src\pages\system\role\Table.js
...
import { TableCard, } from '@/components';
import store from './store';
@observer
class ComTable extends React.Component {
componentDidMount() {
store.fetchRecords()
}
columns = [...];
handleTableChange = ({current}, filters, sorter) => {
store.current = current
store.tableOptions = {
// 排序:好像只支援單個排序
sortField: sorter.field,
sortOrder: sorter.order,
...filters
}
store.fetchRecords();
};
render() {
return (
<TableCard
rowKey="id"
title="角色列表"
loading={store.isFetching}
// 後端的資料來源
dataSource={store.dataSource}
onReload={store.fetchRecords}
onChange={this.handleTableChange}
// 分頁器
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `共 ${total} 條`,
pageSizeOptions: ['10', '20', '50', '100'],
// 如果不傳 total,則以後端返回資料條數作為 total 的值
total: store.total,
// 如果不傳,則預設是第一條,如果需要預設顯示第3條,則必須傳
current: store.current,
}}
columns={this.columns} />
)
}
}
export default ComTable
// myspug\src\pages\system\role\store.js
class Store {
...
// 預設第1頁
@observable current = 1;
// 總共多少頁
@observable total = '';
// 其他引數,例如排序、過濾等等
@observable tableOptions = {}
fetchRecords = () => {
const realParams = {current: this.current, ...this.tableOptions}
this.isFetching = true;
http.get('/api/account/role/', {params: realParams})
.then(res => {
// 可以這麼賦值
// ({data: this.records, total: this.pagination.total} = res)
this.total = res.total
this.records = res.data
})
.finally(() => this.isFetching = false)
}
}
export default new Store()
最終效果如下圖所示:
Tip:本地 mock 模擬資料如下
const getNum = () => String(+new Date()).slice(-3)
// 注:第三個引數必須不能是物件,否則 getNum 不會重新執行
Mock.mock(/\/api\/account\/role\/.*/, 'get', function () {
return {
"data": {
data: new Array(10).fill(0).map((item, index) => ({
"id": index + getNum(), "name": 'name' + index + getNum(), "desc": null,
})),
total: 10000,
}
, "error": ""
}
})
試試刪除 <React.StrictMode>
(官網說:這僅適用於開發模式。生產模式下生命週期不會被呼叫兩次)
疑惑:筆者驗證表格時使用了 mobx,表格沒渲染出來,刪除 <React.StrictMode>
後表格正常,不知是否是 <React.StrictMode>
的副作用。
antd Table 元件某些屬性無法使用:spug 中表格是對 antd Table 元件的封裝,但是現在封裝的元件對外的介面只提供了 antd Table 中有限的幾個屬性。例如上文提到的翻頁請求後端資料需要使用 antd Table 中的 onChange 屬性就沒有提供出來
頭部一定會有:不需要都不行
其他章節請看: