Browse Source

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

# Conflicts:
#	ruoyi-ui/src/views/childRoom/index.vue
master
menghao 2 months ago
parent
commit
43a2b63507
  1. 2
      ruoyi-admin/src/main/resources/application-druid.yml
  2. 11
      ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java
  3. 28
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/RoutesServiceImpl.java
  4. 2
      ruoyi-ui/.env.development
  5. 2
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  6. 222
      ruoyi-ui/src/views/cesiumMap/index.vue
  7. 142
      ruoyi-ui/src/views/childRoom/index.vue
  8. 646
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
  9. 2
      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:
# 从数据源开关/默认关闭

11
ruoyi-system/src/main/java/com/ruoyi/system/domain/Routes.java

@ -47,6 +47,9 @@ public class Routes extends BaseEntity {
private List<RouteWaypoints> waypoints;
/** 关联的平台信息(仅用于 API 返回,非数据库字段) */
private java.util.Map<String, Object> platform;
public void setId(Long id) {
this.id = id;
}
@ -95,6 +98,14 @@ public class Routes extends BaseEntity {
this.waypoints = waypoints;
}
public java.util.Map<String, Object> getPlatform() {
return platform;
}
public void setPlatform(java.util.Map<String, Object> platform) {
this.platform = platform;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)

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

@ -1,8 +1,12 @@
package com.ruoyi.system.service.impl;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.PlatformLib;
import com.ruoyi.system.domain.RouteWaypoints;
import com.ruoyi.system.service.IPlatformLibService;
import com.ruoyi.system.service.IRouteWaypointsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -26,6 +30,9 @@ public class RoutesServiceImpl implements IRoutesService
@Autowired
private IRouteWaypointsService routeWaypointsService;
@Autowired
private IPlatformLibService platformLibService;
/**
* 查询实体部署与航线
*
@ -45,6 +52,17 @@ public class RoutesServiceImpl implements IRoutesService
// 把查出来的航点列表塞进 routes 对象的 waypoints 属性里
routes.setWaypoints(wpList);
// 如有 platformId,查询平台信息并填充
if (routes.getPlatformId() != null) {
PlatformLib lib = platformLibService.selectPlatformLibById(routes.getPlatformId());
if (lib != null) {
Map<String, Object> platform = new HashMap<>();
platform.put("id", lib.getId());
platform.put("name", lib.getName());
platform.put("iconUrl", lib.getIconUrl());
routes.setPlatform(platform);
}
}
}
return routes;
}
@ -66,6 +84,16 @@ public class RoutesServiceImpl implements IRoutesService
queryWp.setRouteId(r.getId());
List<RouteWaypoints> wpList = routeWaypointsService.selectRouteWaypointsList(queryWp);
r.setWaypoints(wpList);
if (r.getPlatformId() != null) {
PlatformLib lib = platformLibService.selectPlatformLibById(r.getPlatformId());
if (lib != null) {
Map<String, Object> platform = new HashMap<>();
platform.put("id", lib.getId());
platform.put("name", lib.getName());
platform.put("iconUrl", lib.getIconUrl());
r.setPlatform(platform);
}
}
}
return list;
}

2
ruoyi-ui/.env.development

@ -8,7 +8,7 @@ ENV = 'development'
VUE_APP_BASE_API = '/dev-api'
# 访问地址(绕过 /dev-api 代理,用于解决静态资源/图片访问 401 认证问题)
VUE_APP_BACKEND_URL = 'http://192.168.50.145:8080'
VUE_APP_BACKEND_URL = 'http://192.168.50.30:8080'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

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

@ -8,7 +8,7 @@
</div>
<!-- 线段特有选项 -->
<div class="menu-section" v-if="entityData.type === 'line'">
<div class="menu-section" v-if="entityData.type === 'line' && !entityData.routeId">
<div class="menu-title">线段属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>

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

@ -338,8 +338,15 @@ export default {
}, 200);
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
//线
renderRouteWaypoints(waypoints, routeId = 'default') {
// URL RouteEditDialog / RightPanel
formatPlatformIconUrl(url) {
if (!url) return '';
const cleanPath = (url + '').replace(/\/+/g, '/');
const backendUrl = process.env.VUE_APP_BACKEND_URL || '';
return backendUrl + cleanPath;
},
//线style waypoint { pixelSize, color, outlineColor, outlineWidth }line { style, width, color, gapColor, dashLength }
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
if (!waypoints || waypoints.length < 1) return;
// 线
const lineId = `route-line-${routeId}`;
@ -360,6 +367,12 @@ export default {
this.viewer.entities.remove(existingArc);
}
});
const wpStyle = (style && style.waypoint) ? style.waypoint : {};
const lineStyle = (style && style.line) ? style.line : {};
const pixelSize = wpStyle.pixelSize != null ? wpStyle.pixelSize : 7;
const wpColor = wpStyle.color || '#ffffff';
const wpOutline = wpStyle.outlineColor || '#0078FF';
const wpOutlineW = wpStyle.outlineWidth != null ? wpStyle.outlineWidth : 2;
//
const originalPositions = [];
waypoints.forEach((wp, index) => {
@ -379,16 +392,16 @@ export default {
dbId: wp.id,
},
point: {
pixelSize: 10,
color: Cesium.Color.WHITE,
outlineColor: Cesium.Color.fromCssColorString('#0078FF'),
outlineWidth: 3,
pixelSize: pixelSize,
color: Cesium.Color.fromCssColorString(wpColor),
outlineColor: Cesium.Color.fromCssColorString(wpOutline),
outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
label: {
text: wp.name || `WP${index + 1}`,
font: '12px MicroSoft YaHei',
pixelOffset: new Cesium.Cartesian2(0, -20),
font: `${Math.max(9, pixelSize + 2)}px MicroSoft YaHei`,
pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
@ -396,6 +409,27 @@ export default {
}
});
});
// 线
const iconUrl = (platform && platform.imageUrl) || (platform && platform.iconUrl);
if (iconUrl && originalPositions.length > 0) {
const platformBillboardId = `route-platform-${routeId}`;
const fullUrl = this.formatPlatformIconUrl(iconUrl);
this.viewer.entities.add({
id: platformBillboardId,
name: (platform && platform.name) || '平台',
position: originalPositions[0],
properties: { routeId: routeId },
billboard: {
image: fullUrl,
width: 144,
height: 144,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
scaleByDistance: new Cesium.NearFarScalar(500, 2.0, 200000, 0.4),
translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6)
}
});
}
// 线
if (waypoints.length > 1) {
let finalPathPositions = [];
@ -408,16 +442,17 @@ export default {
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
}
});
this.viewer.entities.add({
id: `arc-line-${routeId}-${i}`,
polyline: {
positions: arcPoints,
width: 8,
material: Cesium.Color.RED,
clampToGround: true,
zIndex: 20
},
properties: {routeId: routeId}
});
console.log(`>>> 航点 ${waypoints[i].name} 已渲染红色转弯弧,半径: ${radius}`);
}
if (i === 0 || i === waypoints.length - 1 || radius <= 0) {
@ -430,24 +465,32 @@ export default {
finalPathPositions.push(...arcPoints);
}
}
const lineWidth = lineStyle.width != null ? lineStyle.width : 4;
const lineColor = lineStyle.color || '#ffffff';
const gapColor = lineStyle.gapColor != null ? lineStyle.gapColor : '#000000';
const dashLen = lineStyle.dashLength != null ? lineStyle.dashLength : 20;
const useDash = (lineStyle.style || 'dash') === 'dash';
const lineMaterial = useDash
? new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString(lineColor),
gapColor: Cesium.Color.fromCssColorString(gapColor),
dashLength: dashLen
})
: Cesium.Color.fromCssColorString(lineColor);
// 线 Polyline
const routeEntity = this.viewer.entities.add({
id: lineId,
polyline: {
positions: finalPathPositions,
width: 4,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.WHITE,
gapColor: Cesium.Color.BLACK,
dashLength: 20.0
}),
width: lineWidth,
material: lineMaterial,
clampToGround: true,
zIndex: 1
},
properties: {isMissionRouteLine: true, routeId: routeId}
});
if (this.allEntities) {
this.allEntities.push({id: lineId, entity: routeEntity, type: 'line'});
this.allEntities.push({id: lineId, entity: routeEntity, type: 'route'});
}
}
},
@ -494,17 +537,17 @@ export default {
const entityList = this.viewer.entities.values;
for (let i = entityList.length - 1; i >= 0; i--) {
const entity = entityList[i];
// entity routeId
if (entity.properties && entity.properties.routeId) {
// Cesium getValue()
const id = entity.properties.routeId.getValue();
if (id === routeId) {
this.viewer.entities.remove(entity);
}
let shouldRemove = false;
// id
if (entity.id === `route-platform-${routeId}`) {
shouldRemove = true;
} else if (entity.properties && entity.properties.routeId) {
const id = entity.properties.routeId.getValue && entity.properties.routeId.getValue();
if (id === routeId) shouldRemove = true;
}
if (shouldRemove) this.viewer.entities.remove(entity);
}
// allEntities
this.allEntities = this.allEntities.filter(item => item.id !== routeId);
this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}`);
},
checkCesiumLoaded() {
if (typeof Cesium === 'undefined') {
@ -590,6 +633,11 @@ export default {
console.log(`>>> [地图触发] 点击了点 ${dbId}, 属于航线 ${routeId}`);
// 线 ID
this.$emit('open-waypoint-dialog', dbId, routeId);
} else if (props && props.isMissionRouteLine) {
const routeId = props.routeId;
console.log(`>>> [地图触发] 点击了航线 ${routeId}`);
// 线
this.$emit('open-route-dialog', routeId);
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
@ -629,7 +677,7 @@ export default {
}
}
}
if (entityData) {
if (entityData && entityData.type !== 'route') {
//
this.contextMenu = {
visible: true,
@ -673,21 +721,36 @@ export default {
}
// 线
if (entityData && entityData.type === 'line') {
const length = this.calculateLineLength(entityData.positions)
//
const bearingType = entityData.bearingType || 'true';
const bearing = bearingType === 'magnetic'
? this.calculateMagneticBearing(entityData.positions)
: this.calculateTrueBearing(entityData.positions);
//
this.hoverTooltip = {
visible: true,
content: `长度:${length.toFixed(2)}\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}${bearing.toFixed(2)}°`,
position: {
x: movement.endPosition.x + 10,
y: movement.endPosition.y - 10
// 线
const segmentIndex = this.findClosestSegment(entityData.positions, movement.endPosition);
if (segmentIndex !== -1) {
// segmentIndex+1
let cumulativeLength = 0;
for (let i = 0; i < segmentIndex + 1; i++) {
if (i < entityData.positions.length - 1) {
cumulativeLength += Cesium.Cartesian3.distance(entityData.positions[i], entityData.positions[i + 1]);
}
}
};
//
const currentSegmentPositions = [entityData.positions[segmentIndex], entityData.positions[segmentIndex + 1]];
//
const bearingType = entityData.bearingType || 'true';
const bearing = bearingType === 'magnetic'
? this.calculateMagneticBearing(currentSegmentPositions)
: this.calculateTrueBearing(currentSegmentPositions);
//
this.hoverTooltip = {
visible: true,
content: `累计长度:${cumulativeLength.toFixed(2)}\n${bearingType === 'magnetic' ? '磁方位' : '真方位'}${bearing.toFixed(2)}°`,
position: {
x: movement.endPosition.x + 10,
y: movement.endPosition.y - 10
}
};
} else {
//
this.hoverTooltip.visible = false;
}
} else {
// 线
this.hoverTooltip.visible = false;
@ -3016,6 +3079,67 @@ export default {
this.viewer.destroy()
this.viewer = null
}
},
// 线
findClosestSegment(positions, mousePosition) {
if (!positions || positions.length < 2) {
return -1;
}
let closestDistance = Number.MAX_VALUE;
let closestSegmentIndex = -1;
// 线
for (let i = 0; i < positions.length - 1; i++) {
const startPoint = positions[i];
const endPoint = positions[i + 1];
// 线
const distance = this.distanceToSegment(mousePosition, startPoint, endPoint);
//
if (distance < closestDistance) {
closestDistance = distance;
closestSegmentIndex = i;
}
}
//
const threshold = 10; //
return closestDistance < threshold ? closestSegmentIndex : -1;
},
// 线
distanceToSegment(mousePosition, startPoint, endPoint) {
// 3D
const startScreen = this.viewer.scene.cartesianToCanvasCoordinates(startPoint);
const endScreen = this.viewer.scene.cartesianToCanvasCoordinates(endPoint);
if (!startScreen || !endScreen) {
return Number.MAX_VALUE;
}
// 线
const lineVec = [endScreen.x - startScreen.x, endScreen.y - startScreen.y];
// 线
const mouseVec = [mousePosition.x - startScreen.x, mousePosition.y - startScreen.y];
// 线
const lineLengthSquared = lineVec[0] * lineVec[0] + lineVec[1] * lineVec[1];
if (lineLengthSquared === 0) {
// 线0线
return Math.sqrt(mouseVec[0] * mouseVec[0] + mouseVec[1] * mouseVec[1]);
}
// 线t
const t = Math.max(0, Math.min(1, (mouseVec[0] * lineVec[0] + mouseVec[1] * lineVec[1]) / lineLengthSquared));
//
const projection = [startScreen.x + t * lineVec[0], startScreen.y + t * lineVec[1]];
//
const distanceVec = [mousePosition.x - projection[0], mousePosition.y - projection[1]];
return Math.sqrt(distanceVec[0] * distanceVec[0] + distanceVec[1] * distanceVec[1]);
}
}
}

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

@ -7,7 +7,8 @@
<cesiumMap ref="cesiumMap" :drawDomClick="drawDom || airspaceDrawDom"
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
@draw-complete="handleMapDrawComplete"
@open-waypoint-dialog="handleOpenWaypointEdit" />
@open-waypoint-dialog="handleOpenWaypointEdit"
@open-route-dialog="handleOpenRouteEdit" />
<div class="map-overlay-text">
<i class="el-icon-location-outline text-3xl mb-2 block"></i>
<p>二维GIS地图区域</p>
@ -297,7 +298,7 @@ import RightPanel from './RightPanel'
import BottomLeftPanel from './BottomLeftPanel'
import TopHeader from './TopHeader'
import { listScenario,addScenario,delScenario} from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes,delRoutes } from "@/api/system/routes";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes } from "@/api/system/routes";
import { updateWaypoints } from "@/api/system/waypoints";
import { listLib,addLib,delLib} from "@/api/system/lib";
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue";
@ -570,6 +571,34 @@ export default {
}).catch(() => {
});
},
// 线
async handleOpenRouteEdit(routeId) {
console.log(`>>> [父组件接收] 航线 ID: ${routeId}`);
try {
const response = await getRoutes(routeId);
if (response.code === 200 && response.data) {
const routeData = response.data;
// 线
const route = {
id: routeData.id,
name: routeData.callSign,
scenarioId: routeData.scenarioId,
platformId: routeData.platformId,
platform: routeData.platform,
attributes: routeData.attributes,
points: routeData.waypoints ? routeData.waypoints.length : 0,
waypoints: routeData.waypoints || []
};
// 线
this.openRouteDialog(route);
}
} catch (error) {
this.$message.error('获取航线数据失败');
console.error('获取航线数据失败:', error);
}
},
// 线
showOnlineMembersDialog() {
this.showOnlineMembers = true;
@ -643,25 +672,80 @@ export default {
//
this.showPlatformDialog = false;
},
/** 从航线 attributes JSON 解析出地图渲染用的样式对象 */
parseRouteStyle(attributes) {
if (attributes == null || attributes === '') return null;
try {
const attrs = typeof attributes === 'string' ? JSON.parse(attributes) : attributes;
const wp = attrs.waypointStyle || {};
const ln = attrs.lineStyle || {};
if (!wp.pixelSize && !wp.color && !ln.width && !ln.color) return null;
return {
waypoint: {
pixelSize: wp.pixelSize,
color: wp.color,
outlineColor: wp.outlineColor,
outlineWidth: wp.outlineWidth
},
line: {
style: ln.style,
width: ln.width,
color: ln.color,
gapColor: ln.gapColor,
dashLength: ln.dashLength
}
};
} catch (_) {
return null;
}
},
// 线
openRouteDialog(route) {
this.selectedRoute = route;
this.showRouteDialog = true;
},
// 线
updateRoute(updatedRoute) {
async updateRoute(updatedRoute) {
const index = this.routes.findIndex(r => r.id === updatedRoute.id);
if (index !== -1) {
// 使 splice
const newRouteData = {...this.routes[index], ...updatedRoute};
this.routes.splice(index, 1, newRouteData);
// 线
if (this.selectedRouteDetails && this.selectedRouteId === updatedRoute.id) {
this.selectedRouteDetails.name = updatedRoute.name;
if (index === -1) return;
try {
const apiData = {
id: updatedRoute.id,
callSign: updatedRoute.name,
platformId: updatedRoute.platformId || null,
attributes: updatedRoute.attributes != null ? updatedRoute.attributes : undefined
};
const res = await updateRoutes(apiData);
if (res.code === 200) {
const newRouteData = {
...this.routes[index],
name: updatedRoute.name,
platformId: updatedRoute.platformId,
platform: updatedRoute.platform,
attributes: updatedRoute.attributes
};
this.routes.splice(index, 1, newRouteData);
if (this.selectedRouteDetails && this.selectedRouteId === updatedRoute.id) {
this.selectedRouteDetails.name = updatedRoute.name;
this.selectedRouteDetails.platformId = updatedRoute.platformId;
this.selectedRouteDetails.platform = updatedRoute.platform;
this.selectedRouteDetails.attributes = updatedRoute.attributes;
}
this.$message.success('航线更新成功');
const routeStyle = updatedRoute.routeStyle || this.parseRouteStyle(updatedRoute.attributes);
if (this.$refs.cesiumMap && this.activeRouteIds.includes(updatedRoute.id)) {
const route = this.routes.find(r => r.id === updatedRoute.id);
if (route && route.waypoints && route.waypoints.length > 0) {
this.$refs.cesiumMap.removeRouteById(updatedRoute.id);
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, updatedRoute.id, route.platformId, route.platform, routeStyle);
}
}
} else {
this.$message.error(res.msg || '航线更新失败');
}
this.$message.success('航线名称更新成功');
} catch (error) {
this.$message.error('航线更新失败');
console.error('航线更新失败:', error);
}
},
// 线
@ -721,12 +805,15 @@ export default {
routes: []
}));
}
//线
const routeRes = await listRoutes({});
// 线
const routeRes = await listRoutes({ pageNum: 1, pageSize: 9999 });
if (routeRes.code === 200) {
const allRoutes = routeRes.rows.map(item => ({
id: item.id,
name: item.callSign,
platformId: item.platformId,
platform: item.platform,
attributes: item.attributes,
points: item.waypoints ? item.waypoints.length : 0,
waypoints: item.waypoints || [],
conflict: false,
@ -747,7 +834,7 @@ export default {
if (route && route.waypoints && route.waypoints.length > 0) {
if (this.$refs.cesiumMap) {
this.$refs.cesiumMap.removeRouteById(id);
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, id);
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
}
});
@ -840,16 +927,13 @@ export default {
// clearAllWaypoints
}
// 5. 线
this.$nextTick(() => {
if (this.$refs.cesiumMap) {
// 线线
this.$refs.cesiumMap.renderRouteWaypoints(savedRoute.waypoints, newRouteId);
}
this.$message.success('航线及其航点已成功保存并同步');
});
this.getList();
// 5. 线getList activeRouteIds 线
await this.getList();
const routeFromList = this.routes.find(r => r.id === newRouteId);
if (routeFromList) {
this.selectedRouteDetails = { ...routeFromList };
}
this.$message.success('航线及其航点已成功保存并同步');
}
} catch (error) {
console.error(">>> [保存异常]:", error);
@ -885,7 +969,9 @@ export default {
// 线
this.$refs.cesiumMap.renderRouteWaypoints(
this.selectedRouteDetails.waypoints,
this.selectedRouteDetails.id
this.selectedRouteDetails.id,
this.selectedRouteDetails.platformId,
this.selectedRouteDetails.platform
);
}
this.showWaypointDialog = false;
@ -1501,7 +1587,7 @@ export default {
if (waypoints.length > 0) {
//
if (this.$refs.cesiumMap) {
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id);
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
} else {
this.$message.warning('该航线暂无坐标数据,无法在地图展示');

646
ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

@ -2,16 +2,194 @@
<el-dialog
title="编辑航线"
:visible.sync="visible"
width="300px"
width="560px"
:close-on-click-modal="false"
append-to-body
custom-class="blue-dialog"
custom-class="blue-dialog route-edit-dialog"
>
<el-form :model="form" label-width="70px" size="small" @submit.native.prevent>
<el-form-item label="航线名称">
<el-input v-model="form.name" placeholder="请输入航线名称"></el-input>
</el-form-item>
</el-form>
<div class="route-edit-tab-bar">
<button type="button" class="tab-bar-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基础</button>
<button type="button" class="tab-bar-item" :class="{ active: activeTab === 'platform' }" @click="activeTab = 'platform'">平台</button>
</div>
<div v-show="activeTab === 'basic'" class="tab-pane-wrap">
<div class="tab-pane-body basic-tab-content">
<el-form :model="form" label-width="88px" size="small" class="route-edit-form style-form" @submit.native.prevent>
<div class="form-section">
<div class="form-section-label">基本信息</div>
<el-form-item label="航线名称">
<el-input v-model="form.name" placeholder="请输入航线名称" clearable />
</el-form-item>
</div>
<div class="form-section">
<div class="form-section-label">航点样式</div>
<div class="form-row two-col">
<el-form-item label="大小" class="col-item">
<el-input-number v-model="styleForm.waypoint.pixelSize" :min="4" :max="20" :step="1" size="small" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="边框粗细" class="col-item">
<el-input-number v-model="styleForm.waypoint.outlineWidth" :min="1" :max="5" :step="1" size="small" controls-position="right" style="width: 100%" />
</el-form-item>
</div>
<div class="form-row two-col">
<el-form-item label="填充颜色" class="col-item">
<div class="color-picker-wrap">
<el-color-picker v-model="styleForm.waypoint.color" size="small" :predefine="presetColors" />
<span class="color-value">{{ styleForm.waypoint.color }}</span>
</div>
</el-form-item>
<el-form-item label="边框颜色" class="col-item">
<div class="color-picker-wrap">
<el-color-picker v-model="styleForm.waypoint.outlineColor" size="small" :predefine="presetColors" />
<span class="color-value">{{ styleForm.waypoint.outlineColor }}</span>
</div>
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="form-section-label">航线样式</div>
<div class="form-row two-col">
<el-form-item label="线段样式" class="col-item">
<el-select v-model="styleForm.line.style" placeholder="请选择" style="width: 100%">
<el-option label="实线" value="solid" />
<el-option label="虚线" value="dash" />
</el-select>
</el-form-item>
<el-form-item label="线宽" class="col-item">
<el-input-number v-model="styleForm.line.width" :min="2" :max="12" :step="1" size="small" controls-position="right" style="width: 100%" />
</el-form-item>
</div>
<div class="form-row two-col">
<el-form-item label="线条颜色" class="col-item">
<div class="color-picker-wrap">
<el-color-picker v-model="styleForm.line.color" size="small" :predefine="presetColors" />
<span class="color-value">{{ styleForm.line.color }}</span>
</div>
</el-form-item>
<el-form-item v-if="styleForm.line.style === 'dash'" label="间隔颜色" class="col-item">
<div class="color-picker-wrap">
<el-color-picker v-model="styleForm.line.gapColor" size="small" :predefine="presetColors" />
<span class="color-value">{{ styleForm.line.gapColor }}</span>
</div>
</el-form-item>
</div>
</div>
</el-form>
</div>
</div>
<div v-show="activeTab === 'platform'" class="tab-pane-wrap">
<div class="tab-pane-body platform-tab-content">
<div v-if="platformLoading" class="platform-loading">加载中...</div>
<template v-else>
<div class="platform-segmented">
<button
type="button"
class="seg-item"
:class="{ active: platformCategory === 'Air' }"
@click="platformCategory = 'Air'"
>
<i class="seg-icon seg-icon-air"></i>
<span>空中</span>
</button>
<button
type="button"
class="seg-item"
:class="{ active: platformCategory === 'Sea' }"
@click="platformCategory = 'Sea'"
>
<i class="seg-icon seg-icon-sea"></i>
<span>海上</span>
</button>
<button
type="button"
class="seg-item"
:class="{ active: platformCategory === 'Ground' }"
@click="platformCategory = 'Ground'"
>
<i class="seg-icon seg-icon-ground"></i>
<span>地面</span>
</button>
</div>
<div v-show="platformCategory === 'Air'" class="platform-list">
<div
v-for="platform in airPlatforms"
:key="platform.id"
class="platform-card"
>
<div class="platform-card-icon">
<img v-if="isImg(platform.imageUrl)" :src="formatImg(platform.imageUrl)" class="platform-img" alt="" />
<i v-else class="card-icon-placeholder el-icon-s-promotion"></i>
</div>
<div class="platform-card-info">
<div class="platform-card-name">{{ platform.name }}</div>
<div class="platform-card-desc">{{ platformTypeLabel(platform.type) }}</div>
</div>
<button
type="button"
class="platform-card-btn"
:class="{ selected: selectedPlatformId === platform.id }"
@click="selectPlatform(platform)"
>
<i :class="selectedPlatformId === platform.id ? 'el-icon-check' : 'el-icon-plus'"></i>
{{ selectedPlatformId === platform.id ? '已选择' : '选择' }}
</button>
</div>
<div v-if="airPlatforms.length === 0" class="empty-tip">暂无空中平台</div>
</div>
<div v-show="platformCategory === 'Sea'" class="platform-list">
<div
v-for="platform in seaPlatforms"
:key="platform.id"
class="platform-card"
>
<div class="platform-card-icon">
<img v-if="isImg(platform.imageUrl)" :src="formatImg(platform.imageUrl)" class="platform-img" alt="" />
<i v-else class="card-icon-placeholder el-icon-s-flag"></i>
</div>
<div class="platform-card-info">
<div class="platform-card-name">{{ platform.name }}</div>
<div class="platform-card-desc">{{ platformTypeLabel(platform.type) }}</div>
</div>
<button
type="button"
class="platform-card-btn"
:class="{ selected: selectedPlatformId === platform.id }"
@click="selectPlatform(platform)"
>
<i :class="selectedPlatformId === platform.id ? 'el-icon-check' : 'el-icon-plus'"></i>
{{ selectedPlatformId === platform.id ? '已选择' : '选择' }}
</button>
</div>
<div v-if="seaPlatforms.length === 0" class="empty-tip">暂无海上平台</div>
</div>
<div v-show="platformCategory === 'Ground'" class="platform-list">
<div
v-for="platform in groundPlatforms"
:key="platform.id"
class="platform-card"
>
<div class="platform-card-icon">
<img v-if="isImg(platform.imageUrl)" :src="formatImg(platform.imageUrl)" class="platform-img" alt="" />
<i v-else class="card-icon-placeholder el-icon-van"></i>
</div>
<div class="platform-card-info">
<div class="platform-card-name">{{ platform.name }}</div>
<div class="platform-card-desc">{{ platformTypeLabel(platform.type) }}</div>
</div>
<button
type="button"
class="platform-card-btn"
:class="{ selected: selectedPlatformId === platform.id }"
@click="selectPlatform(platform)"
>
<i :class="selectedPlatformId === platform.id ? 'el-icon-check' : 'el-icon-plus'"></i>
{{ selectedPlatformId === platform.id ? '已选择' : '选择' }}
</button>
</div>
<div v-if="groundPlatforms.length === 0" class="empty-tip">暂无地面平台</div>
</div>
</template>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="visible = false"> </el-button>
@ -21,70 +199,474 @@
</template>
<script>
import { listLib } from '@/api/system/lib'
export default {
name: 'RouteEditDialog',
props: {
// v-model="showRouteDialog"
value: {
type: Boolean,
default: false
},
route: {
type: Object,
default: () => ({})
}
value: { type: Boolean, default: false },
route: { type: Object, default: () => ({}) }
},
data() {
return {
activeTab: 'basic',
platformCategory: 'Air',
platformLoading: false,
airPlatforms: [],
seaPlatforms: [],
groundPlatforms: [],
selectedPlatformId: null,
selectedPlatform: null,
form: {
id: '',
name: ''
}
},
defaultStyle: {
waypoint: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 },
line: { style: 'dash', width: 4, color: '#ffffff', gapColor: '#000000', dashLength: 20 }
},
styleForm: {
waypoint: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 },
line: { style: 'dash', width: 4, color: '#ffffff', gapColor: '#000000', dashLength: 20 }
},
presetColors: [
'#ffffff', '#000000', '#0078FF', '#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
'#909399', '#303133', '#00CED1', '#FF1493', '#FFD700', '#4B0082', '#00FF00', '#FF4500'
]
}
},
computed: {
visible: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
get() { return this.value },
set(val) { this.$emit('input', val) }
}
},
watch: {
// route form
route: {
handler(val) {
if (val) {
//
this.form = {
id: val.id,
name: val.name
name: val.name,
attributes: val.attributes != null ? val.attributes : ''
}
this.selectedPlatformId = val.platformId || null
this.selectedPlatform = val.platform ? { ...val.platform } : null
this.parseStyleFromRoute(val)
}
},
immediate: true,
deep: true
},
visible(val) {
if (val && this.activeTab === 'platform') {
this.loadPlatforms()
}
},
activeTab(val) {
if (val === 'platform' && this.visible) {
this.loadPlatforms()
}
}
},
methods: {
loadPlatforms() {
this.platformLoading = true
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'
}
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)
})
}).catch(() => {
this.$message.error('加载平台列表失败')
}).finally(() => {
this.platformLoading = false
})
},
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 || ''
return backendUrl + cleanPath
},
platformTypeLabel(type) {
const map = { Air: '空中平台', Sea: '海上平台', Ground: '地面平台' }
return map[type] || type || ''
},
selectPlatform(platform) {
this.selectedPlatformId = platform.id
this.selectedPlatform = { id: platform.id, name: platform.name, imageUrl: platform.imageUrl }
},
parseStyleFromRoute(route) {
const def = this.defaultStyle
try {
const attrs = typeof route.attributes === 'string' ? JSON.parse(route.attributes || '{}') : (route.attributes || {})
const wp = attrs.waypointStyle || {}
const ln = attrs.lineStyle || {}
this.styleForm = {
waypoint: {
pixelSize: wp.pixelSize != null ? wp.pixelSize : def.waypoint.pixelSize,
color: wp.color || def.waypoint.color,
outlineColor: wp.outlineColor || def.waypoint.outlineColor,
outlineWidth: wp.outlineWidth != null ? wp.outlineWidth : def.waypoint.outlineWidth
},
line: {
style: ln.style || def.line.style,
width: ln.width != null ? ln.width : def.line.width,
color: ln.color || def.line.color,
gapColor: ln.gapColor != null ? ln.gapColor : def.line.gapColor,
dashLength: ln.dashLength != null ? ln.dashLength : def.line.dashLength
}
}
} catch (_) {
this.styleForm = {
waypoint: { ...def.waypoint },
line: { ...def.line }
}
}
},
getAttributesForSave() {
const attrs = {}
try {
const existing = typeof this.form.attributes === 'string' ? JSON.parse(this.form.attributes || '{}') : (this.form.attributes || {})
Object.assign(attrs, existing)
} catch (_) {}
attrs.waypointStyle = { ...this.styleForm.waypoint }
attrs.lineStyle = { ...this.styleForm.line }
return JSON.stringify(attrs)
},
handleSave() {
// form name
this.$emit('save', this.form)
const payload = {
...this.form,
attributes: this.getAttributesForSave(),
platformId: this.selectedPlatformId,
platform: this.selectedPlatform,
routeStyle: {
waypoint: { ...this.styleForm.waypoint },
line: { ...this.styleForm.line }
}
}
this.$emit('save', payload)
this.visible = false
}
}
}
</script>
<style>
/* 航线编辑标题:加粗加黑 */
.route-edit-dialog .el-dialog__title { font-weight: 700; color: #303133; }
.route-edit-dialog .el-dialog__header { padding: 14px 20px 12px; }
/* 弹窗固定于视口正中间,不随页面滚动 */
.route-edit-dialog.el-dialog {
position: fixed !important;
top: 50% !important;
left: 50% !important;
margin: 0 !important;
transform: translate(-50%, -50%);
}
/* 弹窗内容区固定高度,切换基础/平台时大小不变 */
.route-edit-dialog .el-dialog__body {
padding-top: 12px;
height: 420px;
min-height: 420px;
overflow: hidden;
box-sizing: border-box;
}
</style>
<style scoped>
.route-edit-tab-bar {
display: flex;
gap: 24px;
margin-bottom: 0;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
}
.tab-bar-item {
padding: 0;
border: none;
background: none;
font-size: 14px;
color: #909399;
cursor: pointer;
position: relative;
padding-bottom: 12px;
margin-bottom: -12px;
outline: none;
}
.tab-bar-item:hover {
color: #606266;
}
.tab-bar-item.active {
color: #165dff;
font-weight: 500;
}
.tab-bar-item.active::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
background: #165dff;
}
.tab-pane-wrap {
padding-top: 4px;
height: 384px;
min-height: 384px;
overflow: hidden;
box-sizing: border-box;
}
.tab-pane-body {
height: 380px;
min-height: 380px;
max-height: 380px;
box-sizing: border-box;
}
.basic-tab-content {
padding: 0 16px;
height: 380px;
overflow-y: auto;
box-sizing: border-box;
}
/* 基础页区块:与平台卡片同风格,白底 + 柔和阴影,左右留白可展示阴影 */
.form-section {
background: #fff;
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 14px;
border: none;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.25s ease;
}
.form-section:hover {
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.06),
0 8px 20px rgba(0, 0, 0, 0.06);
}
.form-section:last-of-type {
margin-bottom: 0;
}
.form-section-label {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
padding-left: 2px;
}
.basic-tab-content .form-row {
display: flex;
gap: 16px;
margin-bottom: 0;
}
.basic-tab-content .form-row.two-col .col-item {
flex: 1;
min-width: 0;
}
.basic-tab-content .form-row .el-form-item {
margin-bottom: 14px;
}
.basic-tab-content .form-row.two-col .el-form-item {
margin-bottom: 14px;
}
.basic-tab-content .form-section .el-form-item:last-child {
margin-bottom: 0;
}
.color-picker-wrap {
display: inline-flex;
align-items: center;
gap: 8px;
}
.route-edit-form .color-value {
font-size: 12px;
color: #909399;
vertical-align: middle;
}
.route-edit-form .el-slider {
margin-right: 12px;
}
/* 平台页:分段选择(样图风格 - 独立圆角矩形,与上方基础/平台有间距) */
.platform-tab-content {
min-height: 380px;
max-height: 380px;
overflow-y: auto;
background: #fff;
padding: 0 16px;
}
.platform-segmented {
display: inline-flex;
gap: 8px;
margin-top: 16px;
margin-bottom: 16px;
}
.seg-item {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 5px 22px;
border: 1px solid #e4e7ed;
border-radius: 20px;
font-size: 13px;
color: #606266;
background: #f5f5f5;
cursor: pointer;
transition: all 0.2s;
}
.seg-item:hover {
border-color: #dcdfe6;
color: #303133;
}
.seg-item.active {
background: #165dff;
border-color: #165dff;
color: #fff;
font-weight: 500;
}
.seg-icon {
width: 18px;
height: 18px;
display: inline-block;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.9;
}
.seg-item.active .seg-icon {
opacity: 1;
filter: brightness(0) invert(1);
}
.seg-icon-air {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z'/%3E%3C/svg%3E");
}
.seg-icon-sea {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M20 21c-1.39 0-2.78-.47-4-1.32-2.44 1.71-5.56 1.71-8 0C6.78 20.53 5.39 21 4 21H2v2h2c1.38 0 2.74-.35 4-.99 2.52 1.29 5.48 1.29 8 0 1.26.65 2.62.99 4 .99h2v-2h-2zM3.95 19H4c1.6 0 3.02-.88 4-2 .98 1.12 2.4 2 4 2h.05l1.89-6.68c.08-.26.06-.54-.06-.78-.12-.24-.32-.42-.57-.5L20 10.62V6c0-1.1-.9-2-2-2h-3V1H9v3H6c-1.1 0-2 .9-2 2v4.62l-1.29.42c-.25.08-.45.26-.57.5-.12.24-.14.52-.06.78L3.95 19zM6 6h12v3.97L12 8 6 9.97V6z'/%3E%3C/svg%3E");
}
.seg-icon-ground {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23606266'%3E%3Cpath d='M20 8h-3V4H3c-1.1 0-2 .9-2 2v11h2c0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h2v-5l-3-4zM6 18.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5-9l1.96 2.5H17V9.5h2.5zm-1.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z'/%3E%3C/svg%3E");
}
/* 平台卡片列表(与页面同背景色 + 阴影框) */
.platform-loading,
.empty-tip {
padding: 20px;
color: #909399;
text-align: center;
font-size: 13px;
}
.platform-list {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 平台卡片:无硬边框,用柔和阴影做过渡,不死板(参考右侧样图) */
.platform-card {
display: flex;
align-items: center;
padding: 12px 14px;
background: #fff;
border-radius: 8px;
border: none;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.25s ease;
}
.platform-card:hover {
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.06),
0 8px 20px rgba(0, 0, 0, 0.06);
}
.platform-card-icon {
width: 44px;
height: 44px;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
background: #e0f0ff;
border-radius: 50%;
}
.platform-card-icon .platform-img {
width: 28px;
height: 28px;
object-fit: contain;
}
.platform-card-icon .card-icon-placeholder {
font-size: 22px;
color: #165dff;
}
.platform-card-info {
flex: 1;
min-width: 0;
}
.platform-card-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.platform-card-desc {
font-size: 12px;
color: #909399;
}
/* 选择按钮:样图风格圆角矩形(胶囊形) */
.platform-card-btn {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
cursor: pointer;
transition: all 0.2s;
}
.platform-card-btn:hover {
border-color: #165dff;
color: #165dff;
}
.platform-card-btn.selected {
background: #165dff;
border-color: #165dff;
color: #fff;
}
.platform-card-btn i {
font-size: 12px;
}
.blue-btn {
background: #008aff;
border-color: #008aff;
background: #165dff;
border-color: #165dff;
}
.blue-btn:hover {
background: #0066cc;
border-color: #0066cc;
background: #165dff;
border-color: #165dff;
}
</style>

2
ruoyi-ui/vue.config.js

@ -15,7 +15,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const baseUrl = 'http://192.168.50.145:8080' // 后端接口
const baseUrl = 'http://192.168.50.30:8080' // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口
// 定义 Cesium 源码路径

Loading…
Cancel
Save