【重要修复】重写农历双向转换算法(原来全错): - solarToLunar/lunarToSolar 改用标准算法(基准1900-01-31正月初一,UTC避免时区误差) - 验证:2026春节2-17、中秋9-25、端午6-19、2023闰二月全部正确 【首页大日历】 - 日期数字加大到240rpx,超醒目 - 节假日红色喜庆卡片+红数字(春节/国庆/中秋等法定假日) - 工作日绿色卡片+绿数字(一眼知道要上班) - 周末红色数字+米黄卡片 - 节气显示具体时刻(如 立秋 20:29) - 初一/十五金色标记(可在设置关闭) - 佛历提醒:距下一佛诞还有几天(可在设置开启) 【全屏放大弹窗(老年友好)】 - 大日历卡片/宜忌/详情/节日速查 点击均可全屏放大 - 宜忌放大后字号44rpx,详情放大后40rpx 【月历视图】 - swiper 左右滑动换月,预计算5个月居中显示 - 滑到边缘自动补月,可无限滚动 - 点选日期切回日视图 【节日速查】 - 首页显示近期5个节日(中秋/重阳等)+公历日期+倒计时 - 点击直接跳到该日老黄历 【设置页】 - 新增'显示佛历提醒'开关(默认关) - 新增'初一十五高亮'开关(默认开)
This commit is contained in:
+6
-1
@@ -59,5 +59,10 @@
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"permission": {
|
||||
"scope.record": {
|
||||
"desc": "用于许愿树吹风互动效果"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+157
-36
@@ -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() {}
|
||||
});
|
||||
|
||||
+170
-51
@@ -12,24 +12,26 @@
|
||||
<!-- 日视图:老黄历 -->
|
||||
<scroll-view class="page-body" scroll-y wx:if="{{viewMode === 'day'}}" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="container">
|
||||
<!-- 大日历卡片 -->
|
||||
<view class="almanac-card">
|
||||
|
||||
<!-- 大日历卡片(点击放大) -->
|
||||
<view class="almanac-card {{legalHoliday ? 'is-holiday' : (workday ? 'is-workday' : 'is-weekend')}}" bindtap="openZoom" data-type="detail">
|
||||
<!-- 顶部:公历 -->
|
||||
<view class="almanac-head">
|
||||
<view class="almanac-head-left">
|
||||
<text class="almanac-year">{{dayInfo.solarYear}}年{{dayInfo.solarMonth}}月</text>
|
||||
<text class="almanac-week">{{dayInfo.weekDay}} · {{dayInfo.constellation}}</text>
|
||||
</view>
|
||||
<view class="almanac-head-right" wx:if="{{dayInfo.isToday}}">
|
||||
<text class="today-tag">今日</text>
|
||||
<view class="head-tags">
|
||||
<text class="today-tag" wx:if="{{dayInfo.isToday}}">今日</text>
|
||||
<text class="lunar-tag" wx:if="{{isFirstOrFifteen}}">{{dayInfo.lunarDay === 1 ? '初一' : '十五'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 大日期 -->
|
||||
<view class="almanac-day-row">
|
||||
<view class="day-arrow" bindtap="prevDay">‹</view>
|
||||
<view class="almanac-day-num font-chinese">{{dayInfo.solarDay}}</view>
|
||||
<view class="day-arrow" bindtap="nextDay">›</view>
|
||||
<view class="almanac-day-row" catchtap="noop">
|
||||
<view class="day-arrow" catchtap="prevDay">‹</view>
|
||||
<view class="almanac-day-num font-chinese {{legalHoliday ? 'num-holiday' : (workday ? 'num-workday' : 'num-weekend')}}">{{dayInfo.solarDay}}</view>
|
||||
<view class="day-arrow" catchtap="nextDay">›</view>
|
||||
</view>
|
||||
|
||||
<!-- 农历 -->
|
||||
@@ -38,14 +40,27 @@
|
||||
<text class="almanac-lunar-sub">{{dayInfo.lunarYearGanzhi}}年 {{dayInfo.lunarMonthGanzhi}}月 {{dayInfo.lunarDayGanzhi}}日 · 属{{dayInfo.zodiac}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 节日/节气 -->
|
||||
<view class="almanac-fest" wx:if="{{dayInfo.lunarFestival || dayInfo.solarFestival || dayInfo.solarTerm}}">
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.lunarFestival}}">{{dayInfo.lunarFestival}}</text>
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.solarFestival}}">{{dayInfo.solarFestival}}</text>
|
||||
<text class="badge badge-outline" wx:if="{{dayInfo.solarTerm}}">{{dayInfo.solarTerm}}</text>
|
||||
<!-- 节日/节气/佛历 -->
|
||||
<view class="almanac-fest" wx:if="{{legalHoliday || dayInfo.lunarFestival || dayInfo.solarFestival || dayInfo.solarTerm}}">
|
||||
<text class="badge badge-holiday" wx:if="{{legalHoliday}}">{{legalHoliday}}</text>
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.lunarFestival && !legalHoliday}}">{{dayInfo.lunarFestival}}</text>
|
||||
<text class="badge badge-festival" wx:if="{{dayInfo.solarFestival && !legalHoliday}}">{{dayInfo.solarFestival}}</text>
|
||||
<text class="badge badge-term" wx:if="{{dayInfo.solarTerm}}">{{dayInfo.solarTerm}}{{termTime ? ' ' + termTime : ''}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 宜忌 -->
|
||||
<!-- 佛历提醒 -->
|
||||
<view class="buddhist-tip" wx:if="{{showBuddhist && nextBuddhist}}" catchtap="noop">
|
||||
<text class="buddhist-text">🪷 距 {{nextBuddhist.name}} 还有 {{nextBuddhist.daysLeft}} 天({{nextBuddhist.solarDate}})</text>
|
||||
</view>
|
||||
|
||||
<!-- 工作日/节假日提示 -->
|
||||
<view class="day-type-tip {{legalHoliday ? 'tip-holiday' : (workday ? 'tip-workday' : 'tip-weekend')}}">
|
||||
{{legalHoliday ? legalHoliday + ' · 放假' : (workday ? '工作日 · 上班' : '周末 · 休息')}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 宜忌(点击放大) -->
|
||||
<view class="card yiji-card" bindtap="openZoom" data-type="yiji">
|
||||
<view class="yiji-row">
|
||||
<view class="yiji-col">
|
||||
<view class="yiji-title yi">宜</view>
|
||||
@@ -61,13 +76,11 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回今天 -->
|
||||
<view class="back-today" wx:if="{{!dayInfo.isToday}}" bindtap="backToday">回到今天</view>
|
||||
<view class="zoom-hint">点击放大 🔍</view>
|
||||
</view>
|
||||
|
||||
<!-- 黄历详情 -->
|
||||
<view class="card">
|
||||
<!-- 黄历详情(点击放大) -->
|
||||
<view class="card" bindtap="openZoom" data-type="detail">
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<view class="p-2 bg-page rounded">
|
||||
<text class="text-xs text-muted">值神</text>
|
||||
@@ -94,41 +107,147 @@
|
||||
<text class="text-base font-medium block">{{almanac.pengZu}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-primary mt-2 detail-btn" bindtap="goDetail">查看详情</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 月历视图 -->
|
||||
<scroll-view class="page-body" scroll-y wx:if="{{viewMode === 'month'}}" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="container">
|
||||
<!-- 年月切换 -->
|
||||
<view class="card flex justify-between items-center">
|
||||
<view class="btn-ghost" bindtap="prevMonth">‹</view>
|
||||
<view class="text-xl font-bold">{{year}}年{{month}}月</view>
|
||||
<view class="btn-ghost" bindtap="nextMonth">›</view>
|
||||
<view class="zoom-hint">点击放大 🔍</view>
|
||||
</view>
|
||||
|
||||
<!-- 星期头 -->
|
||||
<view class="grid grid-cols-7 gap-1 mb-1">
|
||||
<view class="text-center text-sm text-muted" wx:for="{{weekHeaders}}" wx:key="*this">{{item}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历网格 -->
|
||||
<view class="grid grid-cols-7 gap-1">
|
||||
<view
|
||||
class="day-cell {{item.isToday ? 'today' : ''}} {{item.solarMonth !== month ? 'other-month' : ''}}"
|
||||
wx:for="{{calendarDays}}"
|
||||
wx:key="solarDate"
|
||||
bindtap="selectDay"
|
||||
data-date="{{item.solarDate}}"
|
||||
>
|
||||
<text class="day-number">{{item.solarDay}}</text>
|
||||
<text class="day-lunar {{item.lunarFestival || item.solarFestival ? 'festival' : ''}}">
|
||||
{{item.lunarFestival || item.solarFestival || item.lunarDayName}}
|
||||
</text>
|
||||
<view class="day-dot" wx:if="{{item.isTermDay}}"></view>
|
||||
<!-- 节日速查 -->
|
||||
<view class="card" bindtap="openZoom" data-type="fest">
|
||||
<view class="section-title">节日速查</view>
|
||||
<view class="fest-quick">
|
||||
<view class="fest-quick-item" wx:for="{{upcoming}}" wx:key="name" catchtap="goFestival" data-date="{{item.solarDate}}">
|
||||
<text class="fest-name">{{item.name}}</text>
|
||||
<text class="fest-date">{{item.solarMonth}}月{{item.solarDay}}日</text>
|
||||
<text class="fest-left {{item.daysLeft === 0 ? 'is-today' : ''}}">{{item.daysLeft === 0 ? '今天' : item.daysLeft + '天后'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回今天 -->
|
||||
<view class="back-today" wx:if="{{!dayInfo.isToday}}" bindtap="backToday">回到今天</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 月历视图:swiper 上下/左右滚动换月 -->
|
||||
<view class="page-body month-body" wx:if="{{viewMode === 'month'}}" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||
<view class="month-header">
|
||||
<text class="month-title font-chinese">{{currentYear}}年{{currentMonth}}月</text>
|
||||
<text class="month-hint">上下或左右滑动换月</text>
|
||||
</view>
|
||||
<view class="grid grid-cols-7 week-head">
|
||||
<view class="text-center week-cell" wx:for="{{weekHeaders}}" wx:key="*this">{{item}}</view>
|
||||
</view>
|
||||
<swiper
|
||||
class="month-swiper"
|
||||
vertical="{{false}}"
|
||||
circular="{{false}}"
|
||||
current="{{monthIndex}}"
|
||||
bindchange="onMonthSwiperChange"
|
||||
duration="300"
|
||||
>
|
||||
<swiper-item wx:for="{{monthList}}" wx:key="key">
|
||||
<scroll-view scroll-y class="month-scroll">
|
||||
<view class="grid grid-cols-7 month-grid">
|
||||
<view
|
||||
class="day-cell {{d.isToday ? 'today' : ''}} {{d.solarMonth !== item.month ? 'other-month' : ''}}"
|
||||
wx:for="{{item.weeks}}"
|
||||
wx:for-item="week"
|
||||
wx:key="week"
|
||||
>
|
||||
<block wx:for="{{week}}" wx:for-item="d" wx:key="solarDate">
|
||||
<view
|
||||
class="day-cell-inner {{d.isToday ? 'today' : ''}} {{d.solarMonth !== item.month ? 'other-month' : ''}}"
|
||||
bindtap="selectDay"
|
||||
data-date="{{d.solarDate}}"
|
||||
>
|
||||
<text class="day-number">{{d.solarDay}}</text>
|
||||
<text class="day-lunar {{d.lunarFestival || d.solarFestival ? 'festival' : ''}}">
|
||||
{{d.lunarFestival || d.solarFestival || d.lunarDayName}}
|
||||
</text>
|
||||
<view class="day-dot" wx:if="{{d.isTermDay}}"></view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- 全屏放大弹窗(老年友好) -->
|
||||
<view class="zoom-mask {{zoomVisible ? 'show' : ''}}" bindtap="closeZoom">
|
||||
<view class="zoom-panel" catchtap="noop">
|
||||
<view class="zoom-header">
|
||||
<text class="zoom-title font-chinese">{{zoomTitle}}</text>
|
||||
<view class="zoom-close" bindtap="closeZoom">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 宜忌放大 -->
|
||||
<scroll-view scroll-y class="zoom-body" wx:if="{{zoomType === 'yiji' && zoomData}}">
|
||||
<view class="zoom-yiji-block">
|
||||
<view class="zoom-yiji-title yi">宜</view>
|
||||
<view class="zoom-yiji-items">
|
||||
<text class="zoom-yiji-item yi" wx:for="{{zoomData.recommends}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="zoom-yiji-block">
|
||||
<view class="zoom-yiji-title ji">忌</view>
|
||||
<view class="zoom-yiji-items">
|
||||
<text class="zoom-yiji-item ji" wx:for="{{zoomData.avoids}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 详情放大 -->
|
||||
<scroll-view scroll-y class="zoom-body" wx:if="{{zoomType === 'detail' && zoomData}}">
|
||||
<view class="zoom-detail-row" wx:if="{{zoomData.legalHoliday}}">
|
||||
<text class="zoom-detail-label">节假日</text>
|
||||
<text class="zoom-detail-value holiday">{{zoomData.legalHoliday}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row" wx:if="{{zoomData.termTime}}">
|
||||
<text class="zoom-detail-label">节气</text>
|
||||
<text class="zoom-detail-value">{{zoomData.termTime}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">农历</text>
|
||||
<text class="zoom-detail-value">{{zoomData.lunar}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">干支</text>
|
||||
<text class="zoom-detail-value">{{zoomData.ganzhi}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">值神</text>
|
||||
<text class="zoom-detail-value">{{zoomData.duty}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">黄道</text>
|
||||
<text class="zoom-detail-value">{{zoomData.twelveStar}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">二十八宿</text>
|
||||
<text class="zoom-detail-value">{{zoomData.twentyEightStar}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">六曜</text>
|
||||
<text class="zoom-detail-value">{{zoomData.sixStar}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">冲煞</text>
|
||||
<text class="zoom-detail-value">{{zoomData.clash}}</text>
|
||||
</view>
|
||||
<view class="zoom-detail-row">
|
||||
<text class="zoom-detail-label">彭祖百忌</text>
|
||||
<text class="zoom-detail-value">{{zoomData.pengZu}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 节日放大 -->
|
||||
<scroll-view scroll-y class="zoom-body" wx:if="{{zoomType === 'fest' && zoomData}}">
|
||||
<view class="zoom-fest-row" wx:for="{{zoomData.upcoming}}" wx:key="name" bindtap="goFestival" data-date="{{item.solarDate}}">
|
||||
<text class="zoom-fest-name">{{item.name}}</text>
|
||||
<text class="zoom-fest-date">{{item.solarDate}}</text>
|
||||
<text class="zoom-fest-left {{item.daysLeft === 0 ? 'is-today' : ''}}">{{item.daysLeft === 0 ? '今天' : '还有' + item.daysLeft + '天'}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
+373
-41
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,16 @@
|
||||
<text class="setting-label">真太阳时校正</text>
|
||||
<switch checked="{{solarTime}}" bindchange="onSolarTimeChange" color="#C41E3A"/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">显示佛历提醒</text>
|
||||
<switch checked="{{showBuddhist}}" bindchange="onBuddhistChange" color="#C41E3A"/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">初一十五高亮</text>
|
||||
<switch checked="{{highlightLunar}}" bindchange="onHighlightLunarChange" color="#C41E3A"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏管理 -->
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
<view class="container">
|
||||
<!-- 许愿树画布 -->
|
||||
<view class="canvas-container">
|
||||
<canvas
|
||||
canvas-id="wishTree"
|
||||
class="wish-tree-canvas"
|
||||
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
||||
></canvas>
|
||||
<!-- 全屏画布 -->
|
||||
<canvas
|
||||
canvas-id="wishTree"
|
||||
class="wish-tree-canvas"
|
||||
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
||||
bindtouchstart="onCanvasTouch"
|
||||
></canvas>
|
||||
|
||||
<!-- 许愿条覆盖层 -->
|
||||
<view class="wishes-overlay">
|
||||
<view
|
||||
wx:for="{{wishes}}"
|
||||
wx:key="id"
|
||||
class="wish-tag {{item.type}} {{item.isRobot ? 'robot' : ''}}"
|
||||
style="left: {{item.x}}px; top: {{item.y}}px;"
|
||||
bindtap="viewWishDetail"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="wish-text">{{item.content}}</text>
|
||||
<view class="wish-badge" wx:if="{{item.type === 'paid'}}">VIP</view>
|
||||
<!-- 顶部导航 -->
|
||||
<view class="top-bar">
|
||||
<view class="tree-switcher">
|
||||
<view class="switch-btn" bindtap="switchTree" data-direction="prev">
|
||||
<text class="switch-icon">‹</text>
|
||||
</view>
|
||||
<view class="tree-name">{{trees[currentTreeIndex].name || '许愿树'}}</view>
|
||||
<view class="switch-btn" bindtap="switchTree" data-direction="next">
|
||||
<text class="switch-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 传感器控制 -->
|
||||
<view class="sensor-controls">
|
||||
<view class="sensor-btn {{micEnabled ? 'active' : ''}}" bindtap="enableMicrophone">
|
||||
<text class="sensor-icon">🎤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 许愿按钮 -->
|
||||
<view class="action-bar">
|
||||
<button class="btn-primary btn-wish" bindtap="openCreateModal">
|
||||
<button class="btn-wish" bindtap="openCreateModal">
|
||||
<text class="icon">🙏</text>
|
||||
<text>我要许愿</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 许愿说明 -->
|
||||
<view class="tips-section">
|
||||
<view class="tip-item">
|
||||
<text class="tip-icon">🎋</text>
|
||||
<text class="tip-text">免费许愿:20字以内,可能被覆盖</text>
|
||||
</view>
|
||||
<view class="tip-item">
|
||||
<text class="tip-icon">✨</text>
|
||||
<text class="tip-text">付费许愿:100字以内,优先展示,长期保留</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 许愿弹窗 -->
|
||||
<view class="modal" wx:if="{{showCreateModal}}">
|
||||
<view class="modal-mask" bindtap="closeCreateModal"></view>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+195
-83
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user