850 lines
31 KiB
TypeScript
850 lines
31 KiB
TypeScript
import type {
|
|
ExplainBusinessUnit,
|
|
ExplainGuideStop,
|
|
ExplainTrack,
|
|
GuideLocationResolution,
|
|
GuideLocationResolutionStatus,
|
|
MediaAsset,
|
|
MuseumExhibit,
|
|
MuseumHall,
|
|
AudioPlayTargetType
|
|
} from '@/domain/museum'
|
|
import {
|
|
formatNavFloorLabel
|
|
} from '@/data/adapters/navAssetsAdapter'
|
|
import {
|
|
normalizeSameOriginPublicUrl
|
|
} from '@/utils/publicUrl'
|
|
import {
|
|
getExplainUnitPreviewImage
|
|
} from '@/utils/explainUnitPreviews'
|
|
import type {
|
|
GuideStaticContentPayload,
|
|
GuideStaticDataset,
|
|
GuideStaticExplainDataset,
|
|
GuideStaticExplainNavigationDataset,
|
|
GuideStaticExhibitPayload,
|
|
GuideStaticHallPayload,
|
|
GuideStaticOutlinePayload,
|
|
GuideStaticPoiBridgeEntryPayload,
|
|
GuideStaticPoiPayload,
|
|
GuideStaticStopPayload,
|
|
} from '@/data/providers/staticGuideDataProvider'
|
|
|
|
export interface GuideContentDataAdapterResult {
|
|
halls: MuseumHall[]
|
|
exhibits: MuseumExhibit[]
|
|
tracks: ExplainTrack[]
|
|
mediaAssets: MediaAsset[]
|
|
}
|
|
|
|
export interface GuideExplainNavigationAdapterResult {
|
|
halls: MuseumHall[]
|
|
guideStops: ExplainGuideStop[]
|
|
businessUnitsByHallId: Record<string, ExplainBusinessUnit[]>
|
|
}
|
|
|
|
export interface GuideExplainDataAdapterResult {
|
|
halls: MuseumHall[]
|
|
exhibits: MuseumExhibit[]
|
|
tracks: ExplainTrack[]
|
|
mediaAssets: MediaAsset[]
|
|
guideStops: ExplainGuideStop[]
|
|
businessUnitsByHallId: Record<string, ExplainBusinessUnit[]>
|
|
exhibitByStopId: Record<string, MuseumExhibit>
|
|
}
|
|
|
|
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
|
|
}
|
|
})
|
|
|
|
const toExplainBusinessUnitsByHallId = (
|
|
stops: ExplainGuideStop[],
|
|
topOutlines: Array<{ id: string; name: string; hallId: string; sort?: number }> = []
|
|
) => {
|
|
const unitsByHallId: Record<string, ExplainBusinessUnit[]> = {}
|
|
const stopGroupsByHall = new Map<string, Map<string, ExplainGuideStop[]>>()
|
|
const unitNameByKey = new Map<string, string>()
|
|
const unitOrderByKey = new Map<string, number>()
|
|
|
|
topOutlines.forEach((outline) => {
|
|
const hallGroups = stopGroupsByHall.get(outline.hallId) || new Map<string, ExplainGuideStop[]>()
|
|
if (!hallGroups.has(outline.id)) {
|
|
hallGroups.set(outline.id, [])
|
|
}
|
|
stopGroupsByHall.set(outline.hallId, hallGroups)
|
|
unitNameByKey.set(`${outline.hallId}:${outline.id}`, outline.name)
|
|
unitOrderByKey.set(`${outline.hallId}:${outline.id}`, outline.sort ?? 0)
|
|
})
|
|
|
|
stops
|
|
.slice()
|
|
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
|
.forEach((stop) => {
|
|
const hallId = stop.hallId || ''
|
|
if (!hallId) return
|
|
|
|
const unitId = stop.outlineId || `ungrouped-${hallId}`
|
|
const unitName = stop.outlineName || '其他讲解'
|
|
const hallGroups = stopGroupsByHall.get(hallId) || new Map<string, ExplainGuideStop[]>()
|
|
const groupStops = hallGroups.get(unitId) || []
|
|
groupStops.push(stop)
|
|
hallGroups.set(unitId, groupStops)
|
|
stopGroupsByHall.set(hallId, hallGroups)
|
|
if (!unitNameByKey.has(`${hallId}:${unitId}`)) {
|
|
unitNameByKey.set(`${hallId}:${unitId}`, unitName)
|
|
}
|
|
if (!unitOrderByKey.has(`${hallId}:${unitId}`)) {
|
|
unitOrderByKey.set(`${hallId}:${unitId}`, stop.sort ?? 0)
|
|
}
|
|
})
|
|
|
|
stopGroupsByHall.forEach((groupMap, hallId) => {
|
|
unitsByHallId[hallId] = Array.from(groupMap.entries())
|
|
.map(([unitId, groupStops]) => ({
|
|
id: unitId,
|
|
name: unitNameByKey.get(`${hallId}:${unitId}`) || '其他讲解',
|
|
hallId,
|
|
previewImageUrl: getExplainUnitPreviewImage(unitId),
|
|
guideStopCount: groupStops.length,
|
|
stops: groupStops
|
|
}))
|
|
.sort((a, b) => (
|
|
(unitOrderByKey.get(`${hallId}:${a.id}`) ?? 0) - (unitOrderByKey.get(`${hallId}:${b.id}`) ?? 0)
|
|
))
|
|
})
|
|
|
|
return unitsByHallId
|
|
}
|
|
|
|
const createExplainNavigationAdapter = (
|
|
dataset: GuideStaticExplainNavigationDataset,
|
|
hallGuideCounts = new Map<string, number>(),
|
|
poiBridge: {
|
|
hallToNavPoiId?: Record<string, GuideStaticPoiBridgeEntryPayload>
|
|
sgsPoiToNavPoiId?: Record<string, GuideStaticPoiBridgeEntryPayload>
|
|
} = {}
|
|
): GuideExplainNavigationAdapterResult => {
|
|
const visibleHalls = visibleHallsFrom(dataset.halls)
|
|
const visibleOutlines = visibleOutlinesFrom(dataset.outlines)
|
|
const visibleStops = visibleStopsFrom(dataset.guideStops)
|
|
const hallById = new Map(visibleHalls.map((hall) => [stringifyId(hall.id), hall]))
|
|
const outlineById = new Map(visibleOutlines.map((outline) => [stringifyId(outline.id), outline]))
|
|
const hallBridgeById = poiBridge.hallToNavPoiId || {}
|
|
const sgsPoiBridgeById = 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 findTopOutlineForStop = (stop: GuideStaticStopPayload | undefined) => {
|
|
let current = outlineById.get(stringifyId(stop?.outlineId))
|
|
let guard = 0
|
|
|
|
while (current && guard < 16) {
|
|
const parentId = stringifyId(current.parentId)
|
|
if (parentId && hallById.has(parentId)) {
|
|
return current
|
|
}
|
|
|
|
current = parentId ? outlineById.get(parentId) : undefined
|
|
guard += 1
|
|
}
|
|
|
|
return outlineById.get(stringifyId(stop?.outlineId))
|
|
}
|
|
|
|
const topOutlines = visibleOutlines.flatMap((outline) => {
|
|
const id = stringifyId(outline.id)
|
|
const parentId = stringifyId(outline.parentId)
|
|
if (!id || !parentId || !hallById.has(parentId)) return []
|
|
|
|
return [{
|
|
id,
|
|
name: outline.name || `业务单元 ${id}`,
|
|
hallId: parentId,
|
|
sort: typeof outline.sort === 'number' ? outline.sort : undefined
|
|
}]
|
|
})
|
|
|
|
const resolveHallLocation = (hall: GuideStaticHallPayload | undefined) => {
|
|
const hallId = stringifyId(hall?.id)
|
|
const sourcePoiId = stringifyId(hall?.poiId)
|
|
const bridgedHall = hallId ? toLocationResolution(hallBridgeById[hallId], 'exact', sourcePoiId) : undefined
|
|
if (bridgedHall) return bridgedHall
|
|
|
|
return sourcePoiId ? toLocationResolution(sgsPoiBridgeById[sourcePoiId], 'exact', sourcePoiId) : undefined
|
|
}
|
|
|
|
const halls = toHallModels(visibleHalls, hallGuideCounts, hallBridgeById, sgsPoiBridgeById)
|
|
const guideStops: ExplainGuideStop[] = visibleStops.map((stop) => {
|
|
const topOutline = findTopOutlineForStop(stop)
|
|
const hall = findHallForStop(stop)
|
|
const location = resolveHallLocation(hall)
|
|
const floorId = location?.floorId || formatFloorId(hall, undefined) || stop.floorCode || stringifyId(stop.floorId)
|
|
|
|
return {
|
|
id: stringifyId(stop.id) || '',
|
|
name: firstText(stop.name) || `讲解点 ${stringifyId(stop.id) || ''}`,
|
|
hallId: stringifyId(hall?.id),
|
|
hallName: hall?.name || undefined,
|
|
floorId,
|
|
targetType: 'STOP',
|
|
targetId: stringifyId(stop.id) || '',
|
|
coverImageUrl: resolveImage(stop.coverImageUrl, hall?.coverImageUrl, hall?.galleryUrls),
|
|
description: firstText(stop.name),
|
|
hasAudio: Boolean(stop.audioUrl),
|
|
poiId: location?.poiId,
|
|
outlineId: stringifyId(topOutline?.id),
|
|
outlineName: topOutline?.name || undefined,
|
|
sort: typeof stop.sort === 'number' ? stop.sort : undefined
|
|
}
|
|
})
|
|
|
|
return {
|
|
halls,
|
|
guideStops,
|
|
businessUnitsByHallId: toExplainBusinessUnitsByHallId(guideStops, topOutlines)
|
|
}
|
|
}
|
|
|
|
export const createGuideExplainNavigationAdapter = (
|
|
dataset: GuideStaticExplainNavigationDataset
|
|
): GuideExplainNavigationAdapterResult => createExplainNavigationAdapter(dataset)
|
|
|
|
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 findTopOutlineForStop = (stop: GuideStaticStopPayload | undefined) => {
|
|
let current = outlineById.get(stringifyId(stop?.outlineId))
|
|
let guard = 0
|
|
|
|
while (current && guard < 16) {
|
|
const parentId = stringifyId(current.parentId)
|
|
if (parentId && hallById.has(parentId)) {
|
|
return current
|
|
}
|
|
|
|
current = parentId ? outlineById.get(parentId) : undefined
|
|
guard += 1
|
|
}
|
|
|
|
return outlineById.get(stringifyId(stop?.outlineId))
|
|
}
|
|
|
|
const topOutlines = visibleOutlines.flatMap((outline) => {
|
|
const id = stringifyId(outline.id)
|
|
const parentId = stringifyId(outline.parentId)
|
|
if (!id || !parentId || !hallById.has(parentId)) return []
|
|
|
|
return [{
|
|
id,
|
|
name: outline.name || `业务单元 ${id}`,
|
|
hallId: parentId,
|
|
sort: typeof outline.sort === 'number' ? outline.sort : 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
|
|
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 guideStops: ExplainGuideStop[] = visibleStops.map((stop) => {
|
|
const topOutline = findTopOutlineForStop(stop)
|
|
const hall = findHallForStop(stop)
|
|
const preciseLocation = resolveSgsPoiLocation([stop.poiId], 'exact')
|
|
const location = preciseLocation || resolveHallLocation(hall, 'hallFallback')
|
|
const floorId = location?.floorId || formatFloorId(hall, undefined) || stop.floorCode || stringifyId(stop.floorId)
|
|
const stopGuide = visibleGuides.find((guide) => (
|
|
guide.targetType?.toUpperCase() === 'STOP'
|
|
&& stringifyId(guide.targetId) === stringifyId(stop.id)
|
|
))
|
|
|
|
return {
|
|
id: stringifyId(stop.id) || '',
|
|
name: firstText(stop.name, stopGuide?.title) || `讲解点 ${stringifyId(stop.id) || ''}`,
|
|
hallId: stringifyId(hall?.id),
|
|
hallName: hall?.name || undefined,
|
|
floorId,
|
|
targetType: 'STOP',
|
|
targetId: stringifyId(stop.id) || '',
|
|
coverImageUrl: resolveImage(stop.coverImageUrl, stopGuide?.mediaGallery, hall?.coverImageUrl, hall?.galleryUrls),
|
|
description: firstText(stopGuide?.standardText, stopGuide?.extendedText, stop.name),
|
|
hasAudio: stopGuide ? Boolean(primaryAudioUrl(stopGuide)) : Boolean(stop.audioUrl),
|
|
poiId: location?.poiId,
|
|
outlineId: stringifyId(topOutline?.id),
|
|
outlineName: topOutline?.name || undefined,
|
|
sort: typeof stop.sort === 'number' ? stop.sort : undefined
|
|
}
|
|
})
|
|
const businessUnitsByHallId = toExplainBusinessUnitsByHallId(guideStops, topOutlines)
|
|
|
|
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 exhibitByStopId = exhibits.reduce<Record<string, MuseumExhibit>>((result, exhibit) => {
|
|
if (exhibit.playTargetType === 'STOP' && exhibit.playTargetId) {
|
|
result[exhibit.playTargetId] = exhibit
|
|
}
|
|
return result
|
|
}, {})
|
|
|
|
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,
|
|
guideStops,
|
|
businessUnitsByHallId,
|
|
exhibitByStopId
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|