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>
This commit is contained in:
81
tools/serve-web-bg.ps1
Normal file
81
tools/serve-web-bg.ps1
Normal 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
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
# UI 层诊断 (STA): 反射创建主窗口, 对每个系列执行 LoadSeries,
|
||||
# 检查下拉框数据源是否为空 / 是否抛异常。
|
||||
# 校验选项块/下拉的选项数、可选态、以及选中后零件号实时条的输出。
|
||||
# 用法: powershell -STA -NoProfile -ExecutionPolicy Bypass -File tools\ui-diagnose.ps1
|
||||
param(
|
||||
[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'
|
||||
Add-Type -AssemblyName PresentationFramework
|
||||
@@ -16,23 +17,84 @@ $pkg = $pkgType.GetMethod('Open').Invoke($null, @($Opc))
|
||||
|
||||
$mwType = $asm.GetType('Ounibo.Catalog.App.MainWindow')
|
||||
$mw = [Activator]::CreateInstance($mwType, @($pkg, $false))
|
||||
$ls = $mwType.GetMethod('LoadSeries', [Reflection.BindingFlags]'NonPublic,Instance')
|
||||
$boxesField = $mwType.GetField('_boxes', [Reflection.BindingFlags]'NonPublic,Instance')
|
||||
$np = [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) {
|
||||
try {
|
||||
$ls.Invoke($mw, @($s)) | Out-Null
|
||||
$boxes = $boxesField.GetValue($mw)
|
||||
$tiles = $tilesF.GetValue($mw)
|
||||
$boxes = $boxesF.GetValue($mw)
|
||||
|
||||
# 每个枚举维度: 选项块总数 / 可选数 (与参数定义值域比对)
|
||||
$parts = @()
|
||||
foreach ($k in $boxes.Keys) {
|
||||
$cb = $boxes[$k]
|
||||
$count = 0
|
||||
if ($cb.ItemsSource -ne $null) { $count = $cb.Items.Count }
|
||||
$parts += ($k + '=' + $count)
|
||||
$bad = @()
|
||||
foreach ($p in $s.parameters) {
|
||||
if ($tiles.ContainsKey($p.code)) {
|
||||
$list = $tiles[$p.code]
|
||||
$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 {
|
||||
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()
|
||||
if ($fail -gt 0) { exit 1 }
|
||||
|
||||
Reference in New Issue
Block a user