diff --git a/web/app.js b/web/app.js index 8250476..ca8dfe0 100644 --- a/web/app.js +++ b/web/app.js @@ -246,6 +246,7 @@ setStatus(t('目录加载完成,点击缩略图或左侧系列开始选型', 'Catalog loaded - click a thumbnail or pick a series')); if (location.search.indexOf('selftest=1') >= 0) runSelfTest(); + applyDeepLink(); // URL 参数深度链接 (?series= / ?model= / ?embed=1) }) .catch(function (e) { setStatus(t('加载 catalog.json 失败: ', 'Failed to load catalog.json: ') + e.message + @@ -253,6 +254,67 @@ }); } + // ---------- 深度链接 (供 Odoo 产品页 iframe 调用) ---------- + // ?series=UCBM 直接打开某系列(型号编码前缀或系列 code) + // ?model=UCBM32AK010 直接定位到某个具体型号(复用 jumpToModel) + // ?embed=1 嵌入模式:隐藏自身顶栏/底栏,让 iframe 里更干净 + // ?nav=0 隐藏左侧系列树(产品页已定系列,不需要再让用户挑) + // 说明: 本函数只在 catalog.json 加载完成后调用一次(见 load()) + function applyDeepLink() { + var q = new URLSearchParams(location.search || ''); + + // 嵌入模式:隐藏工具自身的 header/footer,避免在 Odoo 页面里出现"页中页" + if (q.get('embed') === '1') { + document.body.classList.add('embed-mode'); + var st = $('status'); + if (st) st.classList.add('hidden'); + var hd = document.querySelector('header'); + if (hd) hd.classList.add('hidden'); + } + + // 隐藏左侧系列树(产品页场景) + if (q.get('nav') === '0') { + document.body.classList.add('nav-off'); + } + + // 语言: ?lang=en + var lg = q.get('lang'); + if (lg === 'en' || lg === 'zh') { + state.lang = lg; + renderTree(); + renderHome(); + renderCrossRef(); + } + + // 定位型号(优先级最高) + var model = q.get('model'); + if (model) { jumpToModel(model); return; } + + // 打开系列:支持传系列 code(KC)或型号前缀(UCBM32) + var want = q.get('series'); + if (!want) return; + var up = String(want).toUpperCase(); + var hit = null; + (state.catalog.series || []).forEach(function (s) { + if (String(s.code).toUpperCase() === up) hit = s; + }); + if (!hit) { + // 退一步:按型号编码前缀匹配(如 UCBM32 → 系列 UCBM) + (state.catalog.series || []).forEach(function (s) { + if (hit) return; + (s.variants || []).some(function (v) { + if (String(v.modelCode).toUpperCase().indexOf(up) === 0) { hit = s; return true; } + return false; + }); + }); + } + if (hit) { + selectSeries(hit); + } else { + setStatus(t('未找到系列: ', 'Series not found: ') + want); + } + } + // ---------- 目录树 ---------- // 大类图标 (内联 SVG, 24x24 线性, stroke=currentColor → 随主题色/选中态自动变色) var CAT_ICONS = { @@ -588,11 +650,148 @@ im.src = a.path; im.alt = a.path; im.draggable = false; // 阻止原生图片拖拽 (见下方全局 dragstart 拦截) + im.title = t('点击放大', 'Click to enlarge'); + // 点击放大: 同一手册页的多张图可左右切换 (← → 键) + im.onclick = function () { openImageViewer(pages, idx); }; mw.appendChild(cap); mw.appendChild(im); }); } + // ---------- 手册页图片放大 (单击弹窗, 支持多页左右切换) ---------- + var zview = { el: null, pages: [], idx: 0 }; + + function openImageViewer(pages, idx) { + closeImageViewer(); // 先清掉可能存在的旧弹窗 + zview.pages = pages || []; + zview.idx = idx || 0; + + var ov = document.createElement('div'); + ov.className = 'img-zoom-overlay'; + + var im = document.createElement('img'); + im.src = zview.pages[zview.idx].path; + im.alt = zview.pages[zview.idx].path; + im.draggable = false; + + var btn = document.createElement('button'); + btn.className = 'img-zoom-close'; + btn.type = 'button'; + btn.innerHTML = '×'; + btn.setAttribute('aria-label', t('关闭', 'Close')); + btn.onclick = function (ev) { ev.stopPropagation(); closeImageViewer(); }; + + var cap = document.createElement('div'); + cap.className = 'img-zoom-cap'; + cap.textContent = t('手册页', 'Manual page') + ' ' + (zview.idx + 1) + '/' + zview.pages.length + + (zview.pages.length > 1 ? t(' (← → 切换,Esc 关闭)', ' (← → to switch, Esc to close)') : ''); + + ov.appendChild(im); + ov.appendChild(btn); + ov.appendChild(cap); + // 点遮罩空白处关闭(点图片本身不关) + ov.onclick = function (ev) { if (ev.target === ov) closeImageViewer(); }; + + document.body.appendChild(ov); + zview.el = ov; + document.addEventListener('keydown', onZoomKey); + } + + function onZoomKey(ev) { + if (!zview.el) return; + if (ev.key === 'Escape') { closeImageViewer(); } + else if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') { + if (zview.pages.length < 2) return; + var d = ev.key === 'ArrowRight' ? 1 : -1; + zview.idx = (zview.idx + d + zview.pages.length) % zview.pages.length; + var im = zview.el.querySelector('img'); + im.src = zview.pages[zview.idx].path; + var cap = zview.el.querySelector('.img-zoom-cap'); + if (cap) cap.textContent = t('手册页', 'Manual page') + ' ' + (zview.idx + 1) + '/' + zview.pages.length + + t(' (← → 切换,Esc 关闭)', ' (← → to switch, Esc to close)'); + } + } + + function closeImageViewer() { + if (zview.el && zview.el.parentNode) zview.el.parentNode.removeChild(zview.el); + zview.el = null; + document.removeEventListener('keydown', onZoomKey); + } + + // ---------- 数据表 PDF 弹窗预览 ---------- + // 用浏览器内置 PDF 阅读器(iframe),弹窗内保留下载入口。 + // 移动端部分浏览器(尤其 iOS Safari)不内联显示 PDF,所以在弹窗里同时给下载按钮兜底。 + var pview = { el: null }; + + function openPdfViewer(url, fname) { + closePdfViewer(); + + var ov = document.createElement('div'); + ov.className = 'pdf-zoom-overlay'; + + var box = document.createElement('div'); + box.className = 'pdf-zoom-box'; + + var head = document.createElement('div'); + head.className = 'pdf-zoom-head'; + + var title = document.createElement('span'); + title.className = 'pdf-zoom-title'; + title.textContent = fname; + + var dl = document.createElement('a'); + dl.className = 'pdf-zoom-dl'; + dl.href = url; + dl.setAttribute('download', fname); + // 内联 SVG 下载图标(工具无图标字体) + dl.innerHTML = + '' + + ' ' + + t('下载', 'Download'); + + var btn = document.createElement('button'); + btn.className = 'img-zoom-close pdf-zoom-close'; + btn.type = 'button'; + btn.innerHTML = '×'; + btn.setAttribute('aria-label', t('关闭', 'Close')); + btn.onclick = function (ev) { ev.stopPropagation(); closePdfViewer(); }; + + head.appendChild(title); + head.appendChild(dl); + head.appendChild(btn); + + var fr = document.createElement('iframe'); + fr.className = 'pdf-zoom-frame'; + fr.src = url + '#toolbar=1&view=FitH'; + fr.setAttribute('title', fname); + + box.appendChild(head); + box.appendChild(fr); + ov.appendChild(box); + // 点遮罩空白处关闭(点弹窗内部不关) + ov.onclick = function (ev) { if (ev.target === ov) closePdfViewer(); }; + + document.body.appendChild(ov); + pview.el = ov; + document.addEventListener('keydown', onPdfKey); + } + + function onPdfKey(ev) { + if (pview.el && ev.key === 'Escape') closePdfViewer(); + } + + function closePdfViewer() { + if (pview.el && pview.el.parentNode) { + // 先清空 iframe.src 再移除,避免某些浏览器里 PDF 后台继续加载 + var fr = pview.el.querySelector('iframe'); + if (fr) fr.src = 'about:blank'; + pview.el.parentNode.removeChild(pview.el); + } + pview.el = null; + document.removeEventListener('keydown', onPdfKey); + } + // 空字符串是合法枚举值 (如 磁石=""=无磁石), 用哨兵值避免与"未选择"混淆 function optValue(v) { return str(v) === '' ? '__EMPTY__' : str(v); } @@ -772,8 +971,15 @@ if (a.kind === 'dimDrawing' && (a.model === v.modelCode || att === null)) att = a; }); if (att) { - $('dimImage').src = att.path; - $('dimImage').classList.remove('hidden'); + var dimEl = $('dimImage'); + dimEl.src = att.path; + dimEl.classList.remove('hidden'); + dimEl.title = t('点击放大', 'Click to enlarge'); + dimEl.style.cursor = 'zoom-in'; + // 技术文档(尺寸图)同样支持点击弹窗放大 + dimEl.onclick = function () { + openImageViewer([{ path: att.path }], 0); + }; $('dimHint').classList.add('hidden'); } // 数据表 @@ -781,9 +987,30 @@ 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'); + // 数据表 PDF: 点击弹窗预览(用浏览器内置 PDF 阅读器),弹窗内保留下载入口。 + // 原来是直接下载(2026-08-24),用户反馈希望先预览(2026-09-16)。 + // ⚠️ 不能沿用 :那会直接下载。改成 button + 弹窗。 + ds.removeAttribute('href'); + ds.removeAttribute('download'); + ds.style.cursor = 'pointer'; + ds.title = t('点击预览', 'Click to preview'); + // 带预览图标,区别于纯下载按钮(用户要求能一眼看出可预览)。 + // ⚠️ 工具没有图标字体(只带 JSZip),所以用**内联 SVG**(与分类图标同做法)。 + ds.innerHTML = + '' + + '' + + '' + + '' + + '' + t('数据表 PDF', 'Datasheet PDF') + '' + + '' + + '' + + ''; + ds.onclick = function (ev) { + ev.preventDefault(); + openPdfViewer(dsAtt.path, dsAtt.path.split('/').pop() || 'datasheet.pdf'); + }; ds.classList.remove('hidden'); } @@ -1508,7 +1735,14 @@ $('cartDownload').textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)'); renderCartBadge(); $('btnDownload').textContent = t('下载 STEP', 'Download STEP'); - $('btnDatasheet').textContent = t('数据表 PDF', 'Datasheet PDF'); + // ⚠️ 数据表按钮带图标(pdf + 放大镜),不能用 textContent 整段替换, + // 否则切语言时图标会被抹掉。只改文字节点。 + var dsBtn = $('btnDatasheet'); + if (dsBtn) { + var dsTxt = dsBtn.querySelector('.ds-txt'); + if (dsTxt) dsTxt.textContent = t('数据表 PDF', 'Datasheet PDF'); + else dsBtn.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'); @@ -1576,10 +1810,14 @@ } // ---- 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'; + // 部署自适应: 原来硬编码 '/viewer/STEPViewer/index.html' + 站点根绝对路径 '/step/…', + // 一旦部署到子路径(如 Odoo 的 /catalog/)就会 404。 + // 改为从当前页面 URL 推导根路径,本地联调 / 生产子路径 / 根路径都能用。 + // 例: 页面 /catalog/index.html → BASE_DIR = '/catalog' + var BASE_DIR = location.pathname.replace(/\/[^/]*$/, ''); // 去掉文件名, 得 '/catalog' 或 '' + // 查看器位置: 优先同目录下的 viewer/STEPViewer(生产整站一起部署时就是这个) + // 回退到 /viewer/STEPViewer(本地 serve-web.ps1 -ViewerRoot 联调用) + var VIEWER_BASE = BASE_DIR + '/viewer/STEPViewer/index.html'; function load3D(variant) { var frame = $('viewerFrame'); @@ -1588,8 +1826,8 @@ 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'; + // 模型路径同样要带 BASE_DIR 前缀(原为站点根 '/' + step) + frame.src = VIEWER_BASE + '?file=' + encodeURIComponent(BASE_DIR + '/' + variant.step) + '&embed=1&bg=grey&menu=min'; if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer); // 查看器不可用 (404/离线) → 回退内置 Canvas 渲染 v3d.fallbackTimer = setTimeout(function () { diff --git a/web/index.html b/web/index.html index f69b1a1..5ceefd7 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ OneBot数模下载 - +
@@ -43,25 +43,26 @@
-
- - - - -