在这里插入图片描述

一、功能概述

喝水习惯的形成往往需要观察一周内的变化趋势。"周趋势分析"模块让用户能够直观看到过去 7 天的喝水量变化,从而发现自己的规律、调整计划。本篇文章围绕"周趋势分析"展开,介绍如何在 Cordova Web 层 通过 Canvas 或简单的 SVG 绘制折线图,并通过 OpenHarmony ArkTS 插件 提供原生图表渲染能力。

我们继续采用"一段代码一段说明"的结构,通过 HTML/JavaScript 和 ArkTS 示例,构建一条完整的数据可视化动线。

二、Web 端周趋势分析界面

<div id="weekly-trend-page" class="page page-weekly-trend">
  <h1>周趋势分析</h1>

  <div class="trend-controls">
    <button id="btn-prev-week" class="btn-secondary">上一周</button>
    <span id="week-label" class="text-label">本周</span>
    <button id="btn-next-week" class="btn-secondary">下一周</button>
  </div>

  <canvas id="trend-canvas" width="800" height="400"></canvas>
  <div id="trend-summary" class="summary-box"></div>
</div>

这段 HTML 定义了周趋势分析页面的基本结构。顶部的导航按钮允许用户切换不同的周,trend-canvas 用于绘制折线图,trend-summary 展示该周的统计摘要(如平均喝水量、最高值等)。

.page-weekly-trend {
  padding: 16px 24px;
}

.trend-controls {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.text-label {
  flex: 1;
  text-align: center;
  font-weight: bold;
}

#trend-canvas {
  border: 1px solid #555;
  margin-bottom: 16px;
  background: #1f2937;
}

.summary-box {
  background: #374151;
  padding: 12px;
  border-radius: 4px;
  font-size: 14px;
}

CSS 为页面添加布局和样式。Canvas 元素设置了固定的宽高,背景色与深色主题一致,便于绘制图表。

三、加载周数据并绘制折线图

async function loadWeeklyTrend(weekOffset = 0) {
  const today = new Date();
  const startDate = new Date(today);
  startDate.setDate(today.getDate() - today.getDay() + weekOffset * 7);
  startDate.setHours(0, 0, 0, 0);

  const endDate = new Date(startDate);
  endDate.setDate(startDate.getDate() + 6);
  endDate.setHours(23, 59, 59, 999);

  const records = await db.getRecordsByDateRange(startDate, endDate);
  const dailyData = aggregateDailyData(records, startDate);

  renderTrendChart(dailyData);
  renderTrendSummary(dailyData);
  updateWeekLabel(startDate);
}

loadWeeklyTrend 函数是周趋势分析的核心数据加载函数。它首先获取当前日期,然后根据 weekOffset 参数(周偏移量)计算目标周的起始日期。通过 setDate(today.getDate() - today.getDay() + weekOffset * 7) 这行代码,我们计算出该周的周一日期。例如,如果 weekOffset 为 0,表示当前周;为 -1 表示上一周;为 1 表示下一周。

接着,我们设置起始日期的时间为 00:00:00,确保从该天的最开始开始计算。然后计算结束日期为起始日期加 6 天,时间设为 23:59:59,这样就覆盖了整个周的所有时间。

从 IndexedDB 中查询该日期范围内的所有喝水记录后,通过 aggregateDailyData 函数将这些记录按日期分组,得到每天的总喝水量。最后调用 renderTrendChart 绘制折线图,renderTrendSummary 展示统计摘要,updateWeekLabel 更新页面上显示的周日期范围。

function aggregateDailyData(records, startDate) {
  const dailyMap = new Map();

  for (let i = 0; i < 7; i++) {
    const date = new Date(startDate);
    date.setDate(startDate.getDate() + i);
    const key = date.toISOString().split('T')[0];
    dailyMap.set(key, 0);
  }

  records.forEach((r) => {
    const key = new Date(r.timestamp).toISOString().split('T')[0];
    if (dailyMap.has(key)) {
      dailyMap.set(key, dailyMap.get(key) + r.amount);
    }
  });

  return Array.from(dailyMap.values());
}

aggregateDailyData 函数的作用是将原始的喝水记录数据按日期进行聚合。首先,我们创建一个 Map 对象 dailyMap,用于存储每天的喝水总量。接着,通过一个循环为该周的每一天(共 7 天)初始化一个 0 值。这一步很重要,因为即使某天没有喝水记录,我们也需要在图表上显示为 0,这样才能保证图表的完整性和连续性。

然后,我们遍历所有的喝水记录。对于每条记录,我们提取其时间戳中的日期部分(格式为 YYYY-MM-DD),然后检查这个日期是否在我们的 dailyMap 中。如果存在,我们就将该记录的喝水量累加到对应日期的总量上。这样,如果某天有多条记录,它们的喝水量会被累加在一起。

最后,我们使用 Array.from(dailyMap.values()) 将 Map 中的所有值转换为一个数组,这个数组就是按顺序排列的 7 天的喝水量数据,可以直接用于绘制图表。

function renderTrendChart(dailyData) {
  const canvas = document.getElementById('trend-canvas');
  if (!canvas) return;

  const ctx = canvas.getContext('2d');
  const width = canvas.width;
  const height = canvas.height;
  const padding = 40;

  // 清空画布
  ctx.fillStyle = '#1f2937';
  ctx.fillRect(0, 0, width, height);

  // 绘制坐标轴
  ctx.strokeStyle = '#555';
  ctx.beginPath();
  ctx.moveTo(padding, padding);
  ctx.lineTo(padding, height - padding);
  ctx.lineTo(width - padding, height - padding);
  ctx.stroke();

  // 绘制折线
  const maxValue = Math.max(...dailyData, 1);
  const pointSpacing = (width - 2 * padding) / (dailyData.length - 1 || 1);

  ctx.strokeStyle = '#60a5fa';
  ctx.lineWidth = 2;
  ctx.beginPath();

  dailyData.forEach((value, index) => {
    const x = padding + index * pointSpacing;
    const y = height - padding - (value / maxValue) * (height - 2 * padding);

    if (index === 0) {
      ctx.moveTo(x, y);
    } else {
      ctx.lineTo(x, y);
    }
  });

  ctx.stroke();

  // 绘制数据点
  ctx.fillStyle = '#60a5fa';
  dailyData.forEach((value, index) => {
    const x = padding + index * pointSpacing;
    const y = height - padding - (value / maxValue) * (height - 2 * padding);
    ctx.beginPath();
    ctx.arc(x, y, 4, 0, 2 * Math.PI);
    ctx.fill();
  });
}

renderTrendChart 函数使用 Canvas 2D API 绘制一条完整的折线图。首先,我们获取 Canvas 元素和其 2D 绘图上下文,设置画布的宽高和内边距(padding)。内边距用于在画布边缘留出空间,用于绘制坐标轴和标签。

接着,我们清空整个画布,填充深色背景色(#1f2937),这样可以清除之前的绘图内容。然后绘制坐标轴:从左上角(padding, padding)向下绘制一条竖线到左下角(padding, height - padding),再向右绘制一条横线到右下角(width - padding, height - padding),形成一个 L 形的坐标轴。

数据的关键处理在于归一化。我们找出数据中的最大值,然后将所有数据点的 y 坐标按照 (value / maxValue) * (height - 2 * padding) 的公式计算。这样做的好处是,无论数据的绝对值是多少,都能自动适应画布的高度,充分利用画布空间。

计算点的水平间距:pointSpacing = (width - 2 * padding) / (dailyData.length - 1),这样 7 个数据点会均匀分布在画布的宽度上。

然后我们绘制折线。对于第一个点,使用 moveTo 移动到该点;对于后续的点,使用 lineTo 连接到该点。这样就形成了一条连贯的折线。

最后,我们在每个数据点上绘制一个小圆点(半径为 4),这样用户可以清楚地看到每个数据点的位置。整个图表就完成了,用户可以直观地看到一周内的喝水量变化趋势。

function renderTrendSummary(dailyData) {
  const sum = dailyData.reduce((a, b) => a + b, 0);
  const avg = Math.round(sum / dailyData.length);
  const max = Math.max(...dailyData);
  const min = Math.min(...dailyData);

  const summaryDiv = document.getElementById('trend-summary');
  if (!summaryDiv) return;

  summaryDiv.innerHTML = `
    <div>本周总量: ${sum} ml | 平均: ${avg} ml | 最高: ${max} ml | 最低: ${min} ml</div>
  `;
}

renderTrendSummary 函数用于计算和展示该周的统计摘要信息。首先,我们使用 reduce 方法计算所有 7 天喝水量的总和。然后计算平均值,通过将总和除以天数,并使用 Math.round 四舍五入到整数。接着,我们使用 Math.maxMath.min 分别找出该周的最高喝水量和最低喝水量。

这些统计数据能够帮助用户快速了解自己这一周的喝水情况:总量反映了整周的喝水努力程度,平均值显示了日均喝水量,最高值和最低值则反映了喝水量的波动范围。最后,我们将这些数据格式化为易读的字符串,并将其插入到摘要框的 HTML 中。

四、周导航与事件绑定

let currentWeekOffset = 0;

function updateWeekLabel(startDate) {
  const endDate = new Date(startDate);
  endDate.setDate(startDate.getDate() + 6);

  const label = document.getElementById('week-label');
  if (label) {
    label.textContent = `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
  }
}

document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('btn-prev-week')?.addEventListener('click', () => {
    currentWeekOffset--;
    loadWeeklyTrend(currentWeekOffset);
  });

  document.getElementById('btn-next-week')?.addEventListener('click', () => {
    currentWeekOffset++;
    loadWeeklyTrend(currentWeekOffset);
  });

  loadWeeklyTrend(0);
});

这段代码实现了周导航的核心逻辑。我们使用全局变量 currentWeekOffset 来跟踪当前显示的是哪一周。初始值为 0,表示当前周。

updateWeekLabel 函数用于更新页面上显示的周日期范围。它接收起始日期作为参数,然后计算该周的结束日期(起始日期加 6 天),最后将起始日期和结束日期格式化为本地日期字符串,并更新页面上的标签。

DOMContentLoaded 事件中,我们为"上一周"和"下一周"按钮绑定点击事件。当用户点击"上一周"按钮时,currentWeekOffset 减 1,然后调用 loadWeeklyTrend 重新加载数据;当点击"下一周"按钮时,currentWeekOffset 加 1。这样用户就可以方便地在不同的周之间切换,查看历史数据或未来的计划。

最后,在页面加载完成时,我们调用 loadWeeklyTrend(0) 加载当前周的数据,这样用户打开页面时就能立即看到当前周的趋势分析。

五、通过 Cordova 同步周趋势数据到原生层

function syncWeeklyTrendToNative(dailyData) {
  if (!window.cordova) {
    console.warn('[WeeklyTrend] cordova not ready, skip native sync');
    return;
  }

  cordova.exec(
    () => {
      console.info('[WeeklyTrend] sync success');
    },
    (err) => {
      console.error('[WeeklyTrend] sync failed', err);
    },
    'WaterTrackerWeeklyTrend',
    'setWeeklyData',
    [{ dailyData }]
  );
}

syncWeeklyTrendToNative 函数是 Web 层和原生层通信的桥梁。首先,我们检查 window.cordova 是否存在,这是判断 Cordova 环境是否已准备好的标准方法。如果 Cordova 还没有加载,我们就打印一个警告日志并返回,避免调用不存在的方法导致错误。

如果 Cordova 已准备好,我们使用 cordova.exec 方法调用原生插件。这个方法有 5 个参数:

  1. 成功回调函数:当原生侧成功处理请求时调用
  2. 失败回调函数:当原生侧处理失败时调用
  3. 插件名称:‘WaterTrackerWeeklyTrend’
  4. 方法名称:‘setWeeklyData’
  5. 参数数组:包含要传递给原生侧的数据

在这个例子中,我们将 dailyData(7 天的喝水量数据)打包成一个对象并传递给原生侧。原生侧可以接收这些数据,用于绘制原生图表、进行数据分析或其他处理。

六、OpenHarmony ArkTS 插件与周趋势存储

// entry/src/main/ets/plugins/WaterTrackerWeeklyTrendPlugin.ets
import common from '@ohos.app.ability.common';

export interface WeeklyTrendData {
  dailyData: number[];
}

export class WeeklyTrendStore {
  private static _weeklyData: WeeklyTrendData | null = null;

  static setWeeklyData(data: WeeklyTrendData) {
    this._weeklyData = data;
  }

  static get weeklyData() {
    return this._weeklyData;
  }
}

export default class WaterTrackerWeeklyTrendPlugin {
  context: common.UIAbilityContext;

  constructor(ctx: common.UIAbilityContext) {
    this.context = ctx;
  }

  setWeeklyData(args: Array<Object>, callbackId: number) {
    const data = args[0] as WeeklyTrendData;
    WeeklyTrendStore.setWeeklyData(data);
    console.info('[WeeklyTrendPlugin] weekly data set');
  }
}

ArkTS 侧的 WaterTrackerWeeklyTrendPlugin 插件接收周趋势数据,并通过 WeeklyTrendStore 缓存。

七、ArkUI 中展示周趋势图表

// entry/src/main/ets/pages/WeeklyTrendPage.ets
import { WeeklyTrendStore } from '../plugins/WaterTrackerWeeklyTrendPlugin';

@Component
struct WeeklyTrendView {
  build() {
    const data = WeeklyTrendStore.weeklyData;

    Column() {
      Text('周趋势分析')
        .fontSize(18)
        .margin({ bottom: 8 });

      if (data && data.dailyData.length > 0) {
        Text(`本周数据: ${data.dailyData.join(', ')} ml`)
          .fontSize(14);
      } else {
        Text('暂无数据')
          .fontSize(14);
      }
    }
    .padding(16)
  }
}

WeeklyTrendView 组件在原生界面中展示周趋势数据。

八、小结

本篇文章从周数据加载、日期聚合、Canvas 折线图绘制、周导航到 Cordova 桥接和 ArkTS 插件,完整演示了"周趋势分析"在 Cordova&openharmony 混合应用中的实现路径。Web 层通过 loadWeeklyTrendaggregateDailyData 实现了周数据聚合,通过 renderTrendChart 实现了 Canvas 图表绘制;syncWeeklyTrendToNative 将数据推送给原生侧,ArkTS 侧通过 WeeklyTrendStoreWaterTrackerWeeklyTrendPlugin 缓存数据,ArkUI 组件 WeeklyTrendView 则提供原生展示入口。

通过"一段代码一段说明"的方式,我们把整个周趋势分析流程拆解得足够细致。你可以在此基础上进一步扩展,例如添加更多的图表类型(柱状图、饼图)、数据导出、趋势预测等功能,让"周趋势分析"真正成为用户了解自己喝水习惯的有力工具.

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐