Build and Deploy Server / build (push) Failing after 2s
- loadWishes使用safeInitRibbons/safeInitDanmaku,防止初始化出错导致页面崩溃 - getCurrentTree确保返回带type字段的有效树对象 - 添加错误处理,避免未定义错误
1162 lines
33 KiB
JavaScript
1162 lines
33 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.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',
|
|
};
|
|
},
|
|
});
|