Files
portal/app/resources/js/views/errors/ErrorView.vue
T

181 lines
6.2 KiB
Vue
Raw Normal View History

<script setup lang="ts">
/**
* Универсальный экран ошибки для 404 / 403 / 500. Конфигурация через
* `route.meta.errorCode` ('404' | '403' | '500').
*
* Источник дизайна: liderra_v8_handoff/concepts/v8_errors.html.
*
* Layout: meta.layout='error' — AppShell рендерит ErrorView напрямую без
* AppLayout/AuthLayout (full-bleed теало-нуар bg + центрированный card).
*
* Sprint 4 Phase B/3 — split на ErrorBrand + ErrorIllustration + ErrorActions
* + ErrorMeta (audit O-refactor-04 закрытие). State (config, errorCode) — в parent
* ради единого route.meta-driven flow.
*
* Не входит в этот коммит:
* - Реальный requestId/incidentId из backend (сейчас mock).
* - Status-pills из API /api/health (сейчас static на 500).
* - Sentry capture при mount 500 (для production).
*/
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import ErrorBrand from '../../components/errors/ErrorBrand.vue';
import ErrorIllustration from '../../components/errors/ErrorIllustration.vue';
import ErrorActions from '../../components/errors/ErrorActions.vue';
import ErrorMeta from '../../components/errors/ErrorMeta.vue';
import DevIndexBadge from '../../components/DevIndexBadge.vue';
interface ErrorAction {
label: string;
icon: string;
to?: string;
href?: string;
onClick?: () => void;
}
interface ErrorConfig {
code: '404' | '403' | '500';
title: string;
description: string;
primaryAction: ErrorAction;
secondaryAction?: ErrorAction;
showRequestId?: boolean;
requestIdLabel?: string;
requestId?: string;
showStatusList?: boolean;
}
const route = useRoute();
const router = useRouter();
const errorCode = computed<'404' | '403' | '500'>(
() => (route.meta.errorCode as '404' | '403' | '500' | undefined) ?? '404',
);
const config = computed<ErrorConfig>(() => {
if (errorCode.value === '403') {
return {
code: '403',
title: 'У вас нет доступа',
description:
'Эта страница принадлежит другому тенанту, либо ваша роль не позволяет её увидеть. Если вы считаете, что это ошибка — обратитесь к администратору вашей команды или в поддержку.',
primaryAction: {
label: 'На дашборд',
icon: 'mdi-view-dashboard-outline',
to: '/dashboard',
},
secondaryAction: {
label: 'Написать в поддержку',
icon: 'mdi-email-outline',
href: 'mailto:support@liderra.app',
},
showRequestId: true,
requestIdLabel: 'Запрос',
requestId: 'REQ-3F8A2-0007',
};
}
if (errorCode.value === '500') {
return {
code: '500',
title: 'Что-то пошло не так',
description:
'Внутренняя ошибка — мы уже занимаемся. Команда получила уведомление. Большинство сбоев чинятся за 5–10 минут. Можно вернуться через минуту, или открыть страницу статуса.',
primaryAction: {
label: 'Попробовать снова',
icon: 'mdi-refresh',
onClick: () => location.reload(),
},
secondaryAction: {
label: 'Статус сервиса',
icon: 'mdi-pulse',
href: 'https://status.liderra.app',
},
showRequestId: true,
requestIdLabel: 'Инцидент',
requestId: 'INC-2026-0507-0034',
showStatusList: true,
};
}
// default 404
return {
code: '404',
title: 'Страница не найдена',
description:
'Похоже, такой страницы нет — её удалили, переименовали или вы ввели адрес с опечаткой. Все рабочие экраны Лидерра доступны через дашборд.',
primaryAction: {
label: 'На дашборд',
icon: 'mdi-view-dashboard-outline',
to: '/dashboard',
},
secondaryAction: {
label: 'Назад',
icon: 'mdi-arrow-left',
onClick: () => router.back(),
},
};
});
</script>
<template>
<v-app>
<v-main class="error-main">
<ErrorBrand />
<div class="error-content">
<ErrorIllustration :code="config.code" />
<h2 class="err-title">{{ config.title }}</h2>
<p class="err-desc">{{ config.description }}</p>
<ErrorActions :primary="config.primaryAction" :secondary="config.secondaryAction" />
<ErrorMeta
:code="config.code"
:show-status-list="config.showStatusList"
:show-request-id="config.showRequestId"
:request-id-label="config.requestIdLabel"
:request-id="config.requestId"
/>
</div>
</v-main>
<DevIndexBadge :index="route.meta.devIndex" :label="route.meta.devLabel" />
</v-app>
</template>
<style scoped>
.error-main {
background: #012019;
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.error-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px 32px 80px;
text-align: center;
max-width: 560px;
margin: 0 auto;
}
.err-title {
font-size: 28px;
font-weight: 600;
font-variation-settings: 'opsz' 28;
letter-spacing: -0.018em;
margin: 0 0 12px;
color: #fff;
}
.err-desc {
color: #b1c2bd;
line-height: 1.55;
font-size: 15px;
margin: 0 0 24px;
}
</style>