From d4b97379bb5f13cf36c21fc04e74d97fa507cfcc Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Thu, 25 Jun 2026 03:43:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E8=A7=88=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=9D=A2=E6=9D=BF=E4=B8=8E=E6=A5=BC=E5=B1=82=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/map/ThreeMap.vue | 69 ++++++----- src/components/navigation/GuideMapShell.vue | 49 +++++++- .../navigation/RoutePlannerPanel.vue | 78 +++++++++++- .../navigation/RoutePointPicker.vue | 6 +- src/pages/index/index.vue | 116 ++++++++++++++---- 5 files changed, 252 insertions(+), 66 deletions(-) diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index a633500..a072534 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -1158,7 +1158,7 @@ const getMultiFloorVerticalGap = (items: MultiFloorModelItem[]) => { const maxFloorHeight = Math.max(...items.map((item) => item.size.y), 1) const maxFootprint = Math.max(...items.map((item) => Math.max(item.size.x, item.size.z)), 1) - return Math.max(maxFloorHeight * 3.2, maxFootprint * 0.2, 42) + return Math.max(maxFloorHeight * 5.2, maxFootprint * 0.32, 68) } const applyMultiFloorLayout = (items: MultiFloorModelItem[]) => { @@ -1332,6 +1332,42 @@ const setCameraPreset = (preset: CameraPreset) => { } } +const zoomCamera = (direction: 'in' | 'out') => { + if (!camera || !controls) return + + const offset = camera.position.clone().sub(controls.target) + const currentDistance = offset.length() + const zoomFactor = direction === 'in' ? 0.72 : 1.28 + const minDistance = controls.minDistance || 2 + const maxDistance = controls.maxDistance || 1200 + const nextDistance = Math.min(maxDistance, Math.max(minDistance, currentDistance * zoomFactor)) + + if (!Number.isFinite(nextDistance) || nextDistance <= 0) return + + offset.setLength(nextDistance) + camera.position.copy(controls.target).add(offset) + controls.update() + + if (direction === 'in' && activeView.value === 'overview' && !isLoading.value && !isAutoSwitchLocked) { + isAutoSwitchLocked = true + lastAutoSwitchTime = Date.now() + + void runAutoSwitchLoad( + { + from: 'overview', + to: 'floor', + trigger: 'zoom-in', + distance: nextDistance + }, + () => loadFloor(currentFloor.value || props.initialFloorId || 'L1'), + '单楼层模型加载失败' + ) + return + } + + handleControlChange() +} + const loadOverview = async () => { const packageData = renderPackage.value if (!packageData || !scene) return @@ -1447,36 +1483,6 @@ const loadMultiFloor = async () => { activeView.value = 'multi' clearSceneData() setProgress(18, '正在加载多层展示模型...') - const packageData = renderPackage.value - const allFloorsShareOverview = Boolean(packageData) - && floorIndex.value.every((floor) => floor.sharedModelAsset && floor.modelUrl === packageData!.overviewModelUrl) - - if (allFloorsShareOverview && packageData) { - if (!attachCachedSharedModel(packageData.overviewModelUrl, 'GuideMultiFloorModel', loadToken)) { - const gltf = await loadModel(packageData.overviewModelUrl, '正在加载多层展示模型', loadToken) - assertCurrentModelLoad(loadToken, gltf.scene) - - const targetScene = scene - if (!targetScene) { - disposeObject(gltf.scene) - throw createStaleModelLoadError() - } - - activeModel = gltf.scene - activeModel.name = 'GuideMultiFloorModel' - prepareModel(activeModel) - applyModelVisibilityForView(activeModel, 'multi') - cachedOverviewModel = activeModel - cachedSharedModelUrl = packageData.overviewModelUrl - targetScene.add(activeModel) - } - - if (!activeModel) throw createStaleModelLoadError() - fitCameraToObject(activeModel) - refreshPoiVisibilityByDistance() - renderRoutePreview() - return - } const group = new THREE.Group() group.name = 'GuideMultiFloorModel' @@ -2310,6 +2316,7 @@ defineExpose({ showMultiFloor, resetCamera, setCameraPreset, + zoomCamera, focusTargetPoi: (request: TargetPoiFocusRequest) => { queueTargetFocus(request) }, diff --git a/src/components/navigation/GuideMapShell.vue b/src/components/navigation/GuideMapShell.vue index fecdf80..2d3b975 100644 --- a/src/components/navigation/GuideMapShell.vue +++ b/src/components/navigation/GuideMapShell.vue @@ -29,6 +29,8 @@ :target-focus-distance-factor="targetFocusDistanceFactor" :route-preview="routePreview" :show-route="showRoute" + :auto-switch-threshold-low="autoSwitchThresholdLow" + :auto-switch-threshold-high="autoSwitchThresholdHigh" @floor-change="handleThreeFloorChange" @poi-click="handlePoiClick" @selection-clear="handleSelectionClear" @@ -138,7 +140,12 @@ :class="`side-${floorSide}`" :style="floorSwitcherStyle" > - + 楼层 @@ -146,7 +153,7 @@ v-for="floor in floorLabels" :key="floor" class="floor-item" - :class="{ active: activeFloor === floor }" + :class="{ active: activeFloor === floor && layerMode !== 'multi' }" @tap="handleFloorChange(floor)" > {{ floor }} @@ -242,6 +249,7 @@ const props = withDefaults(defineProps<{ layerMode?: LayerDisplayMode searchTop?: string floorTop?: string + floorMaxHeight?: string layerModeToggleTop?: string toolsTop?: string tools?: string[] @@ -275,6 +283,8 @@ const props = withDefaults(defineProps<{ targetFocusDistanceFactor?: number routePreview?: GuideRouteResult | null showRoute?: boolean + autoSwitchThresholdLow?: number + autoSwitchThresholdHigh?: number }>(), { searchText: '请输入地点进行搜索', activeMode: '3d', @@ -284,6 +294,7 @@ const props = withDefaults(defineProps<{ layerMode: 'single', searchTop: '16px', floorTop: '164px', + floorMaxHeight: '', layerModeToggleTop: '154px', toolsTop: '406px', tools: () => [] as string[], @@ -316,7 +327,9 @@ const props = withDefaults(defineProps<{ touchGestureMode: 'orbit', targetFocusDistanceFactor: 0.22, routePreview: null, - showRoute: false + showRoute: false, + autoSwitchThresholdLow: 1.0, + autoSwitchThresholdHigh: 1.3 }) const emit = defineEmits<{ @@ -339,6 +352,7 @@ const indoorRendererRef = ref<{ showMultiFloor?: () => Promise | void resetCamera?: () => void setCameraPreset?: (preset: 'top' | 'oblique') => void + zoomCamera?: (direction: 'in' | 'out') => void clearSelection?: (shouldEmit?: boolean) => void disableAutoSwitchTemporarily?: (durationMs: number) => void } | null>(null) @@ -359,7 +373,8 @@ const searchFieldStyle = computed(() => ({ })) const floorSwitcherStyle = computed(() => ({ - top: props.floorTop + top: props.floorTop, + ...(props.floorMaxHeight ? { maxHeight: props.floorMaxHeight } : {}) })) const layerModeToggleStyle = computed(() => ({ @@ -432,6 +447,10 @@ const handleLayerModeToggle = () => { handleLayerModeChange(nextLayerMode.value) } +const handleFloorHeaderTap = () => { + handleLayerModeChange('multi') +} + const handleToolClick = (tool: string) => { if (props.mapType === 'indoor') { indoorRendererRef.value?.disableAutoSwitchTemporarily?.(10000) @@ -456,6 +475,7 @@ const handleResetView = () => { } const handleZoomClick = (direction: 'in' | 'out') => { + indoorRendererRef.value?.zoomCamera?.(direction) emit('toolClick', direction === 'in' ? '放大' : '缩小') } @@ -797,7 +817,9 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | max-height: calc(100vh - 260px); padding: 1px; box-sizing: border-box; - overflow: visible; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: none; background: rgba(255, 255, 255, 0.84); border: 1px solid #dde5df; border-radius: 8px; @@ -805,6 +827,10 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | z-index: 35; } +.floor-switcher::-webkit-scrollbar { + display: none; +} + .floor-switcher.side-right { right: 16px; } @@ -814,6 +840,7 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | } .floor-header { + position: relative; min-height: 50px; padding: 7px 4px 6px; display: flex; @@ -826,6 +853,10 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | border-radius: 8px 8px 0 0; } +.floor-header.active { + background: #000000; +} + .floor-header::after { content: ''; position: absolute; @@ -842,12 +873,20 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | color: #151713; } +.floor-header.active .floor-header-icon { + color: var(--museum-accent); +} + .floor-header-label { font-size: 11px; line-height: 14px; color: #545861; } +.floor-header.active .floor-header-label { + color: var(--museum-accent); +} + .layer-mode-toggle { position: absolute; right: 16px; diff --git a/src/components/navigation/RoutePlannerPanel.vue b/src/components/navigation/RoutePlannerPanel.vue index 1979010..15b9f7b 100644 --- a/src/components/navigation/RoutePlannerPanel.vue +++ b/src/components/navigation/RoutePlannerPanel.vue @@ -1,5 +1,15 @@ - - + + 线路预览 @@ -122,14 +134,6 @@ 推荐入口 入口参考 - - 室内3D - 位置预览 - - - {{ indoorLayerMode === 'multi' ? '单层' : '多层' }} - 切换楼层 - @@ -153,6 +157,7 @@ @clear="handleRouteClear" @view-route="handleRouteView" @simulate-guide="handleRouteSimulate" + @back="handleRouteBack" /> @@ -262,7 +267,7 @@ const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide' // 3D 模式状态 const is3DMode = ref(true) const guideOutdoorState = ref<'home' | 'entrance'>('home') -const indoorView = ref('floor') +const indoorView = ref('overview') const activeGuideFloor = ref('1F') const selectedGuidePoi = ref(null) @@ -281,6 +286,8 @@ const activeRoutePreview = ref(null) const showGuideMoreMenu = ref(false) const isSimulatingRoute = ref(false) const isPoiCardCollapsed = ref(false) +const guideMoreTouchStartY = ref(0) +const guideMoreTouchCurrentY = ref(0) const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl() const indoorModelSource = guideUseCase.getModelSource() @@ -530,8 +537,9 @@ const buildExplainHallItems = ( } const handleGuideSearchTap = () => { - uni.navigateTo({ - url: '/pages/search/index' + uni.showToast({ + title: '功能待开放', + icon: 'none' }) } @@ -707,6 +715,19 @@ const handleRouteClear = () => { clearRoutePreviewState() } +const handleRouteBack = () => { + if (activeRoutePreview.value) { + activeRoutePreview.value = null + routePlanError.value = '' + isSimulatingRoute.value = false + showRoutePlanner.value = true + return + } + + showRoutePlanner.value = false + isSimulatingRoute.value = false +} + const handleRouteStartPointChange = (point: RoutePointOption | null) => { routeStartPoint.value = point clearRoutePreviewState() @@ -789,7 +810,7 @@ const handleRouteSimulate = () => { // 进入 3D 室内模式 const handleEnter3DMode = () => { console.log('进入 3D 室内模式') - indoorView.value = 'floor' + indoorView.value = 'overview' is3DMode.value = true guideOutdoorState.value = 'home' showGuideMoreMenu.value = false @@ -811,7 +832,7 @@ const handleModeChange = (mode: '2d' | '3d') => { isSimulatingRoute.value = false clearRoutePreviewState() } else { - indoorView.value = 'floor' + indoorView.value = 'overview' if (!routePointOptions.value.length) { void loadRouteTargets() } @@ -841,6 +862,54 @@ const handleGuideMoreClick = () => { selectedGuidePoi.value = null } +const closeGuideMoreMenu = () => { + showGuideMoreMenu.value = false +} + +const getGestureClientY = (event: TouchEvent | MouseEvent) => { + if ('changedTouches' in event) { + return event.changedTouches?.[0]?.clientY + ?? event.touches?.[0]?.clientY + ?? 0 + } + + return event.clientY +} + +const handleGuideMoreTouchStart = (event: TouchEvent) => { + const clientY = getGestureClientY(event) + guideMoreTouchStartY.value = clientY + guideMoreTouchCurrentY.value = clientY +} + +const handleGuideMoreTouchMove = (event: TouchEvent) => { + guideMoreTouchCurrentY.value = getGestureClientY(event) +} + +const handleGuideMoreTouchEnd = (event: TouchEvent) => { + guideMoreTouchCurrentY.value = getGestureClientY(event) + if (guideMoreTouchCurrentY.value - guideMoreTouchStartY.value > 48) { + closeGuideMoreMenu() + } +} + +const handleGuideMoreMouseStart = (event: MouseEvent) => { + const clientY = getGestureClientY(event) + guideMoreTouchStartY.value = clientY + guideMoreTouchCurrentY.value = clientY +} + +const handleGuideMoreMouseMove = (event: MouseEvent) => { + guideMoreTouchCurrentY.value = getGestureClientY(event) +} + +const handleGuideMoreMouseEnd = (event: MouseEvent) => { + guideMoreTouchCurrentY.value = getGestureClientY(event) + if (guideMoreTouchCurrentY.value - guideMoreTouchStartY.value > 48) { + closeGuideMoreMenu() + } +} + const handleMoreRouteGuide = () => { openRoutePlanner() } @@ -864,13 +933,6 @@ const handleMoreRecommendedEntrance = () => { openOutdoorReference('entrance') } -const handleMoreToggleLayer = () => { - is3DMode.value = true - guideOutdoorState.value = 'home' - indoorView.value = indoorLayerMode.value === 'multi' ? 'floor' : 'multi' - showGuideMoreMenu.value = false -} - const guideShellMode = computed(() => (is3DMode.value ? '3d' : '2d')) const guideMapType = computed(() => (is3DMode.value ? 'indoor' : 'outdoor')) const guideOutdoorVariant = computed(() => (