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>
    /// 主窗口: 目录树 + 参数选型 + 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>();
        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);
            _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();
        }

        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();
            _paramPanel.Children.Clear();
            _ranges.Clear(); _rangeBoxes.Clear(); _rangePanel.Children.Clear();

            foreach (var p in s.parameters)
            {
                var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 8) };
                row.Children.Add(new TextBlock { Text = ParamLabel(p), Width = 120, VerticalAlignment = VerticalAlignment.Center });
                var cb = new ComboBox { Width = 150, Tag = p };
                if (p.type == "range")
                {
                    // 参数化 range 域: 可编辑下拉 (标准档位快捷选 + 自由输入, 与网页版一致)
                    cb.IsEditable = true;
                    cb.IsTextSearchEnabled = false;
                    cb.KeyDown += (s2, e2) => { if (e2.Key == System.Windows.Input.Key.Enter) TrySetRangeText(cb); };
                    cb.LostKeyboardFocus += (s2, e2) => TrySetRangeText(cb);
                }
                cb.SelectionChanged += ParamChanged;
                row.Children.Add(cb);
                _paramPanel.Children.Add(row);
                _boxes[p.code] = cb;

                // 数值/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();
        }

        // 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);
                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]));
                }
                TryUpdateSelection();
            }
            finally { _refreshing = false; }
        }

        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();
        }
    }
}
