115 lines
4.8 KiB
Vue
115 lines
4.8 KiB
Vue
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { NInput, NButton, NCheckbox, NText, NScrollbar, NIcon, NEmpty } from 'naive-ui'
|
|
import { VueDraggableNext as draggable } from 'vue-draggable-next'
|
|
import { TbArrowLeft, TbSearch, TbGripVertical, TbX, TbPlus } from 'vue-icons-plus/tb'
|
|
|
|
const props = defineProps({
|
|
title: { type: String, required: true },
|
|
description: { type: String, default: '' },
|
|
items: { type: Array, default: () => [] }, // [{ key, label, unit? }]
|
|
modelValue: { type: Array, default: () => [] }, // выбранные ключи (в порядке)
|
|
searchPlaceholder: { type: String, default: 'Поиск' },
|
|
allowCreate: { type: Boolean, default: false },
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'back', 'create'])
|
|
|
|
const search = ref('')
|
|
|
|
const itemMap = computed(() => Object.fromEntries(props.items.map((i) => [i.key, i])))
|
|
|
|
// Список «Выбрано» — объекты в порядке modelValue (для draggable).
|
|
const selectedItems = computed({
|
|
get: () => props.modelValue.map((key) => itemMap.value[key]).filter(Boolean),
|
|
set: (list) => emit('update:modelValue', list.map((i) => i.key)),
|
|
})
|
|
|
|
const availableItems = computed(() => props.items.filter((i) =>
|
|
!search.value || i.label.toLowerCase().includes(search.value.toLowerCase())
|
|
))
|
|
|
|
const isSelected = (key) => props.modelValue.includes(key)
|
|
|
|
const toggle = (key) => {
|
|
const next = isSelected(key)
|
|
? props.modelValue.filter((k) => k !== key)
|
|
: [...props.modelValue, key]
|
|
emit('update:modelValue', next)
|
|
}
|
|
|
|
const remove = (key) => emit('update:modelValue', props.modelValue.filter((k) => k !== key))
|
|
const reset = () => emit('update:modelValue', [])
|
|
</script>
|
|
|
|
<template>
|
|
<div class="picker-panel">
|
|
<div class="panel-head">
|
|
<NButton text size="small" @click="emit('back')">
|
|
<template #icon><TbArrowLeft /></template>
|
|
Назад
|
|
</NButton>
|
|
<NButton v-if="allowCreate" text size="small" type="primary" @click="emit('create')">
|
|
<template #icon><TbPlus /></template>
|
|
Создать
|
|
</NButton>
|
|
</div>
|
|
|
|
<div class="panel-title">{{ title }}</div>
|
|
<NText v-if="description" depth="3" style="font-size: 12px; display: block; margin-bottom: 10px;">{{ description }}</NText>
|
|
|
|
<NInput v-model:value="search" :placeholder="searchPlaceholder" clearable size="small" style="margin-bottom: 12px;">
|
|
<template #prefix><TbSearch :size="14" /></template>
|
|
</NInput>
|
|
|
|
<NScrollbar style="max-height: calc(100vh - 320px);">
|
|
<div class="block-head">
|
|
<NText depth="2" style="font-size: 12px; font-weight: 600;">Выбрано</NText>
|
|
<NButton v-if="modelValue.length" text size="tiny" @click="reset">Сбросить</NButton>
|
|
</div>
|
|
|
|
<NEmpty v-if="!selectedItems.length" description="Ничего не выбрано" size="small" style="padding: 8px 0;" />
|
|
<draggable v-else v-model="selectedItems" handle=".drag-handle" item-key="key" class="selected-list">
|
|
<div v-for="item in selectedItems" :key="item.key" class="selected-row">
|
|
<NIcon class="drag-handle" :size="16"><TbGripVertical /></NIcon>
|
|
<span class="row-label">{{ item.label }}</span>
|
|
<NButton text size="tiny" @click="remove(item.key)"><NIcon><TbX /></NIcon></NButton>
|
|
</div>
|
|
</draggable>
|
|
|
|
<div class="block-head" style="margin-top: 14px;">
|
|
<NText depth="2" style="font-size: 12px; font-weight: 600;">Доступно</NText>
|
|
</div>
|
|
<div class="available-list">
|
|
<NCheckbox
|
|
v-for="item in availableItems"
|
|
:key="item.key"
|
|
:checked="isSelected(item.key)"
|
|
style="display: flex; padding: 5px 0;"
|
|
@update:checked="toggle(item.key)"
|
|
>
|
|
{{ item.label }}
|
|
</NCheckbox>
|
|
</div>
|
|
</NScrollbar>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.picker-panel { padding: 4px; }
|
|
.panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
.panel-title { font-size: 18px; font-weight: 600; margin-bottom: 2px; }
|
|
.block-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
|
|
.selected-list { display: flex; flex-direction: column; gap: 4px; }
|
|
.selected-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 8px;
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--primary-color) 7%, transparent);
|
|
}
|
|
.row-label { flex: 1; font-size: 13px; }
|
|
.drag-handle { cursor: grab; color: var(--n-text-color-3, #999); }
|
|
</style>
|