feat: wire guide data source and POI focus UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
<view class="action-btn-group">
|
||||
<view class="action-btn" @tap="handleNavigate">
|
||||
<text class="btn-icon">🗺️</text>
|
||||
<text class="btn-text">导航</text>
|
||||
<text class="btn-text">查看位置</text>
|
||||
</view>
|
||||
<view class="action-btn" @tap="handleCollect">
|
||||
<text class="btn-icon">{{ isCollected ? '❤️' : '🤍' }}</text>
|
||||
@@ -70,32 +70,39 @@
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toExplainDetailViewModel,
|
||||
type ExplainDetailViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import {
|
||||
isGuideTopTab,
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
|
||||
const exhibit = ref({
|
||||
id: '1',
|
||||
name: '蒙娜丽莎',
|
||||
artist: '列奥纳多·达·芬奇',
|
||||
year: '1503-1519',
|
||||
material: '木板油画',
|
||||
size: '77cm × 53cm',
|
||||
hall: '1号展厅 1F',
|
||||
const exhibit = ref<ExplainDetailViewModel>({
|
||||
id: '',
|
||||
name: '展项内容',
|
||||
hall: '展厅位置待补充',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '《蒙娜丽莎》是意大利文艺复兴时期画家列奥纳多·达·芬奇创作的油画,现收藏于法国卢浮宫博物馆。该画作主要表现了女性的典雅和恬静的典型形象,塑造了资本主义上升时期一位城市有产阶级的妇女形象。'
|
||||
description: '该展项介绍待正式内容库补充。',
|
||||
hasAudio: false,
|
||||
audioUnavailableReason: '正式讲解音频尚未接入媒体仓储'
|
||||
})
|
||||
|
||||
const isPlaying = ref(false)
|
||||
const isCollected = ref(false)
|
||||
const activeTopTab = ref<GuideTopTab>('guide')
|
||||
|
||||
onLoad((options: any) => {
|
||||
onLoad(async (options: any) => {
|
||||
if (options.id) {
|
||||
// 根据 ID 加载展品数据
|
||||
console.log('加载展品:', options.id)
|
||||
const exhibitData = await explainUseCase.getExhibitById(options.id)
|
||||
if (exhibitData) {
|
||||
exhibit.value = toExplainDetailViewModel(exhibitData)
|
||||
}
|
||||
}
|
||||
|
||||
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
||||
@@ -104,14 +111,29 @@ onLoad((options: any) => {
|
||||
}
|
||||
})
|
||||
|
||||
const handlePlayAudio = () => {
|
||||
isPlaying.value = !isPlaying.value
|
||||
console.log('播放/暂停音频')
|
||||
const handlePlayAudio = async () => {
|
||||
const selection = exhibit.value.id
|
||||
? await explainUseCase.selectAudioForExhibit(exhibit.value.id)
|
||||
: null
|
||||
|
||||
uni.showToast({
|
||||
title: selection?.unavailableMessage || exhibit.value.audioUnavailableReason || '该展项暂无讲解音频',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNavigate = () => {
|
||||
console.log('导航到展品位置')
|
||||
uni.navigateBack()
|
||||
if (!exhibit.value.poiId) {
|
||||
uni.showToast({
|
||||
title: '该展项暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.value.poiId)}&target=${encodeURIComponent(exhibit.value.name)}&state=preview`
|
||||
})
|
||||
}
|
||||
|
||||
const handleCollect = () => {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
floor-top="208px"
|
||||
tools-top="450px"
|
||||
:tools="['回正', '2D']"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@floor-change="handleFloorChange"
|
||||
@@ -115,16 +118,19 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import {
|
||||
NAV_FLOOR_OPTIONS,
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
formatNavFloorLabel,
|
||||
isPoiAccessible,
|
||||
loadCleanNavPoiById
|
||||
} from '@/services/navAssets'
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
toFacilityDetailViewModel,
|
||||
type FacilityDetailViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import type {
|
||||
MuseumFloor
|
||||
} from '@/domain/museum'
|
||||
|
||||
interface ConfirmedStart {
|
||||
label: string
|
||||
@@ -132,19 +138,23 @@ interface ConfirmedStart {
|
||||
source: 'facility-detail'
|
||||
}
|
||||
|
||||
const facility = ref({
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
|
||||
const facility = ref<FacilityDetailViewModel>({
|
||||
id: '',
|
||||
name: '目标位置',
|
||||
status: '可预览',
|
||||
location: 'clean 导览数据点位',
|
||||
traffic: NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
traffic: guideUseCase.getRouteReadiness().message,
|
||||
tags: ['三维位置', 'clean 数据']
|
||||
})
|
||||
const isStartPanelOpen = ref(false)
|
||||
const selectedStartFloor = ref('1F')
|
||||
const selectedStartArea = ref('服务台附近')
|
||||
const confirmedStart = ref<ConfirmedStart | null>(null)
|
||||
const startFloors = NAV_FLOOR_OPTIONS.map((floor) => floor.label)
|
||||
const startFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||||
const startAreas = ['主入口', '服务台附近', '停车场入口']
|
||||
|
||||
const pendingStartLabel = computed(() => `${selectedStartFloor.value} ${selectedStartArea.value}`)
|
||||
@@ -165,7 +175,23 @@ const syncDraftStartSelection = () => {
|
||||
selectedStartArea.value = resolveConfirmedStartArea()
|
||||
}
|
||||
|
||||
const loadGuideFloors = async () => {
|
||||
try {
|
||||
const floors = await guideUseCase.getFloors()
|
||||
guideFloors.value = floors
|
||||
selectedStartFloor.value = floors.find((floor) => floor.label === selectedStartFloor.value)?.label
|
||||
|| floors.find((floor) => floor.id === 'L1')?.label
|
||||
|| floors[0]?.label
|
||||
|| selectedStartFloor.value
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
void loadGuideFloors()
|
||||
|
||||
if (options.id) {
|
||||
facility.value.id = options.id
|
||||
}
|
||||
@@ -177,22 +203,13 @@ onLoad(async (options: any) => {
|
||||
if (!facility.value.id) return
|
||||
|
||||
try {
|
||||
const poi = await loadCleanNavPoiById(facility.value.id)
|
||||
const poi = await guideUseCase.getPoiById(facility.value.id)
|
||||
if (!poi) return
|
||||
|
||||
const floor = formatNavFloorLabel(poi.floorId)
|
||||
facility.value = {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
status: '可预览',
|
||||
location: `${floor} · ${poi.primaryCategoryZh}`,
|
||||
traffic: NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
tags: [
|
||||
floor,
|
||||
poi.primaryCategoryZh,
|
||||
isPoiAccessible(poi) ? '无障碍相关' : '展示点位'
|
||||
]
|
||||
}
|
||||
facility.value = toFacilityDetailViewModel(
|
||||
poi,
|
||||
guideUseCase.getRouteReadiness().message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('加载 clean 设施详情失败:', error)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<view class="hall-hero">
|
||||
<image class="hero-image" :src="hall.image" mode="aspectFill" />
|
||||
<view class="hall-badge">
|
||||
<text class="badge-text">{{ hall.floor }}</text>
|
||||
<text class="badge-text">{{ hall.floorLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn primary" @tap="handleNavigate">
|
||||
<text class="btn-text">导航到展厅</text>
|
||||
<text class="btn-text">查看展厅位置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -60,6 +60,15 @@ import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import ExhibitCard from '@/components/content/ExhibitCard.vue'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toExplainExhibitViewModel,
|
||||
toHallDetailViewModel,
|
||||
type ExplainExhibitViewModel,
|
||||
type HallDetailViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import {
|
||||
isGuideTopTab,
|
||||
navigateToGuideTopTab,
|
||||
@@ -68,24 +77,26 @@ import {
|
||||
|
||||
const activeTopTab = ref<GuideTopTab>('guide')
|
||||
|
||||
const hall = ref({
|
||||
id: '1',
|
||||
name: '1号展厅',
|
||||
floor: '1F',
|
||||
description: '文艺复兴时期艺术作品展厅,展示了欧洲文艺复兴时期最具代表性的绘画和雕塑作品。',
|
||||
const hall = ref<HallDetailViewModel>({
|
||||
id: '',
|
||||
name: '展厅内容',
|
||||
floorLabel: '楼层待补充',
|
||||
description: '该展厅介绍待正式内容库补充。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 25,
|
||||
area: '500㎡'
|
||||
exhibitCount: 0,
|
||||
area: '待补充'
|
||||
})
|
||||
|
||||
const exhibits = ref([
|
||||
{ id: '1', name: '蒙娜丽莎', artist: '达芬奇', hasAudio: true },
|
||||
{ id: '2', name: '最后的晚餐', artist: '达芬奇', hasAudio: true }
|
||||
])
|
||||
const exhibits = ref<ExplainExhibitViewModel[]>([])
|
||||
|
||||
onLoad((options: any) => {
|
||||
onLoad(async (options: any) => {
|
||||
if (options.id) {
|
||||
console.log('加载展厅:', options.id)
|
||||
const hallData = await explainUseCase.getHallById(options.id)
|
||||
if (hallData) {
|
||||
hall.value = toHallDetailViewModel(hallData)
|
||||
const hallExhibits = await explainUseCase.listExhibitsByHallId(hallData.id)
|
||||
exhibits.value = hallExhibits.map(toExplainExhibitViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
||||
@@ -101,8 +112,17 @@ const handleExhibitClick = (exhibit: any) => {
|
||||
}
|
||||
|
||||
const handleNavigate = () => {
|
||||
console.log('导航到展厅')
|
||||
uni.navigateBack()
|
||||
if (!hall.value.poiId) {
|
||||
uni.showToast({
|
||||
title: '该展厅暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?facilityId=${encodeURIComponent(hall.value.poiId)}&target=${encodeURIComponent(hall.value.name)}&state=preview`
|
||||
})
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
:map-type="guideMapType"
|
||||
:outdoor-variant="guideOutdoorVariant"
|
||||
:indoor-asset-base-url="indoorNavAssetBaseUrl"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
indoor-initial-view="overview"
|
||||
:indoor-view="indoorView"
|
||||
:active-floor="activeGuideFloor"
|
||||
@@ -80,6 +83,7 @@
|
||||
|
||||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||||
<ExplainList
|
||||
:exhibits="explainExhibits"
|
||||
@exhibit-click="handleExplainExhibitClick"
|
||||
@featured-click="handleExplainFeaturedClick"
|
||||
@audio-click="handleAudioClick"
|
||||
@@ -109,7 +113,7 @@ import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import ExplainList from '@/components/explain/ExplainList.vue'
|
||||
import ExplainList, { type Exhibit } from '@/components/explain/ExplainList.vue'
|
||||
import FloatingAudioButton from '@/components/audio/FloatingAudioButton.vue'
|
||||
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||
import {
|
||||
@@ -117,9 +121,20 @@ import {
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import {
|
||||
NAV_ASSET_BASE_URL,
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||
} from '@/services/navAssets'
|
||||
} from '@/domain/guideReadiness'
|
||||
import {
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
toExplainExhibitViewModel
|
||||
} from '@/view-models/explainViewModels'
|
||||
import type {
|
||||
MuseumFloor
|
||||
} from '@/domain/museum'
|
||||
|
||||
// 3D 模式状态
|
||||
const is3DMode = ref(false)
|
||||
@@ -135,7 +150,10 @@ const showMarkerDetail = ref(false)
|
||||
const showAreaSelector = ref(false)
|
||||
const selectedAreaId = ref('service')
|
||||
|
||||
const indoorNavAssetBaseUrl = NAV_ASSET_BASE_URL
|
||||
const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
|
||||
const shortcutItems = [
|
||||
{ id: 'toilet', name: '卫生间', abbr: '卫' },
|
||||
@@ -149,6 +167,7 @@ const shortcutItems = [
|
||||
const showAudioPlayer = ref(false)
|
||||
const isAudioPlaying = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
const explainExhibits = ref<Exhibit[]>([])
|
||||
|
||||
// 选中的标记详情
|
||||
interface MarkerDetail {
|
||||
@@ -188,8 +207,35 @@ onLoad((options: any = {}) => {
|
||||
if (isGuideTopTab(tab)) {
|
||||
currentTab.value = tab
|
||||
}
|
||||
|
||||
void loadGuideFloors()
|
||||
void loadExplainExhibits()
|
||||
})
|
||||
|
||||
const loadGuideFloors = async () => {
|
||||
try {
|
||||
const floors = await guideUseCase.getFloors()
|
||||
guideFloors.value = floors
|
||||
activeGuideFloor.value = floors.find((floor) => floor.label === activeGuideFloor.value)?.label
|
||||
|| floors.find((floor) => floor.id === 'L1')?.label
|
||||
|| floors[0]?.label
|
||||
|| activeGuideFloor.value
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const loadExplainExhibits = async () => {
|
||||
try {
|
||||
const exhibits = await explainUseCase.listExhibits()
|
||||
explainExhibits.value = exhibits.map(toExplainExhibitViewModel)
|
||||
} catch (error) {
|
||||
console.error('加载讲解内容失败:', error)
|
||||
explainExhibits.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearchFocus = () => {
|
||||
searchBarFocused.value = true
|
||||
@@ -391,13 +437,23 @@ const guideSearchText = computed(() => {
|
||||
})
|
||||
|
||||
// 音频相关处理
|
||||
const handleAudioClick = (exhibit: any) => {
|
||||
console.log('点击音频播放:', exhibit)
|
||||
const handleAudioClick = async (exhibit: Exhibit) => {
|
||||
const selection = await explainUseCase.selectAudioForExhibit(exhibit.id)
|
||||
|
||||
if (!selection?.playable || !selection.media?.url) {
|
||||
uni.showToast({
|
||||
title: selection?.unavailableMessage || exhibit.audioUnavailableReason || '该展项暂无讲解音频',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
currentAudio.value = {
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
audioUrl: exhibit.audioUrl || 'https://example.com/audio.mp3',
|
||||
image: exhibit.image
|
||||
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
|
||||
}
|
||||
@@ -431,30 +487,14 @@ const handleAudioEnded = (audio: AudioItem) => {
|
||||
}
|
||||
|
||||
// 讲解页面处理
|
||||
const handleExplainExhibitClick = (exhibit: any) => {
|
||||
console.log('点击讲解展品:', exhibit)
|
||||
const handleExplainExhibitClick = (exhibit: Exhibit) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${exhibit.id}&tab=explain`
|
||||
})
|
||||
}
|
||||
|
||||
const handleExplainFeaturedClick = (exhibit: any) => {
|
||||
console.log('点击推荐讲解:', exhibit)
|
||||
// 播放音频讲解
|
||||
if (exhibit.hasAudio) {
|
||||
currentAudio.value = {
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
audioUrl: exhibit.audioUrl || 'https://example.com/audio.mp3',
|
||||
image: exhibit.image
|
||||
}
|
||||
showAudioPlayer.value = true
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '该展品暂无讲解音频',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
const handleExplainFeaturedClick = async (exhibit: Exhibit) => {
|
||||
await handleAudioClick(exhibit)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
>
|
||||
<GuideMapShell
|
||||
v-bind="shellConfig"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
:target-focus-request="targetFocusRequest"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@@ -125,12 +128,18 @@ import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import LocationPreview from '@/components/navigation/LocationPreview.vue'
|
||||
import {
|
||||
NAV_FLOOR_OPTIONS,
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
formatNavFloorLabel,
|
||||
loadCleanNavPoiById,
|
||||
type CleanNavPoi
|
||||
} from '@/services/navAssets'
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
initialLocationPreviewPlan,
|
||||
toTargetFocusRequestViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import type {
|
||||
GuideLocationPreview,
|
||||
GuideLocationPreviewPlan,
|
||||
GuideStartLocation,
|
||||
MuseumFloor
|
||||
} from '@/domain/museum'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
@@ -138,28 +147,11 @@ import {
|
||||
|
||||
type RouteViewState = 'preview' | 'location-error' | 'outdoor-preview'
|
||||
type GuideMode = '2d' | '3d'
|
||||
interface RouteStep {
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
|
||||
interface RouteTargetLocation {
|
||||
poiId: string
|
||||
name: string
|
||||
floorId: string
|
||||
floorLabel?: string
|
||||
primaryCategoryZh?: string
|
||||
positionGltf?: [number, number, number]
|
||||
}
|
||||
|
||||
interface RouteStartLocation {
|
||||
label: string
|
||||
floor?: string
|
||||
source?: string
|
||||
}
|
||||
type RouteTargetLocation = GuideLocationPreview
|
||||
type RouteStartLocation = GuideStartLocation
|
||||
|
||||
interface TargetPoiFocusRequest extends RouteTargetLocation {
|
||||
requestId: number
|
||||
requestId: number | string
|
||||
}
|
||||
|
||||
interface TargetPoiFocusResult {
|
||||
@@ -170,104 +162,44 @@ interface TargetPoiFocusResult {
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface RoutePlan {
|
||||
id: string
|
||||
facilityId: string
|
||||
target: string
|
||||
summary: string
|
||||
steps: RouteStep[]
|
||||
targetLocation: RouteTargetLocation | null
|
||||
startLocation: RouteStartLocation | null
|
||||
}
|
||||
type RoutePlan = GuideLocationPreviewPlan
|
||||
|
||||
const routeDeepLinkStates: RouteViewState[] = ['preview', 'outdoor-preview']
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
|
||||
const startSourceLabels: Record<string, string> = {
|
||||
'facility-detail': '设施详情选择',
|
||||
manual: '手动选择'
|
||||
}
|
||||
|
||||
const formatStartSourceLabel = (source?: string) => (
|
||||
source ? startSourceLabels[source] || source : ''
|
||||
)
|
||||
|
||||
const formatStartStepText = (startLocation: RouteStartLocation) => {
|
||||
const sourceLabel = formatStartSourceLabel(startLocation.source)
|
||||
const floorSuffix = startLocation.floor && !startLocation.label.includes(startLocation.floor)
|
||||
? ` · ${startLocation.floor}`
|
||||
: ''
|
||||
const sourceSuffix = sourceLabel ? `(${sourceLabel})` : ''
|
||||
|
||||
return `起点:${startLocation.label}${floorSuffix}${sourceSuffix}`
|
||||
}
|
||||
|
||||
const createPreviewSteps = (
|
||||
poi: CleanNavPoi | null,
|
||||
startLocation: RouteStartLocation | null
|
||||
): RouteStep[] => {
|
||||
if (startLocation) {
|
||||
return [
|
||||
{
|
||||
id: 'start',
|
||||
text: formatStartStepText(startLocation)
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
text: poi
|
||||
? `目标:${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh} · 位置预览`
|
||||
: '目标来自 clean 导览数据,暂仅支持位置预览'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'floor',
|
||||
text: poi ? `目标位于 ${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh}` : '目标来自 clean 导览数据'
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
text: NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const createRoutePlan = (
|
||||
facilityId: string,
|
||||
target: string,
|
||||
poi: CleanNavPoi | null = null,
|
||||
startLocation: RouteStartLocation | null = null
|
||||
): RoutePlan => ({
|
||||
id: `route-${facilityId || 'custom'}`,
|
||||
facilityId,
|
||||
target,
|
||||
summary: poi
|
||||
? `${formatNavFloorLabel(poi.floorId)} · ${poi.primaryCategoryZh} · 位置预览`
|
||||
: '位置预览 · 尚未接入正式路线图',
|
||||
steps: createPreviewSteps(poi, startLocation),
|
||||
targetLocation: poi ? {
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: formatNavFloorLabel(poi.floorId),
|
||||
primaryCategoryZh: poi.primaryCategoryZh,
|
||||
positionGltf: poi.positionGltf
|
||||
} : null,
|
||||
startLocation
|
||||
})
|
||||
|
||||
const route = ref<RoutePlan>(createRoutePlan('', '目标地点'))
|
||||
const route = ref<RoutePlan>(initialLocationPreviewPlan())
|
||||
const hasRouteTarget = ref(false)
|
||||
const routeViewState = ref<RouteViewState>('preview')
|
||||
const activeFloor = ref('1F')
|
||||
const targetFocusRequest = ref<TargetPoiFocusRequest | null>(null)
|
||||
const targetFocusRequestId = ref(0)
|
||||
const manualFloors = NAV_FLOOR_OPTIONS.map((floor) => floor.label)
|
||||
const manualFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||||
const guideFloorLabel = (floorId: string) => (
|
||||
guideFloors.value.find((floor) => floor.id === floorId)?.label || floorId
|
||||
)
|
||||
const defaultGuideFloorLabel = () => (
|
||||
guideFloors.value.find((floor) => floor.id === 'L1')?.label
|
||||
|| guideFloors.value[0]?.label
|
||||
|| '1F'
|
||||
)
|
||||
const loadGuideFloors = async () => {
|
||||
if (guideFloors.value.length) return guideFloors.value
|
||||
|
||||
try {
|
||||
guideFloors.value = await guideUseCase.getFloors()
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
}
|
||||
|
||||
return guideFloors.value
|
||||
}
|
||||
const manualAreas = ['主入口', '服务台附近', '停车场入口']
|
||||
const isManualLocationPanelOpen = ref(false)
|
||||
const selectedManualFloor = ref('1F')
|
||||
const selectedManualArea = ref('服务台附近')
|
||||
|
||||
const normalizeRouteOption = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0]
|
||||
@@ -380,7 +312,36 @@ const createStartLocationFromOptions = (options: Record<string, unknown>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestTargetFocus = (target: Partial<RouteTargetLocation> | null, fallbackName = route.value.target) => {
|
||||
if (!target?.poiId || !target.floorId) {
|
||||
targetFocusRequest.value = null
|
||||
return false
|
||||
}
|
||||
|
||||
const floorLabel = target.floorLabel || guideFloorLabel(target.floorId)
|
||||
const focusTarget: RouteTargetLocation = {
|
||||
poiId: target.poiId,
|
||||
name: target.name || fallbackName,
|
||||
floorId: target.floorId,
|
||||
floorLabel,
|
||||
primaryCategoryZh: target.primaryCategoryZh,
|
||||
positionGltf: target.positionGltf
|
||||
}
|
||||
|
||||
activeFloor.value = floorLabel
|
||||
routeViewState.value = 'preview'
|
||||
targetFocusRequestId.value += 1
|
||||
targetFocusRequest.value = toTargetFocusRequestViewModel(
|
||||
focusTarget,
|
||||
targetFocusRequestId.value
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
await loadGuideFloors()
|
||||
|
||||
const facilityId = normalizeStringOption(options.facilityId)
|
||||
const target = normalizeStringOption(options.target)
|
||||
const state = normalizeRouteOption(options.state)
|
||||
@@ -388,39 +349,42 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
|
||||
if (!facilityId && !target) {
|
||||
hasRouteTarget.value = false
|
||||
route.value = createRoutePlan('', '目标地点')
|
||||
route.value = initialLocationPreviewPlan()
|
||||
routeViewState.value = 'preview'
|
||||
activeFloor.value = '1F'
|
||||
activeFloor.value = defaultGuideFloorLabel()
|
||||
targetFocusRequest.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
const matchedPoi = facilityId ? await loadCleanNavPoiById(facilityId) : null
|
||||
const matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null
|
||||
|
||||
if (!matchedPoi) {
|
||||
hasRouteTarget.value = false
|
||||
route.value = createRoutePlan('', target || '目标地点')
|
||||
route.value = await guideUseCase.createLocationPreviewPlan('', target || '目标地点', startLocation)
|
||||
routeViewState.value = 'preview'
|
||||
activeFloor.value = startLocation?.floor || '1F'
|
||||
activeFloor.value = startLocation?.floor || defaultGuideFloorLabel()
|
||||
targetFocusRequest.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
route.value = createRoutePlan(
|
||||
route.value = await guideUseCase.createLocationPreviewPlan(
|
||||
facilityId,
|
||||
target || matchedPoi.name,
|
||||
matchedPoi,
|
||||
startLocation
|
||||
)
|
||||
|
||||
hasRouteTarget.value = true
|
||||
routeViewState.value = isRouteViewState(state) ? state : 'preview'
|
||||
activeFloor.value = matchedPoi ? formatNavFloorLabel(matchedPoi.floorId) : startLocation?.floor || '1F'
|
||||
targetFocusRequest.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
|
||||
if (routeViewState.value === 'preview') {
|
||||
requestTargetFocus(route.value.targetLocation, matchedPoi.name)
|
||||
} else {
|
||||
activeFloor.value = matchedPoi.floorLabel || guideFloorLabel(matchedPoi.floorId)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -479,23 +443,14 @@ const handleViewOutdoorMap = () => {
|
||||
routeViewState.value = 'outdoor-preview'
|
||||
}
|
||||
|
||||
const handleShowTargetLocation = (target: RouteTargetLocation | null) => {
|
||||
const targetLocation = target || route.value.targetLocation
|
||||
const handleShowTargetLocation = (target: Partial<RouteTargetLocation> | null) => {
|
||||
const focused = requestTargetFocus(target || route.value.targetLocation)
|
||||
|
||||
if (!targetLocation?.poiId || !targetLocation.floorId) {
|
||||
if (!focused) {
|
||||
uni.showToast({
|
||||
title: '目标暂无三维位置数据',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
routeViewState.value = 'preview'
|
||||
activeFloor.value = targetLocation.floorLabel || formatNavFloorLabel(targetLocation.floorId)
|
||||
targetFocusRequestId.value += 1
|
||||
targetFocusRequest.value = {
|
||||
...targetLocation,
|
||||
requestId: targetFocusRequestId.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,6 +466,7 @@ const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||||
const handleReturnToPreview = () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
}
|
||||
|
||||
const handleRelocate = () => {
|
||||
@@ -540,39 +496,24 @@ const createManualStartLocation = (): RouteStartLocation => ({
|
||||
source: 'manual'
|
||||
})
|
||||
|
||||
const createCurrentTargetStep = (): RouteStep => {
|
||||
const targetLocation = route.value.targetLocation
|
||||
|
||||
return {
|
||||
id: 'target',
|
||||
text: targetLocation
|
||||
? `目标:${targetLocation.floorLabel || formatNavFloorLabel(targetLocation.floorId)} · ${targetLocation.primaryCategoryZh || route.value.target} · 位置预览`
|
||||
: '目标来自 clean 导览数据,暂仅支持位置预览'
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmLocation = () => {
|
||||
const handleConfirmLocation = async () => {
|
||||
const startLocation = createManualStartLocation()
|
||||
|
||||
route.value = {
|
||||
...route.value,
|
||||
startLocation,
|
||||
steps: [
|
||||
{
|
||||
id: 'start',
|
||||
text: formatStartStepText(startLocation)
|
||||
},
|
||||
createCurrentTargetStep()
|
||||
]
|
||||
}
|
||||
route.value = await guideUseCase.createLocationPreviewPlan(
|
||||
route.value.facilityId,
|
||||
route.value.target,
|
||||
startLocation
|
||||
)
|
||||
activeFloor.value = startLocation.floor || activeFloor.value
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: GuideMode) => {
|
||||
if (routeViewState.value === 'outdoor-preview' && mode === '3d') {
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -596,7 +537,7 @@ const handleToolClick = (tool: string) => {
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: tool === '回正' ? '已回到三维总览' : NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
title: tool === '回正' ? '已回到三维总览' : guideUseCase.getRouteReadiness().message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
search-top="60px"
|
||||
mode-top="104px"
|
||||
floor-top="208px"
|
||||
:indoor-model-source="indoorModelSource"
|
||||
:floors="guideFloors"
|
||||
:normalize-floor-id="normalizeGuideFloorId"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@floor-change="handleFloorChange"
|
||||
@@ -100,31 +103,31 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import {
|
||||
formatNavFloorLabel,
|
||||
isPoiAccessible,
|
||||
searchCleanNavPois,
|
||||
type CleanNavPoi
|
||||
} from '@/services/navAssets'
|
||||
guideUseCase
|
||||
} from '@/usecases/guideUseCase'
|
||||
import {
|
||||
toFacilityResultViewModel,
|
||||
type FacilityResultViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import {
|
||||
navigateToGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import type {
|
||||
MuseumFloor
|
||||
} from '@/domain/museum'
|
||||
|
||||
interface FilterItem {
|
||||
id: 'all' | 'accessible' | 'floor'
|
||||
label: string
|
||||
}
|
||||
|
||||
interface FacilityResult {
|
||||
id: string
|
||||
name: string
|
||||
floor: string
|
||||
meta: string
|
||||
accessible: boolean
|
||||
action: '查看'
|
||||
}
|
||||
interface FacilityResult extends FacilityResultViewModel {}
|
||||
|
||||
const searchKeyword = ref('卫生间')
|
||||
const indoorModelSource = guideUseCase.getModelSource()
|
||||
const guideFloors = ref<MuseumFloor[]>([])
|
||||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||||
const searchDraftKeyword = ref('')
|
||||
const currentFilter = ref<FilterItem['id']>('all')
|
||||
const isLoading = ref(false)
|
||||
@@ -140,18 +143,6 @@ const filters: FilterItem[] = [
|
||||
|
||||
const facilities = ref<FacilityResult[]>([])
|
||||
|
||||
const toFacilityResult = (poi: CleanNavPoi): FacilityResult => {
|
||||
const floor = formatNavFloorLabel(poi.floorId)
|
||||
return {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
floor,
|
||||
meta: `${floor}|${poi.primaryCategoryZh}|${poi.navigationReadiness || '展示点位'}`,
|
||||
accessible: isPoiAccessible(poi),
|
||||
action: '查看'
|
||||
}
|
||||
}
|
||||
|
||||
const visibleFacilities = computed(() => {
|
||||
if (currentFilter.value === 'accessible') {
|
||||
return facilities.value.filter((item) => item.accessible)
|
||||
@@ -170,10 +161,10 @@ const resultTitle = computed(() => {
|
||||
const loadFacilityResults = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const pois = await searchCleanNavPois(searchKeyword.value)
|
||||
const pois = await guideUseCase.searchPois(searchKeyword.value)
|
||||
facilities.value = pois
|
||||
.filter((poi) => poi.primaryCategory !== 'touring_poi')
|
||||
.map(toFacilityResult)
|
||||
.filter((poi) => poi.primaryCategory.id !== 'touring_poi')
|
||||
.map(toFacilityResultViewModel)
|
||||
} catch (error) {
|
||||
console.error('加载 clean 导览搜索结果失败:', error)
|
||||
facilities.value = []
|
||||
@@ -186,11 +177,21 @@ const loadFacilityResults = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadGuideFloors = async () => {
|
||||
try {
|
||||
guideFloors.value = await guideUseCase.getFloors()
|
||||
} catch (error) {
|
||||
console.error('加载导览楼层失败:', error)
|
||||
guideFloors.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.keyword) {
|
||||
searchKeyword.value = decodeURIComponent(options.keyword)
|
||||
}
|
||||
searchDraftKeyword.value = searchKeyword.value
|
||||
void loadGuideFloors()
|
||||
void loadFacilityResults()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user