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

@@ -0,0 +1,122 @@
import type {
GuideLocationPreview,
MuseumFloor,
MuseumPoi
} from '@/domain/museum'
import {
NAV_ROUTE_READINESS
} from '@/domain/guideReadiness'
import {
formatNavFloorLabel,
navFloorIdFromLabel,
toMuseumPoi
} from '@/data/adapters/navAssetsAdapter'
import {
defaultStaticNavAssetsProvider,
type StaticNavAssetsProvider
} from '@/data/providers/staticNavAssetsProvider'
const searchableCategories = new Set([
'touring_poi',
'basic_service_facility',
'transport_circulation',
'accessibility_special_service',
'operation_experience'
])
const toSearchText = (poi: MuseumPoi) => [
poi.name,
poi.floorId,
poi.floorLabel,
poi.primaryCategory.label,
poi.primaryCategory.iconType,
...poi.categories.flatMap((category) => [
category.label,
category.id,
category.iconType
])
]
.filter(Boolean)
.join(' ')
.toLowerCase()
const toLocationPreview = (poi: MuseumPoi): GuideLocationPreview => ({
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
primaryCategoryZh: poi.primaryCategory.label,
positionGltf: poi.positionGltf
})
export interface GuideRepository {
getAssetBaseUrl(): string
getFloors(): Promise<MuseumFloor[]>
normalizeFloorId(labelOrId: string): string
listPois(): Promise<MuseumPoi[]>
getPoiById(id: string): Promise<MuseumPoi | null>
searchPois(keyword?: string): Promise<MuseumPoi[]>
getLocationPreview(poiId: string): Promise<GuideLocationPreview | null>
getRouteReadiness(): typeof NAV_ROUTE_READINESS
}
export class StaticGuideRepository implements GuideRepository {
private poiCache: MuseumPoi[] | null = null
constructor(private readonly provider: StaticNavAssetsProvider = defaultStaticNavAssetsProvider) {}
getAssetBaseUrl() {
return this.provider.baseUrl
}
async getFloors() {
const floorIndex = await this.provider.loadFloorIndex()
return [...floorIndex.floors]
.sort((a, b) => b.order - a.order)
.map((floor) => ({
id: floor.floorId,
label: formatNavFloorLabel(floor.floorId),
order: floor.order
}))
}
normalizeFloorId(labelOrId: string) {
return navFloorIdFromLabel(labelOrId)
}
async listPois() {
if (this.poiCache) return this.poiCache
const pois = await this.provider.loadPoiIndex()
this.poiCache = pois
.filter((poi) => searchableCategories.has(poi.primaryCategory))
.map(toMuseumPoi)
return this.poiCache
}
async getPoiById(id: string) {
const pois = await this.listPois()
return pois.find((poi) => poi.id === id) || null
}
async searchPois(keyword = '') {
const pois = await this.listPois()
const normalizedKeyword = keyword.trim().toLowerCase()
if (!normalizedKeyword) return pois
return pois.filter((poi) => toSearchText(poi).includes(normalizedKeyword))
}
async getLocationPreview(poiId: string) {
const poi = await this.getPoiById(poiId)
return poi ? toLocationPreview(poi) : null
}
getRouteReadiness() {
return NAV_ROUTE_READINESS
}
}
export const guideRepository = new StaticGuideRepository()