/
CATEGORY
React
/
7. 使用 MUI X 的 Data Grid 製作台股報價資料

7. 使用 MUI X 的 Data Grid 製作台股報價資料

MUKI AI Summary

使用 MUI X 的 Data Grid 可以輕鬆製作台股即時報價表格。透過 MUI 的分頁元件和資料分割功能,可實現換頁效果。Data Grid 提供自動排序、索引和過濾等功能,無需額外程式碼。雖然免費版功能有限,但對於開源專案來說已足夠使用。

安裝 @mui/x-data-grid 後,只需定義資料欄位並帶入資料,即可展現強大效果。客製化方面,可調整頁碼顯示與資料顏色,甚至能限制每頁顯示筆數。MUI X 的功能強大,雖然文件複雜,但仍值得使用。...

前言

接下來要製作首頁的「台股即時報價」,也就是下方的表格區塊

我在寫這篇文章前,已經做好了用表格串接資料,但富果 API 沒有分頁功能,所以我預期用前端的分頁套件去處理。

而之前我有寫過使用 Vue + Element Plus 的文章,這次想改用 MUI 試試看。

▼ 之前寫過的 Vue + Element Plus 的文章

使用 MUI 表格與分頁元件,處理前端的大量資料

▼ 利用 MUI 分頁元件的 onChange 事件,再搭配資料分割(slice),就可以做到換頁的功能

▼ 但這篇文章的重點不是表格元件,所以我快速帶過,特別需要說明的地方,我有用註解標記。

'use client'
import React, { useEffect, useState } from 'react'
import axios from 'axios'

import Container from '@mui/material/Container'
import { styled } from '@mui/material/styles'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell, { tableCellClasses } from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import Pagination from '@mui/material/Pagination'

// 客製化表格的 head 樣式,MUI 有很多種法可以客製化,我這邊分享的是 styled 搭配 theme 的寫法。
// 另外也能用 sx={{}},詳見第 88 行
const StyledTableCell = styled(TableCell)(({ theme }) => ({
  [`&.${tableCellClasses.head}`]: {
    backgroundColor: theme.palette.info.main,
    color: theme.palette.secondary.main,
  },
  [`&.${tableCellClasses.body}`]: {
    fontSize: 14,
  },
}))

const apiSnapshotQuotesUrl = 'https://api.fugle.tw/marketdata/v1.0/stock/snapshot/quotes'

const Page = () => {
  const [hydrated, setHydrated] = useState(false)
  const [quote, setQuote] = useState<any>([])
  const [page, setPage] = useState(1)
  const rowsPerPage = 15

  const fetchSnapshot = async (): Promise<void> => {
    try {
      const headers = {
        'X-API-KEY': process.env.API_KEY as string
      }
      const response = await axios.get(`${apiSnapshotQuotesUrl}/TSE`, {
        headers: headers
      })
      setQuote(response.data.data)
    } catch (error) {
      console.error('API 請求錯誤:', error)
    }
  }

  // 頁碼改變時,設定新的頁碼
  const handleChangePage = (_: React.ChangeEvent<unknown>, newPage: number) => {
    setPage(newPage)
  }

  useEffect(() => {
    setHydrated(true)
  }, [])

  useEffect(() => {
    if (hydrated) {
      fetchSnapshot()
    }
  }, [hydrated])

  if (!hydrated) return null

  return (
    <>
      <Container maxWidth="xl">
        <TableContainer>
          <Table sx={{ minWidth: 650 }} size="small" aria-label="a quote table">
            <TableHead>
              <TableRow>
    			{/* 如果用 styled 修改元件樣式,要記得改寫成新的標籤 */}
                <StyledTableCell>股票代碼</StyledTableCell>
                <StyledTableCell>股票名稱</StyledTableCell>
                <StyledTableCell align="right">當前價</StyledTableCell>
                <StyledTableCell align="right">開盤價</StyledTableCell>
                <StyledTableCell align="right">最高價</StyledTableCell>
                <StyledTableCell align="right">最低價</StyledTableCell>
                <StyledTableCell align="right">今日漲跌</StyledTableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {/* 用原始陣列搭配 slice 去取得當前頁碼的 15(rowsPerPage) 筆資料 */}
              {(quote.slice((page - 1) * rowsPerPage, (page - 1) * rowsPerPage + rowsPerPage)).map((row: any) => (
                <TableRow key={row.symbol}
			      {/* 另一種修改樣式的方法,使用 sx={{}} */}
                  sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
                >
                  <TableCell component="th" scope="row">{row.symbol}</TableCell>
                  <TableCell>{row.name}</TableCell>
                  <TableCell align="right">{row.closePrice}</TableCell>
                  <TableCell align="right">{row.openPrice}</TableCell>
                  <TableCell align="right">{row.highPrice}</TableCell>
                  <TableCell align="right">{row.lowPrice}</TableCell>
                  <TableCell align="right">{row.change}({row.changePercent})</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
        {/* count = 幾頁,我用無條件進位法取得總頁數 */}
        <Pagination className="my-6 flex justify-center" count={Math.ceil(quote.length / rowsPerPage)} page={page} variant="outlined" shape="rounded" onChange={handleChangePage} />
      </Container>
    </>
  )
}

export default Page

搭配申請好的富果 API 金鑰,再複製上面的完整語法,就能看到表格分頁的效果,手上如果有富果 API金鑰的朋友可以試試。

當初,我以為這樣就做完了,直到我看到 MUI X 的 Data Grid

一鍵做好資料排序 / 索引 / 過濾的 MUI X Data Grid

我用過 Vue.js 的 Element Plus 以及 React.js 的 Ant Design,他們雖然都能做到表格過濾、排序等功能,但還真沒一款如同 MUI X 的 Data Grid 一樣強大,我只要餵給他資料,他就能自動幫我做好排序、索引、過濾,匯出等功能,我完全不用另外再寫程式碼處理。

MUI X 有付費方案,所以免費版的 Data Grid 功能比較少,但只要是 MIT 授權且開源的專案,就能永久免費使用,非常適合我現在做的這個 Side Project 練習。

更詳細的 MUI 價格與功能比較,可參考他們的官方資訊唷。

安裝 Data Grid

我用的是 free community plan,所以只要安裝 data-grid 即可

$ npm install @mui/x-data-grid

使用 Data Grid 元件

我們只要定義好 header 欄位的屬性與名字,再將資料帶入,就能看到 Data Grid 帶來的驚人效果。我的 columns field 欄位也是直接套用 API 回傳給我的資料屬性,唯一要注意的是,columns 必須要有 id 這個欄位,而我選擇直接用 symbol 的欄位資料複製一份成 id

'use client'
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { DataGrid, GridColDef } from '@mui/x-data-grid'

const columns: GridColDef[] = [
  { field: 'symbol', headerName: '股票代碼', width: 100 },
  { field: 'name', headerName: '股票名稱', width: 150 },
  { field: 'closePrice', headerName: '當前價', width: 120 },
  { field: 'openPrice', headerName: '開盤價', width: 120 },
  { field: 'highPrice', headerName: '最高價', width: 120 },
  { field: 'lowPrice', headerName: '最低價', width: 120 },
  { field: 'change', headerName: '今日漲跌', width: 120 },
  { field: 'changePercent', headerName: '今日漲跌幅', width: 120 }
]

const apiSnapshotQuotesUrl = 'https://api.fugle.tw/marketdata/v1.0/stock/snapshot/quotes'

export default function Page() {
  const [quote, setQuote] = useState<any>([])
  const fetchSnapshot = async (): Promise<void> => {
    try {
      const headers = {
        'X-API-KEY': process.env.API_KEY as string
      }
      const response = await axios.get(`${apiSnapshotQuotesUrl}/TSE`, {
        headers: headers
      })
      const data = response.data.data
      if (data.length === 0) return
      // 將 symbol 的欄位資料複製一份成 id
      data.forEach((item: any) => {
        item.id = item.symbol
      })
      setQuote(data)
    } catch (error) {
      console.error('API 請求錯誤:', error)
    }
  }

  useEffect(() => {
    fetchSnapshot()
  }, [])

  return (
    <div style={{ height: 300, width: '100%' }}>
      <DataGrid rows={quote} columns={columns} />
    </div>
  )
}

▼ 如此就做好了!快速又強大的資料展示元件

▼ MUI 官方也有在 Sandbox 做了一份範例

客製化 Data Grid 元件

接下來,我想讓 Data Grid 元件更符合我的版面設計,所以調整了一些元件設定

頁碼有完整的頁數可切換

Data Grid 預設的頁碼只有「上一頁」與「下一頁」,但我想要的是「 1, 2, 3, 4, 5, 6..., 最後一頁」這樣的設計,而且我不想要給使用者 Rows per page 的功能(每一頁幾筆),所以做了一些微調

▼ 步驟一:引入相關的函式

import { gridPageCountSelector, GridPagination, useGridApiContext, useGridSelector } from '@mui/x-data-grid'
import { TablePaginationProps } from '@mui/material/TablePagination'
import MuiPagination from '@mui/material/Pagination'

▼ 步驟二:在 <DataGrid> 加入 slots 設定,把 pagination 改成自定義的 CustomPagination

<DataGrid rows={quote} columns={columns}
  slots={{
    pagination: CustomPagination,
  }}
/>

▼ 步驟三:客製化 Pagination

const Pagination = ({ page, onPageChange }: Pick<TablePaginationProps, 'page' | 'onPageChange'>) => {
  const apiRef = useGridApiContext()
  const pageCount = useGridSelector(apiRef, gridPageCountSelector)
  return (
    <MuiPagination color="primary" shape="rounded" size="small" count={pageCount} page={page + 1}
      onChange={(event, newPage) => {
        onPageChange(event as any, newPage - 1);
      }}
    />
  )
}

const CustomPagination = (props: any) => {
  return <GridPagination ActionsComponent={Pagination} {...props} />
}

CustomPagination 會返回一個 GridPagination 元件,並將 Pagination 作為其 ActionsComponent 屬性。所以我們在 Data Grid 元件的分頁區域,會使用 Pagination 來呈現。

Pagination 會返回 MUI 的 Pagination 元件,他會有「第 1, 2, 3, 4, ... 到最後一頁」的按鈕,讓我們能直接跳頁。

▼ 修改後如下

他的排版有點奇怪,頁碼都推擠到右側了。我查過 CSS 設定,是因為這邊有一個 flex-wrap: wrap 的樣式,所以我要把這個樣式改成 no-wrap,可以使用前面提到的 sx={{}} 去修改

<MuiPagination color="primary" shape="rounded" size="small" count={pageCount} page={page + 1}
  onChange={(event, newPage) => {
    onPageChange(event as any, newPage - 1);
  }}
  {/* 覆蓋 MuiPagination-ul 的 CSS flex-wrap 樣式 */}
  sx={{
    '& .MuiPagination-ul': {
      flexWrap: 'nowrap'
    },
  }}
/>

▼ 這樣排版就正常了

限制每一頁 X 筆,不讓使用者調整

前面有提到,我想把 Row per pages 這個功能給關掉,不想讓使用者選擇,一樣可以在 <DataGrid /> 調整。

▼ 設定 initialState 以及 pageSizeOptions,就能做到該功能,我預設每一頁顯示 12 筆資料

return (
  <DataGrid rows={quote} columns={columns}
    slots={{
      pagination: CustomPagination,
    }}
    initialState={{
      pagination: { paginationModel: { pageSize: 12 } }
    }}
    pageSizeOptions={[12]}
  />
)

▼ 畫面上不會出現 Row per pages 的功能,而且永遠維持一頁 12 筆資料

調整漲跌的顏色

台股的漲是紅色,跌是綠色,所以我想修改「今日漲跌」跟「今日漲跌幅」,讓文字能有對應的顏色。

▼ 要設定 Data Grid 欄位的規則,可以到 const columns = [] 做調整

const columns: GridColDef[] = [
  { field: 'change', headerName: '今日漲跌', width: 120 },
  { field: 'changePercent', headerName: '今日漲跌幅', width: 120 }
]

▼ 設定 cellClassName 以及 valueFormatter

import { GridCellParams } from '@mui/x-data-grid'

const columns: GridColDef[] = [
  {
    field: 'change', headerName: '今日漲跌', width: 120,
    cellClassName: (params: GridCellParams) => {
      return `${Number(params.value) > 0 ? 'text-primary font-bold' : 'text-success font-bold'}`
    },
    valueFormatter: (params) => {
      const valueFormatted = Number(params.value).toFixed(2)
      return `${valueFormatted}`
    }
  },
  {
    field: 'changePercent', headerName: '今日漲跌幅', width: 120,
    cellClassName: (params: GridCellParams) => {
      return `${Number(params.value) > 0 ? 'text-primary font-bold' : 'text-success font-bold'}`
    },
    valueFormatter: (params) => {
      const valueFormatted = Number(params.value).toFixed(2)
      return `${valueFormatted}`
    }
  }
]

▼ 效果請看圖片的「今日漲跌」以及「今日漲跌幅」,如此就能清楚知道今天的漲跌資訊

▼ 我們還可以把 cellClassName 拉出來成為一個新的 function,方便日後維護與調整

const getCellClassName = (params: GridCellParams) => {
  return `${Number(params.value) > 0 ? 'text-primary font-bold' : 'text-success font-bold'}`
}

const columns: GridColDef[] = [
  {
    field: 'change', headerName: '今日漲跌', width: 120,
    // 改呼叫 getCellClassName
    cellClassName: (params: GridCellParams) => getCellClassName(params),
    valueFormatter: (params) => {
      const valueFormatted = Number(params.value).toFixed(2)
      return `${valueFormatted}`
    }
  },
  {
    field: 'changePercent', headerName: '今日漲跌幅', width: 120,
    // 改呼叫 getCellClassName
    cellClassName: (params: GridCellParams) => getCellClassName(params),
    valueFormatter: (params) => {
      const valueFormatted = Number(params.value).toFixed(2)
      return `${valueFormatted}`
    }
  }
]

valueFormatter 也是一樣,可以將相同的功能做成 function

import { GridValueFormatterParams } from '@mui/x-data-grid'

const getValueFormatter = (params: GridValueFormatterParams) => {
  const valueFormatted = Number(params.value).toFixed(2)
  return `${valueFormatted}`
}

const columns: GridColDef[] = [
  {
    field: 'change', headerName: '今日漲跌', width: 120,
    cellClassName: (params: GridCellParams) => getCellClassName(params),
    // 改呼叫 getValueFormatter
    valueFormatter: (params: GridValueFormatterParams) => getValueFormatter(params)
  },
  {
    field: 'changePercent', headerName: '今日漲跌幅', width: 120,
    cellClassName: (params: GridCellParams) => getCellClassName(params),
    // 改呼叫 getValueFormatter
    valueFormatter: (params: GridValueFormatterParams) => getValueFormatter(params)
  }
]

結語

原本覺得 MUI 的文件非常複雜,而且還要把 API 獨立出來非常的繞,跟 Ant Design 比起來,的確沒那麼親民。但當我看到他的 MUI X Data Grid 功能時,又深深佩服他的強大之處,所以我還是會繼續跟 MUI 做好朋友,希望能愈來愈上手!

有任何問題或是錯誤的地方,都請告訴我,感謝 🙏

歡迎給我點鼓勵,讓我知道你來過 :)

3
15
MUKI says:

如果文章有幫助到你,歡迎分享給更多人知道。文章內容皆為 MUKI 本人的原創文章,我會經常更新文章以及修正錯誤內容,因此轉載時建議保留原出處,避免出現版本不一致或已過時的資訊。

本文地址:https://muki.tw/muix-data-grid/ 已複製

Subscribe
Notify of
guest

0 則留言
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Copyright © since 2008 MUKI space* / omegaSS theme All Rights Reserved.