【类 型】:feat

【原  因】:飞行区域安全控制需求,需要前端支持禁飞区与限飞区的可视化设置与展示。
【过  程】:- 新增 RestrictflyControl 控件,支持限飞区绘制并弹窗设置高度;
- 支持将限飞区多边形及高度通过 setRestrictflyData 接口保存;
- 在飞行地图中新增 PolygonToggleControl 控件,可开关显示禁飞区/限飞区图层;
- 限飞区标签支持显示限高信息,如“限飞高度120米”。
【影  响】:

# 类型 包含:
# feat:新功能(feature)
# fix:修补bug
# docs:文档(documentation)
# style: 格式(不影响代码运行的变动)
# refactor:重构(即不是新增功能,也不是修改bug的代码变动)
# test:增加测试
# chore:构建过程或辅助工具的变动
This commit is contained in:
air 2025-06-18 19:58:16 +08:00
parent d731a42bfa
commit 555d480efd
9 changed files with 375 additions and 31477 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store .DS_Store
/node_modules /node_modules
/dist /dist
/package-lock.json

31270
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,16 +9,17 @@
"lint": "eslint --ext .js,.vue src" "lint": "eslint --ext .js,.vue src"
}, },
"dependencies": { "dependencies": {
"@turf/turf": "^7.2.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"geodist": "^0.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mapbox-gl": "^2.15.0", "mapbox-gl": "^2.15.0",
"mqtt": "^2.18.9", "mqtt": "^2.18.9",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"geodist": "^0.2.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vue-template-compiler": "^2.7.16", "vue-template-compiler": "^2.7.16",

View File

@ -6,7 +6,7 @@
<script> <script>
import mapboxgl from 'mapbox-gl' import mapboxgl from 'mapbox-gl'
import { MapboxStyleSwitcherControl, FollowControl, CustomFullscreenControl, NoFlyControl, SaveToFileControl, PolygonToggleControl } from '@/utils/mapboxgl_plugs' import { MapboxStyleSwitcherControl, FollowControl, CustomFullscreenControl, NoFlyControl, RestrictflyControl, SaveToFileControl, PolygonToggleControl } from '@/utils/mapboxgl_plugs'
import planeIcon from '@/assets/svg/plane.svg' import planeIcon from '@/assets/svg/plane.svg'
export default { export default {
@ -172,6 +172,10 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
enableRestrictfly: { //
type: Boolean,
default: false
},
enableShowNofly: { // enableShowNofly: { //
type: Boolean, type: Boolean,
default: false default: false
@ -345,22 +349,23 @@ export default {
// //
if (this.enableNofly) { if (this.enableNofly) {
const noflyPolygons = this.$store.state.noflyData[0]
const restrictflyPolygons = this.$store.state.noflyData[1]
const shopId = this.$store.state.user.shop_id
this.map.addControl(new NoFlyControl({ this.map.addControl(new NoFlyControl({
noflyPolygons, defaultPolygons: this.$store.state.noflyData[0], //
restrictflyPolygons, shopId: this.$store.state.user.shop_id,
shopId, onSave: (data) => {
onSave: (nofly, limit) => { console.log('保存成功:', data)
console.log('保存成功:', nofly, limit) }
}, }), 'top-left')
onDrawFinish: (nofly) => { }
console.log('禁飞区绘制完成:', nofly)
}, //
onLimitFinish: (limit) => { if (this.enableRestrictfly) {
console.log('限飞区绘制完成:', limit) this.map.addControl(new RestrictflyControl({
defaultPolygons: this.$store.state.noflyData[1], //
defaultHeights: this.$store.state.noflyData[2], //
shopId: this.$store.state.user.shop_id,
onSave: (data) => {
console.log('保存成功:', data)
} }
}), 'top-left') }), 'top-left')
} }
@ -369,6 +374,7 @@ export default {
if (this.enableShowNofly) { if (this.enableShowNofly) {
const noflyPolygons = this.$store.state.noflyData[0] || [] const noflyPolygons = this.$store.state.noflyData[0] || []
const restrictflyPolygons = this.$store.state.noflyData[1] || [] const restrictflyPolygons = this.$store.state.noflyData[1] || []
const restrictflyHeights = this.$store.state.noflyData[2] || []
this.map.addControl(new PolygonToggleControl({ this.map.addControl(new PolygonToggleControl({
defaultIconClass: 'iconfont icon-jinfeiqu_weidianji f-s-20 seatFontColor', defaultIconClass: 'iconfont icon-jinfeiqu_weidianji f-s-20 seatFontColor',
@ -376,16 +382,18 @@ export default {
onToggle: (isActive) => { onToggle: (isActive) => {
if (isActive) { if (isActive) {
// //
noflyPolygons.forEach((coords, i) => { noflyPolygons.forEach((coords, i) => {
this.drawPolygonWithLabel(coords, `nofly-polygon-${i}`, '#F33', '禁飞区') this.drawPolygonWithLabel(coords, `nofly-polygon-${i}`, '#F33', '禁飞区')
}) })
// //
restrictflyPolygons.forEach((coords, i) => { restrictflyPolygons.forEach((coords, i) => {
this.drawPolygonWithLabel(coords, `restrict-polygon-${i}`, '#F83', '限飞区') const height = restrictflyHeights[i] || 0
const label = `限飞高度${height}`
this.drawPolygonWithLabel(coords, `restrict-polygon-${i}`, '#F83', label)
}) })
} else { } else {
// //
const layerIds = [] const layerIds = []
const sourceIds = [] const sourceIds = []

View File

@ -53,7 +53,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/register/index', redirect: '/register/index',
meta: { meta: {
title: '飞机管理', title: '管理飞机',
icon: 'el-icon-edit-outline', icon: 'el-icon-edit-outline',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'plane' tapName: 'plane'
@ -116,23 +116,33 @@ const routes = [
{ {
path: '/nofly', path: '/nofly',
component: Layout, component: Layout,
redirect: '/nofly/set', redirect: '/nofly/setNofly',
meta: { meta: {
title: '设置禁飞区', title: '限制飞行',
icon: 'iconfont icon-feihangluxian', icon: 'iconfont icon-jinfeiqu',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'plane' tapName: 'plane'
}, },
children: [ children: [
{ {
path: '/nofly/set', path: '/nofly/setNofly',
component: () => import('@/views/layout/components/main/nofly/set'), component: () => import('@/views/layout/components/main/nofly/setNofly'),
meta: { meta: {
title: '设置禁飞区', title: '设置禁飞区',
icon: 'iconfont icon-huizhi', icon: 'iconfont icon-huizhi',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'plane' tapName: 'plane'
} }
},
{
path: '/nofly/setRestrictfly',
component: () => import('@/views/layout/components/main/nofly/setRestrictfly'),
meta: {
title: '设置限飞区',
icon: 'iconfont icon-huizhi',
roles: ['admin', 'editor'],
tapName: 'plane'
}
} }
] ]
}, },
@ -141,7 +151,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/route/index', redirect: '/route/index',
meta: { meta: {
title: '航线管理', title: '管理航线',
icon: 'iconfont icon-feihangluxian', icon: 'iconfont icon-feihangluxian',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'plane' tapName: 'plane'
@ -185,7 +195,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/site/index', redirect: '/site/index',
meta: { meta: {
title: '站点管理', title: '管理站点',
icon: 'iconfont icon-zhandianguanli', icon: 'iconfont icon-zhandianguanli',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'plane' tapName: 'plane'
@ -252,7 +262,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/shop/edit', redirect: '/shop/edit',
meta: { meta: {
title: '商铺管理', title: '管理商铺',
icon: 'iconfont icon-a-shanghu_choose2x1', icon: 'iconfont icon-a-shanghu_choose2x1',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -262,7 +272,7 @@ const routes = [
path: '/shop/edit', path: '/shop/edit',
component: () => import('@/views/layout/components/main/shop/add'), component: () => import('@/views/layout/components/main/shop/add'),
meta: { meta: {
title: '商铺设置', title: '设置商铺',
icon: 'iconfont icon-dianpuguanli', icon: 'iconfont icon-dianpuguanli',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -340,7 +350,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/category/index', redirect: '/category/index',
meta: { meta: {
title: '分类管理', title: '管理分类',
icon: 'iconfont icon-a-ziliaocaozuoxianshifenleishu', icon: 'iconfont icon-a-ziliaocaozuoxianshifenleishu',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -350,7 +360,7 @@ const routes = [
path: '/category/index', path: '/category/index',
component: () => import('@/views/layout/components/main/category/index'), component: () => import('@/views/layout/components/main/category/index'),
meta: { meta: {
title: '分类管理', title: '管理分类',
icon: 'iconfont icon-a-ziliaocaozuoxianshifenleishu', icon: 'iconfont icon-a-ziliaocaozuoxianshifenleishu',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -363,7 +373,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/spu/index', redirect: '/spu/index',
meta: { meta: {
title: '商品管理', title: '管理商品',
icon: 'iconfont icon-chanpin', icon: 'iconfont icon-chanpin',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -438,7 +448,7 @@ const routes = [
component: Layout, component: Layout,
redirect: '/broadcast/banner', redirect: '/broadcast/banner',
meta: { meta: {
title: '广告管理', title: '管理广告',
icon: 'iconfont icon-guanggao', icon: 'iconfont icon-guanggao',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -448,7 +458,7 @@ const routes = [
path: '/broadcast/banner', path: '/broadcast/banner',
component: () => import('@/views/layout/components/main/broadcast/banner'), component: () => import('@/views/layout/components/main/broadcast/banner'),
meta: { meta: {
title: 'banner设置', title: '设置banner',
icon: 'iconfont icon-banner', icon: 'iconfont icon-banner',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -458,7 +468,7 @@ const routes = [
path: '/broadcast/notice', path: '/broadcast/notice',
component: () => import('@/views/layout/components/main/broadcast/notice'), component: () => import('@/views/layout/components/main/broadcast/notice'),
meta: { meta: {
title: '滚动通知设置', title: '设置滚动通知',
icon: 'iconfont icon-m-gundongwenzi', icon: 'iconfont icon-m-gundongwenzi',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'
@ -481,7 +491,7 @@ const routes = [
path: '/order/index', path: '/order/index',
component: () => import('@/views/layout/components/main/order/index'), component: () => import('@/views/layout/components/main/order/index'),
meta: { meta: {
title: '订单管理', title: '管理订单',
icon: 'iconfont icon-a-SalesOrderManagement', icon: 'iconfont icon-a-SalesOrderManagement',
roles: ['admin', 'editor'], roles: ['admin', 'editor'],
tapName: 'admin' tapName: 'admin'

View File

@ -17,7 +17,7 @@ const store = new Vuex.Store({
airList: [], // 所有飞机列表 airList: [], // 所有飞机列表
siteList: [], // 站点列表 siteList: [], // 站点列表
routeList: [], // 航线列表 routeList: [], // 航线列表
noflyData: [[], []], // [0]禁飞区数据 [1]限制飞区 noflyData: [[], [], []], // [0]禁飞区数据 [1]限制飞区 [2]限飞区高度
categoryList: [], // 分类列表(小程序) categoryList: [], // 分类列表(小程序)
spuList: [], // 商品spu列表 spuList: [], // 商品spu列表
skuList: [], // 商品sku列表 skuList: [], // 商品sku列表
@ -70,13 +70,14 @@ const store = new Vuex.Store({
* @description: 设置禁飞区列表 * @description: 设置禁飞区列表
*/ */
setNoflyData (state, payload) { setNoflyData (state, payload) {
if (payload && payload.nofly_data && payload.restrictfly_data) { if (payload && payload.nofly_data && payload.restrictfly_data && payload.restrictfly_height) {
state.noflyData = [ state.noflyData = [
JSON.parse(payload.nofly_data || '[]'), JSON.parse(payload.nofly_data || '[]'),
JSON.parse(payload.restrictfly_data || '[]') JSON.parse(payload.restrictfly_data || '[]'),
JSON.parse(payload.restrictfly_height || '[]')
] ]
} else { } else {
state.noflyData = [[], []] state.noflyData = [[], [], []]
} }
}, },
/** /**
@ -625,7 +626,7 @@ const store = new Vuex.Store({
if (res.data.status === 1) { if (res.data.status === 1) {
commit('setNoflyData', res.data.noflyData) commit('setNoflyData', res.data.noflyData)
} else { } else {
commit('setNoflyData', [[], []]) commit('setNoflyData', [[], [], []])
Message.warning(res.data.msg || '暂无禁飞区数据') Message.warning(res.data.msg || '暂无禁飞区数据')
} }
return res return res

View File

@ -198,16 +198,15 @@ export async function saveFlyData (data) {
* @param {Array} restrictfly_data 限制飞区数据数组 * @param {Array} restrictfly_data 限制飞区数据数组
* @returns {Object|null} 返回接口响应数据 null 表示失败 * @returns {Object|null} 返回接口响应数据 null 表示失败
*/ */
export async function setNoflyData (shopId, noflyData, restrictflyData) { export async function setNoflyData (shopId, noflyData) {
try { try {
const params = new URLSearchParams() const params = new URLSearchParams()
params.append('shop_id', shopId || '') params.append('shop_id', shopId || '')
params.append('nofly_data', JSON.stringify(noflyData || [])) params.append('nofly_data', JSON.stringify(noflyData || []))
params.append('restrictfly_data', JSON.stringify(restrictflyData || []))
const res = await api.post('setNoflyData', params) const res = await api.post('setNoflyData', params)
if (res.data.status === 1) { if (res.data.status === 1) {
store.dispatch('fetchNoflyData', shopId)// 更新禁飞区数据 store.dispatch('fetchNoflyData', shopId)// 更新禁飞区数据
Message.success(res.data.msg) Message.success(res.data.msg)
} else { } else {
Message.warning(res.data.msg) Message.warning(res.data.msg)
@ -218,3 +217,31 @@ export async function setNoflyData (shopId, noflyData, restrictflyData) {
return null return null
} }
} }
/**
* @description: 保存限飞区数据
* @param {string|number} shop_id 商铺ID
* @param {Array} restrictfly_data 限制飞区数据数组
* @param {Array} restrictfly_height 限制飞区数据高度组
* @returns {Object|null} 返回接口响应数据 null 表示失败
*/
export async function setRestrictflyData (shopId, restrictflyData, restrictflyHeight) {
try {
const params = new URLSearchParams()
params.append('shop_id', shopId || '')
params.append('restrictfly_data', JSON.stringify(restrictflyData || []))
params.append('restrictfly_height', JSON.stringify(restrictflyHeight || []))
const res = await api.post('setNoflyData', params)
if (res.data.status === 1) {
store.dispatch('fetchNoflyData', shopId)// 更新禁限飞区数据
Message.success(res.data.msg)
} else {
Message.warning(res.data.msg)
}
return res.data
} catch (error) {
Message.error('保存限飞区数据失败')
return null
}
}

View File

@ -1,8 +1,8 @@
import mapboxgl from 'mapbox-gl' import mapboxgl from 'mapbox-gl'
import MapboxDraw from '@mapbox/mapbox-gl-draw' import MapboxDraw from '@mapbox/mapbox-gl-draw'
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
import { setNoflyData } from '@/utils/api/table' import { setNoflyData, setRestrictflyData } from '@/utils/api/table'
import { Message } from 'element-ui' import { Message, MessageBox } from 'element-ui'
/** /**
* 自定义地图样式切换控件 * 自定义地图样式切换控件
@ -218,65 +218,45 @@ export class CustomFullscreenControl extends mapboxgl.FullscreenControl {
} }
} }
// 设置禁飞区 限飞区 保存 删除控件 // 禁飞区控件类
export class NoFlyControl { export class NoFlyControl {
constructor (options = {}) { constructor (options = {}) {
this._map = null this._map = null // 地图实例
this._container = null this._container = null // 控件容器(用于放按钮)
this._draw = null // MapboxDraw 实例
this._draw = null this._onDrawFinish = options.onDrawFinish || function () {} // 绘制完成时的回调
this._onDrawFinish = options.onDrawFinish || function () {} this._defaultPolygons = options.defaultPolygons || [] // 初始加载的禁飞区多边形坐标
this._noflyPolygons = options.noflyPolygons || [] this._onSave = options.onSave || function () {} // 保存时的回调
this._shopId = options.shopId // 商户ID用于提交接口
this._onLimitFinish = options.onLimitFinish || function () {}
this._restrictflyPolygons = options.restrictflyPolygons || []
this._onSave = options.onSave || function () {}
this._shopId = options.shopId
this._activeMode = 'nofly' // 当前绘制类型
} }
// 当控件添加到地图上时调用
onAdd (map) { onAdd (map) {
this._map = map this._map = map
// 创建控件按钮容器
this._container = document.createElement('div') this._container = document.createElement('div')
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group' this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group'
// --- 绘制禁飞区按钮 --- // --- 绘制按钮 ---
const drawButton = document.createElement('button') const drawButton = document.createElement('button')
drawButton.className = 'mapboxgl-ctrl-icon' drawButton.className = 'mapboxgl-ctrl-icon'
drawButton.type = 'button' drawButton.type = 'button'
drawButton.innerHTML = '✏️' drawButton.innerHTML = '✏️' // 图标:铅笔
drawButton.title = '绘制禁飞区' drawButton.title = '绘制禁飞区' // 鼠标提示
drawButton.onclick = () => { drawButton.onclick = () => this._enableDraw() // 点击启用绘制模式
this._activeMode = 'nofly'
this._enableDraw()
}
this._container.appendChild(drawButton) this._container.appendChild(drawButton)
// --- 绘制限飞区按钮 ---
const limitButton = document.createElement('button')
limitButton.className = 'mapboxgl-ctrl-icon'
limitButton.type = 'button'
limitButton.innerHTML = '✒️'
limitButton.title = '绘制限飞区'
limitButton.onclick = () => {
this._activeMode = 'limit'
this._enableDraw()
}
this._container.appendChild(limitButton)
// --- 保存按钮 --- // --- 保存按钮 ---
const saveButton = document.createElement('button') const saveButton = document.createElement('button')
saveButton.className = 'mapboxgl-ctrl-icon' saveButton.className = 'mapboxgl-ctrl-icon'
saveButton.type = 'button' saveButton.type = 'button'
saveButton.innerHTML = '💾' saveButton.innerHTML = '💾' // 图标:磁盘
saveButton.title = '保存禁飞区及限飞区' saveButton.title = '保存禁飞区'
saveButton.onclick = () => this._savePolygons() saveButton.onclick = () => this._savePolygons()
this._container.appendChild(saveButton) this._container.appendChild(saveButton)
// --- 删除按钮 --- // --- 删除按钮(删除选中图形) ---
const deleteButton = document.createElement('button') const deleteButton = document.createElement('button')
deleteButton.className = 'mapboxgl-ctrl-icon' deleteButton.className = 'mapboxgl-ctrl-icon'
deleteButton.type = 'button' deleteButton.type = 'button'
@ -285,43 +265,248 @@ export class NoFlyControl {
deleteButton.onclick = () => this._deleteSelected() deleteButton.onclick = () => this._deleteSelected()
this._container.appendChild(deleteButton) this._container.appendChild(deleteButton)
// 初始化 Draw 控件 // --- 初始化 MapboxDraw 控件 ---
this._draw = new MapboxDraw({ this._draw = new MapboxDraw({
displayControlsDefault: false, displayControlsDefault: false, // 不显示默认工具条
styles: this._getDrawStyles() styles: [ // 自定义绘制样式(橙色)
{
id: 'gl-draw-polygon-fill',
type: 'fill',
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
paint: {
'fill-color': '#f33', // 填充颜色
'fill-opacity': 0.3 // 透明度
}
},
{
id: 'gl-draw-polygon-stroke-active',
type: 'line',
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
paint: {
'line-color': '#f33', // 边框颜色
'line-width': 2
}
},
// 控制点外圈(白色光晕)
{
id: 'gl-draw-polygon-and-line-vertex-halo-active',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 7,
'circle-color': '#fff'
}
},
// 控制点内圈
{
id: 'gl-draw-polygon-and-line-vertex-active',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 5,
'circle-color': '#fff'
}
}
]
}) })
// 将 Draw 控件添加到地图上
map.addControl(this._draw) map.addControl(this._draw)
// 加载已有禁飞区 // --- 加载已有禁飞区(回显数据) ---
if (this._noflyPolygons.length > 0) { if (this._defaultPolygons.length > 0) {
const features = this._noflyPolygons.map(coords => ({ const features = this._defaultPolygons.map(coords => ({
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [coords] // 注意GeoJSON Polygon 外层需要再包一层
},
properties: {}
}))
this._draw.set({
type: 'FeatureCollection',
features
})
}
// 监听绘制完成事件
map.on('draw.create', this._handleDraw.bind(this))
return this._container
}
// 当控件被移除时执行
onRemove () {
if (this._map && this._draw) {
this._map.removeControl(this._draw)
}
if (this._container?.parentNode) {
this._container.parentNode.removeChild(this._container)
}
this._map = null
}
// 启用多边形绘制模式
_enableDraw () {
if (this._draw && this._map) {
this._draw.changeMode('draw_polygon')
}
}
// 绘制完成的处理函数
_handleDraw (e) {
const features = this._draw.getAll()
if (features.features.length > 0) {
const coordinates = features.features.map(f => f.geometry.coordinates)
this._onDrawFinish(coordinates) // 通知外部最新绘制结果
}
}
// 删除当前选中的图形
_deleteSelected () {
if (this._draw) {
const selected = this._draw.getSelectedIds()
if (selected.length > 0) {
this._draw.delete(selected)
}
}
}
// 提交所有禁飞区数据到服务器
async _savePolygons () {
if (!this._draw) return
const allFeatures = this._draw.getAll()
// 提取所有 Polygon 类型图形的坐标(只取外环)
const polygons = allFeatures.features
.filter(f => f.geometry.type === 'Polygon')
.map(f => f.geometry.coordinates[0])
try {
const shopId = this._shopId
await setNoflyData(shopId, polygons)
} catch (error) {
Message.error('上传禁飞区数据时发生错误')
console.error(error)
}
// 调用保存成功回调
this._onSave(polygons)
}
}
// 限飞区控件类
export class RestrictflyControl {
constructor (options = {}) {
this._map = null
this._container = null
this._draw = null
this._shopId = options.shopId
this._defaultPolygons = options.defaultPolygons || []
this._defaultHeights = options.defaultHeights || []
this._restrictflyHeights = [] // 保存限高值
this._onSave = options.onSave || function () {}
}
onAdd (map) {
this._map = map
this._container = document.createElement('div')
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group'
// 绘制按钮
const drawButton = document.createElement('button')
drawButton.className = 'mapboxgl-ctrl-icon'
drawButton.type = 'button'
drawButton.innerHTML = '✏️'
drawButton.title = '绘制限飞区'
drawButton.onclick = () => this._enableDraw()
this._container.appendChild(drawButton)
// 保存按钮
const saveButton = document.createElement('button')
saveButton.className = 'mapboxgl-ctrl-icon'
saveButton.type = 'button'
saveButton.innerHTML = '💾'
saveButton.title = '保存限飞区'
saveButton.onclick = () => this._savePolygons()
this._container.appendChild(saveButton)
// 删除按钮
const deleteButton = document.createElement('button')
deleteButton.className = 'mapboxgl-ctrl-icon'
deleteButton.type = 'button'
deleteButton.innerHTML = '🗑️'
deleteButton.title = '删除选中图形'
deleteButton.onclick = () => this._deleteSelected()
this._container.appendChild(deleteButton)
// 初始化 MapboxDraw 控件(橙色样式)
this._draw = new MapboxDraw({
displayControlsDefault: false,
styles: [
{
id: 'gl-draw-polygon-fill',
type: 'fill',
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
paint: {
'fill-color': '#f83',
'fill-opacity': 0.3
}
},
{
id: 'gl-draw-polygon-stroke-active',
type: 'line',
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
paint: {
'line-color': '#f83',
'line-width': 2
}
},
{
id: 'gl-draw-polygon-and-line-vertex-halo-active',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 7,
'circle-color': '#fff'
}
},
{
id: 'gl-draw-polygon-and-line-vertex-active',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 5,
'circle-color': '#fff'
}
}
]
})
map.addControl(this._draw)
// 回显已有限飞区
if (this._defaultPolygons.length > 0) {
const features = this._defaultPolygons.map((coords, index) => ({
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Polygon', type: 'Polygon',
coordinates: [coords] coordinates: [coords]
}, },
properties: { properties: {
type: 'nofly' height: this._defaultHeights[index] || 0
} }
})) }))
this._draw.add({ type: 'FeatureCollection', features }) this._draw.set({
} type: 'FeatureCollection',
features
// 加载已有限飞区 })
if (this._restrictflyPolygons.length > 0) { this._restrictflyHeights = [...this._defaultHeights]
const features = this._restrictflyPolygons.map(coords => ({
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [coords]
},
properties: {
type: 'limit'
}
}))
this._draw.add({ type: 'FeatureCollection', features })
} }
// 监听绘制完成
map.on('draw.create', this._handleDraw.bind(this)) map.on('draw.create', this._handleDraw.bind(this))
return this._container return this._container
@ -343,31 +528,21 @@ export class NoFlyControl {
} }
} }
_handleDraw (e) { async _handleDraw (e) {
const features = e.features const features = this._draw.getAll()
if (features.length === 0) return if (features.features.length > 0) {
const newFeature = features.features[features.features.length - 1]
const type = this._activeMode === 'limit' ? 'limit' : 'nofly' try {
const height = await this._promptHeightInput()
features.forEach(f => { this._restrictflyHeights.push(parseFloat(height))
f.properties.type = type // 可选:在 feature 上记录高度
}) newFeature.properties.height = height
} catch (error) {
// 重新添加以确保类型被写入并触发样式 // 用户取消输入,移除刚才的图形
this._draw.delete(features.map(f => f.id)) this._draw.delete(newFeature.id)
this._draw.add({ Message.info('已取消限飞区域绘制')
type: 'FeatureCollection', }
features
})
const allCoords = this._draw.getAll().features
.filter(f => f.properties.type === type)
.map(f => f.geometry.coordinates)
if (type === 'nofly') {
this._onDrawFinish(allCoords)
} else {
this._onLimitFinish(allCoords)
} }
} }
@ -375,94 +550,39 @@ export class NoFlyControl {
if (this._draw) { if (this._draw) {
const selected = this._draw.getSelectedIds() const selected = this._draw.getSelectedIds()
if (selected.length > 0) { if (selected.length > 0) {
// 同时删除对应的限高值(只支持最后一个被删)
this._restrictflyHeights.pop()
this._draw.delete(selected) this._draw.delete(selected)
} }
} }
} }
async _savePolygons () { async _savePolygons () {
if (!this._draw) return
const allFeatures = this._draw.getAll() const allFeatures = this._draw.getAll()
const nofly = allFeatures.features const polygons = allFeatures.features
.filter(f => f.properties.type === 'nofly') .filter(f => f.geometry.type === 'Polygon')
.map(f => f.geometry.coordinates[0])
const limit = allFeatures.features
.filter(f => f.properties.type === 'limit')
.map(f => f.geometry.coordinates[0]) .map(f => f.geometry.coordinates[0])
try { try {
const shopId = this._shopId await setRestrictflyData(this._shopId, polygons, this._restrictflyHeights)
await setNoflyData(shopId, nofly, limit) this._onSave({ polygons, heights: this._restrictflyHeights })
this._onSave(nofly, limit)
} catch (error) { } catch (error) {
Message.error('上传数据失败') Message.error('保存限飞区失败')
console.error(error) console.error(error)
} }
} }
_getDrawStyles () { // Element UI 弹窗输入限高
const noflyColor = '#ff3333' _promptHeightInput () {
const limitColor = '#ff8833' return MessageBox.prompt('请输入该区域的限飞高度(单位:米)', '限高设置', {
confirmButtonText: '确定',
return [ cancelButtonText: '取消',
// 区域填充样式 inputPattern: /^\d+(\.\d+)?$/,
{ inputErrorMessage: '请输入有效的数字'
id: 'custom-polygon-fill', }).then(({ value }) => value)
type: 'fill',
filter: ['all', ['==', '$type', 'Polygon']],
paint: {
'fill-color': [
'case',
['==', ['get', 'type'], 'nofly'], noflyColor,
['==', ['get', 'type'], 'limit'], limitColor,
limitColor // 默认颜色
],
'fill-opacity': 0.3
}
},
// 区域边框线(不区分 static / active
{
id: 'custom-polygon-stroke',
type: 'line',
filter: ['all', ['==', '$type', 'Polygon']],
paint: {
'line-color': [
'case',
['==', ['get', 'type'], 'nofly'], noflyColor,
['==', ['get', 'type'], 'limit'], limitColor,
limitColor
],
'line-width': 2,
'line-dasharray': [2, 2]
}
},
// 顶点圆点
{
id: 'custom-vertex-point',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 5,
'circle-color': [
'case',
['==', ['get', 'type'], 'nofly'], noflyColor,
['==', ['get', 'type'], 'limit'], limitColor,
limitColor
]
}
},
// 顶点外光环
{
id: 'custom-vertex-halo',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 7,
'circle-color': '#FFFFFF'
}
}
]
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="h-100"> <div class="h-100">
<map-box v-if="showMapbox" :key="mapboxKey" ref="mapbox" :enableNofly="true" /> <map-box v-if="showMapbox" :key="mapboxKey" ref="mapbox" :enableRestrictfly="true" />
</div> </div>
</template> </template>
@ -8,7 +8,7 @@
import MapBox from '@/components/MapBox' import MapBox from '@/components/MapBox'
export default { export default {
name: 'Nofly', name: 'Restrictfly',
components: { components: {
MapBox MapBox
}, },