本文介绍如何基于原生 html5 日期输入逻辑,构建一个可切换“月日”与“年积日(day of year)”显示模式的自定义日历组件,兼顾标准兼容性与卫星数据处理需求。
本文介绍如何基于原生 html5 日期输入逻辑,构建一个可切换“月日”与“年积日(day of year)”显示模式的自定义日历组件,兼顾标准兼容性与卫星数据处理需求。
在遥感、气象及地球观测领域,卫星数据常以 YYYYddd 格式标注时间(如 2024120 表示 2024 年第 120 天,即 4 月 29 日),这种“年积日”(Day of Year, DOY)表示法比传统 YYYY-MM-DD 更便于序列化处理与跨年计算。然而,原生 仅支持 ISO 8601 格式(YYYY-MM-DD),无法直接显示或输入 YYYYddd。本文提供一种轻量、可嵌入、符合 HTML5 语义习惯的解决方案:不替换原生输入框,而是为其配套构建一个功能对齐 NOAA 网站风格的自定义日历弹窗,并支持一键切换“月日视图”与“年积日视图”。
核心设计思路
- 解耦渲染与数据:保留原生 作为表单提交和无障碍访问的基础,同时通过 JavaScript 动态绑定自定义日历面板;
- 双模式日历:点击“Toggle Day Number View”按钮,表格单元格从显示 1–31 切换为显示对应日期在当年的序号(1–365/366);
- 精准 DOY 计算:采用时间戳差值法(new Date(year, month, day) - new Date(year, 0, 0))避免闰年判断误差,确保 getDayNumberOfYear() 结果 100% 准确;
- 响应式交互:支持年/月导航、自动更新标题、空单元格占位,保持视觉一致性。
以下为完整可运行代码(含内联样式与脚本,开箱即用):
<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><!-- 原生日期输入框(保留标准表单能力) --><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>');
}
// 插入当月日期单元格
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>');
}
}
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>
关键注意事项
- ✅ 无障碍与兼容性:原生 仍存在,确保移动端唤起系统日历、屏幕阅读器识别及表单验证正常工作;
- ⚠️ 样式隔离建议:生产环境请将 CSS 提取至外部文件,并使用 CSS Modules 或 Shadow DOM 避免全局污染;
- ? 双向同步增强:当前示例已实现“日历选中 → 输入框更新”,如需反向同步(输入框修改 → 日历跳转),可在 input 事件监听中解析 YYYY-MM-DD 并重置 currentYear/currentMonth;
- ? 模块化封装:推荐将上述逻辑封装为 Web Component(如 doy-calendar 自定义元素),便于复用与维护。
该方案不依赖第三方库,纯原生技术栈实现,既满足科研场景对 YYYYddd 的高频操作需求,又坚守 Web 标准底线——让专业工具回归语义化、可访问、可持续演进的 HTML 生态。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











