You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
615 lines
22 KiB
615 lines
22 KiB
<!DOCTYPE html> |
|
<html lang="ru"> |
|
<head> |
|
<link rel="icon" type="image/x-icon" href="static/favicon.ico"> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Анализ DICOM файлов</title> |
|
<style> |
|
* { |
|
box-sizing: border-box; |
|
margin: 0; |
|
padding: 0; |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
} |
|
|
|
body { |
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
|
min-height: 100vh; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
padding: 20px; |
|
} |
|
|
|
.container { |
|
width: 100%; |
|
max-width: 800px; |
|
background: white; |
|
border-radius: 15px; |
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); |
|
overflow: hidden; |
|
} |
|
|
|
.header { |
|
background: white; |
|
color: #333; |
|
padding: 25px; |
|
text-align: center; |
|
border-bottom: 2px solid #eee; |
|
} |
|
|
|
.logo-container { |
|
background: white; |
|
border-radius: 12px; |
|
padding: 20px; |
|
margin: 0 auto 20px; |
|
display: inline-block; |
|
} |
|
|
|
.logo { |
|
max-width: 200px; |
|
height: auto; |
|
display: block; |
|
} |
|
|
|
.header h1 { |
|
font-size: 24px; |
|
margin-bottom: 10px; |
|
color: #2c3e50; |
|
} |
|
|
|
.header p { |
|
color: #7f8c8d; |
|
} |
|
|
|
.content { |
|
padding: 25px; |
|
} |
|
|
|
.form-group { |
|
margin-bottom: 20px; |
|
} |
|
|
|
label { |
|
display: block; |
|
margin-bottom: 8px; |
|
font-weight: 600; |
|
color: #2c3e50; |
|
} |
|
|
|
select { |
|
width: 100%; |
|
padding: 12px 15px; |
|
border: 2px solid #ddd; |
|
border-radius: 8px; |
|
font-size: 16px; |
|
transition: border-color 0.3s; |
|
} |
|
|
|
select:focus { |
|
border-color: #5ac6c8; |
|
outline: none; |
|
} |
|
|
|
.demo-files { |
|
background-color: #f8f9fa; |
|
border-radius: 8px; |
|
padding: 15px; |
|
margin-top: 15px; |
|
} |
|
|
|
.demo-files h3 { |
|
margin-bottom: 12px; |
|
color: #2c3e50; |
|
font-size: 16px; |
|
} |
|
|
|
.file-list { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); |
|
gap: 10px; |
|
} |
|
|
|
.demo-file { |
|
background: white; |
|
border: 1px solid #e0e0e0; |
|
border-radius: 8px; |
|
padding: 12px; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
text-align: center; |
|
} |
|
|
|
.demo-file:hover { |
|
border-color: #5ac6c8; |
|
box-shadow: 0 5px 15px rgba(90, 198, 200, 0.2); |
|
transform: translateY(-2px); |
|
} |
|
|
|
.demo-file.selected { |
|
border-color: #5ac6c8; |
|
background-color: #e8fcfb; |
|
} |
|
|
|
.drop-zone { |
|
border: 2px dashed #5ac6c8; |
|
border-radius: 8px; |
|
padding: 40px 20px; |
|
text-align: center; |
|
margin-bottom: 20px; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
background-color: #f8f9fa; |
|
} |
|
|
|
.drop-zone.highlight { |
|
background-color: #e8fcfb; |
|
border-color: #c182b9; |
|
} |
|
|
|
.drop-zone p { |
|
margin-bottom: 15px; |
|
color: #7f8c8d; |
|
} |
|
|
|
.drop-zone .icon { |
|
font-size: 48px; |
|
color: #5ac6c8; |
|
margin-bottom: 15px; |
|
} |
|
|
|
.btn { |
|
display: block; |
|
width: 100%; |
|
padding: 15px; |
|
background: linear-gradient(135deg, #c182b9 0%, #5ac6c8 100%); |
|
color: white; |
|
border: none; |
|
border-radius: 8px; |
|
font-size: 16px; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: opacity 0.3s; |
|
} |
|
|
|
.btn:hover { |
|
opacity: 0.9; |
|
} |
|
|
|
.btn:disabled { |
|
background: #bdc3c7; |
|
cursor: not-allowed; |
|
} |
|
|
|
.file-info { |
|
margin-top: 20px; |
|
padding: 15px; |
|
background-color: #f8f9fa; |
|
border-radius: 8px; |
|
border-left: 4px solid #5ac6c8; |
|
} |
|
|
|
.file-info h3 { |
|
margin-bottom: 10px; |
|
color: #2c3e50; |
|
} |
|
|
|
.file-details { |
|
display: flex; |
|
justify-content: space-between; |
|
margin-top: 10px; |
|
padding-top: 10px; |
|
border-top: 1px solid #e0e0e0; |
|
} |
|
|
|
.notification { |
|
padding: 15px; |
|
margin: 20px 0; |
|
border-radius: 8px; |
|
text-align: center; |
|
display: none; |
|
} |
|
|
|
.notification.success { |
|
background-color: #d4edda; |
|
color: #155724; |
|
border: 1px solid #c3e6cb; |
|
display: block !important; |
|
} |
|
|
|
.notification.error { |
|
background-color: #f8d7da; |
|
color: #721c24; |
|
border: 1px solid #f5c6cb; |
|
display: block !important; |
|
} |
|
|
|
.upload-section { |
|
margin-top: 30px; |
|
padding-top: 20px; |
|
border-top: 1px solid #eee; |
|
} |
|
|
|
.section-title { |
|
font-size: 18px; |
|
color: #2c3e50; |
|
margin-bottom: 15px; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.section-title::before { |
|
content: "•"; |
|
margin-right: 10px; |
|
color: #5ac6c8; |
|
} |
|
|
|
.loading { |
|
display: inline-block; |
|
width: 20px; |
|
height: 20px; |
|
border: 3px solid rgba(255,255,255,.3); |
|
border-radius: 50%; |
|
border-top-color: #fff; |
|
animation: spin 1s ease-in-out infinite; |
|
} |
|
|
|
@keyframes spin { |
|
to { transform: rotate(360deg); } |
|
} |
|
|
|
@media (max-width: 600px) { |
|
.file-list { |
|
grid-template-columns: 1fr; |
|
} |
|
|
|
.header h1 { |
|
font-size: 20px; |
|
} |
|
|
|
.logo { |
|
max-width: 150px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<div class="logo-container"> |
|
<img src="static/logo.png" alt="Логотип" class="logo"> |
|
</div> |
|
<h1>Выявление патологий в лучевых исследованиях</h1> |
|
<p>Загрузите DICOM файл или выберите демонстрационный пример</p> |
|
</div> |
|
|
|
<div class="content"> |
|
{% if error %} |
|
<div class="notification error"> |
|
{{ error }} |
|
</div> |
|
{% endif %} |
|
|
|
<form id="upload-form" action="/upload-study" method="post" enctype="multipart/form-data"> |
|
<div class="form-group"> |
|
<label for="pathology">Выберите искомую патологию:</label> |
|
<select id="pathology" name="pathology"> |
|
<option value="sinus">Синусит</option> |
|
<option value="wrist">Перелом костей лучезапястного сустава</option> |
|
<option value="shoulder">Перелом костей плечевого сустава</option> |
|
</select> |
|
|
|
<div class="demo-files"> |
|
<h3>Демонстрационные файлы:</h3> |
|
<div class="file-list" id="demo-files-list"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="upload-section"> |
|
<div class="section-title">Или загрузите свой файл</div> |
|
|
|
<div class="drop-zone" id="drop-zone"> |
|
<div class="icon">📁</div> |
|
<p>Перетащите DICOM файл сюда или нажмите для выбора</p> |
|
<small>Поддерживаются только файлы в формате .dcm</small> |
|
</div> |
|
|
|
<input type="file" id="file-input" name="file" accept=".dcm" style="display: none;"> |
|
|
|
<input type="hidden" id="demo-filename" name="demo_filename" value=""> |
|
|
|
<div id="notification" class="notification"></div> |
|
|
|
<button type="submit" id="analyze-btn" class="btn" disabled> |
|
<span id="btn-text">Запустить анализ</span> |
|
<span id="btn-loading" class="loading" style="display: none;"></span> |
|
</button> |
|
|
|
<div id="file-info" class="file-info" style="display: none;"> |
|
<h3>Информация о файле</h3> |
|
<div id="file-details" class="file-details"> |
|
</div> |
|
</div> |
|
</div> |
|
</form> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
const form = document.getElementById('upload-form'); |
|
const pathologySelect = document.getElementById('pathology'); |
|
const demoFilesList = document.getElementById('demo-files-list'); |
|
const dropZone = document.getElementById('drop-zone'); |
|
const fileInput = document.getElementById('file-input'); |
|
const demoFilenameInput = document.getElementById('demo-filename'); |
|
const analyzeBtn = document.getElementById('analyze-btn'); |
|
const btnText = document.getElementById('btn-text'); |
|
const btnLoading = document.getElementById('btn-loading'); |
|
const fileInfo = document.getElementById('file-info'); |
|
const fileDetails = document.getElementById('file-details'); |
|
const notification = document.getElementById('notification'); |
|
|
|
let selectedFile = null; |
|
let selectedDemoFile = null; |
|
|
|
const demoFiles = { |
|
sinus: [ |
|
{ name: "Рентген пазух носа (норма)", filename: "sinus_normal.dcm" }, |
|
{ name: "Рентген пазух носа (синусит)", filename: "sinusitis.dcm" } |
|
], |
|
wrist: [ |
|
{ name: "Рентген запястья (норма)", filename: "wrist_normal.dcm" }, |
|
{ name: "Рентген запястья (перелом)", filename: "wrist_fracture.dcm" } |
|
], |
|
shoulder: [ |
|
{ name: "Рентген плеча (норма)", filename: "shoulder_normal.dcm" }, |
|
{ name: "Рентген плеча (перелом)", filename: "shoulder_fracture.dcm" } |
|
] |
|
}; |
|
|
|
function showDemoFiles() { |
|
const pathology = pathologySelect.value; |
|
const files = demoFiles[pathology]; |
|
|
|
demoFilesList.innerHTML = ''; |
|
|
|
files.forEach(file => { |
|
const fileElement = document.createElement('div'); |
|
fileElement.className = 'demo-file'; |
|
fileElement.textContent = file.name; |
|
fileElement.dataset.filename = file.filename; |
|
|
|
fileElement.addEventListener('click', function() { |
|
document.querySelectorAll('.demo-file').forEach(f => { |
|
f.classList.remove('selected'); |
|
}); |
|
|
|
this.classList.add('selected'); |
|
|
|
fileInput.value = ''; |
|
selectedFile = null; |
|
|
|
selectedDemoFile = { |
|
name: file.name, |
|
filename: file.filename, |
|
pathology: pathology |
|
}; |
|
|
|
demoFilenameInput.value = file.filename; |
|
|
|
showFileInfo(file.name, 'Демонстрационный файл'); |
|
|
|
analyzeBtn.disabled = false; |
|
|
|
showNotification('Демонстрационный файл выбран. Нажмите "Запустить анализ" для обработки.', 'success'); |
|
}); |
|
|
|
demoFilesList.appendChild(fileElement); |
|
}); |
|
} |
|
|
|
function showFileInfo(name, info) { |
|
fileInfo.style.display = 'block'; |
|
fileDetails.innerHTML = ` |
|
<div> |
|
<strong>Имя файла:</strong> ${name} |
|
</div> |
|
<div> |
|
<strong>Тип:</strong> ${info} |
|
</div> |
|
`; |
|
} |
|
|
|
function showNotification(message, type) { |
|
notification.textContent = message; |
|
notification.className = `notification ${type}`; |
|
notification.style.display = 'block'; |
|
} |
|
|
|
function hideNotification() { |
|
notification.style.display = 'none'; |
|
notification.className = 'notification'; |
|
notification.textContent = ''; |
|
} |
|
|
|
function restoreDemoFileState() { |
|
const demoFilename = demoFilenameInput.value; |
|
if (demoFilename) { |
|
const pathology = pathologySelect.value; |
|
const files = demoFiles[pathology]; |
|
const file = files.find(f => f.filename === demoFilename); |
|
|
|
if (file) { |
|
selectedDemoFile = { |
|
name: file.name, |
|
filename: file.filename, |
|
pathology: pathology |
|
}; |
|
|
|
document.querySelectorAll('.demo-file').forEach(f => { |
|
f.classList.remove('selected'); |
|
if (f.dataset.filename === demoFilename) { |
|
f.classList.add('selected'); |
|
} |
|
}); |
|
|
|
showFileInfo(file.name, 'Демонстрационный файл'); |
|
|
|
analyzeBtn.disabled = false; |
|
|
|
showNotification('Демонстрационный файл выбран. Нажмите "Запустить анализ" для обработки.', 'success'); |
|
} |
|
} |
|
} |
|
|
|
function formatFileSize(bytes) { |
|
if (!bytes || bytes === 0) return 'Неизвестно'; |
|
const k = 1024; |
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
} |
|
|
|
pathologySelect.addEventListener('change', function() { |
|
showDemoFiles(); |
|
|
|
selectedFile = null; |
|
selectedDemoFile = null; |
|
demoFilenameInput.value = ''; |
|
fileInfo.style.display = 'none'; |
|
analyzeBtn.disabled = true; |
|
|
|
fileInput.value = ''; |
|
|
|
document.querySelectorAll('.demo-file').forEach(f => { |
|
f.classList.remove('selected'); |
|
}); |
|
|
|
hideNotification(); |
|
}); |
|
|
|
dropZone.addEventListener('click', () => { |
|
fileInput.click(); |
|
}); |
|
|
|
dropZone.addEventListener('dragover', (e) => { |
|
e.preventDefault(); |
|
dropZone.classList.add('highlight'); |
|
}); |
|
|
|
dropZone.addEventListener('dragleave', () => { |
|
dropZone.classList.remove('highlight'); |
|
}); |
|
|
|
dropZone.addEventListener('drop', (e) => { |
|
e.preventDefault(); |
|
dropZone.classList.remove('highlight'); |
|
|
|
if (e.dataTransfer.files.length) { |
|
handleFileSelect(e.dataTransfer.files[0]); |
|
|
|
document.querySelectorAll('.demo-file').forEach(f => { |
|
f.classList.remove('selected'); |
|
}); |
|
selectedDemoFile = null; |
|
demoFilenameInput.value = ''; |
|
} |
|
}); |
|
|
|
fileInput.addEventListener('change', () => { |
|
if (fileInput.files.length) { |
|
handleFileSelect(fileInput.files[0]); |
|
|
|
document.querySelectorAll('.demo-file').forEach(f => { |
|
f.classList.remove('selected'); |
|
}); |
|
selectedDemoFile = null; |
|
demoFilenameInput.value = ''; |
|
} |
|
}); |
|
|
|
function handleFileSelect(file) { |
|
if (file.name.endsWith('.dcm')) { |
|
selectedFile = file; |
|
selectedDemoFile = null; |
|
demoFilenameInput.value = ''; |
|
showFileInfo(file.name, formatFileSize(file.size)); |
|
analyzeBtn.disabled = false; |
|
showNotification('Файл успешно загружен. Нажмите "Запустить анализ" для обработки.', 'success'); |
|
} else { |
|
showNotification('Пожалуйста, выберите файл в формате DICOM (.dcm).', 'error'); |
|
} |
|
} |
|
|
|
form.addEventListener('submit', function(e) { |
|
e.preventDefault(); |
|
|
|
btnText.style.display = 'none'; |
|
btnLoading.style.display = 'inline-block'; |
|
analyzeBtn.disabled = true; |
|
hideNotification(); |
|
|
|
const formData = new FormData(); |
|
formData.append('pathology', pathologySelect.value); |
|
|
|
if (selectedDemoFile) { |
|
formData.append('demo_filename', selectedDemoFile.filename); |
|
} else if (selectedFile) { |
|
formData.append('file', selectedFile); |
|
} else { |
|
showNotification('Пожалуйста, выберите файл или демонстрационный пример.', 'error'); |
|
btnText.style.display = 'inline'; |
|
btnLoading.style.display = 'none'; |
|
analyzeBtn.disabled = false; |
|
return; |
|
} |
|
|
|
fetch('/upload-study', { |
|
method: 'POST', |
|
body: formData, |
|
headers: { |
|
'X-Requested-With': 'XMLHttpRequest' |
|
} |
|
}) |
|
.then(response => { |
|
if (response.ok) { |
|
return response.json(); |
|
} else { |
|
return response.json().then(err => { throw err; }); |
|
} |
|
}) |
|
.then(data => { |
|
if (data.redirect) { |
|
window.location.href = data.redirect; |
|
} |
|
}) |
|
.catch(error => { |
|
console.error('Ошибка:', error); |
|
const message = error.error || 'Произошла неизвестная ошибка.'; |
|
showNotification(message, 'error'); |
|
btnText.style.display = 'inline'; |
|
btnLoading.style.display = 'none'; |
|
analyzeBtn.disabled = false; |
|
}); |
|
}); |
|
|
|
window.addEventListener('pageshow', function(event) { |
|
btnText.style.display = 'inline'; |
|
btnLoading.style.display = 'none'; |
|
analyzeBtn.disabled = false; |
|
|
|
if (notification.textContent === 'Файл отправляется на анализ...') { |
|
hideNotification(); |
|
} |
|
|
|
restoreDemoFileState(); |
|
}); |
|
|
|
showDemoFiles(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |