Compare commits
7 Commits
547688f96f
...
4c41c3a555
| Author | SHA1 | Date |
|---|---|---|
|
|
4c41c3a555 | 5 days ago |
|
|
06f7245f7a | 5 days ago |
|
|
f03181867d | 5 days ago |
|
|
8f1694ea51 | 5 days ago |
|
|
8eaad72df3 | 6 days ago |
|
|
696e395a02 | 6 days ago |
|
|
69fc744994 | 6 days ago |
20 changed files with 1771 additions and 449 deletions
File diff suppressed because it is too large
@ -0,0 +1,402 @@ |
|||
<template> |
|||
<el-dialog |
|||
:title="$t('generateAirspace.title')" |
|||
:visible.sync="visible" |
|||
width="540px" |
|||
append-to-body |
|||
:close-on-click-modal="false" |
|||
@closed="onClosed" |
|||
> |
|||
<el-form ref="formRef" :model="form" label-width="150px" size="small"> |
|||
<el-form-item :label="$t('generateAirspace.shapeType')"> |
|||
<el-select v-model="form.shapeType" style="width: 100%;"> |
|||
<el-option :label="$t('generateAirspace.polygon')" value="polygon" /> |
|||
<el-option :label="$t('generateAirspace.rectangle')" value="rectangle" /> |
|||
<el-option :label="$t('generateAirspace.circle')" value="circle" /> |
|||
<el-option :label="$t('generateAirspace.sector')" value="sector" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.name')"> |
|||
<el-input v-model="form.name" :placeholder="$t('generateAirspace.namePlaceholder')" clearable /> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.color')"> |
|||
<el-color-picker v-model="form.color" show-alpha /> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.borderWidth')"> |
|||
<el-input-number v-model="form.width" :min="1" :max="20" :step="1" controls-position="right" style="width: 100%;" /> |
|||
</el-form-item> |
|||
|
|||
<template v-if="form.shapeType === 'polygon'"> |
|||
<el-form-item :label="$t('generateAirspace.vertices')" required> |
|||
<el-input |
|||
v-model="form.polygonText" |
|||
type="textarea" |
|||
:rows="6" |
|||
:placeholder="$t('generateAirspace.polygonPlaceholder')" |
|||
/> |
|||
</el-form-item> |
|||
</template> |
|||
|
|||
<template v-else-if="form.shapeType === 'rectangle'"> |
|||
<el-form-item :label="$t('generateAirspace.rectangleSwCorner')" required> |
|||
<el-input v-model="form.swCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable /> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.rectangleNeCorner')" required> |
|||
<el-input v-model="form.neCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable /> |
|||
</el-form-item> |
|||
</template> |
|||
|
|||
<template v-else-if="form.shapeType === 'circle'"> |
|||
<el-form-item :label="$t('generateAirspace.centerLonLat')" required> |
|||
<el-input v-model="form.centerCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable /> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.radiusM')" required> |
|||
<div class="input-with-unit"> |
|||
<el-input-number |
|||
v-model="form.radius" |
|||
:min="0.1" |
|||
:max="2000" |
|||
:step="1" |
|||
:precision="2" |
|||
controls-position="right" |
|||
class="input-with-unit__num" |
|||
/> |
|||
<span class="input-with-unit__suffix">{{ $t('generateAirspace.radiusUnit') }}</span> |
|||
</div> |
|||
</el-form-item> |
|||
</template> |
|||
|
|||
<template v-else-if="form.shapeType === 'sector'"> |
|||
<el-form-item :label="$t('generateAirspace.centerLonLat')" required> |
|||
<el-input v-model="form.centerCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable /> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.radiusM')" required> |
|||
<div class="input-with-unit"> |
|||
<el-input-number |
|||
v-model="form.radius" |
|||
:min="0.1" |
|||
:max="2000" |
|||
:step="1" |
|||
:precision="2" |
|||
controls-position="right" |
|||
class="input-with-unit__num" |
|||
/> |
|||
<span class="input-with-unit__suffix">{{ $t('generateAirspace.radiusUnit') }}</span> |
|||
</div> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.startBearing')" required> |
|||
<el-input-number v-model="form.startBearing" :min="0" :max="360" :precision="2" :step="1" controls-position="right" style="width: 100%;" /> |
|||
</el-form-item> |
|||
<el-form-item :label="$t('generateAirspace.endBearing')" required> |
|||
<el-input-number v-model="form.endBearing" :min="0" :max="360" :precision="2" :step="1" controls-position="right" style="width: 100%;" /> |
|||
</el-form-item> |
|||
</template> |
|||
</el-form> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-button @click="visible = false">{{ $t('generateAirspace.cancel') }}</el-button> |
|||
<el-button type="primary" @click="submit">{{ $t('generateAirspace.confirm') }}</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script> |
|||
function parseNum(v) { |
|||
if (v === null || v === undefined || v === '') return NaN |
|||
const n = typeof v === 'number' ? v : parseFloat(String(v).trim().replace(/,/g, '')) |
|||
return n |
|||
} |
|||
|
|||
/** 从颜色选择器返回值解析填充不透明度(与 Cesium importEntity 的 data.opacity 一致) */ |
|||
function extractOpacityFromColor(colorStr) { |
|||
if (colorStr == null || colorStr === '') return 0 |
|||
const s = String(colorStr).trim() |
|||
const rgba = s.match( |
|||
/^rgba\s*\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/i |
|||
) |
|||
if (rgba) return Math.max(0, Math.min(1, parseFloat(rgba[1]))) |
|||
if (/^rgb\s*\(/i.test(s) && !/^rgba/i.test(s)) return 1 |
|||
const hsla = s.match( |
|||
/^hsla\s*\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*[\d.]+%\s*,\s*([\d.]+)\s*\)/i |
|||
) |
|||
if (hsla) return Math.max(0, Math.min(1, parseFloat(hsla[1]))) |
|||
if (s[0] === '#' && s.length === 9) { |
|||
const a = parseInt(s.slice(7, 9), 16) / 255 |
|||
return Number.isFinite(a) ? Math.max(0, Math.min(1, a)) : 0 |
|||
} |
|||
if (s[0] === '#') return 0 |
|||
return 0 |
|||
} |
|||
|
|||
/** 解析单个角点:(经度,纬度) 或(经度,纬度),也支持无括号的 经度,纬度 */ |
|||
function parseLonLatPair(text) { |
|||
const raw = String(text == null ? '' : text).trim() |
|||
if (!raw) return null |
|||
const parenPair = |
|||
/[((]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[,,]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[))]/ |
|||
const m = raw.match(parenPair) |
|||
if (m) { |
|||
const lng = parseFloat(m[1]) |
|||
const lat = parseFloat(m[2]) |
|||
if (Number.isFinite(lng) && Number.isFinite(lat)) return { lng, lat } |
|||
} |
|||
const cleaned = raw.replace(/^[((]\s*|\s*[))]$/g, '').trim() |
|||
const parts = cleaned.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean) |
|||
if (parts.length >= 2) { |
|||
const lng = parseNum(parts[0]) |
|||
const lat = parseNum(parts[1]) |
|||
if (Number.isFinite(lng) && Number.isFinite(lat)) return { lng, lat } |
|||
} |
|||
return null |
|||
} |
|||
|
|||
export default { |
|||
name: 'GenerateAirspaceDialog', |
|||
props: { |
|||
value: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
form: { |
|||
shapeType: 'polygon', |
|||
name: '', |
|||
color: 'rgba(0, 138, 255, 0)', |
|||
width: 2, |
|||
polygonText: '', |
|||
swCorner: '', |
|||
neCorner: '', |
|||
centerCorner: '', |
|||
radius: 50, |
|||
startBearing: 0, |
|||
endBearing: 90 |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
visible: { |
|||
get() { |
|||
return this.value |
|||
}, |
|||
set(v) { |
|||
this.$emit('input', v) |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
onClosed() { |
|||
this.$emit('closed') |
|||
}, |
|||
resetForm() { |
|||
this.form = { |
|||
shapeType: 'polygon', |
|||
name: '', |
|||
color: 'rgba(0, 138, 255, 0)', |
|||
width: 2, |
|||
polygonText: '', |
|||
swCorner: '', |
|||
neCorner: '', |
|||
centerCorner: '', |
|||
radius: 50, |
|||
startBearing: 0, |
|||
endBearing: 90 |
|||
} |
|||
if (this.$refs.formRef) this.$refs.formRef.clearValidate() |
|||
}, |
|||
/** |
|||
* 支持多种写法: |
|||
* - 每行一对:经度,纬度(可中英文逗号、空格) |
|||
* - 一行多对:用顿号/分号分隔,如 121.47,31.23、120.15,30.28 |
|||
* - 带括号:(121.47, 31.23)、(120.15, 30.28)(全角/半角括号与逗号均可) |
|||
*/ |
|||
parsePolygonPoints(text) { |
|||
const raw = String(text || '').trim() |
|||
if (!raw) return [] |
|||
const points = [] |
|||
const parenPair = |
|||
/[((]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[,,]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[))]/g |
|||
let m |
|||
while ((m = parenPair.exec(raw)) !== null) { |
|||
const lng = parseFloat(m[1]) |
|||
const lat = parseFloat(m[2]) |
|||
if (Number.isFinite(lng) && Number.isFinite(lat)) points.push({ lng, lat }) |
|||
} |
|||
if (points.length > 0) return points |
|||
|
|||
const lines = raw.split(/\r?\n/).map(l => l.trim()).filter(Boolean) |
|||
for (const line of lines) { |
|||
const segments = line.split(/[、;;]+/).map(s => s.trim()).filter(Boolean) |
|||
const segs = segments.length > 0 ? segments : [line] |
|||
for (const seg of segs) { |
|||
const cleaned = seg.replace(/^[((]\s*|\s*[))]$/g, '').trim() |
|||
const parts = cleaned.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean) |
|||
if (parts.length < 2) continue |
|||
const lng = parseNum(parts[0]) |
|||
const lat = parseNum(parts[1]) |
|||
if (Number.isFinite(lng) && Number.isFinite(lat)) points.push({ lng, lat }) |
|||
} |
|||
} |
|||
return points |
|||
}, |
|||
/** 方位角:正北为 0°、顺时针增大,转为 importEntity 扇形所用的数学角(与地图绘制一致) */ |
|||
bearingDegToSectorAngle(bearingDeg) { |
|||
const b = (bearingDeg % 360 + 360) % 360 |
|||
return Math.PI / 2 - (b * Math.PI) / 180 |
|||
}, |
|||
buildPayload() { |
|||
const color = this.form.color || 'rgba(0, 138, 255, 0)' |
|||
const opacity = extractOpacityFromColor(color) |
|||
const width = this.form.width != null ? this.form.width : 2 |
|||
const borderColor = color |
|||
const name = (this.form.name || '').trim() |
|||
const labelBase = { |
|||
polygon: this.$t('generateAirspace.polygon'), |
|||
rectangle: this.$t('generateAirspace.rectangle'), |
|||
circle: this.$t('generateAirspace.circle'), |
|||
sector: this.$t('generateAirspace.sector') |
|||
} |
|||
const label = name || labelBase[this.form.shapeType] || this.$t('generateAirspace.defaultLabel') |
|||
|
|||
switch (this.form.shapeType) { |
|||
case 'polygon': { |
|||
const points = this.parsePolygonPoints(this.form.polygonText) |
|||
if (points.length < 3) { |
|||
this.$message.error(this.$t('generateAirspace.errPolygonPoints')) |
|||
return null |
|||
} |
|||
return { |
|||
type: 'polygon', |
|||
label, |
|||
color, |
|||
data: { |
|||
points, |
|||
opacity, |
|||
width, |
|||
borderColor, |
|||
name |
|||
} |
|||
} |
|||
} |
|||
case 'rectangle': { |
|||
const sw = parseLonLatPair(this.form.swCorner) |
|||
const ne = parseLonLatPair(this.form.neCorner) |
|||
if (!sw || !ne) { |
|||
this.$message.error(this.$t('generateAirspace.errRectNumbers')) |
|||
return null |
|||
} |
|||
const w = Math.min(sw.lng, ne.lng) |
|||
const e = Math.max(sw.lng, ne.lng) |
|||
const s = Math.min(sw.lat, ne.lat) |
|||
const n = Math.max(sw.lat, ne.lat) |
|||
return { |
|||
type: 'rectangle', |
|||
label, |
|||
color, |
|||
data: { |
|||
coordinates: { west: w, south: s, east: e, north: n }, |
|||
points: [ |
|||
{ lng: w, lat: s }, |
|||
{ lng: e, lat: n } |
|||
], |
|||
opacity, |
|||
width, |
|||
borderColor, |
|||
name |
|||
} |
|||
} |
|||
} |
|||
case 'circle': { |
|||
const c = parseLonLatPair(this.form.centerCorner) |
|||
const radiusKm = Number(this.form.radius) |
|||
const radius = radiusKm * 1000 |
|||
if (!c || !Number.isFinite(radiusKm) || radiusKm <= 0) { |
|||
this.$message.error(this.$t('generateAirspace.errCircle')) |
|||
return null |
|||
} |
|||
const lng = c.lng |
|||
const lat = c.lat |
|||
return { |
|||
type: 'circle', |
|||
label, |
|||
color, |
|||
data: { |
|||
center: { lng, lat }, |
|||
radius, |
|||
opacity, |
|||
width, |
|||
borderColor, |
|||
name |
|||
} |
|||
} |
|||
} |
|||
case 'sector': { |
|||
const c = parseLonLatPair(this.form.centerCorner) |
|||
const radiusKm = Number(this.form.radius) |
|||
const radius = radiusKm * 1000 |
|||
const sb = Number(this.form.startBearing) |
|||
const eb = Number(this.form.endBearing) |
|||
if (!c || !Number.isFinite(radiusKm) || radiusKm <= 0) { |
|||
this.$message.error(this.$t('generateAirspace.errSector')) |
|||
return null |
|||
} |
|||
const lng = c.lng |
|||
const lat = c.lat |
|||
if (!Number.isFinite(sb) || !Number.isFinite(eb)) { |
|||
this.$message.error(this.$t('generateAirspace.errBearing')) |
|||
return null |
|||
} |
|||
const startAngle = this.bearingDegToSectorAngle(sb) |
|||
const endAngle = this.bearingDegToSectorAngle(eb) |
|||
return { |
|||
type: 'sector', |
|||
label, |
|||
color, |
|||
data: { |
|||
center: { lng, lat }, |
|||
radius, |
|||
startAngle, |
|||
endAngle, |
|||
opacity, |
|||
width, |
|||
borderColor, |
|||
name |
|||
} |
|||
} |
|||
} |
|||
default: |
|||
return null |
|||
} |
|||
}, |
|||
submit() { |
|||
const payload = this.buildPayload() |
|||
if (!payload) return |
|||
this.$emit('confirm', payload) |
|||
this.visible = false |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.input-with-unit { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
width: 100%; |
|||
} |
|||
.input-with-unit__num { |
|||
flex: 1; |
|||
min-width: 0; |
|||
} |
|||
.input-with-unit__suffix { |
|||
flex-shrink: 0; |
|||
font-size: 13px; |
|||
color: #606266; |
|||
} |
|||
.dialog-footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 8px; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue