优化室内导览点位选择布局

This commit is contained in:
lyf
2026-06-29 00:16:40 +08:00
parent c4d819021f
commit ba4decb5b5

View File

@@ -18,7 +18,7 @@
</view>
</view>
<view class="picker-search">
<view v-if="floorGroups.length" class="picker-search">
<input
class="picker-search-input"
type="text"
@@ -39,26 +39,51 @@
<view v-else-if="error" class="picker-state">
<text class="picker-state-text error">{{ error }}</text>
</view>
<view v-else-if="filteredOptions.length === 0" class="picker-state">
<view v-else-if="floorGroups.length === 0" class="picker-state">
<text class="picker-state-text">{{ emptyText }}</text>
</view>
<scroll-view v-else class="picker-list" scroll-y>
<view
v-for="option in filteredOptions"
:key="option.poiId"
class="picker-option"
:class="{ active: selectedPoiId === option.poiId }"
@tap="handleSelect(option)"
>
<view class="option-main">
<text class="option-name">{{ option.name }}</text>
<text class="option-meta">{{ formatMeta(option) }}</text>
<view v-else class="picker-body">
<scroll-view class="floor-column" scroll-y>
<view
v-for="floor in floorGroups"
:key="floor.floorId"
class="picker-floor-option"
:class="{ active: activeFloorId === floor.floorId }"
@tap="handleFloorSelect(floor.floorId)"
>
<text class="floor-option-name">{{ floor.floorLabel }}</text>
<text class="floor-option-count">{{ floor.options.length }}</text>
</view>
<view v-if="selectedPoiId === option.poiId" class="option-check">
<text class="option-check-text">已选</text>
</scroll-view>
<view class="point-column">
<view class="point-column-header">
<text class="point-column-title">{{ selectedFloorGroup?.floorLabel || '楼层' }}</text>
<text class="point-column-count">{{ filteredOptions.length }} 个点位</text>
</view>
<view v-if="filteredOptions.length === 0" class="picker-state point-empty">
<text class="picker-state-text">{{ emptyText }}</text>
</view>
<scroll-view v-else class="picker-list" scroll-y>
<view
v-for="option in filteredOptions"
:key="option.poiId"
class="picker-option"
:class="{ active: selectedPoiId === option.poiId }"
@tap="handleSelect(option)"
>
<view class="option-main">
<text class="option-name">{{ option.name }}</text>
<text class="option-meta">{{ formatMeta(option) }}</text>
</view>
<view v-if="selectedPoiId === option.poiId" class="option-check">
<text class="option-check-text">已选</text>
</view>
</view>
</scroll-view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
@@ -66,6 +91,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import {
compareFloorsTopToBottom,
isIndoorNavigableFloor
} from '@/domain/guideFloor'
@@ -109,12 +135,42 @@ const emit = defineEmits<{
}>()
const keyword = ref(props.initialKeyword)
const selectedFloorId = ref('')
watch(
() => props.visible,
(visible) => {
if (visible) {
keyword.value = props.initialKeyword
selectedFloorId.value = selectedOption.value?.floorId || floorGroups.value[0]?.floorId || ''
}
}
)
watch(
() => props.options,
() => {
if (!floorGroups.value.length) {
selectedFloorId.value = ''
keyword.value = ''
return
}
const hasSelectedFloor = floorGroups.value.some((floor) => floor.floorId === selectedFloorId.value)
if (!hasSelectedFloor) {
selectedFloorId.value = selectedOption.value?.floorId || floorGroups.value[0]?.floorId || ''
keyword.value = ''
}
}
)
watch(
() => props.selectedPoiId,
() => {
if (!props.visible) return
const floorId = selectedOption.value?.floorId
if (floorId) {
selectedFloorId.value = floorId
}
}
)
@@ -130,15 +186,55 @@ watch(
const normalizedKeyword = computed(() => keyword.value.trim().toLowerCase())
const filteredOptions = computed(() => {
const indoorOptions = props.options.filter((option) => isIndoorNavigableFloor({
const indoorOptions = computed(() => (
props.options.filter((option) => isIndoorNavigableFloor({
floorId: option.floorId,
label: option.floorLabel
}))
))
if (!normalizedKeyword.value) return indoorOptions
const selectedOption = computed(() => (
indoorOptions.value.find((option) => option.poiId === props.selectedPoiId) || null
))
return indoorOptions.filter((option) => {
const floorGroups = computed(() => {
const groups = new Map<string, { floorId: string; floorLabel: string; options: RoutePointOption[] }>()
indoorOptions.value.forEach((option) => {
const floorId = option.floorId
const current = groups.get(floorId)
if (current) {
current.options.push(option)
return
}
groups.set(floorId, {
floorId,
floorLabel: option.floorLabel || option.floorId,
options: [option]
})
})
return Array.from(groups.values()).sort((a, b) => compareFloorsTopToBottom(
{ floorId: a.floorId, label: a.floorLabel },
{ floorId: b.floorId, label: b.floorLabel }
))
})
const selectedFloorGroup = computed(() => (
floorGroups.value.find((floor) => floor.floorId === selectedFloorId.value)
|| floorGroups.value[0]
|| null
))
const activeFloorId = computed(() => selectedFloorGroup.value?.floorId || '')
const filteredOptions = computed(() => {
const floorOptions = selectedFloorGroup.value?.options || []
if (!normalizedKeyword.value) return floorOptions
return floorOptions.filter((option) => {
const fields = [
option.name,
option.floorId,
@@ -168,6 +264,10 @@ const handleSearchConfirm = () => {
emit('search', keyword.value)
}
const handleFloorSelect = (floorId: string) => {
selectedFloorId.value = floorId
}
const handleClose = () => {
emit('close')
}
@@ -269,12 +369,120 @@ const handleSelect = (option: RoutePointOption) => {
color: #8b8b84;
}
.picker-list {
.picker-body {
min-height: 260px;
height: min(48vh, calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 170px));
margin-top: 10px;
max-height: min(48vh, calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 170px));
display: flex;
gap: 10px;
overflow: hidden;
}
.picker-list {
height: 100%;
overscroll-behavior: contain;
}
.floor-column {
flex: 0 0 82px;
width: 82px;
height: 100%;
background: #f6f7f4;
border: 1px solid #ecede7;
border-radius: 8px;
box-sizing: border-box;
overflow: hidden;
overscroll-behavior: contain;
}
.point-column {
min-width: 0;
flex: 1 1 auto;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.point-column-header {
min-height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 0 2px 8px;
box-sizing: border-box;
border-bottom: 1px solid #ecede7;
}
.point-column-title {
min-width: 0;
font-size: 15px;
line-height: 20px;
font-weight: 700;
color: #1f2329;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.point-column-count {
flex: 0 0 auto;
font-size: 12px;
line-height: 16px;
color: #696962;
}
.picker-floor-option {
min-height: 52px;
padding: 8px 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
box-sizing: border-box;
background: transparent;
border-bottom: 1px solid #ecede7;
}
.picker-floor-option.active {
background: #ffffff;
box-shadow: inset 3px 0 0 #1f2329;
}
.floor-option-name {
max-width: 100%;
font-size: 14px;
line-height: 18px;
font-weight: 700;
color: #1f2329;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.floor-option-count {
min-width: 22px;
height: 16px;
padding: 0 5px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
background: #e8ebe3;
border-radius: 8px;
font-size: 11px;
line-height: 14px;
font-weight: 600;
color: #696962;
}
.picker-floor-option.active .floor-option-count {
background: #1f2329;
color: var(--museum-accent);
}
.picker-option {
min-height: 62px;
padding: 12px 10px;
@@ -343,6 +551,11 @@ const handleSelect = (option: RoutePointOption) => {
text-align: center;
}
.picker-state.point-empty {
flex: 1 1 auto;
min-height: 0;
}
.picker-state-text {
max-width: 260px;
font-size: 13px;