<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">📅 日历案例 · 优雅月视图</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}
body {
background: linear-gradient(145deg, #f4f7fc 0%, #e9eef4 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 1.5rem;
}
.calendar-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 2.5rem;
box-shadow: 0 20px 40px rgba(0, 10, 30, 0.2), 0 8px 20px rgba(0, 0, 0, 0.06);
width: 100%;
max-width: 850px;
padding: 2rem 1.8rem 2.2rem;
border: 1px solid rgba(255,255,255,0.4);
transition: 0.3s ease;
}
/* 头部区域 */
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.month-year {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.month-year .month {
font-size: 2.4rem;
font-weight: 600;
color: #1f2b3c;
letter-spacing: -0.5px;
}
.month-year .year {
font-size: 1.6rem;
font-weight: 400;
color: #5b6b7f;
}
.nav-buttons {
display: flex;
gap: 0.6rem;
}
.nav-btn {
background: white;
border: none;
width: 48px;
height: 48px;
border-radius: 20px;
font-size: 1.6rem;
font-weight: 300;
color: #2a405c;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 6px 12px rgba(0, 20, 40, 0.08);
transition: all 0.2s;
border: 1px solid rgba(255,255,255,0.7);
}
.nav-btn:hover {
background: #ffffff;
transform: translateY(-2px);
box-shadow: 0 12px 18px rgba(0, 20, 40, 0.12);
color: #0f2a44;
}
.nav-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(0, 20, 40, 0.08);
}
/* 星期缩写行 */
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
margin-bottom: 0.75rem;
font-weight: 600;
color: #4f6479;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0 0.2rem;
}
.weekdays div {
padding: 0.5rem 0;
}
/* 日期网格 */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-bottom: 1.5rem;
}
.day-cell {
aspect-ratio: 1 / 1.05;
background: rgba(255, 255, 255, 0.5);
border-radius: 18px;
padding: 0.3rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
transition: all 0.15s;
border: 1px solid rgba(255,255,255,0.3);
cursor: pointer;
position: relative;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.02);
backdrop-filter: blur(4px);
}
.day-cell:hover {
background: rgba(255, 255, 255, 0.85);
transform: scale(1.02);
border-color: #9ab8da;
box-shadow: 0 8px 18px rgba(0, 40, 80, 0.08);
}
.day-cell.empty {
background: transparent;
border: 1px solid transparent;
box-shadow: none;
pointer-events: none;
backdrop-filter: none;
}
.date-number {
font-size: 1.2rem;
font-weight: 500;
color: #20344c;
width: 100%;
text-align: left;
padding-left: 0.3rem;
padding-top: 0.2rem;
}
.event-dots {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
gap: 3px;
padding: 0.2rem 0.3rem;
margin-top: auto;
width: 100%;
}
.event-dot {
width: 7px;
height: 7px;
border-radius: 10px;
background: #4f8bd9;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.event-dot.more {
background: #a75fbf;
}
/* 选中日期高亮 */
.selected {
background: #d8e7ff;
border: 2px solid #3c7bc9;
box-shadow: 0 0 0 3px rgba(73, 138, 221, 0.2);
}
/* 今日标记 */
.today .date-number {
font-weight: 700;
color: #0d59a0;
}
.today::after {
content: '●';
position: absolute;
bottom: 6px;
right: 8px;
font-size: 9px;
color: #2b7ecc;
}
/* 底部事件面板 */
.event-panel {
background: rgba(255, 255, 255, 0.6);
border-radius: 2rem;
padding: 1.2rem 1.5rem 1rem;
backdrop-filter: blur(5px);
border: 1px solid rgba(255,255,255,0.6);
box-shadow: inset 0 1px 4px rgba(255,255,255,0.5);
}
.panel-title {
display: flex;
align-items: center;
justify-content: space-between;
color: #2c3e50;
font-weight: 600;
font-size: 1rem;
letter-spacing: 0.02em;
border-bottom: 2px dashed #cbd6e3;
padding-bottom: 0.6rem;
margin-bottom: 0.8rem;
}
.event-list {
min-height: 56px;
display: flex;
flex-direction: column;
gap: 6px;
}
.event-item {
background: white;
padding: 0.6rem 1rem;
border-radius: 40px;
font-size: 0.9rem;
color: #1a2c3f;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.02);
border: 1px solid rgba(200, 210, 225, 0.3);
}
.event-badge {
width: 8px;
height: 8px;
border-radius: 10px;
background: #3f7ec6;
display: inline-block;
}
.event-time {
font-size: 0.75rem;
color: #6a7f99;
letter-spacing: 0.03em;
background: #f0f4fa;
padding: 0.2rem 0.5rem;
border-radius: 30px;
font-weight: 500;
}
.no-event {
color: #7b8a9e;
font-style: italic;
padding-left: 0.8rem;
letter-spacing: 0.02em;
}
.add-event-hint {
color: #1470d2;
font-size: 0.9rem;
margin-top: 0.5rem;
border-top: 1px dashed #cadaef;
padding-top: 0.7rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.add-event-hint button {
background: #3c7bc9;
border: none;
color: white;
padding: 0.5rem 1.1rem;
border-radius: 40px;
font-weight: 500;
font-size: 0.8rem;
cursor: pointer;
transition: 0.2s;
box-shadow: 0 4px 8px rgba(60, 123, 201, 0.2);
}
.add-event-hint button:hover {
background: #2c66a8;
transform: translateY(-1px);
}
.add-event-hint input {
background: white;
border: 1px solid #cddae9;
border-radius: 40px;
padding: 0.55rem 1rem;
width: 60%;
font-size: 0.85rem;
outline: none;
}
.add-event-hint input:focus {
border-color: #3f7ec6;
box-shadow: 0 0 0 3px rgba(63, 126, 198, 0.15);
}
.toast-msg {
font-size: 0.75rem;
color: #a3316b;
padding-top: 0.3rem;
padding-left: 0.5rem;
}
@media (max-width: 580px) {
.calendar-card { padding: 1.2rem; }
.month-year .month { font-size: 1.8rem; }
.day-cell { border-radius: 14px; }
.nav-btn { width: 42px; height: 42px; }
}
</style>
</head>
<body>
<div class="calendar-card" id="calendarApp">
<!-- 头部导航 -->
<div class="calendar-header">
<div class="month-year">
<span class="month" id="monthDisplay">October</span>
<span class="year" id="yearDisplay">2025</span>
</div>
<div class="nav-buttons">
<button class="nav-btn" id="prevBtn" aria-label="上一月">←</button>
<button class="nav-btn" id="nextBtn" aria-label="下一月">→</button>
</div>
</div>
<!-- 星期 -->
<div class="weekdays">
<div>周一</div><div>周二</div><div>周三</div><div>周四</div>
<div>周五</div><div>周六</div><div>周日</div>
</div>
<!-- 日期网格 -->
<div class="calendar-grid" id="calendarGrid"></div>
<!-- 事件详情面板 -->
<div class="event-panel">
<div class="panel-title">
<span id="selectedDateLabel">📌 2025年10月1日</span>
</div>
<div class="event-list" id="eventListContainer">
<div class="no-event">✨ 今天还没有安排,点击日期或添加事件</div>
</div>
<!-- 快速添加事件行 -->
<div class="add-event-hint">
<input type="text" id="eventInput" placeholder="团队会议 17:00" maxlength="40" />
<button id="addEventBtn">+ 添加</button>
</div>
<div id="toastMsg" class="toast-msg"></div>
</div>
</div>
<script>
(function(){
// ---------- 核心状态 ----------
const currentDate = new Date(); // 实际今天
// 当前展示的年月 (显示月份)
let displayYear = currentDate.getFullYear();
let displayMonth = currentDate.getMonth(); // 0-11
// 选中的日期 (默认今天)
let selectedDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
// 事件存储:key = 'YYYY-M-D' (本地)
const eventStore = new Map();
// ---------- DOM 元素 ----------
const monthDisplay = document.getElementById('monthDisplay');
const yearDisplay = document.getElementById('yearDisplay');
const calendarGrid = document.getElementById('calendarGrid');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const selectedDateLabel = document.getElementById('selectedDateLabel');
const eventListContainer = document.getElementById('eventListContainer');
const eventInput = document.getElementById('eventInput');
const addEventBtn = document.getElementById('addEventBtn');
const toastMsg = document.getElementById('toastMsg');
// ---------- 工具函数 ----------
const monthNames = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
const monthNamesFull = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'];
// 根据年月获取该月第一天是星期几(周一作为第一列)
function getFirstDayOfMonth(year, month) {
const day = new Date(year, month, 1).getDay(); // 0=日, 1=一... 6=六
// 周一(1) -> 索引0, 周日(0) -> 索引6
return day === 0 ? 6 : day - 1;
}
// 获取某年某月总天数
function daysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
// 格式化日期为 YYYY-M-D (本地)
function formatKey(date) {
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
}
// 判断两个日期是否为同一天
function isSameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
// 获取某日期的事件数组
function getEventsForDate(date) {
const key = formatKey(date);
return eventStore.get(key) || [];
}
// ---------- 渲染日历网格 ----------
function renderCalendar() {
// 更新头部
monthDisplay.textContent = monthNamesFull[displayMonth];
yearDisplay.textContent = displayYear;
// 获取网格信息
const firstDayOffset = getFirstDayOfMonth(displayYear, displayMonth);
const totalDays = daysInMonth(displayYear, displayMonth);
// 清空网格
calendarGrid.innerHTML = '';
// 填充空白 (前导)
for (let i = 0; i < firstDayOffset; i++) {
const emptyDiv = document.createElement('div');
emptyDiv.className = 'day-cell empty';
calendarGrid.appendChild(emptyDiv);
}
// 填充日期
for (let day = 1; day <= totalDays; day++) {
const cellDate = new Date(displayYear, displayMonth, day);
const cell = document.createElement('div');
cell.className = 'day-cell';
// 今日标记
if (isSameDay(cellDate, currentDate)) {
cell.classList.add('today');
}
// 选中状态
if (isSameDay(cellDate, selectedDate)) {
cell.classList.add('selected');
}
// 日期数字
const dateSpan = document.createElement('span');
dateSpan.className = 'date-number';
dateSpan.textContent = day;
cell.appendChild(dateSpan);
// 事件圆点(最多显示2个)
const events = getEventsForDate(cellDate);
if (events.length > 0) {
const dotsContainer = document.createElement('div');
dotsContainer.className = 'event-dots';
const maxDots = Math.min(events.length, 3);
for (let i = 0; i < maxDots; i++) {
const dot = document.createElement('span');
dot.className = i === 2 && events.length > 3 ? 'event-dot more' : 'event-dot';
dotsContainer.appendChild(dot);
}
cell.appendChild(dotsContainer);
}
// 绑定点击事件
cell.addEventListener('click', (function(d){
return function() {
// 更新选中日期
selectedDate = d;
// 让与选中相关的高亮更新
updateSelectedHighlight();
renderEventPanel();
};
})(cellDate));
calendarGrid.appendChild(cell);
}
// 更新事件面板显示
renderEventPanel();
}
// 高亮更新(移除所有选中类,重新添加)
function updateSelectedHighlight() {
const allCells = document.querySelectorAll('.day-cell');
const firstDayOffset = getFirstDayOfMonth(displayYear, displayMonth);
const totalDays = daysInMonth(displayYear, displayMonth);
let idx = 0;
// 匹配到每个日期格子,并重新根据选中态设置
for (let i = 0; i < firstDayOffset; i++) {
idx++;
}
for (let day = 1; day <= totalDays; day++) {
const cell = allCells[firstDayOffset + day - 1];
const cellDate = new Date(displayYear, displayMonth, day);
if (isSameDay(cellDate, selectedDate)) {
cell.classList.add('selected');
} else {
cell.classList.remove('selected');
}
}
}
// 渲染事件面板
function renderEventPanel() {
const events = getEventsForDate(selectedDate);
const dateStr = `${selectedDate.getFullYear()}年 ${monthNames[selectedDate.getMonth()]} ${selectedDate.getDate()}日`;
selectedDateLabel.textContent = `📌 ${dateStr}`;
if (events.length === 0) {
eventListContainer.innerHTML = `<div class="no-event">✨ 这一天暂无安排,添加一个吧</div>`;
} else {
let html = '';
events.forEach((ev, index) => {
// 简单拆分时间部分,让展示更友好
let timePart = '';
let titlePart = ev;
const parts = ev.split(' ');
// 简单判断:如果最后部分包含 ':' 就视为时间
if (parts.length > 1 && /^\d{1,2}:\d{2}$/.test(parts[parts.length-1])) {
timePart = parts[parts.length-1];
titlePart = parts.slice(0, -1).join(' ');
} else if (/^\d{1,2}:\d{2}$/.test(ev)) {
timePart = ev;
titlePart = '事件';
}
html += `<div class="event-item">
<span class="event-badge"></span>
<span>${titlePart}</span>
${timePart ? `<span class="event-time">${timePart}</span>` : ''}
<span style="margin-left:auto; font-size:0.8rem; color:#9babc1; cursor:pointer;" data-index="${index}" data-key="${formatKey(selectedDate)}" class="delete-event">✕</span>
</div>`;
});
eventListContainer.innerHTML = html;
// 绑定删除事件
document.querySelectorAll('.delete-event').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const idx = parseInt(this.dataset.index, 10);
const key = this.dataset.key;
const eventsArray = eventStore.get(key) || [];
if (idx >= 0 && idx < eventsArray.length) {
eventsArray.splice(idx, 1);
eventStore.set(key, eventsArray);
// 刷新日历和面板
renderCalendar();
renderEventPanel();
}
});
});
}
}
// 添加事件逻辑
function addEventFromInput() {
const raw = eventInput.value.trim();
if (!raw) {
showToast('请输入事件内容');
return;
}
const key = formatKey(selectedDate);
const existing = eventStore.get(key) || [];
// 防止重复或过量
if (existing.length >= 8) {
showToast('最多允许8个事件/天,请删除一些');
return;
}
existing.push(raw);
eventStore.set(key, existing);
eventInput.value = '';
// 重新渲染日历(更新圆点)以及面板
renderCalendar(); // 重绘全部会保留选中状态
renderEventPanel();
showToast('✓ 已添加到 ' + selectedDate.getFullYear() + '/' + (selectedDate.getMonth()+1) + '/' + selectedDate.getDate());
}
// 提示消息
function showToast(msg) {
toastMsg.textContent = msg;
toastMsg.style.opacity = 1;
setTimeout(() => {
toastMsg.style.transition = 'opacity 0.3s';
toastMsg.style.opacity = 0;
}, 1800);
}
// 月份切换
function changeMonth(offset) {
let newMonth = displayMonth + offset;
let newYear = displayYear;
if (newMonth > 11) {
newMonth = 0;
newYear++;
} else if (newMonth < 0) {
newMonth = 11;
newYear--;
}
displayYear = newYear;
displayMonth = newMonth;
// 确保选中日期在该月中,如果不在,则设置为该月1号(体验更佳)
if (selectedDate.getFullYear() !== displayYear || selectedDate.getMonth() !== displayMonth) {
selectedDate = new Date(displayYear, displayMonth, 1);
}
renderCalendar();
}
// 初始化一些示例事件 (让演示有内容)
function seedEvents() {
const today = new Date();
const addEvent = (dateOffset, eventText, monthOffset = 0) => {
const base = new Date(today.getFullYear(), today.getMonth() + monthOffset, today.getDate() + dateOffset);
const key = formatKey(base);
if (!eventStore.has(key)) eventStore.set(key, []);
eventStore.get(key).push(eventText);
};
// 在今天附近加点事
addEvent(0, '💻 站会 10:00');
addEvent(1, '📝 设计评审 14:30');
addEvent(-1, '🧘 拉伸运动 08:30');
// 再向当前显示月填充一些演示 (如果当前不是下个月,就补一些)
const nextMonth = new Date(today.getFullYear(), today.getMonth()+1, 3);
const keyNext = formatKey(nextMonth);
if (!eventStore.has(keyNext)) {
eventStore.set(keyNext, ['📅 里程碑规划', '☕ 咖啡约 15:00']);
}
const prevMonth = new Date(today.getFullYear(), today.getMonth()-1, 15);
const keyPrev = formatKey(prevMonth);
if (!eventStore.has(keyPrev)) {
eventStore.set(keyPrev, ['🎯 月度总结']);
}
}
// ---------- 事件绑定 ----------
prevBtn.addEventListener('click', () => changeMonth(-1));
nextBtn.addEventListener('click', () => changeMonth(1));
addEventBtn.addEventListener('click', addEventFromInput);
eventInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addEventFromInput();
});
// 初始化加载
seedEvents();
renderCalendar();
// 修复外部点击删除后闪烁问题
window.addEventListener('load', ()=>{
renderEventPanel();
});
})();
</script>
</body>
</html>
日历功能与交互
您可以轻松管理每日安排,整个界面围绕“查看”和“记录”两个核心操作设计。

- 日期导航与选择:点击顶部的左右箭头按钮可以切换月份,日历网格中,今天和当前选中的日期会有高亮标记,点击任意日期即可快速选中。
- 事件添加与管理:在底部面板的输入框中输入事件描述(可包含时间,如“团队会议 17:00”),点击“添加”按钮即可将事件绑定到选中日期,已添加的事件会以圆点形式显示在日历上,并在面板中列出,您可以通过点击事件旁的“✕”按钮删除。
- 数据持久化:示例数据会在页面加载时自动创建,您添加的事件会保存在当前页面的内存中,刷新页面后会自动重置为示例数据,方便您反复测试。