feat: 添加 Topic-SubTopic 层级结构与 Accordion 侧边栏
- topics/index.json 重构为嵌套模型(topic → subtopics) - 侧边栏改为 Accordion 交互:默认折叠、同时只展开一个 - 展开/折叠状态通过 localStorage 持久化 - 更新架构文档同步数据模型变更 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+36
-25
@@ -77,20 +77,29 @@ examination/
|
|||||||
"updated": "2026-09-02",
|
"updated": "2026-09-02",
|
||||||
"topics": [
|
"topics": [
|
||||||
{
|
{
|
||||||
"slug": "gc-jvm",
|
"slug": "qunar-ai-fullstack",
|
||||||
"name": "GC / JVM / 三色标记",
|
"name": "去哪儿 AI 面 — AI 全栈方向",
|
||||||
"description": "...",
|
"description": "覆盖 JVM/GC、AI Agent 架构、工具链、幻觉治理等核心考察领域",
|
||||||
"path": "topics/gc-jvm",
|
"subtopics": [
|
||||||
"stats": {
|
{
|
||||||
"total": 20,
|
"slug": "gc-jvm",
|
||||||
"by_type": { "fill_blank": 10, "single_choice": 10 }
|
"name": "GC / JVM / 三色标记",
|
||||||
}
|
"description": "...",
|
||||||
|
"path": "topics/gc-jvm",
|
||||||
|
"stats": { "total": 20, "by_type": { "fill_blank": 10, "single_choice": 10 } }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
前端加载此文件渲染主题列表,`stats` 字段用于展示题目数量。
|
- `topics[]` — 顶层主题列表,每个主题是一个手风琴(Accordion)分区
|
||||||
|
- `slug` / `name` / `description` — 主题标识与展示信息
|
||||||
|
- `subtopics[]` — 子主题列表,隶属于该顶层主题
|
||||||
|
- `path` — 子主题数据目录路径
|
||||||
|
- `stats` — 题目数量统计
|
||||||
|
- 前端以 Accordion 形式渲染:默认全部折叠,同时只展开一个
|
||||||
|
|
||||||
### 3.2 主题元信息 — `topics/{slug}/meta.json`
|
### 3.2 主题元信息 — `topics/{slug}/meta.json`
|
||||||
|
|
||||||
@@ -240,19 +249,20 @@ examination/
|
|||||||
├────────────┬─────────────────────────────┤
|
├────────────┬─────────────────────────────┤
|
||||||
│ Sidebar │ Main Content │
|
│ Sidebar │ Main Content │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ 📚 主题 │ ┌─ Tabs ─────────────────┐ │
|
│ ▼ 去哪儿.. │ ┌─ Tabs ─────────────────┐ │
|
||||||
│ · gc-jvm │ │ 全部 | 填空 | 选择 |.. │ │
|
│ · gc-jvm │ │ 全部 | 填空 | 选择 |.. │ │
|
||||||
│ · ai-so │ └────────────────────────┘ │
|
│ · ai-so │ └────────────────────────┘ │
|
||||||
│ · ai-mm │ ┌─ Toolbar ──────────────┐ │
|
│ · ai-mm │ ┌─ Toolbar ──────────────┐ │
|
||||||
│ · ai-he │ │ 重置 全部显示 提交全部 │ │
|
│ ▶ 其他主题 │ │ 重置 全部显示 提交全部 │ │
|
||||||
│ · ai-at │ └────────────────────────┘ │
|
│ │ └────────────────────────┘ │
|
||||||
│ │ ┌─ Question Card ────────┐ │
|
│ │ ┌─ Question Card ────────┐ │
|
||||||
│ ──────── │ │ #1 ★★★ │ │
|
│ ──────── │ │ #1 ★★★ │ │
|
||||||
│ 📥 导入 │ │ 题干 [____] │ │
|
│ 📥 导入 │ │ 题干 [____] │ │
|
||||||
│ │ │ [检查] [显示答案] │ │
|
│ │ │ [检查] [显示答案] │ │
|
||||||
│ │ │ 💡 解析... │ │
|
│ │ │ 💡 解析... │ │
|
||||||
│ │ └────────────────────────┘ │
|
│ │ └────────────────────────┘ │
|
||||||
└────────────┴─────────────────────────────┘
|
└────────────┴─────────────────────────────┘
|
||||||
|
▼ = 展开中 ▶ = 折叠中(Accordion 模式)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 数据流
|
### 4.2 数据流
|
||||||
@@ -261,9 +271,9 @@ examination/
|
|||||||
页面加载
|
页面加载
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
fetch topics/index.json ──→ 渲染侧边栏主题列表
|
fetch topics/index.json ──→ 渲染侧边栏 Accordion(默认全折叠)
|
||||||
│
|
│
|
||||||
│ 用户点击主题
|
│ 用户展开某主题 + 点击子主题
|
||||||
▼
|
▼
|
||||||
fetch topics/{slug}/meta.json ──→ 获取 question_files 列表
|
fetch topics/{slug}/meta.json ──→ 获取 question_files 列表
|
||||||
│
|
│
|
||||||
@@ -289,12 +299,13 @@ const QUESTION_TYPES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 应用状态
|
// 应用状态
|
||||||
let topics = []; // 从 index.json 加载的主题列表
|
let topicGroups = []; // 顶层主题列表(含 subtopics)
|
||||||
let currentTopic = null; // 当前选中的主题 slug
|
let expandedGroup = null; // 当前展开的顶层主题 slug(Accordion)
|
||||||
let currentQuestions = []; // 当前主题的所有题目(合并自多个题型文件)
|
let currentTopic = null; // 当前选中的子主题 slug
|
||||||
let answers = {}; // qId → { value, correct, revealed }
|
let currentQuestions = []; // 当前子主题的所有题目
|
||||||
let customQuestions = []; // 通过导入功能临时添加的题目
|
let answers = {}; // qId → { value, correct, revealed }
|
||||||
let activeTab = 'all'; // 当前 Tab:'all' 或题型名称
|
let customQuestions = []; // 通过导入功能临时添加的题目
|
||||||
|
let activeTab = 'all'; // 当前 Tab:'all' 或题型名称
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.4 题型配置与渲染
|
### 4.4 题型配置与渲染
|
||||||
|
|||||||
+85
-36
@@ -47,8 +47,20 @@ body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg)
|
|||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
.sidebar-title { font-size: 13px; color: var(--text2); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; padding: 0 8px; }
|
.sidebar-title { font-size: 13px; color: var(--text2); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; padding: 0 8px; }
|
||||||
.topic-list { list-style: none; }
|
|
||||||
.topic-item { padding: 12px 16px; border-radius: 8px; cursor: pointer; transition: all 0.2s; margin-bottom: 4px; }
|
/* Accordion */
|
||||||
|
.accordion-section { margin-bottom: 4px; }
|
||||||
|
.accordion-header { padding: 10px 12px; cursor: pointer; user-select: none; border-radius: 8px; transition: background 0.2s; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.accordion-header:hover { background: var(--surface2); }
|
||||||
|
.accordion-header .accordion-info { flex: 1; min-width: 0; }
|
||||||
|
.accordion-header .accordion-name { font-size: 14px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.accordion-header .accordion-desc { font-size: 11px; color: var(--text2); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.accordion-header .accordion-chevron { font-size: 11px; color: var(--text2); transition: transform 0.2s; flex-shrink: 0; }
|
||||||
|
.accordion-section.expanded .accordion-chevron { transform: rotate(180deg); }
|
||||||
|
.accordion-body { list-style: none; overflow: hidden; max-height: 0; opacity: 0; transition: max-height 0.25s ease, opacity 0.2s ease; }
|
||||||
|
.accordion-section.expanded .accordion-body { max-height: 2000px; opacity: 1; }
|
||||||
|
.accordion-body .topic-item { padding: 10px 12px 10px 24px; }
|
||||||
|
.topic-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: all 0.2s; margin-bottom: 2px; }
|
||||||
.topic-item:hover { background: var(--surface2); }
|
.topic-item:hover { background: var(--surface2); }
|
||||||
.topic-item.active { background: var(--accent); color: white; }
|
.topic-item.active { background: var(--accent); color: white; }
|
||||||
.topic-item .topic-name { font-size: 14px; font-weight: 600; }
|
.topic-item .topic-name { font-size: 14px; font-weight: 600; }
|
||||||
@@ -170,8 +182,7 @@ body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg)
|
|||||||
|
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="sidebar-title">📚 主题列表</div>
|
<div id="accordionContainer"></div>
|
||||||
<ul class="topic-list" id="topicList"></ul>
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<button class="import-btn" onclick="openImportModal()">📥 导入题目</button>
|
<button class="import-btn" onclick="openImportModal()">📥 导入题目</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,16 +226,16 @@ const QUESTION_TYPES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── State ───
|
// ─── State ───
|
||||||
let topics = [];
|
let topicGroups = []; // [{slug, name, description, subtopics: [...]}, ...]
|
||||||
let currentTopic = null;
|
let expandedGroup = null; // slug of the expanded accordion section
|
||||||
let currentQuestions = []; // flattened from all type files
|
let currentTopic = null; // slug of the selected subtopic
|
||||||
let answers = {}; // qId -> { value, correct, revealed }
|
let currentQuestions = [];
|
||||||
let customQuestions = []; // imported
|
let answers = {};
|
||||||
let activeTab = 'all'; // 'all' or type name
|
let customQuestions = [];
|
||||||
|
let activeTab = 'all';
|
||||||
|
|
||||||
// ─── Init ───
|
// ─── Init ───
|
||||||
async function init() {
|
async function init() {
|
||||||
// Theme init
|
|
||||||
const saved = localStorage.getItem('theme') || 'dark';
|
const saved = localStorage.getItem('theme') || 'dark';
|
||||||
document.documentElement.dataset.theme = saved;
|
document.documentElement.dataset.theme = saved;
|
||||||
updateThemeToggle();
|
updateThemeToggle();
|
||||||
@@ -232,10 +243,11 @@ async function init() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('topics/index.json');
|
const res = await fetch('topics/index.json');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
topics = data.topics || [];
|
topicGroups = data.topics || [];
|
||||||
renderTopicList();
|
expandedGroup = localStorage.getItem('expandedGroup') || null;
|
||||||
|
renderAccordion();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('topicList').innerHTML = '<li style="color:var(--red);padding:12px;">加载主题列表失败</li>';
|
document.getElementById('accordionContainer').innerHTML = '<div style="color:var(--red);padding:12px;">加载主题列表失败</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,26 +265,63 @@ function updateThemeToggle() {
|
|||||||
if (btn) btn.textContent = document.documentElement.dataset.theme === 'dark' ? '🌙' : '☀️';
|
if (btn) btn.textContent = document.documentElement.dataset.theme === 'dark' ? '🌙' : '☀️';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Topic List ───
|
// ─── Accordion ───
|
||||||
function renderTopicList() {
|
function toggleAccordion(slug) {
|
||||||
const ul = document.getElementById('topicList');
|
expandedGroup = expandedGroup === slug ? null : slug;
|
||||||
let html = '';
|
localStorage.setItem('expandedGroup', expandedGroup || '');
|
||||||
topics.forEach(t => {
|
renderAccordion();
|
||||||
const active = currentTopic === t.slug ? 'active' : '';
|
}
|
||||||
html += `<li class="topic-item ${active}" onclick="selectTopic('${t.slug}')">
|
|
||||||
<div class="topic-name">${t.name}</div>
|
function findSubtopic(slug) {
|
||||||
<div class="topic-desc">${t.description}</div>
|
for (const g of topicGroups) {
|
||||||
<div class="topic-stats">${t.stats.total} 题 · ${Object.entries(t.stats.by_type).map(([k,v])=>`${typeLabel(k)} ${v}`).join(' / ')}</div>
|
const sub = g.subtopics?.find(s => s.slug === slug);
|
||||||
</li>`;
|
if (sub) return sub;
|
||||||
});
|
|
||||||
if (customQuestions.length) {
|
|
||||||
html += `<li class="topic-item ${currentTopic === '__custom__' ? 'active' : ''}" onclick="selectTopic('__custom__')">
|
|
||||||
<div class="topic-name">📥 自定义导入</div>
|
|
||||||
<div class="topic-desc">临时导入的题目</div>
|
|
||||||
<div class="topic-stats">${customQuestions.length} 题</div>
|
|
||||||
</li>`;
|
|
||||||
}
|
}
|
||||||
ul.innerHTML = html;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAccordion() {
|
||||||
|
const container = document.getElementById('accordionContainer');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
topicGroups.forEach(g => {
|
||||||
|
const expanded = expandedGroup === g.slug;
|
||||||
|
html += `<div class="accordion-section ${expanded ? 'expanded' : ''}">
|
||||||
|
<div class="accordion-header" onclick="toggleAccordion('${g.slug}')">
|
||||||
|
<div class="accordion-info">
|
||||||
|
<div class="accordion-name">${escapeHtml(g.name)}</div>
|
||||||
|
<div class="accordion-desc">${escapeHtml(g.description)}</div>
|
||||||
|
</div>
|
||||||
|
<span class="accordion-chevron">▼</span>
|
||||||
|
</div>
|
||||||
|
<ul class="accordion-body">`;
|
||||||
|
|
||||||
|
(g.subtopics || []).forEach(t => {
|
||||||
|
const active = currentTopic === t.slug ? 'active' : '';
|
||||||
|
html += `<li class="topic-item ${active}" onclick="selectTopic('${t.slug}')">
|
||||||
|
<div class="topic-name">${t.name}</div>
|
||||||
|
<div class="topic-desc">${t.description}</div>
|
||||||
|
<div class="topic-stats">${t.stats.total} 题 · ${Object.entries(t.stats.by_type).map(([k,v])=>`${typeLabel(k)} ${v}`).join(' / ')}</div>
|
||||||
|
</li>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `</ul></div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customQuestions.length) {
|
||||||
|
const active = currentTopic === '__custom__' ? 'active' : '';
|
||||||
|
html += `<div class="accordion-section expanded">
|
||||||
|
<ul class="accordion-body" style="max-height:2000px;opacity:1;">
|
||||||
|
<li class="topic-item ${active}" onclick="selectTopic('__custom__')">
|
||||||
|
<div class="topic-name">📥 自定义导入</div>
|
||||||
|
<div class="topic-desc">临时导入的题目</div>
|
||||||
|
<div class="topic-stats">${customQuestions.length} 题</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeLabel(type) {
|
function typeLabel(type) {
|
||||||
@@ -284,7 +333,7 @@ async function selectTopic(slug) {
|
|||||||
currentTopic = slug;
|
currentTopic = slug;
|
||||||
answers = {};
|
answers = {};
|
||||||
activeTab = 'all';
|
activeTab = 'all';
|
||||||
renderTopicList();
|
renderAccordion();
|
||||||
|
|
||||||
if (slug === '__custom__') {
|
if (slug === '__custom__') {
|
||||||
currentQuestions = customQuestions;
|
currentQuestions = customQuestions;
|
||||||
@@ -292,7 +341,7 @@ async function selectTopic(slug) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const topic = topics.find(t => t.slug === slug);
|
const topic = findSubtopic(slug);
|
||||||
if (!topic) return;
|
if (!topic) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -545,7 +594,7 @@ function doImport() {
|
|||||||
const questions = data.questions || (Array.isArray(data) ? data : [data]);
|
const questions = data.questions || (Array.isArray(data) ? data : [data]);
|
||||||
customQuestions.push(...questions);
|
customQuestions.push(...questions);
|
||||||
closeImportModal();
|
closeImportModal();
|
||||||
renderTopicList();
|
renderAccordion();
|
||||||
if (currentTopic === '__custom__') selectTopic('__custom__');
|
if (currentTopic === '__custom__') selectTopic('__custom__');
|
||||||
document.getElementById('importText').value = '';
|
document.getElementById('importText').value = '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+70
-63
@@ -3,69 +3,76 @@
|
|||||||
"updated": "2026-09-02",
|
"updated": "2026-09-02",
|
||||||
"topics": [
|
"topics": [
|
||||||
{
|
{
|
||||||
"slug": "gc-jvm",
|
"slug": "qunar-ai-fullstack",
|
||||||
"name": "GC / JVM / 三色标记",
|
"name": "去哪儿 AI 面 — AI 全栈方向",
|
||||||
"description": "JVM 堆内存模型、GC 算法、三色标记法、垃圾收集器",
|
"description": "覆盖 JVM/GC、AI Agent 架构、工具链、幻觉治理等核心考察领域",
|
||||||
"path": "topics/gc-jvm",
|
"subtopics": [
|
||||||
"stats": {
|
{
|
||||||
"total": 20,
|
"slug": "gc-jvm",
|
||||||
"by_type": {
|
"name": "GC / JVM / 三色标记",
|
||||||
"fill_blank": 10,
|
"description": "JVM 堆内存模型、GC 算法、三色标记法、垃圾收集器",
|
||||||
"single_choice": 10
|
"path": "topics/gc-jvm",
|
||||||
|
"stats": {
|
||||||
|
"total": 20,
|
||||||
|
"by_type": {
|
||||||
|
"fill_blank": 10,
|
||||||
|
"single_choice": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ai-structured-output",
|
||||||
|
"name": "AI 结构化输出 / Agent 架构",
|
||||||
|
"description": "结构化输出方法、Agent 模式、受限解码",
|
||||||
|
"path": "topics/ai-structured-output",
|
||||||
|
"stats": {
|
||||||
|
"total": 20,
|
||||||
|
"by_type": {
|
||||||
|
"fill_blank": 10,
|
||||||
|
"single_choice": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ai-memory",
|
||||||
|
"name": "Agent 记忆管理 / 冲突处理",
|
||||||
|
"description": "记忆分层架构、淘汰策略、冲突检测与消解",
|
||||||
|
"path": "topics/ai-memory",
|
||||||
|
"stats": {
|
||||||
|
"total": 20,
|
||||||
|
"by_type": {
|
||||||
|
"fill_blank": 10,
|
||||||
|
"single_choice": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ai-hallucination",
|
||||||
|
"name": "AI 幻觉 / 目标评价 / 规范",
|
||||||
|
"description": "幻觉分类、Critic 架构、Context Engineering、评估体系",
|
||||||
|
"path": "topics/ai-hallucination",
|
||||||
|
"stats": {
|
||||||
|
"total": 20,
|
||||||
|
"by_type": {
|
||||||
|
"fill_blank": 10,
|
||||||
|
"single_choice": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "ai-tooling",
|
||||||
|
"name": "AI 工具 / Harness / 综合",
|
||||||
|
"description": "AI 工具选型、Harness 架构、可靠性保障、提效分析",
|
||||||
|
"path": "topics/ai-tooling",
|
||||||
|
"stats": {
|
||||||
|
"total": 20,
|
||||||
|
"by_type": {
|
||||||
|
"fill_blank": 10,
|
||||||
|
"single_choice": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "ai-structured-output",
|
|
||||||
"name": "AI 结构化输出 / Agent 架构",
|
|
||||||
"description": "结构化输出方法、Agent 模式、受限解码",
|
|
||||||
"path": "topics/ai-structured-output",
|
|
||||||
"stats": {
|
|
||||||
"total": 20,
|
|
||||||
"by_type": {
|
|
||||||
"fill_blank": 10,
|
|
||||||
"single_choice": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "ai-memory",
|
|
||||||
"name": "Agent 记忆管理 / 冲突处理",
|
|
||||||
"description": "记忆分层架构、淘汰策略、冲突检测与消解",
|
|
||||||
"path": "topics/ai-memory",
|
|
||||||
"stats": {
|
|
||||||
"total": 20,
|
|
||||||
"by_type": {
|
|
||||||
"fill_blank": 10,
|
|
||||||
"single_choice": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "ai-hallucination",
|
|
||||||
"name": "AI 幻觉 / 目标评价 / 规范",
|
|
||||||
"description": "幻觉分类、Critic 架构、Context Engineering、评估体系",
|
|
||||||
"path": "topics/ai-hallucination",
|
|
||||||
"stats": {
|
|
||||||
"total": 20,
|
|
||||||
"by_type": {
|
|
||||||
"fill_blank": 10,
|
|
||||||
"single_choice": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "ai-tooling",
|
|
||||||
"name": "AI 工具 / Harness / 综合",
|
|
||||||
"description": "AI 工具选型、Harness 架构、可靠性保障、提效分析",
|
|
||||||
"path": "topics/ai-tooling",
|
|
||||||
"stats": {
|
|
||||||
"total": 20,
|
|
||||||
"by_type": {
|
|
||||||
"fill_blank": 10,
|
|
||||||
"single_choice": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user