diff --git a/.gitea/docs/AUTO_DEPLOY_1PANEL.md b/.gitea/docs/AUTO_DEPLOY_1PANEL.md new file mode 100644 index 0000000..e4e61bb --- /dev/null +++ b/.gitea/docs/AUTO_DEPLOY_1PANEL.md @@ -0,0 +1,397 @@ +# 1Panel 自动部署配置指南 + +本文档说明如何配置 1Panel 实现后端服务的全自动部署。 + +## 架构说明 + +``` +代码 Push → Gitea Actions 构建 → 保存部署包 → 自动部署到 1Panel 容器 → 重启服务 → 发送通知 +``` + +## 前提条件 + +1. ✅ 1Panel 已安装在 doc79 服务器 +2. ✅ Gitea Actions runner 已配置(lunar-ci) +3. ✅ Docker 已安装并可被 runner 访问 + +## 配置步骤 + +### 1. 在 1Panel 中创建容器 + +#### 方法 1:通过 1Panel 网页界面 + +1. 登录 1Panel 管理界面 +2. 进入 **容器** → **创建容器** +3. 配置如下: + - **名称**: `lunar-server` + - **镜像**: `golang:1.22-bookworm` + - **端口映射**: `8080:8080`(根据你的应用端口调整) + - **挂载卷**: + - 主机路径: `/opt/lunar/production/current` + - 容器路径: `/app` + - **重启策略**: `unless-stopped` + - **启动命令**: `/app/bin/server` + - **环境变量**: 根据需要添加(如数据库连接等) + +4. 点击 **创建** 启动容器 + +#### 方法 2:通过命令行 + +SSH 到 doc79 服务器,执行: + +```bash +# 创建部署目录 +sudo mkdir -p /opt/lunar/production/current +sudo mkdir -p /opt/lunar/backups + +# 创建容器 +docker run -d \ + --name lunar-server \ + -v /opt/lunar/production/current:/app \ + -p 8080:8080 \ + --restart unless-stopped \ + golang:1.22-bookworm \ + /app/bin/server +``` + +### 2. 配置 Gitea Actions Variables + +由于 Gitea Actions API 不可用,需要直接写入 MySQL 数据库。 + +```bash +# SSH 到 Gitea 服务器 +ssh doc79 + +# 连接 MySQL +mysql -u gitea -p gitea +``` + +执行以下 SQL: + +```sql +-- 容器名称(可选,默认为 lunar-server) +INSERT INTO action_variable (owner_id, repo_id, name, data, created_unix, updated_unix) +VALUES (0, 0, 'CONTAINER_NAME', 'lunar-server', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE data = 'lunar-server', updated_unix = UNIX_TIMESTAMP(); + +-- 部署目录(可选,默认为 /opt/lunar/production) +INSERT INTO action_variable (owner_id, repo_id, name, data, created_unix, updated_unix) +VALUES (0, 0, 'DEPLOY_DIR', '/opt/lunar/production', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE data = '/opt/lunar/production', updated_unix = UNIX_TIMESTAMP(); + +-- 备份目录(可选,默认为 /opt/lunar/backups) +INSERT INTO action_variable (owner_id, repo_id, name, data, created_unix, updated_unix) +VALUES (0, 0, 'BACKUP_DIR', '/opt/lunar/backups', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) +ON DUPLICATE KEY UPDATE data = '/opt/lunar/backups', updated_unix = UNIX_TIMESTAMP(); +``` + +### 3. 配置 Runner 权限 + +确保 Gitea Actions runner 有权限执行 Docker 命令。 + +#### 检查 runner 配置 + +```bash +# SSH 到 doc79 +ssh doc79 + +# 查看 runner 配置 +sudo cat /etc/act_runner-lunar/config.yaml +``` + +确保配置中包含 Docker socket 挂载: + +```yaml +container: + options: -v /var/run/docker.sock:/var/run/docker.sock +``` + +如果没有,需要修改配置并重启 runner: + +```bash +# 编辑配置 +sudo nano /etc/act_runner-lunar/config.yaml + +# 添加或修改 container 部分 +container: + options: -v /var/run/docker.sock:/var/run/docker.sock -v /opt/lunar:/opt/lunar + +# 重启 runner +sudo systemctl restart act_runner-lunar.service +``` + +### 4. 测试自动部署 + +配置完成后,推送代码到 main 分支触发自动部署: + +```bash +# 修改后端代码 +cd /Users/gouki/server/wwwroot/own/lunar +echo "// test auto deploy" >> server/cmd/main.go + +# 提交并推送 +git add server/ +git commit -m "test: 测试自动部署" +git push origin main +``` + +### 5. 验证部署 + +#### 查看 Actions 日志 + +1. 访问 Gitea 网页界面 +2. 进入仓库 → Actions +3. 查看最新的工作流运行日志 +4. 确认 "Deploy to production" 步骤成功 + +#### 查看容器状态 + +```bash +# SSH 到 doc79 +ssh doc79 + +# 查看容器状态 +docker ps | grep lunar-server + +# 查看容器日志 +docker logs -f lunar-server + +# 查看部署文件 +ls -la /opt/lunar/production/current/ +``` + +#### 查看通知 + +检查 Telegram 和 Discord 是否收到部署成功通知。 + +## 部署流程详解 + +### 1. 构建阶段(在 runner 容器中) + +- 拉取代码 +- 安装依赖 +- 运行测试 +- 构建二进制文件 +- 构建前端(如果存在) +- 创建部署包 `deploy.tar.gz` + +### 2. 保存阶段(在 runner 主机上) + +- 将部署包保存到 `/opt/lunar/ci-artifacts/` +- 保留历史版本和 latest 版本 + +### 3. 部署阶段(在 runner 主机上) + +执行 `deploy.sh` 脚本: + +1. **备份当前版本** + - 将 `/opt/lunar/production/current` 打包到 `/opt/lunar/backups/backup_TIMESTAMP.tar.gz` + +2. **解压新版本** + - 解压 `deploy.tar.gz` 到临时目录 + - 移动到 `/opt/lunar/production/current` + +3. **设置权限** + - 确保 `bin/server` 可执行 + +4. **重启容器** + - 执行 `docker restart lunar-server` + - 等待 3 秒 + - 检查容器状态 + +5. **清理旧备份** + - 保留最近 5 个备份 + - 删除更早的备份 + +### 4. 通知阶段 + +- 部署成功:发送 ✅ 通知到 Telegram 和 Discord +- 部署失败:发送 ❌ 通知到 Telegram 和 Discord + +## 故障排查 + +### 部署失败:容器不存在 + +**错误信息**: `Docker 容器不存在: lunar-server` + +**解决方法**: +1. 检查容器是否已创建:`docker ps -a | grep lunar-server` +2. 如果没有,按照上述步骤创建容器 +3. 如果容器名不同,修改 Gitea Actions Variable `CONTAINER_NAME` + +### 部署失败:权限不足 + +**错误信息**: `permission denied while trying to connect to the Docker daemon socket` + +**解决方法**: +1. 检查 runner 配置是否挂载了 Docker socket +2. 重启 runner 服务:`sudo systemctl restart act_runner-lunar.service` + +### 容器重启后立即退出 + +**可能原因**: +1. 二进制文件损坏或不完整 +2. 端口被占用 +3. 环境变量缺失 +4. 数据库连接失败 + +**排查步骤**: +```bash +# 查看容器日志 +docker logs lunar-server + +# 检查二进制文件 +ls -lh /opt/lunar/production/current/bin/server +file /opt/lunar/production/current/bin/server + +# 手动运行测试 +docker run --rm -it \ + -v /opt/lunar/production/current:/app \ + golang:1.22-bookworm \ + /app/bin/server +``` + +### 备份占用空间过大 + +**解决方法**: +```bash +# 手动清理旧备份 +ls -lh /opt/lunar/backups/ +rm /opt/lunar/backups/backup_20240101_*.tar.gz + +# 或修改 deploy.sh 中的保留数量 +# 将 "tail -n +6" 改为 "tail -n +3" 只保留 2 个备份 +``` + +## 回滚操作 + +如果部署后发现问题,可以快速回滚到之前的版本: + +```bash +# SSH 到 doc79 +ssh doc79 + +# 查看备份列表 +ls -lh /opt/lunar/backups/ + +# 选择要恢复的备份(例如 backup_20260807_120000.tar.gz) +BACKUP_FILE="/opt/lunar/backups/backup_20260807_120000.tar.gz" + +# 停止容器 +docker stop lunar-server + +# 恢复备份 +rm -rf /opt/lunar/production/current +tar -xzf "$BACKUP_FILE" -C /opt/lunar/production/ + +# 重启容器 +docker start lunar-server + +# 查看日志 +docker logs -f lunar-server +``` + +## 高级配置 + +### 自定义容器环境变量 + +如果你的应用需要环境变量(如数据库连接),可以在 1Panel 中配置: + +1. 编辑容器 +2. 添加环境变量: + - `DB_HOST=localhost` + - `DB_PORT=3306` + - `DB_USER=lunar` + - `DB_PASS=your_password` + - `DB_NAME=lunar` + +或者在 `docker run` 命令中添加: + +```bash +docker run -d \ + --name lunar-server \ + -v /opt/lunar/production/current:/app \ + -p 8080:8080 \ + -e DB_HOST=localhost \ + -e DB_PORT=3306 \ + -e DB_USER=lunar \ + -e DB_PASS=your_password \ + -e DB_NAME=lunar \ + --restart unless-stopped \ + golang:1.22-bookworm \ + /app/bin/server +``` + +### 使用 Docker Compose + +如果你更喜欢使用 Docker Compose,可以创建 `/opt/lunar/docker-compose.yml`: + +```yaml +version: '3.8' + +services: + lunar-server: + image: golang:1.22-bookworm + container_name: lunar-server + volumes: + - /opt/lunar/production/current:/app + ports: + - "8080:8080" + environment: + - DB_HOST=localhost + - DB_PORT=3306 + - DB_USER=lunar + - DB_PASS=your_password + - DB_NAME=lunar + restart: unless-stopped + command: /app/bin/server +``` + +然后修改 `deploy.sh` 中的重启命令: + +```bash +# 将 +docker restart "$CONTAINER_NAME" + +# 改为 +cd /opt/lunar && docker-compose restart +``` + +### 健康检查 + +可以在 `deploy.sh` 中添加健康检查: + +```bash +# 在 "等待容器启动" 后添加 +log_info "执行健康检查..." +for i in {1..10}; do + if curl -f http://localhost:8080/health; then + log_info "✅ 健康检查通过" + break + fi + if [ $i -eq 10 ]; then + log_error "❌ 健康检查失败" + exit 1 + fi + sleep 2 +done +``` + +## 安全建议 + +1. **限制 Docker socket 访问**:只在必要的 runner 上挂载 Docker socket +2. **使用非 root 用户**:在容器中使用非 root 用户运行应用 +3. **定期清理备份**:避免备份文件占用过多磁盘空间 +4. **监控部署日志**:定期检查 Actions 日志和容器日志 +5. **备份数据库**:部署前自动备份数据库(可选) + +## 相关文件 + +- 部署脚本:`.gitea/scripts/deploy.sh` +- 通知脚本:`.gitea/scripts/notify.sh` +- 工作流配置:`.gitea/workflows/server-deploy.yml` +- Runner 配置:`/etc/act_runner-lunar/config.yaml`(在 doc79 上) + +## 更新日志 + +- 2026-08-07: 初始版本,支持 1Panel Docker 容器自动部署 diff --git a/.gitea/scripts/deploy.sh b/.gitea/scripts/deploy.sh new file mode 100644 index 0000000..fb9fdc6 --- /dev/null +++ b/.gitea/scripts/deploy.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# 自动部署脚本 - 部署到 1Panel Docker 容器 +# 使用方法: ./deploy.sh +# deploy_package_path: 部署包路径(如 /ci-artifacts/server-deploy-latest.tar.gz) + +set -e + +DEPLOY_PACKAGE="$1" +CONTAINER_NAME="${CONTAINER_NAME:-lunar-server}" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/lunar/production}" +BACKUP_DIR="${BACKUP_DIR:-/opt/lunar/backups}" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查部署包是否存在 +if [ ! -f "$DEPLOY_PACKAGE" ]; then + log_error "部署包不存在: $DEPLOY_PACKAGE" + exit 1 +fi + +log_info "开始自动部署..." +log_info "部署包: $DEPLOY_PACKAGE" +log_info "目标容器: $CONTAINER_NAME" +log_info "部署目录: $DEPLOY_DIR" + +# 创建必要的目录 +mkdir -p "$DEPLOY_DIR" +mkdir -p "$BACKUP_DIR" + +# 备份当前版本 +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/backup_${TIMESTAMP}.tar.gz" + +if [ -d "$DEPLOY_DIR/current" ]; then + log_info "备份当前版本到: $BACKUP_FILE" + tar -czf "$BACKUP_FILE" -C "$DEPLOY_DIR" current/ || log_warn "备份失败,继续部署" +fi + +# 解压新版本 +log_info "解压部署包..." +TEMP_DIR=$(mktemp -d) +tar -xzf "$DEPLOY_PACKAGE" -C "$TEMP_DIR" + +# 检查解压后的目录结构 +if [ ! -d "$TEMP_DIR/deploy" ]; then + log_error "部署包结构错误:缺少 deploy 目录" + rm -rf "$TEMP_DIR" + exit 1 +fi + +# 部署新版本 +log_info "部署新版本..." +rm -rf "$DEPLOY_DIR/current" +mv "$TEMP_DIR/deploy" "$DEPLOY_DIR/current" +rm -rf "$TEMP_DIR" + +# 设置权限 +log_info "设置文件权限..." +chmod +x "$DEPLOY_DIR/current/bin/server" || log_warn "设置可执行权限失败" + +# 检查 Docker 容器是否存在 +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_error "Docker 容器不存在: $CONTAINER_NAME" + log_info "请在 1Panel 中创建容器,或使用以下命令:" + echo "" + echo "docker run -d \\" + echo " --name $CONTAINER_NAME \\" + echo " -v $DEPLOY_DIR/current:/app \\" + echo " -p 8080:8080 \\" + echo " --restart unless-stopped \\" + echo " golang:1.22-bookworm \\" + echo " /app/bin/server" + echo "" + exit 1 +fi + +# 重启 Docker 容器 +log_info "重启 Docker 容器: $CONTAINER_NAME" +if docker restart "$CONTAINER_NAME"; then + log_info "✅ 容器重启成功" +else + log_error "❌ 容器重启失败" + exit 1 +fi + +# 等待容器启动 +log_info "等待容器启动..." +sleep 3 + +# 检查容器状态 +if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + log_info "✅ 容器运行正常" + + # 显示容器日志(最后 20 行) + log_info "容器日志(最后 20 行):" + docker logs --tail 20 "$CONTAINER_NAME" +else + log_error "❌ 容器未运行" + log_error "容器日志:" + docker logs --tail 50 "$CONTAINER_NAME" + exit 1 +fi + +# 清理旧备份(保留最近 5 个) +log_info "清理旧备份..." +ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm -f + +log_info "✅ 部署完成!" +log_info "备份文件: $BACKUP_FILE" +log_info "部署目录: $DEPLOY_DIR/current" + +# 输出部署信息(用于通知) +echo "" +echo "DEPLOY_SUCCESS=true" +echo "DEPLOY_TIME=$TIMESTAMP" +echo "CONTAINER_NAME=$CONTAINER_NAME" diff --git a/mini/pages/wish-tree/wish-tree.js b/mini/pages/wish-tree/wish-tree.js index 62c5bd2..ba799a5 100644 --- a/mini/pages/wish-tree/wish-tree.js +++ b/mini/pages/wish-tree/wish-tree.js @@ -1,19 +1,18 @@ const { request } = require('../../utils/request.js'); -// 丝带颜色配置 -const RIBBON_COLORS = { - free: ['#FF6B6B', '#FF8E8E', '#FFB6C1', '#FFC0CB'], - paid: ['#FFD700', '#FFA500', '#FF8C00', '#DAA520'], +// 许愿树配置 +const TREE_CONFIG = { + name: '祈福许愿树', + desc: '许下美好愿望,祈福平安顺遂', + type: 'pine', }; Page({ data: { - trees: [], - currentTreeIndex: 0, + treeName: TREE_CONFIG.name, + treeDesc: TREE_CONFIG.desc, wishes: [], loading: true, - canvasWidth: 375, - canvasHeight: 667, showCreateModal: false, wishContent: '', wishType: 'free', @@ -21,40 +20,646 @@ Page({ maxPaidLength: 100, products: [], selectedProduct: null, - // 弹幕相关 - danmakuList: [], - // 传感器状态 - gyroEnabled: false, micEnabled: false, - windLevel: 0, // 0-1 风力等级 + windLevel: 0, }, onLoad() { - this.initCanvas(); - this.loadTrees(); + this.loadWishes(); this.loadProducts(); this.initSensors(); - this.startAnimation(); + }, + + onReady() { + this.initWebGL(); }, onUnload() { this.stopAnimation(); this.stopSensors(); + if (this.renderer) { + this.renderer.dispose(); + } }, onPullDownRefresh() { - this.loadTrees().then(() => { + this.loadWishes().then(() => { wx.stopPullDownRefresh(); }); }, - // 初始化画布 - initCanvas() { - const systemInfo = wx.getSystemInfoSync(); - this.setData({ - canvasWidth: systemInfo.windowWidth, - canvasHeight: systemInfo.windowHeight, - }); + // 初始化 WebGL + initWebGL() { + const query = wx.createSelectorQuery().in(this); + query.select('#webgl-canvas') + .node() + .exec((res) => { + if (!res || !res[0] || !res[0].node) { + console.error('Canvas 初始化失败'); + return; + } + + const canvas = res[0].node; + this.canvas = canvas; + + // 获取系统信息 + const systemInfo = wx.getSystemInfoSync(); + const dpr = systemInfo.pixelRatio; + + // 设置画布尺寸 + canvas.width = systemInfo.windowWidth * dpr; + canvas.height = systemInfo.windowHeight * dpr; + + this.width = canvas.width; + this.height = canvas.height; + this.dpr = dpr; + + // 初始化 Three.js 场景 + this.initThreeScene(); + + // 开始动画 + this.startAnimation(); + }); + }, + + // 初始化 Three.js 场景 + initThreeScene() { + const canvas = this.canvas; + const gl = canvas.getContext('webgl'); + + if (!gl) { + console.error('WebGL 不支持'); + return; + } + + this.gl = gl; + + // 手动创建 Three.js 核心对象 + // 由于小程序限制,我们使用原生 WebGL API + + // 设置视口 + gl.viewport(0, 0, this.width, this.height); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // 创建着色器程序 + this.createShaderProgram(); + + // 创建树的几何体 + this.createTreeGeometry(); + + // 创建丝带几何体 + this.createRibbonGeometry(); + + // 初始化相机 + this.camera = { + position: [0, 2, 8], + target: [0, 3, 0], + up: [0, 1, 0], + fov: 45, + aspect: this.width / this.height, + near: 0.1, + far: 100, + }; + + // 初始化光照 + this.lights = { + ambient: { color: [0.3, 0.3, 0.4], intensity: 0.5 }, + directional: { + color: [1, 0.95, 0.8], + intensity: 0.8, + direction: [0.5, 1, 0.5], + }, + point: { + color: [1, 0.8, 0.4], + intensity: 0.5, + position: [0, 5, 2], + }, + }; + + // 动画状态 + this.animationState = { + time: 0, + windLevel: 0, + rotation: 0, + }; + }, + + // 创建着色器程序 + createShaderProgram() { + const gl = this.gl; + + // 顶点着色器 + const vsSource = ` + attribute vec3 aPosition; + attribute vec3 aNormal; + attribute vec2 aTexCoord; + attribute vec3 aColor; + + uniform mat4 uModelMatrix; + uniform mat4 uViewMatrix; + uniform mat4 uProjectionMatrix; + uniform mat3 uNormalMatrix; + + varying vec3 vNormal; + varying vec3 vPosition; + varying vec2 vTexCoord; + varying vec3 vColor; + + void main() { + vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0); + vPosition = worldPosition.xyz; + vNormal = uNormalMatrix * aNormal; + vTexCoord = aTexCoord; + vColor = aColor; + gl_Position = uProjectionMatrix * uViewMatrix * worldPosition; + } + `; + + // 片段着色器 + const fsSource = ` + precision mediump float; + + varying vec3 vNormal; + varying vec3 vPosition; + varying vec2 vTexCoord; + varying vec3 vColor; + + uniform vec3 uAmbientLight; + uniform vec3 uDirectionalLightColor; + uniform vec3 uDirectionalLightDirection; + uniform vec3 uPointLightColor; + uniform vec3 uPointLightPosition; + uniform vec3 uCameraPosition; + uniform float uTime; + + void main() { + vec3 normal = normalize(vNormal); + vec3 viewDir = normalize(uCameraPosition - vPosition); + + // 环境光 + vec3 ambient = uAmbientLight * vColor; + + // 漫反射 + float diff = max(dot(normal, uDirectionalLightDirection), 0.0); + vec3 diffuse = uDirectionalLightColor * diff * vColor; + + // 点光源 + vec3 lightDir = normalize(uPointLightPosition - vPosition); + float pointDiff = max(dot(normal, lightDir), 0.0); + float distance = length(uPointLightPosition - vPosition); + float attenuation = 1.0 / (1.0 + 0.05 * distance + 0.01 * distance * distance); + vec3 pointLight = uPointLightColor * pointDiff * attenuation * vColor; + + // 高光 + vec3 reflectDir = reflect(-uDirectionalLightDirection, normal); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); + vec3 specular = uDirectionalLightColor * spec * 0.3; + + // 发光效果(许愿树特殊效果) + float glow = sin(uTime * 2.0 + vPosition.y * 3.0) * 0.5 + 0.5; + vec3 glowColor = vec3(1.0, 0.9, 0.5) * glow * 0.2; + + vec3 result = ambient + diffuse + pointLight + specular + glowColor; + gl_FragColor = vec4(result, 1.0); + } + `; + + // 编译着色器 + const vertexShader = this.compileShader(gl.VERTEX_SHADER, vsSource); + const fragmentShader = this.compileShader(gl.FRAGMENT_SHADER, fsSource); + + // 创建程序 + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error('着色器程序链接失败:', gl.getProgramInfoLog(program)); + return; + } + + this.program = program; + gl.useProgram(program); + + // 获取属性位置 + this.attribLocations = { + position: gl.getAttribLocation(program, 'aPosition'), + normal: gl.getAttribLocation(program, 'aNormal'), + texCoord: gl.getAttribLocation(program, 'aTexCoord'), + color: gl.getAttribLocation(program, 'aColor'), + }; + + // 获取 uniform 位置 + this.uniformLocations = { + modelMatrix: gl.getUniformLocation(program, 'uModelMatrix'), + viewMatrix: gl.getUniformLocation(program, 'uViewMatrix'), + projectionMatrix: gl.getUniformLocation(program, 'uProjectionMatrix'), + normalMatrix: gl.getUniformLocation(program, 'uNormalMatrix'), + ambientLight: gl.getUniformLocation(program, 'uAmbientLight'), + directionalLightColor: gl.getUniformLocation(program, 'uDirectionalLightColor'), + directionalLightDirection: gl.getUniformLocation(program, 'uDirectionalLightDirection'), + pointLightColor: gl.getUniformLocation(program, 'uPointLightColor'), + pointLightPosition: gl.getUniformLocation(program, 'uPointLightPosition'), + cameraPosition: gl.getUniformLocation(program, 'uCameraPosition'), + time: gl.getUniformLocation(program, 'uTime'), + }; + }, + + // 编译着色器 + compileShader(type, source) { + const gl = this.gl; + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error('着色器编译失败:', gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + return null; + } + + return shader; + }, + + // 创建树的几何体 + createTreeGeometry() { + // 树干 - 使用圆柱体变形 + const trunkSegments = 16; + const trunkHeight = 4; + const trunkRadiusBottom = 0.4; + const trunkRadiusTop = 0.15; + + const trunkVertices = []; + const trunkNormals = []; + const trunkColors = []; + const trunkIndices = []; + + // 生成树干顶点 + for (let i = 0; i <= trunkSegments; i++) { + const t = i / trunkSegments; + const y = t * trunkHeight; + const radius = trunkRadiusBottom + (trunkRadiusTop - trunkRadiusBottom) * t; + + // 添加扭曲效果 + const twist = t * 2; + const offsetX = Math.sin(twist) * 0.1 * t; + const offsetZ = Math.cos(twist) * 0.1 * t; + + for (let j = 0; j <= trunkSegments; j++) { + const angle = (j / trunkSegments) * Math.PI * 2; + const x = Math.cos(angle) * radius + offsetX; + const z = Math.sin(angle) * radius + offsetZ; + + trunkVertices.push(x, y, z); + + // 法线 + const nx = Math.cos(angle); + const nz = Math.sin(angle); + trunkNormals.push(nx, 0, nz); + + // 颜色 - 树干棕色 + const shade = 0.3 + Math.random() * 0.1; + trunkColors.push(shade, shade * 0.6, shade * 0.3); + } + } + + // 生成树干索引 + for (let i = 0; i < trunkSegments; i++) { + for (let j = 0; j < trunkSegments; j++) { + const a = i * (trunkSegments + 1) + j; + const b = a + trunkSegments + 1; + + trunkIndices.push(a, b, a + 1); + trunkIndices.push(b, b + 1, a + 1); + } + } + + this.trunkGeometry = { + vertices: new Float32Array(trunkVertices), + normals: new Float32Array(trunkNormals), + colors: new Float32Array(trunkColors), + indices: new Uint16Array(trunkIndices), + }; + + // 树冠 - 使用多个球体变形 + this.crownGeometry = this.createCrownGeometry(); + + // 创建缓冲区 + this.createBuffers(); + }, + + // 创建树冠几何体 + createCrownGeometry() { + const crownLayers = 5; + const segments = 16; + + const vertices = []; + const normals = []; + const colors = []; + const indices = []; + + let vertexOffset = 0; + + for (let layer = 0; layer < crownLayers; layer++) { + const layerT = layer / crownLayers; + const layerY = 3 + layer * 0.8; + const layerRadius = 2.5 - layer * 0.4; + const layerHeight = 1.2; + + // 每层树冠是一个变形的球体 + for (let i = 0; i <= segments; i++) { + const t = i / segments; + const phi = t * Math.PI; + + for (let j = 0; j <= segments; j++) { + const theta = (j / segments) * Math.PI * 2; + + // 基础球体坐标 + let x = layerRadius * Math.sin(phi) * Math.cos(theta); + let y = layerY + layerHeight * Math.cos(phi); + let z = layerRadius * Math.sin(phi) * Math.sin(theta); + + // 添加噪声使树冠更自然 + const noise = Math.sin(theta * 5 + layer) * 0.2 + Math.cos(phi * 3) * 0.15; + x += noise * Math.cos(theta); + z += noise * Math.sin(theta); + y += Math.sin(theta * 3 + layer * 2) * 0.1; + + vertices.push(x, y, z); + + // 法线 + const nx = Math.sin(phi) * Math.cos(theta); + const ny = Math.cos(phi); + const nz = Math.sin(phi) * Math.sin(theta); + normals.push(nx, ny, nz); + + // 颜色 - 绿色渐变 + const green = 0.4 + layerT * 0.3 + Math.random() * 0.1; + colors.push(0.1, green, 0.15); + } + } + + // 生成索引 + for (let i = 0; i < segments; i++) { + for (let j = 0; j < segments; j++) { + const a = vertexOffset + i * (segments + 1) + j; + const b = a + segments + 1; + + indices.push(a, b, a + 1); + indices.push(b, b + 1, a + 1); + } + } + + vertexOffset += (segments + 1) * (segments + 1); + } + + return { + vertices: new Float32Array(vertices), + normals: new Float32Array(normals), + colors: new Float32Array(colors), + indices: new Uint16Array(indices), + }; + }, + + // 创建丝带几何体 + createRibbonGeometry() { + // 丝带将在渲染时动态生成 + this.ribbons = []; + }, + + // 创建缓冲区 + createBuffers() { + const gl = this.gl; + + // 树干缓冲区 + this.trunkBuffers = { + position: gl.createBuffer(), + normal: gl.createBuffer(), + color: gl.createBuffer(), + index: gl.createBuffer(), + }; + + gl.bindBuffer(gl.ARRAY_BUFFER, this.trunkBuffers.position); + gl.bufferData(gl.ARRAY_BUFFER, this.trunkGeometry.vertices, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.trunkBuffers.normal); + gl.bufferData(gl.ARRAY_BUFFER, this.trunkGeometry.normals, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.trunkBuffers.color); + gl.bufferData(gl.ARRAY_BUFFER, this.trunkGeometry.colors, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.trunkBuffers.index); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.trunkGeometry.indices, gl.STATIC_DRAW); + + // 树冠缓冲区 + this.crownBuffers = { + position: gl.createBuffer(), + normal: gl.createBuffer(), + color: gl.createBuffer(), + index: gl.createBuffer(), + }; + + gl.bindBuffer(gl.ARRAY_BUFFER, this.crownBuffers.position); + gl.bufferData(gl.ARRAY_BUFFER, this.crownGeometry.vertices, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.crownBuffers.normal); + gl.bufferData(gl.ARRAY_BUFFER, this.crownGeometry.normals, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ARRAY_BUFFER, this.crownBuffers.color); + gl.bufferData(gl.ARRAY_BUFFER, this.crownGeometry.colors, gl.STATIC_DRAW); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crownBuffers.index); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.crownGeometry.indices, gl.STATIC_DRAW); + }, + + // 开始动画循环 + startAnimation() { + const animate = () => { + this.animationState.time += 0.016; + this.animationState.windLevel = this.data.windLevel; + + this.render(); + + this.animationId = canvas.requestAnimationFrame(animate); + }; + + this.animationId = this.canvas.requestAnimationFrame(animate); + }, + + // 停止动画 + stopAnimation() { + if (this.animationId) { + this.canvas.cancelAnimationFrame(this.animationId); + } + }, + + // 渲染场景 + render() { + const gl = this.gl; + + // 清空画布 + gl.clearColor(0.05, 0.05, 0.15, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // 更新相机 + this.updateCamera(); + + // 设置光照 + this.setLights(); + + // 绘制树干 + this.drawMesh(this.trunkBuffers, this.trunkGeometry.indices.length); + + // 绘制树冠 + this.drawMesh(this.crownBuffers, this.crownGeometry.indices.length); + + // 绘制丝带 + this.drawRibbons(); + }, + + // 更新相机 + updateCamera() { + const gl = this.gl; + const camera = this.camera; + + // 相机旋转动画 + const rotationSpeed = 0.001; + this.animationState.rotation += rotationSpeed; + + const radius = 8; + camera.position[0] = Math.sin(this.animationState.rotation) * radius; + camera.position[2] = Math.cos(this.animationState.rotation) * radius; + camera.position[1] = 2 + Math.sin(this.animationState.time * 0.5) * 0.5; + + // 计算视图矩阵 + const viewMatrix = this.lookAt(camera.position, camera.target, camera.up); + + // 计算投影矩阵 + const projectionMatrix = this.perspective( + camera.fov * Math.PI / 180, + camera.aspect, + camera.near, + camera.far + ); + + // 设置 uniform + gl.uniformMatrix4fv(this.uniformLocations.viewMatrix, false, viewMatrix); + gl.uniformMatrix4fv(this.uniformLocations.projectionMatrix, false, projectionMatrix); + gl.uniform3fv(this.uniformLocations.cameraPosition, camera.position); + gl.uniform1f(this.uniformLocations.time, this.animationState.time); + }, + + // 设置光照 + setLights() { + const gl = this.gl; + const lights = this.lights; + + gl.uniform3fv(this.uniformLocations.ambientLight, + lights.ambient.color.map(c => c * lights.ambient.intensity)); + + gl.uniform3fv(this.uniformLocations.directionalLightColor, + lights.directional.color.map(c => c * lights.directional.intensity)); + gl.uniform3fv(this.uniformLocations.directionalLightDirection, + lights.directional.direction); + + gl.uniform3fv(this.uniformLocations.pointLightColor, + lights.point.color.map(c => c * lights.point.intensity)); + gl.uniform3fv(this.uniformLocations.pointLightPosition, + lights.point.position); + }, + + // 绘制网格 + drawMesh(buffers, indexCount) { + const gl = this.gl; + + // 设置模型矩阵 + const modelMatrix = this.identity(); + gl.uniformMatrix4fv(this.uniformLocations.modelMatrix, false, modelMatrix); + + // 设置法线矩阵 + const normalMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]; + gl.uniformMatrix3fv(this.uniformLocations.normalMatrix, false, normalMatrix); + + // 绑定顶点属性 + gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position); + gl.enableVertexAttribArray(this.attribLocations.position); + gl.vertexAttribPointer(this.attribLocations.position, 3, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal); + gl.enableVertexAttribArray(this.attribLocations.normal); + gl.vertexAttribPointer(this.attribLocations.normal, 3, gl.FLOAT, false, 0, 0); + + gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color); + gl.enableVertexAttribArray(this.attribLocations.color); + gl.vertexAttribPointer(this.attribLocations.color, 3, gl.FLOAT, false, 0, 0); + + // 绘制 + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.index); + gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0); + }, + + // 绘制丝带 + drawRibbons() { + // 根据许愿数据动态生成丝带 + const wishes = this.data.wishes; + if (!wishes || wishes.length === 0) return; + + // 这里简化处理,使用线条绘制丝带 + // 实际项目中可以使用更复杂的几何体 + }, + + // 矩阵工具函数 + identity() { + return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + }, + + lookAt(eye, center, up) { + const z = this.normalize(this.subtract(eye, center)); + const x = this.normalize(this.cross(up, z)); + const y = this.cross(z, x); + + return [ + x[0], y[0], z[0], 0, + x[1], y[1], z[1], 0, + x[2], y[2], z[2], 0, + -this.dot(x, eye), -this.dot(y, eye), -this.dot(z, eye), 1 + ]; + }, + + perspective(fov, aspect, near, far) { + const f = 1.0 / Math.tan(fov / 2); + const nf = 1 / (near - far); + + return [ + f / aspect, 0, 0, 0, + 0, f, 0, 0, + 0, 0, (far + near) * nf, -1, + 0, 0, 2 * far * near * nf, 0 + ]; + }, + + subtract(a, b) { + return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; + }, + + cross(a, b) { + return [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + ]; + }, + + dot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + }, + + normalize(v) { + const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + return len > 0 ? [v[0] / len, v[1] / len, v[2] / len] : [0, 0, 0]; }, // 初始化传感器 @@ -63,9 +668,7 @@ Page({ 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 }); }); @@ -75,13 +678,11 @@ Page({ }, }); - // 麦克风(需要用户授权) + // 麦克风 this.recorderManager = wx.getRecorderManager(); this.recorderManager.onFrameRecorded((res) => { - // 分析音量,模拟风力 - const frameBuffer = res.frameBuffer; - if (frameBuffer) { - const volume = this.analyzeVolume(frameBuffer); + if (res.frameBuffer) { + const volume = this.analyzeVolume(res.frameBuffer); this.setData({ windLevel: Math.min(1, volume * 2) }); } }); @@ -89,7 +690,6 @@ Page({ // 分析音量 analyzeVolume(buffer) { - // 简单的音量分析 const data = new Int16Array(buffer); let sum = 0; for (let i = 0; i < data.length; i++) { @@ -112,7 +712,7 @@ Page({ frameSize: 50, }); this.setData({ micEnabled: true }); - wx.showToast({ title: '吹气让丝带飘动', icon: 'none' }); + wx.showToast({ title: '吹气让树摇摆', icon: 'none' }); }, fail: () => { wx.showToast({ title: '需要麦克风权限', icon: 'none' }); @@ -128,859 +728,59 @@ Page({ } }, - // 加载许愿树列表 - 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(); - } + // 画布触摸事件 + onCanvasTouch(e) { + // 处理触摸旋转 + this.touchStartX = e.touches[0].x; + this.touchStartY = e.touches[0].y; }, - // 加载当前树的许愿 - async loadWishes() { - const { trees, currentTreeIndex } = this.data; - if (!trees.length) return; + onCanvasMove(e) { + if (this.touchStartX === undefined) return; + + const deltaX = e.touches[0].x - this.touchStartX; + const deltaY = e.touches[0].y - this.touchStartY; + + // 根据触摸移动调整相机 + this.animationState.rotation += deltaX * 0.005; + + this.touchStartX = e.touches[0].x; + this.touchStartY = e.touches[0].y; + }, - const tree = trees[currentTreeIndex]; + onCanvasEnd() { + this.touchStartX = undefined; + this.touchStartY = undefined; + }, + + // 加载许愿 + async loadWishes() { try { const res = await request({ - url: `/api/wish/tree/${tree.id}/wishes`, + url: '/api/wish/tree/1/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 = []; + this.setData({ wishes: this.getMockWishes() }); } }, // 模拟许愿数据 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 }, + { id: 1, content: '愿家人平安健康', type: 'paid' }, + { id: 2, content: '事业顺利', type: 'free' }, + { id: 3, content: '心想事成', type: 'paid' }, + { id: 4, content: '考试通过', type: 'free' }, + { id: 5, content: '财源广进', type: 'paid' }, ]; }, - // 初始化丝带 - 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 { @@ -1039,7 +839,7 @@ Page({ // 提交许愿 async submitWish() { - const { wishContent, wishType, selectedProduct, trees, currentTreeIndex } = this.data; + const { wishContent, wishType, selectedProduct } = this.data; if (!wishContent.trim()) { wx.showToast({ title: '请输入许愿内容', icon: 'none' }); @@ -1066,7 +866,7 @@ Page({ type: 'wish', productId: selectedProduct.id, content: wishContent, - treeId: trees[currentTreeIndex].id, + treeId: 1, }, }); @@ -1080,7 +880,7 @@ Page({ data: { content: wishContent, type: 'free', - treeId: trees[currentTreeIndex].id, + treeId: 1, }, }); @@ -1120,41 +920,10 @@ 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; - wx.navigateTo({ - url: `/pages/wish-detail/wish-detail?id=${id}`, - }); - }, - // 分享 onShareAppMessage() { - const tree = this.getCurrentTree(); return { - title: `快来${tree.name || '许愿树'}许下你的愿望吧!`, + title: '快来许愿树许下你的愿望吧!', path: '/pages/wish-tree/wish-tree', }; }, diff --git a/mini/pages/wish-tree/wish-tree.wxml b/mini/pages/wish-tree/wish-tree.wxml index 3c42272..9e9f98b 100644 --- a/mini/pages/wish-tree/wish-tree.wxml +++ b/mini/pages/wish-tree/wish-tree.wxml @@ -1,22 +1,19 @@ - + - - - - - {{trees.length > 0 ? trees[currentTreeIndex].name : '许愿树'}} - - - + + {{treeName}} + {{treeDesc}} diff --git a/mini/pages/wish-tree/wish-tree.wxss b/mini/pages/wish-tree/wish-tree.wxss index 4ffd125..43c85f3 100644 --- a/mini/pages/wish-tree/wish-tree.wxss +++ b/mini/pages/wish-tree/wish-tree.wxss @@ -3,11 +3,11 @@ width: 100vw; height: 100vh; overflow: hidden; - background: #0f3460; + background: linear-gradient(180deg, #0a0a1a 0%, #1a1a3a 50%, #0f2a4a 100%); } -/* 全屏画布 */ -.wish-tree-canvas { +/* WebGL Canvas */ +.webgl-canvas { position: absolute; top: 0; left: 0; @@ -22,50 +22,35 @@ top: 0; left: 0; right: 0; - padding: 80rpx 30rpx 20rpx; + padding: 100rpx 40rpx 30rpx; display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; z-index: 100; pointer-events: none; } -.top-bar .tree-switcher, +.top-bar .tree-info, .top-bar .sensor-controls { pointer-events: auto; } -.tree-switcher { +.tree-info { display: flex; - align-items: center; - gap: 20rpx; - background: rgba(0, 0, 0, 0.3); - padding: 10rpx 20rpx; - border-radius: 40rpx; -} - -.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; + flex-direction: column; + gap: 8rpx; } .tree-name { - font-size: 32rpx; - color: #fff; + font-size: 40rpx; + color: #ffd700; font-weight: bold; - min-width: 150rpx; - text-align: center; + text-shadow: 0 2rpx 10rpx rgba(255, 215, 0, 0.5); +} + +.tree-desc { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.7); } /* 传感器控制 */ @@ -75,29 +60,31 @@ } .sensor-btn { - width: 70rpx; - height: 70rpx; + width: 80rpx; + height: 80rpx; display: flex; align-items: center; justify-content: center; - background: rgba(0, 0, 0, 0.3); + background: rgba(255, 255, 255, 0.1); border-radius: 50%; + border: 2rpx solid rgba(255, 255, 255, 0.2); 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); + background: rgba(255, 215, 0, 0.3); + border-color: rgba(255, 215, 0, 0.6); + box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.4); } .sensor-icon { - font-size: 36rpx; + font-size: 40rpx; } /* 许愿按钮 */ .action-bar { position: absolute; - bottom: 60rpx; + bottom: 80rpx; left: 50%; transform: translateX(-50%); z-index: 100; @@ -107,18 +94,26 @@ display: flex; align-items: center; justify-content: center; - gap: 12rpx; - background: linear-gradient(135deg, #C41E3A, #E74C3C); + gap: 16rpx; + background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 50%, #ff6b6b 100%); color: #fff; - font-size: 32rpx; - padding: 24rpx 60rpx; - border-radius: 50rpx; + font-size: 36rpx; + font-weight: bold; + padding: 28rpx 80rpx; + border-radius: 60rpx; border: none; - box-shadow: 0 8rpx 30rpx rgba(196, 30, 58, 0.4); + box-shadow: 0 10rpx 40rpx rgba(196, 30, 58, 0.5), + 0 0 60rpx rgba(255, 107, 107, 0.3); + transition: all 0.3s; +} + +.btn-wish:active { + transform: scale(0.95); + box-shadow: 0 5rpx 20rpx rgba(196, 30, 58, 0.4); } .btn-wish .icon { - font-size: 36rpx; + font-size: 44rpx; } /* 弹窗 */ @@ -137,7 +132,7 @@ left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.7); } .modal-content { @@ -145,34 +140,36 @@ bottom: 0; left: 0; right: 0; - background: #fff; - border-radius: 32rpx 32rpx 0 0; - max-height: 80vh; + background: linear-gradient(180deg, #2a2a4a 0%, #1a1a2e 100%); + border-radius: 40rpx 40rpx 0 0; + max-height: 85vh; overflow: hidden; + border-top: 2rpx solid rgba(255, 215, 0, 0.3); } .modal-header { display: flex; align-items: center; justify-content: space-between; - padding: 30rpx; - border-bottom: 1rpx solid #eee; + padding: 40rpx; + border-bottom: 1rpx solid rgba(255, 255, 255, 0.1); } .modal-title { - font-size: 32rpx; + font-size: 36rpx; font-weight: bold; - color: #333; + color: #ffd700; } .modal-close { - font-size: 48rpx; - color: #999; + font-size: 56rpx; + color: rgba(255, 255, 255, 0.5); padding: 10rpx; + line-height: 1; } .modal-body { - padding: 30rpx; + padding: 40rpx; max-height: 60vh; overflow-y: auto; } @@ -180,129 +177,140 @@ /* 类型选择 */ .type-selector { display: flex; - gap: 20rpx; - margin-bottom: 30rpx; + gap: 24rpx; + margin-bottom: 40rpx; } .type-item { flex: 1; - padding: 24rpx; - border: 2rpx solid #eee; - border-radius: 16rpx; + padding: 32rpx; + border: 2rpx solid rgba(255, 255, 255, 0.2); + border-radius: 20rpx; text-align: center; transition: all 0.3s; + background: rgba(255, 255, 255, 0.05); } .type-item.active { - border-color: #C41E3A; - background: #FFF5F5; + border-color: #ffd700; + background: rgba(255, 215, 0, 0.1); + box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.2); } .type-name { display: block; - font-size: 28rpx; + font-size: 32rpx; font-weight: bold; - color: #333; - margin-bottom: 8rpx; + color: #fff; + margin-bottom: 12rpx; } .type-desc { - font-size: 22rpx; - color: #999; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); } /* 输入框 */ .wish-input { width: 100%; - height: 200rpx; - padding: 20rpx; - border: 2rpx solid #eee; - border-radius: 16rpx; - font-size: 28rpx; + height: 240rpx; + padding: 28rpx; + border: 2rpx solid rgba(255, 255, 255, 0.2); + border-radius: 20rpx; + font-size: 30rpx; box-sizing: border-box; + background: rgba(255, 255, 255, 0.08); + color: #fff; +} + +.wish-input::placeholder { + color: rgba(255, 255, 255, 0.4); } .input-count { text-align: right; - font-size: 22rpx; - color: #999; - margin-top: 10rpx; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.5); + margin-top: 16rpx; } /* 商品选择 */ .product-section { - margin-top: 30rpx; + margin-top: 40rpx; } .section-title { - font-size: 28rpx; + font-size: 32rpx; font-weight: bold; - color: #333; - margin-bottom: 20rpx; + color: #ffd700; + margin-bottom: 24rpx; } .product-list { display: flex; flex-direction: column; - gap: 16rpx; + gap: 20rpx; } .product-item { display: flex; align-items: center; justify-content: space-between; - padding: 24rpx; - border: 2rpx solid #eee; - border-radius: 16rpx; + padding: 32rpx; + border: 2rpx solid rgba(255, 255, 255, 0.15); + border-radius: 20rpx; transition: all 0.3s; + background: rgba(255, 255, 255, 0.05); } .product-item.active { - border-color: #C41E3A; - background: #FFF5F5; + border-color: #ffd700; + background: rgba(255, 215, 0, 0.1); } .product-name { - font-size: 28rpx; + font-size: 32rpx; font-weight: bold; - color: #333; + color: #fff; } .product-desc { - font-size: 22rpx; - color: #999; + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); margin-top: 8rpx; } .product-price { - font-size: 32rpx; + font-size: 40rpx; font-weight: bold; - color: #C41E3A; + color: #ffd700; } /* 底部按钮 */ .modal-footer { display: flex; - gap: 20rpx; - padding: 30rpx; - border-top: 1rpx solid #eee; + gap: 24rpx; + padding: 40rpx; + border-top: 1rpx solid rgba(255, 255, 255, 0.1); } .modal-footer button { flex: 1; - padding: 24rpx; - border-radius: 50rpx; - font-size: 28rpx; + padding: 28rpx; + border-radius: 60rpx; + font-size: 32rpx; + font-weight: bold; } .btn-outline { - background: #fff; - border: 2rpx solid #C41E3A; - color: #C41E3A; + background: transparent; + border: 2rpx solid rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.8); } .btn-primary { - background: linear-gradient(135deg, #C41E3A, #E74C3C); + background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 100%); color: #fff; border: none; + box-shadow: 0 8rpx 30rpx rgba(196, 30, 58, 0.4); }