Files
Leantime/dev/index.html

502 lines
25 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>