| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- "use strict";
- /* ========== 规则数据(完整规则手册) ========== */
- const MATS=[{n:"双饰面",min:3000,max:3500},{n:"PET",min:4500,max:5000},{n:"混油",min:6000,max:7000}];
- const WARD=[["板门补差价",538,"元/㎡"],["玻璃门补差价",1852,"元/㎡"],["灯带",386,"元/㎡或元/m"],["变压器+开关",536,"元/套"],["自弹器",115,"元/个"],["见光板",1138,"元/㎡"],["铰链",40,"元/个"],["护墙板",1852,"元/㎡"],["拉手",226,"元/个"],["抽屉",1300,"元/个"],["金属拉手(普通)",225,"元/m"],["金属拉手(铝格栅)",680,"元/个"],["衣杆",288,"元/m"],["圆弧",1560,"元/m"]];
- const DOORS=[["开放","开放柜","无门",1748,2380],["开放","装饰柜","无门",868,null],["双饰面板","双饰面-平板门","18MM",1748,2380],["双饰面板","双饰面-平板门","25MM",1918,2550],["双饰面板","双饰面+轻奢卡条","18MM",2216,2848],["双饰面板","双饰面+轻奢卡条","25MM",2386,3018],["水晶板门","水晶板门","18MM",2120,2960],["水晶板门","水晶板门","25MM",2290,3130],["水晶板门","水晶板门+轻奢卡条","18MM",2590,3430],["水晶板门","水晶板门+轻奢卡条","25MM",2758,3598],["铝框门","KM2611门板","生态板/PET",2488,3360],["铝框门","铝框玻璃门(有缝)","灰玻/茶玻/黑玻",2488,3248]];
- const KIT_FIX=[["fridge","冰箱吊柜",2622,"元/m"],["tall","高柜",5950,"元/m"],["counter","台面(石英石)",1480,"元/m"],["over","台面超出部分",2480,"元/m"],["side","见光板",1138,"元/㎡"]];
- const KIT_ACC=[["碗篮",1268],["锅篮",1628],["自弹器",115],["切角",500],["台下盆",500],["包管",500]];
- const RULES=[["目测估算像素/毫米比例","用图纸标注(如有)或厨房台面深度600mm(自动模式)"],["只看到红框部分","完整测量整个红框边界"],["厨房尺寸目测估算","用量尺/标准图/标注/CAD数据"],["混淆W/H方向","取 max(W,H) 为沿墙长度"],["厨房按面积整体计价","分项按延米/按个计价"],["有红框时跳过AI识别","AI始终先执行"],["平面图上假装能识别护墙板","平面图只能识别柜体和厨房,护墙板需手动指定或CAD图"],["护墙板用不同公式","与柜体相同:最长边×2.5=面积"],["报价单不区分基础项和增值项","必须分为两部分"],["橱柜地柜/吊柜固定死单价","支持12种门板配置下拉选择"],["使用门洞/床等不可靠隐式参照物","只保留「厨房台面深度600mm」"],["无标注时要求用户手动标注尺寸","AI全自动:600mm全图锚点,无需用户标注"],["不告知用户精度等级","UI必须标注「精确值」或「估算值」"],["平面图斜线填充误判为护墙板","一律不自动识别为护墙板"]];
- /* ========== 状态 ========== */
- const S={ft:"plan",redbox:false,scale:null,scaleSrc:"",objs:[],mat:0,rate:10,
- ward:WARD.map(()=>({on:false,v:0})),cfgBase:2,cfgHang:2,
- len:{base:0,hang:0,fridge:0,tall:0,counter:0,over:0,side:0},acc:KIT_ACC.map(()=>0)};
- const $=id=>document.getElementById(id);
- const fmt=n=>(Math.round(n)||0).toLocaleString("zh-CN");
- const fmt2=n=>(Math.round(n*100)/100).toLocaleString("zh-CN",{maximumFractionDigits:2});
- const unit=()=>{const m=MATS[S.mat];return m.min+(m.max-m.min)*(S.rate-10)/5;};
- const dimScale=()=>S.ft==="cad"?0.001:(S.scale||0);
- const sleep=ms=>new Promise(r=>setTimeout(r,ms));
- function precision(){if(S.ft==="cad")return{cls:"exact",txt:"精确值 ±0.1-1%"};if(S.redbox)return{cls:"est",txt:"估算值 ±2-5%(红框+标注)"};return S.scale?{cls:"est",txt:"估算值 ±5-8%(600mm锚点)"}:{cls:"na",txt:"精度:待识别"};}
- function toast(msg,cls){const t=document.createElement("div");t.className="toast "+(cls||"toast-info");t.textContent=msg;document.body.appendChild(t);setTimeout(()=>t.remove(),2600);}
- /* ========== 视图/步骤导航 ========== */
- function switchView(v){
- ["quotes","editor","rules"].forEach(k=>{
- $("view-"+k).classList.toggle("hidden",k!==v);
- $("nav-"+k).classList.toggle("active",k===v);
- });
- }
- const STEP_NAMES=["上传资料","AI解析","自动计价","报价单"];
- function qOpenEditorStep(n){
- for(let i=1;i<=4;i++)$("step-"+i).classList.toggle("hidden",i!==n);
- document.querySelectorAll("#steps-bar .step-dot").forEach(d=>{
- const s=+d.dataset.step;
- d.classList.toggle("active",s===n);
- d.classList.toggle("done",s<n);
- });
- $("editor-step-breadcrumb").textContent=`步骤 ${n}/4 · ${STEP_NAMES[n-1]}`;
- if(n===2)$("q-step2-fh-display").textContent=($("q-floor-height").value||"—")+" m";
- if(n===3)recalc();
- if(n===4){recalc();renderTiers();}
- window.scrollTo({top:0,behavior:"smooth"});
- }
- function qNewQuote(){switchView("editor");qOpenEditorStep(1);}
- function backToList(){qSaveQuote(true);switchView("quotes");}
- function qToggleRecogConfig(){const p=$("q-recog-config-panel");p.style.display=p.style.display==="none"?"block":"none";}
- /* ========== 画布与叠加 ========== */
- let img=null,overlays=[];
- const cv=$("q-ai-canvas"),ctx=cv.getContext("2d");
- function draw(){if(!img)return;const k=cv.width/img.naturalWidth;ctx.clearRect(0,0,cv.width,cv.height);ctx.drawImage(img,0,0,cv.width,cv.height);
- overlays.forEach(o=>{ctx.lineWidth=3;
- if(o.t==="anchor"){ctx.strokeStyle="#E53E3E";ctx.beginPath();ctx.moveTo(o.x*k,o.y*k);ctx.lineTo((o.x+o.w)*k,(o.y+o.h)*k);ctx.stroke();}
- else if(o.t==="red"){ctx.strokeStyle="#E53E3E";ctx.strokeRect(o.x*k,o.y*k,o.w*k,o.h*k);}
- else{ctx.strokeStyle=o.t==="cab"?"#3182CE":o.t==="panel"?"#38A169":"#805AD5";ctx.strokeRect(o.x*k,o.y*k,o.w*k,o.h*k);
- ctx.fillStyle=ctx.strokeStyle;ctx.font="bold 14px sans-serif";ctx.fillText(o.label||"",o.x*k+4,o.y*k+16);}});}
- function showImg(im){img=im;cv.width=1000;cv.height=Math.round(1000*im.naturalHeight/im.naturalWidth);draw();
- $("q-btn-analyze").disabled=false;
- $("q-status-drawing").textContent="✅ 图纸:已上传("+im.naturalWidth+"×"+im.naturalHeight+")";
- $("q-status-drawing").style.background="#C6F6D5";$("q-status-drawing").style.color="#276749";}
- /* ========== 示例图纸生成 ========== */
- function genSample(u){
- const c=document.createElement("canvas");c.width=1600;c.height=1100;const g=c.getContext("2d");
- g.fillStyle="#fff";g.fillRect(0,0,1600,1100);
- g.strokeStyle="#000";g.lineWidth=10;g.strokeRect(60,60,1480,980);
- g.lineWidth=4;g.strokeStyle="#000";
- g.beginPath();g.moveTo(800,60);g.lineTo(800,400);g.stroke(); // 内墙
- g.fillStyle="#333"; // 厨房(深色填充带,AI可解算台面深度)
- if(u){g.beginPath();g.moveTo(100,100);g.lineTo(140,100);g.lineTo(140,220);g.lineTo(380,220);g.lineTo(380,100);g.lineTo(420,100);g.lineTo(420,260);g.lineTo(100,260);g.closePath();g.fill();}
- else{g.beginPath();g.moveTo(100,100);g.lineTo(540,100);g.lineTo(540,140);g.lineTo(140,140);g.lineTo(140,360);g.lineTo(100,360);g.closePath();g.fill();}
- g.strokeStyle="#000";g.lineWidth=3; // 柜体(矩形+叉线)
- [[1150,120,300,80],[1150,700,240,70],[620,940,200,70],[300,700,70,220]].forEach(r=>{g.strokeRect(...r);g.beginPath();g.moveTo(r[0],r[1]);g.lineTo(r[0]+r[2],r[1]+r[3]);g.moveTo(r[0]+r[2],r[1]);g.lineTo(r[0],r[1]+r[3]);g.stroke();});
- g.lineWidth=2; // 护墙板斜线填充(AI 必须忽略,规则#14)
- for(let i=0;i<12;i++){g.beginPath();g.moveTo(620+i*26,120);g.lineTo(640+i*26,160);g.stroke();}
- g.fillStyle="#999";g.font="26px sans-serif";g.fillText("厨房",220,320);g.fillText("主卧",1250,450);g.fillText("客厅",600,600);
- if(u){g.strokeStyle="#E53E3E";g.lineWidth=8;g.strokeRect(80,80,360,200);} // 红框
- const im=new Image();im.src=c.toDataURL("image/png");return im;
- }
- function qUseMockDrawing(isU){const im=genSample(isU);im.onload=()=>{showImg(im);toast(isU?"已载入示例户型图(U型厨房+红框)":"已载入示例户型图(L型厨房)");qStartAnalysis();};}
- function qHandleUnified(inp){const f=inp.files[0];if(!f)return;
- if(/\.(pdf|dwg|dxf)$/i.test(f.name)){alert("PDF/DWG 请先转 JPG/PNG(Demo 限制)");return;}
- const rd=new FileReader();rd.onload=()=>{const im=new Image();im.onload=()=>{showImg(im);qStartAnalysis();};im.src=rd.result;};rd.readAsDataURL(f);}
- function qStartAnalysis(){if(!img){toast("请先上传图纸或选择示例","toast-error");return;}qOpenEditorStep(2);runAI();}
- /* ========== AI 识别引擎 v3(墙体分离 + 柜体特征分类) ========== */
- function logAI(html,cls){const el=$("q-ai-log");const s=document.createElement("span");if(cls)s.className=cls;s.innerHTML=html;el.appendChild(s);el.scrollTop=el.scrollHeight;}
- function setProg(i,st){$("q-ai-prog-"+i).className="ai-progress-step "+(st||"");}
- function erodeMask(m,w,h,it){let cur=m;for(let t=0;t<it;t++){const o=new Uint8Array(w*h);
- for(let y=1;y<h-1;y++)for(let x=1;x<w-1;x++){const i=y*w+x;
- if(cur[i]&&cur[i-1]&&cur[i+1]&&cur[i-w]&&cur[i+w]&&cur[i-w-1]&&cur[i-w+1]&&cur[i+w-1]&&cur[i+w+1])o[i]=1;}
- cur=o;}return cur;}
- function dilateMask(m,w,h,it){let cur=m;for(let t=0;t<it;t++){const o=new Uint8Array(cur);
- for(let y=1;y<h-1;y++)for(let x=1;x<w-1;x++){const i=y*w+x;
- if(cur[i]){o[i-1]=1;o[i+1]=1;o[i-w]=1;o[i+w]=1;}}
- cur=o;}return cur;}
- function wallThickness(wm,w,h){const runs=[];
- for(let y=0;y<h;y++){let r=0;for(let x=0;x<=w;x++){const v=x<w?wm[y*w+x]:0;if(v)r++;else{if(r>=3&&r<=40)runs.push(r);r=0;}}}
- for(let x=0;x<w;x++){let r=0;for(let y=0;y<=h;y++){const v=y<h?wm[y*w+x]:0;if(v)r++;else{if(r>=3&&r<=40)runs.push(r);r=0;}}}
- if(!runs.length)return 6;runs.sort((a,b)=>a-b);return runs[runs.length>>1];}
- async function runAI(){
- $("q-ai-log").innerHTML="";overlays=[];S.redbox=false;S.scale=null;S.scaleSrc="";S.objs=[];S.ft="plan";
- $("q-step2-next-btn").disabled=true;
- setProg(0,"running");setProg(1,"");setProg(2,"");
- logAI("▶ AI 全图识别启动(优先级:无红框全图 > 红框校准)","b");await sleep(250);
- const AW=500,k=AW/img.naturalWidth,AH=Math.round(img.naturalHeight*k);
- const oc=document.createElement("canvas");oc.width=AW;oc.height=AH;const og=oc.getContext("2d");
- og.drawImage(img,0,0,AW,AH);const d=og.getImageData(0,0,AW,AH).data;
- const ink=new Uint8Array(AW*AH);
- let rx0=1e9,ry0=1e9,rx1=-1,ry1=-1,rc=0;
- for(let i=0;i<AW*AH;i++){const r=d[i*4],g=d[i*4+1],b=d[i*4+2];
- if(r>140&&r-g>50&&r-b>50){rc++;if(i%AW<rx0)rx0=i%AW;if(i%AW>rx1)rx1=i%AW;const y=(i/AW)|0;if(y<ry0)ry0=y;if(y>ry1)ry1=y;}
- else if(.299*r+.587*g+.114*b<140)ink[i]=1;}
- logAI("· 步骤1 结构扫描:"+AW+"×"+AH+" 栅格化,墨线像素 "+ink.reduce((a,v)=>a+v,0));await sleep(200);
- if(rc>150){S.redbox=true;overlays.push({t:"red",x:rx0/k,y:ry0/k,w:(rx1-rx0)/k,h:(ry1-ry0)/k});logAI("· 检测到红框 → 场景B(有红框+标注,±2-5%)。AI 仍先执行全图识别(规则#6)","w");}
- else logAI("· 未检测到红框 → 场景A(无红框,AI全自动)");
- $("q-ai-scene-chip").textContent=S.redbox?"场景B · 红框+标注":"场景A · 无红框全自动";
- await sleep(200);
- /* 连通域(全墨线) */
- const cc=mask=>{const vis=new Uint8Array(mask.length),out=[];
- for(let s=0;s<mask.length;s++){if(!mask[s]||vis[s])continue;
- const q=[s];vis[s]=1;let x0=1e9,y0=1e9,x1=-1,y1=-1,area=0,hh=0;
- while(hh<q.length){const c=q[hh++];area++;const cx=c%AW,cy=(c/AW)|0;
- if(cx<x0)x0=cx;if(cx>x1)x1=cx;if(cy<y0)y0=cy;if(cy>y1)y1=cy;
- const nb=[c-1,c+1,c-AW,c+AW];for(const n of nb){if(n>=0&&n<mask.length&&mask[n]&&!vis[n]){vis[n]=1;q.push(n);}}}
- out.push({x:x0,y:y0,w:x1-x0,h:y1-y0,area});}
- return out;};
- const compsAll=cc(ink).sort((a,b)=>b.w*b.h-a.w*a.h);
- logAI("· 连通域分析:"+compsAll.length+" 个结构体");await sleep(200);
- setProg(0,"done");setProg(1,"running");
- /* 模式判定:示意图(实心厨房色块) vs 彩色户型图(墙体分离) */
- const solid=compsAll.find(c=>{const f=c.area/(c.w*c.h);return f>0.8&&c.w*c.h>0.02*AW*AH&&c.w<0.6*AW&&c.h<0.6*AH;});
- let kitchen=null;const wards=[];let excluded=0;
- if(solid){
- /* —— 示意图路径:探针判定 L/U + 二次方程解台面深度 —— */
- const c=solid,probes=[[c.x+c.w/2,c.y+3],[c.x+c.w/2,c.y+c.h-3],[c.x+3,c.y+c.h/2],[c.x+c.w-3,c.y+c.h/2]]
- .map(p=>ink[((p[1]|0)*AW+(p[0]|0))]?1:0).reduce((a,v)=>a+v,0);
- const sL=c.w+c.h,discL=sL*sL-4*c.area,sU=c.w+2*c.h,discU=sU*sU-8*c.area,sU2=2*c.w+c.h,discU2=sU2*sU2-8*c.area;
- let dd=0;
- if(probes===2&&discL>0)dd=(sL-Math.sqrt(discL))/2;
- else if(probes===3&&discU>0)dd=(sU-Math.sqrt(discU))/4;
- else if(probes===3&&discU2>0)dd=(sU2-Math.sqrt(discU2))/4;
- if(dd>.02*AW&&dd<.12*AW)kitchen={...c,dd};
- if(kitchen){
- const sc=0.6/(kitchen.dd/k);S.scale=sc;S.scaleSrc="AI定位厨房台面深度=600mm锚点(全图通用)";
- overlays.push({t:"kitchen",x:kitchen.x/k,y:kitchen.y/k,w:kitchen.w/k,h:kitchen.h/k,label:"厨房"});
- overlays.push({t:"anchor",x:kitchen.x/k,y:kitchen.y/k,w:kitchen.dd/k,h:0});
- const kt=kitchen.w>=kitchen.h?"L":"U";
- if(kt==="L"){$("kA").value=(Math.max(kitchen.w,kitchen.h)*sc).toFixed(2);$("kC").value=(Math.min(kitchen.w,kitchen.h)*sc).toFixed(2);$("kB").value="";}
- else{$("kA").value=(kitchen.h*sc).toFixed(2);$("kB").value=(kitchen.h*sc).toFixed(2);$("kC").value=(kitchen.w*sc).toFixed(2);}
- logAI("· 定位厨房(示意图模式):台面深度 "+Math.round(kitchen.dd)+"px = 600mm → 1px="+(sc*1000).toFixed(1)+"mm","b");
- }
- let cn=0;
- for(const c2 of compsAll){
- if(cn>=8)break;
- if(c2.w>.6*AW&&c2.h>.6*AH)continue;
- if(kitchen&&c2.x>=kitchen.x-4&&c2.y>=kitchen.y-4&&c2.x+c2.w<=kitchen.x+kitchen.w+4&&c2.y+c2.h<=kitchen.y+kitchen.h+4)continue;
- const f=c2.area/(c2.w*c2.h);
- if(c2.w>.03*AW&&c2.h>.03*AW&&Math.min(c2.w,c2.h)>=.018*AW&&f>=.10&&f<.7){
- cn++;wards.push(c2);}}
- }else{
- /* —— 彩色户型图路径:墙体分离 + 柜体特征分类 —— */
- logAI("· 彩色户型图模式:腐蚀法分离墙体(粗线)与家具(细线)","b");await sleep(200);
- let wm=erodeMask(ink,AW,AH,2),eit=2;
- if(wm.reduce((a,v)=>a+v,0)<0.005*AW*AH){wm=erodeMask(ink,AW,AH,1);eit=1;}
- let T=wallThickness(wm,AW,AH)+2*eit;T=Math.max(4,Math.min(24,T));
- logAI("· 墙厚先验 T≈"+T+"px(仅用于形状判定,不作尺寸参照,规则#11)");
- const wcore=dilateMask(wm,AW,AH,1);
- const wz=dilateMask(wm,AW,AH,Math.max(3,Math.round(T*0.7)));
- const thin=new Uint8Array(AW*AH);for(let i=0;i<AW*AH;i++)thin[i]=ink[i]&&!wcore[i];
- const comps=cc(thin).sort((a,b)=>b.w*b.h-a.w*a.h);
- const att=c=>{let hit=0,tot=0;
- for(let x=c.x;x<=c.x+c.w;x+=2){for(const y of[c.y,c.y+c.h]){tot++;if(wz[((y|0)*AW+(x|0))])hit++;}}
- for(let y=c.y;y<=c.y+c.h;y+=2){for(const x of[c.x,c.x+c.w]){tot++;if(wz[((y|0)*AW+(x|0))])hit++;}}
- return hit/Math.max(1,tot);};
- const hatch=c=>{let rt=0,ct=0,nr=0,nc=0;
- for(let y=c.y+2;y<c.y+c.h-1;y+=2){nr++;let t=0,pv=0;for(let x=c.x;x<=c.x+c.w;x++){const v=thin[y*AW+x]?1:0;if(v&&!pv)t++;pv=v;}rt+=t;}
- for(let x=c.x+2;x<c.x+c.w-1;x+=2){nc++;let t=0,pv=0;for(let y=c.y;y<=c.y+c.h;y++){const v=thin[y*AW+x]?1:0;if(v&&!pv)t++;pv=v;}ct+=t;}
- return Math.max(rt/Math.max(1,nr),ct/Math.max(1,nc));};
- const xpat=c=>{let h1=0,h2=0;const N=16;
- for(let i=1;i<N;i++){if(thin[((c.y+c.h*i/N)|0)*AW+((c.x+c.w*i/N)|0)])h1++;
- if(thin[((c.y+c.h-c.h*i/N)|0)*AW+((c.x+c.w*i/N)|0)])h2++;}
- return Math.min(h1,h2)/N;};
- const smalls=comps.filter(s=>s.w>=0.5*T&&s.w<=2.5*T&&s.h>=0.5*T&&s.h<=2.5*T);
- const kidsOf=c=>smalls.filter(s=>s!==c&&s.x>c.x&&s.y>c.y&&s.x+s.w<c.x+c.w&&s.y+s.h<c.y+c.h).length;
- const bands=[];
- for(const c of comps){
- if(c.w>0.55*AW||c.h>0.55*AH)continue;
- const short=Math.min(c.w,c.h),long=Math.max(c.w,c.h);
- if(short<1.2*T)continue; // 噪点/小符号
- const a=att(c),f=c.area/(c.w*c.h);
- /* 橱柜台面带:贴墙 + 长条 + 深度≈3T(600mm) + 内含水槽/灶具符号 */
- if(a>=0.25&&long>=2.2*short&&short>=2.2*T&&short<=3.8*T&&(kidsOf(c)>=1||(f>=0.1&&f<=0.35&&long>=3*short))){bands.push(c);continue;}
- /* 衣柜:贴墙 + 斜线填充 或 X交叉 */
- if(a>=0.3&&short>=1.5*T&&short<=5*T&&long>=1.4*short&&(hatch(c)>=4||xpat(c)>=0.55)){wards.push(c);continue;}
- /* 近方形 X交叉柜(玄关柜等) */
- if(a>=0.3&&xpat(c)>=0.55&&short>=1.5*T&&short<=6*T&&long<2.2*short){wards.push(c);continue;}
- excluded++;}
- logAI("· 柜体判定标准:贴墙率≥30% 且(斜线填充 / X交叉 / 600mm台面带+水槽灶具)","b");
- logAI("· 已排除床/沙发/桌椅/洁具等独立家具 "+excluded+" 个(不贴墙或特征不符)");await sleep(200);
- /* 厨房:台面带聚类 → 600mm 锚点 */
- if(bands.length){
- bands.sort((a,b)=>b.w*b.h-a.w*a.h);
- const cluster=[bands[0]];
- for(const b of bands.slice(1)){if(cluster.some(c=>Math.abs(c.x-b.x)<6*T&&Math.abs(c.y-b.y)<6*T||
- (b.x<c.x+c.w+6*T&&c.x<b.x+b.w+6*T&&b.y<c.y+c.h+6*T&&c.y<b.y+b.h+6*T)))cluster.push(b);}
- const b0=cluster[0],dNat=Math.min(b0.w,b0.h)/k;
- S.scale=0.6/dNat;S.scaleSrc="AI定位橱柜台面深度=600mm锚点(全图通用)";
- let ux0=1e9,uy0=1e9,ux1=-1,uy1=-1;
- cluster.forEach(c=>{ux0=Math.min(ux0,c.x);uy0=Math.min(uy0,c.y);ux1=Math.max(ux1,c.x+c.w);uy1=Math.max(uy1,c.y+c.h);});
- kitchen={x:ux0,y:uy0,w:ux1-ux0,h:uy1-uy0};
- overlays.push({t:"kitchen",x:ux0/k,y:uy0/k,w:(ux1-ux0)/k,h:(uy1-uy0)/k,label:"厨房"});
- overlays.push({t:"anchor",x:b0.x/k,y:b0.y/k,w:Math.min(b0.w,b0.h)/k,h:0});
- const lens=cluster.map(c=>Math.max(c.w,c.h)*S.scale).sort((a,b)=>b-a);
- if(lens.length===1){$("kA").value=lens[0].toFixed(2);$("kB").value="";$("kC").value="";logAI("· 厨房:检测到1条台面带 "+lens[0].toFixed(2)+"m,请在下一步补充另一侧边长","w");}
- else if(lens.length===2){$("kA").value=lens[0].toFixed(2);$("kC").value=lens[1].toFixed(2);$("kB").value="";}
- else{$("kA").value=lens[0].toFixed(2);$("kB").value=lens[1].toFixed(2);$("kC").value=lens[2].toFixed(2);}
- logAI("· 定位厨房:"+cluster.length+" 条台面带,深度 "+Math.round(dNat)+"px = 600mm → 1px="+(S.scale*1000).toFixed(1)+"mm","b");
- }else logAI("· 未能自动定位厨房台面带 → 请在下一步补充一个已知尺寸","e");
- }
- draw();await sleep(200);
- setProg(1,"done");setProg(2,"running");
- /* 柜体输出 */
- let cn=0;
- for(const c of wards){
- if(cn>=8)break;
- cn++;S.objs.push({name:"柜体"+cn,kind:"cab",src:"AI",w:Math.round(c.w/k),h:Math.round(c.h/k)});
- overlays.push({t:"cab",x:c.x/k,y:c.y/k,w:c.w/k,h:c.h/k,label:"柜体"+cn});}
- logAI("· 柜体识别:"+cn+" 个(max(W,H)×比例×2.5=投影面积)","b");await sleep(200);
- logAI("· 护墙板:平面图斜线填充不可靠 → <b>不自动识别</b>(规则#14),如需请在下一步手动添加","w");await sleep(150);
- logAI("· 尺寸计算完成。精度等级:"+precision().txt,"b");
- setProg(2,"done");draw();rebuildObjs();recalc();
- $("q-step2-next-btn").disabled=false;
- }
- /* ========== 对象表(步骤3) ========== */
- function rebuildObjs(){
- const tb=$("objBody");tb.innerHTML="";
- S.objs.forEach(p=>{const tr=document.createElement("tr");
- tr.innerHTML=`<td><input type="text" value="${p.name}" style="width:90px" data-f="name"></td><td>${p.src}</td>
- <td><input type="number" value="${p.w}" data-f="w"></td><td><input type="number" value="${p.h}" data-f="h"></td>
- <td class="text-right" data-o="len">-</td><td class="text-right" data-o="area">-</td><td class="text-right" data-o="amt">-</td>
- <td><button class="btn btn-danger btn-sm">删</button></td>`;
- tr.querySelectorAll("input").forEach(inp=>inp.oninput=()=>{p[inp.dataset.f]=inp.dataset.f==="name"?inp.value:(+inp.value||0);recalc();});
- tr.querySelector("button").onclick=()=>{S.objs.splice(S.objs.indexOf(p),1);rebuildObjs();recalc();};
- p._tr=tr;tb.appendChild(tr);});
- }
- $("addCab").onclick=()=>{S.objs.push({name:"柜体"+(S.objs.length+1),kind:"cab",src:"手动",w:0,h:0});rebuildObjs();recalc();};
- $("addPanel").onclick=()=>{S.objs.push({name:"护墙板"+(S.objs.filter(o=>o.kind==="panel").length+1),kind:"panel",src:"手动",w:0,h:0});rebuildObjs();recalc();};
- /* ========== 增值项 / 厨房分项(步骤3表格) ========== */
- function buildWard(){$("wardBody").innerHTML="";WARD.forEach((w,i)=>{const tr=document.createElement("tr");
- tr.innerHTML=`<td><input type="checkbox" style="width:16px"></td><td>${w[0]}</td><td class="text-right">${fmt(w[1])} ${w[2]}</td><td><input type="number" min="0" step="any" placeholder="0"></td><td class="text-right">¥0</td>`;
- const cb=tr.children[0].firstChild,inp=tr.children[3].firstChild;
- cb.checked=S.ward[i].on;inp.value=S.ward[i].v||"";inp.disabled=!S.ward[i].on;
- cb.onchange=()=>{S.ward[i].on=cb.checked;inp.disabled=!cb.checked;recalc();};
- inp.oninput=()=>{S.ward[i].v=+inp.value||0;recalc();};
- w._tr=tr;$("wardBody").appendChild(tr);});}
- function buildKit(){const tb=$("kitBody");tb.innerHTML="";
- const opts=base=>DOORS.map((d,i)=>d[4]===null&&base?null:`<option value="${i}">${d[0]}·${d[1]}·${d[2]} ${base?fmt(d[4])+"元/m":fmt(d[3])+"元/m"}</option>`).filter(Boolean).join("");
- const mk=h=>{const tr=document.createElement("tr");tr.innerHTML=h;tb.appendChild(tr);return tr;};
- mk(`<td>地柜(长=厨房可计费总长)</td><td><select id="selBase">${opts(true)}</select> <input type="number" id="lenBase" step="0.01" style="width:70px"> m</td><td class="text-right" id="pBase">-</td><td class="text-right" id="aBase">¥0</td>`);
- mk(`<td>吊柜</td><td><select id="selHang">${opts(false)}</select> <input type="number" id="lenHang" step="0.01" value="0" style="width:70px"> m</td><td class="text-right" id="pHang">-</td><td class="text-right" id="aHang">¥0</td>`);
- KIT_FIX.forEach(f=>mk(`<td>${f[1]}</td><td><input type="number" id="len_${f[0]}" step="0.01" value="0" style="width:70px"> ${f[3]==="元/㎡"?"㎡":"m"}</td><td class="text-right">${fmt(f[2])} ${f[3]}</td><td class="text-right" id="a_${f[0]}">¥0</td>`));
- KIT_ACC.forEach((a,i)=>mk(`<td>${a[0]}</td><td><input type="number" id="acc_${i}" step="1" value="0" style="width:70px"> 个</td><td class="text-right">${fmt(a[1])} 元/个</td><td class="text-right" id="aa_${i}">¥0</td>`));
- $("selBase").value=S.cfgBase;$("selHang").value=S.cfgHang;
- $("selBase").onchange=e=>{S.cfgBase=+e.target.value;recalc();};
- $("selHang").onchange=e=>{S.cfgHang=+e.target.value;recalc();};
- $("lenBase").oninput=e=>{S.len.base=+e.target.value||0;recalc();};
- $("lenHang").oninput=e=>{S.len.hang=+e.target.value||0;recalc();};
- KIT_FIX.forEach(f=>{const el=$("len_"+f[0]);el.value=S.len[f[0]]||"";el.oninput=e=>{S.len[f[0]]=+e.target.value||0;recalc();};});
- KIT_ACC.forEach((a,i)=>{const el=$("acc_"+i);el.value=S.acc[i]||"";el.oninput=e=>{S.acc[i]=+e.target.value||0;recalc();};});}
- /* ========== 计算引擎 ========== */
- let LASTQ={baseSum:0,kSum:0,wSum:0,varArea:0,baseRows:[],valueRows:[],kitLines:[],cabLines:[],panelLines:[],wardLines:[]};
- function recalc(){
- const pr=precision(),sc=dimScale();
- $("precBadge").className="badge "+pr.cls;$("precBadge").textContent=pr.txt;
- $("q-quote-prec").className="badge "+pr.cls;$("q-quote-prec").textContent=pr.txt;
- $("sceneChip").textContent="场景:"+(S.ft==="cad"?"CAD矢量":S.redbox?"B 有红框+标注":"A 无红框");
- $("scaleInfo").textContent=S.ft==="cad"?"CAD矢量直读(mm)":(S.scale?`1px=${(sc*1000).toFixed(2)}mm(${S.scaleSrc})`:"比例未建立");
- /* 基础项 */
- let baseSum=0,varArea=0;const baseRows=[],cabLines=[],panelLines=[];
- S.objs.forEach(p=>{const len=Math.max(p.w||0,p.h||0)*sc,area=len*2.5,amt=area*unit();baseSum+=amt;varArea+=area;
- baseRows.push({name:p.name,kind:p.kind,src:p.src,len,area,amt});
- const line=`<div class="total-line"><span>${p.name}[${p.src}]:max边 ${fmt2(len)}m ×2.5 = ${fmt2(area)}㎡ × ¥${fmt(unit())}</span><span class="num">¥${fmt(amt)}</span></div>`;
- (p.kind==="panel"?panelLines:cabLines).push(line);
- if(p._tr){p._tr.querySelector('[data-o="len"]').textContent=fmt2(len);p._tr.querySelector('[data-o="area"]').textContent=fmt2(area);p._tr.querySelector('[data-o="amt"]').textContent="¥"+fmt(amt);}});
- $("baseTotal").textContent="¥"+fmt(baseSum);
- /* 厨房判定 */
- const sides=[$("kA").value,$("kB").value,$("kC").value].map(v=>+v||0).filter(v=>v>0);
- let kT=0,kt="未判定",kF="";
- if(sides.length>=2){const mn=Math.min(...sides),mx=[...sides].sort((a,b)=>b-a);
- if(mn<=2){kt="L型";kT=Math.max(mx[0]+mx[mx.length-1]-1.6,0);kF=`L:${fmt2(mx[0])}+${fmt2(mx[mx.length-1])}−0.6−1.0`;}
- else if(sides.length>=3){kt="U型";kT=Math.max(mx[0]+mx[1]+mx[2]-2.2,0);kF=`U:${fmt2(mx[0])}+${fmt2(mx[1])}+${fmt2(mx[2])}−0.6−0.6−1.0`;}}
- $("kType").textContent=kt;$("kType").className="chip "+(kt==="U型"?"u":kt==="L型"?"l":"n");
- $("kFormula").textContent=kT>0?`${kF} = ${fmt2(kT)}m 可计费总长`:kF;
- if(document.activeElement!==$("lenBase")){$("lenBase").value=kT?kT.toFixed(2):"";S.len.base=kT;}
- if(document.activeElement!==$("len_counter")){$("len_counter").value=kT?kT.toFixed(2):"";S.len.counter=kT;}
- $("fbRow").style.display=S.scale||S.ft==="cad"?"none":"flex";
- /* 增值项:衣柜 */
- let wSum=0;const wardLines=[],valueRows=[];
- WARD.forEach((w,i)=>{const v=S.ward[i].on?S.ward[i].v:0,a=v*w[1];wSum+=a;
- if(w._tr)w._tr.children[4].textContent="¥"+fmt(a);
- if(v>0&&S.ward[i].on){wardLines.push(`<div class="total-line"><span>${w[0]} ×${fmt2(v)}</span><span class="num">¥${fmt(a)}</span></div>`);
- valueRows.push({item:"衣柜增值·"+w[0],spec:w[2],qty:fmt2(v),price:fmt(w[1])+" "+w[2],amt:a});}});
- /* 增值项:厨房 */
- let kSum=0;const kitLines=[];const db=DOORS[S.cfgBase],dh=DOORS[S.cfgHang];
- $("pBase").textContent=fmt(db[4]||0)+" 元/m";$("pHang").textContent=fmt(dh[3])+" 元/m";
- const aB=S.len.base*(db[4]||0);kSum+=aB;$("aBase").textContent="¥"+fmt(aB);
- if(aB){kitLines.push(`<div class="total-line"><span>地柜[${db[1]} ${db[2]}] ${fmt2(S.len.base)}m × ¥${fmt(db[4]||0)}</span><span class="num">¥${fmt(aB)}</span></div>`);
- valueRows.push({item:"橱柜·地柜",spec:`${db[0]}·${db[1]}·${db[2]}`,qty:fmt2(S.len.base)+" m",price:fmt(db[4]||0)+" 元/m",amt:aB});}
- const aH=S.len.hang*dh[3];kSum+=aH;$("aHang").textContent="¥"+fmt(aH);
- if(aH){kitLines.push(`<div class="total-line"><span>吊柜[${dh[1]} ${dh[2]}] ${fmt2(S.len.hang)}m × ¥${fmt(dh[3])}</span><span class="num">¥${fmt(aH)}</span></div>`);
- valueRows.push({item:"橱柜·吊柜",spec:`${dh[0]}·${dh[1]}·${dh[2]}`,qty:fmt2(S.len.hang)+" m",price:fmt(dh[3])+" 元/m",amt:aH});}
- KIT_FIX.forEach(f=>{const a=S.len[f[0]]*f[2];kSum+=a;$("a_"+f[0]).textContent="¥"+fmt(a);
- if(a){kitLines.push(`<div class="total-line"><span>${f[1]} ${fmt2(S.len[f[0]])}${f[3]==="元/㎡"?"㎡":"m"} × ¥${fmt(f[2])}</span><span class="num">¥${fmt(a)}</span></div>`);
- valueRows.push({item:"橱柜·"+f[1],spec:"—",qty:fmt2(S.len[f[0]])+(f[3]==="元/㎡"?" ㎡":" m"),price:fmt(f[2])+" "+f[3],amt:a});}});
- KIT_ACC.forEach((ac,i)=>{const a=S.acc[i]*ac[1];kSum+=a;$("aa_"+i).textContent="¥"+fmt(a);
- if(a){kitLines.push(`<div class="total-line"><span>${ac[0]} ×${S.acc[i]}</span><span class="num">¥${fmt(a)}</span></div>`);
- valueRows.push({item:"橱柜·"+ac[0],spec:"配件",qty:S.acc[i]+" 个",price:fmt(ac[1])+" 元/个",amt:a});}});
- $("valueTotal").textContent="¥"+fmt(wSum+kSum);
- $("grandTotal").textContent="¥"+fmt(baseSum+wSum+kSum);
- LASTQ={baseSum,kSum,wSum,varArea,baseRows,valueRows,kitLines,cabLines,panelLines,wardLines};
- renderStep4();
- }
- /* ========== 步骤4:报价单渲染 ========== */
- function renderTiers(){
- const box=$("q-ai-tier-selector");box.innerHTML="";
- MATS.forEach((m,i)=>{const d=document.createElement("div");
- d.className="tier-card"+(i===S.mat?" selected":"");
- d.innerHTML=`<div class="tier-name">${m.n}</div><div class="tier-price">¥${fmt(m.min)} — ¥${fmt(m.max)}/㎡</div><div class="tier-note">${i===S.mat?"✓ 已选择 · 当前单价 ¥"+fmt(unit()):"点击选择"}</div>`;
- d.onclick=()=>{S.mat=i;renderTiers();recalc();};
- box.appendChild(d);});
- }
- function qSetMat(i){S.mat=i;renderTiers();recalc();}
- function renderStep4(){
- const m=MATS[S.mat],Q=LASTQ;
- const fixed=Q.kSum+Q.wSum;
- const lo=Q.varArea*m.min+fixed,hi=Q.varArea*m.max+fixed,cur=Q.varArea*unit()+fixed;
- $("q-quote-meta").textContent=`生成日期:${new Date().toLocaleDateString("zh-CN")} | 材质档:${m.n} | 浮动率:${S.rate}%`;
- $("q-step5-range").textContent=`¥${fmt(lo)} — ¥${fmt(hi)}`;
- $("q-step5-rate-text").textContent=`预估总价区间(${m.n}档单价 ¥${fmt(m.min)}~¥${fmt(m.max)}/㎡) | ${precision().txt}`;
- $("q-ai-range-total").textContent="¥"+fmt(cur);
- $("q-ai-range-note").textContent=`当前参考总价(${m.n} ¥${fmt(unit())}/㎡ × ${fmt2(Q.varArea)}㎡ + 增值项 ¥${fmt(fixed)})`;
- $("q-ai-float-badge").textContent=S.rate+"%";
- $("q-ai-float-slider").value=S.rate;
- $("global-float-slider").value=S.rate;$("global-float-badge").textContent=S.rate+"%";
- document.querySelectorAll(".float-preset button[data-aifp]").forEach(b=>b.classList.toggle("active",+b.dataset.aifp===Math.round(S.rate)));
- $("q-ai-float-result").innerHTML=`当前单价:<b>¥${fmt2(unit())}/㎡</b>(${m.n} ${fmt(m.min)}~${fmt(m.max)},浮动率 ${S.rate}%)· 投影面积 <b>${fmt2(Q.varArea)}㎡</b> · 基础项 ¥${fmt(Q.baseSum)} + 增值项 ¥${fmt(fixed)} = 总计 <b>¥${fmt(cur)}</b>`;
- /* 空间卡片 */
- const cards=[];
- if(Q.kitLines.length)cards.push(["🍳 厨房","¥"+fmt(Q.kSum),Q.kitLines]);
- if(Q.cabLines.length)cards.push(["🗄️ 柜体(投影面积)","¥"+fmt(LASTQ.baseRows.filter(r=>r.kind==="cab").reduce((a,r)=>a+r.amt,0)),Q.cabLines]);
- if(Q.panelLines.length)cards.push(["🧱 护墙板(手动)","¥"+fmt(LASTQ.baseRows.filter(r=>r.kind==="panel").reduce((a,r)=>a+r.amt,0)),Q.panelLines]);
- if(Q.wardLines.length)cards.push(["👔 衣柜增值项","¥"+fmt(Q.wSum),Q.wardLines]);
- $("q-space-cards-container").innerHTML=cards.length?cards.map(c=>`<div class="space-card"><div class="space-header"><span>${c[0]}</span><span style="color:var(--primary)">${c[1]}</span></div><div class="space-lines">${c[2].join("")}</div></div>`).join(""):'<div class="note" style="text-align:center;">暂无识别结果,请先完成步骤 1-3</div>';
- /* 明细表 */
- $("q-quote-base-body").innerHTML=Q.baseRows.length?Q.baseRows.map(r=>`<tr><td>${r.kind==="panel"?"护墙板·":"柜体·"}${r.name}</td><td>${r.src}</td><td class="text-right">${fmt2(r.len)}</td><td class="text-right">${fmt2(r.area)}</td><td class="text-right">${fmt(unit())}</td><td class="text-right">¥${fmt(r.amt)}</td></tr>`).join(""):'<tr><td colspan="6" class="note">暂无</td></tr>';
- $("q-quote-value-body").innerHTML=Q.valueRows.length?Q.valueRows.map(r=>`<tr><td>${r.item}</td><td>${r.spec}</td><td class="text-right">${r.qty}</td><td class="text-right">${r.price}</td><td class="text-right">¥${fmt(r.amt)}</td></tr>`).join(""):'<tr><td colspan="5" class="note">暂无</td></tr>';
- $("q-grand-total-value").textContent="¥"+fmt(cur);
- $("q-grand-total-area").textContent=`投影面积 ${fmt2(Q.varArea)}㎡ · ${m.n} ¥${fmt2(unit())}/㎡`;
- $("q-grand-total-note").textContent=cur>0?`基础项 ¥${fmt(Q.baseSum)} + 增值项 ¥${fmt(fixed)}(${precision().txt})`:"上传图纸并完成 AI 识别后自动计算";
- $("q-step5-disclaimer").textContent=`⚠️ 声明:本报价为 AI 辅助生成的预估价格(${precision().txt}),最终成交价以实际量尺和确认为准。护墙板平面图不自动识别;仅「厨房台面深度600mm」为隐式参照物。`;
- $("global-est-range").textContent=`¥${fmt(lo)} ~ ¥${fmt(hi)}`;
- $("global-est-mid").textContent="参考总价: ¥"+fmt(cur);
- }
- function qOnAiFloatChange(v){S.rate=Math.min(15,Math.max(10,+v||10));renderTiers();recalc();}
- function qSetAiFloat(v){S.rate=Math.min(15,Math.max(10,+v||10));renderTiers();recalc();}
- /* ========== 报价单管理 ========== */
- let QUOTES=[];
- try{QUOTES=JSON.parse(localStorage.getItem("aiq_new_quotes")||"[]");}catch(e){QUOTES=[];}
- function saveQuotes(){try{localStorage.setItem("aiq_new_quotes",JSON.stringify(QUOTES));}catch(e){}}
- function snapshot(){return JSON.stringify({ft:S.ft,redbox:S.redbox,scale:S.scale,scaleSrc:S.scaleSrc,mat:S.mat,rate:S.rate,cfgBase:S.cfgBase,cfgHang:S.cfgHang,len:S.len,acc:S.acc,ward:S.ward,objs:S.objs.map(o=>({name:o.name,kind:o.kind,src:o.src,w:o.w,h:o.h}))});}
- function qSaveQuote(silent){
- if(!S.objs.length&&!LASTQ.baseSum&&!LASTQ.kSum&&!LASTQ.wSum){if(!silent)toast("暂无计价内容,请先上传图纸完成AI识别","toast-error");return;}
- const m=MATS[S.mat],fixed=LASTQ.kSum+LASTQ.wSum;
- const q={id:Date.now(),name:("报价单 "+(QUOTES.length+1)),date:new Date().toLocaleString("zh-CN"),mat:m.n,rate:S.rate,
- lo:LASTQ.varArea*m.min+fixed,hi:LASTQ.varArea*m.max+fixed,cur:LASTQ.varArea*unit()+fixed,prec:precision().txt,snap:snapshot()};
- QUOTES.unshift(q);saveQuotes();renderQuoteList();
- if(!silent)toast("已存入报价单列表 ✓","toast-success");
- }
- function renderQuoteList(){
- const tb=$("quote-table-body");tb.innerHTML="";
- $("quote-empty").style.display=QUOTES.length?"none":"block";
- QUOTES.forEach((q,i)=>{const tr=document.createElement("tr");
- tr.innerHTML=`<td><b>${q.name}</b></td><td>${q.date}</td><td>${q.mat}(${q.rate}%)</td>
- <td class="text-right">¥${fmt(q.lo)} — ¥${fmt(q.hi)}</td><td><span class="badge est" style="font-size:10px;">${q.prec}</span></td>
- <td></td>`;
- const td=tr.children[5];
- const b1=document.createElement("button");b1.className="btn btn-outline btn-sm";b1.textContent="打开";
- b1.onclick=e=>{e.stopPropagation();qOpenQuote(q);};
- const b2=document.createElement("button");b2.className="btn btn-danger btn-sm";b2.textContent="删除";b2.style.marginLeft="6px";
- b2.onclick=e=>{e.stopPropagation();QUOTES.splice(i,1);saveQuotes();renderQuoteList();toast("已删除");};
- td.appendChild(b1);td.appendChild(b2);
- tr.style.cursor="pointer";tr.onclick=()=>qOpenQuote(q);
- tb.appendChild(tr);});
- }
- function qOpenQuote(q){
- try{const snap=JSON.parse(q.snap);
- Object.assign(S,{ft:snap.ft,redbox:snap.redbox,scale:snap.scale,scaleSrc:snap.scaleSrc,mat:snap.mat,rate:snap.rate,cfgBase:snap.cfgBase,cfgHang:snap.cfgHang,len:snap.len,acc:snap.acc,ward:snap.ward,objs:snap.objs});
- $("kA").value="";$("kB").value="";$("kC").value="";
- /* 重新从对象推导厨房边(若曾识别)——此处保持手动值,直接重算 */
- rebuildObjs();buildWard();buildKit();recalc();
- $("editor-title").textContent=q.name;
- switchView("editor");qOpenEditorStep(4);
- toast("已打开:"+q.name);
- }catch(e){toast("打开失败","toast-error");}
- }
- /* ========== 规则配置表 ========== */
- function switchRuleTab(t){
- document.querySelectorAll("#view-rules .tab").forEach(el=>el.classList.remove("active"));
- event.target.classList.add("active");
- document.querySelectorAll(".rule-tab-content").forEach(c=>c.classList.add("hidden"));
- $("rule-tab-"+t).classList.remove("hidden");
- }
- function renderRules(){
- $("rule-table-mats").innerHTML=MATS.map(m=>`<tr><td><b>${m.n}</b></td><td class="text-right">${fmt(m.min)}</td><td class="text-right">${fmt(m.max)}</td></tr>`).join("");
- $("rule-table-wardrobe").innerHTML=WARD.map((w,i)=>`<tr><td>${i+1}</td><td>${w[0]}</td><td class="text-right">${fmt(w[1])}</td><td>${w[2]}</td></tr>`).join("");
- $("rule-table-kitchen").innerHTML=DOORS.map((d,i)=>`<tr><td>${i+1}</td><td>${d[0]}</td><td>${d[1]}</td><td>${d[2]}</td><td class="text-right">${fmt(d[3])}</td><td class="text-right">${d[4]===null?"—(装饰柜无地柜价)":fmt(d[4])}</td></tr>`).join("");
- $("rule-table-kitchen-fix").innerHTML=KIT_FIX.map(f=>`<tr><td>${f[1]}</td><td class="text-right">${fmt(f[2])}</td><td>${f[3]}</td></tr>`).join("")+KIT_ACC.map(a=>`<tr><td>${a[0]}(配件)</td><td class="text-right">${fmt(a[1])}</td><td>元/个</td></tr>`).join("");
- $("rule-table-ban").innerHTML=RULES.map((r,i)=>`<tr><td>${i+1}</td><td style="color:#C53030;">🚫 ${r[0]}</td><td style="color:#276749;">✓ ${r[1]}</td></tr>`).join("");
- }
- /* ========== 事件与初始化 ========== */
- $("kA").oninput=$("kB").oninput=$("kC").oninput=()=>recalc();
- $("btnFb").onclick=()=>{const px=+$("fbPx").value||0,mm=+$("fbMm").value||0;if(px>0&&mm>0){S.scale=(mm/1000)/px;S.scaleSrc="用户补充已知尺寸";recalc();toast("比例已建立");}else toast("请填写像素数与毫米数","toast-error");};
- buildWard();buildKit();renderTiers();renderRules();renderQuoteList();recalc();
|