WholeProcessPlatform/frontend/src/modules/MapStudio/components/LeftToolbar.vue
2026-07-28 18:28:07 +08:00

417 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.

<template>
<div class="map-studio__toolbar">
<button class="map-studio__toolbar-btn" title="放大" @click="zoomIn">
<span class="btn-icon text-[16px]"><PlusOutlined /></span>
<span class="btn-label">放大</span>
</button>
<button class="map-studio__toolbar-btn" title="缩小" @click="zoomOut">
<span class="btn-icon text-[16px]"><MinusOutlined /></span>
<span class="btn-label">缩小</span>
</button>
<div class="map-studio__toolbar-divider" />
<button
class="map-studio__toolbar-btn"
title="全屏"
@click="toggleFullScreen"
>
<span class="btn-icon text-[20px]"></span>
<span class="btn-label">全屏</span>
</button>
<div class="map-studio__toolbar-divider" />
<button class="map-studio__toolbar-btn" title="重置" @click="resetMap">
<span class="btn-icon text-[16px]"></span>
<span class="btn-label">重置</span>
</button>
<div class="map-studio__toolbar-divider" />
<button
class="map-studio__toolbar-btn"
title="撤回"
:disabled="!canUndo"
@click="undo"
>
<span class="btn-icon text-[16px]"></span>
<span class="btn-label">撤回</span>
</button>
<button
class="map-studio__toolbar-btn"
title="取消撤回"
:disabled="!canRedo"
@click="redo"
>
<span class="btn-icon text-[16px]"></span>
<span class="btn-label">取消</span>
</button>
<div class="map-studio__toolbar-divider" />
<a-dropdown
v-model:open="importDropdownOpen"
placement="rightTop"
:trigger="['click']"
:getPopupContainer="getPopupContainer"
>
<button class="map-studio__toolbar-btn" title="导入">
<span class="btn-icon text-[16px]">📁</span>
<span class="btn-label">导入</span>
</button>
<template #overlay>
<a-menu>
<a-menu-item @click="triggerImportAnchors">
<span>导入锚点</span>
</a-menu-item>
<a-menu-item @click="triggerImportImage">
<span>导入图片</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<!-- 隐藏的文件输入用于导入 PNG 图片 -->
<input
ref="importFileInput"
type="file"
accept="image/png"
style="display: none"
@change="onImageFileSelected"
/>
<!-- 导入锚点 Modal -->
<ImportAnchorModal v-model:visible="importAnchorModalVisible" />
<!-- 添加表格按钮 -->
<button
class="map-studio__toolbar-btn"
title="添加表格"
@click="triggerAddTable"
>
<span class="btn-icon text-[16px]"></span>
<span class="btn-label">表格</span>
</button>
<!-- 保存配图 Modal -->
<SaveModal v-model:visible="saveVisible" @saved="onSaved" />
<!-- 历史记录 Modal -->
<HistoryModal v-model:visible="historyVisible" />
<div class="map-studio__toolbar-divider" />
<!-- 绘制工具下拉 -->
<a-dropdown
v-model:open="drawDropdownOpen"
placement="rightTop"
:trigger="['click']"
:getPopupContainer="getPopupContainer"
>
<button
class="map-studio__toolbar-btn"
:class="{ 'is-active': drawToolActive !== null }"
title="绘制"
>
<span class="btn-icon text-[16px]"></span>
<span class="btn-label">{{
drawToolActive ? drawToolLabel : '绘制'
}}</span>
</button>
<template #overlay>
<a-menu @click="onDrawToolSelect" :selectedKeys="menuSelectedKeys">
<a-menu-item key="point">📍 标记点</a-menu-item>
<a-menu-item key="line"> 折线</a-menu-item>
<a-menu-item key="polygon"> 多边形</a-menu-item>
<a-menu-item key="rect"> 矩形</a-menu-item>
<a-menu-item key="circle"> 圆形</a-menu-item>
<a-menu-item key="freehand"> 自由手绘</a-menu-item>
<a-menu-item key="text">A 文字标注</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<!-- 若当前有绘制的工具激活显示删除按钮 -->
<button
v-if="drawToolActive !== null"
class="map-studio__toolbar-btn"
title="删除选中"
@click="deleteSelected"
>
<span class="btn-icon text-[16px]">🗑</span>
<span class="btn-label">删除</span>
</button>
<div class="map-studio__toolbar-divider" />
<button
class="map-studio__toolbar-btn"
title="保存配图"
@click="saveVisible = true"
>
<span class="btn-icon text-[16px]">💾</span>
<span class="btn-label">保存</span>
</button>
<button
class="map-studio__toolbar-btn"
title="历史记录"
@click="historyVisible = true"
>
<span class="btn-icon text-[16px]">🔄</span>
<span class="btn-label">历史</span>
</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, h } from 'vue';
import { storeToRefs } from 'pinia';
import { Modal, Input } from 'ant-design-vue';
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue';
import { useMapStudioStore } from '../stores/mapStudioStore';
import type { DrawToolType } from '../types';
import type { TableOverlayData } from '../types';
import { useUndoRedo } from '../composables/useUndoRedo';
import ImportAnchorModal from './ImportAnchorModal.vue';
import SaveModal from './SaveModal.vue';
import HistoryModal from './HistoryModal.vue';
const store = useMapStudioStore();
const {
mapInstance,
importedImages,
selectedImageId,
activeTab,
drawToolActive
} = storeToRefs(store);
const { canUndo, canRedo, undo, redo } = useUndoRedo();
const { drawCheckpoint } = useUndoRedo();
const importDropdownOpen = ref(false);
const importFileInput = ref<HTMLInputElement | null>(null);
const importAnchorModalVisible = ref(false);
const saveVisible = ref(false);
const historyVisible = ref(false);
const drawDropdownOpen = ref(false);
function getPopupContainer() {
return document.querySelector('.map-studio') || document.body;
}
const DRAW_TOOL_LABELS: Record<DrawToolType, string> = {
point: '标记点',
line: '折线',
polygon: '多边形',
rect: '矩形',
circle: '圆形',
freehand: '自由手绘',
text: '文字标注',
select: '选中/编辑'
};
const drawToolLabel = computed(() =>
drawToolActive.value ? DRAW_TOOL_LABELS[drawToolActive.value] ?? '绘制' : ''
);
/** 菜单选中项,当前激活的工具高亮显示 */
const menuSelectedKeys = computed(() =>
drawToolActive.value ? [drawToolActive.value] : []
);
let imageIdCounter = 0;
let tableIdCounter = 0;
function triggerAddTable() {
const defaultRows = 3;
const defaultCols = 4;
let tempRows = defaultRows;
let tempCols = defaultCols;
Modal.confirm({
title: '添加表格',
content: () =>
h(
'div',
{ style: 'display:flex;flex-direction:column;gap:16px;padding:8px 0;' },
[
h('div', { style: 'display:flex;align-items:center;gap:8px;' }, [
h('span', { style: 'white-space:nowrap;' }, '行数:'),
h(Input, {
style: 'width:120px',
placeholder: '行数',
value: String(tempRows),
type: 'number',
min: 1,
max: 50,
onInput: (e: any) => {
const v = parseInt(e.target?.value ?? '');
if (!isNaN(v) && v > 0) tempRows = v;
}
})
]),
h('div', { style: 'display:flex;align-items:center;gap:8px;' }, [
h('span', { style: 'white-space:nowrap;' }, '列数:'),
h(Input, {
style: 'width:120px',
placeholder: '列数',
value: String(tempCols),
type: 'number',
min: 1,
max: 50,
onInput: (e: any) => {
const v = parseInt(e.target?.value ?? '');
if (!isNaN(v) && v > 0) tempCols = v;
}
})
])
]
),
onOk: () => {
const rows = Math.max(tempRows, 1);
const cols = Math.max(tempCols, 1);
// 生成默认单元格数据
const cellData: string[][] = [];
for (let r = 0; r < rows; r++) {
const row: string[] = [];
for (let c = 0; c < cols; c++) {
row.push('');
}
cellData.push(row);
}
tableIdCounter++;
const table: TableOverlayData = {
id: `table_${Date.now()}_${tableIdCounter}`,
x: 30,
y: 30,
width: 400,
height: 60 + rows * 30,
rows,
cols,
cellData,
fontSize: 14,
fontFamily: 'Microsoft YaHei',
textColor: '#333333',
borderColor: '#000000',
borderWidth: 1,
fillColor: '#ffffff',
fillTransparent: false,
fontBold: false,
hAlign: 'center',
vAlign: 'middle',
rowHeights: Array(rows).fill(30),
colWidths: Array(cols).fill(80)
};
store.tableOverlays.push(table);
store.selectedTableId = table.id;
store.activeTab = 'table';
// 触发撤回快照
drawCheckpoint();
}
});
}
function triggerImportImage() {
importDropdownOpen.value = false;
importFileInput.value?.click();
}
function triggerImportAnchors() {
importDropdownOpen.value = false;
importAnchorModalVisible.value = true;
}
function onImageFileSelected(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const src = reader.result as string;
imageIdCounter++;
// 默认显示在左上角,大小为 200x200
importedImages.value.push({
id: `img_${Date.now()}_${imageIdCounter}`,
src,
x: 10,
y: 10,
width: 200,
height: 200
});
// 选中刚导入的图片并切换到图片 tab
selectedImageId.value =
importedImages.value[importedImages.value.length - 1].id;
activeTab.value = 'images';
};
reader.readAsDataURL(file);
// 清空 input 以允许重复选择同一文件
input.value = '';
}
// ── 绘制工具 ──
function onDrawToolSelect(ev: { key: string }) {
drawDropdownOpen.value = false;
const key = ev.key as DrawToolType;
if (key === drawToolActive.value) {
// 点击已激活的工具则取消
drawToolActive.value = null;
} else {
drawToolActive.value = key;
}
// 切到绘制图形 tab
if (key === 'select' && drawToolActive.value === 'select') {
activeTab.value = 'draw';
}
}
function deleteSelected() {
// 由 useDrawTools 提供的删除方法通过事件通信
window.dispatchEvent(new CustomEvent('map-studio:delete-draw'));
}
function zoomIn() {
if (!mapInstance.value) return;
const view = mapInstance.value.getView();
const zoom = view.getZoom() ?? 1;
view.setZoom(zoom + 0.2);
}
function zoomOut() {
if (!mapInstance.value) return;
const view = mapInstance.value.getView();
const zoom = view.getZoom() ?? 1;
view.setZoom(zoom - 0.2);
}
function toggleFullScreen() {
if (!document.fullscreenElement) {
// 全屏当前 MapStudio 组件容器
const studioEl = document.querySelector('.map-studio') as HTMLElement;
if (!studioEl) return;
studioEl.requestFullscreen().catch(err => {
console.error('进入全屏失败:', err);
});
} else {
document.exitFullscreen().catch(err => {
console.error('退出全屏失败:', err);
});
}
}
function resetMap() {
// 有裁切时,将视角恢复到裁切区域;无裁切时恢复到全国视角
if (store.clipMode !== 'none' && store.fitClipView) {
store.fitClipView();
return;
}
if (store.resetView) {
store.resetView();
}
}
function onSaved() {
// 保存成功后关闭 modal可在此添加提示
console.log('[LeftToolbar] 配图保存成功');
}
</script>