深色医疗青绿主题下ArkUI声明式架构:数字健康监测平台的多维数据可视化与状态管理实践

技术前言

HarmonyOS ArkUI作为华为鸿蒙操作系统的核心UI开发框架,采用了一种全新的声明式UI开发范式。这种范式不同于传统Android的命令式UI编程,也不同于React Native的虚拟DOM diff机制,而是基于状态驱动视图刷新的核心理念。在ArkUI的世界里,开发者只需要声明"界面应该长什么样",而不需要关心"界面怎么从当前状态变到目标状态"。框架内部实现了高效的渲染管线,自动追踪状态变量的变化,并精确地更新与之关联的UI组件。这种设计理念在数字健康监测平台这种多Tab、多图表、多状态交互的复杂应用中,展现出了极大的工程价值。

数字健康监测平台是一个对数据可视化要求极高的应用场景。用户需要在有限的屏幕空间内查看心率趋势、睡眠分析、体型评估、健康报告等多维度数据。每一个数据维度都有其独特的可视化方式:心率数据适合用折线图展示24小时变化趋势,睡眠数据适合用饼图展示各阶段占比,体型评估适合用雷达图展示多维指标。这些不同的可视化需求要求底层架构具有足够的灵活性和扩展性。ArkUI的Canvas组件配合CanvasRenderingContext2D提供了底层的2D绑图能力,让开发者可以精确控制每一个像素的渲染效果。

在ArkUI的组件化开发体系中,@Entry和@Component是最核心的两个装饰器。@Entry标识一个组件为页面入口组件,它会被框架自动注册到路由系统中。@Component则标识一个自定义组件,使其可以被其他组件引用和复用。在康脉健康监测平台中,主页面Page802被@Entry装饰为入口组件,而所有的数据模型类则通过@Observed装饰器实现了可观察对象模式。@Observed是ArkUI提供的类装饰器,被装饰的类实例在作为@State状态变量的值时,其属性变化能够被框架自动感知,从而触发UI刷新。这种机制在健康监测场景中至关重要------当用户的心率数据、睡眠记录、健康报告发生变化时,界面需要实时更新。

@State状态变量是ArkUI状态管理的基础。每一个@State装饰的变量都建立了一个"状态-视图"绑定关系。当变量值改变时,所有引用了该变量的UI组件都会被重新渲染。在健康监测平台中,currentTab状态变量控制着六个Tab页面的切换显示,breath状态变量驱动着呼吸动画效果,sleepList/metricList/reportList/achieveList四个状态变量分别承载着睡眠记录、体征指标、健康报告、成就徽章的数据列表。这种细粒度的状态管理让数据更新非常精准------只有真正发生变化的UI片段才会被重新渲染,避免了全局重绘的性能浪费。

ArkUI的@Builder装饰器提供了UI片段的复用机制。在健康监测平台中,六个Tab页面被封装为六个@Builder方法:tabOverview、tabHeart、tabSleep、tabBody、tabReport、tabMine。每个@Builder方法内部通过Column、Row、Stack等容器组件的组合,构建出布局结构完全不同的页面。这种设计让复杂的页面结构被拆分为可管理的独立单元,每个单元负责自己的布局逻辑和数据展示,互不干扰。同时,@Builder方法支持参数传递,例如modalOverlay方法接受一个onClose回调函数,实现了弹窗关闭逻辑的灵活定制。

生命周期管理是移动端应用开发不可回避的话题。ArkUI提供了aboutToAppear和aboutToDisappear两个生命周期回调。在健康监测平台中,aboutToAppear中创建了一个每秒触发的定时器,用于驱动呼吸动画效果------breath状态变量每秒在true和false之间切换,所有引用了breath的UI组件会因此产生闪烁/呼吸的视觉效果。aboutToDisappear中则清理了这个定时器,防止组件销毁后的内存泄漏。这种定时器的创建-清理模式是ArkUI中资源管理的标准实践。

Canvas绘图在健康监测平台中占据了非常重要的位置。四个不同类型的图表------进度环、折线图、饼图、雷达图------分别服务于不同的数据展示需求。每个Canvas组件都需要一个CanvasRenderingContext2D实例作为绘图上下文,通过RenderingContextSettings可以配置抗锯齿等渲染参数。绘图逻辑被封装在drawRing、drawLine、drawPie、drawRadar四个方法中,每个方法通过Canvas 2D API的beginPath、arc、moveTo、lineTo、stroke、fill等指令完成图形绘制。值得注意的是,HarmonyOS 6.1.1引入了运行时动态开关Canvas抗锯齿的新特性,通过ctx.antialias属性可以在应用运行时实时切换渲染模式,这在医疗级数据展示场景中具有实际意义------医生可能需要像素级锐利的图表来精确判读数据。

颜色系统在深色主题的健康应用中尤为关键。医疗青(#2DD4BF)和健康绿(#4ADE80)构成了平台的主色调,配合深色背景(#061824)和卡片底色(#0E2A3A),营造出专业、沉稳的医疗视觉氛围。辅助色系包括橙色(警示)、红色(危险)、紫色(睡眠)、蓝色(心率偏低)等,每种颜色都有明确的语义映射。颜色系统通过ColorPalette接口进行类型约束,COLORS常量进行实际实例化,确保颜色值在整个应用中的一致性。

数据模型层的@Observed类设计体现了面向对象的封装思想。SleepRecord封装了睡眠时段记录的stage、start、end、duration四个属性;MetricItem封装了体征指标的name、value、unit、status;ReportItem封装了健康报告的date、title、status、summary;HealthAchieve封装了成就徽章的icon、name、desc、got。每个类都有明确的构造函数和预置数据数组,这些预置数据既用于演示目的,也体现了数据结构的规范。在实际应用中,这些预置数据会被网络请求返回的真实数据替换。

辅助函数层提供了业务逻辑与视觉呈现之间的映射桥梁。hrColor函数将心率数值映射为语义颜色------低于60bpm显示蓝色(偏低),60-100显示绿色(正常),100-120显示橙色(偏高),超过120显示红色(危险)。sleepColor函数将睡眠阶段映射为颜色------深睡紫色、浅睡蓝色、REM绿色、清醒橙色。reportColor函数将报告状态映射为颜色------正常绿色、关注橙色、异常红色。这种"数据到颜色"的映射函数在健康应用中无处不在,它将抽象的数值转化为直观的视觉信号,让用户能够快速理解数据背后的健康含义。

弹窗系统通过Stack叠层布局实现。modalOverlay提供了半透明遮罩层,panelAdd/panelEdit/panelDel三个弹窗分别用于报告详情查看、健康目标设置、记录删除确认。每个弹窗都接受一个onClose回调函数,通过点击遮罩层或按钮触发关闭。这种回调函数模式比传统的状态变量控制更加灵活,也更容易复用。弹窗内的布局结构根据功能需求进行了差异化设计------panelAdd展示报告详情信息,panelEdit提供目标选择按钮组,panelDel提供删除确认的双按钮布局。

底部Tab栏采用单排6个Tab的布局方式。每个Tab由图标和文字标签组成,通过currentTab状态变量控制选中态。选中Tab的图标放大、文字变粗、颜色变为青色(tabOn),未选中Tab则保持默认样式。这种简单的视觉反馈机制让用户能够清晰地知道当前所处的页面位置。六个Tab分别对应概览、心率、睡眠、体型、报告、我的六个功能模块,每个模块都有独立的布局结构和数据展示逻辑。

在整体架构上,健康监测平台采用了"头部固定+中间滚动+底部导航"的经典三段式布局。头部展示了位置信息、核心健康指标(心率/步数/卡路里)、搜索栏、快捷入口和健康提醒条。中间的Scroll区域根据currentTab的值条件渲染对应的Tab页面内容,并统一附加了月度步数柱状图卡片。底部的Tab栏提供了全局导航能力。这种布局结构在信息密度和操作效率之间取得了良好的平衡------头部提供关键概览信息,中间提供详细数据分析,底部提供导航能力。

Canvas绘图与状态管理的联动是本应用的一个技术亮点。呼吸动画通过定时器每秒切换breath状态,而breath状态不仅驱动UI组件的opacity变化,还直接参与了Canvas绘图逻辑------在drawRing方法中,breath值影响了进度弧的实际长度(0.92~1.0倍),让进度环产生了"呼吸"般的脉动效果。这种将声明式状态变量与命令式Canvas绘图融合的技术方案,展示了ArkUI处理复杂可视化场景的灵活性。

颜色系统深度解析

颜色系统是任何视觉应用的基础基石。在数字健康监测平台中,颜色不仅承担着美学装饰的职责,更承载着信息传递和状态标识的核心功能。下面我们将通过ColorPalette接口定义和COLORS常量实例化来逐段分析每个颜色值的设计意义。

ColorPalette接口定义

typescript 复制代码
// 颜色调色板接口:定义所有颜色字段
interface ColorPalette {
  bg: string;       // 全局背景色
  card: string;     // 卡片背景色
  chip: string;     // 芯片/标签背景色
  title: string;    // 主标题文字色
  sub: string;      // 副标题文字色
  text3: string;    // 三级辅助文字色
  teal: string;     // 青色主色调
  tealD: string;    // 青色深色调
  tealL: string;    // 青色浅色背景
  green: string;    // 健康绿色
  greenD: string;   // 深绿色调
  greenL: string;   // 浅绿色背景
  orange: string;   // 警示橙色
  orangeL: string;   // 浅橙色背景
  red: string;      // 危险红色
  redL: string;     // 浅红色背景
  purple: string;   // 睡眠紫色
  purpleL: string;  // 浅紫色背景
  blue: string;     // 心率蓝色
  blueL: string;    // 浅蓝色背景
  gold: string;     // 金色
  line: string;     // 分割线颜色
  tabOn: string;    // Tab选中色
  mask: string;     // 遮罩层颜色
}

ColorPalette接口定义了24个颜色字段,覆盖了从背景层到前景层、从主色调到辅助色的完整色彩体系。这种通过接口约束颜色字段的设计方式,确保了颜色系统的一致性和可维护性。每个字段都有明确的语义注释,开发者在使用颜色时不需要记忆具体的十六进制值,只需要通过COLORS.bg、COLORS.card等语义化引用即可。

接口的设计遵循了语义化命名的原则。bg/card/chip三个字段定义了三个层级的背景色------全局背景、卡片背景、标签背景,形成了一个从深到浅的层次结构。title/sub/text3三个字段定义了三个层级的文字色------主标题、副标题、辅助文字,确保了信息层级的清晰传达。teal/green/orange/red/purple/blue/gold七个字段定义了语义化的功能色------青色代表品牌主色、绿色代表健康正常、橙色代表关注警示、红色代表危险异常、紫色代表睡眠数据、蓝色代表心率偏低、金色代表成就徽章。

COLORS常量实例化

typescript 复制代码
// 颜色常量实例化:深色医疗主题色系
const COLORS: ColorPalette = {
  bg: '#061824',        // 深海蓝黑:全局背景,降低眼部疲劳
  card: '#0E2A3A',      // 深青灰:卡片背景,与全局背景形成层次
  chip: '#15384E',      // 中青灰:标签/进度条背景
  title: '#E8F6FF',     // 亮蓝白:主标题,高对比度易读
  sub: '#7AB0C8',       // 中青蓝:副标题,辅助信息
  text3: '#4A7896',     // 暗青灰:三级文字,最弱视觉权重
  teal: '#2DD4BF',     // 医疗青:品牌主色,进度环/Tab选中
  tealD: '#14B8A6',    // 深青:渐变起点
  tealL: '#0A4A42',    // 深绿青:提醒条背景
  green: '#4ADE80',    // 健康绿:正常状态标识
  greenD: '#22C55E',   // 深绿:渐变起点
  greenL: '#0A3D1F',   // 深绿底:正常状态背景
  orange: '#FB923C',   // 警示橙:关注/体温/卡路里
  orangeL: '#2A1A10',  // 深橙底:关注状态背景
  red: '#EF4444',      // 危险红:异常/过高心率
  redL: '#2A1010',     // 深红底:异常状态背景
  purple: '#A78BFA',   // 睡眠紫:深睡阶段标识
  purpleL: '#1A1530',  // 深紫底:紫色相关背景
  blue: '#60A5FA',     // 心率蓝:偏低心率/湿度标识
  blueL: '#0A1A3A',    // 深蓝底:蓝色相关背景
  gold: '#FBBF24',     // 金色:成就徽章/评级
  line: '#15384E',     // 分割线:与chip同色保持一致
  tabOn: '#2DD4BF',    // Tab选中:与teal一致
  mask: 'rgba(0,0,0,0.55)'  // 弹窗遮罩:55%透明黑色
};

COLORS常量将接口定义的抽象字段映射为具体的颜色值。背景色#061824是一个极深的蓝黑色,这种颜色在医疗环境中能够有效降低屏幕亮度对眼部的刺激,特别是在夜间使用场景中。卡片色#0E2A3A比背景色亮约一个层级,通过这种微妙的明度差异在视觉上将卡片从背景中"浮"出来。chip色#15384E则介于背景和卡片之间,用于标签、进度条槽等次要容器元素。

主标题色#E8F6FF是一种带有蓝色调倾向的亮白色,在深色背景上具有极高的对比度,确保了关键数据的可读性。副标题色#7AB0C8和三级文字色#4A7896则通过明度的递减建立了信息层级------越重要的信息用越亮的颜色,越次要的信息用越暗的颜色。

医疗青#2DD4BF是整个平台的品牌主色,它是一种介于青色和绿色之间的色调,既有医疗科技感又有健康活力感。这个颜色被用于进度环的进度弧、Tab选中态、快捷入口的强调文字等关键视觉元素。健康绿#4ADE80用于标识"正常"状态,橙色#FB923C用于标识"关注"状态,红色#EF4444用于标识"异常"状态,这三色构成了健康数据评估的标准色系。

值得注意的是,每个功能色都配有对应的浅色背景版本(以L结尾的字段)。这种"浅色背景+功能色文字"的组合在状态标签中大量使用------例如"正常"标签会使用greenL背景+green文字,"异常"标签会使用redL背景+red文字。这种设计在深色主题中尤其重要,因为直接使用功能色作为大面积背景会导致视觉过于刺眼。

遮罩色rgba(0,0,0,0.55)使用了55%透明度的纯黑色,在弹窗场景中让背景内容变暗但仍然隐约可见,营造了层次感和聚焦感。这个透明度的选择经过了仔细考量------太高会让背景完全不可见失去上下文,太低则无法形成足够的视觉聚焦。

常量定义与数据架构

常量定义层是应用数据的静态骨架,它定义了Tab导航结构、快捷入口标签、Canvas图表数据等关键信息。这些常量在应用启动时即被初始化,为UI渲染提供了即时的数据支撑。

Tab导航常量

typescript 复制代码
// Tab元数据接口:定义Tab项的结构
interface TabMeta {
  icon: string;   // Tab图标(Emoji)
  label: string;  // Tab文字标签
}

// 6个Tab导航项
const TAB_LIST: TabMeta[] = [
  { icon: '🏠', label: '概览' },
  { icon: '❤️', label: '心率' },
  { icon: '😴', label: '睡眠' },
  { icon: '📐', label: '体型' },
  { icon: '📋', label: '报告' },
  { icon: '👤', label: '我的' }
];

TabMeta接口定义了Tab导航项的数据结构,包含icon和label两个字段。icon字段使用Emoji字符作为图标------这种选择在跨平台一致性上有天然优势,不需要维护多套图标资源。六个Tab分别对应健康监测平台的六个核心功能模块:概览提供今日健康数据的汇总视图,心率展示24小时心率趋势折线图,睡眠通过饼图分析睡眠阶段,体型用雷达图评估多维体征,报告以周历卡片形式展示健康报告,我的展示个人成就和时间统计。

快捷入口与图表数据常量

typescript 复制代码
// 快捷入口标签
const QUICK_TAGS: string[] = ['心率', '血氧', '体温', '血压', '睡眠', '步数'];

// Canvas 进度环数据
const RING_PROGRESS: number = 0.72;

// 24小时心率折线图数据
const HR_DATA: number[] = [62, 58, 55, 56, 60, 68, 75, 88, 95, 82, 78, 72, 68, 75, 88, 102, 110, 98, 85, 72, 68, 65, 62, 60];
const HR_MAX: number = 130;
const HR_LABELS: string[] = ['00', '06', '12', '18', '24'];

QUICK_TAGS数组定义了六个快捷入口标签,涵盖了健康监测的核心指标类型。这些标签在头部区域以宫格形式排列,用户可以快速进入对应的监测页面。

RING_PROGRESS常量定义了进度环的完成率0.72,表示今日健康目标完成了72%。HR_DATA数组包含了24个小时的心率数据------从凌晨0点的62bpm到晚上22点的62bpm,完整记录了一天的心率波动。数据中凌晨时段(0-6点)心率最低(55-68bpm)符合睡眠状态,上午时段(8-12点)心率逐渐升高(75-95bpm)符合晨间活动,下午到傍晚(14-20点)出现峰值(88-110bpm)对应下午运动或工作高峰。

typescript 复制代码
// 饼图数据接口:睡眠阶段
interface PieData {
  val: number;    // 百分比值
  label: string;  // 阶段标签
}

// 睡眠阶段数据
const PIE_DATA: PieData[] = [
  { val: 35, label: '深睡' },
  { val: 25, label: '浅睡' },
  { val: 20, label: 'REM' },
  { val: 20, label: '清醒' }
];

// 雷达图数据
const RADAR_LABELS: string[] = ['心肺', '力量', '柔韧', '耐力', '代谢', '恢复'];
const RADAR_VALUES: number[] = [0.85, 0.72, 0.60, 0.90, 0.78, 0.65];

// 月度步数数据
const MONTH_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const STEP_VAL: number[] = [8200, 9500, 7800, 11200, 9800, 8500];
const STEP_MAX: number = 12000;

// 周天数
const WEEK_DAY: string[] = ['一', '二', '三', '四', '五', '六', '日'];

PieData接口定义了饼图数据项的结构。四个睡眠阶段的占比之和为100%------深睡35%是一个较好的比例(健康推荐深睡占比20-25%以上),浅睡25%和REM20%也在正常范围内,清醒20%略高但属于可接受范围。

RADAR_LABELS和RADAR_VALUES定义了雷达图的六个维度。心肺0.85最高表明心血管功能优秀,柔韧0.60最低提示需要加强拉伸训练。这组数据构成了体征综合评估的可视化基础。

月度步数数据STEP_VAL记录了近6个月的日均步数。第4个月(index 3)的11200步是最高值,在柱状图中会被用teal主色高亮显示,其他月份用tealL深色显示。STEP_MAX设为12000作为柱状图的满刻度参考值。

辅助函数解析

辅助函数层提供了"数据到视觉"的映射逻辑,是业务规则与UI呈现之间的桥梁。健康监测平台定义了三个辅助函数,分别处理心率区间、睡眠阶段和报告状态的颜色映射。

心率区间颜色映射

typescript 复制代码
/**
 * 心率区间颜色映射
 * 根据心率值返回对应的语义颜色
 * @param hr - 心率值(bpm)
 * @returns 对应区间的颜色值
 */
function hrColor(hr: number): string {
  if (hr < 60) {
    // 低于60bpm:心率偏低,使用蓝色标识
    return COLORS.blue;
  }
  if (hr < 100) {
    // 60-100bpm:正常心率范围,使用绿色标识
    return COLORS.green;
  }
  if (hr < 120) {
    // 100-120bpm:心率偏高,使用橙色标识
    return COLORS.orange;
  }
  // 超过120bpm:心率过高,使用红色标识
  return COLORS.red;
}

hrColor函数接受一个心率数值,返回对应的语义颜色。这个函数的设计基于医学上公认的心率区间标准:静息心率低于60bpm属于心动过缓(蓝色提示),60-100bpm是正常范围(绿色安全),100-120bpm属于心动过速的边缘(橙色关注),超过120bpm则需要警惕(红色危险)。

这种分级的颜色映射在折线图的数据点着色中尤为重要------用户可以一眼看出心率曲线的哪些时段处于异常范围,而不需要逐个读取数值。函数内部使用简单的if-else链式判断,这种结构在区间数量较少(4个)时比switch-case或查表法更加清晰直观。

睡眠阶段颜色映射

typescript 复制代码
/**
 * 睡眠阶段颜色映射
 * 根据睡眠阶段名称返回对应的语义颜色
 * @param stage - 睡眠阶段名称
 * @returns 对应阶段的颜色值
 */
function sleepColor(stage: string): string {
  if (stage === '深睡') {
    // 深度睡眠:使用紫色标识,表示深度休息
    return COLORS.purple;
  }
  if (stage === '浅睡') {
    // 浅度睡眠:使用蓝色标识,表示轻度休息
    return COLORS.blue;
  }
  if (stage === 'REM') {
    // 快速眼动睡眠:使用绿色标识,表示梦境阶段
    return COLORS.green;
  }
  // 清醒状态:使用橙色标识,表示睡眠中断
  return COLORS.orange;
}

sleepColor函数将四个睡眠阶段映射为四种语义颜色。紫色代表深睡------这是最重要的恢复性睡眠阶段,用最高贵的紫色来标识;蓝色代表浅睡------介于深睡和清醒之间的过渡状态;绿色代表REM(快速眼动睡眠)------做梦的主要阶段,大脑活跃但身体放松;橙色代表清醒------睡眠中的中断状态,需要引起注意。

这个函数在饼图绘制和睡眠时段列表中都被调用。在饼图中,每个扇形使用对应阶段的颜色填充;在时段列表中,每个记录左侧的色条使用对应颜色,右侧的时长文字也使用同色。这种一致的颜色映射让用户在视觉上能够快速关联饼图和列表中的同阶段数据。

报告状态颜色映射

typescript 复制代码
/**
 * 报告状态颜色映射
 * 根据报告状态返回对应的语义颜色
 * @param s - 报告状态字符串
 * @returns 对应状态的颜色值
 */
function reportColor(s: string): string {
  if (s === '正常') {
    // 正常状态:使用绿色标识
    return COLORS.green;
  }
  if (s === '关注') {
    // 关注状态:使用橙色标识,需留意但不紧急
    return COLORS.orange;
  }
  if (s === '异常') {
    // 异常状态:使用红色标识,需及时处理
    return COLORS.red;
  }
  // 默认:使用三级文字色
  return COLORS.text3;
}

reportColor函数处理健康报告的三种状态。这个函数的设计逻辑与前两个函数一致------绿色代表安全、橙色代表关注、红色代表异常。这种"红黄绿"三色体系在医疗应用中是通用的标准,用户无需学习即可理解其含义。
#mermaid-svg-p2T8CzMhoRTLpmZs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p2T8CzMhoRTLpmZs .error-icon{fill:#552222;}#mermaid-svg-p2T8CzMhoRTLpmZs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p2T8CzMhoRTLpmZs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p2T8CzMhoRTLpmZs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p2T8CzMhoRTLpmZs .marker.cross{stroke:#333333;}#mermaid-svg-p2T8CzMhoRTLpmZs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p2T8CzMhoRTLpmZs p{margin:0;}#mermaid-svg-p2T8CzMhoRTLpmZs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs .cluster-label text{fill:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs .cluster-label span{color:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs .cluster-label span p{background-color:transparent;}#mermaid-svg-p2T8CzMhoRTLpmZs .label text,#mermaid-svg-p2T8CzMhoRTLpmZs span{fill:#333;color:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs .node rect,#mermaid-svg-p2T8CzMhoRTLpmZs .node circle,#mermaid-svg-p2T8CzMhoRTLpmZs .node ellipse,#mermaid-svg-p2T8CzMhoRTLpmZs .node polygon,#mermaid-svg-p2T8CzMhoRTLpmZs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p2T8CzMhoRTLpmZs .rough-node .label text,#mermaid-svg-p2T8CzMhoRTLpmZs .node .label text,#mermaid-svg-p2T8CzMhoRTLpmZs .image-shape .label,#mermaid-svg-p2T8CzMhoRTLpmZs .icon-shape .label{text-anchor:middle;}#mermaid-svg-p2T8CzMhoRTLpmZs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p2T8CzMhoRTLpmZs .rough-node .label,#mermaid-svg-p2T8CzMhoRTLpmZs .node .label,#mermaid-svg-p2T8CzMhoRTLpmZs .image-shape .label,#mermaid-svg-p2T8CzMhoRTLpmZs .icon-shape .label{text-align:center;}#mermaid-svg-p2T8CzMhoRTLpmZs .node.clickable{cursor:pointer;}#mermaid-svg-p2T8CzMhoRTLpmZs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p2T8CzMhoRTLpmZs .arrowheadPath{fill:#333333;}#mermaid-svg-p2T8CzMhoRTLpmZs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p2T8CzMhoRTLpmZs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p2T8CzMhoRTLpmZs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p2T8CzMhoRTLpmZs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p2T8CzMhoRTLpmZs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p2T8CzMhoRTLpmZs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p2T8CzMhoRTLpmZs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p2T8CzMhoRTLpmZs .cluster text{fill:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs .cluster span{color:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p2T8CzMhoRTLpmZs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p2T8CzMhoRTLpmZs rect.text{fill:none;stroke-width:0;}#mermaid-svg-p2T8CzMhoRTLpmZs .icon-shape,#mermaid-svg-p2T8CzMhoRTLpmZs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p2T8CzMhoRTLpmZs .icon-shape p,#mermaid-svg-p2T8CzMhoRTLpmZs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p2T8CzMhoRTLpmZs .icon-shape .label rect,#mermaid-svg-p2T8CzMhoRTLpmZs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p2T8CzMhoRTLpmZs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p2T8CzMhoRTLpmZs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p2T8CzMhoRTLpmZs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 心率数值






睡眠阶段
深睡
浅睡
REM
清醒
报告状态
正常
关注
异常
其他
输入数据
数据类型判断
hr < 60?
蓝色 - 偏低
hr < 100?
绿色 - 正常
hr < 120?
橙色 - 偏高
红色 - 过高
stage 判断
紫色
蓝色
绿色
橙色
status 判断
绿色
橙色
红色
灰色

上面的Mermaid流程图完整展示了三个辅助函数的决策路径。无论是心率区间、睡眠阶段还是报告状态,最终都映射到同一个颜色体系中。这种统一的颜色语义体系让整个应用的视觉语言保持一致------用户在任何页面看到绿色就知道代表正常,看到红色就知道需要关注。

数据模型层

数据模型层使用@Observed装饰器定义了四个可观察的数据类,分别对应睡眠记录、体征指标、健康报告和成就徽章。每个类都有明确的属性定义、构造函数和预置数据数组。

SleepRecord - 睡眠时段记录

typescript 复制代码
/**
 * 睡眠时段记录模型
 * 记录每个睡眠阶段的起止时间和持续时长
 */
@Observed
export class SleepRecord {
  stage: string;    // 睡眠阶段:深睡/浅睡/REM/清醒
  start: string;    // 开始时间
  end: string;      // 结束时间
  duration: string; // 持续时长

  /**
   * 构造函数
   * @param stage - 睡眠阶段名称
   * @param start - 开始时间
   * @param end - 结束时间
   * @param duration - 持续时长
   */
  constructor(stage: string, start: string, end: string, duration: string) {
    this.stage = stage;
    this.start = start;
    this.end = end;
    this.duration = duration;
  }
}

// 预置睡眠数据:完整的一晚睡眠记录
const SLEEP_LIST: SleepRecord[] = [
  new SleepRecord('深睡', '23:00', '01:30', '1h30m'),
  new SleepRecord('浅睡', '01:30', '03:00', '1h30m'),
  new SleepRecord('REM', '03:00', '04:30', '1h30m'),
  new SleepRecord('深睡', '04:30', '05:30', '1h00m'),
  new SleepRecord('浅睡', '05:30', '06:20', '0h50m'),
  new SleepRecord('清醒', '06:20', '06:30', '0h10m')
];

SleepRecord类封装了一条睡眠时段记录的完整信息。stage属性记录睡眠阶段,start和end记录起止时间,duration记录持续时长。这个类被@Observed装饰,意味着当其实例作为@State变量的元素时,属性变化可以被框架感知。

预置数据SLEEP_LIST包含了6条睡眠时段记录,模拟了一晚从23:00到06:30的完整睡眠周期。数据展示了健康的睡眠模式------深睡在前半夜占比更高(23:00-01:30和04:30-05:30),REM出现在后半夜(03:00-04:30),清醒时间很短(仅10分钟)。这种数据排列符合人体睡眠生理规律。

MetricItem - 体征指标

typescript 复制代码
/**
 * 体征指标模型
 * 记录各项生命体征指标的数值和状态
 */
@Observed
export class MetricItem {
  name: string;    // 指标名称
  value: string;   // 指标数值
  unit: string;    // 计量单位
  status: string;  // 状态评估

  /**
   * 构造函数
   * @param name - 指标名称
   * @param value - 指标数值
   * @param unit - 计量单位
   * @param status - 状态评估
   */
  constructor(name: string, value: string, unit: string, status: string) {
    this.name = name;
    this.value = value;
    this.unit = unit;
    this.status = status;
  }
}

// 预置体征数据:6项核心生命体征指标
const METRIC_LIST: MetricItem[] = [
  new MetricItem('静息心率', '62', 'bpm', '正常'),
  new MetricItem('血氧饱和度', '98', '%', '正常'),
  new MetricItem('收缩压', '118', 'mmHg', '正常'),
  new MetricItem('舒张压', '78', 'mmHg', '正常'),
  new MetricItem('体温', '36.5', '℃', '正常'),
  new MetricItem('BMI', '22.1', 'kg/m²', '正常')
];

MetricItem类封装了体征指标的四个维度。预置数据包含了6项核心生命体征指标,涵盖了心血管(心率/血压)、呼吸(血氧)、代谢(体温/BMI)等关键健康维度。所有指标都标记为"正常"状态,展示了理想的健康数据模型。

ReportItem - 健康报告

typescript 复制代码
/**
 * 健康报告模型
 * 记录每次健康检测的日期、项目、状态和摘要
 */
@Observed
export class ReportItem {
  date: string;    // 报告日期
  title: string;   // 检测项目
  status: string;  // 状态评估
  summary: string; // 报告摘要

  /**
   * 构造函数
   * @param date - 报告日期
   * @param title - 检测项目名称
   * @param status - 状态评估
   * @param summary - 报告摘要
   */
  constructor(date: string, title: string, status: string, summary: string) {
    this.date = date;
    this.title = title;
    this.status = status;
    this.summary = summary;
  }
}

// 预置报告数据:6份近期的健康检测报告
const REPORT_LIST: ReportItem[] = [
  new ReportItem('08-24', '心电图检测', '正常', '窦性心律,心率 62bpm,未见异常'),
  new ReportItem('08-23', '血常规', '正常', '白细胞、红细胞、血小板均在正常范围'),
  new ReportItem('08-22', '血脂检测', '关注', '低密度脂蛋白偏高,建议控制饮食'),
  new ReportItem('08-21', '尿常规', '正常', '各项指标正常,无异常发现'),
  new ReportItem('08-20', '肝功能', '正常', '谷丙转氨酶、谷草转氨酶正常'),
  new ReportItem('08-19', '甲状腺功能', '正常', 'TSH、FT3、FT4 均在正常范围')
];

ReportItem类封装了健康报告的完整信息。预置数据包含了6份检测报告,从08-19到08-24共6天。值得注意的是08-22的血脂检测标记为"关注"状态------低密度脂蛋白偏高是心血管疾病的风险因子,需要通过饮食控制来改善。这个数据项在报告列表中会用橙色背景突出显示,提醒用户关注。

HealthAchieve - 成就徽章

typescript 复制代码
/**
 * 健康成就徽章模型
 * 记录用户达成的健康成就目标
 */
@Observed
export class HealthAchieve {
  icon: string;   // 徽章图标
  name: string;   // 成就名称
  desc: string;   // 成就描述
  got: boolean;   // 是否已达成

  /**
   * 构造函数
   * @param icon - 徽章图标Emoji
   * @param name - 成就名称
   * @param desc - 成就描述
   * @param got - 是否已达成
   */
  constructor(icon: string, name: string, desc: string, got: boolean) {
    this.icon = icon;
    this.name = name;
    this.desc = desc;
    this.got = got;
  }
}

// 已达成成就数量
const ACHIEVE_GOT: number = 3;

// 预置成就数据:8个健康成就徽章
const ACHIEVE_LIST: HealthAchieve[] = [
  new HealthAchieve('🏃', '万步达人', '单日步数突破 10000', true),
  new HealthAchieve('❤️', '心率稳定', '连续 7 天静息心率 60-70', true),
  new HealthAchieve('😴', '优质睡眠', '深睡占比超过 30%', true),
  new HealthAchieve('📊', '数据控', '连续记录 30 天健康数据', false),
  new HealthAchieve('🥗', '饮食规律', '连续 14 天按时三餐', false),
  new HealthAchieve('💧', '喝水达人', '每日饮水 8 杯连续 7 天', false),
  new HealthAchieve('🧘', '冥想新手', '完成 10 次冥想练习', false),
  new HealthAchieve('🏆', '健康满分', '全项指标达标 30 天', false)
];

HealthAchieve类封装了成就徽章的信息。got字段是一个布尔值,标识该成就是否已达成。预置数据中8个成就有3个已达成(万步达人、心率稳定、优质睡眠),5个未达成。在UI中,已达成徽章使用完整不透明度显示,未达成徽章使用0.3的透明度显示,形成明显的视觉对比。ACHIEVE_GOT常量单独定义了已达成数量3,用于"我的"页面标题中显示"3/8"的进度。

组件主体

组件主体是整个应用的运行时核心。@Entry和@Component装饰器将Page802结构体标记为页面入口组件,框架会自动管理其生命周期和渲染流程。

装饰器与状态变量

typescript 复制代码
/**
 * 康脉健康监测平台主页面
 * 6个Tab:概览/心率/睡眠/体型/报告/我的
 * 4种Canvas图表:进度环/折线图/饼图/雷达图
 */
@Entry
@Component
struct Page802 {
  // 当前选中的Tab索引
  @State currentTab: number = 0;
  // 弹窗状态控制
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  // 编辑/删除索引
  @State editIdx: number = -1;
  @State delIdx: number = -1;
  // 呼吸动画状态
  @State breath: boolean = false;
  // Canvas抗锯齿开关(HarmonyOS 6.1.1 特性)
  @State antialiasOn: boolean = true;
  // 定时器ID
  timer: number = -1;

  // Canvas 绘图上下文(4个独立上下文对应4种图表)
  private ringCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
  private lineCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
  private pieCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
  private radarCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));

  // 数据列表状态变量
  @State sleepList: SleepRecord[] = SLEEP_LIST;
  @State metricList: MetricItem[] = METRIC_LIST;
  @State reportList: ReportItem[] = REPORT_LIST;
  @State achieveList: HealthAchieve[] = ACHIEVE_LIST;

@Entry装饰器将Page802标记为页面入口组件。@Component装饰器使其成为一个可被框架管理的自定义组件。这两个装饰器的组合是ArkUI页面开发的标准模式。

@State状态变量分为三类:交互控制类(currentTab/addModal/editModal/delModal/editIdx/delIdx)、动画效果类(breath)、技术特性类(antialiasOn)。currentTab初始值为0表示应用启动时默认显示概览Tab。三个弹窗状态变量初始值都为false表示弹窗默认不显示。

四个Canvas绘图上下文ringCtx/lineCtx/pieCtx/radarCtx分别用于进度环、折线图、饼图、雷达图的绘制。它们使用private修饰符而非@State------因为绘图上下文本身不需要触发UI刷新,Canvas的刷新是通过调用绘图方法手动触发的。RenderingContextSettings(true)中的true参数开启了初始的抗锯齿。

生命周期函数

typescript 复制代码
  /**
   * 组件即将出现时调用
   * 创建呼吸动画定时器,每秒切换breath状态
   * 当处于概览Tab时同步重绘进度环
   */
  aboutToAppear() {
    this.timer = setInterval(() => {
      // 切换呼吸状态:true/false交替
      this.breath = !this.breath;
      // 仅在概览Tab时重绘进度环(性能优化)
      if (this.currentTab === 0) {
        this.drawRing();
      }
    }, 1000);
  }

  /**
   * 组件即将消失时调用
   * 清理定时器防止内存泄漏
   */
  aboutToDisappear() {
    clearInterval(this.timer);
  }

aboutToAppear在组件实例创建后、UI渲染前被调用。这里通过setInterval创建了一个每1000毫秒(1秒)触发的定时器。定时器回调中做了两件事:切换breath状态变量和条件性重绘进度环。

breath状态的切换驱动了呼吸动画效果------所有引用了this.breath的UI组件会每秒在两种状态间切换,产生闪烁/脉动的视觉效果。在头部区域,饮水提醒的水滴emoji会每秒在opacity 1和0.4之间切换;在"我的"页面,统计图标的emoji会每秒在opacity 1和0.5之间切换。

定时器回调中还包含了条件性Canvas重绘逻辑------只有当currentTab等于0(概览Tab)时才调用drawRing()。这是一个重要的性能优化策略:当用户切换到其他Tab时,进度环不可见,此时重绘是纯粹的性能浪费。通过currentTab条件判断,避免了不必要的Canvas重绘操作。

aboutToDisappear在组件被销毁前调用,这里通过clearInterval清理了定时器。如果不清理,定时器会在组件销毁后继续运行,造成内存泄漏和潜在的空指针异常。这种"创建-清理"的配对模式是资源管理的标准实践。

抗锯齿切换方法

typescript 复制代码
  /**
   * HarmonyOS 6.1.1 新特性:运行时动态开关 Canvas 抗锯齿
   * 切换所有Canvas上下文的抗锯齿状态并重绘当前Tab的图表
   */
  toggleAntialias() {
    // 切换抗锯齿状态
    this.antialiasOn = !this.antialiasOn;
    // 同步到所有Canvas上下文
    this.ringCtx.antialias = this.antialiasOn;
    this.lineCtx.antialias = this.antialiasOn;
    this.pieCtx.antialias = this.antialiasOn;
    this.radarCtx.antialias = this.antialiasOn;
    // 根据当前Tab重绘对应图表
    if (this.currentTab === 0) {
      this.drawRing();
    } else if (this.currentTab === 1) {
      this.drawLine();
    } else if (this.currentTab === 2) {
      this.drawPie();
    } else if (this.currentTab === 3) {
      this.drawRadar();
    }
  }

toggleAntialias方法展示了HarmonyOS 6.1.1引入的运行时Canvas抗锯齿开关特性。传统的Canvas API中,抗锯齿通常在创建上下文时通过RenderingContextSettings设定,一旦创建就无法更改。HarmonyOS 6.1.1突破了这一限制,允许通过ctx.antialias属性在运行时动态切换。

这个特性在医疗场景中有实际意义------"视觉舒适模式"(抗锯齿开启)适合日常浏览,文字边缘平滑减轻视觉疲劳;"医学精准模式"(抗锯齿关闭)适合医生判读,像素级锐利有助于精确识别数据。切换后,方法会根据当前Tab调用对应的绘图方法进行重绘,确保视觉变化即时生效。

build方法与整体布局

build方法是ArkUI组件的核心,它声明了组件的UI结构。所有视觉元素都通过build方法中的组件声明来构建。

整体布局架构

typescript 复制代码
  /**
   * 构建主页面
   * Stack叠层:底部内容 + 弹窗浮层
   * 内部Column:头部 + 滚动区 + Tab栏
   */
  build() {
    Stack() {
      Column() {
        // 头部健康数据面板
        this.headerHealth()
        // 分割线
        Divider().strokeWidth(1).color(COLORS.line)
        // 中间可滚动内容区
        Scroll() {
          Column() {
            // 根据currentTab条件渲染对应Tab页面
            if (this.currentTab === 0) {
              this.tabOverview()
            } else if (this.currentTab === 1) {
              this.tabHeart()
            } else if (this.currentTab === 2) {
              this.tabSleep()
            } else if (this.currentTab === 3) {
              this.tabBody()
            } else if (this.currentTab === 4) {
              this.tabReport()
            } else if (this.currentTab === 5) {
              this.tabMine()
            }
            // 月度步数柱状图(所有Tab通用)
            this.chartCard()
          }
          .padding({ left: 14, right: 14, top: 12, bottom: 12 })
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)

        // 底部Tab导航栏
        this.tabBar()
      }
      .width('100%')
      .height('100%')

      // 弹窗系统(叠层在最上方)
      if (this.addModal) {
        this.panelAdd(() => {
          this.addModal = false;
        })
      }
      if (this.editModal) {
        this.panelEdit(() => {
          this.editModal = false;
        })
      }
      if (this.delModal) {
        this.panelDel(() => {
          this.delModal = false;
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

build方法的最外层是Stack容器------这是为了实现弹窗的叠层效果。Stack的第一层是主内容Column,第二层及以后是条件渲染的弹窗。当弹窗状态变量(addModal/editModal/delModal)为true时,对应的弹窗组件会被渲染到Stack的上层,覆盖住底层内容。

主内容Column采用垂直三段式布局:headerHealth(头部) + Scroll(滚动区) + tabBar(底部导航)。Scroll组件设置了layoutWeight(1),占据头部和底部之间的所有剩余空间。scrollBar设为BarState.Off隐藏了滚动条,保持了界面的简洁性。

Scroll内部是一个Column容器,包含了条件渲染的Tab页面内容和通用的chartCard。Tab页面的条件渲染通过if-else if链实现------当currentTab变化时,对应的@Builder方法被调用,生成新的UI片段替换旧片段。chartCard作为通用组件在所有Tab下都显示,展示了近6个月的步数柱状图趋势。

弹窗系统通过三个独立的if条件实现------每个弹窗有自己的状态变量和关闭回调。回调函数使用箭头函数语法() => { this.addModal = false; },确保this指向组件实例。这种设计让弹窗的开关逻辑集中在build方法中,便于管理和维护。
#mermaid-svg-of7qmcSkpOQdko4S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-of7qmcSkpOQdko4S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-of7qmcSkpOQdko4S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-of7qmcSkpOQdko4S .error-icon{fill:#552222;}#mermaid-svg-of7qmcSkpOQdko4S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-of7qmcSkpOQdko4S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-of7qmcSkpOQdko4S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-of7qmcSkpOQdko4S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-of7qmcSkpOQdko4S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-of7qmcSkpOQdko4S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-of7qmcSkpOQdko4S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-of7qmcSkpOQdko4S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-of7qmcSkpOQdko4S .marker.cross{stroke:#333333;}#mermaid-svg-of7qmcSkpOQdko4S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-of7qmcSkpOQdko4S p{margin:0;}#mermaid-svg-of7qmcSkpOQdko4S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-of7qmcSkpOQdko4S .cluster-label text{fill:#333;}#mermaid-svg-of7qmcSkpOQdko4S .cluster-label span{color:#333;}#mermaid-svg-of7qmcSkpOQdko4S .cluster-label span p{background-color:transparent;}#mermaid-svg-of7qmcSkpOQdko4S .label text,#mermaid-svg-of7qmcSkpOQdko4S span{fill:#333;color:#333;}#mermaid-svg-of7qmcSkpOQdko4S .node rect,#mermaid-svg-of7qmcSkpOQdko4S .node circle,#mermaid-svg-of7qmcSkpOQdko4S .node ellipse,#mermaid-svg-of7qmcSkpOQdko4S .node polygon,#mermaid-svg-of7qmcSkpOQdko4S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-of7qmcSkpOQdko4S .rough-node .label text,#mermaid-svg-of7qmcSkpOQdko4S .node .label text,#mermaid-svg-of7qmcSkpOQdko4S .image-shape .label,#mermaid-svg-of7qmcSkpOQdko4S .icon-shape .label{text-anchor:middle;}#mermaid-svg-of7qmcSkpOQdko4S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-of7qmcSkpOQdko4S .rough-node .label,#mermaid-svg-of7qmcSkpOQdko4S .node .label,#mermaid-svg-of7qmcSkpOQdko4S .image-shape .label,#mermaid-svg-of7qmcSkpOQdko4S .icon-shape .label{text-align:center;}#mermaid-svg-of7qmcSkpOQdko4S .node.clickable{cursor:pointer;}#mermaid-svg-of7qmcSkpOQdko4S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-of7qmcSkpOQdko4S .arrowheadPath{fill:#333333;}#mermaid-svg-of7qmcSkpOQdko4S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-of7qmcSkpOQdko4S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-of7qmcSkpOQdko4S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-of7qmcSkpOQdko4S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-of7qmcSkpOQdko4S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-of7qmcSkpOQdko4S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-of7qmcSkpOQdko4S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-of7qmcSkpOQdko4S .cluster text{fill:#333;}#mermaid-svg-of7qmcSkpOQdko4S .cluster span{color:#333;}#mermaid-svg-of7qmcSkpOQdko4S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-of7qmcSkpOQdko4S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-of7qmcSkpOQdko4S rect.text{fill:none;stroke-width:0;}#mermaid-svg-of7qmcSkpOQdko4S .icon-shape,#mermaid-svg-of7qmcSkpOQdko4S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-of7qmcSkpOQdko4S .icon-shape p,#mermaid-svg-of7qmcSkpOQdko4S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-of7qmcSkpOQdko4S .icon-shape .label rect,#mermaid-svg-of7qmcSkpOQdko4S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-of7qmcSkpOQdko4S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-of7qmcSkpOQdko4S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-of7qmcSkpOQdko4S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 0
1
2
3
4
5
true
true
true
build 方法
Stack 叠层容器
Column 主内容层
addModal?
editModal?
delModal?
headerHealth 头部
Divider 分割线
Scroll 滚动区
tabBar 底部导航
currentTab?
tabOverview 概览
tabHeart 心率
tabSleep 睡眠
tabBody 体型
tabReport 报告
tabMine 我的
chartCard 月度图表
panelAdd 报告详情弹窗
panelEdit 目标设置弹窗
panelDel 删除确认弹窗

上面的Mermaid流程图清晰地展示了build方法的组件树结构。Stack作为根容器管理内容层和弹窗层的叠层关系。内容层的Column将屏幕垂直分为三段,中间的Scroll区域根据currentTab条件渲染6个Tab页面之一。三个弹窗通过各自的状态变量独立控制显示/隐藏。

健康监测头部构建器

头部是用户进入应用后首先看到的区域,它需要在一个紧凑的空间内传达尽可能多的关键健康信息。

headerHealth 方法

typescript 复制代码
  /**
   * 健康监测头部
   * 包含:位置信息、核心指标大数字、搜索栏、快捷入口、健康提醒
   */
  @Builder
  headerHealth() {
    Column({ space: 10 }) {
      // 第一行:位置信息与用户入口
      Row() {
        Column({ space: 2 }) {
          Text('📍 成都·高新区').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Text('今日健康评分 92 分').fontSize(9).fontColor(COLORS.text3)
        }
        Column().layoutWeight(1)
        Row({ space: 8 }) {
          Text('🔔').fontSize(16)
          Text('👤').fontSize(18)
        }
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 8 })

      // 第二行:核心健康指标三列大数字
      Row({ space: 0 }) {
        Column({ space: 2 }) {
          Text('62').fontSize(28).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
          Text('心率 bpm').fontSize(9).fontColor(COLORS.text3)
        }
        .layoutWeight(1)
        Column({ space: 2 }) {
          Text('8432').fontSize(28).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
          Text('步数').fontSize(9).fontColor(COLORS.text3)
        }
        .layoutWeight(1)
        Column({ space: 2 }) {
          Text('320').fontSize(28).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
          Text('卡路里').fontSize(9).fontColor(COLORS.text3)
        }
        .layoutWeight(1)
      }
      .width('90%')

      // 第三行:搜索栏
      Row({ space: 8 }) {
        Text('🔍').fontSize(14)
        Text('搜索健康报告·指标·科室...').fontSize(11).fontColor(COLORS.text3)
        Column().layoutWeight(1)
        Text('搜索').fontSize(11).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .height(38)
      .backgroundColor(COLORS.card)
      .borderRadius(19)
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)

      // 第四行:快捷入口宫格
      Row({ space: 0 }) {
        ForEach(QUICK_TAGS, (tag: string, idx: number) => {
          Column({ space: 4 }) {
            Text(idx === 0 ? '❤️' : idx === 1 ? '🩸' : idx === 2 ? '🌡️' : idx === 3 ? '💉' : idx === 4 ? '😴' : '🏃').fontSize(20)
            Text(tag).fontSize(9).fontColor(COLORS.sub)
          }
          .layoutWeight(1)
        })
      }
      .width('90%')
      .padding({ top: 6, bottom: 6 })

      // 第五行:健康提醒条(呼吸动画)
      Row({ space: 8 }) {
        Text('💧').fontSize(14)
          .opacity(this.breath ? 1 : 0.4)
        Text('今日饮水 5/8 杯,记得多喝水').fontSize(10).fontColor(COLORS.tealD)
        Column().layoutWeight(1)
        Text('打卡 ›').fontSize(9).fontColor(COLORS.teal)
      }
      .width('90%')
      .height(32)
      .backgroundColor(COLORS.tealL)
      .borderRadius(8)
      .padding({ left: 10, right: 10 })
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor(COLORS.card)
  }

headerHealth构建器以Column为容器,垂直排列了五个信息行。第一行是位置信息和用户入口------左侧显示定位城市和健康评分,右侧提供通知和个人中心的入口。第二行是三个核心健康指标的大数字展示------心率62bpm(青色)、步数8432(绿色)、卡路里320(橙色),每个数字使用28号粗体字体确保视觉醒目,三种颜色分别对应不同的健康维度。

第三行是搜索栏,采用圆角胶囊造型(borderRadius 19高度38)的卡片背景设计。第四行是六个快捷入口宫格,通过ForEach遍历QUICK_TAGS数组生成。每个入口使用一个Emoji图标和文字标签,使用三元运算符根据idx选择对应的图标。第五行是饮水提醒条,使用tealL作为背景色,水滴emoji通过this.breath状态变量控制opacity在1和0.4之间切换,产生呼吸闪烁效果。

整个头部使用card色作为背景,与全局bg色形成层次。padding设置合理的内边距确保内容不贴边。width设为90%或100%,在视觉上形成了错落的层次感。

Tab 0 概览页面构建器

概览Tab是应用的默认首页,它需要在一个页面中汇总今日健康数据的整体状况。

tabOverview 方法

typescript 复制代码
  /**
   * Tab 0:概览
   * 数据大卡 + Canvas 进度环 + 今日指标清单
   */
  @Builder
  tabOverview() {
    Column({ space: 14 }) {
      // 标题行
      Row() {
        Text('📊 今日健康概览').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('08-24').fontSize(10).fontColor(COLORS.text3)
      }

      // 数据概览大卡
      Column({ space: 12 }) {
        Row({ space: 0 }) {
          Column({ space: 4 }) {
            Text('62').fontSize(24).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
            Text('心率 bpm').fontSize(9).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
          Column({ space: 4 }) {
            Text('98%').fontSize(24).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
            Text('血氧').fontSize(9).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
          Column({ space: 4 }) {
            Text('36.5').fontSize(24).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
            Text('体温 ℃').fontSize(9).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
        }

        // 打卡进度条
        Column({ space: 4 }) {
          Row() {
            Text('今日健康打卡').fontSize(10).fontColor(COLORS.sub)
            Column().layoutWeight(1)
            Text('5/7').fontSize(10).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
          }
          Row() {
            Column()
              .width('71%')
              .height(6)
              .backgroundColor(COLORS.teal)
              .borderRadius(3)
            Column().layoutWeight(1)
          }
          .width('100%')
          .height(6)
          .backgroundColor(COLORS.chip)
          .borderRadius(3)
        }
      }
      .width('100%')
      .padding(16)
      .backgroundColor(COLORS.card)
      .borderRadius(16)

tabOverview的第一部分是数据概览大卡。这个卡片展示了心率、血氧、体温三个核心指标,每个使用24号粗体字体。与头部的大数字不同,这里的数字字号略小(24 vs 28),因为头部需要更强的视觉冲击力,而内容区需要同时展示更多信息。

打卡进度条使用了两层Row嵌套实现------底层是chip色背景的进度槽(高度6、圆角3),上层是teal色的进度填充(宽度71%)。这种"槽+填充"的双层结构是进度条组件的标准实现模式。71%的宽度对应5/7的打卡进度(5除以7约等于0.714)。

typescript 复制代码
      // HarmonyOS 6.1.1 新特性:运行时抗锯齿开关
      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Row({ space: 6 }) {
            Text(this.antialiasOn ? '🖥️' : '🔬').fontSize(14)
            Text(this.antialiasOn ? '视觉舒适模式' : '医学精准模式').fontSize(11)
              .fontColor(this.antialiasOn ? COLORS.teal : COLORS.orange)
              .fontWeight(FontWeight.Bold)
          }
          Text(this.antialiasOn ? '抗锯齿已开启 · 文字边缘平滑' : '抗锯齿已关闭 · 文字像素锐利')
            .fontSize(8).fontColor(COLORS.text3)
        }
        Column().layoutWeight(1)
        // 拨动开关
        Row({ space: 0 }) {
          Text(this.antialiasOn ? 'ON' : 'OFF').fontSize(9)
            .fontColor(this.antialiasOn ? COLORS.teal : COLORS.text3)
            .fontWeight(FontWeight.Bold).width(28).textAlign(TextAlign.Center)
          Column()
            .width(36).height(18).borderRadius(9)
            .backgroundColor(this.antialiasOn ? COLORS.teal : COLORS.chip)
            .padding({ left: this.antialiasOn ? 18 : 2, right: this.antialiasOn ? 2 : 18, top: 2, bottom: 2 })
          Column()
            .width(14).height(14).borderRadius(7)
            .backgroundColor(COLORS.title)
        }
        .onClick(() => {
          this.toggleAntialias();
        })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.card)
      .borderRadius(10)
      .alignItems(VerticalAlign.Center)

这部分实现了HarmonyOS 6.1.1抗锯齿开关的UI组件。左侧显示当前模式的图标和名称------视觉舒适模式(🖥️/青色)或医学精准模式(🔬/橙色),下方有描述文字说明当前状态。右侧是一个手动实现的拨动开关------通过Column的padding动态调整小圆点的位置(开启时左padding 18推到右侧,关闭时右padding 18推到左侧),背景色也同步切换(teal/chip)。点击整个Row触发toggleAntialias方法。

typescript 复制代码
      // Canvas 进度环
      Column({ space: 8 }) {
        Row() {
          Text('🎯 今日目标完成率').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('详情 ›').fontSize(9).fontColor(COLORS.teal)
        }
        Canvas(this.ringCtx)
          .width('100%')
          .height(200)
          .onReady(() => {
            this.drawRing();
          })
        Row() {
          Text(this.antialiasOn ? '✓ 抗锯齿已开启' : '✗ 抗锯齿已关闭').fontSize(8)
            .fontColor(this.antialiasOn ? COLORS.green : COLORS.orange)
          Column().layoutWeight(1)
          Text('HarmonyOS 6.1.1 antialias 属性').fontSize(7).fontColor(COLORS.text3)
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)

进度环卡片使用Canvas组件作为核心元素。Canvas绑定了ringCtx绘图上下文,宽100%高200。onReady回调在Canvas组件完成初始化后调用drawRing方法进行首次绘制。Canvas下方有一个状态提示行,显示当前抗锯齿状态和技术特性标注。

typescript 复制代码
      // 今日指标清单
      Column({ space: 8 }) {
        Row() {
          Text('✅ 今日指标').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('全部正常 ✓').fontSize(9).fontColor(COLORS.green)
        }
        ForEach(this.metricList, (item: MetricItem, idx: number) => {
          Row({ space: 10 }) {
            Text(idx === 0 ? '❤️' : idx === 1 ? '🩸' : idx === 2 ? '💉' : idx === 3 ? '💉' : idx === 4 ? '🌡️' : '⚖️').fontSize(16)
            Column({ space: 2 }) {
              Text(item.name).fontSize(11).fontColor(COLORS.sub)
              Text(item.status).fontSize(8).fontColor(reportColor(item.status))
            }
            Column().layoutWeight(1)
            Text(item.value).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text(' ' + item.unit).fontSize(9).fontColor(COLORS.text3)
          }
          .width('100%')
          .padding({ top: 6, bottom: 6 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
    }
  }

今日指标清单使用ForEach遍历metricList状态变量。每行显示一个指标:左侧是Emoji图标,中间是指标名称和状态(状态颜色通过reportColor函数映射),右侧是数值和单位。这里复用了reportColor函数来为状态文字着色------虽然函数名包含"report",但其"正常/关注/异常"的三级映射同样适用于体征指标的状态显示。

Tab 1 心率页面构建器

心率Tab通过Canvas折线图展示24小时心率趋势,是应用中最核心的数据可视化模块之一。

tabHeart 方法

typescript 复制代码
  /**
   * Tab 1:心率
   * Canvas 折线图 + 心率区间图例 + 关键数据
   */
  @Builder
  tabHeart() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('❤️ 24 小时心率趋势').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('查看详情 ›').fontSize(9).fontColor(COLORS.teal)
      }

      // Canvas 折线图
      Column({ space: 8 }) {
        Canvas(this.lineCtx)
          .width('100%')
          .height(220)
          .onReady(() => {
            this.drawLine();
          })

        // 心率区间图例
        Row({ space: 12 }) {
          Row({ space: 4 }) {
            Column().width(8).height(8).backgroundColor(COLORS.blue).borderRadius(2)
            Text('偏低 <60').fontSize(8).fontColor(COLORS.text3)
          }
          Row({ space: 4 }) {
            Column().width(8).height(8).backgroundColor(COLORS.green).borderRadius(2)
            Text('正常 60-100').fontSize(8).fontColor(COLORS.text3)
          }
          Row({ space: 4 }) {
            Column().width(8).height(8).backgroundColor(COLORS.orange).borderRadius(2)
            Text('偏高 100-120').fontSize(8).fontColor(COLORS.text3)
          }
          Row({ space: 4 }) {
            Column().width(8).height(8).backgroundColor(COLORS.red).borderRadius(2)
            Text('过高 >120').fontSize(8).fontColor(COLORS.text3)
          }
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)

折线图卡片是心率Tab的核心组件。Canvas绑定lineCtx上下文,高度220提供足够的垂直空间展示心率波动。onReady回调调用drawLine方法进行首次绘制。Canvas下方是四个心率区间的图例------每个图例由一个8x8的色块和文字标签组成,颜色与hrColor函数的映射保持一致。图例让用户能够理解折线图中数据点颜色的含义。

typescript 复制代码
      // 关键数据
      Column({ space: 8 }) {
        Row() {
          Text('📈 关键数据').fontSize(11).fontColor(COLORS.sub).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
        }
        Row({ space: 0 }) {
          Column({ space: 2 }) {
            Text('55').fontSize(18).fontColor(COLORS.blue).fontWeight(FontWeight.Bold)
            Text('最低 bpm').fontSize(8).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
          Column({ space: 2 }) {
            Text('110').fontSize(18).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
            Text('最高 bpm').fontSize(8).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
          Column({ space: 2 }) {
            Text('72').fontSize(18).fontColor(COLORS.green).fontWeight(FontWeight.Bold)
            Text('平均 bpm').fontSize(8).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
    }
  }

关键数据卡片展示了心率统计的三个核心指标------最低55bpm(蓝色,对应偏低区间)、最高110bpm(橙色,对应偏高区间)、平均72bpm(绿色,对应正常区间)。数字颜色与心率区间图例一致,让用户能够快速关联统计数据与区间含义。

Tab 2 睡眠页面构建器

睡眠Tab通过Canvas饼图展示睡眠阶段占比,并配合时段列表展示详细的睡眠记录。

tabSleep 方法

typescript 复制代码
  /**
   * Tab 2:睡眠
   * Canvas 饼图 + 睡眠阶段图例 + 睡眠时段列表
   */
  @Builder
  tabSleep() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('😴 昨晚睡眠分析').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('总睡眠 7h20m').fontSize(9).fontColor(COLORS.text3)
      }

      // Canvas 饼图
      Column({ space: 8 }) {
        Canvas(this.pieCtx)
          .width('100%')
          .height(220)
          .onReady(() => {
            this.drawPie();
          })

        // 睡眠阶段图例
        Row({ space: 10 }) {
          ForEach(PIE_DATA, (d: PieData, idx: number) => {
            Row({ space: 4 }) {
              Column().width(8).height(8)
                .backgroundColor(sleepColor(d.label)).borderRadius(2)
              Text(d.label + ' ' + d.val.toString() + '%').fontSize(8).fontColor(COLORS.text3)
            }
          })
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)

饼图卡片使用pieCtx绘图上下文。图例通过ForEach遍历PIE_DATA数组生成,每个图例的色块颜色通过sleepColor函数映射------深睡紫色、浅睡蓝色、REM绿色、清醒橙色。图例文字同时显示阶段名称和百分比数值。

typescript 复制代码
      // 睡眠时段列表
      Column({ space: 6 }) {
        Text('🕐 睡眠时段明细').fontSize(11).fontColor(COLORS.sub).fontWeight(FontWeight.Bold)
        ForEach(this.sleepList, (item: SleepRecord, idx: number) => {
          Row({ space: 10 }) {
            // 左侧色条
            Column()
              .width(4)
              .height(30)
              .backgroundColor(sleepColor(item.stage))
              .borderRadius(2)
            // 中间信息
            Column({ space: 2 }) {
              Text(item.stage).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Text(item.start + ' - ' + item.end).fontSize(9).fontColor(COLORS.text3)
            }
            Column().layoutWeight(1)
            // 右侧时长
            Text(item.duration).fontSize(11).fontColor(sleepColor(item.stage)).fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .padding({ top: 4, bottom: 4 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
    }
  }

睡眠时段列表使用ForEach遍历sleepList状态变量。每行左侧有一个4x30的色条,颜色通过sleepColor函数映射。中间显示阶段名称和起止时间,右侧显示时长。色条颜色和时长文字颜色都使用sleepColor函数,确保与饼图中的颜色保持一致------用户可以通过颜色快速关联列表中的时段与饼图中的扇形。

Tab 3 体型页面构建器

体型Tab通过Canvas雷达图展示六维体征评估,并配合进度条列表展示各维度的详细得分。

tabBody 方法

typescript 复制代码
  /**
   * Tab 3:体型
   * Canvas 雷达图 + 各项指标详情列表
   */
  @Builder
  tabBody() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('📐 体征综合评估').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('综合 76 分').fontSize(9).fontColor(COLORS.green)
      }

      // Canvas 雷达图
      Column({ space: 8 }) {
        Canvas(this.radarCtx)
          .width('100%')
          .height(220)
          .onReady(() => {
            this.drawRadar();
          })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)

      // 指标详情列表
      Column({ space: 8 }) {
        Text('📊 各项指标详情').fontSize(11).fontColor(COLORS.sub).fontWeight(FontWeight.Bold)
        ForEach(RADAR_LABELS, (label: string, idx: number) => {
          Row({ space: 10 }) {
            Text(label).fontSize(11).fontColor(COLORS.sub)
            Column().layoutWeight(1)
            // 进度条
            Row() {
              Column()
                .width((RADAR_VALUES[idx] * 100).toFixed(0) + '%')
                .height(6)
                .backgroundColor(COLORS.teal)
                .borderRadius(3)
              Column().layoutWeight(1)
            }
            .width(80)
            .height(6)
            .backgroundColor(COLORS.chip)
            .borderRadius(3)
            Text((RADAR_VALUES[idx] * 100).toFixed(0) + '分').fontSize(9)
              .fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .padding({ top: 4, bottom: 4 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.card)
      .borderRadius(12)
    }
  }

雷达图卡片使用radarCtx绘图上下文。下方的指标详情列表通过ForEach遍历RADAR_LABELS数组,每行显示维度名称、进度条和得分。进度条宽度通过(RADAR_VALUES[idx] * 100).toFixed(0) + '%'动态计算------例如心肺0.85对应85%宽度,柔韧0.60对应60%宽度。进度条宽度80是固定的,在手机屏幕上提供足够的视觉参考但不占用过多空间。

Tab 4 报告页面构建器

报告Tab以周历卡片的样式展示健康报告列表,每份报告都有日期、项目、状态和摘要。

tabReport 方法

typescript 复制代码
  /**
   * Tab 4:报告
   * 周历导航 + 报告列表卡片
   */
  @Builder
  tabReport() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('📋 本周健康报告').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('6 份报告').fontSize(9).fontColor(COLORS.text3)
      }

      // 周历导航
      Row({ space: 8 }) {
        Text('‹').fontSize(14).fontColor(COLORS.text3)
        Text('08-19 ~ 08-24').fontSize(11).fontColor(COLORS.sub)
        Text('›').fontSize(14).fontColor(COLORS.text3)
        Column().layoutWeight(1)
        Text('本周').fontSize(9).fontColor(COLORS.teal)
      }
      .width('100%')
      .padding(10)
      .backgroundColor(COLORS.card)
      .borderRadius(10)
      .alignItems(VerticalAlign.Center)

      // 报告列表
      ForEach(this.reportList, (item: ReportItem, idx: number) => {
        Column({ space: 8 }) {
          Row() {
            Text(item.date).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Column().layoutWeight(1)
            Text(item.status).fontSize(9)
              .fontColor(reportColor(item.status))
              .backgroundColor(item.status === '正常' ? COLORS.greenL : item.status === '关注' ? COLORS.orangeL : item.status === '异常' ? COLORS.redL : COLORS.chip)
              .borderRadius(4)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          }
          Text(item.title).fontSize(11).fontColor(COLORS.sub)
          Text(item.summary).fontSize(10).fontColor(COLORS.text3)
            .maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.card)
        .borderRadius(10)
        .onClick(() => {
          this.editIdx = idx;
          this.addModal = true;
        })
      })
    }
  }

周历导航栏提供了前后翻周的交互入口(‹ ›)和当前周的范围显示(08-19 ~ 08-24)。右侧的"本周"标签用teal色强调。

报告列表通过ForEach遍历reportList状态变量。每张报告卡片包含三行信息:第一行是日期和状态标签(状态标签使用reportColor函数着色,背景色根据状态使用对应的浅色版本);第二行是检测项目名称;第三行是报告摘要(最多2行,超出部分省略号显示)。点击报告卡片会设置editIdx并打开panelAdd弹窗查看报告详情------状态标签的背景色通过嵌套三元运算符实现:正常用greenL、关注用orangeL、异常用redL。

Tab 5 我的页面构建器

"我的"Tab展示个人健康记录统计和成就徽章,使用了渐变大卡和四宫格的布局设计。

tabMine 方法

typescript 复制代码
  /**
   * Tab 5:我的
   * 时长渐变大卡 + 成就徽章四宫格
   */
  @Builder
  tabMine() {
    Column({ space: 14 }) {
      // 时长大卡
      Column({ space: 10 }) {
        Row() {
          Text('📊').fontSize(28)
            .opacity(this.breath ? 1 : 0.5)
          Column().layoutWeight(1)
          Column({ space: 2 }) {
            Text('健康记录').fontSize(10).fontColor('rgba(255,255,255,0.8)')
            Row({ space: 4 }) {
              Text('128').fontSize(28).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Text('天').fontSize(10).fontColor('rgba(255,255,255,0.8)')
            }
          }
        }
        Row() {
          Text('距 150 天勋章还需 22 天').fontSize(9).fontColor('rgba(255,255,255,0.8)')
          Column().layoutWeight(1)
          Text('85%').fontSize(9).fontColor('rgba(255,255,255,0.9)')
        }
        Row() {
          Column()
            .width('85%')
            .height(4)
            .backgroundColor(COLORS.teal)
            .borderRadius(2)
          Column().layoutWeight(1)
        }
        .width('100%')
        .height(4)
        .backgroundColor('rgba(255,255,255,0.2)')
        .borderRadius(2)
      }
      .width('100%')
      .padding(20)
      .borderRadius(16)
      .linearGradient({
        angle: 135,
        colors: [[COLORS.tealD, 0], [COLORS.tealL, 1]]
      })

时长大卡使用了linearGradient实现渐变背景------从tealD(深青)到tealL(深绿青),135度角方向。这种渐变背景让卡片从普通的深色卡片中脱颖而出,产生视觉焦点。卡片内部的文字颜色使用了rgba(255,255,255,0.8)的半透明白色,在渐变背景上保持可读性的同时营造层次感。

统计图标的emoji通过this.breath控制opacity在1和0.5之间切换,与头部饮水提醒的呼吸效果保持一致。128天的数字使用28号粗体白色字体,是整个卡片的视觉中心。底部进度条显示85%的完成度(128/150约85.3%),使用teal色填充和半透明白色背景。

typescript 复制代码
      // 成就徽章四宫格
      Column({ space: 10 }) {
        Row() {
          Text('🏆 我的成就 · ' + ACHIEVE_GOT.toString() + '/' + this.achieveList.length.toString()).fontSize(13)
            .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
          Column().layoutWeight(1)
          Text('全部 ›').fontSize(9).fontColor(COLORS.teal)
        }

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(this.achieveList, (item: HealthAchieve, idx: number) => {
            Column({ space: 6 }) {
              Text(item.icon).fontSize(28)
                .opacity(item.got ? 1 : 0.3)
              Text(item.name).fontSize(10).fontColor(item.got ? COLORS.title : COLORS.text3)
              Text(item.desc).fontSize(8).fontColor(COLORS.text3)
                .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .width('23%')
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.card)
            .borderRadius(10)
            .alignItems(HorizontalAlign.Center)
            .margin({ bottom: 8 })
          })
        }
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.chip)
      .borderRadius(12)
    }
  }

成就徽章区域使用Flex容器配合FlexWrap.Wrap实现自动换行的四宫格布局。每个徽章宽度23%,4个一行(23%×4=92%加上间距约8%)正好填满一行。已达成徽章的emoji和文字使用完整不透明度(1)和title色,未达成徽章使用0.3透明度和text3色------这种强烈的视觉对比让用户一眼就能区分已达成和未达成的成就。徽章描述使用maxLines(1)和Ellipsis省略号,确保长文本不会破坏布局。

月度步数图表卡片

chartCard是所有Tab页面底部通用的月度数据可视化组件,以纯ArkUI组件(非Canvas)实现柱状图效果。

chartCard 方法

typescript 复制代码
  /**
   * 月度步数柱状图卡片
   * 使用ArkUI原生组件实现柱状图(非Canvas)
   */
  @Builder
  chartCard() {
    Column({ space: 12 }) {
      // 标题行
      Row() {
        Text('📊 近 6 月步数').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
        Column().layoutWeight(1)
        Text('均 9,250 步').fontSize(11).fontColor(COLORS.teal).fontWeight(FontWeight.Bold)
      }

      // 柱状图区域
      Row({ space: 0 }) {
        ForEach(MONTH_IDX, (mi: number, idx: number) => {
          Column({ space: 4 }) {
            // 柱体
            Column()
              .width(16)
              .height(30 + STEP_VAL[idx] / STEP_MAX * 100)
              .backgroundColor(idx === 3 ? COLORS.teal : COLORS.tealL)
              .borderRadius(4)

            // 数值
            Text(STEP_VAL[idx].toString()).fontSize(7)
              .fontColor(idx === 3 ? COLORS.teal : COLORS.text3)

            // 月份
            Text(MONTH_NAME[idx]).fontSize(8).fontColor(COLORS.text3)
          }
          .layoutWeight(1)
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%')
      .height(110)
      .padding({ top: 8, bottom: 8 })
      .backgroundColor(COLORS.chip)
      .borderRadius(10)
      .alignItems(VerticalAlign.Bottom)
    }
    .width('100%')
    .padding(14)
    .backgroundColor(COLORS.card)
    .borderRadius(12)
    .margin({ top: 12 })
  }

chartCard使用ArkUI原生组件构建柱状图------每个柱体是一个Column,宽度16,高度通过30 + STEP_VAL[idx] / STEP_MAX * 100动态计算。基础高度30确保最低值也有可见的柱体,乘以100的比例系数将步数差异映射到像素差异。第4个月(idx 3)的柱体使用teal主色高亮,其他使用tealL深色------这种高亮策略让用户一眼就能识别出最佳月份。

柱状图容器使用alignItems(VerticalAlign.Bottom)让柱体从底部对齐,justifyContent(FlexAlign.End)让内容垂直靠底。每个柱体上方是数值文字,下方是月份标签。整体高度110包含了柱体高度(最大约124)和文字的空间。

底部Tab栏

底部Tab栏提供了全局导航能力,是应用交互的核心入口。

tabBar 方法

typescript 复制代码
  /**
   * 底部 Tab 导航栏
   * 6个Tab单排排列,选中态高亮
   */
  @Builder
  tabBar() {
    Row({ space: 0 }) {
      ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
        Column({ space: 2 }) {
          // 图标:选中时放大
          Text(tab.icon).fontSize(this.currentTab === idx ? 21 : 19)
            .opacity(this.currentTab === idx ? 1 : 0.6)
          // 标签:选中时变色加粗
          Text(tab.label).fontSize(9)
            .fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
            .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .onClick(() => {
          this.currentTab = idx;
        })
      })
    }
    .width('100%')
    .height(58)
    .backgroundColor(COLORS.card)
    .padding({ top: 6, bottom: 6 })
  }

tabBar通过ForEach遍历TAB_LIST数组生成6个Tab项。每个Tab使用layoutWeight(1)等分屏幕宽度。选中态通过三元运算符实现差异化:图标字号21vs19、不透明度1vs0.6、标签颜色tabOnvs text3、字重Boldvs Normal。这种多维度的视觉差异化确保了选中Tab的辨识度。点击Tab设置currentTab为对应索引,触发条件渲染切换Tab页面内容。

弹窗系统

弹窗系统通过Stack叠层和条件渲染实现,包含遮罩层和三个功能弹窗。

modalOverlay 通用遮罩

typescript 复制代码
  /**
   * 通用遮罩层
   * 半透明黑色背景,点击关闭弹窗
   * @param onClose - 关闭回调函数
   */
  @Builder
  modalOverlay(onClose: () => void) {
    Stack() {
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(COLORS.mask)
    }
    .width('100%')
    .height('100%')
    .onClick(() => {
      onClose();
    })
  }

modalOverlay是所有弹窗共享的遮罩层。使用mask色(55%透明黑色)覆盖全屏,点击触发onClose回调关闭弹窗。这种"点击外部关闭"的交互模式符合用户的直觉操作习惯。

panelAdd 报告详情弹窗

typescript 复制代码
  /**
   * 报告详情弹窗
   * 展示选中报告的完整信息
   * @param onClose - 关闭回调函数
   */
  @Builder
  panelAdd(onClose: () => void) {
    Stack() {
      // 遮罩层
      this.modalOverlay(onClose)

      // 弹窗内容
      Column({ space: 14 }) {
        Text('📋 报告详情').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        // 报告信息展示(条件渲染)
        if (this.editIdx >= 0 && this.editIdx < this.reportList.length) {
          Column({ space: 8 }) {
            Row() {
              Text(this.reportList[this.editIdx].date).fontSize(13)
                .fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Column().layoutWeight(1)
              Text(this.reportList[this.editIdx].status).fontSize(10)
                .fontColor(reportColor(this.reportList[this.editIdx].status))
            }
            Text(this.reportList[this.editIdx].title).fontSize(12).fontColor(COLORS.sub)
            Text(this.reportList[this.editIdx].summary).fontSize(11).fontColor(COLORS.text3)
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.chip)
          .borderRadius(10)
        }

        Text('如需详细解读,请咨询专业医生。').fontSize(11).fontColor(COLORS.sub).textAlign(TextAlign.Center)

        // 按钮行
        Row({ space: 10 }) {
          Text('关闭').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(COLORS.chip)
            .borderRadius(10)
            .onClick(() => { onClose(); })

          Text('查看原文').fontSize(12).fontColor(COLORS.bg)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(COLORS.teal)
            .borderRadius(10)
            .onClick(() => {
              this.editIdx = -1;
              onClose();
            })
        }
        .width('100%')
      }
      .width('80%')
      .padding(20)
      .backgroundColor(COLORS.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

panelAdd弹窗用于展示报告详情。使用Stack叠层将modalOverlay遮罩和内容Column组合在一起。内容区域宽度80%居中显示,使用card背景色和16圆角。报告信息通过条件渲染保护------只有当editIdx在有效范围内时才显示报告内容,避免了数组越界风险。底部按钮行提供"关闭"和"查看原文"两个操作,使用layoutWeight(1)等分宽度。"查看原文"按钮使用teal主色作为背景,fontColor设为bg色(深色背景)确保对比度。

panelEdit 目标设置弹窗

typescript 复制代码
  /**
   * 健康目标设置弹窗
   * 提供步数目标和心率监测频率的选择
   * @param onClose - 关闭回调函数
   */
  @Builder
  panelEdit(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)

      Column({ space: 14 }) {
        Text('🎯 设置健康目标').fontSize(16).fontColor(COLORS.title).fontWeight(FontWeight.Bold)

        // 步数目标选择
        Column({ space: 6 }) {
          Text('每日步数目标').fontSize(11).fontColor(COLORS.sub)
          Row({ space: 8 }) {
            Text('8000').fontSize(10).fontColor(COLORS.text3)
              .padding({ left: 10, right: 10, top: 8, bottom: 8 })
              .backgroundColor(COLORS.chip).borderRadius(8)
            Text('10000').fontSize(10).fontColor(COLORS.bg)
              .backgroundColor(COLORS.teal).borderRadius(8)
              .padding({ left: 10, right: 10, top: 8, bottom: 8 })
            Text('12000').fontSize(10).fontColor(COLORS.text3)
              .padding({ left: 10,  right: 10, top: 8, bottom: 8 })
              .backgroundColor(COLORS.chip).borderRadius(8)
          }
        }

        // 心率监测频率选择
        Column({ space: 6 }) {
          Text('每日心率监测频率').fontSize(11).fontColor(COLORS.sub)
          Row({ space: 8 }) {
            Text('4 次').fontSize(10).fontColor(COLORS.text3)
              .padding({ left: 10, right: 10, top: 8, bottom: 8 })
              .backgroundColor(COLORS.chip).borderRadius(8)
            Text('6 次').fontSize(10).fontColor(COLORS.bg)
              .backgroundColor(COLORS.teal).borderRadius(8)
              .padding({ left: 10, right: 10, top: 8, bottom: 8 })
            Text('8 次').fontSize(10).fontColor(COLORS.text3)
              .padding({ left: 10, right: 10, top: 8, bottom: 8 })
              .backgroundColor(COLORS.chip).borderRadius(8)
          }
        }

        // 按钮行
        Row({ space: 10 }) {
          Text('取消').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(COLORS.chip)
            .borderRadius(10)
            .onClick(() => { onClose(); })

          Text('保存目标').fontSize(12).fontColor(COLORS.bg)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(COLORS.teal)
            .borderRadius(10)
            .onClick(() => { onClose(); })
        }
        .width('100%')
      }
      .width('85%')
      .padding(20)
      .backgroundColor(COLORS.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

panelEdit弹窗提供了健康目标的设置界面。两组三选一的选择按钮------步数目标(8000/10000/12000)和心率监测频率(4次/6次/8次)。选中选项使用teal背景和bg深色文字,未选中使用chip背景和text3文字。这种"选中高亮"的视觉设计让用户一目了然当前选择。弹窗宽度85%比panelAdd的80%略宽,因为需要容纳三列选择按钮。

panelDel 删除确认弹窗

typescript 复制代码
  /**
   * 删除确认弹窗
   * 提供二次确认防止误删
   * @param onClose - 关闭回调函数
   */
  @Builder
  panelDel(onClose: () => void) {
    Stack() {
      this.modalOverlay(onClose)

      Column({ space: 14 }) {
        Text('⚠️ 删除记录').fontSize(16).fontColor(COLORS.red).fontWeight(FontWeight.Bold)

        Text('确定要删除这条健康记录吗?删除后无法恢复。').fontSize(11).fontColor(COLORS.sub).textAlign(TextAlign.Center)

        Row({ space: 10 }) {
          Text('再想想').fontSize(12).fontColor(COLORS.sub)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(COLORS.chip)
            .borderRadius(10)
            .onClick(() => { onClose(); })

          Text('确认删除').fontSize(12).fontColor(COLORS.card)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .backgroundColor(COLORS.red)
            .borderRadius(10)
            .onClick(() => {
              this.delIdx = -1;
              onClose();
            })
        }
        .width('100%')
      }
      .width('75%')
      .padding(20)
      .backgroundColor(COLORS.card)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Center)
  }

panelDel弹窗是最简洁的删除确认对话框。标题使用红色(COLORS.red)和警告emoji(⚠️)传达危险操作的视觉信号。确认删除按钮也使用红色背景,与标题色系一致。弹窗宽度75%是三个弹窗中最窄的,因为内容最简单------只有一行提示文字和两个按钮。
#mermaid-svg-gwxNzWWb9QKInGC1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gwxNzWWb9QKInGC1 .error-icon{fill:#552222;}#mermaid-svg-gwxNzWWb9QKInGC1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gwxNzWWb9QKInGC1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gwxNzWWb9QKInGC1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gwxNzWWb9QKInGC1 .marker.cross{stroke:#333333;}#mermaid-svg-gwxNzWWb9QKInGC1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gwxNzWWb9QKInGC1 p{margin:0;}#mermaid-svg-gwxNzWWb9QKInGC1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 .cluster-label text{fill:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 .cluster-label span{color:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 .cluster-label span p{background-color:transparent;}#mermaid-svg-gwxNzWWb9QKInGC1 .label text,#mermaid-svg-gwxNzWWb9QKInGC1 span{fill:#333;color:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 .node rect,#mermaid-svg-gwxNzWWb9QKInGC1 .node circle,#mermaid-svg-gwxNzWWb9QKInGC1 .node ellipse,#mermaid-svg-gwxNzWWb9QKInGC1 .node polygon,#mermaid-svg-gwxNzWWb9QKInGC1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gwxNzWWb9QKInGC1 .rough-node .label text,#mermaid-svg-gwxNzWWb9QKInGC1 .node .label text,#mermaid-svg-gwxNzWWb9QKInGC1 .image-shape .label,#mermaid-svg-gwxNzWWb9QKInGC1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-gwxNzWWb9QKInGC1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gwxNzWWb9QKInGC1 .rough-node .label,#mermaid-svg-gwxNzWWb9QKInGC1 .node .label,#mermaid-svg-gwxNzWWb9QKInGC1 .image-shape .label,#mermaid-svg-gwxNzWWb9QKInGC1 .icon-shape .label{text-align:center;}#mermaid-svg-gwxNzWWb9QKInGC1 .node.clickable{cursor:pointer;}#mermaid-svg-gwxNzWWb9QKInGC1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gwxNzWWb9QKInGC1 .arrowheadPath{fill:#333333;}#mermaid-svg-gwxNzWWb9QKInGC1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gwxNzWWb9QKInGC1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gwxNzWWb9QKInGC1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gwxNzWWb9QKInGC1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gwxNzWWb9QKInGC1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gwxNzWWb9QKInGC1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gwxNzWWb9QKInGC1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gwxNzWWb9QKInGC1 .cluster text{fill:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 .cluster span{color:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gwxNzWWb9QKInGC1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gwxNzWWb9QKInGC1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-gwxNzWWb9QKInGC1 .icon-shape,#mermaid-svg-gwxNzWWb9QKInGC1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gwxNzWWb9QKInGC1 .icon-shape p,#mermaid-svg-gwxNzWWb9QKInGC1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gwxNzWWb9QKInGC1 .icon-shape .label rect,#mermaid-svg-gwxNzWWb9QKInGC1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gwxNzWWb9QKInGC1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gwxNzWWb9QKInGC1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gwxNzWWb9QKInGC1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 弹窗系统
modalOverlay 遮罩层
panelAdd 报告详情
panelEdit 目标设置
panelDel 删除确认
半透明黑色背景
点击关闭
报告信息展示
关闭/查看原文按钮
宽度80%
步数目标选择
心率频率选择
取消/保存按钮
宽度85%
删除提示文字
再想想/确认删除按钮
宽度75%

三个弹窗的宽度呈递减设计(80%-85%-75%),这与内容复杂度并不完全对应------panelEdit最宽(85%)是因为需要容纳三列选择按钮,panelDel最窄(75%)是因为内容最简单。所有弹窗都使用相同的Stack叠层结构和alignContent(Alignment.Center)居中策略。

Canvas绘制方法

Canvas绘制方法是健康监测平台的核心技术模块,四种图表类型展示了ArkUI Canvas 2D API的完整能力。

drawRing 进度环绘制

typescript 复制代码
  /**
   * 绘制进度环
   * 包含:背景环 + 进度弧 + 中心文字 + 抗锯齿状态标注
   */
  drawRing() {
    const ctx = this.ringCtx;
    // 同步抗锯齿状态(HarmonyOS 6.1.1 特性)
    ctx.antialias = this.antialiasOn;
    const cx = 100;
    const cy = 100;
    const r = 70;
    // 呼吸动画:进度弧长度随breath微动
    const breathVal = this.breath ? 1.0 : 0.92;

    // 清空画布
    ctx.clearRect(0, 0, 200, 200);

    // 绘制背景环(灰色底环)
    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2);
    ctx.strokeStyle = COLORS.chip;
    ctx.lineWidth = 12;
    ctx.stroke();

    // 绘制进度弧(从12点方向顺时针)
    ctx.beginPath();
    ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * RING_PROGRESS * breathVal);
    ctx.strokeStyle = COLORS.teal;
    ctx.lineWidth = 12;
    ctx.lineCap = 'round';
    ctx.stroke();

    // 绘制中心文字
    ctx.fillStyle = COLORS.title;
    ctx.font = 'bold 22px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(Math.round(RING_PROGRESS * 100).toString() + '%', cx, cy);
    ctx.font = '10px sans-serif';
    ctx.fillStyle = COLORS.text3;
    ctx.fillText('今日目标', cx, cy + 18);

    // HarmonyOS 6.1.1 antialias 状态标注
    ctx.font = '7px sans-serif';
    ctx.fillStyle = this.antialiasOn ? COLORS.green : COLORS.orange;
    ctx.fillText(this.antialiasOn ? 'AA: ON' : 'AA: OFF', cx, cy + 34);
  }

drawRing方法绘制了一个完整的进度环图表。首先同步antialias状态,然后定义圆心坐标(cx=100, cy=100)和半径(r=70)。breathVal变量将breath布尔值转换为0.92-1.0的缩放系数,让进度弧的长度产生8%的呼吸脉动。

绘制过程分为四步:背景环使用chip色绘制完整圆环;进度弧从-PI/2(12点方向)开始,顺时针绘制RING_PROGRESS(0.72)乘以breathVal对应的角度,使用teal色和圆头线帽(round);中心文字显示百分比和"今日目标"标签;底部显示抗锯齿状态标注"AA: ON"或"AA: OFF"。
#mermaid-svg-yRJWlO4hQfcMWyvS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yRJWlO4hQfcMWyvS .error-icon{fill:#552222;}#mermaid-svg-yRJWlO4hQfcMWyvS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yRJWlO4hQfcMWyvS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yRJWlO4hQfcMWyvS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yRJWlO4hQfcMWyvS .marker.cross{stroke:#333333;}#mermaid-svg-yRJWlO4hQfcMWyvS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yRJWlO4hQfcMWyvS p{margin:0;}#mermaid-svg-yRJWlO4hQfcMWyvS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS .cluster-label text{fill:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS .cluster-label span{color:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS .cluster-label span p{background-color:transparent;}#mermaid-svg-yRJWlO4hQfcMWyvS .label text,#mermaid-svg-yRJWlO4hQfcMWyvS span{fill:#333;color:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS .node rect,#mermaid-svg-yRJWlO4hQfcMWyvS .node circle,#mermaid-svg-yRJWlO4hQfcMWyvS .node ellipse,#mermaid-svg-yRJWlO4hQfcMWyvS .node polygon,#mermaid-svg-yRJWlO4hQfcMWyvS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yRJWlO4hQfcMWyvS .rough-node .label text,#mermaid-svg-yRJWlO4hQfcMWyvS .node .label text,#mermaid-svg-yRJWlO4hQfcMWyvS .image-shape .label,#mermaid-svg-yRJWlO4hQfcMWyvS .icon-shape .label{text-anchor:middle;}#mermaid-svg-yRJWlO4hQfcMWyvS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yRJWlO4hQfcMWyvS .rough-node .label,#mermaid-svg-yRJWlO4hQfcMWyvS .node .label,#mermaid-svg-yRJWlO4hQfcMWyvS .image-shape .label,#mermaid-svg-yRJWlO4hQfcMWyvS .icon-shape .label{text-align:center;}#mermaid-svg-yRJWlO4hQfcMWyvS .node.clickable{cursor:pointer;}#mermaid-svg-yRJWlO4hQfcMWyvS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yRJWlO4hQfcMWyvS .arrowheadPath{fill:#333333;}#mermaid-svg-yRJWlO4hQfcMWyvS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yRJWlO4hQfcMWyvS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yRJWlO4hQfcMWyvS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yRJWlO4hQfcMWyvS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yRJWlO4hQfcMWyvS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yRJWlO4hQfcMWyvS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yRJWlO4hQfcMWyvS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yRJWlO4hQfcMWyvS .cluster text{fill:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS .cluster span{color:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yRJWlO4hQfcMWyvS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yRJWlO4hQfcMWyvS rect.text{fill:none;stroke-width:0;}#mermaid-svg-yRJWlO4hQfcMWyvS .icon-shape,#mermaid-svg-yRJWlO4hQfcMWyvS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yRJWlO4hQfcMWyvS .icon-shape p,#mermaid-svg-yRJWlO4hQfcMWyvS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yRJWlO4hQfcMWyvS .icon-shape .label rect,#mermaid-svg-yRJWlO4hQfcMWyvS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yRJWlO4hQfcMWyvS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yRJWlO4hQfcMWyvS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yRJWlO4hQfcMWyvS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} drawRing
同步antialias状态
定义圆心和半径
计算breathVal呼吸系数
clearRect清空画布
绘制背景环 - chip色完整圆
绘制进度弧 - teal色部分弧
绘制中心百分比文字
绘制今日目标标签
绘制AA状态标注

drawLine 折线图绘制

typescript 复制代码
  /**
   * 绘制24小时心率折线图
   * 包含:背景网格 + X轴标签 + 渐变填充 + 折线 + 数据点
   */
  drawLine() {
    const ctx = this.lineCtx;
    ctx.antialias = this.antialiasOn;
    const data = HR_DATA;
    const w = 300;
    const h = 180;
    const pad = 30;
    const max = HR_MAX;
    const stepX = (w - pad * 2) / (data.length - 1);

    // 清空画布
    ctx.clearRect(0, 0, w + 20, h + 20);

    // 绘制背景网格(水平线)
    ctx.strokeStyle = COLORS.line;
    ctx.lineWidth = 1;
    for (let i = 0; i <= 3; i++) {
      const y = pad + (h - pad * 2) * i / 3;
      ctx.beginPath();
      ctx.moveTo(pad, y);
      ctx.lineTo(w - pad, y);
      ctx.stroke();
    }

    // 绘制X轴标签
    ctx.font = '8px sans-serif';
    ctx.fillStyle = COLORS.text3;
    ctx.textAlign = 'center';
    for (let i = 0; i < HR_LABELS.length; i++) {
      const x = pad + i * (w - pad * 2) / (HR_LABELS.length - 1);
      ctx.fillText(HR_LABELS[i], x, h - 8);
    }

    // 绘制渐变填充区域
    const grad = ctx.createLinearGradient(0, pad, 0, h - pad);
    grad.addColorStop(0, COLORS.teal);
    grad.addColorStop(1, 'rgba(45,212,191,0.05)');

    ctx.beginPath();
    ctx.moveTo(pad, h - pad);
    for (let i = 0; i < data.length; i++) {
      const x = pad + i * stepX;
      const y = h - pad - (data[i] / max) * (h - pad * 2);
      ctx.lineTo(x, y);
    }
    ctx.lineTo(w - pad, h - pad);
    ctx.closePath();
    ctx.fillStyle = grad;
    ctx.fill();

    // 绘制折线
    ctx.beginPath();
    for (let i = 0; i < data.length; i++) {
      const x = pad + i * stepX;
      const y = h - pad - (data[i] / max) * (h - pad * 2);
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.strokeStyle = COLORS.teal;
    ctx.lineWidth = 2;
    ctx.stroke();

    // 绘制数据点
    for (let i = 0; i < data.length; i++) {
      const x = pad + i * stepX;
      const y = h - pad - (data[i] / max) * (h - pad * 2);
      ctx.beginPath();
      ctx.arc(x, y, 3, 0, Math.PI * 2);
      ctx.fillStyle = COLORS.card;
      ctx.fill();
      ctx.strokeStyle = COLORS.teal;
      ctx.lineWidth = 1.5;
      ctx.stroke();
    }
  }

drawLine方法绘制了24小时心率趋势折线图。数据处理的核心公式是y = h - pad - (data[i] / max) * (h - pad * 2)------将心率值映射到Canvas的Y坐标。由于Canvas的Y轴向下递增,需要用h减去映射值来反转坐标系,让高心率显示在上方。

绘制分为五个步骤:4条水平背景网格线提供参考刻度;X轴标签显示时间点(00/06/12/18/24);渐变填充区域从teal色到几乎透明的teal色,为折线下方区域添加视觉重量;折线本身使用teal色2px线宽绘制;每个数据点绘制为card色填充+teal色描边的圆形,形成"空心圆"效果。

drawPie 饼图绘制

typescript 复制代码
  /**
   * 绘制睡眠阶段饼图
   * 包含:扇形填充 + 中心镂空 + 中心文字
   */
  drawPie() {
    const ctx = this.pieCtx;
    ctx.antialias = this.antialiasOn;
    const cx = 110;
    const cy = 100;
    const r = 75;
    let start = -Math.PI / 2;

    // 清空画布
    ctx.clearRect(0, 0, 220, 200);

    // 绘制扇形
    for (let i = 0; i < PIE_DATA.length; i++) {
      const d = PIE_DATA[i];
      const angle = (d.val / 100) * Math.PI * 2;
      const color = sleepColor(d.label);

      ctx.beginPath();
      ctx.moveTo(cx, cy);
      ctx.arc(cx, cy, r, start, start + angle);
      ctx.fillStyle = color;
      ctx.fill();
      start += angle;
    }

    // 中心镂空(制作环形饼图效果)
    ctx.beginPath();
    ctx.arc(cx, cy, r * 0.5, 0, Math.PI * 2);
    ctx.fillStyle = COLORS.card;
    ctx.fill();

    // 中心文字
    ctx.fillStyle = COLORS.title;
    ctx.font = 'bold 16px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('7h20m', cx, cy);
    ctx.font = '9px sans-serif';
    ctx.fillStyle = COLORS.text3;
    ctx.fillText('总睡眠', cx, cy + 16);
  }

drawPie方法绘制了环形饼图。每个扇形从start角度开始,绘制(d.val / 100) * Math.PI * 2的角度范围。颜色通过sleepColor函数映射,确保与列表中的色条颜色一致。扇形绘制使用moveTo圆心+arc弧线+fill填充的经典模式。

中心镂空通过在圆心绘制一个半径为r*0.5(37.5)的card色圆形实现,将饼图变为环形图。镂空区域内绘制总睡眠时长"7h20m"和标签"总睡眠"。这种环形饼图设计既保留了饼图的占比展示功能,又增加了中心信息展示空间。

drawRadar 雷达图绘制

typescript 复制代码
  /**
   * 绘制体征雷达图
   * 包含:背景多边形 + 轴线 + 数据多边形 + 数据点 + 标签
   */
  drawRadar() {
    const ctx = this.radarCtx;
    ctx.antialias = this.antialiasOn;
    const cx = 110;
    const cy = 100;
    const r = 75;
    const labels = RADAR_LABELS;
    const values = RADAR_VALUES;
    const n = labels.length;

    // 清空画布
    ctx.clearRect(0, 0, 220, 200);

    // 绘制背景多边形(3层)
    for (let layer = 1; layer <= 3; layer++) {
      const lr = r * layer / 3;
      ctx.beginPath();
      for (let i = 0; i < n; i++) {
        const angle = -Math.PI / 2 + (i / n) * Math.PI * 2;
        const x = cx + Math.cos(angle) * lr;
        const y = cy + Math.sin(angle) * lr;
        if (i === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      }
      ctx.closePath();
      ctx.strokeStyle = COLORS.line;
      ctx.lineWidth = 1;
      ctx.stroke();
    }

    // 绘制轴线(从中心到顶点)
    for (let i = 0; i < n; i++) {
      const angle = -Math.PI / 2 + (i / n) * Math.PI * 2;
      ctx.beginPath();
      ctx.moveTo(cx, cy);
      ctx.lineTo(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
      ctx.strokeStyle = COLORS.line;
      ctx.stroke();
    }

    // 绘制数据多边形
    ctx.beginPath();
    for (let i = 0; i < n; i++) {
      const angle = -Math.PI / 2 + (i / n) * Math.PI * 2;
      const val = values[i];
      const x = cx + Math.cos(angle) * r * val;
      const y = cy + Math.sin(angle) * r * val;
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.closePath();
    ctx.fillStyle = COLORS.teal;
    ctx.globalAlpha = 0.3;
    ctx.fill();
    ctx.globalAlpha = 1;
    ctx.strokeStyle = COLORS.teal;
    ctx.lineWidth = 2;
    ctx.stroke();

    // 绘制数据点
    for (let i = 0; i < n; i++) {
      const angle = -Math.PI / 2 + (i / n) * Math.PI * 2;
      const val = values[i];
      const x = cx + Math.cos(angle) * r * val;
      const y = cy + Math.sin(angle) * r * val;
      ctx.beginPath();
      ctx.arc(x, y, 4, 0, Math.PI * 2);
      ctx.fillStyle = COLORS.teal;
      ctx.fill();
    }

    // 绘制标签
    ctx.font = '9px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillStyle = COLORS.sub;
    for (let i = 0; i < n; i++) {
      const angle = -Math.PI / 2 + (i / n) * Math.PI * 2;
      const x = cx + Math.cos(angle) * (r + 14);
      const y = cy + Math.sin(angle) * (r + 14) + 3;
      ctx.fillText(labels[i], x, y);
    }
  }

drawRadar方法绘制了六维体征雷达图。角度计算公式-Math.PI / 2 + (i / n) * Math.PI * 2确保第一个顶点在12点方向(-PI/2),后续顶点顺时针排列。坐标计算使用cx + Math.cos(angle) * lrcy + Math.sin(angle) * lr,其中lr是实际半径(背景多边形使用layer比例半径,数据多边形使用r*val)。

绘制分为五步:3层背景多边形(半径的1/3、2/3、3/3)提供刻度参考;6条轴线从中心到顶点提供维度方向;数据多边形使用teal色+0.3透明度填充+2px线宽描边;6个数据点使用teal色实心圆;6个标签绘制在半径r+14的位置(外侧14px)。
#mermaid-svg-UxtLJjyC5wkkBryF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UxtLJjyC5wkkBryF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UxtLJjyC5wkkBryF .error-icon{fill:#552222;}#mermaid-svg-UxtLJjyC5wkkBryF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UxtLJjyC5wkkBryF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UxtLJjyC5wkkBryF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UxtLJjyC5wkkBryF .marker.cross{stroke:#333333;}#mermaid-svg-UxtLJjyC5wkkBryF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UxtLJjyC5wkkBryF p{margin:0;}#mermaid-svg-UxtLJjyC5wkkBryF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UxtLJjyC5wkkBryF .cluster-label text{fill:#333;}#mermaid-svg-UxtLJjyC5wkkBryF .cluster-label span{color:#333;}#mermaid-svg-UxtLJjyC5wkkBryF .cluster-label span p{background-color:transparent;}#mermaid-svg-UxtLJjyC5wkkBryF .label text,#mermaid-svg-UxtLJjyC5wkkBryF span{fill:#333;color:#333;}#mermaid-svg-UxtLJjyC5wkkBryF .node rect,#mermaid-svg-UxtLJjyC5wkkBryF .node circle,#mermaid-svg-UxtLJjyC5wkkBryF .node ellipse,#mermaid-svg-UxtLJjyC5wkkBryF .node polygon,#mermaid-svg-UxtLJjyC5wkkBryF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UxtLJjyC5wkkBryF .rough-node .label text,#mermaid-svg-UxtLJjyC5wkkBryF .node .label text,#mermaid-svg-UxtLJjyC5wkkBryF .image-shape .label,#mermaid-svg-UxtLJjyC5wkkBryF .icon-shape .label{text-anchor:middle;}#mermaid-svg-UxtLJjyC5wkkBryF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UxtLJjyC5wkkBryF .rough-node .label,#mermaid-svg-UxtLJjyC5wkkBryF .node .label,#mermaid-svg-UxtLJjyC5wkkBryF .image-shape .label,#mermaid-svg-UxtLJjyC5wkkBryF .icon-shape .label{text-align:center;}#mermaid-svg-UxtLJjyC5wkkBryF .node.clickable{cursor:pointer;}#mermaid-svg-UxtLJjyC5wkkBryF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UxtLJjyC5wkkBryF .arrowheadPath{fill:#333333;}#mermaid-svg-UxtLJjyC5wkkBryF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UxtLJjyC5wkkBryF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UxtLJjyC5wkkBryF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UxtLJjyC5wkkBryF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UxtLJjyC5wkkBryF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UxtLJjyC5wkkBryF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UxtLJjyC5wkkBryF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UxtLJjyC5wkkBryF .cluster text{fill:#333;}#mermaid-svg-UxtLJjyC5wkkBryF .cluster span{color:#333;}#mermaid-svg-UxtLJjyC5wkkBryF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UxtLJjyC5wkkBryF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UxtLJjyC5wkkBryF rect.text{fill:none;stroke-width:0;}#mermaid-svg-UxtLJjyC5wkkBryF .icon-shape,#mermaid-svg-UxtLJjyC5wkkBryF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UxtLJjyC5wkkBryF .icon-shape p,#mermaid-svg-UxtLJjyC5wkkBryF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UxtLJjyC5wkkBryF .icon-shape .label rect,#mermaid-svg-UxtLJjyC5wkkBryF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UxtLJjyC5wkkBryF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UxtLJjyC5wkkBryF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UxtLJjyC5wkkBryF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Canvas绘制方法
drawRing 进度环
drawLine 折线图
drawPie 饼图
drawRadar 雷达图
背景环
进度弧
中心文字
背景网格
X轴标签
渐变填充
折线
数据点
扇形填充
中心镂空
中心文字
背景多边形3层
轴线
数据多边形
数据点
标签

架构总结

健康监测平台采用了分层架构设计,从颜色系统到数据模型到组件构建器,每一层都有清晰的职责边界。下面是整体架构的Mermaid图。
渲染错误: Mermaid 渲染失败: Parse error on line 31: ...bgraph 组件主体 EN[@Entry @Component ----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'subgraph', 'end', 'acc_title', 'acc_descr', 'acc_descr_multiline_value', 'AMP', 'COLON', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', 'direction_tb', 'direction_bt', 'direction_rl', 'direction_lr', 'direction_td', got 'LINK_ID'

功能模块对比表

模块名称 核心功能 数据模型 Canvas图表 布局特点 交互方式
概览Tab 今日健康汇总 MetricItem 进度环 数据大卡+指标列表 点击查看指标详情
心率Tab 24h心率趋势 HR_DATA常量 折线图 Canvas+图例+关键数据 查看详情入口
睡眠Tab 睡眠阶段分析 SleepRecord 饼图 Canvas+图例+时段列表 点击查看详情
体型Tab 体征综合评估 RADAR常量 雷达图 Canvas+指标进度条 进度条展示
报告Tab 健康报告列表 ReportItem 周历导航+报告卡片 点击打开详情弹窗
我的Tab 个人成就统计 HealthAchieve 渐变大卡+四宫格 成就查看
月度图表 近6月步数趋势 STEP_VAL常量 无(原生组件) 柱状图 纯展示
头部面板 核心指标概览 QUICK_TAGS 三段式五层布局 搜索+快捷入口
Tab导航 全局页面切换 TAB_LIST 单排6Tab 点击切换
报告弹窗 报告详情展示 ReportItem 80%宽度居中 关闭+查看原文
目标弹窗 健康目标设置 85%宽度三选按钮 取消+保存
删除弹窗 删除确认 75%宽度双按钮 再想想+确认删除

技术亮点对比表

技术亮点 实现方式 应用场景 工程价值 创新程度
@Observed数据模型 类装饰器+@State绑定 睡眠/指标/报告/成就 数据变化自动触发UI刷新
Canvas 4图表类型 CanvasRenderingContext2D 进度环/折线/饼/雷达 一套API覆盖4种可视化
呼吸动画联动 setInterval+breath状态 头部图标+进度环+大卡 声明式状态驱动命令式绘图
运行时抗锯齿开关 ctx.antialias属性 概览Tab抗锯齿切换 视觉舒适/医学精准双模式 极高
条件性Canvas重绘 currentTab判断 aboutToAppear定时器 避免不可见图表重绘
语义化颜色系统 ColorPalette接口 全局颜色统一管理 类型安全+语义清晰
红黄绿三色映射 辅助函数hrColor等 心率/睡眠/报告状态 数据到视觉的标准化桥梁
弹窗回调函数模式 onClose参数传递 三个弹窗统一模式 复用性强+逻辑清晰
进度条双层结构 槽+填充Column嵌套 打卡进度+体型指标 原生组件实现进度条
柱状图原生实现 Column高度动态计算 月度步数趋势 非Canvas实现数据可视化
Flex换行四宫格 FlexWrap.Wrap+SpaceBetween 成就徽章布局 自动适配不同数量
渐变大卡 linearGradient+135度 我的页统计大卡 视觉焦点+层次提升

深度总结

康脉健康监测平台是一个以HarmonyOS ArkUI声明式UI框架为基础,面向数字健康领域的综合性移动应用。通过对该平台源代码的深度解析,我们可以从多个维度总结其技术特征、工程价值和设计理念。

从架构层面看,该平台采用了经典的"颜色系统-常量定义-辅助函数-数据模型-组件主体-构建器群-弹窗系统-Canvas绘制"八层分层架构。每一层都有明确的职责边界------颜色系统提供视觉统一性,常量定义提供静态数据骨架,辅助函数提供数据到视觉的映射逻辑,数据模型提供可观察的数据结构,组件主体管理状态和生命周期,构建器群封装UI片段,弹窗系统处理模态交互,Canvas绘制实现数据可视化。这种分层设计让代码结构清晰可读,每一层都可以独立理解和修改,不会产生跨层的耦合问题。在实际的团队协作开发中,不同的开发者可以负责不同的层级,并行推进开发进度。

从状态管理层面看,@State、@Observed和@Builder三个核心装饰器构成了ArkUI的状态管理三角。@State管理组件内部状态------currentTab控制Tab切换,breath驱动呼吸动画,addModal/editModal/delModal控制弹窗显隐,sleepList/metricList/reportList/achieveList承载数据列表。@Observed管理数据模型类的可观察性------SleepRecord、MetricItem、ReportItem、HealthAchieve四个类的属性变化可以被框架自动感知。@Builder管理UI片段的复用------六个Tab页面构建器、三个弹窗构建器、一个头部构建器、一个图表构建器、一个Tab栏构建器,共十二个@Builder方法将复杂的UI拆分为可管理的独立单元。

从数据可视化层面看,四种Canvas图表类型展示了ArkUI Canvas 2D API的完整能力图谱。进度环展示了arc方法的圆弧绘制能力------通过控制起始角度和结束角度绘制部分弧线,配合lineCap='round'实现圆头线帽效果。折线图展示了moveTo/lineTo的路径绘制能力------通过循环遍历数据数组,计算每个数据点的x/y坐标,用lineTo连接成连续折线,配合createLinearGradient实现渐变填充。饼图展示了arc方法的扇形绘制能力------通过moveTo圆心+arc弧线+fill填充的组合,绘制以圆心为起点的扇形区域,配合中心镂空圆形实现环形效果。雷达图展示了三角函数在多边形绘制中的应用------通过cos和sin计算每个顶点的坐标,配合closePath闭合路径,绘制正多边形背景和数据多边形。

从性能优化层面看,该平台采用了多种性能优化策略。条件性Canvas重绘策略在定时器回调中通过currentTab判断避免不可见图表的重绘------当用户切换到心率Tab时,进度环不可见,此时调用drawRing是纯粹的性能浪费,通过if判断跳过重绘节省了CPU资源。Canvas上下文私有化策略将四个绘图上下文声明为private而非@State------因为绘图上下文本身不需要触发UI刷新,Canvas的刷新是通过手动调用绘图方法触发的,使用private避免了不必要的状态追踪开销。滚动条隐藏策略将scrollBar设为BarState.Off------隐藏滚动条不仅提升了视觉简洁性,也减少了滚动条的渲染开销。

从交互设计层面看,该平台遵循了"视觉反馈即时、操作路径简短、信息层次清晰"三大原则。视觉反馈即时体现在Tab切换的图标放大/颜色变化/字重加粗三重反馈、呼吸动画的opacity切换、拨动开关的padding动态调整等细节中------每一个用户操作都有即时的视觉响应。操作路径简短体现在快捷入口宫格的6个直达入口、底部Tab栏的6个导航入口、头部的搜索栏------用户最多两次点击就能到达任何功能页面。信息层次清晰体现在标题/副标题/三级文字的三级文字色系、card/chip/bg的三级背景色系、teal主色/功能辅助色的二组色系------通过颜色的明度和饱和度差异建立了清晰的信息优先级。

从颜色工程层面看,ColorPalette接口和COLORS常量的设计体现了"语义化命名+类型安全"的颜色管理理念。24个颜色字段覆盖了背景层(bg/card/chip)、文字层(title/sub/text3)、主色调(teal系列)、功能色(green/orange/red/purple/blue/gold)和辅助层(line/tabOn/mask)的完整色彩体系。每个颜色都有明确的语义注释------bg代表全局背景、card代表卡片背景、title代表主标题,开发者在使用时不需要记忆十六进制值,只需要通过COLORS.语义名引用即可。接口的类型约束确保了所有字段都被定义,避免了遗漏颜色值导致的运行时错误。

从健康场景层面看,该平台深入结合了数字健康监测的实际需求。心率区间颜色映射基于医学标准------低于60bpm的心动过缓、60-100的正常范围、100-120的偏高、超过120的过高,每个区间都有对应的语义颜色。睡眠阶段颜色映射基于睡眠科学------深睡/浅睡/REM/清醒四个阶段对应不同的生理状态,紫色代表深度恢复、蓝色代表轻度休息、绿色代表梦境阶段、橙色代表中断状态。报告状态颜色映射基于临床实践------正常/关注/异常三级评估体系是医疗报告的通用标准。这些颜色映射不是随意设计的,而是基于医学知识和临床经验的合理映射。

从HarmonyOS新特性层面看,运行时Canvas抗锯齿开关是HarmonyOS 6.1.1引入的重要特性。传统的Canvas API中,抗锯齿在创建上下文时通过RenderingContextSettings设定,一旦创建就无法更改。HarmonyOS 6.1.1突破了这一限制,允许通过ctx.antialias属性在运行时动态切换。这个特性在医疗场景中有实际意义------"视觉舒适模式"(抗锯齿开启)适合日常浏览,文字边缘平滑减轻视觉疲劳;"医学精准模式"(抗锯齿关闭)适合医生判读,像素级锐利有助于精确识别数据。该平台在概览Tab中实现了这个特性的完整UI------拨动开关、模式名称切换、描述文字更新、Canvas重绘、AA状态标注,形成了一个完整的技术演示。

从代码质量层面看,该平台的源代码体现了良好的工程实践。函数和类都有详细的JSDoc注释,说明参数含义和返回值。常量命名使用大写+下划线(UPPER_SNAKE_CASE)的命名规范。类名使用大驼峰(PascalCase)命名。辅助函数名使用小驼峰(camelCase)命名。颜色字段使用语义化命名而非视觉描述命名------teal而非#2DD4BF,green而非#4ADE80。这些命名规范和注释习惯让代码具有很高的可读性和可维护性。

从扩展性层面看,该平台的架构设计为未来扩展预留了充足空间。新增Tab只需要在TAB_LIST数组中添加一项、新增一个@Builder方法、在build方法的if-else链中添加一个分支。新增数据模型只需要定义一个@Observed类、创建预置数据数组、在组件中声明@State变量。新增Canvas图表类型只需要创建一个CanvasRenderingContext2D实例、实现一个draw方法、在对应Tab的@Builder中添加Canvas组件。新增弹窗只需要定义一个@State布尔变量、实现一个@Builder方法、在build方法的Stack叠层中添加一个条件渲染。这种"数据驱动+组件化"的架构让功能扩展变得简单而安全。

综上所述,康脉健康监测平台不仅是一个功能完整的数字健康应用,更是一个展示HarmonyOS ArkUI声明式UI框架核心能力的优秀技术案例。它涵盖了状态管理、组件化开发、Canvas绘图、生命周期管理、颜色工程、数据可视化、交互设计、性能优化等多个技术维度,为HarmonyOS生态中的健康类应用开发提供了有价值的参考模板。其分层架构设计、语义化颜色系统、@Observed数据模型、Canvas四图表实现、运行时抗锯齿特性等技术亮点,代表了当前HarmonyOS应用开发的最佳实践水平。

附录:DevEco Studio 创建新项目与查看 SDK 版本

本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。


一、创建新项目

1.1 进入欢迎界面

启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 "项目",右侧提供三个主要入口:

  • 新建项目:从头创建新项目
  • 打开项目:打开本地已有项目
  • 克隆仓库:从 Git 等版本控制拉取代码

点击 "新建项目" 按钮,进入项目创建向导。

1.2 选择项目模板

在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:

类型 说明
应用(Application) 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期
元服务(Atomic Service) 开发轻量级的原子化服务,无需安装即可使用

选择 "应用" 标签后,右侧展示多种模板。对于大多数场景,推荐选择 "Empty Ability" ------ 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

1.3 配置项目信息

点击 "下一步" 后,进入项目配置界面,需要填写以下核心参数:

配置项 示例值 说明
项目名称(Project name) rollboat 应用的项目名称,建议使用英文命名
包名(Bundle name) com.rollboat.myapplication 应用唯一标识,采用反向域名格式
保存路径(Save location) D:\CodeFactory\rollboat 项目本地存储路径,避免使用中文和空格
兼容 SDK(Compatible SDK) 6.1.1(24) 目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异
模块名称(Module name) entry 主模块名称,默认 entry 为应用入口模块
设备类型(Device types) ☑ Phone 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV

右侧预览区会实时展示当前模板的默认效果 ------ 一个居中显示的 "Hello World" 文本。

1.4 完成创建

确认配置无误后,点击右下角 "完成" 按钮,IDE 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 Hvigor 构建初始化(Build Init

构建日志中显示 "退出代码为 0" 表示项目初始化成功。

1.5 项目结构概览

创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:

复制代码
rollboat/
├── .hvigor/                   # Hvigor 构建工具缓存
├── .idea/                     # IDE 配置文件
├── AppScope/                  # 应用级全局配置
│   └── app.json5
├── entry/                     # 主模块(入口模块)
│   ├── src/main/ets/
│   │   ├── entryability/      # Ability 生命周期管理
│   │   │   └── EntryAbility.ets
│   │   └── pages/             # UI 页面
│   │       └── Index.ets      # 首页(默认 Hello World)
│   ├── src/main/resources/    # 资源文件
│   ├── module.json5           # 模块配置
│   └── build-profile.json5    # 构建配置
├── oh_modules/                # OHPM 依赖包
├── build-profile.json5        # 工程构建配置
├── hvigorfile.ts              # Hvigor 构建脚本
└── oh-package.json5           # 包管理配置

核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:

typescript 复制代码
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
关键语法 作用
@Entry 标记为页面入口,可用于路由跳转
@Component 声明为自定义组件
@State 状态变量,数据变更时自动触发 UI 刷新
RelativeContainer 相对布局容器,替代传统线性布局
.onClick() 点击事件,此处点击后文本变为 "Welcome"

打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:

文件 → 设置 → HarmonyOS SDK (或快捷键 Ctrl + Alt + S 搜索 "HarmonyOS SDK")

在设置面板中,可以看到当前已安装的 SDK 版本信息:

名称 阶段 状态
HarmonyOS 6.1.1 Release ✅ 已安装

界面顶部提示:"HarmonyOS SDK 已经包含在 IDE,无需单独安装",省去了手动配置 SDK 的繁琐步骤。

2.2 查看 ArkUI-X SDK(跨平台扩展)

如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:

文件 → 设置 → 语言和框架 → ArkUI-X

在这里可以查看已安装和可选的 ArkUI-X SDK 版本:

版本 SDK 版本号 阶段 状态
API Version 24 6.1.1.100 Release ✅ 已安装
API Version 23 6.1.0.28 Beta1 未安装
API Version 22 6.0.2.112 Release 未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。


三、小结

步骤 操作 关键点
创建项目 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 使用 Stage 模型 + ArkTS 语言
查看 SDK 设置 → HarmonyOS SDK SDK 已内置,无需手动安装
跨平台扩展 设置 → ArkUI-X 根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

相关推荐
小雨青年2 小时前
【HarmonyOS 7 悬浮页签深度实战】04 HdsTabsController 如何协调页签切换与显隐
华为·harmonyos
贾伟康2 小时前
【中国方言题库|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击
自动化测试·harmonyos·arkts·回归测试·hypium
RisunJan2 小时前
鸿蒙(HarmonyOS NEXT)开发小白入门学习计划表
学习·华为·harmonyos
fb_123452 小时前
华为存储技术基础精讲|DAS_NAS_SAN_RAID_协议 全考点梳理
华为
夜雨声烦丿3 小时前
拆解二维码名片生成器:原生鸿蒙页面的实现路径与调试方法
华为·harmonyos
DogDaoDao3 小时前
HarmonyOS 深度解析:从微内核到 ArkTS 工程实战
android·linux·ios·华为·程序员·harmonyos·harmonyos next
Magic-ZYJ17 小时前
HarmonyOS Release 构建安全:ArkGuard 混淆、签名配置与秘密管理
安全·华为·harmonyos·移动应用开发·心晴手记
见山是山-见水是水18 小时前
拆解HarmonyOS网络请求适配:原生鸿蒙页面的实现路径与调试方法
华为·harmonyos
m0_7496902318 小时前
【寻迹校园 HarmonyOS NEXT 实战 36】深色模式不是反色:用语义 Token 管理品牌色与状态色
harmonyos·arkts·深色模式·ui设计·主题设计