FloorVisualizer/internal/api/questionnaire.html

360 lines
19 KiB
HTML
Raw Permalink Normal View History

2026-07-27 07:34:32 +00:00
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>地板偏好问卷 - FloorVisualizer</title>
<style>
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",Arial,sans-serif;background:#f5f3ee;color:#24211d}
button,input,select{font:inherit}
.shell{min-height:100vh;display:grid;grid-template-columns:390px 1fr}
.sidebar{background:#faf9f6;border-right:1px solid #ddd6c8;padding:24px;position:sticky;top:0;height:100vh;overflow:auto}
.brand{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:22px}
.brand h1{font-size:20px;line-height:1.2;margin:0;color:#1e3832}
.brand a{color:#51645f;text-decoration:none;font-size:13px}
.progress{height:8px;background:#e4ded1;border-radius:999px;overflow:hidden;margin:10px 0 20px}
.progress span{display:block;height:100%;width:0;background:#2f6f64;transition:width .2s}
.group{padding:18px 0;border-top:1px solid #e3dccf}
.group:first-of-type{border-top:0}
.group h2{font-size:15px;margin:0 0 12px;color:#2d2a25}
.group .optional{font-size:12px;color:#867d70;font-weight:400;margin-left:6px}
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{border:1px solid #cfc5b4;background:#fff;padding:8px 10px;border-radius:8px;color:#39342d;cursor:pointer;min-height:38px}
.chip:hover{border-color:#2f6f64}
.chip.selected{border-color:#2f6f64;background:#dfeee9;color:#183b34}
.chip.swatch{display:flex;align-items:center;gap:8px}
.dot{width:15px;height:15px;border-radius:50%;border:1px solid rgba(0,0,0,.18);flex:0 0 auto}
.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-size:12px;color:#6f675d}
.field input,.field select{width:100%;border:1px solid #cfc5b4;background:#fff;border-radius:8px;padding:10px;color:#24211d}
.actions{display:flex;gap:10px;position:sticky;bottom:0;background:linear-gradient(180deg,rgba(250,249,246,0),#faf9f6 20%);padding-top:18px}
.primary,.secondary{border:0;border-radius:8px;padding:12px 14px;cursor:pointer;font-weight:700}
.primary{background:#2f6f64;color:white;flex:1}
.primary:hover{background:#24594f}
.primary:disabled{background:#9aa5a1;cursor:not-allowed}
.secondary{background:#e9e2d6;color:#3f3931}
.main{padding:28px;overflow:auto}
.summary{display:grid;grid-template-columns:1fr auto;gap:20px;align-items:end;margin-bottom:22px}
.summary h2{font-size:24px;margin:0 0 8px;color:#1d332f}
.summary p{margin:0;color:#6b6258;line-height:1.5}
.stats{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end}
.stat{background:#fff;border:1px solid #ddd6c8;border-radius:8px;padding:10px 12px;min-width:110px}
.stat strong{display:block;font-size:18px;color:#2f6f64}
.stat span{font-size:12px;color:#777064}
.empty{min-height:420px;border:1px dashed #cfc5b4;border-radius:8px;background:#fffaf2;display:flex;align-items:center;justify-content:center;text-align:center;color:#6b6258;line-height:1.7;padding:24px}
.results{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.card{background:#fff;border:1px solid #ddd6c8;border-radius:8px;display:grid;grid-template-columns:160px 1fr;min-height:176px;overflow:hidden}
.media{background:#e6e0d4;position:relative}
.media img{width:100%;height:100%;object-fit:cover;display:block}
.rank{position:absolute;top:10px;left:10px;background:#1e3832;color:white;border-radius:999px;padding:4px 8px;font-size:12px;font-weight:700}
.score{position:absolute;right:10px;bottom:10px;background:#d99c35;color:#20170b;border-radius:999px;padding:4px 8px;font-size:12px;font-weight:800}
.content{padding:14px;display:flex;flex-direction:column;gap:8px}
.meta{display:flex;flex-wrap:wrap;gap:6px}
.pill{background:#eee8dc;border-radius:999px;padding:4px 8px;font-size:12px;color:#5d5449}
.title{font-size:16px;font-weight:800;line-height:1.3;color:#25211b}
.sub{font-size:13px;color:#6f675d;line-height:1.35}
.reasons{display:flex;flex-wrap:wrap;gap:6px;margin-top:auto}
.reason{border:1px solid #cbded8;background:#eef8f5;color:#24594f;border-radius:999px;padding:4px 8px;font-size:12px}
.links{display:flex;gap:8px;margin-top:2px}
.links a{color:#2f6f64;font-size:13px;text-decoration:none;font-weight:700}
.loading{opacity:.7;pointer-events:none}
.notice{display:none;margin-bottom:14px;padding:12px 14px;border:1px solid #d9b978;background:#fff4d8;border-radius:8px;color:#5c4518}
@media(max-width:1100px){.shell{grid-template-columns:1fr}.sidebar{position:relative;height:auto}.summary{grid-template-columns:1fr}.stats{justify-content:flex-start}.results{grid-template-columns:1fr}}
@media(max-width:620px){.sidebar,.main{padding:18px}.card{grid-template-columns:1fr}.media{height:210px}.row{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="shell">
<aside class="sidebar">
<div class="brand">
<h1>地板偏好问卷</h1>
<a href="/">返回 Demo</a>
</div>
<div class="progress"><span id="progress"></span></div>
<section class="group" data-required="rooms">
<h2>铺设空间</h2>
<div class="chips" data-key="rooms" data-multi="true">
<button class="chip" data-value="living_room">客厅</button>
<button class="chip" data-value="bedroom">卧室</button>
<button class="chip" data-value="kitchen">厨房</button>
<button class="chip" data-value="bathroom">卫生间</button>
<button class="chip" data-value="basement">地下室</button>
<button class="chip" data-value="rental">出租房/高频使用</button>
<button class="chip" data-value="commercial">商业空间</button>
</div>
</section>
<section class="group" data-required="styles">
<h2>整体风格</h2>
<div class="chips" data-key="styles" data-multi="true" data-max="2">
<button class="chip" data-value="modern">现代简约</button>
<button class="chip" data-value="warm_natural">温暖自然</button>
<button class="chip" data-value="classic">经典传统</button>
<button class="chip" data-value="rustic">乡村/复古</button>
<button class="chip" data-value="luxury_stone">轻奢石纹</button>
<button class="chip" data-value="industrial">工业冷调</button>
<button class="chip" data-value="coastal">明亮海岸风</button>
</div>
</section>
<section class="group" data-required="looks">
<h2>外观类型</h2>
<div class="chips" data-key="looks" data-multi="true">
<button class="chip" data-value="wood_look">木纹</button>
<button class="chip" data-value="stone_tile">石纹/瓷砖感</button>
<button class="chip" data-value="real_hardwood">真实硬木</button>
<button class="chip" data-value="light_clean">浅色干净</button>
<button class="chip" data-value="dark_rich">深色稳重</button>
</div>
</section>
<section class="group" data-required="color_tones">
<h2>颜色倾向</h2>
<div class="chips" data-key="color_tones" data-multi="true">
<button class="chip swatch" data-value="Natural"><span class="dot" style="background:#b98b58"></span>自然木色</button>
<button class="chip swatch" data-value="Light Gray"><span class="dot" style="background:#b9bbb8"></span>浅灰</button>
<button class="chip swatch" data-value="Off White"><span class="dot" style="background:#eee8da"></span>米白</button>
<button class="chip swatch" data-value="Dark Brown"><span class="dot" style="background:#5a3928"></span>深棕</button>
<button class="chip swatch" data-value="Taupe"><span class="dot" style="background:#9b8d7a"></span>灰褐</button>
<button class="chip swatch" data-value="Cherry"><span class="dot" style="background:#8d3f31"></span>樱桃色</button>
<button class="chip swatch" data-value="Black Walnut"><span class="dot" style="background:#31241d"></span>黑胡桃</button>
</div>
</section>
<section class="group" data-required="budget">
<h2>预算范围</h2>
<div class="chips" data-key="budgetPreset">
<button class="chip" data-min="0" data-max="5">&lt; $5/sqft</button>
<button class="chip" data-min="5" data-max="8">$5-$8</button>
<button class="chip" data-min="8" data-max="12">$8-$12</button>
<button class="chip" data-min="12" data-max="20">$12-$20</button>
<button class="chip" data-min="20" data-max="0">$20+</button>
</div>
<div class="row" style="margin-top:10px">
<div class="field"><label>最低</label><input id="budgetMin" type="number" min="0" step="0.5" placeholder="0"></div>
<div class="field"><label>最高</label><input id="budgetMax" type="number" min="0" step="0.5" placeholder="不限"></div>
</div>
</section>
<section class="group">
<h2>性能偏好<span class="optional">可跳过</span></h2>
<div class="chips" data-key="performance" data-multi="true">
<button class="chip" data-value="waterproof">防水/防潮</button>
<button class="chip" data-value="scratch_resistant">耐磨耐刮</button>
<button class="chip" data-value="easy_clean">容易清洁</button>
<button class="chip" data-value="pet_kid">适合宠物/孩子</button>
<button class="chip" data-value="comfort_quiet">脚感舒适/安静</button>
<button class="chip" data-value="eco">环保倾向</button>
</div>
</section>
<section class="group">
<h2>表面质感<span class="optional">可跳过</span></h2>
<div class="chips" data-key="finishes" data-multi="true">
<button class="chip" data-value="Matte">哑光</button>
<button class="chip" data-value="Gloss">亮面</button>
<button class="chip" data-value="Distressed/Embossed">复古做旧</button>
<button class="chip" data-value="Wire Brushed">拉丝纹理</button>
<button class="chip" data-value="Textured">触感纹理</button>
</div>
</section>
<section class="group">
<h2>规格视觉<span class="optional">可跳过</span></h2>
<div class="chips" data-key="size_preference">
<button class="chip" data-value="wide_plank">宽板木纹</button>
<button class="chip" data-value="standard_plank">标准木板</button>
<button class="chip" data-value="large_tile">大规格石材</button>
<button class="chip" data-value="small_tile">小砖/马赛克</button>
</div>
</section>
<section class="group">
<h2>品牌<span class="optional">可跳过</span></h2>
<div class="chips" id="brandChips" data-key="brands" data-multi="true"></div>
<div class="field" style="margin-top:10px">
<label>品牌处理方式</label>
<select id="brandMode">
<option value="boost">偏好加分</option>
<option value="only">只看所选品牌</option>
</select>
</div>
</section>
<section class="group">
<h2>优先级<span class="optional">可跳过</span></h2>
<div class="chips" data-key="priority">
<button class="chip" data-value="style">风格最重要</button>
<button class="chip" data-value="budget">价格最重要</button>
<button class="chip" data-value="durability">耐用最重要</button>
<button class="chip" data-value="brand">品牌最重要</button>
<button class="chip" data-value="color">颜色最重要</button>
</div>
</section>
<div class="actions">
<button class="secondary" id="resetBtn">重置</button>
<button class="primary" id="submitBtn">推荐 Top 10</button>
</div>
</aside>
<main class="main">
<div class="summary">
<div>
<h2>问卷推荐结果</h2>
<p id="summaryText">回答左侧核心问题后,系统会对产品库打分、合并同款不同尺寸,并返回最匹配的 10 款。</p>
</div>
<div class="stats">
<div class="stat"><strong id="totalStat">-</strong><span>候选 SKU</span></div>
<div class="stat"><strong id="uniqueStat">-</strong><span>去重款式</span></div>
<div class="stat"><strong id="answeredStat">0%</strong><span>问卷完成度</span></div>
</div>
</div>
<div class="notice" id="notice"></div>
<div class="empty" id="empty">请选择你的空间、风格、外观、颜色和预算。</div>
<div class="results" id="results"></div>
</main>
</div>
<script>
const state={rooms:[],styles:[],looks:[],color_tones:[],performance:[],finishes:[],brands:[],size_preference:'',priority:'',budget:{min:0,max:0}};
const required=['rooms','styles','looks','color_tones','budget'];
const $=id=>document.getElementById(id);
function selectedValues(key){return state[key]||[]}
function setProgress(){
let done=0;
for(const key of required){
if(key==='budget'){ if(state.budget.min>0||state.budget.max>0) done++; }
else if((state[key]||[]).length) done++;
}
$('progress').style.width=(done/required.length*100)+'%';
$('answeredStat').textContent=Math.round(done/required.length*100)+'%';
}
function bindChips(root=document){
root.querySelectorAll('.chips').forEach(group=>{
group.addEventListener('click',e=>{
const chip=e.target.closest('.chip');
if(!chip)return;
const key=group.dataset.key;
if(key==='budgetPreset'){
group.querySelectorAll('.chip').forEach(c=>c.classList.remove('selected'));
chip.classList.add('selected');
state.budget={min:Number(chip.dataset.min||0),max:Number(chip.dataset.max||0)};
$('budgetMin').value=state.budget.min||'';
$('budgetMax').value=state.budget.max||'';
setProgress();
return;
}
const multi=group.dataset.multi==='true';
const max=Number(group.dataset.max||0);
const value=chip.dataset.value;
if(multi){
const list=selectedValues(key);
const i=list.indexOf(value);
if(i>=0){list.splice(i,1);chip.classList.remove('selected');}
else{
if(max&&list.length>=max){
const removed=list.shift();
group.querySelector(`.chip[data-value="${CSS.escape(removed)}"]`)?.classList.remove('selected');
}
list.push(value);chip.classList.add('selected');
}
}else{
group.querySelectorAll('.chip').forEach(c=>c.classList.remove('selected'));
if(state[key]===value){state[key]='';}
else{state[key]=value;chip.classList.add('selected');}
}
setProgress();
});
});
}
function budgetInputs(){
state.budget={min:Number($('budgetMin').value||0),max:Number($('budgetMax').value||0)};
document.querySelectorAll('[data-key="budgetPreset"] .chip').forEach(c=>c.classList.remove('selected'));
setProgress();
}
function resetAll(){
for(const k of ['rooms','styles','looks','color_tones','performance','finishes','brands'])state[k]=[];
state.size_preference='';state.priority='';state.budget={min:0,max:0};
document.querySelectorAll('.chip').forEach(c=>c.classList.remove('selected'));
$('budgetMin').value='';$('budgetMax').value='';$('brandMode').value='boost';
$('results').innerHTML='';$('empty').style.display='flex';$('notice').style.display='none';
$('totalStat').textContent='-';$('uniqueStat').textContent='-';setProgress();
}
async function loadOptions(){
try{
const res=await fetch('/questionnaire/options');
const json=await res.json();
const data=json.data||json;
const brands=(data.brands||[]).slice(0,12);
$('brandChips').innerHTML=brands.map(b=>`<button class="chip" data-value="${escapeHtml(b.value)}">${escapeHtml(b.value)} <span class="optional">${b.count}</span></button>`).join('');
bindChips($('brandChips').parentElement);
}catch(e){
$('brandChips').innerHTML='<button class="chip" data-value="Shaw Floors">Shaw Floors</button><button class="chip" data-value="COREtec">COREtec</button><button class="chip" data-value="Bruce">Bruce</button>';
bindChips($('brandChips').parentElement);
}
}
async function recommend(){
budgetInputs();
const payload={...state,brand_mode:$('brandMode').value,limit:10};
$('submitBtn').disabled=true;
$('submitBtn').textContent='推荐中...';
document.body.classList.add('loading');
$('notice').style.display='none';
try{
const res=await fetch('/questionnaire/recommend',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const json=await res.json();
if(!res.ok)throw new Error(json.error||'推荐失败');
renderResults(json.data||json);
}catch(e){
$('notice').style.display='block';
$('notice').textContent=e.message;
}finally{
$('submitBtn').disabled=false;
$('submitBtn').textContent='推荐 Top 10';
document.body.classList.remove('loading');
}
}
function renderResults(data){
const recs=data.recommendations||[];
$('totalStat').textContent=data.total_candidates||0;
$('uniqueStat').textContent=data.unique_candidates||0;
$('empty').style.display=recs.length?'none':'flex';
$('empty').textContent=recs.length?'':'没有找到匹配产品,可以放宽品牌或预算条件。';
$('summaryText').textContent=recs.length?`已按你的偏好筛出 ${recs.length} 款推荐,分数来自风格、颜色、预算、材质、性能和品牌等维度。`:'没有找到匹配产品。';
$('results').innerHTML=recs.map((r,i)=>cardHtml(r,i)).join('');
}
function cardHtml(r,i){
const p=r.product||{};
const img=p.room_image_url||p.main_image_url||'';
const price=p.price_per_sqft?`$${Number(p.price_per_sqft).toFixed(2)}/sqft`:'价格待确认';
const specs=r.spec_count>1?`${r.spec_count} 个规格`:(p.size_label||'规格待确认');
const reasons=(r.reasons||[]).map(x=>`<span class="reason">${escapeHtml(x)}</span>`).join('');
return `<article class="card">
<div class="media">${img?`<img src="${escapeHtml(img)}" alt="">`:''}<span class="rank">#${i+1}</span><span class="score">${Math.round(r.score)}分</span></div>
<div class="content">
<div class="meta"><span class="pill">${escapeHtml(p.brand||'')}</span><span class="pill">${escapeHtml(p.category||'')}</span><span class="pill">${escapeHtml(price)}</span></div>
<div class="title">${escapeHtml(p.style_name||p.series_name||p.sku)}</div>
<div class="sub">${escapeHtml([p.series_name,p.color_tone,p.material,p.finish,specs].filter(Boolean).join(' · '))}</div>
<div class="reasons">${reasons}</div>
<div class="links">${p.sku?`<a href="/products/${encodeURIComponent(p.sku)}" target="_blank">查看详情</a>`:''}${p.source_url?`<a href="${escapeHtml(p.source_url)}" target="_blank">品牌页面</a>`:''}</div>
</div>
</article>`;
}
function escapeHtml(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
bindChips();
$('budgetMin').addEventListener('input',budgetInputs);
$('budgetMax').addEventListener('input',budgetInputs);
$('resetBtn').addEventListener('click',resetAll);
$('submitBtn').addEventListener('click',recommend);
loadOptions();
setProgress();
</script>
</body>
</html>