Files
hair/static/test_interface1.html
xslandCursor 08b31a3baa fix: 测试页同时兼容 base64 与 URL 图片字段
直连 worker 返回 *_base64、经网关则改写为 *_url;统一 resolveImgSrc 后接口1/2/3/5/6 测试页都能正确显示结果图。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 15:49:46 +08:00

288 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>接口1 — 四庭七眼测量 测试页</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 1100px; margin: 0 auto; padding: 24px; }
h1 { font-size: 22px; margin-bottom: 8px; }
.subtitle { color: #888; font-size: 13px; margin-bottom: 24px; }
/* 上传区 */
.upload-card { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 24px; }
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.file-input { flex: 1; min-width: 200px; }
.file-input input[type=file] { width: 100%; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: .2s; }
.btn-primary { background: #2563eb; color: #fff; }
.btn-primary:hover { background: #1d4ed8; }
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
.btn-sm { padding: 6px 14px; font-size: 13px; }
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
.btn-outline:hover { background: #f9fafb; }
.upload-hint { font-size: 12px; color: #9ca3af; margin-top: 8px; }
/* 状态提示 */
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; }
.status.info { background: #dbeafe; color: #1e40af; display: block; }
.status.error { background: #fee2e2; color: #991b1b; display: block; }
.status.success { background: #d1fae5; color: #065f46; display: block; }
/* 结果双栏 */
.results { display: flex; gap: 24px; }
.results > div { flex: 1; min-width: 0; background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; }
.results .panel-header { font-weight: 700; font-size: 14px; padding: 14px 18px; border-bottom: 1px solid #f0f0f0; background: #fafafa; display: flex; justify-content: space-between; align-items: center; }
/* JSON 面板 */
.json-panel { max-height: 600px; overflow: auto; }
.json-content { padding: 16px 18px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
/* 图片面板 — 叠加显示 */
.img-panel { display: flex; align-items: center; justify-content: center; min-height: 300px; background: #222; position: relative; }
.img-stack { position: relative; display: inline-block; line-height: 0; }
.img-stack img { display: block; max-width: 520px; max-height: 620px; object-fit: contain; }
.img-stack .layer-anno { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; }
.img-panel .placeholder { color: #9ca3af; font-size: 14px; }
/* 叠加控制 */
.anno-controls { display: flex; gap: 12px; align-items: center; font-size: 12px; color: #9ca3af; }
.anno-controls label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
.anno-controls input[type=range] { width: 60px; }
/* 指标卡片 */
.metrics { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; margin-top: 16px; }
.metric { background: #f9fafb; border-radius: 8px; padding: 12px 14px; }
.metric .label { font-size: 11px; color: #9ca3af; text-transform: uppercase; letter-spacing: .5px; }
.metric .value { font-size: 18px; font-weight: 700; color: #111827; margin-top: 2px; }
.metric .unit { font-size: 12px; color: #6b7280; font-weight: 400; }
@media (max-width: 768px) { .results { flex-direction: column; } }
.hidden { display: none !important; }
</style>
<script src="/static/img_downscale.js?v=2"></script>
</head>
<body>
<div class="container">
<h1>📐 四庭七眼测量 — 接口测试</h1>
<p class="subtitle">POST /api/v1/face/measure &nbsp;|&nbsp; 上传正面照 → 原图+标注叠加显示</p>
<!-- 上传区 -->
<div class="upload-card">
<div class="upload-row">
<div class="file-input">
<input type="file" id="imageFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png">
</div>
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交测试</button>
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除结果</button>
</div>
<div class="upload-hint">
支持 JPG / PNG 正面照 &nbsp;|&nbsp; 也可拖拽图片到文件选择框
</div>
<div id="statusBar" class="status hidden"></div>
</div>
<!-- 指标速览 -->
<div id="metricsBar" class="metrics hidden"></div>
<!-- 结果双栏 -->
<div class="results hidden" id="resultsArea">
<!-- 左:图片叠加 -->
<div>
<div class="panel-header">
<span>🖼️ 原图 + 标注叠加</span>
<div class="anno-controls">
<label><input type="checkbox" id="showBase" checked onchange="toggleBase()"> 底图</label>
<label><input type="checkbox" id="showAnno" checked onchange="toggleAnno()"> 标注层</label>
<label>透明度 <input type="range" id="annoOpacity" min="0" max="100" value="100" oninput="setOpacity()"></label>
</div>
</div>
<div class="img-panel" id="imgPanel">
<span class="placeholder">提交后显示叠加效果</span>
</div>
</div>
<!-- 右:JSON -->
<div>
<div class="panel-header">
<span>📋 JSON 响应</span>
<button class="btn btn-outline btn-sm" onclick="copyJson()">📋 复制</button>
</div>
<div class="json-panel"><pre class="json-content" id="jsonContent"></pre></div>
</div>
</div>
</div>
<script>
const API_BASE = window.location.origin;
let _origDataUrl = null;
function $(id) { return document.getElementById(id); }
function setStatus(text, type) {
const bar = $('statusBar');
bar.textContent = text;
bar.className = 'status ' + type;
}
async function submitTest() {
const fileInput = $('imageFile');
let file = fileInput.files[0];
if (!file) { setStatus('请先选择一张图片', 'error'); return; }
file = await window.downscaleImageFile(file);
const _reqStart = performance.now();
const btn = $('submitBtn');
btn.disabled = true;
btn.textContent = '⏳ 请求中...';
setStatus('正在请求 ' + API_BASE + '/api/v1/face/measure ...', 'info');
$('resultsArea').classList.add('hidden');
$('metricsBar').classList.add('hidden');
// 缓存原图 data URL(用于叠加显示)
const reader = new FileReader();
reader.onload = () => { _origDataUrl = reader.result; };
reader.readAsDataURL(file);
const form = new FormData();
form.append('image_file', file);
try {
const resp = await fetch(API_BASE + '/api/v1/face/measure', { method: 'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body: form });
const json = await resp.json();
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
$('jsonContent').textContent = JSON.stringify(json, null, 2);
$('resultsArea').classList.remove('hidden');
if (json.code === 0) {
setStatus('✅ 请求成功 (' + _elapsed + 's) — request_id: ' + json.request_id, 'success');
// 兼容本地直连 worker*_base64)与网关(*_url
const _d = json.data || {};
const annoUrl = resolveImgSrc(_d.annotated_image_url, _d.annotated_image_base64, 'image/png');
showOverlay(annoUrl);
renderMetrics(json.data);
$('metricsBar').classList.remove('hidden');
} else {
setStatus('❌ 业务错误 (' + _elapsed + 's) — code: ' + json.code + ' message: ' + json.message, 'error');
$('imgPanel').innerHTML = '<span class="placeholder">请求失败,无标注图</span>';
}
} catch (err) {
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
setStatus('❌ 网络错误 (' + _elapsed + 's): ' + err.message, 'error');
$('jsonContent').textContent = 'Error: ' + err.message;
$('resultsArea').classList.remove('hidden');
$('imgPanel').innerHTML = '<span class="placeholder">请求失败</span>';
} finally {
btn.disabled = false;
btn.textContent = '🚀 提交测试';
}
}
function showOverlay(annoUrl) {
if (!_origDataUrl) {
// 原图还没读完,等 200ms 再试
setTimeout(() => showOverlay(annoUrl), 200);
return;
}
if (!annoUrl) {
$('imgPanel').innerHTML = '<span class="placeholder">后端未返回标注图(annotated_image_base64 为空)</span>';
return;
}
const checked = $('showAnno').checked ? '' : 'display:none';
const opacity = ($('annoOpacity').value / 100).toFixed(2);
$('imgPanel').innerHTML =
'<div class="img-stack">' +
'<img class="layer-base" src="' + _origDataUrl + '" alt="原图">' +
'<img class="layer-anno" src="' + annoUrl + '" alt="标注层" ' +
'style="opacity:' + opacity + ';' + checked + '" ' +
'onerror="this.style.display=\'none\'">' +
'</div>';
}
function toggleBase() {
const base = document.querySelector('.layer-base');
const anno = document.querySelector('.layer-anno');
const show = $('showBase').checked;
if (base) base.style.display = show ? '' : 'none';
// 底图隐藏时,标注层改为普通图片撑开容器
if (anno) anno.style.position = show ? 'absolute' : 'static';
}
function toggleAnno() {
const el = document.querySelector('.layer-anno');
if (el) el.style.display = $('showAnno').checked ? '' : 'none';
}
function setOpacity() {
const el = document.querySelector('.layer-anno');
if (el) el.style.opacity = ($('annoOpacity').value / 100).toFixed(2);
}
function renderMetrics(data) {
const fc = data.four_courts || {};
const se = data.seven_eyes || {};
const items = [
{ label: '全脸总高', value: data.face_total_height_cm, unit: 'cm' },
{ label: '顶庭', value: fc.top_court_cm, unit: 'cm' },
{ label: '上庭', value: fc.upper_court_cm, unit: 'cm' },
{ label: '中庭', value: fc.middle_court_cm, unit: 'cm' },
{ label: '下庭', value: fc.lower_court_cm, unit: 'cm' },
{ label: '单眼宽度', value: se.eye_width_cm, unit: 'cm' },
{ label: '脸宽', value: se.face_width_cm, unit: 'cm' },
{ label: '两眼间距', value: se.inter_eye_distance_cm, unit: 'cm' },
];
let html = '';
items.forEach(m => {
html += '<div class="metric"><div class="label">' + m.label + '</div><div class="value">' + (m.value ?? '—') + ' <span class="unit">' + m.unit + '</span></div></div>';
});
$('metricsBar').innerHTML = html;
}
function clearResults() {
_origDataUrl = null;
$('resultsArea').classList.add('hidden');
$('metricsBar').classList.add('hidden');
$('statusBar').className = 'status hidden';
$('imageFile').value = '';
$('jsonContent').textContent = '';
$('imgPanel').innerHTML = '<span class="placeholder">提交后显示叠加效果</span>';
}
function copyJson() {
const text = $('jsonContent').textContent;
navigator.clipboard.writeText(text).then(() => {
const btn = event.target;
const orig = btn.textContent;
btn.textContent = '✅ 已复制';
setTimeout(() => btn.textContent = orig, 1500);
});
}
// 拖拽上传
document.addEventListener('DOMContentLoaded', () => {
const dropZone = $('imageFile');
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.style.borderColor = '#2563eb'; });
dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#ddd'; });
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.style.borderColor = '#ddd';
if (e.dataTransfer.files.length) {
dropZone.files = e.dataTransfer.files;
setStatus('已选择: ' + e.dataTransfer.files[0].name, 'info');
}
});
dropZone.addEventListener('change', () => {
if (dropZone.files.length) setStatus('已选择: ' + dropZone.files[0].name, 'info');
});
});
</script>
</body>
</html>