You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

MUI自定义滚动表格布局:如何实现表头列与滚动数据列的完美对齐?

MUI自定义滚动表格布局:如何实现表头列与滚动数据列的完美对齐?

核心问题分析

你的原始代码中,滚动数据列与表头错位的根本原因是:嵌套在TableCell内的TableRow没有继承外层表格的列宽定义。你用colSpan=3把滚动区域打包成一个单元格,内部的行完全根据自身内容调整宽度,和表头列宽没有绑定关系,自然会出现错位。

最优解决方案:CSS Grid 布局

使用CSS Grid(结合MUI的Box组件)是实现这种自定义布局的最可靠方式,因为Grid天生支持跨容器的列宽同步,能确保表头和滚动数据的列宽完全一致。以下是经过验证的完整实现:

import { Box, Chip, Typography, Paper } from '@mui/material';
import { useTheme } from '@mui/material/styles';

const AttendanceTable = ({ data }) => {
  const theme = useTheme();

  // 统一定义列宽模板,表头和所有考试区块共用
  const mainGridTemplate = `90px repeat(3, minmax(120px, 1fr)) 90px`;
  // 滚动数据的内部列模板,和表头中间三列完全匹配
  const recordGridTemplate = `repeat(3, minmax(120px, 1fr))`;

  return (
    <Box
      sx={{
        width: '100%',
        borderRadius: '16px',
        p: '1.5rem',
        bgcolor: 'background.default',
      }}
    >
      <Typography variant="h3" sx={{ mb: 2 }}>
        Attendance
      </Typography>
      <Paper
        sx={{
          borderRadius: '9px',
          boxShadow: 'none',
          border: 'none',
          overflow: 'hidden',
        }}
      >
        {/* 表头行:使用主Grid模板 */}
        <Box
          sx={{
            display: 'grid',
            gridTemplateColumns: mainGridTemplate,
            gap: 0,
            borderBottom: `2px solid ${theme.palette.divider}`,
          }}
        >
          <Box sx={{ p: '1.5rem 2rem', textAlign: 'center', fontWeight: 'bold' }}>
            Exam
          </Box>
          <Box sx={{ p: '1.5rem 2rem', textAlign: 'center', fontWeight: 'bold' }}>
            Date
          </Box>
          <Box sx={{ p: '1.5rem 2rem', textAlign: 'center', fontWeight: 'bold' }}>
            Day
          </Box>
          <Box sx={{ p: '1.5rem 2rem', textAlign: 'center', fontWeight: 'bold' }}>
            Status
          </Box>
          <Box sx={{ p: '1.5rem 2rem', textAlign: 'center', fontWeight: 'bold' }}>
            Percentage
          </Box>
        </Box>

        {/* 每个考试的区块 */}
        {data.exams.map((exam, examIndex) => {
          const total = exam.attendanceRecords.length;
          const present = exam.attendanceRecords.filter(r => r.status === 'Present').length;
          const percentage = total ? ((present / total) * 100).toFixed(2) : 0;

          return (
            <Box
              key={`exam-${examIndex}`}
              sx={{
                display: 'grid',
                gridTemplateColumns: mainGridTemplate,
                gap: 0,
                borderBottom: `2px solid ${theme.palette.divider}`,
              }}
            >
              {/* 固定的Exam列 */}
              <Box
                sx={{
                  p: '1.5rem 2rem',
                  textAlign: 'center',
                  fontWeight: 500,
                  whiteSpace: 'nowrap',
                  alignSelf: 'start',
                }}
              >
                {exam.exam}
              </Box>

              {/* 滚动的考勤记录区域(Date/Day/Status) */}
              <Box
                sx={{
                  gridColumn: '2 / 5', // 占据主Grid的第2-4列(对应Date/Day/Status)
                  maxHeight: '180px',
                  overflowY: 'auto',
                  pr: 1, // 给滚动条预留空间,避免内容被遮挡
                  '&::-webkit-scrollbar': { width: '4px' },
                  '&::-webkit-scrollbar-thumb': {
                    backgroundColor: 'rgba(0,0,0,0.3)',
                    borderRadius: '4px',
                  },
                  scrollbarWidth: 'thin',
                  scrollbarColor: 'rgba(0,0,0,0.3) transparent',
                }}
              >
                {exam.attendanceRecords.map((record, idx) => (
                  <Box
                    key={`record-${examIndex}-${idx}`}
                    sx={{
                      display: 'grid',
                      gridTemplateColumns: recordGridTemplate,
                      gap: 0,
                      borderBottom: idx === exam.attendanceRecords.length - 1 ? 'none' : `1px solid ${theme.palette.divider}`,
                    }}
                  >
                    <Box sx={{ p: '1.5rem 2rem', textAlign: 'center' }}>
                      {record.date}
                    </Box>
                    <Box sx={{ p: '1.5rem 2rem', textAlign: 'center' }}>
                      {record.day}
                    </Box>
                    <Box sx={{ p: '1.5rem 2rem', textAlign: 'center' }}>
                      <Chip
                        label={record.status}
                        color={record.status === 'Present' ? 'success' : 'error'}
                        size="small"
                      />
                    </Box>
                  </Box>
                ))}
              </Box>

              {/* 固定的Percentage列 */}
              <Box
                sx={{
                  p: '1.5rem 2rem',
                  textAlign: 'center',
                  fontWeight: 500,
                  whiteSpace: 'nowrap',
                  alignSelf: 'start',
                }}
              >
                {percentage}%
              </Box>
            </Box>
          );
        })}
      </Paper>
    </Box>
  );
};

export default AttendanceTable;

为什么这个方案能解决对齐问题?

  1. 统一列宽模板:表头和每个考试区块都使用相同的gridTemplateColumns定义,确保Date/Day/Status三列的宽度在整个组件中完全一致。
  2. Grid列跨度控制:滚动区域通过gridColumn: '2 /5'精准占据表头对应的三列空间,内部的记录行也使用和表头匹配的列模板。
  3. 响应式且稳定:使用minmax(120px, 1fr)让列宽在保持最小宽度的同时自适应容器,缩放、滚动、窗口 resize 都不会破坏对齐。
  4. 滚动条预留空间:给滚动区域添加pr:1,避免滚动条遮挡内容导致的视觉错位。

替代方案:MUI Table 改造(不推荐)

如果你坚持使用MUI Table组件,需要手动同步内外层表格的列宽,但实现复杂且维护成本高:

  • 给外层Table的Date/Day/Status列设置固定宽度
  • 嵌套在TableCell内的内层Table,必须给对应列设置完全相同的固定宽度
  • 监听窗口 resize 事件动态调整宽度(应对缩放场景)

这种方式容易出现边缘 case,因此更推荐使用前面的Grid布局方案。

关键总结

  • 避免在Table中嵌套Table实现滚动区域,Grid布局是更适合这种非标准表格场景的选择
  • 始终通过统一的布局模板(如gridTemplateColumns)同步表头和数据列的宽度
  • 滚动区域要预留滚动条空间,避免内容偏移

内容来源于stack exchange

火山引擎 最新活动