📢 新文章推送 · 每周更新优质内容 · 订阅更新 →
向下滚动
测试笔记

便签墙功能实现

AI 智能总结

功能概述

便签墙是一个纯前端实现的互动页面,访客可以发布心愿、心情或故事到一面虚拟的便签板上。便签以随机散落的方式排列,支持自由拖拽堆叠,营造真实的便签墙体验。

核心特性:

  • 随机散落布局,模拟真实便签墙的自然错落感
  • 自由拖拽堆叠,支持鼠标和触摸操作
  • 4 种分类筛选(心愿单/爱你/树洞/其他)
  • 用户发布便签,localStorage 持久化存储
  • 管理员密码验证后可删除任意便签
  • 12 种便签颜色,暗色模式完整适配
  • 动态高度计算,随便签数量自动扩容

一、数据架构

1.1 双数据源设计

便签数据来自两个来源,合并后渲染:

数据源存储位置特点
内置便签data/notes.tomlHugo 构建时注入,不可删除
用户便签localStorage运行时添加,可自行删除
 1// 内置便签:Hugo 模板注入
 2var builtinNotes = ({{ $notesData | jsonify | safeJS }}).map(function(n, i) {
 3  return {
 4    id: n.id || 9000 + i,
 5    body: n.body,
 6    author: n.author || '匿名',
 7    cat: n.cat || 'other',
 8    color: mc(n.color),
 9    date: n.date || '',
10    source: 'builtin'
11  };
12});
13
14// 用户便签:localStorage 读取
15var userNotes = load(); // JSON.parse(localStorage.getItem('lumin_notes_v4'))
16
17// 合并渲染
18function all() { return userNotes.concat(builtinNotes); }

1.2 便签数据结构

1{
2  id: 1717400000000,       // 唯一标识(时间戳 + 随机数)
3  body: "愿你历尽千帆...",  // 便签内容(最长500字)
4  author: "雾岛",           // 昵称(最长20字)
5  cat: "wish",             // 分类:wish/love/secret/other
6  color: "note-purple",    // 颜色类名
7  date: "2026-02-26",      // 发布日期
8  source: "user"           // 来源:user/builtin
9}

二、随机散落布局算法

2.1 设计思路

参考真实便签墙的视觉效果,采用「网格分区 + 大幅随机偏移」的策略:

  1. 将便签板划分为等宽列和等高行
  2. 每张便签分配到对应的网格单元
  3. 在单元内大幅随机偏移,产生自然散落感
  4. 30% 概率偏移到相邻列,增加错落感
  5. 随机旋转角度 ±5°,模拟手工贴便签的效果

2.2 核心算法

 1function scatter(notes) {
 2  var bw = board.clientWidth - 20;  // 板宽度(减去边距)
 3  var cw = window.innerWidth <= 600 ? 150 : (window.innerWidth <= 900 ? 185 : 220);
 4  var ch_est = 130;  // 卡片平均高度估计
 5
 6  // 计算列数:卡片宽度 * 0.72 为列间距系数
 7  var cols = Math.max(3, Math.floor(bw / (cw * 0.72)));
 8  var rows = Math.max(2, Math.ceil(count / cols));
 9  var cellW = bw / cols;
10  var cellH = ch_est * 1.15;  // 行高略大于卡片高度
11
12  var maxY = 0;  // 记录最底部卡片位置
13
14  for (var i = 0; i < count; i++) {
15    var col = i % cols;
16    var row = Math.floor(i / cols);
17
18    // 基础位置:网格单元内
19    var baseX = col * cellW + cellW * 0.1;
20    var baseY = row * cellH + 16;
21
22    // 随机偏移
23    var randX = (Math.random() - 0.3) * cellW * 0.6;
24    var randY = (Math.random() - 0.3) * cellH * 0.3;
25    var ox = baseX + randX;
26    var oy = baseY + randY;
27
28    // 30% 概率偏移到相邻列
29    if (Math.random() < 0.3 && i > 0) {
30      var shift = (Math.random() < 0.5 ? -1 : 1) * Math.floor(Math.random() * 2 + 1);
31      var newCol = Math.max(0, Math.min(cols - 1, col + shift));
32      ox = newCol * cellW + Math.random() * (cellW - cw + 10);
33    }
34
35    // 边界约束
36    ox = Math.max(0, Math.min(bw - cw, ox));
37    oy = Math.max(0, oy);
38
39    // 记录最底部位置
40    var cardBottom = oy + ch_est;
41    if (cardBottom > maxY) maxY = cardBottom;
42
43    // 随机旋转 ±5°
44    var rot = (Math.random() - 0.5) * 10;
45    // 随机层级
46    var zi = 50 + Math.floor(Math.random() * 40);
47
48    // 设置卡片位置
49    el.style.cssText = 'left:' + ox + 'px;top:' + oy + 'px;' +
50      'transform:rotate(' + rot.toFixed(1) + 'deg);' +
51      'z-index:' + zi + ';width:' + cw + 'px;';
52  }
53
54  // 动态计算板高度
55  board.style.height = Math.max(400, maxY + 30) + 'px';
56}

2.3 动态高度计算

便签板高度不再固定,而是根据便签实际位置动态计算:

1// 放置所有卡片后,取最底部卡片位置 + 底部边距
2var finalH = Math.max(400, maxY + 30);
3board.style.height = finalH + 'px';
  • 最小高度 400px,避免空板时过矮
  • 便签数量增加时高度自动扩容
  • 不会出现底部大片空白

三、拖拽交互

3.1 实现方式

使用原生 mousedown/mousemove/mouseup + touchstart/touchmove/touchend 事件实现:

 1function bind(el) {
 2  el.addEventListener('mousedown', function(e) {
 3    if (isAdmin || (e.button && e.button !== 0)) return;
 4    if (e.target.closest('button') || e.target.closest('input')) return;
 5    e.preventDefault();
 6    var cs = getComputedStyle(el);
 7    drag = {
 8      el: el,
 9      sx: e.clientX, sy: e.clientY,        // 起始鼠标位置
10      ox: parseInt(cs.left) || 0,            // 起始 left
11      oy: parseInt(cs.top) || 0              // 起始 top
12    };
13    el.classList.add('dragging');
14    el.style.zIndex = '10000';
15  });
16}
17
18document.addEventListener('mousemove', function(e) {
19  if (!drag) return;
20  drag.el.style.left = Math.max(-30, drag.ox + e.clientX - drag.sx) + 'px';
21  drag.el.style.top = Math.max(-15, drag.oy + e.clientY - drag.sy) + 'px';
22});
23
24document.addEventListener('mouseup', function() {
25  if (drag) {
26    drag.el.classList.remove('dragging');
27    drag.el.style.zIndex = '100';
28    drag = null;
29  }
30});

3.2 拖拽视觉反馈

 1.note-sticky:hover {
 2  z-index: 9999 !important;
 3  transform: translateY(-3px) rotate(0deg) !important;
 4  box-shadow: 0 4px 8px rgba(0,0,0,.1), 0 10px 28px rgba(0,0,0,.12) !important;
 5}
 6.note-sticky.dragging {
 7  z-index: 10000 !important;
 8  transform: none !important;
 9  opacity: .93;
10  box-shadow: 0 6px 14px rgba(0,0,0,.2), 0 14px 40px rgba(0,0,0,.28) !important;
11  cursor: grabbing;
12}
  • 悬停时上浮 3px + 旋转归零 + 阴影加深
  • 拖拽时半透明 + 超大阴影 + 抓手光标

四、便签颜色系统

4.1 12 色便签

颜色亮色背景亮色文字暗色背景暗色文字
yellow#fef9c3#5c3d0a#332d1a#fde68a
pink#fce7f3#6b1d40#2d1a22#f9a8d4
blue#dbeafe#172c4c#182338#93c5fd
green#d1fae5#064e3b#142a20#6ee7b7
purple#ede9fe#2e1065#1e1630#c4b5fd
orange#ffedd5#5c2407#2d2014#fdba74
teal#ccfbf1#0f4038#142a26#5eead4
rose#ffe4e6#5c1124#2d1a1e#fda4af
mint#ecfdf5#064e3b#142a22#6ee7c4
lavender#f5f0ff#3b0764#1e1630#d8b4fe
peach#fff5ee#5c2004#2d1e14#fdba74
sky#e8f4fd#0c2d48#162438#7dd3fc

暗色模式下每张便签还带有微光内阴影:

1[data-theme="dark"] .note-yellow {
2  background: #332d1a;
3  color: #fde68a;
4  box-shadow: 0 1px 2px rgba(0,0,0,.2),
5              0 3px 10px rgba(0,0,0,.15),
6              0 0 0 .5px rgba(255,255,180,.03) inset;
7}

4.2 颜色分配

  • 内置便签:通过 data/notes.toml 中的 color 字段指定,支持 c-yellownote-yellow 两种写法
  • 用户便签:发布时随机分配颜色 rc()

五、分类筛选

5.1 筛选逻辑

 1var CAT_NAMES = { wish: '心愿单', love: '爱你', secret: '树洞', other: '其他' };
 2
 3function render() {
 4  var notes = all();
 5  if (curFilter !== 'all') {
 6    notes = notes.filter(function(n) { return n.cat === curFilter; });
 7  }
 8  if (notes.length === 0) {
 9    empty.style.display = 'block';
10    return;
11  }
12  scatter(notes);
13}

筛选按钮使用 data-cat 属性标记分类,点击时更新 curFilter 并重新渲染:

1<button class="notes-filter-btn active" data-cat="all">全部</button>
2<button class="notes-filter-btn" data-cat="wish">心愿单</button>
3<button class="notes-filter-btn" data-cat="love">爱你</button>
4<button class="notes-filter-btn" data-cat="secret">树洞</button>
5<button class="notes-filter-btn" data-cat="other">其他</button>

六、用户发布流程

 1function add() {
 2  var b = (input.value || '').trim();
 3  if (!b) { input.focus(); return; }
 4  var a = (authorInp.value || '').trim() || '匿名';
 5  var c = catSel.value || 'other';
 6  var now = new Date();
 7  var d = now.getFullYear() + '-' +
 8    String(now.getMonth() + 1).padStart(2, '0') + '-' +
 9    String(now.getDate()).padStart(2, '0');
10
11  userNotes.unshift({
12    id: Date.now() + Math.floor(Math.random() * 1e4),
13    body: b, author: a, cat: c,
14    color: rc(), date: d, source: 'user'
15  });
16
17  save(userNotes);  // localStorage 持久化
18  input.value = ''; authorInp.value = ''; catSel.value = 'wish';
19  curFilter = 'all';
20  render();
21  window.scrollTo({ top: board.offsetTop - 40, behavior: 'smooth' });
22}
  • 支持回车提交(Enter 键,非 Shift+Enter)
  • 发布后自动滚动到便签板位置
  • 发布后重置筛选为"全部",确保新便签可见

七、管理员功能

7.1 密码验证

管理员密码通过 Hugo 配置注入:

1var ADMIN_PW = '{{ site.Params.adminPassword | default "your-password" }}';

验证流程:点击「管理」→ 显示密码输入框 → 输入密码回车 → 验证通过进入管理模式

7.2 删除权限

 1// 管理模式下,所有便签显示删除按钮
 2.note-sticky.show-delete .note-delete { display: inline-flex; }
 3
 4// 删除逻辑区分来源
 5if (note.source === 'user') {
 6  // 用户便签:从 localStorage 删除并重新渲染
 7  userNotes = userNotes.filter(function(x) { return x.id !== note.id; });
 8  save(userNotes);
 9  render();
10} else if (isAdmin) {
11  // 管理员删除内置便签:淡出动画后移除 DOM
12  el.style.opacity = '0';
13  el.style.transform = 'scale(0.8)';
14  setTimeout(function() { el.remove(); }, 250);
15}

八、响应式设计

断点卡片宽度容器边距Banner 高度
> 900px220px32px160px
600-900px185px14px160px
< 600px150px8px140px

移动端额外适配:

  • 发布栏改为纵向排列(输入框/昵称/按钮各占一行)
  • 卡片内边距缩小,字号缩小
  • 便签板高度动态计算,无需固定值

九、交互细节

操作效果
悬停便签上浮 3px + 旋转归零 + 阴影加深
拖拽便签半透明 + 超大阴影 + 抓手光标
双击空白重新随机散落排列
发布便签自动滚动到便签板 + 成功提示 3 秒后恢复
窗口缩放防抖 300ms 后重新渲染布局
滚动溢出底部显示「滚轮查看更多」提示,滚动后淡出
3 / 9
版权声明

本文作者 Lumin

本文链接 https://www.zhengquan.xyz/biji/notes-wall-implementation/

许可协议 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

请作者喝杯咖啡 ☕

  • 微信打赏
    微信支付
  • 支付宝打赏
    支付宝
点击按钮查看打赏二维码
🎁 推荐工具
试试这些实用在线工具,提升工作效率
前往工具集 →

留言评论

期待你的想法

评论加载中