chore: freeze guide and explain update
This commit is contained in:
593
src/data/adapters/guideDataAdapter.ts
Normal file
593
src/data/adapters/guideDataAdapter.ts
Normal file
@@ -0,0 +1,593 @@
|
||||
import type {
|
||||
ExplainTrack,
|
||||
GuideLocationResolution,
|
||||
GuideLocationResolutionStatus,
|
||||
MediaAsset,
|
||||
MuseumExhibit,
|
||||
MuseumHall,
|
||||
AudioPlayTargetType
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
formatNavFloorLabel
|
||||
} from '@/data/adapters/navAssetsAdapter'
|
||||
import {
|
||||
normalizeSameOriginPublicUrl
|
||||
} from '@/utils/publicUrl'
|
||||
import type {
|
||||
GuideStaticContentPayload,
|
||||
GuideStaticDataset,
|
||||
GuideStaticExplainDataset,
|
||||
GuideStaticExhibitPayload,
|
||||
GuideStaticHallPayload,
|
||||
GuideStaticOutlinePayload,
|
||||
GuideStaticPoiBridgeEntryPayload,
|
||||
GuideStaticPoiPayload,
|
||||
GuideStaticStopPayload,
|
||||
} from '@/data/providers/staticGuideDataProvider'
|
||||
|
||||
export interface GuideContentDataAdapterResult {
|
||||
halls: MuseumHall[]
|
||||
exhibits: MuseumExhibit[]
|
||||
tracks: ExplainTrack[]
|
||||
mediaAssets: MediaAsset[]
|
||||
}
|
||||
|
||||
export interface GuideExplainDataAdapterResult {
|
||||
halls: MuseumHall[]
|
||||
exhibits: MuseumExhibit[]
|
||||
tracks: ExplainTrack[]
|
||||
mediaAssets: MediaAsset[]
|
||||
}
|
||||
|
||||
const stringifyId = (value: string | number | null | undefined) => (
|
||||
value === null || typeof value === 'undefined' ? undefined : String(value)
|
||||
)
|
||||
|
||||
const isTruthyDeleted = (value: unknown) => value === true || value === 1 || value === '1'
|
||||
const isEnabledStatus = (value: unknown) => value === undefined || value === null || value === true || value === 1 || value === '1'
|
||||
const isVisibleRecord = (record: { deleted?: unknown; status?: unknown }) => (
|
||||
!isTruthyDeleted(record.deleted) && isEnabledStatus(record.status)
|
||||
)
|
||||
|
||||
const compact = (values: Array<string | null | undefined>) => values
|
||||
.map((value) => value?.trim())
|
||||
.filter(Boolean) as string[]
|
||||
|
||||
const firstText = (...values: Array<string | null | undefined>) => (
|
||||
compact(values).find(Boolean) || ''
|
||||
)
|
||||
|
||||
const parseJsonArray = (value: string | null | undefined): string[] => {
|
||||
if (!value) return []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
|
||||
: []
|
||||
} catch {
|
||||
return value.split(',').map((item) => item.trim()).filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveImage = (...values: Array<string | null | undefined>) => normalizeSameOriginPublicUrl(firstText(
|
||||
...values,
|
||||
...values.flatMap(parseJsonArray)
|
||||
))
|
||||
|
||||
const formatFloorId = (hall: GuideStaticHallPayload | undefined, poi?: GuideStaticPoiPayload) => (
|
||||
firstText(
|
||||
hall?.floorCode,
|
||||
stringifyId(hall?.floorId),
|
||||
stringifyId(poi?.floorId)
|
||||
) || undefined
|
||||
)
|
||||
|
||||
const formatFloorLabel = (floorId: string | undefined) => (
|
||||
floorId
|
||||
? floorId.startsWith('L') ? formatNavFloorLabel(floorId) : floorId
|
||||
: undefined
|
||||
)
|
||||
|
||||
const formatArea = (area?: number | null) => (
|
||||
typeof area === 'number' && Number.isFinite(area) ? `${area}㎡` : undefined
|
||||
)
|
||||
|
||||
const locationActionText = (status: GuideLocationResolutionStatus) => {
|
||||
if (status === 'hallFallback') return '查看所属展厅'
|
||||
if (status === 'candidate') return '候选位置'
|
||||
if (status === 'exact') return '查看三维位置'
|
||||
return '暂无位置'
|
||||
}
|
||||
|
||||
const locationNote = (status: GuideLocationResolutionStatus) => {
|
||||
if (status === 'hallFallback') return '当前展示所属展厅位置,展品精确点位待补充。'
|
||||
if (status === 'candidate') return '当前仅有候选点位,需人工确认后再用于正式位置预览。'
|
||||
if (status === 'exact') return '当前支持三维位置预览,馆内路线规划待开放。'
|
||||
return '暂无可用三维位置数据。'
|
||||
}
|
||||
|
||||
const toLocationResolution = (
|
||||
entry: GuideStaticPoiBridgeEntryPayload | undefined,
|
||||
status: Exclude<GuideLocationResolutionStatus, 'unavailable'>,
|
||||
sourcePoiId?: string
|
||||
): GuideLocationResolution | undefined => {
|
||||
const poiId = stringifyId(entry?.navPoiId)
|
||||
if (!poiId) return undefined
|
||||
|
||||
const floorId = stringifyId(entry?.navFloorId)
|
||||
return {
|
||||
status,
|
||||
poiId,
|
||||
sourcePoiId: stringifyId(entry?.sourcePoiId) || sourcePoiId,
|
||||
actionText: locationActionText(status),
|
||||
previewOnly: true,
|
||||
confidence: entry?.confidence || undefined,
|
||||
method: entry?.method || undefined,
|
||||
floorId,
|
||||
floorLabel: formatFloorLabel(floorId),
|
||||
note: locationNote(status)
|
||||
}
|
||||
}
|
||||
|
||||
const guideTargetKey = (guide: GuideStaticContentPayload) => {
|
||||
const targetType = guide.targetType?.toUpperCase()
|
||||
const targetId = stringifyId(guide.targetId)
|
||||
return targetType && targetId ? `${targetType}:${targetId}` : ''
|
||||
}
|
||||
|
||||
const normalizeAudioTargetType = (targetType: string | null | undefined): AudioPlayTargetType | undefined => {
|
||||
const normalizedType = targetType?.toUpperCase()
|
||||
if (normalizedType === 'STOP' || normalizedType === 'ITEM') {
|
||||
return normalizedType
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const resolveAudioPlayTarget = (
|
||||
guide: GuideStaticContentPayload | undefined,
|
||||
exhibitId?: string
|
||||
) => {
|
||||
const guideTargetType = normalizeAudioTargetType(guide?.targetType)
|
||||
const guideTargetId = stringifyId(guide?.targetId)
|
||||
|
||||
if (guideTargetType && guideTargetId) {
|
||||
return {
|
||||
playTargetType: guideTargetType,
|
||||
playTargetId: guideTargetId
|
||||
}
|
||||
}
|
||||
|
||||
return exhibitId
|
||||
? {
|
||||
playTargetType: 'ITEM' as AudioPlayTargetType,
|
||||
playTargetId: exhibitId
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
const primaryAudioUrl = (guide: GuideStaticContentPayload) => normalizeSameOriginPublicUrl(firstText(
|
||||
guide.standardAudioUrl,
|
||||
guide.extendedAudioUrl,
|
||||
guide.audioUrl,
|
||||
guide.standardAudioUrlEn,
|
||||
guide.extendedAudioUrlEn
|
||||
))
|
||||
|
||||
const primaryAudioDuration = (guide: GuideStaticContentPayload) => (
|
||||
guide.standardAudioDuration
|
||||
|| guide.extendedAudioDuration
|
||||
|| guide.audioDuration
|
||||
|| guide.standardAudioDurationEn
|
||||
|| guide.extendedAudioDurationEn
|
||||
|| undefined
|
||||
)
|
||||
|
||||
const primaryGuideText = (guide: GuideStaticContentPayload | undefined, exhibit: GuideStaticExhibitPayload) => firstText(
|
||||
guide?.standardText,
|
||||
guide?.extendedText,
|
||||
exhibit.description,
|
||||
exhibit.narrationText,
|
||||
guide?.standardTextEn,
|
||||
guide?.extendedTextEn,
|
||||
exhibit.descriptionEn
|
||||
)
|
||||
|
||||
const toGuideLevelTags = (guide: GuideStaticContentPayload | undefined, exhibit: GuideStaticExhibitPayload) => {
|
||||
const parsedTags = parseJsonArray(guide?.aiTags)
|
||||
return compact([
|
||||
exhibit.category,
|
||||
exhibit.exhibitLevel,
|
||||
...parsedTags,
|
||||
guide ? '真实讲解词' : '展品档案'
|
||||
])
|
||||
}
|
||||
|
||||
const visibleHallsFrom = (halls: GuideStaticHallPayload[]) => halls.filter(isVisibleRecord)
|
||||
const visibleOutlinesFrom = (outlines: GuideStaticOutlinePayload[]) => outlines.filter((outline) => !isTruthyDeleted(outline.deleted))
|
||||
const visibleStopsFrom = (stops: GuideStaticStopPayload[]) => stops.filter((stop) => !isTruthyDeleted(stop.deleted))
|
||||
const visibleGuidesFrom = (guides: GuideStaticContentPayload[]) => guides.filter(isVisibleRecord)
|
||||
|
||||
const toHallModels = (
|
||||
sourceHalls: GuideStaticHallPayload[],
|
||||
hallExhibitCounts: Map<string, number>,
|
||||
hallBridgeById: Record<string, GuideStaticPoiBridgeEntryPayload>,
|
||||
sgsPoiBridgeById: Record<string, GuideStaticPoiBridgeEntryPayload>
|
||||
) => sourceHalls.map((hall) => {
|
||||
const id = stringifyId(hall.id) || ''
|
||||
const sourcePoiId = stringifyId(hall.poiId)
|
||||
const bridgedHall = id ? toLocationResolution(hallBridgeById[id], 'exact', sourcePoiId) : undefined
|
||||
const location = bridgedHall || (() => {
|
||||
if (!sourcePoiId) return undefined
|
||||
return toLocationResolution(sgsPoiBridgeById[sourcePoiId], 'exact', sourcePoiId)
|
||||
})()
|
||||
const floorId = location?.floorId || formatFloorId(hall)
|
||||
const description = firstText(hall.description, hall.narrationText, hall.subtitle)
|
||||
|
||||
return {
|
||||
id,
|
||||
name: hall.name || hall.hallCode || `展厅 ${id}`,
|
||||
floorId,
|
||||
floorLabel: formatFloorLabel(floorId),
|
||||
description,
|
||||
image: resolveImage(hall.coverImageUrl, hall.galleryUrls),
|
||||
exhibitCount: hallExhibitCounts.get(id) || hall.exhibitCount || 0,
|
||||
area: formatArea(hall.areaSqm),
|
||||
poiId: location?.poiId,
|
||||
location
|
||||
}
|
||||
})
|
||||
|
||||
export const createGuideExplainDataAdapter = (dataset: GuideStaticExplainDataset): GuideExplainDataAdapterResult => {
|
||||
const visibleHalls = visibleHallsFrom(dataset.halls)
|
||||
const visibleOutlines = visibleOutlinesFrom(dataset.outlines)
|
||||
const visibleStops = visibleStopsFrom(dataset.guideStops)
|
||||
const visibleGuides = visibleGuidesFrom(dataset.guideContents)
|
||||
|
||||
const hallById = new Map(visibleHalls.map((hall) => [stringifyId(hall.id), hall]))
|
||||
const outlineById = new Map(visibleOutlines.map((outline) => [stringifyId(outline.id), outline]))
|
||||
const stopById = new Map(visibleStops.map((stop) => [stringifyId(stop.id), stop]))
|
||||
const hallBridgeById = dataset.poiBridge?.hallToNavPoiId || {}
|
||||
const sgsPoiBridgeById = dataset.poiBridge?.sgsPoiToNavPoiId || {}
|
||||
|
||||
const findHallForOutline = (outline: GuideStaticOutlinePayload | undefined) => {
|
||||
let current = outline
|
||||
let guard = 0
|
||||
|
||||
while (current && guard < 16) {
|
||||
const parentId = stringifyId(current.parentId)
|
||||
if (parentId && hallById.has(parentId)) {
|
||||
return hallById.get(parentId)
|
||||
}
|
||||
|
||||
current = parentId ? outlineById.get(parentId) : undefined
|
||||
guard += 1
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const findHallForStop = (stop: GuideStaticStopPayload | undefined) => (
|
||||
findHallForOutline(outlineById.get(stringifyId(stop?.outlineId)))
|
||||
)
|
||||
|
||||
const resolveHallLocation = (
|
||||
hall: GuideStaticHallPayload | undefined,
|
||||
status: Exclude<GuideLocationResolutionStatus, 'unavailable'>
|
||||
) => {
|
||||
const hallId = stringifyId(hall?.id)
|
||||
const sourcePoiId = stringifyId(hall?.poiId)
|
||||
const bridgedHall = hallId ? toLocationResolution(hallBridgeById[hallId], status, sourcePoiId) : undefined
|
||||
if (bridgedHall) return bridgedHall
|
||||
|
||||
return sourcePoiId ? toLocationResolution(sgsPoiBridgeById[sourcePoiId], status, sourcePoiId) : undefined
|
||||
}
|
||||
|
||||
const resolveSgsPoiLocation = (
|
||||
sourcePoiIds: Array<string | number | null | undefined>,
|
||||
status: Exclude<GuideLocationResolutionStatus, 'unavailable'>
|
||||
) => {
|
||||
for (const rawId of sourcePoiIds) {
|
||||
const sourcePoiId = stringifyId(rawId)
|
||||
if (!sourcePoiId) continue
|
||||
|
||||
const location = toLocationResolution(sgsPoiBridgeById[sourcePoiId], status, sourcePoiId)
|
||||
if (location) return location
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const hallGuideCounts = new Map<string, number>()
|
||||
visibleGuides.forEach((guide) => {
|
||||
const stop = guide.targetType?.toUpperCase() === 'STOP'
|
||||
? stopById.get(stringifyId(guide.targetId))
|
||||
: undefined
|
||||
const hall = findHallForStop(stop)
|
||||
const hallId = stringifyId(hall?.id)
|
||||
if (hallId) {
|
||||
hallGuideCounts.set(hallId, (hallGuideCounts.get(hallId) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
const halls = toHallModels(visibleHalls, hallGuideCounts, hallBridgeById, sgsPoiBridgeById)
|
||||
|
||||
const mediaAssets: MediaAsset[] = visibleGuides.map((guide) => {
|
||||
const id = `media-${stringifyId(guide.id) || 'unknown'}`
|
||||
const url = primaryAudioUrl(guide)
|
||||
const title = guide.title || '该讲解'
|
||||
|
||||
return {
|
||||
id,
|
||||
type: 'audio',
|
||||
url: url || undefined,
|
||||
duration: primaryAudioDuration(guide),
|
||||
language: 'zh-CN',
|
||||
available: Boolean(url),
|
||||
unavailableReason: url ? undefined : `${title}暂无已发布音频,当前提供图文讲解。`
|
||||
}
|
||||
})
|
||||
const mediaByGuideId = new Map(mediaAssets.map((media) => [media.id.replace(/^media-/, ''), media]))
|
||||
|
||||
const exhibits: MuseumExhibit[] = visibleGuides.map((guide) => {
|
||||
const id = stringifyId(guide.exhibitId) || `guide-${stringifyId(guide.id) || ''}`
|
||||
const guideId = stringifyId(guide.id) || ''
|
||||
const stop = guide.targetType?.toUpperCase() === 'STOP'
|
||||
? stopById.get(stringifyId(guide.targetId))
|
||||
: undefined
|
||||
const outline = outlineById.get(stringifyId(stop?.outlineId))
|
||||
const hall = findHallForStop(stop)
|
||||
const sourcePoiId = stringifyId(guide.poiId || stop?.poiId || hall?.poiId)
|
||||
const preciseLocation = resolveSgsPoiLocation([guide.poiId, stop?.poiId], 'exact')
|
||||
const location = preciseLocation || resolveHallLocation(hall, 'hallFallback')
|
||||
const floorId = location?.floorId || formatFloorId(hall, undefined) || stop?.floorCode || stringifyId(stop?.floorId)
|
||||
const audioUrl = primaryAudioUrl(guide)
|
||||
const audioPlayTarget = resolveAudioPlayTarget(guide, id)
|
||||
const title = firstText(guide.title, stop?.name)?.replace(/\s*讲解\s*$/, '') || `讲解 ${guideId || id}`
|
||||
|
||||
return {
|
||||
id,
|
||||
name: title,
|
||||
hallId: stringifyId(hall?.id),
|
||||
hallName: hall?.name || undefined,
|
||||
floorId,
|
||||
floorLabel: formatFloorLabel(floorId),
|
||||
image: resolveImage(stop?.coverImageUrl, guide.mediaGallery, hall?.coverImageUrl, hall?.galleryUrls),
|
||||
description: firstText(guide.standardText, guide.extendedText, stop?.name, guide.standardTextEn, guide.extendedTextEn),
|
||||
poiId: location?.poiId,
|
||||
sourcePoiId,
|
||||
location,
|
||||
tags: compact([
|
||||
hall?.name,
|
||||
outline?.name,
|
||||
guide.contentType || undefined,
|
||||
guide ? '真实讲解词' : undefined
|
||||
]),
|
||||
guideContentId: guideId,
|
||||
guideTitle: guide.title || undefined,
|
||||
guideText: firstText(guide.standardText, guide.extendedText, guide.standardTextEn, guide.extendedTextEn) || undefined,
|
||||
audioUrl: audioUrl || undefined,
|
||||
audioDuration: primaryAudioDuration(guide),
|
||||
audioAvailable: Boolean(audioUrl),
|
||||
...audioPlayTarget
|
||||
}
|
||||
})
|
||||
|
||||
const tracks: ExplainTrack[] = visibleGuides.map((guide) => {
|
||||
const id = stringifyId(guide.id) || ''
|
||||
const exhibitId = stringifyId(guide.exhibitId) || `guide-${id}`
|
||||
const stop = guide.targetType?.toUpperCase() === 'STOP'
|
||||
? stopById.get(stringifyId(guide.targetId))
|
||||
: undefined
|
||||
const hall = findHallForStop(stop)
|
||||
const sourcePoiId = stringifyId(guide.poiId || stop?.poiId || hall?.poiId)
|
||||
const preciseLocation = resolveSgsPoiLocation([guide.poiId, stop?.poiId], 'exact')
|
||||
const location = preciseLocation || resolveHallLocation(hall, 'hallFallback')
|
||||
const floorId = location?.floorId || formatFloorId(hall, undefined) || stop?.floorCode || stringifyId(stop?.floorId)
|
||||
const media = mediaByGuideId.get(id)
|
||||
const audioPlayTarget = resolveAudioPlayTarget(guide, exhibitId)
|
||||
|
||||
return {
|
||||
id: `track-${id}`,
|
||||
exhibitId,
|
||||
hallId: stringifyId(hall?.id),
|
||||
title: guide.title || stop?.name || `讲解 ${id}`,
|
||||
summary: firstText(guide.standardText, guide.extendedText, stop?.name),
|
||||
mediaId: media?.id,
|
||||
coverImage: resolveImage(stop?.coverImageUrl, guide.mediaGallery, hall?.coverImageUrl, hall?.galleryUrls),
|
||||
poiId: location?.poiId,
|
||||
sourcePoiId,
|
||||
location,
|
||||
floorId,
|
||||
available: media?.available === true,
|
||||
...audioPlayTarget
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
halls,
|
||||
exhibits,
|
||||
tracks,
|
||||
mediaAssets
|
||||
}
|
||||
}
|
||||
|
||||
export const createGuideContentDataAdapter = (dataset: GuideStaticDataset): GuideContentDataAdapterResult => {
|
||||
const visibleHalls = visibleHallsFrom(dataset.halls)
|
||||
const visibleOutlines = visibleOutlinesFrom(dataset.outlines)
|
||||
const visibleStops = visibleStopsFrom(dataset.guideStops)
|
||||
const visibleExhibits = dataset.exhibits.filter(isVisibleRecord)
|
||||
const visibleGuides = visibleGuidesFrom(dataset.guideContents)
|
||||
|
||||
const hallById = new Map(visibleHalls.map((hall) => [stringifyId(hall.id), hall]))
|
||||
const outlineById = new Map(visibleOutlines.map((outline) => [stringifyId(outline.id), outline]))
|
||||
const stopById = new Map(visibleStops.map((stop) => [stringifyId(stop.id), stop]))
|
||||
const poiById = new Map(dataset.pois.map((poi) => [stringifyId(poi.id), poi]))
|
||||
const exhibitById = new Map(visibleExhibits.map((exhibit) => [stringifyId(exhibit.id), exhibit]))
|
||||
const hallBridgeById = dataset.poiBridge?.hallToNavPoiId || {}
|
||||
const sgsPoiBridgeById = dataset.poiBridge?.sgsPoiToNavPoiId || {}
|
||||
|
||||
const resolveSgsPoiLocation = (
|
||||
sourcePoiIds: Array<string | number | null | undefined>,
|
||||
status: Exclude<GuideLocationResolutionStatus, 'unavailable'>
|
||||
) => {
|
||||
for (const rawId of sourcePoiIds) {
|
||||
const sourcePoiId = stringifyId(rawId)
|
||||
if (!sourcePoiId) continue
|
||||
|
||||
const location = toLocationResolution(sgsPoiBridgeById[sourcePoiId], status, sourcePoiId)
|
||||
if (location) return location
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const resolveHallLocation = (
|
||||
hall: GuideStaticHallPayload | undefined,
|
||||
status: Exclude<GuideLocationResolutionStatus, 'unavailable'>
|
||||
) => {
|
||||
const hallId = stringifyId(hall?.id)
|
||||
const sourcePoiId = stringifyId(hall?.poiId)
|
||||
const bridgedHall = hallId ? toLocationResolution(hallBridgeById[hallId], status, sourcePoiId) : undefined
|
||||
|
||||
return bridgedHall || resolveSgsPoiLocation([sourcePoiId], status)
|
||||
}
|
||||
|
||||
const guideByExhibitId = new Map<string, GuideStaticContentPayload>()
|
||||
const guideByTarget = new Map<string, GuideStaticContentPayload>()
|
||||
for (const guide of visibleGuides) {
|
||||
const key = guideTargetKey(guide)
|
||||
if (key && !guideByTarget.has(key)) {
|
||||
guideByTarget.set(key, guide)
|
||||
}
|
||||
|
||||
const exhibitId = stringifyId(guide.exhibitId)
|
||||
if (exhibitId && !guideByExhibitId.has(exhibitId)) {
|
||||
guideByExhibitId.set(exhibitId, guide)
|
||||
}
|
||||
|
||||
if (guide.targetType?.toUpperCase() === 'ITEM') {
|
||||
const targetId = stringifyId(guide.targetId)
|
||||
if (targetId && !guideByExhibitId.has(targetId)) {
|
||||
guideByExhibitId.set(targetId, guide)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hallExhibitCounts = new Map<string, number>()
|
||||
for (const exhibit of visibleExhibits) {
|
||||
const hallId = stringifyId(exhibit.hallId)
|
||||
if (hallId) {
|
||||
hallExhibitCounts.set(hallId, (hallExhibitCounts.get(hallId) || 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const halls = toHallModels(visibleHalls, hallExhibitCounts, hallBridgeById, sgsPoiBridgeById)
|
||||
|
||||
const exhibits: MuseumExhibit[] = visibleExhibits.map((exhibit) => {
|
||||
const id = stringifyId(exhibit.id) || ''
|
||||
const hall = hallById.get(stringifyId(exhibit.hallId))
|
||||
const outline = outlineById.get(stringifyId(exhibit.outlineId))
|
||||
const stop = stopById.get(stringifyId(exhibit.stopId))
|
||||
const itemGuide = guideByExhibitId.get(id)
|
||||
const stopGuide = stop ? guideByTarget.get(`STOP:${stringifyId(stop.id)}`) : undefined
|
||||
const guide = itemGuide || stopGuide
|
||||
const audioUrl = guide ? primaryAudioUrl(guide) : ''
|
||||
const audioPlayTarget = resolveAudioPlayTarget(guide, id)
|
||||
const sourcePoiId = stringifyId(exhibit.poiId || stop?.poiId || guide?.poiId || hall?.poiId)
|
||||
const sourcePoi = sourcePoiId ? poiById.get(sourcePoiId) : undefined
|
||||
const preciseLocation = resolveSgsPoiLocation([exhibit.poiId, stop?.poiId, guide?.poiId], 'exact')
|
||||
const location = preciseLocation || resolveHallLocation(hall, 'hallFallback')
|
||||
const floorId = location?.floorId || formatFloorId(hall, sourcePoi)
|
||||
const description = primaryGuideText(guide, exhibit)
|
||||
|
||||
return {
|
||||
id,
|
||||
name: exhibit.name || exhibit.exhibitCode || guide?.title || `展品 ${id}`,
|
||||
hallId: stringifyId(exhibit.hallId),
|
||||
hallName: hall?.name || undefined,
|
||||
floorId,
|
||||
floorLabel: formatFloorLabel(floorId),
|
||||
image: resolveImage(exhibit.coverImageUrl, exhibit.galleryUrls, guide?.mediaGallery),
|
||||
description,
|
||||
poiId: location?.poiId,
|
||||
sourcePoiId,
|
||||
location,
|
||||
year: exhibit.era || undefined,
|
||||
material: exhibit.origin || undefined,
|
||||
size: exhibit.exhibitCode || exhibit.dimensions || undefined,
|
||||
tags: compact([
|
||||
hall?.name,
|
||||
outline?.name,
|
||||
stop?.name,
|
||||
...toGuideLevelTags(guide, exhibit)
|
||||
]),
|
||||
guideContentId: stringifyId(guide?.id),
|
||||
guideTitle: guide?.title || undefined,
|
||||
guideText: description || undefined,
|
||||
audioUrl: audioUrl || undefined,
|
||||
audioDuration: guide ? primaryAudioDuration(guide) : undefined,
|
||||
audioAvailable: Boolean(audioUrl),
|
||||
...audioPlayTarget
|
||||
}
|
||||
})
|
||||
|
||||
const mediaAssets: MediaAsset[] = visibleGuides.map((guide) => {
|
||||
const id = `media-${stringifyId(guide.id) || 'unknown'}`
|
||||
const url = primaryAudioUrl(guide)
|
||||
const title = guide.title || '该讲解'
|
||||
|
||||
return {
|
||||
id,
|
||||
type: 'audio',
|
||||
url: url || undefined,
|
||||
duration: primaryAudioDuration(guide),
|
||||
language: 'zh-CN',
|
||||
available: Boolean(url),
|
||||
unavailableReason: url ? undefined : `${title}暂无已发布音频,当前提供图文讲解。`
|
||||
}
|
||||
})
|
||||
|
||||
const mediaByGuideId = new Map(mediaAssets.map((media) => [media.id.replace(/^media-/, ''), media]))
|
||||
const tracks: ExplainTrack[] = visibleGuides.map((guide) => {
|
||||
const id = stringifyId(guide.id) || ''
|
||||
const targetType = guide.targetType?.toUpperCase()
|
||||
const targetId = stringifyId(guide.targetId)
|
||||
const exhibit = stringifyId(guide.exhibitId)
|
||||
? exhibitById.get(stringifyId(guide.exhibitId))
|
||||
: targetType === 'ITEM' && targetId ? exhibitById.get(targetId) : undefined
|
||||
const stop = targetType === 'STOP' && targetId ? stopById.get(targetId) : undefined
|
||||
const hall = exhibit ? hallById.get(stringifyId(exhibit.hallId)) : undefined
|
||||
const sourcePoiId = stringifyId(exhibit?.poiId || stop?.poiId || guide.poiId || hall?.poiId)
|
||||
const sourcePoi = sourcePoiId ? poiById.get(sourcePoiId) : undefined
|
||||
const preciseLocation = resolveSgsPoiLocation([exhibit?.poiId, stop?.poiId, guide.poiId], 'exact')
|
||||
const location = preciseLocation || resolveHallLocation(hall, 'hallFallback')
|
||||
const floorId = location?.floorId || formatFloorId(hall, sourcePoi)
|
||||
const media = mediaByGuideId.get(id)
|
||||
const audioPlayTarget = resolveAudioPlayTarget(guide, stringifyId(exhibit?.id))
|
||||
|
||||
return {
|
||||
id: `track-${id}`,
|
||||
exhibitId: stringifyId(exhibit?.id),
|
||||
hallId: stringifyId(hall?.id),
|
||||
title: guide.title || exhibit?.name || stop?.name || `讲解 ${id}`,
|
||||
summary: firstText(guide.standardText, guide.extendedText, exhibit?.description, stop?.name),
|
||||
mediaId: media?.id,
|
||||
coverImage: resolveImage(exhibit?.coverImageUrl, exhibit?.galleryUrls, stop?.coverImageUrl, guide.mediaGallery),
|
||||
poiId: location?.poiId,
|
||||
sourcePoiId,
|
||||
location,
|
||||
floorId,
|
||||
available: media?.available === true,
|
||||
...audioPlayTarget
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
halls,
|
||||
exhibits,
|
||||
tracks,
|
||||
mediaAssets
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user