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

@@ -1,164 +0,0 @@
export const NAV_ASSET_BASE_URL = '/static/nav-assets/app_nav_assets_v2_clean_20260609_075339'
export const NAV_ROUTE_UNAVAILABLE_MESSAGE = '正式路线数据尚未接入,可先查看馆内三维位置'
export interface NavFloorOption {
id: string
label: string
order: number
}
export interface CleanNavPoiCategory {
topCategory: string
topCategoryZh: string
subcategory: string
iconType: string
reason?: string
}
export interface CleanNavPoi {
id: string
name: string
floorId: string
primaryCategory: string
primaryCategoryZh: string
categories?: CleanNavPoiCategory[]
iconType?: string
positionGltf?: [number, number, number]
navigationReadiness?: string
sourceConfidence?: string
}
interface CleanPoiIndex {
status: string
pois: CleanNavPoi[]
}
export const NAV_FLOOR_OPTIONS: NavFloorOption[] = [
{ id: 'L5', label: '5F', order: 5 },
{ id: 'L4', label: '4F', order: 4 },
{ id: 'L3', label: '3F', order: 3 },
{ id: 'L2', label: '2F', order: 2 },
{ id: 'L1.5', label: '1.5F', order: 1.5 },
{ id: 'L1', label: '1F', order: 1 },
{ id: 'L-1', label: 'B1', order: -1 },
{ id: 'L-2', label: 'B2', order: -2 }
]
const searchableCategories = new Set([
'touring_poi',
'basic_service_facility',
'transport_circulation',
'accessibility_special_service',
'operation_experience'
])
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
const poiCache = new Map<string, CleanNavPoi[]>()
const poiRequestCache = new Map<string, Promise<CleanNavPoi[]>>()
export const navAssetUrl = (relativePath: string, baseUrl = NAV_ASSET_BASE_URL) => (
`${normalizeBaseUrl(baseUrl)}/${relativePath.replace(/^\/+/, '')}`
)
export const formatNavFloorLabel = (floorId: string) => (
NAV_FLOOR_OPTIONS.find((floor) => floor.id === floorId)?.label || floorId
)
export const navFloorIdFromLabel = (labelOrId: string) => (
NAV_FLOOR_OPTIONS.find((floor) => floor.label === labelOrId || floor.id === labelOrId)?.id || labelOrId
)
export const isPoiAccessible = (poi: CleanNavPoi) => (
poi.primaryCategory === 'accessibility_special_service'
|| poi.categories?.some((category) => category.topCategory === 'accessibility_special_service') === true
)
const parseJsonPayload = <T>(payload: unknown): T => {
if (typeof payload === 'string') {
return JSON.parse(payload) as T
}
return payload as T
}
const requestJson = <T>(url: string): Promise<T> => new Promise((resolve, reject) => {
uni.request({
url,
method: 'GET',
success: (response) => {
const statusCode = Number(response.statusCode || 0)
if (statusCode < 200 || statusCode >= 300) {
reject(new Error(`导览资源读取失败: ${statusCode} ${url}`))
return
}
try {
resolve(parseJsonPayload<T>(response.data))
} catch (error) {
reject(error)
}
},
fail: reject
})
})
export const loadCleanNavPois = async (baseUrl = NAV_ASSET_BASE_URL) => {
const cacheKey = normalizeBaseUrl(baseUrl)
const cachedPois = poiCache.get(cacheKey)
if (cachedPois) return cachedPois
const pendingPois = poiRequestCache.get(cacheKey)
if (pendingPois) return pendingPois
const request = requestJson<CleanPoiIndex>(navAssetUrl('data/poi_all.json', cacheKey))
.then((data) => {
if (data.status !== 'pass') {
throw new Error('导览 POI 数据状态不是 pass')
}
const pois = data.pois.filter((poi) => searchableCategories.has(poi.primaryCategory))
poiCache.set(cacheKey, pois)
return pois
})
.finally(() => {
poiRequestCache.delete(cacheKey)
})
poiRequestCache.set(cacheKey, request)
return request
}
export const loadCleanNavPoiById = async (id: string, baseUrl = NAV_ASSET_BASE_URL) => {
const pois = await loadCleanNavPois(baseUrl)
return pois.find((poi) => poi.id === id) || null
}
export const searchCleanNavPois = async (keyword = '', baseUrl = NAV_ASSET_BASE_URL) => {
const pois = await loadCleanNavPois(baseUrl)
const normalizedKeyword = keyword.trim().toLowerCase()
const matchedPois = normalizedKeyword
? pois.filter((poi) => {
const searchableText = [
poi.name,
poi.floorId,
formatNavFloorLabel(poi.floorId),
poi.primaryCategoryZh,
poi.iconType,
...(poi.categories || []).flatMap((category) => [
category.topCategoryZh,
category.subcategory,
category.iconType
])
]
.filter(Boolean)
.join(' ')
.toLowerCase()
return searchableText.includes(normalizedKeyword)
})
: pois
return matchedPois
}