168 lines
4.2 KiB
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>
|