Finder
Иерархический навигатор по колонкам, вдохновлённый Finder на macOS. Два режима: levels (фиксированная глубина через проп levels) и dynamic (рекурсивная/неизвестная глубина через проп load). Выбор элемента автоматически загружает и раскрывает следующую колонку. Листовые узлы открывают панель деталей с полной цепочкой выбора.
Detail Panel
При выборе элемента в последнем уровне справа открывается слот #detail. Слот получает выбранный item и полную цепочку selections — так что можно обратиться к любому родительскому ключу, чтобы подгрузить детали или открыть другой компонент.
Пройдите по иерархии: Departments → Teams → Members, затем кликните по участнику, чтобы увидеть его карточку профиля.
<template>
<VxFinder
:levels="levels"
height="400px"
:detail-width="320"
>
<template #detail="{ item, selections }">
<!-- item = the last-level selected item (with all your custom fields) -->
<!-- selections = full chain: [{ levelKey, key, item }, ...] -->
<MemberProfile
:id="item.key"
:department-id="selections[0]?.key"
:team-id="selections[1]?.key"
/>
</template>
</VxFinder>
</template>
<script setup>
const levels = [
{
key: 'departments',
title: 'Departments',
showAdd: true,
load: () => fetchDepartments(),
},
{
key: 'teams',
title: 'Teams',
showAdd: true,
load: (deptId) => fetchTeams(deptId),
},
{
key: 'members',
title: 'Members',
// This is the last level — selecting opens #detail
load: (teamId) => fetchMembers(teamId),
},
]
</script>Custom Item Templates
Настраивайте отрисовку элементов для каждого уровня с помощью скопированных слотов или поля component в конфиге уровня. Приоритет: слот #item-{levelKey} → слот #item → поле component → метка по умолчанию.
Слоты со слот-пропсами — используйте #item-{levelKey} для конкретного уровня или #item для всех уровней:
<template>
<VxFinder :levels="levels">
<!-- Custom render for 'members' level only -->
<template #item-members="{ item, active }">
<div class="member-row">
<Avatar :name="item.label" />
<span>{{ item.label }}</span>
<VxBadge>{{ item.role }}</VxBadge>
</div>
</template>
<!-- Fallback for all other levels -->
<template #item="{ item, levelKey }">
<span>{{ item.label }}</span>
</template>
</VxFinder>
</template>Компонент в конфиге уровня — передавайте Vue-компонент прямо в JSON:
<script setup>
import MemberItem from '../../components/MemberItem.vue'
const levels = [
{
key: 'departments',
title: 'Departments',
load: () => fetchDepartments(),
},
{
key: 'members',
title: 'Members',
// Custom component receives { item, active, levelKey } props
component: MemberItem,
load: (deptId) => fetchMembers(deptId),
},
]
</script><!-- MemberItem.vue -->
<script setup>
defineProps({ item: Object, active: Boolean })
</script>
<template>
<div class="member-row">
<img :src="item.avatar" class="member-avatar" />
<div>
<div>{{ item.label }}</div>
<small>{{ item.role }}</small>
</div>
</div>
</template>Async Loading with Detail
Иерархия Регион → Город → Район. Выбор района открывает карточку с полным путём и идентификаторами всех родителей.
<template>
<VxFinder :levels="geoLevels" :detail-width="280">
<template #detail="{ item, selections }">
<!-- Use selections to get parent IDs -->
<DistrictDetail
:id="item.key"
:region-id="selections[0]?.key"
:city-id="selections[1]?.key"
/>
</template>
</VxFinder>
</template>File Browser (Sync + Detail)
Выбор файла в последней колонке открывает предпросмотр с информацией о файле и полным путём к папке.
Recursive / Dynamic Depth (load prop)
Для иерархий с неизвестной глубиной — например, HR-системы, где филиалы могут быть вложены на любую глубину — используйте проп load вместо levels. Функция load получает кликнутый элемент (или null для корня) и полную цепочку выбора. Верните FinderColumnResult, чтобы добавить колонку, или null для листового узла (откроется панель деталей).
Пройдите по иерархии: Филиал → Филиал → Департамент → Сотрудник → Карточка пользователя. Филиалы могут содержать вложенные филиалы или департаменты на любой глубине.
<template>
<VxFinder :load="hrLoad" height="420px" :detail-width="300">
<template #detail="{ item, selections }">
<EmployeeCard :employee="item" :path="selections" />
</template>
</VxFinder>
</template>
<script setup>
async function hrLoad(item, selections) {
if (!item) {
// Root: fetch top-level branches
const branches = await api.getBranches()
return { key: 'branches', title: 'Branches', items: branches, filterable: true }
}
if (item.type === 'branch') {
// Branch: may contain sub-branches + departments
const children = await api.getBranchChildren(item.key)
return { key: `sub-${item.key}`, title: item.label, items: children, filterable: true }
}
if (item.type === 'department') {
// Department: show employees
const employees = await api.getEmployees(item.key)
return { key: `staff-${item.key}`, title: `${item.label} Staff`, items: employees }
}
// Employee: leaf node → opens #detail panel
return null
}
</script>Add / Edit / Delete
Включите showAdd, editable и deletable на уровнях, чтобы получить встроенные кнопки действий. Слушайте события @add, @edit, @delete, изменяйте свои данные и вызывайте ref.refresh(levelKey), чтобы перезагрузить колонку.
Нажмите + в заголовке, чтобы добавить элемент; при наведении на элемент появляется меню ... с действиями «Редактировать» и «Удалить».
<template>
<VxFinder
ref="finderRef"
:levels="levels"
@add="onAdd"
@edit="onEdit"
@delete="onDelete"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
const finderRef = ref(null)
const categories = ref([
{ key: 'cat-1', label: 'Frontend' },
{ key: 'cat-2', label: 'Backend' },
])
const items = ref({
'cat-1': [
{ key: 'item-1', label: 'Vue.js', status: 'success' },
{ key: 'item-2', label: 'React', status: 'success' },
],
'cat-2': [
{ key: 'item-3', label: 'Node.js', status: 'success' },
],
})
let nextId = 100
const levels = computed(() => [
{
key: 'categories',
title: 'Categories',
showAdd: true, // "+" button in header
editable: true, // edit action on hover menu
deletable: true, // delete action on hover menu
filterable: true,
load: () => categories.value,
},
{
key: 'items',
title: 'Items',
showAdd: true,
editable: true,
deletable: true,
filterable: true,
load: (parentKey) => items.value[parentKey] || [],
},
])
function onAdd(levelKey) {
const name = prompt(`New ${levelKey}:`)
if (!name) return
const id = `new-${++nextId}`
if (levelKey === 'categories') {
categories.value.push({ key: id, label: name })
} else {
const parentKey = finderRef.value?.selections[0]?.key
if (!parentKey) return
if (!items.value[parentKey]) items.value[parentKey] = []
items.value[parentKey].push({ key: id, label: name, status: 'success' })
}
// Refresh the column to show new data
finderRef.value?.refresh(levelKey)
}
function onEdit(levelKey, item) {
const name = prompt(`Rename "${item.label}":`, item.label)
if (!name || name === item.label) return
if (levelKey === 'categories') {
const cat = categories.value.find(c => c.key === item.key)
if (cat) cat.label = name
} else {
for (const arr of Object.values(items.value)) {
const found = arr.find(i => i.key === item.key)
if (found) { found.label = name; break }
}
}
finderRef.value?.refresh(levelKey)
}
function onDelete(levelKey, item) {
if (!confirm(`Delete "${item.label}"?`)) return
if (levelKey === 'categories') {
categories.value = categories.value.filter(c => c.key !== item.key)
delete items.value[item.key]
} else {
for (const arr of Object.values(items.value)) {
const idx = arr.findIndex(i => i.key === item.key)
if (idx !== -1) { arr.splice(idx, 1); break }
}
}
finderRef.value?.refresh(levelKey)
}
</script>Slot Mode (Manual)
Если проп levels не задан, используйте слот, чтобы вручную собрать VxFinderColumn и VxFinderItem.
<template>
<VxFinder height="250px">
<VxFinderColumn title="Fruits" :width="200">
<VxFinderItem
v-for="f in fruits"
:key="f.id"
:active="selected === f.id"
@click="selected = f.id"
>{{ f.name }}</VxFinderItem>
</VxFinderColumn>
<VxFinderColumn v-if="children.length" title="Colors">
<VxFinderItem v-for="c in children" :key="c.id" :status="c.status">
{{ c.name }}
</VxFinderItem>
</VxFinderColumn>
</VxFinder>
</template>Detail Slot API
Слот #detail принимает два пропа:
| Prop | Type | Description |
|---|---|---|
item | FinderItemData | Выбранный элемент последнего уровня (со всеми пользовательскими полями) |
selections | FinderSelection[] | Полная цепочка выбора по всем уровням |
interface FinderSelection {
levelKey: string // Ключ уровня (например, 'departments')
key: string | number // Ключ выбранного элемента
item: FinderItemData // Полные данные элемента
}
// Доступ к любому родительскому ключу:
selections[0]?.key // Выбранный ключ первого уровня
selections[1]?.key // Выбранный ключ второго уровня
// ...VxFinder Props
| Prop | Type | Default | Description |
|---|---|---|---|
| levels | FinderLevel[] | [] | Описания уровней с фиксированной глубиной. Если пусто, используется слотовый режим |
| load | (item, selections) => FinderColumnResult | null | — | Функция динамической загрузки для рекурсивной/неизвестной глубины. Верните конфиг колонки или null для листа |
| height | string | '100%' | Высота контейнера |
| gap | number | 6 | Промежуток между колонками |
| collapsible | boolean | true | Разрешить сворачивать колонки |
| resizable | boolean | true | Разрешить изменять размер колонок |
| minWidth | number | 150 | Минимальная ширина колонки |
| maxWidth | number | 500 | Максимальная ширина колонки |
| detailWidth | number | string | 360 | Ширина панели деталей |
VxFinder Events
| Event | Payload | Description |
|---|---|---|
| select | (levelKey, item) | Выбран элемент |
| add | (levelKey) | Нажата кнопка добавления |
| edit | (levelKey, item) | Действие редактирования элемента |
| delete | (levelKey, item) | Действие удаления элемента |
VxFinder Slots
| Slot | Props | Description |
|---|---|---|
| default | — | Содержимое ручного режима |
| item | { item, active, levelKey } | Пользовательский шаблон для элементов всех уровней |
| item- | { item, active, levelKey } | Пользовательский шаблон для элементов конкретного уровня |
| detail | { item, selections } | Панель, показываемая при выборе элемента последнего уровня |
VxFinder Exposed
| Name | Type | Description |
|---|---|---|
refresh(key?) | method | Перезагрузить одну или все колонки |
selections | FinderSelection[] | Текущая цепочка выбора |
VxFinderColumn Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | '' | Заголовок колонки |
| collapsible | boolean | true | Разрешить сворачивание |
| collapsed | boolean | false | Состояние свёрнутости (v-model) |
| resizable | boolean | true | Разрешить изменение размера |
| width | number | 260 | Начальная ширина (v-model) |
| minWidth | number | 150 | Минимальная ширина |
| maxWidth | number | 500 | Максимальная ширина |
| showAdd | boolean | false | Показывать кнопку добавления |
| loading | boolean | false | Показывать спиннер |
| filterable | boolean | false | Показать поле поиска вверху колонки |
| filterPlaceholder | string | 'Search...' | Плейсхолдер для поля фильтрации |
VxFinderItem Props
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | false | Состояние «выбрано» |
| disabled | boolean | false | Состояние «отключено» |
| status | 'default' | 'success' | 'warning' | 'danger' | — | Индикатор статуса |
| editable | boolean | false | Показывать действие редактирования |
| deletable | boolean | false | Показывать действие удаления |