feat: 在场景编辑器中新增导入场景的模态框,支持普通导入和分区域/楼层导入,优化文件选择和处理逻辑,提升用户体验
This commit is contained in:
parent
a4f0027ee1
commit
fa5c88ad26
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user