【重要修复】重写农历双向转换算法(原来全错): - 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",
|
"style": "v2",
|
||||||
"sitemapLocation": "sitemap.json",
|
"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 pad(n) { return String(n).padStart(2, '0'); }
|
||||||
function fmt(y, m, d) { return `${y}-${pad(m)}-${pad(d)}`; }
|
function fmt(y, m, d) { return `${y}-${pad(m)}-${pad(d)}`; }
|
||||||
|
|
||||||
|
const MONTH_SPAN = 2; // 当前月前后各2个月,共5个月
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
statusBarHeight: 20,
|
statusBarHeight: 20,
|
||||||
navBarHeight: 44,
|
navBarHeight: 44,
|
||||||
viewMode: 'day', // day | month
|
viewMode: 'day',
|
||||||
dayInfo: {},
|
dayInfo: {},
|
||||||
almanac: {},
|
almanac: {},
|
||||||
// 月历
|
legalHoliday: null,
|
||||||
year: 0,
|
workday: true,
|
||||||
month: 0,
|
termTime: '',
|
||||||
|
isFirstOrFifteen: false,
|
||||||
|
nextBuddhist: null,
|
||||||
|
showBuddhist: false,
|
||||||
|
highlightLunar: true,
|
||||||
|
upcoming: [],
|
||||||
|
// 月历 swiper
|
||||||
|
monthList: [], // [{key, year, month, weeks}]
|
||||||
|
monthIndex: 2, // 当前居中
|
||||||
|
currentYear: 0,
|
||||||
|
currentMonth: 0,
|
||||||
weekHeaders: ['日', '一', '二', '三', '四', '五', '六'],
|
weekHeaders: ['日', '一', '二', '三', '四', '五', '六'],
|
||||||
calendarDays: [],
|
// 全屏弹窗
|
||||||
selectedDay: null
|
zoomVisible: false,
|
||||||
|
zoomTitle: '',
|
||||||
|
zoomType: '', // yiji | detail | fest
|
||||||
|
zoomData: null
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
@@ -23,18 +42,33 @@ Page({
|
|||||||
const menu = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null;
|
const menu = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null;
|
||||||
const statusBarHeight = sys.statusBarHeight || 20;
|
const statusBarHeight = sys.statusBarHeight || 20;
|
||||||
let navBarHeight = 44;
|
let navBarHeight = 44;
|
||||||
if (menu && menu.top) {
|
if (menu && menu.top) navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height;
|
||||||
navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height;
|
|
||||||
}
|
|
||||||
this.setData({ statusBarHeight, navBarHeight });
|
this.setData({ statusBarHeight, navBarHeight });
|
||||||
|
this.loadSettings();
|
||||||
this.loadDay(new Date());
|
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() {
|
onPullDownRefresh() {
|
||||||
if (this.data.viewMode === 'day') {
|
if (this.data.viewMode === 'day') {
|
||||||
this.loadDay(new Date());
|
this.loadDay(new Date());
|
||||||
|
this.loadUpcoming();
|
||||||
} else {
|
} else {
|
||||||
this.loadCalendar();
|
this.buildMonthList(this.data.currentYear, this.data.currentMonth);
|
||||||
}
|
}
|
||||||
wx.stopPullDownRefresh();
|
wx.stopPullDownRefresh();
|
||||||
},
|
},
|
||||||
@@ -46,10 +80,26 @@ Page({
|
|||||||
const d = date.getDate();
|
const d = date.getDate();
|
||||||
const dayInfo = getDayInfo(y, m, d);
|
const dayInfo = getDayInfo(y, m, d);
|
||||||
const almanac = getAlmanacInfo(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); },
|
prevDay() { this.shiftDay(-1); },
|
||||||
nextDay() { this.shiftDay(1); },
|
nextDay() { this.shiftDay(1); },
|
||||||
shiftDay(delta) {
|
shiftDay(delta) {
|
||||||
@@ -58,45 +108,72 @@ Page({
|
|||||||
this.loadDay(date);
|
this.loadDay(date);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 回到今天 */
|
|
||||||
backToday() {
|
backToday() {
|
||||||
this.loadDay(new Date());
|
this.loadDay(new Date());
|
||||||
|
this.loadUpcoming();
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 切换 日视图/月历视图 */
|
/** 切换 日/月 视图 */
|
||||||
toggleView() {
|
toggleView() {
|
||||||
const mode = this.data.viewMode === 'day' ? 'month' : 'day';
|
const mode = this.data.viewMode === 'day' ? 'month' : 'day';
|
||||||
this.setData({ viewMode: mode });
|
this.setData({ viewMode: mode });
|
||||||
if (mode === 'month') {
|
if (mode === 'month') {
|
||||||
const { dayInfo } = this.data;
|
const { dayInfo } = this.data;
|
||||||
this.setData({ year: dayInfo.solarYear, month: dayInfo.solarMonth }, () => this.loadCalendar());
|
this.buildMonthList(dayInfo.solarYear, dayInfo.solarMonth);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 月历 */
|
/** 构建5个月的数据,居中显示 */
|
||||||
loadCalendar() {
|
buildMonthList(year, month) {
|
||||||
const { year, month } = this.data;
|
const monthList = [];
|
||||||
const weeks = getMonthCalendar(year, month);
|
for (let i = -MONTH_SPAN; i <= MONTH_SPAN; i++) {
|
||||||
const calendarDays = [];
|
let y = year, m = month + i;
|
||||||
weeks.forEach(w => w.forEach(d => calendarDays.push(d)));
|
while (m < 1) { m += 12; y--; }
|
||||||
this.setData({ calendarDays });
|
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() {
|
/** swiper 切换 */
|
||||||
let { year, month } = this.data;
|
onMonthSwiperChange(e) {
|
||||||
month--;
|
const index = e.detail.current;
|
||||||
if (month < 1) { month = 12; year--; }
|
const { monthList } = this.data;
|
||||||
this.setData({ year, month }, () => this.loadCalendar());
|
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() {
|
prependMonth() {
|
||||||
let { year, month } = this.data;
|
const { monthList } = this.data;
|
||||||
month++;
|
const first = monthList[0];
|
||||||
if (month > 12) { month = 1; year++; }
|
let y = first.year, m = first.month - 1;
|
||||||
this.setData({ year, month }, () => this.loadCalendar());
|
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) {
|
selectDay(e) {
|
||||||
const date = e.currentTarget.dataset.date;
|
const date = e.currentTarget.dataset.date;
|
||||||
const [y, m, d] = date.split('-').map(Number);
|
const [y, m, d] = date.split('-').map(Number);
|
||||||
@@ -104,13 +181,57 @@ Page({
|
|||||||
this.setData({ viewMode: 'day' });
|
this.setData({ viewMode: 'day' });
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 跳详情页 */
|
|
||||||
goDetail() {
|
goDetail() {
|
||||||
const { dayInfo } = this.data;
|
const { dayInfo } = this.data;
|
||||||
wx.navigateTo({ url: `/pages/day-detail/day-detail?date=${fmt(dayInfo.solarYear, dayInfo.solarMonth, dayInfo.solarDay)}` });
|
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) {
|
navTo(e) {
|
||||||
wx.navigateTo({ url: e.currentTarget.dataset.url });
|
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;">
|
<scroll-view class="page-body" scroll-y wx:if="{{viewMode === 'day'}}" style="padding-top: {{statusBarHeight + navBarHeight}}px;">
|
||||||
<view class="container">
|
<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">
|
||||||
<view class="almanac-head-left">
|
<view class="almanac-head-left">
|
||||||
<text class="almanac-year">{{dayInfo.solarYear}}年{{dayInfo.solarMonth}}月</text>
|
<text class="almanac-year">{{dayInfo.solarYear}}年{{dayInfo.solarMonth}}月</text>
|
||||||
<text class="almanac-week">{{dayInfo.weekDay}} · {{dayInfo.constellation}}</text>
|
<text class="almanac-week">{{dayInfo.weekDay}} · {{dayInfo.constellation}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="almanac-head-right" wx:if="{{dayInfo.isToday}}">
|
<view class="head-tags">
|
||||||
<text class="today-tag">今日</text>
|
<text class="today-tag" wx:if="{{dayInfo.isToday}}">今日</text>
|
||||||
|
<text class="lunar-tag" wx:if="{{isFirstOrFifteen}}">{{dayInfo.lunarDay === 1 ? '初一' : '十五'}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 大日期 -->
|
<!-- 大日期 -->
|
||||||
<view class="almanac-day-row">
|
<view class="almanac-day-row" catchtap="noop">
|
||||||
<view class="day-arrow" bindtap="prevDay">‹</view>
|
<view class="day-arrow" catchtap="prevDay">‹</view>
|
||||||
<view class="almanac-day-num font-chinese">{{dayInfo.solarDay}}</view>
|
<view class="almanac-day-num font-chinese {{legalHoliday ? 'num-holiday' : (workday ? 'num-workday' : 'num-weekend')}}">{{dayInfo.solarDay}}</view>
|
||||||
<view class="day-arrow" bindtap="nextDay">›</view>
|
<view class="day-arrow" catchtap="nextDay">›</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 农历 -->
|
<!-- 农历 -->
|
||||||
@@ -38,14 +40,27 @@
|
|||||||
<text class="almanac-lunar-sub">{{dayInfo.lunarYearGanzhi}}年 {{dayInfo.lunarMonthGanzhi}}月 {{dayInfo.lunarDayGanzhi}}日 · 属{{dayInfo.zodiac}}</text>
|
<text class="almanac-lunar-sub">{{dayInfo.lunarYearGanzhi}}年 {{dayInfo.lunarMonthGanzhi}}月 {{dayInfo.lunarDayGanzhi}}日 · 属{{dayInfo.zodiac}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 节日/节气 -->
|
<!-- 节日/节气/佛历 -->
|
||||||
<view class="almanac-fest" wx:if="{{dayInfo.lunarFestival || dayInfo.solarFestival || dayInfo.solarTerm}}">
|
<view class="almanac-fest" wx:if="{{legalHoliday || dayInfo.lunarFestival || dayInfo.solarFestival || dayInfo.solarTerm}}">
|
||||||
<text class="badge badge-festival" wx:if="{{dayInfo.lunarFestival}}">{{dayInfo.lunarFestival}}</text>
|
<text class="badge badge-holiday" wx:if="{{legalHoliday}}">{{legalHoliday}}</text>
|
||||||
<text class="badge badge-festival" wx:if="{{dayInfo.solarFestival}}">{{dayInfo.solarFestival}}</text>
|
<text class="badge badge-festival" wx:if="{{dayInfo.lunarFestival && !legalHoliday}}">{{dayInfo.lunarFestival}}</text>
|
||||||
<text class="badge badge-outline" wx:if="{{dayInfo.solarTerm}}">{{dayInfo.solarTerm}}</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>
|
||||||
|
|
||||||
<!-- 宜忌 -->
|
<!-- 佛历提醒 -->
|
||||||
|
<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-row">
|
||||||
<view class="yiji-col">
|
<view class="yiji-col">
|
||||||
<view class="yiji-title yi">宜</view>
|
<view class="yiji-title yi">宜</view>
|
||||||
@@ -61,13 +76,11 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="zoom-hint">点击放大 🔍</view>
|
||||||
<!-- 回今天 -->
|
|
||||||
<view class="back-today" wx:if="{{!dayInfo.isToday}}" bindtap="backToday">回到今天</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 黄历详情 -->
|
<!-- 黄历详情(点击放大) -->
|
||||||
<view class="card">
|
<view class="card" bindtap="openZoom" data-type="detail">
|
||||||
<view class="grid grid-cols-2 gap-2">
|
<view class="grid grid-cols-2 gap-2">
|
||||||
<view class="p-2 bg-page rounded">
|
<view class="p-2 bg-page rounded">
|
||||||
<text class="text-xs text-muted">值神</text>
|
<text class="text-xs text-muted">值神</text>
|
||||||
@@ -94,41 +107,147 @@
|
|||||||
<text class="text-base font-medium block">{{almanac.pengZu}}</text>
|
<text class="text-base font-medium block">{{almanac.pengZu}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="btn-primary mt-2 detail-btn" bindtap="goDetail">查看详情</view>
|
<view class="zoom-hint">点击放大 🔍</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>
|
</view>
|
||||||
|
|
||||||
<!-- 星期头 -->
|
<!-- 节日速查 -->
|
||||||
<view class="grid grid-cols-7 gap-1 mb-1">
|
<view class="card" bindtap="openZoom" data-type="fest">
|
||||||
<view class="text-center text-sm text-muted" wx:for="{{weekHeaders}}" wx:key="*this">{{item}}</view>
|
<view class="section-title">节日速查</view>
|
||||||
</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>
|
||||||
<view class="grid grid-cols-7 gap-1">
|
<text class="fest-date">{{item.solarMonth}}月{{item.solarDay}}日</text>
|
||||||
<view
|
<text class="fest-left {{item.daysLeft === 0 ? 'is-today' : ''}}">{{item.daysLeft === 0 ? '今天' : item.daysLeft + '天后'}}</text>
|
||||||
class="day-cell {{item.isToday ? 'today' : ''}} {{item.solarMonth !== month ? 'other-month' : ''}}"
|
</view>
|
||||||
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>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 回今天 -->
|
||||||
|
<view class="back-today" wx:if="{{!dayInfo.isToday}}" bindtap="backToday">回到今天</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-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;
|
min-width: 96rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面主体 */
|
|
||||||
.page-body {
|
.page-body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -55,12 +54,30 @@
|
|||||||
|
|
||||||
/* ===== 老黄历大日历卡片 ===== */
|
/* ===== 老黄历大日历卡片 ===== */
|
||||||
.almanac-card {
|
.almanac-card {
|
||||||
background: linear-gradient(160deg, #FFFFFF 0%, #FFF8EF 100%);
|
|
||||||
border: 1rpx solid #E8D5C4;
|
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
padding: 32rpx 24rpx;
|
padding: 32rpx 24rpx 24rpx;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 16rpx;
|
||||||
position: relative;
|
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 {
|
.almanac-head {
|
||||||
@@ -81,6 +98,11 @@
|
|||||||
margin-left: 16rpx;
|
margin-left: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.head-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.today-tag {
|
.today-tag {
|
||||||
background: #C41E3A;
|
background: #C41E3A;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
@@ -89,37 +111,48 @@
|
|||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lunar-tag {
|
||||||
|
background: #E0A93E;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 4rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
/* 大日期 */
|
/* 大日期 */
|
||||||
.almanac-day-row {
|
.almanac-day-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 16rpx 0 8rpx;
|
margin: 8rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.almanac-day-num {
|
.almanac-day-num {
|
||||||
font-size: 160rpx;
|
font-size: 240rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #C41E3A;
|
line-height: 1.05;
|
||||||
line-height: 1.1;
|
min-width: 400rpx;
|
||||||
min-width: 320rpx;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.num-holiday { color: #C41E3A; }
|
||||||
|
.num-workday { color: #2E7D32; }
|
||||||
|
.num-weekend { color: #C41E3A; }
|
||||||
|
|
||||||
.day-arrow {
|
.day-arrow {
|
||||||
font-size: 64rpx;
|
font-size: 72rpx;
|
||||||
color: #C9B8A6;
|
color: #C9B8A6;
|
||||||
padding: 0 32rpx;
|
padding: 0 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 农历 */
|
/* 农历 */
|
||||||
.almanac-lunar {
|
.almanac-lunar {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.almanac-lunar-main {
|
.almanac-lunar-main {
|
||||||
font-size: 44rpx;
|
font-size: 48rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1A1A1A;
|
color: #1A1A1A;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -136,15 +169,72 @@
|
|||||||
.almanac-fest {
|
.almanac-fest {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 12rpx;
|
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 {
|
.yiji-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-top: 1rpx solid #F0E6DA;
|
|
||||||
padding-top: 24rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yiji-col {
|
.yiji-col {
|
||||||
@@ -171,15 +261,8 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yiji-title.yi {
|
.yiji-title.yi { background: #C41E3A; color: #FFFFFF; }
|
||||||
background: #C41E3A;
|
.yiji-title.ji { background: #546E7A; color: #FFFFFF; }
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yiji-title.ji {
|
|
||||||
background: #546E7A;
|
|
||||||
color: #FFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yiji-items {
|
.yiji-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -189,22 +272,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.yiji-item {
|
.yiji-item {
|
||||||
font-size: 24rpx;
|
font-size: 26rpx;
|
||||||
padding: 4rpx 14rpx;
|
padding: 4rpx 14rpx;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yiji-item.yi {
|
.yiji-item.yi { background: #FEF2F2; color: #C41E3A; }
|
||||||
background: #FEF2F2;
|
.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;
|
color: #C41E3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yiji-item.ji {
|
/* 节日速查 */
|
||||||
background: #F1F5F9;
|
.fest-quick {
|
||||||
color: #546E7A;
|
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 {
|
.back-today {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 20rpx;
|
margin-top: 20rpx;
|
||||||
@@ -213,37 +343,86 @@
|
|||||||
padding: 12rpx;
|
padding: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-btn {
|
/* ===== 月历视图 ===== */
|
||||||
|
.month-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-header {
|
||||||
text-align: center;
|
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 {
|
.day-cell {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 12rpx 4rpx;
|
padding: 16rpx 4rpx;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-radius: 12rpx;
|
border-radius: 12rpx;
|
||||||
min-height: 100rpx;
|
min-height: 110rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-cell.today {
|
.day-cell-inner.today {
|
||||||
background: #C41E3A;
|
background: #C41E3A;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-cell.today .day-lunar {
|
.day-cell-inner.today .day-lunar {
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-cell.other-month {
|
.day-cell-inner.other-month {
|
||||||
opacity: 0.4;
|
opacity: 0.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-number {
|
.day-number {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,3 +444,156 @@
|
|||||||
background: #C41E3A;
|
background: #C41E3A;
|
||||||
margin-top: 4rpx;
|
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,
|
weekStartDay: 0,
|
||||||
ziHourSect: 'lateZiNextDay',
|
ziHourSect: 'lateZiNextDay',
|
||||||
solarTime: true,
|
solarTime: true,
|
||||||
|
showBuddhist: false,
|
||||||
|
highlightLunar: true,
|
||||||
bookmarks: []
|
bookmarks: []
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -21,13 +23,15 @@ Page({
|
|||||||
this.setData({
|
this.setData({
|
||||||
weekStartDay: settings.weekStartDay || 0,
|
weekStartDay: settings.weekStartDay || 0,
|
||||||
ziHourSect: settings.ziHourSect || 'lateZiNextDay',
|
ziHourSect: settings.ziHourSect || 'lateZiNextDay',
|
||||||
solarTime: settings.solarTime !== false
|
solarTime: settings.solarTime !== false,
|
||||||
|
showBuddhist: settings.showBuddhist === true,
|
||||||
|
highlightLunar: settings.highlightLunar !== false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
const { weekStartDay, ziHourSect, solarTime } = this.data;
|
const { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar } = this.data;
|
||||||
wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime });
|
wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar });
|
||||||
},
|
},
|
||||||
|
|
||||||
loadBookmarks() {
|
loadBookmarks() {
|
||||||
@@ -50,6 +54,16 @@ Page({
|
|||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onBuddhistChange(e) {
|
||||||
|
this.setData({ showBuddhist: e.detail.value });
|
||||||
|
this.saveSettings();
|
||||||
|
},
|
||||||
|
|
||||||
|
onHighlightLunarChange(e) {
|
||||||
|
this.setData({ highlightLunar: e.detail.value });
|
||||||
|
this.saveSettings();
|
||||||
|
},
|
||||||
|
|
||||||
removeBookmark(e) {
|
removeBookmark(e) {
|
||||||
const date = e.currentTarget.dataset.date;
|
const date = e.currentTarget.dataset.date;
|
||||||
let bookmarks = this.data.bookmarks.filter(b => b.date !== date);
|
let bookmarks = this.data.bookmarks.filter(b => b.date !== date);
|
||||||
|
|||||||
@@ -36,6 +36,16 @@
|
|||||||
<text class="setting-label">真太阳时校正</text>
|
<text class="setting-label">真太阳时校正</text>
|
||||||
<switch checked="{{solarTime}}" bindchange="onSolarTimeChange" color="#C41E3A"/>
|
<switch checked="{{solarTime}}" bindchange="onSolarTimeChange" color="#C41E3A"/>
|
||||||
</view>
|
</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>
|
</view>
|
||||||
|
|
||||||
<!-- 收藏管理 -->
|
<!-- 收藏管理 -->
|
||||||
|
|||||||
@@ -1,61 +1,556 @@
|
|||||||
const { request } = require('../../utils/request.js');
|
const { request } = require('../../utils/request.js');
|
||||||
|
|
||||||
|
// 丝带颜色配置
|
||||||
|
const RIBBON_COLORS = {
|
||||||
|
free: ['#FF6B6B', '#FF8E8E', '#FFB6C1', '#FFC0CB'],
|
||||||
|
paid: ['#FFD700', '#FFA500', '#FF8C00', '#DAA520'],
|
||||||
|
};
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
tree: null,
|
trees: [],
|
||||||
|
currentTreeIndex: 0,
|
||||||
wishes: [],
|
wishes: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
canvasWidth: 375,
|
canvasWidth: 375,
|
||||||
canvasHeight: 600,
|
canvasHeight: 667,
|
||||||
showCreateModal: false,
|
showCreateModal: false,
|
||||||
wishContent: '',
|
wishContent: '',
|
||||||
wishType: 'free', // free: 免费, paid: 付费
|
wishType: 'free',
|
||||||
maxFreeLength: 20,
|
maxFreeLength: 20,
|
||||||
maxPaidLength: 100,
|
maxPaidLength: 100,
|
||||||
products: [],
|
products: [],
|
||||||
selectedProduct: null,
|
selectedProduct: null,
|
||||||
|
// 弹幕相关
|
||||||
|
danmakuList: [],
|
||||||
|
// 传感器状态
|
||||||
|
gyroEnabled: false,
|
||||||
|
micEnabled: false,
|
||||||
|
windLevel: 0, // 0-1 风力等级
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.loadWishTree();
|
this.initCanvas();
|
||||||
|
this.loadTrees();
|
||||||
this.loadProducts();
|
this.loadProducts();
|
||||||
|
this.initSensors();
|
||||||
|
this.startAnimation();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload() {
|
||||||
|
this.stopAnimation();
|
||||||
|
this.stopSensors();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPullDownRefresh() {
|
onPullDownRefresh() {
|
||||||
this.loadWishTree().then(() => {
|
this.loadTrees().then(() => {
|
||||||
wx.stopPullDownRefresh();
|
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 {
|
try {
|
||||||
const res = await request({
|
const res = await request({
|
||||||
url: '/api/wish/tree',
|
url: '/api/wish/trees',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.setData({
|
this.setData({
|
||||||
tree: res.data.tree,
|
trees: res.data.list || [],
|
||||||
wishes: res.data.wishes || [],
|
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
this.drawTree();
|
this.loadWishes();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载许愿树失败:', err);
|
console.error('加载许愿树失败:', err);
|
||||||
this.setData({ loading: false });
|
// 使用模拟数据
|
||||||
// 使用本地模拟数据
|
|
||||||
this.setData({
|
this.setData({
|
||||||
tree: { id: 1, name: '祈福许愿树', maxWishes: 100 },
|
trees: [
|
||||||
wishes: this.getMockWishes(),
|
{ id: 1, name: '祈福树', type: 'pine', maxWishes: 100 },
|
||||||
|
{ id: 2, name: '姻缘树', type: 'sakura', maxWishes: 50 },
|
||||||
|
{ id: 3, name: '事业树', type: 'bamboo', maxWishes: 80 },
|
||||||
|
],
|
||||||
loading: false,
|
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() {
|
async loadProducts() {
|
||||||
try {
|
try {
|
||||||
@@ -69,73 +564,16 @@ Page({
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载商品失败:', err);
|
console.error('加载商品失败:', err);
|
||||||
// 使用默认商品
|
|
||||||
this.setData({
|
this.setData({
|
||||||
products: [
|
products: [
|
||||||
{ id: 1, name: '普通许愿条', price: 100, duration: 7, position: 0 },
|
{ id: 1, name: '普通许愿条', price: 100, duration: 7 },
|
||||||
{ id: 2, name: '精品许愿条', price: 500, duration: 30, position: 10 },
|
{ id: 2, name: '精品许愿条', price: 500, duration: 30 },
|
||||||
{ id: 3, name: '至尊许愿条', price: 2000, duration: 90, position: 100 },
|
{ 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() {
|
openCreateModal() {
|
||||||
this.setData({ showCreateModal: true });
|
this.setData({ showCreateModal: true });
|
||||||
@@ -171,28 +609,25 @@ Page({
|
|||||||
|
|
||||||
// 提交许愿
|
// 提交许愿
|
||||||
async submitWish() {
|
async submitWish() {
|
||||||
const { wishContent, wishType, selectedProduct } = this.data;
|
const { wishContent, wishType, selectedProduct, trees, currentTreeIndex } = this.data;
|
||||||
|
|
||||||
if (!wishContent.trim()) {
|
if (!wishContent.trim()) {
|
||||||
wx.showToast({ title: '请输入许愿内容', icon: 'none' });
|
wx.showToast({ title: '请输入许愿内容', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查字数限制
|
|
||||||
const maxLength = wishType === 'free' ? this.data.maxFreeLength : this.data.maxPaidLength;
|
const maxLength = wishType === 'free' ? this.data.maxFreeLength : this.data.maxPaidLength;
|
||||||
if (wishContent.length > maxLength) {
|
if (wishContent.length > maxLength) {
|
||||||
wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' });
|
wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 付费许愿需要选择商品
|
|
||||||
if (wishType === 'paid' && !selectedProduct) {
|
if (wishType === 'paid' && !selectedProduct) {
|
||||||
wx.showToast({ title: '请选择许愿商品', icon: 'none' });
|
wx.showToast({ title: '请选择许愿商品', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 如果是付费许愿,先创建订单
|
|
||||||
if (wishType === 'paid') {
|
if (wishType === 'paid') {
|
||||||
const orderRes = await request({
|
const orderRes = await request({
|
||||||
url: '/api/pay/create',
|
url: '/api/pay/create',
|
||||||
@@ -201,29 +636,28 @@ Page({
|
|||||||
type: 'wish',
|
type: 'wish',
|
||||||
productId: selectedProduct.id,
|
productId: selectedProduct.id,
|
||||||
content: wishContent,
|
content: wishContent,
|
||||||
|
treeId: trees[currentTreeIndex].id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orderRes.code === 0) {
|
if (orderRes.code === 0) {
|
||||||
// 调起微信支付
|
|
||||||
await this.wxPay(orderRes.data);
|
await this.wxPay(orderRes.data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 免费许愿直接创建
|
|
||||||
const res = await request({
|
const res = await request({
|
||||||
url: '/api/wish/create',
|
url: '/api/wish/create',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
content: wishContent,
|
content: wishContent,
|
||||||
type: 'free',
|
type: 'free',
|
||||||
treeId: this.data.tree.id,
|
treeId: trees[currentTreeIndex].id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
wx.showToast({ title: '许愿成功', icon: 'success' });
|
wx.showToast({ title: '许愿成功', icon: 'success' });
|
||||||
this.closeCreateModal();
|
this.closeCreateModal();
|
||||||
this.loadWishTree();
|
this.loadWishes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -244,7 +678,7 @@ Page({
|
|||||||
success: () => {
|
success: () => {
|
||||||
wx.showToast({ title: '支付成功', icon: 'success' });
|
wx.showToast({ title: '支付成功', icon: 'success' });
|
||||||
this.closeCreateModal();
|
this.closeCreateModal();
|
||||||
this.loadWishTree();
|
this.loadWishes();
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
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) {
|
viewWishDetail(e) {
|
||||||
const id = e.currentTarget.dataset.id;
|
const id = e.currentTarget.dataset.id;
|
||||||
@@ -266,8 +722,9 @@ Page({
|
|||||||
|
|
||||||
// 分享
|
// 分享
|
||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
|
const tree = this.getCurrentTree();
|
||||||
return {
|
return {
|
||||||
title: '快来许愿树许下你的愿望吧!',
|
title: `快来${tree.name || '许愿树'}许下你的愿望吧!`,
|
||||||
path: '/pages/wish-tree/wish-tree',
|
path: '/pages/wish-tree/wish-tree',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,48 +1,40 @@
|
|||||||
<view class="container">
|
<view class="container">
|
||||||
<!-- 许愿树画布 -->
|
<!-- 全屏画布 -->
|
||||||
<view class="canvas-container">
|
<canvas
|
||||||
<canvas
|
canvas-id="wishTree"
|
||||||
canvas-id="wishTree"
|
class="wish-tree-canvas"
|
||||||
class="wish-tree-canvas"
|
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
||||||
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
bindtouchstart="onCanvasTouch"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<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="wishes-overlay">
|
<view class="sensor-controls">
|
||||||
<view
|
<view class="sensor-btn {{micEnabled ? 'active' : ''}}" bindtap="enableMicrophone">
|
||||||
wx:for="{{wishes}}"
|
<text class="sensor-icon">🎤</text>
|
||||||
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>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 许愿按钮 -->
|
<!-- 许愿按钮 -->
|
||||||
<view class="action-bar">
|
<view class="action-bar">
|
||||||
<button class="btn-primary btn-wish" bindtap="openCreateModal">
|
<button class="btn-wish" bindtap="openCreateModal">
|
||||||
<text class="icon">🙏</text>
|
<text class="icon">🙏</text>
|
||||||
<text>我要许愿</text>
|
<text>我要许愿</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</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" wx:if="{{showCreateModal}}">
|
||||||
<view class="modal-mask" bindtap="closeCreateModal"></view>
|
<view class="modal-mask" bindtap="closeCreateModal"></view>
|
||||||
|
|||||||
@@ -1,127 +1,121 @@
|
|||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 100%);
|
|
||||||
padding-bottom: 120rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 画布容器 */
|
|
||||||
.canvas-container {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 800rpx;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: #0f3460;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 全屏画布 */
|
||||||
.wish-tree-canvas {
|
.wish-tree-canvas {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 许愿条覆盖层 */
|
|
||||||
.wishes-overlay {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wish-tag {
|
/* 顶部导航 */
|
||||||
|
.top-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 8rpx 16rpx;
|
top: 0;
|
||||||
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;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 20rpx 30rpx;
|
padding: 80rpx 30rpx 20rpx;
|
||||||
background: rgba(255,255,255,0.95);
|
display: flex;
|
||||||
box-shadow: 0 -2rpx 20rpx rgba(0,0,0,0.1);
|
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 {
|
.btn-wish {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10rpx;
|
gap: 12rpx;
|
||||||
background: linear-gradient(135deg, #C41E3A, #E74C3C);
|
background: linear-gradient(135deg, #C41E3A, #E74C3C);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
padding: 24rpx;
|
padding: 24rpx 60rpx;
|
||||||
border-radius: 50rpx;
|
border-radius: 50rpx;
|
||||||
border: none;
|
border: none;
|
||||||
|
box-shadow: 0 8rpx 30rpx rgba(196, 30, 58, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-wish .icon {
|
.btn-wish .icon {
|
||||||
font-size: 36rpx;
|
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 {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -138,7 +132,8 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0,0,0,0.5);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@@ -191,6 +186,7 @@
|
|||||||
border: 2rpx solid #eee;
|
border: 2rpx solid #eee;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-item.active {
|
.type-item.active {
|
||||||
@@ -254,6 +250,7 @@
|
|||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
border: 2rpx solid #eee;
|
border: 2rpx solid #eee;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-item.active {
|
.product-item.active {
|
||||||
|
|||||||
@@ -101,6 +101,54 @@ const BUDDHIST_FESTIVALS = {
|
|||||||
'11-17': '阿弥陀佛圣诞', '12-8': '释迦牟尼佛成道日(腊八)', '12-29': '华严菩萨圣诞'
|
'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 = [
|
const NAYIN_TABLE = [
|
||||||
'海中金', '炉中火', '大林木', '路旁土', '剑锋金', '山头火',
|
'海中金', '炉中火', '大林木', '路旁土', '剑锋金', '山头火',
|
||||||
@@ -370,6 +418,10 @@ module.exports = {
|
|||||||
SOLAR_FESTIVALS,
|
SOLAR_FESTIVALS,
|
||||||
LUNAR_FESTIVALS,
|
LUNAR_FESTIVALS,
|
||||||
BUDDHIST_FESTIVALS,
|
BUDDHIST_FESTIVALS,
|
||||||
|
LEGAL_HOLIDAYS_SOLAR,
|
||||||
|
LEGAL_HOLIDAYS_LUNAR,
|
||||||
|
SOLAR_TERM_TIMES,
|
||||||
|
FESTIVAL_QUICK_SEARCH,
|
||||||
NAYIN_TABLE,
|
NAYIN_TABLE,
|
||||||
HOUR_NAMES,
|
HOUR_NAMES,
|
||||||
HOUR_RANGES,
|
HOUR_RANGES,
|
||||||
|
|||||||
+195
-83
@@ -13,6 +13,10 @@ const {
|
|||||||
SOLAR_FESTIVALS,
|
SOLAR_FESTIVALS,
|
||||||
LUNAR_FESTIVALS,
|
LUNAR_FESTIVALS,
|
||||||
BUDDHIST_FESTIVALS,
|
BUDDHIST_FESTIVALS,
|
||||||
|
LEGAL_HOLIDAYS_SOLAR,
|
||||||
|
LEGAL_HOLIDAYS_LUNAR,
|
||||||
|
SOLAR_TERM_TIMES,
|
||||||
|
FESTIVAL_QUICK_SEARCH,
|
||||||
NAYIN_TABLE,
|
NAYIN_TABLE,
|
||||||
HOUR_NAMES,
|
HOUR_NAMES,
|
||||||
HOUR_RANGES,
|
HOUR_RANGES,
|
||||||
@@ -84,124 +88,107 @@ function getLunarMonthDays(year, month) {
|
|||||||
return (LUNAR_INFO[year - 1900] & (0x10000 >> month)) ? 30 : 29;
|
return (LUNAR_INFO[year - 1900] & (0x10000 >> month)) ? 30 : 29;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 公历转农历 */
|
/** 公历转农历(标准算法,基准 1900-01-31 = 农历1900年正月初一) */
|
||||||
function solarToLunar(year, month, day) {
|
function solarToLunar(year, month, day) {
|
||||||
if (year < 1900 || year > 2100) return null;
|
if (year < 1900 || year > 2100) return null;
|
||||||
|
|
||||||
let offset = 0;
|
// 与基准日 1900-01-31(农历1900年正月初一)的天数差(用 UTC 避免时区误差)
|
||||||
for (let y = 1900; y < year; y++) {
|
const baseDate = Date.UTC(1900, 0, 31);
|
||||||
offset += getLunarYearDays(y);
|
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 isLeap = false;
|
||||||
let lunarMonth = 1;
|
let lunarMonth = 1;
|
||||||
let lunarDay = 1;
|
let daysOfMonth = 0;
|
||||||
let daysInMonth = 0;
|
|
||||||
|
|
||||||
// 计算从正月初一到目标日期的天数
|
// 确定农历月日
|
||||||
let targetOffset = offset;
|
for (; lunarMonth < 13 && offset > 0; lunarMonth++) {
|
||||||
for (let m = 1; m < month; m++) {
|
// 闰月
|
||||||
targetOffset += getSolarMonthDays(year, m);
|
if (leapMonth > 0 && lunarMonth === leapMonth + 1 && !isLeap) {
|
||||||
}
|
lunarMonth--;
|
||||||
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--;
|
|
||||||
isLeap = true;
|
isLeap = true;
|
||||||
daysInMonth = getLeapMonthDays(year);
|
daysOfMonth = getLeapMonthDays(lunarYear);
|
||||||
if (remaining < daysInMonth) {
|
} else {
|
||||||
lunarMonth = m + 1;
|
daysOfMonth = getLunarMonthDays(lunarYear, lunarMonth);
|
||||||
lunarDay = remaining + 1;
|
|
||||||
isLeap = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
remaining -= daysInMonth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
year,
|
year: lunarYear,
|
||||||
month: lunarMonth,
|
month: lunarMonth,
|
||||||
day: lunarDay,
|
day: offset + 1,
|
||||||
isLeap: isLeap && lunarMonth === leapMonth
|
isLeap
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 农历转公历 */
|
/** 农历转公历(标准算法,基准 1900-01-31 = 农历1900年正月初一) */
|
||||||
function lunarToSolar(year, month, day, isLeap = false) {
|
function lunarToSolar(year, month, day, isLeap = false) {
|
||||||
if (year < 1900 || year > 2100) return null;
|
if (year < 1900 || year > 2100) return null;
|
||||||
|
|
||||||
|
// 累计从 1900 年正月初一到目标农历日期的天数
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for (let y = 1900; y < year; y++) {
|
for (let y = 1900; y < year; y++) {
|
||||||
offset += getLunarYearDays(y);
|
offset += getLunarYearDays(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
const leapMonth = getLeapMonth(year);
|
const leapMonth = getLeapMonth(year);
|
||||||
let days = 0;
|
|
||||||
|
|
||||||
|
// 累计当年 1 月到目标月前一月的 days
|
||||||
for (let m = 1; m < month; m++) {
|
for (let m = 1; m < month; m++) {
|
||||||
days += getLunarMonthDays(year, m);
|
offset += getLunarMonthDays(year, m);
|
||||||
if (m === leapMonth && !isLeap) {
|
// 经过闰月(在 m 月之后)
|
||||||
days += getLeapMonthDays(year);
|
if (m === leapMonth) {
|
||||||
|
offset += getLeapMonthDays(year);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 目标月本身是闰月:需先加上该月正常月天数
|
||||||
if (isLeap && month === leapMonth) {
|
if (isLeap && month === leapMonth) {
|
||||||
days += getLunarMonthDays(year, month);
|
offset += getLunarMonthDays(year, month);
|
||||||
}
|
}
|
||||||
|
|
||||||
days += day - 1;
|
offset += day - 1;
|
||||||
offset += days;
|
|
||||||
|
|
||||||
// 从1900年1月31日(农历正月初一)开始计算
|
// 基准日 1900-01-31 + offset
|
||||||
let solarYear = 1900;
|
const baseMs = Date.UTC(1900, 0, 31);
|
||||||
let solarMonth = 1;
|
const target = new Date(baseMs + offset * 86400000);
|
||||||
let solarDay = 31;
|
return {
|
||||||
|
year: target.getUTCFullYear(),
|
||||||
let remaining = offset;
|
month: target.getUTCMonth() + 1,
|
||||||
while (remaining > 0) {
|
day: target.getUTCDate()
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取日期的干支 */
|
/** 获取日期的干支 */
|
||||||
@@ -1109,6 +1096,124 @@ function getBuddhistFestival(lunarMonth, lunarDay) {
|
|||||||
return BUDDHIST_FESTIVALS[`${lunarMonth}-${lunarDay}`] || null;
|
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) {
|
function analyzeShensha(bazi) {
|
||||||
const result = [];
|
const result = [];
|
||||||
@@ -1245,6 +1350,13 @@ module.exports = {
|
|||||||
calculatePlumBlossom,
|
calculatePlumBlossom,
|
||||||
calculateBoneWeight,
|
calculateBoneWeight,
|
||||||
getBuddhistFestival,
|
getBuddhistFestival,
|
||||||
|
getLegalHoliday,
|
||||||
|
isWorkday,
|
||||||
|
getWeekDay,
|
||||||
|
getFestivalSolarDate,
|
||||||
|
getUpcomingFestivals,
|
||||||
|
getNextBuddhistFestival,
|
||||||
|
daysBetween,
|
||||||
analyzeShensha,
|
analyzeShensha,
|
||||||
analyzeFortuneGanzhi,
|
analyzeFortuneGanzhi,
|
||||||
getYearMonths,
|
getYearMonths,
|
||||||
|
|||||||
Reference in New Issue
Block a user