1
0
Files
TaskBoard/front_vue/src/components/ui/MobileSelect.vue
2026-01-19 15:10:37 +07:00

366 lines
7.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- Кнопка-триггер -->
<button class="mobile-select-btn" :class="[variant, { compact, 'has-selection': hasSelection }]" @click="open = true">
<i v-if="icon" :data-lucide="icon" class="btn-icon"></i>
<span v-if="!compact" class="btn-label">{{ displayValue }}</span>
<i data-lucide="chevron-down" class="btn-arrow"></i>
</button>
<!-- Fullscreen панель выбора -->
<Teleport to="body">
<Transition name="slide-up">
<div v-if="open" class="mobile-select-overlay" @click.self="open = false">
<div class="mobile-select-panel">
<!-- Header панели -->
<div class="panel-header">
<button class="panel-back" @click="open = false">
<i data-lucide="arrow-left"></i>
</button>
<h3 class="panel-title">{{ title }}</h3>
</div>
<!-- Список опций -->
<div class="panel-options">
<button
v-for="option in options"
:key="option.id"
class="option-item"
:class="{ active: modelValue === option.id }"
@click="selectOption(option.id)"
>
<span class="option-label">{{ option.label }}</span>
<i v-if="modelValue === option.id" data-lucide="check" class="option-check"></i>
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated, onUnmounted } from 'vue'
const props = defineProps({
modelValue: {
type: [Number, String, null],
default: null
},
options: {
type: Array,
required: true
// [{ id: 1, label: 'Option 1' }, ...]
},
title: {
type: String,
default: 'Выберите'
},
placeholder: {
type: String,
default: 'Выберите...'
},
icon: {
type: String,
default: null
},
variant: {
type: String,
default: 'default' // 'default' | 'accent'
},
compact: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const open = ref(false)
const displayValue = computed(() => {
if (props.modelValue === null || props.modelValue === undefined) {
return props.placeholder
}
const option = props.options.find(o => o.id === props.modelValue)
return option?.label || props.placeholder
})
// Проверка: выбран ли не-дефолтный вариант (первый option обычно "Все")
const hasSelection = computed(() => {
if (props.modelValue === null || props.modelValue === undefined) return false
// Если первый option = null — значит "Все", проверяем что выбрано что-то другое
const firstOption = props.options[0]
return firstOption && props.modelValue !== firstOption.id
})
const selectOption = (id) => {
emit('update:modelValue', id)
open.value = false
}
// Refresh icons
const refreshIcons = () => {
if (window.lucide) {
setTimeout(() => window.lucide.createIcons(), 10)
}
}
watch(open, (val) => {
if (val) {
document.body.style.overflow = 'hidden'
refreshIcons()
} else {
document.body.style.overflow = ''
}
})
// Cleanup при unmount — гарантируем сброс overflow
onUnmounted(() => {
if (open.value) {
document.body.style.overflow = ''
}
})
onMounted(refreshIcons)
onUpdated(refreshIcons)
</script>
<style scoped>
/* Кнопка триггер */
.mobile-select-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
min-height: 40px;
background: var(--bg-card);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: var(--text-secondary);
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
@media (hover: hover) {
.mobile-select-btn:hover {
background: var(--bg-card-hover);
color: var(--text-primary);
}
}
.mobile-select-btn:active {
background: var(--bg-card-hover);
color: var(--text-primary);
}
/* Акцентная версия (для проекта) */
.mobile-select-btn.accent {
background: var(--accent-soft);
border-color: var(--accent);
color: var(--accent);
}
@media (hover: hover) {
.mobile-select-btn.accent:hover {
background: var(--accent);
color: #000;
}
}
.mobile-select-btn.accent:active {
background: var(--accent);
color: #000;
}
.mobile-select-btn.accent .btn-icon,
.mobile-select-btn.accent .btn-arrow {
opacity: 1;
}
/* Компактный режим (только иконка) */
.mobile-select-btn.compact {
width: 40px;
height: 40px;
padding: 0;
justify-content: center;
border-radius: 10px;
}
.mobile-select-btn.compact .btn-icon {
width: 18px;
height: 18px;
margin: 0;
}
.mobile-select-btn.compact .btn-arrow {
display: none;
}
/* Подсветка когда выбран активный фильтр */
.mobile-select-btn.has-selection {
background: var(--accent-soft);
border-color: var(--accent);
color: var(--accent);
}
.mobile-select-btn.has-selection .btn-icon {
opacity: 1;
}
.btn-icon {
width: 14px;
height: 14px;
opacity: 0.7;
}
.btn-label {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-arrow {
width: 12px;
height: 12px;
opacity: 0.4;
margin-left: 2px;
}
/* Overlay */
.mobile-select-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
display: flex;
align-items: flex-start;
}
/* Панель */
.mobile-select-panel {
width: 100%;
max-height: 80vh;
background: var(--bg-body);
border-radius: 0 0 20px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
/* Safe area для iPhone notch */
padding-top: env(safe-area-inset-top, 0px);
}
.panel-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.panel-back {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: 8px;
transition: all 0.15s;
}
.panel-back:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--text-primary);
}
.panel-back i {
width: 20px;
height: 20px;
}
.panel-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
/* Опции */
.panel-options {
flex: 1;
overflow-y: auto;
padding: 8px;
padding-bottom: 16px;
}
.option-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 16px;
background: none;
border: none;
border-radius: 12px;
color: var(--text-secondary);
font-family: inherit;
font-size: 16px;
text-align: left;
cursor: pointer;
transition: all 0.15s;
}
@media (hover: hover) {
.option-item:hover {
background: rgba(255, 255, 255, 0.04);
}
}
.option-item:active {
background: rgba(255, 255, 255, 0.08);
}
.option-item.active {
background: var(--accent-soft);
color: var(--accent);
}
.option-label {
flex: 1;
}
.option-check {
width: 20px;
height: 20px;
color: var(--accent);
}
/* Анимация */
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-active .mobile-select-panel,
.slide-up-leave-active .mobile-select-panel {
transition: transform 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
}
.slide-up-enter-from .mobile-select-panel,
.slide-up-leave-to .mobile-select-panel {
transform: translateY(-100%);
}
</style>