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

469 lines
12 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 type { EditorService } from '@core/editor.service';
import { useToolbar } from '@core/useToolbar';
import { computed, inject, type InjectionKey, onBeforeUnmount, onMounted, ref, type ShallowRef } from 'vue';
import ColorConfigPanel from './color/color-config-panel.vue';
// 通用地图工具栏(右下角),临时中文按钮
// 功能:放大、缩小、适配视图、全屏、截图、网格(占位)
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
containerEl?: HTMLElement | null;
};
const props = defineProps<Props>();
const editorRef = inject(props.token)!;
// const isFullscreen = computed(() => !!document.fullscreenElement);
// 颜色配置面板状态
const showColorConfig = ref(false);
// 使用 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 toggleColorConfig = () => {
showColorConfig.value = !showColorConfig.value;
// 如果打开颜色配置,关闭测量模式
if (showColorConfig.value && measuring.value) {
measuring.value = false;
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;
// 也关闭颜色配置面板
showColorConfig.value = false;
}
};
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);
});
// 定义组件名称
defineOptions({
name: 'MapToolbar',
});
</script>
<template>
<div class="map-toolbar">
<a-space size="small">
<a-button class="icon-btn tool-btn" size="small" :title="'放大'" @click="zoomIn">
<img src="/src/assets/icons/png/enlarge.png" alt="放大" class="toolbar-icon" />
</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'缩小'" @click="zoomOut">
<img src="/src/assets/icons/png/shrink.png" alt="缩小" class="toolbar-icon" />
</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 ? '退出测量' : '测量尺'"
>
<img src="/src/assets/icons/png/rule.png" alt="测量尺" class="toolbar-icon" :class="{ 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="'颜色配置'"
:type="showColorConfig ? 'primary' : 'default'"
:ghost="!showColorConfig"
@click="toggleColorConfig"
>
<img
src="/src/assets/icons/png/theme.png"
alt="颜色配置"
class="toolbar-icon"
:class="{ active: showColorConfig }"
/>
</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>
<!-- 颜色配置面板 -->
<div v-if="showColorConfig" class="color-config-overlay" @click.self="showColorConfig = false">
<div class="color-config-container" @click.stop>
<div class="color-config-header">
<h3>编辑器颜色配置</h3>
<a-button type="text" size="small" @click="showColorConfig = false" class="close-btn">
<SvgIcon name="close" :size="16" />
</a-button>
</div>
<div class="color-config-content">
<ColorConfigPanel />
</div>
</div>
</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 || 0"
:y2="(end || current)?.y || 0"
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 || 0" :cy="(end || current)?.y || 0" 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: #eeeff2;
padding: 8px 12px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
color: #333;
:deep(.ant-btn) {
padding: 0 10px;
border: none;
box-shadow: none;
}
.icon-btn {
padding: 6px;
margin: 0 2px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background-color: rgba(13, 187, 138, 0.1);
}
}
.toolbar-icon {
width: 18px;
height: 18px;
transition: all 0.2s ease;
}
}
/* 测量按钮激活状态(背景红色) */
.map-toolbar {
.ant-btn.icon-btn.tool-btn {
width: 32px;
height: 32px;
&:focus-visible {
border: none !important;
outline: none !important;
}
}
/* 测量按钮激活状态 */
.ant-btn.icon-btn.tool-btn.measuring {
background-color: #0dbb8a;
width: 32px;
height: 32px;
border-color: #0dbb8a;
.toolbar-icon {
filter: brightness(0) invert(1);
}
}
}
/* 网格背景(占位实现),通过 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;
}
/* 颜色配置面板 */
.color-config-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.color-config-container {
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 90vw;
max-height: 90vh;
width: 800px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.color-config-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #e8e8e8;
background: #fafafa;
}
.color-config-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
color: #999;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
color: #666;
background: #f0f0f0;
}
.color-config-content {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* 测量叠加层 */
.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>
<!-- 深色主题样式 -->
<style lang="scss">
[theme='dark'] .map-toolbar {
background: #36393a;
color: rgba(255, 255, 255, 0.6509803922);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
.toolbar-icon {
filter: brightness(0) invert(1);
}
.ant-btn.icon-btn.tool-btn:not(:disabled) {
color: rgba(255, 255, 255, 0.6509803922) !important;
&:hover {
background-color: #464949 !important;
}
}
.ant-btn.icon-btn.tool-btn.measuring {
background-color: #0dbb8a;
border-color: #0dbb8a;
.toolbar-icon {
filter: brightness(0) invert(1);
}
}
}
[theme='dark'] .color-config-container {
background: #1f1f1f;
}
[theme='dark'] .color-config-header {
background: #2a2a2a;
border-bottom: 1px solid #404040;
}
[theme='dark'] .color-config-header h3 {
color: #e6e6e6;
}
[theme='dark'] .close-btn {
color: #999;
}
[theme='dark'] .close-btn:hover {
color: #ccc;
background: #404040;
}
</style>