
最近在推进一个线下活动相关的信息整合项目时遇到一个比较现实的诉求很多用户想快速了解某一站演出的完整信息包括演出城市、场馆、时间、票务渠道以及周边安排但这类信息通常散落在各个社交平台和票务 App 中没有一个轻量级的整合展示方案。索性就基于一次具体的演出案例梳理了一套可以复用的“活动信息整理 前端展示 线上预约提醒”的实操流程。这篇文章不会去评价演出本身而是把“JonyJ 某站巡演信息”当作一个典型业务场景演示如何用纯前端 轻量后端方案搭建一个活动信息聚合页支持场次信息展示、收藏关注、开票倒计时和预约提醒。适合正在做活动展示页、个人项目练手、或者想快速搭建一个信息聚合站点的开发者阅读。对应项目信息我们整理成业务用例方便后续编码和测试项目级别说明活动名称JonyJ 本命巡演 石家庄站活动日期需要以票务平台最终公布为准活动城市石家庄演示需求信息聚合、倒计时、收藏、开票提醒技术方案HTML CSS JavaScript localStorage 消息提醒下面一步步拆解这个系统从设计到落地的全过程。1. 需求分析与功能拆分无论是做一场演出的活动聚合页还是做一个小型活动展示系统第一步都是把需求拆开。只有明确功能边界后续写代码时才能少走弯路。1.1 典型活动页面的核心需求以这次巡演信息页为例站在普通用户的角度最关心的信息包括这场演出在哪个城市、哪个场馆举行。演出时间什么时候距离现在还剩多少天。票价档位和购票渠道。是否已经开票如果没有开票希望开票时收到提醒。站在开发者角度还需要额外考虑信息存储在哪里。动态数据用服务端数据库还是先用本地存储模拟。页面如何适配 PC 端和移动端。倒计时怎么实现是纯展示还是需要落库。收藏和开票提醒是否需要后端支持。为了避免一开始就把系统设计得过于复杂我建议先做一个单机版 MVP也就是最小可用版本核心目标是把流程跑通。后续如果需要多人使用再引入后端服务和数据库。1.2 功能清单按照上面的分析我们把功能拆成四块功能模块具体说明优先级活动信息展示展示城市、场馆、时间、票价、票务渠道必须实现开票倒计时根据活动日期计算剩余时间必须实现收藏关注用户点击收藏保存到本地推荐实现开票提醒模拟开票通知提示用户关注增强功能这里有一点需要提前说明演出的实际开票时间和场馆信息具有较强时效性网页在本地展示用的是模拟数据上线前必须替换成票务平台正式发布的信息。这一点同时也是所有信息聚合类项目必须注意的合规点。1.3 技术选型思路这个项目不需要复杂的后端架构。我选择的是HTML5负责页面结构。CSS3负责布局和响应式适配。JavaScript实现倒计时、收藏、本地存储、模拟提醒。localStorage持久化用户的收藏状态和提醒状态。Vite 或纯静态文件本地开发可以直接用浏览器打开也可以使用任意静态服务器。如果以后想扩展成真实可用的系统可以加入 Node.js Express 或 Spring Boot 做服务端把活动数据放到 MySQL 中。但在本文范围内先把前端闭环做完即可。2. 环境准备与项目结构在开始写代码之前先把开发环境准备好。这个项目本身对运行环境要求很低。2.1 开发环境说明本文示例使用以下环境读者可以按自己的实际情况调整环境建议版本操作系统Windows 10/11、macOS 都可以Node.js可选用于启动静态服务浏览器Chrome / Edge 最新版IDEVS Code需要注意localStorage 和倒计时功能在现代浏览器中都支持得很好不需要额外安装依赖。如果你不想安装 Node.js也可以直接使用 VS Code 的 Live Server 插件运行页面。2.2 项目目录结构建议创建一个名为activity-page的项目目录结构如下activity-page/ ├── index.html ├── css/ │ └── style.css ├── js/ │ └── app.js └── README.md这里的目录结构非常简单。后续如果引入后端可以再增加server/目录和database/目录。3. 页面设计与核心实现完成需求拆分后接下来进入编码阶段。我会按照“页面结构 → 页面样式 → 核心逻辑”的顺序逐步实现。3.1 创建 HTML 页面骨架在项目根目录下创建index.html内容如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleJonyJ 本命巡演 石家庄站 · 活动信息聚合页/title link relstylesheet hrefcss/style.css / /head body section classpage-container !-- 活动头部信息 -- header classhero p classhero-tag巡演专场/p h1 classhero-titleJonyJ 本命巡演/h1 p classhero-city石家庄站/p p classhero-desc信息聚合演示页 · 数据仅供本地开发调试/p /header !-- 活动核心信息卡片 -- section classinfo-panel div classinfo-item span classinfo-label演出时间/span strong classinfo-value idshowTime以票务平台公布为准/strong /div div classinfo-item span classinfo-label演出城市/span strong classinfo-value石家庄/strong /div div classinfo-item span classinfo-label演出场馆/span strong classinfo-value待定请以官方发布为准/strong /div div classinfo-item span classinfo-label票务渠道/span strong classinfo-value票务平台官方渠道/strong /div /section !-- 倒计时区域 -- section classcountdown-panel h2开票倒计时/h2 p classcountdown-tip假设活动时间为 2025-08-12可根据实际信息修改/p div classcountdown-box div classcountdown-item span iddays0/span label天/label /div div classcountdown-item span idhours0/span label小时/label /div div classcountdown-item span idminutes0/span label分钟/label /div div classcountdown-item span idseconds0/span label秒/label /div /div /section !-- 收藏与提醒 -- section classaction-panel button classbtn btn-primary idfavoriteBtn收藏关注/button button classbtn btn-default idremindBtn开启开票提醒/button /section section classmessage-panel idmessagePanel/section /section script srcjs/app.js/script /body /html页面结构分为四个部分活动头部hero区域展示活动名称和城市。信息卡片info-panel集中展示时间、城市、场馆和票务渠道。倒计时区域countdown-panel动态计算剩余时间。操作按钮区域实现收藏和模拟提醒。有一点需要强调页面中展示的演出时间如果是“待定”状态前端就不能写死倒计时目标时间。我在示例中预留了一个模拟假设时间实际开发时需要从后端接口读取。3.2 实现页面响应式样式在项目css目录下创建style.css核心样式如下* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: PingFang SC, Microsoft YaHei, sans-serif; background: linear-gradient(135deg, #0f0c29, #1a1a2e, #16213e); color: #f0f0f0; min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .page-container { max-width: 720px; width: 100%; background: rgba(255, 255, 255, 0.05); border-radius: 20px; padding: 30px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); } .hero { text-align: center; margin-bottom: 24px; } .hero-tag { display: inline-block; padding: 6px 16px; border-radius: 20px; background: #e94560; font-size: 14px; margin-bottom: 16px; } .hero-title { font-size: 36px; font-weight: 600; margin-bottom: 6px; } .hero-city { font-size: 22px; color: #e94560; font-weight: 500; } .hero-desc { margin-top: 14px; color: rgba(255, 255, 255, 0.6); font-size: 13px; } .info-panel { background: rgba(255, 255, 255, 0.06); border-radius: 14px; padding: 18px 20px; margin-bottom: 24px; } .info-item { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .info-item:last-child { border-bottom: none; } .info-label { color: rgba(255, 255, 255, 0.7); font-size: 14px; } .info-value { font-size: 14px; text-align: right; max-width: 60%; color: #fff; } .countdown-panel { text-align: center; margin-bottom: 24px; } .countdown-panel h2 { font-size: 20px; margin-bottom: 8px; } .countdown-tip { font-size: 12px; color: rgba(255, 255, 255, 0.5); margin-bottom: 18px; } .countdown-box { display: flex; justify-content: center; gap: 16px; } .countdown-item { background: rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 16px 10px; width: 76px; } .countdown-item span { display: block; font-size: 30px; font-weight: 700; color: #e94560; } .countdown-item label { font-size: 13px; color: rgba(255, 255, 255, 0.7); } .action-panel { display: flex; justify-content: center; gap: 16px; margin-bottom: 20px; } .btn { padding: 12px 24px; border: none; border-radius: 30px; font-size: 15px; cursor: pointer; transition: all 0.3s ease; } .btn-primary { background: #e94560; color: #fff; } .btn-primary:hover { background: #c73652; } .btn-default { background: transparent; border: 1px solid rgba(255, 255, 255, 0.3); color: #fff; } .btn-default:hover { background: rgba(255, 255, 255, 0.1); } .btn-favorited { background: #ffca3a; color: #111; } .message-panel { min-height: 36px; text-align: center; font-size: 14px; } .message-panel .success { color: #8bc34a; } .message-panel .warning { color: #ffca3a; }这段 CSS 的核心设计思路是页面中心使用玻璃拟态风格卡片视觉上突出活动主题。使用flex布局实现倒计时和按钮的横向排列。移动端通过.page-container的width: 100%自动适配小屏。在真实项目中不建议把颜色值写死过多可以把主色抽成 CSS 变量方便后续替换成不同活动的品牌色。3.3 编写 JavaScript 核心逻辑现在进入最关键的部分。在js目录下创建app.js实现倒计时、收藏和模拟提醒功能。// 文件路径js/app.js // 1. activityConfig 集中管理活动信息 // 注意此处的活动时间仅为演示数据请以票务平台正式发布为准 const activityConfig { showDate: 2025-08-12T19:30:00, city: 石家庄, storageKey: jonyj_sjz_favorite, remindKey: jonyj_sjz_remind }; // 2. 获取页面元素 const daysEl document.getElementById(days); const hoursEl document.getElementById(hours); const minutesEl document.getElementById(minutes); const secondsEl document.getElementById(seconds); const favoriteBtn document.getElementById(favoriteBtn); const remindBtn document.getElementById(remindBtn); const messagePanel document.getElementById(messagePanel); // 3. 状态管理 let favorite localStorage.getItem(activityConfig.storageKey) 1; let remindEnabled localStorage.getItem(activityConfig.remindKey) 1; // 4. 更新收藏按钮状态 function updateFavoriteButton() { if (favorite) { favoriteBtn.textContent 已收藏; favoriteBtn.classList.add(btn-favorited); } else { favoriteBtn.textContent 收藏关注; favoriteBtn.classList.remove(btn-favorited); } } // 5. 初始化收藏状态 updateFavoriteButton(); // 6. 倒计时函数 function updateCountdown() { const target new Date(activityConfig.showDate).getTime(); const now Date.now(); const diff target - now; if (diff 0) { daysEl.textContent 0; hoursEl.textContent 0; minutesEl.textContent 0; secondsEl.textContent 0; showMessage(活动时间已到请以现场实际安排为准。, warning); return; } const days Math.floor(diff / (1000 * 60 * 60 * 24)); const hours Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds Math.floor((diff % (1000 * 60)) / 1000); daysEl.textContent days 10 ? 0 days : days; hoursEl.textContent hours 10 ? 0 hours : hours; minutesEl.textContent minutes 10 ? 0 minutes : minutes; secondsEl.textContent seconds 10 ? 0 seconds : seconds; } // 7. 消息提示函数 function showMessage(text, type) { messagePanel.innerHTML ; const div document.createElement(div); div.textContent text; div.className type success ? success : warning; messagePanel.appendChild(div); } // 8. 收藏点击事件 favoriteBtn.addEventListener(click, () { favorite !favorite; if (favorite) { localStorage.setItem(activityConfig.storageKey, 1); showMessage(关注成功后续开票或场次变动将通过消息提醒。, success); } else { localStorage.removeItem(activityConfig.storageKey); showMessage(已取消关注。, warning); } updateFavoriteButton(); }); // 9. 开票提醒点击事件 remindBtn.addEventListener(click, () { remindEnabled !remindEnabled; if (remindEnabled) { localStorage.setItem(activityConfig.remindKey, 1); remindBtn.textContent 已开启开票提醒; showMessage(模拟开票提醒已开启。真实项目可在此处调用服务端订阅接口。, success); } else { localStorage.removeItem(activityConfig.remindKey); remindBtn.textContent 开启开票提醒; showMessage(已关闭开票提醒。, warning); } }); // 10. 页面加载后立即执行一次并每秒刷新倒计时 updateCountdown(); setInterval(updateCountdown, 1000);这段代码的关键点包括activityConfig对象把活动时间、存储键名集中管理避免后续修改时到处找硬编码。收藏状态通过localStorage持久化因此刷新页面后状态不会丢失。通过setInterval每秒更新一次倒计时页面在未最小化时会自动刷新时间。倒计时中做了小于 10 秒补零处理让展示效果更统一。需要特别说明的是localStorage是浏览器本地存储方案只适合单机演示。如果同一用户换一台电脑登录收藏状态就会丢失。真实项目需要把用户 ID 与活动 ID 作为联合条件在后端数据库保存用户关注关系。3.4 运行页面完成以上三个文件后启动方式有两种。方式一直接双击打开index.html。方式二如果安装了 VS Code可以使用 Live Server 插件启动。更贴近企业开发的方式是在项目目录下启动一个本地静态服务cd activity-page npx serve .启动后在浏览器访问http://localhost:3000即可看到活动聚合页。此时页面上的倒计时会以当前时间动态计算收藏按钮点击后状态会保存到浏览器本地。4. 功能验证与预期结果写完代码后不能只停留在“页面能打开”这个层面还要对核心功能做验证。4.1 验证点设计验证项操作预期结果页面展示打开首页正常展示活动城市、时间、票务说明倒计时等待 1 秒秒数自动减少收藏关注点击收藏按钮按钮文字变为“已收藏”刷新页面后状态保持取消收藏再次点击按钮按钮文字恢复“收藏关注”开票提醒点击开启开票提醒提示开启成功按钮状态更新4.2 查看本地存储在浏览器中按F12打开开发者工具切换到Application面板在左侧选择Local Storage可以看到当前站点下面存储了两个键jonyj_sjz_favoritejonyj_sjz_remind键对应的值是1或不存在说明状态管理正常。这也是前端调试本地状态的通用方法。如果发现刷新页面后收藏状态丢失请检查是否在浏览器隐私模式下打开或者是否使用了无痕窗口。无痕模式下 localStorage 行为与常规模式有所不同。4.3 模拟开票提醒的局限当前版本的开票提醒只是把“用户是否开启订阅”保存在本地并没有真正向用户发送通知。要实现真实提醒需要结合服务端和消息推送平台常见方案包括方案适用场景复杂度邮件提醒用户留下邮箱服务端定时扫描开票状态中短信提醒用户注册手机号通过云服务商发送短信高小程序订阅消息微信小程序场景高App 推送有自建 App 的应用高服务端轮询后端每隔一段时间检查票务平台状态中对于本项目而言模拟按钮已经展示了“开启提醒”的交互闭环真实推送环节需要根据业务场景进一步选型。5. 常见问题与排查思路在编写和运行这类活动聚合页时最容易遇到下面几个问题。5.1 倒计时显示为 NaN问题现象页面中倒计时显示NaN / NaN / NaN / NaN。常见原因new Date(2025-08-12T19:30:00)在部分浏览器或系统时区下解析方式不一致导致时间格式无效或者活动时间字段来自接口格式带有毫秒之外的非法字符。解决思路在 JavaScript 控制台执行new Date(2025-08-12T19:30:00)检查是否返回有效日期对象。推荐使用new Date(2025, 7, 12, 19, 30, 0)这种数字传参方式月份从 0 开始因此 8 月传入7。如果时间来自接口建议统一使用时间戳字符串后端返回毫秒时间戳前端解析更稳定。5.2 localStorage 写入失败问题现象点击收藏按钮后控制台报错QuotaExceededError。常见原因浏览器本地存储空间已满或者页面在隐私模式下限制了存储能力。解决思路删除当前域名下无用的 localStorage 数据。使用try...catch包裹写入操作捕获存储失败异常。示例代码如下function saveToLocal(key, value) { try { localStorage.setItem(key, value); return true; } catch (e) { console.error(本地存储失败, e); return false; } }5.3 按钮状态刷新后丢失问题现象刷新页面后按钮重新变成未收藏状态。常见原因后端返回的数据覆盖了本地收藏状态或者在页面加载后没有读取 localStorage。排查步骤打开开发者工具的 Network 面板查看页面后续是否有请求返回活动数据。在updateFavoriteButton()方法处打日志确认初始化时读取到的favorite值。确认storageKey没有因为域名变化而更改。5.4 移动端样式错乱问题现象手机上打开页面倒计时布局换行。常见原因倒计时各个分块的最小宽度过大或者没有设置flex-wrap: wrap。解决思路把.countdown-box的flex-wrap设置为wrap同时让小屏下每个时间块宽度缩小。media (max-width: 420px) { .countdown-box { gap: 8px; } .countdown-item { width: 62px; padding: 12px 6px; } .action-panel { flex-direction: column; align-items: center; } }5.5 活动时间不确定怎么处理如果票务平台还没有公布具体的演出时间页面上直接开启倒计时是没有意义的。建议后端返回到一个状态位状态含义前端表现0未公布显示“待公布”不启动倒计时1已公布正常显示倒计时2已结束页面展示“活动已结束”这样代码就能适应演出筹办过程中的不同阶段不会出现虚假倒计时的问题。6. 工程化扩展与最佳实践如果这个活动聚合页要投入真实项目使用下面这些工程化建议值得关注。6.1 引入开源 UI 组件库本次项目使用纯 CSS 实现了样式展示实际项目可以引入成熟的组件库例如Vue 项目建议使用 Vant 或 Element Plus。React 项目建议使用 Ant Design Mobile。如果只做简单的活动页则使用 Tailwind CSS 可以快速布局。选择合适的组件库能减少重复造轮子但也要考虑包体积。活动页如果对首屏加载速度要求高推荐使用按需加载或直接使用少量原生样式。6.2 数据源设计真实的演出信息通常来自票务渠道比如大麦、秀动等平台。前端不能直接去爬取这些平台的数据更稳妥的是后端对接官方票务 API 或授权数据源。将活动信息缓存到自己的数据库中定时同步。前端通过 RESTful API 获取状态字段。这样设计的好处是页面只依赖自己的服务端接口不直接受到第三方页面结构变化的影响。下面是一个典型的活动信息表结构示例CREATE TABLE activity_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, activity_name VARCHAR(128) NOT NULL COMMENT 活动名称, city VARCHAR(64) NOT NULL COMMENT 城市, venue VARCHAR(128) COMMENT 场馆, show_time DATETIME COMMENT 演出时间, ticket_status TINYINT DEFAULT 0 COMMENT 0未公布 1已开票 2已结束, ticket_url VARCHAR(255) COMMENT 购票地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );前端原本通过activityConfig写死的信息可以改造为通过接口读取async function fetchActivityInfo() { const res await fetch(/api/activity/1001); const data await res.json(); // 更新页面DOM }注意真实项目打开数据库连接时需要遵循最小权限原则。给应用分配的账号只授予必要的SELECT、UPDATE权限不要使用root账号。6.3 用户关注功能的后端化如果收藏和提醒功能要在真实线上运行需要建一张用户活动关注表CREATE TABLE user_activity_favorite ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, activity_id BIGINT NOT NULL, remind_enabled TINYINT DEFAULT 0 COMMENT 0未提醒 1已开启提醒, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_activity (user_id, activity_id) );接口设计至少包含POST /api/activity/favorite收藏活动。DELETE /api/activity/favorite取消收藏。PUT /api/activity/remind开启或关闭提醒。这些接口在后端开发中属于常规的 CRUD 操作关键是权限校验。一定要先确认当前登录用户的身份再执行写入操作避免越权修改其他用户的关注状态。6.4 日志与监控活动聚合页在开票前往往会遇到流量高峰。建议前端接入性能监控与错误上报后端记录关键接口的 QPS 和响应时长并且对“倒计时不准”“票务跳转失败”这类业务异常做专项监控。日志埋点示例function logEvent(eventName, params) { if (window._tracker) { window._tracker.log(eventName, params); } else { console.log([event], eventName, params); } }在关键流程中调用埋点favoriteBtn.addEventListener(click, () { if (favorite) { logEvent(favorite_cancel, { activityId: 1001 }); } else { logEvent(favorite_add, { activityId: 1001 }); } });6.5 防抖与性能倒计时功能通常每秒执行一次updateCountdown。这个函数本身开销很小但如果有大量 DOM 操作或者事件监听器页面可能卡顿。可以注意两点每次更新只修改文本内容不动态重建 DOM。清理服务端数据时使用事件委托避免多个按钮各自监听造成内存泄漏。对于需要大量数据刷新的场景还可以使用requestAnimationFrame优化渲染帧率但倒计时用setInterval已经足够了。6.6 安全边界这里强调的是运营合规层面的边界。信息聚合页不能擅自使用未授权的艺人照片、海报和商标不能将非官方渠道的信息包装成官方发布也不能虚构票价和开票时间。开发者在搭建类似页面时应做到数据来源标注清楚放上票务平台官方链接。活动信息未定时页面明确提示“待定”。不提供任何代抢票、加价购票等违规服务。涉及用户手机号、微信号等个人信息时先获得用户授权并遵守相关法律法规。这是比代码实现更需要关注的部分。7. 总结与下一步学习建议经过本文的搭建我们已经完成了一个可直接运行的“JonyJ 本命巡演 石家庄站”活动信息聚合页。页面支持活动信息展示、开票倒计时、收藏关注和模拟开票提醒核心代码接近 300 行没有引入额外框架适合前端初学者快速理解。通过这个项目重点掌握了以下知识如何将模糊的需求拆解成具体功能列表。如何使用 HTML CSS 搭建响应式活动页面。如何通过setInterval实现倒计时逻辑。如何使用localStorage持久化用户状态。如何从纯前端演示过渡到后端接口与数据库设计。如果你想把项目进一步提升可以从以下方向继续学习学习 Vue 3 或 React将页面重构为组件化开发。学习 Node.js 或 Spring Boot编写真实的活动信息公开接口。学习定时任务调度模拟开票时自动推送提醒。学习 Docker将前端项目容器化部署到服务器。最后有一个实践建议不要满足于把博客里面的代码复制下来运行一次。试着改一改活动时间、换一个城市、增加一个收藏列表页面或者把静态的票务渠道改成可点击跳转的外链。当你把示例项目改造成自己需要的样子对这个技术栈的理解才会真正往前走一步。如果这篇文章对你有帮助可以先收藏备用。下一篇可以继续聊聊如何给这个页面增加后端订阅提醒功能或者如何部署上线分发有需要的话评论区交流。