支持动态展示修改数据,支持导出图片

WXML

<wxs src="/wxs/cdn.wxs" module="cdn" />
<nav-bar id="nav-bar" isInvert="{{true}}" defShowTitle="{{false}}" showBack="{{true}}" pageTitle='作息统计'></nav-bar>
<view class="schedule-container" style="padding-top:{{navigationBarHeight}}px;">
  <!-- 头部 -->
  <view class="header">
    <auth-btn>
      <view wx:if="{{userInfo.nickname}}" class="user-name">你好 {{userInfo.nickname}}</view>
      <view wx:else class="user-name" style="text-decoration-line: underline;">点击登录</view>
      <view class="header-desc">这是你的打卡回顾</view>
    </auth-btn>
    <view>
      <image class="user-avatar" mode="aspectFit" src="{{userInfo.avatar?userInfo.avatar:cdn.image('mine_header_default.png')}}"></image>
    </view>
  </view>
  <!-- 内容 -->
  <view class="content">
    <view class="statistics-box">
      <view class="flex-col wake-days-box">
        <view class=" flex-row-spbt-cen">
          <view class="flex-col statistics-title">
            <view>累计早打卡</view>
            <view class="flex-row-start-cen" style="margin-top: 16rpx;">
              <text class="statistics-desc">{{userInfo.wake_total_days || 0}}</text>天
            </view>
          </view>
          <image class="mine-cloud-icon" mode="aspectFit" src="{{cdn.image('mine_rise_icon.png')}}"></image>
        </view>
        <view class="continuous-number">
          已连续打卡{{userInfo.wake_days || 0}}天
        </view>
      </view>
      <view class="flex-col sleep-days-box">
        <view class=" flex-row-spbt-cen">
          <view class="flex-col statistics-title">
            <view>累计晚打卡</view>
            <view class="flex-row-start-cen" style="margin-top: 16rpx;">
              <text class="statistics-desc">{{userInfo.sleep_total_days || 0}}</text>天
            </view>
          </view>
          <image class="mine-cloud-icon" mode="aspectFit" src="{{cdn.image('mine_sleep_icon.png')}}"></image>
        </view>
        <view class="continuous-number">
          已连续打卡{{userInfo.sleep_days || 0}}天
        </view>
      </view>
    </view>
    <view class="tab-box">
      <view style="line-height: 45rpx;">作息统计报告</view>
      <view class="flex-row-start-cen mt24">
        <view bind:tap="tabChange" data-index="1" class="{{tabIndex==1?'active-tab':'default-tab'}}">近7天</view>
        <view bind:tap="tabChange" data-index="2" class="{{tabIndex==2?'active-tab':'default-tab'}}" style="margin-left: 16rpx;">近30天</view>
      </view>
    </view>
    <view wx:if="{{!allEmpty}}" class="charts-content">
      <!-- 平均睡眠时长 -->
      <view class="charts-box">
        <view class="flex-row-spbt-cen">
          <view class="charts-title">睡眠时长报告</view>
          <view wx:if="{{sleepBarData.xData.length}}" class="charts-download" data-type="1" bind:tap="downLoadImage">保存
            <image class="charts-download-icon" src="{{cdn.image('rest_schedule_download_icon.png')}}" mode="aspectFit" />
          </view>
        </view>
        <view wx:if="{{sleepBarData.xData.length}}" class="average-duration">
          近{{tabIndex==1?'7':'30'}}天平均睡眠时长{{averageDuration}}小时
        </view>
        <view class="echart-view" wx:if="{{sleepBarData.xData.length}}">
          <ec-canvas id="time-range-chart" canvas-id="time-range-chart" ec="{{ timeRangeEc }}"></ec-canvas>
        </view>
        <view wx:else class="empty-box">
          <view class="empty-text">当日晚打卡和次日早打后,才可计算睡眠时长哦</view>
          <van-button bind:click="routeToHome" custom-class="empty-button">去打卡</van-button>
        </view>
      </view>
      <!-- 早打卡趋势 -->
      <view class="charts-box">
        <view class="flex-row-spbt-cen">
          <view class="charts-title">早起打卡趋势报告</view>
          <view wx:if="{{riseLineData.xData.length}}" class="charts-download" data-type="2" bind:tap="downLoadImage">保存
            <image class="charts-download-icon" src="{{cdn.image('rest_schedule_download_icon.png')}}" mode="aspectFit" />
          </view>
        </view>
        <view class="echart-view" wx:if="{{riseLineData.xData.length}}">
          <ec-canvas id="rise-chart" canvas-id="rise-chart" ec="{{ riseEc }}"></ec-canvas>
        </view>
        <view class="empty-box" wx:else>
          <view class="empty-text-tip">
            暂无数据,记得按时打卡哦~
          </view>
        </view>

      </view>
      <!-- 晚打卡趋势 -->
      <view class="charts-box">
        <view class="flex-row-spbt-cen">
          <view class="charts-title">早睡打卡趋势报告</view>
          <view wx:if="{{sleepLineData.xData.length}}" class="charts-download" data-type="3" bind:tap="downLoadImage">保存
            <image class="charts-download-icon" src="{{cdn.image('rest_schedule_download_icon.png')}}" mode="aspectFit" />
          </view>
        </view>
        <view class="echart-view" wx:if="{{sleepLineData.xData.length}}">
          <ec-canvas id="sleep-chart" canvas-id="sleep-chart" ec="{{ sleepEc }}"></ec-canvas>
        </view>
        <view class="empty-box" wx:else>
          <view class="empty-text-tip">
            暂无数据,记得按时打卡哦~
          </view>
        </view>
      </view>
    </view>
    <view wx:else class="empty-box" style="margin-top: 64rpx;">
      <view class="empty-text" style="font-weight: 600;">当日晚打卡和次日早打后,才可计算睡眠时长哦</view>
      <van-button bind:click="routeToHome" custom-class="empty-button">去打卡</van-button>
    </view>

  </view>
</view>
<!--分享弹窗-->
<van-overlay lock-scroll="{{true}}" z-index="9999" show="{{ shareShow }}">
  <view style="height:100%;width: 100%;" class="flex-col-cen-cen">
    <image show-menu-by-longpress="true" src="{{shareImagePath}}" style="width: 630rpx;height: 800rpx;" mode="aspectFit" />
    <van-button bind:click="clickToSave" custom-class="poster-save-btn">
      保存到相册
    </van-button>
    <image class="close-btn" src="{{cdn.image('advert_close.png')}}" catchtap="closeShare"></image>
  </view>
</van-overlay>

<!-- 海报画板 -->
<view style="position:fixed;left: -2000rpx;top: 0;" wx:if="{{showCanvas}}">
  <wxml-to-canvas height="{{cvsHeight}}" width="{{cvsWidth}}" class="widget"></wxml-to-canvas>
</view>
<!-- 隐私接口协议 -->
<privacy id="privacy-timeImage"></privacy>

WXSS

.schedule-container {
  background: #6145F0;
}

.header {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 0 48rpx 0rpx 32rpx;
  height: 166rpx;
}

.user-name {
  color: #FFF;
  margin-top: 24rpx;
  font-family: PingFang SC;
  font-size: 40rpx;
  font-style: normal;
  font-weight: 600;
  line-height: 56rpx;
}

.user-avatar {
  width: 110rpx;
  height: 110rpx;
  border-radius: 50%;
  margin-top: 17rpx;
}

.header-desc {
  color: #FFF;
  margin-top: 8rpx;
  font-family: PingFang TC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 400;
  line-height: 39rpx;
}

/*内容*/
.content {
  border-radius: 64rpx 64rpx 0 0;
  background-color: #fff;
  width: 100%;

  box-sizing: border-box;
  padding-bottom: env(safe-area-inset-bottom);
}

.statistics-box {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 40rpx 32rpx;
}

.box-title {
  font-size: 32rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 600;
  color: #333333;
  line-height: 45rpx;
}

.wake-days-box {
  border-radius: 16rpx;
  background: rgba(255, 249, 229, 0.40);
  padding: 16rpx 16rpx 16rpx 32rpx;
  width: 331rpx;
  box-sizing: border-box;
}

.sleep-days-box {
  border-radius: 16rpx;
  background: rgba(240, 229, 255, 0.40);
  padding: 16rpx 16rpx 16rpx 32rpx;
  width: 331rpx;
  box-sizing: border-box;
}

.mine-cloud-icon {
  height: 96rpx;
  width: 96rpx;
  
  margin-top: 2rpx;
}

.statistics-title {
  color: #333;
  font-family: PingFang SC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 400;
  line-height: 39rpx
}

.statistics-desc {
  color: #697AF6;
  font-family: PingFang SC;
  font-size: 32rpx;
  font-style: normal;
  font-weight: 600;
  line-height: 45rpx;
}

.continuous-number {
  color: #333;
  margin-top: 11rpx;
  font-family: PingFang SC;
  font-size: 20rpx;
  font-style: normal;
  font-weight: 400;
  line-height: 28rpx;
}

.tab-box {
  padding: 0 32rpx;
  color: #000;
  font-family: PingFang SC;
  font-size: 32rpx;
  font-style: normal;
  font-weight: 600;
 
}

.active-tab {
  border-radius: 32px;
  background: #6145F0;
  color: #FFF;
  padding: 12rpx 48rpx;
  text-align: center;
  font-family: PingFang SC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 600;
  line-height: 40rpx;
}

.default-tab {
  border-radius: 32px;
  background: #ECECFF;
  color: #333;
  padding: 12rpx 48rpx;
  text-align: center;
  font-family: PingFang SC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 400;
  line-height: 40rpx;
}

/*图表盒子*/
.charts-content {
  padding: 8rpx 32rpx 60rpx;
}

.charts-box {
  margin-top: 24rpx;
  padding: 24rpx;
  border-radius: 16rpx;
  background: #F9F9FA;
}

.charts-title {
  color: #333;
  font-family: PingFang SC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 600;
  line-height: 39rpx
}

.charts-download {
  color: #697AF6;
  font-family: PingFang SC;
  font-size: 24rpx;
  font-style: normal;
  font-weight: 600;
  line-height: 34rpx;
  display: flex;
  flex-direction: row;
  align-items: center;
}

.charts-download-icon {
  margin-left: 9rpx;
  width: 26rpx;
  height: 26rpx;
}

.average-duration {
  border-radius: 16rpx;
  background: rgba(240, 229, 255, 0.40);
  color: #333;
  padding: 16rpx;
  font-family: PingFang SC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 400;
  line-height: 39rpx;
  margin-top: 24rpx;
  display: inline-block;
}

.echart-view {
  background: #F9F9FA;
  width: 638rpx;
  height: 365rpx;
  margin-top: 24rpx;
}

/*----------空状态---------*/

.empty-box {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.empty-text {
  color: #333;
  margin-top: 48rpx;
  text-align: center;
  font-family: PingFang SC;
  font-size: 28rpx;
  font-style: normal;
  font-weight: 400;
  line-height: 39rpx;
}

.empty-button {
  margin-top: 40rpx;
  margin-bottom: 32rpx;
  width: 534rpx !important;
  height: 91rpx !important;
  border-radius: 100rpx !important;
  background: linear-gradient(180deg, #FFF189 1.97%, #FEBA40 100%) !important;
  box-shadow: 1rpx -1rpx 3rpx 0rpx rgba(255, 255, 255, 0.80) inset !important;
  border: transparent !important;
  color: #333 !important;
  font-size: 32rpx !important;
  font-weight: 600 !important;
  line-height: 48rpx !important;
}

.empty-text-tip{
  color: #999;
  margin-top: 64rpx;
  margin-bottom: 40rpx;
font-family: PingFang SC;
font-size: 28rpx;
font-style: normal;
font-weight: 400;
line-height: 39rpx;
}
/*弹窗*/
.poster-save-btn{
  width: 371rpx !important;
  height: 88rpx !important;
  border-radius: 50rpx!important;
  color: #333!important;
text-align: center !important;
font-size: 32rpx !important;
font-weight: 600!important;
line-height: 88px !important;
background: #FCD34F !important;
border: transparent !important;
margin-top: 50rpx;
}

.close-btn{
  margin-top: 40rpx;
  width: 52rpx;
height: 52rpx;
}

JSON

{
  "usingComponents": {
    "ec-canvas": "../components/ec-canvas/ec-canvas",
    "wxml-to-canvas": "../../miniprogram/miniprogram_npm/wxml-to-canvas/index",
    "nav-bar":"../../components/navBar/navBar",
    "van-button": "../../miniprogram/miniprogram_npm/@vant/weapp/button/index",
    "auth-btn": "../../components/authBtn/authBtn",
    "van-overlay": "../../miniprogram/miniprogram_npm/@vant/weapp/overlay/index",
    "privacy": "../../components/privacy/privacy"
  },
  "navigationStyle": "custom",
  "navigationBarTextStyle": "white"
}

js

import * as echarts from '../components/ec-canvas/echarts';
import echartsOption from './echartsOption.js';
import {
  CDN_PREFIX
} from '../../utils/key'
import {
  QRURL
} from '../../utils/key'
import local from '../../utils/local';
import {
  apiGetStatisticsChart,
  apiGetQrCode
} from '../../http/api'
import {
  getNavbarInfo
} from '../../utils/navigationBar'

const app = getApp()
const {
  events,
} = app.globalData
const {
  wxml,
  style
} = require('./image.js')



const BASE_TIME_MAX = "2023-10-02";
const BASE_TIME_MIN = "2023-10-01";
Page({

  /**
   * 页面的初始数据
   */
  data: {
    navigationBarHeight: '',
    tabIndex: 1, //1:7天 2:30天
    averageDuration: 0,
    ratio: 0.5,
    allEmpty: false,
    shareImagePath: '',
    tempFilePath: '',
    showCanvas: false,
    shareShow: false,
    cvsHeight: '',
    cvsWeight: '',
    userInfo: {},
    sleepLineData: {
      xData: [],
      yData: []
    },
    riseLineData: {
      xData: [],
      yData: []
    },
    sleepBarData: {
      xData: [],
      yData: []
    },
    sleepEc: {
      lazyLoad: true // 懒加载
    },
    riseEc: {
      lazyLoad: true // 懒加载
    },
    timeRangeEc: {
      lazyLoad: true // 懒加载
    },
    qrImg: '',

  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    let navigationBarHeight = getApp().globalNavbarInfo.navigationBarHeight
    let ratio = wx.getSystemInfoSync().windowWidth / 750
    this.setData({
      navigationBarHeight,
      ratio: ratio
    })



    // 监听微信登录成功
    events.on('on-launch-executed', this, async res => {
      this.initialData()
    })
    this.initialData()
  },

  initialData() {
    let userInfo = getApp().globalData.userInfo
    this.setData({
      userInfo
    })
    this.getChartsData()
  },
  async getChartsData() {
    wx.showLoading({
      title: '加载中...',
      mask:true
    })
    const res = await apiGetStatisticsChart({
      type: this.data.tabIndex
    })
    if (res.code == 200) {
      let average = res.data.average
      let sleep = res.data.sleep
      let wake = res.data.wake
      let averageIsEmpty = this.isAllZerosOrEmpty(average)
      let sleepIsEmpty = this.isAllZerosOrEmpty(sleep)
      let wakeIsEmpty = this.isAllZerosOrEmpty(wake)
      if (averageIsEmpty && sleepIsEmpty && wakeIsEmpty) {
        this.setData({
          allEmpty: true
        })
        wx.hideLoading()
        return
      } else {
        this.setData({
          allEmpty: false
        })
      }
      if (!averageIsEmpty) {
        this.getTimeRangeData(average)
      } else {
        this.setData({
          ['sleepBarData.xData']: [],
          ['sleepBarData.yData']: [],
        })
      }
      if (!sleepIsEmpty) {
        this.getSleepData(sleep)
      } else {
        this.setData({
          ['sleepLineData.xData']: [],
          ['sleepLineData.yData']: [],
        })
      }
      if (!wakeIsEmpty) {
        this.getRiseData(wake)
      } else {
        this.setData({
          ['riseLineData.xData']: [],
          ['riseLineData.yData']: [],
        })
      }
    } else {
      this.setData({
        allEmpty: true
      })
    }
    wx.hideLoading()

  },
  //判断对象是否为空
  isAllZerosOrEmpty(obj) {
    for (let value of Object.values(obj)) {
      if (value !== 0 && value !== '' && value !== null) {
        if (typeof value === 'object') {
          // 如果值是对象,递归判断
          if (!this.isAllZerosOrEmpty(value)) {
            return false;
          }
        } else {
          // 值不为0、''、null或对象
          return false;
        }
      }
    }
    return true;

  },

  //切换时间
  tabChange(e) {
    let tabIndex = e.currentTarget.dataset.index
    this.setData({
      tabIndex
    }, () => {
      this.getChartsData()
    })
  },

  getTimeRangeData(average) {

    const keys = Object.keys(average);
    const values = Object.values(average);
    let averageArr = []
    let formatData = values.map(res => {
      if (res) {
        let num = (res / (60 * 60)).toFixed(1)
        averageArr.push(Number(num))
        return num
      } else {
        return ''
      }
    })
    let averageDuration = averageArr.reduce((a, b) => a + b) / averageArr.length;
    const sleepBarData = this.data.sleepBarData
    sleepBarData.xData = keys
    sleepBarData.yData = formatData
    this.setData({
      sleepBarData,
      averageDuration: averageDuration.toFixed(1)
    })
    this.initTimeRangeChart(sleepBarData)
  },
  initTimeRangeChart(sleepBarData) {
    let tabIndex = this.data.tabIndex
    let ratio = this.data.ratio
    // 绑定组件
    this.barComponent = this.selectComponent("#time-range-chart");
    // 初始化柱状图
    this.barComponent.init((canvas, width, height, dpr) => {
      // 初始化图表
      const chart = echarts.init(canvas, null, {
        width: width,
        height: height,
        devicePixelRatio: dpr // 解决模糊显示问题
      })

      // 开发中根据从后端获取sleepLineData数据,动态更新图表
      chart.setOption(echartsOption.sleepBarOption(sleepBarData, tabIndex, ratio, echarts))
      return chart
    })
  },

  getSleepData(sleep) {
    const keys = Object.keys(sleep);
    const values = Object.values(sleep);
    let formatData = values.map(res => {
      if (res) {
        let timeStr = res.time.slice(10, 19)
        return {
          value: res.type == 1 ? BASE_TIME_MIN + timeStr : BASE_TIME_MAX + timeStr,
          realTime: res.time,
        }
      } else {
        return {
          value: '',
          realTime: ''
        }
      }
    })
    const sleepLineData = this.data.sleepLineData
    sleepLineData.xData = keys
    sleepLineData.yData = formatData
    this.setData({
      sleepLineData
    })
    this.initSleepChart(sleepLineData)
  },
  initSleepChart(sleepLineData) {
    let tabIndex = this.data.tabIndex
    let ratio = this.data.ratio
    // 绑定组件
    this.barComponent = this.selectComponent("#sleep-chart");
    // 初始化柱状图
    this.barComponent.init((canvas, width, height, dpr) => {
      // 初始化图表
      const chart = echarts.init(canvas, null, {
        width: width,
        height: height,
        devicePixelRatio: dpr // 解决模糊显示问题
      })
      // 开发中根据从后端获取sleepLineData数据,动态更新图表
      chart.setOption(echartsOption.sleepLineOption(sleepLineData, BASE_TIME_MIN, BASE_TIME_MAX, tabIndex, ratio, echarts))
      return chart
    })
  },
  getRiseData(wake) {
    const keys = Object.keys(wake);
    const values = Object.values(wake);
    let formatData = values.map(res => {
      if (res) {
        let timeStr = res.time.slice(10, 19)
        return {
          value: BASE_TIME_MIN + timeStr,
          realTime: res.time,
        }
      } else {
        return {
          value: '',
          realTime: ''
        }
      }
    })
    const riseLineData = this.data.riseLineData
    riseLineData.xData = keys
    riseLineData.yData = formatData
    this.setData({
      riseLineData
    })
    this.initRiseChart(riseLineData)
  },
  initRiseChart(riseLineData) {
    let tabIndex = this.data.tabIndex
    let ratio = this.data.ratio
    // 绑定组件
    this.barComponent = this.selectComponent("#rise-chart");
    // 初始化柱状图
    this.barComponent.init((canvas, width, height, dpr) => {
      // 初始化图表
      const chart = echarts.init(canvas, null, {
        width: width,
        height: height,
        devicePixelRatio: dpr // 解决模糊显示问题
      })
      // 开发中根据从后端获取sleepLineData数据,动态更新图表
      chart.setOption(echartsOption.riseLineOption(riseLineData, BASE_TIME_MIN, BASE_TIME_MAX, tabIndex, ratio, echarts))
      return chart
    })
  },
  routeToHome() {
    wx.switchTab({
      url: '/pages/index/index',
    })
  },
  // 获取二维码
  async getGetQrCode() {
    var url = local.getSync(QRURL) || ''
    if (!url) {
      let res = await apiGetQrCode()
      if (!res.data) {
        console.log('请求二维码失败');
        return
      }
      url = res.data.url
    }
    this.setData({
      qrImg: url
    })
  },

  async downLoadImage(e) {
    wx.showLoading({
      title: '生成中...',
      mask:true
    })
    let type = e.currentTarget.dataset.type
    //画板准备
    let cvsHeight = 848
    let cvsWidth = 630
    this.setData({
      cvsHeight: cvsHeight,
      cvsWidth: cvsWidth,
      showCanvas: true,
    }, () => {
      this.widget = this.selectComponent('.widget')
      this.widget.setData({
        top: 0,
        left: 0,
        height: this.data.cvsHeight,
        width: this.data.cvsWidth,
      });
    })
    //生成echart图片
    await this.getGetQrCode()
    let tempFilePath = ''
    let title = ''
    if (type == 1) {
      tempFilePath = await this.drawImage1()
      title = '睡眠时长报告'
    } else if (type == 2) {
      tempFilePath = await this.drawImage2()
      title = '早起打卡趋势报告'
    } else if (type == 3) {
      tempFilePath = await this.drawImage3()
      title = '早睡打卡趋势报告'
    }
    this.setData({
      tempFilePath
    }, () => {
      this.renderToCanvas(title)
    })
  },

  /**
   * 绘图
   */

  drawImage1() {
    return new Promise((resolve, reject) => {
      let ecComonnets = this.selectComponent('#time-range-chart')
      ecComonnets.canvasToTempFilePath({
        success: res => {
          console.log("tempFilePath", res.tempFilePath)
          resolve(res.tempFilePath)
        },
        fail: (err) => {
          resolve('')
        }
      })
    })
  },
  drawImage2() {
    return new Promise((resolve, reject) => {
      let ecComonnets = this.selectComponent('#rise-chart')
      ecComonnets.canvasToTempFilePath({
        success: res => {
          console.log("tempFilePath", res.tempFilePath)
          resolve(res.tempFilePath)
        },
        fail: (err) => {
          resolve('')
        }
      })
    })
  },
  drawImage3() {
    return new Promise((resolve, reject) => {
      let ecComonnets = this.selectComponent('#sleep-chart')
      ecComonnets.canvasToTempFilePath({
        success: res => {
          console.log("tempFilePath", res.tempFilePath)
          resolve(res.tempFilePath)
        },
        fail: (err) => {
          resolve('')
        }
      })
    })
  },

  //绘画
  async renderToCanvas(title) {
    let cardInfo = {
      title: title,
      nickname: this.data.userInfo.nickname || '',
      avatar: this.data.userInfo.avatar || '',
      averageDuration: this.data.averageDuration || 0,
      tempFilePath: this.data.tempFilePath || '',
      wake_total_days: this.data.userInfo.wake_total_days || 0,
      sleep_total_days: this.data.userInfo.sleep_total_days || 0,
      qrImg: this.data.qrImg || '',
      maskImage: `${CDN_PREFIX}chart_share_mask_gu.png`,
      tabName: this.data.tabIndex == 1 ? '7' : '30'
    }
    let wakeFontWight = cardInfo.wake_total_days.toString().length
    let sleepFontWight = cardInfo.sleep_total_days.toString().length
    const _wxml = wxml(cardInfo)
    const _style = style(wakeFontWight,sleepFontWight)
    const p1 = this.widget.renderToCanvas({
      wxml: _wxml,
      style:_style
    })
    p1.then((res) => {
      this.extraImage()
    }).catch(function () { 
      wx.hideLoading()
    });
   
  },
  //   绘制图片
  extraImage() {
    const p2 = this.widget.canvasToTempFilePath()
    p2.then(res => {
      console.log('绘制完成src:', res.tempFilePath)
      wx.hideLoading()
      this.setData({
        shareImagePath: res.tempFilePath,
        showCanvas: false
      }, () => {
        this.setData({
          shareShow: true
        })
      })
    }).catch(function () { 
      wx.hideLoading()
    });
  },
  closeShare() {
    this.setData({
      shareShow: false
    })
  },
  // 下载图片
  clickToSave() {
    let imageUrl = this.data.shareImagePath
    wx.getSetting({
      success: (res) => {
        if (!res.authSetting['scope.writePhotosAlbum']) {
          wx.authorize({
            scope: 'scope.writePhotosAlbum',
            success: () => {
              // 用户已经同意小程序使用相册功能
              this.saveImg(imageUrl);
            },
            fail: (e) => {
              wx.showModal({
                title: '保存失败',
                content: '请检查是否授权了相册权限',
                success: ({
                  confirm
                }) => {
                  if (confirm) {
                    wx.openSetting({
                      success: ({
                        authSetting
                      }) => {
                        console.log(authSetting);
                        if (authSetting['scope.writePhotosAlbum']) {
                          this.saveImg(imageUrl);
                        }
                      }
                    })
                  } else {
                    // 用户取消授权,不进行任何操作
                  }
                }
              });
            }
          });
        } else {
          this.saveImg(imageUrl);
        }
      }
    });
  },
  saveImg(imageUrl) {
    wx.saveImageToPhotosAlbum({
      filePath: imageUrl,
      success: () => {
        wx.showToast({
          title: '保存成功',
        })
      }
    });
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady() {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {
    const caniuse = wx.canIUse('onNeedPrivacyAuthorization')
    caniuse && wx.onNeedPrivacyAuthorization(resolve => {
      // 需要用户同意隐私授权时
      // 弹出开发者自定义的隐私授权弹窗
      const privacy = this.selectComponent('#privacy-timeImage')
      privacy.show({
        resolvePrivacy: resolve
      })
    })

  },
  /**
   * 监听滚动事件
   */
  onPageScroll(e) { //nvue暂不支持滚动监听,可用bindingx代替
    let scrollTop = e.scrollTop
    let navBar = this.selectComponent('#nav-bar')
    let navInfo = getNavbarInfo()
    navBar.setBgColor(scrollTop)
    if (e.scrollTop < navInfo.navigationBarHeight) {
      wx.setNavigationBarColor({
        frontColor: '#ffffff',
        backgroundColor: '#ff0000',
        animation: {
          duration: 1,
          timingFunc: 'easeIn'
        }
      })
    } else {
      wx.setNavigationBarColor({
        frontColor: '#000000',
        backgroundColor: '#ff0000',
        animation: {
          duration: 1,
          timingFunc: 'easeIn'
        }
      })
    }
  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide() {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload() {
    events.remove('on-launch-executed', this)
  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage() {

  }
})

OptionJS

function sleepBarOption(data, tabIndex,ratio,echarts,) {
  let barData = data;
  var option = {
    
    backgroundColor: '#f9f9fa',
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'line' //选中区域使用线条
      },
      formatter: function (params, ticket, callback) {
        let showHtm = params[0].name + '\n';
        let seriesName = params[0].seriesName+":"
        let marker =params[0].marker ;
        let value = params[0].value? params[0].value+'小时':'-'
        showHtm += marker+seriesName+value
        return showHtm;
        },
      textStyle: {
        //去除安卓手机错误阴影
        textShadowBlur: 10,
        textShadowColor: "transparent"
      },
      confine: true //是否将 tooltip 框限制在图表的区域内
    },

    xAxis: {
      type: 'category',
      boundaryGap:true,
      data: barData.xData,
      axisLine: {
        lineStyle: {
          color: '#999',
          width:2*ratio
        },
        
      },
      axisPointer: {
        type: 'line'
      },
      axisTick: {
        show: true,
        alignWithLabel: true
      },
      
      axisLabel: {
        fontSize:28*ratio,
        color:'#333333',
        interval:tabIndex==1?1:6,
        formatter: function (value) {
          let time = echarts.format.formatTime('MM-dd', value)
          // 格式化时间
          return time
        },
      },
    },
    yAxis: {
     
      splitLine: {
        show: false
      },
      axisLabel: {
        align: 'right',
        fontSize:28*ratio,
        color:'#333333'
      },
    },
    grid: {
      left:60*ratio,
      width:537*ratio,
      height:275*ratio,
      top:40*ratio
    },
    series: [
      {
        name: '睡眠时长',
        type: 'bar',
        barWidth: tabIndex==1?10:5,
        itemStyle: {
          borderRadius: [5,5,0,0],
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: '#D9BBF7' },
            { offset: 1, color: '#674FF5' }
          ])
        },
        data: barData.yData
      },
    ]
  };
  return option;
}
function sleepLineOption(data, BASE_TIME_MIN, BASE_TIME_MAX,tabIndex,ratio, echarts) {

  let lineData = data;
  var option = {
    backgroundColor: '#f9f9fa',
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'line' //选中区域使用线条
      },
      
      textStyle: {
        //去除安卓手机错误阴影
        textShadowBlur: 10,
        textShadowColor: "transparent",
     
      },
      formatter: function (params, ticket, callback) {
        let showHtm = params[0].name + '\n';
        let seriesName = params[0].seriesName
        let realTime = params[0].data.realTime
        if(realTime){
          realTime = realTime.slice(5,16)
        }else{
          realTime = '-'
        }
        let marker =params[0].marker ;
        showHtm += marker+seriesName+' : ' + realTime
        return showHtm;
        },
      confine: true //是否将 tooltip 框限制在图表的区域内
    },
    xAxis: {
      type: 'category',
      boundaryGap:true,
      data: lineData.xData,
      axisLine: {
        lineStyle: {
          color: '#999',
          width:2*ratio
        }
      },
      axisPointer: {
        type: 'line'
      },
      axisTick: {
        show: true,
        alignWithLabel: true
      },
      axisLabel: {
        formatter: function (value) {
          let time = echarts.format.formatTime('MM-dd', value)
          // 格式化时间
          return time
        },
        fontSize:28*ratio,
        color:'#333333',
        align: 'center',
        interval:tabIndex==1?1:6,
      },


    },
    yAxis: {
      type: 'time',
      min: `${BASE_TIME_MIN} 20:00:00`,
      max: `${BASE_TIME_MAX} 04:00:00`,
      minInterval: 3600 * 1000 * 3,
      axisLabel: {
        align: 'right',
        fontSize:28*ratio,
        color:'#333333',
        formatter: function (value) {
          let time = echarts.format.formatTime('hh:mm', value)
          // 格式化时间
          return time
        }
      },
      splitLine: {
        show: false
      },
      
    },
    grid: {
      left:90*ratio,
      width:517*ratio,
      height:275*ratio,
      top:40*ratio
    },
    series: [{
      name: '晚打卡时间',
      type: 'line',
      smooth: true,
      showAllSymbol: true,
      connectNulls: true,
      symbol: 'circle',
      symbolSize: tabIndex==1?10:5,
      lineStyle:{
        color:'#bdc4ff',
        width:tabIndex==1?3:1,
      },
      itemStyle:{
        color :'#654FF5'
      },
      data: lineData.yData,
    }, 
    {
      name: '晚打卡时间',
      type: 'bar',
      barGap: '-100%',
      barWidth: tabIndex==1?10:5,
      symbol: 'rect',
      itemStyle: {
        borderRadius: [5,5,0,0],
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: 'rgba(219,191,247,0.8)' },
          { offset: 0.2, color: 'rgba(219,191,247,0.5)' },
          { offset: 1, color: 'rgba(219,191,247,0)' }
        ])
      },
  
      data: lineData.yData
    },
    
  ]
  };

  return option;
}
function riseLineOption(data, BASE_TIME_MIN, BASE_TIME_MAX,tabIndex,ratio, echarts) {
  let lineData = data;
  var option = {
    backgroundColor: '#f9f9fa',
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'line' //选中区域使用线条
      },
      textStyle: {
        //去除安卓手机错误阴影
        textShadowBlur: 10,
        textShadowColor: "transparent"
      },
      formatter: function (params, ticket, callback) {
        let showHtm = params[0].name + '\n';
        let seriesName = params[0].seriesName
        let realTime = params[0].data.realTime
        if(realTime){
          realTime = realTime.slice(5,16)
        }else{
          realTime = '-'
        }
        let marker =params[0].marker ;
        showHtm += marker+seriesName+' : ' + realTime
        return showHtm;
        },
      confine: true //是否将 tooltip 框限制在图表的区域内
    },
    xAxis: {
      type: 'category',
      boundaryGap:true,
      data: lineData.xData,
      axisLine: {
        lineStyle: {
          color: '#999',
          width:2*ratio
        }
      },
      axisPointer: {
        type: 'line'
      },
      axisTick: {
        show: true,
        alignWithLabel: true
      },
      axisLabel: {
        formatter: function (value) {
          let time = echarts.format.formatTime('MM-dd', value)
          // 格式化时间
          return time
        },
        fontSize:28*ratio,
        color:'#333333',
        align: 'center',
        interval:tabIndex==1?1:6,
      },

    },
    yAxis: {
      type: 'time',
      min: `${BASE_TIME_MIN} 04:00:00`,
      max: `${BASE_TIME_MIN} 13:00:00`,
      minInterval: 3600 * 1000 * 3,
      axisLabel: {
        align: 'right',
        fontSize:28*ratio,
        color:'#333333',
        formatter: function (value) {
          let time = echarts.format.formatTime('hh:mm', value)
          // 格式化时间
          return time
        }
      },
      splitLine: {
        show: false
      },
    
    },
    grid: {
      left:90*ratio,
      width:517*ratio,
      height:275*ratio,
      top:40*ratio
    },
    series: [{
      name: '早打卡时间',
      type: 'line',
      smooth: true,
      showAllSymbol: true,
      connectNulls: true,
      symbol: 'circle',
      symbolSize: tabIndex==1?10:5,
      lineStyle:{
        color:'#bdc4ff',
        width:tabIndex==1?3:1,
      },
      itemStyle:{
        color :'#654FF5'
      },
      data: lineData.yData,
    }, 
    {
      name: '早打卡时间',
      type: 'bar',
      barGap: '-100%',
      barWidth: tabIndex==1?10:5,
      symbol: 'rect',
      itemStyle: {
        borderRadius:[5,5,0,0],
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: 'rgba(219,191,247,0.8)' },
          { offset: 0.2, color: 'rgba(219,191,247,0.5)' },
          { offset: 1, color: 'rgba(219,191,247,0)' }
        ])
      },
  
      data: lineData.yData
    },
    
  ]
  };

  return option;
}
module.exports = {
  sleepLineOption,
  riseLineOption,
  sleepBarOption
}

绘图JS

const wxml = (cardInfo) => {
//计算天数显示长度

  let html = `
<view class="posterBg">
<image class="maskImage" src="` + cardInfo.maskImage + `" mode="aspectFit" />
          <view class="flexRow">
            <image class="avatar" src="` + cardInfo.avatar + `" mode="aspectFit" />
            <view class="flexColumn">
              <text class="nickname">` + cardInfo.nickname + `</text>
              <text class="title">` + cardInfo.title + `</text>
            </view>
          </view>
          <view class="chartCard">`
  if (cardInfo.title == '睡眠时长报告') {
    html += `<view class="avatarBox">
              <text class="avatarText">近` + cardInfo.tabName + `天平均睡眠时长` + cardInfo.averageDuration + `小时</text>
           </view>`
  } else {
    html += `<view class="nullBox">
              <text class="nullBoxText">健康生活,早睡早起</text>
           </view>`
  }

  html += `<image class="chartImage" src="` + cardInfo.tempFilePath + `" mode="aspectFit" />
          </view>
          <view class="infoCard flexRow">
            <view class="flexColumnCen">
              <text class="totalTitle">
                累计晚打卡
              </text>
              <view class="totalNumberBox">
                <text class="wakeTotalNumber">` + cardInfo.wake_total_days + `</text>
                <text class="totalUnit">天</text>
              </view>
            </view>
            <view class="flexColumnCen ml36">
              <text class="totalTitle">
                累计早打卡
              </text>
              <view class="totalNumberBox">
                <text class="sleepTotalNumber">` + cardInfo.sleep_total_days + `</text>
                <text class="totalUnit">天</text>
              </view>
            </view>
            <view class="flexRow">
            <image class="qrcodeImage" src="` + cardInfo.qrImg + `" mode="aspectFit" />
            <text class="qrcodeText">扫码说早安</text>
            <view>
          </view>
      </view>
`
  return html;
}

const style = (wakeFontWight,sleepFontWight) => {
  return {
    posterBg: {
      width: 630,
      height: 800,
      borderRadius: 32,
      flexDirection: 'column',
      backgroundColor: '#6145F0',
      position: 'relative'
    },
    flexRow: {
      flexDirection: 'row',
      alignItems: 'center'
    },
    flexColumn: {
      flexDirection: 'column',
      justifyContent: 'center'
    },
    flexColumnCen:{
      flexDirection: 'column',
      justifyContent: 'center',
      alignItems: 'center',
    },

    avatar: {
      width: 85,
      height: 85,
      borderRadius: 43,
      marginLeft: 24,
      marginTop: 40
    },
    nickname: {
      width: 353,
      height: 45,
      fontSize: 32,
      lineHeight: 45,
      textAlign: 'left',
      color: '#FFFFFF',
      fontWeight: 'bold',
      marginLeft: 16,
      marginTop: 40
    },
    title: {
      width: 353,
      height: 40,
      fontSize: 28,
      lineHeight: 40,
      textAlign: 'left',
      color: '#FFFFFF',
      marginLeft: 16,
      marginTop: 1
    },
    maskImage: {
      width: 254,
      height: 149,
      position: 'absolute',
      top: 0,
      right: 0
    },
    chartCard: {
      marginTop: 24,
      marginLeft: 24,
      marginRight: 24,
      paddingTop: 16,
      paddingBottom: 16,
      borderRadius: 16,
      backgroundColor: '#F9F9FA',
    },
    nullBox: {
      padding: 16,
      marginLeft: 24,
      borderRadius: 16,
      backgroundColor: 'rgba(240, 229, 255, 0.40)',
      width: 302,
    },
    nullBoxText: {
      width: 260,
      height: 40,
      fontSize: 28,
      lineHeight: 40,
      textAlign: 'center',
      color: '#333333',
    },
    avatarBox: {
      padding: 16,
      marginLeft: 24,
      borderRadius: 16,
      backgroundColor: 'rgba(240, 229, 255, 0.40)',
      width: 402,
    },
    avatarText: {
      width: 370,
      height: 40,
      fontSize: 28,
      lineHeight: 40,
      textAlign: 'center',
      color: '#333333',
    },
    chartImage: {
      width: 534,
      height: 336,
      marginLeft: 24,
    },
    infoCard: {
      marginTop: 32,
      marginLeft: 24,
      marginRight: 24,
      paddingTop: 24,
      paddingLeft: 24,
      paddingRight: 24,
      paddingBottom: 18,
      borderRadius: 16,
      backgroundColor: '#F9F9FA'
    },
    totalTitle: {
      width: 140,
      height: 36,
      fontSize: 26,
      lineHeight: 36,
      color: '#333333',
      marginLeft: 24,

    },
    totalNumberBox: {
      marginTop: 8,
      flexDirection: 'row',
      alignItems: 'center',
      justifyContent: 'center',
      marginLeft: 24,
    },
    wakeTotalNumber: {
      width: 26 * wakeFontWight,
      height: 56,
      fontSize: 40,
      lineHeight: 56,
      textAlign: 'right',
      color: '#333333',
      fontWeight: 'bold',
    },
    sleepTotalNumber: {
      width: 26 * sleepFontWight,
      height: 56,
      fontSize: 40,
      lineHeight: 56,
      textAlign: 'right',
      color: '#333333',
      fontWeight: 'bold',
    },
    totalUnit: {
      width: 24,
      height: 31,
      fontSize: 22,
      lineHeight: 31,
      textAlign: 'left',
      color: '#333333',
      marginLeft: 8,
      marginTop: 4,
    },
    ml36: {
      marginLeft: 36
    },
    qrcodeImage: {
      marginLeft: 67,
      width: 84,
      height: 84
    },
    qrcodeText: {
      width: 22,
      height: 100,
      fontSize: 20,
      lineHeight: 20,
      textAlign: 'left',
      color: '#333333',
      marginLeft:4,
    }
  }
}

module.exports = {
  wxml,
  style
}

代码世界的构建师,现实生活的悠游者。