feat: 在场景编辑器中新增导入场景的模态框,支持普通导入和分区域/楼层导入,优化文件选择和处理逻辑,提升用户体验

This commit is contained in:
xudan 2025-10-16 18:26:13 +08:00
parent a4f0027ee1
commit fa5c88ad26

View File

@ -256,18 +256,139 @@ const toPush = () => {
};
// --- Import/Update Logic ---
const importScene = async () => {
const file = await selectFile('.scene');
if (!file) return;
const jsonString = await decodeTextFile(file);
if (!jsonString) {
message.error('文件内容为空或无法读取');
return;
const importModalVisible = ref(false);
const importMode = ref<'normal' | 'floor'>('normal');
const normalImportFileList = ref([]);
const normalImportKeepProperties = ref(true);
const floorImportList = ref([{ name: 'F1', fileList: [], keepProperties: true }]);
const fileTableColumns = [
{ title: '文件名', dataIndex: 'name', key: 'name' },
{ title: '格式', dataIndex: 'format', key: 'format', width: 100 },
{ title: '文件大小', dataIndex: 'size', key: 'size', width: 120 },
];
const handleNormalFileChange = (info: any) => {
//
let fileList = [...info.fileList];
fileList = fileList.slice(-1);
normalImportFileList.value = fileList;
};
const handleFloorFileChange = (info: any, index: number) => {
floorImportList.value[index].fileList = [...info.fileList];
};
const addFloor = () => {
floorImportList.value.push({
name: `F${floorImportList.value.length + 1}`,
fileList: [],
keepProperties: true,
});
};
const mergeSceneObjects = (scenes: any[]): any => {
if (!scenes || scenes.length === 0) {
return {};
}
try {
const sceneData = JSON.parse(jsonString);
return scenes.reduce((merged, currentScene) => {
const newMerged = { ...merged, ...currentScene };
if (currentScene.pens && Array.isArray(currentScene.pens)) {
const penMap = new Map(merged.pens?.map((p: any) => [p.id, p]) ?? []);
currentScene.pens.forEach((pen: any) => {
if (pen.id) {
penMap.set(pen.id, pen);
}
});
newMerged.pens = Array.from(penMap.values());
}
return newMerged;
}, {});
};
const openImportModal = () => {
importMode.value = 'normal';
importModalVisible.value = true;
};
const handleImportConfirm = async () => {
try {
if (importMode.value === 'normal') {
if (normalImportFileList.value.length === 0) {
message.warn('请选择一个文件');
return;
}
const file = normalImportFileList.value[0].originFileObj as File;
const fileName = file.name.toLowerCase();
let sceneJsonString: string | null = null;
if (fileName.endsWith('.smap')) {
// SMAP
if (normalImportKeepProperties.value) {
//
const currentSceneJson = editor.value?.save();
if (!currentSceneJson) {
message.error('无法获取当前场景数据,请确保场景不为空');
return;
}
const sceneBlob = new Blob([currentSceneJson], { type: 'application/json' });
const sceneFile = new File([sceneBlob], `${title.value || 'current'}.scene`, {
type: 'application/json',
});
sceneJsonString = await convertSmapToScene(file, sceneFile);
} else {
//
sceneJsonString = await convertSmapToScene(file);
}
} else if (fileName.endsWith('.scene')) {
// SCENE
sceneJsonString = await decodeTextFile(file);
} else {
message.error('不支持的文件格式。请选择 .scene 或 .smap 文件。');
return;
}
if (!sceneJsonString) {
message.error('文件内容为空或转换失败');
return;
}
await processAndLoadSceneData(JSON.parse(sceneJsonString));
} else if (importMode.value === 'floor') {
if (floorImportList.value.some((f) => f.fileList.length === 0)) {
message.warn('每个楼层都需要选择至少一个文件');
return;
}
const floorJsons = await Promise.all(
floorImportList.value.map(async (floor) => {
const sceneObjectsForFloor = await Promise.all(
floor.fileList.map(async (fileInfo: any) => {
const file = fileInfo.originFileObj as File;
const jsonString = await decodeTextFile(file);
if (!jsonString) {
throw new Error(`楼层 ${floor.name} 中的文件 ${file.name} 为空`);
}
return JSON.parse(jsonString);
}),
);
return mergeSceneObjects(sceneObjectsForFloor);
}),
);
await processAndLoadSceneData(floorJsons);
}
importModalVisible.value = false;
//
normalImportFileList.value = [];
floorImportList.value = [{ name: 'F1', fileList: [], keepProperties: true }];
} catch (error) {
console.error('导入失败:', error);
message.error(`导入失败: ${(error as Error).message || '请检查文件格式或内容。'}`);
}
};
const processAndLoadSceneData = async (sceneData: any) => {
try {
//
if (Array.isArray(sceneData)) {
requireImportTransform.value = true;
@ -304,8 +425,10 @@ const importScene = async () => {
await editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
}
} catch (error) {
console.error('场景文件解析失败:', error);
message.error('导入失败文件不是有效的JSON格式。');
console.error('场景文件解析或加载失败:', error);
message.error('导入失败文件不是有效的JSON格式或数据无法加载。');
//
throw error;
}
};
@ -489,7 +612,7 @@ const handleFloorChange = async (value: any) => {
</a-button>
<a-button @click="toPush">{{ $t('推送') }}</a-button>
<a-dropdown-button @click="importScene" :loading="isConverting">
<a-dropdown-button @click="openImportModal" :loading="isConverting">
{{ $t('导入') }}
<template #overlay>
<a-menu>
@ -570,6 +693,99 @@ const handleFloorChange = async (value: any) => {
<ImportSmapModal v-model:visible="importSmapModalVisible" @confirm="handleImportSmapConfirm" />
<a-modal v-model:visible="importModalVisible" title="导入场景" :footer="null" width="800px">
<div class="import-modal-content">
<div class="mode-switcher">
<div :class="['mode-tab', { active: importMode === 'normal' }]" @click="importMode = 'normal'">普通导入</div>
<div :class="['mode-tab', { active: importMode === 'floor' }]" @click="importMode = 'floor'">分区域/楼层</div>
</div>
<div class="core-content">
<div v-if="importMode === 'normal'">
<a-upload-dragger
v-model:fileList="normalImportFileList"
name="file"
:multiple="false"
@change="handleNormalFileChange"
:before-upload="() => false"
>
<p class="ant-upload-drag-icon">
<i class="icon upload size-48" />
</p>
<p class="ant-upload-text">点击或拖拽上传文件</p>
</a-upload-dragger>
<a-table
v-if="normalImportFileList.length"
:columns="fileTableColumns"
:data-source="normalImportFileList"
:pagination="false"
size="small"
class="mt-16"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<span>{{ record.name }}</span>
</template>
<template v-if="column.key === 'format'">
<span>{{ record.name.split('.').pop() }}</span>
</template>
<template v-if="column.key === 'size'">
<span>{{ (record.size / 1024).toFixed(2) }}kb</span>
</template>
</template>
</a-table>
<a-checkbox v-model:checked="normalImportKeepProperties" class="mt-16"> 保留原属性 </a-checkbox>
</div>
<!-- 分区域/楼层模式的内容将在这里添加 -->
<div v-else>
<div v-for="(floor, index) in floorImportList" :key="index" class="floor-uploader-item">
<a-input v-model:value="floor.name" placeholder="楼层/区域名称" style="width: 120px; margin-bottom: 8px" />
<a-upload-dragger
v-model:fileList="floor.fileList"
name="file"
:multiple="true"
@change="(info) => handleFloorFileChange(info, index)"
:before-upload="() => false"
>
<p class="ant-upload-drag-icon">
<i class="icon upload size-48" />
</p>
<p class="ant-upload-text">点击或拖拽上传文件</p>
</a-upload-dragger>
<a-table
v-if="floor.fileList.length"
:columns="fileTableColumns"
:data-source="floor.fileList"
:pagination="false"
size="small"
class="mt-16"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<span>{{ record.name }}</span>
</template>
<template v-if="column.key === 'format'">
<span>{{ record.name.split('.').pop() }}</span>
</template>
<template v-if="column.key === 'size'">
<span>{{ (record.size / 1024).toFixed(2) }}kb</span>
</template>
</template>
</a-table>
<a-checkbox v-model:checked="floor.keepProperties" class="mt-16"> 保留原属性 </a-checkbox>
</div>
<a-button @click="addFloor" class="mt-16"> + 添加楼层 </a-button>
</div>
</div>
<div class="action-buttons">
<span class="hint-text">*非scene格式文件将转为scene导入场景中</span>
<a-space>
<a-button @click="importModalVisible = false">取消</a-button>
<a-button type="primary" @click="handleImportConfirm">导入</a-button>
</a-space>
</div>
</div>
</a-modal>
<ExportConverterModal v-model:visible="exportModalVisible" @confirm="handleExportConfirm" />
<AutoCreateStorageModal
@ -663,4 +879,57 @@ const handleFloorChange = async (value: any) => {
:deep(.ant-tabs-tab) {
padding: 14px 12px !important;
}
.import-modal-content {
.mode-switcher {
display: flex;
margin-bottom: 16px;
.mode-tab {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #d9d9d9;
background-color: #fafafa;
&:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-left: none;
}
&.active {
background-color: #0dbb8a;
color: white;
border-color: #0dbb8a;
}
}
}
.core-content {
min-height: 200px;
/* 后续添加具体内容样式 */
}
.action-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
.hint-text {
color: red;
font-size: 12px;
}
}
}
.floor-uploader-item {
border: 1px solid #f0f0f0;
padding: 16px;
border-radius: 4px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
</style>