feat: 在场景编辑器中添加导入和导出功能,支持 SMAP 和 IRAY 格式,优化编辑服务构造函数以接收场景ID
This commit is contained in:
parent
0fe2b966fc
commit
b5a3fd761c
348
src/components/modal/MapConverterModal.vue
Normal file
348
src/components/modal/MapConverterModal.vue
Normal file
@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<a-modal :visible="visible" title="地图格式转换" @cancel="handleCancel" :footer="null" width="600px" height="600px">
|
||||
<div class="converter-container">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择转换类型">
|
||||
<a-radio-group v-model:value="convertType" button-style="solid" style="width: 100%">
|
||||
<a-radio-button value="scene" style="width: 33.33%">SMAP转Scene</a-radio-button>
|
||||
<a-radio-button value="iray" style="width: 33.33%">SMAP转IRAY</a-radio-button>
|
||||
<a-radio-button value="scene-to-smap" style="width: 33.33%">Scene更新SMAP</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="fileInputLabel">
|
||||
<a-upload-dragger
|
||||
:before-upload="() => false"
|
||||
@change="handleFile1Change"
|
||||
:file-list="fileList1"
|
||||
:max-count="1"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽文件到这里上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="sceneInputLabel">
|
||||
<a-upload-dragger
|
||||
:before-upload="() => false"
|
||||
@change="handleFile2Change"
|
||||
:file-list="fileList2"
|
||||
:max-count="1"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<inbox-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽文件到这里上传</p>
|
||||
</a-upload-dragger>
|
||||
</a-form-item>
|
||||
|
||||
<div v-if="convertType === 'iray'" class="iray-params">
|
||||
<p class="param-section-title">地图参数设置 (IRAY转换必填)</p>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="地图宽度 (mm)">
|
||||
<a-input-number v-model:value="irayParams.mapWidth" placeholder="94744" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="地图高度 (mm)">
|
||||
<a-input-number v-model:value="irayParams.mapHeight" placeholder="79960" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="X最小值 (mm)">
|
||||
<a-input-number v-model:value="irayParams.xAttrMin" placeholder="-46762" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="Y最小值 (mm)">
|
||||
<a-input-number v-model:value="irayParams.yAttrMin" placeholder="-63362" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleConvert" :loading="loading" block>{{
|
||||
convertType === 'iray' ? '上传、转换并下载' : '上传并转换'
|
||||
}}</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="downloadUrl">
|
||||
<a-button type="primary" block @click="handleDownload">下载转换后的文件</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item label="转换结果" v-if="result">
|
||||
<a-textarea :value="result" :rows="8" read-only />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { InboxOutlined } from '@ant-design/icons-vue';
|
||||
import http from '@core/http';
|
||||
import { message, type UploadChangeParam } from 'ant-design-vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
/**
|
||||
* 调用地图转换API
|
||||
* @param convertType 转换类型
|
||||
* @param formData 包含文件和参数的FormData
|
||||
* @returns 转换结果
|
||||
*/
|
||||
const convertMap = async (convertType: string, formData: FormData) => {
|
||||
let apiUrl = '';
|
||||
switch (convertType) {
|
||||
case 'iray':
|
||||
apiUrl = import.meta.env.VWED_API_BASE_URL + '/api/vwed-map-converter/smap-to-iray';
|
||||
break;
|
||||
case 'scene-to-smap':
|
||||
apiUrl = import.meta.env.VWED_API_BASE_URL + '/api/vwed-map-converter/scene-to-smap';
|
||||
break;
|
||||
default:
|
||||
apiUrl = import.meta.env.VWED_API_BASE_URL + '/api/vwed-map-converter/smap-to-scene';
|
||||
}
|
||||
|
||||
// 如果是iray转换,需要特殊处理二进制响应
|
||||
if (convertType === 'iray') {
|
||||
const response = await http.post(apiUrl, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const blob = response?.data as unknown as Blob;
|
||||
if (!blob) {
|
||||
throw new Error('返回的二进制文件为空');
|
||||
}
|
||||
const disposition = response?.headers?.['content-disposition'] || '';
|
||||
let filename = 'map_package.zip'; // 默认文件名
|
||||
const filenameMatch = disposition.match(/filename=([^;]+)/i);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1].replace(/"/g, ''));
|
||||
}
|
||||
|
||||
return { blob, filename };
|
||||
} else {
|
||||
// 其他情况,保持原有的JSON响应处理
|
||||
return http.post<any>(apiUrl, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
}
|
||||
};
|
||||
// Modal visibility
|
||||
const visible = ref(false);
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
const showModal = () => {
|
||||
visible.value = true;
|
||||
resetState();
|
||||
};
|
||||
defineExpose({ showModal });
|
||||
|
||||
// Component state
|
||||
const loading = ref(false);
|
||||
const convertType = ref('scene');
|
||||
const file1 = ref<File | null>(null);
|
||||
const file2 = ref<File | null>(null);
|
||||
const fileList1 = ref([]);
|
||||
const fileList2 = ref([]);
|
||||
const result = ref('');
|
||||
const downloadUrl = ref('');
|
||||
const downloadFilename = ref('');
|
||||
|
||||
const irayParams = reactive({
|
||||
mapWidth: 94744,
|
||||
mapHeight: 79960,
|
||||
xAttrMin: -46762,
|
||||
yAttrMin: -63362,
|
||||
});
|
||||
|
||||
// UI labels based on convertType
|
||||
const fileInputLabel = computed(() => {
|
||||
if (convertType.value === 'scene-to-smap') {
|
||||
return '选择 .scene 文件 (必选)';
|
||||
}
|
||||
return '选择 .smap 文件 (必选)';
|
||||
});
|
||||
|
||||
const sceneInputLabel = computed(() => {
|
||||
if (convertType.value === 'iray') {
|
||||
return '选择 .scene 文件 (必选)';
|
||||
}
|
||||
if (convertType.value === 'scene-to-smap') {
|
||||
return '选择 .smap 文件 (必选)';
|
||||
}
|
||||
return '选择 .scene 文件 (可选)';
|
||||
});
|
||||
|
||||
// File handling
|
||||
const handleFile1Change = (info: UploadChangeParam) => {
|
||||
file1.value = (info.file as unknown as File) || null;
|
||||
fileList1.value = info.fileList as never[];
|
||||
};
|
||||
const handleFile2Change = (info: UploadChangeParam) => {
|
||||
file2.value = (info.file as unknown as File) || null;
|
||||
fileList2.value = info.fileList as never[];
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
loading.value = false;
|
||||
convertType.value = 'scene';
|
||||
file1.value = null;
|
||||
file2.value = null;
|
||||
fileList1.value = [];
|
||||
fileList2.value = [];
|
||||
result.value = '';
|
||||
downloadUrl.value = '';
|
||||
downloadFilename.value = '';
|
||||
};
|
||||
|
||||
watch(convertType, () => {
|
||||
file1.value = null;
|
||||
file2.value = null;
|
||||
fileList1.value = [];
|
||||
fileList2.value = [];
|
||||
result.value = '';
|
||||
revokeDownloadUrl();
|
||||
downloadUrl.value = '';
|
||||
downloadFilename.value = '';
|
||||
});
|
||||
|
||||
const revokeDownloadUrl = () => {
|
||||
if (downloadUrl.value) {
|
||||
URL.revokeObjectURL(downloadUrl.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!downloadUrl.value) return;
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl.value;
|
||||
link.download = downloadFilename.value;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// Conversion logic
|
||||
const handleConvert = async () => {
|
||||
// Validation
|
||||
if (!file1.value) {
|
||||
message.error('请选择第一个文件');
|
||||
return;
|
||||
}
|
||||
if ((convertType.value === 'iray' || convertType.value === 'scene-to-smap') && !file2.value) {
|
||||
message.error('请选择第二个文件');
|
||||
return;
|
||||
}
|
||||
if (convertType.value === 'iray') {
|
||||
if (!irayParams.mapWidth || !irayParams.mapHeight || !irayParams.xAttrMin || !irayParams.yAttrMin) {
|
||||
message.error('IRAY转换需要填写所有地图参数');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke previous URL before creating a new one
|
||||
revokeDownloadUrl();
|
||||
|
||||
const formData = new FormData();
|
||||
if (convertType.value === 'scene-to-smap') {
|
||||
formData.append('scene_file', file1.value);
|
||||
if (file2.value) formData.append('smap_file', file2.value);
|
||||
} else {
|
||||
formData.append('smap_file', file1.value);
|
||||
if (file2.value) formData.append('scene_file', file2.value);
|
||||
}
|
||||
|
||||
if (convertType.value === 'iray') {
|
||||
formData.append('map_width', String(irayParams.mapWidth));
|
||||
formData.append('map_height', String(irayParams.mapHeight));
|
||||
formData.append('x_attr_min', String(irayParams.xAttrMin));
|
||||
formData.append('y_attr_min', String(irayParams.yAttrMin));
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
result.value = '正在上传并转换...';
|
||||
downloadUrl.value = '';
|
||||
|
||||
try {
|
||||
// SMAP转IRAY时,API会返回二进制流,需要特殊处理
|
||||
if (convertType.value === 'iray') {
|
||||
// convertMap内部已处理responseType,这里直接获取返回的blob和filename
|
||||
const { blob, filename } = await convertMap(convertType.value, formData);
|
||||
console.log('blob', blob);
|
||||
if (blob && blob.size > 0) {
|
||||
revokeDownloadUrl(); // 清理旧的URL
|
||||
downloadUrl.value = URL.createObjectURL(blob);
|
||||
downloadFilename.value = filename;
|
||||
result.value = `转换成功!文件 ${filename} 已开始自动下载。`;
|
||||
message.success('IRAY地图包已生成,将自动开始下载。');
|
||||
// 不再显示下载按钮,而是直接触发下载
|
||||
handleDownload();
|
||||
// 隐藏下载按钮,因为已经自动下载
|
||||
setTimeout(() => {
|
||||
downloadUrl.value = '';
|
||||
}, 100);
|
||||
} else {
|
||||
throw new Error('返回的二进制文件为空');
|
||||
}
|
||||
} else {
|
||||
// 其他转换类型保持原有的JSON处理逻辑
|
||||
const data = await convertMap(convertType.value, formData);
|
||||
if (data.code == 200 || data.success) {
|
||||
result.value = '转换成功!';
|
||||
if (convertType.value === 'scene' && data.data) {
|
||||
const sceneContent = JSON.stringify(data.data, null, 2);
|
||||
const blob = new Blob([sceneContent], { type: 'application/json' });
|
||||
downloadUrl.value = URL.createObjectURL(blob);
|
||||
downloadFilename.value = data.data.output_filename || 'converted.scene';
|
||||
} else if (convertType.value === 'scene-to-smap' && data.data) {
|
||||
const smapContent = JSON.stringify(data.data?.data, null, 2);
|
||||
const blob = new Blob([smapContent], { type: 'application/json' });
|
||||
downloadUrl.value = URL.createObjectURL(blob);
|
||||
downloadFilename.value = data.data?.output_file ? data.data.output_file.split(/[/\\]/).pop() : 'updated.smap';
|
||||
}
|
||||
} else {
|
||||
result.value = `转换失败: ${data.message || '未知错误'}`;
|
||||
message.error(`转换失败: ${data.message || '未知错误'}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
result.value = '请求失败:' + err;
|
||||
message.error('转换失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.converter-container {
|
||||
padding: 16px;
|
||||
max-height: calc(700px - 110px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.iray-params {
|
||||
margin-top: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.param-section-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.download-link {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.download-link:hover {
|
||||
color: inherit !important;
|
||||
}
|
||||
</style>
|
115
src/hooks/useMapConversion.ts
Normal file
115
src/hooks/useMapConversion.ts
Normal file
@ -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<string | null> => {
|
||||
isConverting.value = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('smap_file', smapFile);
|
||||
const response = await mapConverterHttp.post<any>(`${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<any>(`${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,
|
||||
};
|
||||
}
|
@ -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<Props>();
|
||||
|
||||
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<string>('');
|
||||
|
||||
// 监听标题变化,动态更新页面标题
|
||||
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<HTMLDivElement>();
|
||||
const editor = shallowRef<EditorService>();
|
||||
// 左侧边栏元素引用(用于 Ctrl/Cmd+F 聚焦搜索框)
|
||||
const leftSiderEl = shallowRef<HTMLElement>();
|
||||
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<EditorService>);
|
||||
|
||||
// 监听自动生成库位对话框事件
|
||||
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<boolean>(false);
|
||||
watch(editable, (v) => editor.value?.setState(v));
|
||||
|
||||
// 左侧侧边栏折叠状态
|
||||
const leftCollapsed = ref<boolean>(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<boolean>(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 = () => {
|
||||
<span>{{ $t('启用编辑器') }}</span>
|
||||
</a-button>
|
||||
<a-button @click="toPush">{{ $t('推送') }}</a-button>
|
||||
<a-button @click="importScene">{{ $t('导入') }}</a-button>
|
||||
<a-button @click="importBinTask">导入Bintask</a-button>
|
||||
<a-button @click="exportScene">{{ $t('导出') }}</a-button>
|
||||
|
||||
<a-dropdown-button @click="importScene" :loading="isConverting">
|
||||
{{ $t('导入') }}
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="importSmap">导入 SMAP (.smap)</a-menu-item>
|
||||
<a-menu-item key="2" @click="importBinTask">导入 Bintask (.xlsx)</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
|
||||
<a-dropdown-button @click="exportScene" :loading="isConverting">
|
||||
{{ $t('导出') }}
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="1" @click="exportAsSmap">导出为 SMAP (.smap)</a-menu-item>
|
||||
<a-menu-item key="2" @click="exportAsIray">导出为 IRAY (.zip)</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown-button>
|
||||
</a-space>
|
||||
</a-flex>
|
||||
</a-layout-header>
|
||||
@ -325,10 +347,8 @@ const handleAutoCreateStorageCancel = () => {
|
||||
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
|
||||
</div>
|
||||
|
||||
<!-- 批量编辑工具栏 - 只在编辑模式下显示 -->
|
||||
<BatchEditToolbar v-if="editable && editor" :token="EDITOR_KEY" />
|
||||
|
||||
<!-- 自动生成库位对话框 -->
|
||||
<AutoCreateStorageModal
|
||||
v-model:visible="autoCreateStorageVisible"
|
||||
:area-name="autoCreateStorageData.areaName"
|
||||
|
Loading…
x
Reference in New Issue
Block a user