feat: 文档Tab重构为左右布局——左侧栏目录树+右侧内容区(GitBook式),点击目录右侧显示,默认打开版本变更

This commit is contained in:
hmo
2026-08-11 10:40:41 +08:00
parent 7d75d54734
commit d5771f3630
+63 -49
View File
@@ -1932,35 +1932,40 @@ function inMd(t) {
// ── 📚 文档 Tab2026-08-11 新增,开源项目式文档体系)──
let _docsState = { loaded: false, currentPath: null, view: 'tree' }; // tree | versions | read
// ── 📚 文档 Tab2026-08-11 重构为左右布局:左侧栏目录 + 右侧内容)──
let _docsState = { currentPath: null, tree: null };
async function renderDocs() {
const el = document.getElementById('tab-docs');
if (!el) return;
el.innerHTML =
'<div class="flex gap-2 items-center mb-3 border-b border-slate-800 pb-2">' +
'<button class="doc-nav-btn px-3 py-1.5 text-sm rounded-lg border border-slate-700 ' + (_docsState.view === 'tree' ? 'bg-slate-800 text-blue-400' : 'text-slate-400') + '" onclick="_docsView(\'tree\')">📖 目录</button>' +
'<button class="doc-nav-btn px-3 py-1.5 text-sm rounded-lg border border-slate-700 ' + (_docsState.view === 'versions' ? 'bg-slate-800 text-blue-400' : 'text-slate-400') + '" onclick="_docsView(\'versions\')">📜 版本变更</button>' +
'<span class="text-xs text-slate-500 ml-2">文档中心:帮助文档 + 版本变更说明(面向运营者)</span>' +
'</div>' +
'<div id="docs-body" class="text-sm"></div>';
if (_docsState.view === 'tree') {
await _renderDocsTree();
} else if (_docsState.view === 'versions') {
await _renderDocsVersions();
'<div class="flex gap-4" style="min-height:600px">' +
// 左侧栏:目录树
'<div class="w-64 flex-shrink-0 border-r border-slate-800 pr-3 overflow-y-auto" style="max-height:75vh">' +
'<div class="text-xs font-semibold text-slate-400 mb-2 uppercase tracking-wide sticky top-0 bg-slate-950 py-1">📚 文档目录</div>' +
'<div id="docs-tree" class="space-y-1 text-sm"></div>' +
'</div>' +
// 右侧:内容区
'<div class="flex-1 min-w-0 overflow-y-auto" style="max-height:75vh">' +
'<div id="docs-content" class="text-sm"></div>' +
'</div>' +
'</div>';
// 首次加载:渲染目录树
await _renderDocsTree();
// 默认显示版本变更(运营者最关心的)
if (!_docsState.currentPath) {
await _docsRead('VERSIONS.md');
}
}
async function _docsView(view) {
_docsState.view = view;
await renderDocs();
}
async function _renderDocsTree() {
const body = document.getElementById('docs-body');
const tree = document.getElementById('docs-tree');
const data = await fetchJSON('/api/docs/index');
if (!data.ok) {
body.innerHTML = '<div class="text-red-400">' + (data.error || '加载失败') + '</div>';
tree.innerHTML = '<div class="text-red-400 text-xs">' + (data.error || '加载失败') + '</div>';
return;
}
_docsState.tree = data.index;
const byCat = {};
data.index.forEach(d => {
byCat[d.category] = byCat[d.category] || [];
@@ -1968,54 +1973,63 @@ async function _renderDocsTree() {
});
let html = '';
for (const [cat, docs] of Object.entries(byCat)) {
html += '<div class="mb-4">' +
'<div class="text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wide">' + cat + '</div>' +
html += '<div class="mb-3">' +
'<div class="text-[11px] font-semibold text-slate-500 mb-1 uppercase tracking-wide">' + cat + '</div>' +
'<div class="space-y-0.5">' +
docs.map(d =>
'<div class="cursor-pointer hover:bg-slate-800/60 px-2 py-1 rounded text-slate-300" onclick="_docsRead(\'' + d.path + '\')">' +
(d.core ? '⭐ ' : '📄 ') + d.title + '</div>'
'<div class="cursor-pointer hover:bg-slate-800/60 px-2 py-1 rounded text-slate-300 truncate" title="' + d.path + '" onclick="_docsRead(\'' + d.path + '\')">' +
(d.core ? '⭐ ' : '') + d.title + '</div>'
).join('') +
'</div></div>';
}
body.innerHTML = html;
tree.innerHTML = html;
// 高亮当前选中项
_docsHighlight();
}
async function _renderDocsVersions() {
const body = document.getElementById('docs-body');
const data = await fetchJSON('/api/docs/versions');
if (!data.ok) {
body.innerHTML = '<div class="text-red-400">' + (data.error || '加载失败') + '</div>';
return;
function _docsHighlight() {
document.querySelectorAll('#docs-tree [title]').forEach(el => el.classList.remove('bg-slate-800', 'text-blue-400'));
if (_docsState.currentPath) {
document.querySelectorAll('#docs-tree [title="' + _docsState.currentPath + '"]').forEach(el => {
el.classList.add('bg-slate-800', 'text-blue-400');
});
}
body.innerHTML = data.versions.map(v => {
const parts = v.title.split('—');
const date = parts[0].trim();
const title = parts.slice(1).join('—').trim() || v.title;
return '<div class="mb-4 border-l-2 border-blue-700 pl-3">' +
'<div class="text-xs text-slate-500">' + date + '</div>' +
'<div class="font-semibold text-slate-200 mb-1">' + title + '</div>' +
'<div class="text-slate-400 whitespace-pre-wrap text-xs leading-relaxed">' + v.body + '</div>' +
'</div>';
}).join('');
}
async function _docsRead(path) {
_docsState.view = 'read';
_docsState.currentPath = path;
const body = document.getElementById('docs-body');
const navBar = body.closest('#tab-docs').querySelector('.flex.gap-2');
// 更新导航显示当前路径
_docsHighlight();
const content = document.getElementById('docs-content');
if (!content) return;
content.innerHTML = '<div class="text-slate-500 text-xs">加载中...</div>';
const data = await fetchJSON('/api/docs/read?path=' + encodeURIComponent(path));
if (!data.ok) {
body.innerHTML = '<div class="text-red-400">' + (data.error || '加载失败') + '</div>';
content.innerHTML = '<div class="text-red-400">' + (data.error || '加载失败') + '</div>';
return;
}
body.innerHTML =
'<div class="mb-2">' +
'<button class="text-xs text-blue-400 hover:underline" onclick="_docsView(\'tree\')">← 返回目录</button>' +
'<span class="text-xs text-slate-500 ml-2">' + path + '</span>' +
'</div>' +
'<div class="prose-invert bg-slate-900/50 border border-slate-800 rounded-xl p-4 whitespace-pre-wrap text-sm leading-relaxed text-slate-300 font-mono">' + inMd(data.content) + '</div>';
// 版本变更用特殊排版(日期+标题+正文分段)
if (path === 'VERSIONS.md') {
content.innerHTML = _renderVersionsHTML(data.content);
} else {
content.innerHTML =
'<div class="mb-2 text-xs text-slate-500 border-b border-slate-800 pb-2">📄 ' + path + '</div>' +
'<div class="prose-invert bg-slate-900/50 border border-slate-800 rounded-xl p-5 whitespace-pre-wrap leading-relaxed text-slate-300">' + inMd(data.content) + '</div>';
}
}
function _renderVersionsHTML(md) {
// 解析 VERSIONS.md:按 ## 分段,每段一个版本卡片
const parts = md.split(/^##\s+/m).filter(p => p.trim() && !p.startsWith('阅读指南') && !p.startsWith('维护说明'));
const cards = parts.map(p => {
const lines = p.split('\n');
const title = lines[0].trim();
const body = lines.slice(1).join('\n').trim();
return '<div class="mb-4 border border-slate-800 rounded-xl p-4 bg-slate-900/50">' +
'<div class="font-semibold text-slate-100 mb-2 text-base">' + title + '</div>' +
'<div class="text-slate-300 whitespace-pre-wrap leading-relaxed text-[13px]">' + inMd(body) + '</div>' +
'</div>';
}).join('');
return '<div class="mb-2 text-xs text-slate-500 border-b border-slate-800 pb-2">📜 版本变更记录(给运营者的详细说明)</div>' + cards;
}
function renderResearch() {