This commit is contained in:
@@ -4,8 +4,8 @@ import type {
|
||||
ExplainGuideStop,
|
||||
ExplainGuideStopPage,
|
||||
ExplainTrack,
|
||||
GuideAudioGender,
|
||||
MediaAsset,
|
||||
MuseumAudioOption,
|
||||
MuseumExhibit,
|
||||
MuseumHall,
|
||||
SearchIndexItem
|
||||
@@ -32,6 +32,10 @@ import {
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
import {
|
||||
resolveGuideAudioLanguages,
|
||||
resolveGuideAudioOption
|
||||
} from '@/domain/guideAudioOptions'
|
||||
|
||||
export interface ExplainAudioSelection {
|
||||
exhibit: MuseumExhibit
|
||||
@@ -116,14 +120,16 @@ export class ExplainUseCase {
|
||||
return {
|
||||
targetType: request.targetType,
|
||||
targetId: request.targetId,
|
||||
lang
|
||||
lang,
|
||||
lightweight: true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetType: 'ITEM',
|
||||
targetId: request.exhibitId,
|
||||
lang
|
||||
lang,
|
||||
lightweight: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,36 +162,14 @@ export class ExplainUseCase {
|
||||
return '中文'
|
||||
}
|
||||
|
||||
private audioOptionForLanguage(
|
||||
exhibit: MuseumExhibit,
|
||||
language: AudioLanguage
|
||||
): MuseumAudioOption | undefined {
|
||||
const options = exhibit.audioOptions?.filter((option) => (
|
||||
option.languageCode === language && Boolean(option.audioUrl?.trim())
|
||||
)) || []
|
||||
|
||||
return options.find((option) => option.isDefault) || options[0]
|
||||
}
|
||||
|
||||
private applyLanguageVariant(exhibit: MuseumExhibit, language: AudioLanguage): MuseumExhibit {
|
||||
const variant = exhibit.audioVariants?.[language]
|
||||
const audioOption = this.audioOptionForLanguage(exhibit, language)
|
||||
const hasAudioOptions = Boolean(exhibit.audioOptions?.length)
|
||||
const mandarinText = exhibit.audioVariants?.['zh-CN']?.text
|
||||
|| exhibit.guideText
|
||||
|| exhibit.description
|
||||
const supportedLanguages = exhibit.supportedLanguages?.length
|
||||
? exhibit.supportedLanguages
|
||||
: exhibit.audioOptions?.length
|
||||
? Array.from(new Set(exhibit.audioOptions.map((option) => option.languageCode)))
|
||||
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
|
||||
const legacyPlayable = (variant?.playable ?? variant?.available) === true && Boolean(variant?.audioUrl)
|
||||
const playable = hasAudioOptions ? Boolean(audioOption?.audioUrl) : legacyPlayable
|
||||
const audioUrl = hasAudioOptions ? audioOption?.audioUrl : variant?.audioUrl
|
||||
const audioDuration = hasAudioOptions ? audioOption?.duration : variant?.audioDuration
|
||||
const audioStatus = hasAudioOptions
|
||||
? playable ? 'READY' : 'MISSING'
|
||||
: variant?.audioStatus || (playable ? 'READY' : 'MISSING')
|
||||
: exhibit.audioVariants ? Object.keys(exhibit.audioVariants) : [language]
|
||||
|
||||
if (!variant) {
|
||||
const label = this.languageLabel(language)
|
||||
@@ -195,45 +179,32 @@ export class ExplainUseCase {
|
||||
return {
|
||||
...exhibit,
|
||||
guideText,
|
||||
audioUrl,
|
||||
audioDuration,
|
||||
audioUrl: undefined,
|
||||
audioDuration: undefined,
|
||||
audioLanguage: language,
|
||||
audioHasText: language === 'yue-HK' && Boolean(mandarinText),
|
||||
audioAvailable: playable,
|
||||
audioStatus,
|
||||
audioUnavailableReason: playable ? undefined : `当前讲解暂无${label}音频`,
|
||||
audioChannelCode: audioOption?.channelCode,
|
||||
audioVoiceGender: audioOption?.gender,
|
||||
audioVoiceDisplayName: audioOption?.displayName,
|
||||
audioAvailable: false,
|
||||
audioStatus: 'MISSING',
|
||||
audioUnavailableReason: `当前讲解暂无${label}音频`,
|
||||
supportedLanguages
|
||||
}
|
||||
}
|
||||
|
||||
const textAvailable = variant.textAvailable ?? (variant.hasText === true || Boolean(variant.text))
|
||||
const guideText = variant.text
|
||||
|| (language === 'yue-HK' ? mandarinText : undefined)
|
||||
|| `${this.languageLabel(language)}讲解词暂未配置。`
|
||||
|
||||
return {
|
||||
...exhibit,
|
||||
guideTitle: variant.title || exhibit.guideTitle,
|
||||
guideText,
|
||||
audioUrl: playable ? audioUrl : undefined,
|
||||
audioDuration,
|
||||
guideText: language === 'yue-HK'
|
||||
? mandarinText || '当前讲解词暂未配置。'
|
||||
: variant.text || `${this.languageLabel(language)}讲解词暂未配置。`,
|
||||
audioUrl: variant.audioUrl,
|
||||
audioDuration: variant.audioDuration,
|
||||
audioLanguage: language,
|
||||
audioHasText: textAvailable,
|
||||
audioText: variant.text || undefined,
|
||||
audioTextLength: variant.textLength,
|
||||
audioTextHash: variant.textHash,
|
||||
audioNarrationTier: variant.narrationTier,
|
||||
audioAvailable: playable,
|
||||
audioStatus,
|
||||
audioUnavailableReason: playable
|
||||
audioHasText: language === 'yue-HK' ? Boolean(mandarinText) : variant.hasText === true,
|
||||
audioAvailable: variant.available === true && Boolean(variant.audioUrl),
|
||||
audioStatus: variant.available && variant.audioUrl ? 'READY' : 'MISSING',
|
||||
audioUnavailableReason: variant.available && variant.audioUrl
|
||||
? undefined
|
||||
: audioReasonToText(variant.reason || 'NO_PUBLISHED_AUDIO'),
|
||||
audioChannelCode: audioOption?.channelCode,
|
||||
audioVoiceGender: audioOption?.gender,
|
||||
audioVoiceDisplayName: audioOption?.displayName,
|
||||
: `当前讲解暂无${this.languageLabel(language)}音频`,
|
||||
supportedLanguages
|
||||
}
|
||||
}
|
||||
@@ -249,39 +220,15 @@ export class ExplainUseCase {
|
||||
: undefined
|
||||
const description = stopInfo.description || fallback?.description || '该讲解暂无简介。'
|
||||
const audioTarget = this.resolveStopInfoAudioTarget(stopInfo)
|
||||
const audioVariants = Object.fromEntries(Object.values(stopInfo.languageVariants).map((variant) => [variant.lang, {
|
||||
title: stopInfo.title,
|
||||
text: variant.text,
|
||||
textAvailable: variant.textAvailable,
|
||||
textLength: variant.textLength,
|
||||
textHash: variant.textHash,
|
||||
audioUrl: variant.playUrl,
|
||||
audioDuration: variant.duration,
|
||||
format: variant.format,
|
||||
audioId: variant.audioId,
|
||||
narrationTier: variant.narrationTier,
|
||||
hasText: variant.hasText,
|
||||
enabled: variant.enabled,
|
||||
playable: variant.playable,
|
||||
available: variant.playable,
|
||||
audioStatus: variant.audioStatus,
|
||||
fallback: variant.fallback,
|
||||
reason: variant.reason
|
||||
}]))
|
||||
const audioOptions: MuseumAudioOption[] = (stopInfo.audioOptions || []).map((option) => ({
|
||||
channelCode: option.channelCode,
|
||||
displayName: option.displayName,
|
||||
languageCode: option.languageCode,
|
||||
languageName: option.languageName,
|
||||
gender: option.gender,
|
||||
audioUrl: option.playUrl,
|
||||
duration: option.duration,
|
||||
format: option.format,
|
||||
isDefault: option.isDefault,
|
||||
sortOrder: option.sortOrder
|
||||
}))
|
||||
// Product defaults Chinese and English to male even if the management default is female.
|
||||
const audioOption = resolveGuideAudioOption(stopInfo.audioOptions, stopInfo.lang, 'male')
|
||||
const audioAvailable = Boolean(audioOption?.playUrl) || stopInfo.audioStatus === 'READY'
|
||||
const supportedLanguages = resolveGuideAudioLanguages(
|
||||
stopInfo.audioOptions,
|
||||
stopInfo.supportedLanguages
|
||||
)
|
||||
|
||||
const exhibit: MuseumExhibit = {
|
||||
return {
|
||||
...(fallback || {}),
|
||||
id: fallback?.id || linkedPrimary?.id || stopInfo.targetId,
|
||||
name: stopInfo.title || fallback?.name || linkedPrimary?.name || '讲解内容',
|
||||
@@ -302,15 +249,21 @@ export class ExplainUseCase {
|
||||
tags: fallback?.tags,
|
||||
guideTitle: stopInfo.title || fallback?.guideTitle,
|
||||
guideText: stopInfo.description || fallback?.guideText || fallback?.description,
|
||||
audioUrl: undefined,
|
||||
audioDuration: undefined,
|
||||
audioUrl: audioOption?.playUrl,
|
||||
audioDuration: audioOption?.duration,
|
||||
audioLanguage: stopInfo.lang,
|
||||
audioHasText: stopInfo.hasText,
|
||||
audioAvailable: false,
|
||||
audioStatus: stopInfo.audioStatus,
|
||||
supportedLanguages: stopInfo.supportedLanguages,
|
||||
audioVariants,
|
||||
audioOptions,
|
||||
audioText: undefined,
|
||||
audioTextLength: undefined,
|
||||
audioTextHash: undefined,
|
||||
audioNarrationTier: undefined,
|
||||
audioUnavailableReason: audioAvailable
|
||||
? undefined
|
||||
: audioReasonToText(stopInfo.reason || (stopInfo.audioStatus === 'MISSING' ? 'NO_PUBLISHED_AUDIO' : undefined)),
|
||||
audioAvailable,
|
||||
audioStatus: audioAvailable ? 'READY' : stopInfo.audioStatus,
|
||||
supportedLanguages,
|
||||
audioOptions: stopInfo.audioOptions,
|
||||
imageStatus: stopInfo.imageStatus,
|
||||
imageSource: stopInfo.imageSource,
|
||||
galleryUrls: stopInfo.imageStatus === 'READY' ? stopInfo.galleryUrls : [],
|
||||
@@ -323,8 +276,6 @@ export class ExplainUseCase {
|
||||
playTargetType: audioTarget.targetType,
|
||||
playTargetId: audioTarget.targetId
|
||||
}
|
||||
|
||||
return this.applyLanguageVariant(exhibit, stopInfo.lang)
|
||||
}
|
||||
|
||||
private applyPlayInfo(
|
||||
@@ -497,33 +448,6 @@ export class ExplainUseCase {
|
||||
throw stopInfoResult.error || new Error('讲解详情加载失败')
|
||||
}
|
||||
|
||||
selectExplainDetailLanguage(exhibit: MuseumExhibit, language: AudioLanguage) {
|
||||
return this.applyLanguageVariant(exhibit, language)
|
||||
}
|
||||
|
||||
selectExplainDetailAudioOption(exhibit: MuseumExhibit, channelCode: string): MuseumExhibit {
|
||||
const language = exhibit.audioLanguage as AudioLanguage | undefined
|
||||
const audioOption = exhibit.audioOptions?.find((option) => (
|
||||
option.channelCode === channelCode
|
||||
&& option.languageCode === language
|
||||
&& Boolean(option.audioUrl?.trim())
|
||||
))
|
||||
if (!audioOption) return exhibit
|
||||
|
||||
return {
|
||||
...exhibit,
|
||||
audioUrl: audioOption.audioUrl,
|
||||
audioDuration: audioOption.duration,
|
||||
audioLanguage: audioOption.languageCode,
|
||||
audioAvailable: true,
|
||||
audioStatus: 'READY',
|
||||
audioUnavailableReason: undefined,
|
||||
audioChannelCode: audioOption.channelCode,
|
||||
audioVoiceGender: audioOption.gender,
|
||||
audioVoiceDisplayName: audioOption.displayName
|
||||
}
|
||||
}
|
||||
|
||||
async loadExplainDetailText(exhibit: MuseumExhibit): Promise<ExplainTextSelection> {
|
||||
const targetType = exhibit.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.playTargetId || exhibit.id
|
||||
@@ -723,17 +647,48 @@ export class ExplainUseCase {
|
||||
|
||||
async selectAudioForExplainDetail(
|
||||
exhibit: MuseumExhibit,
|
||||
options: { refreshPlayInfo?: boolean } = {}
|
||||
options: {
|
||||
refreshPlayInfo?: boolean
|
||||
voiceGender?: GuideAudioGender
|
||||
} = {}
|
||||
): Promise<ExplainAudioSelection> {
|
||||
const targetType = exhibit.playTargetType || 'ITEM'
|
||||
const targetId = exhibit.playTargetId || exhibit.id
|
||||
const language = (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage)
|
||||
const audioOption = options.refreshPlayInfo
|
||||
? undefined
|
||||
: resolveGuideAudioOption(exhibit.audioOptions, language, options.voiceGender || 'male')
|
||||
|
||||
if ((!options.refreshPlayInfo || exhibit.audioChannelCode) && exhibit.audioUrl?.trim()) {
|
||||
if (audioOption) {
|
||||
const audioExhibit: MuseumExhibit = {
|
||||
...exhibit,
|
||||
audioUrl: audioOption.playUrl,
|
||||
audioDuration: audioOption.duration,
|
||||
audioLanguage: language,
|
||||
audioAvailable: true,
|
||||
audioStatus: 'READY'
|
||||
}
|
||||
return {
|
||||
exhibit: audioExhibit,
|
||||
track: null,
|
||||
media: {
|
||||
id: `channel-${audioOption.channelCode}`,
|
||||
type: 'audio',
|
||||
url: audioOption.playUrl,
|
||||
duration: audioOption.duration,
|
||||
language,
|
||||
available: true
|
||||
},
|
||||
playable: true
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.refreshPlayInfo && exhibit.audioUrl?.trim()) {
|
||||
return {
|
||||
exhibit,
|
||||
track: null,
|
||||
media: {
|
||||
id: `static-${exhibit.audioChannelCode || `${targetType}-${targetId}`}-${exhibit.audioLanguage || dataSourceConfig.audioLanguage}`,
|
||||
id: `static-${targetType}-${targetId}-${exhibit.audioLanguage || dataSourceConfig.audioLanguage}`,
|
||||
type: 'audio',
|
||||
url: exhibit.audioUrl,
|
||||
duration: exhibit.audioDuration,
|
||||
@@ -758,7 +713,7 @@ export class ExplainUseCase {
|
||||
const playInfo = await this.audioPlayInfo.getPlayInfo({
|
||||
targetType,
|
||||
targetId,
|
||||
lang: (exhibit.audioLanguage as AudioLanguage) || (dataSourceConfig.audioLanguage as AudioLanguage),
|
||||
lang: language,
|
||||
refresh: options.refreshPlayInfo === true
|
||||
})
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
createGuideRouteRepository
|
||||
} from '@/repositories/createGuideRouteRepository'
|
||||
import {
|
||||
NAV_ROUTE_SERVICE_UNAVAILABLE_MESSAGE,
|
||||
applyRouteReadinessGate,
|
||||
NAV_ROUTE_READINESS
|
||||
} from '@/domain/guideReadiness'
|
||||
import { startGuidePerformance } from '@/services/performance/guidePerformance'
|
||||
|
||||
const toSearchText = (target: GuideRouteTarget) => [
|
||||
target.name,
|
||||
@@ -30,7 +32,9 @@ const toSearchText = (target: GuideRouteTarget) => [
|
||||
|
||||
export interface GuideRoutePlanRequest {
|
||||
startPoiId: string
|
||||
endPoiId: string
|
||||
/** 地图对象没有显式接入点时,保留对象坐标交给路线服务吸附。 */
|
||||
startTarget?: GuideRouteTarget
|
||||
endTarget: GuideRouteTarget
|
||||
}
|
||||
|
||||
export interface GuideRoutePlanResult {
|
||||
@@ -40,28 +44,50 @@ export interface GuideRoutePlanResult {
|
||||
|
||||
export class GuideRouteUseCase {
|
||||
private targetsCache: GuideRouteTarget[] | null = null
|
||||
private targetsRequest: Promise<GuideRouteTarget[]> | null = null
|
||||
private readinessCache: GuideRouteReadiness | null = null
|
||||
private readinessRequest: Promise<GuideRouteReadiness> | null = null
|
||||
|
||||
constructor(private readonly repository: GuideRouteRepository = createGuideRouteRepository()) {}
|
||||
|
||||
async getRouteReadiness() {
|
||||
if (this.readinessCache) return applyRouteReadinessGate(this.readinessCache)
|
||||
if (this.readinessRequest) return this.readinessRequest
|
||||
|
||||
this.readinessCache = applyRouteReadinessGate(await this.repository.getRouteReadiness())
|
||||
return applyRouteReadinessGate(this.readinessCache)
|
||||
this.readinessRequest = this.repository.getRouteReadiness()
|
||||
.catch(() => ({
|
||||
ready: false,
|
||||
message: NAV_ROUTE_SERVICE_UNAVAILABLE_MESSAGE,
|
||||
requiredData: NAV_ROUTE_READINESS.requiredData
|
||||
}))
|
||||
.then((readiness) => {
|
||||
this.readinessCache = applyRouteReadinessGate(readiness)
|
||||
return applyRouteReadinessGate(this.readinessCache)
|
||||
})
|
||||
.finally(() => {
|
||||
this.readinessRequest = null
|
||||
})
|
||||
return this.readinessRequest
|
||||
}
|
||||
|
||||
async listTargets() {
|
||||
const readiness = await this.getRouteReadiness()
|
||||
if (!readiness.ready) return []
|
||||
if (this.targetsCache) return this.targetsCache
|
||||
if (this.targetsRequest) return this.targetsRequest
|
||||
|
||||
this.targetsCache = (await this.repository.listRouteTargets())
|
||||
.filter((target) => isIndoorNavigableFloor({
|
||||
floorId: target.floorId,
|
||||
label: target.floorLabel
|
||||
}))
|
||||
return this.targetsCache
|
||||
this.targetsRequest = this.repository.listRouteTargets()
|
||||
.then((targets) => {
|
||||
this.targetsCache = targets.filter((target) => isIndoorNavigableFloor({
|
||||
floorId: target.floorId,
|
||||
label: target.floorLabel
|
||||
}))
|
||||
return this.targetsCache
|
||||
})
|
||||
.finally(() => {
|
||||
this.targetsRequest = null
|
||||
})
|
||||
return this.targetsRequest
|
||||
}
|
||||
|
||||
async searchTargets(keyword = '') {
|
||||
@@ -73,22 +99,53 @@ export class GuideRouteUseCase {
|
||||
return targets.filter((target) => toSearchText(target).includes(normalizedKeyword))
|
||||
}
|
||||
|
||||
async listTargetsForFloor(floorId: string) {
|
||||
const readiness = await this.getRouteReadiness()
|
||||
if (!readiness.ready) return []
|
||||
|
||||
const floorTargets = this.repository.listRouteTargetsForFloor
|
||||
? await this.repository.listRouteTargetsForFloor(floorId)
|
||||
: (await this.listTargets()).filter((target) => String(target.floorId) === String(floorId))
|
||||
|
||||
return floorTargets.filter((target) => isIndoorNavigableFloor({
|
||||
floorId: target.floorId,
|
||||
label: target.floorLabel
|
||||
}))
|
||||
}
|
||||
|
||||
async planRoute(request: GuideRoutePlanRequest): Promise<GuideRoutePlanResult> {
|
||||
const finishPerformance = startGuidePerformance('interaction', 'route-plan', {
|
||||
startFloorId: request.startTarget?.floorId || '',
|
||||
endFloorId: request.endTarget.floorId || '',
|
||||
coordinateFallback: Boolean(request.startTarget && !request.startTarget.routeNodeId)
|
||||
})
|
||||
|
||||
try {
|
||||
const readiness = applyRouteReadinessGate(await this.getRouteReadiness())
|
||||
if (!readiness.ready) {
|
||||
finishPerformance('failure', { reason: 'route-not-ready' })
|
||||
return {
|
||||
route: null,
|
||||
error: readiness.message || NAV_ROUTE_READINESS.message
|
||||
}
|
||||
}
|
||||
|
||||
const route = await this.repository.findRoute(request.startPoiId, request.endPoiId)
|
||||
const route = await this.repository.findRoute(
|
||||
request.startTarget || request.startPoiId,
|
||||
request.endTarget
|
||||
)
|
||||
finishPerformance('success', {
|
||||
distanceMeters: Math.round(route.distanceMeters),
|
||||
floorSegmentCount: route.floorSegments.length
|
||||
})
|
||||
return {
|
||||
route,
|
||||
error: ''
|
||||
}
|
||||
} catch (error) {
|
||||
finishPerformance('failure', {
|
||||
error: error instanceof GuideRouteError ? error.code : error instanceof Error ? error.name : String(error)
|
||||
})
|
||||
return {
|
||||
route: null,
|
||||
error: this.toUserMessage(error)
|
||||
@@ -101,7 +158,10 @@ export class GuideRouteUseCase {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return error instanceof Error ? error.message : '馆内位置关系生成失败'
|
||||
// Repository errors may contain request URLs, status codes, and response
|
||||
// bodies. Keep those details in diagnostics while exposing one stable
|
||||
// visitor-facing retry message.
|
||||
return NAV_ROUTE_SERVICE_UNAVAILABLE_MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
GuidePoiSearchMode,
|
||||
GuidePoiSearchViewState
|
||||
} from '@/domain/poiSearch'
|
||||
import { startGuidePerformance } from '@/services/performance/guidePerformance'
|
||||
|
||||
const startSourceLabels: Record<string, string> = {
|
||||
'facility-detail': '设施详情选择',
|
||||
@@ -174,42 +175,61 @@ export class GuideUseCase {
|
||||
floorId: string | undefined,
|
||||
input: PoiSearchModeInput
|
||||
): Promise<GuidePoiSearchViewState> {
|
||||
const floor = await this.resolvePoiSearchFloor(floorId)
|
||||
const keyword = input.keyword?.trim() || ''
|
||||
const categoryId = input.categoryId || ''
|
||||
const category = categoryId ? getPoiCategoryById(categoryId) : null
|
||||
const mode = input.mode === 'category' && !category
|
||||
? 'default'
|
||||
: input.mode
|
||||
const finishPerformance = startGuidePerformance('interaction', 'poi-search-data', {
|
||||
requestedFloorId: floorId || '',
|
||||
requestedMode: input.mode
|
||||
})
|
||||
|
||||
const [availability, results] = await Promise.all([
|
||||
this.guide.getQuickFindCategoryAvailability(floor.id),
|
||||
mode === 'category' && category
|
||||
? this.guide.listQuickFindPois(category.id, floor.id)
|
||||
: mode === 'keyword'
|
||||
? this.guide.searchPois(keyword, floor.id)
|
||||
: this.guide.listDestinationPois(floor.id)
|
||||
])
|
||||
try {
|
||||
const floor = await this.resolvePoiSearchFloor(floorId)
|
||||
const keyword = input.keyword?.trim() || ''
|
||||
const categoryId = input.categoryId || ''
|
||||
const category = categoryId ? getPoiCategoryById(categoryId) : null
|
||||
const mode = input.mode === 'category' && !category
|
||||
? 'default'
|
||||
: input.mode
|
||||
|
||||
return {
|
||||
mode,
|
||||
floorId: floor.id,
|
||||
floorLabel: floor.label,
|
||||
keyword: mode === 'keyword' ? keyword : category?.label || '',
|
||||
categoryId: mode === 'category' && category ? category.id : '',
|
||||
results,
|
||||
visiblePoiIds: results.map((poi) => poi.id),
|
||||
categories: availability.map((item) => {
|
||||
const definition = getPoiCategoryById(item.id)
|
||||
if (!definition) {
|
||||
throw new Error(`Unknown quick-find category: ${item.id}`)
|
||||
}
|
||||
return {
|
||||
definition,
|
||||
count: item.count,
|
||||
disabled: item.disabled
|
||||
}
|
||||
const [availability, results] = await Promise.all([
|
||||
this.guide.getQuickFindCategoryAvailability(floor.id),
|
||||
mode === 'category' && category
|
||||
? this.guide.listQuickFindPois(category.id, floor.id)
|
||||
: mode === 'keyword'
|
||||
? this.guide.searchPois(keyword, floor.id)
|
||||
: this.guide.listDestinationPois(floor.id)
|
||||
])
|
||||
|
||||
const state = {
|
||||
mode,
|
||||
floorId: floor.id,
|
||||
floorLabel: floor.label,
|
||||
keyword: mode === 'keyword' ? keyword : category?.label || '',
|
||||
categoryId: mode === 'category' && category ? category.id : '',
|
||||
results,
|
||||
visiblePoiIds: results.map((poi) => poi.id),
|
||||
categories: availability.map((item) => {
|
||||
const definition = getPoiCategoryById(item.id)
|
||||
if (!definition) {
|
||||
throw new Error(`Unknown quick-find category: ${item.id}`)
|
||||
}
|
||||
return {
|
||||
definition,
|
||||
count: item.count,
|
||||
disabled: item.disabled
|
||||
}
|
||||
})
|
||||
} satisfies GuidePoiSearchViewState
|
||||
|
||||
finishPerformance('success', {
|
||||
mode: state.mode,
|
||||
floorId: state.floorId,
|
||||
resultCount: state.results.length
|
||||
})
|
||||
return state
|
||||
} catch (error) {
|
||||
finishPerformance('failure', {
|
||||
error: error instanceof Error ? error.name : String(error)
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface PoiDetailTarget {
|
||||
poiId: string
|
||||
floorId: string
|
||||
detailType: PoiDetailType
|
||||
detailAvailable: boolean
|
||||
detailId: string
|
||||
hallId?: string
|
||||
exhibitId?: string
|
||||
@@ -117,6 +118,7 @@ export class PoiDetailUseCase {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
detailType: 'exhibit',
|
||||
detailAvailable: true,
|
||||
detailId: poi.exhibitId,
|
||||
exhibitId: poi.exhibitId,
|
||||
url: createCanonicalDetailUrl(`/pages/exhibit/detail?id=${encodeURIComponent(poi.exhibitId)}`, poi),
|
||||
@@ -135,10 +137,11 @@ export class PoiDetailUseCase {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
detailType: 'hall',
|
||||
detailAvailable: true,
|
||||
detailId: hall.id,
|
||||
hallId: hall.id,
|
||||
url: createCanonicalDetailUrl(explainGuideStopListUrl(hall.id, hall.name), poi),
|
||||
failureMessage: '展厅讲解列表打开失败,请重试'
|
||||
failureMessage: '免费讲解列表打开失败,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +149,7 @@ export class PoiDetailUseCase {
|
||||
poiId: poi.id,
|
||||
floorId: poi.floorId,
|
||||
detailType: 'facility',
|
||||
detailAvailable: false,
|
||||
detailId: poi.id,
|
||||
url: createCanonicalDetailUrl(
|
||||
`/pages/facility/detail?id=${encodeURIComponent(poi.id)}&target=${encodeURIComponent(poi.name)}`,
|
||||
|
||||
Reference in New Issue
Block a user