- 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.
375 lines
22 KiB
HTML
375 lines
22 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>代销平台 API 深潜 · 交互导览</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: #E06B56;
|
||
--color-accent-hover: #C85A48;
|
||
--color-accent-light: #FCEEEA;
|
||
--color-accent-muted: #E89A8A;
|
||
}
|
||
</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">代销平台 API 深潜</span>
|
||
<div class="nav-dots" id="nav-dots" role="tablist">
|
||
<button class="nav-dot" data-target="module-1" data-tooltip="v0.1 路由" role="tab" aria-label="模块 1: v0.1 路由"></button>
|
||
<button class="nav-dot" data-target="module-2" data-tooltip="平台鉴权" role="tab" aria-label="模块 2: 平台鉴权"></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">v0.1 路由:<br>customers / products / advisors / compliance</h1>
|
||
<p class="module-lead animate-in">
|
||
代销平台 API 是「读 Core 模拟库」的
|
||
<span class="term" data-definition="REST = 用固定 URL + HTTP 方法(GET/POST)读写数据的约定,浏览器和 App 都按这个格式发请求。">REST</span>
|
||
门面。路由按<strong>业务域</strong>切分,不用 <code>/api/platform/*</code> 前缀。
|
||
和 Agent 对话线(<code>/api/chat</code>)是两条平行轨道——指挥 AI 加「查持仓」时,先确认走哪条。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>四块 canonical 域(以本文为准)</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>/api/customers/*</h3>
|
||
<p>客户 L0 档案、持仓、流水。<code>customers.py</code> → <code>platform/customer_service</code> → <code>core_ro</code>。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>/api/products/*</h3>
|
||
<p>产品列表、净值曲线。理财师/客户看产品详情都走这里,不另开 Agent Tool HTTP。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>/api/advisors/*</h3>
|
||
<p>理财师名下客户归属。advisor 只能查本人 roster,风控/分析员可全量只读。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>/api/compliance/*</h3>
|
||
<p>适当性判定等合规读接口。<strong>canonical</strong>:Agent 合并期改调同一 Service,不保留第二套路径。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>重复能力以谁为准?</strong> 以本平台 API 为准。Agent 侧已有同能力 Tool 时,合并期改调 <code>app/service/platform/</code>,旧路径仅过渡。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>三层分工(别让 AI 在路由里写 SQL)</h2>
|
||
<div class="flow-animation" data-steps='[
|
||
{"highlight":"flow-actor-1","label":"HTTP 请求进入 customers.py"},
|
||
{"highlight":"flow-actor-2","label":"get_platform_auth_context:验 JWT + 归属","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-3","label":"platform_service:组装、分页、脱敏开关","packet":true,"from":"2","to":"3"},
|
||
{"highlight":"flow-actor-4","label":"core_ro:SELECT 只读,不感知 HTTP","packet":true,"from":"3","to":"4"},
|
||
{"highlight":"flow-actor-1","label":"ok() 统一外壳返回 JSON","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>api/*.py</span></div>
|
||
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🔑</span><span>deps 鉴权</span></div>
|
||
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">📦</span><span>platform Service</span></div>
|
||
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🗄</span><span>core_ro</span></div>
|
||
</div>
|
||
<p class="flow-step-label">点击「下一步」看一层层往下走</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">薄路由 · customers.py</span>
|
||
<pre><code><span class="code-line"><span class="code-comment">"""代销平台 · 客户 L0 与资产读 API(canonical · 不要求 X-Agent-Type)。"""</span></span>
|
||
<span class="code-line">@router.get(<span class="code-string">"/{customer_id}/holdings"</span>)</span>
|
||
<span class="code-line"><span class="code-keyword">def</span> <span class="code-function">list_holdings</span>(</span>
|
||
<span class="code-line"> customer_id: str,</span>
|
||
<span class="code-line"> auth: AuthContext = Depends(get_platform_auth_context),</span>
|
||
<span class="code-line">):</span>
|
||
<span class="code-line"> assert_platform_customer_access(auth, customer_id, ...)</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> ok(platform_service.list_holdings(customer_id))</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">文件头就写明:这是平台 canonical 读接口,不要 X-Agent-Type。</p>
|
||
<p class="tl">URL 用复数 customers + 嵌套 holdings,参数名和域 ID 一致。</p>
|
||
<p class="tl">鉴权用平台专用函数,不是 chat 那条 get_auth_context。</p>
|
||
<p class="tl">先断言「你有没有权看这个 customer_id」,再调 Service,路由里不出现 SQL。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-platform-m1">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。v0.1 拍板:重复能力以本平台 API + platform Service 为准,Agent Tool 合并时改调同一层。"
|
||
data-explanation-wrong="不要在 Agent 里再抄一套查持仓 HTTP;会两套口径、联调必炸。">
|
||
<h3 class="quiz-question">理财师 Agent 也要查客户持仓,应该新建 /api/agent/holdings 吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>要,Agent 专用路径更清晰</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不要,改调 platform Service 或现有 /api/customers 路径</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>直接在 Agent 里写 SQL 更快</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-platform-m1')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-platform-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">get_platform_auth_context:<br>只要 JWT,不要 X-Agent-Type</h1>
|
||
<p class="module-lead animate-in">
|
||
对话线(<code>/api/chat</code>)必须带
|
||
<span class="term" data-definition="HTTP 请求头里的一行,声明这次走 customer/advisor/risk/analyst 哪条 Agent 业务线。">X-Agent-Type</span>;
|
||
平台读 API 故意<strong>不要</strong>——App 和仪表盘只证明「你是谁」,不声明「你在敲哪扇 Agent 窗」。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>和 chat 鉴权的对照表</h2>
|
||
<table style="width:100%; border-collapse:collapse; margin: 1.5rem 0; font-size: 0.95rem;">
|
||
<thead><tr><th>场景</th><th>函数</th><th>X-Agent-Type</th><th>归属断言</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>平台 REST</td><td><code>get_platform_auth_context</code></td><td><strong>不要</strong></td><td><code>assert_platform_customer_access</code></td></tr>
|
||
<tr><td>对话 / 风控 REST</td><td><code>get_auth_context</code></td><td><strong>必须</strong></td><td><code>assert_customer_access</code> + 准入矩阵</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="callout callout-warning">
|
||
<strong>指挥 AI 接前端时:</strong> <code>apiFetch</code> 读持仓只带 Bearer;若误加 <code>X-Agent-Type: customer</code>,平台路由会忽略,但混进 chat 路由就会多一层校验甚至 401。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>群聊:前端问「张三持仓多少」</h2>
|
||
<p>理财师工作台发 GET 持仓——只走平台三层,不进 LangGraph。</p>
|
||
|
||
<div class="chat-window" id="chat-platform-m2">
|
||
<div class="chat-messages">
|
||
<div class="chat-message" data-msg="0" data-sender="frontend" style="display:none">
|
||
<div class="chat-avatar" style="background: var(--color-accent)">🖥</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color: var(--color-accent)">前端</span>
|
||
<p>GET /api/customers/CUST-1001/holdings<br>Authorization: Bearer eyJ…<br><em>(无 X-Agent-Type)</em></p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="1" data-sender="api" style="display:none">
|
||
<div class="chat-avatar" style="background: #5A9DB8">🚪</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color: #5A9DB8">customers.py</span>
|
||
<p>Depends(get_platform_auth_context) → JWT 验签 OK<br>assert_platform_customer_access:STAFF-10086 名下有 CUST-1001 ✓</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="2" data-sender="service" style="display:none">
|
||
<div class="chat-avatar" style="background: #E06B56">📦</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color: #E06B56">platform_service</span>
|
||
<p>list_holdings(customer_id) → prepare_row 脱敏开关检查 → 分页组装 items</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="3" data-sender="core" style="display:none">
|
||
<div class="chat-avatar" style="background: #2D8B55">🗄</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color: #2D8B55">core_ro</span>
|
||
<p>SELECT … FROM holdings WHERE customer_id = ?<br>只读,不写 Core 正式账</p>
|
||
</div>
|
||
</div>
|
||
<div class="chat-message" data-msg="4" data-sender="frontend" style="display:none">
|
||
<div class="chat-avatar" style="background: var(--color-accent)">🖥</div>
|
||
<div class="chat-bubble">
|
||
<span class="chat-sender" style="color: var(--color-accent)">前端</span>
|
||
<p>收到 200 + data.items,渲染持仓表。全程没经过 chat.py。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-typing" style="display:none">
|
||
<div class="chat-avatar" id="chat-platform-m2-typing-avatar">?</div>
|
||
<div class="chat-typing-dots">
|
||
<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>
|
||
</div>
|
||
</div>
|
||
<div class="chat-controls">
|
||
<button class="btn chat-next-btn">下一条</button>
|
||
<button class="btn chat-all-btn">自动播放</button>
|
||
<button class="btn chat-reset-btn">重来</button>
|
||
<span class="chat-progress"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">deps.py</span>
|
||
<pre><code><span class="code-line"><span class="code-keyword">def</span> <span class="code-function">get_platform_auth_context</span>(request: Request) -> AuthContext:</span>
|
||
<span class="code-line"> <span class="code-string">"""平台 API 鉴权:JWT 通道 **不要求** X-Agent-Type。"""</span></span>
|
||
<span class="code-line"> auth_header = request.headers.get(<span class="code-string">"Authorization"</span>, <span class="code-string">""</span>)</span>
|
||
<span class="code-line"> <span class="code-keyword">if</span> auth_header[:<span class="code-number">7</span>].lower() == <span class="code-string">"bearer "</span>:</span>
|
||
<span class="code-line"> claims = verify_token(token)</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> _bind_state(request, _claims_to_auth(claims))</span>
|
||
<span class="code-line"> <span class="code-comment"># dev:X-Debug-Role / X-Debug-Actor 兜底</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">函数名就写死:平台鉴权,不读 X-Agent-Type。</p>
|
||
<p class="tl">有 Bearer 就验 JWT,解析出 actor_id 和 roles。</p>
|
||
<p class="tl">本地开发没 JWT 时,可用 debug 头冒充身份(仅 development)。</p>
|
||
<p class="tl">和 get_auth_context 不同:后者在 JWT 通道会强制校验 X-Agent-Type + 准入矩阵。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-platform-m2">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-a"
|
||
data-explanation-right="问数 POST /api/analyst/chat 走 get_platform_auth_context,前端 analyst.ts 只带 Bearer。"
|
||
data-explanation-wrong="analyst 问数是平台读能力,不是 /api/chat 对话分流,不要 X-Agent-Type。">
|
||
<h3 class="quiz-question">分析员「问数工作台」调 /api/analyst/chat,要带 X-Agent-Type: analyst 吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不要,走平台鉴权</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>要,和 ChatPanel 一样</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-platform-m2')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-platform-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">PLATFORM_RESPONSE_DESENSITIZE:<br>模拟库原样,真库再遮</h1>
|
||
<p class="module-lead animate-in">
|
||
环境变量
|
||
<code>PLATFORM_RESPONSE_DESENSITIZE</code>
|
||
控制平台 API 出参是否打码姓名/手机/证件。
|
||
v0.1 默认<strong>关</strong>——本地 Core 模拟库联调看原值;接真 Core 再开,在
|
||
<span class="term" data-definition="Service 层 = 业务组装层,REST 和日后 Agent Tool 共用,避免两处各写一套脱敏逻辑。">Platform Service</span>
|
||
统一出口处理。
|
||
</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>模拟库联调</th><th>生产 / 真 Core</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>false</code>(默认)</td><td>L0 字段原样返回</td><td>—</td></tr>
|
||
<tr><td><code>true</code></td><td>—</td><td>姓名/手机/证件/银行卡打码;<code>customer_id</code> 不脱敏</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="callout callout-accent">
|
||
<strong>实现位置:</strong> <code>app/service/platform/common.py</code> 的 <code>maybe_desensitize_row</code> → <code>prepare_row</code>。REST 与 Agent 适配层都应走这里,不要在路由里手写 mask。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">platform/common.py</span>
|
||
<pre><code><span class="code-line"><span class="code-keyword">def</span> <span class="code-function">maybe_desensitize_row</span>(row: dict | None) -> dict | None:</span>
|
||
<span class="code-line"> <span class="code-keyword">if</span> row <span class="code-keyword">is</span> None <span class="code-keyword">or not</span> settings.platform_response_desensitize:</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> row</span>
|
||
<span class="code-line"> <span class="code-keyword">for</span> key, masker <span class="code-keyword">in</span> (</span>
|
||
<span class="code-line"> (<span class="code-string">"display_name"</span>, d.mask_name),</span>
|
||
<span class="code-line"> (<span class="code-string">"mobile_phone"</span>, d.mask_phone),</span>
|
||
<span class="code-line"> (<span class="code-string">"id_card_no"</span>, d.mask_id_card),</span>
|
||
<span class="code-line"> ...</span>
|
||
<span class="code-line"> ): ...</span>
|
||
<span class="code-line"><span class="code-keyword">def</span> <span class="code-function">prepare_row</span>(row):</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> to_jsonable(maybe_desensitize_row(row) or row)</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">开关关着 → 数据库行原样返回,方便本地对账。</p>
|
||
<p class="tl">开关开着 → 按字段名找 mask 函数,逐个打码。</p>
|
||
<p class="tl">prepare_row 是统一出口:先脱敏,再把 Decimal/日期转成 JSON 能序列化的类型。</p>
|
||
<p class="tl">指挥 AI 加新字段时,若含 PII,记得在这里登记 masker。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>自检:你能否指挥 AI 正确切换?</h2>
|
||
<div class="quiz-container" id="quiz-platform-m3">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-c"
|
||
data-explanation-right="对。默认 false 是为了模拟库联调;上真库时在 .env 设 PLATFORM_RESPONSE_DESENSITIZE=true,脱敏在 Service 层自动生效。"
|
||
data-explanation-wrong="不要在每个路由里 copy 打码逻辑;开关在 settings,行为在 platform/common.py。">
|
||
<h3 class="quiz-question">准备接真 Core,要让出参打码手机号,最省事的做法是?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>在每个 customers.py 返回前手写 replace</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>让前端自己 mask 手机号</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>.env 设 PLATFORM_RESPONSE_DESENSITIZE=true</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="customer_id 是业务主键,联调和归属校验都要用,契约明确不脱敏。"
|
||
data-explanation-wrong="customer_id 不在 mask 列表里,脱敏只覆盖姓名/手机/证件/银行卡等 PII 字段。">
|
||
<h3 class="quiz-question">脱敏开启后,customer_id 会被打码吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>会,全部敏感字段都遮</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不会,customer_id 保留原值</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-platform-m3')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-platform-m3')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|