From 349f9b03e565c2564093c07599f0fba00c5df58b Mon Sep 17 00:00:00 2001
From: lyf <2514544224@qq.com>
Date: Tue, 7 Jul 2026 18:05:42 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AE=B2=E8=A7=A3=E4=B8=9A?=
=?UTF-8?q?=E5=8A=A1=20API=20=E6=95=B0=E6=8D=AE=E6=BA=90=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 4 +-
.env.production | 4 +-
.env.test | 4 +-
src/components/explain/ExplainHallSelect.vue | 9 +-
src/pages/exhibit/detail.vue | 101 ++++++++++++++++++-
src/pages/explain/list.vue | 15 ++-
src/pages/index/index.vue | 15 ++-
src/repositories/AudioPlayInfoRepository.ts | 4 +-
src/usecases/explainUseCase.ts | 4 +-
9 files changed, 140 insertions(+), 20 deletions(-)
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('讲解详情加载失败')