Личный кабинет пользователя
Товары есть, заказы есть, оплата есть... Осталось только вывести пользователю историю его заказов со статусом оплаты.
Заодно можно и отобразить оплату в админке. На этом, полагаю, с основным функционалом мы закончим.
В этой заметке не будет практически никакой новой информации, потому что всё это мы уже делали много раз: создадим контроллеры, пропишем маршруты, сделаем странички на сайте и выведем нужные данные.
Но, тем не менее, коротенько расскажу про создание личного кабинета для пользователя.
Контроллеры
Для вывода заказов пользователя мы создадим новый класс в Controllers/User/Orders. В эту директорию я кладу контроллеры, работающие с текущим пользователем, такие как Profile. Так сказать, личные контроллеры.
<?php
namespace App\Controllers\User;
// ...
class Orders extends ModelGetController
{
protected $scope = 'profile';
protected $model = Order::class;
protected function beforeGet(Builder $c): Builder
{
// Вывод одного заказа со всеми свойствами
$c->where('user_id', $this->user->id);
$c->with('address');
$c->with('payment');
$c->with('orderProducts', 'orderProducts.product');
return $c;
}
protected function beforeCount(Builder $c): Builder
{
// Выборка списка заказов
$c->where('user_id', $this->user->id);
// Возможность поиска по номеру
if ($query = trim($this->getProperty('query', ''))) {
$c->where('num', 'LIKE', "$query%");
}
return $c;
}
protected function afterCount(Builder $c): Builder
{
// Добор к списку количества товаров и оплаты
$c->withCount('orderProducts');
$c->with('payment:order_id,paid');
return $c;
}
protected function addSorting(Builder $c): Builder
{
// Исправление сортировки по номеру
// Я не придумал, как правильно сделать натуральную сортировку в MySQL,
// поэтому просто меняю её на дату создания - они всё равно совпадают
if ($sort = $this->getProperty('sort')) {
if ($sort === 'num') {
$dir = strtolower($this->getProperty('dir', '')) === 'desc' ? 'desc' : 'asc';
$c->orderBy('created_at', $dir);
} else {
$c = parent::addSorting($c);
}
}
return $c;
}
protected function getPrimaryKey(): ?array
{
// Замена обращения по id на uuid
if ($uuid = $this->getProperty('uuid')) {
return ['uuid' => $uuid];
}
return null;
}
}
Выборку платежа и замену сортировки по номеру я также внёс и в контроллер заказов админки.
Маршрут для нового контроллера вот такой:
$group->group(
'/user',
static function (RouteCollectorProxy $group) {
$group->any('/profile', App\Controllers\User\Profile::class);
$group->any('/addresses', App\Controllers\User\Addresses::class);
$group->any('/orders[/{uuid}]', App\Controllers\User\Orders::class);
}
);
Контроллер выводит как список заказов юзера, так и отдельный заказ со всеми свойствами.
На этом с бэкендом всё.
Пользовательный раздел
Раз у нас есть авторизация, давайте выделим для пользователей и собственный раздел в директории pages/user/.
Там будут находиться внутренние странички личного кабинета, а корнем для них станет файл pages/user.vue:
<template>
<div>
<!-- Отображаем 2 вкладки для вложенных страничек-->
<b-nav tabs class="mt-4">
<b-nav-item :to="{name: 'user-orders'}">{{ $t('models.order.title_many') }}</b-nav-item>
<b-nav-item :to="{name: 'user-profile'}">{{ $t('security.profile') }}</b-nav-item>
</b-nav>
<div class="mt-4">
<!-- Здесь будет содержимое страничек-->
<nuxt-child />
</div>
</div>
</template>
<script>
export default {
name: 'UserPage',
// Загружать раздел можно только юзерам
validate({app}) {
return app.$auth.loggedIn
},
watch: {
// Если юзер выходит прямо в разделе - редирект на главную
'$auth.loggedIn'(newValue) {
if (!newValue) {
this.$router.replace({name: 'index'})
}
},
},
}
</script>
Это файл как-бы роутер для раздела, он отобразит все странички своей директории на месте компонента <nuxt-child />. Имейте в виду, что эти странички являются вложенными маршрутами, поэтому метод validate работает для них всех.
Чтобы при заходе на адрес /user мы не видели только неактивные вкладки с пустотой (подраздел-то не выбран), создаём еще страничку pages/user/index.vue - она сделает редирект на заказы.
<script>
export default {
name: 'UserIndexPage',
asyncData({redirect}) {
redirect({name: 'user-orders'})
},
render() {
return null
},
}
</script>
Страница заказа
Здесь вообще всё по образу и подобию админки. Выводим табличку заказов и показываем их в модальном окошке.
В общем виде таблица выглядит вот так:
<template>
<div>
<vesp-table :url="url" :fields="fields" :filters="filters" :table-actions="tableActions" :sort="sort" :dir="dir">
<template #cell(payment)="{value}">
<div v-if="value.order_id" class="text-center">
<fa v-if="value.paid === null" icon="minus" class="text-muted" :title="$t('models.payment.paid_null')" />
<fa v-else-if="value.paid" icon="check" class="text-success" :title="$t('models.payment.paid_true')" />
<fa v-else icon="times" class="text-danger" :title="$t('models.payment.paid_false')" />
</div>
</template>
</vesp-table>
<nuxt-child />
</div>
</template>
В единственном слоте я добавил отображение статуса оплаты. Такое же изменение внёс и в таблицу админки.
Формы я так же просто скопировал из админки с небольними изменениями: убрал смену юзера, адреса, да и вообще возможность редактирования.
Конечно, я подключил все требуемые компоненты, иконки и стили в nuxt.config.js. Посмотрите изменения в исходниках.
Страница профиля
На страницу профиля я просто перенёс форму из модалки, которую мы раньше использовали.
Конечно, пришлось добавить кнопку отправки и обработку формы, потому что раньше за это отвечал компонент <vesp-modal />, а теперь нужно делать самостоятельно.
Но как вы понимаете, вообще ничегошеньки сложного нет:
<template>
<div>
<b-form @submit.prevent="onSubmit">
<b-overlay :show="loading" opacity="0.5">
<form-user v-model="record" :show-group="false" :show-comment="false" :show-status="false" />
</b-overlay>
<div class="text-center mt-3">
<b-button variant="primary" type="submit" :disabled="loading">
{{ $t('actions.submit') }}
</b-button>
</div>
</b-form>
</div>
</template>
<script>
import FormUser from '../../../admin/components/forms/user'
export default {
name: 'UserProfilePage',
components: {FormUser},
data() {
return {
loading: false,
url: 'user/profile',
record: {},
}
},
mounted() {
this.record = {...this.$auth.user}
},
methods: {
async onSubmit() {
this.loading = true
try {
const {data} = await this.$axios.patch('user/profile', this.record)
this.record = {...data.user}
this.$auth.setUser(this.record)
} catch (e) {
} finally {
this.loading = false
}
},
},
}
</script>
Работа с профилем пользователя переехала уже в третий раз!
Изменение навигационной панели
Тут я добавил вывод аватарки юзера, если она есть, и убрал модальный профиль юзера - заменил его на ссылку. Сам компонент <user-profile /> удалил.
<b-dropdown v-else variant="light">
<template #button-content>
<b-img
v-if="$auth.user.file"
:src="$image($auth.user.file, {w: 50, h: 50})"
class="rounded-circle"
width="25"
height="25"
/>
<fa v-else icon="user" />
</template>
<b-dropdown-text>{{ $auth.user.fullname }}</b-dropdown-text>
<b-dropdown-divider />
<b-dropdown-item :to="{name: 'user-orders'}">{{ $t('models.order.title_many') }}</b-dropdown-item>
<b-dropdown-item :to="{name: 'user-profile'}">{{ $t('security.profile') }}</b-dropdown-item>
<b-dropdown-divider />
<b-dropdown-item link-class="d-flex justify-content-between align-items-center" @click="onLogout">
{{ $t('security.logout') }}
<fa icon="right-from-bracket" />
</b-dropdown-item>
</b-dropdown>
Получилось очень симпатично.
Заключение
Как и предупреждал в начале заметки - сегодня ничего нового. Понятно, что дальше в новый раздел вы можете добавлять любой функционал.
В админке я так же добавил работу с оплатой, но там её можно немного редактировать:
Осталась всего одна заметка - запуск в продакшен с использованием Docker.
Итоговые изменения одним коммитом.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
321
14.09.2023, 10:19:52
2 комментария
Дмитрий П.
15.09.2023, 10:54:39
А какая разница между переопределением метода getPrimaryKey в контроллере, например, Orders, и указанием свойства $primaryKey = ['uuid']?
Василий Наумкин
15.09.2023, 11:27:17
Никакой разницы, только в количестве строк.
Не знаю, почему я так написал - не задумывался -)
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо!
Извини, тупанул.