本文介绍如何基于原生 html5 日期输入逻辑,构建一个可切换“月日”与“年积日(day of year)”显示模式的自定义日历组件,兼顾标准兼容性与卫星数据处理需求。
本文介绍如何基于原生 html5 日期输入逻辑,构建一个可切换“月日”与“年积日(day of year)”显示模式的自定义日历组件,兼顾标准兼容性与卫星数据处理需求。
在遥感、气象及地球观测领域,卫星数据常以 YYYYddd 格式标注时间(如 2024120 表示 2024 年第 120 天,即 4 月 29 日),这种“年积日”(Day of Year, DOY)表示法比传统 YYYY-MM-DD 更便于序列化处理与跨年计算。然而,原生 <input type="date"> 仅支持 ISO 8601 格式(YYYY-MM-DD),无法直接显示或输入 YYYYddd。本文提供一种轻量、可嵌入、符合 HTML5 语义习惯的解决方案:不替换原生输入框,而是为其配套构建一个功能对齐 NOAA 网站风格的自定义日历弹窗,并支持一键切换“月日视图”与“年积日视图”。
以下为完整可运行代码(含内联样式与脚本,开箱即用):
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>DOY-Aware Calendar Picker</title> <style> #calcontainer { position: relative; margin: 20px 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } #calendar { border-collapse: collapse; width: 100%; margin-top: 12px; } #calendar th, #calendar td { border: 1px solid #e0e0e0; padding: 10px 6px; text-align: center; vertical-align: middle; } #calendar th { background-color: #f8f9fa; font-weight: 600; color: #495057; } .calendar-row { height: 48px; } button { margin: 4px; padding: 6px 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; } button:hover { background: #0056b3; } #cp_monthYear { font-weight: bold; margin: 0 16px; display: inline-block; } </style></head><body> <!-- 原生日期输入框(保留标准表单能力) --> <input type="date" id="dateInput" style="margin: 0 20px;"> <div id="calcontainer"> <!-- 导航栏 --> <div> <button id="cp_prevYear" onclick="changeYear(-1)">< 上一年</button> <button id="cp_prevMonth" onclick="changeMonth(-1)">< 上一月</button> <span id="cp_monthYear"></span> <button id="cp_nextMonth" onclick="changeMonth(1)">下一月 ></button> <button id="cp_nextYear" onclick="changeYear(1)">下一年 ></button> </div> <div style="margin-top: 8px;"> <button onclick="toggleDayNumberView()">切换年积日视图</button> </div> <!-- 日历主体 --> <table id="calendar"> <thead> <tr> <th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th> </tr> </thead> <tbody></tbody> </table> </div> <script> let currentYear = new Date().getFullYear(); let currentMonth = new Date().getMonth(); // 0-based let showDayNumbers = false; const dateInput = document.getElementById('dateInput'); function populateCalendar() { const monthYearDisplay = document.getElementById('cp_monthYear'); const tbody = document.querySelector('#calendar tbody'); tbody.innerHTML = ''; const firstDay = new Date(currentYear, currentMonth, 1); const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate(); const startDayOfWeek = firstDay.getDay(); // 0=Sun, 6=Sat // 更新标题 monthYearDisplay.textContent = `${firstDay.toLocaleDateString('zh-CN', { month: 'long' })} ${currentYear}`; // 插入前置空单元格(补齐周日开始) for (let i = 0; i < startDayOfWeek; i++) { tbody.insertAdjacentHTML('beforeend', '<td></td>'); } // 插入当月日期单元格 let dayNum = showDayNumbers ? getDayNumberOfYear(currentYear, currentMonth, 1) : 1; for (let day = 1; day <= daysInMonth; day++) { const cell = document.createElement('td'); cell.textContent = showDayNumbers ? dayNum++ : day; cell.addEventListener('click', () => selectDate(day)); tbody.appendChild(cell); } // 补齐末尾空单元格(保证 6 行) const totalCells = startDayOfWeek + daysInMonth; const remaining = (7 - totalCells % 7) % 7; for (let i = 0; i < remaining; i++) { tbody.insertAdjacentHTML('beforeend', '<td></td>'); } } function selectDate(day) { const selectedDate = new Date(currentYear, currentMonth, day); const isoStr = selectedDate.toISOString().split('T')[0]; // YYYY-MM-DD dateInput.value = isoStr; // 可选:触发 input/change 事件以通知框架 dateInput.dispatchEvent(new Event('input', { bubbles: true })); dateInput.dispatchEvent(new Event('change', { bubbles: true })); } function changeMonth(delta) { currentMonth += delta; if (currentMonth < 0) { currentMonth = 11; currentYear--; } else if (currentMonth > 11) { currentMonth = 0; currentYear++; } populateCalendar(); } function changeYear(delta) { currentYear += delta; populateCalendar(); } function toggleDayNumberView() { showDayNumbers = !showDayNumbers; populateCalendar(); } function getDayNumberOfYear(year, month, day) { const date = new Date(year, month, day); const yearStart = new Date(year, 0, 1); const diffMs = date.getTime() - yearStart.getTime(); return Math.floor(diffMs / (1000 * 60 * 60 * 24)) + 1; } // 初始化并绑定输入框同步(可选增强) dateInput.addEventListener('change', () => { const d = new Date(dateInput.value); if (!isNaN(d.getTime())) { currentYear = d.getFullYear(); currentMonth = d.getMonth(); populateCalendar(); } }); populateCalendar(); </script></body></html>
该方案不依赖第三方库,纯原生技术栈实现,既满足科研场景对 YYYYddd 的高频操作需求,又坚守 Web 标准底线——让专业工具回归语义化、可访问、可持续演进的 HTML 生态。