Files
portal/app/resources/js/views/errors/ErrorView.vue
T
Дмитрий 1a0c9f5c8d
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
fix(ui): иконки-? → Lucide, почта поддержки .app→.ru, без префикса text:, карточка Канбана 0₽→—, crm.bp-gr.ru
UI-аудит раунд 2 (Playwright, протыкивание форм):
- vuetify.ts: +13 mdi→Lucide маппингов — bulk-бар проектов / импорт / экспорт отчётов и сделок / помощь / действия админки больше не падают в HelpCircle-fallback «?»
- config/services.php + ErrorMeta/ErrorView/HelpView: support@liderra.appsupport@liderra.ru (домен продукта .ru); status.liderra.app → status.liderra.ru
- dealsApiMapper: ветка deal.commented — текст комментария в активности без служебного ключа «text:»
- KanbanCard: costKopecks null-aware — «—» вместо врущего «0 ₽» (как в drawer)
- DealsView: подзаголовок «crm.bp» → «crm.bp-gr.ru» (как в импорте/админке)

Верификация: type-check ✓, build ✓, переоткрыто в Playwright локально (иконки/почта/комментарий/карточка/подзаголовок).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:34:03 +03:00

182 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.ru',
},
// Реальный request-id пока не проводится с бэкенда → блок скрыт
// (раньше показывался хардкод «REQ-3F8A2-0007» как настоящий).
showRequestId: false,
};
}
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.ru',
},
// Реальный incident-id и статус сервисов пока не проводятся с бэкенда →
// блоки скрыты (раньше — хардкод «INC-2026-0507-0034» + фейк-список
// статусов с Telegram/YooKassa, которых нет в стеке).
showRequestId: false,
showStatusList: false,
};
}
// 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>