diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java index f69aa6d..5354071 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java @@ -1,17 +1,11 @@ package com.ruoyi.web.controller; +import java.io.IOException; import java.util.List; import javax.servlet.http.HttpServletResponse; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -20,6 +14,9 @@ import com.ruoyi.system.domain.PlatformLib; import com.ruoyi.system.service.IPlatformLibService; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.config.RuoYiConfig; +import org.springframework.web.multipart.MultipartFile; /** * 平台模版库Controller @@ -74,9 +71,18 @@ public class PlatformLibController extends BaseController */ @PreAuthorize("@ss.hasPermi('system:lib:add')") @Log(title = "平台模版库", businessType = BusinessType.INSERT) - @PostMapping - public AjaxResult add(@RequestBody PlatformLib platformLib) + @PostMapping("/add") + public AjaxResult add(PlatformLib platformLib, @RequestParam("file") MultipartFile file) throws IOException { + // 判断前端是否有文件传过来 + if (file != null && !file.isEmpty()) + { + String fileName = FileUploadUtils.upload(RuoYiConfig.getProfile(), file); + // 把这个路径存入实体类的 iconUrl 属性,对应数据库 icon_url 字段 + platformLib.setIconUrl(fileName); + } + + // 执行原有的插入逻辑 return toAjax(platformLibService.insertPlatformLib(platformLib)); } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java index cc1b6f4..88d3615 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java @@ -172,7 +172,9 @@ public class FileUploadUtils { int dirLastIndex = RuoYiConfig.getProfile().length() + 1; String currentDir = StringUtils.substring(uploadDir, dirLastIndex); - return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + String path = Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; + + return path.replaceAll("/+", "/"); } /** diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 511842b..d5f704e 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -23,19 +23,18 @@ import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; /** * spring security配置 - * + * * @author ruoyi */ @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) @Configuration -public class SecurityConfig -{ +public class SecurityConfig { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; - + /** * 认证失败处理类 */ @@ -53,7 +52,7 @@ public class SecurityConfig */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; - + /** * 跨域过滤器 */ @@ -70,8 +69,7 @@ public class SecurityConfig * 身份验证实现 */ @Bean - public AuthenticationManager authenticationManager() - { + public AuthenticationManager authenticationManager() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder()); @@ -94,46 +92,45 @@ public class SecurityConfig * authenticated | 用户登录后可访问 */ @Bean - protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception - { + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity - // CSRF禁用,因为不使用session - .csrf(csrf -> csrf.disable()) - // 禁用HTTP响应标头 - .headers((headersCustomizer) -> { - headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); - }) - // 认证失败处理类 - .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) - // 基于token,所以不需要session - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // 注解标记允许匿名访问的url - .authorizeHttpRequests((requests) -> { - permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); - // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - requests.antMatchers("/login", "/register", "/captchaImage").permitAll() - // 静态资源,可匿名访问 - .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() - .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() - // 除上面外的所有请求全部需要鉴权认证 - .anyRequest().authenticated(); - }) - // 添加Logout filter - .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) - // 添加JWT filter - .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) - // 添加CORS filter - .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) - .addFilterBefore(corsFilter, LogoutFilter.class) - .build(); + // CSRF禁用,因为不使用session + .csrf(csrf -> csrf.disable()) + // 禁用HTTP响应标头 + .headers((headersCustomizer) -> { + headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin()); + }) + // 认证失败处理类 + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + // 基于token,所以不需要session + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 注解标记允许匿名访问的url + .authorizeHttpRequests((requests) -> { + permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); + // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + requests.antMatchers("/login", "/register", "/captchaImage").permitAll() + // 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() + .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() + .antMatchers("/profile/**").permitAll() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated(); + }) + // 添加Logout filter + .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)) + // 添加JWT filter + .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) + // 添加CORS filter + .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) + .addFilterBefore(corsFilter, LogoutFilter.class) + .build(); } /** * 强散列哈希加密实现 */ @Bean - public BCryptPasswordEncoder bCryptPasswordEncoder() - { + public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/PlatformLib.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/PlatformLib.java index 902ebd5..b0d1087 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/PlatformLib.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/PlatformLib.java @@ -7,76 +7,88 @@ import com.ruoyi.common.core.domain.BaseEntity; /** * 平台模版库对象 platform_lib - * + * * @author ruoyi * @date 2026-01-14 */ -public class PlatformLib extends BaseEntity -{ +public class PlatformLib extends BaseEntity { private static final long serialVersionUID = 1L; - /** 平台库ID */ + /** + * 平台库ID + */ private Long id; - /** 平台名称 (如: F-22 猛禽) */ + /** + * 平台名称 (如: F-22 猛禽) + */ @Excel(name = "平台名称 (如: F-22 猛禽)") private String name; - /** 类型: Aircraft, Vehicle, Radar, Ship */ - @Excel(name = "类型: Aircraft, Vehicle, Radar, Ship") + /** + * 类型: Aircraft, Vehicle, Radar, Ship + */ + @Excel(name = "类型: Air, Sea, Ground") private String type; - /** 核心参数JSON: {icon_url, max_speed, max_fuel, radar_range...} */ + /** + * 核心参数JSON: {icon_url, max_speed, max_fuel, radar_range...} + */ @Excel(name = "核心参数JSON: {icon_url, max_speed, max_fuel, radar_range...}") private String specsJson; - public void setId(Long id) - { + @Excel(name = "平台图标路径") + private String iconUrl; + + public void setId(Long id) { this.id = id; } - public Long getId() - { + public Long getId() { return id; } - public void setName(String name) - { + public void setName(String name) { this.name = name; } - public String getName() - { + public String getName() { return name; } - public void setType(String type) - { + public void setType(String type) { this.type = type; } - public String getType() - { + public String getType() { return type; } - public void setSpecsJson(String specsJson) - { + public void setSpecsJson(String specsJson) { this.specsJson = specsJson; } - public String getSpecsJson() - { + public String getSpecsJson() { return specsJson; } + public void setIconUrl(String iconUrl) { + this.iconUrl = iconUrl; + } + + public String getIconUrl() { + return iconUrl; + } + + @Override public String toString() { - return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) - .append("id", getId()) - .append("name", getName()) - .append("type", getType()) - .append("specsJson", getSpecsJson()) - .toString(); + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("name", getName()) + .append("type", getType()) + .append("specsJson", getSpecsJson()) + .append("iconUrl", getIconUrl()) + .toString(); } } diff --git a/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml b/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml index b72202a..7161fdc 100644 --- a/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml @@ -9,10 +9,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + - select id, name, type, specs_json from platform_lib + select id, name, type, specs_json,icon_url from platform_lib @@ -35,11 +37,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" name, type, specs_json, + icon_url, #{name}, #{type}, #{specsJson}, + #{iconUrl}, @@ -49,6 +53,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" name = #{name}, type = #{type}, specs_json = #{specsJson}, + icon_url = #{iconUrl}, where id = #{id} diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development index 09f4bb3..4d098ba 100644 --- a/ruoyi-ui/.env.development +++ b/ruoyi-ui/.env.development @@ -7,5 +7,8 @@ ENV = 'development' # 若依管理系统/开发环境 VUE_APP_BASE_API = '/dev-api' +# 访问地址(绕过 /dev-api 代理,用于解决静态资源/图片访问 401 认证问题) +VUE_APP_BACKEND_URL = 'http://localhost:8080' + # 路由懒加载 VUE_CLI_BABEL_TRANSPILE_MODULES = true diff --git a/ruoyi-ui/src/api/system/lib.js b/ruoyi-ui/src/api/system/lib.js index 89f6aa5..59420c5 100644 --- a/ruoyi-ui/src/api/system/lib.js +++ b/ruoyi-ui/src/api/system/lib.js @@ -20,9 +20,12 @@ export function getLib(id) { // 新增平台模版库 export function addLib(data) { return request({ - url: '/system/lib', + url: '/system/lib/add', method: 'post', - data: data + data: data, + headers: { + 'Content-Type': 'multipart/form-data' + } }) } diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 0e746b8..e003d64 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -181,6 +181,7 @@ export default { this.drawingPoints = []; let activeCursorPosition = null; this.isDrawing = true; + this.viewer.canvas.style.cursor = 'crosshair'; this.drawingHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); window.addEventListener('contextmenu', this.preventContextMenu, true); // 鼠标移动预览逻辑 @@ -256,8 +257,8 @@ export default { name: `WP${index + 1}`, lat: coords.lat, lng: coords.lng, - alt: 500, // 默认业务属性 - speed: 600 // 默认业务属性 + alt: 5000, + speed: 800 }; }); this.$emit('draw-complete', latLngPoints); @@ -271,15 +272,37 @@ export default { }, 200); }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); }, + //正式航线渲染函数 renderRouteWaypoints(waypoints, routeId = 'default') { if (!waypoints || waypoints.length < 1) return; - const positions = []; - // 1. 遍历并绘制航点标记 + // 清理旧线 + const lineId = `route-line-${routeId}`; + const existingLine = this.viewer.entities.getById(lineId); + if (existingLine) { + this.viewer.entities.remove(existingLine); + } + // 清理所有属于该航线的旧点 + waypoints.forEach((wp,index) => { + const waypointEntityId = `wp_${routeId}_${wp.id}`; + const existingWaypoint = this.viewer.entities.getById(waypointEntityId); + if (existingWaypoint) { + this.viewer.entities.remove(existingWaypoint); + } + const arcId = `arc-line-${routeId}-${index}`; + const existingArc = this.viewer.entities.getById(arcId); + if (existingArc) { + this.viewer.entities.remove(existingArc); + } + }); + // 遍历并绘制航点标记 + const originalPositions = []; waypoints.forEach((wp, index) => { const lon = parseFloat(wp.lng); const lat = parseFloat(wp.lat); - const pos = Cesium.Cartesian3.fromDegrees(lon, lat, parseFloat(wp.altitude || wp.alt || 500)); - positions.push(pos); + // 使用 Number 转换 Double 类型 + const altValue = Number(wp.alt || 5000); + const pos = Cesium.Cartesian3.fromDegrees(lon, lat, altValue); + originalPositions.push(pos); this.viewer.entities.add({ id: `wp_${routeId}_${wp.id}`, name: wp.name || `WP${index + 1}`, @@ -307,25 +330,99 @@ export default { } }); }); - // 2. 绘制连线(仅当点数 > 1 时) - if (positions.length > 1) { + // 绘制连线 + if (waypoints.length > 1) { + let finalPathPositions = []; + for (let i = 0; i < waypoints.length; i++) { + const currPos = originalPositions[i]; + const radius = this.getWaypointRadius(waypoints[i]); + // 如果不是首尾点且有半径,我们就画红色的弧线 + if (i > 0 && i < waypoints.length - 1 && radius > 0) { + const prevPos = originalPositions[i - 1]; + const nextPos = originalPositions[i + 1]; + const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius); + // 绘制红色实线弧 + this.viewer.entities.add({ + id: `arc-line-${routeId}-${i}`, + polyline: { + positions: arcPoints, + width: 8, + material: Cesium.Color.RED, + clampToGround: true, + zIndex: 20 + } + }); + console.log(`>>> 航点 ${waypoints[i].name} 已渲染红色转弯弧,半径: ${radius}`); + } + if (i === 0 || i === waypoints.length - 1 || radius <= 0) { + finalPathPositions.push(currPos); + } else { + const prevPos = originalPositions[i - 1]; + const nextPos = originalPositions[i + 1]; + // 计算弧线点集 + const arcPoints = this.computeArcPositions(prevPos, currPos, nextPos, radius); + finalPathPositions.push(...arcPoints); + } + } + // 绘制包含弧线的 Polyline const routeEntity = this.viewer.entities.add({ - id: `route-line-${routeId}`, + id: lineId, polyline: { - positions: positions, + positions: finalPathPositions, width: 4, material: new Cesium.PolylineDashMaterialProperty({ color: Cesium.Color.WHITE, gapColor: Cesium.Color.BLACK, dashLength: 20.0 }), - clampToGround: true + clampToGround: true, + zIndex: 1 }, properties: {isMissionRouteLine: true, routeId: routeId} }); - this.allEntities.push({id: `route-line-${routeId}`, entity: routeEntity, type: 'line'}); + if (this.allEntities) { + this.allEntities.push({id: lineId, entity: routeEntity, type: 'line'}); + } } }, + // 计算单个点的转弯半径 + getWaypointRadius(wp) { + const speed = wp.speed || 800; + const bankAngle = wp.turnAngle || 0; + if (bankAngle <= 0) return 0; + const v_mps = speed / 3.6; + const radians = bankAngle * (Math.PI / 180); + const g = 9.8; + return (v_mps * v_mps) / (g * Math.tan(radians)); + }, + + // 计算转弯处的圆弧点集 + computeArcPositions(p1, p2, p3, radius) { + const v1 = Cesium.Cartesian3.subtract(p1, p2, new Cesium.Cartesian3()); + const v2 = Cesium.Cartesian3.subtract(p3, p2, new Cesium.Cartesian3()); + const dir1 = Cesium.Cartesian3.normalize(v1, new Cesium.Cartesian3()); + const dir2 = Cesium.Cartesian3.normalize(v2, new Cesium.Cartesian3()); + + const angle = Cesium.Cartesian3.angleBetween(dir1, dir2); + const dist = radius / Math.tan(angle / 2); + + const t1 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir1, dist, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + const t2 = Cesium.Cartesian3.add(p2, Cesium.Cartesian3.multiplyByScalar(dir2, dist, new Cesium.Cartesian3()), new Cesium.Cartesian3()); + + let arc = []; + // 采样15个点让弧线丝滑 + for (let t = 0; t <= 1; t += 0.07) { + const c1 = Math.pow(1 - t, 2); + const c2 = 2 * (1 - t) * t; + const c3 = Math.pow(t, 2); + arc.push(new Cesium.Cartesian3( + c1 * t1.x + c2 * p2.x + c3 * t2.x, + c1 * t1.y + c2 * p2.y + c3 * t2.y, + c1 * t1.z + c2 * p2.z + c3 * t2.z + )); + } + return arc; + }, removeRouteById(routeId) { // 从地图上移除所有属于该 routeId 的实体 const entityList = this.viewer.entities.values; @@ -659,37 +756,52 @@ export default { console.log(`开始绘制 ${this.getTypeName(mode)}`) }, stopDrawing() { + // 销毁处理器 if (this.drawingHandler) { this.drawingHandler.destroy(); this.drawingHandler = null; } + //还原鼠标样式 + this.viewer.scene.canvas.style.cursor = 'default'; + // 彻底清理临时点 + const allEntities = this.viewer.entities.values; + for (let i = allEntities.length - 1; i >= 0; i--) { + const entity = allEntities[i]; + if (entity.id && ( + entity.id.toString().startsWith('temp_wp_') || + entity.id.toString().includes('temp-preview') + )) { + this.viewer.entities.remove(entity); + } + } + //清理已知的预览实体 if (this.tempEntity) { this.viewer.entities.remove(this.tempEntity); this.tempEntity = null; } - // 确保也清理预览实体 if (this.tempPreviewEntity) { this.viewer.entities.remove(this.tempPreviewEntity); this.tempPreviewEntity = null; } - // 清理点实体 - if (this.drawingPointEntities) { + // 清理点实体数组 + if (this.drawingPointEntities && this.drawingPointEntities.length > 0) { this.drawingPointEntities.forEach(pointEntity => { this.viewer.entities.remove(pointEntity); }); this.drawingPointEntities = []; } + // 重置状态 this.drawingPoints = []; this.drawingStartPoint = null; this.isDrawing = false; this.activeCursorPosition = null; - // 重新初始化右键点击删除处理器 + // 重新初始化右键处理器 if (!this.rightClickHandler) { this.initRightClickHandler(); } - // 隐藏测量结果 + // 隐藏测量结果 this.measurementResult = null; - this.viewer.scene.canvas.style.cursor = 'default'; + console.log(">>> 绘制状态已彻底重置,临时实体已清理"); }, cancelDrawing() { // 取消绘制,清理临时实体和状态 diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index d7be613..1e532bf 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -77,7 +77,7 @@
@@ -85,10 +85,10 @@
{{ point.name }}
-
高度: {{ point.altitude }}m | 速度: {{ point.speed }}
+
高度: {{ point.alt }}m | 速度: {{ point.speed }}
- +
@@ -144,6 +144,19 @@
+
+
平台列表
+ + 导入平台 + +
@@ -155,7 +168,11 @@ @click="handleOpenPlatformDialog(platform)" >
- + +
{{ platform.name }}
@@ -176,7 +193,11 @@ @click="handleOpenPlatformDialog(platform)" >
- + +
{{ platform.name }}
@@ -197,7 +218,11 @@ @click="handleOpenPlatformDialog(platform)" >
- + +
{{ platform.name }}
@@ -275,7 +300,7 @@ export default { groundPlatforms: { type: Array, default: () => [] - } + }, }, data() { return { @@ -372,6 +397,26 @@ export default { } }, + // 判断是否是图片路径 + isImg(path) { + if (!path) return false; + return path.includes('/') || path.includes('data:image'); + }, + + // 格式化图片地址 + formatImg(url) { + if (!url) return ''; + // 清理路径中的双斜杠 + const cleanPath = url.replace(/\/+/g, '/'); + const backendUrl = process.env.VUE_APP_BACKEND_URL; + const finalUrl = backendUrl + cleanPath; + + // 👈 核心排查日志:按 F12 在控制台看这个输出! + console.log('>>> [图片渲染调试] 最终拼接地址:', finalUrl); + + return finalUrl; + }, + handleHide() { this.$emit('hide') }, @@ -404,6 +449,10 @@ export default { this.$emit('open-plan-dialog', plan) }, + handleImportPlatform() { + this.$emit('open-import-dialog'); + }, + handleOpenRouteDialog(route) { this.$emit('open-route-dialog', route) }, @@ -422,8 +471,12 @@ export default { active: this.activeRouteIds.includes(routeId) } }, - handleOpenWaypointDialog(point) { - this.$emit('open-waypoint-dialog', point) + handleOpenWaypointDialog(point,index,total) { + this.$emit('open-waypoint-dialog', { + ...point, + currentIndex: index, + totalPoints: total + }); }, handleViewConflict(conflict) { @@ -883,4 +936,20 @@ export default { .blue-warning { color: #e6a23c; } + +.platform-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; /* 裁剪超出部分 */ +} + +.platform-img { + width: 100%; + height: 100%; + object-fit: cover; /* 关键:图片自动填充不拉伸 */ + border-radius: 4px; +} diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index c7f6792..bc253d1 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -135,6 +135,7 @@ @resolve-conflict="resolveConflict" @run-conflict-check="runConflictCheck" @open-platform-dialog="openPlatformDialog" + @open-import-dialog="showImportDialog = true" /> @@ -249,6 +250,12 @@ @save="savePageLayout" /> + + + w.id)); } + // 获取当前最新的航点列表 + const waypointsList = this.selectedRouteDetails.waypoints; + // 找到点在数组里的索引 + const index = waypointsList.findIndex(item => item.id == wpId); + const total = waypointsList.length; + if (index !== -1) { + const wpData = waypointsList[index]; + this.selectedWaypoint = { + ...JSON.parse(JSON.stringify(wpData)), + currentIndex: index, + totalPoints: total + }; + console.log(`>>> [地图触发锁定] 序号: ${index}, 总数: ${total}, 数据已装载`); + this.showWaypointDialog = true; + } else { + this.$message.info('未找到该航点的业务数据'); + console.error("查找失败!账本内IDs:", waypointsList.map(w => w.id)); + } }, // 显示在线成员弹窗 showOnlineMembersDialog() { @@ -533,6 +551,61 @@ export default { this.selectedPlatform = platform; this.showPlatformDialog = true; }, + /** 从数据库查询并分拣平台库数据 */ + getPlatformList() { + listLib().then(res => { + const allData = res.rows || []; + this.airPlatforms = []; + this.seaPlatforms = []; + this.groundPlatforms = []; + allData.forEach(item => { + const platform = { + id: item.id, + name: item.name, + type: item.type, + imageUrl: item.iconUrl || '', + icon: item.iconUrl ? '' : 'el-icon-picture-outline', + status: 'ready' + }; + if (item.type === 'Air') { + this.airPlatforms.push(platform); + } else if (item.type === 'Sea') { + this.seaPlatforms.push(platform); + } else if (item.type === 'Ground') { + this.groundPlatforms.push(platform); + } + }); + + }); + }, + /** 导入确认:将弹窗填写的模版数据存入数据库 */ + handleImportConfirm(formData) { + if (!formData.name || !formData.type) { + this.$modal.msgError("请填写完整的平台信息"); + return; + } + // 创建 FormData 对象,这是传输二进制文件的标准格式 + let data = new FormData(); + // 把文件塞进去 + if (formData.icon_file) { + data.append("file", formData.icon_file); + } + // 把其他文本字段也塞进去 + data.append("name", formData.name); + data.append("type", formData.type); + data.append("specsJson", JSON.stringify({ + scenarioId: this.selectedPlanId, + createTime: new Date().getTime() + })); + addLib(data).then(response => { + this.$modal.msgSuccess("导入成功"); + this.showImportDialog = false; + this.getPlatformList(); + }).catch(err => { + // 增加错误提示,防止请求失败时没有任何反应 + console.error("上传出错:", err); + }); + }, updatePlatform(updatedPlatform) { // 这里可以添加实际的更新逻辑 this.$message.success('平台更新成功'); @@ -542,7 +615,7 @@ export default { this.selectedRoute = route; this.showRouteDialog = true; }, - // 更新航线数据(修改后) + // 更新航线数据 updateRoute(updatedRoute) { const index = this.routes.findIndex(r => r.id === updatedRoute.id); if (index !== -1) { @@ -561,13 +634,11 @@ export default { // 新建航线 createRoute(plan) { if (!plan || !plan.id) return; - console.log(`>>> [新建航线触发] 目标方案: ${plan.name}`); // 只有当目标方案和当前方案不同时,才执行“大扫除” if (this.selectedPlanId !== plan.id) { - console.log(">>> [执行跨方案切换] 正在清理旧方案数据..."); this.selectPlan(plan); } else { - console.log(">>> [同方案内操作] 保持当前航线显示,直接进入规划模式"); + console.log(">>> 保持当前航线显示,直接进入规划模式"); } // 进入 Cesium 绘图模式 if (this.$refs.cesiumMap) { @@ -753,34 +824,47 @@ export default { } }, // 航点编辑弹窗相关方法 - openWaypointDialog(waypoint) { - this.selectedWaypoint = waypoint; + openWaypointDialog(data) { + console.log(">>> [父组件接收] 编辑航点详情:", data); + // 将整个对象赋值给弹窗绑定的变量 + this.selectedWaypoint = data; this.showWaypointDialog = true; }, /** 航点编辑保存:更新数据库并同步地图显示 */ async updateWaypoint(updatedWaypoint) { if (!this.selectedRouteDetails || !this.selectedRouteDetails.waypoints) return; try { - const response = await updateWaypoints(updatedWaypoint); // 更新数据库 + if (this.$refs.cesiumMap && updatedWaypoint.turnAngle > 0) { + updatedWaypoint.turnRadius = this.$refs.cesiumMap.getWaypointRadius(updatedWaypoint); + } else { + updatedWaypoint.turnRadius = 0; + } + const response = await updateWaypoints(updatedWaypoint); if (response.code === 200) { const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id); if (index !== -1) { - // 1. 更新本地 Vue 响应式数据,同步右侧面板 UI - this.selectedRouteDetails.waypoints.splice(index, 1, {...updatedWaypoint}); - - // 2. 【核心修改】通过 ID 通知地图组件更新图形 + // 更新本地数据 + this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint }); + // 通知地图组件同步更新 if (this.$refs.cesiumMap) { - // 直接传 ID 和新的名字 + // 更新航点图标 this.$refs.cesiumMap.updateWaypointGraphicById(updatedWaypoint.id, updatedWaypoint.name); + // 重新触发航线渲染 + this.$refs.cesiumMap.renderRouteWaypoints( + this.selectedRouteDetails.waypoints, + this.selectedRouteDetails.id + ); } - this.showWaypointDialog = false; this.$message.success('航点信息已持久化至数据库'); } + } else { + // 如果 code 不是 200,手动抛出错误进入catch + throw new Error(response.msg || '后端业务逻辑校验失败'); } } catch (error) { console.error("更新航点失败:", error); - this.$message.error('数据库更新失败,请重试'); + this.$message.error(error.message || '数据库更新失败,请重试'); } }, updateTime() { @@ -1442,7 +1526,7 @@ export default { const count = this.selectedRouteDetails.waypoints.length + 1; this.selectedRouteDetails.waypoints.push({ name: `WP${count}`, - altitude: 5000, + alt: 5000, speed: '800km/h', eta: `K+01:${(count * 15).toString().padStart(2, '0')}:00` }); diff --git a/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue b/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue new file mode 100644 index 0000000..0efc954 --- /dev/null +++ b/ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue index 57e05d4..6bb0006 100644 --- a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue @@ -14,9 +14,9 @@ - + - + +
+ ⚠️ 首尾航点坡度已锁定为 0,不可编辑 +
@@ -78,7 +82,6 @@ export default { } }, data() { - // 定义一个通用的数字校验函数,允许 0 const validateNumber = (rule, value, callback) => { // 检查 value 是否为 undefined, null 或 空字符串 // 注意:这里不能简单用 !value,因为 !0 是 true @@ -92,16 +95,19 @@ export default { return { formData: { name: '', - altitude: 0, - speed: 0, - turnBank: 0, - startTime: '' + alt: 5000, + speed: 800, + turnAngle: 0, + startTime: '', + currentIndex: -1, + totalPoints: 0, + isBankDisabled: false }, rules: { name: [ { required: true, message: '请输入航点名称', trigger: 'blur' } ], - altitude: [ + alt: [ // 使用自定义 validator 替代 type: 'number' 组合,确保 0 被接受 { required: true, validator: validateNumber, message: '请输入有效高度', trigger: ['blur', 'change'] } ], @@ -109,7 +115,7 @@ export default { // 重点修改:使用自定义 validator,允许速度为 0 { required: true, validator: validateNumber, message: '请输入有效速度', trigger: ['blur', 'change'] } ], - turnBank: [ + turnAngle: [ // 同样应用到转弯坡度 { required: true, validator: validateNumber, message: '请输入有效转弯坡度', trigger: ['blur', 'change'] } ], @@ -133,17 +139,21 @@ export default { }, methods: { initFormData() { - // 保持数据类型正确,确保是 Number 类型 + const index = this.waypoint.currentIndex !== undefined ? this.waypoint.currentIndex : -1; + const total = this.waypoint.totalPoints || 0; + const locked = (index === 0) || (total > 0 && index === total - 1); + this.formData = { name: this.waypoint.name || '', - // 确保初始化时也是数字类型 - altitude: this.waypoint.altitude !== undefined && this.waypoint.altitude !== null ? Number(this.waypoint.altitude) : 0, + alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0, speed: this.waypoint.speed !== undefined && this.waypoint.speed !== null ? Number(this.waypoint.speed) : 0, - turnBank: this.waypoint.turnBank !== undefined && this.waypoint.turnBank !== null ? Number(this.waypoint.turnBank) : 0, - startTime: this.waypoint.startTime || '' + startTime: this.waypoint.startTime || '', + currentIndex: index, + totalPoints: total, + isBankDisabled: locked, + turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0) }; - // 弹窗打开时,尝试清除之前的校验痕迹 this.$nextTick(() => { if (this.$refs.formRef) { this.$refs.formRef.clearValidate();