语言层数据库前端:树形导航 + Markdown 渲染 + 新建/删除
This commit is contained in:
parent
b91fab1530
commit
05b8ed606a
232
persona-brain-db/frontend/index.html
Normal file
232
persona-brain-db/frontend/index.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user