feat: Добавил постраничную загрузку отзывов
Frontend CI / build-and-check (push) Failing after 5m10s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 15s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 18s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 32s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 13s

This commit is contained in:
2026-05-18 02:54:28 +03:00
parent b984d29c50
commit 934682f035
5 changed files with 366 additions and 98 deletions
+117 -57
View File
@@ -35,6 +35,10 @@ const ratingLabel: Record<string, string> = {
const reviews = ref<ReviewDto[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(20)
const totalCount = ref(0)
const totalPages = ref(0)
const reanalyzingId = ref<number | null>(null)
const error = ref('')
@@ -63,6 +67,13 @@ const rows = computed(() =>
})),
)
const pageStart = computed(() =>
totalCount.value === 0 ? 0 : (page.value - 1) * pageSize.value + 1,
)
const pageEnd = computed(() => Math.min(page.value * pageSize.value, totalCount.value))
const canGoPrev = computed(() => page.value > 1)
const canGoNext = computed(() => page.value < totalPages.value)
function formatQuality(value: number | null | undefined) {
if (value === null || value === undefined) return '—'
return Number(value).toFixed(2)
@@ -72,7 +83,13 @@ async function fetchReviews() {
loading.value = true
error.value = ''
try {
reviews.value = await reviewsApi.list({ PageSize: 100 })
const result = await reviewsApi.listPage({
Page: page.value,
PageSize: pageSize.value,
})
reviews.value = result.items
totalCount.value = result.totalCount
totalPages.value = result.totalPages
} catch (err) {
error.value = err instanceof Error ? err.message : 'Не удалось загрузить отзывы.'
} finally {
@@ -80,6 +97,12 @@ async function fetchReviews() {
}
}
async function goToPage(nextPage: number) {
if (nextPage < 1 || (totalPages.value && nextPage > totalPages.value)) return
page.value = nextPage
await fetchReviews()
}
async function reanalyze(id: number) {
reanalyzingId.value = id
try {
@@ -114,70 +137,88 @@ onMounted(fetchReviews)
title="Отзывов нет"
subtitle="Пока нет ни одного отзыва."
/>
<DataTable v-else :columns="columns" :rows="rows">
<template #text="{ value }">
<span class="review-text" :title="value">{{ value }}</span>
</template>
<template #status="{ value, row }">
<StatusBadge :status="value" />
<span class="raw-status">{{ row.rawStatus }}</span>
</template>
<template #sentiment="{ value }">
<span
class="badge"
:class="
value === 'Positive'
? 'badge-green'
: value === 'Negative'
? 'badge-red'
: 'badge-orange'
"
>
{{ value }}
</span>
</template>
<template #quality="{ value }">
<span
:class="
(value ?? 0) >= 0.7
? 'badge badge-green'
: (value ?? 0) >= 0.4
? 'badge badge-orange'
: 'badge badge-red'
"
>
{{ formatQuality(value) }}
</span>
</template>
<template #informativeTags="{ row }">
<div class="tags-cell">
<div v-else class="table-section">
<div class="pagination-bar">
<span> Показаны {{ pageStart }}{{ pageEnd }} из {{ totalCount }} отзывов </span>
<div class="pagination-actions">
<button class="btn-ghost" :disabled="loading || !canGoPrev" @click="goToPage(page - 1)">
Назад
</button>
<span>Страница {{ page }} из {{ totalPages || 1 }}</span>
<button class="btn-ghost" :disabled="loading || !canGoNext" @click="goToPage(page + 1)">
Вперёд
</button>
</div>
</div>
<DataTable :columns="columns" :rows="rows">
<template #text="{ value }">
<span class="review-text" :title="value">{{ value }}</span>
</template>
<template #status="{ value, row }">
<StatusBadge :status="value" />
<span class="raw-status">{{ row.rawStatus }}</span>
</template>
<template #sentiment="{ value }">
<span
class="badge"
:class="
row.informative
value === 'Positive'
? 'badge-green'
: row.informative === false
: value === 'Negative'
? 'badge-red'
: 'badge-gray'
: 'badge-orange'
"
>
{{
row.informative === null || row.informative === undefined
? '—'
: row.informative
? 'Informative'
: 'Not informative'
}}
{{ value }}
</span>
<span v-for="tag in row.tags" :key="tag" class="badge badge-blue">{{ tag }}</span>
</div>
</template>
<template #actions="{ row }">
<button class="btn-ghost" :disabled="reanalyzingId === row.id" @click="reanalyze(row.id)">
{{ reanalyzingId === row.id ? 'Запускаем...' : 'Повторить анализ' }}
</button>
</template>
</DataTable>
</template>
<template #quality="{ value }">
<span
:class="
(value ?? 0) >= 0.7
? 'badge badge-green'
: (value ?? 0) >= 0.4
? 'badge badge-orange'
: 'badge badge-red'
"
>
{{ formatQuality(value) }}
</span>
</template>
<template #informativeTags="{ row }">
<div class="tags-cell">
<span
class="badge"
:class="
row.informative
? 'badge-green'
: row.informative === false
? 'badge-red'
: 'badge-gray'
"
>
{{
row.informative === null || row.informative === undefined
? '—'
: row.informative
? 'Informative'
: 'Not informative'
}}
</span>
<span v-for="tag in row.tags" :key="tag" class="badge badge-blue">{{ tag }}</span>
</div>
</template>
<template #actions="{ row }">
<button
class="btn-ghost"
:disabled="reanalyzingId === row.id"
@click="reanalyze(row.id)"
>
{{ reanalyzingId === row.id ? 'Запускаем...' : 'Повторить анализ' }}
</button>
</template>
</DataTable>
</div>
</GlassCard>
</div>
</template>
@@ -213,6 +254,25 @@ onMounted(fetchReviews)
color: var(--color-text-secondary);
font-size: 11px;
}
.table-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.pagination-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
color: var(--color-text-secondary);
font-size: 13px;
}
.pagination-actions {
display: flex;
align-items: center;
gap: 8px;
}
.tags-cell {
display: flex;
flex-wrap: wrap;