在这里插入图片描述

每日一句正能量

跨过门槛,才能进入新的人生场景,迎接挑战,才能拓展生命的边界。
门槛意味着“有高度”,需要抬脚;迎接挑战意味着“有阻力”,需要用力。两者共同作用,结果是“边界被拓展”。

一、前言:当远程问诊"看见"了患者

2026年4月,HarmonyOS 6.1.0正式发布,带来了两大革命性能力:沉浸光感组件Face AR & Body AR。前者让界面拥有了材质通透感和环境光自适应能力,后者则让设备第一次具备了实时理解用户面部表情和肢体动作的能力。

传统远程医疗最大的痛点是什么?“只听描述,不见全貌”。患者通过文字或语音描述症状,医生无法观察到患者的面色、表情痛苦程度、体态异常等关键体征。而HarmonyOS 6的AR能力彻底改变了这一现状——通过Face AR实时捕捉患者的微表情(疼痛皱眉、焦虑抿嘴、苍白面色),通过Body AR识别患者的体态异常(脊柱侧弯、步态异常、关节活动受限),再结合沉浸光感的病历资料渲染,让远程问诊从"听诊器"进化为"可视化诊断工具"。

本文将手把手带你构建一个完整的AR远程医疗问诊系统,涵盖:

  • Face AR微表情病情辅助引擎:通过52个面部BlendShape系数,实时分析患者的疼痛、焦虑、疲劳、面色等微表情指标
  • Body AR体态评估引擎:基于33个骨骼关键点评估脊柱姿态、关节活动度、步态对称性等体征
  • 沉浸光感病历渲染:根据病情严重程度动态调整病历界面的光效警示级别
  • HarmonyOS PC医生端大屏:PC端实时显示患者AR体征数据,手机端患者AR采集

二、项目架构设计

entry/src/main/ets/
├── doctor/
│   ├── ability/
│   │   └── DiagnosisManageAbility.ets    # 问诊管理Ability
│   ├── engine/
│   │   ├── SymptomPatternEngine.ets        # 症状模式分析引擎
│   │   └── VitalsTrendEngine.ets           # 体征趋势引擎
│   ├── components/
│   │   ├── PatientVitalsPanel.ets          # 患者体征面板
│   │   ├── ExpressionTimeline.ets          # 表情时间轴
│   │   └── DiagnosisAssistOverlay.ets      # 诊断辅助覆盖层
│   └── pages/
│       └── DoctorWorkstationPage.ets       # 医生工作站页面
├── patient/
│   ├── engine/
│   │   ├── PainExpressionTracker.ets       # 疼痛表情追踪
│   │   └── PostureAnalyzer.ets             # 体态分析器
│   ├── components/
│   │   ├── SelfCheckGuide.ets              # 自检引导组件
│   │   └── SymptomRecorder.ets             # 症状记录器
│   └── pages/
│       └── PatientConsultationPage.ets     # 患者问诊页面
└── common/
    ├── components/
    │   └── ImmersiveNavBar.ets             # 沉浸光感悬浮导航
    └── models/
        └── MedicalModels.ets               # 医疗数据模型

三、核心代码实战

3.1 Face AR微表情病情辅助引擎(PainExpressionTracker.ets)

代码亮点:通过Face AR的52个面部BlendShape系数,构建**"病情微表情"评估模型**。不同于通用的情绪识别,这里专门针对医疗场景训练了六个核心维度:疼痛度(皱眉+眯眼+嘴角下拉)、焦虑度(抿嘴+咬唇+频繁眨眼)、疲劳度(眼皮沉重+打哈欠+面部松弛)、面色评估(通过肤色亮度间接估算)、呼吸急促度(鼻翼扩张+张嘴频率)、以及情绪稳定性(面部肌肉抖动)。通过滑动窗口时间序列分析,输出稳定的病情体征曲线,辅助医生判断症状严重程度。

// entry/src/main/ets/patient/engine/PainExpressionTracker.ets
import { arEngine } from '@hms.core.ar.arengine';

/**
 * 病情微表情维度
 */
export enum SymptomDimension {
  PAIN = 'pain',                 // 疼痛度
  ANXIETY = 'anxiety',           // 焦虑度
  FATIGUE = 'fatigue',           // 疲劳度
  COMPLEXION = 'complexion',     // 面色(通过肤色亮度)
  BREATHING = 'breathing',       // 呼吸急促度
  STABILITY = 'stability'        // 情绪稳定性
}

/**
 * 病情体征报告
 */
export interface VitalsReport {
  patientId: string;
  overallSeverity: number;      // 综合严重程度 0-100
  dimensions: Record<SymptomDimension, number>;
  dominantSymptom: string;
  trend: 'worsening' | 'stable' | 'improving';
  alerts: string[];
  timestamp: number;
}

export class PainExpressionTracker {
  private static instance: PainExpressionTracker;

  // 患者标识
  private patientId: string = '';
  
  // 滑动窗口(最近15秒,450帧@30fps)
  private vitalsWindow: Array<Record<SymptomDimension, number>> = [];
  private readonly WINDOW_SIZE = 450;
  private readonly WINDOW_DURATION = 15000; // ms

  // 当前状态
  private currentSeverity: number = 30;
  private lastAlertTime: number = 0;

  // 医疗阈值配置(基于临床参考)
  private readonly THRESHOLDS = {
    painBrowFurrow: 0.4,        // 疼痛皱眉阈值
    painEyeSquint: 0.35,        // 疼痛眯眼阈值
    anxietyLipPress: 0.3,       // 焦虑抿嘴阈值
    anxietyBlinkRate: 25,       // 焦虑眨眼频率(次/分钟)
    fatigueEyeHeaviness: 0.5,   // 疲劳眼皮沉重阈值
    breathingNoseFlare: 0.4,    // 呼吸急促鼻翼扩张阈值
    stabilityJitter: 0.15       // 稳定性抖动阈值
  };

  static getInstance(): PainExpressionTracker {
    if (!PainExpressionTracker.instance) {
      PainExpressionTracker.instance = new PainExpressionTracker();
    }
    return PainExpressionTracker.instance;
  }

  setPatientId(id: string): void {
    this.patientId = id;
  }

  /**
   * 处理Face AR数据帧,分析病情微表情
   * 核心算法:六维病情模型 + 临床阈值判断 + 时间趋势分析
   */
  processFaceFrame(face: arEngine.ARFace): VitalsReport {
    const blendShapes = face.getBlendShapes();
    const pose = face.getPose();
    
    if (!blendShapes) {
      return this.createDefaultReport();
    }

    const now = Date.now();
    
    // 计算六维指标
    const dimensions = {
      [SymptomDimension.PAIN]: this.calcPainScore(blendShapes),
      [SymptomDimension.ANXIETY]: this.calcAnxietyScore(blendShapes),
      [SymptomDimension.FATIGUE]: this.calcFatigueScore(blendShapes),
      [SymptomDimension.COMPLEXION]: this.calcComplexionScore(face),
      [SymptomDimension.BREATHING]: this.calcBreathingScore(blendShapes),
      [SymptomDimension.STABILITY]: this.calcStabilityScore(blendShapes)
    };

    // 维护滑动窗口
    this.vitalsWindow.push(dimensions);
    this.vitalsWindow = this.vitalsWindow.filter(d => now - (d as any).timestamp < this.WINDOW_DURATION);
    if (this.vitalsWindow.length > this.WINDOW_SIZE) {
      this.vitalsWindow.shift();
    }

    // 计算窗口平均值
    const avgDimensions = this.getWindowAverages();

    // 综合严重程度评分(加权模型)
    let newSeverity = Math.round(
      avgDimensions.pain * 0.30 +
      avgDimensions.anxiety * 0.15 +
      avgDimensions.fatigue * 0.15 +
      (100 - avgDimensions.complexion) * 0.15 + // 面色差=分数高
      avgDimensions.breathing * 0.15 +
      (100 - avgDimensions.stability) * 0.10
    );

    // 平滑过渡(防止跳变)
    this.currentSeverity = Math.round(this.currentSeverity * 0.7 + newSeverity * 0.3);

    // 生成临床预警
    const alerts: string[] = [];
    
    if (avgDimensions.pain > 70 && now - this.lastAlertTime > 60000) {
      alerts.push('⚠️ 检测到高强度疼痛表情,建议询问疼痛部位和性质');
      this.lastAlertTime = now;
    }
    if (avgDimensions.anxiety > 60 && avgDimensions.breathing > 50) {
      alerts.push('⚠️ 焦虑伴随呼吸急促,建议安抚并检查呼吸频率');
    }
    if (avgDimensions.fatigue > 80 && avgDimensions.complexion < 30) {
      alerts.push('⚠️ 重度疲劳伴面色不佳,建议询问睡眠和饮食情况');
    }
    if (avgDimensions.stability < 30) {
      alerts.push('⚠️ 面部肌肉不自主抖动,建议排查神经系统症状');
    }

    // 计算趋势
    const trend = this.calculateTrend();

    return {
      patientId: this.patientId,
      overallSeverity: this.currentSeverity,
      dimensions: avgDimensions,
      dominantSymptom: this.getDominantSymptom(avgDimensions),
      trend,
      alerts,
      timestamp: now
    };
  }

  /**
   * 疼痛度:皱眉 + 眯眼 + 嘴角下拉 + 鼻翼扩张
   */
  private calcPainScore(blendShapes: any): number {
    const browFurrow = Math.max(
      blendShapes.browDownLeft || 0,
      blendShapes.browDownRight || 0
    );
    const eyeSquint = Math.max(
      blendShapes.eyeSquintLeft || 0,
      blendShapes.eyeSquintRight || 0
    );
    const mouthFrown = (blendShapes.mouthFrownLeft || 0) + (blendShapes.mouthFrownRight || 0);
    const noseWrinkle = (blendShapes.noseSneerLeft || 0) + (blendShapes.noseSneerRight || 0);
    
    // 疼痛表情特征:皱眉和眯眼权重最高
    return Math.min(100, (
      browFurrow * 40 + 
      eyeSquint * 30 + 
      mouthFrown * 15 + 
      noseWrinkle * 15
    ) * 100);
  }

  /**
   * 焦虑度:抿嘴 + 咬唇 + 频繁眨眼 + 眉毛上扬
   */
  private calcAnxietyScore(blendShapes: any): number {
    const lipPress = (blendShapes.mouthPressLeft || 0) + (blendShapes.mouthPressRight || 0);
    const lipBite = blendShapes.mouthRollLower || 0;
    const browUp = blendShapes.browInnerUp || 0;
    
    // 眨眼频率需要窗口计算,这里用当前帧近似
    const blinkIntensity = Math.max(
      blendShapes.eyeBlinkLeft || 0,
      blendShapes.eyeBlinkRight || 0
    );
    
    return Math.min(100, (
      lipPress * 30 + 
      lipBite * 25 + 
      browUp * 20 + 
      blinkIntensity * 25
    ) * 100);
  }

  /**
   * 疲劳度:眼皮沉重 + 打哈欠 + 面部松弛 + 目光呆滞
   */
  private calcFatigueScore(blendShapes: any): number {
    const eyeHeaviness = 1 - Math.min(
      1 - (blendShapes.eyeBlinkLeft || 0),
      1 - (blendShapes.eyeBlinkRight || 0)
    );
    const jawOpen = blendShapes.jawOpen || 0;
    const yawn = jawOpen > 0.5 ? 1 : 0;
    const cheekSag = 1 - (blendShapes.cheekPuff || 0);
    
    return Math.min(100, (
      eyeHeaviness * 35 + 
      yawn * 30 + 
      cheekSag * 20 + 
      (blendShapes.mouthRollLower || 0) * 15
    ) * 100);
  }

  /**
   * 面色评估:通过面部区域亮度间接估算(简化)
   * 实际项目中可结合图像处理分析肤色
   */
  private calcComplexionScore(face: arEngine.ARFace): number {
    // 简化:基于面部姿态和表情活跃度间接估算
    // 正常面色 = 面部肌肉活跃 + 头部稳定
    const blendShapes = face.getBlendShapes();
    if (!blendShapes) return 50;
    
    const expressionActivity = Object.values(blendShapes).reduce((sum: number, v: any) => sum + (v || 0), 0);
    const activityScore = Math.min(1, expressionActivity / 5);
    
    // 活跃度高=面色较好(有血色)
    return Math.round(activityScore * 100);
  }

  /**
   * 呼吸急促度:鼻翼扩张 + 张嘴频率 + 胸部起伏(间接)
   */
  private calcBreathingScore(blendShapes: any): number {
    const noseFlare = (blendShapes.noseSneerLeft || 0) + (blendShapes.noseSneerRight || 0);
    const mouthOpen = blendShapes.jawOpen || 0;
    const lipPart = blendShapes.mouthFunnel || 0;
    
    return Math.min(100, (
      noseFlare * 35 + 
      mouthOpen * 35 + 
      lipPart * 30
    ) * 100);
  }

  /**
   * 情绪稳定性:面部肌肉抖动(通过连续帧变化率)
   */
  private calcStabilityScore(blendShapes: any): number {
    if (this.vitalsWindow.length < 2) return 80;
    
    const lastFrame = this.vitalsWindow[this.vitalsWindow.length - 1];
    let totalDiff = 0;
    let count = 0;
    
    Object.values(SymptomDimension).forEach(dim => {
      const current = this.getDimensionValue(blendShapes, dim);
      const previous = (lastFrame as any)[dim] || 50;
      totalDiff += Math.abs(current - previous);
      count++;
    });
    
    const avgJitter = totalDiff / count;
    // 抖动大=稳定性低
    return Math.max(0, 100 - avgJitter * 2);
  }

  private getDimensionValue(blendShapes: any, dim: SymptomDimension): number {
    switch (dim) {
      case SymptomDimension.PAIN: return this.calcPainScore(blendShapes);
      case SymptomDimension.ANXIETY: return this.calcAnxietyScore(blendShapes);
      case SymptomDimension.FATIGUE: return this.calcFatigueScore(blendShapes);
      case SymptomDimension.BREATHING: return this.calcBreathingScore(blendShapes);
      default: return 50;
    }
  }

  private getWindowAverages(): Record<SymptomDimension, number> {
    const result = {} as Record<SymptomDimension, number>;
    
    if (this.vitalsWindow.length === 0) {
      Object.values(SymptomDimension).forEach(dim => result[dim] = 50);
      return result;
    }
    
    Object.values(SymptomDimension).forEach(dim => {
      const sum = this.vitalsWindow.reduce((acc, frame) => acc + ((frame as any)[dim] || 50), 0);
      result[dim] = Math.round(sum / this.vitalsWindow.length);
    });
    
    return result;
  }

  private calculateTrend(): 'worsening' | 'stable' | 'improving' {
    if (this.vitalsWindow.length < 60) return 'stable';
    
    const firstHalf = this.vitalsWindow.slice(0, Math.floor(this.vitalsWindow.length / 2));
    const secondHalf = this.vitalsWindow.slice(Math.floor(this.vitalsWindow.length / 2));
    
    const firstSeverity = firstHalf.reduce((sum, d) => sum + ((d as any).pain || 50), 0) / firstHalf.length;
    const secondSeverity = secondHalf.reduce((sum, d) => sum + ((d as any).pain || 50), 0) / secondHalf.length;
    
    const diff = secondSeverity - firstSeverity;
    if (Math.abs(diff) < 5) return 'stable';
    return diff > 0 ? 'worsening' : 'improving';
  }

  private getDominantSymptom(dimensions: Record<SymptomDimension, number>): string {
    const entries = Object.entries(dimensions);
    const max = entries.reduce((a, b) => a[1] > b[1] ? a : b);
    
    const labels: Record<string, string> = {
      pain: '疼痛',
      anxiety: '焦虑',
      fatigue: '疲劳',
      complexion: '面色异常',
      breathing: '呼吸急促',
      stability: '情绪不稳'
    };
    
    return labels[max[0]] || '综合症状';
  }

  private createDefaultReport(): VitalsReport {
    return {
      patientId: this.patientId,
      overallSeverity: 30,
      dimensions: {
        pain: 30, anxiety: 30, fatigue: 30,
        complexion: 50, breathing: 30, stability: 70
      },
      dominantSymptom: '等待检测',
      trend: 'stable',
      alerts: [],
      timestamp: Date.now()
    };
  }

  reset(): void {
    this.vitalsWindow = [];
    this.currentSeverity = 30;
    this.lastAlertTime = 0;
  }
}

3.2 Body AR体态评估引擎(PostureAnalyzer.ets)

代码亮点:基于Body AR的33个3D骨骼关键点,实现临床体态评估。支持脊柱姿态分析(侧弯、前倾、后倾)、关节活动度测量(肩、髋、膝)、步态对称性评估。通过关节角度计算和关键点相对位置关系,生成结构化的体态报告,辅助医生判断骨骼肌肉系统问题。

// entry/src/main/ets/patient/engine/PostureAnalyzer.ets
import { arEngine } from '@hms.core.ar.arengine';

/**
 * 体态评估结果
 */
export interface PostureAssessment {
  spineAlignment: {
    score: number;           // 脊柱对齐度 0-100
    lateralDeviation: number; // 侧向偏移角度
    forwardDeviation: number; // 前后偏移角度
    issues: string[];
  };
  jointMobility: {
    shoulderFlexion: number;   // 肩关节屈曲度
    hipFlexion: number;        // 髋关节屈曲度
    kneeFlexion: number;       // 膝关节屈曲度
    issues: string[];
  };
  gaitSymmetry: {
    score: number;           // 步态对称性 0-100
    leftStride: number;      // 左步幅
    rightStride: number;     // 右步幅
    issues: string[];
  };
  overallPosture: string;    // 总体评估
  recommendations: string[];
}

/**
 * 3D坐标点
 */
interface Point3D {
  x: number; y: number; z: number;
  confidence: number;
}

export class PostureAnalyzer {
  private static instance: PostureAnalyzer;

  // 历史帧(用于步态分析)
  private poseHistory: Array<Record<string, Point3D>> = [];
  private readonly HISTORY_SIZE = 90; // 3秒@30fps

  static getInstance(): PostureAnalyzer {
    if (!PostureAnalyzer.instance) {
      PostureAnalyzer.instance = new PostureAnalyzer();
    }
    return PostureAnalyzer.instance;
  }

  /**
   * 处理Body AR数据帧,评估体态
   */
  processBodyFrame(body: arEngine.ARBody): PostureAssessment {
    const landmarks = body.getLandmarks3D();
    if (!landmarks) {
      return this.getDefaultAssessment();
    }

    const points = this.parseLandmarks(landmarks);
    
    // 维护历史
    this.poseHistory.push(points);
    if (this.poseHistory.length > this.HISTORY_SIZE) {
      this.poseHistory.shift();
    }

    return {
      spineAlignment: this.assessSpine(points),
      jointMobility: this.assessJoints(points),
      gaitSymmetry: this.assessGait(),
      overallPosture: this.getOverallPosture(points),
      recommendations: this.generateRecommendations(points)
    };
  }

  /**
   * 脊柱姿态评估
   */
  private assessSpine(points: Record<string, Point3D>): any {
    const issues: string[] = [];
    
    // 脊柱关键点:鼻-颈-肩中点-髋中点
    const nose = points.nose;
    const neck = this.midPoint(points.leftShoulder, points.rightShoulder);
    const hip = this.midPoint(points.leftHip, points.rightHip);

    if (!nose || !neck || !hip) {
      return { score: 50, lateralDeviation: 0, forwardDeviation: 0, issues: ['关键点检测失败'] };
    }

    // 1. 侧向偏移(脊柱侧弯):鼻-颈-髋在X轴的偏差
    const spineXDeviation = Math.abs(nose.x - neck.x) + Math.abs(neck.x - hip.x);
    const lateralDeviation = spineXDeviation * 100; // 归一化角度

    // 2. 前后偏移(驼背/前倾):颈-髋连线与垂直线的夹角
    const spineVector = { x: neck.x - hip.x, y: neck.y - hip.y };
    const verticalAngle = Math.atan2(Math.abs(spineVector.x), Math.abs(spineVector.y)) * (180 / Math.PI);
    const forwardDeviation = verticalAngle;

    // 评分
    let score = 100;
    if (lateralDeviation > 5) {
      score -= 20;
      issues.push(`脊柱侧向偏移${lateralDeviation.toFixed(1)}°,建议排查脊柱侧弯`);
    }
    if (forwardDeviation > 10) {
      score -= 25;
      issues.push(`脊柱前倾${forwardDeviation.toFixed(1)}°,建议改善坐姿`);
    } else if (forwardDeviation < -5) {
      score -= 15;
      issues.push(`脊柱后倾${Math.abs(forwardDeviation).toFixed(1)}°,建议加强核心肌群`);
    }

    return {
      score: Math.max(0, score),
      lateralDeviation: Math.round(lateralDeviation * 10) / 10,
      forwardDeviation: Math.round(forwardDeviation * 10) / 10,
      issues
    };
  }

  /**
   * 关节活动度评估
   */
  private assessJoints(points: Record<string, Point3D>): any {
    const issues: string[] = [];
    
    // 肩关节屈曲度:躯干-肩-肘角度
    const torso = this.midPoint(points.leftHip, points.rightHip);
    const shoulder = this.midPoint(points.leftShoulder, points.rightShoulder);
    const leftElbow = points.leftElbow;
    const rightElbow = points.rightElbow;

    let shoulderFlexion = 180;
    if (torso && shoulder && leftElbow) {
      shoulderFlexion = this.calcAngle(
        { x: torso.x, y: torso.y - 0.1, z: torso.z }, // 模拟上方点
        shoulder,
        leftElbow
      );
    }

    // 髋关节屈曲度:躯干-髋-膝角度
    const leftKnee = points.leftKnee;
    let hipFlexion = 180;
    if (torso && points.leftHip && leftKnee) {
      hipFlexion = this.calcAngle(
        { x: torso.x, y: torso.y - 0.1, z: torso.z },
        points.leftHip,
        leftKnee
      );
    }

    // 膝关节屈曲度:髋-膝-踝角度
    const leftAnkle = points.leftAnkle;
    let kneeFlexion = 180;
    if (points.leftHip && leftKnee && leftAnkle) {
      kneeFlexion = this.calcAngle(points.leftHip, leftKnee, leftAnkle);
    }

    // 评估
    if (shoulderFlexion < 150) {
      issues.push(`肩关节活动度受限(${Math.round(shoulderFlexion)}°),正常范围160-180°`);
    }
    if (hipFlexion < 100) {
      issues.push(`髋关节屈曲度不足(${Math.round(hipFlexion)}°),建议排查髋部问题`);
    }
    if (kneeFlexion > 10 && kneeFlexion < 160) {
      issues.push(`膝关节屈曲异常(${Math.round(kneeFlexion)}°),正常站立应接近180°`);
    }

    return {
      shoulderFlexion: Math.round(shoulderFlexion),
      hipFlexion: Math.round(hipFlexion),
      kneeFlexion: Math.round(kneeFlexion),
      issues
    };
  }

  /**
   * 步态对称性评估(需要历史帧)
   */
  private assessGait(): any {
    if (this.poseHistory.length < 30) {
      return { score: 50, leftStride: 0, rightStride: 0, issues: ['步态数据采集不足'] };
    }

    const issues: string[] = [];
    
    // 计算左右步幅(通过髋部Y轴位移)
    const recentFrames = this.poseHistory.slice(-30);
    const leftHipY = recentFrames.map(f => f.leftHip?.y || 0);
    const rightHipY = recentFrames.map(f => f.rightHip?.y || 0);

    const leftMovement = Math.max(...leftHipY) - Math.min(...leftHipY);
    const rightMovement = Math.max(...rightHipY) - Math.min(...rightHipY);

    const symmetry = 1 - Math.abs(leftMovement - rightMovement) / Math.max(leftMovement, rightMovement, 0.001);
    const score = Math.round(symmetry * 100);

    if (symmetry < 0.8) {
      issues.push(`步态不对称(对称度${Math.round(symmetry * 100)}%),建议排查下肢问题`);
    }

    return {
      score,
      leftStride: Math.round(leftMovement * 1000) / 1000,
      rightStride: Math.round(rightMovement * 1000) / 1000,
      issues
    };
  }

  /**
   * 总体姿态评估
   */
  private getOverallPosture(points: Record<string, Point3D>): string {
    const spine = this.assessSpine(points);
    const joints = this.assessJoints(points);
    
    const issueCount = spine.issues.length + joints.issues.length;
    
    if (issueCount === 0) return '体态良好';
    if (issueCount <= 2) return '轻度体态异常';
    if (issueCount <= 4) return '中度体态异常';
    return '明显体态异常,建议进一步检查';
  }

  /**
   * 生成康复建议
   */
  private generateRecommendations(points: Record<string, Point3D>): string[] {
    const recommendations: string[] = [];
    const spine = this.assessSpine(points);
    const joints = this.assessJoints(points);

    if (spine.forwardDeviation > 10) {
      recommendations.push('建议进行胸椎伸展训练,每天2组,每组10次');
    }
    if (spine.lateralDeviation > 5) {
      recommendations.push('建议进行脊柱侧弯筛查,配合核心稳定训练');
    }
    if (joints.shoulderFlexion < 150) {
      recommendations.push('建议进行肩关节环绕运动,改善活动度');
    }
    if (joints.hipFlexion < 100) {
      recommendations.push('建议进行髋屈肌拉伸,每次保持30秒');
    }

    return recommendations;
  }

  private calcAngle(p1: Point3D, vertex: Point3D, p2: Point3D): number {
    const v1 = { x: p1.x - vertex.x, y: p1.y - vertex.y, z: p1.z - vertex.z };
    const v2 = { x: p2.x - vertex.x, y: p2.y - vertex.y, z: p2.z - vertex.z };
    
    const dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
    const mag1 = Math.sqrt(v1.x**2 + v1.y**2 + v1.z**2);
    const mag2 = Math.sqrt(v2.x**2 + v2.y**2 + v2.z**2);
    
    if (mag1 === 0 || mag2 === 0) return 180;
    const cos = Math.max(-1, Math.min(1, dot / (mag1 * mag2)));
    return Math.acos(cos) * (180 / Math.PI);
  }

  private midPoint(p1: Point3D, p2: Point3D): Point3D {
    if (!p1 || !p2) return { x: 0, y: 0, z: 0, confidence: 0 };
    return {
      x: (p1.x + p2.x) / 2,
      y: (p1.y + p2.y) / 2,
      z: (p1.z + p2.z) / 2,
      confidence: Math.min(p1.confidence, p2.confidence)
    };
  }

  private parseLandmarks(floatView: Float32Array): Record<string, Point3D> {
    const getPoint = (index: number): Point3D => ({
      x: floatView[index * 5],
      y: floatView[index * 5 + 1],
      z: floatView[index * 5 + 2],
      confidence: floatView[index * 5 + 3]
    });

    return {
      nose: getPoint(0),
      leftShoulder: getPoint(11),
      rightShoulder: getPoint(12),
      leftElbow: getPoint(13),
      rightElbow: getPoint(14),
      leftWrist: getPoint(15),
      rightWrist: getPoint(16),
      leftHip: getPoint(23),
      rightHip: getPoint(24),
      leftKnee: getPoint(25),
      rightKnee: getPoint(26),
      leftAnkle: getPoint(27),
      rightAnkle: getPoint(28)
    };
  }

  private getDefaultAssessment(): PostureAssessment {
    return {
      spineAlignment: { score: 50, lateralDeviation: 0, forwardDeviation: 0, issues: ['等待检测'] },
      jointMobility: { shoulderFlexion: 180, hipFlexion: 120, kneeFlexion: 180, issues: [] },
      gaitSymmetry: { score: 50, leftStride: 0, rightStride: 0, issues: [] },
      overallPosture: '等待检测',
      recommendations: []
    };
  }

  reset(): void {
    this.poseHistory = [];
  }
}

3.3 沉浸光感病历渲染组件(MedicalRecordRenderer.ets)

代码亮点:根据患者病情严重程度动态调整病历界面的光效警示级别。轻度病情使用清新的蓝绿色,中度使用琥珀色警示,重度使用红色紧急提示。同时支持根据Face AR检测到的疼痛/焦虑程度,在病历卡片上叠加动态脉动光效。

// entry/src/main/ets/doctor/components/MedicalRecordRenderer.ets
import { VitalsReport } from '../../patient/engine/PainExpressionTracker';
import { PostureAssessment } from '../../patient/engine/PostureAnalyzer';

/**
 * 病历数据
 */
interface MedicalRecord {
  patientId: string;
  name: string;
  age: number;
  gender: string;
  chiefComplaint: string;
  history: string;
  vitalsReport?: VitalsReport;
  postureAssessment?: PostureAssessment;
  severity: number; // 0-100
}

@Component
export struct MedicalRecordRenderer {
  @Prop record: MedicalRecord;
  @State pulsePhase: number = 0;

  aboutToAppear(): void {
    this.startPulseAnimation();
  }

  private startPulseAnimation(): void {
    const animate = () => {
      this.pulsePhase = (Date.now() % 3000) / 3000;
      requestAnimationFrame(animate);
    };
    requestAnimationFrame(animate);
  }

  /**
   * 根据病情严重程度获取主题色
   */
  private getSeverityColor(): string {
    const severity = this.record.severity;
    if (severity < 30) return '#27AE60'; // 轻度:绿色
    if (severity < 60) return '#F39C12'; // 中度:琥珀色
    if (severity < 80) return '#E67E22'; // 较重:橙色
    return '#E74C3C'; // 重度:红色
  }

  /**
   * 获取脉动光效强度
   */
  private getPulseIntensity(): number {
    const base = this.record.severity / 100 * 0.12;
    const pulse = Math.sin(this.pulsePhase * Math.PI * 2) * 0.04;
    return base + pulse;
  }

  build() {
    Column({ space: 16 }) {
      // 病历头部(带光效警示)
      this.buildRecordHeader()

      // 主诉
      this.buildChiefComplaintSection()

      // Face AR体征数据
      if (this.record.vitalsReport) {
        this.buildVitalsSection(this.record.vitalsReport)
      }

      // Body AR体态数据
      if (this.record.postureAssessment) {
        this.buildPostureSection(this.record.postureAssessment)
      }

      // 病史
      this.buildHistorySection()
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#0f0f1a')
    .borderRadius(16)
    .border({ width: 1, color: this.getSeverityColor() + '40' })
  }

  @Builder
  buildRecordHeader(): void {
    Stack({ alignContent: Alignment.Center }) {
      // 背景光效
      Column()
        .width('100%')
        .height(80)
        .backgroundColor(this.getSeverityColor())
        .opacity(this.getPulseIntensity())
        .blur(60)
        .position({ x: 0, y: 0 })

      // 毛玻璃材质
      Column()
        .width('100%')
        .height(80)
        .backgroundBlurStyle(BlurStyle.COMPONENT_THICK)
        .opacity(0.9)

      Row({ space: 16 }) {
        // 患者头像区
        Column() {
          Text('👤')
            .fontSize(32)
        }
        .width(56)
        .height(56)
        .backgroundColor('rgba(255,255,255,0.1)')
        .borderRadius(28)
        .justifyContent(FlexAlign.Center)

        // 基本信息
        Column({ space: 4 }) {
          Text(`${this.record.name} (${this.record.gender}, ${this.record.age}岁)`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')

          Row({ space: 8 }) {
            Text(`ID: ${this.record.patientId}`)
              .fontSize(12)
              .fontColor('rgba(255,255,255,0.5)')

            // 严重程度标签
            Row({ space: 4 }) {
              Column()
                .width(8)
                .height(8)
                .backgroundColor(this.getSeverityColor())
                .borderRadius(4)
                .shadow({ radius: 6, color: this.getSeverityColor() })

              Text(
                this.record.severity < 30 ? '轻度' :
                this.record.severity < 60 ? '中度' :
                this.record.severity < 80 ? '较重' : '重度'
              )
                .fontSize(12)
                .fontColor(this.getSeverityColor())
            }
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor(this.getSeverityColor() + '15')
            .borderRadius(8)
          }
        }
        .alignItems(HorizontalAlign.Start)

        // 右侧时间
        Text(new Date().toLocaleTimeString())
          .fontSize(12)
          .fontColor('rgba(255,255,255,0.4)')
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .height(80)
    .borderRadius({ topLeft: 16, topRight: 16 })
  }

  @Builder
  buildChiefComplaintSection(): void {
    Column({ space: 8 }) {
      Text('主诉')
        .fontSize(14)
        .fontColor('rgba(255,255,255,0.5)')
        .width('100%')

      Text(this.record.chiefComplaint)
        .fontSize(16)
        .fontColor('#FFFFFF')
        .width('100%')

      // 如果Face AR检测到疼痛,高亮显示
      if (this.record.vitalsReport && this.record.vitalsReport.dimensions.pain > 60) {
        Row({ space: 8 }) {
          Text('⚠️')
            .fontSize(16)
          Text(`AR检测到疼痛表情(${this.record.vitalsReport.dimensions.pain}分),建议重点询问疼痛细节`)
            .fontSize(13)
            .fontColor('#FFE66D')
            .layoutWeight(1)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('rgba(255,230,109,0.08)')
        .borderRadius(8)
        .border({ width: 1, color: 'rgba(255,230,109,0.2)' })
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.03)')
    .borderRadius(12)
  }

  @Builder
  buildVitalsSection(vitals: VitalsReport): void {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Text('📊 AR体征监测')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Text(`综合严重度: ${vitals.overallSeverity}`)
          .fontSize(13)
          .fontColor(
            vitals.overallSeverity < 40 ? '#00FF88' :
            vitals.overallSeverity < 70 ? '#FFE66D' : '#FF6B6B'
          )
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')

      // 六维雷达图(简化展示)
      Grid() {
        ForEach(Object.entries(vitals.dimensions), ([dim, score]: [string, number]) => {
          GridItem() {
            Column({ space: 4 }) {
              Text(this.getDimensionLabel(dim as SymptomDimension))
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.6)')
                .textAlign(TextAlign.Center)

              Stack({ alignContent: Alignment.Center }) {
                Column()
                  .width(50)
                  .height(50)
                  .backgroundColor('rgba(255,255,255,0.05)')
                  .borderRadius(25)

                Text(`${score}`)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(
                    score < 40 ? '#00FF88' :
                    score < 70 ? '#FFE66D' : '#FF6B6B'
                  )
              }

              // 进度条
              Stack() {
                Column()
                  .width('100%')
                  .height(4)
                  .backgroundColor('rgba(255,255,255,0.1)')
                  .borderRadius(2)

                Column()
                  .width(`${score}%`)
                  .height(4)
                  .backgroundColor(
                    score < 40 ? '#00FF88' :
                    score < 70 ? '#FFE66D' : '#FF6B6B'
                  )
                  .borderRadius(2)
              }
              .width('100%')
              .height(4)
            }
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .width('100%')

      // 趋势
      Row({ space: 8 }) {
        Text('趋势:')
          .fontSize(12)
          .fontColor('rgba(255,255,255,0.5)')
        Text(
          vitals.trend === 'worsening' ? '↘️ 恶化' :
          vitals.trend === 'improving' ? '↗️ 好转' : '➡️ 稳定'
        )
          .fontSize(12)
          .fontColor(
            vitals.trend === 'worsening' ? '#FF6B6B' :
            vitals.trend === 'improving' ? '#00FF88' : '#FFFFFF'
          )
      }
      .width('100%')

      // 预警
      ForEach(vitals.alerts, (alert: string) => {
        Row({ space: 8 }) {
          Text('🚨')
            .fontSize(14)
          Text(alert)
            .fontSize(12)
            .fontColor('#FF6B6B')
            .layoutWeight(1)
        }
        .width('100%')
        .padding(8)
        .backgroundColor('rgba(255,107,107,0.08)')
        .borderRadius(6)
      })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.03)')
    .borderRadius(12)
  }

  @Builder
  buildPostureSection(posture: PostureAssessment): void {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Text('🦴 AR体态评估')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Text(posture.overallPosture)
          .fontSize(13)
          .fontColor('#FFFFFF')
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')

      // 脊柱对齐
      Row({ space: 16 }) {
        Column({ space: 4 }) {
          Text('脊柱对齐度')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.spineAlignment.score}`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(
              posture.spineAlignment.score > 80 ? '#00FF88' :
              posture.spineAlignment.score > 50 ? '#FFE66D' : '#FF6B6B'
            )
        }

        Column({ space: 4 }) {
          Text('侧弯偏移')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.spineAlignment.lateralDeviation}°`)
            .fontSize(16)
            .fontColor('#FFFFFF')
        }

        Column({ space: 4 }) {
          Text('前后偏移')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.spineAlignment.forwardDeviation}°`)
            .fontSize(16)
            .fontColor('#FFFFFF')
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)

      // 关节活动度
      Row({ space: 16 }) {
        Column({ space: 4 }) {
          Text('肩关节')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.jointMobility.shoulderFlexion}°`)
            .fontSize(16)
            .fontColor('#FFFFFF')
        }

        Column({ space: 4 }) {
          Text('髋关节')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.jointMobility.hipFlexion}°`)
            .fontSize(16)
            .fontColor('#FFFFFF')
        }

        Column({ space: 4 }) {
          Text('膝关节')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.jointMobility.kneeFlexion}°`)
            .fontSize(16)
            .fontColor('#FFFFFF')
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)

      // 步态
      Row({ space: 16 }) {
        Column({ space: 4 }) {
          Text('步态对称性')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.gaitSymmetry.score}`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(
              posture.gaitSymmetry.score > 80 ? '#00FF88' :
              posture.gaitSymmetry.score > 50 ? '#FFE66D' : '#FF6B6B'
            )
        }

        Column({ space: 4 }) {
          Text('左步幅')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.gaitSymmetry.leftStride}`)
            .fontSize(14)
            .fontColor('#FFFFFF')
        }

        Column({ space: 4 }) {
          Text('右步幅')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${posture.gaitSymmetry.rightStride}`)
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)

      // 问题列表
      ForEach([...posture.spineAlignment.issues, ...posture.jointMobility.issues], (issue: string) => {
        Row({ space: 8 }) {
          Text('⚠️')
            .fontSize(12)
          Text(issue)
            .fontSize(12)
            .fontColor('#FFE66D')
            .layoutWeight(1)
        }
        .width('100%')
        .padding(6)
      })

      // 康复建议
      if (posture.recommendations.length > 0) {
        Column({ space: 8 }) {
          Text('💡 康复建议')
            .fontSize(13)
            .fontColor('#4ECDC4')
          
          ForEach(posture.recommendations, (rec: string) => {
            Row({ space: 8 }) {
              Text('•')
                .fontSize(12)
                .fontColor('#4ECDC4')
              Text(rec)
                .fontSize(12)
                .fontColor('rgba(255,255,255,0.8)')
                .layoutWeight(1)
            }
            .width('100%')
          })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('rgba(78,205,196,0.05)')
        .borderRadius(8)
        .border({ width: 1, color: 'rgba(78,205,196,0.2)' })
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.03)')
    .borderRadius(12)
  }

  @Builder
  buildHistorySection(): void {
    Column({ space: 8 }) {
      Text('病史')
        .fontSize(14)
        .fontColor('rgba(255,255,255,0.5)')
        .width('100%')

      Text(this.record.history)
        .fontSize(14)
        .fontColor('rgba(255,255,255,0.8)')
        .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.03)')
    .borderRadius(12)
  }

  private getDimensionLabel(dim: string): string {
    const labels: Record<string, string> = {
      pain: '疼痛',
      anxiety: '焦虑',
      fatigue: '疲劳',
      complexion: '面色',
      breathing: '呼吸',
      stability: '稳定'
    };
    return labels[dim] || dim;
  }
}

3.4 沉浸光感悬浮导航栏(ImmersiveNavBar.ets)

代码亮点:导航栏根据当前问诊状态动态调整主题色。等待患者接入时显示中性蓝色,问诊进行中根据患者病情严重程度变色(绿色→琥珀色→红色),并显示实时体征数据摘要。

// entry/src/main/ets/common/components/ImmersiveNavBar.ets
import { window } from '@kit.ArkUI';

/**
 * 导航栏配置
 */
interface MedicalNavConfig {
  title: string;
  subtitle: string;
  consultationStatus: 'waiting' | 'in_progress' | 'paused' | 'completed';
  patientSeverity: number;      // 患者严重程度 0-100
  patientName?: string;
  elapsedTime: number;          // 已进行时间(秒)
  alertCount: number;           // 预警数量
}

@Component
export struct ImmersiveNavBar {
  @Prop config: MedicalNavConfig;
  @State bottomAvoidHeight: number = 0;
  @State pulsePhase: number = 0;

  aboutToAppear(): void {
    this.getBottomAvoidArea();
    this.startPulseAnimation();
  }

  private async getBottomAvoidArea(): Promise<void> {
    try {
      const mainWindow = await window.getLastWindow();
      const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      this.bottomAvoidHeight = avoidArea.bottomRect.height;
    } catch (error) {
      console.error('Failed to get avoid area:', error);
    }
  }

  private startPulseAnimation(): void {
    const animate = () => {
      this.pulsePhase = (Date.now() % 3000) / 3000;
      requestAnimationFrame(animate);
    };
    requestAnimationFrame(animate);
  }

  /**
   * 根据问诊状态获取主题色
   */
  private getThemeColor(): string {
    if (this.config.consultationStatus === 'waiting') return '#5B8BD4';
    if (this.config.consultationStatus === 'completed') return '#27AE60';
    if (this.config.consultationStatus === 'paused') return '#95A5A6';
    
    // 进行中:根据严重程度
    if (this.config.patientSeverity < 30) return '#27AE60';
    if (this.config.patientSeverity < 60) return '#F39C12';
    if (this.config.patientSeverity < 80) return '#E67E22';
    return '#E74C3C';
  }

  /**
   * 计算光晕强度(重症患者脉动更快)
   */
  private getGlowIntensity(): number {
    const base = this.config.patientSeverity / 100 * 0.15;
    const pulseFreq = this.config.patientSeverity > 70 ? 2 : 1;
    const pulse = Math.sin(this.pulsePhase * Math.PI * 2 * pulseFreq) * 0.05;
    return Math.max(0.03, base + pulse);
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // 第一层:动态光晕
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(this.getThemeColor())
        .opacity(this.getGlowIntensity())
        .blur(100)
        .position({ x: 0, y: 0 })

      // 第二层:毛玻璃材质
      Column()
        .width('100%')
        .height('100%')
        .backgroundBlurStyle(BlurStyle.COMPONENT_THICK)
        .opacity(0.9)

      // 第三层:顶部高光
      Column()
        .width('100%')
        .height('100%')
        .linearGradient({
          direction: GradientDirection.Top,
          colors: [
            ['rgba(255,255,255,0.2)', 0.0],
            ['rgba(255,255,255,0.05)', 0.4],
            ['transparent', 1.0]
          ]
        })

      // 第四层:内容
      Column({ space: 8 }) {
        Row({ space: 12 }) {
          // 问诊状态标识
          Row({ space: 8 }) {
            Text('🏥')
              .fontSize(20)
            Column({ space: 2 }) {
              Text(this.config.title)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text(this.config.subtitle)
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.6)')
            }
          }

          // 状态标签
          Row({ space: 6 }) {
            Column()
              .width(8)
              .height(8)
              .backgroundColor(this.getThemeColor())
              .borderRadius(4)
              .shadow({ radius: 6, color: this.getThemeColor() })
              .animation({
                duration: this.config.patientSeverity > 70 ? 1000 : 2000,
                curve: Curve.EaseInOut,
                iterations: -1,
                playMode: PlayMode.Alternate
              })
              .scale({ x: 1.3, y: 1.3 })

            Text(this.getStatusLabel(this.config.consultationStatus))
              .fontSize(12)
              .fontColor('#FFFFFF')
          }
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .backgroundColor('rgba(255,255,255,0.1)')
          .borderRadius(12)

          // 右侧信息
          Row({ space: 16 }) {
            // 患者信息
            if (this.config.patientName) {
              Column({ space: 2 }) {
                Text(this.config.patientName)
                  .fontSize(14)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#FFFFFF')
                Text(`严重度 ${this.config.patientSeverity}`)
                  .fontSize(11)
                  .fontColor(
                    this.config.patientSeverity < 40 ? '#00FF88' :
                    this.config.patientSeverity < 70 ? '#FFE66D' : '#FF6B6B'
                  )
              }
            }

            // 时间
            Column({ space: 2 }) {
              Text(this.formatTime(this.config.elapsedTime))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .fontFamily('monospace')
              Text('已进行')
                .fontSize(10)
                .fontColor('rgba(255,255,255,0.5)')
            }

            // 预警数
            if (this.config.alertCount > 0) {
              Badge({
                value: this.config.alertCount.toString(),
                position: BadgePosition.RightTop,
                style: { badgeSize: 18, badgeColor: '#E74C3C' }
              }) {
                Text('🚨')
                  .fontSize(20)
              }
            }
          }
          .layoutWeight(1)
          .justifyContent(FlexAlign.End)
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 12 })

        // 进度条(问诊进度)
        Row({ space: 8 }) {
          Text('进度')
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.6)')
            .width(40)

          Stack() {
            Column()
              .width('100%')
              .height(6)
              .backgroundColor('rgba(255,255,255,0.1)')
              .borderRadius(3)

            Column()
              .width(`${Math.min(this.config.elapsedTime / 600 * 100, 100)}%`)
              .height(6)
              .backgroundColor(this.getThemeColor())
              .borderRadius(3)
              .shadow({ radius: 4, color: this.getThemeColor() })
              .animation({ duration: 500 })
          }
          .width('100%')
          .height(6)

          Text(`${Math.min(Math.round(this.config.elapsedTime / 60), 10)}/10min`)
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.6)')
            .width(60)
        }
        .width('100%')
        .padding({ left: 20, right: 20, bottom: 12 })
      }
      .width('100%')
      .height('100%')
    }
    .width('94%')
    .height(110)
    .margin({
      bottom: this.bottomAvoidHeight + 16,
      left: '3%',
      right: '3%'
    })
    .borderRadius(24)
    .shadow({
      radius: 24,
      color: this.getThemeColor() + '30',
      offsetX: 0,
      offsetY: -6
    })
  }

  private getStatusLabel(status: string): string {
    const labels: Record<string, string> = {
      waiting: '等待接入',
      in_progress: '问诊中',
      paused: '已暂停',
      completed: '已完成'
    };
    return labels[status] || '未知';
  }

  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }
}

四、关键设计要点总结

4.1 Face AR的"医疗微表情"六维模型

与通用的情绪识别不同,本文构建了专门针对医疗场景的六维病情模型:

  • 疼痛度:皱眉 + 眯眼 + 嘴角下拉 + 鼻翼扩张(权重最高40%)
  • 焦虑度:抿嘴 + 咬唇 + 频繁眨眼 + 眉毛上扬
  • 疲劳度:眼皮沉重 + 打哈欠 + 面部松弛 + 目光呆滞
  • 面色评估:通过面部肌肉活跃度间接估算(简化实现,实际可结合图像处理)
  • 呼吸急促度:鼻翼扩张 + 张嘴频率 + 胸部起伏
  • 情绪稳定性:面部肌肉抖动(连续帧变化率)

通过15秒滑动窗口(450帧)时间序列分析,输出稳定的病情体征曲线。

4.2 Body AR的"临床体态"评估体系

通过关节角度计算和关键点相对位置关系,评估:

  • 脊柱姿态:侧向偏移(脊柱侧弯)+ 前后偏移(驼背/前倾)
  • 关节活动度:肩关节屈曲度、髋关节屈曲度、膝关节屈曲度
  • 步态对称性:左右步幅对比,判断下肢问题

所有评估均基于临床参考阈值,生成结构化的体态报告。

4.3 沉浸光感的"病情警示"渲染

病历界面不再是静态文档,而是**"会呼吸的"动态系统**:

  • 轻度(<30分):绿色主题,清新光效
  • 中度(30-60分):琥珀色主题,温和警示
  • 较重(60-80分):橙色主题,明显警示
  • 重度(>80分):红色主题,紧急脉动光效(频率加快)

4.4 HarmonyOS PC的"医生工作站"设计

针对PC大屏幕特性,采用三栏布局

  • 左侧:患者队列和基本信息
  • 中间:视频问诊区 + AR体征覆盖显示
  • 右侧:沉浸光感病历渲染 + 诊断辅助面板

通过分布式软总线与手机端患者AR数据实时同步。

五、效果预览与扩展方向

悬浮导航与沉浸光感效果示意

图:HarmonyOS 6悬浮导航 + Mini栏设计参考

在这里插入图片描述

图:HarmonyOS 6沉浸光感组件效果(支持强/均衡/弱三档)

扩展方向

  1. AI辅助诊断:基于AR体征数据训练模型,提供初步诊断建议
  2. 慢病管理:长期追踪患者体态变化,预警脊柱/关节问题恶化
  3. 康复训练指导:根据体态评估结果,生成个性化康复动作指导
  4. 多科室协同:骨科、神经科、康复科医生同时查看同一患者的AR体征数据

六、结语

HarmonyOS 6的Face AR & Body AR能力,让远程医疗第一次真正"看见"了患者。本文构建的AR远程医疗问诊系统,通过面部微表情辅助判断疼痛和焦虑骨骼体态评估发现脊柱和关节问题沉浸光感病历根据病情动态警示,展示了AR能力从"炫技"走向"医疗普惠"的完整路径。

随着HDC 2026的临近,HarmonyOS 6的生态正在快速成熟。对于医疗行业的开发者而言,现在正是将AR能力融入远程问诊场景的最佳时机。期待更多开发者加入鸿蒙生态,共同探索"看见患者"的智慧医疗未来。


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

Logo

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

更多推荐