diff --git a/src/components/modal/ImportSmapModal.vue b/src/components/modal/ImportSmapModal.vue index 7072209..cefc49f 100644 --- a/src/components/modal/ImportSmapModal.vue +++ b/src/components/modal/ImportSmapModal.vue @@ -1,8 +1,7 @@ @@ -50,7 +56,7 @@ const beforeUpload: UploadProps['beforeUpload'] = (file) => { diff --git a/src/components/modal/MapConverterModal.vue b/src/components/modal/MapConverterModal.vue index c541295..7389399 100644 --- a/src/components/modal/MapConverterModal.vue +++ b/src/components/modal/MapConverterModal.vue @@ -2,11 +2,11 @@
- + - SMAP转Scene - SMAP转IRAY - Scene更新SMAP + SMAP 转 Scene + SMAP 转 IRAY + Scene 转 SMAP @@ -16,11 +16,12 @@ @change="handleFile1Change" :file-list="fileList1" :max-count="1" + :accept="file1Accept" >

-

点击或拖拽文件到这里上传

+

点击或将文件拖拽到此处上传

@@ -30,16 +31,17 @@ @change="handleFile2Change" :file-list="fileList2" :max-count="1" + :accept="file2Accept" >

-

点击或拖拽文件到这里上传

+

点击或将文件拖拽到此处上传

-

地图参数设置 (IRAY转换必填)

+

地图参数设置(SMAP 转 IRAY)

@@ -54,12 +56,12 @@ - + - + @@ -68,14 +70,14 @@ {{ - convertType === 'iray' ? '上传、转换并下载' : '上传并转换' + convertType === 'iray' ? '上传并转换(返回压缩包)' : '上传并转换' }} 下载转换后的文件 - +
@@ -86,27 +88,23 @@ 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) => { +import { computed, reactive, ref, watch, type Ref } from 'vue'; + +type ConversionType = 'scene' | 'iray' | 'scene-to-smap'; + +const convertMap = async (convertType: ConversionType, formData: FormData) => { let apiUrl = ''; switch (convertType) { case 'iray': - apiUrl = import.meta.env.VWED_API_BASE_URL + '/api/vwed-map-converter/smap-to-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'; + 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'; + 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' }, @@ -115,24 +113,23 @@ const convertMap = async (convertType: string, formData: FormData) => { const blob = response?.data as unknown as Blob; if (!blob) { - throw new Error('返回的二进制文件为空'); + throw new Error('返回的压缩包为空'); } const disposition = response?.headers?.['content-disposition'] || ''; - let filename = 'map_package.zip'; // 默认文件名 + 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(apiUrl, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); } + + return http.post(apiUrl, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); }; -// Modal visibility + const visible = ref(false); const handleCancel = () => { visible.value = false; @@ -143,50 +140,95 @@ const showModal = () => { }; defineExpose({ showModal }); -// Component state const loading = ref(false); -const convertType = ref('scene'); +const convertType = ref('scene'); const file1 = ref(null); const file2 = ref(null); -const fileList1 = ref([]); -const fileList2 = ref([]); +const fileList1 = ref([]); +const fileList2 = ref([]); const result = ref(''); const downloadUrl = ref(''); const downloadFilename = ref(''); -const irayParams = reactive({ +const defaultIrayParams = { mapWidth: 94744, mapHeight: 79960, xAttrMin: -46762, yAttrMin: -63362, -}); +}; +const irayParams = reactive({ ...defaultIrayParams }); -// UI labels based on convertType const fileInputLabel = computed(() => { if (convertType.value === 'scene-to-smap') { - return '选择 .scene 文件 (必选)'; + return '上传 Scene 文件(必选)'; } - return '选择 .smap 文件 (必选)'; + return '上传 SMAP 文件(必选)'; }); const sceneInputLabel = computed(() => { if (convertType.value === 'iray') { - return '选择 .scene 文件 (必选)'; + return '上传 Scene 文件(必选)'; } if (convertType.value === 'scene-to-smap') { - return '选择 .smap 文件 (必选)'; + return '上传 SMAP 文件(必选)'; } - return '选择 .scene 文件 (可选)'; + return '上传 Scene 文件(可选)'; }); -// File handling +const file1Accept = computed(() => (convertType.value === 'scene-to-smap' ? '.scene' : '.smap')); +const file2Accept = computed(() => (convertType.value === 'scene-to-smap' ? '.smap' : '.scene')); + +const file1ErrorMessage = computed(() => + convertType.value === 'scene-to-smap' ? '请上传 .scene 格式的文件' : '请上传 .smap 格式的文件', +); +const file2ErrorMessage = computed(() => + convertType.value === 'scene-to-smap' ? '请上传 .smap 格式的文件' : '请上传 .scene 格式的文件', +); + +const parseAccept = (accept: string) => + accept + .split(',') + .map((item) => item.trim().toLowerCase()) + .filter(Boolean); + +const getFileExtension = (fileName: string) => { + const dotIndex = fileName.lastIndexOf('.'); + return dotIndex >= 0 ? fileName.slice(dotIndex).toLowerCase() : ''; +}; + +const isExtensionAllowed = (file: File, accept: string) => { + if (!accept) return true; + const allowed = parseAccept(accept); + if (!allowed.length) return true; + const ext = getFileExtension(file.name || ''); + return allowed.includes(ext); +}; + +const updateFileSelection = ( + info: UploadChangeParam, + fileRef: Ref, + listRef: Ref, + accept: string, + errorMessage: string, +) => { + const rawFile = (info.file?.originFileObj as File | undefined) ?? null; + + if (rawFile && !isExtensionAllowed(rawFile, accept)) { + message.error(errorMessage); + fileRef.value = null; + listRef.value = []; + return; + } + + fileRef.value = rawFile; + listRef.value = info.fileList as UploadChangeParam['fileList']; +}; + const handleFile1Change = (info: UploadChangeParam) => { - file1.value = (info.file as unknown as File) || null; - fileList1.value = info.fileList as never[]; + updateFileSelection(info, file1, fileList1, file1Accept.value, file1ErrorMessage.value); }; const handleFile2Change = (info: UploadChangeParam) => { - file2.value = (info.file as unknown as File) || null; - fileList2.value = info.fileList as never[]; + updateFileSelection(info, file2, fileList2, file2Accept.value, file2ErrorMessage.value); }; const resetState = () => { @@ -197,8 +239,10 @@ const resetState = () => { fileList1.value = []; fileList2.value = []; result.value = ''; + revokeDownloadUrl(); downloadUrl.value = ''; downloadFilename.value = ''; + Object.assign(irayParams, defaultIrayParams); }; watch(convertType, () => { @@ -228,26 +272,26 @@ const handleDownload = () => { document.body.removeChild(link); }; -// Conversion logic const handleConvert = async () => { - // Validation if (!file1.value) { - message.error('请选择第一个文件'); + message.error('请先上传主文件'); return; } if ((convertType.value === 'iray' || convertType.value === 'scene-to-smap') && !file2.value) { - message.error('请选择第二个文件'); + message.error('请上传辅助文件'); return; } if (convertType.value === 'iray') { - if (!irayParams.mapWidth || !irayParams.mapHeight || !irayParams.xAttrMin || !irayParams.yAttrMin) { - message.error('IRAY转换需要填写所有地图参数'); + const { mapWidth, mapHeight, xAttrMin, yAttrMin } = irayParams; + if (!mapWidth || !mapHeight || xAttrMin === undefined || yAttrMin === undefined) { + message.error('IRAY 转换需要填写完整的地图参数'); return; } } - // Revoke previous URL before creating a new one revokeDownloadUrl(); + downloadUrl.value = ''; + downloadFilename.value = ''; const formData = new FormData(); if (convertType.value === 'scene-to-smap') { @@ -267,52 +311,55 @@ const handleConvert = async () => { 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 + revokeDownloadUrl(); downloadUrl.value = URL.createObjectURL(blob); - downloadFilename.value = filename; - result.value = `转换成功!文件 ${filename} 已开始自动下载。`; - message.success('IRAY地图包已生成,将自动开始下载。'); - // 不再显示下载按钮,而是直接触发下载 + downloadFilename.value = filename || 'map_package.zip'; + result.value = `转换成功,文件 ${downloadFilename.value} 已开始自动下载。`; + message.success('IRAY 地图转换成功,已自动开始下载。'); handleDownload(); - // 隐藏下载按钮,因为已经自动下载 setTimeout(() => { + revokeDownloadUrl(); downloadUrl.value = ''; }, 100); } else { - throw new Error('返回的二进制文件为空'); + throw new Error('返回的压缩包为空'); } } else { - // 其他转换类型保持原有的JSON处理逻辑 const data = await convertMap(convertType.value, formData); - if (data.code == 200 || data.success) { - result.value = '转换成功!'; + 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' }); + revokeDownloadUrl(); downloadUrl.value = URL.createObjectURL(blob); downloadFilename.value = data.data.output_filename || 'converted.scene'; + message.success('转换成功,已生成 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' }); + revokeDownloadUrl(); downloadUrl.value = URL.createObjectURL(blob); - downloadFilename.value = data.data?.output_file ? data.data.output_file.split(/[/\\]/).pop() : 'updated.smap'; + const outputFile = data.data?.output_file + ? data.data.output_file.split(/[/\\]/).pop() + : undefined; + downloadFilename.value = outputFile || 'updated.smap'; + message.success('转换成功,已生成 SMAP 文件。'); } } else { - result.value = `转换失败: ${data.message || '未知错误'}`; - message.error(`转换失败: ${data.message || '未知错误'}`); + const errorText = data?.message || '未知错误'; + result.value = `转换失败:${errorText}`; + message.error(`转换失败:${errorText}`); } } } catch (err) { - result.value = '请求失败:' + err; + const errorMessage = err instanceof Error ? err.message : String(err); + result.value = `处理失败:${errorMessage}`; message.error('转换失败'); } finally { loading.value = false;