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 getExhibitById?(id: string): Promise listHalls(): Promise } export interface ExplainContentProvider extends MuseumContentProvider { listExplainExhibits(): Promise searchExplainExhibits?(keyword?: string): Promise listTracks(): Promise getMediaById(id: string): Promise getMediaForExplainTrack(trackId: string): Promise } const normalizeContentId = (prefix: string, value: string | number) => `${prefix}-sgs-${value}` const categoryLabelMap: Record = { 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 { throw this.notReady() } async listExplainExhibits(): Promise { throw this.notReady() } async listHalls(): Promise { throw this.notReady() } async listTracks(): Promise { throw this.notReady() } async getMediaById(): Promise { throw this.notReady() } async getMediaForExplainTrack(): Promise { 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((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()