diff --git a/mini/app.json b/mini/app.json
index 6985ba1..4300bf9 100644
--- a/mini/app.json
+++ b/mini/app.json
@@ -59,5 +59,10 @@
},
"style": "v2",
"sitemapLocation": "sitemap.json",
- "lazyCodeLoading": "requiredComponents"
+ "lazyCodeLoading": "requiredComponents",
+ "permission": {
+ "scope.record": {
+ "desc": "用于许愿树吹风互动效果"
+ }
+ }
}
diff --git a/mini/pages/home/home.js b/mini/pages/home/home.js
index d01c05b..5b9b68d 100644
--- a/mini/pages/home/home.js
+++ b/mini/pages/home/home.js
@@ -1,21 +1,40 @@
-const { getDayInfo, getAlmanacInfo, getMonthCalendar } = require('../../utils/core/index.js');
+const {
+ getDayInfo, getAlmanacInfo, getMonthCalendar,
+ getLegalHoliday, isWorkday, getUpcomingFestivals, getNextBuddhistFestival
+} = require('../../utils/core/index.js');
+const { SOLAR_TERM_TIMES } = require('../../utils/core/data.js');
function pad(n) { return String(n).padStart(2, '0'); }
function fmt(y, m, d) { return `${y}-${pad(m)}-${pad(d)}`; }
+const MONTH_SPAN = 2; // 当前月前后各2个月,共5个月
+
Page({
data: {
statusBarHeight: 20,
navBarHeight: 44,
- viewMode: 'day', // day | month
+ viewMode: 'day',
dayInfo: {},
almanac: {},
- // 月历
- year: 0,
- month: 0,
+ legalHoliday: null,
+ workday: true,
+ termTime: '',
+ isFirstOrFifteen: false,
+ nextBuddhist: null,
+ showBuddhist: false,
+ highlightLunar: true,
+ upcoming: [],
+ // 月历 swiper
+ monthList: [], // [{key, year, month, weeks}]
+ monthIndex: 2, // 当前居中
+ currentYear: 0,
+ currentMonth: 0,
weekHeaders: ['日', '一', '二', '三', '四', '五', '六'],
- calendarDays: [],
- selectedDay: null
+ // 全屏弹窗
+ zoomVisible: false,
+ zoomTitle: '',
+ zoomType: '', // yiji | detail | fest
+ zoomData: null
},
onLoad() {
@@ -23,18 +42,33 @@ Page({
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;
- }
+ if (menu && menu.top) navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height;
this.setData({ statusBarHeight, navBarHeight });
+ this.loadSettings();
this.loadDay(new Date());
+ this.loadUpcoming();
+ },
+
+ onShow() {
+ this.loadSettings();
+ // 设置可能变化,刷新
+ if (this.data.dayInfo.solarYear) this.refreshDerived();
+ },
+
+ loadSettings() {
+ const s = wx.getStorageSync('lunar-settings') || {};
+ this.setData({
+ showBuddhist: s.showBuddhist === true,
+ highlightLunar: s.highlightLunar !== false
+ });
},
onPullDownRefresh() {
if (this.data.viewMode === 'day') {
this.loadDay(new Date());
+ this.loadUpcoming();
} else {
- this.loadCalendar();
+ this.buildMonthList(this.data.currentYear, this.data.currentMonth);
}
wx.stopPullDownRefresh();
},
@@ -46,10 +80,26 @@ Page({
const d = date.getDate();
const dayInfo = getDayInfo(y, m, d);
const almanac = getAlmanacInfo(y, m, d);
- this.setData({ dayInfo, almanac, year: y, month: m });
+ this.setData({ dayInfo, almanac, currentYear: y, currentMonth: m });
+ this.refreshDerived();
+ },
+
+ /** 计算派生数据(节假日/工作日/佛历/初一十五) */
+ refreshDerived() {
+ const { dayInfo, showBuddhist, highlightLunar } = this.data;
+ const { solarYear: y, solarMonth: m, solarDay: d } = dayInfo;
+ const legalHoliday = getLegalHoliday(y, m, d);
+ const workday = isWorkday(y, m, d);
+ const termTime = dayInfo.solarTerm ? (SOLAR_TERM_TIMES[dayInfo.solarTerm] || '') : '';
+ const isFirstOrFifteen = highlightLunar && (dayInfo.lunarDay === 1 || dayInfo.lunarDay === 15);
+ const nextBuddhist = showBuddhist ? getNextBuddhistFestival() : null;
+ this.setData({ legalHoliday, workday, termTime, isFirstOrFifteen, nextBuddhist });
+ },
+
+ loadUpcoming() {
+ this.setData({ upcoming: getUpcomingFestivals(5) });
},
- /** 前一天 / 后一天 */
prevDay() { this.shiftDay(-1); },
nextDay() { this.shiftDay(1); },
shiftDay(delta) {
@@ -58,45 +108,72 @@ Page({
this.loadDay(date);
},
- /** 回到今天 */
backToday() {
this.loadDay(new Date());
+ this.loadUpcoming();
},
- /** 切换 日视图/月历视图 */
+ /** 切换 日/月 视图 */
toggleView() {
const mode = this.data.viewMode === 'day' ? 'month' : 'day';
this.setData({ viewMode: mode });
if (mode === 'month') {
const { dayInfo } = this.data;
- this.setData({ year: dayInfo.solarYear, month: dayInfo.solarMonth }, () => this.loadCalendar());
+ this.buildMonthList(dayInfo.solarYear, dayInfo.solarMonth);
}
},
- /** 月历 */
- loadCalendar() {
- const { year, month } = this.data;
- const weeks = getMonthCalendar(year, month);
- const calendarDays = [];
- weeks.forEach(w => w.forEach(d => calendarDays.push(d)));
- this.setData({ calendarDays });
+ /** 构建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 });
},
- prevMonth() {
- let { year, month } = this.data;
- month--;
- if (month < 1) { month = 12; year--; }
- this.setData({ year, month }, () => this.loadCalendar());
+ /** 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();
+ }
},
- nextMonth() {
- let { year, month } = this.data;
- month++;
- if (month > 12) { month = 1; year++; }
- this.setData({ year, month }, () => this.loadCalendar());
+ 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);
@@ -104,13 +181,57 @@ Page({
this.setData({ viewMode: 'day' });
},
- /** 跳详情页 */
goDetail() {
const { dayInfo } = this.data;
wx.navigateTo({ url: `/pages/day-detail/day-detail?date=${fmt(dayInfo.solarYear, dayInfo.solarMonth, dayInfo.solarDay)}` });
},
+ /** 跳转到某节日的老黄历 */
+ goFestival(e) {
+ const date = e.currentTarget.dataset.date;
+ const [y, m, d] = date.split('-').map(Number);
+ this.loadDay(new Date(y, m - 1, d));
+ this.setData({ viewMode: 'day' });
+ wx.pageScrollTo && wx.pageScrollTo({ scrollTop: 0, duration: 0 });
+ },
+
+ /** ===== 全屏放大弹窗 ===== */
+ openZoom(e) {
+ const type = e.currentTarget.dataset.type;
+ const { dayInfo, almanac, legalHoliday, termTime } = this.data;
+ 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: this.data.upcoming };
+ }
+ this.setData({ zoomVisible: true, zoomTitle, zoomType: type, zoomData });
+ },
+
+ closeZoom() {
+ this.setData({ zoomVisible: false });
+ },
+
navTo(e) {
wx.navigateTo({ url: e.currentTarget.dataset.url });
- }
+ },
+
+ noop() {}
});
diff --git a/mini/pages/home/home.wxml b/mini/pages/home/home.wxml
index df828bc..ee93e63 100644
--- a/mini/pages/home/home.wxml
+++ b/mini/pages/home/home.wxml
@@ -12,24 +12,26 @@
-
-
+
+
+
{{dayInfo.solarYear}}年{{dayInfo.solarMonth}}月
{{dayInfo.weekDay}} · {{dayInfo.constellation}}
-
- 今日
+
+ 今日
+ {{dayInfo.lunarDay === 1 ? '初一' : '十五'}}
-
- ‹
- {{dayInfo.solarDay}}
- ›
+
+ ‹
+ {{dayInfo.solarDay}}
+ ›
@@ -38,14 +40,27 @@
{{dayInfo.lunarYearGanzhi}}年 {{dayInfo.lunarMonthGanzhi}}月 {{dayInfo.lunarDayGanzhi}}日 · 属{{dayInfo.zodiac}}
-
-
- {{dayInfo.lunarFestival}}
- {{dayInfo.solarFestival}}
- {{dayInfo.solarTerm}}
+
+
+ {{legalHoliday}}
+ {{dayInfo.lunarFestival}}
+ {{dayInfo.solarFestival}}
+ {{dayInfo.solarTerm}}{{termTime ? ' ' + termTime : ''}}
-
+
+
+ 🪷 距 {{nextBuddhist.name}} 还有 {{nextBuddhist.daysLeft}} 天({{nextBuddhist.solarDate}})
+
+
+
+
+ {{legalHoliday ? legalHoliday + ' · 放假' : (workday ? '工作日 · 上班' : '周末 · 休息')}}
+
+
+
+
+
宜
@@ -61,13 +76,11 @@
-
-
- 回到今天
+ 点击放大 🔍
-
-
+
+
值神
@@ -94,41 +107,147 @@
{{almanac.pengZu}}
- 查看详情
-
-
-
-
-
-
-
-
-
- ‹
- {{year}}年{{month}}月
- ›
+ 点击放大 🔍
-
-
- {{item}}
-
-
-
-
-
- {{item.solarDay}}
-
- {{item.lunarFestival || item.solarFestival || item.lunarDayName}}
-
-
+
+
+ 节日速查
+
+
+ {{item.name}}
+ {{item.solarMonth}}月{{item.solarDay}}日
+ {{item.daysLeft === 0 ? '今天' : item.daysLeft + '天后'}}
+
+
+
+ 回到今天
+
+
+
+
+
+ {{item}}
+
+
+
+
+
+
+
+
+ {{d.solarDay}}
+
+ {{d.lunarFestival || d.solarFestival || d.lunarDayName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 宜
+
+ {{item}}
+
+
+
+ 忌
+
+ {{item}}
+
+
+
+
+
+
+
+ 节假日
+ {{zoomData.legalHoliday}}
+
+
+ 节气
+ {{zoomData.termTime}}
+
+
+ 农历
+ {{zoomData.lunar}}
+
+
+ 干支
+ {{zoomData.ganzhi}}
+
+
+ 值神
+ {{zoomData.duty}}
+
+
+ 黄道
+ {{zoomData.twelveStar}}
+
+
+ 二十八宿
+ {{zoomData.twentyEightStar}}
+
+
+ 六曜
+ {{zoomData.sixStar}}
+
+
+ 冲煞
+ {{zoomData.clash}}
+
+
+ 彭祖百忌
+ {{zoomData.pengZu}}
+
+
+
+
+
+
+ {{item.name}}
+ {{item.solarDate}}
+ {{item.daysLeft === 0 ? '今天' : '还有' + item.daysLeft + '天'}}
+
+
+
+
diff --git a/mini/pages/home/home.wxss b/mini/pages/home/home.wxss
index ad6f1dd..51e41dd 100644
--- a/mini/pages/home/home.wxss
+++ b/mini/pages/home/home.wxss
@@ -42,7 +42,6 @@
min-width: 96rpx;
}
-/* 页面主体 */
.page-body {
box-sizing: border-box;
min-height: 100vh;
@@ -55,12 +54,30 @@
/* ===== 老黄历大日历卡片 ===== */
.almanac-card {
- background: linear-gradient(160deg, #FFFFFF 0%, #FFF8EF 100%);
- border: 1rpx solid #E8D5C4;
border-radius: 24rpx;
- padding: 32rpx 24rpx;
+ padding: 32rpx 24rpx 24rpx;
margin-bottom: 16rpx;
position: relative;
+ border: 2rpx solid #E8D5C4;
+ background: #FFFFFF;
+}
+
+/* 节假日:喜庆红 */
+.almanac-card.is-holiday {
+ background: linear-gradient(160deg, #FFF5F4 0%, #FFE8E4 100%);
+ border-color: #C41E3A;
+}
+
+/* 工作日:清新绿 */
+.almanac-card.is-workday {
+ background: linear-gradient(160deg, #F4FBF5 0%, #E6F5E9 100%);
+ border-color: #4CAF50;
+}
+
+/* 周末:米黄 */
+.almanac-card.is-weekend {
+ background: linear-gradient(160deg, #FFFBF0 0%, #FFF3DC 100%);
+ border-color: #E0A93E;
}
.almanac-head {
@@ -81,6 +98,11 @@
margin-left: 16rpx;
}
+.head-tags {
+ display: flex;
+ gap: 12rpx;
+}
+
.today-tag {
background: #C41E3A;
color: #FFFFFF;
@@ -89,37 +111,48 @@
border-radius: 999rpx;
}
+.lunar-tag {
+ background: #E0A93E;
+ color: #FFFFFF;
+ font-size: 20rpx;
+ padding: 4rpx 20rpx;
+ border-radius: 999rpx;
+}
+
/* 大日期 */
.almanac-day-row {
display: flex;
align-items: center;
justify-content: center;
- margin: 16rpx 0 8rpx;
+ margin: 8rpx 0;
}
.almanac-day-num {
- font-size: 160rpx;
+ font-size: 240rpx;
font-weight: 700;
- color: #C41E3A;
- line-height: 1.1;
- min-width: 320rpx;
+ line-height: 1.05;
+ min-width: 400rpx;
text-align: center;
}
+.num-holiday { color: #C41E3A; }
+.num-workday { color: #2E7D32; }
+.num-weekend { color: #C41E3A; }
+
.day-arrow {
- font-size: 64rpx;
+ font-size: 72rpx;
color: #C9B8A6;
- padding: 0 32rpx;
+ padding: 0 28rpx;
}
/* 农历 */
.almanac-lunar {
text-align: center;
- margin-bottom: 16rpx;
+ margin-bottom: 12rpx;
}
.almanac-lunar-main {
- font-size: 44rpx;
+ font-size: 48rpx;
font-weight: 600;
color: #1A1A1A;
display: block;
@@ -136,15 +169,72 @@
.almanac-fest {
display: flex;
justify-content: center;
+ flex-wrap: wrap;
gap: 12rpx;
- margin-bottom: 16rpx;
+ margin-bottom: 12rpx;
+}
+
+.badge-holiday {
+ background: #C41E3A;
+ color: #FFFFFF;
+ font-size: 24rpx;
+ padding: 6rpx 24rpx;
+}
+
+.badge-festival {
+ background: #C41E3A;
+ color: #FFFFFF;
+}
+
+.badge-term {
+ background: #2E7D32;
+ color: #FFFFFF;
+}
+
+/* 佛历提醒 */
+.buddhist-tip {
+ text-align: center;
+ padding: 12rpx;
+ margin-bottom: 8rpx;
+}
+
+.buddhist-text {
+ font-size: 26rpx;
+ color: #B8860B;
+}
+
+/* 工作日/节假日提示 */
+.day-type-tip {
+ text-align: center;
+ font-size: 28rpx;
+ font-weight: 600;
+ padding: 12rpx;
+ border-radius: 12rpx;
+ margin-top: 8rpx;
+}
+
+.tip-holiday {
+ color: #C41E3A;
+ background: rgba(196, 30, 58, 0.08);
+}
+
+.tip-workday {
+ color: #2E7D32;
+ background: rgba(46, 125, 50, 0.08);
+}
+
+.tip-weekend {
+ color: #C41E3A;
+ background: rgba(196, 30, 58, 0.08);
}
/* 宜忌 */
+.yiji-card {
+ position: relative;
+}
+
.yiji-row {
display: flex;
- border-top: 1rpx solid #F0E6DA;
- padding-top: 24rpx;
}
.yiji-col {
@@ -171,15 +261,8 @@
flex-shrink: 0;
}
-.yiji-title.yi {
- background: #C41E3A;
- color: #FFFFFF;
-}
-
-.yiji-title.ji {
- background: #546E7A;
- color: #FFFFFF;
-}
+.yiji-title.yi { background: #C41E3A; color: #FFFFFF; }
+.yiji-title.ji { background: #546E7A; color: #FFFFFF; }
.yiji-items {
display: flex;
@@ -189,22 +272,69 @@
}
.yiji-item {
- font-size: 24rpx;
+ font-size: 26rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
}
-.yiji-item.yi {
- background: #FEF2F2;
+.yiji-item.yi { background: #FEF2F2; color: #C41E3A; }
+.yiji-item.ji { background: #F1F5F9; color: #546E7A; }
+
+.zoom-hint {
+ text-align: center;
+ font-size: 20rpx;
+ color: #C9B8A6;
+ margin-top: 12rpx;
+}
+
+.section-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ margin-bottom: 16rpx;
color: #C41E3A;
}
-.yiji-item.ji {
- background: #F1F5F9;
- color: #546E7A;
+/* 节日速查 */
+.fest-quick {
+ display: flex;
+ flex-direction: column;
+ gap: 8rpx;
+}
+
+.fest-quick-item {
+ display: flex;
+ align-items: center;
+ padding: 16rpx;
+ background: #FFFBF5;
+ border-radius: 12rpx;
+ gap: 16rpx;
+}
+
+.fest-name {
+ font-size: 28rpx;
+ font-weight: 600;
+ min-width: 120rpx;
+}
+
+.fest-date {
+ font-size: 24rpx;
+ color: #9E8E7E;
+ flex: 1;
+}
+
+.fest-left {
+ font-size: 24rpx;
+ color: #C41E3A;
+ font-weight: 500;
+}
+
+.fest-left.is-today {
+ background: #C41E3A;
+ color: #FFFFFF;
+ padding: 2rpx 16rpx;
+ border-radius: 999rpx;
}
-/* 回到今天 */
.back-today {
text-align: center;
margin-top: 20rpx;
@@ -213,37 +343,86 @@
padding: 12rpx;
}
-.detail-btn {
+/* ===== 月历视图 ===== */
+.month-body {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ box-sizing: border-box;
+}
+
+.month-header {
text-align: center;
+ padding: 16rpx 0 8rpx;
+}
+
+.month-title {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #C41E3A;
+ display: block;
+}
+
+.month-hint {
+ font-size: 20rpx;
+ color: #C9B8A6;
+}
+
+.week-head {
+ padding: 0 20rpx;
+ margin-bottom: 4rpx;
+}
+
+.week-cell {
+ font-size: 24rpx;
+ color: #9E8E7E;
+ padding: 8rpx 0;
+}
+
+.month-swiper {
+ flex: 1;
+ height: 100%;
+}
+
+.month-scroll {
+ height: 100%;
+}
+
+.month-grid {
+ padding: 0 20rpx;
+ gap: 8rpx;
}
-/* ===== 月历 ===== */
.day-cell {
+ background: transparent;
+}
+
+.day-cell-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
- padding: 12rpx 4rpx;
+ padding: 16rpx 4rpx;
background: #FFFFFF;
border-radius: 12rpx;
- min-height: 100rpx;
+ min-height: 110rpx;
}
-.day-cell.today {
+.day-cell-inner.today {
background: #C41E3A;
color: #FFFFFF;
}
-.day-cell.today .day-lunar {
+.day-cell-inner.today .day-lunar {
color: rgba(255, 255, 255, 0.8);
}
-.day-cell.other-month {
- opacity: 0.4;
+.day-cell-inner.other-month {
+ opacity: 0.35;
}
.day-number {
- font-size: 28rpx;
+ font-size: 30rpx;
font-weight: 500;
}
@@ -265,3 +444,156 @@
background: #C41E3A;
margin-top: 4rpx;
}
+
+/* ===== 全屏放大弹窗 ===== */
+.zoom-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ z-index: 200;
+ display: none;
+}
+
+.zoom-mask.show {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.zoom-panel {
+ width: 92%;
+ max-height: 86%;
+ background: #FFFFFF;
+ border-radius: 24rpx;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.zoom-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24rpx 28rpx;
+ background: #C41E3A;
+}
+
+.zoom-title {
+ color: #FFFFFF;
+ font-size: 36rpx;
+ font-weight: 600;
+}
+
+.zoom-close {
+ color: #FFFFFF;
+ font-size: 44rpx;
+ width: 64rpx;
+ height: 64rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.zoom-body {
+ padding: 32rpx 28rpx;
+ max-height: 70vh;
+}
+
+/* 宜忌放大 */
+.zoom-yiji-block {
+ margin-bottom: 40rpx;
+}
+
+.zoom-yiji-title {
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 52rpx;
+ font-weight: 700;
+ margin-bottom: 24rpx;
+}
+
+.zoom-yiji-title.yi { background: #C41E3A; color: #FFFFFF; }
+.zoom-yiji-title.ji { background: #546E7A; color: #FFFFFF; }
+
+.zoom-yiji-items {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20rpx;
+}
+
+.zoom-yiji-item {
+ font-size: 44rpx;
+ padding: 16rpx 32rpx;
+ border-radius: 16rpx;
+ font-weight: 500;
+}
+
+.zoom-yiji-item.yi { background: #FEF2F2; color: #C41E3A; }
+.zoom-yiji-item.ji { background: #F1F5F9; color: #546E7A; }
+
+/* 详情放大 */
+.zoom-detail-row {
+ display: flex;
+ padding: 24rpx 0;
+ border-bottom: 1rpx solid #F0E6DA;
+ align-items: center;
+}
+
+.zoom-detail-label {
+ width: 200rpx;
+ font-size: 36rpx;
+ color: #9E8E7E;
+ flex-shrink: 0;
+}
+
+.zoom-detail-value {
+ font-size: 40rpx;
+ font-weight: 600;
+ color: #1A1A1A;
+ flex: 1;
+}
+
+.zoom-detail-value.holiday {
+ color: #C41E3A;
+}
+
+/* 节日放大 */
+.zoom-fest-row {
+ display: flex;
+ align-items: center;
+ padding: 28rpx 0;
+ border-bottom: 1rpx solid #F0E6DA;
+ gap: 24rpx;
+}
+
+.zoom-fest-name {
+ font-size: 44rpx;
+ font-weight: 600;
+ min-width: 200rpx;
+}
+
+.zoom-fest-date {
+ font-size: 36rpx;
+ color: #9E8E7E;
+ flex: 1;
+}
+
+.zoom-fest-left {
+ font-size: 36rpx;
+ color: #C41E3A;
+ font-weight: 500;
+}
+
+.zoom-fest-left.is-today {
+ background: #C41E3A;
+ color: #FFFFFF;
+ padding: 4rpx 24rpx;
+ border-radius: 999rpx;
+}
diff --git a/mini/pages/settings/settings.js b/mini/pages/settings/settings.js
index db9e660..6313604 100644
--- a/mini/pages/settings/settings.js
+++ b/mini/pages/settings/settings.js
@@ -4,6 +4,8 @@ Page({
weekStartDay: 0,
ziHourSect: 'lateZiNextDay',
solarTime: true,
+ showBuddhist: false,
+ highlightLunar: true,
bookmarks: []
},
@@ -21,13 +23,15 @@ Page({
this.setData({
weekStartDay: settings.weekStartDay || 0,
ziHourSect: settings.ziHourSect || 'lateZiNextDay',
- solarTime: settings.solarTime !== false
+ solarTime: settings.solarTime !== false,
+ showBuddhist: settings.showBuddhist === true,
+ highlightLunar: settings.highlightLunar !== false
});
},
saveSettings() {
- const { weekStartDay, ziHourSect, solarTime } = this.data;
- wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime });
+ const { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar } = this.data;
+ wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar });
},
loadBookmarks() {
@@ -50,6 +54,16 @@ Page({
this.saveSettings();
},
+ onBuddhistChange(e) {
+ this.setData({ showBuddhist: e.detail.value });
+ this.saveSettings();
+ },
+
+ onHighlightLunarChange(e) {
+ this.setData({ highlightLunar: e.detail.value });
+ this.saveSettings();
+ },
+
removeBookmark(e) {
const date = e.currentTarget.dataset.date;
let bookmarks = this.data.bookmarks.filter(b => b.date !== date);
diff --git a/mini/pages/settings/settings.wxml b/mini/pages/settings/settings.wxml
index ed55d67..393b52d 100644
--- a/mini/pages/settings/settings.wxml
+++ b/mini/pages/settings/settings.wxml
@@ -36,6 +36,16 @@
真太阳时校正
+
+
+ 显示佛历提醒
+
+
+
+
+ 初一十五高亮
+
+
diff --git a/mini/pages/wish-tree/wish-tree.js b/mini/pages/wish-tree/wish-tree.js
index 8c74ae1..ab73773 100644
--- a/mini/pages/wish-tree/wish-tree.js
+++ b/mini/pages/wish-tree/wish-tree.js
@@ -1,61 +1,556 @@
const { request } = require('../../utils/request.js');
+// 丝带颜色配置
+const RIBBON_COLORS = {
+ free: ['#FF6B6B', '#FF8E8E', '#FFB6C1', '#FFC0CB'],
+ paid: ['#FFD700', '#FFA500', '#FF8C00', '#DAA520'],
+};
+
Page({
data: {
- tree: null,
+ trees: [],
+ currentTreeIndex: 0,
wishes: [],
loading: true,
canvasWidth: 375,
- canvasHeight: 600,
+ canvasHeight: 667,
showCreateModal: false,
wishContent: '',
- wishType: 'free', // free: 免费, paid: 付费
+ wishType: 'free',
maxFreeLength: 20,
maxPaidLength: 100,
products: [],
selectedProduct: null,
+ // 弹幕相关
+ danmakuList: [],
+ // 传感器状态
+ gyroEnabled: false,
+ micEnabled: false,
+ windLevel: 0, // 0-1 风力等级
},
onLoad() {
- this.loadWishTree();
+ this.initCanvas();
+ this.loadTrees();
this.loadProducts();
+ this.initSensors();
+ this.startAnimation();
+ },
+
+ onUnload() {
+ this.stopAnimation();
+ this.stopSensors();
},
onPullDownRefresh() {
- this.loadWishTree().then(() => {
+ this.loadTrees().then(() => {
wx.stopPullDownRefresh();
});
},
- // 加载许愿树数据
- async loadWishTree() {
+ // 初始化画布
+ initCanvas() {
+ const systemInfo = wx.getSystemInfoSync();
+ this.setData({
+ canvasWidth: systemInfo.windowWidth,
+ canvasHeight: systemInfo.windowHeight,
+ });
+ },
+
+ // 初始化传感器
+ initSensors() {
+ // 陀螺仪
+ wx.startGyroscope({
+ interval: 'game',
+ success: () => {
+ this.setData({ gyroEnabled: true });
+ wx.onGyroscopeChange((res) => {
+ // 根据陀螺仪数据计算风力
+ const wind = Math.min(1, Math.abs(res.x) * 0.5 + Math.abs(res.y) * 0.3);
+ this.setData({ windLevel: wind });
+ });
+ },
+ fail: () => {
+ console.log('陀螺仪不可用');
+ },
+ });
+
+ // 麦克风(需要用户授权)
+ this.recorderManager = wx.getRecorderManager();
+ this.recorderManager.onFrameRecorded((res) => {
+ // 分析音量,模拟风力
+ const frameBuffer = res.frameBuffer;
+ if (frameBuffer) {
+ const volume = this.analyzeVolume(frameBuffer);
+ this.setData({ windLevel: Math.min(1, volume * 2) });
+ }
+ });
+ },
+
+ // 分析音量
+ analyzeVolume(buffer) {
+ // 简单的音量分析
+ const data = new Int16Array(buffer);
+ let sum = 0;
+ for (let i = 0; i < data.length; i++) {
+ sum += Math.abs(data[i]);
+ }
+ return sum / data.length / 32768;
+ },
+
+ // 开启麦克风
+ enableMicrophone() {
+ wx.authorize({
+ scope: 'scope.record',
+ success: () => {
+ this.recorderManager.start({
+ duration: 600000,
+ sampleRate: 16000,
+ numberOfChannels: 1,
+ encodeBitRate: 48000,
+ format: 'PCM',
+ frameSize: 50,
+ });
+ this.setData({ micEnabled: true });
+ wx.showToast({ title: '吹气让丝带飘动', icon: 'none' });
+ },
+ fail: () => {
+ wx.showToast({ title: '需要麦克风权限', icon: 'none' });
+ },
+ });
+ },
+
+ // 停止传感器
+ stopSensors() {
+ wx.stopGyroscope();
+ if (this.recorderManager) {
+ this.recorderManager.stop();
+ }
+ },
+
+ // 加载许愿树列表
+ async loadTrees() {
try {
const res = await request({
- url: '/api/wish/tree',
+ url: '/api/wish/trees',
method: 'GET',
});
if (res.code === 0) {
this.setData({
- tree: res.data.tree,
- wishes: res.data.wishes || [],
+ trees: res.data.list || [],
loading: false,
});
- this.drawTree();
+ this.loadWishes();
}
} catch (err) {
console.error('加载许愿树失败:', err);
- this.setData({ loading: false });
- // 使用本地模拟数据
+ // 使用模拟数据
this.setData({
- tree: { id: 1, name: '祈福许愿树', maxWishes: 100 },
- wishes: this.getMockWishes(),
+ trees: [
+ { id: 1, name: '祈福树', type: 'pine', maxWishes: 100 },
+ { id: 2, name: '姻缘树', type: 'sakura', maxWishes: 50 },
+ { id: 3, name: '事业树', type: 'bamboo', maxWishes: 80 },
+ ],
loading: false,
});
- this.drawTree();
+ this.loadWishes();
}
},
+ // 加载当前树的许愿
+ async loadWishes() {
+ const { trees, currentTreeIndex } = this.data;
+ if (!trees.length) return;
+
+ const tree = trees[currentTreeIndex];
+ try {
+ const res = await request({
+ url: `/api/wish/tree/${tree.id}/wishes`,
+ method: 'GET',
+ });
+
+ if (res.code === 0) {
+ this.setData({ wishes: res.data.list || [] });
+ this.initRibbons();
+ this.initDanmaku();
+ }
+ } catch (err) {
+ console.error('加载许愿失败:', err);
+ // 使用模拟数据
+ this.setData({
+ wishes: this.getMockWishes(),
+ });
+ this.initRibbons();
+ this.initDanmaku();
+ }
+ },
+
+ // 模拟许愿数据
+ getMockWishes() {
+ return [
+ { id: 1, content: '愿家人平安健康', type: 'paid', createdAt: Date.now() - 86400000 },
+ { id: 2, content: '事业顺利,步步高升', type: 'free', createdAt: Date.now() - 43200000 },
+ { id: 3, content: '心想事成,万事如意', type: 'paid', createdAt: Date.now() - 21600000 },
+ { id: 4, content: '考试通过,金榜题名', type: 'free', createdAt: Date.now() - 10800000 },
+ { id: 5, content: '财源广进,富贵吉祥', type: 'paid', createdAt: Date.now() - 3600000 },
+ { id: 6, content: '身体健康,长命百岁', type: 'free', createdAt: Date.now() - 1800000 },
+ { id: 7, content: '爱情甜蜜,白头偕老', type: 'paid', createdAt: Date.now() - 900000 },
+ { id: 8, content: '出入平安,一帆风顺', type: 'free', createdAt: Date.now() - 300000 },
+ ];
+ },
+
+ // 初始化丝带
+ initRibbons() {
+ const { wishes, canvasWidth, canvasHeight } = this.data;
+ const tree = this.getCurrentTree();
+
+ // 根据树的类型计算丝带挂载点
+ const branches = this.getTreeBranches(tree.type, canvasWidth, canvasHeight);
+
+ this.ribbons = wishes.map((wish, index) => {
+ const branch = branches[index % branches.length];
+ const colors = RIBBON_COLORS[wish.type] || RIBBON_COLORS.free;
+
+ return {
+ id: wish.id,
+ wish: wish,
+ x: branch.x,
+ y: branch.y,
+ length: 60 + Math.random() * 40,
+ color: colors[Math.floor(Math.random() * colors.length)],
+ angle: Math.random() * Math.PI * 2,
+ swingPhase: Math.random() * Math.PI * 2,
+ swingSpeed: 0.02 + Math.random() * 0.02,
+ windEffect: 0.5 + Math.random() * 0.5,
+ };
+ });
+ },
+
+ // 获取树枝位置
+ getTreeBranches(treeType, width, height) {
+ const centerX = width / 2;
+ const baseY = height * 0.75;
+
+ // 根据树类型返回不同的树枝配置
+ const configs = {
+ pine: [
+ { x: centerX - 80, y: baseY - 200 },
+ { x: centerX + 60, y: baseY - 220 },
+ { x: centerX - 40, y: baseY - 280 },
+ { x: centerX + 90, y: baseY - 260 },
+ { x: centerX, y: baseY - 320 },
+ { x: centerX - 100, y: baseY - 150 },
+ { x: centerX + 100, y: baseY - 180 },
+ ],
+ sakura: [
+ { x: centerX - 70, y: baseY - 180 },
+ { x: centerX + 80, y: baseY - 200 },
+ { x: centerX - 30, y: baseY - 250 },
+ { x: centerX + 50, y: baseY - 280 },
+ { x: centerX, y: baseY - 220 },
+ ],
+ bamboo: [
+ { x: centerX - 60, y: baseY - 150 },
+ { x: centerX + 60, y: baseY - 180 },
+ { x: centerX - 30, y: baseY - 220 },
+ { x: centerX + 30, y: baseY - 250 },
+ ],
+ };
+
+ return configs[treeType] || configs.pine;
+ },
+
+ // 初始化弹幕
+ initDanmaku() {
+ const { wishes, canvasWidth } = this.data;
+
+ this.danmaku = wishes.map((wish, index) => ({
+ id: wish.id,
+ content: wish.content,
+ type: wish.type,
+ x: canvasWidth + Math.random() * 200,
+ y: 100 + (index % 5) * 60,
+ speed: 0.5 + Math.random() * 0.5,
+ opacity: 0.7 + Math.random() * 0.3,
+ }));
+ },
+
+ // 开始动画循环
+ startAnimation() {
+ const that = this;
+ this.animationTimer = setInterval(() => {
+ that.draw();
+ that.updateDanmaku();
+ }, 1000 / 60); // 60fps
+ },
+
+ // 停止动画
+ stopAnimation() {
+ if (this.animationTimer) {
+ clearInterval(this.animationTimer);
+ }
+ },
+
+ // 绘制场景
+ draw() {
+ const ctx = wx.createCanvasContext('wishTree', this);
+ const { canvasWidth, canvasHeight, windLevel } = this.data;
+ const tree = this.getCurrentTree();
+
+ // 清空画布
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+
+ // 绘制天空
+ this.drawSky(ctx, canvasWidth, canvasHeight);
+
+ // 绘制云朵
+ this.drawClouds(ctx, canvasWidth, canvasHeight);
+
+ // 绘制地面
+ this.drawGround(ctx, canvasWidth, canvasHeight);
+
+ // 绘制大树
+ this.drawTree(ctx, tree, canvasWidth, canvasHeight);
+
+ // 绘制丝带
+ this.drawRibbons(ctx, windLevel);
+
+ // 绘制弹幕
+ this.drawDanmaku(ctx);
+
+ ctx.draw();
+ },
+
+ // 绘制天空
+ drawSky(ctx, width, height) {
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
+ gradient.addColorStop(0, '#1a1a2e');
+ gradient.addColorStop(0.5, '#16213e');
+ gradient.addColorStop(1, '#0f3460');
+
+ ctx.setFillStyle(gradient);
+ ctx.fillRect(0, 0, width, height);
+
+ // 绘制星星
+ ctx.setFillStyle('#ffffff');
+ for (let i = 0; i < 50; i++) {
+ const x = Math.random() * width;
+ const y = Math.random() * height * 0.6;
+ const size = Math.random() * 2;
+ const opacity = 0.3 + Math.random() * 0.7;
+ ctx.setGlobalAlpha(opacity);
+ ctx.fillRect(x, y, size, size);
+ }
+ ctx.setGlobalAlpha(1);
+ },
+
+ // 绘制云朵
+ drawClouds(ctx, width, height) {
+ const time = Date.now() / 1000;
+
+ ctx.setFillStyle('rgba(255, 255, 255, 0.1)');
+
+ for (let i = 0; i < 3; i++) {
+ const x = ((time * 10 + i * 200) % (width + 200)) - 100;
+ const y = 50 + i * 80;
+
+ ctx.beginPath();
+ ctx.arc(x, y, 30, 0, Math.PI * 2);
+ ctx.arc(x + 25, y - 10, 25, 0, Math.PI * 2);
+ ctx.arc(x + 50, y, 30, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ },
+
+ // 绘制地面
+ drawGround(ctx, width, height) {
+ const groundY = height * 0.75;
+
+ // 草地
+ const gradient = ctx.createLinearGradient(0, groundY, 0, height);
+ gradient.addColorStop(0, '#2d5016');
+ gradient.addColorStop(1, '#1a3009');
+
+ ctx.setFillStyle(gradient);
+ ctx.fillRect(0, groundY, width, height - groundY);
+
+ // 草丛
+ ctx.setStrokeStyle('#3a6b1f');
+ ctx.setLineWidth(2);
+ for (let i = 0; i < 20; i++) {
+ const x = (i * 37) % width;
+ const h = 10 + (i % 3) * 5;
+ ctx.beginPath();
+ ctx.moveTo(x, groundY);
+ ctx.quadraticCurveTo(x + 5, groundY - h, x + 10, groundY);
+ ctx.stroke();
+ }
+ },
+
+ // 绘制大树
+ drawTree(ctx, tree, width, height) {
+ const centerX = width / 2;
+ const baseY = height * 0.75;
+
+ // 树干
+ ctx.setFillStyle('#4a3728');
+ ctx.beginPath();
+ ctx.moveTo(centerX - 25, baseY);
+ ctx.lineTo(centerX - 15, baseY - 150);
+ ctx.lineTo(centerX + 15, baseY - 150);
+ ctx.lineTo(centerX + 25, baseY);
+ ctx.closePath();
+ ctx.fill();
+
+ // 树冠 - 多层
+ const layers = [
+ { radius: 120, y: baseY - 180, color: '#1e4d2b' },
+ { radius: 100, y: baseY - 220, color: '#2d6a3e' },
+ { radius: 80, y: baseY - 260, color: '#3d8b4f' },
+ { radius: 60, y: baseY - 300, color: '#4dac61' },
+ ];
+
+ layers.forEach(layer => {
+ ctx.setFillStyle(layer.color);
+ ctx.beginPath();
+ ctx.arc(centerX, layer.y, layer.radius, 0, Math.PI * 2);
+ ctx.fill();
+ });
+
+ // 装饰果实
+ ctx.setFillStyle('#ffd700');
+ for (let i = 0; i < 8; i++) {
+ const angle = (i / 8) * Math.PI * 2;
+ const r = 60 + Math.random() * 40;
+ const x = centerX + Math.cos(angle) * r;
+ const y = baseY - 220 + Math.sin(angle) * r * 0.6;
+
+ ctx.beginPath();
+ ctx.arc(x, y, 6, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ },
+
+ // 绘制丝带
+ drawRibbons(ctx, windLevel) {
+ if (!this.ribbons) return;
+
+ const time = Date.now() / 1000;
+
+ this.ribbons.forEach(ribbon => {
+ // 计算摆动
+ const baseSwing = Math.sin(time * ribbon.swingSpeed + ribbon.swingPhase) * 0.3;
+ const windSwing = Math.sin(time * 3 + ribbon.swingPhase) * windLevel * ribbon.windEffect;
+ const totalSwing = baseSwing + windSwing;
+
+ // 丝带分段绘制,实现飘动效果
+ const segments = 8;
+ const segmentLength = ribbon.length / segments;
+
+ ctx.setStrokeStyle(ribbon.color);
+ ctx.setLineWidth(4);
+ ctx.setLineCap('round');
+
+ let prevX = ribbon.x;
+ let prevY = ribbon.y;
+
+ for (let i = 1; i <= segments; i++) {
+ const t = i / segments;
+ const wave = Math.sin(time * 2 + ribbon.swingPhase + t * 3) * (5 + windLevel * 15) * t;
+ const x = ribbon.x + totalSwing * i * segmentLength * 0.3 + wave;
+ const y = ribbon.y + i * segmentLength;
+
+ ctx.beginPath();
+ ctx.moveTo(prevX, prevY);
+ ctx.lineTo(x, y);
+ ctx.stroke();
+
+ prevX = x;
+ prevY = y;
+ }
+
+ // 丝带末端装饰
+ ctx.setFillStyle(ribbon.color);
+ ctx.beginPath();
+ ctx.arc(prevX, prevY, 4, 0, Math.PI * 2);
+ ctx.fill();
+ });
+ },
+
+ // 绘制弹幕
+ drawDanmaku(ctx) {
+ if (!this.danmaku) return;
+
+ this.danmaku.forEach(item => {
+ // 弹幕背景 - 手动绘制圆角矩形
+ const padding = 10;
+ const textWidth = item.content.length * 14;
+ const rectX = item.x - padding;
+ const rectY = item.y - 15;
+ const rectW = textWidth + padding * 2;
+ const rectH = 30;
+ const radius = 15;
+
+ ctx.setFillStyle(item.type === 'paid' ? 'rgba(255, 215, 0, 0.8)' : 'rgba(255, 107, 107, 0.8)');
+ ctx.beginPath();
+ ctx.moveTo(rectX + radius, rectY);
+ ctx.lineTo(rectX + rectW - radius, rectY);
+ ctx.arc(rectX + rectW - radius, rectY + radius, radius, -Math.PI / 2, 0);
+ ctx.lineTo(rectX + rectW, rectY + rectH - radius);
+ ctx.arc(rectX + rectW - radius, rectY + rectH - radius, radius, 0, Math.PI / 2);
+ ctx.lineTo(rectX + radius, rectY + rectH);
+ ctx.arc(rectX + radius, rectY + rectH - radius, radius, Math.PI / 2, Math.PI);
+ ctx.lineTo(rectX, rectY + radius);
+ ctx.arc(rectX + radius, rectY + radius, radius, Math.PI, Math.PI * 1.5);
+ ctx.closePath();
+ ctx.fill();
+
+ // 弹幕文字
+ ctx.setFillStyle('#ffffff');
+ ctx.setFontSize(14);
+ ctx.setTextAlign('left');
+ ctx.setTextBaseline('middle');
+ ctx.fillText(item.content, item.x, item.y);
+ });
+ },
+
+ // 更新弹幕位置
+ updateDanmaku() {
+ const { canvasWidth } = this.data;
+
+ if (!this.danmaku) return;
+
+ this.danmaku.forEach(item => {
+ item.x -= item.speed;
+ if (item.x < -200) {
+ item.x = canvasWidth + Math.random() * 100;
+ }
+ });
+ },
+
+ // 获取当前树
+ getCurrentTree() {
+ const { trees, currentTreeIndex } = this.data;
+ return trees[currentTreeIndex] || { type: 'pine' };
+ },
+
+ // 切换树
+ switchTree(e) {
+ const direction = e.currentTarget.dataset.direction;
+ const { trees, currentTreeIndex } = this.data;
+
+ let newIndex = currentTreeIndex;
+ if (direction === 'prev') {
+ newIndex = (currentTreeIndex - 1 + trees.length) % trees.length;
+ } else {
+ newIndex = (currentTreeIndex + 1) % trees.length;
+ }
+
+ this.setData({ currentTreeIndex: newIndex });
+ this.loadWishes();
+ },
+
// 加载许愿商品
async loadProducts() {
try {
@@ -69,73 +564,16 @@ Page({
}
} catch (err) {
console.error('加载商品失败:', err);
- // 使用默认商品
this.setData({
products: [
- { id: 1, name: '普通许愿条', price: 100, duration: 7, position: 0 },
- { id: 2, name: '精品许愿条', price: 500, duration: 30, position: 10 },
- { id: 3, name: '至尊许愿条', price: 2000, duration: 90, position: 100 },
+ { id: 1, name: '普通许愿条', price: 100, duration: 7 },
+ { id: 2, name: '精品许愿条', price: 500, duration: 30 },
+ { id: 3, name: '至尊许愿条', price: 2000, duration: 90 },
],
});
}
},
- // 模拟许愿数据
- getMockWishes() {
- return [
- { id: 1, content: '愿家人平安健康', type: 'paid', position: 10, isRobot: false },
- { id: 2, content: '事业顺利', type: 'free', position: 5, isRobot: false },
- { id: 3, content: '心想事成', type: 'paid', position: 20, isRobot: true },
- { id: 4, content: '考试通过', type: 'free', position: 3, isRobot: false },
- { id: 5, content: '财源广进', type: 'paid', position: 15, isRobot: false },
- ];
- },
-
- // 绘制许愿树
- drawTree() {
- const ctx = wx.createCanvasContext('wishTree', this);
- const { canvasWidth, canvasHeight, wishes } = this.data;
-
- // 清空画布
- ctx.clearRect(0, 0, canvasWidth, canvasHeight);
-
- // 绘制树干
- ctx.setFillStyle('#8B4513');
- ctx.fillRect(canvasWidth / 2 - 20, canvasHeight - 150, 40, 150);
-
- // 绘制树冠(圆形)
- ctx.setFillStyle('#228B22');
- ctx.beginPath();
- ctx.arc(canvasWidth / 2, canvasHeight - 200, 120, 0, 2 * Math.PI);
- ctx.fill();
-
- // 绘制许愿条
- wishes.forEach((wish, index) => {
- const angle = (index / wishes.length) * 2 * Math.PI;
- const radius = 80 + Math.random() * 40;
- const x = canvasWidth / 2 + radius * Math.cos(angle);
- const y = canvasHeight - 200 + radius * Math.sin(angle);
-
- // 根据类型设置颜色
- if (wish.type === 'paid') {
- ctx.setFillStyle('#FFD700'); // 金色 - 付费
- } else {
- ctx.setFillStyle('#FF6B6B'); // 红色 - 免费
- }
-
- // 绘制许愿条(小矩形)
- ctx.fillRect(x - 15, y - 5, 30, 10);
-
- // 绘制文字
- ctx.setFillStyle('#FFFFFF');
- ctx.setFontSize(8);
- ctx.setTextAlign('center');
- ctx.fillText(wish.content.substring(0, 4), x, y + 3);
- });
-
- ctx.draw();
- },
-
// 打开许愿弹窗
openCreateModal() {
this.setData({ showCreateModal: true });
@@ -171,28 +609,25 @@ Page({
// 提交许愿
async submitWish() {
- const { wishContent, wishType, selectedProduct } = this.data;
+ const { wishContent, wishType, selectedProduct, trees, currentTreeIndex } = this.data;
if (!wishContent.trim()) {
wx.showToast({ title: '请输入许愿内容', icon: 'none' });
return;
}
- // 检查字数限制
const maxLength = wishType === 'free' ? this.data.maxFreeLength : this.data.maxPaidLength;
if (wishContent.length > maxLength) {
wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' });
return;
}
- // 付费许愿需要选择商品
if (wishType === 'paid' && !selectedProduct) {
wx.showToast({ title: '请选择许愿商品', icon: 'none' });
return;
}
try {
- // 如果是付费许愿,先创建订单
if (wishType === 'paid') {
const orderRes = await request({
url: '/api/pay/create',
@@ -201,29 +636,28 @@ Page({
type: 'wish',
productId: selectedProduct.id,
content: wishContent,
+ treeId: trees[currentTreeIndex].id,
},
});
if (orderRes.code === 0) {
- // 调起微信支付
await this.wxPay(orderRes.data);
}
} else {
- // 免费许愿直接创建
const res = await request({
url: '/api/wish/create',
method: 'POST',
data: {
content: wishContent,
type: 'free',
- treeId: this.data.tree.id,
+ treeId: trees[currentTreeIndex].id,
},
});
if (res.code === 0) {
wx.showToast({ title: '许愿成功', icon: 'success' });
this.closeCreateModal();
- this.loadWishTree();
+ this.loadWishes();
}
}
} catch (err) {
@@ -244,7 +678,7 @@ Page({
success: () => {
wx.showToast({ title: '支付成功', icon: 'success' });
this.closeCreateModal();
- this.loadWishTree();
+ this.loadWishes();
resolve();
},
fail: (err) => {
@@ -256,6 +690,28 @@ Page({
});
},
+ // 画布触摸事件
+ onCanvasTouch(e) {
+ const { x, y } = e.touches[0];
+
+ // 检测是否点击到丝带
+ if (this.ribbons) {
+ for (let ribbon of this.ribbons) {
+ const dx = x - ribbon.x;
+ const dy = y - ribbon.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < 50) {
+ // 点击到丝带,显示详情
+ wx.navigateTo({
+ url: `/pages/wish-detail/wish-detail?id=${ribbon.id}`,
+ });
+ return;
+ }
+ }
+ }
+ },
+
// 查看许愿详情
viewWishDetail(e) {
const id = e.currentTarget.dataset.id;
@@ -266,8 +722,9 @@ Page({
// 分享
onShareAppMessage() {
+ const tree = this.getCurrentTree();
return {
- title: '快来许愿树许下你的愿望吧!',
+ title: `快来${tree.name || '许愿树'}许下你的愿望吧!`,
path: '/pages/wish-tree/wish-tree',
};
},
diff --git a/mini/pages/wish-tree/wish-tree.wxml b/mini/pages/wish-tree/wish-tree.wxml
index 1f989c8..55510bd 100644
--- a/mini/pages/wish-tree/wish-tree.wxml
+++ b/mini/pages/wish-tree/wish-tree.wxml
@@ -1,48 +1,40 @@
-
-
-
+
+
+
+
+
+
+
+ ‹
+
+ {{trees[currentTreeIndex].name || '许愿树'}}
+
+ ›
+
+
-
-
-
- {{item.content}}
- VIP
+
+
+
+ 🎤
-
-
-
-
- 🎋
- 免费许愿:20字以内,可能被覆盖
-
-
- ✨
- 付费许愿:100字以内,优先展示,长期保留
-
-
-
diff --git a/mini/pages/wish-tree/wish-tree.wxss b/mini/pages/wish-tree/wish-tree.wxss
index 1a3e1c7..d7aad92 100644
--- a/mini/pages/wish-tree/wish-tree.wxss
+++ b/mini/pages/wish-tree/wish-tree.wxss
@@ -1,127 +1,121 @@
.container {
- min-height: 100vh;
- background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 100%);
- padding-bottom: 120rpx;
-}
-
-/* 画布容器 */
-.canvas-container {
position: relative;
- width: 100%;
- height: 800rpx;
+ width: 100vw;
+ height: 100vh;
overflow: hidden;
+ background: #0f3460;
}
+/* 全屏画布 */
.wish-tree-canvas {
- width: 100%;
- height: 100%;
-}
-
-/* 许愿条覆盖层 */
-.wishes-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
- pointer-events: none;
}
-.wish-tag {
+/* 顶部导航 */
+.top-bar {
position: absolute;
- padding: 8rpx 16rpx;
- border-radius: 20rpx;
- font-size: 20rpx;
- color: #fff;
- pointer-events: auto;
- transform: translate(-50%, -50%);
- box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2);
- max-width: 200rpx;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.wish-tag.free {
- background: linear-gradient(135deg, #FF6B6B, #FF8E8E);
-}
-
-.wish-tag.paid {
- background: linear-gradient(135deg, #FFD700, #FFA500);
-}
-
-.wish-tag.robot {
- border: 2rpx dashed #fff;
-}
-
-.wish-text {
- font-size: 20rpx;
-}
-
-.wish-badge {
- position: absolute;
- top: -8rpx;
- right: -8rpx;
- background: #FF4500;
- color: #fff;
- font-size: 16rpx;
- padding: 2rpx 8rpx;
- border-radius: 10rpx;
-}
-
-/* 操作栏 */
-.action-bar {
- position: fixed;
- bottom: 0;
+ top: 0;
left: 0;
right: 0;
- padding: 20rpx 30rpx;
- background: rgba(255,255,255,0.95);
- box-shadow: 0 -2rpx 20rpx rgba(0,0,0,0.1);
+ padding: 80rpx 30rpx 20rpx;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ z-index: 100;
+}
+
+.tree-switcher {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ background: rgba(0, 0, 0, 0.3);
+ padding: 10rpx 20rpx;
+ border-radius: 40rpx;
+ backdrop-filter: blur(10px);
+}
+
+.switch-btn {
+ width: 60rpx;
+ height: 60rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+}
+
+.switch-icon {
+ font-size: 40rpx;
+ color: #fff;
+ font-weight: bold;
+}
+
+.tree-name {
+ font-size: 32rpx;
+ color: #fff;
+ font-weight: bold;
+ min-width: 150rpx;
+ text-align: center;
+}
+
+/* 传感器控制 */
+.sensor-controls {
+ display: flex;
+ gap: 16rpx;
+}
+
+.sensor-btn {
+ width: 70rpx;
+ height: 70rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 50%;
+ backdrop-filter: blur(10px);
+ transition: all 0.3s;
+}
+
+.sensor-btn.active {
+ background: rgba(255, 215, 0, 0.5);
+ box-shadow: 0 0 20rpx rgba(255, 215, 0, 0.5);
+}
+
+.sensor-icon {
+ font-size: 36rpx;
+}
+
+/* 许愿按钮 */
+.action-bar {
+ position: absolute;
+ bottom: 60rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 100;
}
.btn-wish {
display: flex;
align-items: center;
justify-content: center;
- gap: 10rpx;
+ gap: 12rpx;
background: linear-gradient(135deg, #C41E3A, #E74C3C);
color: #fff;
font-size: 32rpx;
- padding: 24rpx;
+ padding: 24rpx 60rpx;
border-radius: 50rpx;
border: none;
+ box-shadow: 0 8rpx 30rpx rgba(196, 30, 58, 0.4);
}
.btn-wish .icon {
font-size: 36rpx;
}
-/* 提示区域 */
-.tips-section {
- padding: 30rpx;
- margin-top: 20rpx;
-}
-
-.tip-item {
- display: flex;
- align-items: center;
- gap: 16rpx;
- padding: 20rpx;
- background: rgba(255,255,255,0.8);
- border-radius: 16rpx;
- margin-bottom: 16rpx;
-}
-
-.tip-icon {
- font-size: 36rpx;
-}
-
-.tip-text {
- font-size: 26rpx;
- color: #666;
-}
-
/* 弹窗 */
.modal {
position: fixed;
@@ -138,7 +132,8 @@
left: 0;
right: 0;
bottom: 0;
- background: rgba(0,0,0,0.5);
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(5px);
}
.modal-content {
@@ -191,6 +186,7 @@
border: 2rpx solid #eee;
border-radius: 16rpx;
text-align: center;
+ transition: all 0.3s;
}
.type-item.active {
@@ -254,6 +250,7 @@
padding: 24rpx;
border: 2rpx solid #eee;
border-radius: 16rpx;
+ transition: all 0.3s;
}
.product-item.active {
diff --git a/mini/utils/core/data.js b/mini/utils/core/data.js
index 5bf41c5..545fe31 100644
--- a/mini/utils/core/data.js
+++ b/mini/utils/core/data.js
@@ -101,6 +101,54 @@ const BUDDHIST_FESTIVALS = {
'11-17': '阿弥陀佛圣诞', '12-8': '释迦牟尼佛成道日(腊八)', '12-29': '华严菩萨圣诞'
};
+/** 法定节假日(公历) */
+const LEGAL_HOLIDAYS_SOLAR = {
+ '1-1': '元旦', '5-1': '劳动节', '10-1': '国庆节'
+};
+
+/** 法定节假日(农历) */
+const LEGAL_HOLIDAYS_LUNAR = {
+ '1-1': '春节', '5-5': '端午节', '8-15': '中秋节', '12-30': '除夕'
+};
+
+/** 二十四节气大致时刻(用于显示,小时:分钟) */
+const SOLAR_TERM_TIMES = {
+ '立春': '04:01', '雨水': '23:51', '惊蛰': '22:07', '春分': '23:06',
+ '清明': '03:20', '谷雨': '10:24', '立夏': '20:26', '小满': '09:23',
+ '芒种': '00:26', '夏至': '17:14', '小暑': '10:38', '大暑': '04:07',
+ '立秋': '20:29', '处暑': '11:16', '白露': '23:32', '秋分': '09:04',
+ '寒露': '15:22', '霜降': '18:36', '立冬': '18:45', '小雪': '16:20',
+ '大雪': '11:46', '冬至': '05:48', '小寒': '23:05', '大寒': '16:30'
+};
+
+/** 常用节日速查(名称 → 类型+月日) */
+const FESTIVAL_QUICK_SEARCH = [
+ { name: '春节', type: 'lunar', date: '1-1' },
+ { name: '元宵节', type: 'lunar', date: '1-15' },
+ { name: '龙抬头', type: 'lunar', date: '2-2' },
+ { name: '端午节', type: 'lunar', date: '5-5' },
+ { name: '七夕', type: 'lunar', date: '7-7' },
+ { name: '中元节', type: 'lunar', date: '7-15' },
+ { name: '中秋节', type: 'lunar', date: '8-15' },
+ { name: '重阳节', type: 'lunar', date: '9-9' },
+ { name: '腊八节', type: 'lunar', date: '12-8' },
+ { name: '小年', type: 'lunar', date: '12-23' },
+ { name: '除夕', type: 'lunar', date: '12-30' },
+ { name: '元旦', type: 'solar', date: '1-1' },
+ { name: '情人节', type: 'solar', date: '2-14' },
+ { name: '妇女节', type: 'solar', date: '3-8' },
+ { name: '植树节', type: 'solar', date: '3-12' },
+ { name: '劳动节', type: 'solar', date: '5-1' },
+ { name: '青年节', type: 'solar', date: '5-4' },
+ { name: '儿童节', type: 'solar', date: '6-1' },
+ { name: '建党节', type: 'solar', date: '7-1' },
+ { name: '建军节', type: 'solar', date: '8-1' },
+ { name: '教师节', type: 'solar', date: '9-10' },
+ { name: '国庆节', type: 'solar', date: '10-1' },
+ { name: '平安夜', type: 'solar', date: '12-24' },
+ { name: '圣诞节', type: 'solar', date: '12-25' }
+];
+
/** 纳音表 */
const NAYIN_TABLE = [
'海中金', '炉中火', '大林木', '路旁土', '剑锋金', '山头火',
@@ -370,6 +418,10 @@ module.exports = {
SOLAR_FESTIVALS,
LUNAR_FESTIVALS,
BUDDHIST_FESTIVALS,
+ LEGAL_HOLIDAYS_SOLAR,
+ LEGAL_HOLIDAYS_LUNAR,
+ SOLAR_TERM_TIMES,
+ FESTIVAL_QUICK_SEARCH,
NAYIN_TABLE,
HOUR_NAMES,
HOUR_RANGES,
diff --git a/mini/utils/core/index.js b/mini/utils/core/index.js
index 926c110..3f9fb07 100644
--- a/mini/utils/core/index.js
+++ b/mini/utils/core/index.js
@@ -13,6 +13,10 @@ const {
SOLAR_FESTIVALS,
LUNAR_FESTIVALS,
BUDDHIST_FESTIVALS,
+ LEGAL_HOLIDAYS_SOLAR,
+ LEGAL_HOLIDAYS_LUNAR,
+ SOLAR_TERM_TIMES,
+ FESTIVAL_QUICK_SEARCH,
NAYIN_TABLE,
HOUR_NAMES,
HOUR_RANGES,
@@ -84,124 +88,107 @@ function getLunarMonthDays(year, month) {
return (LUNAR_INFO[year - 1900] & (0x10000 >> month)) ? 30 : 29;
}
-/** 公历转农历 */
+/** 公历转农历(标准算法,基准 1900-01-31 = 农历1900年正月初一) */
function solarToLunar(year, month, day) {
if (year < 1900 || year > 2100) return null;
- let offset = 0;
- for (let y = 1900; y < year; y++) {
- offset += getLunarYearDays(y);
+ // 与基准日 1900-01-31(农历1900年正月初一)的天数差(用 UTC 避免时区误差)
+ const baseDate = Date.UTC(1900, 0, 31);
+ const objDate = Date.UTC(year, month - 1, day);
+ let offset = Math.floor((objDate - baseDate) / 86400000);
+
+ let lunarYear = 1900;
+ let daysOfYear = 0;
+
+ // 确定农历年
+ for (; lunarYear < 2101 && offset > 0; lunarYear++) {
+ daysOfYear = getLunarYearDays(lunarYear);
+ if (offset < daysOfYear) break;
+ offset -= daysOfYear;
}
- let leapMonth = getLeapMonth(year);
+ const leapMonth = getLeapMonth(lunarYear);
let isLeap = false;
let lunarMonth = 1;
- let lunarDay = 1;
- let daysInMonth = 0;
+ let daysOfMonth = 0;
- // 计算从正月初一到目标日期的天数
- let targetOffset = offset;
- for (let m = 1; m < month; m++) {
- targetOffset += getSolarMonthDays(year, m);
- }
- targetOffset += day - 1;
-
- // 农历年剩余天数
- let daysInLunarYear = getLunarYearDays(year);
- let remaining = targetOffset - offset;
-
- if (remaining < 0 || remaining >= daysInLunarYear) {
- // 需要调整年份
- if (remaining < 0) {
- year--;
- remaining += getLunarYearDays(year);
- } else {
- remaining -= daysInLunarYear;
- year++;
- }
- leapMonth = getLeapMonth(year);
- }
-
- // 逐月计算
- for (let m = 1; m <= 12; m++) {
- daysInMonth = getLunarMonthDays(year, m);
- if (remaining < daysInMonth) {
- lunarMonth = m;
- lunarDay = remaining + 1;
- break;
- }
- remaining -= daysInMonth;
-
- // 处理闰月
- if (m === leapMonth && !isLeap) {
- m--;
+ // 确定农历月日
+ for (; lunarMonth < 13 && offset > 0; lunarMonth++) {
+ // 闰月
+ if (leapMonth > 0 && lunarMonth === leapMonth + 1 && !isLeap) {
+ lunarMonth--;
isLeap = true;
- daysInMonth = getLeapMonthDays(year);
- if (remaining < daysInMonth) {
- lunarMonth = m + 1;
- lunarDay = remaining + 1;
- isLeap = true;
- break;
- }
- remaining -= daysInMonth;
+ daysOfMonth = getLeapMonthDays(lunarYear);
+ } else {
+ daysOfMonth = getLunarMonthDays(lunarYear, lunarMonth);
}
+
+ if (offset < daysOfMonth) break;
+ offset -= daysOfMonth;
+
+ // 解除闰月标记
+ if (isLeap && lunarMonth === leapMonth + 1) isLeap = false;
+ }
+
+ // offset 为 0 且恰好是闰月边界
+ if (offset === 0 && leapMonth > 0 && lunarMonth === leapMonth + 1) {
+ if (isLeap) {
+ isLeap = false;
+ } else {
+ isLeap = true;
+ lunarMonth--;
+ }
+ }
+
+ if (offset < 0) {
+ offset += daysOfMonth;
+ lunarMonth--;
}
return {
- year,
+ year: lunarYear,
month: lunarMonth,
- day: lunarDay,
- isLeap: isLeap && lunarMonth === leapMonth
+ day: offset + 1,
+ isLeap
};
}
-/** 农历转公历 */
+/** 农历转公历(标准算法,基准 1900-01-31 = 农历1900年正月初一) */
function lunarToSolar(year, month, day, isLeap = false) {
if (year < 1900 || year > 2100) return null;
+ // 累计从 1900 年正月初一到目标农历日期的天数
let offset = 0;
for (let y = 1900; y < year; y++) {
offset += getLunarYearDays(y);
}
const leapMonth = getLeapMonth(year);
- let days = 0;
+ // 累计当年 1 月到目标月前一月的 days
for (let m = 1; m < month; m++) {
- days += getLunarMonthDays(year, m);
- if (m === leapMonth && !isLeap) {
- days += getLeapMonthDays(year);
+ offset += getLunarMonthDays(year, m);
+ // 经过闰月(在 m 月之后)
+ if (m === leapMonth) {
+ offset += getLeapMonthDays(year);
}
}
+ // 目标月本身是闰月:需先加上该月正常月天数
if (isLeap && month === leapMonth) {
- days += getLunarMonthDays(year, month);
+ offset += getLunarMonthDays(year, month);
}
- days += day - 1;
- offset += days;
+ offset += day - 1;
- // 从1900年1月31日(农历正月初一)开始计算
- let solarYear = 1900;
- let solarMonth = 1;
- let solarDay = 31;
-
- let remaining = offset;
- while (remaining > 0) {
- const daysInMonth = getSolarMonthDays(solarYear, solarMonth);
- if (remaining < daysInMonth) {
- solarDay = remaining + 1;
- break;
- }
- remaining -= daysInMonth;
- solarMonth++;
- if (solarMonth > 12) {
- solarMonth = 1;
- solarYear++;
- }
- }
-
- return { year: solarYear, month: solarMonth, day: solarDay };
+ // 基准日 1900-01-31 + offset
+ const baseMs = Date.UTC(1900, 0, 31);
+ const target = new Date(baseMs + offset * 86400000);
+ return {
+ year: target.getUTCFullYear(),
+ month: target.getUTCMonth() + 1,
+ day: target.getUTCDate()
+ };
}
/** 获取日期的干支 */
@@ -1109,6 +1096,124 @@ function getBuddhistFestival(lunarMonth, lunarDay) {
return BUDDHIST_FESTIVALS[`${lunarMonth}-${lunarDay}`] || null;
}
+/** 判断是否为法定节假日 */
+function getLegalHoliday(year, month, day) {
+ const solarKey = `${month}-${day}`;
+ if (LEGAL_HOLIDAYS_SOLAR[solarKey]) return LEGAL_HOLIDAYS_SOLAR[solarKey];
+ const lunar = solarToLunar(year, month, day);
+ if (lunar) {
+ const lunarKey = `${lunar.month}-${lunar.day}`;
+ if (LEGAL_HOLIDAYS_LUNAR[lunarKey]) return LEGAL_HOLIDAYS_LUNAR[lunarKey];
+ }
+ return null;
+}
+
+/** 计算某天是星期几(0=周日) */
+function getWeekDay(year, month, day) {
+ return new Date(year, month - 1, day).getDay();
+}
+
+/** 判断是否为工作日(周一到周五且非法定节假日) */
+function isWorkday(year, month, day) {
+ if (getLegalHoliday(year, month, day)) return false;
+ const wd = getWeekDay(year, month, day);
+ return wd >= 1 && wd <= 5;
+}
+
+/** 农历转公历(某年) */
+function lunarFestivalToSolar(year, lunarMonth, lunarDay) {
+ // 除夕特殊:腊月最后一天
+ if (lunarMonth === 12 && lunarDay === 30) {
+ const daysInMonth = getLunarMonthDays(year, 12);
+ const leap = getLeapMonth(year);
+ if (leap === 12) {
+ return lunarToSolar(year, 12, daysInMonth, true);
+ }
+ return lunarToSolar(year, 12, daysInMonth, false);
+ }
+ return lunarToSolar(year, lunarMonth, lunarDay, false);
+}
+
+/** 计算某农历节日在公历 year 年的日期 */
+function getFestivalSolarDate(year, fest) {
+ if (fest.type === 'solar') {
+ const [m, d] = fest.date.split('-').map(Number);
+ return { year, month: m, day: d };
+ }
+ const [lm, ld] = fest.date.split('-').map(Number);
+ // 农历节日可能落在上一公历年(如春节在1-2月)
+ // 先按当前公历年算
+ let solar = lunarFestivalToSolar(year, lm, ld);
+ if (!solar) return null;
+ return solar;
+}
+
+/** 计算两个日期相差天数(target - from) */
+function daysBetween(fromY, fromM, fromD, toY, toM, toD) {
+ const a = new Date(fromY, fromM - 1, fromD);
+ const b = new Date(toY, toM - 1, toD);
+ return Math.round((b - a) / (24 * 60 * 60 * 1000));
+}
+
+/** 获取即将到来的 N 个节日(含今天之后的) */
+function getUpcomingFestivals(count = 6) {
+ const now = new Date();
+ const y = now.getFullYear();
+ const m = now.getMonth() + 1;
+ const d = now.getDate();
+ const list = [];
+
+ for (const fest of FESTIVAL_QUICK_SEARCH) {
+ // 尝试今年和明年
+ for (const tryYear of [y, y + 1]) {
+ const solar = getFestivalSolarDate(tryYear, fest);
+ if (!solar) continue;
+ const diff = daysBetween(y, m, d, solar.year, solar.month, solar.day);
+ if (diff >= 0) {
+ list.push({
+ name: fest.name,
+ solarDate: `${solar.year}-${String(solar.month).padStart(2, '0')}-${String(solar.day).padStart(2, '0')}`,
+ solarYear: solar.year,
+ solarMonth: solar.month,
+ solarDay: solar.day,
+ daysLeft: diff
+ });
+ break;
+ }
+ }
+ }
+
+ list.sort((a, b) => a.daysLeft - b.daysLeft);
+ return list.slice(0, count);
+}
+
+/** 获取下一个佛教节日及倒计时 */
+function getNextBuddhistFestival() {
+ const now = new Date();
+ const y = now.getFullYear();
+ const m = now.getMonth() + 1;
+ const d = now.getDate();
+
+ const entries = Object.entries(BUDDHIST_FESTIVALS);
+ const list = [];
+
+ for (const [key, name] of entries) {
+ const [lm, ld] = key.split('-').map(Number);
+ for (const tryYear of [y, y + 1]) {
+ const solar = lunarFestivalToSolar(tryYear, lm, ld);
+ if (!solar) continue;
+ const diff = daysBetween(y, m, d, solar.year, solar.month, solar.day);
+ if (diff >= 0) {
+ list.push({ name, solarDate: `${solar.year}-${String(solar.month).padStart(2, '0')}-${String(solar.day).padStart(2, '0')}`, daysLeft: diff });
+ break;
+ }
+ }
+ }
+
+ list.sort((a, b) => a.daysLeft - b.daysLeft);
+ return list[0] || null;
+}
+
/** 神煞分析 */
function analyzeShensha(bazi) {
const result = [];
@@ -1245,6 +1350,13 @@ module.exports = {
calculatePlumBlossom,
calculateBoneWeight,
getBuddhistFestival,
+ getLegalHoliday,
+ isWorkday,
+ getWeekDay,
+ getFestivalSolarDate,
+ getUpcomingFestivals,
+ getNextBuddhistFestival,
+ daysBetween,
analyzeShensha,
analyzeFortuneGanzhi,
getYearMonths,