Разработка компонентов отзывов и рейтингов на Vue.js для 1С-Битрикс
Стандартный подход к отзывам на Битрикс — PHP-компонент с полной перезагрузкой страницы при добавлении отзыва. Пользователь заполняет форму, нажимает «Отправить», страница обновляется. Для 2014 года это нормально, для современного e-commerce — нет. Vue.js-компонент решает задачу иначе: форма, список отзывов, пагинация, сортировка — всё работает без перезагрузки, с мгновенной обратной связью.
Архитектура интеграции
Битрикс остаётся бэкендом и источником правды. Vue работает в браузере поверх PHP-страницы. Интеграция происходит через:
- PHP передаёт начальные данные в Vue — первые N отзывов рендерятся на сервере (или передаются как JSON), Vue «гидратирует» их и берёт управление
- REST API — все последующие действия (загрузить ещё, добавить отзыв, поставить оценку полезности) идут через AJAX к PHP-контроллеру
Серверная сторона: PHP-контроллер
Класс контроллера на основе \Bitrix\Main\Engine\Controller:
// /local/components/custom/reviews/controller.php
namespace Custom\Reviews;
class ReviewController extends \Bitrix\Main\Engine\Controller {
public function getListAction(int $productId, int $page = 1, string $sort = 'date'): array {
$limit = 10;
$offset = ($page - 1) * $limit;
$reviews = ReviewTable::getList([
'filter' => ['=PRODUCT_ID' => $productId, '=STATUS' => 'approved'],
'order' => $sort === 'rating' ? ['RATING' => 'DESC'] : ['DATE_CREATE' => 'DESC'],
'limit' => $limit,
'offset' => $offset,
'select' => ['ID', 'AUTHOR_NAME', 'RATING', 'TITLE', 'BODY', 'DATE_CREATE', 'HELPFUL_YES'],
])->fetchAll();
$total = ReviewTable::getCount(['=PRODUCT_ID' => $productId, '=STATUS' => 'approved']);
return ['reviews' => $reviews, 'total' => $total, 'page' => $page];
}
public function addAction(int $productId, int $rating, string $title, string $body): array {
global $USER;
// Проверка: пользователь купил этот товар
if (!$this->hasPurchased($USER->GetID(), $productId)) {
return $this->addError(new \Bitrix\Main\Error('Отзыв доступен только покупателям'));
}
// ...сохранение отзыва со статусом 'pending'
return ['success' => true, 'message' => 'Отзыв отправлен на модерацию'];
}
public function voteHelpfulAction(int $reviewId, string $type): array {
// type: 'yes' | 'no'
// ...
}
}
Контроллер регистрируется в init.php:
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'main', 'OnProlog',
fn() => \Custom\Reviews\ReviewController::register()
);
Vue-компонент: Reviews.vue
<template>
<div class="reviews-widget">
<!-- Сводный рейтинг -->
<div class="rating-summary">
<StarRating :value="summary.avg" :readonly="true" />
<span>{{ summary.avg.toFixed(1) }} из 5 ({{ summary.total }} отзывов)</span>
<RatingBars :distribution="summary.distribution" />
</div>
<!-- Форма нового отзыва -->
<ReviewForm v-if="canReview" @submitted="onReviewSubmitted" />
<!-- Сортировка -->
<select v-model="sort" @change="loadReviews(1)">
<option value="date">По дате</option>
<option value="rating">По рейтингу</option>
<option value="helpful">По полезности</option>
</select>
<!-- Список отзывов -->
<ReviewCard
v-for="review in reviews"
:key="review.ID"
:review="review"
@helpful-vote="voteHelpful"
/>
<!-- Пагинация -->
<button v-if="hasMore" @click="loadMore" :disabled="loading">
{{ loading ? 'Загрузка...' : 'Показать ещё' }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { Review, ReviewSummary } from './types'
const props = defineProps<{ productId: number; initialReviews: Review[] }>()
const reviews = ref<Review[]>(props.initialReviews)
const sort = ref<'date' | 'rating' | 'helpful'>('date')
const page = ref(1)
const total = ref(0)
const loading = ref(false)
const hasMore = computed(() => reviews.value.length < total.value)
async function loadReviews(resetPage = 1) {
loading.value = true
const res = await fetch(`/local/api/reviews/?product=${props.productId}&page=${resetPage}&sort=${sort.value}`)
const data = await res.json()
reviews.value = resetPage === 1 ? data.reviews : [...reviews.value, ...data.reviews]
total.value = data.total
page.value = resetPage
loading.value = false
}
async function loadMore() {
await loadReviews(page.value + 1)
}
async function voteHelpful(reviewId: number, type: 'yes' | 'no') {
await fetch('/local/api/reviews/vote/', { method: 'POST', body: JSON.stringify({ id: reviewId, type }) })
// оптимистичное обновление счётчика
const review = reviews.value.find(r => r.ID === reviewId)
if (review) review.HELPFUL_YES += type === 'yes' ? 1 : 0
}
onMounted(() => { if (!props.initialReviews.length) loadReviews() })
</script>
StarRating.vue: компонент звёздочного рейтинга
<template>
<div class="star-rating" :class="{ readonly }">
<span
v-for="star in 5"
:key="star"
:class="['star', star <= hovered || star <= modelValue ? 'filled' : 'empty']"
@mouseenter="!readonly && (hovered = star)"
@mouseleave="!readonly && (hovered = 0)"
@click="!readonly && $emit('update:modelValue', star)"
>★</span>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ modelValue: number; readonly?: boolean }>()
const hovered = ref(0)
</script>
Интеграция с PHP-шаблоном
Компонент монтируется в шаблоне детальной страницы товара:
// template.php
$initialReviews = json_encode($arResult['REVIEWS'] ?? [], JSON_UNESCAPED_UNICODE);
?>
<div id="reviews-app"
data-product-id="<?= (int)$arResult['ID'] ?>"
data-initial-reviews="<?= htmlspecialchars($initialReviews) ?>">
</div>
<script>
// Инициализация Vue-приложения
import { createApp } from 'vue'
import ReviewsWidget from './Reviews.vue'
const el = document.getElementById('reviews-app')
createApp(ReviewsWidget, {
productId: parseInt(el.dataset.productId),
initialReviews: JSON.parse(el.dataset.initialReviews),
}).mount(el)
</script>
Начальные отзывы (первые 10) рендерятся на сервере и передаются в data-атрибуте — пользователь видит их мгновенно без дополнительного запроса.
Модерация в интерфейсе
Для модераторов — дополнительный Vue-компонент в разделе администрирования. Очередь отзывов, ожидающих одобрения, с кнопками «Одобрить» / «Отклонить» и полем причины отклонения. Изменение статуса — через тот же API-контроллер с проверкой прав ($USER->IsAdmin()).
Сроки
| Объём | Что входит | Срок |
|---|---|---|
| Базовые отзывы | Форма, список, звёзды, AJAX | 2–3 недели |
| Полноценный виджет | + пагинация, сортировка, голосование | 3–5 недель |
| + Фото, модерация, Schema.org | + UGC, админ-интерфейс, разметка | +1–2 недели |
Vue-компонент отзывов — инвестиция в UX. Возможность добавить отзыв без перезагрузки страницы снижает bounce rate после отправки формы и повышает количество оставленных отзывов.







