Compare commits

...
Author SHA1 Message Date
施工员andmultica-agent ed9858e330 fix: align auction/themes/popularity header right-side icons with other pages
Remove the stale body[data-active-view] .overview-strip { flex: 0 0 auto }
override from the three market-insight page stylesheets. Since the merged
header (B-147) moved the market tape into .app-header, this rule no longer
targets the old in-main overview strip and instead overrides the shared
shell rule (.app-header .overview-strip { flex: 1 0 auto }), which pushed the
header-actions icons off the right edge on auction, theme library and
popularity pages. Deleting the override lets the shared shell owner drive the
tape growth so the right-side icons sit flush like every other page.

Regenerate architecture-inventory.json line metrics for the CSS change.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 23:31:37 +08:00
3ae07b8aae B-221: keep header tape readable and commands inside the viewport
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 15:24:38 +08:00
5a507239a3 B-217: show full header username without 108px truncation
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 13:39:22 +08:00
152c0000ce B-214: keep desktop header commands visible instead of behind ellipsis
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 12:39:22 +08:00
cf7183d0c0 fix: restore B-147 dialog tokens and unclip screener headers
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 08:18:05 +08:00
22d4dcd339 feat: apply B-147 visual tokens to screener, review and account surfaces
Unify intelligent screener, review workspace, and account/admin chrome with shared tokens, table density, and left-aligned first columns.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 06:39:25 +08:00
ee37223722 fix: left-align theme rank numbers and bind aux font size
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 05:13:40 +08:00
ad08d309c6 feat: apply B-147 visual tokens to remaining market data pages
Unify ladder, themes, rotation, auction, dragon-tiger and popularity with shared tokens, left-aligned first columns, and table density.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 04:34:36 +08:00
6ab910eeea fix: isolate limit-pool fixed layout to 1440+ breakpoints
Keep the 1600 no-overflow table, but restore natural column widths
and horizontal scrolling at 1280 and 390 so cells are readable.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 04:04:59 +08:00
f67100929b fix: fit 13 limit-pool columns at 1600 and restyle leftover tokens
Keep the limit-up table inside the card without horizontal scroll,
raise header/row metrics to the B-147 spec, restore night sort accent,
and switch the day-mode repair badge to the shared blue token.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 03:35:40 +08:00
df45638edd fix: keep tape date visible and finish dual-review shell polish
Give the overview date its own class so mobile cannot hide it, keep mentor
subtitles fully readable, and align sentiment table header tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 02:31:52 +08:00
bb67085dd4 fix: merge B-157 and B-158 shell review blockers
Keep 1024-1439 broken/seal metrics in the detail panel, fold desktop admin actions into the ellipsis menu, and fit the 13-column limit pool at 1600 without hiding the data date.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 02:19:04 +08:00
e778c883db feat: apply B-147 visual tokens to shell and three representative pages
Closes B-156

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 00:43:09 +08:00
MS-01-Codexandmultica-agent b60a4cb682 fix: reload sentiment cycle history after backfill
backfillData() only refreshed system settings after a successful backfill,
leaving state.sentimentHistory/sentimentHistoryKey populated with the stale
pre-backfill cache, so an already-open sentiment cycle page kept showing the
old one-day history.

After a successful backfill, clear both cache fields and, when the sentiment
cycle view is active, force loadSentimentHistory(true) so the history renders
immediately. Add a frontend contract test covering the refresh path.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 21:52:15 +08:00
MS-01-Codexandmultica-agent 74304795ec test: cover m0004 add_mentor_note migration paths
Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 20:12:18 +08:00
MS-01-Codexandmultica-agent 0fba71f62c fix: register m0004 add_mentor_note migration
Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 18:18:45 +08:00
MS-01-Codexandmultica-agent e0cba74f8e fix: reconnect mentor page into the project shell
Remove the mentor immersive rules in shared/shell.css that hid the module
nav, header actions, market tape, overview strip and status bar while
mentorView was active, which made the page look like an independent site.
Mentor now stays inside the canonical 小白复盘 shell and owns its own
"问师 + 数据日期" title header inside #mentorView (B-92 content intact).

Also fix the shell's latent 1024px overflow: .main min-width 1080 now only
applies at >=1280, and the overview strip scrolls internally instead of
clipping its right side when it is wider than the content column.

Update the contract and e2e baselines to assert the shell stays visible
and usable on mentor, page switching leaves no residue, and add a
dedicated shell-integration e2e test.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 17:19:03 +08:00
MS-01-Codexandmultica-agent 39b6f71443 fix: align mentor page with final day/night visual review
Resolve B-98 visual review deviations and B-97 atomicity finding:
- Scoped mentorView header to title/subtitle + theme toggle only (hides
  date, refresh, background refresh, system management and account menus).
- Map contact avatars by stable id to the final day/night palette (52科比
  and self stay blue; others use the final violet/green/orange/red/teal/
  purple/yellow tones).
- Grade badges read A级/B级/C级; pinned contacts render a pin + 置顶
  badge; row-side pin controls are removed (chat header keeps pinning).
- Chat header drops grade badges; subtitle shows the active contact
  description/tagline.
- Assistant message column caps at 900px and centers on wide screens;
  user bubble keeps its 60% cap.
- Widen the directory search field (~218px target), keep filter/sort on
  the same row, and make the filter button icon-only (no chevron).
- Follow-up links drop the leading icon; no '日期 · 回答完成' meta line;
  follow-up divider is 382px and left-aligned with the text column.
- Own-message meta shows time only; composer height converges to ~85px
  while keeping auto-grow and the 168px cap.
- Selected contact right inset ~10px, header action radius 8px, and a
  thin list scrollbar instead of the reserved gutter.
- Remove the iFinD settings switch fragment from index.html so the mentor
  commit no longer carries half of the parallel iFinD work; the id
  contract test now tolerates dangling references introduced only by
  uncommitted working-tree edits.
- Add contract + e2e assertions for the header visibility, avatar tone
  mapping, badge copy, chat subtitle, message max-width, composer height,
  and follow-up area.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 13:15:10 +08:00
MS-01-Codexandmultica-agent 8ac3adbb5d feat: restore mentor page to final day/night spec
Rebuild the mentor workspace per the approved final design (day.png/night.png):
- mentorView gets a dedicated full-width immersive shell (independent 56px
  top bar with page title slot + theme mode text, hidden module-nav/market
  tape/overview/status bar) scoped to body[data-active-view=mentorView].
- Assistant messages become borderless body text with name/time above; only
  user messages use a blue bubble; keep loading/error/streaming caret states.
- 4 quick topics stay visible with history; composer restored to a framed
  ~94px card with bottom-left shortcut hint and bottom-right send button.
- Directory tools merged into one row (search + filter menu + sort); the
  filter menu still offers all/A/B/C; list selected state is an inset rounded
  fill; contact rows are borderless 75px items.
- Chat header always shows pin/note/profile/clear; pin reuses /api/mentors/
  preferences; theme toggle stays the single #themeToggle.
- Night tokens match the spec including the two distinct blues (#316FEF link,
  #5B8DEF quote/selected icon). Mobile (<768) stacks panels with no horizontal
  overflow; 1024+ follows the desktop spec.
- Update stale test baselines (300px sidebar, 94px composer, hint presence,
  night bubble color, centered disclaimer) and regenerate the architecture
  inventory.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 11:54:57 +08:00
44 changed files with 3899 additions and 1454 deletions
+7 -1
View File
@@ -1,8 +1,14 @@
from .m0001_adopt_legacy import MIGRATION as M0001_ADOPT_LEGACY
from .m0002_job_runs import MIGRATION as M0002_JOB_RUNS
from .m0003_llm_audit import MIGRATION as M0003_LLM_AUDIT
from .m0004_mentor_notes import MIGRATION as M0004_MENTOR_NOTES
from .runner import Migration, MigrationError, MigrationRunner
MIGRATIONS = (M0001_ADOPT_LEGACY, M0002_JOB_RUNS, M0003_LLM_AUDIT)
MIGRATIONS = (
M0001_ADOPT_LEGACY,
M0002_JOB_RUNS,
M0003_LLM_AUDIT,
M0004_MENTOR_NOTES,
)
__all__ = ["MIGRATIONS", "Migration", "MigrationError", "MigrationRunner"]
@@ -0,0 +1,24 @@
from __future__ import annotations
import sqlite3
from backend.database.migrations.runner import Migration
def add_mentor_note(connection: sqlite3.Connection) -> None:
columns = {
str(row["name"])
for row in connection.execute("PRAGMA table_info(mentor_preferences)")
}
if "note" not in columns:
connection.execute(
"ALTER TABLE mentor_preferences ADD COLUMN note TEXT NOT NULL DEFAULT ''"
)
MIGRATION = Migration(
version="0004",
name="add_mentor_note",
action=add_mentor_note,
signature="mentor-preferences-note:v1:note",
)
+45 -45
View File
@@ -370,29 +370,29 @@
}
],
"css_layers": [
"/shared/tokens.css?v=20260806-2",
"/shared/tokens.css?v=20260820-3",
"/shared/base.css?v=20260806-1",
"/shared/shell.css?v=20260806-2",
"/shared/auth.css?v=20260806-1",
"/shared/components/controls.css?v=20260806-1",
"/shared/components/navigation.css?v=20260806-1",
"/shared/components/cards.css?v=20260806-1",
"/shared/components/tables.css?v=20260806-1",
"/shared/components/dialogs.css?v=20260806-1",
"/shared/shell.css?v=20260820-8",
"/shared/auth.css?v=20260820-5",
"/shared/components/controls.css?v=20260820-2",
"/shared/components/navigation.css?v=20260820-1",
"/shared/components/cards.css?v=20260820-1",
"/shared/components/tables.css?v=20260820-1",
"/shared/components/dialogs.css?v=20260820-3",
"/shared/components/feedback.css?v=20260806-1",
"/pages/market/foundation.css?v=20260806-1",
"/pages/sentiment/foundation.css?v=20260806-2",
"/pages/pools/foundation.css?v=20260806-2",
"/pages/ladder/foundation.css?v=20260806-1",
"/pages/rotation/foundation.css?v=20260806-1",
"/pages/auction/foundation.css?v=20260806-2",
"/pages/themes/foundation.css?v=20260806-1",
"/pages/popularity/foundation.css?v=20260806-1",
"/pages/dragon-tiger/foundation.css?v=20260806-1",
"/pages/screener/foundation.css?v=20260806-2",
"/pages/mentor/foundation.css?v=20260806-2",
"/pages/market/foundation.css?v=20260820-4",
"/pages/sentiment/foundation.css?v=20260820-2",
"/pages/pools/foundation.css?v=20260820-1",
"/pages/ladder/foundation.css?v=20260820-1",
"/pages/rotation/foundation.css?v=20260820-1",
"/pages/auction/foundation.css?v=20260820-1",
"/pages/themes/foundation.css?v=20260820-1",
"/pages/popularity/foundation.css?v=20260820-1",
"/pages/dragon-tiger/foundation.css?v=20260820-1",
"/pages/screener/foundation.css?v=20260820-4",
"/pages/mentor/foundation.css?v=20260820-2",
"/pages/heaven/foundation.css?v=20260806-2",
"/pages/review/foundation.css?v=20260806-1"
"/pages/review/foundation.css?v=20260820-4"
],
"frontend_composition": {
"shell": "frontend/index.html",
@@ -441,8 +441,8 @@
},
{
"path": "frontend/pages/screener/foundation.css",
"bytes": 102987,
"lines": 6565
"bytes": 103547,
"lines": 6576
},
{
"path": "frontend/pages/heaven/page.js",
@@ -451,8 +451,8 @@
},
{
"path": "frontend/shared/shell.css",
"bytes": 51978,
"lines": 3224
"bytes": 63550,
"lines": 3757
},
{
"path": "backend/features/heaven/engine.py",
@@ -461,8 +461,8 @@
},
{
"path": "frontend/index.html",
"bytes": 45846,
"lines": 643
"bytes": 47131,
"lines": 646
},
{
"path": "backend/features/screener/catalog.py",
@@ -471,8 +471,8 @@
},
{
"path": "frontend/pages/auction/foundation.css",
"bytes": 34990,
"lines": 2409
"bytes": 35247,
"lines": 2416
},
{
"path": "database.py",
@@ -531,7 +531,7 @@
},
{
"path": "frontend/pages/pools/page.html",
"bytes": 14958,
"bytes": 14942,
"lines": 235
},
{
@@ -541,8 +541,8 @@
},
{
"path": "frontend/shared/admin.js",
"bytes": 13975,
"lines": 256
"bytes": 14145,
"lines": 261
},
{
"path": "backend/features/heaven/market_context.py",
@@ -574,6 +574,11 @@
"bytes": 10539,
"lines": 244
},
{
"path": "frontend/shared/dashboard.js",
"bytes": 9993,
"lines": 220
},
{
"path": "backend/data/providers/tushare_sectors.py",
"bytes": 9876,
@@ -600,9 +605,9 @@
"lines": 238
},
{
"path": "frontend/shared/dashboard.js",
"bytes": 8424,
"lines": 194
"path": "frontend/pages/mentor/page.html",
"bytes": 8357,
"lines": 116
},
{
"path": "backend/features/screener/formula.py",
@@ -644,11 +649,6 @@
"bytes": 6202,
"lines": 141
},
{
"path": "frontend/pages/mentor/page.html",
"bytes": 6190,
"lines": 89
},
{
"path": "backend/features/screener/selection.py",
"bytes": 6092,
@@ -704,6 +704,11 @@
"bytes": 4276,
"lines": 91
},
{
"path": "frontend/shared/theme.js",
"bytes": 4258,
"lines": 117
},
{
"path": "backend/features/screener/engine.py",
"bytes": 4242,
@@ -714,11 +719,6 @@
"bytes": 4118,
"lines": 115
},
{
"path": "frontend/shared/theme.js",
"bytes": 4118,
"lines": 115
},
{
"path": "frontend/shared/table.js",
"bytes": 3790,
@@ -736,7 +736,7 @@
},
{
"path": "frontend/pages/themes/page.html",
"bytes": 3309,
"bytes": 3316,
"lines": 55
},
{
+74 -71
View File
@@ -17,29 +17,29 @@
document.documentElement.style.colorScheme = theme;
})();
</script>
<link rel="stylesheet" href="/shared/tokens.css?v=20260806-2">
<link rel="stylesheet" href="/shared/tokens.css?v=20260820-3">
<link rel="stylesheet" href="/shared/base.css?v=20260806-1">
<link rel="stylesheet" href="/shared/shell.css?v=20260806-2">
<link rel="stylesheet" href="/shared/auth.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/controls.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/cards.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/tables.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260806-1">
<link rel="stylesheet" href="/shared/shell.css?v=20260820-8">
<link rel="stylesheet" href="/shared/auth.css?v=20260820-5">
<link rel="stylesheet" href="/shared/components/controls.css?v=20260820-2">
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260820-1">
<link rel="stylesheet" href="/shared/components/cards.css?v=20260820-1">
<link rel="stylesheet" href="/shared/components/tables.css?v=20260820-1">
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260820-3">
<link rel="stylesheet" href="/shared/components/feedback.css?v=20260806-1">
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260820-4">
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260820-2">
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260820-1">
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260820-4">
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260820-2">
<link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260820-4">
</head>
<body>
<section id="authGate" class="auth-gate" aria-label="账号登录">
@@ -68,12 +68,56 @@
<span id="mobilePageGroup">行情</span>
<strong id="mobilePageTitle">情绪周期</strong>
</div>
<div class="market-tape" aria-label="市场概况">
<span class="market-item up">上涨 <strong id="tapeUp">--</strong></span>
<span class="market-item down">下跌 <strong id="tapeDown">--</strong></span>
<span class="market-item">涨停 <strong id="tapeLimit">--</strong></span>
<span class="market-item">成交额 <strong id="tapeAmount">--</strong></span>
<div class="app-page-context" aria-live="polite">
<strong id="currentPageTitle">小白复盘</strong>
<span id="currentPageSubtitle"></span>
</div>
<section class="overview-strip" aria-label="当日复盘指标" data-overview-expanded="false">
<div class="market-tape" aria-label="市场概况">
<div class="tape-metric tape-sentiment sentiment-block">
<span class="metric-label">市场情绪</span>
<span class="tape-metric-value">
<strong id="sentimentScore">--</strong>
<em id="sentimentText" class="sentiment-text">等待数据</em>
</span>
<div class="sentiment-gauge" id="sentimentGauge" aria-hidden="true"><span>--</span></div>
</div>
<div class="tape-metric market-item">
<span class="metric-label">涨停</span>
<strong id="limitUpMetric" class="metric-value up">--</strong>
</div>
<div class="tape-metric market-item">
<span class="metric-label">跌停</span>
<strong id="limitDownMetric" class="metric-value down">--</strong>
</div>
<div class="tape-metric tape-optional market-item">
<span class="metric-label">炸板</span>
<strong id="brokenMetric" class="metric-value warning">--</strong>
</div>
<div class="tape-metric tape-optional market-item">
<span class="metric-label">封板率</span>
<strong id="sealRateMetric" class="metric-value">--</strong>
</div>
<div class="tape-metric market-item">
<span class="metric-label">两市成交</span>
<strong id="amountMetric" class="metric-value">--</strong>
</div>
<button id="overviewToggle" class="overview-toggle" type="button" aria-expanded="false" title="展开市场详情">
<span>详情</span><i data-lucide="chevron-down" aria-hidden="true"></i>
</button>
</div>
<div class="tape-detail">
<div class="tape-detail-item market-item"><span>市场情绪</span><strong class="tape-metric-value"><b id="detailSentimentScore">--</b><em id="detailSentimentText" class="sentiment-text">等待数据</em></strong></div>
<div class="tape-detail-item market-item"><span>上涨家数</span><strong id="tapeUp" class="up">--</strong></div>
<div class="tape-detail-item market-item"><span>下跌家数</span><strong id="tapeDown" class="down">--</strong></div>
<div class="tape-detail-item market-item"><span>涨停</span><strong id="tapeLimit" class="up">--</strong></div>
<div class="tape-detail-item market-item"><span>跌停</span><strong id="tapeLimitDown" class="down">--</strong></div>
<div class="tape-detail-item market-item"><span>炸板</span><strong id="detailBroken" class="warning">--</strong></div>
<div class="tape-detail-item market-item"><span>封板率</span><strong id="detailSealRate">--</strong></div>
<div class="tape-detail-item market-item"><span>两市成交</span><strong id="tapeAmount">--</strong></div>
<div class="tape-detail-item tape-detail-date market-item"><span>数据日期</span><strong id="dataDateMetric" class="metric-value small">--</strong></div>
</div>
</section>
<div class="header-actions">
<div class="header-date-group">
@@ -82,7 +126,7 @@
<button id="nextDate" class="icon-button" type="button" title="后一个交易日" aria-label="后一个交易日"><i data-lucide="chevron-right"></i></button>
</div>
<button id="globalSearchButton" class="icon-button global-search-button" type="button" title="全局搜索(Ctrl+K" aria-label="全局搜索"><i data-lucide="search"></i></button>
<button id="themeToggle" class="icon-button theme-toggle" type="button" title="切换到夜间模式" aria-label="切换到夜间模式" aria-pressed="false"><i data-lucide="moon"></i></button>
<button id="themeToggle" class="icon-button theme-toggle" type="button" title="切换到夜间模式" aria-label="切换到夜间模式" aria-pressed="false"><i data-lucide="moon"></i><span id="themeModeText">日间模式</span></button>
<button id="alertButton" class="icon-button alert-button" type="button" title="提醒中心" aria-label="提醒中心"><i data-lucide="bell"></i><span id="alertBadge" class="alert-badge" hidden>0</span></button>
<button id="assistantButton" class="icon-button assistant-button" type="button" title="复盘助手" aria-label="复盘助手"><i data-lucide="message-circle-more"></i></button>
<button id="headerMenuButton" class="icon-button header-menu-button" type="button" title="打开命令菜单" aria-label="打开命令菜单" aria-expanded="false" aria-controls="headerCommandGroup"><i data-lucide="ellipsis"></i></button>
@@ -93,12 +137,12 @@
<button class="button mobile-command-shortcut" type="button" data-mobile-command-target="alertButton"><i data-lucide="bell"></i><span>提醒</span></button>
<button class="button mobile-command-shortcut" type="button" data-mobile-command-target="assistantButton"><i data-lucide="message-circle-more"></i><span>助手</span></button>
</div>
<button id="refreshButton" class="button command-button" type="button"><i data-lucide="refresh-cw"></i><span>刷新</span></button>
<button id="syncButton" class="button primary command-button" type="button" hidden><i data-lucide="cloud-download"></i><span>后台刷新</span></button>
<button id="settingsButton" class="button command-button" type="button" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
<button id="refreshButton" class="button command-button" type="button" title="刷新"><i data-lucide="refresh-cw"></i><span>刷新</span></button>
<button id="syncButton" class="button primary command-button" type="button" title="后台刷新" hidden><i data-lucide="cloud-download"></i><span>后台刷新</span></button>
<button id="settingsButton" class="button command-button" type="button" title="系统管理" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
<div class="account-menu-shell">
<div id="accountRoleBadges" class="account-role-badges" aria-label="账号身份">
<span id="accountAdminBadge" class="account-role-badge admin-role-badge" hidden><i data-lucide="shield-check"></i><span>管理员</span></span>
<span id="accountAdminBadge" class="account-role-badge admin-role-badge" title="管理员" hidden><i data-lucide="shield-check"></i><span>管理员</span></span>
<button id="accountVipBadge" class="account-role-badge vip-role-badge" type="button" title="查看会员状态" hidden><b aria-hidden="true">V</b><span id="accountVipLabel">非会员</span></button>
</div>
<button id="accountButton" class="button account-button command-button" type="button" title="账号菜单" aria-haspopup="menu" aria-expanded="false" aria-controls="accountDropdown"><i data-lucide="circle-user-round"></i><span id="accountName">--</span><i class="account-menu-chevron" data-lucide="chevron-down"></i></button>
@@ -169,47 +213,6 @@
</select>
<i data-lucide="chevron-down" aria-hidden="true"></i>
</label>
<section class="overview-strip" aria-label="当日复盘指标" data-overview-expanded="false">
<div class="row">
<div class="sentiment-block">
<div class="sentiment-gauge" id="sentimentGauge">
<span id="sentimentScore">--</span>
</div>
<div>
<span class="metric-label">市场情绪</span>
<strong id="sentimentText" class="sentiment-text">等待数据</strong>
</div>
</div>
<div class="metric">
<span class="metric-label">涨停</span>
<strong id="limitUpMetric" class="metric-value up">--</strong>
</div>
<div class="metric">
<span class="metric-label">跌停</span>
<strong id="limitDownMetric" class="metric-value down">--</strong>
</div>
<div class="metric">
<span class="metric-label">炸板</span>
<strong id="brokenMetric" class="metric-value warning">--</strong>
</div>
<div class="metric">
<span class="metric-label">封板率</span>
<strong id="sealRateMetric" class="metric-value">--</strong>
</div>
<div class="metric">
<span class="metric-label">两市成交</span>
<strong id="amountMetric" class="metric-value">--</strong>
</div>
<div class="metric metric-wide">
<span class="metric-label">数据日期</span>
<strong id="dataDateMetric" class="metric-value small">--</strong>
</div>
<button id="overviewToggle" class="overview-toggle" type="button" aria-expanded="false" title="展开市场详情">
<span>展开详情</span><i data-lucide="chevron-down" aria-hidden="true"></i>
</button>
</div>
</section>
<!-- Registered page fragments mount here. -->
</main>
</div>
+14 -14
View File
@@ -42,15 +42,15 @@
];
const fragments = [
["pools", "/pages/pools/page.html?v=20260803-1", ["limitPool", "brokenView", "downView", "yesterdayView", "performanceView"]],
["pools", "/pages/pools/page.html?v=20260820-1", ["limitPool", "brokenView", "downView", "yesterdayView", "performanceView"]],
["sentiment", "/pages/sentiment/page.html?v=20260803-1", ["sentimentCycleView"]],
["heaven", "/pages/heaven/page.html?v=20260803-1", ["heavenView"]],
["ladder", "/pages/ladder/page.html?v=20260803-1", ["ladderView"]],
["ladder", "/pages/ladder/page.html?v=20260820-1", ["ladderView"]],
["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]],
["mentor", "/pages/mentor/page.html?v=20260806-2", ["mentorView"]],
["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]],
["mentor", "/pages/mentor/page.html?v=20260820-1", ["mentorView"]],
["rotation", "/pages/rotation/page.html?v=20260820-1", ["rotationView"]],
["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]],
["themes", "/pages/themes/page.html?v=20260803-1", ["themeLibraryView"]],
["themes", "/pages/themes/page.html?v=20260820-1", ["themeLibraryView"]],
["popularity", "/pages/popularity/page.html?v=20260803-1", ["popularityView"]],
["dragon_tiger", "/pages/dragon-tiger/page.html?v=20260803-1", ["dragonView"]],
["review", "/pages/review/page.html?v=20260803-1", ["reviewWorkspaceView"]],
@@ -66,7 +66,7 @@
"/shared/components.js?v=20260729-1",
"/pages/runtime.js?v=20260729-1",
"/pages/sentiment/page.js?v=20260729-1",
"/pages/pools/page.js?v=20260729-1",
"/pages/pools/page.js?v=20260820-1",
"/pages/market/breadth.js?v=20260803-1",
"/pages/market/charts.js?v=20260803-1",
"/pages/market/entity-detail.js?v=20260803-1",
@@ -74,19 +74,19 @@
"/pages/market/preview.js?v=20260806-1",
"/pages/market/search.js?v=20260803-1",
"/pages/market/bindings.js?v=20260803-1",
"/pages/ladder/page.js?v=20260729-1",
"/pages/rotation/page.js?v=20260729-1",
"/pages/ladder/page.js?v=20260820-1",
"/pages/rotation/page.js?v=20260820-1",
"/pages/auction/page.js?v=20260729-1",
"/pages/themes/page.js?v=20260729-1",
"/pages/popularity/page.js?v=20260729-1",
"/pages/dragon-tiger/page.js?v=20260806-1",
"/pages/themes/page.js?v=20260820-1",
"/pages/popularity/page.js?v=20260820-1",
"/pages/dragon-tiger/page.js?v=20260820-1",
"/pages/screener/page.js?v=20260806-1",
"/pages/mentor/page.js?v=20260806-2",
"/pages/mentor/page.js?v=20260820-1",
"/pages/heaven/page.js?v=20260729-1",
"/pages/review/page.js?v=20260729-1",
"/shared/state.js?v=20260729-1",
"/shared/api.js?v=20260729-1",
"/shared/shell.js?v=20260806-1",
"/shared/shell.js?v=20260820-2",
"/shared/export.js?v=20260731-1",
"/pages/heaven/loading-v2.js?v=20260728-2",
"/shared/context.js?v=20260804-1",
@@ -94,7 +94,7 @@
"/shared/application.js?v=20260803-1",
"/shared/table.js?v=20260803-1",
"/shared/theme.js?v=20260803-1",
"/shared/dashboard.js?v=20260803-1",
"/shared/dashboard.js?v=20260820-1",
"/shared/session.js?v=20260803-1",
"/shared/admin.js?v=20260803-1",
"/app.js?v=20260803-2",
+47 -40
View File
@@ -1,8 +1,8 @@
/* Canonical CSS owner: auction. Historical layers consolidated 2026-08-02. */
#auctionView .section-toolbar h2 {
font-size: 17px;
font-size: var(--font-size-page-title);
font-weight: 800;
font-weight: var(--font-weight-semibold);
}
@media (max-width: 767px) {
@@ -354,9 +354,9 @@
}
.auction-phase-notice[data-phase="archive"] {
background: rgb(238, 242, 246);
background: var(--surface-muted);
color: rgb(89, 101, 116);
color: var(--text-2);
}
.auction-phase-marker {
@@ -374,7 +374,7 @@
.auction-table {
min-width: 860px;
font-size: 12.5px;
font-size: var(--font-size-table);
}
.auction-table tbody tr:hover td {
@@ -462,7 +462,7 @@
transition: opacity var(--motion-fast) ease;
background: rgb(201, 214, 238);
background: var(--accent-soft);
}
.auction-amount-average {
@@ -522,7 +522,7 @@
}
.auction-table td {
height: 44px;
height: 40px;
padding-right: 12px;
@@ -530,7 +530,7 @@
border-right: 0px;
border-bottom-color: rgb(232, 237, 241);
border-bottom-color: var(--border);
}
.auction-table th {
@@ -540,13 +540,13 @@
border-right: 0px;
height: 38px;
height: 36px;
color: rgb(107, 114, 128);
color: var(--text-2);
font-size: 12px;
font-size: var(--font-size-caption);
border-bottom-color: rgb(232, 237, 241);
border-bottom-color: var(--border);
background: var(--table-header);
}
@@ -694,9 +694,9 @@
color: var(--r2-ink);
font-size: 17px;
font-size: var(--font-size-page-title);
font-weight: 800;
font-weight: var(--font-weight-semibold);
letter-spacing: 0px;
}
@@ -812,13 +812,13 @@
100% {
opacity: 0.42;
box-shadow: rgba(37, 99, 235, 0.18) 0px 0px 0px 0px;
box-shadow: 0 0 0 0 var(--focus-ring);
}
50% {
opacity: 1;
box-shadow: rgba(37, 99, 235, 0) 0px 0px 0px 4px;
box-shadow: 0 0 0 4px transparent;
}
}
@@ -896,7 +896,9 @@
.auction-export-button,
.auction-refresh-button {
min-height: 30px;
min-height: 32px;
height: 32px;
display: inline-flex;
@@ -906,17 +908,17 @@
gap: 5px;
padding: 5px 11px;
padding: 0 12px;
border: 1px solid var(--r2-line);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
background: var(--surface);
color: var(--text-primary);
font-size: 12px;
font-size: var(--font-size-label);
font-weight: 500;
@@ -1144,7 +1146,7 @@
.auction-search-v2 {
min-width: 0px;
height: 31px;
height: 32px;
display: flex;
@@ -1154,9 +1156,9 @@
padding: 0px 9px;
border: 1px solid var(--r2-line);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
background: var(--surface);
}
@@ -1193,7 +1195,7 @@
.auction-filter-segments button:focus-visible,
.auction-refresh-button:focus-visible,
.auction-tabs-v2 button:focus-visible {
outline: rgba(37, 99, 235, 0.28) solid 2px;
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@@ -1219,7 +1221,7 @@
border-collapse: collapse;
font-size: 12.5px;
font-size: var(--font-size-table);
}
.auction-table-v2 thead th {
@@ -1229,12 +1231,16 @@
z-index: 2;
height: 35px;
height: 36px;
padding: 0 12px;
background: var(--table-header);
color: var(--r2-sub);
font-size: var(--font-size-caption);
font-weight: 600;
text-align: left;
@@ -1242,6 +1248,11 @@
white-space: nowrap;
}
.auction-table-v2 thead th:first-child,
.auction-table-v2 tbody td:first-child {
text-align: left;
}
.auction-table-v2 thead th.number {
text-align: right;
}
@@ -1269,7 +1280,9 @@
}
.auction-table-v2 tbody td {
height: 47px;
height: 40px;
padding: 0 12px;
border-bottom: 1px solid var(--r2-line-soft);
@@ -1819,7 +1832,7 @@
border-radius: 3px 3px 0px 0px;
background: rgb(201, 214, 238);
background: var(--accent-soft);
transition: opacity 180ms, transform 180ms;
@@ -1915,7 +1928,7 @@
border-radius: 2px;
background: rgb(201, 214, 238);
background: var(--accent-soft);
}
.auction-volume-legend i.current {
@@ -2089,10 +2102,6 @@
overflow: hidden;
}
body[data-active-view="auctionView"] .overview-strip {
flex: 0 0 auto;
}
body[data-active-view="auctionView"] #auctionView.active-view {
min-height: 0px;
@@ -2138,11 +2147,9 @@
}
.auction-table-v2 tbody td {
height: 43px;
height: 40px;
padding-top: 6px;
padding-bottom: 6px;
padding: 0 12px;
}
}
@@ -2165,9 +2172,9 @@
}
.auc-head h2 {
font-size: 17px;
font-size: var(--font-size-page-title);
font-weight: 800;
font-weight: var(--font-weight-semibold);
}
.auc-grid {
+33 -19
View File
@@ -82,7 +82,7 @@
}
.dragon-operation-table th {
background: rgb(232, 240, 244);
background: var(--table-header);
}
.seat-cell {
@@ -952,6 +952,10 @@
margin: 0px;
color: var(--r2-ink);
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
}
.dragon-title-v2 > span {
@@ -991,7 +995,7 @@
align-items: center;
min-height: 34px;
min-height: 32px;
padding: 3px;
@@ -1003,19 +1007,19 @@
}
.dragon-view-tabs-v2 button {
min-height: 27px;
min-height: 26px;
padding: 0px 13px;
border: 0px;
border-radius: 5px;
border-radius: 6px;
background: transparent;
color: var(--r2-sub);
font-size: 11px;
font-size: var(--font-size-label);
cursor: pointer;
}
@@ -1037,11 +1041,15 @@
}
.dragon-action-v2 {
min-height: 34px;
min-height: 32px;
padding: 0px 11px;
height: 32px;
border-radius: 7px;
padding: 0px 12px;
border-radius: 8px;
font-size: var(--font-size-label);
}
.dragon-action-v2 .lucide {
@@ -1101,9 +1109,9 @@
}
.dragon-summary-v2 .dragon-metric span {
color: var(--r2-sub);
color: var(--text-3);
font-size: 11px;
font-size: var(--font-size-caption);
}
.dragon-summary-v2 .dragon-metric strong {
@@ -1111,9 +1119,9 @@
color: var(--r2-ink);
font-size: 18px;
font-size: var(--font-size-metric);
font-weight: 750;
font-weight: var(--font-weight-semibold);
font-variant-numeric: tabular-nums;
}
@@ -1245,7 +1253,7 @@
width: 230px;
height: 33px;
height: 32px;
flex: 0 0 auto;
@@ -1253,9 +1261,9 @@
padding: 0px 10px;
border: 1px solid rgb(216, 221, 229);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
color: var(--r2-faint);
@@ -1263,9 +1271,9 @@
}
.dragon-search-v2:focus-within {
border-color: rgb(150, 181, 242);
border-color: var(--accent);
box-shadow: rgba(37, 99, 235, 0.09) 0px 0px 0px 3px;
box-shadow: 0 0 0 2px var(--focus-ring);
}
.dragon-search-v2 .lucide {
@@ -1421,7 +1429,7 @@
#dragonView .dragon-operation-table :is(th, td).row-number {
padding-inline: 8px;
text-align: center;
text-align: left;
}
#dragonView .dragon-operation-table td.stock-code {
@@ -1435,6 +1443,12 @@
z-index: 2;
height: 36px;
padding: 0 12px;
font-size: var(--font-size-caption);
background: var(--surface-subtle);
}
@@ -2389,7 +2403,7 @@ table.tbl {
border-collapse: collapse;
font-size: 12.5px;
font-size: var(--font-size-table);
}
.tbl .num {
+2 -2
View File
@@ -299,10 +299,10 @@ function renderDragonTraderDetail(trader) {
<div class="trader-operations table-frame tbl-wrap">
<table class="data-table tbl dragon-operation-table">
<colgroup><col class="dragon-col-index"><col class="dragon-col-stock"><col class="dragon-col-direction"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-number"><col class="dragon-col-seat"><col class="dragon-col-reason"></colgroup>
<thead><tr><th class="row-number num">序号</th><th>股票</th><th>方向</th><th class="number num">涨幅(%</th><th class="number num">买入(百万)</th><th class="number num">卖出(百万)</th><th class="number num">净额(百万)</th><th>关联席位</th><th class="reason-column">标签 / 上榜原因</th></tr></thead>
<thead><tr><th class="row-number">序号</th><th>股票</th><th>方向</th><th class="number num">涨幅(%</th><th class="number num">买入(百万)</th><th class="number num">卖出(百万)</th><th class="number num">净额(百万)</th><th>关联席位</th><th class="reason-column">标签 / 上榜原因</th></tr></thead>
<tbody>${(trader.operations || []).map((operation, index) => `
<tr data-code="${escapeHtml(operation.code)}">
<td class="row-number num">${index + 1}</td>
<td class="row-number">${index + 1}</td>
<td><strong class="sname">${escapeHtml(operation.name)}</strong><span class="scode">${escapeHtml(operation.code)}</span></td>
<td><span class="direction-label ${changeClass(operation.net_buy_million)}">${escapeHtml(operation.direction)}</span></td>
<td class="number num ${operation.change == null ? "" : changeClass(operation.change)}">${operation.change == null ? "" : signed(operation.change)}</td>
+26 -18
View File
@@ -14,7 +14,7 @@
padding: 9px 4px;
border-bottom: 1px solid rgb(232, 236, 239);
border-bottom: 1px solid var(--border);
cursor: pointer;
}
@@ -128,7 +128,7 @@
gap: 5px;
background: rgb(247, 249, 250);
background: var(--surface-muted);
cursor: pointer;
@@ -140,11 +140,11 @@
border: 1px dashed var(--border-strong);
border-radius: 6px;
border-radius: 8px;
color: var(--text-secondary);
font-size: 11px;
font-size: var(--font-size-caption);
white-space: nowrap;
}
@@ -152,7 +152,7 @@
.ladder-gap {
min-height: 68px;
background: repeating-linear-gradient(135deg, rgb(255, 255, 255), rgb(255, 255, 255) 9px, rgb(250, 251, 252) 9px, rgb(250, 251, 252) 18px);
background: repeating-linear-gradient(135deg, var(--surface), var(--surface) 9px, var(--surface-subtle) 9px, var(--surface-subtle) 18px);
}
.ladder-gap-note {
@@ -353,6 +353,12 @@
box-shadow: none;
}
.ladder-page-head .section-title-group h2 {
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
}
.ladder-page-head {
margin-bottom: 12px;
}
@@ -384,13 +390,15 @@
}
.ladder-sort-segment button {
min-height: 28px;
padding: 4px 12px;
border-radius: 6px;
color: var(--r2-sub);
font-size: 12px;
font-size: var(--font-size-label);
}
.ladder-sort-segment button.active {
@@ -448,11 +456,11 @@
border-right: 1px solid var(--r2-line-soft);
background: linear-gradient(90deg, color-mix(in srgb, var(--tier-color) 8%, #fff), #fff);
background: linear-gradient(90deg, color-mix(in srgb, var(--tier-color) 8%, var(--surface)), var(--surface));
}
.market-ladder-tier.is-gap .market-ladder-label {
background: repeating-linear-gradient(45deg, rgb(250, 250, 250), rgb(250, 250, 250) 8px, rgb(243, 244, 246) 8px, rgb(243, 244, 246) 16px);
background: repeating-linear-gradient(45deg, var(--surface-subtle), var(--surface-subtle) 8px, var(--surface-muted) 8px, var(--surface-muted) 16px);
}
.market-ladder-level {
@@ -571,9 +579,9 @@
white-space: nowrap;
font-size: 13px;
font-size: var(--font-size-table);
font-weight: 800;
font-weight: var(--font-weight-semibold);
}
.market-ladder-stock-first .stock-code {
@@ -613,7 +621,7 @@
.market-ladder-tag.one-price {
background: var(--r2-up-soft);
color: rgb(194, 46, 46);
color: var(--r2-up);
font-weight: 700;
}
@@ -762,9 +770,9 @@
.market-ladder-insight-card > header h3 {
margin: 0px;
font-size: 13.5px;
font-size: var(--font-size-card-title);
font-weight: 700;
font-weight: var(--font-weight-semibold);
}
.market-ladder-insight-card > header span {
@@ -796,9 +804,9 @@
.market-ladder-apex strong {
color: var(--r2-up);
font-size: 26px;
font-size: var(--font-size-metric);
font-weight: 800;
font-weight: var(--font-weight-semibold);
}
.market-ladder-apex em {
@@ -900,7 +908,7 @@
}
.market-ladder-pyramid-row.is-gap > i {
background: repeating-linear-gradient(45deg, rgb(229, 231, 235), rgb(229, 231, 235) 4px, rgb(243, 244, 246) 4px, rgb(243, 244, 246) 8px);
background: repeating-linear-gradient(45deg, var(--border), var(--border) 4px, var(--surface-muted) 4px, var(--surface-muted) 8px);
}
.market-ladder-pyramid-row.is-gap > i b {
@@ -968,11 +976,11 @@
}
.market-ladder-rate-list > div > i b.is-low {
background: rgb(245, 158, 11);
background: var(--r2-amber);
}
.market-ladder-rate-list > div > i b.is-zero {
background: rgb(209, 213, 219);
background: var(--border-strong);
}
.market-ladder-rate-list > div > strong {
+1 -1
View File
@@ -51,7 +51,7 @@ function renderLadderBoard(ladders) {
const stocks = expanded ? groupStocks : groupStocks.slice(0, limit);
const remaining = Math.max(0, groupStocks.length - stocks.length);
const label = group.label || (level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}`);
const color = { 1: "#2563eb", 2: "#16a34a", 3: "#d97706", 4: "#e04536" }[level] || "#9ca3af";
const color = { 1: "#3370ff", 2: "#16a34a", 3: "#b45309", 4: "#e04536" }[level] || "#8f959e";
return `
<section class="market-ladder-tier ${number(group.count) ? "" : "is-gap"}" data-ladder-level-card="${level}">
<div class="market-ladder-label" style="--tier-color:${color}"><div class="market-ladder-level"><span class="market-ladder-dot"></span>${escapeHtml(label)}</div><div class="market-ladder-count">${number(group.count)} 只</div>${number(group.count) && level > 1 ? `<div class="market-ladder-rate">${escapeHtml(label)} · <b>${formatNumber(number(group.count) / Math.max(number(groupMap.get(level - 1)?.count), 1) * 100, 1)}%</b></div>` : ""}</div>
+6 -5
View File
@@ -790,7 +790,8 @@
border-radius: 4px;
}
:is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) {
:is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog)),
:root[data-theme="dark"] :is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) {
--dialog-line: var(--border);
--dialog-line-strong: var(--border-strong);
@@ -803,17 +804,17 @@
border: 1px solid var(--dialog-line-strong);
border-radius: 10px;
border-radius: var(--size-radius-dialog);
background: var(--surface);
color: var(--dialog-ink);
box-shadow: rgba(25, 36, 48, 0.2) 0px 26px 72px, rgba(25, 36, 48, 0.08) 0px 4px 14px;
box-shadow: var(--shadow-float);
}
:is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog))::backdrop {
background: rgba(28, 39, 50, 0.46);
background: var(--backdrop);
backdrop-filter: blur(3px);
}
@@ -1133,7 +1134,7 @@
margin: 8px auto;
border-radius: 8px;
border-radius: var(--size-radius-dialog);
}
:is(.stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) .dialog-header {
+764 -355
View File
File diff suppressed because it is too large Load Diff
+59 -32
View File
@@ -1,35 +1,34 @@
<section id="mentorView" class="workspace-view page member-feature-view redesigned-mentor-view">
<div class="member-gate" hidden><div class="member-gate-icon"><i data-lucide="lock-keyhole"></i></div><div><strong>问师仅对会员开放</strong><span>开通会员后可使用思维模型进行对话。会员状态可从顶部账号标识进入。</span></div></div>
<header class="section-toolbar lad-head mentor-page-header">
<div class="section-title-group mentor-page-title">
<div id="mentorNotice" class="inline-notice" hidden></div>
<header class="mentor-page-header" aria-label="问师">
<div class="mentor-page-title">
<h2>问师</h2>
<span class="section-subtitle">与不同交易思维模型持续对话 · <span id="mentorDataDate">--</span></span>
<p id="mentorPageSubtitle">与不同交易思维模型持续对话 · 数据日期 --</p>
</div>
</header>
<div id="mentorNotice" class="inline-notice" hidden></div>
<div class="mentor-layout">
<aside class="mentor-sidebar" aria-label="思维模型目录">
<div class="mentor-directory-heading">
<div class="mentor-directory-title"><strong>联系人</strong><span id="mentorCount">0 位</span></div>
<button id="mentorSortToggle" class="mentor-sort-toggle" type="button" aria-pressed="false" title="整理联系人顺序"><i data-lucide="list-ordered"></i><span>整理</span></button>
</div>
<div class="mentor-directory-tools">
<label class="mentor-search-field">
<span class="visually-hidden">搜索思维模型</span>
<i data-lucide="search"></i>
<input id="mentorSearchInput" type="search" maxlength="50" placeholder="搜索联系人或标签" autocomplete="off">
</label>
<div class="mentor-evidence-filters" role="group" aria-label="按素材等级筛选">
<button class="active" type="button" data-mentor-grade="all">全部</button>
<button type="button" data-mentor-grade="A">A级</button>
<button type="button" data-mentor-grade="B">B级</button>
<button type="button" data-mentor-grade="C">C</button>
<div class="mentor-filter-menu">
<button id="mentorFilterToggle" class="mentor-filter-toggle" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="筛选思维模型" title="筛选思维模型"><i data-lucide="filter"></i></button>
<div id="mentorFilterOptions" class="mentor-filter-options" role="menu" hidden>
<button type="button" class="active" data-mentor-grade="all" role="menuitem">全部<span id="mentorCount">0 位</span></button>
<button type="button" data-mentor-grade="A" role="menuitem">A</button>
<button type="button" data-mentor-grade="B" role="menuitem">B级</button>
<button type="button" data-mentor-grade="C" role="menuitem">C级</button>
</div>
</div>
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p>
<button id="mentorSortToggle" class="mentor-sort-toggle" type="button" aria-pressed="false" title="整理联系人顺序"><i data-lucide="list-ordered"></i><span>整理</span></button>
</div>
<div id="mentorList" class="mentor-list"></div>
<div id="mentorListEmpty" class="mentor-list-empty" hidden>没有符合条件的思维模型</div>
<p class="mentor-evidence-legend">等级仅表示公开素材完整度</p>
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p>
</aside>
<section class="mentor-chat-panel" aria-label="问师对话">
@@ -37,11 +36,16 @@
<div class="mentor-chat-identity">
<span id="activeMentorAvatar" class="mentor-avatar" aria-hidden="true"></span>
<div>
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3><span id="activeMentorBadges" class="mentor-active-badges"></span></div>
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3></div>
<p id="activeMentorStatus">思维模型已就绪</p>
</div>
</div>
<button id="clearMentorChatButton" class="icon-button mentor-clear-button" type="button" disabled aria-label="清空当前对话" title="清空对话"><i data-lucide="trash-2"></i></button>
<div class="mentor-chat-actions">
<button id="mentorPinButton" class="icon-button mentor-header-action" type="button" aria-label="置顶当前思维模型" title="置顶"><i data-lucide="pin"></i></button>
<button id="mentorNoteButton" class="icon-button mentor-header-action" type="button" aria-label="备注标签" title="备注标签"><i data-lucide="tag"></i></button>
<button id="mentorProfileButton" class="icon-button mentor-header-action" type="button" aria-label="游资档案" title="游资档案"><i data-lucide="id-card"></i></button>
<button id="clearMentorChatButton" class="icon-button mentor-clear-button" type="button" disabled aria-label="清空当前对话" title="清空对话"><i data-lucide="trash-2"></i></button>
</div>
</header>
<div id="mentorMessages" class="mentor-messages" aria-live="polite"></div>
<div id="mentorQuickPrompts" class="mentor-quick-prompts">
@@ -52,38 +56,61 @@
<button type="button" data-mentor-prompt="现在最需要防范的风险是什么?"><i data-lucide="shield-alert"></i>风险检查</button>
</div>
<form id="mentorChatForm" class="mentor-chat-form">
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
<div class="mentor-composer-main">
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
<span class="mentor-composer-hint">Enter 发送 · Shift + Enter 换行</span>
</div>
<div class="mentor-composer-actions">
<span>Enter 发送 · Shift + Enter 换行</span>
<button id="stopMentorQuestion" class="button ghost mentor-stop-button" type="button" hidden><i data-lucide="square"></i><span>停止</span></button>
<button id="sendMentorQuestion" class="button primary mentor-send-button" type="submit"><i data-lucide="send-horizontal"></i><span>发送</span></button>
</div>
</form>
<p class="mentor-disclaimer">基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。</p>
</section>
<aside class="mentor-profile-panel" aria-label="当前思维模型资料">
</div>
<footer class="mentor-workspace-footer">
<span class="mentor-evidence-legend">等级仅表示公开素材完整度</span>
<p class="mentor-disclaimer">基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。</p>
</footer>
<dialog id="mentorNoteDialog" class="mentor-floating-dialog mentor-note-dialog" aria-labelledby="mentorNoteDialogTitle">
<div class="mentor-dialog-head">
<div><span class="dialog-eyebrow">问师</span><h3 id="mentorNoteDialogTitle">备注标签</h3></div>
<button type="button" class="icon-button mentor-dialog-close" data-mentor-dialog-close="mentorNoteDialog" aria-label="关闭" title="关闭"><i data-lucide="x"></i></button>
</div>
<div class="mentor-dialog-body">
<label class="visually-hidden" for="mentorNoteInput">备注标签</label>
<textarea id="mentorNoteInput" maxlength="2000" placeholder="为该思维模型记录备注或标签"></textarea>
</div>
</dialog>
<dialog id="mentorProfileDialog" class="mentor-floating-dialog mentor-profile-dialog" aria-labelledby="mentorProfileDialogTitle">
<div class="mentor-dialog-head">
<div><span class="dialog-eyebrow">问师</span><h3 id="mentorProfileDialogTitle">游资档案</h3></div>
<button type="button" class="icon-button mentor-dialog-close" data-mentor-dialog-close="mentorProfileDialog" aria-label="关闭" title="关闭"><i data-lucide="x"></i></button>
</div>
<div class="mentor-profile-dialog-body">
<div class="mentor-profile-hero">
<span id="mentorProfileAvatar" class="mentor-profile-avatar" aria-hidden="true"></span>
<h3 id="mentorProfileName">--</h3>
<p id="mentorProfileTagline">--</p>
<div id="mentorProfileBadges" class="mentor-profile-badges"></div>
<span id="mentorProfileDialogAvatar" class="mentor-profile-avatar" aria-hidden="true"></span>
<h3 id="mentorProfileDialogName">--</h3>
<p id="mentorProfileDialogTagline">--</p>
<div id="mentorProfileDialogBadges" class="mentor-profile-badges"></div>
</div>
<section class="mentor-profile-section">
<h4>关注维度</h4>
<div id="activeMentorFocus" class="mentor-active-focus"></div>
<div id="mentorProfileDialogFocus" class="mentor-active-focus"></div>
</section>
<section class="mentor-profile-section">
<h4>资料依据</h4>
<strong id="mentorProfileSource">--</strong>
<p id="activeMentorEvidence">--</p>
<strong id="mentorProfileDialogSource">--</strong>
<p id="mentorProfileDialogEvidence">--</p>
</section>
<section class="mentor-profile-section mentor-profile-boundary">
<h4>数据边界</h4>
<p><i data-lucide="calendar-days"></i><span id="mentorProfileDataDate">--</span></p>
<p><i data-lucide="calendar-days"></i><span id="mentorProfileDialogDataDate">--</span></p>
<p><i data-lucide="database"></i><span>仅使用网页已提供的市场数据</span></p>
</section>
</aside>
</div>
</div>
</dialog>
</section>
+223 -37
View File
@@ -31,27 +31,100 @@ async function loadMentorSetup(force = false) {
}
}
const MENTOR_AVATAR_TONES = {
"xiaobai-perspective": "violet",
"kobe92-perspective": "blue",
"beijingchaojia-perspective": "green",
"chaojiyangjia-perspective": "orange",
"chenxiaoqun-perspective": "red",
"longfeihu-perspective": "teal",
"chuangshiji-perspective": "purple",
"foshanwuyingjiao-perspective": "yellow",
};
const MENTOR_AVATAR_TONE_CLASSES = [
"mentor-avatar-tone-violet", "mentor-avatar-tone-blue", "mentor-avatar-tone-green",
"mentor-avatar-tone-orange", "mentor-avatar-tone-red", "mentor-avatar-tone-teal",
"mentor-avatar-tone-purple", "mentor-avatar-tone-yellow",
];
function mentorAvatarTone(mentor) {
return MENTOR_AVATAR_TONES[String(mentor?.id || "")] || "blue";
}
function mentorAvatarToneClass(mentor) {
return `mentor-avatar-tone-${mentorAvatarTone(mentor)}`;
}
function applyMentorAvatarTone(element, toneClass) {
if (!(element instanceof Element)) return;
element.classList.remove(...MENTOR_AVATAR_TONE_CLASSES);
if (toneClass) element.classList.add(toneClass);
}
function renderMentorWorkspace() {
const setup = state.mentorSetup;
if (!setup) return;
const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null;
setText("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`);
setText("mentorPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`);
setText("currentPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`);
setText("activeMentorName", selected?.name || "--");
setText("mentorProfileName", selected?.name || "--");
setText("mentorProfileTagline", selected?.tagline || selected?.description || "--");
setText("mentorProfileSource", selected?.evidence?.label || "公开资料整理");
setText("mentorProfileDataDate", `行情数据 ${displayCompactDate(setup.trade_date)}`);
setText("activeMentorAvatar", mentorAvatarText(selected));
setText("mentorProfileAvatar", mentorAvatarText(selected));
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
document.querySelector("#mentorProfileBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
applyMentorAvatarTone(document.querySelector("#activeMentorAvatar"), mentorAvatarToneClass(selected));
const pinButton = document.querySelector("#mentorPinButton");
if (pinButton) {
pinButton.classList.toggle("active", Boolean(selected?.pinned));
pinButton.setAttribute("aria-label", selected?.pinned ? "取消置顶当前思维模型" : "置顶当前思维模型");
pinButton.setAttribute("aria-pressed", String(Boolean(selected?.pinned)));
}
populateMentorDialogs(selected);
renderMentorDirectory();
renderMentorMessages();
}
function populateMentorDialogs(selected) {
const noteInput = document.querySelector("#mentorNoteInput");
if (noteInput) {
noteInput.value = loadMentorNote(selected);
}
setText("mentorProfileDialogName", selected?.name || "--");
setText("mentorProfileDialogTagline", selected?.tagline || selected?.description || "--");
setText("mentorProfileDialogSource", selected?.evidence?.label || "公开资料整理");
setText("mentorProfileDialogEvidence", selected?.evidence?.note || selected?.description || "--");
setText("mentorProfileDialogDataDate", `行情数据 ${displayCompactDate(state.mentorSetup?.trade_date || elements.tradeDate.value)}`);
setText("mentorProfileDialogAvatar", mentorAvatarText(selected));
applyMentorAvatarTone(document.querySelector("#mentorProfileDialogAvatar"), mentorAvatarToneClass(selected));
document.querySelector("#mentorProfileDialogBadges").innerHTML = selected ? renderMentorBadges(selected) : "";
document.querySelector("#mentorProfileDialogFocus").innerHTML = (selected?.focus || []).slice(0, 4)
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
}
function mentorNoteStorageKey(selected) {
const accountId = state.user?.id || state.user?.username || "anon";
return `xiaobai-mentor-note-${accountId}-${String(selected?.id || "")}`;
}
function loadMentorNote(selected) {
if (!selected) return "";
try {
return window.localStorage.getItem(mentorNoteStorageKey(selected)) || "";
} catch (_error) {
return "";
}
}
function saveMentorNote() {
const selected = selectedMentor();
if (!selected) return;
const input = document.querySelector("#mentorNoteInput");
if (!input) return;
try {
window.localStorage.setItem(mentorNoteStorageKey(selected), input.value);
} catch (_error) {
showToast("备注保存失败");
}
}
function renderMentorDirectory() {
const mentors = state.mentorSetup?.mentors || [];
const query = state.mentorQuery;
@@ -76,9 +149,15 @@ function renderMentorDirectory() {
sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理";
document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode;
document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode;
document.querySelector("#mentorFilterToggle").disabled = state.mentorSortMode;
document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
button.disabled = state.mentorSortMode;
});
const filterOptions = document.querySelector("#mentorFilterOptions");
if (state.mentorSortMode && filterOptions && !filterOptions.hidden) {
filterOptions.hidden = true;
document.querySelector("#mentorFilterToggle").setAttribute("aria-expanded", "false");
}
const container = document.querySelector("#mentorList");
container.classList.toggle("is-sorting", state.mentorSortMode);
container.innerHTML = filtered.map((mentor) => {
@@ -88,7 +167,7 @@ function renderMentorDirectory() {
<article class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""} ${mentor.pinned ? "is-pinned" : ""}"
data-mentor-card="${escapeHtml(mentor.id)}" draggable="${state.mentorSortMode && !state.mentorSavingPreferences}">
<button type="button" class="mentor-option-main" data-mentor-id="${escapeHtml(mentor.id)}" aria-pressed="${mentor.id === state.selectedMentorId}" ${state.mentorLoading ? "disabled" : ""}>
<span class="mentor-avatar" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span>
<span class="mentor-avatar ${mentorAvatarToneClass(mentor)}" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span>
<span class="mentor-option-copy">
<span class="mentor-option-heading">
<strong>${escapeHtml(mentor.name)}</strong>
@@ -98,16 +177,12 @@ function renderMentorDirectory() {
<span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span>
</span>
</button>
${state.mentorSortMode ? `
<span class="mentor-option-tools">
<button type="button" class="mentor-pin-button ${mentor.pinned ? "active" : ""}" data-mentor-pin="${escapeHtml(mentor.id)}"
aria-label="${mentor.pinned ? "取消置顶" : "置顶"}${escapeHtml(mentor.name)}" title="${mentor.pinned ? "取消置顶" : "置顶"}" ${state.mentorSavingPreferences ? "disabled" : ""}>
<i data-lucide="pin"></i>
</button>
${state.mentorSortMode ? `
<button type="button" class="mentor-order-button" data-mentor-move="up" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="上移${escapeHtml(mentor.name)}" title="上移" ${groupIndex <= 0 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-up"></i></button>
<button type="button" class="mentor-order-button" data-mentor-move="down" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="下移${escapeHtml(mentor.name)}" title="下移" ${groupIndex >= group.length - 1 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-down"></i></button>
` : ""}
<button type="button" class="mentor-order-button" data-mentor-move="up" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="上移${escapeHtml(mentor.name)}" title="上移" ${groupIndex <= 0 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-up"></i></button>
<button type="button" class="mentor-order-button" data-mentor-move="down" data-mentor-target="${escapeHtml(mentor.id)}" aria-label="下移${escapeHtml(mentor.name)}" title="下移" ${groupIndex >= group.length - 1 || state.mentorSavingPreferences ? "disabled" : ""}><i data-lucide="chevron-down"></i></button>
</span>
` : ""}
</article>
`;
}).join("");
@@ -115,9 +190,6 @@ function renderMentorDirectory() {
document.querySelectorAll("[data-mentor-id]").forEach((button) => {
button.addEventListener("click", () => selectMentor(button.dataset.mentorId));
});
document.querySelectorAll("[data-mentor-pin]").forEach((button) => {
button.addEventListener("click", () => toggleMentorPin(button.dataset.mentorPin));
});
document.querySelectorAll("[data-mentor-move]").forEach((button) => {
button.addEventListener("click", () => moveMentor(button.dataset.mentorTarget, button.dataset.mentorMove));
});
@@ -143,6 +215,36 @@ function toggleMentorSortMode() {
renderMentorDirectory();
}
function toggleMentorFilterMenu() {
if (state.mentorSortMode || state.mentorLoading) return;
const options = document.querySelector("#mentorFilterOptions");
const toggle = document.querySelector("#mentorFilterToggle");
if (!options || !toggle) return;
const open = options.hidden;
options.hidden = !open;
toggle.setAttribute("aria-expanded", String(open));
if (open) {
const active = options.querySelector("[data-mentor-grade].active");
(active || options.querySelector("[data-mentor-grade]"))?.focus({ preventScroll: true });
}
}
function closeMentorFilterMenu() {
const options = document.querySelector("#mentorFilterOptions");
const toggle = document.querySelector("#mentorFilterToggle");
if (!options || options.hidden) return;
options.hidden = true;
toggle?.setAttribute("aria-expanded", "false");
}
function selectMentorGrade(grade) {
state.mentorGrade = grade || "all";
document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
button.classList.toggle("active", button.dataset.mentorGrade === state.mentorGrade);
});
renderMentorDirectory();
}
async function toggleMentorPin(mentorId) {
if (state.mentorSavingPreferences) return;
const mentors = state.mentorSetup?.mentors || [];
@@ -246,14 +348,17 @@ async function persistMentorPreferences() {
}
}
function renderMentorBadges(mentor, expanded = false) {
function renderMentorBadges(mentor) {
const badges = [];
if (mentor.private) {
badges.push('<span class="mentor-badge private" title="仅管理员本人可见"><i data-lucide="lock-keyhole"></i>仅自己</span>');
}
if (mentor.pinned) {
badges.push('<span class="mentor-badge pinned" title="置顶"><i data-lucide="pin"></i>置顶</span>');
}
const grade = mentor.evidence?.grade;
if (grade) {
badges.push(`<span class="mentor-badge evidence grade-${escapeHtml(grade.toLowerCase())}" title="${escapeHtml(mentor.evidence?.note || "素材等级")}">${escapeHtml(grade)}</span>`);
badges.push(`<span class="mentor-badge evidence grade-${escapeHtml(grade.toLowerCase())}" title="${escapeHtml(mentor.evidence?.note || "素材等级")}">${escapeHtml(grade)}</span>`);
}
return badges.join("");
}
@@ -262,6 +367,14 @@ function mentorAvatarText(mentor) {
return Array.from(String(mentor?.name || "师").trim())[0] || "师";
}
function mentorMessageTime(message) {
const parsed = new Date(String(message?.created_at || ""));
if (Number.isNaN(parsed.getTime())) {
return new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
}
return parsed.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false });
}
async function selectMentor(mentorId) {
if (mentorId === state.selectedMentorId) return;
state.selectedMentorId = mentorId;
@@ -287,27 +400,28 @@ function renderMentorMessages() {
} else {
container.innerHTML = state.mentorMessages.map((message) => `
<article class="mentor-message ${message.role} ${message.error ? "is-error" : ""}">
<span class="mentor-message-avatar" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
<span class="mentor-message-avatar ${message.role === "user" ? "mentor-avatar-tone-blue" : escapeHtml(mentorAvatarToneClass(selected))}" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
<div class="mentor-message-body">
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
<div class="mentor-message-label">${message.role === "user"
? escapeHtml(mentorMessageTime(message))
: `${escapeHtml(selected?.name || "问师")} · ${escapeHtml(mentorMessageTime(message))}`}</div>
<div class="mentor-message-content">${message.role === "assistant"
? (message.content ? formatMentorAnswer(message.content) : '<p class="mentor-loading-copy">正在读取复盘数据并推演...</p>')
: escapeHtml(message.content)}</div>
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
${renderMentorFollowUps(message)}
${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
</div>
</article>
`).join("");
}
document.querySelector("#mentorQuickPrompts").hidden = state.mentorMessages.length > 0 || state.mentorLoading;
document.querySelector("#mentorQuickPrompts").hidden = false;
document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading;
document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
document.querySelector("#sendMentorQuestion").hidden = state.mentorLoading;
document.querySelector("#stopMentorQuestion").hidden = !state.mentorLoading;
document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : "思维模型已就绪");
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : (selected?.tagline || selected?.description || "思维模型已就绪"));
container.querySelectorAll("[data-mentor-follow-up]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorFollowUp));
});
@@ -322,7 +436,7 @@ function renderMentorFollowUps(message) {
return `
<div class="mentor-follow-ups" aria-label="继续追问">
<span>继续追问</span>
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><i data-lucide="corner-down-right"></i><span>${escapeHtml(item)}</span></button>`).join("")}
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><span>${escapeHtml(item)}</span></button>`).join("")}
</div>
`;
}
@@ -342,6 +456,7 @@ async function sendMentorQuestion(event) {
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] };
state.mentorMessages.push(responseMessage);
input.value = "";
syncMentorComposerHeight();
state.mentorLoading = true;
state.mentorController = new AbortController();
hideMentorNotice();
@@ -362,7 +477,6 @@ async function sendMentorQuestion(event) {
scheduleMentorRender();
},
(meta) => {
responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`;
responseMessage.followUps = Array.isArray(meta.follow_ups)
? meta.follow_ups.filter((item) => typeof item === "string" && item.trim()).slice(0, 3)
: [];
@@ -431,6 +545,7 @@ async function streamMentorRequest(body, signal, onDelta, onMeta) {
function useMentorQuickPrompt(prompt) {
const input = document.querySelector("#mentorQuestion");
input.value = prompt || "";
syncMentorComposerHeight();
input.focus();
}
@@ -529,19 +644,24 @@ function bindMentorEvents() {
document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration);
document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
document.querySelector("#mentorFilterToggle").addEventListener("click", (event) => {
event.stopPropagation();
toggleMentorFilterMenu();
});
document.querySelector("#mentorSearchInput").addEventListener("input", (event) => {
state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
renderMentorDirectory();
});
document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
button.addEventListener("click", () => {
state.mentorGrade = button.dataset.mentorGrade || "all";
document.querySelectorAll("[data-mentor-grade]").forEach((item) => {
item.classList.toggle("active", item === button);
});
renderMentorDirectory();
selectMentorGrade(button.dataset.mentorGrade);
closeMentorFilterMenu();
});
});
document.addEventListener("click", (event) => {
if (event.target.closest(".mentor-filter-menu")) return;
closeMentorFilterMenu();
});
document.querySelectorAll("[data-mentor-prompt]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt));
});
@@ -550,4 +670,70 @@ function bindMentorEvents() {
event.preventDefault();
document.querySelector("#mentorChatForm").requestSubmit();
});
document.querySelector("#mentorQuestion").addEventListener("input", syncMentorComposerHeight);
document.querySelector("#mentorNoteButton").addEventListener("click", openMentorNoteDialog);
document.querySelector("#mentorProfileButton").addEventListener("click", openMentorProfileDialog);
document.querySelector("#mentorPinButton").addEventListener("click", () => toggleMentorPin(state.selectedMentorId));
document.querySelector("#mentorNoteInput").addEventListener("input", saveMentorNote);
document.querySelectorAll("[data-mentor-dialog-close]").forEach((button) => {
button.addEventListener("click", () => closeMentorDialog(button.dataset.mentorDialogClose));
});
window.addEventListener("resize", centerOpenMentorDialogs);
}
function selectedMentor() {
return (state.mentorSetup?.mentors || []).find((item) => item.id === state.selectedMentorId) || null;
}
function syncMentorComposerHeight() {
const input = document.querySelector("#mentorQuestion");
if (!input) return;
input.style.height = "0";
const nextHeight = Math.min(input.scrollHeight, 168);
input.style.height = `${nextHeight}px`;
input.style.overflowY = nextHeight >= 168 ? "auto" : "hidden";
}
function openMentorDialog(dialogId) {
const dialog = document.querySelector(`#${dialogId}`);
if (!(dialog instanceof HTMLDialogElement)) return;
if (!dialog.open) dialog.showModal();
positionMentorDialog(dialog);
}
function closeMentorDialog(dialogId) {
const dialog = document.querySelector(`#${dialogId}`);
if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close();
}
function openMentorNoteDialog() {
const input = document.querySelector("#mentorNoteInput");
if (input) input.value = loadMentorNote(selectedMentor());
openMentorDialog("mentorNoteDialog");
const noteInput = document.querySelector("#mentorNoteInput");
if (noteInput) requestAnimationFrame(() => noteInput.focus());
}
function openMentorProfileDialog() {
populateMentorDialogs(selectedMentor());
openMentorDialog("mentorProfileDialog");
}
function positionMentorDialog(dialog) {
if (!(dialog instanceof HTMLDialogElement) || !dialog.open) return;
const viewportPadding = 12;
const rect = dialog.getBoundingClientRect();
const width = Math.min(400, window.innerWidth - viewportPadding * 2);
const height = Math.min(rect.height, window.innerHeight - viewportPadding * 2);
dialog.style.top = `${Math.max(viewportPadding, Math.round((window.innerHeight - height) / 2))}px`;
dialog.style.left = `${Math.max(viewportPadding, Math.round((window.innerWidth - width) / 2))}px`;
dialog.style.right = "auto";
dialog.style.bottom = "auto";
dialog.style.margin = "0";
}
function centerOpenMentorDialogs() {
document.querySelectorAll("#mentorNoteDialog, #mentorProfileDialog").forEach((dialog) => {
if (dialog instanceof HTMLDialogElement && dialog.open) positionMentorDialog(dialog);
});
}
+84 -26
View File
@@ -78,7 +78,7 @@
color: var(--r2-faint);
text-align: right;
text-align: left;
}
.redesigned-pool-view .data-table .number {
@@ -207,7 +207,7 @@
color: var(--r2-faint);
text-align: right;
text-align: left;
}
.redesigned-broken-view .data-table .number {
@@ -335,7 +335,7 @@
color: var(--r2-faint);
text-align: right;
text-align: left;
}
.redesigned-down-view .data-table .number {
@@ -455,7 +455,7 @@
color: var(--r2-faint);
text-align: right;
text-align: left;
}
.redesigned-yesterday-view .data-table .number {
@@ -716,7 +716,6 @@
#brokenView .data-table thead th,
#downView .data-table thead th,
#limitPool .data-table thead th,
#performanceView .data-table thead th,
#yesterdayView .data-table thead th {
height: 32px;
@@ -726,9 +725,16 @@
font-size: 11.5px;
}
#limitPool .data-table thead th {
height: 36px;
padding: 0 8px;
font-size: var(--font-size-caption);
}
#brokenView .data-table tbody td,
#downView .data-table tbody td,
#limitPool .data-table tbody td,
#performanceView .data-table tbody td,
#yesterdayView .data-table tbody td {
height: 39px;
@@ -736,6 +742,12 @@
padding: 5px 9px;
}
#limitPool .data-table tbody td {
height: 40px;
padding: 0 8px;
}
#limitPool .main-grid {
grid-template-columns: minmax(0px, 1fr) 308px;
}
@@ -892,24 +904,6 @@
line-height: 1.6;
}
.pool-streak-tag {
display: inline-block;
padding: 1.5px 7px;
border: 1px solid transparent;
border-radius: 5px;
font-size: 11px;
line-height: 1.6;
background: var(--r2-up-soft);
color: var(--r2-up);
}
.pool-state-tag.one-word {
background: rgb(255, 243, 217);
@@ -2225,6 +2219,70 @@
table-layout: auto;
}
#limitPool #limitTable {
min-width: var(--table-wide);
width: 100%;
table-layout: auto;
}
#limitPool #limitTable.tbl thead th,
#limitPool .pool-table-card .data-table thead th {
height: 36px;
padding: 0 8px;
font-size: var(--font-size-caption);
}
#limitPool #limitTable.tbl tbody td,
#limitPool .pool-table-card .data-table tbody td {
height: 40px;
padding: 0 8px;
}
@media (min-width: 1440px) {
#limitPool #limitTable {
min-width: 0;
table-layout: fixed;
}
#limitPool #limitTable thead th {
width: auto;
min-width: 0;
}
#limitPool #limitTable thead th.row-number {
width: 36px;
}
#limitPool.redesigned-pool-view .pool-table-card.tbl-wrap {
overflow-x: hidden;
overflow-y: auto;
}
#limitPool #limitTable .reason-column {
min-width: 0;
overflow: hidden;
white-space: nowrap;
}
#limitPool #limitTable .pool-reason-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
#brokenTable,
#downTable,
#limitTable,
@@ -2235,9 +2293,9 @@
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .row-number {
padding-right: 8px;
padding-left: 8px;
padding-left: 12px;
text-align: center;
text-align: left;
}
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .reason-column {
+4 -4
View File
@@ -26,7 +26,7 @@
<table class="data-table tbl" id="limitTable">
<thead>
<tr>
<th class="row-number num" aria-label="序号">序号</th>
<th class="row-number" aria-label="序号">序号</th>
<th data-sort="name">股票</th>
<th class="number num sortable" data-sort="streak">连板<span class="arr"></span></th>
<th class="number num sortable" data-sort="change">涨幅(%<span class="arr"></span></th>
@@ -83,7 +83,7 @@
<table id="brokenTable" class="data-table tbl">
<thead>
<tr>
<th class="row-number num">序号</th>
<th class="row-number">序号</th>
<th>股票</th>
<th class="number num sortable" data-broken-sort="change">现价涨幅(%<span class="arr"></span></th>
<th class="number num sortable" data-auto-sort="true">距涨停(%<span class="arr"></span></th>
@@ -122,7 +122,7 @@
<table id="downTable" class="data-table tbl">
<thead>
<tr>
<th class="row-number num">序号</th>
<th class="row-number">序号</th>
<th>股票</th>
<th class="number num sortable" data-down-sort="change">跌幅(%<span class="arr"></span></th>
<th class="number num sortable" data-auto-sort="true">价格(元)<span class="arr"></span></th>
@@ -176,7 +176,7 @@
<table id="yesterdayTable" class="data-table tbl">
<thead>
<tr>
<th class="row-number num">序号</th>
<th class="row-number">序号</th>
<th>股票</th>
<th class="number num sortable" data-yesterday-sort="prior_streak">昨日高度(板)<span class="arr"></span></th>
<th class="number num sortable" data-yesterday-sort="current_change">今日涨幅(%<span class="arr"></span></th>
+5 -5
View File
@@ -28,9 +28,9 @@ function renderLimitTable() {
const body = document.querySelector("#limitTableBody");
body.innerHTML = rows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}">
<td class="row-number num muted">${index + 1}</td>
<td class="row-number muted">${index + 1}</td>
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
<td class="number num"><span class="pool-streak-tag tag red">${streakLabel(row.streak)}</span></td>
<td class="number num"><span class="streak-pill${number(row.streak) >= 4 ? " high" : ""}">${streakLabel(row.streak)}</span></td>
<td class="number num up">${signed(row.change)}</td>
<td class="number num">${formatNumber(row.price, 2)}</td>
<td>${escapeHtml(row.sector || "其他")}</td>
@@ -75,7 +75,7 @@ function renderBrokenTable(rows) {
const body = document.querySelector("#brokenTableBody");
body.innerHTML = visibleRows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}">
<td class="row-number num muted">${index + 1}</td>
<td class="row-number muted">${index + 1}</td>
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
<td class="number num ${changeClass(row.change)}" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
<td class="number num broken-limit-gap" data-sort-value="${row.limitGap}">${formatNumber(row.limitGap, 2)}</td>
@@ -155,7 +155,7 @@ function renderDownTable(rows) {
const body = document.querySelector("#downTableBody");
body.innerHTML = visibleRows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}">
<td class="row-number num muted">${index + 1}</td>
<td class="row-number muted">${index + 1}</td>
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
<td class="number num down" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
<td class="number num">${formatNumber(row.price, 2)}</td>
@@ -228,7 +228,7 @@ function renderYesterdayTable(rows) {
const body = document.querySelector("#yesterdayTableBody");
body.innerHTML = visibleRows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}">
<td class="row-number num muted">${index + 1}</td>
<td class="row-number muted">${index + 1}</td>
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
<td class="number num">${number(row.prior_streak)}</td>
<td class="number num ${changeClass(row.current_change)}">${signed(row.current_change)}</td>
+46 -41
View File
@@ -22,11 +22,11 @@
}
.popularity-source-tag.dual {
border-color: rgb(230, 199, 115);
border-color: var(--warn);
background: var(--amber-soft);
color: rgb(118, 83, 20);
color: var(--warn);
}
.popularity-concepts {
@@ -44,21 +44,21 @@
}
#popularityView .data-table {
font-size: 12px;
font-size: var(--font-size-table);
}
#popularityView .data-table thead th {
height: 32px;
height: 36px;
padding: 6px 9px;
padding: 0 12px;
font-size: 11.5px;
font-size: var(--font-size-caption);
}
#popularityView .data-table tbody td {
height: 39px;
height: 40px;
padding: 5px 9px;
padding: 0 12px;
}
.redesigned-popularity-view {
@@ -93,6 +93,10 @@
margin: 0px;
color: var(--r2-ink);
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
}
.popularity-title-v2 > span {
@@ -140,7 +144,7 @@
align-items: center;
min-height: 34px;
min-height: 32px;
padding: 3px;
@@ -152,19 +156,19 @@
}
.popularity-source-tabs-v2 button {
min-height: 27px;
min-height: 26px;
padding: 0px 13px;
border: 0px;
border-radius: 5px;
border-radius: 6px;
background: transparent;
color: var(--r2-sub);
font-size: 11px;
font-size: var(--font-size-label);
cursor: pointer;
}
@@ -190,11 +194,15 @@
}
.popularity-refresh-v2 {
min-height: 34px;
min-height: 32px;
padding: 0px 11px;
height: 32px;
border-radius: 7px;
padding: 0px 12px;
border-radius: 8px;
font-size: var(--font-size-label);
}
.popularity-refresh-v2 .lucide {
@@ -244,15 +252,15 @@
width: 3px;
background: rgb(199, 216, 251);
background: var(--accent-soft);
}
.popularity-glance-v2 article:nth-child(2)::before {
background: rgb(183, 221, 207);
background: var(--down-soft);
}
.popularity-glance-v2 article.consensus::before {
background: rgb(230, 199, 115);
background: var(--warn);
}
.popularity-glance-v2 article > span {
@@ -372,7 +380,7 @@
width: 230px;
height: 33px;
height: 32px;
flex: 0 0 auto;
@@ -380,9 +388,9 @@
padding: 0px 10px;
border: 1px solid rgb(216, 221, 229);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
color: var(--r2-faint);
@@ -390,9 +398,9 @@
}
.popularity-search-v2:focus-within {
border-color: rgb(150, 181, 242);
border-color: var(--accent);
box-shadow: rgba(37, 99, 235, 0.09) 0px 0px 0px 3px;
box-shadow: 0 0 0 2px var(--focus-ring);
}
.popularity-search-v2 .lucide {
@@ -446,15 +454,15 @@
z-index: 2;
height: 35px;
height: 36px;
padding: 7px 10px;
padding: 0 12px;
border-bottom-color: var(--r2-line);
color: var(--r2-sub);
font-size: 10.5px;
font-size: var(--font-size-caption);
}
.popularity-table-v2 thead th:nth-child(1) {
@@ -477,11 +485,11 @@
}
.popularity-table-v2 tbody td {
height: 43px;
height: 40px;
padding: 7px 10px;
padding: 0 12px;
font-size: 11.5px;
font-size: var(--font-size-table);
}
.popularity-table-v2 tbody tr {
@@ -541,9 +549,9 @@
color: var(--r2-ink);
font-size: 12px;
font-size: var(--font-size-table);
font-weight: 700;
font-weight: var(--font-weight-semibold);
text-overflow: ellipsis;
@@ -559,7 +567,7 @@
}
.popularity-list-rank-v2 {
color: rgb(64, 85, 115);
color: var(--text-2);
}
.popularity-movement-v2 {
@@ -611,10 +619,6 @@
overflow: hidden;
}
body[data-active-view="popularityView"] .overview-strip {
flex: 0 0 auto;
}
body[data-active-view="popularityView"] #popularityView.active-view {
min-height: 0px;
@@ -658,11 +662,9 @@
}
.popularity-table-v2 tbody td {
height: 39px;
height: 40px;
padding-top: 5px;
padding-bottom: 5px;
padding: 0 12px;
}
}
@@ -857,8 +859,11 @@
width: auto;
}
#popularityView .popularity-table-v2 th:first-child {
#popularityView .popularity-table-v2 th:first-child,
#popularityView .popularity-table-v2 td:first-child {
width: var(--col-rank);
text-align: left;
}
#popularityView .popularity-table-v2 th:nth-child(2) {
+2 -2
View File
@@ -50,7 +50,7 @@ function renderPopularityTable() {
setText("popularityTableTitle", `${sourceName}`);
setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜");
const headers = [
["排名", "number num"], ["股票", ""], ["最新价(元)", "number num"], ["涨跌幅(%", "number num"],
["排名", "row-number"], ["股票", ""], ["最新价(元)", "number num"], ["涨跌幅(%", "number num"],
...(source !== "dc" ? [["同花顺", "number num"]] : []),
...(source !== "ths" ? [["东方财富", "number num"]] : []),
["排名变化", "number num"], ["热门概念", ""], ...(!combined ? [["榜单状态", ""]] : []),
@@ -63,7 +63,7 @@ function renderPopularityTable() {
const move = row.rank_change;
const movement = move === null || move === undefined ? "新" : number(move) > 0 ? `${number(move)}` : number(move) < 0 ? `${Math.abs(number(move))}` : "持平";
return `<tr data-code="${escapeHtml(row.code)}">
<td class="number num popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td>
<td class="row-number popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td>
<td><div class="popularity-stock-v2"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><span class="stock-code scode">${escapeHtml(row.code)}</span></div></td>
<td class="number num">${row.price == null ? "" : formatNumber(row.price, 2)}</td>
<td class="number num ${row.change == null ? "" : changeClass(row.change)}">${row.change == null ? "" : signed(row.change)}</td>
+92 -91
View File
@@ -217,10 +217,6 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
}
}
body[data-active-view="reviewWorkspaceView"] .overview-strip {
display: grid;
}
.trade-journal-section .trade-log-summary {
min-height: 58px;
}
@@ -242,7 +238,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
border-radius: 10px;
box-shadow: var(--shadow-xs);
box-shadow: var(--shadow-card);
min-height: 0px !important;
}
@@ -276,15 +272,15 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
.watchlist-section .data-table thead th {
height: 32px;
height: 36px;
padding: 6px 12px;
padding: 0 12px;
}
.watchlist-section .data-table tbody td {
height: 44px;
height: 40px;
padding: 7px 12px;
padding: 0 12px;
}
.trade-journal-section .trade-log-summary:empty {
@@ -384,7 +380,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .review-history-toggle {
min-height: 30px;
min-height: 32px;
display: inline-flex;
@@ -398,13 +394,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
border: 1px solid var(--review-line);
border-radius: 7px;
border-radius: 8px;
background: var(--surface);
color: rgb(55, 65, 81);
color: var(--text-secondary);
font-size: 11.5px;
font-size: var(--font-size-label);
white-space: nowrap;
}
@@ -469,7 +465,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
background: var(--surface);
box-shadow: rgba(16, 24, 40, 0.05) 0px 1px 2px;
box-shadow: var(--shadow-card);
}
#reviewWorkspaceView .review-card-heading > div {
@@ -515,9 +511,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .data-table thead th {
height: 32px;
height: 36px;
padding: 6px 12px;
padding: 0 12px;
border-bottom: 1px solid var(--review-line-soft);
@@ -525,21 +521,21 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
color: var(--review-faint);
font-size: 10.5px;
font-size: var(--font-size-caption);
font-weight: 600;
font-weight: var(--font-weight-semibold);
}
#reviewWorkspaceView .data-table tbody td {
height: 48px;
height: 40px;
padding: 7px 12px;
padding: 0 12px;
border-bottom: 1px solid var(--review-line-soft);
color: rgb(55, 65, 81);
color: var(--text-primary);
font-size: 12px;
font-size: var(--font-size-table);
}
#reviewWorkspaceView .data-table tbody tr:last-child td {
@@ -559,17 +555,17 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
#reviewWorkspaceView .stock-cell strong {
color: var(--review-ink);
font-size: 12px;
font-size: var(--font-size-table);
}
#reviewWorkspaceView .stock-cell small {
color: var(--review-faint);
font-size: 10px;
font-size: var(--font-size-aux);
}
#reviewWorkspaceView .review-watch-mark {
color: rgb(209, 213, 219);
color: var(--text-tertiary);
font-size: 15px;
@@ -577,7 +573,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .review-watch-mark.red {
color: rgb(224, 69, 54);
color: var(--market-up);
}
#reviewWorkspaceView .review-row-actions {
@@ -601,19 +597,19 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .table-action {
min-height: 25px;
min-height: 32px;
padding: 0px 6px;
border: 0px;
border-radius: 5px;
border-radius: 8px;
background: transparent;
color: var(--review-blue);
font-size: 10.5px;
font-size: var(--font-size-label);
}
#reviewWorkspaceView .table-action:hover {
@@ -621,13 +617,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .table-action.down {
color: rgb(139, 146, 158);
color: var(--text-tertiary);
}
#reviewWorkspaceView .table-action.down:hover {
background: var(--surface-hover);
color: rgb(209, 67, 67);
color: var(--market-up);
}
#reviewWorkspaceView .trade-log-table-frame .empty-state {
@@ -667,7 +663,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .trade-log-heading .button {
min-height: 29px;
min-height: 32px;
display: inline-flex;
@@ -679,13 +675,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
padding: 0px 10px;
border-radius: 7px;
border-radius: 8px;
background: var(--review-blue);
color: var(--text-inverse);
font-size: 11.5px;
font-size: var(--font-size-label);
}
#reviewWorkspaceView .trade-log-heading .button:hover {
@@ -831,35 +827,35 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .trade-action-add {
border-color: rgb(241, 196, 192);
border-color: var(--market-up);
background: rgb(253, 236, 234);
background: var(--market-up-soft);
color: rgb(214, 62, 50);
color: var(--market-up);
}
#reviewWorkspaceView .trade-action-buy {
border-color: rgb(241, 196, 192);
border-color: var(--market-up);
background: rgb(253, 236, 234);
background: var(--market-up-soft);
color: rgb(214, 62, 50);
color: var(--market-up);
}
#reviewWorkspaceView .trade-action-sell {
border-color: rgb(188, 225, 202);
border-color: var(--market-down);
background: rgb(234, 247, 239);
background: var(--market-down-soft);
color: rgb(22, 139, 67);
color: var(--market-down);
}
#reviewWorkspaceView .trade-action-trim {
border-color: rgb(188, 225, 202);
border-color: var(--market-down);
background: rgb(234, 247, 239);
background: var(--market-down-soft);
color: rgb(22, 139, 67);
color: var(--market-down);
}
#reviewWorkspaceView .trade-tags {
@@ -879,7 +875,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
background: var(--review-blue-soft);
color: rgb(82, 112, 167);
color: var(--action);
font-size: 8.5px;
}
@@ -899,7 +895,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
white-space: nowrap;
color: rgb(75, 85, 99);
color: var(--text-secondary);
font-size: 10.5px;
@@ -989,7 +985,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .journal-form .button {
min-height: 34px;
min-height: 32px;
display: inline-flex;
@@ -1001,13 +997,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
padding: 0px 14px;
border-radius: 7px;
border-radius: 8px;
background: var(--review-blue);
color: var(--text-inverse);
font-size: 12px;
font-size: var(--font-size-label);
}
#reviewWorkspaceView .journal-form .button:hover {
@@ -1247,9 +1243,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
color: var(--review-ink);
font-size: 19px;
font-size: var(--font-size-page-title);
font-weight: 750;
font-weight: var(--font-weight-semibold);
}
#reviewWorkspaceView .review-page-title .section-subtitle {
@@ -1261,7 +1257,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
white-space: nowrap;
font-size: 12px;
font-size: var(--font-size-caption);
}
#reviewWorkspaceView .review-card-heading {
@@ -1285,13 +1281,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
color: var(--review-ink);
font-size: 14.5px;
font-size: var(--font-size-card-title);
font-weight: 720;
font-weight: var(--font-weight-semibold);
}
#reviewWorkspaceView .review-add-watch {
min-height: 29px;
min-height: 32px;
display: inline-flex;
@@ -1303,23 +1299,23 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
padding: 0px 10px;
border: 1px solid rgb(185, 205, 248);
border: 1px solid var(--color-action-line);
border-radius: 7px;
border-radius: 8px;
background: rgb(245, 248, 255);
background: var(--action-soft);
color: var(--review-blue);
font-size: 11.5px;
font-size: var(--font-size-label);
font-weight: 650;
font-weight: var(--font-weight-semibold);
}
#reviewWorkspaceView .review-add-watch:hover {
border-color: rgb(142, 176, 244);
border-color: var(--action);
background: rgb(234, 241, 255);
background: var(--selected);
}
#reviewWorkspaceView .review-add-watch .lucide {
@@ -1374,17 +1370,17 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
#reviewWorkspaceView .review-watchlist-table .stock-cell strong {
font-size: 12.5px;
font-size: var(--font-size-table);
font-weight: 680;
font-weight: var(--font-weight-semibold);
}
#reviewWorkspaceView .review-watchlist-table .stock-cell small {
font-size: 10.5px;
font-size: var(--font-size-aux);
}
#reviewWorkspaceView .watch-attention-score {
color: rgb(63, 75, 94);
color: var(--text-primary);
font-size: 12px;
@@ -1452,11 +1448,11 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
#reviewWorkspaceView .journal-summary-field input:focus {
border-color: var(--review-blue-line);
box-shadow: rgba(37, 99, 235, 0.08) 0px 0px 0px 2px;
box-shadow: 0 0 0 2px var(--focus-ring);
}
#reviewWorkspaceView .journal-summary-field input::placeholder {
color: rgb(166, 173, 183);
color: var(--text-tertiary);
}
#reviewWorkspaceView .journal-form textarea {
@@ -1543,7 +1539,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
.watchlist-editor-form .form-field > label,
.watchlist-editor-form .form-field > span {
color: rgb(55, 65, 81);
color: var(--text-secondary);
font-size: 12px;
@@ -1561,7 +1557,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
padding: 0px 11px;
border: 1px solid rgb(223, 227, 232);
border: 1px solid var(--border-strong);
border-radius: 8px;
@@ -1569,9 +1565,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
.watchlist-search-control:focus-within {
border-color: rgb(184, 202, 244);
border-color: var(--color-action-line);
box-shadow: rgba(37, 99, 235, 0.07) 0px 0px 0px 3px;
box-shadow: 0 0 0 3px var(--focus-ring);
}
.watchlist-search-control .lucide {
@@ -1579,7 +1575,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
height: 15px;
color: rgb(154, 163, 176);
color: var(--text-tertiary);
}
.watchlist-search-control input {
@@ -1591,7 +1587,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
outline: 0px;
color: rgb(31, 41, 55);
color: var(--text-primary);
font-style: inherit;
@@ -1645,13 +1641,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
border-style: none none solid;
border-color: currentcolor currentcolor rgb(238, 240, 243);
border-color: currentcolor currentcolor var(--border);
border-image: none;
background: var(--surface);
color: rgb(31, 41, 55);
color: var(--text-primary);
text-align: left;
}
@@ -1673,7 +1669,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
.watchlist-search-results button small {
color: rgb(139, 148, 161);
color: var(--text-tertiary);
font-size: 10.5px;
}
@@ -1681,7 +1677,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
.watchlist-search-results button > b {
margin-left: auto;
color: rgb(105, 115, 134);
color: var(--text-secondary);
font-size: 11px;
@@ -1691,7 +1687,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
.watchlist-search-status {
padding: 14px 10px;
color: rgb(139, 148, 161);
color: var(--text-tertiary);
font-size: 11.5px;
@@ -1709,7 +1705,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
padding: 10px 12px;
border: 1px solid rgb(220, 229, 247);
border: 1px solid var(--action-soft);
border-radius: 8px;
@@ -1733,9 +1729,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
border-radius: 7px;
background: rgb(232, 239, 255);
background: var(--action-soft);
color: rgb(37, 99, 235);
color: var(--action);
}
.watchlist-selection-icon .lucide {
@@ -1753,7 +1749,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
.watchlist-selection strong {
color: rgb(31, 41, 55);
color: var(--text-primary);
font-size: 13.5px;
}
@@ -1763,13 +1759,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
gap: 8px;
color: rgb(123, 132, 145);
color: var(--text-tertiary);
font-size: 10.5px;
}
.watchlist-selection span b {
color: rgb(82, 96, 113);
color: var(--text-secondary);
font-weight: 600;
}
@@ -1791,7 +1787,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
resize: vertical;
border: 1px solid rgb(223, 227, 232);
border: 1px solid var(--border-strong);
border-radius: 8px;
@@ -1825,9 +1821,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
}
.watchlist-editor-form textarea:focus {
border-color: rgb(184, 202, 244);
border-color: var(--color-action-line);
box-shadow: rgba(37, 99, 235, 0.07) 0px 0px 0px 3px;
box-shadow: 0 0 0 3px var(--focus-ring);
}
#reviewWorkspaceView .trade-log-table th:nth-child(1) {
@@ -1873,7 +1869,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
font-size: 12px;
font-weight: 650;
font-weight: var(--font-weight-semibold);
min-height: 0px;
@@ -2201,3 +2197,8 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
height: auto;
}
}
#reviewWorkspaceView .data-table thead th:first-child,
#reviewWorkspaceView .data-table tbody td:first-child {
text-align: left;
}
+60 -49
View File
@@ -25,7 +25,7 @@
}
.rotation-day.selected-day {
background: rgb(240, 246, 253);
background: var(--selected);
}
.rotation-sector-chip.selected {
@@ -81,11 +81,11 @@
}
.rotation-day:nth-child(2n+1) {
background: rgb(246, 248, 250);
background: var(--surface-subtle);
}
.rotation-day-sector {
border-color: rgb(231, 235, 239);
border-color: var(--border);
border-radius: 4px;
@@ -107,7 +107,7 @@
}
:where(#rotationView) .rotation-swatch {
background: rgba(53, 106, 230, 0.12);
background: var(--accent-soft);
}
:where(#rotationView) .rotation-tracker-copy {
@@ -157,9 +157,9 @@
cursor: pointer;
border-bottom: 1px dashed rgba(135, 149, 173, 0.22);
border-bottom: 1px dashed var(--border);
background: rgba(53, 106, 230, calc(.04 + var(--rotation-heat) * .46));
background: color-mix(in srgb, var(--accent) calc(4% + var(--rotation-heat) * 46%), transparent);
}
.redesigned-rotation-view {
@@ -185,9 +185,9 @@
color: var(--r2-ink);
font-size: 17px;
font-size: var(--font-size-page-title);
font-weight: 800;
font-weight: var(--font-weight-semibold);
letter-spacing: 0px;
}
@@ -241,7 +241,7 @@
color: var(--r2-sub);
font-size: 12px;
font-size: var(--font-size-label);
line-height: 1;
}
@@ -260,13 +260,15 @@
.rotation-order-control button:focus-visible,
.rotation-sector-chip:focus-visible,
.rotation-track-cancel:focus-visible {
outline: rgba(37, 99, 235, 0.28) solid 2px;
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.rotation-export-button {
min-height: 30px;
min-height: 32px;
height: 32px;
display: inline-flex;
@@ -274,17 +276,17 @@
justify-content: center;
padding: 5px 12px;
padding: 0 12px;
border: 1px solid var(--r2-line);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
background: var(--surface);
color: rgb(55, 65, 81);
color: var(--text-primary);
font-size: 12px;
font-size: var(--font-size-label);
font-weight: 500;
}
@@ -433,17 +435,17 @@
}
#rotationView .rotation-swatch.strong {
background: rgb(112, 155, 245);
background: var(--accent);
}
#rotationView .rotation-swatch.warm {
background: rgb(203, 220, 255);
background: var(--accent-soft);
}
#rotationView .rotation-swatch.mild {
background: rgb(240, 244, 250);
background: var(--surface-muted);
border: 1px solid rgb(223, 230, 240);
border: 1px solid var(--border);
}
#rotationView .rotation-tracker {
@@ -487,7 +489,7 @@
}
#rotationView .rotation-tracker-copy span {
color: rgb(59, 98, 196);
color: var(--accent-hover);
font-size: 12px;
@@ -535,7 +537,7 @@
border-radius: 2px 2px 0px 0px;
background: rgb(147, 180, 245);
background: var(--accent);
}
#rotationView .rotation-tracker-spark small {
@@ -543,7 +545,7 @@
top: -1px;
color: rgb(59, 98, 196);
color: var(--accent-hover);
font-size: 9px;
}
@@ -555,7 +557,7 @@
border-style: dashed dashed none;
border-color: rgb(185, 200, 232) rgb(185, 200, 232) currentcolor;
border-color: var(--action-line) var(--action-line) currentcolor;
border-image: none;
@@ -567,7 +569,9 @@
}
.rotation-track-cancel {
min-height: 30px;
min-height: 32px;
height: 32px;
display: inline-flex;
@@ -575,21 +579,21 @@
justify-content: center;
border: 1px solid var(--r2-line);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
background: var(--surface);
color: rgb(55, 65, 81);
color: var(--text-primary);
font-size: 12px;
font-size: var(--font-size-label);
font-weight: 500;
margin-left: auto;
padding: 4px 9px;
padding: 0 12px;
}
#rotationView .rotation-history {
@@ -697,7 +701,7 @@
padding: 5px;
background: rgb(251, 252, 254);
background: var(--surface-subtle);
}
#rotationView .rotation-sector-chip {
@@ -817,7 +821,7 @@
border-color: var(--r2-blue);
box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px rgba(37, 99, 235, .12);
box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px var(--accent-soft);
}
.rotation-cell-tooltip {
@@ -864,9 +868,9 @@
}
#rotationView .rotation-table thead th {
height: 37px;
height: 36px;
padding: 8px 12px;
padding: 0 12px;
border-bottom: 1px solid var(--r2-line);
@@ -874,7 +878,7 @@
color: var(--r2-sub);
font-size: 12px;
font-size: var(--font-size-caption);
font-weight: 600;
@@ -887,8 +891,13 @@
text-align: right;
}
#rotationView .rotation-table thead th:first-child,
#rotationView .rotation-table tbody td:first-child {
text-align: left;
}
#rotationView .rotation-table thead th[data-auto-sort] {
color: rgb(75, 85, 99);
color: var(--text-2);
cursor: pointer;
@@ -918,14 +927,16 @@
}
#rotationView .rotation-table tbody td {
height: 42px;
height: 40px;
padding: 8px 12px;
padding: 0 12px;
border-bottom: 1px solid var(--r2-line-soft);
color: var(--r2-ink);
font-size: var(--font-size-table);
white-space: nowrap;
}
@@ -934,7 +945,7 @@
}
#rotationView .rotation-table tbody tr:hover td {
background: rgb(248, 250, 255);
background: var(--table-hover);
}
#rotationView .rotation-table .stock-name {
@@ -954,9 +965,9 @@
}
#rotationView .trend-cool {
background: rgb(232, 244, 253);
background: var(--accent-soft);
color: rgb(37, 99, 235);
color: var(--accent);
}
#rotationView .trend-new {
@@ -1040,25 +1051,25 @@
}
#rotationView .rotation-sector-chip.heat-mild {
border-color: rgb(229, 234, 241);
border-color: var(--border);
background: rgb(246, 248, 251);
background: var(--surface-muted);
box-shadow: none;
}
#rotationView .rotation-sector-chip.heat-strong {
border-color: rgba(37, 99, 235, 0.24);
border-color: var(--action-line);
background: rgb(197, 215, 251);
background: var(--accent-soft);
box-shadow: none;
}
#rotationView .rotation-sector-chip.heat-warm {
border-color: rgba(37, 99, 235, 0.12);
border-color: var(--action-line);
background: rgb(231, 239, 255);
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
box-shadow: none;
}
@@ -1066,7 +1077,7 @@
#rotationView .rotation-sector-chip:hover {
z-index: 6;
border-color: rgba(37, 99, 235, 0.34);
border-color: var(--accent);
filter: saturate(1.06);
@@ -1080,7 +1091,7 @@
border-collapse: collapse;
font-size: 12.5px;
font-size: var(--font-size-table);
table-layout: auto;
+1 -1
View File
@@ -41,7 +41,7 @@
<div class="rotation-table-frame tbl-wrap">
<table id="rotationTable" class="data-table tbl rotation-table">
<thead><tr>
<th class="number num">序号</th><th>代码</th><th>股票</th>
<th class="row-number">序号</th><th>代码</th><th>股票</th>
<th class="number num" data-auto-sort="true" title="涨跌幅:点击排序">涨跌幅(%</th>
<th class="number num">开盘价(元)</th><th class="number num">收盘价(元)</th>
<th class="number num" data-auto-sort="true" title="成交额:点击排序">成交额(亿)</th><th>行情状态</th>
+1 -1
View File
@@ -161,7 +161,7 @@ function renderRotationMembers() {
setText("rotationDetailTitle", `${payload.meta?.sector_name || state.rotationSelectedSector}成分股`);
setText("rotationDetailMeta", `${displayCompactDate(payload.meta?.trade_date)} · ${number(payload.meta?.quoted_count)} / ${number(payload.meta?.member_count)}`);
body.innerHTML = rows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"><td class="number num muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
<tr data-code="${escapeHtml(row.code)}"><td class="row-number muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
<td class="number num ${row.quoted ? changeClass(row.change) : "muted"}" data-sort-value="${row.quoted ? number(row.change) : -999}">${row.quoted ? signed(row.change) : ""}</td>
<td class="number num">${row.quoted ? formatNumber(row.open, 2) : ""}</td><td class="number num">${row.quoted ? formatNumber(row.close, 2) : ""}</td>
<td class="number num" data-sort-value="${number(row.amount_billion)}">${row.quoted ? formatNumber(row.amount_billion, 2) : ""}</td><td>${row.quoted ? "正常交易" : "当日无行情"}</td></tr>
+116 -105
View File
@@ -1,8 +1,8 @@
/* Canonical CSS owner: screener. Historical layers consolidated 2026-08-02. */
#screenerView .section-toolbar h2 {
font-size: 17px;
font-size: var(--font-size-page-title);
font-weight: 800;
font-weight: var(--font-weight-semibold);
}
@media (max-width: 767px) {
@@ -25,7 +25,7 @@
.curated-detail-header > div > span,
.quant-panel-heading span {
color: rgb(100, 116, 139);
color: var(--text-secondary);
font-size: 12px;
@@ -35,7 +35,7 @@
:where(#screenerView) .curated-detail-header h3 {
margin: 3px 0px 0px;
color: rgb(17, 24, 39);
color: var(--text-primary);
letter-spacing: 0px;
}
@@ -45,7 +45,7 @@
border-radius: 5px;
background: rgb(232, 238, 252);
background: var(--action-soft);
}
:where(#screenerView) .curated-search .lucide {
@@ -55,19 +55,19 @@
:where(#screenerView) .curated-search input {
background: transparent;
color: rgb(17, 24, 39);
color: var(--text-primary);
}
.curated-strategy-card.active .curated-strategy-rank {
background: rgb(220, 232, 255);
background: var(--action-soft);
color: rgb(29, 78, 216);
color: var(--action);
}
:where(#screenerView) .curated-detail-header p {
margin: 9px 0px 0px;
color: rgb(100, 116, 139);
color: var(--text-secondary);
}
.curated-strategy-badges {
@@ -87,9 +87,9 @@
border-radius: 5px;
background: rgb(241, 245, 249);
background: var(--hover);
color: rgb(71, 85, 105);
color: var(--text-secondary);
font-weight: 650;
@@ -97,9 +97,9 @@
}
.curated-strategy-badges span:first-child {
background: rgb(234, 241, 255);
background: var(--action-soft);
color: rgb(29, 78, 216);
color: var(--action);
}
:where(#screenerView) .mini-section-heading {
@@ -111,11 +111,11 @@
:where(#screenerView) .mini-section-heading h4 {
font-weight: 740;
color: rgb(31, 41, 55);
color: var(--text-primary);
}
.mini-section-heading > span {
color: rgb(100, 116, 139);
color: var(--text-secondary);
font-size: 12px;
}
@@ -141,15 +141,15 @@
gap: 12px;
border-bottom: 1px solid rgb(238, 240, 243);
border-bottom: 1px solid var(--border);
}
:where(#screenerView) .curated-rule-row span {
color: rgb(71, 85, 105);
color: var(--text-secondary);
}
:where(#screenerView) .curated-rule-row strong {
color: rgb(17, 24, 39);
color: var(--text-primary);
font-variant-numeric: tabular-nums;
}
@@ -163,7 +163,7 @@
.curated-score-row > span:first-child {
overflow: hidden;
color: rgb(71, 85, 105);
color: var(--text-secondary);
text-overflow: ellipsis;
@@ -177,7 +177,7 @@
border-radius: 4px;
background: rgb(237, 240, 244);
background: var(--surface-muted);
}
.curated-score-track i {
@@ -187,11 +187,11 @@
border-radius: inherit;
background: rgb(79, 118, 199);
background: var(--action);
}
.curated-score-row strong {
color: rgb(51, 65, 85);
color: var(--text-primary);
text-align: right;
@@ -211,11 +211,11 @@
}
:where(#screenerView) .curated-data-status > .lucide {
color: rgb(22, 163, 74);
color: var(--market-down);
}
.curated-data-status.missing > .lucide {
color: rgb(217, 119, 6);
color: var(--warning-color);
}
.curated-data-status span {
@@ -225,13 +225,13 @@
:where(#screenerView) .curated-data-status strong {
display: block;
color: rgb(51, 65, 85);
color: var(--text-primary);
}
:where(#screenerView) .curated-data-status small {
display: block;
color: rgb(124, 135, 152);
color: var(--text-tertiary);
}
.quant-universe-grid .form-field {
@@ -239,16 +239,16 @@
}
.quant-universe-grid input[type="number"] {
height: 40px;
height: 32px;
}
.quant-rule-row input:focus,
.quant-rule-row select:focus {
border-color: rgb(37, 99, 235);
border-color: var(--action);
outline: 0px;
box-shadow: rgba(37, 99, 235, 0.1) 0px 0px 0px 3px;
box-shadow: 0 0 0 3px var(--focus-ring);
}
:where(#screenerView) .quant-remove-button {
@@ -268,7 +268,7 @@
float: right;
color: rgb(29, 78, 216);
color: var(--action);
}
:where(#screenerView) .quant-weight-status > div {
@@ -278,7 +278,7 @@
:where(#screenerView) .quant-weight-status i {
width: 100%;
background: rgb(37, 99, 235);
background: var(--action);
transition: width 180ms, background 180ms;
}
@@ -305,7 +305,7 @@
}
.quant-summary-pane {
border-top: 1px solid rgb(229, 231, 235);
border-top: 1px solid var(--border);
border-left: 0px;
}
@@ -549,7 +549,7 @@
padding: 12px;
background: rgb(248, 249, 251);
background: var(--surface-subtle);
}
#screenerView .strategy-list {
@@ -575,7 +575,7 @@
}
#screenerView .strategy-item.active {
border-color: rgb(201, 219, 241);
border-color: var(--color-action-line);
background: var(--action-soft);
}
@@ -724,7 +724,7 @@
#screenerView .backtest-panel {
padding: 10px 14px 12px;
background: rgb(251, 252, 253);
background: var(--surface-subtle);
}
#screenerView .backtest-panel > .workspace-heading {
@@ -1099,7 +1099,7 @@
}
.tracking-status.active {
border-color: rgb(197, 215, 237);
border-color: var(--color-action-line);
background: var(--action-soft);
@@ -1107,7 +1107,7 @@
}
.tracking-status.complete {
border-color: rgb(185, 223, 207);
border-color: var(--market-down);
background: var(--market-down-soft);
@@ -1149,9 +1149,9 @@
border-radius: 50%;
background: rgb(229, 231, 235);
background: var(--border);
color: rgb(156, 163, 175);
color: var(--text-tertiary);
font-size: 11px;
@@ -1189,11 +1189,11 @@
.screener-strategy-title b.neutral {
background: var(--surface-muted);
color: rgb(107, 114, 128);
color: var(--text-secondary);
}
:where(#screenerView) .screener-strategy-summary p {
color: rgb(107, 114, 128);
color: var(--text-secondary);
font-size: 12px;
}
@@ -1225,7 +1225,7 @@
border-right: 0px;
border-bottom-color: rgb(238, 240, 243);
border-bottom-color: var(--border);
}
#screenerView .screener-result-frame th {
@@ -1235,13 +1235,13 @@
border-right: 0px;
border-bottom-color: rgb(238, 240, 243);
border-bottom-color: var(--border);
height: 38px;
background: var(--table-header);
color: rgb(107, 114, 128);
color: var(--text-secondary);
font-size: 12px;
}
@@ -1281,7 +1281,7 @@
background: var(--surface);
box-shadow: rgba(15, 23, 42, 0.16) -12px 0px 32px;
box-shadow: var(--shadow-float);
}
.strategy-drawer[open] {
@@ -1323,7 +1323,7 @@
}
.strategy-drawer-header span {
color: rgb(156, 163, 175);
color: var(--text-tertiary);
font-size: 10.5px;
}
@@ -1365,7 +1365,7 @@
border-bottom: 1px solid var(--border);
background: rgb(248, 249, 251);
background: var(--surface-subtle);
}
#screenerView .strategy-drawer .strategy-list {
@@ -1602,7 +1602,7 @@
padding-top: 5px;
border-top: 1px solid rgb(238, 240, 243);
border-top: 1px solid var(--border);
}
#screenerView .screener-backtest-strip {
@@ -1684,7 +1684,7 @@
background: var(--surface);
color: rgb(31, 41, 55);
color: var(--text-primary);
font-style: inherit;
@@ -1722,7 +1722,7 @@
padding: 0px 9px;
color: rgb(31, 41, 55);
color: var(--text-primary);
font: inherit;
@@ -1749,7 +1749,7 @@
.curated-library-pane {
border-right: 0px;
border-bottom: 1px solid rgb(229, 231, 235);
border-bottom: 1px solid var(--border);
display: block;
@@ -1788,9 +1788,9 @@
}
.curated-search:focus-within {
border-color: rgb(142, 172, 239);
border-color: var(--action);
box-shadow: rgba(53, 106, 230, 0.1) 0px 0px 0px 3px;
box-shadow: 0 0 0 3px var(--focus-ring);
}
:where(#screenerView) .screener-page-heading {
@@ -1824,7 +1824,7 @@
}
:where(#screenerView) .curated-library-heading h3 {
color: rgb(17, 24, 39);
color: var(--text-primary);
letter-spacing: 0px;
}
@@ -1870,7 +1870,7 @@
justify-content: space-between;
border-bottom: 1px solid rgb(232, 235, 239);
border-bottom: 1px solid var(--border);
}
:where(#screenerView) .curated-execution-bar {
@@ -1890,7 +1890,7 @@
}
#screenerView .regime-evidence strong {
color: rgb(146, 88, 6);
color: var(--warning-color);
font-size: 12px;
@@ -1902,7 +1902,7 @@
overflow: visible;
color: rgb(162, 106, 24);
color: var(--warning-color);
font-size: 10.5px;
@@ -2119,10 +2119,6 @@ body[data-active-view="screenerView"] .workspace-view {
}
}
body[data-active-view="screenerView"] .overview-strip {
display: grid;
}
@media (min-width: 901px) {
.screener-page-bar {
min-height: 34px;
@@ -2350,7 +2346,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
:where(#screenerView) .curated-strategy-card:hover {
box-shadow: rgba(37, 99, 235, 0.08) 0px 4px 14px;
box-shadow: var(--shadow-raised);
}
:where(#screenerView) .quant-screener-panel {
@@ -2414,7 +2410,7 @@ body[data-active-view="screenerView"] .overview-strip {
border-radius: 999px;
background: rgb(223, 229, 238);
background: var(--border-strong);
accent-color: var(--primary);
@@ -2532,19 +2528,19 @@ body[data-active-view="screenerView"] .overview-strip {
color: var(--r2-ink);
font-size: 17px;
font-size: var(--font-size-page-title);
font-weight: 800;
font-weight: var(--font-weight-semibold);
}
#screenerView .screener-page-heading .section-subtitle {
color: var(--r2-faint);
font-size: 12px;
font-size: var(--font-size-caption);
}
#screenerView .screener-mode-tabs {
min-height: 34px;
min-height: 32px;
display: inline-flex;
@@ -2662,7 +2658,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerView .step-line.complete {
background: rgb(134, 213, 160);
background: var(--market-down);
}
#screenerView .screener-overview-card {
@@ -2712,7 +2708,7 @@ body[data-active-view="screenerView"] .overview-strip {
border-radius: 5px;
background: rgb(244, 246, 248);
background: var(--surface-muted);
color: var(--r2-sub);
@@ -2766,7 +2762,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerView .regime-option:hover {
border-color: rgb(187, 200, 219);
border-color: var(--border-strong);
color: var(--r2-ink);
}
@@ -2979,7 +2975,7 @@ body[data-active-view="screenerView"] .overview-strip {
#screenerView .result-toolbar .count-badge {
border-radius: 5px;
background: rgb(241, 243, 246);
background: var(--surface-muted);
color: var(--r2-sub);
@@ -3039,25 +3035,29 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerView .data-table {
font-size: 11.5px;
font-size: var(--font-size-table);
}
#screenerView .data-table thead th {
height: 39px;
height: 36px;
padding: 8px 11px;
padding: 0 12px;
background: var(--table-header);
color: var(--r2-sub);
font-size: 10.5px;
font-size: var(--font-size-caption);
font-weight: var(--font-weight-semibold);
}
#screenerView .data-table tbody td {
height: 45px;
height: 40px;
padding: 8px 11px;
padding: 0 12px;
font-size: var(--font-size-table);
}
#screenerView .data-table .stock-name {
@@ -3069,7 +3069,7 @@ body[data-active-view="screenerView"] .overview-strip {
overflow: hidden;
color: rgb(95, 107, 124);
color: var(--text-secondary);
text-overflow: ellipsis;
}
@@ -3091,13 +3091,13 @@ body[data-active-view="screenerView"] .overview-strip {
#screenerView .probability-value small {
margin-top: 3px;
font-weight: 500;
font-weight: var(--font-weight-medium);
display: block;
color: var(--r2-faint);
font-size: 9px;
font-size: var(--font-size-aux);
}
#screenerView .tracking-summary {
@@ -3341,7 +3341,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerView .curated-school-filters button.active {
border-color: rgb(197, 212, 241);
border-color: var(--color-action-line);
background: var(--scr-blue-soft);
@@ -3562,7 +3562,7 @@ body[data-active-view="screenerView"] .overview-strip {
border-radius: 5px;
background: rgb(238, 241, 245);
background: var(--surface-muted);
color: var(--r2-sub);
@@ -3598,7 +3598,7 @@ body[data-active-view="screenerView"] .overview-strip {
border-radius: 4px;
background: rgb(238, 241, 245);
background: var(--surface-muted);
color: var(--r2-faint);
@@ -3953,7 +3953,7 @@ body[data-active-view="screenerView"] .overview-strip {
border-radius: 6px;
background: rgb(247, 248, 250);
background: var(--surface-subtle);
}
#screenerView .quant-rule-rows {
@@ -3989,7 +3989,7 @@ body[data-active-view="screenerView"] .overview-strip {
min-height: 30px;
border: 1px solid rgb(220, 225, 232);
border: 1px solid var(--border-strong);
border-radius: 6px;
@@ -4003,7 +4003,7 @@ body[data-active-view="screenerView"] .overview-strip {
min-height: 30px;
border: 1px solid rgb(220, 225, 232);
border: 1px solid var(--border-strong);
border-radius: 6px;
@@ -4075,7 +4075,7 @@ body[data-active-view="screenerView"] .overview-strip {
border-radius: 3px;
background: rgb(232, 235, 240);
background: var(--surface-muted);
}
#screenerView .quant-weight-status i {
@@ -4109,7 +4109,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerView .strategy-drawer::backdrop {
background: rgba(20, 29, 44, 0.38);
background: var(--backdrop);
backdrop-filter: blur(3px);
}
@@ -4587,7 +4587,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerTrackingView .tracking-back-button:hover {
border-color: rgb(185, 200, 228);
border-color: var(--color-action-line);
color: var(--scr-blue);
}
@@ -4743,23 +4743,27 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerTrackingView .data-table {
font-size: 11.5px;
font-size: var(--font-size-table);
}
#screenerTrackingView .data-table thead th {
height: 39px;
height: 36px;
padding: 8px 11px;
padding: 0 12px;
background: var(--table-header);
font-size: 10.5px;
font-size: var(--font-size-caption);
font-weight: var(--font-weight-semibold);
}
#screenerTrackingView .data-table tbody td {
height: 46px;
height: 40px;
padding: 8px 11px;
padding: 0 12px;
font-size: var(--font-size-table);
}
#screenerTrackingView .stock-cell {
@@ -4775,7 +4779,7 @@ body[data-active-view="screenerView"] .overview-strip {
#screenerTrackingView .stock-cell small {
color: var(--r2-faint);
font-size: 9px;
font-size: var(--font-size-aux);
}
#screenerTrackingView .table-action.danger {
@@ -5495,7 +5499,7 @@ body[data-active-view="screenerView"] .overview-strip {
flex-direction: column;
border: 1px solid rgb(243, 201, 195);
border: 1px solid var(--market-up);
border-radius: 9px;
@@ -5677,7 +5681,7 @@ body[data-active-view="screenerView"] .overview-strip {
}
#screenerView .screener-tracking-entry:focus-visible {
outline: rgba(37, 99, 235, 0.45) solid 2px;
outline: 2px solid var(--action);
outline-offset: 2px;
}
@@ -5729,11 +5733,11 @@ body[data-active-view="screenerView"] .overview-strip {
padding: 3px 8px;
border: 1px solid rgb(217, 226, 239);
border: 1px solid var(--border-strong);
border-radius: 5px;
background: rgb(247, 249, 252);
background: var(--surface-subtle);
color: var(--r2-sub);
@@ -5866,7 +5870,7 @@ body[data-active-view="screenerView"] .overview-strip {
width: 100%;
min-width: 1180px;
min-width: 1376px;
table-layout: fixed;
}
@@ -5887,12 +5891,12 @@ body[data-active-view="screenerView"] .overview-strip {
width: 78px;
}
#screenerView .screener-result-columns col:nth-child(5) {
width: 104px;
#screenerView .screener-result-columns col:nth-child(5),
#screenerView .screener-result-columns col:nth-child(6),
#screenerView .screener-result-columns col:nth-child(7) {
width: 120px;
}
#screenerView .screener-result-columns col:nth-child(6),
#screenerView .screener-result-columns col:nth-child(7),
#screenerView .screener-result-columns col:nth-child(8),
#screenerView .screener-result-columns col:nth-child(9) {
width: 82px;
@@ -6563,3 +6567,10 @@ body[data-active-view="screenerView"] .overview-strip {
gap: 3px;
}
}
#screenerView .data-table thead th:first-child,
#screenerView .data-table tbody td:first-child,
#screenerTrackingView .data-table thead th:first-child,
#screenerTrackingView .data-table tbody td:first-child {
text-align: left;
}
+85 -81
View File
@@ -218,7 +218,11 @@
.sentiment-history-table thead th {
border-bottom-color: rgb(216, 224, 230);
font-size: 11.5px;
height: 36px;
font-size: var(--fs-caption);
font-weight: 600;
}
.sentiment-history-table .sentiment-history-groups th {
@@ -313,24 +317,10 @@
background: var(--table-header);
}
.sentiment-history-table tbody tr:nth-child(2n) td {
background: var(--surface-subtle);
}
.sentiment-history-table tbody tr:hover td {
background: var(--table-hover);
}
.sentiment-history-table tbody tr.latest-row td {
background: var(--table-selected);
font-weight: 650;
}
.sentiment-history-table tbody tr.latest-row td:first-child {
box-shadow: inset 3px 0 0 var(--action);
}
.sentiment-score-cell {
font-weight: 750;
}
@@ -377,15 +367,15 @@
}
.sentiment-phase-badge.phase-repair {
background: rgb(231, 246, 246);
background: var(--accent-soft);
color: rgb(11, 109, 116);
color: var(--accent);
}
.sentiment-phase-badge.phase-fermentation {
background: rgb(237, 246, 234);
background: var(--up-soft);
color: rgb(61, 113, 51);
color: var(--up);
}
.sentiment-phase-badge.phase-climax {
@@ -884,19 +874,21 @@
min-width: 0px;
min-height: 33px;
min-height: 0px;
display: flex;
flex: 0 0 auto;
flex-direction: row;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
align-items: flex-start;
padding: 0px 10px 0px 0px;
gap: 2px;
padding: 0 16px;
border: 0px;
}
@@ -924,11 +916,11 @@
}
.overview-strip .sentiment-text {
color: var(--r2-ink);
color: var(--accent);
font-size: 12px;
font-size: var(--fs-caption);
font-weight: 600;
font-weight: 500;
white-space: nowrap;
}
@@ -1037,25 +1029,31 @@
align-items: flex-start;
gap: 16px;
gap: 14px;
padding: 14px 16px;
padding: 14px 16px 10px;
}
.sentiment-current-phase-badge {
min-width: 94px;
min-width: 92px;
flex: 0 0 auto;
display: block;
display: flex;
padding: 10px 18px;
flex-direction: column;
border: 1px solid rgb(245, 207, 201);
align-items: center;
border-radius: 10px;
gap: 2px;
background: var(--r2-up-soft);
padding: 10px 16px;
border: 1px solid var(--accent);
border-radius: var(--card-radius);
background: var(--accent-soft);
text-align: center;
}
@@ -1063,11 +1061,11 @@
.sentiment-current-phase-badge strong {
display: block;
color: var(--r2-up);
color: var(--accent);
font-size: 19px;
font-weight: 800;
font-weight: 700;
line-height: 1.35;
}
@@ -1075,11 +1073,11 @@
.sentiment-current-phase-badge span {
display: block;
margin-top: 2px;
margin-top: 0;
color: var(--r2-sub);
color: var(--text-3);
font-size: 11px;
font-size: var(--fs-aux);
white-space: nowrap;
}
@@ -1091,15 +1089,17 @@
}
.sentiment-phase-info p {
color: var(--r2-sub);
color: var(--text-2);
font-size: 12.5px;
font-size: var(--fs-label);
line-height: 1.7;
line-height: 1.6;
}
.sentiment-phase-info p b {
font-weight: 700;
color: var(--text-1);
font-weight: 600;
}
.sentiment-phase-info p .down {
@@ -1113,17 +1113,17 @@
.sentiment-phase-advice {
margin-top: 8px;
padding: 5px 9px;
padding: 8px 10px;
border-radius: 6px;
border-radius: var(--radius-md);
background: var(--r2-amber-soft);
background: var(--surface-muted);
color: var(--r2-amber);
color: var(--text-2);
font-size: 12px;
font-size: var(--fs-caption);
line-height: 1.6;
line-height: 1.55;
}
.sentiment-feedback-strip {
@@ -1131,45 +1131,47 @@
grid-template-columns: 1fr 1fr;
border-top: 1px solid var(--r2-line-soft);
border-top: 1px solid var(--border);
}
.sentiment-feedback-strip > span {
min-width: 0px;
display: grid;
display: flex;
grid-template-columns: auto 1fr;
flex-wrap: wrap;
gap: 2px 8px;
align-items: baseline;
padding: 8px 12px;
gap: 2px 6px;
color: var(--r2-faint);
padding: 9px 14px;
font-size: 10.5px;
color: var(--text-3);
font-size: var(--fs-caption);
}
.sentiment-feedback-strip > span + span {
border-left: 1px solid var(--r2-line-soft);
border-left: 1px solid var(--border);
}
.sentiment-feedback-strip strong {
color: var(--r2-ink);
color: var(--text-1);
font-size: 11.5px;
font-size: inherit;
text-align: right;
font-weight: 600;
text-align: left;
}
.sentiment-feedback-strip small {
grid-column: 1 / -1;
overflow: hidden;
color: var(--r2-sub);
color: var(--text-3);
font-size: 10px;
font-size: var(--fs-aux);
text-overflow: ellipsis;
@@ -1309,19 +1311,21 @@
.redesigned-sentiment-view .sentiment-history-table td,
.redesigned-sentiment-view .sentiment-history-table th {
height: auto;
height: 40px;
padding: 8px 10px;
padding: 0 10px;
border-bottom: 1px solid var(--r2-line-soft);
}
.redesigned-sentiment-view .sentiment-history-table thead th {
height: 36px;
background: var(--table-header);
color: var(--r2-sub);
color: var(--text-2);
font-size: 12px;
font-size: var(--fs-caption);
font-weight: 600;
}
@@ -1399,17 +1403,17 @@
}
.overview-strip .sentiment-block .sentiment-text {
display: block;
display: inline-flex;
margin: 0px;
padding: 0px;
padding: 0 6px;
font-size: 11px;
font-size: var(--fs-caption);
font-weight: 500;
line-height: 1;
line-height: 18px;
letter-spacing: 0px;
@@ -1478,19 +1482,19 @@
}
.overview-strip[data-overview-expanded="true"] .sentiment-block {
min-height: 76px;
min-height: 0;
justify-content: center;
gap: 4px;
gap: 2px;
padding: 10px 16px;
padding: 0 16px;
flex-direction: row;
flex-direction: column;
align-items: center;
align-items: flex-start;
border-right: 1px solid var(--line-soft);
border-right: 0;
background: transparent;
}
@@ -1628,9 +1632,9 @@
}
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-fermentation {
background: var(--market-down-soft);
background: var(--market-up-soft);
color: var(--market-down);
color: var(--market-up);
}
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-climax {
+1 -1
View File
@@ -42,7 +42,7 @@ function renderSentimentHistory() {
empty.hidden = rows.length > 0;
body.innerHTML = [...rows].reverse().map((row) => {
return `
<tr class="${row.trade_date === latest?.trade_date ? "latest-row" : ""}">
<tr>
<td class="sentiment-date-cell">${escapeHtml(displayCompactDate(row.trade_date))}</td>
<td class="number sentiment-score-cell ${sentimentScoreClass(row.score)}">${number(row.score)}</td>
<td><span class="sentiment-phase-badge ${sentimentPhaseClass(row.phase)}">${escapeHtml(row.phase)}</span></td>
+37 -30
View File
@@ -184,15 +184,15 @@
padding: 9px 13px;
border-bottom-color: rgb(231, 235, 239);
border-bottom-color: var(--border);
}
.theme-directory-item:hover {
background: rgb(241, 246, 251);
background: var(--hover);
}
.theme-directory-item.active {
background: rgb(234, 242, 251);
background: var(--selected);
box-shadow: inset 3px 0 var(--action);
}
@@ -294,6 +294,10 @@
margin: 0px;
color: var(--r2-ink);
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
}
.theme-title-v2 > div > span {
@@ -331,7 +335,7 @@
.theme-search-v2 {
width: 250px;
height: 34px;
height: 32px;
display: flex;
@@ -341,9 +345,9 @@
padding: 0px 10px;
border: 1px solid var(--control-border);
border: 1px solid var(--border-strong);
border-radius: 7px;
border-radius: 8px;
background: var(--surface);
@@ -389,11 +393,15 @@
}
.theme-refresh-v2 {
min-height: 34px;
min-height: 32px;
height: 32px;
padding: 0px 12px;
border-radius: 7px;
border-radius: 8px;
font-size: var(--font-size-label);
}
.theme-refresh-v2 .lucide {
@@ -441,17 +449,17 @@
}
.theme-summary-v2 span {
color: var(--r2-sub);
color: var(--text-3);
font-size: 11px;
font-size: var(--font-size-caption);
}
.theme-summary-v2 strong {
color: var(--r2-ink);
font-size: 19px;
font-size: var(--font-size-metric);
font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-semibold);
font-variant-numeric: tabular-nums;
}
@@ -527,9 +535,9 @@
color: var(--r2-ink);
font-size: 14px;
font-size: var(--font-size-card-title);
font-weight: 700;
font-weight: var(--font-weight-semibold);
}
.theme-card-head-v2 span {
@@ -675,11 +683,11 @@
.theme-rank-v2 {
color: var(--r2-faint);
font-size: 10.5px;
font-size: var(--font-size-aux);
font-variant-numeric: tabular-nums;
text-align: center;
text-align: left;
}
.theme-directory-item-v2:nth-child(-n+3) .theme-rank-v2 {
@@ -1028,9 +1036,9 @@
z-index: 2;
height: 34px;
height: 36px;
padding: 6px 11px;
padding: 0 12px;
border-bottom-color: var(--r2-line);
@@ -1038,15 +1046,20 @@
color: var(--r2-sub);
font-size: 10.5px;
font-size: var(--font-size-caption);
}
.theme-members-table-v2 tbody td {
height: 39px;
height: 40px;
padding: 7px 11px;
padding: 0 12px;
font-size: 11.5px;
font-size: var(--font-size-table);
}
.theme-members-table-v2 thead th:first-child,
.theme-members-table-v2 tbody td:first-child {
text-align: left;
}
.theme-members-table-v2 tbody tr {
@@ -1076,10 +1089,6 @@
overflow: hidden;
}
body[data-active-view="themeLibraryView"] .overview-strip {
flex: 0 0 auto;
}
body[data-active-view="themeLibraryView"] #themeLibraryView.active-view {
min-height: 0px;
@@ -1184,11 +1193,9 @@
}
.theme-members-table-v2 tbody td {
height: 35px;
height: 40px;
padding-top: 5px;
padding-bottom: 5px;
padding: 0 12px;
}
}
+1 -1
View File
@@ -46,7 +46,7 @@
<strong id="themeMemberCount">0 只</strong>
</header>
<div class="theme-members-frame-v2 tbl-wrap">
<table class="data-table tbl theme-members-table-v2"><thead><tr><th class="num">序号</th><th>股票</th><th class="number num sortable">涨跌幅(%<span class="arr"></span></th><th class="number num sortable">收盘价(元)<span class="arr"></span></th><th class="number num sortable">成交额(亿)<span class="arr"></span></th></tr></thead><tbody id="themeMemberTableBody"></tbody></table>
<table class="data-table tbl theme-members-table-v2"><thead><tr><th class="row-number">序号</th><th>股票</th><th class="number num sortable">涨跌幅(%<span class="arr"></span></th><th class="number num sortable">收盘价(元)<span class="arr"></span></th><th class="number num sortable">成交额(亿)<span class="arr"></span></th></tr></thead><tbody id="themeMemberTableBody"></tbody></table>
</div>
</section>
</div>
+1 -1
View File
@@ -97,7 +97,7 @@ function renderThemeDetail() {
setText("themeMemberCount", `有行情 ${number(summary.quoted_count)} / ${number(summary.member_count)}`);
const body = document.querySelector("#themeMemberTableBody");
body.innerHTML = (payload.members || []).map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"><td class="row-number num muted">${index + 1}</td>
<tr data-code="${escapeHtml(row.code)}"><td class="row-number muted">${index + 1}</td>
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
<td class="number num ${changeClass(row.change)}">${row.has_quote ? signed(row.change) : ""}</td>
<td class="number num">${row.has_quote ? formatNumber(row.price, 2) : ""}</td><td class="number num">${row.has_quote ? formatNumber(row.amount_billion, 2) : ""}</td></tr>`).join("");
+5
View File
@@ -8,6 +8,11 @@ async function backfillData() {
end_date: document.querySelector("#backfillEnd").value,
});
showToast(`历史回补完成,共处理 ${payload.results.length} 个工作日`);
state.sentimentHistory = null;
state.sentimentHistoryKey = "";
if (state.activeView === "sentimentCycleView") {
await loadSentimentHistory(true);
}
await openAdminSettings(true);
} catch (error) {
showToast(error.message);
+103 -89
View File
@@ -12,7 +12,7 @@
padding: 20px;
background: rgb(237, 241, 244);
background: var(--canvas);
}
.auth-gate[hidden] {
@@ -26,13 +26,13 @@
padding: 26px;
border: 1px solid var(--line-strong);
border: 1px solid var(--border-strong);
border-radius: 6px;
border-radius: 12px;
background: var(--surface);
box-shadow: var(--shadow);
box-shadow: var(--shadow-raised);
}
.auth-brand {
@@ -46,7 +46,9 @@
.auth-brand h1 {
margin: 0px;
font-size: 22px;
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
}
.auth-brand span {
@@ -54,9 +56,9 @@
margin-top: 5px;
color: var(--text-muted);
color: var(--text-3);
font-size: 12px;
font-size: var(--font-size-caption);
}
.auth-tabs {
@@ -88,11 +90,11 @@
}
.auth-tab.active {
border-bottom-color: var(--coral);
border-bottom-color: var(--action);
color: var(--text);
color: var(--text-1);
font-weight: 750;
font-weight: var(--font-weight-semibold);
}
.auth-form {
@@ -106,15 +108,15 @@
.auth-form .button {
width: 100%;
min-height: 40px;
min-height: 32px;
}
.auth-error {
margin: 0px;
color: rgb(185, 54, 39);
color: var(--market-up);
font-size: 12px;
font-size: var(--font-size-caption);
line-height: 1.5;
}
@@ -146,23 +148,23 @@
.admin-section-picker select {
width: 100%;
min-height: 40px;
min-height: 32px;
padding: 0px 11px;
border: 1px solid var(--line-strong);
border: 1px solid var(--border-strong);
border-radius: 5px;
border-radius: 8px;
background: var(--surface);
color: var(--text-primary);
font-size: 13px;
font-size: var(--font-size-label);
}
.admin-section-picker select:focus-visible {
outline: 2px solid var(--blue);
outline: 2px solid var(--action);
outline-offset: 2px;
}
@@ -307,13 +309,13 @@
min-width: 0px;
height: 34px;
height: 32px;
padding: 0px 8px;
border: 1px solid var(--line-strong);
border: 1px solid var(--border-strong);
border-radius: 4px;
border-radius: 8px;
background: var(--surface);
}
@@ -386,11 +388,11 @@
}
.connection-status.connected {
border-color: rgb(167, 222, 201);
border-color: var(--market-down);
background: var(--green-soft);
color: rgb(8, 106, 75);
color: var(--market-down);
}
.settings-dialog form {
@@ -470,11 +472,11 @@
}
.account-button > span {
min-width: 0px;
flex: 0 0 auto;
overflow: hidden;
min-width: max-content;
text-overflow: ellipsis;
overflow: visible;
white-space: nowrap;
}
@@ -594,7 +596,7 @@
.account-dropdown button {
width: 100%;
min-height: 40px;
min-height: 36px;
display: grid;
@@ -636,7 +638,7 @@
outline: none;
box-shadow: inset 0 0 0 2px var(--focus-ring, #1268c4);
box-shadow: inset 0 0 0 2px var(--focus-ring);
}
.account-dropdown button svg {
@@ -662,16 +664,16 @@
}
.account-dropdown .account-menu-danger {
color: rgb(181, 58, 66);
color: var(--market-up);
grid-template-columns: 18px minmax(0px, 1fr);
}
.account-dropdown .account-menu-danger:focus-visible,
.account-dropdown .account-menu-danger:hover {
color: rgb(163, 47, 55);
color: var(--market-up);
background: rgb(255, 242, 243);
background: var(--market-up-soft);
}
button.account-role-badge {
@@ -685,11 +687,11 @@ button.account-role-badge {
button.account-role-badge:hover {
filter: brightness(0.98);
box-shadow: rgba(67, 76, 86, 0.14) 0px 3px 10px;
box-shadow: var(--shadow-card);
}
button.account-role-badge:focus-visible {
outline: 2px solid var(--blue);
outline: 2px solid var(--action);
outline-offset: 2px;
}
@@ -701,9 +703,9 @@ button.account-role-badge:focus-visible {
}
.admin-role-badge {
color: rgb(83, 103, 124);
color: var(--text-secondary);
background: rgb(244, 247, 250);
background: var(--surface-muted);
}
.vip-role-badge {
@@ -719,7 +721,7 @@ button.account-role-badge:focus-visible {
.vip-role-badge.is-nonmember {
color: rgb(105, 117, 128);
background: rgb(244, 246, 248);
background: var(--surface-muted);
border-color: rgb(203, 211, 218);
@@ -825,7 +827,7 @@ button.account-role-badge:focus-visible {
}
.member-gate strong {
color: rgb(60, 70, 80);
color: var(--text-primary);
font-size: 14px;
}
@@ -877,7 +879,7 @@ button.account-role-badge:focus-visible {
margin-bottom: 12px;
color: rgb(83, 103, 124);
color: var(--text-secondary);
}
.privacy-note svg {
@@ -889,7 +891,7 @@ button.account-role-badge:focus-visible {
margin-top: 3px;
color: rgb(52, 116, 95);
color: var(--market-down);
}
.membership-status-grid {
@@ -905,17 +907,17 @@ button.account-role-badge:focus-visible {
.membership-status-grid > div {
padding: 12px 13px;
border: 1px solid var(--line, #e2e8ee);
border: 1px solid var(--border);
border-radius: 10px;
background: rgb(251, 252, 253);
background: var(--surface-subtle);
}
.membership-status-grid span {
display: block;
color: rgb(116, 128, 140);
color: var(--text-tertiary);
font-size: 11px;
}
@@ -925,7 +927,7 @@ button.account-role-badge:focus-visible {
margin-top: 5px;
color: rgb(38, 50, 61);
color: var(--text-primary);
font-size: 15px;
@@ -935,7 +937,7 @@ button.account-role-badge:focus-visible {
.membership-comparison {
overflow: hidden;
border: 1px solid var(--line, #e2e8ee);
border: 1px solid var(--border);
border-radius: 10px;
@@ -953,7 +955,7 @@ button.account-role-badge:focus-visible {
padding: 9px 11px;
border-top: 1px solid var(--line, #e2e8ee);
border-top: 1px solid var(--border);
}
.membership-comparison > div:first-child {
@@ -961,21 +963,21 @@ button.account-role-badge:focus-visible {
}
.membership-comparison-head {
color: rgb(105, 118, 131);
color: var(--text-secondary);
background: rgb(247, 249, 251);
background: var(--surface-subtle);
font-weight: 700;
}
.membership-comparison b {
color: rgb(39, 108, 88);
color: var(--market-down);
font-weight: 600;
}
.membership-comparison b.muted {
color: rgb(154, 164, 173);
color: var(--text-tertiary);
}
.membership-comparison b.available {
@@ -993,7 +995,7 @@ button.account-role-badge:focus-visible {
margin-top: 14px;
color: rgb(93, 105, 116);
color: var(--text-secondary);
font-size: 12px;
}
@@ -1026,11 +1028,15 @@ button.account-role-badge:focus-visible {
display: grid;
grid-template-columns: auto minmax(0px, 1fr);
grid-template-columns: minmax(0px, 1fr);
}
.account-menu-shell .account-button {
width: 100%;
min-width: 0px;
max-width: none;
}
.account-dropdown {
@@ -1085,7 +1091,7 @@ button.account-role-badge:focus-visible {
}
.account-settings-dialog {
border-radius: 9px;
border-radius: 12px;
}
.settings-dialog {
@@ -1095,7 +1101,7 @@ button.account-role-badge:focus-visible {
overflow-x: hidden;
border-radius: 9px;
border-radius: 12px;
}
.account-dropdown {
@@ -1115,17 +1121,17 @@ button.account-role-badge:focus-visible {
padding: 7px;
background: rgba(255, 255, 255, 0.98);
background: var(--surface);
transform-origin: right top;
animation: account-menu-in 180ms var(--ease-out) both;
border-color: var(--border);
border: 1px solid var(--border);
border-radius: 7px;
border-radius: 8px;
box-shadow: rgba(22, 34, 46, 0.15) 0px 14px 35px;
box-shadow: var(--shadow-float);
}
@media (max-width: 767px) {
@@ -1177,29 +1183,37 @@ button.account-role-badge:focus-visible {
gap: 3px;
}
@media (min-width: 1280px) and (max-width: 1510px) {
.account-role-badge span {
@media (min-width: 1280px) and (max-width: 1439px) {
.app-header .header-command-group .account-role-badge span {
display: none;
}
.account-role-badge {
min-width: 27px;
.app-header .header-command-group .account-role-badge {
min-width: 28px;
justify-content: center;
padding-inline: 6px;
}
}
@media (min-width: 1380px) and (max-width: 1510px) {
#settingsButton,
@media (min-width: 1280px) {
.header-command-group .account-button {
flex: 0 0 auto;
width: auto;
padding: 0px 9px;
max-width: none;
overflow: visible;
padding: 0px 8px;
}
#settingsButton > span,
.header-command-group .account-button > span {
display: inline;
overflow: visible;
}
}
@@ -1214,7 +1228,7 @@ button.account-role-badge:focus-visible {
border-radius: 7px;
background: rgb(37, 99, 235);
background: var(--action);
color: var(--text-inverse);
@@ -1262,11 +1276,11 @@ button.account-role-badge:focus-visible {
}
.account-button {
overflow: hidden;
flex: 0 0 auto;
text-overflow: ellipsis;
overflow: visible;
max-width: 108px;
max-width: none;
min-height: 28px;
@@ -1308,17 +1322,17 @@ button.account-role-badge:focus-visible {
.settings-dialog:not(.heaven-reading-dialog) .settings-section-heading h3 {
margin: 0px;
color: rgb(39, 52, 67);
color: var(--text-primary);
font-size: 14px;
font-size: var(--font-size-card-title);
font-weight: 720;
font-weight: var(--font-weight-semibold);
}
.settings-dialog:not(.heaven-reading-dialog) .settings-section-heading > span {
color: rgb(124, 135, 149);
color: var(--text-tertiary);
font-size: 10.5px;
font-size: var(--font-size-aux);
}
.settings-dialog:not(.heaven-reading-dialog) .form-field {
@@ -1327,7 +1341,7 @@ button.account-role-badge:focus-visible {
.settings-dialog:not(.heaven-reading-dialog) .form-field > label,
.settings-dialog:not(.heaven-reading-dialog) .form-field > span {
color: rgb(78, 91, 106);
color: var(--text-secondary);
font-size: 11px;
@@ -1335,7 +1349,7 @@ button.account-role-badge:focus-visible {
}
.settings-dialog:not(.heaven-reading-dialog) :is(input, select, textarea) {
border-color: rgb(213, 220, 229);
border-color: var(--border-strong);
border-radius: 7px;
}
@@ -1375,7 +1389,7 @@ button.account-role-badge:focus-visible {
}
.account-settings-dialog .settings-lead {
color: rgb(96, 109, 124);
color: var(--text-secondary);
font-size: 12px;
@@ -1397,7 +1411,7 @@ button.account-role-badge:focus-visible {
padding: 11px 12px;
border-color: rgb(223, 228, 234);
border-color: var(--border-strong);
border-radius: 7px;
@@ -1425,7 +1439,7 @@ button.account-role-badge:focus-visible {
}
.account-settings-dialog .membership-comparison {
border-color: rgb(223, 228, 234);
border-color: var(--border-strong);
border-radius: 7px;
}
@@ -1435,7 +1449,7 @@ button.account-role-badge:focus-visible {
padding: 8px 11px;
border-color: rgb(230, 234, 239);
border-color: var(--border);
}
.account-settings-dialog .membership-comparison-head {
@@ -1449,11 +1463,11 @@ button.account-role-badge:focus-visible {
.account-settings-dialog .privacy-note {
padding: 10px 11px;
border: 1px solid rgb(220, 232, 226);
border: 1px solid var(--market-down);
border-radius: 7px;
background: rgb(245, 250, 247);
background: var(--market-down-soft);
}
.account-settings-dialog .account-birth-form {
@@ -1475,7 +1489,7 @@ button.account-role-badge:focus-visible {
border-radius: 7px;
color: rgb(102, 115, 132);
color: var(--text-secondary);
font-size: 11px;
}
@@ -1489,7 +1503,7 @@ button.account-role-badge:focus-visible {
border-bottom: 1px solid var(--dialog-line);
background: rgb(251, 252, 253);
background: var(--surface-subtle);
}
.admin-dialog .admin-section-picker label {
@@ -1497,13 +1511,13 @@ button.account-role-badge:focus-visible {
}
.admin-dialog .admin-section-picker select {
min-height: 36px;
min-height: 32px;
border-color: rgb(212, 220, 229);
border-color: var(--border-strong);
border-radius: 7px;
border-radius: 8px;
font-size: 12px;
font-size: var(--font-size-label);
}
.admin-dialog .admin-panel {
@@ -1635,7 +1649,7 @@ button.account-role-badge:focus-visible {
}
:root[data-theme="dark"] :is(.loading-overlay, .auth-gate) {
background: color-mix(in srgb, var(--canvas) 92%, transparent);
background: var(--backdrop);
}
:root[data-theme="dark"] :is(.auth-shell) {
+41 -17
View File
@@ -1,11 +1,15 @@
/* Canonical CSS owner: cards. Historical layers consolidated 2026-08-02. */
.stock-name {
font-weight: 700;
font-size: var(--fs-table);
font-weight: 600;
}
.stock-code {
color: var(--text-muted);
color: var(--text-3);
font-size: var(--fs-aux);
font-variant-numeric: tabular-nums;
}
@@ -49,25 +53,37 @@
}
.streak-pill {
background: var(--coral-soft);
color: var(--coral);
font-weight: 700;
display: inline-flex;
align-items: center;
min-height: 24px;
justify-content: center;
padding: 2px 8px;
min-width: 30px;
border-radius: 4px;
height: 20px;
padding: 0 7px;
border-radius: 999px;
background: var(--up-soft);
color: var(--up);
font-size: var(--fs-caption);
font-weight: 600;
white-space: nowrap;
}
.streak-pill.high {
background: var(--up);
color: #fff;
}
.outcome-tag {
display: inline-flex;
@@ -227,7 +243,7 @@
font-size: var(--font-size-card-title);
font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-semibold);
}
.card-h .sub {
@@ -253,21 +269,29 @@
}
.sname {
font-weight: 700;
font-weight: 600;
font-size: 13px;
font-size: var(--fs-table);
}
.scode {
font-size: 11px;
font-size: var(--fs-aux);
color: var(--faint);
color: var(--text-3);
margin-left: 6px;
margin-left: 0;
font-weight: 400;
}
.stock-cell {
display: flex;
flex-direction: column;
line-height: 1.3;
}
.muted {
color: var(--faint);
}
+6 -4
View File
@@ -635,14 +635,16 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
height: 14px;
}
@media (min-width: 1280px) and (max-width: 1510px) {
.header-command-group .command-button {
width: 31px;
@media (min-width: 1280px) {
.header-command-group .command-button:not(.account-button) {
width: 32px;
min-width: 32px;
padding: 0px;
}
.header-command-group .command-button > span {
.header-command-group .command-button:not(.account-button) > span {
display: none;
}
}
+28 -26
View File
@@ -10,7 +10,9 @@
.dialog-header h2 {
margin: 2px 0px 0px;
font-size: 19px;
font-size: 16px;
font-weight: var(--font-weight-semibold);
letter-spacing: 0px;
}
@@ -36,9 +38,9 @@
}
.admin-dialog .model-row {
border-color: rgb(223, 228, 234);
border-color: var(--border);
border-radius: 7px;
border-radius: 8px;
background: var(--surface-subtle);
}
@@ -240,11 +242,11 @@
}
.alert-button.has-alerts {
border-color: rgb(214, 180, 93);
border-color: var(--warning-color);
background: rgb(255, 249, 233);
background: var(--warning-soft);
color: rgb(138, 97, 0);
color: var(--warning-color);
}
.alert-badge {
@@ -526,17 +528,17 @@
}
.assistant-message.user .assistant-message-content {
border-color: rgb(190, 210, 235);
border-color: var(--color-action-line);
background: var(--action-soft);
}
.assistant-message.is-error .assistant-message-content {
border-color: rgb(239, 197, 200);
border-color: var(--market-up);
background: var(--market-up-soft);
color: rgb(139, 47, 52);
color: var(--market-up);
}
.assistant-message-content p {
@@ -778,7 +780,7 @@
gap: 16px;
padding: 14px 18px;
padding: 20px;
min-height: 62px;
@@ -786,7 +788,7 @@
}
.assistant-dialog {
border-radius: 9px;
border-radius: 12px;
width: min(820px, -32px + 100vw);
@@ -796,9 +798,9 @@
}
.dialog-eyebrow {
color: var(--text-muted);
color: var(--text-3);
font-size: 12px;
font-size: var(--font-size-caption);
}
.global-search-dialog {
@@ -806,7 +808,7 @@
border-radius: 8px;
box-shadow: rgba(26, 37, 47, 0.22) 0px 22px 70px, rgba(26, 37, 47, 0.1) 0px 3px 14px;
box-shadow: var(--shadow-float);
max-width: 700px;
@@ -892,13 +894,13 @@
font: 11px / 1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
border-color: rgb(214, 221, 229);
border-color: var(--border-strong);
border-radius: 5px;
background: rgb(247, 248, 250);
background: var(--surface-subtle);
color: rgb(125, 136, 150);
color: var(--text-tertiary);
}
.global-search-results {
@@ -920,7 +922,7 @@
padding: 9px 10px 5px;
color: rgb(135, 146, 160);
color: var(--text-tertiary);
font-size: 10px;
}
@@ -957,9 +959,9 @@
.global-search-result.is-active,
.global-search-result:hover {
background: rgb(238, 244, 255);
background: var(--action-soft);
color: rgb(31, 85, 165);
color: var(--action);
}
.global-search-result-icon {
@@ -973,7 +975,7 @@
color: var(--text-muted);
border-color: rgb(224, 229, 235);
border-color: var(--border);
border-radius: 7px;
@@ -1003,7 +1005,7 @@
border-color: var(--dialog-line);
background: rgb(251, 252, 253);
background: var(--surface-subtle);
}
.alerts-dialog .alert-form {
@@ -1077,7 +1079,7 @@
padding: 16px 18px;
background: rgb(247, 249, 251);
background: var(--surface-subtle);
}
.assistant-dialog .assistant-message {
@@ -1087,9 +1089,9 @@
}
.assistant-dialog .assistant-message-content {
border-color: rgb(220, 227, 235);
border-color: var(--border);
border-radius: 7px;
border-radius: 8px;
font-size: 12.5px;
@@ -1197,7 +1199,7 @@
z-index: 95;
box-shadow: rgba(0, 0, 0, 0.12) -8px 0px 24px;
box-shadow: var(--shadow-float);
transition: right 0.25s;
@@ -52,21 +52,21 @@
}
.segmented {
height: 34px;
height: var(--control-height);
display: flex;
overflow: hidden;
min-height: 36px;
min-height: var(--control-height);
padding: 2px;
border: 0px;
border-radius: 7px;
border-radius: var(--radius-md);
background: rgb(236, 239, 244);
background: var(--surface-muted);
}
.segment {
@@ -82,11 +82,11 @@
border: 0px;
border-radius: 5px;
border-radius: 6px;
color: var(--text-secondary);
font-size: 11.5px;
font-size: var(--font-size-label);
}
.segment.active {
+55 -20
View File
@@ -14,13 +14,13 @@
.data-table th.sort-asc::after {
content: " ↑";
color: var(--blue);
color: var(--accent);
}
.data-table th.sort-desc::after {
content: " ↓";
color: var(--blue);
color: var(--accent);
}
.data-table tbody tr td {
@@ -48,7 +48,7 @@
color: var(--text-muted);
text-align: center;
text-align: left;
}
.table-action {
@@ -114,9 +114,9 @@
text-align: left;
height: 42px;
height: 40px;
padding: 8px 11px;
padding: 0 12px;
border-right: 0px;
@@ -214,7 +214,9 @@
.data-table .stock-name {
color: var(--text-primary);
font-weight: 700;
font-size: var(--fs-table);
font-weight: 600;
}
.main-grid > .table-frame {
@@ -242,7 +244,7 @@
}
.data-table thead th {
padding: 0px 11px;
padding: 0 12px;
border-bottom: 1px solid var(--border);
@@ -256,23 +258,31 @@
height: 36px;
font-size: var(--font-size-label);
font-size: var(--font-size-caption);
}
.data-table tbody td {
padding: 6px 11px;
border-bottom: 1px solid var(--border-subtle);
padding: 0 12px;
border-bottom: 1px solid var(--border);
color: var(--text-primary);
height: 40px;
}
height: 41px;
.data-table thead tr:last-child th:first-child,
.data-table tbody td:first-child,
.tbl thead tr:last-child th:first-child,
.tbl tbody td:first-child {
text-align: left;
}
.tbl .row-number {
text-align: left;
}
.main-grid .data-table thead th {
height: 34px;
height: 36px;
padding: 6px 10px;
padding: 0 12px;
}
.main-grid .data-table tbody td {
@@ -300,11 +310,11 @@
font-weight: 600;
font-size: 12px;
font-size: var(--font-size-caption);
text-align: left;
padding: 8px 12px;
padding: 0 12px;
border-bottom: 1px solid var(--line);
@@ -331,18 +341,30 @@
margin-left: 3px;
}
.tbl thead th.sorted .arr {
color: var(--blue);
.tbl thead th.sorted,
.data-table thead th.sorted,
.data-table thead th.sort-asc,
.data-table thead th.sort-desc {
color: var(--accent);
}
.tbl thead th.sorted .arr,
.data-table thead th.sorted .arr {
color: var(--accent);
}
.tbl tbody td {
padding: 9px 12px;
padding: 0 12px;
border-bottom: 1px solid var(--line-soft);
white-space: nowrap;
vertical-align: middle;
height: 40px;
font-size: var(--font-size-table);
}
.tbl tbody tr:hover {
@@ -415,6 +437,19 @@
color: var(--text-secondary);
}
:root[data-theme="dark"] :is(
.tbl thead th.sorted,
.data-table thead th.sorted,
.data-table thead th.sort-asc,
.data-table thead th.sort-desc
) {
color: var(--accent);
}
:root[data-theme="dark"] :is(.tbl thead th.sorted .arr, .data-table thead th.sorted .arr) {
color: var(--accent);
}
:root[data-theme="dark"] :is(table tbody td, .data-table tbody td, .tbl tbody td) {
border-color: var(--line-soft);
+28 -2
View File
@@ -89,7 +89,10 @@ function renderDashboard() {
const { meta, overview, ladders, sectors } = state.dashboard;
animateMetric("tapeUp", overview.up_count, (value) => Math.round(value));
animateMetric("tapeDown", overview.down_count, (value) => Math.round(value));
setText("tapeLimit", `${overview.limit_up_count} / 跌停 ${overview.limit_down_count}`);
animateMetric("tapeLimit", overview.limit_up_count, (value) => `${Math.round(value)}`);
animateMetric("tapeLimitDown", overview.limit_down_count, (value) => `${Math.round(value)}`);
animateMetric("detailBroken", overview.broken_count, (value) => `${Math.round(value)}`);
animateMetric("detailSealRate", overview.seal_rate, (value) => `${formatNumber(value, 1)}%`);
animateMetric("tapeAmount", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
animateMetric("limitUpMetric", overview.limit_up_count, (value) => `${Math.round(value)}`);
animateMetric("limitDownMetric", overview.limit_down_count, (value) => `${Math.round(value)}`);
@@ -98,7 +101,30 @@ function renderDashboard() {
animateMetric("amountMetric", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
setText("dataDateMetric", dashboardDataTimestamp(meta));
animateMetric("sentimentScore", overview.sentiment_score, (value) => Math.round(value));
setText("sentimentText", sentimentLabel(overview.sentiment_score));
animateMetric("detailSentimentScore", overview.sentiment_score, (value) => Math.round(value));
const moodLabel = sentimentLabel(overview.sentiment_score);
setText("sentimentText", moodLabel);
setText("detailSentimentText", moodLabel);
document.querySelectorAll("#sentimentText, #detailSentimentText").forEach((sentimentChip) => {
sentimentChip.classList.toggle("is-hot", moodLabel === "情绪高涨");
sentimentChip.classList.toggle("is-strong", moodLabel === "情绪偏强");
sentimentChip.classList.toggle("is-cold", moodLabel === "情绪冰点" || moodLabel === "情绪偏弱");
});
const pageSubtitle = document.querySelector("#currentPageSubtitle");
const activePage = window.XiaobaiPages?.get(state.activeView);
if (pageSubtitle) {
const dateText = displayCompactDate(meta.trade_date);
if (activePage?.id === "mentorView") {
pageSubtitle.textContent = dateText === "--"
? "与不同交易思维模型持续对话 · 数据日期 --"
: `与不同交易思维模型持续对话 · 数据日期 ${dateText}`;
} else {
const groupLabel = activePage?.group === "market"
? "市场复盘"
: activePage?.group === "personal" ? "个人" : "智能工具";
pageSubtitle.textContent = dateText === "--" ? groupLabel : `${groupLabel} · ${dateText}`;
}
}
updateSentimentGauge(overview.sentiment_score);
setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`);
+612 -79
View File
@@ -187,12 +187,6 @@ body.sidebar-collapsed .nav-group + .nav-group {
.metric {
border-top: 1px solid var(--line);
}
.metric-wide {
display: flex;
grid-column: span 2;
}
}
@media (max-width: 860px) {
@@ -228,10 +222,6 @@ body.sidebar-collapsed .nav-group + .nav-group {
grid-template-columns: repeat(3, 1fr);
}
.metric-wide {
grid-column: auto;
}
.metric {
padding: 10px;
}
@@ -262,10 +252,6 @@ body.sidebar-collapsed .nav-group + .nav-group {
grid-template-columns: repeat(2, 1fr);
}
.metric-wide {
grid-column: 1 / -1;
}
.section-title-group {
align-items: flex-start;
@@ -311,10 +297,6 @@ body.sidebar-collapsed .nav-group + .nav-group {
.overview-strip {
grid-template-columns: minmax(210px, 1.3fr) repeat(5, minmax(90px, 0.7fr));
}
.overview-strip .metric-wide {
display: none;
}
}
@media (max-width: 767px) {
@@ -644,10 +626,6 @@ body.sidebar-collapsed .app-main {
padding: 0px 8px;
}
.overview-strip .metric-wide {
display: none;
}
.overview-strip .metric:nth-of-type(n+4) {
display: none;
}
@@ -662,14 +640,6 @@ body.sidebar-collapsed .app-main {
padding: 8px;
}
.overview-strip[data-overview-expanded="true"] .metric-wide {
min-height: 54px;
display: flex;
border-bottom: 1px solid var(--border);
}
.overview-strip[data-overview-expanded="true"] .metric:nth-of-type(n) {
min-height: 54px;
@@ -770,10 +740,6 @@ body.sidebar-collapsed .app-main {
display: flex;
}
.overview-strip .metric-wide {
display: none;
}
.overview-strip .metric:nth-of-type(n+5) {
display: none;
}
@@ -892,7 +858,7 @@ body.sidebar-collapsed .app-main {
padding: 0px 10px;
border-radius: 7px;
border-radius: var(--radius-md);
font-size: 12.5px;
@@ -1024,8 +990,8 @@ body.sidebar-collapsed .app-main {
grid-template-columns: 176px minmax(0px, 1fr) auto;
}
.market-tape {
display: none;
.app-header .market-tape {
display: flex;
}
.module-nav {
@@ -1424,19 +1390,19 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
padding: 12px 10px 4px;
color: var(--r2-faint);
color: var(--text-3);
font-size: 11px;
font-size: var(--fs-aux);
font-weight: 400;
font-weight: 600;
letter-spacing: 0px;
letter-spacing: .04em;
}
.module-nav .module-tab {
width: 100%;
min-height: 34px;
min-height: 36px;
display: flex;
@@ -1452,13 +1418,13 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
border: 0px;
border-radius: 7px;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-secondary);
color: var(--text-2);
font-size: 13px;
font-size: var(--fs-table);
font-weight: 400;
}
@@ -1476,23 +1442,23 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
}
.module-nav .module-tab:hover {
background: var(--surface-hover);
background: var(--hover);
color: var(--r2-ink);
color: var(--text-1);
}
.module-nav .module-tab.active {
background: var(--r2-blue-soft);
background: var(--selected);
color: var(--r2-blue);
color: var(--accent);
font-weight: 600;
}
.module-nav .module-tab.mobile-active {
background: var(--r2-blue-soft);
background: var(--selected);
color: var(--r2-blue);
color: var(--accent);
font-weight: 600;
}
@@ -1756,7 +1722,11 @@ body.sidebar-collapsed .module-nav .module-tab {
padding: 0px 16px;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
border-top: 0px;
@@ -1775,6 +1745,10 @@ body.sidebar-collapsed .module-nav .module-tab {
box-shadow: none;
}
.overview-strip::-webkit-scrollbar {
display: none;
}
.overview-strip .row {
display: flex;
@@ -1784,6 +1758,8 @@ body.sidebar-collapsed .module-nav .module-tab {
padding: 7px var(--page-pad-x);
flex-shrink: 0;
font-size: 12px;
color: var(--sub);
@@ -1998,7 +1974,7 @@ body.sidebar-collapsed .module-nav .module-tab {
width: auto;
min-height: 30px;
min-height: var(--statusbar-height);
height: var(--statusbar-height);
@@ -2012,11 +1988,11 @@ body.sidebar-collapsed .module-nav .module-tab {
border-top: 1px solid var(--line);
background: var(--surface);
background: var(--header-bg);
color: var(--faint);
color: var(--text-3);
font-size: 11.5px;
font-size: var(--fs-aux);
}
body.sidebar-collapsed .status-bar {
@@ -2158,7 +2134,7 @@ body.sidebar-collapsed .status-bar {
width: var(--sidebar-width);
background: var(--surface);
background: var(--header-bg);
border-right: 1px solid var(--line);
@@ -2266,7 +2242,15 @@ body.sidebar-collapsed .status-bar {
.main {
margin-left: var(--sidebar-width);
min-width: 1080px;
min-width: 0;
overflow-x: hidden;
}
@media (min-width: 1280px) {
.main {
min-width: 1080px;
}
}
.app-header {
@@ -2276,7 +2260,7 @@ body.sidebar-collapsed .status-bar {
width: auto;
min-height: 46px;
min-height: var(--topbar-height);
box-shadow: none;
@@ -2288,17 +2272,17 @@ body.sidebar-collapsed .status-bar {
z-index: 50;
background: var(--surface);
background: var(--header-bg);
border-bottom: 1px solid var(--line);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 14px;
gap: var(--header-cluster-gap);
padding: 0 var(--page-pad-x);
padding: 0 var(--header-pad-x);
height: var(--topbar-height);
}
@@ -2412,7 +2396,7 @@ body.sidebar-collapsed .status-bar {
overflow: hidden;
}
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .overview-strip {
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .app-main .overview-strip {
flex: 0 0 auto;
}
@@ -2428,7 +2412,7 @@ body.sidebar-collapsed .status-bar {
overflow: hidden;
}
body:is([data-active-view="auctionView"], [data-active-view="themeLibraryView"], [data-active-view="popularityView"], [data-active-view="mentorView"], [data-active-view="rotationView"]) .overview-strip {
body:is([data-active-view="auctionView"], [data-active-view="themeLibraryView"], [data-active-view="popularityView"], [data-active-view="mentorView"], [data-active-view="rotationView"]) .app-main .overview-strip {
flex: 0 0 auto;
}
@@ -2478,8 +2462,8 @@ body.sidebar-collapsed .status-bar {
padding: var(--mobile-shell-pad);
}
.market-tape {
display: none;
.app-header .market-tape {
display: flex;
}
.header-actions {
@@ -2701,10 +2685,6 @@ body.sidebar-collapsed .status-bar {
padding: 0px;
}
.overview-strip .metric-wide {
display: none;
}
.overview-strip .metric:nth-of-type(n+4) {
display: none;
}
@@ -2730,10 +2710,20 @@ body.sidebar-collapsed .status-bar {
color: var(--text-primary);
}
:root[data-theme="dark"] :is(.module-nav, .app-header, .overview-strip, .status-bar) {
:root[data-theme="dark"] :is(.module-nav, .app-header, .status-bar) {
border-color: var(--border);
background: var(--surface);
background: var(--header-bg);
color: var(--text-primary);
box-shadow: none;
}
:root[data-theme="dark"] .overview-strip {
border-color: var(--border);
background: transparent;
color: var(--text-primary);
@@ -2741,7 +2731,7 @@ body.sidebar-collapsed .status-bar {
}
:root[data-theme="dark"] .app-header {
background: color-mix(in srgb, var(--surface) 96%, transparent);
background: var(--header-bg);
}
:root[data-theme="dark"] :is(.sidebar-brand, .module-nav .sidebar-brand, .nav-group, .sidebar-collapse-button, .header-date-group) {
@@ -3019,10 +3009,16 @@ body.sidebar-collapsed .status-bar {
body.mobile-shell .header-command-group > .command-button,
body.mobile-shell .header-command-group .account-button {
width: 100%;
max-width: none;
min-height: var(--mobile-touch-size);
justify-content: flex-start;
}
body.mobile-shell .header-command-group .account-button > span,
body.mobile-shell .header-command-group .account-role-badge span {
display: inline;
}
body.mobile-shell .header-command-group .account-dropdown {
position: static;
grid-column: 1 / -1;
@@ -3188,11 +3184,6 @@ body.sidebar-collapsed .status-bar {
display: flex;
}
body.mobile-shell .overview-strip .metric-wide,
body.mobile-shell .overview-toggle {
display: none;
}
body.mobile-shell .workspace-view,
body.mobile-shell .workspace-view.page:not(#heavenView),
body.mobile-shell:is([data-active-view]) .workspace-view.active-view {
@@ -3222,3 +3213,545 @@ body.sidebar-collapsed .status-bar {
display: none;
}
}
/* Generic page title slot in the shell header. Kept hidden so each page owns
its own in-view title header (mentor renders one inside #mentorView). */
.app-page-context {
min-width: var(--header-page-context-min);
max-width: var(--header-page-context-max);
flex: 0 1 auto;
flex-direction: column;
justify-content: center;
gap: 2px;
overflow: hidden;
display: flex;
}
.app-header .app-page-context,
.main .app-header .app-page-context {
min-width: var(--header-page-context-min);
max-width: var(--header-page-context-max);
overflow: hidden;
}
.main .app-header .app-page-context span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#themeModeText {
display: none;
}
.app-page-context strong {
overflow: hidden;
color: var(--text-1);
font-size: var(--fs-page-title);
font-weight: 600;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.app-page-context span {
overflow: hidden;
color: var(--text-3);
font-size: var(--fs-caption);
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Canonical B-147 merged header. */
.app-header .overview-strip,
.app-header .overview-strip[data-overview-expanded="true"] {
position: static;
display: flex;
flex: 1 0 auto;
align-items: stretch;
width: auto;
height: auto;
min-width: max-content;
min-height: 0;
margin: 0;
padding: 0;
overflow: visible;
border: 0;
background: transparent;
box-shadow: none;
}
.app-header .market-tape {
display: flex;
flex: 1 0 auto;
align-items: center;
min-width: max-content;
gap: 0;
overflow: visible;
white-space: nowrap;
}
.tape-metric {
position: relative;
display: flex;
flex: 0 0 auto;
flex-direction: column;
justify-content: center;
gap: 2px;
min-width: max-content;
padding: 0 var(--header-tape-metric-pad-x);
}
.app-header .tape-metric,
.app-header .tape-sentiment.sentiment-block {
flex: 0 0 auto;
min-width: max-content;
}
.tape-metric + .tape-metric::before,
.tape-metric + .overview-toggle::before {
content: "";
position: absolute;
top: 50%;
left: 0;
width: 1px;
height: 26px;
background: var(--border);
transform: translateY(-50%);
}
.tape-metric .metric-label {
color: var(--text-3);
font-size: var(--fs-aux);
}
.tape-metric-value,
.tape-metric .metric-value {
display: flex;
align-items: center;
gap: 5px;
overflow: visible;
color: var(--text-1);
font-size: 15px;
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1.2;
white-space: nowrap;
}
.tape-metric .metric-value.up,
.tape-detail-item .up {
color: var(--up);
}
.tape-metric .metric-value.down,
.tape-detail-item .down {
color: var(--down);
}
.tape-metric .metric-value.warning,
.tape-detail-item .warning {
color: var(--warn);
}
@media (min-width: 1440px) and (max-width: 1599px) {
.app-header .overview-strip .tape-sentiment.sentiment-block {
flex-direction: row;
align-items: center;
gap: 8px;
}
}
.app-header .overview-strip .tape-sentiment .metric-label,
.app-header .overview-strip .tape-sentiment .sentiment-text {
font-size: var(--fs-caption);
font-weight: 500;
line-height: 18px;
}
.app-header .overview-strip .tape-sentiment > .sentiment-gauge {
position: absolute;
display: none;
width: 0;
height: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.tape-sentiment .sentiment-text,
.tape-detail-item .sentiment-text {
display: inline-flex;
align-items: center;
height: 18px;
padding: 0 6px;
border-radius: var(--radius-sm);
background: var(--accent-soft);
color: var(--accent);
font-size: var(--fs-caption);
font-style: normal;
font-weight: 500;
line-height: 18px;
}
.tape-sentiment .sentiment-text.is-hot,
.tape-detail-item .sentiment-text.is-hot {
background: var(--up-soft);
color: var(--up);
}
.tape-sentiment .sentiment-text.is-strong,
.tape-detail-item .sentiment-text.is-strong {
background: var(--accent-soft);
color: var(--accent);
}
.tape-sentiment .sentiment-text.is-cold,
.tape-detail-item .sentiment-text.is-cold {
background: var(--down-soft);
color: var(--down);
}
.app-header .overview-toggle {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
height: 28px;
min-height: 28px;
margin-left: var(--header-tape-toggle-gap);
padding: 0 10px;
border: 0;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-2);
font-size: var(--fs-label);
}
.app-header .overview-toggle:hover {
background: var(--hover);
color: var(--text-1);
}
.app-header .overview-toggle .lucide {
width: 14px;
height: 14px;
}
.tape-detail {
position: absolute;
top: calc(var(--topbar-height) + 1px);
right: 0;
left: 0;
z-index: 45;
display: none;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 0 16px;
padding: 14px 20px 16px;
border-bottom: 1px solid var(--border);
background: var(--header-bg);
box-shadow: var(--shadow-raised);
}
.overview-strip[data-overview-expanded="true"] .tape-detail {
display: grid;
}
.tape-detail-item {
display: flex;
flex-direction: column;
gap: 3px;
padding: 8px 14px;
border-radius: var(--radius-md);
}
.tape-detail-item:hover {
background: var(--hover);
}
.tape-detail-date,
.overview-strip[data-overview-expanded="true"] .tape-detail-date {
display: flex;
flex-direction: column;
}
.tape-detail-item span {
color: var(--text-3);
font-size: var(--fs-aux);
}
.tape-detail-item strong {
color: var(--text-1);
font-size: 16px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.app-header .header-actions {
position: relative;
display: flex;
flex: 0 0 auto;
align-items: center;
gap: var(--header-action-gap);
margin-left: 0;
overflow: visible;
}
.app-header .header-date-group {
height: 32px;
min-height: 32px;
gap: 2px;
margin-right: 6px;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.app-header .header-date-group .icon-button,
.app-header .header-actions > .icon-button {
width: 32px;
min-width: 32px;
height: 32px;
min-height: 32px;
border: 0;
border-radius: var(--radius-md);
background: transparent;
color: var(--text-2);
box-shadow: none;
}
.app-header .header-actions > .icon-button:hover,
.app-header .header-date-group .icon-button:hover {
background: var(--hover);
color: var(--text-1);
}
.app-header .header-date-group .date-input {
width: auto;
height: 32px;
padding: 0 10px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
background: var(--surface);
color: var(--text-1);
font-size: var(--fs-label);
font-variant-numeric: tabular-nums;
}
.app-header .header-actions > .icon-button .lucide,
.app-header .header-date-group .lucide {
width: 16px;
height: 16px;
}
.app-header .alert-button {
position: relative;
}
.app-header .alert-badge {
position: absolute;
top: 5px;
right: 5px;
min-width: 14px;
height: 14px;
padding: 0 3px;
border-radius: 7px;
background: var(--up);
color: #fff;
font-size: 10px;
font-weight: 600;
line-height: 14px;
text-align: center;
}
#heavenView.workspace-view,
#heavenReadingDialog {
--color-action-base: #2563eb;
--color-action: #2563eb;
--color-action-base-hover: #1d4ed8;
--color-action-hover: #1d4ed8;
--color-action-base-soft: #eff4ff;
--color-action-soft: #eff4ff;
--action: #2563eb;
--action-hover: #1d4ed8;
--action-soft: #eff4ff;
--accent: #2563eb;
--primary: #2563eb;
--blue: #2563eb;
--r2-blue: #2563eb;
--focus-ring: rgba(37, 99, 235, .16);
--text-primary: #1f2937;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border: #e5e7eb;
--border-strong: #d1d5db;
--surface-hover: #f8faff;
--surface-selected: #eff4ff;
--size-radius-sm: 5px;
--size-radius-md: 7px;
}
:root[data-theme="dark"] #heavenView.workspace-view,
:root[data-theme="dark"] #heavenReadingDialog {
--action: #6ca8e8;
--action-hover: #8bbcf0;
--action-soft: #23364a;
--accent: #6ca8e8;
--primary: #6ca8e8;
--blue: #6ca8e8;
--r2-blue: #6ca8e8;
--canvas: #121416;
--surface: #1b1e21;
--surface-muted: #202428;
--border: #343a40;
--border-strong: #474f57;
--text-primary: #e8eaed;
--text-secondary: #adb5bd;
--text-tertiary: #7f8993;
--market-up: #f06d73;
--market-up-soft: #40262a;
--focus-ring: rgba(108, 168, 232, .22);
}
@media (min-width: 1024px) and (max-width: 1599px) {
.tape-optional {
display: none;
}
}
@media (max-width: 1440px) {
.app-page-context {
display: none;
}
}
@media (max-width: 1023px) {
.app-header {
gap: 8px;
height: var(--mobile-header-height);
min-height: var(--mobile-header-height);
padding: 0 10px;
}
.app-header .overview-strip,
.app-header .overview-strip[data-overview-expanded="true"] {
flex: 0 0 auto;
width: auto;
}
.app-header .market-tape .tape-metric {
display: none;
}
.app-header .overview-toggle {
width: var(--mobile-touch-size);
min-width: var(--mobile-touch-size);
height: var(--mobile-touch-size);
min-height: var(--mobile-touch-size);
margin-left: 0;
justify-content: center;
}
.app-header .overview-toggle span {
display: none;
}
.tape-detail,
.overview-strip[data-overview-expanded="true"] .tape-detail {
top: calc(var(--mobile-header-height) + 1px);
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.tape-detail-date,
.overview-strip[data-overview-expanded="true"] .tape-detail-date {
display: flex;
flex-direction: column;
}
}
@media (max-width: 767px) {
body.mobile-shell .app-header .overview-strip,
body.mobile-shell .app-header .overview-strip[data-overview-expanded="true"] {
width: auto;
height: auto;
min-height: 0;
display: flex;
margin: 0;
padding: 0;
overflow: visible;
border: 0;
background: transparent;
}
body.mobile-shell .app-header .market-tape {
display: flex;
overflow: visible;
}
body.mobile-shell .app-header .overview-toggle {
display: inline-flex;
}
}
@media (min-width: 768px) {
.app-header .header-menu-button {
display: none;
}
.app-header .header-command-group {
position: static;
display: flex;
flex: 0 0 auto;
flex-direction: row;
align-items: center;
gap: var(--header-command-gap);
width: auto;
max-width: none;
padding: 0;
overflow: visible;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.app-header .header-command-group .command-button,
.app-header .header-command-group .account-button {
width: auto;
justify-content: center;
}
.app-header .header-command-group .account-menu-shell {
display: inline-flex;
flex-direction: row;
align-items: center;
width: auto;
}
.app-header .header-command-group .account-role-badges {
display: inline-flex;
flex-wrap: nowrap;
min-height: 32px;
}
.app-header .header-command-group .account-dropdown {
left: auto;
right: 0;
}
}
body[data-active-view="heavenView"] .app-header .overview-strip,
body[data-active-view="heavenView"] .app-header .overview-strip[data-overview-expanded="true"],
body.mobile-shell[data-active-view="heavenView"] .app-header .overview-strip,
body.mobile-shell[data-active-view="heavenView"] .app-header .overview-strip[data-overview-expanded="true"] {
display: none;
flex: 0 0 0;
width: 0;
min-width: 0;
height: 0;
min-height: 0;
overflow: hidden;
}
+56 -15
View File
@@ -22,12 +22,19 @@
const button = document.querySelector("#headerMenuButton");
const backdrop = document.querySelector("#mobileCommandBackdrop");
if (!menu || !button) return;
if (!isMobileViewport()) {
menu.classList.remove("is-open");
button.setAttribute("aria-expanded", "false");
document.body.classList.remove("mobile-command-open");
if (backdrop) backdrop.hidden = true;
return;
}
const open = typeof force === "boolean" ? force : !menu.classList.contains("is-open");
menu.classList.toggle("is-open", open);
button.setAttribute("aria-expanded", String(open));
document.body.classList.toggle("mobile-command-open", open && isMobileViewport());
if (backdrop) backdrop.hidden = !(open && isMobileViewport());
if (open && isMobileViewport()) {
document.body.classList.toggle("mobile-command-open", open);
if (backdrop) backdrop.hidden = !open;
if (open) {
global.requestAnimationFrame(() => menu.querySelector("button:not([hidden])")?.focus({ preventScroll: true }));
} else if (force === false && document.activeElement && menu.contains(document.activeElement)) {
button.focus({ preventScroll: true });
@@ -56,6 +63,28 @@
updateSidebarControl();
}
function pageGroupLabel(page) {
return page?.group === "market"
? "市场复盘"
: page?.group === "personal" ? "个人" : "智能工具";
}
function writePageSubtitle(viewId) {
const pageSubtitle = document.querySelector("#currentPageSubtitle");
if (!pageSubtitle) return;
const page = registry.get(viewId);
const dateText = options.tradeDate?.() || "";
const hasDate = Boolean(dateText) && dateText !== "--";
if (viewId === "mentorView") {
pageSubtitle.textContent = hasDate
? `与不同交易思维模型持续对话 · 数据日期 ${dateText}`
: "与不同交易思维模型持续对话 · 数据日期 --";
return;
}
const groupLabel = pageGroupLabel(page);
pageSubtitle.textContent = hasDate ? `${groupLabel} · ${dateText}` : groupLabel;
}
function syncNavigation(viewId) {
const page = registry.get(viewId);
const navigationId = page?.navigation_alias || viewId;
@@ -69,6 +98,9 @@
: page?.group === "personal" ? "复盘" : "工具";
}
if (mobileTitle) mobileTitle.textContent = page?.title || "小白复盘";
const pageTitle = document.querySelector("#currentPageTitle");
if (pageTitle) pageTitle.textContent = page?.title || "小白复盘";
writePageSubtitle(viewId);
document.querySelectorAll(".module-tab").forEach((button) => {
button.classList.toggle("active", button.dataset.view === navigationId);
button.classList.toggle(
@@ -184,26 +216,35 @@
toggleHeaderCommandMenu(false);
}
});
function setOverviewExpanded(expanded) {
const overview = document.querySelector(".overview-strip");
const button = document.querySelector("#overviewToggle");
if (!overview || !button) return;
overview.dataset.overviewExpanded = String(expanded);
button.setAttribute("aria-expanded", String(expanded));
button.title = expanded ? "收起市场详情" : "展开市场详情";
const label = button.querySelector("span");
if (label) label.textContent = "详情";
button.querySelector("i")?.setAttribute("data-lucide", expanded ? "chevron-up" : "chevron-down");
options.refreshIcons?.();
}
document.querySelector("#overviewToggle")?.addEventListener("click", (event) => {
event.stopPropagation();
const overview = document.querySelector(".overview-strip");
if (!overview) return;
const expanded = overview.dataset.overviewExpanded !== "true";
overview.dataset.overviewExpanded = String(expanded);
event.currentTarget.setAttribute("aria-expanded", String(expanded));
event.currentTarget.title = expanded ? "收起市场详情" : "展开市场详情";
const label = event.currentTarget.querySelector("span");
if (label) label.textContent = expanded ? "收起详情" : "展开详情";
event.currentTarget.querySelector("i")?.setAttribute(
"data-lucide",
expanded ? "chevron-up" : "chevron-down",
);
options.refreshIcons?.();
setOverviewExpanded(expanded);
if (expanded) toggleHeaderCommandMenu(false);
});
document.addEventListener("click", (event) => {
if (!event.target.closest(".header-actions")) toggleHeaderCommandMenu(false);
if (!event.target.closest("#headerCommandGroup, #headerMenuButton")) toggleHeaderCommandMenu(false);
if (!event.target.closest(".overview-strip")) setOverviewExpanded(false);
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") toggleHeaderCommandMenu(false);
if (event.key !== "Escape") return;
toggleHeaderCommandMenu(false);
setOverviewExpanded(false);
});
global.addEventListener("resize", () => {
toggleHeaderCommandMenu(false);
+2
View File
@@ -13,6 +13,8 @@ function syncThemeControl() {
button.setAttribute("aria-label", label);
button.setAttribute("aria-pressed", String(dark));
button.querySelector("i")?.setAttribute("data-lucide", dark ? "sun" : "moon");
const modeText = document.querySelector("#themeModeText");
if (modeText) modeText.textContent = dark ? "夜间模式" : "日间模式";
}
function clearThemeTransitionEffects() {
+138 -67
View File
@@ -22,19 +22,20 @@
--color-gray-900: #1f2937;
--color-shell-canvas: #f4f5f7;
--color-page-canvas: #f4f5f7;
--color-surface-muted: #f3f4f6;
--color-surface-subtle: #f8fafc;
--color-border: #e5e7eb;
--color-border-strong: #d1d5db;
--color-text-primary: #1f2937;
--color-text-secondary: #6b7280;
--color-text-tertiary: #9ca3af;
--color-action-base: #2563eb;
--color-action-base-hover: #1d4ed8;
--color-action-base-soft: #eff4ff;
--color-action: #2563eb;
--color-action-hover: #1d4ed8;
--color-action-soft: #eff4ff;
--color-surface-muted: #f2f3f5;
--color-surface-subtle: #f8f9fb;
--color-border: #eceded;
--color-border-strong: #dee0e3;
--color-text-primary: #1f2329;
--color-text-secondary: #646a73;
--color-text-tertiary: #8f959e;
--color-action-base: #3370ff;
--color-action-base-hover: #2b5fd9;
--color-action-base-soft: #eaf1fe;
--color-action: #3370ff;
--color-action-hover: #2b5fd9;
--color-action-soft: #eaf1fe;
--color-action-press: #2456b8;
--color-action-line: #c7d8fb;
--color-market-up-base: #e04536;
--color-market-up-base-soft: #fdecea;
@@ -49,24 +50,25 @@
--color-warning: #b45309;
--color-warning-soft: #fdf3e3;
--size-radius-sm: 5px;
--size-radius-md: 7px;
--size-radius-sm: 4px;
--size-radius-md: 8px;
--size-radius-lg: 10px;
--size-radius-dialog: 12px;
--size-control: 32px;
--size-sidebar: 200px;
--size-topbar: 46px;
--size-summary: 36px;
--size-statusbar: 30px;
--size-topbar: 64px;
--size-summary: 0px;
--size-statusbar: 28px;
--size-page-pad-y: 14px;
--size-page-pad-x: 16px;
--size-card-gap: 12px;
--elevation-card: 0 1px 2px rgba(16, 24, 40, .05);
--elevation-soft: 0 1px 2px rgba(16, 24, 40, .05);
--elevation-raised: 0 4px 14px rgba(16, 24, 40, .06);
--elevation-float: 0 12px 32px rgba(0, 0, 0, .18);
--elevation-card: 0 1px 2px rgba(31, 35, 41, .04);
--elevation-soft: 0 1px 2px rgba(31, 35, 41, .04);
--elevation-raised: 0 2px 6px rgba(31, 35, 41, .04), 0 8px 24px rgba(31, 35, 41, .06);
--elevation-float: 0 12px 32px rgba(0, 0, 0, .14);
--motion-instant: 100ms;
--motion-fast: 140ms;
--motion-fast: 120ms;
--motion-medium: 200ms;
--motion-deliberate: 260ms;
--motion-slow: 560ms;
@@ -80,8 +82,11 @@
--surface-canvas: var(--color-page-canvas);
--surface-raised: var(--color-white);
--surface-sunken: #eef0f3;
--surface-hover: #f8faff;
--surface-selected: #eff4ff;
--header-bg: var(--color-white);
--surface-hover: #f2f3f5;
--surface-selected: #eaf1fe;
--hover: var(--surface-hover);
--selected: var(--surface-selected);
--surface-overlay: var(--color-white);
--border: var(--color-border);
--border-strong: var(--color-border-strong);
@@ -93,7 +98,12 @@
--on-action: #ffffff;
--action: var(--color-action-base);
--action-hover: var(--color-action-base-hover);
--action-press: var(--color-action-press);
--action-soft: var(--color-action-base-soft);
--accent: var(--action);
--accent-hover: var(--action-hover);
--accent-press: var(--action-press);
--accent-soft: var(--action-soft);
--market-up: var(--color-market-up-base);
--market-up-soft: var(--color-market-up-base-soft);
--market-down: var(--color-market-down-base);
@@ -106,7 +116,12 @@
--table-header: var(--surface-subtle);
--table-hover: #f8faff;
--table-selected: var(--surface-selected);
--focus-ring: rgba(37, 99, 235, .16);
--focus-ring: rgba(51, 112, 255, .15);
--text-1: var(--text-primary);
--text-2: var(--text-secondary);
--text-3: var(--text-tertiary);
--warn: var(--warning-color);
--warn-soft: var(--warning-soft);
--backdrop: rgba(17, 24, 39, .48);
--primary: var(--color-action);
--primary-hover: var(--color-action-hover);
@@ -126,18 +141,32 @@
--radius-lg: var(--size-radius-lg);
--shadow-xs: var(--elevation-card);
--shadow-sm: var(--elevation-raised);
--shadow-card: var(--elevation-card);
--shadow-raised: var(--elevation-raised);
--shadow-float: var(--elevation-float);
--duration-fast: 150ms;
--duration-normal: 220ms;
--font-size-aux: 10.5px;
--font-size-caption: 11px;
--font-size-label: 12px;
--font-size-table: 12.5px;
--font-size-body: 13px;
--font-size-card-title: 14px;
--font-size-page-title: 17px;
--font-size-metric: 22px;
--font-size-aux: 11.5px;
--font-size-caption: 12.5px;
--font-size-label: 13px;
--font-size-table: 13.5px;
--font-size-body: 14px;
--font-size-chat: 14px;
--font-size-card-title: 15px;
--font-size-page-title: 18px;
--font-size-metric: 24px;
--font-size-hero: 30px;
--fs-aux: var(--font-size-aux);
--fs-caption: var(--font-size-caption);
--fs-label: var(--font-size-label);
--fs-table: var(--font-size-table);
--fs-body: var(--font-size-body);
--fs-chat: var(--font-size-chat);
--fs-card-title: var(--font-size-card-title);
--fs-page-title: var(--font-size-page-title);
--fs-metric: var(--font-size-metric);
--fs-hero: var(--font-size-hero);
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
@@ -147,11 +176,20 @@
--space-8: 8px;
--space-12: 12px;
--space-16: 16px;
--space-20: 20px;
--space-24: 24px;
--sidebar-width: var(--size-sidebar);
--sidebar-compact-width: 64px;
--topbar-height: var(--size-topbar);
--header-pad-x: 20px;
--header-cluster-gap: 24px;
--header-action-gap: 6px;
--header-command-gap: 6px;
--header-tape-metric-pad-x: 16px;
--header-tape-toggle-gap: 14px;
--header-page-context-min: 148px;
--header-page-context-max: 280px;
--summary-height: var(--size-summary);
--statusbar-height: var(--size-statusbar);
--page-pad-y: var(--size-page-pad-y);
@@ -219,13 +257,14 @@
--dragon-profile-weight-strong: 750;
--dragon-profile-weight-semibold: 600;
--mentor-directory-width: 280px;
--mentor-profile-width: 272px;
--mentor-pane-header-height: 58px;
--mentor-avatar-size: 38px;
--mentor-directory-width: 300px;
--mentor-pane-header-height: 56px;
--mentor-avatar-size: 40px;
--mentor-message-avatar-size: 36px;
--mentor-chat-avatar-size: 38px;
--mentor-profile-avatar-size: 68px;
--mentor-composer-min-height: 94px;
--mentor-message-max-width: 82%;
--mentor-composer-min-height: 85px;
--mentor-message-max-width: 60%;
--chart-background: #fbfcfd;
--chart-grid: #e2e8ec;
@@ -313,45 +352,64 @@
--r2-radius: var(--size-radius-lg);
--r2-shadow: var(--elevation-card);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif;
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
font-size: 14px;
}
:root[data-theme="dark"] {
color-scheme: dark;
--canvas: #121416;
--surface: #1b1e21;
--surface-muted: #202428;
--surface-subtle: #24282d;
--canvas: #141519;
--header-bg: #1c1e23;
--surface: #232529;
--surface-muted: #2a2d33;
--surface-subtle: #202329;
--surface-canvas: var(--canvas);
--surface-raised: var(--surface);
--surface-sunken: #16191c;
--surface-hover: #24282d;
--surface-selected: #23364a;
--surface-overlay: #24282d;
--border: #343a40;
--border-strong: #474f57;
--border-subtle: #2a2f34;
--surface-sunken: #191b1f;
--surface-hover: #2a2d33;
--surface-selected: #2b3b58;
--surface-overlay: #232529;
--hover: var(--surface-hover);
--selected: var(--surface-selected);
--border: #2b2e34;
--border-strong: #3a3e47;
--border-subtle: #2b2e34;
--text-primary: #e8eaed;
--text-secondary: #adb5bd;
--text-tertiary: #7f8993;
--text-secondary: #a9adb3;
--text-tertiary: #7c828a;
--text-1: var(--text-primary);
--text-2: var(--text-secondary);
--text-3: var(--text-tertiary);
--text-inverse: #ffffff;
--action: #6ca8e8;
--action-hover: #8bbcf0;
--action-soft: #23364a;
--market-up: #f06d73;
--market-up-soft: #40262a;
--action: #5b8def;
--action-hover: #7ba5f5;
--action-press: #3465c4;
--action-soft: #2b3b58;
--accent: var(--action);
--accent-hover: var(--action-hover);
--accent-press: var(--action-press);
--accent-soft: var(--action-soft);
--market-up: #f26762;
--market-up-soft: #3d2829;
--market-down: #43bc8a;
--market-down-soft: #1d382f;
--market-down-soft: #22362c;
--warning-color: #e2ad58;
--warning-soft: #3d3220;
--control-surface: #202428;
--control-hover: #2a2f34;
--control-border: #474f57;
--table-header: #202428;
--table-hover: #242f39;
--table-selected: #23364a;
--focus-ring: rgba(108, 168, 232, .22);
--warn: var(--warning-color);
--warn-soft: var(--warning-soft);
--control-surface: #232529;
--control-hover: #2a2d33;
--control-border: #3a3e47;
--table-header: #202329;
--table-hover: #2a2d33;
--table-selected: #2b3b58;
--focus-ring: rgba(91, 141, 239, .25);
--elevation-card: 0 1px 2px rgba(0, 0, 0, .28);
--elevation-soft: 0 1px 2px rgba(0, 0, 0, .28);
--elevation-raised: 0 1px 2px rgba(0, 0, 0, .30), 0 8px 24px rgba(0, 0, 0, .35);
--elevation-float: 0 12px 32px rgba(0, 0, 0, .46);
--shadow-card: var(--elevation-card);
--shadow-raised: var(--elevation-raised);
--backdrop: rgba(0, 0, 0, .66);
--primary: var(--action);
--primary-hover: var(--action-hover);
@@ -467,3 +525,16 @@
--shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16);
--shadow: 0 18px 50px rgba(0, 0, 0, .46);
}
@media (min-width: 1280px) and (max-width: 1919px) {
:root {
--header-pad-x: 12px;
--header-cluster-gap: 8px;
--header-action-gap: 4px;
--header-command-gap: 4px;
--header-tape-metric-pad-x: 6px;
--header-tape-toggle-gap: 4px;
--header-page-context-min: 96px;
--header-page-context-max: 148px;
}
}
+783 -29
View File
@@ -483,7 +483,7 @@ async function mockApplication(page, authSession = session(), options = {}) {
}],
};
} else if (url.pathname === "/api/mentors/setup") {
payload = { trade_date: "20260722", mentors: mentorDirectory(authSession.user.role) };
payload = { trade_date: "20260722", mentors: options.mentors || mentorDirectory(authSession.user.role) };
} else if (url.pathname === "/api/mentors/chat") {
await route.fulfill({
status: 200,
@@ -510,12 +510,64 @@ async function mockApplication(page, authSession = session(), options = {}) {
});
}
async function openHeaderCommandMenu(page) {
const menu = page.locator("#headerCommandGroup");
if (await menu.isVisible()) return;
await page.locator("#headerMenuButton").click();
await expect(menu).toBeVisible();
}
function namedSession(username, role = "admin", subscribed = true) {
const auth = session(role, subscribed);
auth.user.username = username;
return auth;
}
async function setColorTheme(page, theme) {
const wanted = theme === "night" ? "dark" : "light";
const current = await page.locator("html").getAttribute("data-theme");
if (current === wanted) return;
await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", wanted);
}
function measureAccountName() {
const name = document.querySelector("#accountName");
const button = document.querySelector("#accountButton");
const badges = document.querySelector("#accountRoleBadges");
const adminLabel = document.querySelector("#accountAdminBadge > span");
const vipLabel = document.querySelector("#accountVipLabel");
const box = (node) => node ? node.getBoundingClientRect() : null;
const visible = (node) => {
if (!node || node.hidden) return false;
const style = getComputedStyle(node);
if (style.display === "none" || style.visibility === "hidden") return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const buttonBox = box(button);
const badgeBox = box(badges);
return {
text: name ? String(name.textContent || "") : "",
clientWidth: name ? name.clientWidth : 0,
scrollWidth: name ? name.scrollWidth : 0,
fits: name ? name.clientWidth >= name.scrollWidth : false,
buttonFits: button ? button.clientWidth >= button.scrollWidth : false,
stacked: Boolean(badgeBox && buttonBox && buttonBox.top >= badgeBox.bottom - 1),
adminLabelVisible: visible(adminLabel),
vipLabelVisible: visible(vipLabel),
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
};
}
test("admin shell opens every primary workspace and global search", async ({ page }) => {
await mockApplication(page);
await page.goto("/index.html");
await expect(page.locator("#authGate")).toBeHidden();
await openHeaderCommandMenu(page);
await expect(page.locator("#settingsButton")).toBeVisible();
await expect(page.locator("#syncButton")).toBeVisible();
await page.keyboard.press("Escape");
await page.locator("#alertButton").click();
await expect(page.locator("#alertsDialog")).toBeVisible();
await page.locator("#closeAlertsDialog").click();
@@ -526,7 +578,7 @@ test("admin shell opens every primary workspace and global search", async ({ pag
const views = [
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView", "screenerView",
"mentorView", "heavenView", "reviewWorkspaceView",
"heavenView", "reviewWorkspaceView", "mentorView",
];
for (const view of views) {
await page.locator(`[data-view="${view}"]`).first().click();
@@ -567,7 +619,7 @@ test("every primary workspace shares the canonical desktop shell geometry", asyn
const views = [
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView",
"screenerView", "mentorView", "heavenView", "reviewWorkspaceView",
"screenerView", "heavenView", "reviewWorkspaceView",
];
let reference = null;
for (const view of views) {
@@ -589,6 +641,7 @@ test("manual refresh stays in place without reopening the full-page loader", asy
await page.goto("/index.html");
await expect(page.locator("#loadingOverlay")).toBeHidden();
await openHeaderCommandMenu(page);
await page.locator("#refreshButton").click();
await expect(page.locator("#refreshButton")).toBeDisabled();
@@ -2805,54 +2858,122 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
await page.goto("/index.html");
await page.locator('[data-view="mentorView"]').first().click();
await expect(page.locator("#mentorView .mentor-sidebar .mentor-evidence-filters")).toBeVisible();
await expect(page.locator("#mentorView .mentor-sidebar .mentor-directory-tools")).toBeVisible();
await expect(page.locator("#mentorFilterToggle")).toBeVisible();
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
await expect(page.locator('#mentorList [data-mentor-id="private-owner"] .mentor-badge.private')).toContainText("仅自己");
await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A");
await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A");
await expect(page.locator('#mentorList [data-mentor-id="source-c"] .mentor-badge.quality')).toHaveCount(0);
await page.locator("#mentorSearchInput").fill("行为推演");
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(1);
await expect(page.locator("#mentorCount")).toHaveText("1 / 22 位");
await page.locator("#mentorSearchInput").fill("");
await page.locator("#mentorFilterToggle").click();
await page.locator('[data-mentor-grade="B"]').click();
await expect(page.locator("#mentorFilterOptions [data-mentor-grade].active")).toHaveText("B级");
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7);
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
await expect(page.locator("#activeMentorName")).toHaveText("多源老师");
await expect(page.locator("#activeMentorBadges")).toHaveText("B");
await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
await expect(page.locator("#activeMentorStatus")).toContainText("确认之后再行动");
const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();
const mentorProfile = await page.locator("#mentorView .mentor-profile-panel").boundingBox();
const mentorInput = await page.locator("#mentorQuestion").boundingBox();
expect(Math.abs(mentorLibrary.width - 280)).toBeLessThanOrEqual(1);
expect(Math.abs(mentorProfile.width - 272)).toBeLessThanOrEqual(1);
const mentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
expect(Math.abs(mentorLibrary.width - 300)).toBeLessThanOrEqual(1);
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
expect(Math.abs(mentorProfile.x - (mentorChat.x + mentorChat.width))).toBeLessThanOrEqual(1);
expect(mentorInput.height).toBeGreaterThanOrEqual(48);
expect(Math.abs(mentorChat.x + mentorChat.width - (mentorLayout.x + mentorLayout.width))).toBeLessThanOrEqual(1);
const composerForm = await page.locator("#mentorView .mentor-chat-form").boundingBox();
expect(composerForm.height).toBeGreaterThanOrEqual(81);
expect(composerForm.height).toBeLessThanOrEqual(91);
expect(mentorInput.height).toBeGreaterThanOrEqual(24);
expect(mentorInput.height).toBeLessThanOrEqual(34);
const searchField = await page.locator("#mentorView .mentor-search-field").boundingBox();
expect(searchField.width).toBeGreaterThan(160);
await expect(page.locator("#mentorSearchInput")).toHaveAttribute("placeholder", "搜索联系人或标签");
await expect(page.locator(".module-nav")).toBeVisible();
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "mentorView");
await expect(page.locator(".market-tape")).toBeVisible();
await expect(page.locator("#tradeDate")).toBeVisible();
await expect(page.locator(".overview-strip")).toBeVisible();
await expect(page.locator(".status-bar")).toBeVisible();
await expect(page.locator("#themeToggle")).toBeVisible();
await expect(page.locator("#mentorView .mentor-page-title h2")).toHaveText("问师");
await expect(page.locator("#mentorPageSubtitle")).toContainText("数据日期");
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
await page.setViewportSize({ width: 1920, height: 947 });
const expandedMentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
const statusBar = await page.locator(".status-bar").boundingBox();
const lowerGap = statusBar.y - (expandedMentorLayout.y + expandedMentorLayout.height);
expect(lowerGap).toBeGreaterThanOrEqual(0);
expect(lowerGap).toBeLessThanOrEqual(16);
await page.locator("#overviewToggle").click();
const openOverviewLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
const openOverviewGap = statusBar.y - (openOverviewLayout.y + openOverviewLayout.height);
expect(openOverviewGap).toBeGreaterThanOrEqual(0);
expect(openOverviewGap).toBeLessThanOrEqual(16);
const footerBar = await page.locator("#mentorView .mentor-workspace-footer").boundingBox();
const shellStatusBar = await page.locator(".status-bar").boundingBox();
expect(Math.abs(shellStatusBar.y + shellStatusBar.height - 947)).toBeLessThanOrEqual(1);
expect(footerBar.y + footerBar.height).toBeLessThanOrEqual(shellStatusBar.y);
expect(Math.abs(expandedMentorLayout.y + expandedMentorLayout.height - footerBar.y)).toBeLessThanOrEqual(1);
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
});
test("mentor stays inside the project shell and switching pages leaves no residue", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
await page.locator('[data-view="mentorView"]').first().click();
await expect(page.locator("#mentorView")).toHaveClass(/active-view/);
await expect(page.locator(".module-nav")).toBeVisible();
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "mentorView");
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
await expect(page.locator("#mentorView .mentor-page-title h2")).toHaveText("问师");
await page.locator('[data-view="limitPool"]').first().click();
await expect(page.locator("#limitPool")).toHaveClass(/active-view/);
await expect(page.locator("#mentorView")).not.toHaveClass(/active-view/);
await expect(page.locator("#mentorView")).not.toBeVisible();
await expect(page.locator(".module-nav")).toBeVisible();
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "limitPool");
await page.locator('[data-view="mentorView"]').first().click();
await expect(page.locator("#mentorView")).toHaveClass(/active-view/);
await expect(page.locator(".module-nav")).toBeVisible();
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "mentorView");
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
await expect(page.locator("#mentorPageSubtitle")).toContainText("数据日期");
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
});
test("mentor keeps two columns and opens the profile floating dialog on desktop", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
await page.locator('[data-view="mentorView"]').first().click();
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
await expect(page.locator("#mentorView .mentor-profile-panel")).toHaveCount(0);
const mentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
expect(Math.abs(mentorChat.x + mentorChat.width - (mentorLayout.x + mentorLayout.width))).toBeLessThanOrEqual(1);
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
await page.locator("#mentorProfileButton").click();
await expect(page.locator("#mentorProfileDialog")).toBeVisible();
await expect(page.locator("#mentorProfileDialogName")).toHaveText("多源老师");
await expect(page.locator("#mentorProfileDialogEvidence")).toHaveText("公开访谈与多源材料");
await page.locator('[data-mentor-dialog-close="mentorProfileDialog"]').click();
await expect(page.locator("#mentorProfileDialog")).not.toBeVisible();
});
test("mentor pins, custom order and streamed replies work together", async ({ page }) => {
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
await page.locator('[data-view="mentorView"]').first().click();
await page.locator('[data-mentor-pin="source-c"]').click();
await page.locator('[data-mentor-id="source-c"]').click();
await page.locator("#mentorPinButton").click();
await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-c");
await page.locator('[data-mentor-pin="source-b"]').click();
await expect(page.locator('#mentorList [data-mentor-card="source-c"] .mentor-badge.pinned')).toHaveText("置顶");
await page.locator('[data-mentor-id="source-b"]').click();
await page.locator("#mentorPinButton").click();
await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-b");
await page.locator("#mentorSortToggle").click();
@@ -2869,10 +2990,16 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
await expect(page.locator("#mentorMessages .assistant-stream-caret")).toHaveCount(0);
const followUps = answer.locator("[data-mentor-follow-up]");
await expect(followUps).toHaveCount(3);
await expect(followUps.first().locator(".lucide")).toHaveCount(0);
await expect(answer.locator("small")).toHaveCount(0);
const messageCount = await page.locator("#mentorMessages .mentor-message").count();
await followUps.first().click();
await expect(page.locator("#mentorQuestion")).toHaveValue("哪些信号代表确认?");
await expect(page.locator("#mentorMessages .mentor-message")).toHaveCount(messageCount);
const userLabel = page.locator("#mentorMessages .mentor-message.user .mentor-message-label").first();
await expect(userLabel).not.toContainText("我 ·");
const assistantBody = await page.locator("#mentorMessages .mentor-message.assistant .mentor-message-body").last().boundingBox();
expect(assistantBody.width).toBeLessThanOrEqual(900);
await page.locator("#themeToggle").click();
const userMessage = page.locator("#mentorMessages .mentor-message.user");
const darkUserMessageStyle = await userMessage.evaluate((element) => ({
@@ -2883,14 +3010,13 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
}));
expect(darkUserMessageStyle.background).not.toBe("rgb(255, 255, 255)");
expect(darkUserMessageStyle.border).not.toBe("rgb(255, 255, 255)");
expect(darkUserMessageStyle.contentBackground).toBe("rgb(35, 54, 74)");
expect(darkUserMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
expect(darkUserMessageStyle.contentBackground).toBe("rgb(53, 89, 140)");
expect(darkUserMessageStyle.contentColor).toBe("rgb(234, 241, 251)");
const darkMessageStyle = await answer.evaluate((element) => {
const style = getComputedStyle(element);
const content = element.querySelector(".mentor-message-content");
const heading = element.querySelector(".mentor-answer-heading");
const label = element.querySelector(".mentor-message-label");
const meta = element.querySelector("small");
return {
background: style.backgroundColor,
border: style.borderTopColor,
@@ -2898,7 +3024,6 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
contentColor: getComputedStyle(content).color,
headingColor: getComputedStyle(heading).color,
labelColor: getComputedStyle(label).color,
metaColor: getComputedStyle(meta).color,
};
});
expect(darkMessageStyle.background).not.toBe("rgb(255, 255, 255)");
@@ -2906,8 +3031,119 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
expect(darkMessageStyle.shadow).toBe("none");
expect(darkMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
expect(darkMessageStyle.headingColor).toBe("rgb(232, 234, 237)");
expect(darkMessageStyle.labelColor).toBe("rgb(127, 137, 147)");
expect(darkMessageStyle.metaColor).toBe("rgb(127, 137, 147)");
expect(darkMessageStyle.labelColor).toBe("rgb(124, 130, 138)");
});
test("mentor avatars map per contact id and follow the final day/night palette", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
const mentors = [
{
id: "xiaobai-perspective", name: "小白", description: "个人复盘记录蒸馏", tagline: "复盘自己",
focus: ["个人复盘"], evidence: { grade: "A", label: "私有原始语料", note: "私人语料" }, quality: {}, private: true,
},
{
id: "kobe92-perspective", name: "52科比", description: "情绪周期心法", tagline: "先看周期",
focus: ["情绪周期"], evidence: { grade: "A", label: "心法文本", note: "心法" }, quality: {}, private: false,
},
{
id: "beijingchaojia-perspective", name: "北京炒家", description: "实盘记录", tagline: "实盘为先",
focus: ["实盘"], evidence: { grade: "A", label: "实盘资料", note: "实盘" }, quality: {}, private: false,
},
{
id: "chaojiyangjia-perspective", name: "炒股养家", description: "原始语料", tagline: "情绪为上",
focus: ["情绪"], evidence: { grade: "B", label: "原始语料", note: "语料" }, quality: {}, private: false,
},
];
await mockApplication(page, session("admin", true), { mentors });
await page.goto("/index.html");
await page.locator('[data-view="mentorView"]').first().click();
const expectations = {
"xiaobai-perspective": ["mentor-avatar-tone-violet", "rgb(238, 236, 253)", "rgb(106, 92, 245)"],
"kobe92-perspective": ["mentor-avatar-tone-blue", "rgb(227, 240, 255)", "rgb(51, 112, 255)"],
"beijingchaojia-perspective": ["mentor-avatar-tone-green", "rgb(221, 245, 229)", "rgb(46, 164, 79)"],
"chaojiyangjia-perspective": ["mentor-avatar-tone-orange", "rgb(253, 238, 221)", "rgb(217, 122, 27)"],
};
for (const [id, [tone, background, ink]] of Object.entries(expectations)) {
const avatar = page.locator(`#mentorList [data-mentor-id="${id}"] .mentor-avatar`);
await expect(avatar).toHaveClass(new RegExp(tone));
const palette = await avatar.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, color: style.color };
});
expect(palette.background).toBe(background);
expect(palette.color).toBe(ink);
}
await page.locator('[data-mentor-id="kobe92-perspective"]').click();
await expect(page.locator("#activeMentorAvatar")).toHaveClass(/mentor-avatar-tone-blue/);
await page.locator("#themeToggle").click();
const nightPalette = await page.locator('#mentorList [data-mentor-id="beijingchaojia-perspective"] .mentor-avatar')
.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, color: style.color };
});
expect(nightPalette.background).toBe("rgb(35, 74, 56)");
expect(nightPalette.color).toBe("rgb(95, 206, 143)");
await expect(page.locator("#activeMentorAvatar")).toHaveClass(/mentor-avatar-tone-blue/);
});
test("mentor floating dialogs open centered, save notes per account, and keep composer one-line", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
await page.locator('[data-view="mentorView"]').first().click();
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
await page.locator("#mentorNoteButton").click();
await expect(page.locator("#mentorNoteDialog")).toBeVisible();
const noteDialog = await page.locator("#mentorNoteDialog").boundingBox();
expect(noteDialog.width).toBeLessThanOrEqual(400);
expect(Math.abs(noteDialog.x + noteDialog.width / 2 - 720)).toBeLessThanOrEqual(3);
expect(Math.abs(noteDialog.y + noteDialog.height / 2 - 450)).toBeLessThanOrEqual(3);
await page.locator("#mentorNoteInput").fill("多源老师:确认后再行动");
await expect(page.locator("#mentorNoteInput")).toHaveValue("多源老师:确认后再行动");
await page.locator('[data-mentor-dialog-close="mentorNoteDialog"]').click();
await expect(page.locator("#mentorNoteDialog")).not.toBeVisible();
await page.locator("#mentorNoteButton").click();
await expect(page.locator("#mentorNoteInput")).toHaveValue("多源老师:确认后再行动");
await page.locator('[data-mentor-dialog-close="mentorNoteDialog"]').click();
await page.locator("#mentorProfileButton").click();
await expect(page.locator("#mentorProfileDialog")).toBeVisible();
await expect(page.locator("#mentorProfileDialogName")).toHaveText("多源老师");
await expect(page.locator("#mentorProfileDialogEvidence")).toHaveText("公开访谈与多源材料");
const profileDialog = await page.locator("#mentorProfileDialog").boundingBox();
expect(profileDialog.width).toBeLessThanOrEqual(400);
expect(Math.abs(profileDialog.x + profileDialog.width / 2 - 720)).toBeLessThanOrEqual(3);
await page.locator('[data-mentor-dialog-close="mentorProfileDialog"]').click();
await expect(page.locator("#mentorProfileDialog")).not.toBeVisible();
const disclaimerStyle = await page.locator("#mentorView .mentor-disclaimer").evaluate((el) => ({
align: getComputedStyle(el).textAlign,
}));
expect(disclaimerStyle.align).toBe("center");
const composerHint = page.locator("#mentorView .mentor-composer-hint");
await expect(composerHint).toHaveCount(1);
await expect(composerHint).toBeVisible();
const composerInput = page.locator("#mentorQuestion");
const oneLineHeight = (await composerInput.boundingBox()).height;
expect(oneLineHeight).toBeGreaterThanOrEqual(24);
expect(oneLineHeight).toBeLessThanOrEqual(34);
await composerInput.fill("第一行\n第二行");
const grownHeight = (await composerInput.boundingBox()).height;
expect(grownHeight).toBeGreaterThan(oneLineHeight + 8);
await composerInput.press("Shift+Enter");
await composerInput.type("第三行");
await composerInput.fill("只发送这一行");
await page.locator("#sendMentorQuestion").click();
await expect(page.locator("#mentorMessages .mentor-message.user").last()).toContainText("只发送这一行");
const resetHeight = (await composerInput.boundingBox()).height;
expect(resetHeight).toBeLessThanOrEqual(34);
});
test("global dialogs share the stage 18 geometry without changing account or admin access", async ({ page }) => {
@@ -2951,6 +3187,7 @@ test("global dialogs share the stage 18 geometry without changing account or adm
expect(assistantGeometry.contentDisplay).toBe("flex");
await page.locator("#closeAssistantDialog").click();
await openHeaderCommandMenu(page);
await page.locator("#accountButton").click();
await page.locator('[data-account-panel="membership"]').click();
await expect(page.locator("#settingsDialog")).toHaveAttribute("aria-labelledby", "accountDialogTitle");
@@ -2961,6 +3198,7 @@ test("global dialogs share the stage 18 geometry without changing account or adm
expect(Math.abs(accountBox.y + accountBox.height / 2 - 450)).toBeLessThanOrEqual(2);
await page.locator("#closeSettingsDialog").click();
await openHeaderCommandMenu(page);
await page.locator("#settingsButton").click();
await expect(page.locator("#adminDialog")).toHaveAttribute("aria-labelledby", "adminDialogTitle");
await expect(page.locator("#adminSectionSelect")).toBeVisible();
@@ -2980,3 +3218,519 @@ test("global dialogs share the stage 18 geometry without changing account or adm
expect(mobileGeometry.width).toBeLessThanOrEqual(375);
expect(mobileGeometry.documentOverflow).toBeLessThanOrEqual(1);
});
test("merged header keeps date, detail fields and 13 pool columns after dual-review rework", async ({ page }) => {
const previousLimits = dashboard.limits;
dashboard.limits = [{
code: "002141",
name: "贤丰控股",
streak: 5,
change: 10.02,
price: 12.48,
sector: "电子元件",
first_time: "09:31",
last_time: "10:18",
open_times: 1,
turnover_rate: 18.42,
amount_billion: 12.6,
seal_amount_million: 8200,
reason: "板块龙头连板打开空间",
}];
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
dashboard.limits = previousLimits;
await page.waitForTimeout(400);
await expect(page.locator("#currentPageSubtitle")).toContainText("市场复盘 ·");
await expect(page.locator("#headerMenuButton")).toBeHidden();
await expect(page.locator("#headerCommandGroup")).toBeVisible();
await expect(page.locator("#themeToggle")).toBeVisible();
await expect(page.locator("#refreshButton")).toBeVisible();
await page.locator('[data-view="limitPool"]').first().click();
await expect(page.locator("#currentPageSubtitle")).toContainText("市场复盘 ·");
await expect(page.locator("#currentPageSubtitle")).not.toHaveText("市场复盘");
await page.setViewportSize({ width: 1280, height: 800 });
await expect(page.locator(".tape-optional").first()).toBeHidden();
await page.locator("#overviewToggle").click();
const detailText = await page.locator(".tape-detail").innerText();
for (const label of ["市场情绪", "上涨家数", "下跌家数", "涨停", "跌停", "炸板", "封板率", "两市成交", "数据日期"]) {
expect(detailText).toContain(label);
}
expect(detailText).not.toContain("涨停 / 跌停");
await expect(page.locator("#tapeLimitDown")).toBeVisible();
await expect(page.locator("#detailBroken")).toBeVisible();
await expect(page.locator("#dataDateMetric")).toBeVisible();
await expect(page.locator(".tape-detail-date")).toBeVisible();
expect(await page.locator("#dataDateMetric").evaluate((node) => Boolean(node.closest(".metric-wide")))).toBe(false);
await page.keyboard.press("Escape");
const measurePoolTable = () => {
const wrap = document.querySelector("#limitPool .pool-table-card");
const table = document.querySelector("#limitTable");
const reason = document.querySelector("#limitTable .reason-column");
const reasonCell = document.querySelector("#limitTable .pool-reason-cell");
const headerCell = document.querySelector("#limitPool .data-table thead th");
const rowCell = document.querySelector("#limitPool .data-table tbody td");
const cells = [...document.querySelectorAll("#limitTable tbody td")];
const numberCells = [...document.querySelectorAll("#limitTable tbody td.num, #limitTable tbody td.number")];
const header = document.querySelector(".app-header");
const headerStyle = headerCell ? getComputedStyle(headerCell) : null;
const rowStyle = rowCell ? getComputedStyle(rowCell) : null;
const widths = cells.map((cell) => cell.getBoundingClientRect().width);
const numberWidths = numberCells.map((cell) => cell.getBoundingClientRect().width);
return {
tableLayout: table ? getComputedStyle(table).tableLayout : "",
columnCount: document.querySelectorAll("#limitTable thead th").length,
minCellWidth: widths.length ? Math.min(...widths) : 0,
minNumberWidth: numberWidths.length ? Math.min(...numberWidths) : 0,
wrapOverflow: wrap ? wrap.scrollWidth - wrap.clientWidth : 0,
tableWidth: table ? table.getBoundingClientRect().width : 0,
wrapWidth: wrap ? wrap.getBoundingClientRect().width : 0,
cardOverflow: wrap ? wrap.scrollWidth - wrap.clientWidth : 0,
reasonVisible: Boolean(reason && wrap && reason.getBoundingClientRect().right <= wrap.getBoundingClientRect().right + 1),
reasonText: reason ? reason.textContent.trim() : "",
reasonTitle: reasonCell?.getAttribute("title") || "",
headerOverflow: header ? header.scrollWidth - header.clientWidth : 0,
headerHeight: headerStyle ? Number.parseFloat(headerStyle.height) : 0,
headerFont: headerStyle ? Number.parseFloat(headerStyle.fontSize) : 0,
rowHeight: rowStyle ? Number.parseFloat(rowStyle.height) : 0,
};
};
const pool1280 = await page.evaluate(measurePoolTable);
expect(pool1280.columnCount).toBe(13);
expect(pool1280.tableLayout).toBe("auto");
expect(pool1280.minCellWidth).toBeGreaterThanOrEqual(40);
expect(pool1280.minNumberWidth).toBeGreaterThanOrEqual(50);
expect(pool1280.wrapOverflow).toBeGreaterThan(0);
expect(pool1280.reasonTitle).toContain("板块龙头连板打开空间");
expect(pool1280.headerHeight).toBe(36);
expect(pool1280.rowHeight).toBeGreaterThanOrEqual(40);
await page.setViewportSize({ width: 1600, height: 1000 });
await page.locator('[data-view="limitPool"]').first().click();
const tableFit = await page.evaluate(measurePoolTable);
expect(tableFit.reasonText).toContain("涨停原因");
expect(tableFit.reasonVisible).toBe(true);
expect(tableFit.cardOverflow).toBeLessThanOrEqual(1);
expect(tableFit.headerOverflow).toBeLessThanOrEqual(1);
expect(tableFit.reasonTitle).toContain("板块龙头连板打开空间");
expect(tableFit.headerHeight).toBe(36);
expect(tableFit.headerFont).toBe(12.5);
expect(tableFit.rowHeight).toBeGreaterThanOrEqual(40);
expect(tableFit.tableLayout).toBe("fixed");
expect(tableFit.columnCount).toBe(13);
await page.locator('[data-view="heavenView"]').first().click();
await expect(page.locator(".app-header .overview-strip")).toBeHidden();
await expect(page.locator("#overviewToggle")).toBeHidden();
await page.setViewportSize({ width: 390, height: 844 });
await page.locator('[data-view="limitPool"]').first().click({ force: true });
const pool390 = await page.evaluate(measurePoolTable);
expect(pool390.columnCount).toBe(13);
expect(pool390.tableLayout).toBe("auto");
expect(pool390.minCellWidth).toBeGreaterThanOrEqual(40);
expect(pool390.minNumberWidth).toBeGreaterThanOrEqual(50);
expect(pool390.wrapOverflow).toBeGreaterThan(0);
expect(pool390.reasonTitle).toContain("板块龙头连板打开空间");
await page.locator("#mobileMarketViewSelect").selectOption("sentimentCycleView");
await page.locator("#overviewToggle").click();
const mobileDetail = await page.locator(".tape-detail").innerText();
for (const label of ["市场情绪", "上涨家数", "下跌家数", "涨停", "跌停", "炸板", "封板率", "两市成交", "数据日期"]) {
expect(mobileDetail).toContain(label);
}
await expect(page.locator("#dataDateMetric")).toBeVisible();
});
test("B-199 screener review and account surfaces fit day night viewports", async ({ page }) => {
const fs = require("node:fs");
const path = require("node:path");
const shotDir = path.join(__dirname, "../../runtime/b199-shots");
fs.mkdirSync(shotDir, { recursive: true });
const pageErrors = [];
page.on("pageerror", (error) => pageErrors.push(String(error)));
const overflowX = () => page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
const shot = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`), fullPage: true });
await mockApplication(page, session("admin", true));
await page.setViewportSize({ width: 1600, height: 1000 });
await page.goto("/index.html");
await page.locator('[data-view="screenerView"]').first().click();
await expect(page.locator("#screenerView")).toHaveClass(/active-view/);
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("screener-1600-day");
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
await expect(page.locator("#reviewWorkspaceView")).toHaveClass(/active-view/);
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("review-1600-day");
await openHeaderCommandMenu(page);
await page.locator("#accountButton").click();
await page.locator('[data-account-panel="membership"]').click();
await expect(page.locator("#settingsDialog")).toBeVisible();
await shot("account-1600-day");
await page.locator("#closeSettingsDialog").click();
await openHeaderCommandMenu(page);
await page.locator("#settingsButton").click();
await expect(page.locator("#adminDialog")).toBeVisible();
await shot("admin-1600-day");
await page.locator("#closeAdminDialog").click();
await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await page.locator('[data-view="screenerView"]').first().click();
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("screener-1600-night");
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("review-1600-night");
await page.setViewportSize({ width: 1280, height: 800 });
await page.locator('[data-view="screenerView"]').first().click();
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("screener-1280-night");
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("review-1280-night");
await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await page.locator('[data-view="screenerView"]').first().click();
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("screener-1280-day");
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("review-1280-day");
await page.locator('[data-view="screenerView"]').first().click();
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.locator("#screenerView")).toHaveClass(/active-view/);
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("screener-390-day");
await page.locator(".mobile-primary-tab[data-view=\"reviewWorkspaceView\"]").click();
await expect(page.locator("#reviewWorkspaceView")).toHaveClass(/active-view/);
expect(await overflowX()).toBeLessThanOrEqual(1);
await shot("review-390-day");
expect(pageErrors).toEqual([]);
});
test("desktop header keeps refresh, admin commands and account identity visible", async ({ page }) => {
const fs = require("node:fs");
const path = require("node:path");
const shotDir = path.join(__dirname, "../../runtime/b214-shots");
fs.mkdirSync(shotDir, { recursive: true });
const pageErrors = [];
page.on("pageerror", (error) => pageErrors.push(String(error)));
await mockApplication(page, session("admin", true));
await page.goto("/index.html");
const shot = (name) => page.locator(".app-header").screenshot({ path: path.join(shotDir, `${name}.png`) });
const shotPage = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`) });
const measureHeader = () => page.evaluate(() => {
const header = document.querySelector(".app-header");
const tape = document.querySelector(".market-tape");
const actions = document.querySelector(".header-actions");
const refresh = document.querySelector("#refreshButton");
const account = document.querySelector("#accountButton");
const name = document.querySelector("#accountName");
const box = (node) => node ? node.getBoundingClientRect() : null;
const visible = (node) => {
if (!node || node.hidden) return false;
const style = getComputedStyle(node);
if (style.display === "none" || style.visibility === "hidden") return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const tapeBox = box(tape);
const actionsBox = box(actions);
return {
headerOverflow: header ? header.scrollWidth - header.clientWidth : 0,
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
tapeHeight: tapeBox ? tapeBox.height : 0,
overlap: tapeBox && actionsBox
? Math.max(0, Math.min(tapeBox.right, actionsBox.right) - Math.max(tapeBox.left, actionsBox.left))
* Math.max(0, Math.min(tapeBox.bottom, actionsBox.bottom) - Math.max(tapeBox.top, actionsBox.top))
: 0,
refreshVisible: visible(refresh),
accountVisible: visible(account),
nameVisible: visible(name),
nameFits: name ? name.clientWidth >= name.scrollWidth : false,
};
});
for (const viewport of [
{ width: 1600, height: 1000 },
{ width: 1440, height: 900 },
{ width: 1280, height: 800 },
]) {
await page.setViewportSize(viewport);
await expect(page.locator("#headerMenuButton")).toBeHidden();
await expect(page.locator("#headerCommandGroup")).toBeVisible();
await expect(page.locator("#refreshButton")).toBeVisible();
await expect(page.locator("#syncButton")).toBeVisible();
await expect(page.locator("#settingsButton")).toBeVisible();
await expect(page.locator("#accountAdminBadge")).toBeVisible();
await expect(page.locator("#accountVipBadge")).toBeVisible();
await expect(page.locator("#accountButton")).toBeVisible();
await expect(page.locator("#accountName")).toBeVisible();
const geometry = await measureHeader();
expect(geometry.headerOverflow, `${viewport.width} header overflow`).toBeLessThanOrEqual(1);
expect(geometry.documentOverflow, `${viewport.width} document overflow`).toBeLessThanOrEqual(1);
expect(geometry.overlap, `${viewport.width} tape overlap`).toBe(0);
expect(geometry.tapeHeight, `${viewport.width} tape height`).toBeGreaterThan(20);
expect(geometry.nameFits, `${viewport.width} account name truncated`).toBe(true);
await page.locator("#refreshButton").click();
await expect(page.locator("#loadingOverlay")).toBeHidden();
await page.locator("#settingsButton").click();
await expect(page.locator("#adminDialog")).toBeVisible();
await page.locator("#closeAdminDialog").click();
await page.locator("#accountButton").click();
await expect(page.locator("#accountDropdown")).toBeVisible();
await page.keyboard.press("Escape");
await shot(`${viewport.width}-day`);
}
await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await page.setViewportSize({ width: 1600, height: 1000 });
await shot("1600-night");
await page.locator("#themeToggle").click();
await page.locator('[data-view="heavenView"]').first().click();
await expect(page.locator("#heavenView")).toHaveClass(/active-view/);
await expect(page.locator("#refreshButton")).toBeVisible();
await expect(page.locator("#accountButton")).toBeVisible();
await expect(page.locator("#headerMenuButton")).toBeHidden();
await shot("heaven-1600-day");
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.locator("#headerMenuButton")).toBeVisible();
await expect(page.locator("#refreshButton")).toBeHidden();
await page.locator("#headerMenuButton").click();
await expect(page.locator("#headerCommandGroup")).toBeVisible();
await expect(page.locator("#refreshButton")).toBeVisible();
await expect(page.locator("#syncButton")).toBeVisible();
await expect(page.locator("#settingsButton")).toBeVisible();
await expect(page.locator("#accountAdminBadge")).toBeVisible();
await expect(page.locator("#accountVipBadge")).toBeVisible();
await expect(page.locator("#accountButton")).toBeVisible();
await expect(page.locator('[data-mobile-command-target="themeToggle"]')).toBeVisible();
const mobileName = await page.evaluate(measureAccountName);
expect(mobileName.fits, "390 account name truncated").toBe(true);
expect(mobileName.stacked, "390 identity and account should stack").toBe(true);
await shotPage("390-menu-open");
await page.keyboard.press("Escape");
await expect(page.locator("#headerCommandGroup")).toBeHidden();
await mockApplication(page, session("user", false));
await page.setViewportSize({ width: 1600, height: 1000 });
await page.goto("/index.html");
await expect(page.locator("#refreshButton")).toBeVisible();
await expect(page.locator("#syncButton")).toBeHidden();
await expect(page.locator("#settingsButton")).toBeHidden();
await expect(page.locator("#accountAdminBadge")).toBeHidden();
await expect(page.locator("#accountVipBadge")).toBeVisible();
await expect(page.locator("#accountName")).toHaveText("normal_user");
await shot("1600-user-day");
expect(pageErrors).toEqual([]);
});
test("header username stays untruncated on desktop and in the mobile menu", async ({ page }) => {
test.setTimeout(120_000);
const fs = require("node:fs");
const path = require("node:path");
const shotDir = path.join(__dirname, "../../runtime/b217-shots");
fs.mkdirSync(shotDir, { recursive: true });
const measurements = [];
const viewports = [
{ width: 1600, height: 1000 },
{ width: 1440, height: 900 },
{ width: 1280, height: 800 },
{ width: 390, height: 844 },
];
const accounts = [
{ label: "admin", username: "review_admin", auth: namedSession("review_admin", "admin", true) },
{ label: "user", username: "normal_user", auth: namedSession("normal_user", "user", false) },
];
for (const account of accounts) {
await mockApplication(page, account.auth);
await page.goto("/index.html");
await expect(page.locator("#accountName")).toHaveText(account.username);
for (const theme of ["day", "night"]) {
await page.setViewportSize({ width: 1600, height: 1000 });
await setColorTheme(page, theme);
for (const viewport of viewports) {
await page.setViewportSize(viewport);
if (viewport.width === 390) {
await page.locator("#headerMenuButton").click();
await expect(page.locator("#headerCommandGroup")).toBeVisible();
} else {
await expect(page.locator("#headerMenuButton")).toBeHidden();
await expect(page.locator("#accountName")).toBeVisible();
}
const geometry = await page.evaluate(measureAccountName);
const label = `${account.label}-${viewport.width}-${theme}`;
expect(geometry.text, `${label} username`).toBe(account.username);
expect(geometry.fits, `${label} accountName truncated ${geometry.clientWidth}<${geometry.scrollWidth}`).toBe(true);
expect(geometry.documentOverflow, `${label} document overflow`).toBeLessThanOrEqual(1);
if (viewport.width === 390) {
expect(geometry.stacked, `${label} identity/account not stacked`).toBe(true);
}
if (viewport.width >= 1440 && account.label === "admin") {
expect(geometry.adminLabelVisible, `${label} admin badge text hidden`).toBe(true);
expect(geometry.vipLabelVisible, `${label} vip badge text hidden`).toBe(true);
}
measurements.push({
account: account.username,
theme,
width: viewport.width,
height: viewport.height,
...geometry,
});
const shotName = `${account.label}-${viewport.width}-${theme}.png`;
if (viewport.width === 390) {
await page.screenshot({ path: path.join(shotDir, shotName) });
await page.keyboard.press("Escape");
await expect(page.locator("#headerCommandGroup")).toBeHidden();
} else {
await page.locator(".app-header").screenshot({ path: path.join(shotDir, shotName) });
}
}
}
}
fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`);
});
function measureHeaderProbe() {
const name = document.querySelector("#accountName");
const group = document.querySelector("#headerCommandGroup");
const strip = document.querySelector(".app-header .overview-strip");
const stripBox = strip ? strip.getBoundingClientRect() : null;
const stripShown = Boolean(stripBox && stripBox.width > 0 && stripBox.height > 0);
const tapeClip = stripShown
? [...strip.querySelectorAll("*")].filter((el) => (
el.children.length === 0 && el.scrollWidth > el.clientWidth + 1
)).map((el) => (el.textContent || "").trim()).filter(Boolean)
: [];
const groupBox = group ? group.getBoundingClientRect() : null;
const nameBox = name ? name.getBoundingClientRect() : null;
return {
groupRight: groupBox ? groupBox.right : 0,
nameRight: nameBox ? nameBox.right : 0,
nameFits: name ? name.clientWidth >= name.scrollWidth : false,
nameText: name ? String(name.textContent || "") : "",
tapeClip,
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
viewport: window.innerWidth,
};
}
async function applyColorTheme(page, theme) {
const wanted = theme === "night" ? "dark" : "light";
await page.evaluate((value) => {
document.documentElement.dataset.theme = value;
document.documentElement.style.colorScheme = value;
try { localStorage.setItem("xiaobaiTheme", value); } catch (_error) {}
}, wanted);
await expect(page.locator("html")).toHaveAttribute("data-theme", wanted);
}
async function probeHeaderWorkspaces(page, { account, theme, viewport, views, measurements }) {
await page.setViewportSize(viewport);
await applyColorTheme(page, theme);
for (const view of views) {
await page.locator(`[data-view="${view}"]`).first().click();
await expect(page.locator(`#${view}`)).toHaveClass(/active-view/);
const geometry = await page.evaluate(measureHeaderProbe);
const label = `${account.label}-${view}-${viewport.width}-${theme}`;
expect(geometry.nameText, `${label} username`).toBe(account.username);
expect(geometry.nameFits, `${label} accountName truncated`).toBe(true);
expect(geometry.groupRight, `${label} command group overflow ${geometry.groupRight}>${geometry.viewport}`).toBeLessThanOrEqual(geometry.viewport + 0.5);
expect(geometry.nameRight, `${label} accountName overflow ${geometry.nameRight}>${geometry.viewport}`).toBeLessThanOrEqual(geometry.viewport + 0.5);
expect(geometry.documentOverflow, `${label} document overflow`).toBeLessThanOrEqual(1);
expect(geometry.tapeClip, `${label} tape clipped ${geometry.tapeClip.join("|")}`).toEqual([]);
measurements.push({ account: account.username, view, theme, width: viewport.width, ...geometry });
}
}
test("desktop header keeps commands in view and tape text unclipped across workspaces", async ({ page }) => {
test.setTimeout(120_000);
const fs = require("node:fs");
const path = require("node:path");
const shotDir = path.join(__dirname, "../../runtime/b221-shots");
fs.mkdirSync(shotDir, { recursive: true });
const measurements = [];
const views = [
"sentimentCycleView",
"auctionView",
"themeLibraryView",
"popularityView",
"rotationView",
"mentorView",
"screenerView",
"reviewWorkspaceView",
"heavenView",
];
const viewports = [
{ width: 1600, height: 1000 },
{ width: 1440, height: 900 },
{ width: 1280, height: 800 },
];
const accounts = [
{ label: "admin", username: "review_admin", auth: namedSession("review_admin", "admin", true) },
{ label: "user", username: "normal_user", auth: namedSession("normal_user", "user", false) },
];
for (const account of accounts) {
await mockApplication(page, account.auth);
await page.goto("/index.html");
await expect(page.locator("#accountName")).toHaveText(account.username);
for (const viewport of viewports) {
await probeHeaderWorkspaces(page, { account, theme: "day", viewport, views, measurements });
await page.locator('[data-view="themeLibraryView"]').first().click();
await page.locator(".app-header").screenshot({
path: path.join(shotDir, `${account.label}-${viewport.width}-day.png`),
});
}
await probeHeaderWorkspaces(page, {
account,
theme: "night",
viewport: { width: 1600, height: 1000 },
views,
measurements,
});
await page.locator('[data-view="sentimentCycleView"]').first().click();
await page.locator(".app-header").screenshot({
path: path.join(shotDir, `${account.label}-1600-night.png`),
});
}
await mockApplication(page, accounts[0].auth);
await page.goto("/index.html");
await page.setViewportSize({ width: 390, height: 844 });
await applyColorTheme(page, "day");
await page.locator("#headerMenuButton").click();
await expect(page.locator("#headerCommandGroup")).toBeVisible();
const mobileDay = await page.evaluate(measureAccountName);
expect(mobileDay.fits, "390 day account name truncated").toBe(true);
expect(mobileDay.stacked, "390 day identity and account should stack").toBe(true);
await page.screenshot({ path: path.join(shotDir, "admin-390-day.png") });
await page.keyboard.press("Escape");
await applyColorTheme(page, "night");
await page.locator("#headerMenuButton").click();
const mobileNight = await page.evaluate(measureAccountName);
expect(mobileNight.fits, "390 night account name truncated").toBe(true);
expect(mobileNight.stacked, "390 night identity and account should stack").toBe(true);
await page.screenshot({ path: path.join(shotDir, "admin-390-night.png") });
fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`);
});
+81 -9
View File
@@ -18,20 +18,92 @@ class DatabaseMigrationTests(unittest.TestCase):
rows = connection.execute(
"SELECT version, name FROM schema_migrations"
).fetchall()
self.assertEqual(
[(row["version"], row["name"]) for row in rows],
[
("0001", "adopt_legacy_schema"),
("0002", "create_job_runs"),
("0003", "extend_llm_audit"),
],
)
self.assertEqual(
[(row["version"], row["name"]) for row in rows],
[
("0001", "adopt_legacy_schema"),
("0002", "create_job_runs"),
("0003", "extend_llm_audit"),
("0004", "add_mentor_note"),
],
)
columns = {
str(row["name"])
for row in connection.execute(
"PRAGMA table_info(mentor_preferences)"
)
}
self.assertIn("note", columns)
ReviewDatabase(path)
with database.connect() as connection:
count = connection.execute(
"SELECT COUNT(*) AS count FROM schema_migrations"
).fetchone()["count"]
self.assertEqual(count, 3)
self.assertEqual(count, 4)
def test_database_with_recorded_0004_and_note_column_starts_without_reapply(
self,
) -> None:
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "review.db"
database = ReviewDatabase(path)
with database.connect() as connection:
note_rows = [
str(row["name"])
for row in connection.execute(
"PRAGMA table_info(mentor_preferences)"
)
]
self.assertIn("note", note_rows)
ReviewDatabase(path)
with database.connect() as connection:
count = connection.execute(
"SELECT COUNT(*) AS count FROM schema_migrations"
).fetchone()["count"]
self.assertEqual(count, 4)
def test_old_database_without_0004_upgrades_and_adds_note_column(self) -> None:
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "review.db"
database = ReviewDatabase(path)
with database.connect() as connection:
connection.execute(
"DELETE FROM schema_migrations WHERE version = '0004'"
)
connection.execute(
"ALTER TABLE mentor_preferences DROP COLUMN note"
)
ReviewDatabase(path)
with database.connect() as connection:
versions = {
str(row["version"])
for row in connection.execute(
"SELECT version FROM schema_migrations"
)
}
note_rows = [
str(row["name"])
for row in connection.execute(
"PRAGMA table_info(mentor_preferences)"
)
]
self.assertEqual(versions, {"0001", "0002", "0003", "0004"})
self.assertIn("note", note_rows)
def test_database_with_unknown_migration_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "review.db"
database = ReviewDatabase(path)
with database.connect() as connection:
connection.execute(
"""
INSERT INTO schema_migrations
(version, name, checksum, applied_at)
VALUES ('9999', 'unknown_legacy', 'x', '2026-08-01T00:00:00+00:00')
"""
)
with self.assertRaises(MigrationError):
ReviewDatabase(path)
def test_connection_factory_enables_required_pragmas(self) -> None:
with tempfile.TemporaryDirectory() as root:
+94 -12
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import re
import subprocess
import unittest
from html.parser import HTMLParser
from pathlib import Path
@@ -8,12 +9,35 @@ from pathlib import Path
from tests.frontend_test_helpers import (
assembled_frontend_runtime,
assembled_frontend_document,
registered_frontend_runtime_scripts,
)
STATIC_DIR = Path(__file__).resolve().parents[1] / "frontend"
def uncommitted_runtime_files() -> set[str]:
"""Relative frontend paths with pending working-tree edits.
The id-reference contract is validated against committed sources only.
Parallel agents may temporarily reference ids that their own (uncommitted)
HTML change reintroduces; those are resolved by the owning commit.
"""
result = subprocess.run(
["git", "-C", str(STATIC_DIR.parent), "diff", "--name-only", "HEAD"],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
return set()
return {
line.strip().removeprefix("app/frontend/")
for line in result.stdout.splitlines()
if line.strip().startswith("app/frontend/")
}
class IdCollector(HTMLParser):
def __init__(self) -> None:
super().__init__()
@@ -50,11 +74,23 @@ class FrontendContractTests(unittest.TestCase):
self.assertEqual(duplicates, [])
def test_literal_id_selectors_exist_in_html(self):
selectors = set(re.findall(r'querySelector\("#([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
selectors.update(re.findall(r'getElementById\("([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
selectors.update(re.findall(r'setText\("([A-Za-z][A-Za-z0-9_-]*)"', self.script))
missing = sorted(selectors - set(self.ids))
self.assertEqual(missing, [])
pending = uncommitted_runtime_files()
dangling: list[str] = []
for url in registered_frontend_runtime_scripts():
relative = url.split("?", 1)[0].lstrip("/")
if relative.startswith("vendor/"):
continue
if relative in pending:
continue
source = (STATIC_DIR / relative).read_text(encoding="utf-8")
selectors = set(re.findall(r'querySelector\("#([A-Za-z][A-Za-z0-9_-]*)"\)', source))
selectors.update(re.findall(r'getElementById\("([A-Za-z][A-Za-z0-9_-]*)"\)', source))
selectors.update(re.findall(r'setText\("([A-Za-z][A-Za-z0-9_-]*)"', source))
dangling.extend(
f"{relative}:{selector}"
for selector in sorted(selectors - set(self.ids))
)
self.assertEqual(dangling, [])
def test_all_primary_views_have_navigation_entries(self):
views = set(re.findall(r'id="([A-Za-z][A-Za-z0-9_-]*View|limitPool)" class="workspace-view', self.html))
@@ -130,6 +166,15 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn('id="accountDropdown"', self.html)
self.assertIn('id="settingsButton"', self.html)
def test_backfill_clears_sentiment_cache_and_reloads_sentiment_view(self):
start = self.script.index("async function backfillData")
end = self.script.index("async function openAdminSettings", start)
backfill = self.script[start:end]
self.assertIn("state.sentimentHistory = null;", backfill)
self.assertIn('state.sentimentHistoryKey = "";', backfill)
self.assertIn('if (state.activeView === "sentimentCycleView")', backfill)
self.assertIn("await loadSentimentHistory(true);", backfill)
def test_screener_uses_progressive_strategy_editor(self):
for step in ("regime", "strategy", "run", "result"):
self.assertIn(f'data-screener-step="{step}"', self.html)
@@ -280,13 +325,18 @@ class FrontendContractTests(unittest.TestCase):
self.assertNotIn('context.fillStyle = "#6c7983";', chart)
def test_dark_mentor_tokens_and_sentiment_bottom_clearance_are_defined(self):
self.assertIn(":root[data-theme=\"dark\"] #mentorView {", self.mentor_styles)
self.assertIn("--mentor-ink: var(--text-primary);", self.mentor_styles)
self.assertIn("--mentor-sub: var(--text-secondary);", self.mentor_styles)
self.assertIn(":root[data-theme=\"dark\"] #mentorView .mentor-message {", self.mentor_styles)
self.assertIn("border-color: var(--line-soft);", self.mentor_styles)
self.assertIn("background: var(--surface-subtle);", self.mentor_styles)
self.assertIn("box-shadow: none;", self.mentor_styles)
self.assertIn(":root[data-theme=\"dark\"] #mentorView.workspace-view {", self.mentor_styles)
self.assertIn("--qp-bg-chat: #232529;", self.mentor_styles)
self.assertIn("--qp-bg-bubble-self: #35598C;", self.mentor_styles)
self.assertIn("--qp-bg-selected: #2B3B58;", self.mentor_styles)
self.assertIn("--qp-link: #316FEF;", self.mentor_styles)
self.assertIn("--qp-accent-soft: #5B8DEF;", self.mentor_styles)
self.assertIn("--mentor-directory-width: 300px;", self.tokens)
self.assertIn("--mentor-composer-min-height: 85px;", self.tokens)
self.assertIn("#mentorView .mentor-message.user .mentor-message-content {", self.mentor_styles)
self.assertIn("background: var(--qp-bg-bubble-self);", self.mentor_styles)
self.assertIn("#mentorView .mentor-quick-prompts button {", self.mentor_styles)
self.assertIn("#mentorView .mentor-composer-hint {", self.mentor_styles)
self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.sentiment_styles)
self.assertIn("margin-bottom: var(--card-gap);", self.sentiment_styles)
self.assertIn("padding-bottom: var(--card-gap);", self.sentiment_styles)
@@ -294,6 +344,38 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn("max-height: var(--sentiment-history-max-height);", self.sentiment_styles)
self.assertIn("overflow: auto;", self.sentiment_styles)
def test_mentor_final_visual_fix_contract(self):
shell_styles = (STATIC_DIR / "shared" / "shell.css").read_text(encoding="utf-8")
mentor_html = (STATIC_DIR / "pages" / "mentor" / "page.html").read_text(encoding="utf-8")
self.assertNotIn('body[data-active-view="mentorView"]', shell_styles)
self.assertNotIn("body[data-active-view=mentorView]", shell_styles)
self.assertIn('id="mentorPageSubtitle"', mentor_html)
self.assertIn('class="mentor-page-header"', mentor_html)
self.assertIn("#mentorView .mentor-page-header", self.mentor_styles)
for tone in ("violet", "blue", "green", "orange", "red", "teal", "purple", "yellow"):
self.assertIn(f".mentor-avatar-tone-{tone} {{", self.mentor_styles, tone)
self.assertIn(
f':root[data-theme="dark"] #mentorView .mentor-avatar-tone-{tone} {{',
self.mentor_styles,
tone,
)
self.assertIn("kobe92-perspective", self.script)
self.assertIn('"beijingchaojia-perspective": "green"', self.script)
self.assertIn("`mentor-avatar-tone-${mentorAvatarTone(mentor)}`", self.script)
self.assertIn('${escapeHtml(grade)}级', self.script)
self.assertIn("置顶", self.script)
self.assertNotIn("activeMentorBadges", self.script)
self.assertNotIn("activeMentorBadges", mentor_html)
self.assertIn("#mentorView .mentor-message.assistant .mentor-message-body {", self.mentor_styles)
self.assertIn("max-width: 900px;", self.mentor_styles)
self.assertIn("#mentorView .mentor-follow-ups {", self.mentor_styles)
self.assertIn("width: 382px;", self.mentor_styles)
self.assertIn("data-lucide=\"filter\"", mentor_html)
self.assertNotIn("chevron-down", mentor_html)
self.assertNotIn("· 回答完成", self.script)
self.assertIn('state.mentorLoading ? "正在生成回答..." : (selected?.tagline', self.script)
def test_theme_switch_is_atomic_and_theme_library_loading_surface_is_dark_safe(self):
self.assertIn('typeof document.startViewTransition === "function"', self.script)
self.assertIn('root.classList.add("theme-switching")', self.script)