chore: freeze guide and explain update
This commit is contained in:
444
src/data/providers/staticGuideDataProvider.ts
Normal file
444
src/data/providers/staticGuideDataProvider.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import {
|
||||
dataSourceConfig
|
||||
} from '@/config/dataSource'
|
||||
|
||||
export interface GuideStaticPackagePayload<T> {
|
||||
schemaVersion: string
|
||||
sourceTable?: string
|
||||
rowCount?: number
|
||||
rows: T[]
|
||||
}
|
||||
|
||||
export interface GuideStaticManifestPayload {
|
||||
schemaVersion: string
|
||||
generatedAt: string
|
||||
source: {
|
||||
type: string
|
||||
host: string
|
||||
port: string
|
||||
schema: string
|
||||
configPath: string
|
||||
}
|
||||
files: {
|
||||
halls: string
|
||||
outlines: string
|
||||
guideStops: string
|
||||
exhibits: string
|
||||
guideContents: string
|
||||
pois: string
|
||||
indexes: string
|
||||
poiBridge?: string
|
||||
}
|
||||
counts: Record<string, number>
|
||||
bridgeStats?: Record<string, number>
|
||||
}
|
||||
|
||||
export interface GuideStaticHallPayload {
|
||||
id: number | string
|
||||
mapId?: number | string | null
|
||||
floorId?: number | string | null
|
||||
floorCode?: string | null
|
||||
poiId?: number | string | null
|
||||
hallCode?: string | null
|
||||
name?: string | null
|
||||
nameEn?: string | null
|
||||
subtitle?: string | null
|
||||
description?: string | null
|
||||
descriptionEn?: string | null
|
||||
narrationText?: string | null
|
||||
coverImageUrl?: string | null
|
||||
galleryUrls?: string | null
|
||||
audioUrl?: string | null
|
||||
audioDuration?: number | null
|
||||
videoUrl?: string | null
|
||||
areaSqm?: number | null
|
||||
exhibitCount?: number | null
|
||||
openTime?: string | null
|
||||
sortOrder?: number | null
|
||||
status?: number | boolean | null
|
||||
deleted?: number | boolean | null
|
||||
}
|
||||
|
||||
export interface GuideStaticOutlinePayload {
|
||||
id: number | string
|
||||
parentId?: number | string | null
|
||||
name?: string | null
|
||||
code?: string | null
|
||||
description?: string | null
|
||||
sort?: number | null
|
||||
deleted?: number | boolean | null
|
||||
}
|
||||
|
||||
export interface GuideStaticStopPayload {
|
||||
id: number | string
|
||||
outlineId?: number | string | null
|
||||
name?: string | null
|
||||
sort?: number | null
|
||||
coverImageUrl?: string | null
|
||||
audioUrl?: string | null
|
||||
poiId?: number | string | null
|
||||
status?: number | boolean | null
|
||||
mapX?: number | null
|
||||
mapY?: number | null
|
||||
floorId?: number | string | null
|
||||
floorCode?: string | null
|
||||
deleted?: number | boolean | null
|
||||
}
|
||||
|
||||
export interface GuideStaticExhibitPayload {
|
||||
id: number | string
|
||||
hallId?: number | string | null
|
||||
outlineId?: number | string | null
|
||||
stopId?: number | string | null
|
||||
spatialNodeId?: number | string | null
|
||||
poiId?: number | string | null
|
||||
exhibitCode?: string | null
|
||||
name?: string | null
|
||||
nameEn?: string | null
|
||||
scientificName?: string | null
|
||||
category?: string | null
|
||||
era?: string | null
|
||||
origin?: string | null
|
||||
dimensions?: string | null
|
||||
description?: string | null
|
||||
descriptionEn?: string | null
|
||||
narrationText?: string | null
|
||||
coverImageUrl?: string | null
|
||||
galleryUrls?: string | null
|
||||
audioUrl?: string | null
|
||||
audioDuration?: number | null
|
||||
videoUrl?: string | null
|
||||
model3dUrl?: string | null
|
||||
qrCodeUrl?: string | null
|
||||
isHighlight?: boolean | number | null
|
||||
exhibitLevel?: string | null
|
||||
dataSource?: string | null
|
||||
collectionId?: string | null
|
||||
viewCount?: number | null
|
||||
sortOrder?: number | null
|
||||
status?: number | boolean | null
|
||||
deleted?: number | boolean | null
|
||||
}
|
||||
|
||||
export interface GuideStaticContentPayload {
|
||||
id: number | string
|
||||
targetType?: string | null
|
||||
targetId?: number | string | null
|
||||
exhibitId?: number | string | null
|
||||
poiId?: number | string | null
|
||||
title?: string | null
|
||||
contentType?: string | null
|
||||
standardText?: string | null
|
||||
standardTextEn?: string | null
|
||||
extendedText?: string | null
|
||||
extendedTextEn?: string | null
|
||||
standardAudioUrl?: string | null
|
||||
standardAudioUrlEn?: string | null
|
||||
standardAudioDuration?: number | null
|
||||
standardAudioDurationEn?: number | null
|
||||
extendedAudioUrl?: string | null
|
||||
extendedAudioUrlEn?: string | null
|
||||
extendedAudioDuration?: number | null
|
||||
extendedAudioDurationEn?: number | null
|
||||
audioUrl?: string | null
|
||||
audioDuration?: number | null
|
||||
videoUrl?: string | null
|
||||
mediaGallery?: string | null
|
||||
aiTags?: string | null
|
||||
sortOrder?: number | null
|
||||
status?: number | boolean | null
|
||||
deleted?: number | boolean | null
|
||||
}
|
||||
|
||||
export interface GuideStaticPoiPayload {
|
||||
id: number | string
|
||||
mapId?: number | string | null
|
||||
floorId?: number | string | null
|
||||
name?: string | null
|
||||
nameEn?: string | null
|
||||
type?: string | null
|
||||
exhibitCode?: string | null
|
||||
x?: number | null
|
||||
y?: number | null
|
||||
z?: number | null
|
||||
longitude?: number | null
|
||||
latitude?: number | null
|
||||
address?: string | null
|
||||
iconUrl?: string | null
|
||||
coverImageUrl?: string | null
|
||||
description?: string | null
|
||||
descriptionEn?: string | null
|
||||
extParams?: string | null
|
||||
externalUrl?: string | null
|
||||
images?: string | null
|
||||
spatialAreaId?: number | string | null
|
||||
spatialAreaName?: string | null
|
||||
sortOrder?: number | null
|
||||
isGuidePoint?: boolean | number | null
|
||||
status?: number | boolean | null
|
||||
deleted?: number | boolean | null
|
||||
}
|
||||
|
||||
export interface GuideStaticIndexesPayload {
|
||||
schemaVersion: string
|
||||
outlinesByHallId: Record<string, Array<number | string>>
|
||||
stopsByOutlineId: Record<string, Array<number | string>>
|
||||
exhibitsByHallId: Record<string, Array<number | string>>
|
||||
exhibitsByOutlineId: Record<string, Array<number | string>>
|
||||
exhibitsByStopId: Record<string, Array<number | string>>
|
||||
guidesByExhibitId: Record<string, Array<number | string>>
|
||||
guidesByTarget: Record<string, Array<number | string>>
|
||||
poiById: Record<string, GuideStaticPoiPayload>
|
||||
}
|
||||
|
||||
export interface GuideStaticPoiBridgeEntryPayload {
|
||||
navPoiId?: string | null
|
||||
navPoiName?: string | null
|
||||
navFloorId?: string | null
|
||||
method?: string | null
|
||||
confidence?: string | null
|
||||
distanceMeters?: number | null
|
||||
sourcePoiId?: string | number | null
|
||||
sourcePoiName?: string | null
|
||||
}
|
||||
|
||||
export interface GuideStaticPoiBridgePayload {
|
||||
schemaVersion: string
|
||||
generatedAt?: string
|
||||
source?: Record<string, unknown>
|
||||
stats?: Record<string, number>
|
||||
sgsFloorIdToNavFloorId?: Record<string, string>
|
||||
hallToNavPoiId: Record<string, GuideStaticPoiBridgeEntryPayload>
|
||||
sgsPoiToNavPoiId: Record<string, GuideStaticPoiBridgeEntryPayload>
|
||||
sgsPoiCandidates: Record<string, GuideStaticPoiBridgeEntryPayload>
|
||||
}
|
||||
|
||||
export interface GuideStaticDataset {
|
||||
manifest: GuideStaticManifestPayload
|
||||
halls: GuideStaticHallPayload[]
|
||||
outlines: GuideStaticOutlinePayload[]
|
||||
guideStops: GuideStaticStopPayload[]
|
||||
exhibits: GuideStaticExhibitPayload[]
|
||||
guideContents: GuideStaticContentPayload[]
|
||||
pois: GuideStaticPoiPayload[]
|
||||
indexes: GuideStaticIndexesPayload
|
||||
poiBridge: GuideStaticPoiBridgePayload
|
||||
}
|
||||
|
||||
export interface GuideStaticExplainDataset {
|
||||
manifest: GuideStaticManifestPayload
|
||||
halls: GuideStaticHallPayload[]
|
||||
outlines: GuideStaticOutlinePayload[]
|
||||
guideStops: GuideStaticStopPayload[]
|
||||
guideContents: GuideStaticContentPayload[]
|
||||
poiBridge: GuideStaticPoiBridgePayload
|
||||
}
|
||||
|
||||
const emptyPoiBridge: GuideStaticPoiBridgePayload = {
|
||||
schemaVersion: 'sgs-guide-poi-bridge/empty',
|
||||
hallToNavPoiId: {},
|
||||
sgsPoiToNavPoiId: {},
|
||||
sgsPoiCandidates: {}
|
||||
}
|
||||
|
||||
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 requestJsonByFetch = async <T>(url: string): Promise<T> => {
|
||||
const response = await fetch(url, {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`导览静态数据读取失败: ${response.status} ${url}`)
|
||||
}
|
||||
|
||||
return parseJsonPayload<T>(await response.text())
|
||||
}
|
||||
|
||||
const requestJsonByUni = <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
|
||||
})
|
||||
})
|
||||
|
||||
const requestJson = <T>(url: string): Promise<T> => {
|
||||
if (typeof fetch === 'function') {
|
||||
return requestJsonByFetch<T>(url)
|
||||
}
|
||||
|
||||
return requestJsonByUni<T>(url)
|
||||
}
|
||||
|
||||
export interface StaticGuideDataProvider {
|
||||
readonly baseUrl: string
|
||||
assetUrl(relativePath: string): string
|
||||
loadManifest(): Promise<GuideStaticManifestPayload>
|
||||
loadIndexes(): Promise<GuideStaticIndexesPayload>
|
||||
loadExplainDataset(): Promise<GuideStaticExplainDataset>
|
||||
loadDataset(): Promise<GuideStaticDataset>
|
||||
}
|
||||
|
||||
export const createStaticGuideDataProvider = (
|
||||
baseUrl = dataSourceConfig.guideStaticDataBaseUrl
|
||||
): StaticGuideDataProvider => {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl)
|
||||
let manifestCache: GuideStaticManifestPayload | null = null
|
||||
let manifestRequest: Promise<GuideStaticManifestPayload> | null = null
|
||||
let indexesCache: GuideStaticIndexesPayload | null = null
|
||||
let indexesRequest: Promise<GuideStaticIndexesPayload> | null = null
|
||||
let explainDatasetCache: GuideStaticExplainDataset | null = null
|
||||
let explainDatasetRequest: Promise<GuideStaticExplainDataset> | null = null
|
||||
let datasetCache: GuideStaticDataset | null = null
|
||||
let datasetRequest: Promise<GuideStaticDataset> | null = null
|
||||
|
||||
const provider: StaticGuideDataProvider = {
|
||||
baseUrl: normalizedBaseUrl,
|
||||
assetUrl(relativePath: string) {
|
||||
return `${normalizedBaseUrl}/${relativePath.replace(/^\/+/, '')}`
|
||||
},
|
||||
async loadManifest() {
|
||||
if (manifestCache) return manifestCache
|
||||
if (manifestRequest) return manifestRequest
|
||||
|
||||
manifestRequest = requestJson<GuideStaticManifestPayload>(provider.assetUrl('manifest.json'))
|
||||
.then((manifest) => {
|
||||
manifestCache = manifest
|
||||
return manifest
|
||||
})
|
||||
.finally(() => {
|
||||
manifestRequest = null
|
||||
})
|
||||
|
||||
return manifestRequest
|
||||
},
|
||||
async loadIndexes() {
|
||||
if (indexesCache) return indexesCache
|
||||
if (indexesRequest) return indexesRequest
|
||||
|
||||
indexesRequest = provider.loadManifest()
|
||||
.then(async (manifest) => {
|
||||
indexesCache = await requestJson<GuideStaticIndexesPayload>(provider.assetUrl(manifest.files.indexes))
|
||||
return indexesCache
|
||||
})
|
||||
.finally(() => {
|
||||
indexesRequest = null
|
||||
})
|
||||
|
||||
return indexesRequest
|
||||
},
|
||||
async loadExplainDataset() {
|
||||
if (explainDatasetCache) return explainDatasetCache
|
||||
if (datasetCache) {
|
||||
explainDatasetCache = {
|
||||
manifest: datasetCache.manifest,
|
||||
halls: datasetCache.halls,
|
||||
outlines: datasetCache.outlines,
|
||||
guideStops: datasetCache.guideStops,
|
||||
guideContents: datasetCache.guideContents,
|
||||
poiBridge: datasetCache.poiBridge
|
||||
}
|
||||
return explainDatasetCache
|
||||
}
|
||||
if (explainDatasetRequest) return explainDatasetRequest
|
||||
|
||||
explainDatasetRequest = provider.loadManifest()
|
||||
.then(async (manifest) => {
|
||||
const [
|
||||
halls,
|
||||
outlines,
|
||||
guideStops,
|
||||
guideContents,
|
||||
poiBridge
|
||||
] = await Promise.all([
|
||||
requestJson<GuideStaticPackagePayload<GuideStaticHallPayload>>(provider.assetUrl(manifest.files.halls)),
|
||||
requestJson<GuideStaticPackagePayload<GuideStaticOutlinePayload>>(provider.assetUrl(manifest.files.outlines)),
|
||||
requestJson<GuideStaticPackagePayload<GuideStaticStopPayload>>(provider.assetUrl(manifest.files.guideStops)),
|
||||
requestJson<GuideStaticPackagePayload<GuideStaticContentPayload>>(provider.assetUrl(manifest.files.guideContents)),
|
||||
manifest.files.poiBridge
|
||||
? requestJson<GuideStaticPoiBridgePayload>(provider.assetUrl(manifest.files.poiBridge))
|
||||
: Promise.resolve(emptyPoiBridge)
|
||||
])
|
||||
|
||||
explainDatasetCache = {
|
||||
manifest,
|
||||
halls: halls.rows,
|
||||
outlines: outlines.rows,
|
||||
guideStops: guideStops.rows,
|
||||
guideContents: guideContents.rows,
|
||||
poiBridge
|
||||
}
|
||||
|
||||
return explainDatasetCache
|
||||
})
|
||||
.finally(() => {
|
||||
explainDatasetRequest = null
|
||||
})
|
||||
|
||||
return explainDatasetRequest
|
||||
},
|
||||
async loadDataset() {
|
||||
if (datasetCache) return datasetCache
|
||||
if (datasetRequest) return datasetRequest
|
||||
|
||||
datasetRequest = provider.loadExplainDataset()
|
||||
.then(async (explainDataset) => {
|
||||
const manifest = explainDataset.manifest
|
||||
const [
|
||||
exhibits,
|
||||
pois,
|
||||
indexes
|
||||
] = await Promise.all([
|
||||
requestJson<GuideStaticPackagePayload<GuideStaticExhibitPayload>>(provider.assetUrl(manifest.files.exhibits)),
|
||||
requestJson<GuideStaticPackagePayload<GuideStaticPoiPayload>>(provider.assetUrl(manifest.files.pois)),
|
||||
provider.loadIndexes()
|
||||
])
|
||||
|
||||
datasetCache = {
|
||||
manifest,
|
||||
halls: explainDataset.halls,
|
||||
outlines: explainDataset.outlines,
|
||||
guideStops: explainDataset.guideStops,
|
||||
exhibits: exhibits.rows,
|
||||
guideContents: explainDataset.guideContents,
|
||||
pois: pois.rows,
|
||||
indexes,
|
||||
poiBridge: explainDataset.poiBridge
|
||||
}
|
||||
|
||||
return datasetCache
|
||||
})
|
||||
.finally(() => {
|
||||
datasetRequest = null
|
||||
})
|
||||
|
||||
return datasetRequest
|
||||
}
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
export const staticGuideDataProvider = createStaticGuideDataProvider()
|
||||
@@ -1,7 +1,25 @@
|
||||
import type {
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
dataSourceConfig,
|
||||
isGuideContentMockMode,
|
||||
isGuideContentRemoteMode,
|
||||
isGuideContentStaticMode
|
||||
} from '@/config/dataSource'
|
||||
import {
|
||||
createGuideExplainDataAdapter,
|
||||
createGuideContentDataAdapter,
|
||||
type GuideContentDataAdapterResult,
|
||||
type GuideExplainDataAdapterResult
|
||||
} from '@/data/adapters/guideDataAdapter'
|
||||
import {
|
||||
staticGuideDataProvider,
|
||||
type StaticGuideDataProvider
|
||||
} from '@/data/providers/staticGuideDataProvider'
|
||||
import {
|
||||
SGS_SCENE_EXPLAIN_DATASET,
|
||||
type SgsSceneExhibitMock,
|
||||
@@ -13,6 +31,13 @@ export interface MuseumContentProvider {
|
||||
listHalls(): Promise<MuseumHall[]>
|
||||
}
|
||||
|
||||
export interface ExplainContentProvider extends MuseumContentProvider {
|
||||
listExplainExhibits(): Promise<MuseumExhibit[]>
|
||||
listTracks(): Promise<ExplainTrack[]>
|
||||
getMediaById(id: string): Promise<MediaAsset | null>
|
||||
getMediaForExplainTrack(trackId: string): Promise<MediaAsset | null>
|
||||
}
|
||||
|
||||
const normalizeContentId = (prefix: string, value: string | number) => `${prefix}-sgs-${value}`
|
||||
|
||||
const categoryLabelMap: Record<SgsSceneSpaceMock['spaceCategory'], string> = {
|
||||
@@ -25,61 +50,24 @@ const categoryLabelMap: Record<SgsSceneSpaceMock['spaceCategory'], string> = {
|
||||
BACK_OFFICE: '后勤空间'
|
||||
}
|
||||
|
||||
const boundaryLabelMap: Record<NonNullable<SgsSceneSpaceMock['boundaryStatus']>, string> = {
|
||||
DEFINED: '已定义空间边界',
|
||||
UNDEFINED: '空间边界待完善'
|
||||
}
|
||||
|
||||
const publicSceneSpaces = SGS_SCENE_EXPLAIN_DATASET.spaces.filter((space) => space.isPublic)
|
||||
const publicSpaceById = new Map(publicSceneSpaces.map((space) => [space.id, space]))
|
||||
|
||||
const formatArea = (area?: number) => (typeof area === 'number' ? `${area}㎡` : undefined)
|
||||
|
||||
const hallDescription = (space: SgsSceneSpaceMock) => {
|
||||
const category = categoryLabelMap[space.spaceCategory]
|
||||
const area = formatArea(space.area)
|
||||
const boundary = space.boundaryStatus ? boundaryLabelMap[space.boundaryStatus] : undefined
|
||||
const facts = [space.floorLabel, category, area, boundary].filter(Boolean).join(' · ')
|
||||
|
||||
return `${space.name}来自 SGS 前端地图项目“场景设置”的空间管理数据。${facts ? `当前空间信息:${facts}。` : ''}讲解业务据此展示展厅/展区内容,并通过稳定空间、展品与 POI ID 关联到位置预览。`
|
||||
}
|
||||
|
||||
const exhibitDescription = (exhibit: SgsSceneExhibitMock, hall: SgsSceneSpaceMock | undefined) => {
|
||||
const facts = [
|
||||
exhibit.category,
|
||||
exhibit.era,
|
||||
exhibit.origin,
|
||||
exhibit.zoneName
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
return [
|
||||
exhibit.description,
|
||||
facts ? `展陈信息:${facts}。` : '',
|
||||
hall ? `所属空间:${hall.name}(${hall.floorLabel})。` : ''
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
|
||||
const tagsForExhibit = (exhibit: SgsSceneExhibitMock, hall: SgsSceneSpaceMock | undefined) => [
|
||||
'SGS场景设置',
|
||||
exhibit.category,
|
||||
exhibit.zoneName,
|
||||
exhibit.audioUrl ? '源数据含音频地址' : '图文讲解',
|
||||
hall ? categoryLabelMap[hall.spaceCategory] : undefined
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
const toMuseumHall = (space: SgsSceneSpaceMock): MuseumHall => ({
|
||||
const toMockMuseumHall = (space: SgsSceneSpaceMock): MuseumHall => ({
|
||||
id: normalizeContentId('hall', space.id),
|
||||
name: space.name,
|
||||
floorId: space.floorId,
|
||||
floorLabel: space.floorLabel,
|
||||
description: hallDescription(space),
|
||||
description: `${space.name}为显式开发 mock 数据,仅在 VITE_GUIDE_CONTENT_SOURCE_MODE=mock 且开发环境启用。`,
|
||||
image: '',
|
||||
exhibitCount: SGS_SCENE_EXPLAIN_DATASET.exhibits.filter((exhibit) => exhibit.spaceId === space.id).length || space.exhibitCount || 0,
|
||||
area: formatArea(space.area),
|
||||
poiId: space.poiId
|
||||
})
|
||||
|
||||
const toMuseumExhibit = (exhibit: SgsSceneExhibitMock): MuseumExhibit => {
|
||||
const toMockMuseumExhibit = (exhibit: SgsSceneExhibitMock): MuseumExhibit => {
|
||||
const hall = publicSpaceById.get(exhibit.spaceId)
|
||||
|
||||
return {
|
||||
@@ -90,23 +78,168 @@ const toMuseumExhibit = (exhibit: SgsSceneExhibitMock): MuseumExhibit => {
|
||||
floorId: hall?.floorId || SGS_SCENE_EXPLAIN_DATASET.floorId,
|
||||
floorLabel: hall?.floorLabel || SGS_SCENE_EXPLAIN_DATASET.floorLabel,
|
||||
image: '',
|
||||
description: exhibitDescription(exhibit, hall),
|
||||
description: exhibit.description,
|
||||
poiId: exhibit.poiId || hall?.poiId,
|
||||
year: exhibit.era,
|
||||
material: exhibit.origin,
|
||||
size: exhibit.code,
|
||||
tags: tagsForExhibit(exhibit, hall)
|
||||
tags: [
|
||||
'显式开发mock',
|
||||
exhibit.category,
|
||||
exhibit.zoneName,
|
||||
exhibit.audioUrl ? '源数据含音频地址' : '图文讲解',
|
||||
hall ? categoryLabelMap[hall.spaceCategory] : undefined
|
||||
].filter(Boolean) as string[]
|
||||
}
|
||||
}
|
||||
|
||||
export class StaticMuseumContentProvider implements MuseumContentProvider {
|
||||
const unavailableMockAudio = (id: string): MediaAsset => ({
|
||||
id,
|
||||
type: 'audio',
|
||||
available: false,
|
||||
unavailableReason: '显式开发 mock 数据暂无可播放音频。'
|
||||
})
|
||||
|
||||
export class StaticGuideContentProvider implements ExplainContentProvider {
|
||||
private adapterCache: GuideContentDataAdapterResult | null = null
|
||||
private explainAdapterCache: GuideExplainDataAdapterResult | null = null
|
||||
|
||||
constructor(private readonly provider: StaticGuideDataProvider = staticGuideDataProvider) {}
|
||||
|
||||
private async loadExplainAdapter() {
|
||||
if (this.explainAdapterCache) return this.explainAdapterCache
|
||||
if (this.adapterCache) return this.adapterCache
|
||||
|
||||
const dataset = await this.provider.loadExplainDataset()
|
||||
this.explainAdapterCache = createGuideExplainDataAdapter(dataset)
|
||||
return this.explainAdapterCache
|
||||
}
|
||||
|
||||
private async loadAdapter() {
|
||||
if (this.adapterCache) return this.adapterCache
|
||||
|
||||
const dataset = await this.provider.loadDataset()
|
||||
this.adapterCache = createGuideContentDataAdapter(dataset)
|
||||
return this.adapterCache
|
||||
}
|
||||
|
||||
async listExplainExhibits() {
|
||||
const adapter = await this.loadExplainAdapter()
|
||||
return adapter.exhibits
|
||||
}
|
||||
|
||||
async listExhibits() {
|
||||
return SGS_SCENE_EXPLAIN_DATASET.exhibits.map(toMuseumExhibit)
|
||||
const adapter = await this.loadAdapter()
|
||||
return adapter.exhibits
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
return publicSceneSpaces.map(toMuseumHall)
|
||||
const adapter = await this.loadExplainAdapter()
|
||||
return adapter.halls
|
||||
}
|
||||
|
||||
async listTracks() {
|
||||
const adapter = await this.loadExplainAdapter()
|
||||
return adapter.tracks
|
||||
}
|
||||
|
||||
async getMediaById(id: string) {
|
||||
const explainAdapter = await this.loadExplainAdapter()
|
||||
const explainMedia = explainAdapter.mediaAssets.find((media) => media.id === id)
|
||||
if (explainMedia) return explainMedia
|
||||
|
||||
const adapter = await this.loadAdapter()
|
||||
return adapter.mediaAssets.find((media) => media.id === id) || null
|
||||
}
|
||||
|
||||
async getMediaForExplainTrack(trackId: string) {
|
||||
const normalizedTrackId = trackId.replace(/^track-/, '')
|
||||
return this.getMediaById(`media-${normalizedTrackId}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const staticMuseumContentProvider = new StaticMuseumContentProvider()
|
||||
export class RemoteGuideContentProvider implements ExplainContentProvider {
|
||||
private readonly notReady = () => new Error(
|
||||
`远程讲解数据源尚未接入。当前 VITE_GUIDE_CONTENT_SOURCE_MODE=${dataSourceConfig.guideContentMode},请切回 static 或实现 remote provider。`
|
||||
)
|
||||
|
||||
async listExhibits(): Promise<MuseumExhibit[]> {
|
||||
throw this.notReady()
|
||||
}
|
||||
|
||||
async listExplainExhibits(): Promise<MuseumExhibit[]> {
|
||||
throw this.notReady()
|
||||
}
|
||||
|
||||
async listHalls(): Promise<MuseumHall[]> {
|
||||
throw this.notReady()
|
||||
}
|
||||
|
||||
async listTracks(): Promise<ExplainTrack[]> {
|
||||
throw this.notReady()
|
||||
}
|
||||
|
||||
async getMediaById(): Promise<MediaAsset | null> {
|
||||
throw this.notReady()
|
||||
}
|
||||
|
||||
async getMediaForExplainTrack(): Promise<MediaAsset | null> {
|
||||
throw this.notReady()
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplicitMockMuseumContentProvider implements ExplainContentProvider {
|
||||
listExplainExhibits() {
|
||||
return this.listExhibits()
|
||||
}
|
||||
|
||||
async listExhibits() {
|
||||
return SGS_SCENE_EXPLAIN_DATASET.exhibits.map(toMockMuseumExhibit)
|
||||
}
|
||||
|
||||
async listHalls() {
|
||||
return publicSceneSpaces.map(toMockMuseumHall)
|
||||
}
|
||||
|
||||
async listTracks() {
|
||||
const exhibits = await this.listExhibits()
|
||||
return exhibits.map<ExplainTrack>((exhibit) => ({
|
||||
id: `track-${exhibit.id}`,
|
||||
exhibitId: exhibit.id,
|
||||
hallId: exhibit.hallId,
|
||||
title: `${exhibit.name}讲解`,
|
||||
summary: exhibit.description,
|
||||
mediaId: `media-${exhibit.id}`,
|
||||
coverImage: exhibit.image,
|
||||
poiId: exhibit.poiId,
|
||||
floorId: exhibit.floorId,
|
||||
available: false
|
||||
}))
|
||||
}
|
||||
|
||||
async getMediaById(id: string) {
|
||||
return unavailableMockAudio(id)
|
||||
}
|
||||
|
||||
async getMediaForExplainTrack(trackId: string) {
|
||||
return unavailableMockAudio(`media-${trackId.replace(/^track-/, '')}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const createMuseumContentProvider = (): ExplainContentProvider => {
|
||||
if (isGuideContentStaticMode()) {
|
||||
return new StaticGuideContentProvider()
|
||||
}
|
||||
|
||||
if (isGuideContentRemoteMode()) {
|
||||
return new RemoteGuideContentProvider()
|
||||
}
|
||||
|
||||
if (isGuideContentMockMode()) {
|
||||
return new ExplicitMockMuseumContentProvider()
|
||||
}
|
||||
|
||||
return new StaticGuideContentProvider()
|
||||
}
|
||||
|
||||
export const staticMuseumContentProvider = createMuseumContentProvider()
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {
|
||||
NAV_ASSET_BASE_URL
|
||||
NAV_ASSET_BASE_URL,
|
||||
NAV_ROUTE_READINESS
|
||||
} from '@/domain/guideReadiness'
|
||||
import type {
|
||||
GuideRouteReadiness
|
||||
} from '@/domain/museum'
|
||||
|
||||
export interface StaticNavPoiCategoryPayload {
|
||||
topCategory: string
|
||||
@@ -27,18 +31,26 @@ export interface StaticNavManifestFloorModelPayload {
|
||||
floorId: string
|
||||
order: number
|
||||
asset: string
|
||||
sharedModelAsset?: boolean
|
||||
}
|
||||
|
||||
export interface StaticNavManifestPayload {
|
||||
status: string
|
||||
routeReadiness?: GuideRouteReadiness
|
||||
assets: {
|
||||
overviewModel: {
|
||||
asset: string
|
||||
sharedModelAsset?: boolean
|
||||
}
|
||||
floorModels: StaticNavManifestFloorModelPayload[]
|
||||
}
|
||||
data: {
|
||||
floorIndex: string
|
||||
navData?: string
|
||||
routeGraph?: string
|
||||
routeResult?: string
|
||||
routeQualityAudit?: string
|
||||
routeClearanceAudit?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +58,7 @@ export interface StaticNavFloorIndexItemPayload {
|
||||
floorId: string
|
||||
order: number
|
||||
modelAsset: string
|
||||
sharedModelAsset?: boolean
|
||||
poiDataAsset: string
|
||||
poiCount: number
|
||||
}
|
||||
@@ -66,6 +79,83 @@ interface StaticPoiIndexPayload {
|
||||
pois: StaticNavPoiPayload[]
|
||||
}
|
||||
|
||||
export interface StaticNavRouteNodePayload {
|
||||
id: string
|
||||
kind?: string
|
||||
name?: string
|
||||
floorId?: string
|
||||
floor?: string
|
||||
routePosition?: [number, number, number]
|
||||
position?: [number, number, number]
|
||||
displayPosition?: [number, number, number]
|
||||
connectorType?: string | null
|
||||
}
|
||||
|
||||
export interface StaticNavRouteEdgePayload {
|
||||
id: string
|
||||
fromNodeId: string
|
||||
toNodeId: string
|
||||
edgeType?: string
|
||||
floorIds?: string[]
|
||||
weight?: number
|
||||
distance3D?: number
|
||||
}
|
||||
|
||||
export interface StaticNavRouteGraphPayload {
|
||||
schemaVersion: string
|
||||
generatedAt?: string
|
||||
routeNodes: StaticNavRouteNodePayload[]
|
||||
routeEdges: StaticNavRouteEdgePayload[]
|
||||
qualityGate?: {
|
||||
status?: string
|
||||
allowFindPath?: boolean
|
||||
failureCount?: number
|
||||
}
|
||||
stats?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface StaticNavAnchorPayload {
|
||||
id: string
|
||||
poiId: string
|
||||
name?: string
|
||||
displayName?: string
|
||||
floor?: string
|
||||
floorId?: string
|
||||
position?: [number, number, number]
|
||||
navPosition?: [number, number, number]
|
||||
routePosition?: [number, number, number]
|
||||
displayPosition?: [number, number, number]
|
||||
baseRouteNodeId?: string
|
||||
routeNodeId?: string
|
||||
snapStatus?: string
|
||||
connectorType?: string | null
|
||||
}
|
||||
|
||||
export interface StaticNavBuildingPoiPayload {
|
||||
id: string
|
||||
name?: string
|
||||
displayName?: string
|
||||
floor?: string
|
||||
floorId?: string
|
||||
position?: [number, number, number]
|
||||
displayPosition?: [number, number, number]
|
||||
routeAnchorId?: string
|
||||
}
|
||||
|
||||
export interface StaticNavDataPayload {
|
||||
schemaVersion: string
|
||||
generatedAt?: string
|
||||
floors?: unknown[] | Record<string, unknown>
|
||||
walkableAreas?: unknown[]
|
||||
buildingPois: StaticNavBuildingPoiPayload[]
|
||||
navAnchors: StaticNavAnchorPayload[]
|
||||
qualityGate?: {
|
||||
status?: string
|
||||
failureCount?: number
|
||||
}
|
||||
stats?: Record<string, unknown>
|
||||
}
|
||||
|
||||
const normalizeBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, '')
|
||||
|
||||
const parseJsonPayload = <T>(payload: unknown): T => {
|
||||
@@ -104,6 +194,9 @@ export interface StaticNavAssetsProvider {
|
||||
loadFloorIndex(): Promise<StaticNavFloorIndexPayload>
|
||||
loadPoiIndex(): Promise<StaticNavPoiPayload[]>
|
||||
loadFloorPois(relativePath: string): Promise<StaticNavPoiPayload[]>
|
||||
loadNavData(): Promise<StaticNavDataPayload>
|
||||
loadRouteGraph(): Promise<StaticNavRouteGraphPayload>
|
||||
loadRouteReadiness(): Promise<GuideRouteReadiness>
|
||||
}
|
||||
|
||||
export const createStaticNavAssetsProvider = (
|
||||
@@ -116,6 +209,10 @@ export const createStaticNavAssetsProvider = (
|
||||
let manifestRequest: Promise<StaticNavManifestPayload> | null = null
|
||||
let floorIndexCache: StaticNavFloorIndexPayload | null = null
|
||||
let floorIndexRequest: Promise<StaticNavFloorIndexPayload> | null = null
|
||||
let navDataCache: StaticNavDataPayload | null = null
|
||||
let navDataRequest: Promise<StaticNavDataPayload> | null = null
|
||||
let routeGraphCache: StaticNavRouteGraphPayload | null = null
|
||||
let routeGraphRequest: Promise<StaticNavRouteGraphPayload> | null = null
|
||||
const floorPoiCache = new Map<string, StaticNavPoiPayload[]>()
|
||||
const floorPoiRequests = new Map<string, Promise<StaticNavPoiPayload[]>>()
|
||||
|
||||
@@ -205,6 +302,50 @@ export const createStaticNavAssetsProvider = (
|
||||
|
||||
floorPoiRequests.set(cacheKey, request)
|
||||
return request
|
||||
},
|
||||
async loadNavData() {
|
||||
if (navDataCache) return navDataCache
|
||||
if (navDataRequest) return navDataRequest
|
||||
|
||||
navDataRequest = provider.loadManifest()
|
||||
.then((manifest) => requestJson<StaticNavDataPayload>(provider.assetUrl(manifest.data.navData || 'nav_data.json')))
|
||||
.then((data) => {
|
||||
if (data.qualityGate?.status && data.qualityGate.status !== 'pass') {
|
||||
throw new Error('导览 nav_data 质量门未通过')
|
||||
}
|
||||
|
||||
navDataCache = data
|
||||
return navDataCache
|
||||
})
|
||||
.finally(() => {
|
||||
navDataRequest = null
|
||||
})
|
||||
|
||||
return navDataRequest
|
||||
},
|
||||
async loadRouteGraph() {
|
||||
if (routeGraphCache) return routeGraphCache
|
||||
if (routeGraphRequest) return routeGraphRequest
|
||||
|
||||
routeGraphRequest = provider.loadManifest()
|
||||
.then((manifest) => requestJson<StaticNavRouteGraphPayload>(provider.assetUrl(manifest.data.routeGraph || 'route_graph.json')))
|
||||
.then((data) => {
|
||||
if (data.qualityGate?.status && data.qualityGate.status !== 'pass') {
|
||||
throw new Error('导览 route_graph 质量门未通过')
|
||||
}
|
||||
|
||||
routeGraphCache = data
|
||||
return routeGraphCache
|
||||
})
|
||||
.finally(() => {
|
||||
routeGraphRequest = null
|
||||
})
|
||||
|
||||
return routeGraphRequest
|
||||
},
|
||||
async loadRouteReadiness() {
|
||||
const manifest = await provider.loadManifest()
|
||||
return manifest.routeReadiness || NAV_ROUTE_READINESS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user