diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index 19262b2..7a537aa 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -103,6 +103,8 @@ test('precision refresh uses the visible latest trades immediately without a sam await installInteractionProbe(page, '[data-orderbook-precision-refresh]'); await refresh.click(); await expect(refresh).toBeEnabled(); + await expect(refresh).toHaveAttribute('data-orderbook-precision-refresh-state', 'success'); + await expect(refresh).toHaveAttribute('aria-label', '精度推荐已更新'); await expect(panel.locator('[data-orderbook-precision-value="0.01"]')) .toHaveAttribute('aria-label', '切换价格精度到 0.01,推荐档位'); const probe = await finishInteractionProbe(page); @@ -110,6 +112,7 @@ test('precision refresh uses the visible latest trades immediately without a sam expect(await page.evaluate((symbol) => JSON.parse( localStorage.getItem(`jh_binance_orderbook_precision_samples_v3:${symbol}`) ), scenario.currentSymbol)).toEqual(['0.01', '0.01', '0.01', '0.01', '0.01']); + await expect(refresh).toHaveAttribute('data-orderbook-precision-refresh-state', 'idle', { timeout: 2_000 }); expect(errors).toEqual([]); }); @@ -123,7 +126,10 @@ test('precision refresh explains when the complete visible trade list still lack nodes.forEach((node) => { node.textContent = '81.00'; }); }); - await panel.locator('[data-orderbook-precision-refresh]').click(); + const refresh = panel.locator('[data-orderbook-precision-refresh]'); + await refresh.click(); + await expect(refresh).toHaveAttribute('data-orderbook-precision-refresh-state', 'retry'); + await expect(refresh).toHaveAttribute('aria-label', '近期价格变化不足,请稍后重试'); await expect(panel.locator('#jh-binance-ladder-status')) .toHaveText('近期价格变化不足,请稍后重试'); await expect(panel.locator('[aria-label*="推荐档位"]')).toHaveCount(0); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 084be15..bf3a150 100644 --- a/scripts/binance-orderbook-trade.user.js +++ b/scripts/binance-orderbook-trade.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.127 +// @version 2.7.128 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -1853,6 +1853,7 @@ const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10; const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5; const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4; + const ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS = 900; const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [ "0.00000001", "0.0000001", @@ -1939,6 +1940,8 @@ let orderbookPrecisionObserver = null; let orderbookPrecisionObserverRoot = null; let lastObservedOrderbookPrecision = null; + let orderbookPrecisionRefreshFeedback = { symbol: null, state: "idle" }; + let orderbookPrecisionRefreshFeedbackTimer = null; let orderbookPrecisionState = { symbol: null, samples: [], @@ -2791,6 +2794,40 @@ } return slots; } + function renderOrderbookPrecisionRefreshButton(symbol, disabled) { + const feedbackState = orderbookPrecisionRefreshFeedback.symbol === symbol ? orderbookPrecisionRefreshFeedback.state : "idle"; + const feedback = feedbackState === "success" ? { + label: PANEL_COPY.status.precisionUpdated, + color: "var(--color-Buy)", + icon: '' + } : feedbackState === "retry" ? { + label: PANEL_COPY.status.precisionInsufficient, + color: "var(--color-PrimaryYellow)", + icon: '' + } : { + label: formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT), + color: CONTROL_TEXT_COLOR, + icon: '' + }; + const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : ""; + return ``; + } + function showOrderbookPrecisionRefreshFeedback(symbol, state) { + if (!["success", "retry"].includes(state)) { + throw new Error(`Invalid orderbook precision refresh feedback state: ${state}`); + } + if (orderbookPrecisionRefreshFeedbackTimer !== null) { + window.clearTimeout(orderbookPrecisionRefreshFeedbackTimer); + } + orderbookPrecisionRefreshFeedback = { symbol, state }; + refreshOrderbookPrecisionRecommendation(); + orderbookPrecisionRefreshFeedbackTimer = window.setTimeout(() => { + orderbookPrecisionRefreshFeedbackTimer = null; + if (orderbookPrecisionRefreshFeedback.symbol !== symbol || orderbookPrecisionRefreshFeedback.state !== state) return; + orderbookPrecisionRefreshFeedback = { symbol, state: "idle" }; + refreshOrderbookPrecisionRecommendation(); + }, ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS); + } function refreshOrderbookPrecisionRecommendation(panel = document.getElementById(PANEL_ID)) { const el = panel?.querySelector?.(`#${ORDERBOOK_PRECISION_RECOMMENDATION_ID}`); if (!el) return; @@ -2824,13 +2861,12 @@ ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT); const recommendationHtml = [ '
', '
', `${PANEL_COPY.field.pricePrecision}`, ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy), - ``, + renderOrderbookPrecisionRefreshButton(symbol, !canRefresh), "
", "
" ].join(""); @@ -3041,6 +3077,7 @@ setLadderStatus( recommendation ? PANEL_COPY.status.precisionUpdated : PANEL_COPY.status.precisionInsufficient ); + showOrderbookPrecisionRefreshFeedback(symbol, recommendation ? "success" : "retry"); scheduleRenderPanel(); } function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) { diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 05398b9..d94baf6 100644 --- a/src/binance-orderbook-trade/index.user.js +++ b/src/binance-orderbook-trade/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.127 +// @version 2.7.128 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -274,6 +274,7 @@ import { const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10; const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5; const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4; + const ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS = 900; const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [ '0.00000001', '0.0000001', @@ -362,6 +363,8 @@ import { let orderbookPrecisionObserver = null; let orderbookPrecisionObserverRoot = null; let lastObservedOrderbookPrecision = null; + let orderbookPrecisionRefreshFeedback = { symbol: null, state: 'idle' }; + let orderbookPrecisionRefreshFeedbackTimer = null; let orderbookPrecisionState = { symbol: null, samples: [], @@ -1475,6 +1478,51 @@ import { return slots; } + function renderOrderbookPrecisionRefreshButton(symbol, disabled) { + const feedbackState = orderbookPrecisionRefreshFeedback.symbol === symbol + ? orderbookPrecisionRefreshFeedback.state + : 'idle'; + const feedback = feedbackState === 'success' + ? { + label: PANEL_COPY.status.precisionUpdated, + color: 'var(--color-Buy)', + icon: '', + } + : feedbackState === 'retry' + ? { + label: PANEL_COPY.status.precisionInsufficient, + color: 'var(--color-PrimaryYellow)', + icon: '', + } + : { + label: formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT), + color: CONTROL_TEXT_COLOR, + icon: '', + }; + const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : ''; + return ``; + } + + function showOrderbookPrecisionRefreshFeedback(symbol, state) { + if (!['success', 'retry'].includes(state)) { + throw new Error(`Invalid orderbook precision refresh feedback state: ${state}`); + } + if (orderbookPrecisionRefreshFeedbackTimer !== null) { + window.clearTimeout(orderbookPrecisionRefreshFeedbackTimer); + } + orderbookPrecisionRefreshFeedback = { symbol, state }; + refreshOrderbookPrecisionRecommendation(); + orderbookPrecisionRefreshFeedbackTimer = window.setTimeout(() => { + orderbookPrecisionRefreshFeedbackTimer = null; + if ( + orderbookPrecisionRefreshFeedback.symbol !== symbol + || orderbookPrecisionRefreshFeedback.state !== state + ) return; + orderbookPrecisionRefreshFeedback = { symbol, state: 'idle' }; + refreshOrderbookPrecisionRecommendation(); + }, ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS); + } + function refreshOrderbookPrecisionRecommendation(panel = document.getElementById(PANEL_ID)) { const el = panel?.querySelector?.(`#${ORDERBOOK_PRECISION_RECOMMENDATION_ID}`); if (!el) return; @@ -1517,13 +1565,12 @@ import { ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT); const recommendationHtml = [ '
', '
', `${PANEL_COPY.field.pricePrecision}`, ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy), - ``, + renderOrderbookPrecisionRefreshButton(symbol, !canRefresh), '
', '
', ].join(''); @@ -1770,6 +1817,7 @@ import { ? PANEL_COPY.status.precisionUpdated : PANEL_COPY.status.precisionInsufficient ); + showOrderbookPrecisionRefreshFeedback(symbol, recommendation ? 'success' : 'retry'); scheduleRenderPanel(); } diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index b9d3ae7..fafc825 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -962,13 +962,20 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep|fallbackMovement/); const refreshBody = readFunctionBody('refreshOrderbookPrecisionRecommendation'); + const refreshButtonBody = readFunctionBody('renderOrderbookPrecisionRefreshButton'); + const refreshFeedbackBody = readFunctionBody('showOrderbookPrecisionRefreshFeedback'); assert.match(refreshBody, /recommendOrderbookPrecision/); assert.doesNotMatch(refreshBody, /resolveOrderbookPrecisionSampleState|orderbookPrecisionSampleTimer/); - assert.match(refreshBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/); + assert.match(refreshButtonBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/); + assert.match(refreshButtonBody, /data-orderbook-precision-refresh-state="\$\{feedbackState\}"/); + assert.match(refreshButtonBody, /PANEL_COPY\.status\.precisionUpdated/); + assert.match(refreshButtonBody, /PANEL_COPY\.status\.precisionInsufficient/); + assert.match(refreshFeedbackBody, /ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS/); + assert.match(sampleBody, /showOrderbookPrecisionRefreshFeedback\(symbol, recommendation \? 'success' : 'retry'\)/); assert.match(refreshBody, /const controlsBusy = selectionBusy/); assert.match(refreshBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/); assert.match(refreshBody, /queueOrderbookPrecisionOptionsLoad\(symbol\)/); - assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT\)/); + assert.match(refreshButtonBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT\)/); const shortcutBody = readFunctionBody('renderOrderbookPrecisionShortcut'); assert.match(shortcutBody, /const recommended = value === recommendation/); assert.match(shortcutBody, /position:absolute;top:3px;right:3px;width:6px;height:6px/); @@ -987,7 +994,8 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(refreshBody, /PANEL_COPY\.tooltip\.pricePrecision/); assert.match(refreshBody, /renderOrderbookPrecisionShortcutSlots\(shortcutOptions, current, recommendation, controlsBusy\)/); assert.match(refreshBody, /grid-template-columns:36px repeat\(4,minmax\(0,1fr\)\) 32px/); - assert.match(refreshBody, /data-orderbook-precision-refresh="true"[^`]*