优化室内导览点位选择布局
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user