恋爱时间倒计时网页设计与实现方案
一、项目概述
本项目旨在创建一个高度可定制的恋爱时间倒计时网页,支持纪念日日期设置、背景主题切换、个性化文案定制等功能,并通过localStorage保存用户配置。技术栈将采用HTML5、Tailwind CSS v3和原生JavaScript,结合Canvas粒子动画实现视觉吸引力。
二、核心功能设计
-
双模式计时系统
- 正计时:记录恋爱天数(支持精确到秒级更新)
- 倒计时:重要纪念日提醒(如100天、周年纪念)
-
个性化定制面板
- 日期选择器:支持公历/农历切换
- 背景设置:纯色渐变、粒子动画、自定义图片上传
- 主题系统:预设3套浪漫主题(粉紫渐变/星空蓝/蜜桃粉)
- 文案定制:主标题、副标题自定义
- 字体选择:3种字体风格(手写体/衬线体/无衬线体)
-
视觉动效设计
- 爱心粒子背景:鼠标交互时粒子聚合为爱心形状
- 时间数字动画:数字变化时的平滑过渡效果
- 纪念日里程碑:特殊天数(如520天)的烟花特效
三、技术实现方案
- 倒计时核心逻辑
// 高精度计时实现(避免setInterval延迟问题)
function startCountdown(targetDate) {const updateTimer = () => {const now = new Date().getTime();const diff = targetDate - now;// 时间计算逻辑const days = Math.floor(diff / (1000 * 60 * 60 * 24));// ...小时/分钟/秒计算// DOM更新updateDOM(days, hours, minutes, seconds);if (diff <= 0) {// 倒计时结束逻辑return;}// 动态调整下一次执行时间(修正定时器误差)const nextUpdate = Math.max(1000, diff % 1000);setTimeout(updateTimer, nextUpdate);};updateTimer();
}
- 粒子背景实现(基于Canvas API)
class ParticleBackground {constructor(canvasId) {this.canvas = document.getElementById(canvasId);this.ctx = this.canvas.getContext('2d');this.particles = [];this.resizeCanvas();this.initParticles(120); // 创建120个粒子this.animate();}initParticles(count) {for (let i = 0; i < count; i++) {this.particles.push({x: Math.random() * this.canvas.width,y: Math.random() * this.canvas.height,size: Math.random() * 3 + 1,speedX: (Math.random() - 0.5) * 0.5,speedY: (Math.random() - 0.5) * 0.5,color: this.getRandomColor()});}}// 爱心形状鼠标交互handleMouseMove(e) {// 粒子向鼠标位置聚集形成爱心路径}animate() {this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);// 更新粒子位置和连线requestAnimationFrame(() => this.animate());}
}
- 用户配置持久化
// 配置数据结构
const DEFAULT_CONFIG = {anniversaryDate: '2023-01-01',title: '我们的恋爱时光',subtitle: '记录每一刻心动',theme: 'pink-love',backgroundType: 'particle',customBackground: '',font: 'handwriting'
};// 保存配置到localStorage
function saveConfig(config) {try {localStorage.setItem('loveCounterConfig', JSON.stringify(config));} catch (e) {console.error('配置保存失败:', e);}
}// 加载配置
function loadConfig() {const saved = localStorage.getItem('loveCounterConfig');return saved ? JSON.parse(saved) : DEFAULT_CONFIG;
}
四、UI/UX设计规范
-
色彩系统
- 主色调:#FF6B8B(浪漫粉)
- 辅助色:#8A2BE2(梦幻紫)、#FFD700(香槟金)
- 中性色:#F9F9F9(背景)、#333333(文字)
-
响应式布局
- 移动端:单列布局,配置项折叠为底部抽屉
- 平板:双列布局,左侧配置+右侧预览
- 桌面端:三栏布局,增加快捷操作区
-
交互反馈
- 配置变更时实时预览
- 操作成功的微动画提示
- 错误状态的友好提示
五、项目结构
love-counter/
├── index.html # 主页面
├── src/
│ ├── css/
│ │ └── styles.css # Tailwind自定义样式
│ ├── js/
│ │ ├── countdown.js # 计时逻辑
│ │ ├── particle.js # 粒子背景
│ │ ├── config.js # 配置管理
│ │ └── ui.js # UI交互
│ └── assets/
│ └── fonts/ # 自定义字体
├── tailwind.config.js # 主题配置
└── README.md # 项目文档
六、优化与兼容性
-
性能优化
- Canvas动画节流(requestAnimationFrame)
- 图片懒加载与压缩
- 配置项变更防抖处理
-
浏览器兼容
- 支持Chrome/Edge/Safari最新版
- 降级处理IE浏览器(简化动画效果)
-
可访问性
- 语义化HTML结构
- 键盘导航支持
- 颜色对比度符合WCAG标准
源代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>恋爱时间倒计时</title><script src="https://cdn.tailwindcss.com"></script><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><script>tailwind.config = {theme: {extend: {colors: {love: {pink: '#FF6B8B',purple: '#8A2BE2',gold: '#FFD700',light: '#FFF0F3',dark: '#333333'}},fontFamily: {handwriting: ['Segoe Script', 'Brush Script MT', 'cursive'],serif: ['Georgia', 'Cambria', 'serif'],sans: ['Inter', 'system-ui', 'sans-serif']},animation: {'heartbeat': 'heartbeat 1.5s ease-in-out infinite','fade-in': 'fadeIn 0.5s ease-out forwards','slide-up': 'slideUp 0.5s ease-out forwards'},keyframes: {heartbeat: {'0%, 100%': { transform: 'scale(1)' },'50%': { transform: 'scale(1.2)' }},fadeIn: {'0%': { opacity: '0' },'100%': { opacity: '1' }},slideUp: {'0%': { transform: 'translateY(20px)', opacity: '0' },'100%': { transform: 'translateY(0)', opacity: '1' }}}}}}</script><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.text-shadow {text-shadow: 0 2px 4px rgba(0,0,0,0.1);}.particle-bg {position: absolute;top: 0;left: 0;width: 100%;height: 100%;z-index: 0;}.config-panel {transform: translateX(0);transition: transform 0.3s ease-in-out;}.config-panel.hidden {transform: translateX(100%);}@media (max-width: 768px) {.config-panel {transform: translateY(100%);height: 80vh;border-radius: 1rem 1rem 0 0;}.config-panel.hidden {transform: translateY(100%);}.config-panel.active {transform: translateY(0);}}}</style>
</head>
<body class="font-sans bg-love-light text-love-dark overflow-x-hidden"><!-- 粒子背景画布 --><canvas id="particleCanvas" class="particle-bg"></canvas><!-- 主容器 --><div class="relative min-h-screen flex flex-col items-center justify-center p-4 z-10"><!-- 头部标题区 --><header class="text-center mb-8 animate-fade-in"><h1 id="mainTitle" class="text-[clamp(2rem,5vw,3.5rem)] font-handwriting font-bold text-love-pink text-shadow">我们的恋爱时光</h1><p id="subTitle" class="text-[clamp(1rem,2vw,1.5rem)] text-gray-600 mt-2">记录每一刻心动</p></header><!-- 倒计时显示区 --><div class="w-full max-w-3xl mx-auto bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 md:p-10 animate-slide-up"><div class="flex flex-col md:flex-row justify-around items-center gap-6"><!-- 正计时显示 --><div class="text-center"><div id="loveDays" class="text-[clamp(2.5rem,8vw,5rem)] font-bold text-love-purple">0</div><div class="text-gray-500">恋爱天数</div></div><!-- 分隔符 --><div class="hidden md:block h-20 w-px bg-gray-300"></div><!-- 倒计时显示 --><div class="grid grid-cols-4 gap-2 md:gap-4 w-full md:w-auto"><div class="text-center"><div id="countdownDays" class="text-[clamp(1.5rem,5vw,2.5rem)] font-bold text-love-pink">00</div><div class="text-xs md:text-sm text-gray-500">天</div></div><div class="text-center"><div id="countdownHours" class="text-[clamp(1.5rem,5vw,2.5rem)] font-bold text-love-pink">00</div><div class="text-xs md:text-sm text-gray-500">时</div></div><div class="text-center"><div id="countdownMinutes" class="text-[clamp(1.5rem,5vw,2.5rem)] font-bold text-love-pink">00</div><div class="text-xs md:text-sm text-gray-500">分</div></div><div class="text-center"><div id="countdownSeconds" class="text-[clamp(1.5rem,5vw,2.5rem)] font-bold text-love-pink">00</div><div class="text-xs md:text-sm text-gray-500">秒</div></div></div></div><div class="mt-6 text-center text-gray-500 text-sm"><span id="nextAnniversary">距离下一个纪念日还有:</span><span id="anniversaryName" class="font-medium text-love-purple">恋爱100天</span></div></div><!-- 配置按钮 --><button id="configBtn" class="fixed bottom-6 right-6 bg-love-pink text-white rounded-full p-3 shadow-lg z-20 hover:bg-love-purple transition-colors"><i class="fa fa-cog text-xl"></i></button></div><!-- 配置面板 --><div id="configPanel" class="config-panel fixed top-0 right-0 w-full md:w-80 h-full bg-white shadow-2xl z-30 p-6 overflow-y-auto"><div class="flex justify-between items-center mb-6"><h2 class="text-xl font-bold text-love-pink">个性化设置</h2><button id="closeConfigBtn" class="text-gray-500 hover:text-gray-700"><i class="fa fa-times text-xl"></i></button></div><form id="configForm" class="space-y-6"><!-- 日期设置 --><div class="space-y-2"><label class="block text-sm font-medium text-gray-700">恋爱开始日期</label><input type="date" id="startDate" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-love-pink focus:border-love-pink"></div><!-- 纪念日设置 --><div class="space-y-2"><label class="block text-sm font-medium text-gray-700">下一个纪念日</label><div class="flex gap-2"><input type="date" id="anniversaryDate" class="flex-1 p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-love-pink focus:border-love-pink"></div><input type="text" id="anniversaryNameInput" placeholder="纪念日名称(如:恋爱100天)" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-love-pink focus:border-love-pink"></div><!-- 标题设置 --><div class="space-y-2"><label class="block text-sm font-medium text-gray-700">主标题</label><input type="text" id="titleInput" placeholder="输入主标题" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-love-pink focus:border-love-pink"></div><!-- 背景设置 --><div class="space-y-2"><label class="block text-sm font-medium text-gray-700">背景类型</label><div class="grid grid-cols-3 gap-2"><button type="button" data-bg-type="gradient" class="bg-gradient-to-br from-love-pink to-love-purple h-10 rounded-lg border-2 border-transparent focus:border-love-pink"></button><button type="button" data-bg-type="particle" class="bg-gray-100 h-10 rounded-lg border-2 border-transparent focus:border-love-pink"></button><label class="relative h-10 rounded-lg bg-gray-100 flex items-center justify-center cursor-pointer border-2 border-transparent focus-within:border-love-pink"><i class="fa fa-upload text-gray-400"></i><input type="file" id="bgImageUpload" accept="image/*" class="absolute inset-0 opacity-0 cursor-pointer"></label></div></div><!-- 主题设置 --><div class="space-y-2"><label class="block text-sm font-medium text-gray-700">主题颜色</label><div class="flex gap-2"><button type="button" data-theme="pink" class="w-full h-10 bg-love-pink rounded-lg border-2 border-transparent focus:border-black"></button><button type="button" data-theme="purple" class="w-full h-10 bg-love-purple rounded-lg border-2 border-transparent focus:border-black"></button><button type="button" data-theme="gold" class="w-full h-10 bg-love-gold rounded-lg border-2 border-transparent focus:border-black"></button></div></div><!-- 字体设置 --><div class="space-y-2"><label class="block text-sm font-medium text-gray-700">字体选择</label><select id="fontSelect" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-love-pink focus:border-love-pink"><option value="sans">无衬线体</option><option value="serif">衬线体</option><option value="handwriting">手写体</option></select></div><!-- 保存按钮 --><button type="submit" class="w-full bg-love-pink hover:bg-love-pink/90 text-white font-medium py-2 px-4 rounded-lg transition-colors">保存设置</button></form></div><!-- 移动端配置按钮 --><button id="mobileConfigBtn" class="fixed bottom-6 right-6 md:hidden bg-love-pink text-white rounded-full p-3 shadow-lg z-20"><i class="fa fa-cog text-xl"></i></button><script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script><script>// 全局变量let config = {startDate: '',anniversaryDate: '',anniversaryName: '恋爱100天',title: '我们的恋爱时光',subtitle: '记录每一刻心动',bgType: 'gradient',bgImage: '',theme: 'pink',font: 'sans'};let particleSystem = null;let countdownInterval = null;// DOM元素const elements = {loveDays: document.getElementById('loveDays'),countdownDays: document.getElementById('countdownDays'),countdownHours: document.getElementById('countdownHours'),countdownMinutes: document.getElementById('countdownMinutes'),countdownSeconds: document.getElementById('countdownSeconds'),mainTitle: document.getElementById('mainTitle'),subTitle: document.getElementById('subTitle'),anniversaryName: document.getElementById('anniversaryName'),startDate: document.getElementById('startDate'),anniversaryDate: document.getElementById('anniversaryDate'),anniversaryNameInput: document.getElementById('anniversaryNameInput'),titleInput: document.getElementById('titleInput'),fontSelect: document.getElementById('fontSelect'),configPanel: document.getElementById('configPanel'),configBtn: document.getElementById('configBtn'),closeConfigBtn: document.getElementById('closeConfigBtn'),mobileConfigBtn: document.getElementById('mobileConfigBtn'),configForm: document.getElementById('configForm'),bgImageUpload: document.getElementById('bgImageUpload'),particleCanvas: document.getElementById('particleCanvas')};// 初始化function init() {// 加载配置loadConfig();// 设置表单值updateFormValues();// 初始化粒子背景initParticleSystem();// 更新UIupdateUI();// 启动倒计时startCountdown();// 添加事件监听addEventListeners();}// 加载配置function loadConfig() {const savedConfig = localStorage.getItem('loveCounterConfig');if (savedConfig) {config = JSON.parse(savedConfig);} else {// 默认日期设置为今天const today = new Date();const defaultDate = new Date(today.setDate(today.getDate() - 1)).toISOString().split('T')[0];config.startDate = defaultDate;// 默认纪念日设置为30天后const anniversary = new Date(today.setDate(today.getDate() + 30)).toISOString().split('T')[0];config.anniversaryDate = anniversary;saveConfig();}}// 保存配置function saveConfig() {localStorage.setItem('loveCounterConfig', JSON.stringify(config));}// 更新表单值function updateFormValues() {elements.startDate.value = config.startDate;elements.anniversaryDate.value = config.anniversaryDate;elements.anniversaryNameInput.value = config.anniversaryName;elements.titleInput.value = config.title;elements.subTitle.textContent = config.subtitle;elements.fontSelect.value = config.font;// 设置选中的主题document.querySelector(`[data-theme="${config.theme}"]`).classList.add('border-black');// 设置选中的背景类型document.querySelector(`[data-bg-type="${config.bgType}"]`).classList.add('border-love-pink');}// 初始化粒子系统function initParticleSystem() {if (window.particlesJS && config.bgType === 'particle') {particlesJS('particleCanvas', {"particles": {"number": { "value": 80, "density": { "enable": true, "value_area": 800 } },"color": { "value": "#FF6B8B" },"shape": { "type": "circle" },"opacity": { "value": 0.5, "random": true },"size": { "value": 3, "random": true },"line_linked": {"enable": true,"distance": 150,"color": "#FF6B8B","opacity": 0.2,"width": 1},"move": {"enable": true,"speed": 1,"direction": "none","random": true,"straight": false,"out_mode": "out","bounce": false}},"interactivity": {"detect_on": "canvas","events": {"onhover": { "enable": true, "mode": "grab" },"onclick": { "enable": true, "mode": "push" },"resize": true},"modes": {"grab": { "distance": 140, "line_linked": { "opacity": 0.8 } },"push": { "particles_nb": 3 }}},"retina_detect": true});}}// 更新UIfunction updateUI() {// 更新标题elements.mainTitle.textContent = config.title;elements.subTitle.textContent = config.subtitle;elements.anniversaryName.textContent = config.anniversaryName;// 更新字体document.body.className = `font-${config.font}`;// 更新主题颜色document.documentElement.style.setProperty('--theme-color', config.theme === 'pink' ? '#FF6B8B' : config.theme === 'purple' ? '#8A2BE2' : '#FFD700');// 更新背景updateBackground();// 计算并更新恋爱天数updateLoveDays();// 计算并更新倒计时updateCountdown();}// 更新背景function updateBackground() {const body = document.body;// 清除之前的背景设置body.style.backgroundImage = '';body.className = body.className.replace(/bg-\S+/g, '');if (config.bgType === 'gradient') {body.classList.add('bg-gradient-to-br', config.theme === 'pink' ? 'from-love-pink/20 to-love-purple/20' :config.theme === 'purple' ? 'from-love-purple/20 to-indigo-500/20' :'from-love-gold/20 to-yellow-300/20');} else if (config.bgType === 'particle') {body.classList.add('bg-gray-100');if (!particleSystem) {initParticleSystem();}} else if (config.bgType === 'image' && config.bgImage) {body.style.backgroundImage = `url(${config.bgImage})`;body.classList.add('bg-cover', 'bg-center');}}// 更新恋爱天数function updateLoveDays() {const start = new Date(config.startDate);const now = new Date();const diffTime = Math.abs(now - start);const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));elements.loveDays.textContent = diffDays;}// 更新倒计时function updateCountdown() {const now = new Date();const anniversary = new Date(config.anniversaryDate);// 如果纪念日已过,设置为明年if (anniversary < now) {anniversary.setFullYear(anniversary.getFullYear() + 1);}const diffTime = anniversary - now;// 计算天、时、分、秒const days = Math.floor(diffTime / (1000 * 60 * 60 * 24));const hours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));const minutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60));const seconds = Math.floor((diffTime % (1000 * 60)) / 1000);// 更新DOMelements.countdownDays.textContent = days.toString().padStart(2, '0');elements.countdownHours.textContent = hours.toString().padStart(2, '0');elements.countdownMinutes.textContent = minutes.toString().padStart(2, '0');elements.countdownSeconds.textContent = seconds.toString().padStart(2, '0');}// 启动倒计时function startCountdown() {// 立即更新一次updateLoveDays();updateCountdown();// 清除之前的定时器if (countdownInterval) {clearInterval(countdownInterval);}// 设置定时器countdownInterval = setInterval(() => {updateLoveDays();updateCountdown();}, 1000);}// 添加事件监听function addEventListeners() {// 配置面板切换elements.configBtn.addEventListener('click', () => {elements.configPanel.classList.remove('hidden');elements.configPanel.classList.add('active');});elements.closeConfigBtn.addEventListener('click', () => {elements.configPanel.classList.add('hidden');elements.configPanel.classList.remove('active');});elements.mobileConfigBtn.addEventListener('click', () => {elements.configPanel.classList.toggle('active');});// 背景类型选择document.querySelectorAll('[data-bg-type]').forEach(btn => {btn.addEventListener('click', () => {// 移除所有选中状态document.querySelectorAll('[data-bg-type]').forEach(b => b.classList.remove('border-love-pink'));// 设置当前选中状态btn.classList.add('border-love-pink');config.bgType = btn.dataset.bgType;updateBackground();});});// 主题选择document.querySelectorAll('[data-theme]').forEach(btn => {btn.addEventListener('click', () => {// 移除所有选中状态document.querySelectorAll('[data-theme]').forEach(b => b.classList.remove('border-black'));// 设置当前选中状态btn.classList.add('border-black');config.theme = btn.dataset.theme;updateUI();});});// 图片上传elements.bgImageUpload.addEventListener('change', (e) => {const file = e.target.files[0];if (file) {const reader = new FileReader();reader.onload = (event) => {config.bgImage = event.target.result;config.bgType = 'image';// 更新背景类型选中状态document.querySelectorAll('[data-bg-type]').forEach(b => b.classList.remove('border-love-pink'));document.querySelector(`[data-bg-type="image"]`).classList.add('border-love-pink');updateBackground();};reader.readAsDataURL(file);}});// 表单提交elements.configForm.addEventListener('submit', (e) => {e.preventDefault();// 更新配置config.startDate = elements.startDate.value;config.anniversaryDate = elements.anniversaryDate.value;config.anniversaryName = elements.anniversaryNameInput.value;config.title = elements.titleInput.value;config.font = elements.fontSelect.value;// 保存配置saveConfig();// 更新UIupdateUI();// 关闭配置面板elements.configPanel.classList.add('hidden');elements.configPanel.classList.remove('active');// 显示保存成功提示alert('设置已保存!');});}// 页面加载完成后初始化window.addEventListener('DOMContentLoaded', init);// 窗口大小变化时调整粒子系统window.addEventListener('resize', () => {if (particleSystem) {particleSystem.resize();}});</script>
</body>
</html>