feat: Flask Web UI — 在线cDNA图像处理平台
- 上传图像 + 实时处理 + 6张结果可视化 - 实验室仪器风格深色主题 - 参数统计面板(T/pct/网格/斑点数) - 图片点击放大 + 单张/全部下载
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>cDNA Microarray Processing - Lab Console</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="noise-overlay"></div>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="logo-bracket">[</span>
|
||||
<h1 class="logo-text">cDNA<span class="logo-accent">//</span>PROCESS</h1>
|
||||
<span class="logo-bracket">]</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-label">SYSTEM READY</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="upload-section" id="uploadSection">
|
||||
<div class="upload-zone-wrapper">
|
||||
<div class="upload-zone" id="dropZone">
|
||||
<svg class="upload-svg" width="52" height="52" viewBox="0 0 52 52">
|
||||
<circle cx="26" cy="20" r="8" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||
<path d="M13 42c-5 0-8-1.5-8-6s3.5-9 8-9c0-6 6-10 13-10s13 4 13 10c4.5 0 8 4.5 8 9s-3.5 6-8 6H13z" stroke="currentColor" stroke-width="1.5" fill="none"/>
|
||||
<circle cx="22" cy="16" r="2.2" fill="currentColor"/>
|
||||
<circle cx="26" cy="11" r="2.8" fill="currentColor"/>
|
||||
</svg>
|
||||
<p class="upload-text">Drop image here<span class="upload-cursor">_</span></p>
|
||||
<p class="upload-sub">click to browse · TIFF / PNG / JPEG</p>
|
||||
<input type="file" id="fileInput" accept="image/*" hidden>
|
||||
</div>
|
||||
<div class="upload-preview" id="uploadPreview" style="display:none">
|
||||
<img id="previewImg" alt="Preview">
|
||||
<button class="btn-process" id="btnProcess">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16"><polygon points="3,1 16,8 3,15" fill="currentColor"/></svg>
|
||||
START ANALYSIS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="status-bar" id="statusBar" style="display:none">
|
||||
<div class="status-track">
|
||||
<div class="status-fill" id="statusFill"></div>
|
||||
</div>
|
||||
<span class="status-text" id="statusText">PROCESSING...</span>
|
||||
</div>
|
||||
|
||||
<div class="error-msg" id="errorMsg" style="display:none"></div>
|
||||
|
||||
<section class="stats-panel" id="statsPanel" style="display:none">
|
||||
<div class="stat-card"><span class="stat-label">THRESHOLD</span><span class="stat-value" id="statT">--</span></div>
|
||||
<div class="stat-card"><span class="stat-label">ADAPTIVE %</span><span class="stat-value" id="statPct">--</span></div>
|
||||
<div class="stat-card"><span class="stat-label">GRID</span><span class="stat-value" id="statGrid">--</span></div>
|
||||
<div class="stat-card spot"><span class="stat-label">SPOTS FOUND</span><span class="stat-value" id="statSpots">--</span></div>
|
||||
<div class="stat-card"><span class="stat-label">DIMENSIONS</span><span class="stat-value" id="statSize">--</span></div>
|
||||
</section>
|
||||
|
||||
<section class="gallery" id="gallery" style="display:none">
|
||||
<div class="gallery-header">
|
||||
<h2 class="gallery-title">RESULTS</h2>
|
||||
<button class="btn-dl-all" id="btnDownloadAll"><span>↓</span> DOWNLOAD ALL</button>
|
||||
</div>
|
||||
<div class="gallery-grid" id="galleryGrid"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="lightbox" id="lightbox">
|
||||
<span class="lightbox-close" id="lightboxClose">×</span>
|
||||
<img id="lightboxImg" src="" alt="Full size">
|
||||
<a id="lightboxDownload" class="lightbox-dl" download>↓</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var dz=document.getElementById('dropZone');
|
||||
var fi=document.getElementById('fileInput');
|
||||
var up=document.getElementById('uploadPreview');
|
||||
var pi=document.getElementById('previewImg');
|
||||
var bp=document.getElementById('btnProcess');
|
||||
var sb=document.getElementById('statusBar');
|
||||
var sf=document.getElementById('statusFill');
|
||||
var st=document.getElementById('statusText');
|
||||
var er=document.getElementById('errorMsg');
|
||||
var sp=document.getElementById('statsPanel');
|
||||
var ga=document.getElementById('gallery');
|
||||
var gg=document.getElementById('galleryGrid');
|
||||
var lb=document.getElementById('lightbox');
|
||||
var li=document.getElementById('lightboxImg');
|
||||
var lc=document.getElementById('lightboxClose');
|
||||
var ld=document.getElementById('lightboxDownload');
|
||||
var file=null;
|
||||
|
||||
dz.addEventListener('dragover',function(e){e.preventDefault();dz.classList.add('dragover')});
|
||||
dz.addEventListener('dragleave',function(){dz.classList.remove('dragover')});
|
||||
dz.addEventListener('drop',function(e){e.preventDefault();dz.classList.remove('dragover');if(e.dataTransfer.files.length)h(e.dataTransfer.files[0])});
|
||||
dz.addEventListener('click',function(){fi.click()});
|
||||
fi.addEventListener('change',function(e){if(e.target.files.length)h(e.target.files[0])});
|
||||
|
||||
function h(f){
|
||||
file=f;
|
||||
var r=new FileReader();
|
||||
r.onload=function(e){
|
||||
pi.src=e.target.result;
|
||||
up.style.display='flex';
|
||||
er.style.display='none';
|
||||
sp.style.display='none';
|
||||
ga.style.display='none';
|
||||
};
|
||||
r.readAsDataURL(f);
|
||||
}
|
||||
|
||||
bp.addEventListener('click',function(){
|
||||
if(!file) return;
|
||||
var fd=new FormData(); fd.append('file',file);
|
||||
sb.style.display='block'; sf.style.width='0%'; st.textContent='UPLOADING...'; er.style.display='none';
|
||||
var w=0; var iv=setInterval(function(){w=Math.min(w+2,85);sf.style.width=w+'%'},50);
|
||||
fetch('/process',{method:'POST',body:fd}).then(function(r){
|
||||
clearInterval(iv); sf.style.width='100%'; st.textContent='COMPLETE';
|
||||
if(!r.ok) throw new Error('Server error');
|
||||
return r.json();
|
||||
}).then(function(d){
|
||||
if(d.error) throw new Error(d.error);
|
||||
render(d);
|
||||
setTimeout(function(){sb.style.display='none'},1500);
|
||||
}).catch(function(e){
|
||||
clearInterval(iv); sb.style.display='none';
|
||||
er.textContent='ERROR: '+e.message; er.style.display='block';
|
||||
});
|
||||
});
|
||||
|
||||
function render(d){
|
||||
var s=d.stats;
|
||||
document.getElementById('statT').textContent=s.T_otsu;
|
||||
document.getElementById('statPct').textContent=s.pct+'%';
|
||||
document.getElementById('statGrid').textContent=s.lines_x+' x '+s.lines_y;
|
||||
document.getElementById('statSpots').textContent=s.spots;
|
||||
document.getElementById('statSize').textContent=s.width+' x '+s.height;
|
||||
sp.style.display='flex';
|
||||
var names=['grid_overlay','col_projection','row_projection','histogram','segmentation_raw','post_processed'];
|
||||
var labels=['GRID OVERLAY','COLUMN PROJECTION','ROW PROJECTION','HISTOGRAM + OTSU','SEGMENTATION','POST-PROCESSED'];
|
||||
gg.innerHTML='';
|
||||
names.forEach(function(n,i){
|
||||
var c=document.createElement('div'); c.className='gallery-card';
|
||||
c.innerHTML='<div class="gallery-card-inner"><img src="'+d.images[n]+'" onclick="zoom(\''+n+'.png\',\''+d.images[n]+'\')"></div><div class="gallery-card-label">'+labels[i]+'</div>';
|
||||
gg.appendChild(c);
|
||||
});
|
||||
ga.style.display='block';
|
||||
document.getElementById('btnDownloadAll').onclick=function(){
|
||||
names.forEach(function(n,i){
|
||||
setTimeout(function(){
|
||||
var a=document.createElement('a'); a.href=d.images[n]; a.download=n+'.png';
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
},i*200);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
window.zoom=function(name,src){
|
||||
lb.style.display='flex'; li.src=src; ld.href=src; ld.download=name;
|
||||
};
|
||||
|
||||
lc.addEventListener('click',function(){lb.style.display='none'});
|
||||
lb.addEventListener('click',function(e){if(e.target===lb) lb.style.display='none'});
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape') lb.style.display='none'});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user