251 lines
7.5 KiB
TypeScript
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()
|