feat: 临时改造讲解页方案 B 链路

接入展厅列表、展厅讲解点分组生成临时业务单元,并迁移讲解详情播放正文到新接口链路。
This commit is contained in:
lyf
2026-07-02 00:51:27 +08:00
parent e0aeafe062
commit 9efcef5190
14 changed files with 1429 additions and 288 deletions

View File

@@ -10,73 +10,103 @@
@back="handleBack"
>
<view class="explain-all-page">
<ExplainList
:cards="explainExhibits"
<ExplainHallSelect
:halls="explainHallItems"
:business-units="explainBusinessUnitItems"
:guide-stops="explainGuideStopItems"
:stage="explainStage"
:selected-hall-name="selectedExplainHallName"
:selected-business-unit-name="selectedExplainBusinessUnitName"
:loading="explainLoading"
:error="explainError"
list-title="全部讲解"
page-mode="all"
:initial-render-limit="48"
:page-size="48"
@exhibit-click="handleExplainExhibitClick"
@featured-click="handleExplainFeaturedClick"
@audio-click="handleAudioClick"
@location-click="handleExplainLocationClick"
@hall-click="handleExplainHallClick"
/>
<AudioPlayer
ref="audioPlayerRef"
:visible="showAudioPlayer"
:audio="currentAudio"
:auto-play="false"
avoid-bottom-nav
@update:visible="handleAudioVisibleChange"
@play="handleAudioPlay"
@pause="handleAudioPause"
@ended="handleAudioEnded"
@error="handleAudioError"
@business-unit-click="handleExplainBusinessUnitClick"
@guide-stop-click="handleExplainGuideStopClick"
@back="handlePanelBack"
/>
</view>
</GuidePageFrame>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import ExplainList, { type Exhibit } from '@/components/explain/ExplainList.vue'
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
import ExplainHallSelect, {
type ExplainBusinessUnitSelectItem,
type ExplainGuideStopSelectItem,
type ExplainHallSelectItem,
type ExplainSelectStage
} from '@/components/explain/ExplainHallSelect.vue'
import {
explainUseCase
} from '@/usecases/explainUseCase'
import {
guideUseCase
} from '@/usecases/guideUseCase'
import {
toExplainCardViewModel
} from '@/view-models/explainViewModels'
import type {
ExplainBusinessUnit,
MuseumHall
} from '@/domain/museum'
import {
navigateToGuideTopTab,
type GuideTopTab
} from '@/utils/guideTopTabs'
const explainExhibits = ref<Exhibit[]>([])
const explainHallItems = ref<ExplainHallSelectItem[]>([])
const explainBusinessUnits = ref<ExplainBusinessUnit[]>([])
const explainStage = ref<ExplainSelectStage>('hall')
const selectedExplainHallId = ref('')
const selectedExplainHallName = ref('')
const selectedExplainBusinessUnitId = ref('')
const selectedExplainBusinessUnitName = ref('')
const explainLoading = ref(false)
const explainError = ref('')
const showAudioPlayer = ref(false)
const isAudioPlaying = ref(false)
const currentAudio = ref<AudioItem | null>(null)
const audioPlayerRef = ref<AudioPlayerExpose | null>(null)
let explainLoadPromise: Promise<void> | null = null
interface AudioPlayerExpose {
play: (audio: AudioItem) => void
pause: () => void
}
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
const loadExplainExhibits = async () => {
if (explainExhibits.value.length) return
const buildExplainHallItems = (halls: MuseumHall[]): ExplainHallSelectItem[] => (
halls.map((hall) => ({
id: hall.id,
name: hall.name,
floorLabel: hall.floorLabel,
image: hall.image,
explainCount: hall.exhibitCount || 0,
searchText: normalizeExplainKeyword([
hall.name,
hall.floorLabel,
hall.description,
hall.area
].filter(Boolean).join(' '))
}))
)
const selectedExplainBusinessUnit = computed(() => (
explainBusinessUnits.value.find((unit) => unit.id === selectedExplainBusinessUnitId.value) || null
))
const explainBusinessUnitItems = computed<ExplainBusinessUnitSelectItem[]>(() => (
explainBusinessUnits.value.map((unit) => ({
id: unit.id,
name: unit.name,
hallId: unit.hallId,
guideStopCount: unit.guideStopCount
}))
))
const explainGuideStopItems = computed<ExplainGuideStopSelectItem[]>(() => (
selectedExplainBusinessUnit.value?.stops.map((stop) => ({
id: stop.id,
name: stop.name,
hallId: stop.hallId,
hallName: stop.hallName,
floorId: stop.floorId,
coverImageUrl: stop.coverImageUrl,
description: stop.description,
hasAudio: stop.hasAudio
})) || []
))
const loadExplainHalls = async () => {
if (explainHallItems.value.length) return
if (explainLoadPromise) return explainLoadPromise
explainLoading.value = true
@@ -84,12 +114,12 @@ const loadExplainExhibits = async () => {
explainLoadPromise = (async () => {
try {
const exhibits = await explainUseCase.listExplainExhibits()
explainExhibits.value = exhibits.map(toExplainCardViewModel)
const halls = await explainUseCase.loadExplainHalls()
explainHallItems.value = buildExplainHallItems(halls)
} catch (error) {
console.error('加载全部讲解内容失败:', error)
explainError.value = '讲解内容加载失败,请稍后重试'
explainExhibits.value = []
console.error('加载讲解展厅失败:', error)
explainError.value = '讲解展厅加载失败,请稍后重试'
explainHallItems.value = []
} finally {
explainLoading.value = false
explainLoadPromise = null
@@ -100,7 +130,7 @@ const loadExplainExhibits = async () => {
}
onLoad(() => {
void loadExplainExhibits()
void loadExplainHalls()
})
const handleBack = () => {
@@ -118,120 +148,68 @@ const handleTopTabChange = (tab: GuideTopTab) => {
navigateToGuideTopTab(tab)
}
const handleExplainExhibitClick = (exhibit: Exhibit) => {
const handleExplainHallClick = async (hallId: string) => {
const hall = explainHallItems.value.find((item) => item.id === hallId)
selectedExplainHallId.value = hallId
selectedExplainHallName.value = hall?.name || '业务单元'
selectedExplainBusinessUnitId.value = ''
selectedExplainBusinessUnitName.value = ''
explainBusinessUnits.value = []
explainLoading.value = true
explainError.value = ''
try {
explainBusinessUnits.value = await explainUseCase.loadTemporaryBusinessUnitsByHall(hallId)
explainStage.value = 'unit'
} catch (error) {
console.error('加载展厅讲解点失败:', error)
explainError.value = '展厅讲解点加载失败,请稍后重试'
explainStage.value = 'unit'
} finally {
explainLoading.value = false
}
}
const handleExplainBusinessUnitClick = (unitId: string) => {
const unit = explainBusinessUnits.value.find((item) => item.id === unitId)
selectedExplainBusinessUnitId.value = unitId
selectedExplainBusinessUnitName.value = unit?.name || '讲解点'
explainStage.value = 'stop'
}
const handleExplainGuideStopClick = (stopId: string) => {
const params = new URLSearchParams({
id: stopId,
tab: 'explain',
targetType: 'STOP',
targetId: stopId
})
uni.navigateTo({
url: `/pages/exhibit/detail?id=${exhibit.id}&tab=explain`
url: `/pages/exhibit/detail?${params.toString()}`
})
}
const handleExplainFeaturedClick = async (exhibit: Exhibit) => {
await handleAudioClick(exhibit)
}
const handleExplainHallClick = (hallId: string) => {
uni.navigateTo({
url: `/pages/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
})
}
const handleAudioClick = async (exhibit: Exhibit) => {
if (exhibit.audioUrl) {
const audio: AudioItem = {
id: `media-${exhibit.id}`,
name: exhibit.title,
audioUrl: exhibit.audioUrl,
image: exhibit.coverImage,
duration: exhibit.audioDuration
}
const isSameAudio = currentAudio.value?.id === audio.id
if (showAudioPlayer.value && isSameAudio && isAudioPlaying.value) {
audioPlayerRef.value?.pause()
return
}
currentAudio.value = audio
showAudioPlayer.value = true
audioPlayerRef.value?.play(audio)
const handlePanelBack = () => {
if (explainStage.value === 'stop') {
explainStage.value = 'unit'
selectedExplainBusinessUnitId.value = ''
selectedExplainBusinessUnitName.value = ''
explainError.value = ''
return
}
const selection = await explainUseCase.selectAudioForExhibit(exhibit.id)
if (!selection?.playable || !selection.media?.url) {
uni.showToast({
title: selection?.unavailableMessage || exhibit.audioStatusText || '该讲解暂无可播放音频',
icon: 'none'
})
if (explainStage.value === 'unit') {
explainStage.value = 'hall'
selectedExplainHallId.value = ''
selectedExplainHallName.value = ''
selectedExplainBusinessUnitId.value = ''
selectedExplainBusinessUnitName.value = ''
explainBusinessUnits.value = []
explainError.value = ''
return
}
currentAudio.value = {
id: selection.media.id,
name: selection.track?.title || selection.exhibit.name,
audioUrl: selection.media.url,
image: selection.exhibit.image,
duration: selection.media.duration
}
showAudioPlayer.value = true
uni.showToast({
title: '音频已准备好,请点击底部播放按钮',
icon: 'none'
})
}
const clearAudioState = () => {
showAudioPlayer.value = false
isAudioPlaying.value = false
currentAudio.value = null
}
const handleAudioVisibleChange = (visible: boolean) => {
showAudioPlayer.value = visible
if (!visible) {
isAudioPlaying.value = false
currentAudio.value = null
}
}
const handleAudioPlay = () => {
isAudioPlaying.value = true
}
const handleAudioPause = () => {
isAudioPlaying.value = false
}
const handleAudioEnded = () => {
clearAudioState()
}
const handleAudioError = (_audio: AudioItem | null, message: string) => {
console.warn('全部讲解音频不可播放:', message)
clearAudioState()
}
const handleExplainLocationClick = async (exhibit: Exhibit) => {
if (!exhibit.poiId) {
uni.showToast({
title: '该讲解暂无三维位置数据',
icon: 'none'
})
return
}
const matchedPoi = await guideUseCase.getPoiById(exhibit.poiId)
if (!matchedPoi) {
uni.showToast({
title: '该讲解位置尚未映射到当前三维导览资源',
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.poiId)}&target=${encodeURIComponent(exhibit.title)}&state=preview`
})
handleBack()
}
</script>