首页老黄历全面升级 + 修复农历转换核心bug
Publish Mini Program Dev Version / publish (push) Successful in 2m8s

【重要修复】重写农历双向转换算法(原来全错):
- 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:
gouki
2026-08-07 12:49:57 +00:00
parent 5d40019ea5
commit 82c1a8d694
11 changed files with 1636 additions and 425 deletions
+543 -86
View File
@@ -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',
};
},