2.24

<template>
<div class="app-container">
<!-- 头部标题区域 -->
<header class="header">
<h1 class="main-title">眼疾智能识别系统</h1>
<p class="sub-title">上传眼底图像,快速识别可能的眼部疾病</p>
</header>

<!-- 主要内容区域 -->
<div class="content-container">
<!-- 左侧上传区域 -->
<div class="card upload-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-upload"></i>
上传眼底图像
</h2>
</div>
<div class="card-body">
<div
class="upload-area"
:class="{ 'drag-active': isDragging, 'has-image': leftImageSrc || rightImageSrc }"
@dragenter.prevent="handleDragEnter"
@dragleave.prevent="handleDragLeave"
@dragover.prevent
@drop.prevent="handleDrop"
>
<input
type="file"
ref="leftFileInput"
class="file-input"
accept="image/*"
@change="handleFileChange($event, 'left')"
/>
<input
type="file"
ref="rightFileInput"
class="file-input"
accept="image/*"
@change="handleFileChange($event, 'right')"
/>

<template v-if="!leftImageSrc && !rightImageSrc">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<p class="upload-text">点击或拖拽图片到此处上传</p>
<p class="upload-hint">支持 JPG、PNG 格式</p>
</template>

<div v-else class="preview-container">
<div v-if="leftImageSrc" class="image-preview">
<img :src="leftImageSrc" class="preview-image" alt="左眼图像预览" />
<p class="image-label">左眼图像</p>
</div>
<div v-if="rightImageSrc" class="image-preview">
<img :src="rightImageSrc" class="preview-image" alt="右眼图像预览" />
<p class="image-label">右眼图像</p>
</div>
<button
class="analyze-btn"
:class="{ 'loading': isAnalyzing }"
:disabled="isAnalyzing || !leftImageSrc || !rightImageSrc"
@click="identifyDisease"
>
<i class="fas" :class="isAnalyzing ? 'fa-spinner fa-spin' : 'fa-eye'"></i>
{{ isAnalyzing ? '正在分析...' : '开始识别' }}
</button>
</div>
</div>
</div>
</div>

<!-- 右侧结果区域 -->
<div class="card result-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-eye"></i>
识别结果
</h2>
</div>
<div class="card-body">
<div class="result-container">
<template v-if="!leftImageSrc || !rightImageSrc">
<p class="empty-text">请先上传左右眼的图像</p>
</template>

<template v-else-if="isAnalyzing">
<div class="analyzing">
<i class="fas fa-spinner fa-spin"></i>
<p>正在进行智能分析...</p>
</div>
</template>

<template v-else-if="result">
<div class="result-content">
<div class="result-icon">
<i class="fas fa-clipboard-check"></i>
</div>
<h3 class="result-title">检测结果</h3>
<ul class="result-list">
<li v-for="(prob, label) in result" :key="label">
<span class="label">{{ diseaseMap[label] }}</span>
<span class="prob">{{ prob.toFixed(2) }}%</span>
</li>
</ul>
<div class="result-info">
<p class="info-text">建议及时就医进行进一步检查</p>
</div>
<canvas ref="resultChart" width="400" height="200"></canvas>
<canvas ref="resultBarChart" width="400" height="200"></canvas>
</div>
</template>

<template v-else>
<p class="hint-text">点击"开始识别"按钮进行分析</p>
</template>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import axios from 'axios';
import { Chart, registerables } from 'chart.js';

Chart.register(...registerables);

export default {
name: 'EyeDiseaseDetection',
data() {
return {
leftImageSrc: null,
rightImageSrc: null,
leftImageFile: null,
rightImageFile: null,
result: null,
isAnalyzing: false,
isDragging: false,
diseaseMap: {
N: '正常',
D: '糖尿病',
G: '青光眼',
C: '白内障',
A: 'AMD',
H: '高血压',
M: '近视',
O: '其他疾病/异常'
}
};
},
methods: {
handleDragEnter() {
this.isDragging = true;
},
handleDragLeave() {
this.isDragging = false;
},
handleDrop(e) {
this.isDragging = false;
const files = e.dataTransfer.files;
Array.from(files).forEach(file => {
if (file.type.startsWith('image/')) {
this.handleFile(file);
}
});
},
handleFileChange(e, side) {
const file = e.target.files[0];
if (file) {
this.handleFile(file, side);
}
},
handleFile(file, side) {
const reader = new FileReader();
reader.onload = (e) => {
if (side === 'left') {
this.leftImageSrc = e.target.result;
this.leftImageFile = file;
} else {
this.rightImageSrc = e.target.result;
this.rightImageFile = file;
}
this.result = null;
};
reader.readAsDataURL(file);
},
async identifyDisease() {
if (!this.leftImageFile || !this.rightImageFile) {
alert('请上传左右眼的图像!');
return;
}

this.isAnalyzing = true;
const formData = new FormData();
formData.append('left_image', this.leftImageFile);
formData.append('right_image', this.rightImageFile);

try {
const response = await axios.post('http://localhost:5000/recognize', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
this.result = response.data;
this.result = Object.fromEntries(
Object.entries(this.result).map(([label, prob]) => [label, prob * 100]) // 转换为百分比
);
this.$nextTick(() => {
setTimeout(() => {
this.drawChart();
this.drawBarChart();
}, 100); // 延迟 100ms 确保 DOM 完全更新
});
} catch (error) {
console.error('识别失败:', error);
alert('识别失败,请稍后再试!');
} finally {
this.isAnalyzing = false;
}
},
drawChart() {
const ctx = this.$refs.resultChart.getContext('2d');
if (!ctx) {
console.error('Canvas context not found');
return;
}
new Chart(ctx, {
type: 'pie',
data: {
labels: Object.keys(this.result).map(label => this.diseaseMap[label]),
datasets: [{
label: '预测概率',
data: Object.values(this.result),
backgroundColor: [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#8E5EA2', '#34495E'
],
hoverOffset: 4
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '眼疾预测结果(饼状图)'
}
}
}
});
},
drawBarChart() {
const ctx = this.$refs.resultBarChart.getContext('2d');
if (!ctx) {
console.error('Canvas context not found');
return;
}
new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(this.result).map(label => this.diseaseMap[label]),
datasets: [{
label: '预测概率',
data: Object.values(this.result),
backgroundColor: [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#8E5EA2', '#34495E'
],
borderColor: '#34495E',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: '眼疾预测结果(柱状图)'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
}
};
</script>

<style scoped>
.app-container {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f0f2f5;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}

.header {
text-align: center;
margin-bottom: 20px;
}

.main-title {
font-size: 28px;
color: #333;
font-weight: bold;
}

.sub-title {
font-size: 18px;
color: #666;
}

.content-container {
display: flex;
gap: 20px;
}

.card {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 450px;
transition: transform 0.3s;
}

.card:hover {
transform: translateY(-5px);
}

.card-header {
padding: 15px;
background-color: #f9f9f9;
border-bottom: 1px solid #eaeaea;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}

.card-title {
font-size: 20px;
color: #333;
}

.card-body {
padding: 20px;
}

.upload-area {
border: 2px dashed #ccc;
border-radius: 12px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s, background-color 0.3s;
}

.upload-area.drag-active {
border-color: #4299e1;
background-color: #f0f8ff;
}

.upload-area.has-image {
border-color: #ccc;
background-color: #fff;
}

.upload-icon {
font-size: 48px;
color: #ccc;
}

.upload-text, .upload-hint {
font-size: 16px;
color: #666;
}

.file-input {
display: block;
}

.preview-container {
display: flex;
flex-direction: column;
align-items: center;
}

.preview-image {
max-width: 100%;
max-height: 300px;
border-radius: 12px;
margin-bottom: 1rem;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.image-label {
font-size: 16px;
color: #666;
}

.analyze-btn {
background: linear-gradient(90deg, #4299e1, #3182ce);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background 0.3s, transform 0.3s, box-shadow 0.3s;
}

.analyze-btn:hover {
background: linear-gradient(90deg, #3182ce, #4299e1);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}

.analyze-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

posted @ 2025-02-24 22:33  混沌武士丞  阅读(13)  评论(0)    收藏  举报