114 lines
3.9 KiB
Vue
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>
|