桌面版 (仅网页版 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>
1937 lines
95 KiB
C#
1937 lines
95 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Diagnostics;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.IO.Compression;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Input;
|
||
using System.Windows.Media;
|
||
using System.Windows.Media.Imaging;
|
||
using System.Windows.Media.Media3D; // Helix 窗口内 3D (ModelVisual3D 等)
|
||
using System.Windows.Controls.Primitives; // DataGridColumnHeader (参数表按列筛选)
|
||
using Microsoft.Win32;
|
||
using Ounibo.Catalog.Core;
|
||
using Ounibo.Catalog.Viewer3D;
|
||
|
||
namespace Ounibo.Catalog.App
|
||
{
|
||
/// <summary>简单双语: zh/en 文案切换。</summary>
|
||
public class Lang
|
||
{
|
||
public string Code = "zh";
|
||
public string T(string zh, string en) { return Code == "en" ? en : zh; }
|
||
public void Toggle() { Code = (Code == "zh") ? "en" : "zh"; }
|
||
}
|
||
|
||
public class ComboItem
|
||
{
|
||
public object Value;
|
||
public string Text;
|
||
public ComboItem(object v, string t) { Value = v; Text = t; }
|
||
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>
|
||
/// 主窗口: 目录树 + 参数选型 + 3D 预览 + 尺寸图/参数表/数据表 + 搜索 + STEP 下载/批量下载。
|
||
/// </summary>
|
||
public class MainWindow : Window
|
||
{
|
||
Lang _lang = new Lang();
|
||
OpcPackage _pkg;
|
||
Configurator _cfg;
|
||
SearchEngine _search;
|
||
Series _series;
|
||
readonly Dictionary<string, object> _sel = new Dictionary<string, object>();
|
||
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, TextBox[]> _rangeBoxes = new Dictionary<string, TextBox[]>();
|
||
List<SearchEngine.SearchItem> _searchResult;
|
||
bool _refreshing;
|
||
#if !CUSTOMER_BUILD
|
||
bool _admin;
|
||
#endif
|
||
|
||
TreeView _tree;
|
||
StackPanel _paramPanel, _rangePanel;
|
||
ViewerControl _viewer;
|
||
Image _dimImage;
|
||
StackPanel _dimPanel, _manualPanel;
|
||
DataGrid _grid, _searchGrid, _crossGrid;
|
||
ComboBox _crossBrand;
|
||
TabControl _tabs;
|
||
TextBlock _modelTitle, _status, _pkgInfo;
|
||
Button _btnDownload, _btnDownloadPdf, _btnAddCart, _btnOpen, _btnLang, _btnCart, _btnSearch, _btnExport, _btnRangeApply, _btnRangeReset;
|
||
string _curModelCode; // 当前已选型号 (平替对照按此过滤)
|
||
DataTable _gridDt; // 参数表数据 (RowFilter 按列筛选)
|
||
Dictionary<string, string> _colFilter = new Dictionary<string, string>(); // 参数表按列筛选 {参数code: 值}
|
||
List<CartItem> _cart = new List<CartItem>(); // 购物车 (淘宝式清单, 与选型状态解耦)
|
||
Variant _curVariant; // 当前已解析型号 (加入清单用)
|
||
ParamResult _curParam; // 当前参数化结果 (非标档位时非空, 与 _curVariant 互斥; 一期 2026-08-25)
|
||
Border _viewerHint; // 3D 视口顶部非标档位提示条
|
||
string _genApi = ""; // NX 按需生成服务地址 (catalog.json genApi; 空=非标下载置灰)
|
||
ComboBox _btnGenFmt; // 生成格式选择 (非标档位在线生成, 二期)
|
||
CartWindow _cartWin; // 清单窗口 (非模态, 数据在主窗口)
|
||
StackPanel _fallbackPanel; // 真实数模 (桌面解析器不支持) → 浏览器预览回退面板
|
||
Button _btnBrowser3D;
|
||
LocalViewerServer _previewSrv; // 本机 3D 预览微型服务器 (惰性启动)
|
||
string _curStepName; // 当前变体 STEP 文件名 (浏览器预览用)
|
||
byte[] _curStepBytes;
|
||
HelixToolkit.Wpf.HelixViewport3D _helixView; // 开源 Helix Toolkit (MIT): 真实数模网格 (STL) 窗口内直渲
|
||
Microsoft.Web.WebView2.Wpf.WebView2 _webView2; // 方案 B: 窗口内嵌新迪查看器, 真实 STEP 直显 (免弹浏览器)
|
||
bool _webView2Ready; // WebView2 运行时可用标记 (不可用自动降级)
|
||
#if !CUSTOMER_BUILD
|
||
Button _btnAdmin;
|
||
#endif
|
||
TextBox _searchBox;
|
||
|
||
public OpcPackage Package { get { return _pkg; } }
|
||
|
||
public MainWindow(OpcPackage pkg, bool admin)
|
||
{
|
||
_pkg = pkg;
|
||
_genApi = string.IsNullOrEmpty(pkg.Catalog.genApi) ? "" : pkg.Catalog.genApi.TrimEnd('/'); // NX 按需生成服务 (二期)
|
||
#if !CUSTOMER_BUILD
|
||
_admin = admin;
|
||
#endif
|
||
Width = 1280; Height = 840; MinWidth = 900; MinHeight = 560;
|
||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||
var logo = AppResources.LoadPng("OneBot-logo.png");
|
||
if (logo != null) Icon = logo; // 窗口图标 (OneBot logo)
|
||
BuildUi();
|
||
ApplyLang();
|
||
if (_pkg != null) LoadPackageIntoUi();
|
||
Closed += (s, e) => { if (_previewSrv != null) { try { _previewSrv.Dispose(); } catch { } } };
|
||
}
|
||
|
||
void BuildUi()
|
||
{
|
||
var root = new DockPanel();
|
||
|
||
// ---- 顶栏 ----
|
||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(8, 6, 8, 4) };
|
||
var logoImg = AppResources.LoadPng("OneBot-logo.png");
|
||
if (logoImg != null)
|
||
top.Children.Add(new Image { Source = logoImg, Height = 26, Stretch = Stretch.Uniform, Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center });
|
||
_btnOpen = new Button { Width = 110, Margin = new Thickness(0, 0, 6, 0) };
|
||
_btnOpen.Click += (s, e) => OpenPackage();
|
||
_searchBox = new TextBox { Width = 200, Margin = new Thickness(10, 0, 4, 0) };
|
||
_searchBox.KeyDown += (s, e) => { if (e.Key == Key.Enter) DoSearch(); };
|
||
_btnSearch = new Button { Width = 60, Margin = new Thickness(0, 0, 6, 0) };
|
||
_btnSearch.Click += (s, e) => DoSearch();
|
||
_btnLang = new Button { Width = 60, Margin = new Thickness(0, 0, 6, 0) };
|
||
_btnLang.Click += (s, e) => { _lang.Toggle(); ApplyLang(); };
|
||
// 清单角标按钮 (常驻顶栏, 2026-08-24)
|
||
_btnCart = new Button { Width = 90, Margin = new Thickness(6, 0, 0, 0) };
|
||
_btnCart.Click += (s, e) => OpenCart();
|
||
#if !CUSTOMER_BUILD
|
||
_btnAdmin = new Button { Width = 100, Margin = new Thickness(0, 0, 6, 0), Visibility = _admin ? Visibility.Visible : Visibility.Collapsed };
|
||
_btnAdmin.Click += (s, e) => { var w = new AdminWindow(this); w.ShowDialog(); };
|
||
#endif
|
||
_pkgInfo = new TextBlock { VerticalAlignment = VerticalAlignment.Center };
|
||
top.Children.Add(_btnOpen);
|
||
top.Children.Add(_searchBox); top.Children.Add(_btnSearch);
|
||
top.Children.Add(_btnLang);
|
||
top.Children.Add(_btnCart);
|
||
#if !CUSTOMER_BUILD
|
||
top.Children.Add(_btnAdmin);
|
||
#endif
|
||
top.Children.Add(_pkgInfo);
|
||
DockPanel.SetDock(top, Dock.Top);
|
||
root.Children.Add(top);
|
||
|
||
// ---- 底部状态栏 ----
|
||
_status = new TextBlock { Margin = new Thickness(8, 4, 8, 6) };
|
||
DockPanel.SetDock(_status, Dock.Bottom);
|
||
root.Children.Add(_status);
|
||
|
||
// ---- 中部: 目录树 | 参数面板 | 标签页 (分隔条可拖拽调整宽度) ----
|
||
var center = new Grid();
|
||
center.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(250), MinWidth = 150, MaxWidth = 420 });
|
||
center.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
center.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(320), MinWidth = 220, MaxWidth = 520 });
|
||
center.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
center.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||
|
||
_tree = new TreeView();
|
||
Grid.SetColumn(_tree, 0);
|
||
center.Children.Add(_tree);
|
||
center.Children.Add(MakeSplitter(1));
|
||
|
||
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 };
|
||
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();
|
||
left.Children.Add(_paramPanel); // 参数最多 7 行, 直接平铺不滚动 (避免下方参数被遮挡)
|
||
// 下载操作区 (2026-08-24, 与网页版一致: 参数下方并排 下载 STEP / 下载 PDF, 同款配色)
|
||
var actions = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 10, 0, 0) };
|
||
_btnDownload = new Button
|
||
{
|
||
Padding = new Thickness(14, 6, 14, 6),
|
||
FontSize = 13,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
Visibility = Visibility.Collapsed,
|
||
Background = new SolidColorBrush(Color.FromRgb(0x1B, 0x4F, 0x8C)),
|
||
Foreground = Brushes.White,
|
||
BorderThickness = new Thickness(0),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
_btnDownload.Click += (s, e) =>
|
||
{
|
||
// 非标档位 + 生成服务可用 → 在线生成 (二期); 否则原 STEP 下载
|
||
if (_curParam != null && !string.IsNullOrEmpty(_genApi)) GenSingle(_curParam);
|
||
else DownloadStep();
|
||
};
|
||
// 生成格式选择 (非标档位在线生成用; 生成输出留存全格式, 格式只影响下载)
|
||
_btnGenFmt = new ComboBox
|
||
{
|
||
Width = 108,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
Visibility = Visibility.Collapsed,
|
||
ItemsSource = new[] {
|
||
new ComboItem("zip", "全格式 zip"),
|
||
new ComboItem("step", "STEP"),
|
||
new ComboItem("parasolid", "Parasolid"),
|
||
new ComboItem("iges", "IGES"),
|
||
new ComboItem("stl", "STL")
|
||
},
|
||
SelectedIndex = 0
|
||
};
|
||
_btnDownloadPdf = new Button
|
||
{
|
||
Padding = new Thickness(14, 6, 14, 6),
|
||
FontSize = 13,
|
||
Visibility = Visibility.Collapsed,
|
||
Background = new SolidColorBrush(Color.FromRgb(0xE9, 0xEC, 0xEF)),
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)),
|
||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xCC, 0xCC, 0xCC)),
|
||
BorderThickness = new Thickness(1),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
_btnDownloadPdf.Click += (s, e) => DownloadDatasheet();
|
||
// 加入清单 (淘宝式购物车, 2026-08-24)
|
||
_btnAddCart = new Button
|
||
{
|
||
Padding = new Thickness(14, 6, 14, 6),
|
||
FontSize = 13,
|
||
Margin = new Thickness(0, 0, 8, 0),
|
||
Visibility = Visibility.Collapsed,
|
||
Background = new SolidColorBrush(Color.FromRgb(0xE9, 0xEC, 0xEF)),
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)),
|
||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xCC, 0xCC, 0xCC)),
|
||
BorderThickness = new Thickness(1),
|
||
Cursor = Cursors.Hand
|
||
};
|
||
_btnAddCart.Click += (s, e) =>
|
||
{
|
||
if (_curVariant != null) AddToCart(_curVariant);
|
||
else if (_curParam != null) AddToCart(_curParam);
|
||
};
|
||
actions.Children.Add(_btnDownload); actions.Children.Add(_btnGenFmt); actions.Children.Add(_btnDownloadPdf); actions.Children.Add(_btnAddCart);
|
||
left.Children.Add(actions);
|
||
Grid.SetColumn(left, 2);
|
||
center.Children.Add(left);
|
||
center.Children.Add(MakeSplitter(3));
|
||
|
||
_tabs = new TabControl();
|
||
var tab3d = new TabItem();
|
||
// 3D/尺寸图同页 (2026-08-24, 与网页版布局一致): 左 3D 视口 | 可拖拽分隔条 | 右 尺寸图+手册页
|
||
var grid3d = new Grid();
|
||
grid3d.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star), MinWidth = 200 });
|
||
grid3d.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||
grid3d.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(320), MinWidth = 180, MaxWidth = 640 });
|
||
var view3d = new Grid();
|
||
_viewer = new ViewerControl();
|
||
view3d.Children.Add(_viewer);
|
||
// 窗口内 3D (开源 Helix Toolkit, MIT): 渲染 NX 导出的 STL 网格, 免浏览器
|
||
_helixView = new HelixToolkit.Wpf.HelixViewport3D
|
||
{
|
||
Visibility = Visibility.Collapsed,
|
||
Background = new LinearGradientBrush(
|
||
Color.FromRgb(0xF2, 0xF4, 0xF7), Color.FromRgb(0xD8, 0xDE, 0xE6), 90.0),
|
||
ShowCoordinateSystem = false,
|
||
ShowViewCube = false
|
||
};
|
||
_helixView.Children.Add(new HelixToolkit.Wpf.DefaultLights());
|
||
view3d.Children.Add(_helixView);
|
||
// 窗口内嵌 3D (方案 B, WebView2): 真实 STEP 直接渲染, 不弹外部浏览器
|
||
_webView2 = new Microsoft.Web.WebView2.Wpf.WebView2 { Visibility = Visibility.Collapsed };
|
||
view3d.Children.Add(_webView2);
|
||
// 真实数模回退面板: 桌面内置解析器只支持简化占位几何, 真实 B-rep 走本机浏览器+新迪查看器
|
||
_fallbackPanel = new StackPanel
|
||
{
|
||
Visibility = Visibility.Collapsed,
|
||
HorizontalAlignment = HorizontalAlignment.Center,
|
||
VerticalAlignment = VerticalAlignment.Center,
|
||
MaxWidth = 520
|
||
};
|
||
_fallbackPanel.Children.Add(new TextBlock
|
||
{
|
||
Text = "该型号为真实 CAD 数模,已自动在浏览器中打开 3D 预览(本机运行,完全离线)。\n若浏览器窗口被关闭,可点下方按钮重新打开。",
|
||
TextWrapping = TextWrapping.Wrap,
|
||
TextAlignment = TextAlignment.Center,
|
||
Margin = new Thickness(20, 0, 20, 14)
|
||
});
|
||
_btnBrowser3D = new Button { Width = 240, Height = 42, FontSize = 15 };
|
||
_btnBrowser3D.Click += (s, e) => OpenBrowser3D();
|
||
_fallbackPanel.Children.Add(_btnBrowser3D);
|
||
view3d.Children.Add(_fallbackPanel);
|
||
// 非标档位提示条 (参数化引擎: 3D 展示最近标准档位近似几何时显示; 与网页版一致) — 最后添加保证置顶
|
||
_viewerHint = new Border
|
||
{
|
||
Background = new SolidColorBrush(Color.FromArgb(0xE6, 0xFF, 0xF7, 0xD6)),
|
||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD9, 0xB6, 0x3C)),
|
||
BorderThickness = new Thickness(1),
|
||
Padding = new Thickness(8, 4, 8, 4),
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Top,
|
||
Margin = new Thickness(6),
|
||
Visibility = Visibility.Collapsed
|
||
};
|
||
_viewerHint.Child = new TextBlock { TextWrapping = TextWrapping.Wrap, FontSize = 12 };
|
||
view3d.Children.Add(_viewerHint);
|
||
Grid.SetColumn(view3d, 0);
|
||
grid3d.Children.Add(view3d);
|
||
grid3d.Children.Add(MakeSplitter(1));
|
||
_dimPanel = new StackPanel();
|
||
_dimPanel.Children.Add(_dimImage = new Image { Stretch = Stretch.Uniform, Margin = new Thickness(8), HorizontalAlignment = HorizontalAlignment.Left });
|
||
_manualPanel = new StackPanel();
|
||
_dimPanel.Children.Add(_manualPanel);
|
||
var dimScroll = new ScrollViewer { Content = _dimPanel, VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
|
||
Grid.SetColumn(dimScroll, 2);
|
||
grid3d.Children.Add(dimScroll);
|
||
tab3d.Content = grid3d;
|
||
var tabGrid = new TabItem(); tabGrid.Content = BuildGridTab();
|
||
var tabSearch = new TabItem(); tabSearch.Content = BuildSearchTab();
|
||
var tabCross = new TabItem(); tabCross.Content = BuildCrossRefTab();
|
||
_tabs.Items.Add(tab3d); _tabs.Items.Add(tabGrid); _tabs.Items.Add(tabSearch); _tabs.Items.Add(tabCross);
|
||
Grid.SetColumn(_tabs, 4);
|
||
center.Children.Add(_tabs);
|
||
|
||
root.Children.Add(center);
|
||
Content = root;
|
||
|
||
Loaded += (s, e) => InitWebView2Async(); // WebView2 初始化须在窗口加载后
|
||
Closed += (s, e) => { if (_webView2 != null) _webView2.Dispose(); };
|
||
}
|
||
|
||
// WebView2 运行时初始化 (窗口内嵌 3D 方案 B): 失败则静默降级到浏览器预览
|
||
async void InitWebView2Async()
|
||
{
|
||
try
|
||
{
|
||
var userData = System.IO.Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"OnebotCatalog", "WebView2");
|
||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(null, userData);
|
||
await _webView2.EnsureCoreWebView2Async(env);
|
||
_webView2Ready = true;
|
||
}
|
||
catch
|
||
{
|
||
_webView2Ready = false;
|
||
_status.Text = _lang.T(
|
||
"未检测到 WebView2 运行时, 真实数模用浏览器预览 (Win11 自带; Win10 装 Edge 即可)",
|
||
"WebView2 runtime not found - real models open in browser (built into Win11; install Edge on Win10)");
|
||
}
|
||
}
|
||
|
||
// 列分隔条 (鼠标拖拽调宽)
|
||
GridSplitter MakeSplitter(int col)
|
||
{
|
||
var sp = new GridSplitter { Width = 6, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch };
|
||
Grid.SetColumn(sp, col);
|
||
return sp;
|
||
}
|
||
|
||
static T FindAncestor<T>(DependencyObject d) where T : DependencyObject
|
||
{
|
||
while (d != null)
|
||
{
|
||
if (d is T) return (T)d;
|
||
d = VisualTreeHelper.GetParent(d);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ---- 参数表 Tab (范围筛选 + 批量下载) ----
|
||
DockPanel BuildGridTab()
|
||
{
|
||
var panel = new DockPanel();
|
||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(4) };
|
||
_rangePanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||
top.Children.Add(_rangePanel);
|
||
_btnRangeApply = new Button { Width = 60, Margin = new Thickness(6, 0, 4, 0) };
|
||
_btnRangeApply.Click += (s, e) => ApplyRange();
|
||
_btnRangeReset = new Button { Width = 60, Margin = new Thickness(0, 0, 10, 0) };
|
||
_btnRangeReset.Click += (s, e) => ResetRange();
|
||
_btnExport = new Button { Width = 110 };
|
||
_btnExport.Click += (s, e) => ExportSelected();
|
||
top.Children.Add(_btnRangeApply); top.Children.Add(_btnRangeReset); top.Children.Add(_btnExport);
|
||
DockPanel.SetDock(top, Dock.Top);
|
||
panel.Children.Add(top);
|
||
|
||
_grid = new DataGrid { AutoGenerateColumns = false, IsReadOnly = true, SelectionMode = DataGridSelectionMode.Extended, SelectionUnit = DataGridSelectionUnit.FullRow, Margin = new Thickness(4) };
|
||
// 双击行 → 参数带入选型 (与网页版一致; 单击/框选仍用于批量下载)
|
||
_grid.MouseDoubleClick += (s, e) =>
|
||
{
|
||
var dr = _grid.SelectedItem as DataRowView;
|
||
if (dr != null) NavigateToModel(dr["modelCode"].ToString());
|
||
};
|
||
// 右键列头 → 按列筛选 (2026-08-24, 与网页版列筛选一致)
|
||
_grid.PreviewMouseRightButtonUp += (s, e) =>
|
||
{
|
||
var header = FindAncestor<DataGridColumnHeader>(e.OriginalSource as DependencyObject);
|
||
if (header == null || header.Column == null) return;
|
||
var col = header.Column as DataGridTextColumn;
|
||
if (col == null || !(col.Binding is System.Windows.Data.Binding)) return;
|
||
var path = ((System.Windows.Data.Binding)col.Binding).Path.Path;
|
||
if (path == "modelCode") return;
|
||
var menu = new ContextMenu();
|
||
var all = new MenuItem { Header = _lang.T("全部 (清除本列筛选)", "All (clear filter)") };
|
||
all.Click += (s2, e2) => { _colFilter.Remove(path); BuildGrid(); };
|
||
menu.Items.Add(all);
|
||
var vals = new List<string>();
|
||
foreach (DataRowView r in _grid.Items) { var sv = Convert.ToString(r[path]); if (!vals.Contains(sv)) vals.Add(sv); }
|
||
foreach (var sv in vals)
|
||
{
|
||
var mi = new MenuItem
|
||
{
|
||
Header = sv.Length == 0 ? _lang.T("(空值)", "(empty)") : sv,
|
||
IsChecked = _colFilter.ContainsKey(path) && _colFilter[path] == sv
|
||
};
|
||
mi.Click += (s2, e2) => { _colFilter[path] = sv; BuildGrid(); };
|
||
menu.Items.Add(mi);
|
||
}
|
||
menu.PlacementTarget = header;
|
||
menu.IsOpen = true;
|
||
e.Handled = true;
|
||
};
|
||
var gridHint = new TextBlock
|
||
{
|
||
Text = "双击行 → 参数带入选型 | 右键列头 → 按列筛选 | 单击/框选多行用于批量下载",
|
||
FontSize = 12,
|
||
Foreground = new SolidColorBrush(Color.FromRgb(0x88, 0x88, 0x88)),
|
||
Margin = new Thickness(6, 2, 0, 4)
|
||
};
|
||
DockPanel.SetDock(gridHint, Dock.Bottom);
|
||
panel.Children.Add(gridHint);
|
||
panel.Children.Add(_grid);
|
||
return panel;
|
||
}
|
||
|
||
// ---- 搜索 Tab ----
|
||
DockPanel BuildSearchTab()
|
||
{
|
||
var panel = new DockPanel { Margin = new Thickness(4) };
|
||
var hint = new TextBlock
|
||
{
|
||
Text = "双击结果定位到该型号。支持: 型号编码 / 中文名 / 英文名 / 关键词(拼音) / 竞品型号(平替, 支持模糊)。",
|
||
TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 4)
|
||
};
|
||
DockPanel.SetDock(hint, Dock.Top);
|
||
panel.Children.Add(hint);
|
||
_searchGrid = new DataGrid { AutoGenerateColumns = false, IsReadOnly = true, SelectionMode = DataGridSelectionMode.Single };
|
||
_searchGrid.Columns.Add(new DataGridTextColumn { Header = "型号 Model", Binding = new System.Windows.Data.Binding("ModelCode"), Width = 180 });
|
||
_searchGrid.Columns.Add(new DataGridTextColumn { Header = "系列 Series", Binding = new System.Windows.Data.Binding("SeriesName") });
|
||
_searchGrid.Columns.Add(new DataGridTextColumn { Header = "STEP", Binding = new System.Windows.Data.Binding("Step") });
|
||
_searchGrid.MouseDoubleClick += (s, e) => JumpToSelected();
|
||
panel.Children.Add(_searchGrid);
|
||
return panel;
|
||
}
|
||
|
||
// ---- 数据包加载 ----
|
||
|
||
void OpenPackage()
|
||
{
|
||
var dlg = new OpenFileDialog { Filter = "目录数据包 (*.opc)|*.opc", Title = "打开 .opc 数据包" };
|
||
if (dlg.ShowDialog() == true)
|
||
ReplacePackage(dlg.FileName);
|
||
}
|
||
|
||
public bool ReplacePackage(string path)
|
||
{
|
||
try
|
||
{
|
||
var np = OpcPackage.Open(path);
|
||
if (_pkg != null) _pkg.Dispose();
|
||
_pkg = np;
|
||
LoadPackageIntoUi();
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("数据包打开失败: " + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
void LoadPackageIntoUi()
|
||
{
|
||
_search = new SearchEngine(_pkg.Catalog);
|
||
_tree.Items.Clear();
|
||
foreach (var cat in _pkg.Catalog.categories)
|
||
{
|
||
var catNode = new TreeViewItem { Header = cat.nameZh, Tag = cat };
|
||
foreach (var sc in cat.series)
|
||
{
|
||
var s = _pkg.Catalog.series.FirstOrDefault(x => x.code == sc);
|
||
if (s == null) continue;
|
||
var node = new TreeViewItem { Header = s.nameZh, Tag = s };
|
||
catNode.Items.Add(node);
|
||
}
|
||
catNode.IsExpanded = true;
|
||
_tree.Items.Add(catNode);
|
||
}
|
||
_tree.SelectedItemChanged += TreeSelected;
|
||
_pkgInfo.Text = string.Format("{0} v{1}", _pkg.Catalog.catalogName, _pkg.Catalog.catalogVersion);
|
||
RefreshCrossRef();
|
||
_status.Text = _lang.T("请选择产品系列", "Select a series");
|
||
_series = null; _cfg = null;
|
||
_paramPanel.Children.Clear();
|
||
_boxes.Clear(); _sel.Clear();
|
||
_tiles.Clear(); _tilePanels.Clear();
|
||
if (_skuBar != null) _skuBar.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
void TreeSelected(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||
{
|
||
var node = _tree.SelectedItem as TreeViewItem;
|
||
if (node == null || !(node.Tag is Series)) return;
|
||
LoadSeries((Series)node.Tag);
|
||
}
|
||
|
||
void LoadSeries(Series s)
|
||
{
|
||
_series = s;
|
||
// 下载 PDF 按钮: 系列有数据表附件才显示 (与网页一致)
|
||
_btnDownloadPdf.Visibility = (s.attachments ?? new List<Attachment>()).Any(a => a.kind == "datasheet")
|
||
? Visibility.Visible : Visibility.Collapsed;
|
||
_cfg = new Configurator(s);
|
||
_sel.Clear();
|
||
_boxes.Clear();
|
||
_tiles.Clear();
|
||
_tilePanels.Clear();
|
||
_paramPanel.Children.Clear();
|
||
_ranges.Clear(); _rangeBoxes.Clear(); _rangePanel.Children.Clear();
|
||
if (_skuBar != null) _skuBar.Visibility = Visibility.Visible;
|
||
|
||
foreach (var p in s.parameters)
|
||
{
|
||
var block = new StackPanel { Margin = new Thickness(0, 0, 0, 10) };
|
||
// 维度标题: 小号大写 + 已选值回显
|
||
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")
|
||
{
|
||
// 连续域无法枚举成选项块 → 保留可编辑下拉 (标准档位 + 自由输入)
|
||
var cb = new ComboBox { Width = 150, Tag = p, IsEditable = true, IsTextSearchEnabled = false, HorizontalAlignment = HorizontalAlignment.Left };
|
||
cb.KeyDown += (s2, e2) => { if (e2.Key == System.Windows.Input.Key.Enter) TrySetRangeText(cb); };
|
||
cb.LostKeyboardFocus += (s2, e2) => TrySetRangeText(cb);
|
||
cb.SelectionChanged += ParamChanged;
|
||
block.Children.Add(cb);
|
||
_boxes[p.code] = cb;
|
||
}
|
||
else
|
||
{
|
||
// 枚举维度 → 选项块网格 (有说明显示说明, 无说明只显示代号)
|
||
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 参数 → 参数表范围筛选控件
|
||
if (p.type == "number" || p.type == "range")
|
||
{
|
||
_rangePanel.Children.Add(new TextBlock { Text = ParamLabel(p) + " ", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) });
|
||
var min = new TextBox { Width = 50, Margin = new Thickness(0, 0, 4, 0) };
|
||
var max = new TextBox { Width = 50, Margin = new Thickness(0, 0, 4, 0) };
|
||
_rangePanel.Children.Add(min); _rangePanel.Children.Add(new TextBlock { Text = " ~ ", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) }); _rangePanel.Children.Add(max);
|
||
_rangeBoxes[p.code] = new[] { min, max };
|
||
}
|
||
}
|
||
_viewer.Clear();
|
||
_viewer.Visibility = Visibility.Visible;
|
||
_fallbackPanel.Visibility = Visibility.Collapsed;
|
||
if (_viewerHint != null) _viewerHint.Visibility = Visibility.Collapsed;
|
||
_dimImage.Source = null;
|
||
LoadManualPages(s);
|
||
BuildGrid();
|
||
RefreshOptions();
|
||
}
|
||
|
||
// 手册页 (尺寸图窗口滚动对照, 系列级附件 kind=manual; 标题只显示序号不显示路径)
|
||
void LoadManualPages(Series s)
|
||
{
|
||
_manualPanel.Children.Clear();
|
||
var pages = s.attachments.Where(x => x.kind == "manual").ToList();
|
||
for (int i = 0; i < pages.Count; i++)
|
||
{
|
||
try
|
||
{
|
||
var a = pages[i];
|
||
var img = new Image { Source = BitmapFromBytes(_pkg.ReadAsset(a.path)), Stretch = Stretch.Uniform, Margin = new Thickness(8, 4, 8, 8), HorizontalAlignment = HorizontalAlignment.Left };
|
||
_manualPanel.Children.Add(new TextBlock { Text = _lang.T("手册页", "Manual page") + " " + (i + 1) + "/" + pages.Count, Margin = new Thickness(12, 10, 0, 0), FontWeight = FontWeights.Bold });
|
||
_manualPanel.Children.Add(img);
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
// ---- 选型逻辑 ----
|
||
|
||
string ParamLabel(ParameterDef p)
|
||
{
|
||
var name = _lang.T(p.nameZh, p.nameEn);
|
||
return string.IsNullOrEmpty(p.unit) ? name : name + " (" + p.unit + ")";
|
||
}
|
||
|
||
string DisplayText(ParameterDef p, object v)
|
||
{
|
||
string key = v == null ? "" : Convert.ToString(v, CultureInfo.InvariantCulture);
|
||
if (p.display != null && p.display.ContainsKey(key))
|
||
return p.display[key];
|
||
return key;
|
||
}
|
||
|
||
void ParamChanged(object sender, SelectionChangedEventArgs e)
|
||
{
|
||
if (_refreshing || _cfg == null) return;
|
||
var cb = (ComboBox)sender;
|
||
var p = (ParameterDef)cb.Tag;
|
||
var item = cb.SelectedItem as ComboItem;
|
||
if (item != null) _sel[p.code] = item.Value;
|
||
else _sel.Remove(p.code);
|
||
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 参数自由输入 (参数化引擎): 回车/失焦时校验并写入选择; 非法输入恢复原显示 (与网页版一致)
|
||
void TrySetRangeText(ComboBox cb)
|
||
{
|
||
if (_refreshing || _cfg == null) return;
|
||
var p = (ParameterDef)cb.Tag;
|
||
if (p.type != "range") return;
|
||
var text = (cb.Text ?? "").Trim();
|
||
if (text.Length == 0)
|
||
{
|
||
if (_sel.ContainsKey(p.code)) { _sel.Remove(p.code); RefreshOptions(); }
|
||
return;
|
||
}
|
||
double d;
|
||
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out d))
|
||
{
|
||
object v = d;
|
||
if (ParametricEngine.DomainValid(p, v))
|
||
{
|
||
if (!_sel.ContainsKey(p.code) || !EqStr(_sel[p.code], v)) _sel[p.code] = v;
|
||
RefreshOptions();
|
||
return;
|
||
}
|
||
_status.Text = _lang.T("输入超出范围或与步长不符", "Out of range or not aligned to step");
|
||
}
|
||
RefreshOptions(); // 恢复显示当前已选值 (非法输入不生效)
|
||
}
|
||
|
||
void RefreshOptions()
|
||
{
|
||
if (_cfg == null) return;
|
||
_refreshing = true;
|
||
try
|
||
{
|
||
var allowed = _cfg.AllowedOptions(_sel);
|
||
// 下拉 (range 参数)
|
||
foreach (var kv in _boxes)
|
||
{
|
||
var cb = kv.Value;
|
||
var p = (ParameterDef)cb.Tag;
|
||
List<object> list;
|
||
if (!allowed.TryGetValue(p.code, out list)) list = new List<object>();
|
||
if (_sel.ContainsKey(p.code) && !list.Any(x => EqStr(x, _sel[p.code])))
|
||
_sel.Remove(p.code);
|
||
// range 参数: 标准档位选项标注 "标准" (自由输入值不标)
|
||
var gridVals = p.gridValues ?? new List<object>();
|
||
cb.ItemsSource = list.Select(v => new ComboItem(v,
|
||
p.type == "range" && gridVals.Any(g => EqStr(g, v))
|
||
? DisplayText(p, v) + " (" + _lang.T("标准", "Std") + ")"
|
||
: DisplayText(p, v))).ToList();
|
||
if (_sel.ContainsKey(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();
|
||
}
|
||
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)
|
||
{
|
||
return string.Equals(
|
||
a == null ? "" : Convert.ToString(a, CultureInfo.InvariantCulture),
|
||
b == null ? "" : Convert.ToString(b, CultureInfo.InvariantCulture),
|
||
StringComparison.OrdinalIgnoreCase);
|
||
}
|
||
|
||
void TryUpdateSelection()
|
||
{
|
||
var v = _cfg == null ? null : _cfg.Resolve(_sel);
|
||
ParamResult pres = null;
|
||
if (v == null && _cfg != null && _cfg.IsParametric &&
|
||
_sel.Count == (_series.parameters ?? new List<ParameterDef>()).Count)
|
||
{
|
||
pres = ParametricEngine.Generate(_series, _cfg, _sel);
|
||
if (pres != null && pres.Error != null) pres = null; // 非法组合按"无匹配"处理
|
||
}
|
||
_curVariant = v;
|
||
_curParam = pres;
|
||
_curModelCode = v != null ? v.modelCode : (pres != null ? pres.ModelCode : null);
|
||
|
||
// 下载按钮 (与网页一致): 标准 → 可用; 非标 → 生成服务可用则【在线生成数模】, 否则置灰+提示; 未选全 → 隐藏
|
||
if (v != null)
|
||
{
|
||
_btnDownload.Visibility = Visibility.Visible;
|
||
_btnDownload.IsEnabled = true;
|
||
_btnDownload.ToolTip = null;
|
||
_btnDownload.Content = _lang.T("下载 STEP", "Download STEP");
|
||
if (_btnGenFmt != null) _btnGenFmt.Visibility = Visibility.Collapsed; // 标准档位无多格式
|
||
}
|
||
else if (pres != null)
|
||
{
|
||
_btnDownload.Visibility = Visibility.Visible;
|
||
if (!string.IsNullOrEmpty(_genApi))
|
||
{
|
||
_btnDownload.IsEnabled = true;
|
||
_btnDownload.ToolTip = null;
|
||
_btnDownload.Content = _lang.T("在线生成数模", "Generate 3D model online");
|
||
if (_btnGenFmt != null) _btnGenFmt.Visibility = Visibility.Visible;
|
||
}
|
||
else
|
||
{
|
||
_btnDownload.IsEnabled = false;
|
||
_btnDownload.ToolTip = _lang.T("非标档位暂不支持数模下载,请联系销售或选择标准档位",
|
||
"STEP download unavailable for non-standard config. Contact sales or choose a standard config.");
|
||
_btnDownload.Content = _lang.T("下载 STEP", "Download STEP");
|
||
if (_btnGenFmt != null) _btnGenFmt.Visibility = Visibility.Collapsed;
|
||
}
|
||
}
|
||
else { _btnDownload.Visibility = Visibility.Collapsed; if (_btnGenFmt != null) _btnGenFmt.Visibility = Visibility.Collapsed; }
|
||
_btnAddCart.Visibility = (v != null || pres != null) ? Visibility.Visible : Visibility.Collapsed;
|
||
RefreshCrossRef(); // 平替对照只显示当前型号的平替数据
|
||
|
||
if (v == null && pres == null)
|
||
{
|
||
var missing = _series.parameters.Where(p => !_sel.ContainsKey(p.code)).Select(p => ParamLabel(p)).ToList();
|
||
_modelTitle.Text = _lang.T("请选择完整参数", "Complete the selection");
|
||
if (missing.Count > 0)
|
||
_status.Text = _lang.T(
|
||
"已选 " + _sel.Count + "/" + _series.parameters.Count + " 项, 还缺: " + string.Join("、", missing),
|
||
_sel.Count + "/" + _series.parameters.Count + " selected, missing: " + string.Join(", ", missing));
|
||
else
|
||
_status.Text = _lang.T("该组合无对应型号", "No matching model");
|
||
return;
|
||
}
|
||
|
||
// 非标档位 (参数化引擎): 标题带徽标, 3D 展示最近标准档位近似几何 + 提示条 (下载已置灰)
|
||
if (pres != null)
|
||
{
|
||
_modelTitle.Text = _lang.T("已选型号: ", "Model: ") + pres.ModelCode +
|
||
_lang.T(" [非标档位(无标准数模)]", " [Non-standard (no standard model)]");
|
||
var near = ParametricEngine.NearestStandard(_series, _cfg, pres.Params);
|
||
if (near != null)
|
||
{
|
||
try { TryLegacyRender(near); }
|
||
catch (Exception ex) { _status.Text = _lang.T("3D 加载失败: ", "3D load failed: ") + ex.Message; }
|
||
}
|
||
((TextBlock)_viewerHint.Child).Text = _lang.T(
|
||
"当前为非标档位,展示最近标准档位 " + (near == null ? "?" : near.modelCode) + " 的近似几何",
|
||
"Non-standard config — showing nearest standard model " + (near == null ? "?" : near.modelCode) + " as approximation.");
|
||
_viewerHint.Visibility = Visibility.Visible;
|
||
_status.Text = _lang.T("非标档位: 无对应标准数模, 3D 展示最近标准档位近似几何; STEP 下载需联系销售或选择标准档位",
|
||
"Non-standard config: nearest standard model shown; contact sales or choose a standard config for STEP");
|
||
return;
|
||
}
|
||
|
||
_modelTitle.Text = _lang.T("已选型号: ", "Model: ") + v.modelCode;
|
||
_status.Text = _lang.T("选型完成,可下载 STEP 或查看数据表", "Ready - download STEP or view datasheet");
|
||
|
||
try
|
||
{
|
||
// 3D 显示链 (2026-08-24, 与网页版一致): 占位→自绘; 真实数模→WebView2 窗口内; 无 WebView2→网格 Helix 降级; 都无→浏览器回退
|
||
TryLegacyRender(v);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_status.Text = _lang.T("3D 加载失败: ", "3D load failed: ") + ex.Message;
|
||
}
|
||
|
||
var dim = _series.attachments.FirstOrDefault(a => a.kind == "dimDrawing" && a.model == v.modelCode)
|
||
?? _series.attachments.FirstOrDefault(a => a.kind == "dimDrawing");
|
||
if (dim != null)
|
||
{
|
||
try { _dimImage.Source = BitmapFromBytes(_pkg.ReadAsset(dim.path)); }
|
||
catch { _dimImage.Source = null; }
|
||
}
|
||
}
|
||
|
||
BitmapImage BitmapFromBytes(byte[] bytes)
|
||
{
|
||
var bmp = new BitmapImage();
|
||
bmp.BeginInit();
|
||
bmp.StreamSource = new MemoryStream(bytes);
|
||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||
bmp.EndInit();
|
||
return bmp;
|
||
}
|
||
|
||
// 网格侧车文件: <数据包目录>\mesh\<step 同名>.stl/.obj (NX journal 同步导出的预览网格)
|
||
string FindMeshSidecar(string stepPath)
|
||
{
|
||
if (string.IsNullOrEmpty(stepPath)) return null;
|
||
var baseName = System.IO.Path.GetFileNameWithoutExtension(stepPath.Replace('\\', '/'));
|
||
var dir = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(_pkg.Path) ?? ".", "mesh");
|
||
foreach (var ext in new[] { ".stl", ".obj" })
|
||
{
|
||
var f = System.IO.Path.Combine(dir, baseName + ext);
|
||
if (System.IO.File.Exists(f)) return f;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Helix 视口加载网格 (STL/OBJ), 加载后自适应视角
|
||
void LoadHelixMesh(string path)
|
||
{
|
||
_helixView.Children.Clear();
|
||
_helixView.Children.Add(new HelixToolkit.Wpf.DefaultLights());
|
||
var importer = new HelixToolkit.Wpf.ModelImporter();
|
||
var model = importer.Load(path);
|
||
_helixView.Children.Add(new ModelVisual3D { Content = model });
|
||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background,
|
||
new Action(() => { if (_helixView.IsVisible) _helixView.ZoomExtents(); }));
|
||
}
|
||
|
||
// 3D 显示链 (2026-08-24 定稿): 占位→自绘; 真实 B-rep→WebView2 精确视图直显; 无 WebView2→网格 Helix 降级; 都无→浏览器回退
|
||
void TryLegacyRender(Variant v)
|
||
{
|
||
if (_viewerHint != null) _viewerHint.Visibility = Visibility.Collapsed; // 标准档位无提示条
|
||
try
|
||
{
|
||
var stepText = _pkg.ReadAssetText(v.step);
|
||
var mesh = StepMesh.Parse(stepText);
|
||
if (mesh != null && mesh.Count >= 3)
|
||
{
|
||
_viewer.Load(mesh);
|
||
_viewer.Visibility = Visibility.Visible;
|
||
_helixView.Visibility = Visibility.Collapsed;
|
||
_webView2.Visibility = Visibility.Collapsed;
|
||
_fallbackPanel.Visibility = Visibility.Collapsed;
|
||
return;
|
||
}
|
||
}
|
||
catch { }
|
||
if (TryWebView2(v)) return;
|
||
// WebView2 不可用: 网格 Helix 降级 (老 Win10 无运行时环境)
|
||
var meshPath = FindMeshSidecar(v.step);
|
||
if (meshPath != null)
|
||
{
|
||
try
|
||
{
|
||
LoadHelixMesh(meshPath);
|
||
_helixView.Visibility = Visibility.Visible;
|
||
_viewer.Visibility = Visibility.Collapsed;
|
||
_webView2.Visibility = Visibility.Collapsed;
|
||
_fallbackPanel.Visibility = Visibility.Collapsed;
|
||
_status.Text = _lang.T("3D 已在窗口内显示 (STL 网格降级预览)", "3D shown in-window (STL mesh fallback)");
|
||
return;
|
||
}
|
||
catch { }
|
||
}
|
||
FallbackToBrowser(v);
|
||
}
|
||
|
||
// 方案 B: 窗口内嵌 WebView2 显示真实 STEP (本机微型服务器 URL, 完全离线); 不可用返回 false
|
||
bool TryWebView2(Variant v)
|
||
{
|
||
if (!_webView2Ready) return false;
|
||
try
|
||
{
|
||
_curStepName = System.IO.Path.GetFileName((v.step ?? "").Replace('\\', '/'));
|
||
_curStepBytes = _pkg.ReadAsset(v.step);
|
||
var url = GetPreviewUrl();
|
||
if (url == null) return false;
|
||
_webView2.Source = new Uri(url);
|
||
_webView2.Visibility = Visibility.Visible;
|
||
_viewer.Visibility = Visibility.Collapsed;
|
||
_helixView.Visibility = Visibility.Collapsed;
|
||
_fallbackPanel.Visibility = Visibility.Collapsed;
|
||
_status.Text = _lang.T("真实数模已在窗口内显示 (内嵌 3D 查看器, 完全离线)", "Real model shown in-window (embedded 3D viewer, fully offline)");
|
||
return true;
|
||
}
|
||
catch { return false; }
|
||
}
|
||
|
||
// 真实数模且无网格、WebView2 不可用 → 本机浏览器 + 新迪查看器 (最后回退)
|
||
void FallbackToBrowser(Variant v)
|
||
{
|
||
_helixView.Visibility = Visibility.Collapsed;
|
||
_webView2.Visibility = Visibility.Collapsed;
|
||
_fallbackPanel.Visibility = Visibility.Visible;
|
||
_curStepName = System.IO.Path.GetFileName((v.step ?? "").Replace('\\', '/'));
|
||
_curStepBytes = _pkg.ReadAsset(v.step);
|
||
OpenBrowser3D(true);
|
||
}
|
||
|
||
// 生成 3D 预览 URL (本机微型服务器 + 新迪查看器; WebView2 窗口内与浏览器弹窗共用); 不可用返回 null
|
||
string GetPreviewUrl()
|
||
{
|
||
if (_curStepBytes == null) return null;
|
||
if (_previewSrv == null)
|
||
{
|
||
// 开发版布局 bin\..\viewer; 客户版布局 exe 旁 viewer\
|
||
var viewerRoot = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "viewer");
|
||
if (!Directory.Exists(viewerRoot))
|
||
viewerRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "viewer"));
|
||
if (!Directory.Exists(viewerRoot)) return null;
|
||
_previewSrv = new LocalViewerServer(viewerRoot);
|
||
}
|
||
return _previewSrv.PublishStep(_curStepName ?? "model.step", _curStepBytes);
|
||
}
|
||
|
||
// 真实数模 → 本机浏览器 + 新迪查看器 (完全离线, 零外部依赖)
|
||
void OpenBrowser3D() { OpenBrowser3D(false); }
|
||
void OpenBrowser3D(bool silent)
|
||
{
|
||
try
|
||
{
|
||
var url = GetPreviewUrl();
|
||
if (url == null)
|
||
{
|
||
if (silent)
|
||
{
|
||
_status.Text = _lang.T("未找到 3D 查看器组件 (viewer 目录缺失), 可点下方按钮重试", "3D viewer component not found (missing viewer folder)");
|
||
return;
|
||
}
|
||
MessageBox.Show(_lang.T("未找到 3D 查看器组件 (viewer 目录缺失)", "3D viewer component not found (missing viewer folder)"),
|
||
Title, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||
return;
|
||
}
|
||
Process.Start(url);
|
||
_status.Text = _lang.T("已自动在浏览器打开真实数模 3D 预览 (本机服务, 完全离线)", "Real model 3D preview opened in browser (local, fully offline)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (silent)
|
||
{
|
||
_status.Text = _lang.T("自动打开 3D 预览失败, 请点按钮重试: ", "Auto-open 3D preview failed, retry with the button: ") + ex.Message;
|
||
return;
|
||
}
|
||
MessageBox.Show(_lang.T("打开 3D 预览失败: ", "Failed to open 3D preview: ") + ex.Message,
|
||
Title, MessageBoxButton.OK, MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
// ---- 参数表 (含范围过滤) ----
|
||
|
||
bool InRange(Variant v)
|
||
{
|
||
foreach (var r in _ranges)
|
||
{
|
||
var p = _series.parameters.FirstOrDefault(x => x.code == r.Key);
|
||
if (p == null || p.type != "number") continue;
|
||
object val;
|
||
if (!v.@params.TryGetValue(r.Key, out val)) continue;
|
||
double d = Convert.ToDouble(val, CultureInfo.InvariantCulture);
|
||
if (r.Value[0].HasValue && d < r.Value[0].Value) return false;
|
||
if (r.Value[1].HasValue && d > r.Value[1].Value) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void BuildGrid()
|
||
{
|
||
var dt = new DataTable();
|
||
dt.Columns.Add("modelCode");
|
||
foreach (var p in _series.parameters)
|
||
dt.Columns.Add(p.code);
|
||
foreach (var v in _cfg.ValidVariants().Where(InRange))
|
||
{
|
||
var row = dt.NewRow();
|
||
row["modelCode"] = v.modelCode;
|
||
foreach (var p in _series.parameters)
|
||
{
|
||
object val;
|
||
row[p.code] = v.@params.TryGetValue(p.code, out val) ? Convert.ToString(val, CultureInfo.InvariantCulture) : "";
|
||
}
|
||
dt.Rows.Add(row);
|
||
}
|
||
_grid.Columns.Clear();
|
||
_grid.Columns.Add(new DataGridTextColumn { Header = _lang.T("型号 Model", "Model"), Binding = new System.Windows.Data.Binding("modelCode") });
|
||
foreach (var p in _series.parameters)
|
||
{
|
||
_grid.Columns.Add(new DataGridTextColumn
|
||
{
|
||
Header = _lang.T(p.nameZh, p.nameEn) + (string.IsNullOrEmpty(p.unit) ? "" : " (" + p.unit + ")"),
|
||
Binding = new System.Windows.Data.Binding(p.code)
|
||
});
|
||
}
|
||
_gridDt = dt;
|
||
ApplyGridFilter();
|
||
_grid.ItemsSource = dt.DefaultView;
|
||
}
|
||
|
||
// 按列筛选 (右键列头选值, 多列 AND 叠加; RowFilter 字符串比较)
|
||
void ApplyGridFilter()
|
||
{
|
||
if (_gridDt == null) return;
|
||
var parts = new List<string>();
|
||
foreach (var kv in _colFilter)
|
||
parts.Add("[" + kv.Key + "] = " + (kv.Value.Length == 0 ? "''" : "'" + kv.Value.Replace("'", "''") + "'"));
|
||
_gridDt.DefaultView.RowFilter = parts.Count == 0 ? "" : string.Join(" AND ", parts);
|
||
}
|
||
|
||
void ApplyRange()
|
||
{
|
||
_ranges.Clear();
|
||
foreach (var kv in _rangeBoxes)
|
||
{
|
||
double? min = null, max = null;
|
||
double d;
|
||
if (double.TryParse(kv.Value[0].Text.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out d)) min = d;
|
||
if (double.TryParse(kv.Value[1].Text.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out d)) max = d;
|
||
if (min.HasValue || max.HasValue) _ranges[kv.Key] = new double?[] { min, max };
|
||
}
|
||
BuildGrid();
|
||
_status.Text = _lang.T("范围筛选: " + _ranges.Count + " 项生效", "Range filters: " + _ranges.Count + " active");
|
||
}
|
||
|
||
void ResetRange()
|
||
{
|
||
_ranges.Clear();
|
||
foreach (var kv in _rangeBoxes) { kv.Value[0].Text = ""; kv.Value[1].Text = ""; }
|
||
BuildGrid();
|
||
_status.Text = _lang.T("范围筛选已重置", "Range filters reset");
|
||
}
|
||
|
||
// ---- 购物车 (淘宝式清单批量导出, 2026-08-24; 与网页版语义/文案一致) ----
|
||
|
||
void AddToCart(Variant v)
|
||
{
|
||
if (v == null) return;
|
||
AddToCart(v.modelCode, v.step, BuildParamsSummary(v), true, v.@params);
|
||
}
|
||
|
||
// 非标档位加入清单: 无 STEP, 打包时在线生成或注明 (参数化引擎, 与网页版一致)
|
||
void AddToCart(ParamResult p)
|
||
{
|
||
if (p == null) return;
|
||
AddToCart(p.ModelCode, null,
|
||
_lang.T("【非标档位(打包时无 STEP)】 ", "[Non-standard (no STEP in package)] ") + BuildParamsSummary(p.Params),
|
||
false, p.Params);
|
||
}
|
||
|
||
void AddToCart(string modelCode, string stepPath, string summary, bool isStandard, Dictionary<string, object> pars)
|
||
{
|
||
if (_cart.Any(x => string.Equals(x.ModelCode, modelCode, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
_status.Text = _lang.T("该型号已在清单中", "Already in cart");
|
||
return;
|
||
}
|
||
_cart.Add(new CartItem
|
||
{
|
||
ModelCode = modelCode,
|
||
SeriesCode = _series == null ? "" : _series.code,
|
||
SeriesName = _series == null ? "" : (_lang.Code == "en" ? _series.nameEn : _series.nameZh),
|
||
StepPath = stepPath,
|
||
ParamsSummary = summary,
|
||
IsStandard = isStandard,
|
||
Params = pars
|
||
});
|
||
UpdateCartBadge();
|
||
_status.Text = _lang.T("已加入清单 (共 " + _cart.Count + " 项)", "Added to cart (" + _cart.Count + " items)");
|
||
}
|
||
|
||
// 参数摘要 "键=值; ..." (显示名映射, 空值显示(无))
|
||
string BuildParamsSummary(Variant v)
|
||
{
|
||
return v == null ? "" : BuildParamsSummary(v.@params);
|
||
}
|
||
string BuildParamsSummary(Dictionary<string, object> par)
|
||
{
|
||
if (_series == null) return "";
|
||
var parts = new List<string>();
|
||
foreach (var p in _series.parameters)
|
||
{
|
||
object val;
|
||
if (!par.TryGetValue(p.code, out val)) continue;
|
||
var sv = Convert.ToString(val, CultureInfo.InvariantCulture) ?? "";
|
||
var label = (p.display != null && p.display.ContainsKey(sv)) ? p.display[sv] : sv;
|
||
parts.Add(p.nameZh + "=" + (label.Length == 0 ? "(无)" : label));
|
||
}
|
||
return string.Join("; ", parts);
|
||
}
|
||
|
||
void UpdateCartBadge()
|
||
{
|
||
_btnCart.Content = _lang.T("清单 (" + _cart.Count + ")", "Cart (" + _cart.Count + ")");
|
||
if (_cartWin != null) _cartWin.Refresh();
|
||
}
|
||
|
||
void OpenCart()
|
||
{
|
||
if (_cartWin == null)
|
||
{
|
||
_cartWin = new CartWindow(_cart, UpdateCartBadge, DownloadCartZip);
|
||
_cartWin.Closed += (s, e) => _cartWin = null;
|
||
}
|
||
_cartWin.Show();
|
||
_cartWin.Activate();
|
||
}
|
||
|
||
// 一键打包 (异步, 含非标批量在线生成): 返回 null 成功 / 错误提示字符串
|
||
async System.Threading.Tasks.Task<string> DownloadCartZip()
|
||
{
|
||
if (_cart.Count == 0) return _lang.T("清单为空, 请先加入型号", "Cart is empty");
|
||
var dlg = new SaveFileDialog
|
||
{
|
||
FileName = "ONEBOT_清单_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".zip",
|
||
Filter = "ZIP 压缩包 (*.zip)|*.zip"
|
||
};
|
||
if (dlg.ShowDialog() != true) return null;
|
||
try
|
||
{
|
||
var snap = _cart.ToArray(); // 打包快照: 打包期间的 UI 操作只影响下一次下载
|
||
var missing = new List<string>();
|
||
var csv = new StringBuilder();
|
||
csv.AppendLine("序号,型号编码,系列代码,系列名,数量,参数摘要,STEP文件名");
|
||
|
||
// 非标档位 + 生成服务可用 → 批量在线生成 → 子 zip 并入主 zip (型号子文件夹多格式)
|
||
var nonStd = snap.Where(x => string.IsNullOrEmpty(x.StepPath)).ToList();
|
||
var subZipPath = "";
|
||
if (nonStd.Count > 0 && !string.IsNullOrEmpty(_genApi))
|
||
{
|
||
try { subZipPath = await GenBatch(nonStd); }
|
||
catch (Exception ex) { return _lang.T("非标生成失败: ", "Generation failed: ") + ex.Message; }
|
||
}
|
||
|
||
using (var zip = ZipFile.Open(dlg.FileName, ZipArchiveMode.Create))
|
||
{
|
||
// 先并入在线生成的子 zip (每型号一个子文件夹, 检索命中的已留存型号也在内)
|
||
if (!string.IsNullOrEmpty(subZipPath))
|
||
using (var sub = ZipFile.OpenRead(subZipPath))
|
||
foreach (var e in sub.Entries)
|
||
{
|
||
var entry = zip.CreateEntry(e.FullName);
|
||
using (var es = entry.Open())
|
||
using (var ss = e.Open())
|
||
ss.CopyTo(es);
|
||
}
|
||
|
||
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
for (int i = 0; i < snap.Length; i++)
|
||
{
|
||
var it = snap[i];
|
||
// 非标档位: 生成服务可用时已合并为型号子文件夹; 不可用则显式注明 (不静默跳过)
|
||
if (string.IsNullOrEmpty(it.StepPath))
|
||
{
|
||
csv.AppendLine(CsvEscape(new[] {
|
||
(i + 1).ToString(), it.ModelCode, it.SeriesCode, it.SeriesName,
|
||
it.Qty.ToString(), it.ParamsSummary,
|
||
string.IsNullOrEmpty(_genApi)
|
||
? _lang.T("(非标档位,无 STEP)", "(non-standard, no STEP)")
|
||
: _lang.T("(在线生成, 型号子文件夹)", "(generated online, subfolder)") }));
|
||
continue;
|
||
}
|
||
var ext = Path.GetExtension((it.StepPath ?? "").Replace('\\', '/'));
|
||
if (string.IsNullOrEmpty(ext)) ext = ".step";
|
||
var name = it.ModelCode + ext;
|
||
if (used.Contains(name)) name = it.ModelCode + "_" + it.SeriesCode + ext; // 重名冲突加系列码
|
||
used.Add(name);
|
||
try
|
||
{
|
||
var bytes = _pkg.ReadAsset(it.StepPath);
|
||
var entry = zip.CreateEntry(name);
|
||
using (var es = entry.Open()) es.Write(bytes, 0, bytes.Length);
|
||
}
|
||
catch { missing.Add(it.ModelCode); }
|
||
csv.AppendLine(CsvEscape(new[] {
|
||
(i + 1).ToString(), it.ModelCode, it.SeriesCode, it.SeriesName,
|
||
it.Qty.ToString(), it.ParamsSummary, name }));
|
||
}
|
||
var bom = zip.CreateEntry("BOM.csv");
|
||
using (var es = bom.Open()) { var b = new UTF8Encoding(true).GetBytes(csv.ToString()); es.Write(b, 0, b.Length); }
|
||
var txt = zip.CreateEntry("清单.txt");
|
||
using (var es = txt.Open()) { var b = new UTF8Encoding(true).GetBytes(csv.ToString().Replace(",", " ")); es.Write(b, 0, b.Length); }
|
||
}
|
||
if (!string.IsNullOrEmpty(subZipPath)) try { File.Delete(subZipPath); } catch { }
|
||
if (missing.Count > 0)
|
||
return _lang.T("打包完成, 但以下型号文件缺失: ", "Packed, but missing: ") + string.Join(", ", missing);
|
||
MessageBox.Show(_lang.T("已保存: ", "Saved: ") + dlg.FileName, Title, MessageBoxButton.OK, MessageBoxImage.Information);
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return _lang.T("打包失败: ", "Pack failed: ") + ex.Message;
|
||
}
|
||
}
|
||
|
||
// NX 按需生成 (二期 A 版, 单型号): 提交 → 轮询 → 下载全格式 zip (与网页版同语义/文案)
|
||
async void GenSingle(ParamResult p)
|
||
{
|
||
if (string.IsNullOrEmpty(_genApi) || p == null) return;
|
||
var api = _genApi;
|
||
_btnDownload.IsEnabled = false;
|
||
_btnDownload.Content = _lang.T("正在生成数模…", "Generating…");
|
||
try
|
||
{
|
||
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
string body = ser.Serialize(new { series = _series.code, code = p.ModelCode, @params = p.Params });
|
||
string respText;
|
||
using (var wc = new System.Net.WebClient())
|
||
{
|
||
wc.Encoding = Encoding.UTF8;
|
||
wc.Headers[System.Net.HttpRequestHeader.ContentType] = "application/json";
|
||
respText = await wc.UploadStringTaskAsync(api + "/gen", body);
|
||
}
|
||
var resp = ser.Deserialize<Dictionary<string, object>>(respText);
|
||
if (resp.ContainsKey("immediate"))
|
||
{
|
||
_status.Text = _lang.T("数模已就绪,开始下载", "Ready — downloading");
|
||
}
|
||
else
|
||
{
|
||
var jobId = Convert.ToString(resp["jobId"]);
|
||
bool done = false;
|
||
for (int i = 0; i < 150 && !done; i++)
|
||
{
|
||
await System.Threading.Tasks.Task.Delay(3000);
|
||
string stText;
|
||
using (var wc = new System.Net.WebClient()) { wc.Encoding = Encoding.UTF8; stText = await wc.DownloadStringTaskAsync(api + "/status?job=" + Uri.EscapeDataString(jobId)); }
|
||
var st = ser.Deserialize<Dictionary<string, object>>(stText);
|
||
var state = Convert.ToString(st.ContainsKey("state") ? st["state"] : "");
|
||
if (state == "queued") { _btnDownload.Content = _lang.T("生成队列繁忙,请稍候…", "Queued…"); continue; }
|
||
if (state == "running") { _btnDownload.Content = _lang.T("正在生成数模…", "Generating…"); continue; }
|
||
if (state == "done" && Convert.ToBoolean(st.ContainsKey("ok") && st["ok"] != null ? st["ok"] : false)) { done = true; break; }
|
||
_status.Text = _lang.T("生成失败,请重试或联系销售", "Generation failed — retry or contact sales");
|
||
return;
|
||
}
|
||
if (!done) { _status.Text = _lang.T("生成超时,请重试", "Generation timed out — retry"); return; }
|
||
}
|
||
// 输出已留存: 按所选格式直接下载 (生成服务 /download, 与网页一致)
|
||
var fmtItem = _btnGenFmt != null ? _btnGenFmt.SelectedItem as ComboItem : null;
|
||
var fmtKey = fmtItem != null && fmtItem.Value != null ? fmtItem.Value.ToString() : "zip";
|
||
var extMap = new Dictionary<string, string> { { "zip", ".zip" }, { "step", ".stp" }, { "parasolid", ".x_t" }, { "iges", ".igs" }, { "stl", ".stl" } };
|
||
var url = api + "/download?series=" + Uri.EscapeDataString(_series.code) + "&code=" + Uri.EscapeDataString(p.ModelCode)
|
||
+ (fmtKey != "zip" ? "&fmt=" + Uri.EscapeDataString(fmtKey) : "");
|
||
var dlg = new SaveFileDialog { FileName = p.ModelCode + (extMap.ContainsKey(fmtKey) ? extMap[fmtKey] : ".zip"), Filter = "文件 (*.*)|*.*" };
|
||
if (dlg.ShowDialog() != true) return;
|
||
using (var wc = new System.Net.WebClient()) await wc.DownloadFileTaskAsync(url, dlg.FileName);
|
||
_status.Text = _lang.T("已保存: ", "Saved: ") + dlg.FileName;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_status.Text = _lang.T("生成失败: ", "Generation failed: ") + ex.Message;
|
||
}
|
||
finally
|
||
{
|
||
_btnDownload.IsEnabled = true;
|
||
_btnDownload.Content = _lang.T("在线生成数模", "Generate 3D model online");
|
||
}
|
||
}
|
||
|
||
// 批量在线生成: 提交 → 轮询 → 下载整体 zip 到临时文件 (返回临时文件路径)
|
||
async System.Threading.Tasks.Task<string> GenBatch(List<CartItem> nonStd)
|
||
{
|
||
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
|
||
var api = _genApi;
|
||
string body = ser.Serialize(new
|
||
{
|
||
items = nonStd.Select(x => new { series = x.SeriesCode, code = x.ModelCode, @params = x.Params ?? new Dictionary<string, object>() }).ToList()
|
||
});
|
||
string respText;
|
||
using (var wc = new System.Net.WebClient())
|
||
{
|
||
wc.Encoding = Encoding.UTF8;
|
||
wc.Headers[System.Net.HttpRequestHeader.ContentType] = "application/json";
|
||
respText = await wc.UploadStringTaskAsync(api + "/gen-batch", body);
|
||
}
|
||
var resp = ser.Deserialize<Dictionary<string, object>>(respText);
|
||
if (resp.ContainsKey("error")) throw new Exception(Convert.ToString(resp["error"]));
|
||
var jobId = Convert.ToString(resp["jobId"]);
|
||
var token = Convert.ToString(resp["token"]);
|
||
bool done = false;
|
||
for (int i = 0; i < 200 && !done; i++)
|
||
{
|
||
await System.Threading.Tasks.Task.Delay(3000);
|
||
string stText;
|
||
using (var wc = new System.Net.WebClient()) { wc.Encoding = Encoding.UTF8; stText = await wc.DownloadStringTaskAsync(api + "/status?job=" + Uri.EscapeDataString(jobId)); }
|
||
var st = ser.Deserialize<Dictionary<string, object>>(stText);
|
||
var state = Convert.ToString(st.ContainsKey("state") ? st["state"] : "");
|
||
if (state == "done") { done = true; break; }
|
||
if (state == "failed") throw new Exception(Convert.ToString(st.ContainsKey("error") ? st["error"] : "failed"));
|
||
_status.Text = state == "queued"
|
||
? _lang.T("生成队列繁忙,请稍候…", "Queued…")
|
||
: _lang.T("正在生成数模…", "Generating…");
|
||
}
|
||
if (!done) throw new Exception(_lang.T("生成超时,请重试", "Generation timed out — retry"));
|
||
var tmp = Path.Combine(Path.GetTempPath(), "ounibo_gen_" + jobId + ".zip");
|
||
using (var wc = new System.Net.WebClient()) await wc.DownloadFileTaskAsync(api + "/files?job=" + Uri.EscapeDataString(jobId) + "&token=" + Uri.EscapeDataString(token), tmp);
|
||
return tmp;
|
||
}
|
||
|
||
static string CsvEscape(string[] cells)
|
||
{
|
||
return string.Join(",", cells.Select(c =>
|
||
{
|
||
c = c ?? "";
|
||
if (c.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0)
|
||
c = "\"" + c.Replace("\"", "\"\"") + "\"";
|
||
return c;
|
||
}));
|
||
}
|
||
|
||
// 参数表勾选行 → 批量加入清单 (原"批量导出所选"改造, 统一走清单)
|
||
void ExportSelected()
|
||
{
|
||
var rows = _grid.SelectedItems.OfType<DataRowView>().Select(dr => dr["modelCode"].ToString()).ToList();
|
||
if (rows.Count == 0)
|
||
{
|
||
MessageBox.Show(_lang.T("请先在参数表勾选型号 (Ctrl/Shift 多选)", "Select models in the parameter grid first (Ctrl/Shift)"), Title);
|
||
return;
|
||
}
|
||
int added = 0;
|
||
foreach (var model in rows)
|
||
{
|
||
var v = _cfg.ValidVariants().FirstOrDefault(x => x.modelCode == model);
|
||
if (v == null) continue;
|
||
if (_cart.Any(x => string.Equals(x.ModelCode, v.modelCode, StringComparison.OrdinalIgnoreCase))) continue;
|
||
_cart.Add(new CartItem
|
||
{
|
||
ModelCode = v.modelCode,
|
||
SeriesCode = _series == null ? "" : _series.code,
|
||
SeriesName = _series == null ? "" : (_lang.Code == "en" ? _series.nameEn : _series.nameZh),
|
||
StepPath = v.step,
|
||
ParamsSummary = BuildParamsSummary(v)
|
||
});
|
||
added++;
|
||
}
|
||
UpdateCartBadge();
|
||
_status.Text = _lang.T("已加入清单 " + added + " 项", added + " items added to cart");
|
||
}
|
||
|
||
// ---- 搜索 ----
|
||
|
||
void DoSearch()
|
||
{
|
||
if (_search == null) return;
|
||
var q = _searchBox.Text.Trim();
|
||
if (q.Length == 0) return;
|
||
var items = _search.Query(q, _lang.Code);
|
||
// 平替模糊搜索 (2026-08-24): 竞品型号包含关键词 → 对应我方型号列入结果 (精确命中走引擎 AltCodes 900 分)
|
||
foreach (var cr in _pkg.Catalog.crossRefs ?? new List<CrossRefRow>())
|
||
{
|
||
if (cr.foreignModel == null || cr.foreignModel.IndexOf(q, StringComparison.OrdinalIgnoreCase) < 0) continue;
|
||
if (items.Any(x => string.Equals(x.ModelCode, cr.ourModel, StringComparison.OrdinalIgnoreCase))) continue;
|
||
foreach (var s in _pkg.Catalog.series ?? new List<Series>())
|
||
{
|
||
var vv = (s.variants ?? new List<Variant>()).FirstOrDefault(x => string.Equals(x.modelCode, cr.ourModel, StringComparison.OrdinalIgnoreCase));
|
||
if (vv == null) continue;
|
||
items.Add(new SearchEngine.SearchItem { Series = s, Variant = vv, Note = cr.brand + " " + cr.foreignModel });
|
||
break;
|
||
}
|
||
}
|
||
_searchResult = items;
|
||
var dt = new DataTable();
|
||
dt.Columns.Add("ModelCode");
|
||
dt.Columns.Add("SeriesName");
|
||
dt.Columns.Add("Step");
|
||
foreach (var it in items)
|
||
{
|
||
var row = dt.NewRow();
|
||
row["ModelCode"] = it.ModelCode;
|
||
row["SeriesName"] = _lang.Code == "en" ? it.SeriesNameEn : it.SeriesNameZh;
|
||
row["Step"] = it.Step;
|
||
dt.Rows.Add(row);
|
||
}
|
||
_searchGrid.ItemsSource = dt.DefaultView;
|
||
((TabItem)_tabs.Items[2]).IsSelected = true;
|
||
_status.Text = _lang.T("搜索 \"" + q + "\": " + items.Count + " 条结果", "Search \"" + q + "\": " + items.Count + " result(s)");
|
||
}
|
||
|
||
void JumpToSelected()
|
||
{
|
||
var row = _searchGrid.SelectedItem as DataRowView;
|
||
if (row == null || _searchResult == null) return;
|
||
var idx = _searchGrid.SelectedIndex;
|
||
if (idx < 0 || idx >= _searchResult.Count) return;
|
||
NavigateToModel(_searchResult[idx].ModelCode);
|
||
}
|
||
|
||
// ---- 平替对照 Tab (竞品型号 → 我方型号, 源 catalog\crossref.csv) ----
|
||
DockPanel BuildCrossRefTab()
|
||
{
|
||
var panel = new DockPanel { Margin = new Thickness(6) };
|
||
var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 6) };
|
||
top.Children.Add(new TextBlock { Text = "品牌筛选:", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0) });
|
||
_crossBrand = new ComboBox { Width = 170 };
|
||
_crossBrand.SelectionChanged += (s, e) => RefreshCrossRef();
|
||
top.Children.Add(_crossBrand);
|
||
top.Children.Add(new TextBlock { Text = " 只显示当前型号的平替数据; 双击行跳转 (搜索框输入竞品型号也可直达)", VerticalAlignment = VerticalAlignment.Center, Foreground = System.Windows.Media.Brushes.Gray, Margin = new Thickness(10, 0, 0, 0) });
|
||
DockPanel.SetDock(top, Dock.Top);
|
||
panel.Children.Add(top);
|
||
|
||
_crossGrid = new DataGrid { IsReadOnly = true, AutoGenerateColumns = false };
|
||
_crossGrid.Columns.Add(new DataGridTextColumn { Header = "品牌 Brand", Binding = new System.Windows.Data.Binding("Brand"), Width = 120 });
|
||
_crossGrid.Columns.Add(new DataGridTextColumn { Header = "竞品型号 Foreign Model", Binding = new System.Windows.Data.Binding("Foreign"), Width = 220 });
|
||
_crossGrid.Columns.Add(new DataGridTextColumn { Header = "我方型号 Our Model", Binding = new System.Windows.Data.Binding("Our"), Width = 180 });
|
||
_crossGrid.Columns.Add(new DataGridTextColumn { Header = "备注 Note", Binding = new System.Windows.Data.Binding("Note"), Width = new System.Windows.Controls.DataGridLength(1, System.Windows.Controls.DataGridLengthUnitType.Star) });
|
||
_crossGrid.MouseDoubleClick += (s, e) =>
|
||
{
|
||
var row = _crossGrid.SelectedItem as DataRowView;
|
||
if (row != null) NavigateToModel(Convert.ToString(row["Our"]));
|
||
};
|
||
panel.Children.Add(_crossGrid);
|
||
return panel;
|
||
}
|
||
|
||
void RefreshCrossRef()
|
||
{
|
||
if (_crossGrid == null || _pkg == null) return;
|
||
var brandSel = _crossBrand != null && _crossBrand.SelectedItem != null ? Convert.ToString(_crossBrand.SelectedItem) : null;
|
||
var dt = new DataTable();
|
||
dt.Columns.Add("Brand"); dt.Columns.Add("Foreign"); dt.Columns.Add("Our"); dt.Columns.Add("Note");
|
||
foreach (var cr in _pkg.Catalog.crossRefs ?? new List<CrossRefRow>())
|
||
{
|
||
if (brandSel != null && !string.Equals(cr.brand, brandSel, StringComparison.OrdinalIgnoreCase)) continue;
|
||
// 2026-08-24: 只显示与当前已选型号匹配的平替行 (未选型号则显示全部)
|
||
if (!string.IsNullOrEmpty(_curModelCode) && !string.Equals(cr.ourModel, _curModelCode, StringComparison.OrdinalIgnoreCase)) continue;
|
||
dt.Rows.Add(cr.brand, cr.foreignModel, cr.ourModel, cr.note);
|
||
}
|
||
_crossGrid.ItemsSource = dt.DefaultView;
|
||
if (_crossBrand != null && _crossBrand.Items.Count == 0)
|
||
{
|
||
_crossBrand.Items.Add(_lang.T("全部品牌", "All brands"));
|
||
foreach (var b in (_pkg.Catalog.crossRefs ?? new List<CrossRefRow>()).Select(x => x.brand).Distinct().OrderBy(x => x))
|
||
_crossBrand.Items.Add(b);
|
||
_crossBrand.SelectedIndex = 0;
|
||
}
|
||
}
|
||
|
||
// 按我方型号跳转: 定位系列 → 设参数 → 选型完成 (搜索/平替共用)
|
||
void NavigateToModel(string modelCode)
|
||
{
|
||
foreach (var s in _pkg.Catalog.series ?? new List<Series>())
|
||
{
|
||
var v = (s.variants ?? new List<Variant>()).FirstOrDefault(x => string.Equals(x.modelCode, modelCode, StringComparison.OrdinalIgnoreCase));
|
||
if (v == null) continue;
|
||
foreach (TreeViewItem catNode in _tree.Items)
|
||
foreach (TreeViewItem node in catNode.Items)
|
||
if (node.Tag == s) node.IsSelected = true;
|
||
LoadSeries(s);
|
||
foreach (var kv in v.@params)
|
||
_sel[kv.Key] = kv.Value;
|
||
RefreshOptions();
|
||
((TabItem)_tabs.Items[0]).IsSelected = true;
|
||
return;
|
||
}
|
||
_status.Text = _lang.T("型号未找到: ", "Model not found: ") + modelCode;
|
||
}
|
||
|
||
// ---- 下载与数据表 ----
|
||
|
||
void DownloadStep()
|
||
{
|
||
var v = _cfg == null ? null : _cfg.Resolve(_sel);
|
||
if (v == null)
|
||
{
|
||
MessageBox.Show(_lang.T("请先完成选型", "Complete the selection first"), Title);
|
||
return;
|
||
}
|
||
var dlg = new SaveFileDialog
|
||
{
|
||
FileName = v.modelCode + ".step",
|
||
Filter = "STEP 文件 (*.step)|*.step",
|
||
Title = _lang.T("保存 STEP 文件", "Save STEP file")
|
||
};
|
||
if (dlg.ShowDialog() == true)
|
||
{
|
||
try
|
||
{
|
||
_pkg.ExtractStepTo(v, dlg.FileName);
|
||
MessageBox.Show(_lang.T("已保存: ", "Saved: ") + dlg.FileName, Title, MessageBoxButton.OK, MessageBoxImage.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(_lang.T("保存失败: ", "Save failed: ") + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error);
|
||
}
|
||
}
|
||
}
|
||
|
||
void DownloadDatasheet()
|
||
{
|
||
if (_series == null) return;
|
||
var ds = _series.attachments.FirstOrDefault(a => a.kind == "datasheet" && a.lang == _lang.Code)
|
||
?? _series.attachments.FirstOrDefault(a => a.kind == "datasheet");
|
||
if (ds == null) { MessageBox.Show(_lang.T("该系列无数据表", "No datasheet for this series"), Title); return; }
|
||
var dlg = new SaveFileDialog
|
||
{
|
||
Filter = "PDF (*.pdf)|*.pdf",
|
||
FileName = Path.GetFileName(ds.path.Replace('\\', '/'))
|
||
};
|
||
if (dlg.ShowDialog() != true) return;
|
||
try
|
||
{
|
||
File.WriteAllBytes(dlg.FileName, _pkg.ReadAsset(ds.path));
|
||
MessageBox.Show(_lang.T("已保存: ", "Saved: ") + dlg.FileName, Title, MessageBoxButton.OK, MessageBoxImage.Information);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(_lang.T("保存失败: ", "Save failed: ") + ex.Message, Title, MessageBoxButton.OK, MessageBoxImage.Error);
|
||
}
|
||
}
|
||
|
||
void OpenDatasheet()
|
||
{
|
||
if (_series == null) return;
|
||
var ds = _series.attachments.FirstOrDefault(a => a.kind == "datasheet" && a.lang == _lang.Code)
|
||
?? _series.attachments.FirstOrDefault(a => a.kind == "datasheet");
|
||
if (ds == null) { MessageBox.Show(_lang.T("该系列无数据表", "No datasheet for this series"), Title); return; }
|
||
try
|
||
{
|
||
var tmp = Path.Combine(Path.GetTempPath(), "ounibo", ds.path.Replace('/', '\\'));
|
||
Directory.CreateDirectory(Path.GetDirectoryName(tmp));
|
||
File.WriteAllBytes(tmp, _pkg.ReadAsset(ds.path));
|
||
Process.Start(tmp);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(_lang.T("打开失败: ", "Open failed: ") + ex.Message, Title);
|
||
}
|
||
}
|
||
|
||
// ---- 语言切换 ----
|
||
|
||
void ApplyLang()
|
||
{
|
||
Title = _lang.T("欧霓博气动目录 ONEBOT Catalog", "ONEBOT Pneumatic Catalog");
|
||
_btnOpen.Content = _lang.T("打开数据包", "Open Catalog");
|
||
_btnDownload.Content = _lang.T("下载 STEP", "Download STEP");
|
||
_btnDownloadPdf.Content = _lang.T("下载 PDF", "Download PDF");
|
||
_btnLang.Content = _lang.Code == "zh" ? "EN" : "中";
|
||
#if !CUSTOMER_BUILD
|
||
_btnAdmin.Content = _lang.T("维护入口", "Admin");
|
||
#endif
|
||
if (_btnBrowser3D != null)
|
||
_btnBrowser3D.Content = _lang.T("在浏览器中打开 3D 预览", "Open 3D Preview in Browser");
|
||
_btnSearch.Content = _lang.T("搜索", "Search");
|
||
_btnExport.Content = _lang.T("加入清单", "Add to Cart");
|
||
_btnRangeApply.Content = _lang.T("应用", "Apply");
|
||
_btnRangeReset.Content = _lang.T("重置", "Reset");
|
||
_btnAddCart.Content = _lang.T("加入清单", "Add to Cart");
|
||
_btnCart.Content = _lang.T("清单 (" + _cart.Count + ")", "Cart (" + _cart.Count + ")");
|
||
|
||
if (_tabs != null && _tabs.Items.Count >= 4)
|
||
{
|
||
((TabItem)_tabs.Items[0]).Header = _lang.T("3D / 尺寸图", "3D / Dimension");
|
||
((TabItem)_tabs.Items[1]).Header = _lang.T("参数表", "Parameters");
|
||
((TabItem)_tabs.Items[2]).Header = _lang.T("搜索", "Search");
|
||
((TabItem)_tabs.Items[3]).Header = _lang.T("平替对照", "Cross-Reference");
|
||
}
|
||
|
||
if (_pkg != null)
|
||
{
|
||
foreach (TreeViewItem catNode in _tree.Items)
|
||
{
|
||
var cat = (Category)catNode.Tag;
|
||
catNode.Header = _lang.T(cat.nameZh, cat.nameEn);
|
||
foreach (TreeViewItem sNode in catNode.Items)
|
||
{
|
||
var s = (Series)sNode.Tag;
|
||
sNode.Header = _lang.T(s.nameZh, s.nameEn);
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach (var row in _paramPanel.Children.OfType<StackPanel>())
|
||
{
|
||
var tb = row.Children.OfType<TextBlock>().FirstOrDefault();
|
||
var cb = row.Children.OfType<ComboBox>().FirstOrDefault();
|
||
if (tb != null && cb != null)
|
||
{
|
||
tb.Text = ParamLabel((ParameterDef)cb.Tag);
|
||
if (cb.ItemsSource != null)
|
||
{
|
||
var p = (ParameterDef)cb.Tag;
|
||
var items = cb.Items.OfType<ComboItem>().ToList();
|
||
cb.ItemsSource = items.Select(x => new ComboItem(x.Value, DisplayText(p, x.Value))).ToList();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (_grid != null && _grid.Columns.Count > 0 && _series != null)
|
||
{
|
||
_grid.Columns[0].Header = _lang.T("型号 Model", "Model");
|
||
for (int i = 0; i < _series.parameters.Count && i + 1 < _grid.Columns.Count; i++)
|
||
{
|
||
var p = _series.parameters[i];
|
||
_grid.Columns[i + 1].Header = _lang.T(p.nameZh, p.nameEn) + (string.IsNullOrEmpty(p.unit) ? "" : " (" + p.unit + ")");
|
||
}
|
||
}
|
||
|
||
if (_pkgInfo != null && _pkg != null)
|
||
_pkgInfo.Text = string.Format("{0} v{1}", _lang.T(_pkg.Catalog.catalogName, _pkg.Catalog.catalogNameEn), _pkg.Catalog.catalogVersion);
|
||
|
||
if (_status != null && _cfg != null)
|
||
TryUpdateSelection();
|
||
}
|
||
}
|
||
}
|