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)
This commit is contained in:
@@ -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<HTMLElement | null>(null)
|
||||
|
||||
/** 地图初始化错误(map 本身加载失败,区别于运行错误) */
|
||||
const initError = ref<string | null>(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) {
|
||||
<div class="map-page">
|
||||
<van-nav-bar title="地图" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
|
||||
|
||||
<!-- MapLibre 地图容器占位 -->
|
||||
<!-- MapLibre 地图容器 -->
|
||||
<div id="ytmap-container" class="map-container">
|
||||
<div class="map-placeholder">
|
||||
<van-icon name="map-marked" size="64" color="#ccc" />
|
||||
<p class="placeholder-title">智慧水务地图</p>
|
||||
<p class="placeholder-hint">MapLibre 地图将在此处加载</p>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading && !error && !initError" class="map-overlay map-loading">
|
||||
<van-loading type="spinner" size="32" color="var(--color-primary)" />
|
||||
<p class="overlay-text">地图加载中…</p>
|
||||
</div>
|
||||
|
||||
<!-- 初始化错误 -->
|
||||
<div v-if="initError" class="map-overlay map-error">
|
||||
<van-icon name="warning-o" size="48" color="#F44336" />
|
||||
<p class="overlay-text">{{ initError }}</p>
|
||||
<van-button size="small" type="primary" @click="router.back()">返回</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 运行时错误 -->
|
||||
<div v-if="error" class="map-error-banner">
|
||||
<van-icon name="warning-o" size="16" />
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
|
||||
/** 标记是否已添加设备标记 */
|
||||
const markersAdded = ref(false)
|
||||
|
||||
/** 模拟设备点位 */
|
||||
const devicePoints = ref([
|
||||
@@ -27,7 +39,98 @@ const statusColorMap: Record<string, string> = {
|
||||
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],
|
||||
`<div class="device-popup">
|
||||
<strong>${point.name}</strong><br/>
|
||||
<span style="font-size:12px;color:#666">状态: ${point.status}</span><br/>
|
||||
<span style="font-size:11px;color:#999">${point.lat}, ${point.lng}</span>
|
||||
</div>`,
|
||||
{ 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) {
|
||||
<div class="map-monitoring-page">
|
||||
<van-nav-bar title="地图监控" left-text="返回" left-arrow fixed placeholder @click-left="router.back()" />
|
||||
|
||||
<!-- 地图占位区 -->
|
||||
<div class="map-placeholder">
|
||||
<van-icon name="location-o" size="48" color="#ccc" />
|
||||
<p class="map-title">地图监控</p>
|
||||
<p class="map-hint">设备点位将在此地图上展示</p>
|
||||
<!-- MapLibre 地图容器 -->
|
||||
<div id="mapmonitor-container" class="map-area">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="!map && loading && !initError" class="map-status map-loading">
|
||||
<van-loading type="spinner" size="28" color="var(--color-primary)" />
|
||||
<p>地图加载中…</p>
|
||||
</div>
|
||||
|
||||
<!-- 初始化错误 -->
|
||||
<div v-if="initError" class="map-status map-error-state">
|
||||
<van-icon name="warning-o" size="40" color="#F44336" />
|
||||
<p>{{ initError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 运行时错误 -->
|
||||
<div v-if="error" class="map-error-banner">
|
||||
<van-icon name="warning-o" size="14" />
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备点位列表面板(底部浮动) -->
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user