From 30bf3492bd21793083339371a67bf9e26af91b79 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 27 Aug 2026 14:55:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(EQD-7049):=20=E6=9C=9F=E6=9D=83=E5=AE=9A?= =?UTF-8?q?=E4=BB=B7=E9=A1=B5=E6=A0=87=E7=9A=84=E4=B8=8B=E6=8B=89=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E7=BB=93=E6=9E=9C=E8=90=BD=E5=9C=B0=E5=90=8E=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 现象(客户反馈):输入关键词接口返回20条但下拉只显示2条,继续输入又正常 - 根因:FastVue.autocomplete 对函数型 lookup 只同步取返回值渲染,服务端响应 回来后仅写缓存、未触发重渲染,下拉永远显示"上一次请求结果+当前词再过滤", 慢一拍且被前端二次过滤(品种/IsCombined/contains)进一步砍少 - 修复:响应到达且输入未变、下拉可见时重触发插件 onValueChange 重渲染; _fresh 命中当前关键词直接展示服务端结果(不再前端二次过滤,跨品种结果不误滤), _seq 单调递增丢弃乱序响应,_inflight 防同关键词重复请求 - 移除 mounted 中无效的 autocomplete('search') 调用(插件无此方法,一直为空操作) - 同步修改东莞定制版 structure_dz.js - 已用 Playwright 对照验证:旧代码复现"输入20只显2项/接口返回20条",修复后 两文件 20 项断言全过(显示数=服务端返回、无请求风暴、乱序不污染、可选中) --- .../wwwroot/Scripts/app/pricing/structure.js | 60 +++++++++++------- .../Scripts/app/pricing/structure_dz.js | 61 ++++++++++++------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/YLErpWeb/wwwroot/Scripts/app/pricing/structure.js b/YLErpWeb/wwwroot/Scripts/app/pricing/structure.js index 63a8e75c..39fed9ba 100644 --- a/YLErpWeb/wwwroot/Scripts/app/pricing/structure.js +++ b/YLErpWeb/wwwroot/Scripts/app/pricing/structure.js @@ -112,15 +112,22 @@ const vueTradeType = function () { //标的选择组件(EQD-7049:改为服务端搜索,不再依赖全量 ylotc.underlyings,避免十几万标的整段下载卡死) const vueUnderlying = function () { const _suggestionTpl = _.template($('#underlyingSuggestionTpl').html()); - // 标的缓存:按 品种|关键词 隔离;乱序响应由 token 丢弃(helper 收在函数内,避免全局绑定冲突) + // 标的缓存(同品种各实例共享):_cache 为最近一次服务端结果,_fresh 记录其对应的 品种|关键词, + // _seq 单调递增丢弃乱序/过期响应,_inflight 防同关键词重复请求(helper 收在函数内,避免全局绑定冲突) const _cache = {}; - const _tokens = {}; + const _fresh = {}; + const _seq = {}; + const _inflight = {}; function _fetch(varietyId, query, cb) { - var key = (varietyId || 0) + '|' + (query || ''); - var token = (_tokens[key] = (_tokens[key] || 0) + 1); + var vid = varietyId || 0; + var q = query || ''; + var key = vid + '|' + q; + if (_inflight[key]) return; + var seq = (_seq[vid] = (_seq[vid] || 0) + 1); + _inflight[key] = true; var postData = { - FilterCode: (query || '').toUpperCase(), - VarietyId: varietyId || 0, + FilterCode: q.toUpperCase(), + VarietyId: vid, MaxShowLength: 20, BlackLimit: 1, UseForTrading: true, @@ -128,7 +135,8 @@ const vueUnderlying = function () { CheckLaunch: true }; main.post('/frontdata/AjaxGetUnderlyingSelect', postData).done(function (res) { - if (_tokens[key] !== token) return; // 丢弃过期响应 + delete _inflight[key]; + if (_seq[vid] !== seq) return; // 已有更新的关键词发起请求,丢弃本响应 var arr = (res && (res.obj || res.data)) || []; var norm = arr.map(function (x) { return { @@ -142,7 +150,11 @@ const vueUnderlying = function () { PinYin: x.PinYin || '' }; }); - cb && cb(norm); + _cache[vid] = norm; + _fresh[vid] = key; + cb && cb(norm, q); + }).fail(function () { + delete _inflight[key]; }); } function _filter(list, query, varietyId) { @@ -163,24 +175,28 @@ const vueUnderlying = function () { mounted() { var self = this; this.jqInput = $(this.$el).children(0); - // EQD-7049:预拉默认20条(当前品种),避免下拉空白 - _fetch(self.underlying.VarietyId, '', function (list) { - _cache[self.underlying.VarietyId || 0] = list; - try { $(self.jqInput).autocomplete('search', ''); } catch (e) {} - }); + // EQD-7049:预拉默认20条(当前品种),获得焦点时由插件自身的 onValueChange 呈现 + _fetch(self.underlying.VarietyId, ''); this.autoctrl = FastVue.autocomplete(this.jqInput, { valueField: 'Code', - lookup(query, callback) { + lookup(query) { var varietyId = self.underlying.VarietyId; - var cached = _cache[varietyId || 0] || []; - var immediate = _filter(cached, query, varietyId); - if (query) { - // 有输入时异步向服务端搜索并刷新缓存(乱序响应由 token 丢弃) - _fetch(varietyId, query, function (list) { - _cache[varietyId || 0] = list; - }); + var vid = varietyId || 0; + var key = vid + '|' + (query || ''); + if (_fresh[vid] === key) { + // 命中当前关键词的服务端结果:直接展示(服务端已按 StartsWith+品种/黑名单过滤,不再前端二次过滤) + return (_cache[vid] || []).slice(0, 20); } - return immediate; + // 异步搜索。FastVue 包装的 lookup 只同步取返回值渲染,服务端结果到达后必须重新触发 + // onValueChange 才会显示;重走 lookup 时命中上面的 _fresh 分支直接返回,不会循环请求 + _fetch(varietyId, query, function (list, q) { + var inst = self.jqInput.autocomplete(); + if (!inst || !inst.visible) return; // 下拉已关闭:留待下次获得焦点时呈现 + if ((self.jqInput.val() || '').toLowerCase() !== q.toLowerCase()) return; // 输入已变化:等新关键词的响应 + inst.onValueChange(); + }); + // 过渡兜底:服务端响应到达前用旧缓存按关键词过滤,避免搜索期间下拉空白 + return _filter(_cache[vid] || [], query, varietyId); }, onSelect(data) { if (self.underlying !== data) { diff --git a/YLErpWeb/wwwroot/Scripts/app/pricing/structure_dz.js b/YLErpWeb/wwwroot/Scripts/app/pricing/structure_dz.js index 757f2a16..d0712e3c 100644 --- a/YLErpWeb/wwwroot/Scripts/app/pricing/structure_dz.js +++ b/YLErpWeb/wwwroot/Scripts/app/pricing/structure_dz.js @@ -108,18 +108,26 @@ const vueTradeType = function () { }; }; +//标的选择组件(EQD-7049:改为服务端搜索,不再依赖全量 ylotc.underlyings,避免十几万标的整段下载卡死) //标的选择组件(EQD-7049:改为服务端搜索,不再依赖全量 ylotc.underlyings,避免十几万标的整段下载卡死) const vueUnderlying = function () { const _suggestionTpl = _.template($('#underlyingSuggestionTpl').html()); - // 标的缓存:按 品种|关键词 隔离;乱序响应由 token 丢弃(helper 收在函数内,避免全局绑定冲突) + // 标的缓存(同品种各实例共享):_cache 为最近一次服务端结果,_fresh 记录其对应的 品种|关键词, + // _seq 单调递增丢弃乱序/过期响应,_inflight 防同关键词重复请求(helper 收在函数内,避免全局绑定冲突) const _cache = {}; - const _tokens = {}; + const _fresh = {}; + const _seq = {}; + const _inflight = {}; function _fetch(varietyId, query, cb) { - var key = (varietyId || 0) + '|' + (query || ''); - var token = (_tokens[key] = (_tokens[key] || 0) + 1); + var vid = varietyId || 0; + var q = query || ''; + var key = vid + '|' + q; + if (_inflight[key]) return; + var seq = (_seq[vid] = (_seq[vid] || 0) + 1); + _inflight[key] = true; var postData = { - FilterCode: (query || '').toUpperCase(), - VarietyId: varietyId || 0, + FilterCode: q.toUpperCase(), + VarietyId: vid, MaxShowLength: 20, BlackLimit: 1, UseForTrading: true, @@ -127,7 +135,8 @@ const vueUnderlying = function () { CheckLaunch: true }; main.post('/frontdata/AjaxGetUnderlyingSelect', postData).done(function (res) { - if (_tokens[key] !== token) return; // 丢弃过期响应 + delete _inflight[key]; + if (_seq[vid] !== seq) return; // 已有更新的关键词发起请求,丢弃本响应 var arr = (res && (res.obj || res.data)) || []; var norm = arr.map(function (x) { return { @@ -141,7 +150,11 @@ const vueUnderlying = function () { PinYin: x.PinYin || '' }; }); - cb && cb(norm); + _cache[vid] = norm; + _fresh[vid] = key; + cb && cb(norm, q); + }).fail(function () { + delete _inflight[key]; }); } function _filter(list, query, varietyId) { @@ -162,24 +175,28 @@ const vueUnderlying = function () { mounted() { var self = this; this.jqInput = $(this.$el).children(0); - // EQD-7049:预拉默认20条(当前品种),避免下拉空白 - _fetch(self.underlying.VarietyId, '', function (list) { - _cache[self.underlying.VarietyId || 0] = list; - try { $(self.jqInput).autocomplete('search', ''); } catch (e) {} - }); + // EQD-7049:预拉默认20条(当前品种),获得焦点时由插件自身的 onValueChange 呈现 + _fetch(self.underlying.VarietyId, ''); this.autoctrl = FastVue.autocomplete(this.jqInput, { valueField: 'Code', - lookup(query, callback) { + lookup(query) { var varietyId = self.underlying.VarietyId; - var cached = _cache[varietyId || 0] || []; - var immediate = _filter(cached, query, varietyId); - if (query) { - // 有输入时异步向服务端搜索并刷新缓存(乱序响应由 token 丢弃) - _fetch(varietyId, query, function (list) { - _cache[varietyId || 0] = list; - }); + var vid = varietyId || 0; + var key = vid + '|' + (query || ''); + if (_fresh[vid] === key) { + // 命中当前关键词的服务端结果:直接展示(服务端已按 StartsWith+品种/黑名单过滤,不再前端二次过滤) + return (_cache[vid] || []).slice(0, 20); } - return immediate; + // 异步搜索。FastVue 包装的 lookup 只同步取返回值渲染,服务端结果到达后必须重新触发 + // onValueChange 才会显示;重走 lookup 时命中上面的 _fresh 分支直接返回,不会循环请求 + _fetch(varietyId, query, function (list, q) { + var inst = self.jqInput.autocomplete(); + if (!inst || !inst.visible) return; // 下拉已关闭:留待下次获得焦点时呈现 + if ((self.jqInput.val() || '').toLowerCase() !== q.toLowerCase()) return; // 输入已变化:等新关键词的响应 + inst.onValueChange(); + }); + // 过渡兜底:服务端响应到达前用旧缓存按关键词过滤,避免搜索期间下拉空白 + return _filter(_cache[vid] || [], query, varietyId); }, onSelect(data) { if (self.underlying !== data) {