修复导览与讲解位置预览逻辑
This commit is contained in:
@@ -24,11 +24,6 @@
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="section-heading">
|
||||
<text class="section-title">请选择展厅</text>
|
||||
<text class="section-count">{{ filteredHalls.length }} 个展厅</text>
|
||||
</view>
|
||||
|
||||
<view v-if="filteredHalls.length" class="hall-list">
|
||||
<view
|
||||
v-for="hall in filteredHalls"
|
||||
@@ -37,10 +32,10 @@
|
||||
@tap="handleHallClick(hall.id)"
|
||||
>
|
||||
<image
|
||||
v-if="hall.image"
|
||||
v-if="hallIconUrl(hall)"
|
||||
class="hall-thumb"
|
||||
:src="hall.image"
|
||||
mode="aspectFill"
|
||||
:src="hallIconUrl(hall)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-else class="hall-thumb placeholder">
|
||||
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
|
||||
@@ -96,8 +91,26 @@ const emit = defineEmits<{
|
||||
back: []
|
||||
}>()
|
||||
|
||||
const HALL_ICON_BASE = '/static/icons/halls'
|
||||
|
||||
const hallIconMap: Record<string, string> = {
|
||||
宇宙厅: `${HALL_ICON_BASE}/universe.jpg`,
|
||||
地球厅: `${HALL_ICON_BASE}/earth.jpg`,
|
||||
演化厅: `${HALL_ICON_BASE}/evolution.jpg`,
|
||||
恐龙厅: `${HALL_ICON_BASE}/dinosaur.jpg`,
|
||||
人类厅: `${HALL_ICON_BASE}/human.jpg`,
|
||||
动物厅: `${HALL_ICON_BASE}/animal.jpg`,
|
||||
生物厅: `${HALL_ICON_BASE}/biology.jpg`,
|
||||
生态厅: `${HALL_ICON_BASE}/ecology.jpg`,
|
||||
家园厅: `${HALL_ICON_BASE}/homeland.jpg`
|
||||
}
|
||||
|
||||
const filteredHalls = computed(() => props.halls)
|
||||
|
||||
const hallIconUrl = (hall: ExplainHallSelectItem) => {
|
||||
return hallIconMap[hall.name.trim()] || hall.image || ''
|
||||
}
|
||||
|
||||
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
|
||||
|
||||
const handleHallClick = (hallId: string) => {
|
||||
@@ -124,8 +137,6 @@ const handleBack = () => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.section-count,
|
||||
.state-title,
|
||||
.state-desc,
|
||||
.hall-name,
|
||||
@@ -180,29 +191,6 @@ const handleBack = () => {
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 19px;
|
||||
line-height: 27px;
|
||||
font-weight: 800;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #7b8275;
|
||||
}
|
||||
|
||||
.hall-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -231,7 +219,10 @@ const handleBack = () => {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 8px;
|
||||
background: #f1f2ee;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(36, 49, 42, 0.06);
|
||||
box-sizing: border-box;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.hall-thumb.placeholder {
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
|
||||
<!-- 功能按钮 -->
|
||||
<cover-view class="action-buttons">
|
||||
<!-- 进入 3D 室内模式按钮 -->
|
||||
<!-- 进入 3D 馆内模式按钮 -->
|
||||
<cover-view class="action-btn indoor-mode-btn" @tap="handleEnter3D">
|
||||
<cover-view class="btn-text">🏛</cover-view>
|
||||
<cover-view class="btn-sub-text">室内</cover-view>
|
||||
<cover-view class="btn-sub-text">馆内</cover-view>
|
||||
</cover-view>
|
||||
<cover-view class="action-btn" @tap="handleLocation">
|
||||
<cover-view class="btn-text">📍</cover-view>
|
||||
@@ -129,7 +129,7 @@ const emit = defineEmits<{
|
||||
// POI ID: 7043042949572197975
|
||||
const mapCenter = ref<MapCenter>({
|
||||
latitude: 22.692763, // 深圳自然博物馆纬度
|
||||
longitude: 114.363572 // 深圳自然博物馆经度(调整到室外 2D 视觉中心)
|
||||
longitude: 114.363572 // 深圳自然博物馆经度(调整到馆外 2D 视觉中心)
|
||||
})
|
||||
|
||||
// 地图缩放级别(调整为适中比例,既能看到建筑全貌又能看清细节)
|
||||
@@ -378,9 +378,9 @@ const handleZoomOut = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 进入 3D 室内模式
|
||||
// 进入 3D 馆内模式
|
||||
const handleEnter3D = () => {
|
||||
console.log('进入 3D 室内模式')
|
||||
console.log('进入 3D 馆内模式')
|
||||
emit('enter3DMode')
|
||||
}
|
||||
|
||||
@@ -490,7 +490,7 @@ onMounted(async () => {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
// 室内模式按钮
|
||||
// 馆内模式按钮
|
||||
.indoor-mode-btn {
|
||||
width: 46px;
|
||||
height: 64px !important;
|
||||
|
||||
@@ -72,6 +72,7 @@ type TouchGestureMode = 'orbit' | 'pan'
|
||||
type ZoomCameraSource = 'button' | 'gesture'
|
||||
|
||||
const CANVAS_FONT_FAMILY = '"HarmonyOS Sans SC", "HarmonyOS Sans", "鸿蒙黑体", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", Arial, sans-serif'
|
||||
const FLOOR_CAMERA_DISTANCE_FACTOR = 0.78
|
||||
|
||||
interface CameraFitOptions {
|
||||
distanceFactor?: number
|
||||
@@ -203,7 +204,7 @@ const props = withDefaults(defineProps<{
|
||||
showPoi: false,
|
||||
targetFocus: null,
|
||||
touchGestureMode: 'orbit',
|
||||
targetFocusDistanceFactor: 0.22,
|
||||
targetFocusDistanceFactor: 0.36,
|
||||
routePreview: null,
|
||||
showRoute: false,
|
||||
autoSwitch: true,
|
||||
@@ -327,6 +328,9 @@ const syncControlInteractionOptions = () => {
|
||||
}
|
||||
|
||||
const corePoiCategories = new Set([
|
||||
'touring_poi',
|
||||
'exhibition_hall',
|
||||
'exhibition_hall_entrance',
|
||||
'basic_service_facility',
|
||||
'transport_circulation',
|
||||
'accessibility_special_service'
|
||||
@@ -334,10 +338,12 @@ const corePoiCategories = new Set([
|
||||
|
||||
const poiCategoryPriority: Record<string, number> = {
|
||||
target_preview: 100,
|
||||
exhibition_hall: 96,
|
||||
exhibition_hall_entrance: 92,
|
||||
touring_poi: 90,
|
||||
accessibility_special_service: 88,
|
||||
basic_service_facility: 82,
|
||||
transport_circulation: 76,
|
||||
touring_poi: 58,
|
||||
operation_experience: 46
|
||||
}
|
||||
|
||||
@@ -351,6 +357,9 @@ const poiGlyphMap: Record<string, string> = {
|
||||
stair: '梯',
|
||||
experience: '展',
|
||||
theater: '演',
|
||||
exhibition_hall: '展',
|
||||
hall_entrance: '门',
|
||||
entrance_exit: '门',
|
||||
target_preview: '位'
|
||||
}
|
||||
|
||||
@@ -2217,6 +2226,11 @@ const fitCameraToObject = (object: THREE.Object3D, preset: CameraPreset = 'obliq
|
||||
screenOffsetRatio: new THREE.Vector2(-0.045, 0.02),
|
||||
up: new THREE.Vector3(0, 1, 0)
|
||||
}
|
||||
: activeView.value === 'floor'
|
||||
? {
|
||||
distanceFactor: FLOOR_CAMERA_DISTANCE_FACTOR,
|
||||
up: new THREE.Vector3(0, 1, 0)
|
||||
}
|
||||
: {}
|
||||
|
||||
setCameraView(center, maxDim, direction, overviewFitOptions)
|
||||
@@ -2918,6 +2932,8 @@ const showFocusPoiAffordances = (poi: RenderPoi) => {
|
||||
const getPoiColor = (category: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
touring_poi: '#2f6fed',
|
||||
exhibition_hall: '#2f6fed',
|
||||
exhibition_hall_entrance: '#4659d8',
|
||||
basic_service_facility: '#1f8f5f',
|
||||
transport_circulation: '#d77b20',
|
||||
accessibility_special_service: '#8a5cf6',
|
||||
@@ -3166,7 +3182,7 @@ const triggerPoiTapFeedback = (sprite: THREE.Sprite) => {
|
||||
getPoiSpriteUserData(sprite).feedbackUntil = performance.now() + poiTapFeedbackDurationMs
|
||||
}
|
||||
|
||||
const handleSceneTap = (event: PointerEvent) => {
|
||||
const handleSceneTap = async (event: PointerEvent) => {
|
||||
if (!camera || !renderer || !getContainerElement()) return
|
||||
|
||||
const rect = renderer.domElement.getBoundingClientRect()
|
||||
@@ -3177,7 +3193,7 @@ const handleSceneTap = (event: PointerEvent) => {
|
||||
const raycaster = new THREE.Raycaster()
|
||||
raycaster.setFromCamera(pointer, camera)
|
||||
|
||||
if (activeView.value === 'floor' && poiGroup) {
|
||||
if ((activeView.value === 'floor' || activeView.value === 'overview') && poiGroup) {
|
||||
const poiHits = raycaster.intersectObjects([...getPoiHitTargets(), ...getPoiSprites()], false)
|
||||
const hit = poiHits.find((item) => (
|
||||
item.object.visible
|
||||
@@ -3187,10 +3203,27 @@ const handleSceneTap = (event: PointerEvent) => {
|
||||
const hitMarker = hit ? getPoiMarkerFromHit(hit.object) : findNearestPoiMarkerByScreenPoint(event, rect)
|
||||
|
||||
if (hitMarker?.userData.poi) {
|
||||
const poi = hitMarker.userData.poi as RenderPoi
|
||||
const selectedPoi = hitMarker.userData.poi as RenderPoi
|
||||
if (activeView.value === 'overview') {
|
||||
disableAutoSwitchTemporarily(10000)
|
||||
try {
|
||||
await loadFloor(selectedPoi.floorId, {
|
||||
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
|
||||
suppressProgress: true,
|
||||
allowOverviewFallback: true
|
||||
})
|
||||
emit('floorChange', selectedPoi.floorId)
|
||||
} catch (error) {
|
||||
if (isStaleModelLoadError(error)) return
|
||||
console.error('展厅点位楼层切换失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const poi = findLoadedPoi(selectedPoi.id) || selectedPoi
|
||||
const focusMarker = findPoiSprite(poi.id) || hitMarker
|
||||
selectedPOI.value = poi
|
||||
activeFocusPoiId.value = poi.id
|
||||
triggerPoiTapFeedback(hitMarker)
|
||||
triggerPoiTapFeedback(focusMarker)
|
||||
updatePoiMarkerFocus()
|
||||
showFocusPoiAffordances(poi)
|
||||
focusCameraOnPoi(poi)
|
||||
@@ -3231,7 +3264,7 @@ const handlePointerUp = (event: PointerEvent) => {
|
||||
|
||||
if (moveDistance > 8) return
|
||||
|
||||
handleSceneTap(event)
|
||||
void handleSceneTap(event)
|
||||
}
|
||||
|
||||
const emitTargetFocus = (
|
||||
@@ -3302,9 +3335,12 @@ const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
|
||||
|
||||
try {
|
||||
if (activeView.value !== 'floor' || currentFloor.value !== request.floorId) {
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
await loadFloor(request.floorId)
|
||||
await loadFloor(request.floorId, {
|
||||
preserveCurrentSceneUntilReady: hasRenderableSceneForFloorTransition(),
|
||||
suppressProgress: true,
|
||||
allowOverviewFallback: true
|
||||
})
|
||||
isLoading.value = false
|
||||
emit('floorChange', request.floorId)
|
||||
} else if (shouldRenderPoiMarkers.value && !getPoiSprites().length) {
|
||||
@@ -3419,31 +3455,19 @@ const init3DScene = async () => {
|
||||
|
||||
const handleFloorChange = async (floorId: string) => {
|
||||
const hasSceneToKeep = hasRenderableSceneForFloorTransition()
|
||||
const showLoading = !hasSceneToKeep && !canLoadFloorSilently(floorId)
|
||||
|
||||
try {
|
||||
if (showLoading) {
|
||||
isLoading.value = true
|
||||
}
|
||||
loadError.value = false
|
||||
await loadFloor(floorId, {
|
||||
preserveCurrentSceneUntilReady: hasSceneToKeep,
|
||||
suppressProgress: !showLoading
|
||||
suppressProgress: true,
|
||||
allowOverviewFallback: true
|
||||
})
|
||||
updatePoiVisibilityByDistance()
|
||||
if (showLoading) {
|
||||
isLoading.value = false
|
||||
} else {
|
||||
isLoading.value = false
|
||||
}
|
||||
isLoading.value = false
|
||||
emit('floorChange', floorId)
|
||||
} catch (error) {
|
||||
if (isStaleModelLoadError(error)) {
|
||||
if (showLoading) {
|
||||
isLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isStaleModelLoadError(error)) return
|
||||
|
||||
console.error('楼层模型加载失败:', error)
|
||||
loadError.value = true
|
||||
|
||||
@@ -126,11 +126,15 @@
|
||||
<text class="layer-mode-label">{{ layerModeActionLabel }}</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
<scroll-view
|
||||
v-if="showIndoorRightControls && showFloor"
|
||||
class="floor-switcher"
|
||||
:class="`side-${floorSide}`"
|
||||
:style="floorSwitcherStyle"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
:scroll-into-view="activeFloorScrollIntoView"
|
||||
:scroll-with-animation="true"
|
||||
>
|
||||
<view
|
||||
v-if="showFloorHeader"
|
||||
@@ -142,15 +146,16 @@
|
||||
<text class="floor-header-label">楼层</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="floor in floorLabels"
|
||||
:key="floor"
|
||||
v-for="floor in floorItems"
|
||||
:id="floor.scrollId"
|
||||
:key="floor.id"
|
||||
class="floor-item"
|
||||
:class="{ active: activeFloor === floor && layerMode !== 'multi' }"
|
||||
@tap="handleFloorChange(floor)"
|
||||
:class="{ active: activeFloorId === floor.id && layerMode !== 'multi' }"
|
||||
@tap="handleFloorChange(floor.label)"
|
||||
>
|
||||
<text class="floor-label">{{ floor }}</text>
|
||||
<text class="floor-label">{{ floor.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showZoomControls" class="zoom-controls" :style="zoomControlsStyle">
|
||||
<view class="zoom-btn" @tap="handleZoomClick('in')">
|
||||
@@ -364,7 +369,7 @@ const props = withDefaults(defineProps<{
|
||||
dimmed: false,
|
||||
targetFocusRequest: null,
|
||||
touchGestureMode: 'orbit',
|
||||
targetFocusDistanceFactor: 0.22,
|
||||
targetFocusDistanceFactor: 0.36,
|
||||
routePreview: null,
|
||||
showRoute: false,
|
||||
disableAutoExit: false,
|
||||
@@ -415,7 +420,11 @@ const indoorRendererRef = ref<{
|
||||
} | null>(null)
|
||||
|
||||
const indoorFloors = computed(() => props.floors.filter((floor) => isIndoorNavigableFloor(floor)))
|
||||
const floorLabels = computed(() => indoorFloors.value.map((floor) => floor.label))
|
||||
const createFloorScrollId = (floorId: string) => `guide-floor-${floorId.replace(/[^a-zA-Z0-9_-]/g, '-')}`
|
||||
const floorItems = computed(() => indoorFloors.value.map((floor) => ({
|
||||
...floor,
|
||||
scrollId: createFloorScrollId(floor.id)
|
||||
})))
|
||||
// #ifdef H5
|
||||
const effectiveIndoorModelSource = computed(() => props.indoorModelSource)
|
||||
// #endif
|
||||
@@ -435,6 +444,29 @@ const activeFloorId = computed(() => {
|
||||
))
|
||||
return normalizedFloor?.id || normalizedFloorId
|
||||
})
|
||||
const activeFloorScrollIntoView = ref('')
|
||||
|
||||
const syncActiveFloorScroll = () => {
|
||||
if (props.mapType !== 'indoor' || props.indoorView === 'overview' || props.layerMode === 'multi') {
|
||||
activeFloorScrollIntoView.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const matchedFloor = floorItems.value.find((floor) => floor.id === activeFloorId.value)
|
||||
activeFloorScrollIntoView.value = matchedFloor?.scrollId || ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
activeFloorId.value,
|
||||
props.indoorView,
|
||||
props.layerMode,
|
||||
props.mapType,
|
||||
floorItems.value.map((floor) => floor.id).join('|')
|
||||
],
|
||||
syncActiveFloorScroll,
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const searchFieldStyle = computed(() => ({
|
||||
top: props.searchTop
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<view class="action-row">
|
||||
<view class="action-btn secondary" @tap="emit('viewOutdoorMap')">
|
||||
<text class="action-text">室外入口参考</text>
|
||||
<text class="action-text">馆外入口参考</text>
|
||||
</view>
|
||||
<view class="action-btn primary" @tap="handleShowTargetLocation">
|
||||
<text class="action-text">查看三维位置</text>
|
||||
|
||||
@@ -220,7 +220,7 @@ const canNavigate = computed(() => {
|
||||
|
||||
const navigateButtonText = computed(() => {
|
||||
if (props.loading) return '规划中...'
|
||||
if (hasRoute.value) return '开始导航'
|
||||
if (hasRoute.value) return '打开地图'
|
||||
return '规划路线'
|
||||
})
|
||||
|
||||
|
||||
@@ -195,17 +195,11 @@ const canViewRoute = computed(() => Boolean(
|
||||
|
||||
const canPrimaryAction = computed(() => canViewRoute.value && !props.error)
|
||||
|
||||
const primaryActionText = computed(() => (
|
||||
props.hasRoutePreview
|
||||
? '开始导览'
|
||||
: (props.routeReady ? '查看路线' : '查看位置关系')
|
||||
))
|
||||
const primaryActionText = computed(() => '查看位置关系')
|
||||
|
||||
const routeOptionsTitle = computed(() => '馆内导览')
|
||||
|
||||
const loadingText = computed(() => (
|
||||
props.routeReady ? '正在规划馆内导览路线' : '正在生成馆内位置关系'
|
||||
))
|
||||
const loadingText = computed(() => '正在生成馆内位置关系')
|
||||
|
||||
const collapsedSummary = computed(() => {
|
||||
if (props.startPoint && props.endPoint) {
|
||||
@@ -217,7 +211,7 @@ const collapsedSummary = computed(() => {
|
||||
|
||||
const routeOptions = computed(() => {
|
||||
const unavailable = Boolean(props.error)
|
||||
const summaryText = props.summary || (props.routeReady ? '选择后规划真实路线' : '选择后查看位置关系')
|
||||
const summaryText = props.summary || '选择后查看位置关系'
|
||||
return [
|
||||
{
|
||||
id: 'recommended',
|
||||
@@ -226,23 +220,21 @@ const routeOptions = computed(() => {
|
||||
? '当前暂不可用'
|
||||
: props.hasRoutePreview
|
||||
? summaryText
|
||||
: props.routeReady
|
||||
? '调用 SGS SDK 路线规划'
|
||||
: '查看起点终点位置关系',
|
||||
: '查看起点终点位置关系',
|
||||
active: !unavailable,
|
||||
disabled: unavailable
|
||||
},
|
||||
{
|
||||
id: 'stairs',
|
||||
title: '少走楼梯',
|
||||
meta: props.routeReady ? '偏好能力待接入' : '偏好预览,待路线数据验证',
|
||||
meta: '偏好预览,待路线数据验证',
|
||||
active: false,
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
id: 'elevator',
|
||||
title: '电梯优先',
|
||||
meta: props.routeReady ? '偏好能力待接入' : '偏好预览,待路线数据验证',
|
||||
meta: '偏好预览,待路线数据验证',
|
||||
active: false,
|
||||
disabled: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user