@@ -158,9 +158,14 @@ export interface BackendCatalogStopItem {
|
||||
hasAudio?: boolean | null
|
||||
audioStatus?: string | null
|
||||
supportedLanguages?: string[] | null
|
||||
hasTextRecord?: boolean | null
|
||||
playTargetType?: string | null
|
||||
playTargetId?: string | number | null
|
||||
audioOptions?: Array<{
|
||||
version?: string | null
|
||||
languageCode?: string | null
|
||||
languageName?: string | null
|
||||
gender?: string | null
|
||||
displayName?: string | null
|
||||
isDefault?: boolean | null
|
||||
}> | null
|
||||
}
|
||||
|
||||
export interface BackendExplainAdapterResult {
|
||||
@@ -339,8 +344,6 @@ export const toCatalogGuideStop = (
|
||||
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'
|
||||
|
||||
@@ -350,8 +353,6 @@ export const toCatalogGuideStop = (
|
||||
hallId: hall?.id,
|
||||
hallName: hall?.name,
|
||||
floorId: stringifyId(source.floorId) || hall?.floorId,
|
||||
targetType: playTargetType,
|
||||
targetId: playTargetId,
|
||||
coverImageUrl: canUseStopImage
|
||||
? normalizeSameOriginPublicUrl(firstText(source.coverImageUrl)) || undefined
|
||||
: undefined,
|
||||
@@ -359,7 +360,6 @@ export const toCatalogGuideStop = (
|
||||
hasAudio: source.hasAudio === true,
|
||||
audioStatus: normalizeCatalogAudioStatus(source.audioStatus),
|
||||
supportedLanguages: normalizeSupportedLanguages(source.supportedLanguages),
|
||||
hasTextRecord: source.hasTextRecord === true,
|
||||
poiId: stringifyId(source.poiId) || undefined,
|
||||
mapX: normalizeNumber(source.mapX),
|
||||
mapY: normalizeNumber(source.mapY),
|
||||
@@ -373,8 +373,27 @@ export const toCatalogGuideStop = (
|
||||
linkedExhibits: (source.linkedExhibits || [])
|
||||
.map(toCatalogLinkedExhibitSummary)
|
||||
.filter(Boolean) as NonNullable<ExplainGuideStop['linkedExhibits']>,
|
||||
playTargetType,
|
||||
playTargetId
|
||||
stopId: id,
|
||||
// Catalog audio metadata is intentionally display-only: playback always
|
||||
// comes from the unified stop detail and therefore has no URL here.
|
||||
audioOptions: (source.audioOptions || [])
|
||||
.map((option) => {
|
||||
const gender = option.gender?.trim().toLowerCase()
|
||||
if (gender !== 'male' && gender !== 'female') return null
|
||||
const languageCode = normalizeGuideAudioLanguage(option.languageCode)
|
||||
const version = option.version?.trim().toLowerCase() === 'extended' ? 'extended' : 'standard'
|
||||
return {
|
||||
channelCode: `${version}.${languageCode}.${gender}`,
|
||||
version,
|
||||
languageCode,
|
||||
languageName: firstText(option.languageName) || undefined,
|
||||
gender,
|
||||
displayName: firstText(option.displayName) || undefined,
|
||||
playUrl: '',
|
||||
isDefault: option.isDefault === true
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as NonNullable<ExplainGuideStop['audioOptions']>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,15 +423,15 @@ export const toCatalogMuseumExhibitFromStop = (
|
||||
audioAvailable: audioReady,
|
||||
audioStatus: stop.audioStatus,
|
||||
supportedLanguages: stop.supportedLanguages,
|
||||
audioHasText: stop.hasTextRecord,
|
||||
// List summaries do not carry authoritative text availability. It is read
|
||||
// from the unified detail together with the full text variants.
|
||||
audioHasText: false,
|
||||
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
|
||||
resolvedStopId: stop.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,54 @@ import {
|
||||
|
||||
export type GuideAudioLanguage = 'zh-CN' | 'yue-HK' | 'en-US'
|
||||
|
||||
export type GuideAudioVersion = 'standard' | 'extended'
|
||||
|
||||
export interface BackendGuideTextVariant {
|
||||
version?: string | null
|
||||
languageCode?: string | null
|
||||
languageName?: string | null
|
||||
text?: string | null
|
||||
textLength?: number | string | null
|
||||
textHash?: string | null
|
||||
}
|
||||
|
||||
export interface BackendGuideAudioTrack {
|
||||
version?: string | null
|
||||
languageCode?: string | null
|
||||
languageName?: string | null
|
||||
gender?: string | null
|
||||
displayName?: string | null
|
||||
playUrl?: string | null
|
||||
duration?: number | string | null
|
||||
format?: string | null
|
||||
isDefault?: boolean | null
|
||||
}
|
||||
|
||||
export interface BackendGuideStopDetail {
|
||||
id?: string | number | null
|
||||
version?: string | null
|
||||
outlineId?: string | number | null
|
||||
poiId?: string | number | null
|
||||
floorId?: string | number | null
|
||||
mapX?: number | string | null
|
||||
mapY?: number | string | null
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
coverImageUrl?: string | null
|
||||
galleryUrls?: string | string[] | null
|
||||
imageStatus?: string | null
|
||||
linkedExhibits?: BackendGuideStopLinkedExhibit[] | null
|
||||
linkedExhibitCount?: number | string | null
|
||||
isSharedStop?: boolean | null
|
||||
recommendedTrackCode?: string | null
|
||||
textVariants?: BackendGuideTextVariant[] | null
|
||||
audioTracks?: BackendGuideAudioTrack[] | null
|
||||
hasText?: boolean | null
|
||||
hasAudio?: boolean | null
|
||||
textVariantCount?: number | string | null
|
||||
audioTrackCount?: number | string | null
|
||||
}
|
||||
|
||||
export interface BackendGuideStopLinkedExhibit {
|
||||
id?: string | number | null
|
||||
name?: string | null
|
||||
@@ -34,6 +82,44 @@ export interface BackendGuideAudioOption {
|
||||
sortOrder?: number | string | null
|
||||
}
|
||||
|
||||
export interface GuideTextVariant {
|
||||
version: GuideAudioVersion
|
||||
languageCode: GuideAudioLanguage
|
||||
languageName?: string
|
||||
text?: string
|
||||
textLength?: number
|
||||
textHash?: string
|
||||
}
|
||||
|
||||
export interface GuideAudioTrack extends MuseumGuideAudioOption {
|
||||
version: GuideAudioVersion
|
||||
}
|
||||
|
||||
export interface GuideStopDetail {
|
||||
id: string
|
||||
version: GuideAudioVersion
|
||||
outlineId?: string
|
||||
poiId?: string
|
||||
floorId?: string
|
||||
mapX?: number
|
||||
mapY?: number
|
||||
name: string
|
||||
description?: string
|
||||
coverImageUrl?: string
|
||||
galleryUrls: string[]
|
||||
imageStatus: 'READY' | 'MISSING' | string
|
||||
linkedExhibits: GuideStopLinkedExhibit[]
|
||||
linkedExhibitCount?: number
|
||||
isSharedStop: boolean
|
||||
recommendedTrackCode?: string
|
||||
textVariants: GuideTextVariant[]
|
||||
audioTracks: GuideAudioTrack[]
|
||||
hasText: boolean
|
||||
hasAudio: boolean
|
||||
textVariantCount: number
|
||||
audioTrackCount: number
|
||||
}
|
||||
|
||||
export interface BackendGuideStopInfo {
|
||||
available?: boolean
|
||||
targetType?: string | null
|
||||
@@ -214,6 +300,57 @@ const normalizeAudioGender = (value: string | null | undefined): GuideAudioGende
|
||||
return normalized === 'male' || normalized === 'female' ? normalized : null
|
||||
}
|
||||
|
||||
const normalizeAudioVersion = (value: string | null | undefined): GuideAudioVersion => (
|
||||
value?.trim().toLowerCase() === 'extended' ? 'extended' : 'standard'
|
||||
)
|
||||
|
||||
const normalizeTextVariants = (items: BackendGuideTextVariant[] | null | undefined): GuideTextVariant[] => (
|
||||
(items || [])
|
||||
.map<GuideTextVariant | null>((item) => {
|
||||
const text = item.text?.trim() || undefined
|
||||
const languageCode = normalizeGuideAudioLanguage(item.languageCode)
|
||||
if (!text) return null
|
||||
return {
|
||||
version: normalizeAudioVersion(item.version),
|
||||
languageCode,
|
||||
languageName: item.languageName?.trim() || undefined,
|
||||
text,
|
||||
textLength: normalizeNumber(item.textLength),
|
||||
textHash: item.textHash?.trim() || undefined
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as GuideTextVariant[]
|
||||
)
|
||||
|
||||
const trackCodeFor = (version: GuideAudioVersion, languageCode: GuideAudioLanguage, gender: GuideAudioGender) => (
|
||||
`${version}.${languageCode}.${gender}`
|
||||
)
|
||||
|
||||
const normalizeAudioTracks = (items: BackendGuideAudioTrack[] | null | undefined): GuideAudioTrack[] => (
|
||||
(items || [])
|
||||
.map<GuideAudioTrack | null>((item) => {
|
||||
const languageCode = normalizeGuideAudioLanguage(item.languageCode)
|
||||
const gender = normalizeAudioGender(item.gender)
|
||||
const playUrl = normalizeSameOriginPublicUrl(item.playUrl)
|
||||
if (!gender || !playUrl) return null
|
||||
const version = normalizeAudioVersion(item.version)
|
||||
return {
|
||||
channelCode: trackCodeFor(version, languageCode, gender),
|
||||
displayName: item.displayName?.trim() || undefined,
|
||||
version,
|
||||
languageCode,
|
||||
languageName: item.languageName?.trim() || undefined,
|
||||
gender,
|
||||
playUrl,
|
||||
duration: normalizeNumber(item.duration),
|
||||
format: item.format?.trim() || undefined,
|
||||
isDefault: item.isDefault === true,
|
||||
sortOrder: undefined
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as GuideAudioTrack[]
|
||||
)
|
||||
|
||||
const normalizeAudioOptions = (items: BackendGuideAudioOption[] | null | undefined): MuseumGuideAudioOption[] => (
|
||||
(items || [])
|
||||
.map<MuseumGuideAudioOption | null>((item) => {
|
||||
@@ -240,6 +377,43 @@ const normalizeAudioOptions = (items: BackendGuideAudioOption[] | null | undefin
|
||||
.filter(Boolean) as MuseumGuideAudioOption[]
|
||||
)
|
||||
|
||||
export const toGuideStopDetail = (source: BackendGuideStopDetail): GuideStopDetail => {
|
||||
const id = stringifyId(source.id)
|
||||
if (!id) throw new Error('讲解点详情缺少 id')
|
||||
const imageStatus = source.imageStatus || 'MISSING'
|
||||
const canUseStopImages = imageStatus === 'READY'
|
||||
const textVariants = normalizeTextVariants(source.textVariants)
|
||||
const audioTracks = normalizeAudioTracks(source.audioTracks)
|
||||
const linkedExhibits = normalizeLinkedExhibits(source.linkedExhibits)
|
||||
|
||||
return {
|
||||
id,
|
||||
version: normalizeAudioVersion(source.version),
|
||||
outlineId: stringifyId(source.outlineId) || undefined,
|
||||
poiId: stringifyId(source.poiId) || undefined,
|
||||
floorId: stringifyId(source.floorId) || undefined,
|
||||
mapX: normalizeNumber(source.mapX),
|
||||
mapY: normalizeNumber(source.mapY),
|
||||
name: source.name?.trim() || '讲解内容',
|
||||
description: source.description?.trim() || undefined,
|
||||
coverImageUrl: canUseStopImages
|
||||
? normalizeSameOriginPublicUrl(source.coverImageUrl) || undefined
|
||||
: undefined,
|
||||
galleryUrls: canUseStopImages ? parseGalleryUrls(source.galleryUrls) : [],
|
||||
imageStatus,
|
||||
linkedExhibits,
|
||||
linkedExhibitCount: normalizeNumber(source.linkedExhibitCount) ?? linkedExhibits.length,
|
||||
isSharedStop: source.isSharedStop === true,
|
||||
recommendedTrackCode: source.recommendedTrackCode?.trim() || undefined,
|
||||
textVariants,
|
||||
audioTracks,
|
||||
hasText: source.hasText === true || textVariants.length > 0,
|
||||
hasAudio: source.hasAudio === true || audioTracks.length > 0,
|
||||
textVariantCount: normalizeNumber(source.textVariantCount) ?? textVariants.length,
|
||||
audioTrackCount: normalizeNumber(source.audioTrackCount) ?? audioTracks.length
|
||||
}
|
||||
}
|
||||
|
||||
const parseGalleryUrls = (value: BackendGuideStopInfo['galleryUrls'] | BackendGuideStopLinkedExhibit['galleryUrls']) => {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) {
|
||||
|
||||
@@ -259,10 +259,7 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const params = new URLSearchParams({
|
||||
includeChildren: 'true',
|
||||
lang: catalogLang()
|
||||
})
|
||||
const params = new URLSearchParams({ lang: catalogLang() })
|
||||
const url = `${resolveAppApiBaseUrl()}/gis/guide/catalog/halls/${encodeURIComponent(normalizedHallId)}/outlines?${params.toString()}`
|
||||
const response = await requestJson<CommonResult<BackendCatalogOutlineItem[]>>(url)
|
||||
const outlines = requireArrayData(response, '讲解单元目录加载失败')
|
||||
@@ -511,9 +508,20 @@ export class BackendExplainContentProvider implements ExplainContentProvider {
|
||||
if (inflight) return inflight
|
||||
|
||||
const promise = (async () => {
|
||||
const outlines = await this.requestCatalogOutlinesByHall(normalizedHallId)
|
||||
const [halls, outlines] = await Promise.all([
|
||||
this.requestCatalogHalls(),
|
||||
this.requestCatalogOutlinesByHall(normalizedHallId)
|
||||
])
|
||||
// The outline response carries authoritative counts. Stops are loaded only after unit selection.
|
||||
const units = toCatalogBusinessUnits(normalizedHallId, outlines, [])
|
||||
const hall = halls.find((item) => item.id === normalizedHallId)
|
||||
if ((hall?.stopCount || 0) > 0 && units.length > 0 && units.every((unit) => unit.guideStopCount === 0)) {
|
||||
console.warn('一级单元与讲解点关联异常,一级单元仅作为兼容入口:', {
|
||||
hallId: normalizedHallId,
|
||||
hallStopCount: hall?.stopCount,
|
||||
unitStopCounts: units.map((unit) => ({ id: unit.id, stopCount: unit.guideStopCount }))
|
||||
})
|
||||
}
|
||||
this.setCached(this.businessUnitCache, key, units)
|
||||
return units
|
||||
})().catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user