feat: wire guide data source and POI focus UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
>
|
||||
<GuideMapShell
|
||||
v-bind="shellConfig"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
:target-focus-request="targetFocusRequest"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@@ -125,12 +128,18 @@ import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import LocationPreview from '@/components/navigation/LocationPreview.vue'
|
||||
import {
|
||||
NAV_FLOOR_OPTIONS,
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
formatNavFloorLabel,
|
||||
loadCleanNavPoiById,
|
||||
type CleanNavPoi
|
||||
} from '@/services/navAssets'
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
initialLocationPreviewPlan,
|
||||
toTargetFocusRequestViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import type {
|
||||
GuideLocationPreview,
|
||||
GuideLocationPreviewPlan,
|
||||
GuideStartLocation,
|
||||
MuseumFloor
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
@@ -138,28 +147,11 @@ import {
|
||||
|
||||
type RouteViewState = 'preview' | 'location-error' | 'outdoor-preview'
|
||||
type GuideMode = '2d' | '3d'
|
||||
interface RouteStep {
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
|
||||
interface RouteTargetLocation {
|
||||
poiId: string
|
||||
name: string
|
||||
floorId: string
|
||||
floorLabel?: string
|
||||
primaryCategoryZh?: string
|
||||
positionGltf?: [number, number, number]
|
||||
}
|
||||
|
||||
interface RouteStartLocation {
|
||||
label: string
|
||||
floor?: string
|
||||
source?: string
|
||||
}
|
||||
type RouteTargetLocation = GuideLocationPreview
|
||||
type RouteStartLocation = GuideStartLocation
|
||||
|
||||
interface TargetPoiFocusRequest extends RouteTargetLocation {
|
||||
requestId: number
|
||||
requestId: number | string
|
||||
}
|
||||
|
||||
interface TargetPoiFocusResult {
|
||||
@@ -170,104 +162,44 @@ interface TargetPoiFocusResult {
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface RoutePlan {
|
||||
id: string
|
||||
facilityId: string
|
||||
target: string
|
||||
summary: string
|
||||
steps: RouteStep[]
|
||||
targetLocation: RouteTargetLocation | null
|
||||
startLocation: RouteStartLocation | null
|
||||
}
|
||||
type RoutePlan = GuideLocationPreviewPlan
|
||||
|
||||
const routeDeepLinkStates: RouteViewState[] = ['preview', 'outdoor-preview']
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
|
||||
const startSourceLabels: Record<string, string> = {
|
||||
'facility-detail': '设施详情选择',
|
||||
manual: '手动选择'
|
||||
}
|
||||
|
||||
const formatStartSourceLabel = (source?: string) => (
|
||||
source ? startSourceLabels[source] || source : ''
|
||||
)
|
||||
|
||||
const formatStartStepText = (startLocation: RouteStartLocation) => {
|
||||
const sourceLabel = formatStartSourceLabel(startLocation.source)
|
||||
const floorSuffix = startLocation.floor && !startLocation.label.includes(startLocation.floor)
|
||||
? ` · ${startLocation.floor}`
|
||||
: ''
|
||||
const sourceSuffix = sourceLabel ? `(${sourceLabel})` : ''
|
||||
|
||||
return `起点:${startLocation.label}${floorSuffix}${sourceSuffix}`
|
||||
}
|
||||
|
||||
const createPreviewSteps = (
|
||||
poi: CleanNavPoi | null,
|
||||
startLocation: RouteStartLocation | null
|
||||
): RouteStep[] => {
|
||||
if (startLocation) {
|
||||
return [
|
||||
{
|
||||
id: 'start',
|
||||
text: formatStartStepText(startLocation)
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
text: poi
|
||||
? `目标:${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh} · 位置预览`
|
||||
: '目标来自 clean 导览数据,暂仅支持位置预览'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'floor',
|
||||
text: poi ? `目标位于 ${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh}` : '目标来自 clean 导览数据'
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
text: NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createRoutePlan = (
|
||||
facilityId: string,
|
||||
target: string,
|
||||
poi: CleanNavPoi | null = null,
|
||||
startLocation: RouteStartLocation | null = null
|
||||
): RoutePlan => ({
|
||||
id: `route-${facilityId || 'custom'}`,
|
||||
facilityId,
|
||||
target,
|
||||
summary: poi
|
||||
? `${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh} · 位置预览`
|
||||
: '位置预览 · 尚未接入正式路线图',
|
||||
steps: createPreviewSteps(poi, startLocation),
|
||||
targetLocation: poi ? {
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: formatNavFloorLabel(poi.floorId),
|
||||
primaryCategoryZh: poi.primaryCategoryZh,
|
||||
positionGltf: poi.positionGltf
|
||||
} : null,
|
||||
startLocation
|
||||
})
|
||||
|
||||
const route = ref<RoutePlan>(createRoutePlan('', '目标地点'))
|
||||
const route = ref<RoutePlan>(initialLocationPreviewPlan())
|
||||
const hasRouteTarget = ref(false)
|
||||
const routeViewState = ref<RouteViewState>('preview')
|
||||
const activeFloor = ref('1F')
|
||||
const targetFocusRequest = ref<TargetPoiFocusRequest | null>(null)
|
||||
const targetFocusRequestId = ref(0)
|
||||
const manualFloors = NAV_FLOOR_OPTIONS.map((floor) => floor.label)
|
||||
const manualFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||||
const guideFloorLabel = (floorId: string) => (
|
||||
guideFloors.value.find((floor) => floor.id === floorId)?.label || floorId
|
||||
)
|
||||
const defaultGuideFloorLabel = () => (
|
||||
guideFloors.value.find((floor) => floor.id === 'L1')?.label
|
||||
|| guideFloors.value[0]?.label
|
||||
|| '1F'
|
||||
)
|
||||
const loadGuideFloors = async () => {
|
||||
if (guideFloors.value.length) return guideFloors.value
|
||||
|
||||
try {
|
||||
guideFloors.value = await guideUseCase.getFloors()
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
}
|
||||
|
||||
return guideFloors.value
|
||||
}
|
||||
const manualAreas = ['主入口', '服务台附近', '停车场入口']
|
||||
const isManualLocationPanelOpen = ref(false)
|
||||
const selectedManualFloor = ref('1F')
|
||||
const selectedManualArea = ref('服务台附近')
|
||||
|
||||
const normalizeRouteOption = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0]
|
||||
@@ -380,7 +312,36 @@ const createStartLocationFromOptions = (options: Record<string, unknown>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestTargetFocus = (target: Partial<RouteTargetLocation> | null, fallbackName = route.value.target) => {
|
||||
if (!target?.poiId || !target.floorId) {
|
||||
targetFocusRequest.value = null
|
||||
return false
|
||||
}
|
||||
|
||||
const floorLabel = target.floorLabel || guideFloorLabel(target.floorId)
|
||||
const focusTarget: RouteTargetLocation = {
|
||||
poiId: target.poiId,
|
||||
name: target.name || fallbackName,
|
||||
floorId: target.floorId,
|
||||
floorLabel,
|
||||
primaryCategoryZh: target.primaryCategoryZh,
|
||||
positionGltf: target.positionGltf
|
||||
}
|
||||
|
||||
activeFloor.value = floorLabel
|
||||
routeViewState.value = 'preview'
|
||||
targetFocusRequestId.value += 1
|
||||
targetFocusRequest.value = toTargetFocusRequestViewModel(
|
||||
focusTarget,
|
||||
targetFocusRequestId.value
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
await loadGuideFloors()
|
||||
|
||||
const facilityId = normalizeStringOption(options.facilityId)
|
||||
const target = normalizeStringOption(options.target)
|
||||
const state = normalizeRouteOption(options.state)
|
||||
@@ -388,39 +349,42 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
|
||||
if (!facilityId && !target) {
|
||||
hasRouteTarget.value = false
|
||||
route.value = createRoutePlan('', '目标地点')
|
||||
route.value = initialLocationPreviewPlan()
|
||||
routeViewState.value = 'preview'
|
||||
activeFloor.value = '1F'
|
||||
activeFloor.value = defaultGuideFloorLabel()
|
||||
targetFocusRequest.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
const matchedPoi = facilityId ? await loadCleanNavPoiById(facilityId) : null
|
||||
const matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null
|
||||
|
||||
if (!matchedPoi) {
|
||||
hasRouteTarget.value = false
|
||||
route.value = createRoutePlan('', target || '目标地点')
|
||||
route.value = await guideUseCase.createLocationPreviewPlan('', target || '目标地点', startLocation)
|
||||
routeViewState.value = 'preview'
|
||||
activeFloor.value = startLocation?.floor || '1F'
|
||||
activeFloor.value = startLocation?.floor || defaultGuideFloorLabel()
|
||||
targetFocusRequest.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
route.value = createRoutePlan(
|
||||
route.value = await guideUseCase.createLocationPreviewPlan(
|
||||
facilityId,
|
||||
target || matchedPoi.name,
|
||||
matchedPoi,
|
||||
startLocation
|
||||
)
|
||||
|
||||
hasRouteTarget.value = true
|
||||
routeViewState.value = isRouteViewState(state) ? state : 'preview'
|
||||
activeFloor.value = matchedPoi ? formatNavFloorLabel(matchedPoi.floorId) : startLocation?.floor || '1F'
|
||||
targetFocusRequest.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
|
||||
if (routeViewState.value === 'preview') {
|
||||
requestTargetFocus(route.value.targetLocation, matchedPoi.name)
|
||||
} else {
|
||||
activeFloor.value = matchedPoi.floorLabel || guideFloorLabel(matchedPoi.floorId)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -479,23 +443,14 @@ const handleViewOutdoorMap = () => {
|
||||
routeViewState.value = 'outdoor-preview'
|
||||
}
|
||||
|
||||
const handleShowTargetLocation = (target: RouteTargetLocation | null) => {
|
||||
const targetLocation = target || route.value.targetLocation
|
||||
const handleShowTargetLocation = (target: Partial<RouteTargetLocation> | null) => {
|
||||
const focused = requestTargetFocus(target || route.value.targetLocation)
|
||||
|
||||
if (!targetLocation?.poiId || !targetLocation.floorId) {
|
||||
if (!focused) {
|
||||
uni.showToast({
|
||||
title: '目标暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
routeViewState.value = 'preview'
|
||||
activeFloor.value = targetLocation.floorLabel || formatNavFloorLabel(targetLocation.floorId)
|
||||
targetFocusRequestId.value += 1
|
||||
targetFocusRequest.value = {
|
||||
...targetLocation,
|
||||
requestId: targetFocusRequestId.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,6 +466,7 @@ const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||||
const handleReturnToPreview = () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
}
|
||||
|
||||
const handleRelocate = () => {
|
||||
@@ -540,39 +496,24 @@ const createManualStartLocation = (): RouteStartLocation => ({
|
||||
source: 'manual'
|
||||
})
|
||||
|
||||
const createCurrentTargetStep = (): RouteStep => {
|
||||
const targetLocation = route.value.targetLocation
|
||||
|
||||
return {
|
||||
id: 'target',
|
||||
text: targetLocation
|
||||
? `目标:${targetLocation.floorLabel || formatNavFloorLabel(targetLocation.floorId)} · ${targetLocation.primaryCategoryZh || route.value.target} · 位置预览`
|
||||
: '目标来自 clean 导览数据,暂仅支持位置预览'
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmLocation = () => {
|
||||
const handleConfirmLocation = async () => {
|
||||
const startLocation = createManualStartLocation()
|
||||
|
||||
route.value = {
|
||||
...route.value,
|
||||
startLocation,
|
||||
steps: [
|
||||
{
|
||||
id: 'start',
|
||||
text: formatStartStepText(startLocation)
|
||||
},
|
||||
createCurrentTargetStep()
|
||||
]
|
||||
}
|
||||
route.value = await guideUseCase.createLocationPreviewPlan(
|
||||
route.value.facilityId,
|
||||
route.value.target,
|
||||
startLocation
|
||||
)
|
||||
activeFloor.value = startLocation.floor || activeFloor.value
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: GuideMode) => {
|
||||
if (routeViewState.value === 'outdoor-preview' && mode === '3d') {
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -596,7 +537,7 @@ const handleToolClick = (tool: string) => {
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: tool === '回正' ? '已回到三维总览' : NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
title: tool === '回正' ? '已回到三维总览' : guideUseCase.getRouteReadiness().message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user