This commit is contained in:
2026-09-18 02:48:46 +07:00
parent 4d3fe17bcd
commit a6d151d574
80 changed files with 584 additions and 225 deletions
@@ -228,6 +228,8 @@ export type Scenario = {
updatedAt: wellKnownTimestamp | undefined;
createdAt: wellKnownTimestamp | undefined;
publishedAt: wellKnownTimestamp | undefined;
averageRating: number | undefined;
reviewsCount: number | undefined;
};
export type Story = {
@@ -718,6 +720,51 @@ export type GetGameResultsRsp = {
teams: TeamResult[] | undefined;
};
export type GameReview = {
id: number | undefined;
gameId: number | undefined;
teamId: number | undefined;
teamName: string | undefined;
rating: number | undefined;
text: string | undefined;
status: string | undefined;
createdAt: wellKnownTimestamp | undefined;
};
export type CreateGameReviewReq = {
gameId: number | undefined;
teamId: number | undefined;
password: string | undefined;
rating: number | undefined;
text: string | undefined;
};
export type CreateGameReviewRsp = {
error: string | undefined;
review: GameReview | undefined;
};
export type GetGameReviewsReq = {
gameId: number | undefined;
};
export type GetGameReviewsRsp = {
error: string | undefined;
averageRating: number | undefined;
reviewsCount: number | undefined;
reviews: GameReview[] | undefined;
};
export type ModerateGameReviewReq = {
id: number | undefined;
status: string | undefined;
};
export type ModerateGameReviewRsp = {
error: string | undefined;
review: GameReview | undefined;
};
export type UpdateGameReq = {
id: number | undefined;
name: string | undefined;
@@ -961,6 +1008,10 @@ export interface EveningDetectiveServer {
DownloadTeamQR(request: GetGameReq): Promise<googleapi_HttpBody>;
GetGameAnswers(request: GetGameAnswersReq): Promise<GetGameAnswersRsp>;
GetGameResults(request: GetGameResultsReq): Promise<GetGameResultsRsp>;
CreateGameReview(request: CreateGameReviewReq): Promise<CreateGameReviewRsp>;
GetGameReviews(request: GetGameReviewsReq): Promise<GetGameReviewsRsp>;
GetGameReviewsForModeration(request: GetGameReviewsReq): Promise<GetGameReviewsRsp>;
ModerateGameReview(request: ModerateGameReviewReq): Promise<ModerateGameReviewRsp>;
UpdateGame(request: UpdateGameReq): Promise<UpdateGameRsp>;
StartGame(request: StartGameReq): Promise<StartGameRsp>;
PauseGame(request: PauseGameReq): Promise<PauseGameRsp>;
@@ -2098,6 +2149,86 @@ export function createEveningDetectiveServerClient(
method: "GetGameResults",
}) as Promise<GetGameResultsRsp>;
},
CreateGameReview(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.gameId) {
throw new Error("missing required field request.gameId");
}
const path = `api/games/${request.gameId}/reviews`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "POST",
body,
}, {
service: "EveningDetectiveServer",
method: "CreateGameReview",
}) as Promise<CreateGameReviewRsp>;
},
GetGameReviews(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.gameId) {
throw new Error("missing required field request.gameId");
}
const path = `api/games/${request.gameId}/reviews`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "GET",
body,
}, {
service: "EveningDetectiveServer",
method: "GetGameReviews",
}) as Promise<GetGameReviewsRsp>;
},
GetGameReviewsForModeration(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.gameId) {
throw new Error("missing required field request.gameId");
}
const path = `api/admin/games/${request.gameId}/reviews`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "GET",
body,
}, {
service: "EveningDetectiveServer",
method: "GetGameReviewsForModeration",
}) as Promise<GetGameReviewsRsp>;
},
ModerateGameReview(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/admin/game-reviews/${request.id}`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "PUT",
body,
}, {
service: "EveningDetectiveServer",
method: "ModerateGameReview",
}) as Promise<ModerateGameReviewRsp>;
},
UpdateGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
+15 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NTag } from 'naive-ui'
import { NRate, NTag } from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -45,6 +45,12 @@ getScenarios()
<p class="scenario-description">{{ scenario.description }}</p>
<div class="scenario-rating">
<n-rate :value="scenario.averageRating" readonly allow-half size="small" />
<span v-if="scenario.reviewsCount">{{ scenario.averageRating?.toFixed(1) }} · {{ scenario.reviewsCount }}</span>
<span v-else>Пока нет оценок</span>
</div>
<p class="scenario-label">
<n-tag :bordered="false" type="success" class="status-block"
>Автор: {{ scenario.author?.username }}</n-tag
@@ -124,6 +130,14 @@ getScenarios()
padding-top: 12px;
}
.scenario-rating {
align-items: center;
color: #aaa;
display: flex;
gap: 8px;
margin-top: 12px;
}
.status-block {
height: auto;
line-height: 1.5;
+16 -2
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { useMessage } from 'naive-ui'
import { NTag } from 'naive-ui'
import { NRate, NTag, useMessage } from 'naive-ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
@@ -23,6 +22,8 @@ const scenario = ref<Scenario>({
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined,
averageRating: undefined,
reviewsCount: undefined,
})
async function getScenario(id: number) {
@@ -49,6 +50,11 @@ getScenario(Number(scenarioId))
</div>
<div class="scenario-details">
<p class="scenario-description">{{ scenario.description }}</p>
<div class="scenario-rating">
<n-rate :value="scenario.averageRating" readonly allow-half size="small" />
<span v-if="scenario.reviewsCount">{{ scenario.averageRating?.toFixed(1) }} · {{ scenario.reviewsCount }} отзывов</span>
<span v-else>Пока нет оценок</span>
</div>
<div class="scenario-author">
<n-tag :bordered="false" type="success" class="status-block">
Автор: {{ scenario.author?.username }}
@@ -124,6 +130,14 @@ getScenario(Number(scenarioId))
margin-top: 12px;
}
.scenario-rating {
align-items: center;
color: #aaa;
display: flex;
gap: 8px;
margin-top: 16px;
}
.status-block {
height: auto;
line-height: 1.5;
+2
View File
@@ -58,6 +58,8 @@ const scenario = ref<Scenario>({
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined,
averageRating: undefined,
reviewsCount: undefined,
})
const backgroundFiles = ref<UploadFileInfo[]>([])
+71 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { NButton, NConfigProvider, NFlex, NInput, NQrCode } from 'naive-ui'
import { NButton, NConfigProvider, NFlex, NInput, NQrCode, NRate } from 'naive-ui'
import { useMessage } from 'naive-ui'
import { computed, nextTick, ref } from 'vue'
import { useRoute } from 'vue-router'
@@ -51,6 +51,12 @@ const game = ref<Game | undefined>({
const code = ref('')
const reviewKey = `game-review-${teamId}`
const reviewSubmitted = ref(localStorage.getItem(reviewKey) === '1')
const reviewRating = ref(0)
const reviewText = ref('')
const reviewLoading = ref(false)
const url = window.location.href
const isAtBottom = () => {
@@ -212,6 +218,29 @@ const paperControlsOverrides = {
},
}
async function submitReview() {
if (!reviewRating.value || reviewText.value.trim() === '') {
message.error('Поставьте оценку и напишите отзыв')
return
}
reviewLoading.value = true
const res = await client.CreateGameReview({
gameId: Number(game.value?.id),
teamId: Number(teamId),
password: password as string,
rating: reviewRating.value,
text: reviewText.value.trim(),
})
reviewLoading.value = false
if (res.error != '') {
message.error(res.error!)
return
}
reviewSubmitted.value = true
localStorage.setItem(reviewKey, '1')
message.success('Спасибо за отзыв')
}
async function deleteLast() {
const res = await client.DeleteLastTeamAction({
id: Number(teamId),
@@ -245,6 +274,24 @@ useSimplePolling(() => {
<MessagePaper role="status">
<div class="font">Игра завершена. Спасибо за участие!</div>
</MessagePaper>
<MessagePaper v-if="!reviewSubmitted" class="review-block">
<div class="font review-title">Оцените игру</div>
<hr class="review-hr" />
<div class="font review-label">Оценка</div>
<n-rate v-model:value="reviewRating" :count="5" />
<n-input
v-model:value="reviewText"
class="review-input"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
placeholder="Расскажите, как прошла игра"
/>
<n-flex justify="end">
<button type="button" class="paper-button" :disabled="reviewLoading" @click="submitReview">
Отправить отзыв
</button>
</n-flex>
</MessagePaper>
</div>
<div v-else class="width700">
<GameInputForm
@@ -347,6 +394,29 @@ useSimplePolling(() => {
height: 100dvh;
}
.review-block {
margin-top: 20px;
}
.review-title {
color: black;
}
.review-hr {
border: dashed 1px;
border-color: black;
margin: 10px 0;
}
.review-label {
color: black;
margin-bottom: 6px;
}
.review-input {
margin: 14px 0;
}
.smooth-scroll {
scroll-behavior: smooth;
}