/* 欧霓博在线目录 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, '&').replace(//g, '>').replace(/"/g, '"'); } 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 = ''; 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) + '' + t('非标档位(无标准数模)', 'Non-standard (no standard model)') + ''; 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 = '