Files
cDNA-image-processing/web/templates/index.html
T
Serendipity b07e7a1182 feat: Flask Web UI — 在线cDNA图像处理平台
- 上传图像 + 实时处理 + 6张结果可视化
- 实验室仪器风格深色主题
- 参数统计面板(T/pct/网格/斑点数)
- 图片点击放大 + 单张/全部下载
2026-05-08 11:26:02 +08:00

177 lines
7.3 KiB
HTML

<!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 &middot; 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>&#8595;</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">&times;</span>
<img id="lightboxImg" src="" alt="Full size">
<a id="lightboxDownload" class="lightbox-dl" download>&#8595;</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>