diff --git a/src/components/navigation/ArrivalPanel.vue b/src/components/navigation/ArrivalPanel.vue
new file mode 100644
index 0000000..607c678
--- /dev/null
+++ b/src/components/navigation/ArrivalPanel.vue
@@ -0,0 +1,439 @@
+
+
+
+
+
+
+
+
+ 公交站、停车场信息以第三方地图实时结果为准
+ 暂未接入停车余位
+
+
+
+ 导航到场馆
+
+
+
+ {{ target.title }}
+ {{ target.keyword }}
+
+ 选择地图
+
+
+
+
+ 推荐公共交通
+
+
+
+ {{ target.title }}
+ {{ target.keyword }}
+
+ 选择地图
+
+
+
+
+ 实时查询
+
+
+
+ {{ target.title }}
+ {{ target.keyword }}
+
+ 选择地图
+
+
+
+
+
+
+
+
+
+ {{ provider.label.slice(0, 1) }}
+
+ {{ provider.label }}
+ 打开
+
+
+
+
+
+
+
diff --git a/src/data/arrivalSearchTargets.ts b/src/data/arrivalSearchTargets.ts
new file mode 100644
index 0000000..8afc9f0
--- /dev/null
+++ b/src/data/arrivalSearchTargets.ts
@@ -0,0 +1,89 @@
+export type ArrivalTargetGroup = 'venue' | 'transit' | 'realtime'
+export type ArrivalRealtimeKind = 'bus' | 'parking'
+
+export interface ArrivalSearchTarget {
+ id: string
+ title: string
+ keyword: string
+ region: string
+ group: ArrivalTargetGroup
+ realtimeKind?: ArrivalRealtimeKind
+}
+
+export const ARRIVAL_SEARCH_REGION = '深圳市'
+
+export const ARRIVAL_SEARCH_TARGETS: ArrivalSearchTarget[] = [
+ {
+ id: 'shenzhen-natural-history-museum',
+ title: '深圳自然博物馆',
+ keyword: '深圳自然博物馆',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'venue'
+ },
+ {
+ id: 'shabo-station-exit-d',
+ title: '沙壆站 D口',
+ keyword: '沙壆站D口',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'transit'
+ },
+ {
+ id: 'natural-museum-west-station',
+ title: '自然博物馆西站',
+ keyword: '自然博物馆西站',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'transit'
+ },
+ {
+ id: 'museum-bus-stop',
+ title: '深圳自然博物馆 公交站',
+ keyword: '深圳自然博物馆 公交站',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'realtime',
+ realtimeKind: 'bus'
+ },
+ {
+ id: 'shabo-metro-bus-stop',
+ title: '沙壆地铁站 公交站',
+ keyword: '沙壆地铁站 公交站',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'realtime',
+ realtimeKind: 'bus'
+ },
+ {
+ id: 'natural-museum-west-bus-stop',
+ title: '自然博物馆西 公交站',
+ keyword: '自然博物馆西 公交站',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'realtime',
+ realtimeKind: 'bus'
+ },
+ {
+ id: 'museum-parking',
+ title: '深圳自然博物馆 停车场',
+ keyword: '深圳自然博物馆 停车场',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'realtime',
+ realtimeKind: 'parking'
+ },
+ {
+ id: 'pingshan-cultural-cluster-parking',
+ title: '坪山文化聚落 停车场',
+ keyword: '坪山文化聚落 停车场',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'realtime',
+ realtimeKind: 'parking'
+ },
+ {
+ id: 'yanzi-lake-area-parking',
+ title: '燕子湖片区 停车场',
+ keyword: '燕子湖片区 停车场',
+ region: ARRIVAL_SEARCH_REGION,
+ group: 'realtime',
+ realtimeKind: 'parking'
+ }
+]
+
+export const ARRIVAL_VENUE_TARGETS = ARRIVAL_SEARCH_TARGETS.filter((target) => target.group === 'venue')
+export const ARRIVAL_TRANSIT_TARGETS = ARRIVAL_SEARCH_TARGETS.filter((target) => target.group === 'transit')
+export const ARRIVAL_REALTIME_TARGETS = ARRIVAL_SEARCH_TARGETS.filter((target) => target.group === 'realtime')
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index b164ad8..1d18a2f 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -36,14 +36,13 @@
show-floor-header
:show-layer-mode-toggle="false"
:show-floor="is3DMode && !showRoutePlanner"
- show-zoom-controls
+ :show-zoom-controls="is3DMode"
zoom-controls-top="calc(100vh - 292px)"
:route-preview="activeRoutePreview"
:show-route="Boolean(activeRoutePreview)"
:disable-auto-exit="disableIndoorAutoExit"
:auto-switch-threshold-low="0.58"
:auto-switch-threshold-high="1.18"
- :outdoor-nav-polylines="outdoorNavPolylines"
@mode-change="handleModeChange"
@floor-request="handleFloorRequest"
@floor-change="handleFloorChange"
@@ -56,7 +55,6 @@
@initial-model-ready="handleInitialModelReady"
@initial-model-failed="handleInitialModelFailed"
@tool-click="handleIndoorToolClick"
- @map-tap="handleMapTapForOutdoorNav"
>
@@ -228,25 +226,10 @@
-
@@ -308,7 +291,7 @@ import { onLoad } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
-import OutdoorNavigationPanel from '@/components/navigation/OutdoorNavigationPanel.vue'
+import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue'
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
import type {
RoutePointOption
@@ -346,17 +329,6 @@ import type {
MuseumFloor,
MuseumHall
} from '@/domain/museum'
-import { MUSEUM_LOCATION, type TravelMode } from '@/config/museum'
-import {
- planWalkingRoute,
- planDrivingRoute,
- planTransitRoute,
- type Route,
- type RoutePoint
-} from '@/services/tencent/TencentMapService'
-import {
- navigateToMuseum
-} from '@/services/MapNavigationService'
type GuideIndoorView = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
@@ -566,17 +538,7 @@ const handleLaunchContinue = () => {
hideLaunchOverlay()
}
-// 馆外导航相关状态
-const isOutdoorGuidePanelEnabled = false
-const showOutdoorNavPanel = ref(false)
-const outdoorNavLoading = ref(false)
-const outdoorNavError = ref('')
-const outdoorNavRoute = ref(null)
-const outdoorNavPolylines = ref; color: string; width: number }>>([])
-const outdoorNavStartPoint = ref<{ latitude: number; longitude: number } | null>(null)
-const outdoorNavTravelMode = ref('walking')
-const outdoorNavSelectingManualStart = ref(false)
-const outdoorNavManualMode = ref(false)
+const showArrivalPanel = ref(false)
const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
const indoorModelSource = guideUseCase.getModelSource()
@@ -634,8 +596,9 @@ const disableIndoorAutoExit = computed(() => (
const showGuideHomeDock = computed(() => (
currentTab.value === 'guide'
+ && is3DMode.value
&& !showRoutePlanner.value
- && !showOutdoorNavPanel.value
+ && !showArrivalPanel.value
&& !selectedGuidePoi.value
&& !isSimulatingRoute.value
&& !(guideOutdoorState.value === 'entrance' && !is3DMode.value)
@@ -644,7 +607,7 @@ const showGuideHomeDock = computed(() => (
const showGuideFloatingActions = computed(() => (
currentTab.value === 'guide'
&& !showRoutePlanner.value
- && !showOutdoorNavPanel.value
+ && !showArrivalPanel.value
&& !selectedGuidePoi.value
&& !isSimulatingRoute.value
&& (!showGuideHomeDock.value || !homeSearchExpanded.value)
@@ -652,6 +615,7 @@ const showGuideFloatingActions = computed(() => (
const guideQuickActiveAction = computed<'indoor' | 'arrival' | 'explain'>(() => {
if (currentTab.value === 'explain') return 'explain'
+ if (showArrivalPanel.value) return 'arrival'
return is3DMode.value ? 'indoor' : 'arrival'
})
@@ -777,8 +741,7 @@ const handleTabChange = (tabId: GuideTopTab) => {
showRoutePlanner.value = false
isSimulatingRoute.value = false
isPoiCardCollapsed.value = false
- showOutdoorNavPanel.value = false
- clearOutdoorNavState()
+ closeArrivalPanel()
}
const syncTopTabFromHash = () => {
@@ -793,6 +756,7 @@ const syncTopTabFromHash = () => {
showRoutePlanner.value = false
isSimulatingRoute.value = false
isPoiCardCollapsed.value = false
+ closeArrivalPanel()
}
syncTopTabToUrl(tab)
return
@@ -1239,7 +1203,7 @@ const handleRouteSimulate = () => {
// 进入 3D 馆内模式
const handleEnter3DMode = () => {
console.log('进入 3D 馆内模式')
- closeOutdoorNavPanel()
+ closeArrivalPanel()
indoorView.value = 'overview'
is3DMode.value = true
guideOutdoorState.value = 'home'
@@ -1253,6 +1217,7 @@ const handleModeChange = (mode: '2d' | '3d') => {
is3DMode.value = mode === '3d'
if (mode === '2d') {
+ closeHomeSearchDock()
indoorView.value = 'overview'
guideOutdoorState.value = 'home'
selectedGuidePoi.value = null
@@ -1261,7 +1226,7 @@ const handleModeChange = (mode: '2d' | '3d') => {
isSimulatingRoute.value = false
clearRoutePreviewState()
} else {
- closeOutdoorNavPanel()
+ closeArrivalPanel()
indoorView.value = 'overview'
showIndoorHint()
if (!routePointOptions.value.length) {
@@ -1295,7 +1260,7 @@ const handleSwitchEntrance = () => {
}
const handleMoreRouteGuide = () => {
- closeOutdoorNavPanel()
+ closeArrivalPanel()
showRoutePlanner.value = false
is3DMode.value = true
indoorView.value = 'floor'
@@ -1306,44 +1271,24 @@ const handleMoreRouteGuide = () => {
showIndoorHint(`当前楼层:${getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'}`, 3200)
}
-// 馆外导航处理函数
-const openOutdoorNavPanel = () => {
- if (!isOutdoorGuidePanelEnabled) {
- showOutdoorNavPanel.value = false
- showRoutePlanner.value = false
- selectedGuidePoi.value = null
- isPoiCardCollapsed.value = false
- is3DMode.value = false
- clearOutdoorNavState()
- return
- }
-
- showOutdoorNavPanel.value = true
- showRoutePlanner.value = false
- selectedGuidePoi.value = null
- isPoiCardCollapsed.value = false
- // 切换到馆外 2D 地图
- is3DMode.value = false
- clearOutdoorNavState()
+const closeArrivalPanel = () => {
+ showArrivalPanel.value = false
}
-const clearOutdoorNavState = () => {
- outdoorNavRoute.value = null
- outdoorNavPolylines.value = []
- outdoorNavStartPoint.value = null
- outdoorNavError.value = ''
- outdoorNavLoading.value = false
- outdoorNavSelectingManualStart.value = false
- outdoorNavManualMode.value = false
-}
-
-const closeOutdoorNavPanel = () => {
- showOutdoorNavPanel.value = false
- clearOutdoorNavState()
+const closeHomeSearchDock = () => {
+ homeSearchPanelRef.value?.collapseHomePanel?.()
+ homeSearchExpanded.value = false
}
const handleMoreOutdoorNav = () => {
- openOutdoorNavPanel()
+ showArrivalPanel.value = true
+ showRoutePlanner.value = false
+ is3DMode.value = false
+ guideOutdoorState.value = 'home'
+ selectedGuidePoi.value = null
+ isPoiCardCollapsed.value = false
+ isSimulatingRoute.value = false
+ closeHomeSearchDock()
}
const handleExplainQuickTap = () => {
@@ -1355,8 +1300,7 @@ const handleHomeSearchExpandedChange = (expanded: boolean) => {
}
const handleHomeSearchOutsideTap = () => {
- homeSearchPanelRef.value?.collapseHomePanel?.()
- homeSearchExpanded.value = false
+ closeHomeSearchDock()
}
const handleHomeDockActivate = () => {
@@ -1366,155 +1310,6 @@ const handleHomeDockActivate = () => {
homeSearchExpanded.value = true
}
-const handleOutdoorNavClose = () => {
- closeOutdoorNavPanel()
-}
-
-const handleOutdoorNavBack = () => {
- closeOutdoorNavPanel()
-}
-
-const handleOutdoorNavGpsStart = () => {
- outdoorNavLoading.value = true
- outdoorNavError.value = ''
-
- uni.getLocation({
- type: 'gcj02',
- success: (res) => {
- console.log('GPS 定位成功:', res)
- outdoorNavStartPoint.value = {
- latitude: res.latitude,
- longitude: res.longitude
- }
- void planOutdoorRoute({
- latitude: res.latitude,
- longitude: res.longitude
- })
- },
- fail: (err) => {
- console.error('GPS 定位失败:', err)
- const errMsg = err.errMsg || ''
- if (errMsg.includes('auth deny') || errMsg.includes('auth reject')) {
- outdoorNavError.value = '定位权限被拒绝,请在设置中开启'
- } else if (errMsg.includes('timeout')) {
- outdoorNavError.value = '定位超时,请重试'
- } else if (errMsg.includes('unavailable')) {
- outdoorNavError.value = '位置服务不可用,请检查设备设置'
- } else {
- outdoorNavError.value = '定位失败,请检查位置权限'
- }
- outdoorNavLoading.value = false
- }
- })
-}
-
-const handleOutdoorNavManualStartRequest = () => {
- outdoorNavManualMode.value = true
- outdoorNavSelectingManualStart.value = true
- uni.showToast({
- title: '输入地址搜索,或点击地图选择起点',
- icon: 'none'
- })
-}
-
-const handleOutdoorNavManualStartCancel = () => {
- outdoorNavManualMode.value = false
- outdoorNavSelectingManualStart.value = false
-}
-
-const handleOutdoorNavManualStartConfirm = (location: { latitude: number; longitude: number }) => {
- outdoorNavStartPoint.value = location
- outdoorNavManualMode.value = false
- outdoorNavSelectingManualStart.value = false
- void planOutdoorRoute(location)
-}
-
-const handleOutdoorNavTravelModeChange = (mode: TravelMode) => {
- outdoorNavTravelMode.value = mode
- if (outdoorNavStartPoint.value) {
- outdoorNavLoading.value = true
- void planOutdoorRoute(outdoorNavStartPoint.value)
- }
-}
-
-const planOutdoorRoute = async (from: { latitude: number; longitude: number }) => {
- outdoorNavLoading.value = true
- outdoorNavError.value = ''
-
- const to: RoutePoint = {
- latitude: MUSEUM_LOCATION.latitude,
- longitude: MUSEUM_LOCATION.longitude
- }
-
- try {
- let result
- switch (outdoorNavTravelMode.value) {
- case 'walking':
- result = await planWalkingRoute(from, to)
- break
- case 'driving':
- result = await planDrivingRoute(from, to)
- break
- case 'transit':
- result = await planTransitRoute(from, to)
- break
- default:
- result = await planWalkingRoute(from, to)
- }
-
- if (result.status === 0 && result.routes && result.routes.length > 0) {
- const route = result.routes[0]
- outdoorNavRoute.value = route
- outdoorNavPolylines.value = [{
- points: route.polyline,
- color: '#E0E100',
- width: 6
- }]
- console.log('路线规划成功:', {
- distance: route.distance,
- duration: route.duration,
- steps: route.steps.length
- })
- } else {
- outdoorNavError.value = result.message || '路线规划失败'
- outdoorNavRoute.value = null
- outdoorNavPolylines.value = []
- }
- } catch (error) {
- console.error('路线规划异常:', error)
- outdoorNavError.value = '路线规划失败,请重试'
- outdoorNavRoute.value = null
- outdoorNavPolylines.value = []
- } finally {
- outdoorNavLoading.value = false
- }
-}
-
-const handleOutdoorNavStartNavigation = async () => {
- // 直接调起导航
- await navigateToMuseum()
-}
-
-const handleOutdoorNavClearRoute = () => {
- outdoorNavRoute.value = null
- outdoorNavPolylines.value = []
- outdoorNavStartPoint.value = null
- outdoorNavError.value = ''
-}
-
-// 处理地图点击(用于手动选择起点)
-const handleMapTapForOutdoorNav = (location: { latitude?: number; longitude?: number }) => {
- if (!outdoorNavSelectingManualStart.value) return
-
- const latitude = location.latitude
- const longitude = location.longitude
- if (latitude !== undefined && longitude !== undefined) {
- outdoorNavStartPoint.value = { latitude, longitude }
- outdoorNavSelectingManualStart.value = false
- void planOutdoorRoute({ latitude, longitude })
- }
-}
-
const guideShellMode = computed(() => (is3DMode.value ? '3d' : '2d'))
const guideMapType = computed(() => (is3DMode.value ? 'indoor' : 'outdoor'))
const guideOutdoorVariant = computed(() => (
diff --git a/src/services/ThirdPartyMapSearchService.ts b/src/services/ThirdPartyMapSearchService.ts
new file mode 100644
index 0000000..8328b19
--- /dev/null
+++ b/src/services/ThirdPartyMapSearchService.ts
@@ -0,0 +1,57 @@
+export type ThirdPartyMapProvider = 'amap' | 'baidu' | 'tencent'
+
+export interface ThirdPartyMapSearchTarget {
+ keyword: string
+ region: string
+}
+
+export interface ThirdPartyMapProviderOption {
+ provider: ThirdPartyMapProvider
+ label: string
+}
+
+export const THIRD_PARTY_MAP_PROVIDERS: ThirdPartyMapProviderOption[] = [
+ { provider: 'amap', label: '高德地图' },
+ { provider: 'baidu', label: '百度地图' },
+ { provider: 'tencent', label: '腾讯地图' }
+]
+
+const encode = (value: string) => encodeURIComponent(value)
+
+export const buildThirdPartyMapSearchUri = (
+ provider: ThirdPartyMapProvider,
+ target: ThirdPartyMapSearchTarget
+) => {
+ const keyword = target.keyword.trim()
+ const region = target.region.trim()
+
+ switch (provider) {
+ case 'amap':
+ return `https://uri.amap.com/search?keyword=${encode(keyword)}&city=${encode(region)}&src=museum-guide&callnative=1`
+ case 'baidu':
+ return `https://api.map.baidu.com/place/search?query=${encode(keyword)}®ion=${encode(region)}&output=html&src=museum-guide`
+ case 'tencent':
+ return `https://apis.map.qq.com/uri/v1/search?keyword=${encode(keyword)}®ion=${encode(region)}&referer=museum-guide`
+ default: {
+ const exhaustive: never = provider
+ return exhaustive
+ }
+ }
+}
+
+export const openThirdPartyMapSearch = (
+ provider: ThirdPartyMapProvider,
+ target: ThirdPartyMapSearchTarget
+) => {
+ const uri = buildThirdPartyMapSearchUri(provider, target)
+
+ if (typeof window !== 'undefined' && window.location) {
+ window.location.href = uri
+ return
+ }
+
+ uni.showToast({
+ title: '请在 H5 浏览器中打开地图',
+ icon: 'none'
+ })
+}