From 998d90d425aa51c937575758e59af98e52aa93fc Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Wed, 13 May 2026 09:53:44 +0800 Subject: [PATCH] =?UTF-8?q?frontend-bug:=20=E4=B8=BB=E9=A2=98=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=82=B9=E6=90=9C=E7=B4=A2=E7=AC=AC=E4=B8=80=E5=9B=9E?= =?UTF-8?q?=E4=B8=8D=E7=94=9F=E6=95=88=EF=BC=88=E8=B7=AF=E7=94=B1=E5=AE=88?= =?UTF-8?q?=E5=8D=AB=E5=8F=96=E6=B6=88=E4=BA=86=E8=87=AA=E5=AE=B6=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-line-list-search-first-click-no-effect.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 changelogs/2026-05/13_frontend_bug_admin_product-line-list-search-first-click-no-effect.md diff --git a/changelogs/2026-05/13_frontend_bug_admin_product-line-list-search-first-click-no-effect.md b/changelogs/2026-05/13_frontend_bug_admin_product-line-list-search-first-click-no-effect.md new file mode 100644 index 0000000..3c7ad62 --- /dev/null +++ b/changelogs/2026-05/13_frontend_bug_admin_product-line-list-search-first-click-no-effect.md @@ -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 — 影响所有列表页搜索体验。