|
|
|
@ -1,6 +1,6 @@ |
|
|
|
<template> |
|
|
|
<!-- 以地图为绝对定位背景,所有组件浮动其上 --> |
|
|
|
<div class="mission-planning-container"> |
|
|
|
<div class="mission-planning-container" :class="{ 'screenshot-mode': screenshotMode }"> |
|
|
|
<!-- 地图背景:支持从右侧平台列表拖拽图标到地图 --> |
|
|
|
<div |
|
|
|
id="gis-map-background" |
|
|
|
@ -19,18 +19,19 @@ |
|
|
|
@scale-click="handleScaleClick" |
|
|
|
@platform-icon-updated="onPlatformIconUpdated" |
|
|
|
@platform-icon-removed="onPlatformIconRemoved" /> |
|
|
|
<div class="map-overlay-text"> |
|
|
|
<div v-show="!screenshotMode" class="map-overlay-text"> |
|
|
|
<i class="el-icon-location-outline text-3xl mb-2 block"></i> |
|
|
|
<p>二维GIS地图区域</p> |
|
|
|
<p class="text-sm mt-1">支持标绘/航线/空域/实时态势</p> |
|
|
|
</div> |
|
|
|
<div v-if="missionDrawingActive && missionDrawingPointsCount >= 2" class="mission-drawing-actions" style="position:absolute; bottom:16px; left:50%; transform:translateX(-50%); z-index:10; display:flex; gap:8px; align-items:center;"> |
|
|
|
<div v-if="missionDrawingActive && missionDrawingPointsCount >= 2 && !screenshotMode" class="mission-drawing-actions" style="position:absolute; bottom:16px; left:50%; transform:translateX(-50%); z-index:10; display:flex; gap:8px; align-items:center;"> |
|
|
|
<span class="text-white text-sm">已 {{ missionDrawingPointsCount }} 个航点,右键结束</span> |
|
|
|
<el-button type="primary" size="small" @click="openAddHoldDuringDrawing">插入盘旋</el-button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 地图中间的浮动红点(触发左侧菜单) --> |
|
|
|
<div |
|
|
|
v-show="!screenshotMode" |
|
|
|
class="floating-red-dot left-red-dot" |
|
|
|
:class="{ hidden: !isMenuHidden }" |
|
|
|
@click="showMenu" |
|
|
|
@ -73,6 +74,7 @@ |
|
|
|
</div> |
|
|
|
<!-- 顶部导航栏 --> |
|
|
|
<top-header |
|
|
|
v-show="!screenshotMode" |
|
|
|
:room-code="roomCode" |
|
|
|
:online-count="onlineCount" |
|
|
|
:combat-time="combatTime" |
|
|
|
@ -130,6 +132,7 @@ |
|
|
|
/> |
|
|
|
<!-- 左侧折叠菜单栏 - 蓝色主题 --> |
|
|
|
<left-menu |
|
|
|
v-show="!screenshotMode" |
|
|
|
:is-hidden="isMenuHidden" |
|
|
|
:menu-items="menuItems" |
|
|
|
:active-menu="activeMenu" |
|
|
|
@ -149,6 +152,7 @@ |
|
|
|
/> |
|
|
|
<!-- 右侧实体列表(浮动)- 蓝色主题 --> |
|
|
|
<right-panel |
|
|
|
v-show="!screenshotMode" |
|
|
|
ref="rightPanel" |
|
|
|
:is-hidden="isRightPanelHidden" |
|
|
|
:active-tab="activeRightTab" |
|
|
|
@ -185,9 +189,10 @@ |
|
|
|
@open-import-dialog="showImportDialog = true" |
|
|
|
/> |
|
|
|
<!-- 左下角工具面板 --> |
|
|
|
<bottom-left-panel /> |
|
|
|
<bottom-left-panel v-show="!screenshotMode" /> |
|
|
|
<!-- 底部时间轴(最初版本的样式)- 蓝色主题 --> |
|
|
|
<div |
|
|
|
v-show="!screenshotMode" |
|
|
|
class="floating-timeline blue-theme" |
|
|
|
:class="{ 'show': showKTimePopup }" |
|
|
|
> |
|
|
|
@ -364,6 +369,31 @@ |
|
|
|
<el-button type="primary" @click="confirmCreatePlan">确 定</el-button> |
|
|
|
</div> |
|
|
|
</el-dialog> |
|
|
|
|
|
|
|
<!-- 截图保存弹窗:选择文件名后保存到本地(浏览器会弹出保存位置对话框) --> |
|
|
|
<el-dialog |
|
|
|
title="保存地图截图" |
|
|
|
:visible.sync="showScreenshotDialog" |
|
|
|
width="520px" |
|
|
|
append-to-body |
|
|
|
class="screenshot-save-dialog" |
|
|
|
> |
|
|
|
<div class="screenshot-preview" v-if="screenshotDataUrl"> |
|
|
|
<img :src="screenshotDataUrl" alt="截图预览" /> |
|
|
|
</div> |
|
|
|
<el-form label-width="90px"> |
|
|
|
<el-form-item label="文件名"> |
|
|
|
<el-input v-model="screenshotFileName" placeholder="例如:地图截图.png" /> |
|
|
|
</el-form-item> |
|
|
|
<p class="screenshot-tip">点击「保存」后,浏览器将弹出保存对话框,您可选择保存路径。</p> |
|
|
|
</el-form> |
|
|
|
<div slot="footer" class="dialog-footer"> |
|
|
|
<el-button @click="showScreenshotDialog = false">取 消</el-button> |
|
|
|
<el-button type="primary" @click="confirmSaveScreenshot"> |
|
|
|
<i class="el-icon-download"></i> 保 存 |
|
|
|
</el-button> |
|
|
|
</div> |
|
|
|
</el-dialog> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
@ -441,6 +471,12 @@ export default { |
|
|
|
//导入平台弹窗 |
|
|
|
showImportDialog: false, |
|
|
|
|
|
|
|
// 地图截图 |
|
|
|
screenshotMode: false, |
|
|
|
showScreenshotDialog: false, |
|
|
|
screenshotDataUrl: '', |
|
|
|
screenshotFileName: '', |
|
|
|
|
|
|
|
// 作战信息 |
|
|
|
roomCode: 'JTF-7-ALPHA', |
|
|
|
onlineCount: 30, |
|
|
|
@ -463,7 +499,7 @@ export default { |
|
|
|
{ id: 'pattern', name: '空域', icon: 'ky' }, |
|
|
|
{ id: 'deduction', name: '推演', icon: 'el-icon-video-play' }, |
|
|
|
{ id: 'modify', name: '测距', icon: 'cj' }, |
|
|
|
{ id: 'refresh', name: '刷新', icon: 'el-icon-refresh' }, |
|
|
|
{ id: 'refresh', name: '截图', icon: 'screenshot', action: 'refresh' }, |
|
|
|
{ id: 'basemap', name: '底图', icon: 'dt' }, |
|
|
|
{ id: 'save', name: '保存', icon: 'el-icon-document-checked' }, |
|
|
|
{ id: 'import', name: '导入', icon: 'el-icon-upload2' }, |
|
|
|
@ -1435,7 +1471,8 @@ export default { |
|
|
|
'toggleLandmark': () => this.toggleLandmark(), |
|
|
|
'toggleRoute': () => this.toggleRoute(), |
|
|
|
'layerFavorites': () => this.layerFavorites(), |
|
|
|
'routeFavorites': () => this.routeFavorites() |
|
|
|
'routeFavorites': () => this.routeFavorites(), |
|
|
|
'refresh': () => this.captureMapScreenshot() |
|
|
|
} |
|
|
|
|
|
|
|
if (actionMap[actionId]) { |
|
|
|
@ -1443,6 +1480,76 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** 截图:隐藏上下左右菜单只保留地图,用 postRender + readPixels 避免 WebGL 缓冲被清空导致黑屏 */ |
|
|
|
async captureMapScreenshot() { |
|
|
|
const cm = this.$refs.cesiumMap |
|
|
|
if (!cm || !cm.viewer || !cm.viewer.scene || !cm.viewer.scene.canvas) { |
|
|
|
this.$message.warning('地图未就绪,请稍后再试') |
|
|
|
return |
|
|
|
} |
|
|
|
this.screenshotMode = true |
|
|
|
await this.$nextTick() |
|
|
|
await new Promise(r => setTimeout(r, 350)) |
|
|
|
const viewer = cm.viewer |
|
|
|
const canvas = viewer.scene.canvas |
|
|
|
const self = this |
|
|
|
const removeListener = viewer.scene.postRender.addEventListener(function captureFrame() { |
|
|
|
removeListener() |
|
|
|
self.screenshotMode = false |
|
|
|
try { |
|
|
|
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') |
|
|
|
if (!gl) { |
|
|
|
self.$message.error('无法获取 WebGL 上下文') |
|
|
|
return |
|
|
|
} |
|
|
|
const width = canvas.width |
|
|
|
const height = canvas.height |
|
|
|
if (width === 0 || height === 0) { |
|
|
|
self.$message.error('画布尺寸为 0') |
|
|
|
return |
|
|
|
} |
|
|
|
const pixels = new Uint8Array(width * height * 4) |
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null) |
|
|
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels) |
|
|
|
const offscreen = document.createElement('canvas') |
|
|
|
offscreen.width = width |
|
|
|
offscreen.height = height |
|
|
|
const ctx = offscreen.getContext('2d') |
|
|
|
const imageData = ctx.createImageData(width, height) |
|
|
|
for (let y = 0; y < height; y++) { |
|
|
|
const srcRow = (height - 1 - y) * width * 4 |
|
|
|
const dstRow = y * width * 4 |
|
|
|
for (let i = 0; i < width * 4; i++) imageData.data[dstRow + i] = pixels[srcRow + i] |
|
|
|
} |
|
|
|
ctx.putImageData(imageData, 0, 0) |
|
|
|
const dataUrl = offscreen.toDataURL('image/png') |
|
|
|
const now = new Date() |
|
|
|
const timeStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + '_' + String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0') + String(now.getSeconds()).padStart(2, '0') |
|
|
|
self.screenshotFileName = `地图截图_${timeStr}.png` |
|
|
|
self.screenshotDataUrl = dataUrl |
|
|
|
self.showScreenshotDialog = true |
|
|
|
} catch (e) { |
|
|
|
self.$message.error('截图失败:' + (e && e.message ? e.message : '未知错误')) |
|
|
|
} |
|
|
|
}) |
|
|
|
viewer.scene.requestRender() |
|
|
|
}, |
|
|
|
|
|
|
|
/** 确认保存截图:触发浏览器下载(用户可在保存对话框中选择路径) */ |
|
|
|
confirmSaveScreenshot() { |
|
|
|
if (!this.screenshotDataUrl) return |
|
|
|
let name = (this.screenshotFileName || '地图截图.png').trim() |
|
|
|
if (!name) name = '地图截图.png' |
|
|
|
if (!/\.(png|jpg|jpeg)$/i.test(name)) name = name + '.png' |
|
|
|
const a = document.createElement('a') |
|
|
|
a.href = this.screenshotDataUrl |
|
|
|
a.download = name |
|
|
|
a.click() |
|
|
|
this.showScreenshotDialog = false |
|
|
|
this.screenshotDataUrl = '' |
|
|
|
this.$message.success('已触发下载,请在浏览器保存对话框中选择保存位置') |
|
|
|
}, |
|
|
|
|
|
|
|
handleDeleteMenuItem(deletedItem) { |
|
|
|
const index = this.menuItems.findIndex(item => item.id === deletedItem.id) |
|
|
|
if (index > -1) { |
|
|
|
@ -1478,7 +1585,12 @@ export default { |
|
|
|
arr = typeof data.menuItems === 'string' ? JSON.parse(data.menuItems) : data.menuItems |
|
|
|
} catch (e) { /* 解析失败保留默认 */ } |
|
|
|
if (Array.isArray(arr) && arr.length > 0) { |
|
|
|
this.menuItems = arr |
|
|
|
const defaultMap = (this.defaultMenuItems || []).reduce((m, it) => { m[it.id] = it; return m }, {}) |
|
|
|
this.menuItems = arr.map(item => { |
|
|
|
const def = defaultMap[item.id] |
|
|
|
if (def) return { ...item, name: def.name, icon: def.icon, action: def.action } |
|
|
|
return item |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
if (data.position && ['left', 'top', 'bottom'].includes(data.position)) { |
|
|
|
@ -2646,6 +2758,29 @@ export default { |
|
|
|
z-index: 1; |
|
|
|
} |
|
|
|
|
|
|
|
/* 截图保存弹窗 */ |
|
|
|
.screenshot-save-dialog .screenshot-preview { |
|
|
|
max-height: 320px; |
|
|
|
overflow: hidden; |
|
|
|
border-radius: 4px; |
|
|
|
background: #f5f5f5; |
|
|
|
margin-bottom: 12px; |
|
|
|
} |
|
|
|
.screenshot-save-dialog .screenshot-preview img { |
|
|
|
display: block; |
|
|
|
max-width: 100%; |
|
|
|
width: auto; |
|
|
|
height: auto; |
|
|
|
max-height: 320px; |
|
|
|
object-fit: contain; |
|
|
|
} |
|
|
|
.screenshot-save-dialog .screenshot-tip { |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
margin-top: 8px; |
|
|
|
margin-bottom: 0; |
|
|
|
} |
|
|
|
|
|
|
|
/* ...其余样式省略,保持不变... */ |
|
|
|
.map-overlay-text { |
|
|
|
position: absolute; |
|
|
|
|