feat: wire guide data source and POI focus UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lyf
2026-06-12 09:25:22 +08:00
parent a6bfda30e1
commit 2055b13b90
58 changed files with 5768 additions and 1898 deletions

View File

@@ -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'
})
}