简化来馆面板并优化地图点位展示
This commit is contained in:
@@ -59,7 +59,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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'
|
import { getMuseumEntranceLocation } from '@/services/tencent/TencentMapService'
|
||||||
|
|
||||||
const isH5 = typeof window !== 'undefined'
|
const isH5 = typeof window !== 'undefined'
|
||||||
@@ -103,6 +103,14 @@ interface Polyline {
|
|||||||
dottedLine?: boolean
|
dottedLine?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OutdoorMapMarker {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
}
|
||||||
|
|
||||||
interface MapFloor {
|
interface MapFloor {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
@@ -112,14 +120,21 @@ const props = withDefaults(defineProps<{
|
|||||||
activeFloor?: string
|
activeFloor?: string
|
||||||
floors?: MapFloor[]
|
floors?: MapFloor[]
|
||||||
polylines?: Polyline[]
|
polylines?: Polyline[]
|
||||||
|
outdoorMarkers?: OutdoorMapMarker[]
|
||||||
|
activeOutdoorMarkerId?: string
|
||||||
|
outdoorFocusOffsetY?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
activeFloor: '1F',
|
activeFloor: '1F',
|
||||||
floors: () => [] as MapFloor[],
|
floors: () => [] as MapFloor[],
|
||||||
polylines: () => [] as Polyline[]
|
polylines: () => [] as Polyline[],
|
||||||
|
outdoorMarkers: () => [] as OutdoorMapMarker[],
|
||||||
|
activeOutdoorMarkerId: '',
|
||||||
|
outdoorFocusOffsetY: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
markerClick: [markerId: number]
|
markerClick: [markerId: number]
|
||||||
|
outdoorMarkerClick: [markerId: string]
|
||||||
floorChange: [floor: string]
|
floorChange: [floor: string]
|
||||||
enter3DMode: []
|
enter3DMode: []
|
||||||
mapTap: [location: { latitude: number; longitude: number }]
|
mapTap: [location: { latitude: number; longitude: number }]
|
||||||
@@ -138,9 +153,11 @@ const mapScale = ref(17)
|
|||||||
// 当前楼层
|
// 当前楼层
|
||||||
const currentFloor = ref(props.activeFloor)
|
const currentFloor = ref(props.activeFloor)
|
||||||
const mapComponentRef = ref<unknown>(null)
|
const mapComponentRef = ref<unknown>(null)
|
||||||
|
const userLocationMarker = ref<Marker | null>(null)
|
||||||
let mapAuthErrorObserver: MutationObserver | null = null
|
let mapAuthErrorObserver: MutationObserver | null = null
|
||||||
let mapAuthErrorLogged = false
|
let mapAuthErrorLogged = false
|
||||||
const mapAuthErrorText = '鉴权失败,请传入正确的key'
|
const mapAuthErrorText = '鉴权失败,请传入正确的key'
|
||||||
|
const OUTDOOR_MARKER_ID_BASE = 20000
|
||||||
|
|
||||||
// 楼层列表
|
// 楼层列表
|
||||||
const floors = computed(() => props.floors)
|
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 组件)
|
// 显示的路线列表(转换格式以适配 uni-app map 组件)
|
||||||
const displayPolylines = computed(() => {
|
const displayPolylines = computed(() => {
|
||||||
@@ -233,8 +288,7 @@ const displayPolylines = computed(() => {
|
|||||||
* 更新用户位置标记
|
* 更新用户位置标记
|
||||||
*/
|
*/
|
||||||
const updateUserLocation = (location: { latitude: number; longitude: number }) => {
|
const updateUserLocation = (location: { latitude: number; longitude: number }) => {
|
||||||
const userMarkerIndex = markers.value.findIndex((m) => m.id === 999)
|
userLocationMarker.value = {
|
||||||
const userMarker: Marker = {
|
|
||||||
id: 999,
|
id: 999,
|
||||||
latitude: location.latitude,
|
latitude: location.latitude,
|
||||||
longitude: location.longitude,
|
longitude: location.longitude,
|
||||||
@@ -252,22 +306,13 @@ const updateUserLocation = (location: { latitude: number; longitude: number }) =
|
|||||||
fontSize: 12
|
fontSize: 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userMarkerIndex >= 0) {
|
|
||||||
markers.value[userMarkerIndex] = userMarker
|
|
||||||
} else {
|
|
||||||
markers.value.push(userMarker)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除用户位置标记
|
* 清除用户位置标记
|
||||||
*/
|
*/
|
||||||
const clearUserLocation = () => {
|
const clearUserLocation = () => {
|
||||||
const userMarkerIndex = markers.value.findIndex((m) => m.id === 999)
|
userLocationMarker.value = null
|
||||||
if (userMarkerIndex >= 0) {
|
|
||||||
markers.value.splice(userMarkerIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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({
|
defineExpose({
|
||||||
updateUserLocation,
|
updateUserLocation,
|
||||||
@@ -289,8 +359,12 @@ defineExpose({
|
|||||||
|
|
||||||
// 处理标记点点击
|
// 处理标记点点击
|
||||||
const handleMarkerTap = (e: any) => {
|
const handleMarkerTap = (e: any) => {
|
||||||
const markerId = e.detail.markerId || e.markerId
|
const markerId = Number(e.detail?.markerId ?? e.markerId)
|
||||||
console.log('点击标记点:', markerId)
|
console.log('点击标记点:', markerId)
|
||||||
|
const outdoorMarkerId = outdoorMarkerIdMap.value.get(markerId)
|
||||||
|
if (outdoorMarkerId) {
|
||||||
|
emit('outdoorMarkerClick', outdoorMarkerId)
|
||||||
|
}
|
||||||
emit('markerClick', markerId)
|
emit('markerClick', markerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,8 +402,7 @@ const handleLocation = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加或更新用户位置标记
|
// 添加或更新用户位置标记
|
||||||
const userMarkerIndex = markers.value.findIndex(m => m.id === 999)
|
userLocationMarker.value = {
|
||||||
const userMarker: Marker = {
|
|
||||||
id: 999,
|
id: 999,
|
||||||
latitude: res.latitude,
|
latitude: res.latitude,
|
||||||
longitude: res.longitude,
|
longitude: res.longitude,
|
||||||
@@ -348,12 +421,6 @@ const handleLocation = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userMarkerIndex >= 0) {
|
|
||||||
markers.value[userMarkerIndex] = userMarker
|
|
||||||
} else {
|
|
||||||
markers.value.push(userMarker)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('定位成功:', res)
|
console.log('定位成功:', res)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
@@ -388,6 +455,23 @@ const handleEnter3D = () => {
|
|||||||
emit('enter3DMode')
|
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 getMapComponentElement = () => {
|
||||||
const rawRef = mapComponentRef.value
|
const rawRef = mapComponentRef.value
|
||||||
if (rawRef instanceof HTMLElement) return rawRef
|
if (rawRef instanceof HTMLElement) return rawRef
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@tap="collapsePanel"
|
@tap="collapsePanel"
|
||||||
></view>
|
></view>
|
||||||
<view
|
<view
|
||||||
|
ref="arrivalPanelRef"
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
class="arrival-panel"
|
class="arrival-panel"
|
||||||
:class="{ collapsed: isCollapsed }"
|
:class="{ collapsed: isCollapsed }"
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
<view v-if="isCollapsed" class="arrival-collapsed" @tap="expandPanel">
|
<view v-if="isCollapsed" class="arrival-collapsed" @tap="expandPanel">
|
||||||
<view class="arrival-collapsed-copy">
|
<view class="arrival-collapsed-copy">
|
||||||
<text class="arrival-collapsed-title">来馆</text>
|
<text class="arrival-collapsed-title">来馆</text>
|
||||||
<text class="arrival-collapsed-summary">公交 / 地铁 / 停车</text>
|
<text class="arrival-collapsed-summary">{{ selectedTarget?.title || activeTypeLabel }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="arrival-collapsed-action">
|
<view class="arrival-collapsed-action">
|
||||||
<text class="arrival-collapsed-action-text">展开</text>
|
<text class="arrival-collapsed-action-text">展开</text>
|
||||||
@@ -26,78 +27,59 @@
|
|||||||
<view class="arrival-header">
|
<view class="arrival-header">
|
||||||
<view class="arrival-title-group">
|
<view class="arrival-title-group">
|
||||||
<text class="arrival-kicker">来馆</text>
|
<text class="arrival-kicker">来馆</text>
|
||||||
<text class="arrival-title">选择第三方地图检索目标</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="arrival-collapse" @tap="collapsePanel">
|
<view class="arrival-collapse" @tap="collapsePanel">
|
||||||
<text class="arrival-collapse-text">收起</text>
|
<text class="arrival-collapse-text">收起</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="arrival-notes">
|
<view class="arrival-tabs">
|
||||||
<text class="arrival-note">公交站、停车场信息以第三方地图实时结果为准</text>
|
<view
|
||||||
<text class="arrival-note">暂未接入停车余位</text>
|
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>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="arrival-section">
|
<view class="arrival-list">
|
||||||
<text class="arrival-section-title">导航到场馆</text>
|
|
||||||
<view
|
<view
|
||||||
v-for="target in venueTargets"
|
v-for="target in targets"
|
||||||
:key="target.id"
|
:key="target.id"
|
||||||
class="arrival-target-row"
|
class="arrival-target-row"
|
||||||
|
:class="{ active: target.id === selectedTargetId }"
|
||||||
@tap="handleTargetSelect(target)"
|
@tap="handleTargetSelect(target)"
|
||||||
>
|
>
|
||||||
<view class="arrival-target-dot"></view>
|
<view class="arrival-target-dot"></view>
|
||||||
<view class="arrival-target-copy">
|
<view class="arrival-target-copy">
|
||||||
<text class="arrival-target-title">{{ target.title }}</text>
|
<text class="arrival-target-title">{{ target.title }}</text>
|
||||||
<text class="arrival-target-keyword">{{ target.keyword }}</text>
|
<text class="arrival-target-subtitle">{{ target.subtitle }}</text>
|
||||||
|
<text v-if="target.description" class="arrival-target-desc">{{ target.description }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="arrival-target-action">选择地图</text>
|
<text v-if="target.id === selectedTargetId" class="arrival-target-state">已选</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="arrival-section">
|
|
||||||
<text class="arrival-section-title">推荐公共交通</text>
|
|
||||||
<view
|
<view
|
||||||
v-for="target in transitTargets"
|
class="arrival-primary"
|
||||||
:key="target.id"
|
:class="{ disabled: !selectedTarget }"
|
||||||
class="arrival-target-row"
|
@tap="handleNavigateTap"
|
||||||
@tap="handleTargetSelect(target)"
|
|
||||||
>
|
>
|
||||||
<view class="arrival-target-dot"></view>
|
<text class="arrival-primary-text">第三方导航</text>
|
||||||
<view class="arrival-target-copy">
|
|
||||||
<text class="arrival-target-title">{{ target.title }}</text>
|
|
||||||
<text class="arrival-target-keyword">{{ target.keyword }}</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>
|
|
||||||
<text class="arrival-target-action">选择地图</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-if="selectedTarget"
|
v-if="providerSheetVisible && selectedTarget"
|
||||||
class="provider-scrim"
|
class="provider-scrim"
|
||||||
@tap="closeProviderSheet"
|
@tap="closeProviderSheet"
|
||||||
></view>
|
></view>
|
||||||
<view
|
<view
|
||||||
v-if="selectedTarget"
|
v-if="providerSheetVisible && selectedTarget"
|
||||||
class="provider-sheet"
|
class="provider-sheet"
|
||||||
@tap.stop
|
@tap.stop
|
||||||
>
|
>
|
||||||
@@ -126,12 +108,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
ARRIVAL_REALTIME_TARGETS,
|
ARRIVAL_TARGET_TYPES,
|
||||||
ARRIVAL_TRANSIT_TARGETS,
|
type ArrivalSearchTarget,
|
||||||
ARRIVAL_VENUE_TARGETS,
|
type ArrivalTargetType
|
||||||
type ArrivalSearchTarget
|
|
||||||
} from '@/data/arrivalSearchTargets'
|
} from '@/data/arrivalSearchTargets'
|
||||||
import {
|
import {
|
||||||
openThirdPartyMapSearch,
|
openThirdPartyMapSearch,
|
||||||
@@ -141,25 +122,44 @@ import {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
defaultCollapsed?: boolean
|
activeType: ArrivalTargetType
|
||||||
|
targets?: ArrivalSearchTarget[]
|
||||||
|
selectedTargetId?: string
|
||||||
|
selectedTarget?: ArrivalSearchTarget | null
|
||||||
}>(), {
|
}>(), {
|
||||||
visible: false,
|
visible: false,
|
||||||
defaultCollapsed: false
|
targets: () => [] as ArrivalSearchTarget[],
|
||||||
|
selectedTargetId: '',
|
||||||
|
selectedTarget: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
'update:activeType': [type: ArrivalTargetType]
|
||||||
|
selectTarget: [target: ArrivalSearchTarget]
|
||||||
collapsedChange: [collapsed: boolean]
|
collapsedChange: [collapsed: boolean]
|
||||||
|
layoutChange: [layout: { height: number; collapsed: boolean }]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const venueTargets = computed(() => ARRIVAL_VENUE_TARGETS)
|
const typeOptions = computed(() => ARRIVAL_TARGET_TYPES)
|
||||||
const transitTargets = computed(() => ARRIVAL_TRANSIT_TARGETS)
|
|
||||||
const realtimeTargets = computed(() => ARRIVAL_REALTIME_TARGETS)
|
|
||||||
const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS)
|
const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS)
|
||||||
const selectedTarget = ref<ArrivalSearchTarget | null>(null)
|
const arrivalPanelRef = ref<unknown>(null)
|
||||||
const isCollapsed = ref(false)
|
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) => {
|
const handleTargetSelect = (target: ArrivalSearchTarget) => {
|
||||||
selectedTarget.value = target
|
closeProviderSheet()
|
||||||
|
emit('selectTarget', target)
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsePanel = () => {
|
const collapsePanel = () => {
|
||||||
@@ -173,11 +173,76 @@ const expandPanel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeProviderSheet = () => {
|
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 handleProviderSelect = (provider: ThirdPartyMapProviderOption) => {
|
||||||
const target = selectedTarget.value
|
const target = props.selectedTarget
|
||||||
if (!target) return
|
if (!target) return
|
||||||
|
|
||||||
openThirdPartyMapSearch(provider.provider, {
|
openThirdPartyMapSearch(provider.provider, {
|
||||||
@@ -190,16 +255,37 @@ watch(
|
|||||||
() => props.visible,
|
() => props.visible,
|
||||||
(visible) => {
|
(visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
isCollapsed.value = props.defaultCollapsed
|
|
||||||
emit('collapsedChange', props.defaultCollapsed)
|
|
||||||
} else {
|
|
||||||
selectedTarget.value = null
|
|
||||||
isCollapsed.value = false
|
isCollapsed.value = false
|
||||||
emit('collapsedChange', 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({
|
defineExpose({
|
||||||
expandPanel
|
expandPanel
|
||||||
})
|
})
|
||||||
@@ -210,7 +296,7 @@ defineExpose({
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 2090;
|
z-index: 2090;
|
||||||
background: rgba(0, 0, 0, 0.18);
|
background: rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrival-panel {
|
.arrival-panel {
|
||||||
@@ -219,8 +305,8 @@ defineExpose({
|
|||||||
bottom: calc(env(safe-area-inset-bottom) + 16px);
|
bottom: calc(env(safe-area-inset-bottom) + 16px);
|
||||||
z-index: 2100;
|
z-index: 2100;
|
||||||
width: min(430px, calc(100vw - 24px));
|
width: min(430px, calc(100vw - 24px));
|
||||||
max-height: min(72vh, 620px);
|
max-height: min(58vh, 520px);
|
||||||
padding: 10px 14px 16px;
|
padding: 10px 14px 14px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -346,12 +432,115 @@ defineExpose({
|
|||||||
color: #424754;
|
color: #424754;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrival-notes {
|
.arrival-tabs {
|
||||||
margin-top: 12px;
|
height: 40px;
|
||||||
padding: 10px 12px;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
background: #f5f5ed;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
@@ -362,68 +551,25 @@ defineExpose({
|
|||||||
color: #424754;
|
color: #424754;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrival-section {
|
.arrival-primary {
|
||||||
margin-top: 16px;
|
height: 44px;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: center;
|
||||||
padding: 10px 0;
|
margin-top: 12px;
|
||||||
border-top: 1px solid #eceee7;
|
background: #262421;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrival-section-title + .arrival-target-row {
|
.arrival-primary.disabled {
|
||||||
border-top: 0;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrival-target-dot {
|
.arrival-primary-text {
|
||||||
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 {
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #262421;
|
color: #e0e100;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-scrim {
|
.provider-scrim {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@
|
|||||||
:active-floor="activeFloor"
|
:active-floor="activeFloor"
|
||||||
:floors="props.floors"
|
:floors="props.floors"
|
||||||
:polylines="outdoorNavPolylines"
|
:polylines="outdoorNavPolylines"
|
||||||
|
:outdoor-markers="outdoorMarkers"
|
||||||
|
:active-outdoor-marker-id="activeOutdoorMarkerId"
|
||||||
|
:outdoor-focus-offset-y="outdoorFocusOffsetY"
|
||||||
|
@outdoor-marker-click="handleOutdoorMarkerClick"
|
||||||
@map-tap="handleMapTap"
|
@map-tap="handleMapTap"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -265,6 +269,14 @@ interface OutdoorNavPolyline {
|
|||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OutdoorMapMarker {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
latitude: number
|
||||||
|
longitude: number
|
||||||
|
}
|
||||||
|
|
||||||
type CameraViewMode = 'reset' | 'top' | 'oblique'
|
type CameraViewMode = 'reset' | 'top' | 'oblique'
|
||||||
|
|
||||||
interface TargetPoiFocusRequest {
|
interface TargetPoiFocusRequest {
|
||||||
@@ -347,6 +359,9 @@ const props = withDefaults(defineProps<{
|
|||||||
autoSwitchThresholdLow?: number
|
autoSwitchThresholdLow?: number
|
||||||
autoSwitchThresholdHigh?: number
|
autoSwitchThresholdHigh?: number
|
||||||
outdoorNavPolylines?: OutdoorNavPolyline[]
|
outdoorNavPolylines?: OutdoorNavPolyline[]
|
||||||
|
outdoorMarkers?: OutdoorMapMarker[]
|
||||||
|
activeOutdoorMarkerId?: string
|
||||||
|
outdoorFocusOffsetY?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
searchText: '请输入地点进行搜索',
|
searchText: '请输入地点进行搜索',
|
||||||
activeMode: '3d',
|
activeMode: '3d',
|
||||||
@@ -395,7 +410,10 @@ const props = withDefaults(defineProps<{
|
|||||||
disableAutoExit: false,
|
disableAutoExit: false,
|
||||||
autoSwitchThresholdLow: 1.0,
|
autoSwitchThresholdLow: 1.0,
|
||||||
autoSwitchThresholdHigh: 1.3,
|
autoSwitchThresholdHigh: 1.3,
|
||||||
outdoorNavPolylines: () => [] as OutdoorNavPolyline[]
|
outdoorNavPolylines: () => [] as OutdoorNavPolyline[],
|
||||||
|
outdoorMarkers: () => [] as OutdoorMapMarker[],
|
||||||
|
activeOutdoorMarkerId: '',
|
||||||
|
outdoorFocusOffsetY: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -416,6 +434,7 @@ const emit = defineEmits<{
|
|||||||
initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }]
|
initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }]
|
||||||
initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }]
|
initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }]
|
||||||
mapTap: [location: { latitude: number; longitude: number }]
|
mapTap: [location: { latitude: number; longitude: number }]
|
||||||
|
outdoorMarkerClick: [markerId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 监听视角切换,循环切换:reset -> top -> oblique -> reset
|
// 监听视角切换,循环切换:reset -> top -> oblique -> reset
|
||||||
@@ -739,6 +758,10 @@ const handleMapTap = (location: { latitude?: number; longitude?: number }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOutdoorMarkerClick = (markerId: string) => {
|
||||||
|
emit('outdoorMarkerClick', markerId)
|
||||||
|
}
|
||||||
|
|
||||||
// 暴露 clearRoute 方法供父组件显式调用路线清除
|
// 暴露 clearRoute 方法供父组件显式调用路线清除
|
||||||
defineExpose({
|
defineExpose({
|
||||||
clearRoute: () => {
|
clearRoute: () => {
|
||||||
|
|||||||
@@ -1,89 +1,123 @@
|
|||||||
export type ArrivalTargetGroup = 'venue' | 'transit' | 'realtime'
|
export type ArrivalTargetType = 'bus' | 'metro' | 'parking'
|
||||||
export type ArrivalRealtimeKind = 'bus' | 'parking'
|
|
||||||
|
export interface ArrivalTargetTypeOption {
|
||||||
|
type: ArrivalTargetType
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ArrivalSearchTarget {
|
export interface ArrivalSearchTarget {
|
||||||
id: string
|
id: string
|
||||||
|
type: ArrivalTargetType
|
||||||
title: string
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
description?: string
|
||||||
keyword: string
|
keyword: string
|
||||||
region: string
|
region: string
|
||||||
group: ArrivalTargetGroup
|
latitude: number
|
||||||
realtimeKind?: ArrivalRealtimeKind
|
longitude: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ARRIVAL_SEARCH_REGION = '深圳市'
|
export const ARRIVAL_SEARCH_REGION = '深圳市'
|
||||||
|
|
||||||
export const ARRIVAL_SEARCH_TARGETS: ArrivalSearchTarget[] = [
|
export const ARRIVAL_TARGET_TYPES: ArrivalTargetTypeOption[] = [
|
||||||
{
|
{ type: 'bus', label: '公交' },
|
||||||
id: 'shenzhen-natural-history-museum',
|
{ type: 'metro', label: '地铁' },
|
||||||
title: '深圳自然博物馆',
|
{ type: 'parking', label: '停车场' }
|
||||||
keyword: '深圳自然博物馆',
|
]
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
|
||||||
group: 'venue'
|
export const ARRIVAL_TARGETS: ArrivalSearchTarget[] = [
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
id: 'museum-bus-stop',
|
||||||
title: '深圳自然博物馆 公交站',
|
type: 'bus',
|
||||||
|
title: '深圳自然博物馆公交站',
|
||||||
|
subtitle: '场馆周边公交接驳点',
|
||||||
|
description: '适合到馆前在第三方地图中继续查看实时公交和步行路径。',
|
||||||
keyword: '深圳自然博物馆 公交站',
|
keyword: '深圳自然博物馆 公交站',
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
region: ARRIVAL_SEARCH_REGION,
|
||||||
group: 'realtime',
|
latitude: 22.69258,
|
||||||
realtimeKind: 'bus'
|
longitude: 114.36372
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'shabo-metro-bus-stop',
|
|
||||||
title: '沙壆地铁站 公交站',
|
|
||||||
keyword: '沙壆地铁站 公交站',
|
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
|
||||||
group: 'realtime',
|
|
||||||
realtimeKind: 'bus'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'natural-museum-west-bus-stop',
|
id: 'natural-museum-west-bus-stop',
|
||||||
title: '自然博物馆西 公交站',
|
type: 'bus',
|
||||||
|
title: '自然博物馆西公交站',
|
||||||
|
subtitle: '龙坪路周边公交接驳点',
|
||||||
|
description: '靠近坪山云巴自然博物馆西站,可作为公共交通换乘参考。',
|
||||||
keyword: '自然博物馆西 公交站',
|
keyword: '自然博物馆西 公交站',
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
region: ARRIVAL_SEARCH_REGION,
|
||||||
group: 'realtime',
|
latitude: 22.69193,
|
||||||
realtimeKind: 'bus'
|
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',
|
id: 'museum-parking',
|
||||||
title: '深圳自然博物馆 停车场',
|
type: 'parking',
|
||||||
|
title: '深圳自然博物馆停车场',
|
||||||
|
subtitle: '场馆停车点位',
|
||||||
|
description: '停车开放规则、入口和余位请以现场及第三方地图为准。',
|
||||||
keyword: '深圳自然博物馆 停车场',
|
keyword: '深圳自然博物馆 停车场',
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
region: ARRIVAL_SEARCH_REGION,
|
||||||
group: 'realtime',
|
latitude: 22.69228,
|
||||||
realtimeKind: 'parking'
|
longitude: 114.36318
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pingshan-cultural-cluster-parking',
|
id: 'pingshan-cultural-cluster-parking',
|
||||||
title: '坪山文化聚落 停车场',
|
type: 'parking',
|
||||||
|
title: '坪山文化聚落停车场',
|
||||||
|
subtitle: '周边文化设施停车参考',
|
||||||
|
description: '适合作为周边停车备选,具体开放情况请以第三方地图为准。',
|
||||||
keyword: '坪山文化聚落 停车场',
|
keyword: '坪山文化聚落 停车场',
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
region: ARRIVAL_SEARCH_REGION,
|
||||||
group: 'realtime',
|
latitude: 22.69618,
|
||||||
realtimeKind: 'parking'
|
longitude: 114.34652
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yanzi-lake-area-parking',
|
id: 'yanzi-lake-area-parking',
|
||||||
title: '燕子湖片区 停车场',
|
type: 'parking',
|
||||||
|
title: '燕子湖片区停车场',
|
||||||
|
subtitle: '片区停车参考',
|
||||||
|
description: '请在第三方地图中确认入口、距离与可停情况。',
|
||||||
keyword: '燕子湖片区 停车场',
|
keyword: '燕子湖片区 停车场',
|
||||||
region: ARRIVAL_SEARCH_REGION,
|
region: ARRIVAL_SEARCH_REGION,
|
||||||
group: 'realtime',
|
latitude: 22.69084,
|
||||||
realtimeKind: 'parking'
|
longitude: 114.36522
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ARRIVAL_VENUE_TARGETS = ARRIVAL_SEARCH_TARGETS.filter((target) => target.group === 'venue')
|
export const ARRIVAL_TARGETS_BY_TYPE: Record<ArrivalTargetType, ArrivalSearchTarget[]> = {
|
||||||
export const ARRIVAL_TRANSIT_TARGETS = ARRIVAL_SEARCH_TARGETS.filter((target) => target.group === 'transit')
|
bus: ARRIVAL_TARGETS.filter((target) => target.type === 'bus'),
|
||||||
export const ARRIVAL_REALTIME_TARGETS = ARRIVAL_SEARCH_TARGETS.filter((target) => target.group === 'realtime')
|
metro: ARRIVAL_TARGETS.filter((target) => target.type === 'metro'),
|
||||||
|
parking: ARRIVAL_TARGETS.filter((target) => target.type === 'parking')
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,9 @@
|
|||||||
zoom-controls-top="calc(100vh - 292px)"
|
zoom-controls-top="calc(100vh - 292px)"
|
||||||
:route-preview="activeRoutePreview"
|
:route-preview="activeRoutePreview"
|
||||||
:show-route="Boolean(activeRoutePreview)"
|
:show-route="Boolean(activeRoutePreview)"
|
||||||
|
:outdoor-markers="arrivalOutdoorMarkers"
|
||||||
|
:active-outdoor-marker-id="activeArrivalMarkerId"
|
||||||
|
:outdoor-focus-offset-y="arrivalOutdoorFocusOffsetY"
|
||||||
:disable-auto-exit="disableIndoorAutoExit"
|
:disable-auto-exit="disableIndoorAutoExit"
|
||||||
:auto-switch-threshold-low="0.58"
|
:auto-switch-threshold-low="0.58"
|
||||||
:auto-switch-threshold-high="1.18"
|
:auto-switch-threshold-high="1.18"
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
@initial-model-ready="handleInitialModelReady"
|
@initial-model-ready="handleInitialModelReady"
|
||||||
@initial-model-failed="handleInitialModelFailed"
|
@initial-model-failed="handleInitialModelFailed"
|
||||||
@tool-click="handleIndoorToolClick"
|
@tool-click="handleIndoorToolClick"
|
||||||
|
@outdoor-marker-click="handleArrivalMarkerClick"
|
||||||
>
|
>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
|
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
|
||||||
@@ -230,8 +234,14 @@
|
|||||||
ref="arrivalPanelRef"
|
ref="arrivalPanelRef"
|
||||||
v-if="currentTab === 'guide'"
|
v-if="currentTab === 'guide'"
|
||||||
:visible="showArrivalPanel"
|
: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"
|
@collapsed-change="handleArrivalPanelCollapsedChange"
|
||||||
|
@layout-change="handleArrivalPanelLayoutChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||||||
@@ -332,6 +342,11 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
GuideRenderPoi
|
GuideRenderPoi
|
||||||
} from '@/domain/guideModel'
|
} from '@/domain/guideModel'
|
||||||
|
import {
|
||||||
|
ARRIVAL_TARGETS_BY_TYPE,
|
||||||
|
type ArrivalSearchTarget,
|
||||||
|
type ArrivalTargetType
|
||||||
|
} from '@/data/arrivalSearchTargets'
|
||||||
import type {
|
import type {
|
||||||
GuideRouteResult,
|
GuideRouteResult,
|
||||||
GuideRouteTarget,
|
GuideRouteTarget,
|
||||||
@@ -563,6 +578,9 @@ const handleLaunchContinue = () => {
|
|||||||
const showArrivalPanel = ref(false)
|
const showArrivalPanel = ref(false)
|
||||||
const showArrivalPanelCollapsed = ref(false)
|
const showArrivalPanelCollapsed = ref(false)
|
||||||
const arrivalPanelRef = ref<InstanceType<typeof ArrivalPanel> | null>(null)
|
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 indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
||||||
const indoorModelSource = guideUseCase.getModelSource()
|
const indoorModelSource = guideUseCase.getModelSource()
|
||||||
@@ -637,6 +655,35 @@ const showGuideFloatingActions = computed(() => (
|
|||||||
&& (!showGuideHomeDock.value || !homeSearchExpanded.value)
|
&& (!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'>(() => {
|
const guideQuickActiveAction = computed<'indoor' | 'arrival' | 'explain'>(() => {
|
||||||
if (currentTab.value === 'explain') return 'explain'
|
if (currentTab.value === 'explain') return 'explain'
|
||||||
if (showArrivalPanel.value) return 'arrival'
|
if (showArrivalPanel.value) return 'arrival'
|
||||||
@@ -1295,15 +1342,46 @@ const handleMoreRouteGuide = () => {
|
|||||||
showIndoorHint(`当前楼层:${getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'}`, 3200)
|
showIndoorHint(`当前楼层:${getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'}`, 3200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectFirstArrivalTarget = () => {
|
||||||
|
selectedArrivalTargetId.value = activeArrivalTargets.value[0]?.id || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetArrivalPanelState = () => {
|
||||||
|
activeArrivalType.value = 'bus'
|
||||||
|
selectFirstArrivalTarget()
|
||||||
|
}
|
||||||
|
|
||||||
const closeArrivalPanel = () => {
|
const closeArrivalPanel = () => {
|
||||||
showArrivalPanel.value = false
|
showArrivalPanel.value = false
|
||||||
showArrivalPanelCollapsed.value = false
|
showArrivalPanelCollapsed.value = false
|
||||||
|
selectedArrivalTargetId.value = ''
|
||||||
|
arrivalPanelHeight.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleArrivalPanelCollapsedChange = (collapsed: boolean) => {
|
const handleArrivalPanelCollapsedChange = (collapsed: boolean) => {
|
||||||
showArrivalPanelCollapsed.value = collapsed
|
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 = () => {
|
const closeHomeSearchDock = () => {
|
||||||
homeSearchPanelRef.value?.collapseHomePanel?.()
|
homeSearchPanelRef.value?.collapseHomePanel?.()
|
||||||
homeSearchExpanded.value = false
|
homeSearchExpanded.value = false
|
||||||
@@ -1316,8 +1394,9 @@ const handleMoreOutdoorNav = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetArrivalPanelState()
|
||||||
showArrivalPanel.value = true
|
showArrivalPanel.value = true
|
||||||
showArrivalPanelCollapsed.value = true
|
showArrivalPanelCollapsed.value = false
|
||||||
showRoutePlanner.value = false
|
showRoutePlanner.value = false
|
||||||
is3DMode.value = false
|
is3DMode.value = false
|
||||||
guideOutdoorState.value = 'home'
|
guideOutdoorState.value = 'home'
|
||||||
|
|||||||
Reference in New Issue
Block a user