init
This commit is contained in:
221
frontend/layouts/default.vue
Normal file
221
frontend/layouts/default.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import Avatar from "primevue/avatar";
|
||||
import Button from "primevue/button";
|
||||
|
||||
const route = useRoute();
|
||||
const { user, logout } = useAuth();
|
||||
const { theme, toggleTheme, initializeTheme } = useTheme();
|
||||
const { unreadCount, initializeUnread, clearState } = useSupportUnread();
|
||||
|
||||
const isGuest = computed(() => !user.value);
|
||||
|
||||
const navigationItems = computed(() => {
|
||||
if (!user.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ label: "Боты", to: "/", icon: "pi pi-th-large", badge: null },
|
||||
{
|
||||
label: user.value.role === "admin" ? "Чаты" : "Чат",
|
||||
to: "/chat",
|
||||
icon: "pi pi-comments",
|
||||
badge: unreadCount.value > 0 ? unreadCount.value : null
|
||||
},
|
||||
{ label: "Настройки", to: "/settings", icon: "pi pi-sliders-h", badge: null }
|
||||
];
|
||||
|
||||
if (user.value.role === "admin") {
|
||||
items.push({ label: "Админка", to: "/admin", icon: "pi pi-shield", badge: null });
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
const userRoleLabel = computed(() => (user.value?.role === "admin" ? "Администратор" : "Пользователь"));
|
||||
const userInitial = computed(() => user.value?.email?.charAt(0)?.toUpperCase() ?? "U");
|
||||
|
||||
const isRouteActive = (path: string) => {
|
||||
if (path === "/") return route.path === "/";
|
||||
return route.path.startsWith(path);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeTheme();
|
||||
void initializeUnread();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => user.value?.id,
|
||||
() => {
|
||||
if (user.value) {
|
||||
void initializeUnread();
|
||||
return;
|
||||
}
|
||||
|
||||
clearState();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen"
|
||||
:class="[
|
||||
isGuest ? 'mx-auto w-full max-w-7xl' : 'md:grid md:grid-cols-[18rem_minmax(0,1fr)]',
|
||||
'bg-(--bg)'
|
||||
]"
|
||||
:style="{ background: 'linear-gradient(180deg, color-mix(in srgb, var(--accent) 6%, transparent), transparent 24%), var(--bg)' }"
|
||||
>
|
||||
<aside
|
||||
v-if="user"
|
||||
class="hidden h-screen border-r px-5 py-6 md:sticky md:top-0 md:flex md:flex-col md:gap-6"
|
||||
:style="{
|
||||
borderColor: 'var(--border)',
|
||||
backgroundColor: 'color-mix(in srgb, var(--surface) 90%, white 10%)'
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="grid h-12 w-12 place-items-center overflow-hidden rounded-2xl"
|
||||
:style="{
|
||||
backgroundColor: 'color-mix(in srgb, var(--accent) 12%, var(--surface-soft))',
|
||||
color: 'var(--accent-strong)'
|
||||
}"
|
||||
>
|
||||
<AppLogo size="32px" />
|
||||
</div>
|
||||
<div>
|
||||
<strong class="block text-lg">Антигол</strong>
|
||||
<p class="m-0 text-sm text-(--muted)">Рабочее пространство сигналов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="grid gap-2">
|
||||
<NuxtLink
|
||||
v-for="item in navigationItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="inline-flex items-center gap-3 rounded-2xl px-3 py-3 text-sm font-medium transition"
|
||||
:class="isRouteActive(item.to) ? 'text-(--text)' : 'text-(--muted)'"
|
||||
:style="isRouteActive(item.to) ? { backgroundColor: 'color-mix(in srgb, var(--accent) 10%, var(--surface-soft))' } : {}"
|
||||
>
|
||||
<i :class="item.icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
<span
|
||||
v-if="item.badge"
|
||||
class="signal-row__badge signal-row__badge--win"
|
||||
style=" margin: auto; margin-right: 0; min-height: 1.5rem; padding: 0.2rem 0.55rem;"
|
||||
>
|
||||
{{ item.badge }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<div class="mt-auto">
|
||||
<div class="grid gap-1 rounded-2xl border p-4" :style="{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }">
|
||||
<span class="text-sm text-(--muted)">Доступы</span>
|
||||
<strong>{{ user.botAccesses?.length ?? 0 }} ботов</strong>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="min-h-screen">
|
||||
<header
|
||||
class="flex flex-col gap-4 px-4 py-4 md:flex-row md:justify-between md:px-6 md:py-6"
|
||||
:class="isGuest ? 'md:mx-auto md:w-full md:max-w-7xl md:items-start' : 'md:items-center'"
|
||||
:style="{
|
||||
paddingTop: 'calc(1rem + env(safe-area-inset-top, 0px))'
|
||||
}"
|
||||
>
|
||||
<div class="grid gap-1">
|
||||
<h1 class="m-0 text-2xl font-semibold">{{ user ? "Панель сигналов" : "Антигол" }}</h1>
|
||||
<p class="m-0 text-sm text-(--muted)">
|
||||
{{ user ? "Рабочее пространство по ботам, сигналам и поддержке" : "Авторизуйтесь для доступа к системе" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
class="rounded"
|
||||
text
|
||||
severity="secondary"
|
||||
:icon="theme === 'dark' ? 'pi pi-sun' : 'pi pi-moon'"
|
||||
:aria-label="theme === 'dark' ? 'Светлая тема' : 'Тёмная тема'"
|
||||
@click="toggleTheme"
|
||||
/>
|
||||
|
||||
<template v-if="user">
|
||||
<div class="inline-flex items-center gap-3 rounded-2xl border px-3 py-2" :style="{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }">
|
||||
<Avatar :label="userInitial" shape="circle" />
|
||||
<div>
|
||||
<strong class="block">{{ user.email }}</strong>
|
||||
<span class="block text-xs text-(--muted)">{{ userRoleLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button label="Выйти" icon="pi pi-sign-out" severity="secondary" outlined @click="logout" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<NuxtLink to="/login">
|
||||
<Button label="Войти" severity="secondary" outlined />
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/register">
|
||||
<Button label="Регистрация" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main
|
||||
class="px-4 pb-24 md:px-6 md:pb-8"
|
||||
:class="isGuest ? 'md:mx-auto md:w-full md:max-w-7xl' : ''"
|
||||
:style="{
|
||||
paddingBottom: 'calc(6rem + env(safe-area-inset-bottom, 0px))'
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<nav
|
||||
v-if="user"
|
||||
aria-label="Нижняя навигация"
|
||||
class="fixed inset-x-4 bottom-4 z-40 flex items-center justify-around rounded-[22px] border px-2 py-2 backdrop-blur-[18px] md:hidden"
|
||||
:style="{
|
||||
borderColor: 'color-mix(in srgb, var(--border) 85%, transparent)',
|
||||
backgroundColor: 'color-mix(in srgb, var(--surface) 88%, transparent)',
|
||||
boxShadow: '0 12px 32px color-mix(in srgb, var(--text) 12%, transparent)',
|
||||
bottom: 'calc(1rem + env(safe-area-inset-bottom, 0px))'
|
||||
}"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="item in navigationItems"
|
||||
:key="`bottom-${item.to}`"
|
||||
:to="item.to"
|
||||
class="flex min-w-0 flex-1 flex-col items-center gap-1 rounded-2xl px-2 py-2 text-center text-[0.7rem] font-medium transition"
|
||||
:class="isRouteActive(item.to) ? 'text-(--text)' : 'text-(--muted)'"
|
||||
:style="isRouteActive(item.to) ? { backgroundColor: 'color-mix(in srgb, var(--accent) 12%, var(--surface-soft))' } : {}"
|
||||
>
|
||||
<span class="mobile-nav__icon-wrap">
|
||||
<i :class="[item.icon, 'text-base']" />
|
||||
<span
|
||||
v-if="item.badge"
|
||||
class="mobile-nav__badge"
|
||||
>
|
||||
{{ item.badge }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ item.label }}</span>
|
||||
<span
|
||||
v-if="item.badge"
|
||||
class="signal-row__badge signal-row__badge--win"
|
||||
style="display: none;"
|
||||
>
|
||||
{{ item.badge }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user