Redesign client cabinet UI with capsule nav and card layouts
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="lk-shell">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<main class="container mx-auto p-4 md:p-6">
|
<main class="mx-auto w-full max-w-7xl p-4 md:p-6 lg:p-8">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,72 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--brand-primary: #0f766e;
|
--brand-primary: #139957;
|
||||||
--brand-secondary: #1d4ed8;
|
--brand-primary-deep: #0d854a;
|
||||||
|
--brand-secondary: #ff5600;
|
||||||
|
--brand-surface: #f4fbf7;
|
||||||
|
--brand-muted: #d8eee1;
|
||||||
|
--brand-ink: #0f2f20;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-base-200 text-base-content;
|
font-family: 'Manrope', 'Segoe UI', sans-serif;
|
||||||
|
@apply text-base-content;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 8% 10%, rgba(19, 153, 87, 0.3), transparent 35%),
|
||||||
|
radial-gradient(circle at 92% 0%, rgba(255, 86, 0, 0.16), transparent 28%),
|
||||||
|
linear-gradient(160deg, #f7fcf9 0%, #edf7f0 44%, #f4fbf7 100%);
|
||||||
|
color: var(--brand-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lk-shell {
|
||||||
|
@apply min-h-screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-card {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--brand-primary) 16%, #ffffff 84%);
|
||||||
|
background: linear-gradient(160deg, rgba(255, 255, 255, 0.95), rgba(244, 251, 247, 0.9));
|
||||||
|
box-shadow: 0 20px 48px rgba(13, 133, 74, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-capsule {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.42);
|
||||||
|
background: linear-gradient(120deg, rgba(255, 255, 255, 0.35), rgba(255, 255, 255, 0.12));
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lk-nav-link {
|
||||||
|
@apply inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition-all duration-200;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lk-nav-link:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lk-nav-link-active {
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--brand-primary-deep);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-anim {
|
||||||
|
animation: product-card-in 0.55s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes product-card-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
type NavItem = {
|
||||||
|
to: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const leftCapsule: NavItem[] = [
|
||||||
|
{ to: '/profile', label: 'Профиль', icon: 'user' },
|
||||||
|
{ to: '/notifications', label: 'Каналы', icon: 'bell' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const centerCapsule: NavItem[] = [
|
||||||
|
{ to: '/products', label: 'Каталог', icon: 'grid' },
|
||||||
|
{ to: '/orders', label: 'Мои заказы', icon: 'stack' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rightCapsule: NavItem[] = [
|
||||||
|
{ to: '/cart', label: 'Корзина', icon: 'cart' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function isActive(path: string) {
|
||||||
|
return route.path === path;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="navbar bg-base-100 border-b border-base-300 sticky top-0 z-10">
|
<header class="sticky top-0 z-20 border-b border-white/35 bg-[#0d854a]/90 backdrop-blur-xl">
|
||||||
<div class="navbar-start">
|
<div class="mx-auto flex w-full max-w-7xl flex-col gap-3 px-4 py-3 md:px-6">
|
||||||
<NuxtLink to="/" class="btn btn-ghost text-xl">Fregat</NuxtLink>
|
<div class="flex items-center justify-between">
|
||||||
</div>
|
<NuxtLink to="/" class="inline-flex items-center gap-2 rounded-full bg-white/15 px-4 py-2 text-sm font-bold text-white">
|
||||||
<div class="navbar-center hidden md:flex">
|
<span class="h-2.5 w-2.5 rounded-full bg-[#ff5600]" />
|
||||||
<ul class="menu menu-horizontal px-1">
|
Fregat LK
|
||||||
<li><NuxtLink to="/products">Товары</NuxtLink></li>
|
</NuxtLink>
|
||||||
<li><NuxtLink to="/cart">Корзина</NuxtLink></li>
|
</div>
|
||||||
<li><NuxtLink to="/orders">Заказы</NuxtLink></li>
|
|
||||||
<li><NuxtLink to="/profile">Профиль</NuxtLink></li>
|
<nav class="flex flex-wrap items-center justify-between gap-2 md:flex-nowrap md:gap-4">
|
||||||
</ul>
|
<div class="glass-capsule flex w-full items-center gap-1 rounded-full p-1 md:w-auto">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in leftCapsule"
|
||||||
|
:key="item.to"
|
||||||
|
:to="item.to"
|
||||||
|
:class="['lk-nav-link', { 'lk-nav-link-active': isActive(item.to) }]"
|
||||||
|
>
|
||||||
|
<svg v-if="item.icon === 'user'" class="h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 12a4 4 0 1 0-4-4 4 4 0 0 0 4 4Zm0 2c-4.42 0-8 2.24-8 5v1h16v-1c0-2.76-3.58-5-8-5Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 22a2.5 2.5 0 0 0 2.45-2h-4.9A2.5 2.5 0 0 0 12 22Zm7-6V11a7 7 0 1 0-14 0v5l-2 2v1h18v-1l-2-2Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
{{ item.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-capsule flex w-full items-center gap-1 rounded-full p-1 md:w-auto">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in centerCapsule"
|
||||||
|
:key="item.to"
|
||||||
|
:to="item.to"
|
||||||
|
:class="['lk-nav-link', { 'lk-nav-link-active': isActive(item.to) }]"
|
||||||
|
>
|
||||||
|
<svg v-if="item.icon === 'grid'" class="h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M4 4h7v7H4V4Zm9 0h7v7h-7V4ZM4 13h7v7H4v-7Zm9 0h7v7h-7v-7Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 5h14v3H5V5Zm0 5h14v3H5v-3Zm0 5h14v4H5v-4Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
{{ item.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-capsule flex w-full items-center gap-1 rounded-full p-1 md:w-auto">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="item in rightCapsule"
|
||||||
|
:key="item.to"
|
||||||
|
:to="item.to"
|
||||||
|
:class="['lk-nav-link', { 'lk-nav-link-active': isActive(item.to) }]"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7 4h-2l-1 2v2h1l2.2 9h10.6l2.2-7H8.3L7.8 8H21V6h-12l-1-2Zm2 16a2 2 0 1 0 0 .01V20Zm8 0a2 2 0 1 0 0 .01V20Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
{{ item.label }}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const color = ref('прозрачный');
|
|||||||
const { mutate, loading, onDone, onError } = useMutation(SubmitCalculationOrderDocument);
|
const { mutate, loading, onDone, onError } = useMutation(SubmitCalculationOrderDocument);
|
||||||
const success = ref('');
|
const success = ref('');
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
|
const calculatedVolume = computed(() => Number(quantity.value) * Number(width.value) * Number(thickness.value));
|
||||||
|
|
||||||
onDone((result) => {
|
onDone((result) => {
|
||||||
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
|
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
|
||||||
@@ -38,36 +39,66 @@ function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="space-y-4 max-w-2xl">
|
<section class="space-y-6">
|
||||||
<h1 class="text-2xl font-bold">Корзина / заявка на расчет</h1>
|
<div class="text-center">
|
||||||
<div class="card bg-base-100 border border-base-300">
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Корзина и checkout</h1>
|
||||||
<div class="card-body space-y-3">
|
<p class="mt-1 text-sm text-[#28543f]/80">Заполните параметры и отправьте расчёт менеджеру.</p>
|
||||||
<label class="form-control">
|
</div>
|
||||||
<span class="label-text">Название позиции</span>
|
|
||||||
<input v-model="productName" type="text" class="input input-bordered" />
|
|
||||||
</label>
|
|
||||||
<label class="form-control">
|
|
||||||
<span class="label-text">Количество</span>
|
|
||||||
<input v-model="quantity" type="number" min="1" class="input input-bordered" />
|
|
||||||
</label>
|
|
||||||
<label class="form-control">
|
|
||||||
<span class="label-text">Ширина</span>
|
|
||||||
<input v-model="width" type="number" min="1" class="input input-bordered" />
|
|
||||||
</label>
|
|
||||||
<label class="form-control">
|
|
||||||
<span class="label-text">Толщина</span>
|
|
||||||
<input v-model="thickness" type="number" min="1" class="input input-bordered" />
|
|
||||||
</label>
|
|
||||||
<label class="form-control">
|
|
||||||
<span class="label-text">Цвет</span>
|
|
||||||
<input v-model="color" type="text" class="input input-bordered" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button class="btn btn-primary" :disabled="loading" @click="submit">Отправить менеджеру</button>
|
<div class="mx-auto max-w-4xl rounded-[30px] p-1 shadow-[0_26px_60px_rgba(13,133,74,0.18)]">
|
||||||
|
<div class="surface-card grid gap-6 rounded-[26px] p-5 md:grid-cols-[1.45fr_1fr] md:p-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text font-semibold text-[#194631]">Название позиции</span>
|
||||||
|
<input v-model="productName" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
|
</label>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text font-semibold text-[#194631]">Количество</span>
|
||||||
|
<input v-model="quantity" type="number" min="1" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
|
</label>
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text font-semibold text-[#194631]">Ширина</span>
|
||||||
|
<input v-model="width" type="number" min="1" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
|
</label>
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text font-semibold text-[#194631]">Толщина</span>
|
||||||
|
<input v-model="thickness" type="number" min="1" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
|
</label>
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text font-semibold text-[#194631]">Цвет</span>
|
||||||
|
<input v-model="color" type="text" class="input input-bordered border-[#d0e8d8] bg-white/80">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button class="btn border-0 bg-[#139957] text-white hover:bg-[#0d854a]" :disabled="loading" @click="submit">
|
||||||
|
{{ loading ? 'Отправляем…' : 'Отправить менеджеру' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="rounded-2xl border border-[#cde8d8] bg-white/75 p-4">
|
||||||
|
<h2 class="text-lg font-bold text-[#103221]">Итог checkout</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm text-[#214735]">
|
||||||
|
<li class="flex items-center justify-between">
|
||||||
|
<span>Позиция</span>
|
||||||
|
<span class="font-semibold">{{ productName || 'Не указано' }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center justify-between">
|
||||||
|
<span>Количество</span>
|
||||||
|
<span class="font-semibold">{{ quantity }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center justify-between">
|
||||||
|
<span>Цвет</span>
|
||||||
|
<span class="font-semibold">{{ color }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="mt-4 rounded-xl bg-[#139957]/10 p-3 text-sm text-[#174631]">
|
||||||
|
Оценочный объём: <span class="font-bold">{{ calculatedVolume }}</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="success" class="alert alert-success">{{ success }}</div>
|
<div v-if="success" class="alert alert-success mx-auto max-w-4xl">{{ success }}</div>
|
||||||
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
|
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="space-y-6">
|
<section class="space-y-6">
|
||||||
<h1 class="text-3xl font-bold">Личный кабинет клиента</h1>
|
<div class="surface-card rounded-[30px] p-6 md:p-8">
|
||||||
<p class="text-base-content/80">Основные действия по спецификации: витрина, корзина, заказы и профиль с каналами уведомлений.</p>
|
<h1 class="text-3xl font-extrabold text-[#0f2f20] md:text-4xl">Личный кабинет клиента</h1>
|
||||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<p class="mt-2 max-w-2xl text-sm text-[#28543f]/80 md:text-base">
|
||||||
<NuxtLink to="/products" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Товары</h2></div></NuxtLink>
|
Быстрый доступ к каталогу, корзине, заказам и профилю в новом интерфейсе.
|
||||||
<NuxtLink to="/cart" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Корзина</h2></div></NuxtLink>
|
</p>
|
||||||
<NuxtLink to="/orders" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Заказы</h2></div></NuxtLink>
|
<div class="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||||
<NuxtLink to="/profile" class="card bg-base-100 border border-base-300 shadow-sm"><div class="card-body"><h2 class="card-title">Профиль</h2></div></NuxtLink>
|
<NuxtLink to="/products" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md">
|
||||||
|
<h2 class="font-bold text-[#123824]">Каталог</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/cart" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md">
|
||||||
|
<h2 class="font-bold text-[#123824]">Корзина</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/orders" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md">
|
||||||
|
<h2 class="font-bold text-[#123824]">Мои заказы</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/profile" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md">
|
||||||
|
<h2 class="font-bold text-[#123824]">Профиль</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/notifications" class="rounded-2xl border border-[#d2e9db] bg-white/80 p-4 transition hover:-translate-y-0.5 hover:shadow-md">
|
||||||
|
<h2 class="font-bold text-[#123824]">Уведомления</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
143
app/pages/notifications.vue
Normal file
143
app/pages/notifications.vue
Normal 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>
|
||||||
@@ -1,46 +1,126 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||||
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
|
||||||
import { MyCurrentOrdersDocument, MyOrdersDocument } from '~/composables/graphql/generated';
|
import {
|
||||||
|
ClientReviewOrderDocument,
|
||||||
|
MyCurrentOrdersDocument,
|
||||||
|
MyOrdersDocument,
|
||||||
|
} from '~/composables/graphql/generated';
|
||||||
|
|
||||||
const currentOrders = useQuery(MyCurrentOrdersDocument);
|
const currentOrders = useQuery(MyCurrentOrdersDocument);
|
||||||
const allOrders = useQuery(MyOrdersDocument);
|
const allOrders = useQuery(MyOrdersDocument);
|
||||||
|
const reviewOrder = useMutation(ClientReviewOrderDocument);
|
||||||
|
const actionError = ref('');
|
||||||
|
|
||||||
|
reviewOrder.onError((error) => {
|
||||||
|
actionError.value = error.message;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function approve(orderId: string) {
|
||||||
|
actionError.value = '';
|
||||||
|
await reviewOrder.mutate({ orderId, decision: 'APPROVE' });
|
||||||
|
await Promise.all([currentOrders.refetch(), allOrders.refetch()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reject(orderId: string) {
|
||||||
|
actionError.value = '';
|
||||||
|
await reviewOrder.mutate({ orderId, decision: 'REJECT' });
|
||||||
|
await Promise.all([currentOrders.refetch(), allOrders.refetch()]);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="space-y-8">
|
<section class="space-y-6">
|
||||||
<h1 class="text-2xl font-bold">Мои заказы</h1>
|
<div>
|
||||||
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Мои заказы</h1>
|
||||||
|
<p class="mt-1 text-sm text-[#28543f]/80">Все заявки в широком карточном формате.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div v-if="actionError" class="alert alert-error">{{ actionError }}</div>
|
||||||
<h2 class="text-xl font-semibold">Текущие</h2>
|
|
||||||
<div v-if="currentOrders.loading.value" class="alert">Загрузка...</div>
|
<div class="space-y-4">
|
||||||
|
<h2 class="text-lg font-bold text-[#123824]">Текущие</h2>
|
||||||
|
<div v-if="currentOrders.loading.value" class="alert surface-card border-0">Загрузка...</div>
|
||||||
|
<div v-else-if="(currentOrders.result.value?.myCurrentOrders?.length ?? 0) === 0" class="alert surface-card border-0">
|
||||||
|
Активных заказов пока нет.
|
||||||
|
</div>
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<article v-for="order in currentOrders.result.value?.myCurrentOrders ?? []" :key="order.id" class="card bg-base-100 border border-base-300">
|
<article
|
||||||
<div class="card-body gap-2">
|
v-for="order in currentOrders.result.value?.myCurrentOrders ?? []"
|
||||||
<div class="flex items-center justify-between">
|
:key="order.id"
|
||||||
<h3 class="font-semibold">{{ order.code }}</h3>
|
class="surface-card rounded-3xl p-4 md:p-5"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-[#123824]">{{ order.code }}</h3>
|
||||||
|
<p class="text-xs text-[#355947]">{{ new Date(order.createdAt).toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<OrderStatusBadge :status="order.status" />
|
<OrderStatusBadge :status="order.status" />
|
||||||
</div>
|
</div>
|
||||||
<ul class="text-sm">
|
</div>
|
||||||
<li v-for="item in order.items" :key="item.id">{{ item.productName }} × {{ item.quantity }}</li>
|
|
||||||
</ul>
|
<ul class="mt-4 grid gap-2 text-sm text-[#214735]">
|
||||||
|
<li
|
||||||
|
v-for="item in order.items"
|
||||||
|
:key="item.id"
|
||||||
|
class="rounded-xl border border-[#d6ebde] bg-white/75 px-3 py-2"
|
||||||
|
>
|
||||||
|
{{ item.productName }} × {{ item.quantity }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div v-if="order.status === 'WAITING_DOUBLE_CONFIRM'" class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button class="btn btn-sm border-0 bg-[#139957] text-white hover:bg-[#0d854a]" @click="approve(order.id)">
|
||||||
|
Подтвердить
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm border-0 bg-[#d32422] text-white hover:bg-[#b31f1d]" @click="reject(order.id)">
|
||||||
|
Отклонить
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-4">
|
||||||
<h2 class="text-xl font-semibold">Все</h2>
|
<h2 class="text-lg font-bold text-[#123824]">История заказов</h2>
|
||||||
<article v-for="order in allOrders.result.value?.myOrders ?? []" :key="order.id" class="card bg-base-100 border border-base-300">
|
<div v-if="allOrders.loading.value" class="alert surface-card border-0">Загрузка истории...</div>
|
||||||
<div class="card-body gap-2">
|
<div v-else-if="(allOrders.result.value?.myOrders?.length ?? 0) === 0" class="alert surface-card border-0">
|
||||||
<div class="flex items-center justify-between">
|
История заказов пока пустая.
|
||||||
<h3 class="font-semibold">{{ order.code }}</h3>
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<article
|
||||||
|
v-for="order in allOrders.result.value?.myOrders ?? []"
|
||||||
|
:key="order.id"
|
||||||
|
class="surface-card rounded-3xl p-4 md:p-5"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-[#123824]">{{ order.code }}</h3>
|
||||||
|
<p class="text-xs text-[#355947]">{{ new Date(order.updatedAt).toLocaleString() }}</p>
|
||||||
|
</div>
|
||||||
<OrderStatusBadge :status="order.status" />
|
<OrderStatusBadge :status="order.status" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm">Условия доставки: {{ order.deliveryTerms || 'ожидает обработки менеджером' }}</p>
|
|
||||||
<p class="text-sm">Итого: {{ order.totalPrice ?? 'после обработки менеджером' }}</p>
|
<div class="mt-4 grid gap-3 text-sm text-[#214735] md:grid-cols-2">
|
||||||
</div>
|
<div class="rounded-xl border border-[#d6ebde] bg-white/75 px-3 py-2">
|
||||||
</article>
|
Условия доставки: {{ order.deliveryTerms || 'ожидает обработки менеджером' }}
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-[#d6ebde] bg-white/75 px-3 py-2">
|
||||||
|
Итого: {{ order.totalPrice ?? 'после обработки менеджером' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="order.status === 'WAITING_DOUBLE_CONFIRM'" class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button class="btn btn-sm border-0 bg-[#139957] text-white hover:bg-[#0d854a]" @click="approve(order.id)">
|
||||||
|
Подтвердить
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm border-0 bg-[#d32422] text-white hover:bg-[#b31f1d]" @click="reject(order.id)">
|
||||||
|
Отклонить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,24 +3,73 @@ import { useQuery } from '@vue/apollo-composable';
|
|||||||
import { ClientProductsDocument } from '~/composables/graphql/generated';
|
import { ClientProductsDocument } from '~/composables/graphql/generated';
|
||||||
|
|
||||||
const { result, loading, error } = useQuery(ClientProductsDocument);
|
const { result, loading, error } = useQuery(ClientProductsDocument);
|
||||||
|
|
||||||
|
const coverPresets = [
|
||||||
|
['#e9fbe5', '#acfcd5', '#7be9aa'],
|
||||||
|
['#f5fff7', '#d9f5e6', '#8bd8b0'],
|
||||||
|
['#fef4ed', '#ffe5d8', '#ffd1b8'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function createProductCover(name: string, sku: string) {
|
||||||
|
const seed = `${name}${sku}`.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
const [start, middle, finish] = coverPresets[seed % coverPresets.length];
|
||||||
|
const firstLetter = name.trim().charAt(0).toUpperCase() || 'P';
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="${start}" />
|
||||||
|
<stop offset="56%" stop-color="${middle}" />
|
||||||
|
<stop offset="100%" stop-color="${finish}" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="640" height="480" fill="url(#g)" rx="38" />
|
||||||
|
<g opacity="0.18">
|
||||||
|
<circle cx="520" cy="66" r="100" fill="#0d854a" />
|
||||||
|
<circle cx="80" cy="440" r="100" fill="#0d854a" />
|
||||||
|
</g>
|
||||||
|
<text x="50%" y="53%" text-anchor="middle" fill="#0f2f20" font-family="Manrope, sans-serif" font-size="186" font-weight="700">${firstLetter}</text>
|
||||||
|
</svg>
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="space-y-4">
|
<section class="space-y-5">
|
||||||
<h1 class="text-2xl font-bold">Витрина товаров</h1>
|
<div>
|
||||||
<div v-if="loading" class="alert">Загрузка...</div>
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
|
||||||
|
<p class="mt-1 text-sm text-[#28543f]/80">Витрина в формате карточек: только визуал и название позиции.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="alert surface-card border-0">Загрузка каталога...</div>
|
||||||
<div v-else-if="error" class="alert alert-error">{{ error.message }}</div>
|
<div v-else-if="error" class="alert alert-error">{{ error.message }}</div>
|
||||||
<div v-else class="grid gap-4 lg:grid-cols-2">
|
|
||||||
<article v-for="product in result?.clientProducts ?? []" :key="product.id" class="card bg-base-100 border border-base-300">
|
<div v-else-if="(result?.clientProducts?.length ?? 0) > 0" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
<div class="card-body gap-2">
|
<article
|
||||||
<h2 class="card-title">{{ product.name }}</h2>
|
v-for="(product, index) in result?.clientProducts ?? []"
|
||||||
<p class="text-sm opacity-80">{{ product.description }}</p>
|
:key="product.id"
|
||||||
<p class="text-xs">SKU: {{ product.sku }}</p>
|
class="surface-card product-card-anim overflow-hidden rounded-3xl p-3"
|
||||||
<ul class="text-sm space-y-1">
|
:style="{ animationDelay: `${index * 55}ms` }"
|
||||||
<li v-for="stock in product.availableInWarehouses" :key="stock.warehouse.id">{{ stock.warehouse.name }}: {{ stock.availableQty }}</li>
|
>
|
||||||
</ul>
|
<figure class="overflow-hidden rounded-2xl">
|
||||||
|
<img
|
||||||
|
:src="createProductCover(product.name, product.sku)"
|
||||||
|
:alt="`Изображение товара ${product.name}`"
|
||||||
|
class="h-48 w-full object-cover transition duration-300 hover:scale-105"
|
||||||
|
loading="lazy"
|
||||||
|
>
|
||||||
|
</figure>
|
||||||
|
<div class="px-1 pb-2 pt-3">
|
||||||
|
<h2 class="text-lg font-bold text-[#133826]">{{ product.name }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="alert surface-card border-0">
|
||||||
|
В каталоге пока нет активных товаров.
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,8 +13,30 @@ const registerMutation = useMutation(RegisterSelfDocument);
|
|||||||
const messengerMutation = useMutation(ConnectMessengerDocument);
|
const messengerMutation = useMutation(ConnectMessengerDocument);
|
||||||
|
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
|
const messageTone = ref<'success' | 'error'>('success');
|
||||||
|
|
||||||
|
registerMutation.onDone(() => {
|
||||||
|
message.value = 'Заявка на регистрацию отправлена менеджеру';
|
||||||
|
messageTone.value = 'success';
|
||||||
|
});
|
||||||
|
|
||||||
|
registerMutation.onError((error) => {
|
||||||
|
message.value = error.message;
|
||||||
|
messageTone.value = 'error';
|
||||||
|
});
|
||||||
|
|
||||||
|
messengerMutation.onDone(() => {
|
||||||
|
message.value = 'Канал уведомлений подключен';
|
||||||
|
messageTone.value = 'success';
|
||||||
|
});
|
||||||
|
|
||||||
|
messengerMutation.onError((error) => {
|
||||||
|
message.value = error.message;
|
||||||
|
messageTone.value = 'error';
|
||||||
|
});
|
||||||
|
|
||||||
function register() {
|
function register() {
|
||||||
|
message.value = '';
|
||||||
registerMutation.mutate({
|
registerMutation.mutate({
|
||||||
input: {
|
input: {
|
||||||
companyName: companyName.value,
|
companyName: companyName.value,
|
||||||
@@ -22,50 +44,62 @@ function register() {
|
|||||||
contactName: contactName.value,
|
contactName: contactName.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
},
|
},
|
||||||
}).then(() => {
|
|
||||||
message.value = 'Заявка на регистрацию отправлена менеджеру';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectMessenger() {
|
function connectMessenger() {
|
||||||
|
message.value = '';
|
||||||
messengerMutation.mutate({
|
messengerMutation.mutate({
|
||||||
input: {
|
input: {
|
||||||
type: channelType.value,
|
type: channelType.value,
|
||||||
channelId: channelId.value,
|
channelId: channelId.value,
|
||||||
},
|
},
|
||||||
}).then(() => {
|
|
||||||
message.value = 'Канал уведомлений подключен';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="space-y-6 max-w-2xl">
|
<section class="space-y-6">
|
||||||
<h1 class="text-2xl font-bold">Профиль и каналы уведомлений</h1>
|
<div>
|
||||||
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Профиль</h1>
|
||||||
|
<p class="mt-1 text-sm text-[#28543f]/80">Регистрация компании и подключение каналов уведомлений.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 border border-base-300">
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
<div class="card-body space-y-3">
|
<div class="surface-card rounded-3xl p-5">
|
||||||
<h2 class="card-title">Самостоятельная регистрация</h2>
|
<h2 class="text-xl font-bold text-[#123824]">Самостоятельная регистрация</h2>
|
||||||
<input v-model="companyName" type="text" placeholder="Компания" class="input input-bordered" />
|
<div class="mt-4 space-y-3">
|
||||||
<input v-model="inn" type="text" placeholder="ИНН" class="input input-bordered" />
|
<input v-model="companyName" type="text" placeholder="Компания" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
||||||
<input v-model="contactName" type="text" placeholder="Контактное лицо" class="input input-bordered" />
|
<input v-model="inn" type="text" placeholder="ИНН" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
||||||
<input v-model="email" type="email" placeholder="Email" class="input input-bordered" />
|
<input v-model="contactName" type="text" placeholder="Контактное лицо" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
||||||
<button class="btn btn-primary" @click="register">Отправить заявку</button>
|
<input v-model="email" type="email" placeholder="Email" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
||||||
|
<button class="btn w-full border-0 bg-[#139957] text-white hover:bg-[#0d854a]" :disabled="registerMutation.loading.value" @click="register">
|
||||||
|
{{ registerMutation.loading.value ? 'Отправляем…' : 'Отправить заявку' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="surface-card rounded-3xl p-5">
|
||||||
|
<h2 class="text-xl font-bold text-[#123824]">Каналы уведомлений</h2>
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
<select v-model="channelType" class="select select-bordered w-full border-[#d0e8d8] bg-white/80">
|
||||||
|
<option value="TELEGRAM">Telegram</option>
|
||||||
|
<option value="MAX">Max</option>
|
||||||
|
</select>
|
||||||
|
<input v-model="channelId" type="text" placeholder="ID канала" class="input input-bordered w-full border-[#d0e8d8] bg-white/80">
|
||||||
|
<button class="btn w-full border-0 bg-[#ff5600] text-white hover:bg-[#e24e00]" :disabled="messengerMutation.loading.value" @click="connectMessenger">
|
||||||
|
{{ messengerMutation.loading.value ? 'Подключаем…' : 'Подключить канал' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-100 border border-base-300">
|
<div
|
||||||
<div class="card-body space-y-3">
|
v-if="message"
|
||||||
<h2 class="card-title">Подключение мессенджера</h2>
|
class="alert"
|
||||||
<select v-model="channelType" class="select select-bordered">
|
:class="messageTone === 'success' ? 'alert-success' : 'alert-error'"
|
||||||
<option value="TELEGRAM">Telegram</option>
|
>
|
||||||
<option value="MAX">Max</option>
|
{{ message }}
|
||||||
</select>
|
|
||||||
<input v-model="channelId" type="text" placeholder="ID канала" class="input input-bordered" />
|
|
||||||
<button class="btn btn-secondary" @click="connectMessenger">Подключить канал</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="message" class="alert alert-success">{{ message }}</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user