chore: freeze guide and explain update
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
<GuidePageFrame
|
||||
:active-tab="activeTopTab"
|
||||
variant="static"
|
||||
show-back
|
||||
@tab-change="handleTopTabChange"
|
||||
@back="handleBack"
|
||||
>
|
||||
<view class="detail-page">
|
||||
<view class="detail-page" :class="{ 'with-audio-player': showAudioPlayer }">
|
||||
<scroll-view class="content" scroll-y>
|
||||
<view class="detail-hero">
|
||||
<image
|
||||
@@ -59,7 +61,7 @@
|
||||
</view>
|
||||
|
||||
<view v-if="exhibit.audio.status !== 'playable'" class="audio-note">
|
||||
<text class="audio-note-title">音频待同步</text>
|
||||
<text class="audio-note-title">{{ audioNoteTitle }}</text>
|
||||
<text class="audio-note-desc">
|
||||
{{ exhibit.audio.unavailableReason || '当前仅提供图文讲解,正式音频接入后将显示播放入口。' }}
|
||||
</text>
|
||||
@@ -107,7 +109,7 @@
|
||||
|
||||
<view v-if="exhibit.location" class="location-note">
|
||||
<text class="location-title">位置说明</text>
|
||||
<text class="location-desc">当前支持三维位置预览,馆内路线规划待开放。</text>
|
||||
<text class="location-desc">{{ locationDescription }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -143,14 +145,27 @@
|
||||
<text class="btn-text">分享</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<AudioPlayer
|
||||
ref="audioPlayerRef"
|
||||
:visible="showAudioPlayer"
|
||||
:audio="currentAudio"
|
||||
:auto-play="true"
|
||||
@update:visible="handleAudioVisibleChange"
|
||||
@play="handleAudioPlay"
|
||||
@pause="handleAudioPause"
|
||||
@ended="handleAudioEnded"
|
||||
@error="handleAudioError"
|
||||
/>
|
||||
</view>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
@@ -174,17 +189,25 @@ const defaultDetail: ExplainDetailPageViewModel = {
|
||||
subtitle: '位置待补充',
|
||||
contentType: 'exhibit',
|
||||
coverImages: ['/static/exhibit-placeholder.jpg'],
|
||||
summary: '该讲解内容待 SGS 场景设置或 CMS 内容补充。',
|
||||
body: '该讲解内容待 SGS 场景设置或 CMS 内容补充。',
|
||||
summary: '该讲解内容待补充。',
|
||||
body: '该讲解内容待补充。',
|
||||
audio: {
|
||||
status: 'unavailable',
|
||||
unavailableReason: 'SGS 场景设置音频资源尚未同步到当前 H5 项目'
|
||||
unavailableReason: '该展项暂无已发布音频,当前提供图文讲解。'
|
||||
},
|
||||
chapters: [],
|
||||
relatedItems: []
|
||||
}
|
||||
|
||||
interface AudioPlayerExpose {
|
||||
play: (audio: AudioItem) => void
|
||||
pause: () => void
|
||||
}
|
||||
|
||||
const exhibit = ref<ExplainDetailPageViewModel>(defaultDetail)
|
||||
const audioPlayerRef = ref<AudioPlayerExpose | null>(null)
|
||||
const showAudioPlayer = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
const isCollected = ref(false)
|
||||
const activeTopTab = ref<GuideTopTab>('guide')
|
||||
@@ -197,9 +220,15 @@ const audioStatusText = computed(() => {
|
||||
if (exhibit.value.audio.status === 'none') {
|
||||
return '图文讲解'
|
||||
}
|
||||
return exhibit.value.audio.unavailableReason?.includes('包含音频地址') ? '音频待同步' : '图文讲解'
|
||||
return '图文讲解'
|
||||
})
|
||||
|
||||
const audioNoteTitle = computed(() => (
|
||||
exhibit.value.audio.status === 'playable'
|
||||
? '音频讲解'
|
||||
: '图文讲解'
|
||||
))
|
||||
|
||||
const contentTypeText = computed(() => {
|
||||
const map: Record<ExplainContentType, string> = {
|
||||
hall: '空间讲解',
|
||||
@@ -210,6 +239,10 @@ const contentTypeText = computed(() => {
|
||||
return map[exhibit.value.contentType]
|
||||
})
|
||||
|
||||
const locationDescription = computed(() => (
|
||||
exhibit.value.location?.note || '当前支持三维位置预览。'
|
||||
))
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
if (options.id) {
|
||||
const exhibitData = await explainUseCase.getExhibitById(options.id)
|
||||
@@ -231,6 +264,31 @@ const formatChapterTime = (seconds: number) => {
|
||||
}
|
||||
|
||||
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
|
||||
await nextTick()
|
||||
|
||||
if (!isPlaying.value) {
|
||||
audioPlayerRef.value?.play(audio)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const selection = exhibit.value.id
|
||||
? await explainUseCase.selectAudioForExhibit(exhibit.value.id)
|
||||
: null
|
||||
@@ -243,7 +301,56 @@ const handlePlayAudio = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
isPlaying.value = !isPlaying.value
|
||||
const audio: AudioItem = {
|
||||
id: selection.media.id,
|
||||
name: selection.track?.title || selection.exhibit.name,
|
||||
audioUrl: selection.media.url,
|
||||
image: selection.exhibit.image || heroImage.value,
|
||||
duration: selection.media.duration
|
||||
}
|
||||
|
||||
const isSameAudio = currentAudio.value?.id === audio.id
|
||||
if (showAudioPlayer.value && isSameAudio && isPlaying.value) {
|
||||
audioPlayerRef.value?.pause()
|
||||
return
|
||||
}
|
||||
|
||||
showAudioPlayer.value = true
|
||||
currentAudio.value = audio
|
||||
await nextTick()
|
||||
|
||||
if (!isPlaying.value) {
|
||||
audioPlayerRef.value?.play(audio)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioVisibleChange = (visible: boolean) => {
|
||||
showAudioPlayer.value = visible
|
||||
if (!visible) {
|
||||
isPlaying.value = false
|
||||
currentAudio.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioPlay = () => {
|
||||
isPlaying.value = true
|
||||
}
|
||||
|
||||
const handleAudioPause = () => {
|
||||
isPlaying.value = false
|
||||
}
|
||||
|
||||
const handleAudioEnded = () => {
|
||||
isPlaying.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
}
|
||||
|
||||
const handleAudioError = (_audio: AudioItem | null, message: string) => {
|
||||
console.warn('详情音频不可播放:', message)
|
||||
isPlaying.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
}
|
||||
|
||||
const handleNavigate = async () => {
|
||||
@@ -284,6 +391,17 @@ const handleShare = () => {
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
navigateToGuideTopTab(tab)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -530,6 +648,10 @@ const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
border-top: 1px solid #e7e9e0;
|
||||
}
|
||||
|
||||
.detail-page.with-audio-player .action-bar {
|
||||
margin-bottom: 88px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 46px;
|
||||
display: flex;
|
||||
|
||||
221
src/pages/explain/list.vue
Normal file
221
src/pages/explain/list.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<GuidePageFrame
|
||||
active-tab="explain"
|
||||
variant="static"
|
||||
show-back
|
||||
back-label="讲解首页"
|
||||
@tab-change="handleTopTabChange"
|
||||
@back="handleBack"
|
||||
>
|
||||
<view class="explain-all-page">
|
||||
<ExplainList
|
||||
:cards="explainExhibits"
|
||||
: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
|
||||
:visible="showAudioPlayer"
|
||||
:audio="currentAudio"
|
||||
:auto-play="true"
|
||||
@update:visible="handleAudioVisibleChange"
|
||||
@play="handleAudioPlay"
|
||||
@pause="handleAudioPause"
|
||||
@ended="handleAudioEnded"
|
||||
@error="handleAudioError"
|
||||
/>
|
||||
</view>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { 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 {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
toExplainCardViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
|
||||
const explainExhibits = ref<Exhibit[]>([])
|
||||
const explainLoading = ref(false)
|
||||
const explainError = ref('')
|
||||
const showAudioPlayer = ref(false)
|
||||
const isAudioPlaying = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
let explainLoadPromise: Promise<void> | null = null
|
||||
|
||||
const loadExplainExhibits = async () => {
|
||||
if (explainExhibits.value.length) return
|
||||
if (explainLoadPromise) return explainLoadPromise
|
||||
|
||||
explainLoading.value = true
|
||||
explainError.value = ''
|
||||
|
||||
explainLoadPromise = (async () => {
|
||||
try {
|
||||
const exhibits = await explainUseCase.listExplainExhibits()
|
||||
explainExhibits.value = exhibits.map(toExplainCardViewModel)
|
||||
} catch (error) {
|
||||
console.error('加载全部讲解内容失败:', error)
|
||||
explainError.value = '讲解内容加载失败,请稍后重试'
|
||||
explainExhibits.value = []
|
||||
} finally {
|
||||
explainLoading.value = false
|
||||
explainLoadPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return explainLoadPromise
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
void loadExplainExhibits()
|
||||
})
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
navigateToGuideTopTab(tab)
|
||||
}
|
||||
|
||||
const handleExplainExhibitClick = (exhibit: Exhibit) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${exhibit.id}&tab=explain`
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
currentAudio.value = {
|
||||
id: `media-${exhibit.id}`,
|
||||
name: exhibit.title,
|
||||
audioUrl: exhibit.audioUrl,
|
||||
image: exhibit.coverImage,
|
||||
duration: exhibit.audioDuration
|
||||
}
|
||||
showAudioPlayer.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const selection = await explainUseCase.selectAudioForExhibit(exhibit.id)
|
||||
|
||||
if (!selection?.playable || !selection.media?.url) {
|
||||
uni.showToast({
|
||||
title: selection?.unavailableMessage || exhibit.audioStatusText || '该讲解暂无可播放音频',
|
||||
icon: 'none'
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
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`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.explain-all-page {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #edf0ea;
|
||||
}
|
||||
</style>
|
||||
@@ -13,8 +13,11 @@
|
||||
search-top="60px"
|
||||
mode-top="104px"
|
||||
floor-top="208px"
|
||||
tools-top="450px"
|
||||
:tools="['回正', '2D']"
|
||||
tools-top="176px"
|
||||
:active-mode="activeMode"
|
||||
:active-floor="activeFloor"
|
||||
:map-type="activeMode === '2d' ? 'outdoor' : 'indoor'"
|
||||
:tools="mapTools"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
@@ -31,7 +34,7 @@
|
||||
|
||||
<view class="detail-lines">
|
||||
<text class="detail-line">所在区域:{{ facility.location }}</text>
|
||||
<text class="detail-line">最近垂直交通:{{ facility.traffic }}</text>
|
||||
<text class="detail-line">能力说明:{{ facility.traffic }}</text>
|
||||
<text v-if="confirmedStart" class="detail-line">预览起点:{{ confirmedStart.label }}</text>
|
||||
</view>
|
||||
|
||||
@@ -67,7 +70,7 @@
|
||||
@tap.stop=""
|
||||
>
|
||||
<text class="start-select-title">选择预览起点</text>
|
||||
<text class="start-select-desc">当前仅用于位置预览,不生成真实馆内路线。</text>
|
||||
<text class="start-select-desc">当前支持选择预览起点,用于三维位置预览。</text>
|
||||
|
||||
<view class="start-chip-section">
|
||||
<text class="start-chip-label">楼层</text>
|
||||
@@ -141,20 +144,23 @@ interface ConfirmedStart {
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
const activeMode = ref<'2d' | '3d'>('3d')
|
||||
const activeFloor = ref('1F')
|
||||
|
||||
const facility = ref<FacilityDetailViewModel>({
|
||||
id: '',
|
||||
name: '目标位置',
|
||||
status: '可预览',
|
||||
location: 'clean 导览数据点位',
|
||||
traffic: guideUseCase.getRouteReadiness().message,
|
||||
tags: ['三维位置', 'clean 数据']
|
||||
location: '中心线路网导览点位',
|
||||
traffic: guideUseCase.getRouteReadinessSnapshot().message,
|
||||
tags: ['三维位置', '中心线路网']
|
||||
})
|
||||
const isStartPanelOpen = ref(false)
|
||||
const selectedStartFloor = ref('1F')
|
||||
const selectedStartArea = ref('服务台附近')
|
||||
const confirmedStart = ref<ConfirmedStart | null>(null)
|
||||
const startFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||||
const mapTools = computed(() => ['重置', activeMode.value === '2d' ? '3D' : '2D'])
|
||||
const startAreas = ['主入口', '服务台附近', '停车场入口']
|
||||
|
||||
const pendingStartLabel = computed(() => `${selectedStartFloor.value} ${selectedStartArea.value}`)
|
||||
@@ -179,10 +185,13 @@ const loadGuideFloors = async () => {
|
||||
try {
|
||||
const floors = await guideUseCase.getFloors()
|
||||
guideFloors.value = floors
|
||||
selectedStartFloor.value = floors.find((floor) => floor.label === selectedStartFloor.value)?.label
|
||||
const defaultFloor = floors.find((floor) => floor.label === selectedStartFloor.value)?.label
|
||||
|| floors.find((floor) => floor.id === 'L1')?.label
|
||||
|| floors[0]?.label
|
||||
|| selectedStartFloor.value
|
||||
selectedStartFloor.value = defaultFloor
|
||||
activeFloor.value = floors.find((floor) => floor.label === activeFloor.value)?.label
|
||||
|| defaultFloor
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
@@ -203,15 +212,16 @@ onLoad(async (options: any) => {
|
||||
if (!facility.value.id) return
|
||||
|
||||
try {
|
||||
const routeReadiness = await guideUseCase.getRouteReadiness()
|
||||
const poi = await guideUseCase.getPoiById(facility.value.id)
|
||||
if (!poi) return
|
||||
|
||||
facility.value = toFacilityDetailViewModel(
|
||||
poi,
|
||||
guideUseCase.getRouteReadiness().message
|
||||
routeReadiness.message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('加载 clean 设施详情失败:', error)
|
||||
console.error('加载中心线路网设施详情失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -271,15 +281,36 @@ const handleStartNavigation = () => {
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
console.log('设施详情切换导览模式:', mode)
|
||||
activeMode.value = mode
|
||||
uni.showToast({
|
||||
title: mode === '2d' ? '已切换室外入口参考' : '已切换室内三维位置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
console.log('设施详情切换楼层:', floor)
|
||||
activeFloor.value = floor
|
||||
}
|
||||
|
||||
const handleToolClick = (tool: string) => {
|
||||
console.log('设施详情工具:', tool)
|
||||
if (tool === '2D') {
|
||||
handleModeChange('2d')
|
||||
return
|
||||
}
|
||||
|
||||
if (tool === '3D') {
|
||||
handleModeChange('3d')
|
||||
return
|
||||
}
|
||||
|
||||
if (tool === '重置') {
|
||||
activeMode.value = '3d'
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: tool === '重置' ? '已回到三维位置预览' : '该工具暂未开放',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<GuidePageFrame
|
||||
:active-tab="activeTopTab"
|
||||
variant="static"
|
||||
show-back
|
||||
@tab-change="handleTopTabChange"
|
||||
@back="handleBack"
|
||||
>
|
||||
<view class="detail-page">
|
||||
<scroll-view class="content" scroll-y>
|
||||
@@ -47,8 +49,12 @@
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn primary" @tap="handleNavigate">
|
||||
<text class="btn-text">查看三维位置</text>
|
||||
<view
|
||||
class="action-btn primary"
|
||||
:class="{ disabled: !hall.location?.poiId }"
|
||||
@tap="handleNavigate"
|
||||
>
|
||||
<text class="btn-text">{{ hall.location?.actionText || '暂无位置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -84,7 +90,7 @@ const hall = ref<HallDetailViewModel>({
|
||||
id: '',
|
||||
name: '展厅内容',
|
||||
floorLabel: '楼层待补充',
|
||||
description: '该空间介绍待 SGS 场景设置或 CMS 内容补充。',
|
||||
description: '该展厅暂无介绍文案。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 0,
|
||||
area: '待补充'
|
||||
@@ -140,6 +146,17 @@ const handleNavigate = async () => {
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
navigateToGuideTopTab(tab)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -106,7 +106,7 @@
|
||||
<view v-else-if="routeViewState === 'outdoor-preview'" class="outdoor-preview-card">
|
||||
<text class="outdoor-preview-title">室外地图预览:{{ route.target }}</text>
|
||||
<text class="outdoor-preview-desc">
|
||||
当前仅展示室外地图参考。真实馆内路线需等待 route_graph/nav_data 接入。
|
||||
室外地图仅作入口参考;馆内当前支持三维位置预览。
|
||||
</text>
|
||||
<view class="outdoor-preview-actions">
|
||||
<view class="outdoor-preview-btn secondary" @tap="handleReturnToPreview">
|
||||
@@ -142,6 +142,7 @@ import type {
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
saveLastGuideLocationPreviewUrl,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
|
||||
@@ -269,7 +270,7 @@ const shellConfig = computed(() => {
|
||||
}
|
||||
|
||||
return {
|
||||
searchText: `到 ${route.value.target}`,
|
||||
searchText: `位置预览:${route.value.target}`,
|
||||
searchTop: '60px',
|
||||
activeMode: '3d' as GuideMode,
|
||||
activeFloor: activeFloor.value,
|
||||
@@ -297,6 +298,40 @@ const normalizeStringOption = (value: unknown) => {
|
||||
return typeof normalizedValue === 'string' ? safeDecode(normalizedValue).trim() : ''
|
||||
}
|
||||
|
||||
const setLocationPreviewTitle = () => {
|
||||
if (typeof uni !== 'undefined') {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '位置预览'
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.title = '位置预览'
|
||||
}
|
||||
}
|
||||
|
||||
const saveCurrentLocationPreviewUrl = () => {
|
||||
if (!route.value.facilityId) return
|
||||
|
||||
const params = new URLSearchParams({
|
||||
facilityId: route.value.facilityId,
|
||||
target: route.value.target,
|
||||
state: routeViewState.value
|
||||
})
|
||||
|
||||
if (route.value.startLocation?.label) {
|
||||
params.set('startLabel', route.value.startLocation.label)
|
||||
}
|
||||
if (route.value.startLocation?.floor) {
|
||||
params.set('startFloor', route.value.startLocation.floor)
|
||||
}
|
||||
if (route.value.startLocation?.source) {
|
||||
params.set('startSource', route.value.startLocation.source)
|
||||
}
|
||||
|
||||
saveLastGuideLocationPreviewUrl(`/pages/route/detail?${params.toString()}`)
|
||||
}
|
||||
|
||||
const createStartLocationFromOptions = (options: Record<string, unknown>) => {
|
||||
const label = normalizeStringOption(options.startLabel)
|
||||
|
||||
@@ -385,10 +420,14 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
activeFloor.value = matchedPoi.floorLabel || guideFloorLabel(matchedPoi.floorId)
|
||||
}
|
||||
|
||||
setLocationPreviewTitle()
|
||||
saveCurrentLocationPreviewUrl()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
setLocationPreviewTitle()
|
||||
void applyRouteOptions(options)
|
||||
})
|
||||
|
||||
@@ -441,6 +480,7 @@ const handleSearchTap = () => {
|
||||
const handleViewOutdoorMap = () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'outdoor-preview'
|
||||
saveCurrentLocationPreviewUrl()
|
||||
}
|
||||
|
||||
const handleShowTargetLocation = (target: Partial<RouteTargetLocation> | null) => {
|
||||
@@ -451,7 +491,13 @@ const handleShowTargetLocation = (target: Partial<RouteTargetLocation> | null) =
|
||||
title: '目标暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '已显示三维位置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||||
@@ -467,6 +513,7 @@ const handleReturnToPreview = () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
saveCurrentLocationPreviewUrl()
|
||||
}
|
||||
|
||||
const handleRelocate = () => {
|
||||
@@ -508,17 +555,20 @@ const handleConfirmLocation = async () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
saveCurrentLocationPreviewUrl()
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: GuideMode) => {
|
||||
if (routeViewState.value === 'outdoor-preview' && mode === '3d') {
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
saveCurrentLocationPreviewUrl()
|
||||
return
|
||||
}
|
||||
|
||||
if (routeViewState.value !== 'outdoor-preview' && mode === '2d') {
|
||||
routeViewState.value = 'outdoor-preview'
|
||||
saveCurrentLocationPreviewUrl()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -537,7 +587,7 @@ const handleToolClick = (tool: string) => {
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: tool === '回正' ? '已回到三维总览' : guideUseCase.getRouteReadiness().message,
|
||||
title: tool === '回正' ? '已回到三维总览' : guideUseCase.getRouteReadinessSnapshot().message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
search-top="60px"
|
||||
mode-top="104px"
|
||||
floor-top="208px"
|
||||
tools-top="176px"
|
||||
:active-mode="activeMode"
|
||||
:active-floor="activeFloor"
|
||||
:map-type="activeMode === '2d' ? 'outdoor' : 'indoor'"
|
||||
:tools="mapTools"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@floor-change="handleFloorChange"
|
||||
@tool-click="handleToolClick"
|
||||
>
|
||||
<template #overlay>
|
||||
<view
|
||||
@@ -124,10 +130,12 @@ interface FilterItem {
|
||||
|
||||
interface FacilityResult extends FacilityResultViewModel {}
|
||||
|
||||
const searchKeyword = ref('卫生间')
|
||||
const searchKeyword = ref('')
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
const activeMode = ref<'2d' | '3d'>('3d')
|
||||
const activeFloor = ref('1F')
|
||||
const searchDraftKeyword = ref('')
|
||||
const currentFilter = ref<FilterItem['id']>('all')
|
||||
const isLoading = ref(false)
|
||||
@@ -140,6 +148,7 @@ const filters: FilterItem[] = [
|
||||
{ id: 'accessible', label: '无障碍' },
|
||||
{ id: 'floor', label: '1F 点位' }
|
||||
]
|
||||
const mapTools = computed(() => ['重置', activeMode.value === '2d' ? '3D' : '2D'])
|
||||
|
||||
const facilities = ref<FacilityResult[]>([])
|
||||
|
||||
@@ -154,7 +163,7 @@ const visibleFacilities = computed(() => {
|
||||
})
|
||||
|
||||
const resultTitle = computed(() => {
|
||||
if (isLoading.value) return '正在读取 clean 导览数据'
|
||||
if (isLoading.value) return '正在读取中心线路网导览数据'
|
||||
return `${searchKeyword.value || '设施'} ${visibleFacilities.value.length} 个结果`
|
||||
})
|
||||
|
||||
@@ -166,7 +175,7 @@ const loadFacilityResults = async () => {
|
||||
.filter((poi) => poi.primaryCategory.id !== 'touring_poi')
|
||||
.map(toFacilityResultViewModel)
|
||||
} catch (error) {
|
||||
console.error('加载 clean 导览搜索结果失败:', error)
|
||||
console.error('加载中心线路网导览搜索结果失败:', error)
|
||||
facilities.value = []
|
||||
uni.showToast({
|
||||
title: '导览数据读取失败',
|
||||
@@ -179,7 +188,12 @@ const loadFacilityResults = async () => {
|
||||
|
||||
const loadGuideFloors = async () => {
|
||||
try {
|
||||
guideFloors.value = await guideUseCase.getFloors()
|
||||
const floors = await guideUseCase.getFloors()
|
||||
guideFloors.value = floors
|
||||
activeFloor.value = floors.find((floor) => floor.label === activeFloor.value)?.label
|
||||
|| floors.find((floor) => floor.id === 'L1')?.label
|
||||
|| floors[0]?.label
|
||||
|| activeFloor.value
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
@@ -250,11 +264,36 @@ const handleResultAction = (facility: FacilityResult) => {
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
console.log('切换搜索页导览模式:', mode)
|
||||
activeMode.value = mode
|
||||
uni.showToast({
|
||||
title: mode === '2d' ? '已切换室外入口参考' : '已切换室内三维位置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
console.log('搜索页切换楼层:', floor)
|
||||
activeFloor.value = floor
|
||||
}
|
||||
|
||||
const handleToolClick = (tool: string) => {
|
||||
if (tool === '2D') {
|
||||
handleModeChange('2d')
|
||||
return
|
||||
}
|
||||
|
||||
if (tool === '3D') {
|
||||
handleModeChange('3d')
|
||||
return
|
||||
}
|
||||
|
||||
if (tool === '重置') {
|
||||
activeMode.value = '3d'
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: tool === '重置' ? '已回到三维位置预览' : '该工具暂未开放',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
|
||||
Reference in New Issue
Block a user