@@ -28,39 +28,46 @@
|
||||
show-zoom-controls
|
||||
zoom-controls-top="calc(100vh - 252px)"
|
||||
@floor-change="handleFloorChange"
|
||||
@target-focus="handleTargetFocus"
|
||||
>
|
||||
<view v-if="isDetailPanelVisible" class="detail-sheet">
|
||||
<view class="detail-sheet" data-testid="facility-detail-sheet">
|
||||
<view class="detail-title-row">
|
||||
<view class="target-dot"></view>
|
||||
<view class="detail-title-copy">
|
||||
<text class="detail-title">{{ facility.name }}</text>
|
||||
<text class="detail-subtitle">{{ facility.location }}</text>
|
||||
</view>
|
||||
<view class="detail-close" @tap.stop="handleCloseDetailPanel">
|
||||
<view
|
||||
class="detail-close"
|
||||
data-testid="facility-detail-close"
|
||||
aria-label="关闭点位详情"
|
||||
@tap.stop="handleCloseDetailPanel"
|
||||
>
|
||||
<text class="detail-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tag-row">
|
||||
<view
|
||||
v-for="(tag, index) in facility.tags"
|
||||
:key="tag"
|
||||
class="detail-tag"
|
||||
:class="{ active: index === 0 }"
|
||||
>
|
||||
<text class="detail-tag-text">{{ tag }}</text>
|
||||
<view class="detail-meta" data-testid="facility-detail-meta">
|
||||
<view class="detail-meta-item">
|
||||
<text class="detail-meta-label">类别</text>
|
||||
<text class="detail-meta-value">{{ facility.category }}</text>
|
||||
</view>
|
||||
<view class="detail-meta-item">
|
||||
<text class="detail-meta-label">楼层</text>
|
||||
<text class="detail-meta-value">{{ facility.floor }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="detail-hint">已在对应楼层模型中标出点位</text>
|
||||
</view>
|
||||
<view
|
||||
v-else
|
||||
class="detail-restore"
|
||||
@tap.stop="handleOpenDetailPanel"
|
||||
>
|
||||
<view class="target-dot small"></view>
|
||||
<text class="detail-restore-text">点位</text>
|
||||
<text v-if="facility.description" class="detail-description">
|
||||
{{ facility.description }}
|
||||
</text>
|
||||
|
||||
<view
|
||||
v-if="locationErrorMessage"
|
||||
class="detail-location-error"
|
||||
data-testid="facility-location-error"
|
||||
>
|
||||
<text class="detail-location-error-text">{{ locationErrorMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</GuideMapShell>
|
||||
</GuidePageFrame>
|
||||
@@ -78,8 +85,10 @@ import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
parseFacilityDetailSearchContext,
|
||||
toFacilityDetailViewModel,
|
||||
toTargetFocusRequestViewModel,
|
||||
type FacilityDetailSearchContext,
|
||||
type TargetPoiFocusRequestViewModel,
|
||||
type FacilityDetailViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
@@ -106,17 +115,29 @@ const activeFloor = ref('1F')
|
||||
const requestedTargetFloorId = ref('')
|
||||
const targetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||||
const targetFocusRequestId = ref(0)
|
||||
const isDetailPanelVisible = ref(true)
|
||||
const locationErrorMessage = ref('')
|
||||
const resolutionLoadFailed = ref(false)
|
||||
|
||||
interface TargetPoiFocusResult {
|
||||
requestId: number | string
|
||||
poiId: string
|
||||
floorId: string
|
||||
status: 'focused' | 'missing' | 'error'
|
||||
message?: string
|
||||
}
|
||||
|
||||
const facility = ref<FacilityDetailViewModel>({
|
||||
id: '',
|
||||
name: '目标位置',
|
||||
status: '可预览',
|
||||
location: '正在定位三维点位',
|
||||
traffic: guideUseCase.getRouteReadinessSnapshot().message,
|
||||
tags: ['三维位置', '位置预览']
|
||||
category: '正在加载',
|
||||
floor: '正在加载',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const searchContext = ref<FacilityDetailSearchContext>(
|
||||
parseFacilityDetailSearchContext()
|
||||
)
|
||||
|
||||
const loadGuideFloors = async () => {
|
||||
try {
|
||||
guideFloors.value = await guideUseCase.getFloors()
|
||||
@@ -136,8 +157,12 @@ const loadGuideFloors = async () => {
|
||||
}
|
||||
|
||||
const focusFacilityPoi = (poi: MuseumPoi) => {
|
||||
if (!poi.floorId) return
|
||||
if (!poi.floorId) {
|
||||
locationErrorMessage.value = '该点位缺少楼层信息,暂时无法在地图中定位。'
|
||||
return
|
||||
}
|
||||
|
||||
locationErrorMessage.value = ''
|
||||
activeMode.value = '3d'
|
||||
activeFloor.value = poi.floorId
|
||||
targetFocusRequestId.value += 1
|
||||
@@ -153,8 +178,12 @@ const focusFacilityPoi = (poi: MuseumPoi) => {
|
||||
}
|
||||
|
||||
const focusRenderPoi = (poi: GuideRenderPoi) => {
|
||||
if (!poi.floorId) return
|
||||
if (!poi.floorId) {
|
||||
locationErrorMessage.value = '该点位缺少楼层信息,暂时无法在地图中定位。'
|
||||
return
|
||||
}
|
||||
|
||||
locationErrorMessage.value = ''
|
||||
const floorLabel = guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
|
||||
activeMode.value = '3d'
|
||||
activeFloor.value = poi.floorId
|
||||
@@ -177,20 +206,15 @@ const focusRenderPoi = (poi: GuideRenderPoi) => {
|
||||
const getRenderPoiFloorLabel = (poi: GuideRenderPoi) =>
|
||||
guideFloors.value.find((floor) => floor.id === poi.floorId)?.label || poi.floorId
|
||||
|
||||
const applyRenderPoiToFacility = (poi: GuideRenderPoi, routeMessage: string) => {
|
||||
const applyRenderPoiToFacility = (poi: GuideRenderPoi) => {
|
||||
const floorLabel = getRenderPoiFloorLabel(poi)
|
||||
const categoryLabel = poi.primaryCategoryZh || '三维位置'
|
||||
const categoryLabel = poi.primaryCategoryZh || '未分类'
|
||||
facility.value = {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
status: '可预览',
|
||||
location: `${floorLabel} · ${categoryLabel}`,
|
||||
traffic: routeMessage,
|
||||
tags: [
|
||||
floorLabel,
|
||||
categoryLabel,
|
||||
'展示点位'
|
||||
]
|
||||
category: categoryLabel,
|
||||
floor: floorLabel || '楼层信息缺失',
|
||||
description: ''
|
||||
}
|
||||
focusRenderPoi(poi)
|
||||
}
|
||||
@@ -201,7 +225,15 @@ const normalizePoiName = (value: string) => value
|
||||
.toLowerCase()
|
||||
|
||||
const decodeRouteText = (value: unknown) => (
|
||||
typeof value === 'string' ? decodeURIComponent(value) : ''
|
||||
typeof value === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
})()
|
||||
: ''
|
||||
)
|
||||
|
||||
const isPoiOnRequestedFloor = (poi: Pick<MuseumPoi | GuideRenderPoi, 'floorId'>) => (
|
||||
@@ -222,6 +254,9 @@ const resolveFacilityPoi = async () => {
|
||||
: null
|
||||
if (poiById) return poiById
|
||||
|
||||
// 搜索结果必须按稳定 ID 解析,避免同名点位被定位到错误楼层。
|
||||
if (searchContext.value.source === 'search') return null
|
||||
|
||||
const poiByHall = await resolvePoiFromHall()
|
||||
if (poiByHall) return poiByHall
|
||||
|
||||
@@ -244,9 +279,10 @@ const resolveFacilityPoi = async () => {
|
||||
}) || candidatesToMatch[0] || null
|
||||
}
|
||||
|
||||
const resolveRenderPoiByName = async () => {
|
||||
const resolveRenderPoi = async () => {
|
||||
const resolveByExactId = searchContext.value.source === 'search' && Boolean(facility.value.id)
|
||||
const normalizedTarget = normalizePoiName(facility.value.name)
|
||||
if (!normalizedTarget) return null
|
||||
if (!resolveByExactId && !normalizedTarget) return null
|
||||
|
||||
const modelPackage = await indoorModelSource.loadPackage()
|
||||
const floorsToSearch = requestedTargetFloorId.value
|
||||
@@ -255,8 +291,17 @@ const resolveRenderPoiByName = async () => {
|
||||
|
||||
for (const floor of floorsToSearch) {
|
||||
const floorId = floor.floorId
|
||||
const pois = await indoorModelSource.loadFloorPois(floorId).catch(() => [])
|
||||
let pois: GuideRenderPoi[] = []
|
||||
try {
|
||||
pois = await indoorModelSource.loadFloorPois(floorId)
|
||||
} catch (error) {
|
||||
resolutionLoadFailed.value = true
|
||||
console.warn(`楼层 ${floorId} 的模型点位加载失败:`, error)
|
||||
continue
|
||||
}
|
||||
const matchedPoi = pois.find((poi) => {
|
||||
if (resolveByExactId) return poi.id === facility.value.id
|
||||
|
||||
const poiName = normalizePoiName(poi.name)
|
||||
const hallName = normalizePoiName(poi.hallName || '')
|
||||
const sourceObjectName = normalizePoiName(poi.sourceObjectName || '')
|
||||
@@ -276,8 +321,9 @@ const resolveRenderPoiByName = async () => {
|
||||
|
||||
const tryResolveRenderPoiByName = async () => {
|
||||
try {
|
||||
return await resolveRenderPoiByName()
|
||||
return await resolveRenderPoi()
|
||||
} catch (error) {
|
||||
resolutionLoadFailed.value = true
|
||||
console.warn('当前模型源点位解析失败,尝试静态资源兜底:', error)
|
||||
return null
|
||||
}
|
||||
@@ -287,12 +333,15 @@ const tryResolveFacilityPoi = async () => {
|
||||
try {
|
||||
return await resolveFacilityPoi()
|
||||
} catch (error) {
|
||||
resolutionLoadFailed.value = true
|
||||
console.warn('内容点位解析失败,继续使用模型点位预览:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
onLoad(async (options: Record<string, unknown> = {}) => {
|
||||
searchContext.value = parseFacilityDetailSearchContext(options)
|
||||
resolutionLoadFailed.value = false
|
||||
await loadGuideFloors()
|
||||
|
||||
if (options.id) {
|
||||
@@ -303,40 +352,50 @@ onLoad(async (options: any) => {
|
||||
facility.value.name = decodeRouteText(options.target)
|
||||
}
|
||||
|
||||
if (options.floorId) {
|
||||
requestedTargetFloorId.value = normalizeGuideFloorId(decodeRouteText(options.floorId))
|
||||
const requestedFloorId = decodeRouteText(options.floorId)
|
||||
|| searchContext.value.searchFloorId
|
||||
if (requestedFloorId) {
|
||||
requestedTargetFloorId.value = normalizeGuideFloorId(requestedFloorId)
|
||||
if (requestedTargetFloorId.value) {
|
||||
activeFloor.value = requestedTargetFloorId.value
|
||||
}
|
||||
}
|
||||
|
||||
if (options.floorLabel && requestedTargetFloorId.value) {
|
||||
const floorLabel = decodeRouteText(options.floorLabel)
|
||||
facility.value.location = `${floorLabel} · 位置预览`
|
||||
facility.value.tags = [floorLabel, '三维位置', '位置预览']
|
||||
const requestedFloorLabel = decodeRouteText(options.floorLabel)
|
||||
|| searchContext.value.searchFloorLabel
|
||||
if (requestedFloorLabel) {
|
||||
facility.value.floor = requestedFloorLabel
|
||||
}
|
||||
|
||||
if (!facility.value.id && !normalizePoiName(facility.value.name)) return
|
||||
if (!facility.value.id && !options.target) {
|
||||
locationErrorMessage.value = '未提供可定位的点位信息。'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const routeReadinessMessage = guideUseCase.getRouteReadinessSnapshot().message
|
||||
|
||||
const poi = await tryResolveFacilityPoi()
|
||||
if (poi) {
|
||||
facility.value = toFacilityDetailViewModel(
|
||||
poi,
|
||||
routeReadinessMessage
|
||||
)
|
||||
facility.value = toFacilityDetailViewModel(poi)
|
||||
focusFacilityPoi(poi)
|
||||
return
|
||||
}
|
||||
|
||||
const renderPoi = await tryResolveRenderPoiByName()
|
||||
if (renderPoi) {
|
||||
applyRenderPoiToFacility(renderPoi, routeReadinessMessage)
|
||||
applyRenderPoiToFacility(renderPoi)
|
||||
return
|
||||
}
|
||||
|
||||
facility.value.category = facility.value.category === '正在加载'
|
||||
? '未分类'
|
||||
: facility.value.category
|
||||
facility.value.floor = requestedFloorLabel || '楼层信息缺失'
|
||||
locationErrorMessage.value = resolutionLoadFailed.value
|
||||
? '位置数据加载失败,请稍后重试。'
|
||||
: '该点位缺少可用的位置数据,暂时无法在地图中定位。'
|
||||
} catch (error) {
|
||||
console.error('加载设施位置详情失败:', error)
|
||||
locationErrorMessage.value = '位置数据加载失败,请稍后重试。'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -344,12 +403,19 @@ const handleFloorChange = (floor: string) => {
|
||||
activeFloor.value = floor
|
||||
}
|
||||
|
||||
const handleCloseDetailPanel = () => {
|
||||
isDetailPanelVisible.value = false
|
||||
const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||||
if (result.status === 'focused') {
|
||||
locationErrorMessage.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
locationErrorMessage.value = result.status === 'missing'
|
||||
? '该点位缺少可用的位置数据,暂时无法在地图中定位。'
|
||||
: '地图定位暂不可用,请稍后重试。'
|
||||
}
|
||||
|
||||
const handleOpenDetailPanel = () => {
|
||||
isDetailPanelVisible.value = true
|
||||
const handleCloseDetailPanel = () => {
|
||||
handlePageBack()
|
||||
}
|
||||
|
||||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
@@ -357,6 +423,13 @@ const handleTopTabChange = (tab: GuideTopTab) => {
|
||||
}
|
||||
|
||||
const handlePageBack = () => {
|
||||
if (searchContext.value.source === 'search' && searchContext.value.resultCount === 1) {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
@@ -441,91 +514,62 @@ const handlePageBack = () => {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.detail-subtitle {
|
||||
.detail-meta {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-meta-item {
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
box-sizing: border-box;
|
||||
background: #f7f8f3;
|
||||
border: 1px solid #e5e6de;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-meta-label {
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
line-height: 16px;
|
||||
color: #696962;
|
||||
}
|
||||
|
||||
.detail-meta-value {
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
height: 26px;
|
||||
min-width: 68px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e6de;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-tag.active {
|
||||
background: #151713;
|
||||
border-color: #151713;
|
||||
}
|
||||
|
||||
.detail-tag-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.detail-tag.active .detail-tag-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.detail-hint {
|
||||
.detail-description {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
font-weight: 500;
|
||||
color: #1a7f37;
|
||||
font-size: 13px;
|
||||
line-height: 19px;
|
||||
color: #424754;
|
||||
}
|
||||
|
||||
.detail-restore {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 152px);
|
||||
height: 40px;
|
||||
min-width: 74px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid #dde5df;
|
||||
.detail-location-error {
|
||||
margin-top: 10px;
|
||||
padding: 9px 10px;
|
||||
background: #fff4e8;
|
||||
border: 1px solid #f1c99e;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 18px rgba(110, 127, 115, 0.12);
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.target-dot.small {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: 0;
|
||||
border-width: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.detail-restore-text {
|
||||
.detail-location-error-text {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 700;
|
||||
color: #151713;
|
||||
color: #7a3e05;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
@@ -534,9 +578,8 @@ const handlePageBack = () => {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
min-width: 0;
|
||||
padding: 0 9px;
|
||||
.detail-meta {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
:indoor-view="indoorView"
|
||||
:layer-mode="indoorLayerMode"
|
||||
:active-floor="activeGuideFloor"
|
||||
:visible-poi-ids="searchVisiblePoiIds"
|
||||
:target-focus-request="searchTargetFocusRequest"
|
||||
layer-mode-toggle-top="154px"
|
||||
floor-bottom="193px"
|
||||
floor-max-height="206px"
|
||||
@@ -162,17 +164,20 @@
|
||||
></view>
|
||||
|
||||
<view
|
||||
v-if="showGuideHomeDock"
|
||||
v-show="showGuideHomeDock"
|
||||
class="guide-home-dock"
|
||||
:class="{ expanded: homeSearchExpanded }"
|
||||
@mousedown.capture="handleHomeDockActivate"
|
||||
@click.capture="handleHomeDockActivate"
|
||||
@tap.capture="handleHomeDockActivate"
|
||||
>
|
||||
<PoiSearchPanel
|
||||
ref="homeSearchPanelRef"
|
||||
variant="home"
|
||||
:current-floor-id="activeGuideFloor"
|
||||
:current-floor-label="getGuideFloorLabel(activeGuideFloor)"
|
||||
@expanded-change="handleHomeSearchExpandedChange"
|
||||
@category-mode-change="handleHomeCategoryModeChange"
|
||||
@results-change="handleHomeSearchResultsChange"
|
||||
@result-tap="handleHomeSearchResultTap"
|
||||
@cancel="handleHomeSearchCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -299,8 +304,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
|
||||
@@ -326,12 +331,17 @@ import {
|
||||
import {
|
||||
explainUseCase
|
||||
} from '@/usecases/explainUseCase'
|
||||
import {
|
||||
normalizeExplainDetailTargetFromGuideStop
|
||||
} from '@/domain/explainDetailTarget'
|
||||
import type {
|
||||
GuideRenderPoi
|
||||
} from '@/domain/guideModel'
|
||||
import type {
|
||||
PoiCategoryResultState,
|
||||
PoiSearchContext
|
||||
} from '@/domain/poiSearch'
|
||||
import {
|
||||
toTargetFocusRequestViewModel,
|
||||
type TargetPoiFocusRequestViewModel
|
||||
} from '@/view-models/guideViewModels'
|
||||
import {
|
||||
ARRIVAL_TARGETS_BY_TYPE,
|
||||
type ArrivalSearchTarget,
|
||||
@@ -341,7 +351,8 @@ import type {
|
||||
GuideRouteResult,
|
||||
GuideRouteTarget,
|
||||
MuseumFloor,
|
||||
MuseumHall
|
||||
MuseumHall,
|
||||
MuseumPoi
|
||||
} from '@/domain/museum'
|
||||
|
||||
type GuideIndoorView = 'overview' | 'floor' | 'multi'
|
||||
@@ -424,10 +435,15 @@ const guideMapShellRef = ref<{
|
||||
showOverview?: () => Promise<void> | void
|
||||
} | null>(null)
|
||||
const homeSearchPanelRef = ref<{
|
||||
expandHomePanel?: () => void
|
||||
collapseHomePanel?: () => void
|
||||
resetSearchState?: () => void
|
||||
restoreResultListScroll?: () => Promise<void>
|
||||
} | null>(null)
|
||||
const homeSearchExpanded = ref(false)
|
||||
const homeCategoryModeActive = ref(false)
|
||||
const searchVisiblePoiIds = ref<string[] | null>(null)
|
||||
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||||
let searchTargetFocusRequestId = 0
|
||||
|
||||
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -732,7 +748,7 @@ const selectedGuidePoiIsHall = computed(() => {
|
||||
})
|
||||
|
||||
const selectedGuidePoiCollapsedSubtitle = computed(() => (
|
||||
selectedGuidePoiIsHall.value ? '已定位展厅 · 点按展开操作' : '已定位点位'
|
||||
selectedGuidePoiIsHall.value ? '点按展开查看展厅' : selectedGuidePoiSubtitle.value
|
||||
))
|
||||
|
||||
const routeSimulationStepText = computed(() => {
|
||||
@@ -999,7 +1015,7 @@ const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
||||
isSimulatingRoute.value = false
|
||||
activeGuideFloor.value = poi.floorId
|
||||
renderedFloorId.value = poi.floorId
|
||||
showIndoorHint('已在当前楼层标出点位', 3000)
|
||||
showIndoorHint(`${poi.name} · ${getGuideFloorLabel(poi.floorId)}`, 3000)
|
||||
}
|
||||
|
||||
const handleGuideSelectionClear = () => {
|
||||
@@ -1390,6 +1406,11 @@ const closeHomeSearchDock = () => {
|
||||
homeSearchExpanded.value = false
|
||||
}
|
||||
|
||||
onShow(async () => {
|
||||
await nextTick()
|
||||
await homeSearchPanelRef.value?.restoreResultListScroll?.()
|
||||
})
|
||||
|
||||
const handleMoreOutdoorNav = () => {
|
||||
if (showArrivalPanel.value && showArrivalPanelCollapsed.value) {
|
||||
arrivalPanelRef.value?.expandPanel()
|
||||
@@ -1417,15 +1438,110 @@ const handleHomeSearchExpandedChange = (expanded: boolean) => {
|
||||
homeSearchExpanded.value = expanded
|
||||
}
|
||||
|
||||
const handleHomeSearchOutsideTap = () => {
|
||||
closeHomeSearchDock()
|
||||
const clearHomeSearchMapState = () => {
|
||||
homeCategoryModeActive.value = false
|
||||
searchVisiblePoiIds.value = null
|
||||
searchTargetFocusRequest.value = null
|
||||
}
|
||||
|
||||
const handleHomeDockActivate = () => {
|
||||
if (homeSearchExpanded.value) return
|
||||
const handleHomeCategoryModeChange = (active: boolean) => {
|
||||
homeCategoryModeActive.value = active
|
||||
if (!active) {
|
||||
searchVisiblePoiIds.value = null
|
||||
return
|
||||
}
|
||||
|
||||
homeSearchPanelRef.value?.expandHomePanel?.()
|
||||
homeSearchExpanded.value = true
|
||||
closeArrivalPanel()
|
||||
showRoutePlanner.value = false
|
||||
isSimulatingRoute.value = false
|
||||
selectedGuidePoi.value = null
|
||||
isPoiCardCollapsed.value = false
|
||||
is3DMode.value = true
|
||||
indoorView.value = 'floor'
|
||||
}
|
||||
|
||||
const handleHomeSearchResultsChange = (state: PoiCategoryResultState) => {
|
||||
if (!state.active) {
|
||||
searchVisiblePoiIds.value = null
|
||||
return
|
||||
}
|
||||
|
||||
searchVisiblePoiIds.value = [...state.visiblePoiIds]
|
||||
if (state.floorId) {
|
||||
activeGuideFloor.value = state.floorId
|
||||
}
|
||||
}
|
||||
|
||||
const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
|
||||
const resultCount = homeCategoryModeActive.value
|
||||
? context.floorResultCount
|
||||
: context.resultCount
|
||||
const params = new URLSearchParams({
|
||||
source: 'search',
|
||||
searchOrigin: context.origin,
|
||||
searchKeyword: context.keyword,
|
||||
searchCategoryId: context.categoryId,
|
||||
searchFloorId: context.floorId,
|
||||
searchFloorLabel: context.floorLabel,
|
||||
resultCount: String(Math.max(1, resultCount)),
|
||||
floorResultCount: String(context.floorResultCount),
|
||||
visiblePoiIds: context.visiblePoiIds.join(','),
|
||||
listScrollTop: String(context.listScrollTop),
|
||||
id: poi.id,
|
||||
target: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel
|
||||
})
|
||||
|
||||
return `/pages/facility/detail?${params.toString()}`
|
||||
}
|
||||
|
||||
const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
|
||||
closeArrivalPanel()
|
||||
showRoutePlanner.value = false
|
||||
isSimulatingRoute.value = false
|
||||
selectedGuidePoi.value = null
|
||||
isPoiCardCollapsed.value = false
|
||||
is3DMode.value = true
|
||||
indoorView.value = 'floor'
|
||||
activeGuideFloor.value = poi.floorId
|
||||
searchTargetFocusRequestId += 1
|
||||
searchTargetFocusRequest.value = toTargetFocusRequestViewModel({
|
||||
poiId: poi.id,
|
||||
name: poi.name,
|
||||
floorId: poi.floorId,
|
||||
floorLabel: poi.floorLabel,
|
||||
primaryCategoryZh: poi.primaryCategory.label,
|
||||
positionGltf: poi.positionGltf,
|
||||
sourceObjectName: poi.sourceObjectName,
|
||||
kind: poi.kind,
|
||||
hallId: poi.hallId,
|
||||
hallName: poi.hallName,
|
||||
entrances: poi.entrances
|
||||
}, searchTargetFocusRequestId)
|
||||
|
||||
// 先把楼层与目标请求交给地图,再进入详情页;返回时列表组件仍保持原状态。
|
||||
await nextTick()
|
||||
uni.navigateTo({
|
||||
url: createSearchDetailUrl(poi, context),
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '点位详情打开失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleHomeSearchCancel = () => {
|
||||
clearHomeSearchMapState()
|
||||
selectedGuidePoi.value = null
|
||||
isPoiCardCollapsed.value = false
|
||||
homeSearchExpanded.value = false
|
||||
}
|
||||
|
||||
const handleHomeSearchOutsideTap = () => {
|
||||
closeHomeSearchDock()
|
||||
}
|
||||
|
||||
const guideShellMode = computed(() => (is3DMode.value ? '3d' : '2d'))
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
<view class="search-panel-shell">
|
||||
<PoiSearchPanel
|
||||
ref="searchPanelRef"
|
||||
:initial-keyword="initialKeyword"
|
||||
variant="page"
|
||||
autofocus
|
||||
@@ -21,13 +22,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
|
||||
const initialKeyword = ref('')
|
||||
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
||||
const searchPanelRef = ref<{
|
||||
restoreResultListScroll?: () => Promise<void>
|
||||
} | null>(null)
|
||||
|
||||
onLoad((options: any) => {
|
||||
const keyword = typeof options.keyword === 'string'
|
||||
@@ -37,14 +41,14 @@ onLoad((options: any) => {
|
||||
initialKeyword.value = keyword
|
||||
})
|
||||
|
||||
onShow(async () => {
|
||||
await nextTick()
|
||||
await searchPanelRef.value?.restoreResultListScroll?.()
|
||||
})
|
||||
|
||||
const handleBackToMap = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=guide'
|
||||
})
|
||||
}
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index?tab=guide'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user