Выводим товары на сайте
Ну что, модели созданы, контроллеры отлажены, пора бы и вывести товары на нашем сайте.
Для удобной работы предлагаю использовать очень известную библиотеку Faker, которая позволит нам нагенерировать нужное количество товаров и категорий без лишних сложностей.
Устаналиваем fakerphp/faker зависимостью для разработки, на рабочем окружении она не будет нужна:
composer require fakerphp/faker --dev
Затем создаём новый seed файл в core/db/seeds/Products.php
<?php
use App\Models\Category;
use App\Models\Product;
use Phinx\Seed\AbstractSeed;
use Vesp\Services\Eloquent;
class Products extends AbstractSeed
{
// Количество генерируемых категорий
protected $categories = 100;
// И товаров
protected $products = 1000;
public function run(): void
{
// Если вдруг Faker не установлен - то просто ничего не делаем
if (!class_exists('Faker\Factory')) {
return;
}
// Временно отключаем проверку в БД по foreign key
(new Eloquent())->getConnection()->getSchemaBuilder()
->disableForeignKeyConstraints();
// Чтобы можно было полностью очистить таблицы
Category::query()->truncate();
Product::query()->truncate();
$faker = Faker\Factory::create();
// Создаём 100 категорий
for ($i = 0; $i < $this->categories; $i++) {
Category::query()->create([
'title' => 'Категория ' . ($i + 1),
// Случайный lorem ipsum
'description' => $faker->text,
// Категория активна с вероятностью 90%
'active' => random_int(0, 9) > 0,
]);
}
// Теперь 1000 товаров
for ($i = 0; $i < $this->products; $i++) {
Product::query()->create([
'title' => 'Товар ' . ($i + 1),
'description' => $faker->text,
// Указываем 1 из 100 категорий случайным образом
'category_id' => random_int(1, $this->categories),
// Артикулом назначаем уникальное 9ти-значное число
'sku' => $faker->unique()->randomNumber(9, true),
// Цена тоже случайная, от 100.00 до 10000.99
'price' => $faker->randomFloat(2, 100, 10000),
// 10% товаров неактивны
'active' => random_int(0, 9) > 0,
]);
}
}
}
При каждом запуске этого сида все товары и категории будут перезаписны случайными значениями.
Запускаем только наш новый сид:
composer db:seed-one Products
Web контроллеры
Теперь нам нужно создать публичные контроллеры товаров и категорий. Их основное отличие - они должны скрывать отключённые товары и не требовать авторизации пользователя.
src/Controllers/Web/Categories.php
<?php
namespace App\Controllers\Web;
use App\Models\Category;
use Illuminate\Database\Eloquent\Builder;
use Vesp\Controllers\ModelController;
class Categories extends ModelController
{
protected $model = Category::class;
// При выводе 1 модели обязательно проверяем её статус
protected function beforeGet(Builder $c): Builder
{
$c->where('active', true);
return $c;
}
// При выводе списка моделей делаем то же самое
protected function beforeCount(Builder $c): Builder
{
// Пока условия одинаковые - можно не дублировать код
return $this->beforeGet($c);
}
}
src/Controllers/Web/Products.php
<?php
namespace App\Controllers\Web;
use App\Models\Product;
use Illuminate\Database\Eloquent\Builder;
use Vesp\Controllers\ModelController;
class Products extends ModelController
{
protected $model = Product::class;
protected function beforeGet(Builder $c): Builder
{
// Не только товар должен быть включен
$c->where('active', true);
// Но и его категория!
$c->whereHas('category', static function (Builder $c) {
$c->where('active', true);
});
return $c;
}
protected function beforeCount(Builder $c): Builder
{
return $this->beforeGet($c);
}
protected function afterCount(Builder $c): Builder
{
// Не забываем выбрать категорию товара, пригодится
$c->with('category:id,title');
return $c;
}
}
Осталось только добавить новую группу маршрутов в core/routes.php:
$group->group(
'/web',
static function (RouteCollectorProxy $group) {
$group->any('/categories[/{id}]', App\Controllers\Web\Categories::class);
$group->any('/products[/{id}]', App\Controllers\Web\Products::class);
}
);
Проверяем работу контроллера товаров, переходя по http://vesp-shop.test/api/web/products:
Обратите внимание, какие запросы у нас получаются в SQL. Появилась и проверка на статус родительской категории, и большой массив выбираемых категорий для подстановки.
Тем не менее, выборка 845 товаров проходит за ~9 миллисекунд.
Вывод товаров на сайте
В нашем API всё готово, осталось только вывести товары на сайте. Напоминаю, что в приложении site ничего не особенного не подключено. Там нет мультиязычности, компонентов админки из Vesp - один только BootstrapVue.
Так сделано для минимального размера публичного приложения - как вы помните, я предлагаю подключать нужное, а не отключать ненужное.
Единственное, что нам нужно подключить в site/nuxt.config.js - это Axios, для запросов на сервер:
Config.modules = ['bootstrap-vue/nuxt', '@nuxtjs/axios', '@nuxtjs/pwa']
Теперь создаём новый компонент для вывода списка товаров.
src/site/components/list-products.vue
Для начала пишем простейший шаблон, который будет в цикле проходить по массиву товаров и выводить их id, название и цену:
<template>
<div class="products-list">
<div v-for="product in products" :key="product.id" class="product">
{{ product.id }}. {{ product.title }} — {{ product.price }} руб.
</div>
</div>
</template>
Дальше в этом же файле идёт javascript логика:
<script>
export default {
// Название компонента используется для импорта
name: 'ListProducts',
// Список принимаемых параметров
props: {
// Параметр для работы v-model
value: {
type: [String, Number],
default: 1,
},
// Количество выводимых товаров на странице
limit: {
type: [String, Number],
default: 20,
},
// Сортировка
sort: {
type: String,
default: 'id',
},
// Направление сортировки
dir: {
type: String,
default: 'asc',
},
},
// Внутренние данные компонента
data() {
return {
// Количество загруженных товаров
total: 0,
// Пустой массив для товаров
products: [],
}
},
// Функция загрузки для компонентов NuxtJs
async fetch() {
try {
// Определяем параметры запроса для контроллера
const params = {limit: this.limit, page: this.page, sort: this.sort, dir: this.dir}
// Делаем асинхронный запрос
const {data} = await this.$axios.get('web/products', {params})
// Сохраняем ответ
this.products = data.rows
this.total = data.total
// Генерируем событие об успешной загрузке с передачей полученных данных
// Это могут слушать родительские компоненты, чем мы позже и воспользуемся
this.$emit('load', data)
} catch (e) {
// Если есть ошибка - выводим в консоле
console.error(e)
}
},
// Дальше идут вычисляемые значения
computed: {
// Текущая страница вывода товаров
page: {
get() {
return this.value
},
set(newValue) {
// Уведомление родительского компонента об изменении
this.$emit('input', newValue)
},
},
},
// Следим за некоторыми параметрами
watch: {
// Новый запрос на сервер при изменении страницы
page() {
this.$fetch()
},
},
}
</script>
Итак, что здесь происходит? У нас есть разные параметры по умолчанию, из который номер страницы является v-model компонента, то есть он реактивен.
При этом сам компонент следит за номером страницы, и если он меняется - запускает функцию fetch(), которая отправляет новый запрос в API, используя указанные параметры, включая и страницу. Таким образом мы должны будем управлять постраничной загрузкой товаров.
Приводим src/site/pages/index.vue к самому минимальному виду:
<template>
<div>
<!-- вывод подключенного компонента вообще без параметров -->
<list-products />
</div>
</template>
<script>
// Импорт нашего компонента
import ListProducts from '../components/list-products'
export default {
// И подключение для работы
components: {ListProducts},
}
</script>
Появился совершенно минимальный вывод товаров без какого-либо функционала:
Но нам ведь нужно как-то пролистать весь каталог, верно? Для этого потребуется постраничная навигация, так что мы используем соответствующий компонент из BootstrapVue.
Опять меняем nuxt.config.js и добавляем PaginationPlugin в конфигурацию BootstrapVue:
Config.bootstrapVue.componentPlugins = ['LayoutPlugin', /*...*/ 'PaginationPlugin']
Еще нужно добавить и стили в файле src/site/assets/scss/index.css:
@import '~bootstrap/scss/pagination';
Теперь можно использовать пагинацию в index.vue:
<template>
<div class="mt-5">
<!--добавляем новые параметры вместе с событием на загрузку данных -->
<list-products v-model="page" :limit="limit" @load="onLoad" />
<!-- пагинация использьзует те же параметры -->
<b-pagination v-model="page" :total-rows="total" :per-page="limit" class="mt-3" />
</div>
</template>
<script>
import ListProducts from '../components/list-products'
export default {
components: {ListProducts},
// Добавились параметры по умолчанию
data() {
return {
page: 1,
limit: 20,
total: 0,
}
},
methods: {
// Это действие срабатывает при вызове $emit('load') в list-products,
// и передаёт нам загруженное, а мы из него сохраняем количество результатов
onLoad(data) {
this.total = data.total
},
},
}
</script>
Что здесь происходит? Во-первых, мы добавили пагинацию, которая так же, как и наш компонент, принимает v-model номер страницы. Когда мы указываем одно и то же значение обоим компонентам, мы их связываем и при изменении page в пагинации, оно же поменяется и в list-products, что вызовет новый запрос на сервер.
А во-вторых, мы слушаем событие load в компоненте товаров и сохраняем общее количество результатов, чтобы передать их в b-pagination. Таким образом пагинация узнаёт сколько всего у нас всего результатов и может отрисовать нужное количество страниц.
Итого:
- загружается первая страница в list-products
- b-pagination получает количество результатов и отрисовывает кнопки
- при нажатии на кнопки меняется v-model
- происходит новая загрузка list-products
Заключение
Несмотря на то, что мы изолировали логику загрузки и оформления товаров в отдельный компонент, мы можем получать из него все данные и менять параметры.
Нам для этого понадобилось совсем немного строк кода, все изменения на Github.
На следующем уроке добавим товарам картинки.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
437
20.06.2022 12:29:25
10 комментариев
NightRider
Спасибо! Темп уроков не может не радовать)
Из замечаний, наверное все же вместо src/site/index.vue имелось ввиду src/site/pages/index.vue
И код отличается в статье и гитхабе в файле list-products.vue - нету того же this.$emit('load', data) например.
В файле pages/index.vue со статьей вроде различия только в
Игорь
Рабочий вариант (при котором работает пагинация)
NightRider
На гитхабе как раз тот вариант, который я выше указал.
и
В нашем случае одно и то же.
Указание в функции {total} означает, что мы достаём только этот ключ из присланной переменной, потому что нам больше ничего не надо.
Это называется деструктурирующее присваивание. Код я пишу прямо вместе с заметками, поэтому могут быть небольшие расхождения.
Да, всё верно, спасибо.
Заметку поправил.
Василий, добрый день!
Подскажи, пожалуйста, как подключаются стили Bootstrap-vue?
Вроде бы все очевидно и просто, но что-то не работает и нагуглить быстро не вышло.
На странице с компонентом BootstrapVue Pagination нигде не сказано про стили.
Сделал с подключением компонента BootstrapVue Sidebar по аналогии:
@import '~bootstrap/scss/sidebar;
Но в консоли получил ошибку:
Самый простой способ решить все подобные проблемы - просто подключить вообще все стили
Но мне такое не нравится, потому что тогда будут собраны и ненужные стили, а это утяжелит страницу. Поэтому я что из родного Bootstap, что из Bootstrap-Vue, подключаю только нужное.
Компонента Sidebar у Bootstrap нет, значит нужны только стили из Bootstrap-Vue
Стили Bootstrap-Vue только дополняют таковые из оригинального Bootstrap, когда это нужно для функционала.
Спасибо большое! Тупанул, понял куда смотреть было нужно.
Такой подход одна из сильных сторон Vesp, если не будет порядка, то в последствии захлебнешься в мусоре.
Очень познавательно. Спасибо! Все получилось!
Возник единственный вопрос, но он, подозреваю, связан с моим незнанием темы. При проверке работы контроллера товаров, я получаю следующую картину. По адресу http://my-site.ru/api/web/products я вижу страницу с JSON где выводятся все 830 товаров в 100 категорях, но на этой странице (http://my-site.ru/api/web/products) Clockwork ничего не отображает.
Тут надо оговориться, что сайт у меня висит на VPS - т.е. фактически уже в продакшене, и отображается на домене. И я его перегенерирую после каждого обновления. Clockwork же я могу видеть на странице админки. Т.е. не на странице /api/web/products как у тебя, а на /admin/products . И там у меня Clockwork отображает только 84 категории. Но наверное это все пока не существенно, во всяком случае до тех пор, пока я не научусь в этом предметно разбираться.
Не знаю даже, что ответить. По идее работать должно везде, возможно просто глюк, который лечится перезагрузкой страницы.
У меня всё работает, проверил:
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
04.02.2025 19:27:08
Я таким давно не занимаюсь и с MODX не работаю.
Попробуйте обратиться к ребятам с modx.pro.
Василий Наумкин
23.12.2024 05:33:00
В MODX сначала создали проблему, автоматически генерируя адреса, а потом "решили" заморозкой.
Так ч...
Дмитрий
14.12.2024 09:10:38
Василий, прошу прощения, тупанул, не разобрался сразу. Фреймворк отличный! "Чистый лист" на vue, рис...
Василий Наумкин
05.12.2024 20:01:14
В итоге основная ошибка была в неправильном общем root в Nginx, из-за чего запросы не улетали на фай...
Василий Наумкин
01.07.2024 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Уровни подписки
Спасибо!
500 ₽ в месяц
Эта подписка ничего не даёт, просто возможность сказать спасибо за мои заметки. Подписчики отмечаются зелёненьким цветом в комментариях.
Большое спасибо!
1 000 ₽ в месяц
И эта подписка не даёт ничего, кроме оранжевого цвета в комментариях и возможности сказать спасибо, но уже большое!