主頁 > 企業開發 > react 高效高質量搭建后臺系統 系列 —— 表格的封裝

react 高效高質量搭建后臺系統 系列 —— 表格的封裝

2023-02-12 07:30:50 企業開發

其他章節請看:

react 高效高質量搭建后臺系統 系列

表格

有一種頁面在后臺系統中比較常見:頁面分上下兩部分,上部分是 input、select、時間等查詢項,下部分是查詢項對應的表格資料,包含增刪改查,例如點擊新建進行新增操作,就像這樣:

本篇將對 ant 的表格進行封裝,效果如下:

spug 中 Table 封裝的分析

入口

我們選擇 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=https://www.cnblogs.com/pengjiali/archive/2023/02/11/{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
}

TableCard.js

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=https://www.cnblogs.com/pengjiali/archive/2023/02/11/{fields} onChange={onFieldsChange}>
        {/* 展示所有的列 */}
        {columns.map((item, index) => (
          // 注:值的選中是根據索引來的,因為 columns 是陣列,是有順序的,
          {item.title}
        ))}
      
    )
  }

  // 列展示 - 全選或取消全部
  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

myspug 中 Table 封裝的實作

配置 mobx

筆者這里驗證效果時需要使用狀態管理器 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)

這里需要兩處修改即可:

  • config-overrides.js 中增加 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.jsTable.js),

TableCard.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=https://www.cnblogs.com/pengjiali/archive/2023/02/11/{fields} onChange={onFieldsChange}>
         {/* 展示所有的列 */}
         {columns.map((item, index) => (
           // 注:值的選中是根據索引來的,因為 columns 是陣列,是有順序的,
           {item.title}
         ))}
       
     )
   }
 
   // 列展示 - 全選或取消全部
   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
Table.js

這里是表格的使用,與 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
store.js

狀態管理,例如表格的資料的請求,控制表格 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 自動完成前端分頁效果,

接著我們修改代碼如下:

  • Table.js - 給表格增加了 onChange 對應的回呼以及給分頁器增加 total 屬性
  • store.js - 定義新狀態 current、total
// 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": ""
    }
})

擴展

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
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/543631.html

標籤:其他

上一篇:關于 NodeJs 處理超長字串問題的分析

下一篇:使用JS的DOM(檔案物件模型)獲取前端回圈的引數

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more