generated from VLADIMIR/template_frontend
ups
This commit is contained in:
@@ -516,6 +516,29 @@ service EveningDetectiveServer {
|
||||
};
|
||||
}
|
||||
|
||||
rpc IssueGameTickets(IssueGameTicketsReq) returns (IssueGameTicketsRsp) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/games/{gameId}/tickets"
|
||||
body: "*"
|
||||
};
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
security: { security_requirement: {} }
|
||||
tags: "Билеты";
|
||||
summary: "Выпустить билеты на игру";
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetTicket(GetTicketReq) returns (GetTicketRsp) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/tickets/{token}"
|
||||
};
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
security: { security_requirement: {} }
|
||||
tags: "Билеты";
|
||||
summary: "Получить данные билета";
|
||||
};
|
||||
}
|
||||
|
||||
// Создать сессию экрана. Экран получает токен и показывает QR для привязки.
|
||||
rpc CreateScreenSession(CreateScreenSessionReq) returns (CreateScreenSessionRsp) {
|
||||
option (google.api.http) = {
|
||||
@@ -1354,6 +1377,7 @@ message AddGameReq {
|
||||
google.protobuf.Timestamp start_at = 3;
|
||||
int32 scenario_id = 4;
|
||||
string format = 5;
|
||||
string location = 6;
|
||||
}
|
||||
|
||||
message AddGameRsp {
|
||||
@@ -1379,6 +1403,7 @@ message Game {
|
||||
optional google.protobuf.Timestamp started_at = 8;
|
||||
optional google.protobuf.Timestamp ended_at = 9;
|
||||
string format = 10;
|
||||
string location = 11;
|
||||
}
|
||||
|
||||
enum ScreenMode {
|
||||
@@ -1695,6 +1720,7 @@ message UpdateGameReq {
|
||||
google.protobuf.Timestamp start_at = 4;
|
||||
int32 scenario_id = 5;
|
||||
string format = 6;
|
||||
string location = 7;
|
||||
}
|
||||
|
||||
message UpdateGameRsp {
|
||||
@@ -1861,3 +1887,37 @@ message UpdateScenarioBackgroundReq {
|
||||
message UpdateScenarioBackgroundRsp {
|
||||
string error = 1;
|
||||
}
|
||||
|
||||
message IssueGameTicketsReq {
|
||||
int32 gameId = 1;
|
||||
int32 count = 2;
|
||||
}
|
||||
|
||||
message IssueGameTicketsRsp {
|
||||
string error = 1;
|
||||
repeated Ticket tickets = 2;
|
||||
}
|
||||
|
||||
message Ticket {
|
||||
int32 id = 1;
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
message GetTicketReq {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message GetTicketRsp {
|
||||
string error = 1;
|
||||
TicketPage ticket = 2;
|
||||
}
|
||||
|
||||
message TicketPage {
|
||||
int32 id = 1;
|
||||
string game_name = 2;
|
||||
google.protobuf.Timestamp start_at = 3;
|
||||
string location = 4;
|
||||
string status = 5;
|
||||
string game_status = 6;
|
||||
optional google.protobuf.Timestamp used_at = 7;
|
||||
}
|
||||
|
||||
@@ -474,6 +474,7 @@ export type AddGameReq = {
|
||||
startAt: wellKnownTimestamp | undefined;
|
||||
scenarioId: number | undefined;
|
||||
format: string | undefined;
|
||||
location: string | undefined;
|
||||
};
|
||||
|
||||
export type AddGameRsp = {
|
||||
@@ -500,6 +501,7 @@ export type Game = {
|
||||
startedAt?: wellKnownTimestamp;
|
||||
endedAt?: wellKnownTimestamp;
|
||||
format: string | undefined;
|
||||
location: string | undefined;
|
||||
};
|
||||
|
||||
export type ScreenState = {
|
||||
@@ -780,6 +782,7 @@ export type UpdateGameReq = {
|
||||
startAt: wellKnownTimestamp | undefined;
|
||||
scenarioId: number | undefined;
|
||||
format: string | undefined;
|
||||
location: string | undefined;
|
||||
};
|
||||
|
||||
export type UpdateGameRsp = {
|
||||
@@ -947,6 +950,40 @@ export type UpdateScenarioBackgroundRsp = {
|
||||
error: string | undefined;
|
||||
};
|
||||
|
||||
export type IssueGameTicketsReq = {
|
||||
gameId: number | undefined;
|
||||
count: number | undefined;
|
||||
};
|
||||
|
||||
export type IssueGameTicketsRsp = {
|
||||
error: string | undefined;
|
||||
tickets: Ticket[] | undefined;
|
||||
};
|
||||
|
||||
export type Ticket = {
|
||||
id: number | undefined;
|
||||
url: string | undefined;
|
||||
};
|
||||
|
||||
export type GetTicketReq = {
|
||||
token: string | undefined;
|
||||
};
|
||||
|
||||
export type GetTicketRsp = {
|
||||
error: string | undefined;
|
||||
ticket: TicketPage | undefined;
|
||||
};
|
||||
|
||||
export type TicketPage = {
|
||||
id: number | undefined;
|
||||
gameName: string | undefined;
|
||||
startAt: wellKnownTimestamp | undefined;
|
||||
location: string | undefined;
|
||||
status: string | undefined;
|
||||
gameStatus: string | undefined;
|
||||
usedAt?: wellKnownTimestamp;
|
||||
};
|
||||
|
||||
export interface EveningDetectiveServer {
|
||||
GetParticipation(request: GetParticipationReq): Promise<ParticipationRsp>;
|
||||
AnswerParticipation(request: AnswerParticipationReq): Promise<ParticipationRsp>;
|
||||
@@ -996,6 +1033,8 @@ export interface EveningDetectiveServer {
|
||||
AddGame(request: AddGameReq): Promise<AddGameRsp>;
|
||||
GetGames(request: GetGamesReq): Promise<GetGamesRsp>;
|
||||
GetGame(request: GetGameReq): Promise<GetGameRsp>;
|
||||
IssueGameTickets(request: IssueGameTicketsReq): Promise<IssueGameTicketsRsp>;
|
||||
GetTicket(request: GetTicketReq): Promise<GetTicketRsp>;
|
||||
// Создать сессию экрана. Экран получает токен и показывает QR для привязки.
|
||||
CreateScreenSession(request: CreateScreenSessionReq): Promise<CreateScreenSessionRsp>;
|
||||
// Получить состояние экрана по его токену. Вызывается самим экраном.
|
||||
@@ -1882,6 +1921,46 @@ export function createEveningDetectiveServerClient(
|
||||
method: "GetGame",
|
||||
}) as Promise<GetGameRsp>;
|
||||
},
|
||||
IssueGameTickets(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}/tickets`; // 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: "IssueGameTickets",
|
||||
}) as Promise<IssueGameTicketsRsp>;
|
||||
},
|
||||
GetTicket(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.token) {
|
||||
throw new Error("missing required field request.token");
|
||||
}
|
||||
const path = `api/tickets/${request.token}`; // 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: "GetTicket",
|
||||
}) as Promise<GetTicketRsp>;
|
||||
},
|
||||
CreateScreenSession(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const path = `api/screens`; // eslint-disable-line quotes
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
@@ -48,7 +48,7 @@ const gameId = route.params.id
|
||||
const message = useMessage()
|
||||
|
||||
const showSettingsModal = ref(false)
|
||||
const settingsDraft = ref({ name: '', description: '', format: 'online' })
|
||||
const settingsDraft = ref({ name: '', description: '', format: 'online', location: '' })
|
||||
const showGiveApplicationModal = ref(false)
|
||||
const showAddTeamModal = ref(false)
|
||||
const showDeleteTeamModal = ref(false)
|
||||
@@ -67,6 +67,7 @@ const game = ref<Game>({
|
||||
startAt: undefined,
|
||||
status: undefined,
|
||||
format: undefined,
|
||||
location: undefined,
|
||||
scenario: undefined,
|
||||
teams: undefined,
|
||||
})
|
||||
@@ -321,6 +322,7 @@ function openSettings() {
|
||||
name: game.value.name || '',
|
||||
description: game.value.description || '',
|
||||
format: game.value.format || 'online',
|
||||
location: game.value.location || '',
|
||||
}
|
||||
showSettingsModal.value = true
|
||||
}
|
||||
@@ -335,6 +337,7 @@ async function updateGame() {
|
||||
name: settingsDraft.value.name,
|
||||
description: settingsDraft.value.description,
|
||||
format: settingsDraft.value.format,
|
||||
location: settingsDraft.value.location.trim(),
|
||||
startAt: game.value.startAt,
|
||||
scenarioId: game.value.scenario?.id,
|
||||
})
|
||||
@@ -981,6 +984,13 @@ useSimplePolling(() => {
|
||||
type="textarea"
|
||||
placeholder="Убийство пастуха..."
|
||||
/>
|
||||
|
||||
<p class="settings-header">Место проведения</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.location"
|
||||
type="text"
|
||||
placeholder="Оставьте пустым для онлайн-игры"
|
||||
/>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
|
||||
@@ -48,6 +48,7 @@ const showCreateGameModal = ref(false)
|
||||
const newGameName = ref('')
|
||||
const newGameFormat = ref('online')
|
||||
const newGameDescription = ref('')
|
||||
const newGameLocation = ref('')
|
||||
const newGameScenarioId = ref<number | null>(null)
|
||||
const newGameStartAt = ref<number | null>(null)
|
||||
const isCreating = ref(false)
|
||||
@@ -85,6 +86,7 @@ function openCreateGame() {
|
||||
newGameName.value = ''
|
||||
newGameFormat.value = 'online'
|
||||
newGameDescription.value = ''
|
||||
newGameLocation.value = ''
|
||||
newGameScenarioId.value = null
|
||||
newGameStartAt.value = null
|
||||
showCreateGameModal.value = true
|
||||
@@ -103,6 +105,7 @@ async function createGame() {
|
||||
const res = await client.AddGame({
|
||||
name: name,
|
||||
description: newGameDescription.value,
|
||||
location: newGameLocation.value.trim(),
|
||||
format: newGameFormat.value,
|
||||
// Сервер парсит startAt как google.protobuf.Timestamp — нужен RFC 3339 с таймзоной
|
||||
startAt: new Date(startAtMs).toISOString(),
|
||||
@@ -143,6 +146,7 @@ async function createGame() {
|
||||
<div v-if="formatGameDateTime(game.startAt)" class="game-date">
|
||||
Начало: {{ formatGameDateTime(game.startAt) }}
|
||||
</div>
|
||||
<div v-if="game.location" class="game-date">Место: {{ game.location }}</div>
|
||||
<div v-if="game.status == 'finish'" class="game-actions">
|
||||
<n-button size="small" ghost @click.stop="toResults(game.id!)">Результаты</n-button>
|
||||
</div>
|
||||
@@ -185,6 +189,13 @@ async function createGame() {
|
||||
placeholder="Убийство пастуха..."
|
||||
/>
|
||||
|
||||
<p>Место проведения</p>
|
||||
<n-input
|
||||
v-model:value="newGameLocation"
|
||||
type="text"
|
||||
placeholder="Оставьте пустым для онлайн-игры"
|
||||
/>
|
||||
|
||||
<p>Сценарий (дело из каталога)</p>
|
||||
<n-select
|
||||
v-model:value="newGameScenarioId"
|
||||
|
||||
@@ -194,6 +194,7 @@ async function addGame(name: string, id: number) {
|
||||
const resAddGame = await client.AddGame({
|
||||
name: ('Тестовая игра: ' + name).trim(),
|
||||
description: 'Тестовая игра',
|
||||
location: '',
|
||||
format: 'online',
|
||||
startAt: now,
|
||||
scenarioId: id,
|
||||
|
||||
@@ -45,6 +45,7 @@ const game = ref<Game | undefined>({
|
||||
startAt: undefined,
|
||||
status: undefined,
|
||||
format: undefined,
|
||||
location: undefined,
|
||||
scenario: undefined,
|
||||
teams: undefined,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, NQrCode } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import type { TicketPage as TicketData } from '@/api/generated/crabs/evening_detective_server'
|
||||
import { buildClient } from '@/api/generated/crabs/evening_detective_server/client'
|
||||
import { formatGameDateTime } from '@/utils/datetime'
|
||||
|
||||
const route = useRoute()
|
||||
const client = buildClient()
|
||||
const token = computed(() => String(route.params.token || ''))
|
||||
const ticket = ref<TicketData>()
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const ticketURL = computed(() => window.location.href)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const result = await client.GetTicket({ token: token.value })
|
||||
if (result.error || !result.ticket) {
|
||||
error.value = result.error || 'Билет не найден'
|
||||
return
|
||||
}
|
||||
ticket.value = result.ticket
|
||||
} catch {
|
||||
error.value = 'Не удалось загрузить билет. Попробуйте ещё раз.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ticket-page center-block-custom">
|
||||
<div class="width700">
|
||||
<n-card class="ticket-card" title="Билет на игру">
|
||||
<p v-if="error" class="error" role="alert">{{ error }}</p>
|
||||
<template v-else-if="ticket">
|
||||
<h2>{{ ticket.gameName }}</h2>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Начало</dt>
|
||||
<dd>{{ formatGameDateTime(ticket.startAt) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Место</dt>
|
||||
<dd>{{ ticket.location || 'Онлайн' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="qr-block">
|
||||
<n-qr-code :value="ticketURL" :size="240" />
|
||||
<p>Покажите этот QR-код организатору на входе.</p>
|
||||
</div>
|
||||
|
||||
<p v-if="ticket.status === 'used'" class="used" role="status">
|
||||
Билет уже использован{{ ticket.usedAt ? ` ${formatGameDateTime(ticket.usedAt)}` : '' }}.
|
||||
</p>
|
||||
<p v-else-if="ticket.gameStatus === 'finish'" class="used" role="status">
|
||||
Игра уже завершена.
|
||||
</p>
|
||||
<p v-else class="active" role="status">Билет действителен.</p>
|
||||
</template>
|
||||
<n-button v-else :loading="loading" @click="load">
|
||||
{{ loading ? 'Загрузка' : 'Повторить' }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ticket-card {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #aaa;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.qr-block {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
justify-items: center;
|
||||
margin: 28px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error,
|
||||
.used {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #63e2b7;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,11 @@ const router = createRouter({
|
||||
name: 'participation',
|
||||
component: () => import('../views/ParticipationView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/:token',
|
||||
name: 'ticket',
|
||||
component: () => import('../views/TicketView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import TicketPage from '@/components/TicketPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TicketPage />
|
||||
</template>
|
||||
Reference in New Issue
Block a user