/* 欧霓博在线目录 MVP (纯静态, 零依赖)
 * 与离线版 (OnebotCatalog.exe) 消费同一份 catalog.json, 选型规则语义完全一致。
 * 访问方式: 通过 Web 服务器 (tools\serve-web.ps1) 或上传到任意静态托管。
 * 自测: URL 加 ?selftest=1, 页面底部显示断言结果。
 */
(function () {
  'use strict';

  var state = {
    catalog: null,
    lang: 'zh',
    series: null,          // 当前系列
    sel: {},               // 已选参数 {code: value}
    cart: [],              // 购物车 (2026-08-24; 刷新页面才清空, 与选型状态解耦)
    ranges: {},            // 数值参数范围 {code: [min|null, max|null]}
    searchIndex: [],       // [{series, variant, code, text}]
    searchResult: []
  };

  // ---------- 工具 ----------
  function $(id) { return document.getElementById(id); }
  function str(v) { return v === null || v === undefined ? '' : String(v); }
  function num(v) { return Number(str(v).replace(',', '.')); }
  function t(zh, en) { return state.lang === 'en' ? en : zh; }
  function esc(s) {
    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }
  function paramName(p) {
    var n = t(p.nameZh, p.nameEn);
    return p.unit ? n + ' (' + p.unit + ')' : n;
  }
  function displayValue(p, v) {
    var key = str(v);
    if (p.display && p.display[key] !== undefined) return p.display[key];
    return key;
  }

  // ---------- 选型规则引擎 (与 C# Configurator 同语义) ----------
  function condMatches(c, params) {
    if (!c) return true;
    var val = params[c.Param];
    if (val === undefined || val === null) return false;
    switch (c.Op) {
      case 'eq': return str(val) === str(c.Value);
      case 'ne': return str(val) !== str(c.Value);
      case 'in':
        if (!Array.isArray(c.Value)) return false;
        for (var i = 0; i < c.Value.length; i++) if (str(c.Value[i]) === str(val)) return true;
        return false;
      case 'gt': return num(val) > num(c.Value);
      case 'ge': return num(val) >= num(c.Value);
      case 'lt': return num(val) < num(c.Value);
      case 'le': return num(val) <= num(c.Value);
      case 'between':
        return Array.isArray(c.Value) && c.Value.length === 2 &&
          num(val) >= num(c.Value[0]) && num(val) <= num(c.Value[1]);
      default: return false;
    }
  }

  function ruleViolated(v, rules) {
    for (var i = 0; i < rules.length; i++) {
      var r = rules[i];
      if (condMatches(r.If, v.params) && !condMatches(r.Then, v.params)) return true;
    }
    return false;
  }

  // 部分选择下的规则违背判定 (参数化联动): Then 参数未选时视为"尚可满足" (与 C# RuleViolatedPartial 同语义)
  function ruleViolatedPartial(params, rules) {
    for (var i = 0; i < rules.length; i++) {
      var r = rules[i];
      if (condMatches(r.If, params) && (r.Then.Param in params) && !condMatches(r.Then, params)) return true;
    }
    return false;
  }

  function validatePartial(s, sel) {
    for (var i = 0; i < s.parameters.length; i++) {
      var p = s.parameters[i];
      if (!(p.code in sel)) continue;   // 未选参数不校验
      if (!domainValid(p, sel[p.code])) return 'domain';
    }
    if (ruleViolatedPartial(sel, s.rules || [])) return 'rule';
    return null;
  }

  function validVariants(series) {
    var rules = series.rules || [];
    return series.variants.filter(function (v) { return !ruleViolated(v, rules); });
  }

  function matches(v, sel) {
    for (var k in sel) {
      if (!(k in v.params) || str(v.params[k]) !== str(sel[k])) return false;
    }
    return true;
  }

  function allowedOptions(series, sel) {
    var result = {};
    for (var i = 0; i < series.parameters.length; i++) {
      var p = series.parameters[i];
      // 候选值: 枚举/数值参数取定义值; range 参数取 gridValues 快捷值
      var cands = p.type === 'range' ? (p.gridValues || p.values || []) : (p.values || []);
      var selForP = {};
      for (var k in sel) { if (k !== p.code) selForP[k] = sel[k]; }
      var allowed = [];
      // 联动按"域+规则"判合法 (参数化引擎一期): 自由 range 值没有对应网格行,
      // 若按网格行存在性过滤会把其他参数选项瞬间清空 — 与 C# 同语义
      for (var j = 0; j < cands.length; j++) {
        var combo = {};
        for (var k in selForP) combo[k] = selForP[k];
        combo[p.code] = cands[j];
        if (validatePartial(series, combo) === null) allowed.push(cands[j]);
      }
      // range 参数: 保留当前已选的自由输入值 (域内合法) — 防止联动刷新时清掉自定义值
      if (p.type === 'range' && sel[p.code] !== undefined &&
          !allowed.some(function (v) { return str(v) === str(sel[p.code]); }) &&
          domainValid(p, sel[p.code])) allowed.push(sel[p.code]);
      result[p.code] = allowed;
    }
    return result;
  }

  function resolve(series, sel) {
    if (Object.keys(sel).length < series.parameters.length) return null;
    var valid = validVariants(series);
    for (var i = 0; i < valid.length; i++) {
      if (matches(valid[i], sel)) return valid[i];
    }
    return null;
  }

  // ---------- 参数化引擎 (一期 2026-08-25, 与 C# ParametricEngine 同语义; 同一份 parametric-vectors.json 对拍) ----------
  function isParametric(s) { return !!s && s.mode === 'parametric'; }

  function renderCode(template, p) {
    if (!template) return null;
    var out = '';
    var i = 0;
    while (i < template.length) {
      if (template.charAt(i) === '{') {
        var close = template.indexOf('}', i + 1);
        if (close < 0) return null;
        var token = template.substring(i + 1, close);
        var parts = token.split(':');
        var field = parts[0].trim();
        if (!(field in p)) return null;
        var sv = str(p[field]);
        if (parts.length > 1) {
          var padTok = parts[1].trim();
          if (padTok.toLowerCase().indexOf('pad') === 0) padTok = padTok.substring(3);
          var pad = parseInt(padTok, 10);
          if (!(pad >= 1)) return null;
          while (sv.length < pad) sv = '0' + sv;
        }
        out += sv;
        i = close + 1;
      } else { out += template.charAt(i); i++; }
    }
    return out;
  }

  function domainValid(p, v) {
    if (v === undefined || v === null) return false;
    if (p.type === 'range') {
      if (v === '') return false;
      var d = num(v);
      if (isNaN(d)) return false;
      if (p.min !== undefined && p.min !== null && d < p.min) return false;
      if (p.max !== undefined && p.max !== null && d > p.max) return false;
      var st = p.step || 1;
      var base = p.min || 0;
      if (st > 0 && Math.abs(d - base - Math.round((d - base) / st) * st) > 1e-6) return false;
      return true;
    }
    // 枚举: 空串是合法值 (如 磁石=""=无磁石), 不得在此拦截 (与 C# DomainValid 同语义)
    for (var i = 0; i < p.values.length; i++) if (str(p.values[i]) === str(v)) return true;
    return false;
  }

  function validateCombo(s, sel) {
    for (var i = 0; i < s.parameters.length; i++) {
      var p = s.parameters[i];
      if (!(p.code in sel)) return 'param';
      if (!domainValid(p, sel[p.code])) return 'domain';
    }
    var fake = { modelCode: '', params: sel };
    if (ruleViolated(fake, s.rules || [])) return 'rule';
    return null;
  }

  function parametricGenerate(s, sel) {
    var r = { params: sel, error: null, modelCode: null, isStandard: false, variant: null };
    if (Object.keys(sel).length < s.parameters.length) { r.error = 'param'; return r; }
    r.error = validateCombo(s, sel);
    if (r.error) return r;
    r.modelCode = renderCode(s.modelCodeTemplate, sel);
    if (!r.modelCode) { r.error = 'template'; return r; }
    var valid = validVariants(s);
    for (var i = 0; i < valid.length; i++) {
      if (valid[i].modelCode.toLowerCase() === r.modelCode.toLowerCase()) {
        r.isStandard = true; r.variant = valid[i]; break;
      }
    }
    return r;
  }

  function nearestStandard(s, sel) {
    var valid = validVariants(s);
    var best = null, bestDist = Infinity;
    for (var i = 0; i < valid.length; i++) {
      var v = valid[i];
      var dist = 0;
      for (var j = 0; j < s.parameters.length; j++) {
        var p = s.parameters[j];
        var a = sel[p.code], b = v.params[p.code];
        if (a === undefined || b === undefined) continue;
        if (p.type === 'range') dist += Math.abs(num(a) - num(b));
        else if (str(a) !== str(b)) dist += 1e6;
      }
      if (best === null || dist < bestDist - 1e-9 ||
          (Math.abs(dist - bestDist) < 1e-9 && v.modelCode < best.modelCode)) {
        best = v; bestDist = dist;
      }
    }
    return best;
  }

  // ---------- 加载 ----------
  function load() {
    fetch('catalog.json')
      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function (cat) {
        state.catalog = cat;
        $('ver').textContent = cat.catalogName + ' v' + cat.catalogVersion;
        buildSearchIndex();
        renderTree();
        renderCrossRef();
        setStatus(t('目录加载完成，请选择产品系列', 'Catalog loaded, select a series'));
        if (location.search.indexOf('selftest=1') >= 0) runSelfTest();
      })
      .catch(function (e) {
        setStatus(t('加载 catalog.json 失败: ', 'Failed to load catalog.json: ') + e.message +
          t(' (请通过 Web 服务器访问, 不能直接双击打开本页)', ' (access via a web server, not file://)'));
      });
  }

  // ---------- 目录树 ----------
  function renderTree() {
    var tree = $('tree');
    tree.innerHTML = '';
    state.catalog.categories.forEach(function (cat) {
      var catDiv = document.createElement('div');
      catDiv.className = 'cat';
      catDiv.textContent = t(cat.nameZh, cat.nameEn);
      tree.appendChild(catDiv);
      cat.series.forEach(function (code) {
        var s = state.catalog.series.filter(function (x) { return x.code === code; })[0];
        if (!s) return;
        var div = document.createElement('div');
        div.className = 'series';
        div.textContent = t(s.nameZh, s.nameEn);
        div.onclick = function () {
          var all = tree.querySelectorAll('.series');
          for (var i = 0; i < all.length; i++) all[i].classList.remove('active');
          div.classList.add('active');
          selectSeries(s);
        };
        tree.appendChild(div);
      });
    });
  }

  // ---------- 选型 ----------
  function selectSeries(s) {
    state.series = s;
    state.sel = {};
    state.ranges = {};
    state.tableFilter = {};   // 换系列重置参数表列筛选
    var panel = $('paramPanel');
    panel.innerHTML = '';
    s.parameters.forEach(function (p) {
      var row = document.createElement('div');
      row.className = 'param-row';
      var label = document.createElement('label');
      label.textContent = paramName(p);
      label.id = 'lbl-' + p.code;
      row.appendChild(label);
      if (p.type === 'range') {
        // 参数化 range 域: 输入框 + 标准档位快捷选择 (与离线版可编辑下拉一致)
        var inp = document.createElement('input');
        inp.id = 'sel-' + p.code;
        inp.className = 'param-input';
        inp.setAttribute('list', 'dl-' + p.code);
        inp.setAttribute('placeholder', t('输入或选标准档位', 'Type or pick standard'));
        var dl = document.createElement('datalist');
        dl.id = 'dl-' + p.code;
        inp.onchange = function () {
          var raw = inp.value.trim();
          if (raw === '') { delete state.sel[p.code]; }
          else {
            var d = num(raw);
            if (!isNaN(d) && domainValid(p, d)) state.sel[p.code] = d;
            else {
              setStatus(t('输入超出范围或与步长不符', 'Out of range or not aligned to step'));
              delete state.sel[p.code];
            }
          }
          refreshOptions();
        };
        row.appendChild(inp);
        row.appendChild(dl);
        panel.appendChild(row);
        return;
      }
      var sel = document.createElement('select');
      sel.id = 'sel-' + p.code;
      sel.onchange = function () {
        var raw = sel.value;
        if (raw === '') { delete state.sel[p.code]; }
        else {
          var v = raw === '__EMPTY__' ? '' : (p.type === 'number' ? num(raw) : raw);
          state.sel[p.code] = v;
        }
        refreshOptions();
      };
      row.appendChild(sel);
      panel.appendChild(row);
    });
    $('modelTitle').textContent = t(s.nameZh, s.nameEn);
    $('btnDatasheet').classList.add('hidden');
    // 重置尺寸图区: 选型完成前只显示提示文字, 不显示占位图
    var dimImg = $('dimImage');
    dimImg.src = '';
    dimImg.classList.add('hidden');
    $('dimHint').classList.remove('hidden');
    $('dimHint').textContent = t('选型完成后显示尺寸图', 'Dimension drawing appears after selection');
    renderRangeRow();
    renderManualPages(s);
    refreshOptions();
    renderTable();
  }

  // 手册页 (系列级附件 kind=manual, 尺寸图窗口滚动对照, 选系列即显示; 标题只显示序号不显示路径)
  function renderManualPages(s) {
    var mw = $('manualWrap');
    mw.innerHTML = '';
    var pages = (s.attachments || []).filter(function (a) { return a.kind === 'manual'; });
    pages.forEach(function (a, idx) {
      var cap = document.createElement('p');
      cap.className = 'manual-cap';
      cap.textContent = t('手册页', 'Manual page') + ' ' + (idx + 1) + '/' + pages.length;
      var im = document.createElement('img');
      im.className = 'manual-img';
      im.src = a.path;
      im.alt = a.path;
      mw.appendChild(cap);
      mw.appendChild(im);
    });
  }

  // 空字符串是合法枚举值 (如 磁石=""=无磁石), 用哨兵值避免与"未选择"混淆
  function optValue(v) { return str(v) === '' ? '__EMPTY__' : str(v); }

  function refreshOptions() {
    var s = state.series;
    if (!s) return;
    var allowed = allowedOptions(s, state.sel);
    s.parameters.forEach(function (p) {
      var sel = $('sel-' + p.code);
      var vals = allowed[p.code] || [];
      if (p.type === 'range') {
        // 输入框: 更新 datalist 标准档位 (标注 标准); 已选值回显
        var dl = $('dl-' + p.code);
        dl.innerHTML = '';
        vals.forEach(function (v) {
          var opt = document.createElement('option');
          opt.value = str(v);
          var isGrid = (p.gridValues || []).some(function (g) { return str(g) === str(v); });
          opt.label = isGrid ? str(v) + ' (' + t('标准', 'Std') + ')' : str(v);
          dl.appendChild(opt);
        });
        sel.value = state.sel[p.code] !== undefined ? str(state.sel[p.code]) : '';
        return;
      }
      // 已选值变得不可选时清除
      if (state.sel[p.code] !== undefined &&
          !vals.some(function (v) { return str(v) === str(state.sel[p.code]); })) {
        delete state.sel[p.code];
      }
      sel.innerHTML = '<option value="">' + t('请选择', 'Select') + '</option>';
      vals.forEach(function (v) {
        var opt = document.createElement('option');
        opt.value = optValue(v);
        opt.textContent = displayValue(p, v);
        if (state.sel[p.code] !== undefined && str(v) === str(state.sel[p.code])) opt.selected = true;
        sel.appendChild(opt);
      });
    });
    updateSelection();
    if (crossFill) crossFill();   // 平替对照随选型刷新 (只显示当前型号的平替)
  }

  function updateSelection() {
    var s = state.series;
    var v = resolve(s, state.sel);
    var pres = null;
    if (!v && isParametric(s) && Object.keys(state.sel).length === s.parameters.length) {
      pres = parametricGenerate(s, state.sel);
      if (pres && pres.error) pres = null;   // 非法组合按"无匹配"处理
    }
    curVariant = v;      // 购物车"加入清单"用 (2026-08-24)
    curParam = pres;     // 参数化非标结果 (2026-08-25)
    var dl = $('btnDownload');
    var ds = $('btnDatasheet');
    if (!v && !pres) {
      $('modelTitle').textContent = t(s.nameZh, s.nameEn) + ' — ' +
        t('已选 ' + Object.keys(state.sel).length + '/' + s.parameters.length + ' 项', Object.keys(state.sel).length + '/' + s.parameters.length + ' selected');
      dl.classList.add('hidden');
      ds.classList.add('hidden');
      $('btnGenFmt').classList.add('hidden');
      $('btnAddCart').classList.add('hidden');
      $('dimImage').classList.add('hidden');
      $('dimHint').classList.remove('hidden');
      $('dimHint').textContent = t('选型完成后显示尺寸图', 'Dimension drawing appears after selection');
      $('viewer3dHint').classList.remove('hidden');
      $('viewer3dHint').textContent = t('选型完成后显示 3D 外形 (拖拽旋转 / 滚轮缩放)', '3D preview appears after selection');
      var frame0 = $('viewerFrame');
      frame0.src = 'about:blank';
      frame0.classList.add('hidden');
      return;
    }

    // 非标档位 (参数化引擎): 徽标 + 下载置灰 + 3D 降级最近标准档位 + 提示条 (与离线版一致)
    if (pres) {
      $('modelTitle').innerHTML = t('已选型号: ', 'Model: ') + esc(pres.modelCode) +
        '<span class="nonstd-badge">' + t('非标档位（无标准数模）', 'Non-standard (no standard model)') + '</span>';
      dl.classList.remove('hidden');
      dl.removeAttribute('href');
      var fmtSel = $('btnGenFmt');
      if (genApi()) {
        // 按需生成服务可用 (二期): 按钮升级为【在线生成数模】+ 格式选择 (STEP/Parasolid/IGES/STL/全格式)
        dl.disabled = false;
        dl.title = '';
        dl.setAttribute('data-gen', '1');
        dl.textContent = t('在线生成数模', 'Generate 3D model online');
        dl.onclick = function (ev) { ev.preventDefault(); genSingle(pres); };
        fmtSel.classList.remove('hidden');
      } else {
        fmtSel.classList.add('hidden');
        // 一期过渡态: 置灰 + 提示 (与离线版一致)
        dl.disabled = true;
        dl.removeAttribute('data-gen');
        dl.textContent = t('下载 STEP', 'Download STEP');
        dl.title = t('非标档位暂不支持数模下载，请联系销售或选择标准档位',
          'STEP download unavailable for non-standard config. Contact sales or choose a standard config.');
        dl.onclick = null;
      }
      $('btnAddCart').classList.remove('hidden');
      // 数据表 PDF 照常可下
      var dsAtt2 = (s.attachments || []).filter(function (a) {
        return a.kind === 'datasheet' && a.lang === state.lang;
      })[0] || (s.attachments || []).filter(function (a) { return a.kind === 'datasheet'; })[0];
      if (dsAtt2) {
        ds.href = dsAtt2.path;
        ds.setAttribute('download', dsAtt2.path.split('/').pop() || 'datasheet.pdf');
        ds.classList.remove('hidden');
      } else ds.classList.add('hidden');
      // 非标无尺寸图
      $('dimImage').classList.add('hidden');
      $('dimHint').classList.remove('hidden');
      $('dimHint').textContent = t('非标档位无标准尺寸图', 'No dimension drawing for non-standard config');
      var near = nearestStandard(s, state.sel);
      if (near) load3D(near);
      $('viewer3dHint').classList.remove('hidden');
      $('viewer3dHint').textContent = t(
        '当前为非标档位，展示最近标准档位 ' + (near ? near.modelCode : '?') + ' 的近似几何',
        'Non-standard config — showing nearest standard model ' + (near ? near.modelCode : '?') + ' as approximation.');
      setStatus(t('非标档位: 无对应标准数模; STEP 下载需联系销售或选择标准档位',
        'Non-standard config: contact sales or choose a standard config for STEP'));
      return;
    }

    $('modelTitle').textContent = t('已选型号: ', 'Model: ') + v.modelCode;
    dl.disabled = false;
    dl.title = '';
    dl.textContent = t('下载 STEP', 'Download STEP');
    dl.onclick = null;                 // 清除非标分支的在线生成处理器
    dl.removeAttribute('data-gen');
    $('btnGenFmt').classList.add('hidden');   // 标准档位无多格式 (升级项: 维护端批量出多格式)
    dl.href = v.step;
    dl.setAttribute('download', v.modelCode + '.step');
    dl.classList.remove('hidden');
    dl.dataset.model = v.modelCode;
    $('btnAddCart').classList.remove('hidden');
    setStatus(t('选型完成，点击下载 STEP', 'Ready - click to download STEP'));

    // 尺寸图 (优先匹配型号)
    var att = null;
    (s.attachments || []).forEach(function (a) {
      if (a.kind === 'dimDrawing' && (a.model === v.modelCode || att === null)) att = a;
    });
    if (att) {
      $('dimImage').src = att.path;
      $('dimImage').classList.remove('hidden');
      $('dimHint').classList.add('hidden');
    }
    // 数据表
    var dsAtt = (s.attachments || []).filter(function (a) {
      return a.kind === 'datasheet' && a.lang === state.lang;
    })[0] || (s.attachments || []).filter(function (a) { return a.kind === 'datasheet'; })[0];
    if (dsAtt) {
      // 下载行为 (2026-08-24): 点按钮直接下载 PDF, 与 STEP 下载一致
      ds.href = dsAtt.path;
      ds.setAttribute('download', dsAtt.path.split('/').pop() || 'datasheet.pdf');
      ds.classList.remove('hidden');
    }

    // 3D 预览 (零依赖 Canvas 渲染)
    load3D(v);
  }

  // ---------- 参数表 (含范围筛选) ----------
  function renderRangeRow() {
    var row = $('rangeRow');
    row.innerHTML = '';
    var s = state.series;
    if (!s) return;
    s.parameters.forEach(function (p) {
      if (p.type !== 'number' && p.type !== 'range') return;
      var lbl = document.createElement('span');
      lbl.textContent = paramName(p);
      var min = document.createElement('input');
      min.placeholder = t('最小', 'min');
      var max = document.createElement('input');
      max.placeholder = t('最大', 'max');
      var apply = document.createElement('button');
      apply.textContent = t('应用', 'Apply');
      apply.onclick = function () {
        var lo = min.value === '' ? null : num(min.value);
        var hi = max.value === '' ? null : num(max.value);
        if (lo !== null || hi !== null) state.ranges[p.code] = [lo, hi];
        else delete state.ranges[p.code];
        renderTable();
      };
      row.appendChild(lbl); row.appendChild(min); row.appendChild(max); row.appendChild(apply);
    });
  }

  function inRange(v) {
    var s = state.series;
    for (var code in state.ranges) {
      var p = s.parameters.filter(function (x) { return x.code === code; })[0];
      if (!p || p.type !== 'number') continue;
      var val = num(v.params[code]);
      var r = state.ranges[code];
      if (r[0] !== null && val < r[0]) return false;
      if (r[1] !== null && val > r[1]) return false;
    }
    return true;
  }

  // 2026-08-24: STEP 列去掉 (无意义); 每列表头可点击筛选取值, 与桌面版一致
  var colFilterPop = null;
  function renderTable() {
    var s = state.series;
    var thead = $('variantTable').querySelector('thead');
    var tbody = $('variantTable').querySelector('tbody');
    thead.innerHTML = ''; tbody.innerHTML = '';
    closeColFilter();
    if (!s) return;
    var f = state.tableFilter || {};
    var hr = document.createElement('tr');
    hr.appendChild(th(t('型号 Model', 'Model')));
    s.parameters.forEach(function (p) {
      var h = th(paramName(p));
      var vals = distinctColValues(s, p);
      if (vals.length > 1) {
        h.classList.add('col-filter');
        h.title = t('点击筛选此列', 'Click to filter');
        if (f[p.code] !== undefined) h.textContent = paramName(p) + ' ▾ ' + displayValue(p, f[p.code]);
        (function (p2, h2, vals2) {
          h2.onclick = function (ev) { showColFilter(h2, p2, vals2, ev); };
        })(p, h, vals);
      }
      hr.appendChild(h);
    });
    thead.appendChild(hr);

    validVariants(s).filter(inRange).filter(function (v) {
      for (var k in f) { if (str(v.params[k]) !== str(f[k])) return false; }
      return true;
    }).forEach(function (v) {
      var tr = document.createElement('tr');
      tr.appendChild(td(v.modelCode));
      s.parameters.forEach(function (p) {
        tr.appendChild(td(v.params[p.code] !== undefined ? displayValue(p, v.params[p.code]) : ''));
      });
      // 行尾"+"加入清单 (2026-08-24, 不触发行点击)
      var addTd = document.createElement('td');
      var addBtn = document.createElement('button');
      addBtn.className = 'add-cart-btn';
      addBtn.textContent = '+';
      addBtn.title = t('加入清单', 'Add to Cart');
      addBtn.onclick = function (ev) { ev.stopPropagation(); addToCart(v); };
      addTd.appendChild(addBtn);
      tr.appendChild(addTd);
      tr.onclick = function () {
        state.sel = {};
        s.parameters.forEach(function (p) { state.sel[p.code] = v.params[p.code]; });
        refreshOptions();
        switchTab('3d');   // 3D/尺寸图同页 (2026-08-24 合并后)
      };
      tbody.appendChild(tr);
    });
    setStatus(t('参数表: ' + tbody.children.length + ' 个型号', 'Table: ' + tbody.children.length + ' models'));
  }

  function distinctColValues(s, p) {
    var out = [];
    validVariants(s).forEach(function (v) {
      var sv = str(v.params[p.code]);
      if (out.indexOf(sv) < 0) out.push(sv);
    });
    return out;
  }

  function showColFilter(header, p, vals, ev) {
    closeColFilter();
    var pop = document.createElement('div');
    pop.className = 'col-filter-pop';
    var all = document.createElement('div');
    all.className = 'col-filter-item';
    all.textContent = t('全部 (清除筛选)', 'All (clear)');
    all.onclick = function () { delete state.tableFilter[p.code]; closeColFilter(); renderTable(); };
    pop.appendChild(all);
    vals.forEach(function (sv) {
      var it = document.createElement('div');
      it.className = 'col-filter-item';
      if ((state.tableFilter || {})[p.code] === sv) it.classList.add('sel');
      it.textContent = sv === '' ? t('(空值)', '(empty)') : displayValue(p, sv);
      it.onclick = function () {
        state.tableFilter = state.tableFilter || {};
        state.tableFilter[p.code] = sv;
        closeColFilter();
        renderTable();
      };
      pop.appendChild(it);
    });
    var r = header.getBoundingClientRect();
    pop.style.left = r.left + 'px';
    pop.style.top = (r.bottom + 2) + 'px';
    document.body.appendChild(pop);
    colFilterPop = pop;
    ev.stopPropagation();
  }

  function closeColFilter() {
    if (colFilterPop && colFilterPop.parentNode) colFilterPop.parentNode.removeChild(colFilterPop);
    colFilterPop = null;
  }
  document.addEventListener('click', closeColFilter);

  function th(txt) { var el = document.createElement('th'); el.textContent = txt; return el; }
  function td(txt) { var el = document.createElement('td'); el.textContent = txt; return el; }

  // ---------- 搜索 ----------
  function buildSearchIndex() {
    state.searchIndex = [];
    state.catalog.series.forEach(function (s) {
      (s.variants || []).forEach(function (v) {
        var parts = [t(s.nameZh, s.nameEn), s.code, s.keywords || ''];
        s.parameters.forEach(function (p) {
          parts.push(t(p.nameZh, p.nameEn));
          parts.push(p.keywords || '');
          var dv = displayValue(p, v.params[p.code]);
          parts.push(dv);
        });
        state.searchIndex.push({
          series: s, variant: v, code: v.modelCode,
          text: parts.join(' ').toLowerCase() + ' ' + v.modelCode.toLowerCase()
        });
      });
    });
  }

  function doSearch() {
    var q = $('searchBox').value.trim().toLowerCase();
    if (!q) return;
    var tokens = q.split(/[\s,，;；]+/).filter(function (x) { return x.length > 0; });
    var results = [];
    state.searchIndex.forEach(function (it) {
      var code = it.code.toLowerCase();
      var allIn = tokens.every(function (tk) { return code.indexOf(tk) >= 0 || it.text.indexOf(tk) >= 0; });
      if (!allIn) return;
      var score = 0;
      if (tokens.some(function (tk) { return code === tk; })) score = 1000;
      else if (tokens.every(function (tk) { return code.indexOf(tk) >= 0; })) score = 800;
      else score = 500;
      it.score = score;
      results.push(it);
    });
    // 平替模糊搜索 (2026-08-24): 竞品型号包含关键词 → 对应我方型号也列入结果 (精确命中给高分)
    (state.catalog.crossRefs || []).forEach(function (cr) {
      var fm = (cr.foreignModel || '').toLowerCase();
      var anyIn = tokens.some(function (tk) { return fm.indexOf(tk) >= 0; });
      if (!anyIn) return;
      if (results.some(function (r) { return r.code.toLowerCase() === (cr.ourModel || '').toLowerCase(); })) return;
      var hit = null, hs = null;
      state.catalog.series.forEach(function (s) {
        (s.variants || []).forEach(function (v) {
          if (v.modelCode.toLowerCase() === (cr.ourModel || '').toLowerCase()) { hit = v; hs = s; }
        });
      });
      if (!hit) return;
      results.push({
        series: hs, variant: hit, code: hit.modelCode,
        score: fm === q ? 950 : 750,
        altNote: cr.brand + ' ' + cr.foreignModel
      });
    });
    results.sort(function (a, b) { return b.score - a.score || (a.code < b.code ? -1 : 1); });
    state.searchResult = results;
    var tbody = $('searchTable').querySelector('tbody');
    tbody.innerHTML = '';
    results.forEach(function (it) {
      var tr = document.createElement('tr');
      tr.appendChild(td(it.code));
      tr.appendChild(td(t(it.series.nameZh, it.series.nameEn) + (it.altNote ? (' ← ' + t('平替 ', 'Alt ') + it.altNote) : '')));
      tr.appendChild(td(it.variant.step));
      tr.onclick = function () {
        selectSeries(it.series);
        state.sel = {};
        it.series.parameters.forEach(function (p) { state.sel[p.code] = it.variant.params[p.code]; });
        refreshOptions();
        switchTab('dim');
      };
      tbody.appendChild(tr);
    });
    switchTab('search');
    setStatus(t('搜索 "' + q + '": ' + results.length + ' 条结果', 'Search "' + q + '": ' + results.length + ' result(s)'));
  }

  // ---------- 标签页 / 语言 / 状态 ----------
  // 2026-08-24: 3D 与尺寸图合并为同一页 (tab-3d 内含左右分栏+可拖拽分隔条), 不再有独立尺寸图标签
  function switchTab(name) {
    var tabs = document.querySelectorAll('.tab');
    for (var i = 0; i < tabs.length; i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab') === name);
    ['3d', 'table', 'search', 'cross'].forEach(function (n) {
      $('tab-' + n).classList.toggle('hidden', n !== name);
    });
  }

  // ---------- 平替对照 (竞品型号 → 我方型号, 源 catalog.crossRefs) ----------
  // 2026-08-24: 只显示与当前已选型号匹配的平替行 (未选型号则显示全部), 与桌面版一致
  var crossFill = null;
  function renderCrossRef() {
    var sel = $('crossBrand');
    var rows = state.catalog.crossRefs || [];
    var brands = [];
    rows.forEach(function (r) { if (brands.indexOf(r.brand) < 0) brands.push(r.brand); });
    brands.sort();
    sel.innerHTML = '';
    var opt0 = document.createElement('option');
    opt0.value = ''; opt0.textContent = t('全部品牌', 'All brands');
    sel.appendChild(opt0);
    brands.forEach(function (b) {
      var o = document.createElement('option');
      o.value = b; o.textContent = b;
      sel.appendChild(o);
    });
    sel.onchange = crossFill = fillCrossRef;
    crossFill();
    function fillCrossRef() {
      var b = sel.value;
      var cur = null;
      try { var sv = resolve(state.series, state.sel); cur = sv ? sv.modelCode : null; } catch (e) {}
      var tb = $('crossTable').querySelector('tbody');
      tb.innerHTML = '';
      rows.forEach(function (r) {
        if (b && r.brand !== b) return;
        if (cur && r.ourModel.toLowerCase() !== cur.toLowerCase()) return;
        var tr = document.createElement('tr');
        tr.innerHTML = '<td>' + esc(r.brand) + '</td><td>' + esc(r.foreignModel) + '</td>' +
          '<td><a href="#">' + esc(r.ourModel) + '</a></td><td>' + esc(r.note || '') + '</td>';
        tr.onclick = function () { jumpToModel(r.ourModel); };
        tb.appendChild(tr);
      });
    }
  }

  function jumpToModel(modelCode) {
    var found = null, fs = null;
    state.catalog.series.forEach(function (s) {
      (s.variants || []).forEach(function (v) {
        if (v.modelCode.toLowerCase() === modelCode.toLowerCase()) { found = v; fs = s; }
      });
    });
    if (!found) { setStatus(t('平替型号未找到: ', 'Cross-ref model not found: ') + modelCode); return; }
    selectSeries(fs);
    state.sel = {};
    for (var k in found.params) state.sel[k] = found.params[k];
    refreshOptions();
    switchTab('3d');
    var all = document.querySelectorAll('#tree .series');
    for (var i = 0; i < all.length; i++) all[i].classList.remove('active');
    var node = Array.prototype.filter.call(all, function (el) { return el.textContent === t(fs.nameZh, fs.nameEn); })[0];
    if (node) node.classList.add('active');
  }

  // ---------- 购物车 (淘宝式清单批量导出, 2026-08-24; 与桌面版语义/文案一致) ----------
  var curVariant = null;   // 当前已解析型号 (加入清单用, 由 updateSelection 维护)
  var curParam = null;     // 当前参数化非标结果 (2026-08-25, 与 curVariant 互斥)

  function paramsSummary(v) {
    var s = state.series;
    if (!s) return '';
    var parts = [];
    s.parameters.forEach(function (p) {
      if (v.params[p.code] === undefined) return;
      var sv = str(v.params[p.code]);
      var label = (p.display && p.display[sv] !== undefined) ? p.display[sv] : sv;
      parts.push(t(p.nameZh, p.nameEn) + '=' + (label === '' ? t('(无)', '(none)') : label));
    });
    return parts.join('; ');
  }

  function addToCart(v) {
    if (!v) return;
    var dup = state.cart.some(function (it) { return it.modelCode.toLowerCase() === v.modelCode.toLowerCase(); });
    if (dup) { setStatus(t('该型号已在清单中', 'Already in cart')); return; }
    // 非标档位 (无 step): 打包时跳过并在清单中注明 (与离线版一致)
    var nonStdPrefix = v.step ? '' : t('【非标档位（打包时无 STEP）】 ', '[Non-standard (no STEP in package)] ');
    state.cart.push({
      modelCode: v.modelCode,
      seriesCode: state.series ? state.series.code : '',
      seriesName: state.series ? t(state.series.nameZh, state.series.nameEn) : '',
      stepPath: v.step || null,
      params: v.params || {},   // 非标批量生成提交用 (二期)
      paramsSummary: nonStdPrefix + paramsSummary(v),
      qty: 1
    });
    renderCartBadge();
    setStatus(t('已加入清单 (共 ' + state.cart.length + ' 项)', 'Added to cart (' + state.cart.length + ' items)'));
  }

  function renderCartBadge() {
    $('btnCart').textContent = t('清单 (' + state.cart.length + ')', 'Cart (' + state.cart.length + ')');
  }

  function openCart() {
    $('cartOverlay').classList.remove('hidden');
    $('cartPanel').classList.remove('hidden');
    renderCart();
  }
  function closeCart() {
    $('cartOverlay').classList.add('hidden');
    $('cartPanel').classList.add('hidden');
  }

  function renderCart() {
    var list = $('cartList');
    list.innerHTML = '';
    if (state.cart.length === 0) {
      var empty = document.createElement('div');
      empty.className = 'hint';
      empty.textContent = t('清单为空, 请先加入型号', 'Cart is empty');
      list.appendChild(empty);
    }
    state.cart.forEach(function (it, idx) {
      var div = document.createElement('div');
      div.className = 'cart-item';
      div.innerHTML = '<div class="ci-model">' + esc(it.modelCode) + '</div>' +
        '<div class="ci-sub">' + esc(it.seriesName) + ' · ' + esc(it.paramsSummary) + '</div>';
      var ops = document.createElement('div');
      ops.className = 'ci-ops';
      var lbl = document.createElement('span');
      lbl.textContent = t('数量', 'Qty');
      var qty = document.createElement('input');
      qty.type = 'number'; qty.min = '1'; qty.value = it.qty;
      qty.onchange = function () { it.qty = Math.max(1, parseInt(qty.value, 10) || 1); qty.value = it.qty; };
      var del = document.createElement('button');
      del.className = 'ci-del';
      del.textContent = t('删除', 'Remove');
      del.onclick = function () { state.cart.splice(idx, 1); renderCart(); };
      ops.appendChild(lbl); ops.appendChild(qty); ops.appendChild(del);
      div.appendChild(ops);
      list.appendChild(div);
    });
    renderCartBadge();
  }

  function csvEscape(c) {
    c = c == null ? '' : String(c);
    if (/[",\n\r]/.test(c)) c = '"' + c.replace(/"/g, '""') + '"';
    return c;
  }

  function timestampName() {
    var d = new Date();
    function p2(x) { return (x < 10 ? '0' : '') + x; }
    return '' + d.getFullYear() + p2(d.getMonth() + 1) + p2(d.getDate()) + p2(d.getHours()) + p2(d.getMinutes()) + p2(d.getSeconds());
  }

  // ---------- NX 按需生成服务 (二期 A 版, 2026-08-25; 与离线版语义/文案一致) ----------
  function genApi() {
    return (window.GEN_CONFIG && window.GEN_CONFIG.api) ? window.GEN_CONFIG.api.replace(/\/+$/, '') : '';
  }

  function triggerDownload(url, name) {
    var a = document.createElement('a');
    a.href = url;
    a.download = name;
    document.body.appendChild(a);
    a.click();
    a.remove();
  }

  // 下载地址/文件名按所选格式 (btnGenFmt: zip/step/parasolid/iges/stl; 生成输出留存全格式, 格式只影响下载)
  function genFmt() {
    var sel = $('btnGenFmt');
    return sel ? sel.value : 'zip';
  }
  function downloadUrl(api, code) {
    var fmt = genFmt();
    var url = api + '/download?series=' + encodeURIComponent(state.series.code) + '&code=' + encodeURIComponent(code);
    if (fmt !== 'zip') url += '&fmt=' + encodeURIComponent(fmt);
    return url;
  }
  function downloadName(code) {
    var exts = { zip: '.zip', step: '.stp', iges: '.igs' };
    return code + (exts[genFmt()] || '.zip');
  }

  // 单型号在线生成: 提交 → 轮询 → 网页内 3D 预览 → 选格式 → 下载 (检索先行命中直接进入已生成态)
  async function genSingle(pres) {
    var api = genApi();
    if (!api || !pres) return;
    var dl = $('btnDownload');
    dl.disabled = true;
    dl.textContent = t('正在生成数模，预计 10–60 秒…', 'Generating — typically 10–60 s…');
    try {
      var resp = await fetch(api + '/gen', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ series: state.series.code, code: pres.modelCode, params: pres.params })
      });
      if (!resp.ok) {
        var ej = null;
        try { ej = await resp.json(); } catch (e) { }
        throw new Error(ej && ej.error === 'queue_busy' ? t('生成队列繁忙，请稍后再试', 'Queue busy — try again later')
          : ej && ej.error === 'too_many_requests' ? t('请求过于频繁，请稍后再试', 'Too many requests — try again later')
          : t('请求被拒绝', 'Request rejected'));
      }
      var r = await resp.json();
      if (r.immediate) { finishGen(api, pres, dl); return; }
      var done = false, last = '';
      for (var i = 0; i < 150 && !done; i++) {
        await new Promise(function (res) { setTimeout(res, 3000); });
        var sr = await fetch(api + '/status?job=' + encodeURIComponent(r.jobId));
        if (!sr.ok) continue;
        var st = await sr.json();
        if (st.state === 'queued') { last = t('生成队列繁忙，前面还有任务', 'Queue busy'); }
        else if (st.state === 'running') { last = t('正在生成数模，预计 10–60 秒…', 'Generating — typically 10–60 s…'); }
        else if (st.state === 'done' && st.ok) { done = true; break; }
        else { throw new Error(t('生成失败，请重试或联系销售', 'Generation failed — retry or contact sales')); }
        dl.textContent = last;
      }
      if (!done) throw new Error(t('生成超时，请重试', 'Generation timed out — retry'));
      finishGen(api, pres, dl);
    } catch (e) {
      setStatus(t('生成失败: ', 'Generation failed: ') + e.message);
      dl.disabled = false;
      dl.textContent = t('在线生成数模', 'Generate 3D model online');
    }
  }

  // 生成完成: 网页内 3D 预览 + 按钮变"已生成"，用户选格式后点下载才真正下载
  function finishGen(api, pres, dl) {
    previewGenerated(api, pres);
    dl.disabled = false;
    dl.setAttribute('data-gen', '1');
    dl.textContent = t('已生成，下载数模', 'Generated — download');
    dl.onclick = function (ev) { ev.preventDefault(); triggerDownload(downloadUrl(api, pres.modelCode), downloadName(pres.modelCode)); };
    setStatus(t('数模已生成，请选择格式后下载', 'Generated — select format and download'));
  }

  // 用生成服务返回的 STEP 做网页内 3D 预览 (查看器 url= 参数 fetch GenServer /preview/{series}/{code}.stp)
  function previewGenerated(api, pres) {
    var frame = $('viewerFrame');
    var canvas = $('viewer3d');
    var hint = $('viewer3dHint');
    hint.classList.add('hidden');
    frame.classList.remove('hidden');
    canvas.classList.add('hidden');
    var stepUrl = api + '/preview/' + encodeURIComponent(state.series.code) + '/' + encodeURIComponent(pres.modelCode) + '.stp';
    frame.src = VIEWER_BASE + '?url=' + encodeURIComponent(stepUrl) + '&embed=1&bg=grey&menu=min';
    if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer);
    // 查看器不可用 (404/离线) → 回退内置 Canvas (url= 跨域无法探测 contentDocument, 故直接标记可用)
    v3d.fallbackTimer = setTimeout(function () {
      try {
        var d = frame.contentDocument;
        var ok = !!(d && d.body && d.body.innerHTML.length > 200);
        if (!ok) {
          frame.classList.add('hidden');
          canvas.classList.remove('hidden');
          $('viewer3dHint').classList.remove('hidden');
          $('viewer3dHint').textContent = t('3D 预览加载失败，请直接下载后本地查看', '3D preview failed — download to view locally');
        }
      } catch (e) { /* 跨域视为可用 */ }
    }, 8000);
  }

  async function downloadCartZip() {
    if (state.cart.length === 0) { setStatus(t('清单为空, 请先加入型号', 'Cart is empty')); return; }
    var btn = $('cartDownload');
    btn.disabled = true;
    btn.textContent = t('打包中...', 'Packing...');
    try {
      var snap = state.cart.slice();   // 打包快照, 打包期间操作只影响下一次
      var zip = new JSZip();
      var missing = [];
      var csv = '序号,型号编码,系列代码,系列名,数量,参数摘要,STEP文件名\r\n';
      var used = {};

      // 非标档位 + 生成服务可用 → 先批量在线生成, 成功后合并进打包 zip (型号子文件夹多格式)
      var nonStd = snap.filter(function (it) { return !it.stepPath; });
      var api = genApi();
      if (nonStd.length && api) {
        btn.textContent = t('正在在线生成非标数模…', 'Generating non-standard models…');
        var st0 = null;
        try {
          var r0 = await fetch(api + '/gen-batch', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ items: nonStd.map(function (it) {
              return { series: it.seriesCode, code: it.modelCode, params: it.params || {} };
            }) })
          });
          if (!r0.ok) throw new Error(t('请求被拒绝', 'Request rejected'));
          var j0 = await r0.json();
          if (j0.error) throw new Error(j0.error);
          var done0 = false;
          for (var w = 0; w < 200 && !done0; w++) {
            await new Promise(function (res) { setTimeout(res, 3000); });
            var sr0 = await fetch(api + '/status?job=' + encodeURIComponent(j0.jobId));
            if (!sr0.ok) continue;
            st0 = await sr0.json();
            if (st0.state === 'queued') { btn.textContent = t('生成队列繁忙，请稍候…', 'Queued…'); continue; }
            if (st0.state === 'running') {
              if (st0.done !== undefined && st0.total) btn.textContent = t('正在生成 ' + st0.done + '/' + st0.total + '…', 'Generating ' + st0.done + '/' + st0.total + '…');
              else btn.textContent = t('正在生成数模…', 'Generating…');
              continue;
            }
            if (st0.state === 'done') { done0 = true; break; }
            throw new Error(st0.error || t('生成失败，请重试或联系销售', 'Generation failed — retry or contact sales'));
          }
          if (!done0) throw new Error(t('生成超时，请重试', 'Generation timed out — retry'));
          // 整体 zip (含检索命中的已留存型号) → 合并进打包 zip
          var zr0 = await fetch(api + '/files?job=' + encodeURIComponent(j0.jobId) + '&token=' + encodeURIComponent(j0.token));
          if (!zr0.ok) throw new Error(t('下载失败', 'Download failed'));
          var sub = await JSZip.loadAsync(await zr0.blob());
          await Promise.all(Object.keys(sub.files).map(function (p) {
            if (sub.files[p].dir) return Promise.resolve();
            return sub.files[p].async('uint8array').then(function (d) { zip.file(p, d); });
          }));
          var fail0 = (st0 && st0.items ? st0.items : []).filter(function (x) { return !x.ok; }).map(function (x) { return x.code; });
          if (fail0.length) setStatus(t('部分非标生成失败: ', 'Some generation failed: ') + fail0.join(', '));
        } catch (e) {
          btn.disabled = false;
          btn.textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)');
          setStatus(t('非标生成失败: ', 'Generation failed: ') + e.message);
          return;
        }
      }
      for (var i = 0; i < snap.length; i++) {
        var it = snap[i];
        // 非标档位: 生成服务可用时已合并为型号子文件夹; 不可用则显式注明 (不静默跳过)
        if (!it.stepPath) {
          csv += [i + 1, it.modelCode, it.seriesCode, it.seriesName, it.qty, it.paramsSummary,
            genApi() ? t('(在线生成, 型号子文件夹)', '(generated online, subfolder)') : t('(非标档位，无 STEP)', '(non-standard, no STEP)')].map(csvEscape).join(',') + '\r\n';
          continue;
        }
        var seg = (it.stepPath || '').split('/').pop();
        var ext = seg.indexOf('.') >= 0 ? seg.substring(seg.lastIndexOf('.')) : '.step';
        var name = it.modelCode + ext;
        if (used[name]) name = it.modelCode + '_' + it.seriesCode + ext;   // 重名冲突加系列码
        used[name] = true;
        try {
          var resp = await fetch(it.stepPath);
          if (!resp.ok) throw new Error('HTTP ' + resp.status);
          zip.file(name, await resp.blob());
        } catch (e) { missing.push(it.modelCode); }
        csv += [i + 1, it.modelCode, it.seriesCode, it.seriesName, it.qty, it.paramsSummary, name].map(csvEscape).join(',') + '\r\n';
      }
      zip.file('BOM.csv', '﻿' + csv);          // UTF-8 BOM, Excel 中文不乱码
      zip.file('清单.txt', csv.replace(/,/g, '  '));
      var blob = await zip.generateAsync({ type: 'blob' });
      var url = URL.createObjectURL(blob);
      var a = document.createElement('a');
      a.href = url;
      a.download = 'ONEBOT_清单_' + timestampName() + '.zip';
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(function () { URL.revokeObjectURL(url); }, 8000);
      if (missing.length) setStatus(t('打包完成, 但以下型号文件缺失: ', 'Packed, but missing: ') + missing.join(', '));
      else setStatus(t('已开始下载清单压缩包', 'Cart zip download started'));
    } catch (e) {
      setStatus(t('打包失败: ', 'Pack failed: ') + e.message);
    } finally {
      btn.disabled = false;
      btn.textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)');
    }
  }

  $('btnCart').onclick = openCart;
  $('cartClose').onclick = closeCart;
  $('cartOverlay').onclick = closeCart;
  $('cartClear').onclick = function () { state.cart = []; renderCart(); };
  $('cartDownload').onclick = downloadCartZip;
  $('btnAddCart').onclick = function () { addToCart(curVariant || curParam); };

  function setStatus(msg) { var m = $('statusMsg'); if (m) m.textContent = msg + ' [v3]'; }

  function applyLang() {
    $('btnLang').textContent = state.lang === 'zh' ? 'EN' : '中';
    $('searchBox').placeholder = t('搜索型号 / 关键词 / 拼音', 'Search model / keyword / pinyin');
    $('btnSearch').textContent = t('搜索', 'Search');
    $('btnAddCart').textContent = t('加入清单', 'Add to Cart');
    $('cartTitle').textContent = t('我的清单', 'My Cart');
    $('cartClear').textContent = t('清空', 'Clear All');
    $('cartDownload').textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)');
    renderCartBadge();
    $('btnDownload').textContent = t('下载 STEP', 'Download STEP');
    $('btnDatasheet').textContent = t('数据表 PDF', 'Datasheet PDF');
    $('dimHint').textContent = t('选型完成后显示尺寸图', 'Dimension drawing appears after selection');
    var tabs = document.querySelectorAll('.tab');
    tabs[0].textContent = t('3D / 尺寸图', '3D / Dimension');
    tabs[1].textContent = t('参数表', 'Parameters');
    tabs[2].textContent = t('搜索结果', 'Search');
    tabs[3].textContent = t('平替对照', 'Cross-Reference');
    renderTree();
    if (state.series) selectSeries(state.series);
  }

  // ---------- 自测 (与离线版 --selftest 对齐的关键场景) ----------
  function runSelfTest() {
    var pass = 0, fail = 0;
    var out = [];
    function check(name, ok) { out.push((ok ? 'PASS ' : 'FAIL ') + name); if (ok) pass++; else fail++; }
    try {
      var cat = state.catalog;
      check('catalog 加载 (系列数 ' + cat.series.length + ')', cat.series.length >= 2);
      var sc = cat.series[0];
      check('SC 有效变体 72', validVariants(sc).length === 72);
      var a1 = allowedOptions(sc, { bore: 16 });
      var strokes = a1.stroke.map(function (x) { return num(x); }).sort(function (x, y) { return x - y; });
      check('缸径16→行程仅25/50', strokes.length === 2 && strokes[0] === 25 && strokes[1] === 50);
      var a2 = allowedOptions(sc, { magnet: 'S' });
      check('磁石S→缸径不含16', a2.bore.length === 3 && a2.bore.every(function (x) { return num(x) !== 16; }));
      var v = resolve(sc, { bore: 32, stroke: 50, magnet: 'S', mount: 'LB' });
      check('选型命中 SC32x50S-LB', v && v.modelCode === 'SC32x50S-LB');
      var maq = cat.series[1];
      var a3 = allowedOptions(maq, { bore: 6 });
      check('MAQ 缸径6→行程仅10/20', a3.stroke.length === 2 && num(a3.stroke[0]) === 10 && num(a3.stroke[1]) === 20);
      buildSearchIndex();
      var r1 = [];
      state.searchIndex.forEach(function (it) { if (it.code.indexOf('sc32') >= 0) r1.push(it.code); });
      check('搜索索引含 SC32 系列', r1.length >= 4);
      var r2 = [];
      state.searchIndex.forEach(function (it) { if (it.text.indexOf('gangjing') >= 0) r2.push(it.code); });
      check('拼音关键词 gangjing 索引存在', r2.length > 0);
    } catch (e) {
      fail++;
      out.push('EXCEPTION: ' + e.message);
    }
    out.push('结果: ' + pass + ' 通过, ' + fail + ' 失败');
    setStatus('WEB SELFTEST — ' + out.join(' | '));
    console.log(out.join('\n'));
  }

  // ---------- 3D 预览 (零依赖 Canvas: 解析自有 STEP 圆柱 → 面片画家算法) ----------
  var v3d = { cyls: null, theta: 0.7, phi: 0.45, zoom: 1.0, dragging: false, lastX: 0, lastY: 0 };
  var V3D_COLORS = ['#8C8C8C', '#2F6FB3', '#8C8C8C', '#C8C8C8'];

  function parseStepCyls(text) {
    var rs = [], xs = [], zs = [], m;
    var re = /CYLINDRICAL_SURFACE\('',#\d+,([\d.]+)\);/g;
    while ((m = re.exec(text))) rs.push(parseFloat(m[1]));
    // 圆心点 = 仅有的 y=0 点: (X, 0, Z) 每圆柱一对
    var re2 = /CARTESIAN_POINT\('',\((-?[\d.]+),0,(-?[\d.]+)\)\);/g;
    while ((m = re2.exec(text))) { xs.push(parseFloat(m[1])); zs.push(parseFloat(m[2])); }
    var cy = [];
    for (var i = 0; i < rs.length && 2 * i + 1 < zs.length; i++)
      cy.push({ z0: Math.min(zs[2 * i], zs[2 * i + 1]), z1: Math.max(zs[2 * i], zs[2 * i + 1]), r: rs[i], x: xs[2 * i] });
    return cy;
  }

  // ---- 3D 查看器联动 (新迪 STEPViewer: occt-import-js + three.js, 支持真实 STEP) ----
  // 本地联调: /viewer/ 前缀由 serve-web.ps1 -ViewerRoot 映射到新迪 dev 目录
  // 生产同域部署 (3d.ruiguo.wang): 改为 '/STEPViewer/index.html'
  //   (模型文件已同步到 STEPViewer/step/ 目录, file 参数 = 目录内相对路径)
  var VIEWER_BASE = '/viewer/STEPViewer/index.html';

  function load3D(variant) {
    var frame = $('viewerFrame');
    var canvas = $('viewer3d');
    var hint = $('viewer3dHint');
    hint.classList.add('hidden');
    frame.classList.remove('hidden');
    canvas.classList.add('hidden');
    // 站点根绝对路径 (/step/...): 模型始终从站点 step\ 目录取, 查看器无需自带模型副本 (踩坑 #20)
    frame.src = VIEWER_BASE + '?file=' + encodeURIComponent('/' + variant.step) + '&embed=1&bg=grey&menu=min';
    if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer);
    // 查看器不可用 (404/离线) → 回退内置 Canvas 渲染
    v3d.fallbackTimer = setTimeout(function () {
      var ok = false;
      try {
        var d = frame.contentDocument;
        ok = !!(d && d.body && d.body.innerHTML.length > 200);
      } catch (e) { ok = true; }   // 跨域无法探测, 视为可用
      if (!ok) {
        frame.classList.add('hidden');
        canvas.classList.remove('hidden');
        load3DFallback(variant);
      }
    }, 8000);
  }

  function load3DFallback(variant) {
    fetch(variant.step)
      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
      .then(function (text) {
        v3d.cyls = parseStepCyls(text);
        $('viewer3dHint').classList.add('hidden');
        draw3D();
      })
      .catch(function (e) {
        v3d.cyls = null;
        $('viewer3dHint').classList.remove('hidden');
        $('viewer3dHint').textContent = t('3D 加载失败: ', '3D load failed: ') + e.message;
      });
  }

  function draw3D() {
    var canvas = $('viewer3d');
    var ctx = canvas.getContext('2d');
    var W = canvas.width, H = canvas.height;
    ctx.clearRect(0, 0, W, H);
    if (!v3d.cyls || v3d.cyls.length === 0) return;

    var scale = 60 * v3d.zoom;
    var cx = W / 2, cy = H / 2;
    function proj(p) {
      var ct = Math.cos(v3d.theta), st = Math.sin(v3d.theta);
      var cp = Math.cos(v3d.phi), sp = Math.sin(v3d.phi);
      var x1 = p[0] * ct + p[2] * st;
      var z1 = -p[0] * st + p[2] * ct;
      var y1 = p[1] * cp - z1 * sp;
      var z2 = p[1] * sp + z1 * cp;
      var f = 420 / (420 + z2);
      return { x: cx + x1 * f * scale, y: cy - y1 * f * scale, z: z2 };
    }

    var faces = [];
    for (var ci = 0; ci < v3d.cyls.length; ci++) {
      var c = v3d.cyls[ci];
      var seg = 24;
      var color = V3D_COLORS[ci % V3D_COLORS.length];
      var ring0 = [], ring1 = [];
      for (var i = 0; i <= seg; i++) {
        var a = 2 * Math.PI * i / seg, x = Math.cos(a) * c.r + (c.x || 0), y = Math.sin(a) * c.r;
        ring0.push([x, y, c.z0]);
        ring1.push([x, y, c.z1]);
      }
      for (i = 0; i < seg; i++) {
        var an = (i + 0.5) * 2 * Math.PI / seg;
        faces.push({
          pts: [ring0[i], ring0[i + 1], ring1[i + 1], ring1[i]],
          n: [Math.cos(an), Math.sin(an), 0],
          c: color
        });
      }
      faces.push({ pts: ring0, n: [0, 0, -1], c: color });
      faces.push({ pts: ring1, n: [0, 0, 1], c: color });
    }

    var light = [-0.5, 0.55, -0.75];
    var ll = Math.sqrt(light[0] * light[0] + light[1] * light[1] + light[2] * light[2]);
    light = [light[0] / ll, light[1] / ll, light[2] / ll];

    faces.forEach(function (f) {
      var pr = f.pts.map(proj);
      f.depth = pr.reduce(function (s, q) { return s + q.z; }, 0) / pr.length;
      f.pr = pr;
      var nl = f.n[0] * light[0] + f.n[1] * light[1] + f.n[2] * light[2];
      f.shade = Math.max(0.35, Math.min(1, 0.55 + 0.45 * nl));
    });
    faces.sort(function (a, b) { return a.depth - b.depth; });   // 远 → 近

    faces.forEach(function (f) {
      ctx.beginPath();
      ctx.moveTo(f.pr[0].x, f.pr[0].y);
      for (var k = 1; k < f.pr.length; k++) ctx.lineTo(f.pr[k].x, f.pr[k].y);
      ctx.closePath();
      var rr = parseInt(f.c.substr(1, 2), 16), gg = parseInt(f.c.substr(3, 2), 16), bb = parseInt(f.c.substr(5, 2), 16);
      ctx.fillStyle = 'rgb(' + Math.round(rr * f.shade) + ',' + Math.round(gg * f.shade) + ',' + Math.round(bb * f.shade) + ')';
      ctx.fill();
      ctx.strokeStyle = 'rgba(0,0,0,0.12)';
      ctx.stroke();
    });
  }

  (function () {
    var canvas = $('viewer3d');
    canvas.addEventListener('mousedown', function (e) {
      v3d.dragging = true; v3d.lastX = e.clientX; v3d.lastY = e.clientY;
    });
    window.addEventListener('mousemove', function (e) {
      if (!v3d.dragging) return;
      v3d.theta += (e.clientX - v3d.lastX) * 0.01;
      v3d.phi = Math.max(-1.4, Math.min(1.4, v3d.phi + (e.clientY - v3d.lastY) * 0.01));
      v3d.lastX = e.clientX; v3d.lastY = e.clientY;
      draw3D();
    });
    window.addEventListener('mouseup', function () { v3d.dragging = false; });
    canvas.addEventListener('wheel', function (e) {
      e.preventDefault();
      v3d.zoom = Math.max(0.3, Math.min(5, v3d.zoom * (e.deltaY > 0 ? 0.9 : 1.1)));
      draw3D();
    });
  })();

  // ---------- 面板拖拽调宽 (目录树/参数面板, 与桌面版一致) ----------
  function makeSplitter(id, prevSel, minW, maxW) {
    var el = $(id);
    var prev = document.querySelector(prevSel);
    el.addEventListener('mousedown', function (e) {
      e.preventDefault();
      el.classList.add('dragging');
      var startX = e.clientX, startW = prev.offsetWidth;
      function move(ev) {
        var w = Math.max(minW, Math.min(maxW, startW + ev.clientX - startX));
        prev.style.width = w + 'px';
        prev.style.flex = 'none';
      }
      function up() {
        el.classList.remove('dragging');
        document.removeEventListener('mousemove', move);
        document.removeEventListener('mouseup', up);
      }
      document.addEventListener('mousemove', move);
      document.addEventListener('mouseup', up);
    });
  }
  makeSplitter('split1', '.tree', 150, 420);
  makeSplitter('split2', '.params', 220, 520);
  makeSplitter('split3', '.hsplit-left', 260, 900);   // 3D/尺寸图 分栏分隔条 (同页内拖拽)

  // ---------- 初始化 ----------
  // 下载点击反馈: 立即提示 + 3 秒内防重复点击 (此前无反馈, 用户多次点击导致重复下载)
  var lastDl = 0;
  $('btnDownload').addEventListener('click', function () {
    var now = Date.now();
    if (now - lastDl < 3000) return;
    lastDl = now;
    var m = $('btnDownload').dataset.model || '';
    setStatus(t('正在准备下载 ' + m + '.step ...', 'Preparing ' + m + '.step ...'));
    setTimeout(function () {
      setStatus(t('选型完成，点击下载 STEP', 'Ready - click to download STEP'));
    }, 5000);
  });
  $('btnSearch').onclick = doSearch;
  $('searchBox').addEventListener('keydown', function (e) { if (e.key === 'Enter') doSearch(); });
  $('btnLang').onclick = function () { state.lang = state.lang === 'zh' ? 'en' : 'zh'; applyLang(); };
  document.querySelectorAll('.tab').forEach(function (el) {
    el.onclick = function () { switchTab(el.getAttribute('data-tab')); };
  });
  load();
  startServicePoll();

  // ---------- 生成服务状态轮询 (底部信息栏圆球: 绿=就绪, 红=故障; 文字"服务器状态·状态") ----------
  function startServicePoll() {
    var el = $('srvStatus');
    if (!el) return;
    var txtEl = $('srvStatusText');
    function setState(cls, word) {
      el.className = 'srv-status ' + cls;
      el.title = word;
      if (txtEl) txtEl.textContent = t('服务器状态', 'Server') + ' · ' + word;
    }
    function tick() {
      var api = genApi();
      if (!api) { setState('down', t('未配置', 'Not configured')); return; }
      fetch(api + '/health', { method: 'GET' })
        .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
        .then(function (h) {
          if (!h.nxOk) setState('down', t('NX 未就绪', 'NX not ready'));
          else if (h.workerAlive) setState('ready', t('就绪', 'Ready'));
          else if (h.queued > 0 || h.running > 0) setState('no-worker', t('等待 worker', 'Waiting'));
          else setState('idle', t('就绪', 'Ready'));
        })
        .catch(function () { setState('down', t('不可达', 'Unreachable')); });
    }
    tick();
    setInterval(tick, 5000);
  }
})();
