Build Nuxt 4 client cabinet with Apollo and GraphQL flows

This commit is contained in:
Ruslan Bakiev
2026-03-30 21:41:19 +07:00
parent 0220041129
commit 79d6138cca
36 changed files with 14418 additions and 1 deletions

73
app/pages/cart.vue Normal file
View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable';
import { SubmitCalculationOrderDocument } from '~/composables/graphql/generated';
const productName = ref('');
const quantity = ref(1);
const width = ref(100);
const thickness = ref(50);
const color = ref('прозрачный');
const { mutate, loading, onDone, onError } = useMutation(SubmitCalculationOrderDocument);
const success = ref('');
const errorMessage = ref('');
onDone((result) => {
success.value = `Заявка ${result.data?.submitCalculationOrder.code} отправлена`;
errorMessage.value = '';
});
onError((error) => {
errorMessage.value = error.message;
success.value = '';
});
function submit() {
mutate({
input: {
productName: productName.value,
quantity: Number(quantity.value),
parameters: {
width: Number(width.value),
thickness: Number(thickness.value),
color: color.value,
},
},
});
}
</script>
<template>
<section class="space-y-4 max-w-2xl">
<h1 class="text-2xl font-bold">Корзина / заявка на расчет</h1>
<div class="card bg-base-100 border border-base-300">
<div class="card-body space-y-3">
<label class="form-control">
<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>
</div>
<div v-if="success" class="alert alert-success">{{ success }}</div>
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
</section>
</template>

12
app/pages/index.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<section class="space-y-6">
<h1 class="text-3xl font-bold">Личный кабинет клиента</h1>
<p class="text-base-content/80">Основные действия по спецификации: витрина, корзина, заказы и профиль с каналами уведомлений.</p>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<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>
<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>
<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>
</div>
</section>
</template>

46
app/pages/orders.vue Normal file
View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import OrderStatusBadge from '~/components/orders/OrderStatusBadge.vue';
import { MyCurrentOrdersDocument, MyOrdersDocument } from '~/composables/graphql/generated';
const currentOrders = useQuery(MyCurrentOrdersDocument);
const allOrders = useQuery(MyOrdersDocument);
</script>
<template>
<section class="space-y-8">
<h1 class="text-2xl font-bold">Мои заказы</h1>
<div class="space-y-3">
<h2 class="text-xl font-semibold">Текущие</h2>
<div v-if="currentOrders.loading.value" class="alert">Загрузка...</div>
<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">
<div class="card-body gap-2">
<div class="flex items-center justify-between">
<h3 class="font-semibold">{{ order.code }}</h3>
<OrderStatusBadge :status="order.status" />
</div>
<ul class="text-sm">
<li v-for="item in order.items" :key="item.id">{{ item.productName }} × {{ item.quantity }}</li>
</ul>
</div>
</article>
</div>
</div>
<div class="space-y-3">
<h2 class="text-xl font-semibold">Все</h2>
<article v-for="order in allOrders.result.value?.myOrders ?? []" :key="order.id" class="card bg-base-100 border border-base-300">
<div class="card-body gap-2">
<div class="flex items-center justify-between">
<h3 class="font-semibold">{{ order.code }}</h3>
<OrderStatusBadge :status="order.status" />
</div>
<p class="text-sm">Условия доставки: {{ order.deliveryTerms || 'ожидает обработки менеджером' }}</p>
<p class="text-sm">Итого: {{ order.totalPrice ?? 'после обработки менеджером' }}</p>
</div>
</article>
</div>
</section>
</template>

26
app/pages/products.vue Normal file
View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import { ClientProductsDocument } from '~/composables/graphql/generated';
const { result, loading, error } = useQuery(ClientProductsDocument);
</script>
<template>
<section class="space-y-4">
<h1 class="text-2xl font-bold">Витрина товаров</h1>
<div v-if="loading" class="alert">Загрузка...</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 class="card-body gap-2">
<h2 class="card-title">{{ product.name }}</h2>
<p class="text-sm opacity-80">{{ product.description }}</p>
<p class="text-xs">SKU: {{ product.sku }}</p>
<ul class="text-sm space-y-1">
<li v-for="stock in product.availableInWarehouses" :key="stock.warehouse.id">{{ stock.warehouse.name }}: {{ stock.availableQty }}</li>
</ul>
</div>
</article>
</div>
</section>
</template>

71
app/pages/profile.vue Normal file
View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable';
import { ConnectMessengerDocument, RegisterSelfDocument } from '~/composables/graphql/generated';
const companyName = ref('');
const inn = ref('');
const contactName = ref('');
const email = ref('');
const channelId = ref('');
const channelType = ref<'TELEGRAM' | 'MAX'>('TELEGRAM');
const registerMutation = useMutation(RegisterSelfDocument);
const messengerMutation = useMutation(ConnectMessengerDocument);
const message = ref('');
function register() {
registerMutation.mutate({
input: {
companyName: companyName.value,
inn: inn.value || null,
contactName: contactName.value,
email: email.value,
},
}).then(() => {
message.value = 'Заявка на регистрацию отправлена менеджеру';
});
}
function connectMessenger() {
messengerMutation.mutate({
input: {
type: channelType.value,
channelId: channelId.value,
},
}).then(() => {
message.value = 'Канал уведомлений подключен';
});
}
</script>
<template>
<section class="space-y-6 max-w-2xl">
<h1 class="text-2xl font-bold">Профиль и каналы уведомлений</h1>
<div class="card bg-base-100 border border-base-300">
<div class="card-body space-y-3">
<h2 class="card-title">Самостоятельная регистрация</h2>
<input v-model="companyName" type="text" placeholder="Компания" class="input input-bordered" />
<input v-model="inn" type="text" placeholder="ИНН" class="input input-bordered" />
<input v-model="contactName" type="text" placeholder="Контактное лицо" class="input input-bordered" />
<input v-model="email" type="email" placeholder="Email" class="input input-bordered" />
<button class="btn btn-primary" @click="register">Отправить заявку</button>
</div>
</div>
<div class="card bg-base-100 border border-base-300">
<div class="card-body space-y-3">
<h2 class="card-title">Подключение мессенджера</h2>
<select v-model="channelType" class="select select-bordered">
<option value="TELEGRAM">Telegram</option>
<option value="MAX">Max</option>
</select>
<input v-model="channelId" type="text" placeholder="ID канала" class="input input-bordered" />
<button class="btn btn-secondary" @click="connectMessenger">Подключить канал</button>
</div>
</div>
<div v-if="message" class="alert alert-success">{{ message }}</div>
</section>
</template>