BodyBalanceEvaluation/frontend/src/renderer/src/views/Detection.vue

3540 lines
115 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="detection-container-box">
<Header />
<div class="displaycontainer">
<div class="displayleft">
<img src="@/assets/detection/progress.png" alt="" style=" margin-left:10px;margin-right:15px">
<div style="
font-size: 18px;
font-weight: 700;
font-style: normal;
color: #3BF2C6;"
>检测中...</div>
<div class="patientInfotop1">{{ patientInfo.name }}</div>
<div style="font-size: 20px;"> {{ patientInfo.gender }}</div>
<div class="username-line"></div>
<div style="font-size: 20px;">{{ calculateAge(patientInfo.birth_date) }}</div>
<el-button type="primary" class="endbutton" @click="endClick">结束检测</el-button>
</div>
<div class="displayleft">
<div class="icon-box" :title="remoteStatus =='未连接'?'截图':'截图(有遥控器)'" @click="saveDetectionData">
<img src="@/assets/detection/screenshot.png" alt="">
</div>
<div class="icon-box" @click="isPhotoAlbum = true" title="相册">
<img src="@/assets/detection/photoalbum.svg" alt="">
</div>
<div v-if="!isStartVideo" class="icon-box" @click="startVideoClick"
:title="remoteStatus =='未连接'?'开始录像':'开始录像(有遥控器)'">
<img src="@/assets/detection/startvideo.png" alt="">
</div>
<div v-if="isStartVideo" class="icon-box" @click="stopVideoClick" title="结束录像">
<img src="@/assets/detection/endvideo.png" alt="">
</div>
<div class="startvideo-box" v-if="isStartVideo">
<img src="@/assets/detection/conduct.png" alt="" class="conductanimated">
录像中: {{ formattedTime }}
</div>
<div class="startvideo-box" v-if="!isStartVideo">
</div>
</div>
<div class="displayleft">
<div class="icon-box" title="相机参数设置" @click="cameraUpdate">
<img src="@/assets/detection/settings.png" alt="" >
</div>
<div class="icon-box" title="查看档案" @click="routerClick">
<img src="@/assets/detection/archive.png" alt="" >
</div>
</div>
</div>
<div style="width:100%;height: calc(100% - 125px);" ref="contenGridRef">
<!-- 主内容区域 -->
<el-row :gutter="15" style="padding: 10px;padding-top:0" >
<el-col :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
<div class="body-posture-box">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title1.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">身体姿态</div>
</div>
<div class="body-son-display">
<img :src="femtoboltStatus == '已连接'?refresh_active:refresh" alt="" style="margin-right: 8px;cursor: pointer;"
@click="refreshClick('femtobolt')" title="重启深度相机">
<div class="connecttext" :style="{ color: femtoboltStatus == '已连接' ? '#3bf2c6' : '#808080' }">
{{ femtoboltStatus }}
</div>
</div>
</div>
<div ref="wholeBodyRef" style="display: flex;justify-content: center;height:calc( 100% - 50px);padding-top: 0px;"
:style="{alignItems:(femtoboltStatus === '已连接' && depthCameraImgSrc)?'':'center'}">
<img v-if="(femtoboltStatus === '已连接' && depthCameraImgSrc)" :src="depthCameraImgSrc" alt="深度相机视频流"
style="width: 100%;height: calc(100%);">
<!-- object-fit:contain; -->
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
</div>
</el-col>
<el-col :span="12" style="flex: 0 0 52%;max-width: 52%;height: calc(100% - 0px);">
<!-- 头部姿态模块 -->
<div class="body-header-box">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title2.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">头部姿态</div>
<div class="calibration-zero" @click="calibrationClick" style="margin-left:20px">
<img src="@/assets/detection/calibration.png" style="margin-right:7px">
校准
</div>
<div class="calibration-zero" @click="clearAndStartTracking">
<img src="@/assets/detection/zero.png" style="margin-right:7px">
清零
</div>
</div>
<div class="body-son-display">
<img :src="imuStatus == '已连接'?refresh_active:refresh" alt="" style="margin-right: 8px;cursor: pointer;" title="重启IMU"
@click="refreshClick('imu')">
<div class="connecttext" :style="{ color: imuStatus == '已连接' ? '#3bf2c6' : '#808080' }">
{{ imuStatus }}
</div>
</div>
</div>
<!-- 头部姿态3D模型 -->
<div class="body-header-bottombox" ref="imuHeaderRef">
<div class="body-header-bottombox-left">
<div class="body-header-bottombox-lefttext" style="height: 25%;"></div>
<div class="body-header-bottombox-lefttext" >
<div class="currencytext1">左最大旋转角度</div>
<div class="currencytext2">{{
headPoseMaxValues.rotationLeftMax.toFixed(1) }}°</div>
</div>
<div class="body-header-bottombox-lefttext">
<div>
<div class="currencytext1">倾斜角</div>
<div class="currencytext2">{{ headlist.tilt }}°</div>
</div>
<div style="margin-top: 0px;">
<span>
<span class="currencytext4">左</span>
<span class="currencytext3">
{{ headPoseMaxValues.tiltLeftMax.toFixed(1) }}°
</span>
</span>
<span>|</span>
<span style="padding-left: 20px;">
<span class="currencytext4">右</span>
<span class="currencytext3">
{{ headPoseMaxValues.tiltRightMax.toFixed(1) }}°
</span>
</span>
</div>
</div>
</div>
<div class="body-header-bottombox-content">
<div style="text-align: center; width: 100%;" >
<span class="currencytext1">旋转角:</span>
<span class="currencytext2">{{ headlist.rotation }}°</span>
</div>
<div style="width: 100%;height: 80%;">
<Model :rotation="Number(headlist.rotation)" :tilt="Number(headlist.tilt)" :pitch="Number(headlist.pitch)*(-1)" :gender="patientInfo.gender || '男'" />
</div>
</div>
<div class="body-header-bottombox-right">
<div class="body-header-bottombox-righttext" style="height: 25%;"></div>
<div class="body-header-bottombox-righttext" >
<div class="currencytext1">右最大旋转角度</div>
<div class="currencytext2">
{{ headPoseMaxValues.rotationRightMax.toFixed(1) }}°
</div>
</div>
<div class="body-header-bottombox-righttext">
<div>
<div class="currencytext1">俯仰角</div>
<div class="currencytext2">{{ headlist.pitch }}°</div>
</div>
<div style="margin-top: 0px;">
<span>
<span class="currencytext4">俯</span>
<span class="currencytext3">
{{ headPoseMaxValues.pitchDownMax.toFixed(1) }}°
</span>
</span>
<span>|</span>
<span style="padding-left: 20px;">
<span class="currencytext4">仰</span>
<span style="text-align:right">
{{ headPoseMaxValues.pitchUpMax.toFixed(1) }}°
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="body-foot-box">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title3.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">足部压力</div>
</div>
<div class="body-son-display">
<img :src="pressureStatus == '已连接'?refresh_active:refresh" alt="" style="margin-right: 8px;cursor: pointer;" title="重启足底压力板"
@click="refreshClick('pressure')">
<div class="connecttext" :style="{ color: pressureStatus == '已连接' ? '#3bf2c6' : '#808080' }">
{{ pressureStatus }}
</div>
</div>
</div>
<div class="body-footbottom-box" ref="pressureRef">
<div class="body-footbottom-left">
<div style="width:100%;height: 50px;"></div>
<div class="body-footbottom-leftbottom">
<div class="body-footbottom-leftbox">
<span class="currencytext1">左前足</span>
<span class="currencytext2">
{{ footPressure.left_front }}%
</span>
</div>
<div class="body-footbottom-leftbox">
<span class="currencytext1">左后足</span>
<span class="currencytext2">
{{ footPressure.left_rear }}%
</span>
</div>
<div class="body-footbottom-leftbox">
<span class="currencytext1">左足总压力</span>
<span class="currencytext2">
{{ footPressure.left_total}}%
</span>
</div>
</div>
</div>
<div class="body-footbottom-center">
<div class="body-footbottom-topbox">
<div class="currencytext1" style="font-size:22px;text-align:center;">左足</div>
<div class="currencytext1" style="font-size:22px;text-align:center;">右足</div>
</div>
<div style="position: relative;width: 100%;height:calc(100% - 60px) ;"
:class="(pressureStatus === '已连接' && footImgSrc)?'':'noImageSvg-bg'">
<img v-if="(pressureStatus === '已连接' && footImgSrc)" :src="footImgSrc" style="width: 100%;height: 100%;" alt="">
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
<div class="xline"></div>
<div class="yline"></div>
</div>
</div>
<div class="body-footbottom-left">
<div style="width:100%;height: 50px;"></div>
<div class="body-footbottom-leftbottom">
<div class="body-footbottom-leftbox">
<span class="currencytext1">右前足</span>
<span class="currencytext2">
{{ footPressure.right_front }}%
</span>
</div>
<div class="body-footbottom-leftbox">
<span class="currencytext1">右后足</span>
<span class="currencytext2">
{{ footPressure.right_rear }}%
</span>
</div>
<div class="body-footbottom-leftbox">
<span class="currencytext1">右足总压力</span>
<span class="currencytext2">
{{ footPressure.right_total}}%
</span>
</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col v-if=" camera1Status === '已连接' && camera2Status === '已连接' "
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px); position: relative;">
<div class="body-userinfo-box" :class="isExpand == true?'body-userinfo-expandbox':''">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title4.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">患者信息</div>
</div>
<div class="body-son-display"></div>
</div>
<div class="body-userinfo-content">
<div class="body-userinfo-content-top">
<img src="@/assets/detection/useredit.png" alt="" title="编辑患者信息"
class="userinfo-edit-img" style="cursor: pointer;" @click="handleEditUserInfo">
<div class="useravatar-box">
<img src="@/assets/detection/useravatar.svg" alt="">
</div>
<div>
<div class="userinfo-text-top">
<div class="userinfo-text1" style="margin-right:20px ;">{{ patientInfo.name }}</div>
<div class="userinfo-text2"> {{ patientInfo.gender }}</div>
<div class="userinfo-line"></div>
<div class="userinfo-text2">{{ calculateAge(patientInfo.birth_date) }}岁</div>
</div>
<div class="userinfo-text3">
ID{{ patientInfo.id }}
</div>
</div>
</div>
<div class="body-userinfo-content-bottom" v-if="isExpand == false">
<img src="@/assets/detection/userinfo.png" alt=""
class="userinfo-edit-img" style="cursor: pointer;"
@click="viewClick(true)">
<div class="userinfo-disyplay1" >
<div class="userinfo-text4">电话</div>
<div class="userinfo-text5">
<span v-if="patientInfo && patientInfo.phone">
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
</span>
</div>
</div>
<div class="userinfo-disyplay2">
<div class="userinfo-text4">身高</div>
<div class="userinfo-text5">
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
</div>
</div>
<div class="userinfo-disyplay1" style="padding-top: 20px;">
<div class="userinfo-text4">体重</div>
<div class="userinfo-text5">
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
</div>
</div>
<div class="userinfo-disyplay2">
<div class="userinfo-text4">鞋码</div>
<div class="userinfo-text5">
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
</div>
</div>
<div class="body-userinfo-content-bottom2" v-if="isExpand == true">
<img src="@/assets/detection/userinfo.png" alt=""
class="userinfo-edit-img" style="cursor: pointer;"
@click="viewClick(false)">
<div class="userinfo-disyplaypadding1 ">
<div class="userinfo-text4 padding10">出生日期</div>
<div class="userinfo-text5">
<span v-if="patientInfo && patientInfo.birth_date">
{{ formatDate(patientInfo.birth_date) }}
</span>
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">身高</div>
<div class="userinfo-text5">
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">体重</div>
<div class="userinfo-text5">
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">鞋码</div>
<div class="userinfo-text5">
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">电话</div>
<div class="userinfo-text5">
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">民族</div>
<div class="userinfo-text5">
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">身份证号</div>
<div class="userinfo-text5">
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">职业</div>
<div class="userinfo-text5">
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
</div>
<div class="userinfo-disyplaypadding3">
<div class="userinfo-text4 padding10">居住地</div>
<div class="userinfo-text5">
{{ patientInfo.residence ==''||patientInfo.residence ==null ?'—':patientInfo.residence}}</div>
</div>
<div class="userinfo-disyplaypadding3">
<div class="userinfo-text4 padding10">邮箱</div>
<div class="userinfo-text5">
{{ patientInfo.email ==''||patientInfo.email ==null ?'—':patientInfo.email}}</div>
</div>
</div>
</div>
</div>
<div class="body-video-box" style="position: absolute;top: 347px; width: calc(100% - 15px);">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title5.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">视频</div>
</div>
<div class="body-son-display">
<img :src="cameraStatus == '已连接'?refresh_active:refresh" alt="" style="margin-right: 8px;cursor: pointer;"
title="重启视频"
@click="refreshClick('camera')">
<div class="connecttext" :style="{ color: cameraStatus == '已连接' ? '#3bf2c6' : '#808080' }">
{{ cameraStatus }}
</div>
</div>
</div>
<div class="body-video-content">
<div class="body-video-imgbox1" ref="camera1Ref" :class="(camera1Status === '已连接' && camera1ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera1Status === '已连接' && camera1ImgSrc)"
@click="isBig1 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera1Status === '已连接' && camera1ImgSrc)" :src="camera1ImgSrc" alt="camera1"
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
<div class="body-video-imgbox2" ref="camera2Ref" :class="(camera2Status === '已连接' && camera2ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera2Status === '已连接' && camera2ImgSrc)"
@click="isBig2 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera2Status === '已连接' && camera2ImgSrc)" :src="camera2ImgSrc" alt="camera2"
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col v-if="camera1Status === '已连接' || camera2Status === '已连接'"
:span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
<div class="body-userinfo-box1">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title4.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">患者信息</div>
</div>
<div class="body-son-display"></div>
</div>
<div class="body-userinfo-content">
<div class="body-userinfo-content-top">
<img src="@/assets/detection/useredit.png" alt="" title="编辑患者信息"
class="userinfo-edit-img" style="cursor: pointer;" @click="handleEditUserInfo">
<div class="useravatar-box">
<img src="@/assets/detection/useravatar.svg" alt="">
</div>
<div>
<div class="userinfo-text-top">
<div class="userinfo-text1" style="margin-right:20px ;">{{ patientInfo.name }}</div>
<div class="userinfo-text2"> {{ patientInfo.gender }}</div>
<div class="userinfo-line"></div>
<div class="userinfo-text2">{{ calculateAge(patientInfo.birth_date) }}岁</div>
</div>
<div class="userinfo-text3">
ID{{ patientInfo.id }}
</div>
</div>
</div>
<div class="body-userinfo-content-bottom1">
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">出生日期</div>
<div class="userinfo-text5">
<span v-if="patientInfo && patientInfo.birth_date">
{{ formatDate(patientInfo.birth_date) }}
</span>
</div>
</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">身高</div>
<div class="userinfo-text5">
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
</div>
</div>
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">体重</div>
<div class="userinfo-text5">
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
</div>
</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">鞋码</div>
<div class="userinfo-text5">
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
</div>
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">电话</div>
<div class="userinfo-text5">
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
</div>
</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">民族</div>
<div class="userinfo-text5">
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
</div>
<div class="userinfo-disyplaypadding4">
<div class="userinfo-text4">身份证号</div>
<div class="userinfo-text5">
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
</div>
</div>
<div class="userinfo-disyplaypadding5">
<div class="userinfo-text4">职业</div>
<div class="userinfo-text5">
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
</div>
<div class="userinfo-disyplaypadding6">
<div class="userinfo-text4">居住地</div>
<div class="userinfo-text5">
{{ patientInfo.residence ==''||patientInfo.residence ==null ?'—':patientInfo.residence}}</div>
</div>
<div class="userinfo-disyplaypadding6">
<div class="userinfo-text4">邮箱</div>
<div class="userinfo-text5">
{{ patientInfo.email ==''||patientInfo.email ==null ?'—':patientInfo.email}}</div>
</div>
</div>
</div>
</div>
<div class="body-video-box1">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title5.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">视频</div>
</div>
<div class="body-son-display">
<img :src="cameraStatus == '已连接'?refresh_active:refresh" alt="" style="margin-right: 8px;cursor: pointer;"
title="重启视频"
@click="refreshClick('camera')">
<div class="connecttext" :style="{ color: cameraStatus == '已连接' ? '#3bf2c6' : '#808080' }">
{{ cameraStatus }}
</div>
</div>
</div>
<div class="body-video-content">
<div v-show="camera1Status === '已连接'" class="body-video-imgbox3" ref="camera1Ref" :class="(camera1Status === '已连接' && camera1ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera1Status === '已连接' && camera1ImgSrc)"
@click="isBig1 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera1Status === '已连接' && camera1ImgSrc)" :src="camera1ImgSrc" alt="camera1"
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
<div v-show="camera2Status === '已连接'" class="body-video-imgbox3" ref="camera2Ref" :class="(camera2Status === '已连接' && camera2ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera2Status === '已连接' && camera2ImgSrc)"
@click="isBig2 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera2Status === '已连接' && camera2ImgSrc)" :src="camera2ImgSrc" alt="camera2"
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
</div>
</div>
</el-col>
<el-col v-if="camera1Status !== '已连接' && camera2Status !== '已连接'" :span="6" style="flex: 0 0 24%;height: calc(100% - 0px);">
<div class="body-userinfo-box3">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title4.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">患者信息</div>
</div>
<div class="body-son-display"></div>
</div>
<div class="body-userinfo-content">
<div class="body-userinfo-content-top">
<img src="@/assets/detection/useredit.png" alt="" title="编辑患者信息"
class="userinfo-edit-img" style="cursor: pointer;" @click="handleEditUserInfo">
<div class="useravatar-box">
<img src="@/assets/detection/useravatar.svg" alt="">
</div>
<div>
<div class="userinfo-text-top">
<div class="userinfo-text1" style="margin-right:20px ;">{{ patientInfo.name }}</div>
<div class="userinfo-text2"> {{ patientInfo.gender }}</div>
<div class="userinfo-line"></div>
<div class="userinfo-text2">{{ calculateAge(patientInfo.birth_date) }}岁</div>
</div>
<div class="userinfo-text3">
ID{{ patientInfo.id }}
</div>
</div>
</div>
<div class="body-userinfo-content-bottom0">
<div class="userinfo-disyplaypadding1 ">
<div class="userinfo-text4 padding10">出生日期</div>
<div class="userinfo-text5">
<span v-if="patientInfo && patientInfo.birth_date">
{{ formatDate(patientInfo.birth_date) }}
</span>
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">身高</div>
<div class="userinfo-text5">
{{ patientInfo.height ==''||patientInfo.height ==null ?'—':patientInfo.height}}cm
</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">体重</div>
<div class="userinfo-text5">
{{ patientInfo.weight ==''||patientInfo.weight ==null ?'—':patientInfo.weight}}kg
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">鞋码</div>
<div class="userinfo-text5">
{{ patientInfo.shoe_size ==''||patientInfo.shoe_size ==null ?'—':patientInfo.shoe_size}}码</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">电话</div>
<div class="userinfo-text5">
{{ patientInfo.phone ==''||patientInfo.phone ==null ?'—':patientInfo.phone}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">民族</div>
<div class="userinfo-text5">
{{ patientInfo.nationality ==''||patientInfo.nationality ==null ?'—':patientInfo.nationality}}</div>
</div>
<div class="userinfo-disyplaypadding1">
<div class="userinfo-text4 padding10">身份证号</div>
<div class="userinfo-text5">
{{ patientInfo.idcode ==''||patientInfo.idcode ==null ?'—':patientInfo.idcode}}
</div>
</div>
<div class="userinfo-disyplaypadding2">
<div class="userinfo-text4 padding10">职业</div>
<div class="userinfo-text5">
{{ patientInfo.occupation ==''||patientInfo.occupation ==null ?'—':patientInfo.occupation}}</div>
</div>
<div class="userinfo-disyplaypadding3">
<div class="userinfo-text4 padding10">居住地</div>
<div class="userinfo-text5">
{{ patientInfo.residence ==''||patientInfo.residence ==null ?'—':patientInfo.residence}}</div>
</div>
<div class="userinfo-disyplaypadding3">
<div class="userinfo-text4 padding10">邮箱</div>
<div class="userinfo-text5">
{{ patientInfo.email ==''||patientInfo.email ==null ?'—':patientInfo.email}}</div>
</div>
</div>
</div>
</div>
<div v-show="false" class="body-video-box">
<div class="body-title-display">
<div class="body-son-display">
<img src="@/assets/detection/title5.png" alt="" style="margin-right: 8px;">
<div class="body-posture-text">视频</div>
</div>
<div class="body-son-display">
<img :src="cameraStatus == '已连接'?refresh_active:refresh" alt="" style="margin-right: 8px;cursor: pointer;"
title="重启视频"
@click="refreshClick('camera')">
<div class="connecttext" :style="{ color: cameraStatus == '已连接' ? '#3bf2c6' : '#808080' }">
{{ cameraStatus }}
</div>
</div>
</div>
<div class="body-video-content">
<div class="body-video-imgbox1" ref="camera1Ref" :class="(camera1Status === '已连接' && camera1ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera1Status === '已连接' && camera1ImgSrc)"
@click="isBig1 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera1Status === '已连接' && camera1ImgSrc)" :src="camera1ImgSrc" alt="camera1"
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
<div class="body-video-imgbox2" ref="camera2Ref" :class="(camera2Status === '已连接' && camera2ImgSrc)?'':'noImageSvg-bg'">
<div v-if="(camera2Status === '已连接' && camera2ImgSrc)"
@click="isBig2 = true" class="big-img">
<img src="@/assets/detection/big.png">
</div>
<img v-if="(camera2Status === '已连接' && camera2ImgSrc)" :src="camera2ImgSrc" alt="camera2"
style="width: 100%; height: 100%;" />
<div v-else style="width:90px;height:60px">
<img :src="noImageSvg" style="margin-left: 15px;">
<div style="font-size:14px;color:#ffffff99;text-align: center;">连接已断开!</div>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 无操作退出提示 -->
<div class="pop-up-mask" v-if="isTip">
<div class="pop-up-tip-container">
<div class="pop-up-tip-header">
<div>提示</div>
<img src="@/assets/header/closepage.png" alt="" style="cursor: pointer;" @click="handleCancel">
</div>
<div class="pop-up-tip-text" v-if="!isVideoOperation">本次检测未截图或录像操作,不予存档记录!</div>
<div class="pop-up-tip-text" v-if="isVideoOperation">本次检测未截图操作,存档记录不可生成报告!</div>
<div class="tipconfirmbutton-box">
<el-button type="primary" class="tipconfirmbutton" @click="closeTipClick">确定</el-button>
</div>
</div>
</div>
<!-- 诊断信息 -->
<div class="pop-up-mask" v-if="isDiagnosticMessage">
<DiagnosticMessage @closeDiagnosticMessage="closeDiagnosticMessage"
:selectedPatient="patientInfo" v-if="isDiagnosticMessage"/>
</div>
<div class="pop-up-mask" v-if="isCloseCreat">
<PatientCreate @closecreatbox="closecreatbox" :patienttype="'edit'"
:selectedPatient="selectedPatient" v-if="isCloseCreat"/>
</div>
<div class="pop-up-mask" v-if="cameraDialogVisible">
<div class="pop-up-camera-container">
<div class="pop-up-camera-header">
<div>相机参数设置</div>
<img src="@/assets/close.png" alt="" style="cursor: pointer;" @click="handleCameraCancel">
</div>
<div class="pop-up-camera-body">
<div class="pop-up-camera-display">
<div class="pop-up-camera-line"></div>
<div class="pop-up-camera-title">深度相机</div>
</div>
<div class="pop-up-camera-display" style="padding-top: 10px;">
<div class="pop-up-camera-name">距离范围</div>
<el-input v-model="cameraForm.femtobolt.depth_range_min" placeholder="请输入最小值"
style="width: 200px;" />
<div style="margin: 0 10px;">至</div>
<el-input v-model="cameraForm.femtobolt.depth_range_max" placeholder="请输入最大值"
style="width: 200px;"/>
<el-checkbox v-model="cameraForm.femtobolt.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
</div>
<div class="pop-up-camera-display" style="padding-top: 30px;padding-bottom: 10px;">
<div class="pop-up-camera-line"></div>
<div class="pop-up-camera-title">足部相机</div>
</div>
<div class="pop-up-camera-display" style="padding-top: 0px;">
<div class="pop-up-camera-name">相机上</div>
<el-radio-group v-model="cameraForm.camera1.device_index">
<div style="display: flex;justify-content: space-between;width: 435px;">
<el-radio :value="0" border>1号</el-radio>
<el-radio :value="1" border>2号</el-radio>
<el-radio :value="2" border>3号</el-radio>
<el-radio :value="3" border>4号</el-radio>
<el-radio :value="4" border>5号</el-radio>
</div>
</el-radio-group>
<el-checkbox v-model="cameraForm.camera1.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
</div>
<div class="pop-up-camera-display" style="padding-top: 20px;">
<div class="pop-up-camera-name">相机下</div>
<el-radio-group v-model="cameraForm.camera2.device_index">
<div style="display: flex;justify-content: space-between;width: 435px;">
<el-radio :value="0" border>1号</el-radio>
<el-radio :value="1" border>2号</el-radio>
<el-radio :value="2" border>3号</el-radio>
<el-radio :value="3" border>4号</el-radio>
<el-radio :value="4" border>5号</el-radio>
</div>
</el-radio-group>
<el-checkbox v-model="cameraForm.camera2.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
</div>
<div class="pop-up-camera-display" style="padding-top: 30px;padding-bottom: 00px;">
<div class="pop-up-camera-line"></div>
<div class="pop-up-camera-title">遥控器</div>
</div>
<div class="pop-up-camera-display" style="padding-top: 10px;">
<div class="pop-up-camera-name">串口号</div>
<el-select v-model="cameraForm.remote.port" placeholder="请选择" style="width: 434px;">
<el-option v-for="item in remotePortData" :label="item" :value="item" />
</el-select>
<el-checkbox v-model="cameraForm.remote.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
</div>
<div class="pop-up-camera-display" style="padding-top: 30px;padding-bottom: 00px;">
<div class="pop-up-camera-line"></div>
<div class="pop-up-camera-title">IMU设备</div>
</div>
<div class="pop-up-camera-display" style="padding-top: 10px;">
<div class="pop-up-camera-name">Mac地址</div>
<el-input v-model="cameraForm.imu.mac_address" placeholder="请输入"
style="width: 434px;" />
<el-checkbox v-model="cameraForm.imu.enable" label="有效" size="large" style="width: 60px;margin-left:10px ;" />
</div>
<div class="form-actions-display">
<el-button @click="handleCameraCancel" class="formreturnCancel">退出</el-button>
<el-button type="primary" class="formsaveCancel"
@click="cameraSubmit('cameraForm')">
保存
</el-button>
</div>
</div>
</div>
</div>
<div class="pop-up-mask" v-if="isPhotoAlbum">
<PhotoAlbum @closePhotoAlbum="closePhotoAlbum"
:selectedPatient="patientInfo" v-if="isPhotoAlbum"/>
</div>
<PatientProfile v-if="historyDialogVisible"
:archiveType="true"
:selectedPatient="selectedPatient"
@closePatientProfile="closePatientProfile"/>
<div v-if="isBig1" style="position: fixed;top: 122px;right: 0;
width: 100%;height:calc(100% - 122px);z-index: 9999;
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;">
<svg @click="isBig1=false" style="position: absolute;right: 10px;top:10px;cursor: pointer;" t="1760175800150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5743" width="24" height="24"><path d="M796 163.1L511.1 448l-285-285-63.9 64 285 285-285 285 63.8 63.8 285-285 285 285 63.8-63.8-285-285 285-285-63.8-63.9z" fill="#ffffff" p-id="5744"></path></svg>
<img v-if="isBig1" :src="camera1ImgSrc" alt=""
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" />
</div>
<div v-if="isBig2" style="position: fixed;top: 122px;right: 0;
width: 100%;height:calc(100% - 122px);z-index: 9999;
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;">
<svg @click="isBig2=false" style="position: absolute;right: 10px;top:10px;cursor: pointer;" t="1760175800150" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5743" width="24" height="24"><path d="M796 163.1L511.1 448l-285-285-63.9 64 285 285-285 285 63.8 63.8 285-285 285 285 63.8-63.8-285-285 285-285-63.8-63.9z" fill="#ffffff" p-id="5744"></path></svg>
<img v-if="isBig2" :src="camera2ImgSrc" alt=""
style="width: 100%;height: calc(100%);object-fit:contain;background:#323232;" />
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'
import { io } from 'socket.io-client'
import Header from '@/views/Header.vue'
import { useAuthStore } from '../stores/index.js'
import * as echarts from 'echarts'
import { getBackendUrl, patientAPI } from '../services/api.js'
import noImageSvg from '@/assets/detection/no-image.png'
import DiagnosticMessage from '@/views/DiagnosticMessage.vue'
import PatientCreate from '@/views/PatientCreate.vue'
import PatientProfile from '@/views/PatientProfile.vue'
import Model from './model.vue'
import ViewUserInfo from '@/views/ViewUser.vue'
import PhotoAlbum from '@/views/PhotoAlbum.vue'
import refresh from '@/assets/detection/refresh.png'
import refresh_active from '@/assets/detection/refresh_active.png'
const emit = defineEmits([ 'endChange']);
const props = defineProps({
selectedPatient: {
required: false,
type: Object,
default: null
},
})
const remotePortData =ref([]) // 遥控器串口数据
const videoNum = ref(2) // 视频数量
const videoTop = ref(1) // 视频位置
const isExpand = ref(false) // 是否展开
const isViewUser = ref(false) // 查看信息
const isCloseCreat =ref(false) // 是否打开患者信息编辑框
const isoperation = ref(false) // 是否保存数据
const isVideoOperation = ref(false) // 是否录制视频
const isTip =ref(false)
const isStartVideo = ref(false)
function startVideoClick() {
startRecord()
isVideoOperation.value = true
isStartVideo.value = true
}
function stopVideoClick() {
stopRecord()
isStartVideo.value = false
}
function endClick() {
if(isoperation.value == true){
isDiagnosticMessage.value = true
}else{
isTip.value = true
}
}
function handleCancel(){
isTip.value = false
}
function handleCameraCancel(){
cameraDialogVisible.value = false
}
async function closeTipClick(){
try {
if (timerId.value) {
clearInterval(timerId.value)
timerId.value = null
}
if (isRecording.value === true) {
stopRecord()
}
await stopDetection({})
disconnectWebSocket()
window.removeEventListener('beforeunload', handleBeforeUnload)
emit('endChange',false)
routeTo('/')
} catch (error) {
emit('endChange',false)
}
}
const isDiagnosticMessage = ref(false)
const isBig1 =ref(false)
const isBig2 =ref(false)
const authStore = useAuthStore()
const router = useRouter()
const isRecording = ref(false)
const isConnected = ref(false)
const rtspImgSrc = ref('')
const camera1ImgSrc = ref('')
const camera2ImgSrc = ref('')
const depthCameraImgSrc = ref('') // 深度相机视频流
const screenshotLoading = ref(false)
const cameraDialogVisible =ref(false) // 设置相机参数弹框
const contenGridRef =ref(null) // 实时检查整体box
const wholeBodyRef = ref(null) // 身体姿态ref
const camera1Ref = ref(null) // 视频1ref
const camera2Ref = ref(null) // 视频2ref
const pressureRef = ref(null) // 足底压力ref
const imuHeaderRef = ref(null) // IMUref
const historyDialogVisible = ref(false)
// 患者信息从页面获取或通过API获取
const patientInfo = ref({
id: '',
name: '',
gender: '',
birth_date: '',
age: '',
height: '',
weight: '',
shoe_size: '',
phone: '',
nationality: '',
created_at: '',
sessionId: null // 检查记录ID实际使用时应从路由或API获取
})
// WebSocket相关变量
let socket = null
let devicesSocket = null
let cameraSocket = null
let femtoboltSocket = null
let imuSocket = null
let pressureSocket = null
let restartSocket = null
let frameCount = 0
let latestFrameCamera1 = ''
let latestFrameCamera2 = ''
let rafScheduled = false
let lastRenderTs = 0
const MAX_RENDER_FPS = 30
// 后端服务器地址配置
const BACKEND_URL = getBackendUrl()
const resDialogVisible = ref(false)
const dialogVisible = ref(false)
// 表单引用
const patientFormRef = ref()
// 表单数据
const saveLoading = ref(false)
const patientForm = ref({
id: '',
name: '',
gender: '',
birth_date: '',
nationality: '',
residence: '',
height: '',
weight: '',
shoe_size: '',
phone: '',
occupation: '',
workplace: '',
email: ''
})
const occupationOptions = ref(["学生", "教师", "医生", "护士", "工程师", "程序员", "设计师",
"会计师", "律师", "警察", "消防员", "军人", "公务员", "销售", "市场营销",
"人力资源", "行政", "财务", "咨询师", "建筑师", "科研人员", "记者", "编辑",
"作家", "艺术家", "音乐家", "演员", "导演", "摄影师", "厨师", "服务员",
"司机", "快递员", "外卖员", "农民", "工人", "电工", "焊工", "机械师",
"飞行员", "空乘", "船员", "导游", "翻译", "心理咨询师", "社会工作者",
"运动员", "教练", "经纪人", "投资人", "企业家", "自由职业者"])
const nationalityOptions = ref(["汉族", "满族", "蒙古族", "回族", "藏族", "维吾尔族", "苗族", "彝族", "壮族",
"布依族", "朝鲜族", "侗族", "瑶族", "白族", "土家族", "哈尼族", "哈萨克族", "傣族",
"黎族", "傈僳族", "佤族", "畲族", "高山族", "拉祜族", "水族", "东乡族", "纳西族",
"景颇族", "柯尔克孜族", "土族", "达斡尔族", "仫佬族", "羌族", "布朗族", "撒拉族",
"毛南族", "仡佬族", "锡伯族", "阿昌族", "普米族", "塔吉克族", "怒族", "乌孜别克族",
"俄罗斯族", "鄂温克族", "德昂族", "保安族", "裕固族", "京族", "塔塔尔族", "独龙族",
"鄂伦春族", "赫哲族", "门巴族", "珞巴族", "基诺族"])
const diagnosticForm = ref({
diagnosis_info: '',
treatment_info: '',
suggestion_info: ''
})
const cameraForm = ref({ // 相机参数
camera1:{
device_index: '', // 序号
},
camera2:{
device_index: '', // 序号
},
femtobolt:{
algorithm_type: '', // 算法类型
depth_mode: '', // 相机模式
depth_range_min: '', // 距离范围最小值
depth_range_max: '', // 距离范围最大值
},
imu:{
mac_address: '', // IMU串口号
enable: false
},
remote:{
port: '', // 遥控器串口号
enable: false
},
})
const calculatedAge = ref(null)
//修改
// 模拟历史数据
const historyData = ref([])
const chartoption = ref({
backgroundColor: '#242424',
grid: { top: 0, right: 0, bottom: 0, left: 0 },
animation: false,
series: [
{
type: 'gauge',
radius: '180%',
center: ['50%', '91%'],
startAngle: 180,
endAngle: 0,
min: -90,
max: 90,
splitNumber: 10,
itemStyle: {
color: '#58D9F9',
shadowColor: 'rgba(0,138,255,0.45)',
shadowBlur: 10,
shadowOffsetX: 2,
shadowOffsetY: 2
},
progress: {
show: false,
},
pointer: {
length: '85%',
width: 2,
offsetCenter: [0, '5%'],
itemStyle: {
color: '#ff8d00'
},
},
anchor: {
show: true,
showAbove: true,
size: 18,
icon: 'circle',
itemStyle: {
borderWidth: 3,
color: '#ff8d00',
borderColor: '#ff8d00',
}
},
axisLine: {
roundCap: true,
lineStyle: {
width: 3,
color: [[1, '#0089ff']]
}
},
axisTick: {
splitNumber: 2,
lineStyle: {
width: 2,
color: '#ffffff'
}
},
splitLine: {
length: 12,
lineStyle: {
width: 4,
color: '#0089ff'
}
},
axisLabel: {
//刻度颜色
distance: 10,
color: '#ffffff',
fontSize: 10
},
title: {
show: false
},
detail: {
show: false,
},
data: [
{
value: 0
}
]
}
]
})
// 四个设备的独立连接状态
const camera1Status = ref('未连接')
const camera2Status = ref('未连接')
const cameraStatus = computed(() => (camera1Status.value === '已连接' || camera2Status.value === '已连接') ? '已连接' : '未连接')
const femtoboltStatus = ref('未连接') // 深度相机(FemtoBolt)设备状态
const imuStatus = ref('未连接') // IMU设备状态
const pressureStatus = ref('未连接') // 压力传感器设备状态
const remoteStatus = ref('未连接') // 遥控器设备状态
// 为了向后兼容保留videoStatus但映射到cameraStatus
const videoStatus = computed(() => cameraStatus.value)
// 计时器状态
const seconds = ref(0);
const isRunning = ref(false);
const timerId = ref(null);
const blinkState = ref(false);
const formattedTime = computed(() => {
const mins = Math.floor(seconds.value / 60);
const secs = seconds.value % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
});
const headlist = ref({
rotation: 0,
tilt: 0,
pitch: 0
})
// 开始计时器
const startTimer = () => {
if (isRunning.value) return;
isRunning.value = true;
seconds.value = 0;
// 使用高性能计时器
const startTime = Date.now();
timerId.value = setInterval(() => {
// 计算经过的时间(毫秒)
const elapsed = Date.now() - startTime;
// 转换为秒并四舍五入
seconds.value = Math.round(elapsed / 1000);
// 检测时长超过10分钟1800秒自动停止检测
if (seconds.value >= 1800) {
console.log('⏰ 检测时长超过30分钟自动停止检测');
ElMessage.warning('检测时长已达到30分钟自动停止检测');
stopRecord()
return;
}
// 触发闪烁效果
blinkState.value = !blinkState.value;
}, 1000);
};
// 重置计时器
const resetTimer = () => {
if (timerId.value) {
clearInterval(timerId.value);
timerId.value = null;
}
isRunning.value = false;
seconds.value = 0;
blinkState.value = false;
};
const validateForm = async () => {
try {
await patientFormRef.value.validate()
return true
} catch (error) {
ElMessage.error('请完善必填信息')
return false
}
}
//保存基础信息
const handleSave = async () => {
if (!(await validateForm())) return
saveLoading.value = true
try {
await savePatient()
ElMessage.success('修改成功')
dialogVisible.value = false
saveLoading.value = false
loadPatientInfo()
} catch (error) {
ElMessage.error('修改失败:' + error.message)
saveLoading.value = false
} finally {
saveLoading.value = false
}
}
const savePatient = async () => {
const patientData = {
id: patientForm.value.id,
name: patientForm.value.name,
gender: patientForm.value.gender,
age: calculatedAge.value,
birth_date: patientForm.value.birth_date,
height: patientForm.value.height,
weight: patientForm.value.weight,
shoe_size: patientForm.value.shoe_size,
phone: patientForm.value.phone,
occupation: patientForm.value.occupation,
email: patientForm.value.email,
nationality: patientForm.value.nationality,
residence: patientForm.value.residence,
workplace: patientForm.value.workplace
}
try {
const response = await patientAPI.updatePatient(patientForm.value.id, patientData)
if (response.success) {
return response.data
} else {
throw new Error(response.message || '修改失败')
}
} catch (error) {
throw error
}
}
const isPreventCombo = ref(false)
function routeTo(path = '/') {
if (isPreventCombo.value === true) {
ElMessage.warning(`请勿连续点击回退按钮!`)
return
}
isPreventCombo.value = true
setTimeout(() => {
isPreventCombo.value = false
}, 2000)
router.push(path)
}
function cameraUpdate() { // 相机设置数据更新弹框
cameraForm.value = { // 相机参数
camera1:{
device_index: '', // 序号1
},
camera2:{
device_index: '', // 序号2
},
femtobolt:{
algorithm_type: '', // 算法类型
depth_mode: '', // 相机模式
depth_range_min: '', // 距离范围最小值
depth_range_max: '', // 距离范围最大值
},
imu:{
mac_address: '', // IMU串口号
enable: false
},
remote:{
port: '', // IMU串口号
}
}
// 加载相机参数信息
getDevicesInit()
}
const calculateAge = (birthDate) => {
if (!birthDate) return '—'
const today = new Date()
const birth = new Date(birthDate)
let age = today.getFullYear() - birth.getFullYear()
const monthDiff = today.getMonth() - birth.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--
}
return age
}
const tempInfo = ref({})
// WebSocket连接函数
function connectWebSocket() {
try {
console.log('正在连接到', BACKEND_URL)
// 如果已有连接,先断开
if (socket) {
socket.disconnect()
socket = null
}
if (cameraSocket) {
cameraSocket.disconnect()
cameraSocket = null
}
if (femtoboltSocket) {
femtoboltSocket.disconnect()
femtoboltSocket = null
}
if (imuSocket) {
imuSocket.disconnect()
imuSocket = null
}
if (pressureSocket) {
pressureSocket.disconnect()
pressureSocket = null
}
if (restartSocket) {
restartSocket.disconnect()
restartSocket = null
}
// 创建主Socket.IO连接
socket = io(BACKEND_URL, {
transports: ['websocket'],
timeout: 10000,
forceNew: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
})
// 创建统一设备命名空间连接
devicesSocket = io(BACKEND_URL + '/devices', {
transports: ['websocket'],
timeout: 10000,
forceNew: true
})
// 为了保持兼容性将统一的设备socket赋值给各个设备变量
cameraSocket = devicesSocket
femtoboltSocket = devicesSocket
imuSocket = devicesSocket
pressureSocket = devicesSocket
restartSocket = devicesSocket
// 主连接事件
socket.on('connect', () => {
console.log('✅ 主WebSocket连接成功Socket ID:', socket.id)
isConnected.value = true
//绘制头部仪表盘
initchart()
})
socket.on('connect_error', (error) => {
console.error('❌ 主连接失败:', error.message)
isConnected.value = false
})
socket.on('disconnect', (reason) => {
console.log('⚠️ 主连接断开:', reason)
isConnected.value = false
})
socket.on('reconnect', (attemptNumber) => {
console.log('🔄 主WebSocket重连成功尝试次数:', attemptNumber)
isConnected.value = true
})
socket.on('reconnect_attempt', (attemptNumber) => {
console.log('🔄 正在尝试重连主连接...', attemptNumber)
})
socket.on('reconnect_failed', () => {
console.error('❌ 主WebSocket重连失败')
isConnected.value = false
})
socket.on('error', (error) => {
console.error('❌ 主Socket错误:', error)
})
// 统一设备命名空间事件监听
devicesSocket.on('connect', () => {
console.log('🔗 设备命名空间连接成功')
const enabledCameras = (() => {
const list = []
const idx1 = String((cameraForm.value && cameraForm.value.camera1 && cameraForm.value.camera1.device_index) || '').trim()
const idx2 = String((cameraForm.value && cameraForm.value.camera2 && cameraForm.value.camera2.device_index) || '').trim()
if (idx1 !== '') list.push('camera1')
if (idx2 !== '') list.push('camera2')
if (list.length === 0) list.push('camera1')
return list
})()
enabledCameras.forEach(t => devicesSocket.emit('subscribe_device', { device_type: t }))
devicesSocket.emit('subscribe_device', { device_type: 'femtobolt' })
devicesSocket.emit('subscribe_device', { device_type: 'imu' })
devicesSocket.emit('subscribe_device', { device_type: 'pressure' })
devicesSocket.emit('subscribe_device', { device_type: 'remote' })
// 设备连接成功后启动数据推送
startDeviceDataPush()
})
devicesSocket.on('disconnect', () => {
console.log('🔗 设备命名空间断开连接')
// 断开连接时重置所有设备状态
camera1Status.value = '未连接'
camera2Status.value = '未连接'
femtoboltStatus.value = '未连接'
imuStatus.value = '未连接'
pressureStatus.value = '未连接'
})
devicesSocket.on('connect_error', (error) => {
console.error('❌ 设备命名空间连接失败:', error.message)
})
// 监听设备重启消息事件
devicesSocket.on('device_restart_message', (data) => {
ElMessage.success({
message: data.message,
duration: 1000
})
})
// 监听各设备数据事件
devicesSocket.on('camera_frame', (data) => {
frameCount++
// 区分 camera1 / camera2 帧
const devId = (data && data.device_id) ? String(data.device_id).toLowerCase() : ''
if (!tempInfo.value.camera_frames) {
tempInfo.value.camera_frames = {}
}
if (devId === 'camera1') {
tempInfo.value.camera_frames['camera1'] = data
tempInfo.value.camera1_frame = data
displayCameraFrameById('camera1', data.image)
} else if (devId === 'camera2') {
tempInfo.value.camera_frames['camera2'] = data
tempInfo.value.camera2_frame = data
displayCameraFrameById('camera2', data.image)
}
})
devicesSocket.on('femtobolt_frame', (data) => {
tempInfo.value.femtobolt_frame = data
displayDepthCameraFrame(data.depth_image || data.image)
})
devicesSocket.on('imu_data', (data) => {
tempInfo.value.imu_data = data
handleIMUData(data)
})
devicesSocket.on('pressure_data', (data) => {
tempInfo.value.pressure_data = data
handlePressureData(data)
})
// 监听遥控器事件:根据编码触发页面方法
devicesSocket.on('remote_control', (data) => {
const code = String((data && data.code) || '').toUpperCase()
switch (code) {
case '11':
startVideoClick()
break
case '14':
stopVideoClick()
break
case '12':
clearAndStartTracking()
break
case '0F':
saveDetectionData()
break
default:
break
}
})
// 监听测试状态事件
devicesSocket.on('test_status', (data) => {
console.log('📊 测试状态:', data)
if (data.status === 'started') {
console.log('✅ 设备数据推送已开始')
} else if (data.status === 'stopped') {
console.log('⏹️ 设备数据推送已停止')
} else if (data.status === 'error') {
console.error('❌ 设备数据推送错误:', data.message)
}
})
// 监听各设备独立连接状态事件
devicesSocket.on('device_status', (data) => {
console.log('📱 设备状态更新:', data)
const { device_type, status } = data
const statusText = status ? '已连接' : '未连接'
switch (device_type) {
case 'camera1':
camera1Status.value = statusText
console.log(`📷 相机1状态: ${statusText}`)
break
case 'camera2':
camera2Status.value = statusText
console.log(`📷 相机2状态: ${statusText}`)
break
case 'femtobolt':
femtoboltStatus.value = statusText
console.log(`🔍 深度相机状态: ${statusText}`)
break
case 'imu':
imuStatus.value = statusText
console.log(`🧭 IMU状态: ${statusText}`)
break
case 'pressure':
pressureStatus.value = statusText
console.log(`⚖️ 压力传感器状态: ${statusText}`)
break
case 'remote':
remoteStatus.value = statusText
console.log(`📡 遥控器状态: ${statusText}`)
break
default:
console.warn('⚠️ 未知设备类型:', device_type)
}
})
} catch (error) {
console.error('💥 连接异常:', error.message)
isConnected.value = false
}
}
// 启动设备数据推送
function startDeviceDataPush() {
if (devicesSocket && devicesSocket.connected) {
console.log('🚀 发送启动设备数据推送请求...')
devicesSocket.emit('start_push_data')
} else {
console.warn('⚠️ 设备Socket未连接无法启动设备数据推送')
}
}
// 断开WebSocket连接函数
function disconnectWebSocket() {
try {
if (socket) {
if (socket.connected) {
console.log('正在主动断开WebSocket连接...')
// 移除所有事件监听器
socket.removeAllListeners()
// 断开主连接
socket.disconnect()
console.log('✅ 主WebSocket连接已断开')
}
socket = null
isConnected.value = false
}
// 断开统一设备命名空间连接
if (devicesSocket) {
if (devicesSocket.connected) {
// 取消订阅所有设备
// try {
// devicesSocket.emit('unsubscribe_device', { device_type: 'camera1' })
// devicesSocket.emit('unsubscribe_device', { device_type: 'camera2' })
// devicesSocket.emit('unsubscribe_device', { device_type: 'femtobolt' })
// devicesSocket.emit('unsubscribe_device', { device_type: 'imu' })
// devicesSocket.emit('unsubscribe_device', { device_type: 'pressure' })
// } catch (e) {
// console.warn('取消设备订阅时出错:', e)
// }
// 移除所有事件监听器
devicesSocket.removeAllListeners()
// 断开连接
devicesSocket.disconnect()
console.log('🔗 统一设备命名空间连接已断开')
}
devicesSocket = null
cameraSocket = null
femtoboltSocket = null
imuSocket = null
pressureSocket = null
restartSocket = null
}
// 重置所有设备状态
camera1Status.value = '未连接'
camera2Status.value = '未连接'
femtoboltStatus.value = '未连接'
imuStatus.value = '未连接'
pressureStatus.value = '未连接'
} catch (error) {
console.warn('断开WebSocket连接时出错:', error)
}
}
// WebSocket重连函数
function reconnectWebSocket() {
console.log('开始重新连接WebSocket...')
// 先断开现有连接
disconnectWebSocket()
// 延迟一段时间后重新连接
setTimeout(() => {
connectWebSocket()
console.log('WebSocket重连完成')
}, 1000)
}
// 简单的帧显示函数
function displayFrame(base64Image) {
// 兼容旧调用:默认作为 camera1 更新
displayCameraFrameById('camera1', base64Image)
}
function displayCameraFrameById(deviceId, base64Image) {
if (base64Image && base64Image.length > 0) {
if (String(deviceId).toLowerCase() === 'camera2') {
latestFrameCamera2 = base64Image
} else {
latestFrameCamera1 = base64Image
}
if (!rafScheduled) {
rafScheduled = true
requestAnimationFrame(() => {
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now()
const minInterval = 1000 / MAX_RENDER_FPS
if (now - lastRenderTs >= minInterval) {
if (latestFrameCamera1) {
const url1 = 'data:image/jpeg;base64,' + latestFrameCamera1
camera1ImgSrc.value = url1
rtspImgSrc.value = url1
}
if (latestFrameCamera2) {
const url2 = 'data:image/jpeg;base64,' + latestFrameCamera2
camera2ImgSrc.value = url2
}
lastRenderTs = now
}
rafScheduled = false
})
}
} else {
console.warn('⚠️ 收到空的视频帧数据')
}
}
// 深度相机帧显示函数
function displayDepthCameraFrame(base64Image) {
if (base64Image && base64Image.length > 0) {
depthCameraImgSrc.value = 'data:image/jpeg;base64,' + base64Image
} else {
console.warn('⚠️ 收到空的深度相机帧数据')
}
}
// 头部姿态最值跟踪数据
const headPoseMaxValues = ref({
rotationLeftMax: 0, // 旋转-左旋最大值
rotationRightMax: 0, // 旋转-右旋最大值
tiltLeftMax: 0, // 倾斜-左倾最大值
tiltRightMax: 0, // 倾斜-右倾最大值
pitchUpMax: 0, // 俯仰-上仰最大值
pitchDownMax: 0 // 俯仰-下俯最大值
})
// 头部姿态历史最值记录数组
const headPoseHistory = ref([])
const headPoseData = ref({})
// IMU更新节流与抖动阈值降低频繁DOM与图表更新导致的卡顿
let lastIMUUpdateTs = 0
const IMU_MIN_INTERVAL_MS = 33 // 约30Hz
let lastIMUValues = { rotation: null, tilt: null, pitch: null }
const IMU_CHANGE_EPS = 0.1 // 小于0.1°的变化忽略
// 处理IMU头部姿态数据
function handleIMUData(data) {
try {
if (!data) return
// 兼容两种载荷结构:
// 1) { rotation, tilt, pitch }
// 2) { head_pose: { rotation, tilt, pitch } }
const rotation = (data.rotation ?? (data.head_pose && data.head_pose.rotation))
const tilt = (data.tilt ?? (data.head_pose && data.head_pose.tilt))
const pitch = (data.pitch ?? (data.head_pose && data.head_pose.pitch))
if (rotation === undefined || tilt === undefined || pitch === undefined) {
return
}
headlist.value.rotation = rotation // 旋转角度
headlist.value.tilt = tilt // 倾斜角度
headlist.value.pitch = pitch // 俯仰角度
const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now()
// 若距离上次更新时间很短且变化不明显则跳过避免频繁DOM更新
const tooSoon = (now - lastIMUUpdateTs) < IMU_MIN_INTERVAL_MS
const notSignificant = (
lastIMUValues.rotation !== null && Math.abs(rotation - lastIMUValues.rotation) < IMU_CHANGE_EPS &&
lastIMUValues.tilt !== null && Math.abs(tilt - lastIMUValues.tilt) < IMU_CHANGE_EPS &&
lastIMUValues.pitch !== null && Math.abs(pitch - lastIMUValues.pitch) < IMU_CHANGE_EPS
)
if (tooSoon && notSignificant) return
lastIMUUpdateTs = now
lastIMUValues = { rotation, tilt, pitch }
const rVal = Math.round(rotation * 10) / 10
const pVal = Math.round(pitch * 10) / 10
const tVal = Math.round(tilt * 10) / 10
if (rotationCharts && !rotationCharts.isDisposed()) {
try {
rotationCharts.setOption({
series: [{ data: [{ value: rVal }] }]
})
} catch (e) {
console.warn('rotationCharts setOption error:', e);
}
}
if (pitchCharts && !pitchCharts.isDisposed()) {
try {
pitchCharts.setOption({
series: [{ data: [{ value: pVal }] }]
})
} catch (e) {
console.warn('pitchCharts setOption error:', e);
}
}
if (tiltCharts && !tiltCharts.isDisposed()) {
try {
tiltCharts.setOption({
series: [{ data: [{ value: tVal }] }]
})
} catch (e) {
console.warn('tiltCharts setOption error:', e);
}
}
// 更新最值跟踪逻辑使用原始数值(不做四舍五入)
updateHeadPoseMaxValues({ rotation, tilt, pitch })
} catch (error) {
console.error('❌ 处理IMU数据失败:', error)
}
}
// 更新头部姿态最值
function updateHeadPoseMaxValues(headPose) {
try {
// 更新旋转角最值
if (headPose.rotation > 0) {
// 左旋(负值),取绝对值的最大值
headPoseMaxValues.value.rotationLeftMax = Math.max(
headPoseMaxValues.value.rotationLeftMax,
headPose.rotation
)
} else if (headPose.rotation < 0) {
// 右旋(正值)
headPoseMaxValues.value.rotationRightMax = Math.max(
headPoseMaxValues.value.rotationRightMax,
Math.abs(headPose.rotation)
)
}
// 更新倾斜角最值
if (headPose.tilt > 0) {
// 左倾(负值),取绝对值的最大值
headPoseMaxValues.value.tiltLeftMax = Math.max(
headPoseMaxValues.value.tiltLeftMax,
headPose.tilt
)
} else if (headPose.tilt < 0) {
// 右倾(正值)
headPoseMaxValues.value.tiltRightMax = Math.max(
headPoseMaxValues.value.tiltRightMax,
Math.abs(headPose.tilt)
)
}
// 更新俯仰角最值
if (headPose.pitch >0) {
// 下俯(负值),取绝对值的最大值
headPoseMaxValues.value.pitchDownMax = Math.max(
headPoseMaxValues.value.pitchDownMax,
headPose.pitch
)
} else if (headPose.pitch < 0) {
// 上仰(正值)
headPoseMaxValues.value.pitchUpMax = Math.max(
headPoseMaxValues.value.pitchUpMax,
Math.abs(headPose.pitch)
)
}
} catch (error) {
console.error('❌ 更新头部姿态最值失败:', error)
}
}
// 清零最值并开始跟踪
function clearAndStartTracking() {
try {
// 重置所有最值为0
headPoseMaxValues.value = {
rotationLeftMax: 0,
rotationRightMax: 0,
tiltLeftMax: 0,
tiltRightMax: 0,
pitchUpMax: 0,
pitchDownMax: 0
}
} catch (error) {
ElMessage.error('清零失败')
}
}
// 保存当前最值到历史记录
function saveMaxValuesToHistory() {
try {
// 创建当前最值的副本
const currentMaxValues = {
id: headPoseHistory.value.length + 1,
rotationLeftMax: Number(headPoseMaxValues.value.rotationLeftMax.toFixed(1)),
rotationRightMax: Number(headPoseMaxValues.value.rotationRightMax.toFixed(1)),
tiltLeftMax: Number(headPoseMaxValues.value.tiltLeftMax.toFixed(1)),
tiltRightMax: Number(headPoseMaxValues.value.tiltRightMax.toFixed(1)),
pitchUpMax: Number(headPoseMaxValues.value.pitchUpMax.toFixed(1)),
pitchDownMax: Number(headPoseMaxValues.value.pitchDownMax.toFixed(1)),
timestamp: new Date().toLocaleString()
}
// 添加到历史记录
headPoseHistory.value.push(currentMaxValues)
// console.log('💾 头部姿态最值已保存:', currentMaxValues)
// ElMessage.success(`头部姿态最值已保存(第${currentMaxValues.id}组)`)
// 更新历史数据表格(如果存在的话)
updateHistoryTable()
} catch (error) {
// console.error('❌ 保存最值失败:', error)
ElMessage.error('保存最值失败')
}
}
// 更新历史数据表格
function updateHistoryTable() {
try {
// 将头部姿态最值数据合并到现有的历史数据中
if (headPoseHistory.value.length > 0) {
const latestData = headPoseHistory.value[headPoseHistory.value.length - 1]
// 更新historyData数组添加头部姿态最值数据
const newHistoryItem = {
id: latestData.id,
rotLeft: latestData.rotationLeftMax,
rotRight: latestData.rotationRightMax,
tiltLeft: latestData.tiltLeftMax,
tiltRight: latestData.tiltRightMax,
pitchDown: latestData.pitchDownMax,
pitchUp: latestData.pitchUpMax,
timestamp: latestData.timestamp
}
// 如果historyData数组存在则添加新数据
if (historyData.value) {
historyData.value.push(newHistoryItem)
}
historyData.value.sort((a, b) => b.id - a.id);
console.log('📋 历史数据表格已更新')
}
} catch (error) {
console.error('❌ 更新历史数据表格失败:', error)
}
}
const footPressure = ref({
left_front: '',
left_rear: '',
right_front: '',
right_rear: '',
left_total: '',
right_total: '',
total_pressure: ''
})
const footImgSrc = ref('')
// 处理压力传感器足部压力数据
function handlePressureData(data) {
try {
if (data && data.foot_pressure) {
const pressureData = data.foot_pressure
// 更新足部压力数据
// 显示分区压力值
if (pressureData.pressure_zones) {
footPressure.value = pressureData.pressure_zones
}
// 处理压力图片
if (pressureData.pressure_image) {
// console.log(' 📊 接收到压力分布图片 (base64格式)')
// 这里可以将图片显示在界面上
if (pressureData.pressure_image && pressureData.pressure_image.length > 0) {
footImgSrc.value = pressureData.pressure_image
} else {
console.warn('⚠️ 收到空的压力传感器数据图')
}
}
}
} catch (error) {
console.error('❌ 处理压力传感器数据失败:', error)
}
}
// 保存检测数据
async function saveDetectionData() {
if (screenshotLoading.value) return
let titile_height = 24
try {
screenshotLoading.value = true
// 检查是否有活跃的会话ID
if (!patientInfo.value.sessionId) {
throw new Error('请先开始检测再进行数据保存')
}
const base64 = 'data:image/jpeg;base64,'
let body_image = ""
if(tempInfo.value.femtobolt_frame != null
&& tempInfo.value.femtobolt_frame.depth_image != null){
body_image = base64 + tempInfo.value.femtobolt_frame.depth_image
}
let pressure_image = ""
let foot_data = ""
if(tempInfo.value.pressure_data != null
&& tempInfo.value.pressure_data.foot_pressure != null
&& tempInfo.value.pressure_data.foot_pressure.pressure_image != null){
pressure_image = tempInfo.value.pressure_data.foot_pressure.pressure_image
foot_data = tempInfo.value.pressure_data.foot_pressure.pressure_zones
}
let pressureBox = pressureRef.value?.getBoundingClientRect()
let imuHeaderBox = imuHeaderRef.value?.getBoundingClientRect()
let titile_height = 24
pressure_image =[
Math.round(pressureBox.x), Math.round(pressureBox.y)+ titile_height,
Math.round(pressureBox.width), Math.round(pressureBox.height)
]
let head_data_image = []
head_data_image =[
Math.round(imuHeaderBox.x), Math.round(imuHeaderBox.y)+ titile_height,
Math.round(imuHeaderBox.width), Math.round(imuHeaderBox.height)
]
let foot1_image=""
if(tempInfo.value.camera1_frame != null
&& tempInfo.value.camera1_frame.image != null ){
foot1_image=base64 + tempInfo.value.camera1_frame.image
}
let foot2_image=""
if(tempInfo.value.camera2_frame != null
&& tempInfo.value.camera2_frame.image != null ){
foot2_image=base64 + tempInfo.value.camera2_frame.image
}
let head_pose={}
if(tempInfo.value.imu_data != null ){
head_pose=tempInfo.value.imu_data
}
if(headPoseMaxValues !=null){
head_pose.headPoseMaxValues = headPoseMaxValues.value
}
let screen_location = contenGridRef.value.getBoundingClientRect()
// 调用后端API保存截图
const result = await sendDetectionData({
session_id: patientInfo.value.sessionId,
patient_id: patientInfo.value.id,
screen_location:[Math.round(screen_location.x), Math.round(screen_location.y) + titile_height, Math.round(screen_location.width), Math.round(screen_location.height)],
head_pose:head_pose,
body_pose:null,
body_image: body_image,
foot_data:foot_data,
foot1_image:foot1_image,
foot2_image:foot2_image,
foot_data_image:pressure_image,
head_data_image:head_data_image,
screen_image:null
})
isoperation.value = true
// 显示成功消息和文件路径
ElMessage.success({
message: `检测数据保存成功!`,
duration: 500
})
} catch (error) {
console.error('❌ 检测数据保存失败:', error)
// 根据错误类型显示不同的错误消息
let errorMessage = '检测数据保存失败'
if (error.message.includes('网络连接失败')) {
errorMessage = '网络连接失败,请检查后端服务是否正常运行'
} else if (error.message.includes('服务器错误')) {
errorMessage = error.message
} else if (error.message.includes('未找到检测数据区域')) {
errorMessage = '检测数据区域不存在,请刷新页面重试'
} else if (error.message.includes('未找到检测数据')) {
errorMessage = '检测数据不存在,请刷新页面重试'
} else {
errorMessage = `检测数据保存失败: ${error.message}`
}
ElMessage.error({
message: errorMessage,
duration: 5000
})
} finally {
screenshotLoading.value = false
}
}
// 调用后端API保存检测数据
async function sendDetectionData(data) {
try {
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/save-data`
, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
if (result.success) {
console.log('📸 截图保存成功:', result.filepath)
return result
} else {
throw new Error(result.message || '保存失败')
}
} catch (error) {
console.error('💥 保存截图API调用失败:', error)
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error('网络连接失败,请检查后端服务是否正常运行')
} else if (error.message.includes('HTTP')) {
throw new Error(`服务器错误: ${error.message}`)
} else {
throw new Error(error.message)
}
}
}
// 处理开始/停止按钮点击
// async function handleStartStop() {
// if (isRecording.value) {
// // 停止录制视频
// await stopRecord()
// } else {
// // 开始录制视频
// await startRecord()
// }
// }
// 开始检测
async function startDetection() {
try {
console.log('🚀 正在开始检测...')
// 调用后端API开始检测
const response = await fetch(`${BACKEND_URL}/api/detection/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
patient_id: patientId.value,
// 可以添加其他检测参数
creator_id: creatorId.value,
})
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
if (result.success) {
console.log('✅ 检测开始成功')
// 保存会话ID和检测开始时间
patientInfo.value.sessionId = result.session_id
} else {
throw new Error(result.message || '开始检测失败')
}
} catch (error) {
console.error('💥 开始检测失败:', error)
ElMessage.error(`开始检测失败: ${error.message}`)
throw error
}
}
// 停止检测
async function stopDetection(summary = {}) {
try {
const payload = summary || {}
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error) {
console.error('❌ 停止检测失败:', error)
ElMessage.error(`停止检测失败: ${error.message}`)
}
}
// 格式化日期方法
const formatDate = (dateString) => {
if (!dateString) return '-'
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN')
}
const patientId = ref("")
// 加载患者信息
const loadPatientInfo = async () => {
try {
// 从路由参数获取患者ID
if (patientId.value == '' || patientId.value == null) {
console.warn('未找到患者ID参数')
return
}
// 调用API获取患者信息
const response = await fetch(`${BACKEND_URL}/api/patients/${patientId.value}`)
if (response.ok) {
const result = await response.json()
if (result.success) {
patientInfo.value = { ...result.data, sessionId: null }
console.log('患者信息加载成功:', patientInfo.value)
} else {
throw new Error(result.message)
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error) {
console.error('加载患者信息失败:', error)
ElMessage.warning('加载患者信息失败,请检查网络连接')
}
}
// 处理页面关闭或刷新事件
const handleBeforeUnload = (event) => {
console.log('页面即将关闭,正在清理资源...')
// 断开WebSocket连接
disconnectWebSocket()
console.log('✅ 资源清理完成')
}
const creatorId = ref('')
let rotationCharts = null;
let pitchCharts = null;
let tiltCharts = null;
const initchart = () => {
// 确保 DOM 元素已经渲染
nextTick(() => {
const chartDom = document.getElementById('rotationChartId');
if (chartDom) {
// 如果图表已经存在,先销毁
if (rotationCharts) {
try {
rotationCharts.dispose();
} catch (e) {
console.warn('rotationCharts dispose error:', e);
}
rotationCharts = null;
}
rotationCharts = echarts.init(chartDom);
rotationCharts.setOption(chartoption.value);
} else {
console.warn('找不到 ID 为 rotationChartId 的 DOM 元素');
}
const chartDom2 = document.getElementById('pitchChartId');
if (chartDom2) {
// 如果图表已经存在,先销毁
if (pitchCharts) {
try {
pitchCharts.dispose();
} catch (e) {
console.warn('pitchCharts dispose error:', e);
}
pitchCharts = null;
}
pitchCharts = echarts.init(chartDom2);
pitchCharts.setOption(chartoption.value);
} else {
console.warn('找不到 ID 为 pitchChartId 的 DOM 元素');
}
const chartDom3 = document.getElementById('tiltChartId');
if (chartDom3) {
// 如果图表已经存在,先销毁
if (tiltCharts) {
try {
tiltCharts.dispose();
} catch (e) {
console.warn('tiltCharts dispose error:', e);
}
tiltCharts = null;
}
tiltCharts = echarts.init(chartDom3);
tiltCharts.setOption(chartoption.value);
} else {
console.warn('找不到 ID 为 tiltChartId 的 DOM 元素');
}
// 添加窗口大小调整监听器
window.addEventListener('resize', () => {
if (rotationCharts && !rotationCharts.isDisposed()) {
try {
rotationCharts.resize();
} catch (e) {
console.warn('rotationCharts resize error:', e);
}
}
if (pitchCharts && !pitchCharts.isDisposed()) {
try {
pitchCharts.resize();
} catch (e) {
console.warn('pitchCharts resize error:', e);
}
}
if (tiltCharts && !tiltCharts.isDisposed()) {
try {
tiltCharts.resize();
} catch (e) {
console.warn('tiltCharts resize error:', e);
}
}
});
});
}
const calibrationClick = async () => {
const response = await fetch(`${BACKEND_URL}/api/devices/calibrate/imu`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
})
clearAndStartTracking()
if (response.ok) {
const result = await response.json()
if (result.success) {
ElMessage.success(result.message)
} else {
ElMessage.error(result.message)
}
}
}
const cameraSubmit = async () => {
if(cameraForm.value.camera1.device_index == cameraForm.value.camera2.device_index){
ElMessage.error('相机上和相机下的号码不能相同!')
return
}
const response = await fetch(`${BACKEND_URL}/api/config/devices/all`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(cameraForm.value)
})
if (response.ok) {
const result = await response.json()
if (result.success) {
ElMessage.success(result.message)
cameraDialogVisible.value = false
} else {
ElMessage.error(result.message)
}
}else {
const result = await response.json()
ElMessage.error(result.message)
}
}
// 加载相机参数信息
const getDevicesInit = async () => {
try {
// 调用API获取患者信息
const response = await fetch(`${BACKEND_URL}/api/config/devices`)
if (response.ok) {
const result = await response.json()
if (result.success) {
console.log('相机参数加载成功:', result.data)
cameraForm.value = result.data
cameraDialogVisible.value = true
// console.log('相机参数加载成功:', patientInfo.value)
} else {
throw new Error(result.message)
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error) {
console.error('加载相机参数失败:', error)
ElMessage.warning('加载相机参数失败,请检查网络连接')
}
}
onMounted(() => {
for(let i = 0; i < 20; i++){
let port = "COM" + (i + 1)
remotePortData.value.push(port)
}
if (authStore.currentUser) {
console.log(authStore.currentUser)
creatorId.value = authStore.currentUser.id
}
patientId.value = props.selectedPatient.id
//patientId.value = '202511150005'
// 加载患者信息
loadPatientInfo()
// 启动检测
startDetection()
// 页面加载时自动连接WebSocket
connectWebSocket()
// 监听页面关闭或刷新事件
window.addEventListener('beforeunload', handleBeforeUnload)
})
onUnmounted(() => {
console.log('🔄 Detection组件正在卸载开始清理资源...')
try {
// 清理定时器
if (timerId.value) {
clearInterval(timerId.value)
timerId.value = null
console.log('✅ 定时器已清理')
}
// 断开WebSocket连接
disconnectWebSocket()
// 清理图表资源
if (tiltCharts) {
try {
tiltCharts.dispose()
console.log('✅ tiltCharts已清理')
} catch (e) {
console.warn('tiltCharts dispose error in onUnmounted:', e)
}
tiltCharts = null
}
if (rotationCharts) {
try {
rotationCharts.dispose()
console.log('✅ rotationCharts已清理')
} catch (e) {
console.warn('rotationCharts dispose error in onUnmounted:', e)
}
rotationCharts = null
}
if (pitchCharts) {
try {
pitchCharts.dispose()
} catch (e) {
}
pitchCharts = null
}
// 移除页面关闭事件监听器
window.removeEventListener('beforeunload', handleBeforeUnload)
} catch (error) {
}
})
const startRecord = async () => { // 开始录屏
try {
console.log('🚀 正在开始录屏...')
startTimer()
// 验证患者信息
if (!patientInfo.value || !patientInfo.value.sessionId) {
throw new Error('缺少患者信息,无法开始录屏')
}
let screen_location = contenGridRef.value.getBoundingClientRect()
let femtobolt_location = wholeBodyRef.value.getBoundingClientRect()
let camera1_location = camera1Ref.value?.getBoundingClientRect()
let camera2_location = camera2Ref.value?.getBoundingClientRect()
let titile_height = 24
// 调用后端API开始录屏
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/start_record`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
patient_id: patientId.value,
// 可以添加其他录屏参数
creator_id: creatorId.value,
screen_location:[Math.round(screen_location.x), Math.round(screen_location.y) + titile_height, Math.round(screen_location.width), Math.round(screen_location.height)],
camera1_location:[
Math.round(camera1_location.x), Math.round(camera1_location.y)+ titile_height,
Math.round(camera1_location.width), Math.round(camera1_location.height)
],
camera2_location:[
Math.round(camera2_location.x), Math.round(camera2_location.y)+ titile_height,
Math.round(camera2_location.width), Math.round(camera2_location.height)
],
femtobolt_location:[Math.round(femtobolt_location.x), Math.round(femtobolt_location.y) + titile_height, Math.round(femtobolt_location.width), Math.round(femtobolt_location.height)],
})
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result = await response.json()
if (result.success) {
// 保存会话ID和检测开始时间
patientInfo.value.detectionStartTime = Date.now()
console.log('✅ 录屏会话创建成功会话ID:', patientInfo.value.sessionId)
isRecording.value = true
// ElMessage.success('录屏已开始')
} else {
throw new Error(result.message || '开始录屏失败')
}
} catch (error) {
ElMessage.error(`开始录屏失败: ${error.message}`)
throw error
}
}
const stopRecord = async () => { // 停止录屏
try {
resetTimer()
// 计算检测持续时间
let duration = 0
if (patientInfo.value.detectionStartTime) {
duration = Math.floor((Date.now() - patientInfo.value.detectionStartTime) / 1000)
}
// 调用后端API停止检测
const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop_record`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
duration: duration
})
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
isRecording.value = false
} catch (error) {
console.error('❌ 停止检测失败:', error)
ElMessage.error(`停止检测失败: ${error.message}`)
}
}
function routerClick(){
historyDialogVisible.value = true
}
const isRestart = ref(false) // 防止连击
// 单个刷新数据
function refreshClick(type) {
// 检查是否在冷却期内
if (isRestart.value === true) {
ElMessage.warning(`请勿连续点击设备重启按钮请等待5秒后再试`)
return
}
// 设置冷却状态
isRestart.value = true
// 5秒后重置状态
setTimeout(() => {
isRestart.value = false
}, 5000)
ElMessage.warning(`🚀 发送重启设备请求...`)
if (devicesSocket && devicesSocket.connected) {
if(type == 'camera1'){
devicesSocket.emit('restart_device', { device_type: 'camera1' })
}else if(type == 'camera2'){
devicesSocket.emit('restart_device', { device_type: 'camera2' })
}else if(type == 'femtobolt'){
devicesSocket.emit('restart_device', { device_type: 'femtobolt' })
}else if(type == 'imu'){
devicesSocket.emit('restart_device', { device_type: 'imu' })
}else if(type == 'pressure'){
devicesSocket.emit('restart_device', { device_type: 'pressure' })
}else if(type == 'remote'){
devicesSocket.emit('restart_device', { device_type: 'remote' })
}
} else {
console.warn('⚠️ Socket服务未连接无法重启设备')
}
}
async function closeDiagnosticMessage(e){
isDiagnosticMessage.value = false
if(e == '关闭'){
isTip.value = false
}
if (e === true || (typeof e === 'object' && e)){
try {
// 清理定时器
if (timerId.value) {
clearInterval(timerId.value)
timerId.value = null
console.log('✅ 定时器已清理')
}
// 停止录制
if (isRecording.value === true) {
stopRecord()
console.log('✅ 录制已停止')
}
const summary = typeof e === 'object' && e !== null ? {
diagnosis_info: e.diagnosis_info ?? '',
treatment_info: e.treatment_info ?? '',
remark_info: (e.remark_info ?? e.suggestion_info ?? '')
} : {
diagnosis_info: diagnosticForm.value.diagnosis_info,
treatment_info: diagnosticForm.value.treatment_info,
remark_info: diagnosticForm.value.suggestion_info
}
await stopDetection(summary)
// 断开WebSocket连接
disconnectWebSocket()
// 移除页面关闭事件监听器
window.removeEventListener('beforeunload', handleBeforeUnload)
emit('endChange',true)
routeTo('/')
} catch (error) {
console.error('❌ Detection组件卸载时出错:', error)
emit('endChange',true)
}
}
}
const isPhotoAlbum = ref(false)
function closePhotoAlbum(){
isPhotoAlbum.value = false
}
function closecreatbox(e){
if(e === true){
loadPatientInfo()
}
isCloseCreat.value = false
}
function handleEditUserInfo(){
isCloseCreat.value = true
}
function closePatientProfile(){
historyDialogVisible.value =false
}
function closeViewUserInfo(){
isViewUser.value = false
}
function viewClick(e){
isExpand.value = e
}
</script>
<style scoped>
/* 全局容器 */
.detection-container-box {
/* display: flex; */
height: 100vh;
background-color: #191d28;
color: #FFFFFF;
}
.detection-container-box .el-row {
margin: 0!important;
height: calc(100%);
}
.username-line {
width: 2px;
height: 22px;
margin: 0 10px;
background-color: #fff;
}
.displaycontainer {
width: 100%;
height: 62px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 10px;
}
.displayleft {
display: flex;
align-items: center;
}
.icon-box{
width: 32px;
height: 32px;
background-color: #374151;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
cursor: pointer;
}
.icon-box:hover{
background-color: rgb(20 170 255 / 30%);
border: 1px solid rgb(20 170 255 / 50%);
}
.startvideo-box{
width: 130px;
display: flex;
align-items: center;
margin-left: 10px;
font-family:'Noto Sans SC Bold', 'Noto Sans SC Regular', 'Noto Sans SC';
font-weight:700;
font-style:normal;
font-size:14px;
color:#FFFFFF;
}
.conductanimated{
margin-right: 5px;
animation: fadeInOut 1s infinite;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0; } /* 初始状态和结束状态为完全透明 */
50% { opacity: 1; } /* 中间状态为完全不透明 */
}
.body-posture-box{
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
}
.body-header-box{
width: 100%;
height: calc(50% - 7px) ;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
}
.body-foot-box{
margin-top: 15px;
width: 100%;
height: calc(50% - 7px) ;
/* background:linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
}
.body-title-display{
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 20px;
font-family: "Noto Sans SC Bold", "Noto Sans SC Regular", "Noto Sans SC";
font-weight: 700;
font-style: normal;
font-size: 20px;
color: rgb(255, 255, 255);
}
.body-son-display{
display: flex;
align-items: center;
}
.connecttext{
font-weight: 400;
font-style: normal;
font-size: 14px;
}
.body-header-bottombox{
width: 100%;
height: calc(100% - 50px);
display: flex;
justify-content: center;
box-sizing: border-box;
padding: 0 20px;
}
.body-header-bottombox-left{
width: 25%;
height: calc(100%);
display: flex;
flex-direction: column;
align-content:space-between;
}
.body-header-bottombox-lefttext{
width: 100%;
height: 33%;
white-space:nowrap;
}
.body-header-bottombox-content{
width: 40%;
height: calc(100%);
display: flex;
flex-wrap: wrap;
align-items:center;
}
.body-header-bottombox-right{
width: 25%;
height: calc(100%);
display: flex;
flex-direction: column;
align-content:space-between;
}
.body-header-bottombox-righttext{
width: 100%;
height: 33%;
white-space:nowrap;
text-align: right;
}
.currencytext1{
min-width: 100px;
font-size: 16px;
color: #FFFFFF;
font-family: "Noto Sans SC";
font-weight: 400;
color: #D1D5DB;
}
.currencytext2{
font-weight: 700;
font-style: normal;
font-size: 24px;
color: #3BF2C6;
}
.currencytext3{
width: 60px;
font-weight: 400;
font-style: normal;
font-size: 16px;
color: #FFFFFF;
display: inline-block;
text-align: left;
}
.currencytext4{
width: 20px;
font-weight: 400;
font-style: normal;
font-size: 16px;
color:#D1D5DB;
display: inline-block;
text-align: left;
}
.body-footbottom-box{
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 50px);
}
.body-footbottom-left{
width: 28%;
height: calc(100%);
display: flex;
align-content: center;
justify-content: center;
flex-wrap: wrap;
}
.body-footbottom-leftbox{
min-width: 215px;
width: 80%;
min-height: 60px;
height: 20%;
background: inherit;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 20px;
}
.body-footbottom-center{
width: 40%;
height: calc(100%);
}
.body-footbottom-topbox{
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
height: 50px;
font-weight: 400;
font-style: normal;
font-size: 24px;
color: #FFFFFF;
}
.xline {
position: absolute;
width: calc(100% + 20px) ;
border-top: 1px dashed red;
top: 50%;
left: -10px;
}
.yline {
position: absolute;
height: calc(100% + 20px);
border-left: 1px dashed red;
top: -10px;
left: 50%;
}
.body-footbottom-leftbottom{
width: 100%;
height: calc(100% - 60px);
/* height: 281px; */
display: flex;
justify-content: center;
flex-wrap: wrap;
align-content:space-between ;
}
.body-userinfo-box{
position: relative;
z-index: 10;
width: 100%;
height: 346px;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
}
.body-userinfo-expandbox{
height: 638px ;
}
.body-userinfo-box1{
width: 100%;
height: 534px ;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
}
.body-userinfo-box3{
width: 100%;
height: 100% ;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
}
.body-video-box{
width: 100%;
height: calc(100% - 348px - 14px) ;
/* background: linear-gradient(135deg, rgba(42, 54, 73, 1) 0%, rgba(42, 54, 73, 1) 0%, rgba(34, 43, 56, 1) 100%, rgba(34, 43, 56, 1) 100%); */
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
margin-top: 15px;
}
.body-video-box1{
width: 100%;
height: calc(100% - 534px - 14px) ;
background: linear-gradient(135deg, #1a1e2a 0%, #222b38 100%);
border: 1px solid #262d40;
border-radius: 4px;
margin-top: 15px;
}
.body-userinfo-content{
width: 100%;
height: calc(100% - 50px);
padding: 15px;
}
.body-userinfo-content-top{
position: relative;
width: calc(100%);
height: 116px;
background-color: #374151;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 20px;
}
.body-userinfo-content-bottom{
padding:10px;
padding-top: 20px;
position: relative;
width: calc(100%);
height: calc(100% - 116px);
border-radius: 8px;
display: flex;
flex-wrap: wrap;
align-content: start;
border-width: 0px 1px 1px;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-right-color: rgb(55, 65, 81);
border-bottom-color: rgb(55, 65, 81);
border-left-color: rgb(55, 65, 81);
border-top-style: initial;
border-top-color: initial;
border-radius: 0px 0px 8px 8px;
box-shadow: none;
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
}
.body-userinfo-content-bottom0{
padding:10px;
padding-top: 20px;
position: relative;
width: calc(100%);
height: calc(100% - 116px);
border-radius: 8px;
display: flex;
flex-wrap: wrap;
align-content: start;
border-width: 0px 1px 1px;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-right-color: rgb(55, 65, 81);
border-bottom-color: rgb(55, 65, 81);
border-left-color: rgb(55, 65, 81);
border-top-style: initial;
border-top-color: initial;
border-radius: 0px 0px 8px 8px;
box-shadow: none;
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
}
.body-userinfo-content-bottom1{
padding:10px;
padding-top: 10px;
position: relative;
width: calc(100%);
height: calc(100% - 116px);
border-radius: 8px;
display: flex;
flex-wrap: wrap;
align-content: start;
border-width: 0px 1px 1px;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-right-color: rgb(55, 65, 81);
border-bottom-color: rgb(55, 65, 81);
border-left-color: rgb(55, 65, 81);
border-top-style: initial;
border-top-color: initial;
border-radius: 0px 0px 8px 8px;
box-shadow: none;
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
}
.body-userinfo-content-bottom2{
padding:10px;
padding-top: 20px;
position: relative;
width: calc(100%);
height: calc(100% - 116px);
border-radius: 8px;
display: flex;
flex-wrap: wrap;
align-content: start;
border-width: 0px 1px 1px;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-right-color: rgb(55, 65, 81);
border-bottom-color: rgb(55, 65, 81);
border-left-color: rgb(55, 65, 81);
border-top-style: initial;
border-top-color: initial;
border-radius: 0px 0px 8px 8px;
box-shadow: none;
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
}
.userinfo-disyplay1{
display: flex;
width: calc(64%);
padding-top: 20px;
}
.userinfo-disyplay2{
display: flex;
width: calc(36%);
padding-top: 20px;
}
.userinfo-disyplaypadding1{
width: calc(64%);
padding-bottom: 15px;
}
.padding10{
padding-bottom: 5px;
}
.userinfo-disyplaypadding2{
width: calc(36%);
padding-bottom: 15px;
}
.userinfo-disyplaypadding3{
width: calc(100%);
padding-bottom: 15px;
}
.userinfo-disyplaypadding4{
/* display: flex; */
width: calc(64%);
padding-bottom: 5px;
}
.userinfo-disyplaypadding5{
/* display: flex; */
width: calc(36%);
padding-bottom: 5px;
}
.userinfo-disyplaypadding6{
/* display: flex; */
width: calc(100%);
padding-bottom: 5px;
}
.useravatar-box{
width: 80px;
height: 80px;
border-radius: 50%;
background: #303947;
display: flex;
align-items: center;
justify-content: center;
margin-right: 35px;
}
.userinfo-text-top{
display: flex;
align-items: center;
}
.userinfo-text1{
font-weight: 700;
font-style: normal;
color: #FFFFFF;
font-size: 20px;
}
.userinfo-text2{
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
font-size: 16px;
color: rgba(255, 255, 255, 0.6);
}
.userinfo-text3{
font-family: "Noto Sans SC Bold", "Noto Sans SC Regular", "Noto Sans SC";
font-weight: 700;
font-style: normal;
font-size: 16px;
color: rgb(255, 255, 255);
margin-top: 15px;
}
.userinfo-text4{
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
width: 60px;
}
.userinfo-text5{
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
font-size: 16px;
color: rgb(255, 255, 255);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.userinfo-line{
width: 2px;
height: 16px;
background: rgba(255, 255, 255, 0.6);
margin: 0 8px;
}
.userinfo-edit-img{
position: absolute;
right: 15px;
top: 15px;
}
.body-video-content{
width: 100%;
height: calc(100% - 50px);
padding: 15px;
box-sizing: border-box;
display: flex;
align-content: space-between;
flex-wrap: wrap;
justify-content: center;
}
.body-video-imgbox1{
position: relative;
width: 100%;
height: calc(50% - 7px);
}
.body-video-imgbox2{
position: relative;
width: 100%;
height: calc(50% - 8px);
}
.body-video-imgbox3{
position: relative;
width: 100%;
height: calc(100%);
}
.patientInfotop1{
font-weight: 700;
font-style: normal;
font-size: 20px;
color: #FFFFFF;
margin:0 30px;
margin-left:40px
}
.patientInfotop2{
font-weight: 400;
font-style: normal;
font-size: 16px;
color: #FFFFFF;
}
.endbutton{
width: 72px;
height: 28px;
background-color: rgba(11, 148, 213, 1);
border:1px solid rgba(11, 148, 213, 1);
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #D7D7D7;
margin-left:20px
}
.endbutton:hover {
background-color: #14aaff;
border:1px solid #14aaff;
color: #fff;
}
.pop-up-mask{
position: fixed;
z-index: 99;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
}
.pop-up-tip-container {
width: 400px;
height:220px;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
margin: auto;
border-radius: 10px;
background: #1b202c;
/* background: linear-gradient(135deg, rgba(53, 67, 90, 1) 0%, rgba(53, 67, 90, 1) 0%, rgba(62, 79, 105, 1) 99%, rgba(62, 79, 105, 1) 100%); */
}
.pop-up-camera-container{
width: 668px;
height:630px;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
margin: auto;
background: linear-gradient(135deg, rgb(53, 67, 90) 0%, rgb(53, 67, 90) 0%, rgb(62, 79, 105) 99%, rgb(62, 79, 105) 100%);
border-radius: 10px;
box-shadow: rgb(17, 24, 33) 0px 0px 10px;
}
.pop-up-camera-header{
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 40px;
font-family: 'Noto Sans SC';
font-weight: 700;
font-style: normal;
font-size: 20px;
color: #FFFFFF;
text-align: left;
border-radius:10px 10px 0 0;
}
.pop-up-tip-header{
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 30px;
font-family: 'Noto Sans SC';
font-weight: 700;
font-style: normal;
font-size: 20px;
color: #FFFFFF;
text-align: left;
}
.tipconfirmbutton-box{
width:100%;
display:flex;
align-items: center;
justify-content: center;
}
.tipconfirmbutton{
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgb(255, 255, 255);
background:#0b94d5;
border:1px solid #0b94d5;
width: 80px;
height: 40px;
}
.tipconfirmbutton:hover{
background:#14aaff;
border:1px solid #14aaff;
}
.pop-up-tip-text{
width:100%;
font-weight: 400;
font-style: normal;
font-size: 16px;
color: #FFFFFF;
text-align: center;
padding:30px 10px;
}
.pop-up-camera-body{
padding: 20px 40px;
}
.pop-up-camera-display{
display: flex;
align-items: center;
}
.pop-up-camera-line{
width: 4px;
height: 13px;
background-color: rgba(38, 111, 255, 1);
margin-right: 3px;
}
.pop-up-camera-title{
font-weight: 700;
font-style: normal;
font-size: 15px;
color: #FFFFFF;
}
.pop-up-camera-body :deep(.el-input__wrapper) {
background-color: #2a3649;
border-width: 1px;
border-style: solid;
border-color: #2a3649;
border-radius: 4px;
box-shadow: none;
height: 40px;
}
.pop-up-camera-body :deep(.el-input__wrapper:hover) {
border-color: rgba(38, 111, 255, 1);
}
.pop-up-camera-body :deep( .el-input__inner) {
font-weight: 400;
font-style: normal;
font-size: 14px;
letter-spacing: normal;
color: #FFFFFF;
}
.pop-up-camera-name{
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgba(255,255,255,0.6);
width: 60px;
text-align: right;
margin-right: 20px;
}
.pop-up-camera-radio{
width: 70px;
height: 40px;
background: inherit;
background-color: rgba(40, 40, 40, 1);
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: rgba(54, 54, 54, 1);
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
}
.pop-up-camera-display :deep(.el-radio__label){
font-weight: 400;
font-style: normal;
color: #FFFFFF;
font-size: 14px;
}
.pop-up-camera-display :deep(.el-radio){
width: 70px;
height: 40px;
background: inherit;
background: #2a3649;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: #4a576b;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
}
.pop-up-camera-display :deep(.el-radio.is-checked){
width: 70px;
height: 40px;
background: inherit;
background-color: #2a3649;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: #14aaff;
}
.pop-up-camera-display :deep(.is-checked .el-radio__label){
color: #14aaff;
}
.pop-up-camera-display :deep(.el-radio__input.is-checked .el-radio__inner){
background: #14aaff;
border-color: #14aaff;
}
.pop-up-camera-display :deep(.el-radio__input.is-checked .el-radio__inner){
background: #14aaff;
border-color: #14aaff;
}
.pop-up-camera-display :deep(.el-radio__input.is-checked .el-radio__inner:after){
background-color: #2a3649;
}
.pop-up-camera-display :deep(.el-radio__inner){
background-color: #2a3649;
border-color: #4a576b;
}
</style>
<style>
.historyDialogVisible.el-dialog{
--el-dialog-margin-top: 0 !important;
margin-bottom: 0;
}
.historyDialogVisible .el-dialog__body{
padding: 0;
}
.pop-up-camera-container .form-actions-display{
display: flex;
justify-content: flex-end;
padding-top: 40px;
padding-right: 10px;
}
.calibration-zero{
width: 72px;
height: 28px;
background-color: rgb(69, 91, 123);
display:flex;
align-items:center;
justify-content:center;
border-radius: 4px;
box-shadow: none;
font-family: "Noto Sans SC";
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgb(255, 255, 255);
margin-left:10px;
cursor:pointer;
}
.calibration-zero:hover{
background-color: #14AAFF;
}
.noImageSvg-bg{
display:flex;
justify-content:center;
align-items:center;
}
.pop-up-camera-container .formreturnCancel{
width: 80px;
height: 40px;
background: rgba(89, 113, 148, 1);
border: 1px solid rgba(89, 113, 148, 1);
font-family: 'Noto Sans SC';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
}
.pop-up-camera-container .formreturnCancel:hover{
background: #14aaff;
color:#ffffff;
}
.pop-up-camera-container .formsaveCancel {
width: 80px;
height: 40px;
background: #0b94d5;
font-family: 'Noto Sans SC';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #FFFFFF;
}
.pop-up-camera-container .formsaveCancel:hover{
background: #14AAFF;
color:#ffffff;
}
.big-img{
position: absolute;
left: 10px;
top: 10px;
width: 30px;
height: 30px;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
</style>
<style>
.pop-up-camera-display .el-select__placeholder {
color: #ffffff;
font-size: 14px;
}
.pop-up-camera-display .el-select__wrapper {
background-color: #2a3649;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-radius: 4px;
box-shadow: none;
height: 40px;
}
.pop-up-camera-display .el-select__wrapper:hover{
border-color: #3d4c65;
box-shadow: 0 0 0 1px transparent inset;
}
</style>