wangxinping 2 months ago
parent
commit
d3f2e8a31e
  1. 2
      ruoyi-admin/src/main/resources/application-druid.yml
  2. 26
      ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java
  3. 12
      ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
  4. 31
      ruoyi-ui/src/permission.js
  5. 2
      ruoyi-ui/src/router/index.js
  6. 14
      ruoyi-ui/src/store/modules/permission.js
  7. 32
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  8. 54
      ruoyi-ui/src/views/cesiumMap/LocateDialog.vue
  9. 1490
      ruoyi-ui/src/views/cesiumMap/index.vue
  10. 58
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  11. 81
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  12. 732
      ruoyi-ui/src/views/childRoom/index.vue
  13. 32
      ruoyi-ui/src/views/dialogs/PowerZoneDialog.vue
  14. 153
      ruoyi-ui/src/views/dialogs/RadiusDialog.vue
  15. 83
      ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
  16. 54
      ruoyi-ui/src/views/system/waypoints/index.vue
  17. 1
      ruoyi-ui/vue.config.js

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: 123456
password: A20040303ctw!
# 从库数据源
slave:
# 从数据源开关/默认关闭

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

@ -55,6 +55,14 @@ public class RouteWaypoints extends BaseEntity
@Excel(name = "转弯角度 (用于计算转弯半径)")
private Double turnAngle;
/** 航点类型: normal-普通, hold_circle-圆形盘旋, hold_ellipse-椭圆盘旋 */
@Excel(name = "航点类型")
private String pointType;
/** 盘旋参数JSON: 圆(radius,clockwise) 椭圆(semiMajor,semiMinor,headingDeg,clockwise) */
@Excel(name = "盘旋参数")
private String holdParams;
public void setId(Long id)
{
this.id = id;
@ -155,6 +163,22 @@ public class RouteWaypoints extends BaseEntity
return turnAngle;
}
public void setPointType(String pointType) {
this.pointType = pointType;
}
public String getPointType() {
return pointType;
}
public void setHoldParams(String holdParams) {
this.holdParams = holdParams;
}
public String getHoldParams() {
return holdParams;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@ -168,6 +192,8 @@ public class RouteWaypoints extends BaseEntity
.append("speed", getSpeed())
.append("startTime", getStartTime())
.append("turnAngle", getTurnAngle())
.append("pointType", getPointType())
.append("holdParams", getHoldParams())
.toString();
}

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

@ -15,10 +15,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="speed" column="speed" />
<result property="startTime" column="start_time" />
<result property="turnAngle" column="turn_angle" />
<result property="pointType" column="point_type" />
<result property="holdParams" column="hold_params" />
</resultMap>
<sql id="selectRouteWaypointsVo">
select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle from route_waypoints
select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle, point_type, hold_params from route_waypoints
</sql>
<select id="selectRouteWaypointsList" parameterType="RouteWaypoints" resultMap="RouteWaypointsResult">
@ -33,6 +35,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="speed != null "> and speed = #{speed}</if>
<if test="startTime != null and startTime != ''"> and start_time = #{startTime}</if>
<if test="turnAngle != null "> and turn_angle = #{turnAngle}</if>
<if test="pointType != null and pointType != ''"> and point_type = #{pointType}</if>
<if test="holdParams != null and holdParams != ''"> and hold_params = #{holdParams}</if>
</where>
</select>
@ -57,6 +61,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="speed != null">speed,</if>
<if test="startTime != null and startTime != ''">start_time,</if>
<if test="turnAngle != null">turn_angle,</if>
<if test="pointType != null and pointType != ''">point_type,</if>
<if test="holdParams != null">hold_params,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="routeId != null">#{routeId},</if>
@ -68,6 +74,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="speed != null">#{speed},</if>
<if test="startTime != null and startTime != ''">#{startTime},</if>
<if test="turnAngle != null">#{turnAngle},</if>
<if test="pointType != null and pointType != ''">#{pointType},</if>
<if test="holdParams != null">#{holdParams},</if>
</trim>
</insert>
@ -83,6 +91,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="speed != null">speed = #{speed},</if>
<if test="startTime != null and startTime != ''">start_time = #{startTime},</if>
<if test="turnAngle != null">turn_angle = #{turnAngle},</if>
<if test="pointType != null">point_type = #{pointType},</if>
<if test="holdParams != null">hold_params = #{holdParams},</if>
</trim>
where id = #{id}
</update>

31
ruoyi-ui/src/permission.js

@ -28,20 +28,37 @@ router.beforeEach((to, from, next) => {
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 超时保护:若 getInfo/getRouters 长时间不返回则关闭加载条并提示,避免页面一直白屏
const timeoutMs = 15000
const timeoutId = setTimeout(() => {
if (store.getters.roles.length === 0) {
isRelogin.show = false
NProgress.done()
Message.error('获取用户信息超时,请确认后端服务已启动(默认 8080 端口)')
store.dispatch('LogOut').then(() => {
next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
})
}
}, timeoutMs)
const clearTimeoutAndNext = () => {
clearTimeout(timeoutId)
}
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
clearTimeoutAndNext()
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
router.addRoutes(accessRoutes)
next({ ...to, replace: true })
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
clearTimeoutAndNext()
isRelogin.show = false
store.dispatch('LogOut').then(() => {
Message.error(err || '获取用户信息失败')
next({ path: '/' })
})
})
} else {
next()
}

2
ruoyi-ui/src/router/index.js

@ -87,7 +87,7 @@ export const constantRoutes = [
{
path: '',
component: Layout,
redirect: 'index',
redirect: '/selectRoom',
children: [
{
path: 'index',

14
ruoyi-ui/src/store/modules/permission.js

@ -52,9 +52,18 @@ const permission = {
}
}
// 确保路由必有 path,避免 [vue-router] "path" is required 报错(后端菜单 path 为空时)
function ensureRoutePath(route, fallback) {
const id = route.menuId || route.id || route.name || ('menu-' + Math.random().toString(36).slice(2, 9))
if (route.path === undefined || route.path === null || (typeof route.path === 'string' && route.path.trim() === '')) {
route.path = fallback || ('/hidden-' + id)
}
}
// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
return asyncRouterMap.filter(route => {
ensureRoutePath(route)
if (type && route.children) {
route.children = filterChildren(route.children)
}
@ -82,8 +91,9 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach(el => {
el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
childrenMap.forEach((el, idx) => {
ensureRoutePath(el, 'item-' + idx)
el.path = lastRouter ? (lastRouter.path + '/' + el.path).replace(/\/+/g, '/') : el.path
if (el.children && el.children.length && el.component === 'ParentView') {
children = children.concat(filterChildren(el.children, el))
} else {

32
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue

@ -106,7 +106,7 @@
</div>
<!-- 多边形特有选项 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector'">
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'powerZone'">
<div class="menu-title">填充属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>
@ -272,6 +272,24 @@
</div>
</div>
</div>
<!-- 平台图标拖拽到地图的图标特有选项 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon'">
<div class="menu-title">平台图标</div>
<div class="menu-item" @click.stop="handleShowTransformBox">
<span class="menu-icon">📐</span>
<span>显示伸缩框</span>
</div>
<div class="menu-item" @click="handleEditPlatformPosition">
<span class="menu-icon">📍</span>
<span>修改位置</span>
</div>
<div class="menu-item" @click="handleEditPlatformHeading">
<span class="menu-icon">🧭</span>
<span>修改朝向</span>
<span class="menu-value">{{ entityData.heading != null ? entityData.heading + '°' : '0°' }}</span>
</div>
</div>
</div>
</template>
@ -324,6 +342,18 @@ export default {
this.$emit('delete')
},
handleShowTransformBox() {
this.$emit('show-transform-box')
},
handleEditPlatformPosition() {
this.$emit('edit-platform-position')
},
handleEditPlatformHeading() {
this.$emit('edit-platform-heading')
},
toggleColorPicker(property) {
if (this.showColorPickerFor === property) {
this.showColorPickerFor = null

54
ruoyi-ui/src/views/cesiumMap/LocateDialog.vue

@ -61,7 +61,7 @@
<el-option
v-for="item in waypointList"
:key="item.id"
:label="`${item.name} (${item.lng}, ${item.lat})`"
:label="`${item.name} (${degreesToDMS(item.lng)}, ${degreesToDMS(item.lat)})`"
:value="item.id"
/>
</el-select>
@ -70,9 +70,7 @@
<el-form-item label="经度:">
<el-input
v-model="formData.lng"
type="number"
placeholder="例如 116.40"
step="0.000001"
placeholder="例如 116°23'48.64""
clearable
/>
</el-form-item>
@ -80,9 +78,7 @@
<el-form-item label="纬度:">
<el-input
v-model="formData.lat"
type="number"
placeholder="例如 39.90"
step="0.000001"
placeholder="例如 39°54'33.48""
clearable
/>
</el-form-item>
@ -120,8 +116,8 @@ export default {
scenarioId: null,
routeId: null,
waypointId: null,
lng: '116.3974',
lat: '39.9093'
lng: '116°23\'48.64"',
lat: '39°54\'33.48"'
},
scenarioList: [],
routeList: [],
@ -144,13 +140,29 @@ export default {
}
},
methods: {
degreesToDMS(decimalDegrees) {
const degrees = Math.floor(decimalDegrees)
const minutesDecimal = (decimalDegrees - degrees) * 60
const minutes = Math.floor(minutesDecimal)
const seconds = ((minutesDecimal - minutes) * 60).toFixed(2)
return `${degrees}°${minutes}'${seconds}"`
},
dmsToDegrees(dms) {
const match = dms.match(/^(-?\d+)°(\d+)'([\d.]+)"$/)
if (!match) return null
const degrees = parseFloat(match[1])
const minutes = parseFloat(match[2])
const seconds = parseFloat(match[3])
const sign = degrees < 0 ? -1 : 1
return sign * (Math.abs(degrees) + minutes / 60 + seconds / 3600)
},
resetForm() {
this.formData = {
scenarioId: null,
routeId: null,
waypointId: null,
lng: '116.3974',
lat: '39.9093'
lng: '116°23\'48.64"',
lat: '39°54\'33.48"'
}
this.routeList = []
this.waypointList = []
@ -202,8 +214,8 @@ export default {
if (value) {
const waypoint = this.waypointList.find(w => w.id === value)
if (waypoint) {
this.formData.lng = waypoint.lng
this.formData.lat = waypoint.lat
this.formData.lng = this.degreesToDMS(waypoint.lng)
this.formData.lat = this.degreesToDMS(waypoint.lat)
}
}
},
@ -216,19 +228,27 @@ export default {
handleConfirm() {
const { lng, lat } = this.formData
if (!lng || !lat || isNaN(parseFloat(lng)) || isNaN(parseFloat(lat))) {
if (!lng || !lat) {
this.$message.error('请输入有效的经度和纬度!')
return
}
if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
const lngDegrees = this.dmsToDegrees(lng)
const latDegrees = this.dmsToDegrees(lat)
if (lngDegrees === null || latDegrees === null || isNaN(lngDegrees) || isNaN(latDegrees)) {
this.$message.error('请输入有效的度分秒格式!格式:116°23\'48.64"')
return
}
if (lngDegrees < -180 || lngDegrees > 180 || latDegrees < -90 || latDegrees > 90) {
this.$message.error('经纬度超出有效范围!')
return
}
this.$emit('confirm', {
lng: parseFloat(lng),
lat: parseFloat(lat)
lng: lngDegrees,
lat: latDegrees
})
this.$emit('update:visible', false)
}

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

File diff suppressed because it is too large

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

@ -164,8 +164,10 @@
<div
v-for="platform in airPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
class="platform-item platform-item-draggable"
draggable="true"
@click="handlePlatformItemClick(platform, $event)"
@dragstart="onPlatformDragStart($event, platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<img v-if="isImg(platform.imageUrl)"
@ -195,8 +197,10 @@
<div
v-for="platform in seaPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
class="platform-item platform-item-draggable"
draggable="true"
@click="handlePlatformItemClick(platform, $event)"
@dragstart="onPlatformDragStart($event, platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<img v-if="isImg(platform.imageUrl)"
@ -221,8 +225,10 @@
<div
v-for="platform in groundPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
class="platform-item platform-item-draggable"
draggable="true"
@click="handlePlatformItemClick(platform, $event)"
@dragstart="onPlatformDragStart($event, platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<img v-if="isImg(platform.imageUrl)"
@ -313,7 +319,8 @@ export default {
return {
activePlatformTab: 'air',
expandedPlans: [], //
expandedRoutes: [] // 线
expandedRoutes: [], // 线
platformJustDragged: false //
}
},
watch: {
@ -504,6 +511,35 @@ export default {
handleOpenPlatformDialog(platform) {
this.$emit('open-platform-dialog', platform)
},
/** 平台项点击:若刚拖拽过则不打开弹窗,避免误触 */
handlePlatformItemClick(platform, ev) {
if (this.platformJustDragged) {
this.platformJustDragged = false
return
}
this.handleOpenPlatformDialog(platform)
},
/** 拖拽平台图标到地图时传递平台数据 */
onPlatformDragStart(ev, platform) {
this.platformJustDragged = true
setTimeout(() => { this.platformJustDragged = false }, 300)
try {
ev.dataTransfer.setData('application/json', JSON.stringify({
id: platform.id,
name: platform.name,
type: platform.type,
imageUrl: platform.imageUrl,
iconUrl: platform.iconUrl,
icon: platform.icon,
color: platform.color
}))
ev.dataTransfer.effectAllowed = 'copy'
} catch (e) {
console.warn('Platform drag start failed', e)
}
}
}
}
@ -829,6 +865,14 @@ export default {
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.platform-item-draggable {
cursor: grab;
}
.platform-item-draggable:active {
cursor: grabbing;
}
.platform-icon {
width: 40px;
height: 40px;

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

@ -234,16 +234,22 @@
</div>
<div
class="info-box"
class="info-box combat-time-box"
:class="{ 'clickable': true }"
@click="$emit('set-k-time')"
>
<i class="el-icon-timer info-icon"></i>
<div class="info-content">
<div class="info-label">{{ $t('topHeader.info.combatTime') }}</div>
<div class="info-value">
{{ combatTime }}
<i v-if="canSetKTime" class="el-icon-edit-outline set-k-hint" title="点击设定或修改 K 时(房主/管理员可随时更改)"></i>
<div class="info-content combat-time-content">
<div v-if="kTimeDisplay" class="combat-k-time">
<span class="k-time-label">K </span>
<span class="k-time-value">{{ kTimeDisplay }}</span>
</div>
<div class="combat-time-row">
<span class="info-label">{{ $t('topHeader.info.combatTime') }}</span>
<span class="combat-time-value">
{{ combatTime }}
<i v-if="canSetKTime" class="el-icon-edit-outline set-k-hint" title="点击设定或修改 K 时(房主/管理员可随时更改)"></i>
</span>
</div>
</div>
</div>
@ -264,12 +270,7 @@
</div>
</div>
<!-- 威力区弹窗 -->
<power-zone-dialog
v-model="powerZoneDialogVisible"
:power-zone="currentPowerZone"
@save="savePowerZone"
/>
<!-- 比例尺弹窗 -->
<scale-dialog
@ -291,14 +292,12 @@
</template>
<script>
import PowerZoneDialog from '../dialogs/PowerZoneDialog'
import ScaleDialog from '../dialogs/ScaleDialog'
import ExternalParamsDialog from '../dialogs/ExternalParamsDialog'
export default {
name: 'TopHeader',
components: {
PowerZoneDialog,
ScaleDialog,
ExternalParamsDialog
},
@ -315,6 +314,11 @@ export default {
type: String,
default: 'K+01:30:45'
},
/** 格式化的 K 时基准时刻,如 "2025-02-06 08:00:00" */
kTimeDisplay: {
type: String,
default: ''
},
astroTime: {
type: String,
default: ''
@ -347,8 +351,6 @@ export default {
data() {
return {
activeTopNav: 'file',
powerZoneDialogVisible: false,
currentPowerZone: {},
scaleDialogVisible: false,
currentScale: {},
externalParamsDialogVisible: false,
@ -500,8 +502,7 @@ export default {
//
powerZone() {
this.powerZoneDialogVisible = true
this.currentPowerZone = {}
this.$emit('start-power-zone-drawing')
},
threatZone() {
@ -877,9 +878,43 @@ export default {
box-shadow: 0 4px 12px rgba(0, 138, 255, 0.1);
}
.combat-info-group .info-box:nth-child(3) .info-value {
color: #409EFF;
/* 作战时间区域:显示 K 时 + 当前作战时间 */
.combat-time-box {
min-width: 180px;
}
.combat-time-content {
gap: 6px;
}
.combat-k-time {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #666;
}
.combat-k-time .k-time-label {
color: #008aff;
font-weight: 600;
flex-shrink: 0;
}
.combat-k-time .k-time-value {
color: #333;
font-weight: 500;
}
.combat-time-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.combat-time-row .info-label {
flex-shrink: 0;
}
.combat-time-value {
font-size: 14px;
color: #008aff;
font-weight: 700;
letter-spacing: 0.5px;
}
.combat-info-group .info-box:nth-child(4) .info-value {
@ -897,6 +932,12 @@ export default {
color: #008aff;
vertical-align: middle;
}
.combat-time-box .set-k-hint {
opacity: 0.85;
}
.combat-time-box .set-k-hint:hover {
opacity: 1;
}
.info-icon {
font-size: 20px;

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

File diff suppressed because it is too large

32
ruoyi-ui/src/views/dialogs/PowerZoneDialog.vue

@ -207,14 +207,30 @@ export default {
}
},
methods: {
degreesToDMS(decimalDegrees) {
const degrees = Math.floor(decimalDegrees)
const minutesDecimal = (decimalDegrees - degrees) * 60
const minutes = Math.floor(minutesDecimal)
const seconds = ((minutesDecimal - minutes) * 60).toFixed(2)
return `${degrees}°${minutes}'${seconds}"`
},
dmsToDegrees(dms) {
const match = dms.match(/^(-?\d+)°(\d+)'([\d.]+)"$/)
if (!match) return null
const degrees = parseFloat(match[1])
const minutes = parseFloat(match[2])
const seconds = parseFloat(match[3])
const sign = degrees < 0 ? -1 : 1
return sign * (Math.abs(degrees) + minutes / 60 + seconds / 3600)
},
initFormData() {
this.formData = {
name: this.powerZone.name || '',
font: this.powerZone.font || 'Microsoft YaHei',
shapeType: this.powerZone.shapeType || 'circle',
location: {
lat: this.powerZone.lat || '',
lng: this.powerZone.lng || ''
lat: this.powerZone.lat ? this.degreesToDMS(this.powerZone.lat) : '',
lng: this.powerZone.lng ? this.degreesToDMS(this.powerZone.lng) : ''
},
radius: this.powerZone.radius || 0,
angle: this.powerZone.angle || 0,
@ -236,11 +252,19 @@ export default {
savePowerZone() {
this.$refs.formRef.validate((valid) => {
if (valid) {
const latDegrees = this.dmsToDegrees(this.formData.location.lat);
const lngDegrees = this.dmsToDegrees(this.formData.location.lng);
if (latDegrees === null || lngDegrees === null) {
this.$message.error('请输入有效的度分秒格式!格式:39°54\'33.48"');
return;
}
this.$emit('save', {
...this.powerZone,
...this.formData,
lat: this.formData.location.lat,
lng: this.formData.location.lng
lat: latDegrees,
lng: lngDegrees
});
this.closeDialog();
}

153
ruoyi-ui/src/views/dialogs/RadiusDialog.vue

@ -0,0 +1,153 @@
<template>
<div v-if="visible" class="radius-dialog">
<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="radius">
<el-input-number
v-model="formData.radius"
:min="0.1"
:precision="2"
placeholder="请输入半径"
style="width: 100%;"
></el-input-number>
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-radio-group v-model="formData.unit">
<el-radio-button label="km">千米</el-radio-button>
<el-radio-button label="m"></el-radio-button>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<div class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirmRadius">确定</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RadiusDialog',
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
formData: {
radius: 50,
unit: 'km'
},
rules: {
radius: [
{ required: true, message: '请输入半径', trigger: 'blur' }
]
}
}
},
methods: {
closeDialog() {
this.$emit('update:visible', false)
},
confirmRadius() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$emit('confirm', {
radius: this.formData.radius,
unit: this.formData.unit
})
this.closeDialog()
}
})
}
}
}
</script>
<style scoped>
.radius-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.dialog-content {
position: relative;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 400px;
animation: dialog-fade-in 0.3s ease;
pointer-events: auto;
}
@keyframes dialog-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.close-btn {
font-size: 20px;
color: #999;
cursor: pointer;
transition: color 0.3s;
}
.close-btn:hover {
color: #666;
}
.dialog-body {
padding: 20px;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 16px 20px;
border-top: 1px solid #e8e8e8;
gap: 10px;
}
</style>

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

@ -34,7 +34,7 @@
></el-input-number>
</el-form-item>
<el-form-item label="转弯坡度" prop="turnAngle">
<el-form-item v-if="!isHoldWaypoint" label="转弯坡度" prop="turnAngle">
<el-input-number
v-model="formData.turnAngle"
controls-position="right"
@ -46,6 +46,34 @@
首尾航点坡度已锁定为 0不可编辑
</div>
</el-form-item>
<template v-if="isHoldWaypoint">
<el-form-item label="盘旋类型">
<el-radio-group v-model="formData.pointType">
<el-radio label="hold_circle">圆形</el-radio>
<el-radio label="hold_ellipse">椭圆</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.pointType === 'hold_circle'" label="半径(米)">
<el-input-number v-model="formData.holdRadius" :min="100" :max="50000" style="width:100%" />
</el-form-item>
<template v-if="formData.pointType === 'hold_ellipse'">
<el-form-item label="长半轴(米)">
<el-input-number v-model="formData.holdSemiMajor" :min="100" :max="50000" style="width:100%" />
</el-form-item>
<el-form-item label="短半轴(米)">
<el-input-number v-model="formData.holdSemiMinor" :min="50" :max="50000" style="width:100%" />
</el-form-item>
<el-form-item label="长轴方位(度)">
<el-input-number v-model="formData.holdHeadingDeg" :min="-180" :max="180" style="width:100%" />
</el-form-item>
</template>
<el-form-item label="盘旋方向">
<el-radio-group v-model="formData.holdClockwise">
<el-radio :label="true">顺时针</el-radio>
<el-radio :label="false">逆时针</el-radio>
</el-radio-group>
</el-form-item>
</template>
<el-form-item label="相对 K 时(分钟)" prop="minutesFromK">
<el-input-number
@ -103,7 +131,13 @@ export default {
minutesFromK: 0,
currentIndex: -1,
totalPoints: 0,
isBankDisabled: false
isBankDisabled: false,
pointType: 'normal',
holdRadius: 500,
holdSemiMajor: 500,
holdSemiMinor: 300,
holdHeadingDeg: 0,
holdClockwise: true
},
rules: {
name: [
@ -124,6 +158,12 @@ export default {
}
};
},
computed: {
isHoldWaypoint() {
const t = (this.waypoint && (this.waypoint.pointType || this.waypoint.point_type)) || (this.formData && this.formData.pointType) || 'normal';
return t === 'hold_circle' || t === 'hold_ellipse';
}
},
watch: {
value(newVal) {
if (newVal && this.waypoint) {
@ -142,6 +182,19 @@ export default {
const total = this.waypoint.totalPoints || 0;
const locked = (index === 0) || (total > 0 && index === total - 1);
const pt = (this.waypoint.pointType || this.waypoint.point_type) || 'normal';
let holdRadius = 500, holdSemiMajor = 500, holdSemiMinor = 300, holdHeadingDeg = 0, holdClockwise = true;
try {
const raw = this.waypoint.holdParams || this.waypoint.hold_params;
if (raw) {
const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
holdRadius = p.radius != null ? p.radius : 500;
holdSemiMajor = p.semiMajor ?? p.semiMajorAxis ?? 500;
holdSemiMinor = p.semiMinor ?? p.semiMinorAxis ?? 300;
holdHeadingDeg = p.headingDeg ?? 0;
holdClockwise = p.clockwise !== false;
}
} catch (e) {}
this.formData = {
name: this.waypoint.name || '',
alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0,
@ -150,7 +203,13 @@ export default {
currentIndex: index,
totalPoints: total,
isBankDisabled: locked,
turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0)
turnAngle: locked ? 0 : (Number(this.waypoint.turnAngle) || 0),
pointType: pt,
holdRadius,
holdSemiMajor,
holdSemiMinor,
holdHeadingDeg,
holdClockwise
};
this.$nextTick(() => {
@ -167,11 +226,19 @@ export default {
if (valid) {
const { minutesFromK, ...rest } = this.formData;
const startTimeStr = this.minutesToStartTime(minutesFromK);
this.$emit('save', {
...this.waypoint,
...rest,
startTime: startTimeStr
});
const payload = { ...this.waypoint, ...rest, startTime: startTimeStr };
if (this.formData.pointType && this.formData.pointType !== 'normal') {
payload.pointType = this.formData.pointType;
payload.holdParams = this.formData.pointType === 'hold_circle'
? JSON.stringify({ radius: this.formData.holdRadius, clockwise: this.formData.holdClockwise })
: JSON.stringify({
semiMajor: this.formData.holdSemiMajor,
semiMinor: this.formData.holdSemiMinor,
headingDeg: this.formData.holdHeadingDeg,
clockwise: this.formData.holdClockwise
});
}
this.$emit('save', payload);
this.closeDialog();
}
});

54
ruoyi-ui/src/views/system/waypoints/index.vue

@ -131,8 +131,16 @@
<el-table-column label="所属航线ID (关联 routes.id)" align="center" prop="routeId" />
<el-table-column label="航点名称 (如: WP1)" align="center" prop="name" />
<el-table-column label="航点顺序 (从1开始递增)" align="center" prop="seq" />
<el-table-column label="纬度" align="center" prop="lat" />
<el-table-column label="经度" align="center" prop="lng" />
<el-table-column label="纬度" align="center" prop="lat">
<template slot-scope="scope">
{{ degreesToDMS(scope.row.lat) }}
</template>
</el-table-column>
<el-table-column label="经度" align="center" prop="lng">
<template slot-scope="scope">
{{ degreesToDMS(scope.row.lng) }}
</template>
</el-table-column>
<el-table-column label="高度 (米)" align="center" prop="alt" />
<el-table-column label="速度 (km/h)" align="center" prop="speed" />
<el-table-column label="起始时间 (如: K+00:40:00)" align="center" prop="startTime" />
@ -178,10 +186,10 @@
<el-input v-model="form.seq" placeholder="请输入航点顺序 (从1开始递增)" />
</el-form-item>
<el-form-item label="纬度" prop="lat">
<el-input v-model="form.lat" placeholder="请输入纬度" />
<el-input v-model="form.lat" placeholder="请输入纬度(度分秒格式,如:39°54'33.48"" />
</el-form-item>
<el-form-item label="经度" prop="lng">
<el-input v-model="form.lng" placeholder="请输入经度" />
<el-input v-model="form.lng" placeholder="请输入经度(度分秒格式,如:116°23'48.64"" />
</el-form-item>
<el-form-item label="高度 (米)" prop="alt">
<el-input v-model="form.alt" placeholder="请输入高度 (米)" />
@ -278,6 +286,27 @@ export default {
this.getList()
},
methods: {
degreesToDMS(decimalDegrees) {
if (!decimalDegrees) return ''
const degrees = Math.floor(decimalDegrees)
const minutesDecimal = (decimalDegrees - degrees) * 60
const minutes = Math.floor(minutesDecimal)
const seconds = ((minutesDecimal - minutes) * 60).toFixed(2)
return `${degrees}°${minutes}'${seconds}"`
},
dmsToDegrees(dms) {
if (!dms) return null
const match = dms.match(/^(-?\d+)°(\d+)'([\d.]+)"$/)
if (!match) {
const num = parseFloat(dms)
return isNaN(num) ? null : num
}
const degrees = parseFloat(match[1])
const minutes = parseFloat(match[2])
const seconds = parseFloat(match[3])
const sign = degrees < 0 ? -1 : 1
return sign * (Math.abs(degrees) + minutes / 60 + seconds / 3600)
},
/** 查询航线具体航点明细列表 */
getList() {
this.loading = true
@ -336,6 +365,8 @@ export default {
const id = row.id || this.ids
getWaypoints(id).then(response => {
this.form = response.data
this.form.lat = this.degreesToDMS(this.form.lat)
this.form.lng = this.degreesToDMS(this.form.lng)
this.open = true
this.title = "修改航线具体航点明细"
})
@ -344,14 +375,23 @@ export default {
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
updateWaypoints(this.form).then(response => {
const formData = { ...this.form }
formData.lat = this.dmsToDegrees(formData.lat)
formData.lng = this.dmsToDegrees(formData.lng)
if (formData.lat === null || formData.lng === null) {
this.$modal.msgError("请输入有效的度分秒格式!格式:39°54'33.48\"")
return
}
if (formData.id != null) {
updateWaypoints(formData).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getList()
})
} else {
addWaypoints(this.form).then(response => {
addWaypoints(formData).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getList()

1
ruoyi-ui/vue.config.js

@ -22,7 +22,6 @@ const port = process.env.port || process.env.npm_config_port || 80 // 端口
const cesiumSource = 'node_modules/cesium/Build/Cesium'
module.exports = {
// 部署生产环境和开发环境下的URL。
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
outputDir: 'dist',

Loading…
Cancel
Save