diff --git a/.env.example b/.env.example index 08c5a21..99265df 100644 --- a/.env.example +++ b/.env.example @@ -4,10 +4,10 @@ VITE_APP_PUBLIC_BASE=/ # 主开关:导览业务与讲解业务分离配置,避免互相切换。 VITE_GUIDE_DATA_SOURCE_MODE=static -VITE_EXPLAIN_CONTENT_SOURCE_MODE=static +VITE_EXPLAIN_CONTENT_SOURCE_MODE=remote # 兼容旧变量:新代码优先读取上方显式主开关。 VITE_DATA_SOURCE_MODE=static -VITE_GUIDE_CONTENT_SOURCE_MODE=static +VITE_GUIDE_CONTENT_SOURCE_MODE=remote VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data VITE_API_BASE_URL=/app-api VITE_AUDIO_API_BASE_URL=/app-api diff --git a/.env.production b/.env.production index 458cc7d..dfc08bc 100644 --- a/.env.production +++ b/.env.production @@ -4,10 +4,10 @@ VITE_APP_PUBLIC_BASE=/ # 主开关:导览业务与讲解业务分离配置,避免互相切换。 VITE_GUIDE_DATA_SOURCE_MODE=sdk -VITE_EXPLAIN_CONTENT_SOURCE_MODE=static +VITE_EXPLAIN_CONTENT_SOURCE_MODE=remote # 兼容旧变量:新代码优先读取上方显式主开关。 VITE_DATA_SOURCE_MODE=sdk -VITE_GUIDE_CONTENT_SOURCE_MODE=static +VITE_GUIDE_CONTENT_SOURCE_MODE=remote VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data VITE_API_BASE_URL=/app-api VITE_AUDIO_API_BASE_URL=/app-api diff --git a/.env.test b/.env.test index ce4d4b2..247ad75 100644 --- a/.env.test +++ b/.env.test @@ -4,10 +4,10 @@ VITE_APP_PUBLIC_BASE=/ # 主开关:导览业务与讲解业务分离配置,避免互相切换。 VITE_GUIDE_DATA_SOURCE_MODE=sdk -VITE_EXPLAIN_CONTENT_SOURCE_MODE=static +VITE_EXPLAIN_CONTENT_SOURCE_MODE=remote # 兼容旧变量:新代码优先读取上方显式主开关。 VITE_DATA_SOURCE_MODE=sdk -VITE_GUIDE_CONTENT_SOURCE_MODE=static +VITE_GUIDE_CONTENT_SOURCE_MODE=remote VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data VITE_API_BASE_URL=/app-api VITE_AUDIO_API_BASE_URL=/app-api diff --git a/src/components/explain/ExplainHallSelect.vue b/src/components/explain/ExplainHallSelect.vue index 779aceb..b0e16fc 100644 --- a/src/components/explain/ExplainHallSelect.vue +++ b/src/components/explain/ExplainHallSelect.vue @@ -168,6 +168,8 @@ export interface ExplainGuideStopSelectItem { hasAudio?: boolean audioStatus?: string guideLevel?: string + playTargetType?: 'ITEM' | 'STOP' + playTargetId?: string } export type ExplainSelectStage = 'hall' | 'unit' | 'stop' @@ -195,7 +197,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ hallClick: [hallId: string] businessUnitClick: [unitId: string] - guideStopClick: [stopId: string] + guideStopClick: [stop: ExplainGuideStopSelectItem] back: [] }>() @@ -272,7 +274,10 @@ const handleBusinessUnitClick = (unitId: string) => { } const handleGuideStopClick = (stopId: string) => { - emit('guideStopClick', stopId) + const stop = props.guideStops.find((item) => item.id === stopId) + if (stop) { + emit('guideStopClick', stop) + } } const handleBack = () => { diff --git a/src/pages/exhibit/detail.vue b/src/pages/exhibit/detail.vue index d97b0a1..8a76462 100644 --- a/src/pages/exhibit/detail.vue +++ b/src/pages/exhibit/detail.vue @@ -71,7 +71,16 @@ 讲解内容 - {{ exhibit.body || exhibit.summary }} + {{ detailBodyText }} + + {{ detailTextLoading ? '正在加载' : '查看图文讲解' }} + + {{ detailTextUnavailableMessage }} @@ -192,6 +201,9 @@ const resolvedHallId = ref('') const retryingAudio = ref(false) const selectedAudioLanguage = ref('zh-CN') const languageLoading = ref(false) +const detailTextLoaded = ref(false) +const detailTextLoading = ref(false) +const detailTextUnavailableMessage = ref('') const detailEntryRequest = ref<{ exhibitId: string targetType?: AudioPlayTargetType @@ -227,6 +239,16 @@ const visibleLanguageOptions = computed(() => ( languageOptions.filter((option) => supportedDetailLanguages.value.has(option.value)) )) const isLanguageSwitchVisible = computed(() => visibleLanguageOptions.value.length > 1) +const isTextLoadActionVisible = computed(() => ( + exhibit.value.audio.hasText === true + && !detailTextLoaded.value + && !detailTextUnavailableMessage.value +)) +const detailBodyText = computed(() => ( + detailTextLoaded.value + ? exhibit.value.body || exhibit.value.summary + : exhibit.value.summary +)) // 讲解详情页只定位到所属展厅,不再追踪具体展品点位。 const resolveHallGuidePoi = async () => { @@ -258,6 +280,8 @@ const loadExplainDetail = async ( exhibit.value = toExplainDetailPageViewModel(exhibitData) selectedAudioLanguage.value = lang + detailTextLoaded.value = Boolean(exhibitData.audioText) + detailTextUnavailableMessage.value = '' if (exhibitData.hallId) { resolvedHallId.value = exhibitData.hallId @@ -329,6 +353,44 @@ const handleLanguageSwitch = async (lang: AudioLanguage) => { } } +const handleLoadDetailText = async () => { + if (detailTextLoading.value || detailTextLoaded.value) return + + detailTextLoading.value = true + detailTextUnavailableMessage.value = '' + + try { + const selection = await explainUseCase.loadExplainDetailText({ + id: exhibit.value.id, + name: exhibit.value.title, + hallId: exhibit.value.hallId, + hallName: exhibit.value.hallName, + floorId: exhibit.value.floorId, + floorLabel: exhibit.value.floorLabel, + image: heroImage.value, + description: exhibit.value.summary, + guideText: exhibit.value.summary, + audioLanguage: exhibit.value.audio.language, + audioHasText: exhibit.value.audio.hasText, + playTargetType: exhibit.value.audio.playTargetType, + playTargetId: exhibit.value.audio.playTargetId + }) + + if (selection.available) { + exhibit.value = toExplainDetailPageViewModel(selection.exhibit) + detailTextLoaded.value = true + return + } + + detailTextUnavailableMessage.value = selection.unavailableMessage || '当前语言暂无讲解词' + } catch (error) { + console.error('讲解正文加载失败:', error) + detailTextUnavailableMessage.value = '讲解词服务暂不可用,请稍后重试' + } finally { + detailTextLoading.value = false + } +} + const handlePlayAudio = async () => { const selection = exhibit.value.id ? await explainUseCase.selectAudioForExplainDetail({ @@ -733,6 +795,43 @@ const handleBack = () => { white-space: pre-line; } +.text-load-btn { + width: 100%; + height: 44px; + margin-top: 14px; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + background: #151713; + border-radius: 8px; + color: #e0df00; +} + +.text-load-btn.disabled { + opacity: 0.62; +} + +.text-load-btn-text { + display: block; + max-width: 100%; + font-size: 15px; + line-height: 21px; + font-weight: 800; + color: currentColor; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.section-hint { + display: block; + margin-top: 12px; + font-size: 14px; + line-height: 20px; + color: #68725d; +} + .linked-exhibit-list { margin-top: 14px; display: flex; diff --git a/src/pages/explain/list.vue b/src/pages/explain/list.vue index 944e78e..c0ce256 100644 --- a/src/pages/explain/list.vue +++ b/src/pages/explain/list.vue @@ -114,7 +114,9 @@ const explainGuideStopItems = computed(() => ( description: stop.description, hasAudio: stop.hasAudio, audioStatus: stop.audioStatus, - guideLevel: stop.guideLevel + guideLevel: stop.guideLevel, + playTargetType: stop.playTargetType, + playTargetId: stop.playTargetId })) || [] )) @@ -191,10 +193,15 @@ const handleExplainBusinessUnitClick = (unitId: string) => { explainStage.value = 'stop' } -const handleExplainGuideStopClick = (stopId: string) => { - const target = normalizeExplainDetailTargetFromGuideStop({ id: stopId }) +const handleExplainGuideStopClick = (stop: ExplainGuideStopSelectItem) => { + const target = stop.playTargetType && stop.playTargetId + ? { + targetType: stop.playTargetType, + targetId: stop.playTargetId + } + : normalizeExplainDetailTargetFromGuideStop({ id: stop.id }) const params = new URLSearchParams({ - id: target.targetId, + id: stop.id, tab: 'explain', targetType: target.targetType, targetId: target.targetId diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 82b58fa..b81c6cb 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -730,7 +730,9 @@ const explainGuideStopItems = computed(() => ( description: stop.description, hasAudio: stop.hasAudio, audioStatus: stop.audioStatus, - guideLevel: stop.guideLevel + guideLevel: stop.guideLevel, + playTargetType: stop.playTargetType, + playTargetId: stop.playTargetId })) || [] )) @@ -1551,10 +1553,15 @@ const handleExplainBusinessUnitClick = (unitId: string) => { explainStage.value = 'stop' } -const handleExplainGuideStopClick = (stopId: string) => { - const target = normalizeExplainDetailTargetFromGuideStop({ id: stopId }) +const handleExplainGuideStopClick = (stop: ExplainGuideStopSelectItem) => { + const target = stop.playTargetType && stop.playTargetId + ? { + targetType: stop.playTargetType, + targetId: stop.playTargetId + } + : normalizeExplainDetailTargetFromGuideStop({ id: stop.id }) const params = new URLSearchParams({ - id: target.targetId, + id: stop.id, tab: 'explain', targetType: target.targetType, targetId: target.targetId diff --git a/src/repositories/AudioPlayInfoRepository.ts b/src/repositories/AudioPlayInfoRepository.ts index 6a4ea6c..318f394 100644 --- a/src/repositories/AudioPlayInfoRepository.ts +++ b/src/repositories/AudioPlayInfoRepository.ts @@ -119,7 +119,9 @@ const normalizeBaseUrl = (baseUrl: string) => { } const resolveAudioApiBaseUrl = () => { - const baseUrl = normalizeBaseUrl(dataSourceConfig.apiBaseUrl) + // Catalog APIs use apiBaseUrl in BackendExplainContentProvider; stop/detail/play/text APIs use audioApiBaseUrl. + // They are both /app-api today, but audioApiBaseUrl can be split to a dedicated proxy or host later. + const baseUrl = normalizeBaseUrl(dataSourceConfig.audioApiBaseUrl) return baseUrl.endsWith('/app-api') ? baseUrl : `${baseUrl}/app-api` } diff --git a/src/usecases/explainUseCase.ts b/src/usecases/explainUseCase.ts index d205eca..d89cfdb 100644 --- a/src/usecases/explainUseCase.ts +++ b/src/usecases/explainUseCase.ts @@ -394,7 +394,7 @@ export class ExplainUseCase { const entryTarget = await this.resolveDetailEntryTarget(request) const fallbackExhibit = await this.resolveStaticDetailFallback(request, entryTarget) - if (fallbackExhibit) { + if (fallbackExhibit && dataSourceConfig.explainContentMode !== 'remote') { return this.applyLanguageVariant(fallbackExhibit, entryTarget.lang) } @@ -403,7 +403,7 @@ export class ExplainUseCase { .catch((error) => ({ stopInfo: null, error })) if (stopInfoResult.stopInfo) { - return this.toExhibitFromStopInfo(stopInfoResult.stopInfo, null) + return this.toExhibitFromStopInfo(stopInfoResult.stopInfo, fallbackExhibit) } throw stopInfoResult.error || new Error('讲解详情加载失败')