diff --git a/src/components/modal/MapConverterModal.vue b/src/components/modal/MapConverterModal.vue
new file mode 100644
index 0000000..ddc24b8
--- /dev/null
+++ b/src/components/modal/MapConverterModal.vue
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+ SMAP转Scene
+ SMAP转IRAY
+ Scene更新SMAP
+
+
+
+
+
+
+
+
+ 点击或拖拽文件到这里上传
+
+
+
+
+
+
+
+
+ 点击或拖拽文件到这里上传
+
+
+
+
+
地图参数设置 (IRAY转换必填)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ convertType === 'iray' ? '上传、转换并下载' : '上传并转换'
+ }}
+
+
+ 下载转换后的文件
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/hooks/useMapConversion.ts b/src/hooks/useMapConversion.ts
new file mode 100644
index 0000000..1a83e7e
--- /dev/null
+++ b/src/hooks/useMapConversion.ts
@@ -0,0 +1,115 @@
+import { message } from 'ant-design-vue';
+import axios from 'axios';
+import { ref } from 'vue';
+
+import { downloadFile } from '../services/utils';
+
+const vwedApi = '/vwedApi';
+const API_BASE_URL = '' + vwedApi + '/api/vwed-map-converter';
+
+// Create a new Axios instance specifically for map conversion APIs
+// to avoid conflicts with the global instance's baseURL.
+const mapConverterHttp = axios.create();
+
+export function useMapConversion() {
+ const isConverting = ref(false);
+
+ const convertSmapToScene = async (smapFile: File): Promise => {
+ isConverting.value = true;
+ try {
+ const formData = new FormData();
+ formData.append('smap_file', smapFile);
+ const response = await mapConverterHttp.post(`${API_BASE_URL}/smap-to-scene`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ // Note: axios wraps the response in a `data` object.
+ if (response.data.code === 200 || response.data.success) {
+ message.success('SMAP 转换为 Scene 成功!');
+ return JSON.stringify(response.data.data, null, 2);
+ } else {
+ throw new Error(response.data.message || '转换失败');
+ }
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.message || error.message || 'SMAP 转换为 Scene 失败';
+ message.error(errorMessage);
+ return null;
+ } finally {
+ isConverting.value = false;
+ }
+ };
+
+ const convertSceneToSmap = async (sceneJson: string, filename: string) => {
+ isConverting.value = true;
+ try {
+ const formData = new FormData();
+ const sceneBlob = new Blob([sceneJson], { type: 'application/json' });
+ formData.append('scene_file', sceneBlob, `${filename}.scene`);
+
+ const response = await mapConverterHttp.post(`${API_BASE_URL}/scene-to-smap`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+
+ if (response.data.code === 200 || response.data.success) {
+ const smapContent = JSON.stringify(response.data?.data, null, 2);
+ const blob = new Blob([smapContent], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const outputFilename = response.data?.output_file
+ ? response.data.output_file.split(/[/\\]/).pop()
+ : `${filename}.smap`;
+ downloadFile(url, outputFilename);
+ URL.revokeObjectURL(url);
+ message.success('Scene 转换为 SMAP 并下载成功!');
+ } else {
+ throw new Error(response.data.message || '转换失败');
+ }
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.message || error.message || 'Scene 转换为 SMAP 失败';
+ message.error(errorMessage);
+ } finally {
+ isConverting.value = false;
+ }
+ };
+
+ const convertSceneToIray = async (sceneJson: string, filename: string) => {
+ isConverting.value = true;
+ try {
+ const formData = new FormData();
+ const sceneBlob = new Blob([sceneJson], { type: 'application/json' });
+ formData.append('scene_file', sceneBlob, `${filename}.scene`);
+
+ const response = await mapConverterHttp.post(`${API_BASE_URL}/scene-to-iray`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ responseType: 'blob',
+ });
+
+ const blob = response?.data as unknown as Blob;
+ if (!blob || blob.size === 0) {
+ throw new Error('返回的二进制文件为空');
+ }
+
+ const disposition = response?.headers?.['content-disposition'] || '';
+ let downloadFilename = `${filename}.zip`; // Default
+ const filenameMatch = disposition.match(/filename=([^;]+)/i);
+ if (filenameMatch && filenameMatch[1]) {
+ downloadFilename = decodeURIComponent(filenameMatch[1].replace(/"/g, ''));
+ }
+
+ const url = URL.createObjectURL(blob);
+ downloadFile(url, downloadFilename);
+ URL.revokeObjectURL(url);
+ message.success('Scene 转换为 IRAY 并下载成功!');
+ } catch (error: any) {
+ const errorMessage = error.response?.data?.message || error.message || 'Scene 转换为 IRAY 失败';
+ message.error(errorMessage);
+ } finally {
+ isConverting.value = false;
+ }
+ };
+
+ return {
+ isConverting,
+ convertSmapToScene,
+ convertSceneToSmap,
+ convertSceneToIray,
+ };
+}
diff --git a/src/pages/scene-editor.vue b/src/pages/scene-editor.vue
index 40386aa..58df98f 100644
--- a/src/pages/scene-editor.vue
+++ b/src/pages/scene-editor.vue
@@ -8,6 +8,7 @@ import expandIcon from '../assets/icons/png/expand.png';
import foldIcon from '../assets/icons/png/fold.png';
import BatchEditToolbar from '../components/batch-edit-toolbar.vue';
import AutoCreateStorageModal from '../components/modal/auto-create-storage-modal.vue';
+import { useMapConversion } from '../hooks/useMapConversion';
import { EditorService } from '../services/editor.service';
import { useViewState } from '../services/useViewState';
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '../services/utils';
@@ -21,6 +22,7 @@ type Props = {
const props = defineProps();
const { t } = useI18n();
+const { isConverting, convertSmapToScene, convertSceneToSmap, convertSceneToIray } = useMapConversion();
//#region 接口
const readScene = async () => {
@@ -35,7 +37,6 @@ const saveScene = async () => {
const res = await saveSceneById(props.id, json);
if (!res) return Promise.reject('保存失败');
- // 保存成功后重置历史记录状态,表示当前场景已保存
if (editor.value?.store) {
editor.value.store.historyIndex = undefined;
editor.value.store.histories = [];
@@ -56,7 +57,6 @@ const pushScene = async () => {
const title = ref('');
-// 监听标题变化,动态更新页面标题
onMounted(() => {
document.title = '场景编辑器';
});
@@ -65,7 +65,6 @@ watch(
() => props.id,
async () => {
await readScene();
- // 在场景加载完成后自动保存和恢复视图状态
await handleAutoSaveAndRestoreViewState();
},
{ immediate: true, flush: 'post' },
@@ -73,11 +72,9 @@ watch(
const container = shallowRef();
const editor = shallowRef();
-// 左侧边栏元素引用(用于 Ctrl/Cmd+F 聚焦搜索框)
const leftSiderEl = shallowRef();
provide(EDITOR_KEY, editor);
-// 自动生成库位对话框相关状态
const autoCreateStorageVisible = ref(false);
const autoCreateStorageData = ref<{
areaName: string;
@@ -88,19 +85,14 @@ const autoCreateStorageData = ref<{
});
onMounted(() => {
- editor.value = new EditorService(container.value!);
-
- // 将 editor 存储到 store 中
+ editor.value = new EditorService(container.value!, props.id);
editorStore.setEditor(editor as ShallowRef);
-
- // 监听自动生成库位对话框事件
editor.value?.on('autoCreateStorageDialog', (data: any) => {
autoCreateStorageData.value = data;
autoCreateStorageVisible.value = true;
});
});
-// 判断事件目标是否在可编辑区域(输入框/文本域/可编辑元素)
const isTypingElement = (el: EventTarget | null) => {
const node = el as HTMLElement | null;
if (!node) return false;
@@ -108,7 +100,6 @@ const isTypingElement = (el: EventTarget | null) => {
return tag === 'input' || tag === 'textarea' || node.isContentEditable === true;
};
-// Ctrl/Cmd + F 快捷键聚焦左侧搜索输入框
const focusFindKeydownHandler = (event: KeyboardEvent) => {
const isFindKey = event.key === 'f' || event.key === 'F';
if ((event.ctrlKey || event.metaKey) && isFindKey && !isTypingElement(event.target)) {
@@ -121,14 +112,15 @@ const focusFindKeydownHandler = (event: KeyboardEvent) => {
input.focus();
try {
input.select?.();
- } catch {}
+ } catch {
+ //
+ }
}
}
}
};
onMounted(() => {
- // 监听 Ctrl/Cmd+F 聚焦左侧搜索框
document.addEventListener('keydown', focusFindKeydownHandler);
});
@@ -139,7 +131,6 @@ onUnmounted(() => {
const editable = ref(false);
watch(editable, (v) => editor.value?.setState(v));
-// 左侧侧边栏折叠状态
const leftCollapsed = ref(false);
const toPush = () => {
@@ -161,12 +152,39 @@ const toPush = () => {
});
};
+// --- Import Logic ---
const importScene = async () => {
const file = await selectFile('.scene');
if (!file || !file.size) return;
const json = await decodeTextFile(file);
- editor.value?.load(json, editable.value, undefined, true); // 第四个参数isImport=true
+ editor.value?.load(json, editable.value, undefined, true);
};
+
+const importSmap = async () => {
+ const file = await selectFile('.smap');
+ if (!file || !file.size) return;
+ const sceneJson = await convertSmapToScene(file);
+ if (sceneJson) {
+ editor.value?.load(sceneJson, editable.value, undefined, true);
+ }
+};
+
+const importBinTask = async () => {
+ try {
+ const file = await selectFile('.xlsx,.xls');
+ if (!file || !file.size) return;
+ const success = await importBinTaskExcel(props.id, file);
+ if (success) {
+ message.success('Bintask导入成功');
+ } else {
+ message.error('Bintask导入失败');
+ }
+ } catch (error: any) {
+ message.error(error.message || 'Bintask导入失败');
+ }
+};
+
+// --- Export Logic ---
const exportScene = () => {
const json = editor.value?.save();
if (!json) return;
@@ -177,21 +195,16 @@ const exportScene = () => {
URL.revokeObjectURL(url);
};
-// 导入Bintask Excel文件
-const importBinTask = async () => {
- try {
- const file = await selectFile('.xlsx,.xls');
- if (!file || !file.size) return;
+const exportAsSmap = async () => {
+ const json = editor.value?.save();
+ if (!json) return;
+ await convertSceneToSmap(json, title.value || 'unknown');
+};
- const success = await importBinTaskExcel(props.id, file);
- if (success) {
- message.success('Bintask导入成功');
- } else {
- message.error('Bintask导入失败');
- }
- } catch (error: any) {
- message.error(error.message || 'Bintask导入失败');
- }
+const exportAsIray = async () => {
+ const json = editor.value?.save();
+ if (!json) return;
+ await convertSceneToIray(json, title.value || 'unknown');
};
const show = ref(true);
@@ -218,13 +231,10 @@ const selectRobot = (id: string) => {
editor.value?.inactive();
};
-// 视图状态管理
const { saveViewState, autoSaveAndRestoreViewState, isSaving } = useViewState();
-// 保存当前视图状态
const handleSaveViewState = async () => {
if (!editor.value) return;
-
try {
await saveViewState(editor.value, props.id);
message.success('视图状态保存成功');
@@ -233,26 +243,21 @@ const handleSaveViewState = async () => {
}
};
-// 自动保存和恢复视图状态
const handleAutoSaveAndRestoreViewState = async () => {
if (!editor.value) return;
-
await autoSaveAndRestoreViewState(editor.value, props.id);
};
-// 返回到父级 iframe 的场景卡片
+
const backToCards = () => {
window.parent?.postMessage({ type: 'scene_return_to_cards' }, '*');
};
-// 处理自动生成库位确认
const handleAutoCreateStorageConfirm = (actionPoints: any[]) => {
if (!editor.value) return;
-
editor.value.autoCreateStorageLocations(autoCreateStorageData.value.areaName, actionPoints);
message.success(`已为 ${actionPoints.length} 个动作点自动生成库位`);
};
-// 处理自动生成库位取消
const handleAutoCreateStorageCancel = () => {
autoCreateStorageVisible.value = false;
};
@@ -275,9 +280,26 @@ const handleAutoCreateStorageCancel = () => {
{{ $t('启用编辑器') }}
{{ $t('推送') }}
- {{ $t('导入') }}
- 导入Bintask
- {{ $t('导出') }}
+
+
+ {{ $t('导入') }}
+
+
+ 导入 SMAP (.smap)
+ 导入 Bintask (.xlsx)
+
+
+
+
+
+ {{ $t('导出') }}
+
+
+ 导出为 SMAP (.smap)
+ 导出为 IRAY (.zip)
+
+
+
@@ -325,10 +347,8 @@ const handleAutoCreateStorageCancel = () => {
-
-