|
@@ -20,18 +20,21 @@ function loadEngineSource() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ---------- 主入口 ----------
|
|
// ---------- 主入口 ----------
|
|
|
-export function renderReport({ quote, planSVG = null, imageDataUrl = null, analysis = null, clientName = '', salesNote = '' }) {
|
|
|
|
|
|
|
+export function renderReport({ quote, planSVG = null, imageDataUrl = null, analysis = null, clientName = '', salesNote = '', preferSVG = false }) {
|
|
|
const engine = loadEngineSource();
|
|
const engine = loadEngineSource();
|
|
|
const meta = quote.meta || {};
|
|
const meta = quote.meta || {};
|
|
|
const t = quote.totals || {};
|
|
const t = quote.totals || {};
|
|
|
const money = (n) => '¥' + Number(n || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
|
const money = (n) => '¥' + Number(n || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
|
|
const title = `${clientName || meta.clientName || '客户'} · 全屋定制报价方案`;
|
|
const title = `${clientName || meta.clientName || '客户'} · 全屋定制报价方案`;
|
|
|
|
|
|
|
|
- const planBlock = planSVG
|
|
|
|
|
- ? `<div class="plan-wrap" id="planWrap">${planSVG}</div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>`
|
|
|
|
|
- : imageDataUrl
|
|
|
|
|
- ? `<div class="plan-wrap"><img src="${imageDataUrl}" alt="户型图" class="plan-img"></div><p class="plan-note">📐 原始户型图(以实际量尺为准)</p>`
|
|
|
|
|
- : '';
|
|
|
|
|
|
|
+ // 默认展示原始户型图(用户要求:重绘与原图结构差距大);preferSVG=true 时才用 SVG 重绘
|
|
|
|
|
+ const planBlock = !preferSVG && imageDataUrl
|
|
|
|
|
+ ? `<div class="plan-wrap"><img src="${imageDataUrl}" alt="户型图" class="plan-img" loading="lazy"></div><p class="plan-note">📐 原始户型图(以实际量尺为准)</p>`
|
|
|
|
|
+ : planSVG
|
|
|
|
|
+ ? `<div class="plan-wrap" id="planWrap">${planSVG}</div><p class="plan-note">📐 AI 结构化重绘示意图(非施工图,以实际量尺为准)</p>`
|
|
|
|
|
+ : imageDataUrl
|
|
|
|
|
+ ? `<div class="plan-wrap"><img src="${imageDataUrl}" alt="户型图" class="plan-img" loading="lazy"></div><p class="plan-note">📐 原始户型图(以实际量尺为准)</p>`
|
|
|
|
|
+ : '';
|
|
|
|
|
|
|
|
const dimsBlock = analysis?.dimensions?.length
|
|
const dimsBlock = analysis?.dimensions?.length
|
|
|
? `<details class="dims"><summary>图面尺寸标注(${analysis.dimensions.length} 条,用作交叉验证)</summary><ul>${
|
|
? `<details class="dims"><summary>图面尺寸标注(${analysis.dimensions.length} 条,用作交叉验证)</summary><ul>${
|
|
@@ -108,6 +111,14 @@ input[type=range]{width:100%}
|
|
|
.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
|
|
.cabrow input{width:64px;padding:6px;border:1px solid var(--bd);border-radius:6px;font-size:13px}
|
|
|
.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
|
|
.cabrow .del{color:var(--dg);font-size:18px;padding:0 6px;cursor:pointer}
|
|
|
.cabrow .add{color:var(--ok)}
|
|
.cabrow .add{color:var(--ok)}
|
|
|
|
|
+/* 触屏交互保障 */
|
|
|
|
|
+.chip,.btn,.del,.room-h{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
|
|
|
|
|
+.chip:active{background:#DBEAFE}
|
|
|
|
|
+.btn:active{filter:brightness(.92)}
|
|
|
|
|
+.del:active{transform:scale(1.25)}
|
|
|
|
|
+.room-h:active{background:#F7FAFC}
|
|
|
|
|
+.fpc-toast{position:fixed;left:50%;bottom:calc(96px + env(safe-area-inset-bottom));transform:translateX(-50%) translateY(16px);background:rgba(45,55,72,.94);color:#fff;padding:10px 18px;border-radius:22px;font-size:13px;max-width:82vw;opacity:0;pointer-events:none;transition:all .25s;z-index:200}
|
|
|
|
|
+.fpc-toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
|
@media(min-width:640px){.hero .price{font-size:44px}}
|
|
@media(min-width:640px){.hero .price{font-size:44px}}
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
@@ -143,7 +154,7 @@ input[type=range]{width:100%}
|
|
|
<div class="card" style="margin-top:8px">
|
|
<div class="card" style="margin-top:8px">
|
|
|
<div class="trow"><span>项目小计</span><span class="v" id="tSub">${money(t.itemsSubtotal)}</span></div>
|
|
<div class="trow"><span>项目小计</span><span class="v" id="tSub">${money(t.itemsSubtotal)}</span></div>
|
|
|
<div class="trow dim"><span id="tDiscLabel">${esc(t.discountLabel || '')}</span><span class="v" id="tAfter">${money(t.afterDiscount)}</span></div>
|
|
<div class="trow dim"><span id="tDiscLabel">${esc(t.discountLabel || '')}</span><span class="v" id="tAfter">${money(t.afterDiscount)}</span></div>
|
|
|
- <div class="trow" id="actRow" style="${t.activityTotal ? '' : 'display:none'}"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-${money(t.activityTotal)}</span></div>
|
|
|
|
|
|
|
+ <div class="trow" id="tActRow" style="${t.activityTotal ? '' : 'display:none'}"><span>活动优惠</span><span class="v" style="color:var(--ok)" id="tAct">-${money(t.activityTotal)}</span></div>
|
|
|
<div class="trow total"><span>到手价</span><span class="v" id="tFinal">${money(t.finalPrice)}</span></div>
|
|
<div class="trow total"><span>到手价</span><span class="v" id="tFinal">${money(t.finalPrice)}</span></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -195,6 +206,7 @@ console.log('fpc engine ready');
|
|
|
(function(){
|
|
(function(){
|
|
|
var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
|
|
var Q = window.__Q__; var S = document; var money = function(n){ return '¥' + Math.round(n||0).toLocaleString('zh-CN'); };
|
|
|
var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
|
|
var cfg = Object.assign({ tierKey:'standard', tierName:'PET', floatPct:15, discount:1.0, mode:'quick' }, Q.config||{});
|
|
|
|
|
+ var d = S.getElementById('drawer');
|
|
|
function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: ${JSON.stringify(clientName || '')} }); }
|
|
function calc(){ return buildQuote({ cabinets: Q.cabinets, tierKey: cfg.tierKey, tierName: cfg.tierName, floatPct: cfg.floatPct, discount: cfg.discount, mode: cfg.mode, clientName: ${JSON.stringify(clientName || '')} }); }
|
|
|
function refresh(){
|
|
function refresh(){
|
|
|
var q = calc(); var t = q.totals;
|
|
var q = calc(); var t = q.totals;
|
|
@@ -204,43 +216,67 @@ console.log('fpc engine ready');
|
|
|
S.getElementById('tDiscLabel').textContent = t.discountLabel;
|
|
S.getElementById('tDiscLabel').textContent = t.discountLabel;
|
|
|
S.getElementById('tAfter').textContent = money(t.afterDiscount);
|
|
S.getElementById('tAfter').textContent = money(t.afterDiscount);
|
|
|
S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
|
|
S.getElementById('tAct').textContent = '-' + money(t.activityTotal);
|
|
|
|
|
+ S.getElementById('tActRow').style.display = t.activityTotal ? 'flex' : 'none';
|
|
|
S.getElementById('tFinal').textContent = money(t.finalPrice);
|
|
S.getElementById('tFinal').textContent = money(t.finalPrice);
|
|
|
S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
|
|
S.getElementById('bTier').textContent = (q.meta.tierName||'') + '材质';
|
|
|
S.getElementById('bDisc').textContent = t.discountLabel;
|
|
S.getElementById('bDisc').textContent = t.discountLabel;
|
|
|
}
|
|
}
|
|
|
- // room expand
|
|
|
|
|
- S.querySelectorAll('.room-h').forEach(function(el){ el.addEventListener('click', function(){ el.parentElement.classList.toggle('open'); }); });
|
|
|
|
|
- // chips
|
|
|
|
|
- function bindChips(id, fn){ var box = S.getElementById(id); if(!box) return; box.addEventListener('click', function(e){ var b = e.target.closest('.chip'); if(!b) return; box.querySelectorAll('.chip').forEach(function(x){x.classList.remove('on');}); b.classList.add('on'); fn(b.dataset.v); }); }
|
|
|
|
|
- bindChips('optTier', function(v){ cfg.tierKey = v; refresh(); });
|
|
|
|
|
- bindChips('optMat', function(v){ cfg.tierName = v; refresh(); });
|
|
|
|
|
- bindChips('optDisc', function(v){ cfg.discount = parseFloat(v); refresh(); });
|
|
|
|
|
- var fr = S.getElementById('floatR'); fr.addEventListener('input', function(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value); refresh(); });
|
|
|
|
|
- // drawer
|
|
|
|
|
- var d = S.getElementById('drawer');
|
|
|
|
|
- S.getElementById('btnAdjust').addEventListener('click', function(){ d.classList.add('open'); renderCabs(); });
|
|
|
|
|
- d.addEventListener('click', function(e){ if(e.target === d) d.classList.remove('open'); });
|
|
|
|
|
|
|
+ // ---- 页内 toast(替代 alert,微信内置浏览器兼容) ----
|
|
|
|
|
+ var toastEl = null;
|
|
|
|
|
+ function toast(msg){
|
|
|
|
|
+ if (!toastEl) { toastEl = S.createElement('div'); toastEl.className = 'fpc-toast'; S.body.appendChild(toastEl); }
|
|
|
|
|
+ toastEl.textContent = msg; toastEl.classList.add('show');
|
|
|
|
|
+ clearTimeout(toastEl._t); toastEl._t = setTimeout(function(){ toastEl.classList.remove('show'); }, 2400);
|
|
|
|
|
+ }
|
|
|
|
|
+ // ---- 统一触屏/鼠标激活:事件委托 + click/touchend 双通道(滑动>12px 判定为滚动不触发) ----
|
|
|
|
|
+ var touchStart = null;
|
|
|
|
|
+ S.addEventListener('touchstart', function(e){ if (e.touches.length === 1) { var t0 = e.touches[0]; touchStart = { x: t0.clientX, y: t0.clientY }; } }, { passive: true });
|
|
|
|
|
+ function onActivate(selector, handler){
|
|
|
|
|
+ S.addEventListener('click', function(e){ var t = e.target.closest(selector); if (t) handler(t, e); }, false);
|
|
|
|
|
+ S.addEventListener('touchend', function(e){
|
|
|
|
|
+ var t = e.target.closest(selector); if (!t) return;
|
|
|
|
|
+ if (touchStart && e.changedTouches && e.changedTouches.length) {
|
|
|
|
|
+ var t1 = e.changedTouches[0];
|
|
|
|
|
+ if (Math.hypot(t1.clientX - touchStart.x, t1.clientY - touchStart.y) > 12) return; // 滚动,忽略
|
|
|
|
|
+ }
|
|
|
|
|
+ e.preventDefault(); // 阻止合成 click 双触发
|
|
|
|
|
+ handler(t, e);
|
|
|
|
|
+ }, { passive: false });
|
|
|
|
|
+ }
|
|
|
|
|
+ onActivate('.room-h', function(t){ t.parentElement.classList.toggle('open'); });
|
|
|
|
|
+ onActivate('.chip[data-v]', function(t){
|
|
|
|
|
+ var box = t.closest('.opts');
|
|
|
|
|
+ if (box) box.querySelectorAll('.chip').forEach(function(x){ x.classList.remove('on'); });
|
|
|
|
|
+ t.classList.add('on');
|
|
|
|
|
+ var v = t.dataset.v; var id = box ? box.id : '';
|
|
|
|
|
+ if (id === 'optTier') { cfg.tierKey = v; refresh(); }
|
|
|
|
|
+ else if (id === 'optMat') { cfg.tierName = v; refresh(); }
|
|
|
|
|
+ else if (id === 'optDisc') { cfg.discount = parseFloat(v); refresh(); }
|
|
|
|
|
+ });
|
|
|
|
|
+ var fr = S.getElementById('floatR');
|
|
|
|
|
+ function onFloat(){ S.getElementById('floatV').textContent = fr.value; cfg.floatPct = parseInt(fr.value, 10); refresh(); }
|
|
|
|
|
+ fr.addEventListener('input', onFloat); fr.addEventListener('change', onFloat);
|
|
|
|
|
+ onActivate('#btnAdjust', function(){ d.classList.add('open'); renderCabs(); });
|
|
|
|
|
+ onActivate('#btnApply', function(){ d.classList.remove('open'); toast('✅ 已按新参数重算'); });
|
|
|
|
|
+ onActivate('#drawer', function(t, e){ if (e.target === d) d.classList.remove('open'); }); // 仅点遮罩本身关闭(sheet 内点击不触发)
|
|
|
|
|
+ onActivate('#btnAddCab', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); toast('已添加柜体,可改宽度'); });
|
|
|
|
|
+ onActivate('#btnSave', function(){ toast('已记录!销售将带这份报价与您确认到店时间,到店凭此单领取到店礼'); });
|
|
|
|
|
+ onActivate('.cabrow .del', function(t){ var i = parseInt(t.dataset.i, 10); if (!isNaN(i)) { Q.cabinets.splice(i, 1); renderCabs(); refresh(); } });
|
|
|
|
|
+ // 柜体宽度输入:委托 change(移动端键盘确认/失焦都会触发)
|
|
|
|
|
+ S.addEventListener('change', function(e){
|
|
|
|
|
+ var inp = e.target.closest ? e.target.closest('#cabList input') : null;
|
|
|
|
|
+ if (inp) { var i = parseInt(inp.dataset.i, 10); if (!isNaN(i)) { Q.cabinets[i].width = parseFloat(inp.value) || 1.5; refresh(); } }
|
|
|
|
|
+ });
|
|
|
function renderCabs(){
|
|
function renderCabs(){
|
|
|
var box = S.getElementById('cabList'); box.innerHTML = '';
|
|
var box = S.getElementById('cabList'); box.innerHTML = '';
|
|
|
Q.cabinets.forEach(function(c, i){
|
|
Q.cabinets.forEach(function(c, i){
|
|
|
var row = S.createElement('div'); row.className = 'cabrow';
|
|
var row = S.createElement('div'); row.className = 'cabrow';
|
|
|
- var dims = String(c.width||'') ? String(c.width) : '';
|
|
|
|
|
- row.innerHTML = '<span style="flex:1">' + c.rawName + '</span>' +
|
|
|
|
|
- '<input type="number" step="0.05" value="' + (c.width||1.5) + '" data-i="' + i + '" data-f="width" title="宽(米)">' +
|
|
|
|
|
|
|
+ row.innerHTML = '<span style="flex:1">' + (c.rawName || c.type) + '</span>' +
|
|
|
|
|
+ '<input type="number" inputmode="decimal" step="0.05" min="0.3" value="' + (c.width || 1.5) + '" data-i="' + i + '" title="宽(米)">' +
|
|
|
'<span class="del" data-i="' + i + '">✕</span>';
|
|
'<span class="del" data-i="' + i + '">✕</span>';
|
|
|
box.appendChild(row);
|
|
box.appendChild(row);
|
|
|
});
|
|
});
|
|
|
- box.onclick = function(e){
|
|
|
|
|
- var del = e.target.closest('.del');
|
|
|
|
|
- if (del) { Q.cabinets.splice(parseInt(del.dataset.i),1); renderCabs(); refresh(); return; }
|
|
|
|
|
- var inp = e.target.closest('input');
|
|
|
|
|
- if (inp) { inp.addEventListener('change', function(){ Q.cabinets[parseInt(inp.dataset.i)].width = parseFloat(inp.value)||1.5; refresh(); }); }
|
|
|
|
|
- };
|
|
|
|
|
}
|
|
}
|
|
|
- S.getElementById('btnAddCab').addEventListener('click', function(){ Q.cabinets.push({ type:'储物柜', rawName:'储物柜', width:1.5, height:2.6 }); renderCabs(); refresh(); });
|
|
|
|
|
- S.getElementById('btnSave').addEventListener('click', function(){
|
|
|
|
|
- alert('已记录!销售将带着这份报价与您确认到店时间,到店凭此单领取到店礼。');
|
|
|
|
|
- });
|
|
|
|
|
refresh();
|
|
refresh();
|
|
|
})();
|
|
})();
|
|
|
</script>
|
|
</script>
|