fix: v2.5 浏览器级可视化验证与修复闭环

- 新增 ui-shots.mjs/open-shot.mjs: puppeteer-core 驱动系统 Edge
  无头截图 + 页面 console/pageerror 捕获
- 修复 3D 解析失败: watch immediate 首跑 camera 未创建
- 修复 inferSashGlyphs 兜底 bboxOf 传段对象(286 NaN 扇矩形→0)
- profileGeo 自交轮廓重写为简单多边形
- 样板接口返回 total, 前端消费方适配(修复计数 49)
- 等高对齐落地到加入时(组合画布两单元同高底对齐)
- 渲染页默认模式随路由+自动选中首样板+选择器回填
- router 支持 token 直通(开发便捷)
- shadowMap.type 移除(three r186 已移除 PCFSoft)
- 综合截图验证: 样板库/组合/3D/工作台全部正常无报错
This commit is contained in:
HHY
2026-09-21 15:02:06 -07:00
parent 2cee9febe5
commit b6d7db58d6
19 changed files with 488 additions and 17 deletions
+1
View File
@@ -1,5 +1,6 @@
# CHANGELOG # CHANGELOG
- [2026-09-21 13:20] FIX: v2.5 浏览器级可视化验证与修复闭环。①新增 ui-shots.mjs/open-shot.mjs: puppeteer-core 驱动系统 Edge 无头截图+页面报错捕获(6 页面+开合状态); ②修复 watch immediate 首跑 camera 未创建(解析失败根因); ③修复 inferSashGlyphs 兜底 bboxOf 传段对象(286 个 NaN 扇矩形→0); ④profileGeo 自交轮廓重写; ⑤样板接口返回 total(修复计数 49)与前端各消费方; ⑥等高对齐落地到加入时(组合画布两单元同高底对齐); ⑦渲染页默认模式随路由+自动选中首样板+选择器回填; ⑧router 支持 token 直通(开发便捷); ⑨shadowMap.type 移除(r186); ⑩综合截图验证: 样板库/组合/3D/工作台全部正常, 无页面报错 (Files: frontend/src/**, src/huamenchuang/api/windoor.py, src/huamenchuang/scripts/seed.py, frontend/scripts/*)
- [2026-09-21 13:05] FIX: v2.4 组合画布回归自由拖拽——移除拖动松手后的强制自动排列(其剥夺了自由摆放能力); 吸附升级为标准六边缘对齐(拖动单元左/中/右×其余单元左/中/右, 上/中/下同理, 取最近者吸附+辅助线), 允许自由重叠摆放; 自动排列保留为可选整理按钮; 参考 Konva/Fabric 吸附标准模式与掘金/CSDN 实现分析 (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue) - [2026-09-21 13:05] FIX: v2.4 组合画布回归自由拖拽——移除拖动松手后的强制自动排列(其剥夺了自由摆放能力); 吸附升级为标准六边缘对齐(拖动单元左/中/右×其余单元左/中/右, 上/中/下同理, 取最近者吸附+辅助线), 允许自由重叠摆放; 自动排列保留为可选整理按钮; 参考 Konva/Fabric 吸附标准模式与掘金/CSDN 实现分析 (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue)
- [2026-09-21 12:35] FIX: v2.3 组合排列 3D 重叠修复——①CombineCanvas 新增 autoArrange: 全部单元按 X 序紧贴排布+底边对齐; ②拖动松手重叠检测: 移动单元与其他单元相交时自动重新排列(消除 3D 玻璃面穿插/z-fighting); ③CombineView 工具栏新增自动排列按钮; ④组合画布移除孤儿 @drop(归父容器) (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue) - [2026-09-21 12:35] FIX: v2.3 组合排列 3D 重叠修复——①CombineCanvas 新增 autoArrange: 全部单元按 X 序紧贴排布+底边对齐; ②拖动松手重叠检测: 移动单元与其他单元相交时自动重新排列(消除 3D 玻璃面穿插/z-fighting); ③CombineView 工具栏新增自动排列按钮; ④组合画布移除孤儿 @drop(归父容器) (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue)
- [2026-09-21 12:40] FIX: v2.2 前端完整性治理——引入 vue-tsc 全量类型检查并清零全部错误: ①组合画布 3D 扇面板解构键名错误(rect/s→rect/sash)致扇面板全挂(3D 混乱根因之一); ②WindoorCanvas isDoor 标注 cx 作用域泄漏; ③WindoorThreed rebuild 函数缺失(墙体开关点击报错)+wallBoxes 元组类型; ④量尺页 openCreate 未定义(点击报错); ⑤状态筛选 ref 类型纳入空串(el-select clearable 语义); ⑥CombineCanvas 移除孤儿 @drop(归父容器)+容器补 dragover.prevent(修复拖放入库); ⑦AIRenderCanvas style 属性名保留处理+表单联合类型; ⑧零散隐式 any 参数; 新增检查脚本 scripts/check-frontend.cjs(未定义引用+API 路径一致性比对) (Files: frontend/src/**, frontend/scripts/check-frontend.cjs) - [2026-09-21 12:40] FIX: v2.2 前端完整性治理——引入 vue-tsc 全量类型检查并清零全部错误: ①组合画布 3D 扇面板解构键名错误(rect/s→rect/sash)致扇面板全挂(3D 混乱根因之一); ②WindoorCanvas isDoor 标注 cx 作用域泄漏; ③WindoorThreed rebuild 函数缺失(墙体开关点击报错)+wallBoxes 元组类型; ④量尺页 openCreate 未定义(点击报错); ⑤状态筛选 ref 类型纳入空串(el-select clearable 语义); ⑥CombineCanvas 移除孤儿 @drop(归父容器)+容器补 dragover.prevent(修复拖放入库); ⑦AIRenderCanvas style 属性名保留处理+表单联合类型; ⑧零散隐式 any 参数; 新增检查脚本 scripts/check-frontend.cjs(未定义引用+API 路径一致性比对) (Files: frontend/src/**, frontend/scripts/check-frontend.cjs)
+341
View File
@@ -19,6 +19,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.1.0", "@vitejs/plugin-vue": "^5.1.0",
"puppeteer-core": "^25.11.0",
"typescript": "^5.5.0", "typescript": "^5.5.0",
"vite": "^5.4.0", "vite": "^5.4.0",
"vue-tsc": "^3.3.11" "vue-tsc": "^3.3.11"
@@ -547,6 +548,35 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@puppeteer/browsers": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@puppeteer/browsers/-/browsers-3.2.2.tgz",
"integrity": "sha512-q2BU4YfO9h/Wt7IcWPcggpOOqLk2Tbs1hDwolvKZrweRjy751OJBKMN9zO5bfD0pzU7X/tvKw/exQds4pM/LOg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.8.4",
"yargs": "^18.0.0"
},
"bin": {
"browsers": "lib/main-cli.js"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1",
"yauzl": "^2.10.0 || ^3.4.0"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
},
"yauzl": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.63.4", "version": "4.63.4",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz",
@@ -1225,6 +1255,32 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ansi-regex": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.3.0.tgz",
"integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/async-validator": { "node_modules/async-validator": {
"version": "4.2.5", "version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@@ -1262,6 +1318,56 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/chromium-bidi": {
"version": "17.0.2",
"resolved": "https://registry.npmmirror.com/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
"integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"engines": {
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cliui/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1312,6 +1418,13 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/devtools-protocol": {
"version": "0.0.1680125",
"resolved": "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1680125.tgz",
"integrity": "sha512-rVARKAvOFJCFkvgX01VLvYqKIvt+cRCqvUxIhB2oAqVGPalxk3w3sb5TUGDagpow/EFblBpm/Zl/29QA2dd+jA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1352,6 +1465,13 @@
"vue": "^3.3.7" "vue": "^3.3.7"
} }
}, },
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/entities": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@@ -1448,6 +1568,16 @@
"@esbuild/win32-x64": "0.21.5" "@esbuild/win32-x64": "0.21.5"
} }
}, },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
@@ -1520,6 +1650,29 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.7.0",
"resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz",
"integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1695,6 +1848,23 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true,
"license": "MIT"
},
"node_modules/modern-tar": {
"version": "0.8.5",
"resolved": "https://registry.npmmirror.com/modern-tar/-/modern-tar-0.8.5.tgz",
"integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
@@ -1817,6 +1987,24 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/puppeteer-core": {
"version": "25.11.0",
"resolved": "https://registry.npmmirror.com/puppeteer-core/-/puppeteer-core-25.11.0.tgz",
"integrity": "sha512-Ujym7JbxoAqanaxGcHgmTomfhC01HfDCSuGZ8Hs9boHrDjzbNVMnFRJo2KtbDT9LCRgeJoFnkn9JQZ41eEUrLw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.2",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1680125",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.3",
"ws": "^8.21.3"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.63.4", "version": "4.63.4",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.4.tgz", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.4.tgz",
@@ -1872,12 +2060,52 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/string-width": {
"version": "8.2.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.2.2.tgz",
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/three": { "node_modules/three": {
"version": "0.186.0", "version": "0.186.0",
"resolved": "https://registry.npmmirror.com/three/-/three-0.186.0.tgz", "resolved": "https://registry.npmmirror.com/three/-/three-0.186.0.tgz",
"integrity": "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ==", "integrity": "sha512-cr/fIM2ddMSVbYVgkfD4jLJv7Fh/8ZTjvo+7gQeSVGUZHxpx9FDwoL5iC7hUz/LiRA8wMbqfnb90xKfm1/HHkQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmmirror.com/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
@@ -2043,6 +2271,119 @@
"peerDependencies": { "peerDependencies": {
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.3",
"resolved": "https://registry.npmmirror.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz",
"integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "18.2.0",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-18.2.0.tgz",
"integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^8.2.1",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }
+1
View File
@@ -20,6 +20,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.1.0", "@vitejs/plugin-vue": "^5.1.0",
"puppeteer-core": "^25.11.0",
"typescript": "^5.5.0", "typescript": "^5.5.0",
"vite": "^5.4.0", "vite": "^5.4.0",
"vue-tsc": "^3.3.11" "vue-tsc": "^3.3.11"
+24
View File
@@ -0,0 +1,24 @@
// 开合状态 3D 截图: node this-file.cjs <token>
import puppeteer from 'puppeteer-core'
const browser = await puppeteer.launch({
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
headless: 'new',
args: ['--window-size=1440,900', '--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'],
defaultViewport: { width: 1440, height: 900 },
})
const page = await browser.newPage()
const token = process.argv[2]
await page.goto(`http://localhost:8000/threed?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
await new Promise(r => setTimeout(r, 2200))
await page.evaluate(() => {
const slider = document.querySelector('.ctrl input[type=range]')
if (slider) {
slider.value = '100'
slider.dispatchEvent(new Event('input'))
slider.dispatchEvent(new Event('change'))
}
})
await new Promise(r => setTimeout(r, 1000))
await page.screenshot({ path: 'D:/Code/huamenchuang/shots/07-threed-open.png' })
console.log('✓ 开合状态截图完成')
await browser.close()
+74
View File
@@ -0,0 +1,74 @@
// UI 可视化验证: 真实浏览器截图 + 页面报错捕获
// 用法: node scripts/ui-shots.mjs <登录token>
import puppeteer from 'puppeteer-core'
import fs from 'fs'
const EDGE = 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
const BASE = 'http://localhost:8000'
const OUT = 'D:/Code/huamenchuang/shots'
fs.mkdirSync(OUT, { recursive: true })
const token = process.argv[2]
if (!token) { console.error('缺少 token'); process.exit(1) }
const browser = await puppeteer.launch({
executablePath: EDGE,
headless: 'new',
args: ['--window-size=1440,900', '--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'],
defaultViewport: { width: 1440, height: 900 },
})
const page = await browser.newPage()
page.on('console', m => {
const t = m.text()
if (m.type() === 'error' || t.includes('THREE') || t.includes('NaN')) console.log(`[页面.${m.type()}] ${t.slice(0, 160)}`)
})
page.on('pageerror', e => console.log(`[JS异常] ${String(e).slice(0, 200)}`))
const sleep = ms => new Promise(r => setTimeout(r, ms))
async function shot(name) {
await page.screenshot({ path: `${OUT}/${name}.png` })
console.log(`✓ 截图 ${name}.png`)
}
// 1. 登录(通过 token 直通)进入样板库
await page.goto(`${BASE}/templates?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
await sleep(1500)
await shot('01-templates')
// 2. 组合画布: 加入两个样板 → 截图 → 点击画布选中 → 截图
await page.goto(`${BASE}/combine?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
await sleep(1000)
await page.waitForSelector('.lib-item')
await page.click('.lib-item')
await sleep(500)
const items = await page.$$('.lib-item')
if (items[3]) { await items[3].click(); await sleep(500) }
await shot('02-combine-two')
// 点击画布中心选中第一个单元
const box = await (await page.$('.canvas-box')).boundingBox()
if (box) {
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2)
await sleep(400)
await shot('03-combine-selected')
}
// 3D 预览切换(点击 3D 单选钮)
const radios = await page.$$('label.el-radio-button')
for (const r of radios) {
const txt = await r.evaluate(el => el.textContent.trim())
if (txt === '3D') { await r.click(); break }
}
await sleep(1200)
await shot('04-combine-3d')
// 3. 3D 展示页(独立)
await page.goto(`${BASE}/threed?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
await sleep(2000)
await shot('05-threed')
// 4. 工作台
await page.goto(`${BASE}/dashboard?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
await sleep(1200)
await shot('06-dashboard')
await browser.close()
console.log('完成')
+3
View File
@@ -44,6 +44,9 @@ router.beforeEach(to => {
const auth = useAuthStore() const auth = useAuthStore()
// 公开页:分享订单 // 公开页:分享订单
if (to.path.startsWith('/share/')) return true if (to.path.startsWith('/share/')) return true
// 开发便捷:URL 携带 token 直通登录(生产环境慎用)
const qk = to.query.token as string | undefined
if (qk && !auth.isLogin) auth.setSession(qk, '游客')
if (to.path !== '/login' && !auth.isLogin) return '/login' if (to.path !== '/login' && !auth.isLogin) return '/login'
return true return true
}) })
+1 -1
View File
@@ -127,7 +127,7 @@ function download(i: number) {
} }
onMounted(async () => { onMounted(async () => {
templates.value = await api<any>('get', '/api/windoor/templates?page=1&page_size=254') templates.value = (await api<any>('get', '/api/windoor/templates?page=1&page_size=254')).data
Object.assign(form, { ai_style: form.ai_style, ai_time: form.ai_time, scene: form.scene }, Object.assign(form, { ai_style: form.ai_style, ai_time: form.ai_time, scene: form.scene },
await api<any>('get', '/api/rendering-info')) await api<any>('get', '/api/rendering-info'))
}) })
+18 -2
View File
@@ -110,6 +110,7 @@ const units = ref<UnitMeta[]>([])
const selectedUid = ref<number | null>(null) const selectedUid = ref<number | null>(null)
const saveName = ref('') const saveName = ref('')
const saving = ref(false) const saving = ref(false)
const equalHeight = ref(true)
const canvasRef = ref() const canvasRef = ref()
let uidSeq = 1 let uidSeq = 1
const rev = ref(0) const rev = ref(0)
@@ -123,7 +124,7 @@ const combinedJson = computed(() => {
// ---------- 样板库 ---------- // ---------- 样板库 ----------
async function loadLib() { async function loadLib() {
lib.value = await api<any>('get', `/api/windoor/templates?page=1&page_size=254&is_default=1&name=${keyword.value}`) lib.value = (await api<any>('get', `/api/windoor/templates?page=1&page_size=254&is_default=1&name=${keyword.value}`)).data
} }
// ---------- 加入样板 ---------- // ---------- 加入样板 ----------
@@ -155,7 +156,22 @@ function addShapesAt(detail: any, cx: number | null, cy: number | null, name: st
const uid = uidSeq++ const uid = uidSeq++
const newShapes = shapesOf(detail) const newShapes = shapesOf(detail)
if (!newShapes.length) { ElMessage.warning('该样板没有可用的几何数据'); return false } if (!newShapes.length) { ElMessage.warning('该样板没有可用的几何数据'); return false }
const bb = bbOf(newShapes)! let bb = bbOf(newShapes)!
// 等高对齐: 新单元 Y 向缩放到参考单元(首个)高度, 锚定自身底边
if (equalHeight.value && units.value.length) {
const refBb = bbOf(shapes.value.filter(s => s.__uid === units.value[0].uid))
if (refBb && bb.y1 - bb.y0 > 0) {
const f = (refBb.y1 - refBb.y0) / (bb.y1 - bb.y0)
if (Number.isFinite(f) && f > 0) {
const scY = (p: any) => { p.y = bb.y0 + (p.y - bb.y0) * f }
for (const s of newShapes) {
for (const r of (s.polygon?.polygon ?? [])) for (const seg of r) { scY(seg.ps); scY(seg.pe) }
for (const sp of (s.mm?.sp ?? [])) scY(sp.line.pt)
for (const sub of (s.sm ?? [])) if (sub.bd) { scY(sub.bd.from); scY(sub.bd.to) }
}
}
}
}
let dx = 0, dy = 0 let dx = 0, dy = 0
if (cx !== null && cy !== null) { if (cx !== null && cy !== null) {
dx = cx - (bb.x0 + bb.x1) / 2 dx = cx - (bb.x0 + bb.x1) / 2
+1 -1
View File
@@ -218,7 +218,7 @@ async function openCreate() {
createVisible.value = true createVisible.value = true
const [cs, ts] = await Promise.all([ const [cs, ts] = await Promise.all([
api<any>('get', '/api/contracts?page=1&page_size=100'), api<any>('get', '/api/contracts?page=1&page_size=100'),
api<any>('get', '/api/windoor/templates?page=1&page_size=254'), api<any>('get', '/api/windoor/templates?page=1&page_size=254').then(t => (templateOptions.value = t.data)),
]) ])
contractOptions.value = cs.contracts?.data ?? [] contractOptions.value = cs.contracts?.data ?? []
templateOptions.value = ts templateOptions.value = ts
+9 -3
View File
@@ -46,11 +46,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { api } from '../api' import { api } from '../api'
const route = useRoute()
import WindoorThreed from '../components/WindoorThreed.vue' import WindoorThreed from '../components/WindoorThreed.vue'
import RenderCanvas from '../components/RenderCanvas.vue' import RenderCanvas from '../components/RenderCanvas.vue'
const mode = ref<'render' | 'threed'>('render') const mode = ref<'render' | 'threed'>(route.path.includes('threed') ? 'threed' : 'render')
const templates = ref<any[]>([]) const templates = ref<any[]>([])
const templateId = ref<number | undefined>() const templateId = ref<number | undefined>()
const drawing = ref<string>('') const drawing = ref<string>('')
@@ -63,9 +66,10 @@ const sceneBg = computed(() => ({
} as Record<string, string>)[setting.scene] ?? '#eee') } as Record<string, string>)[setting.scene] ?? '#eee')
async function loadTemplate(tid: number | undefined) { async function loadTemplate(tid: number | undefined) {
if (!tid) { drawing.value = ''; return } if (!tid) { drawing.value = ''; templateId.value = undefined; return }
const t = await api<any>('get', `/api/windoor/templates/${tid}`) const t = await api<any>('get', `/api/windoor/templates/${tid}`)
drawing.value = t.drawing_data drawing.value = t.drawing_data
templateId.value = tid
} }
async function saveSetting() { async function saveSetting() {
@@ -73,8 +77,10 @@ async function saveSetting() {
} }
onMounted(async () => { onMounted(async () => {
templates.value = await api<any>('get', '/api/windoor/templates?page=1&page_size=254') templates.value = (await api<any>('get', '/api/windoor/templates?page=1&page_size=254')).data
Object.assign(setting, await api<any>('get', '/api/rendering-info')) Object.assign(setting, await api<any>('get', '/api/rendering-info'))
// 自动选中首个样板,页面即刻有内容
if (templates.value.length && !templateId.value) await loadTemplate(templates.value[0].id)
}) })
function print() { window.print() } function print() { window.print() }
+2 -3
View File
@@ -101,9 +101,8 @@ async function load() {
try { try {
const qs = `page=${page.value}&page_size=${page_size}&name=${name.value}&is_default=${tab.value === 'official' ? 1 : 0}${catelogId.value ? `&catelog_id=${catelogId.value}` : ''}` const qs = `page=${page.value}&page_size=${page_size}&name=${name.value}&is_default=${tab.value === 'official' ? 1 : 0}${catelogId.value ? `&catelog_id=${catelogId.value}` : ''}`
const d = await api<any>('get', `/api/windoor/templates?${qs}`) const d = await api<any>('get', `/api/windoor/templates?${qs}`)
rows.value = d rows.value = d.data
// 接口无分页元数据时按返回量估计 total.value = d.total
total.value = d.length === page_size ? (page.value - 1) * page_size + d.length + 1 : (page.value - 1) * page_size + d.length
} finally { loading.value = false } } finally { loading.value = false }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+9 -3
View File
@@ -44,15 +44,21 @@ async def search_templates(
conds.append(WindoorTemplate.catelog_id == catelog_id) conds.append(WindoorTemplate.catelog_id == catelog_id)
if is_default is not None: if is_default is not None:
conds.append(WindoorTemplate.is_default == is_default) conds.append(WindoorTemplate.is_default == is_default)
stmt = (select(WindoorTemplate).where(*conds) from sqlalchemy import func as _func
where = *conds,
total = (await db.execute(select(_func.count()).select_from(WindoorTemplate).where(*where))).scalar_one()
stmt = (select(WindoorTemplate).where(*where)
.order_by(WindoorTemplate.id).offset((page - 1) * page_size).limit(page_size)) .order_by(WindoorTemplate.id).offset((page - 1) * page_size).limit(page_size))
items = list((await db.execute(stmt)).scalars()) items = list((await db.execute(stmt)).scalars())
return ok(data=[{ return ok(data={
"total": total, "page": page, "page_size": page_size,
"data": [{
"id": t.id, "name": t.name, "pic_url": t.pic_url, "catelog_id": t.catelog_id, "id": t.id, "name": t.name, "pic_url": t.pic_url, "catelog_id": t.catelog_id,
"note": t.note, "times": t.times, "note": t.note, "times": t.times,
"low_width": t.low_width, "high_width": t.high_width, "low_width": t.low_width, "high_width": t.high_width,
"low_height": t.low_height, "high_height": t.high_height, "low_height": t.low_height, "high_height": t.high_height,
} for t in items]) } for t in items],
})
@router.post("/templates") @router.post("/templates")