摘要
本文详细介绍了一个基于HTML5的便签应用开发过程,重点讲解了如何利用localStorage实现数据持久化存储,以及如何实现流畅的拖拽交互体验。该应用具有创建、编辑、删除便签功能,自动保存用户内容,并支持多便签管理。文章将深入解析核心实现代码,并提供完整的开发案例。
成品显示
文章最后提供完整的代码,复制粘贴后保存为html文件,可以直接打开如下图,在网页上创建便签,随时记录,修改,删除,关闭网页,不删除便签。
核心功能与实现语法
1. 本地存储管理
使用localStorage实现便签数据的持久化存储:
note.operate = {
// 存储便签数据
set: function (id, content) {
var notes = this.getNotes();
if (notes[id]) {
Object.assign(notes[id], content);
} else {
notes[id] = content;
}
localStorage.setItem(this.keyItem, JSON.stringify(notes));
},
// 获取便签数据
getNotes: function () {
return JSON.parse(localStorage.getItem(this.keyItem) || '{}');
},
// 删除便签
remove: function (id) {
var notes = this.getNotes();
delete notes[id];
localStorage.setItem(this.keyItem, JSON.stringify(notes));
}
};
2. 拖拽功能实现
精准的拖拽位置计算和层级管理:
// 标题栏拖拽事件
titleBar.addEventListener('mousedown', function(e) {
movedNote = note;
var rect = note.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
// 提升当前便签的z-index
maxZindex++;
note.style.zIndex = maxZindex;
});
// 全局鼠标移动事件
document.addEventListener('mousemove', function(e) {
if (!movedNote) return;
movedNote.style.left = (e.clientX - startX) + 'px';
movedNote.style.top = (e.clientY - startY) + 'px';
});
3. 自动保存机制
使用防抖技术优化内容自动保存:
// 编辑区域事件
var inputTimer;
edit.addEventListener('input', function() {
var content = edit.innerHTML;
clearTimeout(inputTimer);
inputTimer = setTimeout(function() {
var time = Date.now();
operate.set(that.note.id, {
content: content,
updateTime: time
});
that.updateTime(time);
}, 500); // 500ms防抖延迟
});
4. 时间格式化处理
时间显示格式:
dateFormat: function (ms) {
var d = new Date(ms);
var pad = function (s) {
return s.toString().padStart(2, '0');
}
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}
${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
完整代码
<!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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
text-align: center;
margin-bottom: 30px;
color: white;
width: 100%;
max-width: 1200px;
}
.header h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2rem;
opacity: 0.9;
max-width: 600px;
margin: 0 auto;
}
#u-btn-create {
background: #ff9a00;
color: white;
border: none;
padding: 15px 35px;
font-size: 1.2rem;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: all 0.3s ease;
margin-top: 20px;
font-weight: bold;
letter-spacing: 1px;
}
#u-btn-create:hover {
background: #ff7b00;
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0,0,0,0.25);
}
.m-note {
position: absolute;
width: 300px;
background: #fff9c4;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
overflow: hidden;
transition: box-shadow 0.3s ease;
display: flex;
flex-direction: column;
min-height: 320px;
}
.m-note:hover {
box-shadow: 0 15px 40px rgba(0,0,0,0.2);
}
.u-title {
background: #ffd54f;
color: #333;
padding: 12px 20px;
font-weight: bold;
font-size: 1.1rem;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
.u-close {
cursor: pointer;
font-style: normal;
font-size: 1.5rem;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.u-close:hover {
background: rgba(0,0,0,0.1);
transform: scale(1.1);
}
.u-edit {
flex: 1;
padding: 20px;
font-size: 1rem;
line-height: 1.5;
outline: none;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
background: #fffde7;
}
.u-timeUpdate {
background: rgba(0,0,0,0.05);
padding: 10px 20px;
font-size: 0.85rem;
color: #666;
}
.time {
font-weight: 600;
color: #333;
}
.features {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 20px;
max-width: 1000px;
margin: 40px auto;
}
.feature-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
width: 280px;
color: white;
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}
.feature-card h3 {
font-size: 1.4rem;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.feature-card h3 i {
margin-right: 10px;
font-size: 1.6rem;
}
.feature-card p {
line-height: 1.6;
opacity: 0.9;
}
@media (max-width: 768px) {
.header h1 {
font-size: 2.2rem;
}
.features {
flex-direction: column;
align-items: center;
}
}
</style>
</head>
<body>
<div class="header">
<h1>📝 智能便签应用</h1>
<p>随时随地记录您的想法、待办事项和重要信息</p>
<button id="u-btn-create">+ 新建便签</button>
</div>
<div class="features">
<div class="feature-card">
<h3><i>📌</i> 拖拽移动</h3>
<p>点击标题栏可以拖拽便签到任意位置,方便您整理工作区</p>
</div>
<div class="feature-card">
<h3><i>🔄</i> 自动保存</h3>
<p>内容编辑后会自动保存到本地存储,刷新页面也不会丢失</p>
</div>
<div class="feature-card">
<h3><i>⏱️</i> 时间记录</h3>
<p>每次更新便签内容都会记录时间,帮助您追踪进度</p>
</div>
</div>
<script>
var note = {
util: {}, //存放基础工具的方法
operate: {} //存放便签相关的操作
};
note.util = {
$: function (value, node) {
return (node || document).querySelector(value);
},
dateFormat: function (ms) {
/*格式化时间*/
var d = new Date(ms);
var pad = function (s) {
if (s.toString().length === 1) {
s = '0' + s;
}
return s;
}
var year = d.getFullYear();
var month = d.getMonth() + 1;
var date = d.getDate();
var hour = d.getHours();
var minute = d.getMinutes();
var second = d.getSeconds();
return year + '-' + pad(month) + '-' + pad(date) + ' ' + pad(hour) + ':' + pad(minute) + ':' + pad(second);
}
};
note.operate = {
/*本地存储的关键字*/
keyItem: 'stickyNote',
/*根据便签的id来获取note*/
get: function (id) {
var notes = this.getNotes();
return notes[id] || {};
},
/*设置一个note的id及其内容*/
set: function (id, content) {
var notes = this.getNotes();
if (notes[id]) {
Object.assign(notes[id], content);
} else {
notes[id] = content;
}
localStorage.setItem(this.keyItem, JSON.stringify(notes));
console.log('saved note: id: ' + id + ' content: ' + JSON.stringify(notes[id]));
},
/*根据id删除对应的便签*/
remove: function (id) {
var notes = this.getNotes();
delete notes[id];
localStorage.setItem(this.keyItem, JSON.stringify(notes));
},
/*获取localStorage中所有的note*/
getNotes: function () {
return JSON.parse(localStorage.getItem(this.keyItem) || '{}');
}
};
/*创建note*/
(function (util, operate) {
var $ = util.$;
var movedNote = null;
var startX;
var startY;
var maxZindex = 0;
var noteTemplate = `
<div class="u-title">便签 <i class="u-close">×</i></div>
<div class="u-edit" contenteditable="true"></div>
<div class="u-timeUpdate">
<span>更新时间:</span>
<span class="time"></span>
</div>
`;
function Note(options) {
var note = document.createElement('div');
note.className = 'm-note';
note.id = options.id || 'm-note-' + Date.now();
note.innerHTML = noteTemplate;
$('.u-edit', note).innerHTML = options.content || '';
note.style.left = (options.left || Math.round(Math.random() * (window.innerWidth - 320))) + 'px';
note.style.top = (options.top || Math.round(Math.random() * (window.innerHeight - 320))) + 'px';
note.style.zIndex = options.zIndex || maxZindex++;
document.body.appendChild(note);
this.note = note;
this.updateTime(options.updateTime);
this.addEvent();
}
/*时间更新方法*/
Note.prototype.updateTime = function (ms) {
var ts = $('.time', this.note);
ms = ms || Date.now();
ts.innerHTML = util.dateFormat(ms);
this.updateTimeInMs = ms;
}
/*便签保存*/
Note.prototype.save = function () {
operate.set(this.note.id, {
left: this.note.offsetLeft,
top: this.note.offsetTop,
zIndex: parseInt(this.note.style.zIndex),
content: $('.u-edit', this.note).innerHTML,
updateTime: this.updateTimeInMs
});
console.log('note saved');
}
/*删除此便签*/
Note.prototype.close = function (e) {
console.log('close note');
document.body.removeChild(this.note);
}
Note.prototype.addEvent = function () {
var that = this;
var note = this.note;
var titleBar = $('.u-title', note);
var edit = $('.u-edit', note);
var closeBtn = $('.u-close', note);
// 标题栏拖拽事件
titleBar.addEventListener('mousedown', function(e) {
if (e.target === closeBtn) return; // 排除关闭按钮
movedNote = note;
var rect = note.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
// 提升当前便签的z-index
maxZindex++;
note.style.zIndex = maxZindex;
operate.set(note.id, {
zIndex: maxZindex
});
});
// 编辑区域事件
var inputTimer;
edit.addEventListener('input', function() {
var content = edit.innerHTML;
clearTimeout(inputTimer);
inputTimer = setTimeout(function() {
var time = Date.now();
operate.set(that.note.id, {
content: content,
updateTime: time
});
that.updateTime(time);
}, 500);
});
// 关闭按钮事件
closeBtn.addEventListener('click', function(e) {
operate.remove(that.note.id);
that.close();
});
}
/*按钮的监听事件*/
document.addEventListener('DOMContentLoaded', function (e) {
$('#u-btn-create').addEventListener('click', function (e) {
new Note({});
});
/*全局鼠标移动事件*/
function mousemoveHandler(e) {
if (!movedNote) return;
movedNote.style.left = (e.clientX - startX) + 'px';
movedNote.style.top = (e.clientY - startY) + 'px';
}
function mouseupHandler(e) {
if (!movedNote) return;
operate.set(movedNote.id, {
left: movedNote.offsetLeft,
top: movedNote.offsetTop
});
movedNote = null;
}
document.addEventListener('mousemove', mousemoveHandler);
document.addEventListener('mouseup', mouseupHandler);
/*初始化*/
var notes = operate.getNotes();
Object.keys(notes).forEach(function (id) {
var options = notes[id];
if (options.zIndex > maxZindex) {
maxZindex = options.zIndex;
}
new Note(Object.assign(options, {
id: id
}));
});
// 确保maxZindex至少为1
maxZindex = Math.max(maxZindex, 1);
});
})(note.util, note.operate);
</script>
</body>
</html>