feat(all): 离线包迁移
This commit is contained in:
parent
309bb0a5e6
commit
404e804143
@ -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 */
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
*/
|
||||
import { Mesh, Group, Bone } from "three";
|
||||
import JSZip from "jszip";
|
||||
import { strToU8 } from 'three/examples/jsm/libs/fflate.module.js';
|
||||
import { BASE64_TYPES, TYPED_ARRAYS } from "@/constant";
|
||||
import { unzip, zip, fetchController } from "@/utils";
|
||||
import { PackageSkeleton } from "@/core/loader/Package.Skeleton";
|
||||
@ -16,18 +17,70 @@ import App from "@/core/app/App";
|
||||
import type Viewer from "@/core/viewer/Viewer";
|
||||
|
||||
interface IPackConfig {
|
||||
name: string; // 首包名称
|
||||
layer?: number; // 拆分的最深层级 0:拆分至最深层
|
||||
zipUploadFun: (zip: File) => Promise<any>; // 压缩包上传接口函数,多压缩包
|
||||
onProgress?: (progress: number) => void; // 打包进度回调
|
||||
onComplete?: (_: { firstUploadResult: any, totalSize: number, totalZipNumber: number }) => void; // 打包完成回调
|
||||
// 首包名称
|
||||
name: string;
|
||||
// 拆分的最深层级 0:拆分至最深层
|
||||
layer?: number;
|
||||
// 压缩包上传接口函数,多压缩包
|
||||
zipUploadFun: (zip: File) => Promise<any>;
|
||||
// 原始包上传接口函数(不压缩)
|
||||
rawUploadFun?: (raw: { name: string; files: Record<string, Uint8Array> }) => Promise<any>;
|
||||
// 压缩进度回调(0-100)
|
||||
onZipProgress?: (info: { name: string; progress: number }) => void;
|
||||
// 图片处理并发
|
||||
imageProcessLimit?: number;
|
||||
// 打包进度回调
|
||||
onProgress?: (progress: number) => void;
|
||||
// 打包完成回调
|
||||
onComplete?: (_: { firstUploadResult: any, totalSize: number, totalZipNumber: number }) => void;
|
||||
}
|
||||
|
||||
interface IOfflinePackConfig {
|
||||
// 离线包名称(作为首包 zip 名称)
|
||||
name: string;
|
||||
// 拆分的最深层级;0 表示拆分到最深层
|
||||
layer?: number;
|
||||
// 导出模式:single 为单 zip,multiple 为多 zip
|
||||
mode?: "single" | "multiple";
|
||||
// 单包导出时的包名(不含后缀)
|
||||
bundleName?: string;
|
||||
// 压缩进度回调(0-100)
|
||||
onZipProgress?: (info: { name: string; progress: number }) => void;
|
||||
// 图片处理并发
|
||||
imageProcessLimit?: number;
|
||||
// 进度回调
|
||||
onProgress?: (progress: number) => void;
|
||||
// 完成回调
|
||||
onComplete?: (result: OfflinePackResult) => void;
|
||||
}
|
||||
|
||||
interface OfflinePackResult {
|
||||
mode: "single" | "multiple";
|
||||
// 首包名称(含 .astral)
|
||||
entry: string;
|
||||
// 导出的包文件列表
|
||||
files: File[];
|
||||
// 总大小
|
||||
totalSize: number;
|
||||
// 包数量(首包 + 组包)
|
||||
totalZipNumber: number;
|
||||
}
|
||||
|
||||
interface IUnpackConfig {
|
||||
url: string, // 首包url
|
||||
onSceneLoad?: (sceneJson: ISceneJson, configJson: IAppProject.Config) => void, // 场景首包加载完成回调
|
||||
onProgress?: (progress: number) => void; // 场景加载进度回调
|
||||
onComplete?: () => void // 场景加载完成回调.
|
||||
// 首包url
|
||||
url: string;
|
||||
// 场景首包加载完成回调
|
||||
onSceneLoad?: (sceneJson: ISceneJson, configJson: IAppProject.Config) => void;
|
||||
// 场景加载进度回调
|
||||
onProgress?: (progress: number) => void;
|
||||
// 场景加载完成回调
|
||||
onComplete?: () => void;
|
||||
// 离线多包文件列表(可选)
|
||||
offlineFiles?: File[];
|
||||
// 离线单包(内部包含多个包)
|
||||
offlineBundle?: Blob | ArrayBuffer | Uint8Array;
|
||||
// 离线首包名称(可选,未提供则使用 url 的文件名)
|
||||
offlineEntry?: string;
|
||||
}
|
||||
|
||||
interface SourceData {
|
||||
@ -71,6 +124,19 @@ export class Package {
|
||||
private callFunNum: { value: number; };
|
||||
private skeletonClass: PackageSkeleton;
|
||||
|
||||
// 离线包相关属性
|
||||
// @ts-ignore
|
||||
private offlineZipMap: Map<string, Uint8Array> | null = null;
|
||||
// @ts-ignore
|
||||
private offlineEntryZip: string | null = null;
|
||||
// @ts-ignore
|
||||
private offlineFlatPackages: Map<string, Record<string, Uint8Array>> | null = null;
|
||||
// @ts-ignore
|
||||
private offlineFlatEntryBase: string | null = null;
|
||||
private readonly offlinePackagesPrefix = "Packages/";
|
||||
// @ts-ignore
|
||||
private isOfflineImportContext = false;
|
||||
|
||||
constructor(viewer:Viewer) {
|
||||
this.viewer = viewer;
|
||||
|
||||
@ -1199,4 +1265,140 @@ export class Package {
|
||||
// 5. 释放 viewer 引用(注意:不销毁 viewer,仅移除引用)
|
||||
this.viewer = null as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出离线包(单包或多包)
|
||||
*/
|
||||
async packOffline(offlineConfig: IOfflinePackConfig) {
|
||||
const mode = offlineConfig.mode || "multiple";
|
||||
const packageItems: Array<{ baseName: string; data: Uint8Array }> = [];
|
||||
const rawPackages: Array<{ baseName: string; files: Record<string, Uint8Array> }> = [];
|
||||
|
||||
const packResult = await this.pack({
|
||||
name: offlineConfig.name,
|
||||
layer: mode === "single" ? 1 : offlineConfig.layer,
|
||||
imageProcessLimit: offlineConfig.imageProcessLimit,
|
||||
zipUploadFun: async (zip: File) => {
|
||||
if (mode === "single") {
|
||||
return zip;
|
||||
}
|
||||
const data = new Uint8Array(await zip.arrayBuffer());
|
||||
const baseName = this.getOfflinePackageBaseName(zip.name);
|
||||
packageItems.push({ baseName, data });
|
||||
return zip;
|
||||
},
|
||||
rawUploadFun: mode === "single"
|
||||
? async (raw) => {
|
||||
const baseName = this.getOfflinePackageBaseName(raw.name);
|
||||
rawPackages.push({ baseName, files: raw.files });
|
||||
return raw;
|
||||
}
|
||||
: undefined,
|
||||
onZipProgress: offlineConfig.onZipProgress,
|
||||
onProgress: offlineConfig.onProgress,
|
||||
});
|
||||
|
||||
const entry = this.normalizeOfflinePackageName(offlineConfig.name);
|
||||
const entryBase = this.getOfflinePackageBaseName(entry);
|
||||
|
||||
if (mode === "single") {
|
||||
const filesMap: Record<string, Uint8Array> = {};
|
||||
|
||||
for (const item of rawPackages) {
|
||||
for (const [fileName, fileData] of Object.entries(item.files)) {
|
||||
const fullName = `${this.offlinePackagesPrefix}${item.baseName}/${fileName}`;
|
||||
if (!filesMap[fullName]) {
|
||||
filesMap[fullName] = fileData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
mode: "single",
|
||||
entry,
|
||||
entryBase,
|
||||
packagesPrefix: this.offlinePackagesPrefix,
|
||||
totalZipNumber: packResult.totalZipNumber,
|
||||
};
|
||||
filesMap["offline.json"] = strToU8(JSON.stringify(manifest));
|
||||
|
||||
const bundleName = this.normalizeOfflinePackageName(offlineConfig.bundleName || `${offlineConfig.name}-offline`);
|
||||
const bundleZip = await this.zipRawFiles(filesMap);
|
||||
const bundleFile = new File([bundleZip as any], bundleName, { type: "application/octet-stream" });
|
||||
|
||||
const result: OfflinePackResult = {
|
||||
mode: "single",
|
||||
entry,
|
||||
files: [bundleFile],
|
||||
totalSize: bundleFile.size,
|
||||
totalZipNumber: packResult.totalZipNumber,
|
||||
};
|
||||
|
||||
offlineConfig.onComplete && offlineConfig.onComplete(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const files = packageItems.map(item => new File([item.data as any], this.normalizeOfflinePackageName(item.baseName), { type: "application/octet-stream" }));
|
||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||||
const result: OfflinePackResult = {
|
||||
mode: "multiple",
|
||||
entry,
|
||||
files,
|
||||
totalSize,
|
||||
totalZipNumber: packResult.totalZipNumber,
|
||||
};
|
||||
|
||||
offlineConfig.onComplete && offlineConfig.onComplete(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原始文件映射压缩为 zip 数据
|
||||
*/
|
||||
private async zipRawFiles(files: Record<string, Uint8Array>): Promise<Uint8Array> {
|
||||
const jszip = new JSZip();
|
||||
for (const [name, data] of Object.entries(files)) {
|
||||
jszip.file(name, data);
|
||||
}
|
||||
return await jszip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一离线包名称(.astral)
|
||||
*/
|
||||
private normalizeOfflinePackageName(name: string): string {
|
||||
const baseName = name.split(/[\\/]/).pop() || name;
|
||||
const cleanName = baseName.split(/[?#]/)[0];
|
||||
const lower = cleanName.toLowerCase();
|
||||
if (lower.endsWith(".zip")) {
|
||||
return cleanName.slice(0, -4) + ".astral";
|
||||
}
|
||||
return cleanName + ".astral";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取离线包基础名称(不含后缀)
|
||||
*/
|
||||
private getOfflinePackageBaseName(name: string): string {
|
||||
const baseName = name.split(/[\\/]/).pop() || name;
|
||||
const cleanName = baseName.split(/[?#]/)[0];
|
||||
if (cleanName.toLowerCase().endsWith(".astral")) {
|
||||
return cleanName.slice(0, -7);
|
||||
}
|
||||
if (cleanName.toLowerCase().endsWith(".zip")) {
|
||||
return cleanName.slice(0, -4);
|
||||
}
|
||||
return cleanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试从文件名中提取UUID(处理"xxx (1).astral"等情况)
|
||||
*/
|
||||
// @ts-ignore
|
||||
private extractUuidFromName(name: string): string | null {
|
||||
const match = name.match(/[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 match ? match[0] : null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user