| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>云笔记</title>
- <!-- Highlight.js 代码高亮 -->
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- background: #fafafa;
- color: #333;
- }
- header {
- background: #fff;
- padding: 16px 24px;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
- display: flex;
- justify-content: space-between;
- align-items: center;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 100;
- }
- header h1 {
- font-size: 20px;
- font-weight: 600;
- color: #1a73e8;
- }
- .header-right {
- display: flex;
- align-items: center;
- gap: 16px;
- }
- .search-box {
- display: flex;
- align-items: center;
- background: #f1f3f4;
- border-radius: 24px;
- padding: 8px 16px;
- }
- .search-box input {
- border: none;
- background: transparent;
- outline: none;
- width: 200px;
- font-size: 14px;
- }
- .search-box svg {
- width: 20px;
- height: 20px;
- fill: #666;
- }
- .main {
- display: flex;
- margin-top: 65px;
- height: calc(100vh - 65px);
- }
-
- /* 左侧边栏 */
- .sidebar {
- width: 280px;
- background: #fff;
- border-right: 1px solid #e0e0e0;
- overflow-y: auto;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- }
- .sidebar-header {
- padding: 16px;
- border-bottom: 1px solid #e0e0e0;
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .sidebar-header h3 {
- font-size: 14px;
- font-weight: 600;
- color: #333;
- }
- .sidebar-section {
- padding: 12px;
- }
- .sidebar-section h4 {
- font-size: 11px;
- text-transform: uppercase;
- color: #888;
- margin-bottom: 8px;
- font-weight: 500;
- padding: 0 8px;
- }
-
- /* 树形目录 */
- .tree-view {
- flex: 1;
- overflow-y: auto;
- padding: 8px;
- }
- .tree-item {
- user-select: none;
- }
- .tree-node {
- display: flex;
- align-items: center;
- padding: 8px 10px;
- border-radius: 6px;
- cursor: pointer;
- transition: background 0.15s;
- gap: 6px;
- }
- .tree-node:hover {
- background: #f5f5f5;
- }
- .tree-node.active {
- background: #e3f2fd;
- color: #1976d2;
- }
- .tree-toggle {
- width: 18px;
- height: 18px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: transform 0.2s;
- }
- .tree-toggle svg {
- width: 14px;
- height: 14px;
- fill: #888;
- }
- .tree-toggle.expanded {
- transform: rotate(90deg);
- }
- .tree-icon {
- width: 18px;
- height: 18px;
- flex-shrink: 0;
- }
- .tree-icon svg {
- width: 16px;
- height: 16px;
- }
- .tree-icon.folder svg { fill: #f9a825; }
- .tree-icon.note svg { fill: #42a5f5; }
- .tree-label {
- flex: 1;
- font-size: 13px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .tree-children {
- padding-left: 20px;
- }
- .tree-children.collapsed {
- display: none;
- }
- .tree-item.pinned .tree-icon svg { fill: #f57c00; }
-
- /* 筛选区域 */
- .filter-area {
- padding: 12px;
- border-top: 1px solid #e0e0e0;
- background: #fafafa;
- }
- .filter-item {
- padding: 10px 12px;
- border-radius: 8px;
- cursor: pointer;
- font-size: 13px;
- display: flex;
- align-items: center;
- gap: 10px;
- color: #555;
- transition: background 0.2s;
- }
- .filter-item:hover { background: #f1f3f4; }
- .filter-item.active { background: #e3f2fd; color: #1976d2; }
- .filter-item svg { width: 16px; height: 16px; }
-
- .note-list {
- border-top: 1px solid #e0e0e0;
- padding: 8px;
- }
- .note-item {
- padding: 14px;
- border-radius: 8px;
- cursor: pointer;
- margin-bottom: 4px;
- transition: background 0.2s;
- }
- .note-item:hover { background: #f1f3f4; }
- .note-item.active { background: #e8f0fe; }
- .note-item h4 {
- font-size: 14px;
- font-weight: 500;
- margin-bottom: 4px;
- color: #202124;
- }
- .note-item .preview {
- font-size: 12px;
- color: #666;
- line-height: 1.4;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .note-item .meta {
- font-size: 11px;
- color: #999;
- margin-top: 6px;
- }
- .note-item .tags {
- display: flex;
- gap: 4px;
- margin-top: 6px;
- flex-wrap: wrap;
- }
- .note-item .tag {
- padding: 2px 8px;
- background: #e8f0fe;
- color: #1a73e8;
- border-radius: 12px;
- font-size: 11px;
- }
-
- /* 右侧内容区 */
- .content {
- flex: 1;
- overflow-y: auto;
- background: #fff;
- display: flex;
- }
- /* 笔记目录(正文内) */
- .toc-sidebar {
- width: 220px;
- border-right: 1px solid #e8e8e8;
- padding: 24px 16px;
- background: #fafafa;
- flex-shrink: 0;
- overflow-y: auto;
- }
- .toc-title {
- font-size: 12px;
- font-weight: 600;
- color: #888;
- text-transform: uppercase;
- margin-bottom: 12px;
- padding-left: 8px;
- }
- .toc-list {
- list-style: none;
- }
- .toc-item {
- margin-bottom: 4px;
- }
- .toc-link {
- display: block;
- padding: 6px 8px;
- font-size: 13px;
- color: #555;
- text-decoration: none;
- border-radius: 4px;
- transition: all 0.15s;
- border-left: 2px solid transparent;
- }
- .toc-link:hover {
- background: #e8e8e8;
- color: #333;
- }
- .toc-link.active {
- background: #e3f2fd;
- color: #1976d2;
- border-left-color: #1976d2;
- }
- .toc-link.level-2 { padding-left: 16px; font-size: 12px; }
- .toc-link.level-3 { padding-left: 24px; font-size: 12px; color: #777; }
-
- /* 正文区域 */
- .note-area {
- flex: 1;
- overflow-y: auto;
- padding: 40px;
- }
- .note-detail {
- max-width: 800px;
- margin: 0 auto;
- }
- .note-detail h1 {
- font-size: 32px;
- font-weight: 600;
- margin-bottom: 16px;
- color: #202124;
- }
- .note-detail .meta {
- font-size: 14px;
- color: #666;
- margin-bottom: 24px;
- padding-bottom: 24px;
- border-bottom: 1px solid #e0e0e0;
- }
- .note-detail .meta span {
- margin-right: 16px;
- }
- .note-detail .tags {
- display: inline-flex;
- gap: 8px;
- }
- .note-detail .tag {
- padding: 4px 12px;
- background: #e8f0fe;
- color: #1a73e8;
- border-radius: 16px;
- font-size: 13px;
- }
- .note-content {
- font-size: 16px;
- line-height: 1.8;
- color: #333;
- }
- .note-content h1, .note-content h2, .note-content h3 {
- margin: 24px 0 12px 0;
- color: #202124;
- }
- .note-content h1 { font-size: 28px; }
- .note-content h2 { font-size: 24px; }
- .note-content h3 { font-size: 20px; }
- .note-content p { margin: 12px 0; }
- .note-content code {
- background: #f1f3f4;
- padding: 2px 6px;
- border-radius: 4px;
- font-family: 'Monaco', 'Menlo', monospace;
- font-size: 14px;
- }
- .note-content pre {
- background: #f8f9fa;
- padding: 16px;
- border-radius: 8px;
- overflow-x: auto;
- margin: 16px 0;
- }
- .note-content pre code {
- background: none;
- padding: 0;
- }
- .note-content blockquote {
- border-left: 4px solid #1a73e8;
- padding-left: 16px;
- margin: 16px 0;
- color: #555;
- }
- .note-content ul, .note-content ol {
- margin: 12px 0;
- padding-left: 24px;
- }
- .note-content li { margin: 4px 0; }
- .note-content a { color: #1a73e8; }
-
- /* 空状态 */
- .empty-state {
- text-align: center;
- padding: 80px 40px;
- color: #666;
- }
- .empty-state svg {
- width: 80px;
- height: 80px;
- fill: #ddd;
- margin-bottom: 20px;
- }
- .empty-state h2 {
- font-size: 20px;
- margin-bottom: 8px;
- color: #333;
- }
- .empty-state p { font-size: 14px; }
-
- /* 底部 */
- footer {
- text-align: center;
- padding: 20px;
- color: #999;
- font-size: 12px;
- }
- footer a { color: #1a73e8; text-decoration: none; }
-
- /* Toast */
- .toast {
- position: fixed;
- bottom: 20px;
- right: 20px;
- padding: 12px 24px;
- background: #333;
- color: #fff;
- border-radius: 8px;
- display: none;
- z-index: 1001;
- }
- .toast.show { display: block; }
- .toast.success { background: #28a745; }
- .toast.error { background: #dc3545; }
-
- /* 响应式 */
- @media (max-width: 768px) {
- .sidebar { width: 100%; display: none; }
- .sidebar.show { display: block; }
- .content { padding: 20px; }
- .note-detail h1 { font-size: 24px; }
- }
- </style>
- </head>
- <body>
- <header>
- <h1>云笔记</h1>
- <div class="header-right">
- <div class="search-box">
- <svg viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
- <input type="text" id="searchInput" placeholder="搜索笔记..." onkeyup="handleSearch(event)">
- </div>
- <a href="/admin/" style="color: #666; text-decoration: none; font-size: 14px;">管理</a>
- </div>
- </header>
- <div class="main">
- <div class="sidebar">
- <div class="sidebar-header">
- <svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:#1976d2;"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>
- <h3>笔记目录</h3>
- </div>
-
- <div class="tree-view" id="treeView">
- <div style="text-align: center; padding: 40px; color: #999;">加载中...</div>
- </div>
-
- <div class="filter-area">
- <h4>快速筛选</h4>
- <div class="filter-item active" onclick="filterAll()">
- <svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
- 全部笔记
- </div>
- <div class="filter-item" onclick="filterFavorites()">
- <svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
- 收藏笔记
- </div>
- </div>
- </div>
- <div class="content" id="contentArea">
- <div class="empty-state" id="emptyState">
- <svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
- <h2>选择一篇笔记</h2>
- <p>从左侧列表选择笔记查看内容</p>
- </div>
-
- <div class="toc-sidebar" id="tocSidebar" style="display: none;">
- <div class="toc-title">目录</div>
- <ul class="toc-list" id="tocList"></ul>
- </div>
-
- <div class="note-area">
- <div class="note-detail" id="noteDetail" style="display: none;">
- <h1 id="noteTitle"></h1>
- <div class="meta">
- <span id="noteCategory"></span>
- <span id="noteDate"></span>
- <div class="tags" id="noteTags"></div>
- </div>
- <div class="note-content" id="noteContent"></div>
- </div>
- </div>
- </div>
- </div>
- <footer>
- <a href="/admin/">管理后台</a>
- </footer>
- <div class="toast" id="toast"></div>
- <script>
- const API = '/api';
- let treeData = [];
- let flatNotes = []; // 扁平化的笔记列表(用于筛选)
- let currentNote = null;
- let currentFilter = { type: 'all' };
- let expandedNodes = new Set();
- async function init() {
- await loadTree();
- }
- async function loadTree() {
- try {
- const res = await fetch(`${API}/tree`);
- const data = await res.json();
- if (data.code === 0) {
- treeData = data.data;
- renderTree();
- }
- } catch (err) {
- document.getElementById('treeView').innerHTML =
- '<div style="text-align: center; padding: 40px; color: #999;">加载失败</div>';
- }
- }
- function renderTree() {
- const container = document.getElementById('treeView');
- if (treeData.length === 0) {
- container.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">暂无笔记</div>';
- return;
- }
- // 构建树形结构
- const tree = buildTree(treeData);
- container.innerHTML = tree.map(node => renderTreeNode(node, 0)).join('');
- }
-
- // 构建树形结构
- function buildTree(items) {
- const map = {};
- const roots = [];
-
- // 先把所有节点转成有 children 的对象
- items.forEach(item => {
- map[item.id] = { ...item, children: [] };
- });
-
- // 再构建父子关系
- items.forEach(item => {
- if (item.parent_id && map[item.parent_id]) {
- map[item.parent_id].children.push(map[item.id]);
- } else {
- roots.push(map[item.id]);
- }
- });
-
- return roots;
- }
- function renderTreeNode(node, level) {
- const hasChildren = node.children && node.children.length > 0;
- const isExpanded = expandedNodes.has(node.id);
- const isActive = currentNote && currentNote.id === node.id;
- const pinnedClass = node.is_pinned ? 'pinned' : '';
-
- let html = `
- <div class="tree-item">
- <div class="tree-node ${isActive ? 'active' : ''} ${pinnedClass}"
- onclick="${node.is_folder ? `toggleNode(${node.id})` : `viewNote(${node.id})`}">
- ${hasChildren ? `
- <span class="tree-toggle ${isExpanded ? 'expanded' : ''}" id="toggle-${node.id}">
- <svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
- </span>
- ` : `
- <span class="tree-toggle"></span>
- `}
- <span class="tree-icon ${node.is_folder ? 'folder' : 'note'}">
- ${node.is_folder ? `
- <svg viewBox="0 0 24 24"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
- ` : `
- <svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
- `}
- </span>
- <span class="tree-label">${escapeHtml(node.title)}</span>
- </div>
- ${hasChildren ? `
- <div class="tree-children ${isExpanded ? '' : 'collapsed'}" id="children-${node.id}">
- ${node.children.map(child => renderTreeNode(child, level + 1)).join('')}
- </div>
- ` : ''}
- </div>
- `;
- return html;
- }
- function toggleNode(id) {
- if (expandedNodes.has(id)) {
- expandedNodes.delete(id);
- } else {
- expandedNodes.add(id);
- }
-
- const toggle = document.getElementById(`toggle-${id}`);
- const children = document.getElementById(`children-${id}`);
-
- if (toggle) toggle.classList.toggle('expanded');
- if (children) children.classList.toggle('collapsed');
- }
- async function viewNote(id) {
- try {
- // 先获取笔记信息检查是否有密码
- const basicRes = await fetch(`${API}/notes/${id}`);
- const basicData = await basicRes.json();
-
- if (basicData.code !== 0) {
- showToast(basicData.message || '加载失败', 'error');
- return;
- }
-
- const basicNote = basicData.data;
-
- // 如果有密码,需要验证
- if (basicNote.has_password) {
- const password = prompt('此笔记需要密码访问,请输入密码:');
- if (!password) {
- return; // 用户取消
- }
-
- const accessRes = await fetch(`${API}/notes/${id}/access`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ password: password })
- });
- const accessData = await accessRes.json();
-
- if (accessData.code !== 0) {
- showToast(accessData.message || '密码错误', 'error');
- return;
- }
-
- currentNote = accessData.data;
- } else {
- currentNote = basicNote;
- }
-
- renderTree(); // 重新渲染以更新 active 状态
- renderNoteDetail();
- } catch (err) {
- showToast('加载笔记失败', 'error');
- }
- }
- function renderNoteDetail() {
- if (!currentNote) {
- document.getElementById('emptyState').style.display = 'block';
- document.getElementById('noteDetail').style.display = 'none';
- document.getElementById('tocSidebar').style.display = 'none';
- return;
- }
- document.getElementById('emptyState').style.display = 'none';
- document.getElementById('noteDetail').style.display = 'block';
-
- document.getElementById('noteTitle').textContent = currentNote.title;
- document.getElementById('noteCategory').textContent = currentNote.category || '未分类';
- document.getElementById('noteDate').textContent = formatDate(currentNote.updated_at);
-
- const tags = parseTags(currentNote.tags);
- document.getElementById('noteTags').innerHTML = tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('');
-
- const renderedContent = renderMarkdown(currentNote.content || '');
- document.getElementById('noteContent').innerHTML = renderedContent;
-
- // 生成目录
- generateTOC(currentNote.content || '');
- }
-
- function generateTOC(content) {
- const tocList = document.getElementById('tocList');
- const tocSidebar = document.getElementById('tocSidebar');
-
- // 提取标题
- const headingRegex = /^(#{1,3})\s+(.+)$/gm;
- const headings = [];
- let match;
- while ((match = headingRegex.exec(content)) !== null) {
- const level = match[1].length;
- const text = match[2].trim();
- const id = text.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
- headings.push({ level, text, id });
- }
-
- if (headings.length === 0) {
- tocSidebar.style.display = 'none';
- return;
- }
-
- tocSidebar.style.display = 'block';
- tocList.innerHTML = headings.map(h =>
- `<li class="toc-item">
- <a href="#${h.id}" class="toc-link level-${h.level}" onclick="scrollToHeading('${h.id}')">${escapeHtml(h.text)}</a>
- </li>`
- ).join('');
- }
-
- function scrollToHeading(id) {
- const el = document.getElementById(id);
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- }
- async function filterAll() {
- currentFilter = { type: 'all' };
- updateFilterUI();
- await loadTree();
- }
- async function filterFavorites() {
- currentFilter = { type: 'favorites' };
- updateFilterUI();
- try {
- const res = await fetch(`${API}/notes?favorite=true&page_size=100`);
- const data = await res.json();
- if (data.code === 0) {
- flatNotes = data.data || [];
- renderFlatNotes();
- }
- } catch (err) {
- showToast('加载失败', 'error');
- }
- }
- function renderFlatNotes() {
- const container = document.getElementById('treeView');
- if (flatNotes.length === 0) {
- container.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">暂无收藏笔记</div>';
- return;
- }
-
- flatNotes.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
-
- container.innerHTML = flatNotes.map(note => {
- const tags = parseTags(note.tags);
- const isActive = currentNote && currentNote.id === note.id;
- return `
- <div class="tree-item">
- <div class="tree-node ${isActive ? 'active' : ''} ${note.is_pinned ? 'pinned' : ''}"
- onclick="viewNote(${note.id})">
- <span class="tree-toggle"></span>
- <span class="tree-icon note">
- <svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
- </span>
- <span class="tree-label">${escapeHtml(note.title)}</span>
- ${note.is_pinned ? '<span style="color:#f57c00;font-size:11px;">📌</span>' : ''}
- </div>
- </div>
- `;
- }).join('');
- }
- function updateFilterUI() {
- document.querySelectorAll('.filter-item').forEach(el => el.classList.remove('active'));
- const items = document.querySelectorAll('.filter-item');
- if (currentFilter.type === 'all') {
- items[0].classList.add('active');
- } else if (currentFilter.type === 'favorites') {
- items[1].classList.add('active');
- }
- }
- async function handleSearch(event) {
- if (event.key !== 'Enter') return;
- const keyword = event.target.value.trim();
- if (!keyword) {
- await loadTree();
- return;
- }
- try {
- const res = await fetch(`${API}/notes/search?q=${encodeURIComponent(keyword)}`);
- const data = await res.json();
- if (data.code === 0) {
- flatNotes = data.data || [];
- renderFlatNotes();
- }
- } catch (err) {
- showToast('搜索失败', 'error');
- }
- }
- function parseTags(tagsJson) {
- if (!tagsJson) return [];
- try { return JSON.parse(tagsJson); } catch { return []; }
- }
- function escapeHtml(text) {
- if (!text) return '';
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- function formatDate(dateStr) {
- return new Date(dateStr).toLocaleDateString('zh-CN', {
- year: 'numeric', month: 'long', day: 'numeric'
- });
- }
- function showToast(message, type = 'info') {
- const toast = document.getElementById('toast');
- toast.textContent = message;
- toast.className = `toast show ${type}`;
- setTimeout(() => toast.classList.remove('show'), 3000);
- }
- // 简单的 Markdown 渲染
- function renderMarkdown(text) {
- if (!text) return '<p style="color: #999;">无内容</p>';
-
- // 先处理代码块,保留语言标识
- const codeBlockRegex = /```(\w*)\n?([\s\S]*?)```/g;
- const codeBlocks = [];
- let index = 0;
-
- text = text.replace(codeBlockRegex, (match, lang, code) => {
- const placeholder = `__CODE_BLOCK_${index}__`;
- codeBlocks.push({ lang: lang || 'plaintext', code: code.trim() });
- index++;
- return placeholder;
- });
-
- // 用占位符保护图片,避免被后续处理影响
- const imgPlaceholders = [];
- text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, src) {
- const placeholder = '__IMG_' + imgPlaceholders.length + '__';
- imgPlaceholders.push('<img src="' + src + '" alt="' + alt + '" style="max-width: 100%; border-radius: 4px;">');
- return placeholder;
- });
-
- // 用占位符保护链接
- const linkPlaceholders = [];
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text_, url) {
- const placeholder = '__LINK_' + linkPlaceholders.length + '__';
- linkPlaceholders.push('<a href="' + url + '" target="_blank">' + text_ + '</a>');
- return placeholder;
- });
-
- // 转义 HTML(处理代码块内的内容和其他文本)
- text = escapeHtml(text);
-
- // 恢复链接(在转义之后)
- linkPlaceholders.forEach(function(link, i) {
- text = text.replace('__LINK_' + i + '__', link);
- });
-
- // 恢复图片(在转义之后)
- imgPlaceholders.forEach(function(img, i) {
- text = text.replace('__IMG_' + i + '__', img);
- });
-
- // 处理行内代码
- text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
-
- // 恢复代码块并应用高亮
- codeBlocks.forEach((block, i) => {
- const highlighted = hljs.highlightAuto(block.code, block.lang !== 'plaintext' ? [block.lang] : undefined).value;
- text = text.replace(
- `__CODE_BLOCK_${i}__`,
- `<pre><code class="hljs language-${block.lang}">${highlighted}</code></pre>`
- );
- });
-
- // 标题(带 id 用于目录导航)
- text = text.replace(/^### (.+)$/gm, (match, content) => {
- const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
- return `<h3 id="${id}">${content}</h3>`;
- });
- text = text.replace(/^## (.+)$/gm, (match, content) => {
- const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
- return `<h2 id="${id}">${content}</h2>`;
- });
- text = text.replace(/^# (.+)$/gm, (match, content) => {
- const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
- return `<h1 id="${id}">${content}</h1>`;
- });
-
- // 粗体斜体
- text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
- text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
- text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
-
- // 删除线
- text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
-
- // 引用
- text = text.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>');
-
- // 无序列表
- text = text.replace(/^[\-\*] (.*$)/gm, '<li>$1</li>');
- text = text.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
-
- // 有序列表
- text = text.replace(/^\d+\. (.*$)/gm, '<li>$1</li>');
-
- // 换行
- text = text.replace(/\n\n/g, '</p><p>');
- text = text.replace(/\n/g, '<br>');
-
- return `<p>${text}</p>`;
- }
- init();
- </script>
- </body>
- </html>
|