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.safeInitRibbons(); this.safeInitDanmaku(); } } catch (err) { console.error('加载许愿失败:', err); // 使用模拟数据 this.setData({ wishes: this.getMockWishes(), }); this.safeInitRibbons(); this.safeInitDanmaku(); } }, // 安全初始化丝带 safeInitRibbons() { try { this.initRibbons(); } catch (err) { console.error('初始化丝带失败:', err); this.ribbons = []; } }, // 安全初始化弹幕 safeInitDanmaku() { try { this.initDanmaku(); } catch (err) { console.error('初始化弹幕失败:', err); this.danmaku = []; } }, // 模拟许愿数据 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.78; // 根据树类型返回不同的树枝配置 const configs = { pine: [ { x: centerX - 70, y: baseY - 260 }, { x: centerX + 65, y: baseY - 270 }, { x: centerX - 45, y: baseY - 300 }, { x: centerX + 50, y: baseY - 310 }, { x: centerX, y: baseY - 330 }, { x: centerX - 85, y: baseY - 220 }, { x: centerX + 80, y: baseY - 230 }, { x: centerX - 25, y: baseY - 280 }, { x: centerX + 30, y: baseY - 290 }, { x: centerX - 55, y: baseY - 340 }, { x: centerX + 60, y: baseY - 350 }, ], sakura: [ { x: centerX - 60, y: baseY - 240 }, { x: centerX + 55, y: baseY - 250 }, { x: centerX - 40, y: baseY - 280 }, { x: centerX + 45, y: baseY - 290 }, { x: centerX, y: baseY - 310 }, { x: centerX - 30, y: baseY - 260 }, { x: centerX + 35, y: baseY - 270 }, ], bamboo: [ { x: centerX - 40, y: baseY - 100 }, { x: centerX - 15, y: baseY - 140 }, { x: centerX + 10, y: baseY - 120 }, { x: centerX + 35, y: baseY - 80 }, { x: centerX - 40, y: baseY - 160 }, { x: centerX - 15, y: baseY - 200 }, { x: centerX + 10, y: baseY - 180 }, { x: centerX + 35, y: baseY - 140 }, ], }; 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.78; // 根据树类型绘制不同风格的树 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) { const time = Date.now() / 1000; // 地面装饰 - 草丛 this.drawGrass(ctx, centerX, baseY); // 粗壮树干 - 参考第三张图的扭曲感 ctx.setFillStyle('#5a3a2a'); ctx.beginPath(); ctx.moveTo(centerX - 30, baseY); // 左边缘 - 扭曲向上 ctx.bezierCurveTo( centerX - 35, baseY - 40, centerX - 25, baseY - 80, centerX - 20, baseY - 120 ); ctx.bezierCurveTo( centerX - 15, baseY - 160, centerX - 10, baseY - 180, centerX - 5, baseY - 200 ); // 顶部 ctx.lineTo(centerX + 5, baseY - 200); // 右边缘 ctx.bezierCurveTo( centerX + 10, baseY - 180, centerX + 15, baseY - 160, centerX + 20, baseY - 120 ); ctx.bezierCurveTo( centerX + 25, baseY - 80, centerX + 35, baseY - 40, centerX + 30, baseY ); ctx.closePath(); ctx.fill(); // 树干纹理 - 螺旋感 ctx.setStrokeStyle('#4a2a1a'); ctx.setLineWidth(2); for (let i = 0; i < 6; i++) { const y = baseY - 30 - i * 35; const offset = Math.sin(i * 0.8) * 8; ctx.beginPath(); ctx.moveTo(centerX - 20 + offset, y); ctx.quadraticCurveTo( centerX + offset, y - 15, centerX + 20 + offset, y ); ctx.stroke(); } // 主枝干 - 曲线分散,参考第一张图 const branches = [ { angle: -70, length: 100, width: 4, curl: 20 }, { angle: 70, length: 100, width: 4, curl: -20 }, { angle: -45, length: 90, width: 3, curl: 15 }, { angle: 45, length: 90, width: 3, curl: -15 }, { angle: -20, length: 80, width: 3, curl: 10 }, { angle: 20, length: 80, width: 3, curl: -10 }, { angle: 0, length: 70, width: 2, curl: 0 }, ]; branches.forEach(b => { this.drawCurlyBranch(ctx, centerX, baseY - 200, b.angle, b.length, b.width, b.curl); }); // 树冠 - 多层蓬松感,参考第一张图 const crownLayers = [ { offsetY: -280, radiusX: 110, radiusY: 70, color: '#2a5a1a', alpha: 0.85 }, { offsetY: -320, radiusX: 95, radiusY: 65, color: '#3a7a2a', alpha: 0.8 }, { offsetY: -360, radiusX: 80, radiusY: 60, color: '#4a9a3a', alpha: 0.75 }, { offsetY: -400, radiusX: 65, radiusY: 50, color: '#5aba4a', alpha: 0.7 }, { offsetY: -430, radiusX: 50, radiusY: 40, color: '#6ada5a', alpha: 0.65 }, ]; crownLayers.forEach((layer, idx) => { ctx.save(); ctx.setGlobalAlpha(layer.alpha); ctx.setFillStyle(layer.color); // 蓬松不规则形状 ctx.beginPath(); for (let angle = 0; angle < Math.PI * 2; angle += 0.08) { const noise = Math.sin(angle * 6 + idx) * 12 + Math.cos(angle * 4 + idx * 2) * 8; 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('#3a6a2a'); ctx.setLineWidth(2); for (let i = 0; i < 12; i++) { const angle = (i / 12) * Math.PI * 2; const startR = 60; const startX = centerX + Math.cos(angle) * startR; const startY = baseY - 320 + Math.sin(angle) * startR * 0.5; ctx.beginPath(); ctx.moveTo(startX, startY); // 卷曲线条 for (let t = 0; t < 1; t += 0.1) { const r = startR + t * 40; const a = angle + t * 2; const x = centerX + Math.cos(a) * r; const y = baseY - 320 + Math.sin(a) * r * 0.5 - t * 30; ctx.lineTo(x, y); } ctx.stroke(); } // 彩色装饰物 - 参考第一张图的彩色叶子/果实 const decorations = [ { color: '#ff6b6b', size: 8 }, // 红 { color: '#ffd93d', size: 7 }, // 黄 { color: '#6bcf7f', size: 7 }, // 绿 { color: '#4d96ff', size: 6 }, // 蓝 { color: '#ff9f43', size: 7 }, // 橙 { color: '#a55eea', size: 6 }, // 紫 ]; for (let i = 0; i < 25; i++) { const dec = decorations[i % decorations.length]; const angle = (i / 25) * Math.PI * 2 + Math.random() * 0.5; const r = 50 + Math.random() * 60; const x = centerX + Math.cos(angle) * r; const y = baseY - 350 + Math.sin(angle) * r * 0.6 + Math.sin(time * 2 + i) * 3; ctx.setFillStyle(dec.color); ctx.beginPath(); // 画叶子形状 ctx.ellipse(x, y, dec.size, dec.size * 1.5, angle, 0, Math.PI * 2); ctx.fill(); // 高光 ctx.setFillStyle('rgba(255,255,255,0.3)'); ctx.beginPath(); ctx.ellipse(x - 2, y - 2, dec.size * 0.3, dec.size * 0.5, angle, 0, Math.PI * 2); ctx.fill(); } // 发光效果 - 参考第三张图 ctx.save(); ctx.setGlobalAlpha(0.15 + Math.sin(time) * 0.05); const glowGradient = ctx.createRadialGradient( centerX, baseY - 350, 0, centerX, baseY - 350, 150 ); glowGradient.addColorStop(0, '#ffff00'); glowGradient.addColorStop(0.5, '#88ff00'); glowGradient.addColorStop(1, 'transparent'); ctx.setFillStyle(glowGradient); ctx.beginPath(); ctx.arc(centerX, baseY - 350, 150, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }, // 绘制草丛 drawGrass(ctx, centerX, baseY) { ctx.setStrokeStyle('#3a6a2a'); ctx.setLineWidth(2); for (let i = 0; i < 15; i++) { const x = centerX - 100 + i * 15; const h = 15 + Math.random() * 20; const sway = Math.sin(Date.now() / 500 + i) * 3; ctx.beginPath(); ctx.moveTo(x, baseY); ctx.quadraticCurveTo(x + sway, baseY - h * 0.5, x + sway * 2, baseY - h); ctx.stroke(); } }, // 绘制卷曲树枝 drawCurlyBranch(ctx, startX, startY, angle, length, width, curl) { const rad = (angle * Math.PI) / 180; const endX = startX + Math.sin(rad) * length; const endY = startY - Math.cos(rad) * length; ctx.setStrokeStyle('#5a3a2a'); ctx.setLineWidth(width); ctx.setLineCap('round'); ctx.beginPath(); ctx.moveTo(startX, startY); // 三次贝塞尔曲线,带卷曲 const cp1x = startX + Math.sin(rad) * length * 0.3 + curl; const cp1y = startY - Math.cos(rad) * length * 0.3; const cp2x = startX + Math.sin(rad) * length * 0.7 - curl; const cp2y = startY - Math.cos(rad) * length * 0.7; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY); ctx.stroke(); // 小分支 if (width > 2) { const midX = (startX + endX) / 2; const midY = (startY + endY) / 2; this.drawCurlyBranch(ctx, midX, midY, angle - 30, length * 0.5, width - 1, curl * 0.5); this.drawCurlyBranch(ctx, midX, midY, angle + 30, length * 0.5, width - 1, curl * 0.5); } }, // 绘制姻缘树(樱花树) drawSakuraTree(ctx, centerX, baseY) { const time = Date.now() / 1000; // 地面装饰 this.drawGrass(ctx, centerX, baseY); // 纤细树干 ctx.setFillStyle('#4a3a2a'); ctx.beginPath(); ctx.moveTo(centerX - 15, baseY); ctx.bezierCurveTo( centerX - 12, baseY - 60, centerX - 8, baseY - 120, centerX - 5, baseY - 180 ); ctx.lineTo(centerX + 5, baseY - 180); ctx.bezierCurveTo( centerX + 8, baseY - 120, centerX + 12, baseY - 60, centerX + 15, baseY ); ctx.closePath(); ctx.fill(); // 分散树枝 const branches = [ { angle: -60, length: 90, width: 3 }, { angle: 60, length: 90, width: 3 }, { angle: -35, length: 80, width: 2 }, { angle: 35, length: 80, width: 2 }, { angle: 0, length: 70, width: 2 }, ]; branches.forEach(b => { this.drawCurlyBranch(ctx, centerX, baseY - 180, b.angle, b.length, b.width, 15); }); // 樱花树冠 - 粉色蓬松 const sakuraLayers = [ { offsetY: -260, radiusX: 100, radiusY: 60, color: '#ffb7c5', alpha: 0.5 }, { offsetY: -300, radiusX: 85, radiusY: 55, color: '#ffc9d6', alpha: 0.45 }, { offsetY: -340, radiusX: 70, radiusY: 50, color: '#ffdbe5', alpha: 0.4 }, { offsetY: -370, radiusX: 55, radiusY: 40, color: '#ffedf4', alpha: 0.35 }, ]; sakuraLayers.forEach((layer, idx) => { ctx.save(); ctx.setGlobalAlpha(layer.alpha); ctx.setFillStyle(layer.color); ctx.beginPath(); for (let angle = 0; angle < Math.PI * 2; angle += 0.08) { const noise = Math.sin(angle * 8 + idx) * 10 + Math.cos(angle * 5 + idx) * 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(); }); // 飘落樱花 for (let i = 0; i < 15; i++) { const x = centerX - 80 + Math.random() * 160; const y = baseY - 350 + Math.random() * 100 + Math.sin(time + i) * 10; const size = 4 + Math.random() * 4; const rotation = Math.random() * Math.PI; ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.setFillStyle('#fff0f5'); ctx.beginPath(); // 樱花花瓣形状 ctx.moveTo(0, -size); ctx.bezierCurveTo(size, -size, size, size * 0.5, 0, size); ctx.bezierCurveTo(-size, size * 0.5, -size, -size, 0, -size); ctx.fill(); ctx.restore(); } }, // 绘制事业树(许愿竹,参考第二张图) drawBambooTree(ctx, centerX, baseY) { const time = Date.now() / 1000; // 地面装饰 - 小石子 ctx.setFillStyle('#8a8a7a'); for (let i = 0; i < 8; i++) { const x = centerX - 60 + i * 15; const size = 3 + Math.random() * 4; ctx.beginPath(); ctx.arc(x, baseY - 5, size, 0, Math.PI * 2); ctx.fill(); } // 竹竿 - 多根,参考第二张图 const bambooStalks = [ { offsetX: -40, height: 220, width: 8 }, { offsetX: -15, height: 260, width: 10 }, { offsetX: 10, height: 240, width: 9 }, { offsetX: 35, height: 200, width: 7 }, ]; bambooStalks.forEach((stalk, idx) => { const x = centerX + stalk.offsetX; const topY = baseY - stalk.height; const sway = Math.sin(time * 2 + idx) * 3; // 竹竿主体 - 带摇摆 ctx.setFillStyle('#6aaa4a'); ctx.beginPath(); ctx.moveTo(x - stalk.width / 2, baseY); ctx.quadraticCurveTo( x - stalk.width / 2 + sway, baseY - stalk.height / 2, x - stalk.width / 2 + sway * 2, topY ); ctx.lineTo(x + stalk.width / 2 + sway * 2, topY); ctx.quadraticCurveTo( x + stalk.width / 2 + sway, baseY - stalk.height / 2, x + stalk.width / 2, baseY ); ctx.closePath(); ctx.fill(); // 竹节 ctx.setStrokeStyle('#5a9a3a'); ctx.setLineWidth(2); for (let y = baseY - 30; y > topY + 20; y -= 35) { const nodeSway = sway * (1 - (baseY - y) / stalk.height); ctx.beginPath(); ctx.moveTo(x - stalk.width / 2 + nodeSway, y); ctx.lineTo(x + stalk.width / 2 + nodeSway, y); ctx.stroke(); } // 竹叶 - 更自然 ctx.setFillStyle('#7aba5a'); for (let i = 0; i < 6; i++) { const leafY = topY + 15 + i * 25; const side = i % 2 === 0 ? 1 : -1; const leafSway = sway * 0.5 + Math.sin(time * 3 + i) * 2; ctx.save(); ctx.translate(x + side * 5, leafY); ctx.rotate(side * 0.3 + leafSway * 0.02); ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(side * 30, -15, side * 50, 0); ctx.quadraticCurveTo(side * 30, 15, 0, 10); ctx.closePath(); ctx.fill(); ctx.restore(); } }); // 许愿条挂在竹子上 - 参考第二张图 if (this.ribbons && this.ribbons.length > 0) { this.ribbons.forEach((ribbon, idx) => { const stalk = bambooStalks[idx % bambooStalks.length]; const x = centerX + stalk.offsetX; const y = baseY - 80 - (idx % 5) * 30; // 画许愿条 - 长方形纸片 ctx.save(); ctx.translate(x, y); ctx.rotate(Math.sin(time * 2 + idx) * 0.1); // 纸片 ctx.setFillStyle(ribbon.color); ctx.fillRect(-8, 0, 16, 40); // 顶部挂绳 ctx.setStrokeStyle('#8a6a4a'); ctx.setLineWidth(1); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -10); ctx.stroke(); // 文字 ctx.setFillStyle('#ffffff'); ctx.setFontSize(10); ctx.setTextAlign('center'); ctx.fillText(ribbon.wish.content.substring(0, 4), 0, 25); ctx.restore(); }); } }, // 绘制丝带 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; const tree = trees[currentTreeIndex]; // 确保返回有效的树对象,带默认类型 if (!tree) { return { type: 'pine', name: '许愿树' }; } // 确保有type字段 if (!tree.type) { tree.type = 'pine'; } return tree; }, // 切换树 switchTree(e) { const direction = e.currentTarget.dataset.direction; const { trees, currentTreeIndex } = this.data; if (!trees.length) return; 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', }; }, });