feat: persistent Zabbix-style sidebar on all pages, rename Dashboard to Обзор

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 13:54:41 +10:00
parent dce5b88b2f
commit ac3c20b7db
4 changed files with 180 additions and 90 deletions
+15 -2
View File
@@ -1,5 +1,18 @@
<template>
<div class="layout">
<RouterView />
<div :class="showShell ? 'app-shell' : 'layout layout-login'">
<AppSidebar v-if="showShell" />
<main :class="showShell ? 'app-main' : ''">
<RouterView />
</main>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useRoute } from "vue-router";
import { getToken } from "./api";
import AppSidebar from "./components/AppSidebar.vue";
const route = useRoute();
const showShell = computed(() => route.path !== "/login" && !!getToken());
</script>
+149
View File
@@ -0,0 +1,149 @@
<template>
<aside class="sac-sidebar">
<div class="sac-sidebar-brand">{{ appTitle }}</div>
<nav class="sac-sidebar-nav" aria-label="Основная навигация">
<RouterLink
v-for="item in navItems"
:key="item.to"
:to="item.to"
class="sac-nav-item"
:class="{ 'is-active': isActive(item) }"
>
<span class="sac-nav-icon" aria-hidden="true">{{ item.icon }}</span>
<span class="sac-nav-label">{{ item.label }}</span>
</RouterLink>
</nav>
<div class="sac-sidebar-footer">
<button type="button" class="sac-nav-item sac-nav-logout" @click="logout">
<span class="sac-nav-icon" aria-hidden="true"></span>
<span class="sac-nav-label">Выход</span>
</button>
</div>
</aside>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { clearToken } from "../api";
import { APP_VERSION_LABEL } from "../version";
const appTitle = APP_VERSION_LABEL;
const route = useRoute();
const router = useRouter();
const navItems = [
{ to: "/dashboard", label: "Обзор", icon: "▦", match: "exact" as const },
{ to: "/events", label: "События", icon: "☰", match: "prefix" as const },
{ to: "/reports", label: "Отчёты", icon: "▤", match: "prefix" as const },
{ to: "/problems", label: "Проблемы", icon: "!", match: "prefix" as const },
{ to: "/hosts", label: "Хосты", icon: "⬢", match: "exact" as const },
];
function isActive(item: (typeof navItems)[number]) {
if (item.match === "exact") {
return route.path === item.to;
}
return route.path === item.to || route.path.startsWith(`${item.to}/`);
}
function logout() {
clearToken();
router.push("/login");
}
</script>
<style scoped>
.sac-sidebar {
position: sticky;
top: 0;
align-self: flex-start;
flex-shrink: 0;
display: flex;
flex-direction: column;
width: max-content;
min-width: 12.5rem;
height: 100vh;
max-height: 100vh;
background: #15202b;
border-right: 1px solid #2a3441;
padding: 0.75rem 0.5rem;
}
.sac-sidebar-brand {
font-weight: 700;
color: #fff;
font-size: 0.82rem;
line-height: 1.35;
padding: 0.5rem 0.75rem 0.85rem;
white-space: nowrap;
border-bottom: 1px solid #2a3441;
margin-bottom: 0.5rem;
}
.sac-sidebar-nav {
display: flex;
flex-direction: column;
gap: 0.15rem;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.sac-sidebar-footer {
flex-shrink: 0;
padding-top: 0.75rem;
margin-top: auto;
border-top: 1px solid #2a3441;
}
.sac-nav-item {
display: flex;
align-items: center;
gap: 0.55rem;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
border-radius: 4px;
background: transparent;
color: #c5d0dc;
text-decoration: none;
font: inherit;
text-align: left;
cursor: pointer;
border-left: 3px solid transparent;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.sac-nav-item:hover {
background: #1e2d3d;
color: #fff;
}
.sac-nav-item.is-active {
background: #1a3a52;
color: #fff;
border-left-color: #3db8ff;
}
.sac-nav-icon {
width: 1.1rem;
flex-shrink: 0;
text-align: center;
font-size: 0.95rem;
opacity: 0.9;
}
.sac-nav-label {
white-space: nowrap;
}
.sac-nav-logout {
color: #9aa4b2;
}
.sac-nav-logout:hover {
color: #ff8a8a;
background: #2a1f24;
border-left-color: transparent;
}
</style>
+13 -13
View File
@@ -24,22 +24,22 @@ a {
padding: 1rem 1rem 2rem;
}
nav {
.app-shell {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #2a3441;
width: 100%;
min-height: 100vh;
margin: 0;
padding: 0;
}
nav .brand {
font-weight: 700;
color: #fff;
margin-right: auto;
font-size: 0.95rem;
line-height: 1.3;
max-width: min(420px, 45vw);
.app-main {
flex: 1;
min-width: 0;
padding: 1rem 1.25rem 2rem;
}
.layout-login {
min-height: 100vh;
}
table {
+3 -75
View File
@@ -1,16 +1,6 @@
<template>
<div class="dashboard-layout">
<aside class="dashboard-sidebar">
<div class="dashboard-sidebar-title">{{ appTitle }}</div>
<RouterLink to="/dashboard">Dashboard</RouterLink>
<RouterLink to="/events">События</RouterLink>
<RouterLink to="/reports">Отчёты</RouterLink>
<RouterLink to="/problems">Проблемы</RouterLink>
<RouterLink to="/hosts">Хосты</RouterLink>
<button type="button" class="secondary" @click="logout">Выход</button>
</aside>
<section class="dashboard-content">
<h1>Dashboard</h1>
<div class="overview-page">
<h1>Обзор</h1>
<p v-if="error" class="error">{{ error }}</p>
@@ -293,7 +283,6 @@
</div>
</template>
</section>
</div>
</template>
@@ -302,10 +291,8 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
import { apiFetch, clearToken, getToken, type DashboardSummary, type EventSummary } from "../api";
import { APP_VERSION_LABEL } from "../version";
import { apiFetch, getToken, type DashboardSummary, type EventSummary } from "../api";
@@ -322,15 +309,6 @@ const lastKnownEventDbId = ref<number | null>(null);
let eventSource: EventSource | null = null;
let refreshingRecent = false;
const router = useRouter();
const appTitle = APP_VERSION_LABEL;
function logout() {
clearToken();
router.push("/login");
}
function formatDt(iso: string) {
@@ -570,56 +548,6 @@ onUnmounted(() => {
<style scoped>
.dashboard-layout {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 1rem;
}
.dashboard-sidebar {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: #1a2332;
border: 1px solid #2a3441;
border-radius: 8px;
padding: 0.9rem;
align-self: start;
width: max-content;
}
.dashboard-sidebar-title {
font-weight: 700;
color: #fff;
margin-bottom: 0.5rem;
white-space: nowrap;
}
.dashboard-content {
min-width: 0;
}
.dash-table-scroll {
max-height: 21rem;