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

如何用Isotope.js实现多选项过滤的AND逻辑关系?

Isotope.js 双重过滤(Type + Tag)修复方案

问题背景

使用Isotope.js实现type和tag两组过滤布局,但过滤逻辑不符合预期,例如点击「视频演示与导览」和「Ansible」时,本该仅显示卡片3,实际未生效。预期规则如下:

  • 仅选一个tag:显示包含该tag的内容
  • 选多个tag:显示包含任一选中tag的内容
  • 选type:仅显示该类型的内容
  • 选一个type+一个tag:显示该类型下包含此tag的内容
  • 选一个type+多个tag:显示该类型下包含任一选中tag的内容
  • 选多个type+一个/多个tag:显示任一选中类型下包含对应tag的内容

问题根源

原代码存在3个核心问题:

  1. Type单选逻辑混乱:点击新type选项时未清空同组其他选中状态,且HTML中存在data-filter与卡片class的拼写不匹配(「Video Demos & Tours」的data-filter曾错误写成video-demos-tours,与卡片classvideo-demos-tour不一致)
  2. 组合过滤逻辑错误:原代码将type和tag的过滤规则错误拼接为「(type) OR (type AND tag)」,实际需要的是「(type1 OR type2) AND (tag1 OR tag2)」的交集逻辑
  3. Tag选中状态同步错误:处理tag时判断选中状态的时机错误,导致过滤器数组更新异常

修复后的完整代码

JavaScript

document.addEventListener("DOMContentLoaded", function () {
  const container = document.querySelector(".grid");
  const optionLinks = document.querySelectorAll(".rSidebar__options-li");

  const iso = new Isotope(container, {
    itemSelector: ".resourceCard",
    layoutMode: "fitRows",
    transitionDuration: "0.5s"
  });

  // 存储选中的过滤器:key为分组名,value为选中项数组
  const filters = {
    type: [],
    tag: []
  };

  // 生成符合逻辑的Isotope过滤选择器
  function getFilterSelector() {
    // 处理type:单选模式,生成「.type1」格式
    const typeSelector = filters.type.length 
      ? filters.type.map(t => `.${t}`).join(', ') 
      : '*';
    // 处理tag:多选模式,生成「.tag1, .tag2」格式
    const tagSelector = filters.tag.length 
      ? filters.tag.map(t => `.${t}`).join(', ') 
      : '*';

    // 核心逻辑:(type1 或 type2) 并且 (tag1 或 tag2)
    return `(${typeSelector})${tagSelector !== '*' ? `:is(${tagSelector})` : ''}`;
  }

  function handleFilterClick(listItem) {
    const filterGroup = listItem.closest(".rSidebar__options").getAttribute("data-filter-group");
    const filterValue = listItem.getAttribute("data-filter");
    let isSelected = listItem.classList.contains("selected");

    // Type分组:单选逻辑,点击新选项时清空同组其他选中项
    if (filterGroup === "type") {
      // 移除同组所有已选中项的状态
      document.querySelectorAll(`.rSidebar__options[data-filter-group="type"] .selected`).forEach(item => {
        item.classList.remove("selected");
      });
      // 更新filters:单选模式下,仅保留当前选中项(或清空)
      filters.type = isSelected ? [] : [filterValue];
      // 同步当前项的选中状态
      if (!isSelected) {
        listItem.classList.add("selected");
      }
      isSelected = !isSelected;
    } else {
      // Tag分组:多选逻辑,添加/移除选中项
      if (isSelected) {
        filters.tag = filters.tag.filter(tag => tag !== filterValue);
      } else {
        filters.tag.push(filterValue);
      }
    }

    // 执行Isotope过滤
    const finalFilter = getFilterSelector();
    iso.arrange({ filter: finalFilter });
  }

  // 绑定点击事件
  optionLinks.forEach(optionLink => {
    optionLink.addEventListener("click", function (event) {
      event.preventDefault();
      // Type分组的选中状态由handleFilterClick统一处理,避免冲突
      if (this.closest(".rSidebar__options").getAttribute("data-filter-group") !== "type") {
        this.classList.toggle("selected");
      }
      handleFilterClick(this);
    });
  });
});

HTML(修正拼写错误)

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.min.js"></script>

<div class="post">
  <div class="container">
    <div class="row justify-content-between">

      <!-- 侧边栏过滤器 -->
      <div class="col-3">
        <div class="rSidebar">
          <!-- Tag过滤器组 -->
          <div class="rSidebar__box">
            <span class="rSidebar__label d-block fw-bold">按标签筛选</span>
            <ul class="rSidebar__options button-group" data-filter-group="tag">
              <li class="rSidebar__options-li" data-filter="ansible">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block">Ansible</span>
              </li>
              <li class="rSidebar__options-li" data-filter="automation">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block">Automation</span>
              </li>
            </ul>
          </div>

          <!-- Type过滤器组 -->
          <div class="rSidebar__box">
            <span class="rSidebar__label d-block fw-bold">按类型筛选</span>
            <ul class="rSidebar__options button-group" data-filter-group="type">
              <li class="rSidebar__options-li" data-filter="blogs-and-news">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block">博客与资讯</span>
              </li>
              <li class="rSidebar__options-li" data-filter="video-demos-tour">
                <span class="rSidebar__options-square"></span>
                <span class="rSidebar__options-label d-block">视频演示与导览</span>
              </li>
            </ul>
          </div>
        </div>
      </div>

      <!-- 内容网格 -->
      <div class="col-7">
        <div class="grid">
          <article class="resourceCard grid-item video-demos-tour automation">
            <span class="resourceCard__body-title">卡片1<br>类型:视频演示与导览<br>标签:Automation</span>
          </article>
          <article class="resourceCard grid-item blogs-and-news automation">
            <span class="resourceCard__body-title">卡片2<br>类型:博客与资讯<br>标签:Automation</span>
          </article>
          <article class="resourceCard grid-item video-demos-tour automation ansible">
            <span class="resourceCard__body-title">卡片3<br>类型:视频演示与导览<br>标签:Automation、Ansible</span>
          </article>
          <article class="resourceCard grid-item blogs-and-news ansible">
            <span class="resourceCard__body-title">卡片4<br>类型:博客与资讯<br>标签:Ansible</span>
          </article>
          <article class="resourceCard grid-item blogs-and-news ansible automation">
            <span class="resourceCard__body-title">卡片5<br>类型:博客与资讯<br>标签:Ansible、Automation</span>
          </article>
        </div>
      </div>

    </div>
  </div>
</div>

CSS(保留原样式)

.post {
  padding: 100px;
}

.rSidebar__box {
  margin-bottom: 30px;
}
.rSidebar__options {
  padding-left: 0;
}
.rSidebar__options-li {
  margin-bottom: 17px;
  display: flex;
  align-items: center;
  cursor: pointer;
  width: fit-content;
}
.rSidebar__options-li.selected .rSidebar__options-square {
  background-color: #185A7D;
}
.rSidebar__options-square {
  height: 20px;
  width: 20px;
  transition: all 0.5s ease;
  border: 2px solid #000000;
}
.rSidebar__options-label {
  margin-left: 10px;
}

.grid {
  display: flex;
  flex-wrap: wrap;
  margin: -14px 0 0 -14px;
}
.grid-item {
  box-sizing: border-box;
  width: calc(45% - 14px);
  margin: 14px 0 18px 14px;
  border: 2px solid black;
  padding: 20px;
}

验证场景

  • 仅选「Ansible」:显示卡片3、4、5
  • 选「Ansible」+「Automation」:显示所有卡片
  • 仅选「视频演示与导览」:显示卡片1、3
  • 选「视频演示与导览」+「Ansible」:仅显示卡片3(符合预期)
  • 选「博客与资讯」+「视频演示与导览」+「Ansible」:显示卡片3、4、5

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 05:34:57