Оформление товаров и категорий
Сегодня мы оформим страницы товаров и категорий.
Думаю, на этом работа по выводу каталога будет окончена и можно будет переключиться на корзину и оформление заказов.
Как вы, наверное, помните, мы написали компонент для вывода всех товаров и теперь можем его легко расширить для вывода товаров конкретной категории.
Делается это добавлением нового параметра, и при его наличии, изменением адреса запроса на контроллер товаров категории, который мы написали в прошлом уроке.
Редактируем frontend/src/site/components/list-products.vue:
<script>
export default {
name: 'ListProducts',
props: {
// Указываем наш новый параметр
category: {
type: String,
default: null,
},
// ...
},
async fetch() {
// ...
// меняем адрес для запросов на computed переменную
const {data} = await this.$axios.get(this.url, {params})
// ...
},
computed: {
// И назначаем новую переменную для запроса
// Она зависит от указания alias категории
url() {
return this.category ? 'web/categories/' + this.category + '/products' : 'web/products'
},
// ...
},
</script>
Теперь мы можем просто вызвать наш компонент на странице категории frontend/src/site/pages/_category/index.vue:
<template>
<div class="pt-5">
<!-- Напрямую указываем категорию из параметров маршрута -->
<list-products :category="$route.params.category" />
</div>
</template>
<script>
import ListProducts from '../../components/list-products'
export default {
components: {ListProducts},
// Проверяем наличие категории в параметрах маршрута
validate({params}) {
return Boolean(params.category)
},
}
</script>
Но хотелось бы добавить те же функции, что и на главной странице: пагинацию, сортировку и смену внешнего вида. Только писать это всё заново совсем не хочется, да и вообще, дублировать функционал - всегда плохая идея.
Давайте вынесем верхнюю панель с кнопочками с главной страницы в отдельный компонент, чтобы его можно было использовать и на странице категории.
Компонент управления каталогом
Создаём frontend/src/site/components/list-products-actions.vue:
<template>
<b-row class="mt-3 mb-3">
<!-- Переносим всё содержимое этого тега с главной страницы
без изменений -->
</b-row>
</template>
<script>
export default {
// Имя компонента
name: 'ListProductsActions',
props: {
// v-model у нас будет вариант оформления товаров
// списком или плиткой
value: {
type: Boolean,
required: true,
},
// Дополнительные параметры
sort: {
type: String,
required: true,
},
dir: {
type: String,
required: true,
},
},
computed: {
// Наш v-model, который будет использоваться в template,
// а изменения будет слушать родительская страница
listView: {
get() {
return this.value
},
set(newValue) {
this.$emit('input', newValue)
},
},
},
methods: {
// Этот метод переезжает в иземенённом виде
onSort(key) {
// Так как sort и dir страли параметрами, нельзя их менять напрямую,
// как и v-model, теперь мы будем менять их через события
if (this.sort === key) {
this.$emit('update:dir', this.dir === 'asc' ? 'desc' : 'asc')
} else {
this.$emit('update:sort', key)
this.$emit('update:dir', 'asc')
}
},
},
}
У компонентов Vue 2 может быть только один v-model, и в него мы поместили переключение внешнего вида оформления. Но параметры сортировки тоже будут обновляться внутри компонента, и за ними нужно можно следить через модификатор .sync.
Сейчас покажу, как именно. Редактируем frontend/src/site/pages/index.vue:
<template>
<div>
<!-- Наш новый компонент с кнопками, обратите внимание
на вызов параметров с модификаторами .sync="..." -
это и есть слежка за их изменениями -->
<list-products-actions v-model="listView" :sort.sync="sort" :dir.sync="dir" />
<!-- Тут всё без изменений -->
<list-products v-model="page" :limit="limit" :sort="sort" :dir="dir" :list-view="listView" @load="onLoad" />
<b-pagination v-model="page" :total-rows="total" :per-page="limit" class="mt-5" />
</div>
</template>
<script>
import ListProducts from '../components/list-products'
// Импорт нового компонента
import ListProductsActions from '../components/list-products-actions'
export default {
name: 'IndexPage',
// Регистрация нового компонента
components: {ListProductsActions, ListProducts},
data() {
// ...
},
computed: {
// ...
},
methods: {
onLoad({total}) {
this.total = total
},
// Метод onSort удаляем
},
}
</script>
Еще раз насчёт слежения за передаваемыми параметрами: общий смысл в том, что если параметр может быть изменён внутри компонента, он обновляется через событие снаружи.
Обычно меняется только 1 параметр и тогда достаточно использовать v-model, но в случае с нашим новым компонентом таких параметра сразу 3.
Поэтому мы используем один v-model, как обычно, а 2 других параметра слушаем через модификатор .sync.
Насколько я понимаю, модификатор .sync появился только в Vue 2.3+, а до этого был только v-model, и теперь он стал не особо нужным. Но лично я предпочитаю его использовать для выразительности компонента, чтобы сразу было понятно где передаётся изменяющийся параметр. Случаев с несколькими меняющимися параметрами в моей практике не так уж и много.
Теперь нам нужно вызвать все 3 компонента на странице категории frontend/src/site/pages/_category/index.vue точно так же, как и на главной странице, с одним единственным отличием - у <list-products /> должен быть указан параметр :category="...". Думаю, вы прекрасно с этим справитесь самостоятельно, так что я не буду копировать код страницы сюда.
В принципе, можно было бы даже просто расширить главную страницу и поменять только тег template, но мне кажется, что на главной потом будет что-то ещё, помимо вывода каталога.
Оформление товара
На прошлом занятии мы написали выборку основных данных товара, давайте теперь добавим в них еще и галерею с категорией.
Редактируем контроллер core/src/Controllers/Web/Category/Products.php:
// Меняем запрос перед получением 1 модели
protected function beforeGet(Builder $c): Builder
{
$c->where('active', true);
// Выбираем категорию
$c->with('category:id,alias,title');
// И активные файлы, в порядке сортировки
$c->with('productFiles', static function (HasMany $c) {
$c->where('active', true);
$c->orderBy('rank');
$c->select('product_id', 'file_id');
$c->with('file:id,updated_at');
});
return $c;
}
Редактируем frontend/src/site/pages/_category/_product/index.vue:
<template>
<div class="mt-5">
<b-link :to="{name: 'index'}">← Вернуться назад</b-link>
<b-row class="mt-3">
<b-col v-if="product.product_files.length" md="6" class="mt-3 mt-md-0 order-2 order-md-1">
<!-- Карусель BootstrapVue-->
<b-carousel controls indicators>
<b-carousel-slide v-for="image in product.product_files" :key="image.file_id">
<!-- В карусели указываем собственное оформление картинок -->
<template #img>
<!-- Не забываем про экраны высокой плотности, указываем srcset -->
<b-img
:src="$image(image.file, {w: thumbWidth, h: thumbHeight, fit: 'crop'})"
:srcset="$image(image.file, {w: thumbWidth * 2, h: thumbHeight * 2, fit: 'crop'}) + ' 2x'"
:width="thumbWidth"
:height="thumbHeight"
alt=""
/>
</template>
</b-carousel-slide>
</b-carousel>
</b-col>
<b-col md="6" class="order-1 order-md-2">
<!-- Тут всё стандартно-->
<h1>{{ product.title }}</h1>
<div class="mt-3">{{ product.description }}</div>
<div class="font-weight-bold mt-3">{{ product.price }} руб.</div>
<div class="mt-3">
<!-- Кнопка пока ничего не делает -->
<b-button variant="primary " disabled>В корзину!</b-button>
</div>
</b-col>
</b-row>
</div>
</template>
<script>
export default {
//...
data() {
return {
// Прописываем размеры для картинок
// В будущем их можно сделать динамическими и
// рассчитывать в зависимости от размера экрана
thumbWidth: 540,
thumbHeight: 360,
product: {
// Прописываем ключи, чтобы phpStorm их видел и подсказывал
category: {},
product_files: [],
},
}
},
}
</script>
Для вывода картинок мы используем карусель из BootstrapVue, которую нужно подключить в компонентах frontend/src/site/nuxt.config.js
Config.bootstrapVue.componentPlugins = [
// ...
'CarouselPlugin',
]
И добавить оформление в ``
/* Подключение всех стилей я зыбыл на одном из прошлых занятий -
его нужно закомментировать */
/* @import '~bootstrap/scss/bootstrap'; */
/* Подключаем стили отображения карусели */
@import '~bootstrap/scss/carousel';
@import '~bootstrap/scss/utilities/screenreaders';
/* И стили нужные для отображения загрузки каталога */
@import '~bootstrap/scss/spinners';
@import '~bootstrap/scss/utilities/position';
@import '~bootstrap/scss/utilities/background';
Хлебные крошки
Ссылка "вернуться назад" выглядит не очень, давайте вместо неё напишем свой компонент хлебных крошек, который будем вызывать на страницах категории и товара.
Создаём файл frontend/src/site/components/breadcrumbs.vue:
<template>
<b-breadcrumb>
<!-- Иконку на главную страницу выводим всегда -->
<b-breadcrumb-item :to="{name: 'index'}">
<fa icon="home" />
</b-breadcrumb-item>
<!-- Ссылка на категорию тоже, но активна она только
при наличии товара -->
<b-breadcrumb-item :to="{name: 'category', params: {category: category.alias}}" :active="!product">
{{ category.title }}
</b-breadcrumb-item>
<!-- А вот ссылку на товар выводим только, если его передали -->
<b-breadcrumb-item
v-if="product"
:to="{name: 'category-product', params: {category: category.alias, product: product.alias}}"
active
>
{{ product.title }}
</b-breadcrumb-item>
</b-breadcrumb>
</template>
<script>
export default {
name: 'Breadcrumbs',
// Параметров всего 2
props: {
// Обязательная категория
category: {
type: Object,
required: true,
},
// И опциональный товар
product: {
type: Object,
default: null,
},
},
}
</script>
Как видите, я использую еще один компонент из BootstrapVue, который нам нужно подключить в frontend/src/site/nuxt.config.js:
// Заодно подключаем работу с FontAwesome
Config.buildModules = [/*...*/, '@nuxtjs/fontawesome']
// Основные настройки
Config.fontawesome = {
addCss: false,
component: 'fa',
icons: {
// И загрузка ровно одной иконки, которая используется в крошках
solid: ['faHome'],
},
}
// А вот и компонент хлебных крошек
Config.bootstrapVue.componentPlugins = [
// ...
'BreadcrumbPlugin',
]
И, конечно, добавляем нужные стили в frontend/src/site/assets/scss/index.scss из установленных библиотек в node_modules:
@import '~bootstrap/scss/breadcrumb';
@import '@fortawesome/fontawesome-svg-core/styles.css';
Вызываем крошки на странице товара frontend/src/site/pages/_category/_product/index.vue как обычно - импортируем и регистрируем в script, а затем указываем в template:
<template>
<div class="mt-5">
<breadcrumbs :category="product.category" :product="product" />
<!-- ... -->
</div>
</template>
А вот на странице категории нам еще нужно добавить загрузку данных с сервера.
Потому что на данный момент мы ничего, кроме переданного на страницу alias не знаем, а нам нужен ещё и title. Так в файле frontend/src/site/pages/_category/index.vue добавляем еще asyncData():
<template>
<div class="pt-5">
<breadcrumbs :category="category" />
<!-- ... -->
</div>
</template>
<script>
export default {
async asyncData({app, params, error}) {
try {
const {data} = await app.$axios.get('web/categories/' + params.category)
return {category: data}
} catch (e) {
error({statusCode: e.response.status, message: e.message})
}
},
data() {
return {
// ...
// Место для хранения загруженных данных
category: {},
}
},
}
</script>
На главной странице крошки не выводим.
Заключение
Теперь у нас работает нафигация по каталогу. Вы можете перейти на страницу товара с главной, а затем выйти в его категорию и обратно на главную.
Единственное, что все эти переходы выглядят как-то резко, без анимации. Поэтому давайте добавим стили и для переходов, как в админке сделано по умолчанию.
Прописываем новые стили в frontend/src/site/assets/scss/index.scss:
.page-enter-active,
.page-leave-active,
.layout-enter-active,
.layout-leave-active {
transition: opacity 0.1s
}
.page-enter,
.page-leave-active,
.layout-enter,
.layout-leave-active {
opacity: 0
}
Подробнее про возможности анимирования переходов между страницами можно почитать в документации NuxtJs.
Все изменения, как обычно, одним коммитом на Github.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
301
29.06.2022, 15:19:51
3 комментария
08.01.2023, 22:08:52
Василий, добрый день!
Решил к кнопкам "Добавить в корзину" добавить Bootstrap Toast, вспомнил, что у нас уже установлен для этих целей другой @nuxtjs/toast.
Вопрос: почему выбран @nuxtjs/toast, а не Bootstrap Toas, чем он лучше?
09.01.2023, 08:35:23
У меня не получилось нормально вызывать бутстраповский в перехватчике Axios. Это давно было, сейчас возможно уже поправили.
А так эти компоненты все плюс-минус одинаковые.
09.01.2023, 14:35:02
Ясно, спасибо!
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
Василий, спасибо!
Извини, тупанул.