离线私密日记本:纯前端HTML/CSS/JS实现本地加密存储
发布时间:2026/9/14 1:40:26来源:尧图网络
简介这是一份轻量级、开箱即用的私密日记本网页模板面向前端初学者与注重隐私记录的个人用户解决本地化、安全化数字日记书写需求。资源以HTML单页应用形式实现共4个核心文件index.html为入口页面style.css负责复古日记本视觉样式含双页布局、纸张纹理与装订线等细节script.js处理密码验证、localStorage数据持久化及富文本编辑逻辑password.txt提供默认密码说明整包仅9KB便于快速部署与离线使用。已有67人学习下载适合希望理解本地存储机制、密码保护流程与简易富文本编辑器实现原理的学习者。读者可直接运行体验完整功能输入密码进入、编辑带格式文字、插入图片、设置日期标题并在切换或退出时获得未保存提醒所有数据仅存于浏览器本地无服务器依赖兼顾安全性与易用性。1. 这不是普通 HTML 模板一个运行在浏览器里的「离线私密日记本」所有数据从不离开你的电脑你打开index.html输入密码眼前展开的是一本棕褐色封面、带装订线与纸张纹理的双页笔记本——这不是网页渲染的“假效果”而是用纯 HTML CSS JavaScript 实现的、完全离线运行的本地日记应用。它不调用任何后端接口不上传数据到服务器不依赖网络连接所有日记内容通过localStorage直接写入浏览器本地存储区关机重启后依然存在。它解决的不是“怎么建个博客”而是“如何在没有云服务、不信任第三方、甚至断网状态下安全记录敏感生活片段”的真实需求。适合对隐私有强意识的开发者、备考学生、心理咨询师笔记场景或需要临时隔离环境记录信息的 IT 运维人员。它不提供账号体系、不支持多设备同步恰恰是这种“功能克制”构成了它的安全边界没有远程传输就没有泄露路径没有服务端逻辑就无法被注入或劫持。2. 密码验证与 localStorage 数据持久化为什么它真能“锁住你的文字”2.1 密码校验流程从password.txt到 DOM 阻断的完整链路该模板的密码并非硬编码在 JS 中而是以明文形式存于同目录下的password.txt文件。这看似不安全实则是为离线场景做的权衡设计当用户双击打开index.html即通过file://协议加载浏览器出于安全策略会阻止fetch(password.txt)跨源读取本地文件——但该模板巧妙绕过了这一限制它使用script srcpassword.txt/script的方式尝试加载而password.txt内容实际为一行 JS 赋值语句// password.txt 内容示例 const PASSWORD mySecret2024;提示此设计仅适用于本地双击打开场景。若部署到 HTTP 服务器如http://localhost:8080需改用fetch CORS 配置否则script标签加载.txt会报 MIME 类型错误。生产环境应替换为环境变量注入或服务端渲染。script.js在页面加载时立即执行校验逻辑// script.js 片段密码验证核心 function checkPassword() { const input document.getElementById(passwordInput).value; if (typeof PASSWORD ! undefined input PASSWORD) { document.body.classList.add(unlocked); document.getElementById(loginScreen).style.display none; loadAllEntries(); // 加载 localStorage 中所有日记条目 } else { alert(密码错误请重试); } }该函数绑定在登录按钮onclick上未通过form.submit避免页面刷新导致状态丢失。验证通过后body添加unlocked类CSS 通过.unlocked .page控制日记主区域显隐实现无跳转解锁。2.2 localStorage 存储结构每篇日记如何被序列化与索引所有日记数据以 JSON 格式存入localStorage键名为diaryEntries。其值为数组每个元素代表一篇日记结构如下[ { id: 20240521-153247, title: 项目启动会议纪要, date: 2024-05-21, content: pstrong参会人/strong张三、李四/ppmark关键结论/mark采用微服务架构/p, createdAt: 2024-05-21T15:32:47.123Z }, { id: 20240522-091402, title: 晨间随笔, date: 2024-05-22, content: p阳光很好咖啡微苦。/pimg src\data:image/png;base64,iVBOR...\ alt\手绘草图\, createdAt: 2024-05-22T09:14:02.456Z } ]注意content字段直接存储富文本 HTML 字符串包含strong、mark、img等标签。图片通过FileReader转为 base64 内嵌确保单文件可迁移——这是该模板区别于其他“伪离线”日记本的关键它不依赖外部图片路径所有资源打包进localStorage。script.js中saveEntry()函数负责写入function saveEntry() { const id document.getElementById(entryId).value || generateId(); const title document.getElementById(entryTitle).value; const date document.getElementById(entryDate).value; const content document.getElementById(editor).innerHTML; // 直接取 contenteditable 区域 HTML const entries JSON.parse(localStorage.getItem(diaryEntries) || []); const existingIndex entries.findIndex(e e.id id); if (existingIndex 0) { entries[existingIndex] { id, title, date, content, createdAt: new Date().toISOString() }; } else { entries.push({ id, title, date, content, createdAt: new Date().toISOString() }); } localStorage.setItem(diaryEntries, JSON.stringify(entries)); updateSidebar(); // 刷新侧边栏列表 }generateId()使用Date.now() 随机数生成唯一 ID避免时间精度不足导致冲突。updateSidebar()遍历entries数组动态生成li>let isDirty false; const editor document.getElementById(editor); // 监听编辑器内容变化兼容 IE9 editor.addEventListener(input, () { isDirty true; }); // 切换日记前检查 function switchEntry(entryId) { if (isDirty !confirm(当前日记尚未保存确定要切换吗)) { return; // 阻止切换 } loadEntry(entryId); // 加载目标日记 isDirty false; // 重置标志 } // 页面卸载前检查 window.addEventListener(beforeunload, (e) { if (isDirty) { e.preventDefault(); e.returnValue ; // 触发浏览器确认弹窗 } });提示beforeunload在现代浏览器中已限制自定义提示文案仅显示统一提示如 Chrome 显示“您确定要离开此页面吗”。该设计不依赖文案说服力而靠强制中断流程保障数据不丢失。3. 仿真日记本 UI 实现CSS 如何用纯前端还原纸质质感3.1 双页布局与响应式装订线Flexbox 与伪元素的组合技巧日记主区域.book采用display: flex实现左右双页并通过::before伪元素绘制居中装订线/* style.css 片段 */ .book { display: flex; justify-content: space-between; max-width: 1200px; margin: 0 auto; padding: 2rem 1rem; position: relative; } .book::before { content: ; position: absolute; top: 0; bottom: 0; left: 50%; width: 4px; background: linear-gradient(to bottom, #8B4513, #5D2906); transform: translateX(-50%); box-shadow: 0 0 12px rgba(0,0,0,0.2); z-index: 10; } .page { width: 48%; min-height: 70vh; background: #fdf6e3; border: 1px solid #d4b98a; border-radius: 8px; padding: 1.5rem; position: relative; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.08); } .page::before { content: ; position: absolute; top: 0; left: 0; right: 0; height: 100%; background: linear-gradient(rgba(255,255,255,0.8), rgba(255,255,255,0.8)), url(data:image/svgxml;utf8,svg xmlnshttp://www.w3.org/2000/svg width100 height20line x10 y110 x2100 y210 stroke%23d4b98a stroke-width0.5//svg); background-repeat: repeat-y; background-position: center; pointer-events: none; }background中嵌入 SVG Base64 编码的横线图案实现纸张纹理box-shadow模拟纸张厚度阴影border-radius与border模拟旧书边缘磨损。width: 48%留出 4% 间隙供装订线占用避免视觉挤压。3.2 复古配色系统与 CSS 自定义属性的可维护性设计模板未使用固定色值硬编码而是通过 CSS 自定义属性建立色彩体系便于快速主题切换:root { --primary-color: #8B4513; /* 鞣酸棕封面与装订线主色 */ --page-bg: #fdf6e3; /* 米白纸张底色 */ --line-color: #d4b98a; /* 暖灰褐横线与边框色 */ --text-color: #332a1f; /* 深褐正文文字 */ --highlight-color: #ffeb3b; /* 高亮黄标记色 */ --accent-color: #ff9800; /* 橙色按钮与强调色 */ } .book { background-color: var(--page-bg); } .page { border-color: var(--line-color); } .editor-content p { color: var(--text-color); } .mark { background-color: var(--highlight-color); } .btn-primary { background-color: var(--accent-color); }注意--highlight-color默认设为#ffeb3b黄色但script.js中颜色选择器支持动态修改style.setProperty(--highlight-color, selectedHex)实现运行时主题微调。3.3 响应式适配与移动端交互优化媒体查询与触摸事件补全针对小屏设备模板在media (max-width: 768px)下强制切换单页模式media (max-width: 768px) { .book { flex-direction: column; } .book::before { display: none; /* 移动端隐藏装订线 */ } .page { width: 100%; } .sidebar { position: static; width: 100%; } }同时为兼容 iOS Safari 的contenteditable焦点问题script.js注入了触摸增强逻辑// 修复 iOS Safari 点击编辑器不聚焦问题 if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { document.getElementById(editor).addEventListener(touchstart, function(e) { if (!this.hasAttribute(contenteditable)) { this.setAttribute(contenteditable, true); this.focus(); e.preventDefault(); } }, { passive: false }); }4. 富文本编辑与图片内嵌contenteditable 的深度定制实践4.1 工具栏命令映射document.execCommand的封装与降级处理编辑器工具栏按钮粗体、斜体等全部绑定execCommand但做了兼容性兜底!-- index.html 工具栏 -- div classtoolbar button onclickformatText(bold) title加粗B/button button onclickformatText(italic) title斜体I/button button onclickformatText(underline) title下划线U/button button onclickformatText(strikethrough) title删除线S/button button onclickformatText(backColor, #ffeb3b) title高亮HL/button /div// script.js 封装函数 function formatText(command, value null) { // 确保编辑器获得焦点 const editor document.getElementById(editor); editor.focus(); try { // 执行原生命令 document.execCommand(command, false, value); } catch (e) { // 降级手动包裹选中文本 if (command bold) { wrapSelectionWith(strong, /strong); } else if (command italic) { wrapSelectionWith(em, /em); } else if (command backColor) { wrapSelectionWith(span stylebackground-color:${value}, /span); } } } function wrapSelectionWith(openTag, closeTag) { const sel window.getSelection(); if (sel.rangeCount 0) { const range sel.getRangeAt(0); const fragment range.extractContents(); const wrapper document.createElement(span); wrapper.innerHTML openTag fragment.textContent closeTag; range.insertNode(wrapper); } }提示execCommand已被标记为废弃但目前仍是contenteditable最可靠方案。wrapSelectionWith作为降级路径仅处理纯文本包裹不解析 HTML 结构避免嵌套污染。4.2 图片插入的全流程FileReader Canvas 压缩 Base64 内嵌点击“插入图片”按钮后触发文件选择经压缩后转为 base64 写入编辑器function insertImage() { const input document.createElement(input); input.type file; input.accept image/*; input.onchange function(e) { const file e.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload function(event) { const img new Image(); img.onload function() { // 创建 Canvas 压缩至最大宽度 800px const canvas document.createElement(canvas); const ctx canvas.getContext(2d); const maxWidth 800; let width img.width; let height img.height; if (width maxWidth) { height * maxWidth / width; width maxWidth; } canvas.width width; canvas.height height; ctx.drawImage(img, 0, 0, width, height); // 转为压缩 base64质量 0.8 const compressedBase64 canvas.toDataURL(image/jpeg, 0.8); // 插入编辑器 const editor document.getElementById(editor); const imgTag img src${compressedBase64} alt插入图片 stylemax-width:100%;height:auto;; document.execCommand(insertHTML, false, imgTag); }; img.src event.target.result; }; reader.readAsDataURL(file); }; input.click(); }注意toDataURL(image/jpeg)强制转 JPEG比 PNG 小 40%~60%且localStorage容量有限通常 5~10MB压缩是必要步骤。maxWidth限制防止大图撑爆布局。4.3 日期与标题的元数据管理独立字段与 DOM 同步策略日记的date和title不存于contentHTML 中而是分离为独立表单字段input typedate identryDate classform-control input typetext identryTitle placeholder给这篇日记起个名字... classform-control div ideditor contenteditabletrue classeditor-content/divloadEntry(entryId)函数从localStorage读取数据后分别填充这三个字段function loadEntry(entryId) { const entries JSON.parse(localStorage.getItem(diaryEntries) || []); const entry entries.find(e e.id entryId); if (entry) { document.getElementById(entryId).value entry.id; document.getElementById(entryTitle).value entry.title || ; document.getElementById(entryDate).value entry.date || getCurrentDate(); document.getElementById(editor).innerHTML entry.content || pbr/p; isDirty false; // 加载后重置脏标志 } }getCurrentDate()返回YYYY-MM-DD格式字符串确保input[typedate]兼容。这种分离设计使date和title可被侧边栏列表直接读取无需解析 HTML提升updateSidebar()性能。5. 数据导出与本地备份如何将 localStorage 日记打包为可迁移 ZIP5.1 JSON 导出功能一键下载结构化数据文件模板未内置 ZIP 打包但提供了标准 JSON 导出入口为后续迁移打下基础。点击“导出数据”按钮触发以下逻辑function exportData() { const entries JSON.parse(localStorage.getItem(diaryEntries) || []); const dataStr JSON.stringify(entries, null, 2); // 格式化缩进 const blob new Blob([dataStr], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download diary-export-${new Date().toISOString().slice(0,10)}.json; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }导出的 JSON 文件可被任意文本编辑器查看也可用 Python 脚本批量转换为 Markdown 或 PDF# 示例Python 转 Markdown保存为 export_to_md.py import json with open(diary-export-2024-05-21.json, r, encodingutf-8) as f: entries json.load(f) for entry in entries: md_file fdiary_{entry[id]}.md with open(md_file, w, encodingutf-8) as f: f.write(f# {entry[title]}\n) f.write(f**日期** {entry[date]}\n\n) # 简单 HTML to MD 转换仅处理基础标签 content entry[content].replace(p, ).replace(/p, \n) content content.replace(strong, **).replace(/strong, **) content content.replace(em, *).replace(/em, *) f.write(content)5.2 本地 ZIP 打包方案使用 JSZip 库实现浏览器端压缩若需真正 ZIP 包含index.html、style.css、script.js及导出 JSON可引入轻量库jszip仅 35KBnpm install jszip # 或直接引入 CDN script srchttps://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js/script在script.js中扩展导出函数async function exportAsZip() { const JSZip (await import(https://cdn.jsdelivr.net/npm/jszip3.10.1/dist/jszip.min.js)).default; const zip new JSZip(); // 添加日记数据 const entries JSON.parse(localStorage.getItem(diaryEntries) || []); zip.file(diary-data.json, JSON.stringify(entries, null, 2)); // 添加静态资源需提前 fetch const files [index.html, style.css, script.js]; for (const file of files) { const res await fetch(file); const content await res.text(); zip.file(file, content); } const content await zip.generateAsync({ type: blob }); const url URL.createObjectURL(content); const a document.createElement(a); a.href url; a.download diary-backup-${new Date().toISOString().slice(0,10)}.zip; a.click(); URL.revokeObjectURL(url); }提示fetch读取同目录文件依赖 HTTP 服务器环境file://协议下会跨域失败。部署到http-server或nginx后即可使用。5.3 重置与清空安全擦除本地数据的不可逆操作“清空所有日记”功能执行彻底清除无回收站function clearAllEntries() { if (!confirm(确定要永久删除所有日记此操作不可撤销)) return; localStorage.removeItem(diaryEntries); document.getElementById(sidebar).innerHTML li暂无日记/li; document.getElementById(editor).innerHTML pbr/p; document.getElementById(entryTitle).value ; document.getElementById(entryDate).value getCurrentDate(); document.getElementById(entryId).value ; isDirty false; }该函数直接调用localStorage.removeItem()不保留任何残留键值。对于高敏用户建议在清空后手动打开浏览器开发者工具 → Application → Storage → Clear storage确保localStorage、sessionStorage、缓存全清。本文还有配套的精品资源点击获取
网站建设高端定制企业官网