Files
labweb/public/html/status.html
2025-12-16 11:39:15 +08:00

360 lines
12 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>Predict Result</title>
<link rel="icon" href="/public/assets/images/logo.png" type="image/png">
<link rel="stylesheet" href="/public/css/Tools.css">
<style>
/* 统一的基础样式 */
* {
box-sizing: border-box;
}
.status-wrapper {
max-width: 1000px;
margin: 30px auto;
background: #fff;
border-radius: 10px;
border: 1px solid #e6e6e6;
}
.status-header {
padding: 16px 20px;
border-bottom: 1px solid #e6e6e6;
background: linear-gradient(135deg, #f8f9ff 0%, #eef3ff 100%);
}
.status-body {
padding: 20px;
}
.meta {
display: flex;
gap: 16px;
flex-wrap: wrap;
color: #555;
margin-bottom: 16px;
}
.meta-item {
padding: 6px 10px;
background: #f7f7f7;
border-radius: 6px;
border: 1px solid #eee;
}
.estimate {
color: #2E5A61;
font-weight: 600;
}
.result-area {
margin-top: 20px;
}
.actions {
margin-top: 16px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid #e6e6e6;
background: #f8f9fa;
color: #333;
cursor: pointer;
text-decoration: none;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #4A90A4, #6BC8D1);
color: #fff;
border: none;
}
/* 统一的自适应设计 */
/* 大屏幕 (1400px+) */
@media (min-width: 1400px) {
.status-wrapper {
max-width: 1200px;
}
}
/* 中等屏幕 (992px - 1399px) */
@media (max-width: 1399px) {
.status-wrapper {
max-width: 95%;
}
}
/* 平板 (768px - 991px) */
@media (max-width: 991px) {
.status-wrapper {
max-width: 98%;
margin: 20px auto;
}
.status-header {
padding: 14px 18px;
}
.status-body {
padding: 18px;
}
}
/* 移动设备 (最大768px) */
@media (max-width: 768px) {
.status-wrapper {
max-width: 100%;
margin: 15px 10px;
border-radius: 8px;
}
.status-header {
padding: 12px 15px;
}
.status-body {
padding: 15px;
}
.meta {
gap: 10px;
margin-bottom: 12px;
}
.meta-item {
padding: 5px 8px;
font-size: 13px;
}
.actions {
flex-direction: column;
gap: 8px;
}
.btn {
width: 100%;
text-align: center;
padding: 10px;
}
}
/* 小屏幕移动设备 (最大480px) */
@media (max-width: 480px) {
.status-wrapper {
margin: 10px 5px;
}
.status-header {
padding: 10px 12px;
}
.status-body {
padding: 12px;
}
.meta {
gap: 8px;
}
.meta-item {
padding: 4px 6px;
font-size: 12px;
}
.btn {
font-size: 14px;
padding: 8px;
}
}
/* 超小屏幕 (最大360px) */
@media (max-width: 360px) {
.status-header {
padding: 8px 10px;
}
.status-body {
padding: 10px;
}
.meta-item {
font-size: 11px;
}
.btn {
font-size: 13px;
}
}
</style>
<script>
const API_BASE_URL = 'http://localhost:4000';
let pollTimer = null;
function qs(name){ const url = new URL(location.href); return url.searchParams.get(name) || ''; }
function setText(id, text){ const el = document.getElementById(id); if(el) el.textContent = text; }
async function fetchStatus(jobId){
const res = await fetch(`${API_BASE_URL}/api/status/${encodeURIComponent(jobId)}`);
if (!res.ok) throw new Error('Status interface error');
return res.json();
}
function updateEstimate(status){
const { eta_seconds = null, progress = 0, status: st = 'queued', queue_position } = status || {};
if (eta_seconds != null) {
const mins = Math.max(0, Math.round(eta_seconds / 60));
setText('eta', `${mins} minutes`);
} else {
setText('eta', 'Calculating...');
}
const detail = st === 'queued' ? `Waiting (queue position: ${queue_position ?? 'unknown'})` : (st === 'analyzing' ? 'Analyzing' : st);
setText('status-text', detail);
const fill = document.getElementById('progress-fill');
if (fill) fill.style.width = `${Math.max(0, Math.min(100, progress))}%`;
}
function renderResult(data){
// 与 Tools.html 的展示结构保持一致的简化填充
setText('result-species', data.species_name || '-');
setText('result-taxonomy', data.taxonomy || '-');
setText('result-ph', data.ph || '-');
setText('result-temperature', data.temperature || '-');
setText('result-respiratory', data.respiratory_type || '-');
setText('result-medium', data.culture_medium || '-');
setText('result-growth', data.max_growth_rate || '-');
document.getElementById('download-btn').style.display = 'inline-block';
}
async function startPolling(jobId){
// 首次拉取
try {
const first = await fetchStatus(jobId);
updateEstimate(first);
if (first.status === 'completed') {
renderResult(first);
return;
}
} catch (e) {
console.error(e);
}
pollTimer = setInterval(async () => {
try {
const data = await fetchStatus(jobId);
updateEstimate(data);
if (data.status === 'completed') {
clearInterval(pollTimer);
pollTimer = null;
renderResult(data);
} else if (data.status === 'failed') {
clearInterval(pollTimer);
pollTimer = null;
alert('Mission failed:' + (data.error || 'Unknown error'));
}
} catch (err) {
console.error('Polling failed', err);
}
}, 2000);
}
async function downloadResult(){
const jobId = qs('job');
if (!jobId) return alert('No job number');
const res = await fetch(`${API_BASE_URL}/api/download/${encodeURIComponent(jobId)}`);
if (!res.ok) { try { const e = await res.json(); alert(e.error || 'Download failed'); } catch { alert('Download failed'); } return; }
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `analysis_result_${jobId}.csv`;
document.body.appendChild(a); a.click();
URL.revokeObjectURL(url); document.body.removeChild(a);
}
document.addEventListener('DOMContentLoaded', () => {
const jobId = qs('job');
const type = qs('type');
if (jobId) setText('job-id', jobId);
if (type) setText('job-type', type);
if (!jobId) { alert('Missing job number'); return; }
startPolling(jobId);
});
</script>
</head>
<body>
<div class="status-wrapper">
<div class="status-header">
<strong>Job status and results</strong>
</div>
<div class="status-body">
<div class="meta">
<span class="meta-item">Job Number:<span id="job-id">-</span></span>
<span class="meta-item">Tool Type:<span id="job-type">-</span></span>
<span class="meta-item estimate">Estimated time:<span id="eta">Calculating...</span></span>
<span class="meta-item">state:<span id="status-text">In preparation</span></span>
</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<div class="progress-text" id="progress-text">Real-time progress</div>
<div class="progress-details" id="progress-details">The system is estimating the time and queue</div>
</div>
<div class="result-area">
<div class="result-content">
<ul>
<li>
<p class="field-name">species name</p>
<p class="field-value" id="result-species">-</p>
</li>
<li>
<p class="field-name">taxonomy</p>
<p class="field-value" id="result-taxonomy">-</p>
</li>
<li>
<p class="field-name">pH</p>
<p class="field-value" id="result-ph">-</p>
</li>
<li>
<p class="field-name">temperature</p>
<p class="field-value" id="result-temperature">-</p>
</li>
<li>
<p class="field-name">respiratory type</p>
<p class="field-value" id="result-respiratory">-</p>
</li>
<li>
<p class="field-name">culture medium</p>
<p class="field-value" id="result-medium">-</p>
</li>
<li>
<p class="field-name">Max growth rate</p>
<p class="field-value" id="result-growth">-</p>
</li>
</ul>
</div>
</div>
<div class="actions">
<a class="btn" href="/Tools.html">return Tools</a>
<a class="btn btn-primary" href="javascript:void(0)" onclick="downloadResult()">Download</a>
</div>
</div>
</div>
</body>
</html>