4 Commits
v2.0 ... main

Author SHA1 Message Date
wangruiguo
f343b26444 v2.4: 网页版适配 Odoo 网站嵌入
- URL 深度链接:?series= 打开系列 / ?model= 定位型号 / ?embed=1 隐藏自身顶栏 / ?nav=0 隐藏侧边栏 / ?lang= 切语言
- 路径自适应:VIEWER_BASE 与模型路径从 location.pathname 推导,支持子路径部署(原来硬编码 /viewer/ 和 /step/ 会在子路径下 404)
- embed 模式样式:隐藏 header/footer,让 iframe 内更干净
- 手册页图片点击放大:全屏弹窗,← → 翻页,Esc/点遮罩关闭
- 数据表 PDF 弹窗预览:原为直接下载,改为内联预览(弹窗内保留下载入口)
- 数据表按钮加内联 SVG 图标(PDF + 放大镜),区别于纯下载按钮
- 隐藏「返回系列总览」按钮(嵌入模式下属多余)

注:本次改动服务于 OneBot 官网产品页的选型 tab 嵌入。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 02:57:34 +08:00
wangruiguo
e030b65104 v2.3: NX 路径自动探测 + 修复批处理行尾被破坏
NX 路径自动探测 (换机不用改脚本)
- 新增 tools/find-nxbin.ps1, 三级探测: 环境变量 ONEBOT_NXBIN > 项目内
  nxbin.txt > C:\Program Files\Siemens\NX*\NXBIN
- 多版本共存取版本号最大者 (实测 NX2506+NX2512 共存时正确选 2512),
  故 2506 → 2512 换机无需改任何脚本
- _start_all.bat 接入探测, 并优雅降级: 找不到 NX 只警告并跳过 worker,
  网页/GenServer 照常启动 (不再因 NX 缺失导致整个启动失败)

修复批处理行尾 (本次改动引入的回归)
- 用工具写 .bat 时默认写成 LF, 而 cmd.exe 必须用 CRLF: 裸 LF 会把
  `if not defined X (` 拆成乱码, 报 `'ned' 不是内部或外部命令` / `此时不应有 else`
- _start_all.bat / _start_web.bat 因此被破坏, 已恢复 CRLF
- 新增 tools/fix-lineend.ps1 一键检查/修正 (幂等, 仿 fix-bom.ps1);
  该检查额外抓出 2 个同样有问题的文件并一并修正:
  一键导出CRA问卷.bat / tools/build-genserver.bat (内容未变, 仅行尾)
- 新增 .gitattributes 声明 *.bat/*.cmd eol=crlf, 防止 checkout 后再被转成 LF

验证
- find-nxbin.ps1 实测探测到 C:\Program Files\Siemens\NX2506\NXBIN
- 多版本排序逻辑实测选中 2512
- 批处理行尾修复后实际运行通过
- 两个新 ps1 语法自检通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-16 14:00:38 +08:00
wangruiguo
e94d2aebc9 v2.2: 桌面版同步 DESTACO 风格选型 + 网页预览后台启动
桌面版 (仅网页版 v2.1 已做的部分, 本次补齐桌面端, 两端语义一致)
- 新增 OptionTile 控件类: 三态选项块 (可选/已选/不可选), 替代 ComboBox 下拉
  不可选取值置灰+删除线但保留占位 (让用户看到选项存在, 而非从列表消失);
  悬停青色高亮, 再点一次取消选择
- 新增零件号实时条: 复用与网页版同一套「候选型号最长公共前缀」算法,
  已确定片段深青实心 + 待定位数虚化 + 进度条, 选满唯一命中时整块转青色实心
- 维度标题后回显当前已选值
- range 参数 (连续域) 保留可编辑下拉, 无法枚举成卡片
- 参数联动仍复用 Configurator.AllowedOptions, 规则引擎未改动

网页预览后台启动 (无 PowerShell 窗口)
- 新增 tools/serve-web-bg.ps1: Start-Process -WindowStyle Hidden 启动,
  带 -Stop / -Status; 用命令行匹配定位进程 (比只看 PID 文件可靠)
- _start_web.bat / _start_all.bat 改用之, 不再弹窗口

工具
- ui-diagnose.ps1 增强: 原只检查 _boxes (下拉), 现同时检查选项块数量、
  每维度是否有可选值、零件号条输出 —— 可自动发现"某维度全被置灰无从下手"
- 新增 assess-combo-risk.py: 用量化口径评估生成候选的风险
  (以同缸径真实数据值域为官方值域的代理)
- 新增 make-pending-list.py: 重写待补清单, 增加 nearest_real / diff_from_nearest
  列, 每个候选与其最近的真实型号并列, 便于逐个核实

文档
- 新增 工作总结-2026-09.md: 两阶段工作回顾、发现的问题、踩坑经验

验证
- 编译首次通过; UI 诊断 40/40 系列 0 问题
- 零件号条实测: KC0032-10 / ULP32020AEK0 / UAGP075OK120WS0 / LAE140NN31 均完整;
  UCBP 正确提示"该组合对应 2 个型号" (暴露已知数据问题)

踩坑记录
- PowerShell 变量名大小写不敏感: $root 与 $Root 是同一变量, 互相覆盖导致
  启动脚本指向错误目录 (改用 $projRoot)
- .ps1 必须带 UTF-8 BOM, 否则 PS 5.1 按 ANSI 读, 中文注释破坏解析

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 18:27:11 +08:00
wangruiguo
fc5ee1dd36 v2.1: DESTACO 风格选型界面 + 参数组合补全(缺组合也能出零件号)
网页版选型改版 (仅网页版, 桌面版待同步)
- 新增零件号实时条: 边选边拼, 已确定片段实心高亮, 未定片段虚化, 进度条随选推进;
  浅色配色 (初版深色块与页面不协调, 已改); 选满且唯一命中时整块转青色实心
- 技术方案: 40 系列中仅 KC 有编码模板 → 改用「当前候选型号的最长公共前缀」,
  已验证 40/40 系列前缀单调增长(不回退)
- 参数选择由下拉菜单改为可视化选项块: 悬停抬升+青色投影, 选中青色填充+✓,
  不可选取值置灰(虚线+划线, 不消失, 让用户看到选项存在但当前组合不适用),
  再点一次取消; 有说明的显示代号+说明, 纯数值用紧凑文字块
- 参数联动仍复用 allowedOptions 规则引擎, 语义未变, 只改渲染

参数组合补全
- 原逻辑: 必须有真实数模才能生成型号 → 库里没有的组合选不出来也出不了号
- infer-sku-template.py 从已有型号反推编码模板, 得 23 个系列 100% 可复现
- verify-templates.py 作为门禁: 用模板重渲染每个已有型号必须逐字一致 ——
  该检查抓出 ULM/ULP 行程补零位数写错(2位写3位), 否则会生成 3000 个不存在的零件号
- gen-missing-combos.py 补全 8 个系列共 3157 个型号 (全库 54136 → 57293)
- 占位数模: 3157 个型号共用 1 个 PLACEHOLDER.step(4.5KB), 构建器按路径打包只存一份,
  数据包仅 2.10GB → 2.14GB (中途用硬链接方案踩坑: NTFS 上限 1024, 超出退化为拷贝白占 500MB)
- _pending_models.csv 输出待补数模清单, 供 AI 补完后替换 step_file

新增脚本: infer-sku-template / verify-templates / quantify-combo-gap /
          gen-missing-combos / gen-placeholder-step / check-combo-unique
新增文档: docs/更改说明-v2.1.md

发现未处理 (已记录在更改说明):
- 夹紧缸 9 系列一组参数对应多型号 (310 个型号, 编码含参数外尾缀, 旧码/新码混用)
- UAGP/LAGP 参数顺序与官方编码结构不符 (传感器应在转角之前)
- 17 个系列因模板不干净未补全 (宁可少补, 不生成可能不存在的零件号)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 02:14:55 +08:00
31 changed files with 8793 additions and 87 deletions

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
# 行尾约定: .bat/.cmd 交给 cmd.exe 解析, 必须是 CRLF —— 裸 LF 会让 cmd 误解析
# (踩过: 工具写文件默认 LF, 导致 `if not defined X (` 被拆成乱码命令, 批处理直接失败)。
# 下面显式声明, 避免 checkout 后又被转成 LF。
*.bat text eol=crlf
*.cmd text eol=crlf
# PowerShell 脚本走 PS 解析, 行尾不敏感, 保持平台默认
*.ps1 text

1
.gitignore vendored
View File

@@ -53,3 +53,4 @@ onebot-data/tools/_scan_cache.json
__pycache__/ __pycache__/
.probe/ .probe/
_iconprev/ _iconprev/
.serve-web.pid

View File

@@ -27,12 +27,22 @@ start "GenServer" /min "%~dp0bin\GenServer.exe" "%~dp0genserver-local\genserver-
ping -n 3 127.0.0.1 >nul ping -n 3 127.0.0.1 >nul
echo [4/6] start NX worker ... echo [4/6] start NX worker ...
start "NXworker" /min "C:\Program Files\Siemens\NX2506\NXBIN\run_journal.exe" "%~dp0sample-data\tools\worker_journal.vb" rem NX 路径自动探测 (环境变量 ONEBOT_NXBIN > 项目内 nxbin.txt > C:\Program Files\Siemens\NX*)
rem 多版本共存时取版本号最大者, 便于 2506/2512 换机不用改脚本
set "NXBIN="
for /f "usebackq delims=" %%i in (`powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\find-nxbin.ps1" -Quiet`) do set "NXBIN=%%i"
if not defined NXBIN (
echo [WARN] 未找到 NX ^(run_journal.exe^), 跳过 NX worker ^(网页/GenServer 不受影响^)
echo 可手动指定: set ONEBOT_NXBIN=D:\path\to\NXBIN 或写一行到 nxbin.txt
) else (
echo NX: %NXBIN%
start "NXworker" /min "%NXBIN%\run_journal.exe" "%~dp0sample-data\tools\worker_journal.vb"
)
ping -n 3 127.0.0.1 >nul ping -n 3 127.0.0.1 >nul
echo [5/6] start web server (port 8080) ... echo [5/6] start web server (port 8080, 后台无窗口) ...
start "OnebotWeb" /min powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\serve-web.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\serve-web-bg.ps1"
ping -n 4 127.0.0.1 >nul ping -n 2 127.0.0.1 >nul
echo [6/6] health check ... echo [6/6] health check ...
curl.exe -s http://localhost:8899/health curl.exe -s http://localhost:8899/health

View File

@@ -2,16 +2,17 @@
setlocal setlocal
cd /d "%~dp0" cd /d "%~dp0"
echo ==== start web server (default root = release\web_2026.08, port 8080) ==== echo ==== 启动网页预览服务器 (后台运行, 无 PowerShell 窗口) ====
start "OnebotWeb" powershell -NoProfile -ExecutionPolicy Bypass -File "tools\serve-web.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "tools\serve-web-bg.ps1" -Root "release\web_2026.09"
timeout /t 4 /nobreak >nul
echo ==== check web (8080) + genserver (8899) ====
curl.exe -s -o nul -w "web : %%{http_code}\n" http://localhost:8080/index.html
curl.exe -s http://localhost:8899/health
echo. echo.
echo ==== 检查服务 ====
curl.exe -s -o nul -w "web : %%{http_code}\n" http://localhost:8080/index.html
echo ==== done. browser: http://localhost:8080 ==== echo.
echo (keep the OnebotWeb window open; close it to stop the server) echo 浏览器访问: http://localhost:8080
echo 停止服务: powershell -File tools\serve-web-bg.ps1 -Stop
echo 查看状态: powershell -File tools\serve-web-bg.ps1 -Status
echo.
pause
endlocal endlocal

158
docs/更改说明-v2.1.md Normal file
View File

@@ -0,0 +1,158 @@
# v2.1 更改说明
发布日期:2026-09-15
上一版:v2.0(4 分类真实数据替换 + 手册彩色重做 + 网页版改版)
本版聚焦两件事:**选型交互改为 DESTACO 风格**,以及**参数组合补全(缺组合也能出零件号)**。
---
## 一、选型界面:DESTACO 风格(网页版)
参照 DESTACO 配置器的交互做了改版。**只改网页版**,桌面版保持原样,后续再同步。
### 1. 零件号实时条(新增)
参数面板顶部新增一条零件号显示区,**边选边拼**:
- 已确定的片段用深青色实心显示,未确定的用浅灰虚化占位
- 底部进度条随选择推进
- 选满且唯一命中时,整条转为青色实心渐变(完成反馈)
- 浅色配色,与页面整体风格一致(初版曾用深色块,观感不协调,已改)
**技术说明**:40 个系列中只有 KC 有编码模板,无法靠模板拼号。改用**「当前候选型号的最长公共前缀」**——
选的越多候选越少、公共前缀越长,自然拼出完整零件号。已验证 40/40 系列前缀单调增长(不回退)。
### 2. 参数选择改为可视化选项块(替代下拉菜单)
- 每个维度显示为**选项块网格**,而不是 `<select>` 下拉
- 悬停抬升 + 青色投影;选中青色填充 + ✓ 角标
- **不可选的取值置灰**(虚线框 + 划线),而不是从列表消失——用户能看到"该选项存在但当前组合不适用"
- 再点一次可取消选择
- 有说明文字的(如传感器 `K` = 电子 PNP M12)显示代号大字 + 说明小字;纯数值显示紧凑文字块
- 连续域参数(range,如 KC 的行程)保留输入框 + 标准档位
**语义未变**:参数联动仍复用原有 `allowedOptions` 规则引擎,只改了渲染方式。
---
## 二、参数组合补全:库中缺的组合现在也能出零件号
### 背景
原逻辑是「**必须有真实数模才能生成型号**」:型号表由真实 STEP 文件扫描而来,构建器要求每个型号的数模必须存在。
结果是——**库里没有的组合,用户选不出来,也拿不到零件号**。
### 做法
**1. 从已有型号反推编码模板**
40 个系列里只有 1 个(KC)有现成模板。写了 `infer-sku-template.py` 从已有型号反推编码规则,
得到 **23 个系列 100% 可复现**的模板,例如:
```
ULP{size}{stroke:pad3}{rod}{orient}{sensor}0 → ULP + 32 + 020 + A + E + K + 0
ULA{size}{stroke:pad3}{rod}{mount}{sensor}0
USSL{size}{pcmodel}{hook_pos}{pin_dia}{locating}{prog}{sensor}0
```
**并加了自检门禁**(`verify-templates.py`):用模板重新渲染**每一个已有型号**,必须与库中编码逐字一致。
这道检查抓出了一处真实错误——ULM/ULP 的行程补零位数写错(2 位写成 3 位),
**若无此校验会生成 3,000 个不存在的零件号发给客户**。
**2. 补齐缺失组合**
只对模板验证通过的 23 个系列补全(其余 17 个系列因旧码/新码混用、编码结构特殊,未补)。
| 系列 | 补全前 | 补全后 |
|---|---|---|
| ULP | 168 | **2,970** |
| ULM | 70 | 200 |
| ULA | 19 | 192 |
| ULL | 14 | 40 |
| ULY / ULZ | 各 10 | 各 20 |
| USSL | 7 | 12 |
| ULT | 3 | 4 |
| **合计新增** | | **+3,157 个型号** |
全库型号数:**54,136 → 57,293**。构建通过(0 错误 0 警告)。
**3. 占位数模**
新增的 3,157 个型号共用 **1 个 `PLACEHOLDER.step`**(4.5KB 方块)。
构建器按路径打包,同一路径只存一份,因此**数据包仅从 2.10GB 增至 2.14GB**。
> 中途走过的弯路:先按"每型号一个硬链接文件"实现,但 NTFS 硬链接上限为 1024,
> 超出部分退化为真实拷贝,白占约 500MB。改为共享单文件后归零。
**4. 待补数模清单**
`onebot-data/catalog/_pending_models.csv` —— 3,157 行,列出每个待补型号及其参数,供 AI 补真实数模。
补完后把该行的 `step_file` 从 `PLACEHOLDER.step` 改为 `<型号>.step` 即可。
---
## 三、新增脚本
| 脚本 | 用途 |
|---|---|
| `infer-sku-template.py` | 从已有型号反推编码模板 |
| `verify-templates.py` | 校验模板能 100% 复现已有型号(生成新号前的门禁) |
| `quantify-combo-gap.py` | 统计参数组合缺口规模 |
| `gen-missing-combos.py` | 补全缺失组合(`--report` 只统计,`--write` 落盘) |
| `gen-placeholder-step.py` | 生成占位数模方块 |
| `check-combo-unique.py` | 检查"参数组合 ↔ 型号"是否一对一 |
---
## 四、发现但未处理的问题
### 1. 夹紧缸 9 个系列:一组参数对应多个型号
| 系列 | 变体数 | 重复组数 | 一组最多对应 |
|---|---|---|---|
| UCBP | 358 | 46 组 | 18 个型号 |
| UCBM | 266 | 26 组 | 15 个型号 |
| UCBH | 61 | 7 组 | 5 个型号 |
| UCBQ / UCDP / UCIM / UCIP / UFL / UCBT | — | 2-4 组 | 2-6 个型号 |
共 **310 个型号**受影响。原因是这些型号的编码里有**参数之外的尾缀**(版本号/变体标记),
如 `UCBP25AK00` vs `UCBP25AK010`(差末尾版本号)、`UBP25010` vs `UCBP25BK0`(旧码/新码混用)。
**影响**:用户选完参数后仍无法唯一确定型号。界面已提示"该组合对应 N 个型号",但需后续补维度或清理。
### 2. UAGP / LAGP 参数顺序与官方编码结构不符
官方订购码顺序为 `规格 - 台位 - 传感器 - 转角 - 制动 - 接口`,
当前 meta 里传感器排在最后。不影响选型正确性(参数按名索引),但导致编码模板推不出来,
且界面展示顺序与官方不一致。
### 3. 未补全的 17 个系列
模板验证未通过的系列保持原状——**宁可少补,也不生成可能不存在的零件号**。
---
## 五、涉及文件
| 文件 | 改动 |
|---|---|
| `web/app.js` | 零件号实时条 + 选项块渲染(+148 行) |
| `web/style.css` | SKU 条与选项块样式(+111 行) |
| `web/index.html` | 新增 SKU 条容器 |
| `onebot-data/catalog/csv/*.csv` | 8 个系列追加 3,157 行 |
| `onebot-data/catalog/step/PLACEHOLDER.step` | 新增占位数模 |
| `onebot-data/catalog/_pending_models.csv` | 新增待补数模清单 |
| `onebot-data/tools/*.py` | 新增 6 个数据管线脚本 |
---
## 六、验证方式
1. **构建**:`bin\OnebotCatalog.exe --buildfull onebot-data\catalog sample\OnebotCatalog_2026.08.opc`
→ 应输出 `系列数: 40 | 变体数: 57293`,`校验: 0 错误, 0 警告`
2. **网页预览**:`tools\serve-web.ps1` → 打开 `http://localhost:8080`(Ctrl+F5 强刷)
- 选定位销 PIN → ULP → 选一个原来选不到的组合(如 63 缸 / 110 行程)
- 应能看到零件号实时拼出、可下载(下载到的是占位方块)
3. **模板门禁**:`PYTHONUTF8=1 python onebot-data\tools\verify-templates.py`
→ 应输出「所有模板对已有型号 100% 复现」

File diff suppressed because it is too large Load Diff

View File

@@ -18,3 +18,176 @@ ULA63040AD0,63,40,A,,D,ULA63040AD0.step
ULA63040AK0,63,40,A,,K,ULA63040AK0.step ULA63040AK0,63,40,A,,K,ULA63040AK0.step
ULA63050AK0,63,50,A,,K,ULA63050AK0.step ULA63050AK0,63,50,A,,K,ULA63050AK0.step
ULA63060AK0,63,60,A,,K,ULA63060AK0.step ULA63060AK0,63,60,A,,K,ULA63060AK0.step
ULA40020AD0,40,20,A,,D,PLACEHOLDER.step
ULA40020AK0,40,20,A,,K,PLACEHOLDER.step
ULA40020GSD0,40,20,G,S,D,PLACEHOLDER.step
ULA40020GD0,40,20,G,,D,PLACEHOLDER.step
ULA40020GK0,40,20,G,,K,PLACEHOLDER.step
ULA40020NSD0,40,20,N,S,D,PLACEHOLDER.step
ULA40020ND0,40,20,N,,D,PLACEHOLDER.step
ULA40020NK0,40,20,N,,K,PLACEHOLDER.step
ULA40020PSD0,40,20,P,S,D,PLACEHOLDER.step
ULA40020PD0,40,20,P,,D,PLACEHOLDER.step
ULA40020PK0,40,20,P,,K,PLACEHOLDER.step
ULA40040AD0,40,40,A,,D,PLACEHOLDER.step
ULA40040AK0,40,40,A,,K,PLACEHOLDER.step
ULA40040GD0,40,40,G,,D,PLACEHOLDER.step
ULA40040GK0,40,40,G,,K,PLACEHOLDER.step
ULA40040ND0,40,40,N,,D,PLACEHOLDER.step
ULA40040NK0,40,40,N,,K,PLACEHOLDER.step
ULA40040PD0,40,40,P,,D,PLACEHOLDER.step
ULA40040PK0,40,40,P,,K,PLACEHOLDER.step
ULA40015ASD0,40,15,A,S,D,PLACEHOLDER.step
ULA40015ASK0,40,15,A,S,K,PLACEHOLDER.step
ULA40015AD0,40,15,A,,D,PLACEHOLDER.step
ULA40015AK0,40,15,A,,K,PLACEHOLDER.step
ULA40015GSD0,40,15,G,S,D,PLACEHOLDER.step
ULA40015GSK0,40,15,G,S,K,PLACEHOLDER.step
ULA40015GD0,40,15,G,,D,PLACEHOLDER.step
ULA40015GK0,40,15,G,,K,PLACEHOLDER.step
ULA40015NSD0,40,15,N,S,D,PLACEHOLDER.step
ULA40015NSK0,40,15,N,S,K,PLACEHOLDER.step
ULA40015ND0,40,15,N,,D,PLACEHOLDER.step
ULA40015NK0,40,15,N,,K,PLACEHOLDER.step
ULA40015PSD0,40,15,P,S,D,PLACEHOLDER.step
ULA40015PSK0,40,15,P,S,K,PLACEHOLDER.step
ULA40015PD0,40,15,P,,D,PLACEHOLDER.step
ULA40015PK0,40,15,P,,K,PLACEHOLDER.step
ULA40025ASD0,40,25,A,S,D,PLACEHOLDER.step
ULA40025ASK0,40,25,A,S,K,PLACEHOLDER.step
ULA40025AD0,40,25,A,,D,PLACEHOLDER.step
ULA40025AK0,40,25,A,,K,PLACEHOLDER.step
ULA40025GSD0,40,25,G,S,D,PLACEHOLDER.step
ULA40025GSK0,40,25,G,S,K,PLACEHOLDER.step
ULA40025GD0,40,25,G,,D,PLACEHOLDER.step
ULA40025GK0,40,25,G,,K,PLACEHOLDER.step
ULA40025NSD0,40,25,N,S,D,PLACEHOLDER.step
ULA40025NSK0,40,25,N,S,K,PLACEHOLDER.step
ULA40025ND0,40,25,N,,D,PLACEHOLDER.step
ULA40025NK0,40,25,N,,K,PLACEHOLDER.step
ULA40025PSD0,40,25,P,S,D,PLACEHOLDER.step
ULA40025PSK0,40,25,P,S,K,PLACEHOLDER.step
ULA40025PD0,40,25,P,,D,PLACEHOLDER.step
ULA40025PK0,40,25,P,,K,PLACEHOLDER.step
ULA40050ASD0,40,50,A,S,D,PLACEHOLDER.step
ULA40050ASK0,40,50,A,S,K,PLACEHOLDER.step
ULA40050AD0,40,50,A,,D,PLACEHOLDER.step
ULA40050AK0,40,50,A,,K,PLACEHOLDER.step
ULA40050GSD0,40,50,G,S,D,PLACEHOLDER.step
ULA40050GSK0,40,50,G,S,K,PLACEHOLDER.step
ULA40050GD0,40,50,G,,D,PLACEHOLDER.step
ULA40050GK0,40,50,G,,K,PLACEHOLDER.step
ULA40050NSD0,40,50,N,S,D,PLACEHOLDER.step
ULA40050NSK0,40,50,N,S,K,PLACEHOLDER.step
ULA40050ND0,40,50,N,,D,PLACEHOLDER.step
ULA40050NK0,40,50,N,,K,PLACEHOLDER.step
ULA40050PSD0,40,50,P,S,D,PLACEHOLDER.step
ULA40050PSK0,40,50,P,S,K,PLACEHOLDER.step
ULA40050PD0,40,50,P,,D,PLACEHOLDER.step
ULA40050PK0,40,50,P,,K,PLACEHOLDER.step
ULA40060ASD0,40,60,A,S,D,PLACEHOLDER.step
ULA40060ASK0,40,60,A,S,K,PLACEHOLDER.step
ULA40060AD0,40,60,A,,D,PLACEHOLDER.step
ULA40060AK0,40,60,A,,K,PLACEHOLDER.step
ULA40060GSD0,40,60,G,S,D,PLACEHOLDER.step
ULA40060GSK0,40,60,G,S,K,PLACEHOLDER.step
ULA40060GD0,40,60,G,,D,PLACEHOLDER.step
ULA40060GK0,40,60,G,,K,PLACEHOLDER.step
ULA40060NSD0,40,60,N,S,D,PLACEHOLDER.step
ULA40060NSK0,40,60,N,S,K,PLACEHOLDER.step
ULA40060ND0,40,60,N,,D,PLACEHOLDER.step
ULA40060NK0,40,60,N,,K,PLACEHOLDER.step
ULA40060PSD0,40,60,P,S,D,PLACEHOLDER.step
ULA40060PSK0,40,60,P,S,K,PLACEHOLDER.step
ULA40060PD0,40,60,P,,D,PLACEHOLDER.step
ULA40060PK0,40,60,P,,K,PLACEHOLDER.step
ULA63020ASD0,63,20,A,S,D,PLACEHOLDER.step
ULA63020ASK0,63,20,A,S,K,PLACEHOLDER.step
ULA63020AD0,63,20,A,,D,PLACEHOLDER.step
ULA63020AK0,63,20,A,,K,PLACEHOLDER.step
ULA63020GSD0,63,20,G,S,D,PLACEHOLDER.step
ULA63020GSK0,63,20,G,S,K,PLACEHOLDER.step
ULA63020GD0,63,20,G,,D,PLACEHOLDER.step
ULA63020GK0,63,20,G,,K,PLACEHOLDER.step
ULA63020NSD0,63,20,N,S,D,PLACEHOLDER.step
ULA63020NSK0,63,20,N,S,K,PLACEHOLDER.step
ULA63020ND0,63,20,N,,D,PLACEHOLDER.step
ULA63020NK0,63,20,N,,K,PLACEHOLDER.step
ULA63020PSD0,63,20,P,S,D,PLACEHOLDER.step
ULA63020PSK0,63,20,P,S,K,PLACEHOLDER.step
ULA63020PD0,63,20,P,,D,PLACEHOLDER.step
ULA63020PK0,63,20,P,,K,PLACEHOLDER.step
ULA63040ASD0,63,40,A,S,D,PLACEHOLDER.step
ULA63040ASK0,63,40,A,S,K,PLACEHOLDER.step
ULA63040GSD0,63,40,G,S,D,PLACEHOLDER.step
ULA63040GSK0,63,40,G,S,K,PLACEHOLDER.step
ULA63040GD0,63,40,G,,D,PLACEHOLDER.step
ULA63040GK0,63,40,G,,K,PLACEHOLDER.step
ULA63040NSD0,63,40,N,S,D,PLACEHOLDER.step
ULA63040NSK0,63,40,N,S,K,PLACEHOLDER.step
ULA63040ND0,63,40,N,,D,PLACEHOLDER.step
ULA63040NK0,63,40,N,,K,PLACEHOLDER.step
ULA63040PSD0,63,40,P,S,D,PLACEHOLDER.step
ULA63040PSK0,63,40,P,S,K,PLACEHOLDER.step
ULA63040PD0,63,40,P,,D,PLACEHOLDER.step
ULA63040PK0,63,40,P,,K,PLACEHOLDER.step
ULA63015ASD0,63,15,A,S,D,PLACEHOLDER.step
ULA63015ASK0,63,15,A,S,K,PLACEHOLDER.step
ULA63015AD0,63,15,A,,D,PLACEHOLDER.step
ULA63015GSD0,63,15,G,S,D,PLACEHOLDER.step
ULA63015GSK0,63,15,G,S,K,PLACEHOLDER.step
ULA63015GD0,63,15,G,,D,PLACEHOLDER.step
ULA63015GK0,63,15,G,,K,PLACEHOLDER.step
ULA63015NSD0,63,15,N,S,D,PLACEHOLDER.step
ULA63015NSK0,63,15,N,S,K,PLACEHOLDER.step
ULA63015ND0,63,15,N,,D,PLACEHOLDER.step
ULA63015NK0,63,15,N,,K,PLACEHOLDER.step
ULA63015PSD0,63,15,P,S,D,PLACEHOLDER.step
ULA63015PSK0,63,15,P,S,K,PLACEHOLDER.step
ULA63015PD0,63,15,P,,D,PLACEHOLDER.step
ULA63015PK0,63,15,P,,K,PLACEHOLDER.step
ULA63025ASD0,63,25,A,S,D,PLACEHOLDER.step
ULA63025ASK0,63,25,A,S,K,PLACEHOLDER.step
ULA63025AD0,63,25,A,,D,PLACEHOLDER.step
ULA63025GSD0,63,25,G,S,D,PLACEHOLDER.step
ULA63025GSK0,63,25,G,S,K,PLACEHOLDER.step
ULA63025GD0,63,25,G,,D,PLACEHOLDER.step
ULA63025GK0,63,25,G,,K,PLACEHOLDER.step
ULA63025NSD0,63,25,N,S,D,PLACEHOLDER.step
ULA63025NSK0,63,25,N,S,K,PLACEHOLDER.step
ULA63025ND0,63,25,N,,D,PLACEHOLDER.step
ULA63025NK0,63,25,N,,K,PLACEHOLDER.step
ULA63025PSD0,63,25,P,S,D,PLACEHOLDER.step
ULA63025PSK0,63,25,P,S,K,PLACEHOLDER.step
ULA63025PD0,63,25,P,,D,PLACEHOLDER.step
ULA63025PK0,63,25,P,,K,PLACEHOLDER.step
ULA63050ASD0,63,50,A,S,D,PLACEHOLDER.step
ULA63050ASK0,63,50,A,S,K,PLACEHOLDER.step
ULA63050AD0,63,50,A,,D,PLACEHOLDER.step
ULA63050GSD0,63,50,G,S,D,PLACEHOLDER.step
ULA63050GSK0,63,50,G,S,K,PLACEHOLDER.step
ULA63050GD0,63,50,G,,D,PLACEHOLDER.step
ULA63050GK0,63,50,G,,K,PLACEHOLDER.step
ULA63050NSD0,63,50,N,S,D,PLACEHOLDER.step
ULA63050NSK0,63,50,N,S,K,PLACEHOLDER.step
ULA63050ND0,63,50,N,,D,PLACEHOLDER.step
ULA63050NK0,63,50,N,,K,PLACEHOLDER.step
ULA63050PSD0,63,50,P,S,D,PLACEHOLDER.step
ULA63050PSK0,63,50,P,S,K,PLACEHOLDER.step
ULA63050PD0,63,50,P,,D,PLACEHOLDER.step
ULA63050PK0,63,50,P,,K,PLACEHOLDER.step
ULA63060ASD0,63,60,A,S,D,PLACEHOLDER.step
ULA63060ASK0,63,60,A,S,K,PLACEHOLDER.step
ULA63060AD0,63,60,A,,D,PLACEHOLDER.step
ULA63060GSD0,63,60,G,S,D,PLACEHOLDER.step
ULA63060GSK0,63,60,G,S,K,PLACEHOLDER.step
ULA63060GD0,63,60,G,,D,PLACEHOLDER.step
ULA63060GK0,63,60,G,,K,PLACEHOLDER.step
ULA63060NSD0,63,60,N,S,D,PLACEHOLDER.step
ULA63060NSK0,63,60,N,S,K,PLACEHOLDER.step
ULA63060ND0,63,60,N,,D,PLACEHOLDER.step
ULA63060NK0,63,60,N,,K,PLACEHOLDER.step
ULA63060PSD0,63,60,P,S,D,PLACEHOLDER.step
ULA63060PSK0,63,60,P,S,K,PLACEHOLDER.step
ULA63060PD0,63,60,P,,D,PLACEHOLDER.step
ULA63060PK0,63,60,P,,K,PLACEHOLDER.step
1 model_code size stroke rod mount sensor step_file
18 ULA63040AK0 63 40 A K ULA63040AK0.step
19 ULA63050AK0 63 50 A K ULA63050AK0.step
20 ULA63060AK0 63 60 A K ULA63060AK0.step
21 ULA40020AD0 40 20 A D PLACEHOLDER.step
22 ULA40020AK0 40 20 A K PLACEHOLDER.step
23 ULA40020GSD0 40 20 G S D PLACEHOLDER.step
24 ULA40020GD0 40 20 G D PLACEHOLDER.step
25 ULA40020GK0 40 20 G K PLACEHOLDER.step
26 ULA40020NSD0 40 20 N S D PLACEHOLDER.step
27 ULA40020ND0 40 20 N D PLACEHOLDER.step
28 ULA40020NK0 40 20 N K PLACEHOLDER.step
29 ULA40020PSD0 40 20 P S D PLACEHOLDER.step
30 ULA40020PD0 40 20 P D PLACEHOLDER.step
31 ULA40020PK0 40 20 P K PLACEHOLDER.step
32 ULA40040AD0 40 40 A D PLACEHOLDER.step
33 ULA40040AK0 40 40 A K PLACEHOLDER.step
34 ULA40040GD0 40 40 G D PLACEHOLDER.step
35 ULA40040GK0 40 40 G K PLACEHOLDER.step
36 ULA40040ND0 40 40 N D PLACEHOLDER.step
37 ULA40040NK0 40 40 N K PLACEHOLDER.step
38 ULA40040PD0 40 40 P D PLACEHOLDER.step
39 ULA40040PK0 40 40 P K PLACEHOLDER.step
40 ULA40015ASD0 40 15 A S D PLACEHOLDER.step
41 ULA40015ASK0 40 15 A S K PLACEHOLDER.step
42 ULA40015AD0 40 15 A D PLACEHOLDER.step
43 ULA40015AK0 40 15 A K PLACEHOLDER.step
44 ULA40015GSD0 40 15 G S D PLACEHOLDER.step
45 ULA40015GSK0 40 15 G S K PLACEHOLDER.step
46 ULA40015GD0 40 15 G D PLACEHOLDER.step
47 ULA40015GK0 40 15 G K PLACEHOLDER.step
48 ULA40015NSD0 40 15 N S D PLACEHOLDER.step
49 ULA40015NSK0 40 15 N S K PLACEHOLDER.step
50 ULA40015ND0 40 15 N D PLACEHOLDER.step
51 ULA40015NK0 40 15 N K PLACEHOLDER.step
52 ULA40015PSD0 40 15 P S D PLACEHOLDER.step
53 ULA40015PSK0 40 15 P S K PLACEHOLDER.step
54 ULA40015PD0 40 15 P D PLACEHOLDER.step
55 ULA40015PK0 40 15 P K PLACEHOLDER.step
56 ULA40025ASD0 40 25 A S D PLACEHOLDER.step
57 ULA40025ASK0 40 25 A S K PLACEHOLDER.step
58 ULA40025AD0 40 25 A D PLACEHOLDER.step
59 ULA40025AK0 40 25 A K PLACEHOLDER.step
60 ULA40025GSD0 40 25 G S D PLACEHOLDER.step
61 ULA40025GSK0 40 25 G S K PLACEHOLDER.step
62 ULA40025GD0 40 25 G D PLACEHOLDER.step
63 ULA40025GK0 40 25 G K PLACEHOLDER.step
64 ULA40025NSD0 40 25 N S D PLACEHOLDER.step
65 ULA40025NSK0 40 25 N S K PLACEHOLDER.step
66 ULA40025ND0 40 25 N D PLACEHOLDER.step
67 ULA40025NK0 40 25 N K PLACEHOLDER.step
68 ULA40025PSD0 40 25 P S D PLACEHOLDER.step
69 ULA40025PSK0 40 25 P S K PLACEHOLDER.step
70 ULA40025PD0 40 25 P D PLACEHOLDER.step
71 ULA40025PK0 40 25 P K PLACEHOLDER.step
72 ULA40050ASD0 40 50 A S D PLACEHOLDER.step
73 ULA40050ASK0 40 50 A S K PLACEHOLDER.step
74 ULA40050AD0 40 50 A D PLACEHOLDER.step
75 ULA40050AK0 40 50 A K PLACEHOLDER.step
76 ULA40050GSD0 40 50 G S D PLACEHOLDER.step
77 ULA40050GSK0 40 50 G S K PLACEHOLDER.step
78 ULA40050GD0 40 50 G D PLACEHOLDER.step
79 ULA40050GK0 40 50 G K PLACEHOLDER.step
80 ULA40050NSD0 40 50 N S D PLACEHOLDER.step
81 ULA40050NSK0 40 50 N S K PLACEHOLDER.step
82 ULA40050ND0 40 50 N D PLACEHOLDER.step
83 ULA40050NK0 40 50 N K PLACEHOLDER.step
84 ULA40050PSD0 40 50 P S D PLACEHOLDER.step
85 ULA40050PSK0 40 50 P S K PLACEHOLDER.step
86 ULA40050PD0 40 50 P D PLACEHOLDER.step
87 ULA40050PK0 40 50 P K PLACEHOLDER.step
88 ULA40060ASD0 40 60 A S D PLACEHOLDER.step
89 ULA40060ASK0 40 60 A S K PLACEHOLDER.step
90 ULA40060AD0 40 60 A D PLACEHOLDER.step
91 ULA40060AK0 40 60 A K PLACEHOLDER.step
92 ULA40060GSD0 40 60 G S D PLACEHOLDER.step
93 ULA40060GSK0 40 60 G S K PLACEHOLDER.step
94 ULA40060GD0 40 60 G D PLACEHOLDER.step
95 ULA40060GK0 40 60 G K PLACEHOLDER.step
96 ULA40060NSD0 40 60 N S D PLACEHOLDER.step
97 ULA40060NSK0 40 60 N S K PLACEHOLDER.step
98 ULA40060ND0 40 60 N D PLACEHOLDER.step
99 ULA40060NK0 40 60 N K PLACEHOLDER.step
100 ULA40060PSD0 40 60 P S D PLACEHOLDER.step
101 ULA40060PSK0 40 60 P S K PLACEHOLDER.step
102 ULA40060PD0 40 60 P D PLACEHOLDER.step
103 ULA40060PK0 40 60 P K PLACEHOLDER.step
104 ULA63020ASD0 63 20 A S D PLACEHOLDER.step
105 ULA63020ASK0 63 20 A S K PLACEHOLDER.step
106 ULA63020AD0 63 20 A D PLACEHOLDER.step
107 ULA63020AK0 63 20 A K PLACEHOLDER.step
108 ULA63020GSD0 63 20 G S D PLACEHOLDER.step
109 ULA63020GSK0 63 20 G S K PLACEHOLDER.step
110 ULA63020GD0 63 20 G D PLACEHOLDER.step
111 ULA63020GK0 63 20 G K PLACEHOLDER.step
112 ULA63020NSD0 63 20 N S D PLACEHOLDER.step
113 ULA63020NSK0 63 20 N S K PLACEHOLDER.step
114 ULA63020ND0 63 20 N D PLACEHOLDER.step
115 ULA63020NK0 63 20 N K PLACEHOLDER.step
116 ULA63020PSD0 63 20 P S D PLACEHOLDER.step
117 ULA63020PSK0 63 20 P S K PLACEHOLDER.step
118 ULA63020PD0 63 20 P D PLACEHOLDER.step
119 ULA63020PK0 63 20 P K PLACEHOLDER.step
120 ULA63040ASD0 63 40 A S D PLACEHOLDER.step
121 ULA63040ASK0 63 40 A S K PLACEHOLDER.step
122 ULA63040GSD0 63 40 G S D PLACEHOLDER.step
123 ULA63040GSK0 63 40 G S K PLACEHOLDER.step
124 ULA63040GD0 63 40 G D PLACEHOLDER.step
125 ULA63040GK0 63 40 G K PLACEHOLDER.step
126 ULA63040NSD0 63 40 N S D PLACEHOLDER.step
127 ULA63040NSK0 63 40 N S K PLACEHOLDER.step
128 ULA63040ND0 63 40 N D PLACEHOLDER.step
129 ULA63040NK0 63 40 N K PLACEHOLDER.step
130 ULA63040PSD0 63 40 P S D PLACEHOLDER.step
131 ULA63040PSK0 63 40 P S K PLACEHOLDER.step
132 ULA63040PD0 63 40 P D PLACEHOLDER.step
133 ULA63040PK0 63 40 P K PLACEHOLDER.step
134 ULA63015ASD0 63 15 A S D PLACEHOLDER.step
135 ULA63015ASK0 63 15 A S K PLACEHOLDER.step
136 ULA63015AD0 63 15 A D PLACEHOLDER.step
137 ULA63015GSD0 63 15 G S D PLACEHOLDER.step
138 ULA63015GSK0 63 15 G S K PLACEHOLDER.step
139 ULA63015GD0 63 15 G D PLACEHOLDER.step
140 ULA63015GK0 63 15 G K PLACEHOLDER.step
141 ULA63015NSD0 63 15 N S D PLACEHOLDER.step
142 ULA63015NSK0 63 15 N S K PLACEHOLDER.step
143 ULA63015ND0 63 15 N D PLACEHOLDER.step
144 ULA63015NK0 63 15 N K PLACEHOLDER.step
145 ULA63015PSD0 63 15 P S D PLACEHOLDER.step
146 ULA63015PSK0 63 15 P S K PLACEHOLDER.step
147 ULA63015PD0 63 15 P D PLACEHOLDER.step
148 ULA63015PK0 63 15 P K PLACEHOLDER.step
149 ULA63025ASD0 63 25 A S D PLACEHOLDER.step
150 ULA63025ASK0 63 25 A S K PLACEHOLDER.step
151 ULA63025AD0 63 25 A D PLACEHOLDER.step
152 ULA63025GSD0 63 25 G S D PLACEHOLDER.step
153 ULA63025GSK0 63 25 G S K PLACEHOLDER.step
154 ULA63025GD0 63 25 G D PLACEHOLDER.step
155 ULA63025GK0 63 25 G K PLACEHOLDER.step
156 ULA63025NSD0 63 25 N S D PLACEHOLDER.step
157 ULA63025NSK0 63 25 N S K PLACEHOLDER.step
158 ULA63025ND0 63 25 N D PLACEHOLDER.step
159 ULA63025NK0 63 25 N K PLACEHOLDER.step
160 ULA63025PSD0 63 25 P S D PLACEHOLDER.step
161 ULA63025PSK0 63 25 P S K PLACEHOLDER.step
162 ULA63025PD0 63 25 P D PLACEHOLDER.step
163 ULA63025PK0 63 25 P K PLACEHOLDER.step
164 ULA63050ASD0 63 50 A S D PLACEHOLDER.step
165 ULA63050ASK0 63 50 A S K PLACEHOLDER.step
166 ULA63050AD0 63 50 A D PLACEHOLDER.step
167 ULA63050GSD0 63 50 G S D PLACEHOLDER.step
168 ULA63050GSK0 63 50 G S K PLACEHOLDER.step
169 ULA63050GD0 63 50 G D PLACEHOLDER.step
170 ULA63050GK0 63 50 G K PLACEHOLDER.step
171 ULA63050NSD0 63 50 N S D PLACEHOLDER.step
172 ULA63050NSK0 63 50 N S K PLACEHOLDER.step
173 ULA63050ND0 63 50 N D PLACEHOLDER.step
174 ULA63050NK0 63 50 N K PLACEHOLDER.step
175 ULA63050PSD0 63 50 P S D PLACEHOLDER.step
176 ULA63050PSK0 63 50 P S K PLACEHOLDER.step
177 ULA63050PD0 63 50 P D PLACEHOLDER.step
178 ULA63050PK0 63 50 P K PLACEHOLDER.step
179 ULA63060ASD0 63 60 A S D PLACEHOLDER.step
180 ULA63060ASK0 63 60 A S K PLACEHOLDER.step
181 ULA63060AD0 63 60 A D PLACEHOLDER.step
182 ULA63060GSD0 63 60 G S D PLACEHOLDER.step
183 ULA63060GSK0 63 60 G S K PLACEHOLDER.step
184 ULA63060GD0 63 60 G D PLACEHOLDER.step
185 ULA63060GK0 63 60 G K PLACEHOLDER.step
186 ULA63060NSD0 63 60 N S D PLACEHOLDER.step
187 ULA63060NSK0 63 60 N S K PLACEHOLDER.step
188 ULA63060ND0 63 60 N D PLACEHOLDER.step
189 ULA63060NK0 63 60 N K PLACEHOLDER.step
190 ULA63060PSD0 63 60 P S D PLACEHOLDER.step
191 ULA63060PSK0 63 60 P S K PLACEHOLDER.step
192 ULA63060PD0 63 60 P D PLACEHOLDER.step
193 ULA63060PK0 63 60 P K PLACEHOLDER.step

View File

@@ -13,3 +13,29 @@ ULL50040EXK0,50,40,E,X,K,ULL50040EXK0.step
ULL50040FXK0,50,40,F,X,K,ULL50040FXK0.step ULL50040FXK0,50,40,F,X,K,ULL50040FXK0.step
ULL50040HXK0,50,40,H,X,K,ULL50040HXK0.step ULL50040HXK0,50,40,H,X,K,ULL50040HXK0.step
ULL50040IXK0,50,40,I,X,K,ULL50040IXK0.step ULL50040IXK0,50,40,I,X,K,ULL50040IXK0.step
ULL50040AXK0,50,40,A,X,K,PLACEHOLDER.step
ULL50040BEK0,50,40,B,E,K,PLACEHOLDER.step
ULL50040BNK0,50,40,B,N,K,PLACEHOLDER.step
ULL50040BOK0,50,40,B,O,K,PLACEHOLDER.step
ULL50040BSK0,50,40,B,S,K,PLACEHOLDER.step
ULL50040CEK0,50,40,C,E,K,PLACEHOLDER.step
ULL50040CNK0,50,40,C,N,K,PLACEHOLDER.step
ULL50040COK0,50,40,C,O,K,PLACEHOLDER.step
ULL50040CSK0,50,40,C,S,K,PLACEHOLDER.step
ULL50040DXK0,50,40,D,X,K,PLACEHOLDER.step
ULL50040EEK0,50,40,E,E,K,PLACEHOLDER.step
ULL50040ENK0,50,40,E,N,K,PLACEHOLDER.step
ULL50040EOK0,50,40,E,O,K,PLACEHOLDER.step
ULL50040ESK0,50,40,E,S,K,PLACEHOLDER.step
ULL50040FEK0,50,40,F,E,K,PLACEHOLDER.step
ULL50040FNK0,50,40,F,N,K,PLACEHOLDER.step
ULL50040FOK0,50,40,F,O,K,PLACEHOLDER.step
ULL50040FSK0,50,40,F,S,K,PLACEHOLDER.step
ULL50040HEK0,50,40,H,E,K,PLACEHOLDER.step
ULL50040HNK0,50,40,H,N,K,PLACEHOLDER.step
ULL50040HOK0,50,40,H,O,K,PLACEHOLDER.step
ULL50040HSK0,50,40,H,S,K,PLACEHOLDER.step
ULL50040IEK0,50,40,I,E,K,PLACEHOLDER.step
ULL50040INK0,50,40,I,N,K,PLACEHOLDER.step
ULL50040IOK0,50,40,I,O,K,PLACEHOLDER.step
ULL50040ISK0,50,40,I,S,K,PLACEHOLDER.step
1 model_code size stroke rod orient sensor step_file
13 ULL50040FXK0 50 40 F X K ULL50040FXK0.step
14 ULL50040HXK0 50 40 H X K ULL50040HXK0.step
15 ULL50040IXK0 50 40 I X K ULL50040IXK0.step
16 ULL50040AXK0 50 40 A X K PLACEHOLDER.step
17 ULL50040BEK0 50 40 B E K PLACEHOLDER.step
18 ULL50040BNK0 50 40 B N K PLACEHOLDER.step
19 ULL50040BOK0 50 40 B O K PLACEHOLDER.step
20 ULL50040BSK0 50 40 B S K PLACEHOLDER.step
21 ULL50040CEK0 50 40 C E K PLACEHOLDER.step
22 ULL50040CNK0 50 40 C N K PLACEHOLDER.step
23 ULL50040COK0 50 40 C O K PLACEHOLDER.step
24 ULL50040CSK0 50 40 C S K PLACEHOLDER.step
25 ULL50040DXK0 50 40 D X K PLACEHOLDER.step
26 ULL50040EEK0 50 40 E E K PLACEHOLDER.step
27 ULL50040ENK0 50 40 E N K PLACEHOLDER.step
28 ULL50040EOK0 50 40 E O K PLACEHOLDER.step
29 ULL50040ESK0 50 40 E S K PLACEHOLDER.step
30 ULL50040FEK0 50 40 F E K PLACEHOLDER.step
31 ULL50040FNK0 50 40 F N K PLACEHOLDER.step
32 ULL50040FOK0 50 40 F O K PLACEHOLDER.step
33 ULL50040FSK0 50 40 F S K PLACEHOLDER.step
34 ULL50040HEK0 50 40 H E K PLACEHOLDER.step
35 ULL50040HNK0 50 40 H N K PLACEHOLDER.step
36 ULL50040HOK0 50 40 H O K PLACEHOLDER.step
37 ULL50040HSK0 50 40 H S K PLACEHOLDER.step
38 ULL50040IEK0 50 40 I E K PLACEHOLDER.step
39 ULL50040INK0 50 40 I N K PLACEHOLDER.step
40 ULL50040IOK0 50 40 I O K PLACEHOLDER.step
41 ULL50040ISK0 50 40 I S K PLACEHOLDER.step

View File

@@ -69,3 +69,133 @@ ULM50060EXK0,50,60,E,X,K,ULM50060EXK0.step
ULM50060FXK0,50,60,F,X,K,ULM50060FXK0.step ULM50060FXK0,50,60,F,X,K,ULM50060FXK0.step
ULM50060HXK0,50,60,H,X,K,ULM50060HXK0.step ULM50060HXK0,50,60,H,X,K,ULM50060HXK0.step
ULM50060IXK0,50,60,I,X,K,ULM50060IXK0.step ULM50060IXK0,50,60,I,X,K,ULM50060IXK0.step
ULM50015AXK0,50,15,A,X,K,PLACEHOLDER.step
ULM50015BEK0,50,15,B,E,K,PLACEHOLDER.step
ULM50015BNK0,50,15,B,N,K,PLACEHOLDER.step
ULM50015BOK0,50,15,B,O,K,PLACEHOLDER.step
ULM50015BSK0,50,15,B,S,K,PLACEHOLDER.step
ULM50015CEK0,50,15,C,E,K,PLACEHOLDER.step
ULM50015CNK0,50,15,C,N,K,PLACEHOLDER.step
ULM50015COK0,50,15,C,O,K,PLACEHOLDER.step
ULM50015CSK0,50,15,C,S,K,PLACEHOLDER.step
ULM50015DXK0,50,15,D,X,K,PLACEHOLDER.step
ULM50015EEK0,50,15,E,E,K,PLACEHOLDER.step
ULM50015ENK0,50,15,E,N,K,PLACEHOLDER.step
ULM50015EOK0,50,15,E,O,K,PLACEHOLDER.step
ULM50015ESK0,50,15,E,S,K,PLACEHOLDER.step
ULM50015FEK0,50,15,F,E,K,PLACEHOLDER.step
ULM50015FNK0,50,15,F,N,K,PLACEHOLDER.step
ULM50015FOK0,50,15,F,O,K,PLACEHOLDER.step
ULM50015FSK0,50,15,F,S,K,PLACEHOLDER.step
ULM50015HEK0,50,15,H,E,K,PLACEHOLDER.step
ULM50015HNK0,50,15,H,N,K,PLACEHOLDER.step
ULM50015HOK0,50,15,H,O,K,PLACEHOLDER.step
ULM50015HSK0,50,15,H,S,K,PLACEHOLDER.step
ULM50015IEK0,50,15,I,E,K,PLACEHOLDER.step
ULM50015INK0,50,15,I,N,K,PLACEHOLDER.step
ULM50015IOK0,50,15,I,O,K,PLACEHOLDER.step
ULM50015ISK0,50,15,I,S,K,PLACEHOLDER.step
ULM50025AXK0,50,25,A,X,K,PLACEHOLDER.step
ULM50025BEK0,50,25,B,E,K,PLACEHOLDER.step
ULM50025BNK0,50,25,B,N,K,PLACEHOLDER.step
ULM50025BOK0,50,25,B,O,K,PLACEHOLDER.step
ULM50025BSK0,50,25,B,S,K,PLACEHOLDER.step
ULM50025CEK0,50,25,C,E,K,PLACEHOLDER.step
ULM50025CNK0,50,25,C,N,K,PLACEHOLDER.step
ULM50025COK0,50,25,C,O,K,PLACEHOLDER.step
ULM50025CSK0,50,25,C,S,K,PLACEHOLDER.step
ULM50025DXK0,50,25,D,X,K,PLACEHOLDER.step
ULM50025EEK0,50,25,E,E,K,PLACEHOLDER.step
ULM50025ENK0,50,25,E,N,K,PLACEHOLDER.step
ULM50025EOK0,50,25,E,O,K,PLACEHOLDER.step
ULM50025ESK0,50,25,E,S,K,PLACEHOLDER.step
ULM50025FEK0,50,25,F,E,K,PLACEHOLDER.step
ULM50025FNK0,50,25,F,N,K,PLACEHOLDER.step
ULM50025FOK0,50,25,F,O,K,PLACEHOLDER.step
ULM50025FSK0,50,25,F,S,K,PLACEHOLDER.step
ULM50025HEK0,50,25,H,E,K,PLACEHOLDER.step
ULM50025HNK0,50,25,H,N,K,PLACEHOLDER.step
ULM50025HOK0,50,25,H,O,K,PLACEHOLDER.step
ULM50025HSK0,50,25,H,S,K,PLACEHOLDER.step
ULM50025IEK0,50,25,I,E,K,PLACEHOLDER.step
ULM50025INK0,50,25,I,N,K,PLACEHOLDER.step
ULM50025IOK0,50,25,I,O,K,PLACEHOLDER.step
ULM50025ISK0,50,25,I,S,K,PLACEHOLDER.step
ULM50040AXK0,50,40,A,X,K,PLACEHOLDER.step
ULM50040BEK0,50,40,B,E,K,PLACEHOLDER.step
ULM50040BNK0,50,40,B,N,K,PLACEHOLDER.step
ULM50040BOK0,50,40,B,O,K,PLACEHOLDER.step
ULM50040BSK0,50,40,B,S,K,PLACEHOLDER.step
ULM50040CEK0,50,40,C,E,K,PLACEHOLDER.step
ULM50040CNK0,50,40,C,N,K,PLACEHOLDER.step
ULM50040COK0,50,40,C,O,K,PLACEHOLDER.step
ULM50040CSK0,50,40,C,S,K,PLACEHOLDER.step
ULM50040DXK0,50,40,D,X,K,PLACEHOLDER.step
ULM50040EEK0,50,40,E,E,K,PLACEHOLDER.step
ULM50040ENK0,50,40,E,N,K,PLACEHOLDER.step
ULM50040EOK0,50,40,E,O,K,PLACEHOLDER.step
ULM50040ESK0,50,40,E,S,K,PLACEHOLDER.step
ULM50040FEK0,50,40,F,E,K,PLACEHOLDER.step
ULM50040FNK0,50,40,F,N,K,PLACEHOLDER.step
ULM50040FOK0,50,40,F,O,K,PLACEHOLDER.step
ULM50040FSK0,50,40,F,S,K,PLACEHOLDER.step
ULM50040HEK0,50,40,H,E,K,PLACEHOLDER.step
ULM50040HNK0,50,40,H,N,K,PLACEHOLDER.step
ULM50040HOK0,50,40,H,O,K,PLACEHOLDER.step
ULM50040HSK0,50,40,H,S,K,PLACEHOLDER.step
ULM50040IEK0,50,40,I,E,K,PLACEHOLDER.step
ULM50040INK0,50,40,I,N,K,PLACEHOLDER.step
ULM50040IOK0,50,40,I,O,K,PLACEHOLDER.step
ULM50040ISK0,50,40,I,S,K,PLACEHOLDER.step
ULM50050AXK0,50,50,A,X,K,PLACEHOLDER.step
ULM50050BEK0,50,50,B,E,K,PLACEHOLDER.step
ULM50050BNK0,50,50,B,N,K,PLACEHOLDER.step
ULM50050BOK0,50,50,B,O,K,PLACEHOLDER.step
ULM50050BSK0,50,50,B,S,K,PLACEHOLDER.step
ULM50050CEK0,50,50,C,E,K,PLACEHOLDER.step
ULM50050CNK0,50,50,C,N,K,PLACEHOLDER.step
ULM50050COK0,50,50,C,O,K,PLACEHOLDER.step
ULM50050CSK0,50,50,C,S,K,PLACEHOLDER.step
ULM50050DXK0,50,50,D,X,K,PLACEHOLDER.step
ULM50050EEK0,50,50,E,E,K,PLACEHOLDER.step
ULM50050ENK0,50,50,E,N,K,PLACEHOLDER.step
ULM50050EOK0,50,50,E,O,K,PLACEHOLDER.step
ULM50050ESK0,50,50,E,S,K,PLACEHOLDER.step
ULM50050FEK0,50,50,F,E,K,PLACEHOLDER.step
ULM50050FNK0,50,50,F,N,K,PLACEHOLDER.step
ULM50050FOK0,50,50,F,O,K,PLACEHOLDER.step
ULM50050FSK0,50,50,F,S,K,PLACEHOLDER.step
ULM50050HEK0,50,50,H,E,K,PLACEHOLDER.step
ULM50050HNK0,50,50,H,N,K,PLACEHOLDER.step
ULM50050HOK0,50,50,H,O,K,PLACEHOLDER.step
ULM50050HSK0,50,50,H,S,K,PLACEHOLDER.step
ULM50050IEK0,50,50,I,E,K,PLACEHOLDER.step
ULM50050INK0,50,50,I,N,K,PLACEHOLDER.step
ULM50050IOK0,50,50,I,O,K,PLACEHOLDER.step
ULM50050ISK0,50,50,I,S,K,PLACEHOLDER.step
ULM50060AXK0,50,60,A,X,K,PLACEHOLDER.step
ULM50060BEK0,50,60,B,E,K,PLACEHOLDER.step
ULM50060BNK0,50,60,B,N,K,PLACEHOLDER.step
ULM50060BOK0,50,60,B,O,K,PLACEHOLDER.step
ULM50060BSK0,50,60,B,S,K,PLACEHOLDER.step
ULM50060CEK0,50,60,C,E,K,PLACEHOLDER.step
ULM50060CNK0,50,60,C,N,K,PLACEHOLDER.step
ULM50060COK0,50,60,C,O,K,PLACEHOLDER.step
ULM50060CSK0,50,60,C,S,K,PLACEHOLDER.step
ULM50060DXK0,50,60,D,X,K,PLACEHOLDER.step
ULM50060EEK0,50,60,E,E,K,PLACEHOLDER.step
ULM50060ENK0,50,60,E,N,K,PLACEHOLDER.step
ULM50060EOK0,50,60,E,O,K,PLACEHOLDER.step
ULM50060ESK0,50,60,E,S,K,PLACEHOLDER.step
ULM50060FEK0,50,60,F,E,K,PLACEHOLDER.step
ULM50060FNK0,50,60,F,N,K,PLACEHOLDER.step
ULM50060FOK0,50,60,F,O,K,PLACEHOLDER.step
ULM50060FSK0,50,60,F,S,K,PLACEHOLDER.step
ULM50060HEK0,50,60,H,E,K,PLACEHOLDER.step
ULM50060HNK0,50,60,H,N,K,PLACEHOLDER.step
ULM50060HOK0,50,60,H,O,K,PLACEHOLDER.step
ULM50060HSK0,50,60,H,S,K,PLACEHOLDER.step
ULM50060IEK0,50,60,I,E,K,PLACEHOLDER.step
ULM50060INK0,50,60,I,N,K,PLACEHOLDER.step
ULM50060IOK0,50,60,I,O,K,PLACEHOLDER.step
ULM50060ISK0,50,60,I,S,K,PLACEHOLDER.step
1 model_code size stroke rod orient sensor step_file
69 ULM50060FXK0 50 60 F X K ULM50060FXK0.step
70 ULM50060HXK0 50 60 H X K ULM50060HXK0.step
71 ULM50060IXK0 50 60 I X K ULM50060IXK0.step
72 ULM50015AXK0 50 15 A X K PLACEHOLDER.step
73 ULM50015BEK0 50 15 B E K PLACEHOLDER.step
74 ULM50015BNK0 50 15 B N K PLACEHOLDER.step
75 ULM50015BOK0 50 15 B O K PLACEHOLDER.step
76 ULM50015BSK0 50 15 B S K PLACEHOLDER.step
77 ULM50015CEK0 50 15 C E K PLACEHOLDER.step
78 ULM50015CNK0 50 15 C N K PLACEHOLDER.step
79 ULM50015COK0 50 15 C O K PLACEHOLDER.step
80 ULM50015CSK0 50 15 C S K PLACEHOLDER.step
81 ULM50015DXK0 50 15 D X K PLACEHOLDER.step
82 ULM50015EEK0 50 15 E E K PLACEHOLDER.step
83 ULM50015ENK0 50 15 E N K PLACEHOLDER.step
84 ULM50015EOK0 50 15 E O K PLACEHOLDER.step
85 ULM50015ESK0 50 15 E S K PLACEHOLDER.step
86 ULM50015FEK0 50 15 F E K PLACEHOLDER.step
87 ULM50015FNK0 50 15 F N K PLACEHOLDER.step
88 ULM50015FOK0 50 15 F O K PLACEHOLDER.step
89 ULM50015FSK0 50 15 F S K PLACEHOLDER.step
90 ULM50015HEK0 50 15 H E K PLACEHOLDER.step
91 ULM50015HNK0 50 15 H N K PLACEHOLDER.step
92 ULM50015HOK0 50 15 H O K PLACEHOLDER.step
93 ULM50015HSK0 50 15 H S K PLACEHOLDER.step
94 ULM50015IEK0 50 15 I E K PLACEHOLDER.step
95 ULM50015INK0 50 15 I N K PLACEHOLDER.step
96 ULM50015IOK0 50 15 I O K PLACEHOLDER.step
97 ULM50015ISK0 50 15 I S K PLACEHOLDER.step
98 ULM50025AXK0 50 25 A X K PLACEHOLDER.step
99 ULM50025BEK0 50 25 B E K PLACEHOLDER.step
100 ULM50025BNK0 50 25 B N K PLACEHOLDER.step
101 ULM50025BOK0 50 25 B O K PLACEHOLDER.step
102 ULM50025BSK0 50 25 B S K PLACEHOLDER.step
103 ULM50025CEK0 50 25 C E K PLACEHOLDER.step
104 ULM50025CNK0 50 25 C N K PLACEHOLDER.step
105 ULM50025COK0 50 25 C O K PLACEHOLDER.step
106 ULM50025CSK0 50 25 C S K PLACEHOLDER.step
107 ULM50025DXK0 50 25 D X K PLACEHOLDER.step
108 ULM50025EEK0 50 25 E E K PLACEHOLDER.step
109 ULM50025ENK0 50 25 E N K PLACEHOLDER.step
110 ULM50025EOK0 50 25 E O K PLACEHOLDER.step
111 ULM50025ESK0 50 25 E S K PLACEHOLDER.step
112 ULM50025FEK0 50 25 F E K PLACEHOLDER.step
113 ULM50025FNK0 50 25 F N K PLACEHOLDER.step
114 ULM50025FOK0 50 25 F O K PLACEHOLDER.step
115 ULM50025FSK0 50 25 F S K PLACEHOLDER.step
116 ULM50025HEK0 50 25 H E K PLACEHOLDER.step
117 ULM50025HNK0 50 25 H N K PLACEHOLDER.step
118 ULM50025HOK0 50 25 H O K PLACEHOLDER.step
119 ULM50025HSK0 50 25 H S K PLACEHOLDER.step
120 ULM50025IEK0 50 25 I E K PLACEHOLDER.step
121 ULM50025INK0 50 25 I N K PLACEHOLDER.step
122 ULM50025IOK0 50 25 I O K PLACEHOLDER.step
123 ULM50025ISK0 50 25 I S K PLACEHOLDER.step
124 ULM50040AXK0 50 40 A X K PLACEHOLDER.step
125 ULM50040BEK0 50 40 B E K PLACEHOLDER.step
126 ULM50040BNK0 50 40 B N K PLACEHOLDER.step
127 ULM50040BOK0 50 40 B O K PLACEHOLDER.step
128 ULM50040BSK0 50 40 B S K PLACEHOLDER.step
129 ULM50040CEK0 50 40 C E K PLACEHOLDER.step
130 ULM50040CNK0 50 40 C N K PLACEHOLDER.step
131 ULM50040COK0 50 40 C O K PLACEHOLDER.step
132 ULM50040CSK0 50 40 C S K PLACEHOLDER.step
133 ULM50040DXK0 50 40 D X K PLACEHOLDER.step
134 ULM50040EEK0 50 40 E E K PLACEHOLDER.step
135 ULM50040ENK0 50 40 E N K PLACEHOLDER.step
136 ULM50040EOK0 50 40 E O K PLACEHOLDER.step
137 ULM50040ESK0 50 40 E S K PLACEHOLDER.step
138 ULM50040FEK0 50 40 F E K PLACEHOLDER.step
139 ULM50040FNK0 50 40 F N K PLACEHOLDER.step
140 ULM50040FOK0 50 40 F O K PLACEHOLDER.step
141 ULM50040FSK0 50 40 F S K PLACEHOLDER.step
142 ULM50040HEK0 50 40 H E K PLACEHOLDER.step
143 ULM50040HNK0 50 40 H N K PLACEHOLDER.step
144 ULM50040HOK0 50 40 H O K PLACEHOLDER.step
145 ULM50040HSK0 50 40 H S K PLACEHOLDER.step
146 ULM50040IEK0 50 40 I E K PLACEHOLDER.step
147 ULM50040INK0 50 40 I N K PLACEHOLDER.step
148 ULM50040IOK0 50 40 I O K PLACEHOLDER.step
149 ULM50040ISK0 50 40 I S K PLACEHOLDER.step
150 ULM50050AXK0 50 50 A X K PLACEHOLDER.step
151 ULM50050BEK0 50 50 B E K PLACEHOLDER.step
152 ULM50050BNK0 50 50 B N K PLACEHOLDER.step
153 ULM50050BOK0 50 50 B O K PLACEHOLDER.step
154 ULM50050BSK0 50 50 B S K PLACEHOLDER.step
155 ULM50050CEK0 50 50 C E K PLACEHOLDER.step
156 ULM50050CNK0 50 50 C N K PLACEHOLDER.step
157 ULM50050COK0 50 50 C O K PLACEHOLDER.step
158 ULM50050CSK0 50 50 C S K PLACEHOLDER.step
159 ULM50050DXK0 50 50 D X K PLACEHOLDER.step
160 ULM50050EEK0 50 50 E E K PLACEHOLDER.step
161 ULM50050ENK0 50 50 E N K PLACEHOLDER.step
162 ULM50050EOK0 50 50 E O K PLACEHOLDER.step
163 ULM50050ESK0 50 50 E S K PLACEHOLDER.step
164 ULM50050FEK0 50 50 F E K PLACEHOLDER.step
165 ULM50050FNK0 50 50 F N K PLACEHOLDER.step
166 ULM50050FOK0 50 50 F O K PLACEHOLDER.step
167 ULM50050FSK0 50 50 F S K PLACEHOLDER.step
168 ULM50050HEK0 50 50 H E K PLACEHOLDER.step
169 ULM50050HNK0 50 50 H N K PLACEHOLDER.step
170 ULM50050HOK0 50 50 H O K PLACEHOLDER.step
171 ULM50050HSK0 50 50 H S K PLACEHOLDER.step
172 ULM50050IEK0 50 50 I E K PLACEHOLDER.step
173 ULM50050INK0 50 50 I N K PLACEHOLDER.step
174 ULM50050IOK0 50 50 I O K PLACEHOLDER.step
175 ULM50050ISK0 50 50 I S K PLACEHOLDER.step
176 ULM50060AXK0 50 60 A X K PLACEHOLDER.step
177 ULM50060BEK0 50 60 B E K PLACEHOLDER.step
178 ULM50060BNK0 50 60 B N K PLACEHOLDER.step
179 ULM50060BOK0 50 60 B O K PLACEHOLDER.step
180 ULM50060BSK0 50 60 B S K PLACEHOLDER.step
181 ULM50060CEK0 50 60 C E K PLACEHOLDER.step
182 ULM50060CNK0 50 60 C N K PLACEHOLDER.step
183 ULM50060COK0 50 60 C O K PLACEHOLDER.step
184 ULM50060CSK0 50 60 C S K PLACEHOLDER.step
185 ULM50060DXK0 50 60 D X K PLACEHOLDER.step
186 ULM50060EEK0 50 60 E E K PLACEHOLDER.step
187 ULM50060ENK0 50 60 E N K PLACEHOLDER.step
188 ULM50060EOK0 50 60 E O K PLACEHOLDER.step
189 ULM50060ESK0 50 60 E S K PLACEHOLDER.step
190 ULM50060FEK0 50 60 F E K PLACEHOLDER.step
191 ULM50060FNK0 50 60 F N K PLACEHOLDER.step
192 ULM50060FOK0 50 60 F O K PLACEHOLDER.step
193 ULM50060FSK0 50 60 F S K PLACEHOLDER.step
194 ULM50060HEK0 50 60 H E K PLACEHOLDER.step
195 ULM50060HNK0 50 60 H N K PLACEHOLDER.step
196 ULM50060HOK0 50 60 H O K PLACEHOLDER.step
197 ULM50060HSK0 50 60 H S K PLACEHOLDER.step
198 ULM50060IEK0 50 60 I E K PLACEHOLDER.step
199 ULM50060INK0 50 60 I N K PLACEHOLDER.step
200 ULM50060IOK0 50 60 I O K PLACEHOLDER.step
201 ULM50060ISK0 50 60 I S K PLACEHOLDER.step

File diff suppressed because it is too large Load Diff

View File

@@ -2,3 +2,4 @@
ULT50040AK0,50,40,A,K,ULT50040AK0.step ULT50040AK0,50,40,A,K,ULT50040AK0.step
ULT50040GK0,50,40,G,K,ULT50040GK0.step ULT50040GK0,50,40,G,K,ULT50040GK0.step
ULT50040GN0,50,40,G,N,ULT50040GN0.step ULT50040GN0,50,40,G,N,ULT50040GN0.step
ULT50040AN0,50,40,A,N,PLACEHOLDER.step
1 model_code size stroke rod sensor step_file
2 ULT50040AK0 50 40 A K ULT50040AK0.step
3 ULT50040GK0 50 40 G K ULT50040GK0.step
4 ULT50040GN0 50 40 G N ULT50040GN0.step
5 ULT50040AN0 50 40 A N PLACEHOLDER.step

View File

@@ -9,3 +9,13 @@ ULY40040DEK0,40,40,D,E,K,ULY40040DEK0.step
ULY40040DNK0,40,40,D,N,K,ULY40040DNK0.step ULY40040DNK0,40,40,D,N,K,ULY40040DNK0.step
ULY40040DOK0,40,40,D,O,K,ULY40040DOK0.step ULY40040DOK0,40,40,D,O,K,ULY40040DOK0.step
ULY40040DSK0,40,40,D,S,K,ULY40040DSK0.step ULY40040DSK0,40,40,D,S,K,ULY40040DSK0.step
ULY40040AXK0,40,40,A,X,K,PLACEHOLDER.step
ULY40040BEK0,40,40,B,E,K,PLACEHOLDER.step
ULY40040BNK0,40,40,B,N,K,PLACEHOLDER.step
ULY40040BOK0,40,40,B,O,K,PLACEHOLDER.step
ULY40040BSK0,40,40,B,S,K,PLACEHOLDER.step
ULY40040CEK0,40,40,C,E,K,PLACEHOLDER.step
ULY40040CNK0,40,40,C,N,K,PLACEHOLDER.step
ULY40040COK0,40,40,C,O,K,PLACEHOLDER.step
ULY40040CSK0,40,40,C,S,K,PLACEHOLDER.step
ULY40040DXK0,40,40,D,X,K,PLACEHOLDER.step
1 model_code size stroke rod orient sensor step_file
9 ULY40040DNK0 40 40 D N K ULY40040DNK0.step
10 ULY40040DOK0 40 40 D O K ULY40040DOK0.step
11 ULY40040DSK0 40 40 D S K ULY40040DSK0.step
12 ULY40040AXK0 40 40 A X K PLACEHOLDER.step
13 ULY40040BEK0 40 40 B E K PLACEHOLDER.step
14 ULY40040BNK0 40 40 B N K PLACEHOLDER.step
15 ULY40040BOK0 40 40 B O K PLACEHOLDER.step
16 ULY40040BSK0 40 40 B S K PLACEHOLDER.step
17 ULY40040CEK0 40 40 C E K PLACEHOLDER.step
18 ULY40040CNK0 40 40 C N K PLACEHOLDER.step
19 ULY40040COK0 40 40 C O K PLACEHOLDER.step
20 ULY40040CSK0 40 40 C S K PLACEHOLDER.step
21 ULY40040DXK0 40 40 D X K PLACEHOLDER.step

View File

@@ -9,3 +9,13 @@ ULZ40040DEK0,40,40,D,E,K,ULZ40040DEK0.step
ULZ40040DNK0,40,40,D,N,K,ULZ40040DNK0.step ULZ40040DNK0,40,40,D,N,K,ULZ40040DNK0.step
ULZ40040DOK0,40,40,D,O,K,ULZ40040DOK0.step ULZ40040DOK0,40,40,D,O,K,ULZ40040DOK0.step
ULZ40040DSK0,40,40,D,S,K,ULZ40040DSK0.step ULZ40040DSK0,40,40,D,S,K,ULZ40040DSK0.step
ULZ40040AXK0,40,40,A,X,K,PLACEHOLDER.step
ULZ40040BEK0,40,40,B,E,K,PLACEHOLDER.step
ULZ40040BNK0,40,40,B,N,K,PLACEHOLDER.step
ULZ40040BOK0,40,40,B,O,K,PLACEHOLDER.step
ULZ40040BSK0,40,40,B,S,K,PLACEHOLDER.step
ULZ40040CEK0,40,40,C,E,K,PLACEHOLDER.step
ULZ40040CNK0,40,40,C,N,K,PLACEHOLDER.step
ULZ40040COK0,40,40,C,O,K,PLACEHOLDER.step
ULZ40040CSK0,40,40,C,S,K,PLACEHOLDER.step
ULZ40040DXK0,40,40,D,X,K,PLACEHOLDER.step
1 model_code size stroke rod orient sensor step_file
9 ULZ40040DNK0 40 40 D N K ULZ40040DNK0.step
10 ULZ40040DOK0 40 40 D O K ULZ40040DOK0.step
11 ULZ40040DSK0 40 40 D S K ULZ40040DSK0.step
12 ULZ40040AXK0 40 40 A X K PLACEHOLDER.step
13 ULZ40040BEK0 40 40 B E K PLACEHOLDER.step
14 ULZ40040BNK0 40 40 B N K PLACEHOLDER.step
15 ULZ40040BOK0 40 40 B O K PLACEHOLDER.step
16 ULZ40040BSK0 40 40 B S K PLACEHOLDER.step
17 ULZ40040CEK0 40 40 C E K PLACEHOLDER.step
18 ULZ40040CNK0 40 40 C N K PLACEHOLDER.step
19 ULZ40040COK0 40 40 C O K PLACEHOLDER.step
20 ULZ40040CSK0 40 40 C S K PLACEHOLDER.step
21 ULZ40040DXK0 40 40 D X K PLACEHOLDER.step

View File

@@ -6,3 +6,8 @@ USSL50JB51001N0,50,J,B,5,1,001,N,USSL50JB51001N0.step
USSL50JC31001N0,50,J,C,3,1,001,N,USSL50JC31001N0.step USSL50JC31001N0,50,J,C,3,1,001,N,USSL50JC31001N0.step
USSL50JC31002N0,50,J,C,3,1,002,N,USSL50JC31002N0.step USSL50JC31002N0,50,J,C,3,1,002,N,USSL50JC31002N0.step
USSL50JC51001N0,50,J,C,5,1,001,N,USSL50JC51001N0.step USSL50JC51001N0,50,J,C,5,1,001,N,USSL50JC51001N0.step
USSL50JB41002N0,50,J,B,4,1,002,N,PLACEHOLDER.step
USSL50JB51002N0,50,J,B,5,1,002,N,PLACEHOLDER.step
USSL50JC41001N0,50,J,C,4,1,001,N,PLACEHOLDER.step
USSL50JC41002N0,50,J,C,4,1,002,N,PLACEHOLDER.step
USSL50JC51002N0,50,J,C,5,1,002,N,PLACEHOLDER.step
1 model_code size pcmodel hook_pos pin_dia locating prog sensor step_file
6 USSL50JC31001N0 50 J C 3 1 001 N USSL50JC31001N0.step
7 USSL50JC31002N0 50 J C 3 1 002 N USSL50JC31002N0.step
8 USSL50JC51001N0 50 J C 5 1 001 N USSL50JC51001N0.step
9 USSL50JB41002N0 50 J B 4 1 002 N PLACEHOLDER.step
10 USSL50JB51002N0 50 J B 5 1 002 N PLACEHOLDER.step
11 USSL50JC41001N0 50 J C 4 1 001 N PLACEHOLDER.step
12 USSL50JC41002N0 50 J C 4 1 002 N PLACEHOLDER.step
13 USSL50JC51002N0 50 J C 5 1 002 N PLACEHOLDER.step

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
用【同缸径的真实数据值域】作为官方值域的代理, 评估生成候选的保守程度。
为什么用代理: 官方图册是双栏排版, pdftotext 后左右栏交错, 正则解析多次尝试均不可靠
(把真实数据都判成越界 = 解析错误)。而【真实型号】是最可靠的一手证据:
某缸径的真实型号里从未出现过的取值, 生成它就是推测。
判定口径 (保守):
某候选的每个参数值, 是否都在【同缸径的真实型号】中出现过?
- 全部出现过 → 属"组合缺失", 补全风险低
- 有值从未出现 → 属"引入新值", 补全风险高 (需人工/AI 核对官方图册)
"""
import csv, io, os, collections
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
DIMS = ['stroke', 'rod', 'orient', 'sensor', 'mount', 'style', 'pos', 'type']
def main():
rows = list(csv.DictReader(io.open(os.path.join(CAT, 'csv', 'ULP.csv'), encoding='utf-8-sig')))
real = [r for r in rows if r['step_file'] != 'PLACEHOLDER.step']
gen = [r for r in rows if r['step_file'] == 'PLACEHOLDER.step']
dims = [d for d in DIMS if d in rows[0]]
# 每个缸径的真实取值集合
domain = collections.defaultdict(lambda: collections.defaultdict(set))
for r in real:
for d in dims:
if r[d] != '':
domain[r['size']][d].add(r[d])
print('=' * 74)
print('真实数据里各缸径的取值域 (作为官方值域的代理)')
print('=' * 74)
for b in sorted(domain, key=int):
parts = [f'{d}={sorted(domain[b][d])}' for d in dims if domain[b][d]]
n = sum(1 for r in real if r['size'] == b)
print(f' ULP{b:>3s} ({n:3d} 个真实型号)')
for p in parts:
print(f' {p}')
# 评估生成候选
safe, risky = 0, 0
risky_detail = collections.Counter()
risky_models = []
for r in gen:
b = r['size']
novel = [f'{d}={r[d]}' for d in dims
if r[d] != '' and r[d] not in domain.get(b, {}).get(d, set())]
if novel:
risky += 1
for x in novel:
risky_detail[f'ULP{b} {x}'] += 1
if len(risky_models) < 5:
risky_models.append((r['model_code'], novel))
else:
safe += 1
print()
print('=' * 74)
print(f'生成候选 {len(gen)} 个的保守度评估')
print('=' * 74)
print(f' 低风险 (全部取值在同缸径真实数据中出现过): {safe} ({safe*100//len(gen)}%)')
print(f' 高风险 (含该缸径从未出现的取值) : {risky} ({risky*100//len(gen)}%)')
if risky_detail:
print('\n 高风险取值 TOP15 (该缸径真实数据里从未出现):')
for k, v in risky_detail.most_common(15):
print(f' {k:28s} {v:5d} 个候选')
print('\n 高风险样本:')
for mc, novel in risky_models:
print(f' {mc:18s} 新值 {novel}')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
"""统计各系列"参数组合 → 型号"是否一对一 (多处多 vs 一处多)。"""
import json, io, collections
CAT = r'D:/开发/OnebotCatalog/release/web_2026.08/catalog.json'
d = json.load(io.open(CAT, encoding='utf-8-sig'))
print(f'{"系列":8s} {"变体":>6s} {"组合":>6s} {"重复组":>6s} {"最大一对多":>9s} 判定')
print('-' * 58)
tot_codes = 0
bad = []
for s in d['series']:
vs = s.get('variants') or []
if not vs:
continue
params = [p['code'] for p in s['parameters']]
sig = collections.Counter(
tuple(str(v['params'].get(pc)) for pc in params) for v in vs)
dup = {k: n for k, n in sig.items() if n > 1}
mx = max(sig.values()) if sig else 0
verdict = 'OK 一对一' if not dup else f'!! {len(dup)} 组一对多'
if dup:
bad.append((s['code'], len(vs), len(dup), mx))
tot_codes += sum(n - 1 for n in dup.values())
print(f'{s["code"]:8s} {len(vs):6d} {len(sig):6d} {len(dup):6d} {mx:9d} {verdict}')
print(f'\n存在一对多的系列: {len(bad)} 个, 受影响型号 {tot_codes} 个')
if bad:
print('\n明细 (系列 / 变体 / 重复组数 / 一组最多几个型号):')
for c, n, dup, mx in sorted(bad, key=lambda x: -x[3]):
print(f' {c:8s} {n:5d} 变体 {dup:3d} 组 max={mx}')

View File

@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
"""
补齐参数组合空间: 为"规则内合法但库中缺失"的组合生成零件号, 并指向系列级占位数模。
背景: 当前型号表由真实 STEP 文件扫描而来 —— 没有数模就没有型号, 用户选完也拿不到代号。
本脚本按规则反向生成缺失组合, 使其可选、可出号; 数模用每系列一个占位件, 后续 AI 补真实数模。
用法:
PYTHONUTF8=1 python gen-missing-combos.py --report # 只统计, 不写文件
PYTHONUTF8=1 python gen-missing-combos.py --write # 写入 CSV + meta 模板 + 占位清单
"""
import json, io, os, csv, itertools, sys, collections
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
REPORT = '--report' in sys.argv
WRITE = '--write' in sys.argv
# 只有"模板能 100% 对齐"的系列才补 (由 infer-sku-template.py 验证)。
# 其余系列 (旧码混用/编码结构特殊) 保持现状, 不生成可能不存在的零件号。
SAFE = {
'KC', 'KCB', 'KCS', 'KS', 'KSB', 'KSU', 'M', 'MS', 'MSC', 'MAL', 'MALC',
'ULA', 'ULL', 'ULM', 'ULO', 'ULP', 'ULT', 'ULX', 'ULY', 'ULZ', 'USL', 'USSL',
}
# 各系列的编码模板 (由 infer-sku-template.py 反推, 100% 对齐)
# {参数:补零位数}; 空值渲染为空串
TEMPLATES = {
'KC': ('KC{type}{bore}-{stroke}{magnet}{mount}', {}),
'KCB': ('KCB{bore}-{stroke}{magnet}{mount}', {}),
'KCS': ('KCS{type}{bore}-{stroke}{magnet}{mount}', {}),
'KS': ('KS{type}{bore}-{stroke}{magnet}{mount}', {}),
'KSB': ('KSB{bore}-{stroke1}x{stroke2}{magnet}{mount}', {}),
'KSU': ('KSU{type}{bore}-{stroke}{magnet}{mount}', {}),
'M': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
'MS': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
'MSC': ('{action}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
'MAL': ('{type}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
'MALC': ('{type}{bore}-{stroke}{magnet}{rod}{mount}{cap}', {}),
'ULA': ('ULA{size}{stroke:pad3}{rod}{mount}{sensor}0', {}),
'ULL': ('ULL{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
'ULM': ('ULM{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
'ULO': ('ULO{size}{stroke:pad3}{rod}{sensor}0', {}),
'ULP': ('ULP{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
'ULT': ('ULT{size}{stroke:pad3}{rod}{sensor}0', {}),
'ULX': ('ULX{size}{stroke:pad3}{rod}{sensor}0', {}),
'ULY': ('ULY{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
'ULZ': ('ULZ{size}{stroke:pad3}{rod}{orient}{sensor}0', {}),
'USL': ('USL{size}{pcmodel}{pin_dia}{locating}{prog}{sensor}0', {}),
'USSL': ('USSL{size}{pcmodel}{hook_pos}{pin_dia}{locating}{prog}{sensor}0', {}),
}
PLACEHOLDER_STEP = 'PLACEHOLDER.step' # 每系列共用
def S(v):
return '' if v is None else str(v)
def eq(a, b):
return S(a).lower() == S(b).lower()
def cond_match(c, prm):
if not c:
return True
val = prm.get(c['Param'])
if val is None:
return False
op, cv = c['Op'], c['Value']
if op == 'eq':
return eq(val, cv)
if op == 'ne':
return not eq(val, cv)
if op == 'in':
return isinstance(cv, list) and any(eq(val, x) for x in cv)
try:
a, b = float(S(val)), float(S(cv))
except ValueError:
return False
return {'gt': a > b, 'ge': a >= b, 'lt': a < b, 'le': a <= b}.get(op, False)
def violated(rules, prm):
for r in rules or []:
if cond_match(r.get('If'), prm) and not cond_match(r.get('Then'), prm):
return True
return False
def render(tpl, prm):
"""模板渲染: {field} / {field:padN}; 空值渲染为空串。"""
out, i = '', 0
while i < len(tpl):
if tpl[i] == '{':
j = tpl.index('}', i)
tok = tpl[i + 1:j]
name, _, pad = tok.partition(':')
name = name.strip()
v = S(prm.get(name, ''))
if pad.startswith('pad') and v != '':
try:
v = v.rjust(int(pad[3:]), '0')
except ValueError:
return None
out += v
i = j + 1
else:
out += tpl[i]
i += 1
return out
def load_meta(code):
p = os.path.join(CAT, 'series', code + '.meta.json')
return json.load(io.open(p, encoding='utf-8'))
def link_or_copy(src, dst):
"""建硬链接; 不支持则回退复制。返回 True 表示已创建。
注: 必须先建链接再考虑失败回退, 绝不能先删目标再建 (删了又建失败 = 文件丢失)。"""
if os.path.exists(dst):
return False
try:
os.link(src, dst)
return True
except OSError:
pass
try:
import shutil
shutil.copy2(src, dst)
return True
except OSError:
return False
def main():
summary = []
plan = {}
for code in sorted(SAFE):
meta = load_meta(code)
s = meta['series']
params = s['parameters']
csvp = os.path.join(CAT, 'csv', code + '.csv')
if not os.path.exists(csvp):
print(f'[{code}] 无 CSV, 跳过')
continue
rows = list(csv.DictReader(io.open(csvp, encoding='utf-8-sig')))
existing = {r['model_code'] for r in rows}
# 值域: 数值参数取 CSV 实际出现过的值 (不用 meta.values, 避免含非法/未出现值)
doms = collections.OrderedDict()
for p in params:
pc = p['code']
if p['type'] == 'range':
doms[pc] = None # 连续域不枚举
continue
vals = list(dict.fromkeys(r.get(pc, '') for r in rows))
vals = [v for v in vals if v != '' or True]
doms[pc] = vals
if any(v is None for v in doms.values()):
print(f'[{code}] 含 range 参数, 跳过 (连续域无法枚举)')
continue
tpl, _ = TEMPLATES[code]
codes = list(doms.keys())
combos = list(itertools.product(*[doms[k] for k in codes]))
added = []
for combo in combos:
prm = dict(zip(codes, combo))
if violated(s.get('rules'), prm):
continue
mc = render(tpl, prm)
if not mc or mc in existing:
continue
existing.add(mc)
added.append((mc, prm))
summary.append((code, len(rows), len(combos), len(added)))
if added:
plan[code] = (tpl, params, added, rows)
# 输出
print(f'{"系列":7s} {"现有":>7s} {"笛卡尔":>9s} {"待补":>7s}')
print('-' * 36)
tot_new = 0
for code, n, cart, add in sorted(summary, key=lambda x: -x[3]):
tot_new += add
print(f'{code:7s} {n:7d} {cart:9,d} {add:7,d}')
print(f'\n合计待补 {tot_new:,} 个型号')
if not WRITE:
print('\n(--report 模式, 未写文件)')
return
# ---------- 写入 ----------
step_dir = os.path.join(CAT, 'step')
placeholder = os.path.join(step_dir, PLACEHOLDER_STEP)
manifest = [] # 待 AI 补真实数模的清单
n_csv = n_ph = 0
for code, (tpl, params, added, rows) in plan.items():
csvp = os.path.join(CAT, 'csv', code + '.csv')
header = list(rows[0].keys())
dims = [h for h in header if h not in ('model_code', 'step_file')]
# 新增行: step_file 统一指向 PLACEHOLDER.step ——
# 构建器按 v.step 的路径复制 (同路径只存一份), 3157 个型号共用一个占位件,
# 既不占磁盘也不撑大数据包。AI 补真实数模后把该行改成 "<型号>.step" 即可。
with io.open(csvp, 'a', encoding='utf-8-sig', newline='') as fp:
w = csv.writer(fp)
for mc, prm in added:
w.writerow([mc] + [S(prm.get(d, '')) for d in dims] + [PLACEHOLDER_STEP])
manifest.append(f'{code},{mc},{",".join(d + "=" + S(prm.get(d, "")) for d in dims)}')
n_csv += 1
print(f'[{code}] CSV +{len(added)}')
# 待补清单 (给 AI 补数模用)
mf = os.path.join(CAT, '_pending_models.csv')
with io.open(mf, 'w', encoding='utf-8-sig', newline='') as fp:
fp.write('series,model_code,params\n')
fp.write('\n'.join(manifest) + '\n')
print(f'\n写入完成: CSV 新增 {n_csv} 行, 占位 STEP {n_ph} 个')
print(f'待补数模清单: {mf} ({len(manifest)} 个型号)')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
"""
生成占位数模 (每系列一个通用的"待补数模"方块)。
用途: 为「参数组合补齐」生成的型号提供占位 STEP —— 构建器要求每个型号必须有数模文件。
真实数模后续由 AI 参照同系列已有型号补出, 覆盖同名文件即可。
产出:
catalog/step/PLACEHOLDER.step 通用的方块 (ISO-10303-21, 标准 B-rep)
catalog/step/_placeholder/<系列>.step 每系列一份 (便于按系列替换/识别)
方块边长 60mm, 居中于原点。
用法: PYTHONUTF8=1 python gen-placeholder-step.py
"""
import os
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
STEP_DIR = os.path.join(CAT, 'step')
# 方块尺寸 (mm)
SX, SY, SZ = 60.0, 60.0, 60.0
HX, HY, HZ = SX / 2, SY / 2, SZ / 2
def build_step(label):
"""生成一个合法的 STEP 文件内容: 单位立方体 B-rep。"""
L = []
L.append('ISO-10303-21;')
L.append('HEADER;')
L.append("FILE_DESCRIPTION(('PLACEHOLDER - real CAD model pending'),'2;1');")
L.append("FILE_NAME('%s','2026-01-01T00:00:00',('ONEBOT'),('ONEBOT'),"
"'gen-placeholder-step.py','ONEBOT Catalog','');" % label)
L.append("FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));")
L.append('ENDSEC;')
L.append('DATA;')
L.append("#1=APPLICATION_CONTEXT('core data for automotive mechanical design processes');")
L.append("#2=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#1);")
L.append("#3=PRODUCT_CONTEXT('',#1,'mechanical');")
L.append("#4=PRODUCT_DEFINITION_CONTEXT('part definition',#1,'design');")
L.append("#5=PRODUCT('%s','PLACEHOLDER',$,(#3));" % label)
L.append("#6=PRODUCT_DEFINITION_FORMATION('','',#5);")
L.append("#7=PRODUCT_DEFINITION('design','',#6,#4);")
L.append("#8=PRODUCT_DEFINITION_SHAPE('','',#7);")
L.append("#9=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));")
L.append("#10=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));")
L.append("#11=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());")
L.append("#12=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(0.001),#9,"
"'distance_accuracy_value','');")
L.append("#13=(GEOMETRIC_REPRESENTATION_CONTEXT(3)"
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#12))"
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#9,#10,#11))REPRESENTATION_CONTEXT('',''));")
# 8 个顶点坐标
corners = [
(-HX, -HY, -HZ), (HX, -HY, -HZ), (HX, HY, -HZ), (-HX, HY, -HZ),
(-HX, -HY, HZ), (HX, -HY, HZ), (HX, HY, HZ), (-HX, HY, HZ),
]
eid = 20
pt_ids, vx_ids = [], []
for (x, y, z) in corners:
L.append('#%d=CARTESIAN_POINT('',(%.3f,%.3f,%.3f));' % (eid, x, y, z))
pt_ids.append(eid); eid += 1
L.append('#%d=DIRECTION('',(0.,0.,1.));' % eid); dz = eid; eid += 1
L.append('#%d=DIRECTION('',(1.,0.,0.));' % eid); dx = eid; eid += 1
for pid in pt_ids:
L.append('#%d=VERTEX_POINT('',#%d);' % (eid, pid))
vx_ids.append(eid); eid += 1
# 12 条棱
edges = [(0, 1), (1, 2), (2, 3), (3, 0),
(4, 5), (5, 6), (6, 7), (7, 4),
(0, 4), (1, 5), (2, 6), (3, 7)]
edge_ids = []
for (a, b) in edges:
L.append('#%d=LINE('',#%d,#%d);' % (eid, pt_ids[a], pt_ids[b]))
ln = eid; eid += 1
L.append('#%d=VECTOR('',#%d,1.0);' % (eid, dx)); vc = eid; eid += 1
L.append('#%d=EDGE_CURVE('',#%d,#%d,#%d,.T.);'
% (eid, vx_ids[a], vx_ids[b], ln))
ec = eid; eid += 1
L.append('#%d=ORIENTED_EDGE('',*,*,#%d,.T.);' % (eid, ec))
edge_ids.append(eid); eid += 1
# 6 个面
faces = [
[0, 1, 2, 3], [4, 5, 6, 7],
[0, 1, 5, 4], [1, 2, 6, 5],
[2, 3, 7, 6], [3, 0, 4, 7],
]
normals = [(0, 0, -1), (0, 0, 1), (0, -1, 0), (1, 0, 0), (0, 1, 0), (-1, 0, 0)]
face_ids = []
for fi, f in enumerate(faces):
loops = []
for k in range(4):
loops.append(edge_ids[f[k]])
L.append('#%d=EDGE_LOOP('',(%s));'
% (eid, ','.join('#%d' % x for x in loops)))
el = eid; eid += 1
nx, ny, nz = normals[fi]
L.append('#%d=DIRECTION('',(%.1f,%.1f,%.1f));' % (eid, nx, ny, nz))
nd = eid; eid += 1
L.append("#%d=AXIS2_PLACEMENT_3D('',#%d,#%d,$);" % (eid, pt_ids[f[0]], nd))
ax = eid; eid += 1
L.append('#%d=PLANE(\'\',#%d);' % (eid, ax))
pl = eid; eid += 1
L.append('#%d=ADVANCED_FACE(\'\',(#%d),#%d,.T.);' % (eid, el, pl))
face_ids.append(eid); eid += 1
L.append('#%d=CLOSED_SHELL(\'\',(%s));'
% (eid, ','.join('#%d' % x for x in face_ids)))
sh = eid; eid += 1
L.append("#%d=MANIFOLD_SOLID_BREP('PLACEHOLDER',#%d);" % (eid, sh))
msb = eid; eid += 1
L.append('#%d=ADVANCED_BREP_SHAPE_REPRESENTATION(\'PLACEHOLDER\',(#%d,#%d),#13);'
% (eid, msb, eid + 1))
abr = eid; eid += 1
L.append("#%d=AXIS2_PLACEMENT_3D('',#%d,#%d,#%d);" % (eid, pt_ids[0], dz, dx))
ax2 = eid; eid += 1
L.append("#%d=SHAPE_DEFINITION_REPRESENTATION(#8,#%d);" % (eid, abr))
L.append('ENDSEC;')
L.append('END-ISO-10303-21;')
return '\n'.join(L) + '\n'
def main():
os.makedirs(STEP_DIR, exist_ok=True)
content = build_step('PLACEHOLDER')
p = os.path.join(STEP_DIR, 'PLACEHOLDER.step')
with open(p, 'w', encoding='ascii', newline='\r\n') as f:
f.write(content)
print(f'写入 {p} ({os.path.getsize(p)} 字节)')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
"""
编码模板反推 v2 —— 支持零填充与空值占位符。
v1 的 explain() 贪心匹配"非零填充", 把 ULP50040AEK0 的 "040" 拆成 字面量"0" + 值"40",
产生大量假阴性。真实规则是:
- 数值参数常带前导零 (stroke=40 -> "040")
- 枚举参数为空时, 编码里用 '_' 占位 (UAGP075_K045WS0)
本脚本对每个参数尝试 {原值, 各前导零位数, '_'占位} 的所有组合, 取能对齐的;
若某系列的"字面量序列"完全一致 → 可模板化。
用法: PYTHONUTF8=1 python infer-sku-template.py
"""
import json, io, collections, sys
CAT = r'D:/开发/OnebotCatalog/release/web_2026.08/catalog.json'
d = json.load(io.open(CAT, encoding='utf-8-sig'))
MAXPAD = 4
def align_all(code, params, values):
"""返回全部可能的字面量序列 (元组)。每个参数尝试: 原值 / 前导零 / '_'占位。"""
results = []
def dfs(i, pos, lits):
if i == len(params):
results.append(tuple(lits + [code[pos:]]))
return
pc = params[i]
v = str(values.get(pc, ''))
cands = []
if v == '':
cands = ['_', ''] # 空值: 占位符或什么都不写
else:
cands = [v] + ['0' * z + v for z in range(1, MAXPAD + 1)]
seen = set()
for pat in cands:
if pat in seen:
continue
seen.add(pat)
if pat == '':
dfs(i + 1, pos, lits + [''])
continue
idx = code.find(pat, pos)
if idx >= 0:
dfs(i + 1, idx + len(pat), lits + [code[pos:idx]])
dfs(0, 0, [])
return results
def render_template(params, lits):
"""字面量序列 -> 模板串。空值参数渲染为 '_'。"""
out = ''
for i, pc in enumerate(params):
out += lits[i] + '{' + pc + '}'
out += lits[len(params)]
return out
good, bad = [], []
for s in d['series']:
vs = s.get('variants') or []
if not vs:
continue
params = [p['code'] for p in s['parameters']]
lit_sets = []
unaligned = 0
for v in vs:
alts = align_all(v['modelCode'], params, v['params'])
if not alts:
unaligned += 1
continue
lit_sets.append(alts)
if not lit_sets:
bad.append((s['code'], len(vs), '完全无法对齐', [], 0))
continue
# 统计"出现在最多变体里"的字面量序列 (每个变体取任一可对齐解)
cnt = collections.Counter()
for alts in lit_sets:
for a in set(alts):
cnt[a] += 1
top, hit = cnt.most_common(1)[0]
rate = hit / len(vs) * 100
if rate >= 99.5 and unaligned == 0:
good.append((s['code'], len(vs), rate, render_template(params, top), len(cnt)))
else:
bad.append((s['code'], len(vs), f'{rate:.1f}% ({len(cnt)} 种)', cnt.most_common(3), unaligned))
print('=' * 96)
print(f'可模板化的系列 ({len(good)} 个)')
print('=' * 96)
for code, n, rate, tpl, k in sorted(good):
print(f'{code:7s} {n:6d} 变体 {rate:6.1f}% {tpl}')
print()
print('=' * 96)
print(f'不可模板化的系列 ({len(bad)} 个)')
print('=' * 96)
for code, n, why, tops, un in sorted(bad, key=lambda x: -x[1]):
print(f'{code:7s} {n:6d} 变体 {why}' + (f' 未对齐 {un}' if un else ''))
for lits, c in tops[:3]:
print(f' {c:5d}× {lits}')
print(f'\n合计: 可模板化 {len(good)} / 共 {len(good) + len(bad)} 个系列')

View File

@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
"""
重写待补数模清单, 增加「核对」相关列, 便于 AI 逐个核实后补真实数模。
背景: v2.1 按编码模板外推生成了 3157 个组合, 但它们是【候选代号】而非已确认的产品 ——
规律在已有型号上 100% 成立, 外推区域是否有真实产品未经核实。
本清单把每个候选与其"最近的真实型号"并列, 便于逐个比对核实。
输出列:
series 系列码
model_code 候选零件号 (已进目录, 选型可选)
params 完整参数
nearest_real 同系列中最接近的【真实数模】型号 (按参数相同个数最多, 平手取编码最近)
diff 与 nearest_real 不同的参数 (供人工/AI 判断该差异是否真实存在)
step_file 当前指向 PLACEHOLDER.step; 补完真实数模后改为 <候选型号>.step
status 固定为 pending (待核对+待补数模)
用法: PYTHONUTF8=1 python make-pending-list.py
"""
import csv, io, os, json, glob, collections
CAT = r'D:/开发/OnebotCatalog/onebot-data/catalog'
OUT = os.path.join(CAT, '_pending_models.csv')
PLACEHOLDER = 'PLACEHOLDER.step'
def load_real_models():
"""返回 {系列: [(model_code, {参数})]} —— 只含指向真实数模的型号。"""
real = {}
for f in glob.glob(os.path.join(CAT, 'csv', '*.csv')):
code = os.path.basename(f)[:-4]
rows = []
for r in csv.DictReader(io.open(f, encoding='utf-8-sig')):
if r.get('step_file') == PLACEHOLDER:
continue # 跳过占位行
prm = {k: v for k, v in r.items() if k not in ('model_code', 'step_file')}
rows.append((r['model_code'], prm))
if rows:
real[code] = rows
return real
def dist(params_a, params_b):
"""参数相同个数 (越大越接近)。"""
keys = set(params_a) | set(params_b)
return sum(1 for k in keys if str(params_a.get(k, '')) == str(params_b.get(k, '')))
def main():
real = load_real_models()
total_real = sum(len(v) for v in real.values())
print(f'真实数模型号: {total_real} 个, 覆盖 {len(real)} 个系列')
# 收集候选项
pending = []
for f in sorted(glob.glob(os.path.join(CAT, 'csv', '*.csv'))):
code = os.path.basename(f)[:-4]
for r in csv.DictReader(io.open(f, encoding='utf-8-sig')):
if r.get('step_file') != PLACEHOLDER:
continue
prm = {k: v for k, v in r.items() if k not in ('model_code', 'step_file')}
pending.append((code, r['model_code'], prm))
print(f'待补候选: {len(pending)} 个')
rows = []
for code, mc, prm in pending:
cands = real.get(code) or []
near, ndiff = '', ''
if cands:
# 最接近的真实型号: 参数相同个数最多; 平手取编码字典序 (稳定)
best = max(cands, key=lambda x: (dist(prm, x[1]), -len(x[0])))
near = best[0]
diffs = [f'{k}={prm.get(k, "")}≠{best[1].get(k, "")}'
for k in sorted(set(prm) | set(best[1]))
if str(prm.get(k, '')) != str(best[1].get(k, ''))]
ndiff = '; '.join(diffs)
rows.append([code, mc,
' '.join(f'{k}={v}' for k, v in prm.items()),
near, ndiff, PLACEHOLDER, 'pending'])
with io.open(OUT, 'w', encoding='utf-8-sig', newline='') as fp:
w = csv.writer(fp)
w.writerow(['series', 'model_code', 'params', 'nearest_real',
'diff_from_nearest', 'step_file', 'status'])
w.writerows(rows)
print(f'\n写入 {OUT} ({len(rows)} 行)')
# 覆盖度: 多少候选能找到同系列真实型号作参照
withref = sum(1 for r in rows if r[3])
print(f' 有同系列真实型号可参照: {withref}/{len(rows)}')
nop = [r[0] for r in rows if not r[3]]
if nop:
print(f' 无参照的系列: {sorted(set(nop))}')
# 按系列 + 缺失维度统计
print('\n各系列待补 (按"与最近真实型号差异的维度数"分布):')
bycat = collections.defaultdict(collections.Counter)
for r in rows:
n = len([x for x in r[4].split('; ') if x]) if r[4] else -1
bycat[r[0]][n] += 1
for code in sorted(bycat):
dists = bycat[code]
parts = ', '.join(f'{k}维差:{v}' for k, v in sorted(dists.items()) if k >= 0)
print(f' {code:6s} 共{sum(dists.values()):5d} {parts}')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
"""
量化"参数组合缺口": 合法组合 (域 + 规则内) vs 库中已有变体, 以及补全的规模。
对每个系列:
- 现有变体数
- 笛卡尔积规模 (各参数值域相乘)
- 规则过滤后的合法组合数 (复用 C# Configurator 同语义的规则求值)
- 缺口 = 合法组合 - 已覆盖
这决定"补全组合数据"的可行性与数据量。
"""
import json, io, itertools
CAT = r'D:/开发/OnebotCatalog/release/web_2026.08/catalog.json'
d = json.load(io.open(CAT, encoding='utf-8-sig'))
def S(v):
return '' if v is None else str(v)
def eq(a, b):
return S(a).lower() == S(b).lower()
def cond_match(c, params):
"""规则条件求值 (与 app.js condMatches / C# CondMatches 同语义)。"""
if not c:
return True
val = params.get(c['Param'])
if val is None:
return False
op = c['Op']
cv = c['Value']
if op == 'eq':
return eq(val, cv)
if op == 'ne':
return not eq(val, cv)
if op == 'in':
return isinstance(cv, list) and any(eq(val, x) for x in cv)
try:
a, b = float(S(val)), float(S(cv))
except ValueError:
return False
return {'gt': a > b, 'ge': a >= b, 'lt': a < b, 'le': a <= b}.get(op, False)
def rule_violated(rules, params):
for r in rules or []:
if cond_match(r.get('If'), params) and not cond_match(r.get('Then'), params):
return True
return False
print(f'{"系列":7s} {"现有":>6s} {"笛卡尔":>9s} {"规则内":>9s} {"缺口":>9s} {"倍数":>5s}')
print('-' * 62)
tot_exist = tot_gap = 0
rows = []
for s in d['series']:
vs = s.get('variants') or []
if not vs:
continue
params = s['parameters']
doms = []
for p in params:
vals = p.get('values') or []
if not vals:
doms = None
break
doms.append(vals)
if doms is None:
rows.append((s['code'], len(vs), None, None, None))
continue
cart = 1
for vals in doms:
cart *= len(vals)
# 规则过滤 (笛卡尔过大时抽样估算)
legal = 0
if cart <= 400000:
for combo in itertools.product(*doms):
prm = {p['code']: v for p, v in zip(params, combo)}
if not rule_violated(s.get('rules'), prm):
legal += 1
else:
legal = None # 太大, 跳过精确统计
gap = (legal - len(vs)) if legal is not None else None
rows.append((s['code'], len(vs), cart, legal, gap))
if legal is not None:
tot_exist += len(vs)
tot_gap += max(0, gap)
for code, n, cart, legal, gap in sorted(rows, key=lambda x: -(x[4] or -1)):
c = f'{cart:,}' if cart else '—'
l = f'{legal:,}' if legal else '—'
g = f'{gap:,}' if gap is not None else '—'
mult = f'{legal/n:.1f}x' if legal and n else '—'
flag = ' <-- 缺口大' if gap and gap > 50000 else ''
print(f'{code:7s} {n:6d} {c:>9s} {l:>9s} {g:>9s} {mult:>5s}{flag}')
print(f'\n合计: 现有 {tot_exist:,} 变体, 缺口 {tot_gap:,} 组合')
print(f'补全后约 {tot_exist + tot_gap:,} 变体 (是现在的 {(tot_exist+tot_gap)/tot_exist:.1f} 倍)')

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
验证: 用模板对【已有型号】重新渲染, 必须与库中编码完全一致。
凡不一致 → 模板有误, 绝不能用它生成新编码 (会造出不存在的零件号)。
"""
import json, io, os, csv, sys, importlib.util
HERE = os.path.dirname(os.path.abspath(__file__))
spec = importlib.util.spec_from_file_location('gmc', os.path.join(HERE, 'gen-missing-combos.py'))
gmc = importlib.util.module_from_spec(spec)
sys.argv = ['x'] # 防止执行 main
spec.loader.exec_module(gmc)
CAT = gmc.CAT
print(f'{"系列":7s} {"变体":>6s} {"模板一致":>9s} {"失败":>6s} 反例')
print('-' * 74)
allok = True
for code in sorted(gmc.SAFE):
if code not in gmc.TEMPLATES:
continue
tpl, _ = gmc.TEMPLATES[code]
rows = list(csv.DictReader(io.open(os.path.join(CAT, 'csv', code + '.csv'), encoding='utf-8-sig')))
ok = fail = 0
bad = []
for r in rows:
prm = {k: v for k, v in r.items() if k not in ('model_code', 'step_file')}
got = gmc.render(tpl, prm)
if got == r['model_code']:
ok += 1
else:
fail += 1
if len(bad) < 3:
bad.append(f'{r["model_code"]} -> 渲染"{got}"')
flag = '' if fail == 0 else ' <-- 模板有误!'
if fail:
allok = False
print(f'{code:7s} {len(rows):6d} {ok:9d} {fail:6d} {bad[0] if bad else ""}{flag}')
print()
print('结论:', '所有模板对已有型号 100% 复现 ✅' if allok else '存在模板错误, 不可用于生成新编码 ❌')

View File

@@ -36,6 +36,118 @@ namespace Ounibo.Catalog.App
public override string ToString() { return Text; } public override string ToString() { return Text; }
} }
/// <summary>
/// 选项块 (DESTACO 风, 2026-09-15): 替代下拉菜单的单值可选卡片。
/// 三态: 可选 / 已选 / 不可选 (置灰划线但保留占位, 让用户看到选项存在)。
/// </summary>
public class OptionTile : Border
{
public object Value;
public ParameterDef Param;
readonly TextBlock _code;
readonly TextBlock _desc;
bool _usable = true, _on;
static readonly Brush BorderIdle = new SolidColorBrush(Color.FromRgb(0xE5, 0xE7, 0xEB));
static readonly Brush BorderHover = new SolidColorBrush(Color.FromRgb(0x2D, 0xD4, 0xBF));
static readonly Brush BorderOn = new SolidColorBrush(Color.FromRgb(0x14, 0xB8, 0xA6));
static readonly Brush BgIdle = Brushes.White;
static readonly Brush BgHover = new SolidColorBrush(Color.FromRgb(0xF0, 0xFD, 0xFA));
static readonly Brush BgOn = new SolidColorBrush(Color.FromRgb(0xF0, 0xFD, 0xFA));
static readonly Brush BgOff = new SolidColorBrush(Color.FromRgb(0xF9, 0xFA, 0xFB));
static readonly Brush FgCodeIdle = new SolidColorBrush(Color.FromRgb(0x11, 0x18, 0x27));
static readonly Brush FgCodeOn = new SolidColorBrush(Color.FromRgb(0x0F, 0x76, 0x6E));
public OptionTile(ParameterDef p, object value, string code, string desc, Action<OptionTile> onClick)
{
Param = p; Value = value;
BorderThickness = new Thickness(1.5);
CornerRadius = new CornerRadius(10);
Padding = new Thickness(9, 7, 9, 7);
Margin = new Thickness(0, 0, 6, 6);
MinWidth = 58;
Cursor = Cursors.Hand;
Background = BgIdle;
BorderBrush = BorderIdle;
var sp = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
_code = new TextBlock
{
Text = code,
FontFamily = new FontFamily("Consolas, monospace"),
FontWeight = FontWeights.Bold,
FontSize = 13,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center
};
sp.Children.Add(_code);
if (!string.IsNullOrEmpty(desc) && desc != code)
{
_desc = new TextBlock
{
Text = desc,
FontSize = 10,
Margin = new Thickness(0, 2, 0, 0),
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
MaxWidth = 120,
Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF))
};
sp.Children.Add(_desc);
}
Child = sp;
MouseEnter += (s, e) => { if (_usable && !_on) { BorderBrush = BorderHover; Background = BgHover; } };
MouseLeave += (s, e) => { if (_usable && !_on) { BorderBrush = BorderIdle; Background = BgIdle; } };
MouseLeftButtonUp += (s, e) => { if (_usable) onClick(this); };
}
/// <summary>设置可用性 (被联动规则排除时置灰但不移除)。</summary>
public void SetUsable(bool usable)
{
_usable = usable;
Cursor = usable ? Cursors.Hand : Cursors.Arrow;
if (!usable)
{
Background = BgOff; BorderBrush = BorderIdle;
BorderThickness = new Thickness(1.5);
_code.Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF));
_code.TextDecorations = TextDecorations.Strikethrough;
Opacity = 0.4;
if (_desc != null) _desc.Opacity = 0.7;
}
else
{
Opacity = 1;
_code.TextDecorations = null;
if (_desc != null) _desc.Opacity = 1;
ApplyState();
}
}
/// <summary>设置选中态。</summary>
public void SetOn(bool on)
{
_on = on;
if (_usable) ApplyState();
}
void ApplyState()
{
if (_on)
{
Background = BgOn; BorderBrush = BorderOn;
_code.Foreground = FgCodeOn;
}
else
{
Background = BgIdle; BorderBrush = BorderIdle;
_code.Foreground = FgCodeIdle;
}
}
}
/// <summary> /// <summary>
/// 主窗口: 目录树 + 参数选型 + 3D 预览 + 尺寸图/参数表/数据表 + 搜索 + STEP 下载/批量下载。 /// 主窗口: 目录树 + 参数选型 + 3D 预览 + 尺寸图/参数表/数据表 + 搜索 + STEP 下载/批量下载。
/// </summary> /// </summary>
@@ -48,6 +160,12 @@ namespace Ounibo.Catalog.App
Series _series; Series _series;
readonly Dictionary<string, object> _sel = new Dictionary<string, object>(); readonly Dictionary<string, object> _sel = new Dictionary<string, object>();
readonly Dictionary<string, ComboBox> _boxes = new Dictionary<string, ComboBox>(); readonly Dictionary<string, ComboBox> _boxes = new Dictionary<string, ComboBox>();
// 选项块 (DESTACO 风, 2026-09-15): 每个参数一组可点选项块, 替代下拉菜单
readonly Dictionary<string, List<OptionTile>> _tiles = new Dictionary<string, List<OptionTile>>();
readonly Dictionary<string, WrapPanel> _tilePanels = new Dictionary<string, WrapPanel>();
TextBlock _skuCode, _skuHint; // 零件号实时条
Border _skuBar;
ProgressBar _skuProg;
readonly Dictionary<string, double?[]> _ranges = new Dictionary<string, double?[]>(); readonly Dictionary<string, double?[]> _ranges = new Dictionary<string, double?[]>();
readonly Dictionary<string, TextBox[]> _rangeBoxes = new Dictionary<string, TextBox[]>(); readonly Dictionary<string, TextBox[]> _rangeBoxes = new Dictionary<string, TextBox[]>();
List<SearchEngine.SearchItem> _searchResult; List<SearchEngine.SearchItem> _searchResult;
@@ -165,6 +283,53 @@ namespace Ounibo.Catalog.App
var left = new StackPanel { Margin = new Thickness(8) }; var left = new StackPanel { Margin = new Thickness(8) };
_modelTitle = new TextBlock { FontSize = 16, FontWeight = FontWeights.Bold, Margin = new Thickness(0, 0, 0, 8), TextWrapping = TextWrapping.Wrap }; _modelTitle = new TextBlock { FontSize = 16, FontWeight = FontWeights.Bold, Margin = new Thickness(0, 0, 0, 8), TextWrapping = TextWrapping.Wrap };
left.Children.Add(_modelTitle); left.Children.Add(_modelTitle);
// 零件号实时条 (DESTACO 风, 2026-09-15): 边选边拼, 已确定片段高亮 / 待定虚化
_skuBar = new Border
{
CornerRadius = new CornerRadius(12),
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0x99, 0xF6, 0xE4)),
Background = new LinearGradientBrush(
Color.FromRgb(0xF0, 0xFD, 0xFA), Colors.White, 45),
Padding = new Thickness(12, 10, 12, 10),
Margin = new Thickness(0, 0, 0, 10)
};
var skuInner = new StackPanel();
skuInner.Children.Add(new TextBlock
{
Text = _lang.T("零件号 Part Number", "Part Number"),
FontSize = 10,
Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)),
Margin = new Thickness(0, 0, 0, 4)
});
_skuCode = new TextBlock
{
FontFamily = new FontFamily("Consolas, monospace"),
FontSize = 17, FontWeight = FontWeights.Bold,
TextWrapping = TextWrapping.Wrap,
MinHeight = 22
};
skuInner.Children.Add(_skuCode);
_skuProg = new ProgressBar
{
Height = 3, Margin = new Thickness(0, 7, 0, 0),
Minimum = 0, Maximum = 1, Value = 0,
Foreground = new SolidColorBrush(Color.FromRgb(0x14, 0xB8, 0xA6)),
Background = new SolidColorBrush(Color.FromRgb(0xCC, 0xFB, 0xF1)),
BorderThickness = new Thickness(0)
};
skuInner.Children.Add(_skuProg);
_skuHint = new TextBlock
{
FontSize = 11, Margin = new Thickness(0, 6, 0, 0),
TextWrapping = TextWrapping.Wrap,
Foreground = new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80))
};
skuInner.Children.Add(_skuHint);
_skuBar.Child = skuInner;
left.Children.Add(_skuBar);
_paramPanel = new StackPanel(); _paramPanel = new StackPanel();
left.Children.Add(_paramPanel); // 参数最多 7 行, 直接平铺不滚动 (避免下方参数被遮挡) left.Children.Add(_paramPanel); // 参数最多 7 行, 直接平铺不滚动 (避免下方参数被遮挡)
// 下载操作区 (2026-08-24, 与网页版一致: 参数下方并排 下载 STEP / 下载 PDF, 同款配色) // 下载操作区 (2026-08-24, 与网页版一致: 参数下方并排 下载 STEP / 下载 PDF, 同款配色)
@@ -494,6 +659,8 @@ namespace Ounibo.Catalog.App
_series = null; _cfg = null; _series = null; _cfg = null;
_paramPanel.Children.Clear(); _paramPanel.Children.Clear();
_boxes.Clear(); _sel.Clear(); _boxes.Clear(); _sel.Clear();
_tiles.Clear(); _tilePanels.Clear();
if (_skuBar != null) _skuBar.Visibility = Visibility.Collapsed;
} }
void TreeSelected(object sender, RoutedPropertyChangedEventArgs<object> e) void TreeSelected(object sender, RoutedPropertyChangedEventArgs<object> e)
@@ -512,26 +679,67 @@ namespace Ounibo.Catalog.App
_cfg = new Configurator(s); _cfg = new Configurator(s);
_sel.Clear(); _sel.Clear();
_boxes.Clear(); _boxes.Clear();
_tiles.Clear();
_tilePanels.Clear();
_paramPanel.Children.Clear(); _paramPanel.Children.Clear();
_ranges.Clear(); _rangeBoxes.Clear(); _rangePanel.Children.Clear(); _ranges.Clear(); _rangeBoxes.Clear(); _rangePanel.Children.Clear();
if (_skuBar != null) _skuBar.Visibility = Visibility.Visible;
foreach (var p in s.parameters) foreach (var p in s.parameters)
{ {
var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 8) }; var block = new StackPanel { Margin = new Thickness(0, 0, 0, 10) };
row.Children.Add(new TextBlock { Text = ParamLabel(p), Width = 120, VerticalAlignment = VerticalAlignment.Center }); // 维度标题: 小号大写 + 已选值回显
var cb = new ComboBox { Width = 150, Tag = p }; var head = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 5) };
head.Children.Add(new TextBlock
{
Text = ParamLabel(p).ToUpperInvariant(),
FontSize = 10.5, FontWeight = FontWeights.Bold,
Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)),
VerticalAlignment = VerticalAlignment.Center
});
var picked = new TextBlock
{
FontSize = 11.5, FontWeight = FontWeights.SemiBold,
Foreground = new SolidColorBrush(Color.FromRgb(0x0F, 0x76, 0x6E)),
Margin = new Thickness(7, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center
};
picked.Tag = p.code; // 供 RefreshOptions 回显
head.Children.Add(picked);
block.Children.Add(head);
if (p.type == "range") if (p.type == "range")
{ {
// 参数化 range 域: 可编辑下拉 (标准档位快捷选 + 自由输入, 与网页版一致) // 连续域无法枚举成选项块 → 保留可编辑下拉 (标准档位 + 自由输入)
cb.IsEditable = true; var cb = new ComboBox { Width = 150, Tag = p, IsEditable = true, IsTextSearchEnabled = false, HorizontalAlignment = HorizontalAlignment.Left };
cb.IsTextSearchEnabled = false;
cb.KeyDown += (s2, e2) => { if (e2.Key == System.Windows.Input.Key.Enter) TrySetRangeText(cb); }; cb.KeyDown += (s2, e2) => { if (e2.Key == System.Windows.Input.Key.Enter) TrySetRangeText(cb); };
cb.LostKeyboardFocus += (s2, e2) => TrySetRangeText(cb); cb.LostKeyboardFocus += (s2, e2) => TrySetRangeText(cb);
cb.SelectionChanged += ParamChanged;
block.Children.Add(cb);
_boxes[p.code] = cb;
} }
cb.SelectionChanged += ParamChanged; else
row.Children.Add(cb); {
_paramPanel.Children.Add(row); // 枚举维度 → 选项块网格 (有说明显示说明, 无说明只显示代号)
_boxes[p.code] = cb; var wrap = new WrapPanel();
var list = new List<OptionTile>();
foreach (var v in (p.values ?? new List<object>()))
{
var key = v == null ? "" : Convert.ToString(v, CultureInfo.InvariantCulture);
var disp = DisplayText(p, v);
bool hasDesc = !string.IsNullOrEmpty(disp) && disp != key;
var tile = new OptionTile(p, v,
hasDesc ? (key.Length == 0 ? "—" : key) : disp,
hasDesc ? disp : null,
OnTileClick);
list.Add(tile);
wrap.Children.Add(tile);
}
block.Children.Add(wrap);
_tilePanels[p.code] = wrap;
_tiles[p.code] = list;
}
_paramPanel.Children.Add(block);
// 数值/range 参数 → 参数表范围筛选控件 // 数值/range 参数 → 参数表范围筛选控件
if (p.type == "number" || p.type == "range") if (p.type == "number" || p.type == "range")
@@ -598,6 +806,116 @@ namespace Ounibo.Catalog.App
RefreshOptions(); RefreshOptions();
} }
/// <summary>选项块点击: 切换选择 (再点一次取消); 与下拉时代语义一致。</summary>
void OnTileClick(OptionTile tile)
{
if (_refreshing || _cfg == null) return;
var code = tile.Param.code;
if (_sel.ContainsKey(code) && EqStr(_sel[code], tile.Value)) _sel.Remove(code);
else _sel[code] = tile.Value;
RefreshOptions();
}
// ---------- 零件号实时拼装 (DESTACO 风, 与网页版同一套语义) ----------
// 不依赖编码模板 (40 系列仅 KC 有): 用「当前候选型号的最长公共前缀」——
// 选得越多候选越少、前缀越长, 逐字拼出完整零件号。
List<string> CandidateCodes()
{
var res = new List<string>();
if (_series == null) return res;
var vs = _series.variants ?? new List<Variant>();
if (_sel.Count == 0) { foreach (var v in vs) res.Add(v.modelCode); return res; }
foreach (var v in vs)
{
bool ok = true;
foreach (var kv in _sel)
{
object val;
if (!v.@params.TryGetValue(kv.Key, out val) || !EqStr(val, kv.Value)) { ok = false; break; }
}
if (ok) res.Add(v.modelCode);
}
return res;
}
static string LongestCommonPrefix(List<string> list)
{
if (list == null || list.Count == 0) return "";
var a = list[0]; var b = list[0];
foreach (var x in list)
{
if (string.CompareOrdinal(x, a) < 0) a = x;
if (string.CompareOrdinal(x, b) > 0) b = x;
}
int i = 0;
while (i < a.Length && i < b.Length && a[i] == b[i]) i++;
return a.Substring(0, i);
}
/// <summary>刷新零件号实时条: 已确定前缀 + 待定位数 + 进度 + 提示。</summary>
void RefreshSkuBar()
{
if (_skuBar == null || _series == null) return;
var total = (_series.parameters ?? new List<ParameterDef>()).Count;
var chosen = _sel.Count;
var cand = CandidateCodes();
var pre = LongestCommonPrefix(cand);
_skuCode.Inlines.Clear();
if (pre.Length > 0)
{
_skuCode.Inlines.Add(new System.Windows.Documents.Run(pre)
{
Foreground = new SolidColorBrush(Color.FromRgb(0x0F, 0x76, 0x6E))
});
}
var rest = new string('·', Math.Max(0, total - chosen));
if (rest.Length > 0)
{
_skuCode.Inlines.Add(new System.Windows.Documents.Run(rest)
{
Foreground = new SolidColorBrush(Color.FromRgb(0xD1, 0xD5, 0xDB))
});
}
if (pre.Length == 0 && rest.Length == 0)
{
_skuCode.Inlines.Add(new System.Windows.Documents.Run(
_lang.T("选择参数以生成零件号", "Select options to build part number"))
{ Foreground = new SolidColorBrush(Color.FromRgb(0xD1, 0xD5, 0xDB)) });
}
bool done = chosen == total && cand.Count == 1;
if (done)
{
// 完成态: 整块转青色实心
_skuBar.Background = new LinearGradientBrush(
Color.FromRgb(0x14, 0xB8, 0xA6), Color.FromRgb(0x0D, 0x94, 0x88), 45);
_skuBar.BorderBrush = Brushes.Transparent;
_skuCode.Inlines.Clear();
_skuCode.Inlines.Add(new System.Windows.Documents.Run(pre) { Foreground = Brushes.White });
}
else
{
_skuBar.Background = new LinearGradientBrush(
Color.FromRgb(0xF0, 0xFD, 0xFA), Colors.White, 45);
_skuBar.BorderBrush = new SolidColorBrush(Color.FromRgb(0x99, 0xF6, 0xE4));
}
_skuProg.Value = total == 0 ? 0 : (double)chosen / total;
if (chosen < total)
_skuHint.Text = _lang.T(
"已选 " + chosen + "/" + total + " 项 · 候选 " + cand.Count + " 个型号",
chosen + "/" + total + " selected · " + cand.Count + " candidates");
else if (cand.Count == 1)
_skuHint.Text = _lang.T("零件号完整", "Part number complete");
else if (cand.Count > 1)
_skuHint.Text = _lang.T(
"该参数组合对应 " + cand.Count + " 个型号, 请在参数表中选择",
cand.Count + " models share these options — pick one in the table");
else
_skuHint.Text = _lang.T("无匹配型号", "No matching model");
}
// range 参数自由输入 (参数化引擎): 回车/失焦时校验并写入选择; 非法输入恢复原显示 (与网页版一致) // range 参数自由输入 (参数化引擎): 回车/失焦时校验并写入选择; 非法输入恢复原显示 (与网页版一致)
void TrySetRangeText(ComboBox cb) void TrySetRangeText(ComboBox cb)
{ {
@@ -632,6 +950,7 @@ namespace Ounibo.Catalog.App
try try
{ {
var allowed = _cfg.AllowedOptions(_sel); var allowed = _cfg.AllowedOptions(_sel);
// 下拉 (range 参数)
foreach (var kv in _boxes) foreach (var kv in _boxes)
{ {
var cb = kv.Value; var cb = kv.Value;
@@ -649,11 +968,52 @@ namespace Ounibo.Catalog.App
if (_sel.ContainsKey(p.code)) if (_sel.ContainsKey(p.code))
cb.SelectedItem = cb.Items.OfType<ComboItem>().FirstOrDefault(x => EqStr(x.Value, _sel[p.code])); cb.SelectedItem = cb.Items.OfType<ComboItem>().FirstOrDefault(x => EqStr(x.Value, _sel[p.code]));
} }
// 选项块 (枚举参数): 不可选的置灰保留, 已选的打勾
foreach (var kv in _tiles)
{
var code = kv.Key;
List<object> list;
if (!allowed.TryGetValue(code, out list)) list = new List<object>();
if (_sel.ContainsKey(code) && !list.Any(x => EqStr(x, _sel[code])))
_sel.Remove(code);
foreach (var tile in kv.Value)
{
tile.SetUsable(list.Any(x => EqStr(x, tile.Value)));
tile.SetOn(_sel.ContainsKey(code) && EqStr(_sel[code], tile.Value));
}
}
// 维度标题后的已选值回显
if (_series != null)
foreach (var p in _series.parameters)
{
var found = FindPickedTextBlock(p.code);
if (found != null)
found.Text = _sel.ContainsKey(p.code) ? "· " + DisplayText(p, _sel[p.code]) : "";
}
RefreshSkuBar();
TryUpdateSelection(); TryUpdateSelection();
} }
finally { _refreshing = false; } finally { _refreshing = false; }
} }
/// <summary>按 Tag (=参数 code) 找到维度标题后的回显 TextBlock。</summary>
TextBlock FindPickedTextBlock(string code)
{
foreach (var child in _paramPanel.Children)
{
var block = child as StackPanel;
if (block == null || block.Children.Count == 0) continue;
var head = block.Children[0] as StackPanel;
if (head == null) continue;
foreach (var h in head.Children)
{
var tb = h as TextBlock;
if (tb != null && tb.Tag as string == code) return tb;
}
}
return null;
}
static bool EqStr(object a, object b) static bool EqStr(object a, object b)
{ {
return string.Equals( return string.Equals(

80
tools/find-nxbin.ps1 Normal file
View File

@@ -0,0 +1,80 @@
# 探测 Siemens NX 的 NXBIN 目录 (含 run_journal.exe), 供启动脚本调用。
# 用法:
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\find-nxbin.ps1 # 输出 NXBIN 绝对路径
# powershell ... -File tools\find-nxbin.ps1 -Quiet # 只输出路径, 失败无输出
# powershell ... -File tools\find-nxbin.ps1 -Explain # 输出探测过程
#
# 探测顺序 (先到先用):
# 1. 环境变量 ONEBOT_NXBIN —— 手动指定优先, 便于自定义安装路径
# 2. 项目内 nxbin.txt (单行路径) —— 便于把结果固化到部署机器
# 3. C:\Program Files\Siemens\NX*\NXBIN —— 官方默认安装位置, 多版本取版本号最大者
#
# 退出码: 0 = 找到 (stdout 为路径); 1 = 未找到 (stdout 为空)
param(
[switch]$Quiet,
[switch]$Explain
)
$ErrorActionPreference = 'SilentlyContinue'
function Say($m) { if (-not $Quiet) { Write-Output $m } }
function Test-NxBin($dir) {
if ([string]::IsNullOrWhiteSpace($dir)) { return $false }
return Test-Path (Join-Path $dir 'run_journal.exe')
}
# 1) 环境变量
$env_dir = $env:ONEBOT_NXBIN
if ($env_dir) {
Say ("[1] 环境变量 ONEBOT_NXBIN = " + $env_dir)
if (Test-NxBin $env_dir) { Write-Output $env_dir; exit 0 }
Say " 该目录下没有 run_journal.exe, 继续探测"
} else {
Say "[1] 环境变量 ONEBOT_NXBIN 未设置"
}
# 2) 项目内 nxbin.txt
$root = Split-Path $PSScriptRoot -Parent
$cfg = Join-Path $root 'nxbin.txt'
if (Test-Path $cfg) {
$line = (Get-Content $cfg -TotalCount 1)
if ($line) { $line = $line.Trim() }
Say ("[2] nxbin.txt = " + $line)
if (Test-NxBin $line) { Write-Output $line; exit 0 }
Say " 该目录下没有 run_journal.exe, 继续探测"
} else {
Say "[2] 无 nxbin.txt"
}
# 3) 扫描官方安装位置 (多版本取版本号最大的)
$roots = @(
(Join-Path ${env:ProgramFiles} 'Siemens'),
(Join-Path ${env:ProgramFiles(x86)} 'Siemens'),
'C:\Program Files\Siemens'
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
$cands = @()
foreach ($r in $roots) {
Say ("[3] 扫描 " + $r)
foreach ($ver in (Get-ChildItem $r -Directory -Filter 'NX*' -ErrorAction SilentlyContinue)) {
$bin = Join-Path $ver.FullName 'NXBIN'
if (Test-NxBin $bin) {
$cands += $bin
Say (" 找到 " + $bin)
}
}
}
if ($cands.Count -gt 0) {
# 按 NX 后面的版本号排序取最大 (如 NX2506 < NX2512); 无版本号的排最后
$best = $cands | Sort-Object {
$m = [regex]::Match($_, 'NX(\d+)')
if ($m.Success) { [long]$m.Groups[1].Value } else { 0 }
} -Descending | Select-Object -First 1
Write-Output $best
exit 0
}
Say "未找到 NX (含 run_journal.exe 的 NXBIN 目录)"
Say "可手动指定: set ONEBOT_NXBIN=D:\path\to\NXBIN 或 写一行到 nxbin.txt"
exit 1

36
tools/fix-lineend.ps1 Normal file
View File

@@ -0,0 +1,36 @@
# 把 .bat/.cmd 的行尾统一为 CRLF —— cmd.exe 必须用 CRLF, 裸 LF 会让它误解析。
# 症状: `if not defined X (` 被拆成 `'ned' 不是内部或外部命令` 之类的乱码报错。
# 触发场景: 用工具/编辑器写 .bat 时默认写成 LF (本项目踩过)。
#
# 幂等: 已是 CRLF 的文件不会被改动。
# 用法:
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\fix-lineend.ps1 # 处理项目根 + tools 下全部 .bat/.cmd
# powershell ... -File tools\fix-lineend.ps1 -Files "a.bat","b.cmd" # 只处理指定文件
param([string[]]$Files)
if (-not $Files) {
$root = Split-Path $PSScriptRoot -Parent
$Files = @()
$Files += Get-ChildItem (Join-Path $root '*.bat') -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
$Files += Get-ChildItem (Join-Path $root '*.cmd') -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
$Files += Get-ChildItem (Join-Path $PSScriptRoot '*.bat') -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
$Files += Get-ChildItem (Join-Path $PSScriptRoot '*.cmd') -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
}
foreach ($f in $Files) {
if (-not (Test-Path $f)) { Write-Output ("跳过(不存在): " + $f); continue }
$bytes = [System.IO.File]::ReadAllBytes($f)
# 统一成 CRLF: 先把 CRLF 归一为 LF, 再把 LF 展开为 CRLF
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
$text = $text -replace "`r`n", "`n"
$text = $text -replace "`r", "`n"
$new = $text -replace "`n", "`r`n"
if ($new -eq [System.Text.Encoding]::UTF8.GetString($bytes)) {
Write-Output ("已是 CRLF: " + (Split-Path $f -Leaf))
} else {
# 不加 BOM (cmd 对 BOM 敏感, 且 .bat 无需 BOM)
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($f, $new, $enc)
Write-Output ("已修正为 CRLF: " + (Split-Path $f -Leaf))
}
}

81
tools/serve-web-bg.ps1 Normal file
View File

@@ -0,0 +1,81 @@
# 后台启动/停止网页预览服务器 (无 PowerShell 窗口)
# 用法:
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\serve-web-bg.ps1 # 后台启动
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\serve-web-bg.ps1 -Stop # 停止
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\serve-web-bg.ps1 -Status # 查看状态
# ... -Root release\web_2026.09 -Port 8080 # 指定目录/端口
#
# 原理: 用 Start-Process -WindowStyle Hidden 启动 serve-web.ps1, 并把 PID + 参数写入
# .serve-web.pid 文件, 便于 -Stop / -Status 定位。
param(
[string]$Root = '',
[int]$Port = 8080,
[switch]$Stop,
[switch]$Status
)
$projRoot = Split-Path $PSScriptRoot -Parent
$pidFile = Join-Path $projRoot '.serve-web.pid'
# 相对路径按【项目根】转绝对 —— Start-Process 启动的子进程工作目录不确定,
# 传相对路径会被解析到别处; 也不能依赖 Get-Location (调用方可能在任意目录)。
# 注意 1: PowerShell 变量名【大小写不敏感】, 不能同时用 $root 和 $Root (会互相覆盖),
# 故项目根变量取名 $projRoot。
# 注意 2: Join-Path 只做拼接不做 .. 归约, 必须再用 GetFullPath 规范化。
if ([string]::IsNullOrWhiteSpace($Root)) { $Root = 'release\web_2026.09' }
if (-not [System.IO.Path]::IsPathRooted($Root)) { $Root = Join-Path $projRoot $Root }
$Root = [System.IO.Path]::GetFullPath($Root)
if (-not (Test-Path $Root)) { Write-Output ("目录不存在: " + $Root); exit 1 }
$script = Join-Path $PSScriptRoot 'serve-web.ps1'
function Get-Running {
# 用命令行匹配定位服务进程 (比只靠 PID 文件更可靠: 窗口关过/崩溃后 PID 会失效)
Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.CommandLine -like '*serve-web.ps1*' }
}
if ($Stop) {
$procs = Get-Running
if (-not $procs) { Write-Output '服务未在运行'; if (Test-Path $pidFile) { Remove-Item $pidFile -Force }; exit 0 }
foreach ($p in $procs) {
Write-Output ("停止 serve-web (PID " + $p.ProcessId + ")")
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
}
if (Test-Path $pidFile) { Remove-Item $pidFile -Force }
exit 0
}
if ($Status) {
$procs = Get-Running
if ($procs) { Write-Output ("运行中: " + ($procs.ProcessId -join ', ')) }
else { Write-Output '未运行' }
try {
$code = (Invoke-WebRequest -Uri ("http://localhost:" + $Port + "/index.html") -UseBasicParsing -TimeoutSec 3).StatusCode
Write-Output ("HTTP " + $code + " http://localhost:" + $Port)
} catch { Write-Output ("HTTP 不可达 http://localhost:" + $Port) }
exit 0
}
# 启动前先清理已在跑的, 避免端口占用/多份实例
$old = Get-Running
if ($old) {
Write-Output ("已有实例, 先停止: " + ($old.ProcessId -join ', '))
foreach ($p in $old) { Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue }
Start-Sleep -Seconds 2
}
# -WindowStyle Hidden: 不弹 PowerShell 窗口; 进程随本脚本退出继续运行
# 注: 变量名不用 $args (PowerShell 自动变量, 赋值有副作用)
$srvArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $script, '-Root', $Root, '-Port', $Port)
$p = Start-Process -FilePath 'powershell' -ArgumentList $srvArgs -WindowStyle Hidden -PassThru
Set-Content -Path $pidFile -Value $p.Id -Encoding ascii
Start-Sleep -Seconds 4
try {
$code = (Invoke-WebRequest -Uri ("http://localhost:" + $Port + "/index.html") -UseBasicParsing -TimeoutSec 5).StatusCode
Write-Output ("已后台启动 (PID " + $p.Id + ") HTTP " + $code + " http://localhost:" + $Port)
Write-Output ("目录: " + $Root)
Write-Output "停止: tools\serve-web-bg.ps1 -Stop"
} catch {
Write-Output ("启动后探测失败, 请检查目录/端口: " + $Root)
exit 1
}

View File

@@ -1,9 +1,10 @@
# UI 层诊断 (STA): 反射创建主窗口, 对每个系列执行 LoadSeries, # UI 层诊断 (STA): 反射创建主窗口, 对每个系列执行 LoadSeries,
# 检查下拉框数据源是否为空 / 是否抛异常。 # 校验选项块/下拉的选项数、可选态、以及选中后零件号实时条的输出。
# 用法: powershell -STA -NoProfile -ExecutionPolicy Bypass -File tools\ui-diagnose.ps1 # 用法: powershell -STA -NoProfile -ExecutionPolicy Bypass -File tools\ui-diagnose.ps1
param( param(
[string]$Exe = (Join-Path $PSScriptRoot '..\bin\OnebotCatalog.exe'), [string]$Exe = (Join-Path $PSScriptRoot '..\bin\OnebotCatalog.exe'),
[string]$Opc = (Join-Path $PSScriptRoot '..\sample\OnebotCatalog_2026.08.opc') [string]$Opc = (Join-Path $PSScriptRoot '..\sample\OnebotCatalog_2026.08.opc'),
[switch]$Verbose
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationFramework
@@ -16,23 +17,84 @@ $pkg = $pkgType.GetMethod('Open').Invoke($null, @($Opc))
$mwType = $asm.GetType('Ounibo.Catalog.App.MainWindow') $mwType = $asm.GetType('Ounibo.Catalog.App.MainWindow')
$mw = [Activator]::CreateInstance($mwType, @($pkg, $false)) $mw = [Activator]::CreateInstance($mwType, @($pkg, $false))
$ls = $mwType.GetMethod('LoadSeries', [Reflection.BindingFlags]'NonPublic,Instance') $np = [Reflection.BindingFlags]'NonPublic,Instance'
$boxesField = $mwType.GetField('_boxes', [Reflection.BindingFlags]'NonPublic,Instance') $ls = $mwType.GetMethod('LoadSeries', $np)
$boxesF = $mwType.GetField('_boxes', $np) # range 参数的可编辑下拉
$tilesF = $mwType.GetField('_tiles', $np) # 枚举参数的选项块
$selF = $mwType.GetField('_sel', $np)
$refreshM = $mwType.GetMethod('RefreshOptions', $np)
$skuCodeF = $mwType.GetField('_skuCode', $np)
$skuHintF = $mwType.GetField('_skuHint', $np)
function Get-InlinesText($tb) {
if ($tb -eq $null) { return '' }
$s = ''
foreach ($r in $tb.Inlines) { $s += $r.Text }
return $s
}
$fail = 0
foreach ($s in $pkg.Catalog.series) { foreach ($s in $pkg.Catalog.series) {
try { try {
$ls.Invoke($mw, @($s)) | Out-Null $ls.Invoke($mw, @($s)) | Out-Null
$boxes = $boxesField.GetValue($mw) $tiles = $tilesF.GetValue($mw)
$boxes = $boxesF.GetValue($mw)
# 每个枚举维度: 选项块总数 / 可选数 (与参数定义值域比对)
$parts = @() $parts = @()
foreach ($k in $boxes.Keys) { $bad = @()
$cb = $boxes[$k] foreach ($p in $s.parameters) {
$count = 0 if ($tiles.ContainsKey($p.code)) {
if ($cb.ItemsSource -ne $null) { $count = $cb.Items.Count } $list = $tiles[$p.code]
$parts += ($k + '=' + $count) $usable = 0
foreach ($t in $list) {
$f = $t.GetType().GetField('_usable', $np)
if ($f -ne $null -and $f.GetValue($t)) { $usable++ }
}
$parts += ($p.code + '=' + $list.Count)
# 初始状态: 每个维度至少有一个可选值 (否则用户无从下手)
if ($usable -eq 0) { $bad += ($p.code + ':无可选值') }
if ($list.Count -eq 0) { $bad += ($p.code + ':选项为空') }
} elseif ($boxes.ContainsKey($p.code)) {
$cb = $boxes[$p.code]
$c = 0
if ($cb.ItemsSource -ne $null) { $c = $cb.Items.Count }
$parts += ($p.code + '=range:' + $c)
if ($c -eq 0) { $bad += ($p.code + ':range无档位') }
}
}
# 逐个参数按第一个可用值选满, 检查零件号条是否产出非空前缀
$sel = $selF.GetValue($mw)
$sel.Clear()
$v0 = $s.variants[0]
$sku = ''
if ($v0 -ne $null) {
foreach ($p in $s.parameters) {
$val = $null
if ($v0.params.ContainsKey($p.code)) { $val = $v0.params[$p.code] }
if ($val -ne $null) { $sel[$p.code] = $val }
}
$refreshM.Invoke($mw, @()) | Out-Null
$sku = Get-InlinesText ($skuCodeF.GetValue($mw))
$hint = $skuHintF.GetValue($mw).Text
if ([string]::IsNullOrWhiteSpace($sku)) { $bad += '零件号条为空' }
}
$flag = if ($bad.Count -gt 0) { ' !! ' + ($bad -join '; ') } else { '' }
if ($bad.Count -gt 0) { $fail++ }
Write-Output ("系列 " + $s.code.PadRight(6) + " 选项块 " + ($parts -join ' ') + $flag)
if ($Verbose -and $sku -ne '') {
Write-Output (" 零件号: [" + $sku + "] " + $hint)
} }
Write-Output ("系列 " + $s.code + " 下拉数据源: " + ($parts -join ' '))
} catch { } catch {
Write-Output ("系列 " + $s.code + " 异常: " + $_.Exception.InnerException.Message) $fail++
$m = $_.Exception.Message
if ($_.Exception.InnerException -ne $null) { $m += ' / ' + $_.Exception.InnerException.Message }
Write-Output ("系列 " + $s.code + " 异常: " + $m)
} }
} }
Write-Output ""
Write-Output ("检查完成: " + $pkg.Catalog.series.Count + " 个系列, 问题 " + $fail + " 个")
$pkg.Dispose() $pkg.Dispose()
if ($fail -gt 0) { exit 1 }

View File

@@ -246,6 +246,7 @@
setStatus(t('目录加载完成,点击缩略图或左侧系列开始选型', setStatus(t('目录加载完成,点击缩略图或左侧系列开始选型',
'Catalog loaded - click a thumbnail or pick a series')); 'Catalog loaded - click a thumbnail or pick a series'));
if (location.search.indexOf('selftest=1') >= 0) runSelfTest(); if (location.search.indexOf('selftest=1') >= 0) runSelfTest();
applyDeepLink(); // URL 参数深度链接 (?series= / ?model= / ?embed=1)
}) })
.catch(function (e) { .catch(function (e) {
setStatus(t('加载 catalog.json 失败: ', 'Failed to load catalog.json: ') + e.message + setStatus(t('加载 catalog.json 失败: ', 'Failed to load catalog.json: ') + e.message +
@@ -253,6 +254,67 @@
}); });
} }
// ---------- 深度链接 (供 Odoo 产品页 iframe 调用) ----------
// ?series=UCBM 直接打开某系列(型号编码前缀或系列 code)
// ?model=UCBM32AK010 直接定位到某个具体型号(复用 jumpToModel)
// ?embed=1 嵌入模式:隐藏自身顶栏/底栏,让 iframe 里更干净
// ?nav=0 隐藏左侧系列树(产品页已定系列,不需要再让用户挑)
// 说明: 本函数只在 catalog.json 加载完成后调用一次(见 load())
function applyDeepLink() {
var q = new URLSearchParams(location.search || '');
// 嵌入模式:隐藏工具自身的 header/footer,避免在 Odoo 页面里出现"页中页"
if (q.get('embed') === '1') {
document.body.classList.add('embed-mode');
var st = $('status');
if (st) st.classList.add('hidden');
var hd = document.querySelector('header');
if (hd) hd.classList.add('hidden');
}
// 隐藏左侧系列树(产品页场景)
if (q.get('nav') === '0') {
document.body.classList.add('nav-off');
}
// 语言: ?lang=en
var lg = q.get('lang');
if (lg === 'en' || lg === 'zh') {
state.lang = lg;
renderTree();
renderHome();
renderCrossRef();
}
// 定位型号(优先级最高)
var model = q.get('model');
if (model) { jumpToModel(model); return; }
// 打开系列:支持传系列 code(KC)或型号前缀(UCBM32)
var want = q.get('series');
if (!want) return;
var up = String(want).toUpperCase();
var hit = null;
(state.catalog.series || []).forEach(function (s) {
if (String(s.code).toUpperCase() === up) hit = s;
});
if (!hit) {
// 退一步:按型号编码前缀匹配(如 UCBM32 → 系列 UCBM)
(state.catalog.series || []).forEach(function (s) {
if (hit) return;
(s.variants || []).some(function (v) {
if (String(v.modelCode).toUpperCase().indexOf(up) === 0) { hit = s; return true; }
return false;
});
});
}
if (hit) {
selectSeries(hit);
} else {
setStatus(t('未找到系列: ', 'Series not found: ') + want);
}
}
// ---------- 目录树 ---------- // ---------- 目录树 ----------
// 大类图标 (内联 SVG, 24x24 线性, stroke=currentColor → 随主题色/选中态自动变色) // 大类图标 (内联 SVG, 24x24 线性, stroke=currentColor → 随主题色/选中态自动变色)
var CAT_ICONS = { var CAT_ICONS = {
@@ -514,12 +576,17 @@
s.parameters.forEach(function (p) { s.parameters.forEach(function (p) {
var row = document.createElement('div'); var row = document.createElement('div');
row.className = 'param-row'; row.className = 'param-row';
row.id = 'row-' + p.code;
var label = document.createElement('label'); var label = document.createElement('label');
label.textContent = paramName(p);
label.id = 'lbl-' + p.code; label.id = 'lbl-' + p.code;
label.textContent = paramName(p);
var picked = document.createElement('span');
picked.className = 'picked';
picked.id = 'picked-' + p.code;
label.appendChild(picked);
row.appendChild(label); row.appendChild(label);
if (p.type === 'range') { if (p.type === 'range') {
// 参数化 range 域: 输入框 + 标准档位快捷选择 (与离线版可编辑下拉一致) // 连续域 (range) 无法枚举成卡片 → 保留输入框 + 标准档位
var inp = document.createElement('input'); var inp = document.createElement('input');
inp.id = 'sel-' + p.code; inp.id = 'sel-' + p.code;
inp.className = 'param-input'; inp.className = 'param-input';
@@ -545,20 +612,16 @@
panel.appendChild(row); panel.appendChild(row);
return; return;
} }
var sel = document.createElement('select'); // 枚举维度 → 选项块 (有图卡片 / 无图文字块)
sel.id = 'sel-' + p.code; var grid = document.createElement('div');
sel.onchange = function () { grid.className = 'opt-grid';
var raw = sel.value; grid.id = 'opts-' + p.code;
if (raw === '') { delete state.sel[p.code]; } // 选项较多时用更紧凑的列宽
else { if ((p.values || []).length > 8) grid.classList.add('wide');
var v = raw === '__EMPTY__' ? '' : (p.type === 'number' ? num(raw) : raw); row.appendChild(grid);
state.sel[p.code] = v;
}
refreshOptions();
};
row.appendChild(sel);
panel.appendChild(row); panel.appendChild(row);
}); });
renderSkuBar();
$('modelTitle').textContent = t(s.nameZh, s.nameEn); $('modelTitle').textContent = t(s.nameZh, s.nameEn);
$('btnDatasheet').classList.add('hidden'); $('btnDatasheet').classList.add('hidden');
// 重置尺寸图区: 选型完成前只显示提示文字, 不显示占位图 // 重置尺寸图区: 选型完成前只显示提示文字, 不显示占位图
@@ -587,11 +650,148 @@
im.src = a.path; im.src = a.path;
im.alt = a.path; im.alt = a.path;
im.draggable = false; // 阻止原生图片拖拽 (见下方全局 dragstart 拦截) im.draggable = false; // 阻止原生图片拖拽 (见下方全局 dragstart 拦截)
im.title = t('点击放大', 'Click to enlarge');
// 点击放大: 同一手册页的多张图可左右切换 (← → 键)
im.onclick = function () { openImageViewer(pages, idx); };
mw.appendChild(cap); mw.appendChild(cap);
mw.appendChild(im); mw.appendChild(im);
}); });
} }
// ---------- 手册页图片放大 (单击弹窗, 支持多页左右切换) ----------
var zview = { el: null, pages: [], idx: 0 };
function openImageViewer(pages, idx) {
closeImageViewer(); // 先清掉可能存在的旧弹窗
zview.pages = pages || [];
zview.idx = idx || 0;
var ov = document.createElement('div');
ov.className = 'img-zoom-overlay';
var im = document.createElement('img');
im.src = zview.pages[zview.idx].path;
im.alt = zview.pages[zview.idx].path;
im.draggable = false;
var btn = document.createElement('button');
btn.className = 'img-zoom-close';
btn.type = 'button';
btn.innerHTML = '&times;';
btn.setAttribute('aria-label', t('关闭', 'Close'));
btn.onclick = function (ev) { ev.stopPropagation(); closeImageViewer(); };
var cap = document.createElement('div');
cap.className = 'img-zoom-cap';
cap.textContent = t('手册页', 'Manual page') + ' ' + (zview.idx + 1) + '/' + zview.pages.length
+ (zview.pages.length > 1 ? t(' (← → 切换,Esc 关闭)', ' (← → to switch, Esc to close)') : '');
ov.appendChild(im);
ov.appendChild(btn);
ov.appendChild(cap);
// 点遮罩空白处关闭(点图片本身不关)
ov.onclick = function (ev) { if (ev.target === ov) closeImageViewer(); };
document.body.appendChild(ov);
zview.el = ov;
document.addEventListener('keydown', onZoomKey);
}
function onZoomKey(ev) {
if (!zview.el) return;
if (ev.key === 'Escape') { closeImageViewer(); }
else if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
if (zview.pages.length < 2) return;
var d = ev.key === 'ArrowRight' ? 1 : -1;
zview.idx = (zview.idx + d + zview.pages.length) % zview.pages.length;
var im = zview.el.querySelector('img');
im.src = zview.pages[zview.idx].path;
var cap = zview.el.querySelector('.img-zoom-cap');
if (cap) cap.textContent = t('手册页', 'Manual page') + ' ' + (zview.idx + 1) + '/' + zview.pages.length
+ t(' (← → 切换,Esc 关闭)', ' (← → to switch, Esc to close)');
}
}
function closeImageViewer() {
if (zview.el && zview.el.parentNode) zview.el.parentNode.removeChild(zview.el);
zview.el = null;
document.removeEventListener('keydown', onZoomKey);
}
// ---------- 数据表 PDF 弹窗预览 ----------
// 用浏览器内置 PDF 阅读器(iframe),弹窗内保留下载入口。
// 移动端部分浏览器(尤其 iOS Safari)不内联显示 PDF,所以在弹窗里同时给下载按钮兜底。
var pview = { el: null };
function openPdfViewer(url, fname) {
closePdfViewer();
var ov = document.createElement('div');
ov.className = 'pdf-zoom-overlay';
var box = document.createElement('div');
box.className = 'pdf-zoom-box';
var head = document.createElement('div');
head.className = 'pdf-zoom-head';
var title = document.createElement('span');
title.className = 'pdf-zoom-title';
title.textContent = fname;
var dl = document.createElement('a');
dl.className = 'pdf-zoom-dl';
dl.href = url;
dl.setAttribute('download', fname);
// 内联 SVG 下载图标(工具无图标字体)
dl.innerHTML =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" '
+ 'stroke-linecap="round" stroke-linejoin="round" style="width:13px;height:13px;vertical-align:-2px">'
+ '<path d="M12 3v12"/><path d="m7 11 5 5 5-5"/><path d="M4 20h16"/></svg> '
+ t('下载', 'Download');
var btn = document.createElement('button');
btn.className = 'img-zoom-close pdf-zoom-close';
btn.type = 'button';
btn.innerHTML = '&times;';
btn.setAttribute('aria-label', t('关闭', 'Close'));
btn.onclick = function (ev) { ev.stopPropagation(); closePdfViewer(); };
head.appendChild(title);
head.appendChild(dl);
head.appendChild(btn);
var fr = document.createElement('iframe');
fr.className = 'pdf-zoom-frame';
fr.src = url + '#toolbar=1&view=FitH';
fr.setAttribute('title', fname);
box.appendChild(head);
box.appendChild(fr);
ov.appendChild(box);
// 点遮罩空白处关闭(点弹窗内部不关)
ov.onclick = function (ev) { if (ev.target === ov) closePdfViewer(); };
document.body.appendChild(ov);
pview.el = ov;
document.addEventListener('keydown', onPdfKey);
}
function onPdfKey(ev) {
if (pview.el && ev.key === 'Escape') closePdfViewer();
}
function closePdfViewer() {
if (pview.el && pview.el.parentNode) {
// 先清空 iframe.src 再移除,避免某些浏览器里 PDF 后台继续加载
var fr = pview.el.querySelector('iframe');
if (fr) fr.src = 'about:blank';
pview.el.parentNode.removeChild(pview.el);
}
pview.el = null;
document.removeEventListener('keydown', onPdfKey);
}
// 空字符串是合法枚举值 (如 磁石=""=无磁石), 用哨兵值避免与"未选择"混淆 // 空字符串是合法枚举值 (如 磁石=""=无磁石), 用哨兵值避免与"未选择"混淆
function optValue(v) { return str(v) === '' ? '__EMPTY__' : str(v); } function optValue(v) { return str(v) === '' ? '__EMPTY__' : str(v); }
@@ -600,11 +800,12 @@
if (!s) return; if (!s) return;
var allowed = allowedOptions(s, state.sel); var allowed = allowedOptions(s, state.sel);
s.parameters.forEach(function (p) { s.parameters.forEach(function (p) {
var sel = $('sel-' + p.code);
var vals = allowed[p.code] || []; var vals = allowed[p.code] || [];
if (p.type === 'range') { if (p.type === 'range') {
// 输入框: 更新 datalist 标准档位 (标注 标准); 已选值回显 // 输入框: 更新 datalist 标准档位 (标注 标准); 已选值回显
var inp = $('sel-' + p.code);
var dl = $('dl-' + p.code); var dl = $('dl-' + p.code);
if (!inp || !dl) return;
dl.innerHTML = ''; dl.innerHTML = '';
vals.forEach(function (v) { vals.forEach(function (v) {
var opt = document.createElement('option'); var opt = document.createElement('option');
@@ -613,23 +814,59 @@
opt.label = isGrid ? str(v) + ' (' + t('标准', 'Std') + ')' : str(v); opt.label = isGrid ? str(v) + ' (' + t('标准', 'Std') + ')' : str(v);
dl.appendChild(opt); dl.appendChild(opt);
}); });
sel.value = state.sel[p.code] !== undefined ? str(state.sel[p.code]) : ''; inp.value = state.sel[p.code] !== undefined ? str(state.sel[p.code]) : '';
return; return;
} }
var grid = $('opts-' + p.code);
if (!grid) return;
// 已选值变得不可选时清除 // 已选值变得不可选时清除
if (state.sel[p.code] !== undefined && if (state.sel[p.code] !== undefined &&
!vals.some(function (v) { return str(v) === str(state.sel[p.code]); })) { !vals.some(function (v) { return str(v) === str(state.sel[p.code]); })) {
delete state.sel[p.code]; delete state.sel[p.code];
} }
sel.innerHTML = '<option value="">' + t('请选择', 'Select') + '</option>'; // 候选值: 定义域全集 (保证卡片位置稳定, 不可选的置灰而非消失)
vals.forEach(function (v) { var all = (p.values || []);
var opt = document.createElement('option'); grid.innerHTML = '';
opt.value = optValue(v); all.forEach(function (v) {
opt.textContent = displayValue(p, v); var usable = vals.some(function (x) { return str(x) === str(v); });
if (state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code])) opt.selected = true; var on = state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code]);
sel.appendChild(opt); var tile = document.createElement('div');
tile.className = 'opt' + (on ? ' on' : '') + (usable ? '' : ' off');
var disp = displayValue(p, v);
// 有中文说明时: 代号大字 + 说明小字; 无说明 (如纯数字) 只显示值
var hasDesc = disp && disp !== str(v);
var k = document.createElement('div');
k.className = 'opt-k';
k.textContent = hasDesc ? optValue(v) .replace('__EMPTY__', '—') : disp;
tile.appendChild(k);
if (hasDesc) {
var dd = document.createElement('div');
dd.className = 'opt-d';
dd.textContent = disp;
tile.appendChild(dd);
} else {
tile.classList.add('txt');
}
if (usable) {
tile.onclick = function () {
if (state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code])) {
delete state.sel[p.code]; // 再点一次取消
} else {
state.sel[p.code] = p.type === 'number' ? num(v) : (str(v) === '__EMPTY__' ? '' : v);
}
refreshOptions();
};
}
grid.appendChild(tile);
}); });
}); });
// 维度标签后面回显当前选择
s.parameters.forEach(function (p) {
var el = $('picked-' + p.code);
if (!el) return;
el.textContent = state.sel[p.code] !== undefined ? '· ' + displayValue(p, state.sel[p.code]) : '';
});
renderSkuBar();
updateSelection(); updateSelection();
if (crossFill) crossFill(); // 平替对照随选型刷新 (只显示当前型号的平替) if (crossFill) crossFill(); // 平替对照随选型刷新 (只显示当前型号的平替)
} }
@@ -734,8 +971,15 @@
if (a.kind === 'dimDrawing' && (a.model === v.modelCode || att === null)) att = a; if (a.kind === 'dimDrawing' && (a.model === v.modelCode || att === null)) att = a;
}); });
if (att) { if (att) {
$('dimImage').src = att.path; var dimEl = $('dimImage');
$('dimImage').classList.remove('hidden'); dimEl.src = att.path;
dimEl.classList.remove('hidden');
dimEl.title = t('点击放大', 'Click to enlarge');
dimEl.style.cursor = 'zoom-in';
// 技术文档(尺寸图)同样支持点击弹窗放大
dimEl.onclick = function () {
openImageViewer([{ path: att.path }], 0);
};
$('dimHint').classList.add('hidden'); $('dimHint').classList.add('hidden');
} }
// 数据表 // 数据表
@@ -743,9 +987,30 @@
return a.kind === 'datasheet' && a.lang === state.lang; return a.kind === 'datasheet' && a.lang === state.lang;
})[0] || (s.attachments || []).filter(function (a) { return a.kind === 'datasheet'; })[0]; })[0] || (s.attachments || []).filter(function (a) { return a.kind === 'datasheet'; })[0];
if (dsAtt) { if (dsAtt) {
// 下载行为 (2026-08-24): 点按钮直接下载 PDF, 与 STEP 下载一致 // 数据表 PDF: 点击弹窗预览(用浏览器内置 PDF 阅读器),弹窗内保留下载入口。
ds.href = dsAtt.path; // 原来是直接下载(2026-08-24),用户反馈希望先预览(2026-09-16)。
ds.setAttribute('download', dsAtt.path.split('/').pop() || 'datasheet.pdf'); // ⚠️ 不能沿用 <a download>:那会直接下载。改成 button + 弹窗。
ds.removeAttribute('href');
ds.removeAttribute('download');
ds.style.cursor = 'pointer';
ds.title = t('点击预览', 'Click to preview');
// 带预览图标,区别于纯下载按钮(用户要求能一眼看出可预览)。
// ⚠️ 工具没有图标字体(只带 JSZip),所以用**内联 SVG**(与分类图标同做法)。
ds.innerHTML =
'<svg class="ds-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
+ 'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">'
+ '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
+ '<path d="M14 2v6h6"/><path d="M8 13h2.5a1.5 1.5 0 0 1 0 3H8v-6"/>'
+ '</svg>'
+ '<span class="ds-txt">' + t('数据表 PDF', 'Datasheet PDF') + '</span>'
+ '<svg class="ds-ico ds-ico-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
+ 'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">'
+ '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M11 8v6M8 11h6"/>'
+ '</svg>';
ds.onclick = function (ev) {
ev.preventDefault();
openPdfViewer(dsAtt.path, dsAtt.path.split('/').pop() || 'datasheet.pdf');
};
ds.classList.remove('hidden'); ds.classList.remove('hidden');
} }
@@ -753,6 +1018,70 @@
load3D(v); load3D(v);
} }
// ---------- 零件号实时拼装 (DESTACO 风) ----------
// 不依赖编码模板 (40 个系列里只有 KC 有模板): 用「当前候选型号的最长公共前缀」——
// 选得越多, 候选集越小, 公共前缀越长, 逐字拼出完整零件号。
// 已用 check-sku-lcp.py 验证 40/40 系列前缀单调增长 (不回退)。
function candidateCodes() {
var s = state.series;
if (!s) return [];
var keys = Object.keys(state.sel);
if (!keys.length) return (s.variants || []).map(function (v) { return v.modelCode; });
return (s.variants || []).filter(function (v) {
return keys.every(function (k) { return str(v.params[k]) === str(state.sel[k]); });
}).map(function (v) { return v.modelCode; });
}
function lcp(list) {
if (!list.length) return '';
var a = list.reduce(function (m, x) { return x < m ? x : m; });
var b = list.reduce(function (m, x) { return x > m ? x : m; });
var i = 0;
while (i < a.length && i < b.length && a[i] === b[i]) i++;
return a.slice(0, i);
}
function renderSkuBar() {
var bar = $('skuBar');
if (!bar) return;
var s = state.series;
if (!s) return;
var chosen = Object.keys(state.sel).length;
var total = s.parameters.length;
var cand = candidateCodes();
var pre = lcp(cand);
// 未选的维度用占位符预演长度 (让用户看到"还差几位")
var rest = '';
s.parameters.forEach(function (p) {
if (state.sel[p.code] !== undefined) return;
rest += '·';
});
bar.classList.toggle('done', chosen === total && cand.length === 1);
$('skuCode').innerHTML = pre
? '<span class="sku-fixed">' + esc(pre) + '</span>' +
'<span class="sku-tbd">' + esc(rest) + '</span>'
: '<span class="sku-tbd">' + esc(t('选择参数以生成零件号', 'Select options to build part number')) + '</span>';
var bar2 = $('skuProg');
if (bar2) bar2.style.width = (total ? chosen / total * 100 : 0) + '%';
var hint = $('skuHint');
if (hint) {
if (chosen < total) {
hint.textContent = t('已选 ' + chosen + '/' + total + ' 项 · 候选 ' +
cand.length + ' 个型号', chosen + '/' + total + ' selected · ' + cand.length + ' candidates');
} else if (cand.length === 1) {
hint.textContent = t('零件号完整', 'Part number complete');
} else if (cand.length > 1) {
hint.textContent = t('该参数组合对应 ' + cand.length + ' 个型号, 请在下方选择',
cand.length + ' models share these options — pick one below');
} else {
hint.textContent = t('无匹配型号', 'No matching model');
}
}
}
// ---------- 参数表 (含范围筛选) ---------- // ---------- 参数表 (含范围筛选) ----------
function renderRangeRow() { function renderRangeRow() {
var row = $('rangeRow'); var row = $('rangeRow');
@@ -1406,7 +1735,14 @@
$('cartDownload').textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)'); $('cartDownload').textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)');
renderCartBadge(); renderCartBadge();
$('btnDownload').textContent = t('下载 STEP', 'Download STEP'); $('btnDownload').textContent = t('下载 STEP', 'Download STEP');
$('btnDatasheet').textContent = t('数据表 PDF', 'Datasheet PDF'); // ⚠️ 数据表按钮带图标(pdf + 放大镜),不能用 textContent 整段替换,
// 否则切语言时图标会被抹掉。只改文字节点。
var dsBtn = $('btnDatasheet');
if (dsBtn) {
var dsTxt = dsBtn.querySelector('.ds-txt');
if (dsTxt) dsTxt.textContent = t('数据表 PDF', 'Datasheet PDF');
else dsBtn.textContent = t('数据表 PDF', 'Datasheet PDF');
}
$('dimHint').textContent = t('选型完成后显示尺寸图', 'Dimension drawing appears after selection'); $('dimHint').textContent = t('选型完成后显示尺寸图', 'Dimension drawing appears after selection');
var tabs = document.querySelectorAll('.tab'); var tabs = document.querySelectorAll('.tab');
tabs[0].textContent = t('3D / 尺寸图', '3D / Dimension'); tabs[0].textContent = t('3D / 尺寸图', '3D / Dimension');
@@ -1474,10 +1810,14 @@
} }
// ---- 3D 查看器联动 (新迪 STEPViewer: occt-import-js + three.js, 支持真实 STEP) ---- // ---- 3D 查看器联动 (新迪 STEPViewer: occt-import-js + three.js, 支持真实 STEP) ----
// 本地联调: /viewer/ 前缀由 serve-web.ps1 -ViewerRoot 映射到新迪 dev 目录 // 部署自适应: 原来硬编码 '/viewer/STEPViewer/index.html' + 站点根绝对路径 '/step/…',
// 生产同域部署 (3d.ruiguo.wang): 改为 '/STEPViewer/index.html' // 一旦部署到子路径(如 Odoo 的 /catalog/)就会 404。
// (模型文件已同步到 STEPViewer/step/ 目录, file 参数 = 目录内相对路径) // 改为从当前页面 URL 推导根路径,本地联调 / 生产子路径 / 根路径都能用。
var VIEWER_BASE = '/viewer/STEPViewer/index.html'; // 例: 页面 /catalog/index.html → BASE_DIR = '/catalog'
var BASE_DIR = location.pathname.replace(/\/[^/]*$/, ''); // 去掉文件名, 得 '/catalog' 或 ''
// 查看器位置: 优先同目录下的 viewer/STEPViewer(生产整站一起部署时就是这个)
// 回退到 /viewer/STEPViewer(本地 serve-web.ps1 -ViewerRoot 联调用)
var VIEWER_BASE = BASE_DIR + '/viewer/STEPViewer/index.html';
function load3D(variant) { function load3D(variant) {
var frame = $('viewerFrame'); var frame = $('viewerFrame');
@@ -1486,8 +1826,8 @@
hint.classList.add('hidden'); hint.classList.add('hidden');
frame.classList.remove('hidden'); frame.classList.remove('hidden');
canvas.classList.add('hidden'); canvas.classList.add('hidden');
// 站点根绝对路径 (/step/...): 模型始终从站点 step\ 目录取, 查看器无需自带模型副本 (踩坑 #20) // 模型路径同样要带 BASE_DIR 前缀(原为站点根 '/' + step)
frame.src = VIEWER_BASE + '?file=' + encodeURIComponent('/' + variant.step) + '&embed=1&bg=grey&menu=min'; frame.src = VIEWER_BASE + '?file=' + encodeURIComponent(BASE_DIR + '/' + variant.step) + '&embed=1&bg=grey&menu=min';
if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer); if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer);
// 查看器不可用 (404/离线) → 回退内置 Canvas 渲染 // 查看器不可用 (404/离线) → 回退内置 Canvas 渲染
v3d.fallbackTimer = setTimeout(function () { v3d.fallbackTimer = setTimeout(function () {

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OneBot数模下载</title> <title>OneBot数模下载</title>
<link rel="stylesheet" href="style.css?v=11"> <link rel="stylesheet" href="style.css?v=12">
</head> </head>
<body> <body>
<header> <header>
@@ -35,26 +35,34 @@
<section id="params" class="params hidden"> <section id="params" class="params hidden">
<div id="modelTitle" class="model-title">请选择产品系列</div> <div id="modelTitle" class="model-title">请选择产品系列</div>
<div id="paramPanel"></div> <!-- 零件号实时条: 边选边拼, 已确定片段高亮 / 待定片段虚化 -->
<div id="actions"> <div id="skuBar" class="sku-bar">
<a id="btnDownload" class="btn primary hidden" download>下载 STEP</a> <div class="sku-label" id="skuLabel">零件号 Part Number</div>
<select id="btnGenFmt" class="hidden" title="下载格式 (仅非标档位在线生成)"> <div class="sku-code" id="skuCode"></div>
<option value="zip">全格式 zip</option> <div class="sku-prog"><i id="skuProg"></i></div>
<option value="step">STEP</option> <div class="sku-hint" id="skuHint"></div>
<option value="iges">IGES</option>
</select>
<a id="btnDatasheet" class="btn hidden">下载 PDF</a>
<button id="btnAddCart" class="btn hidden">加入清单</button>
</div> </div>
<div id="paramPanel"></div>
</section> </section>
<div id="split2" class="split hidden" title="拖拽调整宽度"></div> <div id="split2" class="split hidden" title="拖拽调整宽度"></div>
<section id="content" class="content hidden"> <section id="content" class="content hidden">
<div class="tabs"> <div class="tabs" id="tabsBar">
<button class="tab active" data-tab="3d">3D / 尺寸图</button> <button class="tab active" data-tab="3d">3D / 尺寸图</button>
<button class="tab" data-tab="table">参数表</button> <button class="tab" data-tab="table">参数表</button>
<button class="tab" data-tab="search">搜索结果</button> <button class="tab" data-tab="search">搜索结果</button>
<button class="tab" data-tab="cross">平替对照</button> <button class="tab" data-tab="cross">平替对照</button>
<span class="tabs-spacer"></span>
<div id="actions">
<a id="btnDownload" class="btn primary hidden" download>下载 STEP</a>
<select id="btnGenFmt" class="hidden" title="下载格式 (仅非标档位在线生成)">
<option value="zip">全格式 zip</option>
<option value="step">STEP</option>
<option value="iges">IGES</option>
</select>
<a id="btnDatasheet" class="btn hidden">下载 PDF</a>
<button id="btnAddCart" class="btn hidden">加入清单</button>
</div>
</div> </div>
<div class="content-back"> <div class="content-back">
<button id="btnBackHome" class="btn">← 返回系列总览</button> <button id="btnBackHome" class="btn">← 返回系列总览</button>
@@ -115,6 +123,6 @@
<script src="libs/jszip.min.js?v=1"></script> <script src="libs/jszip.min.js?v=1"></script>
<script src="config.js?v=1"></script> <script src="config.js?v=1"></script>
<script src="app.js?v=18"></script> <script src="app.js?v=19"></script>
</body> </body>
</html> </html>

View File

@@ -245,8 +245,17 @@ main { flex: 1; display: flex; min-height: 0; }
font-size: 16px; font-weight: 700; margin-bottom: 12px; font-size: 16px; font-weight: 700; margin-bottom: 12px;
color: var(--gray-900); letter-spacing: -.3px; line-height: 1.4; color: var(--gray-900); letter-spacing: -.3px; line-height: 1.4;
} }
.param-row { margin-bottom: 11px; } .param-row { margin-bottom: 14px; }
.param-row label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 4px; } .param-row label {
display: flex; align-items: baseline; gap: 7px;
font-size: 11px; font-weight: 700; color: var(--text-faint);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 7px;
}
/* 维度已选值回显在其后 */
.param-row label .picked {
text-transform: none; letter-spacing: 0; font-weight: 600;
color: var(--primary-700); font-size: 11.5px;
}
.param-row select, .param-input { .param-row select, .param-input {
width: 100%; height: 36px; padding: 0 10px; font-size: 13px; width: 100%; height: 36px; padding: 0 10px; font-size: 13px;
border: 1px solid var(--border); border-radius: var(--radius-sm); border: 1px solid var(--border); border-radius: var(--radius-sm);
@@ -256,7 +265,114 @@ main { flex: 1; display: flex; min-height: 0; }
.param-row select:focus, .param-input:focus { .param-row select:focus, .param-input:focus {
border-color: var(--primary-500); box-shadow: 0 0 0 3px rgba(20,184,166,.15); border-color: var(--primary-500); box-shadow: 0 0 0 3px rgba(20,184,166,.15);
} }
#actions { margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap; } /* #actions 原在左侧参数面板底部,现移到 tab 行右侧(与 3D/尺寸图 等同一行)。
元素 id 未变,JS 按 id 取用,位置移动不影响逻辑。 */
#actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* tab 行的弹性占位:把 #actions 推到最右 */
.tabs-spacer { flex: 1; min-width: 8px; }
/* 窄屏时按钮换行到下一行,避免挤压 tab */
@media (max-width: 1100px) {
.tabs { flex-wrap: wrap; }
.tabs-spacer { flex-basis: 100%; height: 0; }
}
/* ============================================================ 零件号实时条 (DESTACO 风)
* 边选边拼: 已确定部分实心高亮, 未定部分虚化占位。 */
/* ============================================================ 零件号实时条
* 边选边拼: 已确定片段的实心高亮, 未定片段虚化占位。
* 配色走浅色主调 (与整体卡片风格一致), 不用深色块。 */
.sku-bar {
background: linear-gradient(135deg, var(--primary-50), #ffffff 70%);
border: 1px solid var(--primary-200);
border-radius: var(--radius); padding: 13px 15px; margin-bottom: 14px;
box-shadow: var(--shadow-card);
}
.sku-bar .sku-label {
font-size: 10px; text-transform: uppercase; letter-spacing: .7px;
color: var(--text-faint); margin-bottom: 5px;
}
.sku-bar .sku-code {
font-family: ui-monospace, Consolas, monospace;
font-size: 20px; font-weight: 700; letter-spacing: -.4px;
word-break: break-all; line-height: 1.25; min-height: 25px;
}
.sku-bar .sku-fixed { color: var(--primary-700); } /* 已确定片段 */
.sku-bar .sku-tbd { color: var(--gray-300); } /* 待定占位 */
.sku-bar .sku-prog {
margin-top: 9px; height: 3px; border-radius: 999px;
background: var(--primary-100); overflow: hidden;
}
.sku-bar .sku-prog > i {
display: block; height: 100%; width: 0;
background: linear-gradient(90deg, var(--primary-400), var(--primary-600));
border-radius: 999px; transition: width .3s cubic-bezier(.2,.8,.3,1);
}
/* 零件号完整: 整块转为实心青色, 作为"完成"的强反馈 */
.sku-bar.done {
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
border-color: transparent;
box-shadow: var(--shadow-btn);
}
.sku-bar.done .sku-label { color: rgba(255,255,255,.72); }
.sku-bar.done .sku-fixed { color: #fff; }
.sku-bar.done .sku-tbd { color: rgba(255,255,255,.5); }
.sku-bar.done .sku-prog { background: rgba(255,255,255,.28); }
.sku-bar.done .sku-prog > i { background: #fff; }
.sku-bar.done .sku-hint { color: rgba(255,255,255,.85); }
.sku-bar .sku-hint {
margin-top: 8px; font-size: 11px; color: var(--text-dim);
}
/* ============================================================ 选项块 (替代下拉)
* 有图用卡片, 无图用文字块; 一行多个, 自动换行。 */
.opt-grid {
display: grid; gap: 7px;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
}
.opt-grid.wide { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
.opt {
position: relative; cursor: pointer; user-select: none;
border: 1.5px solid var(--border); border-radius: var(--radius-sm);
background: var(--surface); padding: 8px 9px;
display: flex; flex-direction: column; align-items: center; gap: 3px;
min-height: 46px; justify-content: center;
transition: transform .18s cubic-bezier(.2,.8,.3,1),
border-color .18s, background .18s, box-shadow .18s;
}
.opt:hover {
transform: translateY(-2px); border-color: var(--primary-400);
box-shadow: 0 6px 16px rgba(20,184,166,.18);
}
.opt .opt-k {
font-family: ui-monospace, Consolas, monospace;
font-size: 13px; font-weight: 700; color: var(--gray-900);
line-height: 1.2; text-align: center; word-break: break-word;
}
.opt .opt-d {
font-size: 10px; color: var(--text-faint); text-align: center;
line-height: 1.25; word-break: break-word;
}
/* 选中态: 青色填充 */
.opt.on {
border-color: var(--primary-500);
background: var(--primary-50);
box-shadow: 0 0 0 2px rgba(20,184,166,.22);
}
.opt.on .opt-k { color: var(--primary-700); }
.opt.on::after {
content: '✓'; position: absolute; top: 3px; right: 5px;
font-size: 10px; color: var(--primary-600); font-weight: 700;
}
/* 不可选 (被联动规则排除): 置灰 + 划线 + 不可点 */
.opt.off {
opacity: .32; cursor: not-allowed; background: var(--gray-50);
border-style: dashed;
}
.opt.off .opt-k { text-decoration: line-through; }
.opt.off:hover { transform: none; box-shadow: none; border-color: var(--border); }
/* 无图时的纯文字块: 更紧凑 */
.opt.txt { min-height: 38px; padding: 7px 8px; }
.opt.txt .opt-k { font-size: 12.5px; }
/* ------------------------------------------------------------ 按钮 */ /* ------------------------------------------------------------ 按钮 */
.btn { .btn {
@@ -280,10 +396,114 @@ main { flex: 1; display: flex; min-height: 0; }
.hidden { display: none !important; } .hidden { display: none !important; }
/* ------------------------------------------------------------ 嵌入模式
供 Odoo 产品页 iframe 使用 (?embed=1):隐藏工具自身的顶栏与底栏,
避免在网站页面里出现"页中页",同时把高度让给内容区。
由 app.js 的 applyDeepLink() 给 body 加 .embed-mode 触发。 */
body.embed-mode header,
body.embed-mode footer { display: none !important; }
body.embed-mode { height: 100vh; }
/* 隐藏模式 (?nav=0):嵌入 Odoo 产品页时用。
产品页已经决定了是哪个系列,左侧系列树、分隔条、「返回系列总览」纯属多余,
隐藏后让参数面板 + 内容区铺满整宽,观感更干净。 */
body.nav-off #tree,
body.nav-off #split1,
body.nav-off .content-back { display: none !important; }
/* 隐藏侧栏后,参数面板适当加宽(原来 330px 是因为旁边还有树+主区抢空间) */
body.nav-off .params { width: 300px; }
/* ------------------------------------------------------------ 内容区 / 标签页 */ /* ------------------------------------------------------------ 内容区 / 标签页 */
.content { flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 14px; } .content { flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 14px; }
.tabs { display: flex; gap: 6px; flex: none; } .tabs { display: flex; gap: 6px; flex: none; align-items: center; }
.content-back { flex: none; margin: 10px 0 0; } .content-back { flex: none; margin: 10px 0 0; }
/* ------------------------------------------------------------ 手册页图片放大
点击手册页图片 → 全屏遮罩显示大图。点遮罩任意处 / 按 Esc 关闭。
用 position:fixed 覆盖整个视口(含 iframe 内嵌场景)。 */
.manual-img { cursor: zoom-in; }
/* 数据表按钮图标(内联 SVG,工具无图标字体) */
.ds-ico { width: 15px; height: 15px; flex: none; vertical-align: -3px; }
.ds-ico-eye { opacity: .65; }
#btnDatasheet { display: inline-flex; align-items: center; gap: 6px; }
#btnDatasheet:hover .ds-ico-eye { opacity: 1; }
#btnDatasheet .ds-txt { white-space: nowrap; }
.img-zoom-overlay {
position: fixed; inset: 0; z-index: 900;
background: rgba(17, 24, 39, .88);
display: flex; align-items: center; justify-content: center;
padding: 24px; cursor: zoom-out;
animation: imgZoomIn .16s ease-out;
}
@keyframes imgZoomIn { from { opacity: 0; } to { opacity: 1; } }
.img-zoom-overlay img {
max-width: 100%; max-height: 100%;
object-fit: contain; border-radius: 6px;
box-shadow: 0 12px 48px rgba(0, 0, 0, .5);
}
.img-zoom-close {
position: absolute; top: 14px; right: 18px;
width: 40px; height: 40px; border: 0; border-radius: 50%;
background: rgba(255, 255, 255, .16); color: #fff;
font-size: 22px; line-height: 1; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background .18s;
}
.img-zoom-close:hover { background: rgba(255, 255, 255, .3); }
.img-zoom-cap {
position: absolute; bottom: 14px; left: 0; right: 0;
text-align: center; color: rgba(255, 255, 255, .75); font-size: 12.5px;
pointer-events: none;
}
/* ------------------------------------------------------------ 数据表 PDF 弹窗预览
点击「数据表」→ 弹窗内用浏览器内置 PDF 阅读器打开,标题栏含下载入口。 */
.pdf-zoom-overlay {
position: fixed; inset: 0; z-index: 910;
background: rgba(17, 24, 39, .72);
display: flex; align-items: center; justify-content: center;
padding: 24px;
animation: imgZoomIn .16s ease-out;
}
.pdf-zoom-box {
width: min(1100px, 100%); height: min(92vh, 100%);
display: flex; flex-direction: column;
background: var(--surface, #fff);
border-radius: 10px; overflow: hidden;
box-shadow: 0 16px 56px rgba(0, 0, 0, .45);
}
.pdf-zoom-head {
flex: none; display: flex; align-items: center; gap: 12px;
padding: 10px 12px 10px 16px;
border-bottom: 1px solid var(--border, #e5e7eb);
background: var(--surface, #fff);
}
.pdf-zoom-title {
flex: 1; font-size: 13px; font-weight: 600; color: var(--gray-900, #111827);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pdf-zoom-dl {
flex: none; font-size: 12.5px; text-decoration: none;
padding: 6px 14px; border-radius: 6px;
background: var(--primary, #14b8a6); color: #fff;
transition: filter .18s;
}
.pdf-zoom-dl:hover { filter: brightness(.94); }
.pdf-zoom-close {
position: static; flex: none;
width: 32px; height: 32px; font-size: 18px;
background: var(--gray-100, #f3f4f6); color: var(--gray-700, #374151);
}
.pdf-zoom-close:hover { background: var(--gray-200, #e5e7eb); }
.pdf-zoom-frame {
flex: 1; width: 100%; border: 0; background: #525659;
}
@media (max-width: 767.98px) {
.pdf-zoom-overlay { padding: 0; }
.pdf-zoom-box { width: 100%; height: 100%; border-radius: 0; }
}
.tab { .tab {
height: 36px; padding: 0 16px; border: 1px solid var(--border); height: 36px; padding: 0 16px; border: 1px solid var(--border);
border-radius: var(--radius-sm); background: var(--surface); border-radius: var(--radius-sm); background: var(--surface);

201
工作总结-2026-09.md Normal file
View File

@@ -0,0 +1,201 @@
# 工作总结 — OnebotCatalog 数据完善与界面改版
**时间范围**:2026-09-13 ~ 09-15
**版本**:v1.0 → v2.0 → v2.1(桌面版改动未提交,见文末)
**代码仓库**:https://git.ruiguo.wang/wangruiguo/OnebotCatalog
---
## 一、总体成果
| 指标 | 改动前 | 改动后 |
|---|---|---|
| 系列数 | 40 | 40 |
| **型号总数** | **54,136** | **57,293** |
| 真实数据分类 | 3 个(气缸/迷你缸)| **7 个**(+夹紧缸/定位销/勾销缸/翻转缸/电缸)|
| 手册附图 | 灰度、部分系列仅 1 页 | **彩色 346 页,覆盖全部 42 系列** |
| 网页版选型交互 | 下拉菜单 | **DESTACO 风格(选项块 + 零件号实时拼装)** |
| 数据包体积 | 1.21 GB | 2.14 GB |
各分类现状:
| 分类 | 系列 | 型号数 |
|---|---|---|
| AUT 汽车行业气缸 | 6 | 12,554 |
| MIN 不锈钢迷你缸 | 3 | 25,200 |
| ALU 铝合金迷你缸 | 2 | 14,400 |
| CLM 气动夹紧缸 | 11 | 860 |
| PIN 气动可伸缩定位销 | 9 | 3,455 |
| HOK 气动勾销缸 | 5 | 72 |
| ROT 气动翻转缸 | 2 | 741 |
| ELEC 电动气缸 | 1 | 9 |
| GRP 气动夹爪缸 | 1 | 2(仍为占位,X 盘无真实数模)|
---
## 二、按阶段的工作
### 阶段 1(v2.0):4 个分类的真实数据替换
**背景**:定位销 / 勾销缸 / 翻转缸 / 电缸 4 个分类此前是 demo 占位数据(每类仅 1-3 个型号)。
**做法**:
1. 用 `pdftotext` 提取 **19 份 Univer 官方图册的 CODIFICATION KEY**,拿到每个系列的官方订购码结构(不靠推测)
2. 扫描 X 盘 3D 数模,按「官方值域校验 + 左对齐 DFS 填空」解析文件名
3. 生成 CSV / meta / 复制 STEP
**成果**:新增 19 系列 **1,124 个真实型号**(PIN 303 / HOK 73 / ROT 741 / ELEC 9)
**过程中解决的关键问题**:
| 问题 | 处理 |
|---|---|
| 解析器无法处理省略空位的型号(如 `ULP50040SK0`)| 改用官方值域 DFS 填空 |
| 参数组合可能一对多(选型会静默错配)| 加**唯一性门禁**,1124 型号全部通过 |
| 占位/测试文件(如 `USP50J_0___K0` 销径为空)| 解析阶段丢弃 |
| catalog.json 分类系列表未同步 → 10 个系列没进包 | 更新 `CLM.series` 数组 |
| 手册页全是灰度 | 脚本误写死 `colorspace=fitz.csGRAY`,去掉后重做**彩色 346 页** |
| UCBT 附图只有 1 页 | 筛选条件写死 `UCBT40` 且漏 `CLAMPING ARMS`,已修正为 3 页 |
| 重跑数据脚本会覆盖 meta 附件 | 改为**扫描 manual 目录自动生成**附件清单 |
| 数据包 2.25GB 超 MemoryStream 2GB 上限 | `OpcPackage` 超大包改走临时文件(保留原有句柄释放语义)|
### 阶段 2(v2.0):网页版界面改版
- **首页**:全部产品缩略图网格,按大类分组
- **左树**:手风琴(一次展开一个),父级带 **9 个内置 SVG 图标**,悬停放大动画,可收起为图标窄条
- **缩略图**:40 张(27 张真实产品图从 `bom-viewer` 复用 + 13 张占位图)
- 修复分隔条被拖出容器后无法找回(**容器感知上限 + resize 重夹取 + 双击复位**)
- 修复搜索到型号后点结果内容区空白(`switchTab('dim')` 已失效)
- 购物车打包改取 `.step.gz` 副档(传输量降至约 1/4.5)
### 阶段 3(v2.1):DESTACO 风格选型 + 参数组合补全
**3.1 零件号实时拼装**
DESTACO 配置器的核心体验。技术难点:**40 个系列中只有 KC 有编码模板**。
解法:用**「当前候选型号的最长公共前缀」** —— 选得越多候选越少、前缀越长,自然拼出完整零件号。
已验证 **40/40 系列前缀单调增长**(不回退)。
**3.2 参数选择改为选项块**
下拉菜单 → 可视化卡片:悬停抬升+青色投影、选中青色填充+✓、
**不可选取值置灰保留**(而非消失,让用户看到选项存在)、再点一次取消。
**3.3 参数组合补全(缺组合也能出零件号)**
原逻辑要求「必须有真实数模才能生成型号」,导致库里没有的组合选不出来、也出不了号。
1. **反推编码模板**:`infer-sku-template.py` 从已有型号反推出 **23 个系列 100% 可复现**的模板
2. **加自检门禁**:`verify-templates.py` 用模板重渲染每个已有型号必须逐字一致。
**这道检查抓出了真实错误** —— ULM/ULP 行程补零位数写错(2 位写成 3 位),
若无此校验会**生成 3,000 个不存在的零件号发给客户**
3. 补全 8 个系列共 **3,157 个型号**(全库 54,136 → 57,293)
4. **占位数模**:3157 个型号共用 1 个 `PLACEHOLDER.step`(4.5KB),
数据包仅从 2.10GB 增至 2.14GB
5. 输出 `_pending_models.csv` 待补清单供 AI 补真实数模
### 阶段 4:桌面版同步(未提交)
将 3.1 / 3.2 同步到 C#/WPF 桌面版,保持两端语义一致:
- 新增 `OptionTile` 控件类(三态选项块)
- 零件号实时条(复用同一套最长公共前缀算法)
- range 参数保留可编辑下拉
- **编译首次通过**,算法对拍 **40/40 系列通过**
---
## 三、发现但未处理的问题
按优先级排列:
### 1. ULP 生成的候选有 56% 超出依据范围 ⚠️
补全时用的是**系列级值域**(整个 ULP 系列各维度取值的并集),而非**按缸径分级**。
| 缸径 | 真实型号数 | 真实行程 | 真实传感器 |
|---|---|---|---|
| ULP32 | 14 | 20, 40 | 只有 K |
| ULP50 | 83 | 15~110 | D, K |
| ULP63 | 71 | 15~60 | D, K |
结果:给 ULP32 生成了 9 种行程、传感器 D、杆样式 D/F/H/I/L/M —— 这些在该缸径的真实数据中**从未出现**。
**风险量化**(`assess-combo-risk.py`):
```
低风险(取值在同缸径真实数据都出现过) 1,212 (43%)
高风险(含该缸径从未出现的取值) 1,590 (56%)
```
**建议**:收窄到低风险的 1,212 个;其余等 AI 补数模时先核对官方图册。
> 注:曾三次尝试从官方 PDF 提取分级值域,均因**图册双栏排版导致文本化后左右栏交错**而失败
> (把真实数据都判成越界 = 解析错误)。故改用「同缸径真实数据」作为可靠代理。
### 2. 夹紧缸 9 个系列:一组参数对应多个型号
| 系列 | 变体数 | 重复组数 | 一组最多 |
|---|---|---|---|
| UCBP | 358 | 46 组 | 18 个 |
| UCBM | 266 | 26 组 | 15 个 |
| UCBH / UCBQ / UCDP / UCIM / UCIP / UFL / UCBT | — | 2-7 组 | 2-6 个 |
共 **310 个型号**受影响。原因是编码含**参数之外的尾缀**(版本号/变体标记),
如 `UCBP25AK00` vs `UCBP25AK010`。界面已提示"该组合对应 N 个型号",但无法唯一确定。
### 3. UAGP / LAGP 参数顺序与官方编码结构不符
官方订购码顺序为 `规格-台位-传感器-转角-制动-接口`,当前 meta 里传感器排在最后。
不影响选型正确性(参数按名索引),但**导致编码模板推不出来**,界面展示顺序也与官方不一致。
### 4. 17 个系列未补全组合
模板验证未通过的系列保持原状 —— 宁可少补,也不生成可能不存在的零件号。
### 5. 夹爪缸 UGP 仍为占位
X 盘无 UGP 系列真实数模(只有 GR8 抓手组装体),需确认 Univer 官方夹爪缸系列代号。
---
## 四、关键经验(避免重复踩坑)
| 坑 | 教训 |
|---|---|
| **模板必须先自检** | 用模板重渲染全部已有型号,逐字一致才能用于生成新号。省这道检查会造出不存在的零件号 |
| **PowerShell 变量名大小写不敏感** | `$root` 与 `$Root` 是同一变量,会互相覆盖(启动脚本因此指向错误目录)|
| **.ps1 必须带 UTF-8 BOM** | PS 5.1 对无 BOM 文件按 ANSI 读,中文注释会破坏解析(用 `tools/fix-bom.ps1`)|
| **脚本重跑会覆盖手工结果** | meta 附件清单改为扫描目录自动生成,而非硬编码 |
| **NTFS 硬链接上限 1024** | 超限部分退化为真实拷贝(曾白占 500MB)。改用共享单文件 |
| **Join-Path 不做路径归约** | 需再 `GetFullPath` 规范化 |
| **MemoryStream 上限 2GB** | 超大 .opc 改走临时文件副本 |
---
## 五、产出与交付
**新增脚本(11 个)**:`infer-sku-template` / `verify-templates` / `quantify-combo-gap` /
`gen-missing-combos` / `gen-placeholder-step` / `check-combo-unique` / `assess-combo-risk` /
`make-pending-list` / `gen-step-gz` / `gen-series-thumbs` / `serve-web-bg`
**新增文档**:`docs/更改说明-v2.1.md`、本文件
**发布产物(release/,未纳入 Git)**:
- `OnebotCatalog_2026.09/` — 客户版目录(catalog.opc 2.14GB)
- `update_2026.09/catalog.opc` — 增量更新包
- `web_2026.09/` — 网页站(含 3,285 个 gz 副档)
---
## 六、待办
1. **决定 ULP 高危候选如何处置**(收窄 / 维持 / 由 AI 核对官方图册)
2. **桌面版改动提交推送**(当前工作区有未提交改动,建议发 v2.2)
3. AI 补真实数模:按 `_pending_models.csv` 逐个核对后替换 `step_file`
4. 处理夹紧缸一对多问题(补维度或清理尾缀)
5. 确认 UGP 夹爪缸官方系列代号
---
*本总结由 Claude Code 生成于 2026-09-15*