Files
lunar-mini/mini/pages/wish-tree/wish-tree.js
T
gouki 97c0d4d4e1
Publish Mini Program Dev Version / publish (push) Successful in 54s
fix(wish-tree): 重新绘制大树,更自然的树形
- 松树:贝塞尔曲线树干、树皮纹理、不规则树冠、松针细节、松果
- 樱花树:纤细树干、粉色系树冠、樱花花瓣
- 竹子:多根竹竿、竹节、竹叶
- 添加树枝绘制函数,曲线更自然
- 更新树枝挂载点位置
2026-08-07 18:59:01 +00:00

963 lines
27 KiB
JavaScript

const { request } = require('../../utils/request.js');
// 丝带颜色配置
const RIBBON_COLORS = {
free: ['#FF6B6B', '#FF8E8E', '#FFB6C1', '#FFC0CB'],
paid: ['#FFD700', '#FFA500', '#FF8C00', '#DAA520'],
};
Page({
data: {
trees: [],
currentTreeIndex: 0,
wishes: [],
loading: true,
canvasWidth: 375,
canvasHeight: 667,
showCreateModal: false,
wishContent: '',
wishType: 'free',
maxFreeLength: 20,
maxPaidLength: 100,
products: [],
selectedProduct: null,
// 弹幕相关
danmakuList: [],
// 传感器状态
gyroEnabled: false,
micEnabled: false,
windLevel: 0, // 0-1 风力等级
},
onLoad() {
this.initCanvas();
this.loadTrees();
this.loadProducts();
this.initSensors();
this.startAnimation();
},
onUnload() {
this.stopAnimation();
this.stopSensors();
},
onPullDownRefresh() {
this.loadTrees().then(() => {
wx.stopPullDownRefresh();
});
},
// 初始化画布
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/trees',
method: 'GET',
});
if (res.code === 0) {
this.setData({
trees: res.data.list || [],
loading: false,
});
this.loadWishes();
}
} catch (err) {
console.error('加载许愿树失败:', err);
// 使用模拟数据
this.setData({
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.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 - 60, y: baseY - 180 },
{ x: centerX + 55, y: baseY - 190 },
{ x: centerX - 35, y: baseY - 220 },
{ x: centerX + 40, y: baseY - 230 },
{ x: centerX, y: baseY - 250 },
{ x: centerX - 80, y: baseY - 150 },
{ x: centerX + 75, y: baseY - 160 },
{ x: centerX - 20, y: baseY - 200 },
{ x: centerX + 25, y: baseY - 210 },
],
sakura: [
{ x: centerX - 70, y: baseY - 180 },
{ x: centerX + 65, y: baseY - 190 },
{ x: centerX - 45, y: baseY - 220 },
{ x: centerX + 50, y: baseY - 230 },
{ x: centerX, y: baseY - 250 },
{ x: centerX - 30, y: baseY - 200 },
{ x: centerX + 35, y: baseY - 210 },
],
bamboo: [
{ x: centerX - 30, y: baseY - 160 },
{ x: centerX, y: baseY - 200 },
{ x: centerX + 30, y: baseY - 140 },
{ x: centerX - 25, y: baseY - 120 },
{ x: centerX + 25, y: baseY - 180 },
],
};
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;
// 根据树类型绘制不同风格的树
const treeType = tree.type || 'pine';
if (treeType === 'sakura') {
this.drawSakuraTree(ctx, centerX, baseY);
} else if (treeType === 'bamboo') {
this.drawBambooTree(ctx, centerX, baseY);
} else {
this.drawPineTree(ctx, centerX, baseY);
}
},
// 绘制松树(祈福树)
drawPineTree(ctx, centerX, baseY) {
// 树干 - 用贝塞尔曲线画自然弯曲
ctx.setFillStyle('#3d2914');
ctx.beginPath();
ctx.moveTo(centerX - 20, baseY);
// 左边缘曲线
ctx.bezierCurveTo(
centerX - 18, baseY - 50,
centerX - 12, baseY - 100,
centerX - 8, baseY - 140
);
// 树干顶部
ctx.lineTo(centerX + 8, baseY - 140);
// 右边缘曲线
ctx.bezierCurveTo(
centerX + 12, baseY - 100,
centerX + 18, baseY - 50,
centerX + 20, baseY
);
ctx.closePath();
ctx.fill();
// 树皮纹理
ctx.setStrokeStyle('#2a1d0f');
ctx.setLineWidth(1);
for (let i = 0; i < 5; i++) {
const y = baseY - 20 - i * 30;
ctx.beginPath();
ctx.moveTo(centerX - 15 + i * 2, y);
ctx.quadraticCurveTo(centerX, y - 10, centerX + 15 - i * 2, y);
ctx.stroke();
}
// 主枝干
this.drawBranch(ctx, centerX, baseY - 140, -60, 80, 3);
this.drawBranch(ctx, centerX, baseY - 140, 60, 80, 3);
this.drawBranch(ctx, centerX, baseY - 160, -30, 60, 2);
this.drawBranch(ctx, centerX, baseY - 160, 30, 60, 2);
this.drawBranch(ctx, centerX, baseY - 180, 0, 50, 2);
// 树冠 - 用多个不规则圆形叠加,模拟松针层次
const crownLayers = [
{ offsetY: -200, radiusX: 100, radiusY: 60, color: '#1a3d1f', alpha: 0.9 },
{ offsetY: -240, radiusX: 85, radiusY: 55, color: '#2d5a2d', alpha: 0.85 },
{ offsetY: -280, radiusX: 70, radiusY: 50, color: '#3d7a3d', alpha: 0.8 },
{ offsetY: -320, radiusX: 55, radiusY: 45, color: '#4d9a4d', alpha: 0.75 },
{ offsetY: -350, radiusX: 40, radiusY: 35, color: '#5dba5d', alpha: 0.7 },
];
crownLayers.forEach(layer => {
ctx.save();
ctx.setGlobalAlpha(layer.alpha);
ctx.setFillStyle(layer.color);
// 绘制不规则椭圆形
ctx.beginPath();
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) {
const noise = Math.sin(angle * 5) * 8 + Math.cos(angle * 3) * 5;
const rx = layer.radiusX + noise;
const ry = layer.radiusY + noise * 0.6;
const x = centerX + Math.cos(angle) * rx;
const y = baseY + layer.offsetY + Math.sin(angle) * ry;
if (angle === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
});
// 松针细节
ctx.setStrokeStyle('#6dca6d');
ctx.setLineWidth(1.5);
for (let i = 0; i < 30; i++) {
const angle = (i / 30) * Math.PI * 2;
const r = 50 + Math.random() * 40;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 280 + Math.sin(angle) * r * 0.5;
const len = 8 + Math.random() * 8;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + Math.cos(angle) * len, y + Math.sin(angle) * len);
ctx.stroke();
}
// 装饰果实(松果)
ctx.setFillStyle('#8b6914');
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2 + 0.5;
const r = 45 + Math.random() * 30;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 260 + Math.sin(angle) * r * 0.4;
// 画松果形状
ctx.beginPath();
ctx.ellipse(x, y, 5, 8, angle, 0, Math.PI * 2);
ctx.fill();
}
},
// 绘制樱花树(姻缘树)
drawSakuraTree(ctx, centerX, baseY) {
// 树干 - 更纤细优雅
ctx.setFillStyle('#4a3728');
ctx.beginPath();
ctx.moveTo(centerX - 12, baseY);
ctx.bezierCurveTo(
centerX - 10, baseY - 60,
centerX - 6, baseY - 120,
centerX - 4, baseY - 160
);
ctx.lineTo(centerX + 4, baseY - 160);
ctx.bezierCurveTo(
centerX + 6, baseY - 120,
centerX + 10, baseY - 60,
centerX + 12, baseY
);
ctx.closePath();
ctx.fill();
// 主枝干 - 更分散
this.drawBranch(ctx, centerX, baseY - 160, -70, 90, 2);
this.drawBranch(ctx, centerX, baseY - 160, 70, 90, 2);
this.drawBranch(ctx, centerX, baseY - 180, -40, 70, 2);
this.drawBranch(ctx, centerX, baseY - 180, 40, 70, 2);
this.drawBranch(ctx, centerX, baseY - 200, 0, 60, 2);
// 樱花树冠 - 粉色系
const sakuraLayers = [
{ offsetY: -220, radiusX: 90, radiusY: 50, color: '#ffb7c5', alpha: 0.6 },
{ offsetY: -260, radiusX: 75, radiusY: 45, color: '#ffc9d4', alpha: 0.55 },
{ offsetY: -300, radiusX: 60, radiusY: 40, color: '#ffdbe3', alpha: 0.5 },
{ offsetY: -330, radiusX: 45, radiusY: 35, color: '#ffedf2', alpha: 0.45 },
];
sakuraLayers.forEach(layer => {
ctx.save();
ctx.setGlobalAlpha(layer.alpha);
ctx.setFillStyle(layer.color);
ctx.beginPath();
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) {
const noise = Math.sin(angle * 7) * 10 + Math.cos(angle * 4) * 6;
const rx = layer.radiusX + noise;
const ry = layer.radiusY + noise * 0.5;
const x = centerX + Math.cos(angle) * rx;
const y = baseY + layer.offsetY + Math.sin(angle) * ry;
if (angle === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
});
// 樱花花瓣
ctx.setFillStyle('#fff0f5');
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 40 + Math.random() * 50;
const x = centerX + Math.cos(angle) * r;
const y = baseY - 260 + Math.sin(angle) * r * 0.5;
const size = 3 + Math.random() * 4;
// 画花瓣形状
ctx.beginPath();
ctx.ellipse(x, y, size, size * 1.5, angle, 0, Math.PI * 2);
ctx.fill();
}
},
// 绘制竹子(事业树)
drawBambooTree(ctx, centerX, baseY) {
// 竹竿 - 多根
const bambooPositions = [
{ offsetX: -30, height: 200 },
{ offsetX: 0, height: 240 },
{ offsetX: 30, height: 180 },
];
bambooPositions.forEach(pos => {
const x = centerX + pos.offsetX;
const topY = baseY - pos.height;
// 竹竿主体
ctx.setFillStyle('#5a8a3a');
ctx.beginPath();
ctx.moveTo(x - 6, baseY);
ctx.lineTo(x - 5, topY);
ctx.lineTo(x + 5, topY);
ctx.lineTo(x + 6, baseY);
ctx.closePath();
ctx.fill();
// 竹节
ctx.setStrokeStyle('#4a7a2a');
ctx.setLineWidth(2);
for (let y = baseY - 40; y > topY; y -= 40) {
ctx.beginPath();
ctx.moveTo(x - 6, y);
ctx.lineTo(x + 6, y);
ctx.stroke();
}
// 竹叶
ctx.setFillStyle('#6aaa4a');
for (let i = 0; i < 8; i++) {
const leafY = topY + 20 + i * 20;
const side = i % 2 === 0 ? 1 : -1;
ctx.beginPath();
ctx.moveTo(x, leafY);
ctx.quadraticCurveTo(
x + side * 25, leafY - 10,
x + side * 40, leafY + 5
);
ctx.quadraticCurveTo(
x + side * 25, leafY + 15,
x, leafY + 10
);
ctx.closePath();
ctx.fill();
}
});
},
// 绘制树枝
drawBranch(ctx, startX, startY, angle, length, width) {
const rad = (angle * Math.PI) / 180;
const endX = startX + Math.sin(rad) * length;
const endY = startY - Math.cos(rad) * length;
ctx.setStrokeStyle('#3d2914');
ctx.setLineWidth(width);
ctx.setLineCap('round');
ctx.beginPath();
ctx.moveTo(startX, startY);
// 用曲线让树枝更自然
const midX = startX + Math.sin(rad) * length * 0.5 + (Math.random() - 0.5) * 10;
const midY = startY - Math.cos(rad) * length * 0.5;
ctx.quadraticCurveTo(midX, midY, endX, endY);
ctx.stroke();
},
// 绘制丝带
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 {
const res = await request({
url: '/api/wish/products',
method: 'GET',
});
if (res.code === 0) {
this.setData({ products: res.data.list || [] });
}
} catch (err) {
console.error('加载商品失败:', err);
this.setData({
products: [
{ id: 1, name: '普通许愿条', price: 100, duration: 7 },
{ id: 2, name: '精品许愿条', price: 500, duration: 30 },
{ id: 3, name: '至尊许愿条', price: 2000, duration: 90 },
],
});
}
},
// 打开许愿弹窗
openCreateModal() {
this.setData({ showCreateModal: true });
},
// 关闭许愿弹窗
closeCreateModal() {
this.setData({
showCreateModal: false,
wishContent: '',
wishType: 'free',
selectedProduct: null,
});
},
// 输入许愿内容
onInputContent(e) {
this.setData({ wishContent: e.detail.value });
},
// 选择许愿类型
selectType(e) {
const type = e.currentTarget.dataset.type;
this.setData({ wishType: type });
},
// 选择商品
selectProduct(e) {
const id = e.currentTarget.dataset.id;
const product = this.data.products.find(p => p.id === id);
this.setData({ selectedProduct: product });
},
// 提交许愿
async submitWish() {
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',
method: 'POST',
data: {
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: trees[currentTreeIndex].id,
},
});
if (res.code === 0) {
wx.showToast({ title: '许愿成功', icon: 'success' });
this.closeCreateModal();
this.loadWishes();
}
}
} catch (err) {
console.error('许愿失败:', err);
wx.showToast({ title: '许愿失败,请重试', icon: 'none' });
}
},
// 微信支付
async wxPay(orderData) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: orderData.timeStamp,
nonceStr: orderData.nonceStr,
package: orderData.package,
signType: orderData.signType,
paySign: orderData.paySign,
success: () => {
wx.showToast({ title: '支付成功', icon: 'success' });
this.closeCreateModal();
this.loadWishes();
resolve();
},
fail: (err) => {
console.error('支付失败:', err);
wx.showToast({ title: '支付失败', icon: 'none' });
reject(err);
},
});
});
},
// 画布触摸事件
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;
wx.navigateTo({
url: `/pages/wish-detail/wish-detail?id=${id}`,
});
},
// 分享
onShareAppMessage() {
const tree = this.getCurrentTree();
return {
title: `快来${tree.name || '许愿树'}许下你的愿望吧!`,
path: '/pages/wish-tree/wish-tree',
};
},
});