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:
Ubuntu
2026-06-15 21:39:37 +08:00
parent fc1211faf9
commit 88d1c41cb9
5 changed files with 394 additions and 41 deletions

View File

@@ -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;

View File

@@ -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;