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