yunsan-pc/src/views/driving/components/MetroLineSelect.vue

168 lines
4.2 KiB
Vue

<template>
<div ref="root" class="metro-line-select">
<button class="select-trigger" type="button" aria-haspopup="listbox" :aria-expanded="open" @click="open = !open"
@keydown.down.prevent="moveSelection(1)" @keydown.up.prevent="moveSelection(-1)" @keydown.esc="open = false">
<span class="selected-line" :style="lineStyle(selectedLine)">
<strong v-if="isNumberedLine(selectedLine)">{{ selectedLine.id }}</strong>
<span>{{ isNumberedLine(selectedLine) ? '号线' : selectedLine.name }}</span>
</span>
<img class="arrow" :class="{ open }" src="/imgs/driving/arrow.png" alt="" aria-hidden="true" />
</button>
<Transition name="line-options">
<ul v-if="open" class="options" role="listbox">
<li v-for="line in lineOptions" :key="line.id" :class="{ active: line.id === modelValue }" role="option"
:aria-selected="line.id === modelValue" @click="selectLine(line.id)">
<span class="line-swatch" :style="lineStyle(line)">{{ line.name }}</span>
</li>
</ul>
</Transition>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { METRO_LINES } from '@/utils/const.js'
const props = defineProps({
modelValue: { type: Number, default: 7 }
})
const emit = defineEmits(['update:modelValue', 'change'])
const root = ref(null)
const open = ref(false)
const lineOptions = Object.entries(METRO_LINES).map(([id, theme]) => ({
id: Number(id),
...theme
}))
const selectedLine = computed(() => (
lineOptions.find(line => line.id === props.modelValue) || lineOptions[0]
))
const lineStyle = line => ({ backgroundColor: line.bg, color: line.color })
const isNumberedLine = line => line.id >= 1 && line.id <= 18
const selectLine = id => {
emit('update:modelValue', id)
emit('change', id)
open.value = false
}
const moveSelection = offset => {
open.value = true
const currentIndex = lineOptions.findIndex(line => line.id === props.modelValue)
const nextIndex = (currentIndex + offset + lineOptions.length) % lineOptions.length
selectLine(lineOptions[nextIndex].id)
}
const closeOnOutsideClick = event => {
if (!root.value?.contains(event.target)) open.value = false
}
onMounted(() => document.addEventListener('pointerdown', closeOnOutsideClick))
onBeforeUnmount(() => document.removeEventListener('pointerdown', closeOnOutsideClick))
</script>
<style lang="scss" scoped>
.metro-line-select {
position: relative;
width: 154px;
}
.select-trigger {
display: flex;
align-items: center;
width: 100%;
height: 43px;
padding: 0 13px 0 18px;
border: 0;
border-radius: 4px;
background: linear-gradient(90deg, rgba(51, 99, 164, .9), rgba(9, 38, 92, .66));
cursor: pointer;
}
.selected-line {
display: inline-flex;
// align-items: center;
justify-content: center;
gap: 4px;
min-width: 78px;
height: 33px;
padding: 0 9px;
border-radius: 6px;
font-size: 13px;
line-height: 1.6;
white-space: nowrap;
strong {
font-size: 30px;
line-height: 1;
}
}
.arrow {
width: 16px;
height: 16px;
margin-left: 12px;
object-fit: contain;
transition: transform .18s ease;
&.open {
transform: rotate(180deg);
}
}
.options {
position: absolute;
top: 47px;
right: 0;
z-index: 20;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
width: 238px;
max-height: 330px;
margin: 0;
padding: 9px;
overflow-y: auto;
border: 1px solid rgba(47, 156, 255, .7);
border-radius: 4px;
list-style: none;
background: rgba(3, 24, 68, .98);
box-shadow: 0 8px 24px rgba(0, 8, 30, .55);
}
.options li {
padding: 2px;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
&:hover,
&.active {
border-color: #60d9ff;
}
}
.line-swatch {
display: block;
height: 30px;
border-radius: 3px;
font-size: 14px;
line-height: 30px;
text-align: center;
white-space: nowrap;
}
.line-options-enter-active,
.line-options-leave-active {
transition: opacity .15s ease, transform .15s ease;
}
.line-options-enter-from,
.line-options-leave-to {
opacity: 0;
transform: translateY(-5px);
}
</style>