语言层数据库前端:树形导航 + Markdown 渲染 + 新建/删除

This commit is contained in:
bingshuo 2026-05-27 00:50:49 +08:00
parent b91fab1530
commit 05b8ed606a

View File

@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光湖 · 语言层数据库</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--bg: #fafafa;
--sidebar-bg: #f0f0f0;
--text: #1a1a1a;
--border: #e0e0e0;
--accent: #4f46e5;
--accent-light: #eef2ff;
--danger: #dc2626;
--danger-light: #fef2f2;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; }
/* Header */
.header {
height:48px; border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 16px;
background:#fff; flex-shrink:0;
}
.header h1 { font-size:15px; font-weight:600; }
.header .actions { margin-left:auto; display:flex; gap:8px; }
.btn {
padding:5px 14px; border-radius:6px; border:1px solid var(--border);
background:#fff; font-size:13px; cursor:pointer; transition:all .15s;
}
.btn:hover { background:var(--accent-light); border-color:var(--accent); color:var(--accent); }
.btn-primary { background:var(--accent); color:#fff; border:none; }
.btn-primary:hover { opacity:.9; }
.btn-danger { color:var(--danger); }
.btn-danger:hover { background:var(--danger-light); border-color:var(--danger); }
/* Layout */
.main { display:flex; flex:1; overflow:hidden; }
/* Sidebar */
.sidebar {
width:260px; background:var(--sidebar-bg); border-right:1px solid var(--border);
overflow-y:auto; padding:12px 0; flex-shrink:0;
}
.sidebar .tree-item {
display:flex; align-items:center; padding:6px 16px; cursor:pointer;
font-size:13px; color:var(--text); border-left:3px solid transparent; transition:all .1s;
}
.sidebar .tree-item:hover { background:#e8e8e8; }
.sidebar .tree-item.active { background:var(--accent-light); border-left-color:var(--accent); font-weight:500; }
.sidebar .tree-item .icon { margin-right:6px; font-size:12px; opacity:.5; }
/* Content */
.content {
flex:1; overflow-y:auto; padding:32px 48px; max-width:800px;
}
.content h1 { font-size:24px; margin-bottom:16px; }
.content h2 { font-size:18px; margin:24px 0 8px; }
.content h3 { font-size:15px; margin:16px 0 6px; }
.content p { margin:8px 0; line-height:1.7; font-size:14px; }
.content code { background:#f0f0f0; padding:1px 5px; border-radius:3px; font-size:13px; }
.content pre { background:#1a1a2e; color:#e0e0e0; padding:16px; border-radius:8px; overflow-x:auto; margin:12px 0; font-size:13px; line-height:1.5; }
.content pre code { background:none; padding:0; color:inherit; }
.content blockquote { border-left:3px solid var(--accent); padding:4px 12px; margin:12px 0; color:#666; font-size:13px; }
.content ul, .content ol { margin:8px 0; padding-left:24px; }
.content li { margin:4px 0; font-size:14px; line-height:1.6; }
.content table { border-collapse:collapse; width:100%; margin:12px 0; font-size:13px; }
.content th, .content td { border:1px solid var(--border); padding:6px 12px; text-align:left; }
.content th { background:#f5f5f5; font-weight:600; }
/* Empty state */
.empty { display:flex; align-items:center; justify-content:center; height:100%; color:#999; font-size:14px; flex-direction:column; gap:8px; }
.empty .icon-big { font-size:48px; opacity:.3; }
/* Loading */
.loading { text-align:center; padding:40px; color:#999; }
</style>
</head>
<body>
<div class="header">
<h1>🌊 语言层数据库</h1>
<div class="actions">
<button class="btn btn-primary" onclick="createPage()">+ 新建页面</button>
</div>
</div>
<div class="main">
<div class="sidebar" id="sidebar">
<div class="loading">加载中…</div>
</div>
<div class="content" id="content">
<div class="empty">
<div class="icon-big">📄</div>
<div>选择一个页面,或新建一个</div>
</div>
</div>
</div>
<script>
const API_BASE = '/brain/documents';
let currentDoc = null;
// 加载文档列表
async function loadTree() {
try {
const res = await fetch(API_BASE);
const json = await res.json();
const docs = json.data || [];
// 过滤掉已删除的
const active = docs.filter(d => !d.document_id.startsWith('deleted_'));
// 构建树
const rootDocs = active.filter(d => !d.parent_id);
const childMap = {};
active.forEach(d => {
if (d.parent_id) {
if (!childMap[d.parent_id]) childMap[d.parent_id] = [];
childMap[d.parent_id].push(d);
}
});
let html = '';
function renderTree(list, level) {
list.forEach(doc => {
const indent = level * 16;
html += `<div class="tree-item${currentDoc && currentDoc.document_id === doc.document_id ? ' active' : ''}"
style="padding-left:${indent + 16}px"
onclick="selectDoc('${doc.document_id}')">
<span class="icon">${childMap[doc.document_id] ? '📁' : '📄'}</span>
${escapeHtml(doc.title)}
</div>`;
if (childMap[doc.document_id]) {
renderTree(childMap[doc.document_id], level + 1);
}
});
}
renderTree(rootDocs, 0);
document.getElementById('sidebar').innerHTML = html || '<div class="loading">还没有页面,点「新建页面」开始</div>';
} catch (err) {
document.getElementById('sidebar').innerHTML = '<div class="loading">加载失败: ' + err.message + '</div>';
}
}
// 选择文档
async function selectDoc(id) {
try {
const res = await fetch(`${API_BASE}/${id}`);
const json = await res.json();
currentDoc = json.data;
renderContent();
loadTree();
} catch (err) {
document.getElementById('content').innerHTML = '<div class="loading">加载失败: ' + err.message + '</div>';
}
}
// 渲染内容
function renderContent() {
if (!currentDoc) return;
let html = `
<h1>${escapeHtml(currentDoc.title)}</h1>
<div style="display:flex; gap:8px; margin-bottom:24px; padding-bottom:16px; border-bottom:1px solid var(--border);">
<span style="font-size:12px; color:#999;">路径: ${escapeHtml(currentDoc.path)}</span>
<span style="font-size:12px; color:#999;">版本: ${currentDoc.version}</span>
<span style="font-size:12px; color:#999;">${currentDoc.updated_at ? new Date(currentDoc.updated_at).toLocaleString('zh-CN') : ''}</span>
<button class="btn btn-danger" style="margin-left:auto" onclick="deleteDoc('${currentDoc.document_id}')">删除</button>
</div>
`;
if (currentDoc.content) {
html += marked.parse(currentDoc.content);
} else {
html += '<p style="color:#999;">暂无内容</p>';
}
document.getElementById('content').innerHTML = html;
}
// 新建页面
async function createPage() {
const title = prompt('页面标题:');
if (!title) return;
const parentId = currentDoc ? currentDoc.document_id : null;
try {
const res = await fetch(API_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, parent_id: parentId, content: '# ' + title + '\n\n' })
});
const json = await res.json();
if (json.data) {
currentDoc = json.data;
renderContent();
loadTree();
}
} catch (err) {
alert('创建失败: ' + err.message);
}
}
// 删除页面
async function deleteDoc(id) {
if (!confirm('确定删除这个页面?')) return;
try {
await fetch(`${API_BASE}/${id}`, { method: 'DELETE' });
currentDoc = null;
document.getElementById('content').innerHTML = '<div class="empty"><div class="icon-big">📄</div><div>选择一个页面,或新建一个</div></div>';
loadTree();
} catch (err) {
alert('删除失败: ' + err.message);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 初始化
loadTree();
</script>
</body>
</html>