简化来馆面板并优化地图点位展示

This commit is contained in:
lyf
2026-07-09 18:46:56 +08:00
parent 5b4fe19ce3
commit 3f9c64a36e
5 changed files with 587 additions and 221 deletions

View File

@@ -59,7 +59,7 @@
</template>
<script setup lang="ts">
import { computed, ref, onBeforeUnmount, onMounted, nextTick } from 'vue'
import { computed, ref, onBeforeUnmount, onMounted, nextTick, watch } from 'vue'
import { getMuseumEntranceLocation } from '@/services/tencent/TencentMapService'
const isH5 = typeof window !== 'undefined'
@@ -103,6 +103,14 @@ interface Polyline {
dottedLine?: boolean
}
export interface OutdoorMapMarker {
id: string
title: string
subtitle?: string
latitude: number
longitude: number
}
interface MapFloor {
id: string
label: string
@@ -112,14 +120,21 @@ const props = withDefaults(defineProps<{
activeFloor?: string
floors?: MapFloor[]
polylines?: Polyline[]
outdoorMarkers?: OutdoorMapMarker[]
activeOutdoorMarkerId?: string
outdoorFocusOffsetY?: number
}>(), {
activeFloor: '1F',
floors: () => [] as MapFloor[],
polylines: () => [] as Polyline[]
polylines: () => [] as Polyline[],
outdoorMarkers: () => [] as OutdoorMapMarker[],
activeOutdoorMarkerId: '',
outdoorFocusOffsetY: 0
})
const emit = defineEmits<{
markerClick: [markerId: number]
outdoorMarkerClick: [markerId: string]
floorChange: [floor: string]
enter3DMode: []
mapTap: [location: { latitude: number; longitude: number }]
@@ -138,9 +153,11 @@ const mapScale = ref(17)
// 当前楼层
const currentFloor = ref(props.activeFloor)
const mapComponentRef = ref<unknown>(null)
const userLocationMarker = ref<Marker | null>(null)
let mapAuthErrorObserver: MutationObserver | null = null
let mapAuthErrorLogged = false
const mapAuthErrorText = '鉴权失败请传入正确的key'
const OUTDOOR_MARKER_ID_BASE = 20000
// 楼层列表
const floors = computed(() => props.floors)
@@ -213,8 +230,46 @@ const entranceMarker = ref<Marker>({
}
})
const outdoorMarkerIdMap = computed(() => {
const markerMap = new Map<number, string>()
props.outdoorMarkers.forEach((marker, index) => {
markerMap.set(OUTDOOR_MARKER_ID_BASE + index, marker.id)
})
return markerMap
})
const outdoorDisplayMarkers = computed<Marker[]>(() => (
props.outdoorMarkers.map((marker, index) => {
const isActive = marker.id === props.activeOutdoorMarkerId
return {
id: OUTDOOR_MARKER_ID_BASE + index,
latitude: marker.latitude,
longitude: marker.longitude,
iconPath: '/static/icons/marker-location.svg',
width: isActive ? 42 : 32,
height: isActive ? 42 : 32,
title: marker.title,
callout: {
content: marker.subtitle ? `${marker.title}\n${marker.subtitle}` : marker.title,
display: isActive ? 'ALWAYS' : 'BYCLICK',
padding: 10,
borderRadius: 6,
bgColor: isActive ? '#262421' : '#FFFFFF',
color: isActive ? '#E0E100' : '#262421',
fontSize: 12
}
}
})
))
// 地图标记点列表
const markers = computed(() => [museumMarker.value, entranceMarker.value])
const markers = computed(() => [
museumMarker.value,
entranceMarker.value,
...outdoorDisplayMarkers.value,
...(userLocationMarker.value ? [userLocationMarker.value] : [])
])
// 显示的路线列表(转换格式以适配 uni-app map 组件)
const displayPolylines = computed(() => {
@@ -233,8 +288,7 @@ const displayPolylines = computed(() => {
* 更新用户位置标记
*/
const updateUserLocation = (location: { latitude: number; longitude: number }) => {
const userMarkerIndex = markers.value.findIndex((m) => m.id === 999)
const userMarker: Marker = {
userLocationMarker.value = {
id: 999,
latitude: location.latitude,
longitude: location.longitude,
@@ -252,22 +306,13 @@ const updateUserLocation = (location: { latitude: number; longitude: number }) =
fontSize: 12
}
}
if (userMarkerIndex >= 0) {
markers.value[userMarkerIndex] = userMarker
} else {
markers.value.push(userMarker)
}
}
/**
* 清除用户位置标记
*/
const clearUserLocation = () => {
const userMarkerIndex = markers.value.findIndex((m) => m.id === 999)
if (userMarkerIndex >= 0) {
markers.value.splice(userMarkerIndex, 1)
}
userLocationMarker.value = null
}
/**
@@ -280,6 +325,31 @@ const moveTo = (location: { latitude: number; longitude: number }) => {
}
}
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
const getLatitudeDegreesPerPixel = (latitude: number, scale: number) => {
const latitudeRadians = latitude * Math.PI / 180
const worldPixelSize = 256 * (2 ** scale)
return 360 * Math.cos(latitudeRadians) / worldPixelSize
}
const moveToWithVerticalOffset = (
location: { latitude: number; longitude: number },
offsetY: number
) => {
const effectiveOffsetY = clamp(offsetY, 0, 360)
if (effectiveOffsetY <= 0) {
moveTo(location)
return
}
const latitudeOffset = getLatitudeDegreesPerPixel(location.latitude, mapScale.value) * effectiveOffsetY
mapCenter.value = {
latitude: location.latitude - latitudeOffset,
longitude: location.longitude
}
}
// 暴露方法给父组件
defineExpose({
updateUserLocation,
@@ -289,8 +359,12 @@ defineExpose({
// 处理标记点点击
const handleMarkerTap = (e: any) => {
const markerId = e.detail.markerId || e.markerId
const markerId = Number(e.detail?.markerId ?? e.markerId)
console.log('点击标记点:', markerId)
const outdoorMarkerId = outdoorMarkerIdMap.value.get(markerId)
if (outdoorMarkerId) {
emit('outdoorMarkerClick', outdoorMarkerId)
}
emit('markerClick', markerId)
}
@@ -328,8 +402,7 @@ const handleLocation = () => {
}
// 添加或更新用户位置标记
const userMarkerIndex = markers.value.findIndex(m => m.id === 999)
const userMarker: Marker = {
userLocationMarker.value = {
id: 999,
latitude: res.latitude,
longitude: res.longitude,
@@ -348,12 +421,6 @@ const handleLocation = () => {
}
}
if (userMarkerIndex >= 0) {
markers.value[userMarkerIndex] = userMarker
} else {
markers.value.push(userMarker)
}
console.log('定位成功:', res)
},
fail: (err) => {
@@ -388,6 +455,23 @@ const handleEnter3D = () => {
emit('enter3DMode')
}
watch(
() => [props.activeOutdoorMarkerId, props.outdoorMarkers, props.outdoorFocusOffsetY] as const,
([activeOutdoorMarkerId, outdoorMarkers, outdoorFocusOffsetY]) => {
if (!activeOutdoorMarkerId) return
const activeMarker = outdoorMarkers.find((marker) => marker.id === activeOutdoorMarkerId)
if (!activeMarker) return
moveToWithVerticalOffset(
{
latitude: activeMarker.latitude,
longitude: activeMarker.longitude
},
outdoorFocusOffsetY
)
},
{ immediate: true }
)
const getMapComponentElement = () => {
const rawRef = mapComponentRef.value
if (rawRef instanceof HTMLElement) return rawRef

View File

@@ -5,6 +5,7 @@
@tap="collapsePanel"
></view>
<view
ref="arrivalPanelRef"
v-if="visible"
class="arrival-panel"
:class="{ collapsed: isCollapsed }"
@@ -13,7 +14,7 @@
<view v-if="isCollapsed" class="arrival-collapsed" @tap="expandPanel">
<view class="arrival-collapsed-copy">
<text class="arrival-collapsed-title">来馆</text>
<text class="arrival-collapsed-summary">公交 / 地铁 / 停车</text>
<text class="arrival-collapsed-summary">{{ selectedTarget?.title || activeTypeLabel }}</text>
</view>
<view class="arrival-collapsed-action">
<text class="arrival-collapsed-action-text">展开</text>
@@ -21,83 +22,64 @@
</view>
<template v-else>
<view class="arrival-handle" @tap="collapsePanel"></view>
<view class="arrival-handle" @tap="collapsePanel"></view>
<view class="arrival-header">
<view class="arrival-title-group">
<text class="arrival-kicker">来馆</text>
<text class="arrival-title">选择第三方地图检索目标</text>
</view>
<view class="arrival-collapse" @tap="collapsePanel">
<text class="arrival-collapse-text">收起</text>
</view>
</view>
<view class="arrival-notes">
<text class="arrival-note">公交站停车场信息以第三方地图实时结果为准</text>
<text class="arrival-note">暂未接入停车余位</text>
</view>
<view class="arrival-section">
<text class="arrival-section-title">导航到场馆</text>
<view
v-for="target in venueTargets"
:key="target.id"
class="arrival-target-row"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-keyword">{{ target.keyword }}</text>
<view class="arrival-header">
<view class="arrival-title-group">
<text class="arrival-kicker">来馆</text>
</view>
<text class="arrival-target-action">选择地图</text>
</view>
</view>
<view class="arrival-section">
<text class="arrival-section-title">推荐公共交通</text>
<view
v-for="target in transitTargets"
:key="target.id"
class="arrival-target-row"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-keyword">{{ target.keyword }}</text>
<view class="arrival-collapse" @tap="collapsePanel">
<text class="arrival-collapse-text">收起</text>
</view>
<text class="arrival-target-action">选择地图</text>
</view>
</view>
<view class="arrival-section">
<text class="arrival-section-title">实时查询</text>
<view
v-for="target in realtimeTargets"
:key="target.id"
class="arrival-target-row"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-keyword">{{ target.keyword }}</text>
<view class="arrival-tabs">
<view
v-for="option in typeOptions"
:key="option.type"
class="arrival-tab"
:class="{ active: option.type === activeType }"
@tap="handleTypeChange(option.type)"
>
<text class="arrival-tab-text">{{ option.label }}</text>
</view>
<text class="arrival-target-action">选择地图</text>
</view>
</view>
<view class="arrival-list">
<view
v-for="target in targets"
:key="target.id"
class="arrival-target-row"
:class="{ active: target.id === selectedTargetId }"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-subtitle">{{ target.subtitle }}</text>
<text v-if="target.description" class="arrival-target-desc">{{ target.description }}</text>
</view>
<text v-if="target.id === selectedTargetId" class="arrival-target-state">已选</text>
</view>
</view>
<view
class="arrival-primary"
:class="{ disabled: !selectedTarget }"
@tap="handleNavigateTap"
>
<text class="arrival-primary-text">第三方导航</text>
</view>
</template>
</view>
<view
v-if="selectedTarget"
v-if="providerSheetVisible && selectedTarget"
class="provider-scrim"
@tap="closeProviderSheet"
></view>
<view
v-if="selectedTarget"
v-if="providerSheetVisible && selectedTarget"
class="provider-sheet"
@tap.stop
>
@@ -126,12 +108,11 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import {
ARRIVAL_REALTIME_TARGETS,
ARRIVAL_TRANSIT_TARGETS,
ARRIVAL_VENUE_TARGETS,
type ArrivalSearchTarget
ARRIVAL_TARGET_TYPES,
type ArrivalSearchTarget,
type ArrivalTargetType
} from '@/data/arrivalSearchTargets'
import {
openThirdPartyMapSearch,
@@ -141,25 +122,44 @@ import {
const props = withDefaults(defineProps<{
visible?: boolean
defaultCollapsed?: boolean
activeType: ArrivalTargetType
targets?: ArrivalSearchTarget[]
selectedTargetId?: string
selectedTarget?: ArrivalSearchTarget | null
}>(), {
visible: false,
defaultCollapsed: false
targets: () => [] as ArrivalSearchTarget[],
selectedTargetId: '',
selectedTarget: null
})
const emit = defineEmits<{
'update:activeType': [type: ArrivalTargetType]
selectTarget: [target: ArrivalSearchTarget]
collapsedChange: [collapsed: boolean]
layoutChange: [layout: { height: number; collapsed: boolean }]
}>()
const venueTargets = computed(() => ARRIVAL_VENUE_TARGETS)
const transitTargets = computed(() => ARRIVAL_TRANSIT_TARGETS)
const realtimeTargets = computed(() => ARRIVAL_REALTIME_TARGETS)
const typeOptions = computed(() => ARRIVAL_TARGET_TYPES)
const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS)
const selectedTarget = ref<ArrivalSearchTarget | null>(null)
const arrivalPanelRef = ref<unknown>(null)
const isCollapsed = ref(false)
const providerSheetVisible = ref(false)
let resizeObserver: ResizeObserver | null = null
const activeTypeLabel = computed(() => (
typeOptions.value.find((option) => option.type === props.activeType)?.label || '来馆'
))
const handleTypeChange = (type: ArrivalTargetType) => {
if (type === props.activeType) return
closeProviderSheet()
emit('update:activeType', type)
}
const handleTargetSelect = (target: ArrivalSearchTarget) => {
selectedTarget.value = target
closeProviderSheet()
emit('selectTarget', target)
}
const collapsePanel = () => {
@@ -173,11 +173,76 @@ const expandPanel = () => {
}
const closeProviderSheet = () => {
selectedTarget.value = null
providerSheetVisible.value = false
}
const getArrivalPanelElement = () => {
const rawRef = arrivalPanelRef.value
if (rawRef instanceof HTMLElement) return rawRef
const maybeComponent = rawRef as { $el?: Element } | null
if (maybeComponent?.$el instanceof HTMLElement) return maybeComponent.$el
if (typeof document === 'undefined') return null
return document.querySelector<HTMLElement>('.arrival-panel')
}
const emitLayoutChange = () => {
if (!props.visible) {
emit('layoutChange', { height: 0, collapsed: false })
return
}
void nextTick(() => {
const panelElement = getArrivalPanelElement()
emit('layoutChange', {
height: panelElement?.getBoundingClientRect().height || 0,
collapsed: isCollapsed.value
})
})
}
const disconnectLayoutObserver = () => {
resizeObserver?.disconnect()
resizeObserver = null
}
const startLayoutObserver = () => {
if (typeof window === 'undefined' || typeof ResizeObserver === 'undefined') {
emitLayoutChange()
return
}
void nextTick(() => {
const panelElement = getArrivalPanelElement()
if (!panelElement) {
emitLayoutChange()
return
}
disconnectLayoutObserver()
resizeObserver = new ResizeObserver(() => {
emitLayoutChange()
})
resizeObserver.observe(panelElement)
emitLayoutChange()
})
}
const handleNavigateTap = () => {
if (!props.selectedTarget) {
uni.showToast({
title: '请先选择点位',
icon: 'none'
})
return
}
providerSheetVisible.value = true
}
const handleProviderSelect = (provider: ThirdPartyMapProviderOption) => {
const target = selectedTarget.value
const target = props.selectedTarget
if (!target) return
openThirdPartyMapSearch(provider.provider, {
@@ -190,16 +255,37 @@ watch(
() => props.visible,
(visible) => {
if (visible) {
isCollapsed.value = props.defaultCollapsed
emit('collapsedChange', props.defaultCollapsed)
} else {
selectedTarget.value = null
isCollapsed.value = false
emit('collapsedChange', false)
startLayoutObserver()
} else {
closeProviderSheet()
isCollapsed.value = false
disconnectLayoutObserver()
emit('collapsedChange', false)
emit('layoutChange', { height: 0, collapsed: false })
}
}
)
watch(
() => props.selectedTargetId,
() => {
closeProviderSheet()
}
)
watch(
() => [props.targets.length, isCollapsed.value] as const,
() => {
startLayoutObserver()
}
)
onBeforeUnmount(() => {
disconnectLayoutObserver()
})
defineExpose({
expandPanel
})
@@ -210,7 +296,7 @@ defineExpose({
position: fixed;
inset: 0;
z-index: 2090;
background: rgba(0, 0, 0, 0.18);
background: rgba(0, 0, 0, 0.08);
}
.arrival-panel {
@@ -219,8 +305,8 @@ defineExpose({
bottom: calc(env(safe-area-inset-bottom) + 16px);
z-index: 2100;
width: min(430px, calc(100vw - 24px));
max-height: min(72vh, 620px);
padding: 10px 14px 16px;
max-height: min(58vh, 520px);
padding: 10px 14px 14px;
overflow-y: auto;
box-sizing: border-box;
background: #ffffff;
@@ -346,12 +432,115 @@ defineExpose({
color: #424754;
}
.arrival-notes {
margin-top: 12px;
padding: 10px 12px;
.arrival-tabs {
height: 40px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
margin-top: 14px;
padding: 4px;
box-sizing: border-box;
background: #f3f3f3;
border-radius: 8px;
}
.arrival-tab {
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.arrival-tab.active {
background: #262421;
}
.arrival-tab-text {
font-size: 13px;
line-height: 18px;
font-weight: 700;
color: #424754;
}
.arrival-tab.active .arrival-tab-text {
color: #e0e100;
}
.arrival-list {
margin-top: 10px;
}
.arrival-target-row {
min-height: 68px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 8px;
border-top: 1px solid #eceee7;
border-radius: 8px;
box-sizing: border-box;
}
.arrival-target-row:first-child {
border-top: 0;
}
.arrival-target-row.active {
background: #f5f5ed;
}
.arrival-target-dot {
width: 10px;
height: 10px;
flex-shrink: 0;
background: #d7d9cf;
border: 2px solid #696962;
border-radius: 50%;
}
.arrival-target-row.active .arrival-target-dot {
background: #e0e100;
border-color: #262421;
}
.arrival-target-copy {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
}
.arrival-target-title {
font-size: 15px;
line-height: 21px;
font-weight: 700;
color: #262421;
}
.arrival-target-subtitle,
.arrival-target-desc {
font-size: 12px;
line-height: 17px;
color: #696962;
}
.arrival-target-desc {
color: #424754;
}
.arrival-target-state {
flex-shrink: 0;
font-size: 12px;
line-height: 17px;
font-weight: 700;
color: #262421;
}
.arrival-notes {
margin-top: 10px;
padding: 9px 10px;
background: #f5f5ed;
border-radius: 8px;
}
@@ -362,68 +551,25 @@ defineExpose({
color: #424754;
}
.arrival-section {
margin-top: 16px;
}
.arrival-section-title {
display: block;
margin-bottom: 8px;
font-size: 14px;
line-height: 20px;
font-weight: 700;
color: #262421;
}
.arrival-target-row {
min-height: 58px;
.arrival-primary {
height: 44px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-top: 1px solid #eceee7;
justify-content: center;
margin-top: 12px;
background: #262421;
border-radius: 8px;
}
.arrival-section-title + .arrival-target-row {
border-top: 0;
.arrival-primary.disabled {
opacity: 0.45;
}
.arrival-target-dot {
width: 9px;
height: 9px;
flex-shrink: 0;
background: #e0e100;
border: 2px solid #262421;
border-radius: 50%;
}
.arrival-target-copy {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.arrival-target-title {
.arrival-primary-text {
font-size: 15px;
line-height: 21px;
font-weight: 600;
color: #262421;
}
.arrival-target-keyword {
font-size: 12px;
line-height: 17px;
color: #696962;
}
.arrival-target-action {
flex-shrink: 0;
font-size: 12px;
line-height: 17px;
font-weight: 600;
color: #262421;
font-weight: 700;
color: #e0e100;
}
.provider-scrim {

View File

@@ -44,6 +44,10 @@
:active-floor="activeFloor"
:floors="props.floors"
:polylines="outdoorNavPolylines"
:outdoor-markers="outdoorMarkers"
:active-outdoor-marker-id="activeOutdoorMarkerId"
:outdoor-focus-offset-y="outdoorFocusOffsetY"
@outdoor-marker-click="handleOutdoorMarkerClick"
@map-tap="handleMapTap"
/>
</view>
@@ -265,6 +269,14 @@ interface OutdoorNavPolyline {
width: number
}
interface OutdoorMapMarker {
id: string
title: string
subtitle?: string
latitude: number
longitude: number
}
type CameraViewMode = 'reset' | 'top' | 'oblique'
interface TargetPoiFocusRequest {
@@ -347,6 +359,9 @@ const props = withDefaults(defineProps<{
autoSwitchThresholdLow?: number
autoSwitchThresholdHigh?: number
outdoorNavPolylines?: OutdoorNavPolyline[]
outdoorMarkers?: OutdoorMapMarker[]
activeOutdoorMarkerId?: string
outdoorFocusOffsetY?: number
}>(), {
searchText: '请输入地点进行搜索',
activeMode: '3d',
@@ -395,7 +410,10 @@ const props = withDefaults(defineProps<{
disableAutoExit: false,
autoSwitchThresholdLow: 1.0,
autoSwitchThresholdHigh: 1.3,
outdoorNavPolylines: () => [] as OutdoorNavPolyline[]
outdoorNavPolylines: () => [] as OutdoorNavPolyline[],
outdoorMarkers: () => [] as OutdoorMapMarker[],
activeOutdoorMarkerId: '',
outdoorFocusOffsetY: 0
})
const emit = defineEmits<{
@@ -416,6 +434,7 @@ const emit = defineEmits<{
initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }]
initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }]
mapTap: [location: { latitude: number; longitude: number }]
outdoorMarkerClick: [markerId: string]
}>()
// 监听视角切换循环切换reset -> top -> oblique -> reset
@@ -739,6 +758,10 @@ const handleMapTap = (location: { latitude?: number; longitude?: number }) => {
}
}
const handleOutdoorMarkerClick = (markerId: string) => {
emit('outdoorMarkerClick', markerId)
}
// 暴露 clearRoute 方法供父组件显式调用路线清除
defineExpose({
clearRoute: () => {

View File

@@ -1,89 +1,123 @@
export type ArrivalTargetGroup = 'venue' | 'transit' | 'realtime'
export type ArrivalRealtimeKind = 'bus' | 'parking'
export type ArrivalTargetType = 'bus' | 'metro' | 'parking'
export interface ArrivalTargetTypeOption {
type: ArrivalTargetType
label: string
}
export interface ArrivalSearchTarget {
id: string
type: ArrivalTargetType
title: string
subtitle: string
description?: string
keyword: string
region: string
group: ArrivalTargetGroup
realtimeKind?: ArrivalRealtimeKind
latitude: number
longitude: number
}
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'
},
export const ARRIVAL_TARGET_TYPES: ArrivalTargetTypeOption[] = [
{ type: 'bus', label: '公交' },
{ type: 'metro', label: '地铁' },
{ type: 'parking', label: '停车场' }
]
export const ARRIVAL_TARGETS: ArrivalSearchTarget[] = [
{
id: 'museum-bus-stop',
title: '深圳自然博物馆 公交站',
type: 'bus',
title: '深圳自然博物馆公交站',
subtitle: '场馆周边公交接驳点',
description: '适合到馆前在第三方地图中继续查看实时公交和步行路径。',
keyword: '深圳自然博物馆 公交站',
region: ARRIVAL_SEARCH_REGION,
group: 'realtime',
realtimeKind: 'bus'
},
{
id: 'shabo-metro-bus-stop',
title: '沙壆地铁站 公交站',
keyword: '沙壆地铁站 公交站',
region: ARRIVAL_SEARCH_REGION,
group: 'realtime',
realtimeKind: 'bus'
latitude: 22.69258,
longitude: 114.36372
},
{
id: 'natural-museum-west-bus-stop',
title: '自然博物馆西 公交站',
type: 'bus',
title: '自然博物馆西公交站',
subtitle: '龙坪路周边公交接驳点',
description: '靠近坪山云巴自然博物馆西站,可作为公共交通换乘参考。',
keyword: '自然博物馆西 公交站',
region: ARRIVAL_SEARCH_REGION,
group: 'realtime',
realtimeKind: 'bus'
latitude: 22.69193,
longitude: 114.35821
},
{
id: 'shabo-metro-bus-stop',
type: 'bus',
title: '沙壆地铁站公交站',
subtitle: '地铁 16 号线周边公交接驳',
description: '适合先到沙壆站后换乘公交或步行前往场馆。',
keyword: '沙壆地铁站 公交站',
region: ARRIVAL_SEARCH_REGION,
latitude: 22.68676,
longitude: 114.36441
},
{
id: 'shabo-station-exit-d',
type: 'metro',
title: '沙壆站 D口',
subtitle: '地铁 16 号线出口',
description: '出站后请以第三方地图展示的步行路线为准。',
keyword: '沙壆站D口',
region: ARRIVAL_SEARCH_REGION,
latitude: 22.68676,
longitude: 114.36441
},
{
id: 'natural-museum-west-station',
type: 'metro',
title: '自然博物馆西站',
subtitle: '坪山云巴 1 号线',
description: '云巴站点位于龙坪路与三洋湖工业一路交叉口以北。',
keyword: '自然博物馆西站',
region: ARRIVAL_SEARCH_REGION,
latitude: 22.69193,
longitude: 114.35821
},
{
id: 'museum-parking',
title: '深圳自然博物馆 停车场',
type: 'parking',
title: '深圳自然博物馆停车场',
subtitle: '场馆停车点位',
description: '停车开放规则、入口和余位请以现场及第三方地图为准。',
keyword: '深圳自然博物馆 停车场',
region: ARRIVAL_SEARCH_REGION,
group: 'realtime',
realtimeKind: 'parking'
latitude: 22.69228,
longitude: 114.36318
},
{
id: 'pingshan-cultural-cluster-parking',
title: '坪山文化聚落 停车场',
type: 'parking',
title: '坪山文化聚落停车场',
subtitle: '周边文化设施停车参考',
description: '适合作为周边停车备选,具体开放情况请以第三方地图为准。',
keyword: '坪山文化聚落 停车场',
region: ARRIVAL_SEARCH_REGION,
group: 'realtime',
realtimeKind: 'parking'
latitude: 22.69618,
longitude: 114.34652
},
{
id: 'yanzi-lake-area-parking',
title: '燕子湖片区 停车场',
type: 'parking',
title: '燕子湖片区停车场',
subtitle: '片区停车参考',
description: '请在第三方地图中确认入口、距离与可停情况。',
keyword: '燕子湖片区 停车场',
region: ARRIVAL_SEARCH_REGION,
group: 'realtime',
realtimeKind: 'parking'
latitude: 22.69084,
longitude: 114.36522
}
]
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')
export const ARRIVAL_TARGETS_BY_TYPE: Record<ArrivalTargetType, ArrivalSearchTarget[]> = {
bus: ARRIVAL_TARGETS.filter((target) => target.type === 'bus'),
metro: ARRIVAL_TARGETS.filter((target) => target.type === 'metro'),
parking: ARRIVAL_TARGETS.filter((target) => target.type === 'parking')
}

View File

@@ -40,6 +40,9 @@
zoom-controls-top="calc(100vh - 292px)"
:route-preview="activeRoutePreview"
:show-route="Boolean(activeRoutePreview)"
:outdoor-markers="arrivalOutdoorMarkers"
:active-outdoor-marker-id="activeArrivalMarkerId"
:outdoor-focus-offset-y="arrivalOutdoorFocusOffsetY"
:disable-auto-exit="disableIndoorAutoExit"
:auto-switch-threshold-low="0.58"
:auto-switch-threshold-high="1.18"
@@ -55,6 +58,7 @@
@initial-model-ready="handleInitialModelReady"
@initial-model-failed="handleInitialModelFailed"
@tool-click="handleIndoorToolClick"
@outdoor-marker-click="handleArrivalMarkerClick"
>
<template #overlay>
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
@@ -230,8 +234,14 @@
ref="arrivalPanelRef"
v-if="currentTab === 'guide'"
:visible="showArrivalPanel"
:default-collapsed="true"
:active-type="activeArrivalType"
:targets="activeArrivalTargets"
:selected-target-id="selectedArrivalTargetId"
:selected-target="selectedArrivalTarget"
@update:active-type="handleArrivalTypeChange"
@select-target="handleArrivalTargetSelect"
@collapsed-change="handleArrivalPanelCollapsedChange"
@layout-change="handleArrivalPanelLayoutChange"
/>
<view v-else-if="currentTab === 'explain'" class="explain-page">
@@ -332,6 +342,11 @@ import {
import type {
GuideRenderPoi
} from '@/domain/guideModel'
import {
ARRIVAL_TARGETS_BY_TYPE,
type ArrivalSearchTarget,
type ArrivalTargetType
} from '@/data/arrivalSearchTargets'
import type {
GuideRouteResult,
GuideRouteTarget,
@@ -563,6 +578,9 @@ const handleLaunchContinue = () => {
const showArrivalPanel = ref(false)
const showArrivalPanelCollapsed = ref(false)
const arrivalPanelRef = ref<InstanceType<typeof ArrivalPanel> | null>(null)
const activeArrivalType = ref<ArrivalTargetType>('bus')
const selectedArrivalTargetId = ref('')
const arrivalPanelHeight = ref(0)
const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
const indoorModelSource = guideUseCase.getModelSource()
@@ -637,6 +655,35 @@ const showGuideFloatingActions = computed(() => (
&& (!showGuideHomeDock.value || !homeSearchExpanded.value)
))
const activeArrivalTargets = computed(() => ARRIVAL_TARGETS_BY_TYPE[activeArrivalType.value])
const selectedArrivalTarget = computed(() => (
activeArrivalTargets.value.find((target) => target.id === selectedArrivalTargetId.value)
|| activeArrivalTargets.value[0]
|| null
))
const arrivalOutdoorMarkers = computed(() => {
if (!showArrivalPanel.value || is3DMode.value) return []
return activeArrivalTargets.value.map((target) => ({
id: target.id,
title: target.title,
subtitle: target.subtitle,
latitude: target.latitude,
longitude: target.longitude
}))
})
const activeArrivalMarkerId = computed(() => (
showArrivalPanel.value && !is3DMode.value ? selectedArrivalTarget.value?.id || '' : ''
))
const arrivalOutdoorFocusOffsetY = computed(() => {
if (!showArrivalPanel.value || showArrivalPanelCollapsed.value || is3DMode.value) return 0
return Math.min(260, Math.max(0, Math.round(arrivalPanelHeight.value / 2 + 24)))
})
const guideQuickActiveAction = computed<'indoor' | 'arrival' | 'explain'>(() => {
if (currentTab.value === 'explain') return 'explain'
if (showArrivalPanel.value) return 'arrival'
@@ -1295,15 +1342,46 @@ const handleMoreRouteGuide = () => {
showIndoorHint(`当前楼层:${getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'}`, 3200)
}
const selectFirstArrivalTarget = () => {
selectedArrivalTargetId.value = activeArrivalTargets.value[0]?.id || ''
}
const resetArrivalPanelState = () => {
activeArrivalType.value = 'bus'
selectFirstArrivalTarget()
}
const closeArrivalPanel = () => {
showArrivalPanel.value = false
showArrivalPanelCollapsed.value = false
selectedArrivalTargetId.value = ''
arrivalPanelHeight.value = 0
}
const handleArrivalPanelCollapsedChange = (collapsed: boolean) => {
showArrivalPanelCollapsed.value = collapsed
}
const handleArrivalPanelLayoutChange = (layout: { height: number; collapsed: boolean }) => {
arrivalPanelHeight.value = layout.height
showArrivalPanelCollapsed.value = layout.collapsed
}
const handleArrivalTypeChange = (type: ArrivalTargetType) => {
activeArrivalType.value = type
selectFirstArrivalTarget()
}
const handleArrivalTargetSelect = (target: ArrivalSearchTarget) => {
selectedArrivalTargetId.value = target.id
}
const handleArrivalMarkerClick = (markerId: string) => {
const target = activeArrivalTargets.value.find((item) => item.id === markerId)
if (!target) return
selectedArrivalTargetId.value = target.id
}
const closeHomeSearchDock = () => {
homeSearchPanelRef.value?.collapseHomePanel?.()
homeSearchExpanded.value = false
@@ -1316,8 +1394,9 @@ const handleMoreOutdoorNav = () => {
return
}
resetArrivalPanelState()
showArrivalPanel.value = true
showArrivalPanelCollapsed.value = true
showArrivalPanelCollapsed.value = false
showRoutePlanner.value = false
is3DMode.value = false
guideOutdoorState.value = 'home'