feat(all): 离线包迁移
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<n-button class="mr-2">
|
||||
<template #icon>
|
||||
<n-icon size="22">
|
||||
<DocumentExport/>
|
||||
<DocumentExport />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t("layout.header.Export") }}
|
||||
@@ -13,7 +13,7 @@
|
||||
<n-button type="primary" @click="handleImport">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<DocumentImport/>
|
||||
<DocumentImport />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t("layout.header.Import") }}
|
||||
@@ -21,135 +21,283 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from "vue";
|
||||
import {DocumentImport, DocumentExport} from "@vicons/carbon";
|
||||
import {t} from "@/language";
|
||||
import {App, Export, Loader} from "@astral3d/engine";
|
||||
import { onMounted } from "vue";
|
||||
import { DocumentImport, DocumentExport } from "@vicons/carbon";
|
||||
import { t } from "@/language";
|
||||
import { App, Export, Loader } from "@astral3d/engine";
|
||||
import { useGlobalConfigStore } from "@/store/modules/globalConfig";
|
||||
|
||||
const exportClass = new Export();
|
||||
const exportClass = new Export();
|
||||
const globalConfigStore = useGlobalConfigStore();
|
||||
|
||||
const exportOptions = [
|
||||
{
|
||||
label: t("layout.header['Export Object']"),
|
||||
key: "exportObject",
|
||||
children: [
|
||||
{
|
||||
label: "JSON",
|
||||
key: "exportObjectToJSON"
|
||||
},
|
||||
{
|
||||
label: "GLB",
|
||||
key: "exportObjectToGlb"
|
||||
},
|
||||
{
|
||||
label: "GLTF",
|
||||
key: "exportObjectToGltf"
|
||||
},
|
||||
{
|
||||
label: "OBJ",
|
||||
key: "exportObjectToObj"
|
||||
},
|
||||
{
|
||||
label: "PLY",
|
||||
key: "exportObjectToPly"
|
||||
},
|
||||
{
|
||||
label: t("layout.header['PLY (Binary)']"),
|
||||
key: "exportObjectToPlyBinary"
|
||||
},
|
||||
{
|
||||
label: "STL",
|
||||
key: "exportObjectToStl"
|
||||
},
|
||||
{
|
||||
label: t("layout.header['STL (Binary)']"),
|
||||
key: "exportObjectToStlBinary"
|
||||
},
|
||||
{
|
||||
label: "USDZ",
|
||||
key: "exportObjectToUSDZ"
|
||||
const exportOptions = [
|
||||
{
|
||||
label: t("layout.header['Export Object']"),
|
||||
key: "exportObject",
|
||||
children: [
|
||||
{
|
||||
label: "JSON",
|
||||
key: "exportObjectToJSON",
|
||||
},
|
||||
{
|
||||
label: "GLB",
|
||||
key: "exportObjectToGlb",
|
||||
},
|
||||
{
|
||||
label: "GLTF",
|
||||
key: "exportObjectToGltf",
|
||||
},
|
||||
{
|
||||
label: "OBJ",
|
||||
key: "exportObjectToObj",
|
||||
},
|
||||
{
|
||||
label: "PLY",
|
||||
key: "exportObjectToPly",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['PLY (Binary)']"),
|
||||
key: "exportObjectToPlyBinary",
|
||||
},
|
||||
{
|
||||
label: "STL",
|
||||
key: "exportObjectToStl",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['STL (Binary)']"),
|
||||
key: "exportObjectToStlBinary",
|
||||
},
|
||||
{
|
||||
label: "USDZ",
|
||||
key: "exportObjectToUSDZ",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Export Scene']"),
|
||||
key: "exportScene",
|
||||
children: [
|
||||
{
|
||||
label: "JSON",
|
||||
key: "exportSceneToJSON",
|
||||
},
|
||||
{
|
||||
label: "GLB",
|
||||
key: "exportSceneToGlb",
|
||||
},
|
||||
{
|
||||
label: "GLTF",
|
||||
key: "exportSceneToGltf",
|
||||
},
|
||||
{
|
||||
label: "OBJ",
|
||||
key: "exportSceneToObj",
|
||||
},
|
||||
{
|
||||
label: "PLY",
|
||||
key: "exportSceneToPly",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['PLY (Binary)']"),
|
||||
key: "exportSceneToPlyBinary",
|
||||
},
|
||||
{
|
||||
label: "STL",
|
||||
key: "exportSceneToStl",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['STL (Binary)']"),
|
||||
key: "exportSceneToStlBinary",
|
||||
},
|
||||
{
|
||||
label: "USDZ",
|
||||
key: "exportSceneToUSDZ",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Offline package']"),
|
||||
key: "exportOffline",
|
||||
children: [
|
||||
{
|
||||
label: t("layout.header['Single packet']"),
|
||||
key: "exportOfflineSingle",
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Multiple packet']"),
|
||||
key: "exportOfflineMultiple",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function handleImport() {
|
||||
const form = document.createElement("form");
|
||||
form.style.display = "none";
|
||||
document.body.appendChild(form);
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.multiple = true;
|
||||
fileInput.type = "file";
|
||||
fileInput.addEventListener("change", function () {
|
||||
const files = fileInput.files;
|
||||
if (!files || files.length === 0) {
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t("layout.header['Export Scene']"),
|
||||
key: "exportScene",
|
||||
children: [
|
||||
{
|
||||
label: "JSON",
|
||||
key: "exportSceneToJSON"
|
||||
},
|
||||
{
|
||||
label: "GLB",
|
||||
key: "exportSceneToGlb"
|
||||
},
|
||||
{
|
||||
label: "GLTF",
|
||||
key: "exportSceneToGltf"
|
||||
},
|
||||
{
|
||||
label: "OBJ",
|
||||
key: "exportSceneToObj"
|
||||
},
|
||||
{
|
||||
label: "PLY",
|
||||
key: "exportSceneToPly"
|
||||
},
|
||||
{
|
||||
label: t("layout.header['PLY (Binary)']"),
|
||||
key: "exportSceneToPlyBinary"
|
||||
},
|
||||
{
|
||||
label: "STL",
|
||||
key: "exportSceneToStl"
|
||||
},
|
||||
{
|
||||
label: t("layout.header['STL (Binary)']"),
|
||||
key: "exportSceneToStlBinary"
|
||||
},
|
||||
{
|
||||
label: "USDZ",
|
||||
key: "exportSceneToUSDZ"
|
||||
|
||||
const astralFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith(".astral"));
|
||||
if (astralFiles.length > 0) {
|
||||
handleOfflineFiles(files);
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
function handleImport() {
|
||||
const form = document.createElement('form');
|
||||
form.style.display = 'none';
|
||||
document.body.appendChild(form);
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.multiple = true;
|
||||
fileInput.type = 'file';
|
||||
fileInput.addEventListener('change', function () {
|
||||
Loader.loadFiles(fileInput.files, undefined)
|
||||
.catch((err) => {
|
||||
Loader.loadFiles(files, undefined)
|
||||
.catch(err => {
|
||||
window.$message?.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
form.reset();
|
||||
});
|
||||
});
|
||||
form.appendChild(fileInput);
|
||||
});
|
||||
form.appendChild(fileInput);
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function handleExportSelect(key: string) {
|
||||
if (key.startsWith("exportObject")) {
|
||||
if (App.selected === null) {
|
||||
window.$message?.error(window.$t("prompt['No object selected.']"));
|
||||
function handleExportSelect(key: string) {
|
||||
if (key === "exportOfflineSingle") {
|
||||
exportOffline("single");
|
||||
return;
|
||||
}
|
||||
if (key === "exportOfflineMultiple") {
|
||||
exportOffline("multiple");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.startsWith("exportObject")) {
|
||||
if (App.selected === null) {
|
||||
window.$message?.error(window.$t("prompt['No object selected.']"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
exportClass[key] && exportClass[key]();
|
||||
}
|
||||
|
||||
function downloadFile(file: File) {
|
||||
const url = URL.createObjectURL(file);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function downloadFilesInBatches(files: File[], batchSize = 9, delayMs = 1100) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
downloadFile(files[i]);
|
||||
if ((i + 1) % batchSize === 0 && i < files.length - 1) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exportClass[key]();
|
||||
}
|
||||
function exportOffline(mode: "single" | "multiple") {
|
||||
const packer = (window.viewer as any)?.package;
|
||||
if (!packer || typeof packer.packOffline !== "function") {
|
||||
window.$message?.error("当前SDK暂不支持离线包导出");
|
||||
return;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
const sceneInfo = App.project.getKey("sceneInfo");
|
||||
const sceneName = sceneInfo?.sceneName || "scene";
|
||||
|
||||
globalConfigStore.loading = true;
|
||||
globalConfigStore.loadingText = "正在导出离线包...";
|
||||
|
||||
packer
|
||||
.packOffline({
|
||||
name: sceneName,
|
||||
layer: 2,
|
||||
mode,
|
||||
onProgress: (progress: number) => {
|
||||
globalConfigStore.loadingText = `离线包导出 ${progress}%`;
|
||||
},
|
||||
})
|
||||
.then(async (result: { files: File[] }) => {
|
||||
await downloadFilesInBatches(result.files);
|
||||
window.$message?.success("离线包导出完成");
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
if (err?.code === "SDK_FEATURE_DISABLED") {
|
||||
window.$message?.error("当前授权未开通离线包导出能力");
|
||||
return;
|
||||
}
|
||||
if (typeof err?.message === "string" && err.message.trim()) {
|
||||
window.$message?.error(err.message);
|
||||
return;
|
||||
}
|
||||
window.$message?.error("离线包导出失败");
|
||||
})
|
||||
.finally(() => {
|
||||
globalConfigStore.loading = false;
|
||||
globalConfigStore.loadingText = "";
|
||||
});
|
||||
}
|
||||
|
||||
function handleOfflineFiles(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const astralFiles = Array.from(files).filter(file => file.name.toLowerCase().endsWith(".astral"));
|
||||
if (astralFiles.length === 0) {
|
||||
window.$message?.error("请选择离线包文件(.astral)");
|
||||
return;
|
||||
}
|
||||
|
||||
const packer = (window.viewer as any)?.package;
|
||||
if (!packer || typeof packer.unpack !== "function") {
|
||||
window.$message?.error("当前SDK暂不支持离线包导入");
|
||||
return;
|
||||
}
|
||||
|
||||
const entryFile = guessEntryFile(astralFiles);
|
||||
|
||||
globalConfigStore.loading = true;
|
||||
globalConfigStore.loadingText = "离线包加载中...";
|
||||
|
||||
packer.unpack({
|
||||
url: entryFile.name,
|
||||
offlineFiles: astralFiles,
|
||||
offlineEntry: entryFile.name,
|
||||
onProgress: (progress: number) => {
|
||||
globalConfigStore.loadingText = `离线包加载 ${progress.toFixed(2)}%`;
|
||||
},
|
||||
onComplete: () => {
|
||||
globalConfigStore.loading = false;
|
||||
globalConfigStore.loadingText = "";
|
||||
window.$message?.success("离线包加载完成");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function guessEntryFile(files: File[]): File {
|
||||
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}$/;
|
||||
const findBaseName = (name: string) => name.replace(/\.[^.]+$/, "");
|
||||
|
||||
const candidate = files.find(file => !uuidReg.test(findBaseName(file.name)));
|
||||
return candidate || files[0];
|
||||
}
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -77,6 +77,9 @@ export default {
|
||||
Export: '导出',
|
||||
'Export Object': '导出物体',
|
||||
'Export Scene': '导出场景',
|
||||
'Offline package': '离线包',
|
||||
'Single packet': '单包',
|
||||
'Multiple packet': '多包',
|
||||
'PLY (Binary)': 'PLY(二进制)',
|
||||
'STL (Binary)': 'STL(二进制)',
|
||||
/* File 下的选项 End */
|
||||
|
||||
Reference in New Issue
Block a user