diff --git a/static/app.js b/static/app.js index 41007f5..bacba6c 100644 --- a/static/app.js +++ b/static/app.js @@ -4258,7 +4258,7 @@ async function showStockPreview(code, trigger) { document.body.classList.toggle("stock-preview-open", mobile); requestAnimationFrame(repositionStockPreview); - const cacheKey = `${code}:${elements.tradeDate.value}`; + const cacheKey = `${code}:latest`; const cached = stockPreviewCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) { renderStockPreview(cached.payload); @@ -4268,9 +4268,8 @@ async function showStockPreview(code, trigger) { stockPreviewAbortController?.abort(); stockPreviewAbortController = new AbortController(); try { - const query = new URLSearchParams({ trade_date: elements.tradeDate.value }); const payload = await apiRequest( - `/api/stock/${encodeURIComponent(code)}/preview?${query}`, + `/api/stock/${encodeURIComponent(code)}/preview`, "GET", null, { signal: stockPreviewAbortController.signal }, @@ -4291,11 +4290,10 @@ function renderStockPreviewLoading() { setText("stockPreviewCode", state.stockPreviewCode || "--"); setText("stockPreviewName", fallback.name || "正在加载"); setText("stockPreviewSector", fallback.sector || "--"); - setText("stockPreviewPrice", meaningfulNumber(fallback.price) ? formatNumber(fallback.price, 2) : "--"); - const change = number(fallback.change); - setText("stockPreviewChange", meaningfulNumber(fallback.change) ? `${signed(change)}%` : "--"); - document.querySelector("#stockPreviewChange").className = changeClass(change); - setText("stockPreviewDate", elements.tradeDate.value); + setText("stockPreviewPrice", "--"); + setText("stockPreviewChange", "--"); + document.querySelector("#stockPreviewChange").className = ""; + setText("stockPreviewDate", "最新行情"); setText("stockPreviewSource", "正在读取行情"); setText("stockPreviewSummary", "等待行情数据"); document.querySelector("#stockPreviewLoading").hidden = false; @@ -4306,15 +4304,15 @@ function renderStockPreview(payload) { state.stockPreviewPayload = payload; const fallback = state.stockPreviewFallback || {}; const stock = payload.stock || {}; - const price = stock.price ?? fallback.price; - const change = stock.change ?? fallback.change; + const price = stock.price; + const change = stock.change; setText("stockPreviewCode", stock.code || state.stockPreviewCode); setText("stockPreviewName", stock.name && stock.name !== "--" ? stock.name : fallback.name || "--"); setText("stockPreviewSector", stock.industry && stock.industry !== "其他" ? stock.industry : fallback.sector || "其他"); setText("stockPreviewPrice", meaningfulNumber(price) ? formatNumber(price, 2) : "--"); setText("stockPreviewChange", meaningfulNumber(change) ? `${signed(change)}%` : "--"); document.querySelector("#stockPreviewChange").className = changeClass(change); - setText("stockPreviewDate", payload.meta?.trade_date || elements.tradeDate.value); + setText("stockPreviewDate", payload.meta?.trade_date || "最新行情"); document.querySelector("#stockPreviewLoading").hidden = true; const intradayAvailable = (payload.intraday || []).length > 0; const source = payload.meta?.realtime ? "盘中行情" : "日 K 行情"; @@ -4513,8 +4511,7 @@ function renderAlerts() { async function openReviewAssistant() { if (!hasMemberAccess()) { - showToast("复盘助手仅对会员开放"); - openSettings("membership"); + openSettings("membership", "复盘助手仅对会员开放"); return; } toggleHeaderCommandMenu(false); @@ -5191,10 +5188,13 @@ function selectAccountPanel(panel) { return selected; } -async function openSettings(panel = "profile") { - selectAccountPanel(panel); +async function openSettings(panel = "profile", accessNotice = "") { + const selectedPanel = selectAccountPanel(panel); toggleAccountDropdown(false); toggleHeaderCommandMenu(false); + const membershipNotice = document.querySelector("#membershipAccessNotice"); + membershipNotice.textContent = accessNotice; + membershipNotice.hidden = selectedPanel !== "membership" || !accessNotice; const status = document.querySelector("#connectionStatus"); status.className = "connection-status"; status.textContent = "正在读取账号状态"; @@ -5220,7 +5220,7 @@ async function openSettings(panel = "profile") { ? `${membership.plan || "会员"}${membership.expires_at ? ` · 有效至 ${membershipDateDisplay(membership.expires_at, true)}` : " · 长期有效"}` : membership.is_admin ? "管理员拥有智能功能管理权限,但不会因此显示为已开通会员。" - : "开通会员后可使用智能选股、问师、问天等智能功能。"); + : "开通会员后可使用智能选股、问师、问天、复盘助手等智能功能。"); setText("membershipQuotaHint", `会员默认每日智能分析额度 ${number(access.daily_limit)} 次,由管理员统一设置。`); setText("membershipUsage", membership.active ? `今日已用 ${number(access.used_today)} 次` diff --git a/static/index.html b/static/index.html index 6a51cd6..734fca1 100644 --- a/static/index.html +++ b/static/index.html @@ -1179,7 +1179,8 @@
正在读取账号状态
diff --git a/static/styles.css b/static/styles.css index cfc93a7..38ca898 100644 --- a/static/styles.css +++ b/static/styles.css @@ -11014,6 +11014,17 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin .membership-status-grid > div { padding: 12px 13px; border: 1px solid var(--line, #e2e8ee); border-radius: 10px; background: #fbfcfd; } .membership-status-grid span { display: block; color: #74808c; font-size: 11px; } .membership-status-grid strong { display: block; margin-top: 5px; color: #26323d; font-size: 15px; font-variant-numeric: tabular-nums; } +.membership-access-notice { + margin: 0 0 12px; + padding: 10px 12px; + border: 1px solid #ead9ac; + border-radius: 6px; + background: #fff9ea; + color: #775316; + font-size: 13px; + font-weight: 700; + line-height: 1.5; +} .membership-comparison { overflow: hidden; border: 1px solid var(--line, #e2e8ee); border-radius: 10px; font-size: 12px; } .membership-comparison > div { display: grid; grid-template-columns: minmax(0, 1.6fr) .7fr .7fr; gap: 8px; align-items: center; padding: 9px 11px; border-top: 1px solid var(--line, #e2e8ee); } .membership-comparison > div:first-child { border-top: 0; } diff --git a/tests/e2e/app-shell.spec.js b/tests/e2e/app-shell.spec.js index 7e59faf..2320f00 100644 --- a/tests/e2e/app-shell.spec.js +++ b/tests/e2e/app-shell.spec.js @@ -53,7 +53,17 @@ async function mockApplication(page, authSession = session()) { let payload = { ok: true }; if (url.pathname === "/api/auth/me") payload = authSession; else if (url.pathname === "/api/dashboard") payload = dashboard; - else if (url.pathname === "/api/watchlist" || url.pathname === "/api/notes") payload = { items: [] }; + else if (url.pathname === "/api/stock/002141/preview") { + payload = { + meta: { trade_date: "2026-07-23", realtime: false, intraday_notice: "" }, + stock: { code: "002141", name: "Test Stock", industry: "Test Sector", price: 10.8, change: 2.4 }, + prices: [ + { trade_date: "2026-07-22", open: 10, high: 10.5, low: 9.9, close: 10.2, volume: 1000 }, + { trade_date: "2026-07-23", open: 10.3, high: 10.9, low: 10.2, close: 10.8, volume: 1200 }, + ], + intraday: [], + }; + } else if (url.pathname === "/api/watchlist" || url.pathname === "/api/notes") payload = { items: [] }; else if (url.pathname === "/api/alerts") { payload = { items: [{ @@ -180,6 +190,23 @@ test("regular account cannot see admin controls and member features are gated", await page.locator('[data-view="screenerView"]').first().click(); await expect(page.locator("#screenerView .member-gate")).toBeVisible(); await expect(page.locator("#screenerRunButton")).toBeDisabled(); + await page.locator("#assistantButton").click(); + await expect(page.locator("#settingsDialog")).toBeVisible(); + await expect(page.locator("#membershipAccessNotice")).toHaveText("复盘助手仅对会员开放"); + await expect(page.locator("#membershipAccessNotice")).toBeVisible(); +}); + +test("stock hover preview ignores the selected historical date", async ({ page }) => { + await mockApplication(page, session("user", true)); + await page.goto("/index.html"); + await page.locator("#tradeDate").fill("2026-07-01"); + const requestPromise = page.waitForRequest((request) => request.url().includes("/api/stock/002141/preview")); + await page.evaluate(() => showStockPreview("002141", document.querySelector("#globalSearchButton"))); + const request = await requestPromise; + const requestUrl = new URL(request.url()); + expect(requestUrl.searchParams.has("trade_date")).toBe(false); + await expect(page.locator("#stockPreviewDate")).toHaveText("2026-07-23"); + await expect(page.locator("#stockPreviewName")).toHaveText("Test Stock"); }); test("mobile shell stays within the viewport", async ({ page }) => { diff --git a/tests/test_frontend_contract.py b/tests/test_frontend_contract.py index 9d08a98..f0137b2 100644 --- a/tests/test_frontend_contract.py +++ b/tests/test_frontend_contract.py @@ -72,6 +72,21 @@ class FrontendContractTests(unittest.TestCase): for label in prohibited: self.assertNotIn(label, self.script) + def test_stock_hover_preview_always_uses_latest_market_context(self): + start = self.script.index("async function showStockPreview") + end = self.script.index("function renderStockPreviewLoading", start) + preview_loader = self.script[start:end] + self.assertIn('const cacheKey = `${code}:latest`;', preview_loader) + self.assertIn('/preview`', preview_loader) + self.assertNotIn("trade_date", preview_loader) + self.assertNotIn("elements.tradeDate.value", preview_loader) + + def test_membership_copy_includes_review_assistant_access(self): + self.assertIn("复盘助手仅对会员开放", self.script) + self.assertIn("智能选股、问师、问天、复盘助手等智能功能", self.html) + self.assertIn("自选股、复盘记录与交易日志", self.html) + self.assertIn("每日智能分析额度", self.html) + if __name__ == "__main__": unittest.main()