This commit is contained in:
2026-09-18 03:20:50 +07:00
parent 319d340b62
commit 154de679af
77 changed files with 327 additions and 88 deletions
+76
View File
@@ -9,6 +9,7 @@ import {
NInputNumber,
NModal,
NQrCode,
NRate,
NSelect,
NSpace,
NText,
@@ -22,6 +23,7 @@ import { getBinaryClient } from '@/api/binary_client'
import type {
Application,
Game,
GameReview,
MapItem,
Question,
ScreenCommand,
@@ -519,6 +521,10 @@ async function finishGame() {
// ===== Ответы команд и оценка организатором =====
const showAnswersModal = ref(false)
const showReviewsModal = ref(false)
const gameReviews = ref<GameReview[]>([])
const reviewsAverage = ref(0)
const reviewsCount = ref(0)
const answersQuestions = ref<Question[]>([])
const answersTeams = ref<TeamAnswers[]>([])
@@ -559,6 +565,27 @@ async function openAnswersModal() {
await loadGameAnswers()
}
async function openReviewsModal() {
showReviewsModal.value = true
const res = await client.GetGameReviewsForModeration({ gameId: Number(gameId) })
if (res.error != '') {
message.error(res.error!)
return
}
gameReviews.value = res.reviews || []
reviewsAverage.value = res.averageRating || 0
reviewsCount.value = res.reviewsCount || 0
}
async function moderateReview(review: GameReview, status: string) {
const res = await client.ModerateGameReview({ id: review.id, status })
if (res.error != '') {
message.error(res.error!)
return
}
await openReviewsModal()
}
// Сохраняет баллы всех команд за все вопросы; пустой балл сбрасывает его.
async function saveAllScores() {
for (const question of answersQuestions.value) {
@@ -1192,6 +1219,42 @@ useSimplePolling(() => {
</n-card>
</n-modal>
<n-modal v-model:show="showReviewsModal">
<n-card
style="width: min(94vw, 800px)"
title="Отзывы игры"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
:content-style="{ maxHeight: '70vh', overflow: 'auto' }"
>
<template #header-extra>
<n-button @click="showReviewsModal = false"> x </n-button>
</template>
<n-flex align="center" class="reviews-summary">
<n-rate :value="reviewsAverage" readonly allow-half />
<span>{{ reviewsCount ? `${reviewsAverage.toFixed(1)} · ${reviewsCount}` : 'Пока нет отзывов' }}</span>
</n-flex>
<div v-if="gameReviews.length == 0" class="answers-empty">Отзывов пока нет.</div>
<div v-for="review in gameReviews" :key="review.id" class="review-row">
<n-flex justify="space-between" align="center">
<strong>{{ review.teamName }}</strong>
<n-tag>{{ review.status }}</n-tag>
</n-flex>
<n-rate :value="review.rating" readonly size="small" />
<p>{{ review.text }}</p>
<n-flex v-if="review.status == 'pending'" justify="end">
<n-button type="success" @click="moderateReview(review, 'approved')">Одобрить</n-button>
<n-button type="error" ghost @click="moderateReview(review, 'rejected')">Отклонить</n-button>
</n-flex>
<n-flex v-else-if="review.status == 'approved'" justify="end">
<n-button type="error" ghost @click="moderateReview(review, 'hidden')">Скрыть</n-button>
</n-flex>
</div>
</n-card>
</n-modal>
<HeaderMenu active="games" />
</template>
@@ -1203,6 +1266,19 @@ useSimplePolling(() => {
margin: 10px 0;
}
.reviews-summary {
margin-bottom: 20px;
}
.review-row {
border-top: 1px solid #777;
padding: 16px 0;
}
.review-row p {
white-space: pre-wrap;
}
.team-block-hover:hover {
border: 1px solid #63e2b7;
color: #63e2b7;
+8
View File
@@ -101,6 +101,14 @@ const openMenu = ref(false)
>
Пользователи
</div>
<div
class="menu-item-block"
:class="{ active: props.active == 'reviews' }"
v-if="hasPermission('reviews_page')"
@click="router.push('/reviews')"
>
Отзывы
</div>
</div>
<div class="right-group">
+96
View File
@@ -0,0 +1,96 @@
<script setup lang="ts">
import { NButton, NCard, NFlex, NRate, NTag, useMessage } from 'naive-ui'
import { ref } from 'vue'
import { getAuthClient } from '@/api/auth_client'
import type { GameReview } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
const client = getAuthClient()
const message = useMessage()
const reviews = ref<GameReview[]>([])
async function loadReviews() {
const res = await client.GetReviewsForModeration({})
if (res.error != '') {
message.error(res.error!)
return
}
reviews.value = res.reviews || []
}
async function moderate(review: GameReview, status: string) {
const res = await client.ModerateGameReview({ id: review.id, status })
if (res.error != '') {
message.error(res.error!)
return
}
await loadReviews()
}
loadReviews()
</script>
<template>
<div class="center-block-custom">
<div class="width1200">
<div class="page-header">
<h1>Отзывы</h1>
<n-button @click="loadReviews">Обновить</n-button>
</div>
<n-card v-if="reviews.length == 0">Очередь отзывов пуста.</n-card>
<n-card v-for="review in reviews" :key="review.id" class="review-card">
<n-flex justify="space-between" align="center">
<div>
<div class="review-game">{{ review.gameName }}</div>
<strong>{{ review.teamName }}</strong>
</div>
<n-tag>{{ review.status }}</n-tag>
</n-flex>
<n-rate :value="review.rating" readonly />
<p class="review-text">{{ review.text }}</p>
<n-flex v-if="review.status == 'pending'" justify="end">
<n-button type="success" @click="moderate(review, 'approved')">Одобрить</n-button>
<n-button type="error" ghost @click="moderate(review, 'rejected')">Отклонить</n-button>
</n-flex>
<n-flex v-else-if="review.status == 'approved'" justify="end">
<n-button type="error" ghost @click="moderate(review, 'hidden')">Скрыть</n-button>
</n-flex>
</n-card>
</div>
</div>
<HeaderMenu active="reviews" />
</template>
<style scoped>
.center-block-custom {
height: calc(100dvh - 70px);
}
.page-header {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.page-header h1 {
font-size: 28px;
}
.review-card {
margin: 12px 0;
}
.review-game {
color: #777;
font-size: 13px;
margin-bottom: 4px;
}
.review-text {
white-space: pre-wrap;
}
</style>
+15 -5
View File
@@ -205,8 +205,8 @@ const paperControlsOverrides = {
borderDisabled: '1px dashed black',
borderFocus: '1px solid black',
borderHover: '1px dashed black',
boxShadowFocus: '0 0 0 2px rgb(99 226 183 / 30%)',
caretColor: '#63e2b7',
boxShadowFocus: '0 0 0 1px black',
caretColor: 'black',
color: 'transparent',
colorDisabled: 'transparent',
colorFocus: 'transparent',
@@ -218,8 +218,8 @@ const paperControlsOverrides = {
},
Rate: {
itemColor: 'black',
itemColorHover: 'black',
itemColorActive: 'black',
itemColorHover: '#b8860b',
itemColorActive: '#b8860b',
},
}
@@ -284,7 +284,7 @@ useSimplePolling(() => {
<hr class="questions-hr" />
<n-config-provider :theme-overrides="paperControlsOverrides">
<div class="review-rating font">Оценка</div>
<n-rate v-model:value="reviewRating" :count="5" />
<n-rate v-model:value="reviewRating" :count="5" class="review-rate" />
<n-input
v-model:value="reviewText"
class="review-input"
@@ -405,6 +405,16 @@ useSimplePolling(() => {
margin-top: 20px;
}
.review-rating {
margin-bottom: 6px;
margin-top: 20px;
}
.review-rate {
margin-bottom: 8px;
margin-top: 4px;
}
.review-input {
margin-top: 14px;
}