WholeProcessPlatform/frontend/src/modules/MapStudio/components/LeftToolbar.vue

417 lines
12 KiB
Vue
Raw Normal View History

2026-07-28 18:28:07 +08:00
<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>