-
-
-

-
视频
-
-
-

-
- {{ femtoboltStatus }}
-
-
-
![camera1]()
+
+
+
+

+
视频
+
+
+

+
+ {{ cameraStatus }}
+
+
-
+
+
+
+
+
+
+
-
-
+
本次操作未保存有效数据,不予记录。
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
相机上
+
+
+ 1号
+ 2号
+ 3号
+ 4号
+ 5号
+
+
+
+
+
相机下
+
+
+ 1号
+ 2号
+ 3号
+ 4号
+ 5号
+
+
+
+
+ 退出
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
@@ -300,8 +427,12 @@ import { useAuthStore } from '../stores/index.js'
import * as echarts from 'echarts'
import { getBackendUrl, patientAPI } from '../services/api.js'
import noImageSvg from '@/assets/no-image.svg'
-const authStore = useAuthStore()
+import DiagnosticMessage from '@/views/DiagnosticMessage.vue'
+import PatientCreate from '@/views/PatientCreate.vue'
+import HistoryDashboard from '@/views/PatientProfile.vue'
+
const emit = defineEmits([ 'endChange']);
+const asd = ref(0)
const props = defineProps({
selectedPatient: {
required: false,
@@ -309,49 +440,267 @@ const props = defineProps({
default: null
}
})
+const isCloseCreat =ref(false) // 是否打开患者信息编辑框
+const isoperation = ref(false) // 是否保存数据
+const isTip =ref(false)
+const isStartVideo = ref(false)
+function startVideoClick() {
+ startRecord()
+ isoperation.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
+ }
+function closeTipClick(){
+ emit('endChange',false)
+}
-// 四个设备的独立连接状态
-const cameraStatus = ref('未连接') // 相机设备状态
-const femtoboltStatus = ref('未连接') // 深度相机(FemtoBolt)设备状态
-const imuStatus = ref('未连接') // IMU设备状态
-const pressureStatus = ref('未连接') // 压力传感器设备状态
-const footImgSrc = ref('') // 足底压力传感器图片
-
-
-const patientInfo =ref({})
-const seconds = ref(0);
-const isRunning = ref(false);
-const timerId = ref(null);
-const blinkState = ref(false);
-
+const isDiagnosticMessage = ref(false)
+const isBig =ref(false)
+const authStore = useAuthStore()
+const router = useRouter()
+const route = useRoute()
+const isRecording = ref(false)
+const isConnected = ref(false)
const rtspImgSrc = ref('')
-const camera1ImgSrc = ref('') // 相机视频流1
-const camera2ImgSrc = ref('')// 相机视频流2
-
+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 videoImgRef =ref(null) // 视频流图片ref
+const camera1Ref = ref(null)
+const camera2Ref = ref(null)
+const historyDialogVisible = ref(false)
+// 录像相关变量
+let mediaRecorder = null
+let recordedChunks = []
+let recordingStream = null
+// 患者信息(从页面获取或通过API获取)
+const patientInfo = ref({
+ id: '',
+ name: '',
+ gender: '',
+ birth_date: '',
+ age: '',
+ height: '',
+ weight: '',
+ shoe_size: '',
+ phone: '',
+ nationality: '',
+ created_at: '',
+ sessionId: null // 检查记录ID,实际使用时应从路由或API获取
+})
-// 处理开始/停止按钮点击
-async function handleStartStop() {
- if (isRecording.value) {
- // 停止录制视频
- await stopRecord()
- } else {
- // 开始录制视频
- await startRecord()
- }
+// 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
+
+// 后端服务器地址配置
+const BACKEND_URL = getBackendUrl()
+const resDialogVisible = ref(false)
+const reshandleClose = () => {
+ resDialogVisible.value = false
}
+const cameraHandleClose = () => {
+ cameraDialogVisible.value = false
+}
+const dialogVisible = ref(false)
+const handleClose = () => {
+ dialogVisible.value = 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:{
+ port: '', // IMU串口号
+ }
+})
+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 cameraStatus = ref('未连接') // 相机设备状态
+const femtoboltStatus = ref('未连接') // 深度相机(FemtoBolt)设备状态
+const imuStatus = ref('未连接') // IMU设备状态
+const pressureStatus = 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 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;
@@ -390,210 +739,106 @@ const resetTimer = () => {
seconds.value = 0;
blinkState.value = false;
};
-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('录屏已开始')
+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(result.message || '开始录屏失败')
+ throw new Error(response.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}`)
+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)
}
-const isStartVideo = ref(false)
-function startVideoClick() {
- startRecord()
- isStartVideo.value = true
-}
-function stopVideoClick() {
- stopRecord()
- isStartVideo.value = false
-}
-function endClick() {
- emit('endChange',false)
- ElMessage.success('结束监测')
-}
-const patientId = ref("") // 患者ID
-const loadPatientInfo = async () => { // 加载患者信息
- try {
- debugger
- // 从路由参数获取患者ID
- // patientId.value = props.selectedPatient.id
- patientId.value = '202511150005'
- if (patientId.value == '' || patientId.value == null) {
- console.warn('未找到患者ID参数')
- return
+function cameraUpdate() { // 相机设置数据更新弹框
+ cameraForm.value = { // 相机参数
+ camera1:{
+ device_index: '', // 序号1
+ },
+ camera2:{
+ device_index: '', // 序号2
+ },
+ femtobolt:{
+ algorithm_type: '', // 算法类型
+ depth_mode: '', // 相机模式
+ depth_range_min: '', // 距离范围最小值
+ depth_range_max: '', // 距离范围最大值
+ },
+ imu:{
+ port: '', // IMU串口号
}
-
- // 调用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('加载患者信息失败,请检查网络连接')
}
+ // 加载相机参数信息
+ getDevicesInit()
+
}
-onMounted(() => {
- if (authStore.currentUser) {
- creatorId.value = authStore.currentUser.id
- }
-
- // 加载患者信息
- loadPatientInfo()
- // 启动检测
- // startDetection()
- // 页面加载时自动连接WebSocket
- // connectWebSocket()
-
- // 监听页面关闭或刷新事件
- window.addEventListener('beforeunload', handleBeforeUnload)
-
-})
-// 开始检测
-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: route.params.id,
- // 可以添加其他检测参数
- 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() {
- try {
- // 计算检测持续时间
- let duration = 0
- // 调用后端API停止检测
- const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- duration: duration
- })
- })
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
- } catch (error) {
- console.error('❌ 停止检测失败:', error)
- ElMessage.error(`停止检测失败: ${error.message}`)
+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 {
@@ -654,7 +899,7 @@ function connectWebSocket() {
console.log('✅ 主WebSocket连接成功!Socket ID:', socket.id)
isConnected.value = true
//绘制头部仪表盘
- // initchart()
+ initchart()
})
socket.on('connect_error', (error) => {
@@ -800,7 +1045,671 @@ function connectWebSocket() {
isConnected.value = false
}
}
-const creatorId = ref('')
+
+// 启动设备数据推送
+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: 'camera' })
+ 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
+ }
+
+ // 重置所有设备状态
+ cameraStatus.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) {
+ const url = 'data:image/jpeg;base64,' + base64Image
+ if (String(deviceId).toLowerCase() === 'camera2') {
+ camera2ImgSrc.value = url
+ } else {
+ camera1ImgSrc.value = url
+ // 旧变量保留(避免其它位置引用出错)
+ rtspImgSrc.value = url
+ }
+ } 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,
+ Math.abs(headPose.rotation)
+ )
+ } else if (headPose.rotation > 0) {
+ // 右旋(正值)
+ headPoseMaxValues.value.rotationRightMax = Math.max(
+ headPoseMaxValues.value.rotationRightMax,
+ headPose.rotation
+ )
+ }
+
+ // 更新倾斜角最值
+ if (headPose.tilt < 0) {
+ // 左倾(负值),取绝对值的最大值
+ headPoseMaxValues.value.tiltLeftMax = Math.max(
+ headPoseMaxValues.value.tiltLeftMax,
+ Math.abs(headPose.tilt)
+ )
+ } else if (headPose.tilt > 0) {
+ // 右倾(正值)
+ headPoseMaxValues.value.tiltRightMax = Math.max(
+ headPoseMaxValues.value.tiltRightMax,
+ headPose.tilt
+ )
+ }
+
+ // 更新俯仰角最值
+ if (headPose.pitch < 0) {
+ // 下俯(负值),取绝对值的最大值
+ headPoseMaxValues.value.pitchDownMax = Math.max(
+ headPoseMaxValues.value.pitchDownMax,
+ Math.abs(headPose.pitch)
+ )
+ } else if (headPose.pitch > 0) {
+ // 上仰(正值)
+ headPoseMaxValues.value.pitchUpMax = Math.max(
+ headPoseMaxValues.value.pitchUpMax,
+ 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 handleDiagnosticInfo(status) {
+ try {
+ // 检查是否有活跃的会话ID
+ if (!patientInfo.value.sessionId) {
+ throw new Error('缺少会话Id')
+ }
+ // 调用后端API采集检测数据
+ const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/save-info`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ diagnosis_info: diagnosticForm.diagnosis_info,
+ treatment_info: diagnosticForm.treatment_info,
+ suggestion_info: diagnosticForm.suggestion_info,
+ status: status,
+ session_id: patientInfo.value.sessionId,
+ })
+ })
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+ }
+
+ const result = await response.json()
+ if (result.success) {
+ // 显示成功消息
+ ElMessage.success({
+ message: status + '诊断信息成功',
+ duration: 5000
+ })
+
+ } else {
+ throw new Error(result.message || '诊断信息失败')
+ }
+ } catch (error) {
+ ElMessage.error({
+ message: errorMessage,
+ duration: 5000
+ })
+
+ } finally {
+
+ }
+}
+
+// 保存检测数据
+async function saveDetectionData() {
+ console.log(tempInfo.value)
+ if (screenshotLoading.value) return
+
+ try {
+ screenshotLoading.value = true
+ // 显示保存进度
+ ElMessage.info('正在保存检测截图数据...')
+
+ // 检查是否有活跃的会话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 foot_image=""
+ if(tempInfo.value.camera_frame != null
+ && tempInfo.value.camera_frame.image != null ){
+ foot_image=base64 + tempInfo.value.camera_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,
+
+ head_pose:head_pose,
+ body_pose:null,
+ body_image: body_image,
+
+ foot_data:foot_data,
+ foot_image:foot_image,
+ foot_data_image:pressure_image,
+ screen_image:null
+
+ })
+ isoperation.value = true
+ // 显示成功消息和文件路径
+ ElMessage.success({
+ message: `检测数据保存成功!`,
+ duration: 5000
+ })
+
+
+ } 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}/collect`
+ , {
+ 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() {
+ try {
+ // 计算检测持续时间
+ let duration = 0
+ // 调用后端API停止检测
+ const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/stop`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ duration: duration
+ })
+ })
+ 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('页面即将关闭,正在清理资源...')
@@ -813,29 +1722,88 @@ const handleBeforeUnload = (event) => {
console.log('✅ 资源清理完成')
}
-
-
-
-// 深度相机帧显示函数
-function displayDepthCameraFrame(base64Image) {
- if (base64Image && base64Image.length > 0) {
- depthCameraImgSrc.value = 'data:image/jpeg;base64,' + base64Image
- } else {
- console.warn('⚠️ 收到空的深度相机帧数据')
- }
+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 headPoseMaxValues = ref({
- rotationLeftMax: 0, // 旋转-左旋最大值
- rotationRightMax: 0, // 旋转-右旋最大值
- tiltLeftMax: 0, // 倾斜-左倾最大值
- tiltRightMax: 0, // 倾斜-右倾最大值
- pitchUpMax: 0, // 俯仰-上仰最大值
- pitchDownMax: 0 // 俯仰-下俯最大值
-})
-
-
-const calibrationClick = async () => { // 校准按钮点击事件
+const calibrationClick = async () => {
const response = await fetch(`${BACKEND_URL}/api/devices/calibrate/imu`, {
method: 'POST',
headers: {
@@ -854,23 +1822,222 @@ const calibrationClick = async () => { // 校准按钮点击事件
}
}
-// 清零最值并开始跟踪
-function clearAndStartTracking() {
- try {
- // 重置所有最值为0
- headPoseMaxValues.value = {
- rotationLeftMax: 0,
- rotationRightMax: 0,
- tiltLeftMax: 0,
- tiltRightMax: 0,
- pitchUpMax: 0,
- pitchDownMax: 0
+
+const cameraSubmit = async () => {
+ 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)
}
- } catch (error) {
- ElMessage.error('清零失败')
}
}
+// 加载相机参数信息
+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(() => {
+
+ 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('✅ 定时器已清理')
+ }
+
+ // // 停止录制
+ // if (isRecording.value === true) {
+ // stopRecord()
+ // console.log('✅ 录制已停止')
+ // }
+
+ // // 停止检测
+ // stopDetection()
+
+ // 断开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) {
// 检查是否在冷却期内
@@ -900,47 +2067,50 @@ function refreshClick(type) {
}
}
-// 处理压力传感器足部压力数据
-function handlePressureData(data) {
- try {
- if (data && data.foot_pressure) {
- const pressureData = data.foot_pressure
+function closeDiagnosticMessage(e){
+ isDiagnosticMessage.value = false
+ if(e== true){
+ try {
+ // 清理定时器
+ if (timerId.value) {
+ clearInterval(timerId.value)
+ timerId.value = null
+ console.log('✅ 定时器已清理')
+ }
+
+ // 停止录制
+ if (isRecording.value === true) {
+ stopRecord()
+ console.log('✅ 录制已停止')
+ }
- // 更新足部压力数据
- // 显示分区压力值
- if (pressureData.pressure_zones) {
- footPressure.value = pressureData.pressure_zones
- }
+ // 停止检测
+ stopDetection()
+ // 断开WebSocket连接
+ disconnectWebSocket()
+ // 移除页面关闭事件监听器
+ window.removeEventListener('beforeunload', handleBeforeUnload)
+ emit('endChange',true)
+ } catch (error) {
+ console.error('❌ Detection组件卸载时出错:', error)
+ emit('endChange',true)
+ }
- // 处理压力图片
- 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)
}
+
}
-function displayCameraFrameById(deviceId, base64Image) { // 显示相机帧
- if (base64Image && base64Image.length > 0) {
- const url = 'data:image/jpeg;base64,' + base64Image
- if (String(deviceId).toLowerCase() === 'camera2') {
- camera2ImgSrc.value = url
- } else {
- camera1ImgSrc.value = url
- // 旧变量保留(避免其它位置引用出错)
- rtspImgSrc.value = url
- }
- } else {
- console.warn('⚠️ 收到空的视频帧数据')
+
+
+function closecreatbox(e){
+ if(e == true){
+ loadPatientInfo()
}
+ isCloseCreat.value = false
+}
+
+function handleEditUserInfo(){
+ isCloseCreat.value = true
}
@@ -955,7 +2125,8 @@ function displayCameraFrameById(deviceId, base64Image) { // 显示相机帧
}
.detection-container-box .el-row {
- height: calc(100% - 125px);
+ margin: 0!important;
+ height: calc(100%);
}
.username-line {
@@ -1292,4 +2463,257 @@ function displayCameraFrameById(deviceId, base64Image) { // 显示相机帧
width: 100%;
height: calc(50% - 8px);
}
+.patientInfotop1{
+ font-weight: 700;
+ font-style: normal;
+ font-size: 22px;
+ color: #FFFFFF;
+ margin:0 20px;
+ margin-left:40px
+}
+.patientInfotop2{
+ font-weight: 400;
+ font-style: normal;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+.endbutton{
+ width: 100px;
+ height: 36px;
+ background-color: rgba(255, 102, 0, 1);
+ border:1px solid rgba(255, 102, 0, 1);
+ font-size: 16px;
+ color: #FFFFFF;
+ margin-left:20px
+}
+
+.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;
+ background: #1b1b1b;
+}
+.pop-up-camera-container{
+ width: 608px;
+ height:495px;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ margin: auto;
+ background: #1b1b1b;
+ border-radius: 10px;
+ box-shadow: 0px 0px 10px rgba(80, 80, 80, 1);
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(148, 148, 148, 1);
+}
+.pop-up-camera-header{
+ width: 100%;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: rgba(46, 52, 59, 1);
+ box-sizing: border-box;
+ padding: 0 20px;
+ font-family: 'Noto Sans SC';
+ font-weight: 700;
+ font-style: normal;
+ font-size: 16px;
+ color: #FFFFFF;
+ text-align: left;
+ border-radius:10px 10px 0 0;
+}
+
+.pop-up-tip-header{
+ width: 100%;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: rgba(46, 52, 59, 1);
+ box-sizing: border-box;
+ padding: 0 20px;
+ font-family: 'Noto Sans SC';
+ font-weight: 700;
+ font-style: normal;
+ font-size: 16px;
+ 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:#266fff;
+ border:1px solid #266fff;
+ width: 80px;
+ height: 40px;
+}
+.pop-up-tip-text{
+ width:100%;
+ font-weight: 400;
+ font-style: normal;
+ font-size: 16px;
+ color: #FFFFFF;
+ text-align: center;
+ padding:40px 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: rgba(40, 40, 40, 1);
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(54, 54, 54, 1);
+ 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: #787878;
+ 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-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;
+ margin: 0;
+}
+.pop-up-camera-display :deep(.el-radio.is-checked){
+ 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(38, 111, 255, 1);
+
+}
+.pop-up-camera-display :deep(.is-checked .el-radio__label){
+ color: rgb(38, 111, 255);
+}
+
+.pop-up-camera-display :deep(.el-radio__input.is-checked .el-radio__inner){
+ background: rgb(38, 111, 255);
+ border-color: rgb(38, 111, 255);
+}
+.pop-up-camera-display :deep(.el-radio__input.is-checked .el-radio__inner){
+ background: rgb(38, 111, 255);
+ border-color: rgb(38, 111, 255);
+}
+
+.pop-up-camera-display :deep(.el-radio__input.is-checked .el-radio__inner:after){
+ background-color: #282828;
+}
+.pop-up-camera-display :deep(.el-radio__inner){
+ background-color: #282828;
+ border-color: rgb(124, 124, 124);
+}
+
+
+
\ No newline at end of file
diff --git a/frontend/src/renderer/src/views/DiagnosticMessage.vue b/frontend/src/renderer/src/views/DiagnosticMessage.vue
new file mode 100644
index 00000000..c004f442
--- /dev/null
+++ b/frontend/src/renderer/src/views/DiagnosticMessage.vue
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/renderer/src/views/PatientCreate.vue b/frontend/src/renderer/src/views/PatientCreate.vue
index 554c6018..74d118ef 100644
--- a/frontend/src/renderer/src/views/PatientCreate.vue
+++ b/frontend/src/renderer/src/views/PatientCreate.vue
@@ -3,7 +3,7 @@