feat: 在场景编辑器中新增导入场景的模态框,支持普通导入和分区域/楼层导入,优化文件选择和处理逻辑,提升用户体验
This commit is contained in:
parent
a4f0027ee1
commit
fa5c88ad26
@ -256,18 +256,139 @@ const toPush = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- Import/Update Logic ---
|
// --- Import/Update Logic ---
|
||||||
const importScene = async () => {
|
const importModalVisible = ref(false);
|
||||||
const file = await selectFile('.scene');
|
const importMode = ref<'normal' | 'floor'>('normal');
|
||||||
if (!file) return;
|
const normalImportFileList = ref([]);
|
||||||
const jsonString = await decodeTextFile(file);
|
const normalImportKeepProperties = ref(true);
|
||||||
if (!jsonString) {
|
const floorImportList = ref([{ name: 'F1', fileList: [], keepProperties: true }]);
|
||||||
message.error('文件内容为空或无法读取');
|
|
||||||
|
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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!sceneJsonString) {
|
||||||
const sceneData = JSON.parse(jsonString);
|
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)) {
|
if (Array.isArray(sceneData)) {
|
||||||
requireImportTransform.value = true;
|
requireImportTransform.value = true;
|
||||||
@ -304,8 +425,10 @@ const importScene = async () => {
|
|||||||
await editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
|
await editor.value?.load('{}', editable.value, undefined, requireImportTransform.value);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('场景文件解析失败:', error);
|
console.error('场景文件解析或加载失败:', error);
|
||||||
message.error('导入失败:文件不是有效的JSON格式。');
|
message.error('导入失败:文件不是有效的JSON格式或数据无法加载。');
|
||||||
|
// 抛出错误以确保上层可以捕获
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -489,7 +612,7 @@ const handleFloorChange = async (value: any) => {
|
|||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="toPush">{{ $t('推送') }}</a-button>
|
<a-button @click="toPush">{{ $t('推送') }}</a-button>
|
||||||
|
|
||||||
<a-dropdown-button @click="importScene" :loading="isConverting">
|
<a-dropdown-button @click="openImportModal" :loading="isConverting">
|
||||||
{{ $t('导入') }}
|
{{ $t('导入') }}
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu>
|
<a-menu>
|
||||||
@ -570,6 +693,99 @@ const handleFloorChange = async (value: any) => {
|
|||||||
|
|
||||||
<ImportSmapModal v-model:visible="importSmapModalVisible" @confirm="handleImportSmapConfirm" />
|
<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" />
|
<ExportConverterModal v-model:visible="exportModalVisible" @confirm="handleExportConfirm" />
|
||||||
|
|
||||||
<AutoCreateStorageModal
|
<AutoCreateStorageModal
|
||||||
@ -663,4 +879,57 @@ const handleFloorChange = async (value: any) => {
|
|||||||
:deep(.ant-tabs-tab) {
|
:deep(.ant-tabs-tab) {
|
||||||
padding: 14px 12px !important;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user