sd 2 months ago
parent
commit
53ee0e74cc
  1. 26
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/PlatformLibController.java
  2. 7
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  3. 2
      ruoyi-admin/src/main/resources/application-druid.yml
  4. 4
      ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java
  5. 77
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  6. 72
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PlatformLib.java
  7. 20
      ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java
  8. 8
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/RouteWaypointsMapper.java
  9. 8
      ruoyi-system/src/main/java/com/ruoyi/system/service/IRouteWaypointsService.java
  10. 26
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/MissionScenarioServiceImpl.java
  11. 5
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RouteWaypointsServiceImpl.java
  12. 36
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoutesServiceImpl.java
  13. 7
      ruoyi-system/src/main/resources/mapper/system/PlatformLibMapper.xml
  14. 4
      ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
  15. 1
      ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml
  16. 3
      ruoyi-ui/.env.development
  17. 2
      ruoyi-ui/package.json
  18. 7
      ruoyi-ui/src/api/system/lib.js
  19. 1
      ruoyi-ui/src/store/getters.js
  20. 7
      ruoyi-ui/src/store/modules/user.js
  21. 36
      ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue
  22. 2436
      ruoyi-ui/src/views/cesiumMap/index.vue
  23. 474
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  24. 216
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  25. 823
      ruoyi-ui/src/views/childRoom/index.vue
  26. 50
      ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue
  27. 128
      ruoyi-ui/src/views/dialogs/PlatformImportDialog.vue
  28. 44
      ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
  29. 11
      ruoyi-ui/src/views/selectRoom/index.vue
  30. 11
      ruoyi-ui/vue.config.js

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

7
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java

@ -77,7 +77,12 @@ public class RoutesController extends BaseController
@PostMapping
public AjaxResult add(@RequestBody Routes routes)
{
return toAjax(routesService.insertRoutes(routes));
// 1. 执行插入,MyBatis 会通过 useGeneratedKeys="true" 自动将新 ID 注入 routes 对象
int rows = routesService.insertRoutes(routes);
// 2. 不要用 toAjax,直接返回 success 并带上 routes 对象
// 这样前端 response.data 就会包含这个带有 ID 的完整对象
return rows > 0 ? AjaxResult.success(routes) : AjaxResult.error("新增航线失败");
}
/**

2
ruoyi-admin/src/main/resources/application-druid.yml

@ -8,7 +8,7 @@ spring:
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: A20040303ctw!
password: 123456
# 从库数据源
slave:
# 从数据源开关/默认关闭

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

20
ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java

@ -41,11 +41,11 @@ public class RouteWaypoints extends BaseEntity
/** 高度 (米) */
@Excel(name = "高度 (米)")
private Long alt;
private Double alt;
/** 速度 (km/h) */
@Excel(name = "速度 (km/h)")
private Long speed;
private Double speed;
/** 起始时间 (如: K+00:40:00) */
@Excel(name = "起始时间 (如: K+00:40:00)")
@ -53,7 +53,7 @@ public class RouteWaypoints extends BaseEntity
/** 转弯角度 (用于计算转弯半径) */
@Excel(name = "转弯角度 (用于计算转弯半径)")
private Long turnAngle;
private Double turnAngle;
public void setId(Long id)
{
@ -115,22 +115,22 @@ public class RouteWaypoints extends BaseEntity
return lng;
}
public void setAlt(Long alt)
public void setAlt(Double alt)
{
this.alt = alt;
}
public Long getAlt()
public Double getAlt()
{
return alt;
}
public void setSpeed(Long speed)
public void setSpeed(Double speed)
{
this.speed = speed;
}
public Long getSpeed()
public Double getSpeed()
{
return speed;
}
@ -145,12 +145,12 @@ public class RouteWaypoints extends BaseEntity
return startTime;
}
public void setTurnAngle(Long turnAngle)
public void setTurnAngle(Double turnAngle)
{
this.turnAngle = turnAngle;
}
public Long getTurnAngle()
public Double getTurnAngle()
{
return turnAngle;
}
@ -183,7 +183,7 @@ public class RouteWaypoints extends BaseEntity
// 单位换算:速度从 km/h 转为 m/s
double v_mps = this.speed / 3.6;
// 单位换算:角度从 度(Degree) 转为 弧度(Radians)
double radians = Math.toRadians(this.turnAngle.doubleValue());
double radians = Math.toRadians(this.turnAngle);
// 重力加速度 g
double g = 9.8;
// 计算半径

8
ruoyi-system/src/main/java/com/ruoyi/system/mapper/RouteWaypointsMapper.java

@ -55,6 +55,14 @@ public interface RouteWaypointsMapper
public int deleteRouteWaypointsById(Long id);
/**
* 删除航线具体航点明细
*
* @param routeId 航线主键
* @return 结果
*/
public int deleteRouteWaypointsByRouteId(Long routeId);
/**
* 批量删除航线具体航点明细
*
* @param ids 需要删除的数据主键集合

8
ruoyi-system/src/main/java/com/ruoyi/system/service/IRouteWaypointsService.java

@ -58,4 +58,12 @@ public interface IRouteWaypointsService
* @return 结果
*/
public int deleteRouteWaypointsById(Long id);
/**
* 删除航线具体航点明细信息
*
* @param routeId 航线具体航点明细主键
* @return 结果
*/
public int deleteRouteWaypointsByRouteId(Long routeId);
}

26
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/MissionScenarioServiceImpl.java

@ -1,11 +1,15 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import com.ruoyi.system.domain.Routes;
import com.ruoyi.system.service.IRoutesService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.mapper.MissionScenarioMapper;
import com.ruoyi.system.domain.MissionScenario;
import com.ruoyi.system.service.IMissionScenarioService;
import org.springframework.transaction.annotation.Transactional;
/**
* 任务方案/沙箱Service业务层处理
@ -19,6 +23,9 @@ public class MissionScenarioServiceImpl implements IMissionScenarioService
@Autowired
private MissionScenarioMapper missionScenarioMapper;
@Autowired
private IRoutesService routesService;
/**
* 查询任务方案/沙箱
*
@ -74,9 +81,15 @@ public class MissionScenarioServiceImpl implements IMissionScenarioService
* @return 结果
*/
@Override
@Transactional
public int deleteMissionScenarioByIds(Long[] ids)
{
return missionScenarioMapper.deleteMissionScenarioByIds(ids);
int rows = 0;
for (Long id : ids) {
// 循环调用单条删除逻辑,确保每套方案下的从属数据都被清理干净
rows += this.deleteMissionScenarioById(id);
}
return rows;
}
/**
@ -86,8 +99,19 @@ public class MissionScenarioServiceImpl implements IMissionScenarioService
* @return 结果
*/
@Override
@Transactional
public int deleteMissionScenarioById(Long id)
{
// 查找属于该方案的所有航线
Routes queryRoute = new Routes();
queryRoute.setScenarioId(id);
List<Routes> relatedRoutes = routesService.selectRoutesList(queryRoute);
// 循环删除每一条航线
if (relatedRoutes != null) {
for (Routes route : relatedRoutes) {
routesService.deleteRoutesById(route.getId());
}
}
return missionScenarioMapper.deleteMissionScenarioById(id);
}
}

5
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RouteWaypointsServiceImpl.java

@ -101,4 +101,9 @@ public class RouteWaypointsServiceImpl implements IRouteWaypointsService
{
return routeWaypointsMapper.deleteRouteWaypointsById(id);
}
@Override
public int deleteRouteWaypointsByRouteId(Long routeId) {
return routeWaypointsMapper.deleteRouteWaypointsByRouteId(routeId);
}
}

36
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoutesServiceImpl.java

@ -35,7 +35,18 @@ public class RoutesServiceImpl implements IRoutesService
@Override
public Routes selectRoutesById(Long id)
{
return routesMapper.selectRoutesById(id);
// 查出航线基本信息
Routes routes = routesMapper.selectRoutesById(id);
// 如果查到了航线,就再去查属于它的航点
if (routes != null) {
RouteWaypoints queryWp = new RouteWaypoints();
queryWp.setRouteId(id); // 根据航线ID查询
List<RouteWaypoints> wpList = routeWaypointsService.selectRouteWaypointsList(queryWp);
// 把查出来的航点列表塞进 routes 对象的 waypoints 属性里
routes.setWaypoints(wpList);
}
return routes;
}
/**
@ -47,7 +58,16 @@ public class RoutesServiceImpl implements IRoutesService
@Override
public List<Routes> selectRoutesList(Routes routes)
{
return routesMapper.selectRoutesList(routes);
// 获取基础列表
List<Routes> list = routesMapper.selectRoutesList(routes);
// 遍历列表,为每一条航线补全它的航点信息
for (Routes r : list) {
RouteWaypoints queryWp = new RouteWaypoints();
queryWp.setRouteId(r.getId());
List<RouteWaypoints> wpList = routeWaypointsService.selectRouteWaypointsList(queryWp);
r.setWaypoints(wpList);
}
return list;
}
/**
@ -98,9 +118,17 @@ public class RoutesServiceImpl implements IRoutesService
* @return 结果
*/
@Override
@Transactional
public int deleteRoutesByIds(Long[] ids)
{
return routesMapper.deleteRoutesByIds(ids);
int rows = 0;
for (Long id : ids) {
// 1. 清理航点
routeWaypointsService.deleteRouteWaypointsByRouteId(id);
// 2. 累加删除航线的行数
rows += routesMapper.deleteRoutesById(id);
}
return rows;
}
/**
@ -110,8 +138,10 @@ public class RoutesServiceImpl implements IRoutesService
* @return 结果
*/
@Override
@Transactional
public int deleteRoutesById(Long id)
{
routeWaypointsService.deleteRouteWaypointsByRouteId(id);
return routesMapper.deleteRoutesById(id);
}
}

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>

4
ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml

@ -91,6 +91,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
delete from route_waypoints where id = #{id}
</delete>
<delete id="deleteRouteWaypointsByRouteId" parameterType="Long">
delete from route_waypoints where route_id = #{routeId}
</delete>
<delete id="deleteRouteWaypointsByIds" parameterType="String">
delete from route_waypoints where id in
<foreach item="id" collection="array" open="(" separator="," close=")">

1
ruoyi-system/src/main/resources/mapper/system/RoutesMapper.xml

@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="platformId" column="platform_id" />
<result property="callSign" column="call_sign" />
<result property="attributes" column="attributes" />
<collection property="waypoints" javaType="java.util.List" resultMap="com.ruoyi.system.mapper.RouteWaypointsMapper.RouteWaypointsResult" />
</resultMap>
<sql id="selectRoutesVo">

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://192.168.50.145:8080'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

2
ruoyi-ui/package.json

@ -6,6 +6,7 @@
"license": "MIT",
"scripts": {
"dev": "vue-cli-service serve",
"move-tiles": "node scripts/move-tiles-out-of-public.js",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview"
@ -61,6 +62,7 @@
"chalk": "4.1.0",
"compression-webpack-plugin": "6.1.2",
"connect": "3.6.6",
"express": "^4.18.2",
"sass": "1.32.13",
"sass-loader": "10.1.1",
"script-ext-html-webpack-plugin": "2.1.5",

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

1
ruoyi-ui/src/store/getters.js

@ -13,6 +13,7 @@ const getters = {
introduction: state => state.user.introduction,
roles: state => state.user.roles,
permissions: state => state.user.permissions,
userLevel: state => state.user.userLevel,
permission_routes: state => state.permission.routes,
topbarRouters: state => state.permission.topbarRouters,
defaultRoutes: state => state.permission.defaultRoutes,

7
ruoyi-ui/src/store/modules/user.js

@ -13,7 +13,8 @@ const user = {
nickName: '',
avatar: '',
roles: [],
permissions: []
permissions: [],
userLevel: ''
},
mutations: {
@ -37,6 +38,9 @@ const user = {
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
},
SET_USER_LEVEL: (state, userLevel) => {
state.userLevel = userLevel
}
},
@ -77,6 +81,7 @@ const user = {
commit('SET_NAME', user.userName)
commit('SET_NICK_NAME', user.nickName)
commit('SET_AVATAR', avatar)
commit('SET_USER_LEVEL', user.userLevel || '')
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
MessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {

36
ruoyi-ui/src/views/cesiumMap/DrawingToolbar.vue

@ -30,23 +30,47 @@ export default {
hasEntities: {
type: Boolean,
default: false
},
toolMode: {
type: String,
default: 'airspace' // 'airspace' or 'ranging'
}
},
data() {
return {
toolbarItems: [
{ id: 'point', name: '点', icon: 'el-icon-location' },
{ id: 'line', name: '线', icon: 'el-icon-edit-outline' },
{ id: 'polygon', name: '面', icon: 'el-icon-s-grid' },
{ id: 'rectangle', name: '矩形', icon: 'el-icon-s-data' },
// 线
allToolbarItems: [
{ id: 'mouse', name: '鼠标', icon: 'el-icon-position' },
{ id: 'polygon', name: '面', icon: 'el-icon-house' },
{ id: 'rectangle', name: '矩形', icon: 'el-icon-crop' },
{ id: 'circle', name: '圆形', icon: 'el-icon-circle-plus-outline' },
{ id: 'sector', name: '扇形', icon: 'el-icon-pie-chart' },
{ id: 'arrow', name: '箭头', icon: 'el-icon-right' },
{ id: 'text', name: '文本', icon: 'el-icon-document' },
{ id: 'image', name: '图片', icon: 'el-icon-picture-outline' },
{ id: 'locate', name: '定位', icon: 'el-icon-aim' },
{ id: 'clear', name: '清除', icon: 'el-icon-delete' },
{ id: 'import', name: '导入', icon: 'el-icon-upload' },
{ id: 'export', name: '导出', icon: 'el-icon-download' }
],
//
rangingToolbarItems: [
{ id: 'mouse', name: '鼠标', icon: 'el-icon-position' },
{ id: 'point', name: '点', icon: 'el-icon-location' },
{ id: 'line', name: '线', icon: 'el-icon-edit-outline' },
{ id: 'clear', name: '清除', icon: 'el-icon-delete' }
]
}
},
computed: {
toolbarItems() {
if (this.toolMode === 'ranging') {
return this.rangingToolbarItems;
} else {
return this.allToolbarItems;
}
}
},
methods: {
handleItemClick(item) {
if (item.id === 'clear') {
@ -57,6 +81,8 @@ export default {
this.$emit('import-data')
} else if (item.id === 'locate') {
this.$emit('locate')
} else if (item.id === 'mouse') {
this.$emit('toggle-drawing', null)
} else {
this.$emit('toggle-drawing', item.id)
}

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

File diff suppressed because it is too large

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

@ -26,99 +26,79 @@
{{ $t('rightPanel.createPlan') }}
</el-button>
</div>
<div class="route-list">
<div class="tree-list">
<!-- 方案列表 -->
<div
v-for="plan in plans"
:key="plan.id"
class="route-item"
class="tree-item plan-item"
:class="{ selected: selectedPlanId === plan.id }"
@click="handleSelectPlan(plan)"
>
<i class="el-icon-folder-opened"></i>
<div class="route-info">
<div class="route-name">{{ plan.name }}</div>
<div class="route-meta">{{ plan.routes.length }}{{ $t('rightPanel.routes') }}</div>
</div>
<div class="route-actions">
<i class="el-icon-edit" :title="$t('topHeader.edit.iconEdit')" @click.stop="handleOpenPlanDialog(plan)"></i>
</div>
</div>
</div>
</div>
<div v-if="selectedPlanDetails" class="section">
<div class="section-header">
<div class="section-title">{{ $t('rightPanel.routeList') }}</div>
<el-button
type="primary"
size="mini"
@click="handleCreateRoute"
class="create-route-btn-new"
>
{{ $t('rightPanel.createRoute') }}
</el-button>
</div>
<div class="route-list">
<div
v-for="route in selectedPlanDetails.routes"
:key="route.id"
class="route-item"
:class="{ selected: selectedRouteId === route.id }"
@click="handleSelectRoute(route)"
>
<i class="el-icon-map-location"></i>
<div class="route-info">
<div class="route-name">{{ route.name }}</div>
<div class="route-meta">{{ route.points }}{{ $t('rightPanel.points') }}</div>
</div>
<el-tag
v-if="route.conflict"
size="mini"
type="danger"
class="conflict-tag"
>
{{ $t('rightPanel.conflict') }}
</el-tag>
<div class="route-actions">
<i class="el-icon-edit" :title="$t('topHeader.edit.iconEdit')" @click.stop="handleOpenRouteDialog(route)"></i>
</div>
</div>
</div>
</div>
<div v-if="selectedRouteDetails" class="section">
<div class="section-title">{{ $t('rightPanel.waypointList') }}</div>
<div class="waypoint-list">
<div
v-for="point in selectedRouteDetails.waypoints"
:key="point.name"
class="waypoint-item"
>
<i class="el-icon-location"></i>
<div class="waypoint-info">
<div class="waypoint-name">{{ point.name }}</div>
<div class="waypoint-meta">{{ $t('rightPanel.altitude') }}: {{ point.altitude }}m | {{ $t('rightPanel.speed') }}: {{ point.speed }}</div>
<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">{{ routes.filter(r => r.scenarioId === plan.id).length }}个航线</div>
</div>
<div class="tree-item-actions">
<i class="el-icon-plus" title="新建航线" @click.stop="handleCreateRouteForPlan(plan)"></i>
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenPlanDialog(plan)"></i>
<i class="el-icon-delete" title="删除" @click.stop="handleDeletePlan(plan)"></i>
</div>
</div>
<div class="waypoint-actions">
<i class="el-icon-edit" :title="$t('topHeader.edit.iconEdit')" @click.stop="handleOpenWaypointDialog(point)"></i>
<i class="el-icon-delete" :title="$t('leftMenu.delete')"></i>
<!-- 航线列表 -->
<div v-if="expandedPlans.includes(plan.id)" class="tree-children route-children">
<div
v-for="route in routes.filter(r => r.scenarioId === plan.id)"
:key="route.id"
class="tree-item route-item"
:class="getRouteClasses(route.id)"
>
<div class="tree-item-header" @click="toggleRoute(route.id)">
<i :class="expandedRoutes.includes(route.id) ? 'el-icon-map-location' : 'el-icon-map-location'" class="tree-icon"></i>
<div class="tree-item-info">
<div class="tree-item-name">{{ route.name }}</div>
<div class="tree-item-meta">{{ route.points }}{{ $t('rightPanel.points') }}</div>
</div>
<el-tag
v-if="route.conflict"
size="mini"
type="danger"
class="conflict-tag"
>
{{ $t('rightPanel.conflict') }}
</el-tag>
<div class="tree-item-actions">
<i class="el-icon-view" title="显示/隐藏" @click.stop="handleToggleRouteVisibility(route)"></i>
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenRouteDialog(route)"></i>
<i class="el-icon-delete" title="删除" @click.stop="$emit('delete-route', route)"></i>
</div>
</div>
<!-- 航点列表 -->
<div v-if="expandedRoutes.includes(route.id)" class="tree-children waypoint-children">
<div
v-for="(point,index) in (expandedRoutes.includes(route.id) && route.waypoints ? route.waypoints : [])"
:key="point.name"
class="tree-item waypoint-item"
>
<div class="tree-item-header">
<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.alt }}m | 速度: {{ point.speed }}</div>
</div>
<div class="tree-item-actions">
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenWaypointDialog(point, index, route.waypoints.length)"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="action-buttons">
<el-button type="primary" size="mini" icon="el-icon-circle-plus" class="blue-btn" @click="handleAddWaypoint">
{{ $t('rightPanel.addWaypoint') }}
</el-button>
<el-button size="mini" class="blue-btn" @click="handleCancelRoute">
{{ $t('rightPanel.cancelRoute') }}
</el-button>
</div>
</div>
<div v-if="activeTab === 'conflict'" class="tab-content conflict-content">
<div v-if="conflicts.length > 0" class="conflict-list">
<div
@ -163,8 +143,20 @@
</el-button>
</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="$t('rightPanel.air')" name="air">
@ -176,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>
@ -198,7 +194,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>
@ -220,7 +220,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>
@ -255,6 +259,14 @@ export default {
type: Array,
default: () => []
},
routes: {
type: Array,
default: () => []
},
activeRouteIds: {
type: Array,
default: () => []
},
selectedPlanId: {
type: [String, Number],
default: null
@ -290,23 +302,129 @@ export default {
groundPlatforms: {
type: Array,
default: () => []
}
},
},
data() {
return {
activePlatformTab: 'air'
activePlatformTab: 'air',
expandedPlans: [], //
expandedRoutes: [] // 线
}
},
watch: {
selectedPlanId(newId) {
if (newId) {
console.log('>>> [子组件同步] 检测到方案切换,自动展开 ID:', newId);
//
if (!this.expandedPlans.includes(newId)) {
this.expandedPlans.push(newId);
}
}
},
activeRouteIds: {
handler(newVal) {
console.log('activeRouteIds updated:', newVal);
},
deep: true
}
},
methods: {
handleHide() {
this.$emit('hide')
// /
togglePlan(planId) {
const index = this.expandedPlans.indexOf(planId)
if (index > -1) {
this.expandedPlans.splice(index, 1)
const planRoutes = this.routes.filter(r => r.scenarioId === planId)
planRoutes.forEach(route => {
if (this.activeRouteIds.includes(route.id)) {
this.handleToggleRouteVisibility(route)
}
})
this.$emit('select-plan', { id: null })
} else {
// 线
// 线 activeRouteIds
this.activeRouteIds.forEach(activeId => {
const activeRoute = this.routes.find(r => r.id === activeId);
if (activeRoute) {
this.$emit('toggle-route-visibility', activeRoute);
}
});
//
this.expandedPlans = [];
this.expandedRoutes = [];
// --- ---
this.expandedPlans.push(planId)
const plan = this.plans.find(p => p.id === planId)
if (plan) {
this.$emit('select-plan', plan)
}
// 线
const planRoutes = this.routes.filter(r => r.scenarioId === planId)
planRoutes.forEach(route => {
if (!this.activeRouteIds.includes(route.id)) {
this.handleSelectRoute(route)
}
})
}
},
handleSelectPlan(plan) {
this.$emit('select-plan', plan)
// 线/
toggleRoute(routeId) {
const route = this.routes.find(r => r.id === routeId)
if (!route) return
if (!route.waypoints) {
this.$set(route, 'waypoints', []); // 使 $set
}
const isRouteSelected = this.activeRouteIds.includes(routeId)
const isRouteExpanded = this.expandedRoutes.includes(routeId)
if (isRouteSelected) {
if (isRouteExpanded) {
const index = this.expandedRoutes.indexOf(routeId)
this.expandedRoutes.splice(index, 1)
} else {
this.expandedRoutes.push(routeId)
}
} else {
this.handleSelectRoute(route)
this.$nextTick(() => {
// 线
if (!this.expandedPlans.includes(route.scenarioId)) {
this.expandedPlans.push(route.scenarioId);
}
if (!this.expandedRoutes.includes(routeId)) {
this.expandedRoutes.push(routeId)
}
})
}
},
//
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')
},
handleSelectRoute(route) {
// 线
this.$emit('select-route', route)
},
@ -314,28 +432,53 @@ export default {
this.$emit('create-plan')
},
handleCreateRoute() {
this.$emit('create-route')
handleCreateRouteForPlan(plan) {
this.$emit('create-route', plan)
},
handleDeletePlan(plan) {
this.$confirm(`是否确认删除方案 "${plan.name}"?这将同时删除该方案下的所有航线。`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$emit('delete-plan', plan);
}).catch(() => {
});
},
handleOpenPlanDialog(plan) {
this.$emit('open-plan-dialog', plan)
},
handleImportPlatform() {
this.$emit('open-import-dialog');
},
handleOpenRouteDialog(route) {
this.$emit('open-route-dialog', route)
},
handleOpenWaypointDialog(point) {
this.$emit('open-waypoint-dialog', point)
handleToggleRouteVisibility(route) {
this.$emit('toggle-route-visibility', route)
// 线
const routeIndex = this.expandedRoutes.indexOf(route.id)
if (routeIndex > -1) {
this.expandedRoutes.splice(routeIndex, 1)
}
},
handleAddWaypoint() {
this.$emit('add-waypoint')
getRouteClasses(routeId) {
return {
active: this.activeRouteIds.includes(routeId)
}
},
handleCancelRoute() {
this.$emit('cancel-route')
handleOpenWaypointDialog(point,index,total) {
this.$emit('open-waypoint-dialog', {
...point,
currentIndex: index,
totalPoints: total
});
},
handleViewConflict(conflict) {
@ -459,115 +602,90 @@ export default {
opacity: 0.9;
}
.route-list {
.tree-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.route-item {
.tree-item {
border-radius: 6px;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
}
.tree-item-header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
position: relative;
border-radius: 6px;
}
.route-item:hover {
.tree-item-header:hover {
background: rgba(0, 138, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.route-item.selected {
background: rgba(0, 138, 255, 0.15);
border-color: rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 10px rgba(0, 138, 255, 0.25);
.tree-item.plan-item .tree-item-header {
background: rgba(255, 255, 255, 0.9) !important;
}
.route-info {
flex: 1;
.tree-item.route-item .tree-item-header {
background: rgba(255, 255, 255, 0.8) !important;
}
.route-name {
font-size: 14px;
font-weight: 500;
color: #333;
.tree-item.route-item:not(.active) .tree-item-header {
background: rgba(255, 255, 255, 0.8) !important;
}
.route-meta {
font-size: 12px;
color: #999;
.tree-item.waypoint-item .tree-item-header {
background: rgba(224, 238, 255, 0.8);
}
.route-actions {
display: flex;
gap: 8px;
}
.route-actions i {
cursor: pointer;
color: #008aff;
font-size: 14px;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.route-actions i:hover {
background: rgba(0, 138, 255, 0.1);
transform: scale(1.2);
}
.waypoint-list {
display: flex;
flex-direction: column;
gap: 8px;
.tree-item.active .tree-item-header {
background: rgba(0, 138, 255, 0.15) !important;
border-color: rgba(0, 138, 255, 0.3);
box-shadow: 0 2px 10px rgba(0, 138, 255, 0.25);
}
.waypoint-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: rgba(255, 255, 255, 0.8);
border-radius: 6px;
transition: all 0.3s;
border: 1px solid rgba(0, 138, 255, 0.1);
.tree-item.selected .tree-item-header {
background: rgba(0, 138, 255, 0.1) !important;
border-color: rgba(0, 138, 255, 0.2);
}
.waypoint-item:hover {
background: rgba(0, 138, 255, 0.1);
transform: translateX(-2px);
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
.tree-icon {
font-size: 16px;
color: #008aff;
flex-shrink: 0;
}
.waypoint-info {
.tree-item-info {
flex: 1;
}
.waypoint-name {
.tree-item-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.waypoint-meta {
.tree-item-meta {
font-size: 12px;
color: #999;
}
.waypoint-actions {
.tree-item-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.waypoint-actions i {
.tree-item-actions i {
cursor: pointer;
color: #008aff;
font-size: 14px;
@ -576,11 +694,27 @@ export default {
transition: all 0.2s;
}
.waypoint-actions i:hover {
.tree-item-actions i:hover {
background: rgba(0, 138, 255, 0.1);
transform: scale(1.2);
}
.tree-children {
margin-left: 20px;
margin-top: 4px;
display: flex;
flex-direction: column;
gap: 4px;
}
.route-children {
margin-left: 25px;
}
.waypoint-children {
margin-left: 50px;
}
.action-buttons {
display: flex;
gap: 10px;
@ -804,4 +938,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>

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

@ -3,19 +3,19 @@
<div class="header-left">
<div class="system-title">
<!-- 按照实际路径引入logo.jpg -->
<img
src="@/views/childRoom/logo.png"
class="logo-icon mr-2"
<img
src="@/views/childRoom/logo.png"
class="logo-icon mr-2"
alt="系统logo"
style="width:24px; height:24px; object-fit:contain;"
style="width:24px; height:24px; object-fit:contain;"
>
<span class="title-text blue-title">{{ $t('topHeader.systemTitle') }}</span>
</div>
<!-- 顶部导航菜单 -->
<div class="top-nav-menu">
<div
v-for="item in topNavItems"
<div
v-for="item in topNavItems"
:key="item.id"
class="top-nav-item"
:class="{ active: activeTopNav === item.id }"
@ -23,11 +23,11 @@
>
<i :class="item.icon" class="nav-icon"></i>
<span class="nav-text">{{ item.name }}</span>
<!-- 文件下拉菜单 -->
<el-dropdown
v-if="item.id === 'file'"
trigger="click"
<el-dropdown
v-if="item.id === 'file'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -37,11 +37,11 @@
<el-dropdown-item @click.native="newPlan">{{ $t('topHeader.file.newPlan') }}</el-dropdown-item>
<el-dropdown-item @click.native="openPlan">{{ $t('topHeader.file.open') }}</el-dropdown-item>
<el-dropdown-item @click.native="savePlan">{{ $t('topHeader.file.save') }}</el-dropdown-item>
<el-dropdown-item class="submenu-item">
<span>{{ $t('topHeader.file.import') }}</span>
<el-dropdown
trigger="hover"
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
@ -55,15 +55,15 @@
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item @click.native="exportPlan">{{ $t('topHeader.file.export') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 编辑下拉菜单 -->
<el-dropdown
v-if="item.id === 'edit'"
trigger="click"
<el-dropdown
v-if="item.id === 'edit'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -74,11 +74,11 @@
<el-dropdown-item @click.native="militaryMarking">{{ $t('topHeader.edit.militaryMarking') }}</el-dropdown-item>
<el-dropdown-item @click.native="iconEdit">{{ $t('topHeader.edit.iconEdit') }}</el-dropdown-item>
<el-dropdown-item @click.native="attributeEdit">{{ $t('topHeader.edit.attributeEdit') }}</el-dropdown-item>
<el-dropdown-item class="submenu-item">
<span>{{ $t('topHeader.edit.deductionEdit') }}</span>
<el-dropdown
trigger="hover"
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
@ -93,11 +93,11 @@
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 视图下拉菜单 -->
<el-dropdown
v-if="item.id === 'view'"
trigger="click"
<el-dropdown
v-if="item.id === 'view'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -110,11 +110,11 @@
<el-dropdown-item @click.native="toggleScale">{{ $t('topHeader.view.scale') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 地图下拉菜单 -->
<el-dropdown
v-if="item.id === 'map'"
trigger="click"
<el-dropdown
v-if="item.id === 'map'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -126,11 +126,11 @@
<el-dropdown-item @click.native="loadAeroChart">{{ $t('topHeader.map.aeroChart') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 空域下拉菜单 -->
<el-dropdown
v-if="item.id === 'airspace'"
trigger="click"
<el-dropdown
v-if="item.id === 'airspace'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -141,11 +141,11 @@
<el-dropdown-item @click.native="threatZone">{{ $t('topHeader.airspace.threatZone') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 工具下拉菜单 -->
<el-dropdown
v-if="item.id === 'tools'"
trigger="click"
<el-dropdown
v-if="item.id === 'tools'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -158,11 +158,11 @@
<el-dropdown-item @click.native="coordinateConversion">{{ $t('topHeader.tools.coordinateConversion') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 选项下拉菜单 -->
<el-dropdown
v-if="item.id === 'options'"
trigger="click"
<el-dropdown
v-if="item.id === 'options'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -171,8 +171,8 @@
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item class="submenu-item">
<span>{{ $t('topHeader.options.settings') }}</span>
<el-dropdown
trigger="hover"
<el-dropdown
trigger="hover"
placement="right-start"
class="submenu-dropdown"
>
@ -195,11 +195,11 @@
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 收藏下拉菜单 -->
<el-dropdown
v-if="item.id === 'favorites'"
trigger="click"
<el-dropdown
v-if="item.id === 'favorites'"
trigger="click"
placement="bottom-start"
:hide-on-click="false"
class="file-dropdown"
@ -224,7 +224,7 @@
<div class="info-value">{{ roomCode }}</div>
</div>
</div>
<div class="info-box" @click="showOnlineMembersDialog">
<i class="el-icon-user info-icon"></i>
<div class="info-content">
@ -232,7 +232,7 @@
<div class="info-value">{{ onlineCount }}{{ $t('topHeader.info.people') }}</div>
</div>
</div>
<div class="info-box">
<i class="el-icon-timer info-icon"></i>
<div class="info-content">
@ -240,7 +240,7 @@
<div class="info-value">{{ combatTime }}</div>
</div>
</div>
<div class="info-box">
<i class="el-icon-sunny info-icon"></i>
<div class="info-content">
@ -249,31 +249,31 @@
</div>
</div>
</div>
<!-- 用户状态区域 -->
<div class="user-status-area">
<!-- 用户头像 -->
<el-avatar :size="32" :src="userAvatar" class="user-avatar" />
</div>
</div>
<!-- 威力区弹窗 -->
<power-zone-dialog
v-model="powerZoneDialogVisible"
<power-zone-dialog
v-model="powerZoneDialogVisible"
:power-zone="currentPowerZone"
@save="savePowerZone"
/>
<!-- 比例尺弹窗 -->
<scale-dialog
v-model="scaleDialogVisible"
<scale-dialog
v-model="scaleDialogVisible"
:scale="currentScale"
@save="saveScale"
/>
<!-- 外部参数弹窗 -->
<external-params-dialog
v-model="externalParamsDialogVisible"
<external-params-dialog
v-model="externalParamsDialogVisible"
:external-params="currentExternalParams"
@save="saveExternalParams"
@import-airport="importAirport"
@ -355,7 +355,7 @@ export default {
selectTopNav(item) {
this.$emit('select-nav', item)
},
//
newPlan() {
this.$emit('new-plan')
@ -363,7 +363,7 @@ export default {
openPlan() {
this.$emit('open-plan')
},
//
savePlan() {
if (this.isIconEditMode) {
@ -372,40 +372,40 @@ export default {
this.$emit('save-plan')
}
},
importPlanFile() {
this.$emit('import-plan-file')
},
importACD() {
this.$emit('import-acd')
},
importATO() {
this.$emit('import-ato')
},
importLayer() {
this.$emit('import-layer')
},
importRoute() {
this.$emit('import-route')
},
exportPlan() {
this.$emit('export-plan')
},
//
routeEdit() {
this.$emit('route-edit')
},
militaryMarking() {
this.$emit('military-marking')
},
iconEdit() {
if (this.isIconEditMode) {
this.$emit('toggle-icon-edit', false)
@ -413,163 +413,163 @@ export default {
this.$emit('toggle-icon-edit', true)
}
},
attributeEdit() {
this.$emit('attribute-edit')
},
timeSettings() {
this.$emit('time-settings')
},
aircraftSettings() {
this.$emit('aircraft-settings')
},
keyEventEdit() {
this.$emit('key-event-edit')
},
missileLaunch() {
this.$emit('missile-launch')
},
//
toggle2D3D() {
this.is2DMode = !this.is2DMode
this.$emit('toggle-2d-3d', this.is2DMode)
},
toggleRuler() {
this.isRulerVisible = !this.isRulerVisible
this.$emit('toggle-ruler', this.isRulerVisible)
},
toggleGrid() {
this.$emit('toggle-grid')
},
toggleScale() {
this.scaleDialogVisible = true
this.currentScale = {}
},
//
loadTerrain() {
this.$emit('load-terrain')
},
changeProjection() {
this.$emit('change-projection')
},
loadAeroChart() {
this.$emit('load-aero-chart')
},
//
powerZone() {
this.powerZoneDialogVisible = true
this.currentPowerZone = {}
},
threatZone() {
this.$emit('threat-zone')
},
//
routeCalculation() {
this.$emit('route-calculation')
},
conflictDisplay() {
this.$emit('conflict-display')
},
dataMaterials() {
this.$emit('data-materials')
},
coordinateConversion() {
this.$emit('coordinate-conversion')
},
//
pageLayout() {
this.$emit('page-layout')
},
dataStoragePath() {
this.$emit('data-storage-path')
},
externalParams() {
this.externalParamsDialogVisible = true
this.currentExternalParams = {}
},
toggleAirport() {
this.isAirportVisible = !this.isAirportVisible
this.$emit('toggle-airport', this.isAirportVisible)
},
toggleLandmark() {
this.isLandmarkVisible = !this.isLandmarkVisible
this.$emit('toggle-landmark', this.isLandmarkVisible)
},
toggleRoute() {
this.isRouteVisible = !this.isRouteVisible
this.$emit('toggle-route', this.isRouteVisible)
},
systemDescription() {
this.$emit('system-description')
},
toggleLanguage() {
const newLocale = this.$i18n.locale === 'zh' ? 'en' : 'zh'
this.$i18n.locale = newLocale
localStorage.setItem('language', newLocale)
this.$emit('language-changed', newLocale)
},
//
layerFavorites() {
this.$emit('layer-favorites')
},
routeFavorites() {
this.$emit('route-favorites')
},
showOnlineMembersDialog() {
this.$emit('show-online-members')
},
//
savePowerZone(powerZone) {
this.$emit('save-power-zone', powerZone)
},
//
saveScale(scale) {
this.$emit('save-scale', scale)
},
//
saveExternalParams(externalParams) {
this.$emit('save-external-params', externalParams)
},
importAirport(path) {
this.$emit('import-airport', path)
},
importRoute(path) {
this.$emit('import-route-data', path)
},
importLandmark(path) {
this.$emit('import-landmark', path)
}
@ -892,4 +892,4 @@ export default {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>
</style>

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

File diff suppressed because it is too large

50
ruoyi-ui/src/views/dialogs/PlatformEditDialog.vue

@ -2,21 +2,21 @@
<div v-if="value" class="platform-edit-dialog">
<!-- 遮罩层 -->
<div class="dialog-overlay" @click="closeDialog"></div>
<!-- 弹窗内容 -->
<div class="dialog-content">
<div class="dialog-header">
<h3>平台编辑</h3>
<div class="close-btn" @click="closeDialog">×</div>
</div>
<div class="dialog-body">
<el-form :model="formData" :rules="rules" ref="formRef" label-width="80px" size="small">
<!-- 基本信息 -->
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入平台名称"></el-input>
</el-form-item>
<el-form-item label="位置">
<div class="location-inputs">
<el-input v-model="formData.location.lat" placeholder="纬度" style="width: 120px;"></el-input>
@ -24,54 +24,54 @@
<el-input v-model="formData.location.lng" placeholder="经度" style="width: 120px;"></el-input>
</div>
</el-form-item>
<el-form-item label="速度" prop="speed">
<el-input v-model="formData.speed" placeholder="请输入速度" suffix="km/h"></el-input>
</el-form-item>
<el-form-item label="油耗表" prop="fuelConsumption">
<el-input-number
v-model="formData.fuelConsumption"
:min="0"
:precision="2"
<el-input-number
v-model="formData.fuelConsumption"
:min="0"
:precision="2"
placeholder="请输入油耗"
style="width: 100%;"
suffix="L/km"
></el-input-number>
</el-form-item>
<el-form-item label="高度限制">
<div class="altitude-inputs">
<el-input-number
v-model="formData.altitude.min"
:min="0"
<el-input-number
v-model="formData.altitude.min"
:min="0"
placeholder="最低高度"
style="width: 120px;"
suffix="m"
></el-input-number>
<span class="altitude-separator">~</span>
<el-input-number
v-model="formData.altitude.max"
:min="0"
<el-input-number
v-model="formData.altitude.max"
:min="0"
placeholder="最高高度"
style="width: 120px;"
suffix="m"
></el-input-number>
</div>
</el-form-item>
<el-form-item label="威力区/扇区">
<div class="sector-inputs">
<el-input-number
v-model="formData.sector.radius"
:min="0"
<el-input-number
v-model="formData.sector.radius"
:min="0"
placeholder="半径"
style="width: 100px;"
suffix="km"
></el-input-number>
<el-input-number
v-model="formData.sector.angle"
:min="0"
<el-input-number
v-model="formData.sector.angle"
:min="0"
:max="360"
placeholder="角度"
style="width: 100px;"
@ -81,7 +81,7 @@
</el-form-item>
</el-form>
</div>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="savePlatform">保存</el-button>
@ -292,4 +292,4 @@ export default {
border-top: 1px solid #e8e8e8;
gap: 10px;
}
</style>
</style>

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

11
ruoyi-ui/src/views/selectRoom/index.vue

@ -46,7 +46,6 @@
</div>
</div>
</div>
<div v-if="expandedRooms.includes(room.id)" class="child-rooms">
<div
v-for="childRoom in getChildRooms(room.id)"
@ -75,14 +74,12 @@
</div>
</div>
</div>
<div v-if="getParentRooms.length === 0" class="empty-state">
<i class="fa fa-sitemap"></i>
<h3>暂无房间</h3>
<p>点击下方按钮创建您的第一个房间</p>
</div>
</div>
<div class="action-buttons">
<button @click="showAddRoomDialog" class="btn-secondary">
<i class="fa fa-plus"></i> 新增大房间
@ -97,12 +94,10 @@
</button>
</div>
</div>
<div class="footer-info">
<p>© 2026 网络化任务规划系统 | 支持Windows/国产化系统互通</p>
</div>
</div>
<div
v-if="contextMenu.visible"
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
@ -117,7 +112,6 @@
<span>删除房间</span>
</div>
</div>
<el-dialog
:title="dialog.mode === 'add' ? '新增房间' : '修改房间'"
:visible.sync="dialog.visible"
@ -198,7 +192,10 @@ export default {
},
enterRoom() {
if (this.selectedRoom) {
this.$router.push('/childRoom');
this.$router.push({
path: '/childRoom',
query: { roomId: this.selectedRoom }
});
}
},
showContextMenu(event, room) {

11
ruoyi-ui/vue.config.js

@ -1,10 +1,15 @@
'use strict'
const path = require('path')
const fs = require('fs')
function resolve(dir) {
return path.join(__dirname, dir)
}
// 离线瓦片目录:放在 public 外避免构建时复制导致 EMFILE(too many open files)
const tilesDir = path.join(__dirname, 'tiles')
const tilesInPublic = path.join(__dirname, 'public', 'tiles')
const CompressionPlugin = require('compression-webpack-plugin')
// 引入 CopyWebpackPlugin 用于复制 Cesium 静态资源
const CopyWebpackPlugin = require('copy-webpack-plugin')
@ -37,6 +42,12 @@ module.exports = {
host: '0.0.0.0',
port: port,
open: true,
// 从 public 外提供 /tiles,避免 2 万+ 瓦片参与 public 复制触发 EMFILE
before(app) {
const express = require('express')
const serveAt = fs.existsSync(tilesDir) ? tilesDir : tilesInPublic
app.use('/tiles', express.static(serveAt))
},
proxy: {
[process.env.VUE_APP_BASE_API]: {
target: baseUrl,

Loading…
Cancel
Save