《ESP32 物联网全栈实战-11》小程序进阶

8 阅读2分钟

第 11 篇:小程序进阶——ECharts 图表 + 历史查询 + 告警推送

上篇做了一个能看能控的基础版。这篇加上三个进阶功能:温度曲线(ECharts)、历史数据查询(HTTP API + InfluxDB)、阈值告警推送。


1. 集成 ECharts 温度曲线

微信小程序用 echarts-for-weixin 组件。这是 ECharts 官方适配的小程序版。

安装

# 项目根目录
npm install ec-canvas
# 然后:开发者工具 → 工具 → 构建 npm

页面中引入

pages/history/history.json

{
  "usingComponents": {
    "ec-canvas": "../../ec-canvas/ec-canvas"
  },
  "navigationBarTitleText": "温度历史"
}

pages/history/history.wxml

<view class="container">
  <view class="chart-box">
    <ec-canvas id="tempChart" canvas-id="tempChart" ec="{{ec}}"></ec-canvas>
  </view>

  <view class="time-tabs">
    <button size="mini" bindtap="loadHistory" data-range="1h">近1小时</button>
    <button size="mini" bindtap="loadHistory" data-range="6h">近6小时</button>
    <button size="mini" bindtap="loadHistory" data-range="24h">近24小时</button>
  </view>
</view>

pages/history/history.js

import * as echarts from '../../ec-canvas/echarts';

Page({
  data: {
    ec: {
      // 懒加载:canvas 渲染完成后才初始化 ECharts
      lazyLoad: true,
    },
    tempData: [],
    humiData: [],
  },

  onLoad() {
    // 获取 ec-canvas 组件实例
    this.ecComponent = this.selectComponent('#tempChart');
    this.initChart();
    this.loadHistory('1h');
  },

  initChart() {
    this.ecComponent.init((canvas, width, height, dpr) => {
      const chart = echarts.init(canvas, null, {
        width: width,
        height: height,
        devicePixelRatio: dpr,
      });

      chart.setOption(this.getChartOption());
      this.chart = chart;
      return chart;
    });
  },

  getChartOption() {
    return {
      color: ['#ff6b6b', '#48dbfb'],
      legend: { data: ['温度', '湿度'], bottom: 0 },
      grid: { top: 20, bottom: 40, left: 50, right: 20 },
      xAxis: { type: 'time', axisLabel: { fontSize: 10 } },
      yAxis: [
        { type: 'value', name: '°C', min: 0, max: 50 },
        { type: 'value', name: '%',  min: 0, max: 100 },
      ],
      tooltip: { trigger: 'axis' },
      series: [
        {
          name: '温度', type: 'line', smooth: true,
          data: [], yAxisIndex: 0,
        },
        {
          name: '湿度', type: 'line', smooth: true,
          data: [], yAxisIndex: 1,
        },
      ],
    };
  },

  async loadHistory(range) {
    wx.showLoading({ title: '加载中...' });

    try {
      const res = await wx.request({
        url: `https://api.your-server.com/history?range=${range}`,
        method: 'GET',
      });

      const tempData = res.data.temp.map(d => [new Date(d.time), d.value]);
      const humiData = res.data.humi.map(d => [new Date(d.time), d.value]);

      this.chart.setOption({
        series: [
          { data: tempData },
          { data: humiData },
        ],
      });
    } catch (err) {
      wx.showToast({ title: '加载失败', icon: 'error' });
    } finally {
      wx.hideLoading();
    }
  },
});

2. 后端 API——从 InfluxDB 查历史数据

小程序不能直接连数据库,需要一个中间 API 服务。最简单的方案:用 Node.js/Express 写一个轻量 API,跑在服务器上。

// api-server.js (Node.js + Express)
const express = require('express');
const { InfluxDB } = require('@influxdata/influxdb-client');

const app = express();
const influx = new InfluxDB({
  url: 'http://localhost:8086',
  token: 'my-super-secret-token',
});

app.get('/history', async (req, res) => {
  const range = req.query.range || '1h';  // 1h / 6h / 24h

  const queryApi = influx.getQueryApi('iot-org');
  const query = `
    from(bucket: "sensor-data")
      |> range(start: -${range})
      |> filter(fn: (r) => r._measurement == "mqtt_consumer")
      |> filter(fn: (r) => r._field == "temp" or r._field == "humi")
      |> aggregateWindow(every: ${range === '1h' ? '30s' : '5m'}, fn: mean)
  `;

  const temp = [], humi = [];
  for await (const {values, row} of queryApi.iterateRows(query)) {
    const point = { time: row[2], value: row[5] };
    if (row[4] === 'temp') temp.push(point);
    else humi.push(point);
  }

  res.json({ temp, humi });
});

app.listen(3000, () => console.log('API Server on :3000'));

3. 阈值告警推送

方案 A:小程序内告警

// 在 MQTT message 回调中
this.client.on('message', (topic, payload) => {
  const data = JSON.parse(payload.toString());

  if (data.temp > 40) {
    wx.showModal({
      title: '⚠️ 高温警告',
      content: `当前温度 ${data.temp}°C,超过阈值 40°C!`,
      showCancel: false,
    });

    // 播放提示音(小程序支持)
    wx.vibrateLong();
  }

  this.setData({
    temperature: data.temp.toFixed(1),
    humidity: data.humi.toFixed(1),
  });
});

方案 B:微信服务通知(需要订阅消息)

流程:
1. 用户点击"订阅告警"按钮 → 调用 wx.requestSubscribeMessage
2. 用户同意 → 拿到一次推送权限
3. 后端检测到温度超标 → 调用微信 API 推送模板消息
// 小程序端:订阅
wx.requestSubscribeMessage({
  tmplIds: ['模板ID_高温告警'],
  success(res) {
    if (res['模板ID_高温告警'] === 'accept') {
      // 保存用户 openid → 后端用于推送
    }
  },
});

4. 增加刷新与重连机制

IoT 小程序最大的痛点是"不知道数据为什么不更新了"。增强健壮性:

Page({
  data: { connected: false, lastUpdate: '' },

  onLoad() {
    this.connectMQTT();
    // 每 30 秒检查一次 MQTT 心跳
    this.heartbeatTimer = setInterval(() => {
      if (!this.data.connected) {
        console.log('重连中...');
        this.connectMQTT();
      }
    }, 30000);
  },

  // 记录最后更新时间
  updateLastTime() {
    const now = new Date();
    this.setData({
      lastUpdate: `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`,
    });
  },

  // 手动刷新
  refreshData() {
    this.client.publish('device/refresh/cmd', '1');
  },

  onUnload() {
    clearInterval(this.heartbeatTimer);
    if (this.client) this.client.end();
  },
});

5. 调试技巧

问题排查方法
MQTT 连不上检查 wss:// 域名是否已配置到白名单
数据不更新打开开发者工具 → Console → 看有没有 message 日志
ECharts 白屏检查 ec-canvas 组件路径是否正确
API 请求失败检查服务器域名白名单(request 合法域名)
真机不显示预览时勾选"不校验合法域名"仅开发有效,真机必须配置

下一篇:完整项目——智能 WiFi 插座,从硬件到小程序全链路