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
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:
@@ -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<UserDto[]>([])
|
||||
const lectures = ref<LectureDto[]>([])
|
||||
const reviews = ref<ReviewDto[]>([])
|
||||
const pendingReviewsCount = ref(0)
|
||||
const syncStatus = ref<SyncStatusDto | null>(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
|
||||
})
|
||||
</script>
|
||||
@@ -41,7 +44,12 @@ onMounted(async () => {
|
||||
<StatsWidget label="Пользователей" :value="users.length" icon="users" color="green" />
|
||||
<StatsWidget label="Лекций" :value="lectures.length" icon="books" color="aqua" />
|
||||
<StatsWidget label="Записей" :value="enrollmentCount" icon="calendar-event" color="orange" />
|
||||
<StatsWidget label="Отзывов в LLM" :value="reviews.length" icon="message-circle" color="purple" />
|
||||
<StatsWidget
|
||||
label="Отзывов в LLM"
|
||||
:value="pendingReviewsCount"
|
||||
icon="message-circle"
|
||||
color="purple"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
@@ -49,12 +57,14 @@ onMounted(async () => {
|
||||
<div class="section-title">Состояние синхронизации расписания</div>
|
||||
<StatusBadge :status="syncStatus?.status ?? 'pending'" />
|
||||
<div class="sync-meta">{{ syncMeta }}</div>
|
||||
<div class="sync-error" v-if="syncStatus?.lastResult?.error">Ошибка: {{ syncStatus.lastResult.error }}</div>
|
||||
<div class="sync-error" v-if="syncStatus?.lastResult?.error">
|
||||
Ошибка: {{ syncStatus.lastResult.error }}
|
||||
</div>
|
||||
</GlassCard>
|
||||
<GlassCard>
|
||||
<div class="section-title">Очередь LLM-анализа</div>
|
||||
<div class="queue-meta">В очереди: {{ reviews.length }} отзывов</div>
|
||||
<ProgressBar :value="Math.min(reviews.length * 10, 100)" :max="100" />
|
||||
<div class="queue-meta">В очереди: {{ pendingReviewsCount }} отзывов</div>
|
||||
<ProgressBar :value="Math.min(pendingReviewsCount * 10, 100)" :max="100" />
|
||||
<div class="queue-status">Следующая проверка через 12 минут</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
@@ -62,11 +72,89 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-dashboard { display: flex; flex-direction: column; gap: 18px; }
|
||||
.stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
|
||||
.sync-meta { font-size: 12px; color: var(--color-text-secondary); margin-top: 6px; }
|
||||
.sync-error { font-size: 12px; color: var(--color-error); margin-top: 8px; }
|
||||
.queue-meta { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 8px; }
|
||||
.queue-status { font-size: 12px; color: var(--color-text-secondary); margin-top: 6px; }
|
||||
.admin-dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.bar-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.bar {
|
||||
background: var(--color-black-a08);
|
||||
border-radius: 6px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bar-fill {
|
||||
background: var(--gradient-progress-success);
|
||||
height: 100%;
|
||||
}
|
||||
.percent {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.metric {
|
||||
margin-bottom: 10px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.activity {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.day-bar {
|
||||
width: 16px;
|
||||
background: var(--gradient-bar-neutral-vertical);
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
.sync-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.sync-error {
|
||||
font-size: 12px;
|
||||
color: var(--color-error);
|
||||
margin-top: 8px;
|
||||
}
|
||||
.queue-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.queue-status {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user