feat: 临时改造讲解页方案 B 链路
接入展厅列表、展厅讲解点分组生成临时业务单元,并迁移讲解详情播放正文到新接口链路。
This commit is contained in:
@@ -54,6 +54,15 @@
|
||||
<view class="content-section">
|
||||
<text class="section-title">讲解内容</text>
|
||||
<text class="section-text">{{ exhibit.body || exhibit.summary }}</text>
|
||||
<view
|
||||
v-if="canExpandText"
|
||||
class="text-expand-btn"
|
||||
:class="{ disabled: textLoading }"
|
||||
@tap="handleExpandText"
|
||||
>
|
||||
<text class="text-expand-label">{{ textLoading ? '正在加载讲解词' : '展开讲解词全文' }}</text>
|
||||
</view>
|
||||
<text v-if="textError" class="text-error">{{ textError }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -102,6 +111,9 @@ import {
|
||||
toExplainDetailPageViewModel,
|
||||
type ExplainDetailPageViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import type {
|
||||
AudioPlayTargetType
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
isGuideTopTab,
|
||||
type GuideTopTab
|
||||
@@ -117,7 +129,7 @@ const defaultDetail: ExplainDetailPageViewModel = {
|
||||
body: '该讲解内容待补充。',
|
||||
audio: {
|
||||
status: 'unavailable',
|
||||
unavailableReason: '该讲解暂无已发布音频,当前提供图文讲解。'
|
||||
unavailableReason: '当前语言暂无语音讲解。'
|
||||
},
|
||||
chapters: [],
|
||||
relatedItems: []
|
||||
@@ -135,6 +147,10 @@ const currentAudio = ref<AudioItem | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
const activeTopTab = ref<GuideTopTab>('explain')
|
||||
const resolvedHallId = ref('')
|
||||
const textLoading = ref(false)
|
||||
const textExpanded = ref(false)
|
||||
const textError = ref('')
|
||||
const retryingAudio = ref(false)
|
||||
|
||||
const heroImage = computed(() => exhibit.value.coverImages[0])
|
||||
const detailMeta = computed(() => [
|
||||
@@ -142,6 +158,11 @@ const detailMeta = computed(() => [
|
||||
exhibit.value.floorLabel
|
||||
].filter(Boolean).join(' · '))
|
||||
const hallLocationId = computed(() => exhibit.value.hallId || resolvedHallId.value)
|
||||
const canExpandText = computed(() => (
|
||||
exhibit.value.audio.hasText === true
|
||||
&& !textExpanded.value
|
||||
&& !textLoading.value
|
||||
))
|
||||
|
||||
// 讲解详情页只定位到所属展厅,不再追踪具体展品点位。
|
||||
const resolveHallGuidePoi = async () => {
|
||||
@@ -163,51 +184,44 @@ onLoad(async (options: any = {}) => {
|
||||
const exhibitId = Array.isArray(options.id) ? options.id[0] : options.id
|
||||
if (!exhibitId) return
|
||||
|
||||
const exhibitData = await explainUseCase.getExhibitById(exhibitId)
|
||||
if (exhibitData) {
|
||||
const rawTargetType = Array.isArray(options.targetType) ? options.targetType[0] : options.targetType
|
||||
const targetType: AudioPlayTargetType | undefined = rawTargetType === 'ITEM' || rawTargetType === 'STOP'
|
||||
? rawTargetType
|
||||
: undefined
|
||||
const rawTargetId = Array.isArray(options.targetId) ? options.targetId[0] : options.targetId
|
||||
const targetId = rawTargetId ? String(rawTargetId) : undefined
|
||||
|
||||
try {
|
||||
const exhibitData = await explainUseCase.enterExplainDetail({
|
||||
exhibitId,
|
||||
targetType,
|
||||
targetId
|
||||
})
|
||||
exhibit.value = toExplainDetailPageViewModel(exhibitData)
|
||||
if (exhibitData.hallId) {
|
||||
resolvedHallId.value = exhibitData.hallId
|
||||
}
|
||||
void explainUseCase.enrichExhibitDetailAudio(exhibitId)
|
||||
.then((enrichedExhibit) => {
|
||||
if (!enrichedExhibit || exhibit.value.id !== exhibitId) return
|
||||
|
||||
exhibit.value = toExplainDetailPageViewModel(enrichedExhibit)
|
||||
if (enrichedExhibit.hallId) {
|
||||
resolvedHallId.value = enrichedExhibit.hallId
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('讲解详情音频正文后台补充失败:', error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('讲解详情加载失败:', error)
|
||||
uni.showToast({
|
||||
title: '讲解详情加载失败,请稍后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const handlePlayAudio = async () => {
|
||||
if (exhibit.value.audio.url) {
|
||||
const audio: AudioItem = {
|
||||
id: `media-${exhibit.value.id}`,
|
||||
name: exhibit.value.title,
|
||||
audioUrl: exhibit.value.audio.url,
|
||||
image: heroImage.value,
|
||||
duration: undefined
|
||||
}
|
||||
|
||||
const isSameAudio = currentAudio.value?.id === audio.id
|
||||
if (showAudioPlayer.value && isSameAudio && isPlaying.value) {
|
||||
audioPlayerRef.value?.pause()
|
||||
return
|
||||
}
|
||||
|
||||
showAudioPlayer.value = true
|
||||
currentAudio.value = audio
|
||||
audioPlayerRef.value?.play(audio)
|
||||
return
|
||||
}
|
||||
|
||||
const selection = exhibit.value.id
|
||||
? await explainUseCase.selectAudioForExhibit(exhibit.value.id)
|
||||
? await explainUseCase.selectAudioForExplainDetail({
|
||||
id: exhibit.value.id,
|
||||
name: exhibit.value.title,
|
||||
image: heroImage.value,
|
||||
audioStatus: exhibit.value.audio.status === 'playable' ? 'READY' : 'MISSING',
|
||||
audioLanguage: exhibit.value.audio.language,
|
||||
audioUnavailableReason: exhibit.value.audio.unavailableReason,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId
|
||||
})
|
||||
: null
|
||||
|
||||
if (!selection?.playable || !selection.media?.url) {
|
||||
@@ -220,9 +234,9 @@ const handlePlayAudio = async () => {
|
||||
|
||||
const audio: AudioItem = {
|
||||
id: selection.media.id,
|
||||
name: selection.track?.title || selection.exhibit.name,
|
||||
name: selection.playInfo?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: selection.exhibit.image || heroImage.value,
|
||||
image: heroImage.value,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
|
||||
@@ -235,10 +249,41 @@ const handlePlayAudio = async () => {
|
||||
showAudioPlayer.value = true
|
||||
currentAudio.value = audio
|
||||
await nextTick()
|
||||
uni.showToast({
|
||||
title: '音频已准备好,请点击底部播放按钮',
|
||||
icon: 'none'
|
||||
})
|
||||
audioPlayerRef.value?.play(audio)
|
||||
}
|
||||
|
||||
const handleExpandText = async () => {
|
||||
if (!exhibit.value.id || textLoading.value || textExpanded.value) return
|
||||
|
||||
textLoading.value = true
|
||||
textError.value = ''
|
||||
|
||||
try {
|
||||
const selection = await explainUseCase.loadExplainDetailText({
|
||||
id: exhibit.value.id,
|
||||
name: exhibit.value.title,
|
||||
image: heroImage.value,
|
||||
audioLanguage: exhibit.value.audio.language,
|
||||
audioStatus: exhibit.value.audio.status === 'playable' ? 'READY' : 'MISSING',
|
||||
hallId: exhibit.value.hallId,
|
||||
hallName: exhibit.value.hallName,
|
||||
floorLabel: exhibit.value.floorLabel,
|
||||
location: exhibit.value.location,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId,
|
||||
audioHasText: exhibit.value.audio.hasText
|
||||
})
|
||||
|
||||
if (!selection.available) {
|
||||
textError.value = selection.unavailableMessage || '当前语言暂无讲解词'
|
||||
return
|
||||
}
|
||||
|
||||
exhibit.value = toExplainDetailPageViewModel(selection.exhibit)
|
||||
textExpanded.value = true
|
||||
} finally {
|
||||
textLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioVisibleChange = (visible: boolean) => {
|
||||
@@ -250,6 +295,7 @@ const handleAudioVisibleChange = (visible: boolean) => {
|
||||
}
|
||||
|
||||
const handleAudioPlay = () => {
|
||||
retryingAudio.value = false
|
||||
isPlaying.value = true
|
||||
}
|
||||
|
||||
@@ -261,11 +307,43 @@ const handleAudioEnded = () => {
|
||||
isPlaying.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
retryingAudio.value = false
|
||||
}
|
||||
|
||||
const handleAudioError = (_audio: AudioItem | null, message: string) => {
|
||||
const handleAudioError = async (_audio: AudioItem | null, message: string) => {
|
||||
console.warn('详情音频不可播放:', message)
|
||||
isPlaying.value = false
|
||||
|
||||
if (!retryingAudio.value && exhibit.value.audio.status === 'playable') {
|
||||
retryingAudio.value = true
|
||||
const selection = await explainUseCase.selectAudioForExplainDetail({
|
||||
id: exhibit.value.id,
|
||||
name: exhibit.value.title,
|
||||
image: heroImage.value,
|
||||
audioStatus: 'READY',
|
||||
audioLanguage: exhibit.value.audio.language,
|
||||
playTargetType: exhibit.value.audio.playTargetType,
|
||||
playTargetId: exhibit.value.audio.playTargetId
|
||||
})
|
||||
|
||||
if (selection.playable && selection.media?.url) {
|
||||
const retryAudio: AudioItem = {
|
||||
id: selection.media.id,
|
||||
name: selection.playInfo?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: heroImage.value,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
|
||||
currentAudio.value = retryAudio
|
||||
showAudioPlayer.value = true
|
||||
await nextTick()
|
||||
audioPlayerRef.value?.play(retryAudio)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
retryingAudio.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
exhibit.value = {
|
||||
@@ -485,6 +563,39 @@ const handleBack = () => {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.text-expand-btn {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #151713;
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.text-expand-btn.disabled {
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.text-expand-label {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
font-weight: 700;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: #8a4a31;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -203,8 +203,18 @@ const itemMeta = (exhibit: ExplainExhibitViewModel) => {
|
||||
}
|
||||
|
||||
const handleExhibitClick = (exhibit: ExplainExhibitViewModel) => {
|
||||
const params = new URLSearchParams({
|
||||
id: exhibit.id,
|
||||
tab: activeTopTab.value
|
||||
})
|
||||
|
||||
if (exhibit.playTargetType && exhibit.playTargetId) {
|
||||
params.set('targetType', exhibit.playTargetType)
|
||||
params.set('targetId', exhibit.playTargetId)
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${encodeURIComponent(exhibit.id)}&tab=${activeTopTab.value}`
|
||||
url: `/pages/exhibit/detail?${params.toString()}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -204,9 +204,16 @@
|
||||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||||
<ExplainHallSelect
|
||||
:halls="explainHallItems"
|
||||
:business-units="explainBusinessUnitItems"
|
||||
:guide-stops="explainGuideStopItems"
|
||||
:stage="explainStage"
|
||||
:selected-hall-name="selectedExplainHallName"
|
||||
:selected-business-unit-name="selectedExplainBusinessUnitName"
|
||||
:loading="explainLoading"
|
||||
:error="explainError"
|
||||
@hall-click="handleExplainHallClick"
|
||||
@business-unit-click="handleExplainBusinessUnitClick"
|
||||
@guide-stop-click="handleExplainGuideStopClick"
|
||||
@back="handleExplainBack"
|
||||
/>
|
||||
</view>
|
||||
@@ -224,6 +231,9 @@ import type {
|
||||
RoutePointOption
|
||||
} from '@/components/navigation/RoutePointPicker.vue'
|
||||
import ExplainHallSelect, {
|
||||
type ExplainBusinessUnitSelectItem,
|
||||
type ExplainGuideStopSelectItem,
|
||||
type ExplainSelectStage,
|
||||
type ExplainHallSelectItem
|
||||
} from '@/components/explain/ExplainHallSelect.vue'
|
||||
import {
|
||||
@@ -246,7 +256,7 @@ import type {
|
||||
import type {
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget,
|
||||
MuseumExhibit,
|
||||
ExplainBusinessUnit,
|
||||
MuseumFloor,
|
||||
MuseumHall
|
||||
} from '@/domain/museum'
|
||||
@@ -435,10 +445,40 @@ const routeSimulationStepText = computed(() => {
|
||||
})
|
||||
|
||||
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('')
|
||||
let explainLoadPromise: Promise<void> | null = null
|
||||
|
||||
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
|
||||
})) || []
|
||||
))
|
||||
|
||||
// Tab 切换处理
|
||||
const syncTopTabToUrl = (tabId: GuideTopTab) => {
|
||||
if (typeof window === 'undefined') return
|
||||
@@ -452,7 +492,7 @@ const syncTopTabToUrl = (tabId: GuideTopTab) => {
|
||||
|
||||
const loadTopTabData = (tabId: GuideTopTab) => {
|
||||
if (tabId === 'explain') {
|
||||
void loadExplainExhibits()
|
||||
void loadExplainHalls()
|
||||
} else {
|
||||
void loadGuideFloors()
|
||||
}
|
||||
@@ -554,7 +594,7 @@ const loadGuideFloors = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadExplainExhibits = async () => {
|
||||
const loadExplainHalls = async () => {
|
||||
if (explainHallItems.value.length) return
|
||||
if (explainLoadPromise) return explainLoadPromise
|
||||
|
||||
@@ -563,11 +603,8 @@ const loadExplainExhibits = async () => {
|
||||
|
||||
explainLoadPromise = (async () => {
|
||||
try {
|
||||
const [halls, exhibits] = await Promise.all([
|
||||
explainUseCase.listHalls(),
|
||||
explainUseCase.listExplainExhibits()
|
||||
])
|
||||
explainHallItems.value = buildExplainHallItems(halls, exhibits)
|
||||
const halls = await explainUseCase.loadExplainHalls()
|
||||
explainHallItems.value = buildExplainHallItems(halls)
|
||||
} catch (error) {
|
||||
console.error('加载讲解内容失败:', error)
|
||||
explainError.value = '讲解内容加载失败,请稍后重试'
|
||||
@@ -584,52 +621,14 @@ const loadExplainExhibits = async () => {
|
||||
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
|
||||
|
||||
const buildExplainHallItems = (
|
||||
halls: Awaited<ReturnType<typeof explainUseCase.listHalls>>,
|
||||
exhibits: MuseumExhibit[]
|
||||
halls: Awaited<ReturnType<typeof explainUseCase.loadExplainHalls>>
|
||||
): ExplainHallSelectItem[] => {
|
||||
const countsByHallId = new Map<string, number>()
|
||||
const countsByHallName = new Map<string, number>()
|
||||
const keywordsByHallId = new Map<string, Set<string>>()
|
||||
const keywordsByHallName = new Map<string, Set<string>>()
|
||||
|
||||
exhibits.forEach((exhibit) => {
|
||||
const keywordParts = [
|
||||
exhibit.name,
|
||||
exhibit.hallName,
|
||||
exhibit.floorLabel,
|
||||
exhibit.description,
|
||||
exhibit.guideTitle,
|
||||
exhibit.guideText,
|
||||
...(exhibit.tags || [])
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
if (exhibit.hallId) {
|
||||
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
|
||||
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
|
||||
keywordParts.forEach((part) => keywords.add(part))
|
||||
keywordsByHallId.set(exhibit.hallId, keywords)
|
||||
}
|
||||
|
||||
if (exhibit.hallName) {
|
||||
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
|
||||
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
|
||||
keywordParts.forEach((part) => keywords.add(part))
|
||||
keywordsByHallName.set(exhibit.hallName, keywords)
|
||||
}
|
||||
})
|
||||
|
||||
return halls.map((hall) => {
|
||||
const explainCount = countsByHallId.get(hall.id)
|
||||
|| countsByHallName.get(hall.name)
|
||||
|| hall.exhibitCount
|
||||
|| 0
|
||||
const keywordParts = [
|
||||
hall.name,
|
||||
hall.floorLabel,
|
||||
hall.description,
|
||||
hall.area,
|
||||
...Array.from(keywordsByHallId.get(hall.id) || []),
|
||||
...Array.from(keywordsByHallName.get(hall.name) || [])
|
||||
hall.area
|
||||
].filter(Boolean) as string[]
|
||||
|
||||
return {
|
||||
@@ -637,7 +636,7 @@ const buildExplainHallItems = (
|
||||
name: hall.name,
|
||||
floorLabel: hall.floorLabel,
|
||||
image: hall.image,
|
||||
explainCount,
|
||||
explainCount: hall.exhibitCount || 0,
|
||||
searchText: normalizeExplainKeyword(keywordParts.join(' '))
|
||||
}
|
||||
})
|
||||
@@ -1222,13 +1221,67 @@ const guideSearchText = computed(() => {
|
||||
})
|
||||
|
||||
// 讲解页面处理
|
||||
const handleExplainHallClick = (hallId: string) => {
|
||||
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 = []
|
||||
explainError.value = ''
|
||||
explainLoading.value = true
|
||||
|
||||
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/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
|
||||
url: `/pages/exhibit/detail?${params.toString()}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleExplainBack = () => {
|
||||
if (explainStage.value === 'stop') {
|
||||
explainStage.value = 'unit'
|
||||
selectedExplainBusinessUnitId.value = ''
|
||||
selectedExplainBusinessUnitName.value = ''
|
||||
explainError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (explainStage.value === 'unit') {
|
||||
explainStage.value = 'hall'
|
||||
selectedExplainHallId.value = ''
|
||||
selectedExplainHallName.value = ''
|
||||
selectedExplainBusinessUnitId.value = ''
|
||||
selectedExplainBusinessUnitName.value = ''
|
||||
explainBusinessUnits.value = []
|
||||
explainError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
currentTab.value = 'guide'
|
||||
syncTopTabToUrl('guide')
|
||||
loadTopTabData('guide')
|
||||
|
||||
Reference in New Issue
Block a user