1 Commits
v2.3 ... main

Author SHA1 Message Date
wangruiguo
f343b26444 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>
2026-09-17 02:57:34 +08:00
3 changed files with 379 additions and 27 deletions

View File

@@ -246,6 +246,7 @@
setStatus(t('目录加载完成,点击缩略图或左侧系列开始选型', setStatus(t('目录加载完成,点击缩略图或左侧系列开始选型',
'Catalog loaded - click a thumbnail or pick a series')); 'Catalog loaded - click a thumbnail or pick a series'));
if (location.search.indexOf('selftest=1') >= 0) runSelfTest(); if (location.search.indexOf('selftest=1') >= 0) runSelfTest();
applyDeepLink(); // URL 参数深度链接 (?series= / ?model= / ?embed=1)
}) })
.catch(function (e) { .catch(function (e) {
setStatus(t('加载 catalog.json 失败: ', 'Failed to load catalog.json: ') + e.message + 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 → 随主题色/选中态自动变色) // 大类图标 (内联 SVG, 24x24 线性, stroke=currentColor → 随主题色/选中态自动变色)
var CAT_ICONS = { var CAT_ICONS = {
@@ -588,11 +650,148 @@
im.src = a.path; im.src = a.path;
im.alt = a.path; im.alt = a.path;
im.draggable = false; // 阻止原生图片拖拽 (见下方全局 dragstart 拦截) im.draggable = false; // 阻止原生图片拖拽 (见下方全局 dragstart 拦截)
im.title = t('点击放大', 'Click to enlarge');
// 点击放大: 同一手册页的多张图可左右切换 (← → 键)
im.onclick = function () { openImageViewer(pages, idx); };
mw.appendChild(cap); mw.appendChild(cap);
mw.appendChild(im); 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 = '&times;';
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 = '&times;';
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); } 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 (a.kind === 'dimDrawing' && (a.model === v.modelCode || att === null)) att = a;
}); });
if (att) { if (att) {
$('dimImage').src = att.path; var dimEl = $('dimImage');
$('dimImage').classList.remove('hidden'); 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'); $('dimHint').classList.add('hidden');
} }
// 数据表 // 数据表
@@ -781,9 +987,30 @@
return a.kind === 'datasheet' && a.lang === state.lang; return a.kind === 'datasheet' && a.lang === state.lang;
})[0] || (s.attachments || []).filter(function (a) { return a.kind === 'datasheet'; })[0]; })[0] || (s.attachments || []).filter(function (a) { return a.kind === 'datasheet'; })[0];
if (dsAtt) { if (dsAtt) {
// 下载行为 (2026-08-24): 点按钮直接下载 PDF, 与 STEP 下载一致 // 数据表 PDF: 点击弹窗预览(用浏览器内置 PDF 阅读器),弹窗内保留下载入口。
ds.href = dsAtt.path; // 原来是直接下载(2026-08-24),用户反馈希望先预览(2026-09-16)。
ds.setAttribute('download', dsAtt.path.split('/').pop() || 'datasheet.pdf'); // ⚠️ 不能沿用 <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'); ds.classList.remove('hidden');
} }
@@ -1508,7 +1735,14 @@
$('cartDownload').textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)'); $('cartDownload').textContent = t('下载全部 (zip + BOM)', 'Download All (zip + BOM)');
renderCartBadge(); renderCartBadge();
$('btnDownload').textContent = t('下载 STEP', 'Download STEP'); $('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'); $('dimHint').textContent = t('选型完成后显示尺寸图', 'Dimension drawing appears after selection');
var tabs = document.querySelectorAll('.tab'); var tabs = document.querySelectorAll('.tab');
tabs[0].textContent = t('3D / 尺寸图', '3D / Dimension'); tabs[0].textContent = t('3D / 尺寸图', '3D / Dimension');
@@ -1576,10 +1810,14 @@
} }
// ---- 3D 查看器联动 (新迪 STEPViewer: occt-import-js + three.js, 支持真实 STEP) ---- // ---- 3D 查看器联动 (新迪 STEPViewer: occt-import-js + three.js, 支持真实 STEP) ----
// 本地联调: /viewer/ 前缀由 serve-web.ps1 -ViewerRoot 映射到新迪 dev 目录 // 部署自适应: 原来硬编码 '/viewer/STEPViewer/index.html' + 站点根绝对路径 '/step/…',
// 生产同域部署 (3d.ruiguo.wang): 改为 '/STEPViewer/index.html' // 一旦部署到子路径(如 Odoo 的 /catalog/)就会 404。
// (模型文件已同步到 STEPViewer/step/ 目录, file 参数 = 目录内相对路径) // 改为从当前页面 URL 推导根路径,本地联调 / 生产子路径 / 根路径都能用。
var VIEWER_BASE = '/viewer/STEPViewer/index.html'; // 例: 页面 /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) { function load3D(variant) {
var frame = $('viewerFrame'); var frame = $('viewerFrame');
@@ -1588,8 +1826,8 @@
hint.classList.add('hidden'); hint.classList.add('hidden');
frame.classList.remove('hidden'); frame.classList.remove('hidden');
canvas.classList.add('hidden'); canvas.classList.add('hidden');
// 站点根绝对路径 (/step/...): 模型始终从站点 step\ 目录取, 查看器无需自带模型副本 (踩坑 #20) // 模型路径同样要带 BASE_DIR 前缀(原为站点根 '/' + step)
frame.src = VIEWER_BASE + '?file=' + encodeURIComponent('/' + variant.step) + '&embed=1&bg=grey&menu=min'; frame.src = VIEWER_BASE + '?file=' + encodeURIComponent(BASE_DIR + '/' + variant.step) + '&embed=1&bg=grey&menu=min';
if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer); if (v3d.fallbackTimer) clearTimeout(v3d.fallbackTimer);
// 查看器不可用 (404/离线) → 回退内置 Canvas 渲染 // 查看器不可用 (404/离线) → 回退内置 Canvas 渲染
v3d.fallbackTimer = setTimeout(function () { v3d.fallbackTimer = setTimeout(function () {

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OneBot数模下载</title> <title>OneBot数模下载</title>
<link rel="stylesheet" href="style.css?v=11"> <link rel="stylesheet" href="style.css?v=12">
</head> </head>
<body> <body>
<header> <header>
@@ -43,25 +43,26 @@
<div class="sku-hint" id="skuHint"></div> <div class="sku-hint" id="skuHint"></div>
</div> </div>
<div id="paramPanel"></div> <div id="paramPanel"></div>
<div id="actions">
<a id="btnDownload" class="btn primary hidden" download>下载 STEP</a>
<select id="btnGenFmt" class="hidden" title="下载格式 (仅非标档位在线生成)">
<option value="zip">全格式 zip</option>
<option value="step">STEP</option>
<option value="iges">IGES</option>
</select>
<a id="btnDatasheet" class="btn hidden">下载 PDF</a>
<button id="btnAddCart" class="btn hidden">加入清单</button>
</div>
</section> </section>
<div id="split2" class="split hidden" title="拖拽调整宽度"></div> <div id="split2" class="split hidden" title="拖拽调整宽度"></div>
<section id="content" class="content hidden"> <section id="content" class="content hidden">
<div class="tabs"> <div class="tabs" id="tabsBar">
<button class="tab active" data-tab="3d">3D / 尺寸图</button> <button class="tab active" data-tab="3d">3D / 尺寸图</button>
<button class="tab" data-tab="table">参数表</button> <button class="tab" data-tab="table">参数表</button>
<button class="tab" data-tab="search">搜索结果</button> <button class="tab" data-tab="search">搜索结果</button>
<button class="tab" data-tab="cross">平替对照</button> <button class="tab" data-tab="cross">平替对照</button>
<span class="tabs-spacer"></span>
<div id="actions">
<a id="btnDownload" class="btn primary hidden" download>下载 STEP</a>
<select id="btnGenFmt" class="hidden" title="下载格式 (仅非标档位在线生成)">
<option value="zip">全格式 zip</option>
<option value="step">STEP</option>
<option value="iges">IGES</option>
</select>
<a id="btnDatasheet" class="btn hidden">下载 PDF</a>
<button id="btnAddCart" class="btn hidden">加入清单</button>
</div>
</div> </div>
<div class="content-back"> <div class="content-back">
<button id="btnBackHome" class="btn">← 返回系列总览</button> <button id="btnBackHome" class="btn">← 返回系列总览</button>
@@ -122,6 +123,6 @@
<script src="libs/jszip.min.js?v=1"></script> <script src="libs/jszip.min.js?v=1"></script>
<script src="config.js?v=1"></script> <script src="config.js?v=1"></script>
<script src="app.js?v=18"></script> <script src="app.js?v=19"></script>
</body> </body>
</html> </html>

View File

@@ -265,7 +265,16 @@ main { flex: 1; display: flex; min-height: 0; }
.param-row select:focus, .param-input:focus { .param-row select:focus, .param-input:focus {
border-color: var(--primary-500); box-shadow: 0 0 0 3px rgba(20,184,166,.15); border-color: var(--primary-500); box-shadow: 0 0 0 3px rgba(20,184,166,.15);
} }
#actions { margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap; } /* #actions 原在左侧参数面板底部,现移到 tab 行右侧(与 3D/尺寸图 等同一行)。
元素 id 未变,JS 按 id 取用,位置移动不影响逻辑。 */
#actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* tab 行的弹性占位:把 #actions 推到最右 */
.tabs-spacer { flex: 1; min-width: 8px; }
/* 窄屏时按钮换行到下一行,避免挤压 tab */
@media (max-width: 1100px) {
.tabs { flex-wrap: wrap; }
.tabs-spacer { flex-basis: 100%; height: 0; }
}
/* ============================================================ 零件号实时条 (DESTACO 风) /* ============================================================ 零件号实时条 (DESTACO 风)
* 边选边拼: 已确定部分实心高亮, 未定部分虚化占位。 */ * 边选边拼: 已确定部分实心高亮, 未定部分虚化占位。 */
@@ -387,10 +396,114 @@ main { flex: 1; display: flex; min-height: 0; }
.hidden { display: none !important; } .hidden { display: none !important; }
/* ------------------------------------------------------------ 嵌入模式
供 Odoo 产品页 iframe 使用 (?embed=1):隐藏工具自身的顶栏与底栏,
避免在网站页面里出现"页中页",同时把高度让给内容区。
由 app.js 的 applyDeepLink() 给 body 加 .embed-mode 触发。 */
body.embed-mode header,
body.embed-mode footer { display: none !important; }
body.embed-mode { height: 100vh; }
/* 隐藏模式 (?nav=0):嵌入 Odoo 产品页时用。
产品页已经决定了是哪个系列,左侧系列树、分隔条、「返回系列总览」纯属多余,
隐藏后让参数面板 + 内容区铺满整宽,观感更干净。 */
body.nav-off #tree,
body.nav-off #split1,
body.nav-off .content-back { display: none !important; }
/* 隐藏侧栏后,参数面板适当加宽(原来 330px 是因为旁边还有树+主区抢空间) */
body.nav-off .params { width: 300px; }
/* ------------------------------------------------------------ 内容区 / 标签页 */ /* ------------------------------------------------------------ 内容区 / 标签页 */
.content { flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 14px; } .content { flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 14px; }
.tabs { display: flex; gap: 6px; flex: none; } .tabs { display: flex; gap: 6px; flex: none; align-items: center; }
.content-back { flex: none; margin: 10px 0 0; } .content-back { flex: none; margin: 10px 0 0; }
/* ------------------------------------------------------------ 手册页图片放大
点击手册页图片 → 全屏遮罩显示大图。点遮罩任意处 / 按 Esc 关闭。
用 position:fixed 覆盖整个视口(含 iframe 内嵌场景)。 */
.manual-img { cursor: zoom-in; }
/* 数据表按钮图标(内联 SVG,工具无图标字体) */
.ds-ico { width: 15px; height: 15px; flex: none; vertical-align: -3px; }
.ds-ico-eye { opacity: .65; }
#btnDatasheet { display: inline-flex; align-items: center; gap: 6px; }
#btnDatasheet:hover .ds-ico-eye { opacity: 1; }
#btnDatasheet .ds-txt { white-space: nowrap; }
.img-zoom-overlay {
position: fixed; inset: 0; z-index: 900;
background: rgba(17, 24, 39, .88);
display: flex; align-items: center; justify-content: center;
padding: 24px; cursor: zoom-out;
animation: imgZoomIn .16s ease-out;
}
@keyframes imgZoomIn { from { opacity: 0; } to { opacity: 1; } }
.img-zoom-overlay img {
max-width: 100%; max-height: 100%;
object-fit: contain; border-radius: 6px;
box-shadow: 0 12px 48px rgba(0, 0, 0, .5);
}
.img-zoom-close {
position: absolute; top: 14px; right: 18px;
width: 40px; height: 40px; border: 0; border-radius: 50%;
background: rgba(255, 255, 255, .16); color: #fff;
font-size: 22px; line-height: 1; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background .18s;
}
.img-zoom-close:hover { background: rgba(255, 255, 255, .3); }
.img-zoom-cap {
position: absolute; bottom: 14px; left: 0; right: 0;
text-align: center; color: rgba(255, 255, 255, .75); font-size: 12.5px;
pointer-events: none;
}
/* ------------------------------------------------------------ 数据表 PDF 弹窗预览
点击「数据表」→ 弹窗内用浏览器内置 PDF 阅读器打开,标题栏含下载入口。 */
.pdf-zoom-overlay {
position: fixed; inset: 0; z-index: 910;
background: rgba(17, 24, 39, .72);
display: flex; align-items: center; justify-content: center;
padding: 24px;
animation: imgZoomIn .16s ease-out;
}
.pdf-zoom-box {
width: min(1100px, 100%); height: min(92vh, 100%);
display: flex; flex-direction: column;
background: var(--surface, #fff);
border-radius: 10px; overflow: hidden;
box-shadow: 0 16px 56px rgba(0, 0, 0, .45);
}
.pdf-zoom-head {
flex: none; display: flex; align-items: center; gap: 12px;
padding: 10px 12px 10px 16px;
border-bottom: 1px solid var(--border, #e5e7eb);
background: var(--surface, #fff);
}
.pdf-zoom-title {
flex: 1; font-size: 13px; font-weight: 600; color: var(--gray-900, #111827);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pdf-zoom-dl {
flex: none; font-size: 12.5px; text-decoration: none;
padding: 6px 14px; border-radius: 6px;
background: var(--primary, #14b8a6); color: #fff;
transition: filter .18s;
}
.pdf-zoom-dl:hover { filter: brightness(.94); }
.pdf-zoom-close {
position: static; flex: none;
width: 32px; height: 32px; font-size: 18px;
background: var(--gray-100, #f3f4f6); color: var(--gray-700, #374151);
}
.pdf-zoom-close:hover { background: var(--gray-200, #e5e7eb); }
.pdf-zoom-frame {
flex: 1; width: 100%; border: 0; background: #525659;
}
@media (max-width: 767.98px) {
.pdf-zoom-overlay { padding: 0; }
.pdf-zoom-box { width: 100%; height: 100%; border-radius: 0; }
}
.tab { .tab {
height: 36px; padding: 0 16px; border: 1px solid var(--border); height: 36px; padding: 0 16px; border: 1px solid var(--border);
border-radius: var(--radius-sm); background: var(--surface); border-radius: var(--radius-sm); background: var(--surface);