Redesign client cabinet UI with capsule nav and card layouts

This commit is contained in:
Ruslan Bakiev
2026-04-01 18:59:20 +07:00
parent 5b5f6250d3
commit b4537c1483
9 changed files with 598 additions and 111 deletions

143
app/pages/notifications.vue Normal file
View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
MyMessengerConnectionsDocument,
MyNotificationHistoryDocument,
SendTestMessengerMessageDocument,
} from '~/composables/graphql/generated';
const selectedChannel = ref<'TELEGRAM' | 'MAX'>('TELEGRAM');
const channelId = ref('');
const customMessage = ref('Тест канала уведомлений Fregat');
const feedback = ref('');
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
const historyQuery = useQuery(MyNotificationHistoryDocument, () => ({
channel: selectedChannel.value,
limit: 50,
}));
const sendTestMutation = useMutation(SendTestMessengerMessageDocument);
watch(selectedChannel, () => {
feedback.value = '';
historyQuery.refetch({
channel: selectedChannel.value,
limit: 50,
});
});
const activeConnection = computed(() =>
connectionsQuery.result.value?.myMessengerConnections?.find(
(item: { type: 'TELEGRAM' | 'MAX'; isActive: boolean; channelId: string }) =>
item.type === selectedChannel.value && item.isActive,
),
);
async function sendTest() {
feedback.value = '';
const result = await sendTestMutation.mutate({
type: selectedChannel.value,
channelId: channelId.value || undefined,
message: customMessage.value || undefined,
});
const payload = result?.data?.sendTestMessengerMessage;
if (!payload) {
feedback.value = 'Не удалось отправить тестовое сообщение.';
return;
}
feedback.value = payload.success
? `Отправлено в ${payload.type}: ${payload.detail}`
: `Ошибка отправки: ${payload.detail}`;
await historyQuery.refetch({
channel: selectedChannel.value,
limit: 50,
});
}
</script>
<template>
<section class="space-y-6">
<div>
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Уведомления</h1>
<p class="mt-1 text-sm text-[#28543f]/80">Управление Telegram и Max в едином стиле кабинета.</p>
</div>
<div class="surface-card rounded-3xl p-5">
<div class="space-y-4">
<div class="tabs tabs-boxed w-fit">
<button
class="tab"
:class="{ 'tab-active': selectedChannel === 'TELEGRAM' }"
@click="selectedChannel = 'TELEGRAM'"
>
Telegram
</button>
<button
class="tab"
:class="{ 'tab-active': selectedChannel === 'MAX' }"
@click="selectedChannel = 'MAX'"
>
Max
</button>
</div>
<p class="text-sm opacity-80">
Активный канал:
<span class="font-semibold">
{{ activeConnection ? activeConnection.channelId : 'не подключен' }}
</span>
</p>
<label class="form-control">
<span class="label-text">Канал (опционально)</span>
<input
v-model="channelId"
class="input input-bordered border-[#d0e8d8] bg-white/80"
placeholder="если пусто, берется активный подключенный канал"
>
</label>
<label class="form-control">
<span class="label-text">Тестовое сообщение</span>
<textarea v-model="customMessage" class="textarea textarea-bordered border-[#d0e8d8] bg-white/80" rows="3" />
</label>
<button
class="btn w-fit border-0 bg-[#139957] text-white hover:bg-[#0d854a]"
:disabled="sendTestMutation.loading.value"
@click="sendTest"
>
Отправить тест
</button>
<div v-if="feedback" class="alert" :class="feedback.startsWith('Ошибка') ? 'alert-error' : 'alert-success'">
{{ feedback }}
</div>
</div>
</div>
<div class="surface-card rounded-3xl p-5">
<div class="space-y-3">
<h2 class="text-xl font-bold text-[#123824]">История по каналу {{ selectedChannel }}</h2>
<div v-if="historyQuery.loading.value" class="alert border-0 bg-white/75">Загрузка истории...</div>
<div v-else-if="(historyQuery.result.value?.myNotificationHistory?.length ?? 0) === 0" class="alert">
История пока пустая.
</div>
<ul v-else class="space-y-3">
<li
v-for="item in historyQuery.result.value?.myNotificationHistory ?? []"
:key="item.id"
class="rounded-xl border border-[#d6ebde] bg-white/75 p-3"
>
<p class="font-semibold">{{ item.title }}</p>
<p class="text-sm opacity-80">{{ item.message }}</p>
<p class="text-xs opacity-60">{{ new Date(item.createdAt).toLocaleString() }}</p>
</li>
</ul>
</div>
</div>
</section>
</template>