适配H5讲解统一详情接口
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-08-05 14:52:36 +08:00
parent c287ef3a2a
commit 2edb6bd9f2
25 changed files with 946 additions and 1148 deletions

View File

@@ -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
}
}

View File

@@ -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)) {

View File

@@ -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) => {