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 做好朋友,希望能愈來愈上手!
有任何問題或是錯誤的地方,都請告訴我,感謝 🙏