533 lines
17 KiB
HTML
533 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CardioAI - 心血管疾病智能预测</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
color: white;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 10px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.1rem;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 8px;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select {
|
|
padding: 12px 15px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 10px;
|
|
font-size: 1rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
.form-group .hint {
|
|
font-size: 0.8rem;
|
|
color: #888;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.btn-container {
|
|
text-align: center;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.btn-predict {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 15px 50px;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
border-radius: 50px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.btn-predict:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
|
}
|
|
|
|
.btn-predict:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.btn-reset {
|
|
background: #f0f0f0;
|
|
color: #333;
|
|
border: none;
|
|
padding: 15px 40px;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
border-radius: 50px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
margin-left: 15px;
|
|
}
|
|
|
|
.btn-reset:hover {
|
|
background: #e0e0e0;
|
|
}
|
|
|
|
.result-container {
|
|
margin-top: 30px;
|
|
padding: 25px;
|
|
border-radius: 15px;
|
|
display: none;
|
|
animation: slideIn 0.5s ease;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.result-container.low-risk {
|
|
background: linear-gradient(135deg, #d4fc79 0%, #96e6a1 100%);
|
|
}
|
|
|
|
.result-container.medium-risk {
|
|
background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
|
|
}
|
|
|
|
.result-container.high-risk {
|
|
background: linear-gradient(135deg, #fab1a0 0%, #e17055 100%);
|
|
}
|
|
|
|
.result-container.very-high-risk {
|
|
background: linear-gradient(135deg, #ff7675 0%, #d63031 100%);
|
|
color: white;
|
|
}
|
|
|
|
.result-header {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.result-header h2 {
|
|
font-size: 1.8rem;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.result-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-box {
|
|
background: rgba(255,255,255,0.3);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.stat-box .label {
|
|
font-size: 0.9rem;
|
|
opacity: 0.8;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.stat-box .value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.probability-bar {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.probability-bar .bar-bg {
|
|
background: rgba(255,255,255,0.3);
|
|
height: 20px;
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.probability-bar .bar-fill {
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.2);
|
|
border-radius: 10px;
|
|
transition: width 1s ease;
|
|
}
|
|
|
|
.probability-bar .label {
|
|
text-align: center;
|
|
margin-top: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.loading {
|
|
display: none;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.spinner {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #667eea;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.error-message {
|
|
background: #ff7675;
|
|
color: white;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
margin-top: 20px;
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.header h1 {
|
|
font-size: 1.8rem;
|
|
}
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.result-stats {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>❤️ CardioAI</h1>
|
|
<p>心血管疾病智能预测系统</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<form id="predictionForm">
|
|
<div class="form-grid">
|
|
<!-- 年龄 -->
|
|
<div class="form-group">
|
|
<label for="age_years">年龄 (岁)</label>
|
|
<input type="number" id="age_years" name="age_years"
|
|
min="1" max="120" value="45" required>
|
|
<span class="hint">请输入您的年龄</span>
|
|
</div>
|
|
|
|
<!-- 性别 -->
|
|
<div class="form-group">
|
|
<label for="gender">性别</label>
|
|
<select id="gender" name="gender" required>
|
|
<option value="1">女性</option>
|
|
<option value="2">男性</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 身高 -->
|
|
<div class="form-group">
|
|
<label for="height">身高 (cm)</label>
|
|
<input type="number" id="height" name="height"
|
|
min="100" max="250" value="165" required>
|
|
<span class="hint">范围: 100-250 cm</span>
|
|
</div>
|
|
|
|
<!-- 体重 -->
|
|
<div class="form-group">
|
|
<label for="weight">体重 (kg)</label>
|
|
<input type="number" id="weight" name="weight"
|
|
min="30" max="200" value="65" required>
|
|
<span class="hint">范围: 30-200 kg</span>
|
|
</div>
|
|
|
|
<!-- 收缩压 -->
|
|
<div class="form-group">
|
|
<label for="ap_hi">收缩压 (mmHg)</label>
|
|
<input type="number" id="ap_hi" name="ap_hi"
|
|
min="90" max="250" value="120" required>
|
|
<span class="hint">范围: 90-250 mmHg</span>
|
|
</div>
|
|
|
|
<!-- 舒张压 -->
|
|
<div class="form-group">
|
|
<label for="ap_lo">舒张压 (mmHg)</label>
|
|
<input type="number" id="ap_lo" name="ap_lo"
|
|
min="60" max="150" value="80" required>
|
|
<span class="hint">范围: 60-150 mmHg</span>
|
|
</div>
|
|
|
|
<!-- 胆固醇 -->
|
|
<div class="form-group">
|
|
<label for="cholesterol">胆固醇水平</label>
|
|
<select id="cholesterol" name="cholesterol" required>
|
|
<option value="1">正常</option>
|
|
<option value="2">高于正常</option>
|
|
<option value="3">远高于正常</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 血糖 -->
|
|
<div class="form-group">
|
|
<label for="gluc">血糖水平</label>
|
|
<select id="gluc" name="gluc" required>
|
|
<option value="1">正常</option>
|
|
<option value="2">高于正常</option>
|
|
<option value="3">远高于正常</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 吸烟 -->
|
|
<div class="form-group">
|
|
<label for="smoke">是否吸烟</label>
|
|
<select id="smoke" name="smoke" required>
|
|
<option value="0">不吸烟</option>
|
|
<option value="1">吸烟</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 饮酒 -->
|
|
<div class="form-group">
|
|
<label for="alco">是否饮酒</label>
|
|
<select id="alco" name="alco" required>
|
|
<option value="0">不饮酒</option>
|
|
<option value="1">饮酒</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 运动 -->
|
|
<div class="form-group">
|
|
<label for="active">是否运动</label>
|
|
<select id="active" name="active" required>
|
|
<option value="0">不运动</option>
|
|
<option value="1">运动</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="btn-container">
|
|
<button type="submit" class="btn-predict" id="predictBtn">
|
|
🔍 开始预测
|
|
</button>
|
|
<button type="button" class="btn-reset" onclick="resetForm()">
|
|
🔄 重置
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- 加载动画 -->
|
|
<div class="loading" id="loading">
|
|
<div class="spinner"></div>
|
|
<p style="margin-top: 15px; color: #666;">正在分析您的健康数据...</p>
|
|
</div>
|
|
|
|
<!-- 错误消息 -->
|
|
<div class="error-message" id="errorMessage"></div>
|
|
|
|
<!-- 结果展示 -->
|
|
<div class="result-container" id="resultContainer">
|
|
<div class="result-header">
|
|
<h2 id="resultTitle">预测结果</h2>
|
|
<p id="riskLevel" style="font-size: 1.2rem; font-weight: 600;"></p>
|
|
</div>
|
|
|
|
<div class="result-stats">
|
|
<div class="stat-box">
|
|
<div class="label">预测结果</div>
|
|
<div class="value" id="predictionResult">-</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="label">疾病概率</div>
|
|
<div class="value" id="probabilityValue">-</div>
|
|
</div>
|
|
<div class="stat-box">
|
|
<div class="label">风险等级</div>
|
|
<div class="value" id="riskLevelValue">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="probability-bar">
|
|
<div class="bar-bg">
|
|
<div class="bar-fill" id="probabilityBar"></div>
|
|
</div>
|
|
<div class="label" id="probabilityLabel">疾病风险概率</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top: 20px; color: white; opacity: 0.8;">
|
|
<p>⚠️ 本预测结果仅供参考,不能替代专业医疗诊断</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const form = document.getElementById('predictionForm');
|
|
const loading = document.getElementById('loading');
|
|
const resultContainer = document.getElementById('resultContainer');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const predictBtn = document.getElementById('predictBtn');
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
// 获取表单数据
|
|
const formData = new FormData(form);
|
|
const data = Object.fromEntries(formData.entries());
|
|
|
|
// 验证血压数据
|
|
if (parseInt(data.ap_lo) >= parseInt(data.ap_hi)) {
|
|
showError('舒张压不能大于或等于收缩压,请检查输入!');
|
|
return;
|
|
}
|
|
|
|
// 显示加载动画
|
|
loading.style.display = 'block';
|
|
resultContainer.style.display = 'none';
|
|
errorMessage.style.display = 'none';
|
|
predictBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch('/predict_cardio', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.error) {
|
|
showError(result.error);
|
|
} else {
|
|
displayResult(result);
|
|
}
|
|
} catch (error) {
|
|
showError('网络错误,请确保服务已启动');
|
|
console.error('Error:', error);
|
|
} finally {
|
|
loading.style.display = 'none';
|
|
predictBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
function displayResult(result) {
|
|
const probability = result.probability * 100;
|
|
const prediction = result.prediction === 1 ? '有疾病风险' : '无疾病风险';
|
|
|
|
// 设置预测结果
|
|
document.getElementById('predictionResult').textContent = prediction;
|
|
document.getElementById('probabilityValue').textContent = probability.toFixed(1) + '%';
|
|
document.getElementById('riskLevelValue').textContent = result.risk_level;
|
|
document.getElementById('riskLevel').textContent = result.risk_level;
|
|
|
|
// 设置概率条
|
|
const bar = document.getElementById('probabilityBar');
|
|
setTimeout(() => {
|
|
bar.style.width = probability + '%';
|
|
}, 100);
|
|
|
|
// 根据风险等级设置样式
|
|
resultContainer.className = 'result-container';
|
|
if (result.risk_level === '低风险') {
|
|
resultContainer.classList.add('low-risk');
|
|
} else if (result.risk_level === '中等风险') {
|
|
resultContainer.classList.add('medium-risk');
|
|
} else if (result.risk_level === '高风险') {
|
|
resultContainer.classList.add('high-risk');
|
|
} else {
|
|
resultContainer.classList.add('very-high-risk');
|
|
}
|
|
|
|
// 显示结果
|
|
resultContainer.style.display = 'block';
|
|
}
|
|
|
|
function showError(message) {
|
|
errorMessage.textContent = message;
|
|
errorMessage.style.display = 'block';
|
|
}
|
|
|
|
function resetForm() {
|
|
form.reset();
|
|
resultContainer.style.display = 'none';
|
|
errorMessage.style.display = 'none';
|
|
document.getElementById('probabilityBar').style.width = '0%';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|