文章详情

返回首页

CF上部署Markdown在线笔记

分享文章 作者: Ws01 创建时间: 2025-12-19 📝 字数: 149,630 字 👁️ 阅读: 1 次
/** * mmnote v1.0.0 - my markdown note. * Copyright (c) 2025, mmnote.com. (MIT Licensed) * https://mmnote.com */ // 保存笔记的路径前缀 const SAVE_PATH = '_tmp'; // 有效的笔记名称格式(只允许字母、数字、下划线和连字符) const VALID_NOTE_PATTERN = /^[a-zA-Z0-9_-]+$/; /** * 使用 notePath 生成加密密钥 * @param {string} notePath - 笔记路径 * @returns {Promise<CryptoKey>} 加密密钥 */ async function generateEncryptionKey(notePath) { const encoder = new TextEncoder(); const keyData = encoder.encode(notePath); const hashBuffer = await crypto.subtle.digest('SHA-256', keyData); return await crypto.subtle.importKey( 'raw', hashBuffer, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'] ); } /** * 加密文本 * @param {string} text - 要加密的文本 * @param {CryptoKey} key - 加密密钥 * @returns {Promise<string>} 加密后的base64字符串 */ async function encryptText(text, key) { const encoder = new TextEncoder(); const data = encoder.encode(text); const iv = crypto.getRandomValues(new Uint8Array(12)); const encryptedData = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, data ); const encryptedArray = new Uint8Array(encryptedData); const resultArray = new Uint8Array(iv.length + encryptedArray.length); resultArray.set(iv); resultArray.set(encryptedArray, iv.length); return btoa(String.fromCharCode(...resultArray)); } /** * 解密文本 * @param {string} encryptedText - 加密的base64字符串 * @param {CryptoKey} key - 解密密钥 * @returns {Promise<string>} 解密后的文本 */ async function decryptText(encryptedText, key) { try { const encryptedArray = new Uint8Array(atob(encryptedText).split('').map(c => c.charCodeAt(0))); const iv = encryptedArray.slice(0, 12); const data = encryptedArray.slice(12); const decryptedData = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, data ); const decoder = new TextDecoder(); return decoder.decode(decryptedData); } catch (error) { console.error('解密失败:', error); return ''; // 解密失败返回空字符串 } } /** * 处理所有传入的请求 * @param {Request} request - 传入的请求对象 * @returns {Response} 响应对象 */ async function handleRequest(request) { const url = new URL(request.url); const pathParts = url.pathname.split('/').filter(Boolean); const noteName = pathParts[0]; const action = pathParts[1]; // 处理分享相关的请求 if (noteName === 'share') { if (request.method === 'POST' && action) { try { const shareData = await request.json(); await NOTES_KV.put('share_' + action, JSON.stringify(shareData)); return new Response(null, { status: 200 }); } catch (error) { return new Response('保存分享数据失败', { status: 500 }); } } else if (request.method === 'GET' && action) { return await handleShareRequest(action); } } // 如果笔记名无效或不存在,生成随机名称并重定向 if (!noteName || noteName.length > 64 || !VALID_NOTE_PATTERN.test(noteName)) { const randomNoteName = generateRandomNoteName(); return Response.redirect(`${url.origin}/${randomNoteName}`, 302); } const notePath = `${SAVE_PATH}/${noteName}`; // 处理密码相关的请求 if (action) { // 使用笔记名和固定盐值生成密码存储键 const passwordKey = await generatePasswordKey(noteName); switch (action) { case 'password': if (request.method === 'POST') { const { password } = await request.json(); // 为每个密码生成唯一盐值 const salt = crypto.getRandomValues(new Uint8Array(16)); const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join(''); // 使用盐值和密码生成最终哈希 const finalHash = await hashPasswordWithSalt(password, salt); // 存储盐值和哈希 await NOTES_KV.put(passwordKey, JSON.stringify({ hash: finalHash, salt: saltHex })); return new Response(null, { status: 200 }); } else if (request.method === 'DELETE') { const { password } = await request.json(); const storedData = await NOTES_KV.get(passwordKey); if (storedData) { const { hash, salt } = JSON.parse(storedData); const saltArray = new Uint8Array(salt.match(/.{2}/g).map(byte => parseInt(byte, 16))); const checkHash = await hashPasswordWithSalt(password, saltArray); if (checkHash === hash) { await NOTES_KV.delete(passwordKey); return new Response(null, { status: 200 }); } } return new Response('Invalid password', { status: 401 }); } break; case 'password-check': const hasPassword = await NOTES_KV.get(passwordKey); return new Response(null, { status: hasPassword ? 200 : 404 }); case 'password-verify': const { password } = await request.json(); const storedData = await NOTES_KV.get(passwordKey); if (storedData) { const { hash, salt } = JSON.parse(storedData); const saltArray = new Uint8Array(salt.match(/.{2}/g).map(byte => parseInt(byte, 16))); const checkHash = await hashPasswordWithSalt(password, saltArray); if (checkHash === hash) { return new Response(null, { status: 200 }); } } return new Response('Invalid password', { status: 401 }); } } const raw = url.searchParams.has('raw'); // 根据请求方法分发处理 switch (request.method) { case 'POST': return await handlePostRequest(request, notePath); case 'GET': return raw || isCommandLineRequest(request) ? await handleRawRequest(notePath) : await handleGetRequest(notePath, noteName); default: return new Response('Method Not Allowed', { status: 405 }); } } /** * 使用笔记名和固定盐值生成密码存储键 * @param {string} noteName - 笔记名称 * @returns {Promise<string>} 密码存储键 */ async function generatePasswordKey(noteName) { const encoder = new TextEncoder(); const data = encoder.encode(noteName + '_pwd_protected'); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return '_secure_' + hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } /** * 使用盐值对密码进行哈希 * @param {string} password - 原始密码 * @param {Uint8Array} salt - 盐值 * @returns {Promise<string>} 哈希后的密码 */ async function hashPasswordWithSalt(password, salt) { const encoder = new TextEncoder(); const passwordData = encoder.encode(password); // 将密码和盐值连接 const dataToHash = new Uint8Array(passwordData.length + salt.length); dataToHash.set(passwordData); dataToHash.set(salt, passwordData.length); // 进行哈希 const hashBuffer = await crypto.subtle.digest('SHA-256', dataToHash); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } /** * 处理POST请求 - 保存或删除笔记内容 * @param {Request} request - POST请求对象 * @param {string} notePath - 笔记保存路径 * @returns {Response} 响应对象 */ async function handlePostRequest(request, notePath) { const formData = await request.formData(); const text = formData.get('text') || await request.text(); // 如果内容为空,删除笔记 if (text.trim().length === 0) { await handleDeleteRequest(notePath); return new Response('Note will be deleted', { status: 200 }); } else { await saveNoteContent(notePath, text); return new Response(null, { status: 204 }); } } /** * 处理GET请求 - 返回笔记的HTML页面 * @param {string} notePath - 笔记路径 * @param {string} noteName - 笔记名称 * @returns {Response} HTML响应 */ async function handleGetRequest(notePath, noteName) { const noteContent = await getNoteContent(notePath); const html = generateHTML(noteName, noteContent); return new Response(html, { headers: { 'Content-Type': 'text/html' } }); } /** * 处理原始内容请求 - 返回纯文本格式的笔记内容 * @param {string} notePath - 笔记路径 * @returns {Response} 文本响应 */ async function handleRawRequest(notePath) { const noteContent = await getNoteContent(notePath); return noteContent ? new Response(noteContent, { headers: { 'Content-Type': 'text/plain' } }) : new Response('404 Not Found', { status: 404 }); } /** * 处理删除请求 * @param {string} notePath - 要删除的笔记路径 * @returns {Response} 响应对象 */ async function handleDeleteRequest(notePath) { await deleteNoteContent(notePath); return new Response(null, { status: 204 }); } /** * 生成随机笔记名称 * @returns {string} 5位随机字符串 */ function generateRandomNoteName() { const chars = '0123456789abcdefghijklmnopqrstuvwxyz'; return Array.from({ length: 5 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } /** * 检查是否为命令行请求 * @param {Request} request - 请求对象 * @returns {boolean} 是否为命令行请求 */ function isCommandLineRequest(request) { const userAgent = request.headers.get('User-Agent') || ''; return userAgent.startsWith('curl') || userAgent.startsWith('Wget'); } function generateHTML(noteName, noteContent) { return `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${noteName} - 在线笔记</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" media="(prefers-color-scheme: dark)"> <!-- 添加KaTeX依赖 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"> <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script> <!-- 添加Mermaid依赖 --> <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script> <style> :root { --primary-color: #4e92d1; --secondary-color: #6c757d; --bg-color: #ffffff; --text-color: #333333; --border-color: #e0e0e0; --container-bg: #ffffff; --editor-bg: #f8f8f8; --shadow-color: rgba(0, 0, 0, 0.1); --hover-color: #f0f0f0; --link-color: #0366d6; --link-hover-color: #0969da; --link-visited-color: #6f42c1; } [data-theme="dark"] { --primary-color: #a2c2f5; --secondary-color: #9ca3af; --bg-color: #1a1a1a; --text-color: #f1f1f1; --border-color: #404040; --container-bg: #2a2a2a; --editor-bg: #333333; --shadow-color: rgba(0, 0, 0, 0.3); --hover-color: #3a3a3a; --link-color: #58a6ff; --link-hover-color: #79b8ff; --link-visited-color: #bc8cff; } * { box-sizing: border-box; margin: 0; padding: 0; } body { margin: 0; background: var(--bg-color); color: var(--text-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 16px; line-height: 1.6; transition: all 0.3s ease; } .container { width: 100%; max-width: 100%; margin: 0 auto; padding: 10px; min-height: 100vh; height: 100vh; display: flex; flex-direction: column; background-color: var(--container-bg); } .container.toolbar-hidden { padding: 10px 10px 10px 10px; } .container.toolbar-hidden .editor-container { height: calc(100vh - 20px); } @media (min-width: 1200px) { .container { max-width: 95%; box-shadow: 0 0 20px var(--shadow-color); } } .toolbar { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: var(--editor-bg); border-radius: 12px; margin-bottom: 10px; gap: 10px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; flex-shrink: 0; transition: all 0.3s ease; cursor: grab; } /* 添加工具栏滚动条样式 */ .toolbar::-webkit-scrollbar { height: 6px; width: 6px; } .toolbar::-webkit-scrollbar-track { background: transparent; } .toolbar::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 3px; } .toolbar::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* 拖动时的光标样式 */ .toolbar.dragging { cursor: grabbing; } .toolbar.hidden { display: none; } .toolbar-left { display: flex; align-items: center; gap: 15px; flex-shrink: 0; /* 防止工具栏压缩 */ } .toolbar-right { display: flex; align-items: center; gap: 15px; flex-shrink: 0; /* 防止工具栏压缩 */ } .toolbar-divider { width: 1px; height: 20px; background-color: var(--border-color); margin: 0 2px; } .switch-label { display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--text-color); font-size: 0.9rem; } .switch-label input[type="checkbox"] { width: 16px; height: 16px; } .mode-toggle { background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 8px; border-radius: 50%; transition: all 0.3s ease; color: var(--text-color); display: flex; align-items: center; justify-content: center; } .mode-toggle:hover { background: var(--hover-color); } .mode-toggle .sun-icon, .mode-toggle .moon-icon { display: none; } [data-theme="dark"] .moon-icon { display: block; } [data-theme="light"] .sun-icon { display: block; } .icon-container { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } .note-name { color: var(--primary-color); font-weight: 500; cursor: pointer; padding: 5px 10px; border-radius: 6px; transition: all 0.3s ease; display: flex; align-items: center; gap: 5px; transform-origin: left center; } .note-name:hover { background: var(--hover-color); transform: scale(1.1); } #save-status { color: var(--secondary-color); font-size: 0.9rem; margin-left: 10px; } .editor-container { display: grid; grid-template-columns: 1fr; gap: 15px; flex: 1; min-height: 0; margin: 0; position: relative; transition: all 0.3s ease; } /* 编辑器包装器 */ .editor-wrapper { position: relative; width: 100%; height: 100%; display: flex; background: var(--editor-bg); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; flex-direction: column; } /* 设置统一的行高和字符宽度 */ .editor-wrapper textarea, .line-numbers span { line-height: 1.5; min-height: 1.5em; font-size: 14px; font-family: 'Consolas', 'Monaco', monospace; box-sizing: border-box; letter-spacing: 0; } .line-numbers { padding: 15px 2px 40px 2px; background: var(--editor-bg); border-right: 1px solid var(--border-color); color: var(--secondary-color); user-select: none; overflow: hidden; min-width: 28px; width: auto; display: flex; flex-direction: column; align-items: flex-end; } .line-numbers span { display: block; padding: 0 4px; min-width: 24px; text-align: right; white-space: nowrap; } .line-numbers.hidden { display: none; } /* 编辑器主体区域 */ .editor-main { display: flex; flex: 1; overflow: hidden; position: relative; } /* 状态栏样式统一 */ .status-bar, .preview-status-bar { position: relative; height: 25px; background: var(--editor-bg); border-top: 1px solid var(--border-color); display: flex; align-items: center; padding: 0 10px; font-size: 12px; color: var(--secondary-color); justify-content: space-between; overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: none; white-space: nowrap; flex-shrink: 0; } /* 状态栏滚动条样式 */ .status-bar::-webkit-scrollbar, .preview-status-bar::-webkit-scrollbar { height: 4px; width: 4px; } .status-bar::-webkit-scrollbar-track, .preview-status-bar::-webkit-scrollbar-track { background: transparent; } .status-bar::-webkit-scrollbar-thumb, .preview-status-bar::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 2px; } .status-bar::-webkit-scrollbar-thumb:hover, .preview-status-bar::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } .status-left, .preview-status-left { display: flex; align-items: center; flex-wrap: nowrap; min-width: min-content; gap: 8px; padding-right: 8px; } .status-right, .preview-status-right { display: flex; align-items: center; margin-left: auto; padding-left: 15px; flex-wrap: nowrap; min-width: min-content; gap: 8px; } /* 确保状态栏内容不会被截断 */ .status-bar > div, .preview-status-bar > div { flex-shrink: 0; } .status-item, .preview-status-item { display: flex; align-items: center; gap: 5px; font-family: 'Consolas', 'Monaco', monospace; white-space: nowrap; color: var(--secondary-color); } .status-item label, .preview-status-item label { display: flex; align-items: center; gap: 4px; cursor: pointer; font-family: inherit; font-size: inherit; color: var(--secondary-color); } .status-item input[type="checkbox"], .preview-status-item input[type="checkbox"] { width: 14px; height: 14px; cursor: pointer; margin: 0; } .status-item span, .preview-status-item span { color: var(--secondary-color); } .editor-container textarea { flex: 1; height: 100%; padding: 15px 10px; border: none; border-radius: 0; background: var(--editor-bg); color: var(--text-color); resize: none; tab-size: 4; -moz-tab-size: 4; white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; overflow-x: hidden; width: 100%; transition: all 0.3s ease; } .editor-container textarea:focus { outline: none; } .editor-container.preview-mode { grid-template-columns: 1fr 1fr; } .preview-container { display: none; height: 100%; padding: 0; /* 移除padding */ border-radius: 8px; border: 1px solid var(--border-color); background: var(--editor-bg); color: var(--text-color); overflow: hidden; /* 改为hidden,防止整体滚动 */ position: relative; -webkit-user-select: none; /* Safari */ -ms-user-select: none; /* IE 10+ */ user-select: none; /* Standard syntax */ } /* 添加预览内容容器样式 */ .preview-content { height: calc(100% - 25px); /* 减去状态栏高度 */ padding: 10px 12px; overflow-y: auto; overflow-x: hidden; -webkit-user-select: text; /* Safari */ -ms-user-select: text; /* IE 10+ */ user-select: text; /* Standard syntax */ } /* 预览区状态栏样式 */ .preview-status-bar { position: absolute; bottom: 0; left: 0; right: 0; height: 25px; background: var(--editor-bg); border-top: 1px solid var(--border-color); display: flex; align-items: center; padding: 0 10px; font-size: 12px; color: var(--secondary-color); z-index: 1; justify-content: space-between; min-width: 100%; white-space: nowrap; } .preview-status-left { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } .preview-status-right { display: flex; align-items: center; gap: 15px; flex-shrink: 0; margin-left: auto; } .preview-status-item { display: flex; align-items: center; gap: 5px; font-family: 'Consolas', 'Monaco', monospace; white-space: nowrap; flex-shrink: 0; } .preview-status-item label { display: flex; align-items: center; gap: 4px; cursor: pointer; font-family: inherit; font-size: inherit; color: var(--secondary-color); } .preview-status-item input[type="checkbox"] { width: 14px; height: 14px; cursor: pointer; margin: 0; } .preview-status-item span { color: var(--secondary-color); } .fullscreen-toggle { background: none; border: none; color: var(--text-color); cursor: pointer; padding: 2px 8px; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; font-size: 12px; } .fullscreen-toggle:hover { background: var(--hover-color); } /* Markdown 内容样式 */ .preview-container > *:first-child { margin-top: 0; } .preview-container > *:last-child { margin-bottom: 0; } .preview-container h1, .preview-container h2, .preview-container h3, .preview-container h4, .preview-container h5, .preview-container h6 { margin-top: 1.8em; margin-bottom: 0.8em; line-height: 1.2; color: var(--text-color); } .preview-container h1:first-child, .preview-container h2:first-child, .preview-container h3:first-child, .preview-container h4:first-child, .preview-container h5:first-child, .preview-container h6:first-child { margin-top: 0; } .preview-container p { margin: 1.2em 0; line-height: 1.8; } .preview-container a { display: inline; color: var(--link-color); text-decoration: none; border-bottom: 1px solid transparent; transition: all 0.2s ease; padding: 2px 4px; margin: 0 -4px; border-radius: 4px; text-align: left; } /* 处理单独一行的链接 */ .preview-container p > a:only-child { display: inline-block; text-align: left; width: auto; } /* 确保链接在段落中的对齐方式 */ .preview-container p { text-align: left; } .preview-container a:hover { background: var(--hover-color); color: var(--link-hover-color); border-bottom-color: var(--link-hover-color); } .preview-container a:visited { color: var(--link-visited-color); } .preview-container a:visited:hover { background: var(--hover-color); border-bottom-color: var(--link-visited-color); } .preview-container a[href^="http"]::after { content: "↗"; display: inline; margin-left: 2px; font-size: 0.9em; opacity: 0.6; } .preview-container ul, .preview-container ol { margin: 1em 0; padding-left: 1.5em; } .preview-container li { margin: 0.5em 0; } .preview-container blockquote { margin: 1.2em 0; padding: 1em 1.2em; border-left: 4px solid var(--border-color); background: var(--bg-color); border-radius: 0 4px 4px 0; display: flow-root; width: fit-content; max-width: 100%; box-shadow: 0 2px 4px var(--shadow-color); transition: all 0.2s ease; } .preview-container blockquote > *:first-child { margin-top: 0; } .preview-container blockquote > *:last-child { margin-bottom: 0; } .preview-container blockquote p { margin: 0.8em 0; line-height: 1.6; } .preview-container blockquote + blockquote { margin-top: -0.5em; } /* 嵌套引用的样式 */ .preview-container blockquote blockquote { margin: 0.8em 0; border-left-color: var(--secondary-color); background: var(--editor-bg); box-shadow: none; } /* 移动端适配 */ @media (max-width: 768px) { .preview-container blockquote { padding: 0.8em 1em; margin: 1em 0; width: 100%; } } /* 代码块基础样式 */ .preview-container pre { background: var(--editor-bg); padding: 1.2em 1em; border-radius: 8px; overflow: auto; position: relative; margin: 1.5em 0; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; line-height: 1.5; font-size: 0.95em; border: 1px solid var(--border-color); scrollbar-width: thin; scrollbar-color: var(--secondary-color) transparent; } /* 行内代码样式 */ .preview-container code { background: var(--editor-bg); padding: 0.2em 0.4em; margin: 0 0.2em; border-radius: 4px; font-size: 0.9em; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; border: 1px solid var(--border-color); } /* 代码块中的代码样式 */ .preview-container pre code { background: none; padding: 0; margin: 0; font-size: 0.95em; white-space: pre; word-break: normal; word-wrap: normal; line-height: inherit; tab-size: 2; hyphens: none; border: none; } /* 代码块滚动条样式 */ .preview-container pre::-webkit-scrollbar { width: 6px; height: 6px; } .preview-container pre::-webkit-scrollbar-track { background: transparent; } .preview-container pre::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 3px; border: 2px solid var(--editor-bg); } .preview-container pre::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* 代码块语言标签 */ .preview-container pre::before { content: attr(data-language); position: absolute; top: 0.5em; right: 0.5em; font-size: 0.85em; color: var(--secondary-color); padding: 0.2em 0.5em; border-radius: 3px; background: var(--container-bg); opacity: 0.8; transition: opacity 0.2s ease; } .preview-container pre:hover::before { opacity: 1; } /* 代码高亮主题 - 浅色模式 */ .hljs { color: #383a42; background: var(--editor-bg); } .hljs-comment, .hljs-quote { color: #a0a1a7; font-style: italic; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: #a626a4; } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: #e45649; } .hljs-literal { color: #0184bb; } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string { color: #50a14f; } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: #986801; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: #4078f2; } .hljs-built_in, .hljs-title.class_, .hljs-class .hljs-title { color: #c18401; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } /* 代码高亮主题 - 深色模式 */ [data-theme="dark"] .hljs { color: #abb2bf; background: var(--editor-bg); } [data-theme="dark"] .hljs-comment, [data-theme="dark"] .hljs-quote { color: #7f848e; font-style: italic; } [data-theme="dark"] .hljs-doctag, [data-theme="dark"] .hljs-keyword, [data-theme="dark"] .hljs-formula { color: #c678dd; } [data-theme="dark"] .hljs-section, [data-theme="dark"] .hljs-name, [data-theme="dark"] .hljs-selector-tag, [data-theme="dark"] .hljs-deletion, [data-theme="dark"] .hljs-subst { color: #e06c75; } [data-theme="dark"] .hljs-literal { color: #56b6c2; } [data-theme="dark"] .hljs-string, [data-theme="dark"] .hljs-regexp, [data-theme="dark"] .hljs-addition, [data-theme="dark"] .hljs-attribute, [data-theme="dark"] .hljs-meta .hljs-string { color: #98c379; } [data-theme="dark"] .hljs-attr, [data-theme="dark"] .hljs-variable, [data-theme="dark"] .hljs-template-variable, [data-theme="dark"] .hljs-type, [data-theme="dark"] .hljs-selector-class, [data-theme="dark"] .hljs-selector-attr, [data-theme="dark"] .hljs-selector-pseudo, [data-theme="dark"] .hljs-number { color: #d19a66; } [data-theme="dark"] .hljs-symbol, [data-theme="dark"] .hljs-bullet, [data-theme="dark"] .hljs-link, [data-theme="dark"] .hljs-meta, [data-theme="dark"] .hljs-selector-id, [data-theme="dark"] .hljs-title { color: #61afef; } [data-theme="dark"] .hljs-built_in, [data-theme="dark"] .hljs-title.class_, [data-theme="dark"] .hljs-class .hljs-title { color: #e6c07b; } [data-theme="dark"] .hljs-emphasis { font-style: italic; } [data-theme="dark"] .hljs-strong { font-weight: bold; } /* 代码复制按钮 */ .copy-button { position: absolute; top: 0.5em; right: 0.5em; padding: 0.2em 0.5em; font-size: 0.85em; color: var(--text-color); background: var(--container-bg); border: 1px solid var(--border-color); border-radius: 3px; cursor: pointer; opacity: 0; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px; } .preview-container pre:hover .copy-button { opacity: 0.8; } .copy-button:hover { opacity: 1 !important; background: var(--hover-color); } .copy-button.copied { color: #4caf50; border-color: #4caf50; opacity: 1; } /* 移动端适配 */ @media (max-width: 768px) { .preview-container pre { padding: 1em 0.8em; font-size: 0.9em; } .preview-container code { font-size: 0.85em; } .preview-container pre::before { opacity: 1; } } /* 代码块中的链接样式 */ .preview-container pre a, .preview-container code a { border-bottom: none; } .preview-container pre a::after, .preview-container code a::after { display: none; } /* 图片链接样式 */ .preview-container a:has(img) { border-bottom: none; } .preview-container a:has(img)::after { display: none; } .preview-container hr { margin: 2em 0; border: none; border-top: 1px solid var(--border-color); } .preview-container table { width: 100%; border-collapse: collapse; margin: 1.5em 0; overflow-x: auto; display: block; } .preview-container th, .preview-container td { border: 1px solid var(--border-color); padding: 8px 12px; text-align: left; } .preview-container th { background-color: var(--hover-color); font-weight: 600; } .preview-container tr:nth-child(even) { background-color: var(--editor-bg); } .preview-container tr:hover { background-color: var(--hover-color); } .preview-container img { max-width: 100%; margin: 1em 0; border-radius: 4px; } /* 滚动条样式 */ .editor-container textarea, .preview-container, .preview-content { scrollbar-width: thin; scrollbar-color: var(--secondary-color) transparent; } .editor-container textarea::-webkit-scrollbar, .preview-container::-webkit-scrollbar, .preview-content::-webkit-scrollbar { width: 8px; height: 8px; } .editor-container textarea::-webkit-scrollbar-track, .preview-container::-webkit-scrollbar-track, .preview-content::-webkit-scrollbar-track { background: transparent; } .editor-container textarea::-webkit-scrollbar-thumb, .preview-container::-webkit-scrollbar-thumb, .preview-content::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 4px; border: 2px solid var(--editor-bg); } .editor-container textarea::-webkit-scrollbar-thumb:hover, .preview-container::-webkit-scrollbar-thumb:hover, .preview-content::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* 移动端滚动条优化 */ @media (max-width: 768px) { .editor-container textarea::-webkit-scrollbar, .preview-container::-webkit-scrollbar, .preview-content::-webkit-scrollbar { width: 6px; height: 6px; } .editor-container textarea::-webkit-scrollbar-thumb, .preview-container::-webkit-scrollbar-thumb, .preview-content::-webkit-scrollbar-thumb { border-width: 1.5px; } } /* 移动端响应式布局优化 */ @media (max-width: 768px) { .container { padding: 5px; height: 100vh; max-height: -webkit-fill-available; display: flex; flex-direction: column; width: 100%; } .container.toolbar-hidden { padding: 5px; } .editor-container { flex: 1; min-height: 0; gap: 5px; margin-bottom: env(safe-area-inset-bottom, 15px); width: 100%; display: flex; flex-direction: column; } .editor-container.preview-mode { display: flex; flex-direction: column; height: calc(100vh - 60px - env(safe-area-inset-bottom, 15px)); /* 减去工具栏和底部安全区域 */ } .editor-wrapper, .preview-container { flex: 1; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; width: 100%; } .editor-container.preview-mode .editor-wrapper, .editor-container.preview-mode .preview-container { flex: 1; height: 0; /* 让flex:1生效 */ min-height: 0; max-height: none; width: 100%; } /* 处理键盘弹出时的布局 */ @supports (-webkit-touch-callout: none) { .editor-container.preview-mode { height: calc(100vh - 60px - env(safe-area-inset-bottom, 15px) - env(keyboard-inset-height, 0px)); } } /* 确保内容可滚动 */ .editor-container.preview-mode .editor-wrapper textarea, .editor-container.preview-mode .preview-container { height: 100%; overflow-y: auto; } } /* 处理超小屏幕设备 */ @media (max-width: 320px) { .container { padding: 3px 3px 12px 3px; } .container.toolbar-hidden { padding: 8px; } .editor-wrapper textarea, .line-numbers span { font-size: 15px; } .preview-container { font-size: 15px; padding: 10px 10px 20px 10px; } } /* 处理横屏模式 */ @media (max-height: 480px) and (orientation: landscape) { .container { padding: 5px; width: 100%; } .container.toolbar-hidden { padding: 5px; } .toolbar { padding: 6px 10px; margin-bottom: 8px; width: 100%; } .editor-container { height: calc(100vh - 80px); width: 100%; } .editor-container.preview-mode { grid-template-columns: 1fr 1fr; /* 横屏时恢复左右布局 */ gap: 8px; width: 100%; } .editor-wrapper, .preview-container { width: 100%; height: 100%; box-sizing: border-box; } .editor-wrapper textarea, .preview-container { width: 100%; height: 100%; box-sizing: border-box; } .editor-container.preview-mode .editor-wrapper, .editor-container.preview-mode .preview-container { height: 100%; width: 100%; } /* 横屏模式下状态栏优化 */ .status-bar, .preview-status-bar { height: 24px; font-size: 11px; padding: 0 6px; width: 100%; } .status-bar .status-left, .status-bar .status-right, .preview-status-bar .preview-status-left, .preview-status-bar .preview-status-right { gap: 8px; } .status-bar .status-right, .preview-status-bar .preview-status-right { padding-left: 8px; } .status-item, .preview-status-item { margin-right: 8px; } .status-item label, .preview-status-item label { gap: 2px; } .status-item input[type="checkbox"], .preview-status-item input[type="checkbox"] { width: 12px; height: 12px; } /* 调整状态栏右侧开关的间距 */ .status-right .status-item { margin-right: 4px; } .status-right .switch-label, .preview-status-right .switch-label { font-size: 11px; padding: 2px 4px; } } /* 适配折叠屏设备 */ @media (max-width: 350px) and (min-height: 600px) { .container { padding: 4px; } .toolbar { flex-direction: column; align-items: stretch; } .toolbar-left, .toolbar-right { justify-content: center; } .editor-container { height: calc(100vh - 140px); } } /* 适配深色模式和高对比度显示 */ @media (prefers-contrast: high) { :root { --border-color: #666666; --shadow-color: rgba(0, 0, 0, 0.3); } .editor-container textarea, .preview-container { border-width: 2px; } } /* 适配强制颜色模式 */ @media (forced-colors: active) { :root { --border-color: CanvasText; --text-color: CanvasText; --bg-color: Canvas; --primary-color: LinkText; --secondary-color: GrayText; --container-bg: Canvas; --editor-bg: Canvas; --hover-color: Highlight; --link-color: LinkText; --link-hover-color: LinkText; --link-visited-color: VisitedText; } .editor-container textarea, .preview-container, .toolbar, .status-bar, .preview-status-bar { border: 1px solid CanvasText; } .markdown-toolbar button, .toolbar-select, .fullscreen-toggle { border: 1px solid CanvasText; background: Canvas; color: CanvasText; } .markdown-toolbar button:hover, .toolbar-select:hover, .fullscreen-toggle:hover { background: Highlight; color: HighlightText; } } /* 减少动画以适应省电模式 */ @media (prefers-reduced-motion: reduce) { * { transition: none !important; } } /* 工具栏样式 */ .markdown-toolbar { display: flex; gap: 4px; align-items: center; } .markdown-toolbar button { padding: 4px 8px; background: var(--editor-bg); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-color); cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; min-width: 28px; height: 28px; transition: all 0.2s ease; } .markdown-toolbar button:hover { background: var(--hover-color); } .toolbar-select { padding: 4px 8px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--editor-bg); color: var(--text-color); font-size: 14px; cursor: pointer; } .toolbar-select:hover { background: var(--hover-color); } /* Emoji 选择器样式 */ .emoji-picker { position: fixed; /* 改为fixed定位,避免滚动问题 */ left: 50%; top: 50%; transform: translate(-50%, -50%); background: var(--container-bg); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 2px 8px var(--shadow-color); padding: 15px; display: none; z-index: 1000; max-height: 80vh; width: 90%; max-width: 400px; overflow-y: auto; -webkit-overflow-scrolling: touch; } .emoji-picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); gap: 8px; } .emoji-picker button { width: 40px; height: 40px; padding: 8px; border: none; background: none; cursor: pointer; border-radius: 8px; transition: all 0.2s ease; font-size: 20px; display: flex; align-items: center; justify-content: center; } .emoji-picker button:hover { background: var(--hover-color); } .emoji-picker-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999; display: none; } /* 全屏模式样式 */ .preview-container.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100vw; height: 100vh; z-index: 9999; border-radius: 0; border: none; } /* 真实全屏模式的样式 */ .preview-container:fullscreen { background-color: var(--container-bg); width: 100vw; height: 100vh; padding: 0; margin: 0; border: none; border-radius: 0; } .preview-container:fullscreen .preview-content { height: calc(100vh - 25px); padding: 20px; max-width: 1200px; margin: 0 auto; } .preview-container:fullscreen .preview-status-bar { position: fixed; bottom: 0; left: 0; right: 0; background: var(--editor-bg); border-top: 1px solid var(--border-color); } /* 移动端全屏适配 */ @media (max-width: 768px) { .preview-container:fullscreen .preview-content { padding: 15px; height: calc(100vh - 25px - env(safe-area-inset-bottom, 0px)); } .preview-container:fullscreen .preview-status-bar { padding-bottom: env(safe-area-inset-bottom, 0px); } } .preview-container.fullscreen .preview-content { height: calc(100vh - 25px); padding: 20px; } /* 全屏切换按钮样式 */ .fullscreen-toggle { background: none; border: none; color: var(--text-color); cursor: pointer; padding: 2px 8px; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; font-size: 12px; } .fullscreen-toggle:hover { background: var(--hover-color); } .fullscreen-toggle .fullscreen-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; } /* 预览区双击时禁止选中文本 */ .preview-container.fullscreen .preview-content { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } /* 预览区恢复正常文本选择 */ .preview-container:not(.fullscreen) .preview-content { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } /* 全屏模式下的状态栏样式 */ .preview-container.fullscreen .preview-status-bar { position: fixed; bottom: env(safe-area-inset-bottom, 0); left: 0; right: 0; background: var(--editor-bg); border-top: 1px solid var(--border-color); z-index: 10000; } /* 响应式布局 - 移动端优化 */ @media (max-width: 768px) { .toolbar { padding: 8px; margin-bottom: 12px; white-space: nowrap; gap: 8px; } .toolbar-left, .toolbar-right { gap: 8px; } .markdown-toolbar button { min-width: 32px; height: 32px; padding: 4px; } .toolbar-select { padding: 4px; font-size: 13px; } .note-name { font-size: 0.9rem; padding: 4px 8px; } .emoji-picker { padding: 10px; } .emoji-picker button { width: 36px; height: 36px; font-size: 18px; } } /* 移动端状态栏适配 */ @media (max-width: 768px) { .preview-status-bar { padding: 0 8px; overflow: hidden; } .preview-status-right { gap: 8px; } .preview-status-item { margin-left: 8px; font-size: 11px; } } /* 处理超小屏幕设备状态栏 */ @media (max-width: 320px) { .preview-status-bar { padding: 0 5px; } .preview-status-item { margin-left: 5px; font-size: 10px; } } /* 全屏模式下的移动端优化 */ @media (max-width: 768px) { .preview-container.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; width: 100vw; height: 100vh; height: -webkit-fill-available; z-index: 9999; border-radius: 0; padding-bottom: env(safe-area-inset-bottom, 0); } .preview-container.fullscreen .preview-content { height: calc(100vh - 25px - env(safe-area-inset-bottom, 0)); padding: 15px; } } /* 处理横屏模式 */ @media (max-height: 480px) and (orientation: landscape) { .preview-container.fullscreen .preview-status-bar { height: 24px; font-size: 11px; padding: 0 6px; } .preview-container.fullscreen .preview-content { height: calc(100vh - 24px); padding: 10px; } } /* 在现有样式的末尾添加密码相关样式 */ .password-dialog { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--container-bg); border: 1px solid var(--border-color); border-radius: 12px; padding: 24px; box-shadow: 0 8px 24px var(--shadow-color); z-index: 1000; width: 90%; max-width: 360px; transition: all 0.3s ease; } .password-dialog h3 { margin: 0 0 20px 0; color: var(--text-color); font-size: 1.2em; font-weight: 500; display: flex; align-items: center; gap: 8px; } .password-dialog h3::before { content: '🔒'; font-size: 1.1em; } .password-dialog input[type="password"] { width: 100%; padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--editor-bg); color: var(--text-color); font-size: 15px; transition: all 0.2s ease; } .password-dialog input[type="password"]:hover { border-color: var(--secondary-color); } .password-dialog input[type="password"]:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px var(--primary-color-alpha); } .password-dialog-message { margin-bottom: 20px; font-size: 0.95em; color: #e74c3c; min-height: 20px; transition: all 0.3s ease; opacity: 0; display: flex; align-items: center; gap: 6px; } .password-dialog-message::before { content: '⚠️'; font-size: 1.1em; } .password-dialog-message.success { color: #2ecc71; } .password-dialog-message.success::before { content: '✅'; } .password-dialog-message.show { opacity: 1; } .password-dialog-buttons { display: flex; justify-content: flex-end; gap: 12px; } .password-dialog button { padding: 8px 16px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--editor-bg); color: var(--text-color); cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; } .password-dialog button:hover { background: var(--hover-color); border-color: var(--secondary-color); } .password-dialog button.primary { background: var(--primary-color); color: white; border-color: var(--primary-color); } .password-dialog button.primary:hover { opacity: 0.9; transform: translateY(-1px); } .password-dialog button:active { transform: translateY(1px); } .password-dialog-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); z-index: 999; transition: all 0.3s ease; } .password-protected .editor-container, .password-protected .toolbar { filter: blur(8px); pointer-events: none; user-select: none; } .password-status { display: flex; align-items: center; gap: 8px; color: var(--text-color); font-size: 0.9rem; padding: 4px 8px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } .password-status:hover { background: var(--hover-color); } .password-status-icon { font-size: 1.2rem; transition: transform 0.3s ease; } .password-status:hover .password-status-icon { transform: scale(1.1); } @media (max-width: 768px) { .password-dialog { width: 90%; padding: 20px; /* 保持在屏幕中间 */ top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0; } .password-dialog h3 { font-size: 1.1em; } .password-dialog input[type="password"] { font-size: 14px; padding: 8px 12px; } .password-dialog button { padding: 7px 14px; } } @media (max-width: 480px) { .password-dialog { width: 100%; max-width: none; border-radius: 12px 12px 0 0; bottom: 0; top: auto; transform: translateX(-50%); padding-bottom: calc(20px + env(safe-area-inset-bottom)); } } /* 工具栏右侧按钮样式 */ .toolbar-right { display: flex; align-items: center; gap: 8px; } .toolbar-button { display: flex; align-items: center; gap: 8px; color: var(--text-color); font-size: 0.9rem; padding: 4px 8px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; border: none; background: none; } .toolbar-button:hover { background: var(--hover-color); } .toolbar-button .icon { font-size: 1.2rem; transition: transform 0.3s ease; } .toolbar-button:hover .icon { transform: scale(1.1); } .toolbar-button .label { display: none; } @media (min-width: 768px) { .toolbar-button { padding: 6px 12px; } .toolbar-button .label { display: inline; } } /* 主题切换按钮特定样式 */ .theme-toggle .sun-icon, .theme-toggle .moon-icon { display: none; } [data-theme="dark"] .moon-icon { display: block; } [data-theme="light"] .sun-icon { display: block; } /* 密码状态按钮特定样式 */ .password-status { display: flex; align-items: center; gap: 8px; color: var(--text-color); font-size: 0.9rem; padding: 4px 8px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } .password-status:hover { background: var(--hover-color); } .password-status .icon { font-size: 1.2rem; transition: transform 0.3s ease; } .password-status:hover .icon { transform: scale(1.1); } /* 添加 Toast 提示框样式 */ .toast-container { position: fixed; top: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 10px; pointer-events: none; /* 保持在右上角 */ top: 20px; right: 20px; width: auto; max-width: calc(100% - 40px); transform: none; } .toast { background: var(--container-bg); color: var(--text-color); padding: 12px 24px; border-radius: 8px; box-shadow: 0 4px 12px var(--shadow-color); font-size: 14px; display: flex; align-items: center; gap: 8px; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; border: 1px solid var(--border-color); pointer-events: all; max-width: 300px; word-break: break-word; } .toast.show { opacity: 1; transform: translateX(0); } .toast.success { border-left: 4px solid #4caf50; } .toast.error { border-left: 4px solid #f44336; } .toast.info { border-left: 4px solid #2196f3; } .toast.warning { border-left: 4px solid #ff9800; } .toast-icon { font-size: 18px; flex-shrink: 0; } .toast-message { flex: 1; margin-right: 8px; } .toast-close { cursor: pointer; opacity: 0.7; transition: opacity 0.2s ease; padding: 4px; margin: -4px; border-radius: 4px; } .toast-close:hover { opacity: 1; background: var(--hover-color); } @media (max-width: 768px) { .toast-container { /* 保持在右上角 */ top: 20px; right: 20px; width: auto; max-width: calc(100% - 40px); transform: none; } .toast { width: 100%; transform: translateY(100%); } .toast.show { transform: translateY(0); } } /* 添加复制全部按钮样式 */ .copy-all-button { background: none; border: none; color: var(--text-color); cursor: pointer; padding: 0px 4px; border-radius: 4px; font-size: 12px; display: flex; align-items: center; gap: 4px; transition: all 0.2s ease; } .copy-all-button:hover { background: var(--hover-color); } /* 添加复制全部的动效样式 */ .copy-all-item { cursor: pointer; transition: all 0.2s ease; padding: 0px 4px; border-radius: 4px; display: flex; align-items: center; gap: 4px; } .copy-all-item:hover { background: var(--hover-color); } .copy-all-item .copy-icon { transition: transform 0.2s ease; } .copy-all-item:hover .copy-icon { transform: scale(1.1); } .copy-all-item.copied { animation: copied-animation 0.5s ease; } @keyframes copied-animation { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } /* 表格样式 */ .content table { width: 100%; border-collapse: collapse; margin: 1.5em 0; overflow-x: auto; display: block; } .content th, .content td { border: 1px solid var(--border-color); padding: 8px 12px; text-align: left; } .content th { background-color: var(--hover-color); font-weight: 600; } .content tr:nth-child(even) { background-color: var(--editor-bg); } .content tr:hover { background-color: var(--hover-color); } /* 移动端表格适配 */ @media (max-width: 768px) { .content table { font-size: 14px; } .content th, .content td { padding: 6px 8px; } } /* 表格滚动条样式 */ .content table::-webkit-scrollbar { height: 8px; width: 8px; } .content table::-webkit-scrollbar-track { background: transparent; } .content table::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 4px; border: 2px solid var(--editor-bg); } .content table::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* 表格滚动条样式 */ .content table::-webkit-scrollbar { height: 8px; width: 8px; } .content table::-webkit-scrollbar-track { background: transparent; } .content table::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 4px; border: 2px solid var(--editor-bg); } .content table::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* 图片样式 */ .content img { max-width: 100%; height: auto; margin: 1em 0; border-radius: 8px; display: block; box-shadow: 0 2px 8px var(--shadow-color); transition: all 0.3s ease; opacity: 0; animation: fadeIn 0.5s ease forwards; } /* 图片容器 */ .content p:has(img) { text-align: center; margin: 2em 0; } /* 图片悬停效果 */ .content img:hover { transform: scale(1.01); box-shadow: 0 4px 12px var(--shadow-color); } /* 图片标题样式 */ .content img + em { display: block; text-align: center; color: var(--secondary-color); font-size: 0.9em; margin-top: 0.5em; } /* 移动端图片适配 */ @media (max-width: 768px) { .content img { border-radius: 6px; margin: 0.8em 0; } .content p:has(img) { margin: 1.5em 0; } /* 禁用移动端图片缩放动画 */ .content img:hover { transform: none; box-shadow: 0 2px 8px var(--shadow-color); } } /* 图片加载动画 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* 图片加载失败样式 */ .content img:not([src]), .content img[src=""], .content img[src*="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="] { position: relative; min-height: 100px; background: var(--editor-bg); border: 1px dashed var(--border-color); display: flex; align-items: center; justify-content: center; } .content img:not([src])::after, .content img[src=""]::after, .content img[src*="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="]::after { content: "图片加载失败"; position: absolute; color: var(--secondary-color); font-size: 0.9em; } /* 大图查看模式 */ .content img.enlarged { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 90vw; max-height: 90vh; object-fit: contain; z-index: 1000; cursor: zoom-out; margin: 0; padding: 0; background: var(--bg-color); box-shadow: 0 0 20px var(--shadow-color); } /* 大图查看遮罩层 */ .image-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); z-index: 999; display: none; opacity: 0; transition: opacity 0.3s ease; } .image-overlay.active { display: block; opacity: 1; } /* 代码块复制按钮样式 */ .content pre { position: relative; } .copy-button { position: absolute; top: 0.5em; right: 0.5em; padding: 0.2em 0.5em; font-size: 0.85em; color: var(--text-color); background: var(--container-bg); border: 1px solid var(--border-color); border-radius: 3px; cursor: pointer; opacity: 0; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px; z-index: 2; } .content pre:hover .copy-button { opacity: 0.8; } .copy-button:hover { opacity: 1 !important; background: var(--hover-color); transform: translateY(-1px); } .copy-button.copied { color: #4caf50; border-color: #4caf50; opacity: 1; } .copy-button.error { color: #f44336; border-color: #f44336; opacity: 1; } /* 代码块语言标签 */ .content pre::before { content: attr(data-language); position: absolute; top: 0; right: 0; padding: 0.2em 0.5em; font-size: 0.85em; background: var(--container-bg); border-bottom-left-radius: 4px; color: var(--secondary-color); opacity: 0.8; transition: opacity 0.2s ease; } .content pre:hover::before { opacity: 0; } @media (max-width: 768px) { .copy-button { padding: 0.15em 0.4em; font-size: 0.8em; } .content pre::before { font-size: 0.8em; padding: 0.15em 0.4em; } } .info-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px; background: var(--editor-bg); border-radius: 12px; margin-bottom: 20px; font-size: 14px; border: 1px solid var(--border-color); overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 15px; min-width: 0; } .info-bar::-webkit-scrollbar { height: 6px; width: 6px; } .info-bar::-webkit-scrollbar-track { background: transparent; } .info-bar::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 3px; } .info-bar::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } .info-left { display: flex; gap: 15px; flex-shrink: 0; margin-right: auto; } .info-right { display: flex; align-items: center; gap: 15px; flex-shrink: 0; margin-left: auto; } .info-item { display: flex; align-items: center; gap: 8px; padding: 4px 10px; border-radius: 6px; transition: all 0.2s ease; white-space: nowrap; flex-shrink: 0; } .info-item:hover { background: var(--hover-color); } .theme-toggle { display: flex; align-items: center; gap: 8px; padding: 4px 10px; border-radius: 6px; border: none; color: var(--text-color); cursor: pointer; font-size: 14px; transition: all 0.2s ease; background: transparent; white-space: nowrap; flex-shrink: 0; } .theme-toggle:hover { background: var(--hover-color); } .theme-toggle:active { transform: scale(0.95); } .theme-toggle .sun-icon, .theme-toggle .moon-icon { display: none; font-size: 1.2rem; } .theme-toggle .label { font-size: 14px; } [data-theme="dark"] .moon-icon { display: block; } [data-theme="light"] .sun-icon { display: block; } @media (max-width: 768px) { .theme-toggle { padding: 4px 8px; } .theme-toggle .label { font-size: 13px; } } @media (max-width: 768px) { .info-bar { padding: 10px; gap: 10px; } .info-left, .info-right { gap: 10px; } .info-item { padding: 4px 8px; font-size: 13px; } } /* 添加KaTeX相关样式 */ .katex-display { overflow-x: auto; overflow-y: hidden; padding: 1em 0; margin: 1em 0; } .katex-display::-webkit-scrollbar { height: 6px; } .katex-display::-webkit-scrollbar-track { background: transparent; } .katex-display::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 3px; } .katex-display::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* Mermaid图表样式 */ .mermaid { margin: 1.5em 0; text-align: center; background: var(--editor-bg); padding: 1em; border-radius: 8px; border: 1px solid var(--border-color); overflow-x: auto; } .mermaid svg { max-width: 100%; height: auto; } /* 深色模式下的Mermaid样式 */ [data-theme="dark"] .mermaid { --mermaid-bg: var(--editor-bg); --mermaid-fg: var(--text-color); --mermaid-edge: var(--text-color); --mermaid-label: var(--text-color); --mermaid-cluster: var(--border-color); } [data-theme="dark"] .mermaid .node rect, [data-theme="dark"] .mermaid .node circle, [data-theme="dark"] .mermaid .node ellipse, [data-theme="dark"] .mermaid .node polygon, [data-theme="dark"] .mermaid .node path { fill: var(--mermaid-bg); stroke: var(--mermaid-fg); } [data-theme="dark"] .mermaid .edgePath .path { stroke: var(--mermaid-edge) !important; } [data-theme="dark"] .mermaid .edgeLabel { color: var(--mermaid-label); background-color: var(--mermaid-bg); } [data-theme="dark"] .mermaid .cluster rect { fill: var(--mermaid-bg) !important; stroke: var(--mermaid-cluster) !important; } [data-theme="dark"] .mermaid .label { color: var(--mermaid-label); } [data-theme="dark"] .mermaid .node .label { color: var(--mermaid-label); } [data-theme="dark"] .mermaid marker { fill: var(--mermaid-edge); } </style> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/emoji-toolkit@7.0.0/lib/js/joypixels.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emoji-toolkit@7.0.0/extras/css/joypixels.min.css"> </head> <body> <div class="container"> <div class="toolbar"> <div class="toolbar-left"> <div class="note-name" onclick="copyNoteLink()" title="点击复制链接"> 📋 ${noteName} </div> <div class="toolbar-divider"></div> <!-- 字体和字号选择器 --> <select id="font-family" class="toolbar-select" onchange="applyFont()"> <option value="default">默认字体</option> <option value="serif">宋体</option> <option value="yahei">微软雅黑</option> <option value="kaiti">楷体</option> <option value="heiti">黑体</option> <option value="fangsong">仿宋</option> <option value="songti">新宋体</option> <option value="monospace">等宽字体</option> <option value="arial">Arial</option> <option value="times">Times New Roman</option> <option value="helvetica">Helvetica</option> </select> <select id="font-size" class="toolbar-select" onchange="applyFontSize()"> <option value="12">12px</option> <option value="13">13px</option> <option value="14">14px</option> <option value="15">15px</option> <option value="16" selected>16px</option> <option value="17">17px</option> <option value="18">18px</option> <option value="20">20px</option> <option value="22">22px</option> <option value="24">24px</option> <option value="26">26px</option> <option value="28">28px</option> <option value="32">32px</option> </select> <div class="toolbar-divider"></div> <!-- Markdown 工具栏 --> <div class="markdown-toolbar"> <button onclick="applyMarkdown('bold')" title="粗体 Ctrl+B">B</button> <button onclick="applyMarkdown('italic')" title="斜体 Ctrl+I">I</button> <button onclick="applyMarkdown('heading')" title="标题 Ctrl+H">H</button> <button onclick="applyMarkdown('strikethrough')" title="删除线 Ctrl+D">S</button> <button onclick="applyMarkdown('list')" title="无序列表 Ctrl+U">•</button> <button onclick="applyMarkdown('ordered-list')" title="有序列表 Ctrl+O">1.</button> <button onclick="applyMarkdown('task')" title="任务列表 Ctrl+T">☐</button> <button onclick="applyMarkdown('quote')" title="引用 Ctrl+Q">""</button> <button onclick="applyMarkdown('code')" title="代码 Ctrl+K">{}</button> <button onclick="applyMarkdown('table')" title="表格">⚏</button> <button onclick="applyMarkdown('divider')" title="分割线">—</button> <button onclick="applyMarkdown('link')" title="链接 Ctrl+L">🔗</button> <button onclick="applyMarkdown('image')" title="图片 Ctrl+P">🖼</button> <button onclick="showEmojiPicker()" title="表情">😊</button> <div class="toolbar-divider"></div> <button onclick="applyMarkdown('latex-inline')" title="行内公式">∑</button> <button onclick="applyMarkdown('latex-block')" title="公式块">∫</button> <button onclick="applyMarkdown('mermaid')" title="流程图">📊</button> </div> <span id="save-status"></span> </div> <div class="toolbar-right"> <div class="password-status toolbar-button" onclick="showPasswordDialog()" title="密码保护设置"> <span class="icon" id="password-status-icon">🔓</span> <span class="label">密码保护</span> </div> <button onclick="toggleDarkMode()" class="toolbar-button theme-toggle" title="切换主题"> <div class="icon"> <span class="sun-icon">☀️</span> <span class="moon-icon">🌙</span> </div> <span class="label">主题</span> </button> <div class="share-button toolbar-button" onclick="shareNote()" title="分享笔记"> <span class="icon">📤</span> <span class="label">分享</span> </div> </div> </div> <div class="editor-container"> <div class="editor-wrapper"> <div class="editor-main"> <div class="line-numbers"></div> <textarea id="content" placeholder="开始输入笔记内容..." onscroll="handleEditorScroll()">${noteContent}</textarea> </div> <div class="status-bar"> <div class="status-left"> <div class="status-item"> <label title="显示/隐藏行号"> <input type="checkbox" id="line-numbers-toggle" checked onchange="toggleLineNumbers()"> 行号 </label> </div> <div class="status-item"> <label title="显示/隐藏工具栏"> <input type="checkbox" id="toolbar-toggle" checked onchange="toggleToolbar()"> 工具栏 </label> </div> <div class="status-item"> <label class="switch-label"> <input type="checkbox" id="preview-toggle" onchange="togglePreview()"> 预览 </label> </div> </div> <div class="status-right"> <div class="status-item copy-all-item" onclick="copyAllContent()" title="复制全部内容"> <span class="copy-icon">📋</span> <span>复制全部</span> </div> <div class="status-item"> <span>字数:</span> <span id="char-count">0</span> </div> <div class="status-item"> <span>单词:</span> <span id="word-count">0</span> </div> <div class="status-item"> <span>行:</span> <span id="line-count">1</span> </div> <div class="status-item"> <span>列:</span> <span id="column-count">1</span> </div> </div> </div> </div> <div id="preview" class="preview-container"> <div class="preview-content" onscroll="handlePreviewScroll()" ondblclick="handlePreviewDoubleTap(event)"></div> <div class="preview-status-bar"> <div class="preview-status-left"> <button onclick="togglePreviewFullscreen()" class="fullscreen-toggle" title="切换全屏"> <span class="fullscreen-icon" id="fullscreen-icon">⛶</span> </button> <div class="preview-status-item"> <label class="switch-label"> <input type="checkbox" id="sync-scroll-toggle" onchange="toggleSyncScroll()"> 同步滚动 </label> </div> </div> <div class="preview-status-right"> <div class="preview-status-item"> <span>字数:</span> <span id="preview-char-count">0</span> </div> <div class="preview-status-item"> <span>单词:</span> <span id="preview-word-count">0</span> </div> <div class="preview-status-item"> <span>段落:</span> <span id="preview-paragraph-count">0</span> </div> </div> </div> </div> </div> </div> <div id="emoji-picker" class="emoji-picker"></div> <!-- 添加密码对话框 --> <div class="password-dialog-overlay" id="password-overlay"></div> <div class="password-dialog" id="password-dialog"> <h3 id="password-dialog-title">设置密码保护</h3> <input type="password" id="password-input" placeholder="请输入密码" autocomplete="new-password"> <div class="password-dialog-message" id="password-message"></div> <div class="password-dialog-buttons"> <button onclick="closePasswordDialog()"> <span>取消</span> </button> <button class="primary" onclick="handlePasswordAction()" id="password-action-btn"> <span>确定</span> </button> </div> </div> <!-- 添加 Toast 容器 --> <div class="toast-container" id="toast-container"></div> <div class="image-overlay" id="imageOverlay" onclick="closeEnlargedImage()"></div> <script> const content = document.getElementById('content'); const preview = document.getElementById('preview'); const previewToggle = document.getElementById('preview-toggle'); const syncScrollToggle = document.getElementById('sync-scroll-toggle'); const saveStatus = document.getElementById('save-status'); let saveTimeout; let isEditorScrolling = false; let isPreviewScrolling = false; let isSyncScrollEnabled = true; let isEmptyNote = false; // Flag to track if the note is empty and space was added document.addEventListener('DOMContentLoaded', () => { // 配置 marked marked.setOptions({ gfm: true, breaks: true, tables: true, headerIds: true, mangle: false, sanitize: false, smartLists: true, smartypants: true, xhtml: false, langPrefix: 'language-', pedantic: false, highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (err) {} } try { return hljs.highlightAuto(code).value; } catch (err) {} return code; } }); // 自定义 emoji 渲染 const renderer = new marked.Renderer(); const originalText = renderer.text.bind(renderer); renderer.text = (text) => { return joypixels.shortnameToImage(originalText(text)); }; marked.setOptions({ renderer }); // 初始化主题 const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); // 初始化预览状态 const showPreview = localStorage.getItem('preview') === 'true'; previewToggle.checked = showPreview; // 初始化同步滚动状态 const savedSyncScroll = localStorage.getItem('sync-scroll'); isSyncScrollEnabled = savedSyncScroll !== 'false'; syncScrollToggle.checked = isSyncScrollEnabled; const editorContainer = document.querySelector('.editor-container'); editorContainer.classList.toggle('preview-mode', showPreview); preview.style.display = showPreview ? 'block' : 'none'; if (showPreview) { updatePreview(content.value); } content.addEventListener('input', () => { updatePreview(content.value); // If the content becomes empty, insert a space to trigger the save if (content.value.trim().length === 0) { if (!isEmptyNote) { content.value = ' '; // Automatically add a space if content is empty saveStatus.textContent = 'Note will be deleted'; // Notify the user that the note will be deleted isEmptyNote = true; // Flag that space was added } } else { // If content is no longer empty, remove the space and update status if (isEmptyNote) { content.value = content.value.trim(); // Remove the space if user starts typing again isEmptyNote = false; // Reset the flag } saveStatus.textContent = 'Saving...'; // Show saving status while content is being edited } debounceSaveContent(content.value); }); window.addEventListener('resize', () => { if (previewToggle.checked) { syncScrollPositions('editor'); } }); // 初始化行号显示状态 const showLineNumbers = localStorage.getItem('show-line-numbers') !== 'false'; document.getElementById('line-numbers-toggle').checked = showLineNumbers; const lineNumbers = document.querySelector('.line-numbers'); lineNumbers.classList.toggle('hidden', !showLineNumbers); // 初始化行号和文本统计 updateLineNumbers(); updateTextStats(); // 初始化工具栏显示状态 const showToolbar = localStorage.getItem('show-toolbar') !== 'false'; document.getElementById('toolbar-toggle').checked = showToolbar; const toolbar = document.querySelector('.toolbar'); const container = document.querySelector('.container'); toolbar.classList.toggle('hidden', !showToolbar); container.classList.toggle('toolbar-hidden', !showToolbar); // 恢复全屏状态 const savedFullscreen = localStorage.getItem('preview-fullscreen') === 'true'; if (savedFullscreen) { togglePreviewFullscreen(); } // 添加 Firefox 双击事件监听 const previewContent = document.querySelector('.preview-content'); if (previewContent) { previewContent.addEventListener('mousedown', (e) => { if (e.detail === 2) { // 检测双击 e.preventDefault(); // 阻止默认的文本选择 } }); } // 添加工具栏鼠标滚动支持 let isMouseDown = false; let startX; let scrollLeft; toolbar.addEventListener('mousedown', (e) => { isMouseDown = true; toolbar.classList.add('dragging'); startX = e.pageX - toolbar.offsetLeft; scrollLeft = toolbar.scrollLeft; }); toolbar.addEventListener('mouseleave', () => { isMouseDown = false; toolbar.classList.remove('dragging'); }); toolbar.addEventListener('mouseup', () => { isMouseDown = false; toolbar.classList.remove('dragging'); }); toolbar.addEventListener('mousemove', (e) => { if (!isMouseDown) return; e.preventDefault(); const x = e.pageX - toolbar.offsetLeft; const walk = (x - startX) * 2; toolbar.scrollLeft = scrollLeft - walk; }); // 支持鼠标滚轮横向滚动 toolbar.addEventListener('wheel', (e) => { e.preventDefault(); toolbar.scrollLeft += e.deltaY; }); }); function togglePreview() { const showPreview = previewToggle.checked; const editorContainer = document.querySelector('.editor-container'); editorContainer.classList.toggle('preview-mode', showPreview); preview.style.display = showPreview ? 'block' : 'none'; localStorage.setItem('preview', showPreview); if (showPreview) { updatePreview(content.value); setTimeout(() => syncScrollPositions('editor'), 100); } } function toggleSyncScroll() { isSyncScrollEnabled = syncScrollToggle.checked; localStorage.setItem('sync-scroll', isSyncScrollEnabled); } function updatePreview(text) { if (previewToggle.checked) { const previewContent = preview.querySelector('.preview-content'); const scrollPos = previewContent.scrollTop; // 渲染Markdown内容 previewContent.innerHTML = marked.parse(text); // 渲染LaTeX公式 renderMathInElement(previewContent, { delimiters: [ {left: '$$', right: '$$', display: true}, {left: '$', right: '$', display: false}, // 移除普通方括号作为数学公式的标记 // {left: '\\[', right: '\\]', display: true}, // 替换为更明确的数学公式标记 {left: 'math\\[', right: '\\]', display: true}, {left: '\\(', right: '\\)', display: false}, {left: '\\begin{align}', right: '\\end{align}', display: true}, // 添加对 align 环境的支持 ], throwOnError: false, output: 'html' }); // 渲染Mermaid图表 mermaid.initialize({ startOnLoad: false, theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'default', securityLevel: 'loose', fontFamily: 'var(--font-family)', }); const mermaidDiagrams = previewContent.querySelectorAll('pre code.language-mermaid'); mermaidDiagrams.forEach(async (diagram, index) => { try { const pre = diagram.parentElement; const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.id = 'mermaid-' + Date.now() + '-' + index; // 添加唯一ID mermaidDiv.textContent = diagram.textContent; pre.parentNode.replaceChild(mermaidDiv, pre); } catch (error) { console.error('Mermaid渲染错误:', error); } }); // 等待所有图表渲染完成 if (mermaidDiagrams.length > 0) { mermaid.run(); } // 更新预览区统计信息 updatePreviewStats(text); // 为所有代码块添加复制按钮和语言标签 const codeBlocks = previewContent.querySelectorAll('pre code'); codeBlocks.forEach(code => { const pre = code.parentElement; // 跳过Mermaid图表 if (code.classList.contains('language-mermaid')) return; // 获取语言类名 const langClass = Array.from(code.classList).find(cl => cl.startsWith('language-')); const language = langClass ? langClass.replace('language-', '') : '代码'; // 设置语言标签 pre.setAttribute('data-language', language); // 添加复制按钮 if (!pre.querySelector('.copy-button')) { const button = document.createElement('button'); button.className = 'copy-button'; button.innerHTML = '📋 复制'; button.onclick = (e) => { e.preventDefault(); copyToClipboard(code.innerText, button); }; pre.appendChild(button); } }); // 重新应用代码高亮 hljs.highlightAll(); previewContent.scrollTop = scrollPos; } } async function copyToClipboard(text, button) { try { await navigator.clipboard.writeText(text); const originalText = button.innerHTML; button.innerHTML = '✅ 已复制'; button.classList.add('copied'); setTimeout(() => { button.innerHTML = originalText; button.classList.remove('copied'); }, 2000); } catch (err) { console.error('复制失败:', err); button.innerHTML = '❌ 复制失败'; setTimeout(() => { button.innerHTML = '📋 复制'; }, 2000); } } function handleEditorScroll() { if (!isPreviewScrolling && previewToggle.checked && isSyncScrollEnabled) { isEditorScrolling = true; syncScrollPositions('editor'); setTimeout(() => { isEditorScrolling = false; }, 50); } } function handlePreviewScroll() { if (!isEditorScrolling && previewToggle.checked && isSyncScrollEnabled) { isPreviewScrolling = true; const previewContent = preview.querySelector('.preview-content'); const editorHeight = content.scrollHeight - content.clientHeight; const previewHeight = previewContent.scrollHeight - previewContent.clientHeight; if (previewHeight > 0) { const scrollPercentage = previewContent.scrollTop / previewHeight; content.scrollTop = scrollPercentage * editorHeight; } setTimeout(() => { isPreviewScrolling = false; }, 50); } } function syncScrollPositions(source) { if (!previewToggle.checked || !isSyncScrollEnabled) return; const editor = content; const previewContent = preview.querySelector('.preview-content'); const editorHeight = editor.scrollHeight - editor.clientHeight; const previewHeight = previewContent.scrollHeight - previewContent.clientHeight; if (source === 'editor' && editorHeight > 0) { const scrollPercentage = editor.scrollTop / editorHeight; previewContent.scrollTop = scrollPercentage * previewHeight; } else if (source === 'preview' && previewHeight > 0) { const scrollPercentage = previewContent.scrollTop / previewHeight; editor.scrollTop = scrollPercentage * editorHeight; } } function debounceSaveContent(text) { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => saveContent(text), 1000); } async function saveContent(text) { try { const response = await fetch(window.location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'text=' + encodeURIComponent(text), }); saveStatus.textContent = response.status === 204 ? '已保存' : '笔记将被删除'; } catch (error) { saveStatus.textContent = '保存失败'; } setTimeout(() => { saveStatus.textContent = ''; }, 2000); } // 添加 Toast 提示框功能 function showToast(message, type = 'info', duration = 3000) { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = 'toast ' + type; // 设置图标 const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' }; toast.innerHTML = '<span class="toast-icon">' + (icons[type] || icons.info) + '</span>' + '<span class="toast-message">' + message + '</span>' + '<span class="toast-close" onclick="this.parentElement.remove()">✕</span>'; container.appendChild(toast); // 触发重排以启动动画 void toast.offsetWidth; toast.classList.add('show'); // 自动关闭 setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } // 替换原有的 alert 调用 function copyNoteLink() { const link = window.location.href; navigator.clipboard.writeText(link).then(() => { showToast('笔记链接已复制到剪贴板!', 'success'); }).catch(() => { showToast('复制失败,请手动复制链接。', 'error'); }); } function toggleDarkMode() { const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); // 更新Mermaid主题 mermaid.initialize({ startOnLoad: false, theme: theme === 'dark' ? 'dark' : 'default', securityLevel: 'loose', fontFamily: 'var(--font-family)', }); // 重新渲染预览内容以更新Mermaid图表 if (previewToggle.checked) { updatePreview(content.value); } } // Markdown 编辑功能 function applyMarkdown(type) { const textarea = document.getElementById('content'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; let result; switch(type) { case 'bold': result = insertAround(text, start, end, '**'); break; case 'italic': result = insertAround(text, start, end, '_'); break; case 'heading': result = insertAtLineStart(text, start, '# '); break; case 'strikethrough': result = insertAround(text, start, end, '~~'); break; case 'list': result = insertAtLineStart(text, start, '- '); break; case 'ordered-list': result = insertAtLineStart(text, start, '1. '); break; case 'task': result = insertAtLineStart(text, start, '- [ ] '); break; case 'quote': result = insertAtLineStart(text, start, '> '); break; case 'code': result = insertAround(text, start, end, '\`\`\`\\n', '\\n\`\`\`'); break; case 'table': result = insertTable(text, start); break; case 'divider': result = insertDivider(text, start); break; case 'link': result = insertLink(text, start, end); break; case 'image': result = insertImage(text, start, end); break; case 'latex-inline': result = insertAround(text, start, end, '$'); break; case 'latex-block': result = insertAround(text, start, end, '$$\\n', '\\n$$'); break; case 'mermaid': result = insertMermaid(text, start); break; } if (result) { textarea.value = result.text; textarea.selectionStart = result.selectionStart; textarea.selectionEnd = result.selectionEnd; textarea.focus(); updatePreview(textarea.value); debounceSaveContent(textarea.value); } } // 辅助函数:在选中文本周围插入标记 function insertAround(text, start, end, mark, endMark = mark) { const selection = text.substring(start, end); const before = text.substring(0, start); const after = text.substring(end); const newText = before + mark + selection + endMark + after; return { text: newText, selectionStart: start + mark.length, selectionEnd: end + mark.length }; } // 辅助函数:在行首插入标记 function insertAtLineStart(text, start, mark) { const lines = text.split('\\n'); let currentPos = 0; let targetLine = 0; // 找到光标所在行 for (let i = 0; i < lines.length; i++) { if (currentPos + lines[i].length >= start) { targetLine = i; break; } currentPos += lines[i].length + 1; } // 在目标行前添加标记 lines[targetLine] = mark + lines[targetLine]; return { text: lines.join('\\n'), selectionStart: currentPos + mark.length, selectionEnd: currentPos + mark.length }; } // 插入表格 function insertTable(text, start) { const tableTemplate = '\\n| 标题1 | 标题2 | 标题3 |\\n|--------|--------|--------|\\n| 内容1 | 内容2 | 内容3 |\\n'; const newText = text.substring(0, start) + tableTemplate + text.substring(start); return { text: newText, selectionStart: start + tableTemplate.length, selectionEnd: start + tableTemplate.length }; } // 添加插入Mermaid图表的函数 function insertMermaid(text, start) { const mermaidTemplate = '\\n\`\`\`mermaid\\ngraph TD\\n A[开始] --> B[步骤1]\\n B --> C[步骤2]\\n C --> D[结束]\\n\`\`\`\\n'; const newText = text.substring(0, start) + mermaidTemplate + text.substring(start); return { text: newText, selectionStart: start + mermaidTemplate.length, selectionEnd: start + mermaidTemplate.length }; } // 插入链接 function insertLink(text, start, end) { const selection = text.substring(start, end).trim(); const link = selection || '链接文字'; const template = '[' + link + '](https://)'; const newText = text.substring(0, start) + template + text.substring(end); return { text: newText, selectionStart: start + link.length + 3, selectionEnd: start + template.length - 1 }; } // 插入图片 function insertImage(text, start, end) { const template = '![图片描述](https://)'; return { text: text.substring(0, start) + template + text.substring(end), selectionStart: start + 2, selectionEnd: start + 6 }; } // 插入分割线 function insertDivider(text, start) { const divider = '\\n---\\n'; const newText = text.substring(0, start) + divider + text.substring(start); return { text: newText, selectionStart: start + divider.length, selectionEnd: start + divider.length }; } // 字体设置 function applyFont() { const select = document.getElementById('font-family'); const textarea = document.getElementById('content'); const fontMap = { 'default': '', 'serif': 'SimSun, serif', 'yahei': '"Microsoft YaHei", "微软雅黑", sans-serif', 'kaiti': 'KaiTi, "楷体", serif', 'heiti': 'SimHei, "黑体", sans-serif', 'fangsong': 'FangSong, "仿宋", serif', 'songti': 'NSimSun, "新宋体", serif', 'monospace': 'Monaco, Consolas, monospace', 'arial': 'Arial, sans-serif', 'times': '"Times New Roman", Times, serif', 'helvetica': 'Helvetica, Arial, sans-serif' }; textarea.style.fontFamily = fontMap[select.value] || ''; } // 字号设置 function applyFontSize() { const select = document.getElementById('font-size'); const textarea = document.getElementById('content'); textarea.style.fontSize = select.value + 'px'; } // Emoji 选择器 function showEmojiPicker() { const picker = document.getElementById('emoji-picker'); const overlay = document.querySelector('.emoji-picker-overlay') || createOverlay(); if (picker.style.display === 'block') { picker.style.display = 'none'; overlay.style.display = 'none'; return; } // 如果是第一次显示,初始化表情列表 if (!picker.children.length) { const gridContainer = document.createElement('div'); gridContainer.className = 'emoji-picker-grid'; const emojis = ['😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩', '🥳', '😏', '😒', '❤️', '🙌', '👍', '🎉', '✨', '🔥', '💡', '⭐', '💪', '🎯', '✅', '❌', '💬', '👀', '🎨', '🎮', '🎵', '🎬', '📚', '💻', '🔍', '⚡', '🌈', '🍀']; emojis.forEach(emoji => { const button = document.createElement('button'); button.textContent = emoji; button.onclick = () => { insertEmoji(emoji); picker.style.display = 'none'; overlay.style.display = 'none'; }; gridContainer.appendChild(button); }); picker.appendChild(gridContainer); } overlay.style.display = 'block'; picker.style.display = 'block'; } // 创建遮罩层 function createOverlay() { const overlay = document.createElement('div'); overlay.className = 'emoji-picker-overlay'; document.body.appendChild(overlay); overlay.addEventListener('click', () => { const picker = document.getElementById('emoji-picker'); picker.style.display = 'none'; overlay.style.display = 'none'; }); return overlay; } // 插入表情 function insertEmoji(emoji) { const textarea = document.getElementById('content'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; textarea.value = text.substring(0, start) + emoji + text.substring(end); textarea.selectionStart = textarea.selectionEnd = start + emoji.length; textarea.focus(); updatePreview(textarea.value); debounceSaveContent(textarea.value); } // 添加快捷键支持 document.addEventListener('keydown', function(e) { if (!e.ctrlKey) return; const shortcuts = { 'b': 'bold', 'i': 'italic', 'h': 'heading', 'd': 'strikethrough', 'u': 'list', 'o': 'ordered-list', 't': 'task', 'q': 'quote', 'k': 'code', 'l': 'link', 'p': 'image' }; if (shortcuts[e.key.toLowerCase()]) { e.preventDefault(); applyMarkdown(shortcuts[e.key.toLowerCase()]); } }); // 更新行号和调整布局 function updateLineNumbers() { const textarea = document.getElementById('content'); const lineNumbers = document.querySelector('.line-numbers'); const fullText = textarea.value; const totalLines = fullText.endsWith('\\n') ? fullText.split('\\n').length : fullText.split('\\n').length; // 清空现有行号 lineNumbers.innerHTML = ''; // 为每一行创建行号 span 元素 for (let i = 0; i < totalLines; i++) { const span = document.createElement('span'); span.textContent = i + 1; lineNumbers.appendChild(span); } // 确保至少有一行 if (totalLines === 0) { const span = document.createElement('span'); span.textContent = '1'; lineNumbers.appendChild(span); } // 调整行号区域宽度 const maxLineNumber = totalLines || 1; const minWidth = 28; // 最小宽度 const digitWidth = 8; // 每个数字的估计宽度 const newWidth = Math.max(minWidth, String(maxLineNumber).length * digitWidth + 8); lineNumbers.style.minWidth = newWidth + 'px'; } // 监听窗口大小变化 window.addEventListener('resize', () => { updateLineNumbers(); }); // 更新文本统计 function updateTextStats() { const textarea = document.getElementById('content'); const text = textarea.value; const position = textarea.selectionStart; // 计算字符数(不包括空格和换行) const charCount = text.replace(/\s/g, '').length; // 计算单词数 const wordCount = text.trim().split(/\s+/).filter(word => word.length > 0).length; // 计算当前行和列 const textBeforeCursor = text.substring(0, position); const lines = textBeforeCursor.split('\\n'); const currentLine = lines.length; const currentLineContent = lines[lines.length - 1]; const currentColumn = currentLineContent ? currentLineContent.length + 1 : 1; // 更新显示(使用更紧凑的格式) document.getElementById('char-count').textContent = String(charCount); document.getElementById('word-count').textContent = String(wordCount); document.getElementById('line-count').textContent = String(currentLine); document.getElementById('column-count').textContent = String(currentColumn); } // 同步滚动行号 function syncLineNumbersScroll() { const textarea = document.getElementById('content'); const lineNumbers = document.querySelector('.line-numbers'); // 计算最大可滚动高度 const maxScroll = textarea.scrollHeight - textarea.clientHeight; const currentScroll = textarea.scrollTop; // 确保不会滚动过头 if (currentScroll <= maxScroll) { lineNumbers.scrollTop = currentScroll; } } // 监听输入事件 content.addEventListener('input', () => { updatePreview(content.value); updateLineNumbers(); updateTextStats(); debounceSaveContent(content.value); // 输入时也同步滚动 syncLineNumbersScroll(); }); // 监听光标位置变化 content.addEventListener('keyup', updateTextStats); content.addEventListener('click', updateTextStats); content.addEventListener('scroll', syncLineNumbersScroll); // 切换行号显示/隐藏 function toggleLineNumbers() { const lineNumbers = document.querySelector('.line-numbers'); const isVisible = document.getElementById('line-numbers-toggle').checked; lineNumbers.classList.toggle('hidden', !isVisible); localStorage.setItem('show-line-numbers', isVisible); } // 切换工具栏显示/隐藏 function toggleToolbar() { const isVisible = document.getElementById('toolbar-toggle').checked; const toolbar = document.querySelector('.toolbar'); const container = document.querySelector('.container'); toolbar.classList.toggle('hidden', !isVisible); container.classList.toggle('toolbar-hidden', !isVisible); localStorage.setItem('show-toolbar', isVisible); // 触发一次窗口大小变化事件,以更新编辑器布局 window.dispatchEvent(new Event('resize')); } // 添加预览区统计功能 function updatePreviewStats(text) { // 计算字符数(不包括空格和换行) const charCount = text.replace(/\\s/g, '').length; // 计算单词数 const wordCount = text.trim().split(/\\s+/).filter(word => word.length > 0).length; // 计算段落数(通过空行分隔) const paragraphCount = text.split(/\\n\\s*\\n/).filter(para => para.trim().length > 0).length; // 更新显示 document.getElementById('preview-char-count').textContent = String(charCount); document.getElementById('preview-word-count').textContent = String(wordCount); document.getElementById('preview-paragraph-count').textContent = String(paragraphCount); } // 添加全屏切换功能 function togglePreviewFullscreen() { const preview = document.getElementById('preview'); const fullscreenIcon = document.getElementById('fullscreen-icon'); const isFullscreen = preview.classList.contains('fullscreen'); if (isFullscreen) { // 如果已经是容器全屏,切换到真实全屏 if (document.fullscreenElement) { document.exitFullscreen(); } else { preview.requestFullscreen(); } } else { // 首次点击进入容器全屏 preview.classList.add('fullscreen'); fullscreenIcon.textContent = '⛶'; } // 触发resize事件以更新布局 window.dispatchEvent(new Event('resize')); // 保存全屏状态到本地存储 localStorage.setItem('preview-fullscreen', preview.classList.contains('fullscreen')); } // ESC键退出全屏 function handleFullscreenEsc(e) { if (e.key === 'Escape') { const preview = document.getElementById('preview'); if (preview.classList.contains('fullscreen')) { preview.classList.remove('fullscreen'); const fullscreenIcon = document.getElementById('fullscreen-icon'); fullscreenIcon.textContent = '⛶'; localStorage.setItem('preview-fullscreen', false); } } } // 监听浏览器全屏变化事件 document.addEventListener('fullscreenchange', () => { const preview = document.getElementById('preview'); const fullscreenIcon = document.getElementById('fullscreen-icon'); if (!document.fullscreenElement && preview.classList.contains('fullscreen')) { // 从真实全屏退出时,也退出容器全屏 preview.classList.remove('fullscreen'); fullscreenIcon.textContent = '⛶'; localStorage.setItem('preview-fullscreen', false); } }); // 双击进入/退出全屏(移动端支持) function handlePreviewDoubleTap(e) { const preview = document.getElementById('preview'); // 确保不是在状态栏上双击 if (!e.target.closest('.preview-status-bar')) { e.preventDefault(); // 阻止默认行为 // 清除任何可能的文本选择 window.getSelection().removeAllRanges(); togglePreviewFullscreen(); } } // 添加密码相关功能 let isPasswordProtected = false; let isPasswordVerified = false; let currentPasswordAction = ''; async function checkPasswordProtection() { try { const response = await fetch(window.location.pathname + '/password-check', { method: 'GET', }); if (response.status === 200) { isPasswordProtected = true; document.body.classList.add('password-protected'); // 添加密码保护状态类 showPasswordVerification(); } updatePasswordStatus(); } catch (error) { console.error('检查密码保护状态失败:', error); } } function showPasswordMessage(message, isSuccess = false) { const messageEl = document.getElementById('password-message'); messageEl.textContent = message; messageEl.classList.toggle('success', isSuccess); messageEl.classList.add('show'); // 1秒后自动隐藏成功消息 if (isSuccess) { setTimeout(() => { messageEl.classList.remove('show'); }, 1000); } } function clearPasswordMessage() { const messageEl = document.getElementById('password-message'); messageEl.classList.remove('show', 'success'); messageEl.textContent = ''; } function showPasswordDialog() { const dialog = document.getElementById('password-dialog'); const overlay = document.getElementById('password-overlay'); const title = document.getElementById('password-dialog-title'); const actionBtn = document.getElementById('password-action-btn'); const passwordInput = document.getElementById('password-input'); clearPasswordMessage(); if (isPasswordProtected && isPasswordVerified) { title.textContent = '移除密码保护'; actionBtn.textContent = '移除'; currentPasswordAction = 'remove'; } else if (!isPasswordProtected) { title.textContent = '设置密码保护'; actionBtn.textContent = '设置'; currentPasswordAction = 'set'; } dialog.style.display = 'block'; overlay.style.display = 'block'; passwordInput.focus(); // 添加回车键事件监听 passwordInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handlePasswordAction(); } }; } function showPasswordVerification() { const dialog = document.getElementById('password-dialog'); const overlay = document.getElementById('password-overlay'); const title = document.getElementById('password-dialog-title'); const actionBtn = document.getElementById('password-action-btn'); const passwordInput = document.getElementById('password-input'); clearPasswordMessage(); title.textContent = '请输入密码'; actionBtn.textContent = '验证'; currentPasswordAction = 'verify'; dialog.style.display = 'block'; overlay.style.display = 'block'; passwordInput.focus(); // 添加回车键事件监听 passwordInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handlePasswordAction(); } }; } function closePasswordDialog() { const dialog = document.getElementById('password-dialog'); const overlay = document.getElementById('password-overlay'); const passwordInput = document.getElementById('password-input'); dialog.style.display = 'none'; overlay.style.display = 'none'; passwordInput.value = ''; passwordInput.onkeydown = null; // 移除回车键事件监听 clearPasswordMessage(); } async function handlePasswordAction() { const password = document.getElementById('password-input').value; if (!password) { showPasswordMessage('请输入密码'); showToast('请输入密码', 'warning'); return; } try { let response; switch (currentPasswordAction) { case 'set': response = await fetch(window.location.pathname + '/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: await hashPassword(password) }) }); if (response.status === 200) { isPasswordProtected = true; isPasswordVerified = true; document.body.classList.add('password-protected'); updatePasswordStatus(); showPasswordMessage('密码保护已设置', true); showToast('密码保护已设置', 'success'); setTimeout(closePasswordDialog, 1500); } break; case 'verify': response = await fetch(window.location.pathname + '/password-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: await hashPassword(password) }) }); if (response.status === 200) { isPasswordVerified = true; document.body.classList.remove('password-protected'); updatePasswordStatus(); showPasswordMessage('密码验证成功', true); showToast('密码验证成功', 'success'); setTimeout(closePasswordDialog, 1500); } else { showPasswordMessage('密码错误'); showToast('密码错误', 'error'); return; } break; case 'remove': response = await fetch(window.location.pathname + '/password', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: await hashPassword(password) }) }); if (response.status === 200) { isPasswordProtected = false; isPasswordVerified = false; document.body.classList.remove('password-protected'); updatePasswordStatus(); showPasswordMessage('密码保护已移除', true); showToast('密码保护已移除', 'success'); setTimeout(closePasswordDialog, 1500); } else { showPasswordMessage('密码错误'); showToast('密码错误', 'error'); return; } break; } } catch (error) { console.error('密码操作失败:', error); showPasswordMessage('操作失败,请重试'); showToast('操作失败,请重试', 'error'); } } async function hashPassword(password) { const encoder = new TextEncoder(); const data = encoder.encode(password); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } function updatePasswordStatus() { const icon = document.getElementById('password-status-icon'); const label = icon.nextElementSibling; if (isPasswordProtected) { icon.textContent = '🔒'; icon.title = '已启用密码保护'; label.textContent = '已加密'; } else { icon.textContent = '🔓'; icon.title = '未启用密码保护'; label.textContent = '密码保护'; } } // 在页面加载时检查密码保护状态 document.addEventListener('DOMContentLoaded', async () => { await checkPasswordProtection(); // ... existing DOMContentLoaded code ... }); // 修改复制全部内容功能,添加动效 async function copyAllContent() { const copyAllItem = document.querySelector('.copy-all-item'); const textarea = document.getElementById('content'); const text = textarea.value; try { await navigator.clipboard.writeText(text); copyAllItem.classList.add('copied'); setTimeout(() => copyAllItem.classList.remove('copied'), 500); showToast('已复制全部内容到剪贴板', 'success'); } catch (err) { console.error('复制失败:', err); showToast('复制失败,请重试', 'error'); } } async function shareNote() { const preview = document.querySelector('.preview-content'); if (!preview) { showToast('请先开启预览模式', 'warning'); return; } try { // 生成分享ID const shareId = Array.from({length: 8}, () => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'[Math.floor(Math.random() * 62)]).join(''); // 准备分享数据 const shareData = { content: preview.innerHTML, createTime: new Date().toISOString(), lastEditTime: new Date().toISOString(), visitCount: 0 }; // 保存分享数据 const response = await fetch('/share/' + shareId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(shareData) }); if (response.ok) { // 生成分享链接 const shareUrl = window.location.origin + '/share/' + shareId; // 复制链接到剪贴板 await navigator.clipboard.writeText(shareUrl); showToast('分享链接已复制到剪贴板', 'success'); } else { showToast('分享失败,请重试', 'error'); } } catch (error) { console.error('分享失败:', error); showToast('分享失败,请重试', 'error'); } } // 图片点击放大 document.addEventListener('DOMContentLoaded', () => { const content = document.querySelector('.content'); const overlay = document.getElementById('imageOverlay'); content.addEventListener('click', (e) => { if (e.target.tagName === 'IMG' && !e.target.classList.contains('enlarged')) { e.target.classList.add('enlarged'); overlay.classList.add('active'); document.body.style.overflow = 'hidden'; } }); }); // 关闭放大的图片 function closeEnlargedImage() { const enlargedImage = document.querySelector('.enlarged'); const overlay = document.getElementById('imageOverlay'); if (enlargedImage) { enlargedImage.classList.remove('enlarged'); overlay.classList.remove('active'); document.body.style.overflow = ''; } } // ESC键关闭放大图片 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeEnlargedImage(); } }); // 图片加载错误处理 document.addEventListener('DOMContentLoaded', () => { const images = document.querySelectorAll('.content img'); images.forEach(img => { img.onerror = () => { img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; }; }); }); </script> </body> </html>`; } /** * 从KV存储获取笔记内容 * @param {string} notePath - 笔记路径 * @returns {Promise<string>} 笔记内容 */ async function getNoteContent(notePath) { const encryptedText = await NOTES_KV.get(notePath); if (!encryptedText) return ''; const key = await generateEncryptionKey(notePath); return await decryptText(encryptedText, key); } /** * 保存笔记内容到KV存储 * @param {string} notePath - 笔记路径 * @param {string} text - 笔记内容 */ async function saveNoteContent(notePath, text) { const key = await generateEncryptionKey(notePath); const encryptedText = await encryptText(text, key); await NOTES_KV.put(notePath, encryptedText); } /** * 从KV存储删除笔记 * @param {string} notePath - 要删除的笔记路径 */ async function deleteNoteContent(notePath) { await NOTES_KV.delete(notePath); } /** * 处理分享页面请求 * @param {string} shareId - 分享ID * @returns {Response} 响应对象 */ async function handleShareRequest(shareId) { // 获取分享数据 const shareKey = 'share_' + shareId; const shareData = await NOTES_KV.get(shareKey); if (!shareData) { return new Response('分享内容不存在或已过期', { status: 404 }); } try { const data = JSON.parse(shareData); // 更新访问次数 data.visitCount++; await NOTES_KV.put(shareKey, JSON.stringify(data)); // 生成分享页面 const html = generateShareHTML(shareId, data); return new Response(html, { headers: { 'Content-Type': 'text/html;charset=utf-8' } }); } catch (error) { return new Response('加载分享内容失败', { status: 500 }); } } /** * 生成分享页面HTML * @param {string} shareId - 分享ID * @param {Object} data - 分享数据 * @returns {string} HTML内容 */ function generateShareHTML(shareId, data) { const shareTime = new Date(data.createTime).toLocaleString('zh-CN'); return `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>分享的笔记</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" media="(prefers-color-scheme: dark)"> <!-- 添加KaTeX依赖 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"> <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script> <!-- 添加Mermaid依赖 --> <script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script> <style> :root { --primary-color: #4e92d1; --secondary-color: #6c757d; --bg-color: #ffffff; --text-color: #333333; --border-color: #e0e0e0; --container-bg: #ffffff; --editor-bg: #f8f8f8; --shadow-color: rgba(0, 0, 0, 0.1); --hover-color: #f0f0f0; --link-color: #0366d6; --link-hover-color: #0969da; --link-visited-color: #6f42c1; } [data-theme="dark"] { --primary-color: #a2c2f5; --secondary-color: #9ca3af; --bg-color: #1a1a1a; --text-color: #f1f1f1; --border-color: #404040; --container-bg: #2a2a2a; --editor-bg: #333333; --shadow-color: rgba(0, 0, 0, 0.3); --hover-color: #3a3a3a; --link-color: #58a6ff; --link-hover-color: #79b8ff; --link-visited-color: #bc8cff; } * { box-sizing: border-box; margin: 0; padding: 0; } body { margin: 0; background: var(--bg-color); color: var(--text-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 16px; line-height: 1.6; transition: all 0.3s ease; } .container { width: 100%; max-width: 100%; margin: 0 auto; padding: 20px; min-height: 100vh; background-color: var(--container-bg); } @media (min-width: 1200px) { .container { max-width: 95%; box-shadow: 0 0 20px var(--shadow-color); } } .info-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px; background: var(--editor-bg); border-radius: 12px; margin-bottom: 20px; font-size: 14px; border: 1px solid var(--border-color); overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; gap: 15px; min-width: 0; } .info-bar::-webkit-scrollbar { height: 6px; width: 6px; } .info-bar::-webkit-scrollbar-track { background: transparent; } .info-bar::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 3px; } .info-bar::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } .info-left { display: flex; gap: 15px; flex-shrink: 0; margin-right: auto; } .info-right { display: flex; align-items: center; gap: 15px; flex-shrink: 0; margin-left: auto; } .info-item { display: flex; align-items: center; gap: 8px; padding: 4px 10px; border-radius: 6px; transition: all 0.2s ease; white-space: nowrap; flex-shrink: 0; } .info-item:hover { background: var(--hover-color); } .theme-toggle { display: flex; align-items: center; gap: 8px; padding: 4px 10px; border-radius: 6px; border: none; color: var(--text-color); cursor: pointer; font-size: 14px; transition: all 0.2s ease; background: transparent; white-space: nowrap; flex-shrink: 0; } .theme-toggle:hover { background: var(--hover-color); } .theme-toggle:active { transform: scale(0.95); } .theme-toggle .sun-icon, .theme-toggle .moon-icon { display: none; font-size: 1.2rem; } .theme-toggle .label { font-size: 14px; } [data-theme="dark"] .moon-icon { display: block; } [data-theme="light"] .sun-icon { display: block; } @media (max-width: 768px) { .theme-toggle { padding: 4px 8px; } .theme-toggle .label { font-size: 13px; } } @media (max-width: 768px) { .info-bar { padding: 10px; gap: 10px; } .info-left, .info-right { gap: 10px; } .info-item { padding: 4px 8px; font-size: 13px; } } .content { padding: 20px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--editor-bg); overflow-x: auto; } /* Markdown 内容样式 */ .content > *:first-child { margin-top: 0; } .content > *:last-child { margin-bottom: 0; } .content h1, .content h2, .content h3, .content h4, .content h5, .content h6 { margin-top: 1.8em; margin-bottom: 0.8em; line-height: 1.2; color: var(--text-color); } .content h1:first-child, .content h2:first-child, .content h3:first-child, .content h4:first-child, .content h5:first-child, .content h6:first-child { margin-top: 0; } .content p { text-align: left !important; margin: 1.2em 0; line-height: 1.8; } .content p:has(> a:only-child) { text-align: left !important; } .content a { display: inline; color: var(--link-color); text-decoration: none; border-bottom: 1px solid transparent; transition: all 0.2s ease; padding: 2px 4px; margin: 0 -4px; border-radius: 4px; text-align: left !important; } /* 处理单独一行的链接 */ .content p > a:only-child { display: inline-block; text-align: left !important; width: fit-content; margin-left: 0; } /* 处理图片链接的特殊情况 */ .content p:has(img) { text-align: center; margin: 2em 0; } .content a:hover { background: var(--hover-color); color: var(--link-hover-color); border-bottom-color: var(--link-hover-color); } .content a:visited { color: var(--link-visited-color); } .content a:visited:hover { background: var(--hover-color); border-bottom-color: var(--link-visited-color); } .content a[href^="http"]::after { content: "↗"; display: inline; margin-left: 2px; font-size: 0.9em; opacity: 0.6; } .content ul, .content ol { margin: 1em 0; padding-left: 1.5em; } .content li { margin: 0.5em 0; } .content blockquote { margin: 1.2em 0; padding: 1em 1.2em; border-left: 4px solid var(--border-color); background: var(--bg-color); border-radius: 0 4px 4px 0; display: flow-root; width: fit-content; max-width: 100%; box-shadow: 0 2px 4px var(--shadow-color); transition: all 0.2s ease; } .content blockquote > *:first-child { margin-top: 0; } .content blockquote > *:last-child { margin-bottom: 0; } .content blockquote p { margin: 0.8em 0; line-height: 1.6; } .content blockquote + blockquote { margin-top: -0.5em; } .content blockquote blockquote { margin: 0.8em 0; border-left-color: var(--secondary-color); background: var(--editor-bg); box-shadow: none; } .content pre { background: var(--editor-bg); padding: 1.2em 1em; border-radius: 8px; overflow: auto; position: relative; margin: 1.5em 0; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; line-height: 1.5; font-size: 0.95em; border: 1px solid var(--border-color); scrollbar-width: thin; scrollbar-color: var(--secondary-color) transparent; } .content code { background: var(--editor-bg); padding: 0.2em 0.4em; margin: 0 0.2em; border-radius: 4px; font-size: 0.9em; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; border: 1px solid var(--border-color); } .content pre code { background: none; padding: 0; margin: 0; font-size: 0.95em; white-space: pre; word-break: normal; word-wrap: normal; line-height: inherit; tab-size: 2; hyphens: none; border: none; } .content pre::-webkit-scrollbar { width: 6px; height: 6px; } .content pre::-webkit-scrollbar-track { background: transparent; } .content pre::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 3px; border: 2px solid var(--editor-bg); } .content pre::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } .content pre::before { content: attr(data-language); position: absolute; top: 0.5em; right: 0.5em; font-size: 0.85em; color: var(--secondary-color); padding: 0.2em 0.5em; border-radius: 3px; background: var(--container-bg); opacity: 0.8; transition: opacity 0.2s ease; } .content pre:hover::before { opacity: 1; } .copy-button { position: absolute; top: 0.5em; right: 0.5em; padding: 0.2em 0.5em; font-size: 0.85em; color: var(--text-color); background: var(--container-bg); border: 1px solid var(--border-color); border-radius: 3px; cursor: pointer; opacity: 0; transition: all 0.2s ease; display: flex; align-items: center; gap: 4px; } .content pre:hover .copy-button { opacity: 0.8; } .copy-button:hover { opacity: 1 !important; background: var(--hover-color); } .copy-button.copied { color: #4caf50; border-color: #4caf50; opacity: 1; } /* Toast 提示框样式 */ .toast-container { position: fixed; top: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 10px; pointer-events: none; } .toast { background: var(--container-bg); color: var(--text-color); padding: 12px 24px; border-radius: 8px; box-shadow: 0 4px 12px var(--shadow-color); font-size: 14px; display: flex; align-items: center; gap: 8px; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; border: 1px solid var(--border-color); pointer-events: all; max-width: 300px; word-break: break-word; } .toast.show { opacity: 1; transform: translateX(0); } .toast.success { border-left: 4px solid #4caf50; } .toast.error { border-left: 4px solid #f44336; } .toast-icon { font-size: 18px; flex-shrink: 0; } .toast-message { flex: 1; margin-right: 8px; } .toast-close { cursor: pointer; opacity: 0.7; transition: opacity 0.2s ease; padding: 4px; margin: -4px; border-radius: 4px; } .toast-close:hover { opacity: 1; background: var(--hover-color); } /* 表格样式 */ .content table { width: 100%; border-collapse: collapse; margin: 1.5em 0; overflow-x: auto; display: block; } .content th, .content td { border: 1px solid var(--border-color); padding: 8px 12px; text-align: left; } .content th { background-color: var(--hover-color); font-weight: 600; } .content tr:nth-child(even) { background-color: var(--editor-bg); } .content tr:hover { background-color: var(--hover-color); } /* 移动端表格适配 */ @media (max-width: 768px) { .content table { font-size: 14px; } .content th, .content td { padding: 6px 8px; } } /* 表格滚动条样式 */ .content table::-webkit-scrollbar { height: 8px; width: 8px; } .content table::-webkit-scrollbar-track { background: transparent; } .content table::-webkit-scrollbar-thumb { background-color: var(--secondary-color); border-radius: 4px; border: 2px solid var(--editor-bg); } .content table::-webkit-scrollbar-thumb:hover { background-color: var(--text-color); } /* 图片样式 */ .content img { max-width: 100%; height: auto; margin: 1em 0; border-radius: 8px; display: block; box-shadow: 0 2px 8px var(--shadow-color); transition: all 0.3s ease; opacity: 0; animation: fadeIn 0.5s ease forwards; } /* 图片容器 */ .content p:has(img) { text-align: center; margin: 2em 0; } /* 图片悬停效果 */ .content img:hover { transform: scale(1.01); box-shadow: 0 4px 12px var(--shadow-color); } /* 图片标题样式 */ .content img + em { display: block; text-align: center; color: var(--secondary-color); font-size: 0.9em; margin-top: 0.5em; } /* 移动端图片适配 */ @media (max-width: 768px) { .content img { border-radius: 6px; margin: 0.8em 0; } .content p:has(img) { margin: 1.5em 0; } /* 禁用移动端图片缩放动画 */ .content img:hover { transform: none; box-shadow: 0 2px 8px var(--shadow-color); } } /* 图片加载动画 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* 图片加载失败样式 */ .content img:not([src]), .content img[src=""], .content img[src*="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="] { position: relative; min-height: 100px; background: var(--editor-bg); border: 1px dashed var(--border-color); display: flex; align-items: center; justify-content: center; } .content img:not([src])::after, .content img[src=""]::after, .content img[src*="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="]::after { content: "图片加载失败"; position: absolute; color: var(--secondary-color); font-size: 0.9em; } /* 大图查看模式 */ .content img.enlarged { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 90vw; max-height: 90vh; object-fit: contain; z-index: 1000; cursor: zoom-out; margin: 0; padding: 0; background: var(--bg-color); box-shadow: 0 0 20px var(--shadow-color); } /* 大图查看遮罩层 */ .image-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); z-index: 999; display: none; opacity: 0; transition: opacity 0.3s ease; } .image-overlay.active { display: block; opacity: 1; } </style> </head> <body> <div class="container"> <div class="info-bar"> <div class="info-left"> <div class="info-item"> <span>📝 笔记</span> <span>${shareId}</span> </div> <div class="info-item"> <span>🕒 分享于</span> <span>${shareTime}</span> </div> <div class="info-item"> <span>👀 访问</span> <span>${data.visitCount}</span> </div> </div> <div class="info-right"> <button onclick="toggleDarkMode()" class="theme-toggle" title="切换主题"> <span class="sun-icon">☀️</span> <span class="moon-icon">🌙</span> <span class="label">主题</span> </button> </div> </div> <div class="content"> ${data.content} </div> </div> <div class="image-overlay" id="imageOverlay" onclick="closeEnlargedImage()"></div> <div class="toast-container" id="toast-container"></div> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script> <script> // 初始化主题和代码块 document.addEventListener('DOMContentLoaded', () => { const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); // 为所有代码块添加复制按钮和语言标签 const codeBlocks = document.querySelectorAll('pre code'); codeBlocks.forEach(code => { const pre = code.parentElement; // 获取语言类名 const langClass = Array.from(code.classList).find(cl => cl.startsWith('language-')); const language = langClass ? langClass.replace('language-', '') : '代码'; pre.setAttribute('data-language', language); // 添加复制按钮 const button = document.createElement('button'); button.className = 'copy-button'; button.innerHTML = '<span>📋</span><span>复制</span>'; button.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); await copyCode(code, button); }; pre.appendChild(button); }); // 应用代码高亮 hljs.highlightAll(); // 图片点击放大 const content = document.querySelector('.content'); const overlay = document.getElementById('imageOverlay'); content.addEventListener('click', (e) => { if (e.target.tagName === 'IMG' && !e.target.classList.contains('enlarged')) { e.target.classList.add('enlarged'); overlay.classList.add('active'); document.body.style.overflow = 'hidden'; } }); // 图片加载错误处理 const images = document.querySelectorAll('.content img'); images.forEach(img => { img.onerror = () => { img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; }; }); }); // 复制代码到剪贴板 async function copyCode(codeElement, button) { const originalText = button.innerHTML; const code = codeElement.innerText; try { await navigator.clipboard.writeText(code); button.innerHTML = '<span>✅</span><span>已复制</span>'; button.classList.add('copied'); showToast('代码已复制到剪贴板', 'success'); } catch (err) { console.error('复制失败:', err); button.innerHTML = '<span>❌</span><span>复制失败</span>'; button.classList.add('error'); showToast('复制失败,请重试', 'error'); } setTimeout(() => { button.innerHTML = originalText; button.classList.remove('copied', 'error'); }, 2000); } // 切换暗色模式 function toggleDarkMode() { const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); // 更新Mermaid主题 mermaid.initialize({ startOnLoad: false, theme: theme === 'dark' ? 'dark' : 'default', securityLevel: 'loose', fontFamily: 'var(--font-family)', }); // 重新渲染预览内容以更新Mermaid图表 if (previewToggle.checked) { updatePreview(content.value); } } // 关闭放大的图片 function closeEnlargedImage() { const enlargedImage = document.querySelector('.enlarged'); const overlay = document.getElementById('imageOverlay'); if (enlargedImage) { enlargedImage.classList.remove('enlarged'); overlay.classList.remove('active'); document.body.style.overflow = ''; } } // ESC键关闭放大图片 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeEnlargedImage(); } }); // Toast 提示框 function showToast(message, type = 'info', duration = 3000) { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = 'toast ' + type; const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' }; toast.innerHTML = '<span class="toast-icon">' + (icons[type] || icons.info) + '</span>' + '<span class="toast-message">' + message + '</span>' + '<span class="toast-close" onclick="this.parentElement.remove()">✕</span>'; container.appendChild(toast); void toast.offsetWidth; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } </script> </body> </html>`; } // 监听所有fetch请求 addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); });