UI коструктора отчетов
This commit is contained in:
114
resources/js/Pages/Analytics/Components/PickerPanel.vue
Normal file
114
resources/js/Pages/Analytics/Components/PickerPanel.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user