Files
frontend-miniapp/src/data/providers/staticMuseumContentProvider.ts
2026-06-30 17:22:20 +08:00

251 lines
7.5 KiB
TypeScript

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,
type SgsSceneSpaceMock
} from '@/data/mock/sgsSceneExplainData'
import {
BackendExplainContentProvider
} from '@/data/providers/backendExplainContentProvider'
export interface MuseumContentProvider {
listExhibits(): Promise<MuseumExhibit[]>
getExhibitById?(id: string): Promise<MuseumExhibit | null>
listHalls(): Promise<MuseumHall[]>
}
export interface ExplainContentProvider extends MuseumContentProvider {
listExplainExhibits(): Promise<MuseumExhibit[]>
searchExplainExhibits?(keyword?: string): 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> = {
EXHIBITION: '展陈空间',
EXPERIENCE: '体验空间',
SERVICE: '服务空间',
COMMERCIAL: '商业空间',
EDUCATION: '教育空间',
CIRCULATION: '公共交通空间',
BACK_OFFICE: '后勤空间'
}
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 toMockMuseumHall = (space: SgsSceneSpaceMock): MuseumHall => ({
id: normalizeContentId('hall', space.id),
name: space.name,
floorId: space.floorId,
floorLabel: space.floorLabel,
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 toMockMuseumExhibit = (exhibit: SgsSceneExhibitMock): MuseumExhibit => {
const hall = publicSpaceById.get(exhibit.spaceId)
return {
id: normalizeContentId('exhibit', exhibit.id),
name: exhibit.name,
hallId: hall ? normalizeContentId('hall', hall.id) : undefined,
hallName: hall?.name || exhibit.spaceName,
floorId: hall?.floorId || SGS_SCENE_EXPLAIN_DATASET.floorId,
floorLabel: hall?.floorLabel || SGS_SCENE_EXPLAIN_DATASET.floorLabel,
image: '',
description: exhibit.description,
poiId: exhibit.poiId || hall?.poiId,
year: exhibit.era,
material: exhibit.origin,
size: exhibit.code,
tags: [
'显式开发mock',
exhibit.category,
exhibit.zoneName,
exhibit.audioUrl ? '源数据含音频地址' : '图文讲解',
hall ? categoryLabelMap[hall.spaceCategory] : undefined
].filter(Boolean) as string[]
}
}
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() {
const adapter = await this.loadAdapter()
return adapter.exhibits
}
async listHalls() {
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 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 BackendExplainContentProvider(new StaticGuideContentProvider())
}
if (isGuideContentMockMode()) {
return new ExplicitMockMuseumContentProvider()
}
return new StaticGuideContentProvider()
}
export const staticMuseumContentProvider = createMuseumContentProvider()