Browse Source

航线的导入导出

ctw
cuitw 3 weeks ago
parent
commit
37c517b086
  1. 37
      ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java
  2. 4
      ruoyi-ui/src/lang/en.js
  3. 4
      ruoyi-ui/src/lang/zh.js
  4. 18
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  5. 143
      ruoyi-ui/src/views/childRoom/index.vue
  6. 237
      ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue
  7. 8
      ruoyi-ui/src/views/dialogs/ExternalParamsDialog.vue
  8. 240
      ruoyi-ui/src/views/dialogs/ImportRoutesDialog.vue

37
ruoyi-admin/src/main/java/com/ruoyi/websocket/controller/RoomWebSocketController.java

@ -1,10 +1,8 @@
package com.ruoyi.websocket.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
@ -21,7 +19,6 @@ import com.ruoyi.system.domain.Rooms;
import com.ruoyi.system.service.IRoomsService;
import com.ruoyi.websocket.dto.RoomMemberDTO;
import com.ruoyi.websocket.service.RoomChatService;
import com.ruoyi.websocket.service.RoomRoomStateService;
import com.ruoyi.websocket.service.RoomWebSocketService;
/**
@ -44,9 +41,6 @@ public class RoomWebSocketController {
@Autowired
private IRoomsService roomsService;
@Autowired
private RoomRoomStateService roomRoomStateService;
private static final String TYPE_JOIN = "JOIN";
private static final String TYPE_LEAVE = "LEAVE";
private static final String TYPE_PING = "PING";
@ -188,13 +182,7 @@ public class RoomWebSocketController {
chatHistoryMsg.put("messages", chatHistory);
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", chatHistoryMsg);
Set<Long> visibleRouteIds = roomRoomStateService.getVisibleRouteIds(roomId);
if (visibleRouteIds != null && !visibleRouteIds.isEmpty()) {
Map<String, Object> roomStateMsg = new HashMap<>();
roomStateMsg.put("type", TYPE_ROOM_STATE);
roomStateMsg.put("visibleRouteIds", new ArrayList<>(visibleRouteIds));
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", roomStateMsg);
}
// 小房间内每人展示各自内容,新加入用户不同步他人的可见航线
}
private void handleLeave(Long roomId, String sessionId, LoginUser loginUser) {
@ -296,28 +284,9 @@ public class RoomWebSocketController {
messagingTemplate.convertAndSendToUser(loginUser.getUsername(), "/queue/private", resp);
}
/** 广播航线显隐变更,供其他设备实时同步;并持久化到 Redis 供新加入用户同步 */
/** 航线显隐变更:小房间内每人展示各自内容,不再广播和持久化 */
private void handleSyncRouteVisibility(Long roomId, Map<String, Object> body, String sessionId) {
if (body == null || !body.containsKey("routeId")) return;
Object routeIdObj = body.get("routeId");
boolean visible = body.get("visible") != null && Boolean.TRUE.equals(body.get("visible"));
Long routeId = null;
if (routeIdObj instanceof Number) {
routeId = ((Number) routeIdObj).longValue();
} else if (routeIdObj != null) {
try {
routeId = Long.parseLong(routeIdObj.toString());
} catch (NumberFormatException ignored) {}
}
if (routeId != null) {
roomRoomStateService.updateRouteVisibility(roomId, routeId, visible);
}
Map<String, Object> msg = new HashMap<>();
msg.put("type", TYPE_SYNC_ROUTE_VISIBILITY);
msg.put("routeId", body.get("routeId"));
msg.put("visible", visible);
msg.put("senderSessionId", sessionId);
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg);
// 小房间内每人展示各自内容,航线显隐不再同步给他人、不再持久化
}
/** 广播航点变更,供其他设备实时同步 */

4
ruoyi-ui/src/lang/en.js

@ -21,7 +21,9 @@ export default {
importATO: 'Import ATO',
importLayer: 'Import Layer',
importRoute: 'Import Route',
export: 'Export'
export: 'Export',
exportRoute: 'Export Routes',
exportPlan: 'Export Plan'
},
edit: {
routeEdit: 'Route Edit',

4
ruoyi-ui/src/lang/zh.js

@ -21,7 +21,9 @@ export default {
importATO: '导入ATO',
importLayer: '导入图层',
importRoute: '导入航线',
export: '导出'
export: '导出',
exportRoute: '导出航线',
exportPlan: '导出计划'
},
edit: {
routeEdit: '航线编辑',

18
ruoyi-ui/src/views/childRoom/TopHeader.vue

@ -56,7 +56,20 @@
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item @click.native="exportPlan">{{ $t('topHeader.file.export') }}</el-dropdown-item>
<el-dropdown-item class="submenu-item">
<span>{{ $t('topHeader.file.export') }}</span>
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
<div class="submenu-trigger"></div>
<el-dropdown-menu slot="dropdown" class="submenu">
<el-dropdown-item @click.native="exportRoute">{{ $t('topHeader.file.exportRoute') }}</el-dropdown-item>
<el-dropdown-item @click.native="exportPlan">{{ $t('topHeader.file.exportPlan') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@ -497,6 +510,9 @@ export default {
exportRoute() {
this.$emit('export-routes')
},
exportPlan() {
this.$emit('export-plan')
},

143
ruoyi-ui/src/views/childRoom/index.vue

@ -119,6 +119,7 @@
@import-ato="importATO"
@import-layer="importLayer"
@import-route="importRoute"
@export-routes="openExportRoutesDialog"
@export-plan="exportPlan"
@route-edit="routeEdit"
@military-marking="militaryMarking"
@ -385,6 +386,23 @@
@confirm="handleImportConfirm"
/>
<!-- 导出航线弹窗 -->
<export-routes-dialog
v-model="showExportRoutesDialog"
:routes="routes"
:plans="plans"
@export="handleExportRoutes"
/>
<!-- 导入航线弹窗 -->
<import-routes-dialog
ref="importRoutesDialog"
v-model="showImportRoutesDialog"
:plans="plans"
:all-platforms="allPlatformsForImport"
@import="handleImportRoutes"
/>
<!-- 4T悬浮窗THREAT/TASK/TARGET/TACTIC- 仅点击4T图标时打开 -->
<four-t-panel
v-if="show4TPanel && !screenshotMode"
@ -484,10 +502,14 @@ import { getMenuConfig, saveMenuConfig } from "@/api/system/userMenuConfig";
import { listByRoomId as listRoomPlatformIcons, addRoomPlatformIcon, updateRoomPlatformIcon, delRoomPlatformIcon } from "@/api/system/roomPlatformIcon";
import { listWhiteboards, getWhiteboard, createWhiteboard, updateWhiteboard, deleteWhiteboard } from "@/api/system/whiteboard";
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue";
import ExportRoutesDialog from "@/views/dialogs/ExportRoutesDialog.vue";
import ImportRoutesDialog from "@/views/dialogs/ImportRoutesDialog.vue";
export default {
name: 'MissionPlanningView',
components: {
PlatformImportDialog,
ExportRoutesDialog,
ImportRoutesDialog,
cesiumMap,
OnlineMembersDialog,
PlatformEditDialog,
@ -545,6 +567,9 @@ export default {
platformIconSaveTimer: null,
//
showImportDialog: false,
// /线
showExportRoutesDialog: false,
showImportRoutesDialog: false,
// /
bottomPanelVisible: false,
//
@ -813,6 +838,10 @@ export default {
return Object.values(merged)
},
/** 所有平台(用于导入航线时选择默认平台) */
allPlatformsForImport() {
return [...(this.airPlatforms || []), ...(this.seaPlatforms || []), ...(this.groundPlatforms || [])];
},
/** 被其他成员编辑锁定的航线 ID 列表,供地图禁止拖拽等 */
routeLockedByOtherRouteIds() {
const myId = this.currentUserId;
@ -1477,9 +1506,8 @@ export default {
const newer = existing.filter(m => (m.timestamp || 0) > maxTs);
this.$set(this.privateChatMessages, targetUserId, [...history, ...newer]);
},
onSyncRouteVisibility: (routeId, visible, senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncRouteVisibility(routeId, visible);
onSyncRouteVisibility: () => {
// 线
},
onSyncWaypoints: (routeId, senderSessionId) => {
if (this.isMySyncSession(senderSessionId)) return;
@ -1497,8 +1525,8 @@ export default {
if (this.isMySyncSession(senderSessionId)) return;
this.applySyncPlatformStyles();
},
onRoomState: (visibleRouteIds) => {
this.applyRoomStateVisibleRoutes(visibleRouteIds);
onRoomState: () => {
// 线
},
onConnected: () => {},
onDisconnected: () => {
@ -1983,15 +2011,16 @@ export default {
routes: []
}));
}
// 线线
// 线线 planIds
const planIds = this.plans.map(p => p.id);
const routeParams = isParentRoom && planIds.length > 0
const routeParams = planIds.length > 0
? { scenarioIdsStr: planIds.join(','), pageNum: 1, pageSize: 9999 }
: { pageNum: 1, pageSize: 9999 };
const routeRes = await listRoutes(routeParams);
if (routeRes.code === 200) {
let routeRows = routeRes.rows || [];
if (isParentRoom && planIds.length > 0) {
// 线 API planIds
if (planIds.length > 0) {
routeRows = routeRows.filter(r => planIds.includes(r.scenarioId));
}
const allRoutes = routeRows.map(item => ({
@ -2535,8 +2564,90 @@ export default {
},
importRoute() {
this.$message.success('导入航线');
// 线
this.showImportRoutesDialog = true;
},
openExportRoutesDialog() {
this.showExportRoutesDialog = true;
},
/** 导出航线:获取完整数据并下载 JSON */
async handleExportRoutes(selectedIds) {
if (!selectedIds || selectedIds.length === 0) return;
try {
const routeDataList = [];
for (const id of selectedIds) {
const res = await getRoutes(id);
if (res.code === 200 && res.data) {
const d = res.data;
const waypoints = (d.waypoints || []).map(wp => {
const { id: _id, routeId: _rid, ...rest } = wp;
return rest;
});
routeDataList.push({
route: {
callSign: d.callSign,
platformId: d.platformId,
platform: d.platform,
attributes: d.attributes
},
waypoints
});
}
}
const exportData = {
version: 1,
exportTime: new Date().toISOString(),
routes: routeDataList
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `航线导出_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
this.showExportRoutesDialog = false;
this.$message.success(`已导出 ${selectedIds.length} 条航线`);
} catch (err) {
console.error('导出航线失败:', err);
this.$message.error('导出失败,请重试');
}
},
/** 导入航线:从 JSON 创建新航线 */
async handleImportRoutes({ routeItems, targetScenarioId, targetPlatformId }) {
if (!routeItems || routeItems.length === 0 || !targetScenarioId) return;
const importDialog = this.$refs.importRoutesDialog;
if (importDialog && importDialog.setImporting) importDialog.setImporting(true);
try {
let successCount = 0;
for (const item of routeItems) {
const route = item.route || item;
const waypoints = item.waypoints || route.waypoints || [];
const cleanWaypoints = waypoints.map((wp, idx) => {
const { id: _id, routeId: _rid, ...rest } = wp;
return {
...rest,
seq: idx + 1
};
});
const payload = {
callSign: route.callSign || route.name || `导入航线${successCount + 1}`,
scenarioId: targetScenarioId,
platformId: (targetPlatformId != null ? targetPlatformId : route.platformId) || 1,
attributes: route.attributes || this.getDefaultRouteAttributes(),
waypoints: cleanWaypoints
};
const res = await addRoutes(payload);
if (res.code === 200) successCount++;
}
this.showImportRoutesDialog = false;
await this.getList();
this.$message.success(`成功导入 ${successCount} 条航线`);
} catch (err) {
console.error('导入航线失败:', err);
this.$message.error('导入失败:' + (err.message || '请重试'));
} finally {
if (importDialog && importDialog.setImporting) importDialog.setImporting(false);
}
},
exportPlan() {
@ -3256,8 +3367,8 @@ export default {
},
importRouteData(path) {
console.log('导入航路:', path)
this.$message.success('航路数据导入成功');
// 线
this.showImportRoutesDialog = true;
},
importLandmark(path) {
@ -4304,7 +4415,7 @@ export default {
this.selectedRouteDetails = null;
}
}
this.wsConnection?.sendSyncRouteVisibility?.(route.id, false);
// 线
this.$message.info(`已取消航线: ${route.name}`);
return;
}
@ -4352,7 +4463,7 @@ export default {
} else {
this.$message.warning('该航线暂无坐标数据,无法在地图展示');
}
this.wsConnection?.sendSyncRouteVisibility?.(route.id, true);
// 线
}
} catch (error) {
console.error("获取航线详情失败:", error);
@ -4477,7 +4588,7 @@ export default {
if (this.$refs.cesiumMap) {
this.$refs.cesiumMap.removeRouteById(route.id);
}
if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, false);
// 线
if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) {
if (this.activeRouteIds.length > 0) {
const lastId = this.activeRouteIds[this.activeRouteIds.length - 1];
@ -4505,7 +4616,7 @@ export default {
} else {
// 线
await this.selectRoute(route);
if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, true);
// 线
}
},

237
ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue

@ -0,0 +1,237 @@
<template>
<el-dialog
title="导出航线"
:visible.sync="visible"
width="720px"
top="52vh"
append-to-body
class="export-routes-dialog"
@close="handleClose"
>
<div v-if="routes.length === 0" class="empty-tip">
<i class="el-icon-warning-outline"></i>
<p>暂无航线可导出请先创建航线</p>
</div>
<div v-else>
<div class="select-actions">
<el-button type="text" size="small" @click="selectAll">全选</el-button>
<el-button type="text" size="small" @click="selectNone">全不选</el-button>
</div>
<div class="tree-list">
<div
v-for="plan in plansWithRoutes"
:key="plan.id"
class="tree-item plan-item"
>
<div class="tree-item-header" @click="togglePlan(plan.id)">
<i :class="expandedPlans.includes(plan.id) ? 'el-icon-folder-opened' : 'el-icon-folder'" class="tree-icon"></i>
<div class="tree-item-info">
<div class="tree-item-name">{{ plan.name }}</div>
<div class="tree-item-meta">{{ planRoutes(plan.id).length }} 个航线</div>
</div>
</div>
<div v-if="expandedPlans.includes(plan.id)" class="tree-children route-children">
<div
v-for="route in planRoutes(plan.id)"
:key="route.id"
class="tree-item route-item"
:class="{ selected: selectedIds.includes(route.id) }"
@click.stop="toggleRouteSelect(route.id)"
>
<el-checkbox
:value="selectedIds.includes(route.id)"
@change="(v) => setRouteSelected(route.id, v)"
@click.native.stop
>
<span class="route-name">{{ route.name }}</span>
<span class="route-meta">{{ route.points || (route.waypoints && route.waypoints.length) || 0 }} 个航点</span>
</el-checkbox>
</div>
</div>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" :disabled="selectedIds.length === 0" @click="handleExport">
导出 {{ selectedIds.length > 0 ? `(${selectedIds.length} 条)` : '' }}
</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ExportRoutesDialog',
props: {
value: {
type: Boolean,
default: false
},
routes: {
type: Array,
default: () => []
},
plans: {
type: Array,
default: () => []
}
},
data() {
return {
selectedIds: [],
expandedPlans: []
};
},
computed: {
visible: {
get() {
return this.value;
},
set(v) {
this.$emit('input', v);
}
},
/** 有航线的方案列表(用于展示) */
plansWithRoutes() {
return this.plans.filter(p => this.planRoutes(p.id).length > 0);
}
},
watch: {
value(v) {
if (v) {
this.selectedIds = this.routes.map(r => r.id);
this.expandedPlans = this.plansWithRoutes.map(p => p.id);
}
}
},
methods: {
planRoutes(planId) {
return this.routes.filter(r => r.scenarioId === planId);
},
togglePlan(planId) {
const idx = this.expandedPlans.indexOf(planId);
if (idx >= 0) {
this.expandedPlans.splice(idx, 1);
} else {
this.expandedPlans.push(planId);
}
},
toggleRouteSelect(routeId) {
const idx = this.selectedIds.indexOf(routeId);
if (idx >= 0) {
this.selectedIds.splice(idx, 1);
} else {
this.selectedIds.push(routeId);
}
},
setRouteSelected(routeId, selected) {
if (selected) {
if (!this.selectedIds.includes(routeId)) this.selectedIds.push(routeId);
} else {
this.selectedIds = this.selectedIds.filter(id => id !== routeId);
}
},
selectAll() {
this.selectedIds = this.routes.map(r => r.id);
},
selectNone() {
this.selectedIds = [];
},
handleExport() {
this.$emit('export', this.selectedIds);
},
handleClose() {
this.selectedIds = [];
this.expandedPlans = [];
}
}
};
</script>
<style scoped>
.export-routes-dialog .empty-tip {
text-align: center;
padding: 32px 0;
color: #909399;
}
.export-routes-dialog .empty-tip i {
font-size: 48px;
margin-bottom: 12px;
display: block;
}
.export-routes-dialog .select-actions {
margin-bottom: 12px;
}
.export-routes-dialog .tree-list {
max-height: 360px;
overflow-y: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px;
}
.export-routes-dialog .tree-item {
user-select: none;
}
.export-routes-dialog .tree-item-header {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
}
.export-routes-dialog .tree-item-header:hover {
background: #f5f7fa;
}
.export-routes-dialog .plan-item .tree-item-header {
font-weight: 500;
}
.export-routes-dialog .tree-icon {
margin-right: 8px;
color: #909399;
font-size: 16px;
}
.export-routes-dialog .tree-item-info {
flex: 1;
min-width: 0;
}
.export-routes-dialog .tree-item-name {
font-size: 14px;
color: #303133;
}
.export-routes-dialog .tree-item-meta {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.export-routes-dialog .tree-children {
padding-left: 24px;
}
.export-routes-dialog .route-children {
margin-bottom: 4px;
}
.export-routes-dialog .route-item {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.export-routes-dialog .route-item:hover {
background: #f5f7fa;
}
.export-routes-dialog .route-item .route-name {
font-weight: 500;
}
.export-routes-dialog .route-item .route-meta {
margin-left: 8px;
font-size: 12px;
color: #909399;
}
.export-routes-dialog >>> .route-item .el-checkbox {
display: flex;
align-items: center;
width: 100%;
}
.export-routes-dialog >>> .route-item .el-checkbox__label {
flex: 1;
}
</style>

8
ruoyi-ui/src/views/dialogs/ExternalParamsDialog.vue

@ -174,12 +174,8 @@ export default {
this.$emit('import-airport', this.formData.airportPath);
},
importRoute() {
if (!this.formData.routePath) {
this.$message.warning('请先选择航路数据文件');
return;
}
this.$message.success('航路数据导入成功');
this.$emit('import-route-data', this.formData.routePath);
// 线 JSON
this.$emit('import-route-data', this.formData.routePath || '');
},
importLandmark() {
if (!this.formData.landmarkPath) {

240
ruoyi-ui/src/views/dialogs/ImportRoutesDialog.vue

@ -0,0 +1,240 @@
<template>
<el-dialog
title="导入航线"
:visible.sync="visible"
width="520px"
append-to-body
class="import-routes-dialog"
:modal-append-to-body="true"
@close="handleClose"
>
<div v-if="!parsedData" class="empty-tip">
<i class="el-icon-upload"></i>
<p>请选择要导入的航线 JSON 文件</p>
<el-button type="primary" size="small" @click="triggerFileInput">选择文件</el-button>
<input
ref="fileInput"
type="file"
accept=".json"
style="display:none"
@change="onFileChange"
/>
</div>
<div v-else>
<div class="import-preview">
<div class="preview-header">
<i class="el-icon-document"></i>
<span> {{ routeItems.length }} 条航线待导入</span>
</div>
<div class="route-preview-list">
<div v-for="(item, idx) in routeItems" :key="idx" class="route-preview-item">
<span class="route-name">{{ item.callSign || item.name || `航线${idx + 1}` }}</span>
<span class="route-meta">{{ (item.waypoints || []).length }} 个航点</span>
</div>
</div>
</div>
<el-form label-width="100px" size="small" class="import-form">
<el-form-item label="目标方案" required>
<el-select v-model="targetScenarioId" placeholder="请选择方案" style="width:100%" clearable>
<el-option v-for="p in plans" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
<div v-if="plans.length === 0" class="el-form-item__error" style="margin-top:4px;">暂无方案请先创建方案后再导入</div>
</el-form-item>
<el-form-item label="默认平台">
<el-select v-model="targetPlatformId" placeholder="导入时使用的平台(可后续在编辑中修改)" style="width:100%" clearable>
<el-option
v-for="p in allPlatforms"
:key="p.id"
:label="p.name"
:value="p.id"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button v-if="parsedData" @click="resetFile">重新选择</el-button>
<el-button @click="visible = false"> </el-button>
<el-button
type="primary"
:disabled="!canImport"
:loading="importing"
@click="handleImport"
>
导入
</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ImportRoutesDialog',
props: {
value: {
type: Boolean,
default: false
},
plans: {
type: Array,
default: () => []
},
allPlatforms: {
type: Array,
default: () => []
}
},
data() {
return {
parsedData: null,
targetScenarioId: null,
targetPlatformId: null,
importing: false
};
},
computed: {
visible: {
get() {
return this.value;
},
set(v) {
this.$emit('input', v);
}
},
routeItems() {
if (!this.parsedData) return [];
const d = this.parsedData;
if (Array.isArray(d.routes)) return d.routes;
if (d.route && d.waypoints) return [{ ...d.route, waypoints: d.waypoints }];
return [];
},
canImport() {
return this.parsedData && this.targetScenarioId && this.routeItems.length > 0;
}
},
watch: {
value(v) {
if (v && !this.parsedData) {
this.targetScenarioId = this.plans[0] && this.plans[0].id;
this.targetPlatformId = this.allPlatforms[0] && this.allPlatforms[0].id;
}
},
plans: {
immediate: true,
handler(plans) {
if (plans.length > 0 && !this.targetScenarioId) {
this.targetScenarioId = plans[0].id;
}
}
},
allPlatforms: {
immediate: true,
handler(platforms) {
if (platforms.length > 0 && !this.targetPlatformId) {
this.targetPlatformId = platforms[0].id;
}
}
}
},
methods: {
triggerFileInput() {
this.$refs.fileInput && this.$refs.fileInput.click();
},
onFileChange(e) {
const file = e.target.files && e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const text = ev.target.result;
const data = JSON.parse(text);
if (!data.routes && !(data.route && data.waypoints)) {
this.$message.error('文件格式不正确,缺少 routes 或 route/waypoints 数据');
return;
}
this.parsedData = data;
if (this.plans.length > 0 && !this.targetScenarioId) {
this.targetScenarioId = this.plans[0].id;
}
if (this.allPlatforms.length > 0 && !this.targetPlatformId) {
this.targetPlatformId = this.allPlatforms[0].id;
}
} catch (err) {
this.$message.error('JSON 解析失败:' + (err.message || '格式错误'));
}
e.target.value = '';
};
reader.readAsText(file, 'UTF-8');
},
resetFile() {
this.parsedData = null;
this.triggerFileInput();
},
handleImport() {
if (!this.canImport) return;
this.$emit('import', {
routeItems: this.routeItems,
targetScenarioId: this.targetScenarioId,
targetPlatformId: this.targetPlatformId
});
},
handleClose() {
this.parsedData = null;
this.targetScenarioId = null;
this.targetPlatformId = null;
},
setImporting(v) {
this.importing = v;
}
}
};
</script>
<style scoped>
.import-routes-dialog >>> .el-dialog__body {
min-height: 180px;
}
.import-routes-dialog .empty-tip {
text-align: center;
padding: 32px 0;
color: #606266;
min-height: 120px;
}
.import-routes-dialog .empty-tip i {
font-size: 48px;
margin-bottom: 12px;
display: block;
}
.import-routes-dialog .empty-tip p {
margin-bottom: 16px;
}
.import-routes-dialog .import-preview {
margin-bottom: 16px;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
}
.import-routes-dialog .preview-header {
margin-bottom: 8px;
font-weight: 500;
}
.import-routes-dialog .preview-header i {
margin-right: 8px;
}
.import-routes-dialog .route-preview-list {
max-height: 160px;
overflow-y: auto;
}
.import-routes-dialog .route-preview-item {
padding: 6px 0;
display: flex;
justify-content: space-between;
}
.import-routes-dialog .route-preview-item .route-name {
font-weight: 500;
}
.import-routes-dialog .route-preview-item .route-meta {
font-size: 12px;
color: #909399;
}
</style>
Loading…
Cancel
Save