<template>
<div class="foot-temperature-analysis">
<h2 class="page-title">足部温度测量分析</h2>
<!-- 顶部视图区域 -->
<div class="views-container">
<!-- 主要视图区域 -->
<div class="main-view">
<div class="view-title">实时视图</div>
<div class="image-placeholder">
<svg
width="100%"
height="100%"
viewBox="0 0 400 300"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="100%" height="100%" fill="#f0f0f0" />
<text
x="50%"
y="50%"
font-size="20"
text-anchor="middle"
dominant-baseline="middle"
>
实时视图
</text>
</svg>
</div>
</div>
<!-- 辅助视图区域 -->
<div class="auxiliary-views">
<div class="aux-view">
<div class="view-title">左脚</div>
<div class="image-placeholder">
<svg
width="100%"
height="100%"
viewBox="0 0 150 200"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="100%" height="100%" fill="#f0f0f0" />
<text
x="50%"
y="50%"
font-size="14"
text-anchor="middle"
dominant-baseline="middle"
>
左脚
</text>
</svg>
</div>
</div>
<div class="aux-view">
<div class="view-title">右脚</div>
<div class="image-placeholder">
<svg
width="100%"
height="100%"
viewBox="0 0 150 200"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="100%" height="100%" fill="#f0f0f0" />
<text
x="50%"
y="50%"
font-size="14"
text-anchor="middle"
dominant-baseline="middle"
>
右脚
</text>
</svg>
</div>
</div>
</div>
</div>
<!-- 评估结果区域 -->
<div class="results-section">
<h3>评估结果</h3>
<div class="results-content">
<p>温度分布正常,未发现明显异常区域。</p>
<p>整体温度偏差在正常范围内,建议继续保持足部护理习惯。</p>
</div>
</div>
<!-- 数据展示区域 -->
<div class="data-section">
<div class="data-tables">
<!-- 左脚温度数据表格 -->
<div class="data-table">
<h4>左脚温度数据</h4>
<el-table :data="leftFootData" border stripe>
<el-table-column prop="region" label="区域" width="60" />
<el-table-column prop="maxTemp" label="最高温度" />
<el-table-column prop="minTemp" label="最低温度" />
<el-table-column prop="avgTemp" label="平均温度" />
</el-table>
</div>
<!-- 右脚温度数据表格 -->
<div class="data-table">
<h4>右脚温度数据</h4>
<el-table :data="rightFootData" border stripe>
<el-table-column prop="region" label="区域" width="60" />
<el-table-column prop="maxTemp" label="最高温度" />
<el-table-column prop="minTemp" label="最低温度" />
<el-table-column prop="avgTemp" label="平均温度" />
</el-table>
</div>
</div>
</div>
<!-- 操作按钮区域 -->
<div class="action-buttons">
<el-dropdown @command="handleCameraSelect">
<el-button type="primary" class="action-button" size="small">
<el-icon><VideoPlay /></el-icon>
启动
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="flir">Flir红外相机</el-dropdown-item>
<el-dropdown-item command="gaode">高德红外相机</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
type="primary"
class="action-button"
size="small"
@click="handleUpload"
>
<el-icon><Upload /></el-icon>
上传
</el-button>
<el-button
type="primary"
class="action-button"
size="small"
@click="handleAnalyze"
>
<el-icon><DataAnalysis /></el-icon>
分析
</el-button>
</div>
<!-- 足部选择弹窗 -->
<el-dialog
v-model="footSelectDialogVisible"
title="选择足部"
width="30%"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<div class="foot-select-content">
<el-button
type="primary"
@click="handleFootSelect(0)"
class="foot-select-btn"
>
左脚
</el-button>
<el-button
type="primary"
@click="handleFootSelect(1)"
class="foot-select-btn"
>
右脚
</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, onMounted, onUnmounted } from 'vue'
import {
VideoPlay,
Upload,
DataAnalysis,
ArrowDown
} from '@element-plus/icons-vue'
import service from '@renderer/utils/request'
import { ElMessage } from 'element-plus'
const props = defineProps({
patientId: {
type: String,
required: true
}
})
// 评估结果内容
const resultsContent = ref<string[]>([])
// 左脚温度数据
const leftFootData = ref<
Array<{ region: string; maxTemp: string; minTemp: string; avgTemp: string }>
>([])
// 右脚温度数据
const rightFootData = ref<
Array<{ region: string; maxTemp: string; minTemp: string; avgTemp: string }>
>([])
// 足部选择弹窗的显示状态
const footSelectDialogVisible = ref(false)
// 高德相机相关状态
const gaodeConnected = ref(false)
const gaodeRecording = ref(false)
const gaodePreviewUrl = ref('')
const gaodeWebSocket = ref<WebSocket | null>(null)
// 获取数据的方法
const fetchData = async () => {
try {
const response = await service.get(
`/patient/footResultGet/${props.patientId}`
)
const result = response.data
console.log('获取到的数据:', result)
if (result.errorMsg) {
console.error('获取数据失败:', result.errorMsg)
return
}
// 处理数据
if (result.data && result.data.length > 0) {
const firstItem = result.data[0]
// 处理左脚数据
leftFootData.value = firstItem.list?.length
? firstItem.list
.filter((item) => item.footSide === 'left')
.map((item) => ({
region: item.region?.toString() || '',
maxTemp: `${item.maxTemperature || 0}°C`,
minTemp: `${item.minTemperature || 0}°C`,
avgTemp: `${item.avgTemperature || 0}°C`
}))
: []
// 处理右脚数据
rightFootData.value = firstItem.list?.length
? firstItem.list
.filter((item) => item.footSide === 'right')
.map((item) => ({
region: item.region?.toString() || '',
maxTemp: `${item.maxTemperature || 0}°C`,
minTemp: `${item.minTemperature || 0}°C`,
avgTemp: `${item.avgTemperature || 0}°C`
}))
: []
// 更新评估结果
resultsContent.value = [
firstItem.evaluationResult || '暂无评估结果',
firstItem.recommendation || '暂无建议'
]
// 更新图片
if (firstItem.leftThermalImagePath) {
document
.querySelector('.left-foot-image')
?.setAttribute('src', firstItem.leftThermalImagePath)
}
if (firstItem.rightThermalImagePath) {
document
.querySelector('.right-foot-image')
?.setAttribute('src', firstItem.rightThermalImagePath)
}
}
console.log('数据获取成功')
} catch (error) {
console.error('获取数据时出错:', error)
}
}
// 组件挂载时自动获取数据
onMounted(() => {
fetchData()
})
const handleUpload = async () => {
try {
// 创建文件选择input元素
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
input.accept = 'image/*,.csv'
input.onchange = async (e) => {
if (!e.target || !(e.target as HTMLInputElement).files) {
console.error('文件选择事件无效')
return
}
const files = Array.from((e.target as HTMLInputElement).files || [])
// 分类文件
const leftTempPhoto = files.find(
(f: unknown) =>
(f as File).name.includes('left') &&
((f as File).type.includes('image') ||
(f as File).name.endsWith('.jpg') ||
(f as File).name.endsWith('.png'))
)
const rightTempPhoto = files.find(
(f: unknown) =>
(f as File).name.includes('right') &&
((f as File).type.includes('image') ||
(f as File).name.endsWith('.jpg') ||
(f as File).name.endsWith('.png'))
)
const leftCsv = files.find(
(f: unknown) =>
(f as File).name.includes('left') && (f as File).name.endsWith('.csv')
)
const rightCsv = files.find(
(f: unknown) =>
(f as File).name.includes('right') &&
(f as File).name.endsWith('.csv')
)
if (!leftTempPhoto || !rightTempPhoto || !leftCsv || !rightCsv) {
console.error('请选择完整的4个文件(左右脚热图和CSV)')
return
}
const formData = new FormData()
formData.append('patientId', props.patientId)
if (leftTempPhoto instanceof File)
formData.append('leftTempPhoto', leftTempPhoto)
if (rightTempPhoto instanceof File)
formData.append('rightTempPhoto', rightTempPhoto)
if (leftCsv instanceof File) formData.append('leftCsv', leftCsv)
if (rightCsv instanceof File) formData.append('rightCsv', rightCsv)
const response = await service.post('/patient/footCsvSave', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.errorMsg) {
console.error('上传失败:', response.data.errorMsg)
} else {
console.log('上传成功')
}
}
input.click()
} catch (error) {
console.error('上传过程中出错:', error)
}
}
const handleAnalyze = () => {
console.log('分析数据')
// 实现分析功能
}
// 处理Flir相机拍摄
const handleFlirCapture = async (side: number) => {
try {
const response = await service.post(`/flir/capture/${props.patientId}`, {
params: {
side
}
})
const result = response.data
if (result.errorMsg) {
console.error('Flir相机拍摄失败:', result.errorMsg)
return
}
// 处理返回的数据
if (result.data) {
const { temperatureDataPath, thermalImagePath } = result.data
// 更新对应的图片
if (thermalImagePath) {
const imageElement =
side === 0
? document.querySelector('.left-foot-image')
: document.querySelector('.right-foot-image')
if (imageElement) {
imageElement.setAttribute('src', thermalImagePath)
}
}
// 如果有温度数据,可以更新温度数据表格
if (temperatureDataPath) {
// 这里可以添加获取温度数据的逻辑
console.log('温度数据路径:', temperatureDataPath)
}
}
} catch (error) {
console.error('Flir相机拍摄出错:', error)
}
}
// 连接高德相机
const connectGaodeCamera = async () => {
try {
const response = await service.get('/gaode/connect')
if (response.data.errorMsg) {
console.error('连接高德相机失败:', response.data.errorMsg)
return false
}
gaodeConnected.value = true
return true
} catch (error) {
console.error('连接高德相机出错:', error)
return false
}
}
// 断开高德相机连接
const disconnectGaodeCamera = async () => {
try {
const response = await service.get('/gaode/disconnect')
if (response.data.errorMsg) {
console.error('断开高德相机连接失败:', response.data.errorMsg)
return false
}
gaodeConnected.value = false
return true
} catch (error) {
console.error('断开高德相机连接出错:', error)
return false
}
}
// 获取预览流地址
const getGaodePreview = async () => {
try {
const response = await service.get('/gaode/preview')
if (response.data.errorMsg) {
console.error('获取预览流失败:', response.data.errorMsg)
return
}
gaodePreviewUrl.value = response.data.data
} catch (error) {
console.error('获取预览流出错:', error)
}
}
// 开始录像
const startGaodeRecording = async () => {
try {
const response = await service.get('/gaode/record/start')
if (response.data.errorMsg) {
console.error('开始录像失败:', response.data.errorMsg)
return false
}
gaodeRecording.value = true
return true
} catch (error) {
console.error('开始录像出错:', error)
return false
}
}
// 停止录像
const stopGaodeRecording = async () => {
try {
const response = await service.get('/gaode/record/stop')
if (response.data.errorMsg) {
console.error('停止录像失败:', response.data.errorMsg)
return false
}
gaodeRecording.value = false
return true
} catch (error) {
console.error('停止录像出错:', error)
return false
}
}
// 高德相机拍照
const handleGaodeCapture = async (side: number) => {
try {
const response = await service.post('/gaode/capture', {
patientId: props.patientId,
side,
path: 'lib\\gaode'
})
const result = response.data
if (result.errorMsg) {
console.error('高德相机拍照失败:', result.errorMsg)
return
}
// 处理返回的数据
if (result.data) {
const { temperatureDataPath, thermalImagePath } = result.data
// 更新对应的图片
if (thermalImagePath) {
const imageElement =
side === 0
? document.querySelector('.left-foot-image')
: document.querySelector('.right-foot-image')
if (imageElement) {
imageElement.setAttribute('src', thermalImagePath)
}
}
// 如果有温度数据,可以更新温度数据表格
if (temperatureDataPath) {
console.log('温度数据路径:', temperatureDataPath)
}
}
} catch (error) {
console.error('高德相机拍照出错:', error)
}
}
// 处理高德相机选择
const handleGaodeCamera = async () => {
if (!gaodeConnected.value) {
const connected = await connectGaodeCamera()
if (!connected) {
ElMessage.error('连接高德相机失败')
return
}
await getGaodePreview()
ElMessage.success('高德相机连接成功,请选择要拍摄的足部')
}
// 显示足部选择弹窗
footSelectDialogVisible.value = true
}
// 处理足部选择
const handleFootSelect = (side: number) => {
footSelectDialogVisible.value = false
if (gaodeConnected.value) {
handleGaodeCapture(side)
} else {
handleFlirCapture(side)
}
}
// 处理相机选择
const handleCameraSelect = (cameraType: string) => {
console.log('选择的相机类型:', cameraType)
if (cameraType === 'flir') {
// 显示足部选择弹窗
footSelectDialogVisible.value = true
} else if (cameraType === 'gaode') {
handleGaodeCamera()
}
}
// 组件卸载时断开连接
onUnmounted(async () => {
if (gaodeConnected.value) {
if (gaodeRecording.value) {
await stopGaodeRecording()
}
await disconnectGaodeCamera()
}
})
</script>
<style scoped>
.foot-temperature-analysis {
padding: 20px;
height: calc(100vh - 120px);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.page-title {
margin: 0 0 20px 0;
color: #333;
font-size: 22px;
}
.views-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
height: 300px;
}
.main-view {
flex: 3;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.auxiliary-views {
flex: 2;
display: flex;
gap: 20px;
}
.aux-view {
flex: 1;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.view-title {
padding: 10px;
background-color: #8cb8f9;
font-weight: bold;
text-align: center;
border-bottom: 1px solid #e6e6e6;
}
.image-placeholder {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 200px;
padding: 10px;
}
.results-section {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 15px;
}
.results-section h3 {
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 10px;
}
.results-content {
color: #333;
line-height: 1.6;
}
.data-section {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 15px;
}
.data-tables {
display: flex;
gap: 30px;
}
.data-table {
flex: 1;
}
.data-table h4 {
margin-top: 0;
margin-bottom: 15px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
padding: 15px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.action-button {
min-width: 100px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.views-container {
flex-direction: column;
height: auto;
}
.main-view {
height: 250px;
}
.auxiliary-views {
height: 200px;
}
.data-tables {
flex-direction: column;
}
.data-table {
margin-bottom: 20px;
}
.action-buttons {
flex-wrap: wrap;
}
}
@media (max-width: 768px) {
.auxiliary-views {
flex-direction: column;
height: auto;
}
.aux-view {
height: 200px;
}
}
.foot-select-content {
display: flex;
justify-content: space-around;
padding: 20px 0;
}
.foot-select-btn {
width: 120px;
height: 120px;
font-size: 18px;
}
</style>
后端为:package com.g60health.dfrasdesktop.controller;
import com.g60health.dfrascommon.result.ResponseResult;
import com.g60health.dfrasdesktop.dto.CaptureDTO;
import com.g60health.dfrasdesktop.service.FlirCameraService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/flir")
@Tag(name = "Flir红外相机控制")
public class FlirCameraController {
@Resource
private FlirCameraService flirCameraService;
@PostMapping("/capture/{patientId}")
@Operation(summary = "Flir设备进行拍照(红外图片和CSV)")
//拍摄的图片和csv数据集均存放在D:/image
public ResponseResult<CaptureDTO> capture(
@Parameter(description = "患者id") @PathVariable Integer patientId,
@Parameter(description = "拍摄的足部方位(0: 左足, 1: 右足)") @Schema(allowableValues = {"0", "1"})
@RequestParam(defaultValue = "0") Integer side) {
CaptureDTO result = flirCameraService.flirCapture(patientId, side);
return ResponseResult.success(result);
}
}
接口对接是否有问题