v2.4: 网页版适配 Odoo 网站嵌入
- URL 深度链接:?series= 打开系列 / ?model= 定位型号 / ?embed=1 隐藏自身顶栏 / ?nav=0 隐藏侧边栏 / ?lang= 切语言 - 路径自适应:VIEWER_BASE 与模型路径从 location.pathname 推导,支持子路径部署(原来硬编码 /viewer/ 和 /step/ 会在子路径下 404) - embed 模式样式:隐藏 header/footer,让 iframe 内更干净 - 手册页图片点击放大:全屏弹窗,← → 翻页,Esc/点遮罩关闭 - 数据表 PDF 弹窗预览:原为直接下载,改为内联预览(弹窗内保留下载入口) - 数据表按钮加内联 SVG 图标(PDF + 放大镜),区别于纯下载按钮 - 隐藏「返回系列总览」按钮(嵌入模式下属多余) 注:本次改动服务于 OneBot 官网产品页的选型 tab 嵌入。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
262
web/app.js
262
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 =
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" '
|
||||
+ 'stroke-linecap="round" stroke-linejoin="round" style="width:13px;height:13px;vertical-align:-2px">'
|
||||
+ '<path d="M12 3v12"/><path d="m7 11 5 5 5-5"/><path d="M4 20h16"/></svg> '
|
||||
+ 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)。
|
||||
// ⚠️ 不能沿用 <a download>:那会直接下载。改成 button + 弹窗。
|
||||
ds.removeAttribute('href');
|
||||
ds.removeAttribute('download');
|
||||
ds.style.cursor = 'pointer';
|
||||
ds.title = t('点击预览', 'Click to preview');
|
||||
// 带预览图标,区别于纯下载按钮(用户要求能一眼看出可预览)。
|
||||
// ⚠️ 工具没有图标字体(只带 JSZip),所以用**内联 SVG**(与分类图标同做法)。
|
||||
ds.innerHTML =
|
||||
'<svg class="ds-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
|
||||
+ 'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">'
|
||||
+ '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
|
||||
+ '<path d="M14 2v6h6"/><path d="M8 13h2.5a1.5 1.5 0 0 1 0 3H8v-6"/>'
|
||||
+ '</svg>'
|
||||
+ '<span class="ds-txt">' + t('数据表 PDF', 'Datasheet PDF') + '</span>'
|
||||
+ '<svg class="ds-ico ds-ico-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" '
|
||||
+ 'stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">'
|
||||
+ '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5M11 8v6M8 11h6"/>'
|
||||
+ '</svg>';
|
||||
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 () {
|
||||
|
||||
Reference in New Issue
Block a user