简化来馆面板并优化地图点位展示

This commit is contained in:
lyf
2026-07-09 18:46:56 +08:00
parent 5b4fe19ce3
commit 3f9c64a36e
5 changed files with 587 additions and 221 deletions

View File

@@ -5,6 +5,7 @@
@tap="collapsePanel"
></view>
<view
ref="arrivalPanelRef"
v-if="visible"
class="arrival-panel"
:class="{ collapsed: isCollapsed }"
@@ -13,7 +14,7 @@
<view v-if="isCollapsed" class="arrival-collapsed" @tap="expandPanel">
<view class="arrival-collapsed-copy">
<text class="arrival-collapsed-title">来馆</text>
<text class="arrival-collapsed-summary">公交 / 地铁 / 停车</text>
<text class="arrival-collapsed-summary">{{ selectedTarget?.title || activeTypeLabel }}</text>
</view>
<view class="arrival-collapsed-action">
<text class="arrival-collapsed-action-text">展开</text>
@@ -21,83 +22,64 @@
</view>
<template v-else>
<view class="arrival-handle" @tap="collapsePanel"></view>
<view class="arrival-handle" @tap="collapsePanel"></view>
<view class="arrival-header">
<view class="arrival-title-group">
<text class="arrival-kicker">来馆</text>
<text class="arrival-title">选择第三方地图检索目标</text>
</view>
<view class="arrival-collapse" @tap="collapsePanel">
<text class="arrival-collapse-text">收起</text>
</view>
</view>
<view class="arrival-notes">
<text class="arrival-note">公交站停车场信息以第三方地图实时结果为准</text>
<text class="arrival-note">暂未接入停车余位</text>
</view>
<view class="arrival-section">
<text class="arrival-section-title">导航到场馆</text>
<view
v-for="target in venueTargets"
:key="target.id"
class="arrival-target-row"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-keyword">{{ target.keyword }}</text>
<view class="arrival-header">
<view class="arrival-title-group">
<text class="arrival-kicker">来馆</text>
</view>
<text class="arrival-target-action">选择地图</text>
</view>
</view>
<view class="arrival-section">
<text class="arrival-section-title">推荐公共交通</text>
<view
v-for="target in transitTargets"
:key="target.id"
class="arrival-target-row"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-keyword">{{ target.keyword }}</text>
<view class="arrival-collapse" @tap="collapsePanel">
<text class="arrival-collapse-text">收起</text>
</view>
<text class="arrival-target-action">选择地图</text>
</view>
</view>
<view class="arrival-section">
<text class="arrival-section-title">实时查询</text>
<view
v-for="target in realtimeTargets"
:key="target.id"
class="arrival-target-row"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-keyword">{{ target.keyword }}</text>
<view class="arrival-tabs">
<view
v-for="option in typeOptions"
:key="option.type"
class="arrival-tab"
:class="{ active: option.type === activeType }"
@tap="handleTypeChange(option.type)"
>
<text class="arrival-tab-text">{{ option.label }}</text>
</view>
<text class="arrival-target-action">选择地图</text>
</view>
</view>
<view class="arrival-list">
<view
v-for="target in targets"
:key="target.id"
class="arrival-target-row"
:class="{ active: target.id === selectedTargetId }"
@tap="handleTargetSelect(target)"
>
<view class="arrival-target-dot"></view>
<view class="arrival-target-copy">
<text class="arrival-target-title">{{ target.title }}</text>
<text class="arrival-target-subtitle">{{ target.subtitle }}</text>
<text v-if="target.description" class="arrival-target-desc">{{ target.description }}</text>
</view>
<text v-if="target.id === selectedTargetId" class="arrival-target-state">已选</text>
</view>
</view>
<view
class="arrival-primary"
:class="{ disabled: !selectedTarget }"
@tap="handleNavigateTap"
>
<text class="arrival-primary-text">第三方导航</text>
</view>
</template>
</view>
<view
v-if="selectedTarget"
v-if="providerSheetVisible && selectedTarget"
class="provider-scrim"
@tap="closeProviderSheet"
></view>
<view
v-if="selectedTarget"
v-if="providerSheetVisible && selectedTarget"
class="provider-sheet"
@tap.stop
>
@@ -126,12 +108,11 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import {
ARRIVAL_REALTIME_TARGETS,
ARRIVAL_TRANSIT_TARGETS,
ARRIVAL_VENUE_TARGETS,
type ArrivalSearchTarget
ARRIVAL_TARGET_TYPES,
type ArrivalSearchTarget,
type ArrivalTargetType
} from '@/data/arrivalSearchTargets'
import {
openThirdPartyMapSearch,
@@ -141,25 +122,44 @@ import {
const props = withDefaults(defineProps<{
visible?: boolean
defaultCollapsed?: boolean
activeType: ArrivalTargetType
targets?: ArrivalSearchTarget[]
selectedTargetId?: string
selectedTarget?: ArrivalSearchTarget | null
}>(), {
visible: false,
defaultCollapsed: false
targets: () => [] as ArrivalSearchTarget[],
selectedTargetId: '',
selectedTarget: null
})
const emit = defineEmits<{
'update:activeType': [type: ArrivalTargetType]
selectTarget: [target: ArrivalSearchTarget]
collapsedChange: [collapsed: boolean]
layoutChange: [layout: { height: number; collapsed: boolean }]
}>()
const venueTargets = computed(() => ARRIVAL_VENUE_TARGETS)
const transitTargets = computed(() => ARRIVAL_TRANSIT_TARGETS)
const realtimeTargets = computed(() => ARRIVAL_REALTIME_TARGETS)
const typeOptions = computed(() => ARRIVAL_TARGET_TYPES)
const mapProviders = computed(() => THIRD_PARTY_MAP_PROVIDERS)
const selectedTarget = ref<ArrivalSearchTarget | null>(null)
const arrivalPanelRef = ref<unknown>(null)
const isCollapsed = ref(false)
const providerSheetVisible = ref(false)
let resizeObserver: ResizeObserver | null = null
const activeTypeLabel = computed(() => (
typeOptions.value.find((option) => option.type === props.activeType)?.label || '来馆'
))
const handleTypeChange = (type: ArrivalTargetType) => {
if (type === props.activeType) return
closeProviderSheet()
emit('update:activeType', type)
}
const handleTargetSelect = (target: ArrivalSearchTarget) => {
selectedTarget.value = target
closeProviderSheet()
emit('selectTarget', target)
}
const collapsePanel = () => {
@@ -173,11 +173,76 @@ const expandPanel = () => {
}
const closeProviderSheet = () => {
selectedTarget.value = null
providerSheetVisible.value = false
}
const getArrivalPanelElement = () => {
const rawRef = arrivalPanelRef.value
if (rawRef instanceof HTMLElement) return rawRef
const maybeComponent = rawRef as { $el?: Element } | null
if (maybeComponent?.$el instanceof HTMLElement) return maybeComponent.$el
if (typeof document === 'undefined') return null
return document.querySelector<HTMLElement>('.arrival-panel')
}
const emitLayoutChange = () => {
if (!props.visible) {
emit('layoutChange', { height: 0, collapsed: false })
return
}
void nextTick(() => {
const panelElement = getArrivalPanelElement()
emit('layoutChange', {
height: panelElement?.getBoundingClientRect().height || 0,
collapsed: isCollapsed.value
})
})
}
const disconnectLayoutObserver = () => {
resizeObserver?.disconnect()
resizeObserver = null
}
const startLayoutObserver = () => {
if (typeof window === 'undefined' || typeof ResizeObserver === 'undefined') {
emitLayoutChange()
return
}
void nextTick(() => {
const panelElement = getArrivalPanelElement()
if (!panelElement) {
emitLayoutChange()
return
}
disconnectLayoutObserver()
resizeObserver = new ResizeObserver(() => {
emitLayoutChange()
})
resizeObserver.observe(panelElement)
emitLayoutChange()
})
}
const handleNavigateTap = () => {
if (!props.selectedTarget) {
uni.showToast({
title: '请先选择点位',
icon: 'none'
})
return
}
providerSheetVisible.value = true
}
const handleProviderSelect = (provider: ThirdPartyMapProviderOption) => {
const target = selectedTarget.value
const target = props.selectedTarget
if (!target) return
openThirdPartyMapSearch(provider.provider, {
@@ -190,16 +255,37 @@ watch(
() => props.visible,
(visible) => {
if (visible) {
isCollapsed.value = props.defaultCollapsed
emit('collapsedChange', props.defaultCollapsed)
} else {
selectedTarget.value = null
isCollapsed.value = false
emit('collapsedChange', false)
startLayoutObserver()
} else {
closeProviderSheet()
isCollapsed.value = false
disconnectLayoutObserver()
emit('collapsedChange', false)
emit('layoutChange', { height: 0, collapsed: false })
}
}
)
watch(
() => props.selectedTargetId,
() => {
closeProviderSheet()
}
)
watch(
() => [props.targets.length, isCollapsed.value] as const,
() => {
startLayoutObserver()
}
)
onBeforeUnmount(() => {
disconnectLayoutObserver()
})
defineExpose({
expandPanel
})
@@ -210,7 +296,7 @@ defineExpose({
position: fixed;
inset: 0;
z-index: 2090;
background: rgba(0, 0, 0, 0.18);
background: rgba(0, 0, 0, 0.08);
}
.arrival-panel {
@@ -219,8 +305,8 @@ defineExpose({
bottom: calc(env(safe-area-inset-bottom) + 16px);
z-index: 2100;
width: min(430px, calc(100vw - 24px));
max-height: min(72vh, 620px);
padding: 10px 14px 16px;
max-height: min(58vh, 520px);
padding: 10px 14px 14px;
overflow-y: auto;
box-sizing: border-box;
background: #ffffff;
@@ -346,12 +432,115 @@ defineExpose({
color: #424754;
}
.arrival-notes {
margin-top: 12px;
padding: 10px 12px;
.arrival-tabs {
height: 40px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 4px;
margin-top: 14px;
padding: 4px;
box-sizing: border-box;
background: #f3f3f3;
border-radius: 8px;
}
.arrival-tab {
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.arrival-tab.active {
background: #262421;
}
.arrival-tab-text {
font-size: 13px;
line-height: 18px;
font-weight: 700;
color: #424754;
}
.arrival-tab.active .arrival-tab-text {
color: #e0e100;
}
.arrival-list {
margin-top: 10px;
}
.arrival-target-row {
min-height: 68px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 8px;
border-top: 1px solid #eceee7;
border-radius: 8px;
box-sizing: border-box;
}
.arrival-target-row:first-child {
border-top: 0;
}
.arrival-target-row.active {
background: #f5f5ed;
}
.arrival-target-dot {
width: 10px;
height: 10px;
flex-shrink: 0;
background: #d7d9cf;
border: 2px solid #696962;
border-radius: 50%;
}
.arrival-target-row.active .arrival-target-dot {
background: #e0e100;
border-color: #262421;
}
.arrival-target-copy {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
gap: 2px;
}
.arrival-target-title {
font-size: 15px;
line-height: 21px;
font-weight: 700;
color: #262421;
}
.arrival-target-subtitle,
.arrival-target-desc {
font-size: 12px;
line-height: 17px;
color: #696962;
}
.arrival-target-desc {
color: #424754;
}
.arrival-target-state {
flex-shrink: 0;
font-size: 12px;
line-height: 17px;
font-weight: 700;
color: #262421;
}
.arrival-notes {
margin-top: 10px;
padding: 9px 10px;
background: #f5f5ed;
border-radius: 8px;
}
@@ -362,68 +551,25 @@ defineExpose({
color: #424754;
}
.arrival-section {
margin-top: 16px;
}
.arrival-section-title {
display: block;
margin-bottom: 8px;
font-size: 14px;
line-height: 20px;
font-weight: 700;
color: #262421;
}
.arrival-target-row {
min-height: 58px;
.arrival-primary {
height: 44px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
border-top: 1px solid #eceee7;
justify-content: center;
margin-top: 12px;
background: #262421;
border-radius: 8px;
}
.arrival-section-title + .arrival-target-row {
border-top: 0;
.arrival-primary.disabled {
opacity: 0.45;
}
.arrival-target-dot {
width: 9px;
height: 9px;
flex-shrink: 0;
background: #e0e100;
border: 2px solid #262421;
border-radius: 50%;
}
.arrival-target-copy {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.arrival-target-title {
.arrival-primary-text {
font-size: 15px;
line-height: 21px;
font-weight: 600;
color: #262421;
}
.arrival-target-keyword {
font-size: 12px;
line-height: 17px;
color: #696962;
}
.arrival-target-action {
flex-shrink: 0;
font-size: 12px;
line-height: 17px;
font-weight: 600;
color: #262421;
font-weight: 700;
color: #e0e100;
}
.provider-scrim {