主题
渲染连续页码与优化连续页码(Lesson17 3/3)
本页对应 Lesson17,共 5 段视频,约 1 小时 19 分。
学习目标
- 能根据当前页、总页数和连续页码数,计算连续页码的起始位置与结束位置,并处理边界越界情况。
- 能使用循环渲染连续页码,并正确实现当前页高亮、首页/上一页/下一页/末页及省略号的条件显示。
- 能优化连续页码在总页数少于连续页码数、当前页靠前或靠后等场景下的显示逻辑。
- 能通过自定义事件和路由查询参数实现页码点击切换,并支持指定当前页与每页显示条数。
- 能定位并修复搜索条件、标签或排序变更后页码未回到第一页的 BUG。
- 能定位并修复点击分类时误删其他搜索条件的 BUG,确保仅移除与分类相关的查询条件。
知识点
一、先明确分页组件的数据流
连续页码的目标是:在“上一页、下一页、总页数”之间,渲染一段连续页码,例如当前第 16 页、连续显示 5 个,就显示 14、15、16、17、18。
为什么需要:页码不能只显示当前页,也不能把总页数全部铺开;需要根据 pageNo、continue、total、pageSize 算出连续区间。
怎么想:先算总页数 pageSum = Math.ceil(total / pageSize);再根据当前页 pageNo 和连续条数 continue 算起始 start、结束 end。continue 一般取奇数,这样当前页左右对称,标红时视觉更自然。
怎么做:当前页 16、continue=5 时,左右各 (5-1)/2=2 页,所以 start=16-2=14,end=16+2=18。公式为:
js
start = this.pageNo - (this.continue - 1) / 2
end = this.pageNo + (this.continue - 1) / 2易错点:continue 不要传偶数;pageNo 是父组件传入的 props,子组件不要直接修改;公式里用的是“连续条数”,不是 pageSize。
二、用计算属性得到 startAndEnd
为什么需要:模板里要反复使用起止页码,放在 computed 里最合适,数据变化会自动重算。
怎么做:在 Pagination.vue 中定义 startAndEnd,返回 { start, end }。笔记里先用 0 初始化,再计算并返回,调试时用 <h3>{{ startAndEnd }}</h3> 看对象结果。
易错点:不要拆成两个计算属性再互相依赖,没必要;调试用的 h3 最后应删除;startAndEnd 只是计算结果,不要在这里发请求。
三、渲染连续页码的三种写法
笔记保留了三种方案:
v-for="page in (end - start + 1)",显示start + page - 1。因为page从 1 开始,所以要减 1。v-for="(_, index) in (end - start + 1)",显示start + index。index从 0 开始,所以不加不减。- 直接
v-for="page in startAndEnd.end",再用v-show="page >= startAndEnd.start"过滤。笔记最终采用这种。
为什么需要理解三种:第一种和第二种是“按数量遍历”,第三种是“先遍历到结束页,再隐藏小于起始页的按钮”。第三种能工作,但会渲染一部分被隐藏的按钮。
易错点:v-for 的 page 从 1 开始,index 从 0 开始;active 判断应写 pageNo === page;不推荐 v-for 和 v-if 同时使用,因为会先遍历再卸载,效率不高;v-show 只是隐藏,DOM 仍在。
四、边界优化:总页数少、起始小于 1、结束超过总页数
为什么需要:如果总页数只有 4 页,而 continue=5,按原公式会算出 14 到 18,明显错误;当前页为 1 或最后一页时,也会算出越界区间。
怎么想:先判断 continue > pageSum,此时没有“省略号”的必要,直接 start=1、end=pageSum。否则再处理左右越界:
- 若
start < 1:start = 1,end = start + continue - 1。 - 若
end > pageSum:end = pageSum,start = end - continue + 1。
易错点:end = start + continue - 1 必须减 1,否则会多显示一个;右越界时 start = pageSum - continue + 1,不是减 continue。
省略号与首尾按钮的显示条件也要配套:第一页按钮在 start > 1 时显示;左省略号在 start > 2 时显示;最后一页按钮在 pageSum > end 时显示;右省略号在 pageSum > end + 1 时显示。上一页在 pageNo === 1 时禁用,下一页在 pageNo === pageSum 时禁用。
五、页码点击与父组件通信
为什么需要:Pagination 是封装组件,不能把 router.push 写死在里面,否则其他页面无法复用。
怎么做:子组件点击页码时 $emit('change-page', page);上一页传 pageNo - 1,下一页传 pageNo + 1,第一页传 1,最后一页传 pageSum。父组件监听事件,在方法里跳转并保留原查询条件:
js
changePage(page) {
this.$router.push({
path: '/search',
query: { ...this.$route.query, pageNo: page }
})
}易错点:跳转时必须展开原 query,否则标签、排序等条件会丢失;页码变化后要重新请求列表;watch 里可同步 pageNo、pageSize,但以 query 传入值为准。
六、调试中遇到的两个 bug
第一个 bug:搜索条件、分类、排序改变后,页码没有回到第一页。原因是跳转时没有覆盖 pageNo。解决:所有会改变搜索条件的跳转都加上 pageNo: 1。
第二个 bug:点击分类会把其他搜索条件删掉。原因是跳转时只保留了部分 query。解决:用 ...this.$route.query 保留原条件,再覆盖分类参数和 pageNo: 1;删除分类时只删分类相关参数,标签、排序等继续保留。
七、本节组件代码
以下保留笔记中的 Pagination.vue 代码,调试用的 h3 可在最后删除:
vue
<template>
<div class="pagination">
<button>上一页</button>
<button>1</button>
<span>···</span>
<!--1-->
<!-- <button :class="{active:(pageNo===startAndEnd.start+page-1)}" -->
<!-- v-for="page in (startAndEnd.end - startAndEnd.start+1)">-->
<!-- {{startAndEnd.start+page-1}}-->
<!-- </button>-->
<!--2-->
<!-- <button :class="{active:(pageNo===startAndEnd.start+index)}"-->
<!-- v-for="(_,index) in (startAndEnd.end - startAndEnd.start+1)">-->
<!-- {{startAndEnd.start+index}}-->
<!-- </button>-->
<!--3-->
<button :class="{active:pageNo===page}" v-show="page >=startAndEnd.start" v-for="page in startAndEnd.end" :key="page">
{{page}}
</button>
<span>···</span>
<button>{{pageSum}}</button>
<button>下一页</button>
<span>共 {{total}} 条</span>
<div>
<h3>
{{startAndEnd}}
</h3>
</div>
</div>
</template>
<script>
export default {
props:["total","pageSize","continue","pageNo"],
name: "index",
computed:{
pageSum(){
return Math.ceil(this.total/this.pageSize)
},
// 计算起始与结束位置
startAndEnd(){
let start = 0;
let end = 0;
// pageNo = 16,start==>14 end=18
start = this.pageNo-(this.continue-1)/2;
end = this.pageNo + (this.continue-1)/2
return {start,end}
}
}
}
</script>先明确:连续页码要解决什么问题
当总页数很多时,不能把所有页码都铺出来,否则页面又长又难用。通常只显示连续的一小段页码,并让当前页尽量居中;左侧用“1 + …”提示还能回第一页,右侧用“… + 最后一页”提示还能去末尾。这里连续页码一般取奇数,转写稿中取 continue = 5,因为当前页居中时左右对称,标红位置更好看。
核心不是直接写死 1 2 3 4 5,而是根据 pageNo、continue、pageSum 算出 start 和 end。例如总页数 21、当前页 16、连续 5 个,应得到 14 15 16 17 18,即 start=14、end=18。
计算起止位置:先推公式,再补边界
为什么需要:模板要知道从第几页渲染到第几页。怎么想:连续 5 个,当前页居中,左右各 (5-1)/2=2 个,所以 start=pageNo-2、end=pageNo+2。怎么做:写成 startAndEnd 计算属性,返回 {start,end}。转写稿中先让 start=0、end=0 渲染调试,看到 0/0 后代入 pageNo=16,推出 14 和 18,再补三类边界:
continue > pageSum:总页数不够连续页数,直接start=1、end=pageSum;start < 1:左边越界,令start=1,end=start+continue-1;end > pageSum:右边越界,令end=pageSum,start=pageSum-continue+1。
易错点:continue 一般用奇数;公式括号别写错;总页数小于连续页数时必须单独处理,否则会出现 14 15 16 17 18 但总页数只有 4 的荒唐结果。
本节保留代码
vue
<template>
<div class="pagination">
<button :disabled="pageNo===1">上一页</button>
<!-- 起始页数start大于1 -->
<button v-show="startAndEnd.start>1">1</button>
<span v-show="startAndEnd.start>2">···</span>
<button :class="{active:(pageNo===startAndEnd.start+index)}"
v-for="(_,index) in (startAndEnd.end - startAndEnd.start+1)">
{{ startAndEnd.start + index }}
</button>
<span v-show="pageSum>startAndEnd.end+1">···</span>
<button v-show="pageSum>startAndEnd.end">{{ pageSum }}</button>
<button :disabled="pageNo===pageSum">下一页</button>
<span>共 {{ total }} 条</span>
<div>
<h3>
{{ startAndEnd }}
</h3>
</div>
</div>
</template>
<script>
export default {
props: ["total", "pageSize", "continue", "pageNo"],
name: "index",
computed: {
pageSum() {
return Math.ceil(this.total / this.pageSize)
},
// 计算起始与结束位置
startAndEnd() {
let start = 0;
let end = 0;
// 如果连续页数大于总页数
if (this.continue > this.pageSum) {
start = 1;
end = this.pageSum;
} else {
// pageNo = 16,start==>14 end=18
start = this.pageNo - (this.continue - 1) / 2;
end = this.pageNo + (this.continue - 1) / 2
if (start < 1) {
start = 1;
end = start + this.continue - 1;
}
if(end>this.pageSum){
end = this.pageSum;
start = this.pageSum-this.continue+1;
}
}
return {start, end}
}
}
}
</script>说明:模板里的 h3 是转写稿中的调试输出,正式使用可删除。
渲染连续页码与省略号
为什么需要:有起止位置后,v-for 的长度应是 end-start+1,不能直接用 end,否则会多渲染。怎么做:循环 index 从 0 开始,显示 start+index;高亮判断 pageNo === start+index。左侧“1”在 start>1 时显示,左省略号在 start>2 时显示;右侧省略号在 pageSum>end+1 时显示,最后一页在 pageSum>end 时显示。易错点:转写稿特别提醒不要 v-if 和 v-for 混在同一元素上,先遍历再卸载效率低;更推荐本笔记这种按长度遍历,或先计算过滤后的页码数组。
接入点击与真实数据
页码组件不应写死 router.push,否则复用性差。转写稿中的做法是:子组件点击页码时 $emit("changePageName", 页码),父组件监听后统一跳转,并在 query 中保留原条件、设置 pageName。搜索条件、分类、排序发生变化时,要把 pageName 重置为 1,否则会出现“条件变了还在第 2 页”的 bug。分类点击时不要只保留 keyword,否则会把品牌、标签等条件全删掉;全部结果统计里还要过滤掉 pageName,避免把页码当搜索条件。
组件分工:子组件只负责发事件,父组件决定路由与请求
为什么需要:分页组件要能被搜索页、订单页等复用,不能把 $router.push 写死在分页组件里,否则组件就和具体页面绑死了。
怎么想:Pagination 只接收 total、pageNo、pageSize、continue,渲染页码;用户点击时通过自定义事件 change-page-no 把目标页码交给父组件。
怎么做:在 src/components/Pagination/index.vue 中,上一页传 pageNo-1,下一页传 pageNo+1,数字页码传 startAndEnd.start + index。
易错点:不要在分页组件内部直接跳路由;事件名、参数要统一,父组件才能稳定接收。
vue
<template>
<div class="pagination">
<button @click="$emit('change-page-no',pageNo-1)" :disabled="pageNo===1">上一页</button>
<!-- 起始页数start大于1 -->
<button @click="$emit('change-page-no',1)" v-show="startAndEnd.start>1">1</button>
<span v-show="startAndEnd.start>2">···</span>
<button @click="$emit('change-page-no',startAndEnd.start + index)" :class="{active:(pageNo===startAndEnd.start+index)}"
v-for="(_,index) in (startAndEnd.end - startAndEnd.start+1)">
{{ startAndEnd.start + index }}
</button>
<span v-show="pageSum>startAndEnd.end+1">···</span>
<button @click="$emit('change-page-no',pageSum)" v-show="pageSum>startAndEnd.end">{{ pageSum }}</button>
<button @click="$emit('change-page-no',pageNo+1)" :disabled="pageNo===pageSum">下一页</button>
<span>共 {{ total }} 条</span>
</div>
</template>连续页码的起止位置怎么算
为什么需要:不能把所有页码都铺出来,也不能只显示当前页;要显示“当前页附近固定数量”的连续页码。
怎么想:设连续页码数为 continue,一般取奇数,保证当前页左右对称。转写稿中的推导是:当前页 16、连续 5 个,左边 2 个、右边 2 个,所以起始 start = 16 - (5-1)/2 = 14,结束 end = 14 + 5 - 1 = 18。
怎么做:在 Pagination 中用计算属性 startAndEnd 返回 { start, end },模板只使用 startAndEnd.start/end;总页数由 total 和 pageSize 得到 pageSum。
易错点:公式里“连续数减 1 再除以 2”容易漏括号;结束位置是 start + continue - 1,不是 start + continue,否则会多显示一页。
渲染连续页码与当前页高亮
为什么需要:起止位置只是数据,还要循环渲染按钮,并让当前页有 active 样式。
怎么想:转写稿里试过三种写法,推荐用下标方案:v-for="(_, index) in (end - start + 1)",页码是 start + index。这样不用额外维护计数器。
怎么做:按钮点击时 $emit('change-page-no', startAndEnd.start + index);高亮条件写成 pageNo === startAndEnd.start + index。
易错点:不要用 v-if 和 v-for 同时控制连续页码的显示隐藏。转写稿明确提醒:这样会先渲染再卸载,效率低;应在循环范围或数据层处理边界。
边界优化:总页数少、起始小于 1、结束超过总页数
为什么需要:真实数据下会出现总页数只有 4 页却要连续显示 5 页;当前页为 2 时起始可能算成 0;当前页为 20 时结束可能超过总页数。
怎么想:转写稿的调试过程是:总页数小于连续页数时,直接显示 1...pageSum;start < 1 时令 start = 1,再令 end = start + continue - 1;end > pageSum 时令 end = pageSum,再令 start = pageSum - continue + 1。
怎么做:把这三个判断放在 startAndEnd 计算属性里,模板不用堆判断。
易错点:修正 start=1 后,end 要重新算,否则会多显示一页;修正 end=pageSum 后,start 也要反推,保证仍然显示 continue 个页码。
父页面接收事件:改路由 query,再触发请求
为什么需要:页码变化后要重新请求列表,并且刷新页面时还能保持当前页。
怎么想:Search/index.vue 监听 $route.query,从 store 取 pageNo、pageSize,再 dispatch("product/postProductListAsync", {...})。点击页码时用 $router.push 把新 pageNo 合并进 query。
怎么做:父组件绑定 @change-page-no="changePageNo",方法里保留原 query:...this.$route.query, pageNo。
易错点:不要直接覆盖整个 query,否则搜索关键字、品牌等条件会丢;watch 要加 immediate:true,首次进入页面也能请求。
vue
<template>
<!-- 分页组件-->
<Pagination :continue="5"
@change-page-no="changePageNo"
:total="$store.state.product.searchProductResult.total"
:pageNo="$store.state.product.searchProductResult.pageNo"
:pageSize="$store.state.product.searchProductResult.pageSize"></Pagination>
</template>
<script>
export default {
methods:{
changePageNo(pageNo){
console.log("changePageNo",pageNo);
this.$router.push({
path:"/search",
query:{
...this.$route.query,
pageNo
}
})
}
},
watch:{
"$route.query":{
handler(query){
// console.log("query",query);
const {pageNo,pageSize} = this.$store.state.product.searchProductResult;
// 指定页码以及每页显示的条数
this.$store.dispatch("product/postProductListAsync",{
pageNo,
pageSize,
...query
})
},
immediate:true
}
},
}
</script>调试补充:搜索条件变化要回到第一页
为什么需要:转写稿中出现的 bug 是:已经点到第 2 页,再删除分类或切换排序,页码仍停在 2,列表可能为空或错位。
怎么想:搜索条件变了,原页码没有意义,应回到第 1 页。
怎么做:在删除分类、切换排序、切换标签等会改 query 的跳转里,合并 pageNo: 1;移除分类时只删除分类相关参数,保留品牌、标签等其他 query。展示“全部结果”时还要过滤掉 pageNo/pageSize 这类分页参数,避免被当成搜索条件。
易错点:不要为了删一个分类把整个 query 清空;也不要在条件变化后继续沿用旧页码。
问题现象:删掉分类标签后页码没有回到第一页
为什么需要:搜索页的页码不是孤立状态,它依赖当前搜索条件。比如用户已经在第 15 页,此时删掉“台灯”这个分类标签,搜索结果集合已经变了,如果页码还停在 15,就可能出现空白页、页码错乱,或者用户以为“没有数据”。所以只要搜索条件发生更改,页码就应该回到第 1 页。
怎么想:不要直接改 this.$route.query。路由查询参数是当前地址的一部分,正确做法是先复制一份 query,在副本上删除与“类别标签”相关的字段,再通过 this.$router.push 跳转,并在新 query 中把 pageNo 覆盖成 1。这样既保留了关键词、品牌、排序等其他条件,又只移除当前要删掉的类别条件。
怎么做:在 src/pages/Search/index.vue 中处理移除类别标签:
js
// 移除类别标签
moveCategoryName(){
// 1- category3Id=249&categoryName=台灯
const query = {...this.$route.query};
delete query.categoryName;
delete query.category3Id;
delete query.category2Id;
delete query.category1Id;
// console.log(this.$route.query)
this.$router.push({
path:"/search",
query:{
...query,
pageNo:1
}
})
},这里的关键是 ...query 先把其他搜索条件带回来,后面的 pageNo:1 再覆盖旧页码。展开顺序不能反,否则旧页码可能又把 1 覆盖掉。
易错点:只删除 categoryName,却忘了删除 category1Id/category2Id/category3Id,URL 里仍会残留类别条件;直接写 this.$route.query.pageNo = 1,这只是改对象,不一定触发路由跳转;把整个 query 清空,会误删关键词、品牌、标签、排序等条件。转写稿里就出现过“删分类后所有条件都没了”的问题,原因是当时只保留了 keyword 一类字段,而不是“只删类别相关字段、保留其他字段”。
调试过程:为什么页码还停在第 2 页
为什么需要:仅删除类别字段还不够。转写稿中演示过:先点到第 2 页,再删掉分类标签,页面仍然停在 pageNo=2,因为删除条件时没有显式重置页码。页码是 query 的一部分,不写 pageNo:1,它就会沿用旧值。
怎么想:把“条件变化”和“页码重置”看成一次完整导航:新条件 + 新页码。只要本次操作会改变结果集,就应该在跳转时把 pageNo 设为 1。
怎么做:如上面的代码,在 query 对象最后加 pageNo:1。调试时可以观察地址栏:删除前是 ?categoryName=台灯&pageNo=2,删除后应变成保留其他条件且 pageNo=1。
易错点:只在删除 categoryName 时重置页码,却漏掉删除 category3Id 等字段;或者把 pageNo 写在 ...query 前面,导致被旧值覆盖。另一个常见问题是分类点击、排序切换、搜索关键词变化时也要回第 1 页,转写稿提到排序和分类跳转同样要加 pageNo:1,否则也会出现“条件变了但页码没变”的 bug。
相关延伸:移除标签不要误删其他搜索条件
为什么需要:搜索页往往同时有类别、品牌、关键词、标签、排序等条件。用户删“手机”这个类别,不应该把已经选中的品牌“vivo”或标签一起删掉。
怎么想:采用“白名单式删除”不如“黑名单式删除”稳妥:明确只删除类别相关字段,其他字段通过 ...query 保留。转写稿中对比过:早先只保留 keyword,会把其他条件全干掉;后来改成只删 categoryName 和各级 categoryId,其他条件就能保留。
怎么做:保持 const query = {...this.$route.query},只对类别字段执行 delete,再合并 pageNo:1 跳转。若分类点击也要保留其他条件,同样应在原 query 基础上合并新条件并重置页码,而不是重新构造一个只剩分类 id 的 query。
易错点:把“移除类别标签”写成“清空搜索条件”;忘记 delete 多个层级 id;跳转后没有检查地址栏和列表请求参数。调试时建议先看 URL 的 query,再看接口请求里的页码和条件,最后看列表是否回到第一页,这样能快速定位是“条件没删干净”还是“页码没重置”。
BUG现象:条件变了,页码却没回到第一页
这个 BUG 的表现是:用户已经翻到第 15 页,此时删除某个分类标签、重新搜索,或点击“价格/综合”排序,列表条件已经变了,但请求里仍然带着旧的 pageNo=15,于是出现空列表、错页,或者看起来“筛选没生效”。
为什么需要:分页页码表示“在当前结果集中的第几页”。搜索词、标签、排序一旦变化,结果集就变了,旧页码没有意义。
怎么想:不要把它当成分页组件的问题,而要先看请求参数。转写稿里的复现过程很典型:在第 15 页删掉分类后,页面并没有回到第 1 页;点击排序后,页码也不是 1。根因就是条件变更时只改了搜索/标签/排序参数,没有同时重置当前页。
怎么做:先确认所有会改变查询条件的入口:搜索提交、标签添加/删除、排序切换。它们跳转或更新 query 时,都要带上当前页为 1。
易错点:只在搜索里加,漏掉标签和排序;或者只改了页面显示,没有改真正发请求的 query。
修复原则:改条件就回第一页
本节笔记给出的方案很明确:
解决BUG方案:进行跳转时增加
pageNo:1
为什么需要:把规则统一成“条件变更 → 回第一页”,可以避免每个入口各写一套逻辑,也避免用户在新条件下停留在不存在的页码。
怎么想:翻页操作只改 pageNo;搜索、标签、排序属于“条件变更”,要同时把 pageNo 覆盖为 1。二者不要混在一起。
怎么做:在路由跳转时合并原 query,并覆盖 pageNo:
js
this.$router.push({
path: '/search',
query: {
...this.$route.query,
pageNo: 1
}
})转写稿里说的“配置name/配置单”指的就是当前页参数,“配置s/配置袋子”指每页条数。实际项目里如果当前页参数叫 pageName,就按项目命名写,但本节笔记统一记为 pageNo:1。
易错点:对象展开顺序要注意。若写成 { pageNo: 1, ...this.$route.query },旧的 pageNo 可能把 1 覆盖掉。应让 pageNo: 1 放在展开之后。
三个入口逐一落地
搜索:提交搜索词时,除了写搜索参数,还要加 pageNo: 1。
标签:添加或删除标签时也要加 pageNo: 1。转写稿还提到一个相关坑:删除标签时如果只保留 qword,会把品牌、标签等其他条件一起删掉;正确做法是只删除和当前类别相关的条件,保留其他 query,并补上 pageNo: 1。
排序:点击综合、价格等排序入口时,同样在 query 中加 pageNo: 1。示意如下,排序参数按项目实际字段填:
js
this.$router.push({
path: '/search',
query: {
...this.$route.query,
// 这里放本次变更的搜索/标签/排序参数
pageNo: 1
}
})易错点:标签删除不要误删其他筛选条件;排序切换不要只改排序字段;全部结果统计要过滤掉 pageNo、pageSize 这类分页参数,否则“全部结果”数量会被页码影响。转写稿中检查“全部结果”时也发现要把 pageNo 过滤掉。
调试与验证
为什么需要:这个 BUG 很容易被“页面看起来变了”迷惑,必须看请求参数。
怎么想:按“复现 → 看 query → 看请求 → 改跳转处 → 回归三个入口”的顺序排查。
怎么做:在第 15 页删除分类,打开 Network 看请求里的 pageNo 是否变成 1;再点价格排序,看是否回第一页;再删标签,确认其他条件还在且 pageNo=1。最后点分页组件的页码,确认翻页时不会被错误重置。
易错点:分页组件点击页码是正常翻页,不应强制回 1;连续页码的边界优化不是本 BUG 的主修复,别把两件事混在一起。验证时还要注意:搜索、标签、排序三个入口都要覆盖,不能只修一个。
小结
本节的修复可以压缩成一句话:凡是会改变搜索条件的跳转,都增加 pageNo: 1。这样搜索、切换标签、排序后都会从第一页重新请求,旧页码导致空列表或错页的问题就解决了。
先复现:点击分类为什么把标签和排序也删了
为什么需要:分页、分类、标签、排序都放在 URL 的 query 里。点击分类时如果直接覆盖 query,其他搜索条件就会丢失,用户会看到“只删了手机,vivo、苹果、4G 全没了”。
怎么想:先按转写稿复现:选手机 → 选 vivo → 再点第二页;然后删掉手机分类。此时不仅分类没了,vivo 也没了。再试“手机 → 苹果 → 4G”,删手机后苹果和 4G 同样消失。说明不是单个字段问题,而是移除条件时把整个 query 重写了。
怎么做:打开处理“移除内部标签”的方法,检查它是否只保留了 qWord 之类的单个字段。转写稿里明确说:“当时是不是只保留了一个 Qword,其他是不是都没有保留。”
易错点:不要把“删除分类”写成“重建 query”。删除一个条件,应当是“复制原 query → 删除分类相关字段 → 跳转”,而不是 query = { qWord: ... }。
根因定位:移除条件时不能整体覆盖 query
为什么需要:URL query 是多个搜索条件的共同载体,分类只是其中一类。
怎么想:把 query 看成一张表:qWord、分类、品牌、标签、排序、pageName 各占一列。删分类只能删分类列,不能把整张表换掉。
怎么做:保留原 query,只删和类别相关的参数。关键代码示意如下,字段名按你项目实际替换:
js
// 移除分类条件时:保留其它搜索条件
const query = { ...this.$route.query }
// 只删除分类/类别相关字段
delete query.categoryName
// 如有多个类别字段,一并删除
// delete query.categoryId
this.$router.push({ path: '/search', query })易错点:不要写成 this.$router.push({ query: { qWord: this.qWord } }),这样标签、排序、页码都会被清掉。转写稿里验证过:改成“只删类别相关,保留其他”后,删手机时 vivo 还在,苹果、4G 也还在。
连带问题:搜索条件变化后页码必须回到第一页
为什么需要:用户在第 3 页删掉分类后,结果集已经变了,仍停在第 3 页可能空白或错位。转写稿的调试现象是:在第 2 页删条件,页码仍是 2;排序切“综合/价格”时也没有回第一页。
怎么想:凡是会改变结果集的操作——删分类、切标签、改排序、改搜索词——都应该把 pageName 重置为 1。
怎么做:在跳转时合并原 query,并显式写入 pageName: 1:
js
this.$router.push({
path: '/search',
query: {
...this.$route.query,
pageName: 1
}
})排序切换同理,在原有排序参数基础上加 pageName: 1。转写稿还提到:点击分类跳转时不要只保留部分项,要写 ...query 保留原条件,再加分类参数和 pageName: 1。
易错点:只加分类参数、不写 pageName,页码不会自动回第一页;只写 pageName: 1、不写 ...query,又会把标签和排序删掉。
收尾:全部结果计数要过滤 pageName 与空值
为什么需要:转写稿里删完条件后,“全部结果”区域仍显示异常,原因是计数时把 pageName 也当成了搜索条件,甚至把没有值的项也算进去。
怎么想:统计“全部结果”时,只统计真正的筛选条件;pageName 是分页参数,不算筛选条件;值为 undefined 的项也不应参与。
怎么做:在过滤逻辑中排除 pageName 和空值:
js
const keys = Object.keys(this.$route.query).filter(key => {
return key !== 'pageName' && this.$route.query[key]
})易错点:不要只过滤“没有值”的项,还要显式排除 pageName。转写稿的结论是:把 pageName 过滤掉后,全部结果区域才恢复正常。
作业:把面包屑导航抽成组件
为什么需要:面包屑要在多个搜索页复用,继续写在页面里会重复且难维护。
怎么想:组件只负责“展示当前条件 + 触发删除事件”,不直接写死跳转逻辑;删除后由父组件统一更新 query,并补 pageName: 1。
怎么做:父组件传入当前条件列表,子组件点击某项时 $emit('remove', 条件字段);父组件收到后复制原 query、删除对应字段、跳转。
易错点:不要在面包屑组件里直接整体覆盖 query;也不要把分类删除写成只保留 qWord,否则本节 BUG3 会再次出现。
课堂画面





代码示例
vue
<!-- src/components/Pagination/index.vue -->
<template>
<div class="pagination">
<button
:disabled="pageNo === 1"
@click="$emit('change-page-no', pageNo - 1)"
>
上一页
</button>
<!-- 起始页大于1时显示首页;大于2时显示左侧省略号 -->
<button
v-show="startAndEnd.start > 1"
@click="$emit('change-page-no', 1)"
>
1
</button>
<span v-show="startAndEnd.start > 2">···</span>
<!-- 连续页码:startAndEnd.start + index -->
<button
v-for="(_, index) in (startAndEnd.end - startAndEnd.start + 1)"
:key="startAndEnd.start + index"
:class="{ active: pageNo === startAndEnd.start + index }"
@click="$emit('change-page-no', startAndEnd.start + index)"
>
{{ startAndEnd.start + index }}
</button>
<!-- 右侧省略号与尾页 -->
<span v-show="pageSum > startAndEnd.end + 1">···</span>
<button
v-show="pageSum > startAndEnd.end"
@click="$emit('change-page-no', pageSum)"
>
{{ pageSum }}
</button>
<button
:disabled="pageNo === pageSum"
@click="$emit('change-page-no', pageNo + 1)"
>
下一页
</button>
<span>共 {{ total }} 条</span>
</div>
</template>
<script>
export default {
name: "Pagination",
props: ["total", "pageSize", "continue", "pageNo"],
computed: {
pageSum() {
return Math.ceil(this.total / this.pageSize);
},
startAndEnd() {
let start = 0;
let end = 0;
// 连续页数大于总页数时,直接显示 1 ~ pageSum
if (this.continue > this.pageSum) {
start = 1;
end = this.pageSum;
} else {
start = this.pageNo - (this.continue - 1) / 2;
end = this.pageNo + (this.continue - 1) / 2;
// 左侧越界:夹紧到第1页
if (start < 1) {
start = 1;
end = start + this.continue - 1;
}
// 右侧越界:夹紧到最后一页
if (end > this.pageSum) {
end = this.pageSum;
start = this.pageSum - this.continue + 1;
}
}
return { start, end };
}
}
};
</script>
<style scoped>
.pagination button.active {
color: red;
}
.pagination button:disabled {
cursor: not-allowed;
}
</style>要点解释:
pageSum = Math.ceil(total / pageSize)计算总页数,是连续页码边界判断的基础。startAndEnd先按pageNo ± (continue - 1) / 2计算左右边界,因此continue一般传奇数,保证当前页左右对称。- 必须处理三种边界:
continue > pageSum、start < 1、end > pageSum,否则会渲染出不存在的页码,例如总页数 4 却渲染 14~18。 v-for使用(_, index)时,页码是startAndEnd.start + index,不要直接拿index当页码。- 首尾页、省略号用
v-show控制;转写稿特别提醒不要v-for与v-if同级混用,避免先渲染再卸载造成无用功。 - 点击页码通过
$emit('change-page-no', ...)交给父组件处理,分页组件内部不要直接写$router.push,否则组件复用性变差。
vue
<!-- src/pages/Search/index.vue -->
<template>
<div class="search-page">
<Pagination
:continue="5"
@change-page-no="changePageNo"
:total="$store.state.product.searchProductResult.total"
:pageNo="$store.state.product.searchProductResult.pageNo"
:pageSize="$store.state.product.searchProductResult.pageSize"
/>
</div>
</template>
<script>
import Pagination from "@/components/Pagination/index.vue";
export default {
name: "Search",
components: { Pagination },
methods: {
changePageNo(pageNo) {
console.log("changePageNo", pageNo);
this.$router.push({
path: "/search",
query: {
...this.$route.query,
pageNo
}
});
},
// 移除类别标签:只删除 category 相关参数,并回到第1页
moveCategoryName() {
const query = { ...this.$route.query };
delete query.categoryName;
delete query.category3Id;
delete query.category2Id;
delete query.category1Id;
this.$router.push({
path: "/search",
query: {
...query,
pageNo: 1
}
});
}
},
watch: {
"$route.query": {
handler(query) {
const { pageNo, pageSize } =
this.$store.state.product.searchProductResult;
this.$store.dispatch("product/postProductListAsync", {
pageNo,
pageSize,
...query
});
},
immediate: true
}
}
};
</script>要点解释:
@change-page-no="changePageNo"接收分页组件抛出的页码,再通过路由 query 更新页码。query: { ...this.$route.query, pageNo }保留原有搜索条件,只覆盖pageNo;不要直接清空 query,否则标签、排序等条件会丢失。moveCategoryName中只删除categoryName / category3Id / category2Id / category1Id,并加pageNo: 1;转写稿指出不能只保留keyword,否则会把品牌、标签等其它条件一起删掉。- 搜索条件发生更改时页码应回到第 1 页,因此跳转时增加
pageNo: 1;排序、标签切换同理也要加pageNo: 1。 watch "$route.query"使用immediate: true,首次进入页面也会派发请求;...query放在后面,URL 中的pageNo会覆盖 store 中的默认页码。- 易错点:
total为 0 时pageSum为 0,本段沿用笔记逻辑,业务上需要额外兜底,避免显示“下一页”等异常状态。
常见错误
总页数小于连续页码数时仍按居中计算
现象→页码出现14~18、0~4等不合理区间。原因→未先判断continue > pageSum。修法→直接令start=1、end=pageSum,或先取continue=Math.min(continue,pageSum)。当前页靠前/靠后时区间越界
现象→出现0、负数页码或大于总页数的按钮。原因→只做居中计算,未钳制start/end。修法→start<1时令start=1、end=start+continue-1;end>pageSum时令end=pageSum、start=pageSum-continue+1。用
v-for + v-show过滤页码
现象→DOM 中先渲染全部页码再隐藏,调试可见大量隐藏按钮。原因→遍历完整范围后再隐藏,做了无用渲染。修法→用startAndEnd区间配合v-for只渲染连续页;不要靠v-show做页码过滤。在
Pagination内直接写this.$router.push
现象→组件只能在 Search 页使用,换订单页需改组件源码。原因→业务跳转写死在展示组件。修法→组件内只$emit('change-page-no', pageNo),由父组件处理路由与请求。搜索条件、品牌、标签、排序变化后未重置页码
现象→结果变少后列表为空,或页码仍停在旧页。原因→合并 query 时保留旧pageNo。修法→所有条件变更的跳转显式加pageNo:1,或统一走resetPageNo(query)。移除分类标签时重建 query 或删除全部 query
现象→删分类后品牌、标签、排序一起消失。原因→移除逻辑覆盖了整个 query。修法→const query={...this.$route.query}后只delete categoryName/category1Id/category2Id/category3Id,再push({...query,pageNo:1})。“全部结果”计数未过滤分页参数与空值
现象→条件数里多出pageNo、pageSize或undefined。原因→直接统计 query 的 key。修法→过滤pageNo/pageSize及undefined/null/''后再计数。
练习
- 为
startAndEnd写边界验收矩阵:total=100,pageSize=30,continue=5时pageNo=1/2/3/4的期望值;total=100,pageSize=10,continue=5时pageNo=1/2/9/10的期望值。要求覆盖continue>pageSum、start<1、end>pageSum三类情况。 - 把笔记中
v-for + v-show的页码方案重构为区间v-for,保留active、首尾1、首尾pageSum、左右省略号的显示逻辑,并写出各v-show条件。 - 将
Pagination改为受控组件:删除组件内路由跳转,只保留change-page-no事件;在父组件Search中实现changePageNo(pageNo),跳转时保留原 query 并覆盖pageNo。 - 实现统一重置页码:写一个
resetPageNo(query)辅助函数,并列出品牌切换、标签切换、排序切换、搜索提交、移除分类标签五处需要调用它的位置。 - 修复“全部结果”计数:写一个过滤函数,排除
pageNo、pageSize以及值为undefined/null/''的 query 字段,并用至少 3 组 query 验证。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 计算连接页码的起始与结束位置 | 09:04 |
| 2 | 渲染连续页码 | 10:57 |
| 3 | 优化连续页码 | 25:39 |
| 4 | 实现页码功能 | 17:30 |
| 5 | 解决BUG | 15:57 |
小结
- 分页组件验收先跑边界矩阵:首页、末页、总页数小于
continue、当前页靠前、当前页靠后。 startAndEnd必须始终满足1 <= start <= end <= pageSum,否则页码按钮一定出问题。continue建议约束为奇数,并在组件入口做兜底,避免非整数区间。- 展示组件不碰路由;页码变化只通过事件向上抛,父组件决定跳转与请求。
- 任何搜索条件变更都要统一重置
pageNo=1,不要依赖旧 query 残留。 - 省略号与首尾页按钮应由
start/end/pageSum联动控制,不要用隐藏 DOM 的方式凑效果。 - 复用检查:
Pagination只依赖total/pageSize/continue/pageNo和change-page-no事件即可工作。 - 面包屑、条件计数等派生 UI 要过滤分页参数和空值,避免把
pageNo当成搜索条件展示。