502 lines
25 KiB
HTML
502 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>图纸模型在线查看</title>
|
||
<link rel="icon" href="DWGViewer/favicon.ico">
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden;
|
||
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif; font-size: 14px; color: #333; }
|
||
#frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: none; }
|
||
|
||
/* 落地页:没有任何文件时的打开界面 */
|
||
#landing {
|
||
position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center;
|
||
justify-content: flex-start; overflow: auto; padding: 40px 20px 60px;
|
||
background: linear-gradient(180deg, #f7fbff 0%, #eef5fc 100%); gap: 18px;
|
||
}
|
||
.logo { font-size: 30px; font-weight: 600; color: #2b9ae8; letter-spacing: 2px; }
|
||
.sub { color: #8aa0b4; font-size: 14px; }
|
||
.open-btn {
|
||
background: #2b9ae8; color: #fff; border: none; border-radius: 6px; padding: 13px 34px;
|
||
font-size: 16px; cursor: pointer; box-shadow: 0 3px 10px rgba(43,154,232,.35);
|
||
}
|
||
.open-btn:hover { background: #2389cf; }
|
||
.hint { color: #9fb2c4; font-size: 13px; margin-top: 10px; line-height: 2; text-align: center; }
|
||
.hint b { color: #6b8498; font-weight: 600; }
|
||
.drop-box {
|
||
border: 2px dashed #b7d4ec; border-radius: 12px; background: rgba(255,255,255,.6);
|
||
padding: 26px 70px; font-size: 15px; color: #2b9ae8; text-align: center; pointer-events: none;
|
||
}
|
||
#landing.dragging .drop-box { border-color: #2b9ae8; background: #fff; }
|
||
|
||
/* 2D/3D 数模库 */
|
||
.lib-panel {
|
||
margin-top: 26px; width: 100%; max-width: 1000px; background: #fff; border-radius: 10px;
|
||
box-shadow: 0 2px 12px rgba(30,60,90,.08); padding: 16px 18px 18px; text-align: left;
|
||
}
|
||
.lib-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
|
||
.lib-title { font-size: 16px; font-weight: 600; color: #2b5f8a; }
|
||
.lib-tip { color: #9fb2c4; font-size: 12px; flex: 1; min-width: 200px; }
|
||
.lib-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.lib-table th { background: #f2f7fc; color: #6b8498; font-weight: 600; text-align: left; padding: 8px 10px; border-bottom: 1px solid #e3ecf4; white-space: nowrap; }
|
||
.lib-table td { padding: 8px 10px; border-bottom: 1px solid #eef3f8; vertical-align: middle; }
|
||
.lib-table tr:hover td { background: #f8fbff; }
|
||
.lib-name, .lib-path, .lib-note { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.lib-name { max-width: 190px; }
|
||
.lib-path { color: #7a93a8; font-family: Consolas, monospace; font-size: 12px; max-width: 300px; }
|
||
.lib-note { color: #8aa0b4; max-width: 140px; }
|
||
.badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 12px; }
|
||
.badge-2d { background: #e8f5ff; color: #2b9ae8; }
|
||
.badge-3d { background: #eef9ef; color: #3aa85c; }
|
||
.small-btn {
|
||
border: 1px solid #cfe0ee; background: #fff; color: #2b6f9e; border-radius: 4px;
|
||
padding: 4px 10px; font-size: 12px; cursor: pointer; margin-left: 6px;
|
||
}
|
||
.small-btn:hover { border-color: #2b9ae8; color: #2b9ae8; }
|
||
.small-btn.primary { background: #2b9ae8; border-color: #2b9ae8; color: #fff; }
|
||
.small-btn.primary:hover { background: #2389cf; }
|
||
.small-btn.danger { color: #d9534f; border-color: #f0c9c7; }
|
||
.small-btn.danger:hover { border-color: #d9534f; }
|
||
.small-btn:disabled { opacity: .5; cursor: default; }
|
||
.lib-hint { margin-top: 10px; color: #c0392b; font-size: 12px; min-height: 16px; }
|
||
.lib-table input, .lib-table select { border: 1px solid #cfe0ee; border-radius: 4px; padding: 4px 6px; font-size: 12px; width: 100%; }
|
||
.lib-empty { color: #9fb2c4; text-align: center; padding: 18px 0; }
|
||
.lib-search { border: 1px solid #cfe0ee; border-radius: 4px; padding: 5px 10px; font-size: 12px; width: 210px; }
|
||
.lib-search:focus, .lib-filter:focus { outline: none; border-color: #2b9ae8; }
|
||
.lib-filter { border: 1px solid #cfe0ee; border-radius: 4px; padding: 5px 6px; font-size: 12px; }
|
||
.lib-scroll { overflow-x: auto; }
|
||
|
||
/* 手机适配(≤768px)—— 弹窗部分见文件末尾的媒体查询(须在 .modal-box 基础规则之后) */
|
||
@media (max-width: 768px) {
|
||
#landing { padding: 20px 8px 40px; gap: 12px; }
|
||
.logo { font-size: 22px; }
|
||
.sub { font-size: 12px; }
|
||
.drop-box { padding: 16px 20px; font-size: 13px; }
|
||
.hint { font-size: 12px; }
|
||
.lib-panel { margin-top: 14px; padding: 10px; }
|
||
.lib-tip { display: none; }
|
||
.lib-head { gap: 6px; }
|
||
.lib-search { width: 100%; flex: 1; }
|
||
.lib-table { font-size: 12px; min-width: 640px; } /* 窄屏横向滚动,不挤压列 */
|
||
}
|
||
|
||
/* 嵌入预览弹窗 */
|
||
.modal-backdrop {
|
||
position: fixed; inset: 0; background: rgba(20,40,60,.55); z-index: 50;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.modal-backdrop.hidden { display: none; }
|
||
.modal-box {
|
||
background: #fff; border-radius: 10px; width: min(960px, 92vw); height: min(720px, 90vh);
|
||
display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 10px 40px rgba(0,0,0,.3);
|
||
}
|
||
.modal-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid #e3ecf4; }
|
||
.modal-title { font-weight: 600; color: #2b5f8a; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.modal-close { border: none; background: none; font-size: 20px; color: #8aa0b4; cursor: pointer; padding: 0 6px; }
|
||
.modal-close:hover { color: #2b9ae8; }
|
||
.modal-frame { flex: 1; border: 0; width: 100%; }
|
||
.modal-foot { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-top: 1px solid #e3ecf4; }
|
||
.modal-foot input { flex: 1; border: 1px solid #cfe0ee; border-radius: 4px; padding: 6px 8px; font-size: 12px; color: #7a93a8; font-family: Consolas, monospace; }
|
||
|
||
/* 手机:弹窗全屏 */
|
||
@media (max-width: 768px) {
|
||
.modal-box { width: 100vw; height: 100dvh; border-radius: 0; }
|
||
#pvPath { display: none; }
|
||
.modal-foot { flex-wrap: wrap; }
|
||
.modal-foot input { min-width: 0; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<iframe id="frame" title="查看器"></iframe>
|
||
|
||
<div id="landing">
|
||
<div class="logo">图纸 · 模型 在线查看</div>
|
||
<div class="sub">DWG / DXF 图纸 · STEP 三维模型 —— 浏览器直接打开,无需转换</div>
|
||
<div class="drop-box" id="dropBox">把 DWG / DXF / STEP 文件拖到这里<br>或</div>
|
||
<button class="open-btn" id="btnOpen">选择文件</button>
|
||
<div class="hint">
|
||
同一入口自动识别格式:<b>2D</b> 图纸(.dwg / .dxf)→ 图纸查看器 · <b>3D</b> 模型(.step / .stp)→ 三维查看器<br>
|
||
也可以直接在地址栏带参数:<code>?file=图纸.dwg</code> 或 <code>?file=模型.stp</code>
|
||
</div>
|
||
|
||
<div class="lib-panel">
|
||
<div class="lib-head">
|
||
<span class="lib-title">2D / 3D 数模库</span>
|
||
<span class="lib-tip">上传过的文件自动收录 · 除「文件路径」外均可编辑 · 删除只删记录,不动服务器文件</span>
|
||
<input id="libSearch" type="text" class="lib-search" placeholder="🔍 搜索名称 / 路径 / 备注">
|
||
<select id="libTypeFilter" class="lib-filter">
|
||
<option value="">全部类型</option>
|
||
<option value="3d">仅 3D</option>
|
||
<option value="2d">仅 2D</option>
|
||
</select>
|
||
<button id="btnLibAdd" class="small-btn primary">新增记录</button>
|
||
<button id="btnLibRefresh" class="small-btn">刷新</button>
|
||
</div>
|
||
<div class="lib-scroll">
|
||
<table class="lib-table">
|
||
<thead><tr>
|
||
<th style="width:22%">名称</th><th style="width:7%">类型</th>
|
||
<th style="width:33%">文件路径(链接)</th><th style="width:17%">备注</th><th style="width:21%">操作</th>
|
||
</tr></thead>
|
||
<tbody id="libBody"></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="libHint" class="lib-hint"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 嵌入预览弹窗:iframe 直接以 embed=1 打开查看器,展示真实嵌入效果 -->
|
||
<div id="embedPreview" class="modal-backdrop hidden">
|
||
<div class="modal-box">
|
||
<div class="modal-head">
|
||
<span class="modal-title" id="pvTitle">嵌入预览</span>
|
||
<span class="lib-tip" id="pvPath"></span>
|
||
<button class="modal-close" id="pvClose" title="关闭">✕</button>
|
||
</div>
|
||
<iframe class="modal-frame" id="pvFrame" title="嵌入预览"></iframe>
|
||
<div class="modal-foot">
|
||
<input type="text" id="pvCode" readonly>
|
||
<button class="small-btn primary" id="pvCopy">复制代码</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<input type="file" id="fileInput" accept=".dwg,.dxf,.step,.stp,.DWG,.DXF,.STEP,.STP" style="display:none">
|
||
|
||
<script>
|
||
(function () {
|
||
var params = new URLSearchParams(location.search);
|
||
var file = params.get('file') || params.get('url');
|
||
var landing = document.getElementById('landing');
|
||
var frame = document.getElementById('frame');
|
||
var input = document.getElementById('fileInput');
|
||
|
||
// 直接给文件参数:立即分发到对应查看器,不显示落地页
|
||
if (file) { route(file); return; }
|
||
|
||
document.getElementById('btnOpen').addEventListener('click', function () { input.click(); });
|
||
input.addEventListener('change', function () {
|
||
if (input.files.length) handleFile(input.files[0]);
|
||
input.value = '';
|
||
});
|
||
|
||
// 拖拽打开
|
||
var depth = 0;
|
||
document.addEventListener('dragenter', function (e) { e.preventDefault(); depth++; landing.classList.add('dragging'); });
|
||
document.addEventListener('dragleave', function () { if (--depth <= 0) { depth = 0; landing.classList.remove('dragging'); } });
|
||
document.addEventListener('dragover', function (e) { e.preventDefault(); });
|
||
document.addEventListener('drop', function (e) {
|
||
e.preventDefault();
|
||
depth = 0;
|
||
landing.classList.remove('dragging');
|
||
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
|
||
});
|
||
|
||
/** 本地文件:按扩展名挑查看器,把文件内容直接送进 iframe(不落地、不上传) */
|
||
function handleFile(f) {
|
||
var name = (f.name || '').toLowerCase();
|
||
if (/\.(dwg|dxf)$/.test(name)) sendTo('DWGViewer/index.html', 'dwg', f);
|
||
else if (/\.(step|stp)$/.test(name)) sendTo('STEPViewer/index.html', 'step', f);
|
||
else alert('只支持 .dwg / .dxf / .step / .stp 文件');
|
||
}
|
||
|
||
/**
|
||
* URL:按扩展名挑查看器,由查看器自己去拉文件。
|
||
*
|
||
* 站内路径先按「查看器目录内」理解(samples/xxx.dwg、裸文件名),用 HEAD
|
||
* 探测不存在再退回「入口页所在目录」;绝对地址(http(s)://)原样交给查看器
|
||
* fetch(跨域加载需对方服务器允许 CORS)。
|
||
*/
|
||
async function route(u) {
|
||
var m = /^([a-z]+:\/\/[^\/]+\/)?(.+)$/i.exec(u);
|
||
var rootPart = m[1] || '';
|
||
var p = m[2].replace(/^\/+/, '');
|
||
var parts = [];
|
||
p.split('/').forEach(function (seg) {
|
||
if (!seg || seg === '.') return;
|
||
if (seg === '..') { if (parts.length) parts.pop(); return; }
|
||
parts.push(seg);
|
||
});
|
||
p = parts.join('/');
|
||
var name = p.toLowerCase();
|
||
var viewer, viewerDir;
|
||
if (/\.(dwg|dxf)$/.test(name)) { viewer = 'DWGViewer/index.html'; viewerDir = 'DWGViewer'; }
|
||
else if (/\.(step|stp)$/.test(name)) { viewer = 'STEPViewer/index.html'; viewerDir = 'STEPViewer'; }
|
||
else { showLanding(); console.warn('无法识别的文件类型:' + u); return; }
|
||
|
||
var fileParam;
|
||
if (rootPart) {
|
||
// 绝对地址:整段传给查看器(它是同源相对地址才需要下面的换算)
|
||
fileParam = rootPart + p;
|
||
} else if (p.indexOf(viewerDir + '/') === 0) {
|
||
fileParam = p.slice(viewerDir.length + 1);
|
||
} else if (p !== viewerDir) {
|
||
// 优先查看器目录内(最常见的 samples/ 或裸文件名),不存在再试入口目录下。
|
||
// 选中哪个候选就把路径换算成「相对查看器目录」的形式再传。
|
||
var cands = [viewerDir + '/' + p, '../' + p];
|
||
var win = await firstExisting(cands);
|
||
fileParam = win === cands[0] ? p : (win || p);
|
||
} else {
|
||
fileParam = '';
|
||
}
|
||
var q = 'file=' + encodeURIComponent(fileParam) + viewerParams();
|
||
show(viewer + '?' + q);
|
||
}
|
||
|
||
/** 把入口页收到的嵌入/背景参数透传给查看器(否则嵌入模式下顶栏不会隐藏) */
|
||
function viewerParams() {
|
||
var s = '';
|
||
if (params.get('embed') === '1') s += '&embed=1';
|
||
var bg = params.get('bg');
|
||
if (bg) s += '&bg=' + encodeURIComponent(bg);
|
||
return s;
|
||
}
|
||
|
||
/** 返回第一个存在(HEAD 200)的候选路径,都探测不到返回 null */
|
||
function firstExisting(cands) {
|
||
return Promise.all(cands.map(function (c) {
|
||
return fetch(c, { method: 'HEAD' }).then(
|
||
function (r) { return r.ok ? c : null; },
|
||
function () { return null; } // 网络异常当作不存在,走下一候选
|
||
);
|
||
})).then(function (rs) {
|
||
for (var i = 0; i < rs.length; i++) if (rs[i]) return rs[i];
|
||
return null;
|
||
});
|
||
}
|
||
|
||
var pendingSend = null;
|
||
function show(url) {
|
||
landing.style.display = 'none';
|
||
frame.style.display = 'block';
|
||
frame.src = url;
|
||
}
|
||
function showLanding() {
|
||
landing.style.display = '';
|
||
frame.style.display = 'none';
|
||
frame.src = 'about:blank';
|
||
}
|
||
|
||
function sendTo(url, kind, f) {
|
||
f.arrayBuffer().then(function (buf) {
|
||
show(url);
|
||
pendingSend = { kind: kind, name: f.name, buffer: buf };
|
||
// 等查看器 iframe 起来发 viewer-ready 再送,早了会丢消息
|
||
});
|
||
}
|
||
function deliver(msg) {
|
||
if (!msg) return;
|
||
pendingSend = null;
|
||
frame.contentWindow.postMessage({ type: 'viewer-file', kind: msg.kind, name: msg.name, buffer: msg.buffer }, location.origin);
|
||
}
|
||
|
||
window.addEventListener('message', function (e) {
|
||
if (e.source !== frame.contentWindow) return;
|
||
if (e.data && e.data.type === 'library-updated') { loadLibrary(); return; }
|
||
if (e.data && e.data.type === 'viewer-ready') deliver(pendingSend);
|
||
});
|
||
|
||
/* ================= 2D/3D 数模库 =================
|
||
* 记录存在入口目录的 library.json 里;查看器上传成功后也会自动追加一条。
|
||
* 读取走静态 JSON(没有 PHP 也能看),增删改走 library.php(需要 PHP)。
|
||
* 路径(= 嵌入链接的核心)只在新增时可填,之后锁定;其余字段可编辑。
|
||
*/
|
||
|
||
var libRecords = [];
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||
});
|
||
}
|
||
|
||
function setLibHint(t) { document.getElementById('libHint').textContent = t || ''; }
|
||
|
||
/** 查看器地址:按记录的类型挑 2D/3D(.enc 加密文件没有扩展名可判,必须靠记录的类型) */
|
||
function viewerOf(type, path) {
|
||
var v = type === '2d' ? 'DWGViewer/index.html' : 'STEPViewer/index.html';
|
||
return v + '?file=' + encodeURIComponent(path);
|
||
}
|
||
|
||
function loadLibrary() {
|
||
return fetch('library.json', { cache: 'no-store' }).then(function (r) {
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
return r.json();
|
||
}).then(function (j) {
|
||
libRecords = (j && j.records) || [];
|
||
renderLibrary();
|
||
setLibHint('');
|
||
}).catch(function (e) {
|
||
libRecords = [];
|
||
renderLibrary();
|
||
setLibHint('数模库读取失败:' + e.message + '(入口目录需要 library.json;增删改需要 PHP 接口 library.php)');
|
||
});
|
||
}
|
||
|
||
function renderLibrary() {
|
||
var tb = document.getElementById('libBody');
|
||
tb.innerHTML = '';
|
||
var kw = document.getElementById('libSearch').value.trim().toLowerCase();
|
||
var tf = document.getElementById('libTypeFilter').value;
|
||
var list = libRecords.filter(function (rec) {
|
||
if (tf && rec.type !== tf) return false;
|
||
if (kw && ((rec.name || '') + ' ' + (rec.path || '') + ' ' + (rec.note || '')).toLowerCase().indexOf(kw) === -1) return false;
|
||
return true;
|
||
});
|
||
if (!libRecords.length) {
|
||
tb.innerHTML = '<tr><td colspan="5" class="lib-empty">还没有记录。上传过文件后会自动收录,也可以点「新增记录」手动添加。</td></tr>';
|
||
return;
|
||
}
|
||
if (!list.length) {
|
||
tb.innerHTML = '<tr><td colspan="5" class="lib-empty">没有匹配的记录(' + libRecords.length + ' 条记录被筛选掉)。</td></tr>';
|
||
return;
|
||
}
|
||
list.forEach(function (rec) {
|
||
var tr = document.createElement('tr');
|
||
tr.innerHTML =
|
||
'<td class="lib-name" title="' + esc(rec.name) + '">' + esc(rec.name) + '</td>' +
|
||
'<td><span class="badge badge-' + (rec.type === '2d' ? '2d' : '3d') + '">' + (rec.type === '2d' ? '2D' : '3D') + '</span></td>' +
|
||
'<td class="lib-path" title="' + esc(rec.path) + '">' + esc(rec.path) + '</td>' +
|
||
'<td class="lib-note" title="' + esc(rec.note || '') + '">' + esc(rec.note || '') + '</td>' +
|
||
'<td style="white-space:nowrap">' +
|
||
'<button class="small-btn" data-act="open">打开</button>' +
|
||
'<button class="small-btn" data-act="copy">复制链接</button>' +
|
||
'<button class="small-btn" data-act="preview">预览</button>' +
|
||
'<button class="small-btn" data-act="edit">编辑</button>' +
|
||
'<button class="small-btn danger" data-act="del">删除</button>' +
|
||
'</td>';
|
||
tr.querySelector('[data-act=open]').onclick = function () { show(viewerOf(rec.type, rec.path)); };
|
||
tr.querySelector('[data-act=copy]').onclick = function () { copyEmbedLink(rec); };
|
||
tr.querySelector('[data-act=preview]').onclick = function () { openPreview(rec); };
|
||
tr.querySelector('[data-act=edit]').onclick = function () { editRow(tr, rec); };
|
||
tr.querySelector('[data-act=del]').onclick = function () { delRow(rec); };
|
||
tb.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
/* ================= 嵌入预览弹窗 ================= */
|
||
|
||
function openPreview(rec) {
|
||
var link = viewerOf(rec.type, rec.path) + '&embed=1';
|
||
document.getElementById('pvTitle').textContent = (rec.name || rec.path) + ' · 嵌入预览';
|
||
document.getElementById('pvPath').textContent = rec.path;
|
||
document.getElementById('pvFrame').src = link;
|
||
document.getElementById('pvCode').value =
|
||
'<iframe src="' + (location.origin + location.pathname.replace(/[^\/]*$/, '') + link) +
|
||
'" width="100%" height="600" frameborder="0" allowfullscreen></iframe>';
|
||
document.getElementById('embedPreview').classList.remove('hidden');
|
||
}
|
||
|
||
function closePreview() {
|
||
document.getElementById('embedPreview').classList.add('hidden');
|
||
document.getElementById('pvFrame').src = 'about:blank'; // 关掉 iframe 里的请求
|
||
}
|
||
|
||
document.getElementById('pvClose').onclick = closePreview;
|
||
document.getElementById('embedPreview').addEventListener('click', function (e) {
|
||
if (e.target === this) closePreview();
|
||
});
|
||
document.getElementById('pvCopy').onclick = function () {
|
||
var t = document.getElementById('pvCode');
|
||
navigator.clipboard.writeText(t.value).then(function () {
|
||
setLibHint('嵌入代码已复制');
|
||
}, function () {
|
||
t.select();
|
||
setLibHint('复制失败,请手动选择复制');
|
||
});
|
||
};
|
||
|
||
document.getElementById('libSearch').addEventListener('input', renderLibrary);
|
||
document.getElementById('libTypeFilter').addEventListener('change', renderLibrary);
|
||
|
||
function copyEmbedLink(rec) {
|
||
// 嵌入链接基于本页所在目录(兼容部署在子路径下的情况)
|
||
var base = location.origin + location.pathname.replace(/[^\/]*$/, '');
|
||
var link = base + viewerOf(rec.type, rec.path) + '&embed=1';
|
||
navigator.clipboard.writeText(link).then(function () {
|
||
setLibHint('已复制嵌入链接:' + link);
|
||
}, function () {
|
||
setLibHint('复制失败,请手动复制:' + link);
|
||
});
|
||
}
|
||
|
||
/** 行内编辑:名称/类型/备注可改;路径(链接)只在新增时可填,之后锁定 */
|
||
function editRow(tr, rec) {
|
||
tr.innerHTML =
|
||
'<td><input id="eName" value="' + esc(rec.name || '') + '"></td>' +
|
||
'<td><select id="eType"><option value="3d"' + (rec.type !== '2d' ? ' selected' : '') + '>3D</option>' +
|
||
'<option value="2d"' + (rec.type === '2d' ? ' selected' : '') + '>2D</option></select></td>' +
|
||
'<td class="lib-path" title="' + esc(rec.path) + '">' + esc(rec.path) + ' <span title="链接不可编辑">🔒</span></td>' +
|
||
'<td><input id="eNote" value="' + esc(rec.note || '') + '"></td>' +
|
||
'<td style="white-space:nowrap"><button class="small-btn primary" id="eSave">保存</button><button class="small-btn" id="eCancel">取消</button></td>';
|
||
tr.querySelector('#eSave').onclick = function () {
|
||
rec.name = tr.querySelector('#eName').value.trim();
|
||
rec.note = tr.querySelector('#eNote').value.trim();
|
||
rec.type = tr.querySelector('#eType').value;
|
||
saveLibrary('已保存');
|
||
};
|
||
tr.querySelector('#eCancel').onclick = function () { renderLibrary(); };
|
||
}
|
||
|
||
function delRow(rec) {
|
||
if (!confirm('删除记录「' + (rec.name || rec.path) + '」?\n只删库表记录,服务器上的文件不会动。')) return;
|
||
libRecords.splice(libRecords.indexOf(rec), 1);
|
||
saveLibrary('已删除');
|
||
}
|
||
|
||
function addRow() {
|
||
var tr = document.createElement('tr');
|
||
tr.innerHTML =
|
||
'<td><input id="eName" placeholder="显示名称"></td>' +
|
||
'<td><select id="eType"><option value="3d">3D</option><option value="2d">2D</option></select></td>' +
|
||
'<td><input id="ePath" placeholder="samples/xxx.stp 或 文件名.stp"></td>' +
|
||
'<td><input id="eNote" placeholder="备注(可选)"></td>' +
|
||
'<td style="white-space:nowrap"><button class="small-btn primary" id="eSave">保存</button><button class="small-btn" id="eCancel">取消</button></td>';
|
||
document.getElementById('libBody').insertBefore(tr, document.getElementById('libBody').firstChild);
|
||
tr.querySelector('#eSave').onclick = function () {
|
||
var path = tr.querySelector('#ePath').value.trim();
|
||
var name = tr.querySelector('#eName').value.trim();
|
||
if (!path) { setLibHint('请填写文件路径'); return; }
|
||
if (path.indexOf('..') !== -1) { setLibHint('路径不能包含 ..'); return; }
|
||
var t = tr.querySelector('#eType').value;
|
||
// 按扩展名自动纠正类型,防止打开时进错查看器
|
||
if (/\.(dwg|dxf)$/i.test(path)) t = '2d';
|
||
else if (/\.(step|stp)$/i.test(path)) t = '3d';
|
||
libRecords.unshift({ path: path, name: name || path.split('/').pop(), type: t, note: tr.querySelector('#eNote').value.trim(), time: 0 });
|
||
saveLibrary('已新增');
|
||
};
|
||
tr.querySelector('#eCancel').onclick = function () { renderLibrary(); };
|
||
}
|
||
|
||
function saveLibrary(okMsg) {
|
||
fetch('library.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ records: libRecords }),
|
||
}).then(function (r) {
|
||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||
return r.json();
|
||
}).then(function () {
|
||
setLibHint(okMsg + '(共 ' + libRecords.length + ' 条记录)');
|
||
renderLibrary();
|
||
}).catch(function (e) {
|
||
// 先回滚到服务器上的版本,再把失败原因写上(loadLibrary 成功后会清空提示)
|
||
loadLibrary().then(function () {
|
||
setLibHint('保存失败:' + e.message + '(增删改需要站点 PHP 接口;本地开发服务器没有 PHP)');
|
||
});
|
||
});
|
||
}
|
||
|
||
document.getElementById('btnLibAdd').onclick = addRow;
|
||
document.getElementById('btnLibRefresh').onclick = function () { loadLibrary(); };
|
||
|
||
loadLibrary();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|