修复讲解业务 API 数据源配置
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
|
||||
@@ -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('讲解详情加载失败')
|
||||
|
||||
Reference in New Issue
Block a user