diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 013b28e..68125ff 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -10,6 +10,7 @@ import type { LocationDto, PagedResult, ReviewDto, + ReviewQuery, SyncResultDto, SyncScheduleRequest, SyncStatusDto, @@ -115,19 +116,53 @@ export const notificationsApi = { markAllRead: () => apiRequest('/notifications/read-all', { method: 'PATCH' }), } +function normalizePagedResult( + payload: PagedResult | T[] | undefined, + query: { Page?: number; PageSize?: number } = {}, +): PagedResult { + if (!Array.isArray(payload) && payload) return payload + + const items = payload ?? [] + const page = query.Page ?? 1 + const pageSize = query.PageSize ?? items.length + const totalPages = pageSize > 0 ? Math.ceil(items.length / pageSize) : 0 + + return { + items, + totalCount: items.length, + page, + pageSize, + totalPages, + } +} + +async function listReviewsPage(query: ReviewQuery = {}) { + const payload = await apiRequest | ReviewDto[]>('/reviews', { + query: query as Record, + }) + return normalizePagedResult(payload, query) +} + +async function listPendingReviewsPage(query: ReviewQuery = {}) { + const payload = await apiRequest | ReviewDto[]>('/reviews/pending', { + query: query as Record, + }) + return normalizePagedResult(payload, query) +} + export const reviewsApi = { create: (lectureId: string | number, rating: 'Like' | 'Neutral' | 'Dislike', text: string) => apiRequest('/reviews', { method: 'POST', body: JSON.stringify({ lectureId: Number(lectureId), rating, text }), }), - async list(query: Record = { PageSize: 100 }) { - const payload = await apiRequest | ReviewDto[]>('/reviews', { query }) - return extractItems(payload) + listPage: listReviewsPage, + async list(query: ReviewQuery = { PageSize: 100 }) { + return (await listReviewsPage(query)).items }, - async pending() { - const payload = await apiRequest | ReviewDto[]>('/reviews/pending') - return extractItems(payload) + pendingPage: listPendingReviewsPage, + async pending(query: ReviewQuery = { PageSize: 100 }) { + return (await listPendingReviewsPage(query)).items }, reanalyze: (id: string | number) => apiRequest(`/reviews/${id}/reanalyze`, { method: 'POST' }), diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 940cc74..c3a2afc 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -99,6 +99,11 @@ export interface CreateLectureRequest { onlineUrl?: string | null } +export interface ReviewQuery { + Page?: number + PageSize?: number +} + export interface ReviewDto { id: number lectureId: number diff --git a/frontend/src/views/admin/AdminDashboardView.vue b/frontend/src/views/admin/AdminDashboardView.vue index 06474e4..3f167cd 100644 --- a/frontend/src/views/admin/AdminDashboardView.vue +++ b/frontend/src/views/admin/AdminDashboardView.vue @@ -5,14 +5,16 @@ import StatsWidget from '@/components/ui/StatsWidget.vue' import ProgressBar from '@/components/ui/ProgressBar.vue' import StatusBadge from '@/components/ui/StatusBadge.vue' import { lecturesApi, reviewsApi, syncApi, usersApi } from '@/api' -import type { LectureDto, ReviewDto, SyncStatusDto, UserDto } from '@/api/types' +import type { LectureDto, SyncStatusDto, UserDto } from '@/api/types' const users = ref([]) const lectures = ref([]) -const reviews = ref([]) +const pendingReviewsCount = ref(0) const syncStatus = ref(null) -const enrollmentCount = computed(() => lectures.value.reduce((sum, lecture) => sum + lecture.enrollmentsCount, 0)) +const enrollmentCount = computed(() => + lectures.value.reduce((sum, lecture) => sum + lecture.enrollmentsCount, 0), +) const syncMeta = computed(() => syncStatus.value?.lastSyncAt ? `Последняя синхронизация: ${new Date(syncStatus.value.lastSyncAt).toLocaleString('ru-RU')}` @@ -23,12 +25,13 @@ onMounted(async () => { const [usersResult, lecturesResult, reviewsResult, syncResult] = await Promise.allSettled([ usersApi.list({ PageSize: 100 }), lecturesApi.list({ PageSize: 100 }), - reviewsApi.pending(), + reviewsApi.pendingPage({ Page: 1, PageSize: 1 }), syncApi.status(), ]) if (usersResult.status === 'fulfilled') users.value = usersResult.value if (lecturesResult.status === 'fulfilled') lectures.value = lecturesResult.value - if (reviewsResult.status === 'fulfilled') reviews.value = reviewsResult.value + if (reviewsResult.status === 'fulfilled') + pendingReviewsCount.value = reviewsResult.value.totalCount if (syncResult.status === 'fulfilled') syncStatus.value = syncResult.value }) @@ -41,7 +44,12 @@ onMounted(async () => { - +
@@ -49,12 +57,14 @@ onMounted(async () => {
Состояние синхронизации расписания
{{ syncMeta }}
-
Ошибка: {{ syncStatus.lastResult.error }}
+
+ Ошибка: {{ syncStatus.lastResult.error }} +
Очередь LLM-анализа
-
В очереди: {{ reviews.length }} отзывов
- +
В очереди: {{ pendingReviewsCount }} отзывов
+
Следующая проверка через 12 минут
@@ -62,11 +72,89 @@ onMounted(async () => { diff --git a/frontend/src/views/admin/AdminLLMQueueView.vue b/frontend/src/views/admin/AdminLLMQueueView.vue index da7cfd2..0acfeca 100644 --- a/frontend/src/views/admin/AdminLLMQueueView.vue +++ b/frontend/src/views/admin/AdminLLMQueueView.vue @@ -22,10 +22,14 @@ const columns = [ const reviews = ref([]) const loading = ref(false) +const page = ref(1) +const pageSize = ref(20) +const totalCount = ref(0) +const totalPages = ref(0) const error = ref('') const rows = computed(() => - reviews.value.map(review => ({ + reviews.value.map((review) => ({ id: review.id, lecture: review.lectureId, student: review.userName, @@ -37,11 +41,24 @@ 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) + async function fetchPending() { loading.value = true error.value = '' try { - reviews.value = (await reviewsApi.pending()).map(mapApiReview) + const result = await reviewsApi.pendingPage({ + Page: page.value, + PageSize: pageSize.value, + }) + reviews.value = result.items.map(mapApiReview) + totalCount.value = result.totalCount + totalPages.value = result.totalPages } catch (err) { error.value = err instanceof Error ? err.message : 'Не удалось загрузить очередь LLM.' } finally { @@ -49,6 +66,12 @@ async function fetchPending() { } } +async function goToPage(nextPage: number) { + if (nextPage < 1 || (totalPages.value && nextPage > totalPages.value)) return + page.value = nextPage + await fetchPending() +} + async function reanalyze(id: string) { await reviewsApi.reanalyze(id) await fetchPending() @@ -61,30 +84,87 @@ onMounted(fetchPending)

Очередь LLM-анализа отзывов

- +
- - - - - - + +
+
+ В очереди {{ totalCount }} отзывов; показаны {{ pageStart }}–{{ pageEnd }} +
+ + Страница {{ page }} из {{ totalPages || 1 }} + +
+
+ + + + + +
diff --git a/frontend/src/views/admin/AdminReviewsView.vue b/frontend/src/views/admin/AdminReviewsView.vue index 807c003..32675bb 100644 --- a/frontend/src/views/admin/AdminReviewsView.vue +++ b/frontend/src/views/admin/AdminReviewsView.vue @@ -35,6 +35,10 @@ const ratingLabel: Record = { const reviews = ref([]) const loading = ref(false) +const page = ref(1) +const pageSize = ref(20) +const totalCount = ref(0) +const totalPages = ref(0) const reanalyzingId = ref(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="Пока нет ни одного отзыва." /> - - - - - - + + + + + @@ -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;