news 2026/9/25 18:14:14

仿网易云年度听歌报告:纯前端源码包与滚动动画实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
仿网易云年度听歌报告:纯前端源码包与滚动动画实战

简介:这是一套可直接运行的网页版年度音乐报告前端模板,面向具备基础HTML/CSS/JS能力、希望快速搭建数据可视化报告页的开发者与设计爱好者,解决从零复刻网易云年度听歌报告交互与视觉风格的成本问题。资源包共25个文件,约825KB,以9个HTML页面构成首页入口与多页报告展示结构,6个JS脚本负责数据渲染、滑动翻页与埋点逻辑,3个CSS文件统一管理样式,另含PNG图片、部署说明文档及仓库同步脚本,整体采用静态文件架构,无需后端即可本地打开预览。已有20人学习关注。读者可获得完整的页面骨架与翻页交互实现思路,理解CSS/JS分离与哈希命名带来的加载优化方式,并借助二维码生成、资源监听等预置能力,将其二次开发为个人年度总结、企业用户行为报告或活动数据可视化页面,目录结构清晰,便于按模块替换数据与样式。

1. 仿网易云年度听歌报告:从一张截图到一个能跑的前端源码包

年底朋友圈被网易云年度听歌报告刷屏的时候,我盯着那张「你今年听了 1287 首歌,最常听的是凌晨 2 点」的页面看了很久。不是感慨,是职业病——这种页面用纯前端能不能复刻出来?答案是能,而且不需要后端、不需要接口、不需要任何账号体系,一个 HTML 文件加 CSS 和 JS 就能跑。这个源码包要解决的核心问题就是:把年度报告那种「一页一屏、滚动切换、数据驱动、动画入场」的交互体验,用最朴素的三件套实现出来,不依赖框架,不依赖构建工具,双击打开就能看效果。

适合谁?如果你是前端新手,想找一个比 TodoList 有成就感、比后台管理系统简单的练手项目,这个方向非常合适;如果你在准备前端面试,面试题里经常问的滚动监听、CSS 动画、数据渲染、事件节流,这个项目能一次性覆盖;如果你只是想给自己或朋友做一份「假年度报告」当礼物,改改数据就能用。整篇文章会从页面结构怎么拆、CSS 动画怎么做、JS 数据怎么驱动、滚动切换怎么实现,一路讲到实际写的时候容易翻车的地方。

2. 页面结构拆解:年度报告为什么适合用「全屏滚动 + 数据驱动」来做

2.1 先想清楚一屏放什么,再动手写 HTML

年度听歌报告这类页面有一个很明显的特征:它不是传统意义上的「网页」,而更像一个可以滑动的故事板。每一屏只讲一件事——今年听了多少歌、最常听的歌手是谁、听得最多的那一天、你的听歌时间段分布。这种「一屏一信息」的结构决定了它天然适合全屏滚动布局,而不是常规的文档流。

我一般会先把每一屏的内容列出来,再决定 HTML 的骨架。常见的做法是用一个外层容器包住所有 section,每个 section 占满视口高度,通过滚动或切换来控制当前显示哪一屏。下面是一个最小可用的 HTML 结构,你可以直接抄:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>我的年度听歌报告</title> <link rel="stylesheet" href="style.css"> </head> <body> <!-- 整个报告的外层容器,负责滚动或切换 --> <div class="report-wrapper" id="reportWrapper"> <!-- 第一屏:年度总览 --> <section class="report-page">/* 全局重置,去掉默认边距 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: #1a1a2e; color: #fff; overflow: hidden; /* 外层不滚动,由 wrapper 控制 */ } /* 滚动容器:开启纵向滚动,每一屏吸附 */ .report-wrapper { height: 100vh; height: 100dvh; /* 优先用动态视口高度 */ overflow-y: scroll; scroll-snap-type: y mandatory; /* 滚动吸附,一屏一停 */ scroll-behavior: smooth; } /* 每一屏:占满视口,内容居中 */ .report-page { height: 100vh; height: 100dvh; scroll-snap-align: start; /* 吸附到顶部 */ display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; } /* 内容区:限制最大宽度,防止大屏上文字拉太长 */ .page-content { text-align: center; padding: 0 32px; max-width: 480px; width: 100%; } /* 数字样式:大号、加粗、渐变文字 */ .page-number { font-size: 72px; font-weight: 700; background: linear-gradient(135deg, #ff6b6b, #ffa502); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1.2; } .page-label { font-size: 16px; color: rgba(255, 255, 255, 0.6); margin-bottom: 12px; letter-spacing: 2px; } .page-unit { font-size: 18px; color: rgba(255, 255, 255, 0.8); margin-top: 8px; } .page-artist, .page-time, .page-keyword { font-size: 40px; font-weight: 700; margin: 16px 0; } .page-desc { font-size: 15px; color: rgba(255, 255, 255, 0.5); margin-top: 12px; } /* 进度条:固定在右侧 */ .progress-bar { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 12px; z-index: 10; } .progress-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255, 255, 255, 0.3); cursor: pointer; transition: all 0.3s ease; } .progress-dot.active { background: #ff6b6b; transform: scale(1.5); }

这里最核心的两个属性是scroll-snap-type: y mandatory和scroll-snap-align: start。前者告诉浏览器「这个容器要按纵向吸附滚动」,后者告诉浏览器「每个 section 滚动停止时对齐到容器顶部」。这两个属性配合起来,就实现了类似网易云年度报告那种「滑一下翻一页」的效果,而且不需要写任何 JS 滚动逻辑。scroll-behavior: smooth让滚动过程有过渡动画,不会显得生硬。

提示:scroll-snap在 iOS Safari 和 Chrome 上支持都很好,但如果你需要兼容非常老的浏览器,就得用 JS 手动计算滚动位置来模拟,代码量会大不少。做练手项目的话,直接用scroll-snap就够了。

3. JS 数据驱动:数字滚动、入场动画和滚动监听怎么串起来

3.1 数字从 0 涨到 1287:用 requestAnimationFrame 做递增动画

年度报告里最有仪式感的一个效果,就是那个大数字从 0 快速涨到最终值。这个效果用requestAnimationFrame来做最自然,因为它能跟着浏览器的刷新节奏走,不会掉帧。核心思路是:记录动画开始时间,每一帧计算已经过去了多少时间,根据进度算出当前应该显示的数字,直到进度达到 1 就停止。

/** * 数字递增动画 * @param {HTMLElement} el - 要显示数字的元素 * @param {number} target - 目标数字 * @param {number} duration - 动画时长(毫秒) */ function animateNumber(el, target, duration = 1500) { const startTime = performance.now(); function update(currentTime) { const elapsed = currentTime - startTime; // 计算进度,最大为 1 const progress = Math.min(elapsed / duration, 1); // 用 easeOutCubic 缓动函数让数字先快后慢 const eased = 1 - Math.pow(1 - progress, 3); const current = Math.floor(eased * target); el.textContent = current.toLocaleString(); // 加千分位 if (progress < 1) { requestAnimationFrame(update); } else { el.textContent = target.toLocaleString(); // 确保最终值精确 } } requestAnimationFrame(update); }

这段代码里几个参数需要解释。duration控制动画总时长,我一般设 1500 毫秒左右,太快没有仪式感,太慢用户会等得不耐烦。缓动函数easeOutCubic的公式是1 - (1 - progress)^3,效果是数字一开始涨得很快,接近目标值时变慢,视觉上更舒服。toLocaleString()会自动给数字加千分位逗号,1287 会显示成 1,287,看起来更正式。

注意:performance.now()比Date.now()精度更高,而且不受系统时间调整影响,做动画计时优先用它。

3.2 入场动画:用 IntersectionObserver 判断当前屏是否可见

数字动画什么时候触发?总不能页面一加载就全部跑完,那样用户滑到第二屏的时候动画已经结束了。正确的做法是:当某一屏进入视口时,才触发这一屏的动画。IntersectionObserver就是干这个的,它比监听 scroll 事件再手动计算位置要高效得多,而且代码更简洁。

// 配置:当元素有 50% 进入视口时触发 const observerOptions = { root: document.getElementById('reportWrapper'), threshold: 0.5 }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const page = entry.target; const index = page.dataset.index; // 根据当前是第几屏,触发不同的动画 if (index === '0') { const numberEl = page.querySelector('.page-number'); const target = parseInt(numberEl.dataset.count, 10); animateNumber(numberEl, target); } // 给当前屏加一个 active 类,CSS 里可以写入场动画 page.classList.add('page-active'); // 更新进度条 updateProgress(index); } }); }, observerOptions); // 观察每一屏 document.querySelectorAll('.report-page').forEach(page => { observer.observe(page); }); /** * 更新右侧进度条高亮 * @param {string} index - 当前屏的索引 */ function updateProgress(index) { document.querySelectorAll('.progress-dot').forEach(dot => { dot.classList.toggle('active', dot.dataset.target === index); }); }

threshold: 0.5表示当目标元素有 50% 的面积进入视口时才触发回调。这个值可以根据需要调整,设成 0.3 会更早触发,设成 0.8 会更晚。root指定为滚动容器reportWrapper,因为滚动发生在它身上,而不是整个页面。如果不指定 root,默认是视口,在这个项目里也能用,但指定了更精确。

配合这段 JS,CSS 里可以加入场动画。比如让内容从下方淡入:

.page-content { opacity: 0; transform: translateY(30px); transition: opacity 0.8s ease, transform 0.8s ease; } .page-active .page-content { opacity: 1; transform: translateY(0); }

这样当某一屏被IntersectionObserver检测到可见时,JS 给它加上page-active类,CSS 过渡就会自动播放。数字动画和内容淡入可以同时进行,视觉层次更丰富。

3.3 进度条点击跳转:scrollIntoView 的用法和参数

右侧的进度点不只是装饰,用户点击某个点应该能直接跳到对应屏。实现方式很简单,用scrollIntoView就行:

document.querySelectorAll('.progress-dot').forEach(dot => { dot.addEventListener('click', () => { const targetIndex = dot.dataset.target; const targetPage = document.querySelector( `.report-page[data-index="${targetIndex}"]` ); if (targetPage) { targetPage.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); });

behavior: 'smooth'让跳转有平滑滚动效果,block: 'start'表示对齐到容器顶部。这两个参数配合scroll-snap使用,跳转后会自动吸附到位,不会出现「跳过去又弹回来」的情况。

提示:如果点击进度点后滚动位置不对,检查一下scroll-snap-type是不是设在了正确的容器上。scrollIntoView是让元素滚动到可视区域,而scroll-snap是容器级别的行为,两者作用对象不同,但配合起来没问题。

4. 避坑与排查:仿网易云年度报告最容易翻车的 5 个地方

4.1 移动端 100vh 高度跳动,页面出现「半截屏」

现象:在手机浏览器上打开,地址栏显示的时候页面正常,地址栏隐藏后底部露出一截空白,或者内容被裁掉。

原因:100vh在移动端浏览器里指的是「最大视口高度」,不随地址栏的显示隐藏而变化。当地址栏隐藏时,实际可视区域变大了,但100vh还是按原来的算,导致内容对不齐。

解决:优先使用100dvh(动态视口高度),它会根据当前可视区域动态变化。写法是height: 100vh; height: 100dvh;,不支持dvh的浏览器会回退到vh。如果还需要更精确的控制,可以在 JS 里监听resize事件,用window.innerHeight动态设置高度。

4.2 数字动画重复触发,滑回去再滑过来数字又从头涨

现象:第一屏的数字已经涨到 1287 了,用户滑到第二屏再滑回第一屏,数字又从 0 开始涨。

原因:IntersectionObserver在元素每次进入视口时都会触发回调,没有做「只触发一次」的判断。

解决:在元素上加一个标记,比如dataset.animated,触发过就跳过。或者用observer.unobserve(page)在触发后停止观察该元素。我一般用前者,因为代码更直观:

if (entry.isIntersecting && !page.dataset.animated) { page.dataset.animated = 'true'; // ... 触发动画 }

4.3 scroll-snap 和 scroll-behavior 冲突,滚动卡顿

现象:设置了scroll-snap-type之后,手动滚动变得很「粘」,有时候滑不动,有时候一下子跳好几屏。

原因:scroll-snap-type: y mandatory要求每次滚动都必须停在一个吸附点上,如果内容高度和视口高度不完全一致,或者有 margin 干扰,就会导致吸附行为异常。另外scroll-behavior: smooth和scroll-snap同时使用时,某些浏览器版本会有兼容问题。

解决:确保每个 section 的高度严格等于容器高度,不要加 margin 或 padding 影响高度计算。如果还是卡顿,把mandatory改成proximity,它只在靠近吸附点时才吸附,滚动更自由。scroll-behavior: smooth可以保留,但如果你发现点击进度点跳转时动画不流畅,可以暂时去掉它,用 JS 的scrollTo加behavior: 'smooth'替代。

4.4 渐变文字在部分安卓机上不显示

现象:.page-number用了background-clip: text做渐变文字,在 Chrome 桌面正常,但在某些安卓机自带浏览器上文字直接消失。

原因:-webkit-background-clip: text需要配合-webkit-text-fill-color: transparent使用,但部分安卓浏览器对这两个属性的支持不完整,导致文字被「裁掉」了。

解决:加一个降级方案,用@supports检测:

.page-number { color: #ff6b6b; /* 降级颜色 */ } @supports (-webkit-background-clip: text) { .page-number { background: linear-gradient(135deg, #ff6b6b, #ffa502); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } }

这样不支持渐变文字的浏览器会显示纯色,至少不会看不见。

4.5 滚动监听用 scroll 事件导致性能问题

现象:一开始用window.addEventListener('scroll', ...)来判断当前在哪一屏,页面滑动时明显掉帧,手机上尤其严重。

原因:scroll 事件触发频率极高,每一帧可能触发多次,如果在回调里做 DOM 查询或复杂计算,主线程会被阻塞。

解决:换成IntersectionObserver,它由浏览器底层调度,不占用主线程。如果非要用 scroll 事件,至少加一个节流函数,比如用requestAnimationFrame包一层,确保每帧最多执行一次。这个项目里IntersectionObserver完全够用,没必要用 scroll 事件。

5. 进阶技巧:把静态数据换成可配置的 JSON,以及一个验证动画是否流畅的笨办法

5.1 数据抽离:让不懂代码的人也能改报告内容

现在数据是写死在 HTML 里的,改起来要翻源码。更好的做法是把数据抽成一个 JSON 对象,JS 读取后动态渲染。这样即使不懂 HTML 的人,只要会改 JSON 里的文字和数字,就能生成自己的报告。

// data.js - 报告数据配置 const reportData = { totalSongs: 1287, topArtist: '陈奕迅', artistCount: 326, topTimeRange: '凌晨 2:00 - 3:00', timeCount: 89, keyword: '单曲循环', keywordDesc: '有一首歌你听了 47 遍' }; // main.js - 渲染逻辑 function renderPage(page, data) { const index = page.dataset.index; if (index === '0') { page.querySelector('.page-number').dataset.count = data.totalSongs; } else if (index === '1') { page.querySelector('.page-artist').textContent = data.topArtist; page.querySelector('.page-desc').textContent = `你听了 TA 的 ${data.artistCount} 首歌`; } else if (index === '2') { page.querySelector('.page-time').textContent = data.topTimeRange; page.querySelector('.page-desc').textContent = `这个时间段你听了 ${data.timeCount} 次`; } else if (index === '3') { page.querySelector('.page-keyword').textContent = data.keyword; page.querySelector('.page-desc').textContent = data.keywordDesc; } } // 初始化时渲染所有页面 document.querySelectorAll('.report-page').forEach(page => { renderPage(page, reportData); });

这样改数据只需要动data.js一个文件,HTML 结构完全不用碰。如果你想把数据放到单独的.json文件里用fetch加载,也可以,但那样就必须起一个本地服务器,不能直接双击 HTML 打开了。做练手项目的话,用 JS 对象更省事。

5.2 验证动画流畅度:用 Chrome DevTools 的 Performance 面板录一段

动画写完怎么知道流不流畅?别靠肉眼猜,用工具测。Chrome DevTools 的 Performance 面板可以录制一段时间的页面运行情况,然后看 FPS 曲线。具体操作:打开 DevTools,切到 Performance 面板,点录制按钮,然后在页面上滑动几屏,停止录制。你会看到一条 FPS 曲线,绿色表示流畅(接近 60fps),红色表示掉帧。

重点看两个东西:一是 FPS 曲线有没有大幅波动,二是 Main 线程有没有长任务(超过 50ms 的任务会标红)。如果数字动画期间 FPS 掉到 30 以下,说明requestAnimationFrame里的计算太重了,可以简化缓动函数或者减少每帧的 DOM 操作。我一般会把textContent的更新放在requestAnimationFrame里,但确保不做其他多余操作。

另一个笨办法但很有效:在动画运行时打开 DevTools 的 Rendering 面板,勾选「Paint flashing」,如果数字区域频繁闪烁绿色,说明重绘太频繁,可以考虑用transform或opacity做动画,而不是改textContent。不过数字滚动必须改文本内容,所以这个场景下重绘不可避免,只要 FPS 达标就行。

提示:Performance 面板录制时尽量模拟真实使用场景,比如在手机上滑动,而不是在桌面用鼠标滚轮。桌面滚轮和触摸滑动的滚动行为差异很大,测试结果也会不同。

5.3 一个我踩过的坑:scroll-snap 在 Safari 上需要加前缀

最后说一个血泪经验。scroll-snap-type和scroll-snap-align在较老版本的 Safari 上需要加-webkit-前缀才能生效。如果你发现代码在 Chrome 上正常,在 Safari 上滚动不吸附,先检查前缀:

.report-wrapper { -webkit-overflow-scrolling: touch; /* iOS 惯性滚动 */ scroll-snap-type: y mandatory; } .report-page { scroll-snap-align: start; }

-webkit-overflow-scrolling: touch是 iOS 上开启惯性滚动的属性,加上之后滑动更跟手。虽然现在新版 Safari 已经支持无前缀的scroll-snap,但加上前缀不亏,兼容性更好。

我现在的习惯是:任何涉及滚动、动画、视口高度的 CSS 属性,先在 Can I use 上查一眼兼容性,然后在真机上跑一遍。桌面浏览器和手机浏览器的差异,比你想象的大得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 18:08:40

Stable Diffusion图像生成实战指南:从提示工程到商业落地

1. 这份报告不是“看热闹”的PPT&#xff0c;而是图像生成赛道的实操地图你点开这份《AIGC产业研究报告 2023——图像生成篇》&#xff0c;别急着划到结论页。我连续三年蹲在AIGC工具链一线&#xff0c;从Stable Diffusion 0.15版本开始调参&#xff0c;给电商公司搭过千图/天的…

作者头像 李华
网站建设 2026/9/25 18:04:39

基于Python的搜索引擎设计与实现:从爬虫到倒排索引的完整实战

做毕设的时候&#xff0c;我选了“基于Python的搜索引擎设计与实现”这个题目。说实话&#xff0c;刚开始心里挺没底的&#xff0c;因为搜索引擎这东西听起来就像是个巨头才能搞的项目&#xff0c;百度谷歌那是多大的工程。但真正把一个能用的搜索引擎从零写出来之后&#xff0…

作者头像 李华
网站建设 2026/9/25 18:04:14

纯前端复刻网易云年度听歌报告:HTML/CSS/JS 滚动叙事实战

简介&#xff1a;这是一套可直接运行的网页版年度音乐报告前端模板&#xff0c;面向具备基础HTML/CSS/JS能力、希望快速搭建数据可视化报告页的开发者与设计爱好者&#xff0c;解决从零实现翻页交互与动态渲染成本高的问题。资源包共25个文件&#xff0c;约825KB&#xff0c;以…

作者头像 李华
网站建设 2026/9/25 18:02:14

高温热管工质选择:钠钾锂的工作温度窗口与兼容性

高温热管工质按工作温度选&#xff1a;钾热管约400-700℃&#xff0c;钠热管约600-900℃&#xff0c;锂热管可达1200℃以上。除温度窗口外&#xff0c;还要看工质与管材的兼容性&#xff08;腐蚀&#xff09;和启动特性。选型原则是"工质匹配工况、管材匹配工质"。高…

作者头像 李华
网站建设 2026/9/25 18:01:36

8GB 显存跑通实时视频生成:LTX-Video 部署

8GB 显存跑通实时视频生成&#xff1a;LTX-Video 部署 【免费下载链接】LTX-Video Official repository for LTX-Video 项目地址: https://gitcode.com/GitHub_Trending/ltx/LTX-Video LTX-Video 是 DiT 架构的实时视频生成模型&#xff1a;因果视频自编码器把视频压进潜…

作者头像 李华