|
|
@@ -0,0 +1,1036 @@
|
|
|
+<!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: #f5f5f5;
|
|
|
+ height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ header {
|
|
|
+ background: #fff;
|
|
|
+ padding: 12px 20px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ height: 56px;
|
|
|
+ }
|
|
|
+ header h1 { font-size: 18px; color: #333; }
|
|
|
+ .header-actions { display: flex; gap: 12px; align-items: center; }
|
|
|
+ .btn {
|
|
|
+ padding: 8px 16px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+ .btn-primary { background: #1a73e8; color: #fff; }
|
|
|
+ .btn-primary:hover { background: #1557b0; }
|
|
|
+ .btn-secondary { background: #f1f3f4; color: #333; }
|
|
|
+ .btn-secondary:hover { background: #e8eaed; }
|
|
|
+ .btn-danger { background: #dc3545; color: #fff; }
|
|
|
+ .btn-danger:hover { background: #c82333; }
|
|
|
+ .btn-icon {
|
|
|
+ padding: 8px;
|
|
|
+ background: transparent;
|
|
|
+ color: #666;
|
|
|
+ min-width: 36px;
|
|
|
+ }
|
|
|
+ .btn-icon:hover { background: #f1f3f4; color: #333; }
|
|
|
+
|
|
|
+ .main {
|
|
|
+ display: flex;
|
|
|
+ height: calc(100vh - 56px);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 左侧边栏 - 树形目录 */
|
|
|
+ .sidebar {
|
|
|
+ width: 280px;
|
|
|
+ background: #fff;
|
|
|
+ border-right: 1px solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .sidebar-header {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+ .sidebar-header h2 { font-size: 14px; color: #666; }
|
|
|
+ .tree-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 8px;
|
|
|
+ }
|
|
|
+ .tree-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ .tree-item:hover { background: #f1f3f4; }
|
|
|
+ .tree-item.active { background: #e8f0fe; color: #1a73e8; }
|
|
|
+ .tree-item.folder { font-weight: 500; }
|
|
|
+ .tree-item .icon {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ margin-right: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ .tree-item .title {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .tree-item .actions {
|
|
|
+ display: none;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+ .tree-item:hover .actions { display: flex; }
|
|
|
+ .tree-item .actions button {
|
|
|
+ padding: 2px 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .tree-item .actions button:hover { background: #e8eaed; }
|
|
|
+ .tree-children { padding-left: 20px; }
|
|
|
+
|
|
|
+ /* 右侧内容区 - 左右分栏 */
|
|
|
+ .content {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 编辑区 */
|
|
|
+ .editor-pane {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-right: 1px solid #e0e0e0;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+ .editor-header {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+ .editor-header input {
|
|
|
+ flex: 1;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ .editor-header input:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #1a73e8;
|
|
|
+ }
|
|
|
+ .editor-meta {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ .editor-meta input {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ .editor-meta input:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #1a73e8;
|
|
|
+ }
|
|
|
+ .editor-content {
|
|
|
+ flex: 1;
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+ .editor-content textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: none;
|
|
|
+ resize: none;
|
|
|
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.7;
|
|
|
+ padding: 0;
|
|
|
+ outline: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 预览区 */
|
|
|
+ .preview-pane {
|
|
|
+ flex: 1;
|
|
|
+ background: #fff;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+ .preview-header {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ .preview-header svg { width: 18px; height: 18px; }
|
|
|
+ .preview-content {
|
|
|
+ flex: 1;
|
|
|
+ padding: 24px;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+ .preview-content h1, .preview-content h2, .preview-content h3 {
|
|
|
+ margin: 20px 0 10px 0;
|
|
|
+ color: #202124;
|
|
|
+ }
|
|
|
+ .preview-content h1 { font-size: 28px; }
|
|
|
+ .preview-content h2 { font-size: 22px; }
|
|
|
+ .preview-content h3 { font-size: 18px; }
|
|
|
+ .preview-content p { margin: 10px 0; line-height: 1.8; }
|
|
|
+ .preview-content code {
|
|
|
+ background: #f1f3f4;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-family: monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ .preview-content pre {
|
|
|
+ background: #f8f9fa;
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow-x: auto;
|
|
|
+ margin: 12px 0;
|
|
|
+ }
|
|
|
+ .preview-content pre code { background: none; padding: 0; }
|
|
|
+ .preview-content blockquote {
|
|
|
+ border-left: 4px solid #1a73e8;
|
|
|
+ padding-left: 16px;
|
|
|
+ margin: 16px 0;
|
|
|
+ color: #555;
|
|
|
+ }
|
|
|
+ .preview-content ul, .preview-content ol {
|
|
|
+ margin: 10px 0;
|
|
|
+ padding-left: 24px;
|
|
|
+ }
|
|
|
+ .preview-content li { margin: 6px 0; }
|
|
|
+
|
|
|
+ /* 空状态 */
|
|
|
+ .empty-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+ .empty-state svg {
|
|
|
+ width: 64px;
|
|
|
+ height: 64px;
|
|
|
+ fill: #ddd;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+ .empty-state h3 { font-size: 18px; margin-bottom: 8px; color: #666; }
|
|
|
+ .empty-state p { font-size: 14px; }
|
|
|
+
|
|
|
+ /* 弹窗 */
|
|
|
+ .modal {
|
|
|
+ display: none;
|
|
|
+ position: fixed;
|
|
|
+ top: 0; left: 0;
|
|
|
+ width: 100%; height: 100%;
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 1000;
|
|
|
+ }
|
|
|
+ .modal.active { display: flex; }
|
|
|
+ .modal-content {
|
|
|
+ background: #fff;
|
|
|
+ padding: 24px;
|
|
|
+ border-radius: 12px;
|
|
|
+ max-width: 400px;
|
|
|
+ width: 90%;
|
|
|
+ }
|
|
|
+ .modal-content h3 { margin-bottom: 16px; font-size: 16px; }
|
|
|
+ .modal-content input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ .modal-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Toast */
|
|
|
+ .toast {
|
|
|
+ position: fixed;
|
|
|
+ bottom: 20px; right: 20px;
|
|
|
+ padding: 12px 20px;
|
|
|
+ background: #333;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ display: none;
|
|
|
+ z-index: 1001;
|
|
|
+ }
|
|
|
+ .toast.show { display: block; }
|
|
|
+ .toast.success { background: #28a745; }
|
|
|
+ .toast.error { background: #dc3545; }
|
|
|
+
|
|
|
+ /* Resize handle */
|
|
|
+ .resize-handle {
|
|
|
+ width: 4px;
|
|
|
+ cursor: col-resize;
|
|
|
+ background: transparent;
|
|
|
+ transition: background 0.2s;
|
|
|
+ }
|
|
|
+ .resize-handle:hover { background: #1a73e8; }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <header>
|
|
|
+ <h1>笔记管理后台</h1>
|
|
|
+ <div class="header-actions">
|
|
|
+ <a href="/" class="btn btn-secondary" target="_blank">前台预览</a>
|
|
|
+ <button class="btn btn-secondary" onclick="logout()">退出登录</button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="main">
|
|
|
+ <!-- 左侧边栏 - 树形目录 -->
|
|
|
+ <div class="sidebar">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <h2>笔记目录</h2>
|
|
|
+ <div>
|
|
|
+ <button class="btn btn-icon" onclick="showNewFolderModal(0)" title="新建目录">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-1 8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z"/></svg>
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-icon" onclick="showNewNoteModal(0)" title="新建笔记">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="tree-container" id="treeContainer">
|
|
|
+ <div class="loading">加载中...</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>
|
|
|
+ <h3>选择一个笔记或目录</h3>
|
|
|
+ <p>从左侧列表选择,或点击 + 新建</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 编辑器和预览(隐藏直到选中笔记) -->
|
|
|
+ <div id="editorView" style="display: none; flex: 1; display: none; flex-direction: column;">
|
|
|
+ <!-- 编辑区 -->
|
|
|
+ <div class="editor-pane" id="editorPane">
|
|
|
+ <div class="editor-header">
|
|
|
+ <input type="text" id="noteTitle" placeholder="笔记标题">
|
|
|
+ <button class="btn btn-primary" onclick="saveNote()">保存</button>
|
|
|
+ <button class="btn btn-secondary" onclick="exportNote()">导出</button>
|
|
|
+ <button class="btn btn-warning" onclick="document.getElementById('importFile').click()">导入</button>
|
|
|
+ <input type="file" id="importFile" accept=".md" style="display:none" onchange="importNote(this.files[0])">
|
|
|
+ <button class="btn btn-danger" onclick="confirmDelete()">删除</button>
|
|
|
+ </div>
|
|
|
+ <div class="editor-meta">
|
|
|
+ <input type="text" id="noteCategory" placeholder="分类(可选)" style="flex: 1;">
|
|
|
+ <input type="text" id="noteTags" placeholder="标签,多个用逗号分隔" style="flex: 1;">
|
|
|
+ <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: #666; white-space: nowrap;">
|
|
|
+ <input type="checkbox" id="notePublic" checked style="width: 16px; height: 16px;">
|
|
|
+ <svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: #666;"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
|
|
|
+ 公开
|
|
|
+ </label>
|
|
|
+ <div style="display: flex; align-items: center; gap: 6px; margin-left: 12px;">
|
|
|
+ <svg viewBox="0 0 24 24" style="width: 14px; height: 14px; fill: #666;"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
|
|
+ <input type="password" id="notePassword" placeholder="访问密码(可选)" style="width: 120px; padding: 6px 10px; border: 1px solid #e0e0e0; border-radius: 4px; font-size: 12px;">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="editor-content">
|
|
|
+ <textarea id="noteContent" placeholder="使用 Markdown 编写内容..."></textarea>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="resize-handle" id="resizeHandle"></div>
|
|
|
+ <!-- 预览区 -->
|
|
|
+ <div class="preview-pane" id="previewPane">
|
|
|
+ <div class="preview-header">
|
|
|
+ <svg viewBox="0 0 24 24" fill="#666"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
|
|
|
+ 预览
|
|
|
+ </div>
|
|
|
+ <div class="preview-content" id="previewContent">
|
|
|
+ <p style="color: #999;">开始编辑以查看预览...</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 新建/编辑目录弹窗 -->
|
|
|
+ <div class="modal" id="folderModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <h3 id="folderModalTitle">新建目录</h3>
|
|
|
+ <input type="hidden" id="folderParentId">
|
|
|
+ <input type="hidden" id="folderEditId">
|
|
|
+ <input type="text" id="folderName" placeholder="目录名称">
|
|
|
+ <div class="modal-actions">
|
|
|
+ <button class="btn btn-secondary" onclick="closeFolderModal()">取消</button>
|
|
|
+ <button class="btn btn-primary" onclick="saveFolder()">保存</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 删除确认弹窗 -->
|
|
|
+ <div class="modal" id="deleteModal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <h3>确认删除</h3>
|
|
|
+ <p id="deleteMessage">确定要删除吗?此操作不可撤销。</p>
|
|
|
+ <div class="modal-actions">
|
|
|
+ <button class="btn btn-secondary" onclick="closeDeleteModal()">取消</button>
|
|
|
+ <button class="btn btn-danger" onclick="doDelete()">删除</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="toast" id="toast"></div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ const API = '/api';
|
|
|
+ const ADMIN_API = '/admin/api';
|
|
|
+ let treeData = [];
|
|
|
+ let currentItem = null;
|
|
|
+ let isFolderMode = false;
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ async function init() {
|
|
|
+ await loadTree();
|
|
|
+ setupResize();
|
|
|
+ setupImagePaste();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置粘贴图片监听
|
|
|
+ function setupImagePaste() {
|
|
|
+ const textarea = document.getElementById('noteContent');
|
|
|
+ if (!textarea) return;
|
|
|
+
|
|
|
+ textarea.addEventListener('paste', async (e) => {
|
|
|
+ // 检查剪贴板中是否有图片
|
|
|
+ const items = e.clipboardData?.items;
|
|
|
+ if (!items) return;
|
|
|
+
|
|
|
+ for (let item of items) {
|
|
|
+ if (item.type.indexOf('image') === 0) {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ const file = item.getAsFile();
|
|
|
+ if (!file) continue;
|
|
|
+
|
|
|
+ // 显示上传提示
|
|
|
+ const originalValue = textarea.value;
|
|
|
+ const cursorPos = textarea.selectionStart;
|
|
|
+ const beforeCursor = originalValue.substring(0, cursorPos);
|
|
|
+ const afterCursor = originalValue.substring(cursorPos);
|
|
|
+
|
|
|
+ textarea.value = beforeCursor + '\n\n' + afterCursor;
|
|
|
+ textarea.selectionStart = textarea.selectionEnd = cursorPos + 14;
|
|
|
+ textarea.focus();
|
|
|
+
|
|
|
+ try {
|
|
|
+ const url = await uploadImage(file);
|
|
|
+ // 替换上传提示为实际图片
|
|
|
+ textarea.value = textarea.value.replace('', ``);
|
|
|
+ // 手动触发预览更新
|
|
|
+ updatePreview();
|
|
|
+ } catch (err) {
|
|
|
+ // 替换为错误信息
|
|
|
+ textarea.value = textarea.value.replace('', '');
|
|
|
+ showToast('图片上传失败', 'error');
|
|
|
+ // 手动触发预览更新
|
|
|
+ updatePreview();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传图片
|
|
|
+ async function uploadImage(file) {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('image', file);
|
|
|
+
|
|
|
+ const res = await fetch(`${ADMIN_API}/upload`, {
|
|
|
+ method: 'POST',
|
|
|
+ credentials: 'include',
|
|
|
+ body: formData
|
|
|
+ });
|
|
|
+
|
|
|
+ const data = await res.json();
|
|
|
+ if (data.code !== 0) {
|
|
|
+ throw new Error(data.message || '上传失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ return data.data.url;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载树形数据
|
|
|
+ async function loadTree() {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`/admin/api/tree`);
|
|
|
+ const data = await res.json();
|
|
|
+ if (data.code === 0) {
|
|
|
+ treeData = data.data;
|
|
|
+ renderTree();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('加载目录失败', 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染树形结构
|
|
|
+ function renderTree() {
|
|
|
+ const container = document.getElementById('treeContainer');
|
|
|
+ if (treeData.length === 0) {
|
|
|
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">暂无内容,点击 + 新建</div>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建树形结构
|
|
|
+ const rootItems = treeData.filter(item => item.parent_id === 0);
|
|
|
+ container.innerHTML = renderTreeItems(rootItems);
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderTreeItems(items) {
|
|
|
+ return items.map(item => {
|
|
|
+ const children = treeData.filter(c => c.parent_id === item.id);
|
|
|
+ const hasChildren = children.length > 0;
|
|
|
+ const isActive = currentItem && currentItem.id === item.id;
|
|
|
+
|
|
|
+ if (item.is_folder) {
|
|
|
+ return `
|
|
|
+ <div class="tree-item folder ${isActive ? 'active' : ''}" onclick="selectItem(${item.id})">
|
|
|
+ <span class="icon">
|
|
|
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="${isActive ? '#1a73e8' : '#fbbc04'}">
|
|
|
+ <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
|
|
|
+ </svg>
|
|
|
+ </span>
|
|
|
+ <span class="title">${escapeHtml(item.title)}</span>
|
|
|
+ <div class="actions">
|
|
|
+ <button onclick="event.stopPropagation(); showNewFolderModal(${item.id})">+目录</button>
|
|
|
+ <button onclick="event.stopPropagation(); showNewNoteModal(${item.id})">+笔记</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ${hasChildren ? `<div class="tree-children">${renderTreeItems(children)}</div>` : ''}
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ return `
|
|
|
+ <div class="tree-item ${isActive ? 'active' : ''}" onclick="selectItem(${item.id})">
|
|
|
+ <span class="icon">
|
|
|
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="#666">
|
|
|
+ <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>
|
|
|
+ </span>
|
|
|
+ <span class="title">${escapeHtml(item.title)}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ }).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选择项目
|
|
|
+ async function selectItem(id) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API}/notes/${id}`);
|
|
|
+ const data = await res.json();
|
|
|
+ if (data.code === 0) {
|
|
|
+ currentItem = data.data;
|
|
|
+ isFolderMode = currentItem.is_folder;
|
|
|
+
|
|
|
+ document.getElementById('emptyState').style.display = 'none';
|
|
|
+ const editorView = document.getElementById('editorView');
|
|
|
+
|
|
|
+ if (isFolderMode) {
|
|
|
+ // 目录模式
|
|
|
+ document.getElementById('editorPane').style.display = 'none';
|
|
|
+ document.getElementById('resizeHandle').style.display = 'none';
|
|
|
+ document.getElementById('previewPane').style.display = 'none';
|
|
|
+ editorView.style.display = 'flex';
|
|
|
+ editorView.innerHTML = `
|
|
|
+ <div style="padding: 40px; text-align: center;">
|
|
|
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="#fbbc04" style="margin-bottom: 16px;">
|
|
|
+ <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
|
|
|
+ </svg>
|
|
|
+ <h3 style="margin-bottom: 8px;">${escapeHtml(currentItem.title)}</h3>
|
|
|
+ <p style="color: #666; margin-bottom: 20px;">目录:包含 ${treeData.filter(t => t.parent_id === id).length} 个项目</p>
|
|
|
+ <button class="btn btn-secondary" onclick="showEditFolderModal()">编辑目录</button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ // 笔记模式
|
|
|
+ document.getElementById('editorPane').style.display = 'flex';
|
|
|
+ document.getElementById('resizeHandle').style.display = 'block';
|
|
|
+ document.getElementById('previewPane').style.display = 'flex';
|
|
|
+ editorView.style.display = 'flex';
|
|
|
+ editorView.style.flexDirection = 'row';
|
|
|
+
|
|
|
+ document.getElementById('noteTitle').value = currentItem.title || '';
|
|
|
+ document.getElementById('noteCategory').value = currentItem.category || '';
|
|
|
+ document.getElementById('noteTags').value = parseTags(currentItem.tags).join(', ');
|
|
|
+ document.getElementById('noteContent').value = currentItem.content || '';
|
|
|
+ document.getElementById('notePublic').checked = currentItem.is_public !== false;
|
|
|
+ document.getElementById('notePassword').value = ''; // 不显示密码
|
|
|
+ updatePreview();
|
|
|
+ }
|
|
|
+
|
|
|
+ renderTree();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('加载失败', 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示新建目录弹窗
|
|
|
+ function showNewFolderModal(parentId) {
|
|
|
+ document.getElementById('folderModalTitle').textContent = '新建目录';
|
|
|
+ document.getElementById('folderParentId').value = parentId;
|
|
|
+ document.getElementById('folderEditId').value = '';
|
|
|
+ document.getElementById('folderName').value = '';
|
|
|
+ document.getElementById('folderModal').classList.add('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示编辑目录弹窗
|
|
|
+ function showEditFolderModal() {
|
|
|
+ if (!currentItem) return;
|
|
|
+ document.getElementById('folderModalTitle').textContent = '编辑目录';
|
|
|
+ document.getElementById('folderParentId').value = currentItem.parent_id;
|
|
|
+ document.getElementById('folderEditId').value = currentItem.id;
|
|
|
+ document.getElementById('folderName').value = currentItem.title;
|
|
|
+ document.getElementById('folderModal').classList.add('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeFolderModal() {
|
|
|
+ document.getElementById('folderModal').classList.remove('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveFolder() {
|
|
|
+ const name = document.getElementById('folderName').value.trim();
|
|
|
+ const parentId = parseInt(document.getElementById('folderParentId').value) || 0;
|
|
|
+ const editId = document.getElementById('folderEditId').value;
|
|
|
+
|
|
|
+ if (!name) {
|
|
|
+ showToast('请输入目录名称', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ let res, data;
|
|
|
+ if (editId) {
|
|
|
+ // 更新
|
|
|
+ res = await fetch(`${API}/notes/${editId}`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ title: name })
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 新建
|
|
|
+ res = await fetch(`${API}/notes`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ title: name, is_folder: true, parent_id: parentId })
|
|
|
+ });
|
|
|
+ }
|
|
|
+ data = await res.json();
|
|
|
+
|
|
|
+ if (data.code === 0) {
|
|
|
+ showToast(editId ? '目录已更新' : '目录已创建', 'success');
|
|
|
+ closeFolderModal();
|
|
|
+ await loadTree();
|
|
|
+ if (editId && currentItem && currentItem.id == editId) {
|
|
|
+ selectItem(parseInt(editId));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ showToast(data.message || '保存失败', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('保存失败', 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示新建笔记弹窗
|
|
|
+ function showNewNoteModal(parentId) {
|
|
|
+ if (!parentId && parentId !== 0) return;
|
|
|
+ currentItem = { parent_id: parentId || 0, is_folder: false, id: null };
|
|
|
+ document.getElementById('emptyState').style.display = 'none';
|
|
|
+ document.getElementById('editorView').style.display = 'flex';
|
|
|
+ document.getElementById('editorView').style.flexDirection = 'row';
|
|
|
+ document.getElementById('editorPane').style.display = 'flex';
|
|
|
+ document.getElementById('resizeHandle').style.display = 'block';
|
|
|
+ document.getElementById('previewPane').style.display = 'flex';
|
|
|
+ document.getElementById('noteTitle').value = '';
|
|
|
+ document.getElementById('noteCategory').value = '';
|
|
|
+ document.getElementById('noteTags').value = '';
|
|
|
+ document.getElementById('noteContent').value = '';
|
|
|
+ updatePreview();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存笔记
|
|
|
+ async function saveNote() {
|
|
|
+ const title = document.getElementById('noteTitle').value.trim();
|
|
|
+ const content = document.getElementById('noteContent').value;
|
|
|
+ const category = document.getElementById('noteCategory').value.trim();
|
|
|
+ const tagsInput = document.getElementById('noteTags').value.trim();
|
|
|
+ const isPublic = document.getElementById('notePublic').checked;
|
|
|
+ const password = document.getElementById('notePassword').value;
|
|
|
+
|
|
|
+ if (!title) {
|
|
|
+ showToast('请输入标题', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let tags = '[]';
|
|
|
+ if (tagsInput) {
|
|
|
+ const arr = tagsInput.split(',').map(t => t.trim()).filter(t => t);
|
|
|
+ tags = JSON.stringify(arr);
|
|
|
+ }
|
|
|
+
|
|
|
+ const payload = {
|
|
|
+ title,
|
|
|
+ content,
|
|
|
+ category,
|
|
|
+ tags,
|
|
|
+ is_public: isPublic,
|
|
|
+ parent_id: currentItem && currentItem.parent_id ? currentItem.parent_id : 0
|
|
|
+ };
|
|
|
+
|
|
|
+ // 只有填写了密码才传递
|
|
|
+ if (password) {
|
|
|
+ payload.password = password;
|
|
|
+ } else {
|
|
|
+ // 如果没填密码但原有笔记有密码,不清除
|
|
|
+ // 如果没填密码且原有笔记也没有密码,不传
|
|
|
+ payload.remove_password = !currentItem?.has_password;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ let res, data;
|
|
|
+ if (currentItem && currentItem.id && !currentItem.is_folder) {
|
|
|
+ // 更新
|
|
|
+ res = await fetch(`${API}/notes/${currentItem.id}`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 新建
|
|
|
+ res = await fetch(`${API}/notes`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ data = await res.json();
|
|
|
+
|
|
|
+ if (data.code === 0) {
|
|
|
+ showToast(currentItem && currentItem.id ? '笔记已更新' : '笔记已创建', 'success');
|
|
|
+ await loadTree();
|
|
|
+ if (data.data && data.data.id) {
|
|
|
+ selectItem(data.data.id);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ showToast(data.message || '保存失败', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('保存失败', 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导出笔记为 Markdown 文件
|
|
|
+ function exportNote() {
|
|
|
+ if (!currentItem || !currentItem.id || currentItem.is_folder) {
|
|
|
+ showToast('请先选择一个笔记(不是目录)', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ window.location.href = `${ADMIN_API}/export/${currentItem.id}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导入 Markdown 文件
|
|
|
+ async function importNote(file) {
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${ADMIN_API}/import`, {
|
|
|
+ method: 'POST',
|
|
|
+ credentials: 'include',
|
|
|
+ body: formData
|
|
|
+ });
|
|
|
+ const data = await res.json();
|
|
|
+
|
|
|
+ if (data.code === 0) {
|
|
|
+ showToast('导入成功', 'success');
|
|
|
+ await loadTree();
|
|
|
+ if (data.data && data.data.id) {
|
|
|
+ selectItem(data.data.id);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ showToast(data.message || '导入失败', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('导入失败', 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清空文件输入
|
|
|
+ document.getElementById('importFile').value = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除确认
|
|
|
+ function confirmDelete() {
|
|
|
+ if (!currentItem || !currentItem.id) return;
|
|
|
+ const message = currentItem.is_folder
|
|
|
+ ? '确定要删除这个目录及其下所有内容吗?此操作不可撤销。'
|
|
|
+ : '确定要删除这篇笔记吗?此操作不可撤销。';
|
|
|
+ document.getElementById('deleteMessage').textContent = message;
|
|
|
+ document.getElementById('deleteModal').classList.add('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeDeleteModal() {
|
|
|
+ document.getElementById('deleteModal').classList.remove('active');
|
|
|
+ }
|
|
|
+
|
|
|
+ async function doDelete() {
|
|
|
+ if (!currentItem || !currentItem.id) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API}/notes/${currentItem.id}`, { method: 'DELETE' });
|
|
|
+ const data = await res.json();
|
|
|
+
|
|
|
+ if (data.code === 0) {
|
|
|
+ showToast('已删除', 'success');
|
|
|
+ closeDeleteModal();
|
|
|
+ currentItem = null;
|
|
|
+ document.getElementById('emptyState').style.display = 'flex';
|
|
|
+ document.getElementById('editorView').style.display = 'none';
|
|
|
+ await loadTree();
|
|
|
+ } else {
|
|
|
+ showToast(data.message || '删除失败', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('删除失败', 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新预览
|
|
|
+ function updatePreview() {
|
|
|
+ const content = document.getElementById('noteContent').value;
|
|
|
+ document.getElementById('previewContent').innerHTML = renderMarkdown(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听输入更新预览
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {
|
|
|
+ const contentArea = document.getElementById('noteContent');
|
|
|
+ if (contentArea) {
|
|
|
+ contentArea.addEventListener('input', updatePreview);
|
|
|
+ }
|
|
|
+ init();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 退出登录
|
|
|
+ async function logout() {
|
|
|
+ await fetch('/admin/logout', { method: 'POST' });
|
|
|
+ window.location.href = '/admin/login';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 工具函数
|
|
|
+ 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 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>';
|
|
|
+
|
|
|
+ // 第一步:所有 Markdown 语法先转为占位符,不输出 HTML
|
|
|
+ const placeholders = [];
|
|
|
+ let placeholderIndex = 0;
|
|
|
+
|
|
|
+ // 代码块占位符
|
|
|
+ text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, function(match, lang, code) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ placeholders.push({ ph: ph, type: 'code', lang: lang || 'plaintext', code: code.trim() });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 图片占位符
|
|
|
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, src) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ placeholders.push({ ph: ph, type: 'img', src: src, alt: alt });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 链接占位符
|
|
|
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, href) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ placeholders.push({ ph: ph, type: 'link', text: text, href: href });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 行内代码占位符
|
|
|
+ text = text.replace(/`([^`]+)`/g, function(match, code) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ placeholders.push({ ph: ph, type: 'inlinecode', code: code });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 标题占位符
|
|
|
+ text = text.replace(/^### (.+)$/gm, function(match, content) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
|
|
|
+ placeholders.push({ ph: ph, type: 'h3', content: content, id: id });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+ text = text.replace(/^## (.+)$/gm, function(match, content) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
|
|
|
+ placeholders.push({ ph: ph, type: 'h2', content: content, id: id });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+ text = text.replace(/^# (.+)$/gm, function(match, content) {
|
|
|
+ const ph = '__PH_' + placeholderIndex++ + '__';
|
|
|
+ const id = content.toLowerCase().replace(/[^\w\u4e00-\u9fa5]+/g, '-');
|
|
|
+ placeholders.push({ ph: ph, type: 'h1', content: content, id: id });
|
|
|
+ return ph;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 转义剩余的 HTML 特殊字符
|
|
|
+ text = escapeHtml(text);
|
|
|
+
|
|
|
+ // 恢复所有占位符为 HTML
|
|
|
+ placeholders.forEach(function(item) {
|
|
|
+ let html = '';
|
|
|
+ switch(item.type) {
|
|
|
+ case 'code':
|
|
|
+ var highlighted = hljs.highlightAuto(item.code, item.lang !== 'plaintext' ? [item.lang] : undefined).value;
|
|
|
+ html = '<pre><code class="hljs language-' + item.lang + '">' + highlighted + '</code></pre>';
|
|
|
+ break;
|
|
|
+ case 'img':
|
|
|
+ html = '<img src="' + item.src + '" alt="' + item.alt + '" style="max-width: 100%; border-radius: 4px;">';
|
|
|
+ break;
|
|
|
+ case 'link':
|
|
|
+ html = '<a href="' + item.href + '" target="_blank">' + item.text + '</a>';
|
|
|
+ break;
|
|
|
+ case 'inlinecode':
|
|
|
+ html = '<code>' + item.code + '</code>';
|
|
|
+ break;
|
|
|
+ case 'h3':
|
|
|
+ html = '<h3 id="' + item.id + '">' + item.content + '</h3>';
|
|
|
+ break;
|
|
|
+ case 'h2':
|
|
|
+ html = '<h2 id="' + item.id + '">' + item.content + '</h2>';
|
|
|
+ break;
|
|
|
+ case 'h1':
|
|
|
+ html = '<h1 id="' + item.id + '">' + item.content + '</h1>';
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ text = text.replace(item.ph, html);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 其他格式
|
|
|
+ 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(/\n\n/g, '</p><p>');
|
|
|
+ text = text.replace(/\n/g, '<br>');
|
|
|
+
|
|
|
+ return '<p>' + text + '</p>';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分栏拖动调整
|
|
|
+ function setupResize() {
|
|
|
+ const handle = document.getElementById('resizeHandle');
|
|
|
+ const editorPane = document.getElementById('editorPane');
|
|
|
+ const previewPane = document.getElementById('previewPane');
|
|
|
+
|
|
|
+ let isResizing = false;
|
|
|
+
|
|
|
+ handle?.addEventListener('mousedown', (e) => {
|
|
|
+ isResizing = true;
|
|
|
+ document.body.style.cursor = 'col-resize';
|
|
|
+ document.body.style.userSelect = 'none';
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('mousemove', (e) => {
|
|
|
+ if (!isResizing) return;
|
|
|
+ const container = document.getElementById('contentArea');
|
|
|
+ const containerRect = container.getBoundingClientRect();
|
|
|
+ const percentage = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
|
+
|
|
|
+ if (percentage > 20 && percentage < 80) {
|
|
|
+ editorPane.style.flex = `0 0 ${percentage}%`;
|
|
|
+ previewPane.style.flex = `0 0 ${100 - percentage}%`;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('mouseup', () => {
|
|
|
+ isResizing = false;
|
|
|
+ document.body.style.cursor = '';
|
|
|
+ document.body.style.userSelect = '';
|
|
|
+ });
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|