Files
hair/static/test_interface5.html
T
2026-06-25 20:25:49 +08:00

215 lines
11 KiB
HTML
Raw 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>接口5 — 发际线PNG生成 测试页</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: 1200px; margin: 0 auto; padding: 24px; }
h1 { font-size: 22px; margin-bottom: 6px; }
.subtitle { color: #888; font-size: 13px; margin-bottom: 24px; }
.card { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
.card-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; }
.card-body { padding: 18px; }
.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; }
.form-group { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.form-group label { font-size: 13px; font-weight: 600; color: #374151; }
.form-group select { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
.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; }
.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-layout { display: flex; gap: 24px; }
.col-main { flex: 1.5; min-width: 0; }
.col-side { flex: 1; min-width: 0; }
/* 方案网格 */
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; margin-top: 12px; }
.result-card { background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; cursor: pointer; transition: .15s; border: 3px solid transparent; }
.result-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,.12); }
.result-card.selected { border-color: #2563eb; }
.result-card .img-wrap { background: #222; min-height: 150px; max-height: 200px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
.result-card .img-wrap img { max-width: 100%; max-height: 200px; object-fit: contain; }
.result-card .info { padding: 10px 12px; font-size: 12px; display: flex; justify-content: space-between; align-items: center; }
.result-card .info .badge { background: #2563eb; color: #fff; padding: 1px 8px; border-radius: 10px; font-size: 11px; }
/* 预览叠加 */
.preview-stack { position: relative; display: inline-block; line-height: 0; background: #222; border-radius: 8px; overflow: hidden; }
.preview-stack img { display: block; max-width: 100%; max-height: 450px; }
.preview-stack .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
.preview-controls { display: flex; gap: 14px; align-items: center; font-size: 12px; color: #9ca3af; }
.preview-controls label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
/* 坐标高亮 */
.coord-box { background: #f9fafb; border-radius: 8px; padding: 12px 16px; margin-top: 10px; font-size: 14px; }
.coord-box .label { font-size: 11px; color: #9ca3af; margin-bottom: 4px; }
.coord-box .value { font-weight: 700; font-size: 18px; color: #111827; }
.json-panel { max-height: 550px; overflow: auto; }
.json-content { padding: 14px 16px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
.hidden { display: none !important; }
@media (max-width: 800px) { .results-layout { flex-direction: column; } }
</style>
</head>
<body>
<div class="container">
<h1>💈 接口5 — 发际线 PNG 生成 测试</h1>
<p class="subtitle">POST /api/v1/hairline/generate &nbsp;|&nbsp; 上传正面照 + 性别 → N 张发际线叠加图 + 最佳中心点坐标</p>
<div class="card">
<div class="card-body">
<div class="upload-row">
<div class="file-input"><input type="file" id="imageFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png"></div>
<div class="form-group">
<label>性别</label>
<select id="gender"><option value="female" selected>👩 Female5种)</option><option value="male">👨 Male4种)</option></select>
</div>
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
</div>
<div class="hint">JPG/PNG 正面照 &nbsp;|&nbsp; Female: 椭圆/花瓣/心形/直线/波浪 &nbsp;|&nbsp; Male: 椭圆/M形/直线/倒弧 &nbsp;|&nbsp; 返回中心点坐标(原图像素)</div>
<div id="statusBar" class="status hidden"></div>
</div>
</div>
<div class="results-layout hidden" id="resultsArea">
<div class="col-main">
<!-- 预览 -->
<div class="card">
<div class="card-header">
<span>📐 原图 + 发际线叠加</span>
<div class="preview-controls">
<label><input type="checkbox" id="showBase" checked onchange="syncOverlay()"> 底图</label>
<label><input type="checkbox" id="showOver" checked onchange="syncOverlay()"> 发际线</label>
</div>
</div>
<div class="card-body" style="text-align:center">
<div id="previewArea"><span style="color:#9ca3af">← 点击下方方案卡片</span></div>
<div class="coord-box">
<div class="label">📍 最佳发际线中心点(best_hairline_center_point)— 原图像素坐标</div>
<div class="value" id="centerPoint"></div>
</div>
</div>
</div>
<!-- 方案列表 -->
<div class="card">
<div class="card-header"><span>🎯 发际线方案</span><span style="font-weight:400;font-size:12px;color:#9ca3af">点击切换预览 &nbsp;|&nbsp; order=1 最佳</span></div>
<div class="card-body"><div class="results-grid" id="resultsGrid"></div></div>
</div>
</div>
<div class="col-side">
<div class="card">
<div class="card-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>
</div>
<script>
const API_BASE = window.location.origin;
let _images = [], _origUrl = '', _center = null;
function $(id) { return document.getElementById(id); }
function setStatus(t, type) { const b=$('statusBar'); b.textContent=t; b.className='status '+type; }
async function submitTest() {
const f = $('imageFile').files[0];
if (!f) { setStatus('请选择图片', 'error'); return; }
_origUrl = URL.createObjectURL(f);
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
setStatus('请求中...', 'info'); $('resultsArea').classList.add('hidden');
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value);
try {
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', body:fd });
const json = await r.json();
$('jsonContent').textContent = JSON.stringify(json, null, 2);
$('resultsArea').classList.remove('hidden');
if (json.code === 0) {
_images = json.data.hairline_images || [];
_center = json.data.best_hairline_center_point;
$('centerPoint').textContent = _center ? '(' + _center.x + ', ' + _center.y + ')' : '—';
setStatus('✅ ' + _images.length + ' 个方案', 'success');
renderGrid();
if (_images.length) selectCard(0);
} else {
setStatus('❌ code=' + json.code + ' ' + json.message, 'error');
}
} catch(e) { setStatus('❌ ' + e.message, 'error'); $('resultsArea').classList.remove('hidden'); }
finally { $('submitBtn').disabled = false; $('submitBtn').textContent = '🚀 提交'; }
}
function renderGrid() {
let h = '';
_images.forEach((img, i) => {
h += '<div class="result-card'+(i===0?' selected':'')+'" onclick="selectCard('+i+',this)">'+
'<div class="img-wrap"><img src="'+img.image_url+'" alt="#'+img.order+'" loading="lazy"></div>'+
'<div class="info"><span class="badge">#'+(img.order||'—')+'</span>'+(i===0?'<span style="font-size:11px;color:#059669">⭐ 最佳</span>':'')+'</div></div>';
});
$('resultsGrid').innerHTML = h;
}
function selectCard(idx, el) {
document.querySelectorAll('.result-card').forEach(c => c.classList.remove('selected'));
if (el) el.classList.add('selected');
$('previewArea').innerHTML =
'<div class="preview-stack">'+
'<img class="layer-base5" src="'+_origUrl+'" alt="原图">'+
'<img class="layer-over5" src="'+_images[idx].image_url+'" alt="#'+_images[idx].order+'">'+
'</div>';
syncOverlay();
}
function syncOverlay() {
const base = document.querySelector('.layer-base5');
const over = document.querySelector('.layer-over5');
if (base) base.style.display = $('showBase').checked ? '' : 'none';
if (over) over.style.display = $('showOver').checked ? '' : 'none';
}
function clearResults() {
_images=[]; _origUrl=''; _center=null;
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
$('imageFile').value=''; $('jsonContent').textContent=''; $('resultsGrid').innerHTML='';
$('previewArea').innerHTML='<span style="color:#9ca3af">← 点击下方方案卡片</span>';
$('centerPoint').textContent='—';
}
function copyJson() {
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
});
}
document.addEventListener('DOMContentLoaded', () => {
const dz = $('imageFile');
dz.addEventListener('dragover', e => { e.preventDefault(); dz.style.borderColor='#2563eb'; });
dz.addEventListener('dragleave', () => dz.style.borderColor='#ddd');
dz.addEventListener('drop', e => { e.preventDefault(); dz.style.borderColor='#ddd'; if(e.dataTransfer.files.length){ dz.files=e.dataTransfer.files; setStatus('已选择: '+e.dataTransfer.files[0].name, 'info'); } });
dz.addEventListener('change', () => { if(dz.files.length) setStatus('已选择: '+dz.files[0].name, 'info'); });
});
</script>
</body>
</html>