鸿蒙5健康数据仪表盘开发指南 原创

进修的泡芙
发布于 2025-6-20 13:44
浏览
0收藏

鸿蒙5健康数据仪表盘开发指南

一、项目概述

本文基于HarmonyOS 5的Canvas绘制能力和分布式技术,开发一款健康数据仪表盘应用,借鉴《鸿蒙跨端U同步》中游戏多设备同步的技术原理,实现健康数据的可视化展示和跨设备同步。该应用能够通过Canvas绘制动态图表,并利用分布式能力实现多设备数据共享。

二、系统架构

±--------------------+ ±--------------------+ ±--------------------+
健康数据原子服务 <-----> 设备能力共享服务 <-----> 分布式数据管理
(Atomic Service) (CapabilityService) (DistributedData)
±---------±---------+ ±---------±---------+ ±---------±---------+

±---------v----------+ ±---------v----------+ ±---------v----------+
图表渲染引擎 数据同步服务 健康数据采集
(ChartEngine) (SyncService) (HealthCollector)

±--------------------+ ±--------------------+ ±--------------------+

三、核心代码实现
健康数据模型

// src/main/ets/model/HealthDataModel.ts
export class HealthData {
userId: string;
deviceId: string;
timestamp: number;
heartRate: number;
bloodPressure: {
systolic: number;
diastolic: number;
};
steps: number;
calories: number;
sleepHours: number;

constructor(userId: string) {
this.userId = userId;
this.deviceId = ‘’;
this.timestamp = Date.now();
this.heartRate = 0;
this.bloodPressure = { systolic: 0, diastolic: 0 };
this.steps = 0;
this.calories = 0;
this.sleepHours = 0;
static generateMockData(userId: string): HealthData {

const data = new HealthData(userId);
data.heartRate = 60 + Math.floor(Math.random() * 40);
data.bloodPressure = {
  systolic: 110 + Math.floor(Math.random() * 30),
  diastolic: 70 + Math.floor(Math.random() * 20)
};
data.steps = Math.floor(Math.random() * 10000);
data.calories = 1500 + Math.floor(Math.random() * 1000);
data.sleepHours = 6 + Math.random() * 2;
return data;

toJson(): string {

return JSON.stringify({
  userId: this.userId,
  deviceId: this.deviceId,
  timestamp: this.timestamp,
  heartRate: this.heartRate,
  bloodPressure: this.bloodPressure,
  steps: this.steps,
  calories: this.calories,
  sleepHours: this.sleepHours
});

static fromJson(jsonStr: string): HealthData {

const json = JSON.parse(jsonStr);
const data = new HealthData(json.userId);
data.deviceId = json.deviceId;
data.timestamp = json.timestamp;
data.heartRate = json.heartRate;
data.bloodPressure = json.bloodPressure;
data.steps = json.steps;
data.calories = json.calories;
data.sleepHours = json.sleepHours;
return data;

}

图表渲染引擎

// src/main/ets/engine/ChartEngine.ts
import { HealthData } from ‘…/model/HealthDataModel’;

export class ChartEngine {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private width: number;
private height: number;
private dpr: number = 1;

constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext(‘2d’) as CanvasRenderingContext2D;
this.initCanvas();
private initCanvas(): void {

this.width = this.canvas.width;
this.height = this.canvas.height;
this.dpr = window.devicePixelRatio || 1;

// 设置Canvas实际尺寸
this.canvas.width = this.width * this.dpr;
this.canvas.height = this.height * this.dpr;
this.canvas.style.width = ${this.width}px;
this.canvas.style.height = ${this.height}px;
this.ctx.scale(this.dpr, this.dpr);

public drawDashboard(data: HealthData): void {

this.clearCanvas();
this.drawBackground();
this.drawHeartRateChart(data.heartRate);
this.drawBloodPressureChart(data.bloodPressure);
this.drawStepsChart(data.steps);
this.drawSleepChart(data.sleepHours);
this.drawCaloriesChart(data.calories);

private clearCanvas(): void {

this.ctx.clearRect(0, 0, this.width, this.height);

private drawBackground(): void {

// 绘制渐变背景
const gradient = this.ctx.createLinearGradient(0, 0, this.width, this.height);
gradient.addColorStop(0, '#1a2a6c');
gradient.addColorStop(1, '#b21f1f');

this.ctx.fillStyle = gradient;
this.ctx.fillRect(0, 0, this.width, this.height);

// 绘制网格线
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
this.ctx.lineWidth = 1;

const gridSize = 20;
for (let x = 0; x < this.width; x += gridSize) {
  this.ctx.beginPath();
  this.ctx.moveTo(x, 0);
  this.ctx.lineTo(x, this.height);
  this.ctx.stroke();

for (let y = 0; y < this.height; y += gridSize) {

  this.ctx.beginPath();
  this.ctx.moveTo(0, y);
  this.ctx.lineTo(this.width, y);
  this.ctx.stroke();

}

private drawHeartRateChart(rate: number): void {
const centerX = this.width * 0.25;
const centerY = this.height * 0.3;
const radius = 50;

// 绘制心跳波形
this.ctx.save();
this.ctx.translate(centerX, centerY);

// 绘制心电图
this.ctx.beginPath();
this.ctx.strokeStyle = '#FF5252';
this.ctx.lineWidth = 3;

const points = 20;
const amplitude = 30 * (rate / 100);

for (let i = 0; i < points; i++) {
  const x = (i - points / 2) * 10;
  let y = 0;
  
  // 模拟心电图波形
  if (i > 2 && i < 5) {

= -amplitude * (i - 2) / 2;

else if (i === 5) {

= -amplitude * 1.5;

else if (i > 5 && i < 8) {

= -amplitude * (8 - i) / 2;

else if (i > 10 && i < 13) {

= amplitude 0.7 (i - 10) / 2;

else if (i === 13) {

= amplitude * 0.8;

else if (i > 13 && i < 16) {

= amplitude 0.7 (16 - i) / 2;

if (i === 0) {

    this.ctx.moveTo(x, y);

else {

    this.ctx.lineTo(x, y);

}

this.ctx.stroke();
this.ctx.restore();

// 绘制文本
this.ctx.fillStyle = 'white';
this.ctx.font = 'bold 16px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('心率', centerX, centerY - 40);

this.ctx.font = '20px sans-serif';
this.ctx.fillText(${rate} BPM, centerX, centerY + 60);

private drawBloodPressureChart(bp: { systolic: number; diastolic: number }): void {

const centerX = this.width * 0.75;
const centerY = this.height * 0.3;
const barWidth = 20;
const maxPressure = 180;

// 计算柱状图高度
const systolicHeight = (bp.systolic / maxPressure) * 80;
const diastolicHeight = (bp.diastolic / maxPressure) * 80;

// 绘制收缩压柱
this.ctx.fillStyle = '#FF9800';
this.ctx.fillRect(centerX - barWidth - 5, centerY - systolicHeight, barWidth, systolicHeight);

// 绘制舒张压柱
this.ctx.fillStyle = '#FFC107';
this.ctx.fillRect(centerX + 5, centerY - diastolicHeight, barWidth, diastolicHeight);

// 绘制文本
this.ctx.fillStyle = 'white';
this.ctx.font = 'bold 16px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('血压', centerX, centerY - 50);

this.ctx.font = '14px sans-serif';
this.ctx.fillText(收缩压: ${bp.systolic}, centerX - barWidth, centerY + 30);
this.ctx.fillText(舒张压: ${bp.diastolic}, centerX + barWidth, centerY + 30);

private drawStepsChart(steps: number): void {

const centerX = this.width * 0.25;
const centerY = this.height * 0.7;
const radius = 50;
const maxSteps = 10000;
const percentage = Math.min(steps / maxSteps, 1);

// 绘制背景圆
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
this.ctx.lineWidth = 10;
this.ctx.stroke();

// 绘制进度圆
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + Math.PI  2  percentage);
this.ctx.strokeStyle = '#4CAF50';
this.ctx.lineWidth = 10;
this.ctx.stroke();

// 绘制文本
this.ctx.fillStyle = 'white';
this.ctx.font = 'bold 16px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('步数', centerX, centerY - 30);

this.ctx.font = '20px sans-serif';
this.ctx.fillText(steps.toString(), centerX, centerY + 10);

private drawSleepChart(hours: number): void {

const centerX = this.width * 0.75;
const centerY = this.height * 0.7;
const radius = 50;
const maxHours = 10;
const percentage = Math.min(hours / maxHours, 1);

// 绘制背景圆
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
this.ctx.lineWidth = 10;
this.ctx.stroke();

// 绘制进度圆
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + Math.PI  2  percentage);
this.ctx.strokeStyle = '#2196F3';
this.ctx.lineWidth = 10;
this.ctx.stroke();

// 绘制月亮图标
this.ctx.fillStyle = '#FFEB3B';
this.ctx.beginPath();
this.ctx.arc(centerX - 15, centerY - 10, 8, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.arc(centerX - 10, centerY - 10, 8, 0, Math.PI * 2);
this.ctx.fillStyle = '#1a2a6c';
this.ctx.fill();

// 绘制文本
this.ctx.fillStyle = 'white';
this.ctx.font = 'bold 16px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('睡眠', centerX, centerY - 40);

this.ctx.font = '20px sans-serif';
this.ctx.fillText(${hours.toFixed(1)} 小时, centerX, centerY + 20);

private drawCaloriesChart(calories: number): void {

const centerX = this.width * 0.5;
const centerY = this.height * 0.8;
const width = 150;
const height = 20;
const maxCalories = 2500;
const percentage = Math.min(calories / maxCalories, 1);

// 绘制背景条
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
this.ctx.fillRect(centerX - width / 2, centerY - height / 2, width, height);

// 绘制进度条
this.ctx.fillStyle = '#FF5722';
this.ctx.fillRect(centerX - width / 2, centerY - height / 2, width * percentage, height);

// 绘制火焰图标
this.drawFlameIcon(centerX, centerY - 30);

// 绘制文本
this.ctx.fillStyle = 'white';
this.ctx.font = 'bold 16px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('卡路里', centerX, centerY - 60);

this.ctx.font = '20px sans-serif';
this.ctx.fillText({calories} / {maxCalories}, centerX, centerY + 40);

private drawFlameIcon(x: number, y: number): void {

this.ctx.save();
this.ctx.translate(x, y);

// 绘制火焰形状
this.ctx.beginPath();
this.ctx.moveTo(0, -15);
this.ctx.bezierCurveTo(5, -20, 10, -15, 5, -5);
this.ctx.bezierCurveTo(15, -10, 10, 5, 0, 10);
this.ctx.bezierCurveTo(-10, 5, -15, -10, -5, -5);
this.ctx.bezierCurveTo(-10, -15, -5, -20, 0, -15);
this.ctx.closePath();

// 添加渐变
const gradient = this.ctx.createRadialGradient(0, 0, 5, 0, 0, 15);
gradient.addColorStop(0, '#FFEB3B');
gradient.addColorStop(1, '#FF5722');
this.ctx.fillStyle = gradient;
this.ctx.fill();

this.ctx.restore();

}

分布式同步服务

// src/main/ets/service/DistributedSyncService.ts
import { distributedData } from ‘@ohos.data.distributedData’;
import { BusinessError } from ‘@ohos.base’;
import { HealthData } from ‘…/model/HealthDataModel’;
import { deviceManager } from ‘@ohos.distributedDeviceManager’;

export class DistributedSyncService {
private static instance: DistributedSyncService;
private kvManager: distributedData.KVManager;
private kvStore: distributedData.KVStore;
private readonly STORE_ID = ‘health_data_store’;
private readonly SYNC_KEY_PREFIX = ‘health_’;
private subscribers: ((data: HealthData) => void)[] = [];

private constructor() {
this.initDistributedData();
public static getInstance(): DistributedSyncService {

if (!DistributedSyncService.instance) {
  DistributedSyncService.instance = new DistributedSyncService();

return DistributedSyncService.instance;

private initDistributedData(): void {

const config: distributedData.KVManagerConfig = {
  bundleName: 'com.example.healthdashboard',
  userInfo: {
    userId: '0',
    userType: distributedData.UserType.SAME_USER_ID

};

try {
  distributedData.createKVManager(config, (err: BusinessError, manager: distributedData.KVManager) => {
    if (err) {
      console.error(Failed to create KVManager. Code: {err.code}, message: {err.message});
      return;

this.kvManager = manager;

    const options: distributedData.Options = {
      createIfMissing: true,
      encrypt: true,
      backup: false,
      autoSync: true,
      kvStoreType: distributedData.KVStoreType.SINGLE_VERSION,
      schema: '',
      securityLevel: distributedData.SecurityLevel.S2
    };

    this.kvManager.getKVStore(this.STORE_ID, options, (err: BusinessError, store: distributedData.KVStore) => {
      if (err) {
        console.error(Failed to get KVStore. Code: {err.code}, message: {err.message});
        return;

this.kvStore = store;

      this.registerDataListener();
    });
  });

catch (e) {

  console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});

}

private registerDataListener(): void {
try {
this.kvStore.on(‘dataChange’, distributedData.SubscribeType.SUBSCRIBE_TYPE_ALL, (data: distributedData.ChangeData) => {
if (data.key.startsWith(this.SYNC_KEY_PREFIX)) {
const healthData = HealthData.fromJson(data.value.value as string);
this.notifySubscribers(healthData);
});

catch (e) {

  console.error(Failed to register data listener. Code: {e.code}, message: {e.message});

}

public subscribe(callback: (data: HealthData) => void): void {
this.subscribers.push(callback);
public unsubscribe(callback: (data: HealthData) => void): void {

this.subscribers = this.subscribers.filter(sub => sub !== callback);

private notifySubscribers(data: HealthData): void {

this.subscribers.forEach(callback => callback(data));

public syncHealthData(data: HealthData): void {

if (!this.kvStore) {
  console.error('KVStore is not initialized');
  return;

deviceManager.getLocalDeviceInfo((err: BusinessError, info) => {

  if (!err) {
    data.deviceId = info.deviceId;
    
    try {
      const key = this.SYNC_KEY_PREFIX + data.userId + '_' + data.timestamp;
      this.kvStore.put(key, data.toJson(), (err: BusinessError) => {
        if (err) {
          console.error(Failed to put data. Code: {err.code}, message: {err.message});

});

catch (e) {

      console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});

}

});

public async getHealthHistory(userId: string): Promise<HealthData[]> {

return new Promise((resolve) => {
  if (!this.kvStore) {
    resolve([]);
    return;

try {

    const query: distributedData.Query = {
      prefixKey: this.SYNC_KEY_PREFIX + userId
    };

    this.kvStore.getEntries(query, (err: BusinessError, entries: distributedData.Entry[]) => {
      if (err) {
        console.error(Failed to get entries. Code: {err.code}, message: {err.message});
        resolve([]);
        return;

const history = entries

        .map(entry => HealthData.fromJson(entry.value.value as string))
        .sort((a, b) => b.timestamp - a.timestamp);
      
      resolve(history);
    });

catch (e) {

    console.error(An unexpected error occurred. Code: {e.code}, message: {e.message});
    resolve([]);

});

}

主界面实现

// src/main/ets/pages/Index.ets
import { HealthData } from ‘…/model/HealthDataModel’;
import { DistributedSyncService } from ‘…/service/DistributedSyncService’;
import { ChartEngine } from ‘…/engine/ChartEngine’;

@Entry
@Component
struct Index {
@State currentData: HealthData = new HealthData(‘user_123’);
@State historyData: HealthData[] = [];
@State selectedTab: number = 0;
@State canvasRef: CanvasRenderingContext2D | null = null;
private chartEngine: ChartEngine | null = null;
private syncService = DistributedSyncService.getInstance();

aboutToAppear(): void {
this.syncService.subscribe(this.handleDataUpdate.bind(this));
this.loadMockData();
this.loadHistoryData();
aboutToDisappear(): void {

this.syncService.unsubscribe(this.handleDataUpdate.bind(this));

private handleDataUpdate(data: HealthData): void {

if (data.userId === this.currentData.userId) {
  this.currentData = data;
  this.updateChart();

}

private loadMockData(): void {
this.currentData = HealthData.generateMockData(‘user_123’);
this.syncService.syncHealthData(this.currentData);
private async loadHistoryData(): Promise<void> {

this.historyData = await this.syncService.getHealthHistory('user_123');

private onCanvasReady(event: { context: CanvasRenderingContext2D }): void {

this.canvasRef = event.context;
const canvas = document.getElementById('healthCanvas') as HTMLCanvasElement;
this.chartEngine = new ChartEngine(canvas);
this.updateChart();

private updateChart(): void {

if (this.chartEngine) {
  this.chartEngine.drawDashboard(this.currentData);

}

private refreshData(): void {
this.loadMockData();
this.updateChart();
private showHistoryData(data: HealthData): void {

this.currentData = data;
this.updateChart();
this.selectedTab = 0;

build() {

Column() {
  // 标题和刷新按钮
  Row() {
    Text('健康数据仪表盘')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
    
    Button('刷新数据')
      .margin({left: 20})
      .onClick(() => this.refreshData())

.width(‘100%’)

  .justifyContent(FlexAlign.Center)
  .margin({bottom: 20})

  // 选项卡
  Tabs({ barPosition: BarPosition.Start, index: this.selectedTab }) {
    TabContent() {
      // 主仪表盘
      Column() {
        // Canvas图表
        Canvas(this.canvasRef)
          .id('healthCanvas')
          .width('100%')
          .height('60%')
          .onReady((event) => this.onCanvasReady(event))
          .backgroundColor('#1a2a6c')
        
        // 数据摘要
        Row() {
          Column() {
            Text('今日步数')
              .fontSize(16)
            Text(${this.currentData.steps})
              .fontSize(20)
              .fontColor('#4CAF50')

.margin({right: 15})

          Column() {
            Text('心率')
              .fontSize(16)
            Text(${this.currentData.heartRate} BPM)
              .fontSize(20)
              .fontColor('#FF5252')

.margin({right: 15})

          Column() {
            Text('血压')
              .fontSize(16)
            Text({this.currentData.bloodPressure.systolic}/{this.currentData.bloodPressure.diastolic})
              .fontSize(20)
              .fontColor('#FF9800')

.margin({right: 15})

          Column() {
            Text('睡眠')
              .fontSize(16)
            Text(${this.currentData.sleepHours.toFixed(1)} 小时)
              .fontSize(20)
              .fontColor('#2196F3')

}

        .width('100%')
        .justifyContent(FlexAlign.SpaceAround)
        .margin({top: 20})

}

    .tabBar('仪表盘')
    
    TabContent() {
      // 历史数据列表
      List({ space: 10 }) {
        ForEach(this.historyData, (item: HealthData) => {
          ListItem() {
            Column() {
              Row() {
                Text(new Date(item.timestamp).toLocaleString())
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                
                Text(${item.steps} 步)
                  .fontSize(14)
                  .margin({left: 10})

Row() {

                Text(心率: ${item.heartRate} BPM)
                  .fontSize(12)
                  .margin({right: 10})
                
                Text(血压: {item.bloodPressure.systolic}/{item.bloodPressure.diastolic})
                  .fontSize(12)

.margin({top: 5})

.width(‘100%’)

            .padding(10)
            .borderRadius(10)
            .backgroundColor('#f5f5f5')

.onClick(() => this.showHistoryData(item))

        })

}

    .tabBar('历史记录')

.width(‘100%’)

  .height('100%')

.width(‘100%’)

.height('100%')
.padding(10)

}

四、与游戏同步技术的结合点
实时数据同步:类似游戏中的玩家状态同步,实现健康数据的多设备实时同步

设备能力共享:借鉴游戏多设备共享技术,实现健康数据的跨设备访问

分布式数据管理:使用与游戏相同的分布式数据管理机制存储健康历史数据

状态更新通知:采用类似游戏事件通知机制,实现数据变化的实时响应

安全通信:利用游戏中的安全通信机制保障健康数据的隐私和安全

五、关键特性实现
Canvas动态渲染:

  // 高性能动画渲染

private renderAnimationFrame(): void {
if (!this.isAnimating) return;

 this.updateChart();
 requestAnimationFrame(() => this.renderAnimationFrame());

数据压缩传输:

  // 压缩健康数据

private compressHealthData(data: HealthData): Uint8Array {
const buffer = new ArrayBuffer(32);
const view = new DataView(buffer);
view.setUint32(0, data.timestamp / 1000);
view.setUint16(4, data.heartRate);
view.setUint16(6, data.bloodPressure.systolic);
view.setUint16(8, data.bloodPressure.diastolic);
view.setUint32(10, data.steps);
view.setUint32(14, data.calories);
view.setUint8(18, Math.round(data.sleepHours * 10));
return new Uint8Array(buffer);

自适应布局:

  // 根据设备屏幕尺寸调整布局

private adjustLayout(): void {
const screenWidth = window.screen.width;
if (screenWidth < 600) {
this.chartScale = 0.8;
else if (screenWidth < 1024) {

   this.chartScale = 1.0;

else {

   this.chartScale = 1.2;

}

低功耗模式:

  // 后台运行时降低更新频率

private handleAppStateChange(state: string): void {
if (state === ‘background’) {
this.updateInterval = 30000; // 30秒
else {

   this.updateInterval = 5000;  // 5秒

}

六、性能优化策略
Canvas渲染优化:

  // 使用离屏Canvas预渲染静态元素

private createOffscreenCanvas(): void {
this.offscreenCanvas = document.createElement(‘canvas’);
this.offscreenCanvas.width = this.width;
this.offscreenCanvas.height = this.height;
this.renderStaticElements();

数据批量处理:

  // 批量处理高频数据更新

private addToBatch(data: HealthData): void {
this.batchData.push(data);
if (!this.batchTimer) {
this.batchTimer = setTimeout(() => {
this.processBatch();
this.batchTimer = null;
}, 200);
}

差异化设备处理:

  // 根据设备类型调整功能

if (device.deviceType === ‘watch’) {
this.enableSimplifiedMode();
else {

 this.enableFullFeatures();

智能缓存策略:

  // 缓存历史数据

private cacheHistoryData(data: HealthData[]): void {
localStorage.setItem(‘health_history’, JSON.stringify(data));

七、项目扩展方向
健康趋势分析:增加健康数据趋势图表和预测功能

智能提醒:基于数据分析提供个性化健康建议

社交分享:将健康成就分享到社交平台

医疗对接:与医疗机构系统对接提供专业建议

运动计划:制定个性化运动计划并同步到设备

八、总结

本健康数据仪表盘实现了以下核心功能:
健康数据的可视化展示(心率、血压、步数、睡眠、卡路里)

Canvas动态图表渲染

历史数据存储和查询

多设备间的数据同步

响应式UI设计

通过借鉴游戏中的同步技术,我们构建了一个直观、高效的健康数据可视化服务。该项目展示了HarmonyOS分布式能力和Canvas技术的强大组合,为开发者提供了实现数据可视化应用的参考方案。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐