9ff19f3cc3
feat(bot): всеобъемлющая инструкция (36 статей) + 31 узкая экскурсия, которая открывает окна Живой урок владельца 12.07.2026: «дубли чистятся в 2-х местах, а бот выдумал третье», «стыдно такого бота показывать», «видео показываем полное, а я спрашиваю про одно — надо резать», «надо не только кнопку показывать, а и что за ней». База знаний: 17 обзорных статей → 36 узких по темам. Портал обойдён целиком (5 агентов): все экраны, окна, подсказки «(?)», тексты ошибок, внутренние правила. Новое, чего бот не знал: дубли в ДВУХ местах (Поле и Предложения) + окно «источник уже есть»; правило 18:00; почему приходит меньше лимита; оплата по счёту и акты; списания; безопасность и 2FA; отчёты; массовые действия; регистрация. Экскурсии: 15 → 31, каждая на свою тему (3–7 шагов). Раннер GuidedTour научен шагу `open` — сам открывает окно/вкладку и подсвечивает то, что ВНУТРИ (окно дублей, форму подбора, диалог пополнения, панель тарифов, карточку сделки). 99 новых якорей data-tour расставлено по всему порталу (4 агента). Поиск: веса в tsvector (заголовок A / синонимы B / текст C) — иначе на «чистка дублей» первой всплывала статья «Списания» из-за мимоходного упоминания «поля». Тесты: Pest бот 39/39, Vitest 1222/1222. Новые сторожа: у каждой статьи есть экскурсия; экскурсия не короче 3 и не длиннее 7 шагов; каждая цель экскурсии реально существует в разметке; экскурсии про действия обязаны открывать окно. Живая проверка в браузере: экскурсия про дубли сама открыла окно объединения и перешла на второе место чистки. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> @
121 lines
7.8 KiB
Vue
121 lines
7.8 KiB
Vue
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { useAuthStore } from '../stores/auth';
|
||
import { submitSupportRequest } from '../api/support';
|
||
|
||
const auth = useAuthStore();
|
||
|
||
// FAQ (Фаза 2, UX-аудит 25.06): самопомощь до обращения в поддержку. Простым языком.
|
||
// Тексты — статически в коде (YAGNI: CMS не нужна), правятся обычным деплоем.
|
||
const faqItems: { q: string; a: string }[] = [
|
||
{ q: 'Что такое «заявка» (лид)?', a: 'Заявка — это контакт человека, который интересуется услугой в вашей нише. Мы находим таких людей и передаём их вам. За каждую переданную заявку списывается фиксированная цена.' },
|
||
{ q: 'Сколько стоит одна заявка?', a: 'Цена зависит от объёма: чем больше заявок в месяц, тем дешевле каждая (7 ступеней). Текущую цену видно на странице «Биллинг» в блоке «Цены за лид».' },
|
||
{ q: 'Как начать получать заявки?', a: 'Три шага: 1) пополните баланс; 2) создайте проект — укажите, по какому сайту/телефону собирать клиентов и сколько заявок в день нужно; 3) готово, проект встаёт в сбор и заявки идут со следующего дня.' },
|
||
{ q: 'Как пополнить баланс?', a: '«Биллинг» → «Пополнить» → выберите сумму (минимум 100 ₽). Из баланса списывается оплата за заявки.' },
|
||
{ q: 'Почему с меня списали деньги?', a: 'За каждую переданную заявку списывается её цена. Все списания видно в «Биллинг» → «Списания»: дата, заявка, тариф, сумма. Скрытых комиссий нет.' },
|
||
{ q: 'Что значит «хватит на N дней»?', a: 'Это прогноз: на сколько дней хватит баланса при дневном заказе ваших проектов. Если активных проектов нет — прогноз не считается.' },
|
||
{ q: 'Почему приём заявок «на паузе»?', a: 'Чаще всего закончился баланс — приём приостанавливается, чтобы вы не ушли в минус. Пополните баланс — приём включится автоматически.' },
|
||
{ q: 'Что такое «лимит заявок в день»?', a: 'Сколько заявок в день вы готовы принимать и оплачивать по проекту. Лимит можно менять в любой момент.' },
|
||
{ q: 'Чем «Список» отличается от «Доски» (Канбан)?', a: 'Это одни и те же ваши заявки в разном виде: «Список» — таблицей, «Доска» — карточками по статусам. Выбирайте, как удобнее.' },
|
||
{ q: 'Можно ли оплатить картой онлайн?', a: 'Онлайн-оплата картой подключается в ближайшее время. Пока по вопросам оплаты пишите в поддержку.' },
|
||
];
|
||
|
||
const supportEmail =
|
||
document.querySelector('meta[name="support-email"]')?.getAttribute('content') ?? 'support@liderra.ru';
|
||
|
||
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" data-testid="faq-card" data-tour="help-faq">
|
||
<h3 class="text-subtitle-2 mb-3">Частые вопросы</h3>
|
||
<v-expansion-panels variant="accordion" data-testid="faq-panels">
|
||
<v-expansion-panel v-for="(item, i) in faqItems" :key="i" data-tour="help-faq-item">
|
||
<v-expansion-panel-title>{{ item.q }}</v-expansion-panel-title>
|
||
<v-expansion-panel-text>{{ item.a }}</v-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
</v-expansion-panels>
|
||
</v-card>
|
||
|
||
<v-card variant="outlined" class="pa-5 mb-4" max-width="640" data-tour="help-email">
|
||
<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" data-tour="help-form">
|
||
<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>
|