From 88d1c41cb90be2c3d583e4c0533e297f435fd05f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 15 Jun 2026 21:39:37 +0800 Subject: [PATCH] feat: MapLibre integration + app init + deploy - Map pages wired to real MapLibre engine (MapManager/Factory/composables) - mapMonitoring page with device markers and popups - App.vue with keep-alive + route transitions - main.ts with global error handlers + MapLibre CSS - index.html with WeChat/WxJSBridge detection - Deployed to ygcxy.top (nginx-static via Traefik) --- index.html | 15 ++ src/App.vue | 62 +++++- src/main.ts | 28 +++ src/views/map/index.vue | 140 +++++++++++-- .../monitoringEquipment/mapMonitoring.vue | 190 ++++++++++++++++-- 5 files changed, 394 insertions(+), 41 deletions(-) diff --git a/index.html b/index.html index c1570df..b83b49a 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,21 @@
+ + diff --git a/src/App.vue b/src/App.vue index 4c4e20a..d3254bd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,13 +3,26 @@ * 应用根组件 * * 使用 Vant ConfigProvider 包裹路由视图, - * 提供全局主题配置和路由出口。 + * 提供全局主题配置、路由缓存 (keep-alive) 和过渡动画。 */ +import { useAppStore } from '@/stores/app' + +const appStore = useAppStore() @@ -19,4 +32,49 @@ width: 100%; min-height: 100vh; } + +/* ── 路由过渡动画 ── */ + +/* 淡入 + 滑动(默认) */ +.fade-slide-enter-active, +.fade-slide-leave-active { + transition: opacity 0.25s ease, transform 0.25s ease; +} + +.fade-slide-enter-from { + opacity: 0; + transform: translateX(20px); +} + +.fade-slide-leave-to { + opacity: 0; + transform: translateX(-20px); +} + +/* 淡入 */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.2s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +/* 向上滑动(用于详情页等) */ +.slide-up-enter-active, +.slide-up-leave-active { + transition: opacity 0.3s ease, transform 0.3s ease; +} + +.slide-up-enter-from { + opacity: 0; + transform: translateY(30px); +} + +.slide-up-leave-to { + opacity: 0; + transform: translateY(-30px); +} diff --git a/src/main.ts b/src/main.ts index 2c335fe..2b6acf1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,15 +42,38 @@ import { // ── Vant 样式(组件样式按需引入) ── import 'vant/lib/index.css' +// ── MapLibre GL 样式 ── +import 'maplibre-gl/dist/maplibre-gl.css' + // ── 全局样式 ── import './styles/index.scss' +// ── Store ── +import { useAppStore } from './stores/app' + // ── 路由 ── import router from './router' // ── 根组件 ── import App from './App.vue' +// ══════════════════════════════════════════════ +// 全局错误处理 +// ══════════════════════════════════════════════ + +/** 捕获未处理的 Promise 拒绝 */ +window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + console.error('[Yuto Water H5] 未处理的 Promise 拒绝:', event.reason) + event.preventDefault() +}) + +/** 捕获全局 JavaScript 错误 */ +window.addEventListener('error', (event: ErrorEvent) => { + console.error('[Yuto Water H5] 全局错误:', event.message, 'at', event.filename, ':', event.lineno) + // 防止错误冒泡导致白屏 + event.preventDefault() +}) + // ══════════════════════════════════════════════ // 创建应用 // ══════════════════════════════════════════════ @@ -61,6 +84,11 @@ const app = createApp(App) const pinia = createPinia() app.use(pinia) +// ── 初始化暗黑模式(从 localStorage 读取并应用) ── +// useAppStore 内部已通过 watch(darkMode, ..., { immediate: true }) +// 在首次创建时自动同步 dark 类名到 元素 +useAppStore() + // ── 注册 Vant 组件(组件类) ── // Toast / Dialog / ImagePreview 为函数式 API, // 直接从 vant 导入即可使用,无需 app.use 注册。 diff --git a/src/views/map/index.vue b/src/views/map/index.vue index c7b0930..5966b57 100644 --- a/src/views/map/index.vue +++ b/src/views/map/index.vue @@ -5,13 +5,29 @@ * MapLibre 地图容器,底部工具栏可切换图层、 * 打开弹出窗口等操作。 */ -import { ref } from 'vue' +import { ref, onMounted, onUnmounted } from 'vue' import { useRouter } from 'vue-router' +import { MapManager } from '@/map/MapManager' +import { MapFactory } from '@/map/MapFactory' +import { useMap } from '@/map/composables/useMap' +import { useLayer } from '@/map/composables/useLayer' +import { usePopup } from '@/map/composables/usePopup' import TckzPop from './tckzPop.vue' import YbPop from './ybPop.vue' const router = useRouter() +// ── 地图状态 ────────────────────────────────────────────── +const { map, loading, error, isReady } = useMap() +const { addLayer, removeLayer } = useLayer() +const { showPopup, hidePopup } = usePopup() + +/** 地图容器 ref */ +const mapContainer = ref(null) + +/** 地图初始化错误(map 本身加载失败,区别于运行错误) */ +const initError = ref(null) + /** 底部工具栏状态 */ const tools = [ { icon: 'location-o', label: '定位' }, @@ -25,8 +41,60 @@ const tools = [ const showTckz = ref(false) const showYb = ref(false) +// ── 初始化地图 ─────────────────────────────────────────── +onMounted(() => { + const manager = MapManager.getInstance() + + // 如果地图已存在,复用即可 + if (manager.hasMap()) return + + const container = document.getElementById('ytmap-container') + if (!container) { + initError.value = '地图容器元素 #ytmap-container 未找到' + return + } + mapContainer.value = container + + try { + MapFactory.createWithBasemap('osm', { + container: 'ytmap-container', + zoom: 10, + center: [104.065735, 30.659462], // 成都中心 + onLoad: (m) => { + console.log('[MapPage] 地图已加载', m.getCenter()) + }, + }) + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : '地图初始化失败' + initError.value = msg + console.error('[MapPage] 地图创建失败:', err) + } +}) + +// ── 组件卸载 ───────────────────────────────────────────── +onUnmounted(() => { + MapManager.getInstance().destroyMap() +}) + +// ── 工具栏操作 ─────────────────────────────────────────── function handleToolClick(label: string) { - if (label === '台账') { + if (label === '定位') { + const m = map.value + if (m) { + // 尝试使用浏览器定位 + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (pos) => { + m.flyTo({ center: [pos.coords.longitude, pos.coords.latitude], zoom: 14 }) + }, + () => { + // 定位失败,回到默认中心 + m.flyTo({ center: [104.065735, 30.659462], zoom: 10 }) + }, + ) + } + } + } else if (label === '台账') { showTckz.value = true } else if (label === '仪表') { showYb.value = true @@ -38,12 +106,25 @@ function handleToolClick(label: string) {
- +
-
- -

智慧水务地图

-

MapLibre 地图将在此处加载

+ +
+ +

地图加载中…

+
+ + +
+ +

{{ initError }}

+ 返回 +
+ + +
+ + {{ error }}
@@ -85,9 +166,15 @@ function handleToolClick(label: string) { flex: 1; position: relative; margin: 0; + + :deep(.maplibregl-map) { + width: 100%; + height: 100%; + } } -.map-placeholder { +/* ── 加载 & 错误覆盖层 ── */ +.map-overlay { position: absolute; inset: 0; display: flex; @@ -95,20 +182,39 @@ function handleToolClick(label: string) { align-items: center; justify-content: center; background: #f0f3f7; + z-index: 5; + gap: 12px; +} - .placeholder-title { - margin: 12px 0 4px 0; - font-size: 18px; - font-weight: 600; - color: var(--color-text-regular); - } +.overlay-text { + font-size: 14px; + color: var(--color-text-placeholder); + margin: 0; +} - .placeholder-hint { - font-size: 13px; - color: var(--color-text-placeholder); +.map-error { + .overlay-text { + color: #F44336; + font-size: 14px; } } +/* ── 运行时错误横幅 ── */ +.map-error-banner { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 6; + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: #fff3f3; + color: #F44336; + font-size: 12px; +} + .bottom-tools { display: flex; justify-content: space-around; diff --git a/src/views/monitoringEquipment/mapMonitoring.vue b/src/views/monitoringEquipment/mapMonitoring.vue index 9536216..f08494a 100644 --- a/src/views/monitoringEquipment/mapMonitoring.vue +++ b/src/views/monitoringEquipment/mapMonitoring.vue @@ -5,13 +5,25 @@ * 在地图上展示监测设备位置和状态, * 可点击设备查看实时数据。 */ -import { ref } from 'vue' +import { ref, watch, onMounted, onUnmounted } from 'vue' import { useRouter } from 'vue-router' +import maplibregl from 'maplibre-gl' +import { MapManager } from '@/map/MapManager' +import { MapFactory } from '@/map/MapFactory' +import { useMap } from '@/map/composables/useMap' +import { usePopup } from '@/map/composables/usePopup' const router = useRouter() -/** 地图加载状态 */ -const mapLoaded = ref(false) +// ── 地图状态 ────────────────────────────────────────────── +const { map, loading, error, isReady } = useMap() +const { showPopup, hidePopup } = usePopup() + +/** 初始化错误 */ +const initError = ref(null) + +/** 标记是否已添加设备标记 */ +const markersAdded = ref(false) /** 模拟设备点位 */ const devicePoints = ref([ @@ -27,7 +39,98 @@ const statusColorMap: Record = { offline: '#999', } -/** 跳转设备详情 */ +/** 存储动态创建的 Marker,用于清理 */ +const markers: maplibregl.Marker[] = [] + +// ── 添加设备点位标记 ──────────────────────────────────── +function addDeviceMarkers(m: maplibregl.Map): void { + if (markersAdded.value) return + markersAdded.value = true + + devicePoints.value.forEach((point) => { + const color = statusColorMap[point.status] || '#999' + const el = document.createElement('div') + el.style.cssText = ` + width: 14px; height: 14px; border-radius: 50%; + background: ${color}; border: 2px solid #fff; + box-shadow: 0 1px 4px rgba(0,0,0,0.3); cursor: pointer; + ` + const marker = new maplibregl.Marker({ element: el }) + .setLngLat([point.lng, point.lat]) + .addTo(m) + + // 点击弹出设备信息 + el.addEventListener('click', () => { + showPopup( + [point.lng, point.lat], + `
+ ${point.name}
+ 状态: ${point.status}
+ ${point.lat}, ${point.lng} +
`, + { maxWidth: '220px', closeOnClick: true }, + ) + }) + + markers.push(marker) + }) +} + +function clearMarkers(): void { + markers.forEach((m) => m.remove()) + markers.length = 0 + markersAdded.value = false +} + +// ── 初始化地图 ─────────────────────────────────────────── +onMounted(() => { + const manager = MapManager.getInstance() + + // 如果地图已存在,复用并添加标记 + if (manager.hasMap()) { + const m = manager.getMap() + if (m) addDeviceMarkers(m) + return + } + + const container = document.getElementById('mapmonitor-container') + if (!container) { + initError.value = '地图容器 #mapmonitor-container 未找到' + return + } + + try { + MapFactory.createWithBasemap('osm', { + container: 'mapmonitor-container', + zoom: 10, + center: [104.0, 30.5], + onLoad: (m) => { + console.log('[MapMonitoring] 地图已加载') + addDeviceMarkers(m) + }, + }) + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : '地图初始化失败' + initError.value = msg + console.error('[MapMonitoring] 地图创建失败:', err) + } +}) + +// ── 监听 isReady,加载标记 ────────────────────────────── +watch(isReady, (ready) => { + if (ready && map.value && !markersAdded.value) { + addDeviceMarkers(map.value) + } +}) + +// ── 组件卸载 ───────────────────────────────────────────── +onUnmounted(() => { + clearMarkers() + hidePopup() + MapManager.getInstance().destroyMap() +}) + +// ── 跳转设备详情 ───────────────────────────────────────── function goEquipmentInfo(id: number) { router.push(`/equipmentInfo?id=${id}`) } @@ -37,11 +140,25 @@ function goEquipmentInfo(id: number) {
- -
- -

地图监控

-

设备点位将在此地图上展示

+ +
+ +
+ +

地图加载中…

+
+ + +
+ +

{{ initError }}

+
+ + +
+ + {{ error }} +
@@ -82,31 +199,60 @@ function goEquipmentInfo(id: number) { } } -.map-placeholder { +.map-area { flex: 1; + position: relative; + margin: 8px; + border-radius: 10px; + overflow: hidden; + min-height: 300px; + + :deep(.maplibregl-map) { + width: 100%; + height: 100%; + } +} + +.map-status { + position: absolute; + inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #f5f7fa; - margin: 8px; border-radius: 10px; - border: 1px dashed #ddd; - min-height: 300px; + z-index: 5; + gap: 8px; - .map-title { - margin: 12px 0 4px 0; - font-size: 18px; - font-weight: 600; - color: var(--color-text-regular); - } - - .map-hint { - font-size: 13px; + p { + margin: 0; + font-size: 14px; color: var(--color-text-placeholder); } } +.map-error-state { + p { + color: #F44336; + } +} + +.map-error-banner { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 6; + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background: #fff3f3; + color: #F44336; + font-size: 11px; +} + .device-panel { background: var(--color-bg-card); margin: 0 8px 8px;