提交H5业务闭环修复代码
This commit is contained in:
@@ -143,35 +143,15 @@ const detailMeta = computed(() => [
|
||||
].filter(Boolean).join(' · '))
|
||||
const hallLocationId = computed(() => exhibit.value.hallId || resolvedHallId.value)
|
||||
|
||||
const normalizePoiName = (value: string) => value
|
||||
.trim()
|
||||
.replace(/[\s()()【】[\]_-]/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
// 讲解详情页只定位到所属展厅,不再追踪具体展品点位。
|
||||
const resolveHallGuidePoi = async () => {
|
||||
const hallId = exhibit.value.hallId
|
||||
if (hallId) {
|
||||
const hallPoi = await guideUseCase.getPoiById(hallId)
|
||||
if (hallPoi?.kind === 'hall' || hallPoi?.primaryCategory.id === 'exhibition_hall') {
|
||||
return hallPoi
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedTarget = normalizePoiName(exhibit.value.hallName || '')
|
||||
if (!normalizedTarget) return null
|
||||
|
||||
const candidates = await guideUseCase.searchPois(exhibit.value.hallName || exhibit.value.title || '')
|
||||
return candidates.find((candidate) => {
|
||||
const candidateName = normalizePoiName(candidate.name)
|
||||
const candidateHallName = normalizePoiName(candidate.hallName || '')
|
||||
return candidate.kind === 'hall'
|
||||
|| candidate.primaryCategory.id === 'exhibition_hall'
|
||||
|| candidateName === normalizedTarget
|
||||
|| candidateHallName === normalizedTarget
|
||||
|| candidateName.includes(normalizedTarget)
|
||||
|| normalizedTarget.includes(candidateName)
|
||||
}) || candidates[0] || null
|
||||
return guideUseCase.resolveContentLocationPreviewTarget({
|
||||
directPoiId: exhibit.value.location?.poiId,
|
||||
location: exhibit.value.location,
|
||||
hallId: exhibit.value.hallId,
|
||||
hallName: exhibit.value.hallName,
|
||||
targetName: exhibit.value.title
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(async (options: any = {}) => {
|
||||
@@ -288,6 +268,15 @@ const handleAudioError = (_audio: AudioItem | null, message: string) => {
|
||||
isPlaying.value = false
|
||||
showAudioPlayer.value = false
|
||||
currentAudio.value = null
|
||||
exhibit.value = {
|
||||
...exhibit.value,
|
||||
audio: {
|
||||
...exhibit.value.audio,
|
||||
status: 'unavailable',
|
||||
url: undefined,
|
||||
unavailableReason: message || '音频加载失败,当前提供图文讲解。'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleNavigate = async () => {
|
||||
|
||||
@@ -95,6 +95,9 @@ import {
|
||||
isGuideTopTab,
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import {
|
||||
HALL_PLACEHOLDER_IMAGE
|
||||
} from '@/utils/placeholders'
|
||||
|
||||
const activeTopTab = ref<GuideTopTab>('explain')
|
||||
const loading = ref(false)
|
||||
@@ -106,7 +109,7 @@ const hall = ref<HallDetailViewModel>({
|
||||
name: '展厅讲解',
|
||||
floorLabel: '楼层待补充',
|
||||
description: '该展厅暂无介绍文案。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
image: HALL_PLACEHOLDER_IMAGE,
|
||||
exhibitCount: 0,
|
||||
area: '待补充'
|
||||
})
|
||||
@@ -119,12 +122,6 @@ const explainCountText = computed(() => (
|
||||
|
||||
const hallLocationPoiId = computed(() => hall.value.poiId || resolvedHallPoiId.value)
|
||||
|
||||
const normalizePoiName = (value: string) => value
|
||||
.trim()
|
||||
.replace(/[\s()()【】[\]_-]/g, '')
|
||||
.toLowerCase()
|
||||
|
||||
// 展厅数据里可能没有直接写入 poiId,优先走内容层映射,再按展厅名称兜底匹配导览点位。
|
||||
const applyMatchedHallPoi = (matchedPoi: Awaited<ReturnType<typeof guideUseCase.getPoiById>>) => {
|
||||
if (!matchedPoi) return
|
||||
|
||||
@@ -146,26 +143,13 @@ const applyMatchedHallPoi = (matchedPoi: Awaited<ReturnType<typeof guideUseCase.
|
||||
}
|
||||
|
||||
const resolveHallGuidePoi = async () => {
|
||||
const directPoiId = hall.value.location?.poiId || hall.value.poiId
|
||||
if (directPoiId) {
|
||||
const directPoi = await guideUseCase.getPoiById(directPoiId)
|
||||
if (directPoi) return directPoi
|
||||
}
|
||||
|
||||
const normalizedTarget = normalizePoiName(hall.value.name)
|
||||
if (!normalizedTarget) return null
|
||||
|
||||
const candidates = await guideUseCase.searchPois(hall.value.name)
|
||||
return candidates.find((candidate) => {
|
||||
const candidateName = normalizePoiName(candidate.name)
|
||||
const candidateHallName = normalizePoiName(candidate.hallName || '')
|
||||
return candidate.kind === 'hall'
|
||||
|| candidate.primaryCategory.id === 'exhibition_hall'
|
||||
|| candidateName === normalizedTarget
|
||||
|| candidateHallName === normalizedTarget
|
||||
|| candidateName.includes(normalizedTarget)
|
||||
|| normalizedTarget.includes(candidateName)
|
||||
}) || candidates[0] || null
|
||||
return guideUseCase.resolveContentLocationPreviewTarget({
|
||||
directPoiId: hall.value.poiId,
|
||||
location: hall.value.location,
|
||||
hallId: hall.value.id,
|
||||
hallName: hall.value.name,
|
||||
targetName: hall.value.name
|
||||
})
|
||||
}
|
||||
|
||||
const resolveHallGuidePoiInBackground = () => {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
floor-side="left"
|
||||
show-floor-header
|
||||
:show-layer-mode-toggle="false"
|
||||
:show-floor="is3DMode"
|
||||
:show-floor="is3DMode && !showRoutePlanner"
|
||||
show-zoom-controls
|
||||
zoom-controls-top="calc(100vh - 252px)"
|
||||
:route-preview="activeRoutePreview"
|
||||
@@ -858,20 +858,6 @@ const refreshRouteReadinessState = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openRoutePlanner = () => {
|
||||
closeOutdoorNavPanel()
|
||||
showRoutePlanner.value = true
|
||||
is3DMode.value = true
|
||||
indoorView.value = indoorView.value === 'overview' ? 'floor' : indoorView.value
|
||||
guideOutdoorState.value = 'home'
|
||||
isPoiCardCollapsed.value = false
|
||||
isSimulatingRoute.value = false
|
||||
if (!routePointOptions.value.length) {
|
||||
void loadRouteTargets()
|
||||
}
|
||||
void refreshRouteReadinessState()
|
||||
}
|
||||
|
||||
const clearRoutePreviewState = () => {
|
||||
guideMapShellRef.value?.clearRoute?.()
|
||||
activeRoutePreview.value = null
|
||||
@@ -1036,7 +1022,15 @@ const handleSwitchEntrance = () => {
|
||||
}
|
||||
|
||||
const handleMoreRouteGuide = () => {
|
||||
openRoutePlanner()
|
||||
closeOutdoorNavPanel()
|
||||
showRoutePlanner.value = false
|
||||
is3DMode.value = true
|
||||
indoorView.value = 'floor'
|
||||
guideOutdoorState.value = 'home'
|
||||
selectedGuidePoi.value = null
|
||||
isPoiCardCollapsed.value = false
|
||||
isSimulatingRoute.value = false
|
||||
showIndoorHint(`当前楼层:${getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'}`, 3200)
|
||||
}
|
||||
|
||||
// 馆外导航处理函数
|
||||
|
||||
@@ -114,9 +114,20 @@
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-else-if="routeViewState === 'preview'"
|
||||
v-else-if="routeViewState === 'preview' && !isIndoorGuidePanelHidden"
|
||||
class="indoor-guide-card"
|
||||
:class="{ dragging: isIndoorGuideDragging }"
|
||||
:style="indoorGuideCardStyle"
|
||||
@touchstart.stop="handleIndoorGuideGestureStart"
|
||||
@touchmove.stop="handleIndoorGuideGestureMove"
|
||||
@touchend.stop="handleIndoorGuideGestureEnd"
|
||||
@touchcancel.stop="handleIndoorGuideGestureEnd"
|
||||
@mousedown.stop="handleIndoorGuideMouseStart"
|
||||
@mousemove.stop="handleIndoorGuideMouseMove"
|
||||
@mouseup.stop="handleIndoorGuideMouseEnd"
|
||||
@mouseleave.stop="handleIndoorGuideMouseEnd"
|
||||
>
|
||||
<view class="indoor-guide-handle" @tap.stop="hideIndoorGuidePanel"></view>
|
||||
<text class="indoor-guide-title">{{ indoorGuideTitle }}</text>
|
||||
<text class="indoor-guide-desc">{{ indoorGuideDesc }}</text>
|
||||
<view v-if="routePlanError" class="indoor-guide-error">
|
||||
@@ -140,6 +151,15 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-else-if="routeViewState === 'preview' && isIndoorGuidePanelHidden"
|
||||
class="indoor-guide-peek"
|
||||
@tap="showIndoorGuidePanel"
|
||||
>
|
||||
<view class="indoor-guide-peek-handle"></view>
|
||||
<text class="indoor-guide-peek-text">点位预览</text>
|
||||
</view>
|
||||
</GuideMapShell>
|
||||
</GuidePageFrame>
|
||||
</template>
|
||||
@@ -207,6 +227,14 @@ const routeReadinessMessage = ref('')
|
||||
const routePlanning = ref(false)
|
||||
const routePlanError = ref('')
|
||||
const activeRoutePreview = ref<GuideRouteResult | null>(null)
|
||||
const isIndoorGuidePanelHidden = ref(false)
|
||||
const indoorGuideTouchStartY = ref(0)
|
||||
const indoorGuideTouchCurrentY = ref(0)
|
||||
const indoorGuideTouchStartTime = ref(0)
|
||||
const isIndoorGuideDragging = ref(false)
|
||||
const indoorGuideHideDistance = 60
|
||||
const indoorGuideFastSwipeDistance = 24
|
||||
const indoorGuideFastSwipeVelocity = 0.5
|
||||
const manualFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||||
const guideFloorLabel = (floorId: string) => (
|
||||
guideFloors.value.find((floor) => floor.id === floorId)?.label || floorId
|
||||
@@ -336,6 +364,17 @@ const indoorGuideDesc = computed(() => {
|
||||
: routeReadinessMessage.value || '当前可查看位置预览,暂不提供正式室内导航。'
|
||||
})
|
||||
|
||||
const indoorGuideDragOffset = computed(() => Math.max(
|
||||
0,
|
||||
Math.min(120, indoorGuideTouchCurrentY.value - indoorGuideTouchStartY.value)
|
||||
))
|
||||
|
||||
const indoorGuideCardStyle = computed(() => (
|
||||
indoorGuideDragOffset.value > 0
|
||||
? { transform: `translateY(${indoorGuideDragOffset.value}px)` }
|
||||
: {}
|
||||
))
|
||||
|
||||
const safeDecode = (value: string) => {
|
||||
try {
|
||||
return decodeURIComponent(value)
|
||||
@@ -456,6 +495,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
targetFocusRequest.value = null
|
||||
activeRoutePreview.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
isIndoorGuidePanelHidden.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -469,6 +509,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
targetFocusRequest.value = null
|
||||
activeRoutePreview.value = null
|
||||
isManualLocationPanelOpen.value = false
|
||||
isIndoorGuidePanelHidden.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -481,6 +522,7 @@ const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||||
hasRouteTarget.value = true
|
||||
routeViewState.value = isRouteViewState(state) ? state : 'preview'
|
||||
isManualLocationPanelOpen.value = false
|
||||
isIndoorGuidePanelHidden.value = false
|
||||
|
||||
if (routeViewState.value === 'preview') {
|
||||
requestTargetFocus(route.value.targetLocation, matchedPoi.name)
|
||||
@@ -556,12 +598,95 @@ const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||||
|
||||
const handleReturnToPreview = () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
isIndoorGuidePanelHidden.value = false
|
||||
activeRoutePreview.value = null
|
||||
routeViewState.value = 'preview'
|
||||
requestTargetFocus(route.value.targetLocation)
|
||||
saveCurrentLocationPreviewUrl()
|
||||
}
|
||||
|
||||
const hideIndoorGuidePanel = () => {
|
||||
isIndoorGuidePanelHidden.value = true
|
||||
isIndoorGuideDragging.value = false
|
||||
indoorGuideTouchStartY.value = 0
|
||||
indoorGuideTouchCurrentY.value = 0
|
||||
indoorGuideTouchStartTime.value = 0
|
||||
}
|
||||
|
||||
const showIndoorGuidePanel = () => {
|
||||
isIndoorGuidePanelHidden.value = false
|
||||
indoorGuideTouchStartY.value = 0
|
||||
indoorGuideTouchCurrentY.value = 0
|
||||
indoorGuideTouchStartTime.value = 0
|
||||
}
|
||||
|
||||
const getIndoorGuideGestureClientY = (event: TouchEvent | MouseEvent) => {
|
||||
if ('changedTouches' in event) {
|
||||
return event.changedTouches?.[0]?.clientY
|
||||
?? event.touches?.[0]?.clientY
|
||||
?? 0
|
||||
}
|
||||
|
||||
return event.clientY
|
||||
}
|
||||
|
||||
const startIndoorGuidePanelDrag = (clientY: number) => {
|
||||
isIndoorGuideDragging.value = true
|
||||
indoorGuideTouchStartY.value = clientY
|
||||
indoorGuideTouchCurrentY.value = clientY
|
||||
indoorGuideTouchStartTime.value = Date.now()
|
||||
}
|
||||
|
||||
const moveIndoorGuidePanelDrag = (clientY: number) => {
|
||||
if (!isIndoorGuideDragging.value) return
|
||||
indoorGuideTouchCurrentY.value = clientY
|
||||
}
|
||||
|
||||
const finishIndoorGuidePanelDrag = (clientY: number) => {
|
||||
if (!isIndoorGuideDragging.value) return
|
||||
indoorGuideTouchCurrentY.value = clientY
|
||||
|
||||
const distance = indoorGuideTouchCurrentY.value - indoorGuideTouchStartY.value
|
||||
const duration = Math.max(Date.now() - indoorGuideTouchStartTime.value, 1)
|
||||
const velocity = distance / duration
|
||||
const shouldHideByDistance = distance > indoorGuideHideDistance
|
||||
const shouldHideBySpeed = distance > indoorGuideFastSwipeDistance && velocity > indoorGuideFastSwipeVelocity
|
||||
|
||||
if (shouldHideByDistance || shouldHideBySpeed) {
|
||||
hideIndoorGuidePanel()
|
||||
return
|
||||
}
|
||||
|
||||
isIndoorGuideDragging.value = false
|
||||
indoorGuideTouchStartY.value = 0
|
||||
indoorGuideTouchCurrentY.value = 0
|
||||
indoorGuideTouchStartTime.value = 0
|
||||
}
|
||||
|
||||
const handleIndoorGuideGestureStart = (event: TouchEvent) => {
|
||||
startIndoorGuidePanelDrag(getIndoorGuideGestureClientY(event))
|
||||
}
|
||||
|
||||
const handleIndoorGuideGestureMove = (event: TouchEvent) => {
|
||||
moveIndoorGuidePanelDrag(getIndoorGuideGestureClientY(event))
|
||||
}
|
||||
|
||||
const handleIndoorGuideGestureEnd = (event: TouchEvent) => {
|
||||
finishIndoorGuidePanelDrag(getIndoorGuideGestureClientY(event))
|
||||
}
|
||||
|
||||
const handleIndoorGuideMouseStart = (event: MouseEvent) => {
|
||||
startIndoorGuidePanelDrag(getIndoorGuideGestureClientY(event))
|
||||
}
|
||||
|
||||
const handleIndoorGuideMouseMove = (event: MouseEvent) => {
|
||||
moveIndoorGuidePanelDrag(getIndoorGuideGestureClientY(event))
|
||||
}
|
||||
|
||||
const handleIndoorGuideMouseEnd = (event: MouseEvent) => {
|
||||
finishIndoorGuidePanelDrag(getIndoorGuideGestureClientY(event))
|
||||
}
|
||||
|
||||
const handleRelocate = () => {
|
||||
isManualLocationPanelOpen.value = false
|
||||
routeViewState.value = 'preview'
|
||||
@@ -740,7 +865,7 @@ const handlePageBack = () => {
|
||||
right: 16px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 116px);
|
||||
min-height: 156px;
|
||||
padding: 18px 18px 16px;
|
||||
padding: 24px 18px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
@@ -750,6 +875,56 @@ const handlePageBack = () => {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.14);
|
||||
z-index: 45;
|
||||
transition: transform 0.18s ease;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.indoor-guide-card.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.indoor-guide-handle {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
width: 42px;
|
||||
height: 4px;
|
||||
transform: translateX(-50%);
|
||||
background: #d5d8cf;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.indoor-guide-peek {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 104px);
|
||||
width: 132px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
box-sizing: border-box;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid #e5e6de;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 18px rgba(36, 49, 42, 0.14);
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.indoor-guide-peek-handle {
|
||||
width: 22px;
|
||||
height: 4px;
|
||||
background: #151713;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.indoor-guide-peek-text {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
color: #151713;
|
||||
}
|
||||
|
||||
.indoor-guide-title {
|
||||
|
||||
Reference in New Issue
Block a user