- Introduced `build_all.py` script to automate the assembly of course modules into a single `index.html` file. - Created `index.html` for the main course overview, featuring a structured layout and navigation for various modules. - Developed `_base.html` and `_footer.html` templates for the advisor module, ensuring consistent styling and structure. - Added `build.sh` script for individual module assembly, enhancing modularity and ease of updates. - Implemented multiple module HTML files detailing specific training scenarios and functionalities for advisors, including interactive elements and quizzes. This update significantly enhances the course delivery framework, providing a comprehensive and interactive learning experience for advisors.
355 lines
20 KiB
HTML
355 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>前端 web 深潜 · 交互导览</title>
|
||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400;1,9..40,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
|
||
<link rel="stylesheet" href="styles.css">
|
||
|
||
<style>
|
||
:root {
|
||
--color-accent: #132B3A;
|
||
--color-accent-hover: #0f2230;
|
||
--color-accent-light: #E8EEF1;
|
||
--color-accent-muted: #5a6b75;
|
||
}
|
||
</style>
|
||
|
||
<script src="main.js" defer></script>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="nav" id="nav">
|
||
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||
<div class="nav-inner">
|
||
<span class="nav-title">前端 web 深潜</span>
|
||
<div class="nav-dots" id="nav-dots" role="tablist">
|
||
<button class="nav-dot" data-target="module-1" data-tooltip="四角色路由" role="tab" aria-label="模块 1: 四角色路由"></button>
|
||
<button class="nav-dot" data-target="module-2" data-tooltip="Chat 鉴权" role="tab" aria-label="模块 2: Chat 鉴权"></button>
|
||
<button class="nav-dot" data-target="module-3" data-tooltip="本地调试" role="tab" aria-label="模块 3: 本地调试"></button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main id="main">
|
||
<section class="module" id="module-1">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 1 · 路由与登录</p>
|
||
<h1 class="module-title animate-in">四角色路由、HashRouter<br>与 Demo 账号</h1>
|
||
<p class="module-lead animate-in">
|
||
前端在 <code>web/</code>,用 React +
|
||
<span class="term" data-definition="HashRouter = URL 里 # 后面的路径由浏览器本地切换,不请求服务器新页面,适合静态部署。">HashRouter</span>
|
||
做四角色工作台。登录后按角色跳进不同「家」——改路由时先对 <code>App.tsx</code> 和 <code>demoAccounts.ts</code>。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>四角色默认入口</h2>
|
||
<div class="badge-list">
|
||
<div class="badge-item"><span class="badge-code">CUST-9527</span><span class="badge-desc">客户 → /app/customer/home</span></div>
|
||
<div class="badge-item"><span class="badge-code">STAFF-10086</span><span class="badge-desc">理财师 → /app/advisor/home</span></div>
|
||
<div class="badge-item"><span class="badge-code">STAFF-20001</span><span class="badge-desc">分析员 → /app/analyst/home</span></div>
|
||
<div class="badge-item"><span class="badge-code">STAFF-30001</span><span class="badge-desc">风控 → /app/risk/home</span></div>
|
||
</div>
|
||
<p>登录页一键 Demo:<code>LoginPage</code> 调 <code>POST /api/auth/login</code> 拿 JWT,<code>saveAuth</code> 写入 localStorage,再 <code>navigate(defaultRoute)</code>。</p>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>路由树(<code>App.tsx</code>)</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>customer/*</h3>
|
||
<p>home、profile、holdings、trades、chat。持仓/流水走平台 API,对话走 ChatPanel。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>advisor/*</h3>
|
||
<p>home、customers、chat。顾问助手 SSE,可指定 customer_id 查名下客户。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>analyst/*</h3>
|
||
<p>home、analytics/query(问数)、analytics/assets、analytics/chat。问数走 /api/analyst/*。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>risk/*</h3>
|
||
<p>home、alerts、simulate、chat。台账 REST + 风控对话,JWT 须带 risk 角色。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>HashRouter 为什么?</strong> 构建产物是纯静态文件,<code>#/app/customer/home</code> 不依赖服务端路由重写,双击 <code>dist/index.html</code> 也能跑(API 仍要 proxy 或同域)。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">main.tsx + App.tsx</span>
|
||
<pre><code><span class="code-line"><span class="code-comment">// main.tsx</span></span>
|
||
<span class="code-line"><HashRouter></span>
|
||
<span class="code-line"> <AppRoutes /></span>
|
||
<span class="code-line"></HashRouter></span>
|
||
<span class="code-line"></span>
|
||
<span class="code-line"><span class="code-comment">// demoAccounts.ts</span></span>
|
||
<span class="code-line">defaultRoute: <span class="code-string">'#/app/customer/home'</span></span>
|
||
<span class="code-line"></span>
|
||
<span class="code-line"><span class="code-comment">// App.tsx · RequireAuth</span></span>
|
||
<span class="code-line"><span class="code-keyword">if</span> (!auth) <span class="code-keyword">return</span> <Navigate to=<span class="code-string">"/login"</span> /></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">整站包在 HashRouter 里,路径变化不刷新整页。</p>
|
||
<p class="tl">每个 Demo 账号绑好「登录后跳哪」——带 # 前缀,和 Hash 路由一致。</p>
|
||
<p class="tl">/app 下子路由都要过 RequireAuth:没 token 踢回登录页。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m1">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="STAFF-20001 登录后 defaultRoute 是 #/app/analyst/home,见 demoAccounts.ts。"
|
||
data-explanation-wrong="四角色各有独立 home,分析员不会进 customer 或 risk 路径。">
|
||
<h3 class="quiz-question">分析员 Demo 登录成功,默认打开哪条路径?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>/app/customer/home</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>/app/analyst/home</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>/app/risk/alerts</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m1')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-m1')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-2">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 2 · 对话与鉴权</p>
|
||
<h1 class="module-title animate-in">ChatPanel / useChatPanel:<br>三条 API 线的头不一样</h1>
|
||
<p class="module-lead animate-in">
|
||
对话 UI 统一用 <code>ChatPanel</code> + <code>useChatPanel</code>,但后端入口分三条:
|
||
<code>/api/chat</code>(四 Agent)、<code>/api/analyst/*</code>(问数平台)、风控台账 REST。
|
||
指挥 AI 接新页面时,<strong>先对表再写 headers</strong>。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>鉴权差异对照</h2>
|
||
<table style="width:100%; border-collapse:collapse; margin: 1.5rem 0; font-size: 0.95rem;">
|
||
<thead><tr><th>前端模块</th><th>API</th><th>Authorization</th><th>X-Agent-Type</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>api/chat.ts</code></td><td>/api/chat, /stream, sessions</td><td>Bearer</td><td><strong>必须</strong>(与 prop agentType 一致)</td></tr>
|
||
<tr><td><code>api/analyst.ts</code></td><td>/api/analyst/chat 等</td><td>Bearer(apiFetch)</td><td><strong>不要</strong></td></tr>
|
||
<tr><td><code>api/risk.ts</code></td><td>/api/risk/*</td><td>Bearer</td><td><strong>必须 risk</strong></td></tr>
|
||
<tr><td>持仓/产品页</td><td>/api/customers/* 等</td><td>Bearer</td><td>不要</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="callout callout-warning">
|
||
<strong>真实 bug:</strong> <code>risk.ts</code> 曾漏 <code>X-Agent-Type: risk</code>,JWT 正确也 401。接新风控页先 grep 这个头。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>数据流:理财师打开顾问助手</h2>
|
||
<div class="flow-animation" data-steps='[
|
||
{"highlight":"flow-actor-1","label":"ChatPanel(agentType=advisor, mode=stream)"},
|
||
{"highlight":"flow-actor-2","label":"useChatPanel → listChatSessions(token, advisor)","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-3","label":"chat.ts:Bearer + X-Agent-Type: advisor","packet":true,"from":"2","to":"3"},
|
||
{"highlight":"flow-actor-4","label":"chat.py:get_auth_context + 准入矩阵","packet":true,"from":"3","to":"4"},
|
||
{"highlight":"flow-actor-1","label":"SSE 流式 delta → UI 逐字显示","packet":true,"from":"4","to":"1"}
|
||
]'>
|
||
<div class="flow-actors">
|
||
<div class="flow-actor" id="flow-actor-1"><span class="flow-actor-icon">💬</span><span>ChatPanel</span></div>
|
||
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🪝</span><span>useChatPanel</span></div>
|
||
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">📡</span><span>api/chat.ts</span></div>
|
||
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🚪</span><span>chat.py</span></div>
|
||
</div>
|
||
<p class="flow-step-label">对比:问数走 analyst.ts,无 X-Agent-Type</p>
|
||
<div class="flow-controls">
|
||
<button class="btn flow-next-btn">下一步</button>
|
||
<button class="btn flow-reset-btn">重来</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">chat.ts vs analyst.ts</span>
|
||
<pre><code><span class="code-line"><span class="code-comment">// chat.ts — 对话线</span></span>
|
||
<span class="code-line"><span class="code-keyword">function</span> <span class="code-function">chatHeaders</span>(token, agentType) {</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> {</span>
|
||
<span class="code-line"> Authorization: <span class="code-string">`Bearer ${token}`</span>,</span>
|
||
<span class="code-line"> <span class="code-string">'X-Agent-Type'</span>: agentType,</span>
|
||
<span class="code-line"> }</span>
|
||
<span class="code-line">}</span>
|
||
<span class="code-line"></span>
|
||
<span class="code-line"><span class="code-comment">// analyst.ts — 问数平台线</span></span>
|
||
<span class="code-line">apiFetch(<span class="code-string">'/api/analyst/chat'</span>, { method: <span class="code-string">'POST'</span>, token, body })</span>
|
||
<span class="code-line"><span class="code-comment">// apiFetch 只加 Authorization,不加 X-Agent-Type</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">Chat 相关请求:token 证明身份,agentType 证明走哪条 Agent 线。</p>
|
||
<p class="tl">useChatPanel 把 agentType 从页面 prop 一路传到每次 list/send/close。</p>
|
||
<p class="tl">问数用 apiFetch 通用封装,后端走 get_platform_auth_context,不需要 Agent 头。</p>
|
||
<p class="tl">customer 线 mode 常用 sync(后端 LangGraph 整图跑完再推);advisor/risk 用 stream SSE。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m2">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-c"
|
||
data-explanation-right="风控对话页 ChatPanel agentType=risk,chat.ts 自动带 X-Agent-Type: risk。"
|
||
data-explanation-wrong="风控 Chat 走 /api/chat,不是 /api/risk;但 REST 台账才用 risk.ts。">
|
||
<h3 class="quiz-question">RiskChatPage 发消息,请求头应长什么样?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>只要 Bearer,和问数一样</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>Bearer + X-Agent-Type: analyst</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>Bearer + X-Agent-Type: risk</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m2')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-m2')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-3">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 3 · 本地调试</p>
|
||
<h1 class="module-title animate-in">npm run dev、Vite proxy<br>与 401 排障</h1>
|
||
<p class="module-lead animate-in">
|
||
前端 dev 服务器跑在 <code>5173</code>,API 在 <code>8000</code>。
|
||
<span class="term" data-definition="Vite proxy = 开发时把 /api 请求转发到后端,浏览器以为 API 和页面同域,避免跨域 CORS 麻烦。">Vite proxy</span>
|
||
帮你转发——没起后端或头带错,Network 面板一眼 401。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>启动顺序</h2>
|
||
<div class="numbered-steps">
|
||
<div class="step-card">
|
||
<span class="step-num">1</span>
|
||
<div><strong>后端</strong>:<code>uvicorn app.main:app --reload --port 8000</code>(Core 模拟库 + Redis 6380 按需)</div>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">2</span>
|
||
<div><strong>前端</strong>:<code>cd web && npm run dev</code> → 打开 <code>http://localhost:5173</code></div>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">3</span>
|
||
<div><strong>登录</strong>:点 Demo 账号 → 看 localStorage 里 token → 再点各角色菜单</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">vite.config.ts</span>
|
||
<pre><code><span class="code-line">server: {</span>
|
||
<span class="code-line"> port: <span class="code-number">5173</span>,</span>
|
||
<span class="code-line"> proxy: {</span>
|
||
<span class="code-line"> <span class="code-string">'/api'</span>: {</span>
|
||
<span class="code-line"> target: <span class="code-string">'http://127.0.0.1:8000'</span>,</span>
|
||
<span class="code-line"> changeOrigin: <span class="code-keyword">true</span>,</span>
|
||
<span class="code-line"> },</span>
|
||
<span class="code-line"> },</span>
|
||
<span class="code-line">},</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">浏览器请求 localhost:5173/api/... 时,Vite 偷偷转给 8000 端口。</p>
|
||
<p class="tl">前端代码里写相对路径 /api 即可,不用硬编码 :8000。</p>
|
||
<p class="tl">生产构建没有 proxy,要 Nginx 或同域部署反代。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>常见 401 速查</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>AUTH_401_MISSING_BEARER</h3>
|
||
<p>没登录或 token 过期 → 重新点 Demo 登录;改过后端角色也要重登拿新 JWT。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>AUTH_401_MISSING_AGENT_TYPE</h3>
|
||
<p>走了 /api/chat 但没带 X-Agent-Type → 查 ChatPanel 的 agentType 和 chat.ts headers。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>AUTH_403_AGENT_MISMATCH</h3>
|
||
<p>JWT 角色和 X-Agent-Type 不配,如客户 token 却带 advisor → 换账号或改 prop。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>连不上 API</h3>
|
||
<p>8000 没起或 proxy 失效 → curl http://127.0.0.1:8000/health 先确认后端活着。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m3">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="持仓页走平台 API,只需 Bearer;缺的是 X-Agent-Type 才会在 chat 线 401。"
|
||
data-explanation-wrong="平台读接口不要 X-Agent-Type;401 missing bearer 才是没 token。">
|
||
<h3 class="quiz-question">客户持仓页 401,响应 AUTH_401_MISSING_AGENT_TYPE,最可能原因是?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>平台 API 忘了带 X-Agent-Type(正常不该带)</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>请求误打到 /api/chat 或混用了 chat 封装</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>vite proxy 端口写错</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<div class="quiz-question-block"
|
||
data-correct="option-a"
|
||
data-explanation-right="对。/api → 127.0.0.1:8000,前端 dev 标准配置。"
|
||
data-explanation-wrong="proxy target 是 8000 不是 5173;5173 是 Vite 自己。">
|
||
<h3 class="quiz-question">npm run dev 时,/api/chat 实际打到哪?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>http://127.0.0.1:8000/api/chat</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>http://127.0.0.1:5173/api/chat(Vite 自己处理)</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m3')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-m3')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|