feat: wire guide data source and POI focus UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
123
src/data/providers/staticMuseumContentProvider.ts
Normal file
123
src/data/providers/staticMuseumContentProvider.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
|
||||
export interface MuseumContentProvider {
|
||||
listExhibits(): Promise<MuseumExhibit[]>
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
}
|
||||
|
||||
const transitionalHalls: MuseumHall[] = [
|
||||
{
|
||||
id: 'hall-evolution',
|
||||
name: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
description: '自然演化主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 3,
|
||||
area: '待补充'
|
||||
},
|
||||
{
|
||||
id: 'hall-dinosaur',
|
||||
name: '恐龙厅',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
description: '恐龙与古生物主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 2,
|
||||
area: '待补充'
|
||||
},
|
||||
{
|
||||
id: 'hall-biology',
|
||||
name: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
description: '生物多样性主题内容。当前为内容仓储过渡数据,待 CMS/API 接入后替换。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 2,
|
||||
area: '待补充'
|
||||
}
|
||||
]
|
||||
|
||||
const transitionalExhibits: MuseumExhibit[] = [
|
||||
{
|
||||
id: 'exhibit-ginkgo-fossil',
|
||||
name: '银杏化石',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '银杏化石内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['化石', '古植物']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-trilobite-fossil',
|
||||
name: '三叶虫化石',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '三叶虫化石内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['化石', '古生物']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-brachiosaurus',
|
||||
name: '腕龙骨骼',
|
||||
hallId: 'hall-dinosaur',
|
||||
hallName: '恐龙厅',
|
||||
floorId: 'L2',
|
||||
floorLabel: '2F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '腕龙骨骼内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['恐龙', '骨骼']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-archaeopteryx',
|
||||
name: '始祖鸟标本',
|
||||
hallId: 'hall-evolution',
|
||||
hallName: '演化厅',
|
||||
floorId: 'L-1',
|
||||
floorLabel: 'B1',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '始祖鸟标本内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['演化', '标本']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-mammoth-tusk',
|
||||
name: '猛犸象象牙',
|
||||
hallId: 'hall-biology',
|
||||
hallName: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '猛犸象象牙内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['古生物', '标本']
|
||||
},
|
||||
{
|
||||
id: 'exhibit-coral-reef',
|
||||
name: '珊瑚礁标本',
|
||||
hallId: 'hall-biology',
|
||||
hallName: '生物厅',
|
||||
floorId: 'L3',
|
||||
floorLabel: '3F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '珊瑚礁标本内容待由正式内容库补充。当前仅用于讲解业务架构迁移。',
|
||||
tags: ['海洋生物', '标本']
|
||||
}
|
||||
]
|
||||
|
||||
export class StaticMuseumContentProvider implements MuseumContentProvider {
|
||||
async listExhibits() {
|
||||
return transitionalExhibits
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
return transitionalHalls
|
||||
}
|
||||
}
|
||||
|
||||
export const staticMuseumContentProvider = new StaticMuseumContentProvider()
|
||||
214
src/data/providers/staticNavAssetsProvider.ts
Normal file
214
src/data/providers/staticNavAssetsProvider.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
NAV_ASSET_BASE_URL
|
||||
} from '@/domain/guideReadiness'
|
||||
|
||||
export interface StaticNavPoiCategoryPayload {
|
||||
topCategory: string
|
||||
topCategoryZh: string
|
||||
subcategory: string
|
||||
iconType: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export interface StaticNavPoiPayload {
|
||||
id: string
|
||||
name: string
|
||||
floorId: string
|
||||
primaryCategory: string
|
||||
primaryCategoryZh: string
|
||||
categories?: StaticNavPoiCategoryPayload[]
|
||||
iconType?: string
|
||||
positionGltf?: [number, number, number]
|
||||
navigationReadiness?: string
|
||||
sourceConfidence?: string
|
||||
}
|
||||
|
||||
export interface StaticNavManifestFloorModelPayload {
|
||||
floorId: string
|
||||
order: number
|
||||
asset: string
|
||||
}
|
||||
|
||||
export interface StaticNavManifestPayload {
|
||||
status: string
|
||||
assets: {
|
||||
overviewModel: {
|
||||
asset: string
|
||||
}
|
||||
floorModels: StaticNavManifestFloorModelPayload[]
|
||||
}
|
||||
data: {
|
||||
floorIndex: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface StaticNavFloorIndexItemPayload {
|
||||
floorId: string
|
||||
order: number
|
||||
modelAsset: string
|
||||
poiDataAsset: string
|
||||
poiCount: number
|
||||
}
|
||||
|
||||
export interface StaticNavFloorIndexPayload {
|
||||
status: string
|
||||
floors: StaticNavFloorIndexItemPayload[]
|
||||
}
|
||||
|
||||
interface StaticFloorPoiPayload {
|
||||
status: string
|
||||
floorId: string
|
||||
pois: StaticNavPoiPayload[]
|
||||
}
|
||||
|
||||
interface StaticPoiIndexPayload {
|
||||
status: string
|
||||
pois: StaticNavPoiPayload[]
|
||||
}
|
||||
|
||||
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
|
||||
|
||||
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 interface StaticNavAssetsProvider {
|
||||
readonly baseUrl: string
|
||||
assetUrl(relativePath: string): string
|
||||
loadManifest(): Promise<StaticNavManifestPayload>
|
||||
loadFloorIndex(): Promise<StaticNavFloorIndexPayload>
|
||||
loadPoiIndex(): Promise<StaticNavPoiPayload[]>
|
||||
loadFloorPois(relativePath: string): Promise<StaticNavPoiPayload[]>
|
||||
}
|
||||
|
||||
export const createStaticNavAssetsProvider = (
|
||||
baseUrl = NAV_ASSET_BASE_URL
|
||||
): StaticNavAssetsProvider => {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
|
||||
let poiCache: StaticNavPoiPayload[] | null = null
|
||||
let poiRequest: Promise<StaticNavPoiPayload[]> | null = null
|
||||
let manifestCache: StaticNavManifestPayload | null = null
|
||||
let manifestRequest: Promise<StaticNavManifestPayload> | null = null
|
||||
let floorIndexCache: StaticNavFloorIndexPayload | null = null
|
||||
let floorIndexRequest: Promise<StaticNavFloorIndexPayload> | null = null
|
||||
const floorPoiCache = new Map<string, StaticNavPoiPayload[]>()
|
||||
const floorPoiRequests = new Map<string, Promise<StaticNavPoiPayload[]>>()
|
||||
|
||||
const provider: StaticNavAssetsProvider = {
|
||||
baseUrl: normalizedBaseUrl,
|
||||
assetUrl(relativePath: string) {
|
||||
return `${normalizedBaseUrl}/${relativePath.replace(/^\/+/, '')}`
|
||||
},
|
||||
async loadManifest() {
|
||||
if (manifestCache) return manifestCache
|
||||
if (manifestRequest) return manifestRequest
|
||||
|
||||
manifestRequest = requestJson<StaticNavManifestPayload>(provider.assetUrl('app_nav_manifest.json'))
|
||||
.then((data) => {
|
||||
if (data.status !== 'pass') {
|
||||
throw new Error('室内导览资源包状态不是 pass')
|
||||
}
|
||||
|
||||
manifestCache = data
|
||||
return manifestCache
|
||||
})
|
||||
.finally(() => {
|
||||
manifestRequest = null
|
||||
})
|
||||
|
||||
return manifestRequest
|
||||
},
|
||||
async loadFloorIndex() {
|
||||
if (floorIndexCache) return floorIndexCache
|
||||
if (floorIndexRequest) return floorIndexRequest
|
||||
|
||||
floorIndexRequest = provider.loadManifest()
|
||||
.then((manifest) => requestJson<StaticNavFloorIndexPayload>(provider.assetUrl(manifest.data.floorIndex)))
|
||||
.then((data) => {
|
||||
if (data.status !== 'pass') {
|
||||
throw new Error('导览楼层索引状态不是 pass')
|
||||
}
|
||||
|
||||
floorIndexCache = data
|
||||
return floorIndexCache
|
||||
})
|
||||
.finally(() => {
|
||||
floorIndexRequest = null
|
||||
})
|
||||
|
||||
return floorIndexRequest
|
||||
},
|
||||
async loadPoiIndex() {
|
||||
if (poiCache) return poiCache
|
||||
if (poiRequest) return poiRequest
|
||||
|
||||
poiRequest = requestJson<StaticPoiIndexPayload>(provider.assetUrl('data/poi_all.json'))
|
||||
.then((data) => {
|
||||
if (data.status !== 'pass') {
|
||||
throw new Error('导览 POI 数据状态不是 pass')
|
||||
}
|
||||
|
||||
poiCache = data.pois
|
||||
return poiCache
|
||||
})
|
||||
.finally(() => {
|
||||
poiRequest = null
|
||||
})
|
||||
|
||||
return poiRequest
|
||||
},
|
||||
async loadFloorPois(relativePath: string) {
|
||||
const cacheKey = relativePath.replace(/^\/+/, '')
|
||||
const cached = floorPoiCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const pending = floorPoiRequests.get(cacheKey)
|
||||
if (pending) return pending
|
||||
|
||||
const request = requestJson<StaticFloorPoiPayload>(provider.assetUrl(cacheKey))
|
||||
.then((data) => {
|
||||
if (data.status !== 'pass') {
|
||||
throw new Error('导览楼层 POI 数据状态不是 pass')
|
||||
}
|
||||
|
||||
floorPoiCache.set(cacheKey, data.pois)
|
||||
return data.pois
|
||||
})
|
||||
.finally(() => {
|
||||
floorPoiRequests.delete(cacheKey)
|
||||
})
|
||||
|
||||
floorPoiRequests.set(cacheKey, request)
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
export const defaultStaticNavAssetsProvider = createStaticNavAssetsProvider()
|
||||
Reference in New Issue
Block a user