Files
ljwx-health/ljwx-bigscreen/bigscreen/bigScreen/templates/personal_star.html
T
bruno ce2937c9d1 feat: 添加 ljwx-bigscreen/bigscreen/bigScreen 目录所有重要代码文件
- 🐍 添加所有 Python 源码文件(*.py)
- 📄 添加所有 HTML 模板文件(templates/)
- 🗂️ 添加控制器、模型、测试等目录结构
- 📋 添加文档文件(*.md)和配置文件
- ⚙️ 更新 .gitignore 配置,只忽略日志和 SQL 文件
- 🚀 确保所有重要代码都被 Git 跟踪和版本控制

根据要求:ljwx-bigscreen/bigscreen/bigScreen 里面除了 log 和 sql 都需要提交
2025-08-14 21:09:12 +08:00

736 lines
41 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>赛博朋克健康行星系统</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#000;color:#0ff;font-family:'Consolas','Microsoft YaHei';overflow:hidden;height:100vh}
#canvas3d{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1}
.hud-overlay{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100}
.planet-info{position:absolute;background:linear-gradient(145deg,rgba(0,20,40,0.95),rgba(0,255,255,0.08));border:2px solid transparent;border-radius:20px;padding:20px;backdrop-filter:blur(15px);pointer-events:auto;cursor:pointer;transition:all 0.4s cubic-bezier(0.25,0.8,0.25,1);box-shadow:0 8px 32px rgba(0,255,255,0.15),inset 0 1px 0 rgba(255,255,255,0.1);min-width:120px;transform-style:preserve-3d}
.planet-info::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(45deg,#0ff,#0f9,#09f,#f0f);border-radius:20px;padding:2px;mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask-composite:xor;z-index:-1;opacity:0.8}
.planet-info::after{content:'';position:absolute;top:-2px;left:-2px;right:-2px;bottom:-2px;background:linear-gradient(45deg,rgba(0,255,255,0.3),rgba(0,255,157,0.3),rgba(0,157,255,0.3),rgba(255,0,255,0.3));border-radius:22px;z-index:-2;filter:blur(8px);opacity:0}
.planet-info:hover{transform:scale(1.15) translateZ(10px);box-shadow:0 20px 60px rgba(0,255,255,0.4),inset 0 1px 0 rgba(255,255,255,0.2);border-color:#0f9}
.planet-info:hover::after{opacity:1}
.planet-value{font-size:22px;font-weight:900;color:#fff;text-shadow:0 0 15px #0ff,0 0 30px #0ff;text-align:center;margin-bottom:8px;letter-spacing:1px}
.planet-label{font-size:14px;color:#0f9;text-align:center;font-weight:600;text-transform:uppercase;letter-spacing:2px;margin-bottom:10px}
.planet-status{width:24px;height:24px;border-radius:50%;margin:8px auto;position:relative;overflow:hidden}
.planet-status::before{content:'';position:absolute;top:0;left:0;width:100%;height:100%;border-radius:50%;background:inherit;filter:blur(8px);opacity:0.6}
.status-normal{background:radial-gradient(circle,#0f9,#0a6);box-shadow:0 0 20px #0f9,inset 0 0 10px rgba(255,255,255,0.3)}
.status-warning{background:radial-gradient(circle,#ff0,#fa0);box-shadow:0 0 20px #ff0,inset 0 0 10px rgba(255,255,255,0.3)}
.status-critical{background:radial-gradient(circle,#f44,#a22);box-shadow:0 0 20px #f44,inset 0 0 10px rgba(255,255,255,0.3)}
.main-title{position:fixed;top:30px;left:50%;transform:translateX(-50%);color:#0ff;font-size:28px;font-weight:bold;text-shadow:0 0 20px #0ff;z-index:200;pointer-events:none;animation:titleGlow 3s infinite}
@keyframes titleGlow{0%,100%{text-shadow:0 0 20px #0ff}50%{text-shadow:0 0 40px #0f9}}
.cyber-panel{position:fixed;background:linear-gradient(135deg,rgba(0,255,255,0.05),rgba(0,20,40,0.8));border:1px solid rgba(0,255,255,0.3);border-radius:10px;backdrop-filter:blur(15px);padding:20px;z-index:150}
.left-panel{left:20px;top:20%;width:280px;height:60vh}
.right-panel{right:20px;top:20%;width:280px;height:60vh}
.panel-title{color:#0ff;font-size:16px;font-weight:bold;text-align:center;margin-bottom:15px;text-shadow:0 0 10px #0ff}
.info-list{max-height:calc(60vh - 80px);overflow-y:auto}
.info-item{background:rgba(0,255,255,0.05);border:1px solid rgba(0,255,255,0.2);border-radius:8px;padding:12px;margin:8px 0;transition:all 0.3s ease}
.info-item:hover{background:rgba(0,255,255,0.1);transform:translateX(5px)}
.info-item-title{color:#0ff;font-size:14px;font-weight:bold}
.info-item-desc{color:#0f9;font-size:12px;margin-top:5px}
.info-item-time{color:#888;font-size:10px;margin-top:3px}
.status-bar{position:fixed;top:20px;right:20px;display:flex;gap:15px;z-index:200}
.status-indicator{width:12px;height:12px;border-radius:50%;animation:pulse 2s infinite}
.battery-display{position:fixed;top:20px;left:20px;display:flex;align-items:center;gap:10px;color:#0ff;z-index:200}
.battery-icon{width:40px;height:20px;border:2px solid #0ff;border-radius:3px;position:relative}
.battery-level{height:100%;background:linear-gradient(90deg,#f44,#ff0,#0f9);border-radius:1px;transition:width 0.3s ease}
.battery-tip{position:absolute;right:-5px;top:6px;width:3px;height:8px;background:#0ff;border-radius:0 1px 1px 0}
.chart-modal{position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.9);display:none;z-index:3000;backdrop-filter:blur(20px)}
.chart-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:900px;height:600px;background:linear-gradient(135deg,rgba(0,20,40,0.95),rgba(0,255,255,0.05));border:2px solid #0ff;border-radius:20px;backdrop-filter:blur(25px);box-shadow:0 30px 80px rgba(0,20,40,0.9)}
.chart-header{padding:20px;border-bottom:1px solid rgba(0,255,255,0.3);display:flex;justify-content:space-between;align-items:center}
.chart-title{color:#0ff;font-size:24px;font-weight:bold;text-shadow:0 0 15px #0ff}
.close-btn{width:40px;height:40px;border-radius:50%;background:rgba(255,68,68,0.8);border:none;color:#fff;font-size:20px;cursor:pointer;transition:all 0.3s ease}
.close-btn:hover{background:#f44;transform:scale(1.1)}
.chart-content{display:flex;height:520px}
.chart-main{flex:2;padding:20px}
.chart-canvas{width:100%;height:100%;background:rgba(0,20,40,0.3);border-radius:10px}
.chart-sidebar{flex:1;padding:20px;border-left:1px solid rgba(0,255,255,0.3);overflow-y:auto}
.stat-card{background:rgba(0,20,40,0.8);border:1px solid rgba(0,255,255,0.3);border-radius:10px;padding:15px;margin:10px 0;transition:all 0.3s ease}
.stat-card:hover{background:rgba(0,255,255,0.05);transform:translateX(5px)}
.stat-title{color:#0ff;font-size:16px;font-weight:bold;margin-bottom:10px}
.stat-value{color:#0f9;font-size:20px;font-weight:bold;text-shadow:0 0 5px #0f9}
.stat-unit{color:#0cf;font-size:14px;margin-left:5px}
.stat-desc{color:#0cf;font-size:12px;margin-top:5px}
.baseline-info{background:rgba(0,255,255,0.05);border:1px solid rgba(0,255,255,0.3);border-radius:10px;padding:15px;margin:10px 0}
.baseline-title{color:#0ff;font-size:14px;font-weight:bold;margin-bottom:8px}
.baseline-range{color:#0f9;font-size:16px;font-weight:bold}
.loading{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#0ff;font-size:16px;z-index:1000}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
@media(max-width:1600px){.cyber-panel{width:250px}.chart-container{width:800px;height:500px}}
@media(max-width:1366px){.cyber-panel{width:220px}.chart-container{width:700px;height:450px}}
</style>
</head>
<body>
<div class="main-title">{{ BIGSCREEN_TITLE or '赛博朋克健康行星系统' }}</div>
<div class="battery-display">
<div class="battery-icon">
<div class="battery-level" id="batteryLevel"></div>
<div class="battery-tip"></div>
</div>
<span id="batteryText">0%</span>
</div>
<div class="status-bar">
<div class="status-indicator" id="onlineStatus" style="background:#0f9"></div>
<div class="status-indicator" id="chargingStatus" style="background:#ff0"></div>
<div class="status-indicator" id="wearStatus" style="background:#f44"></div>
</div>
<canvas id="canvas3d"></canvas>
<div class="hud-overlay" id="hudOverlay"></div>
<div class="cyber-panel left-panel">
<div class="panel-title">系统警报</div>
<div class="info-list" id="alertList"></div>
</div>
<div class="cyber-panel right-panel">
<div class="panel-title">数据传输</div>
<div class="info-list" id="messageList"></div>
</div>
<div class="chart-modal" id="chartModal">
<div class="chart-container">
<div class="chart-header">
<div class="chart-title" id="chartTitle">行星数据分析</div>
<button class="close-btn" onclick="closeChart()">&times;</button>
</div>
<div class="chart-content">
<div class="chart-main">
<div class="chart-canvas" id="chartCanvas"></div>
<div class="loading" id="chartLoading">数据同步中...</div>
</div>
<div class="chart-sidebar" id="chartSidebar"></div>
</div>
</div>
</div>
<script>
let scene,camera,renderer,planetSystem,controls,currentChart=null,healthPlanets=[]; // 行星系统核心变量
const PLANETS_CONFIG=[
{id:'heartRate',name:'心率',color:0xff3366,orbit:2.5,speed:0.003,size:0.15,metric:'heart_rate'},
{id:'bloodOxygen',name:'血氧',color:0x3366ff,orbit:3.2,speed:0.0025,size:0.12,metric:'blood_oxygen'},
{id:'temperature',name:'体温',color:0xff9933,orbit:3.8,speed:0.002,size:0.14,metric:'temperature'},
{id:'stress',name:'压力',color:0xff33ff,orbit:4.5,speed:0.0015,size:0.11,metric:'stress'},
{id:'steps',name:'步数',color:0x33ff66,orbit:5.2,speed:0.001,size:0.13,metric:'step'},
{id:'distance',name:'距离',color:0x66ff33,orbit:5.8,speed:0.0008,size:0.12,metric:'distance'},
{id:'calorie',name:'卡路里',color:0xffff33,orbit:6.5,speed:0.0006,size:0.14,metric:'calorie'},
{id:'sleep',name:'睡眠',color:0x33ffff,orbit:7.2,speed:0.0004,size:0.16,metric:'sleep'}
]; // 行星配置数据,更慢的旋转速度
function init3D(){
try{
// Three.js兼容性检查
if(!window.THREE){
console.error('Three.js未加载');
return;
}
console.log('Three.js版本:',THREE.REVISION);
const canvas=document.getElementById('canvas3d');
scene=new THREE.Scene();
camera=new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,1000);
renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setClearColor(0x000000,1);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
// 赛博朋克灯光系统
const ambientLight=new THREE.AmbientLight(0x0066aa,0.3);
scene.add(ambientLight);
const mainLight=new THREE.DirectionalLight(0x00ffff,1.5);
mainLight.position.set(0,20,10);
mainLight.castShadow=true;
mainLight.shadow.mapSize.width=2048;
mainLight.shadow.mapSize.height=2048;
scene.add(mainLight);
const neonLight1=new THREE.PointLight(0xff0099,2,20);
neonLight1.position.set(10,5,0);
scene.add(neonLight1);
const neonLight2=new THREE.PointLight(0x00ff99,2,20);
neonLight2.position.set(-10,5,0);
scene.add(neonLight2);
createPlanetSystem(); // 创建行星系统
createCyberGrid(); // 创建赛博网格
createParticleField(); // 创建粒子场
camera.position.set(0,8,12);
camera.lookAt(0,0,0);
animate();
}catch(e){
console.error('初始化失败:',e);
}
}
function createPlanetSystem(){
planetSystem=new THREE.Group();
// 中央AI核心
const coreGeo=new THREE.SphereGeometry(0.8,32,32);
const coreMat=new THREE.MeshPhongMaterial({
color:0x0099ff,transparent:true,opacity:0.8,
emissive:0x003366,shininess:100
});
const core=new THREE.Mesh(coreGeo,coreMat);
core.castShadow=true;
planetSystem.add(core);
// 核心发光环
const ringGeo=new THREE.RingGeometry(1.2,1.4,64);
const ringMat=new THREE.MeshBasicMaterial({
color:0x00ffff,transparent:true,opacity:0.6,
side:THREE.DoubleSide
});
const ring=new THREE.Mesh(ringGeo,ringMat);
ring.rotation.x=-Math.PI/2;
planetSystem.add(ring);
// 创建行星和轨道
PLANETS_CONFIG.forEach((config,i)=>{
const planetGroup=new THREE.Group();
// 轨道线
const orbitGeo=new THREE.RingGeometry(config.orbit-0.02,config.orbit+0.02,128);
const orbitMat=new THREE.MeshBasicMaterial({
color:0x0099ff,transparent:true,opacity:0.3,
side:THREE.DoubleSide
});
const orbit=new THREE.Mesh(orbitGeo,orbitMat);
orbit.rotation.x=-Math.PI/2;
planetSystem.add(orbit);
// 行星本体
const planetGeo=new THREE.SphereGeometry(config.size,16,16);
const planetMat=new THREE.MeshPhongMaterial({
color:config.color,transparent:true,opacity:0.9,
emissive:config.color,emissiveIntensity:0.3,
shininess:100
});
const planet=new THREE.Mesh(planetGeo,planetMat);
planet.position.x=config.orbit;
planet.castShadow=true;
planet.userData={config,angle:i*Math.PI/4,info:null};
planetGroup.add(planet);
// 行星发光光环
const glowGeo=new THREE.RingGeometry(config.size*1.5,config.size*2,32);
const glowMat=new THREE.MeshBasicMaterial({
color:config.color,transparent:true,opacity:0.4,
side:THREE.DoubleSide
});
const glow=new THREE.Mesh(glowGeo,glowMat);
glow.position.copy(planet.position);
glow.lookAt(0,0,1);
planetGroup.add(glow);
planetSystem.add(planetGroup);
healthPlanets.push({planet,glow,group:planetGroup,config});
// 创建HUD信息面板
createPlanetHUD(config,i);
});
scene.add(planetSystem);
}
function createPlanetHUD(config,index){
const hudDiv=document.createElement('div');
hudDiv.className='planet-info';
hudDiv.id=`planet-${config.id}`;
hudDiv.innerHTML=`
<div class="planet-value" id="value-${config.id}">--</div>
<div class="planet-label">${config.name}</div>
<div class="planet-status status-normal" id="status-${config.id}"></div>
`;
hudDiv.addEventListener('click',()=>showHealthChart(config.metric));
document.getElementById('hudOverlay').appendChild(hudDiv);
}
function createCyberGrid(){
// 底面网格
const gridSize=20,divisions=40;
const gridHelper=new THREE.GridHelper(gridSize,divisions,0x0099ff,0x003366);
gridHelper.position.y=-2;
if(gridHelper.material){
gridHelper.material.transparent=true;
gridHelper.material.opacity=0.3;
}
scene.add(gridHelper);
// 竖直网格线
for(let i=0;i<8;i++){
const angle=i*Math.PI/4;
const lineGeo=new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(Math.cos(angle)*3,0,Math.sin(angle)*3),
new THREE.Vector3(Math.cos(angle)*8,0,Math.sin(angle)*8)
]);
const lineMat=new THREE.LineBasicMaterial({color:0x0099ff,transparent:true,opacity:0.5});
const line=new THREE.Line(lineGeo,lineMat);
scene.add(line);
}
// 添加动态光环
for(let i=0;i<3;i++){
const ringGeo=new THREE.RingGeometry(10+i*8,10.5+i*8,64);
const ringMat=new THREE.MeshBasicMaterial({
color:0x00ffff,transparent:true,opacity:0.1-i*0.02,side:THREE.DoubleSide
});
const ring=new THREE.Mesh(ringGeo,ringMat);
ring.rotation.x=-Math.PI/2;
ring.userData={speed:0.001+i*0.0005};
scene.add(ring);
}
// 外层旋转光束
for(let i=0;i<12;i++){
const angle=i*Math.PI/6;
const beamGeo=new THREE.PlaneGeometry(0.05,25);
const beamMat=new THREE.MeshBasicMaterial({
color:0x00aaff,transparent:true,opacity:0.3,side:THREE.DoubleSide
});
const beam=new THREE.Mesh(beamGeo,beamMat);
beam.position.set(0,0,0);
beam.rotation.y=angle;
beam.userData={speed:0.002};
scene.add(beam);
}
// 垂直能量柱
for(let i=0;i<6;i++){
const angle=i*Math.PI/3;
const pillarGeo=new THREE.CylinderGeometry(0.1,0.1,15,8);
const pillarMat=new THREE.MeshBasicMaterial({
color:0x0099ff,transparent:true,opacity:0.2
});
const pillar=new THREE.Mesh(pillarGeo,pillarMat);
pillar.position.set(Math.cos(angle)*15,7.5,Math.sin(angle)*15);
scene.add(pillar);
}
}
function createParticleField(){
// 星空粒子
const starCount=800;
const starPositions=new Float32Array(starCount*3);
const starColors=new Float32Array(starCount*3);
for(let i=0;i<starCount;i++){
starPositions[i*3]=(Math.random()-0.5)*80;
starPositions[i*3+1]=(Math.random()-0.5)*40;
starPositions[i*3+2]=(Math.random()-0.5)*80;
const color=new THREE.Color();
color.setHSL(0.5+Math.random()*0.3,0.8,0.6+Math.random()*0.4);
starColors[i*3]=color.r;
starColors[i*3+1]=color.g;
starColors[i*3+2]=color.b;
}
const starGeo=new THREE.BufferGeometry();
starGeo.setAttribute('position',new THREE.BufferAttribute(starPositions,3));
starGeo.setAttribute('color',new THREE.BufferAttribute(starColors,3));
const starMat=new THREE.PointsMaterial({
size:0.15,vertexColors:THREE.VertexColors||true,transparent:true,opacity:0.8,
blending:THREE.AdditiveBlending
});
const stars=new THREE.Points(starGeo,starMat);
scene.add(stars);
// 流动粒子
const flowCount=200;
const flowPositions=new Float32Array(flowCount*3);
const flowColors=new Float32Array(flowCount*3);
for(let i=0;i<flowCount;i++){
const angle=Math.random()*Math.PI*2;
const radius=5+Math.random()*15;
flowPositions[i*3]=Math.cos(angle)*radius;
flowPositions[i*3+1]=Math.random()*10-5;
flowPositions[i*3+2]=Math.sin(angle)*radius;
const color=new THREE.Color(0x00ffff);
flowColors[i*3]=color.r;
flowColors[i*3+1]=color.g;
flowColors[i*3+2]=color.b;
}
const flowGeo=new THREE.BufferGeometry();
flowGeo.setAttribute('position',new THREE.BufferAttribute(flowPositions,3));
flowGeo.setAttribute('color',new THREE.BufferAttribute(flowColors,3));
const flowMat=new THREE.PointsMaterial({
size:0.3,vertexColors:THREE.VertexColors||true,transparent:true,opacity:0.6,
blending:THREE.AdditiveBlending
});
const flowParticles=new THREE.Points(flowGeo,flowMat);
flowParticles.userData={type:'flow'};
scene.add(flowParticles);
// 能量波纹
for(let i=0;i<5;i++){
const waveGeo=new THREE.RingGeometry(8+i*3,8.2+i*3,64);
const waveMat=new THREE.MeshBasicMaterial({
color:0x00aaff,transparent:true,opacity:0.05,side:THREE.DoubleSide
});
const wave=new THREE.Mesh(waveGeo,waveMat);
wave.rotation.x=-Math.PI/2;
wave.position.y=-1;
wave.userData={type:'wave',speed:0.01+i*0.002,originalY:-1};
scene.add(wave);
}
}
function animate(){
requestAnimationFrame(animate);
const time=Date.now()*0.001;
// 背景动态效果
scene.children.forEach(child=>{
if(child.userData){
if(child.userData.speed&&child.geometry){
if(child.geometry.type==='RingGeometry'){
child.rotation.z+=child.userData.speed;
}else if(child.geometry.type==='PlaneGeometry'){
child.rotation.y+=child.userData.speed;
}
}
// 流动粒子动画
if(child.userData.type==='flow'){
const positions=child.geometry.attributes.position.array;
for(let i=0;i<positions.length;i+=3){
positions[i+1]+=0.02;
if(positions[i+1]>5){positions[i+1]=-5;}
}
child.geometry.attributes.position.needsUpdate=true;
}
// 能量波纹动画
if(child.userData.type==='wave'){
child.position.y=child.userData.originalY+Math.sin(time*2+child.userData.speed*100)*0.5;
child.material.opacity=0.05+Math.sin(time*3+child.userData.speed*150)*0.02;
}
}
});
if(planetSystem){
// 行星轨道运动
healthPlanets.forEach((item,i)=>{
const config=item.config;
item.planet.userData.angle+=config.speed;
const angle=item.planet.userData.angle;
item.planet.position.x=Math.cos(angle)*config.orbit;
item.planet.position.z=Math.sin(angle)*config.orbit;
item.planet.rotation.y+=0.02;
item.glow.position.copy(item.planet.position);
const cameraDir=new THREE.Vector3();
cameraDir.subVectors(camera.position,item.glow.position).normalize();
item.glow.lookAt(camera.position.x,camera.position.y,camera.position.z);
item.glow.rotation.z+=0.01;
// 更新HUD位置
updatePlanetHUDPosition(config.id,item.planet.position);
});
// 核心自转
planetSystem.children[0].rotation.y+=0.002;
planetSystem.children[1].rotation.z+=0.005;
}
renderer.render(scene,camera);
}
function updatePlanetHUDPosition(planetId,worldPos){
const hudElement=document.getElementById(`planet-${planetId}`);
if(!hudElement)return;
const vector=worldPos.clone().project(camera);
const x=(vector.x*0.5+0.5)*window.innerWidth;
const y=(-vector.y*0.5+0.5)*window.innerHeight;
hudElement.style.left=`${x-50}px`;
hudElement.style.top=`${y-40}px`;
}
function updateHealthPoint(id,value,unit='',alertLevel='normal'){
const valueEl=document.getElementById(`value-${id}`);
const statusEl=document.getElementById(`status-${id}`);
if(!valueEl||!statusEl)return;
valueEl.textContent=value+unit;
statusEl.className=`planet-status status-${alertLevel==='critical'?'critical':alertLevel==='medium'?'warning':'normal'}`;
// 更新行星效果
const planet=healthPlanets.find(p=>p.config.id===id);
if(planet){
const intensity=alertLevel==='critical'?1:alertLevel==='medium'?0.7:0.3;
planet.planet.material.emissiveIntensity=intensity;
planet.glow.material.opacity=0.4+intensity*0.4;
}
}
function updateDeviceStatus(online,charging,wearing){
document.getElementById('onlineStatus').style.background=online?'#0f9':'#f44';
document.getElementById('chargingStatus').style.background=charging?'#ff0':'#666';
document.getElementById('wearStatus').style.background=wearing?'#0f9':'#f44';
}
function updateBattery(level){
const batteryEl=document.getElementById('batteryLevel');
const textEl=document.getElementById('batteryText');
batteryEl.style.width=level+'%';
textEl.textContent=level+'%';
}
function getAlertLevel(type,value){
const thresholds={heart_rate:[60,100],blood_oxygen:[95,100],temperature:[36,37.5],stress:[0,50]};
if(!thresholds[type])return 'normal';
const[min,max]=thresholds[type];
return value<min||value>max?'critical':value<min*1.1||value>max*0.9?'medium':'normal';
}
async function showHealthChart(metric){
const modal=document.getElementById('chartModal');
const title=document.getElementById('chartTitle');
const canvas=document.getElementById('chartCanvas');
const sidebar=document.getElementById('chartSidebar');
const loading=document.getElementById('chartLoading');
modal.style.display='block';
loading.style.display='block';
const deviceSn='{{deviceSn}}';
try{
const metricNames={heart_rate:'心率',blood_oxygen:'血氧',temperature:'体温',stress:'压力',step:'步数',distance:'距离',calorie:'卡路里',sleep:'睡眠'};
title.textContent=`${metricNames[metric]||metric}行星数据分析`;
// 并行获取趋势数据和基线数据
const[trendsResponse,baselineResponse]=await Promise.all([
fetch(`./api/health/trends/${metric}?deviceSn=${deviceSn}&days=7`),
fetch(`./api/health/baseline/${metric}?deviceSn=${deviceSn}`)
]);
const trendsData=await trendsResponse.json();
const baselineData=await baselineResponse.json();
if(trendsData.success){
renderTrendChart(canvas,trendsData.data,metric);
renderSidebar(sidebar,trendsData.data,baselineData.success?baselineData.data:null);
}else{
// 使用模拟数据
renderMockChart(canvas,metric);
renderMockSidebar(sidebar,metric);
}
loading.style.display='none';
}catch(e){
console.error('API调用失败:',e);
// 使用模拟数据
renderMockChart(canvas,metric);
renderMockSidebar(sidebar,metric);
loading.style.display='none';
}
}
function closeChart(){
document.getElementById('chartModal').style.display='none';
if(currentChart){currentChart.dispose();currentChart=null;}
}
function renderAlerts(alerts){
const listEl=document.getElementById('alertList');
if(!alerts||!alerts.length){listEl.innerHTML='<div class="info-item"><div class="info-item-title">系统正常</div></div>';return;}
listEl.innerHTML=alerts.slice(0,6).map(alert=>`<div class="info-item"><div class="info-item-title">${alert.alert_type||'异常警报'}</div><div class="info-item-desc">${alert.alert_desc||'数据异常'}</div><div class="info-item-time">${alert.alert_timestamp||''}</div></div>`).join('');
}
function renderMessages(messages){
const listEl=document.getElementById('messageList');
if(!messages||!messages.length){listEl.innerHTML='<div class="info-item"><div class="info-item-title">传输正常</div></div>';return;}
listEl.innerHTML=messages.slice(0,6).map(msg=>`<div class="info-item"><div class="info-item-title">${msg.message_type||'数据包'}</div><div class="info-item-desc">${msg.message||'传输完成'}</div><div class="info-item-time">${msg.sent_time||''}</div></div>`).join('');
}
async function fetchAndUpdate(){
try{
const response=await fetch(`./get_personal_info?deviceSn={{deviceSn}}`);
const data=await response.json();
if(data?.success){
const health=data.data.health_data?.data?.healthData?.[0];
if(health){
updateHealthPoint('heartRate',health.heart_rate||'--','',getAlertLevel('heart_rate',parseInt(health.heart_rate)));
updateHealthPoint('bloodOxygen',health.blood_oxygen||'--','%',getAlertLevel('blood_oxygen',parseInt(health.blood_oxygen)));
updateHealthPoint('temperature',health.temperature||'--','°C',getAlertLevel('temperature',parseFloat(health.temperature)));
updateHealthPoint('stress',health.stress||'--','',getAlertLevel('stress',parseInt(health.stress)));
updateHealthPoint('steps',health.step||'--');
updateHealthPoint('distance',(health.distance||0).toFixed(1),'km');
updateHealthPoint('calorie',Math.round(health.calorie||0));
updateHealthPoint('sleep',(health.sleep||0).toFixed(1),'h');
}
const device=data.data.device_info?.data?.devices?.[0];
if(device){
updateBattery(device.battery_level||0);
updateDeviceStatus(device.status==='ACTIVE',device.charging_status==='CHARGING',device.wearable_status==='WORN');
}
renderAlerts(data.data.alert_info?.data?.alerts);
renderMessages(data.data.message_info?.data?.messages);
}
}catch(e){
console.error('数据获取失败:',e);
updateHealthPoint('heartRate','59','',getAlertLevel('heart_rate',59));
updateHealthPoint('bloodOxygen','99','%',getAlertLevel('blood_oxygen',99));
updateHealthPoint('temperature','36.6','°C',getAlertLevel('temperature',36.6));
updateHealthPoint('stress','29','',getAlertLevel('stress',29));
updateHealthPoint('steps','3');
updateHealthPoint('distance','23.3','km');
updateHealthPoint('calorie','19825');
updateHealthPoint('sleep','6.2','h');
updateBattery(15);
updateDeviceStatus(true,true,false);
}
}
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
document.addEventListener('DOMContentLoaded',()=>{
init3D();
fetchAndUpdate();
setInterval(fetchAndUpdate,5000);
});
function renderMockChart(container,metric){
if(currentChart)currentChart.dispose();
currentChart=echarts.init(container);
const metricConfig={
heart_rate:{unit:'次/分',color:'#ff3366',data:[65,68,72,70,69,73,71]},
blood_oxygen:{unit:'%',color:'#3366ff',data:[98,97,99,98,99,97,98]},
temperature:{unit:'°C',color:'#ff9933',data:[36.4,36.6,36.5,36.7,36.6,36.8,36.6]},
stress:{unit:'',color:'#ff33ff',data:[25,30,28,32,29,27,31]},
step:{unit:'步',color:'#33ff66',data:[8520,9200,7800,10500,8900,9600,8750]},
distance:{unit:'km',color:'#66ff33',data:[6.8,7.2,6.3,8.1,7.0,7.5,6.9]},
calorie:{unit:'千卡',color:'#ffff33',data:[2150,2300,2050,2400,2200,2350,2180]},
sleep:{unit:'h',color:'#33ffff',data:[7.2,6.8,7.5,6.9,7.1,7.3,7.0]}
};
const config=metricConfig[metric]||metricConfig.heart_rate;
const dates=['12-01','12-02','12-03','12-04','12-05','12-06','12-07'];
const option={
backgroundColor:'transparent',
title:{text:`${metric}趋势分析`,textStyle:{color:'#0ff',fontSize:20,fontWeight:'bold'},left:'center'},
tooltip:{trigger:'axis',backgroundColor:'rgba(0,20,40,0.9)',borderColor:'rgba(0,255,255,0.6)',textStyle:{color:'#fff'}},
grid:{left:60,right:40,top:80,bottom:60,borderColor:'rgba(0,255,255,0.4)'},
xAxis:{type:'category',data:dates,axisLine:{lineStyle:{color:'rgba(0,255,255,0.6)'}},axisLabel:{color:'#0cf'}},
yAxis:{type:'value',axisLine:{lineStyle:{color:'rgba(0,255,255,0.6)'}},axisLabel:{color:'#0cf'},splitLine:{lineStyle:{color:'rgba(0,255,255,0.15)'}}},
series:[{
name:metric,type:'line',data:config.data,smooth:true,symbol:'circle',symbolSize:8,
lineStyle:{color:config.color,width:3,shadowColor:config.color,shadowBlur:10},
itemStyle:{color:config.color,shadowColor:config.color,shadowBlur:5},
areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:config.color+'90'},{offset:1,color:config.color+'30'}]}}
}]
};
currentChart.setOption(option);
}
function renderMockSidebar(container,metric){
const metricConfig={
heart_rate:{unit:'次/分',avg:69,min:65,max:73,normal:[60,100]},
blood_oxygen:{unit:'%',avg:98,min:97,max:99,normal:[95,100]},
temperature:{unit:'°C',avg:36.6,min:36.4,max:36.8,normal:[36.0,37.5]},
stress:{unit:'',avg:29,min:25,max:32,normal:[0,50]},
step:{unit:'步',avg:8900,min:7800,max:10500,normal:[6000,12000]},
distance:{unit:'km',avg:7.1,min:6.3,max:8.1,normal:[5,10]},
calorie:{unit:'千卡',avg:2230,min:2050,max:2400,normal:[1800,2500]},
sleep:{unit:'h',avg:7.1,min:6.8,max:7.5,normal:[7,9]}
};
const config=metricConfig[metric]||metricConfig.heart_rate;
container.innerHTML=`
<div class="stat-card">
<div class="stat-title">当前统计</div>
<div class="stat-value">${config.avg}<span class="stat-unit">${config.unit}</span></div>
<div class="stat-desc">7天平均值</div>
</div>
<div class="stat-card">
<div class="stat-title">数值范围</div>
<div class="stat-value">${config.min} - ${config.max}<span class="stat-unit">${config.unit}</span></div>
<div class="stat-desc">最小值到最大值</div>
</div>
<div class="baseline-info">
<div class="baseline-title">正常范围</div>
<div class="baseline-range">${config.normal[0]} - ${config.normal[1]} ${config.unit}</div>
</div>
`;
}
function renderTrendChart(container,data,metric){
if(currentChart)currentChart.dispose();
currentChart=echarts.init(container);
const trendData=data.trend_data||[];
const xAxisData=trendData.map(item=>item.date);
const seriesData=trendData.map(item=>item.value);
const normalRange=data.statistics.normal_range;
const color=data.chart_config.color;
const option={
backgroundColor:'transparent',
title:{text:data.chart_config.title,textStyle:{color:'#0ff',fontSize:20,fontWeight:'bold'},left:'center'},
tooltip:{
trigger:'axis',backgroundColor:'rgba(0,20,40,0.9)',borderColor:'rgba(0,255,255,0.6)',textStyle:{color:'#fff'},
formatter:function(params){
const point=params[0];
return `<div style="padding:12px;"><div style="color:#0ff;font-weight:bold;">${point.name}</div><div style="color:#0f9;margin-top:6px;">${point.seriesName}: ${point.value}${data.chart_config.unit}</div><div style="color:#0cf;font-size:13px;margin-top:6px;">正常范围: ${normalRange[0]}-${normalRange[1]}${data.chart_config.unit}</div></div>`;
}
},
legend:{data:[`${data.chart_config.title}值`,'正常范围'],textStyle:{color:'#0cf'},top:35},
grid:{left:70,right:50,top:90,bottom:70,borderColor:'rgba(0,255,255,0.4)'},
xAxis:{type:'category',data:xAxisData,axisLine:{lineStyle:{color:'rgba(0,255,255,0.6)'}},axisTick:{lineStyle:{color:'rgba(0,255,255,0.4)'}},axisLabel:{color:'#0cf',fontSize:13}},
yAxis:{type:'value',axisLine:{lineStyle:{color:'rgba(0,255,255,0.6)'}},axisTick:{lineStyle:{color:'rgba(0,255,255,0.4)'}},axisLabel:{color:'#0cf',fontSize:13},splitLine:{lineStyle:{color:'rgba(0,255,255,0.15)'}}},
series:[
{name:`${data.chart_config.title}值`,type:'line',data:seriesData,smooth:true,symbol:'circle',symbolSize:10,lineStyle:{color:color,width:4,shadowColor:color,shadowBlur:12},itemStyle:{color:color,shadowColor:color,shadowBlur:6},areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:color+'90'},{offset:1,color:color+'30'}]}}},
{name:'正常范围',type:'line',data:xAxisData.map(()=>normalRange[1]),lineStyle:{color:'rgba(0,255,157,0.6)',type:'dashed'},symbol:'none',silent:true},
{name:'正常范围下限',type:'line',data:xAxisData.map(()=>normalRange[0]),lineStyle:{color:'rgba(0,255,157,0.6)',type:'dashed'},symbol:'none',silent:true,areaStyle:{color:'rgba(0,255,157,0.15)'}}
]
};
currentChart.setOption(option);
}
function renderSidebar(container,trendsData,baselineData){
const stats=trendsData.statistics;
const unit=trendsData.chart_config.unit;
let html=`<div class="stat-card"><div class="stat-title">当前统计</div><div class="stat-value">${stats.average}<span class="stat-unit">${unit}</span></div><div class="stat-desc">平均值 (${stats.count}个数据点)</div></div><div class="stat-card"><div class="stat-title">数值范围</div><div class="stat-value">${stats.min} - ${stats.max}<span class="stat-unit">${unit}</span></div><div class="stat-desc">最小值到最大值</div></div><div class="baseline-info"><div class="baseline-title">正常范围</div><div class="baseline-range">${stats.normal_range[0]} - ${stats.normal_range[1]} ${unit}</div></div>`;
if(baselineData)html+=`<div class="stat-card"><div class="stat-title">30天基线</div><div class="stat-value">${baselineData.average}<span class="stat-unit">${unit}</span></div><div class="stat-desc">基于${baselineData.data_points}个数据点</div></div><div class="baseline-info"><div class="baseline-title">个人基线范围</div><div class="baseline-range">${baselineData.percentile_25} - ${baselineData.percentile_75} ${unit}</div></div>`;
container.innerHTML=html;
}
</script>
</body>
</html>