Browse Source

Merge branch 'mh' of http://124.70.32.114:3100/woka/cesium-map-object into ctw

# Conflicts:
#	ruoyi-ui/src/views/cesiumMap/index.vue
wxp
ctw 2 months ago
parent
commit
606c31a6e1
  1. 26
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java
  2. 4
      ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
  3. 77
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  4. 72
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PlatformLib.java
  5. 7
      ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml
  6. 3
      ruoyi-ui/.env.development
  7. 7
      ruoyi-ui/src/api/system/lib.js
  8. 195
      ruoyi-ui/src/views/cesiumMap/index.vue
  9. 87
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  10. 146
      ruoyi-ui/src/views/childRoom/index.vue
  11. 128
      ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue
  12. 44
      ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue

26
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));
}

4
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("/+", "/");
}
/**

77
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();
}
}

72
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();
}
}

7
ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml

@ -9,10 +9,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="name" column="name" />
<result property="type" column="type" />
<result property="specsJson" column="specs_json" />
<result property="iconUrl" column="icon_url" />
</resultMap>
<sql id="selectPlatformLibVo">
select id, name, type, specs_json from platform_lib
select id, name, type, specs_json,icon_url from platform_lib
</sql>
<select id="selectPlatformLibList" parameterType="PlatformLib" resultMap="PlatformLibResult">
@ -21,6 +22,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
<if test="type != null and type != ''"> and type = #{type}</if>
<if test="specsJson != null and specsJson != ''"> and specs_json = #{specsJson}</if>
<if test="iconUrl != null and iconUrl != ''"> and icon_url = #{iconUrl}</if>
</where>
</select>
@ -35,11 +37,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="name != null and name != ''">name,</if>
<if test="type != null and type != ''">type,</if>
<if test="specsJson != null and specsJson != ''">specs_json,</if>
<if test="iconUrl != null and iconUrl != ''">icon_url,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null and name != ''">#{name},</if>
<if test="type != null and type != ''">#{type},</if>
<if test="specsJson != null and specsJson != ''">#{specsJson},</if>
<if test="iconUrl != null and iconUrl != ''">#{iconUrl},</if>
</trim>
</insert>
@ -49,6 +53,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="name != null and name != ''">name = #{name},</if>
<if test="type != null and type != ''">type = #{type},</if>
<if test="specsJson != null and specsJson != ''">specs_json = #{specsJson},</if>
<if test="iconUrl != null and iconUrl != ''">icon_url = #{iconUrl},</if>
</trim>
where id = #{id}
</update>

3
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

7
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'
}
})
}

195
ruoyi-ui/src/views/cesiumMap/index.vue

@ -191,16 +191,16 @@ export default {
// 线线
this.allEntities = this.allEntities.filter(item => {
// 线
const isRouteRelated = item.type === 'route' || item.routeId ||
(item.entity && item.entity.properties &&
const isRouteRelated = item.type === 'route' || item.routeId ||
(item.entity && item.entity.properties &&
(item.entity.properties.isMissionWaypoint || item.entity.properties.isMissionRouteLine));
// 线
if (isRouteRelated && item.entity) {
this.viewer.entities.remove(item.entity);
return false;
}
// 线
return true;
});
@ -247,6 +247,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);
//
@ -322,8 +323,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);
@ -337,15 +338,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}`,
@ -373,25 +396,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;
@ -471,7 +568,7 @@ export default {
this.handler.setInputAction((click) => {
//
this.contextMenu.visible = false;
if (this.isDrawing) return;
const pickedObject = this.viewer.scene.pick(click.position);
@ -511,10 +608,10 @@ export default {
if (this.isDrawing) {
return;
}
//
this.contextMenu.visible = false;
const pickedObject = this.viewer.scene.pick(click.position)
if (Cesium.defined(pickedObject) && pickedObject.id) {
const pickedEntity = pickedObject.id
@ -579,8 +676,8 @@ export default {
const length = this.calculateLineLength(entityData.positions)
//
const bearingType = entityData.bearingType || 'true';
const bearing = bearingType === 'magnetic'
? this.calculateMagneticBearing(entityData.positions)
const bearing = bearingType === 'magnetic'
? this.calculateMagneticBearing(entityData.positions)
: this.calculateTrueBearing(entityData.positions);
//
this.hoverTooltip = {
@ -763,38 +860,54 @@ 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.hoverTooltip.visible = false;
this.viewer.scene.canvas.style.cursor = 'default';
console.log(">>> 绘制状态已彻底重置,临时实体已清理");
},
cancelDrawing() {
//
@ -2104,23 +2217,23 @@ export default {
// 线
const startPos = positions[positions.length - 2];
const endPos = positions[positions.length - 1];
//
const startCartographic = Cesium.Cartographic.fromCartesian(startPos);
const endCartographic = Cesium.Cartographic.fromCartesian(endPos);
//
const startLat = startCartographic.latitude;
const startLng = startCartographic.longitude;
const endLat = endCartographic.latitude;
const endLng = endCartographic.longitude;
//
const dLng = endLng - startLng;
const y = Math.sin(dLng) * Math.cos(endLat);
const x = Math.cos(startLat) * Math.sin(endLat) - Math.sin(startLat) * Math.cos(endLat) * Math.cos(dLng);
let bearing = Math.atan2(y, x);
// 0-360
bearing = Cesium.Math.toDegrees(bearing);
return (bearing + 360) % 360;
@ -2129,21 +2242,21 @@ export default {
calculateMagneticDeclination(lat, lng) {
//
// 使 WMM (World Magnetic Model)
//
const latDeg = Cesium.Math.toDegrees(lat);
const lngDeg = Cesium.Math.toDegrees(lng);
//
//
let declination;
if (lngDeg >= 73 && lngDeg <= 135 && latDeg >= 18 && latDeg <= 53) {
//
// 20200.1
const year = new Date().getFullYear();
const yearOffset = (year - 2020) * 0.1;
if (lngDeg < 100) {
// 西
declination = 2.0 - yearOffset;
@ -2158,7 +2271,7 @@ export default {
// 0
declination = 0;
}
return declination;
},
//
@ -2166,22 +2279,22 @@ export default {
if (!positions || positions.length < 2) {
return 0;
}
//
const trueBearing = this.calculateTrueBearing(positions);
// 线
const startPos = positions[positions.length - 2];
const startCartographic = Cesium.Cartographic.fromCartesian(startPos);
const startLat = startCartographic.latitude;
const startLng = startCartographic.longitude;
//
const declination = this.calculateMagneticDeclination(startLat, startLng);
//
let magneticBearing = trueBearing + declination;
// 0-360
return (magneticBearing + 360) % 360;
},
@ -2833,7 +2946,7 @@ export default {
selectedLineEntity.positions = newPositions
//
selectedLineEntity.points[pointIndex] = this.cartesianToLatLng(newPosition)
// 线
//
const length = this.calculateLineLength(selectedLineEntity.positions)
//

87
ruoyi-ui/src/views/childRoom/RightPanel.vue

@ -77,7 +77,7 @@
<!-- 航点列表 -->
<div v-if="expandedRoutes.includes(route.id)" class="tree-children waypoint-children">
<div
v-for="point in (expandedRoutes.includes(route.id) && route.waypoints ? route.waypoints : [])"
v-for="(point,index) in (expandedRoutes.includes(route.id) && route.waypoints ? route.waypoints : [])"
:key="point.name"
class="tree-item waypoint-item"
>
@ -85,10 +85,10 @@
<i class="el-icon-location tree-icon"></i>
<div class="tree-item-info">
<div class="tree-item-name">{{ point.name }}</div>
<div class="tree-item-meta">高度: {{ point.altitude }}m | 速度: {{ point.speed }}</div>
<div class="tree-item-meta">高度: {{ point.alt }}m | 速度: {{ point.speed }}</div>
</div>
<div class="tree-item-actions">
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenWaypointDialog(point)"></i>
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenWaypointDialog(point, index, route.waypoints.length)"></i>
</div>
</div>
</div>
@ -144,6 +144,19 @@
</div>
</div>
<div v-if="activeTab === 'platform'" class="tab-content platform-content">
<div class="section-header" style="padding: 10px 0; display: flex; justify-content: space-between; align-items: center;">
<div class="section-title">平台列表</div>
<el-button
type="primary"
size="mini"
icon="el-icon-upload2"
@click="handleImportPlatform"
class="create-route-btn-new"
style="width: 90px;height: 25px;line-height: 25px;padding: 0;font-size: 14px;"
>
导入平台
</el-button>
</div>
<div class="platform-categories">
<el-tabs v-model="activePlatformTab" type="card" size="mini" class="blue-tabs">
<el-tab-pane label="空中" name="air">
@ -155,7 +168,11 @@
@click="handleOpenPlatformDialog(platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<i :class="platform.icon"></i>
<img v-if="isImg(platform.imageUrl)"
:src="formatImg(platform.imageUrl)"
class="platform-img"
/>
<i v-else :class="platform.icon || 'el-icon-picture-outline'"></i>
</div>
<div class="platform-info">
<div class="platform-name">{{ platform.name }}</div>
@ -176,7 +193,11 @@
@click="handleOpenPlatformDialog(platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<i :class="platform.icon"></i>
<img v-if="isImg(platform.imageUrl)"
:src="formatImg(platform.imageUrl)"
class="platform-img"
/>
<i v-else :class="platform.icon || 'el-icon-picture-outline'"></i>
</div>
<div class="platform-info">
<div class="platform-name">{{ platform.name }}</div>
@ -197,7 +218,11 @@
@click="handleOpenPlatformDialog(platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<i :class="platform.icon"></i>
<img v-if="isImg(platform.imageUrl)"
:src="formatImg(platform.imageUrl)"
class="platform-img"
/>
<i v-else :class="platform.icon || 'el-icon-picture-outline'"></i>
</div>
<div class="platform-info">
<div class="platform-name">{{ platform.name }}</div>
@ -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;
}
</style>

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

@ -137,6 +137,7 @@
@resolve-conflict="resolveConflict"
@run-conflict-check="runConflictCheck"
@open-platform-dialog="openPlatformDialog"
@open-import-dialog="showImportDialog = true"
/>
<!-- 左下角工具面板 -->
<bottom-left-panel />
@ -251,6 +252,12 @@
@save="savePageLayout"
/>
<!-- 导入平台弹窗 -->
<PlatformImportDialog
:visible.sync="showImportDialog"
@confirm="handleImportConfirm"
/>
<el-dialog
title="新建方案"
:visible.sync="showPlanNameDialog"
@ -287,9 +294,12 @@ import TopHeader from './TopHeader'
import { listScenario,addScenario,delScenario} from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes,delRoutes } from "@/api/system/routes";
import { updateWaypoints } from "@/api/system/waypoints";
import { listLib,addLib } from "@/api/system/lib";
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue";
export default {
name: 'MissionPlanningView',
components: {
PlatformImportDialog,
cesiumMap,
OnlineMembersDialog,
PlatformEditDialog,
@ -329,6 +339,8 @@ export default {
showNameDialog: false,
newRouteName: '',
tempMapPoints: [],
//
showImportDialog: false,
//
roomCode: 'JTF-7-ALPHA',
@ -439,22 +451,9 @@ export default {
//
activePlatformTab: 'air',
airPlatforms: [
{ id: 1, name: 'J-20 歼击机', type: '战斗机', icon: 'el-icon-fighter', color: '#52c41a', status: 'ready' },
{ id: 2, name: 'KJ-2000 预警机', type: '预警机', icon: 'el-icon-s-promotion', color: '#1890ff', status: 'flying' },
{ id: 3, name: 'UAV-01 无人机', type: '侦察无人机', icon: 'el-icon-drone', color: '#fa8c16', status: 'scouting' },
{ id: 4, name: 'H-6K 轰炸机', type: '轰炸机', icon: 'el-icon-bomb', color: '#722ed1', status: 'ready' },
],
seaPlatforms: [
{ id: 5, name: '辽宁舰', type: '航空母舰', icon: 'el-icon-s-cooperation', color: '#1890ff', status: 'sailing' },
{ id: 6, name: '055型驱逐舰', type: '驱逐舰', icon: 'el-icon-ship', color: '#52c41a', status: 'patrol' },
{ id: 7, name: '093型潜艇', type: '核潜艇', icon: 'el-icon-s-help', color: '#333', status: 'hidden' },
],
groundPlatforms: [
{ id: 8, name: 'HQ-9防空系统', type: '防空导弹', icon: 'el-icon-mic', color: '#f5222d', status: 'alert' },
{ id: 9, name: 'PLZ-05自行火炮', type: '自行火炮', icon: 'el-icon-aim', color: '#fa8c16', status: 'ready' },
{ id: 10, name: '指挥控制车', type: '指挥车', icon: 'el-icon-monitor', color: '#1890ff', status: 'operating' },
],
airPlatforms: [],
seaPlatforms: [],
groundPlatforms: [],
//
timeProgress: 45,
@ -491,6 +490,7 @@ export default {
this.currentRoomId = this.$route.query.roomId;
console.log("从路由接收到的真实房间 ID:", this.currentRoomId);
this.getList();
this.getPlatformList();
},
methods: {
//
@ -526,6 +526,24 @@ export default {
this.$message.info('未找到该航点的业务数据');
console.error("查找失败!账本内IDs:", this.selectedRouteDetails.waypoints.map(w => 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() {
@ -536,6 +554,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('平台更新成功');
@ -545,7 +618,7 @@ export default {
this.selectedRoute = route;
this.showRouteDialog = true;
},
// 线
// 线
updateRoute(updatedRoute) {
const index = this.routes.findIndex(r => r.id === updatedRoute.id);
if (index !== -1) {
@ -564,13 +637,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) {
@ -756,34 +827,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 200catch
throw new Error(response.msg || '后端业务逻辑校验失败');
}
} catch (error) {
console.error("更新航点失败:", error);
this.$message.error('数据库更新失败,请重试');
this.$message.error(error.message || '数据库更新失败,请重试');
}
},
updateTime() {
@ -1457,7 +1541,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`
});

128
ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue

@ -0,0 +1,128 @@
<template>
<el-dialog
title="新增平台"
:visible="visible"
width="500px"
@close="handleClose"
custom-class="blue-dialog"
>
<el-form :model="formData" :rules="rules" ref="importForm" label-width="100px" size="small">
<el-form-item label="平台名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入平台名称"></el-input>
</el-form-item>
<el-form-item label="平台分类" prop="type">
<el-select v-model="formData.type" placeholder="请选择分类" style="width: 100%">
<el-option label="空中" value="Air"></el-option>
<el-option label="海上" value="Sea"></el-option>
<el-option label="地面" value="Ground"></el-option>
</el-select>
</el-form-item>
<el-form-item label="平台图标">
<el-upload
class="icon-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="handleIconChange"
>
<div v-if="imageUrl" class="preview-container">
<img :src="imageUrl" class="platform-icon-preview">
<div class="re-upload-mask">修改图标</div>
</div>
<i v-else class="el-icon-plus uploader-icon"></i>
</el-upload>
<div class="upload-tip">建议尺寸: 64x64, 支持 png/jpg</div>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确认新增</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'PlatformImportDialog',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
imageUrl: '',
formData: {
name: '',
type: '',
icon_file: null
},
rules: {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择分类', trigger: 'change' }]
}
}
},
methods: {
handleIconChange(file) {
//
this.imageUrl = URL.createObjectURL(file.raw);
this.formData.icon_file = file.raw;
},
handleClose() {
this.imageUrl = '';
//
this.formData = {
name: '',
type: '',
icon_file: null
};
this.$emit('update:visible', false);
},
handleConfirm() {
this.$refs.importForm.validate((valid) => {
if (valid) {
// File formData
this.$emit('confirm', { ...this.formData });
this.handleClose();
}
});
}
}
}
</script>
<style scoped>
/* 简单的上传样式 */
.icon-uploader {
border: 1px dashed #409EFF;
border-radius: 6px;
width: 80px;
height: 80px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.platform-icon-preview {
width: 60px;
height: 60px;
object-fit: contain;
}
.uploader-icon {
font-size: 24px;
color: #8c939d;
}
.upload-tip {
font-size: 12px;
color: #999;
line-height: 20px;
}
</style>

44
ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue

@ -14,9 +14,9 @@
<el-input v-model="formData.name" placeholder="请输入航点名称"></el-input>
</el-form-item>
<el-form-item label="高度" prop="altitude">
<el-form-item label="高度" prop="alt">
<el-input-number
v-model="formData.altitude"
v-model="formData.alt"
:min="0"
controls-position="right"
placeholder="请输入高度"
@ -34,13 +34,17 @@
></el-input-number>
</el-form-item>
<el-form-item label="转弯坡度" prop="turnBank">
<el-form-item label="转弯坡度" prop="turnAngle">
<el-input-number
v-model="formData.turnBank"
v-model="formData.turnAngle"
controls-position="right"
placeholder="请输入转弯坡度"
style="width: 100%;"
:disabled="formData.isBankDisabled"
></el-input-number>
<div v-if="formData.isBankDisabled" style="color: #909399; font-size: 12px; margin-top: 4px;">
首尾航点坡度已锁定为 0不可编辑
</div>
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
@ -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();

Loading…
Cancel
Save