You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1488 lines
48 KiB

2 months ago
<template>
<div>
<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="handleCopyBoxSelection">
<span class="menu-icon">📋</span>
<span>复制</span>
</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')">
2 months ago
<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')">
2 months ago
<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>
2 months ago
</div>
<!-- 航线上飞机显示/隐藏/编辑标牌编辑平台 -->
2 months ago
<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>
2 months ago
</div>
2 months ago
<!-- 线段特有选项 -->
<div class="menu-section" v-if="entityData.type === 'line' && !entityData.routeId">
2 months ago
<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>
2 months ago
</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>
1 month ago
<!-- 空域图形调整位置 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'auxiliaryLine' || entityData.type === 'arrow'">
1 month ago
<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>
2 months ago
<!-- 多边形特有选项 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'powerZone'">
2 months ago
<div class="menu-title">填充属性</div>
2 months ago
<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>
2 months ago
<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>
2 months ago
</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>
2 months ago
<!-- 箭头特有选项 -->
<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>
2 months ago
3 weeks ago
<!-- 平台图标拖拽到地图的图标特有选项白板平台不显示探测区/威力区/航线 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && !entityData.isWhiteboard">
2 months ago
<div class="menu-title">平台图标</div>
<div class="menu-item" @click="openRoomPlatformIconColorDialog">
<span class="menu-icon">🎨</span>
<span>图标颜色</span>
</div>
3 weeks ago
<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>
2 months ago
</div>
<div class="menu-item" @click="handlePowerZonePlatform">
<span class="menu-icon"></span>
<span>威力区</span>
2 months ago
</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>
2 months ago
</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>
2 months ago
</div>
3 weeks ago
<!-- 探测区单个区 -->
<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>
<!-- 白板平台 -->
3 weeks ago
<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="openWhiteboardPlatformStyleDialog">
<span class="menu-icon">🎨</span>
<span>颜色与大小</span>
</div>
</div>
3 weeks ago
</div>
<el-dialog
:title="platformIconColorDialogScope === 'room' ? '图标颜色' : '颜色与大小'"
:visible.sync="showWhiteboardPlatformStyleDialog"
width="360px"
append-to-body
:close-on-click-modal="false"
>
<el-form :model="whiteboardPlatformStyleForm" label-width="90px" size="small">
<el-form-item label="着色">
<el-checkbox v-model="whiteboardPlatformStyleForm.useNativeColor" @change="onWhiteboardNativeColorChange">
与列表原图一致不着色
</el-checkbox>
</el-form-item>
<el-form-item label="颜色" v-show="!whiteboardPlatformStyleForm.useNativeColor">
<el-color-picker
v-model="whiteboardPlatformStyleForm.color"
:predefine="presetColors"
@active-change="handleWhiteboardColorActiveChange"
/>
</el-form-item>
<el-form-item v-if="platformIconColorDialogScope === 'whiteboard'" label="大小">
<el-select v-model="whiteboardPlatformStyleForm.iconScale" style="width:100%">
<el-option
v-for="scale in presetPlatformScales"
:key="'wb-style-scale-' + scale"
:label="scale + 'x'"
:value="scale"
/>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="showWhiteboardPlatformStyleDialog = false">取消</el-button>
<el-button type="primary" @click="confirmWhiteboardPlatformStyle">确定</el-button>
</span>
</el-dialog>
2 months ago
</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
},
2 months ago
entityData: {
type: Object,
default: null
2 months ago
},
routeLocked: {
type: Object,
default: () => ({})
},
detectionZoneVisible: {
type: Boolean,
default: true
},
powerZoneVisible: {
type: Boolean,
default: true
},
toolMode: {
type: String,
default: 'airspace'
},
rangingDistanceUnit: {
type: String,
default: 'km'
2 months ago
}
},
data() {
return {
expandedAddWaypoint: null,
showAddWaypointDialog: false,
showHoldSpeedDialog: false,
addWaypointDialogMode: null,
addWaypointDialogSegmentMode: null,
addWaypointForm: { segmentTargetMinutes: 10, segmentTargetSpeed: 800 },
holdSpeedForm: { speed: 800 },
2 months ago
showColorPickerFor: null,
showWidthPicker: false,
showSizePicker: false,
sizePickerType: '',
showWhiteboardPlatformStyleDialog: false,
/** room:房间地图独立平台,仅颜色;whiteboard:白板平台颜色+缩放 */
platformIconColorDialogScope: 'whiteboard',
whiteboardPlatformStyleForm: {
useNativeColor: true,
color: '#165dff',
iconScale: 1.5
},
2 months ago
showOpacityPicker: false,
showFontSizePicker: false,
showBearingTypeMenu: false,
showRangingUnitMenu: false,
2 months ago
presetColors: [
2 weeks ago
'#FF0000', '#00FF00', '#165dff', '#FFFF00', '#FF00FF', '#00FFFF',
2 months ago
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
2 weeks ago
'#CC99FF', '#165dff', '#99FF99', '#FFFF99', '#FFCC99', '#FF9999'
2 months ago
],
presetWidths: [1, 2, 3, 4, 5, 6, 8, 10, 12],
presetSizes: [6, 8, 10, 12, 14, 16, 18, 20, 24],
presetPlatformScales: [0.8, 1, 1.2, 1.5, 1.8, 2.2, 2.6, 3],
presetOpacities: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
2 months ago
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 }
},
2 months ago
positionStyle() {
return {
left: this.adjustedPosition.x + 'px',
top: this.adjustedPosition.y + 'px'
2 months ago
}
2 months ago
},
isRouteLocked() {
if (!this.entityData || this.entityData.routeId == null) return false
2 months ago
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} - 设置参数`
2 months ago
}
},
methods: {
handleDelete() {
this.$emit('delete')
},
handleDeleteBoxSelection() {
this.$emit('delete-box-selected-platforms')
},
handleCopyBoxSelection() {
this.$emit('copy-box-selected-platforms')
},
1 month ago
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')
2 months ago
},
handlePowerZonePlatform() {
this.$emit('power-zone')
},
handleToggleDetectionZone() {
this.$emit('toggle-detection-zone')
2 months ago
},
handleTogglePowerZone() {
this.$emit('toggle-power-zone')
2 months ago
},
handleStartRouteBeforePlatform() {
this.$emit('start-route-before-platform', this.entityData)
},
handleStartRouteAfterPlatform() {
this.$emit('start-route-after-platform', this.entityData)
},
3 weeks ago
handleShowTransformBox() {
this.$emit('show-transform-box')
},
openWhiteboardPlatformStyleDialog() {
const raw = this.entityData && this.entityData.color
const useNative =
raw == null ||
raw === '' ||
(typeof raw === 'string' && raw.trim() === '')
const color = useNative ? '#165dff' : raw
const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1.5
this.platformIconColorDialogScope = 'whiteboard'
this.whiteboardPlatformStyleForm = {
useNativeColor: useNative,
color,
iconScale: Number.isFinite(iconScale) ? iconScale : 1.5
}
this.showWhiteboardPlatformStyleDialog = true
},
openRoomPlatformIconColorDialog() {
const raw = this.entityData && this.entityData.color
const useNative =
raw == null ||
raw === '' ||
(typeof raw === 'string' && raw.trim() === '')
const color = useNative ? '#165dff' : raw
const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1
this.platformIconColorDialogScope = 'room'
this.whiteboardPlatformStyleForm = {
useNativeColor: useNative,
color,
iconScale: Number.isFinite(iconScale) ? iconScale : 1
}
this.showWhiteboardPlatformStyleDialog = true
},
onWhiteboardNativeColorChange(native) {
if (!native && this.whiteboardPlatformStyleForm && !this.whiteboardPlatformStyleForm.color) {
this.whiteboardPlatformStyleForm.color = '#165dff'
}
},
handleWhiteboardColorActiveChange(color) {
if (color) this.whiteboardPlatformStyleForm.color = color
},
confirmWhiteboardPlatformStyle() {
const useNative = !!this.whiteboardPlatformStyleForm.useNativeColor
const color = useNative
? null
: this.whiteboardPlatformStyleForm.color || '#165dff'
if (this.platformIconColorDialogScope === 'room') {
this.showWhiteboardPlatformStyleDialog = false
this.$emit('apply-room-platform-icon-color', {
id: this.entityData && this.entityData.id,
color
})
this.$emit('close-menu')
return
}
const iconScale = Number(this.whiteboardPlatformStyleForm.iconScale)
if (!Number.isFinite(iconScale) || iconScale <= 0) {
this.$message && this.$message.warning('请选择有效大小')
return
}
this.showWhiteboardPlatformStyleDialog = false
this.$emit('apply-whiteboard-platform-style', {
id: this.entityData && this.entityData.id,
color,
iconScale
})
// 兜底关闭菜单,避免菜单残留
this.$emit('close-menu')
},
3 weeks ago
2 months ago
handleToggleRouteLabel() {
this.$emit('toggle-route-label')
},
2 months ago
editName() {
const newName = prompt('请输入威力区名称:', this.entityData.name || '')
if (newName !== null && newName.trim() !== '') {
this.$emit('update-property', 'name', newName.trim())
}
},
2 months ago
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')
},
2 months ago
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
2 months ago
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
2 months ago
this.showWidthPicker = true
}
},
selectWidth(width) {
this.$emit('update-property', 'width', width)
this.showWidthPicker = false
},
toggleSizePicker(type = 'default') {
2 months ago
if (this.showSizePicker) {
this.showSizePicker = false
this.sizePickerType = ''
2 months ago
} else {
// 隐藏其他选择器
this.showColorPickerFor = null
this.showWidthPicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.sizePickerType = type
2 months ago
this.showSizePicker = true
}
},
selectSize(size) {
this.$emit('update-property', 'size', size)
this.showSizePicker = false
this.sizePickerType = ''
2 months ago
},
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
2 months ago
}
},
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
2 months ago
}
}
}
</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 {
2 weeks ago
background-color: #ebf3ff;
color: #333;
}
.form-tip-small {
font-size: 11px;
color: #999;
margin-top: 4px;
line-height: 1.3;
}
2 months ago
/* 颜色选择器样式 */
.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);
2 weeks ago
box-shadow: 0 0 0 2px white, 0 0 0 3px #165dff;
2 months ago
}
/* 线宽选择器样式 */
.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 {
2 weeks ago
background-color: #ebf3ff;
border-color: #165dff;
2 months ago
}
.width-item.active {
2 weeks ago
background-color: #165dff;
2 months ago
color: white;
2 weeks ago
border-color: #165dff;
2 months ago
}
/* 大小选择器样式 */
.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 {
2 weeks ago
background-color: #ebf3ff;
border-color: #165dff;
2 months ago
}
.size-item.active {
2 weeks ago
background-color: #165dff;
2 months ago
color: white;
2 weeks ago
border-color: #165dff;
2 months ago
}
/* 透明度选择器样式 */
.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 {
2 weeks ago
background-color: #ebf3ff;
border-color: #165dff;
2 months ago
}
.opacity-item.active {
2 weeks ago
background-color: #165dff;
2 months ago
color: white;
2 weeks ago
border-color: #165dff;
2 months ago
}
/* 字号选择器样式 */
.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 {
2 weeks ago
background-color: #ebf3ff;
border-color: #165dff;
2 months ago
}
.font-size-item.active {
2 weeks ago
background-color: #165dff;
2 months ago
color: white;
2 weeks ago
border-color: #165dff;
2 months ago
}
/* 子菜单样式 */
.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 {
2 weeks ago
background-color: #ebf3ff;
2 months ago
}
.sub-menu-item.active {
2 weeks ago
background-color: #165dff;
2 months ago
color: white;
}
</style>