📄 路径: /测试
🌓 主题转换
🔒 权限管理
📋 复制文本
📋 复制链接
🌐 查看原文
💾 强制保存
export default { async fetch(request, env, ctx) { const url = new URL(request.url); const path = url.pathname; if (path === "/favicon.ico" || path === "/robots.txt") { return new Response(null, { status: 404 }); } if (!env.NOTEPAD_KV) { return new Response("错误:未找到 NOTEPAD_KV 绑定,请在 Cloudflare 后台设置 KV 绑定。", { status: 500 }); } // 1. 首页路由 if (path === "/" || path === "") { return new Response(getHomeHTML(url.origin), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } const slug = decodeURIComponent(path.substring(1)).trim(); if (!slug) { return new Response("无效的路径", { status: 400 }); } // 读取并解析 KV 数据 (兼容老版本的纯文本数据) let note = { content: "", isLocked: false, password: "" }; const rawData = await env.NOTEPAD_KV.get(slug); if (rawData) { try { const parsed = JSON.parse(rawData); if (parsed && typeof parsed === 'object' && parsed.content !== undefined) { note = parsed; } else { note.content = rawData; } } catch (e) { note.content = rawData; // 老数据平滑过渡 } } // 2. API 路由:纯文本下载 (?raw) if (url.searchParams.has("raw")) { return new Response(note.content, { headers: { "Content-Type": "text/plain;charset=UTF-8" } }); } // 3. API 路由:保存与权限控制 (POST/PUT) if (request.method === "POST" || request.method === "PUT") { try { const body = await request.json(); // 权限校验:如果已被锁定,必须验证密码 if (note.isLocked) { if (body.password !== note.password) { return new Response(JSON.stringify({ success: false, error: "LOCKED", message: "此便签已被作者锁定,请输入正确密码!" }), { status: 403, headers: { "Content-Type": "application/json" } }); } } // 处理核心动作 if (body.action === 'lock') { // 动作:锁定页面 if (!body.password || body.password.trim() === "") { return new Response(JSON.stringify({ success: false, error: "密码不能为空" }), { status: 400 }); } note.isLocked = true; note.password = body.password; if (body.content !== undefined) note.content = body.content; } else if (body.action === 'unlock') { // 动作:解除锁定,恢复公开 note.isLocked = false; note.password = ""; } else { // 动作:日常自动同步内容 note.content = body.content || ""; } // 限制大小 (2MB) if (note.content.length > 2 * 1024 * 1024) { return new Response(JSON.stringify({ success: false, error: "内容过大,最大支持 2MB" }), { status: 400 }); } // 写入 KV await env.NOTEPAD_KV.put(slug, JSON.stringify(note)); return new Response(JSON.stringify({ success: true, isLocked: note.isLocked })); } catch (err) { return new Response(JSON.stringify({ success: false, error: err.message }), { status: 500 }); } } // 4. 主路由:渲染带权限控制的编辑器 return new Response(getEditorHTML(slug, note.content, note.isLocked, url.origin), { headers: { "Content-Type": "text/html;charset=UTF-8" } }); } }; // ==================== HTML 模板:首页 ==================== function getHomeHTML(origin) { const cleanOrigin = origin.replace(/^https?:\/\//, ''); 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> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #f5f5f7; color: #1d1d1f; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; } .container { text-align: center; background: white; padding: 2.5rem; border-radius: 16px; box-shadow: 0 4px 30px rgba(0,0,0,0.05); max-width: 420px; width: 90%; } h1 { margin-bottom: 0.5rem; font-size: 1.8rem; font-weight: 600; } p { color: #86868b; margin-bottom: 2rem; font-size: 0.95rem; } .input-group { display: flex; margin-bottom: 1.2rem; border: 1px solid #d2d2d7; border-radius: 10px; overflow: hidden; background: #fff; } .prefix { background: #f5f5f7; padding: 0.8rem; color: #86868b; font-size: 0.95rem; border-right: 1px solid #d2d2d7; white-space: nowrap; } input { border: none; padding: 0.8rem; flex: 1; font-size: 0.95rem; outline: none; } button { width: 100%; padding: 0.8rem; background: #0071e3; color: white; border: none; border-radius: 10px; font-size: 1rem; cursor: pointer; font-weight: 500; transition: background 0.2s; margin-bottom: 0.6rem; } button:hover { background: #0077ed; } .btn-random { background: #34c759; } .btn-random:hover { background: #30b651; } </style> </head> <body> <div class="container"> <h1>在线云记事本</h1> <p>创建属于你的临时文本中转或权限加密记事本</p> <div class="input-group"> <span class="prefix">${cleanOrigin}/</span> <input type="text" id="customPath" placeholder="输入自定义路径" autofocus /> </div> <button onclick="goToPath()">进入 / 创建</button> <button class="btn-random" onclick="createRandom()">随机新建一个</button> </div> <script> function goToPath() { const path = document.getElementById('customPath').value.trim(); if (path) window.location.href = '/' + encodeURIComponent(path); else alert('请输入路径名称'); } function createRandom() { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 6; i++) result += chars.charAt(Math.floor(Math.random() * chars.length)); window.location.href = '/' + result; } document.getElementById('customPath').addEventListener('keypress', function(e) { if (e.key === 'Enter') goToPath(); }); </script> </body> </html>`; } // ==================== HTML 模板:编辑器 ==================== function getEditorHTML(slug, content, isLocked, origin) { const escapedContent = content .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); return `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${slug} - 云记事本</title> <style> * { box-sizing: border-box; } body, html { margin: 0; padding: 0; height: 100%; font-family: "Courier New", Courier, monospace, "SF Pro", sans-serif; background-color: #ffffff; color: #1d1d1f; display: flex; flex-direction: column; } body.dark-mode { background-color: #1e1e1e; color: #d4d4d4; } .header { display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: #f5f5f7; border-bottom: 1px solid #d2d2d7; font-size: 0.9rem; user-select: none; } body.dark-mode .header { background: #2d2d2d; border-bottom: 1px solid #3c3c3c; } .title-area { font-weight: 600; display: flex; align-items: center; gap: 6px; } .actions { display: flex; gap: 8px; align-items: center; } .btn { background: #0071e3; color: white; border: none; padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 500; } .btn:hover { background: #0077ed; } .btn-sec { background: #e3e3e8; color: #1d1d1f; } body.dark-mode .btn-sec { background: #3a3a3c; color: #e5e5ea; } .btn-sec:hover { background: #d1d1d6; } body.dark-mode .btn-sec:hover { background: #48484a; } .btn-lock { background: #ff9500; color: white; } .btn-lock:hover { background: #e08300; } .editor-container { flex: 1; position: relative; width: 100%; } textarea { width: 100%; height: 100%; border: none; resize: none; padding: 20px; font-size: 1.05rem; line-height: 1.6; font-family: inherit; outline: none; background: transparent; color: inherit; } textarea[readonly] { background-color: rgba(0,0,0,0.02); color: #86868b; } body.dark-mode textarea[readonly] { background-color: rgba(255,255,255,0.02); color: #8e8e93; } .footer { display: flex; justify-content: space-between; padding: 6px 20px; background: #f5f5f7; border-top: 1px solid #d2d2d7; font-size: 0.8rem; color: #86868b; } body.dark-mode .footer { background: #2d2d2d; border-top: 1px solid #3c3c3c; color: #8e8e93; } .status-saved { color: #34c759; font-weight: bold; } .status-saving { color: #ff9500; } .status-error { color: #ff3b30; font-weight: bold; } </style> </head> <body> <div class="header"> <div class="title-area"> <span>📄 路径: /${slug}</span> </div> <div class="actions"> <button class="btn btn-sec" onclick="toggleTheme()">🌓 主题转换</button> <button class="btn btn-lock" id="lockBtn" onclick="handlePermissionClick()">🔒 权限管理</button> <button class="btn btn-sec" onclick="copyText()">📋 复制文本</button> <button class="btn btn-sec" onclick="copyLink()">📋 复制链接</button> <button class="btn btn-sec" onclick="viewRaw()">🌐 查看原文</button> <button class="btn" id="saveBtn" onclick="manualSave()">💾 强制保存</button> </div> </div> <div class="editor-container"> <textarea id="editor" placeholder="在此处输入内容..." autofocus>${escapedContent}</textarea> </div> <div class="footer"> <div id="status">正在检查权限...</div> <div>字数: <span id="charCount">0</span></div> </div> <script> const slug = "${slug}"; const isLockedServer = ${isLocked}; const editor = document.getElementById('editor'); const lockBtn = document.getElementById('lockBtn'); const status = document.getElementById('status'); const charCount = document.getElementById('charCount'); let timeout = null; // 从本地浏览器缓存读取可能记录过的密码 let cachedPassword = localStorage.getItem('notepad_pwd_' + slug) || ''; // 1. 初始化权限展现 function initPermissions() { charCount.textContent = editor.value.length; if (isLockedServer) { if (!cachedPassword) { // 处于被锁定状态,且当前访问者没有密码缓存 -> 限制为【只读】 editor.readOnly = true; editor.placeholder = "🔒 此便签已被作者锁定。您目前只有【观看和复制】权限,无法进行修改。"; lockBtn.textContent = "🔑 输入密码编辑"; status.innerHTML = "<span class='status-error'>模式:🔒 只读模式(他人不可修改)</span>"; } else { // 处于锁定状态,但当前访问者就是作者(本地有正确密码)-> 允许编辑 editor.readOnly = false; lockBtn.textContent = "🔓 解除锁定模式"; status.innerHTML = "<span class='status-saved'>模式:🔐 已通过密码认证(您拥有修改权限)</span>"; } } else { // 未锁定状态 -> 任何人均可公开编辑 editor.readOnly = false; lockBtn.textContent = "🔒 锁定此页(只读)"; status.textContent = "模式:🌐 公开编辑模式(任何人拿到链接均可改)"; } } initPermissions(); // 2. 权限控制按钮点击事件 async function handlePermissionClick() { if (!isLockedServer) { // 情况 A:当前是公开的,作者点击想要“转为只读” const pwd = prompt("请输入你想为该网页设置的【保护密码】\\n设置后,其他没有密码的人打开将只能观看和复制:"); if (!pwd) return; const res = await fetch(window.location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'lock', password: pwd, content: editor.value }) }); if (res.ok) { localStorage.setItem('notepad_pwd_' + slug, pwd); alert("锁定成功!当前浏览器已自动记住您的作者身份。其他人访问此链接将只能看、不能改。"); location.reload(); } else { alert("设置失败,请重试. "); } } else { if (!cachedPassword) { // 情况 B:普通访客点击“输入密码编辑” const pwd = prompt("请输入此便签的保护密码以开启编辑权限:"); if (!pwd) return; // 尝试发送带密码的保存请求来验证密码是否正确 const res = await fetch(window.location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pwd, content: editor.value }) }); if (res.status === 403) { alert("密码错误,验证失败!"); } else { localStorage.setItem('notepad_pwd_' + slug, pwd); alert("密码正确!编辑权限已临时解锁。"); location.reload(); } } else { // 情况 C:作者本人点击“解除锁定模式”,想恢复为所有人公开可改 if (confirm("确定要【解除密码锁定】,将该页面恢复为【所有人均可公开修改】的状态吗?")) { const res = await fetch(window.location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'unlock', password: cachedPassword }) }); if (res.ok) { localStorage.removeItem('notepad_pwd_' + slug); alert("已成功解除锁定,便签恢复公开。"); location.reload(); } else { alert("解除失败,密码可能已被后端重置。"); } } } } } // 3. 自动数据保存逻辑 async function saveData() { if (editor.readOnly) return; status.className = 'status-saving'; status.textContent = '正在安全同步至云端...'; try { const response = await fetch(window.location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: editor.value, password: cachedPassword }) }); if (response.status === 403) { status.className = 'status-error'; status.textContent = '保存失败:此页面已被锁定,您没有修改权限!'; editor.readOnly = true; return; } const data = await response.json(); if (data.success) { status.className = 'status-saved'; status.innerHTML = (isLockedServer ? "🔐 更改已加密同步 " : "🌐 更改已公开同步 ") + new Date().toLocaleTimeString(); } else { status.className = 'status-error'; status.textContent = '保存失败: ' + (data.error || '未知错误'); } } catch (err) { status.className = 'status-error'; status.textContent = '网络错误,请点击右上角强制保存重试。'; } } editor.addEventListener('input', () => { charCount.textContent = editor.value.length; status.className = ''; status.textContent = '正在输入...'; clearTimeout(timeout); timeout = setTimeout(saveData, 1000); }); function manualSave() { clearTimeout(timeout); saveData(); } if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.body.classList.add('dark-mode'); } function toggleTheme() { document.body.classList.toggle('dark-mode'); localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light'); } function copyText() { navigator.clipboard.writeText(editor.value); alert('便签文本内容已成功复制到剪贴板!'); } function copyLink() { navigator.clipboard.writeText(window.location.href); alert('本页链接已复制到剪贴板!'); } function viewRaw() { window.open(window.location.pathname + '?raw', '_blank'); } </script> </body> </html>`; }