chore: create Multica handoff checkpoint
This commit is contained in:
@@ -15,6 +15,11 @@ class MentorAgentError(RuntimeError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
FOLLOW_UP_START = "<XIAOBAI_FOLLOW_UPS>"
|
||||||
|
FOLLOW_UP_END = "</XIAOBAI_FOLLOW_UPS>"
|
||||||
|
MAX_FOLLOW_UP_LENGTH = 80
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class MentorSkill:
|
class MentorSkill:
|
||||||
skill_id: str
|
skill_id: str
|
||||||
@@ -185,6 +190,8 @@ def stream_with_mentor(
|
|||||||
base_url: str,
|
base_url: str,
|
||||||
model: str,
|
model: str,
|
||||||
timeout: int = 90,
|
timeout: int = 90,
|
||||||
|
*,
|
||||||
|
follow_ups: list[str] | None = None,
|
||||||
) -> Iterator[str]:
|
) -> Iterator[str]:
|
||||||
if not api_key or not model:
|
if not api_key or not model:
|
||||||
raise MentorAgentError("LLM API Key 或模型尚未配置。")
|
raise MentorAgentError("LLM API Key 或模型尚未配置。")
|
||||||
@@ -193,8 +200,10 @@ def stream_with_mentor(
|
|||||||
messages = [{"role": "system", "content": system_prompt}]
|
messages = [{"role": "system", "content": system_prompt}]
|
||||||
messages.extend(history[-10:])
|
messages.extend(history[-10:])
|
||||||
messages.append({"role": "user", "content": question})
|
messages.append({"role": "user", "content": question})
|
||||||
|
if follow_ups is not None:
|
||||||
|
follow_ups.clear()
|
||||||
try:
|
try:
|
||||||
yield from llm_transport.stream_chat_completion(
|
upstream = llm_transport.stream_chat_completion(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
model=model,
|
model=model,
|
||||||
@@ -202,6 +211,7 @@ def stream_with_mentor(
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
user_agent="XiaobaiReviewWeb/0.6",
|
user_agent="XiaobaiReviewWeb/0.6",
|
||||||
)
|
)
|
||||||
|
yield from _stream_answer_and_collect_follow_ups(upstream, follow_ups)
|
||||||
except llm_transport.OpenAIEmptyResponseError as exc:
|
except llm_transport.OpenAIEmptyResponseError as exc:
|
||||||
raise MentorAgentError("问师模型未返回有效内容。") from exc
|
raise MentorAgentError("问师模型未返回有效内容。") from exc
|
||||||
except llm_transport.OpenAIHTTPError as exc:
|
except llm_transport.OpenAIHTTPError as exc:
|
||||||
@@ -223,6 +233,10 @@ def _build_system_prompt(skill: MentorSkill, market_context: dict[str, Any]) ->
|
|||||||
5. 优先回答用户真正的问题。市场分析通常按“判断、数据依据、思维模型下的应对、失效条件”组织;纯交易心理或方法问题可以自然回答,不强制套模板。
|
5. 优先回答用户真正的问题。市场分析通常按“判断、数据依据、思维模型下的应对、失效条件”组织;纯交易心理或方法问题可以自然回答,不强制套模板。
|
||||||
6. 保留该 Skill 的核心心智模型和表达节奏,但不要复述身份履历,不要宣称自己就是真人,不攻击或贬低用户。
|
6. 保留该 Skill 的核心心智模型和表达节奏,但不要复述身份履历,不要宣称自己就是真人,不攻击或贬低用户。
|
||||||
7. 使用中文,信息密度高,避免空泛口号。引用数字时标明数据日期。
|
7. 使用中文,信息密度高,避免空泛口号。引用数字时标明数据日期。
|
||||||
|
8. 正文结束后必须输出2至3条与本轮问题和正文直接相关的追问。追问用于帮助用户继续核实条件、风险或失效边界,不得引入正文没有依据的新事实,不得给出无条件买卖指令。严格使用以下机器结构,不要放进Markdown代码块,结束标签后不要再输出文字:
|
||||||
|
<XIAOBAI_FOLLOW_UPS>
|
||||||
|
["追问一?","追问二?","追问三?"]
|
||||||
|
</XIAOBAI_FOLLOW_UPS>
|
||||||
|
|
||||||
网页市场数据:
|
网页市场数据:
|
||||||
{context_json}
|
{context_json}
|
||||||
@@ -233,6 +247,67 @@ def _build_system_prompt(skill: MentorSkill, market_context: dict[str, Any]) ->
|
|||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_answer_and_collect_follow_ups(
|
||||||
|
chunks: Iterator[str], follow_ups: list[str] | None
|
||||||
|
) -> Iterator[str]:
|
||||||
|
buffer = ""
|
||||||
|
collecting = False
|
||||||
|
for raw_chunk in chunks:
|
||||||
|
chunk = str(raw_chunk or "")
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
buffer += chunk
|
||||||
|
if collecting:
|
||||||
|
continue
|
||||||
|
marker_index = buffer.find(FOLLOW_UP_START)
|
||||||
|
if marker_index >= 0:
|
||||||
|
if marker_index:
|
||||||
|
yield buffer[:marker_index]
|
||||||
|
buffer = buffer[marker_index + len(FOLLOW_UP_START):]
|
||||||
|
collecting = True
|
||||||
|
continue
|
||||||
|
overlap = _marker_prefix_overlap(buffer, FOLLOW_UP_START)
|
||||||
|
emit_length = len(buffer) - overlap
|
||||||
|
if emit_length:
|
||||||
|
yield buffer[:emit_length]
|
||||||
|
buffer = buffer[emit_length:]
|
||||||
|
|
||||||
|
if not collecting:
|
||||||
|
if buffer:
|
||||||
|
yield buffer
|
||||||
|
return
|
||||||
|
raw_follow_ups = buffer.split(FOLLOW_UP_END, 1)[0].strip()
|
||||||
|
parsed = _parse_follow_ups(raw_follow_ups)
|
||||||
|
if follow_ups is not None and len(parsed) >= 2:
|
||||||
|
follow_ups.extend(parsed)
|
||||||
|
|
||||||
|
|
||||||
|
def _marker_prefix_overlap(value: str, marker: str) -> int:
|
||||||
|
max_length = min(len(value), len(marker) - 1)
|
||||||
|
for length in range(max_length, 0, -1):
|
||||||
|
if value.endswith(marker[:length]):
|
||||||
|
return length
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_follow_ups(payload: str) -> list[str]:
|
||||||
|
try:
|
||||||
|
values = json.loads(payload)
|
||||||
|
except (TypeError, json.JSONDecodeError):
|
||||||
|
return []
|
||||||
|
if not isinstance(values, list):
|
||||||
|
return []
|
||||||
|
result: list[str] = []
|
||||||
|
for value in values:
|
||||||
|
question = re.sub(r"\s+", " ", str(value or "")).strip()
|
||||||
|
if not question or len(question) > MAX_FOLLOW_UP_LENGTH or question in result:
|
||||||
|
continue
|
||||||
|
result.append(question)
|
||||||
|
if len(result) == 3:
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _parse_frontmatter(content: str) -> dict[str, str]:
|
def _parse_frontmatter(content: str) -> dict[str, str]:
|
||||||
if not content.startswith("---"):
|
if not content.startswith("---"):
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -129,9 +129,10 @@ class MentorServiceMixin:
|
|||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
answer_parts: list[str] = []
|
answer_parts: list[str] = []
|
||||||
|
follow_ups: list[str] = []
|
||||||
events = self.llm_gateway.stream(
|
events = self.llm_gateway.stream(
|
||||||
"mentor",
|
"mentor",
|
||||||
f"mentor-skill-v1:{skill.skill_id}",
|
f"mentor-skill-v2:{skill.skill_id}",
|
||||||
lambda profile: stream_with_mentor(
|
lambda profile: stream_with_mentor(
|
||||||
skill,
|
skill,
|
||||||
context,
|
context,
|
||||||
@@ -140,6 +141,7 @@ class MentorServiceMixin:
|
|||||||
profile.api_key,
|
profile.api_key,
|
||||||
profile.base_url,
|
profile.base_url,
|
||||||
profile.model,
|
profile.model,
|
||||||
|
follow_ups=follow_ups,
|
||||||
),
|
),
|
||||||
(MentorAgentError,),
|
(MentorAgentError,),
|
||||||
)
|
)
|
||||||
@@ -160,6 +162,7 @@ class MentorServiceMixin:
|
|||||||
yield {
|
yield {
|
||||||
"type": "meta",
|
"type": "meta",
|
||||||
"data_trade_date": context["data_trade_date"],
|
"data_trade_date": context["data_trade_date"],
|
||||||
|
"follow_ups": follow_ups or self._mentor_follow_up_fallback(question),
|
||||||
"notice": "智能解读已自动切换可用服务。"
|
"notice": "智能解读已自动切换可用服务。"
|
||||||
if event.role == "fallback"
|
if event.role == "fallback"
|
||||||
else "",
|
else "",
|
||||||
@@ -167,6 +170,27 @@ class MentorServiceMixin:
|
|||||||
|
|
||||||
return generate()
|
return generate()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _mentor_follow_up_fallback(question: str) -> list[str]:
|
||||||
|
normalized = question.strip()
|
||||||
|
if any(keyword in normalized for keyword in ("风险", "亏损", "回撤", "止损")):
|
||||||
|
return [
|
||||||
|
"这些风险最早会从哪些信号中暴露?",
|
||||||
|
"哪些变化会让当前风险判断失效?",
|
||||||
|
"如果风险继续扩大,仓位预案应如何调整?",
|
||||||
|
]
|
||||||
|
if any(keyword in normalized for keyword in ("股票", "个股", "代码", "怎么看")):
|
||||||
|
return [
|
||||||
|
"这个判断最关键的确认信号是什么?",
|
||||||
|
"哪些变化会让当前结论失效?",
|
||||||
|
"明日盘中应该优先观察哪些数据?",
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
"这个判断最关键的确认依据是什么?",
|
||||||
|
"哪些变化会让当前结论失效?",
|
||||||
|
"下一步应该优先观察什么?",
|
||||||
|
]
|
||||||
|
|
||||||
def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]:
|
def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]:
|
||||||
mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
|
mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
|
||||||
trade_date = normalize_date(trade_date)
|
trade_date = normalize_date(trade_date)
|
||||||
|
|||||||
@@ -370,29 +370,29 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"css_layers": [
|
"css_layers": [
|
||||||
"/shared/tokens.css?v=20260729-1",
|
"/shared/tokens.css?v=20260806-2",
|
||||||
"/shared/base.css?v=20260802-5",
|
"/shared/base.css?v=20260806-1",
|
||||||
"/shared/shell.css?v=20260802-5",
|
"/shared/shell.css?v=20260806-2",
|
||||||
"/shared/auth.css?v=20260802-5",
|
"/shared/auth.css?v=20260806-1",
|
||||||
"/shared/components/controls.css?v=20260802-5",
|
"/shared/components/controls.css?v=20260806-1",
|
||||||
"/shared/components/navigation.css?v=20260802-5",
|
"/shared/components/navigation.css?v=20260806-1",
|
||||||
"/shared/components/cards.css?v=20260802-5",
|
"/shared/components/cards.css?v=20260806-1",
|
||||||
"/shared/components/tables.css?v=20260802-5",
|
"/shared/components/tables.css?v=20260806-1",
|
||||||
"/shared/components/dialogs.css?v=20260802-5",
|
"/shared/components/dialogs.css?v=20260806-1",
|
||||||
"/shared/components/feedback.css?v=20260802-5",
|
"/shared/components/feedback.css?v=20260806-1",
|
||||||
"/pages/market/foundation.css?v=20260802-5",
|
"/pages/market/foundation.css?v=20260806-1",
|
||||||
"/pages/sentiment/foundation.css?v=20260802-5",
|
"/pages/sentiment/foundation.css?v=20260806-2",
|
||||||
"/pages/pools/foundation.css?v=20260802-5",
|
"/pages/pools/foundation.css?v=20260806-2",
|
||||||
"/pages/ladder/foundation.css?v=20260802-5",
|
"/pages/ladder/foundation.css?v=20260806-1",
|
||||||
"/pages/rotation/foundation.css?v=20260802-5",
|
"/pages/rotation/foundation.css?v=20260806-1",
|
||||||
"/pages/auction/foundation.css?v=20260802-5",
|
"/pages/auction/foundation.css?v=20260806-2",
|
||||||
"/pages/themes/foundation.css?v=20260802-5",
|
"/pages/themes/foundation.css?v=20260806-1",
|
||||||
"/pages/popularity/foundation.css?v=20260802-5",
|
"/pages/popularity/foundation.css?v=20260806-1",
|
||||||
"/pages/dragon-tiger/foundation.css?v=20260802-5",
|
"/pages/dragon-tiger/foundation.css?v=20260806-1",
|
||||||
"/pages/screener/foundation.css?v=20260804-1",
|
"/pages/screener/foundation.css?v=20260806-2",
|
||||||
"/pages/mentor/foundation.css?v=20260802-5",
|
"/pages/mentor/foundation.css?v=20260806-2",
|
||||||
"/pages/heaven/foundation.css?v=20260804-3",
|
"/pages/heaven/foundation.css?v=20260806-2",
|
||||||
"/pages/review/foundation.css?v=20260802-5"
|
"/pages/review/foundation.css?v=20260806-1"
|
||||||
],
|
],
|
||||||
"frontend_composition": {
|
"frontend_composition": {
|
||||||
"shell": "frontend/index.html",
|
"shell": "frontend/index.html",
|
||||||
@@ -436,19 +436,24 @@
|
|||||||
"code_hotspots": [
|
"code_hotspots": [
|
||||||
{
|
{
|
||||||
"path": "frontend/pages/heaven/foundation.css",
|
"path": "frontend/pages/heaven/foundation.css",
|
||||||
"bytes": 185468,
|
"bytes": 185936,
|
||||||
"lines": 11721
|
"lines": 11734
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/pages/screener/foundation.css",
|
"path": "frontend/pages/screener/foundation.css",
|
||||||
"bytes": 101609,
|
"bytes": 102987,
|
||||||
"lines": 6509
|
"lines": 6565
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/pages/heaven/page.js",
|
"path": "frontend/pages/heaven/page.js",
|
||||||
"bytes": 97189,
|
"bytes": 97189,
|
||||||
"lines": 2069
|
"lines": 2069
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "frontend/shared/shell.css",
|
||||||
|
"bytes": 51978,
|
||||||
|
"lines": 3224
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/heaven/engine.py",
|
"path": "backend/features/heaven/engine.py",
|
||||||
"bytes": 51764,
|
"bytes": 51764,
|
||||||
@@ -456,13 +461,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/index.html",
|
"path": "frontend/index.html",
|
||||||
"bytes": 44688,
|
"bytes": 45846,
|
||||||
"lines": 632
|
"lines": 643
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "frontend/shared/shell.css",
|
|
||||||
"bytes": 40845,
|
|
||||||
"lines": 2825
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": 32995,
|
"bytes": 34990,
|
||||||
"lines": 2317
|
"lines": 2409
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "database.py",
|
"path": "database.py",
|
||||||
@@ -644,6 +644,11 @@
|
|||||||
"bytes": 6202,
|
"bytes": 6202,
|
||||||
"lines": 141
|
"lines": 141
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "frontend/pages/mentor/page.html",
|
||||||
|
"bytes": 6190,
|
||||||
|
"lines": 89
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/screener/selection.py",
|
"path": "backend/features/screener/selection.py",
|
||||||
"bytes": 6092,
|
"bytes": 6092,
|
||||||
@@ -659,11 +664,6 @@
|
|||||||
"bytes": 5690,
|
"bytes": 5690,
|
||||||
"lines": 124
|
"lines": 124
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "frontend/pages/mentor/page.html",
|
|
||||||
"bytes": 5501,
|
|
||||||
"lines": 72
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "backend/data/providers/tushare_indices.py",
|
"path": "backend/data/providers/tushare_indices.py",
|
||||||
"bytes": 5451,
|
"bytes": 5451,
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
# 小白复盘项目交接说明
|
||||||
|
|
||||||
|
> 核实日期:2026-08-06(Asia/Shanghai)
|
||||||
|
> 正式源码边界:`webapp/app/`
|
||||||
|
> 产品行为基准:`docs/product/小白复盘-完整产品规格说明书.md`
|
||||||
|
|
||||||
|
本文件不是聊天摘要。内容以当前仓库、配置注册表、测试、Git 状态和产品规格交叉核实为准。后续维护者应先阅读根目录 `AGENTS.md`、`ARCHITECTURE.md`、本文件和产品规格,再修改代码。
|
||||||
|
|
||||||
|
## 0. 状态口径与证据
|
||||||
|
|
||||||
|
本文使用四种状态,不能混用:
|
||||||
|
|
||||||
|
- **已实现**:当前正式源码中存在对应实现。
|
||||||
|
- **自动验证通过**:有测试或注册表检查证明,不等同于人工视觉验收。
|
||||||
|
- **人工已验收**:用户已经确认迁移后的正式 `app/` 在功能和视觉上与迁移前等价;该结论只覆盖当时基线。
|
||||||
|
- **待验收/待实现**:代码尚未完成,或虽已写入工作区但尚未取得本轮人工确认和 Git 回档点。
|
||||||
|
|
||||||
|
### 0.1 Git 与运行快照
|
||||||
|
|
||||||
|
- 分支:`main`。
|
||||||
|
- 当前提交:`bd97ba1 feat: unify trading workspace visual system`。
|
||||||
|
- `HEAD` 与 `origin/main` 一致;远端为内部 Gitea 仓库。
|
||||||
|
- 生成本文前工作区已有 37 个修改文件,约 `2490` 行新增、`2958` 行删除,主要是全站视觉调整和最新问师改造;这些改动不是本文创建的,禁止丢弃。
|
||||||
|
- 生成本文时 `8797` 端口没有监听进程,因此实时数据源和 LLM 的运行可用性没有通过在线健康检查确认。
|
||||||
|
- 当前正式数据库为 `data/review.db`,使用 SQLite WAL;数据库、`.env`、Token、私有 Skill、日志和运行产物不进入 Git。
|
||||||
|
- 本轮文档生成后的自动验证结果见本文末尾“验证记录”。
|
||||||
|
|
||||||
|
## 1. 项目目标和当前状态
|
||||||
|
|
||||||
|
### 1.1 项目目标
|
||||||
|
|
||||||
|
小白复盘是面向 A 股盘后复盘和盘前观察的本地/局域网 Web 工作台。目标不是自动交易,而是把真实行情、市场情绪、涨跌停结构、集合竞价、板块题材、选股、思维模型问答、传统文化观察和个人复盘放在一套可追溯、可复现、账号隔离的系统中。
|
||||||
|
|
||||||
|
产品必须坚持以下底线:
|
||||||
|
|
||||||
|
1. 不使用演示行情冒充真实数据,不静默混用日期、单位、复权或数据源。
|
||||||
|
2. 计算型数据缺失时失败关闭;公开网页源只允许作为已登记的展示兜底。
|
||||||
|
3. 阶段、策略筛选、情绪、观势取象和六爻排盘由确定性程序完成;LLM 只编译自然语言条件或解释确定性结果。
|
||||||
|
4. 用户自选、复盘、交易日志、问师/问天历史等私有数据必须按账号隔离。
|
||||||
|
5. PC 端优先达到稳定、精致、可长期维护;移动端必须独立设计,不能把 PC 页面简单压缩。
|
||||||
|
|
||||||
|
### 1.2 当前状态
|
||||||
|
|
||||||
|
正式版本已经从历史混乱目录保真迁入 `webapp/app/`,用户已人工确认迁移本身在功能和视觉上成功。项目已经完成模块化单体边界、页面碎片化、数据网关、LLM 网关、后台任务、数据库迁移、注册表和统一验收工具等结构治理。
|
||||||
|
|
||||||
|
当前不是“从零重写”状态,也不应再次从旧根目录或失败的 `next/` 复制实现。现阶段属于:
|
||||||
|
|
||||||
|
- 核心 PC 产品可用,16 个主工作区均有正式实现。
|
||||||
|
- 当前工作区正在进行全站 PC 视觉一致性调整,以及问师经典 QQ 式三栏界面和动态追问能力;自动化测试已覆盖,尚待本轮人工视觉验收和提交。
|
||||||
|
- 移动端明确暂停,当前存在样式但不能据此宣称可用。
|
||||||
|
- 完整 IC 动态加权、稳定宏观/政策/隔夜消息、分析师一致预期、Level-2 等依赖数据与算法的能力尚未完成。
|
||||||
|
- 局域网单实例是当前部署边界;公网多实例能力不属于当前完成范围。
|
||||||
|
|
||||||
|
## 2. 技术架构与主要目录
|
||||||
|
|
||||||
|
### 2.1 总体架构
|
||||||
|
|
||||||
|
项目采用**模块化单体**:一个 Python 进程、一个 SQLite WAL 数据库、无构建工具的 HTML/CSS/JavaScript 前端。
|
||||||
|
|
||||||
|
```text
|
||||||
|
Browser
|
||||||
|
-> frontend/shared/api.js
|
||||||
|
-> backend/http + backend/features/<feature>/routes.py
|
||||||
|
-> feature service
|
||||||
|
-> Repository / DataGateway / LLMGateway
|
||||||
|
-> SQLite / Tushare / iFinD / display-only providers / LLM provider
|
||||||
|
|
||||||
|
Scheduler
|
||||||
|
-> backend/jobs
|
||||||
|
-> 同一套 feature service / repository / gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
该结构适合当前局域网单实例产品:部署简单、数据本地、回档直接,同时通过领域边界避免再次退化成单文件应用。除非进入公网多实例阶段,不要提前引入微服务、消息队列或前端构建框架。
|
||||||
|
|
||||||
|
### 2.2 主要目录
|
||||||
|
|
||||||
|
| 路径 | 唯一职责 |
|
||||||
|
|---|---|
|
||||||
|
| `server.py` | 稳定启动/导入门面 |
|
||||||
|
| `backend/bootstrap/` | 配置、依赖组装、启动与组合根 |
|
||||||
|
| `backend/http/` | 鉴权、请求 ID、JSON/NDJSON、静态文件、流式连接和统一异常 |
|
||||||
|
| `backend/features/` | 按账户、市场、选股、问师、问天、复盘等领域组织业务、路由和 Repository |
|
||||||
|
| `backend/data/` | `DataGateway`、数据源策略、来源/日期/单位/新鲜度/覆盖率质量门 |
|
||||||
|
| `backend/data/providers/` | Tushare、iFinD 等供应商适配;不得由业务模块直接调用 |
|
||||||
|
| `backend/database/` | SQLite 连接、顺序迁移和 Repository 组合 |
|
||||||
|
| `backend/jobs/` | 行情刷新、盘后选股、事件补充的锁、状态、幂等和重试 |
|
||||||
|
| `backend/llm/` | 模型选择、会员/额度、主辅回退、流式协议、取消和审计 |
|
||||||
|
| `frontend/index.html` | 登录层、全站 Shell、摘要条、状态栏、全局弹窗和唯一页面挂载点 |
|
||||||
|
| `frontend/shared/` | 唯一 API 出口、状态、Shell、会话、主题和公共组件 |
|
||||||
|
| `frontend/pages/` | 页面局部 `page.html`、`page.js`、`foundation.css` |
|
||||||
|
| `config/` | 页面、功能、API、数据字段、质量和任务注册表 |
|
||||||
|
| `data/` | 正式数据库与私有数据,不入 Git |
|
||||||
|
| `runtime/` | 日志、PID、缓存、测试结果,不入 Git |
|
||||||
|
| `tests/` | Python 单元/边界/契约测试与 Playwright 浏览器回归 |
|
||||||
|
| `tools/` | 启动、注册表生成、架构清单和统一验收工具 |
|
||||||
|
| `docs/` | 产品规格、维护、治理、历史迁移和当前交接/Issue |
|
||||||
|
|
||||||
|
### 2.3 注册表和运行事实
|
||||||
|
|
||||||
|
- `config/pages.config.json`:16 个主页面,默认页为情绪周期。
|
||||||
|
- `config/features.config.json`:20 个功能及 `public/authenticated/member/admin` 权限。
|
||||||
|
- `config/api.config.json`:当前 53 个精确 API 路径和 11 个正则路径,由工具生成并校验。
|
||||||
|
- `config/jobs.config.json`:行情刷新、15:10 后盘后选股、iFinD 事件补充三类任务。
|
||||||
|
- `config/data-fields.config.json`:数据源与字段用途;Tushare/iFinD 可进入已登记计算,东方财富/腾讯只允许展示,未解决数据集显式阻塞。
|
||||||
|
- `config/data-quality.config.json`:单位、覆盖率、新鲜度和失败关闭规则。
|
||||||
|
- `config/architecture-inventory.json`:生成的架构清单和代码热点,不应手工编造。
|
||||||
|
|
||||||
|
### 2.4 数据源边界
|
||||||
|
|
||||||
|
| 数据源 | 当前角色 | 约束 |
|
||||||
|
|---|---|---|
|
||||||
|
| Tushare | 交易日、股票主数据、日线、估值、财务、资金、申万行业、涨跌停、最终竞价、热榜、龙虎榜等主要计算数据 | 按接口权限和质量门使用 |
|
||||||
|
| iFinD | 动态竞价、展示型日 K/分时和盘后事件补充 | 凭据/授权到期时必须显式不可用,不得伪造 |
|
||||||
|
| 东方财富/腾讯 | 分时或实时指数的展示观察兜底 | 不得静默进入情绪、选股或问天计算 |
|
||||||
|
| Local | 情绪等确定性派生结果 | 保存算法/输入版本,保证可复现 |
|
||||||
|
| unresolved | 分析师一致预期、Level-2 | 当前阻塞,不能用名称或空字段冒充实现 |
|
||||||
|
|
||||||
|
## 3. 已完成功能
|
||||||
|
|
||||||
|
以下表示当前正式源码存在实现;人工视觉结论仅继承用户对迁移基线的确认,不覆盖本轮未提交视觉改动。
|
||||||
|
|
||||||
|
### 3.1 全局与账户
|
||||||
|
|
||||||
|
- 注册、登录、退出、首账号管理员、普通/会员/管理员权限。
|
||||||
|
- 个人资料、生辰资料、修改密码、会员状态、系统管理与公共凭据配置。
|
||||||
|
- 顶栏日期、默认最近真实交易日、情绪摘要条、日间/夜间、全局搜索、提醒中心。
|
||||||
|
- 股票、题材、板块、指数详情;日 K/分时与代码/题材悬浮预览。
|
||||||
|
- 统一 Toast、弹窗、空态、加载、错误转换和页面生命周期基础设施。
|
||||||
|
|
||||||
|
### 3.2 市场复盘页面
|
||||||
|
|
||||||
|
- 情绪周期:温度、阶段、方向、置信度、构成、趋势和交易日明细。
|
||||||
|
- 涨停池、炸板池、跌停池、昨日涨停、涨停表现。
|
||||||
|
- 市场天梯、板块轮动与成分股联动。
|
||||||
|
- 集合竞价:盘前状态、9:25 最终筛选、普通异动/一字板、成交额对比和自选。
|
||||||
|
- 题材库、人气热榜、龙虎榜和游资名录/详情基础能力。
|
||||||
|
|
||||||
|
### 3.3 智能选股
|
||||||
|
|
||||||
|
- 六阶段盘后候选、29 套精选策略、策略适用说明和确定性候选结果。
|
||||||
|
- 自定义公式 DSL、自然语言编译公式、因子与权重手动配置。
|
||||||
|
- 候选按策略/日期隔离,盘后自动发布最近完整交易日结果。
|
||||||
|
- 用户手动加入五交易日策略跟踪,T+1/T+3/T+5 反馈和幂等提醒。
|
||||||
|
- 数据缺失、无符合条件、任务失败等状态区分。
|
||||||
|
- 当前多因子为基础动态版;完整 IC 版不在“已完成”范围内。
|
||||||
|
|
||||||
|
### 3.4 问师与 LLM
|
||||||
|
|
||||||
|
- 公共/管理员私有思维模型 Skill 注册、证据等级、关注维度和排序偏好。
|
||||||
|
- 按账号、模型、交易日隔离对话;最多带入最近 10 条历史。
|
||||||
|
- 按模型类型提供不同市场上下文,识别个股时追加有限标的数据。
|
||||||
|
- 统一 LLM 会员/额度、主辅回退、流式去重、停止生成、审计和安全错误。
|
||||||
|
- 当前工作区已经实现经典 QQ 式联系人/会话/资料三栏和同次调用动态追问;状态为“自动验证通过、待人工验收和提交”,详见 Issue 001。
|
||||||
|
|
||||||
|
### 3.5 问天
|
||||||
|
|
||||||
|
- 观势:真实行情安全门、三才六爻、势值、本卦/之卦、客观数据补录与恢复自动数据。
|
||||||
|
- 观气:历法、节气、中运/司天在泉/主客气、个人合参、五行行业取象和每日解运持久化。
|
||||||
|
- 观心:交易/心境/无题预设、呼吸流程、六次铜钱起卦、第一念、京房纳甲/八宫世应/六亲/六神/旬空等确定性排盘。
|
||||||
|
- 本地知识检索、答案一致性校验和 LLM 解释;LLM 不起卦、不修改程序结果。
|
||||||
|
|
||||||
|
### 3.6 个人复盘
|
||||||
|
|
||||||
|
- 账号私有自选追踪、个股笔记、三个独立输入框的每日复盘及历史。
|
||||||
|
- 结构化交易日志、编辑删除、胜率/盈亏/仓位统计。
|
||||||
|
- 复盘助手流式对话,读取共享市场和当前用户记录,不执行交易。
|
||||||
|
- 手工提醒、已读状态、策略跟踪 T+1/T+5 自动提醒和幂等去重。
|
||||||
|
|
||||||
|
### 3.7 工程治理
|
||||||
|
|
||||||
|
- 正式源码独立于父目录旧程序和失败 `next/`。
|
||||||
|
- 页面结构、行为和样式已按领域拆分;浏览器请求统一经过 `frontend/shared/api.js`。
|
||||||
|
- Tushare 大客户端、智能选股、问天、市场洞察和 HTTP 层已拆成职责明确的模块门面。
|
||||||
|
- 有正式数据库 migration、数据/LLM/job 网关、API/功能/页面/数据注册表。
|
||||||
|
- 统一验收工具覆盖 Python、注册表、JS 语法、Git 空白、SQLite 完整性和可选 Playwright。
|
||||||
|
|
||||||
|
## 4. 尚未完成的功能
|
||||||
|
|
||||||
|
每项均有独立 Issue,Issue 状态优先于历史聊天中的阶段编号。
|
||||||
|
|
||||||
|
| Issue | 状态 | 优先级 | 未完成内容 |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| [ISSUE-001](issues/ISSUE-001-finalize-mentor-redesign.md) | 待人工验收/提交 | P0 | 问师三栏界面、停止生成和动态追问收口 |
|
||||||
|
| [ISSUE-002](issues/ISSUE-002-checkpoint-current-pc-visual-work.md) | 待审查/提交 | P0 | 当前全站 PC 视觉改动的逐页验收、拆分和回档点 |
|
||||||
|
| [ISSUE-003](issues/ISSUE-003-mobile-redesign.md) | 明确延期 | P2 | 独立移动 Shell、逐页信息架构和触控交互 |
|
||||||
|
| [ISSUE-004](issues/ISSUE-004-full-ic-multifactor.md) | 未实现 | P1 | 12 个月 Rank IC、季度重算、中性化和前 5% 输出 |
|
||||||
|
| [ISSUE-005](issues/ISSUE-005-policy-macro-overnight-data.md) | 数据源未定 | P1 | 稳定政策/宏观/隔夜消息序列与竞价量化 |
|
||||||
|
| [ISSUE-006](issues/ISSUE-006-analyst-consensus-data.md) | 数据阻塞 | P2 | 一致预期、预测修正、评级/目标价等字段 |
|
||||||
|
| [ISSUE-007](issues/ISSUE-007-level2-auction.md) | 授权阻塞 | P2 | Level-2 委托队列、逐笔和动态竞价深度 |
|
||||||
|
| [ISSUE-008](issues/ISSUE-008-hot-money-profile-history.md) | 低优先级 | P3 | 游资档案的更完整历史画像和归类质量 |
|
||||||
|
| [ISSUE-009](issues/ISSUE-009-documentation-status-drift.md) | 待整理 | P1 | 活跃文档/注册表中移动端、端口和验收状态漂移 |
|
||||||
|
| [ISSUE-010](issues/ISSUE-010-live-provider-llm-readiness.md) | 待运行核验 | P0 | 启动正式服务并验证数据源、iFinD、LLM 与任务健康 |
|
||||||
|
| [ISSUE-011](issues/ISSUE-011-public-deployment-hardening.md) | 未来范围 | P3 | 公网多实例、TLS、PostgreSQL、队列、缓存和集中监控 |
|
||||||
|
|
||||||
|
明确不是待办:问师自主联网取数当前已因风险高于收益而延期;全能金融爬虫 Skill 已放弃;旧 `next/` 已冻结失败;不要把这些内容重新加入实现。
|
||||||
|
|
||||||
|
## 5. 已知问题与风险
|
||||||
|
|
||||||
|
### 5.1 用户可见问题
|
||||||
|
|
||||||
|
1. **移动端整体不可用或交互较差。** 当前存在大量媒体查询和 `mobile_layout: dedicated` 注册值,但这只证明代码存在,不证明通过人工可用性验收。
|
||||||
|
2. **当前问师与全站视觉改动未完成交付闭环。** 自动化已通过,但工作区未提交,且用户尚未对本轮 QQ 式问师界面进行视觉确认。
|
||||||
|
3. **实时数据和 LLM 当前在线状态未知。** 生成本文时 8797 未启动;外部服务还受本机网络、系统凭据、接口权限和 iFinD 授权有效期影响。
|
||||||
|
4. **缺失数据不能被误显示为无信号。** 分析师一致预期、Level-2 和部分宏观/新闻数据目前无正式来源;相关策略或页面必须显示数据缺失/阻塞。
|
||||||
|
|
||||||
|
### 5.2 维护风险
|
||||||
|
|
||||||
|
- `frontend/pages/heaven/foundation.css` 约 11,734 行、`frontend/pages/screener/foundation.css` 约 6,565 行、`frontend/shared/shell.css` 约 3,224 行;它们是当前最大 CSS 热点。没有具体回归证据时不得为了“减行数”盲拆。
|
||||||
|
- `frontend/pages/heaven/page.js` 约 2,069 行、`backend/features/heaven/engine.py` 约 1,183 行,问天仍是高复杂度领域。
|
||||||
|
- 根 `database.py` 仍是历史 schema/Repository 组合锚点,不是新增业务查询的位置;继续向其中加功能会破坏治理结果。
|
||||||
|
- 自动化测试不能替代产品规格第 25 至 27 节的全矩阵人工验收,尤其是外部真实数据、LLM、日夜主题、1080P/4K 和移动端。
|
||||||
|
- 当前脏工作区横跨 37 个文件。提交前必须按功能拆分或至少留下清晰回档说明,不能把无关改动混成无法审计的大提交。
|
||||||
|
|
||||||
|
## 6. 已作出的重要技术决策及原因
|
||||||
|
|
||||||
|
| 决策 | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| `webapp/app/` 是唯一正式源码 | 已完成保真迁移并人工确认;避免继续依赖父目录旧代码或失败 `next/` |
|
||||||
|
| 保持模块化单体 | 当前局域网单实例用一个进程和 SQLite 最简单;领域边界已经足以控制复杂度 |
|
||||||
|
| 不更换技术栈,前端保持无构建 HTML/CSS/JS | 迁移目标是整理和减法,不是重拍功能;减少部署与人工维护成本 |
|
||||||
|
| 页面、功能、API、数据和任务采用注册表 | 防止入口散落、权限漂移和“代码有但系统不知道” |
|
||||||
|
| 浏览器 API、外部数据和 LLM 各自只有一个网关出口 | 统一鉴权、错误、质量、额度、降级和审计 |
|
||||||
|
| 计算数据失败关闭,展示兜底隔离 | 防止公开网页源或旧快照静默污染情绪、选股、竞价和问天结果 |
|
||||||
|
| 智能选股由条件和数据确定执行,LLM 只编译公式 | 保证同日期同策略可复现,避免刷新结果漂移 |
|
||||||
|
| 问天确定性引擎负责历法/卦象,LLM 只解释 | 结果可复现、可测试,避免模型改卦或编造事实 |
|
||||||
|
| 问师外部工具自主取数暂缓 | 当前缺少成熟权限、来源和失败边界,风险高于收益 |
|
||||||
|
| 放弃通用金融爬虫 Skill | 网页规则不稳定、版权/安全/口径不可控,不适合进入正式计算链 |
|
||||||
|
| 移动端暂停并要求独立设计 | 密集 PC 表格不能靠压缩获得可用手机体验;先保证 PC 功能与视觉 |
|
||||||
|
| 不确定代码默认保留,删除需扫描、差异测试和人工验收 | 防止“减法”误删隐含功能;历史迁移日志用于回档证据 |
|
||||||
|
| 公网能力不提前实现 | 当前用户场景是本地/局域网;多实例、PostgreSQL 和队列应由真实部署需求驱动 |
|
||||||
|
|
||||||
|
## 7. 当前正在处理的事项
|
||||||
|
|
||||||
|
### 7.1 问师改造
|
||||||
|
|
||||||
|
当前未提交代码已经完成:
|
||||||
|
|
||||||
|
- 经典 QQ 式 PC 三栏结构:联系人、对话、当前模型资料/证据。
|
||||||
|
- 动态追问:模型在同一次输出末尾返回 `<XIAOBAI_FOLLOW_UPS>` 机器块;服务端剥离机器块,并在最终 NDJSON `meta.follow_ups` 返回 2 至 3 条建议。
|
||||||
|
- 动态追问不额外调用 LLM、不重复扣额度;点击只预填输入框。
|
||||||
|
- 停止生成控制、Enter 发送、流式占位与回答状态。
|
||||||
|
- 问师 CSS 从历史约 2,800 行收敛到约 988 行。
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `backend/features/mentor/agent.py`
|
||||||
|
- `backend/features/mentor/service.py`
|
||||||
|
- `frontend/pages/mentor/page.html`
|
||||||
|
- `frontend/pages/mentor/page.js`
|
||||||
|
- `frontend/pages/mentor/foundation.css`
|
||||||
|
- `tests/test_mentor_stream.py`
|
||||||
|
- `tests/e2e/app-shell.spec.js`
|
||||||
|
|
||||||
|
尚缺:启动正式服务、接入真实 LLM 做一次端到端验证、用户人工确认日间/夜间及 1080P/4K 视觉、建立提交并推送回档点。
|
||||||
|
|
||||||
|
### 7.2 当前全站视觉改动
|
||||||
|
|
||||||
|
工作区还包含 Shell、设计令牌、公共组件以及市场、情绪、股池、天梯、轮动、竞价、题材、热榜、龙虎榜、选股、问天、复盘等页面样式改动。它们已进入自动化回归,但尚未形成独立验收结论。移动端已被产品决策暂停,因此不能因为这些 CSS 中存在移动规则就标记移动端完成。
|
||||||
|
|
||||||
|
## 8. 推荐的后续执行顺序
|
||||||
|
|
||||||
|
1. **先恢复运行环境并核验外部能力。** 启动 8797,检查健康、登录、最近真实交易日、Tushare/iFinD、LLM 主辅模型和后台任务;不通过时先解决 Issue 010。
|
||||||
|
2. **人工验收问师。** 完成 Issue 001 的真实 LLM、流式、停止、动态追问、日夜和分辨率检查。
|
||||||
|
3. **审查当前全站视觉差异。** 按 16 页逐页检查 Issue 002,确认哪些是 PC 正式改动、哪些是已暂停移动尝试,保持功能等价。
|
||||||
|
4. **建立回档点。** 将问师和全站视觉按可审计边界提交并推送,不夹带密钥、数据库或运行产物。
|
||||||
|
5. **清理活跃文档状态漂移。** 完成 Issue 009,使 README、注册表和交接状态不再暗示移动端已验收。
|
||||||
|
6. **先补可获得的高价值数据,再升级算法。** 先确定 Issue 005/006 的合法稳定来源,再实施 Issue 004;没有完整历史覆盖时不能伪造 IC。
|
||||||
|
7. **有正式授权后再做 Level-2。** Issue 007 不能用普通快照模拟。
|
||||||
|
8. **低优先级完善游资档案。** Issue 008 不应阻塞市场、选股、问师和问天稳定性。
|
||||||
|
9. **PC 稳定后才重启移动端设计。** Issue 003 必须单独打样和逐页人工验收。
|
||||||
|
10. **确定公网商业化再做部署升级。** Issue 011 需要单独架构决策和迁移方案。
|
||||||
|
|
||||||
|
## 9. 每项任务的验收标准
|
||||||
|
|
||||||
|
本节是交接总表;独立 Issue 内给出更具体的范围和命令。产品规格第 25 至 27 节固定案例仍是最终依据。
|
||||||
|
|
||||||
|
| 任务 | 必须满足的验收标准 |
|
||||||
|
|---|---|
|
||||||
|
| Issue 001 问师收口 | 真实 LLM 只输出一份正文;同次调用出现 2 至 3 条有效追问;点击只预填;停止后不上演回退重放;无额外额度;日夜、1080P/4K 人工通过 |
|
||||||
|
| Issue 002 PC 视觉回档 | 16 页日间/夜间、1920×1080、4K 无白块、遮挡、双滚动和功能回归;当前差异可解释;提交可独立回退 |
|
||||||
|
| Issue 003 移动端 | 320/375/390/430/768 及横屏无页面横溢;底部五入口、市场子导航、弹窗/抽屉、宽表和键盘交互可用;用户逐页验收 |
|
||||||
|
| Issue 004 完整 IC | 行业内去极值、z-score、行业/市值中性化、过去 12 月下期收益 Rank IC、季度重算、前 5% 均有版本化确定性测试;无未来函数;UI 明确基础/IC 模式 |
|
||||||
|
| Issue 005 政策宏观隔夜 | 合法稳定来源、字段/单位/时间/版权/新鲜度登记完整;历史归档可复现;缺失显式失败;消息只按确认规则进入竞价量化 |
|
||||||
|
| Issue 006 一致预期 | 五类字段有 point-in-time 历史、公告时点和覆盖率;策略缺数据与无命中可区分;回测无未来函数 |
|
||||||
|
| Issue 007 Level-2 | 有正式授权;委托队列/逐笔/快照时间可追溯;盘中断线不伪造;与 9:25 最终归档区分;回放测试通过 |
|
||||||
|
| Issue 008 游资画像 | 名录、别名、席位归类和历史操作可追溯;未知席位保留;同名误合并有回归测试;左名录右详情无超长弹窗 |
|
||||||
|
| Issue 009 文档漂移 | 活跃文档、端口、移动端状态、完成状态与注册表一致;历史迁移文档明确只作审计,不被当运行说明;文档链接有效 |
|
||||||
|
| Issue 010 在线就绪 | `/api/health` 可达;登录和最近真实快照正常;数据源与 LLM 分别可诊断;失效凭据不泄露;重启后任务与结果不重复 |
|
||||||
|
| Issue 011 公网部署 | 完成 ADR;TLS、可信 Host、限流、集中密钥、审计、备份恢复、多实例数据库和任务互斥全部通过;不破坏局域网数据边界 |
|
||||||
|
|
||||||
|
### 9.1 通用自动验收
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\MoBai\Documents\gupiaofupan\webapp\app
|
||||||
|
python tools/verify_baseline.py
|
||||||
|
python tools/verify_baseline.py --e2e
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 通用人工验收
|
||||||
|
|
||||||
|
- 普通、会员、管理员三种权限。
|
||||||
|
- 正常、有数据为空、数据缺失、上游失败、请求超时、最近快照九类状态。
|
||||||
|
- 日间、夜间、1920×1080、3840×2160;移动 Issue 开始后再加入完整移动视口矩阵。
|
||||||
|
- 真实行情日期与图表一致;开盘前不制造当天空 K 线。
|
||||||
|
- 用户甲乙的自选、复盘、日志、对话、问天历史互不可见。
|
||||||
|
- 所有保存/删除/添加只出现可关闭的规范反馈,不出现超长空弹窗。
|
||||||
|
- 密钥、数据库、日志和私有 Skill 不进入 Git diff。
|
||||||
|
|
||||||
|
## 10. 验证记录
|
||||||
|
|
||||||
|
2026-08-06 本轮结果;后续代码变化后不能沿用:
|
||||||
|
|
||||||
|
- Python:326 个测试全部通过(约 11.8 秒)。
|
||||||
|
- Playwright:49 个测试全部通过(约 2.2 分钟)。
|
||||||
|
- API 注册表与架构清单:均为 current。
|
||||||
|
- JavaScript:统一工具枚举的全部 `.js/.mjs` 均通过 `node --check`。
|
||||||
|
- SQLite:`data/review.db` 的 `PRAGMA integrity_check` 为 `ok`,验证时大小为 455,434,240 字节。
|
||||||
|
- Git:`git diff --check` 通过。
|
||||||
|
- 说明:组合命令在本代理的 120 秒命令上限处被终止于 Playwright 阶段;Playwright 随后以同一配置单独运行并完整通过,因此上述各子项均有本轮实际结果。
|
||||||
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
# 文档索引
|
# 文档索引
|
||||||
|
|
||||||
- `product/小白复盘-完整产品规格说明书.md`:从零恢复产品时的完整功能与行为资产。
|
- `product/小白复盘-完整产品规格说明书.md`:从零恢复产品时的完整功能与行为资产。
|
||||||
|
- `HANDOFF.md`:当前仓库、架构、完成度、风险和后续验收的交接基线。
|
||||||
|
- `issues/README.md`:尚未完成事项的独立 Issue 索引;Issue 不是已创建的 Gitea 工单。
|
||||||
- `maintenance/人工维护指南.md`:当前正式源码的启动、修改、验收、数据和回退流程。
|
- `maintenance/人工维护指南.md`:当前正式源码的启动、修改、验收、数据和回退流程。
|
||||||
- `governance/`:架构决策、注册表治理和历次结构治理记录。
|
- `governance/`:架构决策、注册表治理和历次结构治理记录。
|
||||||
- `migration/`:从旧根目录保真迁入`app/`的历史账本、证据和失败版本记录。
|
- `migration/`:从旧根目录保真迁入`app/`的历史账本、证据和失败版本记录。
|
||||||
|
|
||||||
日常维护优先阅读根目录`AGENTS.md`、`ARCHITECTURE.md`和维护指南。`migration/`只用于审计与
|
日常维护优先阅读根目录`AGENTS.md`、`ARCHITECTURE.md`、`HANDOFF.md`和维护指南。`migration/`只用于审计与
|
||||||
追溯,不参与应用启动、测试选择或运行时路径解析。
|
追溯,不参与应用启动、测试选择或运行时路径解析。
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# ISSUE-001:问师界面与动态追问收口
|
||||||
|
|
||||||
|
- 状态:待人工验收/提交
|
||||||
|
- 优先级:P0
|
||||||
|
- 来源:当前工作区问师改造、产品规格 L06、14.3
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
收口当前未提交的经典 QQ 式 PC 三栏问师界面和同次 LLM 调用动态追问,保持既有权限、流式、额度、对话隔离和错误回退行为。
|
||||||
|
|
||||||
|
## 已有实现
|
||||||
|
|
||||||
|
- 联系人、会话、资料/证据三栏。
|
||||||
|
- `<XIAOBAI_FOLLOW_UPS>` 机器块剥离,最终 NDJSON `meta.follow_ups` 返回 2 至 3 条。
|
||||||
|
- 点击追问只预填输入框;停止按钮不重新播放答案;没有第二次业务 LLM 调用。
|
||||||
|
- 相关 Python 和 Playwright 测试已加入工作区。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
不在本 Issue 内加入问师自动联网取数、金融爬虫、真人身份暗示或新的交易建议能力。
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
本地 LLM 主/辅助凭据、会员账号、可用的真实市场快照,以及 Issue 002 的公共 Shell 视觉状态。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 会员可以切换模型、加载历史、发送问题、停止生成、清空当前会话。
|
||||||
|
2. 流式正文只出现一次;主模型首字前失败最多辅助回退一次;已输出后失败不重放。
|
||||||
|
3. 成功回答显示 2 至 3 条相关追问;点击不自动发送、不增加额度;下一轮、换模型、清空后旧追问消失。
|
||||||
|
4. 追问结构无效、回答失败或用户停止时不显示追问。
|
||||||
|
5. 日间/夜间及 1920×1080、3840×2160 无白边、遮挡、超长弹窗和输入区不可达。
|
||||||
|
6. 普通用户不能发起 LLM 调用;账号甲乙对话互不可见。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest tests.test_mentor_stream tests.test_llm_stream tests.test_llm_gateway
|
||||||
|
python tools/verify_baseline.py --e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
通过人工验收后单独提交并推送,记录回档提交号。
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# ISSUE-002:当前 PC 全站视觉改动验收与回档
|
||||||
|
|
||||||
|
- 状态:待审查/提交
|
||||||
|
- 优先级:P0
|
||||||
|
- 来源:工作区现有 37 个修改文件和 `bd97ba1` 之后的视觉调整
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
逐页确认当前公共 Shell、令牌、夜间模式和 16 个页面的视觉修改,保留用户认可的 PC 变化,拆出或回退无关变化,并建立可以人工回档的提交。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
检查 `frontend/shared/`、所有 `frontend/pages/*/foundation.css`、页面 HTML/JS、架构清单和对应 E2E 测试。重点是夜间白块、边距/滚动、表格对齐、弹窗层级、图表背景和问师三栏。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
不以本 Issue 重写业务算法,不删除未确认的旧代码,不开始移动端独立设计;移动端另见 Issue 003。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 16 个页面均可从默认情绪周期进入,功能和权限与提交前一致。
|
||||||
|
2. 日间/夜间主题一次切换完成,不出现白闪、白色表头/搜索框/弹窗或 Hover 反色不可读。
|
||||||
|
3. 1920×1080 和 3840×2160 下页面内容、固定状态栏、全页滚动和局部滚动符合规格,无双滚动冲突。
|
||||||
|
4. 交易日明细、股池、天梯、轮动、竞价、题材、龙虎榜和选股的列宽/对齐/空态可读。
|
||||||
|
5. 每个修改文件都有明确原因;提交不包含 `.env`、数据库、日志、缓存、截图或私有 Skill。
|
||||||
|
6. 自动化、人工验收和 Git diff 检查均通过,提交可独立回退。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/verify_baseline.py --e2e
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
人工截图至少覆盖日间/夜间、1920×1080、3840×2160;验收结论写回 `docs/HANDOFF.md`。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ISSUE-003:移动端独立重设
|
||||||
|
|
||||||
|
- 状态:明确延期,PC 稳定前不启动
|
||||||
|
- 优先级:P2
|
||||||
|
- 来源:产品规格第 20 节;当前移动端人工验收未通过
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在不改变 PC DOM、API、权限、数据口径和业务结果的前提下,单独设计移动 Shell、底部五入口、行情子导航、对话输入、抽屉和宽表摘要视图。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
不能把桌面页面缩小、不能用 Hover 作为唯一入口、不能通过隐藏页面解决不可达问题,也不修改 PC 视觉作为“移动适配”。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 320、375、390、430、768px 及手机横屏无页面横向溢出和内容遮挡。
|
||||||
|
2. 底部行情/选股/问师/问天/复盘五入口可触控,目标不小于 44×44px。
|
||||||
|
3. 行情子页面通过选择器或抽屉切换;宽表在 320px 使用摘要+详情或局部横滚,页面本身不横滚。
|
||||||
|
4. 弹窗改为底部抽屉或全屏页后仍有关闭/返回路径;键盘弹出不遮挡问师/复盘输入区。
|
||||||
|
5. 问天动画、表格状态、日夜主题和权限锁定在窄屏可读。
|
||||||
|
6. 用户人工逐页验收后,才能把 `pages.config` 的移动意图标为完成。
|
||||||
|
|
||||||
|
## 依赖与验证
|
||||||
|
|
||||||
|
依赖 Issue 002。需要 Playwright 视口回归、真实手机人工操作和产品规格第 25.8、27.3 验收案例。
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ISSUE-004:完整 IC 动态多因子
|
||||||
|
|
||||||
|
- 状态:未实现,当前仅有基础动态多因子
|
||||||
|
- 优先级:P1
|
||||||
|
- 来源:产品规格 17.6、24.2;当前实现基线明确不得夸大
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
把五类因子(估值、成长、质量、动量、情绪)从基础合成升级为可复现的 IC 动态加权模式,同时保留用户手动权重的专业模式。
|
||||||
|
|
||||||
|
## 必须实现
|
||||||
|
|
||||||
|
- 行业内去极值。
|
||||||
|
- 因子 z-score 标准化。
|
||||||
|
- 行业和市值中性化。
|
||||||
|
- 使用过去 12 个月“因子值与下一期收益”的 Rank IC 均值定权。
|
||||||
|
- 每季度重算、记录算法版本和样本覆盖。
|
||||||
|
- 综合得分前 5% 输出;数据不足时逐因子说明缺失。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
不使用 LLM 计算分数,不把缺失当 0,不将基础动态权重 UI 改名为 IC 完整版。
|
||||||
|
|
||||||
|
## 验收标准与验证
|
||||||
|
|
||||||
|
1. 固定样本可复现每个因子预处理、IC、权重和排名。
|
||||||
|
2. 严格按公告时点和下一期收益计算,无未来函数。
|
||||||
|
3. 季度边界、行业小样本、缺因子、负 IC 和极端值均有测试。
|
||||||
|
4. UI 明确“基础动态权重/IC 自动权重”差异及数据日期。
|
||||||
|
5. 回测结果保存输入快照、算法版本和输出版本。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m unittest discover -s tests
|
||||||
|
```
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ISSUE-005:政策、宏观与隔夜消息数据
|
||||||
|
|
||||||
|
- 状态:数据源未定
|
||||||
|
- 优先级:P1
|
||||||
|
- 来源:产品规格 14.4、17.6、23.3;当前字段注册表尚无稳定计算来源
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
为宏观思维模型、集合竞价和问师补充可授权、可归档、带时间戳的政策、公告、指数、ETF、汇率、利率、商品和隔夜资讯数据。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 每个字段登记来源、授权、发布时间、交易日、单位、新鲜度、覆盖率和显示/计算用途。
|
||||||
|
2. 历史结果能按快照和版本复现,公告发布时间晚于目标时点的数据不能进入过去结果。
|
||||||
|
3. 消息去重、来源冲突、撤回/修订和上游失败有明确规则。
|
||||||
|
4. 竞价量化只使用已经登记且在目标时点可见的消息证据;无数据显示“数据缺失”,不显示“暂无信号”。
|
||||||
|
5. 问师宏观模型只追加适用上下文,不把政策新闻强塞给其他流派。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
不接入未经授权的网页爬虫,不把金融爬虫 Skill 作为正式 Provider。
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
供应商授权和 Issue 010 的数据质量/凭据核验。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# ISSUE-006:分析师一致预期数据
|
||||||
|
|
||||||
|
- 状态:数据阻塞(`research.consensus` 为 `unresolved`)
|
||||||
|
- 优先级:P2
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
提供按公告时间可追溯的盈利预测、一致预期、预测修正、评级变化、目标价和研报数量,供策略因子和问师宏观/基本面上下文按需使用。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 字段包含来源、分析师/机构(如授权允许)、公告时间、报告期、单位、币种和版本。
|
||||||
|
2. 目标日期只能读取当时已发布的数据;修正保留历史,不覆盖旧快照。
|
||||||
|
3. 缺失、过期、覆盖不足和无命中状态可区分。
|
||||||
|
4. 策略候选与回测固定输入下可复现;不能把缺失预测当 0 或中性。
|
||||||
|
5. 权限和授权边界经过审计,私有/授权数据不进入浏览器或普通日志。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
没有稳定授权来源前,不在 UI 中显示伪造的“分析师共识”指标。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# ISSUE-007:Level-2 与动态竞价深度
|
||||||
|
|
||||||
|
- 状态:授权阻塞(`market.level2` 为 `unresolved`)
|
||||||
|
- 优先级:P2
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在取得正式授权后,接入逐笔成交、逐笔委托、委托队列、未匹配量和开板深度,增强集合竞价与盘中观察。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 供应商、授权、字段、时间精度、单位和保留期限登记在数据配置。
|
||||||
|
2. 动态快照标出采集时间;断线、延迟和部分覆盖不会伪装成实时完整数据。
|
||||||
|
3. 9:15–9:25 动态观察与 9:25 最终竞价归档分开保存;不能用最终快照冒充动态过程。
|
||||||
|
4. Level-2 数据不进入未授权的历史回测;重连和重复消息幂等。
|
||||||
|
5. UI 在数据缺失时保留已有成功快照并给出可执行提示。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
未获得授权前不使用公开网页抓取或模拟队列填充。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# ISSUE-008:游资档案历史画像
|
||||||
|
|
||||||
|
- 状态:低优先级,基础名录和详情已存在
|
||||||
|
- 优先级:P3
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
完善游资名录、席位别名、历史上榜、买卖倾向、常见题材和证据来源,支持龙虎榜页面左名录右详情,不再依赖超长弹窗。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. Tushare 已收录席位完整展示;未知或无法识别席位保留原名并标记待归类。
|
||||||
|
2. 别名合并有来源、人工修订记录和可回退历史。
|
||||||
|
3. 历史画像按日期、股票、方向和金额可追溯;不把单日行为直接概括为稳定风格。
|
||||||
|
4. 点击名录进入详情,桌面/夜间/低分辨率可读,无异常空弹窗。
|
||||||
|
5. 龙虎榜出现“有股票上榜但席位不可识别”时,展示该状态而不是“无数据”。
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
依赖稳定龙虎榜席位数据和人工别名维护;不阻塞其他市场页面。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ISSUE-009:活跃文档和状态漂移
|
||||||
|
|
||||||
|
- 状态:待整理
|
||||||
|
- 优先级:P1
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让活跃文档、配置注册表、启动端口和完成状态与正式 `app/` 实际一致,历史迁移材料继续保留但明确仅用于审计。
|
||||||
|
|
||||||
|
## 检查范围
|
||||||
|
|
||||||
|
- `README.md`、`docs/README.md`、`docs/maintenance/人工维护指南.md`。
|
||||||
|
- `config/pages.config.json` 的移动端意图与实际验收状态。
|
||||||
|
- 默认端口 8765、局域网验收端口 8797 及 Docker 文档的区分。
|
||||||
|
- 迁移文档中的“待人工验收”等历史表述是否被误读为当前状态。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 活跃文档只描述当前正式源码和真实启动方式,过期内容链接到历史说明并标注日期。
|
||||||
|
2. 文档声明移动端仍延期,不能把媒体查询或 `dedicated` 注册值当作已完成。
|
||||||
|
3. 端口、健康检查、数据目录、备份和回退命令在干净环境可执行。
|
||||||
|
4. `docs/HANDOFF.md` 与 Issue 索引同步更新,所有相对链接有效。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/verify_baseline.py
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ISSUE-010:数据源与 LLM 在线就绪核验
|
||||||
|
|
||||||
|
- 状态:待运行核验
|
||||||
|
- 优先级:P0
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在不把任何密钥写入仓库的前提下,恢复本地/局域网服务并分别证明进程、数据库、Tushare、iFinD、LLM 主/辅助模型和后台任务的健康状态。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. `0.0.0.0:8797`(或部署指定端口)可访问 `/api/health`,健康响应区分进程、数据库、数据源、任务和模型。
|
||||||
|
2. 登录后能读取最近真实交易日;无快照时显示等待同步,不显示演示数字。
|
||||||
|
3. 管理员诊断页可看到脱敏的来源、错误类型、关联 ID 和最后成功时间;普通用户看不到 Token、URL、模型名或堆栈。
|
||||||
|
4. Tushare/iFinD/LLM 单独测试成功或给出明确缺失/授权/网络原因;失败不清空已有成功数据。
|
||||||
|
5. 重启后账号、行情、问师/问天历史、选股和复盘记录不丢,盘后任务不重复产出。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File tools/start_local.ps1 -Port 8797
|
||||||
|
Invoke-RestMethod http://127.0.0.1:8797/api/health
|
||||||
|
python tools/verify_baseline.py --e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
凭据只从管理员系统设置或受保护环境注入,不能写入 Issue、截图或日志。
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# ISSUE-011:公网部署加固
|
||||||
|
|
||||||
|
- 状态:未来范围,局域网版不阻塞
|
||||||
|
- 优先级:P3
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在真正转向外网和商业授权前,将当前单实例局域网部署升级为可审计的公网部署方案。
|
||||||
|
|
||||||
|
## 必须完成
|
||||||
|
|
||||||
|
- 反向代理、TLS、可信 Host、Secure Cookie、CSRF、限流和审计。
|
||||||
|
- PostgreSQL 或等价正式数据库迁移,连接池和并发写入策略。
|
||||||
|
- 后台任务队列、分布式锁、缓存、健康检查、集中日志和告警。
|
||||||
|
- 多实例数据一致性、密钥托管、备份恢复和升级回退演练。
|
||||||
|
- 会员/激活码/授权模型的服务端鉴权和额度审计。
|
||||||
|
|
||||||
|
## 范围外
|
||||||
|
|
||||||
|
在没有部署决策和容量指标前,不为局域网版本预先拆微服务或引入云依赖。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
公网威胁模型、ADR、压测、故障注入、备份恢复、跨实例重复任务和安全扫描全部有记录;局域网数据边界和产品行为不变。
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 未完成事项 Issue 索引
|
||||||
|
|
||||||
|
这些 Issue 是仓库内的可审计任务说明,供人工维护或后续智能体执行。它们不是通过 Gitea API 创建的远端工单;需要远端协作时,应在确认范围后逐项复制到 Gitea,并保留本地文件作为产品交接记录。
|
||||||
|
|
||||||
|
| Issue | 标题 | 状态 | 优先级 |
|
||||||
|
|---|---|---|---:|
|
||||||
|
| [001](ISSUE-001-finalize-mentor-redesign.md) | 问师界面与动态追问收口 | 待人工验收/提交 | P0 |
|
||||||
|
| [002](ISSUE-002-checkpoint-current-pc-visual-work.md) | 当前 PC 全站视觉改动验收与回档 | 待审查/提交 | P0 |
|
||||||
|
| [003](ISSUE-003-mobile-redesign.md) | 移动端独立重设 | 明确延期 | P2 |
|
||||||
|
| [004](ISSUE-004-full-ic-multifactor.md) | 完整 IC 动态多因子 | 未实现 | P1 |
|
||||||
|
| [005](ISSUE-005-policy-macro-overnight-data.md) | 政策、宏观与隔夜消息数据 | 数据源未定 | P1 |
|
||||||
|
| [006](ISSUE-006-analyst-consensus-data.md) | 分析师一致预期数据 | 数据阻塞 | P2 |
|
||||||
|
| [007](ISSUE-007-level2-auction.md) | Level-2 与动态竞价深度 | 授权阻塞 | P2 |
|
||||||
|
| [008](ISSUE-008-hot-money-profile-history.md) | 游资档案历史画像 | 低优先级 | P3 |
|
||||||
|
| [009](ISSUE-009-documentation-status-drift.md) | 活跃文档和状态漂移 | 待整理 | P1 |
|
||||||
|
| [010](ISSUE-010-live-provider-llm-readiness.md) | 数据源与 LLM 在线就绪核验 | 待运行核验 | P0 |
|
||||||
|
| [011](ISSUE-011-public-deployment-hardening.md) | 公网部署加固 | 未来范围 | P3 |
|
||||||
|
|
||||||
|
关闭任一 Issue 前,必须更新 `docs/HANDOFF.md` 的状态、验证日期和回档提交;不能只改 Issue 标题。
|
||||||
@@ -1065,6 +1065,9 @@ iFinD动态竞价不可用时,不能把Tushare最终竞价伪装成动态监
|
|||||||
- 回答必须是真流式:每个文本片段只追加一次;流结束后不能再次追加完整答案,避免双份结果。
|
- 回答必须是真流式:每个文本片段只追加一次;流结束后不能再次追加完整答案,避免双份结果。
|
||||||
- 主模型在尚未输出任何内容前失败时可切换辅助模型。
|
- 主模型在尚未输出任何内容前失败时可切换辅助模型。
|
||||||
- 一旦已经向用户输出文本,中途失败只能提示连接中断,不能切换模型后重复整个回答。
|
- 一旦已经向用户输出文本,中途失败只能提示连接中断,不能切换模型后重复整个回答。
|
||||||
|
- 每次成功回答可在同一次模型输出末尾生成2至3条动态追问;追问必须结合本轮问题、回答和当前思维模型,不得额外发起一次LLM业务调用或重复扣减额度。
|
||||||
|
- 动态追问是当前回答的临时操作建议,不写入对话正文。点击追问只预填输入框,由用户确认或编辑后发送;切换模型、清空对话或开始下一次提问时,旧追问立即失效。
|
||||||
|
- 回答失败、被用户停止或追问结构无效时不显示动态追问;追问不得包含无条件买卖指令、收益承诺或正文未支持的新事实。
|
||||||
- 回答必须声明这是基于公开资料蒸馏的思维模型,不是真人本人,不构成投资建议。
|
- 回答必须声明这是基于公开资料蒸馏的思维模型,不是真人本人,不构成投资建议。
|
||||||
|
|
||||||
### 14.4 按模型类型提供数据
|
### 14.4 按模型类型提供数据
|
||||||
@@ -1914,6 +1917,7 @@ iFinD动态竞价不可用时,不能把Tushare最终竞价伪装成动态监
|
|||||||
| L03 | 主模型输出一半失败 | 不切辅助重放,提示连接中断并保留已输出 |
|
| L03 | 主模型输出一半失败 | 不切辅助重放,提示连接中断并保留已输出 |
|
||||||
| L04 | 选择宏观模型询问市场 | 上下文含宽基指数和ETF,不强塞短线席位数据 |
|
| L04 | 选择宏观模型询问市场 | 上下文含宽基指数和ETF,不强塞短线席位数据 |
|
||||||
| L05 | 普通用户打开问师/问天/助手 | 同结构锁定态,不能发起LLM调用 |
|
| L05 | 普通用户打开问师/问天/助手 | 同结构锁定态,不能发起LLM调用 |
|
||||||
|
| L06 | 问师成功完成一轮回答 | 回答下方出现2至3条与本轮相关的动态追问;点击后只预填输入框,不自动发送,也不产生额外LLM额度调用 |
|
||||||
| W01 | 观势未输入股票 | 只提示输入代码或名称,不显示暂不成卦 |
|
| W01 | 观势未输入股票 | 只提示输入代码或名称,不显示暂不成卦 |
|
||||||
| W02 | 中国平安所属保险行业仅5只成分且5只有行情 | 小样本但覆盖完整时行业安全门可通过 |
|
| W02 | 中国平安所属保险行业仅5只成分且5只有行情 | 小样本但覆盖完整时行业安全门可通过 |
|
||||||
| W03 | 行业缺涨跌但用户补录客观值 | 按原公式重算;用户不能直接选阴阳 |
|
| W03 | 行业缺涨跌但用户补录客观值 | 按原公式重算;用户不能直接选阴阳 |
|
||||||
|
|||||||
+35
-24
@@ -17,29 +17,29 @@
|
|||||||
document.documentElement.style.colorScheme = theme;
|
document.documentElement.style.colorScheme = theme;
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/shared/tokens.css?v=20260729-1">
|
<link rel="stylesheet" href="/shared/tokens.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/shared/base.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/base.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/shell.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/shell.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/shared/auth.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/auth.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/components/controls.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/components/controls.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/components/navigation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/components/cards.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/components/cards.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/components/tables.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/components/tables.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/components/dialogs.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/shared/components/feedback.css?v=20260802-5">
|
<link rel="stylesheet" href="/shared/components/feedback.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/market/foundation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/sentiment/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/pools/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/ladder/foundation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/rotation/foundation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/auction/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/themes/foundation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/popularity/foundation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/dragon-tiger/foundation.css?v=20260806-1">
|
||||||
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260804-1">
|
<link rel="stylesheet" href="/pages/screener/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/mentor/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260804-3">
|
<link rel="stylesheet" href="/pages/heaven/foundation.css?v=20260806-2">
|
||||||
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260802-5">
|
<link rel="stylesheet" href="/pages/review/foundation.css?v=20260806-1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="authGate" class="auth-gate" aria-label="账号登录">
|
<section id="authGate" class="auth-gate" aria-label="账号登录">
|
||||||
@@ -64,6 +64,10 @@
|
|||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
|
<div class="mobile-page-context" aria-live="polite">
|
||||||
|
<span id="mobilePageGroup">行情</span>
|
||||||
|
<strong id="mobilePageTitle">情绪周期</strong>
|
||||||
|
</div>
|
||||||
<div class="market-tape" aria-label="市场概况">
|
<div class="market-tape" aria-label="市场概况">
|
||||||
<span class="market-item up">上涨 <strong id="tapeUp">--</strong></span>
|
<span class="market-item up">上涨 <strong id="tapeUp">--</strong></span>
|
||||||
<span class="market-item down">下跌 <strong id="tapeDown">--</strong></span>
|
<span class="market-item down">下跌 <strong id="tapeDown">--</strong></span>
|
||||||
@@ -83,6 +87,12 @@
|
|||||||
<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>
|
||||||
<div id="headerCommandGroup" class="header-command-group">
|
<div id="headerCommandGroup" class="header-command-group">
|
||||||
|
<div class="mobile-command-shortcuts" aria-label="快捷工具">
|
||||||
|
<button class="button mobile-command-shortcut" type="button" data-mobile-command-target="globalSearchButton"><i data-lucide="search"></i><span>搜索</span></button>
|
||||||
|
<button class="button mobile-command-shortcut" type="button" data-mobile-command-target="themeToggle"><i data-lucide="moon"></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>
|
||||||
|
</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"><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" 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" hidden><i data-lucide="settings-2"></i><span>系统管理</span></button>
|
||||||
@@ -105,6 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<button id="mobileCommandBackdrop" class="mobile-command-backdrop" type="button" aria-label="关闭命令菜单" hidden></button>
|
||||||
|
|
||||||
<nav class="module-nav" aria-label="复盘模块">
|
<nav class="module-nav" aria-label="复盘模块">
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
@@ -128,7 +139,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="nav-group tool-nav-group">
|
<div class="nav-group tool-nav-group">
|
||||||
<div class="nav-group-label">智能工具</div>
|
<div class="nav-group-label">智能工具</div>
|
||||||
<button class="module-tab mobile-primary-tab" type="button" data-view="screenerView" title="智能选股"><i data-lucide="search-check"></i><span>智能选股</span></button>
|
<button class="module-tab mobile-primary-tab" type="button" data-view="screenerView" title="智能选股"><i data-lucide="search-check"></i><span class="nav-label-desktop">智能选股</span><span class="nav-label-mobile">选股</span></button>
|
||||||
<button class="module-tab mobile-primary-tab" type="button" data-view="mentorView" title="问师"><i data-lucide="messages-square"></i><span>问师</span></button>
|
<button class="module-tab mobile-primary-tab" type="button" data-view="mentorView" title="问师"><i data-lucide="messages-square"></i><span>问师</span></button>
|
||||||
<button class="module-tab mobile-primary-tab" type="button" data-view="heavenView" title="问天"><i data-lucide="sparkles"></i><span>问天</span></button>
|
<button class="module-tab mobile-primary-tab" type="button" data-view="heavenView" title="问天"><i data-lucide="sparkles"></i><span>问天</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
["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=20260803-1", ["ladderView"]],
|
||||||
["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]],
|
["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]],
|
||||||
["mentor", "/pages/mentor/page.html?v=20260803-1", ["mentorView"]],
|
["mentor", "/pages/mentor/page.html?v=20260806-2", ["mentorView"]],
|
||||||
["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]],
|
["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]],
|
||||||
["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]],
|
["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]],
|
||||||
["themes", "/pages/themes/page.html?v=20260803-1", ["themeLibraryView"]],
|
["themes", "/pages/themes/page.html?v=20260803-1", ["themeLibraryView"]],
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"/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",
|
||||||
"/pages/market/stock-detail.js?v=20260803-1",
|
"/pages/market/stock-detail.js?v=20260803-1",
|
||||||
"/pages/market/preview.js?v=20260803-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=20260729-1",
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
"/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=20260729-1",
|
||||||
"/pages/popularity/page.js?v=20260729-1",
|
"/pages/popularity/page.js?v=20260729-1",
|
||||||
"/pages/dragon-tiger/page.js?v=20260729-1",
|
"/pages/dragon-tiger/page.js?v=20260806-1",
|
||||||
"/pages/screener/page.js?v=20260804-1",
|
"/pages/screener/page.js?v=20260806-1",
|
||||||
"/pages/mentor/page.js?v=20260729-1",
|
"/pages/mentor/page.js?v=20260806-2",
|
||||||
"/pages/heaven/page.js?v=20260729-1",
|
"/pages/heaven/page.js?v=20260729-1",
|
||||||
"/pages/review/page.js?v=20260729-1",
|
"/pages/review/page.js?v=20260729-1",
|
||||||
"/shared/state.js?v=20260729-1",
|
"/shared/state.js?v=20260729-1",
|
||||||
"/shared/api.js?v=20260729-1",
|
"/shared/api.js?v=20260729-1",
|
||||||
"/shared/shell.js?v=20260729-1",
|
"/shared/shell.js?v=20260806-1",
|
||||||
"/shared/export.js?v=20260731-1",
|
"/shared/export.js?v=20260731-1",
|
||||||
"/pages/heaven/loading-v2.js?v=20260728-2",
|
"/pages/heaven/loading-v2.js?v=20260728-2",
|
||||||
"/shared/context.js?v=20260804-1",
|
"/shared/context.js?v=20260804-1",
|
||||||
|
|||||||
+101
-9
@@ -5,8 +5,8 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#auctionView {
|
body.mobile-shell #auctionView {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
max-width: 155px;
|
max-width: 155px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.auction-phase-notice time {
|
.auction-phase-notice time {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
@@ -506,7 +506,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
|
|
||||||
.auction-phase-notice {
|
.auction-phase-notice {
|
||||||
grid-template-columns: 10px minmax(0px, 1fr);
|
grid-template-columns: 10px minmax(0px, 1fr);
|
||||||
@@ -1962,7 +1962,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-auction-view {
|
.redesigned-auction-view {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -2080,7 +2080,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body[data-active-view="auctionView"] .app-main {
|
body[data-active-view="auctionView"] .app-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -2104,7 +2104,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-height: 900px) {
|
@media (min-width: 768px) and (max-height: 900px) {
|
||||||
.redesigned-auction-view {
|
.redesigned-auction-view {
|
||||||
padding: 10px 0 12px;
|
padding: 10px 0 12px;
|
||||||
}
|
}
|
||||||
@@ -2196,7 +2196,7 @@
|
|||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
#auctionView .auction-page-head-v2 {
|
#auctionView .auction-page-head-v2 {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
@@ -2242,7 +2242,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
#auctionView .auction-workspace-v2 {
|
#auctionView .auction-workspace-v2 {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -2315,3 +2315,95 @@
|
|||||||
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile auction keeps one page scrollbar; dense rows scroll only sideways. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#auctionView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-workspace-v2 {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-12);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-primary-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-tabs-v2 {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
padding-inline: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-tabs-v2 button {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
justify-content: center;
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
font-size: var(--font-size-label);
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-tabs-v2 .auction-summary-v2 {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
margin-left: 0;
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-tools-v2,
|
||||||
|
#auctionView .auction-expectation-v2,
|
||||||
|
#auctionView .auction-tool-actions {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-filter-segments {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-filter-segments button {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-search-v2,
|
||||||
|
#auctionView .auction-search-v2 input {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-table-frame-v2 {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: var(--mobile-table-min-height);
|
||||||
|
max-height: none;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auctionView .auction-side-v2 {
|
||||||
|
max-height: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-12);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+36
-9
@@ -661,7 +661,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
:where(#dragonView) .dragon-trader-list {
|
:where(#dragonView) .dragon-trader-list {
|
||||||
padding: 0px 15px;
|
padding: 0px 15px;
|
||||||
}
|
}
|
||||||
@@ -842,7 +842,7 @@
|
|||||||
padding: 9px 14px;
|
padding: 9px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.dragon-filterbar {
|
.dragon-filterbar {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
@@ -1558,7 +1558,7 @@
|
|||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body[data-active-view="dragonView"] .app-main {
|
body[data-active-view="dragonView"] .app-main {
|
||||||
height: var(--workspace-height);
|
height: var(--workspace-height);
|
||||||
|
|
||||||
@@ -1576,7 +1576,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-height: 900px) {
|
@media (min-width: 768px) and (max-height: 900px) {
|
||||||
.redesigned-dragon-view {
|
.redesigned-dragon-view {
|
||||||
padding-top: 9px;
|
padding-top: 9px;
|
||||||
|
|
||||||
@@ -1688,7 +1688,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-dragon-view {
|
.redesigned-dragon-view {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
@@ -1795,7 +1795,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dragonView .dragon-trader-detail .trader-operations {
|
body.mobile-shell #dragonView .dragon-trader-detail .trader-operations {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2306,13 +2306,13 @@
|
|||||||
font-weight: var(--dragon-profile-weight-semibold);
|
font-weight: var(--dragon-profile-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body[data-active-view="dragonView"] .hot-money-profiles-v2 {
|
body[data-active-view="dragonView"] .hot-money-profiles-v2 {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.hot-money-profiles-v2 {
|
.hot-money-profiles-v2 {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
@@ -2354,7 +2354,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body[data-active-view="dragonView"] .dragon-daily-content-v2 {
|
body[data-active-view="dragonView"] .dragon-daily-content-v2 {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -2473,3 +2473,30 @@ table.tbl {
|
|||||||
|
|
||||||
background: var(--surface-muted) !important;
|
background: var(--surface-muted) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile dragon-tiger pages use document scrolling, including long operation lists. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#dragonView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dragonView :is(.dragon-daily-content-v2, .dragon-trader-detail-v2, .dragon-unclassified-v2, .hot-money-profiles-v2) {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dragonView .dragon-trader-detail .trader-operations {
|
||||||
|
max-height: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dragonView .hot-money-profile-list-v2 {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dragonView .dragon-operation-table {
|
||||||
|
min-width: var(--table-wide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ function layoutDragonCards(container = document.querySelector("#dragonTraderList
|
|||||||
const cards = [...container.querySelectorAll(".dragon-trader-card")];
|
const cards = [...container.querySelectorAll(".dragon-trader-card")];
|
||||||
const hitZones = [...container.querySelectorAll(".dragon-card-hit-zone")];
|
const hitZones = [...container.querySelectorAll(".dragon-card-hit-zone")];
|
||||||
if (!cards.length) return;
|
if (!cards.length) return;
|
||||||
const compact = window.innerWidth <= 720;
|
const compact = window.innerWidth <= 767;
|
||||||
const cardWidth = compact ? 148 : 176;
|
const cardWidth = compact ? 148 : 176;
|
||||||
const available = Math.max(cardWidth, container.clientWidth - (compact ? 30 : 72));
|
const available = Math.max(cardWidth, container.clientWidth - (compact ? 30 : 72));
|
||||||
const spread = Math.min(available - cardWidth, compact ? 310 : 1050);
|
const spread = Math.min(available - cardWidth, compact ? 310 : 1050);
|
||||||
|
|||||||
+20
-7
@@ -81,7 +81,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body[data-active-view="heavenView"] .workspace-view {
|
body[data-active-view="heavenView"] .workspace-view {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
|
||||||
@@ -1440,7 +1440,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body[data-active-view="heavenView"] .overview-strip {
|
body[data-active-view="heavenView"] .overview-strip {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -5738,7 +5738,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.heaven-calibration-heading {
|
.heaven-calibration-heading {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
@@ -6139,7 +6139,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.heart-toolbar-controls {
|
.heart-toolbar-controls {
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
|
||||||
@@ -6290,7 +6290,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
min-height: 340px;
|
min-height: 340px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#heavenView .heaven-toolbar {
|
#heavenView .heaven-toolbar {
|
||||||
min-height: 62px;
|
min-height: 62px;
|
||||||
|
|
||||||
@@ -6653,7 +6653,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
--wt-history-bg: #fbfaf6;
|
--wt-history-bg: #fbfaf6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
:root[data-theme="light"] body[data-active-view="heavenView"] {
|
:root[data-theme="light"] body[data-active-view="heavenView"] {
|
||||||
--wt-bg: #f4f5f7;
|
--wt-bg: #f4f5f7;
|
||||||
}
|
}
|
||||||
@@ -9236,7 +9236,7 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body[data-active-view="heavenView"] {
|
body[data-active-view="heavenView"] {
|
||||||
--wt-bg: #0b1120;
|
--wt-bg: #0b1120;
|
||||||
}
|
}
|
||||||
@@ -11719,3 +11719,16 @@ body[data-active-view="heavenView"] .workspace-view {
|
|||||||
width: min(100% - 40px, 1280px);
|
width: min(100% - 40px, 1280px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
:root[data-theme="dark"] body.mobile-shell[data-active-view="heavenView"] .app-main,
|
||||||
|
:root[data-theme="dark"] body.mobile-shell[data-active-view="heavenView"] #heavenView.heaven-shell {
|
||||||
|
background-color: var(--wt-bg);
|
||||||
|
background-image: var(--wt-stage-bg);
|
||||||
|
color: var(--wt-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] body.mobile-shell[data-active-view="heavenView"] #heavenView .heaven-panel {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+99
-4
@@ -66,7 +66,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
|
|
||||||
.ladder-more {
|
.ladder-more {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
box-shadow: rgba(22, 34, 46, 0.04) 0px 1px 3px;
|
box-shadow: rgba(22, 34, 46, 0.04) 0px 1px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.ladder-board {
|
.ladder-board {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
@@ -1015,7 +1015,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-ladder-view {
|
.redesigned-ladder-view {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -1097,7 +1097,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
:root #ladderView.active-view {
|
:root #ladderView.active-view {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
@@ -1170,3 +1170,98 @@
|
|||||||
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile ladder becomes a readable vertical hierarchy instead of a narrow rail. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#ladderView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .ladder-page-head {
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .ladder-head-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .ladder-sort-segment {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-workspace,
|
||||||
|
#ladderView .lad-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-board {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-tier {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-label {
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding: var(--space-8) var(--space-12);
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-level {
|
||||||
|
display: inline-flex;
|
||||||
|
margin: 0;
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-rate {
|
||||||
|
display: inline-flex;
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-stocks {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-stock {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-gap-note,
|
||||||
|
#ladderView .market-ladder-more {
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
margin: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ladderView .market-ladder-insights {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
/* Canonical CSS owner: market. Historical layers consolidated 2026-08-02. */
|
/* Canonical CSS owner: market. Historical layers consolidated 2026-08-02. */
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.stock-dialog {
|
.stock-dialog {
|
||||||
width: calc(-16px + 100vw);
|
width: calc(-16px + 100vw);
|
||||||
|
|
||||||
@@ -628,7 +628,7 @@
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body.stock-preview-open {
|
body.stock-preview-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -770,7 +770,7 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.entity-detail-dialog .detail-grid {
|
.entity-detail-dialog .detail-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function findStockFallback(code) {
|
|||||||
|
|
||||||
function supportsStockPreviewHover() {
|
function supportsStockPreviewHover() {
|
||||||
return window.matchMedia("(hover: hover) and (pointer: fine)").matches
|
return window.matchMedia("(hover: hover) and (pointer: fine)").matches
|
||||||
&& window.innerWidth > 720;
|
&& window.innerWidth > 767;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStockPreviewPointerOver(event) {
|
function handleStockPreviewPointerOver(event) {
|
||||||
@@ -138,7 +138,7 @@ function handleStockPreviewFocusOut(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMobileStockPreviewClick(event) {
|
function handleMobileStockPreviewClick(event) {
|
||||||
if (window.innerWidth > 720) return;
|
if (window.innerWidth > 767) return;
|
||||||
const trigger = event.target.closest?.(".stock-preview-trigger");
|
const trigger = event.target.closest?.(".stock-preview-trigger");
|
||||||
if (!trigger) return;
|
if (!trigger) return;
|
||||||
const code = stockCodeFromTrigger(trigger);
|
const code = stockCodeFromTrigger(trigger);
|
||||||
@@ -160,7 +160,7 @@ function handleStockPreviewKeydown(event) {
|
|||||||
const code = stockCodeFromTrigger(trigger);
|
const code = stockCodeFromTrigger(trigger);
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (window.innerWidth <= 720) showStockPreview(code, trigger);
|
if (window.innerWidth <= 767) showStockPreview(code, trigger);
|
||||||
else openStock(code, findStockFallback(code));
|
else openStock(code, findStockFallback(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ async function showStockPreview(code, trigger) {
|
|||||||
state.stockPreviewChart = "daily";
|
state.stockPreviewChart = "daily";
|
||||||
renderStockPreviewLoading();
|
renderStockPreviewLoading();
|
||||||
elements.stockPreview.hidden = false;
|
elements.stockPreview.hidden = false;
|
||||||
const mobile = window.innerWidth <= 720;
|
const mobile = window.innerWidth <= 767;
|
||||||
elements.stockPreviewBackdrop.hidden = !mobile;
|
elements.stockPreviewBackdrop.hidden = !mobile;
|
||||||
document.body.classList.toggle("stock-preview-open", mobile);
|
document.body.classList.toggle("stock-preview-open", mobile);
|
||||||
requestAnimationFrame(repositionStockPreview);
|
requestAnimationFrame(repositionStockPreview);
|
||||||
@@ -239,7 +239,7 @@ async function showEntityPreview(item, trigger) {
|
|||||||
state.stockPreviewChart = "daily";
|
state.stockPreviewChart = "daily";
|
||||||
renderStockPreviewLoading();
|
renderStockPreviewLoading();
|
||||||
elements.stockPreview.hidden = false;
|
elements.stockPreview.hidden = false;
|
||||||
const mobile = window.innerWidth <= 720;
|
const mobile = window.innerWidth <= 767;
|
||||||
elements.stockPreviewBackdrop.hidden = !mobile;
|
elements.stockPreviewBackdrop.hidden = !mobile;
|
||||||
document.body.classList.toggle("stock-preview-open", mobile);
|
document.body.classList.toggle("stock-preview-open", mobile);
|
||||||
requestAnimationFrame(repositionStockPreview);
|
requestAnimationFrame(repositionStockPreview);
|
||||||
@@ -433,7 +433,7 @@ function openStockDetailFromPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function repositionStockPreview() {
|
function repositionStockPreview() {
|
||||||
if (elements.stockPreview.hidden || window.innerWidth <= 720 || !stockPreviewAnchor?.isConnected) return;
|
if (elements.stockPreview.hidden || window.innerWidth <= 767 || !stockPreviewAnchor?.isConnected) return;
|
||||||
const anchor = stockPreviewAnchor.getBoundingClientRect();
|
const anchor = stockPreviewAnchor.getBoundingClientRect();
|
||||||
const preview = elements.stockPreview.getBoundingClientRect();
|
const preview = elements.stockPreview.getBoundingClientRect();
|
||||||
const gap = 12;
|
const gap = 12;
|
||||||
|
|||||||
+677
-2490
File diff suppressed because it is too large
Load Diff
@@ -1,72 +1,89 @@
|
|||||||
<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">
|
<header class="section-toolbar lad-head mentor-page-header">
|
||||||
<div class="section-title-group mentor-page-title">
|
<div class="section-title-group mentor-page-title">
|
||||||
<h2>问师</h2>
|
<h2>问师</h2>
|
||||||
<span class="section-subtitle">向思维模型请教 · <span id="mentorDataDate">--</span></span>
|
<span class="section-subtitle">与不同交易思维模型持续对话 · <span id="mentorDataDate">--</span></span>
|
||||||
</div>
|
|
||||||
<div class="toolbar-controls mentor-page-controls">
|
|
||||||
<div class="mentor-evidence-filters seg" role="group" aria-label="按素材等级筛选">
|
|
||||||
<button class="active" type="button" data-mentor-grade="all">全部</button>
|
|
||||||
<button type="button" data-mentor-grade="A">A级</button>
|
|
||||||
<button type="button" data-mentor-grade="B">B级</button>
|
|
||||||
<button type="button" data-mentor-grade="C">C级</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="mentorNotice" class="inline-notice" hidden></div>
|
<div id="mentorNotice" class="inline-notice" hidden></div>
|
||||||
<div class="mentor-layout mentor-grid">
|
<div class="mentor-layout">
|
||||||
<aside class="mentor-sidebar mentor-library-card card">
|
<aside class="mentor-sidebar" aria-label="思维模型目录">
|
||||||
<button id="mentorDirectoryToggle" class="mentor-directory-toggle" type="button" aria-expanded="false" aria-controls="mentorDirectoryContent">
|
<div class="mentor-directory-heading">
|
||||||
<span><i data-lucide="users-round"></i><span><small>当前思维模型</small><strong id="mobileActiveMentorName">--</strong></span></span>
|
<div class="mentor-directory-title"><strong>联系人</strong><span id="mentorCount">0 位</span></div>
|
||||||
<i data-lucide="chevron-up"></i>
|
<button id="mentorSortToggle" class="mentor-sort-toggle" type="button" aria-pressed="false" title="整理联系人顺序"><i data-lucide="list-ordered"></i><span>整理</span></button>
|
||||||
</button>
|
</div>
|
||||||
<div id="mentorDirectoryBackdrop" class="mentor-directory-backdrop" hidden></div>
|
<div class="mentor-directory-tools">
|
||||||
<div id="mentorDirectoryContent" class="mentor-directory-content">
|
|
||||||
<div class="workspace-heading card-h mentor-directory-heading">
|
|
||||||
<div class="mentor-directory-title"><h3>模型库</h3><span>语料完整度决定回答质量</span></div>
|
|
||||||
<div class="mentor-directory-actions">
|
|
||||||
<strong id="mentorCount" class="mentor-count">0 位</strong>
|
|
||||||
<button id="mentorSortToggle" class="mentor-sort-toggle" type="button" aria-pressed="false" title="整理顺序"><i data-lucide="list-ordered"></i><span>整理</span></button>
|
|
||||||
<button id="closeMentorDirectory" class="icon-button mentor-directory-close" type="button" aria-label="关闭思维模型目录" title="关闭"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
||||||
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动卡片,或使用箭头调整顺序</p>
|
<div class="mentor-evidence-filters" role="group" aria-label="按素材等级筛选">
|
||||||
<div id="mentorList" class="mentor-list"></div>
|
<button class="active" type="button" data-mentor-grade="all">全部</button>
|
||||||
<div id="mentorListEmpty" class="mentor-list-empty" hidden>没有符合条件的思维模型</div>
|
<button type="button" data-mentor-grade="A">A级</button>
|
||||||
<p class="mentor-evidence-legend">素材等级反映蒸馏依据,不代表人物能力或收益水平。</p>
|
<button type="button" data-mentor-grade="B">B级</button>
|
||||||
</div>
|
<button type="button" data-mentor-grade="C">C级</button>
|
||||||
</aside>
|
|
||||||
<section class="mentor-chat-panel card">
|
|
||||||
<header class="mentor-chat-header card-h">
|
|
||||||
<div class="mentor-active-profile">
|
|
||||||
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3><span id="activeMentorBadges" class="mentor-active-badges"></span></div>
|
|
||||||
<p id="activeMentorEvidence">--</p>
|
|
||||||
<div id="activeMentorFocus" class="mentor-active-focus"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="clearMentorChatButton" class="button ghost mentor-clear-button" type="button" disabled><i data-lucide="trash-2"></i><span>清空对话</span></button>
|
<p id="mentorSortHint" class="mentor-sort-hint" hidden>拖动联系人,或使用箭头调整顺序</p>
|
||||||
</header>
|
|
||||||
<div class="chat-box">
|
|
||||||
<div id="mentorMessages" class="mentor-messages chat-log" aria-live="polite"></div>
|
|
||||||
<div id="mentorQuickPrompts" class="mentor-quick-prompts">
|
|
||||||
<span class="mentor-prompt-label">试着这样问</span>
|
|
||||||
<button type="button" data-mentor-prompt="怎么看今天的市场环境?">市场环境</button>
|
|
||||||
<button type="button" data-mentor-prompt="当前的市场主线和情绪周期是什么?">主线与周期</button>
|
|
||||||
<button type="button" data-mentor-prompt="如果今天是空仓状态,你会怎么制定操作预案?">空仓预案</button>
|
|
||||||
<button type="button" data-mentor-prompt="现在最需要防范的风险是什么?">风险检查</button>
|
|
||||||
</div>
|
</div>
|
||||||
<form id="mentorChatForm" class="mentor-chat-form chat-input">
|
<div id="mentorList" class="mentor-list"></div>
|
||||||
|
<div id="mentorListEmpty" class="mentor-list-empty" hidden>没有符合条件的思维模型</div>
|
||||||
|
<p class="mentor-evidence-legend">等级仅表示公开素材完整度</p>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="mentor-chat-panel" aria-label="问师对话">
|
||||||
|
<header class="mentor-chat-header">
|
||||||
|
<div class="mentor-chat-identity">
|
||||||
|
<span id="activeMentorAvatar" class="mentor-avatar" aria-hidden="true">师</span>
|
||||||
|
<div>
|
||||||
|
<div class="mentor-active-title"><h3 id="activeMentorName">--</h3><span id="activeMentorBadges" class="mentor-active-badges"></span></div>
|
||||||
|
<p id="activeMentorStatus">思维模型已就绪</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="clearMentorChatButton" class="icon-button mentor-clear-button" type="button" disabled aria-label="清空当前对话" title="清空对话"><i data-lucide="trash-2"></i></button>
|
||||||
|
</header>
|
||||||
|
<div id="mentorMessages" class="mentor-messages" aria-live="polite"></div>
|
||||||
|
<div id="mentorQuickPrompts" class="mentor-quick-prompts">
|
||||||
|
<span class="mentor-prompt-label">开始一个话题</span>
|
||||||
|
<button type="button" data-mentor-prompt="怎么看今天的市场环境?"><i data-lucide="activity"></i>市场环境</button>
|
||||||
|
<button type="button" data-mentor-prompt="当前的市场主线和情绪周期是什么?"><i data-lucide="route"></i>主线与周期</button>
|
||||||
|
<button type="button" data-mentor-prompt="如果今天是空仓状态,你会怎么制定操作预案?"><i data-lucide="notebook-tabs"></i>空仓预案</button>
|
||||||
|
<button type="button" data-mentor-prompt="现在最需要防范的风险是什么?"><i data-lucide="shield-alert"></i>风险检查</button>
|
||||||
|
</div>
|
||||||
|
<form id="mentorChatForm" class="mentor-chat-form">
|
||||||
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
|
<label class="visually-hidden" for="mentorQuestion">向当前思维模型提问</label>
|
||||||
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
|
<textarea id="mentorQuestion" maxlength="2000" placeholder="输入市场、板块、个股代码或交易问题"></textarea>
|
||||||
<button id="sendMentorQuestion" class="button primary" type="submit"><i data-lucide="send-horizontal"></i><span>发送</span></button>
|
<div class="mentor-composer-actions">
|
||||||
|
<span>Enter 发送 · Shift + Enter 换行</span>
|
||||||
|
<button id="stopMentorQuestion" class="button ghost mentor-stop-button" type="button" hidden><i data-lucide="square"></i><span>停止</span></button>
|
||||||
|
<button id="sendMentorQuestion" class="button primary mentor-send-button" type="submit"><i data-lucide="send-horizontal"></i><span>发送</span></button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p class="mentor-disclaimer">基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。</p>
|
<p class="mentor-disclaimer">基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。</p>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<aside class="mentor-profile-panel" aria-label="当前思维模型资料">
|
||||||
|
<div class="mentor-profile-hero">
|
||||||
|
<span id="mentorProfileAvatar" class="mentor-profile-avatar" aria-hidden="true">师</span>
|
||||||
|
<h3 id="mentorProfileName">--</h3>
|
||||||
|
<p id="mentorProfileTagline">--</p>
|
||||||
|
<div id="mentorProfileBadges" class="mentor-profile-badges"></div>
|
||||||
|
</div>
|
||||||
|
<section class="mentor-profile-section">
|
||||||
|
<h4>关注维度</h4>
|
||||||
|
<div id="activeMentorFocus" class="mentor-active-focus"></div>
|
||||||
|
</section>
|
||||||
|
<section class="mentor-profile-section">
|
||||||
|
<h4>资料依据</h4>
|
||||||
|
<strong id="mentorProfileSource">--</strong>
|
||||||
|
<p id="activeMentorEvidence">--</p>
|
||||||
|
</section>
|
||||||
|
<section class="mentor-profile-section mentor-profile-boundary">
|
||||||
|
<h4>数据边界</h4>
|
||||||
|
<p><i data-lucide="calendar-days"></i><span id="mentorProfileDataDate">--</span></p>
|
||||||
|
<p><i data-lucide="database"></i><span>仅使用网页已提供的市场数据</span></p>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -37,8 +37,14 @@ function renderMentorWorkspace() {
|
|||||||
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("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`);
|
||||||
setText("activeMentorName", selected?.name || "--");
|
setText("activeMentorName", selected?.name || "--");
|
||||||
setText("mobileActiveMentorName", selected?.name || "选择思维模型");
|
setText("mentorProfileName", selected?.name || "--");
|
||||||
|
setText("mentorProfileTagline", selected?.tagline || selected?.description || "--");
|
||||||
|
setText("mentorProfileSource", selected?.evidence?.label || "公开资料整理");
|
||||||
|
setText("mentorProfileDataDate", `行情数据 ${displayCompactDate(setup.trade_date)}`);
|
||||||
|
setText("activeMentorAvatar", mentorAvatarText(selected));
|
||||||
|
setText("mentorProfileAvatar", mentorAvatarText(selected));
|
||||||
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
||||||
|
document.querySelector("#mentorProfileBadges").innerHTML = selected ? renderMentorBadges(selected, true) : "";
|
||||||
setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
|
setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--");
|
||||||
document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
|
document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4)
|
||||||
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
|
.map((item) => `<span>${escapeHtml(item)}</span>`).join("");
|
||||||
@@ -82,16 +88,14 @@ 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-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>
|
||||||
<span class="mentor-option-badges">${renderMentorBadges(mentor)}</span>
|
<span class="mentor-option-badges">${renderMentorBadges(mentor)}</span>
|
||||||
</span>
|
</span>
|
||||||
<em title="${escapeHtml(mentor.description || "")}">${escapeHtml(mentor.description || mentor.tagline || "思维模型")}</em>
|
<em title="${escapeHtml(mentor.description || "")}">${escapeHtml(mentor.description || mentor.tagline || "思维模型")}</em>
|
||||||
<span class="mentor-option-meta">
|
<span class="mentor-option-meta">${escapeHtml((mentor.focus || [])[0] || mentor.evidence?.label || "公开资料模型")}</span>
|
||||||
${mentor.evidence?.label ? `<span class="mentor-evidence-source" title="${escapeHtml(mentor.evidence?.note || "素材说明")}">${escapeHtml(mentor.evidence.label)}</span>` : ""}
|
|
||||||
${(mentor.focus || []).slice(0, 2).map((item) => `<span>#${escapeHtml(item)}</span>`).join("")}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span class="mentor-option-tools">
|
<span class="mentor-option-tools">
|
||||||
@@ -254,29 +258,16 @@ function renderMentorBadges(mentor, expanded = false) {
|
|||||||
return badges.join("");
|
return badges.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMentorDirectory(open) {
|
function mentorAvatarText(mentor) {
|
||||||
const mobileOpen = Boolean(open) && window.innerWidth <= 720;
|
return Array.from(String(mentor?.name || "师").trim())[0] || "师";
|
||||||
state.mentorDirectoryOpen = mobileOpen;
|
|
||||||
const sidebar = document.querySelector("#mentorView .mentor-sidebar");
|
|
||||||
const backdrop = document.querySelector("#mentorDirectoryBackdrop");
|
|
||||||
const toggle = document.querySelector("#mentorDirectoryToggle");
|
|
||||||
sidebar.classList.toggle("is-open", mobileOpen);
|
|
||||||
backdrop.hidden = !mobileOpen;
|
|
||||||
toggle.setAttribute("aria-expanded", String(mobileOpen));
|
|
||||||
document.body.classList.toggle("mentor-directory-open", mobileOpen);
|
|
||||||
if (mobileOpen) requestAnimationFrame(() => document.querySelector("#mentorSearchInput").focus());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectMentor(mentorId) {
|
async function selectMentor(mentorId) {
|
||||||
if (mentorId === state.selectedMentorId) {
|
if (mentorId === state.selectedMentorId) return;
|
||||||
toggleMentorDirectory(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.selectedMentorId = mentorId;
|
state.selectedMentorId = mentorId;
|
||||||
state.mentorMessages = [];
|
state.mentorMessages = [];
|
||||||
hideMentorNotice();
|
hideMentorNotice();
|
||||||
renderMentorWorkspace();
|
renderMentorWorkspace();
|
||||||
toggleMentorDirectory(false);
|
|
||||||
state.mentorMessages = await loadMentorMessages();
|
state.mentorMessages = await loadMentorMessages();
|
||||||
renderMentorMessages();
|
renderMentorMessages();
|
||||||
}
|
}
|
||||||
@@ -289,35 +280,53 @@ function renderMentorMessages() {
|
|||||||
<div class="mentor-empty-state">
|
<div class="mentor-empty-state">
|
||||||
<span class="mentor-empty-mark" aria-hidden="true"><i data-lucide="messages-square"></i></span>
|
<span class="mentor-empty-mark" aria-hidden="true"><i data-lucide="messages-square"></i></span>
|
||||||
<strong>向「${escapeHtml(selected?.name || "问师")}」请教</strong>
|
<strong>向「${escapeHtml(selected?.name || "问师")}」请教</strong>
|
||||||
<p>${escapeHtml(selected?.tagline || selected?.description || "选择一个问题开始对话")}</p>
|
<p>${escapeHtml(selected?.tagline || selected?.description || "从一个具体问题开始对话")}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
refreshIcons();
|
refreshIcons();
|
||||||
} 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" : ""}">
|
||||||
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
|
<span class="mentor-message-avatar" aria-hidden="true">${message.role === "user" ? "我" : escapeHtml(mentorAvatarText(selected))}</span>
|
||||||
<div class="mentor-message-content">${message.role === "assistant" ? formatMentorAnswer(message.content) : escapeHtml(message.content)}</div>
|
<div class="mentor-message-body">
|
||||||
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
|
<div class="mentor-message-label">${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}</div>
|
||||||
${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
|
<div class="mentor-message-content">${message.role === "assistant"
|
||||||
|
? (message.content ? formatMentorAnswer(message.content) : '<p class="mentor-loading-copy">正在读取复盘数据并推演...</p>')
|
||||||
|
: escapeHtml(message.content)}</div>
|
||||||
|
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
|
||||||
|
${renderMentorFollowUps(message)}
|
||||||
|
${message.meta && !message.streaming ? `<small>${escapeHtml(message.meta)}</small>` : ""}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`).join("");
|
`).join("");
|
||||||
if (state.mentorLoading && !state.mentorMessages.some((message) => message.streaming)) {
|
|
||||||
container.insertAdjacentHTML("beforeend", `
|
|
||||||
<article class="mentor-message assistant loading-message">
|
|
||||||
<div class="mentor-message-label">${escapeHtml(selected?.name || "问师")}</div>
|
|
||||||
<p>正在读取复盘数据并推演...</p>
|
|
||||||
</article>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
document.querySelector("#mentorQuickPrompts").hidden = state.mentorMessages.length > 0 || state.mentorLoading;
|
||||||
document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading;
|
document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading;
|
||||||
document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
|
document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
|
||||||
document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
|
document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId;
|
||||||
|
document.querySelector("#sendMentorQuestion").hidden = state.mentorLoading;
|
||||||
|
document.querySelector("#stopMentorQuestion").hidden = !state.mentorLoading;
|
||||||
document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
|
document.querySelector("#mentorSortToggle").disabled = state.mentorLoading;
|
||||||
|
setText("activeMentorStatus", state.mentorLoading ? "正在生成回答..." : "思维模型已就绪");
|
||||||
|
container.querySelectorAll("[data-mentor-follow-up]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorFollowUp));
|
||||||
|
});
|
||||||
|
refreshIcons();
|
||||||
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMentorFollowUps(message) {
|
||||||
|
if (message.role !== "assistant" || message.streaming || message.error || !Array.isArray(message.followUps)) return "";
|
||||||
|
const items = message.followUps.filter(Boolean).slice(0, 3);
|
||||||
|
if (items.length < 2) return "";
|
||||||
|
return `
|
||||||
|
<div class="mentor-follow-ups" aria-label="继续追问">
|
||||||
|
<span>继续追问</span>
|
||||||
|
${items.map((item) => `<button type="button" data-mentor-follow-up="${escapeHtml(item)}"><i data-lucide="corner-down-right"></i><span>${escapeHtml(item)}</span></button>`).join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendMentorQuestion(event) {
|
async function sendMentorQuestion(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (state.mentorLoading || !state.selectedMentorId) return;
|
if (state.mentorLoading || !state.selectedMentorId) return;
|
||||||
@@ -328,8 +337,9 @@ async function sendMentorQuestion(event) {
|
|||||||
role: item.role,
|
role: item.role,
|
||||||
content: item.content.slice(0, 3500),
|
content: item.content.slice(0, 3500),
|
||||||
}));
|
}));
|
||||||
|
state.mentorMessages.forEach((message) => { delete message.followUps; });
|
||||||
state.mentorMessages.push({ role: "user", content: question });
|
state.mentorMessages.push({ role: "user", content: question });
|
||||||
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "" };
|
const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] };
|
||||||
state.mentorMessages.push(responseMessage);
|
state.mentorMessages.push(responseMessage);
|
||||||
input.value = "";
|
input.value = "";
|
||||||
state.mentorLoading = true;
|
state.mentorLoading = true;
|
||||||
@@ -353,6 +363,9 @@ async function sendMentorQuestion(event) {
|
|||||||
},
|
},
|
||||||
(meta) => {
|
(meta) => {
|
||||||
responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`;
|
responseMessage.meta = `${displayCompactDate(meta.data_trade_date || elements.tradeDate.value)} · 回答完成`;
|
||||||
|
responseMessage.followUps = Array.isArray(meta.follow_ups)
|
||||||
|
? meta.follow_ups.filter((item) => typeof item === "string" && item.trim()).slice(0, 3)
|
||||||
|
: [];
|
||||||
if (meta.notice) showMentorNotice(meta.notice);
|
if (meta.notice) showMentorNotice(meta.notice);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -360,13 +373,23 @@ async function sendMentorQuestion(event) {
|
|||||||
setStatus("问师回答完成");
|
setStatus("问师回答完成");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
responseMessage.streaming = false;
|
responseMessage.streaming = false;
|
||||||
responseMessage.error = true;
|
responseMessage.followUps = [];
|
||||||
if (!responseMessage.content) {
|
if (state.mentorController?.signal.aborted) {
|
||||||
state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
|
if (responseMessage.content) {
|
||||||
|
responseMessage.meta = "生成已停止";
|
||||||
|
} else {
|
||||||
|
state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
|
||||||
|
}
|
||||||
|
setStatus("已停止问师回答");
|
||||||
|
} else {
|
||||||
|
responseMessage.error = true;
|
||||||
|
if (!responseMessage.content) {
|
||||||
|
state.mentorMessages = state.mentorMessages.filter((item) => item !== responseMessage);
|
||||||
|
}
|
||||||
|
showMentorNotice(error.message || "问师回答失败");
|
||||||
|
showToast(error.message || "问师回答失败");
|
||||||
|
setStatus("问师回答失败");
|
||||||
}
|
}
|
||||||
showMentorNotice(error.message || "问师回答失败");
|
|
||||||
showToast(error.message || "问师回答失败");
|
|
||||||
setStatus("问师回答失败");
|
|
||||||
} finally {
|
} finally {
|
||||||
state.mentorLoading = false;
|
state.mentorLoading = false;
|
||||||
state.mentorController = null;
|
state.mentorController = null;
|
||||||
@@ -376,6 +399,12 @@ async function sendMentorQuestion(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopMentorGeneration() {
|
||||||
|
if (!state.mentorLoading || !state.mentorController) return;
|
||||||
|
state.mentorController.abort();
|
||||||
|
setText("activeMentorStatus", "正在停止...");
|
||||||
|
}
|
||||||
|
|
||||||
let mentorRenderFrame = 0;
|
let mentorRenderFrame = 0;
|
||||||
|
|
||||||
function scheduleMentorRender() {
|
function scheduleMentorRender() {
|
||||||
@@ -497,12 +526,8 @@ function formatMentorInline(content) {
|
|||||||
|
|
||||||
function bindMentorEvents() {
|
function bindMentorEvents() {
|
||||||
document.querySelector("#mentorChatForm").addEventListener("submit", sendMentorQuestion);
|
document.querySelector("#mentorChatForm").addEventListener("submit", sendMentorQuestion);
|
||||||
|
document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration);
|
||||||
document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
|
document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation);
|
||||||
document.querySelector("#mentorDirectoryToggle").addEventListener("click", () => {
|
|
||||||
toggleMentorDirectory(!state.mentorDirectoryOpen);
|
|
||||||
});
|
|
||||||
document.querySelector("#closeMentorDirectory").addEventListener("click", () => toggleMentorDirectory(false));
|
|
||||||
document.querySelector("#mentorDirectoryBackdrop").addEventListener("click", () => toggleMentorDirectory(false));
|
|
||||||
document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
|
document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode);
|
||||||
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");
|
||||||
@@ -520,10 +545,9 @@ function bindMentorEvents() {
|
|||||||
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));
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.querySelector("#mentorQuestion").addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape") toggleMentorDirectory(false);
|
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
||||||
});
|
event.preventDefault();
|
||||||
window.addEventListener("resize", () => {
|
document.querySelector("#mentorChatForm").requestSubmit();
|
||||||
if (window.innerWidth > 720) toggleMentorDirectory(false);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-7
@@ -584,7 +584,7 @@
|
|||||||
border-bottom-color: var(--border);
|
border-bottom-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.main-grid {
|
.main-grid {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -1146,7 +1146,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
.pool-page-head .toolbar-controls {
|
.pool-page-head .toolbar-controls {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@@ -1290,7 +1290,7 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
.broken-page-head .toolbar-controls {
|
.broken-page-head .toolbar-controls {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
@@ -2182,7 +2182,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-performance-view .performance-cards {
|
.redesigned-performance-view .performance-cards {
|
||||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||||
}
|
}
|
||||||
@@ -2211,7 +2211,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-performance-view {
|
.redesigned-performance-view {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -2284,7 +2284,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .pool-reason-cell {
|
:is(#limitTable, #brokenTable, #downTable, #yesterdayTable) .pool-reason-cell {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
@@ -2374,7 +2374,7 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
#yesterdayView.active-view {
|
#yesterdayView.active-view {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
|
||||||
@@ -2489,3 +2489,79 @@
|
|||||||
.ladder-mini.pool-side-list {
|
.ladder-mini.pool-side-list {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile pool workspaces keep filters usable and tables locally horizontal. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
:is(#limitPool, #brokenView, #downView, #yesterdayView, #performanceView) {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.pool-page-head, .broken-page-head, .down-page-head, .yesterday-page-head) {
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell :is(.pool-page-head, .broken-page-head, .down-page-head, .yesterday-page-head) .toolbar-controls {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-8);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-page-head .pool-filter-segments {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-page-head .pool-filter-segments .segment {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding-inline: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.pool-page-head, .broken-page-head, .down-page-head, .yesterday-page-head) .pool-search-field {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.pool-page-head, .broken-page-head, .down-page-head, .yesterday-page-head) .pool-search-field input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#limitPool .redesigned-pool-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(#limitPool, #brokenView, #downView, #yesterdayView) .tbl-wrap {
|
||||||
|
min-height: var(--mobile-table-min-height);
|
||||||
|
max-height: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#limitPool .pool-insight-rail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#limitPool .pool-insight-rail .rail-section {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#yesterdayView .yesterday-result-summary {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#yesterdayView .yesterday-summary-cell {
|
||||||
|
min-width: var(--mobile-summary-card-width);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-4
@@ -602,7 +602,7 @@
|
|||||||
color: var(--r2-amber);
|
color: var(--r2-amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body[data-active-view="popularityView"] .app-main {
|
body[data-active-view="popularityView"] .app-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
@@ -626,7 +626,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-height: 900px) {
|
@media (min-width: 768px) and (max-height: 900px) {
|
||||||
.redesigned-popularity-view {
|
.redesigned-popularity-view {
|
||||||
padding-top: 9px;
|
padding-top: 9px;
|
||||||
|
|
||||||
@@ -684,7 +684,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-popularity-view {
|
.redesigned-popularity-view {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
@@ -869,7 +869,7 @@
|
|||||||
width: var(--col-number);
|
width: var(--col-number);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
#popularityView .popularity-glance-v2,
|
#popularityView .popularity-glance-v2,
|
||||||
#popularityView .popularity-page-head-v2 {
|
#popularityView .popularity-page-head-v2 {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -919,3 +919,16 @@
|
|||||||
|
|
||||||
box-shadow: var(--control-shadow);
|
box-shadow: var(--control-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#popularityView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popularityView .popularity-table-frame-v2 {
|
||||||
|
min-height: var(--mobile-table-min-height);
|
||||||
|
max-height: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+42
-8
@@ -109,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.trade-log-dialog {
|
.trade-log-dialog {
|
||||||
width: calc(-16px + 100vw);
|
width: calc(-16px + 100vw);
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
border-bottom-color: var(--border);
|
border-bottom-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
:where(#reviewWorkspaceView) .workspace-section {
|
:where(#reviewWorkspaceView) .workspace-section {
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ body[data-active-view="reviewWorkspaceView"] .workspace-view {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body[data-active-view="reviewWorkspaceView"] .workspace-view {
|
body[data-active-view="reviewWorkspaceView"] .workspace-view {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
:where(#reviewWorkspaceView) .review-workspace {
|
:where(#reviewWorkspaceView) .review-workspace {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -1058,7 +1058,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#reviewWorkspaceView .note-row {
|
#reviewWorkspaceView .note-row {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
@@ -1091,7 +1091,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#reviewWorkspaceView .review-page-header {
|
#reviewWorkspaceView .review-page-header {
|
||||||
min-height: 68px;
|
min-height: 68px;
|
||||||
|
|
||||||
@@ -1908,7 +1908,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#reviewWorkspaceView .review-watchlist-table {
|
#reviewWorkspaceView .review-watchlist-table {
|
||||||
min-width: 760px;
|
min-width: 760px;
|
||||||
}
|
}
|
||||||
@@ -2057,7 +2057,7 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
:root #reviewWorkspaceView.active-view {
|
:root #reviewWorkspaceView.active-view {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
@@ -2167,3 +2167,37 @@ body[data-active-view="reviewWorkspaceView"] .overview-strip {
|
|||||||
:root[data-theme="dark"] #reviewWorkspaceView .data-table tbody td {
|
:root[data-theme="dark"] #reviewWorkspaceView .data-table tbody td {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile review is a single vertical workflow with locally horizontal tables. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
body.mobile-shell #reviewWorkspaceView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #reviewWorkspaceView .review-workspace,
|
||||||
|
body.mobile-shell #reviewWorkspaceView .review-grid,
|
||||||
|
body.mobile-shell #reviewWorkspaceView .review-left-stack {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #reviewWorkspaceView :is(.workspace-table-frame, .trade-log-table-frame) {
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #reviewWorkspaceView :is(.notes-history-section, .notes-history) {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #reviewWorkspaceView .journal-section {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+15
-4
@@ -62,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.rotation-history {
|
.rotation-history {
|
||||||
min-height: 224px;
|
min-height: 224px;
|
||||||
}
|
}
|
||||||
@@ -991,7 +991,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-rotation-view {
|
.redesigned-rotation-view {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -1087,7 +1087,7 @@
|
|||||||
min-width: var(--table-wide);
|
min-width: var(--table-wide);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
#rotationView.active-view {
|
#rotationView.active-view {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
@@ -1157,7 +1157,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
#rotationView.active-view {
|
#rotationView.active-view {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -1245,3 +1245,14 @@
|
|||||||
|
|
||||||
background: var(--surface-muted);
|
background: var(--surface-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#rotationView :is(.rotation-table-frame, .rotation-detail-card) {
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rotationView .rotation-table-frame {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+67
-11
@@ -5,7 +5,7 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#screenerView {
|
#screenerView {
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#screenerView.mobile-results [data-screener-panel]:not([hidden]) {
|
#screenerView.mobile-results [data-screener-panel]:not([hidden]) {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
@@ -786,7 +786,7 @@
|
|||||||
box-shadow: 1px 0 0 var(--border);
|
box-shadow: 1px 0 0 var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) and (min-width: 721px) {
|
@media (max-width: 1023px) and (min-width: 768px) {
|
||||||
|
|
||||||
#screenerView .factor-data-status,
|
#screenerView .factor-data-status,
|
||||||
#screenerView .regime-evidence {
|
#screenerView .regime-evidence {
|
||||||
@@ -794,7 +794,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body[data-active-view="screenerView"] .overview-strip {
|
body[data-active-view="screenerView"] .overview-strip {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1114,7 @@
|
|||||||
color: var(--market-down);
|
color: var(--market-down);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.tracking-summary {
|
.tracking-summary {
|
||||||
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||||
}
|
}
|
||||||
@@ -1450,7 +1450,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
|
body.mobile-shell #screenerView .screener-stepper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding-inline: var(--space-8);
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #screenerView .screener-step {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 0;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--space-4);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #screenerView .screener-step > div {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #screenerView .screener-step :is(strong, small) {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #screenerView .step-line {
|
||||||
|
width: var(--space-8);
|
||||||
|
flex: 0 0 var(--space-8);
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: var(--space-12) var(--space-4) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
body.mobile-shell #screenerView,
|
||||||
|
body.mobile-shell #screenerTrackingView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #screenerView :is(.screener-result-frame, .curated-strategy-list, .quant-builder-pane, .quant-summary-pane),
|
||||||
|
body.mobile-shell #screenerTrackingView .tracking-table-frame.tbl-wrap {
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell #screenerView .screener-result-frame,
|
||||||
|
body.mobile-shell #screenerTrackingView .tracking-table-frame {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
.screener-page-heading {
|
.screener-page-heading {
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
|
|
||||||
@@ -1673,7 +1729,7 @@
|
|||||||
border-color: var(--border-strong);
|
border-color: var(--border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
#screenerView .screener-strategy-view {
|
#screenerView .screener-strategy-view {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
@@ -2031,7 +2087,7 @@ body[data-active-view="screenerView"] .workspace-view {
|
|||||||
min-height: 45px;
|
min-height: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.quant-screener-panel {
|
.quant-screener-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -2041,7 +2097,7 @@ body[data-active-view="screenerView"] .workspace-view {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body[data-active-view="screenerView"] .workspace-view {
|
body[data-active-view="screenerView"] .workspace-view {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
|
||||||
@@ -2373,7 +2429,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.screener-mode-tabs {
|
.screener-mode-tabs {
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
|
|
||||||
@@ -6042,7 +6098,7 @@ body[data-active-view="screenerView"] .overview-strip {
|
|||||||
min-width: var(--table-wide);
|
min-width: var(--table-wide);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
:root #screenerView.active-view {
|
:root #screenerView.active-view {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
|
|||||||
@@ -859,7 +859,7 @@ async function executeScreenerFormula({ mode, formula, strategyName, strategyId
|
|||||||
renderScreenerResult();
|
renderScreenerResult();
|
||||||
if (executionMode === "smart") setText("screenerRunStatus", `完成 · ${payload.result.candidates.length} 只`);
|
if (executionMode === "smart") setText("screenerRunStatus", `完成 · ${payload.result.candidates.length} 只`);
|
||||||
updateBacktestTaskStatus();
|
updateBacktestTaskStatus();
|
||||||
if (window.innerWidth <= 720) selectScreenerMobileView("results");
|
if (window.innerWidth <= 767) selectScreenerMobileView("results");
|
||||||
setStatus(`${strategyName}完成 · ${payload.result.candidates.length} 只候选`);
|
setStatus(`${strategyName}完成 · ${payload.result.candidates.length} 只候选`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message);
|
showToast(error.message);
|
||||||
|
|||||||
+96
-10
@@ -1,5 +1,5 @@
|
|||||||
/* Canonical CSS owner: sentiment. Historical layers consolidated 2026-08-02. */
|
/* Canonical CSS owner: sentiment. Historical layers consolidated 2026-08-02. */
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
#sentimentCycleView {
|
#sentimentCycleView {
|
||||||
--sentiment-history-max-height: min(480px, calc(100dvh - 210px));
|
--sentiment-history-max-height: min(480px, calc(100dvh - 210px));
|
||||||
}
|
}
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body[data-active-view="sentimentCycleView"] .overview-strip {
|
body[data-active-view="sentimentCycleView"] .overview-strip {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -541,7 +541,7 @@
|
|||||||
background: rgb(238, 243, 246);
|
background: rgb(238, 243, 246);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.sentiment-block {
|
.sentiment-block {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
|
||||||
@@ -619,7 +619,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.overview-strip .sentiment-block {
|
.overview-strip .sentiment-block {
|
||||||
grid-area: 1 / 1 / 3;
|
grid-area: 1 / 1 / 3;
|
||||||
|
|
||||||
@@ -1336,7 +1336,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) and (min-width: 721px) {
|
@media (max-width: 1023px) and (min-width: 768px) {
|
||||||
.redesigned-emotion-grid {
|
.redesigned-emotion-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -1348,7 +1348,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
.redesigned-emotion-grid {
|
.redesigned-emotion-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -1508,7 +1508,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
#sentimentCycleView.active-view {
|
#sentimentCycleView.active-view {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
|
||||||
@@ -1534,7 +1534,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-height: 1100px) {
|
@media (min-width: 768px) and (max-height: 1100px) {
|
||||||
#sentimentCycleView .sentiment-phase-block {
|
#sentimentCycleView .sentiment-phase-block {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
@@ -1570,7 +1570,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
:root #sentimentCycleView.active-view {
|
:root #sentimentCycleView.active-view {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
@@ -1661,7 +1661,7 @@
|
|||||||
padding-bottom: var(--card-gap);
|
padding-bottom: var(--card-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
.redesigned-sentiment-view .sentiment-chart-shell {
|
.redesigned-sentiment-view .sentiment-chart-shell {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
|
|
||||||
@@ -1674,3 +1674,89 @@
|
|||||||
grid-template-columns: minmax(270px, 1.25fr) repeat(3, minmax(160px, 1fr));
|
grid-template-columns: minmax(270px, 1.25fr) repeat(3, minmax(160px, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile information order: trend, current phase, composition, then history. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#sentimentCycleView .sentiment-cycle-toolbar {
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-cycle-analysis,
|
||||||
|
#sentimentCycleView .redesigned-emotion-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-analysis-main {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-analysis-rail {
|
||||||
|
order: 2;
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-trend-panel,
|
||||||
|
#sentimentCycleView .sentiment-components-panel {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-chart-legend {
|
||||||
|
gap: var(--space-12);
|
||||||
|
padding: 0 var(--space-12);
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-chart-shell,
|
||||||
|
#sentimentCycleView .sentiment-chart-shell canvas {
|
||||||
|
height: var(--mobile-chart-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-phase-block {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: var(--space-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-current-phase-badge {
|
||||||
|
min-width: var(--mobile-phase-badge-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-detail-toolbar {
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
padding: var(--space-8) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-history-frame {
|
||||||
|
min-height: var(--mobile-table-min-height);
|
||||||
|
max-height: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 359px) {
|
||||||
|
#sentimentCycleView .sentiment-phase-block {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
#sentimentCycleView .redesigned-emotion-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sentimentCycleView .sentiment-analysis-rail {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+25
-7
@@ -117,7 +117,7 @@
|
|||||||
border-right: 0px;
|
border-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.theme-detail-empty {
|
.theme-detail-empty {
|
||||||
min-height: 260px;
|
min-height: 260px;
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
|
|
||||||
.theme-directory {
|
.theme-directory {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
@@ -1100,7 +1100,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1180px) {
|
@media (min-width: 768px) and (max-width: 1180px) {
|
||||||
body[data-active-view="themeLibraryView"] .app-main {
|
body[data-active-view="themeLibraryView"] .app-main {
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
}
|
}
|
||||||
@@ -1128,7 +1128,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-height: 900px) {
|
@media (min-width: 768px) and (max-height: 900px) {
|
||||||
.redesigned-theme-view {
|
.redesigned-theme-view {
|
||||||
padding-top: 9px;
|
padding-top: 9px;
|
||||||
|
|
||||||
@@ -1192,7 +1192,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.redesigned-theme-view {
|
.redesigned-theme-view {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
@@ -1404,7 +1404,7 @@
|
|||||||
grid-template-columns: var(--right-rail-wide) minmax(0,1fr);
|
grid-template-columns: var(--right-rail-wide) minmax(0,1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
#themeLibraryView .theme-page-head-v2,
|
#themeLibraryView .theme-page-head-v2,
|
||||||
#themeLibraryView .theme-summary-v2 {
|
#themeLibraryView .theme-summary-v2 {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -1452,7 +1452,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
#themeLibraryView .theme-library-workspace-v2 {
|
#themeLibraryView .theme-library-workspace-v2 {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
@@ -1534,3 +1534,21 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#themeLibraryView {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#themeLibraryView :is(.theme-directory-v2, .theme-detail-column-v2, .theme-detail-stack-v2) {
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#themeLibraryView .theme-members-frame-v2 {
|
||||||
|
min-height: var(--mobile-table-min-height);
|
||||||
|
max-height: none;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -479,13 +479,13 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.account-button {
|
.account-button {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.header-command-group .account-button {
|
.header-command-group .account-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -1020,7 +1020,7 @@ button.account-role-badge:focus-visible {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.account-menu-shell {
|
.account-menu-shell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -1128,7 +1128,7 @@ button.account-role-badge:focus-visible {
|
|||||||
box-shadow: rgba(22, 34, 46, 0.15) 0px 14px 35px;
|
box-shadow: rgba(22, 34, 46, 0.15) 0px 14px 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.account-settings-dialog,
|
.account-settings-dialog,
|
||||||
.settings-dialog {
|
.settings-dialog {
|
||||||
width: calc(-16px + 100vw);
|
width: calc(-16px + 100vw);
|
||||||
@@ -1648,7 +1648,7 @@ button.account-role-badge:focus-visible {
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
.account-menu-shell {
|
.account-menu-shell {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,13 +125,13 @@ dialog::backdrop {
|
|||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body {
|
body {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
html {
|
html {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -185,3 +185,28 @@ dialog::backdrop {
|
|||||||
animation: auto ease 0s 1 normal none running none;
|
animation: auto ease 0s 1 normal none running none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile document contract: the page owns the only vertical scrollbar. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
min-width: var(--mobile-min-width);
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell {
|
||||||
|
width: 100%;
|
||||||
|
min-width: var(--mobile-min-width);
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding-bottom: var(--mobile-content-bottom);
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell.mobile-command-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
.redesigned-page-head {
|
.redesigned-page-head {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
:is(.redesigned-auction-view, .redesigned-theme-view, .redesigned-popularity-view, .redesigned-dragon-view) {
|
:is(.redesigned-auction-view, .redesigned-theme-view, .redesigned-popularity-view, .redesigned-dragon-view) {
|
||||||
width: min(100%, 2200px);
|
width: min(100%, 2200px);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
stroke-width: 1.75;
|
stroke-width: 1.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.header-command-group .command-button {
|
.header-command-group .command-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.command-button {
|
.command-button {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1023px) {
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
.sidebar-collapse-button span {
|
.sidebar-collapse-button span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.header-command-group .button.primary {
|
.header-command-group .button.primary {
|
||||||
border-color: var(--action);
|
border-color: var(--action);
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.stock-heaven-button span {
|
.stock-heaven-button span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
font-weight: 670;
|
font-weight: 670;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.sidebar-collapse-button span {
|
.sidebar-collapse-button span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.header-menu-button {
|
.header-menu-button {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
|
|
||||||
@@ -593,7 +593,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
box-shadow: 0 0 0 3px var(--focus-ring);
|
box-shadow: 0 0 0 3px var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.module-nav .sidebar-collapse-button {
|
.module-nav .sidebar-collapse-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -751,7 +751,7 @@ body.sidebar-collapsed .sidebar-collapse-button {
|
|||||||
color: var(--text-inverse);
|
color: var(--text-inverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) and (min-width: 721px) {
|
@media (max-width: 1023px) and (min-width: 768px) {
|
||||||
.sidebar-collapse-button span {
|
.sidebar-collapse-button span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -944,33 +944,7 @@ textarea {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
display: flex;
|
|
||||||
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
padding: 12px 16px;
|
|
||||||
|
|
||||||
border-top: 1px solid var(--line-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input input {
|
|
||||||
flex: 1 1 0%;
|
|
||||||
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
padding: 9px 12px;
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input input:focus {
|
|
||||||
border-color: var(--blue-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
|
||||||
.sidebar-collapse-button {
|
.sidebar-collapse-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1034,8 +1008,84 @@ textarea {
|
|||||||
box-shadow: var(--control-shadow);
|
box-shadow: var(--control-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
.header-menu-button {
|
.header-menu-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile control contract. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
body.mobile-shell .header-menu-button,
|
||||||
|
body.mobile-shell .header-actions > .header-menu-button {
|
||||||
|
width: var(--mobile-touch-size);
|
||||||
|
min-width: var(--mobile-touch-size);
|
||||||
|
height: var(--mobile-touch-size);
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group .command-button {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 var(--space-12);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--control-surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group .command-button > span {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-command-shortcut {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-market-selector:not([hidden]) {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--mobile-touch-size);
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) var(--mobile-nav-icon-size);
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-8);
|
||||||
|
margin: var(--mobile-page-pad) 0 0;
|
||||||
|
padding: 0 var(--space-12);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-market-selector select {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--mobile-touch-size) - 2px);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
text-align: right;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell :is(.button, .icon-button, button) {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] .mobile-command-shortcut {
|
||||||
|
border-color: var(--border);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.dialog-header-actions {
|
.dialog-header-actions {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.global-search-dialog {
|
.global-search-dialog {
|
||||||
width: calc(-16px + 100vw);
|
width: calc(-16px + 100vw);
|
||||||
|
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
height: 34px;
|
height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.alerts-dialog {
|
.alerts-dialog {
|
||||||
width: calc(-16px + 100vw);
|
width: calc(-16px + 100vw);
|
||||||
|
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.assistant-dialog {
|
.assistant-dialog {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-message p {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes breathe-core {
|
@keyframes breathe-core {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
@@ -163,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
@keyframes command-menu-enter {
|
@keyframes command-menu-enter {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.toolbar-controls {
|
.toolbar-controls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
box-shadow: rgba(0, 0, 0, 0.08) 0px 1px 2px;
|
box-shadow: rgba(0, 0, 0, 0.08) 0px 1px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
.redesigned-page-head .toolbar-controls {
|
.redesigned-page-head .toolbar-controls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -219,8 +219,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
.toolbar-controls {
|
.toolbar-controls {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile navigation and filter contract. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
body.mobile-shell .toolbar-controls,
|
||||||
|
body.mobile-shell .redesigned-page-head .toolbar-controls {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-8);
|
||||||
|
margin-left: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .toolbar-controls::-webkit-scrollbar,
|
||||||
|
body.mobile-shell .redesigned-page-head .toolbar-controls::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .segmented,
|
||||||
|
body.mobile-shell .redesigned-page-head .segmented {
|
||||||
|
width: max-content;
|
||||||
|
min-width: max-content;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .segment,
|
||||||
|
body.mobile-shell .redesigned-page-head .segment {
|
||||||
|
min-width: var(--mobile-touch-size);
|
||||||
|
min-height: calc(var(--mobile-touch-size) - var(--space-4));
|
||||||
|
padding: 0 var(--space-12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
background: var(--table-selected);
|
background: var(--table-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.data-table {
|
.data-table {
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
}
|
}
|
||||||
|
|||||||
+426
-27
@@ -65,21 +65,13 @@ body.sidebar-collapsed .nav-group + .nav-group {
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.header-actions {
|
.header-actions {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1023px) {
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
.module-nav {
|
|
||||||
width: 64px;
|
|
||||||
|
|
||||||
padding-right: 7px;
|
|
||||||
|
|
||||||
padding-left: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-group:first-of-type {
|
.nav-group:first-of-type {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
@@ -93,7 +85,7 @@ body.sidebar-collapsed .nav-group + .nav-group {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.market-tape {
|
.market-tape {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -139,6 +131,16 @@ body.sidebar-collapsed .nav-group + .nav-group {
|
|||||||
.status-bar {
|
.status-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:is(
|
||||||
|
[data-active-view="screenerView"],
|
||||||
|
[data-active-view="screenerTrackingView"],
|
||||||
|
[data-active-view="mentorView"],
|
||||||
|
[data-active-view="heavenView"],
|
||||||
|
[data-active-view="reviewWorkspaceView"]
|
||||||
|
) .overview-strip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-breadth-panel .workspace-heading {
|
.market-breadth-panel .workspace-heading {
|
||||||
@@ -295,7 +297,7 @@ body.sidebar-collapsed .nav-group + .nav-group {
|
|||||||
font-weight: 770;
|
font-weight: 770;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
body.sidebar-collapsed .module-nav {
|
body.sidebar-collapsed .module-nav {
|
||||||
width: 68px;
|
width: 68px;
|
||||||
|
|
||||||
@@ -315,7 +317,7 @@ body.sidebar-collapsed .nav-group + .nav-group {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.header-command-group {
|
.header-command-group {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
@@ -441,7 +443,7 @@ body.sidebar-collapsed .module-tab {
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1023px) {
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
.module-tab span {
|
.module-tab span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -457,7 +459,7 @@ body.sidebar-collapsed .module-tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.module-tab span {
|
.module-tab span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -473,7 +475,7 @@ body.sidebar-collapsed .module-tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.header-date-group .icon-button {
|
.header-date-group .icon-button {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
|
|
||||||
@@ -535,7 +537,7 @@ body.sidebar-collapsed .module-tab {
|
|||||||
border-right-color: var(--border);
|
border-right-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.metric {
|
.metric {
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|
||||||
@@ -557,7 +559,7 @@ body.sidebar-collapsed .module-tab {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.metric-label {
|
.metric-label {
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
}
|
}
|
||||||
@@ -695,7 +697,7 @@ body.sidebar-collapsed .app-main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
.module-nav .market-sub-tab {
|
.module-nav .market-sub-tab {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1015,7 +1017,7 @@ body.sidebar-collapsed .app-main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) and (max-width: 1279px) {
|
@media (min-width: 768px) and (max-width: 1279px) {
|
||||||
.app-header {
|
.app-header {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
|
||||||
@@ -1033,7 +1035,7 @@ body.sidebar-collapsed .app-main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 767px) {
|
||||||
body.sidebar-collapsed {
|
body.sidebar-collapsed {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -2055,7 +2057,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) and (min-width: 721px) {
|
@media (max-width: 1023px) and (min-width: 768px) {
|
||||||
.module-nav {
|
.module-nav {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
|
|
||||||
@@ -2089,7 +2091,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
.redesigned-page-head .section-title-group {
|
.redesigned-page-head .section-title-group {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -2101,7 +2103,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
.app-main {
|
.app-main {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
|
|
||||||
@@ -2397,7 +2399,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .app-main {
|
body:is([data-active-view="sentimentCycleView"], [data-active-view="yesterdayView"]) .app-main {
|
||||||
height: var(--workspace-height);
|
height: var(--workspace-height);
|
||||||
|
|
||||||
@@ -2449,7 +2451,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px), (max-width: 1023px) and (max-height: 600px) {
|
@media (max-width: 767px), (max-width: 1023px) and (max-height: 600px) {
|
||||||
body.sidebar-collapsed {
|
body.sidebar-collapsed {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -2808,7 +2810,7 @@ body.sidebar-collapsed .status-bar {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 768px) {
|
||||||
.header-actions > .icon-button {
|
.header-actions > .icon-button {
|
||||||
min-width: 31px;
|
min-width: 31px;
|
||||||
}
|
}
|
||||||
@@ -2823,3 +2825,400 @@ body.sidebar-collapsed .status-bar {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.main,
|
||||||
|
body.sidebar-collapsed .main {
|
||||||
|
width: calc(100% - var(--sidebar-compact-width));
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: var(--sidebar-compact-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-nav,
|
||||||
|
body.sidebar-collapsed .module-nav {
|
||||||
|
width: var(--sidebar-compact-width);
|
||||||
|
padding-right: 7px;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar,
|
||||||
|
body.sidebar-collapsed .status-bar {
|
||||||
|
left: var(--sidebar-compact-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Canonical mobile shell. Page-specific responsive rules remain with each page. */
|
||||||
|
.mobile-page-context,
|
||||||
|
.mobile-command-shortcuts,
|
||||||
|
.mobile-command-backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mobile-shell,
|
||||||
|
.mobile-shell.sidebar-collapsed {
|
||||||
|
display: block;
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding-bottom: var(--mobile-content-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .main,
|
||||||
|
body.mobile-shell.sidebar-collapsed .main {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 100dvh;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .app-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--mobile-layer-header);
|
||||||
|
width: 100%;
|
||||||
|
height: var(--mobile-header-height);
|
||||||
|
min-height: var(--mobile-header-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding: var(--space-4) var(--mobile-shell-pad);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell.mobile-command-open .app-header {
|
||||||
|
z-index: var(--mobile-layer-menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-page-context {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-page-context span {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--font-size-aux);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-page-context strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-body);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: 1.35;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .market-tape,
|
||||||
|
body.mobile-shell .header-actions > :is(.global-search-button, .theme-toggle, .alert-button, .assistant-button) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-actions {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-date-group {
|
||||||
|
width: var(--mobile-date-width);
|
||||||
|
height: var(--mobile-touch-size);
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--control-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-date-group > .icon-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-date-group .date-input {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--mobile-touch-size) - 2px);
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-caption);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .app-header .header-actions > .header-menu-button {
|
||||||
|
width: var(--mobile-touch-size);
|
||||||
|
min-width: var(--mobile-touch-size);
|
||||||
|
height: var(--mobile-touch-size);
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--control-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group {
|
||||||
|
position: fixed;
|
||||||
|
inset: auto var(--mobile-shell-pad) var(--mobile-content-bottom);
|
||||||
|
z-index: var(--mobile-layer-menu);
|
||||||
|
width: auto;
|
||||||
|
max-height: calc(100dvh - var(--mobile-header-height) - var(--mobile-content-bottom) - var(--space-16));
|
||||||
|
display: none;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding: var(--space-12);
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--surface-overlay);
|
||||||
|
box-shadow: var(--shadow-float);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group.is-open {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-command-shortcuts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding-bottom: var(--space-8);
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group .mobile-command-shortcuts > .mobile-command-shortcut {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-8) var(--space-4);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size-caption);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group > .command-button,
|
||||||
|
body.mobile-shell .header-command-group .account-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .header-command-group .account-dropdown {
|
||||||
|
position: static;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .mobile-command-backdrop:not([hidden]) {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: var(--mobile-layer-backdrop);
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: var(--backdrop);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav,
|
||||||
|
body.mobile-shell.sidebar-collapsed .module-nav {
|
||||||
|
position: fixed;
|
||||||
|
inset: auto 0 0;
|
||||||
|
z-index: var(--mobile-layer-nav);
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--mobile-nav-height) + var(--mobile-safe-bottom));
|
||||||
|
min-height: calc(var(--mobile-nav-height) + var(--mobile-safe-bottom));
|
||||||
|
max-height: calc(var(--mobile-nav-height) + var(--mobile-safe-bottom));
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
align-items: start;
|
||||||
|
gap: 0;
|
||||||
|
padding: var(--space-4) var(--space-4) max(var(--space-4), var(--mobile-safe-bottom));
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--surface-overlay);
|
||||||
|
box-shadow: var(--elevation-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav :is(.sidebar-brand, .nav-group-label, .market-sub-tab, .sidebar-collapse-button),
|
||||||
|
body.mobile-shell.sidebar-collapsed .module-nav :is(.sidebar-brand, .nav-group-label, .market-sub-tab, .sidebar-collapse-button) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .nav-group,
|
||||||
|
body.mobile-shell.sidebar-collapsed .module-nav .nav-group {
|
||||||
|
display: contents;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab,
|
||||||
|
body.mobile-shell.sidebar-collapsed .module-nav .module-tab {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab.mobile-primary-tab,
|
||||||
|
body.mobile-shell.sidebar-collapsed .module-nav .module-tab.mobile-primary-tab {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: var(--mobile-tab-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--font-size-aux);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab.mobile-primary-tab .lucide {
|
||||||
|
width: var(--mobile-nav-icon-size);
|
||||||
|
height: var(--mobile-nav-icon-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab.mobile-primary-tab:is(.active, .mobile-active) {
|
||||||
|
background: var(--action-soft);
|
||||||
|
color: var(--action);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab.mobile-primary-tab span,
|
||||||
|
body.mobile-shell.sidebar-collapsed .module-nav .module-tab.mobile-primary-tab span {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab.mobile-primary-tab .nav-label-desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .module-nav .module-tab.mobile-primary-tab .nav-label-mobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .app-main,
|
||||||
|
body.mobile-shell.sidebar-collapsed .app-main,
|
||||||
|
body.mobile-shell:is([data-active-view]) .app-main {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: calc(100dvh - var(--mobile-header-height) - var(--mobile-content-bottom));
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 var(--mobile-page-pad) var(--mobile-page-pad);
|
||||||
|
overflow: visible;
|
||||||
|
background: var(--surface-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .overview-strip,
|
||||||
|
body.mobile-shell .overview-strip[data-overview-expanded="true"] {
|
||||||
|
width: calc(100% + var(--mobile-page-pad) + var(--mobile-page-pad));
|
||||||
|
height: var(--mobile-tab-height);
|
||||||
|
min-height: var(--mobile-tab-height);
|
||||||
|
display: flex;
|
||||||
|
margin: 0 calc(var(--mobile-page-pad) * -1);
|
||||||
|
padding: 0 var(--mobile-page-pad);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .overview-strip::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .overview-strip .row {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-8);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .overview-strip .sentiment-block,
|
||||||
|
body.mobile-shell .overview-strip .metric,
|
||||||
|
body.mobile-shell .overview-strip[data-overview-expanded="true"] .metric {
|
||||||
|
min-width: max-content;
|
||||||
|
min-height: var(--mobile-touch-size);
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: 0 var(--space-8);
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .overview-strip .metric:nth-of-type(n) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .overview-strip .metric-wide,
|
||||||
|
body.mobile-shell .overview-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .workspace-view,
|
||||||
|
body.mobile-shell .workspace-view.page:not(#heavenView),
|
||||||
|
body.mobile-shell:is([data-active-view]) .workspace-view.active-view {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--mobile-page-pad) 0 0;
|
||||||
|
overflow: visible;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-shell .status-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 359px) {
|
||||||
|
.header-date-group {
|
||||||
|
width: calc(var(--mobile-date-width) - var(--space-16));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-page-context span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,25 +2,42 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const SIDEBAR_STORAGE_KEY = "xiaobai-sidebar-collapsed";
|
const SIDEBAR_STORAGE_KEY = "xiaobai-sidebar-collapsed";
|
||||||
|
const MOBILE_BREAKPOINT = 767;
|
||||||
|
|
||||||
|
function isMobileViewport() {
|
||||||
|
return global.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches;
|
||||||
|
}
|
||||||
|
|
||||||
function create(options) {
|
function create(options) {
|
||||||
const state = options.state;
|
const state = options.state;
|
||||||
const registry = options.pages;
|
const registry = options.pages;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
function syncViewportMode() {
|
||||||
|
document.body.classList.toggle("mobile-shell", isMobileViewport());
|
||||||
|
}
|
||||||
|
|
||||||
function toggleHeaderCommandMenu(force) {
|
function toggleHeaderCommandMenu(force) {
|
||||||
const menu = document.querySelector("#headerCommandGroup");
|
const menu = document.querySelector("#headerCommandGroup");
|
||||||
const button = document.querySelector("#headerMenuButton");
|
const button = document.querySelector("#headerMenuButton");
|
||||||
|
const backdrop = document.querySelector("#mobileCommandBackdrop");
|
||||||
if (!menu || !button) return;
|
if (!menu || !button) 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());
|
||||||
|
if (backdrop) backdrop.hidden = !(open && isMobileViewport());
|
||||||
|
if (open && isMobileViewport()) {
|
||||||
|
global.requestAnimationFrame(() => menu.querySelector("button:not([hidden])")?.focus({ preventScroll: true }));
|
||||||
|
} else if (force === false && document.activeElement && menu.contains(document.activeElement)) {
|
||||||
|
button.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSidebarControl() {
|
function updateSidebarControl() {
|
||||||
const button = document.querySelector("#sidebarCollapseButton");
|
const button = document.querySelector("#sidebarCollapseButton");
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
const automaticallyCollapsed = global.innerWidth <= 1023 && global.innerWidth > 720;
|
const automaticallyCollapsed = global.innerWidth <= 1023 && !isMobileViewport();
|
||||||
const collapsed = document.body.classList.contains("sidebar-collapsed") || automaticallyCollapsed;
|
const collapsed = document.body.classList.contains("sidebar-collapsed") || automaticallyCollapsed;
|
||||||
button.setAttribute("aria-expanded", String(!collapsed));
|
button.setAttribute("aria-expanded", String(!collapsed));
|
||||||
button.setAttribute("aria-label", collapsed ? "展开侧栏" : "收起侧栏");
|
button.setAttribute("aria-label", collapsed ? "展开侧栏" : "收起侧栏");
|
||||||
@@ -44,11 +61,19 @@
|
|||||||
const navigationId = page?.navigation_alias || viewId;
|
const navigationId = page?.navigation_alias || viewId;
|
||||||
const marketView = page?.group === "market";
|
const marketView = page?.group === "market";
|
||||||
document.body.dataset.activeView = viewId;
|
document.body.dataset.activeView = viewId;
|
||||||
|
const mobileGroup = document.querySelector("#mobilePageGroup");
|
||||||
|
const mobileTitle = document.querySelector("#mobilePageTitle");
|
||||||
|
if (mobileGroup) {
|
||||||
|
mobileGroup.textContent = page?.group === "market"
|
||||||
|
? "行情"
|
||||||
|
: page?.group === "personal" ? "复盘" : "工具";
|
||||||
|
}
|
||||||
|
if (mobileTitle) mobileTitle.textContent = page?.title || "小白复盘";
|
||||||
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(
|
||||||
"mobile-active",
|
"mobile-active",
|
||||||
global.innerWidth <= 720
|
isMobileViewport()
|
||||||
&& marketView
|
&& marketView
|
||||||
&& button.dataset.view === "limitPool"
|
&& button.dataset.view === "limitPool"
|
||||||
&& viewId !== "limitPool",
|
&& viewId !== "limitPool",
|
||||||
@@ -127,6 +152,7 @@
|
|||||||
collapsed = false;
|
collapsed = false;
|
||||||
}
|
}
|
||||||
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
||||||
|
syncViewportMode();
|
||||||
updateSidebarControl();
|
updateSidebarControl();
|
||||||
syncNavigation(state.activeView);
|
syncNavigation(state.activeView);
|
||||||
document.querySelectorAll(".module-tab").forEach((button) => {
|
document.querySelectorAll(".module-tab").forEach((button) => {
|
||||||
@@ -143,6 +169,16 @@
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleHeaderCommandMenu();
|
toggleHeaderCommandMenu();
|
||||||
});
|
});
|
||||||
|
document.querySelector("#mobileCommandBackdrop")?.addEventListener("click", () => {
|
||||||
|
toggleHeaderCommandMenu(false);
|
||||||
|
});
|
||||||
|
document.querySelectorAll("[data-mobile-command-target]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const target = document.getElementById(button.dataset.mobileCommandTarget || "");
|
||||||
|
toggleHeaderCommandMenu(false);
|
||||||
|
target?.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
document.querySelector("#headerCommandGroup")?.addEventListener("click", (event) => {
|
document.querySelector("#headerCommandGroup")?.addEventListener("click", (event) => {
|
||||||
if (event.target.closest("button") && !event.target.closest(".account-menu-shell")) {
|
if (event.target.closest("button") && !event.target.closest(".account-menu-shell")) {
|
||||||
toggleHeaderCommandMenu(false);
|
toggleHeaderCommandMenu(false);
|
||||||
@@ -170,7 +206,8 @@
|
|||||||
if (event.key === "Escape") toggleHeaderCommandMenu(false);
|
if (event.key === "Escape") toggleHeaderCommandMenu(false);
|
||||||
});
|
});
|
||||||
global.addEventListener("resize", () => {
|
global.addEventListener("resize", () => {
|
||||||
if (global.innerWidth > 720) toggleHeaderCommandMenu(false);
|
toggleHeaderCommandMenu(false);
|
||||||
|
syncViewportMode();
|
||||||
updateSidebarControl();
|
updateSidebarControl();
|
||||||
syncNavigation(state.activeView);
|
syncNavigation(state.activeView);
|
||||||
});
|
});
|
||||||
@@ -184,6 +221,7 @@
|
|||||||
setPageStatus,
|
setPageStatus,
|
||||||
setStatus,
|
setStatus,
|
||||||
syncNavigation,
|
syncNavigation,
|
||||||
|
syncViewportMode,
|
||||||
toggleHeaderCommandMenu,
|
toggleHeaderCommandMenu,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
updateSidebarControl,
|
updateSidebarControl,
|
||||||
|
|||||||
@@ -150,6 +150,7 @@
|
|||||||
--space-24: 24px;
|
--space-24: 24px;
|
||||||
|
|
||||||
--sidebar-width: var(--size-sidebar);
|
--sidebar-width: var(--size-sidebar);
|
||||||
|
--sidebar-compact-width: 64px;
|
||||||
--topbar-height: var(--size-topbar);
|
--topbar-height: var(--size-topbar);
|
||||||
--summary-height: var(--size-summary);
|
--summary-height: var(--size-summary);
|
||||||
--statusbar-height: var(--size-statusbar);
|
--statusbar-height: var(--size-statusbar);
|
||||||
@@ -173,12 +174,25 @@
|
|||||||
--sentiment-history-min-height: 220px;
|
--sentiment-history-min-height: 220px;
|
||||||
--primary-share: 1.45fr;
|
--primary-share: 1.45fr;
|
||||||
--secondary-share: .75fr;
|
--secondary-share: .75fr;
|
||||||
--mobile-nav-height: 58px;
|
--mobile-nav-height: 64px;
|
||||||
--mobile-header-height: 50px;
|
--mobile-header-height: 52px;
|
||||||
--mobile-tab-height: 54px;
|
--mobile-tab-height: 56px;
|
||||||
--mobile-shell-pad: 8px;
|
--mobile-touch-size: 44px;
|
||||||
--mobile-page-pad: 10px;
|
--mobile-date-width: 116px;
|
||||||
|
--mobile-nav-icon-size: 20px;
|
||||||
|
--mobile-chart-height: 240px;
|
||||||
|
--mobile-table-min-height: 260px;
|
||||||
|
--mobile-phase-badge-width: 88px;
|
||||||
|
--mobile-summary-card-width: 124px;
|
||||||
|
--mobile-shell-pad: var(--space-8);
|
||||||
|
--mobile-page-pad: var(--space-12);
|
||||||
--mobile-min-width: 320px;
|
--mobile-min-width: 320px;
|
||||||
|
--mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--mobile-content-bottom: calc(var(--mobile-nav-height) + var(--mobile-safe-bottom) + var(--mobile-page-pad));
|
||||||
|
--mobile-layer-header: 50;
|
||||||
|
--mobile-layer-nav: 70;
|
||||||
|
--mobile-layer-menu: 80;
|
||||||
|
--mobile-layer-backdrop: 75;
|
||||||
--font-aux: var(--font-size-aux);
|
--font-aux: var(--font-size-aux);
|
||||||
|
|
||||||
--dragon-profile-list-width: 340px;
|
--dragon-profile-list-width: 340px;
|
||||||
@@ -205,6 +219,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-profile-width: 272px;
|
||||||
|
--mentor-pane-header-height: 58px;
|
||||||
|
--mentor-avatar-size: 38px;
|
||||||
|
--mentor-profile-avatar-size: 68px;
|
||||||
|
--mentor-composer-min-height: 94px;
|
||||||
|
--mentor-message-max-width: 82%;
|
||||||
|
|
||||||
--chart-background: #fbfcfd;
|
--chart-background: #fbfcfd;
|
||||||
--chart-grid: #e2e8ec;
|
--chart-grid: #e2e8ec;
|
||||||
--chart-axis: #6c7983;
|
--chart-axis: #6c7983;
|
||||||
|
|||||||
+105
-31
@@ -491,7 +491,12 @@ async function mockApplication(page, authSession = session(), options = {}) {
|
|||||||
body: [
|
body: [
|
||||||
JSON.stringify({ type: "delta", content: "## 判断\n先看市场结构。\n\n" }),
|
JSON.stringify({ type: "delta", content: "## 判断\n先看市场结构。\n\n" }),
|
||||||
JSON.stringify({ type: "delta", content: "- 等待确认\n- 控制仓位" }),
|
JSON.stringify({ type: "delta", content: "- 等待确认\n- 控制仓位" }),
|
||||||
JSON.stringify({ type: "meta", data_trade_date: "20260722", notice: "" }),
|
JSON.stringify({
|
||||||
|
type: "meta",
|
||||||
|
data_trade_date: "20260722",
|
||||||
|
notice: "",
|
||||||
|
follow_ups: ["哪些信号代表确认?", "这个判断在什么情况下失效?", "空仓时应该先观察什么?"],
|
||||||
|
}),
|
||||||
JSON.stringify({ type: "done" }),
|
JSON.stringify({ type: "done" }),
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
});
|
});
|
||||||
@@ -1931,8 +1936,19 @@ test("mobile shell stays within the viewport", async ({ page }) => {
|
|||||||
await page.goto("/index.html");
|
await page.goto("/index.html");
|
||||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
||||||
expect(overflow).toBeLessThanOrEqual(1);
|
expect(overflow).toBeLessThanOrEqual(1);
|
||||||
await expect(page.locator("#globalSearchButton")).toBeVisible();
|
await expect(page.locator("#globalSearchButton")).toBeHidden();
|
||||||
|
await expect(page.locator("#headerMenuButton")).toBeVisible();
|
||||||
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeVisible();
|
||||||
|
await expect(page.locator('[data-mobile-command-target="globalSearchButton"]')).toBeVisible();
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.locator("#headerCommandGroup")).toBeHidden();
|
||||||
|
await expect(page.locator(".module-nav .mobile-primary-tab:visible")).toHaveCount(5);
|
||||||
await expect(page.locator('[data-view="limitPool"]')).toHaveClass(/mobile-active/);
|
await expect(page.locator('[data-view="limitPool"]')).toHaveClass(/mobile-active/);
|
||||||
|
for (const entry of await page.locator(".module-nav .mobile-primary-tab:visible").all()) {
|
||||||
|
const box = await entry.boundingBox();
|
||||||
|
expect(box.height).toBeGreaterThanOrEqual(44);
|
||||||
|
}
|
||||||
const mobileShell = await page.evaluate(() => {
|
const mobileShell = await page.evaluate(() => {
|
||||||
const header = document.querySelector(".app-header").getBoundingClientRect();
|
const header = document.querySelector(".app-header").getBoundingClientRect();
|
||||||
const main = document.querySelector(".app-main").getBoundingClientRect();
|
const main = document.querySelector(".app-main").getBoundingClientRect();
|
||||||
@@ -1941,6 +1957,69 @@ test("mobile shell stays within the viewport", async ({ page }) => {
|
|||||||
expect(mobileShell.mainTop).toBeGreaterThanOrEqual(mobileShell.headerBottom - 1);
|
expect(mobileShell.mainTop).toBeGreaterThanOrEqual(mobileShell.headerBottom - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mobile shell remains usable at supported narrow widths", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 430, height: 932 });
|
||||||
|
await mockApplication(page, session("user", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
const registeredViews = await page.evaluate(() => window.XiaobaiPages.pages.map((entry) => entry.id));
|
||||||
|
for (const viewport of [
|
||||||
|
{ width: 430, height: 932 },
|
||||||
|
{ width: 390, height: 844 },
|
||||||
|
{ width: 320, height: 720 },
|
||||||
|
]) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await expect(page.locator("body")).toHaveClass(/mobile-shell/);
|
||||||
|
for (const theme of ["light", "dark"]) {
|
||||||
|
for (const view of registeredViews) {
|
||||||
|
await page.evaluate(({ targetView, targetTheme }) => {
|
||||||
|
document.documentElement.dataset.theme = targetTheme;
|
||||||
|
openView(targetView);
|
||||||
|
}, { targetView: view, targetTheme: theme });
|
||||||
|
const geometry = await page.evaluate(() => ({
|
||||||
|
overflow: document.documentElement.scrollWidth - innerWidth,
|
||||||
|
visibleEntries: Array.from(document.querySelectorAll(".module-nav .mobile-primary-tab"))
|
||||||
|
.filter((entry) => getComputedStyle(entry).display !== "none").length,
|
||||||
|
smallestTarget: Math.min(...Array.from(document.querySelectorAll(".module-nav .mobile-primary-tab"))
|
||||||
|
.filter((entry) => getComputedStyle(entry).display !== "none")
|
||||||
|
.map((entry) => entry.getBoundingClientRect().height)),
|
||||||
|
}));
|
||||||
|
expect(geometry.overflow, `${view} overflows at ${viewport.width}px in ${theme} theme`).toBeLessThanOrEqual(1);
|
||||||
|
expect(geometry.visibleEntries).toBe(5);
|
||||||
|
expect(geometry.smallestTarget).toBeGreaterThanOrEqual(44);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.evaluate(() => openView("ladderView"));
|
||||||
|
await expect(page.locator("#mobileMarketSelector")).toBeVisible();
|
||||||
|
await expect(page.locator("#mobileMarketViewSelect option")).toHaveCount(12);
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("768px boundary uses the compact desktop shell", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 768, height: 900 });
|
||||||
|
await mockApplication(page, session("user", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.evaluate(() => openView("sentimentCycleView"));
|
||||||
|
|
||||||
|
await expect(page.locator("body")).not.toHaveClass(/mobile-shell/);
|
||||||
|
const geometry = await page.evaluate(() => {
|
||||||
|
const nav = document.querySelector(".module-nav").getBoundingClientRect();
|
||||||
|
const main = document.querySelector(".main").getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
overflow: document.documentElement.scrollWidth - innerWidth,
|
||||||
|
navWidth: nav.width,
|
||||||
|
navRight: nav.right,
|
||||||
|
mainLeft: main.left,
|
||||||
|
mainRight: main.right,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(geometry.overflow).toBeLessThanOrEqual(1);
|
||||||
|
expect(geometry.navWidth).toBeGreaterThanOrEqual(44);
|
||||||
|
expect(geometry.navWidth).toBeLessThan(200);
|
||||||
|
expect(Math.abs(geometry.mainLeft - geometry.navRight)).toBeLessThanOrEqual(1);
|
||||||
|
expect(geometry.mainRight).toBeLessThanOrEqual(768);
|
||||||
|
});
|
||||||
|
|
||||||
test("native dialogs share one lifecycle and success feedback stays content-sized", async ({ page }) => {
|
test("native dialogs share one lifecycle and success feedback stays content-sized", async ({ page }) => {
|
||||||
await mockApplication(page, session("admin", true));
|
await mockApplication(page, session("admin", true));
|
||||||
await page.goto("/index.html");
|
await page.goto("/index.html");
|
||||||
@@ -2482,10 +2561,12 @@ test("screener publishes atomically and exposes latest, active, and historical c
|
|||||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
||||||
expect(overflow).toBeLessThanOrEqual(1);
|
expect(overflow).toBeLessThanOrEqual(1);
|
||||||
|
|
||||||
await page.locator("#themeToggle").click();
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await page.locator('[data-mobile-command-target="themeToggle"]').click();
|
||||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||||
await expect(page.locator("#screenerView .screener-results-view")).toBeVisible();
|
await expect(page.locator("#screenerView .screener-results-view")).toBeVisible();
|
||||||
await page.locator("#themeToggle").click();
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await page.locator('[data-mobile-command-target="themeToggle"]').click();
|
||||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
|
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2724,7 +2805,7 @@ 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-page-header .mentor-evidence-filters")).toBeVisible();
|
await expect(page.locator("#mentorView .mentor-sidebar .mentor-evidence-filters")).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");
|
||||||
@@ -2742,22 +2823,25 @@ test("mentor directory exposes evidence filters and private owner metadata", asy
|
|||||||
await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
|
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 - 340)).toBeLessThanOrEqual(1);
|
expect(Math.abs(mentorLibrary.width - 280)).toBeLessThanOrEqual(1);
|
||||||
expect(mentorChat.x - (mentorLibrary.x + mentorLibrary.width)).toBeGreaterThanOrEqual(11);
|
expect(Math.abs(mentorProfile.width - 272)).toBeLessThanOrEqual(1);
|
||||||
expect(mentorInput.height).toBeLessThanOrEqual(40);
|
expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(mentorProfile.x - (mentorChat.x + mentorChat.width))).toBeLessThanOrEqual(1);
|
||||||
|
expect(mentorInput.height).toBeGreaterThanOrEqual(48);
|
||||||
expect(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 statusBar = await page.locator(".status-bar").boundingBox();
|
||||||
const lowerGap = statusBar.y - (expandedMentorLayout.y + expandedMentorLayout.height);
|
const lowerGap = statusBar.y - (expandedMentorLayout.y + expandedMentorLayout.height);
|
||||||
expect(lowerGap).toBeGreaterThanOrEqual(0);
|
expect(lowerGap).toBeGreaterThanOrEqual(0);
|
||||||
expect(lowerGap).toBeLessThanOrEqual(8);
|
expect(lowerGap).toBeLessThanOrEqual(16);
|
||||||
await page.locator("#overviewToggle").click();
|
await page.locator("#overviewToggle").click();
|
||||||
const openOverviewLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
const openOverviewLayout = await page.locator("#mentorView .mentor-layout").boundingBox();
|
||||||
const openOverviewGap = statusBar.y - (openOverviewLayout.y + openOverviewLayout.height);
|
const openOverviewGap = statusBar.y - (openOverviewLayout.y + openOverviewLayout.height);
|
||||||
expect(openOverviewGap).toBeGreaterThanOrEqual(0);
|
expect(openOverviewGap).toBeGreaterThanOrEqual(0);
|
||||||
expect(openOverviewGap).toBeLessThanOrEqual(8);
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2783,6 +2867,12 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
await expect(answer.locator(".mentor-answer-list li")).toHaveCount(2);
|
await expect(answer.locator(".mentor-answer-list li")).toHaveCount(2);
|
||||||
await expect(answer.locator("br")).toHaveCount(0);
|
await expect(answer.locator("br")).toHaveCount(0);
|
||||||
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]");
|
||||||
|
await expect(followUps).toHaveCount(3);
|
||||||
|
const messageCount = await page.locator("#mentorMessages .mentor-message").count();
|
||||||
|
await followUps.first().click();
|
||||||
|
await expect(page.locator("#mentorQuestion")).toHaveValue("哪些信号代表确认?");
|
||||||
|
await expect(page.locator("#mentorMessages .mentor-message")).toHaveCount(messageCount);
|
||||||
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) => ({
|
||||||
@@ -2791,9 +2881,9 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
contentBackground: getComputedStyle(element.querySelector(".mentor-message-content")).backgroundColor,
|
contentBackground: getComputedStyle(element.querySelector(".mentor-message-content")).backgroundColor,
|
||||||
contentColor: getComputedStyle(element.querySelector(".mentor-message-content")).color,
|
contentColor: getComputedStyle(element.querySelector(".mentor-message-content")).color,
|
||||||
}));
|
}));
|
||||||
expect(darkUserMessageStyle.background).toBe("rgb(35, 54, 74)");
|
expect(darkUserMessageStyle.background).not.toBe("rgb(255, 255, 255)");
|
||||||
expect(darkUserMessageStyle.border).toBe("rgb(66, 105, 142)");
|
expect(darkUserMessageStyle.border).not.toBe("rgb(255, 255, 255)");
|
||||||
expect(darkUserMessageStyle.contentBackground).toBe("rgba(0, 0, 0, 0)");
|
expect(darkUserMessageStyle.contentBackground).toBe("rgb(35, 54, 74)");
|
||||||
expect(darkUserMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
|
expect(darkUserMessageStyle.contentColor).toBe("rgb(232, 234, 237)");
|
||||||
const darkMessageStyle = await answer.evaluate((element) => {
|
const darkMessageStyle = await answer.evaluate((element) => {
|
||||||
const style = getComputedStyle(element);
|
const style = getComputedStyle(element);
|
||||||
@@ -2820,23 +2910,6 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa
|
|||||||
expect(darkMessageStyle.metaColor).toBe("rgb(127, 137, 147)");
|
expect(darkMessageStyle.metaColor).toBe("rgb(127, 137, 147)");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mobile mentor directory opens as a searchable selector and hides private mentors", async ({ page }) => {
|
|
||||||
await page.setViewportSize({ width: 375, height: 812 });
|
|
||||||
await mockApplication(page, session("user", true));
|
|
||||||
await page.goto("/index.html");
|
|
||||||
await page.locator('[data-view="mentorView"]').first().click();
|
|
||||||
|
|
||||||
await expect(page.locator("#mentorDirectoryToggle")).toBeVisible();
|
|
||||||
await page.locator("#mentorDirectoryToggle").click();
|
|
||||||
await expect(page.locator("#mentorView .mentor-sidebar")).toHaveClass(/is-open/);
|
|
||||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(21);
|
|
||||||
await expect(page.locator('#mentorList [data-mentor-id="private-owner"]')).toHaveCount(0);
|
|
||||||
await page.locator('#mentorList [data-mentor-id="source-c"]').click();
|
|
||||||
await expect(page.locator("#mentorView .mentor-sidebar")).not.toHaveClass(/is-open/);
|
|
||||||
await expect(page.locator("#mobileActiveMentorName")).toHaveText("推演老师");
|
|
||||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 }) => {
|
||||||
await page.setViewportSize({ width: 1440, height: 900 });
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
await mockApplication(page, session("admin", true));
|
await mockApplication(page, session("admin", true));
|
||||||
@@ -2898,7 +2971,8 @@ test("global dialogs share the stage 18 geometry without changing account or adm
|
|||||||
await page.locator("#closeAdminDialog").click();
|
await page.locator("#closeAdminDialog").click();
|
||||||
|
|
||||||
await page.setViewportSize({ width: 375, height: 812 });
|
await page.setViewportSize({ width: 375, height: 812 });
|
||||||
await page.locator("#alertButton").click();
|
await page.locator("#headerMenuButton").click();
|
||||||
|
await page.locator('[data-mobile-command-target="alertButton"]').click();
|
||||||
const mobileGeometry = await page.locator("#alertsDialog").evaluate((dialog) => ({
|
const mobileGeometry = await page.locator("#alertsDialog").evaluate((dialog) => ({
|
||||||
width: dialog.getBoundingClientRect().width,
|
width: dialog.getBoundingClientRect().width,
|
||||||
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
|
documentOverflow: document.documentElement.scrollWidth - window.innerWidth,
|
||||||
|
|||||||
@@ -103,6 +103,35 @@ class MentorStreamTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(chunks, ["first", " second"])
|
self.assertEqual(chunks, ["first", " second"])
|
||||||
|
|
||||||
|
def test_follow_up_block_is_collected_without_leaking_into_answer(self):
|
||||||
|
lines = [
|
||||||
|
'data: {"choices":[{"delta":{"content":"先看承接,再等确认。\\n<XIAOBAI_FOL"}}]}\n'.encode(),
|
||||||
|
'data: {"choices":[{"delta":{"content":"LOW_UPS>\\n[\\"什么信号代表承接有效?\\",\\"这个判断何时失效?\\"]\\n</XIAOBAI_FOLLOW_UPS>"}}]}\n'.encode(),
|
||||||
|
b"data: [DONE]\n",
|
||||||
|
]
|
||||||
|
follow_ups: list[str] = []
|
||||||
|
with patch(
|
||||||
|
"backend.llm.transport.urllib.request.urlopen",
|
||||||
|
return_value=FakeStreamResponse(lines),
|
||||||
|
):
|
||||||
|
chunks = list(
|
||||||
|
stream_with_mentor(
|
||||||
|
self.skill,
|
||||||
|
{},
|
||||||
|
"question",
|
||||||
|
[],
|
||||||
|
"key",
|
||||||
|
"https://example.test/v1",
|
||||||
|
"model",
|
||||||
|
follow_ups=follow_ups,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual("".join(chunks), "先看承接,再等确认。\n")
|
||||||
|
self.assertEqual(
|
||||||
|
follow_ups,
|
||||||
|
["什么信号代表承接有效?", "这个判断何时失效?"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user