diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index eb61278..d44aef4 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -1568,7 +1568,11 @@ export default { mapProjection: new Cesium.WebMercatorProjection(), imageryProvider: false, terrainProvider: new Cesium.EllipsoidTerrainProvider(), - baseLayer: false + baseLayer: false, + // 允许截图时读取 canvas 内容(否则 toDataURL 会得到黑屏) + contextOptions: { + preserveDrawingBuffer: true + } }) this.viewer.cesiumWidget.creditContainer.style.display = "none" this.loadOfflineMap() diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index dfdd111..f60e3f6 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -1,6 +1,6 @@ @@ -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;