接入H5讲解后端数据闭环
This commit is contained in:
228
src/data/adapters/backendExplainDataAdapter.ts
Normal file
228
src/data/adapters/backendExplainDataAdapter.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import type {
|
||||
AudioPlayTargetType,
|
||||
ExplainTrack,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
normalizeSameOriginPublicUrl
|
||||
} from '@/utils/publicUrl'
|
||||
|
||||
export interface BackendGuideContent {
|
||||
id?: string | number | null
|
||||
title?: string | null
|
||||
targetType?: string | null
|
||||
targetId?: string | number | null
|
||||
standardText?: string | null
|
||||
extendedText?: string | null
|
||||
standardAudioUrl?: string | null
|
||||
extendedAudioUrl?: string | null
|
||||
audioUrl?: string | null
|
||||
standardAudioDuration?: number | null
|
||||
extendedAudioDuration?: number | null
|
||||
audioDuration?: number | null
|
||||
}
|
||||
|
||||
export interface BackendExhibit {
|
||||
id?: string | number | null
|
||||
hallId?: string | number | null
|
||||
hallName?: string | null
|
||||
floorId?: string | number | null
|
||||
floorLabel?: string | null
|
||||
poiId?: string | number | null
|
||||
exhibitCode?: string | null
|
||||
name?: string | null
|
||||
scientificName?: string | null
|
||||
category?: string | null
|
||||
era?: string | null
|
||||
origin?: string | null
|
||||
dimensions?: string | null
|
||||
description?: string | null
|
||||
narrationText?: string | null
|
||||
coverImageUrl?: string | null
|
||||
galleryUrls?: string | string[] | null
|
||||
audioUrl?: string | null
|
||||
audioDuration?: number | null
|
||||
playTargetType?: string | null
|
||||
playTargetId?: string | number | null
|
||||
hasAudio?: boolean | null
|
||||
supportedLanguages?: string[] | null
|
||||
audioStatus?: string | null
|
||||
guideContents?: BackendGuideContent[] | null
|
||||
}
|
||||
|
||||
export interface BackendExplainAdapterResult {
|
||||
exhibit: MuseumExhibit
|
||||
track: ExplainTrack
|
||||
media: MediaAsset | null
|
||||
}
|
||||
|
||||
const stringifyId = (value: string | number | null | undefined) => (
|
||||
value === null || typeof value === 'undefined' ? '' : String(value)
|
||||
)
|
||||
|
||||
const firstText = (...values: Array<string | number | null | undefined>) => (
|
||||
values
|
||||
.map((value) => (value === null || typeof value === 'undefined' ? '' : String(value).trim()))
|
||||
.find(Boolean) || ''
|
||||
)
|
||||
|
||||
const parseGalleryUrls = (value: BackendExhibit['galleryUrls']) => {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) return value.map(String).filter(Boolean)
|
||||
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed
|
||||
.flatMap((entry) => String(entry).split(','))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
} catch {
|
||||
// 兼容后端历史逗号拼接字段。
|
||||
}
|
||||
|
||||
return trimmed.split(',').map((entry) => entry.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
const normalizeAudioTargetType = (targetType: string | null | undefined): AudioPlayTargetType | undefined => {
|
||||
const normalized = targetType?.toUpperCase()
|
||||
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : undefined
|
||||
}
|
||||
|
||||
const isAudioReady = (value?: string | null) => (
|
||||
!value || ['READY', 'PUBLISHED', 'AVAILABLE'].includes(value.toUpperCase())
|
||||
)
|
||||
|
||||
const resolveImage = (exhibit: BackendExhibit) => normalizeSameOriginPublicUrl(
|
||||
firstText(exhibit.coverImageUrl, parseGalleryUrls(exhibit.galleryUrls)[0])
|
||||
)
|
||||
|
||||
const resolveGuideAudioUrl = (guide?: BackendGuideContent | null, exhibit?: BackendExhibit) => normalizeSameOriginPublicUrl(
|
||||
firstText(guide?.standardAudioUrl, guide?.audioUrl, guide?.extendedAudioUrl, exhibit?.audioUrl)
|
||||
)
|
||||
|
||||
const resolveGuideAudioDuration = (guide?: BackendGuideContent | null, exhibit?: BackendExhibit) => {
|
||||
const duration = guide?.standardAudioDuration
|
||||
|| guide?.audioDuration
|
||||
|| guide?.extendedAudioDuration
|
||||
|| exhibit?.audioDuration
|
||||
return typeof duration === 'number' && Number.isFinite(duration) && duration > 0 ? duration : undefined
|
||||
}
|
||||
|
||||
const resolveGuideText = (guide?: BackendGuideContent | null, exhibit?: BackendExhibit) => (
|
||||
firstText(guide?.standardText, guide?.extendedText, exhibit?.narrationText, exhibit?.description)
|
||||
)
|
||||
|
||||
const selectPrimaryGuideContent = (exhibit: BackendExhibit) => {
|
||||
const guides = exhibit.guideContents || []
|
||||
return guides.find((guide) => Boolean(resolveGuideAudioUrl(guide, exhibit)))
|
||||
|| guides.find((guide) => Boolean(resolveGuideText(guide, exhibit)))
|
||||
|| guides[0]
|
||||
|| null
|
||||
}
|
||||
|
||||
const buildTags = (exhibit: BackendExhibit) => Array.from(new Set([
|
||||
firstText(exhibit.category),
|
||||
firstText(exhibit.scientificName),
|
||||
firstText(exhibit.exhibitCode),
|
||||
firstText(exhibit.era),
|
||||
firstText(exhibit.origin)
|
||||
].filter(Boolean)))
|
||||
|
||||
export const toBackendMuseumExhibit = (
|
||||
source: BackendExhibit,
|
||||
hall?: MuseumHall | null,
|
||||
options: { includeDetail?: boolean } = {}
|
||||
): MuseumExhibit => {
|
||||
const id = stringifyId(source.id)
|
||||
const guide = options.includeDetail ? selectPrimaryGuideContent(source) : null
|
||||
const guideText = options.includeDetail ? resolveGuideText(guide, source) : firstText(source.narrationText, source.description)
|
||||
const audioUrl = options.includeDetail ? resolveGuideAudioUrl(guide, source) : normalizeSameOriginPublicUrl(firstText(source.audioUrl))
|
||||
const targetType = normalizeAudioTargetType(guide?.targetType || source.playTargetType)
|
||||
const targetId = stringifyId(guide?.targetId || source.playTargetId)
|
||||
const supportedLanguage = source.supportedLanguages?.find((language) => language === 'zh-CN' || language === 'en-US')
|
||||
const hasAudioSummary = source.hasAudio === true && isAudioReady(source.audioStatus)
|
||||
const audioAvailable = Boolean(audioUrl) || hasAudioSummary
|
||||
|
||||
return {
|
||||
id,
|
||||
name: firstText(source.name, source.scientificName, source.exhibitCode, `展品${id}`),
|
||||
hallId: stringifyId(source.hallId) || hall?.id,
|
||||
hallName: firstText(source.hallName, hall?.name),
|
||||
floorId: stringifyId(source.floorId) || hall?.floorId,
|
||||
floorLabel: firstText(source.floorLabel, hall?.floorLabel),
|
||||
image: resolveImage(source) || '/static/exhibit-placeholder.jpg',
|
||||
description: firstText(source.description, source.narrationText, source.scientificName, '该展项暂无讲解文稿。'),
|
||||
poiId: stringifyId(source.poiId) || undefined,
|
||||
year: firstText(source.era) || undefined,
|
||||
material: firstText(source.origin) || undefined,
|
||||
size: firstText(source.exhibitCode, source.dimensions) || undefined,
|
||||
tags: buildTags(source),
|
||||
guideContentId: stringifyId(guide?.id) || undefined,
|
||||
guideTitle: firstText(guide?.title) || undefined,
|
||||
guideText: guideText || undefined,
|
||||
audioUrl: audioUrl || undefined,
|
||||
audioDuration: resolveGuideAudioDuration(guide, source),
|
||||
audioLanguage: supportedLanguage,
|
||||
audioText: options.includeDetail && guideText ? guideText : undefined,
|
||||
audioHasText: options.includeDetail ? Boolean(guideText) : undefined,
|
||||
audioAvailable,
|
||||
audioUnavailableReason: audioAvailable ? undefined : '该讲解暂无已发布音频,当前提供图文讲解。',
|
||||
playTargetType: targetType,
|
||||
playTargetId: targetId || undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const toBackendExplainTrack = (exhibit: MuseumExhibit): ExplainTrack => ({
|
||||
id: `track-${exhibit.guideContentId || exhibit.id}`,
|
||||
exhibitId: exhibit.id,
|
||||
hallId: exhibit.hallId,
|
||||
title: exhibit.guideTitle || `${exhibit.name}讲解`,
|
||||
summary: exhibit.guideText || exhibit.description,
|
||||
mediaId: exhibit.audioUrl ? `media-${exhibit.guideContentId || exhibit.id}` : undefined,
|
||||
coverImage: exhibit.image,
|
||||
poiId: exhibit.poiId,
|
||||
floorId: exhibit.floorId,
|
||||
available: Boolean(exhibit.audioUrl) || exhibit.audioAvailable === true,
|
||||
playTargetType: exhibit.playTargetType,
|
||||
playTargetId: exhibit.playTargetId
|
||||
})
|
||||
|
||||
export const toBackendMediaAsset = (exhibit: MuseumExhibit): MediaAsset | null => {
|
||||
if (!exhibit.audioUrl) return null
|
||||
|
||||
return {
|
||||
id: `media-${exhibit.guideContentId || exhibit.id}`,
|
||||
type: 'audio',
|
||||
url: exhibit.audioUrl,
|
||||
duration: exhibit.audioDuration,
|
||||
language: exhibit.audioLanguage,
|
||||
available: true
|
||||
}
|
||||
}
|
||||
|
||||
export const toBackendHall = (
|
||||
hallId: string,
|
||||
exhibits: MuseumExhibit[],
|
||||
fallback?: MuseumHall | null
|
||||
): MuseumHall => {
|
||||
const first = exhibits[0]
|
||||
return {
|
||||
id: hallId,
|
||||
name: firstText(first?.hallName, fallback?.name, '展厅'),
|
||||
floorId: firstText(first?.floorId, fallback?.floorId) || undefined,
|
||||
floorLabel: firstText(first?.floorLabel, fallback?.floorLabel, '楼层待补充'),
|
||||
description: fallback?.description || '该展厅讲解内容来自后端展品接口。',
|
||||
image: fallback?.image || '/static/hall-placeholder.jpg',
|
||||
exhibitCount: exhibits.length,
|
||||
area: fallback?.area,
|
||||
poiId: fallback?.poiId,
|
||||
location: fallback?.location
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user