feat: 云笔记增强 - 安全加固 + 回收站/版本历史/分享/批量导出 + 前端优化
- 安全: 认证改随机token会话(弃固定cookie), 笔记密码SHA256升级为bcrypt(自动迁移), 堵住GET /api/notes/:id泄露带密码笔记, CORS收紧+SameSite防CSRF, 上传图片内容嗅探 - 回收站: 软删除(deleted_at), 列表/恢复/彻底删除/清空, 目录子树连删连恢复 - 版本历史: note_versions表存快照, 每次保存自动留档, 支持查看/回滚 - 分享: 生成随机token分享链接, 支持过期时间, 公开阅读页share.html - 批量导出: 全部笔记打包zip(按目录结构+front matter) - 前端: 深色模式, Mermaid图表, 待办清单checkbox, 字数统计; 后台新增回收站/历史/分享面板和批量导出按钮 - 新增deploy/note-manager.service systemd单元与smoke_test.py
This commit is contained in:
+97
-8
@@ -7,6 +7,15 @@
|
||||
<!-- 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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
// 初始化 Mermaid(延迟到 DOM 加载)
|
||||
window.addEventListener('load', function() {
|
||||
try {
|
||||
mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' });
|
||||
} catch(e) { console.warn('mermaid init failed', e); }
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -14,6 +23,23 @@
|
||||
background: #fafafa;
|
||||
color: #333;
|
||||
}
|
||||
/* 待办清单 */
|
||||
.task-item { list-style: none; margin: 2px 0; }
|
||||
.task-checkbox { margin-right: 6px; vertical-align: middle; width: 15px; height: 15px; }
|
||||
.task-done { text-decoration: line-through; color: #888; }
|
||||
.mermaid { text-align: center; margin: 16px 0; overflow-x: auto; }
|
||||
/* ───── 深色模式覆盖 ───── */
|
||||
body.dark { background: #1a1d21; color: #e0e0e0; }
|
||||
body.dark header { background: #20242a; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
|
||||
body.dark .sidebar { background: #23272e; border-color: #333; }
|
||||
body.dark .main { background: #1a1d21; }
|
||||
body.dark .search-box { background: #2d323a; }
|
||||
body.dark .search-box input { color: #e0e0e0; }
|
||||
body.dark .note-detail { background: #1e2228; }
|
||||
body.dark .note-content pre, body.dark .note-content code { background: #111; }
|
||||
body.dark .tree-item:hover { background: #2d323a; }
|
||||
body.dark .toc-sidebar { border-color: #333; }
|
||||
body.dark a { color: #7ab8ff; }
|
||||
header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
@@ -426,6 +452,7 @@
|
||||
<input type="text" id="searchInput" placeholder="搜索笔记..." onkeyup="handleSearch(event)">
|
||||
</div>
|
||||
<a href="/admin/" style="color: #666; text-decoration: none; font-size: 14px;">管理</a>
|
||||
<button onclick="toggleSiteDark()" id="darkBtn" style="background:none;border:1px solid #ccc;border-radius:20px;padding:5px 12px;font-size:13px;cursor:pointer;color:#666;">🌙 深色</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -471,6 +498,7 @@
|
||||
<div class="meta">
|
||||
<span id="noteCategory"></span>
|
||||
<span id="noteDate"></span>
|
||||
<span id="noteStats" style="color:#999;font-size:12px;margin-left:8px;"></span>
|
||||
<div class="tags" id="noteTags"></div>
|
||||
</div>
|
||||
<div class="note-content" id="noteContent"></div>
|
||||
@@ -659,8 +687,20 @@
|
||||
const renderedContent = renderMarkdown(currentNote.content || '');
|
||||
document.getElementById('noteContent').innerHTML = renderedContent;
|
||||
|
||||
// 字数统计
|
||||
const statsEl = document.getElementById('noteStats');
|
||||
if (statsEl) {
|
||||
const text = (currentNote.content || '').replace(/\s+/g, '');
|
||||
const words = text.length;
|
||||
const readMin = Math.max(1, Math.ceil(words / 400));
|
||||
statsEl.textContent = `${words} 字 · 约 ${readMin} 分钟阅读`;
|
||||
}
|
||||
|
||||
// 生成目录
|
||||
generateTOC(currentNote.content || '');
|
||||
|
||||
// 渲染 mermaid 图表(异步)
|
||||
setTimeout(renderMermaid, 50);
|
||||
}
|
||||
|
||||
function generateTOC(content) {
|
||||
@@ -803,6 +843,29 @@
|
||||
}
|
||||
|
||||
// 简单的 Markdown 渲染
|
||||
// Mermaid 图表渲染支持
|
||||
let mmdCounter = 0;
|
||||
let mmdItems = [];
|
||||
|
||||
// 渲染页面中的 mermaid 图表
|
||||
function renderMermaid() {
|
||||
if (typeof mermaid === 'undefined' || !mmdItems.length) return;
|
||||
const items = mmdItems.splice(0, mmdItems.length);
|
||||
items.forEach(item => {
|
||||
const el = document.getElementById(item.uid);
|
||||
if (!el) return;
|
||||
try {
|
||||
mermaid.render(item.uid, item.code, el).then(({svg}) => {
|
||||
el.innerHTML = svg;
|
||||
}).catch(() => {
|
||||
el.innerHTML = '<pre style="color:#c0392b;background:#fdf0f0;padding:12px;border-radius:6px;">Mermaid 渲染失败(请检查图表语法)</pre>';
|
||||
});
|
||||
} catch(e) {
|
||||
el.innerHTML = '<pre style="color:#c0392b;background:#fdf0f0;padding:12px;border-radius:6px;">Mermaid 渲染失败(请检查图表语法)</pre>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '<p style="color: #999;">无内容</p>';
|
||||
|
||||
@@ -816,8 +879,7 @@
|
||||
codeBlocks.push({ lang: lang || 'plaintext', code: code.trim() });
|
||||
index++;
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
});
|
||||
// 用占位符保护图片,避免被后续处理影响
|
||||
const imgPlaceholders = [];
|
||||
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, src) {
|
||||
@@ -850,13 +912,19 @@
|
||||
// 处理行内代码
|
||||
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// 恢复代码块并应用高亮
|
||||
// 恢复代码块并应用高亮(mermaid 代码块渲染为图表)
|
||||
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>`
|
||||
);
|
||||
let rendered;
|
||||
if (block.lang === 'mermaid') {
|
||||
// 生成 mermaid 图表占位
|
||||
const uid = 'mmd_' + (++mmdCounter);
|
||||
mmdItems.push({ uid: uid, code: block.code });
|
||||
rendered = `<div class="mermaid" id="${uid}">${block.code}</div>`;
|
||||
} else {
|
||||
const highlighted = hljs.highlightAuto(block.code, block.lang !== 'plaintext' ? [block.lang] : undefined).value;
|
||||
rendered = `<pre><code class="hljs language-${block.lang}">${highlighted}</code></pre>`;
|
||||
}
|
||||
text = text.replace(`__CODE_BLOCK_${i}__`, rendered);
|
||||
});
|
||||
|
||||
// 标题(带 id 用于目录导航)
|
||||
@@ -881,6 +949,13 @@
|
||||
// 删除线
|
||||
text = text.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
||||
|
||||
// 待办清单(- [ ] / - [x])
|
||||
text = text.replace(/^[-*] \[([ xX])\] (.*$)/gm, (m, checked, content) => {
|
||||
const state = (checked === 'x' || checked === 'X') ? 'checked' : '';
|
||||
const cls = state ? 'task-done' : '';
|
||||
return `<li class="task-item"><input type="checkbox" class="task-checkbox" ${state} disabled> <span class="${cls}">${content}</span></li>`;
|
||||
});
|
||||
|
||||
// 引用
|
||||
text = text.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>');
|
||||
|
||||
@@ -898,7 +973,21 @@
|
||||
return `<p>${text}</p>`;
|
||||
}
|
||||
|
||||
// 深色模式切换(前台)
|
||||
function toggleSiteDark() {
|
||||
const isDark = document.body.classList.toggle('dark');
|
||||
localStorage.setItem('nm_dark', isDark ? '1' : '0');
|
||||
document.getElementById('darkBtn').textContent = isDark ? '☀️ 白天' : '🌙 深色';
|
||||
}
|
||||
function initSiteDark() {
|
||||
if (localStorage.getItem('nm_dark') === '1') {
|
||||
document.body.classList.add('dark');
|
||||
document.getElementById('darkBtn').textContent = '☀️ 白天';
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
initSiteDark();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user