Publish Mini Program Dev Version / publish (push) Successful in 45s
1. 节日速查显示最近10条(之前只显示3条) 2. 修复右侧'还有XX天'溢出问题 - fest-left 添加 flex-shrink: 0 和 min-width - 确保文字不会溢出屏幕
468 lines
16 KiB
JavaScript
468 lines
16 KiB
JavaScript
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: '老黄历 - 传统农历黄历查询'
|
||
};
|
||
}
|
||
});
|