feat(scene-editor): 新增对接 JSZip 库以支持 ZIP 文件导入,优化导入逻辑

This commit is contained in:
xudan 2025-10-21 14:06:46 +08:00
parent 11cdde454c
commit 259c881469
3 changed files with 109 additions and 13 deletions

View File

@ -17,6 +17,7 @@
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.8.4", "axios": "^1.8.4",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",

70
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
dayjs: dayjs:
specifier: ^1.11.13 specifier: ^1.11.13
version: 1.11.13 version: 1.11.13
jszip:
specifier: ^3.10.1
version: 3.10.1
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@ -1004,6 +1007,9 @@ packages:
core-js@3.45.1: core-js@3.45.1:
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cosmiconfig@9.0.0: cosmiconfig@9.0.0:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -1380,6 +1386,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@5.1.3: immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
@ -1436,6 +1445,9 @@ packages:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'} engines: {node: '>=12.13'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -1467,6 +1479,9 @@ packages:
json-stable-stringify-without-jsonify@1.0.1: json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -1491,6 +1506,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -1630,6 +1648,9 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1765,6 +1786,9 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2: readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -1813,6 +1837,9 @@ packages:
rxjs@7.8.2: rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1: safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -1946,6 +1973,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shallow-equal@1.2.1: shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
@ -1987,6 +2017,9 @@ packages:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0: string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@ -3128,6 +3161,8 @@ snapshots:
core-js@3.45.1: {} core-js@3.45.1: {}
core-util-is@1.0.3: {}
cosmiconfig@9.0.0(typescript@5.7.3): cosmiconfig@9.0.0(typescript@5.7.3):
dependencies: dependencies:
env-paths: 2.2.1 env-paths: 2.2.1
@ -3535,6 +3570,8 @@ snapshots:
ignore@7.0.5: {} ignore@7.0.5: {}
immediate@3.0.6: {}
immutable@5.1.3: {} immutable@5.1.3: {}
import-fresh@3.3.1: import-fresh@3.3.1:
@ -3575,6 +3612,8 @@ snapshots:
is-what@4.1.16: {} is-what@4.1.16: {}
isarray@1.0.0: {}
isexe@2.0.0: {} isexe@2.0.0: {}
js-sdsl@4.3.0: {} js-sdsl@4.3.0: {}
@ -3597,6 +3636,13 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@ -3618,6 +3664,10 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
local-pkg@1.1.2: local-pkg@1.1.2:
@ -3773,6 +3823,8 @@ snapshots:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
pako@1.0.11: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
@ -3887,6 +3939,16 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2: readable-stream@3.6.2:
dependencies: dependencies:
inherits: 2.0.4 inherits: 2.0.4
@ -3948,6 +4010,8 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {} safe-buffer@5.2.1: {}
sass-embedded-all-unknown@1.91.0: sass-embedded-all-unknown@1.91.0:
@ -4053,6 +4117,8 @@ snapshots:
semver@7.7.2: {} semver@7.7.2: {}
setimmediate@1.0.5: {}
shallow-equal@1.2.1: {} shallow-equal@1.2.1: {}
shebang-command@2.0.0: shebang-command@2.0.0:
@ -4087,6 +4153,10 @@ snapshots:
is-fullwidth-code-point: 3.0.0 is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1 strip-ansi: 6.0.1
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0: string_decoder@1.3.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1

View File

@ -2,6 +2,7 @@
// //
import { DOOR_AREA_TYPE } from '@api/map/door-area'; import { DOOR_AREA_TYPE } from '@api/map/door-area';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import JSZip from 'jszip';
import { computed, onMounted, onUnmounted, provide, ref, type ShallowRef, shallowRef, watch } from 'vue'; import { computed, onMounted, onUnmounted, provide, ref, type ShallowRef, shallowRef, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -287,7 +288,7 @@ const toPush = () => {
// --- Import/Update Logic --- // --- Import/Update Logic ---
const importModalVisible = ref(false); const importModalVisible = ref(false);
const importMode = ref<'normal' | 'floor'>('normal'); const importMode = ref<'normal' | 'floor'>('floor');
const normalImportFileList = ref([]); const normalImportFileList = ref([]);
const normalImportKeepProperties = ref(true); const normalImportKeepProperties = ref(true);
const floorImportList = ref([{ name: 'F1', fileList: [], keepProperties: true }]); const floorImportList = ref([{ name: 'F1', fileList: [], keepProperties: true }]);
@ -319,13 +320,16 @@ const addFloor = () => {
}; };
const openImportModal = () => { const openImportModal = () => {
importMode.value = 'normal'; importMode.value = 'floor'; // 使
importModalVisible.value = true; importModalVisible.value = true;
//
floorImportList.value = [{ name: 'F1', fileList: [], keepProperties: true }];
OriginalProperties.value = []; OriginalProperties.value = [];
}; };
const handleImportConfirm = async () => { const handleImportConfirm = async () => {
try { try {
/* NORMAL_IMPORT_DISABLED
if (importMode.value === 'normal') { if (importMode.value === 'normal') {
if (normalImportFileList.value.length === 0) { if (normalImportFileList.value.length === 0) {
message.warn('请选择一个文件'); message.warn('请选择一个文件');
@ -366,7 +370,9 @@ const handleImportConfirm = async () => {
return; return;
} }
await processAndLoadSceneData(JSON.parse(sceneJsonString)); await processAndLoadSceneData(JSON.parse(sceneJsonString));
} else if (importMode.value === 'floor') { }
NORMAL_IMPORT_DISABLED */
if (importMode.value === 'floor') {
// === OriginalProperties === // === OriginalProperties ===
try { try {
if (floorImportList.value.some((f) => f.fileList.length === 0)) { if (floorImportList.value.some((f) => f.fileList.length === 0)) {
@ -402,11 +408,30 @@ const handleImportConfirm = async () => {
// 3) // 3)
const result = await postProcessJsonList(payload); const result = await postProcessJsonList(payload);
if (result.kind === 'zip') { if (result.kind === 'zip') {
const url = URL.createObjectURL(result.blob); try {
const filename = result.filename || 'converted_map.zip'; const zip = await JSZip.loadAsync(result.blob);
downloadFile(url, filename); const sceneFile = Object.values(zip.files).find((file) => file.name.toLowerCase().endsWith('.scene'));
URL.revokeObjectURL(url);
message.success('转换成功已下载ZIP结果'); if (sceneFile) {
const sceneJsonString = await sceneFile.async('string');
if (!sceneJsonString) {
message.error('ZIP中的 .scene 文件内容为空');
return;
}
await processAndLoadSceneData(JSON.parse(sceneJsonString));
message.success('已成功从ZIP包中解压并导入场景');
} else {
message.error('ZIP文件中未找到 .scene 格式的场景文件');
// 退
const url = URL.createObjectURL(result.blob);
const filename = result.filename || 'converted_map.zip';
downloadFile(url, filename);
URL.revokeObjectURL(url);
}
} catch (zipError) {
console.error('处理ZIP文件失败:', zipError);
message.error('自动解压导入失败请检查ZIP文件是否有效');
}
} else { } else {
message.success(result?.data?.message || '转换成功'); message.success(result?.data?.message || '转换成功');
console.log('MapConverter JSON 响应:', result.data); console.log('MapConverter JSON 响应:', result.data);
@ -765,12 +790,12 @@ const handleFloorChange = async (value: any) => {
<a-modal v-model:visible="importModalVisible" title="导入场景" :footer="null" width="800px"> <a-modal v-model:visible="importModalVisible" title="导入场景" :footer="null" width="800px">
<div class="import-modal-content"> <div class="import-modal-content">
<div class="mode-switcher"> <!-- <div class="mode-switcher">
<div :class="['mode-tab', { active: importMode === 'normal' }]" @click="importMode = 'normal'">普通导入</div> <div :class="['mode-tab', { active: importMode === 'normal' }]" @click="importMode = 'normal'">普通导入</div>
<div :class="['mode-tab', { active: importMode === 'floor' }]" @click="importMode = 'floor'">分区域/楼层</div> <div :class="['mode-tab', { active: importMode === 'floor' }]" @click="importMode = 'floor'">分区域/楼层</div>
</div> </div> -->
<div class="core-content"> <div class="core-content">
<div v-if="importMode === 'normal'"> <!-- <div v-if="importMode === 'normal'">
<a-upload-dragger <a-upload-dragger
v-model:fileList="normalImportFileList" v-model:fileList="normalImportFileList"
name="file" name="file"
@ -808,9 +833,9 @@ const handleFloorChange = async (value: any) => {
<div style="margin-top: 8px; color: #999; font-size: 12px"> <div style="margin-top: 8px; color: #999; font-size: 12px">
提示支持 .scene .smap若需上传多个文件请使用分楼层/批量模式 提示支持 .scene .smap若需上传多个文件请使用分楼层/批量模式
</div> </div>
</div> </div> -->
<!-- 分区域/楼层模式的内容将在这里添加 --> <!-- 分区域/楼层模式的内容将在这里添加 -->
<div v-else> <div>
<div v-for="(floor, index) in floorImportList" :key="index" class="floor-uploader-item"> <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-input v-model:value="floor.name" placeholder="楼层/区域名称" style="width: 120px; margin-bottom: 8px" />
<a-upload-dragger <a-upload-dragger