Add hub usage help page with settings nav toggle.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""中控「使用说明」:读取 manual_trading_hub/docs/help 下的 MD."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from lib.hub.hub_strategy_lib import render_markdown_html
|
||||
from lib.paths import REPO_ROOT
|
||||
|
||||
HELP_SECTIONS: tuple[dict[str, str], ...] = (
|
||||
{"key": "quickstart", "label": "快速开始", "title": "快速开始", "md_file": "01-quickstart.md"},
|
||||
{"key": "hub-nav", "label": "中控导航", "title": "中控导航说明", "md_file": "02-hub-nav.md"},
|
||||
{"key": "monitor", "label": "监控区", "title": "监控区与实例入口", "md_file": "03-monitor.md"},
|
||||
{"key": "instance", "label": "实例页面", "title": "实例页导航说明", "md_file": "04-instance.md"},
|
||||
{"key": "settings", "label": "设置说明", "title": "设置与配置说明", "md_file": "05-settings.md"},
|
||||
)
|
||||
|
||||
|
||||
def _help_dir() -> Path:
|
||||
return REPO_ROOT / "manual_trading_hub" / "docs" / "help"
|
||||
|
||||
|
||||
def _section_meta(key: str) -> dict[str, str]:
|
||||
k = (key or "").strip().lower()
|
||||
for item in HELP_SECTIONS:
|
||||
if item["key"] == k:
|
||||
return item
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def _md_path(section_key: str) -> Path:
|
||||
return _help_dir() / _section_meta(section_key)["md_file"]
|
||||
|
||||
|
||||
def help_meta_payload() -> dict[str, Any]:
|
||||
sections = [{"key": s["key"], "label": s["label"], "title": s["title"]} for s in HELP_SECTIONS]
|
||||
return {"ok": True, "sections": sections}
|
||||
|
||||
|
||||
def load_help_payload(section_key: str) -> dict[str, Any]:
|
||||
key = (section_key or "").strip().lower()
|
||||
meta = _section_meta(key)
|
||||
md_path = _md_path(key)
|
||||
md_text = md_path.read_text(encoding="utf-8") if md_path.is_file() else ""
|
||||
return {
|
||||
"ok": True,
|
||||
"section_key": key,
|
||||
"label": meta["label"],
|
||||
"title": meta["title"],
|
||||
"md_source": str(md_path.relative_to(REPO_ROOT)).replace("\\", "/"),
|
||||
"content_html": render_markdown_html(md_text),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# 快速开始
|
||||
|
||||
## 系统是什么
|
||||
|
||||
**中控**聚合三所(币安 / OKX / Gate)的持仓、委托、关键位与趋势计划,并提供资金曲线、行情、复盘与 AI 教练。**实际下单、关键位配置、策略执行、交易复盘**在各交易所**实例网页**完成。
|
||||
|
||||
```
|
||||
浏览器 → 中控(默认 :5100)
|
||||
├─ 监控区 / 资金 / 行情 / 内照明心 …
|
||||
└─ 点击「下单 / 期权」→ 实例页(内嵌或新标签)
|
||||
实例 Flask(币安 :5001 · Gate :5000 · OKX :5004)
|
||||
```
|
||||
|
||||
## 日常推荐流程
|
||||
|
||||
1. 打开 **监控区**,查看三所持仓、浮盈亏、关键位与趋势计划。
|
||||
2. 需要操作某所时,点该卡片 **「下单」**(或 **「期权」**)进入实例页。
|
||||
3. 复盘与统计:中控 **内照明心**、**数据看板**,或实例 **交易记录与复盘**。
|
||||
4. 事前计划:中控 **开仓计划**;策略 playbook:中控 **策略说明**(与实例「策略交易」不同,见下文)。
|
||||
|
||||
## 三账户默认对应
|
||||
|
||||
| 账户 | 实例端口 | 常见能力 |
|
||||
|------|----------|----------|
|
||||
| 币安 | 5001 | 关键位 + 趋势 |
|
||||
| OKX | 5004 | 关键位 + 趋势 + **期权**(需在设置勾选「监控期权」) |
|
||||
| Gate | 5000 | 关键位 + 趋势 |
|
||||
|
||||
## 两个容易混淆的名称
|
||||
|
||||
| 名称 | 在哪里 | 是什么 |
|
||||
|------|--------|--------|
|
||||
| **策略说明** | 中控顶栏 | 各所策略文档与开仓检查清单 |
|
||||
| **策略交易** | 实例顶栏 | 趋势回调 / 顺势加仓等自动化功能 |
|
||||
@@ -0,0 +1,22 @@
|
||||
# 中控导航说明
|
||||
|
||||
顶栏各页面用途如下(可在 **系统设置 → 显示与导航** 中隐藏不需要的 Tab)。
|
||||
|
||||
| 导航 | 用途 |
|
||||
|------|------|
|
||||
| **资金概况** | 总资金曲线、分户权益、回撤与 24h 变化 |
|
||||
| **开仓计划** | 事前写下计划、跟踪进行中、统计历史胜率 |
|
||||
| **监控区** | **核心操作台**:三所持仓卡片、全平/撤单、关键位与趋势计划摘要 |
|
||||
| **策略说明** | 三所策略 playbook + 开仓检查清单(非系统操作手册) |
|
||||
| **使用说明** | 本页:中控与实例怎么用 |
|
||||
| **行情区** | K 线、指标、画线;可从持仓跳转带币种 |
|
||||
| **计算器** | 趋势回调 / 滚仓张数与盈亏测算(手动填价) |
|
||||
| **内照明心** | 复盘语录、归档交易、永久 5m K 线 |
|
||||
| **数据看板** | 当日 KPI、分户卡片、SSE 刷新 |
|
||||
| **AI 教练** | 交易教练对话、监管推送(需配置密钥) |
|
||||
| **系统日志** | 中控与三实例 PM2 日志(排错用,非部署说明) |
|
||||
| **系统设置** | 中控密码、导航显示、交易所地址、监控能力勾选 |
|
||||
|
||||
## 默认首页
|
||||
|
||||
登录后默认进入 **监控区**(`/monitor`)。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 监控区与实例入口
|
||||
|
||||
## 监控卡片里有什么
|
||||
|
||||
每张交易所卡片通常包含:
|
||||
|
||||
- **资金行**:资金账户 / 交易账户 / 浮动盈亏(可在设置关闭)
|
||||
- **永续持仓**:表格摘要;点击标题栏 **进入全屏** 可看完整持仓卡片
|
||||
- **OKX 期权**(勾选「监控期权」后):永续与期权分块;全屏时期权也以卡片展示(与实例期权页字段一致,只读)
|
||||
- **关键位 / 下单监控 / 趋势回调 / 顺势加仓**:只读摘要(数据来自实例)
|
||||
|
||||
## 全屏模式
|
||||
|
||||
点击卡片标题栏(或移动端卡片主体)进入 **全屏**:
|
||||
|
||||
- 永续:每币种一张持仓卡,可 **委托 / 平仓**(非日内纪律模式)
|
||||
- 期权(OKX):只读卡片,含权利金、标记价、买盘深度等
|
||||
- 下方:关键位、下单监控、趋势与滚仓区块
|
||||
|
||||
按 `Esc` 或 **返回监控** 退出全屏。
|
||||
|
||||
## 打开实例(SSO)
|
||||
|
||||
监控卡片或全屏顶栏按钮:
|
||||
|
||||
| 按钮 | 进入实例页 | 说明 |
|
||||
|------|------------|------|
|
||||
| **打开实例** | 实盘下单 | 新浏览器标签 |
|
||||
| **下单** | 实盘下单 | 中控内 iframe |
|
||||
| **监控位** | 关键位监控 | |
|
||||
| **复盘** | 交易记录与复盘 | |
|
||||
| **期权** | 期权页 | 仅 OKX 且勾选监控期权 |
|
||||
|
||||
实例地址在 **系统设置 → 交易所** 配置 `flask_url`;未配置时不会出现上述按钮。
|
||||
|
||||
## 常见操作
|
||||
|
||||
| 操作 | 位置 |
|
||||
|------|------|
|
||||
| 紧急全平 | 卡片 **全平**(日内纪律账户可能禁用) |
|
||||
| 改止盈止损 | 持仓行 **委托** 或全屏卡片 |
|
||||
| 撤条件单 | 监控区条件单列表 |
|
||||
| 停止趋势计划 | 趋势回调区块 **停止 / 保本** 等 |
|
||||
@@ -0,0 +1,30 @@
|
||||
# 实例页导航说明
|
||||
|
||||
从监控区 **下单 / 打开实例** 进入后,实例顶栏常见 Tab 如下(部分可在实例 **系统设置 → 导航显示** 中隐藏)。
|
||||
|
||||
| Tab | 用途 |
|
||||
|-----|------|
|
||||
| **关键位监控** | 配置 5m 门禁关键位,可选自动下单 |
|
||||
| **实盘下单** | 人工下单、下单监控、预估盈亏比 |
|
||||
| **策略交易** | 趋势回调、顺势加仓计划(自动化) |
|
||||
| **策略交易记录** | 上述策略的执行历史 |
|
||||
| **交易记录与复盘** | 平仓记录、日记、AI 复盘 |
|
||||
| **统计分析** | 按周期汇总盈亏 |
|
||||
| **期权** | OKX 期权链、持仓、多档平仓(OKX 且已启用) |
|
||||
| **风控说明** | 只读展示当前风控相关 env |
|
||||
| **env配置** | 修改运行参数(中文标签) |
|
||||
| **系统设置** | 实例登录密码、导航 Tab 开关等 |
|
||||
|
||||
## 与中控的分工
|
||||
|
||||
| 在中控做 | 在实例做 |
|
||||
|----------|----------|
|
||||
| 看三所持仓汇总、全平 | 下单、改单、平仓 |
|
||||
| 看关键位 / 趋势摘要 | 新建 / 修改关键位与策略 |
|
||||
| 内照明心、数据看板 | 交易记录详情、日记 |
|
||||
| 开仓计划 | 策略交易执行 |
|
||||
| OKX 期权只读监控 | 期权开仓、多档平仓 |
|
||||
|
||||
## iframe 内操作
|
||||
|
||||
在中控 iframe 打开实例时,顶栏有 **返回监控 / 刷新 / 新标签打开**,无需重复登录。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 设置与配置说明
|
||||
|
||||
## 三层配置,不要混用
|
||||
|
||||
| 层级 | 入口 | 管什么 |
|
||||
|------|------|--------|
|
||||
| **中控系统设置** | 中控 `/settings` | 中控密码、顶栏显示、交易所 URL、监控能力(关键位/趋势/期权)、宏观日历、备份 |
|
||||
| **实例系统设置** | 实例 `/settings` | 实例登录密码、实例顶栏 Tab 显示 |
|
||||
| **实例 env配置** | 实例 `/env_config` | 交易参数(止损比例、风控开关等) |
|
||||
|
||||
改 env 后通常需 **重启对应实例 PM2** 才完全生效;具体字段含义见实例 env 页说明或仓库 `docs/env配置说明.md`(无需在中控内阅读)。
|
||||
|
||||
## 中控 · 显示与导航
|
||||
|
||||
可隐藏不常用的顶栏 Tab(**监控区**、**系统设置** 无法隐藏)。
|
||||
|
||||
## 中控 · 交易所
|
||||
|
||||
每项需配置:
|
||||
|
||||
- **flask_url**:实例 HTTP 地址(中控聚合与打开实例用)
|
||||
- **agent_url**:子代理地址(持仓与全平)
|
||||
- **capabilities**:勾选 **关键位 / 趋势 / 期权** 决定监控区展示哪些块
|
||||
|
||||
## 实例 · 导航显示
|
||||
|
||||
固定保留:**关键位监控、实盘下单、系统设置**。其余 Tab 可按需开关。
|
||||
|
||||
## 使用向常见问题
|
||||
|
||||
**监控卡片没有「下单」按钮**
|
||||
→ 检查该所 `flask_url` 是否填写且实例可访问。
|
||||
|
||||
**OKX 看不到期权**
|
||||
→ 中控设置勾选「监控期权」,且实例已启用期权模块。
|
||||
|
||||
**策略说明 vs 策略交易**
|
||||
→ 前者在中控,是文档;后者在实例,是自动化功能。
|
||||
@@ -89,6 +89,7 @@ from lib.hub.hub_entry_plan_lib import (
|
||||
meta_payload as entry_plan_meta_payload,
|
||||
update_entry_plan,
|
||||
)
|
||||
from lib.hub.hub_help_lib import help_meta_payload, load_help_payload
|
||||
from lib.hub.hub_strategy_lib import (
|
||||
build_export_html,
|
||||
build_print_html,
|
||||
@@ -983,6 +984,7 @@ def root_redirect():
|
||||
@app.get("/funds")
|
||||
@app.get("/ai")
|
||||
@app.get("/strategy")
|
||||
@app.get("/help")
|
||||
@app.get("/logs")
|
||||
@app.get("/settings")
|
||||
def shell_pages():
|
||||
@@ -1094,6 +1096,7 @@ class SettingsDisplayBody(BaseModel):
|
||||
show_nav_ai: bool = True
|
||||
show_nav_calculator: bool = True
|
||||
show_nav_strategy: bool = True
|
||||
show_nav_help: bool = True
|
||||
show_nav_logs: bool = True
|
||||
|
||||
|
||||
@@ -3275,6 +3278,19 @@ def api_strategy_meta():
|
||||
return strategy_meta_payload()
|
||||
|
||||
|
||||
@app.get("/api/help/meta")
|
||||
def api_help_meta():
|
||||
return help_meta_payload()
|
||||
|
||||
|
||||
@app.get("/api/help/{section_key}")
|
||||
def api_help_section(section_key: str):
|
||||
try:
|
||||
return load_help_payload(section_key.strip().lower())
|
||||
except KeyError:
|
||||
return JSONResponse({"ok": False, "msg": "unknown section"}, status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/system-logs/meta")
|
||||
def api_system_logs_meta():
|
||||
return system_logs_meta()
|
||||
|
||||
@@ -29,6 +29,7 @@ DEFAULT_DISPLAY = {
|
||||
"show_nav_ai": True,
|
||||
"show_nav_calculator": True,
|
||||
"show_nav_strategy": True,
|
||||
"show_nav_help": True,
|
||||
"show_nav_logs": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -8652,6 +8652,82 @@ body.funds-fullscreen-open {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 使用说明 ── */
|
||||
.help-toolbar {
|
||||
margin-bottom: 12px;
|
||||
min-height: 1.2em;
|
||||
}
|
||||
.help-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
.help-toc-card {
|
||||
padding: 16px 14px 18px;
|
||||
position: sticky;
|
||||
top: 72px;
|
||||
}
|
||||
.help-toc-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-soft);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.help-toc-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.help-toc-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.88rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.help-toc-item:hover {
|
||||
background: var(--surface-2, rgba(255, 255, 255, 0.04));
|
||||
color: var(--text);
|
||||
}
|
||||
.help-toc-item.is-active {
|
||||
background: var(--accent-soft, rgba(59, 130, 246, 0.12));
|
||||
color: var(--accent, #3b82f6);
|
||||
font-weight: 600;
|
||||
}
|
||||
.help-doc-card {
|
||||
padding: 18px 20px 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.help-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.help-toc-card {
|
||||
position: static;
|
||||
}
|
||||
.help-toc-nav {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.help-toc-item {
|
||||
width: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.help-doc-card .strategy-doc-body {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 监控页:永续 / 期权分块 ── */
|
||||
.hub-monitor-block {
|
||||
margin-bottom: 4px;
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
return displayPref("show_nav_strategy", true);
|
||||
}
|
||||
|
||||
function showNavHelpPref() {
|
||||
return displayPref("show_nav_help", true);
|
||||
}
|
||||
|
||||
function showNavLogsPref() {
|
||||
return displayPref("show_nav_logs", true);
|
||||
}
|
||||
@@ -54,6 +58,7 @@
|
||||
const navAi = document.getElementById("nav-ai");
|
||||
const navCalc = document.getElementById("nav-calculator");
|
||||
const navStrategy = document.getElementById("nav-strategy");
|
||||
const navHelp = document.getElementById("nav-help");
|
||||
const navLogs = document.getElementById("nav-logs");
|
||||
if (navFunds) navFunds.classList.toggle("nav-hidden", d.show_nav_funds === false);
|
||||
if (navDash) navDash.classList.toggle("nav-hidden", d.show_nav_dashboard === false);
|
||||
@@ -62,6 +67,7 @@
|
||||
if (navAi) navAi.classList.toggle("nav-hidden", d.show_nav_ai === false);
|
||||
if (navCalc) navCalc.classList.toggle("nav-hidden", d.show_nav_calculator === false);
|
||||
if (navStrategy) navStrategy.classList.toggle("nav-hidden", d.show_nav_strategy === false);
|
||||
if (navHelp) navHelp.classList.toggle("nav-hidden", d.show_nav_help === false);
|
||||
if (navLogs) navLogs.classList.toggle("nav-hidden", d.show_nav_logs === false);
|
||||
}
|
||||
|
||||
@@ -73,6 +79,7 @@
|
||||
if (page === "ai") return showNavAiPref();
|
||||
if (page === "calculator") return showNavCalculatorPref();
|
||||
if (page === "strategy") return showNavStrategyPref();
|
||||
if (page === "help") return showNavHelpPref();
|
||||
if (page === "logs") return showNavLogsPref();
|
||||
return true;
|
||||
}
|
||||
@@ -87,6 +94,7 @@
|
||||
const aiCb = document.getElementById("pref-show-nav-ai");
|
||||
const calcCb = document.getElementById("pref-show-nav-calculator");
|
||||
const strategyCb = document.getElementById("pref-show-nav-strategy");
|
||||
const helpCb = document.getElementById("pref-show-nav-help");
|
||||
const logsCb = document.getElementById("pref-show-nav-logs");
|
||||
if (pnlCb) pnlCb.checked = d.show_account_pnl !== false;
|
||||
if (fundsCb) fundsCb.checked = d.show_nav_funds !== false;
|
||||
@@ -96,6 +104,7 @@
|
||||
if (aiCb) aiCb.checked = d.show_nav_ai !== false;
|
||||
if (calcCb) calcCb.checked = d.show_nav_calculator !== false;
|
||||
if (strategyCb) strategyCb.checked = d.show_nav_strategy !== false;
|
||||
if (helpCb) helpCb.checked = d.show_nav_help !== false;
|
||||
if (logsCb) logsCb.checked = d.show_nav_logs !== false;
|
||||
syncNavVisibility(data);
|
||||
}
|
||||
@@ -1208,6 +1217,7 @@
|
||||
if (p.includes("funds")) return "funds";
|
||||
if (p.includes("plan")) return "plan";
|
||||
if (p.includes("calculator")) return "calculator";
|
||||
if (p.includes("help")) return "help";
|
||||
if (p.includes("strategy")) return "strategy";
|
||||
if (p.includes("logs")) return "logs";
|
||||
if (p.includes("market")) return "market";
|
||||
@@ -1222,6 +1232,7 @@
|
||||
if (page === "funds") return "page-funds";
|
||||
if (page === "plan") return "page-plan";
|
||||
if (page === "calculator") return "page-calculator";
|
||||
if (page === "help") return "page-help";
|
||||
if (page === "strategy") return "page-strategy";
|
||||
if (page === "logs") return "page-logs";
|
||||
if (page === "market") return "page-market";
|
||||
@@ -1283,6 +1294,11 @@
|
||||
} else if (window.hubStrategyPage && window.hubStrategyPage.destroy) {
|
||||
window.hubStrategyPage.destroy();
|
||||
}
|
||||
if (page === "help" && window.hubHelpPage) {
|
||||
window.hubHelpPage.init();
|
||||
} else if (window.hubHelpPage && window.hubHelpPage.destroy) {
|
||||
window.hubHelpPage.destroy();
|
||||
}
|
||||
if (page === "logs" && window.hubLogsPage) {
|
||||
window.hubLogsPage.init();
|
||||
} else if (window.hubLogsPage && window.hubLogsPage.destroy) {
|
||||
@@ -4706,6 +4722,7 @@
|
||||
const aiCb = document.getElementById("pref-show-nav-ai");
|
||||
const calcCb = document.getElementById("pref-show-nav-calculator");
|
||||
const strategyCb = document.getElementById("pref-show-nav-strategy");
|
||||
const helpCb = document.getElementById("pref-show-nav-help");
|
||||
const logsCb = document.getElementById("pref-show-nav-logs");
|
||||
const supEnabled = document.getElementById("supervisor-enabled");
|
||||
const supProg = document.getElementById("supervisor-wechat-program");
|
||||
@@ -4727,6 +4744,7 @@
|
||||
show_nav_ai: aiCb ? !!aiCb.checked : true,
|
||||
show_nav_calculator: calcCb ? !!calcCb.checked : true,
|
||||
show_nav_strategy: strategyCb ? !!strategyCb.checked : true,
|
||||
show_nav_help: helpCb ? !!helpCb.checked : true,
|
||||
show_nav_logs: logsCb ? !!logsCb.checked : true,
|
||||
},
|
||||
supervisor: {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 使用说明:中控 docs/help MD 章节.
|
||||
*/
|
||||
(function () {
|
||||
const page = document.getElementById("page-help");
|
||||
if (!page) return;
|
||||
|
||||
const tocEl = document.getElementById("help-toc-nav");
|
||||
const statusEl = document.getElementById("help-load-status");
|
||||
const docBody = document.getElementById("help-doc-body");
|
||||
const docSource = document.getElementById("help-doc-source");
|
||||
|
||||
let sectionsMeta = [];
|
||||
let activeKey = "quickstart";
|
||||
let cache = {};
|
||||
let bound = false;
|
||||
|
||||
async function apiFetch(url) {
|
||||
const r = await fetch(url, { credentials: "same-origin" });
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.ok) throw new Error((data && data.msg) || r.statusText || "请求失败");
|
||||
return data;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function sectionFromHash() {
|
||||
const h = (window.location.hash || "").replace(/^#/, "").trim().toLowerCase();
|
||||
if (!h) return null;
|
||||
return sectionsMeta.some((s) => s.key === h) ? h : null;
|
||||
}
|
||||
|
||||
function setHash(key) {
|
||||
const next = `#${key}`;
|
||||
if (window.location.hash !== next) {
|
||||
history.replaceState(null, "", `/help${next}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderToc() {
|
||||
if (!tocEl) return;
|
||||
tocEl.innerHTML = sectionsMeta
|
||||
.map(
|
||||
(s) =>
|
||||
`<button type="button" class="help-toc-item${s.key === activeKey ? " is-active" : ""}" data-key="${esc(s.key)}">${esc(s.label)}</button>`
|
||||
)
|
||||
.join("");
|
||||
tocEl.querySelectorAll(".help-toc-item").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const key = btn.getAttribute("data-key");
|
||||
if (!key || key === activeKey) return;
|
||||
activeKey = key;
|
||||
setHash(key);
|
||||
renderToc();
|
||||
void loadSection(key);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderSection(data) {
|
||||
if (docBody) docBody.innerHTML = data.content_html || "";
|
||||
if (docSource) {
|
||||
docSource.textContent = data.md_source ? `来源: ${data.md_source}` : "";
|
||||
}
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
}
|
||||
|
||||
async function loadSection(key) {
|
||||
if (cache[key]) {
|
||||
renderSection(cache[key]);
|
||||
return;
|
||||
}
|
||||
if (statusEl) statusEl.textContent = "加载中…";
|
||||
try {
|
||||
const data = await apiFetch(`/api/help/${encodeURIComponent(key)}`);
|
||||
cache[key] = data;
|
||||
renderSection(data);
|
||||
} catch (err) {
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
if (docBody) docBody.innerHTML = `<p class="muted">${esc(err.message || "加载失败")}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMeta() {
|
||||
const data = await apiFetch("/api/help/meta");
|
||||
sectionsMeta = data.sections || [];
|
||||
const fromHash = sectionFromHash();
|
||||
if (fromHash) activeKey = fromHash;
|
||||
else if (sectionsMeta.length && !sectionsMeta.some((s) => s.key === activeKey)) {
|
||||
activeKey = sectionsMeta[0].key;
|
||||
}
|
||||
renderToc();
|
||||
await loadSection(activeKey);
|
||||
if (!sectionFromHash() && activeKey) setHash(activeKey);
|
||||
}
|
||||
|
||||
function bindOnce() {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
window.addEventListener("hashchange", () => {
|
||||
const key = sectionFromHash();
|
||||
if (!key || key === activeKey) return;
|
||||
activeKey = key;
|
||||
renderToc();
|
||||
void loadSection(key);
|
||||
});
|
||||
}
|
||||
|
||||
window.hubHelpPage = {
|
||||
init() {
|
||||
bindOnce();
|
||||
void loadMeta().catch((err) => {
|
||||
if (statusEl) statusEl.textContent = "";
|
||||
if (docBody) docBody.innerHTML = `<p class="muted">${esc(err.message || "加载失败")}</p>`;
|
||||
});
|
||||
},
|
||||
destroy() {},
|
||||
};
|
||||
})();
|
||||
@@ -52,6 +52,7 @@
|
||||
<a href="/plan" id="nav-plan">开仓计划</a>
|
||||
<a href="/monitor" id="nav-monitor">监控区</a>
|
||||
<a href="/strategy" id="nav-strategy">策略说明</a>
|
||||
<a href="/help" id="nav-help">使用说明</a>
|
||||
<a href="/market" id="nav-market">行情区</a>
|
||||
<a href="/calculator" id="nav-calculator">计算器</a>
|
||||
<a href="/archive" id="nav-archive">内照明心</a>
|
||||
@@ -890,6 +891,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-help" class="page hidden">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1><span class="head-tag">HLP</span> 使用说明</h1>
|
||||
<p class="page-desc">中控与实例 · 日常操作指南(不含部署说明)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-toolbar">
|
||||
<span id="help-load-status" class="toolbar-meta"></span>
|
||||
</div>
|
||||
<div class="help-layout">
|
||||
<aside class="card help-toc-card">
|
||||
<h3 class="help-toc-title">目录</h3>
|
||||
<nav id="help-toc-nav" class="help-toc-nav" aria-label="使用说明目录"></nav>
|
||||
</aside>
|
||||
<section class="card help-doc-card">
|
||||
<div id="help-doc-body" class="strategy-doc-body prose"></div>
|
||||
<p id="help-doc-source" class="strategy-doc-source"></p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-logs" class="page hidden">
|
||||
<div class="page-head hub-logs-page-head">
|
||||
<div>
|
||||
@@ -1010,6 +1033,10 @@
|
||||
<input type="checkbox" id="pref-show-nav-strategy" checked />
|
||||
顶栏显示「策略说明」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-nav-help" checked />
|
||||
顶栏显示「使用说明」
|
||||
</label>
|
||||
<label class="chk-label settings-display-chk">
|
||||
<input type="checkbox" id="pref-show-nav-logs" checked />
|
||||
顶栏显示「系统日志」
|
||||
@@ -1233,6 +1260,7 @@
|
||||
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
||||
<script src="/assets/dashboard.js?v=20260708-options-expiry-cd"></script>
|
||||
<script src="/assets/strategy.js?v=3"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
<script src="/assets/logs.js?v=1"></script>
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||
|
||||
Reference in New Issue
Block a user