Fix intermittent empty options expiry dropdown after coin select.

Retry instrument fetch, reject empty chains instead of caching them, and ignore stale loadChain races.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-16 12:04:08 +08:00
parent b50eebe37c
commit 12c4e55f24
4 changed files with 173 additions and 38 deletions
+105 -20
View File
@@ -26,6 +26,7 @@
let lastGoodPositions = null;
let lastGoodPositionsAt = 0;
let positionsRefreshSeq = 0;
let chainLoadSeq = 0;
let refreshAllTimer = null;
let pendingRefreshTimer = null;
let pendingTtlSeconds = 600;
@@ -484,8 +485,8 @@
const sel = document.getElementById("opt-exp-select");
if (!sel) return;
const prev = preserveSelection !== false ? sel.value : "";
sel.innerHTML = '<option value="">选择到期日</option>';
const exps = (state.chain && state.chain.expiries) || [];
sel.innerHTML = '<option value="">选择到期日</option>';
exps.forEach(function (e) {
const o = document.createElement("option");
o.value = String(e.exp_time);
@@ -497,6 +498,20 @@
}
}
function setExpirySelectStatus(text) {
const sel = document.getElementById("opt-exp-select");
if (!sel) return;
sel.innerHTML = "";
const o = document.createElement("option");
o.value = "";
o.textContent = text || "选择到期日";
sel.appendChild(o);
}
function chainHasExpiries(chain) {
return !!(chain && Array.isArray(chain.expiries) && chain.expiries.length > 0);
}
function renderExpiries() {
renderExpiryOptions(true);
renderIndexLine();
@@ -938,25 +953,93 @@
}
}
async function loadChain() {
const d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(state.underlying));
if (!d.ok) {
alert(d.msg || "加载失败");
return;
async function loadChain(opts) {
const soft = !!(opts && opts.soft);
const uly = state.underlying;
const seq = ++chainLoadSeq;
const btn = document.getElementById("opt-load-chain");
if (btn && !soft) btn.disabled = true;
if (!soft) {
setExpirySelectStatus("加载到期日中…");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
tbody.innerHTML =
'<tr><td colspan="' + strikeTableColspan() + '" class="muted">加载期权链…</td></tr>';
}
}
try {
let d = null;
let lastMsg = "";
for (let attempt = 0; attempt < 2; attempt++) {
if (seq !== chainLoadSeq) return;
d = await apiJson("/api/options/chain?underlying=" + encodeURIComponent(uly));
if (seq !== chainLoadSeq) return;
if (d && d.ok && chainHasExpiries(d)) break;
lastMsg = (d && (d.msg || d.chain_error)) || "暂无到期日";
d = null;
if (attempt === 0) {
if (!soft) setExpirySelectStatus("重试加载到期日…");
await new Promise(function (resolve) { setTimeout(resolve, 400); });
}
}
if (seq !== chainLoadSeq) return;
if (!d || !d.ok || !chainHasExpiries(d)) {
if (chainHasExpiries(state.chain) && state.chain.underlying === uly) {
if (!soft) {
renderExpiries();
renderStrikes();
}
return;
}
if (soft) return;
setExpirySelectStatus("选择到期日");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
tbody.innerHTML =
'<tr><td colspan="' + strikeTableColspan() + '" class="muted">' +
(lastMsg || "暂无到期日,请点「刷新链」") +
"</td></tr>";
}
alert(lastMsg || "加载到期日失败,请点「刷新链」重试");
return;
}
const keepSel = soft && state.selectedInst;
const keepExp = soft ? (document.getElementById("opt-exp-select") || {}).value : "";
state.chain = d;
panelCache.chain = d;
panelCache.underlying = uly;
panelCache.optType = state.optType;
if (!soft) {
state.selectedInst = null;
resetMoneyFilterToAll();
state.strikeExpandAll = false;
const expandCb = document.getElementById("opt-strike-expand-all");
if (expandCb) expandCb.checked = false;
parkOrderPanel();
}
updateUnderlyingLabel();
renderExpiries();
if (soft && keepExp) {
const sel = document.getElementById("opt-exp-select");
if (sel && Array.from(sel.options).some(function (o) { return o.value === keepExp; })) {
sel.value = keepExp;
}
}
renderStrikes();
if (soft && keepSel) state.selectedInst = keepSel;
} catch (e) {
if (seq !== chainLoadSeq || soft) return;
setExpirySelectStatus("选择到期日");
const tbody = document.getElementById("opt-strike-tbody");
if (tbody) {
tbody.innerHTML =
'<tr><td colspan="' + strikeTableColspan() + '" class="muted">加载失败: ' +
String((e && e.message) || e) +
"</td></tr>";
}
} finally {
if (seq === chainLoadSeq && btn) btn.disabled = false;
}
state.chain = d;
panelCache.chain = d;
panelCache.underlying = state.underlying;
panelCache.optType = state.optType;
state.selectedInst = null;
resetMoneyFilterToAll();
state.strikeExpandAll = false;
const expandCb = document.getElementById("opt-strike-expand-all");
if (expandCb) expandCb.checked = false;
parkOrderPanel();
updateUnderlyingLabel();
renderExpiries();
renderStrikes();
}
async function openPosition() {
@@ -1756,7 +1839,7 @@
refreshPendingOrders();
startPendingOrdersPoll();
const hasCache =
panelCache.chain &&
chainHasExpiries(panelCache.chain) &&
panelCache.underlying === state.underlying &&
panelCache.optType === state.optType;
if (hasCache) {
@@ -1764,6 +1847,8 @@
renderExpiries();
renderStrikes();
refreshAllPositions();
// 后台静默刷新,避免缓存过期后到期日变空
loadChain({ soft: true });
return;
}
requestAnimationFrame(function () {