Files
portal/app/resources/js/components/layout/CommandPalette.vue
T

114 lines
3.9 KiB
Vue

<script setup lang="ts">
/**
* Минимальная command-palette (audit B3). Открывается по ⌘K / Ctrl+K, кликом
* на плашку в AppSidebar или кнопку «Поиск» в AppTopbar. Список — навигация
* по 8 разделам портала; фильтр по подстроке; Enter → первый результат.
* Монтируется один раз в AppLayout.
*/
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useCommandPalette } from '../../composables/useCommandPalette';
interface PaletteItem {
title: string;
icon: string;
to: string;
}
const NAV_ITEMS: PaletteItem[] = [
{ title: 'Проекты', icon: 'mdi-folder-multiple-outline', to: '/projects' },
{ title: 'Сделки', icon: 'mdi-format-list-bulleted', to: '/deals' },
{ title: 'Канбан', icon: 'mdi-view-column-outline', to: '/kanban' },
{ title: 'Дашборд', icon: 'mdi-view-dashboard-outline', to: '/dashboard' },
{ title: 'Импорт данных', icon: 'mdi-database-import-outline', to: '/import' },
{ title: 'Биллинг', icon: 'mdi-credit-card-outline', to: '/billing' },
{ title: 'Отчёты', icon: 'mdi-chart-box-outline', to: '/reports' },
{ title: 'Настройки', icon: 'mdi-cog-outline', to: '/settings' },
];
const { open, closePalette } = useCommandPalette();
const router = useRouter();
const query = ref('');
const filteredItems = computed<PaletteItem[]>(() => {
const q = query.value.trim().toLowerCase();
if (q === '') return NAV_ITEMS;
return NAV_ITEMS.filter((i) => i.title.toLowerCase().includes(q));
});
// Сброс query при каждом открытии.
watch(open, (isOpen) => {
if (isOpen) query.value = '';
});
function selectItem(item: PaletteItem): void {
closePalette();
void router.push(item.to);
}
function onSubmit(): void {
const first = filteredItems.value[0];
if (first) selectItem(first);
}
function onGlobalKeydown(e: KeyboardEvent): void {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
if (open.value) return;
e.preventDefault();
open.value = true;
}
}
onMounted(() => window.addEventListener('keydown', onGlobalKeydown));
onUnmounted(() => window.removeEventListener('keydown', onGlobalKeydown));
defineExpose({ query, filteredItems, selectItem, onSubmit });
</script>
<template>
<v-dialog v-model="open" :max-width="520" data-testid="command-palette">
<v-card class="cmdk-card">
<v-text-field
v-model="query"
autofocus
placeholder="Поиск разделов…"
variant="plain"
density="comfortable"
hide-details
prepend-inner-icon="mdi-magnify"
class="cmdk-input px-3 pt-2"
data-testid="command-palette-input"
@keydown.enter="onSubmit"
/>
<v-divider />
<v-list density="compact" class="cmdk-list" data-testid="command-palette-list">
<v-list-item
v-for="item in filteredItems"
:key="item.to"
:prepend-icon="item.icon"
:title="item.title"
data-testid="command-palette-item"
@click="selectItem(item)"
/>
<v-list-item
v-if="filteredItems.length === 0"
class="text-medium-emphasis"
title="Ничего не найдено"
data-testid="command-palette-empty"
/>
</v-list>
</v-card>
</v-dialog>
</template>
<style scoped>
.cmdk-card {
overflow: hidden;
}
.cmdk-list {
max-height: 320px;
overflow-y: auto;
}
</style>