Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6174efaee5 | ||
|
|
574a375292 | ||
|
|
7bf5d8c2b6 | ||
|
|
6144a480c7 | ||
|
|
b213b4362a | ||
|
|
1b333b6b93 | ||
|
|
224e2a7f21 | ||
|
|
ed9858e330 | ||
|
|
3ae07b8aae | ||
|
|
5a507239a3 | ||
|
|
152c0000ce | ||
|
|
cf7183d0c0 | ||
|
|
22d4dcd339 | ||
|
|
ee37223722 | ||
|
|
ad08d309c6 | ||
|
|
6ab910eeea | ||
|
|
f67100929b | ||
|
|
df45638edd | ||
|
|
bb67085dd4 | ||
|
|
e778c883db | ||
|
|
b60a4cb682 | ||
|
|
74304795ec | ||
|
|
0fba71f62c | ||
|
|
e0cba74f8e | ||
|
|
39b6f71443 | ||
|
|
8ac3adbb5d |
@@ -1,8 +1,14 @@
|
|||||||
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 = (M0001_ADOPT_LEGACY, M0002_JOB_RUNS, M0003_LLM_AUDIT)
|
MIGRATIONS = (
|
||||||
|
M0001_ADOPT_LEGACY,
|
||||||
|
M0002_JOB_RUNS,
|
||||||
|
M0003_LLM_AUDIT,
|
||||||
|
M0004_MENTOR_NOTES,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ["MIGRATIONS", "Migration", "MigrationError", "MigrationRunner"]
|
__all__ = ["MIGRATIONS", "Migration", "MigrationError", "MigrationRunner"]
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from backend.database.migrations.runner import Migration
|
||||||
|
|
||||||
|
|
||||||
|
def add_mentor_note(connection: sqlite3.Connection) -> None:
|
||||||
|
columns = {
|
||||||
|
str(row["name"])
|
||||||
|
for row in connection.execute("PRAGMA table_info(mentor_preferences)")
|
||||||
|
}
|
||||||
|
if "note" not in columns:
|
||||||
|
connection.execute(
|
||||||
|
"ALTER TABLE mentor_preferences ADD COLUMN note TEXT NOT NULL DEFAULT ''"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MIGRATION = Migration(
|
||||||
|
version="0004",
|
||||||
|
name="add_mentor_note",
|
||||||
|
action=add_mentor_note,
|
||||||
|
signature="mentor-preferences-note:v1:note",
|
||||||
|
)
|
||||||
@@ -104,6 +104,13 @@ 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:
|
||||||
|
|||||||
@@ -370,29 +370,29 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"css_layers": [
|
"css_layers": [
|
||||||
"/shared/tokens.css?v=20260806-2",
|
"/shared/tokens.css?v=20260820-3",
|
||||||
"/shared/base.css?v=20260806-1",
|
"/shared/base.css?v=20260806-1",
|
||||||
"/shared/shell.css?v=20260806-2",
|
"/shared/shell.css?v=20260820-8",
|
||||||
"/shared/auth.css?v=20260806-1",
|
"/shared/auth.css?v=20260820-5",
|
||||||
"/shared/components/controls.css?v=20260806-1",
|
"/shared/components/controls.css?v=20260820-2",
|
||||||
"/shared/components/navigation.css?v=20260806-1",
|
"/shared/components/navigation.css?v=20260820-1",
|
||||||
"/shared/components/cards.css?v=20260806-1",
|
"/shared/components/cards.css?v=20260820-1",
|
||||||
"/shared/components/tables.css?v=20260806-1",
|
"/shared/components/tables.css?v=20260820-1",
|
||||||
"/shared/components/dialogs.css?v=20260806-1",
|
"/shared/components/dialogs.css?v=20260820-3",
|
||||||
"/shared/components/feedback.css?v=20260806-1",
|
"/shared/components/feedback.css?v=20260806-1",
|
||||||
"/pages/market/foundation.css?v=20260806-1",
|
"/pages/market/foundation.css?v=20260820-4",
|
||||||
"/pages/sentiment/foundation.css?v=20260806-2",
|
"/pages/sentiment/foundation.css?v=20260820-2",
|
||||||
"/pages/pools/foundation.css?v=20260806-2",
|
"/pages/pools/foundation.css?v=20260820-1",
|
||||||
"/pages/ladder/foundation.css?v=20260806-1",
|
"/pages/ladder/foundation.css?v=20260820-1",
|
||||||
"/pages/rotation/foundation.css?v=20260806-1",
|
"/pages/rotation/foundation.css?v=20260820-1",
|
||||||
"/pages/auction/foundation.css?v=20260806-2",
|
"/pages/auction/foundation.css?v=20260820-1",
|
||||||
"/pages/themes/foundation.css?v=20260806-1",
|
"/pages/themes/foundation.css?v=20260820-1",
|
||||||
"/pages/popularity/foundation.css?v=20260806-1",
|
"/pages/popularity/foundation.css?v=20260820-1",
|
||||||
"/pages/dragon-tiger/foundation.css?v=20260806-1",
|
"/pages/dragon-tiger/foundation.css?v=20260820-1",
|
||||||
"/pages/screener/foundation.css?v=20260806-2",
|
"/pages/screener/foundation.css?v=20260820-4",
|
||||||
"/pages/mentor/foundation.css?v=20260806-2",
|
"/pages/mentor/foundation.css?v=20260820-2",
|
||||||
"/pages/heaven/foundation.css?v=20260806-2",
|
"/pages/heaven/foundation.css?v=20260806-2",
|
||||||
"/pages/review/foundation.css?v=20260806-1"
|
"/pages/review/foundation.css?v=20260820-4"
|
||||||
],
|
],
|
||||||
"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": 102987,
|
"bytes": 103547,
|
||||||
"lines": 6565
|
"lines": 6576
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": 51978,
|
"bytes": 63550,
|
||||||
"lines": 3224
|
"lines": 3757
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": 45846,
|
"bytes": 47131,
|
||||||
"lines": 643
|
"lines": 646
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": 34990,
|
"bytes": 35247,
|
||||||
"lines": 2409
|
"lines": 2416
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "database.py",
|
"path": "database.py",
|
||||||
@@ -531,7 +531,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/pages/pools/page.html",
|
"path": "frontend/pages/pools/page.html",
|
||||||
"bytes": 14958,
|
"bytes": 14942,
|
||||||
"lines": 235
|
"lines": 235
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -541,8 +541,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/shared/admin.js",
|
"path": "frontend/shared/admin.js",
|
||||||
"bytes": 13975,
|
"bytes": 14145,
|
||||||
"lines": 256
|
"lines": 261
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/heaven/market_context.py",
|
"path": "backend/features/heaven/market_context.py",
|
||||||
@@ -574,6 +574,11 @@
|
|||||||
"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,
|
||||||
@@ -600,9 +605,9 @@
|
|||||||
"lines": 238
|
"lines": 238
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/shared/dashboard.js",
|
"path": "frontend/pages/mentor/page.html",
|
||||||
"bytes": 8424,
|
"bytes": 8357,
|
||||||
"lines": 194
|
"lines": 116
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/screener/formula.py",
|
"path": "backend/features/screener/formula.py",
|
||||||
@@ -644,11 +649,6 @@
|
|||||||
"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,6 +704,11 @@
|
|||||||
"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,
|
||||||
@@ -714,11 +719,6 @@
|
|||||||
"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": 3309,
|
"bytes": 3316,
|
||||||
"lines": 55
|
"lines": 55
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
> ⚠️ **本文档已过时,仅留档备查,请勿删除。**
|
||||||
|
> 本交接说明核实于 2026-08-06,其中「当前提交」「当前状态」「正在处理的事项」「验证记录」等已与代码现状不符(当时的未提交改动现已合并,项目已推进到全站视觉统一收尾阶段)。
|
||||||
|
> 最新内容请看 `docs/项目需求.md`、`docs/最新进度.md`、`docs/任务清单.md` 和 `docs/README.md`。
|
||||||
|
> 架构与维护规矩仍以根目录 `AGENTS.md`、`ARCHITECTURE.md` 为准;本文第 2、6 节(架构与决策)仍可作参考。
|
||||||
|
|
||||||
# 小白复盘项目交接说明
|
# 小白复盘项目交接说明
|
||||||
|
|
||||||
> 核实日期:2026-08-06(Asia/Shanghai)
|
> 核实日期:2026-08-06(Asia/Shanghai)
|
||||||
|
|||||||
+33
-9
@@ -1,11 +1,35 @@
|
|||||||
# 文档索引
|
# 小白复盘 · 交接手册首页(首页说明)
|
||||||
|
|
||||||
- `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. 更新完提交并推送进仓库(保存并上传到放代码的网站),不能只留在自己电脑里。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 旧文档不能删:被替代的旧文档开头要加一行「⚠️ 本文档已过时,仅留档备查,请勿删除」,再写新版。
|
||||||
|
- 用中文大白话写,专业词要带通俗解释,让不懂代码的人也能看懂。
|
||||||
|
- 「问天」板块是冻结区,任何改动都不许碰;写文档时别误导后来人去改它。
|
||||||
|
|||||||
@@ -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/HANDOFF.md` 的状态、验证日期和回档提交;不能只改 Issue 标题。
|
关闭任一 Issue 前,必须同步更新 `docs/最新进度.md` 与 `docs/任务清单.md` 的状态、验证日期和回档提交;不能只改 Issue 标题。旧版 `docs/HANDOFF.md` 已过时,仅留档备查。
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 任务清单
|
||||||
|
|
||||||
|
> 分三栏:正在做 / 已做完 / 还没安排。以任务板(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)。
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 最新进度
|
||||||
|
|
||||||
|
> 核实日期: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`。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# 项目需求
|
||||||
|
|
||||||
|
> 大白话版。逐页、逐字段的完整需求请看 `product/小白复盘-完整产品规格说明书.md`(那是最权威的“说明书”,从零重建项目都用它)。
|
||||||
|
|
||||||
|
## 这个项目是干什么的
|
||||||
|
|
||||||
|
「小白复盘」是一个给老板个人用的股票复盘工具网站,放在内网访问(地址 `192.168.200.36:8765`)。
|
||||||
|
|
||||||
|
它不是炒股下单软件,而是「收盘后和开盘前」用来复盘、观察市场的工具:
|
||||||
|
|
||||||
|
- 把当天(或最近)的真实行情、涨停跌停、板块轮动、集合竞价等信息整理清楚,帮老板复盘。
|
||||||
|
- 提供智能选股、问师(跟“游资思维”老师对话)等辅助分析。
|
||||||
|
- 记录自己的交易和复盘。
|
||||||
|
|
||||||
|
一句话:它帮你“看清市场、想清楚思路、记下来”,但不替你做买卖决定。
|
||||||
|
|
||||||
|
## 要解决什么问题
|
||||||
|
|
||||||
|
1. **市场信息太散**:涨停池、炸板池、跌停板、龙虎榜、人气榜、板块轮动这些信息本来分散在各处,这个网站把它们集中到一处,还配了日间/夜间两套配色,看起来统一。
|
||||||
|
2. **复盘靠脑子记不住**:提供“我的复盘”和交易日志,把每天的判断、操作、情绪记录下来。
|
||||||
|
3. **选股没思路**:智能选股用规则和因子(影响股价的数据指标)帮你筛出候选股票。
|
||||||
|
4. **想听“高手”怎么看**:问师模块可以按不同的游资思维(比如佛山无影脚、北京炒家等)跟老师对话。
|
||||||
|
|
||||||
|
## 主要功能(板块)
|
||||||
|
|
||||||
|
登录后侧栏有 16 个主工作区,默认进入「情绪周期」:
|
||||||
|
|
||||||
|
- **市场类(12 个)**:情绪周期、涨停池、炸板池、跌停板、昨日涨停、涨停表现、市场天梯、板块轮动、集合竞价、题材库、人气热榜、龙虎榜。
|
||||||
|
- **智能工具类(3 个)**:智能选股、问师、问天。
|
||||||
|
- **个人类(1 个)**:我的复盘。
|
||||||
|
|
||||||
|
其中「问天」是冻结区(见下面的硬规矩)。
|
||||||
|
|
||||||
|
## 几条硬规矩(不能破坏的边界)
|
||||||
|
|
||||||
|
- 「问天」板块是**冻结区**,任何改动都不许碰它。
|
||||||
|
- **不用假数据冒充真行情**;数据缺失就明说“没有/不可用”,不能编。
|
||||||
|
- **每个用户自己的数据互相隔离**(自选、复盘、对话、问天历史等),看不到别人的。
|
||||||
|
- **计算由程序确定性完成**(情绪周期、智能选股、问天排盘等),AI 大模型(LLM,就是会聊天的那个 AI)只负责解释或编译自然语言条件,不能改计算结果。
|
||||||
|
- **不接券商、不自动下单**,不承诺收益。
|
||||||
|
|
||||||
|
## 权限
|
||||||
|
|
||||||
|
- **普通用户**:能用大部分市场页面和「我的复盘」。
|
||||||
|
- **会员**:额外能用智能选股、问师、问天(需要管理员开通)。
|
||||||
|
- **管理员**:管理公共行情密钥、会员额度和系统配置等。
|
||||||
+89
-71
@@ -5,6 +5,21 @@
|
|||||||
<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";
|
||||||
@@ -17,29 +32,29 @@
|
|||||||
document.documentElement.style.colorScheme = theme;
|
document.documentElement.style.colorScheme = theme;
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/shared/tokens.css?v=20260806-2">
|
<link rel="stylesheet" href="/shared/tokens.css?v=20260820-3">
|
||||||
<link rel="stylesheet" href="/shared/base.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/base.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/shell.css?v=20260806-2">
|
<link rel="stylesheet" href="/shared/shell.css?v=20260820-8">
|
||||||
<link rel="stylesheet" href="/shared/auth.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/auth.css?v=20260820-5">
|
||||||
<link rel="stylesheet" href="/shared/components/controls.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/components/controls.css?v=20260820-2">
|
||||||
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/shared/components/cards.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/components/cards.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/shared/components/tables.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/components/tables.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260820-3">
|
||||||
<link rel="stylesheet" href="/shared/components/feedback.css?v=20260806-1">
|
<link rel="stylesheet" href="/shared/components/feedback.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260820-4">
|
||||||
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260806-2">
|
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260820-2">
|
||||||
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260806-2">
|
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260806-2">
|
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260820-1">
|
||||||
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260806-2">
|
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260820-4">
|
||||||
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260806-2">
|
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260820-2">
|
||||||
<link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260806-2">
|
<link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260806-1">
|
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260820-4">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="authGate" class="auth-gate" aria-label="账号登录">
|
<section id="authGate" class="auth-gate" aria-label="账号登录">
|
||||||
@@ -68,12 +83,56 @@
|
|||||||
<span id="mobilePageGroup">行情</span>
|
<span id="mobilePageGroup">行情</span>
|
||||||
<strong id="mobilePageTitle">情绪周期</strong>
|
<strong id="mobilePageTitle">情绪周期</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="market-tape" aria-label="市场概况">
|
<div class="app-page-context" aria-live="polite">
|
||||||
<span class="market-item up">上涨 <strong id="tapeUp">--</strong></span>
|
<strong id="currentPageTitle">小白复盘</strong>
|
||||||
<span class="market-item down">下跌 <strong id="tapeDown">--</strong></span>
|
<span id="currentPageSubtitle"></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">
|
||||||
@@ -82,7 +141,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></button>
|
<button id="themeToggle" class="icon-button theme-toggle" type="button" title="切换到夜间模式" aria-label="切换到夜间模式" aria-pressed="false"><i data-lucide="moon"></i><span id="themeModeText">日间模式</span></button>
|
||||||
<button id="alertButton" class="icon-button alert-button" type="button" title="提醒中心" aria-label="提醒中心"><i data-lucide="bell"></i><span id="alertBadge" class="alert-badge" hidden>0</span></button>
|
<button id="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>
|
||||||
@@ -93,12 +152,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"><i data-lucide="refresh-cw"></i><span>刷新</span></button>
|
<button id="refreshButton" class="button command-button" type="button" title="刷新"><i data-lucide="refresh-cw"></i><span>刷新</span></button>
|
||||||
<button id="syncButton" class="button primary command-button" type="button" hidden><i data-lucide="cloud-download"></i><span>后台刷新</span></button>
|
<button id="syncButton" class="button primary command-button" type="button" title="后台刷新" hidden><i data-lucide="cloud-download"></i><span>后台刷新</span></button>
|
||||||
<button id="settingsButton" class="button command-button" type="button" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
|
<button id="settingsButton" class="button command-button" type="button" title="系统管理" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
|
||||||
<div class="account-menu-shell">
|
<div 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" hidden><i data-lucide="shield-check"></i><span>管理员</span></span>
|
<span id="accountAdminBadge" class="account-role-badge admin-role-badge" title="管理员" hidden><i data-lucide="shield-check"></i><span>管理员</span></span>
|
||||||
<button id="accountVipBadge" class="account-role-badge vip-role-badge" type="button" title="查看会员状态" hidden><b aria-hidden="true">V</b><span id="accountVipLabel">非会员</span></button>
|
<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>
|
||||||
@@ -169,47 +228,6 @@
|
|||||||
</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>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
(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: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
global.MobileNav = nav;
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-header-btn:active {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-grid-item:active {
|
||||||
|
background: var(--surface);
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--action);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-btn-primary:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-tabbar-item:active {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-tabbar-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
--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-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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!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">
|
||||||
|
</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>
|
||||||
|
<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="js/api.js"></script>
|
||||||
|
<script src="js/session.js"></script>
|
||||||
|
<script src="js/router.js"></script>
|
||||||
|
<script src="js/boot.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let csrfToken = "";
|
||||||
|
|
||||||
|
function setCsrfToken(token) {
|
||||||
|
csrfToken = token || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestOptions(method, body) {
|
||||||
|
const normalized = String(method || "GET").toUpperCase();
|
||||||
|
const options = { method: normalized, headers: {} };
|
||||||
|
if (!["GET", "HEAD", "OPTIONS"].includes(normalized) && csrfToken) {
|
||||||
|
options.headers["X-CSRF-Token"] = csrfToken;
|
||||||
|
}
|
||||||
|
if (body !== null && body !== undefined) {
|
||||||
|
options.headers["Content-Type"] = "application/json";
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(url, method, body) {
|
||||||
|
const response = await fetch(url, requestOptions(method, body));
|
||||||
|
let payload = {};
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch (_error) {
|
||||||
|
payload = {};
|
||||||
|
}
|
||||||
|
if (!response.ok || payload.error) {
|
||||||
|
const error = new Error(payload.error || "请求失败");
|
||||||
|
error.status = response.status;
|
||||||
|
error.payload = payload;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
global.MobileAPI = { setCsrfToken, request };
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
(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;
|
||||||
|
}
|
||||||
|
const inset = Math.max(0, global.innerHeight - visual.height - visual.offsetTop);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
global.MobileRouter.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", boot, { once: true });
|
||||||
|
} else {
|
||||||
|
boot();
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
(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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_HASH = "#/hub/market";
|
||||||
|
|
||||||
|
const stack = [];
|
||||||
|
let internalNav = 0;
|
||||||
|
let authMode = "login";
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(hash) {
|
||||||
|
if (stack.length) stack[stack.length - 1] = hash;
|
||||||
|
else stack.push(hash);
|
||||||
|
setHash(hash);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parent = parentRoute(currentRoute());
|
||||||
|
if (parent) replace(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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">' +
|
||||||
|
'<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 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) {
|
||||||
|
const title = findFeatureLabel(key) || key;
|
||||||
|
updateHeader({ title: title, back: true });
|
||||||
|
document.getElementById("m-view").innerHTML = placeholderHtml(title, "该功能页将在后续批次实现。");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAssistant() {
|
||||||
|
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">登录</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";
|
||||||
|
document.getElementById("m-auth-submit").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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
errorElement.textContent = "两次输入的密码不一致。";
|
||||||
|
errorElement.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const submit = document.getElementById("m-auth-submit");
|
||||||
|
submit.disabled = true;
|
||||||
|
try {
|
||||||
|
if (authMode === "register") {
|
||||||
|
await global.MobileSession.register(username, password);
|
||||||
|
} else {
|
||||||
|
await global.MobileSession.login(username, password);
|
||||||
|
}
|
||||||
|
replace(DEFAULT_HASH);
|
||||||
|
} catch (error) {
|
||||||
|
errorElement.textContent = error.message || "账号操作失败";
|
||||||
|
errorElement.hidden = false;
|
||||||
|
} finally {
|
||||||
|
submit.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const route = currentRoute();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
window.addEventListener("hashchange", function () {
|
||||||
|
if (internalNav > 0) {
|
||||||
|
internalNav--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hash = currentHash();
|
||||||
|
if (stack.length > 1 && hash === stack[stack.length - 2]) {
|
||||||
|
stack.pop();
|
||||||
|
} else if (hash !== stack[stack.length - 1]) {
|
||||||
|
stack.push(hash);
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.MobileRouter = {
|
||||||
|
init: init,
|
||||||
|
navigate: navigate,
|
||||||
|
replace: replace,
|
||||||
|
back: back,
|
||||||
|
render: render,
|
||||||
|
currentRoute: currentRoute
|
||||||
|
};
|
||||||
|
})(window);
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
(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);
|
||||||
@@ -42,15 +42,15 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const fragments = [
|
const fragments = [
|
||||||
["pools", "/pages/pools/page.html?v=20260803-1", ["limitPool", "brokenView", "downView", "yesterdayView", "performanceView"]],
|
["pools", "/pages/pools/page.html?v=20260820-1", ["limitPool", "brokenView", "downView", "yesterdayView", "performanceView"]],
|
||||||
["sentiment", "/pages/sentiment/page.html?v=20260803-1", ["sentimentCycleView"]],
|
["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=20260803-1", ["ladderView"]],
|
["ladder", "/pages/ladder/page.html?v=20260820-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=20260806-2", ["mentorView"]],
|
["mentor", "/pages/mentor/page.html?v=20260820-1", ["mentorView"]],
|
||||||
["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]],
|
["rotation", "/pages/rotation/page.html?v=20260820-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=20260803-1", ["themeLibraryView"]],
|
["themes", "/pages/themes/page.html?v=20260820-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=20260729-1",
|
"/pages/pools/page.js?v=20260820-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=20260729-1",
|
"/pages/ladder/page.js?v=20260820-1",
|
||||||
"/pages/rotation/page.js?v=20260729-1",
|
"/pages/rotation/page.js?v=20260820-1",
|
||||||
"/pages/auction/page.js?v=20260729-1",
|
"/pages/auction/page.js?v=20260729-1",
|
||||||
"/pages/themes/page.js?v=20260729-1",
|
"/pages/themes/page.js?v=20260820-1",
|
||||||
"/pages/popularity/page.js?v=20260729-1",
|
"/pages/popularity/page.js?v=20260820-1",
|
||||||
"/pages/dragon-tiger/page.js?v=20260806-1",
|
"/pages/dragon-tiger/page.js?v=20260820-1",
|
||||||
"/pages/screener/page.js?v=20260806-1",
|
"/pages/screener/page.js?v=20260806-1",
|
||||||
"/pages/mentor/page.js?v=20260806-2",
|
"/pages/mentor/page.js?v=20260820-1",
|
||||||
"/pages/heaven/page.js?v=20260729-1",
|
"/pages/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=20260806-1",
|
"/shared/shell.js?v=20260820-2",
|
||||||
"/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=20260803-1",
|
"/shared/dashboard.js?v=20260820-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",
|
||||||
|
|||||||
+47
-40
@@ -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: 17px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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: rgb(238, 242, 246);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
color: rgb(89, 101, 116);
|
color: var(--text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-phase-marker {
|
.auction-phase-marker {
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
.auction-table {
|
.auction-table {
|
||||||
min-width: 860px;
|
min-width: 860px;
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: rgb(201, 214, 238);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-amount-average {
|
.auction-amount-average {
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auction-table td {
|
.auction-table td {
|
||||||
height: 44px;
|
height: 40px;
|
||||||
|
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
|
|
||||||
@@ -530,7 +530,7 @@
|
|||||||
|
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
|
|
||||||
border-bottom-color: rgb(232, 237, 241);
|
border-bottom-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-table th {
|
.auction-table th {
|
||||||
@@ -540,13 +540,13 @@
|
|||||||
|
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
|
|
||||||
height: 38px;
|
height: 36px;
|
||||||
|
|
||||||
color: rgb(107, 114, 128);
|
color: var(--text-2);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
border-bottom-color: rgb(232, 237, 241);
|
border-bottom-color: var(--border);
|
||||||
|
|
||||||
background: var(--table-header);
|
background: var(--table-header);
|
||||||
}
|
}
|
||||||
@@ -694,9 +694,9 @@
|
|||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 17px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
}
|
}
|
||||||
@@ -812,13 +812,13 @@
|
|||||||
100% {
|
100% {
|
||||||
opacity: 0.42;
|
opacity: 0.42;
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0.18) 0px 0px 0px 0px;
|
box-shadow: 0 0 0 0 var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0) 0px 0px 0px 4px;
|
box-shadow: 0 0 0 4px transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,7 +896,9 @@
|
|||||||
|
|
||||||
.auction-export-button,
|
.auction-export-button,
|
||||||
.auction-refresh-button {
|
.auction-refresh-button {
|
||||||
min-height: 30px;
|
min-height: 32px;
|
||||||
|
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -906,17 +908,17 @@
|
|||||||
|
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|
||||||
padding: 5px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border: 1px solid var(--r2-line);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
@@ -1144,7 +1146,7 @@
|
|||||||
.auction-search-v2 {
|
.auction-search-v2 {
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
|
|
||||||
height: 31px;
|
height: 32px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -1154,9 +1156,9 @@
|
|||||||
|
|
||||||
padding: 0px 9px;
|
padding: 0px 9px;
|
||||||
|
|
||||||
border: 1px solid var(--r2-line);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
@@ -1193,7 +1195,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: rgba(37, 99, 235, 0.28) solid 2px;
|
outline: 2px solid var(--accent);
|
||||||
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -1219,7 +1221,7 @@
|
|||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-table-v2 thead th {
|
.auction-table-v2 thead th {
|
||||||
@@ -1229,12 +1231,16 @@
|
|||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
height: 35px;
|
height: 36px;
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1242,6 +1248,11 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -1269,7 +1280,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auction-table-v2 tbody td {
|
.auction-table-v2 tbody td {
|
||||||
height: 47px;
|
height: 40px;
|
||||||
|
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--r2-line-soft);
|
border-bottom: 1px solid var(--r2-line-soft);
|
||||||
|
|
||||||
@@ -1819,7 +1832,7 @@
|
|||||||
|
|
||||||
border-radius: 3px 3px 0px 0px;
|
border-radius: 3px 3px 0px 0px;
|
||||||
|
|
||||||
background: rgb(201, 214, 238);
|
background: var(--accent-soft);
|
||||||
|
|
||||||
transition: opacity 180ms, transform 180ms;
|
transition: opacity 180ms, transform 180ms;
|
||||||
|
|
||||||
@@ -1915,7 +1928,7 @@
|
|||||||
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
background: rgb(201, 214, 238);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-volume-legend i.current {
|
.auction-volume-legend i.current {
|
||||||
@@ -2089,10 +2102,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -2138,11 +2147,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auction-table-v2 tbody td {
|
.auction-table-v2 tbody td {
|
||||||
height: 43px;
|
height: 40px;
|
||||||
|
|
||||||
padding-top: 6px;
|
padding: 0 12px;
|
||||||
|
|
||||||
padding-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2165,9 +2172,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auc-head h2 {
|
.auc-head h2 {
|
||||||
font-size: 17px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auc-grid {
|
.auc-grid {
|
||||||
|
|||||||
+33
-19
@@ -82,7 +82,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dragon-operation-table th {
|
.dragon-operation-table th {
|
||||||
background: rgb(232, 240, 244);
|
background: var(--table-header);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seat-cell {
|
.seat-cell {
|
||||||
@@ -952,6 +952,10 @@
|
|||||||
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 {
|
||||||
@@ -991,7 +995,7 @@
|
|||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|
||||||
@@ -1003,19 +1007,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dragon-view-tabs-v2 button {
|
.dragon-view-tabs-v2 button {
|
||||||
min-height: 27px;
|
min-height: 26px;
|
||||||
|
|
||||||
padding: 0px 13px;
|
padding: 0px 13px;
|
||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1037,11 +1041,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dragon-action-v2 {
|
.dragon-action-v2 {
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
padding: 0px 11px;
|
height: 32px;
|
||||||
|
|
||||||
border-radius: 7px;
|
padding: 0px 12px;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragon-action-v2 .lucide {
|
.dragon-action-v2 .lucide {
|
||||||
@@ -1101,9 +1109,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dragon-summary-v2 .dragon-metric span {
|
.dragon-summary-v2 .dragon-metric span {
|
||||||
color: var(--r2-sub);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragon-summary-v2 .dragon-metric strong {
|
.dragon-summary-v2 .dragon-metric strong {
|
||||||
@@ -1111,9 +1119,9 @@
|
|||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: var(--font-size-metric);
|
||||||
|
|
||||||
font-weight: 750;
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
@@ -1245,7 +1253,7 @@
|
|||||||
|
|
||||||
width: 230px;
|
width: 230px;
|
||||||
|
|
||||||
height: 33px;
|
height: 32px;
|
||||||
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
@@ -1253,9 +1261,9 @@
|
|||||||
|
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
|
|
||||||
border: 1px solid rgb(216, 221, 229);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
@@ -1263,9 +1271,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dragon-search-v2:focus-within {
|
.dragon-search-v2:focus-within {
|
||||||
border-color: rgb(150, 181, 242);
|
border-color: var(--accent);
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0.09) 0px 0px 0px 3px;
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragon-search-v2 .lucide {
|
.dragon-search-v2 .lucide {
|
||||||
@@ -1421,7 +1429,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: center;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dragonView .dragon-operation-table td.stock-code {
|
#dragonView .dragon-operation-table td.stock-code {
|
||||||
@@ -1435,6 +1443,12 @@
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2389,7 +2403,7 @@ table.tbl {
|
|||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tbl .num {
|
.tbl .num {
|
||||||
|
|||||||
@@ -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 num">序号</th><th>股票</th><th>方向</th><th class="number num">涨幅(%)</th><th class="number num">买入(百万)</th><th class="number num">卖出(百万)</th><th class="number num">净额(百万)</th><th>关联席位</th><th class="reason-column">标签 / 上榜原因</th></tr></thead>
|
<thead><tr><th class="row-number">序号</th><th>股票</th><th>方向</th><th class="number num">涨幅(%)</th><th class="number num">买入(百万)</th><th class="number num">卖出(百万)</th><th class="number num">净额(百万)</th><th>关联席位</th><th class="reason-column">标签 / 上榜原因</th></tr></thead>
|
||||||
<tbody>${(trader.operations || []).map((operation, index) => `
|
<tbody>${(trader.operations || []).map((operation, index) => `
|
||||||
<tr data-code="${escapeHtml(operation.code)}">
|
<tr data-code="${escapeHtml(operation.code)}">
|
||||||
<td class="row-number num">${index + 1}</td>
|
<td class="row-number">${index + 1}</td>
|
||||||
<td><strong class="sname">${escapeHtml(operation.name)}</strong><span class="scode">${escapeHtml(operation.code)}</span></td>
|
<td><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>
|
||||||
|
|||||||
+26
-18
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
padding: 9px 4px;
|
padding: 9px 4px;
|
||||||
|
|
||||||
border-bottom: 1px solid rgb(232, 236, 239);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
|
||||||
background: rgb(247, 249, 250);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@@ -140,11 +140,11 @@
|
|||||||
|
|
||||||
border: 1px dashed var(--border-strong);
|
border: 1px dashed var(--border-strong);
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
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, rgb(255, 255, 255), rgb(255, 255, 255) 9px, rgb(250, 251, 252) 9px, rgb(250, 251, 252) 18px);
|
background: repeating-linear-gradient(135deg, var(--surface), var(--surface) 9px, var(--surface-subtle) 9px, var(--surface-subtle) 18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ladder-gap-note {
|
.ladder-gap-note {
|
||||||
@@ -353,6 +353,12 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -384,13 +390,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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: 12px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ladder-sort-segment button.active {
|
.ladder-sort-segment button.active {
|
||||||
@@ -448,11 +456,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%, #fff), #fff);
|
background: linear-gradient(90deg, color-mix(in srgb, var(--tier-color) 8%, var(--surface)), var(--surface));
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-tier.is-gap .market-ladder-label {
|
.market-ladder-tier.is-gap .market-ladder-label {
|
||||||
background: repeating-linear-gradient(45deg, rgb(250, 250, 250), rgb(250, 250, 250) 8px, rgb(243, 244, 246) 8px, rgb(243, 244, 246) 16px);
|
background: repeating-linear-gradient(45deg, var(--surface-subtle), var(--surface-subtle) 8px, var(--surface-muted) 8px, var(--surface-muted) 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-level {
|
.market-ladder-level {
|
||||||
@@ -571,9 +579,9 @@
|
|||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
font-size: 13px;
|
font-size: var(--font-size-table);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-stock-first .stock-code {
|
.market-ladder-stock-first .stock-code {
|
||||||
@@ -613,7 +621,7 @@
|
|||||||
.market-ladder-tag.one-price {
|
.market-ladder-tag.one-price {
|
||||||
background: var(--r2-up-soft);
|
background: var(--r2-up-soft);
|
||||||
|
|
||||||
color: rgb(194, 46, 46);
|
color: var(--r2-up);
|
||||||
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -762,9 +770,9 @@
|
|||||||
.market-ladder-insight-card > header h3 {
|
.market-ladder-insight-card > header h3 {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
font-size: 13.5px;
|
font-size: var(--font-size-card-title);
|
||||||
|
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-insight-card > header span {
|
.market-ladder-insight-card > header span {
|
||||||
@@ -796,9 +804,9 @@
|
|||||||
.market-ladder-apex strong {
|
.market-ladder-apex strong {
|
||||||
color: var(--r2-up);
|
color: var(--r2-up);
|
||||||
|
|
||||||
font-size: 26px;
|
font-size: var(--font-size-metric);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-apex em {
|
.market-ladder-apex em {
|
||||||
@@ -900,7 +908,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-pyramid-row.is-gap > i {
|
.market-ladder-pyramid-row.is-gap > i {
|
||||||
background: repeating-linear-gradient(45deg, rgb(229, 231, 235), rgb(229, 231, 235) 4px, rgb(243, 244, 246) 4px, rgb(243, 244, 246) 8px);
|
background: repeating-linear-gradient(45deg, var(--border), var(--border) 4px, var(--surface-muted) 4px, var(--surface-muted) 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-pyramid-row.is-gap > i b {
|
.market-ladder-pyramid-row.is-gap > i b {
|
||||||
@@ -968,11 +976,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-rate-list > div > i b.is-low {
|
.market-ladder-rate-list > div > i b.is-low {
|
||||||
background: rgb(245, 158, 11);
|
background: var(--r2-amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-rate-list > div > i b.is-zero {
|
.market-ladder-rate-list > div > i b.is-zero {
|
||||||
background: rgb(209, 213, 219);
|
background: var(--border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-ladder-rate-list > div > strong {
|
.market-ladder-rate-list > div > strong {
|
||||||
|
|||||||
@@ -51,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: "#2563eb", 2: "#16a34a", 3: "#d97706", 4: "#e04536" }[level] || "#9ca3af";
|
const color = { 1: "#3370ff", 2: "#16a34a", 3: "#b45309", 4: "#e04536" }[level] || "#8f959e";
|
||||||
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>
|
||||||
|
|||||||
+6
-5
@@ -790,7 +790,8 @@
|
|||||||
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);
|
||||||
@@ -803,17 +804,17 @@
|
|||||||
|
|
||||||
border: 1px solid var(--dialog-line-strong);
|
border: 1px solid var(--dialog-line-strong);
|
||||||
|
|
||||||
border-radius: 10px;
|
border-radius: var(--size-radius-dialog);
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: var(--dialog-ink);
|
color: var(--dialog-ink);
|
||||||
|
|
||||||
box-shadow: rgba(25, 36, 48, 0.2) 0px 26px 72px, rgba(25, 36, 48, 0.08) 0px 4px 14px;
|
box-shadow: var(--shadow-float);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog))::backdrop {
|
:is(.global-search-dialog, .stock-dialog, .settings-dialog:not(.heaven-reading-dialog))::backdrop {
|
||||||
background: rgba(28, 39, 50, 0.46);
|
background: var(--backdrop);
|
||||||
|
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
@@ -1133,7 +1134,7 @@
|
|||||||
|
|
||||||
margin: 8px auto;
|
margin: 8px auto;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: var(--size-radius-dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) .dialog-header {
|
:is(.stock-dialog, .settings-dialog:not(.heaven-reading-dialog)) .dialog-header {
|
||||||
|
|||||||
+764
-355
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,34 @@
|
|||||||
<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>
|
||||||
<header class="section-toolbar lad-head mentor-page-header">
|
<div id="mentorNotice" class="inline-notice" hidden></div>
|
||||||
<div class="section-title-group mentor-page-title">
|
<header class="mentor-page-header" aria-label="问师">
|
||||||
|
<div class="mentor-page-title">
|
||||||
<h2>问师</h2>
|
<h2>问师</h2>
|
||||||
<span class="section-subtitle">与不同交易思维模型持续对话 · <span id="mentorDataDate">--</span></span>
|
<p id="mentorPageSubtitle">与不同交易思维模型持续对话 · 数据日期 --</p>
|
||||||
</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-evidence-filters" role="group" aria-label="按素材等级筛选">
|
<div class="mentor-filter-menu">
|
||||||
<button class="active" type="button" data-mentor-grade="all">全部</button>
|
<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 type="button" data-mentor-grade="A">A级</button>
|
<div id="mentorFilterOptions" class="mentor-filter-options" role="menu" hidden>
|
||||||
<button type="button" data-mentor-grade="B">B级</button>
|
<button type="button" class="active" data-mentor-grade="all" role="menuitem">全部<span id="mentorCount">0 位</span></button>
|
||||||
<button type="button" data-mentor-grade="C">C级</button>
|
<button type="button" data-mentor-grade="A" role="menuitem">A级</button>
|
||||||
|
<button type="button" data-mentor-grade="B" role="menuitem">B级</button>
|
||||||
|
<button type="button" data-mentor-grade="C" role="menuitem">C级</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p>
|
<button id="mentorSortToggle" class="mentor-sort-toggle" type="button" aria-pressed="false" title="整理联系人顺序"><i data-lucide="list-ordered"></i><span>整理</span></button>
|
||||||
</div>
|
</div>
|
||||||
<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 class="mentor-evidence-legend">等级仅表示公开素材完整度</p>
|
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="mentor-chat-panel" aria-label="问师对话">
|
<section class="mentor-chat-panel" aria-label="问师对话">
|
||||||
@@ -37,11 +36,16 @@
|
|||||||
<div class="mentor-chat-identity">
|
<div class="mentor-chat-identity">
|
||||||
<span id="activeMentorAvatar" class="mentor-avatar" aria-hidden="true">师</span>
|
<span id="activeMentorAvatar" class="mentor-avatar" aria-hidden="true">师</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3><span id="activeMentorBadges" class="mentor-active-badges"></span></div>
|
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3></div>
|
||||||
<p id="activeMentorStatus">思维模型已就绪</p>
|
<p id="activeMentorStatus">思维模型已就绪</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="clearMentorChatButton" class="icon-button mentor-clear-button" type="button" disabled aria-label="清空当前对话" title="清空对话"><i data-lucide="trash-2"></i></button>
|
<div class="mentor-chat-actions">
|
||||||
|
<button id="mentorPinButton" class="icon-button mentor-header-action" type="button" aria-label="置顶当前思维模型" title="置顶"><i data-lucide="pin"></i></button>
|
||||||
|
<button id="mentorNoteButton" class="icon-button mentor-header-action" type="button" aria-label="备注标签" title="备注标签"><i data-lucide="tag"></i></button>
|
||||||
|
<button id="mentorProfileButton" class="icon-button mentor-header-action" type="button" aria-label="游资档案" title="游资档案"><i data-lucide="id-card"></i></button>
|
||||||
|
<button id="clearMentorChatButton" class="icon-button mentor-clear-button" type="button" disabled aria-label="清空当前对话" title="清空对话"><i data-lucide="trash-2"></i></button>
|
||||||
|
</div>
|
||||||
</header>
|
</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">
|
||||||
@@ -52,38 +56,61 @@
|
|||||||
<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">
|
||||||
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
|
<div class="mentor-composer-main">
|
||||||
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
|
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
|
||||||
|
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
|
||||||
|
<span class="mentor-composer-hint">Enter 发送 · Shift + Enter 换行</span>
|
||||||
|
</div>
|
||||||
<div class="mentor-composer-actions">
|
<div class="mentor-composer-actions">
|
||||||
<span>Enter 发送 · Shift + Enter 换行</span>
|
|
||||||
<button id="stopMentorQuestion" class="button ghost mentor-stop-button" type="button" hidden><i data-lucide="square"></i><span>停止</span></button>
|
<button id="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>
|
||||||
|
|
||||||
<aside class="mentor-profile-panel" aria-label="当前思维模型资料">
|
</div>
|
||||||
|
<footer class="mentor-workspace-footer">
|
||||||
|
<span class="mentor-evidence-legend">等级仅表示公开素材完整度</span>
|
||||||
|
<p class="mentor-disclaimer">基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<dialog id="mentorNoteDialog" class="mentor-floating-dialog mentor-note-dialog" aria-labelledby="mentorNoteDialogTitle">
|
||||||
|
<div class="mentor-dialog-head">
|
||||||
|
<div><span class="dialog-eyebrow">问师</span><h3 id="mentorNoteDialogTitle">备注标签</h3></div>
|
||||||
|
<button type="button" class="icon-button mentor-dialog-close" data-mentor-dialog-close="mentorNoteDialog" aria-label="关闭" title="关闭"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="mentor-dialog-body">
|
||||||
|
<label class="visually-hidden" for="mentorNoteInput">备注标签</label>
|
||||||
|
<textarea id="mentorNoteInput" maxlength="2000" placeholder="为该思维模型记录备注或标签"></textarea>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="mentorProfileDialog" class="mentor-floating-dialog mentor-profile-dialog" aria-labelledby="mentorProfileDialogTitle">
|
||||||
|
<div class="mentor-dialog-head">
|
||||||
|
<div><span class="dialog-eyebrow">问师</span><h3 id="mentorProfileDialogTitle">游资档案</h3></div>
|
||||||
|
<button type="button" class="icon-button mentor-dialog-close" data-mentor-dialog-close="mentorProfileDialog" aria-label="关闭" title="关闭"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="mentor-profile-dialog-body">
|
||||||
<div class="mentor-profile-hero">
|
<div class="mentor-profile-hero">
|
||||||
<span id="mentorProfileAvatar" class="mentor-profile-avatar" aria-hidden="true">师</span>
|
<span id="mentorProfileDialogAvatar" class="mentor-profile-avatar" aria-hidden="true">师</span>
|
||||||
<h3 id="mentorProfileName">--</h3>
|
<h3 id="mentorProfileDialogName">--</h3>
|
||||||
<p id="mentorProfileTagline">--</p>
|
<p id="mentorProfileDialogTagline">--</p>
|
||||||
<div id="mentorProfileBadges" class="mentor-profile-badges"></div>
|
<div id="mentorProfileDialogBadges" class="mentor-profile-badges"></div>
|
||||||
</div>
|
</div>
|
||||||
<section class="mentor-profile-section">
|
<section class="mentor-profile-section">
|
||||||
<h4>关注维度</h4>
|
<h4>关注维度</h4>
|
||||||
<div id="activeMentorFocus" class="mentor-active-focus"></div>
|
<div id="mentorProfileDialogFocus" class="mentor-active-focus"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="mentor-profile-section">
|
<section class="mentor-profile-section">
|
||||||
<h4>资料依据</h4>
|
<h4>资料依据</h4>
|
||||||
<strong id="mentorProfileSource">--</strong>
|
<strong id="mentorProfileDialogSource">--</strong>
|
||||||
<p id="activeMentorEvidence">--</p>
|
<p id="mentorProfileDialogEvidence">--</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="mentorProfileDataDate">--</span></p>
|
<p><i data-lucide="calendar-days"></i><span id="mentorProfileDialogDataDate">--</span></p>
|
||||||
<p><i data-lucide="database"></i><span>仅使用网页已提供的市场数据</span></p>
|
<p><i data-lucide="database"></i><span>仅使用网页已提供的市场数据</span></p>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</div>
|
||||||
</div>
|
</dialog>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -31,27 +31,100 @@ async function loadMentorSetup(force = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MENTOR_AVATAR_TONES = {
|
||||||
|
"xiaobai-perspective": "violet",
|
||||||
|
"kobe92-perspective": "blue",
|
||||||
|
"beijingchaojia-perspective": "green",
|
||||||
|
"chaojiyangjia-perspective": "orange",
|
||||||
|
"chenxiaoqun-perspective": "red",
|
||||||
|
"longfeihu-perspective": "teal",
|
||||||
|
"chuangshiji-perspective": "purple",
|
||||||
|
"foshanwuyingjiao-perspective": "yellow",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MENTOR_AVATAR_TONE_CLASSES = [
|
||||||
|
"mentor-avatar-tone-violet", "mentor-avatar-tone-blue", "mentor-avatar-tone-green",
|
||||||
|
"mentor-avatar-tone-orange", "mentor-avatar-tone-red", "mentor-avatar-tone-teal",
|
||||||
|
"mentor-avatar-tone-purple", "mentor-avatar-tone-yellow",
|
||||||
|
];
|
||||||
|
|
||||||
|
function mentorAvatarTone(mentor) {
|
||||||
|
return MENTOR_AVATAR_TONES[String(mentor?.id || "")] || "blue";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mentorAvatarToneClass(mentor) {
|
||||||
|
return `mentor-avatar-tone-${mentorAvatarTone(mentor)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMentorAvatarTone(element, toneClass) {
|
||||||
|
if (!(element instanceof Element)) return;
|
||||||
|
element.classList.remove(...MENTOR_AVATAR_TONE_CLASSES);
|
||||||
|
if (toneClass) element.classList.add(toneClass);
|
||||||
|
}
|
||||||
|
|
||||||
function renderMentorWorkspace() {
|
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("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`);
|
setText("mentorPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`);
|
||||||
|
setText("currentPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`);
|
||||||
setText("activeMentorName", selected?.name || "--");
|
setText("activeMentorName", selected?.name || "--");
|
||||||
setText("mentorProfileName", selected?.name || "--");
|
|
||||||
setText("mentorProfileTagline", selected?.tagline || selected?.description || "--");
|
|
||||||
setText("mentorProfileSource", selected?.evidence?.label || "公开资料整理");
|
|
||||||
setText("mentorProfileDataDate", `行情数据 ${displayCompactDate(setup.trade_date)}`);
|
|
||||||
setText("activeMentorAvatar", mentorAvatarText(selected));
|
setText("activeMentorAvatar", mentorAvatarText(selected));
|
||||||
setText("mentorProfileAvatar", mentorAvatarText(selected));
|
applyMentorAvatarTone(document.querySelector("#activeMentorAvatar"), mentorAvatarToneClass(selected));
|
||||||
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
const pinButton = document.querySelector("#mentorPinButton");
|
||||||
document.querySelector("#mentorProfileBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
if (pinButton) {
|
||||||
setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
|
pinButton.classList.toggle("active", Boolean(selected?.pinned));
|
||||||
document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
|
pinButton.setAttribute("aria-label", selected?.pinned ? "取消置顶当前思维模型" : "置顶当前思维模型");
|
||||||
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
|
pinButton.setAttribute("aria-pressed", String(Boolean(selected?.pinned)));
|
||||||
|
}
|
||||||
|
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;
|
||||||
@@ -76,9 +149,15 @@ 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) => {
|
||||||
@@ -88,7 +167,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" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span>
|
<span class="mentor-avatar ${mentorAvatarToneClass(mentor)}" data-grade="${escapeHtml(String(mentor.evidence?.grade || "").toLowerCase())}" aria-hidden="true">${escapeHtml(mentorAvatarText(mentor))}</span>
|
||||||
<span class="mentor-option-copy">
|
<span class="mentor-option-copy">
|
||||||
<span class="mentor-option-heading">
|
<span class="mentor-option-heading">
|
||||||
<strong>${escapeHtml(mentor.name)}</strong>
|
<strong>${escapeHtml(mentor.name)}</strong>
|
||||||
@@ -98,16 +177,12 @@ function renderMentorDirectory() {
|
|||||||
<span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span>
|
<span 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-pin-button ${mentor.pinned ? "active" : ""}" data-mentor-pin="${escapeHtml(mentor.id)}"
|
<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>
|
||||||
aria-label="${mentor.pinned ? "取消置顶" : "置顶"}${escapeHtml(mentor.name)}" title="${mentor.pinned ? "取消置顶" : "置顶"}" ${state.mentorSavingPreferences ? "disabled" : ""}>
|
<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>
|
||||||
<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("");
|
||||||
@@ -115,9 +190,6 @@ 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));
|
||||||
});
|
});
|
||||||
@@ -143,6 +215,36 @@ 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 || [];
|
||||||
@@ -246,14 +348,17 @@ async function persistMentorPreferences() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMentorBadges(mentor, expanded = false) {
|
function renderMentorBadges(mentor) {
|
||||||
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("");
|
||||||
}
|
}
|
||||||
@@ -262,6 +367,14 @@ 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;
|
||||||
@@ -287,27 +400,28 @@ 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" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
|
<span class="mentor-message-avatar ${message.role === "user" ? "mentor-avatar-tone-blue" : escapeHtml(mentorAvatarToneClass(selected))}" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
|
||||||
<div class="mentor-message-body">
|
<div class="mentor-message-body">
|
||||||
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
|
<div class="mentor-message-label">${message.role === "user"
|
||||||
|
? escapeHtml(mentorMessageTime(message))
|
||||||
|
: `${escapeHtml(selected?.name || "问师")} · ${escapeHtml(mentorMessageTime(message))}`}</div>
|
||||||
<div class="mentor-message-content">${message.role === "assistant"
|
<div class="mentor-message-content">${message.role === "assistant"
|
||||||
? (message.content ? formatMentorAnswer(message.content) : '<p class="mentor-loading-copy">正在读取复盘数据并推演...</p>')
|
? (message.content ? formatMentorAnswer(message.content) : '<p class="mentor-loading-copy">正在读取复盘数据并推演...</p>')
|
||||||
: escapeHtml(message.content)}</div>
|
: 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 = state.mentorMessages.length > 0 || state.mentorLoading;
|
document.querySelector("#mentorQuickPrompts").hidden = false;
|
||||||
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 ? "正在生成回答..." : "思维模型已就绪");
|
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : (selected?.tagline || selected?.description || "思维模型已就绪"));
|
||||||
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));
|
||||||
});
|
});
|
||||||
@@ -322,7 +436,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)}"><i data-lucide="corner-down-right"></i><span>${escapeHtml(item)}</span></button>`).join("")}
|
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><span>${escapeHtml(item)}</span></button>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -342,6 +456,7 @@ 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();
|
||||||
@@ -362,7 +477,6 @@ 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)
|
||||||
: [];
|
: [];
|
||||||
@@ -431,6 +545,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,19 +644,24 @@ 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", () => {
|
||||||
state.mentorGrade = button.dataset.mentorGrade || "all";
|
selectMentorGrade(button.dataset.mentorGrade);
|
||||||
document.querySelectorAll("[data-mentor-grade]").forEach((item) => {
|
closeMentorFilterMenu();
|
||||||
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));
|
||||||
});
|
});
|
||||||
@@ -550,4 +670,70 @@ 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-26
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
text-align: right;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: right;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: right;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: right;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redesigned-yesterday-view .data-table .number {
|
.redesigned-yesterday-view .data-table .number {
|
||||||
@@ -716,7 +716,6 @@
|
|||||||
|
|
||||||
#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;
|
||||||
@@ -726,9 +725,16 @@
|
|||||||
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;
|
||||||
@@ -736,6 +742,12 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -892,24 +904,6 @@
|
|||||||
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);
|
||||||
|
|
||||||
@@ -2225,6 +2219,70 @@
|
|||||||
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,
|
||||||
@@ -2235,9 +2293,9 @@
|
|||||||
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .row-number {
|
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .row-number {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
|
|
||||||
padding-left: 8px;
|
padding-left: 12px;
|
||||||
|
|
||||||
text-align: center;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .reason-column {
|
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .reason-column {
|
||||||
|
|||||||
@@ -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 num" aria-label="序号">序号</th>
|
<th class="row-number" 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 num">序号</th>
|
<th class="row-number">序号</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 num">序号</th>
|
<th class="row-number">序号</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 num">序号</th>
|
<th class="row-number">序号</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>
|
||||||
|
|||||||
@@ -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 num muted">${index + 1}</td>
|
<td class="row-number muted">${index + 1}</td>
|
||||||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
||||||
<td class="number num"><span class="pool-streak-tag tag red">${streakLabel(row.streak)}</span></td>
|
<td class="number num"><span class="streak-pill${number(row.streak) >= 4 ? " high" : ""}">${streakLabel(row.streak)}</span></td>
|
||||||
<td class="number num up">${signed(row.change)}</td>
|
<td class="number num 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 num muted">${index + 1}</td>
|
<td class="row-number muted">${index + 1}</td>
|
||||||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
<td><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 num muted">${index + 1}</td>
|
<td class="row-number muted">${index + 1}</td>
|
||||||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
<td><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 num muted">${index + 1}</td>
|
<td class="row-number muted">${index + 1}</td>
|
||||||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
<td><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>
|
||||||
|
|||||||
+46
-41
@@ -22,11 +22,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-source-tag.dual {
|
.popularity-source-tag.dual {
|
||||||
border-color: rgb(230, 199, 115);
|
border-color: var(--warn);
|
||||||
|
|
||||||
background: var(--amber-soft);
|
background: var(--amber-soft);
|
||||||
|
|
||||||
color: rgb(118, 83, 20);
|
color: var(--warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-concepts {
|
.popularity-concepts {
|
||||||
@@ -44,21 +44,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#popularityView .data-table {
|
#popularityView .data-table {
|
||||||
font-size: 12px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#popularityView .data-table thead th {
|
#popularityView .data-table thead th {
|
||||||
height: 32px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 6px 9px;
|
padding: 0 12px;
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
#popularityView .data-table tbody td {
|
#popularityView .data-table tbody td {
|
||||||
height: 39px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 5px 9px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redesigned-popularity-view {
|
.redesigned-popularity-view {
|
||||||
@@ -93,6 +93,10 @@
|
|||||||
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 {
|
||||||
@@ -140,7 +144,7 @@
|
|||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|
||||||
@@ -152,19 +156,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-source-tabs-v2 button {
|
.popularity-source-tabs-v2 button {
|
||||||
min-height: 27px;
|
min-height: 26px;
|
||||||
|
|
||||||
padding: 0px 13px;
|
padding: 0px 13px;
|
||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -190,11 +194,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-refresh-v2 {
|
.popularity-refresh-v2 {
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
padding: 0px 11px;
|
height: 32px;
|
||||||
|
|
||||||
border-radius: 7px;
|
padding: 0px 12px;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-refresh-v2 .lucide {
|
.popularity-refresh-v2 .lucide {
|
||||||
@@ -244,15 +252,15 @@
|
|||||||
|
|
||||||
width: 3px;
|
width: 3px;
|
||||||
|
|
||||||
background: rgb(199, 216, 251);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-glance-v2 article:nth-child(2)::before {
|
.popularity-glance-v2 article:nth-child(2)::before {
|
||||||
background: rgb(183, 221, 207);
|
background: var(--down-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-glance-v2 article.consensus::before {
|
.popularity-glance-v2 article.consensus::before {
|
||||||
background: rgb(230, 199, 115);
|
background: var(--warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-glance-v2 article > span {
|
.popularity-glance-v2 article > span {
|
||||||
@@ -372,7 +380,7 @@
|
|||||||
|
|
||||||
width: 230px;
|
width: 230px;
|
||||||
|
|
||||||
height: 33px;
|
height: 32px;
|
||||||
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
@@ -380,9 +388,9 @@
|
|||||||
|
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
|
|
||||||
border: 1px solid rgb(216, 221, 229);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
@@ -390,9 +398,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-search-v2:focus-within {
|
.popularity-search-v2:focus-within {
|
||||||
border-color: rgb(150, 181, 242);
|
border-color: var(--accent);
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0.09) 0px 0px 0px 3px;
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-search-v2 .lucide {
|
.popularity-search-v2 .lucide {
|
||||||
@@ -446,15 +454,15 @@
|
|||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
height: 35px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 7px 10px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom-color: var(--r2-line);
|
border-bottom-color: var(--r2-line);
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-table-v2 thead th:nth-child(1) {
|
.popularity-table-v2 thead th:nth-child(1) {
|
||||||
@@ -477,11 +485,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-table-v2 tbody td {
|
.popularity-table-v2 tbody td {
|
||||||
height: 43px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 7px 10px;
|
padding: 0 12px;
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-table-v2 tbody tr {
|
.popularity-table-v2 tbody tr {
|
||||||
@@ -541,9 +549,9 @@
|
|||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-table);
|
||||||
|
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
@@ -559,7 +567,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-list-rank-v2 {
|
.popularity-list-rank-v2 {
|
||||||
color: rgb(64, 85, 115);
|
color: var(--text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popularity-movement-v2 {
|
.popularity-movement-v2 {
|
||||||
@@ -611,10 +619,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -658,11 +662,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popularity-table-v2 tbody td {
|
.popularity-table-v2 tbody td {
|
||||||
height: 39px;
|
height: 40px;
|
||||||
|
|
||||||
padding-top: 5px;
|
padding: 0 12px;
|
||||||
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,8 +859,11 @@
|
|||||||
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) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function renderPopularityTable() {
|
|||||||
setText("popularityTableTitle", `${sourceName}榜`);
|
setText("popularityTableTitle", `${sourceName}榜`);
|
||||||
setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜");
|
setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜");
|
||||||
const headers = [
|
const headers = [
|
||||||
["排名", "number num"], ["股票", ""], ["最新价(元)", "number num"], ["涨跌幅(%)", "number num"],
|
["排名", "row-number"], ["股票", ""], ["最新价(元)", "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="number num popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td>
|
<td class="row-number popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td>
|
||||||
<td><div class="popularity-stock-v2"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><span class="stock-code scode">${escapeHtml(row.code)}</span></div></td>
|
<td><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>
|
||||||
|
|||||||
+92
-91
@@ -217,10 +217,6 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trade-journal-section .trade-log-summary {
|
.trade-journal-section .trade-log-summary {
|
||||||
min-height: 58px;
|
min-height: 58px;
|
||||||
}
|
}
|
||||||
@@ -242,7 +238,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
box-shadow: var(--shadow-xs);
|
box-shadow: var(--shadow-card);
|
||||||
|
|
||||||
min-height: 0px !important;
|
min-height: 0px !important;
|
||||||
}
|
}
|
||||||
@@ -276,15 +272,15 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-section .data-table thead th {
|
.watchlist-section .data-table thead th {
|
||||||
height: 32px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 6px 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-section .data-table tbody td {
|
.watchlist-section .data-table tbody td {
|
||||||
height: 44px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 7px 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trade-journal-section .trade-log-summary:empty {
|
.trade-journal-section .trade-log-summary:empty {
|
||||||
@@ -384,7 +380,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-history-toggle {
|
#reviewWorkspaceView .review-history-toggle {
|
||||||
min-height: 30px;
|
min-height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -398,13 +394,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
border: 1px solid var(--review-line);
|
border: 1px solid var(--review-line);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: rgb(55, 65, 81);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -469,7 +465,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
box-shadow: rgba(16, 24, 40, 0.05) 0px 1px 2px;
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-card-heading > div {
|
#reviewWorkspaceView .review-card-heading > div {
|
||||||
@@ -515,9 +511,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .data-table thead th {
|
#reviewWorkspaceView .data-table thead th {
|
||||||
height: 32px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 6px 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--review-line-soft);
|
border-bottom: 1px solid var(--review-line-soft);
|
||||||
|
|
||||||
@@ -525,21 +521,21 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
color: var(--review-faint);
|
color: var(--review-faint);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .data-table tbody td {
|
#reviewWorkspaceView .data-table tbody td {
|
||||||
height: 48px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 7px 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--review-line-soft);
|
border-bottom: 1px solid var(--review-line-soft);
|
||||||
|
|
||||||
color: rgb(55, 65, 81);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .data-table tbody tr:last-child td {
|
#reviewWorkspaceView .data-table tbody tr:last-child td {
|
||||||
@@ -559,17 +555,17 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
#reviewWorkspaceView .stock-cell strong {
|
#reviewWorkspaceView .stock-cell strong {
|
||||||
color: var(--review-ink);
|
color: var(--review-ink);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .stock-cell small {
|
#reviewWorkspaceView .stock-cell small {
|
||||||
color: var(--review-faint);
|
color: var(--review-faint);
|
||||||
|
|
||||||
font-size: 10px;
|
font-size: var(--font-size-aux);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-watch-mark {
|
#reviewWorkspaceView .review-watch-mark {
|
||||||
color: rgb(209, 213, 219);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
@@ -577,7 +573,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-watch-mark.red {
|
#reviewWorkspaceView .review-watch-mark.red {
|
||||||
color: rgb(224, 69, 54);
|
color: var(--market-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-row-actions {
|
#reviewWorkspaceView .review-row-actions {
|
||||||
@@ -601,19 +597,19 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .table-action {
|
#reviewWorkspaceView .table-action {
|
||||||
min-height: 25px;
|
min-height: 32px;
|
||||||
|
|
||||||
padding: 0px 6px;
|
padding: 0px 6px;
|
||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
color: var(--review-blue);
|
color: var(--review-blue);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .table-action:hover {
|
#reviewWorkspaceView .table-action:hover {
|
||||||
@@ -621,13 +617,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .table-action.down {
|
#reviewWorkspaceView .table-action.down {
|
||||||
color: rgb(139, 146, 158);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .table-action.down:hover {
|
#reviewWorkspaceView .table-action.down:hover {
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
|
|
||||||
color: rgb(209, 67, 67);
|
color: var(--market-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-log-table-frame .empty-state {
|
#reviewWorkspaceView .trade-log-table-frame .empty-state {
|
||||||
@@ -667,7 +663,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-log-heading .button {
|
#reviewWorkspaceView .trade-log-heading .button {
|
||||||
min-height: 29px;
|
min-height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -679,13 +675,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--review-blue);
|
background: var(--review-blue);
|
||||||
|
|
||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-log-heading .button:hover {
|
#reviewWorkspaceView .trade-log-heading .button:hover {
|
||||||
@@ -831,35 +827,35 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-action-add {
|
#reviewWorkspaceView .trade-action-add {
|
||||||
border-color: rgb(241, 196, 192);
|
border-color: var(--market-up);
|
||||||
|
|
||||||
background: rgb(253, 236, 234);
|
background: var(--market-up-soft);
|
||||||
|
|
||||||
color: rgb(214, 62, 50);
|
color: var(--market-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-action-buy {
|
#reviewWorkspaceView .trade-action-buy {
|
||||||
border-color: rgb(241, 196, 192);
|
border-color: var(--market-up);
|
||||||
|
|
||||||
background: rgb(253, 236, 234);
|
background: var(--market-up-soft);
|
||||||
|
|
||||||
color: rgb(214, 62, 50);
|
color: var(--market-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-action-sell {
|
#reviewWorkspaceView .trade-action-sell {
|
||||||
border-color: rgb(188, 225, 202);
|
border-color: var(--market-down);
|
||||||
|
|
||||||
background: rgb(234, 247, 239);
|
background: var(--market-down-soft);
|
||||||
|
|
||||||
color: rgb(22, 139, 67);
|
color: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-action-trim {
|
#reviewWorkspaceView .trade-action-trim {
|
||||||
border-color: rgb(188, 225, 202);
|
border-color: var(--market-down);
|
||||||
|
|
||||||
background: rgb(234, 247, 239);
|
background: var(--market-down-soft);
|
||||||
|
|
||||||
color: rgb(22, 139, 67);
|
color: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-tags {
|
#reviewWorkspaceView .trade-tags {
|
||||||
@@ -879,7 +875,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
background: var(--review-blue-soft);
|
background: var(--review-blue-soft);
|
||||||
|
|
||||||
color: rgb(82, 112, 167);
|
color: var(--action);
|
||||||
|
|
||||||
font-size: 8.5px;
|
font-size: 8.5px;
|
||||||
}
|
}
|
||||||
@@ -899,7 +895,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
color: rgb(75, 85, 99);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
|
|
||||||
@@ -989,7 +985,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .journal-form .button {
|
#reviewWorkspaceView .journal-form .button {
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -1001,13 +997,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
padding: 0px 14px;
|
padding: 0px 14px;
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--review-blue);
|
background: var(--review-blue);
|
||||||
|
|
||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .journal-form .button:hover {
|
#reviewWorkspaceView .journal-form .button:hover {
|
||||||
@@ -1247,9 +1243,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
color: var(--review-ink);
|
color: var(--review-ink);
|
||||||
|
|
||||||
font-size: 19px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 750;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-page-title .section-subtitle {
|
#reviewWorkspaceView .review-page-title .section-subtitle {
|
||||||
@@ -1261,7 +1257,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-card-heading {
|
#reviewWorkspaceView .review-card-heading {
|
||||||
@@ -1285,13 +1281,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
color: var(--review-ink);
|
color: var(--review-ink);
|
||||||
|
|
||||||
font-size: 14.5px;
|
font-size: var(--font-size-card-title);
|
||||||
|
|
||||||
font-weight: 720;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-add-watch {
|
#reviewWorkspaceView .review-add-watch {
|
||||||
min-height: 29px;
|
min-height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -1303,23 +1299,23 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
|
|
||||||
border: 1px solid rgb(185, 205, 248);
|
border: 1px solid var(--color-action-line);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: rgb(245, 248, 255);
|
background: var(--action-soft);
|
||||||
|
|
||||||
color: var(--review-blue);
|
color: var(--review-blue);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
font-weight: 650;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-add-watch:hover {
|
#reviewWorkspaceView .review-add-watch:hover {
|
||||||
border-color: rgb(142, 176, 244);
|
border-color: var(--action);
|
||||||
|
|
||||||
background: rgb(234, 241, 255);
|
background: var(--selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-add-watch .lucide {
|
#reviewWorkspaceView .review-add-watch .lucide {
|
||||||
@@ -1374,17 +1370,17 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-watchlist-table .stock-cell strong {
|
#reviewWorkspaceView .review-watchlist-table .stock-cell strong {
|
||||||
font-size: 12.5px;
|
font-size: var(--font-size-table);
|
||||||
|
|
||||||
font-weight: 680;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .review-watchlist-table .stock-cell small {
|
#reviewWorkspaceView .review-watchlist-table .stock-cell small {
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-aux);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .watch-attention-score {
|
#reviewWorkspaceView .watch-attention-score {
|
||||||
color: rgb(63, 75, 94);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
@@ -1452,11 +1448,11 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
#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: rgba(37, 99, 235, 0.08) 0px 0px 0px 2px;
|
box-shadow: 0 0 0 2px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .journal-summary-field input::placeholder {
|
#reviewWorkspaceView .journal-summary-field input::placeholder {
|
||||||
color: rgb(166, 173, 183);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .journal-form textarea {
|
#reviewWorkspaceView .journal-form textarea {
|
||||||
@@ -1543,7 +1539,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
.watchlist-editor-form .form-field > label,
|
.watchlist-editor-form .form-field > label,
|
||||||
.watchlist-editor-form .form-field > span {
|
.watchlist-editor-form .form-field > span {
|
||||||
color: rgb(55, 65, 81);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
@@ -1561,7 +1557,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
padding: 0px 11px;
|
padding: 0px 11px;
|
||||||
|
|
||||||
border: 1px solid rgb(223, 227, 232);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
@@ -1569,9 +1565,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-search-control:focus-within {
|
.watchlist-search-control:focus-within {
|
||||||
border-color: rgb(184, 202, 244);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0.07) 0px 0px 0px 3px;
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-search-control .lucide {
|
.watchlist-search-control .lucide {
|
||||||
@@ -1579,7 +1575,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
|
||||||
color: rgb(154, 163, 176);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-search-control input {
|
.watchlist-search-control input {
|
||||||
@@ -1591,7 +1587,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
outline: 0px;
|
outline: 0px;
|
||||||
|
|
||||||
color: rgb(31, 41, 55);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-style: inherit;
|
font-style: inherit;
|
||||||
|
|
||||||
@@ -1645,13 +1641,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
border-style: none none solid;
|
border-style: none none solid;
|
||||||
|
|
||||||
border-color: currentcolor currentcolor rgb(238, 240, 243);
|
border-color: currentcolor currentcolor var(--border);
|
||||||
|
|
||||||
border-image: none;
|
border-image: none;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: rgb(31, 41, 55);
|
color: var(--text-primary);
|
||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -1673,7 +1669,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-search-results button small {
|
.watchlist-search-results button small {
|
||||||
color: rgb(139, 148, 161);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
}
|
}
|
||||||
@@ -1681,7 +1677,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
.watchlist-search-results button > b {
|
.watchlist-search-results button > b {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
color: rgb(105, 115, 134);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
||||||
@@ -1691,7 +1687,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
.watchlist-search-status {
|
.watchlist-search-status {
|
||||||
padding: 14px 10px;
|
padding: 14px 10px;
|
||||||
|
|
||||||
color: rgb(139, 148, 161);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
|
|
||||||
@@ -1709,7 +1705,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|
||||||
border: 1px solid rgb(220, 229, 247);
|
border: 1px solid var(--action-soft);
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
@@ -1733,9 +1729,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
background: rgb(232, 239, 255);
|
background: var(--action-soft);
|
||||||
|
|
||||||
color: rgb(37, 99, 235);
|
color: var(--action);
|
||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-selection-icon .lucide {
|
.watchlist-selection-icon .lucide {
|
||||||
@@ -1753,7 +1749,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-selection strong {
|
.watchlist-selection strong {
|
||||||
color: rgb(31, 41, 55);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
}
|
}
|
||||||
@@ -1763,13 +1759,13 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
color: rgb(123, 132, 145);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-selection span b {
|
.watchlist-selection span b {
|
||||||
color: rgb(82, 96, 113);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -1791,7 +1787,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
|
||||||
border: 1px solid rgb(223, 227, 232);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
@@ -1825,9 +1821,9 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.watchlist-editor-form textarea:focus {
|
.watchlist-editor-form textarea:focus {
|
||||||
border-color: rgb(184, 202, 244);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0.07) 0px 0px 0px 3px;
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
#reviewWorkspaceView .trade-log-table th:nth-child(1) {
|
#reviewWorkspaceView .trade-log-table th:nth-child(1) {
|
||||||
@@ -1873,7 +1869,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
font-weight: 650;
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
|
||||||
@@ -2201,3 +2197,8 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#reviewWorkspaceView .data-table thead th:first-child,
|
||||||
|
#reviewWorkspaceView .data-table tbody td:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|||||||
+60
-49
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rotation-day.selected-day {
|
.rotation-day.selected-day {
|
||||||
background: rgb(240, 246, 253);
|
background: var(--selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: rgb(246, 248, 250);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotation-day-sector {
|
.rotation-day-sector {
|
||||||
border-color: rgb(231, 235, 239);
|
border-color: var(--border);
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:where(#rotationView) .rotation-swatch {
|
:where(#rotationView) .rotation-swatch {
|
||||||
background: rgba(53, 106, 230, 0.12);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#rotationView) .rotation-tracker-copy {
|
:where(#rotationView) .rotation-tracker-copy {
|
||||||
@@ -157,9 +157,9 @@
|
|||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
border-bottom: 1px dashed rgba(135, 149, 173, 0.22);
|
border-bottom: 1px dashed var(--border);
|
||||||
|
|
||||||
background: rgba(53, 106, 230, calc(.04 + var(--rotation-heat) * .46));
|
background: color-mix(in srgb, var(--accent) calc(4% + var(--rotation-heat) * 46%), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.redesigned-rotation-view {
|
.redesigned-rotation-view {
|
||||||
@@ -185,9 +185,9 @@
|
|||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 17px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -260,13 +260,15 @@
|
|||||||
.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: rgba(37, 99, 235, 0.28) solid 2px;
|
outline: 2px solid var(--accent);
|
||||||
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotation-export-button {
|
.rotation-export-button {
|
||||||
min-height: 30px;
|
min-height: 32px;
|
||||||
|
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -274,17 +276,17 @@
|
|||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
padding: 5px 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border: 1px solid var(--r2-line);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: rgb(55, 65, 81);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -433,17 +435,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-swatch.strong {
|
#rotationView .rotation-swatch.strong {
|
||||||
background: rgb(112, 155, 245);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-swatch.warm {
|
#rotationView .rotation-swatch.warm {
|
||||||
background: rgb(203, 220, 255);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-swatch.mild {
|
#rotationView .rotation-swatch.mild {
|
||||||
background: rgb(240, 244, 250);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
border: 1px solid rgb(223, 230, 240);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-tracker {
|
#rotationView .rotation-tracker {
|
||||||
@@ -487,7 +489,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-tracker-copy span {
|
#rotationView .rotation-tracker-copy span {
|
||||||
color: rgb(59, 98, 196);
|
color: var(--accent-hover);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
@@ -535,7 +537,7 @@
|
|||||||
|
|
||||||
border-radius: 2px 2px 0px 0px;
|
border-radius: 2px 2px 0px 0px;
|
||||||
|
|
||||||
background: rgb(147, 180, 245);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-tracker-spark small {
|
#rotationView .rotation-tracker-spark small {
|
||||||
@@ -543,7 +545,7 @@
|
|||||||
|
|
||||||
top: -1px;
|
top: -1px;
|
||||||
|
|
||||||
color: rgb(59, 98, 196);
|
color: var(--accent-hover);
|
||||||
|
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
@@ -555,7 +557,7 @@
|
|||||||
|
|
||||||
border-style: dashed dashed none;
|
border-style: dashed dashed none;
|
||||||
|
|
||||||
border-color: rgb(185, 200, 232) rgb(185, 200, 232) currentcolor;
|
border-color: var(--action-line) var(--action-line) currentcolor;
|
||||||
|
|
||||||
border-image: none;
|
border-image: none;
|
||||||
|
|
||||||
@@ -567,7 +569,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rotation-track-cancel {
|
.rotation-track-cancel {
|
||||||
min-height: 30px;
|
min-height: 32px;
|
||||||
|
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -575,21 +579,21 @@
|
|||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
border: 1px solid var(--r2-line);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: rgb(55, 65, 81);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-label);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
padding: 4px 9px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-history {
|
#rotationView .rotation-history {
|
||||||
@@ -697,7 +701,7 @@
|
|||||||
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
background: rgb(251, 252, 254);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-sector-chip {
|
#rotationView .rotation-sector-chip {
|
||||||
@@ -817,7 +821,7 @@
|
|||||||
|
|
||||||
border-color: var(--r2-blue);
|
border-color: var(--r2-blue);
|
||||||
|
|
||||||
box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px rgba(37, 99, 235, .12);
|
box-shadow: inset 3px 0 0 var(--r2-blue), 0 0 0 1px var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotation-cell-tooltip {
|
.rotation-cell-tooltip {
|
||||||
@@ -864,9 +868,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-table thead th {
|
#rotationView .rotation-table thead th {
|
||||||
height: 37px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 8px 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--r2-line);
|
border-bottom: 1px solid var(--r2-line);
|
||||||
|
|
||||||
@@ -874,7 +878,7 @@
|
|||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
@@ -887,8 +891,13 @@
|
|||||||
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: rgb(75, 85, 99);
|
color: var(--text-2);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@@ -918,14 +927,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-table tbody td {
|
#rotationView .rotation-table tbody td {
|
||||||
height: 42px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 8px 12px;
|
padding: 0 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,7 +945,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-table tbody tr:hover td {
|
#rotationView .rotation-table tbody tr:hover td {
|
||||||
background: rgb(248, 250, 255);
|
background: var(--table-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-table .stock-name {
|
#rotationView .rotation-table .stock-name {
|
||||||
@@ -954,9 +965,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .trend-cool {
|
#rotationView .trend-cool {
|
||||||
background: rgb(232, 244, 253);
|
background: var(--accent-soft);
|
||||||
|
|
||||||
color: rgb(37, 99, 235);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .trend-new {
|
#rotationView .trend-new {
|
||||||
@@ -1040,25 +1051,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-sector-chip.heat-mild {
|
#rotationView .rotation-sector-chip.heat-mild {
|
||||||
border-color: rgb(229, 234, 241);
|
border-color: var(--border);
|
||||||
|
|
||||||
background: rgb(246, 248, 251);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-sector-chip.heat-strong {
|
#rotationView .rotation-sector-chip.heat-strong {
|
||||||
border-color: rgba(37, 99, 235, 0.24);
|
border-color: var(--action-line);
|
||||||
|
|
||||||
background: rgb(197, 215, 251);
|
background: var(--accent-soft);
|
||||||
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rotationView .rotation-sector-chip.heat-warm {
|
#rotationView .rotation-sector-chip.heat-warm {
|
||||||
border-color: rgba(37, 99, 235, 0.12);
|
border-color: var(--action-line);
|
||||||
|
|
||||||
background: rgb(231, 239, 255);
|
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
|
||||||
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@@ -1066,7 +1077,7 @@
|
|||||||
#rotationView .rotation-sector-chip:hover {
|
#rotationView .rotation-sector-chip:hover {
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
|
|
||||||
border-color: rgba(37, 99, 235, 0.34);
|
border-color: var(--accent);
|
||||||
|
|
||||||
filter: saturate(1.06);
|
filter: saturate(1.06);
|
||||||
|
|
||||||
@@ -1080,7 +1091,7 @@
|
|||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: var(--font-size-table);
|
||||||
|
|
||||||
table-layout: auto;
|
table-layout: auto;
|
||||||
|
|
||||||
|
|||||||
@@ -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="number num">序号</th><th>代码</th><th>股票</th>
|
<th class="row-number">序号</th><th>代码</th><th>股票</th>
|
||||||
<th class="number num" data-auto-sort="true" title="涨跌幅:点击排序">涨跌幅(%)</th>
|
<th class="number num" 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>
|
||||||
|
|||||||
@@ -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="number num muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
|
<tr data-code="${escapeHtml(row.code)}"><td class="row-number muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
|
||||||
<td class="number num ${row.quoted ? changeClass(row.change) : "muted"}" data-sort-value="${row.quoted ? number(row.change) : -999}">${row.quoted ? signed(row.change) : ""}</td>
|
<td class="number num ${row.quoted ? 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>
|
||||||
|
|||||||
+116
-105
@@ -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: 17px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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: rgb(100, 116, 139);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
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: rgb(17, 24, 39);
|
color: var(--text-primary);
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(232, 238, 252);
|
background: var(--action-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
: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: rgb(17, 24, 39);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.curated-strategy-card.active .curated-strategy-rank {
|
.curated-strategy-card.active .curated-strategy-rank {
|
||||||
background: rgb(220, 232, 255);
|
background: var(--action-soft);
|
||||||
|
|
||||||
color: rgb(29, 78, 216);
|
color: var(--action);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-detail-header p {
|
:where(#screenerView) .curated-detail-header p {
|
||||||
margin: 9px 0px 0px;
|
margin: 9px 0px 0px;
|
||||||
|
|
||||||
color: rgb(100, 116, 139);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.curated-strategy-badges {
|
.curated-strategy-badges {
|
||||||
@@ -87,9 +87,9 @@
|
|||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(241, 245, 249);
|
background: var(--hover);
|
||||||
|
|
||||||
color: rgb(71, 85, 105);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
|
||||||
@@ -97,9 +97,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.curated-strategy-badges span:first-child {
|
.curated-strategy-badges span:first-child {
|
||||||
background: rgb(234, 241, 255);
|
background: var(--action-soft);
|
||||||
|
|
||||||
color: rgb(29, 78, 216);
|
color: var(--action);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .mini-section-heading {
|
: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: rgb(31, 41, 55);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-section-heading > span {
|
.mini-section-heading > span {
|
||||||
color: rgb(100, 116, 139);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -141,15 +141,15 @@
|
|||||||
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid rgb(238, 240, 243);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-rule-row span {
|
:where(#screenerView) .curated-rule-row span {
|
||||||
color: rgb(71, 85, 105);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-rule-row strong {
|
:where(#screenerView) .curated-rule-row strong {
|
||||||
color: rgb(17, 24, 39);
|
color: var(--text-primary);
|
||||||
|
|
||||||
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: rgb(71, 85, 105);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
background: rgb(237, 240, 244);
|
background: var(--surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.curated-score-track i {
|
.curated-score-track i {
|
||||||
@@ -187,11 +187,11 @@
|
|||||||
|
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
|
|
||||||
background: rgb(79, 118, 199);
|
background: var(--action);
|
||||||
}
|
}
|
||||||
|
|
||||||
.curated-score-row strong {
|
.curated-score-row strong {
|
||||||
color: rgb(51, 65, 85);
|
color: var(--text-primary);
|
||||||
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
@@ -211,11 +211,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-data-status > .lucide {
|
:where(#screenerView) .curated-data-status > .lucide {
|
||||||
color: rgb(22, 163, 74);
|
color: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
.curated-data-status.missing > .lucide {
|
.curated-data-status.missing > .lucide {
|
||||||
color: rgb(217, 119, 6);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: rgb(51, 65, 85);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-data-status small {
|
:where(#screenerView) .curated-data-status small {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
color: rgb(124, 135, 152);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: 40px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quant-rule-row input:focus,
|
.quant-rule-row input:focus,
|
||||||
.quant-rule-row select:focus {
|
.quant-rule-row select:focus {
|
||||||
border-color: rgb(37, 99, 235);
|
border-color: var(--action);
|
||||||
|
|
||||||
outline: 0px;
|
outline: 0px;
|
||||||
|
|
||||||
box-shadow: rgba(37, 99, 235, 0.1) 0px 0px 0px 3px;
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .quant-remove-button {
|
:where(#screenerView) .quant-remove-button {
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
|
|
||||||
float: right;
|
float: right;
|
||||||
|
|
||||||
color: rgb(29, 78, 216);
|
color: var(--action);
|
||||||
}
|
}
|
||||||
|
|
||||||
: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: rgb(37, 99, 235);
|
background: var(--action);
|
||||||
|
|
||||||
transition: width 180ms, background 180ms;
|
transition: width 180ms, background 180ms;
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quant-summary-pane {
|
.quant-summary-pane {
|
||||||
border-top: 1px solid rgb(229, 231, 235);
|
border-top: 1px solid var(--border);
|
||||||
|
|
||||||
border-left: 0px;
|
border-left: 0px;
|
||||||
}
|
}
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
background: rgb(248, 249, 251);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .strategy-list {
|
#screenerView .strategy-list {
|
||||||
@@ -575,7 +575,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .strategy-item.active {
|
#screenerView .strategy-item.active {
|
||||||
border-color: rgb(201, 219, 241);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
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: rgb(251, 252, 253);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .backtest-panel > .workspace-heading {
|
#screenerView .backtest-panel > .workspace-heading {
|
||||||
@@ -1099,7 +1099,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tracking-status.active {
|
.tracking-status.active {
|
||||||
border-color: rgb(197, 215, 237);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
background: var(--action-soft);
|
background: var(--action-soft);
|
||||||
|
|
||||||
@@ -1107,7 +1107,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tracking-status.complete {
|
.tracking-status.complete {
|
||||||
border-color: rgb(185, 223, 207);
|
border-color: var(--market-down);
|
||||||
|
|
||||||
background: var(--market-down-soft);
|
background: var(--market-down-soft);
|
||||||
|
|
||||||
@@ -1149,9 +1149,9 @@
|
|||||||
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
background: rgb(229, 231, 235);
|
background: var(--border);
|
||||||
|
|
||||||
color: rgb(156, 163, 175);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
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: rgb(107, 114, 128);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .screener-strategy-summary p {
|
:where(#screenerView) .screener-strategy-summary p {
|
||||||
color: rgb(107, 114, 128);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -1225,7 +1225,7 @@
|
|||||||
|
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
|
|
||||||
border-bottom-color: rgb(238, 240, 243);
|
border-bottom-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-result-frame th {
|
#screenerView .screener-result-frame th {
|
||||||
@@ -1235,13 +1235,13 @@
|
|||||||
|
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
|
|
||||||
border-bottom-color: rgb(238, 240, 243);
|
border-bottom-color: var(--border);
|
||||||
|
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
|
||||||
background: var(--table-header);
|
background: var(--table-header);
|
||||||
|
|
||||||
color: rgb(107, 114, 128);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -1281,7 +1281,7 @@
|
|||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
box-shadow: rgba(15, 23, 42, 0.16) -12px 0px 32px;
|
box-shadow: var(--shadow-float);
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-drawer[open] {
|
.strategy-drawer[open] {
|
||||||
@@ -1323,7 +1323,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.strategy-drawer-header span {
|
.strategy-drawer-header span {
|
||||||
color: rgb(156, 163, 175);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
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: rgb(248, 249, 251);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .strategy-drawer .strategy-list {
|
#screenerView .strategy-drawer .strategy-list {
|
||||||
@@ -1602,7 +1602,7 @@
|
|||||||
|
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
|
|
||||||
border-top: 1px solid rgb(238, 240, 243);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-backtest-strip {
|
#screenerView .screener-backtest-strip {
|
||||||
@@ -1684,7 +1684,7 @@
|
|||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: rgb(31, 41, 55);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-style: inherit;
|
font-style: inherit;
|
||||||
|
|
||||||
@@ -1722,7 +1722,7 @@
|
|||||||
|
|
||||||
padding: 0px 9px;
|
padding: 0px 9px;
|
||||||
|
|
||||||
color: rgb(31, 41, 55);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|
||||||
@@ -1749,7 +1749,7 @@
|
|||||||
.curated-library-pane {
|
.curated-library-pane {
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
|
|
||||||
border-bottom: 1px solid rgb(229, 231, 235);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -1788,9 +1788,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.curated-search:focus-within {
|
.curated-search:focus-within {
|
||||||
border-color: rgb(142, 172, 239);
|
border-color: var(--action);
|
||||||
|
|
||||||
box-shadow: rgba(53, 106, 230, 0.1) 0px 0px 0px 3px;
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .screener-page-heading {
|
:where(#screenerView) .screener-page-heading {
|
||||||
@@ -1824,7 +1824,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-library-heading h3 {
|
:where(#screenerView) .curated-library-heading h3 {
|
||||||
color: rgb(17, 24, 39);
|
color: var(--text-primary);
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
}
|
}
|
||||||
@@ -1870,7 +1870,7 @@
|
|||||||
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
border-bottom: 1px solid rgb(232, 235, 239);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-execution-bar {
|
:where(#screenerView) .curated-execution-bar {
|
||||||
@@ -1890,7 +1890,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .regime-evidence strong {
|
#screenerView .regime-evidence strong {
|
||||||
color: rgb(146, 88, 6);
|
color: var(--warning-color);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
@@ -1902,7 +1902,7 @@
|
|||||||
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
color: rgb(162, 106, 24);
|
color: var(--warning-color);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
|
|
||||||
@@ -2119,10 +2119,6 @@ body[data-active-view="screenerView"] .workspace-view {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-active-view="screenerView"] .overview-strip {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 901px) {
|
@media (min-width: 901px) {
|
||||||
.screener-page-bar {
|
.screener-page-bar {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
@@ -2350,7 +2346,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .curated-strategy-card:hover {
|
:where(#screenerView) .curated-strategy-card:hover {
|
||||||
box-shadow: rgba(37, 99, 235, 0.08) 0px 4px 14px;
|
box-shadow: var(--shadow-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(#screenerView) .quant-screener-panel {
|
:where(#screenerView) .quant-screener-panel {
|
||||||
@@ -2414,7 +2410,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|
||||||
background: rgb(223, 229, 238);
|
background: var(--border-strong);
|
||||||
|
|
||||||
accent-color: var(--primary);
|
accent-color: var(--primary);
|
||||||
|
|
||||||
@@ -2532,19 +2528,19 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 17px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-page-heading .section-subtitle {
|
#screenerView .screener-page-heading .section-subtitle {
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-mode-tabs {
|
#screenerView .screener-mode-tabs {
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -2662,7 +2658,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .step-line.complete {
|
#screenerView .step-line.complete {
|
||||||
background: rgb(134, 213, 160);
|
background: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-overview-card {
|
#screenerView .screener-overview-card {
|
||||||
@@ -2712,7 +2708,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(244, 246, 248);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
@@ -2766,7 +2762,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .regime-option:hover {
|
#screenerView .regime-option:hover {
|
||||||
border-color: rgb(187, 200, 219);
|
border-color: var(--border-strong);
|
||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
}
|
}
|
||||||
@@ -2979,7 +2975,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
#screenerView .result-toolbar .count-badge {
|
#screenerView .result-toolbar .count-badge {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(241, 243, 246);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
@@ -3039,25 +3035,29 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .data-table {
|
#screenerView .data-table {
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .data-table thead th {
|
#screenerView .data-table thead th {
|
||||||
height: 39px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 8px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
background: var(--table-header);
|
background: var(--table-header);
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .data-table tbody td {
|
#screenerView .data-table tbody td {
|
||||||
height: 45px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 8px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .data-table .stock-name {
|
#screenerView .data-table .stock-name {
|
||||||
@@ -3069,7 +3069,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
color: rgb(95, 107, 124);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
@@ -3091,13 +3091,13 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
#screenerView .probability-value small {
|
#screenerView .probability-value small {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: var(--font-weight-medium);
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
font-size: 9px;
|
font-size: var(--font-size-aux);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .tracking-summary {
|
#screenerView .tracking-summary {
|
||||||
@@ -3341,7 +3341,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .curated-school-filters button.active {
|
#screenerView .curated-school-filters button.active {
|
||||||
border-color: rgb(197, 212, 241);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
background: var(--scr-blue-soft);
|
background: var(--scr-blue-soft);
|
||||||
|
|
||||||
@@ -3562,7 +3562,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(238, 241, 245);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
@@ -3598,7 +3598,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
background: rgb(238, 241, 245);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
@@ -3953,7 +3953,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
background: rgb(247, 248, 250);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .quant-rule-rows {
|
#screenerView .quant-rule-rows {
|
||||||
@@ -3989,7 +3989,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
|
|
||||||
border: 1px solid rgb(220, 225, 232);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
@@ -4003,7 +4003,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
|
|
||||||
border: 1px solid rgb(220, 225, 232);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
@@ -4075,7 +4075,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
background: rgb(232, 235, 240);
|
background: var(--surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .quant-weight-status i {
|
#screenerView .quant-weight-status i {
|
||||||
@@ -4109,7 +4109,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .strategy-drawer::backdrop {
|
#screenerView .strategy-drawer::backdrop {
|
||||||
background: rgba(20, 29, 44, 0.38);
|
background: var(--backdrop);
|
||||||
|
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
@@ -4587,7 +4587,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerTrackingView .tracking-back-button:hover {
|
#screenerTrackingView .tracking-back-button:hover {
|
||||||
border-color: rgb(185, 200, 228);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
color: var(--scr-blue);
|
color: var(--scr-blue);
|
||||||
}
|
}
|
||||||
@@ -4743,23 +4743,27 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerTrackingView .data-table {
|
#screenerTrackingView .data-table {
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerTrackingView .data-table thead th {
|
#screenerTrackingView .data-table thead th {
|
||||||
height: 39px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 8px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
background: var(--table-header);
|
background: var(--table-header);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerTrackingView .data-table tbody td {
|
#screenerTrackingView .data-table tbody td {
|
||||||
height: 46px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 8px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
|
font-size: var(--font-size-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerTrackingView .stock-cell {
|
#screenerTrackingView .stock-cell {
|
||||||
@@ -4775,7 +4779,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
#screenerTrackingView .stock-cell small {
|
#screenerTrackingView .stock-cell small {
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
font-size: 9px;
|
font-size: var(--font-size-aux);
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerTrackingView .table-action.danger {
|
#screenerTrackingView .table-action.danger {
|
||||||
@@ -5495,7 +5499,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
border: 1px solid rgb(243, 201, 195);
|
border: 1px solid var(--market-up);
|
||||||
|
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
|
|
||||||
@@ -5677,7 +5681,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-tracking-entry:focus-visible {
|
#screenerView .screener-tracking-entry:focus-visible {
|
||||||
outline: rgba(37, 99, 235, 0.45) solid 2px;
|
outline: 2px solid var(--action);
|
||||||
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -5729,11 +5733,11 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
|
|
||||||
border: 1px solid rgb(217, 226, 239);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(247, 249, 252);
|
background: var(--surface-subtle);
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
@@ -5866,7 +5870,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-width: 1180px;
|
min-width: 1376px;
|
||||||
|
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
@@ -5887,12 +5891,12 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
width: 78px;
|
width: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-result-columns col:nth-child(5) {
|
#screenerView .screener-result-columns col:nth-child(5),
|
||||||
width: 104px;
|
#screenerView .screener-result-columns col:nth-child(6),
|
||||||
|
#screenerView .screener-result-columns col:nth-child(7) {
|
||||||
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#screenerView .screener-result-columns col:nth-child(6),
|
|
||||||
#screenerView .screener-result-columns col:nth-child(7),
|
|
||||||
#screenerView .screener-result-columns col:nth-child(8),
|
#screenerView .screener-result-columns col:nth-child(8),
|
||||||
#screenerView .screener-result-columns col:nth-child(9) {
|
#screenerView .screener-result-columns col:nth-child(9) {
|
||||||
width: 82px;
|
width: 82px;
|
||||||
@@ -6563,3 +6567,10 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
+85
-81
@@ -218,7 +218,11 @@
|
|||||||
.sentiment-history-table thead th {
|
.sentiment-history-table thead th {
|
||||||
border-bottom-color: rgb(216, 224, 230);
|
border-bottom-color: rgb(216, 224, 230);
|
||||||
|
|
||||||
font-size: 11.5px;
|
height: 36px;
|
||||||
|
|
||||||
|
font-size: var(--fs-caption);
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-history-table .sentiment-history-groups th {
|
.sentiment-history-table .sentiment-history-groups th {
|
||||||
@@ -313,24 +317,10 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -377,15 +367,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-phase-badge.phase-repair {
|
.sentiment-phase-badge.phase-repair {
|
||||||
background: rgb(231, 246, 246);
|
background: var(--accent-soft);
|
||||||
|
|
||||||
color: rgb(11, 109, 116);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-phase-badge.phase-fermentation {
|
.sentiment-phase-badge.phase-fermentation {
|
||||||
background: rgb(237, 246, 234);
|
background: var(--up-soft);
|
||||||
|
|
||||||
color: rgb(61, 113, 51);
|
color: var(--up);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-phase-badge.phase-climax {
|
.sentiment-phase-badge.phase-climax {
|
||||||
@@ -884,19 +874,21 @@
|
|||||||
|
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
|
|
||||||
min-height: 33px;
|
min-height: 0px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
|
|
||||||
gap: 6px;
|
align-items: flex-start;
|
||||||
|
|
||||||
padding: 0px 10px 0px 0px;
|
gap: 2px;
|
||||||
|
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
@@ -924,11 +916,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overview-strip .sentiment-text {
|
.overview-strip .sentiment-text {
|
||||||
color: var(--r2-ink);
|
color: var(--accent);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--fs-caption);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -1037,25 +1029,31 @@
|
|||||||
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
|
|
||||||
padding: 14px 16px;
|
padding: 14px 16px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-current-phase-badge {
|
.sentiment-current-phase-badge {
|
||||||
min-width: 94px;
|
min-width: 92px;
|
||||||
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
display: block;
|
display: flex;
|
||||||
|
|
||||||
padding: 10px 18px;
|
flex-direction: column;
|
||||||
|
|
||||||
border: 1px solid rgb(245, 207, 201);
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 10px;
|
gap: 2px;
|
||||||
|
|
||||||
background: var(--r2-up-soft);
|
padding: 10px 16px;
|
||||||
|
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
|
||||||
|
background: var(--accent-soft);
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -1063,11 +1061,11 @@
|
|||||||
.sentiment-current-phase-badge strong {
|
.sentiment-current-phase-badge strong {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
color: var(--r2-up);
|
color: var(--accent);
|
||||||
|
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
|
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
|
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
@@ -1075,11 +1073,11 @@
|
|||||||
.sentiment-current-phase-badge span {
|
.sentiment-current-phase-badge span {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--fs-aux);
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -1091,15 +1089,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-phase-info p {
|
.sentiment-phase-info p {
|
||||||
color: var(--r2-sub);
|
color: var(--text-2);
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: var(--fs-label);
|
||||||
|
|
||||||
line-height: 1.7;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-phase-info p b {
|
.sentiment-phase-info p b {
|
||||||
font-weight: 700;
|
color: var(--text-1);
|
||||||
|
|
||||||
|
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: 5px 9px;
|
padding: 8px 10px;
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-md);
|
||||||
|
|
||||||
background: var(--r2-amber-soft);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
color: var(--r2-amber);
|
color: var(--text-2);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--fs-caption);
|
||||||
|
|
||||||
line-height: 1.6;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-feedback-strip {
|
.sentiment-feedback-strip {
|
||||||
@@ -1131,45 +1131,47 @@
|
|||||||
|
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
||||||
border-top: 1px solid var(--r2-line-soft);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-feedback-strip > span {
|
.sentiment-feedback-strip > span {
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
|
|
||||||
display: grid;
|
display: flex;
|
||||||
|
|
||||||
grid-template-columns: auto 1fr;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
gap: 2px 8px;
|
align-items: baseline;
|
||||||
|
|
||||||
padding: 8px 12px;
|
gap: 2px 6px;
|
||||||
|
|
||||||
color: var(--r2-faint);
|
padding: 9px 14px;
|
||||||
|
|
||||||
font-size: 10.5px;
|
color: var(--text-3);
|
||||||
|
|
||||||
|
font-size: var(--fs-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-feedback-strip > span + span {
|
.sentiment-feedback-strip > span + span {
|
||||||
border-left: 1px solid var(--r2-line-soft);
|
border-left: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-feedback-strip strong {
|
.sentiment-feedback-strip strong {
|
||||||
color: var(--r2-ink);
|
color: var(--text-1);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: inherit;
|
||||||
|
|
||||||
text-align: right;
|
font-weight: 600;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sentiment-feedback-strip small {
|
.sentiment-feedback-strip small {
|
||||||
grid-column: 1 / -1;
|
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 10px;
|
font-size: var(--fs-aux);
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
@@ -1309,19 +1311,21 @@
|
|||||||
|
|
||||||
.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: auto;
|
height: 40px;
|
||||||
|
|
||||||
padding: 8px 10px;
|
padding: 0 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(--r2-sub);
|
color: var(--text-2);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--fs-caption);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -1399,17 +1403,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overview-strip .sentiment-block .sentiment-text {
|
.overview-strip .sentiment-block .sentiment-text {
|
||||||
display: block;
|
display: inline-flex;
|
||||||
|
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0 6px;
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--fs-caption);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
line-height: 1;
|
line-height: 18px;
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
|
|
||||||
@@ -1478,19 +1482,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.overview-strip[data-overview-expanded="true"] .sentiment-block {
|
.overview-strip[data-overview-expanded="true"] .sentiment-block {
|
||||||
min-height: 76px;
|
min-height: 0;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
|
|
||||||
padding: 10px 16px;
|
padding: 0 16px;
|
||||||
|
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
|
||||||
border-right: 1px solid var(--line-soft);
|
border-right: 0;
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
@@ -1628,9 +1632,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-fermentation {
|
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-fermentation {
|
||||||
background: var(--market-down-soft);
|
background: var(--market-up-soft);
|
||||||
|
|
||||||
color: var(--market-down);
|
color: var(--market-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-climax {
|
:root[data-theme="dark"] #sentimentCycleView .sentiment-phase-badge.phase-climax {
|
||||||
|
|||||||
@@ -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 class="${row.trade_date === latest?.trade_date ? "latest-row" : ""}">
|
<tr>
|
||||||
<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>
|
||||||
|
|||||||
+37
-30
@@ -184,15 +184,15 @@
|
|||||||
|
|
||||||
padding: 9px 13px;
|
padding: 9px 13px;
|
||||||
|
|
||||||
border-bottom-color: rgb(231, 235, 239);
|
border-bottom-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-directory-item:hover {
|
.theme-directory-item:hover {
|
||||||
background: rgb(241, 246, 251);
|
background: var(--hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-directory-item.active {
|
.theme-directory-item.active {
|
||||||
background: rgb(234, 242, 251);
|
background: var(--selected);
|
||||||
|
|
||||||
box-shadow: inset 3px 0 var(--action);
|
box-shadow: inset 3px 0 var(--action);
|
||||||
}
|
}
|
||||||
@@ -294,6 +294,10 @@
|
|||||||
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 {
|
||||||
@@ -331,7 +335,7 @@
|
|||||||
.theme-search-v2 {
|
.theme-search-v2 {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
|
||||||
height: 34px;
|
height: 32px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -341,9 +345,9 @@
|
|||||||
|
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
|
|
||||||
border: 1px solid var(--control-border);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
@@ -389,11 +393,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-refresh-v2 {
|
.theme-refresh-v2 {
|
||||||
min-height: 34px;
|
min-height: 32px;
|
||||||
|
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
padding: 0px 12px;
|
padding: 0px 12px;
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-refresh-v2 .lucide {
|
.theme-refresh-v2 .lucide {
|
||||||
@@ -441,17 +449,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-summary-v2 span {
|
.theme-summary-v2 span {
|
||||||
color: var(--r2-sub);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-summary-v2 strong {
|
.theme-summary-v2 strong {
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 19px;
|
font-size: var(--font-size-metric);
|
||||||
|
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
@@ -527,9 +535,9 @@
|
|||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--r2-ink);
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: var(--font-size-card-title);
|
||||||
|
|
||||||
font-weight: 700;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-card-head-v2 span {
|
.theme-card-head-v2 span {
|
||||||
@@ -675,11 +683,11 @@
|
|||||||
.theme-rank-v2 {
|
.theme-rank-v2 {
|
||||||
color: var(--r2-faint);
|
color: var(--r2-faint);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-aux);
|
||||||
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
text-align: center;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-directory-item-v2:nth-child(-n+3) .theme-rank-v2 {
|
.theme-directory-item-v2:nth-child(-n+3) .theme-rank-v2 {
|
||||||
@@ -1028,9 +1036,9 @@
|
|||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
height: 34px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 6px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom-color: var(--r2-line);
|
border-bottom-color: var(--r2-line);
|
||||||
|
|
||||||
@@ -1038,15 +1046,20 @@
|
|||||||
|
|
||||||
color: var(--r2-sub);
|
color: var(--r2-sub);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-members-table-v2 tbody td {
|
.theme-members-table-v2 tbody td {
|
||||||
height: 39px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 7px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-table);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-members-table-v2 thead th:first-child,
|
||||||
|
.theme-members-table-v2 tbody td:first-child {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-members-table-v2 tbody tr {
|
.theme-members-table-v2 tbody tr {
|
||||||
@@ -1076,10 +1089,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -1184,11 +1193,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-members-table-v2 tbody td {
|
.theme-members-table-v2 tbody td {
|
||||||
height: 35px;
|
height: 40px;
|
||||||
|
|
||||||
padding-top: 5px;
|
padding: 0 12px;
|
||||||
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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="num">序号</th><th>股票</th><th class="number num sortable">涨跌幅(%)<span class="arr">↕</span></th><th class="number num sortable">收盘价(元)<span class="arr">↕</span></th><th class="number num sortable">成交额(亿)<span class="arr">↕</span></th></tr></thead><tbody id="themeMemberTableBody"></tbody></table>
|
<table class="data-table tbl theme-members-table-v2"><thead><tr><th class="row-number">序号</th><th>股票</th><th class="number num sortable">涨跌幅(%)<span class="arr">↕</span></th><th class="number num sortable">收盘价(元)<span class="arr">↕</span></th><th class="number num sortable">成交额(亿)<span class="arr">↕</span></th></tr></thead><tbody id="themeMemberTableBody"></tbody></table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 num muted">${index + 1}</td>
|
<tr data-code="${escapeHtml(row.code)}"><td class="row-number muted">${index + 1}</td>
|
||||||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
<td><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("");
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ 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);
|
||||||
|
|||||||
+103
-89
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
background: rgb(237, 241, 244);
|
background: var(--canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-gate[hidden] {
|
.auth-gate[hidden] {
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
|
|
||||||
padding: 26px;
|
padding: 26px;
|
||||||
|
|
||||||
border: 1px solid var(--line-strong);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 12px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow-raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-brand {
|
.auth-brand {
|
||||||
@@ -46,7 +46,9 @@
|
|||||||
.auth-brand h1 {
|
.auth-brand h1 {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
font-size: 22px;
|
font-size: var(--font-size-page-title);
|
||||||
|
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-brand span {
|
.auth-brand span {
|
||||||
@@ -54,9 +56,9 @@
|
|||||||
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
||||||
color: var(--text-muted);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-tabs {
|
.auth-tabs {
|
||||||
@@ -88,11 +90,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auth-tab.active {
|
.auth-tab.active {
|
||||||
border-bottom-color: var(--coral);
|
border-bottom-color: var(--action);
|
||||||
|
|
||||||
color: var(--text);
|
color: var(--text-1);
|
||||||
|
|
||||||
font-weight: 750;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form {
|
.auth-form {
|
||||||
@@ -106,15 +108,15 @@
|
|||||||
.auth-form .button {
|
.auth-form .button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-height: 40px;
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-error {
|
.auth-error {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
color: rgb(185, 54, 39);
|
color: var(--market-up);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@@ -146,23 +148,23 @@
|
|||||||
.admin-section-picker select {
|
.admin-section-picker select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-height: 40px;
|
min-height: 32px;
|
||||||
|
|
||||||
padding: 0px 11px;
|
padding: 0px 11px;
|
||||||
|
|
||||||
border: 1px solid var(--line-strong);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 13px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-section-picker select:focus-visible {
|
.admin-section-picker select:focus-visible {
|
||||||
outline: 2px solid var(--blue);
|
outline: 2px solid var(--action);
|
||||||
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -307,13 +309,13 @@
|
|||||||
|
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
|
|
||||||
height: 34px;
|
height: 32px;
|
||||||
|
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
|
|
||||||
border: 1px solid var(--line-strong);
|
border: 1px solid var(--border-strong);
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
@@ -386,11 +388,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connection-status.connected {
|
.connection-status.connected {
|
||||||
border-color: rgb(167, 222, 201);
|
border-color: var(--market-down);
|
||||||
|
|
||||||
background: var(--green-soft);
|
background: var(--green-soft);
|
||||||
|
|
||||||
color: rgb(8, 106, 75);
|
color: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-dialog form {
|
.settings-dialog form {
|
||||||
@@ -470,11 +472,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-button > span {
|
.account-button > span {
|
||||||
min-width: 0px;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
overflow: hidden;
|
min-width: max-content;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
overflow: visible;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -594,7 +596,7 @@
|
|||||||
.account-dropdown button {
|
.account-dropdown button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-height: 40px;
|
min-height: 36px;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
@@ -636,7 +638,7 @@
|
|||||||
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
box-shadow: inset 0 0 0 2px var(--focus-ring, #1268c4);
|
box-shadow: inset 0 0 0 2px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-dropdown button svg {
|
.account-dropdown button svg {
|
||||||
@@ -662,16 +664,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-dropdown .account-menu-danger {
|
.account-dropdown .account-menu-danger {
|
||||||
color: rgb(181, 58, 66);
|
color: var(--market-up);
|
||||||
|
|
||||||
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: rgb(163, 47, 55);
|
color: var(--market-up);
|
||||||
|
|
||||||
background: rgb(255, 242, 243);
|
background: var(--market-up-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.account-role-badge {
|
button.account-role-badge {
|
||||||
@@ -685,11 +687,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: rgba(67, 76, 86, 0.14) 0px 3px 10px;
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.account-role-badge:focus-visible {
|
button.account-role-badge:focus-visible {
|
||||||
outline: 2px solid var(--blue);
|
outline: 2px solid var(--action);
|
||||||
|
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -701,9 +703,9 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-role-badge {
|
.admin-role-badge {
|
||||||
color: rgb(83, 103, 124);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
background: rgb(244, 247, 250);
|
background: var(--surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vip-role-badge {
|
.vip-role-badge {
|
||||||
@@ -719,7 +721,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: rgb(244, 246, 248);
|
background: var(--surface-muted);
|
||||||
|
|
||||||
border-color: rgb(203, 211, 218);
|
border-color: rgb(203, 211, 218);
|
||||||
|
|
||||||
@@ -825,7 +827,7 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.member-gate strong {
|
.member-gate strong {
|
||||||
color: rgb(60, 70, 80);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -877,7 +879,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
color: rgb(83, 103, 124);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.privacy-note svg {
|
.privacy-note svg {
|
||||||
@@ -889,7 +891,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
|
|
||||||
color: rgb(52, 116, 95);
|
color: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership-status-grid {
|
.membership-status-grid {
|
||||||
@@ -905,17 +907,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(--line, #e2e8ee);
|
border: 1px solid var(--border);
|
||||||
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
background: rgb(251, 252, 253);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership-status-grid span {
|
.membership-status-grid span {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
color: rgb(116, 128, 140);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
@@ -925,7 +927,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
||||||
color: rgb(38, 50, 61);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
@@ -935,7 +937,7 @@ button.account-role-badge:focus-visible {
|
|||||||
.membership-comparison {
|
.membership-comparison {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
border: 1px solid var(--line, #e2e8ee);
|
border: 1px solid var(--border);
|
||||||
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
@@ -953,7 +955,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
padding: 9px 11px;
|
padding: 9px 11px;
|
||||||
|
|
||||||
border-top: 1px solid var(--line, #e2e8ee);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership-comparison > div:first-child {
|
.membership-comparison > div:first-child {
|
||||||
@@ -961,21 +963,21 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.membership-comparison-head {
|
.membership-comparison-head {
|
||||||
color: rgb(105, 118, 131);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
background: rgb(247, 249, 251);
|
background: var(--surface-subtle);
|
||||||
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership-comparison b {
|
.membership-comparison b {
|
||||||
color: rgb(39, 108, 88);
|
color: var(--market-down);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership-comparison b.muted {
|
.membership-comparison b.muted {
|
||||||
color: rgb(154, 164, 173);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.membership-comparison b.available {
|
.membership-comparison b.available {
|
||||||
@@ -993,7 +995,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
|
|
||||||
color: rgb(93, 105, 116);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -1026,11 +1028,15 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
grid-template-columns: auto minmax(0px, 1fr);
|
grid-template-columns: 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 {
|
||||||
@@ -1085,7 +1091,7 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-settings-dialog {
|
.account-settings-dialog {
|
||||||
border-radius: 9px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-dialog {
|
.settings-dialog {
|
||||||
@@ -1095,7 +1101,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
border-radius: 9px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-dropdown {
|
.account-dropdown {
|
||||||
@@ -1115,17 +1121,17 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
|
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: var(--surface);
|
||||||
|
|
||||||
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-color: var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
box-shadow: rgba(22, 34, 46, 0.15) 0px 14px 35px;
|
box-shadow: var(--shadow-float);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
@@ -1177,29 +1183,37 @@ button.account-role-badge:focus-visible {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) and (max-width: 1510px) {
|
@media (min-width: 1280px) and (max-width: 1439px) {
|
||||||
.account-role-badge span {
|
.app-header .header-command-group .account-role-badge span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-role-badge {
|
.app-header .header-command-group .account-role-badge {
|
||||||
min-width: 27px;
|
min-width: 28px;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
padding-inline: 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1380px) and (max-width: 1510px) {
|
@media (min-width: 1280px) {
|
||||||
#settingsButton,
|
|
||||||
.header-command-group .account-button {
|
.header-command-group .account-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
padding: 0px 9px;
|
max-width: none;
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1214,7 +1228,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
background: rgb(37, 99, 235);
|
background: var(--action);
|
||||||
|
|
||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
|
|
||||||
@@ -1262,11 +1276,11 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-button {
|
.account-button {
|
||||||
overflow: hidden;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
overflow: visible;
|
||||||
|
|
||||||
max-width: 108px;
|
max-width: none;
|
||||||
|
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
|
|
||||||
@@ -1308,17 +1322,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: rgb(39, 52, 67);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: var(--font-size-card-title);
|
||||||
|
|
||||||
font-weight: 720;
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-dialog:not(.heaven-reading-dialog) .settings-section-heading > span {
|
.settings-dialog:not(.heaven-reading-dialog) .settings-section-heading > span {
|
||||||
color: rgb(124, 135, 149);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 10.5px;
|
font-size: var(--font-size-aux);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-dialog:not(.heaven-reading-dialog) .form-field {
|
.settings-dialog:not(.heaven-reading-dialog) .form-field {
|
||||||
@@ -1327,7 +1341,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
.settings-dialog:not(.heaven-reading-dialog) .form-field > label,
|
.settings-dialog:not(.heaven-reading-dialog) .form-field > label,
|
||||||
.settings-dialog:not(.heaven-reading-dialog) .form-field > span {
|
.settings-dialog:not(.heaven-reading-dialog) .form-field > span {
|
||||||
color: rgb(78, 91, 106);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|
||||||
@@ -1335,7 +1349,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: rgb(213, 220, 229);
|
border-color: var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
@@ -1375,7 +1389,7 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-settings-dialog .settings-lead {
|
.account-settings-dialog .settings-lead {
|
||||||
color: rgb(96, 109, 124);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
@@ -1397,7 +1411,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
padding: 11px 12px;
|
padding: 11px 12px;
|
||||||
|
|
||||||
border-color: rgb(223, 228, 234);
|
border-color: var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
@@ -1425,7 +1439,7 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.account-settings-dialog .membership-comparison {
|
.account-settings-dialog .membership-comparison {
|
||||||
border-color: rgb(223, 228, 234);
|
border-color: var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
@@ -1435,7 +1449,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
padding: 8px 11px;
|
padding: 8px 11px;
|
||||||
|
|
||||||
border-color: rgb(230, 234, 239);
|
border-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-settings-dialog .membership-comparison-head {
|
.account-settings-dialog .membership-comparison-head {
|
||||||
@@ -1449,11 +1463,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 rgb(220, 232, 226);
|
border: 1px solid var(--market-down);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
background: rgb(245, 250, 247);
|
background: var(--market-down-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-settings-dialog .account-birth-form {
|
.account-settings-dialog .account-birth-form {
|
||||||
@@ -1475,7 +1489,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
color: rgb(102, 115, 132);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
@@ -1489,7 +1503,7 @@ button.account-role-badge:focus-visible {
|
|||||||
|
|
||||||
border-bottom: 1px solid var(--dialog-line);
|
border-bottom: 1px solid var(--dialog-line);
|
||||||
|
|
||||||
background: rgb(251, 252, 253);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dialog .admin-section-picker label {
|
.admin-dialog .admin-section-picker label {
|
||||||
@@ -1497,13 +1511,13 @@ button.account-role-badge:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-dialog .admin-section-picker select {
|
.admin-dialog .admin-section-picker select {
|
||||||
min-height: 36px;
|
min-height: 32px;
|
||||||
|
|
||||||
border-color: rgb(212, 220, 229);
|
border-color: var(--border-strong);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-dialog .admin-panel {
|
.admin-dialog .admin-panel {
|
||||||
@@ -1635,7 +1649,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: color-mix(in srgb, var(--canvas) 92%, transparent);
|
background: var(--backdrop);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] :is(.auth-shell) {
|
:root[data-theme="dark"] :is(.auth-shell) {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
/* 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-weight: 700;
|
font-size: var(--fs-table);
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stock-code {
|
.stock-code {
|
||||||
color: var(--text-muted);
|
color: var(--text-3);
|
||||||
|
|
||||||
|
font-size: var(--fs-aux);
|
||||||
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
@@ -49,25 +53,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
|
|
||||||
min-height: 24px;
|
justify-content: center;
|
||||||
|
|
||||||
padding: 2px 8px;
|
min-width: 30px;
|
||||||
|
|
||||||
border-radius: 4px;
|
height: 20px;
|
||||||
|
|
||||||
|
padding: 0 7px;
|
||||||
|
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
background: var(--up-soft);
|
||||||
|
|
||||||
|
color: var(--up);
|
||||||
|
|
||||||
|
font-size: var(--fs-caption);
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.streak-pill.high {
|
||||||
|
background: var(--up);
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.outcome-tag {
|
.outcome-tag {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
@@ -227,7 +243,7 @@
|
|||||||
|
|
||||||
font-size: var(--font-size-card-title);
|
font-size: var(--font-size-card-title);
|
||||||
|
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-h .sub {
|
.card-h .sub {
|
||||||
@@ -253,21 +269,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sname {
|
.sname {
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
|
|
||||||
font-size: 13px;
|
font-size: var(--fs-table);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scode {
|
.scode {
|
||||||
font-size: 11px;
|
font-size: var(--fs-aux);
|
||||||
|
|
||||||
color: var(--faint);
|
color: var(--text-3);
|
||||||
|
|
||||||
margin-left: 6px;
|
margin-left: 0;
|
||||||
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stock-cell {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,14 +635,16 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) and (max-width: 1510px) {
|
@media (min-width: 1280px) {
|
||||||
.header-command-group .command-button {
|
.header-command-group .command-button:not(.account-button) {
|
||||||
width: 31px;
|
width: 32px;
|
||||||
|
|
||||||
|
min-width: 32px;
|
||||||
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-command-group .command-button > span {
|
.header-command-group .command-button:not(.account-button) > span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
.dialog-header h2 {
|
.dialog-header h2 {
|
||||||
margin: 2px 0px 0px;
|
margin: 2px 0px 0px;
|
||||||
|
|
||||||
font-size: 19px;
|
font-size: 16px;
|
||||||
|
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: 0px;
|
||||||
}
|
}
|
||||||
@@ -36,9 +38,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-dialog .model-row {
|
.admin-dialog .model-row {
|
||||||
border-color: rgb(223, 228, 234);
|
border-color: var(--border);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background: var(--surface-subtle);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
@@ -240,11 +242,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-button.has-alerts {
|
.alert-button.has-alerts {
|
||||||
border-color: rgb(214, 180, 93);
|
border-color: var(--warning-color);
|
||||||
|
|
||||||
background: rgb(255, 249, 233);
|
background: var(--warning-soft);
|
||||||
|
|
||||||
color: rgb(138, 97, 0);
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-badge {
|
.alert-badge {
|
||||||
@@ -526,17 +528,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-message.user .assistant-message-content {
|
.assistant-message.user .assistant-message-content {
|
||||||
border-color: rgb(190, 210, 235);
|
border-color: var(--color-action-line);
|
||||||
|
|
||||||
background: var(--action-soft);
|
background: var(--action-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-message.is-error .assistant-message-content {
|
.assistant-message.is-error .assistant-message-content {
|
||||||
border-color: rgb(239, 197, 200);
|
border-color: var(--market-up);
|
||||||
|
|
||||||
background: var(--market-up-soft);
|
background: var(--market-up-soft);
|
||||||
|
|
||||||
color: rgb(139, 47, 52);
|
color: var(--market-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-message-content p {
|
.assistant-message-content p {
|
||||||
@@ -778,7 +780,7 @@
|
|||||||
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
padding: 14px 18px;
|
padding: 20px;
|
||||||
|
|
||||||
min-height: 62px;
|
min-height: 62px;
|
||||||
|
|
||||||
@@ -786,7 +788,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-dialog {
|
.assistant-dialog {
|
||||||
border-radius: 9px;
|
border-radius: 12px;
|
||||||
|
|
||||||
width: min(820px, -32px + 100vw);
|
width: min(820px, -32px + 100vw);
|
||||||
|
|
||||||
@@ -796,9 +798,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-eyebrow {
|
.dialog-eyebrow {
|
||||||
color: var(--text-muted);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-search-dialog {
|
.global-search-dialog {
|
||||||
@@ -806,7 +808,7 @@
|
|||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
box-shadow: rgba(26, 37, 47, 0.22) 0px 22px 70px, rgba(26, 37, 47, 0.1) 0px 3px 14px;
|
box-shadow: var(--shadow-float);
|
||||||
|
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
|
|
||||||
@@ -892,13 +894,13 @@
|
|||||||
|
|
||||||
font: 11px / 1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
|
font: 11px / 1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||||
|
|
||||||
border-color: rgb(214, 221, 229);
|
border-color: var(--border-strong);
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background: rgb(247, 248, 250);
|
background: var(--surface-subtle);
|
||||||
|
|
||||||
color: rgb(125, 136, 150);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-search-results {
|
.global-search-results {
|
||||||
@@ -920,7 +922,7 @@
|
|||||||
|
|
||||||
padding: 9px 10px 5px;
|
padding: 9px 10px 5px;
|
||||||
|
|
||||||
color: rgb(135, 146, 160);
|
color: var(--text-tertiary);
|
||||||
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
@@ -957,9 +959,9 @@
|
|||||||
|
|
||||||
.global-search-result.is-active,
|
.global-search-result.is-active,
|
||||||
.global-search-result:hover {
|
.global-search-result:hover {
|
||||||
background: rgb(238, 244, 255);
|
background: var(--action-soft);
|
||||||
|
|
||||||
color: rgb(31, 85, 165);
|
color: var(--action);
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-search-result-icon {
|
.global-search-result-icon {
|
||||||
@@ -973,7 +975,7 @@
|
|||||||
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|
||||||
border-color: rgb(224, 229, 235);
|
border-color: var(--border);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
@@ -1003,7 +1005,7 @@
|
|||||||
|
|
||||||
border-color: var(--dialog-line);
|
border-color: var(--dialog-line);
|
||||||
|
|
||||||
background: rgb(251, 252, 253);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts-dialog .alert-form {
|
.alerts-dialog .alert-form {
|
||||||
@@ -1077,7 +1079,7 @@
|
|||||||
|
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
|
|
||||||
background: rgb(247, 249, 251);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-dialog .assistant-message {
|
.assistant-dialog .assistant-message {
|
||||||
@@ -1087,9 +1089,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-dialog .assistant-message-content {
|
.assistant-dialog .assistant-message-content {
|
||||||
border-color: rgb(220, 227, 235);
|
border-color: var(--border);
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: 8px;
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
|
||||||
@@ -1197,7 +1199,7 @@
|
|||||||
|
|
||||||
z-index: 95;
|
z-index: 95;
|
||||||
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.12) -8px 0px 24px;
|
box-shadow: var(--shadow-float);
|
||||||
|
|
||||||
transition: right 0.25s;
|
transition: right 0.25s;
|
||||||
|
|
||||||
|
|||||||
@@ -52,21 +52,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.segmented {
|
.segmented {
|
||||||
height: 34px;
|
height: var(--control-height);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
min-height: 36px;
|
min-height: var(--control-height);
|
||||||
|
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: var(--radius-md);
|
||||||
|
|
||||||
background: rgb(236, 239, 244);
|
background: var(--surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.segment {
|
.segment {
|
||||||
@@ -82,11 +82,11 @@
|
|||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--font-size-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
.segment.active {
|
.segment.active {
|
||||||
|
|||||||
@@ -14,13 +14,13 @@
|
|||||||
.data-table th.sort-asc::after {
|
.data-table th.sort-asc::after {
|
||||||
content: " ↑";
|
content: " ↑";
|
||||||
|
|
||||||
color: var(--blue);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table th.sort-desc::after {
|
.data-table th.sort-desc::after {
|
||||||
content: " ↓";
|
content: " ↓";
|
||||||
|
|
||||||
color: var(--blue);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tbody tr td {
|
.data-table tbody tr td {
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|
||||||
text-align: center;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-action {
|
.table-action {
|
||||||
@@ -114,9 +114,9 @@
|
|||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
height: 42px;
|
height: 40px;
|
||||||
|
|
||||||
padding: 8px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
|
|
||||||
@@ -214,7 +214,9 @@
|
|||||||
.data-table .stock-name {
|
.data-table .stock-name {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
font-weight: 700;
|
font-size: var(--fs-table);
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-grid > .table-frame {
|
.main-grid > .table-frame {
|
||||||
@@ -242,7 +244,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.data-table thead th {
|
.data-table thead th {
|
||||||
padding: 0px 11px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
@@ -256,23 +258,31 @@
|
|||||||
|
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|
||||||
font-size: var(--font-size-label);
|
font-size: var(--font-size-caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tbody td {
|
.data-table tbody td {
|
||||||
padding: 6px 11px;
|
padding: 0 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
height: 41px;
|
.data-table thead tr:last-child th:first-child,
|
||||||
|
.data-table tbody td:first-child,
|
||||||
|
.tbl thead tr:last-child th:first-child,
|
||||||
|
.tbl tbody td:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbl .row-number {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-grid .data-table thead th {
|
.main-grid .data-table thead th {
|
||||||
height: 34px;
|
height: 36px;
|
||||||
|
|
||||||
padding: 6px 10px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-grid .data-table tbody td {
|
.main-grid .data-table tbody td {
|
||||||
@@ -300,11 +310,11 @@
|
|||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: var(--font-size-caption);
|
||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
padding: 8px 12px;
|
padding: 0 12px;
|
||||||
|
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
|
|
||||||
@@ -331,18 +341,30 @@
|
|||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tbl thead th.sorted .arr {
|
.tbl thead th.sorted,
|
||||||
color: var(--blue);
|
.data-table thead th.sorted,
|
||||||
|
.data-table thead th.sort-asc,
|
||||||
|
.data-table thead th.sort-desc {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbl thead th.sorted .arr,
|
||||||
|
.data-table thead th.sorted .arr {
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tbl tbody td {
|
.tbl tbody td {
|
||||||
padding: 9px 12px;
|
padding: 0 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 {
|
||||||
@@ -415,6 +437,19 @@
|
|||||||
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);
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ 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));
|
||||||
setText("tapeLimit", `${overview.limit_up_count} / 跌停 ${overview.limit_down_count}`);
|
animateMetric("tapeLimit", overview.limit_up_count, (value) => `${Math.round(value)} 家`);
|
||||||
|
animateMetric("tapeLimitDown", overview.limit_down_count, (value) => `${Math.round(value)} 家`);
|
||||||
|
animateMetric("detailBroken", overview.broken_count, (value) => `${Math.round(value)} 家`);
|
||||||
|
animateMetric("detailSealRate", overview.seal_rate, (value) => `${formatNumber(value, 1)}%`);
|
||||||
animateMetric("tapeAmount", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`);
|
animateMetric("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)} 家`);
|
||||||
@@ -98,7 +101,30 @@ 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));
|
||||||
setText("sentimentText", sentimentLabel(overview.sentiment_score));
|
animateMetric("detailSentimentScore", overview.sentiment_score, (value) => Math.round(value));
|
||||||
|
const moodLabel = sentimentLabel(overview.sentiment_score);
|
||||||
|
setText("sentimentText", moodLabel);
|
||||||
|
setText("detailSentimentText", moodLabel);
|
||||||
|
document.querySelectorAll("#sentimentText, #detailSentimentText").forEach((sentimentChip) => {
|
||||||
|
sentimentChip.classList.toggle("is-hot", moodLabel === "情绪高涨");
|
||||||
|
sentimentChip.classList.toggle("is-strong", moodLabel === "情绪偏强");
|
||||||
|
sentimentChip.classList.toggle("is-cold", moodLabel === "情绪冰点" || moodLabel === "情绪偏弱");
|
||||||
|
});
|
||||||
|
const pageSubtitle = document.querySelector("#currentPageSubtitle");
|
||||||
|
const activePage = window.XiaobaiPages?.get(state.activeView);
|
||||||
|
if (pageSubtitle) {
|
||||||
|
const dateText = displayCompactDate(meta.trade_date);
|
||||||
|
if (activePage?.id === "mentorView") {
|
||||||
|
pageSubtitle.textContent = dateText === "--"
|
||||||
|
? "与不同交易思维模型持续对话 · 数据日期 --"
|
||||||
|
: `与不同交易思维模型持续对话 · 数据日期 ${dateText}`;
|
||||||
|
} else {
|
||||||
|
const groupLabel = activePage?.group === "market"
|
||||||
|
? "市场复盘"
|
||||||
|
: activePage?.group === "personal" ? "个人" : "智能工具";
|
||||||
|
pageSubtitle.textContent = dateText === "--" ? groupLabel : `${groupLabel} · ${dateText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
updateSentimentGauge(overview.sentiment_score);
|
updateSentimentGauge(overview.sentiment_score);
|
||||||
setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`);
|
setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`);
|
||||||
|
|
||||||
|
|||||||
+612
-79
@@ -187,12 +187,6 @@ 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) {
|
||||||
@@ -228,10 +222,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -262,10 +252,6 @@ 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;
|
||||||
|
|
||||||
@@ -311,10 +297,6 @@ 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) {
|
||||||
@@ -644,10 +626,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -662,14 +640,6 @@ 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;
|
||||||
|
|
||||||
@@ -770,10 +740,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -892,7 +858,7 @@ body.sidebar-collapsed .app-main {
|
|||||||
|
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: var(--radius-md);
|
||||||
|
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
|
||||||
@@ -1024,8 +990,8 @@ body.sidebar-collapsed .app-main {
|
|||||||
grid-template-columns: 176px minmax(0px, 1fr) auto;
|
grid-template-columns: 176px minmax(0px, 1fr) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-tape {
|
.app-header .market-tape {
|
||||||
display: none;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-nav {
|
.module-nav {
|
||||||
@@ -1424,19 +1390,19 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
|
|||||||
|
|
||||||
padding: 12px 10px 4px;
|
padding: 12px 10px 4px;
|
||||||
|
|
||||||
color: var(--r2-faint);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 11px;
|
font-size: var(--fs-aux);
|
||||||
|
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
|
|
||||||
letter-spacing: 0px;
|
letter-spacing: .04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-nav .module-tab {
|
.module-nav .module-tab {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
min-height: 34px;
|
min-height: 36px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -1452,13 +1418,13 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
|
|||||||
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
||||||
border-radius: 7px;
|
border-radius: var(--radius-md);
|
||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-2);
|
||||||
|
|
||||||
font-size: 13px;
|
font-size: var(--fs-table);
|
||||||
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
@@ -1476,23 +1442,23 @@ body.sidebar-collapsed .module-nav .nav-group + .nav-group {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-nav .module-tab:hover {
|
.module-nav .module-tab:hover {
|
||||||
background: var(--surface-hover);
|
background: var(--hover);
|
||||||
|
|
||||||
color: var(--r2-ink);
|
color: var(--text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-nav .module-tab.active {
|
.module-nav .module-tab.active {
|
||||||
background: var(--r2-blue-soft);
|
background: var(--selected);
|
||||||
|
|
||||||
color: var(--r2-blue);
|
color: var(--accent);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-nav .module-tab.mobile-active {
|
.module-nav .module-tab.mobile-active {
|
||||||
background: var(--r2-blue-soft);
|
background: var(--selected);
|
||||||
|
|
||||||
color: var(--r2-blue);
|
color: var(--accent);
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -1756,7 +1722,11 @@ body.sidebar-collapsed .module-nav .module-tab {
|
|||||||
|
|
||||||
padding: 0px 16px;
|
padding: 0px 16px;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
|
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
border-top: 0px;
|
border-top: 0px;
|
||||||
|
|
||||||
@@ -1775,6 +1745,10 @@ 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;
|
||||||
|
|
||||||
@@ -1784,6 +1758,8 @@ 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);
|
||||||
@@ -1998,7 +1974,7 @@ body.sidebar-collapsed .module-nav .module-tab {
|
|||||||
|
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
min-height: 30px;
|
min-height: var(--statusbar-height);
|
||||||
|
|
||||||
height: var(--statusbar-height);
|
height: var(--statusbar-height);
|
||||||
|
|
||||||
@@ -2012,11 +1988,11 @@ body.sidebar-collapsed .module-nav .module-tab {
|
|||||||
|
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--header-bg);
|
||||||
|
|
||||||
color: var(--faint);
|
color: var(--text-3);
|
||||||
|
|
||||||
font-size: 11.5px;
|
font-size: var(--fs-aux);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.sidebar-collapsed .status-bar {
|
body.sidebar-collapsed .status-bar {
|
||||||
@@ -2158,7 +2134,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
|
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--header-bg);
|
||||||
|
|
||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
|
|
||||||
@@ -2266,7 +2242,15 @@ body.sidebar-collapsed .status-bar {
|
|||||||
.main {
|
.main {
|
||||||
margin-left: var(--sidebar-width);
|
margin-left: var(--sidebar-width);
|
||||||
|
|
||||||
min-width: 1080px;
|
min-width: 0;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.main {
|
||||||
|
min-width: 1080px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header {
|
.app-header {
|
||||||
@@ -2276,7 +2260,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
|
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
min-height: 46px;
|
min-height: var(--topbar-height);
|
||||||
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
@@ -2288,17 +2272,17 @@ body.sidebar-collapsed .status-bar {
|
|||||||
|
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--header-bg);
|
||||||
|
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
gap: 14px;
|
gap: var(--header-cluster-gap);
|
||||||
|
|
||||||
padding: 0 var(--page-pad-x);
|
padding: 0 var(--header-pad-x);
|
||||||
|
|
||||||
height: var(--topbar-height);
|
height: var(--topbar-height);
|
||||||
}
|
}
|
||||||
@@ -2412,7 +2396,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .overview-strip {
|
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .app-main .overview-strip {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2428,7 +2412,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"]) .overview-strip {
|
body:is([data-active-view="auctionView"], [data-active-view="themeLibraryView"], [data-active-view="popularityView"], [data-active-view="mentorView"], [data-active-view="rotationView"]) .app-main .overview-strip {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2478,8 +2462,8 @@ body.sidebar-collapsed .status-bar {
|
|||||||
padding: var(--mobile-shell-pad);
|
padding: var(--mobile-shell-pad);
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-tape {
|
.app-header .market-tape {
|
||||||
display: none;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
@@ -2701,10 +2685,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -2730,10 +2710,20 @@ body.sidebar-collapsed .status-bar {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] :is(.module-nav, .app-header, .overview-strip, .status-bar) {
|
:root[data-theme="dark"] :is(.module-nav, .app-header, .status-bar) {
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--header-bg);
|
||||||
|
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .overview-strip {
|
||||||
|
border-color: var(--border);
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
@@ -2741,7 +2731,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] .app-header {
|
:root[data-theme="dark"] .app-header {
|
||||||
background: color-mix(in srgb, var(--surface) 96%, transparent);
|
background: var(--header-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] :is(.sidebar-brand, .module-nav .sidebar-brand, .nav-group, .sidebar-collapse-button, .header-date-group) {
|
:root[data-theme="dark"] :is(.sidebar-brand, .module-nav .sidebar-brand, .nav-group, .sidebar-collapse-button, .header-date-group) {
|
||||||
@@ -3019,10 +3009,16 @@ body.sidebar-collapsed .status-bar {
|
|||||||
body.mobile-shell .header-command-group > .command-button,
|
body.mobile-shell .header-command-group > .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;
|
||||||
@@ -3188,11 +3184,6 @@ 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 {
|
||||||
@@ -3222,3 +3213,545 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,19 @@
|
|||||||
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 && isMobileViewport());
|
document.body.classList.toggle("mobile-command-open", open);
|
||||||
if (backdrop) backdrop.hidden = !(open && isMobileViewport());
|
if (backdrop) backdrop.hidden = !open;
|
||||||
if (open && isMobileViewport()) {
|
if (open) {
|
||||||
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 });
|
||||||
@@ -56,6 +63,28 @@
|
|||||||
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;
|
||||||
@@ -69,6 +98,9 @@
|
|||||||
: 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(
|
||||||
@@ -184,26 +216,35 @@
|
|||||||
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";
|
||||||
overview.dataset.overviewExpanded = String(expanded);
|
setOverviewExpanded(expanded);
|
||||||
event.currentTarget.setAttribute("aria-expanded", String(expanded));
|
if (expanded) toggleHeaderCommandMenu(false);
|
||||||
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(".header-actions")) toggleHeaderCommandMenu(false);
|
if (!event.target.closest("#headerCommandGroup, #headerMenuButton")) toggleHeaderCommandMenu(false);
|
||||||
|
if (!event.target.closest(".overview-strip")) setOverviewExpanded(false);
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape") toggleHeaderCommandMenu(false);
|
if (event.key !== "Escape") return;
|
||||||
|
toggleHeaderCommandMenu(false);
|
||||||
|
setOverviewExpanded(false);
|
||||||
});
|
});
|
||||||
global.addEventListener("resize", () => {
|
global.addEventListener("resize", () => {
|
||||||
toggleHeaderCommandMenu(false);
|
toggleHeaderCommandMenu(false);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ 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() {
|
||||||
|
|||||||
+138
-67
@@ -22,19 +22,20 @@
|
|||||||
--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: #f3f4f6;
|
--color-surface-muted: #f2f3f5;
|
||||||
--color-surface-subtle: #f8fafc;
|
--color-surface-subtle: #f8f9fb;
|
||||||
--color-border: #e5e7eb;
|
--color-border: #eceded;
|
||||||
--color-border-strong: #d1d5db;
|
--color-border-strong: #dee0e3;
|
||||||
--color-text-primary: #1f2937;
|
--color-text-primary: #1f2329;
|
||||||
--color-text-secondary: #6b7280;
|
--color-text-secondary: #646a73;
|
||||||
--color-text-tertiary: #9ca3af;
|
--color-text-tertiary: #8f959e;
|
||||||
--color-action-base: #2563eb;
|
--color-action-base: #3370ff;
|
||||||
--color-action-base-hover: #1d4ed8;
|
--color-action-base-hover: #2b5fd9;
|
||||||
--color-action-base-soft: #eff4ff;
|
--color-action-base-soft: #eaf1fe;
|
||||||
--color-action: #2563eb;
|
--color-action: #3370ff;
|
||||||
--color-action-hover: #1d4ed8;
|
--color-action-hover: #2b5fd9;
|
||||||
--color-action-soft: #eff4ff;
|
--color-action-soft: #eaf1fe;
|
||||||
|
--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;
|
||||||
@@ -49,24 +50,25 @@
|
|||||||
--color-warning: #b45309;
|
--color-warning: #b45309;
|
||||||
--color-warning-soft: #fdf3e3;
|
--color-warning-soft: #fdf3e3;
|
||||||
|
|
||||||
--size-radius-sm: 5px;
|
--size-radius-sm: 4px;
|
||||||
--size-radius-md: 7px;
|
--size-radius-md: 8px;
|
||||||
--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: 46px;
|
--size-topbar: 64px;
|
||||||
--size-summary: 36px;
|
--size-summary: 0px;
|
||||||
--size-statusbar: 30px;
|
--size-statusbar: 28px;
|
||||||
--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(16, 24, 40, .05);
|
--elevation-card: 0 1px 2px rgba(31, 35, 41, .04);
|
||||||
--elevation-soft: 0 1px 2px rgba(16, 24, 40, .05);
|
--elevation-soft: 0 1px 2px rgba(31, 35, 41, .04);
|
||||||
--elevation-raised: 0 4px 14px rgba(16, 24, 40, .06);
|
--elevation-raised: 0 2px 6px rgba(31, 35, 41, .04), 0 8px 24px rgba(31, 35, 41, .06);
|
||||||
--elevation-float: 0 12px 32px rgba(0, 0, 0, .18);
|
--elevation-float: 0 12px 32px rgba(0, 0, 0, .14);
|
||||||
--motion-instant: 100ms;
|
--motion-instant: 100ms;
|
||||||
--motion-fast: 140ms;
|
--motion-fast: 120ms;
|
||||||
--motion-medium: 200ms;
|
--motion-medium: 200ms;
|
||||||
--motion-deliberate: 260ms;
|
--motion-deliberate: 260ms;
|
||||||
--motion-slow: 560ms;
|
--motion-slow: 560ms;
|
||||||
@@ -80,8 +82,11 @@
|
|||||||
--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;
|
||||||
--surface-hover: #f8faff;
|
--header-bg: var(--color-white);
|
||||||
--surface-selected: #eff4ff;
|
--surface-hover: #f2f3f5;
|
||||||
|
--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);
|
||||||
@@ -93,7 +98,12 @@
|
|||||||
--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);
|
||||||
@@ -106,7 +116,12 @@
|
|||||||
--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(37, 99, 235, .16);
|
--focus-ring: rgba(51, 112, 255, .15);
|
||||||
|
--text-1: var(--text-primary);
|
||||||
|
--text-2: var(--text-secondary);
|
||||||
|
--text-3: var(--text-tertiary);
|
||||||
|
--warn: var(--warning-color);
|
||||||
|
--warn-soft: var(--warning-soft);
|
||||||
--backdrop: rgba(17, 24, 39, .48);
|
--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);
|
||||||
@@ -126,18 +141,32 @@
|
|||||||
--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: 10.5px;
|
--font-size-aux: 11.5px;
|
||||||
--font-size-caption: 11px;
|
--font-size-caption: 12.5px;
|
||||||
--font-size-label: 12px;
|
--font-size-label: 13px;
|
||||||
--font-size-table: 12.5px;
|
--font-size-table: 13.5px;
|
||||||
--font-size-body: 13px;
|
--font-size-body: 14px;
|
||||||
--font-size-card-title: 14px;
|
--font-size-chat: 14px;
|
||||||
--font-size-page-title: 17px;
|
--font-size-card-title: 15px;
|
||||||
--font-size-metric: 22px;
|
--font-size-page-title: 18px;
|
||||||
|
--font-size-metric: 24px;
|
||||||
|
--font-size-hero: 30px;
|
||||||
|
--fs-aux: var(--font-size-aux);
|
||||||
|
--fs-caption: var(--font-size-caption);
|
||||||
|
--fs-label: var(--font-size-label);
|
||||||
|
--fs-table: var(--font-size-table);
|
||||||
|
--fs-body: var(--font-size-body);
|
||||||
|
--fs-chat: var(--font-size-chat);
|
||||||
|
--fs-card-title: var(--font-size-card-title);
|
||||||
|
--fs-page-title: var(--font-size-page-title);
|
||||||
|
--fs-metric: var(--font-size-metric);
|
||||||
|
--fs-hero: var(--font-size-hero);
|
||||||
--font-weight-regular: 400;
|
--font-weight-regular: 400;
|
||||||
--font-weight-medium: 500;
|
--font-weight-medium: 500;
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
@@ -147,11 +176,20 @@
|
|||||||
--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);
|
||||||
@@ -219,13 +257,14 @@
|
|||||||
--dragon-profile-weight-strong: 750;
|
--dragon-profile-weight-strong: 750;
|
||||||
--dragon-profile-weight-semibold: 600;
|
--dragon-profile-weight-semibold: 600;
|
||||||
|
|
||||||
--mentor-directory-width: 280px;
|
--mentor-directory-width: 300px;
|
||||||
--mentor-profile-width: 272px;
|
--mentor-pane-header-height: 56px;
|
||||||
--mentor-pane-header-height: 58px;
|
--mentor-avatar-size: 40px;
|
||||||
--mentor-avatar-size: 38px;
|
--mentor-message-avatar-size: 36px;
|
||||||
|
--mentor-chat-avatar-size: 38px;
|
||||||
--mentor-profile-avatar-size: 68px;
|
--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;
|
||||||
@@ -313,45 +352,64 @@
|
|||||||
--r2-radius: var(--size-radius-lg);
|
--r2-radius: var(--size-radius-lg);
|
||||||
--r2-shadow: var(--elevation-card);
|
--r2-shadow: var(--elevation-card);
|
||||||
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", "PingFang SC", "Microsoft YaHei UI", sans-serif;
|
font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="dark"] {
|
:root[data-theme="dark"] {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--canvas: #121416;
|
--canvas: #141519;
|
||||||
--surface: #1b1e21;
|
--header-bg: #1c1e23;
|
||||||
--surface-muted: #202428;
|
--surface: #232529;
|
||||||
--surface-subtle: #24282d;
|
--surface-muted: #2a2d33;
|
||||||
|
--surface-subtle: #202329;
|
||||||
--surface-canvas: var(--canvas);
|
--surface-canvas: var(--canvas);
|
||||||
--surface-raised: var(--surface);
|
--surface-raised: var(--surface);
|
||||||
--surface-sunken: #16191c;
|
--surface-sunken: #191b1f;
|
||||||
--surface-hover: #24282d;
|
--surface-hover: #2a2d33;
|
||||||
--surface-selected: #23364a;
|
--surface-selected: #2b3b58;
|
||||||
--surface-overlay: #24282d;
|
--surface-overlay: #232529;
|
||||||
--border: #343a40;
|
--hover: var(--surface-hover);
|
||||||
--border-strong: #474f57;
|
--selected: var(--surface-selected);
|
||||||
--border-subtle: #2a2f34;
|
--border: #2b2e34;
|
||||||
|
--border-strong: #3a3e47;
|
||||||
|
--border-subtle: #2b2e34;
|
||||||
--text-primary: #e8eaed;
|
--text-primary: #e8eaed;
|
||||||
--text-secondary: #adb5bd;
|
--text-secondary: #a9adb3;
|
||||||
--text-tertiary: #7f8993;
|
--text-tertiary: #7c828a;
|
||||||
|
--text-1: var(--text-primary);
|
||||||
|
--text-2: var(--text-secondary);
|
||||||
|
--text-3: var(--text-tertiary);
|
||||||
--text-inverse: #ffffff;
|
--text-inverse: #ffffff;
|
||||||
--action: #6ca8e8;
|
--action: #5b8def;
|
||||||
--action-hover: #8bbcf0;
|
--action-hover: #7ba5f5;
|
||||||
--action-soft: #23364a;
|
--action-press: #3465c4;
|
||||||
--market-up: #f06d73;
|
--action-soft: #2b3b58;
|
||||||
--market-up-soft: #40262a;
|
--accent: var(--action);
|
||||||
|
--accent-hover: var(--action-hover);
|
||||||
|
--accent-press: var(--action-press);
|
||||||
|
--accent-soft: var(--action-soft);
|
||||||
|
--market-up: #f26762;
|
||||||
|
--market-up-soft: #3d2829;
|
||||||
--market-down: #43bc8a;
|
--market-down: #43bc8a;
|
||||||
--market-down-soft: #1d382f;
|
--market-down-soft: #22362c;
|
||||||
--warning-color: #e2ad58;
|
--warning-color: #e2ad58;
|
||||||
--warning-soft: #3d3220;
|
--warning-soft: #3d3220;
|
||||||
--control-surface: #202428;
|
--warn: var(--warning-color);
|
||||||
--control-hover: #2a2f34;
|
--warn-soft: var(--warning-soft);
|
||||||
--control-border: #474f57;
|
--control-surface: #232529;
|
||||||
--table-header: #202428;
|
--control-hover: #2a2d33;
|
||||||
--table-hover: #242f39;
|
--control-border: #3a3e47;
|
||||||
--table-selected: #23364a;
|
--table-header: #202329;
|
||||||
--focus-ring: rgba(108, 168, 232, .22);
|
--table-hover: #2a2d33;
|
||||||
|
--table-selected: #2b3b58;
|
||||||
|
--focus-ring: rgba(91, 141, 239, .25);
|
||||||
|
--elevation-card: 0 1px 2px rgba(0, 0, 0, .28);
|
||||||
|
--elevation-soft: 0 1px 2px rgba(0, 0, 0, .28);
|
||||||
|
--elevation-raised: 0 1px 2px rgba(0, 0, 0, .30), 0 8px 24px rgba(0, 0, 0, .35);
|
||||||
|
--elevation-float: 0 12px 32px rgba(0, 0, 0, .46);
|
||||||
|
--shadow-card: var(--elevation-card);
|
||||||
|
--shadow-raised: var(--elevation-raised);
|
||||||
--backdrop: rgba(0, 0, 0, .66);
|
--backdrop: rgba(0, 0, 0, .66);
|
||||||
--primary: var(--action);
|
--primary: var(--action);
|
||||||
--primary-hover: var(--action-hover);
|
--primary-hover: var(--action-hover);
|
||||||
@@ -467,3 +525,16 @@
|
|||||||
--shadow-soft: 0 1px 2px rgba(0, 0, 0, .28), 0 8px 24px rgba(0, 0, 0, .16);
|
--shadow-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+783
-29
@@ -483,7 +483,7 @@ async function mockApplication(page, authSession = session(), options = {}) {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
} else if (url.pathname === "/api/mentors/setup") {
|
} else if (url.pathname === "/api/mentors/setup") {
|
||||||
payload = { trade_date: "20260722", mentors: mentorDirectory(authSession.user.role) };
|
payload = { trade_date: "20260722", mentors: options.mentors || mentorDirectory(authSession.user.role) };
|
||||||
} else if (url.pathname === "/api/mentors/chat") {
|
} else if (url.pathname === "/api/mentors/chat") {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -510,12 +510,64 @@ async function mockApplication(page, authSession = session(), options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openHeaderCommandMenu(page) {
|
||||||
|
const menu = page.locator("#headerCommandGroup");
|
||||||
|
if (await menu.isVisible()) return;
|
||||||
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await expect(menu).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
function namedSession(username, role = "admin", subscribed = true) {
|
||||||
|
const auth = session(role, subscribed);
|
||||||
|
auth.user.username = username;
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setColorTheme(page, theme) {
|
||||||
|
const wanted = theme === "night" ? "dark" : "light";
|
||||||
|
const current = await page.locator("html").getAttribute("data-theme");
|
||||||
|
if (current === wanted) return;
|
||||||
|
await page.locator("#themeToggle").click();
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", wanted);
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureAccountName() {
|
||||||
|
const name = document.querySelector("#accountName");
|
||||||
|
const button = document.querySelector("#accountButton");
|
||||||
|
const badges = document.querySelector("#accountRoleBadges");
|
||||||
|
const adminLabel = document.querySelector("#accountAdminBadge > span");
|
||||||
|
const vipLabel = document.querySelector("#accountVipLabel");
|
||||||
|
const box = (node) => node ? node.getBoundingClientRect() : null;
|
||||||
|
const visible = (node) => {
|
||||||
|
if (!node || node.hidden) return false;
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
if (style.display === "none" || style.visibility === "hidden") return false;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
};
|
||||||
|
const buttonBox = box(button);
|
||||||
|
const badgeBox = box(badges);
|
||||||
|
return {
|
||||||
|
text: name ? String(name.textContent || "") : "",
|
||||||
|
clientWidth: name ? name.clientWidth : 0,
|
||||||
|
scrollWidth: name ? name.scrollWidth : 0,
|
||||||
|
fits: name ? name.clientWidth >= name.scrollWidth : false,
|
||||||
|
buttonFits: button ? button.clientWidth >= button.scrollWidth : false,
|
||||||
|
stacked: Boolean(badgeBox && buttonBox && buttonBox.top >= badgeBox.bottom - 1),
|
||||||
|
adminLabelVisible: visible(adminLabel),
|
||||||
|
vipLabelVisible: visible(vipLabel),
|
||||||
|
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test("admin shell opens every primary workspace and global search", async ({ page }) => {
|
test("admin shell opens every primary workspace and global search", async ({ page }) => {
|
||||||
await mockApplication(page);
|
await mockApplication(page);
|
||||||
await page.goto("/index.html");
|
await page.goto("/index.html");
|
||||||
await expect(page.locator("#authGate")).toBeHidden();
|
await expect(page.locator("#authGate")).toBeHidden();
|
||||||
|
await openHeaderCommandMenu(page);
|
||||||
await expect(page.locator("#settingsButton")).toBeVisible();
|
await expect(page.locator("#settingsButton")).toBeVisible();
|
||||||
await expect(page.locator("#syncButton")).toBeVisible();
|
await expect(page.locator("#syncButton")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
await page.locator("#alertButton").click();
|
await page.locator("#alertButton").click();
|
||||||
await expect(page.locator("#alertsDialog")).toBeVisible();
|
await expect(page.locator("#alertsDialog")).toBeVisible();
|
||||||
await page.locator("#closeAlertsDialog").click();
|
await page.locator("#closeAlertsDialog").click();
|
||||||
@@ -526,7 +578,7 @@ test("admin shell opens every primary workspace and global search", async ({ pag
|
|||||||
const views = [
|
const views = [
|
||||||
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
|
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
|
||||||
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView", "screenerView",
|
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView", "screenerView",
|
||||||
"mentorView", "heavenView", "reviewWorkspaceView",
|
"heavenView", "reviewWorkspaceView", "mentorView",
|
||||||
];
|
];
|
||||||
for (const view of views) {
|
for (const view of views) {
|
||||||
await page.locator(`[data-view="${view}"]`).first().click();
|
await page.locator(`[data-view="${view}"]`).first().click();
|
||||||
@@ -567,7 +619,7 @@ test("every primary workspace shares the canonical desktop shell geometry", asyn
|
|||||||
const views = [
|
const views = [
|
||||||
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
|
"auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView",
|
||||||
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView",
|
"performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView",
|
||||||
"screenerView", "mentorView", "heavenView", "reviewWorkspaceView",
|
"screenerView", "heavenView", "reviewWorkspaceView",
|
||||||
];
|
];
|
||||||
let reference = null;
|
let reference = null;
|
||||||
for (const view of views) {
|
for (const view of views) {
|
||||||
@@ -589,6 +641,7 @@ test("manual refresh stays in place without reopening the full-page loader", asy
|
|||||||
await page.goto("/index.html");
|
await page.goto("/index.html");
|
||||||
await expect(page.locator("#loadingOverlay")).toBeHidden();
|
await expect(page.locator("#loadingOverlay")).toBeHidden();
|
||||||
|
|
||||||
|
await openHeaderCommandMenu(page);
|
||||||
await page.locator("#refreshButton").click();
|
await page.locator("#refreshButton").click();
|
||||||
|
|
||||||
await expect(page.locator("#refreshButton")).toBeDisabled();
|
await expect(page.locator("#refreshButton")).toBeDisabled();
|
||||||
@@ -2805,54 +2858,122 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
|
|||||||
await page.goto("/index.html");
|
await page.goto("/index.html");
|
||||||
await page.locator('[data-view="mentorView"]').first().click();
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
|
||||||
await expect(page.locator("#mentorView .mentor-sidebar .mentor-evidence-filters")).toBeVisible();
|
await expect(page.locator("#mentorView .mentor-sidebar .mentor-directory-tools")).toBeVisible();
|
||||||
|
await expect(page.locator("#mentorFilterToggle")).toBeVisible();
|
||||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
|
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
|
||||||
await expect(page.locator('#mentorList [data-mentor-id="private-owner"] .mentor-badge.private')).toContainText("仅自己");
|
await expect(page.locator('#mentorList [data-mentor-id="private-owner"] .mentor-badge.private')).toContainText("仅自己");
|
||||||
await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A");
|
await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A级");
|
||||||
await expect(page.locator('#mentorList [data-mentor-id="source-c"] .mentor-badge.quality')).toHaveCount(0);
|
await expect(page.locator('#mentorList [data-mentor-id="source-c"] .mentor-badge.quality')).toHaveCount(0);
|
||||||
|
|
||||||
await page.locator("#mentorSearchInput").fill("行为推演");
|
await page.locator("#mentorSearchInput").fill("行为推演");
|
||||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(1);
|
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(1);
|
||||||
await expect(page.locator("#mentorCount")).toHaveText("1 / 22 位");
|
await expect(page.locator("#mentorCount")).toHaveText("1 / 22 位");
|
||||||
await page.locator("#mentorSearchInput").fill("");
|
await page.locator("#mentorSearchInput").fill("");
|
||||||
|
await page.locator("#mentorFilterToggle").click();
|
||||||
await page.locator('[data-mentor-grade="B"]').click();
|
await page.locator('[data-mentor-grade="B"]').click();
|
||||||
|
await expect(page.locator("#mentorFilterOptions [data-mentor-grade].active")).toHaveText("B级");
|
||||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7);
|
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7);
|
||||||
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
|
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
|
||||||
await expect(page.locator("#activeMentorName")).toHaveText("多源老师");
|
await expect(page.locator("#activeMentorName")).toHaveText("多源老师");
|
||||||
await expect(page.locator("#activeMentorBadges")).toHaveText("B");
|
await expect(page.locator("#activeMentorStatus")).toContainText("确认之后再行动");
|
||||||
await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
|
|
||||||
const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
|
const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
|
||||||
const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();
|
const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();
|
||||||
const mentorProfile = await page.locator("#mentorView .mentor-profile-panel").boundingBox();
|
|
||||||
const mentorInput = await page.locator("#mentorQuestion").boundingBox();
|
const mentorInput = await page.locator("#mentorQuestion").boundingBox();
|
||||||
expect(Math.abs(mentorLibrary.width - 280)).toBeLessThanOrEqual(1);
|
const mentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
||||||
expect(Math.abs(mentorProfile.width - 272)).toBeLessThanOrEqual(1);
|
expect(Math.abs(mentorLibrary.width - 300)).toBeLessThanOrEqual(1);
|
||||||
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
|
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
|
||||||
expect(Math.abs(mentorProfile.x - (mentorChat.x + mentorChat.width))).toBeLessThanOrEqual(1);
|
expect(Math.abs(mentorChat.x + mentorChat.width - (mentorLayout.x + mentorLayout.width))).toBeLessThanOrEqual(1);
|
||||||
expect(mentorInput.height).toBeGreaterThanOrEqual(48);
|
const composerForm = await page.locator("#mentorView .mentor-chat-form").boundingBox();
|
||||||
|
expect(composerForm.height).toBeGreaterThanOrEqual(81);
|
||||||
|
expect(composerForm.height).toBeLessThanOrEqual(91);
|
||||||
|
expect(mentorInput.height).toBeGreaterThanOrEqual(24);
|
||||||
|
expect(mentorInput.height).toBeLessThanOrEqual(34);
|
||||||
|
const searchField = await page.locator("#mentorView .mentor-search-field").boundingBox();
|
||||||
|
expect(searchField.width).toBeGreaterThan(160);
|
||||||
|
await expect(page.locator("#mentorSearchInput")).toHaveAttribute("placeholder", "搜索联系人或标签");
|
||||||
|
await expect(page.locator(".module-nav")).toBeVisible();
|
||||||
|
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "mentorView");
|
||||||
|
await expect(page.locator(".market-tape")).toBeVisible();
|
||||||
|
await expect(page.locator("#tradeDate")).toBeVisible();
|
||||||
|
await expect(page.locator(".overview-strip")).toBeVisible();
|
||||||
|
await expect(page.locator(".status-bar")).toBeVisible();
|
||||||
|
await expect(page.locator("#themeToggle")).toBeVisible();
|
||||||
|
await expect(page.locator("#mentorView .mentor-page-title h2")).toHaveText("问师");
|
||||||
|
await expect(page.locator("#mentorPageSubtitle")).toContainText("数据日期");
|
||||||
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
|
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
|
||||||
await page.setViewportSize({ width: 1920, height: 947 });
|
await page.setViewportSize({ width: 1920, height: 947 });
|
||||||
const expandedMentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
const expandedMentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
||||||
const statusBar = await page.locator(".status-bar").boundingBox();
|
const footerBar = await page.locator("#mentorView .mentor-workspace-footer").boundingBox();
|
||||||
const lowerGap = statusBar.y - (expandedMentorLayout.y + expandedMentorLayout.height);
|
const shellStatusBar = await page.locator(".status-bar").boundingBox();
|
||||||
expect(lowerGap).toBeGreaterThanOrEqual(0);
|
expect(Math.abs(shellStatusBar.y + shellStatusBar.height - 947)).toBeLessThanOrEqual(1);
|
||||||
expect(lowerGap).toBeLessThanOrEqual(16);
|
expect(footerBar.y + footerBar.height).toBeLessThanOrEqual(shellStatusBar.y);
|
||||||
await page.locator("#overviewToggle").click();
|
expect(Math.abs(expandedMentorLayout.y + expandedMentorLayout.height - footerBar.y)).toBeLessThanOrEqual(1);
|
||||||
const openOverviewLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
|
||||||
const openOverviewGap = statusBar.y - (openOverviewLayout.y + openOverviewLayout.height);
|
|
||||||
expect(openOverviewGap).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(openOverviewGap).toBeLessThanOrEqual(16);
|
|
||||||
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
|
expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mentor stays inside the project shell and switching pages leaves no residue", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await mockApplication(page, session("admin", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
|
||||||
|
await expect(page.locator("#mentorView")).toHaveClass(/active-view/);
|
||||||
|
await expect(page.locator(".module-nav")).toBeVisible();
|
||||||
|
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "mentorView");
|
||||||
|
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
|
||||||
|
await expect(page.locator("#mentorView .mentor-page-title h2")).toHaveText("问师");
|
||||||
|
|
||||||
|
await page.locator('[data-view="limitPool"]').first().click();
|
||||||
|
await expect(page.locator("#limitPool")).toHaveClass(/active-view/);
|
||||||
|
await expect(page.locator("#mentorView")).not.toHaveClass(/active-view/);
|
||||||
|
await expect(page.locator("#mentorView")).not.toBeVisible();
|
||||||
|
await expect(page.locator(".module-nav")).toBeVisible();
|
||||||
|
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "limitPool");
|
||||||
|
|
||||||
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
await expect(page.locator("#mentorView")).toHaveClass(/active-view/);
|
||||||
|
await expect(page.locator(".module-nav")).toBeVisible();
|
||||||
|
await expect(page.locator(".module-tab.active")).toHaveAttribute("data-view", "mentorView");
|
||||||
|
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
|
||||||
|
await expect(page.locator("#mentorPageSubtitle")).toContainText("数据日期");
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mentor keeps two columns and opens the profile floating dialog on desktop", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await mockApplication(page, session("admin", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
|
||||||
|
|
||||||
|
await expect(page.locator("#mentorView .mentor-profile-panel")).toHaveCount(0);
|
||||||
|
const mentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
||||||
|
const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox();
|
||||||
|
const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox();
|
||||||
|
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(mentorChat.x + mentorChat.width - (mentorLayout.x + mentorLayout.width))).toBeLessThanOrEqual(1);
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
|
await page.locator("#mentorProfileButton").click();
|
||||||
|
await expect(page.locator("#mentorProfileDialog")).toBeVisible();
|
||||||
|
await expect(page.locator("#mentorProfileDialogName")).toHaveText("多源老师");
|
||||||
|
await expect(page.locator("#mentorProfileDialogEvidence")).toHaveText("公开访谈与多源材料");
|
||||||
|
await page.locator('[data-mentor-dialog-close="mentorProfileDialog"]').click();
|
||||||
|
await expect(page.locator("#mentorProfileDialog")).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("mentor pins, custom order and streamed replies work together", async ({ page }) => {
|
test("mentor pins, custom order and streamed replies work together", async ({ page }) => {
|
||||||
await mockApplication(page, session("admin", true));
|
await mockApplication(page, session("admin", true));
|
||||||
await page.goto("/index.html");
|
await page.goto("/index.html");
|
||||||
await page.locator('[data-view="mentorView"]').first().click();
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
|
||||||
await page.locator('[data-mentor-pin="source-c"]').click();
|
await page.locator('[data-mentor-id="source-c"]').click();
|
||||||
|
await page.locator("#mentorPinButton").click();
|
||||||
await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-c");
|
await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-c");
|
||||||
await page.locator('[data-mentor-pin="source-b"]').click();
|
await expect(page.locator('#mentorList [data-mentor-card="source-c"] .mentor-badge.pinned')).toHaveText("置顶");
|
||||||
|
await page.locator('[data-mentor-id="source-b"]').click();
|
||||||
|
await page.locator("#mentorPinButton").click();
|
||||||
await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-b");
|
await expect(page.locator("#mentorList [data-mentor-card]").first()).toHaveAttribute("data-mentor-card", "source-b");
|
||||||
|
|
||||||
await page.locator("#mentorSortToggle").click();
|
await page.locator("#mentorSortToggle").click();
|
||||||
@@ -2869,10 +2990,16 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
await expect(page.locator("#mentorMessages .assistant-stream-caret")).toHaveCount(0);
|
await expect(page.locator("#mentorMessages .assistant-stream-caret")).toHaveCount(0);
|
||||||
const followUps = answer.locator("[data-mentor-follow-up]");
|
const followUps = answer.locator("[data-mentor-follow-up]");
|
||||||
await expect(followUps).toHaveCount(3);
|
await expect(followUps).toHaveCount(3);
|
||||||
|
await expect(followUps.first().locator(".lucide")).toHaveCount(0);
|
||||||
|
await expect(answer.locator("small")).toHaveCount(0);
|
||||||
const messageCount = await page.locator("#mentorMessages .mentor-message").count();
|
const messageCount = await page.locator("#mentorMessages .mentor-message").count();
|
||||||
await followUps.first().click();
|
await followUps.first().click();
|
||||||
await expect(page.locator("#mentorQuestion")).toHaveValue("哪些信号代表确认?");
|
await expect(page.locator("#mentorQuestion")).toHaveValue("哪些信号代表确认?");
|
||||||
await expect(page.locator("#mentorMessages .mentor-message")).toHaveCount(messageCount);
|
await expect(page.locator("#mentorMessages .mentor-message")).toHaveCount(messageCount);
|
||||||
|
const userLabel = page.locator("#mentorMessages .mentor-message.user .mentor-message-label").first();
|
||||||
|
await expect(userLabel).not.toContainText("我 ·");
|
||||||
|
const assistantBody = await page.locator("#mentorMessages .mentor-message.assistant .mentor-message-body").last().boundingBox();
|
||||||
|
expect(assistantBody.width).toBeLessThanOrEqual(900);
|
||||||
await page.locator("#themeToggle").click();
|
await page.locator("#themeToggle").click();
|
||||||
const userMessage = page.locator("#mentorMessages .mentor-message.user");
|
const userMessage = page.locator("#mentorMessages .mentor-message.user");
|
||||||
const darkUserMessageStyle = await userMessage.evaluate((element) => ({
|
const darkUserMessageStyle = await userMessage.evaluate((element) => ({
|
||||||
@@ -2883,14 +3010,13 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
}));
|
}));
|
||||||
expect(darkUserMessageStyle.background).not.toBe("rgb(255, 255, 255)");
|
expect(darkUserMessageStyle.background).not.toBe("rgb(255, 255, 255)");
|
||||||
expect(darkUserMessageStyle.border).not.toBe("rgb(255, 255, 255)");
|
expect(darkUserMessageStyle.border).not.toBe("rgb(255, 255, 255)");
|
||||||
expect(darkUserMessageStyle.contentBackground).toBe("rgb(35, 54, 74)");
|
expect(darkUserMessageStyle.contentBackground).toBe("rgb(53, 89, 140)");
|
||||||
expect(darkUserMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
|
expect(darkUserMessageStyle.contentColor).toBe("rgb(234, 241, 251)");
|
||||||
const darkMessageStyle = await answer.evaluate((element) => {
|
const darkMessageStyle = await answer.evaluate((element) => {
|
||||||
const style = getComputedStyle(element);
|
const style = getComputedStyle(element);
|
||||||
const content = element.querySelector(".mentor-message-content");
|
const content = element.querySelector(".mentor-message-content");
|
||||||
const heading = element.querySelector(".mentor-answer-heading");
|
const heading = element.querySelector(".mentor-answer-heading");
|
||||||
const label = element.querySelector(".mentor-message-label");
|
const label = element.querySelector(".mentor-message-label");
|
||||||
const meta = element.querySelector("small");
|
|
||||||
return {
|
return {
|
||||||
background: style.backgroundColor,
|
background: style.backgroundColor,
|
||||||
border: style.borderTopColor,
|
border: style.borderTopColor,
|
||||||
@@ -2898,7 +3024,6 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
contentColor: getComputedStyle(content).color,
|
contentColor: getComputedStyle(content).color,
|
||||||
headingColor: getComputedStyle(heading).color,
|
headingColor: getComputedStyle(heading).color,
|
||||||
labelColor: getComputedStyle(label).color,
|
labelColor: getComputedStyle(label).color,
|
||||||
metaColor: getComputedStyle(meta).color,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
expect(darkMessageStyle.background).not.toBe("rgb(255, 255, 255)");
|
expect(darkMessageStyle.background).not.toBe("rgb(255, 255, 255)");
|
||||||
@@ -2906,8 +3031,119 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
expect(darkMessageStyle.shadow).toBe("none");
|
expect(darkMessageStyle.shadow).toBe("none");
|
||||||
expect(darkMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
|
expect(darkMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
|
||||||
expect(darkMessageStyle.headingColor).toBe("rgb(232, 234, 237)");
|
expect(darkMessageStyle.headingColor).toBe("rgb(232, 234, 237)");
|
||||||
expect(darkMessageStyle.labelColor).toBe("rgb(127, 137, 147)");
|
expect(darkMessageStyle.labelColor).toBe("rgb(124, 130, 138)");
|
||||||
expect(darkMessageStyle.metaColor).toBe("rgb(127, 137, 147)");
|
});
|
||||||
|
|
||||||
|
test("mentor avatars map per contact id and follow the final day/night palette", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
const mentors = [
|
||||||
|
{
|
||||||
|
id: "xiaobai-perspective", name: "小白", description: "个人复盘记录蒸馏", tagline: "复盘自己",
|
||||||
|
focus: ["个人复盘"], evidence: { grade: "A", label: "私有原始语料", note: "私人语料" }, quality: {}, private: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kobe92-perspective", name: "52科比", description: "情绪周期心法", tagline: "先看周期",
|
||||||
|
focus: ["情绪周期"], evidence: { grade: "A", label: "心法文本", note: "心法" }, quality: {}, private: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "beijingchaojia-perspective", name: "北京炒家", description: "实盘记录", tagline: "实盘为先",
|
||||||
|
focus: ["实盘"], evidence: { grade: "A", label: "实盘资料", note: "实盘" }, quality: {}, private: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "chaojiyangjia-perspective", name: "炒股养家", description: "原始语料", tagline: "情绪为上",
|
||||||
|
focus: ["情绪"], evidence: { grade: "B", label: "原始语料", note: "语料" }, quality: {}, private: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await mockApplication(page, session("admin", true), { mentors });
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
|
||||||
|
const expectations = {
|
||||||
|
"xiaobai-perspective": ["mentor-avatar-tone-violet", "rgb(238, 236, 253)", "rgb(106, 92, 245)"],
|
||||||
|
"kobe92-perspective": ["mentor-avatar-tone-blue", "rgb(227, 240, 255)", "rgb(51, 112, 255)"],
|
||||||
|
"beijingchaojia-perspective": ["mentor-avatar-tone-green", "rgb(221, 245, 229)", "rgb(46, 164, 79)"],
|
||||||
|
"chaojiyangjia-perspective": ["mentor-avatar-tone-orange", "rgb(253, 238, 221)", "rgb(217, 122, 27)"],
|
||||||
|
};
|
||||||
|
for (const [id, [tone, background, ink]] of Object.entries(expectations)) {
|
||||||
|
const avatar = page.locator(`#mentorList [data-mentor-id="${id}"] .mentor-avatar`);
|
||||||
|
await expect(avatar).toHaveClass(new RegExp(tone));
|
||||||
|
const palette = await avatar.evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return { background: style.backgroundColor, color: style.color };
|
||||||
|
});
|
||||||
|
expect(palette.background).toBe(background);
|
||||||
|
expect(palette.color).toBe(ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.locator('[data-mentor-id="kobe92-perspective"]').click();
|
||||||
|
await expect(page.locator("#activeMentorAvatar")).toHaveClass(/mentor-avatar-tone-blue/);
|
||||||
|
|
||||||
|
await page.locator("#themeToggle").click();
|
||||||
|
const nightPalette = await page.locator('#mentorList [data-mentor-id="beijingchaojia-perspective"] .mentor-avatar')
|
||||||
|
.evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return { background: style.backgroundColor, color: style.color };
|
||||||
|
});
|
||||||
|
expect(nightPalette.background).toBe("rgb(35, 74, 56)");
|
||||||
|
expect(nightPalette.color).toBe("rgb(95, 206, 143)");
|
||||||
|
await expect(page.locator("#activeMentorAvatar")).toHaveClass(/mentor-avatar-tone-blue/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mentor floating dialogs open centered, save notes per account, and keep composer one-line", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await mockApplication(page, session("admin", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.locator('[data-view="mentorView"]').first().click();
|
||||||
|
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
|
||||||
|
|
||||||
|
await page.locator("#mentorNoteButton").click();
|
||||||
|
await expect(page.locator("#mentorNoteDialog")).toBeVisible();
|
||||||
|
const noteDialog = await page.locator("#mentorNoteDialog").boundingBox();
|
||||||
|
expect(noteDialog.width).toBeLessThanOrEqual(400);
|
||||||
|
expect(Math.abs(noteDialog.x + noteDialog.width / 2 - 720)).toBeLessThanOrEqual(3);
|
||||||
|
expect(Math.abs(noteDialog.y + noteDialog.height / 2 - 450)).toBeLessThanOrEqual(3);
|
||||||
|
await page.locator("#mentorNoteInput").fill("多源老师:确认后再行动");
|
||||||
|
await expect(page.locator("#mentorNoteInput")).toHaveValue("多源老师:确认后再行动");
|
||||||
|
await page.locator('[data-mentor-dialog-close="mentorNoteDialog"]').click();
|
||||||
|
await expect(page.locator("#mentorNoteDialog")).not.toBeVisible();
|
||||||
|
|
||||||
|
await page.locator("#mentorNoteButton").click();
|
||||||
|
await expect(page.locator("#mentorNoteInput")).toHaveValue("多源老师:确认后再行动");
|
||||||
|
await page.locator('[data-mentor-dialog-close="mentorNoteDialog"]').click();
|
||||||
|
|
||||||
|
await page.locator("#mentorProfileButton").click();
|
||||||
|
await expect(page.locator("#mentorProfileDialog")).toBeVisible();
|
||||||
|
await expect(page.locator("#mentorProfileDialogName")).toHaveText("多源老师");
|
||||||
|
await expect(page.locator("#mentorProfileDialogEvidence")).toHaveText("公开访谈与多源材料");
|
||||||
|
const profileDialog = await page.locator("#mentorProfileDialog").boundingBox();
|
||||||
|
expect(profileDialog.width).toBeLessThanOrEqual(400);
|
||||||
|
expect(Math.abs(profileDialog.x + profileDialog.width / 2 - 720)).toBeLessThanOrEqual(3);
|
||||||
|
await page.locator('[data-mentor-dialog-close="mentorProfileDialog"]').click();
|
||||||
|
await expect(page.locator("#mentorProfileDialog")).not.toBeVisible();
|
||||||
|
|
||||||
|
const disclaimerStyle = await page.locator("#mentorView .mentor-disclaimer").evaluate((el) => ({
|
||||||
|
align: getComputedStyle(el).textAlign,
|
||||||
|
}));
|
||||||
|
expect(disclaimerStyle.align).toBe("center");
|
||||||
|
|
||||||
|
const composerHint = page.locator("#mentorView .mentor-composer-hint");
|
||||||
|
await expect(composerHint).toHaveCount(1);
|
||||||
|
await expect(composerHint).toBeVisible();
|
||||||
|
|
||||||
|
const composerInput = page.locator("#mentorQuestion");
|
||||||
|
const oneLineHeight = (await composerInput.boundingBox()).height;
|
||||||
|
expect(oneLineHeight).toBeGreaterThanOrEqual(24);
|
||||||
|
expect(oneLineHeight).toBeLessThanOrEqual(34);
|
||||||
|
await composerInput.fill("第一行\n第二行");
|
||||||
|
const grownHeight = (await composerInput.boundingBox()).height;
|
||||||
|
expect(grownHeight).toBeGreaterThan(oneLineHeight + 8);
|
||||||
|
await composerInput.press("Shift+Enter");
|
||||||
|
await composerInput.type("第三行");
|
||||||
|
await composerInput.fill("只发送这一行");
|
||||||
|
await page.locator("#sendMentorQuestion").click();
|
||||||
|
await expect(page.locator("#mentorMessages .mentor-message.user").last()).toContainText("只发送这一行");
|
||||||
|
const resetHeight = (await composerInput.boundingBox()).height;
|
||||||
|
expect(resetHeight).toBeLessThanOrEqual(34);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("global dialogs share the stage 18 geometry without changing account or admin access", async ({ page }) => {
|
test("global dialogs share the stage 18 geometry without changing account or admin access", async ({ page }) => {
|
||||||
@@ -2951,6 +3187,7 @@ test("global dialogs share the stage 18 geometry without changing account or adm
|
|||||||
expect(assistantGeometry.contentDisplay).toBe("flex");
|
expect(assistantGeometry.contentDisplay).toBe("flex");
|
||||||
await page.locator("#closeAssistantDialog").click();
|
await page.locator("#closeAssistantDialog").click();
|
||||||
|
|
||||||
|
await openHeaderCommandMenu(page);
|
||||||
await page.locator("#accountButton").click();
|
await page.locator("#accountButton").click();
|
||||||
await page.locator('[data-account-panel="membership"]').click();
|
await page.locator('[data-account-panel="membership"]').click();
|
||||||
await expect(page.locator("#settingsDialog")).toHaveAttribute("aria-labelledby", "accountDialogTitle");
|
await expect(page.locator("#settingsDialog")).toHaveAttribute("aria-labelledby", "accountDialogTitle");
|
||||||
@@ -2961,6 +3198,7 @@ test("global dialogs share the stage 18 geometry without changing account or adm
|
|||||||
expect(Math.abs(accountBox.y + accountBox.height / 2 - 450)).toBeLessThanOrEqual(2);
|
expect(Math.abs(accountBox.y + accountBox.height / 2 - 450)).toBeLessThanOrEqual(2);
|
||||||
await page.locator("#closeSettingsDialog").click();
|
await page.locator("#closeSettingsDialog").click();
|
||||||
|
|
||||||
|
await openHeaderCommandMenu(page);
|
||||||
await page.locator("#settingsButton").click();
|
await page.locator("#settingsButton").click();
|
||||||
await expect(page.locator("#adminDialog")).toHaveAttribute("aria-labelledby", "adminDialogTitle");
|
await expect(page.locator("#adminDialog")).toHaveAttribute("aria-labelledby", "adminDialogTitle");
|
||||||
await expect(page.locator("#adminSectionSelect")).toBeVisible();
|
await expect(page.locator("#adminSectionSelect")).toBeVisible();
|
||||||
@@ -2980,3 +3218,519 @@ test("global dialogs share the stage 18 geometry without changing account or adm
|
|||||||
expect(mobileGeometry.width).toBeLessThanOrEqual(375);
|
expect(mobileGeometry.width).toBeLessThanOrEqual(375);
|
||||||
expect(mobileGeometry.documentOverflow).toBeLessThanOrEqual(1);
|
expect(mobileGeometry.documentOverflow).toBeLessThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("merged header keeps date, detail fields and 13 pool columns after dual-review rework", async ({ page }) => {
|
||||||
|
const previousLimits = dashboard.limits;
|
||||||
|
dashboard.limits = [{
|
||||||
|
code: "002141",
|
||||||
|
name: "贤丰控股",
|
||||||
|
streak: 5,
|
||||||
|
change: 10.02,
|
||||||
|
price: 12.48,
|
||||||
|
sector: "电子元件",
|
||||||
|
first_time: "09:31",
|
||||||
|
last_time: "10:18",
|
||||||
|
open_times: 1,
|
||||||
|
turnover_rate: 18.42,
|
||||||
|
amount_billion: 12.6,
|
||||||
|
seal_amount_million: 8200,
|
||||||
|
reason: "板块龙头连板打开空间",
|
||||||
|
}];
|
||||||
|
await mockApplication(page, session("admin", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
dashboard.limits = previousLimits;
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
await expect(page.locator("#currentPageSubtitle")).toContainText("市场复盘 ·");
|
||||||
|
await expect(page.locator("#headerMenuButton")).toBeHidden();
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeVisible();
|
||||||
|
await expect(page.locator("#themeToggle")).toBeVisible();
|
||||||
|
await expect(page.locator("#refreshButton")).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('[data-view="limitPool"]').first().click();
|
||||||
|
await expect(page.locator("#currentPageSubtitle")).toContainText("市场复盘 ·");
|
||||||
|
await expect(page.locator("#currentPageSubtitle")).not.toHaveText("市场复盘");
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 800 });
|
||||||
|
await expect(page.locator(".tape-optional").first()).toBeHidden();
|
||||||
|
await page.locator("#overviewToggle").click();
|
||||||
|
const detailText = await page.locator(".tape-detail").innerText();
|
||||||
|
for (const label of ["市场情绪", "上涨家数", "下跌家数", "涨停", "跌停", "炸板", "封板率", "两市成交", "数据日期"]) {
|
||||||
|
expect(detailText).toContain(label);
|
||||||
|
}
|
||||||
|
expect(detailText).not.toContain("涨停 / 跌停");
|
||||||
|
await expect(page.locator("#tapeLimitDown")).toBeVisible();
|
||||||
|
await expect(page.locator("#detailBroken")).toBeVisible();
|
||||||
|
await expect(page.locator("#dataDateMetric")).toBeVisible();
|
||||||
|
await expect(page.locator(".tape-detail-date")).toBeVisible();
|
||||||
|
expect(await page.locator("#dataDateMetric").evaluate((node) => Boolean(node.closest(".metric-wide")))).toBe(false);
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
|
const measurePoolTable = () => {
|
||||||
|
const wrap = document.querySelector("#limitPool .pool-table-card");
|
||||||
|
const table = document.querySelector("#limitTable");
|
||||||
|
const reason = document.querySelector("#limitTable .reason-column");
|
||||||
|
const reasonCell = document.querySelector("#limitTable .pool-reason-cell");
|
||||||
|
const headerCell = document.querySelector("#limitPool .data-table thead th");
|
||||||
|
const rowCell = document.querySelector("#limitPool .data-table tbody td");
|
||||||
|
const cells = [...document.querySelectorAll("#limitTable tbody td")];
|
||||||
|
const numberCells = [...document.querySelectorAll("#limitTable tbody td.num, #limitTable tbody td.number")];
|
||||||
|
const header = document.querySelector(".app-header");
|
||||||
|
const headerStyle = headerCell ? getComputedStyle(headerCell) : null;
|
||||||
|
const rowStyle = rowCell ? getComputedStyle(rowCell) : null;
|
||||||
|
const widths = cells.map((cell) => cell.getBoundingClientRect().width);
|
||||||
|
const numberWidths = numberCells.map((cell) => cell.getBoundingClientRect().width);
|
||||||
|
return {
|
||||||
|
tableLayout: table ? getComputedStyle(table).tableLayout : "",
|
||||||
|
columnCount: document.querySelectorAll("#limitTable thead th").length,
|
||||||
|
minCellWidth: widths.length ? Math.min(...widths) : 0,
|
||||||
|
minNumberWidth: numberWidths.length ? Math.min(...numberWidths) : 0,
|
||||||
|
wrapOverflow: wrap ? wrap.scrollWidth - wrap.clientWidth : 0,
|
||||||
|
tableWidth: table ? table.getBoundingClientRect().width : 0,
|
||||||
|
wrapWidth: wrap ? wrap.getBoundingClientRect().width : 0,
|
||||||
|
cardOverflow: wrap ? wrap.scrollWidth - wrap.clientWidth : 0,
|
||||||
|
reasonVisible: Boolean(reason && wrap && reason.getBoundingClientRect().right <= wrap.getBoundingClientRect().right + 1),
|
||||||
|
reasonText: reason ? reason.textContent.trim() : "",
|
||||||
|
reasonTitle: reasonCell?.getAttribute("title") || "",
|
||||||
|
headerOverflow: header ? header.scrollWidth - header.clientWidth : 0,
|
||||||
|
headerHeight: headerStyle ? Number.parseFloat(headerStyle.height) : 0,
|
||||||
|
headerFont: headerStyle ? Number.parseFloat(headerStyle.fontSize) : 0,
|
||||||
|
rowHeight: rowStyle ? Number.parseFloat(rowStyle.height) : 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const pool1280 = await page.evaluate(measurePoolTable);
|
||||||
|
expect(pool1280.columnCount).toBe(13);
|
||||||
|
expect(pool1280.tableLayout).toBe("auto");
|
||||||
|
expect(pool1280.minCellWidth).toBeGreaterThanOrEqual(40);
|
||||||
|
expect(pool1280.minNumberWidth).toBeGreaterThanOrEqual(50);
|
||||||
|
expect(pool1280.wrapOverflow).toBeGreaterThan(0);
|
||||||
|
expect(pool1280.reasonTitle).toContain("板块龙头连板打开空间");
|
||||||
|
expect(pool1280.headerHeight).toBe(36);
|
||||||
|
expect(pool1280.rowHeight).toBeGreaterThanOrEqual(40);
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||||
|
await page.locator('[data-view="limitPool"]').first().click();
|
||||||
|
const tableFit = await page.evaluate(measurePoolTable);
|
||||||
|
expect(tableFit.reasonText).toContain("涨停原因");
|
||||||
|
expect(tableFit.reasonVisible).toBe(true);
|
||||||
|
expect(tableFit.cardOverflow).toBeLessThanOrEqual(1);
|
||||||
|
expect(tableFit.headerOverflow).toBeLessThanOrEqual(1);
|
||||||
|
expect(tableFit.reasonTitle).toContain("板块龙头连板打开空间");
|
||||||
|
expect(tableFit.headerHeight).toBe(36);
|
||||||
|
expect(tableFit.headerFont).toBe(12.5);
|
||||||
|
expect(tableFit.rowHeight).toBeGreaterThanOrEqual(40);
|
||||||
|
expect(tableFit.tableLayout).toBe("fixed");
|
||||||
|
expect(tableFit.columnCount).toBe(13);
|
||||||
|
|
||||||
|
await page.locator('[data-view="heavenView"]').first().click();
|
||||||
|
await expect(page.locator(".app-header .overview-strip")).toBeHidden();
|
||||||
|
await expect(page.locator("#overviewToggle")).toBeHidden();
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.locator('[data-view="limitPool"]').first().click({ force: true });
|
||||||
|
const pool390 = await page.evaluate(measurePoolTable);
|
||||||
|
expect(pool390.columnCount).toBe(13);
|
||||||
|
expect(pool390.tableLayout).toBe("auto");
|
||||||
|
expect(pool390.minCellWidth).toBeGreaterThanOrEqual(40);
|
||||||
|
expect(pool390.minNumberWidth).toBeGreaterThanOrEqual(50);
|
||||||
|
expect(pool390.wrapOverflow).toBeGreaterThan(0);
|
||||||
|
expect(pool390.reasonTitle).toContain("板块龙头连板打开空间");
|
||||||
|
await page.locator("#mobileMarketViewSelect").selectOption("sentimentCycleView");
|
||||||
|
await page.locator("#overviewToggle").click();
|
||||||
|
const mobileDetail = await page.locator(".tape-detail").innerText();
|
||||||
|
for (const label of ["市场情绪", "上涨家数", "下跌家数", "涨停", "跌停", "炸板", "封板率", "两市成交", "数据日期"]) {
|
||||||
|
expect(mobileDetail).toContain(label);
|
||||||
|
}
|
||||||
|
await expect(page.locator("#dataDateMetric")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("B-199 screener review and account surfaces fit day night viewports", async ({ page }) => {
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const shotDir = path.join(__dirname, "../../runtime/b199-shots");
|
||||||
|
fs.mkdirSync(shotDir, { recursive: true });
|
||||||
|
const pageErrors = [];
|
||||||
|
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
||||||
|
|
||||||
|
const overflowX = () => page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
||||||
|
const shot = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`), fullPage: true });
|
||||||
|
|
||||||
|
await mockApplication(page, session("admin", true));
|
||||||
|
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||||
|
await page.goto("/index.html");
|
||||||
|
|
||||||
|
await page.locator('[data-view="screenerView"]').first().click();
|
||||||
|
await expect(page.locator("#screenerView")).toHaveClass(/active-view/);
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("screener-1600-day");
|
||||||
|
|
||||||
|
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
|
||||||
|
await expect(page.locator("#reviewWorkspaceView")).toHaveClass(/active-view/);
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("review-1600-day");
|
||||||
|
|
||||||
|
await openHeaderCommandMenu(page);
|
||||||
|
await page.locator("#accountButton").click();
|
||||||
|
await page.locator('[data-account-panel="membership"]').click();
|
||||||
|
await expect(page.locator("#settingsDialog")).toBeVisible();
|
||||||
|
await shot("account-1600-day");
|
||||||
|
await page.locator("#closeSettingsDialog").click();
|
||||||
|
|
||||||
|
await openHeaderCommandMenu(page);
|
||||||
|
await page.locator("#settingsButton").click();
|
||||||
|
await expect(page.locator("#adminDialog")).toBeVisible();
|
||||||
|
await shot("admin-1600-day");
|
||||||
|
await page.locator("#closeAdminDialog").click();
|
||||||
|
|
||||||
|
await page.locator("#themeToggle").click();
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||||
|
await page.locator('[data-view="screenerView"]').first().click();
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("screener-1600-night");
|
||||||
|
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("review-1600-night");
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1280, height: 800 });
|
||||||
|
await page.locator('[data-view="screenerView"]').first().click();
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("screener-1280-night");
|
||||||
|
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("review-1280-night");
|
||||||
|
|
||||||
|
await page.locator("#themeToggle").click();
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
|
||||||
|
await page.locator('[data-view="screenerView"]').first().click();
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("screener-1280-day");
|
||||||
|
await page.locator('[data-view="reviewWorkspaceView"]').first().click();
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("review-1280-day");
|
||||||
|
|
||||||
|
await page.locator('[data-view="screenerView"]').first().click();
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(page.locator("#screenerView")).toHaveClass(/active-view/);
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("screener-390-day");
|
||||||
|
await page.locator(".mobile-primary-tab[data-view=\"reviewWorkspaceView\"]").click();
|
||||||
|
await expect(page.locator("#reviewWorkspaceView")).toHaveClass(/active-view/);
|
||||||
|
expect(await overflowX()).toBeLessThanOrEqual(1);
|
||||||
|
await shot("review-390-day");
|
||||||
|
|
||||||
|
expect(pageErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("desktop header keeps refresh, admin commands and account identity visible", async ({ page }) => {
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const shotDir = path.join(__dirname, "../../runtime/b214-shots");
|
||||||
|
fs.mkdirSync(shotDir, { recursive: true });
|
||||||
|
const pageErrors = [];
|
||||||
|
page.on("pageerror", (error) => pageErrors.push(String(error)));
|
||||||
|
await mockApplication(page, session("admin", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
const shot = (name) => page.locator(".app-header").screenshot({ path: path.join(shotDir, `${name}.png`) });
|
||||||
|
const shotPage = (name) => page.screenshot({ path: path.join(shotDir, `${name}.png`) });
|
||||||
|
|
||||||
|
const measureHeader = () => page.evaluate(() => {
|
||||||
|
const header = document.querySelector(".app-header");
|
||||||
|
const tape = document.querySelector(".market-tape");
|
||||||
|
const actions = document.querySelector(".header-actions");
|
||||||
|
const refresh = document.querySelector("#refreshButton");
|
||||||
|
const account = document.querySelector("#accountButton");
|
||||||
|
const name = document.querySelector("#accountName");
|
||||||
|
const box = (node) => node ? node.getBoundingClientRect() : null;
|
||||||
|
const visible = (node) => {
|
||||||
|
if (!node || node.hidden) return false;
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
if (style.display === "none" || style.visibility === "hidden") return false;
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
};
|
||||||
|
const tapeBox = box(tape);
|
||||||
|
const actionsBox = box(actions);
|
||||||
|
return {
|
||||||
|
headerOverflow: header ? header.scrollWidth - header.clientWidth : 0,
|
||||||
|
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
|
||||||
|
tapeHeight: tapeBox ? tapeBox.height : 0,
|
||||||
|
overlap: tapeBox && actionsBox
|
||||||
|
? Math.max(0, Math.min(tapeBox.right, actionsBox.right) - Math.max(tapeBox.left, actionsBox.left))
|
||||||
|
* Math.max(0, Math.min(tapeBox.bottom, actionsBox.bottom) - Math.max(tapeBox.top, actionsBox.top))
|
||||||
|
: 0,
|
||||||
|
refreshVisible: visible(refresh),
|
||||||
|
accountVisible: visible(account),
|
||||||
|
nameVisible: visible(name),
|
||||||
|
nameFits: name ? name.clientWidth >= name.scrollWidth : false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const viewport of [
|
||||||
|
{ width: 1600, height: 1000 },
|
||||||
|
{ width: 1440, height: 900 },
|
||||||
|
{ width: 1280, height: 800 },
|
||||||
|
]) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await expect(page.locator("#headerMenuButton")).toBeHidden();
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeVisible();
|
||||||
|
await expect(page.locator("#refreshButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#syncButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#settingsButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountAdminBadge")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountVipBadge")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountName")).toBeVisible();
|
||||||
|
const geometry = await measureHeader();
|
||||||
|
expect(geometry.headerOverflow, `${viewport.width} header overflow`).toBeLessThanOrEqual(1);
|
||||||
|
expect(geometry.documentOverflow, `${viewport.width} document overflow`).toBeLessThanOrEqual(1);
|
||||||
|
expect(geometry.overlap, `${viewport.width} tape overlap`).toBe(0);
|
||||||
|
expect(geometry.tapeHeight, `${viewport.width} tape height`).toBeGreaterThan(20);
|
||||||
|
expect(geometry.nameFits, `${viewport.width} account name truncated`).toBe(true);
|
||||||
|
await page.locator("#refreshButton").click();
|
||||||
|
await expect(page.locator("#loadingOverlay")).toBeHidden();
|
||||||
|
await page.locator("#settingsButton").click();
|
||||||
|
await expect(page.locator("#adminDialog")).toBeVisible();
|
||||||
|
await page.locator("#closeAdminDialog").click();
|
||||||
|
await page.locator("#accountButton").click();
|
||||||
|
await expect(page.locator("#accountDropdown")).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await shot(`${viewport.width}-day`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.locator("#themeToggle").click();
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||||
|
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||||
|
await shot("1600-night");
|
||||||
|
await page.locator("#themeToggle").click();
|
||||||
|
|
||||||
|
await page.locator('[data-view="heavenView"]').first().click();
|
||||||
|
await expect(page.locator("#heavenView")).toHaveClass(/active-view/);
|
||||||
|
await expect(page.locator("#refreshButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#headerMenuButton")).toBeHidden();
|
||||||
|
await shot("heaven-1600-day");
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(page.locator("#headerMenuButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#refreshButton")).toBeHidden();
|
||||||
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeVisible();
|
||||||
|
await expect(page.locator("#refreshButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#syncButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#settingsButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountAdminBadge")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountVipBadge")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountButton")).toBeVisible();
|
||||||
|
await expect(page.locator('[data-mobile-command-target="themeToggle"]')).toBeVisible();
|
||||||
|
const mobileName = await page.evaluate(measureAccountName);
|
||||||
|
expect(mobileName.fits, "390 account name truncated").toBe(true);
|
||||||
|
expect(mobileName.stacked, "390 identity and account should stack").toBe(true);
|
||||||
|
await shotPage("390-menu-open");
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeHidden();
|
||||||
|
|
||||||
|
await mockApplication(page, session("user", false));
|
||||||
|
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await expect(page.locator("#refreshButton")).toBeVisible();
|
||||||
|
await expect(page.locator("#syncButton")).toBeHidden();
|
||||||
|
await expect(page.locator("#settingsButton")).toBeHidden();
|
||||||
|
await expect(page.locator("#accountAdminBadge")).toBeHidden();
|
||||||
|
await expect(page.locator("#accountVipBadge")).toBeVisible();
|
||||||
|
await expect(page.locator("#accountName")).toHaveText("normal_user");
|
||||||
|
await shot("1600-user-day");
|
||||||
|
expect(pageErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("header username stays untruncated on desktop and in the mobile menu", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const shotDir = path.join(__dirname, "../../runtime/b217-shots");
|
||||||
|
fs.mkdirSync(shotDir, { recursive: true });
|
||||||
|
const measurements = [];
|
||||||
|
const viewports = [
|
||||||
|
{ width: 1600, height: 1000 },
|
||||||
|
{ width: 1440, height: 900 },
|
||||||
|
{ width: 1280, height: 800 },
|
||||||
|
{ width: 390, height: 844 },
|
||||||
|
];
|
||||||
|
const accounts = [
|
||||||
|
{ label: "admin", username: "review_admin", auth: namedSession("review_admin", "admin", true) },
|
||||||
|
{ label: "user", username: "normal_user", auth: namedSession("normal_user", "user", false) },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
await mockApplication(page, account.auth);
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await expect(page.locator("#accountName")).toHaveText(account.username);
|
||||||
|
|
||||||
|
for (const theme of ["day", "night"]) {
|
||||||
|
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||||
|
await setColorTheme(page, theme);
|
||||||
|
for (const viewport of viewports) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
if (viewport.width === 390) {
|
||||||
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeVisible();
|
||||||
|
} else {
|
||||||
|
await expect(page.locator("#headerMenuButton")).toBeHidden();
|
||||||
|
await expect(page.locator("#accountName")).toBeVisible();
|
||||||
|
}
|
||||||
|
const geometry = await page.evaluate(measureAccountName);
|
||||||
|
const label = `${account.label}-${viewport.width}-${theme}`;
|
||||||
|
expect(geometry.text, `${label} username`).toBe(account.username);
|
||||||
|
expect(geometry.fits, `${label} accountName truncated ${geometry.clientWidth}<${geometry.scrollWidth}`).toBe(true);
|
||||||
|
expect(geometry.documentOverflow, `${label} document overflow`).toBeLessThanOrEqual(1);
|
||||||
|
if (viewport.width === 390) {
|
||||||
|
expect(geometry.stacked, `${label} identity/account not stacked`).toBe(true);
|
||||||
|
}
|
||||||
|
if (viewport.width >= 1440 && account.label === "admin") {
|
||||||
|
expect(geometry.adminLabelVisible, `${label} admin badge text hidden`).toBe(true);
|
||||||
|
expect(geometry.vipLabelVisible, `${label} vip badge text hidden`).toBe(true);
|
||||||
|
}
|
||||||
|
measurements.push({
|
||||||
|
account: account.username,
|
||||||
|
theme,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
...geometry,
|
||||||
|
});
|
||||||
|
const shotName = `${account.label}-${viewport.width}-${theme}.png`;
|
||||||
|
if (viewport.width === 390) {
|
||||||
|
await page.screenshot({ path: path.join(shotDir, shotName) });
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeHidden();
|
||||||
|
} else {
|
||||||
|
await page.locator(".app-header").screenshot({ path: path.join(shotDir, shotName) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function measureHeaderProbe() {
|
||||||
|
const name = document.querySelector("#accountName");
|
||||||
|
const group = document.querySelector("#headerCommandGroup");
|
||||||
|
const strip = document.querySelector(".app-header .overview-strip");
|
||||||
|
const stripBox = strip ? strip.getBoundingClientRect() : null;
|
||||||
|
const stripShown = Boolean(stripBox && stripBox.width > 0 && stripBox.height > 0);
|
||||||
|
const tapeClip = stripShown
|
||||||
|
? [...strip.querySelectorAll("*")].filter((el) => (
|
||||||
|
el.children.length === 0 && el.scrollWidth > el.clientWidth + 1
|
||||||
|
)).map((el) => (el.textContent || "").trim()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const groupBox = group ? group.getBoundingClientRect() : null;
|
||||||
|
const nameBox = name ? name.getBoundingClientRect() : null;
|
||||||
|
return {
|
||||||
|
groupRight: groupBox ? groupBox.right : 0,
|
||||||
|
nameRight: nameBox ? nameBox.right : 0,
|
||||||
|
nameFits: name ? name.clientWidth >= name.scrollWidth : false,
|
||||||
|
nameText: name ? String(name.textContent || "") : "",
|
||||||
|
tapeClip,
|
||||||
|
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
|
||||||
|
viewport: window.innerWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyColorTheme(page, theme) {
|
||||||
|
const wanted = theme === "night" ? "dark" : "light";
|
||||||
|
await page.evaluate((value) => {
|
||||||
|
document.documentElement.dataset.theme = value;
|
||||||
|
document.documentElement.style.colorScheme = value;
|
||||||
|
try { localStorage.setItem("xiaobaiTheme", value); } catch (_error) {}
|
||||||
|
}, wanted);
|
||||||
|
await expect(page.locator("html")).toHaveAttribute("data-theme", wanted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeHeaderWorkspaces(page, { account, theme, viewport, views, measurements }) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await applyColorTheme(page, theme);
|
||||||
|
for (const view of views) {
|
||||||
|
await page.locator(`[data-view="${view}"]`).first().click();
|
||||||
|
await expect(page.locator(`#${view}`)).toHaveClass(/active-view/);
|
||||||
|
const geometry = await page.evaluate(measureHeaderProbe);
|
||||||
|
const label = `${account.label}-${view}-${viewport.width}-${theme}`;
|
||||||
|
expect(geometry.nameText, `${label} username`).toBe(account.username);
|
||||||
|
expect(geometry.nameFits, `${label} accountName truncated`).toBe(true);
|
||||||
|
expect(geometry.groupRight, `${label} command group overflow ${geometry.groupRight}>${geometry.viewport}`).toBeLessThanOrEqual(geometry.viewport + 0.5);
|
||||||
|
expect(geometry.nameRight, `${label} accountName overflow ${geometry.nameRight}>${geometry.viewport}`).toBeLessThanOrEqual(geometry.viewport + 0.5);
|
||||||
|
expect(geometry.documentOverflow, `${label} document overflow`).toBeLessThanOrEqual(1);
|
||||||
|
expect(geometry.tapeClip, `${label} tape clipped ${geometry.tapeClip.join("|")}`).toEqual([]);
|
||||||
|
measurements.push({ account: account.username, view, theme, width: viewport.width, ...geometry });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("desktop header keeps commands in view and tape text unclipped across workspaces", async ({ page }) => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const shotDir = path.join(__dirname, "../../runtime/b221-shots");
|
||||||
|
fs.mkdirSync(shotDir, { recursive: true });
|
||||||
|
const measurements = [];
|
||||||
|
const views = [
|
||||||
|
"sentimentCycleView",
|
||||||
|
"auctionView",
|
||||||
|
"themeLibraryView",
|
||||||
|
"popularityView",
|
||||||
|
"rotationView",
|
||||||
|
"mentorView",
|
||||||
|
"screenerView",
|
||||||
|
"reviewWorkspaceView",
|
||||||
|
"heavenView",
|
||||||
|
];
|
||||||
|
const viewports = [
|
||||||
|
{ width: 1600, height: 1000 },
|
||||||
|
{ width: 1440, height: 900 },
|
||||||
|
{ width: 1280, height: 800 },
|
||||||
|
];
|
||||||
|
const accounts = [
|
||||||
|
{ label: "admin", username: "review_admin", auth: namedSession("review_admin", "admin", true) },
|
||||||
|
{ label: "user", username: "normal_user", auth: namedSession("normal_user", "user", false) },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
await mockApplication(page, account.auth);
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await expect(page.locator("#accountName")).toHaveText(account.username);
|
||||||
|
for (const viewport of viewports) {
|
||||||
|
await probeHeaderWorkspaces(page, { account, theme: "day", viewport, views, measurements });
|
||||||
|
await page.locator('[data-view="themeLibraryView"]').first().click();
|
||||||
|
await page.locator(".app-header").screenshot({
|
||||||
|
path: path.join(shotDir, `${account.label}-${viewport.width}-day.png`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await probeHeaderWorkspaces(page, {
|
||||||
|
account,
|
||||||
|
theme: "night",
|
||||||
|
viewport: { width: 1600, height: 1000 },
|
||||||
|
views,
|
||||||
|
measurements,
|
||||||
|
});
|
||||||
|
await page.locator('[data-view="sentimentCycleView"]').first().click();
|
||||||
|
await page.locator(".app-header").screenshot({
|
||||||
|
path: path.join(shotDir, `${account.label}-1600-night.png`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await mockApplication(page, accounts[0].auth);
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await applyColorTheme(page, "day");
|
||||||
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeVisible();
|
||||||
|
const mobileDay = await page.evaluate(measureAccountName);
|
||||||
|
expect(mobileDay.fits, "390 day account name truncated").toBe(true);
|
||||||
|
expect(mobileDay.stacked, "390 day identity and account should stack").toBe(true);
|
||||||
|
await page.screenshot({ path: path.join(shotDir, "admin-390-day.png") });
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await applyColorTheme(page, "night");
|
||||||
|
await page.locator("#headerMenuButton").click();
|
||||||
|
const mobileNight = await page.evaluate(measureAccountName);
|
||||||
|
expect(mobileNight.fits, "390 night account name truncated").toBe(true);
|
||||||
|
expect(mobileNight.stacked, "390 night identity and account should stack").toBe(true);
|
||||||
|
await page.screenshot({ path: path.join(shotDir, "admin-390-night.png") });
|
||||||
|
fs.writeFileSync(path.join(shotDir, "measurements.json"), `${JSON.stringify(measurements, null, 2)}\n`);
|
||||||
|
});
|
||||||
|
|||||||
@@ -18,20 +18,92 @@ 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, 3)
|
self.assertEqual(count, 4)
|
||||||
|
|
||||||
|
def test_database_with_recorded_0004_and_note_column_starts_without_reapply(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as root:
|
||||||
|
path = Path(root) / "review.db"
|
||||||
|
database = ReviewDatabase(path)
|
||||||
|
with database.connect() as connection:
|
||||||
|
note_rows = [
|
||||||
|
str(row["name"])
|
||||||
|
for row in connection.execute(
|
||||||
|
"PRAGMA table_info(mentor_preferences)"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.assertIn("note", note_rows)
|
||||||
|
ReviewDatabase(path)
|
||||||
|
with database.connect() as connection:
|
||||||
|
count = connection.execute(
|
||||||
|
"SELECT COUNT(*) AS count FROM schema_migrations"
|
||||||
|
).fetchone()["count"]
|
||||||
|
self.assertEqual(count, 4)
|
||||||
|
|
||||||
|
def test_old_database_without_0004_upgrades_and_adds_note_column(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as root:
|
||||||
|
path = Path(root) / "review.db"
|
||||||
|
database = ReviewDatabase(path)
|
||||||
|
with database.connect() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"DELETE FROM schema_migrations WHERE version = '0004'"
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"ALTER TABLE mentor_preferences DROP COLUMN note"
|
||||||
|
)
|
||||||
|
ReviewDatabase(path)
|
||||||
|
with database.connect() as connection:
|
||||||
|
versions = {
|
||||||
|
str(row["version"])
|
||||||
|
for row in connection.execute(
|
||||||
|
"SELECT version FROM schema_migrations"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
note_rows = [
|
||||||
|
str(row["name"])
|
||||||
|
for row in connection.execute(
|
||||||
|
"PRAGMA table_info(mentor_preferences)"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.assertEqual(versions, {"0001", "0002", "0003", "0004"})
|
||||||
|
self.assertIn("note", note_rows)
|
||||||
|
|
||||||
|
def test_database_with_unknown_migration_is_rejected(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as root:
|
||||||
|
path = Path(root) / "review.db"
|
||||||
|
database = ReviewDatabase(path)
|
||||||
|
with database.connect() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO schema_migrations
|
||||||
|
(version, name, checksum, applied_at)
|
||||||
|
VALUES ('9999', 'unknown_legacy', 'x', '2026-08-01T00:00:00+00:00')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with self.assertRaises(MigrationError):
|
||||||
|
ReviewDatabase(path)
|
||||||
|
|
||||||
def test_connection_factory_enables_required_pragmas(self) -> None:
|
def test_connection_factory_enables_required_pragmas(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as root:
|
with tempfile.TemporaryDirectory() as root:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
@@ -8,12 +9,35 @@ 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__()
|
||||||
@@ -50,11 +74,23 @@ 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):
|
||||||
selectors = set(re.findall(r'querySelector\("#([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
|
pending = uncommitted_runtime_files()
|
||||||
selectors.update(re.findall(r'getElementById\("([A-Za-z][A-Za-z0-9_-]*)"\)', self.script))
|
dangling: list[str] = []
|
||||||
selectors.update(re.findall(r'setText\("([A-Za-z][A-Za-z0-9_-]*)"', self.script))
|
for url in registered_frontend_runtime_scripts():
|
||||||
missing = sorted(selectors - set(self.ids))
|
relative = url.split("?", 1)[0].lstrip("/")
|
||||||
self.assertEqual(missing, [])
|
if relative.startswith("vendor/"):
|
||||||
|
continue
|
||||||
|
if relative in pending:
|
||||||
|
continue
|
||||||
|
source = (STATIC_DIR / relative).read_text(encoding="utf-8")
|
||||||
|
selectors = set(re.findall(r'querySelector\("#([A-Za-z][A-Za-z0-9_-]*)"\)', source))
|
||||||
|
selectors.update(re.findall(r'getElementById\("([A-Za-z][A-Za-z0-9_-]*)"\)', source))
|
||||||
|
selectors.update(re.findall(r'setText\("([A-Za-z][A-Za-z0-9_-]*)"', source))
|
||||||
|
dangling.extend(
|
||||||
|
f"{relative}:{selector}"
|
||||||
|
for selector in sorted(selectors - set(self.ids))
|
||||||
|
)
|
||||||
|
self.assertEqual(dangling, [])
|
||||||
|
|
||||||
def test_all_primary_views_have_navigation_entries(self):
|
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))
|
||||||
@@ -130,6 +166,15 @@ 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)
|
||||||
@@ -280,13 +325,18 @@ class FrontendContractTests(unittest.TestCase):
|
|||||||
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_tokens_and_sentiment_bottom_clearance_are_defined(self):
|
||||||
self.assertIn(":root[data-theme=\"dark\"] #mentorView {", self.mentor_styles)
|
self.assertIn(":root[data-theme=\"dark\"] #mentorView.workspace-view {", self.mentor_styles)
|
||||||
self.assertIn("--mentor-ink: var(--text-primary);", self.mentor_styles)
|
self.assertIn("--qp-bg-chat: #232529;", self.mentor_styles)
|
||||||
self.assertIn("--mentor-sub: var(--text-secondary);", self.mentor_styles)
|
self.assertIn("--qp-bg-bubble-self: #35598C;", self.mentor_styles)
|
||||||
self.assertIn(":root[data-theme=\"dark\"] #mentorView .mentor-message {", self.mentor_styles)
|
self.assertIn("--qp-bg-selected: #2B3B58;", self.mentor_styles)
|
||||||
self.assertIn("border-color: var(--line-soft);", self.mentor_styles)
|
self.assertIn("--qp-link: #316FEF;", self.mentor_styles)
|
||||||
self.assertIn("background: var(--surface-subtle);", 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("--mentor-composer-min-height: 85px;", self.tokens)
|
||||||
|
self.assertIn("#mentorView .mentor-message.user .mentor-message-content {", self.mentor_styles)
|
||||||
|
self.assertIn("background: var(--qp-bg-bubble-self);", self.mentor_styles)
|
||||||
|
self.assertIn("#mentorView .mentor-quick-prompts button {", self.mentor_styles)
|
||||||
|
self.assertIn("#mentorView .mentor-composer-hint {", self.mentor_styles)
|
||||||
self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.sentiment_styles)
|
self.assertIn("#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)
|
||||||
@@ -294,6 +344,38 @@ 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user