This commit is contained in:
@@ -61,12 +61,22 @@ export interface BackendExhibit {
|
||||
export interface BackendHall {
|
||||
id?: string | number | null
|
||||
name?: string | null
|
||||
poiId?: string | number | null
|
||||
hallCode?: string | null
|
||||
nameEn?: string | null
|
||||
subtitle?: string | null
|
||||
description?: string | null
|
||||
coverImageUrl?: string | null
|
||||
floorId?: string | number | null
|
||||
floorCode?: string | null
|
||||
exhibitCount?: number | null
|
||||
outlineCount?: number | null
|
||||
stopCount?: number | null
|
||||
linkedExhibitCount?: number | null
|
||||
audioReadyStopCount?: number | null
|
||||
hasAudio?: boolean | null
|
||||
audioStatus?: string | null
|
||||
supportedLanguages?: string[] | null
|
||||
}
|
||||
|
||||
export interface BackendGuideStop {
|
||||
@@ -87,6 +97,61 @@ export interface BackendGuideStop {
|
||||
sort?: number | null
|
||||
}
|
||||
|
||||
export interface BackendCatalogHallItem extends BackendHall {
|
||||
mapId?: string | number | null
|
||||
areaSqm?: string | number | null
|
||||
sortOrder?: number | null
|
||||
}
|
||||
|
||||
export interface BackendCatalogOutlineItem {
|
||||
id?: string | number | null
|
||||
parentId?: string | number | null
|
||||
hallId?: string | number | null
|
||||
name?: string | null
|
||||
code?: string | null
|
||||
description?: string | null
|
||||
sort?: number | null
|
||||
level?: number | null
|
||||
children?: BackendCatalogOutlineItem[] | null
|
||||
stopCount?: number | null
|
||||
linkedExhibitCount?: number | null
|
||||
audioReadyStopCount?: number | null
|
||||
hasAudio?: boolean | null
|
||||
audioStatus?: string | null
|
||||
supportedLanguages?: string[] | null
|
||||
}
|
||||
|
||||
export interface BackendCatalogLinkedExhibitItem {
|
||||
id?: string | number | null
|
||||
name?: string | null
|
||||
nameEn?: string | null
|
||||
code?: string | null
|
||||
exhibitCode?: string | null
|
||||
coverImageUrl?: string | null
|
||||
sortOrder?: number | null
|
||||
}
|
||||
|
||||
export interface BackendCatalogStopItem {
|
||||
stopId?: string | number | null
|
||||
id?: string | number | null
|
||||
outlineId?: string | number | null
|
||||
name?: string | null
|
||||
guideLevel?: string | null
|
||||
description?: string | null
|
||||
sort?: number | null
|
||||
coverImageUrl?: string | null
|
||||
imageStatus?: string | null
|
||||
linkedExhibitCount?: number | null
|
||||
isSharedStop?: boolean | null
|
||||
linkedExhibits?: BackendCatalogLinkedExhibitItem[] | null
|
||||
hasAudio?: boolean | null
|
||||
audioStatus?: string | null
|
||||
supportedLanguages?: string[] | null
|
||||
hasTextRecord?: boolean | null
|
||||
playTargetType?: string | null
|
||||
playTargetId?: string | number | null
|
||||
}
|
||||
|
||||
export interface BackendExplainAdapterResult {
|
||||
exhibit: MuseumExhibit
|
||||
track: ExplainTrack
|
||||
@@ -130,6 +195,15 @@ const normalizeAudioTargetType = (targetType: string | null | undefined): AudioP
|
||||
return normalized === 'ITEM' || normalized === 'STOP' ? normalized : undefined
|
||||
}
|
||||
|
||||
const normalizeNumber = (value: number | string | null | undefined) => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const numeric = Number(value)
|
||||
return Number.isFinite(numeric) ? numeric : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const isAudioReady = (value?: string | null) => (
|
||||
!value || ['READY', 'PUBLISHED', 'AVAILABLE'].includes(value.toUpperCase())
|
||||
)
|
||||
@@ -170,6 +244,230 @@ const buildTags = (exhibit: BackendExhibit) => Array.from(new Set([
|
||||
firstText(exhibit.origin)
|
||||
].filter(Boolean)))
|
||||
|
||||
const normalizeSupportedLanguages = (languages: string[] | null | undefined) => (
|
||||
Array.isArray(languages) ? languages.map(String).filter(Boolean) : []
|
||||
)
|
||||
|
||||
const normalizeCatalogAudioStatus = (status?: string | null) => (
|
||||
firstText(status) || 'MISSING'
|
||||
)
|
||||
|
||||
export const toCatalogHall = (
|
||||
source: BackendCatalogHallItem,
|
||||
fallback?: MuseumHall | null
|
||||
): MuseumHall => {
|
||||
const id = stringifyId(source.id) || fallback?.id || firstText(source.hallCode, source.name)
|
||||
const name = firstText(source.name, fallback?.name, source.hallCode, '展厅')
|
||||
const area = firstText(source.areaSqm)
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
floorId: stringifyId(source.floorId) || fallback?.floorId,
|
||||
floorLabel: firstText(source.floorCode, fallback?.floorLabel) || undefined,
|
||||
description: firstText(source.description, source.subtitle, fallback?.description, '该展厅暂无介绍。'),
|
||||
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || fallback?.image || HALL_PLACEHOLDER_IMAGE,
|
||||
exhibitCount: normalizeNumber(source.exhibitCount) ?? fallback?.exhibitCount ?? 0,
|
||||
outlineCount: normalizeNumber(source.outlineCount),
|
||||
stopCount: normalizeNumber(source.stopCount),
|
||||
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||
audioReadyStopCount: normalizeNumber(source.audioReadyStopCount),
|
||||
hasAudio: source.hasAudio === true,
|
||||
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||
area: area ? `${area}㎡` : fallback?.area,
|
||||
poiId: stringifyId(source.poiId) || fallback?.poiId,
|
||||
location: fallback?.location
|
||||
}
|
||||
}
|
||||
|
||||
export const toCatalogLinkedExhibit = (
|
||||
source: BackendCatalogLinkedExhibitItem,
|
||||
hall?: MuseumHall | null
|
||||
): MuseumExhibit | null => {
|
||||
const id = stringifyId(source.id)
|
||||
if (!id) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
name: firstText(source.name, source.exhibitCode, source.code, `展品${id}`),
|
||||
hallId: hall?.id,
|
||||
hallName: hall?.name,
|
||||
floorId: hall?.floorId,
|
||||
floorLabel: hall?.floorLabel,
|
||||
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined,
|
||||
size: firstText(source.exhibitCode, source.code) || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const toCatalogLinkedExhibitSummary = (source: BackendCatalogLinkedExhibitItem) => {
|
||||
const id = stringifyId(source.id)
|
||||
if (!id) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
name: firstText(source.name, source.exhibitCode, source.code, `展品 ${id}`),
|
||||
nameEn: firstText(source.nameEn) || undefined,
|
||||
exhibitCode: firstText(source.exhibitCode, source.code) || undefined,
|
||||
coverImageUrl: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const toCatalogGuideStop = (
|
||||
source: BackendCatalogStopItem,
|
||||
hall?: MuseumHall | null,
|
||||
outline?: Pick<ExplainBusinessUnit, 'id' | 'name'> | null
|
||||
): ExplainGuideStop | null => {
|
||||
const id = stringifyId(source.stopId) || stringifyId(source.id)
|
||||
if (!id) return null
|
||||
|
||||
const playTargetType = normalizeAudioTargetType(source.playTargetType) || 'STOP'
|
||||
const playTargetId = stringifyId(source.playTargetId) || id
|
||||
const imageStatus = firstText(source.imageStatus) || 'MISSING'
|
||||
const canUseStopImage = imageStatus === 'READY'
|
||||
|
||||
return {
|
||||
id,
|
||||
name: firstText(source.name, `讲解点${id}`),
|
||||
hallId: hall?.id,
|
||||
hallName: hall?.name,
|
||||
floorId: hall?.floorId,
|
||||
targetType: playTargetType,
|
||||
targetId: playTargetId,
|
||||
coverImageUrl: canUseStopImage
|
||||
? normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined
|
||||
: undefined,
|
||||
description: firstText(source.description) || undefined,
|
||||
hasAudio: source.hasAudio === true,
|
||||
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||
hasTextRecord: source.hasTextRecord === true,
|
||||
outlineId: stringifyId(source.outlineId) || outline?.id,
|
||||
outlineName: outline?.name,
|
||||
sort: typeof source.sort === 'number' ? source.sort : undefined,
|
||||
guideLevel: firstText(source.guideLevel) || undefined,
|
||||
imageStatus,
|
||||
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||
isSharedStop: source.isSharedStop === true,
|
||||
linkedExhibits: (source.linkedExhibits || [])
|
||||
.map(toCatalogLinkedExhibitSummary)
|
||||
.filter(Boolean) as NonNullable<ExplainGuideStop['linkedExhibits']>,
|
||||
playTargetType,
|
||||
playTargetId
|
||||
}
|
||||
}
|
||||
|
||||
export const toCatalogMuseumExhibitFromStop = (
|
||||
stop: ExplainGuideStop,
|
||||
hall?: MuseumHall | null
|
||||
): MuseumExhibit => {
|
||||
const linkedPrimary = stop.linkedExhibits?.[0]
|
||||
const audioReady = stop.audioStatus === 'READY'
|
||||
|
||||
return {
|
||||
id: stop.id,
|
||||
name: stop.name,
|
||||
hallId: stop.hallId || hall?.id,
|
||||
hallName: stop.hallName || hall?.name,
|
||||
floorId: stop.floorId || hall?.floorId,
|
||||
floorLabel: hall?.floorLabel,
|
||||
image: stop.coverImageUrl || undefined,
|
||||
description: stop.description || linkedPrimary?.name || '该讲解点暂无简介。',
|
||||
tags: [stop.outlineName, linkedPrimary?.exhibitCode].filter(Boolean) as string[],
|
||||
guideTitle: stop.name,
|
||||
guideText: stop.description,
|
||||
audioAvailable: audioReady,
|
||||
audioStatus: stop.audioStatus,
|
||||
supportedLanguages: stop.supportedLanguages,
|
||||
audioHasText: stop.hasTextRecord,
|
||||
imageStatus: stop.imageStatus,
|
||||
linkedExhibitCount: stop.linkedExhibitCount,
|
||||
isSharedStop: stop.isSharedStop,
|
||||
linkedExhibits: stop.linkedExhibits,
|
||||
stopInfoAvailable: true,
|
||||
resolvedStopId: stop.id,
|
||||
playTargetType: stop.playTargetType || stop.targetType || 'STOP',
|
||||
playTargetId: stop.playTargetId || stop.targetId || stop.id
|
||||
}
|
||||
}
|
||||
|
||||
const outlineKey = (outlineId?: string | null) => outlineId || '__unassigned__'
|
||||
|
||||
export const flattenCatalogOutlines = (
|
||||
outlines: BackendCatalogOutlineItem[]
|
||||
): BackendCatalogOutlineItem[] => {
|
||||
const flattened: BackendCatalogOutlineItem[] = []
|
||||
|
||||
const visit = (items: BackendCatalogOutlineItem[]) => {
|
||||
items
|
||||
.slice()
|
||||
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
.forEach((item) => {
|
||||
flattened.push(item)
|
||||
if (Array.isArray(item.children) && item.children.length) {
|
||||
visit(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
visit(outlines)
|
||||
return flattened
|
||||
}
|
||||
|
||||
export const toCatalogBusinessUnits = (
|
||||
hallId: string,
|
||||
outlines: BackendCatalogOutlineItem[],
|
||||
stops: ExplainGuideStop[]
|
||||
): ExplainBusinessUnit[] => {
|
||||
const flatOutlines = flattenCatalogOutlines(outlines)
|
||||
const stopMap = new Map<string, ExplainGuideStop[]>()
|
||||
stops.forEach((stop) => {
|
||||
const key = outlineKey(stop.outlineId)
|
||||
stopMap.set(key, [...(stopMap.get(key) || []), stop])
|
||||
})
|
||||
|
||||
const outlineChildren = new Set(
|
||||
flatOutlines
|
||||
.map((item) => stringifyId(item.parentId))
|
||||
.filter((parentId) => parentId && parentId !== hallId)
|
||||
)
|
||||
|
||||
const units = flatOutlines
|
||||
.map<ExplainBusinessUnit | null>((outline) => {
|
||||
const id = stringifyId(outline.id)
|
||||
if (!id) return null
|
||||
|
||||
const directStops = (stopMap.get(id) || [])
|
||||
.slice()
|
||||
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
const hasChildOutlines = outlineChildren.has(id)
|
||||
const rawStopCount = normalizeNumber(outline.stopCount) ?? 0
|
||||
if (!directStops.length && hasChildOutlines) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
name: firstText(outline.name, outline.code, `业务单元${id}`),
|
||||
hallId: stringifyId(outline.hallId) || hallId,
|
||||
parentId: stringifyId(outline.parentId) || undefined,
|
||||
code: firstText(outline.code) || undefined,
|
||||
description: firstText(outline.description) || undefined,
|
||||
sort: typeof outline.sort === 'number' ? outline.sort : undefined,
|
||||
level: typeof outline.level === 'number' ? outline.level : undefined,
|
||||
guideStopCount: directStops.length || rawStopCount,
|
||||
linkedExhibitCount: normalizeNumber(outline.linkedExhibitCount),
|
||||
audioReadyStopCount: normalizeNumber(outline.audioReadyStopCount),
|
||||
hasAudio: outline.hasAudio === true,
|
||||
audioStatus: normalizeCatalogAudioStatus(outline.audioStatus),
|
||||
supportedLanguages: normalizeSupportedLanguages(outline.supportedLanguages),
|
||||
stops: directStops
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as ExplainBusinessUnit[]
|
||||
|
||||
if (units.length) return units
|
||||
return groupGuideStopsByOutline(hallId, stops)
|
||||
}
|
||||
|
||||
export const toBackendMuseumExhibit = (
|
||||
source: BackendExhibit,
|
||||
hall?: MuseumHall | null,
|
||||
@@ -277,15 +575,22 @@ export const toBackendHallFromList = (source: BackendHall, fallback?: MuseumHall
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
floorId: fallback?.floorId,
|
||||
floorLabel: fallback?.floorLabel,
|
||||
floorId: stringifyId(source.floorId) || fallback?.floorId,
|
||||
floorLabel: firstText(source.floorCode, fallback?.floorLabel) || undefined,
|
||||
description: firstText(source.description, source.subtitle, fallback?.description, '该展厅暂无介绍。'),
|
||||
image: normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || fallback?.image || HALL_PLACEHOLDER_IMAGE,
|
||||
exhibitCount: typeof source.exhibitCount === 'number'
|
||||
? source.exhibitCount
|
||||
: fallback?.exhibitCount || 0,
|
||||
outlineCount: normalizeNumber(source.outlineCount),
|
||||
stopCount: normalizeNumber(source.stopCount),
|
||||
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount),
|
||||
audioReadyStopCount: normalizeNumber(source.audioReadyStopCount),
|
||||
hasAudio: source.hasAudio === true,
|
||||
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||
area: fallback?.area,
|
||||
poiId: fallback?.poiId,
|
||||
poiId: stringifyId(source.poiId) || fallback?.poiId,
|
||||
location: fallback?.location
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user