You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Grid+Subgrid可折叠表格的布局与视觉动画不同步问题排查求助

Grid+Subgrid可折叠表格的布局与视觉动画不同步问题排查求助

我正在尝试用Grid和Subgrid实现一个可折叠表格,需求是折叠后只显示3行,显示的行取决于用户选中的行。动画部分我用translateY让行块靠拢,同时通过计算显示行的数量来设置表格的高度实现整体折叠。选择Subgrid而非Flex是因为我没法给列设置固定宽度限制,需要右侧列自适应内容,左侧列自动拉伸。

但现在遇到了麻烦:折叠动画过程中出现了明显的滞后不同步,中间状态下视觉效果错位,行的移动和表格高度变化没对齐。

下面是我的完整实现代码,包含JS逻辑和CSS样式:

JavaScript 逻辑

let isOpen = false;
let state = {
  items: [],
  visibleRows: 3,
  isMultiSelect: false,
};

// 初始化状态
function initializeState() {
  const rows = document.querySelectorAll('.table-row');
  state.items = Array.from(rows).map((row, index) => ({
    id: index,
    selected: row.querySelector('input').checked,
    transformMultiplier: 0,
    element: row,
  }));
  calculateTransforms();
}

// 计算行的位移乘数(从React组件移植过来的逻辑)
function calculateTransforms() {
  const selectedRows = state.items.reduce(
    (count, item) => count + (item.selected ? 1 : 0),
    0,
  );

  let gracePortion = Math.max(0, 3 - selectedRows);
  let distance = 0;
  const distances = [];

  const firstPass = state.items.map((item, index) => {
    let transformMultiplier = null;
    if (item.selected) {
      transformMultiplier = distance;
      distances.push(distance);
    } else if (gracePortion > 0) {
      transformMultiplier = distance;
      distances.push(distance);
      gracePortion--;
    } else if (state.items.length === index + 1) {
      distance++;
      transformMultiplier = distance;
      distances.push(distance);
    } else {
      distance++;
    }
    return { ...item, transformMultiplier: transformMultiplier };
  });

  let distancePosition = 0;
  const items = firstPass.map((item) => {
    let transformMultiplier = item.transformMultiplier;
    if (transformMultiplier === null) {
      transformMultiplier = distances[distancePosition];
    } else {
      distancePosition++;
    }
    return { ...item, transformMultiplier: transformMultiplier };
  });

  state.items = items;
  state.visibleRows = Math.max(3, selectedRows);
  updateTransforms();
}

// 更新行的位移和表格高度
function updateTransforms() {
  const table = document.getElementById('table');
  const totalRows = isOpen ? state.items.length : state.visibleRows;
  // 更新CSS自定义属性控制表格高度
  table.style.setProperty('--number-of-rows', totalRows);

  state.items.forEach((item, index) => {
    const translateY = isOpen ? 0 : -item.transformMultiplier * 100;
    item.element.style.transform = `translateY(${translateY}%)`;
    item.element.style.zIndex = state.items.length - index;
  });
}

// 切换展开/折叠状态
function toggleRows() {
  isOpen = !isOpen;
  const button = document.querySelector('.footer-button');
  button.innerHTML = isOpen 
    ? 'Show less <span class="chevron open" id="chevron"></span>' 
    : 'Show more <span class="chevron" id="chevron"></span>';
  calculateTransforms();
}

// 处理单选框选择变化
function handleSelectionChange() {
  const rows = document.querySelectorAll('.table-row');
  rows.forEach((row, index) => {
    const input = row.querySelector('input');
    state.items[index].selected = input.checked;
  });
  calculateTransforms();
}

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function () {
  initializeState();
  // 给单选框绑定事件
  const inputs = document.querySelectorAll('input[type="radio"]');
  inputs.forEach((input) => {
    input.addEventListener('change', handleSelectionChange);
  });
});

CSS 样式

:root {
  --duration: 500ms;
}

.table-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 600px;
  margin: 20px auto;
}

.table {
  --number-of-rows: 3;
  position: relative;
  display: grid;
  grid-template-columns: auto max-content;
  overflow: hidden;
  height: calc(var(--number-of-rows) * 51px + 42px);
  transition: height var(--duration) ease;
}

.table-header {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: span 2;
  background-color: grey;
  height: 40px;
  align-items: center;
  color: white;
  font-weight: bold;
  border: solid 1px black;
  z-index: 9999;
}

.table-row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: span 2;
  height: 50px;
  align-items: center;
  border-inline: solid 1px red;
  border-bottom: solid 1px red;
  transition: transform var(--duration) ease;
}

.table-row:nth-child(even) {
  background-color: #f8f8f8;
}

.table-row:nth-child(odd) {
  background-color: #e8e8e8;
}

.table-row input[type="radio"] {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  margin: 0;
}

.table-row:has(:checked) {
  outline: 2px solid green;
  outline-offset: -2px;
}

.footer-button {
  width: 100%;
  padding: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  cursor: pointer;
  text-align: center;
  transition: background-color 0.2s ease;
}

.footer-button:hover {
  background-color: #0056b3;
}

.item {
  padding: 10px;
}

.chevron {
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid white;
  margin-left: 5px;
}

.chevron.open {
  border-top: none;
  border-bottom: 5px solid white;
}

我排查了很久,怀疑是行的translateY动画和表格高度动画的时序或者计算逻辑有问题,但没找到根因。有没有大佬能帮忙分析下,为什么动画过程中会出现滞后不同步,以及怎么修复这个问题?

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 03:13:08