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

React中如何为ag-Grid右侧表格设置默认分类并自动归类拖拽行

实现ag-Grid右侧表格默认分类及拖拽自动归类功能

问题概述

我正在开发一个使用ag-Grid的React应用,现有左右两个ag-Grid表格,已实现左侧行拖拽到右侧的功能,需要优化以下用户体验:

  • 右侧表格无数据时,默认显示“Male”和“Female”分类
  • 拖拽过来的行自动归入对应分类(如男性运动员到Male分类)

核心需求:

  1. 右侧表格始终展示Male、Female分组,无数据时不消失
  2. 拖拽行自动匹配到性别对应的分组

解决方案

利用ag-Grid的分组功能和拖拽事件处理,通过以下步骤实现需求:

1. 配置右侧表格分组规则

让右侧表格按gender字段分组,设置默认展开分组,同时保留空分组的显示。

2. 初始化空分组占位数据

为右侧表格添加Male、Female的空占位数据,确保无数据时分组依然可见。

3. 处理拖拽事件实现自动归类

修改拖拽结束事件,根据行数据的gender字段,将其添加到对应分组下,并移除对应性别的占位数据。

修改后的完整代码

import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import React, { useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { AgGridReact } from 'ag-grid-react';

const SportRenderer = (props) => {
  return (
    <i
      className="far fa-trash-alt"
      style={{ cursor: 'pointer' }}
      onClick={() => props.api.applyTransaction({ remove: [props.node.data] })}
    ></i>
  );
};

const leftColumns = [
  {
    rowDrag: true,
    maxWidth: 50,
    suppressMenu: true,
    rowDragText: (params, dragItemCount) => {
      if (dragItemCount > 1) {
        return dragItemCount + ' athletes';
      }
      return params.rowNode.data.athlete;
    },
  },
  {
    colId: 'checkbox',
    maxWidth: 50,
    checkboxSelection: true,
    suppressMenu: true,
    headerCheckboxSelection: true,
  },
  { field: 'athlete' },
  { field: 'sport' },
  { field: 'gender' },
];

const rightColumns = [
  {
    rowDrag: true,
    maxWidth: 50,
    suppressMenu: true,
    rowDragText: (params, dragItemCount) => {
      if (dragItemCount > 1) {
        return dragItemCount + ' athletes';
      }
      return params.rowNode.data.athlete;
    },
  },
  { field: 'athlete' },
  { field: 'sport' },
  { field: 'gender' },
  {
    suppressMenu: true,
    maxWidth: 50,
    cellRenderer: SportRenderer,
  },
];

const defaultColDef = {
  flex: 1,
  minWidth: 100,
  sortable: true,
  filter: true,
  resizable: true,
};

// 初始化空分组占位数据
const initialRightData = [
  { athlete: '', sport: '', gender: 'Male', __isPlaceholder: true },
  { athlete: '', sport: '', gender: 'Female', __isPlaceholder: true },
];

const GridExample = () => {
  const [leftApi, setLeftApi] = useState(null);
  const [leftColumnApi, setLeftColumnApi] = useState(null);
  const [rightApi, setRightApi] = useState(null);
  const [rawData, setRawData] = useState([]);
  const [leftRowData, setLeftRowData] = useState(null);
  const [rightRowData, setRightRowData] = useState([...initialRightData]);
  const [radioChecked, setRadioChecked] = useState(0);
  const [checkBoxSelected, setCheckBoxSelected] = useState(true);

  useEffect(() => {
    if (!rawData.length) {
      fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
        .then((resp) => resp.json())
        .then((data) => {
          const athletes = [];
          let i = 0;

          while (athletes.length < 20 && i < data.length) {
            var pos = i++;
            if (athletes.some((rec) => rec.athlete === data[pos].athlete)) {
              continue;
            }
            athletes.push(data[pos]);
          }
          setRawData(athletes);
        });
    }
  }, [rawData]);

  const loadGrids = useCallback(() => {
    setLeftRowData([...rawData]);
    setRightRowData([...initialRightData]);
    leftApi.deselectAll();
  }, [leftApi, rawData]);

  useEffect(() => {
    if (rawData.length) {
      loadGrids();
    }
  }, [rawData, loadGrids]);

  useEffect(() => {
    if (leftColumnApi && leftApi) {
      leftColumnApi.setColumnVisible('checkbox', checkBoxSelected);
      leftApi.setSuppressRowClickSelection(checkBoxSelected);
    }
  }, [leftColumnApi, leftApi, checkBoxSelected]);

  const reset = () => {
    setRadioChecked(0);
    setCheckBoxSelected(true);
    loadGrids();
  };

  const onRadioChange = (e) => {
    setRadioChecked(parseInt(e.target.value, 10));
  };

  const onCheckboxChange = (e) => {
    const checked = e.target.checked;
    setCheckBoxSelected(checked);
  };

  const getRowId = (params) => params.data.athlete || `${params.data.gender}-placeholder`;

  const onDragStop = useCallback(
    (params) => {
      const draggedNodes = params.nodes;
      const draggedData = draggedNodes.map(node => node.data);

      // 更新右侧表格数据:移除对应性别的占位符,添加拖拽数据
      const updatedRightData = [...rightRowData];
      draggedData.forEach(item => {
        // 移除该性别下的占位符(如果存在)
        const placeholderIndex = updatedRightData.findIndex(
          data => data.__isPlaceholder && data.gender === item.gender
        );
        if (placeholderIndex !== -1) {
          updatedRightData.splice(placeholderIndex, 1);
        }
        // 添加拖拽过来的数据
        updatedRightData.push(item);
      });
      setRightRowData(updatedRightData);

      // 处理左侧表格的操作
      if (radioChecked === 0) {
        leftApi.applyTransaction({
          remove: draggedData,
        });
      } else if (radioChecked === 1) {
        leftApi.setNodesSelected({ nodes: draggedNodes, newValue: false });
      }
    },
    [leftApi, radioChecked, rightRowData]
  );

  useEffect(() => {
    if (!leftApi || !rightApi) {
      return;
    }
    const dropZoneParams = rightApi.getRowDropZoneParams({ onDragStop });

    leftApi.removeRowDropZone(dropZoneParams);
    leftApi.addRowDropZone(dropZoneParams);
  }, [leftApi, rightApi, onDragStop]);

  const onGridReady = (params, side) => {
    if (side === 0) {
      setLeftApi(params.api);
      setLeftColumnApi(params.columnApi);
    }

    if (side === 1) {
      setRightApi(params.api);
      // 配置分组
      params.columnApi.setRowGroupColumns(['gender']);
    }
  };

  const getTopToolBar = () => (
    <div className="example-toolbar panel panel-default">
      <div className="panel-body">
        <div onChange={onRadioChange}>
          <input
            type="radio"
            id="move"
            name="radio"
            value="0"
            checked={radioChecked === 0}
          /> {' '}
          <label htmlFor="move">Remove Source Rows</label>
          <input
            type="radio"
            id="deselect"
            name="radio"
            value="1"
            checked={radioChecked === 1}
          /> {' '}
          <label htmlFor="deselect">Only Deselect Source Rows</label>
          <input
            type="radio"
            id="none"
            name="radio"
            value="2"
            checked={radioChecked === 2}
          /> {' '}
          <label htmlFor="none">None</label>
        </div>
        <input
          type="checkbox"
          id="toggleCheck"
          checked={checkBoxSelected}
          onChange={onCheckboxChange}
        />
        <label htmlFor="toggleCheck">Checkbox Select</label>
        <span className="input-group-button">
          <button
            type="button"
            className="btn btn-default reset"
            style={{ marginLeft: '5px' }}
            onClick={reset}
          >
            <i className="fas fa-redo" style={{ marginRight: '5px' }}></i>Reset
          </button>
        </span>
      </div>
    </div>
  );

  const getGridWrapper = (id) => (
    <div className="panel panel-primary" style={{ marginRight: '10px', width: '48%' }}>
      <div className="panel-heading">
        {id === 0 ? 'Athletes' : 'Selected Athletes'}
      </div>
      <div className="panel-body" style={{ height: '500px' }}>
        <AgGridReact
          style={{ height: '100%' }}
          defaultColDef={defaultColDef}
          getRowId={getRowId}
          rowDragManaged={true}
          animateRows={true}
          rowSelection={id === 0 ? 'multiple' : undefined}
          rowDragMultiRow={id === 0}
          suppressRowClickSelection={id === 0}
          suppressMoveWhenRowDragging={id === 0}
          rowData={id === 0 ? leftRowData : rightRowData}
          columnDefs={id === 0 ? leftColumns : rightColumns}
          onGridReady={(params) => onGridReady(params, id)}
          // 右侧表格分组配置
          groupDefaultExpanded={1}
          suppressEmptyChildren={false}
        />
      </div>
    </div>
  );

  return (
    <div className="top-container">
      {getTopToolBar()}
      <div className="grid-wrapper ag-theme-alpine" style={{ display: 'flex', justifyContent: 'flex-start' }}>
        {getGridWrapper(0)}
        {getGridWrapper(1)}
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById('root'));
root.render(<GridExample />);

关键修改说明

  1. 空分组初始化:创建initialRightData数组,包含Male和Female的占位数据,通过__isPlaceholder标记区分真实数据
  2. 分组配置:在右侧表格的onGridReady中设置按gender分组,开启groupDefaultExpanded默认展开分组,suppressEmptyChildren={false}保留空分组显示
  3. 拖拽逻辑优化:修改onDragStop事件,先移除对应性别的占位数据,再添加拖拽行,实现自动归类
  4. 行ID处理:更新getRowId方法,为占位数据生成唯一ID,避免ag-Grid出现重复行ID报错

内容的提问来源于stack exchange,提问作者vishnu vinod

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 09:44:58