feat(G7-A): экран «Помощь» (форма-заявка) + пункт меню + роут

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-19 15:04:17 +03:00
parent dd365b0a0e
commit 7ebe25385b
4 changed files with 123 additions and 1 deletions
+11
View File
@@ -0,0 +1,11 @@
import { apiClient } from './client';
export interface SupportRequestPayload {
name: string;
contact: string;
message: string;
}
export async function submitSupportRequest(payload: SupportRequestPayload): Promise<void> {
await apiClient.post('/api/support-requests', payload);
}
@@ -61,7 +61,10 @@ const navGroups = computed<NavGroup[]>(() => [
},
{
eyebrow: 'Команда',
items: [{ title: 'Настройки', icon: 'mdi-cog-outline', to: '/settings' }],
items: [
{ title: 'Настройки', icon: 'mdi-cog-outline', to: '/settings' },
{ title: 'Помощь', icon: 'mdi-lifebuoy', to: '/help' },
],
},
]);
+13
View File
@@ -300,6 +300,19 @@ const routes: RouteRecordRaw[] = [
devLabel: 'Admin PD Requests',
},
},
{
path: '/help',
name: 'help',
component: () => import('../views/HelpView.vue'),
meta: {
layout: 'app',
title: 'Помощь',
requiresAuth: true,
transition: 'ld-route-fadeup',
devIndex: 33,
devLabel: 'Помощь',
},
},
// Error pages: 403/500 явные + catch-all 404 (всегда последний).
{
path: '/403',
+95
View File
@@ -0,0 +1,95 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '../stores/auth';
import { submitSupportRequest } from '../api/support';
const auth = useAuthStore();
const supportEmail =
document.querySelector('meta[name="support-email"]')?.getAttribute('content') ?? 'support@liderra.app';
const name = ref(
[auth.user?.first_name, auth.user?.last_name].filter(Boolean).join(' ') || '',
);
const contact = ref(auth.user?.email ?? '');
const message = ref('');
const loading = ref(false);
const sent = ref(false);
const errorMsg = ref('');
const fieldErrors = ref<Record<string, string[]>>({});
async function submit() {
errorMsg.value = '';
fieldErrors.value = {};
if (!name.value.trim() || !contact.value.trim() || !message.value.trim()) {
errorMsg.value = 'Заполните все поля.';
return;
}
loading.value = true;
try {
await submitSupportRequest({ name: name.value, contact: contact.value, message: message.value });
sent.value = true;
message.value = '';
} catch (e: unknown) {
const err = e as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } };
if (err.response?.status === 422 && err.response.data?.errors) {
fieldErrors.value = err.response.data.errors;
} else {
errorMsg.value = 'Не удалось отправить. Попробуйте ещё раз или напишите на почту.';
}
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="help-view pa-6" data-testid="help-view">
<h1 class="text-h5 mb-1">Помощь</h1>
<p class="text-body-2 text-medium-emphasis mb-6">
Напишите нам ответим на ваш контакт. Можно по почте, через форму ниже или в чат справа.
</p>
<v-card variant="outlined" class="pa-5 mb-4" max-width="640">
<h3 class="text-subtitle-2 mb-2">Почта техподдержки</h3>
<a :href="`mailto:${supportEmail}`" class="text-primary" data-testid="support-email">{{ supportEmail }}</a>
</v-card>
<v-card variant="outlined" class="pa-5" max-width="640">
<h3 class="text-subtitle-2 mb-3">Оставить заявку</h3>
<v-alert v-if="sent" type="success" variant="tonal" class="mb-4" data-testid="support-sent">
Заявка отправлена. Мы свяжемся с вами по указанному контакту.
</v-alert>
<v-alert v-if="errorMsg" type="error" variant="tonal" class="mb-4">{{ errorMsg }}</v-alert>
<v-text-field
v-model="name"
label="Имя"
:error-messages="fieldErrors.name"
density="comfortable"
class="mb-2"
data-testid="support-name"
/>
<v-text-field
v-model="contact"
label="Контакт (телефон или email)"
:error-messages="fieldErrors.contact"
density="comfortable"
class="mb-2"
data-testid="support-contact"
/>
<v-textarea
v-model="message"
label="Сообщение"
:error-messages="fieldErrors.message"
rows="4"
density="comfortable"
class="mb-3"
data-testid="support-message"
/>
<v-btn color="primary" :loading="loading" data-testid="support-submit" @click="submit">Отправить</v-btn>
</v-card>
</div>
</template>