← 返回目录

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

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

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

姓名 角色 年龄 状态

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

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

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

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

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

filter // 筛出符合条件的元素,返回【新数组】(不改原数组)→ 挑出留下的,原数组不动
find // 找出【第一个】符合条件的元素(只返回一个)→ 只找一个
some // 有没有【至少一个】符合 → true / false → 有一个满足就行
every // 是不是【所有】都符合 → true / false → 全都满足才算
sort // 排序(⚠️ 会【改原数组】,记得先拷贝)→ 直接在原数组上排,危险

// 单条件:年龄 ≥ 30 → 只要 30 岁及以上
const result = users.filter(user => user.age >= 30);
// 多条件叠加:北京 且 年龄 ≥ 25 → 既要在北京、又要 25 岁以上
const result2 = users.filter(user => 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. 永远从【原始数据】开始,不从上一次的结果接着筛 → 每次都从头开始
  let result = [...originalUsers];     // 先把原始数据整个复制一份当起点

  // 3. 条件一个一个叠加:哪个条件填了,就用它再筛一遍 → 剥洋葱,填了哪个剥哪层
  if (keyword) result = result.filter(u => u.name.includes(keyword));   // 填了关键词:再按姓名筛
  if (role) result = result.filter(u => u.role === role);                // 填了角色:再按角色筛
  if (status !== "") result = result.filter(u => u.status === parseInt(status)); // 选了状态:再按状态筛(转成数字再比)

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

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

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

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

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

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

let currentSort = { field: "", order: "" }; // 记录当前按哪列、升还是降 → 记住现在排的是哪列、正还是反

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

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

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

$.ajax({                            // 找后端要筛选结果
 url: "/api/users/search",          // 搜索接口地址
 type: "GET",                       // 用 GET 方式,参数拼在网址后面
 data: {                            // 把用户填的条件打包发过去
  keyword: params.keyword,           // 关键词
  role: params.role,                // 角色
  page: params.page || 1,           // 第几页,没传就默认第 1 页
  pageSize: params.pageSize || 10   // 每页几条,没传就默认 10 条
 },
 success: function (res) {          // 后端把结果送回来
  if (res.code === 0) {             // 0 表示成功
   renderTable(res.data.list);      // 把后端返回的这页列表画出来
   renderPagination(res.data.total); // 后端还给了总条数,据此画分页
  }
 }
});
	

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

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

// utils/filter.js —— 用户列表、商品列表、订单列表都能复用 → 写一次到处用
function applyFilters(data, filters) {       // 传数据 + 一组筛选条件
  return data.filter(item => {              // 逐个数据挑
    return filters.every(filter => {         // 必须【全部】条件都满足才算留下
      const { field, type, value } = filter; // 这条条件:筛哪个字段、用什么方式、值是多少
      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 &amp;&amp; v <= value[1]; // 在最小值和最大值之间
        case "dateRange":                     // 日期范围
          return v >= value[0] &amp;&amp; v <= value[1]; // 在起始日期和结束日期之间
        case "in":                            // 在某几个选项里
          return value.includes(v);           // 值只要在这个候选列表里就行
        default:                              // 不认识的筛法
          return true;                        // 一律放行
      }
    });
  });
}

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

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

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

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

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

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

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