提交H5业务闭环修复代码

This commit is contained in:
lyf
2026-07-01 16:02:21 +08:00
parent c43aa00038
commit e0aeafe062
26 changed files with 396 additions and 95 deletions

View File

@@ -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 () => {

View File

@@ -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 = () => {

View File

@@ -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)
}
// 馆外导航处理函数

View File

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