在这里插入图片描述

每日一句正能量

给自己一个清晰的目标,恰恰是走出犹豫,开始行动的关键。
犹豫的根源 往往不是懒,而是目标模糊。不知道要什么,自然无法决定第一步。清晰的目标 提供方向感、筛选标准和动力。哪怕目标很小(比如“今天读10页书”),也能立刻打破“做还是不做”的内耗。不是等不犹豫了才定目标,而是定了目标才能不再犹豫。

前言

摘要:HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR特性,为医疗康复领域开辟了全新的交互维度。本文将实战开发一款面向HarmonyOS PC的"灵犀康养"智能康复系统,展示如何利用systemMaterialEffect构建随康复阶段动态变化的治疗环境光感,通过悬浮导航实现训练项目与康复阶段的快速切换,基于Face AR实现患者情绪状态与疼痛反馈的实时捕捉,基于Body AR实现康复动作的精准追踪与关节角度分析,以及基于多窗口架构构建训练指导、康复数据、医生反馈和家属关怀的协作康复界面。


一、前言:康复训练的交互范式革新

传统康复训练往往依赖治疗师一对一指导和固定器械,患者难以获得实时反馈,训练数据也难以量化追踪。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation)沉浸光感(Immersive Light Effects)Face AR & Body AR特性,为康复训练带来了"情绪即反馈、肢体即处方"的全新可能。

本文核心亮点

  • 阶段感知光效:根据康复阶段(急性期/恢复期/强化期/维持期)动态切换治疗环境光色与氛围
  • 情绪疼痛监测:通过Face AR实时捕捉患者微表情,识别疼痛、焦虑、疲惫等状态,自动调整训练强度
  • 动作精准追踪:Body AR追踪20+骨骼关键点,实时分析关节角度、活动范围、对称性
  • 悬浮项目导航:底部悬浮页签切换训练项目,支持透明度调节,最大化训练画面区域
  • 多窗口康复协作:主训练窗口 + 浮动动作指导 + 浮动数据面板 + 浮动医生视频 + 浮动家属关怀

二、核心特性解析与技术选型

2.1 沉浸光感在康复场景中的价值

HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为UI组件带来细腻的光晕与反射效果。在康复场景中,这种材质效果能够:

  • 增强治疗氛围:急性期的冷静蓝光缓解焦虑、恢复期的温暖绿光促进愈合、强化期的活力橙光激发动力
  • 进度可视化:康复进度通过光效强度变化直观呈现,增强患者信心
  • 情绪调节:检测到焦虑情绪时自动切换为柔和光效,降低患者紧张感
  • 安全警示:动作超出安全范围时标题栏泛红警示,防止二次损伤

2.2 Face AR在康复中的创新应用

HarmonyOS 6的Face AR能力支持实时精确捕捉人脸微表情(64种BlendShape参数),在康复中可以:

  • 疼痛等级评估:通过皱眉、咬牙、眼部眯起等微表情自动评估疼痛等级(0-10分)
  • 情绪状态识别:识别焦虑、沮丧、积极等情绪,辅助心理治疗师调整方案
  • 疲劳度监测:通过眨眼频率、打哈欠、眼神游离判断疲劳程度,建议休息
  • 康复依从性:记录训练过程中的表情变化,生成"康复情绪档案"供医生参考

2.3 Body AR在动作追踪中的创新应用

HarmonyOS 6的Body AR能力支持20+骨骼关键点追踪,在康复动作追踪中可以:

  • 关节角度测量:实时测量肩、肘、腕、髋、膝、踝等关节的活动角度
  • 动作对称性分析:对比左右侧肢体动作差异,评估康复效果
  • 运动轨迹记录:记录肢体运动轨迹,与标准动作比对,给出纠正建议
  • 跌倒风险预警:检测身体重心偏移、步态异常,及时预警跌倒风险

三、环境配置与权限声明

3.1 模块依赖配置

oh-package.json5中添加AR Engine、图形引擎和UI Design Kit依赖:

{
  "dependencies": {
    "@hms.core.ar.engine": "^6.1.0",
    "@hms.core.graphics.2d": "^6.0.0",
    "@hms.core.arkui.design": "^6.0.0",
    "@hms.core.health.kit": "^6.0.0",
    "@hms.core.ai.vision": "^6.0.0",
    "@hms.core.distributed.device": "^6.0.0"
  }
}

3.2 权限声明(module.json5)

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:body_ar_camera_permission",
        "usedScene": {
          "abilities": ["RehabAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:network_permission"
      },
      {
        "name": "ohos.permission.HEALTH_DATA",
        "reason": "$string:health_data_access"
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "$string:doctor_sync"
      }
    ]
  }
}

隐私说明:Face AR与Body AR的所有图像数据仅在端侧NPU处理,患者生物特征数据不上传云端,符合医疗隐私保护要求。


四、核心代码实战

4.1 康复阶段光感引擎(RehabLightEngine.ets)

代码亮点:根据康复阶段动态生成治疗环境光感参数,支持光效过渡、情绪调节和安全警示。

// engines/RehabLightEngine.ets
import { ColorUtils } from '@hms.core.graphics.2d';

export enum RehabPhase {
  ACUTE = 'acute',           // 急性期 - 冷静蓝光,缓解疼痛焦虑
  RECOVERY = 'recovery',     // 恢复期 - 温暖绿光,促进愈合
  STRENGTHEN = 'strengthen', // 强化期 - 活力橙光,激发动力
  MAINTAIN = 'maintain'      // 维持期 - 稳定白光,保持状态
}

export enum RehabMood {
  CALM = 'calm',           // 平静
  ANXIOUS = 'anxious',     // 焦虑
  PAINFUL = 'painful',     // 疼痛
  TIRED = 'tired',         // 疲劳
  MOTIVATED = 'motivated', // 积极
  FRUSTRATED = 'frustrated' // 挫败
}

export interface RehabLightTheme {
  primaryColor: ResourceColor;
  secondaryColor: ResourceColor;
  ambientColor: ResourceColor;
  safeColor: ResourceColor;      // 安全范围指示色
  warningColor: ResourceColor;   // 警告色
  glowIntensity: number;
  pulseSpeed: number;
  colorTemperature: number;
  brightness: number;
  relaxationMode: boolean;      // 放松模式
}

export class RehabLightEngine {
  private static themes: Map<RehabPhase, RehabLightTheme> = new Map([
    [RehabPhase.ACUTE, {
      primaryColor: '#3B82F6',        // 冷静蓝
      secondaryColor: '#1E40AF',      // 深蓝
      ambientColor: 'rgba(59, 130, 246, 0.08)',
      safeColor: '#4ADE80',           // 安全绿
      warningColor: '#EF4444',        // 警告红
      glowIntensity: 0.4,
      pulseSpeed: 6000,
      colorTemperature: 5500,
      brightness: 0.75,
      relaxationMode: true
    }],
    [RehabPhase.RECOVERY, {
      primaryColor: '#22C55E',        // 愈合绿
      secondaryColor: '#15803D',      // 深绿
      ambientColor: 'rgba(34, 197, 94, 0.1)',
      safeColor: '#4ADE80',
      warningColor: '#F59E0B',
      glowIntensity: 0.6,
      pulseSpeed: 4000,
      colorTemperature: 4200,
      brightness: 0.85,
      relaxationMode: true
    }],
    [RehabPhase.STRENGTHEN, {
      primaryColor: '#F97316',        // 活力橙
      secondaryColor: '#C2410C',      // 深橙
      ambientColor: 'rgba(249, 115, 22, 0.12)',
      safeColor: '#4ADE80',
      warningColor: '#EF4444',
      glowIntensity: 0.8,
      pulseSpeed: 2500,
      colorTemperature: 3800,
      brightness: 0.9,
      relaxationMode: false
    }],
    [RehabPhase.MAINTAIN, {
      primaryColor: '#E2E8F0',        // 稳定白
      secondaryColor: '#94A3B8',      // 灰
      ambientColor: 'rgba(226, 232, 240, 0.06)',
      safeColor: '#4ADE80',
      warningColor: '#F59E0B',
      glowIntensity: 0.5,
      pulseSpeed: 5000,
      colorTemperature: 4500,
      brightness: 0.85,
      relaxationMode: true
    }]
  ]);

  static getTheme(phase: RehabPhase): RehabLightTheme {
    return this.themes.get(phase) || this.themes.get(RehabPhase.ACUTE)!;
  }

  // 根据患者情绪调整光效
  static adjustByMood(theme: RehabLightTheme, mood: RehabMood): RehabLightTheme {
    const adjusted = { ...theme };
    
    switch (mood) {
      case RehabMood.ANXIOUS:
        adjusted.primaryColor = '#818CF8';  // 柔和紫蓝安抚
        adjusted.glowIntensity *= 0.7;
        adjusted.pulseSpeed *= 1.5;
        adjusted.relaxationMode = true;
        break;
      case RehabMood.PAINFUL:
        adjusted.primaryColor = '#FCA5A5';  // 淡粉缓解
        adjusted.glowIntensity *= 0.6;
        adjusted.brightness *= 0.85;
        adjusted.relaxationMode = true;
        break;
      case RehabMood.TIRED:
        adjusted.brightness *= 0.8;
        adjusted.glowIntensity *= 0.7;
        adjusted.colorTemperature = Math.max(2700, adjusted.colorTemperature - 300);
        break;
      case RehabMood.MOTIVATED:
        adjusted.glowIntensity *= 1.2;
        adjusted.pulseSpeed *= 0.8;
        adjusted.brightness *= 1.05;
        break;
      case RehabMood.FRUSTRATED:
        adjusted.primaryColor = '#FBBF24';  // 暖黄鼓励
        adjusted.glowIntensity *= 1.1;
        break;
      case RehabMood.CALM:
        // 保持默认
        break;
    }
    
    return adjusted;
  }

  // 安全警示光效
  static getSafetyLight(isSafe: boolean, intensity: number = 1): RehabLightTheme {
    const base = this.getTheme(RehabPhase.RECOVERY);
    if (isSafe) {
      return { ...base, primaryColor: base.safeColor, glowIntensity: 0.6 * intensity };
    }
    return { 
      ...base, 
      primaryColor: base.warningColor, 
      glowIntensity: 1.0 * intensity,
      pulseSpeed: 1000  // 快速闪烁警示
    };
  }
}

4.2 Face AR疼痛与情绪评估系统(PainEmotionSystem.ets)

代码亮点:利用Face AR的64种BlendShape参数,实时评估患者疼痛等级与情绪状态,自动调整康复方案。

// systems/PainEmotionSystem.ets
import { ARSession, ARFaceTrack, ARBlendShapes } from '@hms.core.ar.engine';

export interface PainAssessment {
  level: number;           // 疼痛等级 0-10
  type: 'sharp' | 'dull' | 'burning' | 'aching' | 'throbbing';
  confidence: number;
  triggers: string[];        // 触发动作
}

export interface EmotionState {
  primary: RehabMood;
  intensity: number;       // 情绪强度 0-1
  stability: number;       // 稳定性 0-1
  trend: 'improving' | 'stable' | 'worsening';
}

export class PainEmotionSystem {
  private session: ARSession | null = null;
  private faceTrack: ARFaceTrack | null = null;

  // 历史记录
  private expressionHistory: FaceExpression[] = [];
  private painHistory: PainAssessment[] = [];
  private emotionHistory: EmotionState[] = [];

  async initialize(): Promise<void> {
    this.session = await ARSession.create({
      featureTypes: [ARFeatureType.FACE],
      cameraConfig: {
        facing: CameraFacing.FRONT,
        resolution: CameraResolution.HD_720P
      }
    });
    
    this.faceTrack = this.session.getFaceTrack();
    await this.session.start();
  }

  // 每帧更新:评估疼痛与情绪
  update(frameData: ARFrame): { pain: PainAssessment; emotion: EmotionState } | null {
    if (!this.faceTrack) return null;

    const faces = this.faceTrack.getTrackedFaces(frameData);
    if (faces.length === 0) return null;

    const face = faces[0];
    const blendshapes = face.getBlendShapes();

    const expression = this.parseExpression(blendshapes);
    this.expressionHistory.push(expression);
    if (this.expressionHistory.length > 30) this.expressionHistory.shift();

    const pain = this.assessPain(expression);
    const emotion = this.assessEmotion(expression);

    this.painHistory.push(pain);
    this.emotionHistory.push(emotion);

    // 通知系统调整
    AppStorage.set('current_pain_level', pain.level);
    AppStorage.set('current_emotion_state', emotion);

    return { pain, emotion };
  }

  private parseExpression(blendshapes: ARBlendShapes): FaceExpression {
    return {
      browLower: blendshapes.getValue('BROW_LOWERER') || 0,
      browRaise: blendshapes.getValue('BROW_RAISE') || 0,
      eyeSquintLeft: blendshapes.getValue('EYE_SQUINT_LEFT') || 0,
      eyeSquintRight: blendshapes.getValue('EYE_SQUINT_RIGHT') || 0,
      noseWrinkle: blendshapes.getValue('NOSE_WRINKLE') || 0,
      mouthOpen: blendshapes.getValue('MOUTH_OPEN') || 0,
      jawClench: blendshapes.getValue('JAW_CLENCH') || 0,
      lipCornerDepress: blendshapes.getValue('MOUTH_CORNER_DEPRESS_LEFT') || 0,
      eyeBlink: blendshapes.getValue('EYE_BLINK_LEFT') || 0,
      timestamp: Date.now()
    };
  }

  private assessPain(expression: FaceExpression): PainAssessment {
    let level = 0;
    const triggers: string[] = [];

    // 皱眉程度
    if (expression.browLower > 0.3) {
      level += expression.browLower * 3;
      triggers.push('皱眉');
    }

    // 眼部眯起(疼痛典型反应)
    const eyeSquint = (expression.eyeSquintLeft + expression.eyeSquintRight) / 2;
    if (eyeSquint > 0.4) {
      level += eyeSquint * 2.5;
      triggers.push('眯眼');
    }

    // 鼻子皱起
    if (expression.noseWrinkle > 0.3) {
      level += expression.noseWrinkle * 2;
      triggers.push('鼻皱');
    }

    // 咬牙/张嘴
    if (expression.jawClench > 0.5) {
      level += expression.jawClench * 1.5;
      triggers.push('咬牙');
    }

    // 嘴角下垂(痛苦表情)
    if (expression.lipCornerDepress > 0.4) {
      level += expression.lipCornerDepress * 1.5;
      triggers.push('嘴角下垂');
    }

    level = Math.min(10, Math.max(0, level));

    // 判断疼痛类型
    let type: PainAssessment['type'] = 'aching';
    if (expression.jawClench > 0.6 && expression.browLower > 0.6) {
      type = 'sharp';
    } else if (expression.eyeSquintLeft > 0.7 && expression.eyeSquintRight > 0.7) {
      type = 'burning';
    } else if (expression.mouthOpen > 0.5 && expression.browLower < 0.4) {
      type = 'dull';
    }

    return {
      level: Math.round(level * 10) / 10,
      type,
      confidence: Math.min(1, level / 5),
      triggers
    };
  }

  private assessEmotion(expression: FaceExpression): EmotionState {
    // 分析最近10帧的趋势
    const recent = this.expressionHistory.slice(-10);
    
    let anxiousScore = 0;
    let tiredScore = 0;
    let motivatedScore = 0;
    let frustratedScore = 0;

    recent.forEach(expr => {
      // 焦虑:频繁眨眼 + 眉毛抬高
      if (expr.eyeBlink > 0.6 && expr.browRaise > 0.3) anxiousScore++;
      
      // 疲劳:眨眼频率低 + 眼神呆滞
      if (expr.eyeBlink < 0.2 && expr.browLower < 0.2) tiredScore++;
      
      // 积极:微笑 + 眼神明亮
      if (expr.browRaise > 0.2 && expr.browLower < 0.3) motivatedScore++;
      
      // 挫败:紧咬牙关 + 嘴角下垂
      if (expr.jawClench > 0.4 && expr.lipCornerDepress > 0.3) frustratedScore++;
    });

    const scores = [
      { mood: RehabMood.ANXIOUS, score: anxiousScore },
      { mood: RehabMood.TIRED, score: tiredScore },
      { mood: RehabMood.MOTIVATED, score: motivatedScore },
      { mood: RehabMood.FRUSTRATED, score: frustratedScore }
    ];

    scores.sort((a, b) => b.score - a.score);
    const primary = scores[0].score > 3 ? scores[0].mood : RehabMood.CALM;

    // 计算稳定性
    const stability = this.expressionHistory.length > 10 
      ? 1 - (this.calculateVariance(recent.map(e => e.browLower)) * 2)
      : 0.5;

    // 判断趋势
    let trend: EmotionState['trend'] = 'stable';
    if (this.emotionHistory.length > 5) {
      const previous = this.emotionHistory[this.emotionHistory.length - 5];
      if (scores[0].score > 3 && previous.primary !== primary) {
        trend = scores[0].score > previous.intensity * 10 ? 'worsening' : 'improving';
      }
    }

    return {
      primary,
      intensity: scores[0].score / 10,
      stability: Math.max(0, Math.min(1, stability)),
      trend
    };
  }

  private calculateVariance(values: number[]): number {
    const mean = values.reduce((a, b) => a + b, 0) / values.length;
    const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
    return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / values.length);
  }

  // 获取疼痛历史
  getPainHistory(): PainAssessment[] {
    return [...this.painHistory];
  }

  // 获取情绪历史
  getEmotionHistory(): EmotionState[] {
    return [...this.emotionHistory];
  }

  release(): void {
    this.session?.stop();
    this.session?.release();
  }
}

interface FaceExpression {
  browLower: number;
  browRaise: number;
  eyeSquintLeft: number;
  eyeSquintRight: number;
  noseWrinkle: number;
  mouthOpen: number;
  jawClench: number;
  lipCornerDepress: number;
  eyeBlink: number;
  timestamp: number;
}

4.3 Body AR康复动作追踪系统(RehabMotionSystem.ets)

代码亮点:将Body AR的20+骨骼关键点映射为康复动作分析,实现精准的动作评估与纠正。

// systems/RehabMotionSystem.ets
import { ARSession, ARBodyTrack, ARBodySkeleton, KeyPointType } from '@hms.core.ar.engine';

export enum JointType {
  SHOULDER_LEFT = 'shoulder_left',
  SHOULDER_RIGHT = 'shoulder_right',
  ELBOW_LEFT = 'elbow_left',
  ELBOW_RIGHT = 'elbow_right',
  WRIST_LEFT = 'wrist_left',
  WRIST_RIGHT = 'wrist_right',
  HIP_LEFT = 'hip_left',
  HIP_RIGHT = 'hip_right',
  KNEE_LEFT = 'knee_left',
  KNEE_RIGHT = 'knee_right',
  ANKLE_LEFT = 'ankle_left',
  ANKLE_RIGHT = 'ankle_right'
}

export interface JointAngle {
  joint: JointType;
  angle: number;           // 角度 0-180
  targetRange: { min: number; max: number };
  deviation: number;       // 偏离度
  isSafe: boolean;
}

export interface MotionAnalysis {
  timestamp: number;
  jointAngles: JointAngle[];
  symmetry: number;        // 对称性 0-100
  stability: number;       // 稳定性 0-100
  rangeOfMotion: number;   // 活动度 0-100
  repetitions: number;     // 重复次数
  formScore: number;       // 动作质量 0-100
  feedback: string[];
}

export interface RehabExercise {
  id: string;
  name: string;
  targetJoints: JointType[];
  targetRange: { min: number; max: number };
  repetitions: number;
  holdTime: number;         // 保持时间(秒)
  restTime: number;         // 休息时间(秒)
}

export class RehabMotionSystem {
  private session: ARSession | null = null;
  private bodyTrack: ARBodyTrack | null = null;

  // 运动历史
  private motionHistory: MotionAnalysis[] = [];
  private currentExercise: RehabExercise | null = null;
  private repCount: number = 0;
  private lastRepTime: number = 0;
  private inHoldPhase: boolean = false;
  private holdStartTime: number = 0;

  async initialize(): Promise<void> {
    this.session = await ARSession.create({
      featureTypes: [ARFeatureType.BODY],
      cameraConfig: {
        facing: CameraFacing.FRONT,
        resolution: CameraResolution.HD_720P
      }
    });
    
    this.bodyTrack = this.session.getBodyTrack();
    await this.session.start();
  }

  // 每帧更新:分析康复动作
  update(frameData: ARFrame, exercise: RehabExercise): MotionAnalysis | null {
    if (!this.bodyTrack) return null;

    const bodies = this.bodyTrack.getTrackedBodies(frameData);
    if (bodies.length === 0) return null;

    const body = bodies[0];
    const skeleton = body.getSkeleton();
    const keypoints = skeleton.getKeyPoints();

    this.currentExercise = exercise;

    // 计算各关节角度
    const jointAngles = this.calculateJointAngles(keypoints, exercise);
    
    // 计算对称性
    const symmetry = this.calculateSymmetry(jointAngles);
    
    // 计算稳定性
    const stability = this.calculateStability(jointAngles);
    
    // 计算活动度
    const rangeOfMotion = this.calculateRangeOfMotion(jointAngles, exercise);
    
    // 计数重复次数
    const repetitions = this.countRepetitions(jointAngles, exercise);
    
    // 评估动作质量
    const { formScore, feedback } = this.assessForm(jointAngles, exercise);

    const analysis: MotionAnalysis = {
      timestamp: Date.now(),
      jointAngles,
      symmetry,
      stability,
      rangeOfMotion,
      repetitions: this.repCount,
      formScore,
      feedback
    };

    this.motionHistory.push(analysis);
    if (this.motionHistory.length > 120) this.motionHistory.shift();

    // 通知UI更新
    AppStorage.set('motion_analysis', analysis);

    return analysis;
  }

  private calculateJointAngles(keypoints: KeyPoint[], exercise: RehabExercise): JointAngle[] {
    const angles: JointAngle[] = [];

    exercise.targetJoints.forEach(joint => {
      const angle = this.measureJointAngle(keypoints, joint);
      const deviation = Math.abs(angle - (exercise.targetRange.min + exercise.targetRange.max) / 2);
      const isSafe = angle >= exercise.targetRange.min - 10 && angle <= exercise.targetRange.max + 10;

      angles.push({
        joint,
        angle,
        targetRange: exercise.targetRange,
        deviation,
        isSafe
      });
    });

    return angles;
  }

  private measureJointAngle(keypoints: KeyPoint[], joint: JointType): number {
    // 根据关节类型选择对应的骨骼点
    let p1: KeyPoint | undefined, p2: KeyPoint | undefined, p3: KeyPoint | undefined;

    switch (joint) {
      case JointType.ELBOW_LEFT:
        p1 = keypoints.find(kp => kp.type === KeyPointType.LEFT_SHOULDER);
        p2 = keypoints.find(kp => kp.type === KeyPointType.LEFT_ELBOW);
        p3 = keypoints.find(kp => kp.type === KeyPointType.LEFT_WRIST);
        break;
      case JointType.ELBOW_RIGHT:
        p1 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_SHOULDER);
        p2 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ELBOW);
        p3 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_WRIST);
        break;
      case JointType.KNEE_LEFT:
        p1 = keypoints.find(kp => kp.type === KeyPointType.LEFT_HIP);
        p2 = keypoints.find(kp => kp.type === KeyPointType.LEFT_KNEE);
        p3 = keypoints.find(kp => kp.type === KeyPointType.LEFT_ANKLE);
        break;
      case JointType.KNEE_RIGHT:
        p1 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_HIP);
        p2 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_KNEE);
        p3 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ANKLE);
        break;
      case JointType.SHOULDER_LEFT:
        p1 = keypoints.find(kp => kp.type === KeyPointType.LEFT_ELBOW);
        p2 = keypoints.find(kp => kp.type === KeyPointType.LEFT_SHOULDER);
        p3 = keypoints.find(kp => kp.type === KeyPointType.LEFT_HIP);
        break;
      case JointType.SHOULDER_RIGHT:
        p1 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ELBOW);
        p2 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_SHOULDER);
        p3 = keypoints.find(kp => kp.type === KeyPointType.RIGHT_HIP);
        break;
    }

    if (!p1 || !p2 || !p3) return 0;

    // 计算向量夹角
    const v1 = { x: p1.x - p2.x, y: p1.y - p2.y };
    const v2 = { x: p3.x - p2.x, y: p3.y - p2.y };

    const dot = v1.x * v2.x + v1.y * v2.y;
    const mag1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
    const mag2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);

    if (mag1 === 0 || mag2 === 0) return 0;

    const cosAngle = dot / (mag1 * mag2);
    return Math.acos(Math.max(-1, Math.min(1, cosAngle))) * (180 / Math.PI);
  }

  private calculateSymmetry(jointAngles: JointAngle[]): number {
    // 计算左右对称性
    const leftJoints = jointAngles.filter(j => j.joint.includes('left'));
    const rightJoints = jointAngles.filter(j => j.joint.includes('right'));

    if (leftJoints.length === 0 || rightJoints.length === 0) return 100;

    let totalDiff = 0;
    leftJoints.forEach(left => {
      const right = rightJoints.find(r => 
        r.joint.replace('right', 'left') === left.joint
      );
      if (right) {
        totalDiff += Math.abs(left.angle - right.angle);
      }
    });

    return Math.max(0, 100 - (totalDiff / leftJoints.length) * 2);
  }

  private calculateStability(jointAngles: JointAngle[]): number {
    if (this.motionHistory.length < 5) return 100;

    const recent = this.motionHistory.slice(-5);
    let variance = 0;

    jointAngles.forEach(joint => {
      const angles = recent.map(m => {
        const j = m.jointAngles.find(ja => ja.joint === joint.joint);
        return j ? j.angle : 0;
      });
      variance += this.calculateVariance(angles);
    });

    return Math.max(0, 100 - variance * 5);
  }

  private calculateRangeOfMotion(jointAngles: JointAngle[], exercise: RehabExercise): number {
    const targetMid = (exercise.targetRange.min + exercise.targetRange.max) / 2;
    const currentMid = jointAngles.reduce((sum, j) => sum + j.angle, 0) / jointAngles.length;
    
    return Math.min(100, (currentMid / targetMid) * 100);
  }

  private countRepetitions(jointAngles: JointAngle[], exercise: RehabExercise): number {
    const now = Date.now();
    const avgAngle = jointAngles.reduce((sum, j) => sum + j.angle, 0) / jointAngles.length;

    // 检测动作峰值
    const isAtPeak = avgAngle >= exercise.targetRange.max * 0.9;
    const isAtRest = avgAngle <= exercise.targetRange.min * 1.1;

    if (isAtPeak && !this.inHoldPhase) {
      this.inHoldPhase = true;
      this.holdStartTime = now;
    }

    if (isAtRest && this.inHoldPhase) {
      const holdDuration = (now - this.holdStartTime) / 1000;
      if (holdDuration >= exercise.holdTime * 0.8) {  // 允许20%容差
        this.repCount++;
        this.lastRepTime = now;
        this.inHoldPhase = false;
        
        // 播放完成音效
        AppStorage.set('rep_completed', this.repCount);
      }
    }

    return this.repCount;
  }

  private assessForm(jointAngles: JointAngle[], exercise: RehabExercise): { formScore: number; feedback: string[] } {
    const feedback: string[] = [];
    let score = 100;

    // 检查各关节是否在安全范围
    jointAngles.forEach(joint => {
      if (!joint.isSafe) {
        score -= 15;
        feedback.push(`${this.getJointName(joint.joint)}角度超出安全范围`);
      }
    });

    // 检查对称性
    const symmetry = this.calculateSymmetry(jointAngles);
    if (symmetry < 80) {
      score -= 10;
      feedback.push('左右动作不对称,请注意平衡');
    }

    // 检查稳定性
    const stability = this.calculateStability(jointAngles);
    if (stability < 70) {
      score -= 10;
      feedback.push('动作不够稳定,请控制速度');
    }

    // 鼓励反馈
    if (score >= 90) {
      feedback.push('动作标准,继续保持!');
    } else if (score >= 70) {
      feedback.push('动作良好,稍加调整即可');
    }

    return { formScore: Math.max(0, score), feedback };
  }

  private getJointName(joint: JointType): string {
    const names: Map<JointType, string> = new Map([
      [JointType.SHOULDER_LEFT, '左肩'],
      [JointType.SHOULDER_RIGHT, '右肩'],
      [JointType.ELBOW_LEFT, '左肘'],
      [JointType.ELBOW_RIGHT, '右肘'],
      [JointType.WRIST_LEFT, '左腕'],
      [JointType.WRIST_RIGHT, '右腕'],
      [JointType.HIP_LEFT, '左髋'],
      [JointType.HIP_RIGHT, '右髋'],
      [JointType.KNEE_LEFT, '左膝'],
      [JointType.KNEE_RIGHT, '右膝'],
      [JointType.ANKLE_LEFT, '左踝'],
      [JointType.ANKLE_RIGHT, '右踝']
    ]);
    return names.get(joint) || joint;
  }

  private calculateVariance(values: number[]): number {
    const mean = values.reduce((a, b) => a + b, 0) / values.length;
    const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
    return Math.sqrt(squaredDiffs.reduce((a, b) => a + b, 0) / values.length);
  }

  // 获取运动历史
  getMotionHistory(): MotionAnalysis[] {
    return [...this.motionHistory];
  }

  // 重置计数
  resetCount(): void {
    this.repCount = 0;
    this.inHoldPhase = false;
  }

  release(): void {
    this.session?.stop();
    this.session?.release();
  }
}

4.4 沉浸光感康复标题栏(ImmersiveRehabTitleBar.ets)

代码亮点:标题栏随康复阶段和患者情绪动态变化,显示训练进度、疼痛等级和情绪状态。

// components/ImmersiveRehabTitleBar.ets
import { RehabLightEngine, RehabPhase, RehabMood } from '../engines/RehabLightEngine';
import { PainAssessment } from '../systems/PainEmotionSystem';
import { MotionAnalysis } from '../systems/RehabMotionSystem';

@Component
export struct ImmersiveRehabTitleBar {
  @Prop currentPhase: RehabPhase;
  @Prop patientName: string;
  @Prop exerciseName: string;
  @Prop painAssessment: PainAssessment;
  @Prop motionAnalysis: MotionAnalysis;
  @Prop rehabProgress: number;  // 0-100

  @State theme = RehabLightEngine.getTheme(RehabPhase.ACUTE);
  @State pulseAnimation: boolean = false;

  aboutToAppear(): void {
    this.theme = RehabLightEngine.getTheme(this.currentPhase);
    setInterval(() => {
      this.pulseAnimation = !this.pulseAnimation;
    }, this.theme.pulseSpeed / 2);
  }

  build() {
    Row() {
      // 左侧:患者与阶段信息
      Row({ space: 12 }) {
        // 康复阶段徽标
        Stack() {
          Circle()
            .width(44)
            .height(44)
            .fill(this.theme.primaryColor)
            .shadow({
              radius: this.pulseAnimation ? 20 : 8,
              color: this.theme.primaryColor,
              offsetX: 0,
              offsetY: 0
            })
            .animation({
              duration: this.theme.pulseSpeed / 2,
              curve: Curve.EaseInOut,
              iterations: -1
            })

          Text(this.getPhaseIcon())
            .fontSize(22)
        }

        Column({ space: 4 }) {
          Text(this.patientName)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')

          Text(`${this.getPhaseName()} | ${this.exerciseName}`)
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.7)')
        }
      }

      // 中间:疼痛与动作数据
      Row({ space: 20 }) {
        // 疼痛等级
        Column({ space: 2 }) {
          Row({ space: 4 }) {
            Text('😣')
              .fontSize(12)
            Text(`疼痛: ${this.painAssessment.level.toFixed(1)}/10`)
              .fontSize(14)
              .fontColor(this.getPainColor())
          }
          Progress({ value: this.painAssessment.level, total: 10, type: ProgressType.Linear })
            .width(80)
            .height(4)
            .color(this.getPainColor())
            .backgroundColor('rgba(255,255,255,0.2)')
        }

        // 动作质量
        Column({ space: 2 }) {
          Row({ space: 4 }) {
            Text('⭐')
              .fontSize(12)
            Text(`${this.motionAnalysis.formScore.toFixed(0)}`)
              .fontSize(14)
              .fontColor(this.motionAnalysis.formScore > 80 ? '#4ADE80' : '#FBBF24')
          }
          Progress({ value: this.motionAnalysis.formScore, total: 100, type: ProgressType.Linear })
            .width(80)
            .height(4)
            .color(this.motionAnalysis.formScore > 80 ? '#4ADE80' : '#FBBF24')
            .backgroundColor('rgba(255,255,255,0.2)')
        }

        // 重复次数
        Column({ space: 2 }) {
          Text('🔁')
            .fontSize(12)
          Text(`${this.motionAnalysis.repetitions}`)
            .fontSize(14)
            .fontColor('#FFFFFF')
        }

        // 对称性
        Column({ space: 2 }) {
          Text('⚖️')
            .fontSize(12)
          Text(`${this.motionAnalysis.symmetry.toFixed(0)}%`)
            .fontSize(14)
            .fontColor(this.motionAnalysis.symmetry > 80 ? '#4ADE80' : '#F59E0B')
        }
      }

      // 右侧:康复进度
      Row({ space: 12 }) {
        // 安全状态
        if (!this.motionAnalysis.jointAngles.every(j => j.isSafe)) {
          Row({ space: 4 }) {
            Text('⚠️')
              .fontSize(14)
            Text('注意安全')
              .fontSize(11)
              .fontColor('#EF4444')
          }
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .backgroundColor('rgba(239, 68, 68, 0.15)')
          .borderRadius(12)
          .animation({
            duration: 800,
            curve: Curve.EaseInOut,
            iterations: -1
          })
        }

        // 总体进度
        Column({ space: 2 }) {
          Text('📈')
            .fontSize(12)
          Text(`${this.rehabProgress.toFixed(0)}%`)
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
      }
    }
    .width('100%')
    .height(64)
    .padding({ left: 24, right: 24 })
    .backgroundColor(this.theme.ambientColor)
    .backdropBlur(20)
    .systemMaterialEffect(MaterialStyle.IMMERSIVE)
    .borderRadius({ bottomLeft: 20, bottomRight: 20 })
    .shadow({
      radius: 25,
      color: this.theme.primaryColor,
      offsetX: 0,
      offsetY: 6
    })
    .justifyContent(FlexAlign.SpaceBetween)
  }

  private getPhaseIcon(): string {
    const icons: Map<RehabPhase, string> = new Map([
      [RehabPhase.ACUTE, '🧊'],
      [RehabPhase.RECOVERY, '🌱'],
      [RehabPhase.STRENGTHEN, '💪'],
      [RehabPhase.MAINTAIN, '✅']
    ]);
    return icons.get(this.currentPhase) || '❓';
  }

  private getPhaseName(): string {
    const names: Map<RehabPhase, string> = new Map([
      [RehabPhase.ACUTE, '急性期'],
      [RehabPhase.RECOVERY, '恢复期'],
      [RehabPhase.STRENGTHEN, '强化期'],
      [RehabPhase.MAINTAIN, '维持期']
    ]);
    return names.get(this.currentPhase) || '未知';
  }

  private getPainColor(): ResourceColor {
    const level = this.painAssessment.level;
    if (level <= 3) return '#4ADE80';
    if (level <= 6) return '#FBBF24';
    return '#EF4444';
  }
}

4.5 悬浮训练导航面板(FloatExerciseNav.ets)

代码亮点:底部悬浮面板采用HdsTabs悬浮样式,四周留白,支持训练项目切换和透明度调节。

// components/FloatExerciseNav.ets
import { HdsTabs, HdsTabBarStyle } from '@hms.core.arkui.design';
import { RehabLightEngine, RehabPhase } from '../engines/RehabLightEngine';

interface ExerciseItem {
  title: string;
  icon: Resource;
  phase: RehabPhase;
  exercise: string;
  targetJoints: string[];
}

@Component
export struct FloatExerciseNav {
  @Prop currentPhase: RehabPhase;
  @Prop currentExercise: string;
  @Prop transparencyLevel: number;

  @State selectedIndex: number = 0;
  @State theme = RehabLightEngine.getTheme(RehabPhase.ACUTE);

  private exercises: ExerciseItem[] = [
    { title: '肩外展', icon: $r('app.media.exer_shoulder'), phase: RehabPhase.ACUTE, exercise: 'shoulder_abduction', targetJoints: ['肩关节'] },
    { title: '肘屈伸', icon: $r('app.media.exer_elbow'), phase: RehabPhase.RECOVERY, exercise: 'elbow_flexion', targetJoints: ['肘关节'] },
    { title: '膝屈伸', icon: $r('app.media.exer_knee'), phase: RehabPhase.RECOVERY, exercise: 'knee_flexion', targetJoints: ['膝关节'] },
    { title: '踝泵', icon: $r('app.media.exer_ankle'), phase: RehabPhase.ACUTE, exercise: 'ankle_pump', targetJoints: ['踝关节'] },
    { title: '平衡', icon: $r('app.media.exer_balance'), phase: RehabPhase.STRENGTHEN, exercise: 'balance', targetJoints: ['髋关节', '膝关节', '踝关节'] },
    { title: '步态', icon: $r('app.media.exer_gait'), phase: RehabPhase.STRENGTHEN, exercise: 'gait', targetJoints: ['全下肢'] }
  ];

  build() {
    Column() {
      HdsTabs({
        barStyle: HdsTabBarStyle.FLOATING,
        index: this.selectedIndex,
        onChange: (index: number) => {
          this.selectedIndex = index;
          this.handleExerciseChange(this.exercises[index]);
        }
      }) {
        ForEach(this.exercises, (item: ExerciseItem, index: number) => {
          TabContent() {
            Stack() {}
          }
          .tabBar(this.buildTabBar(item, index))
        })
      }
      .barBackgroundColor(`rgba(15, 15, 35, ${this.transparencyLevel})`)
      .barActiveColor(this.theme.primaryColor)
      .barInactiveColor('#666666')
      .barHeight(72)
      .barMargin({ left: 48, right: 48, bottom: 20 })
      .barBorderRadius(36)
      .systemMaterialEffect(MaterialStyle.IMMERSIVE)
      .backdropBlur(20)
    }
    .width('100%')
    .padding({ bottom: 16 })
  }

  @Builder
  buildTabBar(item: ExerciseItem, index: number): void {
    Column({ space: 4 }) {
      Stack() {
        Image(item.icon)
          .width(26)
          .height(26)
          .fillColor(index === this.selectedIndex ? this.theme.primaryColor : '#666666')

        // 当前训练指示器
        if (item.exercise === this.currentExercise) {
          Circle()
            .width(10)
            .height(10)
            .fill('#00F0FF')
            .position({ x: 20, y: -6 })
            .shadow({ radius: 6, color: 'rgba(0, 240, 255, 0.6)' })
        }
      }

      Text(item.title)
        .fontSize(11)
        .fontColor(index === this.selectedIndex ? this.theme.primaryColor : '#666666')
    }
    .width(68)
    .height(60)
    .justifyContent(FlexAlign.Center)
  }

  private handleExerciseChange(item: ExerciseItem): void {
    // 切换康复阶段光效
    this.theme = RehabLightEngine.getTheme(item.phase);
    AppStorage.set('switch_phase', item.phase);

    // 切换训练项目
    AppStorage.set('switch_exercise', item.exercise);

    // 显示训练信息
    AppStorage.set('exercise_info', {
      name: item.title,
      targetJoints: item.targetJoints,
      phase: item.phase
    });
  }
}

4.6 主康复训练页面(RehabMainPage.ets)

代码亮点:整合AR数据流、康复光感、悬浮导航和多窗口管理,实现完整的"灵犀康养"康复体验。

// pages/RehabMainPage.ets
import { PainEmotionSystem, PainAssessment, EmotionState } from '../systems/PainEmotionSystem';
import { RehabMotionSystem, MotionAnalysis, RehabExercise, JointType } from '../systems/RehabMotionSystem';
import { RehabLightEngine, RehabPhase, RehabMood } from '../engines/RehabLightEngine';
import { ImmersiveRehabTitleBar } from '../components/ImmersiveRehabTitleBar';
import { FloatExerciseNav } from '../components/FloatExerciseNav';

@Entry
@Component
struct RehabMainPage {
  // AR系统
  private painSystem: PainEmotionSystem = new PainEmotionSystem();
  private motionSystem: RehabMotionSystem = new RehabMotionSystem();

  // 患者状态
  @State patientName: string = '李先生';
  @State currentPhase: RehabPhase = RehabPhase.RECOVERY;
  @State currentExercise: string = 'shoulder_abduction';
  @State exerciseName: string = '肩关节外展训练';
  
  // 评估数据
  @State painAssessment: PainAssessment = {
    level: 2,
    type: 'aching',
    confidence: 0.8,
    triggers: []
  };
  @State emotionState: EmotionState = {
    primary: RehabMood.CALM,
    intensity: 0.3,
    stability: 0.8,
    trend: 'stable'
  };
  @State motionAnalysis: MotionAnalysis = {
    timestamp: Date.now(),
    jointAngles: [],
    symmetry: 85,
    stability: 90,
    rangeOfMotion: 75,
    repetitions: 0,
    formScore: 82,
    feedback: ['准备开始训练']
  };
  @State rehabProgress: number = 45;

  // 多窗口
  @State showGuide: boolean = true;
  @State showData: boolean = false;
  @State showDoctor: boolean = false;
  @State showFamily: boolean = false;

  // 当前训练
  private currentExerciseConfig: RehabExercise = {
    id: 'shoulder_abduction',
    name: '肩关节外展',
    targetJoints: [JointType.SHOULDER_LEFT],
    targetRange: { min: 0, max: 90 },
    repetitions: 10,
    holdTime: 3,
    restTime: 5
  };

  aboutToAppear(): void {
    this.setupImmersiveWindow();
    this.initializeRehabSystems();
    this.setupEventListeners();
  }

  private setupImmersiveWindow(): void {
    const window = windowStage.getMainWindowSync();
    window.setWindowLayoutFullScreen(true);
    window.setWindowBackgroundColor('#0A0A0F');
  }

  private async initializeRehabSystems(): Promise<void> {
    try {
      await this.painSystem.initialize();
      await this.motionSystem.initialize();
      this.startRehabLoop();
    } catch (err) {
      console.error('康复系统初始化失败:', err);
    }
  }

  private async startRehabLoop(): Promise<void> {
    const loop = async () => {
      try {
        const frame = await this.painSystem.session?.getCurrentFrame();
        if (!frame) {
          requestAnimationFrame(loop);
          return;
        }

        // Face AR:疼痛与情绪评估
        const painEmotion = this.painSystem.update(frame);
        if (painEmotion) {
          this.painAssessment = painEmotion.pain;
          this.emotionState = painEmotion.emotion;
          
          // 根据疼痛和情绪调整光效
          this.updateRehabLighting();
        }

        // Body AR:动作追踪
        const motion = this.motionSystem.update(frame, this.currentExerciseConfig);
        if (motion) {
          this.motionAnalysis = motion;
          
          // 更新康复进度
          this.rehabProgress = Math.min(100, (motion.repetitions / this.currentExerciseConfig.repetitions) * 100);
        }

      } catch (err) {
        console.error('康复循环错误:', err);
      }

      requestAnimationFrame(loop);
    };

    requestAnimationFrame(loop);
  }

  private updateRehabLighting(): void {
    const baseTheme = RehabLightEngine.getTheme(this.currentPhase);
    
    // 根据疼痛等级调整
    if (this.painAssessment.level > 6) {
      // 高疼痛:切换为安抚模式
      const soothingTheme = RehabLightEngine.adjustByMood(baseTheme, RehabMood.PAINFUL);
      AppStorage.set('current_rehab_theme', soothingTheme);
      return;
    }
    
    // 根据情绪调整
    const adjustedTheme = RehabLightEngine.adjustByMood(baseTheme, this.emotionState.primary);
    
    // 检查动作安全性
    const isSafe = this.motionAnalysis.jointAngles.every(j => j.isSafe);
    if (!isSafe) {
      const warningTheme = RehabLightEngine.getSafetyLight(false);
      AppStorage.set('current_rehab_theme', warningTheme);
      return;
    }
    
    AppStorage.set('current_rehab_theme', adjustedTheme);
  }

  private setupEventListeners(): void {
    AppStorage.watch('switch_phase', (phase: RehabPhase) => {
      this.currentPhase = phase;
    });

    AppStorage.watch('switch_exercise', (exercise: string) => {
      this.currentExercise = exercise;
      this.motionSystem.resetCount();
      this.updateExerciseConfig(exercise);
    });

    AppStorage.watch('show_guide', (show: boolean) => {
      this.showGuide = show;
    });

    AppStorage.watch('show_data', (show: boolean) => {
      this.showData = show;
    });
  }

  private updateExerciseConfig(exerciseId: string): void {
    const configs: Map<string, RehabExercise> = new Map([
      ['shoulder_abduction', {
        id: 'shoulder_abduction',
        name: '肩关节外展',
        targetJoints: [JointType.SHOULDER_LEFT, JointType.SHOULDER_RIGHT],
        targetRange: { min: 0, max: 90 },
        repetitions: 10,
        holdTime: 3,
        restTime: 5
      }],
      ['elbow_flexion', {
        id: 'elbow_flexion',
        name: '肘关节屈伸',
        targetJoints: [JointType.ELBOW_LEFT, JointType.ELBOW_RIGHT],
        targetRange: { min: 0, max: 135 },
        repetitions: 15,
        holdTime: 2,
        restTime: 3
      }],
      ['knee_flexion', {
        id: 'knee_flexion',
        name: '膝关节屈伸',
        targetJoints: [JointType.KNEE_LEFT, JointType.KNEE_RIGHT],
        targetRange: { min: 0, max: 120 },
        repetitions: 12,
        holdTime: 3,
        restTime: 4
      }]
    ]);
    
    this.currentExerciseConfig = configs.get(exerciseId) || this.currentExerciseConfig;
    this.exerciseName = this.currentExerciseConfig.name;
  }

  build() {
    Stack() {
      // 背景环境光
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(RehabLightEngine.getTheme(this.currentPhase).ambientColor)
        .animation({
          duration: 1500,
          curve: Curve.EaseInOut
        })

      // 主训练画面
      Column() {
        // AR人体骨架叠加
        BodySkeletonOverlay({
          motionAnalysis: this.motionAnalysis,
          painLevel: this.painAssessment.level
        })
        .width('100%')
        .height('100%')

        // 训练指导动画
        if (this.showGuide) {
          ExerciseGuide({
            exercise: this.currentExerciseConfig,
            currentRep: this.motionAnalysis.repetitions,
            formScore: this.motionAnalysis.formScore
          })
          .position({ x: '50%', y: '20%' })
          .translate({ x: '-50%' })
        }
      }
      .width('100%')
      .height('100%')

      // 沉浸光感标题栏
      ImmersiveRehabTitleBar({
        currentPhase: this.currentPhase,
        patientName: this.patientName,
        exerciseName: this.exerciseName,
        painAssessment: this.painAssessment,
        motionAnalysis: this.motionAnalysis,
        rehabProgress: this.rehabProgress
      })
      .position({ x: 0, y: 0 })
      .zIndex(100)

      // 浮动数据面板
      if (this.showData) {
        FloatDataPanel({
          motionHistory: this.motionSystem.getMotionHistory(),
          painHistory: this.painSystem.getPainHistory(),
          emotionHistory: this.painSystem.getEmotionHistory(),
          onClose: () => {
            this.showData = false;
          }
        })
        .position({ x: '78%', y: '15%' })
        .width(340)
        .height('70%')
        .zIndex(90)
      }

      // 浮动医生视频
      if (this.showDoctor) {
        FloatDoctorPanel({
          doctorName: '王医生',
          onClose: () => {
            this.showDoctor = false;
          }
        })
        .position({ x: '2%', y: '15%' })
        .width(320)
        .height('40%')
        .zIndex(90)
      }

      // 底部悬浮训练导航
      FloatExerciseNav({
        currentPhase: this.currentPhase,
        currentExercise: this.currentExercise,
        transparencyLevel: 0.65
      })
      .position({ x: 0, y: '100%' })
      .translate({ y: -88 })
      .zIndex(100)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0A0A0F')
  }

  aboutToDisappear(): void {
    this.painSystem.release();
    this.motionSystem.release();
  }
}

// 人体骨架叠加组件
@Component
struct BodySkeletonOverlay {
  @Prop motionAnalysis: MotionAnalysis;
  @Prop painLevel: number;

  build() {
    Stack() {
      Canvas(this.drawSkeleton)
        .width('100%')
        .height('100%')
        .backgroundColor('transparent')
    }
  }

  private drawSkeleton = (context: CanvasRenderingContext2D) => {
    const canvas = context.canvas;
    const w = canvas.width;
    const h = canvas.height;

    context.clearRect(0, 0, w, h);

    // 绘制关节点和连线(简化示意)
    this.motionAnalysis.jointAngles.forEach(joint => {
      const color = joint.isSafe 
        ? (this.painLevel > 5 ? '#FBBF24' : '#4ADE80')
        : '#EF4444';

      // 绘制关节点
      context.beginPath();
      context.arc(w / 2, h / 2, 20, 0, Math.PI * 2);
      context.fillStyle = color;
      context.fill();

      // 绘制角度指示
      context.fillStyle = '#FFFFFF';
      context.font = '16px sans-serif';
      context.textAlign = 'center';
      context.fillText(`${joint.angle.toFixed(0)}°`, w / 2, h / 2 + 40);
    });
  };
}

// 训练指导组件
@Component
struct ExerciseGuide {
  @Prop exercise: RehabExercise;
  @Prop currentRep: number;
  @Prop formScore: number;

  build() {
    Column({ space: 16 }) {
      Text(this.exercise.name)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')

      Text(`目标: ${exercise.repetitions}次 | 保持${exercise.holdTime}`)
        .fontSize(14)
        .fontColor('rgba(255,255,255,0.7)')

      // 进度环
      Stack() {
        Circle()
          .width(120)
          .height(120)
          .stroke('rgba(255,255,255,0.2)')
          .strokeWidth(8)
          .fill('transparent')

        Circle()
          .width(120)
          .height(120)
          .stroke(this.currentRep >= this.exercise.repetitions ? '#4ADE80' : '#3B82F6')
          .strokeWidth(8)
          .fill('transparent')
          .strokeDashArray([
            (this.currentRep / this.exercise.repetitions) * 377, 
            377
          ])
          .rotate({ angle: -90, centerX: '50%', centerY: '50%' })

        Text(`${this.currentRep}/${this.exercise.repetitions}`)
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }

      // 动作质量
      Text(`动作质量: ${this.formScore.toFixed(0)}`)
        .fontSize(16)
        .fontColor(this.formScore > 80 ? '#4ADE80' : '#FBBF24')
    }
    .padding(30)
    .backgroundColor('rgba(0, 0, 0, 0.7)')
    .borderRadius(24)
    .backdropBlur(20)
  }
}

// 浮动数据面板组件
@Component
struct FloatDataPanel {
  @Prop motionHistory: MotionAnalysis[];
  @Prop painHistory: PainAssessment[];
  @Prop emotionHistory: EmotionState[];
  @Prop onClose: () => void;

  build() {
    Column({ space: 16 }) {
      Row() {
        Text('📊 康复数据')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Button('✕')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .backgroundColor('transparent')
          .onClick(this.onClose)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      // 疼痛趋势图
      Column({ space: 8 }) {
        Text('疼痛趋势')
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.8)')

        Canvas(this.drawPainTrend)
          .width('100%')
          .height(100)
          .backgroundColor('rgba(255,255,255,0.05)')
          .borderRadius(8)
      }

      // 活动度趋势
      Column({ space: 8 }) {
        Text('活动度趋势')
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.8)')

        Canvas(this.drawRomTrend)
          .width('100%')
          .height(100)
          .backgroundColor('rgba(255,255,255,0.05)')
          .borderRadius(8)
      }

      // 情绪分布
      Column({ space: 8 }) {
        Text('情绪状态')
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.8)')

        Row({ space: 12 }) {
          ForEach(this.getEmotionDistribution(), ([emotion, count]: [string, number]) => {
            Column({ space: 4 }) {
              Circle()
                .width(12)
                .height(12)
                .fill(this.getEmotionColor(emotion))

              Text(`${count}`)
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.6)')
            }
          })
        }
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('rgba(20, 20, 40, 0.85)')
    .borderRadius(20)
    .backdropBlur(20)
    .systemMaterialEffect(MaterialStyle.IMMERSIVE)
  }

  private drawPainTrend = (context: CanvasRenderingContext2D) => {
    const canvas = context.canvas;
    const w = canvas.width;
    const h = canvas.height;

    context.clearRect(0, 0, w, h);

    if (this.painHistory.length < 2) return;

    // 绘制疼痛趋势线
    context.beginPath();
    context.moveTo(0, h - (this.painHistory[0].level / 10) * h);

    this.painHistory.forEach((pain, index) => {
      const x = (index / (this.painHistory.length - 1)) * w;
      const y = h - (pain.level / 10) * h;
      context.lineTo(x, y);
    });

    context.strokeStyle = '#EF4444';
    context.lineWidth = 2;
    context.stroke();
  };

  private drawRomTrend = (context: CanvasRenderingContext2D) => {
    const canvas = context.canvas;
    const w = canvas.width;
    const h = canvas.height;

    context.clearRect(0, 0, w, h);

    if (this.motionHistory.length < 2) return;

    context.beginPath();
    context.moveTo(0, h - (this.motionHistory[0].rangeOfMotion / 100) * h);

    this.motionHistory.forEach((motion, index) => {
      const x = (index / (this.motionHistory.length - 1)) * w;
      const y = h - (motion.rangeOfMotion / 100) * h;
      context.lineTo(x, y);
    });

    context.strokeStyle = '#4ADE80';
    context.lineWidth = 2;
    context.stroke();
  };

  private getEmotionDistribution(): Map<string, number> {
    const distribution = new Map<string, number>();
    this.emotionHistory.forEach(e => {
      const count = distribution.get(e.primary) || 0;
      distribution.set(e.primary, count + 1);
    });
    return distribution;
  }

  private getEmotionColor(emotion: string): ResourceColor {
    const colors: Map<string, ResourceColor> = new Map([
      ['calm', '#4ADE80'],
      ['anxious', '#F59E0B'],
      ['painful', '#EF4444'],
      ['tired', '#6B7280'],
      ['motivated', '#EC4899'],
      ['frustrated', '#DC2626']
    ]);
    return colors.get(emotion) || '#888888';
  }
}

// 浮动医生面板组件
@Component
struct FloatDoctorPanel {
  @Prop doctorName: string;
  @Prop onClose: () => void;

  build() {
    Column({ space: 12 }) {
      Row() {
        Text(`👨‍⚕️ ${this.doctorName}`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Button('✕')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .backgroundColor('transparent')
          .onClick(this.onClose)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      // 医生视频区域(简化)
      Column()
        .width('100%')
        .height(200)
        .backgroundColor('rgba(255,255,255,0.1)')
        .borderRadius(12)
        .justifyContent(FlexAlign.Center)
        .overlay(
          Text('医生视频通话')
            .fontSize(14)
            .fontColor('rgba(255,255,255,0.5)')
        )

      // 快速消息
      Row({ space: 8 }) {
        Button('加油!')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('rgba(59, 130, 246, 0.5)')
          .borderRadius(16)

        Button('休息一下')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('rgba(34, 197, 94, 0.5)')
          .borderRadius(16)

        Button('调整强度')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('rgba(249, 115, 22, 0.5)')
          .borderRadius(16)
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('rgba(20, 20, 40, 0.85)')
    .borderRadius(20)
    .backdropBlur(20)
    .systemMaterialEffect(MaterialStyle.IMMERSIVE)
  }
}

五、关键技术总结

5.1 Face AR在康复中的适配清单

适配项 说明 代码位置
疼痛评估精度 综合皱眉、眯眼、鼻皱、咬牙等微表情 PainEmotionSystem.assessPain()
情绪稳定性分析 连续10帧表情一致性检测 PainEmotionSystem.assessEmotion()
疲劳度监测 眨眼频率 + 眼神呆滞检测 FaceExpression.eyeBlink
隐私保护 端侧处理,不上传云端 module.json5权限声明

5.2 Body AR动作追踪最佳实践

实践项 说明 代码位置
关节角度计算 向量夹角法计算关节活动度 RehabMotionSystem.measureJointAngle()
对称性评估 左右关节角度差异分析 RehabMotionSystem.calculateSymmetry()
重复次数检测 动作峰值检测 + 保持时间验证 RehabMotionSystem.countRepetitions()
安全范围警示 关节角度超出预设范围时告警 JointAngle.isSafe

5.3 沉浸光感康复适配要点

  1. 阶段色动态切换:急性期冷静蓝、恢复期愈合绿、强化期活力橙、维持期稳定白
  2. 情绪响应光效:焦虑时柔和紫蓝安抚、疼痛时淡粉缓解、积极时亮度提升
  3. 安全警示机制:动作超出安全范围时标题栏快速闪烁红色警示
  4. 护眼模式:训练超过30分钟或检测到疲劳时自动降低蓝光和亮度

六、调试与测试建议

6.1 AR性能监控

// 在康复循环中添加性能监控
const startTime = performance.now();
// ... AR处理逻辑
const processTime = performance.now() - startTime;
if (processTime > 33) {
  console.warn(`AR处理帧耗时${processTime.toFixed(1)}ms,存在掉帧风险`);
}

6.2 多窗口测试矩阵

测试场景 预期结果
主窗口全屏 + 浮动数据面板 标题栏光效同步,面板不遮挡训练画面
分屏模式(左训练右医生视频) 悬浮导航自动适配宽度,功能按钮不重叠
外接显示器扩展 多窗口可拖拽至副屏,光效状态同步
疼痛等级>7时 环境光效自动切换为安抚模式,亮度降低

6.3 常见问题排查

现象 原因 解决方案
疼痛评估不准确 摄像头角度导致面部遮挡 调整摄像头至正面平视角度
关节角度测量偏差 人体与摄像头距离过远 建议患者站在摄像头前1.5-2米处
光效切换闪烁 动画时长过短 调整duration至1500ms以上
多窗口光效不同步 AppStorage键名不一致 统一使用current_rehab_theme作为同步键

七、总结与展望

本文基于HarmonyOS 6(API 23)的悬浮导航沉浸光感Face AR & Body AR特性,完整实战了一款PC端"灵犀康养"智能康复系统。核心创新点总结:

  1. 阶段感知光效系统:根据康复阶段动态切换主题色,急性期冷静蓝缓解焦虑、恢复期愈合绿促进愈合、强化期活力橙激发动力、维持期稳定白保持状态
  2. Face AR疼痛评估:利用64种BlendShape参数实时评估疼痛等级(0-10分),识别疼痛类型(尖锐/钝痛/灼烧/酸痛/搏动性)
  3. 情绪驱动康复调节:检测到焦虑时自动切换安抚光效、检测到疼痛时降低训练强度、检测到积极情绪时适当提升挑战
  4. Body AR动作精准追踪:通过20+骨骼关键点追踪,实时测量关节角度、评估动作对称性、计数重复次数、评估动作质量
  5. 悬浮导航自适应:采用HdsTabs悬浮样式,四周留白,支持透明度三档调节,最大化训练画面区域
  6. PC级多窗口协作:主训练窗口 + 浮动动作指导 + 浮动数据面板 + 浮动医生视频 + 浮动家属关怀,通过WindowManager实现跨窗口光效联动

未来扩展方向

  • AI康复处方:结合患者历史数据,AI自动生成个性化康复方案
  • 分布式康复监护:通过鸿蒙分布式软总线,实现家中康复设备 → 社区康复中心 → 医院康复科的多级联动
  • 家属远程关怀:家属通过智慧屏实时查看患者康复状态,发送鼓励消息
  • 虚拟现实融合:结合VR头显,将康复训练转化为虚拟游戏场景,提升患者依从性
  • 康复大数据:汇聚匿名化康复数据,训练更精准的疼痛评估和动作识别模型

转载自:https://blog.csdn.net/u014727709/article/details/161020889
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐