← 返回目录

3. 筛选搜索与多条件联动

把这页想成"在一堆衣服里挑衣服":你先定条件"只要红色",再叠加"只要 M 码",再叠加"价格不超过 200"——条件一个一个往上加,范围越收越窄。管理后台 80% 的列表页都是这个逻辑。这页要搞懂三件事:前端筛选和后端搜索怎么选多个条件怎么叠加点表头怎么排序。核心武器就是数组的 filtersort

3.1 先跑起来:多条件筛选 + 表头排序

姓名 角色 年龄 状态

试试:姓名输入"张" + 角色选"管理员",条件是叠加生效的;点表头可以升序/降序切换。

3.2 前端筛选 vs 后端搜索:先分清再写码

对比维度 前端筛选 后端搜索
数据来源 已经加载到浏览器里的数据 服务器数据库里的数据
在哪执行 浏览器(JS 操作数组) 服务器(跑 SQL 查询)
响应速度 毫秒级,不联网,飞快 依赖网络,慢一些
数据量限制 建议 ≤ 2000 条 无限制
典型场景 管理后台小列表 电商搜索、海量用户

记个口诀:数据少用前端切,数据多用后端查;页面不刷新、不发请求的是前端,要发请求、等接口的是后端。本页演示是前端筛选——数据早就躺在浏览器的数组里了,点"筛选"只是在内存里重新挑一遍,所以秒出结果。

3.3 前端筛选核心 API(数组方法全家桶)

// 筛出符合条件的元素
filter

// 找出【第一个】符合条件的元素(只返回一个)→ 只找一个
find

// 有没有【至少一个】符合 → true / false → 有一个满足就行
some

// 是不是【所有】都符合
every

// 排序(会【改原数组】,记得先拷贝)→ 直接在原数组上排,危险
sort

// 单条件:年龄 ≥ 30 → 只要 30 岁及以上
const result = users.filter(function (user) { return user.age >= 30; });

// 多条件叠加:北京 且 年龄 ≥ 25
const result2 = users.filter(function (user) { return user.city === "北京" && user.age >= 25; });

重点记住 filter 的脾气:它返回一个新数组,原数组一动不动,这正好适合"反复筛选"。而 sort 反过来——它直接改原数组,所以排序前一定要先拷贝一份,别把原始数据排乱了。

3.4 多条件筛选:条件累加模式(最标准、最不容易错的写法)

// 点"筛选"时执行
function applyFilters() {
  // 1. 先把所有条件从页面上收集下来 → 把用户填的东西都取出来
  // 姓名关键词(去掉两头空格)
  const keyword = $("#kw").val().trim();

  // 选的角色
  const role = $("#roleSel").val();

  // 选的状态
  const status = $("#statusSel").val();

  // 2. 永远从【原始数据】开始,不从上一次的结果接着筛 → 每次都从头开始
  // 先把原始数据整个复制一份当起点(slice 浅拷贝
  let result = originalUsers.slice();

  // 3. 条件一个一个叠加:哪个条件填了,就用它再筛一遍 → 剥洋葱,填了哪个剥哪层
  // 填了关键词:再按姓名筛
  if (keyword) result = result.filter(function (u) {
    return u.name.includes(keyword);
  });

  // 填了角色:再按角色筛
  if (role) result = result.filter(function (u) {
    return u.role === role;
  });

  // 选了状态:再按状态筛(转成数字再比)
  if (status !== "") result = result.filter(function (u) {
    return u.status === parseInt(status, 10);
  });

  // 4. 渲染 → 把最终结果画到表格上
  renderTable(result);
}

这套"条件累加"是面试常考、工作常用的标准写法。思路就像剥洋葱:先拿全部数据,填了哪个条件就剥一层,没填的就跳过。用 if (条件) 判断"这个条件到底生不生效",生效才 filter,不生效就原样保留。这样不管用户填了 1 个条件还是 3 个条件,代码都不用改,自动适配。

3.5 链式写法(和上面等价,更紧凑)

// 从原始数据开始
const result = originalUsers
  // 关键词没填就直接放行,填了才按姓名筛
  .filter(function (u) {
    return !filters.keyword || u.name.includes(filters.keyword);
  })
  // 角色没填就放行,填了才按角色筛
  .filter(function (u) {
    return !filters.role || u.role === filters.role;
  })
  // 状态没选就放行,选了才按状态筛
  .filter(function (u) {
    return filters.status === "" || u.status === parseInt(filters.status, 10);
  });

这是把"条件为空就放行"用短路运算写进 filter 里:!条件 || 判断。意思是——如果条件是空的(!条件 为 true),整个表达式直接 true,这条就放行;只有条件非空时,才去做真正的判断。看不懂没关系,3.4 的 if 累加写法是首选,这段了解即可。

3.6 表头排序(sort + 升降序切换)

// 记录当前排哪列、升还是降
let currentSort = { field: "", order: "" };

// 传数据、排哪列、升还是降
function sortData(data, field, order) {
  // 两两比较排序
  return data.sort(function (a, b) {
    // 这一列是文字(姓名)
    if (typeof a[field] === "string") {
      // 字符串:用 localeCompare 按字典序(中文也能正
      // 升序正着比,降序反着比
      return order === "asc" ? a[field].localeCompare(b[field]) : b[field].localeCompare(a[field]);
    }
    // 数字直接相减
    // 升序小在前,降序大在前
    return order === "asc" ? a[field] - b[field] : b[field] - a[field];
  });
}

排序的小规矩:点同一个表头就反向(升变降、降变升),点新表头默认升序。字符串(比如姓名)不能直接用减法,要用 localeCompare,否则中文会排错。还有,排序前先 [...数组] 拷贝一份,因为 sort 会改原数组,别把原始数据排乱了。

3.7 后端搜索(把条件发给接口)

// 找后端要筛选结果
$.ajax({
  // 搜索接口地址
  url: "/api/users/search",

  // 用 GET 方式,参数拼在网址后面
  type: "GET",

  // 把用户填的条件打包发过去
  data: {
    // 关键词
    keyword: params.keyword,
    // 角色
    role: params.role,
    // 第几页,没传就默认第 1 页
    page: params.page || 1,
    // 每页几条,没传就默认 10 条
    pageSize: params.pageSize || 10
  },

  // 后端把结果送回来
  success: function (res) {
    // 0 表示成功
    if (res.code === 0) {
      // 把后端返回的这页列表画出来
      renderTable(res.data.list);
      // 后端还给了总条数,据此画分页
      renderPagination(res.data.total);
    }
  }
});

后端搜索时,前端只是个"传话筒":把用户填的条件拼成 query 参数发给服务器,服务器在数据库里跑 SQL,只把符合条件的结果返回给前端渲染。前端不负责筛选逻辑本身。

3.8 进阶:抽成通用筛选器(逻辑迁移)

// utils/filter.js —— 用户列表、商品列表、订单列表都能复用 → 写一次到处用
// 传数据 + 一组筛选条件
function applyFilters(data, filters) {
  // 逐个数据挑(普通 function)
  return data.filter(function (item) {
    // 必须【全部】条件都满足才算留下(普通 function)
    return filters.every(function (filter) {
      // 这条条件:筛哪个字段(拆开取,不用解构)
      let field = filter.field;
      // 用什么方式
      let type = filter.type;
      // 值是多少
      let value = filter.value;
      // 空条件放行 → 没填就不算数
      if (value === "" || value === null || value === undefined) return true;
      // 取出这条数据对应字段的值
      const v = item[field];
      // 看这条条件是哪种筛法
      switch (type) {
        // 模糊搜索(包含)
        case "like":
          // 都转成小写再比包含
          return String(v).toLowerCase.includes(String(value).toLowerCase);
        // 完全相等
        case "equal":
          // 值必须一模一样
          return v === value;
        // 数值范围
        case "range":
          // 在最小值和最大值之间
          return value[0] <= v && v <= value[1];
        // 日期范围
        case "dateRange":
          // 在起始日期和结束日期之间
          return v >= value[0] && v <= value[1];
        // 在某几个选项里
        case "in":
          // 值只要在这个候选列表里就行
          return value.includes(v);
        // 不认识的筛法
        default:
          // 一律放行
          return true;
      }
    });
  });
}

// 页面只需要配置:筛哪个字段、用什么方式、值从哪来 → 不用重写逻辑,配一下就行
const userConfig = [
  // 姓名:模糊搜,取输入框(普通 function)
  { field: "name", type: "like", getValue: function () { return $("#kw").val().trim(); } },
  // 角色:完全相等,取下拉框
  { field: "role", type: "equal", getValue: function () { return $("#roleSel").val(); } }
];

这就是"逻辑迁移":变的是字段,不变的是筛选逻辑。把筛选逻辑写成一个通用函数,新页面只要配一下"筛哪个字段、怎么筛"就行,不用重写一遍。工作里写多了你就会不自觉地这么抽。

3.9 实战:用在哪 / 常见坑 / 怎么解决

① 可能在什么地方用:后台用户管理(按角色/状态/关键词筛)、商品列表(按分类/价格区间/销量排序)、订单列表(按状态/时间范围筛)——凡是"表格上带筛选框和排序表头"的页面都是这套。

② 常见的问题:越筛越少(在已筛结果上接着筛,没用原始数据);排序后原始数据被改乱(忘了 slice/拷贝 再 sort);中文排序乱掉(用了减法而不是 localeCompare);状态是数字 0/1 却拿字符串比("0" !== 0);条件填了空格没生效(忘了 trim)。

③ 解决思路:applyFilters 里把收集到的三个条件和最终 result 的长度打出来,看是不是从原始数据开始筛的;排序前 console.log 一下原数组,确认没被改;数字字段记得 parseInt 转一下再比;筛选没结果时,先单独测一个条件,看是哪个条件把数据全干掉了。

一句话:小数据前端 filter,大数据后端请求;多条件用"条件累加"(从原始数据开始、if 判断生效),排序前先拷贝、中文用 localeCompare

本页重点

API作用参数怎么传返回/结果代码示例
filter挑出符合条件的留下(条件函数)新数组,原数组不动let r = users.filter(function(u) { return u.age >= 30; });
sort给列排序(会改原数组)(a,b)=>比大小排好的数组data.sort(function(a,b){ return a.age - b.age; });
localeCompare中文按字典序比大小("张三","李四")正/负/0a.name.localeCompare(b.name, "zh");
parseInt状态 "1"/"0" 转数字(值, 10)数字let s = parseInt(status, 10);
trim去掉输入框两头空格()干净的字符串let kw = $("#kw").val().trim();