diff --git a/src/components/HomeSidebar.astro b/src/components/HomeSidebar.astro
index 6aae57f..9a61c80 100644
--- a/src/components/HomeSidebar.astro
+++ b/src/components/HomeSidebar.astro
@@ -121,11 +121,62 @@ const recentPosts = posts.slice(0, 5);
(() => {
const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const pad = (value) => String(value).padStart(2, '0');
+ const dayMs = 86400000;
const percent = (used, total) => Math.min(100, Math.max(0, (used / total) * 100));
+ const lunarFormatter = new Intl.DateTimeFormat('zh-CN-u-ca-chinese', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ const solarFestivals = {
+ '01-01': '元旦',
+ '03-08': '妇女节',
+ '03-12': '植树节',
+ '05-01': '劳动节',
+ '05-04': '青年节',
+ '06-01': '儿童节',
+ '07-01': '建党节',
+ '08-01': '建军节',
+ '09-10': '教师节',
+ '10-01': '国庆节',
+ };
+ const lunarFestivals = {
+ '正月-1': '春节',
+ '正月-15': '元宵',
+ '五月-5': '端午',
+ '七月-7': '七夕',
+ '七月-15': '中元',
+ '八月-15': '中秋',
+ '九月-9': '重阳',
+ '腊月-8': '腊八',
+ '腊月-23': '北方小年',
+ '腊月-24': '南方小年',
+ };
+ const springFestivalDates = [
+ '2024-02-10',
+ '2025-01-29',
+ '2026-02-17',
+ '2027-02-06',
+ '2028-01-26',
+ '2029-02-13',
+ '2030-02-03',
+ '2031-01-23',
+ '2032-02-11',
+ ];
const setText = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
+ const toDateKey = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
+ const toMonthDayKey = (date) => `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
+ const escapeHtml = (value) =>
+ String(value).replace(/[&<>"']/g, (char) => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ })[char]);
function dayOfYear(date) {
const start = new Date(date.getFullYear(), 0, 1);
@@ -134,15 +185,55 @@ const recentPosts = posts.slice(0, 5);
function weekOfYear(date) {
const firstDay = new Date(date.getFullYear(), 0, 1);
- const pastDays = Math.floor((date - firstDay) / 86400000);
+ const pastDays = Math.floor((date - firstDay) / dayMs);
return Math.ceil((pastDays + firstDay.getDay() + 1) / 7);
}
+ function getLunarInfo(date) {
+ const parts = lunarFormatter.formatToParts(date);
+ const month = parts.find((part) => part.type === 'month')?.value || '';
+ const day = parts.find((part) => part.type === 'day')?.value || '';
+ const yearName = parts.find((part) => part.type === 'yearName')?.value || '';
+ return {
+ yearName,
+ month,
+ day,
+ dayName: day === '1' ? month : `${day}日`,
+ festival: lunarFestivals[`${month}-${day}`] || '',
+ };
+ }
+
+ function getFestival(date) {
+ const lunar = getLunarInfo(date);
+ const solar = solarFestivals[toMonthDayKey(date)] || '';
+ return solar || lunar.festival;
+ }
+
+ function getNextNewYearEve(now) {
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const nextSpring = springFestivalDates
+ .map((date) => new Date(`${date}T00:00:00`))
+ .find((date) => date > today);
+
+ if (!nextSpring) {
+ return new Date(now.getFullYear() + 1, 1, 16);
+ }
+
+ return new Date(nextSpring.getFullYear(), nextSpring.getMonth(), nextSpring.getDate() - 1);
+ }
+
function renderCalendar(now) {
+ const todayLunar = getLunarInfo(now);
+ const todayFestival = getFestival(now);
setText('calendar-week', `第${weekOfYear(now)}周 ${dayNames[now.getDay()]}`);
setText('calendar-date', pad(now.getDate()));
setText('calendar-solar', `${now.getFullYear()}年${now.getMonth() + 1}月 第${dayOfYear(now)}天`);
- setText('calendar-lunar', '万千风景,五历如一');
+ setText(
+ 'calendar-lunar',
+ todayFestival
+ ? `${todayLunar.yearName}年 ${todayFestival}`
+ : `${todayLunar.yearName}年 农历${todayLunar.month}${todayLunar.day}日`
+ );
const grid = document.getElementById('calendar-grid');
if (!grid) return;
@@ -162,8 +253,19 @@ const recentPosts = posts.slice(0, 5);
: dayNumber < 1
? previousDays + dayNumber
: dayNumber - days;
+ const cellDate = new Date(year, month, dayNumber);
+ const lunar = getLunarInfo(cellDate);
const isToday = isCurrent && displayDay === now.getDate();
- cells.push(`${displayDay}`);
+ const title = `${toDateKey(cellDate)} 农历${lunar.month}${lunar.day}日`;
+ cells.push(`
+
+ ${displayDay}
+
+ `);
}
grid.innerHTML = cells.join('');
@@ -171,8 +273,8 @@ const recentPosts = posts.slice(0, 5);
function renderSchedule(now) {
const year = now.getFullYear();
- const nextEve = new Date(year + 1, 1, 16);
- const daysToEve = Math.max(0, Math.ceil((nextEve - now) / 86400000));
+ const nextEve = getNextNewYearEve(now);
+ const daysToEve = Math.max(0, Math.ceil((nextEve - now) / dayMs));
const yearStart = new Date(year, 0, 1);
const yearEnd = new Date(year + 1, 0, 1);
const monthStart = new Date(year, now.getMonth(), 1);
@@ -192,10 +294,15 @@ const recentPosts = posts.slice(0, 5);
rows.forEach(([name, start, end]) => {
const total = end - start;
const used = now - start;
- const left = Math.max(0, Math.ceil((end - now) / 86400000));
+ const progressValue = percent(used, total);
+ const left = Math.max(0, Math.ceil((end - now) / dayMs));
const bar = document.getElementById(`${name}-progress`);
- if (bar) bar.value = percent(used, total);
- setText(`${name}-left`, `还剩${left}天`);
+ if (bar) {
+ bar.value = progressValue;
+ bar.title = `已过 ${progressValue.toFixed(1)}%`;
+ bar.setAttribute('aria-valuetext', `已过 ${progressValue.toFixed(1)}%`);
+ }
+ setText(`${name}-left`, `${progressValue.toFixed(0)}% / ${left}天`);
});
}
diff --git a/src/styles/site.css b/src/styles/site.css
index 217c22d..aa5b6ec 100644
--- a/src/styles/site.css
+++ b/src/styles/site.css
@@ -800,17 +800,24 @@ a {
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
- gap: 5px 7px;
+ gap: 4px 7px;
color: #8aa6be;
- font-size: 0.78rem;
text-align: center;
}
.calendar-grid span {
display: grid;
place-items: center;
- min-height: 20px;
+ min-height: 22px;
+ padding: 0;
+ border: 1px solid transparent;
border-radius: 999px;
+ line-height: 1;
+}
+
+.calendar-grid b {
+ font-size: 0.78rem;
+ font-weight: 700;
}
.calendar-grid .muted {
@@ -820,6 +827,7 @@ a {
.calendar-grid .today {
color: #fff;
background: #66c8f5;
+ border-color: #66c8f5;
}
.schedule-bars {
@@ -829,7 +837,7 @@ a {
.schedule-row {
display: grid;
- grid-template-columns: 34px 1fr 64px;
+ grid-template-columns: 34px minmax(52px, 1fr) 74px;
align-items: center;
gap: 8px;
color: var(--muted);
@@ -838,27 +846,33 @@ a {
.schedule-row progress {
width: 100%;
- height: 18px;
+ height: 10px;
overflow: hidden;
border: 0;
- border-radius: 4px;
- background: rgba(102, 200, 245, 0.18);
+ border-radius: 999px;
+ background: rgba(102, 200, 245, 0.16);
}
.schedule-row progress::-webkit-progress-bar {
- background: rgba(102, 200, 245, 0.18);
+ border-radius: 999px;
+ background: rgba(102, 200, 245, 0.16);
}
.schedule-row progress::-webkit-progress-value {
- background: rgba(102, 200, 245, 0.58);
+ border-radius: 999px;
+ background: linear-gradient(90deg, #5ec7f4, #42d4c4);
}
.schedule-row progress::-moz-progress-bar {
- background: rgba(102, 200, 245, 0.58);
+ border-radius: 999px;
+ background: linear-gradient(90deg, #5ec7f4, #42d4c4);
}
.schedule-row em {
+ min-width: 0;
font-style: normal;
+ text-align: right;
+ white-space: nowrap;
}
.sidebar-posts {