|
|
|
|
<template>
|
|
|
|
|
<div class="context-menu" v-if="visible" :style="positionStyle">
|
|
|
|
|
<!-- 重叠/接近时:切换选择其他图形 -->
|
|
|
|
|
<div class="menu-section" v-if="pickList && pickList.length > 1">
|
|
|
|
|
<div class="menu-item" @click="$emit('switch-pick')">
|
|
|
|
|
<span class="menu-icon">🔄</span>
|
|
|
|
|
<span>切换选择 ({{ (pickIndex || 0) + 1 }}/{{ pickList.length }})</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 框选多平台:仅批量删除 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData && entityData.type === 'platformBoxSelection'">
|
|
|
|
|
<div class="menu-title">框选平台({{ entityData.count }} 个)</div>
|
|
|
|
|
<div class="menu-item" @click="handleDeleteBoxSelection">
|
|
|
|
|
<span class="menu-icon">🗑️</span>
|
|
|
|
|
<span>删除全部框选平台</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="menu-section" v-if="(!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')) && (!entityData || entityData.type !== 'platformBoxSelection')">
|
|
|
|
|
<div class="menu-item" @click="handleDelete">
|
|
|
|
|
<span class="menu-icon">🗑️</span>
|
|
|
|
|
<span>删除</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 航点:编辑、向前/向后增加航点(可选默认/定时,定速点仅能通过航点编辑切换) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData && entityData.type === 'routeWaypoint'">
|
|
|
|
|
<div class="menu-title">航点</div>
|
|
|
|
|
<div class="menu-item" @click="handleEditWaypoint">
|
|
|
|
|
<span class="menu-icon">📝</span>
|
|
|
|
|
<span>编辑航点</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" v-if="isHoldWaypoint" @click="openEditHoldSpeedDialog">
|
|
|
|
|
<span class="menu-icon">✈️</span>
|
|
|
|
|
<span>编辑盘旋速度</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click.stop="toggleAddWaypointExpand('before')">
|
|
|
|
|
<span class="menu-icon">⬅️</span>
|
|
|
|
|
<span>向前增加航点</span>
|
|
|
|
|
<span class="menu-expand-icon">{{ expandedAddWaypoint === 'before' ? '▼' : '▶' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="expandedAddWaypoint === 'before'" class="menu-sub-group">
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleAddWaypointWithMode('before', null)">
|
|
|
|
|
<span class="menu-icon-sub">·</span><span>默认航点</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleAddWaypointWithMode('before', 'fixed_time')">
|
|
|
|
|
<span class="menu-icon-sub">·</span><span>定时点</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click.stop="toggleAddWaypointExpand('after')">
|
|
|
|
|
<span class="menu-icon">➡️</span>
|
|
|
|
|
<span>向后增加航点</span>
|
|
|
|
|
<span class="menu-expand-icon">{{ expandedAddWaypoint === 'after' ? '▼' : '▶' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="expandedAddWaypoint === 'after'" class="menu-sub-group">
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleAddWaypointWithMode('after', null)">
|
|
|
|
|
<span class="menu-icon-sub">·</span><span>默认航点</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleAddWaypointWithMode('after', 'fixed_time')">
|
|
|
|
|
<span class="menu-icon-sub">·</span><span>定时点</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleToggleWaypointHold">
|
|
|
|
|
<span class="menu-icon">🔄</span>
|
|
|
|
|
<span>切换盘旋航点</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 定时/定速点参数弹窗:选择定时或定速后填写固定时间或速度 -->
|
|
|
|
|
<el-dialog
|
|
|
|
|
:title="addWaypointDialogTitle"
|
|
|
|
|
:visible.sync="showAddWaypointDialog"
|
|
|
|
|
width="360px"
|
|
|
|
|
append-to-body
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
>
|
|
|
|
|
<el-form :model="addWaypointForm" label-width="120px" size="small">
|
|
|
|
|
<el-form-item v-if="addWaypointDialogSegmentMode === 'fixed_time'" label="距上一航点(分)">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="addWaypointForm.segmentTargetMinutes"
|
|
|
|
|
:min="0.1"
|
|
|
|
|
:max="9999"
|
|
|
|
|
:precision="1"
|
|
|
|
|
placeholder="从上一航点飞至此点的飞行时间"
|
|
|
|
|
style="width:100%"
|
|
|
|
|
/>
|
|
|
|
|
<div class="form-tip-small">飞行时间,如10表示飞10分钟到达。相对K时=上一航点+10,速度将根据距离自动计算</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item v-else-if="addWaypointDialogSegmentMode === 'fixed_speed'" label="定速(km/h)">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="addWaypointForm.segmentTargetSpeed"
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="1"
|
|
|
|
|
placeholder="本航段使用的固定速度"
|
|
|
|
|
style="width:100%"
|
|
|
|
|
/>
|
|
|
|
|
<div class="form-tip-small">上一航点到新航点之间使用的固定速度</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
<el-button @click="showAddWaypointDialog = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="confirmAddWaypointParams">确定</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
title="编辑盘旋速度"
|
|
|
|
|
:visible.sync="showHoldSpeedDialog"
|
|
|
|
|
width="360px"
|
|
|
|
|
append-to-body
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
>
|
|
|
|
|
<el-form :model="holdSpeedForm" label-width="120px" size="small">
|
|
|
|
|
<el-form-item label="盘旋速度(km/h)">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model.number="holdSpeedForm.speed"
|
|
|
|
|
:min="100"
|
|
|
|
|
:max="2000"
|
|
|
|
|
:precision="1"
|
|
|
|
|
:step="10"
|
|
|
|
|
style="width:100%"
|
|
|
|
|
/>
|
|
|
|
|
<div class="form-tip-small">该值用于盘旋段推演速度,默认 800 km/h。</div>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
<el-button @click="showHoldSpeedDialog = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="confirmEditHoldSpeed">确定</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- 航线:上锁/解锁、复制、单条航线推演(航点右键时也显示,用 routeId) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData && (entityData.type === 'route' || entityData.type === 'routeWaypoint')">
|
|
|
|
|
<div class="menu-title">航线编辑</div>
|
|
|
|
|
<div class="menu-item" @click="handleToggleRouteLock">
|
|
|
|
|
<span class="menu-icon">{{ isRouteLocked ? '🔓' : '🔒' }}</span>
|
|
|
|
|
<span>{{ isRouteLocked ? '解锁' : '上锁' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleCopyRoute">
|
|
|
|
|
<span class="menu-icon">📋</span>
|
|
|
|
|
<span>复制</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="!isRouteLocked" class="menu-item" @click="handleRouteSegmentSplit">
|
|
|
|
|
<span class="menu-icon">✂️</span>
|
|
|
|
|
<span>拆分航段</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="!isRouteLocked" class="menu-item" @click="handleRouteSegmentCopy">
|
|
|
|
|
<span class="menu-icon">📄</span>
|
|
|
|
|
<span>拆分复制</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleSingleRouteDeduction">
|
|
|
|
|
<span class="menu-icon">▶️</span>
|
|
|
|
|
<span>单条航线推演</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 航线上飞机:显示/隐藏/编辑标牌、编辑平台 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData && entityData.type === 'routePlatform'">
|
|
|
|
|
<div class="menu-title">飞机标牌</div>
|
|
|
|
|
<div class="menu-item" @click="handleToggleRouteLabel">
|
|
|
|
|
<span class="menu-icon">🏷️</span>
|
|
|
|
|
<span>{{ entityData.labelVisible ? '隐藏标牌' : '显示标牌' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleEditPlatform">
|
|
|
|
|
<span class="menu-icon">📝</span>
|
|
|
|
|
<span>编辑</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleDetectionZone">
|
|
|
|
|
<span class="menu-icon">🔍</span>
|
|
|
|
|
<span>探测区</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handlePowerZone">
|
|
|
|
|
<span class="menu-icon">⭕</span>
|
|
|
|
|
<span>威力区</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleToggleDetectionZone">
|
|
|
|
|
<span class="menu-icon">{{ detectionZoneVisible ? '👁' : '👁🗨' }}</span>
|
|
|
|
|
<span>{{ detectionZoneVisible ? '隐藏探测区' : '显示探测区' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleTogglePowerZone">
|
|
|
|
|
<span class="menu-icon">{{ powerZoneVisible ? '👁' : '👁🗨' }}</span>
|
|
|
|
|
<span>{{ powerZoneVisible ? '隐藏威力区' : '显示威力区' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleLaunchMissile">
|
|
|
|
|
<span class="menu-icon">🚀</span>
|
|
|
|
|
<span>发射导弹</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 线段特有选项 -->
|
|
|
|
|
<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>
|
|
|
|
|
<span>颜色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 颜色选择器 -->
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('color', color)"
|
|
|
|
|
:class="{ active: entityData.color === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleWidthPicker">
|
|
|
|
|
<span class="menu-icon">📏</span>
|
|
|
|
|
<span>线宽</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.width }}px</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 线宽选择器 -->
|
|
|
|
|
<div class="width-picker-container" v-if="showWidthPicker">
|
|
|
|
|
<div class="width-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="width in presetWidths"
|
|
|
|
|
:key="width"
|
|
|
|
|
class="width-item"
|
|
|
|
|
@click="selectWidth(width)"
|
|
|
|
|
:class="{ active: entityData.width === width }"
|
|
|
|
|
>
|
|
|
|
|
{{ width }}px
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleBearingTypeMenu">
|
|
|
|
|
<span class="menu-icon">🧭</span>
|
|
|
|
|
<span>方位角类型</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.bearingType === 'magnetic' ? '磁方位' : '真方位' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 方位角类型选择菜单 -->
|
|
|
|
|
<div class="sub-menu" v-if="showBearingTypeMenu">
|
|
|
|
|
<div class="sub-menu-item" @click="selectBearingType('true')" :class="{ active: entityData.bearingType === 'true' }">
|
|
|
|
|
<span>真方位</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sub-menu-item" @click="selectBearingType('magnetic')" :class="{ active: entityData.bearingType === 'magnetic' }">
|
|
|
|
|
<span>磁方位</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<template v-if="toolMode === 'ranging'">
|
|
|
|
|
<div class="menu-item" @click="toggleRangingUnitMenu">
|
|
|
|
|
<span class="menu-icon">📏</span>
|
|
|
|
|
<span>距离单位</span>
|
|
|
|
|
<span class="menu-value">{{ rangingDistanceUnit === 'nm' ? '海里' : '公里' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sub-menu" v-if="showRangingUnitMenu">
|
|
|
|
|
<div class="sub-menu-item" @click="selectRangingUnit('km')" :class="{ active: rangingDistanceUnit === 'km' }">
|
|
|
|
|
<span>公里(km)</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sub-menu-item" @click="selectRangingUnit('nm')" :class="{ active: rangingDistanceUnit === 'nm' }">
|
|
|
|
|
<span>海里(1 海里 = 1852 米)</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 点特有选项 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'point'">
|
|
|
|
|
<div class="menu-title">点属性</div>
|
|
|
|
|
<div class="menu-item" @click="toggleColorPicker('color')">
|
|
|
|
|
<span class="menu-icon">🎨</span>
|
|
|
|
|
<span>颜色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 颜色选择器 -->
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('color', color)"
|
|
|
|
|
:class="{ active: entityData.color === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleSizePicker">
|
|
|
|
|
<span class="menu-icon">🔵</span>
|
|
|
|
|
<span>大小</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.size }}px</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 大小选择器 -->
|
|
|
|
|
<div class="size-picker-container" v-if="showSizePicker">
|
|
|
|
|
<div class="size-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="size in presetSizes"
|
|
|
|
|
:key="size"
|
|
|
|
|
class="size-item"
|
|
|
|
|
@click="selectSize(size)"
|
|
|
|
|
:class="{ active: entityData.size === size }"
|
|
|
|
|
>
|
|
|
|
|
{{ size }}px
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 空域图形:调整位置 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'auxiliaryLine' || entityData.type === 'arrow'">
|
|
|
|
|
<div class="menu-title">位置</div>
|
|
|
|
|
<div class="menu-item" @click="handleAdjustPosition">
|
|
|
|
|
<span class="menu-icon">📍</span>
|
|
|
|
|
<span>调整位置</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 空域图形:命名(多边形、矩形、圆形、扇形) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector'">
|
|
|
|
|
<div class="menu-title">命名</div>
|
|
|
|
|
<div class="menu-item" @click="handleEditAirspaceName">
|
|
|
|
|
<span class="menu-icon">📝</span>
|
|
|
|
|
<span>命名</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.name || '(未命名)' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 多边形特有选项 -->
|
|
|
|
|
<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="editName" v-if="entityData.type === 'powerZone'">
|
|
|
|
|
<span class="menu-icon">📝</span>
|
|
|
|
|
<span>名称</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.name || '' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleColorPicker('color')">
|
|
|
|
|
<span class="menu-icon">🎨</span>
|
|
|
|
|
<span>填充色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 颜色选择器 -->
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('color', color)"
|
|
|
|
|
:class="{ active: entityData.color === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleOpacityPicker">
|
|
|
|
|
<span class="menu-icon">🌫️</span>
|
|
|
|
|
<span>透明度</span>
|
|
|
|
|
<span class="menu-value">{{ Math.round(entityData.opacity * 100) }}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 透明度选择器 -->
|
|
|
|
|
<div class="opacity-picker-container" v-if="showOpacityPicker">
|
|
|
|
|
<div class="opacity-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="opacity in presetOpacities"
|
|
|
|
|
:key="opacity"
|
|
|
|
|
class="opacity-item"
|
|
|
|
|
@click="selectOpacity(opacity)"
|
|
|
|
|
:class="{ active: Math.round(entityData.opacity * 100) === Math.round(opacity * 100) }"
|
|
|
|
|
>
|
|
|
|
|
{{ Math.round(opacity * 100) }}%
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleColorPicker('borderColor')">
|
|
|
|
|
<span class="menu-icon">🖌️</span>
|
|
|
|
|
<span>边框色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.borderColor || entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 边框颜色选择器 -->
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'borderColor'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('borderColor', color)"
|
|
|
|
|
:class="{ active: (entityData.borderColor || entityData.color) === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleWidthPicker">
|
|
|
|
|
<span class="menu-icon">📏</span>
|
|
|
|
|
<span>边框宽</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.width }}px</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 边框宽度选择器 -->
|
|
|
|
|
<div class="width-picker-container" v-if="showWidthPicker">
|
|
|
|
|
<div class="width-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="width in presetWidths"
|
|
|
|
|
:key="width"
|
|
|
|
|
class="width-item"
|
|
|
|
|
@click="selectWidth(width)"
|
|
|
|
|
:class="{ active: entityData.width === width }"
|
|
|
|
|
>
|
|
|
|
|
{{ width }}px
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 辅助线特有选项 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'auxiliaryLine'">
|
|
|
|
|
<div class="menu-title">辅助线属性</div>
|
|
|
|
|
<div class="menu-item" @click="toggleColorPicker('color')">
|
|
|
|
|
<span class="menu-icon">🎨</span>
|
|
|
|
|
<span>颜色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('color', color)"
|
|
|
|
|
:class="{ active: entityData.color === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleWidthPicker">
|
|
|
|
|
<span class="menu-icon">📏</span>
|
|
|
|
|
<span>线宽</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.width }}px</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="width-picker-container" v-if="showWidthPicker">
|
|
|
|
|
<div class="width-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="width in presetWidths"
|
|
|
|
|
:key="width"
|
|
|
|
|
class="width-item"
|
|
|
|
|
@click="selectWidth(width)"
|
|
|
|
|
:class="{ active: entityData.width === width }"
|
|
|
|
|
>
|
|
|
|
|
{{ width }}px
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 箭头特有选项 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'arrow'">
|
|
|
|
|
<div class="menu-title">箭头属性</div>
|
|
|
|
|
<div class="menu-item" @click="toggleColorPicker('color')">
|
|
|
|
|
<span class="menu-icon">🎨</span>
|
|
|
|
|
<span>颜色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 颜色选择器 -->
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('color', color)"
|
|
|
|
|
:class="{ active: entityData.color === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleWidthPicker">
|
|
|
|
|
<span class="menu-icon">📏</span>
|
|
|
|
|
<span>线宽</span>
|
|
|
|
|
<span class="menu-value">{{ entityData.width }}px</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 线宽选择器 -->
|
|
|
|
|
<div class="width-picker-container" v-if="showWidthPicker">
|
|
|
|
|
<div class="width-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="width in presetWidths"
|
|
|
|
|
:key="width"
|
|
|
|
|
class="width-item"
|
|
|
|
|
@click="selectWidth(width)"
|
|
|
|
|
:class="{ active: entityData.width === width }"
|
|
|
|
|
>
|
|
|
|
|
{{ width }}px
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 文本特有选项 -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'text'">
|
|
|
|
|
<div class="menu-title">文本属性</div>
|
|
|
|
|
<div class="menu-item" @click="toggleColorPicker('color')">
|
|
|
|
|
<span class="menu-icon">🎨</span>
|
|
|
|
|
<span>文字颜色</span>
|
|
|
|
|
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 颜色选择器 -->
|
|
|
|
|
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
|
|
|
|
|
<div class="color-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="color in presetColors"
|
|
|
|
|
:key="color"
|
|
|
|
|
class="color-item"
|
|
|
|
|
:style="{backgroundColor: color}"
|
|
|
|
|
@click="selectColor('color', color)"
|
|
|
|
|
:class="{ active: entityData.color === color }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="showFontPicker">
|
|
|
|
|
<span class="menu-icon">📝</span>
|
|
|
|
|
<span>字体</span>
|
|
|
|
|
<span class="menu-value">{{ getFontName(entityData.font) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="toggleFontSizePicker">
|
|
|
|
|
<span class="menu-icon">🔤</span>
|
|
|
|
|
<span>字号</span>
|
|
|
|
|
<span class="menu-value">{{ getFontSize(entityData.font) }}px</span>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 字号选择器 -->
|
|
|
|
|
<div class="font-size-picker-container" v-if="showFontSizePicker">
|
|
|
|
|
<div class="font-size-grid">
|
|
|
|
|
<div
|
|
|
|
|
v-for="size in presetFontSizes"
|
|
|
|
|
:key="size"
|
|
|
|
|
class="font-size-item"
|
|
|
|
|
@click="selectFontSize(size)"
|
|
|
|
|
:class="{ active: getFontSize(entityData.font) === size.toString() }"
|
|
|
|
|
>
|
|
|
|
|
{{ size }}px
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 平台图标(拖拽到地图的图标)特有选项(白板平台不显示探测区/威力区/航线) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'platformIcon' && !entityData.isWhiteboard">
|
|
|
|
|
<div class="menu-title">平台图标</div>
|
|
|
|
|
<div class="menu-item" @click="handleShowTransformBox">
|
|
|
|
|
<span class="menu-icon">🔄</span>
|
|
|
|
|
<span>显示伸缩框</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleDetectionZonePlatform">
|
|
|
|
|
<span class="menu-icon">🔍</span>
|
|
|
|
|
<span>探测区</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handlePowerZonePlatform">
|
|
|
|
|
<span class="menu-icon">⭕</span>
|
|
|
|
|
<span>威力区</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleToggleDetectionZone">
|
|
|
|
|
<span class="menu-icon">{{ detectionZoneVisible ? '👁' : '👁🗨' }}</span>
|
|
|
|
|
<span>{{ detectionZoneVisible ? '隐藏探测区' : '显示探测区' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleTogglePowerZone">
|
|
|
|
|
<span class="menu-icon">{{ powerZoneVisible ? '👁' : '👁🗨' }}</span>
|
|
|
|
|
<span>{{ powerZoneVisible ? '隐藏威力区' : '显示威力区' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-title" style="margin-top:8px;">航线</div>
|
|
|
|
|
<div class="menu-item" @click="handleStartRouteBeforePlatform">
|
|
|
|
|
<span class="menu-icon">⬅️</span>
|
|
|
|
|
<span>在此之前插入航线</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="menu-item" @click="handleStartRouteAfterPlatform">
|
|
|
|
|
<span class="menu-icon">➡️</span>
|
|
|
|
|
<span>在此之后插入航线</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 探测区(单个区) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData && entityData.type === 'detectionZone'">
|
|
|
|
|
<div class="menu-title">探测区</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleToggleDetectionZone">
|
|
|
|
|
<span class="menu-icon">{{ detectionZoneVisible ? '👁' : '👁🗨' }}</span>
|
|
|
|
|
<span>{{ detectionZoneVisible ? '隐藏该探测区' : '显示该探测区' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 威力区(单个区) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData && entityData.type === 'powerZone'">
|
|
|
|
|
<div class="menu-title">威力区</div>
|
|
|
|
|
<div class="menu-item menu-item-sub" @click="handleTogglePowerZone">
|
|
|
|
|
<span class="menu-icon">{{ powerZoneVisible ? '👁' : '👁🗨' }}</span>
|
|
|
|
|
<span>{{ powerZoneVisible ? '隐藏该威力区' : '显示该威力区' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 白板平台:仅显示伸缩框(用于旋转) -->
|
|
|
|
|
<div class="menu-section" v-if="entityData.type === 'platformIcon' && entityData.isWhiteboard">
|
|
|
|
|
<div class="menu-title">白板平台</div>
|
|
|
|
|
<div class="menu-item" @click="handleShowTransformBox">
|
|
|
|
|
<span class="menu-icon">🔄</span>
|
|
|
|
|
<span>显示伸缩框</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
export default {
|
|
|
|
|
name: 'ContextMenu',
|
|
|
|
|
props: {
|
|
|
|
|
visible: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false
|
|
|
|
|
},
|
|
|
|
|
position: {
|
|
|
|
|
type: Object,
|
|
|
|
|
default: () => ({ x: 0, y: 0 })
|
|
|
|
|
},
|
|
|
|
|
pickList: {
|
|
|
|
|
type: Array,
|
|
|
|
|
default: null
|
|
|
|
|
},
|
|
|
|
|
pickIndex: {
|
|
|
|
|
type: Number,
|
|
|
|
|
default: 0
|
|
|
|
|
},
|
|
|
|
|
entityData: {
|
|
|
|
|
type: Object,
|
|
|
|
|
default: null
|
|
|
|
|
},
|
|
|
|
|
routeLocked: {
|
|
|
|
|
type: Object,
|
|
|
|
|
default: () => ({})
|
|
|
|
|
},
|
|
|
|
|
detectionZoneVisible: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
powerZoneVisible: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true
|
|
|
|
|
},
|
|
|
|
|
toolMode: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'airspace'
|
|
|
|
|
},
|
|
|
|
|
rangingDistanceUnit: {
|
|
|
|
|
type: String,
|
|
|
|
|
default: 'km'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
expandedAddWaypoint: null,
|
|
|
|
|
showAddWaypointDialog: false,
|
|
|
|
|
showHoldSpeedDialog: false,
|
|
|
|
|
addWaypointDialogMode: null,
|
|
|
|
|
addWaypointDialogSegmentMode: null,
|
|
|
|
|
addWaypointForm: { segmentTargetMinutes: 10, segmentTargetSpeed: 800 },
|
|
|
|
|
holdSpeedForm: { speed: 800 },
|
|
|
|
|
showColorPickerFor: null,
|
|
|
|
|
showWidthPicker: false,
|
|
|
|
|
showSizePicker: false,
|
|
|
|
|
showOpacityPicker: false,
|
|
|
|
|
showFontSizePicker: false,
|
|
|
|
|
showBearingTypeMenu: false,
|
|
|
|
|
showRangingUnitMenu: false,
|
|
|
|
|
presetColors: [
|
|
|
|
|
'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
|
|
|
|
|
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
|
|
|
|
|
'#CC99FF', '#99CCFF', '#99FF99', '#FFFF99', '#FFCC99', '#FF9999'
|
|
|
|
|
],
|
|
|
|
|
presetWidths: [1, 2, 3, 4, 5, 6, 8, 10, 12],
|
|
|
|
|
presetSizes: [6, 8, 10, 12, 14, 16, 18, 20, 24],
|
|
|
|
|
presetOpacities: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
|
|
|
|
presetFontSizes: [8, 10, 12, 14, 16, 18, 20, 24, 28, 32]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
/** 根据视口边界修正菜单位置,避免菜单在屏幕底部或右侧被截断 */
|
|
|
|
|
adjustedPosition() {
|
|
|
|
|
const padding = 12
|
|
|
|
|
const menuMaxWidth = 220
|
|
|
|
|
const menuMaxHeight = 560
|
|
|
|
|
const winW = typeof window !== 'undefined' ? window.innerWidth : 1920
|
|
|
|
|
const winH = typeof window !== 'undefined' ? window.innerHeight : 1080
|
|
|
|
|
let x = this.position.x
|
|
|
|
|
let y = this.position.y
|
|
|
|
|
if (x + menuMaxWidth + padding > winW) x = winW - menuMaxWidth - padding
|
|
|
|
|
if (x < padding) x = padding
|
|
|
|
|
if (y + menuMaxHeight + padding > winH) y = winH - menuMaxHeight - padding
|
|
|
|
|
if (y < padding) y = padding
|
|
|
|
|
return { x, y }
|
|
|
|
|
},
|
|
|
|
|
positionStyle() {
|
|
|
|
|
return {
|
|
|
|
|
left: this.adjustedPosition.x + 'px',
|
|
|
|
|
top: this.adjustedPosition.y + 'px'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
isRouteLocked() {
|
|
|
|
|
if (!this.entityData || this.entityData.routeId == null) return false
|
|
|
|
|
return !!this.routeLocked[this.entityData.routeId]
|
|
|
|
|
},
|
|
|
|
|
isHoldWaypoint() {
|
|
|
|
|
const ed = this.entityData || {}
|
|
|
|
|
if (!ed || ed.type !== 'routeWaypoint') return false
|
|
|
|
|
if (ed.fromHold) return true
|
|
|
|
|
const pt = ed.pointType || ed.point_type || ''
|
|
|
|
|
return pt === 'hold_circle' || pt === 'hold_ellipse'
|
|
|
|
|
},
|
|
|
|
|
addWaypointDialogTitle() {
|
|
|
|
|
if (!this.addWaypointDialogMode) return '设置参数'
|
|
|
|
|
const dir = this.addWaypointDialogMode === 'before' ? '向前' : '向后'
|
|
|
|
|
const type = this.addWaypointDialogSegmentMode === 'fixed_time' ? '定时点' : '定速点'
|
|
|
|
|
return `${dir}增加${type} - 设置参数`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
handleDelete() {
|
|
|
|
|
this.$emit('delete')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleDeleteBoxSelection() {
|
|
|
|
|
this.$emit('delete-box-selected-platforms')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleAdjustPosition() {
|
|
|
|
|
this.$emit('adjust-airspace-position')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleEditAirspaceName() {
|
|
|
|
|
const name = prompt('请输入图形名称:', this.entityData.name || '')
|
|
|
|
|
if (name !== null) {
|
|
|
|
|
this.$emit('update-property', 'name', name.trim())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleDetectionZonePlatform() {
|
|
|
|
|
this.$emit('detection-zone')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handlePowerZonePlatform() {
|
|
|
|
|
this.$emit('power-zone')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleToggleDetectionZone() {
|
|
|
|
|
this.$emit('toggle-detection-zone')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleTogglePowerZone() {
|
|
|
|
|
this.$emit('toggle-power-zone')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleStartRouteBeforePlatform() {
|
|
|
|
|
this.$emit('start-route-before-platform', this.entityData)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleStartRouteAfterPlatform() {
|
|
|
|
|
this.$emit('start-route-after-platform', this.entityData)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleShowTransformBox() {
|
|
|
|
|
this.$emit('show-transform-box')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleToggleRouteLabel() {
|
|
|
|
|
this.$emit('toggle-route-label')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
editName() {
|
|
|
|
|
const newName = prompt('请输入威力区名称:', this.entityData.name || '')
|
|
|
|
|
if (newName !== null && newName.trim() !== '') {
|
|
|
|
|
this.$emit('update-property', 'name', newName.trim())
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleToggleRouteLock() {
|
|
|
|
|
this.$emit('toggle-route-lock')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleCopyRoute() {
|
|
|
|
|
this.$emit('copy-route')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleRouteSegmentSplit() {
|
|
|
|
|
this.$emit('route-segment-split')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleRouteSegmentCopy() {
|
|
|
|
|
this.$emit('route-segment-copy')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleSingleRouteDeduction() {
|
|
|
|
|
this.$emit('single-route-deduction', this.entityData.routeId)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleEditWaypoint() {
|
|
|
|
|
this.$emit('open-waypoint-dialog', this.entityData.dbId, this.entityData.routeId, this.entityData.waypointIndex)
|
|
|
|
|
},
|
|
|
|
|
openEditHoldSpeedDialog() {
|
|
|
|
|
const current = this.entityData && this.entityData.holdSpeed != null
|
|
|
|
|
? Number(this.entityData.holdSpeed)
|
|
|
|
|
: (this.entityData && this.entityData.speed != null ? Number(this.entityData.speed) : 800)
|
|
|
|
|
this.holdSpeedForm = { speed: Number.isFinite(current) && current > 0 ? current : 800 }
|
|
|
|
|
this.showHoldSpeedDialog = true
|
|
|
|
|
},
|
|
|
|
|
confirmEditHoldSpeed() {
|
|
|
|
|
const v = Number(this.holdSpeedForm.speed)
|
|
|
|
|
if (!Number.isFinite(v) || v <= 0) {
|
|
|
|
|
this.$message && this.$message.warning('请填写大于0的盘旋速度')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
this.showHoldSpeedDialog = false
|
|
|
|
|
this.$emit('edit-hold-speed', {
|
|
|
|
|
routeId: this.entityData.routeId,
|
|
|
|
|
dbId: this.entityData.dbId,
|
|
|
|
|
waypointIndex: this.entityData.waypointIndex,
|
|
|
|
|
speed: v
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleAddWaypointExpand(which) {
|
|
|
|
|
this.expandedAddWaypoint = this.expandedAddWaypoint === which ? null : which
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleAddWaypointWithMode(mode, segmentMode) {
|
|
|
|
|
this.expandedAddWaypoint = null
|
|
|
|
|
if (segmentMode === 'fixed_time' || segmentMode === 'fixed_speed') {
|
|
|
|
|
this.addWaypointDialogMode = mode
|
|
|
|
|
this.addWaypointDialogSegmentMode = segmentMode
|
|
|
|
|
this.addWaypointForm = {
|
|
|
|
|
segmentTargetMinutes: 10,
|
|
|
|
|
segmentTargetSpeed: 800
|
|
|
|
|
}
|
|
|
|
|
this.showAddWaypointDialog = true
|
|
|
|
|
} else {
|
|
|
|
|
this.emitAddWaypointAt(mode, segmentMode, null, null)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
confirmAddWaypointParams() {
|
|
|
|
|
const segmentMode = this.addWaypointDialogSegmentMode
|
|
|
|
|
if (segmentMode === 'fixed_time') {
|
|
|
|
|
const v = this.addWaypointForm.segmentTargetMinutes
|
|
|
|
|
if (v == null || v === '' || Number(v) <= 0) {
|
|
|
|
|
this.$message && this.$message.warning('请填写距上一航点的飞行时间(分),且必须大于0')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else if (segmentMode === 'fixed_speed') {
|
|
|
|
|
const v = this.addWaypointForm.segmentTargetSpeed
|
|
|
|
|
if (v == null || v === '' || Number(v) <= 0) {
|
|
|
|
|
this.$message && this.$message.warning('请填写定速(km/h),且必须大于0')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.showAddWaypointDialog = false
|
|
|
|
|
const mode = this.addWaypointDialogMode
|
|
|
|
|
const segmentTargetMinutes = segmentMode === 'fixed_time' ? this.addWaypointForm.segmentTargetMinutes : null
|
|
|
|
|
const segmentTargetSpeed = segmentMode === 'fixed_speed' ? Number(this.addWaypointForm.segmentTargetSpeed) : null
|
|
|
|
|
this.emitAddWaypointAt(mode, segmentMode, segmentTargetMinutes, segmentTargetSpeed)
|
|
|
|
|
this.addWaypointDialogMode = null
|
|
|
|
|
this.addWaypointDialogSegmentMode = null
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
emitAddWaypointAt(mode, segmentMode, segmentTargetMinutes, segmentTargetSpeed) {
|
|
|
|
|
const payload = {
|
|
|
|
|
routeId: this.entityData.routeId,
|
|
|
|
|
waypointIndex: this.entityData.waypointIndex,
|
|
|
|
|
mode,
|
|
|
|
|
segmentMode
|
|
|
|
|
}
|
|
|
|
|
if (segmentTargetMinutes != null) payload.segmentTargetMinutes = segmentTargetMinutes
|
|
|
|
|
if (segmentTargetSpeed != null) payload.segmentTargetSpeed = segmentTargetSpeed
|
|
|
|
|
this.$emit('add-waypoint-at', payload)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleToggleWaypointHold() {
|
|
|
|
|
this.$emit('toggle-waypoint-hold', { routeId: this.entityData.routeId, dbId: this.entityData.dbId, waypointIndex: this.entityData.waypointIndex })
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleEditPlatform() {
|
|
|
|
|
this.$emit('edit-platform')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleDetectionZone() {
|
|
|
|
|
this.$emit('detection-zone')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handlePowerZone() {
|
|
|
|
|
this.$emit('power-zone')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleLaunchMissile() {
|
|
|
|
|
this.$emit('launch-missile')
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleColorPicker(property) {
|
|
|
|
|
if (this.showColorPickerFor === property) {
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
} else {
|
|
|
|
|
// 隐藏其他选择器
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
this.showBearingTypeMenu = false
|
|
|
|
|
this.showRangingUnitMenu = false
|
|
|
|
|
this.showColorPickerFor = property
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectColor(property, color) {
|
|
|
|
|
this.$emit('update-property', property, color)
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleWidthPicker() {
|
|
|
|
|
if (this.showWidthPicker) {
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
} else {
|
|
|
|
|
// 隐藏其他选择器
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
this.showBearingTypeMenu = false
|
|
|
|
|
this.showRangingUnitMenu = false
|
|
|
|
|
this.showWidthPicker = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectWidth(width) {
|
|
|
|
|
this.$emit('update-property', 'width', width)
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleSizePicker() {
|
|
|
|
|
if (this.showSizePicker) {
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
} else {
|
|
|
|
|
// 隐藏其他选择器
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
this.showSizePicker = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectSize(size) {
|
|
|
|
|
this.$emit('update-property', 'size', size)
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleOpacityPicker() {
|
|
|
|
|
if (this.showOpacityPicker) {
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
} else {
|
|
|
|
|
// 隐藏其他选择器
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
this.showOpacityPicker = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectOpacity(opacity) {
|
|
|
|
|
this.$emit('update-property', 'opacity', opacity)
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleFontSizePicker() {
|
|
|
|
|
if (this.showFontSizePicker) {
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
} else {
|
|
|
|
|
// 隐藏其他选择器
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
this.showFontSizePicker = true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectFontSize(size) {
|
|
|
|
|
const fontName = this.getFontName(this.entityData.font)
|
|
|
|
|
this.$emit('update-property', 'font', `${size}px ${fontName}`)
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
showFontPicker() {
|
|
|
|
|
const fonts = ['Arial', 'Microsoft YaHei', 'SimSun', 'SimHei', 'KaiTi']
|
|
|
|
|
const fontName = prompt(`请选择字体:\n${fonts.map((f, i) => `${i+1}. ${f}`).join('\n')}\n\n输入序号:`)
|
|
|
|
|
if (fontName && !isNaN(fontName) && fonts[parseInt(fontName)-1]) {
|
|
|
|
|
const selectedFont = fonts[parseInt(fontName)-1]
|
|
|
|
|
const fontSize = this.getFontSize(this.entityData.font)
|
|
|
|
|
this.$emit('update-property', 'font', `${fontSize}px ${selectedFont}`)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getFontName(font) {
|
|
|
|
|
const match = font.match(/\d+px\s+(.+)/)
|
|
|
|
|
return match ? match[1] : 'Arial'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getFontSize(font) {
|
|
|
|
|
const match = font.match(/(\d+)px/)
|
|
|
|
|
return match ? match[1] : '14'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleBearingTypeMenu() {
|
|
|
|
|
// 切换方位角类型选择菜单的显示/隐藏
|
|
|
|
|
this.showBearingTypeMenu = !this.showBearingTypeMenu
|
|
|
|
|
// 隐藏其他选择器
|
|
|
|
|
if (this.showBearingTypeMenu) {
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
this.showRangingUnitMenu = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectBearingType(bearingType) {
|
|
|
|
|
// 选择方位角类型
|
|
|
|
|
this.$emit('update-property', 'bearingType', bearingType)
|
|
|
|
|
this.showBearingTypeMenu = false
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleRangingUnitMenu() {
|
|
|
|
|
this.showRangingUnitMenu = !this.showRangingUnitMenu
|
|
|
|
|
if (this.showRangingUnitMenu) {
|
|
|
|
|
this.showColorPickerFor = null
|
|
|
|
|
this.showWidthPicker = false
|
|
|
|
|
this.showSizePicker = false
|
|
|
|
|
this.showOpacityPicker = false
|
|
|
|
|
this.showFontSizePicker = false
|
|
|
|
|
this.showBearingTypeMenu = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
selectRangingUnit(unit) {
|
|
|
|
|
if (unit === 'km' || unit === 'nm') {
|
|
|
|
|
this.$emit('ranging-distance-unit', unit)
|
|
|
|
|
}
|
|
|
|
|
this.showRangingUnitMenu = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.context-menu {
|
|
|
|
|
position: fixed;
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
max-width: 220px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-section {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-section:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-title {
|
|
|
|
|
padding: 4px 16px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-item:hover {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-icon {
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-preview {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-value {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #666;
|
|
|
|
|
min-width: 40px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-item span {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-expand-icon {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: #999;
|
|
|
|
|
flex: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-sub-group {
|
|
|
|
|
padding-left: 0;
|
|
|
|
|
background-color: #fafafa;
|
|
|
|
|
border-left: 2px solid #e0e0e0;
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-icon-sub {
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-item-sub {
|
|
|
|
|
padding: 6px 16px 6px 24px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #666;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.menu-item-sub:hover {
|
|
|
|
|
background-color: #f0f7ff;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-tip-small {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 颜色选择器样式 */
|
|
|
|
|
.color-picker-container {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.color-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(6, 1fr);
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.color-item {
|
|
|
|
|
width: 24px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.color-item:hover {
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.color-item.active {
|
|
|
|
|
transform: scale(1.2);
|
|
|
|
|
box-shadow: 0 0 0 2px white, 0 0 0 3px #007bff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 线宽选择器样式 */
|
|
|
|
|
.width-picker-container {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.width-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.width-item {
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.width-item:hover {
|
|
|
|
|
background-color: #e3f2fd;
|
|
|
|
|
border-color: #2196f3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.width-item.active {
|
|
|
|
|
background-color: #2196f3;
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: #1976d2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 大小选择器样式 */
|
|
|
|
|
.size-picker-container {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-item {
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-item:hover {
|
|
|
|
|
background-color: #e3f2fd;
|
|
|
|
|
border-color: #2196f3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-item.active {
|
|
|
|
|
background-color: #2196f3;
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: #1976d2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 透明度选择器样式 */
|
|
|
|
|
.opacity-picker-container {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.opacity-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.opacity-item {
|
|
|
|
|
padding: 4px 6px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.opacity-item:hover {
|
|
|
|
|
background-color: #e3f2fd;
|
|
|
|
|
border-color: #2196f3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.opacity-item.active {
|
|
|
|
|
background-color: #2196f3;
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: #1976d2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 字号选择器样式 */
|
|
|
|
|
.font-size-picker-container {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.font-size-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(5, 1fr);
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.font-size-item {
|
|
|
|
|
padding: 4px 6px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.font-size-item:hover {
|
|
|
|
|
background-color: #e3f2fd;
|
|
|
|
|
border-color: #2196f3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.font-size-item.active {
|
|
|
|
|
background-color: #2196f3;
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: #1976d2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 子菜单样式 */
|
|
|
|
|
.sub-menu {
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-menu-item {
|
|
|
|
|
padding: 6px 16px 6px 32px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-menu-item:hover {
|
|
|
|
|
background-color: #e3f2fd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-menu-item.active {
|
|
|
|
|
background-color: #2196f3;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
</style>
|