修复地图起点确认弹窗闪退
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-28 20:16:28 +08:00
parent 93bfb7e778
commit 0380a4f875
3 changed files with 73 additions and 7 deletions

View File

@@ -833,6 +833,7 @@ const emitFloorChange = (floorId: string, sceneRevision = props.sceneRevision) =
} }
const currentFloor = ref(props.initialFloorId) const currentFloor = ref(props.initialFloorId)
const selectedPOI = ref<RenderPoi | null>(null) const selectedPOI = ref<RenderPoi | null>(null)
let routeStartSelectionGeneration = 0
const activeFocusPoiId = ref('') const activeFocusPoiId = ref('')
const floorIndex = ref<FloorIndexItem[]>([]) const floorIndex = ref<FloorIndexItem[]>([])
const renderPackage = ref<GuideModelRenderPackage | null>(null) const renderPackage = ref<GuideModelRenderPackage | null>(null)
@@ -8157,6 +8158,14 @@ const selectRouteStartCandidateNear = (
const handleSceneTap = async (event: PointerEvent) => { const handleSceneTap = async (event: PointerEvent) => {
if (!camera || !renderer || !getContainerElement()) return if (!camera || !renderer || !getContainerElement()) return
const routeStartSelectionActiveAtTap = props.routeStartSelectionActive
const routeStartSelectionGenerationAtTap = routeStartSelectionGeneration
const canResolveRouteStartForTap = () => (
routeStartSelectionActiveAtTap
&& props.routeStartSelectionActive
&& routeStartSelectionGeneration === routeStartSelectionGenerationAtTap
)
const rect = renderer.domElement.getBoundingClientRect() const rect = renderer.domElement.getBoundingClientRect()
const pointer = new THREE.Vector2( const pointer = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1, ((event.clientX - rect.left) / rect.width) * 2 - 1,
@@ -8215,7 +8224,8 @@ const handleSceneTap = async (event: PointerEvent) => {
} }
const poi = findLoadedPoi(selectedPoi.id) || selectedPoi const poi = findLoadedPoi(selectedPoi.id) || selectedPoi
if (props.routeStartSelectionActive) { if (routeStartSelectionActiveAtTap) {
if (!canResolveRouteStartForTap()) return
if (poi.positionGltf) { if (poi.positionGltf) {
selectRouteStartCandidateNear( selectRouteStartCandidateNear(
poi.floorId, poi.floorId,
@@ -8248,7 +8258,8 @@ const handleSceneTap = async (event: PointerEvent) => {
: [] : []
const matchedModelPoi = findPoiForModelHits(modelHits) const matchedModelPoi = findPoiForModelHits(modelHits)
if (props.routeStartSelectionActive) { if (routeStartSelectionActiveAtTap) {
if (!canResolveRouteStartForTap()) return
if (matchedModelPoi?.poi.positionGltf) { if (matchedModelPoi?.poi.positionGltf) {
selectRouteStartCandidateNear( selectRouteStartCandidateNear(
matchedModelPoi.poi.floorId, matchedModelPoi.poi.floorId,
@@ -9426,11 +9437,13 @@ watch(() => props.visiblePoiIds, () => {
}) })
watch(() => props.routeStartSelectionActive, (isSelectingStart) => { watch(() => props.routeStartSelectionActive, (isSelectingStart) => {
routeStartSelectionGeneration += 1
if (isSelectingStart) { if (isSelectingStart) {
clearMapSelection(false) clearMapSelection(false)
} }
}, { }, {
immediate: true immediate: true,
flush: 'sync'
}) })
watch(() => [props.routePreview, props.showRoute, props.routeNavigationActive], () => { watch(() => [props.routePreview, props.showRoute, props.routeNavigationActive], () => {

View File

@@ -38,15 +38,15 @@
</view> </view>
</view> </view>
<view v-if="startCandidate" class="route-confirm-mask" @tap.stop="emit('cancelStart')"> <view v-if="startCandidate" class="route-confirm-mask" @tap.stop="handleCancelStart">
<view class="route-confirm-card" @tap.stop> <view class="route-confirm-card" @tap.stop>
<text class="route-confirm-title">确认选择</text> <text class="route-confirm-title">确认选择</text>
<text class="route-confirm-message">{{ normalizeVisitorPoiDisplayName(startCandidate.name) }} 为起点吗</text> <text class="route-confirm-message">{{ normalizeVisitorPoiDisplayName(startCandidate.name) }} 为起点吗</text>
<view class="route-confirm-actions"> <view class="route-confirm-actions">
<view class="route-confirm-action secondary" @tap="emit('cancelStart')"> <view class="route-confirm-action secondary" @tap.stop="handleCancelStart">
<text class="route-confirm-action-text">取消</text> <text class="route-confirm-action-text">取消</text>
</view> </view>
<view class="route-confirm-action primary" @tap="emit('confirmStart', startCandidate)"> <view class="route-confirm-action primary" @tap.stop="handleConfirmStart">
<text class="route-confirm-action-text">确定</text> <text class="route-confirm-action-text">确定</text>
</view> </view>
</view> </view>
@@ -56,7 +56,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, onBeforeUnmount, ref, watch } from 'vue'
import type { RoutePointOption } from '@/components/navigation/RoutePointPicker.vue' import type { RoutePointOption } from '@/components/navigation/RoutePointPicker.vue'
import { normalizeVisitorPoiDisplayName } from '@/view-models/visitorPoiPresentation' import { normalizeVisitorPoiDisplayName } from '@/view-models/visitorPoiPresentation'
@@ -87,6 +87,42 @@ const emit = defineEmits<{
back: [] back: []
}>() }>()
const confirmationInteractive = ref(false)
let confirmationInteractiveTimer: ReturnType<typeof window.setTimeout> | null = null
const confirmationInteractionGuardMs = 350
const clearConfirmationInteractiveTimer = () => {
if (confirmationInteractiveTimer === null) return
window.clearTimeout(confirmationInteractiveTimer)
confirmationInteractiveTimer = null
}
watch(() => props.startCandidate, (candidate) => {
clearConfirmationInteractiveTimer()
confirmationInteractive.value = false
if (!candidate) return
confirmationInteractiveTimer = window.setTimeout(() => {
confirmationInteractiveTimer = null
confirmationInteractive.value = Boolean(props.startCandidate)
}, confirmationInteractionGuardMs)
}, {
immediate: true,
flush: 'sync'
})
onBeforeUnmount(clearConfirmationInteractiveTimer)
const handleCancelStart = () => {
if (!confirmationInteractive.value) return
emit('cancelStart')
}
const handleConfirmStart = () => {
if (!confirmationInteractive.value || !props.startCandidate) return
emit('confirmStart', props.startCandidate)
}
const pointMeta = (point: RoutePointOption) => ( const pointMeta = (point: RoutePointOption) => (
point.categoryLabel ? `${point.floorLabel} · ${point.categoryLabel}` : point.floorLabel point.categoryLabel ? `${point.floorLabel} · ${point.categoryLabel}` : point.floorLabel
) )

View File

@@ -13,6 +13,23 @@ const point = (name: string, floorLabel: string) => ({
}) })
describe('RoutePlannerPanel', () => { describe('RoutePlannerPanel', () => {
it('忽略打开确认层的同一次点击,再响应后续确认或取消操作', async () => {
const wrapper = mount(RoutePlannerPanel, {
props: {
startCandidate: point('服务台', '1F')
}
})
await wrapper.get('.route-confirm-mask').trigger('tap')
await wrapper.get('.route-confirm-action.primary').trigger('tap')
expect(wrapper.emitted('cancelStart')).toBeUndefined()
expect(wrapper.emitted('confirmStart')).toBeUndefined()
await new Promise<void>((resolve) => window.setTimeout(resolve, 350))
await wrapper.get('.route-confirm-action.primary').trigger('tap')
expect(wrapper.emitted('confirmStart')).toEqual([[expect.objectContaining({ poiId: '服务台' })]])
})
it('在导航副标题中显示起点和终点楼层', () => { it('在导航副标题中显示起点和终点楼层', () => {
const wrapper = mount(RoutePlannerPanel, { const wrapper = mount(RoutePlannerPanel, {
props: { props: {