FORMA

vxe-table 行 Hover 联动高亮

近期需求是双表格联动高亮:

  • 鼠标移入左侧表格某一行时,右侧对应行同步高亮;
  • 鼠标移入右侧表格某一行时,左侧对应行也同步高亮。

实现思路

项目使用 vxe-table。在当前场景下,采用原生事件监听 trmouseenter/mouseleave 更直接可控:

  1. 给目标表格行绑定移入/移出事件;
  2. 在回调中拿到当前行索引;
  3. 根据映射关系找到另一侧需要联动的行索引;
  4. 对目标行批量设置/清除高亮样式。

为避免重复绑定导致事件叠加,使用 WeakMap 保存并复用事件处理函数引用。

ts
import { isArray, isFunction, isString } from "lodash-es";

type Tr = HTMLTableRowElement;
type TrList = NodeListOf<Tr>;

type EventType = "mouseenter" | "mouseleave";

type TrEventCallBack = (ops: Omit<IItemTrFunCbParams, "callback">) => void;
interface ITrMouseEnterParams {
  /** 表格tr选择器 */
  selector: string;
  /** 回调函数 */
  callback?: TrEventCallBack;
}

interface IItemTrFunCbParams {
  /** 鼠标事件对象或事件目标元素 */
  row: MouseEvent | { target: Tr };
  /** 行索引 */
  rowIndex: number;
  /** 事件类型 */
  eventType: EventType;
  /** 回调函数 */
  callback?: TrEventCallBack;
}

interface IFindTableTrParams {
  /** 表格tr选择器 */
  selector: string;
  /** 行索引数组 */
  indexes: number[];
  /** 事件类型 */
  eventType: EventType;
}

// 使用 WeakMap 存储每个元素的事件处理函数引用
const eventHandlersMap = new WeakMap<
  Tr,
  {
    mouseenter: (e: MouseEvent) => void;
    mouseleave: (e: MouseEvent) => void;
  }
>();

class HoverTableUtil {
  static trColor = "#ff880019";

  /**
   * 查找表格tr元素并绑定鼠标移入移出事件
   * @param param0 查找表格tr参数
   * @returns 无返回值
   */
  findTableTr({ selector, indexes, eventType }: IFindTableTrParams) {
    if (!selector || !isString(selector)) return;
    if (!isArray(indexes)) return;
    for (let i = 0; i < indexes.length; i++) {
      const item = indexes[i];
      const tr = document.querySelectorAll(selector)[item] as Tr;
      if (!tr) continue;
      HoverTableUtil.itemTrFunCb({
        row: { target: tr },
        rowIndex: i,
        eventType,
      });
    }
  }

  /**
   * 表格 tr 鼠标事件回调函数
   * @param param0 表格 tr 鼠标事件参数
   * @returns 无返回值
   */
  static itemTrFunCb({ row, rowIndex, eventType, callback }: IItemTrFunCbParams) {
    if (!row) return;
    const target = row.target as Tr;
    target.style.backgroundColor = eventType === "mouseenter" ? HoverTableUtil.trColor : "";
    if (isFunction(callback)) callback({ row, rowIndex, eventType });
  }

  /**
   * 表格 tr 鼠标移入移出事件绑定
   * @param param0 表格 tr 鼠标事件参数
   * @returns 无返回值
   */
  trMouseEnter({ selector, callback }: ITrMouseEnterParams) {
    if (!selector || !isString(selector)) return;
    const trList: TrList = document.querySelectorAll(selector);
    for (let i = 0; i < trList.length; i++) {
      const item = trList[i];
      const existingHandlers = eventHandlersMap.get(item);
      if (existingHandlers) {
        item.removeEventListener("mouseenter", existingHandlers.mouseenter);
        item.removeEventListener("mouseleave", existingHandlers.mouseleave);
        eventHandlersMap.delete(item);
      }
      // 创建新的事件处理函数
      const mouseenterHandler = (row: MouseEvent) => {
        HoverTableUtil.itemTrFunCb({
          row,
          rowIndex: i,
          eventType: "mouseenter",
          callback,
        });
      };
      const mouseleaveHandler = (row: MouseEvent) => {
        HoverTableUtil.itemTrFunCb({
          row,
          rowIndex: i,
          eventType: "mouseleave",
          callback,
        });
      };

      // 保存函数引用
      eventHandlersMap.set(item, {
        mouseenter: mouseenterHandler,
        mouseleave: mouseleaveHandler,
      });

      // 挂载事件
      item.addEventListener("mouseenter", mouseenterHandler);
      item.addEventListener("mouseleave", mouseleaveHandler);
    }
  }
}

export const hoverTableUtil = new HoverTableUtil();

使用示例

ts
// 使用demo

watch(
  () => data1.value,
  (val) => {
    hoverTableUtil.trMouseEnter({
      selector: ".left-table table tbody tr",
      callback: ({ rowIndex, eventType }) => {
        hoverTableUtil.findTableTr({
          selector: ".right-table table tbody tr",
          indexes: [], // 高亮的索引合集
          eventType,
        });
      },
    });
  },
);

watch(
  () => data2.value,
  (val) => {
    hoverTableUtil.trMouseEnter({
      selector: ".right-table table tbody tr",
      callback: ({ rowIndex, eventType }) => {
        hoverTableUtil.findTableTr({
          selector: ".left-table table tbody tr",
          indexes: [], // 高亮的索引合集
          eventType,
        });
      },
    });
  },
);

注意事项

  1. watch 重新触发时会重复绑定事件,已通过 WeakMap + removeEventListener 做清理。
  2. indexes 建议由业务层提前计算好(如主键映射),避免在 hover 回调里做重计算。
  3. 高亮样式建议统一为 class 控制,复杂场景可替换直接写 style 的方式。