修复讲解业务 API 数据源配置

This commit is contained in:
lyf
2026-07-07 18:05:42 +08:00
parent eaec80bdc3
commit 349f9b03e5
9 changed files with 140 additions and 20 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,7 +71,16 @@
<view class="content-section">
<text class="section-title">讲解内容</text>
<text class="section-text">{{ exhibit.body || exhibit.summary }}</text>
<text class="section-text">{{ detailBodyText }}</text>
<view
v-if="isTextLoadActionVisible"
class="text-load-btn"
:class="{ disabled: detailTextLoading }"
@tap="handleLoadDetailText"
>
<text class="text-load-btn-text">{{ detailTextLoading ? '正在加载' : '查看图文讲解' }}</text>
</view>
<text v-if="detailTextUnavailableMessage" class="section-hint">{{ detailTextUnavailableMessage }}</text>
</view>
<view v-if="visibleLinkedExhibits.length" class="content-section">
@@ -192,6 +201,9 @@ const resolvedHallId = ref('')
const retryingAudio = ref(false)
const selectedAudioLanguage = ref<AudioLanguage>('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;

View File

@@ -114,7 +114,9 @@ const explainGuideStopItems = computed<ExplainGuideStopSelectItem[]>(() => (
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

View File

@@ -730,7 +730,9 @@ const explainGuideStopItems = computed<ExplainGuideStopSelectItem[]>(() => (
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

View File

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

View File

@@ -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('讲解详情加载失败')