const { getDayInfo, getAlmanacInfo, getMonthCalendar, getLegalHoliday, isWorkday, getUpcomingFestivals, getNextBuddhistFestival, getYearFestivals } = require('../../utils/core/index.js'); const { SOLAR_TERM_TIMES } = require('../../utils/core/data.js'); const { request } = require('../../utils/request.js'); function pad(n) { return String(n).padStart(2, '0'); } function fmt(y, m, d) { return `${y}-${pad(m)}-${pad(d)}`; } /** 历史上的今天年份显示:负数为公元前,0 为年份不详 */ function fmtYear(year) { if (!year) return ''; return year < 0 ? `前${-year}年` : `${year}年`; } const OTD_CACHE_KEY = 'otd-cache'; const OTD_CACHE_TTL = 48 * 3600 * 1000; // 48 小时(数据按月日维度存储,变化频率低) const MONTH_SPAN = 2; // 当前月前后各2个月,共5个月 const DAY_SPAN = 3; // 当前日前后各3天,共7天(固定窗口,避免动态增删导致动画丢失) Page({ data: { statusBarHeight: 20, navBarHeight: 44, viewMode: 'day', // 日视图 swiper dayList: [], // [{key, dayInfo, almanac, legalHoliday, workday, termTime, isFirstOrFifteen, nextBuddhist, showBuddhist, upcoming}] dayIndex: 2, // 当前居中 // 月历 swiper monthList: [], monthIndex: 2, currentYear: 0, currentMonth: 0, weekHeaders: ['日', '一', '二', '三', '四', '五', '六'], // 当前选中日期(用于月历联动) dayInfo: {}, // 历史上的今天加载中 otdLoading: false, // 标记是否正在程序触发 swiper 切换(避免 onDaySwiperChange 重复处理) swiperAnimating: false, // 标记是否需要在动画完成后滚动窗口 pendingShift: 0, // 全屏弹窗 zoomVisible: false, zoomTitle: '', zoomType: '', zoomData: null, // 设置 showBuddhist: false, highlightLunar: true }, onLoad() { const sys = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); const menu = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; const statusBarHeight = sys.statusBarHeight || 20; let navBarHeight = 44; if (menu && menu.top) navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height; this.setData({ statusBarHeight, navBarHeight }); this.loadSettings(); this.buildDayList(new Date()); }, onShow() { this.loadSettings(); if (this.data.dayList.length > 0) this.refreshDayListSettings(); }, loadSettings() { const s = wx.getStorageSync('lunar-settings') || {}; this.setData({ showBuddhist: s.showBuddhist === true, highlightLunar: s.highlightLunar !== false }); }, onPullDownRefresh() { if (this.data.viewMode === 'day') { this.buildDayList(new Date()); } else { this.buildMonthList(this.data.currentYear, this.data.currentMonth); } wx.stopPullDownRefresh(); }, /** 构建某日的完整数据 */ buildDayData(date) { const y = date.getFullYear(); const m = date.getMonth() + 1; const d = date.getDate(); const dayInfo = getDayInfo(y, m, d); const almanac = getAlmanacInfo(y, m, d); const legalHoliday = getLegalHoliday(y, m, d); const workday = isWorkday(y, m, d); const termTime = dayInfo.solarTerm ? (SOLAR_TERM_TIMES[dayInfo.solarTerm] || '') : ''; const isFirstOrFifteen = this.data.highlightLunar && (dayInfo.lunarDay === 1 || dayInfo.lunarDay === 15); const nextBuddhist = this.data.showBuddhist ? getNextBuddhistFestival(y, m, d) : null; const upcoming = getUpcomingFestivals(10, y, m, d); return { key: fmt(y, m, d), dayInfo, almanac, legalHoliday, workday, termTime, isFirstOrFifteen, nextBuddhist, upcoming, showBuddhist: this.data.showBuddhist, otd: null, // 历史上的今天数据(异步加载) otdTab: 'event' // event:大事记 birth:出生 }; }, /** 构建7天的数据,居中显示 */ buildDayList(centerDate) { const dayList = []; for (let i = -DAY_SPAN; i <= DAY_SPAN; i++) { const date = new Date(centerDate.getFullYear(), centerDate.getMonth(), centerDate.getDate() + i); dayList.push(this.buildDayData(date)); } const center = dayList[DAY_SPAN]; this.setData({ dayList, dayIndex: DAY_SPAN, dayInfo: center.dayInfo, currentYear: center.dayInfo.solarYear, currentMonth: center.dayInfo.solarMonth }); this.ensureOtd(DAY_SPAN); }, /** 设置变化后刷新 dayList */ refreshDayListSettings() { const dayList = this.data.dayList.map(item => ({ ...item, showBuddhist: this.data.showBuddhist, isFirstOrFifteen: this.data.highlightLunar && (item.dayInfo.lunarDay === 1 || item.dayInfo.lunarDay === 15), nextBuddhist: this.data.showBuddhist ? getNextBuddhistFestival(item.dayInfo.solarYear, item.dayInfo.solarMonth, item.dayInfo.solarDay) : null })); this.setData({ dayList }); }, /** 日视图 swiper 切换 */ onDaySwiperChange(e) { const index = e.detail.current; const { dayList, swiperAnimating } = this.data; const cur = dayList[index]; this.setData({ dayIndex: index, dayInfo: cur.dayInfo, currentYear: cur.dayInfo.solarYear, currentMonth: cur.dayInfo.solarMonth }); this.ensureOtd(index); // 程序触发切换时不处理边缘滚动(避免打断动画) if (swiperAnimating) { this.setData({ swiperAnimating: false }); return; } // 用户手动滑动到边缘时,标记待滚动方向,等动画完成后再滚动 if (index === 0) { this.setData({ pendingShift: -1 }); } else if (index === dayList.length - 1) { this.setData({ pendingShift: 1 }); } }, /** 日视图 swiper 动画完成 */ onDaySwiperFinish() { const { pendingShift } = this.data; if (pendingShift !== 0) { this.setData({ pendingShift: 0 }); this.shiftDayWindow(pendingShift); } }, /** 滚动日数据窗口:direction -1=向前,1=向后 */ shiftDayWindow(direction) { const { dayList } = this.data; const edge = direction < 0 ? dayList[0] : dayList[dayList.length - 1]; const offset = direction < 0 ? -1 : 1; const newDate = new Date(edge.dayInfo.solarYear, edge.dayInfo.solarMonth - 1, edge.dayInfo.solarDay + offset); const newDay = this.buildDayData(newDate); let newList, newIndex; if (direction < 0) { // 向前滚动:新数据加到前面,当前索引+1(因为前面多了一天) newList = [newDay, ...dayList.slice(0, -1)]; newIndex = this.data.dayIndex + 1; } else { // 向后滚动:新数据加到后面,当前索引-1(因为前面少了一天) newList = [...dayList.slice(1), newDay]; newIndex = this.data.dayIndex - 1; } // 滚动窗口后,current 保持连续,swiper 会平滑滑动到新位置 this.setData({ dayList: newList, dayIndex: newIndex, dayInfo: newList[newIndex].dayInfo, currentYear: newList[newIndex].dayInfo.solarYear, currentMonth: newList[newIndex].dayInfo.solarMonth }); this.ensureOtd(newIndex); }, prevDay() { const { dayIndex, dayList } = this.data; if (dayIndex > 0) { // 不在边缘,正常切换 const index = dayIndex - 1; this.setData({ dayIndex: index, swiperAnimating: true }); this.ensureOtd(index); } else { // 在边缘,滚动窗口(窗口滚动后 dayIndex 会调整为 1,swiper 从 0 滑到 1,有动画) this.shiftDayWindow(-1); } }, nextDay() { const { dayIndex, dayList } = this.data; if (dayIndex < dayList.length - 1) { // 不在边缘,正常切换 const index = dayIndex + 1; this.setData({ dayIndex: index, swiperAnimating: true }); this.ensureOtd(index); } else { // 在边缘,滚动窗口(窗口滚动后 dayIndex 会调整为 5,swiper 从 6 滑到 5,有动画) this.shiftDayWindow(1); } }, /** ===== 历史上的今天(数据来源:中文维基百科) ===== */ /** 确保指定 index 的日期已加载 OTD 数据(优先读缓存) */ ensureOtd(index) { const item = this.data.dayList[index]; if (!item || item.otd || item.otdLoading) return; const m = item.dayInfo.solarMonth; const d = item.dayInfo.solarDay; const cacheKey = `${m}-${d}`; // 读本地缓存(按月日维度,48 小时有效) try { const cache = wx.getStorageSync(OTD_CACHE_KEY) || {}; const hit = cache[cacheKey]; if (hit && hit.data && Date.now() - hit.ts < OTD_CACHE_TTL) { this.applyOtd(index, item.key, hit.data); return; } } catch (e) { /* 缓存不可用时忽略 */ } this.setData({ [`dayList[${index}].otdLoading`]: true }); request({ url: `/api/wiki/on-this-day?month=${m}&day=${d}` }) .then(res => { if (res && res.code === 0 && res.data) { this.applyOtd(index, item.key, res.data); this.saveOtdCache(cacheKey, res.data); } }) .catch(() => { /* 网络失败时不显示卡片 */ }) .then(() => { // 窗口可能已滚动,先确认 index 处仍是同一天 const cur = this.data.dayList[index]; if (cur && cur.key === item.key) { this.setData({ [`dayList[${index}].otdLoading`]: false }); } }); }, /** 将 OTD 数据写入指定日期(异步返回时窗口可能已滚动,需校验 key) */ applyOtd(index, key, data) { const cur = this.data.dayList[index]; if (!cur || cur.key !== key) return; const withYear = list => (list || []).map(it => ({ ...it, yearText: fmtYear(it.year) })); const events = withYear(data.events); const births = withYear(data.births); const deaths = withYear(data.deaths); const festivals = withYear(data.festivals); if (!events.length && !births.length) return; // 无数据不显示卡片 this.setData({ [`dayList[${index}].otd`]: { events, births, deaths, festivals, eventsPreview: events.slice(0, 4), birthsPreview: births.slice(0, 4) } }); }, /** 写入本地缓存(最多保留 80 天,超出淘汰最早的) */ saveOtdCache(cacheKey, data) { try { const cache = wx.getStorageSync(OTD_CACHE_KEY) || {}; cache[cacheKey] = { ts: Date.now(), data }; const keys = Object.keys(cache); if (keys.length > 80) delete cache[keys[0]]; wx.setStorageSync(OTD_CACHE_KEY, cache); } catch (e) { /* 存储失败时忽略 */ } }, /** 卡片内切换 大事记/出生 */ switchOtdTab(e) { const { date, tab } = e.currentTarget.dataset; const index = this.data.dayList.findIndex(item => item.key === date); if (index >= 0) { this.setData({ [`dayList[${index}].otdTab`]: tab }); } }, /** 更新当前 dayInfo(不触发 swiper 事件) */ updateCurrentDayInfo() { const cur = this.data.dayList[this.data.dayIndex]; if (cur) { this.setData({ dayInfo: cur.dayInfo, currentYear: cur.dayInfo.solarYear, currentMonth: cur.dayInfo.solarMonth }); } }, backToday() { this.buildDayList(new Date()); }, /** 切换 日/月 视图 */ toggleView() { const mode = this.data.viewMode === 'day' ? 'month' : 'day'; this.setData({ viewMode: mode }); if (mode === 'month') { const { dayInfo } = this.data; this.buildMonthList(dayInfo.solarYear, dayInfo.solarMonth); } }, /** 构建5个月的数据,居中显示 */ buildMonthList(year, month) { const monthList = []; for (let i = -MONTH_SPAN; i <= MONTH_SPAN; i++) { let y = year, m = month + i; while (m < 1) { m += 12; y--; } while (m > 12) { m -= 12; y++; } const weeks = getMonthCalendar(y, m); monthList.push({ key: `${y}-${m}`, year: y, month: m, weeks }); } this.setData({ monthList, monthIndex: MONTH_SPAN, currentYear: year, currentMonth: month }); }, /** 月历 swiper 切换 */ onMonthSwiperChange(e) { const index = e.detail.current; const { monthList } = this.data; const cur = monthList[index]; this.setData({ monthIndex: index, currentYear: cur.year, currentMonth: cur.month }); if (index === 0) { this.prependMonth(); } else if (index === monthList.length - 1) { this.appendMonth(); } }, prependMonth() { const { monthList } = this.data; const first = monthList[0]; let y = first.year, m = first.month - 1; if (m < 1) { m = 12; y--; } const weeks = getMonthCalendar(y, m); monthList.unshift({ key: `${y}-${m}`, year: y, month: m, weeks }); if (monthList.length > 7) monthList.pop(); this.setData({ monthList, monthIndex: this.data.monthIndex + 1 }); }, appendMonth() { const { monthList } = this.data; const last = monthList[monthList.length - 1]; let y = last.year, m = last.month + 1; if (m > 12) { m = 1; y++; } const weeks = getMonthCalendar(y, m); monthList.push({ key: `${y}-${m}`, year: y, month: m, weeks }); if (monthList.length > 7) monthList.shift(); this.setData({ monthList, monthIndex: this.data.monthIndex - 1 }); }, /** 月历点选某天 → 切回日视图 */ selectDay(e) { const date = e.currentTarget.dataset.date; const [y, m, d] = date.split('-').map(Number); this.buildDayList(new Date(y, m - 1, d)); this.setData({ viewMode: 'day' }); }, /** 跳转到某节日的老黄历 */ goFestival(e) { const date = e.currentTarget.dataset.date; const [y, m, d] = date.split('-').map(Number); this.buildDayList(new Date(y, m - 1, d)); this.setData({ viewMode: 'day' }); }, /** ===== 全屏放大弹窗 ===== */ openZoom(e) { const type = e.currentTarget.dataset.type; const date = e.currentTarget.dataset.date; const dayItem = this.data.dayList.find(item => item.key === date); if (!dayItem) return; const { dayInfo, almanac, legalHoliday, termTime } = dayItem; let zoomTitle = ''; let zoomData = null; if (type === 'yiji') { zoomTitle = `${dayInfo.solarMonth}月${dayInfo.solarDay}日 宜忌`; zoomData = { recommends: almanac.recommends, avoids: almanac.avoids }; } else if (type === 'detail') { zoomTitle = `${dayInfo.solarMonth}月${dayInfo.solarDay}日 黄历详情`; zoomData = { lunar: `${dayInfo.lunarMonthName}${dayInfo.lunarDayName}`, ganzhi: `${dayInfo.lunarYearGanzhi}年 ${dayInfo.lunarMonthGanzhi}月 ${dayInfo.lunarDayGanzhi}日`, duty: almanac.duty, twelveStar: `${almanac.twelveStar.name}(${almanac.twelveStar.ecliptic})`, twentyEightStar: almanac.twentyEightStar.name, sixStar: almanac.sixStar, clash: `冲${almanac.clash} 煞${almanac.evilDirection}`, pengZu: almanac.pengZu, legalHoliday, termTime: dayInfo.solarTerm ? `${dayInfo.solarTerm} ${termTime}` : '' }; } else if (type === 'fest') { zoomTitle = '近期节日'; zoomData = { upcoming: dayItem.upcoming }; } else if (type === 'fest-year') { zoomTitle = `${dayInfo.solarYear}年节日`; zoomData = { yearFestivals: getYearFestivals(dayInfo.solarYear) }; } else if (type === 'otd') { zoomTitle = `${dayInfo.solarMonth}月${dayInfo.solarDay}日 历史上的今天`; zoomData = dayItem.otd; } this.setData({ zoomVisible: true, zoomTitle, zoomType: type, zoomData }); }, closeZoom() { this.setData({ zoomVisible: false }); }, navTo(e) { wx.navigateTo({ url: e.currentTarget.dataset.url }); }, noop() {}, // 分享给好友 onShareAppMessage() { return { title: '老黄历 - 传统农历黄历查询', path: '/pages/home/home' }; }, // 分享到朋友圈 onShareTimeline() { return { title: '老黄历 - 传统农历黄历查询' }; } });