- 将踢脚线和勾缝线选项由复选框改为下拉选择,支持多种颜色选择 - 更新BuildCombinedFloorPrompt函数,支持传入颜色参数并显示对应中文名称 - 修改前端元素ID及相关逻辑,保持命名一致性和代码规范 - 新增material_source请求参数,支持图片来源选择 - 调整日志中跳过轮询请求,减少无用日志输出 - 修复部分函数命名和调用错误,提升代码健壮性
467 lines
23 KiB
HTML
467 lines
23 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>FloorVisualizer Demo</title>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",Arial,sans-serif;background:#1a1a2e;color:#eee;min-height:100vh}
|
||
header{background:#16213e;padding:20px;text-align:center;border-bottom:2px solid #e94560}
|
||
header h1{font-size:24px;color:#e94560}
|
||
header p{font-size:14px;color:#aaa;margin-top:6px}
|
||
.container{max-width:1280px;margin:0 auto;padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:20px}
|
||
.panel{background:#16213e;border-radius:12px;padding:22px;margin-bottom:16px}
|
||
.panel h2{font-size:18px;margin-bottom:16px;color:#e94560}
|
||
.upload-zone{border:2px dashed #444;border-radius:10px;padding:32px;min-height:220px;display:flex;align-items:center;justify-content:center;text-align:center;cursor:pointer;transition:.2s}
|
||
.upload-zone:hover,.upload-zone.dragover{border-color:#e94560;background:rgba(233,68,96,.06)}
|
||
.upload-zone img{max-width:100%;max-height:320px;border-radius:8px}
|
||
.upload-zone span{color:#888;font-size:14px;line-height:1.7}
|
||
.options{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
|
||
.option{padding:12px;border:2px solid #333;border-radius:8px;cursor:pointer;text-align:center;transition:.2s;user-select:none;min-height:64px;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||
.option:hover{border-color:#e94560}
|
||
.option.selected{border-color:#e94560;background:rgba(233,68,96,.15)}
|
||
.option.disabled{opacity:.45;cursor:not-allowed}
|
||
.option .name{font-size:14px;font-weight:700;line-height:1.35}
|
||
.option .meta{font-size:12px;color:#aaa;margin-top:4px;line-height:1.35}
|
||
.floor-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;max-height:430px;overflow:auto;padding-right:4px}
|
||
.floor-card{border:2px solid #333;border-radius:8px;padding:10px;cursor:pointer;transition:.2s;background:#11172d}
|
||
.floor-card:hover{border-color:#e94560}
|
||
.floor-card.selected{border-color:#e94560;background:rgba(233,68,96,.14)}
|
||
.floor-card img{width:100%;height:70px;object-fit:cover;border-radius:6px;background:#222;margin-bottom:8px}
|
||
.floor-card .title{font-size:13px;font-weight:700;line-height:1.3}
|
||
.floor-card .desc{font-size:11px;color:#aaa;margin-top:4px;line-height:1.35}
|
||
.toolbar{display:flex;gap:10px;margin-bottom:10px}
|
||
.toolbar input{flex:1;background:#0f3460;border:1px solid #34456c;border-radius:8px;color:#eee;padding:10px}
|
||
.toggle-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||
.toggle{border:2px dashed #444;border-radius:8px;padding:12px;cursor:pointer;background:#11172d}
|
||
.toggle.selected{border-color:#e94560;background:rgba(233,68,96,.14)}
|
||
.toggle.disabled{opacity:.45;cursor:not-allowed}
|
||
.toggle input{margin-right:8px}
|
||
.btn{width:100%;padding:14px;margin-top:4px;border:0;border-radius:8px;background:#e94560;color:#fff;font-size:16px;font-weight:700;cursor:pointer}
|
||
.btn:hover{background:#ff647b}
|
||
.btn:disabled{background:#555;cursor:not-allowed}
|
||
.spinner{display:none;width:18px;height:18px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:8px}
|
||
.loading .spinner{display:inline-block}
|
||
.loading .btn-text{display:none}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.result-panel{display:flex;flex-direction:column;min-height:720px}
|
||
.placeholder{flex:1;min-height:360px;border:1px dashed #333;border-radius:8px;display:flex;align-items:center;justify-content:center;text-align:center;color:#777;line-height:1.8}
|
||
.result-img{display:none;width:100%;border-radius:8px;margin-top:12px}
|
||
.compare{position:relative;width:100%;overflow:hidden;border-radius:8px;margin-top:12px;display:none;user-select:none;background:#111;cursor:ew-resize}
|
||
.compare img.base{display:block;width:100%;height:auto}
|
||
.clip{position:absolute;left:0;top:0;width:50%;height:100%;overflow:hidden}
|
||
.clip img{display:block;height:100%;object-fit:cover;object-position:left top;max-width:none}
|
||
.handle{position:absolute;top:0;bottom:0;left:50%;transform:translateX(-50%);width:4px;background:#e94560;z-index:3;pointer-events:none}
|
||
.knob{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:42px;height:42px;border-radius:50%;background:#e94560;display:grid;place-items:center;box-shadow:0 2px 16px rgba(0,0,0,.5)}
|
||
.labels{position:absolute;left:0;right:0;bottom:14px;display:flex;justify-content:space-between;padding:0 16px;pointer-events:none;z-index:2}
|
||
.labels span{background:rgba(0,0,0,.76);color:#fff;padding:4px 12px;border-radius:4px;font-size:12px;font-weight:700}
|
||
.status-box{display:none;background:#0f3460;border-radius:8px;padding:16px;margin-top:12px}
|
||
.step-label{font-size:13px;color:#e94560;font-weight:700;margin-bottom:6px}
|
||
.step-msg{font-size:14px;color:#eee;line-height:1.5;white-space:pre-wrap}
|
||
.progress{height:6px;background:#333;border-radius:3px;margin-top:12px;overflow:hidden}
|
||
.fill{height:100%;width:0;background:linear-gradient(90deg,#e94560,#f0a500);transition:width .35s}
|
||
.error-box{display:none;margin-top:12px;border:1px solid #90323d;background:#2b1220;color:#ffb4bd;border-radius:8px;padding:14px}
|
||
.error-box h3{font-size:14px;margin-bottom:8px;color:#ff6b81}
|
||
.error-box pre{white-space:pre-wrap;word-break:break-word;font-family:Consolas,monospace;font-size:12px;line-height:1.5;color:#ffd6dc}
|
||
.small{font-size:12px;color:#888;margin-top:8px}
|
||
@media(max-width:900px){.container{grid-template-columns:1fr;padding:14px}.floor-grid{grid-template-columns:1fr}.options,.toggle-row{grid-template-columns:1fr}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>FloorVisualizer AI 换地板 Demo</h1>
|
||
<p>上传房间照片,选择真实产品、铺设方式和可选效果,生成写实换地板预览</p>
|
||
</header>
|
||
|
||
<div class="container">
|
||
<div>
|
||
<div class="panel">
|
||
<h2>第一步:上传房间照片</h2>
|
||
<div class="upload-zone" id="uploadZone">
|
||
<input id="fileInput" type="file" accept="image/*" hidden>
|
||
<img id="previewImg" alt="" style="display:none">
|
||
<span id="uploadHint">点击或拖拽上传图片<br>建议使用能清楚看到地面的室内照片</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>房间类型</h2>
|
||
<div class="options" id="roomOptions"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>地板类型</h2>
|
||
<div class="options" id="typeOptions">
|
||
<div class="option selected" data-type="wood"><div class="name">木地板</div><div class="meta">Hardwood</div></div>
|
||
<div class="option" data-type="tile"><div class="name">瓷砖</div><div class="meta">Tile / Stone</div></div>
|
||
<div class="option" data-type="vinyl"><div class="name">乙烯基地板</div><div class="meta">Luxury Vinyl / SPC</div></div>
|
||
<div class="option" data-type="special"><div class="name">特殊测试</div><div class="meta">7款 · 含AI描述</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>铺设方式</h2>
|
||
<div class="options" id="patternOptions"></div>
|
||
</div>
|
||
|
||
<div class="panel" id="sizePanel" style="display:none">
|
||
<h2>尺寸</h2>
|
||
<div class="options" id="sizeOptions"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2 id="floorTitle">选择地板款式</h2>
|
||
<div class="toolbar"><input id="floorSearch" placeholder="搜索款式、颜色、材质、SKU"></div>
|
||
<div class="floor-grid" id="floorOptions"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>材质参考方式</h2>
|
||
<div class="options" id="materialSourceOptions">
|
||
<div class="option selected" data-source="image"><div class="name">仅图片</div><div class="meta">参考图</div></div>
|
||
<div class="option" data-source="json"><div class="name">仅JSON</div><div class="meta">材质描述</div></div>
|
||
<div class="option" data-source="both"><div class="name">图片+JSON</div><div class="meta">两者都用</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>可选效果</h2>
|
||
<div class="toggle-row">
|
||
<div class="toggle" style="display:flex;flex-direction:column;align-items:flex-start;gap:6px;cursor:default">
|
||
<span style="font-weight:700">勾缝线</span>
|
||
<select id="groutSelect" style="width:100%;background:#0f3460;color:#eee;border:1px solid #34456c;border-radius:6px;padding:6px">
|
||
<option value="">不使用</option>
|
||
<option value="white">白色</option>
|
||
<option value="tile">与瓷砖同色</option>
|
||
<option value="lightgray">浅灰色</option>
|
||
<option value="gray">灰色</option>
|
||
<option value="beige">米色</option>
|
||
<option value="darkgray">深灰色</option>
|
||
</select>
|
||
</div>
|
||
<div class="toggle" style="display:flex;flex-direction:column;align-items:flex-start;gap:6px;cursor:default">
|
||
<span style="font-weight:700">踢脚线</span>
|
||
<select id="baseboardSelect" style="width:100%;background:#0f3460;color:#eee;border:1px solid #34456c;border-radius:6px;padding:6px">
|
||
<option value="">不使用</option>
|
||
<option value="white">白色</option>
|
||
<option value="floor">与地板同色</option>
|
||
<option value="cream">米白</option>
|
||
<option value="lightgray">浅灰</option>
|
||
<option value="darkgray">深灰</option>
|
||
<option value="wood">木色(深棕)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn" id="generateBtn" disabled><span class="spinner"></span><span class="btn-text">生成预览图</span></button>
|
||
<div class="status-box" id="statusBox">
|
||
<div class="step-label" id="stepLabel"></div>
|
||
<div class="step-msg" id="stepMsg"></div>
|
||
<div class="live-timer" id="liveTimer" style="display:none;color:#f0a500;font-size:13px;margin-top:8px;font-weight:700"></div>
|
||
<div class="progress"><div class="fill" id="progressFill"></div></div>
|
||
</div>
|
||
<div class="error-box" id="errorBox">
|
||
<h3>生成失败原因</h3>
|
||
<pre id="errorDetail"></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel result-panel">
|
||
<h2>生成结果</h2>
|
||
<div class="placeholder" id="resultPlaceholder">上传图片并选择地板选项后<br>点击生成预览图查看效果</div>
|
||
<img class="result-img" id="resultImg" alt="生成结果">
|
||
<div class="compare" id="compare">
|
||
<img class="base" id="afterImg" alt="生成图">
|
||
<div class="clip" id="clip"><img id="beforeImg" alt="原图"></div>
|
||
<div class="handle" id="handle"><div class="knob">↔</div></div>
|
||
<div class="labels"><span>原图</span><span>生成图</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const $=id=>document.getElementById(id);
|
||
const state={options:null,type:'wood',file:null,originalUrl:null,room:null,pattern:null,floor:null,timer:null,lastStatus:null};
|
||
|
||
function showError(title, detail){
|
||
$('errorBox').style.display='block';
|
||
$('errorDetail').textContent=(title||'生成失败')+(detail?'\n\n'+detail:'');
|
||
$('resultPlaceholder').style.display='flex';
|
||
$('resultPlaceholder').innerHTML='生成失败';
|
||
}
|
||
function clearError(){ $('errorBox').style.display='none'; $('errorDetail').textContent=''; }
|
||
function setProgress(step,msg,width){
|
||
$('statusBox').style.display='block';
|
||
$('stepLabel').textContent=step;
|
||
$('stepMsg').textContent=msg||'';
|
||
$('progressFill').style.width=width;
|
||
}
|
||
function apiData(json){ return json && json.data ? json.data : json; }
|
||
function floorList(){
|
||
if(!state.options)return[];
|
||
if(state.type==='tile')return state.options.tile_floors||[];
|
||
if(state.type==='vinyl')return state.options.vinyl_floors||[];
|
||
if(state.type==='special')return state.options.special_floors||[];
|
||
return state.options.wood_floors||[];
|
||
}
|
||
function patternList(){
|
||
if(!state.options)return[];
|
||
return state.type==='tile' ? (state.options.tile_patterns||[]) : (state.options.wood_patterns||[]);
|
||
}
|
||
function selectIn(container,item,el,key){
|
||
[...container.querySelectorAll(key)].forEach(x=>x.classList.remove('selected'));
|
||
el.classList.add('selected');
|
||
return item;
|
||
}
|
||
function checkReady(){
|
||
$('generateBtn').disabled=!(state.file&&state.pattern&&state.floor);
|
||
}
|
||
state.materialSource='image';
|
||
$('materialSourceOptions').onclick=e=>{
|
||
const opt=e.target.closest('.option');
|
||
if(!opt)return;
|
||
[...$('materialSourceOptions').querySelectorAll('.option')].forEach(x=>x.classList.remove('selected'));
|
||
opt.classList.add('selected');
|
||
state.materialSource=opt.dataset.source;
|
||
};
|
||
// Material source selector
|
||
state.materialSource='image';
|
||
$('materialSourceOptions').onclick=e=>{
|
||
const opt=e.target.closest('.option');
|
||
if(!opt)return;
|
||
[...$('materialSourceOptions').querySelectorAll('.option')].forEach(x=>x.classList.remove('selected'));
|
||
opt.classList.add('selected');
|
||
state.materialSource=opt.dataset.source;
|
||
};
|
||
function render尺寸s(floor){
|
||
const el=$('sizeOptions'); el.innerHTML=''; state.sizeVariant=null;
|
||
const variants=floor.variants||[];
|
||
$('sizePanel').style.display='block';
|
||
// Collect all sizes: main entry first, then variants
|
||
const mainLabel=floor.size_label||'默认尺寸';
|
||
const all=[{sku:floor.sku,label:mainLabel},...variants.map(v=>({sku:v.sku,label:v.size_label||v.sku}))];
|
||
all.forEach((v,i)=>{
|
||
const div=document.createElement('div');
|
||
div.className='option';
|
||
div.innerHTML=`<div class="name">${v.label}</div><div class="meta">${v.sku}</div>`;
|
||
div.onclick=()=>{state.sizeVariant=selectIn(el,v,div,'.option')};
|
||
el.appendChild(div);
|
||
if(i===0){div.click();}
|
||
});
|
||
}
|
||
function cardText(f){
|
||
return [f.name,f.color_tone,f.finish,f.material,f.sku].filter(Boolean).join(' ');
|
||
}
|
||
function renderRooms(){
|
||
const el=$('roomOptions'); el.innerHTML='';
|
||
(state.options.rooms||[]).forEach((r,i)=>{
|
||
const div=document.createElement('div');
|
||
div.className='option';
|
||
div.innerHTML=`<div class="name">${r.name}</div><div class="meta">code ${r.code}</div>`;
|
||
div.onclick=()=>{state.room=selectIn(el,r,div,'.option')};
|
||
el.appendChild(div);
|
||
if(i===0)div.click();
|
||
});
|
||
}
|
||
function renderPatterns(){
|
||
const el=$('patternOptions'); el.innerHTML=''; state.pattern=null;
|
||
patternList().forEach((p,i)=>{
|
||
const div=document.createElement('div');
|
||
div.className='option';
|
||
div.innerHTML=`<div class="name">${p.name}</div><div class="meta">code ${p.code}</div>`;
|
||
div.onclick=()=>{state.pattern=selectIn(el,p,div,'.option');checkReady()};
|
||
el.appendChild(div);
|
||
if(i===0)div.click();
|
||
});
|
||
}
|
||
function renderFloors(){
|
||
const el=$('floorOptions'); el.innerHTML=''; state.floor=null;
|
||
const q=$('floorSearch').value.trim().toLowerCase();
|
||
const list=floorList().filter(f=>!q||cardText(f).toLowerCase().includes(q)).slice(0,200);
|
||
if(list.length===0){el.innerHTML='<div class="small">没有匹配的地板款式</div>';checkReady();return}
|
||
list.forEach(f=>{
|
||
const div=document.createElement('div');
|
||
div.className='floor-card';
|
||
const img=f.image_url ? `<img src="${f.image_url}" alt="">` : '<img alt="">';
|
||
div.innerHTML=`${img}<div class="title">${f.name||f.sku}</div><div class="desc">${[f.color_tone,f.finish,f.material,f.sku].filter(Boolean).join(' · ')}</div>`;
|
||
div.onclick=()=>{state.floor=selectIn(el,f,div,'.floor-card');render尺寸s(f);checkReady()};
|
||
el.appendChild(div);
|
||
});
|
||
el.querySelector('.floor-card')?.click();
|
||
}
|
||
function setType(type){
|
||
state.type=type;
|
||
[...$('typeOptions').querySelectorAll('.option')].forEach(x=>x.classList.toggle('selected',x.dataset.type===type));
|
||
$('floorTitle').textContent=type==='tile'?'选择瓷砖款式':type==='vinyl'?'选择乙烯基地板款式':type==='special'?'特殊测试地板 (含AI描述)':'选择木地板款式';
|
||
const groutDisabled=type!=='tile'&&type!=='special';
|
||
$('groutSelect').disabled=groutDisabled;
|
||
if(groutDisabled)$('groutSelect').value='';
|
||
renderPatterns(); renderFloors(); checkReady();
|
||
}
|
||
async function loadOptions(){
|
||
setProgress('加载选项','正在读取数据库中的地板款式...', '8%');
|
||
const res=await fetch('/floor/options');
|
||
const text=await res.text();
|
||
let json; try{json=JSON.parse(text)}catch{throw new 错误('选项接口返回非 JSON\n'+text)}
|
||
if(!res.ok||json.code!==200)throw new 错误(json.error||text);
|
||
state.options=apiData(json);
|
||
renderRooms(); setType('wood');
|
||
$('statusBox').style.display='none';
|
||
}
|
||
function handleFile(file){
|
||
state.file=file; state.originalUrl=URL.createObjectURL(file);
|
||
$('previewImg').src=state.originalUrl;
|
||
$('previewImg').style.display='block';
|
||
$('uploadHint').style.display='none';
|
||
$('compare').style.display='none';
|
||
$('resultImg').style.display='none';
|
||
$('resultPlaceholder').style.display='flex';
|
||
$('resultPlaceholder').innerHTML='已选择图片,等待生成';
|
||
checkReady();
|
||
}
|
||
async function readJSON(res){
|
||
const text=await res.text();
|
||
try{return {json:JSON.parse(text),raw:text}}catch{return {json:null,raw:text}}
|
||
}
|
||
function progressInfo(data){
|
||
if(data.step==='check')return ['第一步:内容审核','20%'];
|
||
if(data.step==='mask')return ['第二步:生成地板蒙版','45%'];
|
||
if(data.step==='inpaint')return ['第三步:铺设地板','75%'];
|
||
if(data.step==='generate')return ['第二步:AI生成地板','75%'];
|
||
if(data.step==='done')return ['完成','100%'];
|
||
if(data.step==='error')return ['错误','100%'];
|
||
return ['处理中','35%'];
|
||
}
|
||
function formatMs(ms){
|
||
if(!ms)return '';
|
||
return ms>=60000 ? (ms/60000).toFixed(0)+'m'+(ms%60000/1000).toFixed(0)+'s' : ms>=1000 ? (ms/1000).toFixed(1)+'s' : ms+'ms';
|
||
}
|
||
function formatTimings(data){
|
||
const timings=data.timings_ms||{};
|
||
const stepNames={check:'内容审核',mask:'蒙版生成',inpaint:'地板铺设',generate:'AI生成',cleanup:'清理',total:'总耗时'};
|
||
const parts=[];
|
||
for(const key of ['check','mask','inpaint','generate','total']){
|
||
if(timings[key])parts.push(`${stepNames[key]||key}: ${formatMs(timings[key])}`);
|
||
}
|
||
return parts.length ? '\n⏱️ '+parts.join(' | ') : '';
|
||
}
|
||
// Live elapsed timer
|
||
function startLiveTimer(){
|
||
if(state.liveInterval)clearInterval(state.liveInterval);
|
||
state.liveStart=Date.now();
|
||
state.liveInterval=setInterval(()=>{
|
||
const elapsed=Date.now()-state.liveStart;
|
||
const el=$('liveTimer'); if(!el)return;
|
||
el.textContent='⏱️ 已运行: '+formatMs(elapsed);
|
||
el.style.display='block';
|
||
},1000);
|
||
}
|
||
async function poll(jobId){
|
||
const res=await fetch('/floor/status?job_id='+encodeURIComponent(jobId));
|
||
const {json,raw}=await readJSON(res);
|
||
const data=apiData(json);
|
||
state.lastStatus=data;
|
||
if(!res.ok||!json)throw new 错误(`状态接口错误 HTTP ${res.status}\n${raw}`);
|
||
const [label,width]=progressInfo(data);
|
||
setProgress(label,`job_id: ${jobId}\n状态: ${data.status||''}\n步骤: ${data.step||''}\n消息: ${data.message||''}${formatTimings(data)}`,width);
|
||
if(data.status==='done'||data.status==='error'){
|
||
if(state.liveInterval)clearInterval(state.liveInterval);
|
||
}
|
||
if(data.status==='error'||data.step==='error'){
|
||
throw new 错误(`${data.error||data.message||'未知错误'}\n\n原始状态响应:\n${JSON.stringify(json,null,2)}`);
|
||
}
|
||
if(data.status==='done'&&data.result){
|
||
clearInterval(state.timer); state.timer=null;
|
||
$('generateBtn').classList.remove('loading');
|
||
$('generateBtn').disabled=false;
|
||
const url=data.result+'?t='+Date.now();
|
||
$('resultPlaceholder').style.display='none';
|
||
$('resultImg').src=url; $('resultImg').style.display='block';
|
||
setupCompare(state.originalUrl,url);
|
||
}
|
||
}
|
||
function setupCompare(before,after){
|
||
$('beforeImg').src=before; $('afterImg').src=after;
|
||
$('compare').style.display='block';
|
||
requestAnimationFrame(()=>$('beforeImg').style.width=$('compare').offsetWidth+'px');
|
||
}
|
||
function updateCompare(clientX){
|
||
const rect=$('compare').getBoundingClientRect();
|
||
let pct=(clientX-rect.left)/rect.width*100;
|
||
pct=Math.max(2,Math.min(98,pct));
|
||
$('clip').style.width=pct+'%';
|
||
$('handle').style.left=pct+'%';
|
||
}
|
||
|
||
$('uploadZone').onclick=()=>$('fileInput').click();
|
||
$('fileInput').onchange=()=>{if($('fileInput').files[0])handleFile($('fileInput').files[0])};
|
||
$('uploadZone').ondragover=e=>{e.preventDefault();$('uploadZone').classList.add('dragover')};
|
||
$('uploadZone').ondragleave=()=>$('uploadZone').classList.remove('dragover');
|
||
$('uploadZone').ondrop=e=>{e.preventDefault();$('uploadZone').classList.remove('dragover');if(e.dataTransfer.files[0])handleFile(e.dataTransfer.files[0])};
|
||
$('typeOptions').onclick=e=>{const opt=e.target.closest('.option');if(opt)setType(opt.dataset.type)};
|
||
$('floorSearch').oninput=renderFloors;
|
||
|
||
$('generateBtn').onclick=async()=>{
|
||
if(!state.file||!state.floor||!state.pattern)return;
|
||
clearInterval(state.timer); clearError();
|
||
$('generateBtn').classList.add('loading');
|
||
$('generateBtn').disabled=true;
|
||
$('compare').style.display='none';
|
||
$('resultImg').style.display='none';
|
||
$('resultPlaceholder').style.display='flex';
|
||
$('resultPlaceholder').innerHTML='正在提交任务...';
|
||
setProgress('提交任务','正在上传图片...', '10%');
|
||
startLiveTimer();
|
||
try{
|
||
const floorId=(state.sizeVariant&&state.sizeVariant.sku)||state.floor.id||state.floor.sku||'';
|
||
const patternCode=state.pattern.code||'';
|
||
const roomCode=state.room?state.room.code:0;
|
||
const fd=new FormData();
|
||
fd.append('image',state.file);
|
||
fd.append('floor_id',floorId);
|
||
fd.append('pattern_code',patternCode);
|
||
fd.append('room_code',roomCode);
|
||
fd.append('grout_lines',$('groutSelect').value||'');
|
||
fd.append('white_baseboard',$('baseboardSelect').value||'');
|
||
fd.append('material_source',state.materialSource||'image');
|
||
fd.append('material_source',state.materialSource||'image');
|
||
const submitted=`提交参数:\nfloor_id: ${floorId}\npattern_code: ${patternCode}\nroom_code: ${roomCode}\ngrout_lines: ${$('grout').checked}\nwhite_baseboard: ${$('baseboard').checked}`;
|
||
setProgress('提交任务',submitted, '10%');
|
||
const res=await fetch('/floor/generate',{method:'POST',body:fd});
|
||
const {json,raw}=await readJSON(res);
|
||
if(!res.ok||!json||json.code!==200){
|
||
throw new 错误(`${json?.error||'提交失败'}\n\n${submitted}\n\nHTTP ${res.status}\n${raw}`);
|
||
}
|
||
const jobId=json.data.job_id;
|
||
await poll(jobId);
|
||
state.timer=setInterval(()=>poll(jobId).catch(err=>{
|
||
clearInterval(state.timer); state.timer=null;
|
||
$('generateBtn').classList.remove('loading');
|
||
$('generateBtn').disabled=false;
|
||
showError(err.message, state.lastStatus ? JSON.stringify(state.lastStatus,null,2) : '');
|
||
}),2500);
|
||
}catch(err){
|
||
if(state.liveInterval){clearInterval(state.liveInterval);$('liveTimer').style.display='none'}
|
||
$('generateBtn').classList.remove('loading');
|
||
$('generateBtn').disabled=false;
|
||
$('statusBox').style.display='none';
|
||
showError(err.message,'');
|
||
}
|
||
};
|
||
|
||
let dragging=false;
|
||
$('compare').addEventListener('mousedown',e=>{dragging=true;updateCompare(e.clientX);e.preventDefault()});
|
||
window.addEventListener('mousemove',e=>{if(dragging)updateCompare(e.clientX)});
|
||
window.addEventListener('mouseup',()=>dragging=false);
|
||
$('compare').addEventListener('touchstart',e=>{dragging=true;updateCompare(e.touches[0].clientX);e.preventDefault()},{passive:false});
|
||
window.addEventListener('touchmove',e=>{if(dragging)updateCompare(e.touches[0].clientX)},{passive:false});
|
||
window.addEventListener('touchend',()=>dragging=false);
|
||
window.addEventListener('resize',()=>{if($('compare').style.display==='block')$('beforeImg').style.width=$('compare').offsetWidth+'px'});
|
||
|
||
loadOptions().catch(err=>showError('选项加载失败',err.message));
|
||
</script>
|
||
</body>
|
||
</html>
|