简介:面向情人节表白场景的网页定制资源包,整合网页结构、样式美化与动态交互知识,适合想亲手制作浪漫网页传递心意、同时入门前端开发的新手。资源共43个文件,压缩包约1.08MB,包含4个网页页面、7个样式表、8个脚本,以及多类图片和字体素材,目录按页面与资源分类,清晰易找。页面已预留可替换的日期与图片位,配合心形元素、悬浮动效和背景音乐,只需改成自己的纪念日和合照,通过简单的文本替换即可完成个性化定制,变成一份专属礼物。除主页面外,还提供相册、表白、历程等子页面,串联起完整的情感叙事流程。阅读源码可以学习响应式布局、动画触发、字体图标引用等实用技巧,也能为后续前端项目积累现成案例。该资源已有9236人学习下载,适合情人节、纪念日等场景快速落地,同样适合当作前端练手素材。
1. 项目定位:情人节不送模板,送一个会自己动的表白网页
情人节前夜还在挑礼物的人,最后多半会落在同一个问题上:买来的东西太通用,想送的其实是"我心里有你"这件具体的事。这份"送给女朋友的精美网页"解决的就是这个需求——一个纯前端表白网页,不依赖任何后端服务,不要求对方安装 App,一个 HTML 文件双击就能打开。页面里装着飘落的花瓣动画、自动轮播的合照、逐字打出的情书和一首合适的背景音乐;滚动鼠标或点击屏幕,页面会依次呈现照片、告白、纪念日倒计时,像在翻一本会自己动起来的相册。
这个项目适合两类人:一类是完全没写过前端的新手,想在节日前把心意变成能打开的东西;另一类是写过一点 HTML 但没时间从零搭动画的老手,拿来改改就能用。它零依赖、无框架、无构建步骤,改文案换照片就能跑,这也是我拆这个资源时最看重的一点——很多表白页项目堆了一堆库,反而让新手第一步就卡在环境上,这个项目把门槛压到了最低。
2. 技术选型与目录结构:纯前端三件套,零依赖怎么撑起完整交互
2.1 为什么只用 HTML/CSS/JavaScript
这个项目技术栈极简:HTML5 负责页面结构,CSS3 负责动画和响应式布局,原生 JavaScript 负责交互逻辑。没有 Vue、React,也没有 jQuery,更没有任何构建工具。拆之前我也有点怀疑,不引库能不能把花瓣飘落、照片轮播、打字机这些效果写清楚;拆完发现,这些效果用原生 API 反而更直接——requestAnimationFrame 做逐帧动画,Canvas 2D 画粒子,CSS transform 做轮播位移,每个模块代码都在百行以内就能讲明白。
选零依赖还有一个实际收益:整个文件夹拷贝到任何电脑都能跑,不会因为缺 node_modules 而翻车。对接收方来说,这意味着她双击 index.html 就能看到完整效果,不用装环境、不用跑命令行、不用理解什么是依赖管理。这也是表白网页项目最该守住的原则——仪式感不能建立在对方的操作成本上,越简单越不容易坏。
2.2 目录结构与每个文件的职责
项目的文件结构大致如下:
valentine-web/ ├── index.html # 页面唯一入口,四个模块的挂载点 ├── css/ │ └── style.css # 全局样式、动画关键帧、响应式断点 ├── js/ │ ├── main.js # 初始化入口,控制模块启动顺序 │ ├── petals.js # 花瓣飘落粒子系统 │ ├── carousel.js # 照片轮播逻辑 │ ├── typewriter.js # 情书逐字输出效果 │ └── music.js # 背景音乐播放与按钮状态 ├── images/ │ └── photos/ # 合照目录,按 01.jpg 递增命名 ├── music/ │ └── bgm.mp3 # 背景音乐,建议 2MB 以内 └── README.md # 给使用者的说明每个文件职责单一:index.html 只定义区块和引入脚本,style.css 管视觉,四个 JS 文件互不引用,唯一的外部联系是 main.js 在页面加载完成后依次调用它们的初始化函数。这样的划分对新手很友好——想改花瓣,只动 petals.js;想换轮播节奏,只动 carousel.js 里的一个数字,不需要在几百行代码里来回找断点。
2.3 四个模块怎么协作成完整链路
页面加载后的执行顺序是这样的:index.html 底部按顺序引入五个脚本,全部带 defer 属性,保证 DOM 解析完才执行。main.js 放在最后,确保它运行时前面四个模块的函数都已经被定义。
<script src="js/petals.js" defer></script> <script src="js/carousel.js" defer></script> <script src="js/typewriter.js" defer></script> <script src="js/music.js" defer></script> <script src="js/main.js" defer></script>main.js 做初始化检查后启动 petals 的画布动画和 carousel 的自动轮播;typewriter 不主动启动,它注册了一个 IntersectionObserver,等用户滚动到情书区块才触发打字;music 则全程被动,监听文档第一次点击来绕过浏览器的自动播放限制。四个模块通过 DOM 元素和自定义事件间接通信,没有共享全局状态。这也是我推荐新手模仿的协作方式——每个文件管好自己的状态,对外只暴露一个 init 函数,出问题时能快速定位到具体文件。
3. 核心模块拆解:从页面打开到触动的完整链路
3.1 花瓣飘落:用 Canvas 画一片不会停的花雨
花瓣是页面的第一个视觉记忆点。它的本质是一个粒子系统:每个花瓣是一个对象,记录位置、速度、旋转角度和透明度,然后每一帧更新这些属性并重新绘制。
// js/petals.js —— 花瓣粒子系统核心 const canvas = document.getElementById('petal-canvas'); const ctx = canvas.getContext('2d'); const PETAL_COUNT = 60; // 花瓣数量,手机端可调小到 30 function createPetal() { return { x: Math.random() * canvas.width, y: -20 - Math.random() * canvas.height / 2, // 从屏幕外开始 size: 8 + Math.random() * 12, // 花瓣半径 speedY: 0.8 + Math.random() * 1.5, // 下落速度 speedX: -0.5 + Math.random() * 1, // 水平漂移 rotate: Math.random() * Math.PI, // 初始旋转角 rotateSpeed: -0.02 + Math.random() * 0.04, // 旋转角速度 opacity: 0.5 + Math.random() * 0.5 // 透明度,模拟层次 }; } function drawPetal(p) { ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rotate); ctx.globalAlpha = p.opacity; ctx.fillStyle = '#ffb6c1'; ctx.beginPath(); ctx.ellipse(0, 0, p.size, p.size / 2, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); petals.forEach(p => { p.y += p.speedY; p.x += p.speedX + Math.sin(p.y * 0.01) * 0.5; // 正弦摆动模拟风 p.rotate += p.rotateSpeed; if (p.y > canvas.height + 20) { Object.assign(p, createPetal()); p.y = -20; } drawPetal(p); }); requestAnimationFrame(animate); } function resizeCanvas() { const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } resizeCanvas(); window.addEventListener('resize', resizeCanvas); petals = Array.from({ length: PETAL_COUNT }, createPetal); animate();关键逻辑在三处:createPetal 里所有属性都按随机区间初始化,让花瓣看起来没有"复制感";animate 循环里用 Math.sin 给水平位移加正弦摆动,模拟风的不规则吹拂;花瓣掉出屏幕底部后不销毁,直接重置回顶部,避免频繁创建对象引发垃圾回收卡顿。画布尺寸用 devicePixelRatio 做了限制,高分屏不会因为绘制面积过大而掉帧。
各参数对效果的影响整理如下:
| 参数 | 默认范围 | 作用 | 调参建议 |
|---|---|---|---|
| PETAL_COUNT | 60 | 花瓣密度 | 手机端降到 30 |
| speedY | 0.8 ~ 2.3 | 下落速度 | 值越大落得越快,像下雨 |
| size | 8 ~ 20 | 花瓣半径 | 影响视觉占比 |
| opacity | 0.5 ~ 1.0 | 透明度 | 越小越淡,适合浅色背景 |
3.2 照片轮播:CSS transform 实现的轻量相册
照片墙是整页情感浓度最高的部分。轮播没有用滑块库,而是用一个 flex 容器加 transform 位移实现——每个 slide 占满一屏宽度,容器整体向左平移当前索引乘以 100% 的距离。
// js/carousel.js —— 轮播核心逻辑 const track = document.querySelector('.carousel-track'); const slides = document.querySelectorAll('.carousel-slide'); const slideCount = slides.length; let currentIndex = 0; function goToSlide(index) { if (index < 0) index = slideCount - 1; if (index >= slideCount) index = 0; currentIndex = index; track.style.transform = `translateX(-${currentIndex * 100}%)`; } const AUTO_INTERVAL = 4000; // 自动播放间隔,单位毫秒 setInterval(() => goToSlide(currentIndex + 1), AUTO_INTERVAL); document.querySelector('.prev-btn') .addEventListener('click', () => goToSlide(currentIndex - 1)); document.querySelector('.next-btn') .addEventListener('click', () => goToSlide(currentIndex + 1));配合的 CSS 核心规则只有三条:
.carousel-track { display: flex; transition: transform 0.8s ease-in-out; } .carousel-slide { flex: 0 0 100%; min-height: 60vh; } .carousel-slide img { width: 100%; height: 100%; object-fit: cover; }用百分比而不是像素做位移,好处是图片宽度自适应任何屏幕,JS 里不需要测量容器宽度。goToSlide 里对越界索引做了取模回绕,所以自动轮播到最后一章会平滑跳回第一张,不会卡在边界。这里有个容易踩的细节:如果某张照片没设宽高,图片加载前后 slide 高度会跳变,导致轮播错位,所以 index.html 里每张 img 都配合了上面的 CSS 固定满宽满高。
3.3 情书打字机:让文字一个字一个字敲进心里
打字机效果比想象中简单:维护一个字符索引,每隔几十毫秒把字符串的前 N 个字符写入 DOM。真正的技巧在于触发时机——不能页面加载就开始打字,要等用户滚动到情书区块。
// js/typewriter.js —— 打字机 + 滚动触发 const letterText = document.getElementById('love-letter').dataset.text; const output = document.getElementById('typewriter-output'); let charIndex = 0; let started = false; function typeNextChar() { if (charIndex <= letterText.length) { output.textContent = letterText.slice(0, charIndex); charIndex++; setTimeout(typeNextChar, 50 + Math.random() * 40); } } const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !started) { started = true; typeNextChar(); } }); }, { threshold: 0.3 }); observer.observe(document.getElementById('love-letter-section'));两个设计细节值得注意:一是 setTimeout 的延迟用 50 + Math.random() * 40,模拟真实打字的不均匀节奏,而不是匀速机器输出;二是 IntersectionObserver 的 threshold 设为 0.3,意味着情书区块 30% 进入视口才开始打字,避免刚露个边就急着输出的突兀感。情书全文存在 index.html 里 love-letter 元素的>// js/music.js —— 首次点击启动 + 按钮切换 const audio = document.getElementById('bgm'); const toggleBtn = document.getElementById('music-toggle'); let isPlaying = false; function startMusic() { audio.volume = 0.5; // 初始音量,避免吓到人 audio.play() .then(() => { isPlaying = true; toggleBtn.textContent = '[ 音乐播放中 ]'; }) .catch(() => { /* 被拦截,等下一次点击 */ }); document.removeEventListener('click', startMusic); } document.addEventListener('click', startMusic); toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); // 防止触发 startMusic 造成状态打架 if (isPlaying) { audio.pause(); isPlaying = false; toggleBtn.textContent = '[ 音乐已暂停 ]'; } else { audio.play(); isPlaying = true; toggleBtn.textContent = '[ 音乐播放中 ]'; } });
这里有个我踩过的坑:如果不给按钮点击加 e.stopPropagation(),点击播放按钮时会同时触发 document 上的 startMusic,音乐先被暂停又被播放,界面状态却停在"播放中"。另外建议把音乐文件控制在 2MB 以内,用 128kbps 码率压缩。表白页通常是对方用手机流量打开,一首 10MB 的无损音乐会直接劝退。
4. 本地预览与上线部署:从双击 index.html 到生成可分享的链接
4.1 本地预览的三种方式与选择
拿到项目后第一件事是本地跑起来。最直接的方式是双击 index.html,对纯静态页面来说大部分效果能正常显示,但有两个隐患:一是部分浏览器对 file:// 协议下加载本地图片有限制,照片墙可能白屏;二是地址栏显示的是本地磁盘路径,发给别人也打不开。所以更推荐起一个本地静态服务,一行命令的事:
cd valentine-web python3 -m http.server 8080然后在浏览器打开 http://localhost:8080 就能看到完整效果。没有 Python 环境的话,用代码编辑器自带的 Live Server 插件也可以,右键 index.html 选择"用 Live Server 打开",效果一样。本地服务器解决了 file:// 协议的资源加载限制,开发阶段我建议固定用这种方式预览,能少踩很多资源路径的坑。
提示:如果 8080 端口被占用,换成 8090、3000 之类的其他端口即可,命令里改一下数字就行。
4.2 部署到静态托管:让链接可以被任何人打开
本地预览只能自己看,要送给对方,必须把页面部署到一个公网可访问的静态托管平台上。常见的操作路径:把项目初始化为 git 仓库,推送到代码托管平台,再开启该平台的静态页面托管功能。
git init git add . git commit -m "valentine page v1.0" git branch -M main git remote add origin <你的仓库地址> git push -u origin main推送完成后,在平台的项目设置里找到静态页面相关选项并开启,通常会得到一个形如 https://你的用户名.平台域名/valentine-web/ 的访问地址。整个流程大概十分钟。部署完建议先用浏览器无痕模式访问一次,确认图片和音乐都正常加载,再把这个链接发给对方。
如果想在正式部署前让对方先看个临时预览,也可以直接用局域网访问:电脑和手机连同一个 WiFi,查电脑的局域网 IP(macOS 和 Linux 用 ifconfig,Windows 用 ipconfig),然后在手机浏览器输入 http://你的局域网IP:8080 即可。这种方式只适合临时看一眼,电脑关机或换网络就失效,最终还是要走静态托管。
4.3 手机端适配检查清单
表白页 90% 的打开场景是手机,所以手机预览比桌面端更重要。检查时重点看四件事:首屏花瓣动画是否流畅,卡的话就降低花瓣数;照片轮播在竖屏下有没有被裁切;打字机文字在小屏上换行是否正常;音乐按钮能不能在首次点击时顺利启动。我一般会用浏览器开发者工具切成手机尺寸过一遍,再用真实手机走一遍,两个都过了才敢把链接发出去。
5. 常见问题排查:照片白屏、音乐不响、动画卡顿的踩坑记录
5.1 双击打开 index.html,照片全部不显示
现象:图片区域一片空白,浏览器控制台报错,提示加载本地资源被拒绝。
原因:file:// 协议下,部分浏览器出于安全策略不允许页面读取同目录之外的本地文件,照片和音乐被直接拦截。
解决:改用本地静态服务器预览,执行 python3 -m http.server 8080 后访问 localhost 地址;或者直接把整个文件夹部署到静态托管平台,用 https 链接访问。不建议在代码里绕过这个限制,那会让页面在真实部署时表现不一致。
5.2 手机打开页面,花瓣动画明显掉帧
现象:花瓣飘落变成"瞬移",页面滚动时卡顿明显。
原因:手机浏览器对 Canvas 绘制的性能远低于桌面端,60 个带透明度和旋转角的花瓣在中低端机型上就是极限,再加上滚动和轮播动画同时运行,主线程压力过大。
解决:检测到移动端时把 PETAL_COUNT 降到 30,同时用 devicePixelRatio 限制画布分辨率,避免高分屏上绘制面积爆炸:
const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);5.3 背景音乐点击后没声音,按钮却显示"播放中"
现象:点击页面后音乐没响,但按钮文字已经切到"播放中"。
原因:audio.play() 返回的 Promise 被 reject 后,代码里 .catch 只留了空注释,但 .then 里的状态更新可能已经执行,或者状态更新发生在 Promise 被拒绝之后,导致界面与实际播放状态不一致。
解决:把状态更新全部放到 then / catch 各自的回调里,播放失败时保持 isPlaying 为 false,按钮恢复成"点击播放音乐",等下一次真正的用户点击再试。另外优先用 MP3 格式,别用 WAV 这类体积大的格式,加载慢也会造成"点了没反应"的错觉。
5.4 照片轮播在图片加载完成后突然跳位
现象:前两张图轮播正常,到第三张开始在错误的位置偏移,或者图片还没显示就被切走。
原因:图片没有预设宽高,加载前后容器高度变化导致整体布局跳动;如果开启了图片懒加载,轮播的自动切换不会等图片就绪。
解决:给 .carousel-slide 设置 min-height,给图片设置 width: 100% 和 height: 100% 配合 object-fit: cover;如果照片数量多,在 window 的 load 事件之后再启动 setInterval 轮播,保证首屏资源就绪。
5.5 链接在手机 App 内置浏览器里打不开
现象:对方点击链接显示空白或"无法访问此网站",但电脑浏览器打开正常。
原因:很多临时预览方式生成的是 http 明文链接,部分 App 的内置浏览器会拦截不安全的请求;另外部分托管平台在移动网络下访问不稳定,首屏加载超时。
解决:优先使用支持 HTTPS 的静态托管平台,确保最终链接是 https:// 开头;发之前先关闭 WiFi 用自己的手机流量完整访问一遍,确认公网可达。如果对方打开还是很慢,检查音乐文件体积,通常这是首屏加载的最大瓶颈。
6. 二次开发指南:把通用模板改成只属于你们的版本
6.1 换照片、文案、音乐的最小改动路径
拿到模板先别急着改代码,按这个顺序操作:第一步,把照片按 01.jpg、02.jpg 的命名格式放进 images/photos 目录,覆盖原有文件,数量不够就删掉多余的 .carousel-slide 节点;第二步,找到 index.html 中 id 为 love-letter 的元素,把>// main.js 中新增的倒计时天逻辑 const startDate = new Date('2023-06-01T00:00:00'); // 改成你们的纪念日 const countdownEl = document.getElementById('countdown-days'); function updateDaysTogether() { const now = new Date(); const diff = now - startDate; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); countdownEl.textContent = `已在一起 ${days} 天`; } updateDaysTogether(); setInterval(updateDaysTogether, 60 * 1000); // 按分钟刷新足够
注意时区问题:new Date('2023-06-01T00:00:00') 用的是本地时区,如果接收方在别的时区打开,天数可能差一天。保险做法是存纯日期字符串自己在代码里解析,或者用 UTC 时间戳计算。
6.3 数据驱动重构:一份配置控制整页
当你开始做第二个版本、第三个主题时,直接在代码里搜索替换就很累了。我会把整页的可变项抽成一个配置对象放在 main.js 顶部:
// main.js —— 页面级配置 const CONFIG = { herName: '小鹿', startDate: '2023-06-01', photos: ['01.jpg', '02.jpg', '03.jpg', '04.jpg'], letter: '想陪你走很远的路,也想陪你过很慢的日子。', musicSrc: 'music/bgm.mp3', petalCount: 60, themeColor: '#ffb6c1' };然后把 petals.js 里写死的颜色、carousel.js 里写死的轮播间隔、index.html 里写死的照片路径,全部改成从 CONFIG 读取。下次想做春节版或生日版,复制整个文件夹,只改 CONFIG 里的照片路径、文字、主题色,其他代码完全不动。做完这个改造后,自测流程也固定成一套:本地服务器跑一遍、手机流量访问一遍、无痕模式访问一遍,每次只检查照片、音乐、动画三件事。从那以后我每次改完都强制走一遍这个流程,宁可在发出去之前多花五分钟自测,也不想让对方在打开页面的那一刻看到任何翻车现场。希望这套模板和上面的改造思路能帮到你,愿你的心意和这个页面一样,值得被认真打开。
本文还有配套的精品资源,点击获取