Compare commits

..
Author SHA1 Message Date
MS-01-Codexandmultica-agent 228e08a5e2 BAI-15: group mentor answer bubbles
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 16:40:00 +08:00
MS-01-Codexandmultica-agent 777af0cb8b BAI-3: harden live readiness diagnostics
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 12:29:01 +08:00
MS-01-Codexandmultica-agent a326df2a8d BAI-15: refine dark mentor answer bubble
Co-authored-by: multica-agent <github@multica.ai>
2026-08-07 10:14:02 +08:00
69 changed files with 1863 additions and 14441 deletions
+2
View File
@@ -43,6 +43,7 @@ from backend.features.screener.service import (
from backend.features.sentiment import SentimentServiceMixin from backend.features.sentiment import SentimentServiceMixin
from backend.features.sentiment.routes import SentimentRoutesMixin from backend.features.sentiment.routes import SentimentRoutesMixin
from backend.features.system import SystemHttpMixin from backend.features.system import SystemHttpMixin
from backend.features.system.health import HealthServiceMixin
from backend.features.system.routes import SystemRoutesMixin from backend.features.system.routes import SystemRoutesMixin
from backend.features.system.service import SystemServiceMixin from backend.features.system.service import SystemServiceMixin
from backend.features.themes import ThemeServiceMixin from backend.features.themes import ThemeServiceMixin
@@ -77,6 +78,7 @@ LEGACY_SECRET_KEYS = {
class DashboardService( class DashboardService(
HealthServiceMixin,
SystemServiceMixin, SystemServiceMixin,
AccountApplicationMixin, AccountApplicationMixin,
JobServiceMixin, JobServiceMixin,
+1 -7
View File
@@ -1,14 +1,8 @@
from .m0001_adopt_legacy import MIGRATION as M0001_ADOPT_LEGACY from .m0001_adopt_legacy import MIGRATION as M0001_ADOPT_LEGACY
from .m0002_job_runs import MIGRATION as M0002_JOB_RUNS from .m0002_job_runs import MIGRATION as M0002_JOB_RUNS
from .m0003_llm_audit import MIGRATION as M0003_LLM_AUDIT 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 from .runner import Migration, MigrationError, MigrationRunner
MIGRATIONS = ( MIGRATIONS = (M0001_ADOPT_LEGACY, M0002_JOB_RUNS, M0003_LLM_AUDIT)
M0001_ADOPT_LEGACY,
M0002_JOB_RUNS,
M0003_LLM_AUDIT,
M0004_MENTOR_NOTES,
)
__all__ = ["MIGRATIONS", "Migration", "MigrationError", "MigrationRunner"] __all__ = ["MIGRATIONS", "Migration", "MigrationError", "MigrationRunner"]
@@ -1,24 +0,0 @@
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",
)
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
class HealthServiceMixin:
def health_status(self) -> dict[str, Any]:
"""Return a cheap public readiness summary without provider secrets."""
database_ready = False
database_status: dict[str, Any] = {}
try:
with self.database.connect() as connection:
database_ready = connection.execute("SELECT 1").fetchone() is not None
database_status = self.database.status()
except Exception:
database_ready = False
try:
recent_jobs = self.jobs.repository.recent(12)
except Exception:
recent_jobs = []
last_sync = database_status.get("last_sync") or {}
snapshot_dates = int(database_status.get("snapshot_dates") or 0)
data_configured = bool(self.configured)
last_sync_success = last_sync.get("status") == "success"
if data_configured and snapshot_dates and last_sync_success:
data_status = "ready"
elif snapshot_dates:
data_status = "degraded"
else:
data_status = "unavailable"
latest_job = recent_jobs[0] if recent_jobs else {}
failed_jobs = sum(1 for job in recent_jobs if job.get("status") == "failed")
if not database_ready:
jobs_status = "unavailable"
elif latest_job.get("status") == "running":
jobs_status = "running"
elif latest_job.get("status") == "failed":
jobs_status = "degraded"
else:
jobs_status = "ready"
platform = self._platform_llm_profile()
primary_ready = self._profile_configured(platform["primary"])
fallback_ready = self._profile_configured(platform["fallback"])
models_status = "ready" if primary_ready else "unavailable"
component_states = (
"ready",
"ready" if database_ready else "unavailable",
data_status,
jobs_status,
models_status,
)
return {
"ok": database_ready,
"status": (
"ready"
if all(state in {"ready", "running"} for state in component_states)
else "degraded"
),
"account_required": True,
"time": datetime.now().astimezone().isoformat(timespec="seconds"),
"components": {
"process": {"status": "ready"},
"database": {
"status": "ready" if database_ready else "unavailable",
"snapshot_dates": snapshot_dates,
"last_snapshot_at": database_status.get("updated_at") or "",
},
"data_sources": {
"status": data_status,
"configured": data_configured,
"snapshot_available": snapshot_dates > 0,
"latest_trade_date": str(last_sync.get("trade_date") or ""),
"last_success_at": (
str(last_sync.get("finished_at") or "") if last_sync_success else ""
),
},
"jobs": {
"status": jobs_status,
"recent_failures": failed_jobs,
"latest_finished_at": str(latest_job.get("finished_at") or ""),
},
"models": {
"status": models_status,
"primary": "ready" if primary_ready else "unavailable",
"fallback": "ready" if fallback_ready else "disabled",
},
},
}
+1 -9
View File
@@ -1,20 +1,12 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
class SystemRoutesMixin: class SystemRoutesMixin:
def _handle_system_public_get(self, parsed) -> bool: def _handle_system_public_get(self, parsed) -> bool:
if parsed.path == "/api/health": if parsed.path == "/api/health":
self.send_json( self.send_json(self.application_service.health_status())
{
"ok": True,
"storage": "sqlite",
"account_required": True,
"time": datetime.now().astimezone().isoformat(timespec="seconds"),
}
)
return True return True
return False return False
-7
View File
@@ -104,13 +104,6 @@ class HttpTransportMixin:
except ValueError: except ValueError:
self.send_error(HTTPStatus.FORBIDDEN) self.send_error(HTTPStatus.FORBIDDEN)
return return
if candidate.is_dir():
candidate = (candidate / "index.html").resolve()
try:
candidate.relative_to(STATIC_DIR.resolve())
except ValueError:
self.send_error(HTTPStatus.FORBIDDEN)
return
if not candidate.is_file(): if not candidate.is_file():
candidate = STATIC_DIR / "index.html" candidate = STATIC_DIR / "index.html"
try: try:
+31 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime, timedelta
from typing import Any from typing import Any
from database import ReviewDatabase from database import ReviewDatabase
@@ -27,9 +27,37 @@ class SQLiteJobRunRepository:
def start( def start(
self, job_id: str, idempotency_key: str, output_version: str, self, job_id: str, idempotency_key: str, output_version: str,
metadata: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None,
) -> int: stale_after_seconds: int = 0,
now = datetime.now().astimezone().isoformat(timespec="seconds") ) -> int | None:
current = datetime.now().astimezone()
now = current.isoformat(timespec="seconds")
stale_before = (
current - timedelta(seconds=max(1, int(stale_after_seconds or 1)))
).isoformat(timespec="seconds")
with self.database.connect() as connection: with self.database.connect() as connection:
# The process-local runner lock cannot protect two server processes.
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"""
UPDATE job_runs
SET status = 'failed', finished_at = ?, error_code = 'StaleRun',
message = 'Previous run exceeded its execution window.'
WHERE job_id = ? AND idempotency_key = ? AND status = 'running'
AND started_at < ?
""",
(now, job_id, idempotency_key, stale_before),
)
claimed = connection.execute(
"""
SELECT 1 FROM job_runs
WHERE job_id = ? AND idempotency_key = ?
AND status IN ('running', 'success')
LIMIT 1
""",
(job_id, idempotency_key),
).fetchone()
if claimed:
return None
row = connection.execute( row = connection.execute(
""" """
SELECT COALESCE(MAX(attempt), 0) + 1 AS attempt FROM job_runs SELECT COALESCE(MAX(attempt), 0) + 1 AS attempt FROM job_runs
+12 -6
View File
@@ -51,8 +51,7 @@ class InProcessJobRunner:
lock = self._lock(definition.lock_key) lock = self._lock(definition.lock_key)
if not lock.acquire(blocking=False): if not lock.acquire(blocking=False):
return False return False
self._execute_locked(job_id, idempotency_key, action, metadata, lock) return self._execute_locked(job_id, idempotency_key, action, metadata, lock)
return True
def start_scheduler( def start_scheduler(
self, callback: Callable[[], None], interval_seconds: float, self, callback: Callable[[], None], interval_seconds: float,
@@ -105,13 +104,19 @@ class InProcessJobRunner:
def _execute_locked( def _execute_locked(
self, job_id: str, idempotency_key: str, action: JobAction, self, job_id: str, idempotency_key: str, action: JobAction,
metadata: dict[str, Any] | None, lock: threading.Lock, metadata: dict[str, Any] | None, lock: threading.Lock,
) -> None: ) -> bool:
definition = self.registry.get(job_id) definition = self.registry.get(job_id)
try: try:
for attempt in range(1, definition.max_attempts + 1): for attempt in range(1, definition.max_attempts + 1):
run_id = self.repository.start( run_id = self.repository.start(
job_id, idempotency_key, definition.output_version, metadata job_id,
idempotency_key,
definition.output_version,
metadata,
definition.timeout_seconds,
) )
if run_id is None:
return False
started = time.perf_counter() started = time.perf_counter()
try: try:
result = action() result = action()
@@ -119,7 +124,7 @@ class InProcessJobRunner:
raise RuntimeError(str(result.get("error") or "Job reported failure")) raise RuntimeError(str(result.get("error") or "Job reported failure"))
elapsed_ms = round((time.perf_counter() - started) * 1000) elapsed_ms = round((time.perf_counter() - started) * 1000)
self.repository.finish(run_id, "success", elapsed_ms) self.repository.finish(run_id, "success", elapsed_ms)
return return True
except Exception as exc: except Exception as exc:
elapsed_ms = round((time.perf_counter() - started) * 1000) elapsed_ms = round((time.perf_counter() - started) * 1000)
self.repository.finish( self.repository.finish(
@@ -127,7 +132,8 @@ class InProcessJobRunner:
type(exc).__name__, str(exc), type(exc).__name__, str(exc),
) )
if attempt >= definition.max_attempts: if attempt >= definition.max_attempts:
return return True
return True
finally: finally:
lock.release() lock.release()
+55 -55
View File
@@ -370,29 +370,29 @@
} }
], ],
"css_layers": [ "css_layers": [
"/shared/tokens.css?v=20260820-3", "/shared/tokens.css?v=20260806-2",
"/shared/base.css?v=20260806-1", "/shared/base.css?v=20260806-1",
"/shared/shell.css?v=20260820-8", "/shared/shell.css?v=20260806-2",
"/shared/auth.css?v=20260820-5", "/shared/auth.css?v=20260806-1",
"/shared/components/controls.css?v=20260820-2", "/shared/components/controls.css?v=20260806-1",
"/shared/components/navigation.css?v=20260820-1", "/shared/components/navigation.css?v=20260806-1",
"/shared/components/cards.css?v=20260820-1", "/shared/components/cards.css?v=20260806-1",
"/shared/components/tables.css?v=20260820-1", "/shared/components/tables.css?v=20260806-1",
"/shared/components/dialogs.css?v=20260820-3", "/shared/components/dialogs.css?v=20260806-1",
"/shared/components/feedback.css?v=20260806-1", "/shared/components/feedback.css?v=20260806-1",
"/pages/market/foundation.css?v=20260820-4", "/pages/market/foundation.css?v=20260806-1",
"/pages/sentiment/foundation.css?v=20260820-2", "/pages/sentiment/foundation.css?v=20260806-2",
"/pages/pools/foundation.css?v=20260820-1", "/pages/pools/foundation.css?v=20260806-2",
"/pages/ladder/foundation.css?v=20260820-1", "/pages/ladder/foundation.css?v=20260806-1",
"/pages/rotation/foundation.css?v=20260820-1", "/pages/rotation/foundation.css?v=20260806-1",
"/pages/auction/foundation.css?v=20260820-1", "/pages/auction/foundation.css?v=20260806-2",
"/pages/themes/foundation.css?v=20260820-1", "/pages/themes/foundation.css?v=20260806-1",
"/pages/popularity/foundation.css?v=20260820-1", "/pages/popularity/foundation.css?v=20260806-1",
"/pages/dragon-tiger/foundation.css?v=20260820-1", "/pages/dragon-tiger/foundation.css?v=20260806-1",
"/pages/screener/foundation.css?v=20260820-4", "/pages/screener/foundation.css?v=20260806-2",
"/pages/mentor/foundation.css?v=20260820-2", "/pages/mentor/foundation.css?v=20260807-1",
"/pages/heaven/foundation.css?v=20260806-2", "/pages/heaven/foundation.css?v=20260806-2",
"/pages/review/foundation.css?v=20260820-4" "/pages/review/foundation.css?v=20260806-1"
], ],
"frontend_composition": { "frontend_composition": {
"shell": "frontend/index.html", "shell": "frontend/index.html",
@@ -441,8 +441,8 @@
}, },
{ {
"path": "frontend/pages/screener/foundation.css", "path": "frontend/pages/screener/foundation.css",
"bytes": 103547, "bytes": 102987,
"lines": 6576 "lines": 6565
}, },
{ {
"path": "frontend/pages/heaven/page.js", "path": "frontend/pages/heaven/page.js",
@@ -451,8 +451,8 @@
}, },
{ {
"path": "frontend/shared/shell.css", "path": "frontend/shared/shell.css",
"bytes": 63550, "bytes": 51978,
"lines": 3757 "lines": 3224
}, },
{ {
"path": "backend/features/heaven/engine.py", "path": "backend/features/heaven/engine.py",
@@ -461,8 +461,8 @@
}, },
{ {
"path": "frontend/index.html", "path": "frontend/index.html",
"bytes": 47871, "bytes": 45846,
"lines": 661 "lines": 643
}, },
{ {
"path": "backend/features/screener/catalog.py", "path": "backend/features/screener/catalog.py",
@@ -471,8 +471,8 @@
}, },
{ {
"path": "frontend/pages/auction/foundation.css", "path": "frontend/pages/auction/foundation.css",
"bytes": 35247, "bytes": 34990,
"lines": 2416 "lines": 2409
}, },
{ {
"path": "database.py", "path": "database.py",
@@ -531,7 +531,7 @@
}, },
{ {
"path": "frontend/pages/pools/page.html", "path": "frontend/pages/pools/page.html",
"bytes": 14942, "bytes": 14958,
"lines": 235 "lines": 235
}, },
{ {
@@ -541,8 +541,8 @@
}, },
{ {
"path": "frontend/shared/admin.js", "path": "frontend/shared/admin.js",
"bytes": 14145, "bytes": 13975,
"lines": 261 "lines": 256
}, },
{ {
"path": "backend/features/heaven/market_context.py", "path": "backend/features/heaven/market_context.py",
@@ -574,11 +574,6 @@
"bytes": 10539, "bytes": 10539,
"lines": 244 "lines": 244
}, },
{
"path": "frontend/shared/dashboard.js",
"bytes": 9993,
"lines": 220
},
{ {
"path": "backend/data/providers/tushare_sectors.py", "path": "backend/data/providers/tushare_sectors.py",
"bytes": 9876, "bytes": 9876,
@@ -605,25 +600,25 @@
"lines": 238 "lines": 238
}, },
{ {
"path": "frontend/pages/mentor/page.html", "path": "frontend/shared/dashboard.js",
"bytes": 8357, "bytes": 8424,
"lines": 116 "lines": 194
}, },
{ {
"path": "backend/features/screener/formula.py", "path": "backend/features/screener/formula.py",
"bytes": 6983, "bytes": 6983,
"lines": 146 "lines": 146
}, },
{
"path": "backend/application.py",
"bytes": 6837,
"lines": 180
},
{ {
"path": "backend/data/providers/tushare_daily.py", "path": "backend/data/providers/tushare_daily.py",
"bytes": 6837, "bytes": 6837,
"lines": 160 "lines": 160
}, },
{
"path": "backend/application.py",
"bytes": 6751,
"lines": 178
},
{ {
"path": "backend/features/market/insights_popularity.py", "path": "backend/features/market/insights_popularity.py",
"bytes": 6739, "bytes": 6739,
@@ -649,6 +644,11 @@
"bytes": 6202, "bytes": 6202,
"lines": 141 "lines": 141
}, },
{
"path": "frontend/pages/mentor/page.html",
"bytes": 6190,
"lines": 89
},
{ {
"path": "backend/features/screener/selection.py", "path": "backend/features/screener/selection.py",
"bytes": 6092, "bytes": 6092,
@@ -704,11 +704,6 @@
"bytes": 4276, "bytes": 4276,
"lines": 91 "lines": 91
}, },
{
"path": "frontend/shared/theme.js",
"bytes": 4258,
"lines": 117
},
{ {
"path": "backend/features/screener/engine.py", "path": "backend/features/screener/engine.py",
"bytes": 4242, "bytes": 4242,
@@ -719,6 +714,11 @@
"bytes": 4118, "bytes": 4118,
"lines": 115 "lines": 115
}, },
{
"path": "frontend/shared/theme.js",
"bytes": 4118,
"lines": 115
},
{ {
"path": "frontend/shared/table.js", "path": "frontend/shared/table.js",
"bytes": 3790, "bytes": 3790,
@@ -736,7 +736,7 @@
}, },
{ {
"path": "frontend/pages/themes/page.html", "path": "frontend/pages/themes/page.html",
"bytes": 3316, "bytes": 3309,
"lines": 55 "lines": 55
}, },
{ {
@@ -829,11 +829,6 @@
"bytes": 1455, "bytes": 1455,
"lines": 48 "lines": 48
}, },
{
"path": "backend/features/system/routes.py",
"bytes": 1423,
"lines": 40
},
{ {
"path": "backend/features/themes/routes.py", "path": "backend/features/themes/routes.py",
"bytes": 1337, "bytes": 1337,
@@ -844,6 +839,11 @@
"bytes": 1195, "bytes": 1195,
"lines": 30 "lines": 30
}, },
{
"path": "backend/features/system/routes.py",
"bytes": 1178,
"lines": 32
},
{ {
"path": "frontend/pages/ladder/page.html", "path": "frontend/pages/ladder/page.html",
"bytes": 1143, "bytes": 1143,
-5
View File
@@ -1,8 +1,3 @@
> ⚠️ **本文档已过时,仅留档备查,请勿删除。**
> 本交接说明核实于 2026-08-06,其中「当前提交」「当前状态」「正在处理的事项」「验证记录」等已与代码现状不符(当时的未提交改动现已合并,项目已推进到全站视觉统一收尾阶段)。
> 最新内容请看 `docs/项目需求.md`、`docs/最新进度.md`、`docs/任务清单.md` 和 `docs/README.md`。
> 架构与维护规矩仍以根目录 `AGENTS.md`、`ARCHITECTURE.md` 为准;本文第 2、6 节(架构与决策)仍可作参考。
# 小白复盘项目交接说明 # 小白复盘项目交接说明
> 核实日期:2026-08-06Asia/Shanghai > 核实日期:2026-08-06Asia/Shanghai
+9 -33
View File
@@ -1,35 +1,11 @@
# 小白复盘 · 交接手册首页(首页说明) # 文档索引
> 一句话:这是「小白复盘」项目的交接手册入口。新来的智能体(或人)先看这一页,再按下面顺序读四份文档,就能知道这个项目是干什么的、干到哪了、下一步做什么 - `product/小白复盘-完整产品规格说明书.md`:从零恢复产品时的完整功能与行为资产
- `HANDOFF.md`:当前仓库、架构、完成度、风险和后续验收的交接基线。
- `issues/README.md`:尚未完成事项的独立 Issue 索引;Issue 不是已创建的 Gitea 工单。
- `maintenance/人工维护指南.md`:当前正式源码的启动、修改、验收、数据和回退流程。
- `governance/`:架构决策、注册表治理和历次结构治理记录。
- `migration/`:从旧根目录保真迁入`app/`的历史账本、证据和失败版本记录。
## 先读哪些文件(按顺序) 日常维护优先阅读根目录`AGENTS.md``ARCHITECTURE.md``HANDOFF.md`和维护指南。`migration/`只用于审计与
追溯,不参与应用启动、测试选择或运行时路径解析。
1. `项目需求.md` —— 这个项目是干什么的、要解决什么问题、有哪些功能。
2. `最新进度.md` —— 目前整体做到哪一步了。
3. `任务清单.md` —— 正在做 / 已做完 / 还没安排,三栏一目了然。
4. 本文件 `README.md` —— 就是你现在看的这一页。
读完上面四份,就算“接手”了。想深入了解实现细节,再往下读。
## 想深入了解时再读这些
- `product/小白复盘-完整产品规格说明书.md` —— 最完整、最权威的“产品需求”说明书,从零重建项目都用它。
- `maintenance/人工维护指南.md` —— 怎么启动、怎么改代码、怎么跑测试、怎么备份和回退。
- `governance/` —— 架构决策和历次结构治理记录。
- `migration/` —— 从旧代码保真迁进 `app/` 的历史账本和证据,只用于审计和追溯,不参与运行。
- 根目录的 `AGENTS.md`(维护硬规矩)、`ARCHITECTURE.md`(技术架构)。
## 更新规矩(每完成或新增一个任务都要做)
任何智能体完成或新增一个任务后,必须顺手把这份手册更新到位,不能只改代码:
1. 任务做完或新增 → 更新 `任务清单.md`:把任务从「正在做」挪到「已做完」,或把新任务加进对应栏目。
2. 整体进度变了 → 更新 `最新进度.md`
3. 需求或功能变了 → 更新 `项目需求.md`(重大变化还要同步 `product/` 里的完整说明书)。
4. 更新完提交并推送进仓库(保存并上传到放代码的网站),不能只留在自己电脑里。
## 注意事项
- 旧文档不能删:被替代的旧文档开头要加一行「⚠️ 本文档已过时,仅留档备查,请勿删除」,再写新版。
- 用中文大白话写,专业词要带通俗解释,让不懂代码的人也能看懂。
- 「问天」板块是冻结区,任何改动都不许碰;写文档时别误导后来人去改它。
+1 -1
View File
@@ -16,4 +16,4 @@
| [010](ISSUE-010-live-provider-llm-readiness.md) | 数据源与 LLM 在线就绪核验 | 待运行核验 | P0 | | [010](ISSUE-010-live-provider-llm-readiness.md) | 数据源与 LLM 在线就绪核验 | 待运行核验 | P0 |
| [011](ISSUE-011-public-deployment-hardening.md) | 公网部署加固 | 未来范围 | P3 | | [011](ISSUE-011-public-deployment-hardening.md) | 公网部署加固 | 未来范围 | P3 |
关闭任一 Issue 前,必须同步更新 `docs/最新进度.md``docs/任务清单.md` 的状态、验证日期和回档提交;不能只改 Issue 标题。旧版 `docs/HANDOFF.md` 已过时,仅留档备查。 关闭任一 Issue 前,必须更新 `docs/HANDOFF.md` 的状态、验证日期和回档提交;不能只改 Issue 标题。
-35
View File
@@ -1,35 +0,0 @@
# 任务清单
> 分三栏:正在做 / 已做完 / 还没安排。以任务板(Multica)和仓库内 `issues/` 目录为准,核实日期 2026-08-23。
## 正在做
| 任务 | 说明 | 状态 |
|---|---|---|
| 全站视觉统一改造收尾 | 主线。17 个阶段已完成,正在最终验收、代码合并 | 收尾中 |
| 手机端独立重新设计 | 先出视觉/交互规范和技术架构方案,等老板确认后再施工 | 方案送审中 |
## 已做完
| 任务 | 说明 |
|---|---|
| 全站视觉统一改造(17 个阶段) | 公共外壳、各市场数据页、智能工具页、账户页、顶栏布局返工等,逐批通过功能 + 视觉双重审核 |
| 数据库历史问题修复收口 | 老数据未被改动 |
| 顶端栏图标对齐 | 集合竞价 / 题材库 / 人气热榜三页,已部署 `.36:8765`(提交 `ed9858e` |
| 情绪周期页头改造等页面细节 | 去背景框、圆角分段控件 / 导出按钮(任务卡 HEL-31/36/42 等) |
| 架构治理(保真迁移) | 从旧根目录迁进 `app/`,模块化单体 + 注册表 + 统一验收工具,2026-08-01 人工验收 |
| 仓库交接手册整理 | 四份中文交接文档(需求 / 进度 / 任务 / 首页),已提交推送(提交 `224e2a7` |
## 还没安排
| 任务 | 说明 | 大致优先级 |
|---|---|---|
| 游资档案历史画像 | 更完整的游资历史操作画像 | 低 |
| 完整 IC 动态多因子选股 | 需要约 12 个月历史数据做因子有效性计算 | 中 |
| 公网部署加固 | 多实例、TLS、PostgreSQL 等(当前只内网用) | 低 / 未来 |
| 政策 / 宏观 / 隔夜消息数据 | 数据源还没定 | 中 |
| 分析师一致预期数据 | 数据源被阻塞 | 中低 |
| Level-2 竞价深度 | 需要授权 | 中低 |
> 说明:手机端施工,以及上面这些数据/算法类功能,做之前都要先和老板确认优先级,不要擅自开工。
> 仓库内更细的任务说明见 `issues/` 目录(ISSUE-001 到 ISSUE-011)。
-32
View File
@@ -1,32 +0,0 @@
# 最新进度
> 核实日期:2026-08-23,以代码仓库当前提交 `224e2a7` 为准(`ed9858e` 是最近一笔代码改动)。
## 一句话总结
项目主体功能早就做好并上线内网了。当前主线是「全站视觉统一改造」,已经走完 17 个阶段,正处于**最终验收、代码合并的收尾阶段**。同时新开了一条「手机端独立重新设计」的线(还在出方案,没动工)。本仓库的交接手册已整理完成(见 `任务清单.md`)。
## 已经做到哪了
- **代码结构**:已经从历史杂乱目录保真迁移进 `app/`,整理成「模块化单体」(一个 Python 进程、一个 SQLite 数据库、不需要构建工具的前端),用户在 2026-08-01 人工验收通过。
- **功能**:16 个主工作区都有正式实现(见 `项目需求.md`)。
- **视觉统一**:17 个阶段全部完成,每批都过了功能和视觉双重审核,返修版已部署到内网验收地址 `192.168.200.36:8765`
- **数据库历史问题**:已修复收口,老数据没有被改动。
- **最近的代码改动**(2026-08 下旬):主要是视觉收尾的细节——顶端栏图标对齐、设计令牌统一、问师/情绪周期等页面样式修整等,已合并进 `main`,并部署到内网 `.36:8765`
## 正在做的事
1. **全站视觉统一改造收尾**:最终验收 + 代码合并(主线)。
2. **手机端独立重新设计**:先出视觉规范和架构方案,等老板确认后再施工(见 `任务清单.md`)。
## 哪些还不能算“完成”
- **手机端**:当前有样式但基本是电脑端缩小,不可用。正在按“独立手机产品”重新设计,还没施工。
- **一些依赖外部数据/算法的高级功能还没做**:完整 IC 动态多因子选股、政策/宏观/隔夜消息、分析师一致预期、Level-2 竞价深度、游资档案历史画像(详见 `任务清单.md`)。
- **公网部署**:现在只在内网用,还没做公网加固。
## 关键时间点
- 2026-08-01`app/` 保真迁移完成,用户人工验收通过。
- 2026-08-06:上一版交接说明(`HANDOFF.md`)生成(现已过时,仅留档)。
- 2026-08-21:视觉收尾提交 `ed9858e` 合并进 `main`,部署到内网 `.36:8765`
-46
View File
@@ -1,46 +0,0 @@
# 项目需求
> 大白话版。逐页、逐字段的完整需求请看 `product/小白复盘-完整产品规格说明书.md`(那是最权威的“说明书”,从零重建项目都用它)。
## 这个项目是干什么的
「小白复盘」是一个给老板个人用的股票复盘工具网站,放在内网访问(地址 `192.168.200.36:8765`)。
它不是炒股下单软件,而是「收盘后和开盘前」用来复盘、观察市场的工具:
- 把当天(或最近)的真实行情、涨停跌停、板块轮动、集合竞价等信息整理清楚,帮老板复盘。
- 提供智能选股、问师(跟“游资思维”老师对话)等辅助分析。
- 记录自己的交易和复盘。
一句话:它帮你“看清市场、想清楚思路、记下来”,但不替你做买卖决定。
## 要解决什么问题
1. **市场信息太散**:涨停池、炸板池、跌停板、龙虎榜、人气榜、板块轮动这些信息本来分散在各处,这个网站把它们集中到一处,还配了日间/夜间两套配色,看起来统一。
2. **复盘靠脑子记不住**:提供“我的复盘”和交易日志,把每天的判断、操作、情绪记录下来。
3. **选股没思路**:智能选股用规则和因子(影响股价的数据指标)帮你筛出候选股票。
4. **想听“高手”怎么看**:问师模块可以按不同的游资思维(比如佛山无影脚、北京炒家等)跟老师对话。
## 主要功能(板块)
登录后侧栏有 16 个主工作区,默认进入「情绪周期」:
- **市场类(12 个)**:情绪周期、涨停池、炸板池、跌停板、昨日涨停、涨停表现、市场天梯、板块轮动、集合竞价、题材库、人气热榜、龙虎榜。
- **智能工具类(3 个)**:智能选股、问师、问天。
- **个人类(1 个)**:我的复盘。
其中「问天」是冻结区(见下面的硬规矩)。
## 几条硬规矩(不能破坏的边界)
- 「问天」板块是**冻结区**,任何改动都不许碰它。
- **不用假数据冒充真行情**;数据缺失就明说“没有/不可用”,不能编。
- **每个用户自己的数据互相隔离**(自选、复盘、对话、问天历史等),看不到别人的。
- **计算由程序确定性完成**(情绪周期、智能选股、问天排盘等),AI 大模型(LLM,就是会聊天的那个 AI)只负责解释或编译自然语言条件,不能改计算结果。
- **不接券商、不自动下单**,不承诺收益。
## 权限
- **普通用户**:能用大部分市场页面和「我的复盘」。
- **会员**:额外能用智能选股、问师、问天(需要管理员开通)。
- **管理员**:管理公共行情密钥、会员额度和系统配置等。
+71 -89
View File
@@ -5,21 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<title>小白复盘</title> <title>小白复盘</title>
<script>
(() => {
const params = new URLSearchParams(window.location.search);
const ui = params.get("ui");
const path = window.location.pathname;
const alreadyMobile = path === "/m" || path.indexOf("/m/") === 0;
const forcedMobile = ui === "mobile";
const forcedDesktop = ui === "desktop";
const autoMobile = window.matchMedia && window.matchMedia("(max-width: 720px)").matches;
const mobileUA = /Android|iPhone|iPad|iPod|Mobile|Windows Phone/i.test(navigator.userAgent || "");
if (!forcedDesktop && !alreadyMobile && (forcedMobile || autoMobile || mobileUA)) {
window.location.replace("/m/" + window.location.search + window.location.hash);
}
})();
</script>
<script> <script>
(() => { (() => {
let theme = "light"; let theme = "light";
@@ -32,29 +17,29 @@
document.documentElement.style.colorScheme = theme; document.documentElement.style.colorScheme = theme;
})(); })();
</script> </script>
<link rel="stylesheet" href="/shared/tokens.css?v=20260820-3"> <link rel="stylesheet" href="/shared/tokens.css?v=20260806-2">
<link rel="stylesheet" href="/shared/base.css?v=20260806-1"> <link rel="stylesheet" href="/shared/base.css?v=20260806-1">
<link rel="stylesheet" href="/shared/shell.css?v=20260820-8"> <link rel="stylesheet" href="/shared/shell.css?v=20260806-2">
<link rel="stylesheet" href="/shared/auth.css?v=20260820-5"> <link rel="stylesheet" href="/shared/auth.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/controls.css?v=20260820-2"> <link rel="stylesheet" href="/shared/components/controls.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260820-1"> <link rel="stylesheet" href="/shared/components/navigation.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/cards.css?v=20260820-1"> <link rel="stylesheet" href="/shared/components/cards.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/tables.css?v=20260820-1"> <link rel="stylesheet" href="/shared/components/tables.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260820-3"> <link rel="stylesheet" href="/shared/components/dialogs.css?v=20260806-1">
<link rel="stylesheet" href="/shared/components/feedback.css?v=20260806-1"> <link rel="stylesheet" href="/shared/components/feedback.css?v=20260806-1">
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260820-4"> <link rel="stylesheet" href="/pages/market/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260820-2"> <link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/pools/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/auction/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/themes/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260820-1"> <link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260806-1">
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260820-4"> <link rel="stylesheet" href="/pages/screener/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260820-2"> <link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260807-1">
<link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260806-2"> <link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260806-2">
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260820-4"> <link rel="stylesheet" href="/pages/review/foundation.css?v=20260806-1">
</head> </head>
<body> <body>
<section id="authGate" class="auth-gate" aria-label="账号登录"> <section id="authGate" class="auth-gate" aria-label="账号登录">
@@ -83,56 +68,12 @@
<span id="mobilePageGroup">行情</span> <span id="mobilePageGroup">行情</span>
<strong id="mobilePageTitle">情绪周期</strong> <strong id="mobilePageTitle">情绪周期</strong>
</div> </div>
<div class="app-page-context" aria-live="polite"> <div class="market-tape" aria-label="市场概况">
<strong id="currentPageTitle">小白复盘</strong> <span class="market-item up">上涨 <strong id="tapeUp">--</strong></span>
<span id="currentPageSubtitle"></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> </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-actions">
<div class="header-date-group"> <div class="header-date-group">
@@ -141,7 +82,7 @@
<button id="nextDate" class="icon-button" type="button" title="后一个交易日" aria-label="后一个交易日"><i data-lucide="chevron-right"></i></button> <button id="nextDate" class="icon-button" type="button" title="后一个交易日" aria-label="后一个交易日"><i data-lucide="chevron-right"></i></button>
</div> </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="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><span id="themeModeText">日间模式</span></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="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="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="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> <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>
@@ -152,12 +93,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="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> <button class="button mobile-command-shortcut" type="button" data-mobile-command-target="assistantButton"><i data-lucide="message-circle-more"></i><span>助手</span></button>
</div> </div>
<button id="refreshButton" class="button command-button" type="button" title="刷新"><i data-lucide="refresh-cw"></i><span>刷新</span></button> <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" title="后台刷新" hidden><i data-lucide="cloud-download"></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" title="系统管理" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button> <button id="settingsButton" class="button command-button" type="button" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
<div class="account-menu-shell"> <div class="account-menu-shell">
<div id="accountRoleBadges" class="account-role-badges" aria-label="账号身份"> <div id="accountRoleBadges" class="account-role-badges" aria-label="账号身份">
<span id="accountAdminBadge" class="account-role-badge admin-role-badge" title="管理员" hidden><i data-lucide="shield-check"></i><span>管理员</span></span> <span id="accountAdminBadge" class="account-role-badge admin-role-badge" 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> <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> </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> <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>
@@ -228,6 +169,47 @@
</select> </select>
<i data-lucide="chevron-down" aria-hidden="true"></i> <i data-lucide="chevron-down" aria-hidden="true"></i>
</label> </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. --> <!-- Registered page fragments mount here. -->
</main> </main>
</div> </div>
-302
View File
@@ -1,302 +0,0 @@
(function (global) {
"use strict";
const nav = {
entries: [
{ key: "market", title: "行情数据", subtitle: "情绪 · 梯队 · 题材 · 龙虎榜", icon: "bar-chart-3" },
{ key: "tools", title: "智能工具", subtitle: "智能选股 · 策略跟踪 · 问师", icon: "wand-2" },
{ key: "review", title: "我的复盘", subtitle: "自选 · 交易 · 复盘 · 笔记 · 提醒", icon: "notebook-pen" },
{ key: "assistant", title: "复盘助手", subtitle: "AI 对话复盘", icon: "message-square" },
{ key: "system", title: "系统管理", subtitle: "账号 · 密码 · 会员 · 设置", icon: "settings" }
],
hubs: {
market: {
title: "行情数据",
items: [
{ key: "market/sentiment", label: "情绪周期", icon: "activity" },
{ key: "market/limit-up", label: "涨停池", icon: "trending-up" },
{ key: "market/broken", label: "炸板池", icon: "zap" },
{ key: "market/limit-down", label: "跌停池", icon: "trending-down" },
{ key: "market/yesterday", label: "昨日涨停", icon: "history" },
{ key: "market/performance", label: "涨停表现", icon: "chart-line" },
{ key: "market/ladder", label: "市场天梯", icon: "layers" },
{ key: "market/rotation", label: "主题轮动", icon: "refresh-cw" },
{ key: "market/auction", label: "竞价", icon: "gavel" },
{ key: "market/themes", label: "题材库", icon: "book-open" },
{ key: "market/popularity", label: "人气榜", icon: "flame" },
{ key: "market/dragon", label: "龙虎榜", icon: "crown" }
]
},
tools: {
title: "智能工具",
items: [
{ key: "tools/screener", label: "智能选股", icon: "filter" },
{ key: "tools/tracking", label: "策略跟踪", icon: "target" },
{ key: "tools/mentor", label: "问师", icon: "bot" }
]
},
review: {
title: "我的复盘",
items: [
{ key: "review/watchlist", label: "自选股", icon: "star" },
{ key: "review/trades", label: "交易日志", icon: "scroll-text" },
{ key: "review/daily", label: "每日复盘", icon: "calendar-check" },
{ key: "review/notes", label: "个股笔记", icon: "sticky-note" },
{ key: "review/alerts", label: "提醒中心", icon: "bell" }
]
},
system: {
title: "系统管理",
items: [
{ key: "system/profile", label: "账号资料", icon: "user" },
{ key: "system/password", label: "修改密码", icon: "lock" },
{ key: "system/membership", label: "会员状态", icon: "gem" },
{ key: "system/admin", label: "系统设置", icon: "sliders-horizontal", adminOnly: true },
{ key: "system/members", label: "会员管理", icon: "users", adminOnly: true }
]
}
},
tableDefaults: {
frozenColumns: ["name", "code"],
primaryColumns: [],
scrollColumns: []
},
tableColumns: {
"market/limit-up": {
summary: "limit",
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "streak", label: "连板", type: "streak" },
{ key: "change", label: "涨幅%", type: "change" },
{ key: "price", label: "价格", type: "price" }
],
scrollColumns: [
{ key: "sector", label: "所属板块", type: "text" },
{ key: "first_time", label: "首封", type: "text" },
{ key: "last_time", label: "最后封板", type: "text" },
{ key: "open_times", label: "开板", type: "int" },
{ key: "turnover_rate", label: "换手%", type: "rate" },
{ key: "amount_billion", label: "成交额亿", type: "money" },
{ key: "seal_amount_million", label: "封单额万", type: "int", hideZero: true },
{ key: "reason", label: "涨停原因", type: "text", wide: true }
]
},
"market/broken": {
summary: "broken",
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "change", label: "现价涨幅%", type: "change" },
{ key: "limitGap", label: "距涨停%", type: "gap" },
{ key: "price", label: "价格", type: "price" }
],
scrollColumns: [
{ key: "sector", label: "所属板块", type: "text" },
{ key: "first_time", label: "首次触板", type: "text" },
{ key: "open_times", label: "开板", type: "int" },
{ key: "turnover_rate", label: "换手%", type: "rate" },
{ key: "amount_billion", label: "成交额亿", type: "money" },
{ key: "reason", label: "炸板原因", type: "text", wide: true }
]
},
"market/limit-down": {
summary: "down",
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "change", label: "跌幅%", type: "change" },
{ key: "price", label: "价格", type: "price" }
],
scrollColumns: [
{ key: "sector", label: "所属板块", type: "text" },
{ key: "turnover_rate", label: "换手%", type: "rate" },
{ key: "amount_billion", label: "成交额亿", type: "money" },
{ key: "streak", label: "连续跌停", type: "int", hideZero: true },
{ key: "reason", label: "风险线索", type: "text", wide: true }
]
},
"market/yesterday": {
summary: "yesterday",
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "current_change", label: "今日涨幅%", type: "change" },
{ key: "outcome", label: "今日结果", type: "outcome" },
{ key: "prior_streak", label: "昨日高度", type: "int" }
],
scrollColumns: [
{ key: "current_streak", label: "当前高度", type: "height" },
{ key: "sector", label: "所属板块", type: "text" },
{ key: "reason", label: "涨停逻辑", type: "text", wide: true }
]
},
"market/performance": {
summary: "performance",
frozenColumns: [
{ key: "label", label: "梯队", type: "text", width: 96 }
],
primaryColumns: [
{ key: "advance_rate", label: "晋级率%", type: "advance" },
{ key: "advanced", label: "晋级", type: "int" },
{ key: "positive_rate", label: "收红率%", type: "rate" }
],
scrollColumns: [
{ key: "count", label: "样本", type: "int" },
{ key: "average_change", label: "平均涨幅%", type: "change" }
]
},
"market/popularity": {
summary: "popularity",
sources: ["combined", "ths", "dc"],
columns: {
combined: {
frozenColumns: [
{ key: "rank", label: "#", type: "rank", width: 40 },
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "price", label: "最新价", type: "price" },
{ key: "change", label: "涨跌幅%", type: "change" }
],
scrollColumns: [
{ key: "ths_rank", label: "同花顺", type: "int", hideZero: true },
{ key: "dc_rank", label: "东方财富", type: "int", hideZero: true },
{ key: "rank_change", label: "排名变化", type: "move" },
{ key: "concepts", label: "热门概念", type: "concepts" }
]
},
ths: {
frozenColumns: [
{ key: "rank", label: "#", type: "rank", width: 40 },
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "price", label: "最新价", type: "price" },
{ key: "change", label: "涨跌幅%", type: "change" }
],
scrollColumns: [
{ key: "dc_rank", label: "东方财富", type: "int", hideZero: true },
{ key: "rank_change", label: "排名变化", type: "move" },
{ key: "concepts", label: "热门概念", type: "concepts" },
{ key: "dual_source", label: "榜单状态", type: "dual" }
]
},
dc: {
frozenColumns: [
{ key: "rank", label: "#", type: "rank", width: 40 },
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "price", label: "最新价", type: "price" },
{ key: "change", label: "涨跌幅%", type: "change" }
],
scrollColumns: [
{ key: "ths_rank", label: "同花顺", type: "int", hideZero: true },
{ key: "rank_change", label: "排名变化", type: "move" },
{ key: "concepts", label: "热门概念", type: "concepts" },
{ key: "dual_source", label: "榜单状态", type: "dual" }
]
}
}
},
"market/sentiment/history": {
frozenColumns: [
{ key: "trade_date", label: "日期", type: "text", width: 88 }
],
primaryColumns: [
{ key: "score", label: "温度", type: "int" },
{ key: "phase", label: "阶段", type: "text" },
{ key: "direction", label: "方向", type: "text" }
],
scrollColumns: [
{ key: "limit_up_count", label: "涨停", type: "int" },
{ key: "first_board_count", label: "首板", type: "int" },
{ key: "second_board_count", label: "二板", type: "int" },
{ key: "three_plus_count", label: "三板+", type: "int" },
{ key: "max_height", label: "高度", type: "int" },
{ key: "broken_count", label: "炸板", type: "int" },
{ key: "limit_down_count", label: "跌停", type: "int" },
{ key: "seal_rate", label: "封板率%", type: "rate" },
{ key: "previous_limit_count", label: "昨涨停", type: "int" },
{ key: "previous_positive_rate", label: "昨红率%", type: "rate" }
]
},
"market/auction": {
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "attention_score", label: "关注分", type: "score" },
{ key: "change", label: "竞价涨幅%", type: "change" },
{ key: "expectation", label: "预期", type: "expectation" }
],
scrollColumns: [
{ key: "sector", label: "方向", type: "text" },
{ key: "amount_million", label: "竞价额百万", type: "money" },
{ key: "volume_ratio", label: "量比", type: "rate" }
]
},
"market/rotation/members": {
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "change", label: "涨跌幅%", type: "change" },
{ key: "close", label: "收盘", type: "price" }
],
scrollColumns: [
{ key: "open", label: "开盘", type: "price" },
{ key: "amount_billion", label: "成交额亿", type: "money" }
]
},
"market/themes/members": {
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "change", label: "涨跌幅%", type: "change" },
{ key: "price", label: "现价", type: "price" }
],
scrollColumns: [
{ key: "amount_billion", label: "成交额亿", type: "money" }
]
},
"market/dragon/operations": {
frozenColumns: [
{ key: "stock", label: "股票", type: "stock", width: 96 }
],
primaryColumns: [
{ key: "direction", label: "方向", type: "direction" },
{ key: "change", label: "涨幅%", type: "change" }
],
scrollColumns: [
{ key: "buy_million", label: "买入百万", type: "money" },
{ key: "sell_million", label: "卖出百万", type: "money" },
{ key: "net_buy_million", label: "净额百万", type: "change" },
{ key: "seat_name", label: "席位", type: "text", wide: true },
{ key: "reason", label: "上榜原因", type: "text", wide: true }
]
}
}
};
global.MobileNav = nav;
})(window);
File diff suppressed because it is too large Load Diff
-559
View File
@@ -1,559 +0,0 @@
[hidden] {
display: none !important;
}
#m-app,
#m-app * {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
background: #f4f5f7;
}
#m-app {
min-height: 100vh;
background: var(--canvas);
color: var(--text-primary);
overflow-x: hidden;
-webkit-tap-highlight-color: transparent;
transition: background-color var(--motion-pop) var(--ease-press),
border-color var(--motion-pop) var(--ease-press);
}
#m-boot-splash {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--canvas);
z-index: 200;
}
#m-boot-splash .m-brand-mark {
margin-bottom: 0;
}
.m-motion-fade-in {
animation: m-fade-in var(--motion-fade) var(--ease-press) both;
}
@keyframes m-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.m-motion-rise-in {
animation: m-rise-in var(--motion-enter) var(--ease-enter) both;
}
@keyframes m-rise-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.m-motion-push-in {
animation: m-push-in var(--motion-enter) var(--ease-enter) both;
}
@keyframes m-push-in {
from { opacity: 0; transform: translateX(16px); }
to { opacity: 1; transform: none; }
}
.m-motion-pop-in {
animation: m-pop-in var(--motion-pop) var(--ease-enter) both;
}
@keyframes m-pop-in {
from { opacity: 0; transform: translateX(-16px); }
to { opacity: 1; transform: none; }
}
.m-motion-boot-in {
animation: m-fade-in var(--motion-press-release) var(--ease-press) both;
}
.m-header {
position: sticky;
top: 0;
z-index: 50;
display: flex;
align-items: center;
height: var(--mobile-header-height);
padding: 0 4px;
background: var(--surface);
border-bottom: 1px solid var(--border);
transition: background-color var(--motion-pop) var(--ease-press),
border-color var(--motion-pop) var(--ease-press);
}
.m-header-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--mobile-touch-size);
height: var(--mobile-touch-size);
flex: 0 0 var(--mobile-touch-size);
border: 0;
background: transparent;
color: var(--text-primary);
cursor: pointer;
transition: transform var(--motion-press-release) var(--ease-press),
background-color var(--motion-press-release) var(--ease-press);
}
.m-header-btn:active {
background: var(--surface-hover);
transform: scale(0.94);
transition-duration: var(--motion-press);
}
.m-title {
flex: 1;
margin: 0;
font-size: var(--font-size-page-title);
font-weight: 600;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: color var(--motion-pop) var(--ease-press);
}
.m-actions {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
}
.m-view {
padding: var(--mobile-page-padding);
padding-bottom: calc(var(--mobile-page-padding) + var(--mobile-safe-bottom));
max-width: 100%;
overflow-x: hidden;
}
#m-app[data-tabbar="true"] .m-view {
padding-bottom: calc(var(--mobile-tabbar-height) + var(--mobile-safe-bottom) + var(--mobile-page-padding));
}
.m-placeholder {
padding: 40px 16px;
text-align: center;
}
.m-placeholder-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
margin-bottom: 12px;
border-radius: 50%;
background: var(--surface-muted);
color: var(--text-tertiary);
}
.m-placeholder h2 {
margin: 0 0 8px;
font-size: var(--font-size-card-title);
font-weight: 600;
}
.m-placeholder p {
margin: 0;
font-size: var(--font-size-label);
color: var(--text-secondary);
line-height: 1.5;
}
.m-hub-grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.m-grid-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: var(--mobile-icon-cell);
padding: 8px 4px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: transform var(--motion-press-release) var(--ease-press),
background-color var(--motion-press-release) var(--ease-press);
}
.m-grid-item:active {
background: var(--surface-hover);
transform: scale(0.94);
transition-duration: var(--motion-press);
}
.m-grid-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 12px;
background: var(--action-soft);
color: var(--action);
flex: 0 0 auto;
transition: background-color var(--motion-pop) var(--ease-press),
border-color var(--motion-pop) var(--ease-press);
}
.m-grid-icon svg {
width: 22px;
height: 22px;
}
.m-grid-label {
font-size: 12px;
line-height: 1.3;
color: var(--text-primary);
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
transition: color var(--motion-pop) var(--ease-press);
}
.m-auth {
padding-top: 8px;
}
.m-auth-brand {
text-align: center;
margin: 24px 0 20px;
}
.m-brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: 16px;
background: var(--action);
color: var(--text-inverse);
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
}
.m-auth-brand h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.m-auth-brand p {
margin: 4px 0 0;
font-size: var(--font-size-label);
color: var(--text-secondary);
}
.m-auth-tabs {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.m-auth-tab {
flex: 1;
height: 44px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--surface);
color: var(--text-secondary);
font-size: var(--font-size-body);
cursor: pointer;
}
.m-auth-tab.active {
background: var(--action-soft);
border-color: var(--action);
color: var(--action);
font-weight: 600;
}
.m-form-field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.m-form-field span {
font-size: var(--font-size-label);
color: var(--text-secondary);
}
.m-form-field input {
height: 44px;
padding: 0 12px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--surface);
color: var(--text-primary);
font-size: 14px;
}
.m-form-field input:focus {
outline: none;
border-color: var(--action);
}
.m-auth-error {
margin: 0 0 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--market-up-soft);
color: var(--market-up);
font-size: var(--font-size-label);
}
.m-btn-primary {
width: 100%;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 0;
border-radius: 8px;
background: var(--action);
color: var(--text-inverse);
font-size: var(--font-size-body);
font-weight: 600;
cursor: pointer;
transition: transform var(--motion-press-release) var(--ease-press),
background-color var(--motion-press-release) var(--ease-press);
}
.m-btn-primary:active {
background: var(--action-hover);
transform: scale(0.98);
transition-duration: var(--motion-press);
}
.m-btn-primary:disabled {
opacity: 0.5;
cursor: default;
pointer-events: none;
}
.m-btn-spinner {
display: inline-block;
width: 14px;
height: 14px;
flex: 0 0 auto;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: m-spin 800ms linear infinite;
}
@keyframes m-spin {
to { transform: rotate(360deg); }
}
.m-tabbar {
position: fixed;
left: 0;
right: 0;
bottom: var(--m-keyboard-inset);
z-index: 60;
display: flex;
align-items: stretch;
height: calc(var(--mobile-tabbar-height) + var(--mobile-safe-bottom));
padding-bottom: var(--mobile-safe-bottom);
background: var(--surface);
border-top: 1px solid var(--border);
transition: background-color var(--motion-pop) var(--ease-press),
border-color var(--motion-pop) var(--ease-press);
}
.m-tabbar-item {
flex: 1;
min-width: 0;
min-height: var(--mobile-tabbar-height);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
border: 0;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: transform var(--motion-press-release) var(--ease-press),
background-color var(--motion-press-release) var(--ease-press),
color var(--motion-pop) var(--ease-press);
}
.m-tabbar-item:active {
background: var(--surface-hover);
transform: scale(0.94);
transition-duration: var(--motion-press), var(--motion-press), var(--motion-pop);
}
.m-tabbar-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex: 0 0 auto;
transition: transform var(--motion-press-release) var(--ease-press);
}
.m-tabbar-item:active .m-tabbar-icon {
transform: scale(0.88);
transition-duration: var(--motion-press);
}
.m-tabbar-label {
font-size: var(--mobile-tabbar-label-size);
line-height: 1.2;
color: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.m-tabbar-item.active {
color: var(--action);
}
.m-theme-section {
margin-bottom: 12px;
}
.m-theme-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 12px;
background: var(--surface);
box-shadow: var(--elevation-card);
transition: background-color var(--motion-pop) var(--ease-press),
border-color var(--motion-pop) var(--ease-press);
}
.m-theme-row-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 12px;
background: var(--action-soft);
color: var(--action);
flex: 0 0 auto;
transition: background-color var(--motion-pop) var(--ease-press),
border-color var(--motion-pop) var(--ease-press);
}
.m-theme-row-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.m-theme-row-body strong {
font-size: var(--font-size-card-title);
font-weight: 600;
line-height: 1.4;
}
.m-theme-row-body small {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.4;
}
.m-theme-switch {
position: relative;
flex: 0 0 auto;
width: 48px;
height: 28px;
padding: 0;
border: 0;
border-radius: 999px;
background: var(--border-strong);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.m-theme-switch[aria-checked="true"] {
background: var(--action);
}
.m-theme-switch-thumb {
position: absolute;
top: 3px;
left: 3px;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--text-inverse);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transition: transform 0.15s ease;
}
.m-theme-switch[aria-checked="true"] .m-theme-switch-thumb {
transform: translateX(20px);
}
@media (prefers-reduced-motion: reduce) {
.m-motion-rise-in,
.m-motion-push-in,
.m-motion-pop-in {
animation-name: m-fade-in;
animation-duration: 100ms;
}
.m-motion-fade-in {
animation-duration: 100ms;
}
.m-grid-item:active,
.m-header-btn:active,
.m-btn-primary:active,
.m-tabbar-item:active {
transform: none;
}
.m-tabbar-item:active .m-tabbar-icon {
transform: none;
}
}
-101
View File
@@ -1,101 +0,0 @@
#m-app {
color-scheme: light;
--canvas: #f4f5f7;
--surface: #ffffff;
--surface-muted: #f2f3f5;
--surface-subtle: #f8f9fb;
--surface-raised: #ffffff;
--surface-hover: #f2f3f5;
--surface-selected: #eaf1fe;
--border: #eceded;
--border-strong: #dee0e3;
--border-subtle: #eef0f3;
--text-primary: #1f2329;
--text-secondary: #646a73;
--text-tertiary: #8f959e;
--text-inverse: #ffffff;
--action: #3370ff;
--action-hover: #2b5fd9;
--action-soft: #eaf1fe;
--market-up: #e04536;
--market-up-soft: #fdecea;
--market-down: #16a34a;
--market-down-soft: #e9f7ee;
--warning: #b45309;
--warning-soft: #fdf3e3;
--chart-up: #c93f45;
--chart-down: #087a55;
--elevation-card: 0 1px 2px rgba(31, 35, 41, .04);
--elevation-float: 0 12px 32px rgba(0, 0, 0, .14);
--backdrop: rgba(17, 24, 39, .48);
--motion-press: 100ms;
--motion-press-release: 160ms;
--motion-fade: 120ms;
--motion-enter: 260ms;
--motion-exit: 200ms;
--motion-pop: 240ms;
--ease-enter: cubic-bezier(0.22, 1, 0.36, 1);
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
--ease-press: ease-out;
--mobile-header-height: 52px;
--mobile-touch-size: 44px;
--mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
--mobile-page-padding: 12px;
--mobile-chart-height: 240px;
--mobile-chart-height-compact: 168px;
--mobile-table-row-height: 44px;
--mobile-table-header-height: 40px;
--mobile-icon-cell: 78px;
--mobile-sheet-radius: 12px;
--mobile-min-width: 320px;
--mobile-tabbar-height: 56px;
--mobile-tabbar-label-size: 11px;
--m-keyboard-inset: 0px;
--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-card-title: 15px;
--font-size-page-title: 18px;
--font-size-metric: 24px;
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
font-size: 14px;
}
#m-app[data-theme="dark"] {
color-scheme: dark;
--canvas: #141519;
--surface: #232529;
--surface-muted: #2a2d33;
--surface-subtle: #202329;
--surface-raised: #232529;
--surface-hover: #2a2d33;
--surface-selected: #2b3b58;
--border: #2b2e34;
--border-strong: #3a3e47;
--border-subtle: #2b2e34;
--text-primary: #e8eaed;
--text-secondary: #a9adb3;
--text-tertiary: #7c828a;
--text-inverse: #ffffff;
--action: #5b8def;
--action-hover: #7ba5f5;
--action-soft: #2b3b58;
--market-up: #f26762;
--market-up-soft: #3d2829;
--market-down: #43bc8a;
--market-down-soft: #22362c;
--warning: #e2ad58;
--warning-soft: #3d3220;
--chart-up: #f06d73;
--chart-down: #43bc8a;
--elevation-card: 0 1px 2px rgba(0, 0, 0, .28);
--elevation-float: 0 12px 32px rgba(0, 0, 0, .46);
--backdrop: rgba(0, 0, 0, .66);
}
-46
View File
@@ -1,46 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<title>小白复盘</title>
<link rel="stylesheet" href="css/tokens.css">
<link rel="stylesheet" href="css/shell.css">
<link rel="stylesheet" href="css/features.css">
</head>
<body>
<div id="m-app" data-theme="light">
<script>
(() => {
let theme = "light";
try {
theme = localStorage.getItem("xiaobaiTheme") === "dark" ? "dark" : "light";
} catch (_error) {
theme = "light";
}
document.getElementById("m-app").setAttribute("data-theme", theme);
})();
</script>
<div id="m-boot-splash" aria-hidden="true">
<span class="m-brand-mark"></span>
</div>
<header id="m-header" class="m-header">
<button id="m-back" class="m-header-btn" type="button" aria-label="返回" hidden>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
</button>
<h1 id="m-title" class="m-title">小白复盘</h1>
<div id="m-actions" class="m-actions"></div>
</header>
<main id="m-view" class="m-view"></main>
<nav id="m-tabbar" class="m-tabbar" aria-label="主导航" hidden></nav>
</div>
<script src="config/nav.config.js"></script>
<script src="../shared/api.js"></script>
<script src="js/api.js"></script>
<script src="js/session.js"></script>
<script src="js/router.js"></script>
<script src="js/pages.js"></script>
<script src="js/boot.js"></script>
</body>
</html>
-30
View File
@@ -1,30 +0,0 @@
(function (global) {
"use strict";
// 手机端 API 薄封装:唯一 fetch 出口收敛到 shared/api.jsXiaobaiAPI)。
// 本文件不再直接调用 fetch,仅负责把 CSRF Token 配置进共享出口并转发请求,
// 保持既有 MobileAPI.request(url[, method, body]) 调用面不变。
let csrfToken = "";
function setCsrfToken(token) {
csrfToken = token || "";
global.XiaobaiAPI.configure({
csrfToken: function () {
return csrfToken;
}
});
}
async function request(url, method, body) {
return global.XiaobaiAPI.request(url, method, body);
}
// 流式聊天复用共享层的 streamNdjson:每解析一行 NDJSON 就回调 onEvent
// 调用方通过 { signal } 传入 AbortController 以支持聊天页的「停止」按钮。
function streamNdjson(url, options) {
return global.XiaobaiAPI.streamNdjson(url, options || {});
}
global.MobileAPI = { setCsrfToken: setCsrfToken, request: request, streamNdjson: streamNdjson };
})(window);
-86
View File
@@ -1,86 +0,0 @@
(function (global) {
"use strict";
const THEME_KEY = "xiaobaiTheme";
function readTheme() {
try {
return global.localStorage.getItem(THEME_KEY) === "dark" ? "dark" : "light";
} catch (_error) {
return "light";
}
}
function applyTheme(theme) {
const normalized = theme === "dark" ? "dark" : "light";
const app = document.getElementById("m-app");
app.dataset.theme = normalized;
app.style.colorScheme = normalized;
document.body.style.backgroundColor = normalized === "dark" ? "#141519" : "#f4f5f7";
return normalized;
}
function toggle() {
const next = readTheme() === "dark" ? "light" : "dark";
try {
global.localStorage.setItem(THEME_KEY, next);
} catch (_error) {
// Theme still applies for the current page when storage is unavailable.
}
applyTheme(next);
}
global.MobileTheme = { readTheme: readTheme, applyTheme: applyTheme, toggle: toggle };
function syncKeyboardInset() {
const app = document.getElementById("m-app");
const visual = global.visualViewport;
if (!visual) {
app.style.setProperty("--m-keyboard-inset", "0px");
return;
}
// 键盘高度 = 布局视口高度 - 可视视口高度。iOS 弹键盘时会同时抬升 offsetTop,
// 若再扣 offsetTop 会把 inset 算成 0,导致输入区/底栏不避让,故只按高度差计算。
const inset = Math.max(0, global.innerHeight - visual.height);
app.style.setProperty("--m-keyboard-inset", inset + "px");
}
function bindViewport() {
const visual = global.visualViewport;
if (visual) {
visual.addEventListener("resize", syncKeyboardInset);
visual.addEventListener("scroll", syncKeyboardInset);
}
syncKeyboardInset();
}
async function boot() {
applyTheme(readTheme());
bindViewport();
let session = { authenticated: false };
try {
session = await global.MobileSession.me();
} catch (_error) {
session = { authenticated: false };
}
const hash = global.location.hash || "";
if (!session.authenticated) {
if (!/^#\/?auth/.test(hash)) global.location.replace("#/auth");
} else if (/^#\/?auth/.test(hash)) {
global.location.replace("#/hub/market");
}
removeBootSplash();
global.MobileRouter.init();
}
function removeBootSplash() {
const splash = document.getElementById("m-boot-splash");
if (splash) splash.remove();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
})(window);
File diff suppressed because it is too large Load Diff
-427
View File
@@ -1,427 +0,0 @@
(function (global) {
"use strict";
const ICONS = {
"chevron-left": '<path d="m15 18-6-6 6-6"/>',
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
"sun": '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
"moon": '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
"bar-chart-3": '<path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>',
"wand-2": '<path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72Z"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/>',
"notebook-pen": '<path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"/><path d="M2 6h4"/><path d="M2 10h4"/><path d="M2 14h4"/><path d="M2 18h4"/><path d="m21.38 5.63-3-3a1 1 0 0 0-1.42 0l-5.01 5.01a2 2 0 0 0-.5.85l-.84 2.87a.5.5 0 0 0 .62.62l2.87-.84a2 2 0 0 0 .85-.5z"/>',
"message-square": '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
"settings": '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
"inbox": '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
"activity": '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
"trending-up": '<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/>',
"trending-down": '<polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/>',
"zap": '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
"history": '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
"chart-line": '<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>',
"layers": '<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>',
"refresh-cw": '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
"gavel": '<path d="m14 13-7.5 7.5c-.83.83-2.17.83-3 0 0 0 0 0 0 0a2.12 2.12 0 0 1 0-3L11 10"/><path d="m16 16 6-6"/><path d="m8 8 6-6"/><path d="m9 7 8 8"/><path d="m21 11-8-8"/>',
"book-open": '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>',
"flame": '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
"crown": '<path d="M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.735H5.81a1 1 0 0 1-.957-.735L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z"/><path d="M5 21h14"/>',
"filter": '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>',
"target": '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
"bot": '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>',
"star": '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
"scroll-text": '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
"calendar-check": '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="m9 16 2 2 4-4"/>',
"sticky-note": '<path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8Z"/><path d="M15 3v4a2 2 0 0 0 2 2h4"/>',
"bell": '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
"user": '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
"lock": '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
"gem": '<path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/>',
"sliders-horizontal": '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
"users": '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>'
};
function icon(name, size) {
const body = ICONS[name] || "";
return '<svg width="' + (size || 22) + '" height="' + (size || 22) + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + body + "</svg>";
}
function escapeHtml(value) {
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
});
}
const DEFAULT_HASH = "#/hub/market";
const stack = [];
let internalNav = 0;
let authMode = "login";
const VIEW_MOTION_CLASSES = ["m-motion-fade-in", "m-motion-push-in", "m-motion-pop-in", "m-motion-boot-in"];
function applyViewMotion(motion) {
const view = document.getElementById("m-view");
VIEW_MOTION_CLASSES.forEach(function (cls) {
view.classList.remove(cls);
});
void view.offsetWidth;
view.classList.add(motion || "m-motion-fade-in");
}
function currentHash() {
let hash = window.location.hash || "";
if (!hash || hash === "#" || hash === "#/") return DEFAULT_HASH;
if (hash.charAt(0) !== "#") hash = "#" + hash;
return hash;
}
function routeOf(hash) {
const path = hash.replace(/^#\/?/, "");
const parts = path.split("/").filter(Boolean);
return { name: parts[0] || "home", params: parts.slice(1) };
}
function currentRoute() {
return routeOf(stack.length ? stack[stack.length - 1] : currentHash());
}
function entryKeyOfRoute(route) {
if (route.name === "hub") return route.params[0];
if (route.name === "feature") return route.params[0];
if (route.name === "assistant") return "assistant";
return null;
}
function entryRoute(key) {
return key === "assistant" ? "#/assistant/chat" : "#/hub/" + key;
}
function setHash(hash) {
internalNav++;
window.location.hash = hash;
}
function navigate(hash) {
stack.push(hash);
setHash(hash);
render("m-motion-push-in");
}
function replace(hash, motion) {
if (stack.length) stack[stack.length - 1] = hash;
else stack.push(hash);
setHash(hash);
render(motion);
}
function resetStack(hash) {
stack.length = 0;
stack.push(hash);
setHash(hash);
render();
}
function parentRoute(route) {
if (route.name === "feature") return "#/hub/" + route.params[0];
return null;
}
function back() {
if (stack.length > 1) {
stack.pop();
setHash(stack[stack.length - 1]);
render("m-motion-pop-in");
return;
}
const parent = parentRoute(currentRoute());
if (parent) replace(parent, "m-motion-pop-in");
}
function switchEntry(key) {
const route = currentRoute();
const current = entryKeyOfRoute(route);
if (current === key) {
if (route.name === "feature") {
resetStack(entryRoute(key));
}
return;
}
resetStack(entryRoute(key));
}
function updateHeader(options) {
document.getElementById("m-title").textContent = options.title || "小白复盘";
document.getElementById("m-back").hidden = !options.back;
document.getElementById("m-actions").innerHTML = options.actions || "";
}
function placeholderHtml(title, subtitle) {
return '<div class="m-placeholder m-motion-rise-in">' +
'<span class="m-placeholder-icon">' + icon("inbox", 26) + "</span>" +
"<h2>" + escapeHtml(title) + "</h2>" +
"<p>" + escapeHtml(subtitle || "该页面将在后续批次实现,返回即可继续浏览。") + "</p>" +
"</div>";
}
function findFeatureLabel(key) {
const hubs = global.MobileNav.hubs;
for (const hubKey in hubs) {
const items = hubs[hubKey].items || [];
for (const item of items) {
if (item.key === key) return item.label;
}
}
return null;
}
function themeToggleSection() {
const dark = document.getElementById("m-app").dataset.theme === "dark";
return '<div class="m-theme-section">' +
'<div class="m-theme-row">' +
'<span class="m-theme-row-icon">' + icon(dark ? "moon" : "sun") + "</span>" +
'<span class="m-theme-row-body"><strong>外观主题</strong><small>' + (dark ? "当前:夜间模式" : "当前:日间模式") + "</small></span>" +
'<button class="m-theme-switch" type="button" data-theme-toggle role="switch" aria-checked="' + (dark ? "true" : "false") + '" aria-label="切换日间/夜间模式"><span class="m-theme-switch-thumb"></span></button>' +
"</div></div>";
}
function syncThemeToggleUI() {
const dark = document.getElementById("m-app").dataset.theme === "dark";
const toggle = document.querySelector("[data-theme-toggle]");
if (toggle) toggle.setAttribute("aria-checked", dark ? "true" : "false");
const rowIcon = document.querySelector(".m-theme-row-icon");
if (rowIcon) rowIcon.innerHTML = icon(dark ? "moon" : "sun");
const rowBodySmall = document.querySelector(".m-theme-row-body small");
if (rowBodySmall) rowBodySmall.textContent = dark ? "当前:夜间模式" : "当前:日间模式";
}
function visibleHubItems(hub) {
const items = hub && hub.items ? hub.items : [];
return items.filter(function (item) {
return !item.adminOnly || global.MobileSession.isAdmin();
});
}
function renderHub(key) {
const hub = global.MobileNav.hubs[key];
if (!hub) {
replace(DEFAULT_HASH);
return;
}
const items = visibleHubItems(hub);
updateHeader({ title: hub.title, back: false });
const section = key === "system" ? themeToggleSection() : "";
document.getElementById("m-view").innerHTML =
section +
'<ul class="m-hub-grid">' +
items.map(function (item) {
return '<li>' +
'<button class="m-grid-item" type="button" data-route="#/feature/' + item.key + '">' +
'<span class="m-grid-icon">' + icon(item.icon) + "</span>" +
'<span class="m-grid-label">' + escapeHtml(item.label) + "</span>" +
"</button></li>";
}).join("") +
"</ul>";
}
function renderFeature(key) {
if (global.MobilePages && global.MobilePages.has(key)) {
global.MobilePages.render(key);
return;
}
const title = findFeatureLabel(key) || key;
updateHeader({ title: title, back: true });
document.getElementById("m-view").innerHTML = placeholderHtml(title, "该功能页将在后续批次实现。");
}
function renderAssistant() {
if (global.MobilePages && global.MobilePages.has("assistant/chat")) {
global.MobilePages.render("assistant/chat");
return;
}
updateHeader({ title: "复盘助手", back: false });
document.getElementById("m-view").innerHTML = placeholderHtml("复盘助手", "聊天工作台将在后续批次实现。");
}
function renderAuth() {
updateHeader({ title: "小白复盘", back: false, actions: "" });
document.getElementById("m-view").innerHTML = [
'<div class="m-auth">',
'<div class="m-auth-brand">',
'<span class="m-brand-mark">复</span>',
'<h2>小白复盘</h2>',
'<p>登录后进入你的复盘空间</p>',
"</div>",
'<div class="m-auth-tabs">',
'<button class="m-auth-tab active" type="button" data-auth-mode="login">登录</button>',
'<button class="m-auth-tab" type="button" data-auth-mode="register">注册</button>',
"</div>",
'<form id="m-auth-form">',
'<label class="m-form-field"><span>账号名</span><input id="m-auth-username" type="text" minlength="3" maxlength="30" autocomplete="username" required></label>',
'<label class="m-form-field"><span>密码</span><input id="m-auth-password" type="password" minlength="8" maxlength="128" autocomplete="current-password" required></label>',
'<label class="m-form-field" id="m-auth-confirm-field" hidden><span>确认密码</span><input id="m-auth-confirm" type="password" minlength="8" maxlength="128" autocomplete="new-password"></label>',
'<p class="m-auth-error" id="m-auth-error" hidden></p>',
'<button class="m-btn-primary" id="m-auth-submit" type="submit"><span class="m-btn-spinner" aria-hidden="true" hidden></span><span class="m-btn-label">登录</span></button>',
"</form>",
"</div>"
].join("");
authMode = "login";
bindAuth();
}
function setAuthMode(mode) {
authMode = mode === "register" ? "register" : "login";
document.querySelectorAll("[data-auth-mode]").forEach(function (button) {
button.classList.toggle("active", button.dataset.authMode === authMode);
});
document.getElementById("m-auth-confirm-field").hidden = authMode !== "register";
document.getElementById("m-auth-confirm").required = authMode === "register";
document.getElementById("m-auth-password").autocomplete = authMode === "register" ? "new-password" : "current-password";
const submitLabel = document.querySelector("#m-auth-submit .m-btn-label");
if (submitLabel) submitLabel.textContent = authMode === "register" ? "注册并进入" : "登录";
document.getElementById("m-auth-error").hidden = true;
}
function bindAuth() {
document.querySelectorAll("[data-auth-mode]").forEach(function (button) {
button.addEventListener("click", function () { setAuthMode(button.dataset.authMode); });
});
document.getElementById("m-auth-form").addEventListener("submit", submitAuth);
}
function showAuthError(element, message) {
element.textContent = message;
element.classList.remove("m-motion-fade-in");
void element.offsetWidth;
element.classList.add("m-motion-fade-in");
element.hidden = false;
}
async function submitAuth(event) {
event.preventDefault();
const username = document.getElementById("m-auth-username").value.trim();
const password = document.getElementById("m-auth-password").value;
const errorElement = document.getElementById("m-auth-error");
if (authMode === "register" && password !== document.getElementById("m-auth-confirm").value) {
showAuthError(errorElement, "两次输入的密码不一致。");
return;
}
const submit = document.getElementById("m-auth-submit");
const spinner = submit.querySelector(".m-btn-spinner");
const label = submit.querySelector(".m-btn-label");
submit.disabled = true;
spinner.hidden = false;
label.textContent = authMode === "register" ? "注册中…" : "登录中…";
try {
if (authMode === "register") {
await global.MobileSession.register(username, password);
} else {
await global.MobileSession.login(username, password);
}
replace(DEFAULT_HASH);
} catch (error) {
showAuthError(errorElement, error.message || "账号操作失败");
} finally {
submit.disabled = false;
spinner.hidden = true;
label.textContent = authMode === "register" ? "注册并进入" : "登录";
}
}
function buildTabbar() {
document.getElementById("m-tabbar").innerHTML = global.MobileNav.entries.map(function (entry) {
return '<button class="m-tabbar-item" type="button" data-tab="' + entry.key + '">' +
'<span class="m-tabbar-icon">' + icon(entry.icon, 24) + "</span>" +
'<span class="m-tabbar-label">' + escapeHtml(entry.title) + "</span>" +
"</button>";
}).join("");
}
function updateTabbar(activeKey) {
document.querySelectorAll("#m-tabbar .m-tabbar-item").forEach(function (button) {
const active = button.dataset.tab === activeKey;
button.classList.toggle("active", active);
if (active) button.setAttribute("aria-current", "page");
else button.removeAttribute("aria-current");
});
}
function render(motion) {
const route = currentRoute();
document.getElementById("m-view").classList.remove("m-view-feature");
if (route.name === "home") {
replace(DEFAULT_HASH);
return;
}
const tabbar = document.getElementById("m-tabbar");
if (route.name === "auth") {
tabbar.hidden = true;
document.getElementById("m-app").dataset.tabbar = "false";
} else {
tabbar.hidden = false;
document.getElementById("m-app").dataset.tabbar = "true";
updateTabbar(entryKeyOfRoute(route));
}
if (route.name === "hub") renderHub(route.params[0]);
else if (route.name === "feature") renderFeature(route.params.join("/"));
else if (route.name === "assistant") renderAssistant();
else if (route.name === "auth") renderAuth();
applyViewMotion(motion);
}
function bindGlobalEvents() {
document.getElementById("m-view").addEventListener("click", function (event) {
const entry = event.target.closest("[data-route]");
if (entry) {
navigate(entry.dataset.route);
}
});
document.getElementById("m-tabbar").addEventListener("click", function (event) {
const item = event.target.closest("[data-tab]");
if (item) switchEntry(item.dataset.tab);
});
document.getElementById("m-back").addEventListener("click", back);
document.addEventListener("click", function (event) {
const toggle = event.target.closest("[data-theme-toggle]");
if (!toggle) return;
global.MobileTheme.toggle();
syncThemeToggleUI();
});
window.addEventListener("hashchange", function () {
if (internalNav > 0) {
internalNav--;
return;
}
const hash = currentHash();
if (stack.length > 1 && hash === stack[stack.length - 2]) {
stack.pop();
render("m-motion-pop-in");
} else if (hash !== stack[stack.length - 1]) {
stack.push(hash);
render("m-motion-push-in");
}
});
}
function init() {
bindGlobalEvents();
buildTabbar();
stack.length = 0;
const raw = window.location.hash || "";
if (!raw || raw === "#" || raw === "#/") {
stack.push(DEFAULT_HASH);
setHash(DEFAULT_HASH);
} else {
stack.push(currentHash());
}
render("m-motion-boot-in");
}
global.MobileRouter = {
init: init,
navigate: navigate,
replace: replace,
back: back,
render: render,
currentRoute: currentRoute,
updateHeader: updateHeader
};
})(window);
-51
View File
@@ -1,51 +0,0 @@
(function (global) {
"use strict";
const state = { user: null, csrfToken: "", authenticated: false };
function applySession(payload) {
state.user = payload.user || null;
state.csrfToken = payload.csrf_token || "";
state.authenticated = Boolean(payload.authenticated);
global.MobileAPI.setCsrfToken(state.csrfToken);
return state;
}
async function me() {
const payload = await global.MobileAPI.request("/api/auth/me");
return applySession(payload);
}
async function login(username, password) {
const payload = await global.MobileAPI.request("/api/auth/login", "POST", {
username: username,
password: password
});
return applySession(payload);
}
async function register(username, password) {
const payload = await global.MobileAPI.request("/api/auth/register", "POST", {
username: username,
password: password
});
return applySession(payload);
}
async function logout() {
try {
await global.MobileAPI.request("/api/auth/logout", "POST", {});
} finally {
state.user = null;
state.csrfToken = "";
state.authenticated = false;
global.MobileAPI.setCsrfToken("");
}
}
function isAdmin() {
return Boolean(state.user && state.user.role === "admin");
}
global.MobileSession = { state: state, me: me, login: login, register: register, logout: logout, isAdmin: isAdmin };
})(window);
+14 -14
View File
@@ -42,15 +42,15 @@
]; ];
const fragments = [ const fragments = [
["pools", "/pages/pools/page.html?v=20260820-1", ["limitPool", "brokenView", "downView", "yesterdayView", "performanceView"]], ["pools", "/pages/pools/page.html?v=20260803-1", ["limitPool", "brokenView", "downView", "yesterdayView", "performanceView"]],
["sentiment", "/pages/sentiment/page.html?v=20260803-1", ["sentimentCycleView"]], ["sentiment", "/pages/sentiment/page.html?v=20260803-1", ["sentimentCycleView"]],
["heaven", "/pages/heaven/page.html?v=20260803-1", ["heavenView"]], ["heaven", "/pages/heaven/page.html?v=20260803-1", ["heavenView"]],
["ladder", "/pages/ladder/page.html?v=20260820-1", ["ladderView"]], ["ladder", "/pages/ladder/page.html?v=20260803-1", ["ladderView"]],
["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]], ["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]],
["mentor", "/pages/mentor/page.html?v=20260820-1", ["mentorView"]], ["mentor", "/pages/mentor/page.html?v=20260806-2", ["mentorView"]],
["rotation", "/pages/rotation/page.html?v=20260820-1", ["rotationView"]], ["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]],
["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]], ["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]],
["themes", "/pages/themes/page.html?v=20260820-1", ["themeLibraryView"]], ["themes", "/pages/themes/page.html?v=20260803-1", ["themeLibraryView"]],
["popularity", "/pages/popularity/page.html?v=20260803-1", ["popularityView"]], ["popularity", "/pages/popularity/page.html?v=20260803-1", ["popularityView"]],
["dragon_tiger", "/pages/dragon-tiger/page.html?v=20260803-1", ["dragonView"]], ["dragon_tiger", "/pages/dragon-tiger/page.html?v=20260803-1", ["dragonView"]],
["review", "/pages/review/page.html?v=20260803-1", ["reviewWorkspaceView"]], ["review", "/pages/review/page.html?v=20260803-1", ["reviewWorkspaceView"]],
@@ -66,7 +66,7 @@
"/shared/components.js?v=20260729-1", "/shared/components.js?v=20260729-1",
"/pages/runtime.js?v=20260729-1", "/pages/runtime.js?v=20260729-1",
"/pages/sentiment/page.js?v=20260729-1", "/pages/sentiment/page.js?v=20260729-1",
"/pages/pools/page.js?v=20260820-1", "/pages/pools/page.js?v=20260729-1",
"/pages/market/breadth.js?v=20260803-1", "/pages/market/breadth.js?v=20260803-1",
"/pages/market/charts.js?v=20260803-1", "/pages/market/charts.js?v=20260803-1",
"/pages/market/entity-detail.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/preview.js?v=20260806-1",
"/pages/market/search.js?v=20260803-1", "/pages/market/search.js?v=20260803-1",
"/pages/market/bindings.js?v=20260803-1", "/pages/market/bindings.js?v=20260803-1",
"/pages/ladder/page.js?v=20260820-1", "/pages/ladder/page.js?v=20260729-1",
"/pages/rotation/page.js?v=20260820-1", "/pages/rotation/page.js?v=20260729-1",
"/pages/auction/page.js?v=20260729-1", "/pages/auction/page.js?v=20260729-1",
"/pages/themes/page.js?v=20260820-1", "/pages/themes/page.js?v=20260729-1",
"/pages/popularity/page.js?v=20260820-1", "/pages/popularity/page.js?v=20260729-1",
"/pages/dragon-tiger/page.js?v=20260820-1", "/pages/dragon-tiger/page.js?v=20260806-1",
"/pages/screener/page.js?v=20260806-1", "/pages/screener/page.js?v=20260806-1",
"/pages/mentor/page.js?v=20260820-1", "/pages/mentor/page.js?v=20260806-2",
"/pages/heaven/page.js?v=20260729-1", "/pages/heaven/page.js?v=20260729-1",
"/pages/review/page.js?v=20260729-1", "/pages/review/page.js?v=20260729-1",
"/shared/state.js?v=20260729-1", "/shared/state.js?v=20260729-1",
"/shared/api.js?v=20260729-1", "/shared/api.js?v=20260729-1",
"/shared/shell.js?v=20260820-2", "/shared/shell.js?v=20260806-1",
"/shared/export.js?v=20260731-1", "/shared/export.js?v=20260731-1",
"/pages/heaven/loading-v2.js?v=20260728-2", "/pages/heaven/loading-v2.js?v=20260728-2",
"/shared/context.js?v=20260804-1", "/shared/context.js?v=20260804-1",
@@ -94,7 +94,7 @@
"/shared/application.js?v=20260803-1", "/shared/application.js?v=20260803-1",
"/shared/table.js?v=20260803-1", "/shared/table.js?v=20260803-1",
"/shared/theme.js?v=20260803-1", "/shared/theme.js?v=20260803-1",
"/shared/dashboard.js?v=20260820-1", "/shared/dashboard.js?v=20260803-1",
"/shared/session.js?v=20260803-1", "/shared/session.js?v=20260803-1",
"/shared/admin.js?v=20260803-1", "/shared/admin.js?v=20260803-1",
"/app.js?v=20260803-2", "/app.js?v=20260803-2",
+40 -47
View File
@@ -1,8 +1,8 @@
/* Canonical CSS owner: auction. Historical layers consolidated 2026-08-02. */ /* Canonical CSS owner: auction. Historical layers consolidated 2026-08-02. */
#auctionView .section-toolbar h2 { #auctionView .section-toolbar h2 {
font-size: var(--font-size-page-title); font-size: 17px;
font-weight: var(--font-weight-semibold); font-weight: 800;
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@@ -354,9 +354,9 @@
} }
.auction-phase-notice[data-phase="archive"] { .auction-phase-notice[data-phase="archive"] {
background: var(--surface-muted); background: rgb(238, 242, 246);
color: var(--text-2); color: rgb(89, 101, 116);
} }
.auction-phase-marker { .auction-phase-marker {
@@ -374,7 +374,7 @@
.auction-table { .auction-table {
min-width: 860px; min-width: 860px;
font-size: var(--font-size-table); font-size: 12.5px;
} }
.auction-table tbody tr:hover td { .auction-table tbody tr:hover td {
@@ -462,7 +462,7 @@
transition: opacity var(--motion-fast) ease; transition: opacity var(--motion-fast) ease;
background: var(--accent-soft); background: rgb(201, 214, 238);
} }
.auction-amount-average { .auction-amount-average {
@@ -522,7 +522,7 @@
} }
.auction-table td { .auction-table td {
height: 40px; height: 44px;
padding-right: 12px; padding-right: 12px;
@@ -530,7 +530,7 @@
border-right: 0px; border-right: 0px;
border-bottom-color: var(--border); border-bottom-color: rgb(232, 237, 241);
} }
.auction-table th { .auction-table th {
@@ -540,13 +540,13 @@
border-right: 0px; border-right: 0px;
height: 36px; height: 38px;
color: var(--text-2); color: rgb(107, 114, 128);
font-size: var(--font-size-caption); font-size: 12px;
border-bottom-color: var(--border); border-bottom-color: rgb(232, 237, 241);
background: var(--table-header); background: var(--table-header);
} }
@@ -694,9 +694,9 @@
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-page-title); font-size: 17px;
font-weight: var(--font-weight-semibold); font-weight: 800;
letter-spacing: 0px; letter-spacing: 0px;
} }
@@ -812,13 +812,13 @@
100% { 100% {
opacity: 0.42; opacity: 0.42;
box-shadow: 0 0 0 0 var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.18) 0px 0px 0px 0px;
} }
50% { 50% {
opacity: 1; opacity: 1;
box-shadow: 0 0 0 4px transparent; box-shadow: rgba(37, 99, 235, 0) 0px 0px 0px 4px;
} }
} }
@@ -896,9 +896,7 @@
.auction-export-button, .auction-export-button,
.auction-refresh-button { .auction-refresh-button {
min-height: 32px; min-height: 30px;
height: 32px;
display: inline-flex; display: inline-flex;
@@ -908,17 +906,17 @@
gap: 5px; gap: 5px;
padding: 0 12px; padding: 5px 11px;
border: 1px solid var(--border-strong); border: 1px solid var(--r2-line);
border-radius: 8px; border-radius: 7px;
background: var(--surface); background: var(--surface);
color: var(--text-primary); color: var(--text-primary);
font-size: var(--font-size-label); font-size: 12px;
font-weight: 500; font-weight: 500;
@@ -1146,7 +1144,7 @@
.auction-search-v2 { .auction-search-v2 {
min-width: 0px; min-width: 0px;
height: 32px; height: 31px;
display: flex; display: flex;
@@ -1156,9 +1154,9 @@
padding: 0px 9px; padding: 0px 9px;
border: 1px solid var(--border-strong); border: 1px solid var(--r2-line);
border-radius: 8px; border-radius: 7px;
background: var(--surface); background: var(--surface);
} }
@@ -1195,7 +1193,7 @@
.auction-filter-segments button:focus-visible, .auction-filter-segments button:focus-visible,
.auction-refresh-button:focus-visible, .auction-refresh-button:focus-visible,
.auction-tabs-v2 button:focus-visible { .auction-tabs-v2 button:focus-visible {
outline: 2px solid var(--accent); outline: rgba(37, 99, 235, 0.28) solid 2px;
outline-offset: 2px; outline-offset: 2px;
} }
@@ -1221,7 +1219,7 @@
border-collapse: collapse; border-collapse: collapse;
font-size: var(--font-size-table); font-size: 12.5px;
} }
.auction-table-v2 thead th { .auction-table-v2 thead th {
@@ -1231,16 +1229,12 @@
z-index: 2; z-index: 2;
height: 36px; height: 35px;
padding: 0 12px;
background: var(--table-header); background: var(--table-header);
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-caption);
font-weight: 600; font-weight: 600;
text-align: left; text-align: left;
@@ -1248,11 +1242,6 @@
white-space: nowrap; 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 { .auction-table-v2 thead th.number {
text-align: right; text-align: right;
} }
@@ -1280,9 +1269,7 @@
} }
.auction-table-v2 tbody td { .auction-table-v2 tbody td {
height: 40px; height: 47px;
padding: 0 12px;
border-bottom: 1px solid var(--r2-line-soft); border-bottom: 1px solid var(--r2-line-soft);
@@ -1832,7 +1819,7 @@
border-radius: 3px 3px 0px 0px; border-radius: 3px 3px 0px 0px;
background: var(--accent-soft); background: rgb(201, 214, 238);
transition: opacity 180ms, transform 180ms; transition: opacity 180ms, transform 180ms;
@@ -1928,7 +1915,7 @@
border-radius: 2px; border-radius: 2px;
background: var(--accent-soft); background: rgb(201, 214, 238);
} }
.auction-volume-legend i.current { .auction-volume-legend i.current {
@@ -2102,6 +2089,10 @@
overflow: hidden; overflow: hidden;
} }
body[data-active-view="auctionView"] .overview-strip {
flex: 0 0 auto;
}
body[data-active-view="auctionView"] #auctionView.active-view { body[data-active-view="auctionView"] #auctionView.active-view {
min-height: 0px; min-height: 0px;
@@ -2147,9 +2138,11 @@
} }
.auction-table-v2 tbody td { .auction-table-v2 tbody td {
height: 40px; height: 43px;
padding: 0 12px; padding-top: 6px;
padding-bottom: 6px;
} }
} }
@@ -2172,9 +2165,9 @@
} }
.auc-head h2 { .auc-head h2 {
font-size: var(--font-size-page-title); font-size: 17px;
font-weight: var(--font-weight-semibold); font-weight: 800;
} }
.auc-grid { .auc-grid {
+19 -33
View File
@@ -82,7 +82,7 @@
} }
.dragon-operation-table th { .dragon-operation-table th {
background: var(--table-header); background: rgb(232, 240, 244);
} }
.seat-cell { .seat-cell {
@@ -952,10 +952,6 @@
margin: 0px; margin: 0px;
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
} }
.dragon-title-v2 > span { .dragon-title-v2 > span {
@@ -995,7 +991,7 @@
align-items: center; align-items: center;
min-height: 32px; min-height: 34px;
padding: 3px; padding: 3px;
@@ -1007,19 +1003,19 @@
} }
.dragon-view-tabs-v2 button { .dragon-view-tabs-v2 button {
min-height: 26px; min-height: 27px;
padding: 0px 13px; padding: 0px 13px;
border: 0px; border: 0px;
border-radius: 6px; border-radius: 5px;
background: transparent; background: transparent;
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-label); font-size: 11px;
cursor: pointer; cursor: pointer;
} }
@@ -1041,15 +1037,11 @@
} }
.dragon-action-v2 { .dragon-action-v2 {
min-height: 32px; min-height: 34px;
height: 32px; padding: 0px 11px;
padding: 0px 12px; border-radius: 7px;
border-radius: 8px;
font-size: var(--font-size-label);
} }
.dragon-action-v2 .lucide { .dragon-action-v2 .lucide {
@@ -1109,9 +1101,9 @@
} }
.dragon-summary-v2 .dragon-metric span { .dragon-summary-v2 .dragon-metric span {
color: var(--text-3); color: var(--r2-sub);
font-size: var(--font-size-caption); font-size: 11px;
} }
.dragon-summary-v2 .dragon-metric strong { .dragon-summary-v2 .dragon-metric strong {
@@ -1119,9 +1111,9 @@
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-metric); font-size: 18px;
font-weight: var(--font-weight-semibold); font-weight: 750;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -1253,7 +1245,7 @@
width: 230px; width: 230px;
height: 32px; height: 33px;
flex: 0 0 auto; flex: 0 0 auto;
@@ -1261,9 +1253,9 @@
padding: 0px 10px; padding: 0px 10px;
border: 1px solid var(--border-strong); border: 1px solid rgb(216, 221, 229);
border-radius: 8px; border-radius: 7px;
color: var(--r2-faint); color: var(--r2-faint);
@@ -1271,9 +1263,9 @@
} }
.dragon-search-v2:focus-within { .dragon-search-v2:focus-within {
border-color: var(--accent); border-color: rgb(150, 181, 242);
box-shadow: 0 0 0 2px var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.09) 0px 0px 0px 3px;
} }
.dragon-search-v2 .lucide { .dragon-search-v2 .lucide {
@@ -1429,7 +1421,7 @@
#dragonView .dragon-operation-table :is(th, td).row-number { #dragonView .dragon-operation-table :is(th, td).row-number {
padding-inline: 8px; padding-inline: 8px;
text-align: left; text-align: center;
} }
#dragonView .dragon-operation-table td.stock-code { #dragonView .dragon-operation-table td.stock-code {
@@ -1443,12 +1435,6 @@
z-index: 2; z-index: 2;
height: 36px;
padding: 0 12px;
font-size: var(--font-size-caption);
background: var(--surface-subtle); background: var(--surface-subtle);
} }
@@ -2403,7 +2389,7 @@ table.tbl {
border-collapse: collapse; border-collapse: collapse;
font-size: var(--font-size-table); font-size: 12.5px;
} }
.tbl .num { .tbl .num {
+2 -2
View File
@@ -299,10 +299,10 @@ function renderDragonTraderDetail(trader) {
<div class="trader-operations table-frame tbl-wrap"> <div class="trader-operations table-frame tbl-wrap">
<table class="data-table tbl dragon-operation-table"> <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> <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">序号</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 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>
<tbody>${(trader.operations || []).map((operation, index) => ` <tbody>${(trader.operations || []).map((operation, index) => `
<tr data-code="${escapeHtml(operation.code)}"> <tr data-code="${escapeHtml(operation.code)}">
<td class="row-number">${index + 1}</td> <td class="row-number num">${index + 1}</td>
<td><strong class="sname">${escapeHtml(operation.name)}</strong><span class="scode">${escapeHtml(operation.code)}</span></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><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> <td class="number num ${operation.change == null ? "" : changeClass(operation.change)}">${operation.change == null ? "" : signed(operation.change)}</td>
+18 -26
View File
@@ -14,7 +14,7 @@
padding: 9px 4px; padding: 9px 4px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid rgb(232, 236, 239);
cursor: pointer; cursor: pointer;
} }
@@ -128,7 +128,7 @@
gap: 5px; gap: 5px;
background: var(--surface-muted); background: rgb(247, 249, 250);
cursor: pointer; cursor: pointer;
@@ -140,11 +140,11 @@
border: 1px dashed var(--border-strong); border: 1px dashed var(--border-strong);
border-radius: 8px; border-radius: 6px;
color: var(--text-secondary); color: var(--text-secondary);
font-size: var(--font-size-caption); font-size: 11px;
white-space: nowrap; white-space: nowrap;
} }
@@ -152,7 +152,7 @@
.ladder-gap { .ladder-gap {
min-height: 68px; min-height: 68px;
background: repeating-linear-gradient(135deg, var(--surface), var(--surface) 9px, var(--surface-subtle) 9px, var(--surface-subtle) 18px); background: repeating-linear-gradient(135deg, rgb(255, 255, 255), rgb(255, 255, 255) 9px, rgb(250, 251, 252) 9px, rgb(250, 251, 252) 18px);
} }
.ladder-gap-note { .ladder-gap-note {
@@ -353,12 +353,6 @@
box-shadow: none; 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 { .ladder-page-head {
margin-bottom: 12px; margin-bottom: 12px;
} }
@@ -390,15 +384,13 @@
} }
.ladder-sort-segment button { .ladder-sort-segment button {
min-height: 28px;
padding: 4px 12px; padding: 4px 12px;
border-radius: 6px; border-radius: 6px;
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-label); font-size: 12px;
} }
.ladder-sort-segment button.active { .ladder-sort-segment button.active {
@@ -456,11 +448,11 @@
border-right: 1px solid var(--r2-line-soft); border-right: 1px solid var(--r2-line-soft);
background: linear-gradient(90deg, color-mix(in srgb, var(--tier-color) 8%, var(--surface)), var(--surface)); background: linear-gradient(90deg, color-mix(in srgb, var(--tier-color) 8%, #fff), #fff);
} }
.market-ladder-tier.is-gap .market-ladder-label { .market-ladder-tier.is-gap .market-ladder-label {
background: repeating-linear-gradient(45deg, var(--surface-subtle), var(--surface-subtle) 8px, var(--surface-muted) 8px, var(--surface-muted) 16px); background: repeating-linear-gradient(45deg, rgb(250, 250, 250), rgb(250, 250, 250) 8px, rgb(243, 244, 246) 8px, rgb(243, 244, 246) 16px);
} }
.market-ladder-level { .market-ladder-level {
@@ -579,9 +571,9 @@
white-space: nowrap; white-space: nowrap;
font-size: var(--font-size-table); font-size: 13px;
font-weight: var(--font-weight-semibold); font-weight: 800;
} }
.market-ladder-stock-first .stock-code { .market-ladder-stock-first .stock-code {
@@ -621,7 +613,7 @@
.market-ladder-tag.one-price { .market-ladder-tag.one-price {
background: var(--r2-up-soft); background: var(--r2-up-soft);
color: var(--r2-up); color: rgb(194, 46, 46);
font-weight: 700; font-weight: 700;
} }
@@ -770,9 +762,9 @@
.market-ladder-insight-card > header h3 { .market-ladder-insight-card > header h3 {
margin: 0px; margin: 0px;
font-size: var(--font-size-card-title); font-size: 13.5px;
font-weight: var(--font-weight-semibold); font-weight: 700;
} }
.market-ladder-insight-card > header span { .market-ladder-insight-card > header span {
@@ -804,9 +796,9 @@
.market-ladder-apex strong { .market-ladder-apex strong {
color: var(--r2-up); color: var(--r2-up);
font-size: var(--font-size-metric); font-size: 26px;
font-weight: var(--font-weight-semibold); font-weight: 800;
} }
.market-ladder-apex em { .market-ladder-apex em {
@@ -908,7 +900,7 @@
} }
.market-ladder-pyramid-row.is-gap > i { .market-ladder-pyramid-row.is-gap > i {
background: repeating-linear-gradient(45deg, var(--border), var(--border) 4px, var(--surface-muted) 4px, var(--surface-muted) 8px); background: repeating-linear-gradient(45deg, rgb(229, 231, 235), rgb(229, 231, 235) 4px, rgb(243, 244, 246) 4px, rgb(243, 244, 246) 8px);
} }
.market-ladder-pyramid-row.is-gap > i b { .market-ladder-pyramid-row.is-gap > i b {
@@ -976,11 +968,11 @@
} }
.market-ladder-rate-list > div > i b.is-low { .market-ladder-rate-list > div > i b.is-low {
background: var(--r2-amber); background: rgb(245, 158, 11);
} }
.market-ladder-rate-list > div > i b.is-zero { .market-ladder-rate-list > div > i b.is-zero {
background: var(--border-strong); background: rgb(209, 213, 219);
} }
.market-ladder-rate-list > div > strong { .market-ladder-rate-list > div > strong {
+3 -2
View File
@@ -33,6 +33,7 @@ function renderLadderBoard(ladders) {
return groupMap.get(level) || { level, label: level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}`, count: 0, stocks: [] }; return groupMap.get(level) || { level, label: level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}`, count: 0, stocks: [] };
}); });
const total = ordered.reduce((sum, group) => sum + number(group.count), 0); const total = ordered.reduce((sum, group) => sum + number(group.count), 0);
const spaceStocks = ordered.find((group) => number(group.level) === maxLevel)?.stocks || [];
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value); const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || ""); const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
setText("ladderDateRange", `数据日期 ${currentDate}`); setText("ladderDateRange", `数据日期 ${currentDate}`);
@@ -50,7 +51,7 @@ function renderLadderBoard(ladders) {
const stocks = expanded ? groupStocks : groupStocks.slice(0, limit); const stocks = expanded ? groupStocks : groupStocks.slice(0, limit);
const remaining = Math.max(0, groupStocks.length - stocks.length); const remaining = Math.max(0, groupStocks.length - stocks.length);
const label = group.label || (level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}`); const label = group.label || (level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}`);
const color = { 1: "#3370ff", 2: "#16a34a", 3: "#b45309", 4: "#e04536" }[level] || "#8f959e"; const color = { 1: "#2563eb", 2: "#16a34a", 3: "#d97706", 4: "#e04536" }[level] || "#9ca3af";
return ` return `
<section class="market-ladder-tier ${number(group.count) ? "" : "is-gap"}" data-ladder-level-card="${level}"> <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> <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>
@@ -76,7 +77,7 @@ function renderLadderBoard(ladders) {
const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度,重点观察承接而非单看高度。" : maxLevel >= 3 ? "空间位于中段,梯队延续性比绝对高度更重要。" : "高度受到压缩,先观察首板向二板的结构修复。"; const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度,重点观察承接而非单看高度。" : maxLevel >= 3 ? "空间位于中段,梯队延续性比绝对高度更重要。" : "高度受到压缩,先观察首板向二板的结构修复。";
const strongestGroup = structureRows.reduce((best, group) => number(group.count) > number(best?.count) ? group : best, structureRows[0]); const strongestGroup = structureRows.reduce((best, group) => number(group.count) > number(best?.count) ? group : best, structureRows[0]);
insights.innerHTML = ` insights.innerHTML = `
<section class="market-ladder-insight-card market-ladder-apex-card"><header><h3>空间板</h3><span>市场高度</span></header><div class="market-ladder-apex"><div><strong>${maxLevel ? `${maxLevel}` : "--"}</strong><em>${escapeHtml(spaceChange)}</em></div></div><p>${spaceNote}</p></section> <section class="market-ladder-insight-card market-ladder-apex-card"><header><h3>空间板</h3><span>市场高度</span></header><div class="market-ladder-apex"><div><strong>${maxLevel ? `${maxLevel}` : "--"}</strong><em>${escapeHtml(spaceChange)}</em></div><p>${spaceStocks.length ? spaceStocks.map((stock) => `<b>${escapeHtml(stock.name)}</b>${escapeHtml(stock.sector || "其他")}`).join(" · ") : "暂无空间板"}</p></div><p>${spaceNote}</p></section>
<section class="market-ladder-insight-card"><header><h3>梯队结构</h3><span>完整度</span></header><div class="market-ladder-pyramid">${structureRows.map((group) => `<div class="market-ladder-pyramid-row ${number(group.count) ? "" : "is-gap"}"><span>${escapeHtml(group.label || `${number(group.level)}`)}</span><i><b style="width:${Math.max(number(group.count) ? 8 : 100, number(group.count) / maxCount * 100)}%"></b></i><strong>${number(group.count) ? `${number(group.count)}` : "断层"}</strong></div>`).join("")}</div><p>断层越少,梯队从低位向高位传导越连贯。当前腰部为 <b>${escapeHtml(strongestGroup?.label || "--")}</b>。</p></section> <section class="market-ladder-insight-card"><header><h3>梯队结构</h3><span>完整度</span></header><div class="market-ladder-pyramid">${structureRows.map((group) => `<div class="market-ladder-pyramid-row ${number(group.count) ? "" : "is-gap"}"><span>${escapeHtml(group.label || `${number(group.level)}`)}</span><i><b style="width:${Math.max(number(group.count) ? 8 : 100, number(group.count) / maxCount * 100)}%"></b></i><strong>${number(group.count) ? `${number(group.count)}` : "断层"}</strong></div>`).join("")}</div><p>断层越少,梯队从低位向高位传导越连贯。当前腰部为 <b>${escapeHtml(strongestGroup?.label || "--")}</b>。</p></section>
<section class="market-ladder-insight-card"><header><h3>晋级率参考</h3><span>昨日梯队 → 今日</span></header><div class="market-ladder-rate-list">${rateRows.length ? rateRows.map((row) => `<div><span>${escapeHtml(row.label)}</span><i><b class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}" style="width:${Math.max(row.value, row.value > 0 ? 2 : 0)}%"></b></i><strong class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}">${formatNumber(row.value, 1)}%</strong></div>`).join("") : '<div class="empty-state">暂无可比梯队</div>'}</div><small class="market-ladder-source">数据来自“涨停表现”页 · 昨日梯队样本</small></section>`; <section class="market-ladder-insight-card"><header><h3>晋级率参考</h3><span>昨日梯队 → 今日</span></header><div class="market-ladder-rate-list">${rateRows.length ? rateRows.map((row) => `<div><span>${escapeHtml(row.label)}</span><i><b class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}" style="width:${Math.max(row.value, row.value > 0 ? 2 : 0)}%"></b></i><strong class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}">${formatNumber(row.value, 1)}%</strong></div>`).join("") : '<div class="empty-state">暂无可比梯队</div>'}</div><small class="market-ladder-source">数据来自“涨停表现”页 · 昨日梯队样本</small></section>`;
container.querySelectorAll("[data-ladder-level]").forEach((button) => { container.querySelectorAll("[data-ladder-level]").forEach((button) => {
+5 -6
View File
@@ -790,8 +790,7 @@
border-radius: 4px; 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: var(--border);
--dialog-line-strong: var(--border-strong); --dialog-line-strong: var(--border-strong);
@@ -804,17 +803,17 @@
border: 1px solid var(--dialog-line-strong); border: 1px solid var(--dialog-line-strong);
border-radius: var(--size-radius-dialog); border-radius: 10px;
background: var(--surface); background: var(--surface);
color: var(--dialog-ink); color: var(--dialog-ink);
box-shadow: var(--shadow-float); box-shadow: rgba(25, 36, 48, 0.2) 0px 26px 72px, rgba(25, 36, 48, 0.08) 0px 4px 14px;
} }
:is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog))::backdrop { :is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog))::backdrop {
background: var(--backdrop); background: rgba(28, 39, 50, 0.46);
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
} }
@@ -1134,7 +1133,7 @@
margin: 8px auto; margin: 8px auto;
border-radius: var(--size-radius-dialog); border-radius: 8px;
} }
:is(.stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) .dialog-header { :is(.stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) .dialog-header {
+369 -807
View File
File diff suppressed because it is too large Load Diff
+32 -67
View File
@@ -1,52 +1,39 @@
<section id="mentorView" class="workspace-view page member-feature-view redesigned-mentor-view"> <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> <div class="member-gate" hidden><div class="member-gate-icon"><i data-lucide="lock-keyhole"></i></div><div><strong>问师仅对会员开放</strong><span>开通会员后可使用思维模型进行对话。会员状态可从顶部账号标识进入。</span></div></div>
<div id="mentorNotice" class="inline-notice" hidden></div> <header class="section-toolbar lad-head mentor-page-header">
<header class="mentor-page-header" aria-label="问师"> <div class="section-title-group mentor-page-title">
<div class="mentor-page-title">
<h2>问师</h2> <h2>问师</h2>
<p id="mentorPageSubtitle">与不同交易思维模型持续对话 · 数据日期 --</p> <span class="section-subtitle">与不同交易思维模型持续对话 · <span id="mentorDataDate">--</span></span>
</div> </div>
</header> </header>
<div id="mentorNotice" class="inline-notice" hidden></div>
<div class="mentor-layout"> <div class="mentor-layout">
<aside class="mentor-sidebar" aria-label="思维模型目录"> <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"> <div class="mentor-directory-tools">
<label class="mentor-search-field"> <label class="mentor-search-field">
<span class="visually-hidden">搜索思维模型</span> <span class="visually-hidden">搜索思维模型</span>
<i data-lucide="search"></i> <i data-lucide="search"></i>
<input id="mentorSearchInput" type="search" maxlength="50" placeholder="搜索联系人或标签" autocomplete="off"> <input id="mentorSearchInput" type="search" maxlength="50" placeholder="搜索联系人或标签" autocomplete="off">
</label> </label>
<div class="mentor-filter-menu"> <div class="mentor-evidence-filters" role="group" aria-label="按素材等级筛选">
<button id="mentorFilterToggle" class="mentor-filter-toggle" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="筛选思维模型" title="筛选思维模型"><i data-lucide="filter"></i></button> <button class="active" type="button" data-mentor-grade="all">全部</button>
<div id="mentorFilterOptions" class="mentor-filter-options" role="menu" hidden> <button type="button" data-mentor-grade="A">A级</button>
<button type="button" class="active" data-mentor-grade="all" role="menuitem">全部<span id="mentorCount">0 位</span></button> <button type="button" data-mentor-grade="B">B级</button>
<button type="button" data-mentor-grade="A" role="menuitem">A</button> <button type="button" data-mentor-grade="C">C</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> </div>
<button id="mentorSortToggle" class="mentor-sort-toggle" type="button" aria-pressed="false" title="整理联系人顺序"><i data-lucide="list-ordered"></i><span>整理</span></button> <p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p>
</div> </div>
<div id="mentorList" class="mentor-list"></div> <div id="mentorList" class="mentor-list"></div>
<div id="mentorListEmpty" class="mentor-list-empty" hidden>没有符合条件的思维模型</div> <div id="mentorListEmpty" class="mentor-list-empty" hidden>没有符合条件的思维模型</div>
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p> <p class="mentor-evidence-legend">等级仅表示公开素材完整度</p>
</aside> </aside>
<section class="mentor-chat-panel" aria-label="问师对话"> <section class="mentor-chat-panel" aria-label="问师对话">
<header class="mentor-chat-header"> <p id="activeMentorStatus" class="visually-hidden" aria-live="polite">思维模型已就绪</p>
<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></div>
<p id="activeMentorStatus">思维模型已就绪</p>
</div>
</div>
<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="mentorMessages" class="mentor-messages" aria-live="polite"></div>
<div id="mentorQuickPrompts" class="mentor-quick-prompts"> <div id="mentorQuickPrompts" class="mentor-quick-prompts">
<span class="mentor-prompt-label">开始一个话题</span> <span class="mentor-prompt-label">开始一个话题</span>
@@ -56,61 +43,39 @@
<button type="button" data-mentor-prompt="现在最需要防范的风险是什么?"><i data-lucide="shield-alert"></i>风险检查</button> <button type="button" data-mentor-prompt="现在最需要防范的风险是什么?"><i data-lucide="shield-alert"></i>风险检查</button>
</div> </div>
<form id="mentorChatForm" class="mentor-chat-form"> <form id="mentorChatForm" class="mentor-chat-form">
<div class="mentor-composer-main"> <label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label> <textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
<span class="mentor-composer-hint">Enter 发送 · Shift + Enter 换行</span>
</div>
<div class="mentor-composer-actions"> <div class="mentor-composer-actions">
<span>Enter 发送 · Shift + Enter 换行</span>
<button id="clearMentorChatButton" class="icon-button mentor-clear-button" type="button" disabled aria-label="清空当前对话" title="清空对话"><i data-lucide="trash-2"></i></button>
<button id="stopMentorQuestion" class="button ghost mentor-stop-button" type="button" hidden><i data-lucide="square"></i><span>停止</span></button> <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> <button id="sendMentorQuestion" class="button primary mentor-send-button" type="submit"><i data-lucide="send-horizontal"></i><span>发送</span></button>
</div> </div>
</form> </form>
<p class="mentor-disclaimer">基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。</p>
</section> </section>
</div> <aside class="mentor-profile-panel" aria-label="当前思维模型资料">
<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"> <div class="mentor-profile-hero">
<span id="mentorProfileDialogAvatar" class="mentor-profile-avatar" aria-hidden="true"></span> <span id="mentorProfileAvatar" class="mentor-profile-avatar" aria-hidden="true"></span>
<h3 id="mentorProfileDialogName">--</h3> <h3 id="mentorProfileName">--</h3>
<p id="mentorProfileDialogTagline">--</p> <p id="mentorProfileTagline">--</p>
<div id="mentorProfileDialogBadges" class="mentor-profile-badges"></div> <div id="mentorProfileBadges" class="mentor-profile-badges"></div>
</div> </div>
<section class="mentor-profile-section"> <section class="mentor-profile-section">
<h4>关注维度</h4> <h4>关注维度</h4>
<div id="mentorProfileDialogFocus" class="mentor-active-focus"></div> <div id="activeMentorFocus" class="mentor-active-focus"></div>
</section> </section>
<section class="mentor-profile-section"> <section class="mentor-profile-section">
<h4>资料依据</h4> <h4>资料依据</h4>
<strong id="mentorProfileDialogSource">--</strong> <strong id="mentorProfileSource">--</strong>
<p id="mentorProfileDialogEvidence">--</p> <p id="activeMentorEvidence">--</p>
</section> </section>
<section class="mentor-profile-section mentor-profile-boundary"> <section class="mentor-profile-section mentor-profile-boundary">
<h4>数据边界</h4> <h4>数据边界</h4>
<p><i data-lucide="calendar-days"></i><span id="mentorProfileDialogDataDate">--</span></p> <p><i data-lucide="calendar-days"></i><span id="mentorProfileDataDate">--</span></p>
<p><i data-lucide="database"></i><span>仅使用网页已提供的市场数据</span></p> <p><i data-lucide="database"></i><span>仅使用网页已提供的市场数据</span></p>
</section> </section>
</div> </aside>
</dialog> </div>
</section> </section>
+73 -236
View File
@@ -31,100 +31,25 @@ 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() { function renderMentorWorkspace() {
const setup = state.mentorSetup; const setup = state.mentorSetup;
if (!setup) return; if (!setup) return;
const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null; const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null;
setText("mentorPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`); setText("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`);
setText("currentPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`); setText("mentorProfileName", selected?.name || "--");
setText("activeMentorName", selected?.name || "--"); setText("mentorProfileTagline", selected?.tagline || selected?.description || "--");
setText("activeMentorAvatar", mentorAvatarText(selected)); setText("mentorProfileSource", selected?.evidence?.label || "公开资料整理");
applyMentorAvatarTone(document.querySelector("#activeMentorAvatar"), mentorAvatarToneClass(selected)); setText("mentorProfileDataDate", `行情数据 ${displayCompactDate(setup.trade_date)}`);
const pinButton = document.querySelector("#mentorPinButton"); setText("mentorProfileAvatar", mentorAvatarText(selected));
if (pinButton) { document.querySelector("#mentorProfileAvatar").dataset.grade = String(selected?.evidence?.grade || "").toLowerCase();
pinButton.classList.toggle("active", Boolean(selected?.pinned)); document.querySelector("#mentorProfileBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
pinButton.setAttribute("aria-label", selected?.pinned ? "取消置顶当前思维模型" : "置顶当前思维模型"); setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
pinButton.setAttribute("aria-pressed", String(Boolean(selected?.pinned))); document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
} .map((item) => `<span>${escapeHtml(item)}</span>`).join("");
populateMentorDialogs(selected);
renderMentorDirectory(); renderMentorDirectory();
renderMentorMessages(); 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() { function renderMentorDirectory() {
const mentors = state.mentorSetup?.mentors || []; const mentors = state.mentorSetup?.mentors || [];
const query = state.mentorQuery; const query = state.mentorQuery;
@@ -149,15 +74,9 @@ function renderMentorDirectory() {
sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理"; sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理";
document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode; document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode;
document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode; document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode;
document.querySelector("#mentorFilterToggle").disabled = state.mentorSortMode;
document.querySelectorAll("[data-mentor-grade]").forEach((button) => { document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
button.disabled = state.mentorSortMode; 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"); const container = document.querySelector("#mentorList");
container.classList.toggle("is-sorting", state.mentorSortMode); container.classList.toggle("is-sorting", state.mentorSortMode);
container.innerHTML = filtered.map((mentor) => { container.innerHTML = filtered.map((mentor) => {
@@ -167,7 +86,7 @@ function renderMentorDirectory() {
<article class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""} ${mentor.pinned ? "is-pinned" : ""}" <article class="mentor-option ${mentor.id === state.selectedMentorId ? "active" : ""} ${mentor.pinned ? "is-pinned" : ""}"
data-mentor-card="${escapeHtml(mentor.id)}" draggable="${state.mentorSortMode && !state.mentorSavingPreferences}"> 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" : ""}> <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 ${mentorAvatarToneClass(mentor)}" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span> <span class="mentor-avatar" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span>
<span class="mentor-option-copy"> <span class="mentor-option-copy">
<span class="mentor-option-heading"> <span class="mentor-option-heading">
<strong>${escapeHtml(mentor.name)}</strong> <strong>${escapeHtml(mentor.name)}</strong>
@@ -177,12 +96,16 @@ function renderMentorDirectory() {
<span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span> <span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span>
</span> </span>
</button> </button>
${state.mentorSortMode ? `
<span class="mentor-option-tools"> <span class="mentor-option-tools">
<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-pin-button ${mentor.pinned ? "active" : ""}" data-mentor-pin="${escapeHtml(mentor.id)}"
<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> 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>
` : ""}
</span> </span>
` : ""}
</article> </article>
`; `;
}).join(""); }).join("");
@@ -190,6 +113,9 @@ function renderMentorDirectory() {
document.querySelectorAll("[data-mentor-id]").forEach((button) => { document.querySelectorAll("[data-mentor-id]").forEach((button) => {
button.addEventListener("click", () => selectMentor(button.dataset.mentorId)); 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) => { document.querySelectorAll("[data-mentor-move]").forEach((button) => {
button.addEventListener("click", () => moveMentor(button.dataset.mentorTarget, button.dataset.mentorMove)); button.addEventListener("click", () => moveMentor(button.dataset.mentorTarget, button.dataset.mentorMove));
}); });
@@ -215,36 +141,6 @@ function toggleMentorSortMode() {
renderMentorDirectory(); 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) { async function toggleMentorPin(mentorId) {
if (state.mentorSavingPreferences) return; if (state.mentorSavingPreferences) return;
const mentors = state.mentorSetup?.mentors || []; const mentors = state.mentorSetup?.mentors || [];
@@ -348,17 +244,14 @@ async function persistMentorPreferences() {
} }
} }
function renderMentorBadges(mentor) { function renderMentorBadges(mentor, expanded = false) {
const badges = []; const badges = [];
if (mentor.private) { if (mentor.private) {
badges.push('<span class="mentor-badge private" title="仅管理员本人可见"><i data-lucide="lock-keyhole"></i>仅自己</span>'); 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; const grade = mentor.evidence?.grade;
if (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(""); return badges.join("");
} }
@@ -367,14 +260,6 @@ function mentorAvatarText(mentor) {
return Array.from(String(mentor?.name || "师").trim())[0] || "师"; 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) { async function selectMentor(mentorId) {
if (mentorId === state.selectedMentorId) return; if (mentorId === state.selectedMentorId) return;
state.selectedMentorId = mentorId; state.selectedMentorId = mentorId;
@@ -400,28 +285,29 @@ function renderMentorMessages() {
} else { } else {
container.innerHTML = state.mentorMessages.map((message) => ` container.innerHTML = state.mentorMessages.map((message) => `
<article class="mentor-message ${message.role} ${message.error ? "is-error" : ""}"> <article class="mentor-message ${message.role} ${message.error ? "is-error" : ""}">
<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> <span class="mentor-message-avatar" data-grade="${message.role === "assistant" ? escapeHtml(String(selected?.evidence?.grade || "").toLowerCase()) : ""}" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
<div class="mentor-message-body"> <div class="mentor-message-body">
<div class="mentor-message-label">${message.role === "user" <div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
? escapeHtml(mentorMessageTime(message)) ${message.role === "assistant"
: `${escapeHtml(selected?.name || "问师")} · ${escapeHtml(mentorMessageTime(message))}`}</div> ? `<div class="mentor-message-stack">${message.content
<div class="mentor-message-content">${message.role === "assistant" ? formatMentorAnswer(message.content)
? (message.content ? formatMentorAnswer(message.content) : '<p class="mentor-loading-copy">正在读取复盘数据并推演...</p>') : '<div class="mentor-message-content"><p class="mentor-loading-copy">正在读取复盘数据并推演...</p></div>'}</div>`
: escapeHtml(message.content)}</div> : `<div class="mentor-message-content">${escapeHtml(message.content)}</div>`}
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""} ${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
${renderMentorFollowUps(message)} ${renderMentorFollowUps(message)}
${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
</div> </div>
</article> </article>
`).join(""); `).join("");
} }
document.querySelector("#mentorQuickPrompts").hidden = false; document.querySelector("#mentorQuickPrompts").hidden = state.mentorMessages.length > 0 || state.mentorLoading;
document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading; document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading;
document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId; document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId; document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
document.querySelector("#sendMentorQuestion").hidden = state.mentorLoading; document.querySelector("#sendMentorQuestion").hidden = state.mentorLoading;
document.querySelector("#stopMentorQuestion").hidden = !state.mentorLoading; document.querySelector("#stopMentorQuestion").hidden = !state.mentorLoading;
document.querySelector("#mentorSortToggle").disabled = state.mentorLoading; document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : (selected?.tagline || selected?.description || "思维模型已就绪")); setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : "思维模型已就绪");
container.querySelectorAll("[data-mentor-follow-up]").forEach((button) => { container.querySelectorAll("[data-mentor-follow-up]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorFollowUp)); button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorFollowUp));
}); });
@@ -436,7 +322,7 @@ function renderMentorFollowUps(message) {
return ` return `
<div class="mentor-follow-ups" aria-label="继续追问"> <div class="mentor-follow-ups" aria-label="继续追问">
<span>继续追问</span> <span>继续追问</span>
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><span>${escapeHtml(item)}</span></button>`).join("")} ${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("")}
</div> </div>
`; `;
} }
@@ -456,7 +342,6 @@ async function sendMentorQuestion(event) {
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] }; const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] };
state.mentorMessages.push(responseMessage); state.mentorMessages.push(responseMessage);
input.value = ""; input.value = "";
syncMentorComposerHeight();
state.mentorLoading = true; state.mentorLoading = true;
state.mentorController = new AbortController(); state.mentorController = new AbortController();
hideMentorNotice(); hideMentorNotice();
@@ -477,6 +362,7 @@ async function sendMentorQuestion(event) {
scheduleMentorRender(); scheduleMentorRender();
}, },
(meta) => { (meta) => {
responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`;
responseMessage.followUps = Array.isArray(meta.follow_ups) responseMessage.followUps = Array.isArray(meta.follow_ups)
? meta.follow_ups.filter((item) => typeof item === "string" && item.trim()).slice(0, 3) ? meta.follow_ups.filter((item) => typeof item === "string" && item.trim()).slice(0, 3)
: []; : [];
@@ -545,7 +431,6 @@ async function streamMentorRequest(body, signal, onDelta, onMeta) {
function useMentorQuickPrompt(prompt) { function useMentorQuickPrompt(prompt) {
const input = document.querySelector("#mentorQuestion"); const input = document.querySelector("#mentorQuestion");
input.value = prompt || ""; input.value = prompt || "";
syncMentorComposerHeight();
input.focus(); input.focus();
} }
@@ -593,12 +478,14 @@ function hideMentorNotice() {
} }
function formatMentorAnswer(content) { function formatMentorAnswer(content) {
const blocks = []; const sections = [[]];
let currentSection = sections[0];
let headingCount = 0;
let listType = ""; let listType = "";
let listItems = []; let listItems = [];
const flushList = () => { const flushList = () => {
if (!listItems.length) return; if (!listItems.length) return;
blocks.push(`<${listType} class="mentor-answer-list">${listItems.map((item) => `<li>${item}</li>`).join("")}</${listType}>`); currentSection.push(`<${listType} class="mentor-answer-list">${listItems.map((item) => `<li>${item}</li>`).join("")}</${listType}>`);
listItems = []; listItems = [];
listType = ""; listType = "";
}; };
@@ -608,18 +495,23 @@ function formatMentorAnswer(content) {
flushList(); flushList();
return; return;
} }
const heading = line.match(/^#{1,3}\s+(.+)$/); const heading = mentorAnswerHeading(line);
const bullet = line.match(/^[-*]\s+(.+)$/); const bullet = line.match(/^[-*]\s+(.+)$/);
const ordered = line.match(/^\d+[.、]\s*(.+)$/); const ordered = line.match(/^\d+[.、]\s*(.+)$/);
if (heading) { if (heading) {
flushList(); flushList();
blocks.push(`<strong class="mentor-answer-heading">${formatMentorInline(escapeHtml(heading[1]))}</strong>`); if (currentSection.length) {
currentSection = [];
sections.push(currentSection);
}
headingCount += 1;
currentSection.push(`<strong class="mentor-answer-heading">${formatMentorInline(escapeHtml(heading))}</strong>`);
} else if (/^-{3,}$/.test(line)) { } else if (/^-{3,}$/.test(line)) {
flushList(); flushList();
blocks.push('<span class="mentor-answer-rule"></span>'); currentSection.push('<span class="mentor-answer-rule"></span>');
} else if (line.startsWith("> ")) { } else if (line.startsWith("> ")) {
flushList(); flushList();
blocks.push(`<span class="mentor-answer-quote">${formatMentorInline(escapeHtml(line.slice(2)))}</span>`); currentSection.push(`<span class="mentor-answer-quote">${formatMentorInline(escapeHtml(line.slice(2)))}</span>`);
} else if (bullet || ordered) { } else if (bullet || ordered) {
const nextType = bullet ? "ul" : "ol"; const nextType = bullet ? "ul" : "ol";
if (listType && listType !== nextType) flushList(); if (listType && listType !== nextType) flushList();
@@ -627,11 +519,27 @@ function formatMentorAnswer(content) {
listItems.push(formatMentorInline(escapeHtml((bullet || ordered)[1]))); listItems.push(formatMentorInline(escapeHtml((bullet || ordered)[1])));
} else { } else {
flushList(); flushList();
blocks.push(`<p class="mentor-answer-paragraph">${formatMentorInline(escapeHtml(line))}</p>`); currentSection.push(`<p class="mentor-answer-paragraph">${formatMentorInline(escapeHtml(line))}</p>`);
} }
}); });
flushList(); flushList();
return blocks.join(""); const populatedSections = sections.filter((section) => section.length);
if (headingCount < 2) {
return `<div class="mentor-message-content">${populatedSections.flat().join("")}</div>`;
}
return populatedSections.map((section) => (
`<section class="mentor-message-content mentor-answer-bubble">${section.join("")}</section>`
)).join("");
}
function mentorAnswerHeading(line) {
const markdownHeading = line.match(/^#{1,3}\s+(.+)$/);
if (markdownHeading) return markdownHeading[1].trim();
const boldHeading = line.match(/^\*\*([^*]+)\*\*$/);
if (!boldHeading) return "";
const title = boldHeading[1].trim();
if (!title || title.length > 32 || /[。!?!?;:]$/.test(title)) return "";
return title;
} }
function formatMentorInline(content) { function formatMentorInline(content) {
@@ -644,24 +552,19 @@ function bindMentorEvents() {
document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration); document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration);
document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation); document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode); document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
document.querySelector("#mentorFilterToggle").addEventListener("click", (event) => {
event.stopPropagation();
toggleMentorFilterMenu();
});
document.querySelector("#mentorSearchInput").addEventListener("input", (event) => { document.querySelector("#mentorSearchInput").addEventListener("input", (event) => {
state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN"); state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
renderMentorDirectory(); renderMentorDirectory();
}); });
document.querySelectorAll("[data-mentor-grade]").forEach((button) => { document.querySelectorAll("[data-mentor-grade]").forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
selectMentorGrade(button.dataset.mentorGrade); state.mentorGrade = button.dataset.mentorGrade || "all";
closeMentorFilterMenu(); document.querySelectorAll("[data-mentor-grade]").forEach((item) => {
item.classList.toggle("active", item === button);
});
renderMentorDirectory();
}); });
}); });
document.addEventListener("click", (event) => {
if (event.target.closest(".mentor-filter-menu")) return;
closeMentorFilterMenu();
});
document.querySelectorAll("[data-mentor-prompt]").forEach((button) => { document.querySelectorAll("[data-mentor-prompt]").forEach((button) => {
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt)); button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt));
}); });
@@ -670,70 +573,4 @@ function bindMentorEvents() {
event.preventDefault(); event.preventDefault();
document.querySelector("#mentorChatForm").requestSubmit(); 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);
});
} }
+26 -84
View File
@@ -78,7 +78,7 @@
color: var(--r2-faint); color: var(--r2-faint);
text-align: left; text-align: right;
} }
.redesigned-pool-view .data-table .number { .redesigned-pool-view .data-table .number {
@@ -207,7 +207,7 @@
color: var(--r2-faint); color: var(--r2-faint);
text-align: left; text-align: right;
} }
.redesigned-broken-view .data-table .number { .redesigned-broken-view .data-table .number {
@@ -335,7 +335,7 @@
color: var(--r2-faint); color: var(--r2-faint);
text-align: left; text-align: right;
} }
.redesigned-down-view .data-table .number { .redesigned-down-view .data-table .number {
@@ -455,7 +455,7 @@
color: var(--r2-faint); color: var(--r2-faint);
text-align: left; text-align: right;
} }
.redesigned-yesterday-view .data-table .number { .redesigned-yesterday-view .data-table .number {
@@ -716,6 +716,7 @@
#brokenView .data-table thead th, #brokenView .data-table thead th,
#downView .data-table thead th, #downView .data-table thead th,
#limitPool .data-table thead th,
#performanceView .data-table thead th, #performanceView .data-table thead th,
#yesterdayView .data-table thead th { #yesterdayView .data-table thead th {
height: 32px; height: 32px;
@@ -725,16 +726,9 @@
font-size: 11.5px; 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, #brokenView .data-table tbody td,
#downView .data-table tbody td, #downView .data-table tbody td,
#limitPool .data-table tbody td,
#performanceView .data-table tbody td, #performanceView .data-table tbody td,
#yesterdayView .data-table tbody td { #yesterdayView .data-table tbody td {
height: 39px; height: 39px;
@@ -742,12 +736,6 @@
padding: 5px 9px; padding: 5px 9px;
} }
#limitPool .data-table tbody td {
height: 40px;
padding: 0 8px;
}
#limitPool .main-grid { #limitPool .main-grid {
grid-template-columns: minmax(0px, 1fr) 308px; grid-template-columns: minmax(0px, 1fr) 308px;
} }
@@ -904,6 +892,24 @@
line-height: 1.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 { .pool-state-tag.one-word {
background: rgb(255, 243, 217); background: rgb(255, 243, 217);
@@ -2219,70 +2225,6 @@
table-layout: auto; 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, #brokenTable,
#downTable, #downTable,
#limitTable, #limitTable,
@@ -2293,9 +2235,9 @@
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .row-number { :is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .row-number {
padding-right: 8px; padding-right: 8px;
padding-left: 12px; padding-left: 8px;
text-align: left; text-align: center;
} }
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .reason-column { :is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .reason-column {
+4 -4
View File
@@ -26,7 +26,7 @@
<table class="data-table tbl" id="limitTable"> <table class="data-table tbl" id="limitTable">
<thead> <thead>
<tr> <tr>
<th class="row-number" aria-label="序号">序号</th> <th class="row-number num" aria-label="序号">序号</th>
<th data-sort="name">股票</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="streak">连板<span class="arr"></span></th>
<th class="number num sortable" data-sort="change">涨幅(%<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"> <table id="brokenTable" class="data-table tbl">
<thead> <thead>
<tr> <tr>
<th class="row-number">序号</th> <th class="row-number num">序号</th>
<th>股票</th> <th>股票</th>
<th class="number num sortable" data-broken-sort="change">现价涨幅(%<span class="arr"></span></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> <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"> <table id="downTable" class="data-table tbl">
<thead> <thead>
<tr> <tr>
<th class="row-number">序号</th> <th class="row-number num">序号</th>
<th>股票</th> <th>股票</th>
<th class="number num sortable" data-down-sort="change">跌幅(%<span class="arr"></span></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> <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"> <table id="yesterdayTable" class="data-table tbl">
<thead> <thead>
<tr> <tr>
<th class="row-number">序号</th> <th class="row-number num">序号</th>
<th>股票</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="prior_streak">昨日高度(板)<span class="arr"></span></th>
<th class="number num sortable" data-yesterday-sort="current_change">今日涨幅(%<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"); const body = document.querySelector("#limitTableBody");
body.innerHTML = rows.map((row, index) => ` body.innerHTML = rows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"> <tr data-code="${escapeHtml(row.code)}">
<td class="row-number muted">${index + 1}</td> <td class="row-number num 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><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="streak-pill${number(row.streak) >= 4 ? " high" : ""}">${streakLabel(row.streak)}</span></td> <td class="number num"><span class="pool-streak-tag tag red">${streakLabel(row.streak)}</span></td>
<td class="number num up">${signed(row.change)}</td> <td class="number num up">${signed(row.change)}</td>
<td class="number num">${formatNumber(row.price, 2)}</td> <td class="number num">${formatNumber(row.price, 2)}</td>
<td>${escapeHtml(row.sector || "其他")}</td> <td>${escapeHtml(row.sector || "其他")}</td>
@@ -75,7 +75,7 @@ function renderBrokenTable(rows) {
const body = document.querySelector("#brokenTableBody"); const body = document.querySelector("#brokenTableBody");
body.innerHTML = visibleRows.map((row, index) => ` body.innerHTML = visibleRows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"> <tr data-code="${escapeHtml(row.code)}">
<td class="row-number muted">${index + 1}</td> <td class="row-number num 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><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 ${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> <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"); const body = document.querySelector("#downTableBody");
body.innerHTML = visibleRows.map((row, index) => ` body.innerHTML = visibleRows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"> <tr data-code="${escapeHtml(row.code)}">
<td class="row-number muted">${index + 1}</td> <td class="row-number num 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><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 down" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
<td class="number num">${formatNumber(row.price, 2)}</td> <td class="number num">${formatNumber(row.price, 2)}</td>
@@ -228,7 +228,7 @@ function renderYesterdayTable(rows) {
const body = document.querySelector("#yesterdayTableBody"); const body = document.querySelector("#yesterdayTableBody");
body.innerHTML = visibleRows.map((row, index) => ` body.innerHTML = visibleRows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"> <tr data-code="${escapeHtml(row.code)}">
<td class="row-number muted">${index + 1}</td> <td class="row-number num 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><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">${number(row.prior_streak)}</td>
<td class="number num ${changeClass(row.current_change)}">${signed(row.current_change)}</td> <td class="number num ${changeClass(row.current_change)}">${signed(row.current_change)}</td>
+41 -46
View File
@@ -22,11 +22,11 @@
} }
.popularity-source-tag.dual { .popularity-source-tag.dual {
border-color: var(--warn); border-color: rgb(230, 199, 115);
background: var(--amber-soft); background: var(--amber-soft);
color: var(--warn); color: rgb(118, 83, 20);
} }
.popularity-concepts { .popularity-concepts {
@@ -44,21 +44,21 @@
} }
#popularityView .data-table { #popularityView .data-table {
font-size: var(--font-size-table); font-size: 12px;
} }
#popularityView .data-table thead th { #popularityView .data-table thead th {
height: 36px; height: 32px;
padding: 0 12px; padding: 6px 9px;
font-size: var(--font-size-caption); font-size: 11.5px;
} }
#popularityView .data-table tbody td { #popularityView .data-table tbody td {
height: 40px; height: 39px;
padding: 0 12px; padding: 5px 9px;
} }
.redesigned-popularity-view { .redesigned-popularity-view {
@@ -93,10 +93,6 @@
margin: 0px; margin: 0px;
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
} }
.popularity-title-v2 > span { .popularity-title-v2 > span {
@@ -144,7 +140,7 @@
align-items: center; align-items: center;
min-height: 32px; min-height: 34px;
padding: 3px; padding: 3px;
@@ -156,19 +152,19 @@
} }
.popularity-source-tabs-v2 button { .popularity-source-tabs-v2 button {
min-height: 26px; min-height: 27px;
padding: 0px 13px; padding: 0px 13px;
border: 0px; border: 0px;
border-radius: 6px; border-radius: 5px;
background: transparent; background: transparent;
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-label); font-size: 11px;
cursor: pointer; cursor: pointer;
} }
@@ -194,15 +190,11 @@
} }
.popularity-refresh-v2 { .popularity-refresh-v2 {
min-height: 32px; min-height: 34px;
height: 32px; padding: 0px 11px;
padding: 0px 12px; border-radius: 7px;
border-radius: 8px;
font-size: var(--font-size-label);
} }
.popularity-refresh-v2 .lucide { .popularity-refresh-v2 .lucide {
@@ -252,15 +244,15 @@
width: 3px; width: 3px;
background: var(--accent-soft); background: rgb(199, 216, 251);
} }
.popularity-glance-v2 article:nth-child(2)::before { .popularity-glance-v2 article:nth-child(2)::before {
background: var(--down-soft); background: rgb(183, 221, 207);
} }
.popularity-glance-v2 article.consensus::before { .popularity-glance-v2 article.consensus::before {
background: var(--warn); background: rgb(230, 199, 115);
} }
.popularity-glance-v2 article > span { .popularity-glance-v2 article > span {
@@ -380,7 +372,7 @@
width: 230px; width: 230px;
height: 32px; height: 33px;
flex: 0 0 auto; flex: 0 0 auto;
@@ -388,9 +380,9 @@
padding: 0px 10px; padding: 0px 10px;
border: 1px solid var(--border-strong); border: 1px solid rgb(216, 221, 229);
border-radius: 8px; border-radius: 7px;
color: var(--r2-faint); color: var(--r2-faint);
@@ -398,9 +390,9 @@
} }
.popularity-search-v2:focus-within { .popularity-search-v2:focus-within {
border-color: var(--accent); border-color: rgb(150, 181, 242);
box-shadow: 0 0 0 2px var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.09) 0px 0px 0px 3px;
} }
.popularity-search-v2 .lucide { .popularity-search-v2 .lucide {
@@ -454,15 +446,15 @@
z-index: 2; z-index: 2;
height: 36px; height: 35px;
padding: 0 12px; padding: 7px 10px;
border-bottom-color: var(--r2-line); border-bottom-color: var(--r2-line);
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-caption); font-size: 10.5px;
} }
.popularity-table-v2 thead th:nth-child(1) { .popularity-table-v2 thead th:nth-child(1) {
@@ -485,11 +477,11 @@
} }
.popularity-table-v2 tbody td { .popularity-table-v2 tbody td {
height: 40px; height: 43px;
padding: 0 12px; padding: 7px 10px;
font-size: var(--font-size-table); font-size: 11.5px;
} }
.popularity-table-v2 tbody tr { .popularity-table-v2 tbody tr {
@@ -549,9 +541,9 @@
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-table); font-size: 12px;
font-weight: var(--font-weight-semibold); font-weight: 700;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -567,7 +559,7 @@
} }
.popularity-list-rank-v2 { .popularity-list-rank-v2 {
color: var(--text-2); color: rgb(64, 85, 115);
} }
.popularity-movement-v2 { .popularity-movement-v2 {
@@ -619,6 +611,10 @@
overflow: hidden; overflow: hidden;
} }
body[data-active-view="popularityView"] .overview-strip {
flex: 0 0 auto;
}
body[data-active-view="popularityView"] #popularityView.active-view { body[data-active-view="popularityView"] #popularityView.active-view {
min-height: 0px; min-height: 0px;
@@ -662,9 +658,11 @@
} }
.popularity-table-v2 tbody td { .popularity-table-v2 tbody td {
height: 40px; height: 39px;
padding: 0 12px; padding-top: 5px;
padding-bottom: 5px;
} }
} }
@@ -859,11 +857,8 @@
width: auto; 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); width: var(--col-rank);
text-align: left;
} }
#popularityView .popularity-table-v2 th:nth-child(2) { #popularityView .popularity-table-v2 th:nth-child(2) {
+2 -2
View File
@@ -50,7 +50,7 @@ function renderPopularityTable() {
setText("popularityTableTitle", `${sourceName}`); setText("popularityTableTitle", `${sourceName}`);
setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜"); setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜");
const headers = [ const headers = [
["排名", "row-number"], ["股票", ""], ["最新价(元)", "number num"], ["涨跌幅(%", "number num"], ["排名", "number num"], ["股票", ""], ["最新价(元)", "number num"], ["涨跌幅(%", "number num"],
...(source !== "dc" ? [["同花顺", "number num"]] : []), ...(source !== "dc" ? [["同花顺", "number num"]] : []),
...(source !== "ths" ? [["东方财富", "number num"]] : []), ...(source !== "ths" ? [["东方财富", "number num"]] : []),
["排名变化", "number num"], ["热门概念", ""], ...(!combined ? [["榜单状态", ""]] : []), ["排名变化", "number num"], ["热门概念", ""], ...(!combined ? [["榜单状态", ""]] : []),
@@ -63,7 +63,7 @@ function renderPopularityTable() {
const move = row.rank_change; const move = row.rank_change;
const movement = move === null || move === undefined ? "新" : number(move) > 0 ? `${number(move)}` : number(move) < 0 ? `${Math.abs(number(move))}` : "持平"; 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)}"> return `<tr data-code="${escapeHtml(row.code)}">
<td class="row-number popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td> <td class="number num 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><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.price == null ? "" : formatNumber(row.price, 2)}</td>
<td class="number num ${row.change == null ? "" : changeClass(row.change)}">${row.change == null ? "" : signed(row.change)}</td> <td class="number num ${row.change == null ? "" : changeClass(row.change)}">${row.change == null ? "" : signed(row.change)}</td>
+91 -92
View File
@@ -217,6 +217,10 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
} }
body[data-active-view="reviewWorkspaceView"] .overview-strip {
display: grid;
}
.trade-journal-section .trade-log-summary { .trade-journal-section .trade-log-summary {
min-height: 58px; min-height: 58px;
} }
@@ -238,7 +242,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
border-radius: 10px; border-radius: 10px;
box-shadow: var(--shadow-card); box-shadow: var(--shadow-xs);
min-height: 0px !important; min-height: 0px !important;
} }
@@ -272,15 +276,15 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
.watchlist-section .data-table thead th { .watchlist-section .data-table thead th {
height: 36px; height: 32px;
padding: 0 12px; padding: 6px 12px;
} }
.watchlist-section .data-table tbody td { .watchlist-section .data-table tbody td {
height: 40px; height: 44px;
padding: 0 12px; padding: 7px 12px;
} }
.trade-journal-section .trade-log-summary:empty { .trade-journal-section .trade-log-summary:empty {
@@ -380,7 +384,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .review-history-toggle { #reviewWorkspaceView .review-history-toggle {
min-height: 32px; min-height: 30px;
display: inline-flex; display: inline-flex;
@@ -394,13 +398,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
border: 1px solid var(--review-line); border: 1px solid var(--review-line);
border-radius: 8px; border-radius: 7px;
background: var(--surface); background: var(--surface);
color: var(--text-secondary); color: rgb(55, 65, 81);
font-size: var(--font-size-label); font-size: 11.5px;
white-space: nowrap; white-space: nowrap;
} }
@@ -465,7 +469,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-card); box-shadow: rgba(16, 24, 40, 0.05) 0px 1px 2px;
} }
#reviewWorkspaceView .review-card-heading > div { #reviewWorkspaceView .review-card-heading > div {
@@ -511,9 +515,9 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .data-table thead th { #reviewWorkspaceView .data-table thead th {
height: 36px; height: 32px;
padding: 0 12px; padding: 6px 12px;
border-bottom: 1px solid var(--review-line-soft); border-bottom: 1px solid var(--review-line-soft);
@@ -521,21 +525,21 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
color: var(--review-faint); color: var(--review-faint);
font-size: var(--font-size-caption); font-size: 10.5px;
font-weight: var(--font-weight-semibold); font-weight: 600;
} }
#reviewWorkspaceView .data-table tbody td { #reviewWorkspaceView .data-table tbody td {
height: 40px; height: 48px;
padding: 0 12px; padding: 7px 12px;
border-bottom: 1px solid var(--review-line-soft); border-bottom: 1px solid var(--review-line-soft);
color: var(--text-primary); color: rgb(55, 65, 81);
font-size: var(--font-size-table); font-size: 12px;
} }
#reviewWorkspaceView .data-table tbody tr:last-child td { #reviewWorkspaceView .data-table tbody tr:last-child td {
@@ -555,17 +559,17 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
#reviewWorkspaceView .stock-cell strong { #reviewWorkspaceView .stock-cell strong {
color: var(--review-ink); color: var(--review-ink);
font-size: var(--font-size-table); font-size: 12px;
} }
#reviewWorkspaceView .stock-cell small { #reviewWorkspaceView .stock-cell small {
color: var(--review-faint); color: var(--review-faint);
font-size: var(--font-size-aux); font-size: 10px;
} }
#reviewWorkspaceView .review-watch-mark { #reviewWorkspaceView .review-watch-mark {
color: var(--text-tertiary); color: rgb(209, 213, 219);
font-size: 15px; font-size: 15px;
@@ -573,7 +577,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .review-watch-mark.red { #reviewWorkspaceView .review-watch-mark.red {
color: var(--market-up); color: rgb(224, 69, 54);
} }
#reviewWorkspaceView .review-row-actions { #reviewWorkspaceView .review-row-actions {
@@ -597,19 +601,19 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .table-action { #reviewWorkspaceView .table-action {
min-height: 32px; min-height: 25px;
padding: 0px 6px; padding: 0px 6px;
border: 0px; border: 0px;
border-radius: 8px; border-radius: 5px;
background: transparent; background: transparent;
color: var(--review-blue); color: var(--review-blue);
font-size: var(--font-size-label); font-size: 10.5px;
} }
#reviewWorkspaceView .table-action:hover { #reviewWorkspaceView .table-action:hover {
@@ -617,13 +621,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .table-action.down { #reviewWorkspaceView .table-action.down {
color: var(--text-tertiary); color: rgb(139, 146, 158);
} }
#reviewWorkspaceView .table-action.down:hover { #reviewWorkspaceView .table-action.down:hover {
background: var(--surface-hover); background: var(--surface-hover);
color: var(--market-up); color: rgb(209, 67, 67);
} }
#reviewWorkspaceView .trade-log-table-frame .empty-state { #reviewWorkspaceView .trade-log-table-frame .empty-state {
@@ -663,7 +667,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .trade-log-heading .button { #reviewWorkspaceView .trade-log-heading .button {
min-height: 32px; min-height: 29px;
display: inline-flex; display: inline-flex;
@@ -675,13 +679,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
padding: 0px 10px; padding: 0px 10px;
border-radius: 8px; border-radius: 7px;
background: var(--review-blue); background: var(--review-blue);
color: var(--text-inverse); color: var(--text-inverse);
font-size: var(--font-size-label); font-size: 11.5px;
} }
#reviewWorkspaceView .trade-log-heading .button:hover { #reviewWorkspaceView .trade-log-heading .button:hover {
@@ -827,35 +831,35 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .trade-action-add { #reviewWorkspaceView .trade-action-add {
border-color: var(--market-up); border-color: rgb(241, 196, 192);
background: var(--market-up-soft); background: rgb(253, 236, 234);
color: var(--market-up); color: rgb(214, 62, 50);
} }
#reviewWorkspaceView .trade-action-buy { #reviewWorkspaceView .trade-action-buy {
border-color: var(--market-up); border-color: rgb(241, 196, 192);
background: var(--market-up-soft); background: rgb(253, 236, 234);
color: var(--market-up); color: rgb(214, 62, 50);
} }
#reviewWorkspaceView .trade-action-sell { #reviewWorkspaceView .trade-action-sell {
border-color: var(--market-down); border-color: rgb(188, 225, 202);
background: var(--market-down-soft); background: rgb(234, 247, 239);
color: var(--market-down); color: rgb(22, 139, 67);
} }
#reviewWorkspaceView .trade-action-trim { #reviewWorkspaceView .trade-action-trim {
border-color: var(--market-down); border-color: rgb(188, 225, 202);
background: var(--market-down-soft); background: rgb(234, 247, 239);
color: var(--market-down); color: rgb(22, 139, 67);
} }
#reviewWorkspaceView .trade-tags { #reviewWorkspaceView .trade-tags {
@@ -875,7 +879,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
background: var(--review-blue-soft); background: var(--review-blue-soft);
color: var(--action); color: rgb(82, 112, 167);
font-size: 8.5px; font-size: 8.5px;
} }
@@ -895,7 +899,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
white-space: nowrap; white-space: nowrap;
color: var(--text-secondary); color: rgb(75, 85, 99);
font-size: 10.5px; font-size: 10.5px;
@@ -985,7 +989,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .journal-form .button { #reviewWorkspaceView .journal-form .button {
min-height: 32px; min-height: 34px;
display: inline-flex; display: inline-flex;
@@ -997,13 +1001,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
padding: 0px 14px; padding: 0px 14px;
border-radius: 8px; border-radius: 7px;
background: var(--review-blue); background: var(--review-blue);
color: var(--text-inverse); color: var(--text-inverse);
font-size: var(--font-size-label); font-size: 12px;
} }
#reviewWorkspaceView .journal-form .button:hover { #reviewWorkspaceView .journal-form .button:hover {
@@ -1243,9 +1247,9 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
color: var(--review-ink); color: var(--review-ink);
font-size: var(--font-size-page-title); font-size: 19px;
font-weight: var(--font-weight-semibold); font-weight: 750;
} }
#reviewWorkspaceView .review-page-title .section-subtitle { #reviewWorkspaceView .review-page-title .section-subtitle {
@@ -1257,7 +1261,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
white-space: nowrap; white-space: nowrap;
font-size: var(--font-size-caption); font-size: 12px;
} }
#reviewWorkspaceView .review-card-heading { #reviewWorkspaceView .review-card-heading {
@@ -1281,13 +1285,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
color: var(--review-ink); color: var(--review-ink);
font-size: var(--font-size-card-title); font-size: 14.5px;
font-weight: var(--font-weight-semibold); font-weight: 720;
} }
#reviewWorkspaceView .review-add-watch { #reviewWorkspaceView .review-add-watch {
min-height: 32px; min-height: 29px;
display: inline-flex; display: inline-flex;
@@ -1299,23 +1303,23 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
padding: 0px 10px; padding: 0px 10px;
border: 1px solid var(--color-action-line); border: 1px solid rgb(185, 205, 248);
border-radius: 8px; border-radius: 7px;
background: var(--action-soft); background: rgb(245, 248, 255);
color: var(--review-blue); color: var(--review-blue);
font-size: var(--font-size-label); font-size: 11.5px;
font-weight: var(--font-weight-semibold); font-weight: 650;
} }
#reviewWorkspaceView .review-add-watch:hover { #reviewWorkspaceView .review-add-watch:hover {
border-color: var(--action); border-color: rgb(142, 176, 244);
background: var(--selected); background: rgb(234, 241, 255);
} }
#reviewWorkspaceView .review-add-watch .lucide { #reviewWorkspaceView .review-add-watch .lucide {
@@ -1370,17 +1374,17 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
#reviewWorkspaceView .review-watchlist-table .stock-cell strong { #reviewWorkspaceView .review-watchlist-table .stock-cell strong {
font-size: var(--font-size-table); font-size: 12.5px;
font-weight: var(--font-weight-semibold); font-weight: 680;
} }
#reviewWorkspaceView .review-watchlist-table .stock-cell small { #reviewWorkspaceView .review-watchlist-table .stock-cell small {
font-size: var(--font-size-aux); font-size: 10.5px;
} }
#reviewWorkspaceView .watch-attention-score { #reviewWorkspaceView .watch-attention-score {
color: var(--text-primary); color: rgb(63, 75, 94);
font-size: 12px; font-size: 12px;
@@ -1448,11 +1452,11 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
#reviewWorkspaceView .journal-summary-field input:focus { #reviewWorkspaceView .journal-summary-field input:focus {
border-color: var(--review-blue-line); border-color: var(--review-blue-line);
box-shadow: 0 0 0 2px var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.08) 0px 0px 0px 2px;
} }
#reviewWorkspaceView .journal-summary-field input::placeholder { #reviewWorkspaceView .journal-summary-field input::placeholder {
color: var(--text-tertiary); color: rgb(166, 173, 183);
} }
#reviewWorkspaceView .journal-form textarea { #reviewWorkspaceView .journal-form textarea {
@@ -1539,7 +1543,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
.watchlist-editor-form .form-field > label, .watchlist-editor-form .form-field > label,
.watchlist-editor-form .form-field > span { .watchlist-editor-form .form-field > span {
color: var(--text-secondary); color: rgb(55, 65, 81);
font-size: 12px; font-size: 12px;
@@ -1557,7 +1561,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
padding: 0px 11px; padding: 0px 11px;
border: 1px solid var(--border-strong); border: 1px solid rgb(223, 227, 232);
border-radius: 8px; border-radius: 8px;
@@ -1565,9 +1569,9 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
.watchlist-search-control:focus-within { .watchlist-search-control:focus-within {
border-color: var(--color-action-line); border-color: rgb(184, 202, 244);
box-shadow: 0 0 0 3px var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.07) 0px 0px 0px 3px;
} }
.watchlist-search-control .lucide { .watchlist-search-control .lucide {
@@ -1575,7 +1579,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
height: 15px; height: 15px;
color: var(--text-tertiary); color: rgb(154, 163, 176);
} }
.watchlist-search-control input { .watchlist-search-control input {
@@ -1587,7 +1591,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
outline: 0px; outline: 0px;
color: var(--text-primary); color: rgb(31, 41, 55);
font-style: inherit; font-style: inherit;
@@ -1641,13 +1645,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
border-style: none none solid; border-style: none none solid;
border-color: currentcolor currentcolor var(--border); border-color: currentcolor currentcolor rgb(238, 240, 243);
border-image: none; border-image: none;
background: var(--surface); background: var(--surface);
color: var(--text-primary); color: rgb(31, 41, 55);
text-align: left; text-align: left;
} }
@@ -1669,7 +1673,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
.watchlist-search-results button small { .watchlist-search-results button small {
color: var(--text-tertiary); color: rgb(139, 148, 161);
font-size: 10.5px; font-size: 10.5px;
} }
@@ -1677,7 +1681,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
.watchlist-search-results button > b { .watchlist-search-results button > b {
margin-left: auto; margin-left: auto;
color: var(--text-secondary); color: rgb(105, 115, 134);
font-size: 11px; font-size: 11px;
@@ -1687,7 +1691,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
.watchlist-search-status { .watchlist-search-status {
padding: 14px 10px; padding: 14px 10px;
color: var(--text-tertiary); color: rgb(139, 148, 161);
font-size: 11.5px; font-size: 11.5px;
@@ -1705,7 +1709,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
padding: 10px 12px; padding: 10px 12px;
border: 1px solid var(--action-soft); border: 1px solid rgb(220, 229, 247);
border-radius: 8px; border-radius: 8px;
@@ -1729,9 +1733,9 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
border-radius: 7px; border-radius: 7px;
background: var(--action-soft); background: rgb(232, 239, 255);
color: var(--action); color: rgb(37, 99, 235);
} }
.watchlist-selection-icon .lucide { .watchlist-selection-icon .lucide {
@@ -1749,7 +1753,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
.watchlist-selection strong { .watchlist-selection strong {
color: var(--text-primary); color: rgb(31, 41, 55);
font-size: 13.5px; font-size: 13.5px;
} }
@@ -1759,13 +1763,13 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
gap: 8px; gap: 8px;
color: var(--text-tertiary); color: rgb(123, 132, 145);
font-size: 10.5px; font-size: 10.5px;
} }
.watchlist-selection span b { .watchlist-selection span b {
color: var(--text-secondary); color: rgb(82, 96, 113);
font-weight: 600; font-weight: 600;
} }
@@ -1787,7 +1791,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
resize: vertical; resize: vertical;
border: 1px solid var(--border-strong); border: 1px solid rgb(223, 227, 232);
border-radius: 8px; border-radius: 8px;
@@ -1821,9 +1825,9 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
} }
.watchlist-editor-form textarea:focus { .watchlist-editor-form textarea:focus {
border-color: var(--color-action-line); border-color: rgb(184, 202, 244);
box-shadow: 0 0 0 3px var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.07) 0px 0px 0px 3px;
} }
#reviewWorkspaceView .trade-log-table th:nth-child(1) { #reviewWorkspaceView .trade-log-table th:nth-child(1) {
@@ -1869,7 +1873,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
font-size: 12px; font-size: 12px;
font-weight: var(--font-weight-semibold); font-weight: 650;
min-height: 0px; min-height: 0px;
@@ -2197,8 +2201,3 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
height: auto; height: auto;
} }
} }
#reviewWorkspaceView .data-table thead th:first-child,
#reviewWorkspaceView .data-table tbody td:first-child {
text-align: left;
}
+49 -60
View File
@@ -25,7 +25,7 @@
} }
.rotation-day.selected-day { .rotation-day.selected-day {
background: var(--selected); background: rgb(240, 246, 253);
} }
.rotation-sector-chip.selected { .rotation-sector-chip.selected {
@@ -81,11 +81,11 @@
} }
.rotation-day:nth-child(2n+1) { .rotation-day:nth-child(2n+1) {
background: var(--surface-subtle); background: rgb(246, 248, 250);
} }
.rotation-day-sector { .rotation-day-sector {
border-color: var(--border); border-color: rgb(231, 235, 239);
border-radius: 4px; border-radius: 4px;
@@ -107,7 +107,7 @@
} }
:where(#rotationView) .rotation-swatch { :where(#rotationView) .rotation-swatch {
background: var(--accent-soft); background: rgba(53, 106, 230, 0.12);
} }
:where(#rotationView) .rotation-tracker-copy { :where(#rotationView) .rotation-tracker-copy {
@@ -157,9 +157,9 @@
cursor: pointer; cursor: pointer;
border-bottom: 1px dashed var(--border); border-bottom: 1px dashed rgba(135, 149, 173, 0.22);
background: color-mix(in srgb, var(--accent) calc(4% + var(--rotation-heat) * 46%), transparent); background: rgba(53, 106, 230, calc(.04 + var(--rotation-heat) * .46));
} }
.redesigned-rotation-view { .redesigned-rotation-view {
@@ -185,9 +185,9 @@
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-page-title); font-size: 17px;
font-weight: var(--font-weight-semibold); font-weight: 800;
letter-spacing: 0px; letter-spacing: 0px;
} }
@@ -241,7 +241,7 @@
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-label); font-size: 12px;
line-height: 1; line-height: 1;
} }
@@ -260,15 +260,13 @@
.rotation-order-control button:focus-visible, .rotation-order-control button:focus-visible,
.rotation-sector-chip:focus-visible, .rotation-sector-chip:focus-visible,
.rotation-track-cancel:focus-visible { .rotation-track-cancel:focus-visible {
outline: 2px solid var(--accent); outline: rgba(37, 99, 235, 0.28) solid 2px;
outline-offset: 2px; outline-offset: 2px;
} }
.rotation-export-button { .rotation-export-button {
min-height: 32px; min-height: 30px;
height: 32px;
display: inline-flex; display: inline-flex;
@@ -276,17 +274,17 @@
justify-content: center; justify-content: center;
padding: 0 12px; padding: 5px 12px;
border: 1px solid var(--border-strong); border: 1px solid var(--r2-line);
border-radius: 8px; border-radius: 7px;
background: var(--surface); background: var(--surface);
color: var(--text-primary); color: rgb(55, 65, 81);
font-size: var(--font-size-label); font-size: 12px;
font-weight: 500; font-weight: 500;
} }
@@ -435,17 +433,17 @@
} }
#rotationView .rotation-swatch.strong { #rotationView .rotation-swatch.strong {
background: var(--accent); background: rgb(112, 155, 245);
} }
#rotationView .rotation-swatch.warm { #rotationView .rotation-swatch.warm {
background: var(--accent-soft); background: rgb(203, 220, 255);
} }
#rotationView .rotation-swatch.mild { #rotationView .rotation-swatch.mild {
background: var(--surface-muted); background: rgb(240, 244, 250);
border: 1px solid var(--border); border: 1px solid rgb(223, 230, 240);
} }
#rotationView .rotation-tracker { #rotationView .rotation-tracker {
@@ -489,7 +487,7 @@
} }
#rotationView .rotation-tracker-copy span { #rotationView .rotation-tracker-copy span {
color: var(--accent-hover); color: rgb(59, 98, 196);
font-size: 12px; font-size: 12px;
@@ -537,7 +535,7 @@
border-radius: 2px 2px 0px 0px; border-radius: 2px 2px 0px 0px;
background: var(--accent); background: rgb(147, 180, 245);
} }
#rotationView .rotation-tracker-spark small { #rotationView .rotation-tracker-spark small {
@@ -545,7 +543,7 @@
top: -1px; top: -1px;
color: var(--accent-hover); color: rgb(59, 98, 196);
font-size: 9px; font-size: 9px;
} }
@@ -557,7 +555,7 @@
border-style: dashed dashed none; border-style: dashed dashed none;
border-color: var(--action-line) var(--action-line) currentcolor; border-color: rgb(185, 200, 232) rgb(185, 200, 232) currentcolor;
border-image: none; border-image: none;
@@ -569,9 +567,7 @@
} }
.rotation-track-cancel { .rotation-track-cancel {
min-height: 32px; min-height: 30px;
height: 32px;
display: inline-flex; display: inline-flex;
@@ -579,21 +575,21 @@
justify-content: center; justify-content: center;
border: 1px solid var(--border-strong); border: 1px solid var(--r2-line);
border-radius: 8px; border-radius: 7px;
background: var(--surface); background: var(--surface);
color: var(--text-primary); color: rgb(55, 65, 81);
font-size: var(--font-size-label); font-size: 12px;
font-weight: 500; font-weight: 500;
margin-left: auto; margin-left: auto;
padding: 0 12px; padding: 4px 9px;
} }
#rotationView .rotation-history { #rotationView .rotation-history {
@@ -701,7 +697,7 @@
padding: 5px; padding: 5px;
background: var(--surface-subtle); background: rgb(251, 252, 254);
} }
#rotationView .rotation-sector-chip { #rotationView .rotation-sector-chip {
@@ -821,7 +817,7 @@
border-color: var(--r2-blue); border-color: var(--r2-blue);
box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px var(--accent-soft); box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px rgba(37, 99, 235, .12);
} }
.rotation-cell-tooltip { .rotation-cell-tooltip {
@@ -868,9 +864,9 @@
} }
#rotationView .rotation-table thead th { #rotationView .rotation-table thead th {
height: 36px; height: 37px;
padding: 0 12px; padding: 8px 12px;
border-bottom: 1px solid var(--r2-line); border-bottom: 1px solid var(--r2-line);
@@ -878,7 +874,7 @@
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-caption); font-size: 12px;
font-weight: 600; font-weight: 600;
@@ -891,13 +887,8 @@
text-align: right; 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] { #rotationView .rotation-table thead th[data-auto-sort] {
color: var(--text-2); color: rgb(75, 85, 99);
cursor: pointer; cursor: pointer;
@@ -927,16 +918,14 @@
} }
#rotationView .rotation-table tbody td { #rotationView .rotation-table tbody td {
height: 40px; height: 42px;
padding: 0 12px; padding: 8px 12px;
border-bottom: 1px solid var(--r2-line-soft); border-bottom: 1px solid var(--r2-line-soft);
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-table);
white-space: nowrap; white-space: nowrap;
} }
@@ -945,7 +934,7 @@
} }
#rotationView .rotation-table tbody tr:hover td { #rotationView .rotation-table tbody tr:hover td {
background: var(--table-hover); background: rgb(248, 250, 255);
} }
#rotationView .rotation-table .stock-name { #rotationView .rotation-table .stock-name {
@@ -965,9 +954,9 @@
} }
#rotationView .trend-cool { #rotationView .trend-cool {
background: var(--accent-soft); background: rgb(232, 244, 253);
color: var(--accent); color: rgb(37, 99, 235);
} }
#rotationView .trend-new { #rotationView .trend-new {
@@ -1051,25 +1040,25 @@
} }
#rotationView .rotation-sector-chip.heat-mild { #rotationView .rotation-sector-chip.heat-mild {
border-color: var(--border); border-color: rgb(229, 234, 241);
background: var(--surface-muted); background: rgb(246, 248, 251);
box-shadow: none; box-shadow: none;
} }
#rotationView .rotation-sector-chip.heat-strong { #rotationView .rotation-sector-chip.heat-strong {
border-color: var(--action-line); border-color: rgba(37, 99, 235, 0.24);
background: var(--accent-soft); background: rgb(197, 215, 251);
box-shadow: none; box-shadow: none;
} }
#rotationView .rotation-sector-chip.heat-warm { #rotationView .rotation-sector-chip.heat-warm {
border-color: var(--action-line); border-color: rgba(37, 99, 235, 0.12);
background: color-mix(in srgb, var(--accent) 12%, var(--surface)); background: rgb(231, 239, 255);
box-shadow: none; box-shadow: none;
} }
@@ -1077,7 +1066,7 @@
#rotationView .rotation-sector-chip:hover { #rotationView .rotation-sector-chip:hover {
z-index: 6; z-index: 6;
border-color: var(--accent); border-color: rgba(37, 99, 235, 0.34);
filter: saturate(1.06); filter: saturate(1.06);
@@ -1091,7 +1080,7 @@
border-collapse: collapse; border-collapse: collapse;
font-size: var(--font-size-table); font-size: 12.5px;
table-layout: auto; table-layout: auto;
+1 -1
View File
@@ -41,7 +41,7 @@
<div class="rotation-table-frame tbl-wrap"> <div class="rotation-table-frame tbl-wrap">
<table id="rotationTable" class="data-table tbl rotation-table"> <table id="rotationTable" class="data-table tbl rotation-table">
<thead><tr> <thead><tr>
<th class="row-number">序号</th><th>代码</th><th>股票</th> <th class="number num">序号</th><th>代码</th><th>股票</th>
<th class="number num" data-auto-sort="true" title="涨跌幅:点击排序">涨跌幅(%</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">开盘价(元)</th><th class="number num">收盘价(元)</th>
<th class="number num" data-auto-sort="true" title="成交额:点击排序">成交额(亿)</th><th>行情状态</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("rotationDetailTitle", `${payload.meta?.sector_name || state.rotationSelectedSector}成分股`);
setText("rotationDetailMeta", `${displayCompactDate(payload.meta?.trade_date)} · ${number(payload.meta?.quoted_count)} / ${number(payload.meta?.member_count)}`); setText("rotationDetailMeta", `${displayCompactDate(payload.meta?.trade_date)} · ${number(payload.meta?.quoted_count)} / ${number(payload.meta?.member_count)}`);
body.innerHTML = rows.map((row, index) => ` body.innerHTML = rows.map((row, index) => `
<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> <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>
<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 ? 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">${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> <td class="number num" data-sort-value="${number(row.amount_billion)}">${row.quoted ? formatNumber(row.amount_billion, 2) : ""}</td><td>${row.quoted ? "正常交易" : "当日无行情"}</td></tr>
+105 -116
View File
@@ -1,8 +1,8 @@
/* Canonical CSS owner: screener. Historical layers consolidated 2026-08-02. */ /* Canonical CSS owner: screener. Historical layers consolidated 2026-08-02. */
#screenerView .section-toolbar h2 { #screenerView .section-toolbar h2 {
font-size: var(--font-size-page-title); font-size: 17px;
font-weight: var(--font-weight-semibold); font-weight: 800;
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@@ -25,7 +25,7 @@
.curated-detail-header > div > span, .curated-detail-header > div > span,
.quant-panel-heading span { .quant-panel-heading span {
color: var(--text-secondary); color: rgb(100, 116, 139);
font-size: 12px; font-size: 12px;
@@ -35,7 +35,7 @@
:where(#screenerView) .curated-detail-header h3 { :where(#screenerView) .curated-detail-header h3 {
margin: 3px 0px 0px; margin: 3px 0px 0px;
color: var(--text-primary); color: rgb(17, 24, 39);
letter-spacing: 0px; letter-spacing: 0px;
} }
@@ -45,7 +45,7 @@
border-radius: 5px; border-radius: 5px;
background: var(--action-soft); background: rgb(232, 238, 252);
} }
:where(#screenerView) .curated-search .lucide { :where(#screenerView) .curated-search .lucide {
@@ -55,19 +55,19 @@
:where(#screenerView) .curated-search input { :where(#screenerView) .curated-search input {
background: transparent; background: transparent;
color: var(--text-primary); color: rgb(17, 24, 39);
} }
.curated-strategy-card.active .curated-strategy-rank { .curated-strategy-card.active .curated-strategy-rank {
background: var(--action-soft); background: rgb(220, 232, 255);
color: var(--action); color: rgb(29, 78, 216);
} }
:where(#screenerView) .curated-detail-header p { :where(#screenerView) .curated-detail-header p {
margin: 9px 0px 0px; margin: 9px 0px 0px;
color: var(--text-secondary); color: rgb(100, 116, 139);
} }
.curated-strategy-badges { .curated-strategy-badges {
@@ -87,9 +87,9 @@
border-radius: 5px; border-radius: 5px;
background: var(--hover); background: rgb(241, 245, 249);
color: var(--text-secondary); color: rgb(71, 85, 105);
font-weight: 650; font-weight: 650;
@@ -97,9 +97,9 @@
} }
.curated-strategy-badges span:first-child { .curated-strategy-badges span:first-child {
background: var(--action-soft); background: rgb(234, 241, 255);
color: var(--action); color: rgb(29, 78, 216);
} }
:where(#screenerView) .mini-section-heading { :where(#screenerView) .mini-section-heading {
@@ -111,11 +111,11 @@
:where(#screenerView) .mini-section-heading h4 { :where(#screenerView) .mini-section-heading h4 {
font-weight: 740; font-weight: 740;
color: var(--text-primary); color: rgb(31, 41, 55);
} }
.mini-section-heading > span { .mini-section-heading > span {
color: var(--text-secondary); color: rgb(100, 116, 139);
font-size: 12px; font-size: 12px;
} }
@@ -141,15 +141,15 @@
gap: 12px; gap: 12px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid rgb(238, 240, 243);
} }
:where(#screenerView) .curated-rule-row span { :where(#screenerView) .curated-rule-row span {
color: var(--text-secondary); color: rgb(71, 85, 105);
} }
:where(#screenerView) .curated-rule-row strong { :where(#screenerView) .curated-rule-row strong {
color: var(--text-primary); color: rgb(17, 24, 39);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -163,7 +163,7 @@
.curated-score-row > span:first-child { .curated-score-row > span:first-child {
overflow: hidden; overflow: hidden;
color: var(--text-secondary); color: rgb(71, 85, 105);
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -177,7 +177,7 @@
border-radius: 4px; border-radius: 4px;
background: var(--surface-muted); background: rgb(237, 240, 244);
} }
.curated-score-track i { .curated-score-track i {
@@ -187,11 +187,11 @@
border-radius: inherit; border-radius: inherit;
background: var(--action); background: rgb(79, 118, 199);
} }
.curated-score-row strong { .curated-score-row strong {
color: var(--text-primary); color: rgb(51, 65, 85);
text-align: right; text-align: right;
@@ -211,11 +211,11 @@
} }
:where(#screenerView) .curated-data-status > .lucide { :where(#screenerView) .curated-data-status > .lucide {
color: var(--market-down); color: rgb(22, 163, 74);
} }
.curated-data-status.missing > .lucide { .curated-data-status.missing > .lucide {
color: var(--warning-color); color: rgb(217, 119, 6);
} }
.curated-data-status span { .curated-data-status span {
@@ -225,13 +225,13 @@
:where(#screenerView) .curated-data-status strong { :where(#screenerView) .curated-data-status strong {
display: block; display: block;
color: var(--text-primary); color: rgb(51, 65, 85);
} }
:where(#screenerView) .curated-data-status small { :where(#screenerView) .curated-data-status small {
display: block; display: block;
color: var(--text-tertiary); color: rgb(124, 135, 152);
} }
.quant-universe-grid .form-field { .quant-universe-grid .form-field {
@@ -239,16 +239,16 @@
} }
.quant-universe-grid input[type="number"] { .quant-universe-grid input[type="number"] {
height: 32px; height: 40px;
} }
.quant-rule-row input:focus, .quant-rule-row input:focus,
.quant-rule-row select:focus { .quant-rule-row select:focus {
border-color: var(--action); border-color: rgb(37, 99, 235);
outline: 0px; outline: 0px;
box-shadow: 0 0 0 3px var(--focus-ring); box-shadow: rgba(37, 99, 235, 0.1) 0px 0px 0px 3px;
} }
:where(#screenerView) .quant-remove-button { :where(#screenerView) .quant-remove-button {
@@ -268,7 +268,7 @@
float: right; float: right;
color: var(--action); color: rgb(29, 78, 216);
} }
:where(#screenerView) .quant-weight-status > div { :where(#screenerView) .quant-weight-status > div {
@@ -278,7 +278,7 @@
:where(#screenerView) .quant-weight-status i { :where(#screenerView) .quant-weight-status i {
width: 100%; width: 100%;
background: var(--action); background: rgb(37, 99, 235);
transition: width 180ms, background 180ms; transition: width 180ms, background 180ms;
} }
@@ -305,7 +305,7 @@
} }
.quant-summary-pane { .quant-summary-pane {
border-top: 1px solid var(--border); border-top: 1px solid rgb(229, 231, 235);
border-left: 0px; border-left: 0px;
} }
@@ -549,7 +549,7 @@
padding: 12px; padding: 12px;
background: var(--surface-subtle); background: rgb(248, 249, 251);
} }
#screenerView .strategy-list { #screenerView .strategy-list {
@@ -575,7 +575,7 @@
} }
#screenerView .strategy-item.active { #screenerView .strategy-item.active {
border-color: var(--color-action-line); border-color: rgb(201, 219, 241);
background: var(--action-soft); background: var(--action-soft);
} }
@@ -724,7 +724,7 @@
#screenerView .backtest-panel { #screenerView .backtest-panel {
padding: 10px 14px 12px; padding: 10px 14px 12px;
background: var(--surface-subtle); background: rgb(251, 252, 253);
} }
#screenerView .backtest-panel > .workspace-heading { #screenerView .backtest-panel > .workspace-heading {
@@ -1099,7 +1099,7 @@
} }
.tracking-status.active { .tracking-status.active {
border-color: var(--color-action-line); border-color: rgb(197, 215, 237);
background: var(--action-soft); background: var(--action-soft);
@@ -1107,7 +1107,7 @@
} }
.tracking-status.complete { .tracking-status.complete {
border-color: var(--market-down); border-color: rgb(185, 223, 207);
background: var(--market-down-soft); background: var(--market-down-soft);
@@ -1149,9 +1149,9 @@
border-radius: 50%; border-radius: 50%;
background: var(--border); background: rgb(229, 231, 235);
color: var(--text-tertiary); color: rgb(156, 163, 175);
font-size: 11px; font-size: 11px;
@@ -1189,11 +1189,11 @@
.screener-strategy-title b.neutral { .screener-strategy-title b.neutral {
background: var(--surface-muted); background: var(--surface-muted);
color: var(--text-secondary); color: rgb(107, 114, 128);
} }
:where(#screenerView) .screener-strategy-summary p { :where(#screenerView) .screener-strategy-summary p {
color: var(--text-secondary); color: rgb(107, 114, 128);
font-size: 12px; font-size: 12px;
} }
@@ -1225,7 +1225,7 @@
border-right: 0px; border-right: 0px;
border-bottom-color: var(--border); border-bottom-color: rgb(238, 240, 243);
} }
#screenerView .screener-result-frame th { #screenerView .screener-result-frame th {
@@ -1235,13 +1235,13 @@
border-right: 0px; border-right: 0px;
border-bottom-color: var(--border); border-bottom-color: rgb(238, 240, 243);
height: 38px; height: 38px;
background: var(--table-header); background: var(--table-header);
color: var(--text-secondary); color: rgb(107, 114, 128);
font-size: 12px; font-size: 12px;
} }
@@ -1281,7 +1281,7 @@
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-float); box-shadow: rgba(15, 23, 42, 0.16) -12px 0px 32px;
} }
.strategy-drawer[open] { .strategy-drawer[open] {
@@ -1323,7 +1323,7 @@
} }
.strategy-drawer-header span { .strategy-drawer-header span {
color: var(--text-tertiary); color: rgb(156, 163, 175);
font-size: 10.5px; font-size: 10.5px;
} }
@@ -1365,7 +1365,7 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
background: var(--surface-subtle); background: rgb(248, 249, 251);
} }
#screenerView .strategy-drawer .strategy-list { #screenerView .strategy-drawer .strategy-list {
@@ -1602,7 +1602,7 @@
padding-top: 5px; padding-top: 5px;
border-top: 1px solid var(--border); border-top: 1px solid rgb(238, 240, 243);
} }
#screenerView .screener-backtest-strip { #screenerView .screener-backtest-strip {
@@ -1684,7 +1684,7 @@
background: var(--surface); background: var(--surface);
color: var(--text-primary); color: rgb(31, 41, 55);
font-style: inherit; font-style: inherit;
@@ -1722,7 +1722,7 @@
padding: 0px 9px; padding: 0px 9px;
color: var(--text-primary); color: rgb(31, 41, 55);
font: inherit; font: inherit;
@@ -1749,7 +1749,7 @@
.curated-library-pane { .curated-library-pane {
border-right: 0px; border-right: 0px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid rgb(229, 231, 235);
display: block; display: block;
@@ -1788,9 +1788,9 @@
} }
.curated-search:focus-within { .curated-search:focus-within {
border-color: var(--action); border-color: rgb(142, 172, 239);
box-shadow: 0 0 0 3px var(--focus-ring); box-shadow: rgba(53, 106, 230, 0.1) 0px 0px 0px 3px;
} }
:where(#screenerView) .screener-page-heading { :where(#screenerView) .screener-page-heading {
@@ -1824,7 +1824,7 @@
} }
:where(#screenerView) .curated-library-heading h3 { :where(#screenerView) .curated-library-heading h3 {
color: var(--text-primary); color: rgb(17, 24, 39);
letter-spacing: 0px; letter-spacing: 0px;
} }
@@ -1870,7 +1870,7 @@
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--border); border-bottom: 1px solid rgb(232, 235, 239);
} }
:where(#screenerView) .curated-execution-bar { :where(#screenerView) .curated-execution-bar {
@@ -1890,7 +1890,7 @@
} }
#screenerView .regime-evidence strong { #screenerView .regime-evidence strong {
color: var(--warning-color); color: rgb(146, 88, 6);
font-size: 12px; font-size: 12px;
@@ -1902,7 +1902,7 @@
overflow: visible; overflow: visible;
color: var(--warning-color); color: rgb(162, 106, 24);
font-size: 10.5px; font-size: 10.5px;
@@ -2119,6 +2119,10 @@ body[data-active-view="screenerView"] .workspace-view {
} }
} }
body[data-active-view="screenerView"] .overview-strip {
display: grid;
}
@media (min-width: 901px) { @media (min-width: 901px) {
.screener-page-bar { .screener-page-bar {
min-height: 34px; min-height: 34px;
@@ -2346,7 +2350,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
:where(#screenerView) .curated-strategy-card:hover { :where(#screenerView) .curated-strategy-card:hover {
box-shadow: var(--shadow-raised); box-shadow: rgba(37, 99, 235, 0.08) 0px 4px 14px;
} }
:where(#screenerView) .quant-screener-panel { :where(#screenerView) .quant-screener-panel {
@@ -2410,7 +2414,7 @@ body[data-active-view="screenerView"] .workspace-view {
border-radius: 999px; border-radius: 999px;
background: var(--border-strong); background: rgb(223, 229, 238);
accent-color: var(--primary); accent-color: var(--primary);
@@ -2528,19 +2532,19 @@ body[data-active-view="screenerView"] .workspace-view {
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-page-title); font-size: 17px;
font-weight: var(--font-weight-semibold); font-weight: 800;
} }
#screenerView .screener-page-heading .section-subtitle { #screenerView .screener-page-heading .section-subtitle {
color: var(--r2-faint); color: var(--r2-faint);
font-size: var(--font-size-caption); font-size: 12px;
} }
#screenerView .screener-mode-tabs { #screenerView .screener-mode-tabs {
min-height: 32px; min-height: 34px;
display: inline-flex; display: inline-flex;
@@ -2658,7 +2662,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerView .step-line.complete { #screenerView .step-line.complete {
background: var(--market-down); background: rgb(134, 213, 160);
} }
#screenerView .screener-overview-card { #screenerView .screener-overview-card {
@@ -2708,7 +2712,7 @@ body[data-active-view="screenerView"] .workspace-view {
border-radius: 5px; border-radius: 5px;
background: var(--surface-muted); background: rgb(244, 246, 248);
color: var(--r2-sub); color: var(--r2-sub);
@@ -2762,7 +2766,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerView .regime-option:hover { #screenerView .regime-option:hover {
border-color: var(--border-strong); border-color: rgb(187, 200, 219);
color: var(--r2-ink); color: var(--r2-ink);
} }
@@ -2975,7 +2979,7 @@ body[data-active-view="screenerView"] .workspace-view {
#screenerView .result-toolbar .count-badge { #screenerView .result-toolbar .count-badge {
border-radius: 5px; border-radius: 5px;
background: var(--surface-muted); background: rgb(241, 243, 246);
color: var(--r2-sub); color: var(--r2-sub);
@@ -3035,29 +3039,25 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerView .data-table { #screenerView .data-table {
font-size: var(--font-size-table); font-size: 11.5px;
} }
#screenerView .data-table thead th { #screenerView .data-table thead th {
height: 36px; height: 39px;
padding: 0 12px; padding: 8px 11px;
background: var(--table-header); background: var(--table-header);
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-caption); font-size: 10.5px;
font-weight: var(--font-weight-semibold);
} }
#screenerView .data-table tbody td { #screenerView .data-table tbody td {
height: 40px; height: 45px;
padding: 0 12px; padding: 8px 11px;
font-size: var(--font-size-table);
} }
#screenerView .data-table .stock-name { #screenerView .data-table .stock-name {
@@ -3069,7 +3069,7 @@ body[data-active-view="screenerView"] .workspace-view {
overflow: hidden; overflow: hidden;
color: var(--text-secondary); color: rgb(95, 107, 124);
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@@ -3091,13 +3091,13 @@ body[data-active-view="screenerView"] .workspace-view {
#screenerView .probability-value small { #screenerView .probability-value small {
margin-top: 3px; margin-top: 3px;
font-weight: var(--font-weight-medium); font-weight: 500;
display: block; display: block;
color: var(--r2-faint); color: var(--r2-faint);
font-size: var(--font-size-aux); font-size: 9px;
} }
#screenerView .tracking-summary { #screenerView .tracking-summary {
@@ -3341,7 +3341,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerView .curated-school-filters button.active { #screenerView .curated-school-filters button.active {
border-color: var(--color-action-line); border-color: rgb(197, 212, 241);
background: var(--scr-blue-soft); background: var(--scr-blue-soft);
@@ -3562,7 +3562,7 @@ body[data-active-view="screenerView"] .workspace-view {
border-radius: 5px; border-radius: 5px;
background: var(--surface-muted); background: rgb(238, 241, 245);
color: var(--r2-sub); color: var(--r2-sub);
@@ -3598,7 +3598,7 @@ body[data-active-view="screenerView"] .workspace-view {
border-radius: 4px; border-radius: 4px;
background: var(--surface-muted); background: rgb(238, 241, 245);
color: var(--r2-faint); color: var(--r2-faint);
@@ -3953,7 +3953,7 @@ body[data-active-view="screenerView"] .workspace-view {
border-radius: 6px; border-radius: 6px;
background: var(--surface-subtle); background: rgb(247, 248, 250);
} }
#screenerView .quant-rule-rows { #screenerView .quant-rule-rows {
@@ -3989,7 +3989,7 @@ body[data-active-view="screenerView"] .workspace-view {
min-height: 30px; min-height: 30px;
border: 1px solid var(--border-strong); border: 1px solid rgb(220, 225, 232);
border-radius: 6px; border-radius: 6px;
@@ -4003,7 +4003,7 @@ body[data-active-view="screenerView"] .workspace-view {
min-height: 30px; min-height: 30px;
border: 1px solid var(--border-strong); border: 1px solid rgb(220, 225, 232);
border-radius: 6px; border-radius: 6px;
@@ -4075,7 +4075,7 @@ body[data-active-view="screenerView"] .workspace-view {
border-radius: 3px; border-radius: 3px;
background: var(--surface-muted); background: rgb(232, 235, 240);
} }
#screenerView .quant-weight-status i { #screenerView .quant-weight-status i {
@@ -4109,7 +4109,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerView .strategy-drawer::backdrop { #screenerView .strategy-drawer::backdrop {
background: var(--backdrop); background: rgba(20, 29, 44, 0.38);
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
} }
@@ -4587,7 +4587,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerTrackingView .tracking-back-button:hover { #screenerTrackingView .tracking-back-button:hover {
border-color: var(--color-action-line); border-color: rgb(185, 200, 228);
color: var(--scr-blue); color: var(--scr-blue);
} }
@@ -4743,27 +4743,23 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerTrackingView .data-table { #screenerTrackingView .data-table {
font-size: var(--font-size-table); font-size: 11.5px;
} }
#screenerTrackingView .data-table thead th { #screenerTrackingView .data-table thead th {
height: 36px; height: 39px;
padding: 0 12px; padding: 8px 11px;
background: var(--table-header); background: var(--table-header);
font-size: var(--font-size-caption); font-size: 10.5px;
font-weight: var(--font-weight-semibold);
} }
#screenerTrackingView .data-table tbody td { #screenerTrackingView .data-table tbody td {
height: 40px; height: 46px;
padding: 0 12px; padding: 8px 11px;
font-size: var(--font-size-table);
} }
#screenerTrackingView .stock-cell { #screenerTrackingView .stock-cell {
@@ -4779,7 +4775,7 @@ body[data-active-view="screenerView"] .workspace-view {
#screenerTrackingView .stock-cell small { #screenerTrackingView .stock-cell small {
color: var(--r2-faint); color: var(--r2-faint);
font-size: var(--font-size-aux); font-size: 9px;
} }
#screenerTrackingView .table-action.danger { #screenerTrackingView .table-action.danger {
@@ -5499,7 +5495,7 @@ body[data-active-view="screenerView"] .workspace-view {
flex-direction: column; flex-direction: column;
border: 1px solid var(--market-up); border: 1px solid rgb(243, 201, 195);
border-radius: 9px; border-radius: 9px;
@@ -5681,7 +5677,7 @@ body[data-active-view="screenerView"] .workspace-view {
} }
#screenerView .screener-tracking-entry:focus-visible { #screenerView .screener-tracking-entry:focus-visible {
outline: 2px solid var(--action); outline: rgba(37, 99, 235, 0.45) solid 2px;
outline-offset: 2px; outline-offset: 2px;
} }
@@ -5733,11 +5729,11 @@ body[data-active-view="screenerView"] .workspace-view {
padding: 3px 8px; padding: 3px 8px;
border: 1px solid var(--border-strong); border: 1px solid rgb(217, 226, 239);
border-radius: 5px; border-radius: 5px;
background: var(--surface-subtle); background: rgb(247, 249, 252);
color: var(--r2-sub); color: var(--r2-sub);
@@ -5870,7 +5866,7 @@ body[data-active-view="screenerView"] .workspace-view {
width: 100%; width: 100%;
min-width: 1376px; min-width: 1180px;
table-layout: fixed; table-layout: fixed;
} }
@@ -5891,12 +5887,12 @@ body[data-active-view="screenerView"] .workspace-view {
width: 78px; width: 78px;
} }
#screenerView .screener-result-columns col:nth-child(5), #screenerView .screener-result-columns col:nth-child(5) {
#screenerView .screener-result-columns col:nth-child(6), width: 104px;
#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(8),
#screenerView .screener-result-columns col:nth-child(9) { #screenerView .screener-result-columns col:nth-child(9) {
width: 82px; width: 82px;
@@ -6567,10 +6563,3 @@ body[data-active-view="screenerView"] .workspace-view {
gap: 3px; 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;
}
+81 -85
View File
@@ -218,11 +218,7 @@
.sentiment-history-table thead th { .sentiment-history-table thead th {
border-bottom-color: rgb(216, 224, 230); border-bottom-color: rgb(216, 224, 230);
height: 36px; font-size: 11.5px;
font-size: var(--fs-caption);
font-weight: 600;
} }
.sentiment-history-table .sentiment-history-groups th { .sentiment-history-table .sentiment-history-groups th {
@@ -317,10 +313,24 @@
background: var(--table-header); background: var(--table-header);
} }
.sentiment-history-table tbody tr:nth-child(2n) td {
background: var(--surface-subtle);
}
.sentiment-history-table tbody tr:hover td { .sentiment-history-table tbody tr:hover td {
background: var(--table-hover); 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 { .sentiment-score-cell {
font-weight: 750; font-weight: 750;
} }
@@ -367,15 +377,15 @@
} }
.sentiment-phase-badge.phase-repair { .sentiment-phase-badge.phase-repair {
background: var(--accent-soft); background: rgb(231, 246, 246);
color: var(--accent); color: rgb(11, 109, 116);
} }
.sentiment-phase-badge.phase-fermentation { .sentiment-phase-badge.phase-fermentation {
background: var(--up-soft); background: rgb(237, 246, 234);
color: var(--up); color: rgb(61, 113, 51);
} }
.sentiment-phase-badge.phase-climax { .sentiment-phase-badge.phase-climax {
@@ -874,21 +884,19 @@
min-width: 0px; min-width: 0px;
min-height: 0px; min-height: 33px;
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;
flex-direction: column; flex-direction: row;
justify-content: center; align-items: center;
align-items: flex-start; gap: 6px;
gap: 2px; padding: 0px 10px 0px 0px;
padding: 0 16px;
border: 0px; border: 0px;
} }
@@ -916,11 +924,11 @@
} }
.overview-strip .sentiment-text { .overview-strip .sentiment-text {
color: var(--accent); color: var(--r2-ink);
font-size: var(--fs-caption); font-size: 12px;
font-weight: 500; font-weight: 600;
white-space: nowrap; white-space: nowrap;
} }
@@ -1029,31 +1037,25 @@
align-items: flex-start; align-items: flex-start;
gap: 14px; gap: 16px;
padding: 14px 16px 10px; padding: 14px 16px;
} }
.sentiment-current-phase-badge { .sentiment-current-phase-badge {
min-width: 92px; min-width: 94px;
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: block;
flex-direction: column; padding: 10px 18px;
align-items: center; border: 1px solid rgb(245, 207, 201);
gap: 2px; border-radius: 10px;
padding: 10px 16px; background: var(--r2-up-soft);
border: 1px solid var(--accent);
border-radius: var(--card-radius);
background: var(--accent-soft);
text-align: center; text-align: center;
} }
@@ -1061,11 +1063,11 @@
.sentiment-current-phase-badge strong { .sentiment-current-phase-badge strong {
display: block; display: block;
color: var(--accent); color: var(--r2-up);
font-size: 19px; font-size: 19px;
font-weight: 700; font-weight: 800;
line-height: 1.35; line-height: 1.35;
} }
@@ -1073,11 +1075,11 @@
.sentiment-current-phase-badge span { .sentiment-current-phase-badge span {
display: block; display: block;
margin-top: 0; margin-top: 2px;
color: var(--text-3); color: var(--r2-sub);
font-size: var(--fs-aux); font-size: 11px;
white-space: nowrap; white-space: nowrap;
} }
@@ -1089,17 +1091,15 @@
} }
.sentiment-phase-info p { .sentiment-phase-info p {
color: var(--text-2); color: var(--r2-sub);
font-size: var(--fs-label); font-size: 12.5px;
line-height: 1.6; line-height: 1.7;
} }
.sentiment-phase-info p b { .sentiment-phase-info p b {
color: var(--text-1); font-weight: 700;
font-weight: 600;
} }
.sentiment-phase-info p .down { .sentiment-phase-info p .down {
@@ -1113,17 +1113,17 @@
.sentiment-phase-advice { .sentiment-phase-advice {
margin-top: 8px; margin-top: 8px;
padding: 8px 10px; padding: 5px 9px;
border-radius: var(--radius-md); border-radius: 6px;
background: var(--surface-muted); background: var(--r2-amber-soft);
color: var(--text-2); color: var(--r2-amber);
font-size: var(--fs-caption); font-size: 12px;
line-height: 1.55; line-height: 1.6;
} }
.sentiment-feedback-strip { .sentiment-feedback-strip {
@@ -1131,47 +1131,45 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
border-top: 1px solid var(--border); border-top: 1px solid var(--r2-line-soft);
} }
.sentiment-feedback-strip > span { .sentiment-feedback-strip > span {
min-width: 0px; min-width: 0px;
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: auto 1fr;
align-items: baseline; gap: 2px 8px;
gap: 2px 6px; padding: 8px 12px;
padding: 9px 14px; color: var(--r2-faint);
color: var(--text-3); font-size: 10.5px;
font-size: var(--fs-caption);
} }
.sentiment-feedback-strip > span + span { .sentiment-feedback-strip > span + span {
border-left: 1px solid var(--border); border-left: 1px solid var(--r2-line-soft);
} }
.sentiment-feedback-strip strong { .sentiment-feedback-strip strong {
color: var(--text-1); color: var(--r2-ink);
font-size: inherit; font-size: 11.5px;
font-weight: 600; text-align: right;
text-align: left;
} }
.sentiment-feedback-strip small { .sentiment-feedback-strip small {
grid-column: 1 / -1;
overflow: hidden; overflow: hidden;
color: var(--text-3); color: var(--r2-sub);
font-size: var(--fs-aux); font-size: 10px;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -1311,21 +1309,19 @@
.redesigned-sentiment-view .sentiment-history-table td, .redesigned-sentiment-view .sentiment-history-table td,
.redesigned-sentiment-view .sentiment-history-table th { .redesigned-sentiment-view .sentiment-history-table th {
height: 40px; height: auto;
padding: 0 10px; padding: 8px 10px;
border-bottom: 1px solid var(--r2-line-soft); border-bottom: 1px solid var(--r2-line-soft);
} }
.redesigned-sentiment-view .sentiment-history-table thead th { .redesigned-sentiment-view .sentiment-history-table thead th {
height: 36px;
background: var(--table-header); background: var(--table-header);
color: var(--text-2); color: var(--r2-sub);
font-size: var(--fs-caption); font-size: 12px;
font-weight: 600; font-weight: 600;
} }
@@ -1403,17 +1399,17 @@
} }
.overview-strip .sentiment-block .sentiment-text { .overview-strip .sentiment-block .sentiment-text {
display: inline-flex; display: block;
margin: 0px; margin: 0px;
padding: 0 6px; padding: 0px;
font-size: var(--fs-caption); font-size: 11px;
font-weight: 500; font-weight: 500;
line-height: 18px; line-height: 1;
letter-spacing: 0px; letter-spacing: 0px;
@@ -1482,19 +1478,19 @@
} }
.overview-strip[data-overview-expanded="true"] .sentiment-block { .overview-strip[data-overview-expanded="true"] .sentiment-block {
min-height: 0; min-height: 76px;
justify-content: center; justify-content: center;
gap: 2px; gap: 4px;
padding: 0 16px; padding: 10px 16px;
flex-direction: column; flex-direction: row;
align-items: flex-start; align-items: center;
border-right: 0; border-right: 1px solid var(--line-soft);
background: transparent; background: transparent;
} }
@@ -1632,9 +1628,9 @@
} }
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-fermentation { :root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-fermentation {
background: var(--market-up-soft); background: var(--market-down-soft);
color: var(--market-up); color: var(--market-down);
} }
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-climax { :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; empty.hidden = rows.length > 0;
body.innerHTML = [...rows].reverse().map((row) => { body.innerHTML = [...rows].reverse().map((row) => {
return ` return `
<tr> <tr class="${row.trade_date === latest?.trade_date ? "latest-row" : ""}">
<td class="sentiment-date-cell">${escapeHtml(displayCompactDate(row.trade_date))}</td> <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 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> <td><span class="sentiment-phase-badge ${sentimentPhaseClass(row.phase)}">${escapeHtml(row.phase)}</span></td>
+30 -37
View File
@@ -184,15 +184,15 @@
padding: 9px 13px; padding: 9px 13px;
border-bottom-color: var(--border); border-bottom-color: rgb(231, 235, 239);
} }
.theme-directory-item:hover { .theme-directory-item:hover {
background: var(--hover); background: rgb(241, 246, 251);
} }
.theme-directory-item.active { .theme-directory-item.active {
background: var(--selected); background: rgb(234, 242, 251);
box-shadow: inset 3px 0 var(--action); box-shadow: inset 3px 0 var(--action);
} }
@@ -294,10 +294,6 @@
margin: 0px; margin: 0px;
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-page-title);
font-weight: var(--font-weight-semibold);
} }
.theme-title-v2 > div > span { .theme-title-v2 > div > span {
@@ -335,7 +331,7 @@
.theme-search-v2 { .theme-search-v2 {
width: 250px; width: 250px;
height: 32px; height: 34px;
display: flex; display: flex;
@@ -345,9 +341,9 @@
padding: 0px 10px; padding: 0px 10px;
border: 1px solid var(--border-strong); border: 1px solid var(--control-border);
border-radius: 8px; border-radius: 7px;
background: var(--surface); background: var(--surface);
@@ -393,15 +389,11 @@
} }
.theme-refresh-v2 { .theme-refresh-v2 {
min-height: 32px; min-height: 34px;
height: 32px;
padding: 0px 12px; padding: 0px 12px;
border-radius: 8px; border-radius: 7px;
font-size: var(--font-size-label);
} }
.theme-refresh-v2 .lucide { .theme-refresh-v2 .lucide {
@@ -449,17 +441,17 @@
} }
.theme-summary-v2 span { .theme-summary-v2 span {
color: var(--text-3); color: var(--r2-sub);
font-size: var(--font-size-caption); font-size: 11px;
} }
.theme-summary-v2 strong { .theme-summary-v2 strong {
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-metric); font-size: 19px;
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-bold);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -535,9 +527,9 @@
color: var(--r2-ink); color: var(--r2-ink);
font-size: var(--font-size-card-title); font-size: 14px;
font-weight: var(--font-weight-semibold); font-weight: 700;
} }
.theme-card-head-v2 span { .theme-card-head-v2 span {
@@ -683,11 +675,11 @@
.theme-rank-v2 { .theme-rank-v2 {
color: var(--r2-faint); color: var(--r2-faint);
font-size: var(--font-size-aux); font-size: 10.5px;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-align: left; text-align: center;
} }
.theme-directory-item-v2:nth-child(-n+3) .theme-rank-v2 { .theme-directory-item-v2:nth-child(-n+3) .theme-rank-v2 {
@@ -1036,9 +1028,9 @@
z-index: 2; z-index: 2;
height: 36px; height: 34px;
padding: 0 12px; padding: 6px 11px;
border-bottom-color: var(--r2-line); border-bottom-color: var(--r2-line);
@@ -1046,20 +1038,15 @@
color: var(--r2-sub); color: var(--r2-sub);
font-size: var(--font-size-caption); font-size: 10.5px;
} }
.theme-members-table-v2 tbody td { .theme-members-table-v2 tbody td {
height: 40px; height: 39px;
padding: 0 12px; padding: 7px 11px;
font-size: var(--font-size-table); font-size: 11.5px;
}
.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 { .theme-members-table-v2 tbody tr {
@@ -1089,6 +1076,10 @@
overflow: hidden; overflow: hidden;
} }
body[data-active-view="themeLibraryView"] .overview-strip {
flex: 0 0 auto;
}
body[data-active-view="themeLibraryView"] #themeLibraryView.active-view { body[data-active-view="themeLibraryView"] #themeLibraryView.active-view {
min-height: 0px; min-height: 0px;
@@ -1193,9 +1184,11 @@
} }
.theme-members-table-v2 tbody td { .theme-members-table-v2 tbody td {
height: 40px; height: 35px;
padding: 0 12px; padding-top: 5px;
padding-bottom: 5px;
} }
} }
+1 -1
View File
@@ -46,7 +46,7 @@
<strong id="themeMemberCount">0 只</strong> <strong id="themeMemberCount">0 只</strong>
</header> </header>
<div class="theme-members-frame-v2 tbl-wrap"> <div class="theme-members-frame-v2 tbl-wrap">
<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> <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>
</div> </div>
</section> </section>
</div> </div>
+1 -1
View File
@@ -97,7 +97,7 @@ function renderThemeDetail() {
setText("themeMemberCount", `有行情 ${number(summary.quoted_count)} / ${number(summary.member_count)}`); setText("themeMemberCount", `有行情 ${number(summary.quoted_count)} / ${number(summary.member_count)}`);
const body = document.querySelector("#themeMemberTableBody"); const body = document.querySelector("#themeMemberTableBody");
body.innerHTML = (payload.members || []).map((row, index) => ` body.innerHTML = (payload.members || []).map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"><td class="row-number muted">${index + 1}</td> <tr data-code="${escapeHtml(row.code)}"><td class="row-number num 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><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 ${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(""); <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,11 +8,6 @@ async function backfillData() {
end_date: document.querySelector("#backfillEnd").value, end_date: document.querySelector("#backfillEnd").value,
}); });
showToast(`历史回补完成,共处理 ${payload.results.length} 个工作日`); showToast(`历史回补完成,共处理 ${payload.results.length} 个工作日`);
state.sentimentHistory = null;
state.sentimentHistoryKey = "";
if (state.activeView === "sentimentCycleView") {
await loadSentimentHistory(true);
}
await openAdminSettings(true); await openAdminSettings(true);
} catch (error) { } catch (error) {
showToast(error.message); showToast(error.message);
+89 -103
View File
@@ -12,7 +12,7 @@
padding: 20px; padding: 20px;
background: var(--canvas); background: rgb(237, 241, 244);
} }
.auth-gate[hidden] { .auth-gate[hidden] {
@@ -26,13 +26,13 @@
padding: 26px; padding: 26px;
border: 1px solid var(--border-strong); border: 1px solid var(--line-strong);
border-radius: 12px; border-radius: 6px;
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-raised); box-shadow: var(--shadow);
} }
.auth-brand { .auth-brand {
@@ -46,9 +46,7 @@
.auth-brand h1 { .auth-brand h1 {
margin: 0px; margin: 0px;
font-size: var(--font-size-page-title); font-size: 22px;
font-weight: var(--font-weight-semibold);
} }
.auth-brand span { .auth-brand span {
@@ -56,9 +54,9 @@
margin-top: 5px; margin-top: 5px;
color: var(--text-3); color: var(--text-muted);
font-size: var(--font-size-caption); font-size: 12px;
} }
.auth-tabs { .auth-tabs {
@@ -90,11 +88,11 @@
} }
.auth-tab.active { .auth-tab.active {
border-bottom-color: var(--action); border-bottom-color: var(--coral);
color: var(--text-1); color: var(--text);
font-weight: var(--font-weight-semibold); font-weight: 750;
} }
.auth-form { .auth-form {
@@ -108,15 +106,15 @@
.auth-form .button { .auth-form .button {
width: 100%; width: 100%;
min-height: 32px; min-height: 40px;
} }
.auth-error { .auth-error {
margin: 0px; margin: 0px;
color: var(--market-up); color: rgb(185, 54, 39);
font-size: var(--font-size-caption); font-size: 12px;
line-height: 1.5; line-height: 1.5;
} }
@@ -148,23 +146,23 @@
.admin-section-picker select { .admin-section-picker select {
width: 100%; width: 100%;
min-height: 32px; min-height: 40px;
padding: 0px 11px; padding: 0px 11px;
border: 1px solid var(--border-strong); border: 1px solid var(--line-strong);
border-radius: 8px; border-radius: 5px;
background: var(--surface); background: var(--surface);
color: var(--text-primary); color: var(--text-primary);
font-size: var(--font-size-label); font-size: 13px;
} }
.admin-section-picker select:focus-visible { .admin-section-picker select:focus-visible {
outline: 2px solid var(--action); outline: 2px solid var(--blue);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -309,13 +307,13 @@
min-width: 0px; min-width: 0px;
height: 32px; height: 34px;
padding: 0px 8px; padding: 0px 8px;
border: 1px solid var(--border-strong); border: 1px solid var(--line-strong);
border-radius: 8px; border-radius: 4px;
background: var(--surface); background: var(--surface);
} }
@@ -388,11 +386,11 @@
} }
.connection-status.connected { .connection-status.connected {
border-color: var(--market-down); border-color: rgb(167, 222, 201);
background: var(--green-soft); background: var(--green-soft);
color: var(--market-down); color: rgb(8, 106, 75);
} }
.settings-dialog form { .settings-dialog form {
@@ -472,11 +470,11 @@
} }
.account-button > span { .account-button > span {
flex: 0 0 auto; min-width: 0px;
min-width: max-content; overflow: hidden;
overflow: visible; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -596,7 +594,7 @@
.account-dropdown button { .account-dropdown button {
width: 100%; width: 100%;
min-height: 36px; min-height: 40px;
display: grid; display: grid;
@@ -638,7 +636,7 @@
outline: none; outline: none;
box-shadow: inset 0 0 0 2px var(--focus-ring); box-shadow: inset 0 0 0 2px var(--focus-ring, #1268c4);
} }
.account-dropdown button svg { .account-dropdown button svg {
@@ -664,16 +662,16 @@
} }
.account-dropdown .account-menu-danger { .account-dropdown .account-menu-danger {
color: var(--market-up); color: rgb(181, 58, 66);
grid-template-columns: 18px minmax(0px, 1fr); grid-template-columns: 18px minmax(0px, 1fr);
} }
.account-dropdown .account-menu-danger:focus-visible, .account-dropdown .account-menu-danger:focus-visible,
.account-dropdown .account-menu-danger:hover { .account-dropdown .account-menu-danger:hover {
color: var(--market-up); color: rgb(163, 47, 55);
background: var(--market-up-soft); background: rgb(255, 242, 243);
} }
button.account-role-badge { button.account-role-badge {
@@ -687,11 +685,11 @@ button.account-role-badge {
button.account-role-badge:hover { button.account-role-badge:hover {
filter: brightness(0.98); filter: brightness(0.98);
box-shadow: var(--shadow-card); box-shadow: rgba(67, 76, 86, 0.14) 0px 3px 10px;
} }
button.account-role-badge:focus-visible { button.account-role-badge:focus-visible {
outline: 2px solid var(--action); outline: 2px solid var(--blue);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -703,9 +701,9 @@ button.account-role-badge:focus-visible {
} }
.admin-role-badge { .admin-role-badge {
color: var(--text-secondary); color: rgb(83, 103, 124);
background: var(--surface-muted); background: rgb(244, 247, 250);
} }
.vip-role-badge { .vip-role-badge {
@@ -721,7 +719,7 @@ button.account-role-badge:focus-visible {
.vip-role-badge.is-nonmember { .vip-role-badge.is-nonmember {
color: rgb(105, 117, 128); color: rgb(105, 117, 128);
background: var(--surface-muted); background: rgb(244, 246, 248);
border-color: rgb(203, 211, 218); border-color: rgb(203, 211, 218);
@@ -827,7 +825,7 @@ button.account-role-badge:focus-visible {
} }
.member-gate strong { .member-gate strong {
color: var(--text-primary); color: rgb(60, 70, 80);
font-size: 14px; font-size: 14px;
} }
@@ -879,7 +877,7 @@ button.account-role-badge:focus-visible {
margin-bottom: 12px; margin-bottom: 12px;
color: var(--text-secondary); color: rgb(83, 103, 124);
} }
.privacy-note svg { .privacy-note svg {
@@ -891,7 +889,7 @@ button.account-role-badge:focus-visible {
margin-top: 3px; margin-top: 3px;
color: var(--market-down); color: rgb(52, 116, 95);
} }
.membership-status-grid { .membership-status-grid {
@@ -907,17 +905,17 @@ button.account-role-badge:focus-visible {
.membership-status-grid > div { .membership-status-grid > div {
padding: 12px 13px; padding: 12px 13px;
border: 1px solid var(--border); border: 1px solid var(--line, #e2e8ee);
border-radius: 10px; border-radius: 10px;
background: var(--surface-subtle); background: rgb(251, 252, 253);
} }
.membership-status-grid span { .membership-status-grid span {
display: block; display: block;
color: var(--text-tertiary); color: rgb(116, 128, 140);
font-size: 11px; font-size: 11px;
} }
@@ -927,7 +925,7 @@ button.account-role-badge:focus-visible {
margin-top: 5px; margin-top: 5px;
color: var(--text-primary); color: rgb(38, 50, 61);
font-size: 15px; font-size: 15px;
@@ -937,7 +935,7 @@ button.account-role-badge:focus-visible {
.membership-comparison { .membership-comparison {
overflow: hidden; overflow: hidden;
border: 1px solid var(--border); border: 1px solid var(--line, #e2e8ee);
border-radius: 10px; border-radius: 10px;
@@ -955,7 +953,7 @@ button.account-role-badge:focus-visible {
padding: 9px 11px; padding: 9px 11px;
border-top: 1px solid var(--border); border-top: 1px solid var(--line, #e2e8ee);
} }
.membership-comparison > div:first-child { .membership-comparison > div:first-child {
@@ -963,21 +961,21 @@ button.account-role-badge:focus-visible {
} }
.membership-comparison-head { .membership-comparison-head {
color: var(--text-secondary); color: rgb(105, 118, 131);
background: var(--surface-subtle); background: rgb(247, 249, 251);
font-weight: 700; font-weight: 700;
} }
.membership-comparison b { .membership-comparison b {
color: var(--market-down); color: rgb(39, 108, 88);
font-weight: 600; font-weight: 600;
} }
.membership-comparison b.muted { .membership-comparison b.muted {
color: var(--text-tertiary); color: rgb(154, 164, 173);
} }
.membership-comparison b.available { .membership-comparison b.available {
@@ -995,7 +993,7 @@ button.account-role-badge:focus-visible {
margin-top: 14px; margin-top: 14px;
color: var(--text-secondary); color: rgb(93, 105, 116);
font-size: 12px; font-size: 12px;
} }
@@ -1028,15 +1026,11 @@ button.account-role-badge:focus-visible {
display: grid; display: grid;
grid-template-columns: minmax(0px, 1fr); grid-template-columns: auto minmax(0px, 1fr);
} }
.account-menu-shell .account-button { .account-menu-shell .account-button {
width: 100%;
min-width: 0px; min-width: 0px;
max-width: none;
} }
.account-dropdown { .account-dropdown {
@@ -1091,7 +1085,7 @@ button.account-role-badge:focus-visible {
} }
.account-settings-dialog { .account-settings-dialog {
border-radius: 12px; border-radius: 9px;
} }
.settings-dialog { .settings-dialog {
@@ -1101,7 +1095,7 @@ button.account-role-badge:focus-visible {
overflow-x: hidden; overflow-x: hidden;
border-radius: 12px; border-radius: 9px;
} }
.account-dropdown { .account-dropdown {
@@ -1121,17 +1115,17 @@ button.account-role-badge:focus-visible {
padding: 7px; padding: 7px;
background: var(--surface); background: rgba(255, 255, 255, 0.98);
transform-origin: right top; transform-origin: right top;
animation: account-menu-in 180ms var(--ease-out) both; animation: account-menu-in 180ms var(--ease-out) both;
border: 1px solid var(--border); border-color: var(--border);
border-radius: 8px; border-radius: 7px;
box-shadow: var(--shadow-float); box-shadow: rgba(22, 34, 46, 0.15) 0px 14px 35px;
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@@ -1183,37 +1177,29 @@ button.account-role-badge:focus-visible {
gap: 3px; gap: 3px;
} }
@media (min-width: 1280px) and (max-width: 1439px) { @media (min-width: 1280px) and (max-width: 1510px) {
.app-header .header-command-group .account-role-badge span { .account-role-badge span {
display: none; display: none;
} }
.app-header .header-command-group .account-role-badge { .account-role-badge {
min-width: 28px; min-width: 27px;
justify-content: center; justify-content: center;
padding-inline: 6px;
} }
} }
@media (min-width: 1280px) { @media (min-width: 1380px) and (max-width: 1510px) {
#settingsButton,
.header-command-group .account-button { .header-command-group .account-button {
flex: 0 0 auto;
width: auto; width: auto;
max-width: none; padding: 0px 9px;
overflow: visible;
padding: 0px 8px;
} }
#settingsButton > span,
.header-command-group .account-button > span { .header-command-group .account-button > span {
display: inline; display: inline;
overflow: visible;
} }
} }
@@ -1228,7 +1214,7 @@ button.account-role-badge:focus-visible {
border-radius: 7px; border-radius: 7px;
background: var(--action); background: rgb(37, 99, 235);
color: var(--text-inverse); color: var(--text-inverse);
@@ -1276,11 +1262,11 @@ button.account-role-badge:focus-visible {
} }
.account-button { .account-button {
flex: 0 0 auto; overflow: hidden;
overflow: visible; text-overflow: ellipsis;
max-width: none; max-width: 108px;
min-height: 28px; min-height: 28px;
@@ -1322,17 +1308,17 @@ button.account-role-badge:focus-visible {
.settings-dialog:not(.heaven-reading-dialog) .settings-section-heading h3 { .settings-dialog:not(.heaven-reading-dialog) .settings-section-heading h3 {
margin: 0px; margin: 0px;
color: var(--text-primary); color: rgb(39, 52, 67);
font-size: var(--font-size-card-title); font-size: 14px;
font-weight: var(--font-weight-semibold); font-weight: 720;
} }
.settings-dialog:not(.heaven-reading-dialog) .settings-section-heading > span { .settings-dialog:not(.heaven-reading-dialog) .settings-section-heading > span {
color: var(--text-tertiary); color: rgb(124, 135, 149);
font-size: var(--font-size-aux); font-size: 10.5px;
} }
.settings-dialog:not(.heaven-reading-dialog) .form-field { .settings-dialog:not(.heaven-reading-dialog) .form-field {
@@ -1341,7 +1327,7 @@ button.account-role-badge:focus-visible {
.settings-dialog:not(.heaven-reading-dialog) .form-field > label, .settings-dialog:not(.heaven-reading-dialog) .form-field > label,
.settings-dialog:not(.heaven-reading-dialog) .form-field > span { .settings-dialog:not(.heaven-reading-dialog) .form-field > span {
color: var(--text-secondary); color: rgb(78, 91, 106);
font-size: 11px; font-size: 11px;
@@ -1349,7 +1335,7 @@ button.account-role-badge:focus-visible {
} }
.settings-dialog:not(.heaven-reading-dialog) :is(input, select, textarea) { .settings-dialog:not(.heaven-reading-dialog) :is(input, select, textarea) {
border-color: var(--border-strong); border-color: rgb(213, 220, 229);
border-radius: 7px; border-radius: 7px;
} }
@@ -1389,7 +1375,7 @@ button.account-role-badge:focus-visible {
} }
.account-settings-dialog .settings-lead { .account-settings-dialog .settings-lead {
color: var(--text-secondary); color: rgb(96, 109, 124);
font-size: 12px; font-size: 12px;
@@ -1411,7 +1397,7 @@ button.account-role-badge:focus-visible {
padding: 11px 12px; padding: 11px 12px;
border-color: var(--border-strong); border-color: rgb(223, 228, 234);
border-radius: 7px; border-radius: 7px;
@@ -1439,7 +1425,7 @@ button.account-role-badge:focus-visible {
} }
.account-settings-dialog .membership-comparison { .account-settings-dialog .membership-comparison {
border-color: var(--border-strong); border-color: rgb(223, 228, 234);
border-radius: 7px; border-radius: 7px;
} }
@@ -1449,7 +1435,7 @@ button.account-role-badge:focus-visible {
padding: 8px 11px; padding: 8px 11px;
border-color: var(--border); border-color: rgb(230, 234, 239);
} }
.account-settings-dialog .membership-comparison-head { .account-settings-dialog .membership-comparison-head {
@@ -1463,11 +1449,11 @@ button.account-role-badge:focus-visible {
.account-settings-dialog .privacy-note { .account-settings-dialog .privacy-note {
padding: 10px 11px; padding: 10px 11px;
border: 1px solid var(--market-down); border: 1px solid rgb(220, 232, 226);
border-radius: 7px; border-radius: 7px;
background: var(--market-down-soft); background: rgb(245, 250, 247);
} }
.account-settings-dialog .account-birth-form { .account-settings-dialog .account-birth-form {
@@ -1489,7 +1475,7 @@ button.account-role-badge:focus-visible {
border-radius: 7px; border-radius: 7px;
color: var(--text-secondary); color: rgb(102, 115, 132);
font-size: 11px; font-size: 11px;
} }
@@ -1503,7 +1489,7 @@ button.account-role-badge:focus-visible {
border-bottom: 1px solid var(--dialog-line); border-bottom: 1px solid var(--dialog-line);
background: var(--surface-subtle); background: rgb(251, 252, 253);
} }
.admin-dialog .admin-section-picker label { .admin-dialog .admin-section-picker label {
@@ -1511,13 +1497,13 @@ button.account-role-badge:focus-visible {
} }
.admin-dialog .admin-section-picker select { .admin-dialog .admin-section-picker select {
min-height: 32px; min-height: 36px;
border-color: var(--border-strong); border-color: rgb(212, 220, 229);
border-radius: 8px; border-radius: 7px;
font-size: var(--font-size-label); font-size: 12px;
} }
.admin-dialog .admin-panel { .admin-dialog .admin-panel {
@@ -1649,7 +1635,7 @@ button.account-role-badge:focus-visible {
} }
:root[data-theme="dark"] :is(.loading-overlay, .auth-gate) { :root[data-theme="dark"] :is(.loading-overlay, .auth-gate) {
background: var(--backdrop); background: color-mix(in srgb, var(--canvas) 92%, transparent);
} }
:root[data-theme="dark"] :is(.auth-shell) { :root[data-theme="dark"] :is(.auth-shell) {
+17 -41
View File
@@ -1,15 +1,11 @@
/* Canonical CSS owner: cards. Historical layers consolidated 2026-08-02. */ /* Canonical CSS owner: cards. Historical layers consolidated 2026-08-02. */
.stock-name { .stock-name {
font-size: var(--fs-table); font-weight: 700;
font-weight: 600;
} }
.stock-code { .stock-code {
color: var(--text-3); color: var(--text-muted);
font-size: var(--fs-aux);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -53,37 +49,25 @@
} }
.streak-pill { .streak-pill {
background: var(--coral-soft);
color: var(--coral);
font-weight: 700;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; min-height: 24px;
min-width: 30px; padding: 2px 8px;
height: 20px; border-radius: 4px;
padding: 0 7px;
border-radius: 999px;
background: var(--up-soft);
color: var(--up);
font-size: var(--fs-caption);
font-weight: 600;
white-space: nowrap; white-space: nowrap;
} }
.streak-pill.high {
background: var(--up);
color: #fff;
}
.outcome-tag { .outcome-tag {
display: inline-flex; display: inline-flex;
@@ -243,7 +227,7 @@
font-size: var(--font-size-card-title); font-size: var(--font-size-card-title);
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-bold);
} }
.card-h .sub { .card-h .sub {
@@ -269,29 +253,21 @@
} }
.sname { .sname {
font-weight: 600; font-weight: 700;
font-size: var(--fs-table); font-size: 13px;
} }
.scode { .scode {
font-size: var(--fs-aux); font-size: 11px;
color: var(--text-3); color: var(--faint);
margin-left: 0; margin-left: 6px;
font-weight: 400; font-weight: 400;
} }
.stock-cell {
display: flex;
flex-direction: column;
line-height: 1.3;
}
.muted { .muted {
color: var(--faint); color: var(--faint);
} }
+4 -6
View File
@@ -635,16 +635,14 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
height: 14px; height: 14px;
} }
@media (min-width: 1280px) { @media (min-width: 1280px) and (max-width: 1510px) {
.header-command-group .command-button:not(.account-button) { .header-command-group .command-button {
width: 32px; width: 31px;
min-width: 32px;
padding: 0px; padding: 0px;
} }
.header-command-group .command-button:not(.account-button) > span { .header-command-group .command-button > span {
display: none; display: none;
} }
} }
+26 -28
View File
@@ -10,9 +10,7 @@
.dialog-header h2 { .dialog-header h2 {
margin: 2px 0px 0px; margin: 2px 0px 0px;
font-size: 16px; font-size: 19px;
font-weight: var(--font-weight-semibold);
letter-spacing: 0px; letter-spacing: 0px;
} }
@@ -38,9 +36,9 @@
} }
.admin-dialog .model-row { .admin-dialog .model-row {
border-color: var(--border); border-color: rgb(223, 228, 234);
border-radius: 8px; border-radius: 7px;
background: var(--surface-subtle); background: var(--surface-subtle);
} }
@@ -242,11 +240,11 @@
} }
.alert-button.has-alerts { .alert-button.has-alerts {
border-color: var(--warning-color); border-color: rgb(214, 180, 93);
background: var(--warning-soft); background: rgb(255, 249, 233);
color: var(--warning-color); color: rgb(138, 97, 0);
} }
.alert-badge { .alert-badge {
@@ -528,17 +526,17 @@
} }
.assistant-message.user .assistant-message-content { .assistant-message.user .assistant-message-content {
border-color: var(--color-action-line); border-color: rgb(190, 210, 235);
background: var(--action-soft); background: var(--action-soft);
} }
.assistant-message.is-error .assistant-message-content { .assistant-message.is-error .assistant-message-content {
border-color: var(--market-up); border-color: rgb(239, 197, 200);
background: var(--market-up-soft); background: var(--market-up-soft);
color: var(--market-up); color: rgb(139, 47, 52);
} }
.assistant-message-content p { .assistant-message-content p {
@@ -780,7 +778,7 @@
gap: 16px; gap: 16px;
padding: 20px; padding: 14px 18px;
min-height: 62px; min-height: 62px;
@@ -788,7 +786,7 @@
} }
.assistant-dialog { .assistant-dialog {
border-radius: 12px; border-radius: 9px;
width: min(820px, -32px + 100vw); width: min(820px, -32px + 100vw);
@@ -798,9 +796,9 @@
} }
.dialog-eyebrow { .dialog-eyebrow {
color: var(--text-3); color: var(--text-muted);
font-size: var(--font-size-caption); font-size: 12px;
} }
.global-search-dialog { .global-search-dialog {
@@ -808,7 +806,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: var(--shadow-float); box-shadow: rgba(26, 37, 47, 0.22) 0px 22px 70px, rgba(26, 37, 47, 0.1) 0px 3px 14px;
max-width: 700px; max-width: 700px;
@@ -894,13 +892,13 @@
font: 11px / 1.2 ui-monospace, SFMono-Regular, Consolas, monospace; font: 11px / 1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
border-color: var(--border-strong); border-color: rgb(214, 221, 229);
border-radius: 5px; border-radius: 5px;
background: var(--surface-subtle); background: rgb(247, 248, 250);
color: var(--text-tertiary); color: rgb(125, 136, 150);
} }
.global-search-results { .global-search-results {
@@ -922,7 +920,7 @@
padding: 9px 10px 5px; padding: 9px 10px 5px;
color: var(--text-tertiary); color: rgb(135, 146, 160);
font-size: 10px; font-size: 10px;
} }
@@ -959,9 +957,9 @@
.global-search-result.is-active, .global-search-result.is-active,
.global-search-result:hover { .global-search-result:hover {
background: var(--action-soft); background: rgb(238, 244, 255);
color: var(--action); color: rgb(31, 85, 165);
} }
.global-search-result-icon { .global-search-result-icon {
@@ -975,7 +973,7 @@
color: var(--text-muted); color: var(--text-muted);
border-color: var(--border); border-color: rgb(224, 229, 235);
border-radius: 7px; border-radius: 7px;
@@ -1005,7 +1003,7 @@
border-color: var(--dialog-line); border-color: var(--dialog-line);
background: var(--surface-subtle); background: rgb(251, 252, 253);
} }
.alerts-dialog .alert-form { .alerts-dialog .alert-form {
@@ -1079,7 +1077,7 @@
padding: 16px 18px; padding: 16px 18px;
background: var(--surface-subtle); background: rgb(247, 249, 251);
} }
.assistant-dialog .assistant-message { .assistant-dialog .assistant-message {
@@ -1089,9 +1087,9 @@
} }
.assistant-dialog .assistant-message-content { .assistant-dialog .assistant-message-content {
border-color: var(--border); border-color: rgb(220, 227, 235);
border-radius: 8px; border-radius: 7px;
font-size: 12.5px; font-size: 12.5px;
@@ -1199,7 +1197,7 @@
z-index: 95; z-index: 95;
box-shadow: var(--shadow-float); box-shadow: rgba(0, 0, 0, 0.12) -8px 0px 24px;
transition: right 0.25s; transition: right 0.25s;
@@ -52,21 +52,21 @@
} }
.segmented { .segmented {
height: var(--control-height); height: 34px;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
min-height: var(--control-height); min-height: 36px;
padding: 2px; padding: 2px;
border: 0px; border: 0px;
border-radius: var(--radius-md); border-radius: 7px;
background: var(--surface-muted); background: rgb(236, 239, 244);
} }
.segment { .segment {
@@ -82,11 +82,11 @@
border: 0px; border: 0px;
border-radius: 6px; border-radius: 5px;
color: var(--text-secondary); color: var(--text-secondary);
font-size: var(--font-size-label); font-size: 11.5px;
} }
.segment.active { .segment.active {
+20 -55
View File
@@ -14,13 +14,13 @@
.data-table th.sort-asc::after { .data-table th.sort-asc::after {
content: " ↑"; content: " ↑";
color: var(--accent); color: var(--blue);
} }
.data-table th.sort-desc::after { .data-table th.sort-desc::after {
content: " ↓"; content: " ↓";
color: var(--accent); color: var(--blue);
} }
.data-table tbody tr td { .data-table tbody tr td {
@@ -48,7 +48,7 @@
color: var(--text-muted); color: var(--text-muted);
text-align: left; text-align: center;
} }
.table-action { .table-action {
@@ -114,9 +114,9 @@
text-align: left; text-align: left;
height: 40px; height: 42px;
padding: 0 12px; padding: 8px 11px;
border-right: 0px; border-right: 0px;
@@ -214,9 +214,7 @@
.data-table .stock-name { .data-table .stock-name {
color: var(--text-primary); color: var(--text-primary);
font-size: var(--fs-table); font-weight: 700;
font-weight: 600;
} }
.main-grid > .table-frame { .main-grid > .table-frame {
@@ -244,7 +242,7 @@
} }
.data-table thead th { .data-table thead th {
padding: 0 12px; padding: 0px 11px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
@@ -258,31 +256,23 @@
height: 36px; height: 36px;
font-size: var(--font-size-caption); font-size: var(--font-size-label);
} }
.data-table tbody td { .data-table tbody td {
padding: 0 12px; padding: 6px 11px;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--border-subtle);
color: var(--text-primary); color: var(--text-primary);
height: 40px;
}
.data-table thead tr:last-child th:first-child, height: 41px;
.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 { .main-grid .data-table thead th {
height: 36px; height: 34px;
padding: 0 12px; padding: 6px 10px;
} }
.main-grid .data-table tbody td { .main-grid .data-table tbody td {
@@ -310,11 +300,11 @@
font-weight: 600; font-weight: 600;
font-size: var(--font-size-caption); font-size: 12px;
text-align: left; text-align: left;
padding: 0 12px; padding: 8px 12px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
@@ -341,30 +331,18 @@
margin-left: 3px; margin-left: 3px;
} }
.tbl thead th.sorted, .tbl thead th.sorted .arr {
.data-table thead th.sorted, color: var(--blue);
.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 { .tbl tbody td {
padding: 0 12px; padding: 9px 12px;
border-bottom: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
height: 40px;
font-size: var(--font-size-table);
} }
.tbl tbody tr:hover { .tbl tbody tr:hover {
@@ -437,19 +415,6 @@
color: var(--text-secondary); 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) { :root[data-theme="dark"] :is(table tbody td, .data-table tbody td, .tbl tbody td) {
border-color: var(--line-soft); border-color: var(--line-soft);
+2 -28
View File
@@ -89,10 +89,7 @@ function renderDashboard() {
const { meta, overview, ladders, sectors } = state.dashboard; const { meta, overview, ladders, sectors } = state.dashboard;
animateMetric("tapeUp", overview.up_count, (value) => Math.round(value)); animateMetric("tapeUp", overview.up_count, (value) => Math.round(value));
animateMetric("tapeDown", overview.down_count, (value) => Math.round(value)); animateMetric("tapeDown", overview.down_count, (value) => Math.round(value));
animateMetric("tapeLimit", overview.limit_up_count, (value) => `${Math.round(value)}`); setText("tapeLimit", `${overview.limit_up_count} / 跌停 ${overview.limit_down_count}`);
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("tapeAmount", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
animateMetric("limitUpMetric", overview.limit_up_count, (value) => `${Math.round(value)}`); animateMetric("limitUpMetric", overview.limit_up_count, (value) => `${Math.round(value)}`);
animateMetric("limitDownMetric", overview.limit_down_count, (value) => `${Math.round(value)}`); animateMetric("limitDownMetric", overview.limit_down_count, (value) => `${Math.round(value)}`);
@@ -101,30 +98,7 @@ function renderDashboard() {
animateMetric("amountMetric", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`); animateMetric("amountMetric", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
setText("dataDateMetric", dashboardDataTimestamp(meta)); setText("dataDateMetric", dashboardDataTimestamp(meta));
animateMetric("sentimentScore", overview.sentiment_score, (value) => Math.round(value)); animateMetric("sentimentScore", overview.sentiment_score, (value) => Math.round(value));
animateMetric("detailSentimentScore", overview.sentiment_score, (value) => Math.round(value)); setText("sentimentText", sentimentLabel(overview.sentiment_score));
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); updateSentimentGauge(overview.sentiment_score);
setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`); setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`);
+79 -612
View File
@@ -187,6 +187,12 @@ body.sidebar-collapsed .nav-group + .nav-group {
.metric { .metric {
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
.metric-wide {
display: flex;
grid-column: span 2;
}
} }
@media (max-width: 860px) { @media (max-width: 860px) {
@@ -222,6 +228,10 @@ body.sidebar-collapsed .nav-group + .nav-group {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
.metric-wide {
grid-column: auto;
}
.metric { .metric {
padding: 10px; padding: 10px;
} }
@@ -252,6 +262,10 @@ body.sidebar-collapsed .nav-group + .nav-group {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.metric-wide {
grid-column: 1 / -1;
}
.section-title-group { .section-title-group {
align-items: flex-start; align-items: flex-start;
@@ -297,6 +311,10 @@ body.sidebar-collapsed .nav-group + .nav-group {
.overview-strip { .overview-strip {
grid-template-columns: minmax(210px, 1.3fr) repeat(5, minmax(90px, 0.7fr)); grid-template-columns: minmax(210px, 1.3fr) repeat(5, minmax(90px, 0.7fr));
} }
.overview-strip .metric-wide {
display: none;
}
} }
@media (max-width: 767px) { @media (max-width: 767px) {
@@ -626,6 +644,10 @@ body.sidebar-collapsed .app-main {
padding: 0px 8px; padding: 0px 8px;
} }
.overview-strip .metric-wide {
display: none;
}
.overview-strip .metric:nth-of-type(n+4) { .overview-strip .metric:nth-of-type(n+4) {
display: none; display: none;
} }
@@ -640,6 +662,14 @@ body.sidebar-collapsed .app-main {
padding: 8px; 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) { .overview-strip[data-overview-expanded="true"] .metric:nth-of-type(n) {
min-height: 54px; min-height: 54px;
@@ -740,6 +770,10 @@ body.sidebar-collapsed .app-main {
display: flex; display: flex;
} }
.overview-strip .metric-wide {
display: none;
}
.overview-strip .metric:nth-of-type(n+5) { .overview-strip .metric:nth-of-type(n+5) {
display: none; display: none;
} }
@@ -858,7 +892,7 @@ body.sidebar-collapsed .app-main {
padding: 0px 10px; padding: 0px 10px;
border-radius: var(--radius-md); border-radius: 7px;
font-size: 12.5px; font-size: 12.5px;
@@ -990,8 +1024,8 @@ body.sidebar-collapsed .app-main {
grid-template-columns: 176px minmax(0px, 1fr) auto; grid-template-columns: 176px minmax(0px, 1fr) auto;
} }
.app-header .market-tape { .market-tape {
display: flex; display: none;
} }
.module-nav { .module-nav {
@@ -1390,19 +1424,19 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
padding: 12px 10px 4px; padding: 12px 10px 4px;
color: var(--text-3); color: var(--r2-faint);
font-size: var(--fs-aux); font-size: 11px;
font-weight: 600; font-weight: 400;
letter-spacing: .04em; letter-spacing: 0px;
} }
.module-nav .module-tab { .module-nav .module-tab {
width: 100%; width: 100%;
min-height: 36px; min-height: 34px;
display: flex; display: flex;
@@ -1418,13 +1452,13 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
border: 0px; border: 0px;
border-radius: var(--radius-md); border-radius: 7px;
background: transparent; background: transparent;
color: var(--text-2); color: var(--text-secondary);
font-size: var(--fs-table); font-size: 13px;
font-weight: 400; font-weight: 400;
} }
@@ -1442,23 +1476,23 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
} }
.module-nav .module-tab:hover { .module-nav .module-tab:hover {
background: var(--hover); background: var(--surface-hover);
color: var(--text-1); color: var(--r2-ink);
} }
.module-nav .module-tab.active { .module-nav .module-tab.active {
background: var(--selected); background: var(--r2-blue-soft);
color: var(--accent); color: var(--r2-blue);
font-weight: 600; font-weight: 600;
} }
.module-nav .module-tab.mobile-active { .module-nav .module-tab.mobile-active {
background: var(--selected); background: var(--r2-blue-soft);
color: var(--accent); color: var(--r2-blue);
font-weight: 600; font-weight: 600;
} }
@@ -1722,11 +1756,7 @@ body.sidebar-collapsed .module-nav .module-tab {
padding: 0px 16px; padding: 0px 16px;
overflow-x: auto; overflow: hidden;
overflow-y: hidden;
scrollbar-width: none;
border-top: 0px; border-top: 0px;
@@ -1745,10 +1775,6 @@ body.sidebar-collapsed .module-nav .module-tab {
box-shadow: none; box-shadow: none;
} }
.overview-strip::-webkit-scrollbar {
display: none;
}
.overview-strip .row { .overview-strip .row {
display: flex; display: flex;
@@ -1758,8 +1784,6 @@ body.sidebar-collapsed .module-nav .module-tab {
padding: 7px var(--page-pad-x); padding: 7px var(--page-pad-x);
flex-shrink: 0;
font-size: 12px; font-size: 12px;
color: var(--sub); color: var(--sub);
@@ -1974,7 +1998,7 @@ body.sidebar-collapsed .module-nav .module-tab {
width: auto; width: auto;
min-height: var(--statusbar-height); min-height: 30px;
height: var(--statusbar-height); height: var(--statusbar-height);
@@ -1988,11 +2012,11 @@ body.sidebar-collapsed .module-nav .module-tab {
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
background: var(--header-bg); background: var(--surface);
color: var(--text-3); color: var(--faint);
font-size: var(--fs-aux); font-size: 11.5px;
} }
body.sidebar-collapsed .status-bar { body.sidebar-collapsed .status-bar {
@@ -2134,7 +2158,7 @@ body.sidebar-collapsed .status-bar {
width: var(--sidebar-width); width: var(--sidebar-width);
background: var(--header-bg); background: var(--surface);
border-right: 1px solid var(--line); border-right: 1px solid var(--line);
@@ -2242,15 +2266,7 @@ body.sidebar-collapsed .status-bar {
.main { .main {
margin-left: var(--sidebar-width); margin-left: var(--sidebar-width);
min-width: 0; min-width: 1080px;
overflow-x: hidden;
}
@media (min-width: 1280px) {
.main {
min-width: 1080px;
}
} }
.app-header { .app-header {
@@ -2260,7 +2276,7 @@ body.sidebar-collapsed .status-bar {
width: auto; width: auto;
min-height: var(--topbar-height); min-height: 46px;
box-shadow: none; box-shadow: none;
@@ -2272,17 +2288,17 @@ body.sidebar-collapsed .status-bar {
z-index: 50; z-index: 50;
background: var(--header-bg); background: var(--surface);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--line);
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--header-cluster-gap); gap: 14px;
padding: 0 var(--header-pad-x); padding: 0 var(--page-pad-x);
height: var(--topbar-height); height: var(--topbar-height);
} }
@@ -2396,7 +2412,7 @@ body.sidebar-collapsed .status-bar {
overflow: hidden; overflow: hidden;
} }
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .app-main .overview-strip { body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .overview-strip {
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -2412,7 +2428,7 @@ body.sidebar-collapsed .status-bar {
overflow: hidden; overflow: hidden;
} }
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 { body:is([data-active-view="auctionView"], [data-active-view="themeLibraryView"], [data-active-view="popularityView"], [data-active-view="mentorView"], [data-active-view="rotationView"]) .overview-strip {
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -2462,8 +2478,8 @@ body.sidebar-collapsed .status-bar {
padding: var(--mobile-shell-pad); padding: var(--mobile-shell-pad);
} }
.app-header .market-tape { .market-tape {
display: flex; display: none;
} }
.header-actions { .header-actions {
@@ -2685,6 +2701,10 @@ body.sidebar-collapsed .status-bar {
padding: 0px; padding: 0px;
} }
.overview-strip .metric-wide {
display: none;
}
.overview-strip .metric:nth-of-type(n+4) { .overview-strip .metric:nth-of-type(n+4) {
display: none; display: none;
} }
@@ -2710,20 +2730,10 @@ body.sidebar-collapsed .status-bar {
color: var(--text-primary); color: var(--text-primary);
} }
:root[data-theme="dark"] :is(.module-nav, .app-header, .status-bar) { :root[data-theme="dark"] :is(.module-nav, .app-header, .overview-strip, .status-bar) {
border-color: var(--border); border-color: var(--border);
background: var(--header-bg); background: var(--surface);
color: var(--text-primary);
box-shadow: none;
}
:root[data-theme="dark"] .overview-strip {
border-color: var(--border);
background: transparent;
color: var(--text-primary); color: var(--text-primary);
@@ -2731,7 +2741,7 @@ body.sidebar-collapsed .status-bar {
} }
:root[data-theme="dark"] .app-header { :root[data-theme="dark"] .app-header {
background: var(--header-bg); background: color-mix(in srgb, var(--surface) 96%, transparent);
} }
:root[data-theme="dark"] :is(.sidebar-brand, .module-nav .sidebar-brand, .nav-group, .sidebar-collapse-button, .header-date-group) { :root[data-theme="dark"] :is(.sidebar-brand, .module-nav .sidebar-brand, .nav-group, .sidebar-collapse-button, .header-date-group) {
@@ -3009,16 +3019,10 @@ body.sidebar-collapsed .status-bar {
body.mobile-shell .header-command-group > .command-button, body.mobile-shell .header-command-group > .command-button,
body.mobile-shell .header-command-group .account-button { body.mobile-shell .header-command-group .account-button {
width: 100%; width: 100%;
max-width: none;
min-height: var(--mobile-touch-size); min-height: var(--mobile-touch-size);
justify-content: flex-start; 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 { body.mobile-shell .header-command-group .account-dropdown {
position: static; position: static;
grid-column: 1 / -1; grid-column: 1 / -1;
@@ -3184,6 +3188,11 @@ body.sidebar-collapsed .status-bar {
display: flex; 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,
body.mobile-shell .workspace-view.page:not(#heavenView), body.mobile-shell .workspace-view.page:not(#heavenView),
body.mobile-shell:is([data-active-view]) .workspace-view.active-view { body.mobile-shell:is([data-active-view]) .workspace-view.active-view {
@@ -3213,545 +3222,3 @@ body.sidebar-collapsed .status-bar {
display: none; 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;
}
+15 -56
View File
@@ -22,19 +22,12 @@
const button = document.querySelector("#headerMenuButton"); const button = document.querySelector("#headerMenuButton");
const backdrop = document.querySelector("#mobileCommandBackdrop"); const backdrop = document.querySelector("#mobileCommandBackdrop");
if (!menu || !button) return; 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"); const open = typeof force === "boolean" ? force : !menu.classList.contains("is-open");
menu.classList.toggle("is-open", open); menu.classList.toggle("is-open", open);
button.setAttribute("aria-expanded", String(open)); button.setAttribute("aria-expanded", String(open));
document.body.classList.toggle("mobile-command-open", open); document.body.classList.toggle("mobile-command-open", open && isMobileViewport());
if (backdrop) backdrop.hidden = !open; if (backdrop) backdrop.hidden = !(open && isMobileViewport());
if (open) { if (open && isMobileViewport()) {
global.requestAnimationFrame(() => menu.querySelector("button:not([hidden])")?.focus({ preventScroll: true })); global.requestAnimationFrame(() => menu.querySelector("button:not([hidden])")?.focus({ preventScroll: true }));
} else if (force === false && document.activeElement && menu.contains(document.activeElement)) { } else if (force === false && document.activeElement && menu.contains(document.activeElement)) {
button.focus({ preventScroll: true }); button.focus({ preventScroll: true });
@@ -63,28 +56,6 @@
updateSidebarControl(); 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) { function syncNavigation(viewId) {
const page = registry.get(viewId); const page = registry.get(viewId);
const navigationId = page?.navigation_alias || viewId; const navigationId = page?.navigation_alias || viewId;
@@ -98,9 +69,6 @@
: page?.group === "personal" ? "复盘" : "工具"; : page?.group === "personal" ? "复盘" : "工具";
} }
if (mobileTitle) mobileTitle.textContent = page?.title || "小白复盘"; 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) => { document.querySelectorAll(".module-tab").forEach((button) => {
button.classList.toggle("active", button.dataset.view === navigationId); button.classList.toggle("active", button.dataset.view === navigationId);
button.classList.toggle( button.classList.toggle(
@@ -216,35 +184,26 @@
toggleHeaderCommandMenu(false); 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) => { document.querySelector("#overviewToggle")?.addEventListener("click", (event) => {
event.stopPropagation();
const overview = document.querySelector(".overview-strip"); const overview = document.querySelector(".overview-strip");
if (!overview) return; if (!overview) return;
const expanded = overview.dataset.overviewExpanded !== "true"; const expanded = overview.dataset.overviewExpanded !== "true";
setOverviewExpanded(expanded); overview.dataset.overviewExpanded = String(expanded);
if (expanded) toggleHeaderCommandMenu(false); 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?.();
}); });
document.addEventListener("click", (event) => { document.addEventListener("click", (event) => {
if (!event.target.closest("#headerCommandGroup, #headerMenuButton")) toggleHeaderCommandMenu(false); if (!event.target.closest(".header-actions")) toggleHeaderCommandMenu(false);
if (!event.target.closest(".overview-strip")) setOverviewExpanded(false);
}); });
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return; if (event.key === "Escape") toggleHeaderCommandMenu(false);
toggleHeaderCommandMenu(false);
setOverviewExpanded(false);
}); });
global.addEventListener("resize", () => { global.addEventListener("resize", () => {
toggleHeaderCommandMenu(false); toggleHeaderCommandMenu(false);
-2
View File
@@ -13,8 +13,6 @@ function syncThemeControl() {
button.setAttribute("aria-label", label); button.setAttribute("aria-label", label);
button.setAttribute("aria-pressed", String(dark)); button.setAttribute("aria-pressed", String(dark));
button.querySelector("i")?.setAttribute("data-lucide", dark ? "sun" : "moon"); button.querySelector("i")?.setAttribute("data-lucide", dark ? "sun" : "moon");
const modeText = document.querySelector("#themeModeText");
if (modeText) modeText.textContent = dark ? "夜间模式" : "日间模式";
} }
function clearThemeTransitionEffects() { function clearThemeTransitionEffects() {
+68 -139
View File
@@ -22,20 +22,19 @@
--color-gray-900: #1f2937; --color-gray-900: #1f2937;
--color-shell-canvas: #f4f5f7; --color-shell-canvas: #f4f5f7;
--color-page-canvas: #f4f5f7; --color-page-canvas: #f4f5f7;
--color-surface-muted: #f2f3f5; --color-surface-muted: #f3f4f6;
--color-surface-subtle: #f8f9fb; --color-surface-subtle: #f8fafc;
--color-border: #eceded; --color-border: #e5e7eb;
--color-border-strong: #dee0e3; --color-border-strong: #d1d5db;
--color-text-primary: #1f2329; --color-text-primary: #1f2937;
--color-text-secondary: #646a73; --color-text-secondary: #6b7280;
--color-text-tertiary: #8f959e; --color-text-tertiary: #9ca3af;
--color-action-base: #3370ff; --color-action-base: #2563eb;
--color-action-base-hover: #2b5fd9; --color-action-base-hover: #1d4ed8;
--color-action-base-soft: #eaf1fe; --color-action-base-soft: #eff4ff;
--color-action: #3370ff; --color-action: #2563eb;
--color-action-hover: #2b5fd9; --color-action-hover: #1d4ed8;
--color-action-soft: #eaf1fe; --color-action-soft: #eff4ff;
--color-action-press: #2456b8;
--color-action-line: #c7d8fb; --color-action-line: #c7d8fb;
--color-market-up-base: #e04536; --color-market-up-base: #e04536;
--color-market-up-base-soft: #fdecea; --color-market-up-base-soft: #fdecea;
@@ -50,25 +49,24 @@
--color-warning: #b45309; --color-warning: #b45309;
--color-warning-soft: #fdf3e3; --color-warning-soft: #fdf3e3;
--size-radius-sm: 4px; --size-radius-sm: 5px;
--size-radius-md: 8px; --size-radius-md: 7px;
--size-radius-lg: 10px; --size-radius-lg: 10px;
--size-radius-dialog: 12px;
--size-control: 32px; --size-control: 32px;
--size-sidebar: 200px; --size-sidebar: 200px;
--size-topbar: 64px; --size-topbar: 46px;
--size-summary: 0px; --size-summary: 36px;
--size-statusbar: 28px; --size-statusbar: 30px;
--size-page-pad-y: 14px; --size-page-pad-y: 14px;
--size-page-pad-x: 16px; --size-page-pad-x: 16px;
--size-card-gap: 12px; --size-card-gap: 12px;
--elevation-card: 0 1px 2px rgba(31, 35, 41, .04); --elevation-card: 0 1px 2px rgba(16, 24, 40, .05);
--elevation-soft: 0 1px 2px rgba(31, 35, 41, .04); --elevation-soft: 0 1px 2px rgba(16, 24, 40, .05);
--elevation-raised: 0 2px 6px rgba(31, 35, 41, .04), 0 8px 24px rgba(31, 35, 41, .06); --elevation-raised: 0 4px 14px rgba(16, 24, 40, .06);
--elevation-float: 0 12px 32px rgba(0, 0, 0, .14); --elevation-float: 0 12px 32px rgba(0, 0, 0, .18);
--motion-instant: 100ms; --motion-instant: 100ms;
--motion-fast: 120ms; --motion-fast: 140ms;
--motion-medium: 200ms; --motion-medium: 200ms;
--motion-deliberate: 260ms; --motion-deliberate: 260ms;
--motion-slow: 560ms; --motion-slow: 560ms;
@@ -82,11 +80,8 @@
--surface-canvas: var(--color-page-canvas); --surface-canvas: var(--color-page-canvas);
--surface-raised: var(--color-white); --surface-raised: var(--color-white);
--surface-sunken: #eef0f3; --surface-sunken: #eef0f3;
--header-bg: var(--color-white); --surface-hover: #f8faff;
--surface-hover: #f2f3f5; --surface-selected: #eff4ff;
--surface-selected: #eaf1fe;
--hover: var(--surface-hover);
--selected: var(--surface-selected);
--surface-overlay: var(--color-white); --surface-overlay: var(--color-white);
--border: var(--color-border); --border: var(--color-border);
--border-strong: var(--color-border-strong); --border-strong: var(--color-border-strong);
@@ -98,12 +93,7 @@
--on-action: #ffffff; --on-action: #ffffff;
--action: var(--color-action-base); --action: var(--color-action-base);
--action-hover: var(--color-action-base-hover); --action-hover: var(--color-action-base-hover);
--action-press: var(--color-action-press);
--action-soft: var(--color-action-base-soft); --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: var(--color-market-up-base);
--market-up-soft: var(--color-market-up-base-soft); --market-up-soft: var(--color-market-up-base-soft);
--market-down: var(--color-market-down-base); --market-down: var(--color-market-down-base);
@@ -116,12 +106,7 @@
--table-header: var(--surface-subtle); --table-header: var(--surface-subtle);
--table-hover: #f8faff; --table-hover: #f8faff;
--table-selected: var(--surface-selected); --table-selected: var(--surface-selected);
--focus-ring: rgba(51, 112, 255, .15); --focus-ring: rgba(37, 99, 235, .16);
--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); --backdrop: rgba(17, 24, 39, .48);
--primary: var(--color-action); --primary: var(--color-action);
--primary-hover: var(--color-action-hover); --primary-hover: var(--color-action-hover);
@@ -141,32 +126,18 @@
--radius-lg: var(--size-radius-lg); --radius-lg: var(--size-radius-lg);
--shadow-xs: var(--elevation-card); --shadow-xs: var(--elevation-card);
--shadow-sm: var(--elevation-raised); --shadow-sm: var(--elevation-raised);
--shadow-card: var(--elevation-card);
--shadow-raised: var(--elevation-raised);
--shadow-float: var(--elevation-float); --shadow-float: var(--elevation-float);
--duration-fast: 150ms; --duration-fast: 150ms;
--duration-normal: 220ms; --duration-normal: 220ms;
--font-size-aux: 11.5px; --font-size-aux: 10.5px;
--font-size-caption: 12.5px; --font-size-caption: 11px;
--font-size-label: 13px; --font-size-label: 12px;
--font-size-table: 13.5px; --font-size-table: 12.5px;
--font-size-body: 14px; --font-size-body: 13px;
--font-size-chat: 14px; --font-size-card-title: 14px;
--font-size-card-title: 15px; --font-size-page-title: 17px;
--font-size-page-title: 18px; --font-size-metric: 22px;
--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-regular: 400;
--font-weight-medium: 500; --font-weight-medium: 500;
--font-weight-semibold: 600; --font-weight-semibold: 600;
@@ -176,20 +147,11 @@
--space-8: 8px; --space-8: 8px;
--space-12: 12px; --space-12: 12px;
--space-16: 16px; --space-16: 16px;
--space-20: 20px;
--space-24: 24px; --space-24: 24px;
--sidebar-width: var(--size-sidebar); --sidebar-width: var(--size-sidebar);
--sidebar-compact-width: 64px; --sidebar-compact-width: 64px;
--topbar-height: var(--size-topbar); --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); --summary-height: var(--size-summary);
--statusbar-height: var(--size-statusbar); --statusbar-height: var(--size-statusbar);
--page-pad-y: var(--size-page-pad-y); --page-pad-y: var(--size-page-pad-y);
@@ -257,14 +219,13 @@
--dragon-profile-weight-strong: 750; --dragon-profile-weight-strong: 750;
--dragon-profile-weight-semibold: 600; --dragon-profile-weight-semibold: 600;
--mentor-directory-width: 300px; --mentor-directory-width: 280px;
--mentor-pane-header-height: 56px; --mentor-profile-width: 272px;
--mentor-avatar-size: 40px; --mentor-pane-header-height: 58px;
--mentor-message-avatar-size: 36px; --mentor-avatar-size: 46px;
--mentor-chat-avatar-size: 38px; --mentor-profile-avatar-size: 80px;
--mentor-profile-avatar-size: 68px; --mentor-composer-min-height: 94px;
--mentor-composer-min-height: 85px; --mentor-message-max-width: 82%;
--mentor-message-max-width: 60%;
--chart-background: #fbfcfd; --chart-background: #fbfcfd;
--chart-grid: #e2e8ec; --chart-grid: #e2e8ec;
@@ -352,64 +313,45 @@
--r2-radius: var(--size-radius-lg); --r2-radius: var(--size-radius-lg);
--r2-shadow: var(--elevation-card); --r2-shadow: var(--elevation-card);
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif;
font-size: 14px; font-size: 14px;
} }
:root[data-theme="dark"] { :root[data-theme="dark"] {
color-scheme: dark; color-scheme: dark;
--canvas: #141519; --canvas: #121416;
--header-bg: #1c1e23; --surface: #1b1e21;
--surface: #232529; --surface-muted: #202428;
--surface-muted: #2a2d33; --surface-subtle: #24282d;
--surface-subtle: #202329;
--surface-canvas: var(--canvas); --surface-canvas: var(--canvas);
--surface-raised: var(--surface); --surface-raised: var(--surface);
--surface-sunken: #191b1f; --surface-sunken: #16191c;
--surface-hover: #2a2d33; --surface-hover: #24282d;
--surface-selected: #2b3b58; --surface-selected: #23364a;
--surface-overlay: #232529; --surface-overlay: #24282d;
--hover: var(--surface-hover); --border: #343a40;
--selected: var(--surface-selected); --border-strong: #474f57;
--border: #2b2e34; --border-subtle: #2a2f34;
--border-strong: #3a3e47;
--border-subtle: #2b2e34;
--text-primary: #e8eaed; --text-primary: #e8eaed;
--text-secondary: #a9adb3; --text-secondary: #adb5bd;
--text-tertiary: #7c828a; --text-tertiary: #7f8993;
--text-1: var(--text-primary);
--text-2: var(--text-secondary);
--text-3: var(--text-tertiary);
--text-inverse: #ffffff; --text-inverse: #ffffff;
--action: #5b8def; --action: #6ca8e8;
--action-hover: #7ba5f5; --action-hover: #8bbcf0;
--action-press: #3465c4; --action-soft: #23364a;
--action-soft: #2b3b58; --market-up: #f06d73;
--accent: var(--action); --market-up-soft: #40262a;
--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: #43bc8a;
--market-down-soft: #22362c; --market-down-soft: #1d382f;
--warning-color: #e2ad58; --warning-color: #e2ad58;
--warning-soft: #3d3220; --warning-soft: #3d3220;
--warn: var(--warning-color); --control-surface: #202428;
--warn-soft: var(--warning-soft); --control-hover: #2a2f34;
--control-surface: #232529; --control-border: #474f57;
--control-hover: #2a2d33; --table-header: #202428;
--control-border: #3a3e47; --table-hover: #242f39;
--table-header: #202329; --table-selected: #23364a;
--table-hover: #2a2d33; --focus-ring: rgba(108, 168, 232, .22);
--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); --backdrop: rgba(0, 0, 0, .66);
--primary: var(--action); --primary: var(--action);
--primary-hover: var(--action-hover); --primary-hover: var(--action-hover);
@@ -525,16 +467,3 @@
--shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .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); --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;
}
}
File diff suppressed because it is too large Load Diff
-346
View File
@@ -1,346 +0,0 @@
const { test, expect } = require("@playwright/test");
// 手机端(/m/)全页面回归:P5 收官打磨。
// 覆盖:登录、四个入口图标页、行情 12 页、工具 3 页、复盘 5 页、复盘助手,
// 以及日夜两套渲染、空态、错误态、横屏健壮性、深底深字对比度抽查。
const EMPTY_DASHBOARD = {
meta: {
trade_date: "2026-07-22",
requested_date: "2026-07-22",
source: "tushare",
realtime: false,
cached: true,
market_status: "closed",
previous_trade_date: "2026-07-21",
},
overview: {
up_count: 2100,
down_count: 2800,
limit_up_count: 42,
limit_down_count: 8,
broken_count: 17,
seal_rate: 71.2,
amount_billion: 12600,
sentiment_score: 48,
},
limits: [],
broken: [],
down_limits: [],
yesterday_limits: [],
limit_performance: [],
ladders: [],
sectors: [],
sector_rotation: [],
};
const DASHBOARD = {
...EMPTY_DASHBOARD,
limits: [
{ code: "002141", name: "贤丰控股", streak: 4, change: 9.98, 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: "板块龙头连板打开空间" },
],
broken: [
{ code: "002156", name: "通富微电", change: 9.77, price: 76.64, sector: "半导体", first_time: "10:08:03", open_times: 4, turnover_rate: 17.36, amount_billion: 198.18, reason: "芯片方向冲高回落" },
],
down_limits: [
{ code: "000037", name: "深南电A", change: -10.03, price: 8.97, sector: "电力", turnover_rate: 11.78, amount_billion: 3.68, streak: 2, reason: "连续弱势跌停" },
],
yesterday_limits: [
{ code: "000011", name: "深物业A", prior_streak: 1, current_change: 9.99, outcome: "晋级", current_streak: 2, sector: "房地产开发", reason: "地产政策预期" },
],
limit_performance: [
{ level: 3, label: "昨日3板", count: 4, advanced: 2, advance_rate: 50, positive_rate: 75, average_change: 3.4 },
{ level: 2, label: "昨日2板", count: 9, advanced: 2, advance_rate: 22.2, positive_rate: 44.4, average_change: 0.8 },
],
ladders: [
{ level: 4, label: "4板", count: 1, stocks: [{ code: "002141", name: "贤丰控股", sector: "电子元件", first_time: "09:31", open_times: 0, seal_amount_million: 8200 }] },
{ level: 3, label: "3板", count: 2, stocks: [{ code: "000011", name: "深物业A", sector: "房地产开发", first_time: "09:40", open_times: 1, amount_billion: 3.2 }] },
],
};
function authSession(role = "admin", subscribed = true) {
return {
authenticated: true,
csrf_token: "mobile-test-csrf",
user: {
id: role === "admin" ? 1 : 2,
username: role === "admin" ? "admin_user" : "normal_user",
role,
membership: { active: role === "admin" || subscribed, subscribed, is_admin: role === "admin" },
},
};
}
async function mockMobileApi(page, options = {}) {
const auth = options.auth || authSession();
const dashboard = options.emptyDashboard ? EMPTY_DASHBOARD : DASHBOARD;
await page.route("**/api/**", async (route) => {
const url = new URL(route.request().url());
const path = url.pathname;
let payload = { ok: true };
if (options.failDashboard && path === "/api/dashboard") {
await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "行情服务暂时不可用" }) });
return;
}
if (path === "/api/auth/me") payload = auth;
else if (path === "/api/dashboard") payload = dashboard;
else if (path === "/api/popularity") {
const hot = { rank: 1, code: "002141", ts_code: "002141.SZ", name: "贤丰控股", change: 2.4, price: 12.48, ths_rank: 1, dc_rank: 2, rank_change: 3, concepts: ["电子元件"], dual_source: true };
payload = { meta: { trade_date: "2026-07-22" }, combined: [hot], ths: [{ ...hot }], dc: [{ ...hot, rank: 2 }] };
} else if (path === "/api/sentiment/history") {
payload = { rows: [{ trade_date: "2026-07-22", score: 48, phase: "修复", direction: "升温", label: "情绪修复", day_change: 4, seal_rate: 71, limit_up_count: 42, broken_count: 17, components: {} }] };
} else if (path === "/api/rotation/history") {
payload = { rows: [{ trade_date: "2026-07-22", sectors: [{ name: "人工智能", rank: 1, count: 8, strength: 88 }] }] };
} else if (path === "/api/rotation/members") {
payload = { meta: { trade_date: "20260722", sector_name: "人工智能", member_count: 1, quoted_count: 1 }, rows: [{ code: "002141", name: "贤丰控股", change: 4.8, open: 18.21, close: 19.06, amount_billion: 19.6 }] };
} else if (path === "/api/auction") {
payload = {
meta: { trade_date: "2026-07-22", phase: "finalized", available: true },
summary: { stock_count: 3, focus_count: 1, one_price_count: 1, amount_billion: 2.5 },
amount_history: [{ trade_date: "2026-07-22", amount_billion: 2.5, stock_count: 2 }],
themes: { carry: [{ name: "电子元件", status: "强承接", prior_limit_count: 2, leader: "贤丰控股", matched_count: 1, median_change: 4.2, amount_million: 15 }] },
focus_rows: [{ code: "002141", name: "贤丰控股", sector: "电子元件", change: 4.2, price: 12.48, amount_million: 15, volume_ratio: 1.8, expectation: "超预期", attention_score: 88.5 }],
rows: [],
one_price_rows: [],
watchlist_rows: [],
};
} else if (path === "/api/themes") {
payload = { meta: { trade_date: "2026-07-22" }, summary: { theme_count: 1, up_count: 1, down_count: 0, hot_count: 1 }, items: [{ code: "885728.TI", name: "人工智能", member_count: 8, change: 2.2, turnover_rate: 3.1, hot_rank: 1, has_quote: true }] };
} else if (path === "/api/themes/detail") {
payload = { meta: { trade_date: "2026-07-22" }, theme: { code: "885728.TI", name: "人工智能", member_count: 8, change: 2.2, turnover_rate: 3.1 }, summary: { member_count: 8, quoted_count: 8, up_count: 6, down_count: 2 }, members: [{ code: "002141", name: "贤丰控股", change: 2.4, price: 12.48, amount_billion: 3.2 }] };
} else if (path === "/api/dragon-tiger") {
payload = { meta: { trade_date: "2026-07-22", status: "ok" }, summary: { trader_count: 1, operation_count: 2, active_stock_count: 1 }, traders: [{ id: "t1", name: "赵老哥", identity_type: "trader", recognized: true, stock_count: 1, operation_count: 2, net_buy_million: 150, operations: [] }], unclassified_seats: [] };
} else if (path === "/api/dragon-tiger/profiles") {
payload = { meta: { status: "success" }, summary: { profile_count: 1, described_count: 1, organization_count: 1 }, profiles: [{ id: "p1", name: "赵老哥", description: "聚焦核心。", organizations: ["华泰证券浙江分公司"], organization_count: 1 }] };
} else if (path === "/api/screener/setup") {
payload = {
trade_date: "20260722",
regime: { id: "repair", label: "修复", confidence: 70, reason: "测试" },
regimes: [{ id: "repair", label: "修复" }],
factor_data: { ready: true, date_count: 45 },
strategies: [{ id: 1, name: "修复确认", description: "保留原有流程", regimes: ["repair"], builtin: true, data_ready: true, missing_data: [], formula: { meta: { library: "smart" }, universe: { exclude_st: true }, filters: [], score: [], limit: 15 } }],
};
} else if (path === "/api/screener/tracking") {
payload = { batches: [], summary: { total: 0, observed: 0, t1_win_rate: null, t5_win_rate: null, average_t5: null } };
} else if (path === "/api/watchlist") {
payload = { items: [{ code: "000002", name: "万科A", sector: "房地产开发", color: "red", change: 1.86, return_5d: 8.92, attention_score: 72.4, remark: "" }] };
} else if (path === "/api/trades") {
payload = { items: [{ id: 7, trade_date: "20260722", code: "002141", name: "贤丰控股", action: "buy", action_label: "买入", price: 10.2, quantity: 1000, position_pct: 20, pnl_amount: null, pnl_pct: null, emotion: "calm", emotion_label: "平静", tags: [], thesis: "", execution: "" }], summary: { total: 1, realized: 0, win_rate: null, pnl_amount: null, average_position: 20 } };
} else if (path === "/api/notes") {
payload = { items: [{ id: 12, code: "002141", stock_name: "贤丰控股", trade_date: "20260722", summary: "缩量修复", content: "等待确认。", plan: "" }] };
} else if (path === "/api/alerts") {
payload = { items: [{ id: 11, kind: "manual", available_date: "20260722", title: "复盘开盘强度", content: "", code: "002141", is_read: false, due: true }], unread_count: 1 };
} else if (path === "/api/assistant/messages") {
payload = { items: [] };
} else if (path === "/api/mentors/setup") {
payload = { trade_date: "20260722", mentors: [{ id: "source-a", name: "原帖老师", tagline: "先看周期。", focus: ["情绪周期"], evidence: { grade: "A" }, private: false }] };
} else if (path === "/api/mentors/messages") {
payload = { items: [] };
} else if (path === "/api/search") {
payload = { groups: { stocks: [{ id: "002141", code: "002141", name: "贤丰控股", type: "stock", industry: "电子元件" }], sectors: [], themes: [], indices: [] } };
} else if (/^\/api\/stock\/\d+\/preview$/.test(path)) {
payload = {
meta: { trade_date: "2026-07-22", intraday_status: "available" },
stock: { code: "002141", name: "贤丰控股", industry: "电子元件", price: 12.48, change: 2.4 },
prices: [{ trade_date: "2026-07-21", open: 10, high: 10.5, low: 9.9, close: 10.2, volume: 1000 }, { trade_date: "2026-07-22", open: 10.3, high: 10.9, low: 10.2, close: 12.48, volume: 1200 }],
intraday: [{ date: "2026-07-22", time: "09:30", open: 10.2, high: 10.24, low: 10.18, close: 10.22, volume: 100, average: 10.22 }],
};
}
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(payload) });
});
}
async function openMobile(page) {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/m/");
await expect(page.locator("#m-boot-splash")).toBeHidden();
await expect(page.locator("#m-tabbar")).toBeVisible();
}
async function setMobileTheme(page, theme) {
const wanted = theme === "night" ? "dark" : "light";
await page.evaluate((value) => window.MobileTheme.applyTheme(value), wanted);
await expect(page.locator("#m-app")).toHaveAttribute("data-theme", wanted);
}
async function navigateToFeature(page, key) {
await page.evaluate((k) => { window.MobileRouter.navigate("#/feature/" + k); }, key);
await expect(page.locator(`#m-view .m-page[data-page="${key}"]`)).toBeVisible();
}
function measureOverflow(page) {
return page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
}
// 深底深字 / 隐形文字抽查:文本元素的前景色若与其实际背景色完全一致即为不可见文字。
function findInvisibleText(page) {
return page.evaluate(() => {
const issues = [];
const seen = new Set();
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const text = node.textContent.trim();
if (!text || text.length < 1) continue;
const el = node.parentElement;
if (!el || seen.has(el)) continue;
seen.add(el);
const style = getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
let bg = "transparent";
let cur = el;
while (cur && (bg === "transparent" || bg === "rgba(0, 0, 0, 0)")) {
bg = getComputedStyle(cur).backgroundColor;
cur = cur.parentElement;
}
if (style.color === bg) {
issues.push({ tag: el.tagName, cls: String(el.className).slice(0, 40), text: text.slice(0, 24), color: style.color });
}
}
return issues;
});
}
const MARKET_PAGES = [
["market/sentiment", "情绪周期"],
["market/limit-up", "涨停池"],
["market/broken", "炸板池"],
["market/limit-down", "跌停池"],
["market/yesterday", "昨日涨停"],
["market/performance", "涨停表现"],
["market/ladder", "市场天梯"],
["market/rotation", "主题轮动"],
["market/auction", "竞价"],
["market/themes", "题材库"],
["market/popularity", "人气榜"],
["market/dragon", "龙虎榜"],
];
const TOOL_PAGES = [
["tools/screener", "智能选股"],
["tools/tracking", "策略跟踪"],
["tools/mentor", "问师"],
];
const REVIEW_PAGES = [
["review/watchlist", "自选股"],
["review/trades", "交易日志"],
["review/daily", "每日复盘"],
["review/notes", "个股笔记"],
["review/alerts", "提醒中心"],
];
test("mobile login renders before authentication", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.route("**/api/**", async (route) => {
const url = new URL(route.request().url());
if (url.pathname === "/api/auth/me") {
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ authenticated: false }) });
return;
}
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }) });
});
await page.goto("/m/");
await expect(page.locator("#m-auth-form")).toBeVisible();
await expect(page.locator("#m-auth-username")).toBeVisible();
await expect(page.locator("#m-tabbar")).toBeHidden();
});
test("four hub pages render their icon grids", async ({ page }) => {
await mockMobileApi(page);
await openMobile(page);
for (const hub of ["market", "tools", "review", "system"]) {
await page.evaluate((h) => { window.MobileRouter.navigate("#/hub/" + h); }, hub);
await expect(page.locator(".m-hub-grid")).toBeVisible();
await expect(page.locator(".m-hub-grid .m-grid-item").first()).toBeVisible();
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
}
});
for (const theme of ["day", "night"]) {
test(`market 12 pages render without overflow or invisible text (${theme})`, async ({ page }) => {
await mockMobileApi(page);
await openMobile(page);
await setMobileTheme(page, theme);
for (const [key, label] of MARKET_PAGES) {
await navigateToFeature(page, key);
await expect(page.locator("#m-title")).toHaveText(label);
await expect(page.locator("#m-view .m-page")).toBeVisible();
await page.waitForTimeout(120);
expect(await measureOverflow(page), `${key} overflows in ${theme}`).toBeLessThanOrEqual(1);
const invisible = await findInvisibleText(page);
expect(invisible, `${key} invisible text in ${theme}: ${JSON.stringify(invisible)}`).toEqual([]);
}
});
}
test("tools pages render screener, tracking and mentor", async ({ page }) => {
await mockMobileApi(page);
await openMobile(page);
for (const [key, label] of TOOL_PAGES) {
await navigateToFeature(page, key);
await expect(page.locator("#m-title")).toHaveText(label);
await page.waitForTimeout(120);
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
}
});
test("review five pages render watchlist, trades, daily, notes and alerts", async ({ page }) => {
await mockMobileApi(page);
await openMobile(page);
for (const [key, label] of REVIEW_PAGES) {
await navigateToFeature(page, key);
await expect(page.locator("#m-title")).toHaveText(label);
await page.waitForTimeout(120);
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
}
});
test("assistant chat renders with presets and input", async ({ page }) => {
await mockMobileApi(page);
await openMobile(page);
await page.evaluate(() => { window.MobileRouter.navigate("#/assistant/chat"); });
await expect(page.locator("#m-title")).toHaveText("复盘助手");
await expect(page.locator("#m-chat-input")).toBeVisible();
await expect(page.locator(".m-chat-presets")).toBeVisible();
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
});
test("empty dashboard shows empty state instead of a bare table", async ({ page }) => {
await mockMobileApi(page, { emptyDashboard: true });
await openMobile(page);
await navigateToFeature(page, "market/limit-up");
await expect(page.locator(".m-state")).toBeVisible();
await expect(page.locator(".m-state p")).toContainText("暂无相关数据");
});
test("dashboard failure shows error state with retry", async ({ page }) => {
await mockMobileApi(page, { failDashboard: true });
await openMobile(page);
await navigateToFeature(page, "market/limit-up");
await expect(page.locator(".m-state--error")).toBeVisible();
await expect(page.locator(".m-btn-retry")).toBeVisible();
});
test("landscape keeps pages usable and returns to portrait", async ({ page }) => {
await mockMobileApi(page);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/m/");
await expect(page.locator("#m-tabbar")).toBeVisible();
await page.setViewportSize({ width: 844, height: 390 });
await page.evaluate(() => { window.MobileRouter.navigate("#/feature/market/sentiment"); });
await page.waitForTimeout(150);
await expect(page.locator("#m-title")).toHaveText("情绪周期");
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
await expect(page.locator("#m-tabbar")).toBeVisible();
await page.evaluate(() => { window.MobileRouter.navigate("#/feature/market/limit-up"); });
await page.waitForTimeout(150);
await expect(page.locator(".m-table")).toBeVisible();
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
await page.setViewportSize({ width: 390, height: 844 });
await expect(page.locator("#m-tabbar")).toBeVisible();
expect(await measureOverflow(page)).toBeLessThanOrEqual(1);
});
+9 -81
View File
@@ -18,92 +18,20 @@ class DatabaseMigrationTests(unittest.TestCase):
rows = connection.execute( rows = connection.execute(
"SELECT version, name FROM schema_migrations" "SELECT version, name FROM schema_migrations"
).fetchall() ).fetchall()
self.assertEqual( self.assertEqual(
[(row["version"], row["name"]) for row in rows], [(row["version"], row["name"]) for row in rows],
[ [
("0001", "adopt_legacy_schema"), ("0001", "adopt_legacy_schema"),
("0002", "create_job_runs"), ("0002", "create_job_runs"),
("0003", "extend_llm_audit"), ("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) ReviewDatabase(path)
with database.connect() as connection: with database.connect() as connection:
count = connection.execute( count = connection.execute(
"SELECT COUNT(*) AS count FROM schema_migrations" "SELECT COUNT(*) AS count FROM schema_migrations"
).fetchone()["count"] ).fetchone()["count"]
self.assertEqual(count, 4) self.assertEqual(count, 3)
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: def test_connection_factory_enables_required_pragmas(self) -> None:
with tempfile.TemporaryDirectory() as root: with tempfile.TemporaryDirectory() as root:
+18 -95
View File
@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import re import re
import subprocess
import unittest import unittest
from html.parser import HTMLParser from html.parser import HTMLParser
from pathlib import Path from pathlib import Path
@@ -9,35 +8,12 @@ from pathlib import Path
from tests.frontend_test_helpers import ( from tests.frontend_test_helpers import (
assembled_frontend_runtime, assembled_frontend_runtime,
assembled_frontend_document, assembled_frontend_document,
registered_frontend_runtime_scripts,
) )
STATIC_DIR = Path(__file__).resolve().parents[1] / "frontend" 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): class IdCollector(HTMLParser):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -74,23 +50,11 @@ class FrontendContractTests(unittest.TestCase):
self.assertEqual(duplicates, []) self.assertEqual(duplicates, [])
def test_literal_id_selectors_exist_in_html(self): def test_literal_id_selectors_exist_in_html(self):
pending = uncommitted_runtime_files() selectors = set(re.findall(r'querySelector\("#([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
dangling: list[str] = [] selectors.update(re.findall(r'getElementById\("([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
for url in registered_frontend_runtime_scripts(): selectors.update(re.findall(r'setText\("([A-Za-z][A-Za-z0-9_-]*)"', self.script))
relative = url.split("?", 1)[0].lstrip("/") missing = sorted(selectors - set(self.ids))
if relative.startswith("vendor/"): self.assertEqual(missing, [])
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): 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)) views = set(re.findall(r'id="([A-Za-z][A-Za-z0-9_-]*View|limitPool)" class="workspace-view', self.html))
@@ -166,15 +130,6 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn('id="accountDropdown"', self.html) self.assertIn('id="accountDropdown"', self.html)
self.assertIn('id="settingsButton"', 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): def test_screener_uses_progressive_strategy_editor(self):
for step in ("regime", "strategy", "run", "result"): for step in ("regime", "strategy", "run", "result"):
self.assertIn(f'data-screener-step="{step}"', self.html) self.assertIn(f'data-screener-step="{step}"', self.html)
@@ -324,19 +279,19 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn("context.fillStyle = palette.axis;", chart) self.assertIn("context.fillStyle = palette.axis;", chart)
self.assertNotIn('context.fillStyle = "#6c7983";', chart) self.assertNotIn('context.fillStyle = "#6c7983";', chart)
def test_dark_mentor_tokens_and_sentiment_bottom_clearance_are_defined(self): def test_dark_mentor_answer_bubble_and_sentiment_bottom_clearance_are_defined(self):
self.assertIn(":root[data-theme=\"dark\"] #mentorView.workspace-view {", self.mentor_styles) self.assertIn(":root[data-theme=\"dark\"] #mentorView {", self.mentor_styles)
self.assertIn("--qp-bg-chat: #232529;", self.mentor_styles) self.assertIn("--mentor-ink: var(--text-primary);", self.mentor_styles)
self.assertIn("--qp-bg-bubble-self: #35598C;", self.mentor_styles) self.assertIn("--mentor-sub: var(--text-secondary);", self.mentor_styles)
self.assertIn("--qp-bg-selected: #2B3B58;", self.mentor_styles) self.assertIn(":root[data-theme=\"dark\"] #mentorView .mentor-message {", self.mentor_styles)
self.assertIn("--qp-link: #316FEF;", self.mentor_styles) self.assertIn("background: transparent;", self.mentor_styles)
self.assertIn("--qp-accent-soft: #5B8DEF;", self.mentor_styles) self.assertIn("box-shadow: none;", self.mentor_styles)
self.assertIn("--mentor-directory-width: 300px;", self.tokens) self.assertIn(
self.assertIn("--mentor-composer-min-height: 85px;", self.tokens) ':root[data-theme="dark"] #mentorView .mentor-message.assistant .mentor-message-content {',
self.assertIn("#mentorView .mentor-message.user .mentor-message-content {", self.mentor_styles) 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("border-color: var(--border);", self.mentor_styles)
self.assertIn("#mentorView .mentor-composer-hint {", self.mentor_styles) self.assertIn("background: var(--surface-muted);", self.mentor_styles)
self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.sentiment_styles) self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.sentiment_styles)
self.assertIn("margin-bottom: var(--card-gap);", self.sentiment_styles) self.assertIn("margin-bottom: var(--card-gap);", self.sentiment_styles)
self.assertIn("padding-bottom: var(--card-gap);", self.sentiment_styles) self.assertIn("padding-bottom: var(--card-gap);", self.sentiment_styles)
@@ -344,38 +299,6 @@ class FrontendContractTests(unittest.TestCase):
self.assertIn("max-height: var(--sentiment-history-max-height);", self.sentiment_styles) self.assertIn("max-height: var(--sentiment-history-max-height);", self.sentiment_styles)
self.assertIn("overflow: auto;", 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): 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('typeof document.startViewTransition === "function"', self.script)
self.assertIn('root.classList.add("theme-switching")', self.script) self.assertIn('root.classList.add("theme-switching")', self.script)
+22
View File
@@ -59,6 +59,28 @@ class JobRunnerTests(unittest.TestCase):
release.set() release.set()
self.assertTrue(self.runner.wait_for_idle()) self.assertTrue(self.runner.wait_for_idle())
def test_database_claim_rejects_the_same_job_from_another_runner(self) -> None:
other_runner = InProcessJobRunner(JobRegistry.load(), self.repository)
entered = threading.Event()
release = threading.Event()
calls = []
def wait() -> None:
calls.append("first")
entered.set()
release.wait(2)
self.assertTrue(self.runner.submit("screener.automatic", "shared-key", wait))
self.assertTrue(entered.wait(1))
self.assertFalse(
other_runner.run_inline(
"screener.automatic", "shared-key", lambda: calls.append("second")
)
)
release.set()
self.assertTrue(self.runner.wait_for_idle())
self.assertEqual(calls, ["first"])
def test_failed_status_payload_is_recorded_as_failure(self) -> None: def test_failed_status_payload_is_recorded_as_failure(self) -> None:
self.assertTrue( self.assertTrue(
self.runner.run_inline( self.runner.run_inline(
+136
View File
@@ -0,0 +1,136 @@
from __future__ import annotations
import json
import unittest
from types import SimpleNamespace
from backend.features.system.health import HealthServiceMixin
from backend.features.system.routes import SystemRoutesMixin
class _Connection:
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def execute(self, _query: str):
return self
def fetchone(self):
return (1,)
class _Database:
def connect(self):
return _Connection()
def status(self):
return {
"database": "secret-review.db",
"snapshot_dates": 31,
"updated_at": "2026-08-07T11:34:55+08:00",
"last_sync": {
"id": 991,
"trade_date": "20260807",
"source": "tushare",
"status": "success",
"finished_at": "2026-08-07T11:34:55+08:00",
"message": "https://provider.invalid?token=secret",
},
}
class _Service(HealthServiceMixin):
def __init__(self):
self.database = _Database()
self.jobs = SimpleNamespace(
repository=SimpleNamespace(
recent=lambda _limit: [
{
"id": 77,
"status": "success",
"finished_at": "2026-08-07T11:34:55+08:00",
"message": "secret upstream response",
}
]
)
)
self._system_credentials = {
"tushare_token": "secret-token",
"primary_model_id": "primary-id",
"fallback_model_id": "fallback-id",
}
@property
def configured(self):
return bool(self._system_credentials.get("tushare_token"))
def _platform_llm_profile(self):
return {
"primary": {
"api_key": "secret-primary-key",
"base_url": "https://models.invalid/v1",
"model": "secret-primary-model",
},
"fallback": {
"api_key": "secret-fallback-key",
"base_url": "https://models.invalid/v1",
"model": "secret-fallback-model",
},
}
@staticmethod
def _profile_configured(profile):
return all(profile.get(key) for key in ("api_key", "base_url", "model"))
class SystemHealthTests(unittest.TestCase):
def test_public_route_uses_the_service_health_contract(self):
handler = SystemRoutesMixin()
expected = {"ok": True, "components": {"process": {"status": "ready"}}}
handler.application_service = SimpleNamespace(health_status=lambda: expected)
responses = []
handler.send_json = responses.append
handled = handler._handle_system_public_get(SimpleNamespace(path="/api/health"))
self.assertTrue(handled)
self.assertEqual(responses, [expected])
def test_health_distinguishes_runtime_components(self):
result = _Service().health_status()
self.assertTrue(result["ok"])
self.assertEqual(result["status"], "ready")
self.assertEqual(
set(result["components"]),
{"process", "database", "data_sources", "jobs", "models"},
)
self.assertEqual(result["components"]["data_sources"]["latest_trade_date"], "20260807")
self.assertEqual(result["components"]["models"]["primary"], "ready")
self.assertEqual(result["components"]["models"]["fallback"], "ready")
def test_public_health_never_exposes_engineering_or_secret_fields(self):
payload = json.dumps(_Service().health_status(), ensure_ascii=False).lower()
for forbidden in (
"tushare",
"ifind",
"secret",
"token",
"https://",
".db",
"primary-id",
"fallback-id",
"model",
):
if forbidden == "model":
self.assertNotIn("secret-primary-model", payload)
else:
self.assertNotIn(forbidden, payload)
if __name__ == "__main__":
unittest.main()