【重要修复】重写农历双向转换算法(原来全错): - 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:
@@ -1,61 +1,556 @@
|
||||
const { request } = require('../../utils/request.js');
|
||||
|
||||
// 丝带颜色配置
|
||||
const RIBBON_COLORS = {
|
||||
free: ['#FF6B6B', '#FF8E8E', '#FFB6C1', '#FFC0CB'],
|
||||
paid: ['#FFD700', '#FFA500', '#FF8C00', '#DAA520'],
|
||||
};
|
||||
|
||||
Page({
|
||||
data: {
|
||||
tree: null,
|
||||
trees: [],
|
||||
currentTreeIndex: 0,
|
||||
wishes: [],
|
||||
loading: true,
|
||||
canvasWidth: 375,
|
||||
canvasHeight: 600,
|
||||
canvasHeight: 667,
|
||||
showCreateModal: false,
|
||||
wishContent: '',
|
||||
wishType: 'free', // free: 免费, paid: 付费
|
||||
wishType: 'free',
|
||||
maxFreeLength: 20,
|
||||
maxPaidLength: 100,
|
||||
products: [],
|
||||
selectedProduct: null,
|
||||
// 弹幕相关
|
||||
danmakuList: [],
|
||||
// 传感器状态
|
||||
gyroEnabled: false,
|
||||
micEnabled: false,
|
||||
windLevel: 0, // 0-1 风力等级
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadWishTree();
|
||||
this.initCanvas();
|
||||
this.loadTrees();
|
||||
this.loadProducts();
|
||||
this.initSensors();
|
||||
this.startAnimation();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.stopAnimation();
|
||||
this.stopSensors();
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadWishTree().then(() => {
|
||||
this.loadTrees().then(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
// 加载许愿树数据
|
||||
async loadWishTree() {
|
||||
// 初始化画布
|
||||
initCanvas() {
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
canvasWidth: systemInfo.windowWidth,
|
||||
canvasHeight: systemInfo.windowHeight,
|
||||
});
|
||||
},
|
||||
|
||||
// 初始化传感器
|
||||
initSensors() {
|
||||
// 陀螺仪
|
||||
wx.startGyroscope({
|
||||
interval: 'game',
|
||||
success: () => {
|
||||
this.setData({ gyroEnabled: true });
|
||||
wx.onGyroscopeChange((res) => {
|
||||
// 根据陀螺仪数据计算风力
|
||||
const wind = Math.min(1, Math.abs(res.x) * 0.5 + Math.abs(res.y) * 0.3);
|
||||
this.setData({ windLevel: wind });
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
console.log('陀螺仪不可用');
|
||||
},
|
||||
});
|
||||
|
||||
// 麦克风(需要用户授权)
|
||||
this.recorderManager = wx.getRecorderManager();
|
||||
this.recorderManager.onFrameRecorded((res) => {
|
||||
// 分析音量,模拟风力
|
||||
const frameBuffer = res.frameBuffer;
|
||||
if (frameBuffer) {
|
||||
const volume = this.analyzeVolume(frameBuffer);
|
||||
this.setData({ windLevel: Math.min(1, volume * 2) });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 分析音量
|
||||
analyzeVolume(buffer) {
|
||||
// 简单的音量分析
|
||||
const data = new Int16Array(buffer);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
sum += Math.abs(data[i]);
|
||||
}
|
||||
return sum / data.length / 32768;
|
||||
},
|
||||
|
||||
// 开启麦克风
|
||||
enableMicrophone() {
|
||||
wx.authorize({
|
||||
scope: 'scope.record',
|
||||
success: () => {
|
||||
this.recorderManager.start({
|
||||
duration: 600000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 48000,
|
||||
format: 'PCM',
|
||||
frameSize: 50,
|
||||
});
|
||||
this.setData({ micEnabled: true });
|
||||
wx.showToast({ title: '吹气让丝带飘动', icon: 'none' });
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '需要麦克风权限', icon: 'none' });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 停止传感器
|
||||
stopSensors() {
|
||||
wx.stopGyroscope();
|
||||
if (this.recorderManager) {
|
||||
this.recorderManager.stop();
|
||||
}
|
||||
},
|
||||
|
||||
// 加载许愿树列表
|
||||
async loadTrees() {
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/api/wish/tree',
|
||||
url: '/api/wish/trees',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
this.setData({
|
||||
tree: res.data.tree,
|
||||
wishes: res.data.wishes || [],
|
||||
trees: res.data.list || [],
|
||||
loading: false,
|
||||
});
|
||||
this.drawTree();
|
||||
this.loadWishes();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载许愿树失败:', err);
|
||||
this.setData({ loading: false });
|
||||
// 使用本地模拟数据
|
||||
// 使用模拟数据
|
||||
this.setData({
|
||||
tree: { id: 1, name: '祈福许愿树', maxWishes: 100 },
|
||||
wishes: this.getMockWishes(),
|
||||
trees: [
|
||||
{ id: 1, name: '祈福树', type: 'pine', maxWishes: 100 },
|
||||
{ id: 2, name: '姻缘树', type: 'sakura', maxWishes: 50 },
|
||||
{ id: 3, name: '事业树', type: 'bamboo', maxWishes: 80 },
|
||||
],
|
||||
loading: false,
|
||||
});
|
||||
this.drawTree();
|
||||
this.loadWishes();
|
||||
}
|
||||
},
|
||||
|
||||
// 加载当前树的许愿
|
||||
async loadWishes() {
|
||||
const { trees, currentTreeIndex } = this.data;
|
||||
if (!trees.length) return;
|
||||
|
||||
const tree = trees[currentTreeIndex];
|
||||
try {
|
||||
const res = await request({
|
||||
url: `/api/wish/tree/${tree.id}/wishes`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
this.setData({ wishes: res.data.list || [] });
|
||||
this.initRibbons();
|
||||
this.initDanmaku();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载许愿失败:', err);
|
||||
// 使用模拟数据
|
||||
this.setData({
|
||||
wishes: this.getMockWishes(),
|
||||
});
|
||||
this.initRibbons();
|
||||
this.initDanmaku();
|
||||
}
|
||||
},
|
||||
|
||||
// 模拟许愿数据
|
||||
getMockWishes() {
|
||||
return [
|
||||
{ id: 1, content: '愿家人平安健康', type: 'paid', createdAt: Date.now() - 86400000 },
|
||||
{ id: 2, content: '事业顺利,步步高升', type: 'free', createdAt: Date.now() - 43200000 },
|
||||
{ id: 3, content: '心想事成,万事如意', type: 'paid', createdAt: Date.now() - 21600000 },
|
||||
{ id: 4, content: '考试通过,金榜题名', type: 'free', createdAt: Date.now() - 10800000 },
|
||||
{ id: 5, content: '财源广进,富贵吉祥', type: 'paid', createdAt: Date.now() - 3600000 },
|
||||
{ id: 6, content: '身体健康,长命百岁', type: 'free', createdAt: Date.now() - 1800000 },
|
||||
{ id: 7, content: '爱情甜蜜,白头偕老', type: 'paid', createdAt: Date.now() - 900000 },
|
||||
{ id: 8, content: '出入平安,一帆风顺', type: 'free', createdAt: Date.now() - 300000 },
|
||||
];
|
||||
},
|
||||
|
||||
// 初始化丝带
|
||||
initRibbons() {
|
||||
const { wishes, canvasWidth, canvasHeight } = this.data;
|
||||
const tree = this.getCurrentTree();
|
||||
|
||||
// 根据树的类型计算丝带挂载点
|
||||
const branches = this.getTreeBranches(tree.type, canvasWidth, canvasHeight);
|
||||
|
||||
this.ribbons = wishes.map((wish, index) => {
|
||||
const branch = branches[index % branches.length];
|
||||
const colors = RIBBON_COLORS[wish.type] || RIBBON_COLORS.free;
|
||||
|
||||
return {
|
||||
id: wish.id,
|
||||
wish: wish,
|
||||
x: branch.x,
|
||||
y: branch.y,
|
||||
length: 60 + Math.random() * 40,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
swingPhase: Math.random() * Math.PI * 2,
|
||||
swingSpeed: 0.02 + Math.random() * 0.02,
|
||||
windEffect: 0.5 + Math.random() * 0.5,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 获取树枝位置
|
||||
getTreeBranches(treeType, width, height) {
|
||||
const centerX = width / 2;
|
||||
const baseY = height * 0.75;
|
||||
|
||||
// 根据树类型返回不同的树枝配置
|
||||
const configs = {
|
||||
pine: [
|
||||
{ x: centerX - 80, y: baseY - 200 },
|
||||
{ x: centerX + 60, y: baseY - 220 },
|
||||
{ x: centerX - 40, y: baseY - 280 },
|
||||
{ x: centerX + 90, y: baseY - 260 },
|
||||
{ x: centerX, y: baseY - 320 },
|
||||
{ x: centerX - 100, y: baseY - 150 },
|
||||
{ x: centerX + 100, y: baseY - 180 },
|
||||
],
|
||||
sakura: [
|
||||
{ x: centerX - 70, y: baseY - 180 },
|
||||
{ x: centerX + 80, y: baseY - 200 },
|
||||
{ x: centerX - 30, y: baseY - 250 },
|
||||
{ x: centerX + 50, y: baseY - 280 },
|
||||
{ x: centerX, y: baseY - 220 },
|
||||
],
|
||||
bamboo: [
|
||||
{ x: centerX - 60, y: baseY - 150 },
|
||||
{ x: centerX + 60, y: baseY - 180 },
|
||||
{ x: centerX - 30, y: baseY - 220 },
|
||||
{ x: centerX + 30, y: baseY - 250 },
|
||||
],
|
||||
};
|
||||
|
||||
return configs[treeType] || configs.pine;
|
||||
},
|
||||
|
||||
// 初始化弹幕
|
||||
initDanmaku() {
|
||||
const { wishes, canvasWidth } = this.data;
|
||||
|
||||
this.danmaku = wishes.map((wish, index) => ({
|
||||
id: wish.id,
|
||||
content: wish.content,
|
||||
type: wish.type,
|
||||
x: canvasWidth + Math.random() * 200,
|
||||
y: 100 + (index % 5) * 60,
|
||||
speed: 0.5 + Math.random() * 0.5,
|
||||
opacity: 0.7 + Math.random() * 0.3,
|
||||
}));
|
||||
},
|
||||
|
||||
// 开始动画循环
|
||||
startAnimation() {
|
||||
const that = this;
|
||||
this.animationTimer = setInterval(() => {
|
||||
that.draw();
|
||||
that.updateDanmaku();
|
||||
}, 1000 / 60); // 60fps
|
||||
},
|
||||
|
||||
// 停止动画
|
||||
stopAnimation() {
|
||||
if (this.animationTimer) {
|
||||
clearInterval(this.animationTimer);
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制场景
|
||||
draw() {
|
||||
const ctx = wx.createCanvasContext('wishTree', this);
|
||||
const { canvasWidth, canvasHeight, windLevel } = this.data;
|
||||
const tree = this.getCurrentTree();
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制天空
|
||||
this.drawSky(ctx, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制云朵
|
||||
this.drawClouds(ctx, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制地面
|
||||
this.drawGround(ctx, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制大树
|
||||
this.drawTree(ctx, tree, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制丝带
|
||||
this.drawRibbons(ctx, windLevel);
|
||||
|
||||
// 绘制弹幕
|
||||
this.drawDanmaku(ctx);
|
||||
|
||||
ctx.draw();
|
||||
},
|
||||
|
||||
// 绘制天空
|
||||
drawSky(ctx, width, height) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, '#1a1a2e');
|
||||
gradient.addColorStop(0.5, '#16213e');
|
||||
gradient.addColorStop(1, '#0f3460');
|
||||
|
||||
ctx.setFillStyle(gradient);
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// 绘制星星
|
||||
ctx.setFillStyle('#ffffff');
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const x = Math.random() * width;
|
||||
const y = Math.random() * height * 0.6;
|
||||
const size = Math.random() * 2;
|
||||
const opacity = 0.3 + Math.random() * 0.7;
|
||||
ctx.setGlobalAlpha(opacity);
|
||||
ctx.fillRect(x, y, size, size);
|
||||
}
|
||||
ctx.setGlobalAlpha(1);
|
||||
},
|
||||
|
||||
// 绘制云朵
|
||||
drawClouds(ctx, width, height) {
|
||||
const time = Date.now() / 1000;
|
||||
|
||||
ctx.setFillStyle('rgba(255, 255, 255, 0.1)');
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const x = ((time * 10 + i * 200) % (width + 200)) - 100;
|
||||
const y = 50 + i * 80;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 30, 0, Math.PI * 2);
|
||||
ctx.arc(x + 25, y - 10, 25, 0, Math.PI * 2);
|
||||
ctx.arc(x + 50, y, 30, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制地面
|
||||
drawGround(ctx, width, height) {
|
||||
const groundY = height * 0.75;
|
||||
|
||||
// 草地
|
||||
const gradient = ctx.createLinearGradient(0, groundY, 0, height);
|
||||
gradient.addColorStop(0, '#2d5016');
|
||||
gradient.addColorStop(1, '#1a3009');
|
||||
|
||||
ctx.setFillStyle(gradient);
|
||||
ctx.fillRect(0, groundY, width, height - groundY);
|
||||
|
||||
// 草丛
|
||||
ctx.setStrokeStyle('#3a6b1f');
|
||||
ctx.setLineWidth(2);
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const x = (i * 37) % width;
|
||||
const h = 10 + (i % 3) * 5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, groundY);
|
||||
ctx.quadraticCurveTo(x + 5, groundY - h, x + 10, groundY);
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制大树
|
||||
drawTree(ctx, tree, width, height) {
|
||||
const centerX = width / 2;
|
||||
const baseY = height * 0.75;
|
||||
|
||||
// 树干
|
||||
ctx.setFillStyle('#4a3728');
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX - 25, baseY);
|
||||
ctx.lineTo(centerX - 15, baseY - 150);
|
||||
ctx.lineTo(centerX + 15, baseY - 150);
|
||||
ctx.lineTo(centerX + 25, baseY);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 树冠 - 多层
|
||||
const layers = [
|
||||
{ radius: 120, y: baseY - 180, color: '#1e4d2b' },
|
||||
{ radius: 100, y: baseY - 220, color: '#2d6a3e' },
|
||||
{ radius: 80, y: baseY - 260, color: '#3d8b4f' },
|
||||
{ radius: 60, y: baseY - 300, color: '#4dac61' },
|
||||
];
|
||||
|
||||
layers.forEach(layer => {
|
||||
ctx.setFillStyle(layer.color);
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, layer.y, layer.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// 装饰果实
|
||||
ctx.setFillStyle('#ffd700');
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = (i / 8) * Math.PI * 2;
|
||||
const r = 60 + Math.random() * 40;
|
||||
const x = centerX + Math.cos(angle) * r;
|
||||
const y = baseY - 220 + Math.sin(angle) * r * 0.6;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制丝带
|
||||
drawRibbons(ctx, windLevel) {
|
||||
if (!this.ribbons) return;
|
||||
|
||||
const time = Date.now() / 1000;
|
||||
|
||||
this.ribbons.forEach(ribbon => {
|
||||
// 计算摆动
|
||||
const baseSwing = Math.sin(time * ribbon.swingSpeed + ribbon.swingPhase) * 0.3;
|
||||
const windSwing = Math.sin(time * 3 + ribbon.swingPhase) * windLevel * ribbon.windEffect;
|
||||
const totalSwing = baseSwing + windSwing;
|
||||
|
||||
// 丝带分段绘制,实现飘动效果
|
||||
const segments = 8;
|
||||
const segmentLength = ribbon.length / segments;
|
||||
|
||||
ctx.setStrokeStyle(ribbon.color);
|
||||
ctx.setLineWidth(4);
|
||||
ctx.setLineCap('round');
|
||||
|
||||
let prevX = ribbon.x;
|
||||
let prevY = ribbon.y;
|
||||
|
||||
for (let i = 1; i <= segments; i++) {
|
||||
const t = i / segments;
|
||||
const wave = Math.sin(time * 2 + ribbon.swingPhase + t * 3) * (5 + windLevel * 15) * t;
|
||||
const x = ribbon.x + totalSwing * i * segmentLength * 0.3 + wave;
|
||||
const y = ribbon.y + i * segmentLength;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(prevX, prevY);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
|
||||
// 丝带末端装饰
|
||||
ctx.setFillStyle(ribbon.color);
|
||||
ctx.beginPath();
|
||||
ctx.arc(prevX, prevY, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
},
|
||||
|
||||
// 绘制弹幕
|
||||
drawDanmaku(ctx) {
|
||||
if (!this.danmaku) return;
|
||||
|
||||
this.danmaku.forEach(item => {
|
||||
// 弹幕背景 - 手动绘制圆角矩形
|
||||
const padding = 10;
|
||||
const textWidth = item.content.length * 14;
|
||||
const rectX = item.x - padding;
|
||||
const rectY = item.y - 15;
|
||||
const rectW = textWidth + padding * 2;
|
||||
const rectH = 30;
|
||||
const radius = 15;
|
||||
|
||||
ctx.setFillStyle(item.type === 'paid' ? 'rgba(255, 215, 0, 0.8)' : 'rgba(255, 107, 107, 0.8)');
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(rectX + radius, rectY);
|
||||
ctx.lineTo(rectX + rectW - radius, rectY);
|
||||
ctx.arc(rectX + rectW - radius, rectY + radius, radius, -Math.PI / 2, 0);
|
||||
ctx.lineTo(rectX + rectW, rectY + rectH - radius);
|
||||
ctx.arc(rectX + rectW - radius, rectY + rectH - radius, radius, 0, Math.PI / 2);
|
||||
ctx.lineTo(rectX + radius, rectY + rectH);
|
||||
ctx.arc(rectX + radius, rectY + rectH - radius, radius, Math.PI / 2, Math.PI);
|
||||
ctx.lineTo(rectX, rectY + radius);
|
||||
ctx.arc(rectX + radius, rectY + radius, radius, Math.PI, Math.PI * 1.5);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// 弹幕文字
|
||||
ctx.setFillStyle('#ffffff');
|
||||
ctx.setFontSize(14);
|
||||
ctx.setTextAlign('left');
|
||||
ctx.setTextBaseline('middle');
|
||||
ctx.fillText(item.content, item.x, item.y);
|
||||
});
|
||||
},
|
||||
|
||||
// 更新弹幕位置
|
||||
updateDanmaku() {
|
||||
const { canvasWidth } = this.data;
|
||||
|
||||
if (!this.danmaku) return;
|
||||
|
||||
this.danmaku.forEach(item => {
|
||||
item.x -= item.speed;
|
||||
if (item.x < -200) {
|
||||
item.x = canvasWidth + Math.random() * 100;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 获取当前树
|
||||
getCurrentTree() {
|
||||
const { trees, currentTreeIndex } = this.data;
|
||||
return trees[currentTreeIndex] || { type: 'pine' };
|
||||
},
|
||||
|
||||
// 切换树
|
||||
switchTree(e) {
|
||||
const direction = e.currentTarget.dataset.direction;
|
||||
const { trees, currentTreeIndex } = this.data;
|
||||
|
||||
let newIndex = currentTreeIndex;
|
||||
if (direction === 'prev') {
|
||||
newIndex = (currentTreeIndex - 1 + trees.length) % trees.length;
|
||||
} else {
|
||||
newIndex = (currentTreeIndex + 1) % trees.length;
|
||||
}
|
||||
|
||||
this.setData({ currentTreeIndex: newIndex });
|
||||
this.loadWishes();
|
||||
},
|
||||
|
||||
// 加载许愿商品
|
||||
async loadProducts() {
|
||||
try {
|
||||
@@ -69,73 +564,16 @@ Page({
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载商品失败:', err);
|
||||
// 使用默认商品
|
||||
this.setData({
|
||||
products: [
|
||||
{ id: 1, name: '普通许愿条', price: 100, duration: 7, position: 0 },
|
||||
{ id: 2, name: '精品许愿条', price: 500, duration: 30, position: 10 },
|
||||
{ id: 3, name: '至尊许愿条', price: 2000, duration: 90, position: 100 },
|
||||
{ id: 1, name: '普通许愿条', price: 100, duration: 7 },
|
||||
{ id: 2, name: '精品许愿条', price: 500, duration: 30 },
|
||||
{ id: 3, name: '至尊许愿条', price: 2000, duration: 90 },
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 模拟许愿数据
|
||||
getMockWishes() {
|
||||
return [
|
||||
{ id: 1, content: '愿家人平安健康', type: 'paid', position: 10, isRobot: false },
|
||||
{ id: 2, content: '事业顺利', type: 'free', position: 5, isRobot: false },
|
||||
{ id: 3, content: '心想事成', type: 'paid', position: 20, isRobot: true },
|
||||
{ id: 4, content: '考试通过', type: 'free', position: 3, isRobot: false },
|
||||
{ id: 5, content: '财源广进', type: 'paid', position: 15, isRobot: false },
|
||||
];
|
||||
},
|
||||
|
||||
// 绘制许愿树
|
||||
drawTree() {
|
||||
const ctx = wx.createCanvasContext('wishTree', this);
|
||||
const { canvasWidth, canvasHeight, wishes } = this.data;
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 绘制树干
|
||||
ctx.setFillStyle('#8B4513');
|
||||
ctx.fillRect(canvasWidth / 2 - 20, canvasHeight - 150, 40, 150);
|
||||
|
||||
// 绘制树冠(圆形)
|
||||
ctx.setFillStyle('#228B22');
|
||||
ctx.beginPath();
|
||||
ctx.arc(canvasWidth / 2, canvasHeight - 200, 120, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
// 绘制许愿条
|
||||
wishes.forEach((wish, index) => {
|
||||
const angle = (index / wishes.length) * 2 * Math.PI;
|
||||
const radius = 80 + Math.random() * 40;
|
||||
const x = canvasWidth / 2 + radius * Math.cos(angle);
|
||||
const y = canvasHeight - 200 + radius * Math.sin(angle);
|
||||
|
||||
// 根据类型设置颜色
|
||||
if (wish.type === 'paid') {
|
||||
ctx.setFillStyle('#FFD700'); // 金色 - 付费
|
||||
} else {
|
||||
ctx.setFillStyle('#FF6B6B'); // 红色 - 免费
|
||||
}
|
||||
|
||||
// 绘制许愿条(小矩形)
|
||||
ctx.fillRect(x - 15, y - 5, 30, 10);
|
||||
|
||||
// 绘制文字
|
||||
ctx.setFillStyle('#FFFFFF');
|
||||
ctx.setFontSize(8);
|
||||
ctx.setTextAlign('center');
|
||||
ctx.fillText(wish.content.substring(0, 4), x, y + 3);
|
||||
});
|
||||
|
||||
ctx.draw();
|
||||
},
|
||||
|
||||
// 打开许愿弹窗
|
||||
openCreateModal() {
|
||||
this.setData({ showCreateModal: true });
|
||||
@@ -171,28 +609,25 @@ Page({
|
||||
|
||||
// 提交许愿
|
||||
async submitWish() {
|
||||
const { wishContent, wishType, selectedProduct } = this.data;
|
||||
const { wishContent, wishType, selectedProduct, trees, currentTreeIndex } = this.data;
|
||||
|
||||
if (!wishContent.trim()) {
|
||||
wx.showToast({ title: '请输入许愿内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查字数限制
|
||||
const maxLength = wishType === 'free' ? this.data.maxFreeLength : this.data.maxPaidLength;
|
||||
if (wishContent.length > maxLength) {
|
||||
wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 付费许愿需要选择商品
|
||||
if (wishType === 'paid' && !selectedProduct) {
|
||||
wx.showToast({ title: '请选择许愿商品', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果是付费许愿,先创建订单
|
||||
if (wishType === 'paid') {
|
||||
const orderRes = await request({
|
||||
url: '/api/pay/create',
|
||||
@@ -201,29 +636,28 @@ Page({
|
||||
type: 'wish',
|
||||
productId: selectedProduct.id,
|
||||
content: wishContent,
|
||||
treeId: trees[currentTreeIndex].id,
|
||||
},
|
||||
});
|
||||
|
||||
if (orderRes.code === 0) {
|
||||
// 调起微信支付
|
||||
await this.wxPay(orderRes.data);
|
||||
}
|
||||
} else {
|
||||
// 免费许愿直接创建
|
||||
const res = await request({
|
||||
url: '/api/wish/create',
|
||||
method: 'POST',
|
||||
data: {
|
||||
content: wishContent,
|
||||
type: 'free',
|
||||
treeId: this.data.tree.id,
|
||||
treeId: trees[currentTreeIndex].id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
wx.showToast({ title: '许愿成功', icon: 'success' });
|
||||
this.closeCreateModal();
|
||||
this.loadWishTree();
|
||||
this.loadWishes();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -244,7 +678,7 @@ Page({
|
||||
success: () => {
|
||||
wx.showToast({ title: '支付成功', icon: 'success' });
|
||||
this.closeCreateModal();
|
||||
this.loadWishTree();
|
||||
this.loadWishes();
|
||||
resolve();
|
||||
},
|
||||
fail: (err) => {
|
||||
@@ -256,6 +690,28 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
// 画布触摸事件
|
||||
onCanvasTouch(e) {
|
||||
const { x, y } = e.touches[0];
|
||||
|
||||
// 检测是否点击到丝带
|
||||
if (this.ribbons) {
|
||||
for (let ribbon of this.ribbons) {
|
||||
const dx = x - ribbon.x;
|
||||
const dy = y - ribbon.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 50) {
|
||||
// 点击到丝带,显示详情
|
||||
wx.navigateTo({
|
||||
url: `/pages/wish-detail/wish-detail?id=${ribbon.id}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 查看许愿详情
|
||||
viewWishDetail(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
@@ -266,8 +722,9 @@ Page({
|
||||
|
||||
// 分享
|
||||
onShareAppMessage() {
|
||||
const tree = this.getCurrentTree();
|
||||
return {
|
||||
title: '快来许愿树许下你的愿望吧!',
|
||||
title: `快来${tree.name || '许愿树'}许下你的愿望吧!`,
|
||||
path: '/pages/wish-tree/wish-tree',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
<view class="container">
|
||||
<!-- 许愿树画布 -->
|
||||
<view class="canvas-container">
|
||||
<canvas
|
||||
canvas-id="wishTree"
|
||||
class="wish-tree-canvas"
|
||||
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
||||
></canvas>
|
||||
<!-- 全屏画布 -->
|
||||
<canvas
|
||||
canvas-id="wishTree"
|
||||
class="wish-tree-canvas"
|
||||
style="width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
|
||||
bindtouchstart="onCanvasTouch"
|
||||
></canvas>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="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
|
||||
wx:for="{{wishes}}"
|
||||
wx:key="id"
|
||||
class="wish-tag {{item.type}} {{item.isRobot ? 'robot' : ''}}"
|
||||
style="left: {{item.x}}px; top: {{item.y}}px;"
|
||||
bindtap="viewWishDetail"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="wish-text">{{item.content}}</text>
|
||||
<view class="wish-badge" wx:if="{{item.type === 'paid'}}">VIP</view>
|
||||
<!-- 传感器控制 -->
|
||||
<view class="sensor-controls">
|
||||
<view class="sensor-btn {{micEnabled ? 'active' : ''}}" bindtap="enableMicrophone">
|
||||
<text class="sensor-icon">🎤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 许愿按钮 -->
|
||||
<view class="action-bar">
|
||||
<button class="btn-primary btn-wish" bindtap="openCreateModal">
|
||||
<button class="btn-wish" bindtap="openCreateModal">
|
||||
<text class="icon">🙏</text>
|
||||
<text>我要许愿</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 许愿说明 -->
|
||||
<view class="tips-section">
|
||||
<view class="tip-item">
|
||||
<text class="tip-icon">🎋</text>
|
||||
<text class="tip-text">免费许愿:20字以内,可能被覆盖</text>
|
||||
</view>
|
||||
<view class="tip-item">
|
||||
<text class="tip-icon">✨</text>
|
||||
<text class="tip-text">付费许愿:100字以内,优先展示,长期保留</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 许愿弹窗 -->
|
||||
<view class="modal" wx:if="{{showCreateModal}}">
|
||||
<view class="modal-mask" bindtap="closeCreateModal"></view>
|
||||
|
||||
@@ -1,127 +1,121 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #87CEEB 0%, #E0F6FF 100%);
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 画布容器 */
|
||||
.canvas-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 800rpx;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #0f3460;
|
||||
}
|
||||
|
||||
/* 全屏画布 */
|
||||
.wish-tree-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 许愿条覆盖层 */
|
||||
.wishes-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wish-tag {
|
||||
/* 顶部导航 */
|
||||
.top-bar {
|
||||
position: absolute;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
pointer-events: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.2);
|
||||
max-width: 200rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wish-tag.free {
|
||||
background: linear-gradient(135deg, #FF6B6B, #FF8E8E);
|
||||
}
|
||||
|
||||
.wish-tag.paid {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
}
|
||||
|
||||
.wish-tag.robot {
|
||||
border: 2rpx dashed #fff;
|
||||
}
|
||||
|
||||
.wish-text {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.wish-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
background: #FF4500;
|
||||
color: #fff;
|
||||
font-size: 16rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx 30rpx;
|
||||
background: rgba(255,255,255,0.95);
|
||||
box-shadow: 0 -2rpx 20rpx rgba(0,0,0,0.1);
|
||||
padding: 80rpx 30rpx 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tree-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 40rpx;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tree-name {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
min-width: 150rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 传感器控制 */
|
||||
.sensor-controls {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.sensor-btn {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sensor-btn.active {
|
||||
background: rgba(255, 215, 0, 0.5);
|
||||
box-shadow: 0 0 20rpx rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
.sensor-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
/* 许愿按钮 */
|
||||
.action-bar {
|
||||
position: absolute;
|
||||
bottom: 60rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.btn-wish {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
gap: 12rpx;
|
||||
background: linear-gradient(135deg, #C41E3A, #E74C3C);
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
padding: 24rpx;
|
||||
padding: 24rpx 60rpx;
|
||||
border-radius: 50rpx;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 30rpx rgba(196, 30, 58, 0.4);
|
||||
}
|
||||
|
||||
.btn-wish .icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
/* 提示区域 */
|
||||
.tips-section {
|
||||
padding: 30rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx;
|
||||
background: rgba(255,255,255,0.8);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
@@ -138,7 +132,8 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@@ -191,6 +186,7 @@
|
||||
border: 2rpx solid #eee;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.type-item.active {
|
||||
@@ -254,6 +250,7 @@
|
||||
padding: 24rpx;
|
||||
border: 2rpx solid #eee;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.product-item.active {
|
||||
|
||||
Reference in New Issue
Block a user