
每年七夕前后朋友圈总会被各种表白 H5、动态相册、情侣互动小游戏刷屏。可细看下来大多页面长得都差不多粉色渐变背景、一对卡通小人、滚动字幕再配一首烂大街的背景音乐。打开第一个和打开第十个体验几乎没有差别。于是很多人会发出这样的感叹七夕怎么会和谁过都一样呢。其实答案很简单因为大家都是直接套模板没有把“你们的真实内容”放进页面。与其用网上千篇一律的七夕模板不如用前端三件套亲手做一个专属表白网页。把对方喜欢的配色、你们的纪念日、想说的话、挑选的音乐都放进去做到“第一眼就知道是为你做的”。这篇文章我会完整拆解一个七夕表白网页的开发过程包含粒子动画背景、七夕倒计时、情书翻卡和背景音乐四大功能。全程使用 HTML、CSS、原生 JavaScript不需要安装任何框架复制代码就能运行。1. 七夕网页为什么会“千篇一律”1.1 模板页的共性问题网上的七夕表白模板往往存在几个问题视觉效果同质化严重粉色背景、发光文字、爱心飘落换汤不换药。内容无法沉淀模板里的情话是作者写的不是你写的缺少真实感。交互过于单一只有“滑动翻页”或“点击按钮”没有参与感。代码不透明很多模板是压缩或加密后的代码想改样式只能照猫画虎。这些问题带来的直接结果就是页面看起来很漂亮但和用户没有任何情感连接。1.2 自己开发的优势自己动手写一个七夕网页优势非常明显完全可控每一处文案、颜色、图片、音乐都可以自定义。学习价值高即使最终不使用过程中也能掌握 Canvas、日期计算、CSS 3D 变换等前端实用技能。可扩展性强今天做七夕页面明天改成生日页面、纪念日页面只需要换配置文本。符合实际场景可以放进 Nginx、GitHub Pages、对象存储等任意静态托管环境手机电脑都能打开。1.3 本文项目功能概览本文要实现的页面包含四个核心模块功能模块技术方案效果说明粒子背景Canvas 2D requestAnimationFrame粉色粒子漂浮鼠标靠近时产生推离效果七夕倒计时Date 对象 setInterval实时显示距七夕的天、时、分、秒情书翻卡CSS transform 3D点击按钮后卡片 3D 翻转展示情书内容背景音乐HTML Audio 元素点击按钮播放/暂停默认不自动播放这四个功能都基于原生 Web 技术实现不依赖 Vue、React 等框架也没有构建步骤非常适合拿来练手也方便理解底层原理。2. 环境准备与项目结构2.1 开发环境这个项目的开发门槛很低只需要满足以下条件操作系统Windows / macOS / Linux 均可。浏览器推荐 Chrome、Edge 或 Firefox 最新版本需要支持 Canvas 和 CSS 3D 属性。编辑器Visual Studio Code或任何能编辑文本文件的工具。运行方式直接双击index.html用浏览器打开不需要安装 Node.js 或 Python。如果你希望在本地启动一个静态服务也可以使用 VS Code 的 Live Server 插件或者执行npx serve .这类工具。不过本文的最小示例不依赖这些。2.2 项目目录建议先创建如下目录结构valentine-web/ ├── index.html ├── css/ │ └── style.css ├── js/ │ └── main.js └── assets/ └── music.mp3其中index.html是页面骨架。css/style.css负责布局、配色和动画效果。js/main.js负责粒子背景、倒计时、卡片翻转和音乐控制。assets/music.mp3是背景音乐文件你可以替换成自己的音频。2.3 技术选型为什么要用原生技术而不是框架原因有三场景足够简单不需要组件化拆分。原生 DOM API 和 Canvas API 可以直观展示核心原理。页面最终是要部署到静态托管平台的原生三件套体积最小加载最快。如果你后续要扩展投票、留言、多人互动等功能再考虑引入 Vue 或 React 也不迟。当前阶段应优先把核心效果跑通。3. 核心原理解析在写完整代码之前先把四个核心功能的原理讲清楚。理解原理后再看代码会顺畅很多。3.1 Canvas 粒子背景Canvas 是 HTML5 提供的绘图能力。它并不是真正的矢量图形而是一个像素画布通过 JavaScript 控制每个点的位置和颜色。粒子背景的核心逻辑是获取 Canvas 的 2D 上下文也就是ctx。初始化一组粒子对象每个粒子包含位置x、y、移动速度vx、vy和大小size。在每一帧里更新粒子的坐标。如果粒子的横坐标或纵坐标超出边界就让对应方向的速度取反产生反弹效果。计算粒子与鼠标之间的距离如果足够近就把粒子推开形成交互。最后绘制粒子并在相邻粒子之间画连接线。第 6 步需要遍历粒子数组做两两距离计算。当粒子数量不多时这个 O(n²) 的算法完全没有问题。如果粒子数量提升到几千就需要考虑空间网格或四叉树优化。3.2 倒计时实现倒计时的本质是“目标时间戳”减去“当前时间戳”得到一个毫秒差再换算成天、时、分、秒。计算公式如下天数 毫秒差 / 86400000 后向下取整。小时 毫秒差 % 86400000 再除以 3600000 后向下取整。分钟 毫秒差 % 3600000 再除以 60000 后向下取整。秒 毫秒差 % 60000 再除以 1000 后向下取整。这里要注意一个隐藏问题如果目标日期已经过去也就是年份配置错了毫秒差会变成负数。如果不处理页面上会出现负数。所以代码里需要加一层判断当差值小于 0 时直接置为 0避免显示异常。3.3 CSS 3D 翻卡效果情书卡片使用 CSS 3D 变换实现。核心属性是perspective和transform-style: preserve-3d。perspective给父容器添加透视距离让子元素的 3D 变换看起来有立体感。transform-style: preserve-3d让子元素在三维空间内保留自己的变换。backface-visibility: hidden隐藏元素的背面翻转时不会看到镜像内容。卡片由.letter-front和.letter-back两层组成两层都绝对定位在同一个容器内部。默认情况下正面朝上背面通过rotateY(180deg)预先翻转。点击按钮后给外层卡片添加.flipped类让整体容器旋转 180 度背面自然转到前方。3.4 背景音乐交互浏览器对音频自动播放有严格限制。绝大多数现代浏览器都要求用户与页面发生一次交互后才能调用audio.play()。所以代码里不能直接一打开页面就播放音乐而是要把播放动作绑定在按钮点击事件上。音频元素使用 HTML 的audio标签audio idbgMusic srcassets/music.mp3 loop preloadauto/audioloop表示循环播放preloadauto表示页面加载时尽量预加载音频文件。播放时调用audio.play()暂停时调用audio.pause()并通过按钮文案切换当前状态。3.5 响应式与性能要点页面需要在手机和电脑上都能正常显示所以要做响应式适配。布局方面主容器使用flex居中倒计时模块允许换行在窄屏下自动排列。字体大小不建议像传统 PC 页面那样固定写死可以使用 CSS 媒体查询在小屏幕下适当缩小。性能方面粒子背景的每一帧都在执行 JavaScript 绘制。如果粒子数量过多移动端会卡顿。因此粒子数量要根据屏幕面积动态计算而不是写死一个很大的值。文章代码里使用了一个简单的自适应公式屏幕越大粒子越多但不会超过上限。4. 完整实战案例下面进入完整实现环节。按照 4.1 到 4.5 的顺序一步一步把页面搭建起来。4.1 创建项目结构先在本地新建一个项目目录例如valentine-web然后创建如下文件夹和文件valentine-web/ ├── index.html ├── css/ │ └── style.css ├── js/ │ └── main.js └── assets/ └── music.mp3如果暂时没有音乐文件assets/music.mp3可以先留空等找到合适的音频后再放进去。不要使用未经授权受版权保护的音乐建议使用自己录制的音频或免费音乐素材。4.2 编写 index.html 页面骨架文件路径index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title七夕专属告白页/title link relstylesheet hrefcss/style.css /head body canvas idparticleCanvas/canvas main classcontainer section classhero h1 classhero-title七夕快乐/h1 p classhero-subtitle这不是模板网页是我为你写的专属心意/p div idcountdown classcountdown div classcountdown-item span iddays00/span span天/span /div div classcountdown-item span idhours00/span span时/span /div div classcountdown-item span idminutes00/span span分/span /div div classcountdown-item span idseconds00/span span秒/span /div /div /section section classletter-section button idopenLetterBtn classbtn-open打开情书/button div idletterCard classletter-card div classletter-card-inner div classletter-front p点击翻面查看情书/p /div div classletter-back p亲爱的你/p p遇见你之后每天都是七夕。/p p—— 来自会写代码的人/p /div /div /div /section section classmusic-section button idmusicBtn classbtn-music播放音乐/button audio idbgMusic srcassets/music.mp3 loop preloadauto/audio /section /main script srcjs/main.js/script /body /html这个文件的结构很清晰canvas是背景层main.container是内容层。内容层包含标题、倒计时、情书卡片和音乐按钮四块。4.3 编写 style.css 样式文件路径css/style.css/* 全局基础样式 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: PingFang SC, Microsoft YaHei, sans-serif; background: linear-gradient(135deg, #2d1b3d 0%, #8e4b6d 50%, #f7b7c4 100%); min-height: 100vh; color: #fff; overflow-x: hidden; } /* 粒子画布作为背景层 */ #particleCanvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; } /* 内容层 */ .container { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 40px 20px; text-align: center; } .hero-title { font-size: 48px; letter-spacing: 4px; text-shadow: 0 0 20px rgba(255, 182, 193, 0.8); } .hero-subtitle { margin-top: 12px; font-size: 18px; opacity: 0.9; } /* 倒计时卡片 */ .countdown { display: flex; gap: 20px; margin-top: 24px; flex-wrap: wrap; justify-content: center; } .countdown-item { background: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 12px; padding: 16px 20px; min-width: 80px; backdrop-filter: blur(6px); } .countdown-item span:first-child { display: block; font-size: 36px; font-weight: bold; } .countdown-item span:last-child { font-size: 14px; opacity: 0.8; } /* 按钮 */ .btn-open, .btn-music { border: none; border-radius: 30px; padding: 12px 28px; font-size: 16px; cursor: pointer; background: linear-gradient(135deg, #f78fb3, #f06595); color: #fff; box-shadow: 0 4px 15px rgba(240, 101, 149, 0.4); transition: transform 0.2s ease, box-shadow 0.2s ease; } .btn-open:hover, .btn-music:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(240, 101, 149, 0.5); } /* 情书卡片 */ .letter-section { margin-top: 40px; } .letter-card { width: 320px; height: 220px; margin: 20px auto 0; perspective: 1000px; } .letter-card-inner { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.8s ease; } .letter-card.flipped .letter-card-inner { transform: rotateY(180deg); } .letter-front, .letter-back { position: absolute; top: 0; left: 0; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); } .letter-front { background: linear-gradient(135deg, #ffecd2, #fcb69f); color: #6b3f3f; } .letter-back { background: linear-gradient(135deg, #fff5f7, #fde3e7); color: #5c3a3a; transform: rotateY(180deg); } .music-section { margin-top: 30px; }这里说明几个关键点pointer-events: none让 Canvas 不拦截鼠标事件按钮和卡片都能正常点击。粒子 Canvas 使用position: fixed铺满全屏内容层使用position: relative和更高的z-index覆盖在上方。卡片使用perspective和transform-style: preserve-3d实现 3D 翻转。按钮在 hover 时轻微上移并增加阴影提升交互反馈感。4.4 编写 main.js 核心逻辑文件路径js/main.js(function () { use strict; // 配置项 const CONFIG { // 目标时间七夕是农历七月初七每年公历日期不同需要按实际年份修改 targetDate: 2026-08-19T00:00:00, // 粒子数量上限 particleCount: 80, // 粒子之间连线的最大距离 linkDistance: 120, // 粒子颜色 particleColor: #f78fb3, // 连线颜色 linkColor: rgba(255, 182, 193, 0.35) }; // 粒子背景 const canvas document.getElementById(particleCanvas); const ctx canvas.getContext(2d); let particles []; let width 0; let height 0; const mouse { x: null, y: null, radius: 150 }; function initCanvas() { width canvas.width window.innerWidth; height canvas.height window.innerHeight; } function createParticles() { particles []; // 根据屏幕面积动态调整粒子数量避免移动端卡顿 const count Math.min(CONFIG.particleCount, Math.floor((width * height) / 18000)); for (let i 0; i count; i) { particles.push({ x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.8, vy: (Math.random() - 0.5) * 0.8, size: Math.random() * 2 1 }); } } function drawParticles() { ctx.clearRect(0, 0, width, height); for (let i 0; i particles.length; i) { const p particles[i]; // 更新坐标 p.x p.vx; p.y p.vy; // 边界反弹 if (p.x 0 || p.x width) { p.vx * -1; } if (p.y 0 || p.y height) { p.vy * -1; } // 鼠标推离效果 if (mouse.x ! null mouse.y ! null) { const dx mouse.x - p.x; const dy mouse.y - p.y; const dist Math.sqrt(dx * dx dy * dy); if (dist mouse.radius) { p.x - (dx / dist) * 0.6; p.y - (dy / dist) * 0.6; } } // 绘制粒子 ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fillStyle CONFIG.particleColor; ctx.fill(); } // 绘制粒子连线 for (let i 0; i particles.length; i) { for (let j i 1; j particles.length; j) { const a particles[i]; const b particles[j]; const dx a.x - b.x; const dy a.y - b.y; const dist Math.sqrt(dx * dx dy * dy); if (dist CONFIG.linkDistance) { ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.strokeStyle CONFIG.linkColor; ctx.lineWidth 1; ctx.stroke(); } } } requestAnimationFrame(drawParticles); } // 倒计时 function updateCountdown() { const target new Date(CONFIG.targetDate).getTime(); const now Date.now(); let diff target - now; // 目标时间已过时直接归零 if (diff 0) { diff 0; } const days Math.floor(diff / 86400000); const hours Math.floor((diff % 86400000) / 3600000); const minutes Math.floor((diff % 3600000) / 60000); const seconds Math.floor((diff % 60000) / 1000); document.getElementById(days).textContent String(days).padStart(2, 0); document.getElementById(hours).textContent String(hours).padStart(2, 0); document.getElementById(minutes).textContent String(minutes).padStart(2, 0); document.getElementById(seconds).textContent String(seconds).padStart(2, 0); } // 情书卡片 function initLetterCard() { const btn document.getElementById(openLetterBtn); const card document.getElementById(letterCard); btn.addEventListener(click, function () { card.classList.toggle(flipped); btn.textContent card.classList.contains(flipped) ? 收起情书 : 打开情书; }); } // 背景音乐 function initMusic() { const btn document.getElementById(musicBtn); const audio document.getElementById(bgMusic); let playing false; btn.addEventListener(click, function () { if (!playing) { audio.play().then(function () { playing true; btn.textContent 暂停音乐; }).catch(function () { alert(无法自动播放音乐请点击页面后重试); }); } else { audio.pause(); playing false; btn.textContent 播放音乐; } }); } // 初始化 function init() { initCanvas(); createParticles(); updateCountdown(); setInterval(updateCountdown, 1000); initLetterCard(); initMusic(); // 鼠标交互监听挂载在 window 上避免被 Canvas 的 pointer-events 影响 window.addEventListener(mousemove, function (event) { mouse.x event.clientX; mouse.y event.clientY; }); window.addEventListener(mouseout, function (event) { if (!event.relatedTarget) { mouse.x null; mouse.y null; } }); window.addEventListener(resize, function () { initCanvas(); createParticles(); }); // 启动粒子动画 drawParticles(); } document.addEventListener(DOMContentLoaded, init); })();代码看起来不短但逻辑是线性的。前面是配置项后面按功能模块拆分成小函数最后在init中依次调用。这样做的好处是想调整粒子颜色只需要改CONFIG.particleColor想换倒计时日期只需要改CONFIG.targetDate想换情书内容直接改 HTML 里的.letter-back文案。4.5 运行与验证写完上述三个文件后双击打开index.html或者在 VS Code 中右键选择 “Open with Live Server”应该能看到以下效果页面背景是从深紫到粉红的渐变。粉色粒子在背景中缓慢移动鼠标靠近时粒子会被推开。标题下方显示倒计时卡片每秒跳动一次。点击“打开情书”按钮卡片正面翻转显示情书内容。点击“播放音乐”按钮背景音乐开始播放按钮文案变成“暂停音乐”。如果某个功能没有生效不用急着改代码先打开浏览器开发者工具查看 Console 面板有没有红色报错。大部分问题都会在控制台暴露出来。5. 常见问题与排查思路在实际开发和部署中新手最容易遇到以下几类问题。问题现象常见原因解决思路打开页面后没有粒子main.js路径错误或浏览器不支持 Canvas检查script标签的src路径查看控制台报错倒计时显示NaNtargetDate格式不符合new Date()解析规则改为2026-08-19T00:00:00这种标准格式音乐按钮点击没反应音频文件路径错误或格式不支持确认assets/music.mp3是否存在建议使用 MP3 格式按钮点击后无法播放音乐浏览器阻止自动播放将播放逻辑绑定到点击事件或提示用户先点击页面卡片翻转后文字镜像缺少backface-visibility: hidden检查.letter-front和.letter-back是否都设置了隐藏背面手机端滑动明显卡顿粒子数量过多连线计算量太大降低particleCount或缩小linkDistance页面打开时背景音乐没有自动播放浏览器自动播放策略限制从产品体验角度考虑点击后再播放更合理不建议强行绕过逐个解释一下。第一类问题是路径问题。index.html在项目根目录main.js在js目录下所以引用时要写js/main.js而不是main.js。CSS 同理要写css/style.css。如果文件路径写错控制台会直接提示 404。第二类问题是日期格式。new Date(2026-08-19)在大多数浏览器中可以解析但为了保险建议使用带时间的完整格式例如2026-08-19T00:00:00。要特别注意七夕是农历七月初七每年的公历日期都不同使用前一定要查一下当年的日期。第三类问题是音频资源。如果使用本地路径assets/music.mp3但assets目录下没有这个文件audio.play()会返回一个失败的 Promise所以代码里加了.catch提示用户。更多时候是文件本身存在但格式不受支持建议统一使用 MP3 格式兼容性最好。第四类问题是浏览器自动播放限制。Chrome、Safari 等浏览器要求用户与页面交互后才能播放有声音的媒体文件。所以示例代码没有写audio.play()在页面加载时执行而是绑定在按钮点击事件中。如果确实希望进入页面后自动播放只能在用户手动点击页面任意位置后再播放但这种方式体验并不好不建议采用。第五类问题是 CSS 3D 效果。如果某个低版本浏览器不支持backface-visibility卡片翻转后可能会看到镜像文字。建议在 Chrome、Edge、Firefox 的最新版本中测试。如果必须兼容旧浏览器可以加各浏览器前缀例如-webkit-backface-visibility。第六类是性能问题。粒子连接线算法是双重循环计算量会随粒子数量增加而快速上升。比如 50 个粒子需要计算 1225 组距离100 个粒子需要计算 4950 组130 个粒子接近 8385 组。对于移动端设备建议把粒子数量控制在 40 到 60 个之间并把linkDistance缩短到 80 左右。6. 最佳实践与工程建议功能跑通只是第一步。如果想把这个页面真正发给对方或者作为项目作品展示还需要考虑工程化和部署层面的事情。6.1 把配置项统一抽离代码里的CONFIG对象就是一个很好的做法。所有可能变化的参数都集中放在文件顶部而不是散落在函数内部。后续调整日期、颜色、粒子数量时只需要改一处。同理情书文案也应该集中放在 HTML 结构里不要直接写在 JavaScript 中这样即使不懂 JavaScript 的人也能通过替换文字来修改内容。6.2 注意个人信息安全如果页面要分享给指定的人建议不要在页面公开展示过多个人真实信息比如出生日期、身份证号、家庭住址等。网页一旦部署到公网就可能被搜索引擎收录或被人转发隐私风险不可忽视。情书内容尽量使用温和、正向的表达。6.3 使用免费或自制的音乐素材背景音乐涉及版权问题。如果使用有版权的商业歌曲即使只是个人分享也可能存在侵权风险。建议使用自己录制或创作的音频。使用标注了“可商用”的免费音乐素材库。在页面角落标注音乐来源。6.4 部署到静态托管平台当页面开发完成后可以将整个valentine-web目录上传到任意静态网站托管服务。这里推荐几种常见方式GitHub Pages适合保存代码和展示页面。Nginx适合有服务器、希望绑定自己域名的场景。对象存储静态网站托管适合追求高可用和访问速度的场景。部署时注意路径问题。如果项目不是部署在域名根目录而是放在子路径下那么 CSS、JS 和音频的路径建议使用相对路径也就是代码中一直使用的css/style.css、js/main.js这种写法而不是以/开头。这样切换到任何子路径都能正常访问。6.5 移动端体验优化七夕表白页面大部分场景是在手机上打开。建议针对移动端做以下优化使用viewport设置页面宽度代码中已经包含。大标题在小屏幕下降低字号避免换行错乱。按钮的点击区域不要太小建议至少 44px 高度。粒子动画性能优先减少粒子数量。背景音乐建议控制音量避免突然播放惊吓到用户。6.6 代码版本管理不要因为项目小就直接跳过版本管理。使用 Git 初期可能会有额外成本但长期来看可以帮你随时回退到可用版本也能把页面保存到远程仓库防止本地文件丢失。在项目根目录执行以下命令git init git add . git commit -m feat: 完成七夕表白页第一版以后每次修改都提交一次注释写明改了什么例如“fix: 修复倒计时显示 NaN 的问题”。6.7 增加合理异常处理前端页面虽然简单但异常处理不能忽略。比如浏览器不支持 Canvas 时应提示用户升级浏览器。音频加载失败时播放按钮要给出提示。倒计时日期配置错误时页面上应该显示可读的提示而不是NaN。这些逻辑都可以通过if判断和try/catch来覆盖。代码示例已经处理了音频播放失败的情况其他异常你也可以参考这个思路继续完善。7. 总结与后续学习方向这篇文章从“七夕模板千篇一律”这个痛点出发完整实现了一个可定制、可部署的七夕表白网页。你可以掌握以下关键技能使用 Canvas 2D 实现粒子背景和鼠标交互。使用 Date 对象和 setInterval 实现倒计时。使用 CSS 3D 变换实现翻卡动画。使用 HTML Audio 实现背景音乐控制。掌握静态页面的资源路径、响应式和部署注意事项。如果你想把页面做得更丰富可以继续沿着这些方向扩展在 Canvas 中绘制爱心形状的粒子群而不是只做随机漂浮。加入照片轮播或文字打字机效果。增加留言输入框把对方回给的内容保存到后端。封装成“纪念日通用页面”通过 URL 参数控制文案和日期。引入 CSS 动画库让标题、按钮出现时带进场动画。七夕这个日子原本没有特别之处正因为我们把时间和心思放进去它才有了意义。网页模板可以复制但你想说的话、你选的歌、你配置的日期组合在一起就成了独一无二的东西。代码已经全部放在上面剩下的创意就交给你了。如果这篇文章对你有帮助可以先收藏备用等你真正动手做一个表白页的时候一定能用得上。