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 { /// 简单双语: zh/en 文案切换。 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; } } /// /// 选项块 (DESTACO 风, 2026-09-15): 替代下拉菜单的单值可选卡片。 /// 三态: 可选 / 已选 / 不可选 (置灰划线但保留占位, 让用户看到选项存在)。 /// 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 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); }; } /// 设置可用性 (被联动规则排除时置灰但不移除)。 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(); } } /// 设置选中态。 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; } } } /// /// 主窗口: 目录树 + 参数选型 + 3D 预览 + 尺寸图/参数表/数据表 + 搜索 + STEP 下载/批量下载。 /// public class MainWindow : Window { Lang _lang = new Lang(); OpcPackage _pkg; Configurator _cfg; SearchEngine _search; Series _series; readonly Dictionary _sel = new Dictionary(); readonly Dictionary _boxes = new Dictionary(); // 选项块 (DESTACO 风, 2026-09-15): 每个参数一组可点选项块, 替代下拉菜单 readonly Dictionary> _tiles = new Dictionary>(); readonly Dictionary _tilePanels = new Dictionary(); TextBlock _skuCode, _skuHint; // 零件号实时条 Border _skuBar; ProgressBar _skuProg; readonly Dictionary _ranges = new Dictionary(); readonly Dictionary _rangeBoxes = new Dictionary(); List _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 _colFilter = new Dictionary(); // 参数表按列筛选 {参数code: 值} List _cart = new List(); // 购物车 (淘宝式清单, 与选型状态解耦) 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(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(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(); 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 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()).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(); foreach (var v in (p.values ?? new List())) { 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(); } /// 选项块点击: 切换选择 (再点一次取消); 与下拉时代语义一致。 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 CandidateCodes() { var res = new List(); if (_series == null) return res; var vs = _series.variants ?? new List(); 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 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); } /// 刷新零件号实时条: 已确定前缀 + 待定位数 + 进度 + 提示。 void RefreshSkuBar() { if (_skuBar == null || _series == null) return; var total = (_series.parameters ?? new List()).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 list; if (!allowed.TryGetValue(p.code, out list)) list = new List(); if (_sel.ContainsKey(p.code) && !list.Any(x => EqStr(x, _sel[p.code]))) _sel.Remove(p.code); // range 参数: 标准档位选项标注 "标准" (自由输入值不标) var gridVals = p.gridValues ?? new List(); 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().FirstOrDefault(x => EqStr(x.Value, _sel[p.code])); } // 选项块 (枚举参数): 不可选的置灰保留, 已选的打勾 foreach (var kv in _tiles) { var code = kv.Key; List list; if (!allowed.TryGetValue(code, out list)) list = new List(); 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; } } /// 按 Tag (=参数 code) 找到维度标题后的回显 TextBlock。 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()).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\.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(); 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 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 par) { if (_series == null) return ""; var parts = new List(); 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 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(); 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(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>(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>(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 { { "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 GenBatch(List 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() }).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>(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>(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().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()) { 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()) { var vv = (s.variants ?? new List()).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()) { 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()).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()) { var v = (s.variants ?? new List()).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()) { var tb = row.Children.OfType().FirstOrDefault(); var cb = row.Children.OfType().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().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(); } } }