在这里插入图片描述

每日一句正能量

不要因为未知而退缩,也不要因为困难而止步。
直面两个最常见的行动杀手:对未知的恐惧,对困难的回避。
在我们的一生中,没有人会为你等待,没有机遇会为你停留,成功也需要速度。

一、前言:当课堂"看见"了学生

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采集

二、项目架构设计

entry/src/main/ets/
├── teacher/
│   ├── ability/
│   │   └── ClassroomManageAbility.ets    # 课堂管理Ability
│   ├── engine/
│   │   ├── AttentionAnalyticsEngine.ets  # 注意力分析引擎
│   │   └── BehaviorPatternEngine.ets     # 行为模式引擎
│   ├── components/
│   │   ├── StudentHeatmapPanel.ets       # 学生状态热力图
│   │   ├── AttentionTrendChart.ets       # 注意力趋势图
│   │   └── AlertBroadcastPanel.ets       # 预警广播面板
│   └── pages/
│       └── TeacherDashboardPage.ets      # 教师大屏控制台
├── student/
│   ├── engine/
│   │   ├── AttentionTracker.ets          # 学生端注意力追踪
│   │   └── BehaviorDetector.ets          # 学生端行为检测
│   ├── components/
│   │   ├── FocusIndicator.ets            # 专注度指示器
│   │   └── CoursewareRenderer.ets        # 沉浸光感课件渲染
│   └── pages/
│       └── StudentClassroomPage.ets      # 学生课堂页面
└── common/
    ├── components/
    │   └── ImmersiveNavBar.ets           # 沉浸光感悬浮导航
    └── models/
        └── ClassroomModels.ets           # 数据模型

三、核心代码实战

3.1 Face AR注意力监测引擎(AttentionTracker.ets)

代码亮点:通过Face AR的52个面部BlendShape系数,构建**"课堂注意力"评估模型**。不同于通用的疲劳检测,这里专门针对"学习场景"训练了五个核心维度:专注度(目光聚焦+面部稳定)、困惑度(皱眉+歪头+眯眼)、走神度(目光游离+表情呆滞)、疲劳度(打哈欠+眼皮沉重)、参与度(点头+微笑+眉毛上扬)。通过滑动窗口时间序列分析,输出稳定的注意力曲线。

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

/**
 * 注意力状态等级
 */
export enum AttentionLevel {
  HIGHLY_FOCUSED = 0,    // 高度专注
  FOCUSED = 1,           // 专注
  DISTRACTED = 2,        // 走神
  CONFUSED = 3,          // 困惑
  FATIGUED = 4,          // 疲劳
  ABSENT = 5             // 离席/无面部
}

/**
 * 注意力维度数据
 */
interface AttentionDimension {
  focus: number;          // 专注度 0-100
  confusion: number;      // 困惑度 0-100
  distraction: number;    // 走神度 0-100
  fatigue: number;        // 疲劳度 0-100
  engagement: number;     // 参与度 0-100
  timestamp: number;
}

/**
 * 注意力报告(上报教师端)
 */
export interface AttentionReport {
  studentId: string;
  currentLevel: AttentionLevel;
  score: number;          // 综合注意力分 0-100
  dimensions: {
    focus: number;
    confusion: number;
    distraction: number;
    fatigue: number;
    engagement: number;
  };
  trend: 'improving' | 'stable' | 'declining';
  alerts: string[];
  timestamp: number;
}

export class AttentionTracker {
  private static instance: AttentionTracker;

  // 学生标识
  private studentId: string = '';
  
  // 滑动窗口(最近10秒,300帧@30fps)
  private attentionWindow: AttentionDimension[] = [];
  private readonly WINDOW_SIZE = 300;
  private readonly WINDOW_DURATION = 10000; // ms

  // 当前状态
  private currentLevel: AttentionLevel = AttentionLevel.FOCUSED;
  private attentionScore: number = 75;
  private lastAlertTime: number = 0;

  // 阈值配置
  private readonly THRESHOLDS = {
    eyeGazeDeviation: 0.3,      // 目光偏离阈值
    browFurrowSustained: 0.35,   // 持续皱眉阈值
    headStillness: 0.15,        // 头部静止度阈值
    yawnOpenness: 0.6,          // 打哈欠张嘴阈值
    blinkRateNormal: 15,        // 正常眨眼频率(次/分钟)
    nodAngle: 10                // 点头角度阈值
  };

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

  setStudentId(id: string): void {
    this.studentId = id;
  }

  /**
   * 处理Face AR数据帧,更新注意力状态
   * 核心算法:五维注意力模型 + 时间序列趋势分析
   */
  processFaceFrame(face: arEngine.ARFace): AttentionReport {
    const blendShapes = face.getBlendShapes();
    const pose = face.getPose();
    
    if (!blendShapes) {
      return this.createReport(AttentionLevel.ABSENT, 0, [], 'stable');
    }

    const now = Date.now();
    
    // 计算五维指标
    const dimension: AttentionDimension = {
      focus: this.calcFocusScore(blendShapes, pose),
      confusion: this.calcConfusionScore(blendShapes),
      distraction: this.calcDistractionScore(blendShapes, pose),
      fatigue: this.calcFatigueScore(blendShapes),
      engagement: this.calcEngagementScore(blendShapes, pose),
      timestamp: now
    };

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

    // 计算综合注意力分(加权模型)
    const avgFocus = this.getWindowAverage('focus');
    const avgConfusion = this.getWindowAverage('confusion');
    const avgDistraction = this.getWindowAverage('distraction');
    const avgFatigue = this.getWindowAverage('fatigue');
    const avgEngagement = this.getWindowAverage('engagement');

    // 综合评分:专注和参与正向,困惑、走神、疲劳负向
    let newScore = Math.round(
      avgFocus * 0.35 +
      avgEngagement * 0.25 +
      (100 - avgConfusion) * 0.15 +
      (100 - avgDistraction) * 0.15 +
      (100 - avgFatigue) * 0.10
    );

    // 平滑过渡
    this.attentionScore = Math.round(this.attentionScore * 0.8 + newScore * 0.2);

    // 确定注意力等级
    const previousLevel = this.currentLevel;
    let newLevel: AttentionLevel;
    
    if (this.attentionScore >= 85) {
      newLevel = AttentionLevel.HIGHLY_FOCUSED;
    } else if (this.attentionScore >= 65) {
      newLevel = AttentionLevel.FOCUSED;
    } else if (this.attentionScore >= 45) {
      newLevel = AttentionLevel.DISTRACTED;
    } else if (avgConfusion > 60 && avgFocus < 40) {
      newLevel = AttentionLevel.CONFUSED;
    } else if (avgFatigue > 70) {
      newLevel = AttentionLevel.FATIGUED;
    } else {
      newLevel = AttentionLevel.DISTRACTED;
    }

    this.currentLevel = newLevel;

    // 生成预警
    const alerts: string[] = [];
    if (newLevel === AttentionLevel.CONFUSED && previousLevel !== AttentionLevel.CONFUSED) {
      alerts.push('学生表现出困惑,建议放慢讲解速度');
    }
    if (newLevel === AttentionLevel.FATIGUED && previousLevel !== AttentionLevel.FATIGUED) {
      alerts.push('学生疲劳度较高,建议插入互动环节');
    }
    if (avgDistraction > 60 && now - this.lastAlertTime > 30000) {
      alerts.push('学生频繁走神,建议提醒或提问');
      this.lastAlertTime = now;
    }

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

    return this.createReport(newLevel, this.attentionScore, alerts, trend);
  }

  /**
   * 专注度:目光稳定 + 面部朝向屏幕 + 无多余微表情
   */
  private calcFocusScore(blendShapes: any, pose: any): number {
    // 眼睛睁开度(不眯眼)
    const eyeOpenness = Math.min(
      1 - (blendShapes.eyeBlinkLeft || 0),
      1 - (blendShapes.eyeBlinkRight || 0)
    );
    
    // 头部姿态(面向屏幕)
    const headYaw = Math.abs(pose?.rotation?.yaw || 0);
    const headPitch = Math.abs(pose?.rotation?.pitch || 0);
    const facingScreen = headYaw < 20 && headPitch < 25 ? 1 : 0;
    
    // 面部肌肉稳定度(无多余表情)
    const expressionNoise = (
      (blendShapes.browInnerUp || 0) +
      (blendShapes.mouthSmileLeft || 0) +
      (blendShapes.mouthFrownLeft || 0) +
      (blendShapes.cheekPuff || 0)
    ) / 4;
    const stability = Math.max(0, 1 - expressionNoise);

    return Math.round((eyeOpenness * 30 + facingScreen * 40 + stability * 30));
  }

  /**
   * 困惑度:皱眉 + 歪头 + 眯眼 + 抿嘴
   */
  private calcConfusionScore(blendShapes: any): number {
    const browFurrow = Math.max(
      blendShapes.browDownLeft || 0,
      blendShapes.browDownRight || 0
    );
    const eyeSquint = Math.max(
      blendShapes.eyeSquintLeft || 0,
      blendShapes.eyeSquintRight || 0
    );
    const lipPress = (blendShapes.mouthPressLeft || 0) + (blendShapes.mouthPressRight || 0);
    const mouthFrown = (blendShapes.mouthFrownLeft || 0) + (blendShapes.mouthFrownRight || 0);
    
    return Math.min(100, (browFurrow * 35 + eyeSquint * 25 + lipPress * 20 + mouthFrown * 20) * 100);
  }

  /**
   * 走神度:目光游离 + 表情呆滞 + 眨眼频率异常
   */
  private calcDistractionScore(blendShapes: any, pose: any): number {
    // 头部转动幅度大
    const headYaw = Math.abs(pose?.rotation?.yaw || 0);
    const headPitch = Math.abs(pose?.rotation?.pitch || 0);
    const headMovement = Math.min(1, (headYaw + headPitch) / 60);

    // 表情呆滞(面部肌肉活动极少)
    const totalExpression = Object.values(blendShapes).reduce((sum: number, v: any) => sum + (v || 0), 0);
    const dullness = Math.max(0, 1 - totalExpression / 10);

    // 眨眼频率计算(简化)
    const blinkRate = this.calcRecentBlinkRate();

    return Math.min(100, (headMovement * 40 + dullness * 35 + blinkRate * 25));
  }

  /**
   * 疲劳度:打哈欠 + 眼皮沉重 + 头部下垂
   */
  private calcFatigueScore(blendShapes: any): number {
    const jawOpen = blendShapes.jawOpen || 0;
    const yawn = jawOpen > this.THRESHOLDS.yawnOpenness ? 1 : 0;
    
    const eyeHeaviness = 1 - Math.min(
      1 - (blendShapes.eyeBlinkLeft || 0),
      1 - (blendShapes.eyeBlinkRight || 0)
    );
    
    const browRelax = 1 - Math.max(
      blendShapes.browInnerUp || 0,
      blendShapes.browOuterUpLeft || 0
    );

    return Math.min(100, (yawn * 40 + eyeHeaviness * 35 + browRelax * 25) * 100);
  }

  /**
   * 参与度:点头 + 微笑 + 眉毛上扬(积极反馈)
   */
  private calcEngagementScore(blendShapes: any, pose: any): number {
    const nodding = this.detectNodding(pose);
    const smiling = Math.max(
      blendShapes.mouthSmileLeft || 0,
      blendShapes.mouthSmileRight || 0
    );
    const browUp = Math.max(
      blendShapes.browInnerUp || 0,
      blendShapes.browOuterUpLeft || 0,
      blendShapes.browOuterUpRight || 0
    );

    return Math.min(100, (nodding * 40 + smiling * 35 + browUp * 25) * 100);
  }

  /**
   * 检测点头动作(通过头部俯仰角变化)
   */
  private detectNodding(pose: any): number {
    if (!pose?.rotation?.pitch || this.attentionWindow.length < 10) return 0;
    
    const recentPitches = this.attentionWindow.slice(-10).map(d => {
      // 从窗口数据中提取头部姿态(简化)
      return 0; 
    });
    
    // 简化:检测连续的俯仰角变化
    let nodCount = 0;
    for (let i = 2; i < recentPitches.length; i++) {
      if (recentPitches[i] < recentPitches[i-1] && recentPitches[i-1] > recentPitches[i-2]) {
        nodCount++;
      }
    }
    
    return Math.min(1, nodCount / 2);
  }

  private calcRecentBlinkRate(): number {
    // 简化实现
    return 0;
  }

  private getWindowAverage(key: keyof AttentionDimension): number {
    if (this.attentionWindow.length === 0) return 50;
    const sum = this.attentionWindow.reduce((acc, d) => acc + (d[key] as number), 0);
    return Math.round(sum / this.attentionWindow.length);
  }

  private calculateTrend(): 'improving' | 'stable' | 'declining' {
    if (this.attentionWindow.length < 60) return 'stable';
    
    const firstHalf = this.attentionWindow.slice(0, Math.floor(this.attentionWindow.length / 2));
    const secondHalf = this.attentionWindow.slice(Math.floor(this.attentionWindow.length / 2));
    
    const firstScore = firstHalf.reduce((sum, d) => sum + d.focus, 0) / firstHalf.length;
    const secondScore = secondHalf.reduce((sum, d) => sum + d.focus, 0) / secondHalf.length;
    
    const diff = secondScore - firstScore;
    if (Math.abs(diff) < 5) return 'stable';
    return diff > 0 ? 'improving' : 'declining';
  }

  private createReport(
    level: AttentionLevel, 
    score: number, 
    alerts: string[], 
    trend: string
  ): AttentionReport {
    return {
      studentId: this.studentId,
      currentLevel: level,
      score,
      dimensions: {
        focus: this.getWindowAverage('focus'),
        confusion: this.getWindowAverage('confusion'),
        distraction: this.getWindowAverage('distraction'),
        fatigue: this.getWindowAverage('fatigue'),
        engagement: this.getWindowAverage('engagement')
      },
      trend: trend as 'improving' | 'stable' | 'declining',
      alerts,
      timestamp: Date.now()
    };
  }

  reset(): void {
    this.attentionWindow = [];
    this.currentLevel = AttentionLevel.FOCUSED;
    this.attentionScore = 75;
    this.lastAlertTime = 0;
  }
}

3.2 Body AR课堂行为识别引擎(BehaviorDetector.ets)

代码亮点:基于Body AR的33个3D骨骼关键点,实现课堂行为识别。支持举手提问、点头赞同、趴桌走神、离席、转头等关键行为的实时检测。通过关节角度阈值和关键点相对位置关系判断行为类型,并通过状态机避免误判。

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

/**
 * 课堂行为类型
 */
export enum ClassroomBehavior {
  HAND_RAISED = 'hand_raised',         // 举手
  HEAD_NOD = 'head_nod',               // 点头
  HEAD_SHAKE = 'head_shake',           // 摇头
  LEANING_FORWARD = 'leaning_forward', // 前倾(专注)
  SLOUCHING = 'slouching',             // 趴桌/驼背
  STANDING = 'standing',               // 站立
  LEFT_SEAT = 'left_seat',             // 离席
  TURNING_AROUND = 'turning_around',   // 转身
  NORMAL = 'normal'                    // 正常坐姿
}

/**
 * 行为检测结果
 */
export interface BehaviorDetection {
  behavior: ClassroomBehavior;
  confidence: number;       // 置信度 0-1
  duration: number;         // 持续时长(ms)
  isNew: boolean;           // 是否新检测到的行为
}

export class BehaviorDetector {
  private static instance: BehaviorDetector;

  // 行为状态机
  private currentBehavior: ClassroomBehavior = ClassroomBehavior.NORMAL;
  private behaviorStartTime: number = 0;
  private behaviorConfidence: number = 0;
  
  // 历史行为(用于去抖动)
  private behaviorHistory: ClassroomBehavior[] = [];
  private readonly HISTORY_SIZE = 10;

  // 头部姿态历史(用于点头/摇头检测)
  private headPoseHistory: Array<{ pitch: number; yaw: number; timestamp: number }> = [];
  private readonly HEAD_HISTORY_SIZE = 30;

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

  /**
   * 处理Body AR数据帧,识别课堂行为
   * 核心算法:关键点几何分析 + 状态机去抖动
   */
  processBodyFrame(body: arEngine.ARBody): BehaviorDetection {
    const landmarks = body.getLandmarks3D();
    if (!landmarks) {
      return this.createDetection(ClassroomBehavior.LEFT_SEAT, 0.9, 0, true);
    }

    const points = this.parseLandmarks(landmarks);
    const now = Date.now();

    // 更新头部姿态历史
    if (points.nose) {
      this.headPoseHistory.push({
        pitch: this.estimateHeadPitch(points),
        yaw: this.estimateHeadYaw(points),
        timestamp: now
      });
      if (this.headPoseHistory.length > this.HEAD_HISTORY_SIZE) {
        this.headPoseHistory.shift();
      }
    }

    // 检测各种行为(按优先级)
    const detections: Array<{ behavior: ClassroomBehavior; confidence: number }> = [];

    // 1. 举手检测(高优先级)
    const handRaised = this.detectHandRaised(points);
    if (handRaised.confidence > 0.7) detections.push(handRaised);

    // 2. 点头/摇头检测
    const headGesture = this.detectHeadGesture();
    if (headGesture.confidence > 0.6) detections.push(headGesture);

    // 3. 坐姿检测
    const posture = this.detectPosture(points);
    if (posture.confidence > 0.6) detections.push(posture);

    // 4. 站立/离席检测
    const standing = this.detectStanding(points);
    if (standing.confidence > 0.7) detections.push(standing);

    // 5. 转身检测
    const turning = this.detectTurning(points);
    if (turning.confidence > 0.6) detections.push(turning);

    // 选择置信度最高的行为
    let bestDetection = detections.length > 0 
      ? detections.reduce((best, curr) => curr.confidence > best.confidence ? curr : best)
      : { behavior: ClassroomBehavior.NORMAL, confidence: 0.9 };

    // 状态机去抖动:新行为需要连续多帧确认
    this.behaviorHistory.push(bestDetection.behavior);
    if (this.behaviorHistory.length > this.HISTORY_SIZE) {
      this.behaviorHistory.shift();
    }

    const stabilizedBehavior = this.stabilizeBehavior(bestDetection.behavior);
    const isNewBehavior = stabilizedBehavior !== this.currentBehavior;
    
    if (isNewBehavior) {
      this.currentBehavior = stabilizedBehavior;
      this.behaviorStartTime = now;
      this.behaviorConfidence = bestDetection.confidence;
    }

    const duration = now - this.behaviorStartTime;

    return this.createDetection(
      this.currentBehavior,
      this.behaviorConfidence,
      duration,
      isNewBehavior
    );
  }

  /**
   * 举手检测:手腕高于肩膀且肘关节角度大于90度
   */
  private detectHandRaised(points: Record<string, any>): { behavior: ClassroomBehavior; confidence: number } {
    const leftWristY = points.leftWrist?.y || 1;
    const rightWristY = points.rightWrist?.y || 1;
    const leftShoulderY = points.leftShoulder?.y || 0.5;
    const rightShoulderY = points.rightShoulder?.y || 0.5;

    // 检查是否有手腕高于肩膀
    const leftRaised = leftWristY < leftShoulderY - 0.1;
    const rightRaised = rightWristY < rightShoulderY - 0.1;
    const handRaised = leftRaised || rightRaised;

    if (!handRaised) return { behavior: ClassroomBehavior.NORMAL, confidence: 0 };

    // 计算肘关节角度确认
    let confidence = 0.7;
    if (leftRaised) {
      const leftElbowAngle = this.calcAngle(points.leftShoulder, points.leftElbow, points.leftWrist);
      if (leftElbowAngle > 90 && leftElbowAngle < 180) confidence += 0.2;
    }
    if (rightRaised) {
      const rightElbowAngle = this.calcAngle(points.rightShoulder, points.rightElbow, points.rightWrist);
      if (rightElbowAngle > 90 && rightElbowAngle < 180) confidence += 0.2;
    }

    return { behavior: ClassroomBehavior.HAND_RAISED, confidence: Math.min(1, confidence) };
  }

  /**
   * 点头/摇头检测(基于头部姿态历史)
   */
  private detectHeadGesture(): { behavior: ClassroomBehavior; confidence: number } {
    if (this.headPoseHistory.length < 10) return { behavior: ClassroomBehavior.NORMAL, confidence: 0 };

    const recent = this.headPoseHistory.slice(-10);
    const pitches = recent.map(h => h.pitch);
    const yaws = recent.map(h => h.yaw);

    // 检测点头:连续的俯仰角变化(下-上)
    let nodCount = 0;
    for (let i = 2; i < pitches.length; i++) {
      if (pitches[i-2] > pitches[i-1] && pitches[i-1] < pitches[i] && 
          Math.abs(pitches[i-2] - pitches[i-1]) > 5) {
        nodCount++;
      }
    }

    // 检测摇头:连续的偏航角变化(左-右-左)
    let shakeCount = 0;
    for (let i = 2; i < yaws.length; i++) {
      if ((yaws[i-2] < yaws[i-1] && yaws[i-1] > yaws[i]) ||
          (yaws[i-2] > yaws[i-1] && yaws[i-1] < yaws[i])) {
        if (Math.abs(yaws[i-2] - yaws[i-1]) > 8) shakeCount++;
      }
    }

    if (nodCount >= 1) return { behavior: ClassroomBehavior.HEAD_NOD, confidence: 0.7 + nodCount * 0.1 };
    if (shakeCount >= 1) return { behavior: ClassroomBehavior.HEAD_SHAKE, confidence: 0.7 + shakeCount * 0.1 };
    
    return { behavior: ClassroomBehavior.NORMAL, confidence: 0 };
  }

  /**
   * 坐姿检测:前倾(专注)vs 趴桌/驼背(走神)
   */
  private detectPosture(points: Record<string, any>): { behavior: ClassroomBehavior; confidence: number } {
    const shoulderY = (points.leftShoulder?.y + points.rightShoulder?.y) / 2;
    const hipY = (points.leftHip?.y + points.rightHip?.y) / 2;
    const headY = points.nose?.y || shoulderY - 0.2;

    // 计算背部角度(肩-髋连线与垂直线的夹角)
    const backAngle = Math.atan2(Math.abs(shoulderY - hipY), 
      Math.abs(points.leftShoulder?.x - points.leftHip?.x)) * (180 / Math.PI);

    if (backAngle > 30 && backAngle < 70) {
      // 前倾
      return { behavior: ClassroomBehavior.LEANING_FORWARD, confidence: 0.8 };
    } else if (backAngle > 80 || headY > shoulderY + 0.15) {
      // 趴桌/驼背
      return { behavior: ClassroomBehavior.SLOUCHING, confidence: 0.75 };
    }

    return { behavior: ClassroomBehavior.NORMAL, confidence: 0 };
  }

  /**
   * 站立/离席检测
   */
  private detectStanding(points: Record<string, any>): { behavior: ClassroomBehavior; confidence: number } {
    const hipY = (points.leftHip?.y + points.rightHip?.y) / 2;
    const kneeY = (points.leftKnee?.y + points.rightKnee?.y) / 2;
    const ankleY = (points.leftAnkle?.y + points.rightAnkle?.y) / 2;

    // 站立:髋-膝-踝大致在一条垂直线上且整体位置较高
    const legStraightness = Math.abs((hipY - kneeY) - (kneeY - ankleY));
    const isStanding = legStraightness < 0.1 && hipY < 0.6;

    if (isStanding) {
      // 判断是否在座位附近(简化:通过x坐标判断是否偏离中心)
      const centerX = (points.leftHip?.x + points.rightHip?.x) / 2;
      if (Math.abs(centerX - 0.5) > 0.3) {
        return { behavior: ClassroomBehavior.LEFT_SEAT, confidence: 0.85 };
      }
      return { behavior: ClassroomBehavior.STANDING, confidence: 0.8 };
    }

    return { behavior: ClassroomBehavior.NORMAL, confidence: 0 };
  }

  /**
   * 转身检测
   */
  private detectTurning(points: Record<string, any>): { behavior: ClassroomBehavior; confidence: number } {
    const leftShoulderX = points.leftShoulder?.x || 0;
    const rightShoulderX = points.rightShoulder?.x || 1;
    const shoulderWidth = Math.abs(rightShoulderX - leftShoulderX);

    // 正常面向屏幕时肩宽较大,转身时肩宽变小(透视缩短)
    if (shoulderWidth < 0.15) {
      return { behavior: ClassroomBehavior.TURNING_AROUND, confidence: 0.7 };
    }

    return { behavior: ClassroomBehavior.NORMAL, confidence: 0 };
  }

  /**
   * 状态机去抖动:行为需要连续多帧确认
   */
  private stabilizeBehavior(candidate: ClassroomBehavior): ClassroomBehavior {
    if (this.behaviorHistory.length < 5) return this.currentBehavior;

    // 统计最近历史中出现次数最多的行为
    const counts: Record<string, number> = {};
    this.behaviorHistory.forEach(b => {
      counts[b] = (counts[b] || 0) + 1;
    });

    const maxBehavior = Object.entries(counts).reduce((a, b) => a[1] > b[1] ? a : b);
    
    // 如果候选行为在历史中出现超过60%,则确认
    if (maxBehavior[0] === candidate && maxBehavior[1] / this.behaviorHistory.length > 0.6) {
      return candidate;
    }

    // 否则保持当前行为(防止抖动)
    return this.currentBehavior;
  }

  private createDetection(
    behavior: ClassroomBehavior,
    confidence: number,
    duration: number,
    isNew: boolean
  ): BehaviorDetection {
    return { behavior, confidence, duration, isNew };
  }

  private calcAngle(p1: any, vertex: any, p2: any): number {
    if (!p1 || !vertex || !p2) return 180;
    
    const v1 = { x: p1.x - vertex.x, y: p1.y - vertex.y };
    const v2 = { x: p2.x - vertex.x, y: p2.y - vertex.y };
    
    const dot = v1.x * v2.x + v1.y * v2.y;
    const mag1 = Math.sqrt(v1.x**2 + v1.y**2);
    const mag2 = Math.sqrt(v2.x**2 + v2.y**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 estimateHeadPitch(points: Record<string, any>): number {
    // 通过鼻子和耳朵的相对位置估算俯仰角
    const noseY = points.nose?.y || 0;
    const earY = ((points.leftEar?.y || 0) + (points.rightEar?.y || 0)) / 2;
    return (noseY - earY) * 100; // 归一化角度
  }

  private estimateHeadYaw(points: Record<string, any>): number {
    // 通过左右耳的可见度估算偏航角
    const leftEarX = points.leftEar?.x || 0;
    const rightEarX = points.rightEar?.x || 1;
    return (rightEarX - leftEarX - 0.5) * 100;
  }

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

    return {
      nose: getPoint(0),
      leftEar: getPoint(7),
      rightEar: getPoint(8),
      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)
    };
  }

  reset(): void {
    this.currentBehavior = ClassroomBehavior.NORMAL;
    this.behaviorHistory = [];
    this.headPoseHistory = [];
    this.behaviorStartTime = 0;
  }
}

3.3 沉浸光感动态课件渲染(CoursewareRenderer.ets)

代码亮点:根据全班学生的注意力状态动态调整课件渲染效果。当检测到多数学生困惑时,自动高亮关键知识点并放慢动画;当学生高度专注时,增强视觉冲击力;当学生疲劳时,切换为护眼模式并插入互动提示。

// entry/src/main/ets/student/components/CoursewareRenderer.ets
import { AttentionLevel } from '../engine/AttentionTracker';

/**
 * 课件渲染配置
 */
interface CoursewareConfig {
  backgroundMode: 'normal' | 'focus' | 'confusion' | 'fatigue';
  highlightIntensity: number;     // 重点高亮强度 0-1
  animationSpeed: number;         // 动画速度倍率
  colorTemperature: number;       // 色温 3000K-6500K
  brightness: number;             // 亮度 0.5-1.2
  interactiveHints: boolean;      // 是否显示互动提示
}

/**
 * 课件页面数据
 */
interface CoursewarePage {
  id: string;
  title: string;
  content: string;
  keyPoints: string[];
  mediaUrl?: string;
  interactiveElements?: Array<{
    type: 'quiz' | 'discussion' | 'break';
    trigger: string;
  }>;
}

@Component
export struct CoursewareRenderer {
  @Prop currentPage: CoursewarePage;
  @Prop classAttentionState: {
    avgScore: number;
    dominantLevel: AttentionLevel;
    confusionRatio: number;
    fatigueRatio: number;
  };
  @State currentConfig: CoursewareConfig = this.getDefaultConfig();
  @State pulsePhase: number = 0;

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

  private startAdaptiveRendering(): void {
    // 根据班级注意力状态动态调整渲染配置
    const adapt = () => {
      this.currentConfig = this.calculateAdaptiveConfig();
      this.pulsePhase = (Date.now() % 4000) / 4000;
      requestAnimationFrame(adapt);
    };
    requestAnimationFrame(adapt);
  }

  /**
   * 计算自适应渲染配置
   */
  private calculateAdaptiveConfig(): CoursewareConfig {
    const { avgScore, dominantLevel, confusionRatio, fatigueRatio } = this.classAttentionState;

    // 困惑模式:多数学生困惑
    if (confusionRatio > 0.4 || dominantLevel === AttentionLevel.CONFUSED) {
      return {
        backgroundMode: 'confusion',
        highlightIntensity: 0.9,
        animationSpeed: 0.5,
        colorTemperature: 4500,
        brightness: 1.0,
        interactiveHints: true
      };
    }

    // 疲劳模式:多数学生疲劳
    if (fatigueRatio > 0.4 || dominantLevel === AttentionLevel.FATIGUED) {
      return {
        backgroundMode: 'fatigue',
        highlightIntensity: 0.5,
        animationSpeed: 0.3,
        colorTemperature: 3500, // 暖色调护眼
        brightness: 0.8,
        interactiveHints: true
      };
    }

    // 高度专注模式
    if (avgScore > 80 || dominantLevel === AttentionLevel.HIGHLY_FOCUSED) {
      return {
        backgroundMode: 'focus',
        highlightIntensity: 0.7,
        animationSpeed: 1.2,
        colorTemperature: 5500,
        brightness: 1.1,
        interactiveHints: false
      };
    }

    // 正常模式
    return this.getDefaultConfig();
  }

  private getDefaultConfig(): CoursewareConfig {
    return {
      backgroundMode: 'normal',
      highlightIntensity: 0.5,
      animationSpeed: 1.0,
      colorTemperature: 5000,
      brightness: 1.0,
      interactiveHints: false
    };
  }

  /**
   * 根据模式获取背景色
   */
  private getBackgroundColor(): string {
    switch (this.currentConfig.backgroundMode) {
      case 'focus':
        return '#0a1628'; // 深蓝(专注)
      case 'confusion':
        return '#1a0a28'; // 深紫(困惑)
      case 'fatigue':
        return '#1a1a0a'; // 深褐(护眼)
      default:
        return '#0f0f1a'; // 默认深蓝黑
    }
  }

  /**
   * 根据色温调整文字颜色
   */
  private getTextColor(): string {
    const temp = this.currentConfig.colorTemperature;
    if (temp < 4000) return '#FFF8E7'; // 暖白
    if (temp > 5500) return '#F0F8FF'; // 冷白
    return '#FFFFFF';
  }

  build() {
    Stack({ alignContent: Alignment.Center }) {
      // 动态背景层
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(this.getBackgroundColor())
        .animation({ duration: 1000 })

      // 环境光效层(沉浸光感)
      this.buildAmbientLightLayer()

      // 课件内容层
      Column({ space: 20 }) {
        // 标题
        Text(this.currentPage.title)
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.getTextColor())
          .width('100%')
          .textAlign(TextAlign.Center)

        // 关键知识点(根据困惑度动态高亮)
        ForEach(this.currentPage.keyPoints, (point: string, index: number) => {
          Row({ space: 12 }) {
            // 动态高亮标记
            Column()
              .width(6)
              .height(40)
              .backgroundColor(this.currentConfig.highlightIntensity > 0.7 ? '#FFD700' : '#4A90E2')
              .borderRadius(3)
              .shadow({
                radius: this.currentConfig.highlightIntensity * 10,
                color: this.currentConfig.highlightIntensity > 0.7 ? '#FFD70050' : 'transparent'
              })

            Text(point)
              .fontSize(18)
              .fontColor(this.getTextColor())
              .fontWeight(this.currentConfig.highlightIntensity > 0.7 ? FontWeight.Bold : FontWeight.Normal)
              .layoutWeight(1)
              .opacity(this.currentConfig.highlightIntensity > 0.7 ? 1.0 : 0.85)
          }
          .width('100%')
          .padding(16)
          .backgroundColor(
            this.currentConfig.highlightIntensity > 0.7 
              ? 'rgba(255,215,0,0.08)' 
              : 'rgba(255,255,255,0.03)'
          )
          .borderRadius(12)
          .border({
            width: this.currentConfig.highlightIntensity > 0.7 ? 1 : 0,
            color: 'rgba(255,215,0,0.3)'
          })
          .animation({ duration: 800 })
        })

        // 互动提示(疲劳/困惑时显示)
        if (this.currentConfig.interactiveHints) {
          Column({ space: 8 }) {
            Text('💡 互动时间')
              .fontSize(16)
              .fontColor('#FFE66D')
            
            Text('老师注意到大家有些困惑,让我们做个小练习巩固一下')
              .fontSize(14)
              .fontColor('rgba(255,255,255,0.8)')
              .textAlign(TextAlign.Center)

            Button('开始练习')
              .type(ButtonType.Capsule)
              .fontSize(14)
              .fontColor('#FFFFFF')
              .backgroundColor('#4A90E2')
              .height(40)
              .padding({ left: 32, right: 32 })
          }
          .width('100%')
          .padding(20)
          .backgroundColor('rgba(255,230,109,0.05)')
          .borderRadius(16)
          .border({ width: 1, color: 'rgba(255,230,109,0.2)' })
          .animation({ duration: 500 })
        }
      }
      .width('90%')
      .padding(24)
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildAmbientLightLayer(): void {
    Column() {
      // 顶部柔光
      Column()
        .width('100%')
        .height(200)
        .backgroundColor(
          this.currentConfig.backgroundMode === 'confusion' ? '#9B59B6' :
          this.currentConfig.backgroundMode === 'fatigue' ? '#D4A574' :
          '#4A90E2'
        )
        .opacity(0.05 + Math.sin(this.pulsePhase * Math.PI * 2) * 0.02)
        .blur(100)
        .position({ x: 0, y: 0 })

      // 底部氛围光
      Column()
        .width('100%')
        .height(150)
        .backgroundColor(
          this.currentConfig.backgroundMode === 'confusion' ? '#E74C3C' :
          this.currentConfig.backgroundMode === 'fatigue' ? '#27AE60' :
          '#3498DB'
        )
        .opacity(0.03)
        .blur(80)
        .position({ x: 0, y: '85%' })
    }
    .width('100%')
    .height('100%')
    .pointerEvents(PointerEventMode.None)
  }
}

3.4 教师端学生状态热力图(StudentHeatmapPanel.ets)

代码亮点:HarmonyOS PC端大屏展示全班学生的实时注意力状态。采用六边形蜂窝热力图布局,每个六边形代表一个学生,颜色表示注意力等级(绿色=专注,黄色=走神,红色=困惑/疲劳)。支持点击单个学生查看详细数据,并可直接发送提醒。

// entry/src/main/ets/teacher/components/StudentHeatmapPanel.ets
import { AttentionLevel } from '../../student/engine/AttentionTracker';
import { ClassroomBehavior } from '../../student/engine/BehaviorDetector';

/**
 * 学生状态数据
 */
interface StudentStatus {
  id: string;
  name: string;
  attentionLevel: AttentionLevel;
  attentionScore: number;
  currentBehavior: ClassroomBehavior;
  behaviorDuration: number;
  trend: 'improving' | 'stable' | 'declining';
  lastUpdate: number;
}

@Component
export struct StudentHeatmapPanel {
  @Prop students: StudentStatus[];
  @State selectedStudent: StudentStatus | null = null;
  @State hoverStudentId: string = '';

  // 注意力等级颜色映射
  private readonly LEVEL_COLORS: Record<AttentionLevel, string> = {
    [AttentionLevel.HIGHLY_FOCUSED]: '#00C853',
    [AttentionLevel.FOCUSED]: '#64DD17',
    [AttentionLevel.DISTRACTED]: '#FFD600',
    [AttentionLevel.CONFUSED]: '#FF9100',
    [AttentionLevel.FATIGUED]: '#FF1744',
    [AttentionLevel.ABSENT]: '#9E9E9E'
  };

  private readonly LEVEL_LABELS: Record<AttentionLevel, string> = {
    [AttentionLevel.HIGHLY_FOCUSED]: '高度专注',
    [AttentionLevel.FOCUSED]: '专注',
    [AttentionLevel.DISTRACTED]: '走神',
    [AttentionLevel.CONFUSED]: '困惑',
    [AttentionLevel.FATIGUED]: '疲劳',
    [AttentionLevel.ABSENT]: '离席'
  };

  build() {
    Column({ space: 16 }) {
      // 标题栏
      Row({ space: 12 }) {
        Text('👥 学生状态热力图')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        // 图例
        Row({ space: 8 }) {
          ForEach([
            { level: AttentionLevel.HIGHLY_FOCUSED, label: '专注' },
            { level: AttentionLevel.DISTRACTED, label: '走神' },
            { level: AttentionLevel.CONFUSED, label: '困惑' },
            { level: AttentionLevel.FATIGUED, label: '疲劳' }
          ], (item: any) => {
            Row({ space: 4 }) {
              Column()
                .width(12)
                .height(12)
                .backgroundColor(this.LEVEL_COLORS[item.level])
                .borderRadius(6)
              Text(item.label)
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.7)')
            }
          })
        }
        .layoutWeight(1)
        .justifyContent(FlexAlign.End)
      }
      .width('100%')

      // 热力图网格
      Grid() {
        ForEach(this.students, (student: StudentStatus) => {
          GridItem() {
            this.buildStudentHexagon(student)
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
      .columnsGap(8)
      .rowsGap(8)
      .width('100%')
      .layoutWeight(1)

      // 选中学生详情
      if (this.selectedStudent) {
        this.buildStudentDetailPanel(this.selectedStudent)
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }

  @Builder
  buildStudentHexagon(student: StudentStatus): void {
    Stack({ alignContent: Alignment.Center }) {
      // 六边形背景(使用圆角矩形模拟)
      Column()
        .width('100%')
        .aspectRatio(1.15)
        .backgroundColor(this.LEVEL_COLORS[student.attentionLevel] + '20')
        .borderRadius(12)
        .border({
          width: this.hoverStudentId === student.id ? 2 : 1,
          color: this.LEVEL_COLORS[student.attentionLevel]
        })
        .shadow({
          radius: this.hoverStudentId === student.id ? 12 : 4,
          color: this.LEVEL_COLORS[student.attentionLevel] + '40'
        })

      // 状态指示点
      Column()
        .width(10)
        .height(10)
        .backgroundColor(this.LEVEL_COLORS[student.attentionLevel])
        .borderRadius(5)
        .position({ x: '85%', y: '15%' })
        .shadow({ radius: 6, color: this.LEVEL_COLORS[student.attentionLevel] })

      // 学生信息
      Column({ space: 4 }) {
        Text(student.name)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)

        Text(`${student.attentionScore}`)
          .fontSize(12)
          .fontColor(
            student.attentionScore > 70 ? '#00FF88' : 
            student.attentionScore > 40 ? '#FFE66D' : '#FF6B6B'
          )

        // 行为图标
        Text(this.getBehaviorEmoji(student.currentBehavior))
          .fontSize(16)
      }
    }
    .width('100%')
    .onClick(() => {
      this.selectedStudent = student;
    })
    .onHover((isHover: boolean) => {
      this.hoverStudentId = isHover ? student.id : '';
    })
    .animation({ duration: 300 })
  }

  @Builder
  buildStudentDetailPanel(student: StudentStatus): void {
    Column({ space: 12 }) {
      Row({ space: 12 }) {
        Text(`👤 ${student.name}`)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')

        Text(this.LEVEL_LABELS[student.attentionLevel])
          .fontSize(13)
          .fontColor(this.LEVEL_COLORS[student.attentionLevel])
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .backgroundColor(this.LEVEL_COLORS[student.attentionLevel] + '20')
          .borderRadius(8)

        Button('✕')
          .type(ButtonType.Circle)
          .fontSize(12)
          .fontColor('rgba(255,255,255,0.5)')
          .backgroundColor('transparent')
          .width(28)
          .height(28)
          .onClick(() => {
            this.selectedStudent = null;
          })
        .layoutWeight(1)
        .justifyContent(FlexAlign.End)
      }
      .width('100%')

      // 注意力趋势
      Row({ space: 16 }) {
        Column({ space: 4 }) {
          Text('注意力分')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${student.attentionScore}`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor(
              student.attentionScore > 70 ? '#00FF88' : 
              student.attentionScore > 40 ? '#FFE66D' : '#FF6B6B'
            )
        }

        Column({ space: 4 }) {
          Text('趋势')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(
            student.trend === 'improving' ? '↗️ 上升' :
            student.trend === 'declining' ? '↘️ 下降' : '➡️ 稳定'
          )
            .fontSize(16)
            .fontColor('#FFFFFF')
        }

        Column({ space: 4 }) {
          Text('当前行为')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(this.getBehaviorLabel(student.currentBehavior))
            .fontSize(14)
            .fontColor('#FFFFFF')
        }

        Column({ space: 4 }) {
          Text('持续时长')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.5)')
          Text(`${Math.round(student.behaviorDuration / 1000)}`)
            .fontSize(14)
            .fontColor('#FFFFFF')
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)

      // 操作按钮
      Row({ space: 12 }) {
        Button('发送提醒')
          .type(ButtonType.Capsule)
          .fontSize(13)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF9500')
          .height(36)
          .layoutWeight(1)
          .onClick(() => {
            this.sendAlert(student.id, '请注意听讲');
          })

        Button('提问该生')
          .type(ButtonType.Capsule)
          .fontSize(13)
          .fontColor('#FFFFFF')
          .backgroundColor('#4A90E2')
          .height(36)
          .layoutWeight(1)
          .onClick(() => {
            this.sendAlert(student.id, '请回答这个问题');
          })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius(16)
    .border({ width: 1, color: 'rgba(255,255,255,0.1)' })
  }

  private getBehaviorEmoji(behavior: ClassroomBehavior): string {
    const emojis: Record<ClassroomBehavior, string> = {
      [ClassroomBehavior.HAND_RAISED]: '✋',
      [ClassroomBehavior.HEAD_NOD]: '👍',
      [ClassroomBehavior.HEAD_SHAKE]: '👎',
      [ClassroomBehavior.LEANING_FORWARD]: '🔍',
      [ClassroomBehavior.SLOUCHING]: '💤',
      [ClassroomBehavior.STANDING]: '🧍',
      [ClassroomBehavior.LEFT_SEAT]: '🚶',
      [ClassroomBehavior.TURNING_AROUND]: '🔄',
      [ClassroomBehavior.NORMAL]: '✅'
    };
    return emojis[behavior] || '❓';
  }

  private getBehaviorLabel(behavior: ClassroomBehavior): string {
    const labels: Record<ClassroomBehavior, string> = {
      [ClassroomBehavior.HAND_RAISED]: '举手提问',
      [ClassroomBehavior.HEAD_NOD]: '点头赞同',
      [ClassroomBehavior.HEAD_SHAKE]: '摇头否定',
      [ClassroomBehavior.LEANING_FORWARD]: '前倾专注',
      [ClassroomBehavior.SLOUCHING]: '趴桌走神',
      [ClassroomBehavior.STANDING]: '站立',
      [ClassroomBehavior.LEFT_SEAT]: '离席',
      [ClassroomBehavior.TURNING_AROUND]: '转身',
      [ClassroomBehavior.NORMAL]: '正常坐姿'
    };
    return labels[behavior] || '未知';
  }

  private sendAlert(studentId: string, message: string): void {
    // 通过分布式通道发送提醒到学生端
    console.log(`Sending alert to ${studentId}: ${message}`);
  }
}

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

代码亮点:导航栏根据课堂整体氛围动态调整主题色。当多数学生专注时显示清新的蓝绿色,当检测到困惑时变为温暖的琥珀色提示教师放慢节奏,当学生疲劳时切换为柔和的暖灰色并显示休息倒计时。

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

/**
 * 导航栏配置
 */
interface ClassroomNavConfig {
  title: string;
  subtitle: string;
  classAttentionLevel: AttentionLevel;
  avgAttentionScore: number;
  studentCount: number;
  onlineCount: number;
  currentMode: 'lecture' | 'discussion' | 'break' | 'quiz';
  breakCountdown?: number; // 休息倒计时(秒)
}

@Component
export struct ImmersiveNavBar {
  @Prop config: ClassroomNavConfig;
  @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 {
    switch (this.config.classAttentionLevel) {
      case AttentionLevel.HIGHLY_FOCUSED:
      case AttentionLevel.FOCUSED:
        return '#00C853'; // 专注:绿色
      case AttentionLevel.CONFUSED:
        return '#FF9100'; // 困惑:琥珀色
      case AttentionLevel.FATIGUED:
        return '#78909C'; // 疲劳:蓝灰
      case AttentionLevel.DISTRACTED:
        return '#FFD600'; // 走神:黄色
      default:
        return '#4A90E2'; // 默认:蓝色
    }
  }

  /**
   * 计算光晕强度
   */
  private getGlowIntensity(): number {
    const base = this.config.avgAttentionScore / 100 * 0.12;
    const pulse = Math.sin(this.pulsePhase * Math.PI * 2) * 0.04;
    return 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: 2000,
                curve: Curve.EaseInOut,
                iterations: -1,
                playMode: PlayMode.Alternate
              })
              .scale({ x: 1.3, y: 1.3 })

            Text(this.getModeLabel(this.config.currentMode))
              .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 }) {
            Column({ space: 2 }) {
              Text(`${this.config.onlineCount}/${this.config.studentCount}`)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('在线')
                .fontSize(10)
                .fontColor('rgba(255,255,255,0.5)')
            }

            Column({ space: 2 }) {
              Text(`${this.config.avgAttentionScore}`)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor(
                  this.config.avgAttentionScore > 70 ? '#00FF88' : 
                  this.config.avgAttentionScore > 40 ? '#FFE66D' : '#FF6B6B'
                )
              Text('均分')
                .fontSize(10)
                .fontColor('rgba(255,255,255,0.5)')
            }
          }
          .layoutWeight(1)
          .justifyContent(FlexAlign.End)
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 12 })

        // 休息倒计时(疲劳模式)
        if (this.config.currentMode === 'break' && this.config.breakCountdown) {
          Row({ space: 8 }) {
            Text('⏰ 休息中')
              .fontSize(12)
              .fontColor('#FFE66D')
            
            Text(`${Math.floor(this.config.breakCountdown / 60)}:${(this.config.breakCountdown % 60).toString().padStart(2, '0')}`)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFE66D')
              .fontFamily('monospace')
          }
          .width('100%')
          .padding({ left: 20, right: 20, bottom: 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(`${this.config.avgAttentionScore}%`)
              .height(6)
              .backgroundColor(this.getThemeColor())
              .borderRadius(3)
              .shadow({ radius: 4, color: this.getThemeColor() })
              .animation({ duration: 500, curve: Curve.EaseOut })
          }
          .width('100%')
          .height(6)

          Text(`${this.config.avgAttentionScore}%`)
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.6)')
            .width(40)
        }
        .width('100%')
        .padding({ left: 20, right: 20, bottom: 12 })
      }
      .width('100%')
      .height('100%')
    }
    .width('94%')
    .height(this.config.currentMode === 'break' ? 130 : 110)
    .margin({
      bottom: this.bottomAvoidHeight + 16,
      left: '3%',
      right: '3%'
    })
    .borderRadius(24)
    .shadow({
      radius: 24,
      color: this.getThemeColor() + '30',
      offsetX: 0,
      offsetY: -6
    })
  }

  private getModeLabel(mode: string): string {
    const labels: Record<string, string> = {
      lecture: '讲授中',
      discussion: '讨论中',
      break: '休息中',
      quiz: '测验中'
    };
    return labels[mode] || '课堂中';
  }
}

四、关键设计要点总结

4.1 Face AR的"课堂注意力"五维模型

与通用的疲劳检测不同,本文构建了专门针对学习场景的五维注意力模型:

  • 专注度:目光稳定 + 面部朝向屏幕 + 无多余微表情
  • 困惑度:皱眉 + 歪头 + 眯眼 + 抿嘴
  • 走神度:目光游离 + 表情呆滞 + 眨眼频率异常
  • 疲劳度:打哈欠 + 眼皮沉重 + 头部下垂
  • 参与度:点头 + 微笑 + 眉毛上扬(积极反馈)

通过10秒滑动窗口(300帧)时间序列分析,输出稳定的注意力曲线,避免单次表情误判。

4.2 Body AR的"课堂行为"状态机

通过关节角度阈值和关键点相对位置关系,识别六种关键课堂行为:

  • 举手提问:手腕高于肩膀 + 肘关节角度>90°
  • 点头/摇头:头部俯仰/偏航角连续变化模式
  • 前倾专注:背部角度30-70°
  • 趴桌走神:背部角度>80° 或 头部低于肩膀
  • 站立/离席:腿部伸直 + 整体位置判断
  • 转身:肩宽透视缩短

状态机去抖动机制确保行为需要连续多帧(>60%历史帧)确认才上报。

4.3 沉浸光感的"自适应课件"渲染

课件不再是静态内容,而是**"会感知学生状态"的动态系统**:

  • 困惑模式:深紫背景 + 金色高亮 + 放慢动画 + 显示互动提示
  • 疲劳模式:深褐背景 + 暖色调 + 降低亮度 + 插入休息提醒
  • 专注模式:深蓝背景 + 增强视觉冲击力 + 加速动画
  • 正常模式:标准深蓝黑 + 平衡渲染

4.4 HarmonyOS PC的"教师大屏"设计

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

  • 左侧:课件控制区(翻页、标注、模式切换)
  • 中间:课件主视区(沉浸光感渲染)
  • 右侧:学生状态热力图(六边形蜂窝布局)+ 实时预警

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

五、效果预览与扩展方向

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

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

在这里插入图片描述

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

扩展方向

  1. AI教学助手:基于注意力数据训练模型,自动生成个性化复习计划
  2. 家长端看板:家长可查看孩子的课堂专注度报告(隐私脱敏后)
  3. 多校区联动:总部教师通过PC大屏同时监控多个校区的课堂状态
  4. VR沉浸式课堂:结合VR头显,将Body AR数据映射到虚拟化身,实现远程"面对面"教学

六、结语

HarmonyOS 6的Face AR & Body AR能力,让在线教育第一次真正"看见"了学生。本文构建的AR智慧课堂系统,通过面部注意力监测防止学生走神骨骼行为识别捕捉课堂互动沉浸光感课件根据学情自适应调整,展示了AR能力从"炫技"走向"教育普惠"的完整路径。

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


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

Logo

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

更多推荐