2026-05-08 19:11:09 +03:00
< 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).
*
2026-05-10 04:53:09 +03:00
* Sprint 4 Phase B/3 — split на ErrorBrand + ErrorIllustration + ErrorActions
* + ErrorMeta (audit O-refactor-04 закрытие). State (config, errorCode) — в parent
* ради единого route.meta-driven flow.
*
2026-05-08 19:11:09 +03:00
* Не входит в этот коммит:
* - Реальный 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' ;
2026-05-10 04:53:09 +03:00
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' ;
2026-05-12 04:45:18 +03:00
import DevIndexBadge from '../../components/DevIndexBadge.vue' ;
2026-05-10 04:53:09 +03:00
interface ErrorAction {
label : string ;
icon : string ;
to ? : string ;
href ? : string ;
onClick ? : ( ) => void ;
}
2026-05-08 19:11:09 +03:00
interface ErrorConfig {
code : '404' | '403' | '500' ;
title : string ;
description : string ;
2026-05-10 04:53:09 +03:00
primaryAction : ErrorAction ;
secondaryAction ? : ErrorAction ;
2026-05-08 19:11:09 +03:00
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" >
2026-05-10 04:53:09 +03:00
< ErrorBrand / >
2026-05-08 19:11:09 +03:00
< div class = "error-content" >
2026-05-10 04:53:09 +03:00
< ErrorIllustration :code = "config.code" / >
2026-05-08 19:11:09 +03:00
< h2 class = "err-title" > { { config . title } } < / h2 >
< p class = "err-desc" > { { config . description } } < / p >
2026-05-10 04:53:09 +03:00
< ErrorActions :primary = "config.primaryAction" :secondary = "config.secondaryAction" / >
2026-05-08 19:11:09 +03:00
2026-05-10 04:53:09 +03:00
< ErrorMeta
:code = "config.code"
:show-status-list = "config.showStatusList"
:show-request-id = "config.showRequestId"
:request-id-label = "config.requestIdLabel"
:request-id = "config.requestId"
/ >
2026-05-08 19:11:09 +03:00
< / div >
< / v-main >
2026-05-12 04:45:18 +03:00
< DevIndexBadge :index = "route.meta.devIndex" :label = "route.meta.devLabel" / >
2026-05-08 19:11:09 +03:00
< / v-app >
< / template >
< style scoped >
. error - main {
background : # 012019 ;
color : # fff ;
min - height : 100 vh ;
display : flex ;
flex - direction : column ;
}
. error - content {
flex : 1 ;
display : flex ;
flex - direction : column ;
align - items : center ;
justify - content : center ;
padding : 24 px 32 px 80 px ;
text - align : center ;
max - width : 560 px ;
margin : 0 auto ;
}
. err - title {
font - size : 28 px ;
font - weight : 600 ;
font - variation - settings : 'opsz' 28 ;
letter - spacing : - 0.018 em ;
margin : 0 0 12 px ;
color : # fff ;
}
. err - desc {
color : # b1c2bd ;
line - height : 1.55 ;
font - size : 15 px ;
margin : 0 0 24 px ;
}
< / style >