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

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

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>

View File

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

View File

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