十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

纯HTML+CSS+JS实现交互式日历与实时天气预报

纯HTML+CSS+JS实现交互式日历与实时天气预报 简介这是一份面向HTML初学者与前端入门学习者的网页设计实践项目聚焦日历与天气预报两大实用功能的静态页面实现帮助学生掌握HTML结构搭建、CSS样式美化及基础JavaScript交互逻辑。资源包共32个文件包含3个HTML主页面、3个CSS样式表、2个JS脚本文件、18张PNG/JPG图片素材涵盖晴雨雪等天气图标及背景图、1段MP3背景音乐及LICENSE说明文件整体压缩包仅3.55MB轻量易部署。已有1549人学习下载适合课堂期中作业提交、个人作品集构建或前端技能阶梯式训练。读者可直接运行查看完整效果获得从页面布局、响应式适配、图标资源组织到音频控制的全流程参考尤其适合理解如何将静态页面升级为具备基础交互能力的前端小应用。1. 这不是静态页面练习而是一次真实前端能力的闭环验证用纯 HTMLCSSJS 实现带交互逻辑的日历与天气预报双模块网页很多同学把“HTML期中作业”当成标签堆砌任务结果交上去的页面点不动、日期不会变、天气数据是写死的 lorem ipsum。但真实场景中一个能展示当前月份日历、支持前后月切换、自动高亮今日、并实时获取本地城市天气温度、湿度、图标、简要描述的网页必须同时满足三重约束语义化结构time、section、article的合理嵌套、响应式布局在手机屏上日历格子不挤、天气卡片不溢出、以及可维护的 JS 逻辑日期计算不依赖 moment.js 等外部库天气请求有 fallback 机制。它面向的是刚学完 DOM 操作和 Fetch API 的大一学生也适合作为前端新人的自测项目——因为所有功能都必须在单个.html文件内完成不引入构建工具、不依赖后端接口使用公开免密天气 API且兼容 Chrome/Firefox/Edge 最新版。你不需要懂 Vue 或 React但必须清楚new Date()的月份索引为什么从 0 开始、meta charsetutf-8为什么必须放在head最前面、以及fetch()失败时如何优雅降级显示“暂无数据”。2. 用原生 HTML5 语义化结构搭建骨架从!doctype html到可访问性友好的区域划分2.1 严格遵循 W3C 推荐的文档类型与语言声明所有现代 HTML 页面必须以!doctype html开头这是触发浏览器标准模式的唯一可靠方式。紧接着的html langzh-cn不仅声明中文内容更影响屏幕阅读器的发音规则和搜索引擎的语义理解。head中的meta charsetutf-8必须置于title之前——若位置错误中文标题可能显示为乱码而meta nameviewport contentwidthdevice-width, initial-scale1.0是响应式的基石缺它则移动端页面会默认按 980px 渲染导致文字过小、按钮难点击。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title日历与天气预报 | HTML期中作业/title link relstylesheet hrefstyle.css /head body !-- 主体内容 -- /body /html提示link relstylesheet hrefstyle.css建议放在head底部而非body内避免 CSS 加载阻塞 DOM 解析若追求极致首屏速度可将关键 CSS 内联但本作业以结构清晰为优先。2.2 按功能域划分语义化区块拒绝div classbox式命名使用main包裹核心内容section分隔日历与天气两大模块每个模块内用header标注标题区、article封装可独立分发的数据单元如单条天气预报。日历部分采用table而非 CSS Grid —— 因为课程要求考察 HTML 表格语义caption、thead、tbody、th scopecol且表格天然具备行列对齐优势。天气模块用dl定义列表组织键值对比pstrong温度/strongspan24℃/span/p更符合 ARIA 规范。main section idcalendar-section header h2数字日历/h2 button idprev-monthlt;/button span idcurrent-month-year2024年10月/span button idnext-monthgt;/button /header table aria-label2024年10月日历 caption2024年10月日历/caption thead tr th scopecol日/th th scopecol一/th th scopecol二/th th scopecol三/th th scopecol四/th th scopecol五/th th scopecol六/th /tr /thead tbody idcalendar-body !-- JS 动态生成 -- /tbody /table /section section idweather-section header h2本地天气预报/h2 p idweather-location正在定位.../p /header article idweather-data dl div dt当前温度/dt dd idtemp-value--℃/dd /div div dt天气状况/dt dd idweather-desc加载中/dd /div div dt湿度/dt dd idhumidity-value--%/dd /div div dt风速/dt dd idwind-value--m/s/dd /div /dl img idweather-icon alt天气图标 width64 height64 /article /section /main注意table的aria-label和caption双重声明确保屏幕阅读器能准确播报th scopecol明确列头作用比单纯th更利于辅助技术解析。2.3 关键元信息配置让搜索引擎和移动端正确识别除基础meta外增加 Open Graph 协议标签使网页在微信、QQ 等社交平台分享时显示规范标题和缩略图添加theme-color让 Chrome 地址栏在 Android 上匹配页面主色。这些虽非课程硬性要求但体现工程化思维。!-- 在 head 中追加 -- meta propertyog:title content日历与天气预报 | HTML期中作业 meta propertyog:description content纯 HTMLCSSJS 实现的交互式日历与实时天气预报网页 meta propertyog:image contenthttps://via.placeholder.com/600x315/4a90e2/ffffff?textCalendar%26Weather meta nametheme-color content#4a90e23. 用原生 JavaScript 实现动态日历渲染与月份导航逻辑3.1 构建可复用的日期工具函数避开getMonth()的经典陷阱JavaScript 的Date对象月份索引从 0 开始01月1112月这是初学者高频踩坑点。我们封装getDaysInMonth(year, month)和getFirstDayOfMonth(year, month)两个纯函数前者返回指定年月的天数自动处理闰年后者返回该月第一天是星期几0周日6周六为表格填充提供坐标依据。// 工具函数获取某年某月的总天数 function getDaysInMonth(year, month) { // month 是 0-11需 1 后传入 new Date() return new Date(year, month 1, 0).getDate(); } // 工具函数获取某年某月第一天是星期几0周日 function getFirstDayOfMonth(year, month) { return new Date(year, month, 1).getDay(); } // 当前日期状态 let currentDate new Date(); let currentYear currentDate.getFullYear(); let currentMonth currentDate.getMonth(); // 0-11 // 渲染日历的核心函数 function renderCalendar() { const tbody document.getElementById(calendar-body); const year currentYear; const month currentMonth; const daysInMonth getDaysInMonth(year, month); const firstDayOfWeek getFirstDayOfMonth(year, month); // 0-6 // 清空旧内容 tbody.innerHTML ; // 创建空白单元格占位到第一个日期前 for (let i 0; i firstDayOfWeek; i) { const tr document.createElement(tr); const td document.createElement(td); td.classList.add(empty); tr.appendChild(td); tbody.appendChild(tr); } // 填充日期单元格 let date 1; while (date daysInMonth) { const tr document.createElement(tr); for (let col 0; col 7; col) { const td document.createElement(td); if (date daysInMonth) { td.textContent date; // 高亮今日 if (date currentDate.getDate() month currentDate.getMonth() year currentDate.getFullYear()) { td.classList.add(today); } date; } else { td.classList.add(empty); } tr.appendChild(td); } tbody.appendChild(tr); } // 更新页眉显示 const monthNames [1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月]; document.getElementById(current-month-year).textContent ${year}年${monthNames[month]}; }逻辑说明new Date(year, month 1, 0)利用“下个月第0天即本月最后一天”的特性获取天数getDay()返回 0-6 直接对应表格列索引classList.add(today)为后续 CSS 样式提供钩子避免内联样式污染结构。3.2 实现无刷新月份切换事件委托与状态管理为“上月/下月”按钮绑定事件注意使用event.preventDefault()阻止默认行为并通过currentMonth状态变量控制边界1月不能切到0月12月不能切到13月。关键技巧是不在按钮上硬编码>document.getElementById(prev-month).addEventListener(click, function(e) { e.preventDefault(); if (currentMonth 0) { currentMonth--; } else { currentMonth 11; currentYear--; } renderCalendar(); }); document.getElementById(next-month).addEventListener(click, function(e) { e.preventDefault(); if (currentMonth 11) { currentMonth; } else { currentMonth 0; currentYear; } renderCalendar(); });参数说明currentMonth和currentYear是全局状态变量所有日历操作均基于此renderCalendar()被设计为幂等函数重复调用不会产生副作用便于调试。3.3 CSS 响应式日历样式用minmax()和aspect-ratio控制格子比例日历表格在桌面端需保持正方形格子在移动端需适应窄屏。利用 CSS Grid 替代传统表格样式保留语义化table结构通过grid-template-columns: repeat(7, 1fr)均分列宽并用aspect-ratio: 1/1强制单元格为正方形。media查询在 768px 下改为font-size: 14px避免小屏文字溢出。/* style.css */ #calendar-body td { width: 14.2857%; /* 100% / 7 */ aspect-ratio: 1/1; display: flex; align-items: center; justify-content: center; font-weight: 500; border: 1px solid #e0e0e0; } #calendar-body .today { background-color: #4a90e2; color: white; border-radius: 50%; font-weight: bold; } media (max-width: 768px) { #calendar-body td { font-size: 14px; padding: 4px; } #calendar-section header button { padding: 6px 12px; } }提示aspect-ratio兼容 Chrome 88、Firefox 89、Safari 15.4覆盖主流浏览器若需兼容旧版可用padding-top: 100%position: relative方案但本作业以现代标准为准。4. 集成免费天气 API 实现真实数据获取与错误处理4.1 选用 Open-Meteo 免密 API零注册、无配额限制、支持中文城市放弃需要申请 Key 的商业天气服务如心知天气、和风天气采用 Open-Meteo 的地理编码 天气预报组合方案。其优势在于1通过geocoding接口根据城市名获取经纬度2再用forecast接口查实时天气3全程 HTTPS、CORS 友好、响应快。关键参数nameBeijing城市名、count1只取最匹配结果、languagezh返回中文描述。// 获取城市经纬度 async function fetchCoordinates(cityName) { try { const res await fetch( https://geocoding-api.open-meteo.com/v1/search?name${encodeURIComponent(cityName)}count1languagezhformatjson ); if (!res.ok) throw new Error(地理编码请求失败: ${res.status}); const data await res.json(); if (data.results data.results.length 0) { return { latitude: data.results[0].latitude, longitude: data.results[0].longitude, timezone: data.results[0].timezone }; } else { throw new Error(未找到该城市); } } catch (err) { console.error(获取坐标失败:, err); updateWeatherUI(城市未找到, --℃, --%, --m/s, ); return null; } } // 根据经纬度获取天气 async function fetchWeather(lat, lon) { try { const res await fetch( https://api.open-meteo.com/v1/forecast?latitude${lat}longitude${lon}currenttemperature_2m,relative_humidity_2m,wind_speed_10m,weather_codedailyweather_code,temperature_2m_max,temperature_2m_mintimezoneautoforecast_days1languagezh ); if (!res.ok) throw new Error(天气请求失败: ${res.status}); return await res.json(); } catch (err) { console.error(获取天气失败:, err); return null; } }逻辑说明encodeURIComponent(cityName)防止中文城市名如“上海”导致 URL 解析错误timezoneauto自动匹配时区避免时间显示偏差languagezh确保weather_code对应的中文描述如“晴天”。4.2 天气数据解析与 UI 更新映射 weather_code 到图标和文字Open-Meteo 的weather_code是数字编码0晴天3多云45雾需建立映射表转换为可读文字和 SVG 图标。我们使用 Weather Icons 的 SVG 片段内联避免额外 HTTP 请求。// weather_code 映射表精简版 const weatherCodeMap { 0: { desc: 晴天, icon: svg viewBox0 0 24 24circle cx12 cy12 r5/path dM12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42//svg }, 1: { desc: 晴间多云, icon: svg viewBox0 0 24 24circle cx12 cy12 r5/path dM12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42/path dM16 8c0 2.21-1.79 4-4 4s-4-1.79-4-4 1.79-4 4-4 4 1.79 4 4z//svg }, 3: { desc: 多云, icon: svg viewBox0 0 24 24path dM18 10h-1.26c-.15.68-.36 1.33-.63 1.95a2.5 2.5 0 1 1-4.9 0c-.27-.62-.48-1.27-.63-1.95H6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z//svg } }; function updateWeatherUI(location, temp, humidity, wind, code) { document.getElementById(weather-location).textContent location; document.getElementById(temp-value).textContent temp; document.getElementById(humidity-value).textContent humidity; document.getElementById(wind-value).textContent wind; const weatherData weatherCodeMap[code] || weatherCodeMap[0]; document.getElementById(weather-desc).textContent weatherData.desc; document.getElementById(weather-icon).innerHTML weatherData.icon; }参数说明weatherCodeMap仅包含作业常用代码完整版可扩展至 100SVG 内联避免跨域问题且比 PNG 更清晰updateWeatherUI()统一更新所有 UI 元素保证状态一致性。4.3 定位失败时的优雅降级策略从 IP 定位到手动输入浏览器navigator.geolocation可能被用户拒绝或超时。我们设计三级 fallback1尝试获取用户地理位置2失败则用默认城市“北京”3最终失败则显示输入框让用户手动填写。所有步骤均不中断页面其他功能。async function initWeather() { // 尝试获取用户位置 if (geolocation in navigator) { navigator.geolocation.getCurrentPosition( async (pos) { const { latitude, longitude } pos.coords; const weather await fetchWeather(latitude, longitude); if (weather weather.current) { const city await reverseGeocode(latitude, longitude); updateWeatherUI(city || 当前位置, ${Math.round(weather.current.temperature_2m)}℃, ${weather.current.relative_humidity_2m}%, ${weather.current.wind_speed_10m}m/s, weather.current.weather_code ); } else { // 降级到北京 await loadWeatherForCity(北京); } }, (err) { console.warn(定位被拒绝或超时使用默认城市, err); loadWeatherForCity(北京); }, { timeout: 5000 } ); } else { // 浏览器不支持定位 loadWeatherForCity(北京); } } async function loadWeatherForCity(city) { const coords await fetchCoordinates(city); if (coords) { const weather await fetchWeather(coords.latitude, coords.longitude); if (weather weather.current) { updateWeatherUI(city, ${Math.round(weather.current.temperature_2m)}℃, ${weather.current.relative_humidity_2m}%, ${weather.current.wind_speed_10m}m/s, weather.current.weather_code ); } } }注意timeout: 5000限制定位等待时间避免用户长时间等待reverseGeocode()函数可调用 Open-Meteo 的反向地理编码接口此处为简化省略实现实际作业中可补充。5. 优化加载性能与可访问性预加载关键资源与键盘导航支持5.1 使用link relpreload提前加载字体与天气图标天气图标 SVG 虽小但首次渲染时仍可能造成 FOUCFlash of Unstyled Content。通过preload提前发起请求确保图标在 JS 执行前已就绪。同时预加载中文字体如Noto Sans SC避免系统字体渲染差异。!-- 在 head 中 -- link relpreload hrefhttps://fonts.googleapis.com/css2?familyNotoSansSC:wght300;400;500;700displayswap asstyle onloadthis.onloadnull;this.relstylesheet link relpreload hrefdata:image/svgxml,svg xmlnshttp://www.w3.org/2000/svg viewBox0 0 24 24circle cx12 cy12 r5//svg asimage提示onload属性用于动态加载字体 CSS避免阻塞渲染SVG 数据 URI 预加载可减少后续内联时的解析延迟。5.2 为日历按钮和天气区域添加键盘焦点与快捷键满足 WCAG 2.1 AA 标准所有交互元素必须可通过Tab键聚焦Enter或Space触发。为“上月/下月”按钮添加rolebutton和tabindex0并监听keydown事件。// 为按钮添加键盘支持 document.querySelectorAll(#prev-month, #next-month).forEach(btn { btn.setAttribute(role, button); btn.setAttribute(tabindex, 0); btn.addEventListener(keydown, function(e) { if (e.key Enter || e.key ) { e.preventDefault(); this.click(); } }); }); // 天气区域支持焦点管理 document.getElementById(weather-section).addEventListener(keydown, function(e) { if (e.key Escape) { // 按 Esc 清除天气焦点如有 this.blur(); } });逻辑说明e.preventDefault()阻止空格键滚动页面rolebutton告诉屏幕阅读器这是可点击控件tabindex0使其进入自然 Tab 顺序。5.3 用IntersectionObserver实现天气数据懒加载天气模块在页面底部用户可能不滚动到底部。使用IntersectionObserver监听天气区域是否进入视口仅当可见时才发起 API 请求节省带宽并提升首屏速度。// 初始化观察器 const weatherSection document.getElementById(weather-section); const observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { initWeather(); observer.unobserve(weatherSection); // 加载后停止观察 } }); }, { threshold: 0.1 }); observer.observe(weatherSection);参数说明threshold: 0.1表示当 10% 区域可见时触发unobserve()避免重复加载若用户禁用 JS天气区域仍会显示“正在定位...”符合渐进增强原则。页面加载完成后日历自动渲染当前月份天气模块在用户滚动至可视区域时发起请求。整个流程无需构建工具、不依赖 Node.js 环境一个.html文件即可运行完全符合 HTML 期中作业的技术边界与教学目标。本文还有配套的精品资源点击获取
返回列表