HTML5二十四节气网站实战:从语义化结构到SVG动画与zip打包
简介这是一套基于HTML5、CSS与JavaScript构建的二十四节气主题网站模板定位为前端学习者的大作业或毕业设计参考也可用于传统文化科普站点。模板采用单页式结构包含首页、节气概述、分类、时间轴、图鉴、文化、在线留言等模块并集成了页面加载动画、粒子背景、轮播图、3D悬停卡片、视频播放、快速导航、标签页切换、回到顶部等交互效果能直观展示节气文化与前端常见动效。压缩包共44个文件以39张jpg图片素材为主辅以2个js、1个css、1个html和1个mp4视频包体约24.44MB样式与脚本独立存放页面适配手机端附有部分中文注释适合初学者边看边改、逐步理解页面结构与动效实现。目前已有111人学习对于需要完成课程作业或毕业设计的学生而言是一份可以直接套版、快速出效果的实用参考。1. 一份HTML5二十四节气网站的zip装的不只是网页拿到一个“HTML5二十四节气网站.zip”第一反应往往是解压、双击index.html然后发现浏览器里要么空白、要么样式错乱。问题多半不在代码本身而在于这个项目的运行前提它是一套不依赖构建工具的纯静态站点HTML、CSS、JavaScript、SVG资源全部本地化任何一处用了绝对路径或跨域请求直接双击打开就会失效。二十四节气本身是一条线性历法适合用信息展示型页面呈现而HTML5恰好提供了语义化结构、原生交互API和SVG动画三样地基。这篇文章要拆的就是把这类节气主题站点从数据层做到视觉层、交互层最后封进一个能正常解压运行的zip的完整路径。适合想独立完成一版前端作品、或者要交付一个不需要安装依赖的静态站的人。2. 先用语义化HTML5把二十四节气的信息骨架立住很多人在做节气网站时第一步就是写24个div然后再回头补样式。这样做的直接后果是后续要加详情、做季节过滤、按节气名搜索全都得改HTML。更稳妥的做法是先想清楚页面分成哪几个区域再把节气内容从HTML中剥离到JavaScript数据结构里。zip包里跑的是一套“数据 渲染脚本”而不是一张把24个节气写死的静态页。2.1 首页分区header、aside、figure 在节气页面里的实际用途一个常规节气展示页面的信息层级大致是全局头部站名、当前节气、主体区24节气卡片网格、侧边栏季节导航、页脚数据来源说明。HTML5语义标签在这里不是装饰而是让页面结构可读、可维护的基础。!doctype html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title二十四节气 · HTML5/title link relstylesheet hrefassets/css/style.css /head body header classsite-header h1二十四节气/h1 p classcurrent-term idcurrentTerm今日节气计算中…/p /header main idtermGrid classterm-grid aria-label二十四节气列表 !-- 卡片由JS生成 -- /main aside classseason-sidebar aria-label季节导航 button typebutton>// assets/js/terms-data.js window.SOLAR_TERMS [ { id: 1, name: 立春, season: spring, date: 2025-02-03, time: 22:10:13, description: 春气始而建立万物随阳气上升而萌芽。, customs: [咬春, 打春牛], foods: [春饼, 萝卜, 五辛盘], poem: 东风带雨逐西风大地阳和暖气生。, icon: licun } // 其余23个节气按相同结构补齐 ];逻辑说明挂到window.SOLAR_TERMS而不是用const声明是为了让main.js在另一个脚本文件里直接读取若改用ES Module的import在file://协议下同样会被CORS拦下这是zip场景与传统前端工程最大的差异点。date和time分开存既支持按日期判断“今天属于哪个节气”又保留了精确到时刻的扩展能力。season字段把24个节气映射到四季切换主题时只需filter(term term.season current)。icon对应SVG雪碧图中的symbol id渲染时用use href#licun引入。参数说明日期统一用YYYY-MM-DD字符串字典序即时间序判断节气区间时可以直接用字符串比较如果混入2025/02/03这类格式比较结果会错乱。id从1到24保持稳定后续做URL定位#term-5和localStorage存储都依赖它。下面的字段规划表是写数据文件前建议先列好的字段类型用途注意事项idnumber唯一标识、URL定位固定1-24不建议用数组下标代替namestring节气名称保持标准写法立春、雨水…seasonstring四季主题映射只取 spring/summer/autumn/winterdatestring公历日期YYYY-MM-DD不要带星期timestring交节时刻格式HH:mm:ss仅展示用iconstringSVG symbol引用与sprite中id严格一致2.2.1 当前节气判断区间比较而非日期等值比较用户打开页面时页面顶部要显示“今日节气大暑”。这个判断不能写成“当前日期等于数据里的日期”因为节气之间隔了约15天立春日期是2月3日那么2月4日到2月18日之间仍然属于立春区间。正确做法是找到“当前日期所在的那一段节气区间”function getCurrentTermIndex(now, terms) { const nowDate new Date(now); const nowKey dateToKey(nowDate); // 输出 2025-07-20 for (let i 0; i terms.length; i) { const startKey terms[i].date; const endKey i terms.length - 1 ? terms[i 1].date : ${nowDate.getFullYear()}-12-31; if (nowKey startKey nowKey endKey) { return i; } } return 0; // 兜底数据缺失时默认立春 } function dateToKey(d) { const m String(d.getMonth() 1).padStart(2, 0); const day String(d.getDate()).padStart(2, 0); return ${d.getFullYear()}-${m}-${day}; }参数说明判断区间用的是右开[start, end)即当前日期等于某个节气的date时算作进入该节气而等于下一个节气的date时进入下一个。这个边界如果不处理立春和雨水交界的当天会同时命中两条。endKey在最后一个节气时落入12-31哨兵值跨年时记得更新数据表否则冬至之后到12月31日之间会找不到所属节气。骨架层到这里已经具备页面有语义分区数据表可驱动渲染。接下来进入视觉层让节气主题从“一堆文字卡片”变成“有季节氛围的页面”。3. 视觉层用CSS3与SVG把节气氛围做成可切换主题节气网站最忌做成信息瀑布流24张卡片排得整整齐齐每张上面一张图下面一段字整体毫无记忆点。要让页面有“季节感”核心方案是设计一套可切换的四季主题变量再用SVG动画让节气图标活起来。整个过程不引入任何框架浏览器原生就够。3.1 CSS变量与四季主题改一处属性全站换肤在:root里定义春季节气的基准色其余季节通过body[data-season]覆盖变量值。这样切换季节时JavaScript只需要改body上的>:root { --bg: #f7f3e8; --card-bg: #fffdf7; --text: #3b342c; --accent: #4a7c59; --card-radius: 16px; } body[data-seasonsummer] { --bg: #e8f2e6; --accent: #2f6b4f; } body[data-seasonautumn] { --bg: #f8efe3; --accent: #a4583c; } body[data-seasonwinter] { --bg: #eef1f5; --accent: #3e5a7a; } body { background: var(--bg); color: var(--text); transition: background-color .4s ease, color .4s ease; }逻辑说明CSS变量在这里的价值不是炫技而是省掉“24张卡片 3种季节”的排列组合选择器。卡片文字、边框、图标描边统一引用var(--accent)换季节只改一个变量。用CSS变量而非Sass的原因很实际zip包要求解压即用不能有“安装依赖 → 编译 → 输出”的中间链路。transition只写在background-color和color上避免其他属性参与变换导致的重绘开销。参数说明--card-radius这类非颜色变量也可以放入主题区统一管理圆角与间距。若想让“当前节气卡片”比普通卡片更醒目可直接在.term-card.is-current上覆盖--accent视觉上会与同季节其他卡片形成层次差异。3.2 内联SVG图标symbol雪碧图与CSS动画配合24个节气图标如果全部用PNG包体膨胀且风格难统一。更合理的方案是做一个SVG雪碧图里面放24个symbol页面中通过use引用。这样做的好处是图标整体只有一份文件CSS能直接选中symbol内的path做动画。svg xmlnshttp://www.w3.org/2000/svg styledisplay:none symbol idrain viewBox0 0 48 48 path idrain-cloud dM14 34a7 7 0 0 1 0-14 9 9 0 0 1 17-4 8 8 0 0 1 3 15H14z fill#8aa6b3 / g idrain-drops stroke#4A90D9 stroke-width2 stroke-linecapround fillnone line x118 y136 x216 y242 / line x125 y136 x223 y242 / line x132 y136 x230 y242 / /g /symbol /svg渲染节气卡时图标节点只需要一行svg classterm-icon>.term-card:hover .term-icon[data-iconrain] { animation: rainfall 1.2s ease-in-out infinite; } keyframes rainfall { 0% { transform: translateY(0); opacity: .9; } 50% { transform: translateY(4px); opacity: .6; } 100% { transform: translateY(0); opacity: .9; } }逻辑说明整张雪碧图用display:none隐藏但use引用到的内容仍会正常渲染这是SVG文档内嵌引用的标准写法。>const canvas document.getElementById(fx); const reduceMotion window.matchMedia((prefers-reduced-motion: reduce)).matches; if (canvas !reduceMotion) { const ctx canvas.getContext(2d); const particles []; const COUNT window.innerWidth 768 ? 24 : 48; for (let i 0; i COUNT; i) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * 2 1, vy: Math.random() * 0.6 0.2, vx: (Math.random() - 0.5) * 0.2 }); } function tick() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle rgba(255,255,255,0.75); particles.forEach(p { p.y p.vy; p.x p.vx; if (p.y canvas.height) p.y 0; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); }); requestAnimationFrame(tick); } tick(); }逻辑说明prefers-reduced-motion是浏览器暴露的用户偏好系统开启“减弱动态效果”时直接跳过canvas初始化一帧都不会跑。粒子数量按屏幕宽度切到24或48是移动端与桌面端的直接取舍。白色半透明圆点适合表现雪或柳絮要做雨丝把fillStyle换成细线、vy提到2~4即可。参数说明canvas元素的CSS尺寸要和绘制像素尺寸一致否则粒子会发虚或被拉伸。可用以下方式兜底canvas.width canvas.offsetWidth; canvas.height canvas.offsetHeight;设置时放在粒子初始化之前并监听resize重新赋值。还有一种更常见的坑canvas默认有内联宽度300x150如果CSS设了width: 100%; height: 100%但没同步给canvas.width粒子只会出现在左上角一小块区域。视觉层做完后页面已经具备“季节感”。下一层把交互补上详情展开、浏览位置记忆、滚动动画、季节筛选。4. 交互层不引依赖用HTML5自带能力做详情加载与状态记忆一个zip包内的节气网站不应该要求用户联网加载CDN脚本。所有交互尽量走浏览器原生API像details、localStorage、IntersectionObserver这些都是现成的能力写起来反而比引库更稳。4.1 用 details/summary 做无JS的节气详情展开节气卡片上通常有“习俗、饮食、诗词”三类信息默认全部展开会让页面变得很长折叠起来又需要JavaScript帮忙。原生details恰好解决这个问题details classterm-detail summary习俗与饮食/summary ul li咬春/li li打春牛/li li吃春饼、萝卜/li /ul /details逻辑说明浏览器原生支持折叠/展开键盘Tab聚焦、回车切换都不需要额外写事件。默认details是关闭的要对“当前节气”默认展开在渲染时判定命中的卡片加open属性。参数说明Chrome里summary自带黑色三角标记通常要隐藏掉再替换成自定义加号.summary::-webkit-details-marker { display: none; } .term-detail summary { cursor: pointer; list-style: none; position: relative; padding-left: 22px; } .term-detail summary::before { content: ; position: absolute; left: 0; }注意list-style: none是Firefox隐藏三角的写法::-webkit-details-marker是WebKit内核的写法两者都要保留。如果希望每次只展开一个详情可以监听toggle事件关闭其他卡片的open属性这个手风琴行为不是原生的但多数用户期望是这样。4.2 localStorage记录浏览位置与主题偏好刷新不迷路用户找了半天才点开“大暑”刷新页面后又回到顶部。把当前节气id和季节主题存进localStorage下次回访时直接恢复。这里的坑比功能本身更值得注意const STORE_KEY solar-site-pref-v1; function savePref() { const data { lastTermId: currentTermId, season: document.body.dataset.season || spring, ts: Date.now() }; try { localStorage.setItem(STORE_KEY, JSON.stringify(data)); } catch (err) { console.warn(localStorage不可用忽略本次保存, err); } } function loadPref() { let raw null; try { raw localStorage.getItem(STORE_KEY); } catch (err) { return null; } if (!raw) return null; try { return JSON.parse(raw); } catch (err) { console.warn(preference解析失败忽略恢复, err); return null; } }逻辑说明setItem在浏览器隐私模式、存储配额满、部分企业安全策略下会抛异常不包try/catch会中断整段脚本。JSON.parse同样要保护因为localStorage里的内容是可被用户手动修改的坏了不能影响页面正常显示。ts字段先存着后续若做“超过7天重置偏好”的功能直接用这个时间戳判断即可。参数说明存储key建议带版本号v1以后数据结构变化时旧key可以直接废弃不用写迁移逻辑。存储内容只放“偏好”不放节气原始数据数据仍从terms-data.js读取状态与数据分离避免用户清理浏览器后页面变成空白。4.3 IntersectionObserver做滚动进入动画替代scroll监听节气卡滚入视口时做一个淡入上移的动画。传统做法是监听scroll每次滚动遍历所有卡片计算位置这个页面有24张卡虽然没那么夸张但移动端滚动监听的开销仍然明显。IntersectionObserver由浏览器在合适的时机回调不算性能负担const io new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { entry.target.classList.add(is-visible); io.unobserve(entry.target); } }); }, { root: null, threshold: 0.15, rootMargin: 0px 0px -30px 0px }); document.querySelectorAll(.term-card).forEach(card io.observe(card));逻辑说明unobserve是性能要点。动画只播一次观察完的卡片从观察列表里移除后续滚动不再重复触发。卡片初始样式设为opacity: 0; transform: translateY(16px);加上.is-visible后过渡到可见状态。首屏卡片在页面加载后立即触发视觉上相当于直接显示不会有明显的延迟闪现。参数说明threshold: 0.15表示卡片有15%进入视口才触发这个值对横向卡片比较友好rootMargin: 0px 0px -30px 0px表示底部边界提前30px触发适合让动画发生在“用户刚看到卡片边缘”的时机。调试时若发现卡片停在透明状态优先检查IntersectionObserver是否在旧浏览器里不存在if (!(IntersectionObserver in window)) { document.querySelectorAll(.term-card).forEach(card { card.classList.add(is-visible); }); }这段降级代码在所有卡片加载后执行直接显示全部卡片避免不支持该API的浏览器出现“内容永远看不见”的问题。4.4 事件委托、节气高亮与URL状态同步24张卡逐个绑定click事件既冗余又难维护常见做法是把监听绑在同一个父容器上再用closest找到目标卡片const grid document.getElementById(termGrid); grid.addEventListener(click, (e) { const card e.target.closest([data-term-id]); if (!card) return; const id Number(card.dataset.termId); const term SOLAR_TERMS.find(t t.id id); if (!term) return; document.querySelectorAll(.term-card).forEach(c c.classList.remove(is-current)); card.classList.add(is-current); document.body.dataset.season term.season; window.location.hash term-${id}; savePref(); });逻辑说明closest([data-term-id])会沿DOM向上找带该属性的祖先所以点击卡片里的文字、SVG图标都能命中。用dataset.termId与数据表匹配而不是比中文节气名避免全角半角、空格差异导致匹配失败。window.location.hash写入#term-5刷新后可以从location.hash恢复高亮这个hash可以分享别人打开同一链接能定位到同一张卡。参数说明Number(card.dataset.termId)必须转换类型dataset取出的值都是字符串而数据表里id是数字直接用比较会永远为false。savePref()放在hash更新之后确保恢复页面时能还原到刚选中的节气。5. 收尾把HTML5二十四节气网站封装成可复现的zip5.1 一个最小可发布的目录结构与瘦身清单打包之前先做一次目录收拢将源码与发布目录分离。常见交付结构如下dist/ ├── index.html ├── assets/ │ ├── css/style.css │ ├── js/terms-data.js │ ├── js/main.js │ ├── icons/sprite.svg │ ├── img/ # 未被SVG替代的位图 │ └── fonts/ # 可选中文字体子集 └── README.txt发布前三项必做检查所有资源都使用相对路径任何/assets/...绝对路径在解压到子目录后都会404图片转WebP后保留同名的PNG回退用picture标签做兼容SVG文件删除编辑器导出的元数据、空分组和未使用的滤镜。这个步骤能直接把zip体积砍掉三到五成。5.2 用命令行压缩处理中文文件名与目录层次Windows下最稳妥的打包方式是7-Zip命令行而不是PowerShell的Compress-Archive。后者的中文文件名在部分老旧解压工具里会出现乱码目录嵌套也容易出错。固定参数写成一个bat脚本即可7z a -tzip -scsUTF-8 -r ..\release\24-solar-terms.zip .\dist\*翻译成PowerShell同样适用7z a -tzip -scsUTF-8 -r ..\release\24-solar-terms.zip .\dist\*参数含义-tzip指定zip格式-scsUTF-8让文件名按UTF-8编码写入这是中文文件名不乱码的关键-r递归包含子目录.\dist\*只打包dist目录内部内容避免外层目录被套进zip造成“解开后还要再进一层文件夹才能看到index.html”。输出到..\release\是为了防止压缩包出现在dist内部把自己套进包里。macOS/Linux 上的zip命令同样需要排除系统文件cd dist zip -r ../24-solar-terms.zip . -x *.DS_Store -x *__MACOSX*打包后务必原地解压一次确认根目录直接出现index.html。如果解压后第一层是个文件夹说明打包时多套了一层目录。5.3 解压后的验收清单以下五条检查项可以在本地直接完成不用上传服务器检查项预期结果失败常见原因双击打开index.html24张节气卡全部渲染main.js或terms-data.js路径错误浏览器控制台无报错无404、无红色报错sprite.svg路径或use引用id错误点击节气卡片卡片高亮、hash变为#term-x>cd dist python -m http.server 8080浏览器访问http://localhost:8080重点看右键菜单里的“查看源代码”能否正常定位到assets/下所有文件。所有验证通过后zip才算真正具备“解压即用”的交付质量我通常还会在README.txt里写明运行方式、数据口径和已测浏览器列表减少对方拿到包后反复询问的沟通成本。本文还有配套的精品资源点击获取