425 lines
12 KiB
HTML
425 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>TkAstral3D Offline Package Test</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0b1020;
|
||
--panel: rgba(15, 23, 42, 0.88);
|
||
--line: rgba(148, 163, 184, 0.24);
|
||
--text: #e2e8f0;
|
||
--muted: #94a3b8;
|
||
--accent: #22c55e;
|
||
--accent-2: #38bdf8;
|
||
--danger: #f87171;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body {
|
||
margin: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
background:
|
||
radial-gradient(circle at top left, rgba(56, 189, 248, 0.12), transparent 32%),
|
||
radial-gradient(circle at top right, rgba(34, 197, 94, 0.14), transparent 28%),
|
||
linear-gradient(180deg, #0b1020, #050814 72%);
|
||
color: var(--text);
|
||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
}
|
||
|
||
.app {
|
||
display: grid;
|
||
grid-template-columns: 360px 1fr;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.sidebar {
|
||
padding: 20px;
|
||
border-right: 1px solid var(--line);
|
||
background: var(--panel);
|
||
backdrop-filter: blur(10px);
|
||
overflow: auto;
|
||
}
|
||
|
||
.viewer-wrap {
|
||
position: relative;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
#viewer {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0 0 10px;
|
||
font-size: 24px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.desc,
|
||
.hint,
|
||
.meta,
|
||
.log {
|
||
color: var(--muted);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.desc {
|
||
margin-bottom: 16px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.card {
|
||
margin-top: 16px;
|
||
padding: 14px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 14px;
|
||
background: rgba(2, 6, 23, 0.36);
|
||
}
|
||
|
||
.card h2 {
|
||
margin: 0 0 10px;
|
||
font-size: 15px;
|
||
color: #f8fafc;
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
button,
|
||
input[type="file"] {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 14px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 10px;
|
||
background: linear-gradient(180deg, rgba(56, 189, 248, 0.16), rgba(34, 197, 94, 0.16));
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
}
|
||
|
||
button.secondary {
|
||
background: rgba(15, 23, 42, 0.9);
|
||
}
|
||
|
||
input[type="file"] {
|
||
width: 100%;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.status {
|
||
margin-top: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--line);
|
||
background: rgba(15, 23, 42, 0.72);
|
||
font-size: 13px;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.status.success {
|
||
border-color: rgba(34, 197, 94, 0.35);
|
||
color: #bbf7d0;
|
||
}
|
||
|
||
.status.error {
|
||
border-color: rgba(248, 113, 113, 0.35);
|
||
color: #fecaca;
|
||
}
|
||
|
||
.file-list {
|
||
margin: 10px 0 0;
|
||
padding-left: 18px;
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
background: rgba(56, 189, 248, 0.1);
|
||
border: 1px solid rgba(56, 189, 248, 0.18);
|
||
font-size: 12px;
|
||
color: #bae6fd;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.app {
|
||
grid-template-columns: 1fr;
|
||
grid-template-rows: auto 1fr;
|
||
}
|
||
|
||
.sidebar {
|
||
border-right: 0;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<aside class="sidebar">
|
||
<h1>Tk 离线包测试</h1>
|
||
<div class="desc">
|
||
这个页面直接使用 Tk 构建产物来验证离线包加载。
|
||
支持单包与多包 .astral 文件,直接通过 SDK 的离线参数加载,不依赖编辑器页面。
|
||
</div>
|
||
|
||
<div class="row">
|
||
<span class="tag">SDK: TkAstral3D/Test/dist/astral3d.umd.js</span>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>选择离线包</h2>
|
||
<input id="fileInput" type="file" multiple accept=".astral" />
|
||
<div class="row">
|
||
<button id="loadBtn">加载离线包</button>
|
||
<button id="loadLocalSplitBtn" class="secondary">直接加载本目录(分包)</button>
|
||
<button id="loadLocalSingleBtn" class="secondary">直接加载本目录(单包)</button>
|
||
<button id="resetBtn" class="secondary">重置场景</button>
|
||
</div>
|
||
<div id="status" class="status">等待选择 .astral 文件,或使用“直接加载本目录”</div>
|
||
<ul id="fileList" class="file-list"></ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>说明</h2>
|
||
<div class="hint">
|
||
1. 多包模式:直接选中全部 .astral 文件后加载。\n
|
||
2. 单包模式:只需要选择导出的单个 .astral 文件。\n
|
||
3. 本目录快捷加载:分包=1121212.astral + 7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral;单包=1121212.astral。\n
|
||
4. 建议通过本地静态服务器打开此页面,而不是直接 file:// 打开。
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="viewer-wrap">
|
||
<div id="viewer"></div>
|
||
</main>
|
||
</div>
|
||
|
||
<script src="./dist/astral3d.umd.js"></script>
|
||
<script>
|
||
(function () {
|
||
const SDK = globalThis.Astral3D;
|
||
if (!SDK) {
|
||
document.getElementById("status").textContent = "Tk SDK 加载失败,请检查 astral3d.umd.js 路径";
|
||
document.getElementById("status").className = "status error";
|
||
return;
|
||
}
|
||
|
||
const viewerEl = document.getElementById("viewer");
|
||
const fileInput = document.getElementById("fileInput");
|
||
const loadBtn = document.getElementById("loadBtn");
|
||
const loadLocalSplitBtn = document.getElementById("loadLocalSplitBtn");
|
||
const loadLocalSingleBtn = document.getElementById("loadLocalSingleBtn");
|
||
const resetBtn = document.getElementById("resetBtn");
|
||
const statusEl = document.getElementById("status");
|
||
const fileListEl = document.getElementById("fileList");
|
||
const LOCAL_SPLIT_FILES = [
|
||
"1121212.astral",
|
||
"7ff6dd32-25b3-4eea-abdc-6f316ecd668e.astral",
|
||
];
|
||
const LOCAL_SINGLE_FILE = "1121212-offline.astral";
|
||
|
||
let viewer = null;
|
||
|
||
function setStatus(text, type) {
|
||
statusEl.textContent = text;
|
||
statusEl.className = "status" + (type ? ` ${type}` : "");
|
||
}
|
||
|
||
function guessEntryFile(files) {
|
||
const uuidReg = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||
return files.find(f => !uuidReg.test(f.name.replace(/\.[^.]+$/, ""))) || files[0];
|
||
}
|
||
|
||
function ensureViewer() {
|
||
if (viewer) return viewer;
|
||
viewer = new SDK.Viewer({
|
||
container: viewerEl,
|
||
edit: { enabled: false },
|
||
grid: { enabled: true },
|
||
request: { baseUrl: "" },
|
||
});
|
||
globalThis.viewer = viewer;
|
||
return viewer;
|
||
}
|
||
|
||
function resetViewer() {
|
||
if (viewer && typeof viewer.dispose === "function") {
|
||
viewer.dispose();
|
||
}
|
||
viewer = null;
|
||
viewerEl.innerHTML = "";
|
||
setStatus("场景已重置", "success");
|
||
}
|
||
|
||
function renderFileList(files) {
|
||
fileListEl.innerHTML = "";
|
||
Array.from(files || []).forEach(file => {
|
||
const li = document.createElement("li");
|
||
li.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
|
||
fileListEl.appendChild(li);
|
||
});
|
||
}
|
||
|
||
async function loadByOfflineFiles(files, entryName) {
|
||
setStatus(`开始加载离线包:${entryName}`, "");
|
||
const currentViewer = ensureViewer();
|
||
const packer = currentViewer.package || new SDK.Package(currentViewer);
|
||
currentViewer.package = packer;
|
||
|
||
await packer.unpack({
|
||
url: entryName,
|
||
offlineFiles: files,
|
||
offlineEntry: entryName,
|
||
onProgress: progress => {
|
||
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
|
||
},
|
||
onSceneLoad: () => {
|
||
setStatus("场景首包已解析,正在继续加载资源...", "");
|
||
},
|
||
onComplete: () => {
|
||
setStatus(`离线包加载完成:${entryName}`, "success");
|
||
},
|
||
});
|
||
}
|
||
|
||
async function loadByOfflineBundle(bundle, entryName) {
|
||
setStatus(`开始加载离线包:${entryName}`, "");
|
||
const currentViewer = ensureViewer();
|
||
const packer = currentViewer.package || new SDK.Package(currentViewer);
|
||
currentViewer.package = packer;
|
||
console.log({
|
||
url: "/",
|
||
offlineBundle: bundle,
|
||
offlineEntry: entryName,
|
||
onProgress: progress => {
|
||
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
|
||
},
|
||
onSceneLoad: () => {
|
||
setStatus("场景首包已解析,正在继续加载资源...", "");
|
||
},
|
||
onComplete: () => {
|
||
setStatus(`离线包加载完成:${entryName}`, "success");
|
||
},
|
||
})
|
||
await packer.unpack({
|
||
url: "/",
|
||
offlineBundle: bundle,
|
||
offlineEntry: entryName,
|
||
onProgress: progress => {
|
||
setStatus(`离线包加载中... ${Number(progress || 0).toFixed(2)}%`, "");
|
||
},
|
||
onSceneLoad: () => {
|
||
setStatus("场景首包已解析,正在继续加载资源...", "");
|
||
},
|
||
onComplete: () => {
|
||
setStatus(`离线包加载完成:${entryName}`, "success");
|
||
},
|
||
});
|
||
}
|
||
|
||
async function fetchLocalAstralFile(name) {
|
||
const response = await fetch(`./${encodeURIComponent(name)}`);
|
||
if (!response.ok) {
|
||
throw new Error(`读取本地文件失败:${name}`);
|
||
}
|
||
const blob = await response.blob();
|
||
return new File([blob], name, { type: "application/octet-stream" });
|
||
}
|
||
|
||
async function loadOfflinePackage() {
|
||
const files = Array.from(fileInput.files || []).filter(f => f.name.toLowerCase().endsWith(".astral"));
|
||
if (files.length === 0) {
|
||
setStatus("请选择 .astral 文件", "error");
|
||
return;
|
||
}
|
||
|
||
renderFileList(files);
|
||
try {
|
||
if (files.length === 1) {
|
||
await loadByOfflineBundle(files[0], files[0].name);
|
||
} else {
|
||
const entry = guessEntryFile(files);
|
||
await loadByOfflineFiles(files, entry.name);
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(`加载失败:${error && error.message ? error.message : error}`, "error");
|
||
}
|
||
}
|
||
|
||
async function loadLocalSplitPackage() {
|
||
try {
|
||
renderFileList(LOCAL_SPLIT_FILES.map(name => ({ name, size: 0 })));
|
||
const files = [];
|
||
for (const name of LOCAL_SPLIT_FILES) {
|
||
files.push(await fetchLocalAstralFile(name));
|
||
}
|
||
await loadByOfflineFiles(files, LOCAL_SPLIT_FILES[0]);
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(`本目录分包加载失败:${error && error.message ? error.message : error}`, "error");
|
||
}
|
||
}
|
||
|
||
async function loadLocalSinglePackage() {
|
||
try {
|
||
renderFileList([{ name: LOCAL_SINGLE_FILE, size: 0 }]);
|
||
const file = await fetchLocalAstralFile(LOCAL_SINGLE_FILE);
|
||
await loadByOfflineBundle(file, LOCAL_SINGLE_FILE);
|
||
} catch (error) {
|
||
console.error(error);
|
||
setStatus(`本目录单包加载失败:${error && error.message ? error.message : error}`, "error");
|
||
}
|
||
}
|
||
|
||
fileInput.addEventListener("change", () => {
|
||
renderFileList(fileInput.files);
|
||
setStatus("文件已选择,点击“加载离线包”开始测试", "");
|
||
});
|
||
|
||
loadBtn.addEventListener("click", () => {
|
||
loadOfflinePackage();
|
||
});
|
||
|
||
loadLocalSplitBtn.addEventListener("click", () => {
|
||
loadLocalSplitPackage();
|
||
});
|
||
|
||
loadLocalSingleBtn.addEventListener("click", () => {
|
||
loadLocalSinglePackage();
|
||
});
|
||
|
||
resetBtn.addEventListener("click", () => {
|
||
resetViewer();
|
||
});
|
||
|
||
ensureViewer();
|
||
setStatus("Tk SDK 已加载,等待选择离线包", "success");
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html> |