@@ -255,12 +255,14 @@ const props = withDefaults(defineProps<{
|
|||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
currentFloorId?: string
|
currentFloorId?: string
|
||||||
currentFloorLabel?: string
|
currentFloorLabel?: string
|
||||||
|
locationPreviewSelection?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
initialKeyword: '',
|
initialKeyword: '',
|
||||||
variant: 'page',
|
variant: 'page',
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
currentFloorId: '',
|
currentFloorId: '',
|
||||||
currentFloorLabel: ''
|
currentFloorLabel: '',
|
||||||
|
locationPreviewSelection: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -913,7 +915,7 @@ const handleResultTap = (poi: MuseumPoi) => {
|
|||||||
// H5 普通 view 的滚动事件并非始终派发,进入详情前直接读取真实位置。
|
// H5 普通 view 的滚动事件并非始终派发,进入详情前直接读取真实位置。
|
||||||
captureResultListScroll()
|
captureResultListScroll()
|
||||||
const context = createSearchContext()
|
const context = createSearchContext()
|
||||||
if (props.variant === 'home') {
|
if (props.variant === 'home' || props.locationPreviewSelection) {
|
||||||
emit('resultTap', poi, context)
|
emit('resultTap', poi, context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
>
|
>
|
||||||
<view class="explain-detail-page">
|
<view class="explain-detail-page">
|
||||||
|
<view v-if="detailState !== 'ready'" class="detail-page-state" :class="`is-${detailState}`" data-testid="exhibit-detail-state">
|
||||||
|
<text class="detail-page-state-title">{{ detailState === 'loading' ? '正在加载讲解对象' : detailState === 'missing' ? '缺少讲解对象参数' : '讲解对象加载失败' }}</text>
|
||||||
|
<text class="detail-page-state-desc">{{ detailState === 'loading' ? '请稍候' : detailStateMessage }}</text>
|
||||||
|
<view v-if="detailState !== 'loading'" class="detail-page-state-actions">
|
||||||
|
<button v-if="detailState === 'error'" class="detail-page-state-button" @tap="retryDetail">重新加载</button>
|
||||||
|
<button class="detail-page-state-button secondary" @tap="handleBack">返回讲解列表</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
<view class="immersive-hero" :class="{ 'has-real-image': heroImage }">
|
<view class="immersive-hero" :class="{ 'has-real-image': heroImage }">
|
||||||
<image
|
<image
|
||||||
v-if="heroImage"
|
v-if="heroImage"
|
||||||
@@ -129,6 +138,7 @@
|
|||||||
<text class="location-text">查看位置</text>
|
<text class="location-text">查看位置</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</GuidePageFrame>
|
</GuidePageFrame>
|
||||||
</template>
|
</template>
|
||||||
@@ -183,6 +193,8 @@ const defaultDetail: ExplainDetailPageViewModel = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exhibit = ref<ExplainDetailPageViewModel>(defaultDetail)
|
const exhibit = ref<ExplainDetailPageViewModel>(defaultDetail)
|
||||||
|
const detailState = ref<'idle' | 'loading' | 'ready' | 'missing' | 'error'>('idle')
|
||||||
|
const detailStateMessage = ref('请检查链接后重试。')
|
||||||
const globalAudioPlayer = useGlobalAudioPlayer()
|
const globalAudioPlayer = useGlobalAudioPlayer()
|
||||||
const activeTopTab = ref<GuideTopTab>('explain')
|
const activeTopTab = ref<GuideTopTab>('explain')
|
||||||
const resolvedHallId = ref('')
|
const resolvedHallId = ref('')
|
||||||
@@ -467,6 +479,7 @@ const loadExplainDetail = async (
|
|||||||
request: NonNullable<typeof detailEntryRequest.value>,
|
request: NonNullable<typeof detailEntryRequest.value>,
|
||||||
lang: AudioLanguage
|
lang: AudioLanguage
|
||||||
) => {
|
) => {
|
||||||
|
detailState.value = 'loading'
|
||||||
const exhibitData = await explainUseCase.enterExplainDetail({
|
const exhibitData = await explainUseCase.enterExplainDetail({
|
||||||
...request,
|
...request,
|
||||||
lang
|
lang
|
||||||
@@ -487,6 +500,7 @@ const loadExplainDetail = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loadFullDetailTextForLanguage(request, resolvedLanguage, exhibit.value)
|
void loadFullDetailTextForLanguage(request, resolvedLanguage, exhibit.value)
|
||||||
|
detailState.value = 'ready'
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(async (options: any = {}) => {
|
onLoad(async (options: any = {}) => {
|
||||||
@@ -497,7 +511,10 @@ onLoad(async (options: any = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exhibitId = Array.isArray(options.id) ? options.id[0] : options.id
|
const exhibitId = Array.isArray(options.id) ? options.id[0] : options.id
|
||||||
if (!exhibitId) return
|
if (!exhibitId) {
|
||||||
|
detailState.value = 'missing'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rawTargetType = Array.isArray(options.targetType) ? options.targetType[0] : options.targetType
|
const rawTargetType = Array.isArray(options.targetType) ? options.targetType[0] : options.targetType
|
||||||
const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM'
|
const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM'
|
||||||
@@ -527,13 +544,21 @@ onLoad(async (options: any = {}) => {
|
|||||||
await loadExplainDetail(detailEntryRequest.value, lang)
|
await loadExplainDetail(detailEntryRequest.value, lang)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('讲解详情加载失败:', error)
|
console.error('讲解详情加载失败:', error)
|
||||||
uni.showToast({
|
detailState.value = 'error'
|
||||||
title: '讲解详情加载失败,请稍后重试',
|
detailStateMessage.value = error instanceof Error ? error.message : '请检查网络后重试。'
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const retryDetail = async () => {
|
||||||
|
if (!detailEntryRequest.value || detailState.value === 'loading') return
|
||||||
|
try {
|
||||||
|
await loadExplainDetail(detailEntryRequest.value, selectedAudioLanguage.value)
|
||||||
|
} catch (error) {
|
||||||
|
detailState.value = 'error'
|
||||||
|
detailStateMessage.value = error instanceof Error ? error.message : '请检查网络后重试。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// iOS H5 may hide a uni page before its route is fully unloaded.
|
// iOS H5 may hide a uni page before its route is fully unloaded.
|
||||||
onHide(closeDetailAudioOnExit)
|
onHide(closeDetailAudioOnExit)
|
||||||
onUnload(closeDetailAudioOnExit)
|
onUnload(closeDetailAudioOnExit)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
:loading="explainLoading"
|
:loading="explainLoading"
|
||||||
:error="explainError"
|
:error="explainError"
|
||||||
@hall-click="handleExplainHallClick"
|
@hall-click="handleExplainHallClick"
|
||||||
|
@retry="loadExplainHalls"
|
||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -396,6 +396,13 @@ onLoad(async (options: Record<string, unknown> = {}) => {
|
|||||||
|
|
||||||
const handleFloorChange = (floor: string) => {
|
const handleFloorChange = (floor: string) => {
|
||||||
activeFloor.value = floor
|
activeFloor.value = floor
|
||||||
|
const currentFloorId = normalizeGuideFloorId(floor)
|
||||||
|
if (requestedDetailFloorId.value && currentFloorId !== requestedDetailFloorId.value) {
|
||||||
|
// The selected target stays pinned to its real floor; do not leave a focus
|
||||||
|
// marker on a different committed renderer floor.
|
||||||
|
targetFocusRequest.value = null
|
||||||
|
locationErrorMessage.value = `目标位于 ${facility.value.floor},当前浏览 ${floor}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
:loading="explainLoading"
|
:loading="explainLoading"
|
||||||
:error="explainError"
|
:error="explainError"
|
||||||
@hall-click="handleExplainHallClick"
|
@hall-click="handleExplainHallClick"
|
||||||
|
@retry="loadExplainHalls"
|
||||||
@back="handleExplainBack"
|
@back="handleExplainBack"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -1202,9 +1203,9 @@ const navigateFromSelectedPoiToDetail = (url: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigateToHallDetail = (hallId: string) => {
|
const navigateToHallExplainObjects = (hallId: string, hallName = '讲解') => {
|
||||||
navigateFromSelectedPoiToDetail(
|
navigateFromSelectedPoiToDetail(
|
||||||
`/pages/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
|
explainGuideStopListUrl(hallId, hallName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1218,7 +1219,7 @@ const handleViewSelectedHall = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToHallDetail(hall.id)
|
navigateToHallExplainObjects(hall.id, hall.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectedPoiRelated = async () => {
|
const handleSelectedPoiRelated = async () => {
|
||||||
@@ -1226,24 +1227,16 @@ const handleSelectedPoiRelated = async () => {
|
|||||||
|
|
||||||
const hall = await resolveSelectedPoiHall()
|
const hall = await resolveSelectedPoiHall()
|
||||||
if (hall) {
|
if (hall) {
|
||||||
navigateToHallDetail(hall.id)
|
navigateToHallExplainObjects(hall.id, hall.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyword = selectedGuidePoi.value.hallName || selectedGuidePoi.value.name
|
const keyword = selectedGuidePoi.value.hallName || selectedGuidePoi.value.name
|
||||||
const results = await explainUseCase.searchExplain(keyword)
|
const results = await explainUseCase.searchExplain(keyword)
|
||||||
const firstHall = results.find((item) => item.type === 'hall' && item.hallId)
|
const firstHall = results.find((item) => item.type === 'hall' && item.hallId)
|
||||||
const firstExhibit = results.find((item) => item.type === 'exhibit' && item.exhibitId)
|
|
||||||
|
|
||||||
if (firstHall?.hallId) {
|
if (firstHall?.hallId) {
|
||||||
navigateToHallDetail(firstHall.hallId)
|
navigateToHallExplainObjects(firstHall.hallId, selectedGuidePoi.value.hallName || '讲解')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstExhibit?.exhibitId) {
|
|
||||||
navigateFromSelectedPoiToDetail(
|
|
||||||
`/pages/exhibit/detail?id=${encodeURIComponent(firstExhibit.exhibitId)}&tab=explain`
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,9 +207,13 @@ import {
|
|||||||
} from '@/usecases/guideRouteUseCase'
|
} from '@/usecases/guideRouteUseCase'
|
||||||
import {
|
import {
|
||||||
navigateToGuideTopTab,
|
navigateToGuideTopTab,
|
||||||
|
clearLastGuideLocationPreviewUrl,
|
||||||
saveLastGuideLocationPreviewUrl,
|
saveLastGuideLocationPreviewUrl,
|
||||||
type GuideTopTab
|
type GuideTopTab
|
||||||
} from '@/utils/guideTopTabs'
|
} from '@/utils/guideTopTabs'
|
||||||
|
import {
|
||||||
|
locationPreviewSearchUrl
|
||||||
|
} from '@/utils/locationPreviewSelection'
|
||||||
|
|
||||||
type RouteViewState = 'preview' | 'location-error' | 'outdoor-preview'
|
type RouteViewState = 'preview' | 'location-error' | 'outdoor-preview'
|
||||||
type GuideMode = '2d' | '3d'
|
type GuideMode = '2d' | '3d'
|
||||||
@@ -503,8 +507,10 @@ const requestTargetFocus = (target: Partial<RouteTargetLocation> | null, fallbac
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||||
|
const requestSequence = ++routeOptionsSequence
|
||||||
await loadGuideFloors()
|
await loadGuideFloors()
|
||||||
await refreshRouteReadinessState()
|
await refreshRouteReadinessState()
|
||||||
|
if (requestSequence !== routeOptionsSequence) return false
|
||||||
|
|
||||||
const facilityId = normalizeStringOption(options.facilityId)
|
const facilityId = normalizeStringOption(options.facilityId)
|
||||||
const target = normalizeStringOption(options.target)
|
const target = normalizeStringOption(options.target)
|
||||||
@@ -512,6 +518,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
|||||||
const startLocation = createStartLocationFromOptions(options)
|
const startLocation = createStartLocationFromOptions(options)
|
||||||
|
|
||||||
if (!facilityId && !target) {
|
if (!facilityId && !target) {
|
||||||
|
clearLastGuideLocationPreviewUrl()
|
||||||
hasRouteTarget.value = false
|
hasRouteTarget.value = false
|
||||||
route.value = initialLocationPreviewPlan()
|
route.value = initialLocationPreviewPlan()
|
||||||
routeViewState.value = 'preview'
|
routeViewState.value = 'preview'
|
||||||
@@ -524,6 +531,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null
|
const matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null
|
||||||
|
if (requestSequence !== routeOptionsSequence) return false
|
||||||
|
|
||||||
if (!matchedPoi) {
|
if (!matchedPoi) {
|
||||||
hasRouteTarget.value = false
|
hasRouteTarget.value = false
|
||||||
@@ -560,6 +568,8 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let routeOptionsSequence = 0
|
||||||
|
|
||||||
onLoad((options: any) => {
|
onLoad((options: any) => {
|
||||||
setLocationPreviewTitle()
|
setLocationPreviewTitle()
|
||||||
void applyRouteOptions(options)
|
void applyRouteOptions(options)
|
||||||
@@ -585,7 +595,6 @@ const syncH5RouteOptions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
syncH5RouteOptions()
|
|
||||||
window.addEventListener('hashchange', syncH5RouteOptions)
|
window.addEventListener('hashchange', syncH5RouteOptions)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -596,10 +605,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
const handleSelectTarget = () => {
|
const handleSelectTarget = () => {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: '/pages/search/index',
|
url: locationPreviewSearchUrl(),
|
||||||
fail: () => {
|
fail: () => {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/search/index'
|
url: locationPreviewSearchUrl()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -874,6 +883,7 @@ const handleTopTabChange = (tab: GuideTopTab) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePageBack = () => {
|
const handlePageBack = () => {
|
||||||
|
clearLastGuideLocationPreviewUrl()
|
||||||
if (isManualLocationPanelOpen.value) {
|
if (isManualLocationPanelOpen.value) {
|
||||||
handleManualLocationCancel()
|
handleManualLocationCancel()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
:initial-keyword="initialKeyword"
|
:initial-keyword="initialKeyword"
|
||||||
variant="page"
|
variant="page"
|
||||||
autofocus
|
autofocus
|
||||||
|
:location-preview-selection="isLocationPreviewSelection"
|
||||||
|
@result-tap="handleLocationPreviewResult"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -26,8 +28,15 @@ import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
|||||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
|
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
|
||||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||||
|
import {
|
||||||
|
isLocationPreviewSelectionMode,
|
||||||
|
locationPreviewDetailUrl
|
||||||
|
} from '@/utils/locationPreviewSelection'
|
||||||
|
import type { MuseumPoi } from '@/domain/museum'
|
||||||
|
import type { PoiSearchContext } from '@/domain/poiSearch'
|
||||||
|
|
||||||
const initialKeyword = ref('')
|
const initialKeyword = ref('')
|
||||||
|
const isLocationPreviewSelection = ref(false)
|
||||||
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
||||||
const searchPanelRef = ref<{
|
const searchPanelRef = ref<{
|
||||||
restoreResultListScroll?: () => Promise<void>
|
restoreResultListScroll?: () => Promise<void>
|
||||||
@@ -39,6 +48,7 @@ onLoad((options: any) => {
|
|||||||
: ''
|
: ''
|
||||||
|
|
||||||
initialKeyword.value = keyword
|
initialKeyword.value = keyword
|
||||||
|
isLocationPreviewSelection.value = isLocationPreviewSelectionMode(options.selection)
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
@@ -47,11 +57,23 @@ onShow(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleBackToMap = () => {
|
const handleBackToMap = () => {
|
||||||
|
if (isLocationPreviewSelection.value) {
|
||||||
|
uni.redirectTo({ url: '/pages/route/detail' })
|
||||||
|
return
|
||||||
|
}
|
||||||
uni.reLaunch({
|
uni.reLaunch({
|
||||||
url: '/pages/index/index?tab=guide'
|
url: '/pages/index/index?tab=guide'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLocationPreviewResult = (poi: MuseumPoi, _context: PoiSearchContext) => {
|
||||||
|
if (!isLocationPreviewSelection.value) return
|
||||||
|
uni.redirectTo({
|
||||||
|
url: locationPreviewDetailUrl(poi.id, poi.name),
|
||||||
|
fail: () => uni.navigateTo({ url: locationPreviewDetailUrl(poi.id, poi.name) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
let removeSearchViewportResizeListener: (() => void) | null = null
|
let removeSearchViewportResizeListener: (() => void) | null = null
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type {
|
|||||||
GuideRouteTarget
|
GuideRouteTarget
|
||||||
} from '@/domain/museum'
|
} from '@/domain/museum'
|
||||||
import {
|
import {
|
||||||
NAV_ROUTE_READINESS
|
NAV_ROUTE_READINESS,
|
||||||
|
applyRouteReadinessGate
|
||||||
} from '@/domain/guideReadiness'
|
} from '@/domain/guideReadiness'
|
||||||
import {
|
import {
|
||||||
isIndoorNavigableFloor
|
isIndoorNavigableFloor
|
||||||
@@ -90,12 +91,17 @@ export class SdkGuideRouteRepository {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getRouteReadiness(): Promise<GuideRouteReadiness> {
|
async getRouteReadiness(): Promise<GuideRouteReadiness> {
|
||||||
|
// This is a product gate, not an SDK capability check. SDK diagnostics must
|
||||||
|
// never enable navigation while the verified graph package is unavailable.
|
||||||
|
const hardGate = applyRouteReadinessGate(NAV_ROUTE_READINESS)
|
||||||
|
if (!hardGate.ready) return hardGate
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||||
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
const floors = (diagnostics.floors || []).filter(isIndoorNavigableFloor)
|
||||||
|
|
||||||
if (!floors.length) {
|
if (!floors.length) {
|
||||||
return createUnavailableReadiness('SDK 地图楼层数据为空')
|
return applyRouteReadinessGate(createUnavailableReadiness('SDK 地图楼层数据为空'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 地图级 diagnostics 的楼层摘要只保证 routePlanningReady/navigablePlaceCount;
|
// 地图级 diagnostics 的楼层摘要只保证 routePlanningReady/navigablePlaceCount;
|
||||||
@@ -131,7 +137,7 @@ export class SdkGuideRouteRepository {
|
|||||||
navigablePlaceCount: floor.navigablePlaceCount
|
navigablePlaceCount: floor.navigablePlaceCount
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
return createUnavailableReadiness('无法定位,当前仅支持点位位置预览')
|
return applyRouteReadinessGate(createUnavailableReadiness('无法定位,当前仅支持点位位置预览'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有楼层未就绪,给出警告但仍然可用
|
// 如果有楼层未就绪,给出警告但仍然可用
|
||||||
@@ -139,23 +145,23 @@ export class SdkGuideRouteRepository {
|
|||||||
const floorLabels = notReadyFloors
|
const floorLabels = notReadyFloors
|
||||||
.map((f) => f.floorName || f.floorCode || String(f.floorId))
|
.map((f) => f.floorName || f.floorCode || String(f.floorId))
|
||||||
.join('、')
|
.join('、')
|
||||||
return {
|
return applyRouteReadinessGate({
|
||||||
ready: true,
|
ready: true,
|
||||||
message: `部分楼层位置关系数据未就绪:${floorLabels},可查看已就绪楼层的位置关系`,
|
message: `部分楼层位置关系数据未就绪:${floorLabels},可查看已就绪楼层的位置关系`,
|
||||||
requiredData: []
|
requiredData: []
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return applyRouteReadinessGate({
|
||||||
ready: true,
|
ready: true,
|
||||||
message: 'SDK 位置关系数据已接入,可进行位置预览',
|
message: 'SDK 位置关系数据已接入,可进行位置预览',
|
||||||
requiredData: []
|
requiredData: []
|
||||||
}
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SDKRouteReadiness] 读取 SDK 路线状态失败', error)
|
console.error('[SDKRouteReadiness] 读取 SDK 路线状态失败', error)
|
||||||
return createUnavailableReadiness(
|
return applyRouteReadinessGate(createUnavailableReadiness(
|
||||||
error instanceof Error ? error.message : 'SDK 位置关系数据加载失败'
|
error instanceof Error ? error.message : 'SDK 位置关系数据加载失败'
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +185,8 @@ export class SdkGuideRouteRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listRouteTargets(): Promise<GuideRouteTarget[]> {
|
async listRouteTargets(): Promise<GuideRouteTarget[]> {
|
||||||
|
const readiness = await this.getRouteReadiness()
|
||||||
|
if (!readiness.ready) return []
|
||||||
if (this.targetsCache) return this.targetsCache
|
if (this.targetsCache) return this.targetsCache
|
||||||
|
|
||||||
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
const diagnostics = await this.sdkApiProvider.getMapDiagnostics()
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
createGuideRouteRepository
|
createGuideRouteRepository
|
||||||
} from '@/repositories/createGuideRouteRepository'
|
} from '@/repositories/createGuideRouteRepository'
|
||||||
|
import {
|
||||||
|
applyRouteReadinessGate,
|
||||||
|
NAV_ROUTE_READINESS
|
||||||
|
} from '@/domain/guideReadiness'
|
||||||
|
|
||||||
const toSearchText = (target: GuideRouteTarget) => [
|
const toSearchText = (target: GuideRouteTarget) => [
|
||||||
target.name,
|
target.name,
|
||||||
@@ -41,13 +45,15 @@ export class GuideRouteUseCase {
|
|||||||
constructor(private readonly repository: GuideRouteRepository = createGuideRouteRepository()) {}
|
constructor(private readonly repository: GuideRouteRepository = createGuideRouteRepository()) {}
|
||||||
|
|
||||||
async getRouteReadiness() {
|
async getRouteReadiness() {
|
||||||
if (this.readinessCache) return this.readinessCache
|
if (this.readinessCache) return applyRouteReadinessGate(this.readinessCache)
|
||||||
|
|
||||||
this.readinessCache = await this.repository.getRouteReadiness()
|
this.readinessCache = applyRouteReadinessGate(await this.repository.getRouteReadiness())
|
||||||
return this.readinessCache
|
return applyRouteReadinessGate(this.readinessCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTargets() {
|
async listTargets() {
|
||||||
|
const readiness = await this.getRouteReadiness()
|
||||||
|
if (!readiness.ready) return []
|
||||||
if (this.targetsCache) return this.targetsCache
|
if (this.targetsCache) return this.targetsCache
|
||||||
|
|
||||||
this.targetsCache = (await this.repository.listRouteTargets())
|
this.targetsCache = (await this.repository.listRouteTargets())
|
||||||
@@ -69,11 +75,11 @@ export class GuideRouteUseCase {
|
|||||||
|
|
||||||
async planRoute(request: GuideRoutePlanRequest): Promise<GuideRoutePlanResult> {
|
async planRoute(request: GuideRoutePlanRequest): Promise<GuideRoutePlanResult> {
|
||||||
try {
|
try {
|
||||||
const readiness = await this.getRouteReadiness()
|
const readiness = applyRouteReadinessGate(await this.getRouteReadiness())
|
||||||
if (!readiness.ready) {
|
if (!readiness.ready) {
|
||||||
return {
|
return {
|
||||||
route: null,
|
route: null,
|
||||||
error: readiness.message || '当前可查看位置预览和位置关系,暂不提供正式室内导航'
|
error: readiness.message || NAV_ROUTE_READINESS.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,20 @@ export const getLastGuideLocationPreviewUrl = () => {
|
|||||||
return window.sessionStorage.getItem(GUIDE_LOCATION_PREVIEW_STORAGE_KEY) || ''
|
return window.sessionStorage.getItem(GUIDE_LOCATION_PREVIEW_STORAGE_KEY) || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const clearLastGuideLocationPreviewUrl = () => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.sessionStorage.removeItem(GUIDE_LOCATION_PREVIEW_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const consumeLastGuideLocationPreviewUrl = () => {
|
||||||
|
const url = getLastGuideLocationPreviewUrl()
|
||||||
|
clearLastGuideLocationPreviewUrl()
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
export const navigateToGuideTopTab = (tab: GuideTopTab) => {
|
export const navigateToGuideTopTab = (tab: GuideTopTab) => {
|
||||||
if (tab === 'guide') {
|
if (tab === 'guide') {
|
||||||
const lastLocationPreviewUrl = getLastGuideLocationPreviewUrl()
|
const lastLocationPreviewUrl = consumeLastGuideLocationPreviewUrl()
|
||||||
if (lastLocationPreviewUrl) {
|
if (lastLocationPreviewUrl) {
|
||||||
uni.reLaunch({
|
uni.reLaunch({
|
||||||
url: lastLocationPreviewUrl
|
url: lastLocationPreviewUrl
|
||||||
|
|||||||
18
src/utils/locationPreviewSelection.ts
Normal file
18
src/utils/locationPreviewSelection.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const LOCATION_PREVIEW_SELECTION_MODE = 'location-preview'
|
||||||
|
|
||||||
|
export const isLocationPreviewSelectionMode = (value: unknown) => (
|
||||||
|
value === LOCATION_PREVIEW_SELECTION_MODE
|
||||||
|
)
|
||||||
|
|
||||||
|
export const locationPreviewSearchUrl = () => (
|
||||||
|
`/pages/search/index?selection=${LOCATION_PREVIEW_SELECTION_MODE}`
|
||||||
|
)
|
||||||
|
|
||||||
|
export const locationPreviewDetailUrl = (facilityId: string, target: string) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
facilityId,
|
||||||
|
target,
|
||||||
|
state: 'preview'
|
||||||
|
})
|
||||||
|
return `/pages/route/detail?${params.toString()}`
|
||||||
|
}
|
||||||
@@ -501,7 +501,7 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
await viewHallAction.trigger('tap')
|
await viewHallAction.trigger('tap')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/hall/detail?id=hall-l2')
|
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
|
||||||
await testState.onShowHandler?.()
|
await testState.onShowHandler?.()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||||||
@@ -528,8 +528,8 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(testState.resetToViewBaselineCalls).toHaveLength(1)
|
expect(testState.resetToViewBaselineCalls).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('模型点选展厅的相关讲解可进入展品详情,并在返回时复位原楼层', async () => {
|
it('模型点选展厅的相关讲解进入该展厅的讲解对象列表,并在返回时复位原楼层', async () => {
|
||||||
testState.explainResults = [{ type: 'exhibit', exhibitId: 'exhibit-l2' }]
|
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||||||
const wrapper = await mountIndex()
|
const wrapper = await mountIndex()
|
||||||
const map = wrapper.getComponent(GuideMapShellStub)
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
|
||||||
@@ -546,7 +546,7 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
await relatedActions[1]!.trigger('tap')
|
await relatedActions[1]!.trigger('tap')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/exhibit/detail?id=exhibit-l2')
|
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
|
||||||
confirmLatestNavigation()
|
confirmLatestNavigation()
|
||||||
await testState.onShowHandler?.()
|
await testState.onShowHandler?.()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -559,7 +559,7 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('模型点选展厅的相关讲解优先进入展厅详情并保留一次性返回上下文', async () => {
|
it('模型点选展厅的相关讲解优先进入讲解对象列表并保留一次性返回上下文', async () => {
|
||||||
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||||||
const wrapper = await mountIndex()
|
const wrapper = await mountIndex()
|
||||||
const map = wrapper.getComponent(GuideMapShellStub)
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
@@ -574,7 +574,7 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
await wrapper.findAll('.poi-action')[1]!.trigger('tap')
|
await wrapper.findAll('.poi-action')[1]!.trigger('tap')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/hall/detail?id=hall-l2')
|
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
|
||||||
confirmLatestNavigation()
|
confirmLatestNavigation()
|
||||||
await testState.onShowHandler?.()
|
await testState.onShowHandler?.()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
Reference in New Issue
Block a user