Перевод защищённой части приложения на Vue.js + Inertia
Дашборд, зоны, устройства, правила автоматизации и профиль теперь Vue 3 + Inertia вместо Blade+Alpine — гостевые страницы Breeze (логин/регистрация) сознательно оставлены на Blade, чтобы не трогать уже протестированную аутентификацию. Контроллеры возвращают Inertia::render() вместо view(), переиспользуют существующие API Resources и FormRequest. Ключевой нюанс: Resource/ResourceCollection, переданные напрямую в проп Inertia, заворачиваются в ключ "data" (Inertia вызывает toResponse() у Responsable-объектов) — везде используется ->resolve() и явное резолвление вложенных ресурсов (zone/device_type внутри Device и т.п.), иначе вложенные поля тоже задваивались бы обёрткой. Alpine.js и все Blade-вьюхи защищённой части удалены как мёртвый код. Тесты (ZoneControllerTest/DeviceControllerTest) переведены на assertInertia(). UserFactory теперь явно задаёт role=owner — без этого Eloquent не подтягивает DB-default обратно в модель после create(), что уронило бы общий auth.user проп на любой странице.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Link, router, usePage } from '@inertiajs/vue3';
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
import NavLink from '@/Components/NavLink.vue';
|
||||
import { routes } from '@/routes';
|
||||
|
||||
const page = usePage();
|
||||
const user = computed(() => page.props.auth.user);
|
||||
const flash = computed(() => page.props.flash);
|
||||
|
||||
const currentPath = computed(() => new URL(page.url, window.location.origin).pathname);
|
||||
const isActive = (prefix) => currentPath.value === prefix || currentPath.value.startsWith(`${prefix}/`);
|
||||
|
||||
const showingMobileMenu = ref(false);
|
||||
const showingUserDropdown = ref(false);
|
||||
|
||||
function logout() {
|
||||
router.post(routes.logout);
|
||||
}
|
||||
|
||||
const navLinks = [
|
||||
{ href: routes.dashboard, label: 'Dashboard', prefix: routes.dashboard },
|
||||
{ href: routes.zones.index, label: 'Зоны', prefix: routes.zones.index },
|
||||
{ href: routes.devices.index, label: 'Устройства', prefix: routes.devices.index },
|
||||
{ href: routes.automationRules.index, label: 'Правила', prefix: routes.automationRules.index },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<nav class="bg-white border-b border-gray-100">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<div class="shrink-0 flex items-center">
|
||||
<Link :href="routes.dashboard">
|
||||
<ApplicationLogo class="block h-9 w-auto fill-current text-gray-800" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<NavLink
|
||||
v-for="link in navLinks"
|
||||
:key="link.href"
|
||||
:href="link.href"
|
||||
:active="isActive(link.prefix)"
|
||||
>
|
||||
{{ link.label }}
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
|
||||
@click="showingUserDropdown = !showingUserDropdown"
|
||||
>
|
||||
<div>{{ user?.name }}</div>
|
||||
<svg class="ms-1 fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-show="showingUserDropdown"
|
||||
class="absolute end-0 z-50 mt-2 w-48 rounded-md shadow-lg"
|
||||
@click="showingUserDropdown = false"
|
||||
>
|
||||
<div class="rounded-md ring-1 ring-black ring-opacity-5 py-1 bg-white">
|
||||
<Link :href="routes.profile.edit" class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100">
|
||||
Профиль
|
||||
</Link>
|
||||
<button type="button" class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100" @click="logout">
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none transition duration-150 ease-in-out"
|
||||
@click="showingMobileMenu = !showingMobileMenu"
|
||||
>
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path v-if="!showingMobileMenu" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showingMobileMenu" class="sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<NavLink
|
||||
v-for="link in navLinks"
|
||||
:key="link.href"
|
||||
:href="link.href"
|
||||
:active="isActive(link.prefix)"
|
||||
mobile
|
||||
>
|
||||
{{ link.label }}
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="px-4">
|
||||
<div class="font-medium text-base text-gray-800">{{ user?.name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ user?.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<NavLink :href="routes.profile.edit" mobile>Профиль</NavLink>
|
||||
<button type="button" class="block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300" @click="logout">
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header v-if="$slots.header" class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div v-if="flash.status || flash.error" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-4">
|
||||
<div v-if="flash.status" class="rounded-md bg-green-50 p-4 text-sm text-green-700">{{ flash.status }}</div>
|
||||
<div v-if="flash.error" class="rounded-md bg-red-50 p-4 text-sm text-red-700">{{ flash.error }}</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user