fix
This commit is contained in:
1 parent
7b76924b29
commit
a6ddcd4a35
4 files changed
+857
-37
No files matched your search
+640
-16
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, watch, ref } from 'vue'
|
||||
import { Viewer, ScreenSpaceEventHandler, ScreenSpaceEventType, defined, Cartographic, sampleTerrainMostDetailed, Cartesian3, Math as cesiumMath, Ion } from 'cesium'
|
||||
import { Wayline } from './wayline'
|
||||
import { useStore } from './store'
|
||||
@@ -7,6 +7,142 @@ import { createViewer } from './basemap'
|
||||
import * as egm96 from 'egm96-universal'
|
||||
|
||||
const store = useStore()
|
||||
const isDraggingSlider = ref(false)
|
||||
const isMinimapExpanded = ref(false)
|
||||
|
||||
// 声明全局变量类型
|
||||
declare global {
|
||||
interface Window {
|
||||
wayline?: Wayline
|
||||
}
|
||||
}
|
||||
|
||||
// 监听镜头模式变化,切换时重置变焦
|
||||
watch(() => store.lensMode, (newMode) => {
|
||||
if (window.wayline) {
|
||||
const minimapViewer = window.wayline.minimapViewer
|
||||
if (minimapViewer) {
|
||||
function fovFromZoom(zoom: number, fov1: number = 1.3418): number {
|
||||
const k = Math.tan(fov1 / 2)
|
||||
return 2 * Math.atan(k / zoom)
|
||||
}
|
||||
if (newMode === 'wide') {
|
||||
// 切换到广角模式时,重置为1x
|
||||
const currentFov = fovFromZoom(1)
|
||||
store.updateZoomFactor(1)
|
||||
minimapViewer.camera.frustum.fov = currentFov
|
||||
} else {
|
||||
// 切换到变焦模式时,如果当前是1x,设置为1x(保持不变)
|
||||
if (store.zoomFactor === 1) {
|
||||
const currentFov = fovFromZoom(1)
|
||||
minimapViewer.camera.frustum.fov = currentFov
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听变焦倍数变化,更新滑块位置
|
||||
watch(() => store.zoomFactor, () => {
|
||||
// 滑块位置会自动通过getSliderPosition()计算更新
|
||||
})
|
||||
|
||||
// 变焦滑块相关函数
|
||||
function getSliderPosition(): number {
|
||||
if (store.lensMode === 'wide') {
|
||||
return 0 // 广角模式固定在1x位置
|
||||
}
|
||||
// 变焦模式:1x在底部(0%),56x在顶部(100%)
|
||||
const minZoom = 1
|
||||
const maxZoom = 56
|
||||
const zoom = store.zoomFactor
|
||||
const percentage = ((zoom - minZoom) / (maxZoom - minZoom)) * 100
|
||||
// bottom: 0% => 1x, bottom: 100% => 56x
|
||||
return percentage
|
||||
}
|
||||
|
||||
function zoomFromPosition(percentage: number): number {
|
||||
// percentage: 0% = 底部(1x), 100% = 顶部(56x)
|
||||
const minZoom = 1
|
||||
const maxZoom = 56
|
||||
const zoom = minZoom + (percentage / 100) * (maxZoom - minZoom)
|
||||
return Math.round(Math.max(minZoom, Math.min(maxZoom, zoom)))
|
||||
}
|
||||
|
||||
function onSliderMouseDown(event: MouseEvent) {
|
||||
if (store.lensMode === 'wide') return // 广角模式不允许调整
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.classList.contains('minimap-zoom-track') && !target.classList.contains('zoom-track')) return
|
||||
isDraggingSlider.value = true
|
||||
updateZoomFromEvent(event)
|
||||
}
|
||||
|
||||
function onSliderMouseMove(event: MouseEvent) {
|
||||
if (!isDraggingSlider.value || store.lensMode === 'wide') return
|
||||
updateZoomFromEvent(event)
|
||||
}
|
||||
|
||||
function onSliderMouseUp() {
|
||||
isDraggingSlider.value = false
|
||||
}
|
||||
|
||||
function onHandleMouseDown(event: MouseEvent) {
|
||||
if (store.lensMode === 'wide') return
|
||||
event.stopPropagation()
|
||||
isDraggingSlider.value = true
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (isDraggingSlider.value && store.lensMode !== 'wide') {
|
||||
// 找到对应的track元素
|
||||
const handle = event.target as HTMLElement
|
||||
const track = handle.closest('.minimap-zoom-track') || handle.closest('.zoom-track')
|
||||
if (track) {
|
||||
const fakeEvent = { ...e, currentTarget: track, target: track } as MouseEvent
|
||||
updateZoomFromEvent(fakeEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleMouseUp = () => {
|
||||
isDraggingSlider.value = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
function updateZoomFromEvent(event: MouseEvent) {
|
||||
if (store.lensMode === 'wide') return
|
||||
|
||||
// 查找滑块轨道元素(支持minimap-zoom-track和zoom-track)
|
||||
let sliderElement = (event.currentTarget || event.target) as HTMLElement
|
||||
if (!sliderElement || (!sliderElement.classList.contains('zoom-track') && !sliderElement.classList.contains('minimap-zoom-track'))) {
|
||||
// 如果点击的是handle,查找父元素中的track
|
||||
sliderElement = sliderElement.closest('.minimap-zoom-track') as HTMLElement || sliderElement.closest('.zoom-track') as HTMLElement
|
||||
if (!sliderElement) return
|
||||
}
|
||||
|
||||
const rect = sliderElement.getBoundingClientRect()
|
||||
const y = event.clientY - rect.top
|
||||
const percentage = Math.max(0, Math.min(100, (y / rect.height) * 100))
|
||||
|
||||
const newZoom = zoomFromPosition(percentage)
|
||||
if (window.wayline) {
|
||||
const minimapViewer = window.wayline.minimapViewer
|
||||
if (minimapViewer) {
|
||||
function fovFromZoom(zoom: number, fov1: number = 1.3418): number {
|
||||
const k = Math.tan(fov1 / 2)
|
||||
return 2 * Math.atan(k / zoom)
|
||||
}
|
||||
const currentFov = fovFromZoom(newZoom)
|
||||
store.updateZoomFactor(newZoom)
|
||||
minimapViewer.camera.frustum.fov = currentFov
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMinimapExpand() {
|
||||
isMinimapExpanded.value = !isMinimapExpanded.value
|
||||
}
|
||||
|
||||
let viewer: Viewer
|
||||
Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI4NDU1Zjk5ZS0xOWQzLTQ2ZDEtYTk5MC03NmFlMGIwYTE4YzIiLCJpZCI6MTMwMTIsInNjb3BlcyI6WyJhc3IiLCJnYyJdLCJpYXQiOjE1NjIzMTc2OTh9.1gS4dOGsPgIB_Xd5ERV_LPBPrj12oVUX4ZtyQyoYw58'
|
||||
@@ -67,26 +203,136 @@ async function getTerrainPosition(cartographic: Cartographic) {
|
||||
|
||||
<template>
|
||||
<div id="cesium-viewer" ref="viewerDivRef"></div>
|
||||
<div id="minimap">
|
||||
<div class="zoom-frame warning" style="width: 200px; height: 150px;">
|
||||
<div id="minimap" :class="{ 'expanded': isMinimapExpanded }">
|
||||
<!-- 小框放大按钮 -->
|
||||
<button class="minimap-expand-btn" @click="toggleMinimapExpand" :title="isMinimapExpanded ? '缩小' : '放大'">
|
||||
<svg v-if="!isMinimapExpanded" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 2H6V3H3V6H2V2ZM10 2H14V6H13V3H10V2ZM2 10V14H6V13H3V10H2ZM13 10V13H10V14H14V10H13Z" fill="white"/>
|
||||
</svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 5H13V7H11V5ZM5 5H7V7H5V5ZM11 9H13V11H11V9ZM5 9H7V11H5V9Z" fill="white"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Minimap HUD 覆盖层(按截图布局) -->
|
||||
<div class="minimap-hud">
|
||||
<!-- 中间:对焦框(zoom-frame) -->
|
||||
<div class="zoom-frame warning">
|
||||
<div class="top left corner"></div>
|
||||
<div class="top right corner"></div>
|
||||
<div class="bottom left corner"></div>
|
||||
<div class="bottom right corner"></div><span class="frame-title map-text-shadow warning">
|
||||
<div class="bottom right corner"></div>
|
||||
<span class="frame-title map-text-shadow warning">
|
||||
{{ store.zoomFactor }}X
|
||||
<span class="uranus-icon uranus-icon-cursor-pointer warning-tooltips warning-tooltips warning-tooltips">
|
||||
<svg class="svgfont svgfont-warning_outline" style="width: 1em; height: 1em;">
|
||||
<use xlink:href="#warning_outline"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 顶部居中:镜头切换 -->
|
||||
<div class="minimap-lens-controls">
|
||||
<button
|
||||
:class="['minimap-lens-btn', { 'active': store.lensMode === 'wide' }]"
|
||||
@click="store.setLensMode('wide')"
|
||||
>
|
||||
广角 {{ store.lensMode === 'wide' ? store.zoomFactor : 1 }}X
|
||||
</button>
|
||||
<button
|
||||
:class="['minimap-lens-btn', { 'active': store.lensMode === 'zoom' }]"
|
||||
@click="store.setLensMode('zoom')"
|
||||
>
|
||||
变焦 {{ store.lensMode === 'zoom' ? store.zoomFactor : 1 }}X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 中上:偏航角 -->
|
||||
<div class="minimap-yaw">
|
||||
偏航角 {{ Math.round(store.yawAngle) }}
|
||||
</div>
|
||||
|
||||
<!-- 左侧:云台 -->
|
||||
<div class="minimap-gimbal">
|
||||
<div class="minimap-gimbal-title">云台</div>
|
||||
<div class="minimap-gimbal-value">{{ Math.round(store.gimbalPitch) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部居中:保存 -->
|
||||
<button class="minimap-save-btn" type="button">
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="minimap-zoom-slider" :class="{ 'disabled': store.lensMode === 'wide' }">
|
||||
<div class="minimap-zoom-marks">
|
||||
<span class="minimap-zoom-mark">56x</span>
|
||||
<span class="minimap-zoom-mark">14x</span>
|
||||
<span class="minimap-zoom-mark">7x</span>
|
||||
<span class="minimap-zoom-mark">2x</span>
|
||||
<span class="minimap-zoom-mark">1x</span>
|
||||
</div>
|
||||
<div
|
||||
class="minimap-zoom-track"
|
||||
:class="{ 'disabled': store.lensMode === 'wide' }"
|
||||
@mousedown="onSliderMouseDown"
|
||||
@mousemove="onSliderMouseMove"
|
||||
@mouseup="onSliderMouseUp"
|
||||
@mouseleave="onSliderMouseUp"
|
||||
>
|
||||
<div
|
||||
class="minimap-zoom-handle"
|
||||
:class="{ 'disabled': store.lensMode === 'wide' }"
|
||||
:style="{ bottom: getSliderPosition() + '%' }"
|
||||
@mousedown.stop="onHandleMouseDown"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: fixed; bottom: 20px; left: 200px;">
|
||||
<div id="degree" style="color: white; text-align: center;"></div>
|
||||
<img id="compass" src="/compass.svg" draggable="false">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="point-list-container">
|
||||
<div class="point-list-header">航点列表 ({{ store.pointList.length }})</div>
|
||||
<div class="point-list-content">
|
||||
<div
|
||||
v-for="point in store.pointList"
|
||||
:key="point.index"
|
||||
:class="['point-item', { 'selected': point.selected }]"
|
||||
>
|
||||
<div class="point-index">#{{ point.index + 1 }}</div>
|
||||
<div class="point-info">
|
||||
<div class="point-row">
|
||||
<span class="label">经度:</span>
|
||||
<span class="value">{{ point.longitude.toFixed(6) }}°</span>
|
||||
</div>
|
||||
<div class="point-row">
|
||||
<span class="label">纬度:</span>
|
||||
<span class="value">{{ point.latitude.toFixed(6) }}°</span>
|
||||
</div>
|
||||
<div class="point-row">
|
||||
<span class="label">ASL:</span>
|
||||
<span class="value">{{ point.asl.toFixed(1) }} m</span>
|
||||
</div>
|
||||
<div class="point-row">
|
||||
<span class="label">HAE:</span>
|
||||
<span class="value">{{ point.alt.toFixed(1) }} m</span>
|
||||
</div>
|
||||
<div class="point-row">
|
||||
<span class="label">航向:</span>
|
||||
<span class="value">{{ point.heading.toFixed(1) }}°</span>
|
||||
</div>
|
||||
<div class="point-row">
|
||||
<span class="label">俯仰:</span>
|
||||
<span class="value">{{ point.pitch.toFixed(1) }}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="store.pointList.length === 0" class="empty-message">
|
||||
暂无航点,按空格键创建航点
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -101,6 +347,33 @@ async function getTerrainPosition(cartographic: Cartographic) {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 2px solid rgba(255, 255, 255, 0.65);
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#minimap::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Cesium minimap canvas fill */
|
||||
#minimap :deep(.cesium-viewer) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#minimap.expanded {
|
||||
width: 600px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
._dji_compass_p0y8b_78 {
|
||||
@@ -108,9 +381,6 @@ async function getTerrainPosition(cartographic: Cartographic) {
|
||||
}
|
||||
|
||||
.zoom-frame {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@@ -118,6 +388,8 @@ async function getTerrainPosition(cartographic: Cartographic) {
|
||||
z-index: 999;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.zoom-frame .corner {
|
||||
@@ -152,10 +424,12 @@ async function getTerrainPosition(cartographic: Cartographic) {
|
||||
}
|
||||
|
||||
.zoom-frame .frame-title {
|
||||
color: #00ee8b;
|
||||
bottom: -8px;
|
||||
right: 0;
|
||||
transform: translateY(100%);
|
||||
color: #fff;
|
||||
top: -10px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.frame-title {
|
||||
@@ -180,4 +454,354 @@ async function getTerrainPosition(cartographic: Cartographic) {
|
||||
transform: translate(-50%, -100%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.point-list-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 40px);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 2px solid #00D690;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.point-list-header {
|
||||
padding: 12px 16px;
|
||||
background: rgba(0, 214, 144, 0.2);
|
||||
border-bottom: 1px solid #00D690;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.point-list-content {
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
max-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.point-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.point-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(0, 214, 144, 0.5);
|
||||
}
|
||||
|
||||
.point-item.selected {
|
||||
background: rgba(0, 214, 144, 0.2);
|
||||
border-color: #00D690;
|
||||
box-shadow: 0 0 10px rgba(0, 214, 144, 0.3);
|
||||
}
|
||||
|
||||
.point-index {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #00D690;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.point-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.point-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.point-row .label {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.point-row .value {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 顶部中央:镜头模式切换 */
|
||||
/* 左上角:放大/缩小按钮(按截图) */
|
||||
.minimap-expand-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.minimap-expand-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* HUD 覆盖层容器 */
|
||||
.minimap-hud {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 顶部居中:镜头切换(按截图两段按钮) */
|
||||
.minimap-lens-controls {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.minimap-lens-btn {
|
||||
padding: 10px 22px;
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: rgba(255, 255, 255, 0.70);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.minimap-lens-btn:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.minimap-lens-btn:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.minimap-lens-btn.active {
|
||||
background: #2F80ED;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.minimap-lens-btn:not(.active):hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 中上:偏航角 */
|
||||
.minimap-yaw {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* 左侧:云台 */
|
||||
.minimap-gimbal {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
left: 48px;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.minimap-gimbal-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.minimap-gimbal-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
/* 底部:保存按钮 */
|
||||
.minimap-save-btn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 18px;
|
||||
transform: translateX(-50%);
|
||||
width: 160px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2F80ED;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.minimap-save-btn:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
/* Minimap内的变焦滑块(右侧竖条) */
|
||||
.minimap-zoom-slider {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 16px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 3;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.minimap-zoom-marks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 180px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.minimap-zoom-mark {
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.minimap-zoom-track {
|
||||
position: relative;
|
||||
width: 6px;
|
||||
height: 180px;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.minimap-zoom-handle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.minimap-zoom-handle:active {
|
||||
cursor: grabbing;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.minimap-zoom-slider.disabled .minimap-zoom-track,
|
||||
.minimap-zoom-track.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.minimap-zoom-handle.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 小框放大按钮 */
|
||||
.minimap-expand-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.minimap-expand-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 云台和偏航角数据显示 */
|
||||
.gimbal-data-display {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.data-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
+30
-1
@@ -1,12 +1,41 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface WayPointInfo {
|
||||
index: number;
|
||||
asl: number;
|
||||
alt: number;
|
||||
fov: number;
|
||||
selected: boolean;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
heading: number;
|
||||
pitch: number;
|
||||
roll: number;
|
||||
}
|
||||
|
||||
export type LensMode = 'wide' | 'zoom';
|
||||
|
||||
export const useStore = defineStore('main', {
|
||||
state: () => ({
|
||||
zoomFactor: 0
|
||||
zoomFactor: 1,
|
||||
lensMode: 'wide' as LensMode,
|
||||
pointList: [] as WayPointInfo[],
|
||||
gimbalPitch: 0, // 云台俯仰角(度)
|
||||
yawAngle: 0 // 偏航角(度)
|
||||
}),
|
||||
actions: {
|
||||
updateZoomFactor(zoomFactor: number) {
|
||||
this.zoomFactor = zoomFactor;
|
||||
},
|
||||
updatePointList(pointList: WayPointInfo[]) {
|
||||
this.pointList = pointList;
|
||||
},
|
||||
setLensMode(mode: LensMode) {
|
||||
this.lensMode = mode;
|
||||
},
|
||||
updateGimbalData(pitch: number, yaw: number) {
|
||||
this.gimbalPitch = pitch;
|
||||
this.yawAngle = yaw;
|
||||
}
|
||||
}
|
||||
})
|
||||
+119
-15
@@ -81,6 +81,17 @@ export class Wayline {
|
||||
this.setupDragEvent()
|
||||
this.setupMovementEvent()
|
||||
this.setupSelectEvent()
|
||||
this.syncPointListToStore()
|
||||
|
||||
// 初始化云台数据
|
||||
const store = useStore()
|
||||
const initialPitch = CesiumMath.toDegrees(this.minimapViewer.camera.pitch)
|
||||
const initialHeading = this.minimapViewer.camera.heading
|
||||
let initialYaw = CesiumMath.toDegrees(initialHeading)
|
||||
if (initialYaw > 180) {
|
||||
initialYaw = initialYaw - 360
|
||||
}
|
||||
store.updateGimbalData(initialPitch, initialYaw)
|
||||
}
|
||||
|
||||
createFrustum(): void {
|
||||
@@ -104,6 +115,7 @@ export class Wayline {
|
||||
|
||||
this.selectedWaypoint = wayPoint
|
||||
wayPoint.select()
|
||||
this.syncPointListToStore()
|
||||
}
|
||||
|
||||
setupSelectEvent(): void {
|
||||
@@ -115,6 +127,7 @@ export class Wayline {
|
||||
this.selectedWaypoint?.unselect()
|
||||
this.selectedWaypoint = (object.id as any).wayPoint
|
||||
this.selectedWaypoint.select()
|
||||
this.syncPointListToStore()
|
||||
}
|
||||
}, ScreenSpaceEventType.LEFT_CLICK)
|
||||
}
|
||||
@@ -124,17 +137,26 @@ export class Wayline {
|
||||
|
||||
let isDragging = false
|
||||
let isLifting = false
|
||||
let isAdjustingHeight = false
|
||||
let draggingWayPoint: WayPoint | null = null
|
||||
|
||||
const scene = viewer.scene
|
||||
handler.setInputAction((click: ScreenSpaceEventHandler.PositionedEvent) => {
|
||||
const object = scene.pick(click.position)
|
||||
if (object && object.id && object.id.wayPoint && object.id.wayPoint.selected) {
|
||||
isDragging = true
|
||||
draggingWayPoint = object.id.wayPoint
|
||||
|
||||
if (object && object.id) {
|
||||
// 检查是否点击了高度调整按钮
|
||||
if ((object.id as any).heightAdjustButton) {
|
||||
isAdjustingHeight = true
|
||||
draggingWayPoint = (object.id as any).heightAdjustButton
|
||||
scene.screenSpaceCameraController.enableRotate = false
|
||||
}
|
||||
// 检查是否点击了航点三角形(billboard)
|
||||
else if ((object.id as any).wayPoint && (object.id as any).wayPoint.selected) {
|
||||
isDragging = true
|
||||
draggingWayPoint = (object.id as any).wayPoint
|
||||
scene.screenSpaceCameraController.enableRotate = false
|
||||
}
|
||||
}
|
||||
}, ScreenSpaceEventType.LEFT_DOWN)
|
||||
|
||||
handler.setInputAction((click: ScreenSpaceEventHandler.PositionedEvent) => {
|
||||
@@ -146,10 +168,13 @@ export class Wayline {
|
||||
}, ScreenSpaceEventType.LEFT_DOWN, KeyboardEventModifier.ALT)
|
||||
|
||||
handler.setInputAction(() => {
|
||||
if (isDragging) {
|
||||
if (isDragging || isAdjustingHeight) {
|
||||
isDragging = false
|
||||
isAdjustingHeight = false
|
||||
|
||||
this.minimapViewer.camera.position = draggingWayPoint!.position.clone()
|
||||
if (draggingWayPoint) {
|
||||
this.minimapViewer.camera.position = draggingWayPoint.position.clone()
|
||||
}
|
||||
draggingWayPoint = null
|
||||
scene.screenSpaceCameraController.enableRotate = true
|
||||
}
|
||||
@@ -162,13 +187,32 @@ export class Wayline {
|
||||
|
||||
handler.setInputAction((movement: ScreenSpaceEventHandler.MotionEvent) => {
|
||||
const object = scene.pick(movement.endPosition)
|
||||
if (object && object.id && (object.id as any).wayPoint && (object.id as any).wayPoint.selectd) {
|
||||
if (object && object.id) {
|
||||
if ((object.id as any).heightAdjustButton) {
|
||||
scene.canvas.style.cursor = 'ns-resize' // 上下调整光标
|
||||
} else if ((object.id as any).wayPoint && (object.id as any).wayPoint.selected) {
|
||||
scene.canvas.style.cursor = 'move'
|
||||
} else {
|
||||
scene.canvas.style.cursor = 'default'
|
||||
}
|
||||
} else {
|
||||
scene.canvas.style.cursor = 'default'
|
||||
}
|
||||
|
||||
if (isDragging && draggingWayPoint) {
|
||||
// 高度调整:点击高度按钮后拖拽调整高度
|
||||
if (isAdjustingHeight && draggingWayPoint) {
|
||||
const deltaY = movement.endPosition.y - movement.startPosition.y
|
||||
const deltaHeight = deltaY * getVerticalMetersPerPixel(viewer, draggingWayPoint.position)
|
||||
|
||||
const lngLat = Cartographic.fromCartesian(draggingWayPoint.position)
|
||||
lngLat.height -= deltaHeight
|
||||
|
||||
draggingWayPoint.asl -= deltaHeight
|
||||
draggingWayPoint.alt -= deltaHeight
|
||||
draggingWayPoint.updatePosition(Cartesian3.fromRadians(lngLat.longitude, lngLat.latitude, lngLat.height))
|
||||
}
|
||||
// 左右平移:点击三角形后拖拽进行水平移动
|
||||
else if (isDragging && draggingWayPoint) {
|
||||
const endPosition = scene.globe.pick(scene.camera.getPickRay(movement.endPosition), scene)
|
||||
const startPosition = scene.globe.pick(scene.camera.getPickRay(movement.startPosition), scene)
|
||||
|
||||
@@ -294,6 +338,11 @@ export class Wayline {
|
||||
heading = heading % (2 * Math.PI)
|
||||
if (heading < 0) heading += 2 * Math.PI
|
||||
|
||||
// 限制pitch在-90度(正朝下)到+45度(朝上)之间
|
||||
const minPitch = -Math.PI / 2 // -90度
|
||||
const maxPitch = Math.PI / 4 // +45度
|
||||
pitch = CesiumMath.clamp(pitch, minPitch, maxPitch)
|
||||
|
||||
camera.setView({
|
||||
destination: position,
|
||||
orientation: {
|
||||
@@ -303,6 +352,15 @@ export class Wayline {
|
||||
}
|
||||
})
|
||||
|
||||
// 更新云台数据和偏航角到store
|
||||
const store = useStore()
|
||||
const pitchDegrees = CesiumMath.toDegrees(pitch)
|
||||
let yawDegrees = CesiumMath.toDegrees(heading)
|
||||
if (yawDegrees > 180) {
|
||||
yawDegrees = yawDegrees - 360
|
||||
}
|
||||
store.updateGimbalData(pitchDegrees, yawDegrees)
|
||||
|
||||
requestAnimationFrame(updateCameraControl)
|
||||
}
|
||||
|
||||
@@ -354,7 +412,11 @@ export class Wayline {
|
||||
heading = heading % (2 * Math.PI)
|
||||
if (heading < 0) heading += 2 * Math.PI
|
||||
|
||||
pitch = CesiumMath.clamp(pitch, -Math.PI / 2, Math.PI / 2)
|
||||
// 限制pitch在-90度(正朝下)到+45度(朝上)之间,水平是0
|
||||
// Cesium中pitch: -90度 = -Math.PI/2, +45度 = Math.PI/4, 0度 = 0
|
||||
const minPitch = -Math.PI / 2 // -90度
|
||||
const maxPitch = Math.PI / 4 // +45度
|
||||
pitch = CesiumMath.clamp(pitch, minPitch, maxPitch)
|
||||
|
||||
const position = Cartesian3.clone(minimapViewer.camera.position)
|
||||
minimapViewer.camera.setView({
|
||||
@@ -365,6 +427,14 @@ export class Wayline {
|
||||
roll: minimapViewer.camera.roll
|
||||
}
|
||||
})
|
||||
|
||||
// 更新云台数据和偏航角到store
|
||||
const pitchDegrees = CesiumMath.toDegrees(pitch)
|
||||
let yawDegrees = CesiumMath.toDegrees(heading)
|
||||
if (yawDegrees > 180) {
|
||||
yawDegrees = yawDegrees - 360
|
||||
}
|
||||
store.updateGimbalData(pitchDegrees, yawDegrees)
|
||||
}, ScreenSpaceEventType.MOUSE_MOVE)
|
||||
|
||||
handler.setInputAction(() => {
|
||||
@@ -377,20 +447,34 @@ export class Wayline {
|
||||
}
|
||||
|
||||
let currentFov = minimapViewer.camera.frustum.fov
|
||||
let zoomFactor = 2
|
||||
const minZoomFactor = 2
|
||||
const maxZoomFactor = 200
|
||||
let zoomFactor = store.zoomFactor || 1
|
||||
|
||||
handler.setInputAction(function (movement: any) {
|
||||
const delta = movement
|
||||
const currentMode = store.lensMode
|
||||
|
||||
if (currentMode === 'wide') {
|
||||
// 广角模式只能1x
|
||||
zoomFactor = 1
|
||||
} else {
|
||||
// 变焦模式可以1-56x
|
||||
const step = delta > 0 ? 1 : -1
|
||||
zoomFactor += step
|
||||
zoomFactor = CesiumMath.clamp(zoomFactor, 1, 56)
|
||||
}
|
||||
|
||||
zoomFactor += delta > 0 ? 2 : -2
|
||||
zoomFactor = CesiumMath.clamp(zoomFactor, minZoomFactor, maxZoomFactor)
|
||||
currentFov = fovFromZoom(zoomFactor)
|
||||
|
||||
store.updateZoomFactor(zoomFactor)
|
||||
minimapViewer.camera.frustum.fov = currentFov
|
||||
}, ScreenSpaceEventType.WHEEL)
|
||||
|
||||
// 初始化时根据模式设置变焦
|
||||
if (store.lensMode === 'wide') {
|
||||
zoomFactor = 1
|
||||
currentFov = fovFromZoom(zoomFactor)
|
||||
store.updateZoomFactor(zoomFactor)
|
||||
minimapViewer.camera.frustum.fov = currentFov
|
||||
}
|
||||
}
|
||||
|
||||
createWayline(): void {
|
||||
@@ -491,6 +575,26 @@ export class Wayline {
|
||||
)
|
||||
}
|
||||
|
||||
syncPointListToStore(): void {
|
||||
const store = useStore()
|
||||
const pointListInfo = this.pointList.map(point => {
|
||||
const cartographic = Cartographic.fromCartesian(point.position)
|
||||
return {
|
||||
index: point.index,
|
||||
asl: point.asl,
|
||||
alt: point.alt,
|
||||
fov: point.fov,
|
||||
selected: point.selected,
|
||||
longitude: CesiumMath.toDegrees(cartographic.longitude),
|
||||
latitude: CesiumMath.toDegrees(cartographic.latitude),
|
||||
heading: CesiumMath.toDegrees(point.orientation.heading),
|
||||
pitch: CesiumMath.toDegrees(point.orientation.pitch),
|
||||
roll: CesiumMath.toDegrees(point.orientation.roll)
|
||||
}
|
||||
})
|
||||
store.updatePointList(pointListInfo)
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
viewer.entities.remove(this.wayLineEntity)
|
||||
viewer.entities.remove(this.takeOffPointEntity)
|
||||
|
||||
+68
-5
@@ -7,6 +7,7 @@ import {
|
||||
HeightReference,
|
||||
LabelStyle,
|
||||
VerticalOrigin,
|
||||
HorizontalOrigin,
|
||||
PerspectiveFrustum,
|
||||
DebugCameraPrimitive,
|
||||
Entity,
|
||||
@@ -28,6 +29,7 @@ export class WayPoint {
|
||||
selected!: boolean
|
||||
heightLabel!: Entity
|
||||
wayPointEntity!: Entity
|
||||
heightAdjustButton!: Entity
|
||||
orientation: { heading: number; pitch: number; roll: number }
|
||||
debugCameraPrimitive: any
|
||||
distanceLabel1: any
|
||||
@@ -91,11 +93,20 @@ export class WayPoint {
|
||||
const pointList = this.wayline.pointList
|
||||
this.position = position
|
||||
this.wayPointEntity.position = position
|
||||
if (this.heightLabel) {
|
||||
this.heightLabel.position = position
|
||||
}
|
||||
if (this.heightAdjustButton) {
|
||||
this.heightAdjustButton.position = position
|
||||
}
|
||||
if (this.debugCameraPrimitive) {
|
||||
this.debugCameraPrimitive._camera.position = position
|
||||
}
|
||||
|
||||
const text = `ASL: ${this.asl.toFixed(1)} m\nHAE: ${this.alt.toFixed(1)} m`
|
||||
if (this.heightLabel) {
|
||||
this.heightLabel.label.text = text
|
||||
}
|
||||
|
||||
const index = this.index
|
||||
const prev = pointList[index - 1]
|
||||
@@ -107,9 +118,11 @@ export class WayPoint {
|
||||
position
|
||||
)
|
||||
const midpoint = Cartesian3.lerp(prev.position, position, 0.5, new Cartesian3())
|
||||
if (this.distanceLabel1) {
|
||||
this.distanceLabel1.position = midpoint
|
||||
this.distanceLabel1.label.text = distance.toFixed(1) + 'm'
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
const distance = Cartesian3.distance(
|
||||
@@ -117,17 +130,22 @@ export class WayPoint {
|
||||
next.position
|
||||
)
|
||||
const midpoint = Cartesian3.lerp(position, next.position, 0.5, new Cartesian3())
|
||||
if (this.distanceLabel2) {
|
||||
this.distanceLabel2.position = midpoint
|
||||
this.distanceLabel2.label.text = distance.toFixed(1) + 'm'
|
||||
}
|
||||
}
|
||||
|
||||
this.wayline.syncPointListToStore()
|
||||
}
|
||||
|
||||
select(): void {
|
||||
this.selected = true
|
||||
this.wayPointEntity.billboard.image = generateHighlightedNumberSvgDataUrl(this.index + 1)
|
||||
|
||||
this.showDistanceLabels()
|
||||
this.showHeightLabel()
|
||||
this.showHeightAdjustButton()
|
||||
|
||||
const camera = new Camera(viewer.scene)
|
||||
camera.frustum = new PerspectiveFrustum({
|
||||
@@ -155,17 +173,20 @@ export class WayPoint {
|
||||
})
|
||||
|
||||
viewer.scene.primitives.add(this.debugCameraPrimitive)
|
||||
this.wayline.syncPointListToStore()
|
||||
}
|
||||
|
||||
unselect(): void {
|
||||
this.selected = false
|
||||
this.wayPointEntity.billboard.image = generateNumberSvgDataUrl(this.index + 1)
|
||||
|
||||
viewer.entities.remove(this.distanceLabel0)
|
||||
viewer.entities.remove(this.distanceLabel1)
|
||||
viewer.entities.remove(this.distanceLabel2)
|
||||
viewer.entities.remove(this.heightLabel)
|
||||
viewer.scene.primitives.remove(this.debugCameraPrimitive)
|
||||
if (this.distanceLabel0) viewer.entities.remove(this.distanceLabel0)
|
||||
if (this.distanceLabel1) viewer.entities.remove(this.distanceLabel1)
|
||||
if (this.distanceLabel2) viewer.entities.remove(this.distanceLabel2)
|
||||
if (this.heightLabel) viewer.entities.remove(this.heightLabel)
|
||||
if (this.heightAdjustButton) viewer.entities.remove(this.heightAdjustButton)
|
||||
if (this.debugCameraPrimitive) viewer.scene.primitives.remove(this.debugCameraPrimitive)
|
||||
this.wayline.syncPointListToStore()
|
||||
}
|
||||
distanceLabel0(distanceLabel0: any) {
|
||||
throw new Error('Method not implemented.')
|
||||
@@ -175,13 +196,20 @@ export class WayPoint {
|
||||
const pointList = this.wayline.pointList
|
||||
|
||||
viewer.entities.remove(this.wayPointEntity)
|
||||
if (this.heightAdjustButton) {
|
||||
viewer.entities.remove(this.heightAdjustButton)
|
||||
}
|
||||
if (this.debugCameraPrimitive) {
|
||||
viewer.scene.primitives.remove(this.debugCameraPrimitive)
|
||||
}
|
||||
|
||||
pointList.splice(this.index, 1)
|
||||
for (let i = this.index; i < pointList.length; i++) {
|
||||
pointList[i].wayPointEntity.billboard.image = generateNumberSvgDataUrl(i + 1)
|
||||
pointList[i].index = i
|
||||
}
|
||||
|
||||
this.wayline.syncPointListToStore()
|
||||
}
|
||||
|
||||
private showHeightLabel(): void {
|
||||
@@ -206,6 +234,24 @@ export class WayPoint {
|
||||
})
|
||||
}
|
||||
|
||||
private showHeightAdjustButton(): void {
|
||||
const { position } = this
|
||||
const buttonUrl = generateHeightAdjustButtonSvgDataUrl()
|
||||
|
||||
this.heightAdjustButton = viewer.entities.add({
|
||||
position,
|
||||
heightAdjustButton: this,
|
||||
billboard: {
|
||||
image: buttonUrl,
|
||||
scale: 0.8,
|
||||
verticalOrigin: VerticalOrigin.CENTER,
|
||||
horizontalOrigin: HorizontalOrigin.LEFT,
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
||||
pixelOffset: new Cartesian2(25, 0) // 在航点右侧显示
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private showDistanceLabels(): void {
|
||||
const index = this.index
|
||||
const pointList = this.wayline.pointList
|
||||
@@ -310,3 +356,20 @@ function generateHighlightedNumberSvgDataUrl(number: number): string {
|
||||
|
||||
return `data:image/svg+xml,${encoded}`
|
||||
}
|
||||
|
||||
function generateHeightAdjustButtonSvgDataUrl(): string {
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 40" width="24" height="40">
|
||||
<rect width="24" height="40" rx="4" fill="#00D690" fill-opacity="0.9" stroke="#FFF" stroke-width="1.5"/>
|
||||
<path d="M12 8 L8 12 L12 16 M12 8 L16 12 L12 16" stroke="#FFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="20" x2="12" y2="32" stroke="#FFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M12 32 L8 28 L12 24 M12 32 L16 28 L12 24" stroke="#FFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
const encoded = encodeURIComponent(svg)
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/"/g, '%22')
|
||||
|
||||
return `data:image/svg+xml,${encoded}`
|
||||
}
|
||||
Reference in new issue
Block a user