Przeglądaj źródła

fix: 修复切换图纸时的错位页面与存档恢复导致的 undefined 崩溃

- showImg/runAI 重置后立即清空旧 overlays 并重绘干净画布,避免新图纸复用上一张图的叠加红框(错位页面)

- 新增 ensureS() 兜底:旧版存档缺 objs/redRects/redLabels 时回退空数组,杜绝 Cannot read properties of undefined (reading 'length') 类崩溃

- qStartAnalysis 对 runAI 增加 .catch 异常兜底,识别异常不再留下半残页面;qResetEditor 补充重置 redRects/redLabels

- secure_preview_server 对齐 _server:/@vite/* 返回 200 空JS、剥离条件请求头、HTTP/1.1 keep-alive,消除 Trae 预览注入热更新脚本引起的加载中断/自动重载
缪子怡 2 tygodni temu
rodzic
commit
83e5d5e6e5
2 zmienionych plików z 49 dodań i 8 usunięć
  1. 16 8
      AI快速报价_Demo2.optimizing.html
  2. 33 0
      secure_preview_server.py

+ 16 - 8
AI快速报价_Demo2.optimizing.html

@@ -755,6 +755,8 @@ const RULES=[["目测估算像素/毫米比例","用图纸标注(如有)或
 /* rate 默认15 → 单价=所选材质档最高单价(双饰面3500);厨房=宽(最长边)/高(最短边)/总长度/单价 均可调 */
 const S={ft:"plan",redbox:false,scale:null,scale600:null,scaleSrc:"",objs:[],mat:0,rate:15,
   ruleX:Object.fromEntries(EXTRA.map(e=>[e.id,!e.off])),kitExtras:mkExtras("",true),kitW:null,kitH:null,kitLen:null,kitPrice:null,brand:0,door:2};
+/* 状态兜底:旧版存档/异常恢复可能缺失 objs 等数组字段 → 统一回退空数组,杜绝 undefined.length / undefined.forEach 崩溃 */
+function ensureS(){if(!Array.isArray(S.objs))S.objs=[];if(!Array.isArray(S.redRects))S.redRects=[];if(!Array.isArray(S.redLabels))S.redLabels=[];}
 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});
@@ -798,7 +800,7 @@ function qNewQuote(){qResetEditor();switchView("editor");qOpenEditorStep(1);}
 /* 新建报价单:完整清空上一单状态与识别UI,避免携带旧数据 */
 function qResetEditor(){
   img=null;overlays=[];ctx.clearRect(0,0,cv.width,cv.height);
-  Object.assign(S,{ft:"plan",redbox:false,scale:null,scale600:null,scaleSrc:"",objs:[],mat:0,rate:15,kitW:null,kitH:null,kitLen:null,kitPrice:null,kitBox:null,redArrow:null,brand:0,door:2});
+  Object.assign(S,{ft:"plan",redbox:false,scale:null,scale600:null,scaleSrc:"",objs:[],mat:0,rate:15,kitW:null,kitH:null,kitLen:null,kitPrice:null,kitBox:null,redArrow:null,redRects:[],redLabels:[],brand:0,door:2});
   S.ruleX=Object.fromEntries(EXTRA.map(e=>[e.id,!e.off]));S.kitExtras=mkExtras("",true);
   $("q-file-unified").value="";$("q-btn-analyze").disabled=true;$("q-step2-next-btn").disabled=true;
   $("q-status-drawing").textContent="⬜ 图纸:未上传";$("q-status-drawing").style.background="#EDF2F7";$("q-status-drawing").style.color="var(--text-muted)";
@@ -831,7 +833,7 @@ function draw(){if(!img)return;const k=cv.width/img.naturalWidth;ctx.clearRect(0
       ctx.lineWidth=6;ctx.strokeStyle="rgba(255,255,255,.92)";ctx.strokeText(lb,lx,ly);
       ctx.fillStyle=o.t==="panel"?"#38A169":"#E53E3E";ctx.fillText(lb,lx,ly);
       ctx.textAlign="left";}});}
-function showImg(im){img=im;cv.width=1000;cv.height=Math.round(1000*im.naturalHeight/im.naturalWidth);draw();
+function showImg(im){img=im;overlays=[];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";}
@@ -865,7 +867,11 @@ function qUseMockDrawing(isU,isCad){const im=genSample(isU,isCad);im.onload=()=>
 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();}
+function qStartAnalysis(){if(!img){toast("请先上传图纸或选择示例","toast-error");return;}qOpenEditorStep(2);
+  runAI().catch(e=>{ /* 识别异常兜底:清叠加、重绘画布、提示重试,避免留下错位/半残页面 */
+    overlays=[];if(img)draw();setProg(0,"");setProg(1,"");setProg(2,"");
+    logAI("⚠ AI 识别引擎异常:"+(e&&e.message?e.message:e)+"。画布已重置,请重试或更换图纸。","e");
+    toast("AI 识别异常,请重试","toast-error");});}
 /* ========== 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||"");}
@@ -977,6 +983,7 @@ function wallThickness(wm,w,h){const runs=[];
   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.scale600=null;S.scaleSrc="";S.objs=[];S.ft="plan";S.redRects=[];S.redLabels=[];S.kitBox=null;S.redArrow=null;S.kitW=null;S.kitH=null;S.kitLen=null;S.kitPrice=null;
+  if(img)draw();   /* 重置后立即重绘干净画布:避免新图纸复用上一张图的叠加红框,出现“错位页面” */
   $("q-ai-results").style.display="none";
   $("q-red-anno-row").style.display="none";
   $("q-step2-next-btn").disabled=true;
@@ -1426,10 +1433,10 @@ async function runAI(){
   $("q-step2-next-btn").disabled=false;
 }
 function fh(){return parseFloat($("q-floor-height").value)||2.5;}
-function qFhChanged(src){const v=(src?src.value:$("q-floor-height").value)||"2.5";
+function qFhChanged(src){ensureS();const v=(src?src.value:$("q-floor-height").value)||"2.5";
   $("q-floor-height").value=v;const i2=$("q-floor-height2");if(i2)i2.value=v;
   if(S.objs.length||S.kitBox){renderAIResults();recalc();}}
-function renderAIResults(){
+function renderAIResults(){ensureS();
   const sc=dimScale();
   $("q-red-anno-row").style.display=S.redArrow?"block":"none";
   renderAnnoInfer();
@@ -1454,7 +1461,7 @@ function renderAIResults(){
 
 /* ========== 对象表(步骤3) ========== */
 let KIT_TR=null,TOTAL_TR=null;
-function rebuildObjs(){
+function rebuildObjs(){ensureS();
   const tb=$("objBody");tb.innerHTML="";KIT_TR=null;TOTAL_TR=null;
   /* 厨房行:与柜体合并同表;宽=最长边、高=最短边、总长度(L/U型自动算,可改)、单价(元/m,默认材质档最高,可改) */
   if(S.kitW>0&&S.kitH>0){const tr=document.createElement("tr");KIT_TR=tr;
@@ -1569,7 +1576,7 @@ function calcExtras(p,isKit){
 
 /* ========== 计算引擎 ========== */
 let LASTQ={baseSum:0,kSum:0,wSum:0,varArea:0,cabArea:0,cabAmt:0,baseRows:[],valueRows:[]};
-function recalc(){
+function recalc(){ensureS();
   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;
@@ -1652,7 +1659,7 @@ 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,kitW:S.kitW,kitH:S.kitH,kitLen:S.kitLen,kitPrice:S.kitPrice,kitEx:S.kitExtras,ruleX:S.ruleX,brand:S.brand,door:S.door,objs:S.objs.map(o=>({name:o.name,kind:o.kind,src:o.src,w:o.w,h:o.h,lenM:o.lenM!=null?o.lenM:null,hgtM:o.hgtM!=null?o.hgtM:null,price:o.price!=null?o.price:null,ex:o.extras||{}}))});}
-function qSaveQuote(silent){
+function qSaveQuote(silent){ensureS();
   if(!S.objs.length&&!LASTQ.baseSum&&!LASTQ.wSum){if(!silent)toast("暂无计价内容,请先上传图纸完成AI识别","toast-error");return;}
   const m=MATS[S.mat],fixed=LASTQ.wSum;
   const q={id:Date.now(),name:("报价单 "+(QUOTES.length+1)),date:new Date().toLocaleString("zh-CN"),mat:m.n,rate:S.rate,
@@ -1679,6 +1686,7 @@ function renderQuoteList(){
 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,kitW:snap.kitW!=null?snap.kitW:null,kitH:snap.kitH!=null?snap.kitH:null,kitLen:snap.kitLen!=null?snap.kitLen:null,kitPrice:snap.kitPrice!=null?snap.kitPrice:null,objs:snap.objs});
+    ensureS();   /* 旧版存档可能缺 objs 等字段 → 回退空数组,避免恢复后 undefined 崩溃 */
     S.kitExtras=snap.kitEx||mkExtras("",true);
     S.ruleX=Object.assign(Object.fromEntries(EXTRA.map(e=>[e.id,!e.off])),snap.ruleX||{});
     S.brand=snap.brand!=null?snap.brand:0;

+ 33 - 0
secure_preview_server.py

@@ -54,16 +54,49 @@ def _normalize_image(value):
 
 
 class Handler(SimpleHTTPRequestHandler):
+    # HTTP/1.1 keep-alive:避免 HTTP/1.0 每请求关连接,预览面板复用连接时遇到 RST 而 ERR_ABORTED
+    protocol_version = "HTTP/1.1"
+
+    def _vite(self):
+        # Trae 预览会注入 /@vite/client 热更新脚本:返回 200 空JS,避免 404 中断主文档加载
+        self.send_response(200)
+        self.send_header("Content-Type", "text/javascript")
+        self.send_header("Cache-Control", "no-store")
+        self.send_header("Content-Length", "2")
+        self.end_headers()
+        self.wfile.write(b"//")
+
+    def _strip_cond(self):
+        # 剥离条件请求头 → 永远 200 全量正文:
+        # 预览面板需改写 HTML 正文注入热更新脚本,304 无正文会使其放弃本次加载并重发(net::ERR_ABORTED)
+        for h in ("If-Modified-Since", "If-None-Match"):
+            if h in self.headers:
+                del self.headers[h]
+
+    def send_header(self, keyword, value):
+        # Python 3.14 的 send_head 用 "Content-type"(小写t),比较必须忽略大小写
+        if keyword.lower() == "content-type" and "text/html" in value:
+            super().send_header("Cache-Control", "no-store")
+        super().send_header(keyword, value)
+
     def do_GET(self):
         # 【加固】静态文件服务遇到浏览器中断连接(预览重载/取消)时静默忽略,
         # 避免异常冒泡导致线程 traceback 刷屏甚至进程退出
         try:
+            if self.path.split("?", 1)[0].startswith("/@vite/"):
+                self._vite()
+                return
+            self._strip_cond()
             super().do_GET()
         except (BrokenPipeError, ConnectionResetError, TimeoutError):
             pass
 
     def do_HEAD(self):
         try:
+            if self.path.split("?", 1)[0].startswith("/@vite/"):
+                self._vite()
+                return
+            self._strip_cond()
             super().do_HEAD()
         except (BrokenPipeError, ConnectionResetError, TimeoutError):
             pass