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:
15
index.html
15
index.html
@@ -8,6 +8,21 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- WeixinJSBridge 检测 — 判断是否在微信环境中运行 -->
|
||||
<script>
|
||||
(function () {
|
||||
var ua = navigator.userAgent.toLowerCase()
|
||||
var isWechat = ua.indexOf('micromessenger') !== -1
|
||||
// 标记微信环境,供应用内业务逻辑判断(如微信 JSSDK 授权)
|
||||
window.__IS_WEIXIN__ = isWechat
|
||||
// 如果是微信环境,监听 WeixinJSBridge 就绪事件
|
||||
if (isWechat) {
|
||||
document.addEventListener('WeixinJSBridgeReady', function () {
|
||||
window.__WEIXIN_BRIDGE_READY__ = true
|
||||
}, false)
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<!-- 运行时全局配置 — 开发环境 -->
|
||||
<script src="/config/globalConfig.dev.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
62
src/App.vue
62
src/App.vue
@@ -3,13 +3,26 @@
|
||||
* 应用根组件
|
||||
*
|
||||
* 使用 Vant ConfigProvider 包裹路由视图,
|
||||
* 提供全局主题配置和路由出口。
|
||||
* 提供全局主题配置、路由缓存 (keep-alive) 和过渡动画。
|
||||
*/
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-config-provider>
|
||||
<router-view />
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition
|
||||
:name="route.meta.transition as string || 'fade-slide'"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<keep-alive :include="appStore.cachedViews">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</van-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
</style>
|
||||
|
||||
28
src/main.ts
28
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 类名到 <html> 元素
|
||||
useAppStore()
|
||||
|
||||
// ── 注册 Vant 组件(组件类) ──
|
||||
// Toast / Dialog / ImagePreview 为函数式 API,
|
||||
// 直接从 vant 导入即可使用,无需 app.use 注册。
|
||||
|
||||
@@ -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