frontend-bug: 主题列表点搜索第一回不生效(路由守卫取消了自家请求)

这个提交包含在:
API Changelog Bot 2026-05-13 09:53:44 +08:00
父节点 81104fc6d9
当前提交 998d90d425

查看文件

@ -0,0 +1,115 @@
---
date: 2026-05-13
type: frontend-bug
module: admin-product-line-list
priority: high
notify: ["@mmg"]
status: pending
---
# 主题列表「点搜索第一回必然不好使,第二回才好使」— 路由守卫自己取消了自己发的请求
## 现象
管理后台「产品管理 → 主题列表」(`/product/line`),切换筛选条件(主题类型下拉或关键词)后点「搜索」按钮:
- **第 1 次点搜索:列表无任何变化**(看起来没反应)
- **第 2 次点同一个搜索:列表才刷新**
DevTools Network 面板可见:每次点搜索都发出 **2 个 URL 完全相同的请求**(如 `list?keyword=&productType=CORE&page=1&pageSize=20`
- 请求 1`status = (canceled)``size = 0.0 kB`,4ms
- 请求 2`status = 200``size = 1.2 kB`,61ms
100% 必现。
## 根因(纯前端,后端无关)
文件:`hl-ui/src/views/product/line/index.vue` + 通用的 `hl-ui/src/router/index.js` + `hl-ui/src/utils/request.js`
`handleSearchSubmit` 里干了两件事(顺序):
```js
const handleSearchSubmit = debounce(() => {
syncToUrl() // 1. 调 router.replace({ query }) 把筛选条件同步到 URL
resetAndLoad() // 2. 调 fetchList(),axios 发请求
}, 300)
```
`router/index.js``beforeEach` 守卫里每次路由跳转前会调 `clearAllPendingRequests()` 取消所有正在飞的 axios 请求(项目自带的请求清理机制,用来防止路由切换后还在跑的旧请求污染新页面)。
**时序冲突**
1. `syncToUrl()` 触发 `router.replace`,但 `beforeEach` 是**异步**触发的(微任务/下一拍)
2. `resetAndLoad()` **同步**立即调 axios 发出请求 A,加入 `pendingRequests` Map
3. 这一拍微任务跑到 `beforeEach``clearAllPendingRequests()`**把刚发出的请求 A 取消了**canceled
4. 但 `watch(route.query)``useSearchParams` 的 query 同步逻辑监听到 URL 变化,**又补发了请求 B**
5. 请求 B 这次没有再触发路由跳转,beforeEach 不再跑,请求 B 顺利 200 → 列表刷新
**所以用户看到的现象是:点 1 次搜索 → 2 个请求 → 第 1 个被自家路由守卫掐了 → 第 2 个才成功 → 因为防抖 300ms 的存在,用户感觉是"点了没反应",下一次手动再点才好使。**
实际上每次都是「第 1 个被取消、第 2 个成功」;但视觉上数据更新有延迟 + canceled 的请求让 loading 状态错乱,**用户主观感受就是「第一次不生效」**。
## 修复方案前端,2 选 1
### 改法 A推荐,最小改动`handleSearchSubmit` 里只发请求,不动 URL
URL 同步不是必须每次都做,可以从"搜索时同步"改成"组件卸载/页面切换时同步",或者干脆只用 `replaceState` 不触发 vue-router避免触发 beforeEach
```js
const handleSearchSubmit = debounce(() => {
// syncToUrl() // ❌ 删掉,或下面替换成静默 replaceState
// 静默版(如果还想保留 URL 可分享的能力):
// const url = new URL(window.location.href)
// Object.entries(searchForm).forEach(([k,v]) => v ? url.searchParams.set(k,v) : url.searchParams.delete(k))
// window.history.replaceState({}, '', url) // 绕过 vue-router,不触发 beforeEach
resetAndLoad()
}, 300)
```
`window.history.replaceState` 不会触发 vue-router 的 `beforeEach`,自然不会触发 `clearAllPendingRequests`
### 改法 B — `syncToUrl()` 后等 router.replace 完成的 Promise 再 `resetAndLoad()`
```js
const handleSearchSubmit = debounce(async () => {
await syncToUrl() // 让 router.replace 走完 beforeEach此时 pendingRequests 是空的,cancel 啥都没影响)
resetAndLoad() // 这次发出的请求不会被自己取消
}, 300)
```
需要 `syncToUrl()` 返回 `router.replace(...)` 的 Promise。
### 改法 C不推荐— 让 `clearAllPendingRequests` 不在 beforeEach 跑
这是改通用 request 拦截器,会影响所有页面的路由切换清理逻辑,风险大,**不推荐**。
## 推荐方案
**改法 A**`handleSearchSubmit` 内不用 vue-router 改 URL,直接 `window.history.replaceState` 静默同步。改动局限在 `product/line/index.vue` 一个文件,零副作用。
## 顺手检查
同一个团队规律:项目中其他列表页凡是用了「点搜索 → syncToUrl → fetchList」的双步模式都可能踩这个坑。建议 grep `syncToUrl` 找出所有调用点,统一改成方案 A 的写法。常见嫌疑列表:
- `views/order/list`(订单列表)
- `views/order-v2/list`(订单管理 v2
- `views/product-v2/list`(产品 v2 列表)
- `views/customer/list`(客户管理)
- `views/agency/list`(公司管理)
如果上述页面也有「点搜索第一次不灵」的反馈,根因 100% 一样。
## 验证清单(前端改完自测)
- [ ] 进入「主题列表」,切换主题类型下拉 → 点搜索 1 次 → 列表立即刷新Network 只有 1 个请求且 200
- [ ] 输入关键词 → 点搜索 1 次 → 列表立即刷新
- [ ] 点「重置」按钮 → 列表恢复全量
- [ ] URL query 仍能正确反映当前筛选条件(用于分享/刷新保持状态)
- [ ] 浏览器前进/后退键能恢复对应筛选条件
- [ ] 顺手验证:上面"顺手检查"列出的 5 个嫌疑列表页是否同样存在双请求,如有一并修
## 后端无需任何改动
后端 `GET /admin/product-line/list` 接口本身正常,2 个请求带的参数也都正确,问题完全在前端搜索按钮的实现 + 路由守卫的清理时序冲突。这条 changelog 只为通知前端修。
@mmg 看下 `hl-ui/src/views/product/line/index.vue``handleSearchSubmit`,按改法 A 改即可,预计 P1 — 影响所有列表页搜索体验。