web-map/src/components/map-toolbar.vue

307 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import SvgIcon from '@common/svg-icon.vue';
import type { EditorService } from '@core/editor.service';
import { useToolbar } from '@core/useToolbar';
import { computed, inject, type InjectionKey, onBeforeUnmount, onMounted, ref, type ShallowRef } from 'vue';
// 通用地图工具栏(右下角),临时中文按钮
// 功能:放大、缩小、适配视图、全屏、截图、网格(占位)
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
containerEl?: HTMLElement | null;
};
const props = defineProps<Props>();
const editorRef = inject(props.token)!;
const isFullscreen = computed(() => !!document.fullscreenElement);
// 使用 useToolbar 的相关能力(内部使用 jumpToPosition、修改 store 等)
const {
toggleGrid: _toggleGrid,
toggleRule: _toggleRule,
fitView: _fitView,
zoomIn: _zoomIn,
zoomOut: _zoomOut,
} = useToolbar();
const zoomIn = () => {
if (editorRef.value) _zoomIn(editorRef.value);
};
const zoomOut = () => {
if (editorRef.value) _zoomOut(editorRef.value);
};
const fitView = async () => {
if (editorRef.value) await _fitView(editorRef.value);
};
const toggleFullscreen = async () => {
try {
const el = props.containerEl || document.documentElement;
if (!document.fullscreenElement) {
await el.requestFullscreen?.();
} else {
await document.exitFullscreen?.();
}
} catch (e) {
console.warn('全屏切换失败', e);
}
};
const downloadBase64 = (base64: string, filename = 'map.png') => {
const a = document.createElement('a');
a.href = base64;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const exportImage = () => {
try {
const base64 = editorRef.value?.toPng?.(2);
if (base64) downloadBase64(base64, '地图截图.png');
} catch (e) {
console.warn('截图失败', e);
}
};
// 网格/标尺开关:调用 useToolbar 封装
const toggleGrid = () => editorRef.value && _toggleGrid(editorRef.value);
const toggleRule = () => editorRef.value && _toggleRule(editorRef.value);
// =============== 测量尺(像素距离) ===============
const measuring = ref(false);
const start = ref<{ x: number; y: number } | null>(null);
const end = ref<{ x: number; y: number } | null>(null);
const current = ref<{ x: number; y: number } | null>(null);
const distance = computed(() => {
const s = start.value;
const e = end.value || current.value;
if (!measuring.value || !s || !e) return 0;
const dx = e.x - s.x;
const dy = e.y - s.y;
return Math.sqrt(dx * dx + dy * dy);
});
const midpoint = computed(() => {
const s = start.value;
const e = end.value || current.value;
if (!measuring.value || !s || !e) return { x: 0, y: 0 };
return { x: (s.x + e.x) / 2, y: (s.y + e.y) / 2 };
});
const toggleMeasure = () => {
measuring.value = !measuring.value;
if (!measuring.value) {
// 退出时清空
start.value = null;
end.value = null;
current.value = null;
}
};
const onKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
measuring.value = false;
start.value = null;
end.value = null;
current.value = null;
}
};
const onMouseDownOverlay = (e: MouseEvent) => {
if (!measuring.value) return;
const p = { x: e.clientX, y: e.clientY };
if (!start.value) {
start.value = p;
end.value = null;
current.value = null;
} else if (!end.value) {
end.value = p;
} else {
// 已有一段完成,继续点击则开始新的测量
start.value = p;
end.value = null;
current.value = null;
}
};
const onMouseMoveOverlay = (e: MouseEvent) => {
if (!measuring.value) return;
if (start.value && !end.value) {
current.value = { x: e.clientX, y: e.clientY };
}
};
onMounted(() => {
window.addEventListener('keydown', onKeydown);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeydown);
});
</script>
<template>
<div class="map-toolbar">
<a-space size="small">
<a-button class="icon-btn tool-btn" size="small" :title="'放大'" @click="zoomIn">
<SvgIcon name="enlarge" :size="18" :forceColor="true" />
</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'缩小'" @click="zoomOut">
<SvgIcon name="shrink" :size="18" :forceColor="true" />
</a-button>
<!-- <a-button class="icon-btn tool-btn" size="small" :title="'适配视图'" @click="fitView"> 适配视图 </a-button> -->
<a-button
class="icon-btn tool-btn"
size="small"
type="primary"
:ghost="!measuring"
:class="{ measuring }"
@click="toggleMeasure"
:title="measuring ? '退出测量' : '测量尺'"
>
<SvgIcon name="rule" :size="16" :forceColor="true" :active="measuring" />
</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'标尺'" @click="toggleRule">标尺</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'网格'" @click="toggleGrid">网格</a-button>
<!-- <a-button class="icon-btn tool-btn" size="small" :title="'截图'" @click="exportImage">截图</a-button>
<a-button
class="icon-btn tool-btn"
size="small"
:title="isFullscreen ? '退出全屏' : '全屏'"
@click="toggleFullscreen"
>
{{ isFullscreen ? '退出全屏' : '全屏' }}
</a-button> -->
</a-space>
</div>
<!-- 测量叠加层全屏SVG仅在测量模式下启用点击事件 -->
<div
class="measure-overlay"
:class="{ active: measuring }"
@mousedown="onMouseDownOverlay"
@mousemove="onMouseMoveOverlay"
>
<svg class="measure-svg" xmlns="http://www.w3.org/2000/svg">
<g v-if="measuring && start && (end || current)">
<defs>
<marker id="arrow" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
<path d="M0,0 L0,6 L6,3 z" fill="#1e90ff" />
</marker>
</defs>
<line
:x1="start.x"
:y1="start.y"
:x2="(end || current)!.x"
:y2="(end || current)!.y"
stroke="#1e90ff"
stroke-width="2"
marker-end="url(#arrow)"
/>
<!-- 起点/终点标记 -->
<circle :cx="start.x" :cy="start.y" r="4" fill="#1e90ff" />
<circle :cx="(end || current)!.x" :cy="(end || current)!.y" r="4" fill="#1e90ff" />
<!-- 文本标签像素距离 -->
<g :transform="`translate(${midpoint.x}, ${midpoint.y})`">
<rect x="-40" y="-22" width="80" height="20" rx="4" ry="4" fill="rgba(0,0,0,0.6)" />
<text x="0" y="-8" fill="#fff" font-size="12" text-anchor="middle">{{ distance.toFixed(1) }} px</text>
</g>
</g>
<g v-else-if="measuring">
<text x="20" y="40" fill="#fff" font-size="13">提示依次点击两点进行测量 Esc 退出</text>
</g>
</svg>
</div>
</template>
<style scoped lang="scss">
.map-toolbar {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 101;
background: rgba(0, 0, 0, 0.45);
padding: 8px 10px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
color: #fff;
:deep(.ant-btn) {
padding: 0 10px;
}
.icon-btn {
padding: 8px;
margin: 0 4px;
// width: 32px;
// height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: get-color(primary);
}
/* 提升特异性,覆盖全局 .ant-btn.icon-btn.tool-btn:hover 的 !important 背景色 */
:deep(.ant-btn.icon-btn.tool-btn:hover) {
background-color: transparent !important;
color: #0dbb8a !important;
box-shadow: none !important;
}
.ant-btn.icon-btn.tool-btn:not(:disabled):hover {
background-color: transparent !important;
color: #0dbb8a !important;
box-shadow: none !important;
}
}
/* 测量按钮激活状态(背景红色) */
.map-toolbar {
.ant-btn.icon-btn.tool-btn {
width: 32px;
height: 32px;
&:focus-visible {
border: none !important;
outline: none !important;
}
}
/* 常规与悬停覆盖(需高特异性与 !important 以压过上面的 hover 规则) */
.ant-btn.icon-btn.tool-btn.measuring {
background-color: #0dbb8a;
width: 32px;
height: 32px;
border-color: #0dbb8a;
}
// :deep(.ant-btn.icon-btn.tool-btn.measuring:hover) {
// background-color: #ff4d4f !important;
// border-color: #ff4d4f !important;
// color: #fff !important;
// box-shadow: none !important;
// }
}
/* 网格背景(占位实现),通过 toggleGrid 开关 */
:global(.editor-container.grid-bg) {
background-image:
linear-gradient(90deg, rgba(120, 120, 120, 0.25) 1px, transparent 0),
linear-gradient(rgba(120, 120, 120, 0.25) 1px, transparent 0);
background-size: 20px 20px;
}
/* 测量叠加层 */
.measure-overlay {
position: fixed;
inset: 0;
z-index: 109; /* 高于面板,低于全屏按钮浮层 */
pointer-events: none;
}
.measure-overlay.active {
pointer-events: auto; /* 仅测量时可交互 */
}
.measure-svg {
width: 100%;
height: 100%;
}
</style>