Files
antigol-service/frontend/layouts/default.vue
talorr cda36918e8 init
2026-03-27 03:36:08 +03:00

222 lines
7.6 KiB
Vue

<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>