> 技术文档 > 恋爱时间倒计时网页设计与实现方案

恋爱时间倒计时网页设计与实现方案

恋爱时间倒计时网页设计与实现方案

一、项目概述

本项目旨在创建一个高度可定制的恋爱时间倒计时网页,支持纪念日日期设置、背景主题切换、个性化文案定制等功能,并通过localStorage保存用户配置。技术栈将采用HTML5、Tailwind CSS v3和原生JavaScript,结合Canvas粒子动画实现视觉吸引力。

二、核心功能设计
  1. 双模式计时系统

    • 正计时:记录恋爱天数(支持精确到秒级更新)
    • 倒计时:重要纪念日提醒(如100天、周年纪念)
  2. 个性化定制面板

    • 日期选择器:支持公历/农历切换
    • 背景设置:纯色渐变、粒子动画、自定义图片上传
    • 主题系统:预设3套浪漫主题(粉紫渐变/星空蓝/蜜桃粉)
    • 文案定制:主标题、副标题自定义
    • 字体选择:3种字体风格(手写体/衬线体/无衬线体)
  3. 视觉动效设计

    • 爱心粒子背景:鼠标交互时粒子聚合为爱心形状
    • 时间数字动画:数字变化时的平滑过渡效果
    • 纪念日里程碑:特殊天数(如520天)的烟花特效
三、技术实现方案
  1. 倒计时核心逻辑
// 高精度计时实现(避免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();}
  1. 粒子背景实现(基于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()); }}
  1. 用户配置持久化
// 配置数据结构const DEFAULT_CONFIG = { anniversaryDate: \'2023-01-01\', title: \'我们的恋爱时光\', subtitle: \'记录每一刻心动\', theme: \'pink-love\', backgroundType: \'particle\', customBackground: \'\', font: \'handwriting\'};// 保存配置到localStoragefunction 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设计规范
  1. 色彩系统

    • 主色调:#FF6B8B(浪漫粉)
    • 辅助色:#8A2BE2(梦幻紫)、#FFD700(香槟金)
    • 中性色:#F9F9F9(背景)、#333333(文字)
  2. 响应式布局

    • 移动端:单列布局,配置项折叠为底部抽屉
    • 平板:双列布局,左侧配置+右侧预览
    • 桌面端:三栏布局,增加快捷操作区
  3. 交互反馈

    • 配置变更时实时预览
    • 操作成功的微动画提示
    • 错误状态的友好提示
五、项目结构
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 # 项目文档
六、优化与兼容性
  1. 性能优化

    • Canvas动画节流(requestAnimationFrame)
    • 图片懒加载与压缩
    • 配置项变更防抖处理
  2. 浏览器兼容

    • 支持Chrome/Edge/Safari最新版
    • 降级处理IE浏览器(简化动画效果)
  3. 可访问性

    • 语义化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(); // 更新UI updateUI(); // 启动倒计时 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 }); } } // 更新UI function 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); // 更新DOM elements.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(); // 更新UI updateUI(); // 关闭配置面板 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>