/
45case
/
ptable
Обзор
Документация
Войти
/
45case
/
ptable
Код
Запросы
3
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev
src/app/AppNavbar.vue
228 строк
8 KB
Olya
Характеристики атомной массы, электронных оболочек, электроотрицательности и температуры плавления
21 июл 2026, 18:20
21 июл 2026, 18:20
f69abd2
Код
Авторство
О чём код?
<script setup lang="ts"> import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { getElementToneStyle } from "@/entities/element/lib/presentation"; import type { PeriodicElement } from "@/entities/element/model/elements"; import { searchAll, type SearchResult } from "@/shared/lib/search"; import { useTableView } from "@/shared/lib/useTableView"; import SearchIcon from "@/shared/ui/icons/SearchIcon.vue"; import NavLink from "@/shared/ui/NavLink.vue"; import ThemeSwitchButton from "@/shared/ui/ThemeSwitchButton.vue"; const props = defineProps<{ searchQuery: string }>(); const emit = defineEmits<{ "update:searchQuery": [value: string] }>(); const router = useRouter(); const route = useRoute(); const { isTableExtended, toggleTableExtended } = useTableView(); const inputRef = ref<HTMLInputElement | null>(null); const containerRef = ref<HTMLElement | null>(null); const isFocused = ref(false); const results = ref<SearchResult[]>([]); const selectedIndex = ref(-1); const isSearching = ref(false); const currentSlug = computed(() => route.query.slug as string | undefined); const links = computed(() => [ { to: "/", label: "Свойства" }, { to: { path: "/electrons", query: { slug: currentSlug.value } }, label: "Электроны" }, { to: { path: "/isotopes", query: { slug: currentSlug.value } }, label: "Изотопы" }, { to: { path: "/compounds", query: { slug: currentSlug.value } }, label: "Соединения" }, ]); const showInput = computed(() => isFocused.value || props.searchQuery.length > 0); let debounceTimer: ReturnType<typeof setTimeout> | null = null; watch( () => props.searchQuery, (val) => { if (debounceTimer) clearTimeout(debounceTimer); if (!val.trim()) { results.value = []; selectedIndex.value = -1; return; } isSearching.value = true; debounceTimer = setTimeout(async () => { results.value = await searchAll(val); selectedIndex.value = -1; isSearching.value = false; }, 150); }, ); function onInput(e: Event) { emit("update:searchQuery", (e.target as HTMLInputElement).value); } function activateSearch() { isFocused.value = true; nextTick(() => inputRef.value?.focus()); } function closeSearch() { emit("update:searchQuery", ""); results.value = []; selectedIndex.value = -1; isFocused.value = false; inputRef.value?.blur(); } function selectElement(element: PeriodicElement) { closeSearch(); router.push({ path: "/", query: { slug: element.slug } }); } function onKeydown(e: KeyboardEvent) { if (e.key === "Escape") { closeSearch(); return; } if (e.key === "ArrowDown") { e.preventDefault(); selectedIndex.value = Math.min(selectedIndex.value + 1, results.value.length - 1); return; } if (e.key === "ArrowUp") { e.preventDefault(); selectedIndex.value = Math.max(selectedIndex.value - 1, -1); return; } if (e.key === "Enter" && selectedIndex.value >= 0) { const result = results.value[selectedIndex.value]; if (result) selectElement(result.element); return; } } function onDocumentMousedown(e: MouseEvent) { if (containerRef.value && !containerRef.value.contains(e.target as Node)) { closeSearch(); } } onMounted(() => { document.addEventListener("mousedown", onDocumentMousedown); }); onBeforeUnmount(() => { if (debounceTimer) clearTimeout(debounceTimer); document.removeEventListener("mousedown", onDocumentMousedown); }); </script> <template> <header class="sticky top-0 z-50 px-5 pt-4"> <div class="mx-auto flex w-full max-w-350 flex-wrap items-center justify-center gap-4 rounded-3xl border border-(--color-border) bg-[color-mix(in_srgb,var(--color-surface)_90%,transparent)] px-4 py-3 shadow-[0_10px_30px_var(--color-shadow)] backdrop-blur-xl sm:justify-between sm:px-5" > <RouterLink class="font-(family-name:--font-heading) text-2xl font-semibold tracking-[-0.04em] no-underline" to="/" > Периодическая таблица </RouterLink> <nav aria-label="Основная навигация" class="order-1 flex w-full flex-wrap justify-center gap-2 sm:order-0 sm:w-auto sm:flex-1" > <NavLink v-for="link in links" :key="link.label" :to="link.to"> {{ link.label }} </NavLink> </nav> <div class="flex items-center gap-2"> <button v-if="route.path === '/' || route.path === '/electrons'" type="button" :aria-pressed="isTableExtended" :title=" isTableExtended ? 'Свернуть: убрать лантаноиды и актиноиды вниз' : 'Широкий: показать лантаноиды и актиноиды в 6 и 7 периодах' " class="inline-flex min-h-10 items-center justify-center rounded-full border px-3.5 text-sm font-semibold no-underline transition-[color,background-color,border-color] duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--color-focus-ring)" :class=" isTableExtended ? 'border-(--color-border) bg-(--color-surface-muted) text-(--color-text-primary)' : 'border-transparent text-(--color-text-secondary) hover:border-(--color-border) hover:bg-(--color-hover-surface) hover:text-(--color-text-primary)' " @click="toggleTableExtended" > Широкий </button> <div ref="containerRef" class="relative"> <div class="flex items-center gap-1 rounded-lg transition-colors" :class="showInput ? 'bg-(--color-hover-surface) px-2' : ''" > <button type="button" aria-label="Поиск" class="flex h-8 w-8 items-center justify-center rounded-lg text-(--color-text-secondary) transition-colors hover:text-(--color-text-primary)" @click="activateSearch" > <SearchIcon class="h-5 w-5" /> </button> <input v-if="showInput" ref="inputRef" :value="props.searchQuery" type="text" placeholder="Поиск элемента…" class="w-48 bg-transparent py-1.5 text-sm text-(--color-text-primary) outline-none placeholder:text-(--color-text-secondary)" @input="onInput" @keydown="onKeydown" /> </div> <div v-if="results.length > 0" class="absolute right-0 z-50 mt-2 w-80 overflow-hidden rounded-xl border border-(--color-border) bg-(--color-surface-elevated) shadow-lg" > <ul class="max-h-80 overflow-y-auto py-1"> <li v-for="(result, index) in results" :key="result.element.atomicNumber" class="flex cursor-pointer items-center gap-3 px-3 py-2 transition-colors" :class="index === selectedIndex ? 'bg-(--color-hover-surface)' : 'hover:bg-(--color-hover-surface)'" @click="selectElement(result.element)" @mouseenter="selectedIndex = index" > <span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-sm font-bold leading-none" :style="{ background: getElementToneStyle(result.element)['--element-tone-surface'], color: getElementToneStyle(result.element)['--element-tone-accent'], border: '1px solid ' + getElementToneStyle(result.element)['--element-tone-border-strong'], }" > {{ result.element.symbol }} </span> <div class="min-w-0"> <p class="truncate text-sm font-medium text-(--color-text-primary)"> {{ result.element.name }} </p> <p class="truncate text-xs text-(--color-text-secondary)"> {{ result.element.nameEn }} · №{{ result.element.atomicNumber }} </p> </div> </li> </ul> </div> </div> <ThemeSwitchButton /> </div> </div> </header> </template>