HEX
Server: nginx/1.28.1
System: Linux 10-41-63-61 6.8.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 20 00:40:06 UTC 2024 x86_64
User: www (1001)
PHP: 7.4.33
Disabled: passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
Upload Files
File: //proc/2131/cwd/20260313/src/components/HealthComponents.tsx
import React from 'react';
import { motion } from 'motion/react';
import { 
  Heart, 
  Thermometer, 
  Wind,
  RefreshCw
} from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import { OuraData } from '../types';

export function HealthScoreGauge({ score }: { score: number | null }) {
  if (score === null) return null;

  const getScoreColor = (s: number) => {
    if (s >= 85) return 'text-emerald-500';
    if (s >= 70) return 'text-blue-500';
    if (s >= 50) return 'text-orange-500';
    return 'text-red-500';
  };

  const getScoreBg = (s: number) => {
    if (s >= 85) return 'bg-emerald-50';
    if (s >= 70) return 'bg-blue-50';
    if (s >= 50) return 'bg-orange-50';
    return 'bg-red-50';
  };

  const getScoreLabel = (s: number) => {
    if (s >= 85) return '极佳';
    if (s >= 70) return '良好';
    if (s >= 50) return '一般';
    return '需注意';
  };

  return (
    <motion.div 
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      className="bg-white rounded-2xl md:rounded-3xl p-6 md:p-8 shadow-sm border border-stone-200/60 flex flex-col md:flex-row items-center gap-6 md:gap-10"
    >
      <div className="relative w-32 h-32 md:w-40 md:h-40 flex items-center justify-center">
        <svg className="w-full h-full -rotate-90">
          <circle
            cx="50%"
            cy="50%"
            r="45%"
            className="fill-none stroke-stone-100 stroke-[8]"
          />
          <motion.circle
            cx="50%"
            cy="50%"
            r="45%"
            initial={{ strokeDasharray: "0 1000" }}
            animate={{ strokeDasharray: `${score * 2.82} 1000` }}
            transition={{ duration: 1.5, ease: "easeOut" }}
            className={`fill-none stroke-[8] stroke-linecap-round ${getScoreColor(score).replace('text-', 'stroke-')}`}
          />
        </svg>
        <div className="absolute inset-0 flex flex-col items-center justify-center">
          <span className={`text-3xl md:text-5xl font-black ${getScoreColor(score)}`}>{score}</span>
          <span className="text-[10px] md:text-xs text-stone-400 font-bold uppercase tracking-widest">Health Score</span>
        </div>
      </div>
      
      <div className="flex-1 text-center md:text-left">
        <div className="flex items-center justify-center md:justify-start gap-2 mb-2">
          <h2 className="text-xl md:text-2xl font-bold text-stone-800">今日综合健康指数</h2>
          <span className={`px-2 py-0.5 rounded-full text-[10px] md:text-xs font-bold ${getScoreBg(score)} ${getScoreColor(score)}`}>
            {getScoreLabel(score)}
          </span>
        </div>
        <p className="text-sm md:text-base text-stone-500 leading-relaxed max-w-md">
          基于您的准备程度、睡眠质量及活动强度综合计算。{score >= 85 ? '您现在的身体状态非常出色,适合迎接高强度挑战!' : score >= 70 ? '状态平稳,保持规律作息是维持健康的关键。' : '建议今天适当减压,关注身体发出的疲劳信号。'}
        </p>
      </div>
    </motion.div>
  );
}

export function StatCard({ title, value, icon, color }: { title: string, value: any, icon: React.ReactNode, color: string }) {
  return (
    <motion.div 
      whileHover={{ y: -4 }}
      className="bg-white rounded-2xl md:rounded-3xl p-3 md:p-6 shadow-sm border border-stone-200/60 flex flex-col md:flex-row items-center md:justify-between text-center md:text-left gap-2 md:gap-0"
    >
      <div className="order-2 md:order-1">
        <p className="text-stone-400 text-[10px] md:text-sm font-medium mb-0.5 md:mb-1">{title}</p>
        <p className="text-lg md:text-3xl font-bold">{value}</p>
      </div>
      <div className={`w-8 h-8 md:w-12 md:h-12 ${color} rounded-lg md:rounded-2xl flex items-center justify-center order-1 md:order-2`}>
        {icon}
      </div>
    </motion.div>
  );
}

export function HealthWarnings({ data }: { data: OuraData | null }) {
  if (!data) return null;
  
  const lastReadiness = data.readiness[data.readiness.length - 1];
  const lastSleep = data.sleep[data.sleep.length - 1];
  const warnings = [];

  // Temperature check
  const tempDev = lastReadiness?.contributors?.temperature_deviation || 0;
  if (tempDev > 0.5) {
    warnings.push({
      type: 'warning',
      icon: <Thermometer className="text-orange-500" size={18} />,
      title: '体温异常升高',
      message: `体温偏离基准线 +${tempDev.toFixed(1)}°C,可能预示身体正在应对压力或潜在不适。`
    });
  }

  // RHR check
  const currentRHR = lastReadiness?.contributors?.resting_heart_rate || 0;
  const avgRHR = data.readiness.reduce((acc, curr) => acc + (curr.contributors?.resting_heart_rate || 0), 0) / data.readiness.length;
  if (currentRHR > avgRHR + 5) {
    warnings.push({
      type: 'info',
      icon: <Heart className="text-red-500" size={18} />,
      title: '静息心率偏高',
      message: `当前 RHR (${currentRHR} bpm) 显著高于您的周平均水平 (${Math.round(avgRHR)} bpm),建议增加休息。`
    });
  }

  // Respiratory Rate check
  const respRate = lastSleep?.average_breath || 0;
  const avgResp = data.sleep.reduce((acc, curr) => acc + (curr.average_breath || 0), 0) / data.sleep.length;
  if (Math.abs(respRate - avgResp) > 1.5) {
    warnings.push({
      type: 'info',
      icon: <Wind className="text-blue-500" size={18} />,
      title: '呼吸频率波动',
      message: `睡眠呼吸频率 (${respRate.toFixed(1)}) 出现波动,请留意睡眠环境或身体状态。`
    });
  }

  if (warnings.length === 0) return null;

  return (
    <div className="space-y-3">
      {warnings.map((w, i) => (
        <motion.div 
          key={i}
          initial={{ opacity: 0, x: -20 }}
          animate={{ opacity: 1, x: 0 }}
          className={`flex items-start gap-3 p-4 rounded-xl md:rounded-2xl border ${
            w.type === 'warning' ? 'bg-orange-50 border-orange-100' : 'bg-blue-50 border-blue-100'
          }`}
        >
          <div className="mt-0.5">{w.icon}</div>
          <div>
            <h4 className={`text-sm font-bold ${w.type === 'warning' ? 'text-orange-800' : 'text-blue-800'}`}>
              {w.title}
            </h4>
            <p className={`text-xs mt-1 ${w.type === 'warning' ? 'text-orange-700' : 'text-blue-700'}`}>
              {w.message}
            </p>
          </div>
        </motion.div>
      ))}
    </div>
  );
}

export function SleepStageChart({ data }: { data: any }) {
  if (!data) return null;

  const chartData = [
    { name: '深睡', value: data.deep_sleep_duration || 0, color: '#4338ca' },
    { name: 'REM', value: data.rem_sleep_duration || 0, color: '#3b82f6' },
    { name: '浅睡', value: data.light_sleep_duration || 0, color: '#94a3b8' },
    { name: '清醒', value: data.awake_duration || 0, color: '#f1f5f9' },
  ];

  return (
    <ResponsiveContainer width="100%" height="100%">
      <PieChart>
        <Pie
          data={chartData}
          cx="50%"
          cy="50%"
          innerRadius={60}
          outerRadius={80}
          paddingAngle={5}
          dataKey="value"
        >
          {chartData.map((entry, index) => (
            <Cell key={`cell-${index}`} fill={entry.color} />
          ))}
        </Pie>
        <Tooltip 
          formatter={(value: number) => formatDuration(value)}
          contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
        />
        <Legend verticalAlign="bottom" height={36}/>
      </PieChart>
    </ResponsiveContainer>
  );
}

export function formatDuration(seconds: number | undefined) {
  if (!seconds) return '0h 0m';
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  return `${h}h ${m}m`;
}

export function ContributorSection({ title, icon, contributors, labels }: { title: string, icon: React.ReactNode, contributors: any, labels: Record<string, string> }) {
  if (!contributors) return null;

  return (
    <div className="bg-white rounded-2xl md:rounded-3xl p-4 md:p-8 shadow-sm border border-stone-200/60">
      <div className="flex items-center gap-2 font-semibold mb-6">
        {icon}
        <span className="text-sm md:text-base">{title}</span>
      </div>
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
        {Object.entries(labels).map(([key, label]) => {
          const value = contributors[key];
          if (value === undefined) return null;
          
          return (
            <div key={key} className="space-y-1.5">
              <div className="flex justify-between items-center">
                <span className="text-[10px] md:text-xs text-stone-400 font-medium">{label}</span>
                <span className="text-[10px] md:text-xs font-bold text-stone-600">{value}</span>
              </div>
              <div className="h-1.5 bg-stone-100 rounded-full overflow-hidden">
                <motion.div 
                  initial={{ width: 0 }}
                  animate={{ width: `${value}%` }}
                  transition={{ duration: 1, ease: "easeOut" }}
                  className={`h-full rounded-full ${
                    value >= 85 ? 'bg-emerald-500' : 
                    value >= 70 ? 'bg-blue-500' : 
                    value >= 50 ? 'bg-orange-400' : 'bg-red-400'
                  }`}
                />
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}