This commit is contained in:
2026-09-16 02:46:07 +07:00
parent 3b383596fa
commit 521306f376
99 changed files with 3223 additions and 2914 deletions
+52 -6
View File
@@ -57,6 +57,33 @@ const game = ref<Game>({
scenario: undefined,
teams: undefined,
})
const participationFilter = ref('all')
const participationOptions = [
{ label: 'Все команды', value: 'all' },
{ label: 'Ожидает ответа', value: 'pending' },
{ label: 'Подтверждено', value: 'confirmed' },
{ label: 'Отменено', value: 'cancelled' },
]
const participationSummary = computed(() =>
participationOptions.slice(1).map((option) => {
const teams = (game.value.teams || []).filter(
(team) => team.participationStatus === option.value,
)
return {
...option,
count: teams.length,
participants: teams.reduce((sum, team) => sum + (team.participantsCount || 0), 0),
}
}),
)
const activeTeamCount = computed(
() => (game.value.teams || []).filter((team) => team.participationStatus !== 'cancelled').length,
)
const visibleTeams = computed(() =>
(game.value.teams || []).filter(
(team) => participationFilter.value === 'all' || team.participationStatus === participationFilter.value,
),
)
const screens = ref<ScreenState[]>([])
const mapItems = ref<MapItem[]>([])
const mapSaving = ref(false)
@@ -308,6 +335,10 @@ const giveApplicationTeam = ref<Team>({
applications: [],
captainEmail: undefined,
participantsCount: undefined,
participationStatus: undefined,
participationSource: undefined,
participationEmailError: undefined,
participationEmailSent: undefined,
})
const deletedTeamName = ref('')
@@ -320,6 +351,10 @@ const deletedTeam = ref<Team>({
applications: [],
captainEmail: undefined,
participantsCount: undefined,
participationStatus: undefined,
participationSource: undefined,
participationEmailError: undefined,
participationEmailSent: undefined,
})
const giveApplicationApplication = ref<Application>({
@@ -570,7 +605,7 @@ useSimplePolling(() => {
<div class="settings-block">
<n-flex justify="space-between">
<n-space>
<n-tag :bordered="false" type="info"> Команд: {{ game.teams?.length }} </n-tag>
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
<n-tag type="info">
{{ timeString }}
</n-tag>
@@ -588,7 +623,7 @@ useSimplePolling(() => {
</Icon>
Итоги
</n-button>
<n-button v-if="game.teams?.length" quaternary :loading="isDownloadingQR" :disabled="isDownloadingQR" @click="downloadTeamQR()">
<n-button v-if="activeTeamCount" quaternary :loading="isDownloadingQR" :disabled="isDownloadingQR" @click="downloadTeamQR()">
QR команд
</n-button>
<n-button quaternary @click="openSettings()">
@@ -705,7 +740,14 @@ useSimplePolling(() => {
<p class="place-image-plus">+</p>
</div>
<div v-for="team in game.teams" :key="team.id" class="team-block">
<n-space vertical>
<n-tag v-for="item in participationSummary" :key="item.value">
{{ item.label }}: {{ item.count }} команд · {{ item.participants }} участников
</n-tag>
<n-select v-model:value="participationFilter" :options="participationOptions" aria-label="Статус участия" />
</n-space>
<div v-for="team in visibleTeams" :key="team.id" class="team-block">
<div class="team-content-block">
<n-flex justify="space-between">
<div class="title-block">
@@ -714,14 +756,18 @@ useSimplePolling(() => {
<small v-if="team.captainEmail"> — {{ team.captainEmail }}</small>
<small v-if="team.participantsCount"> · Участников: {{ team.participantsCount }}</small>
</span>
<div>Поездки: {{ team.actionsCount }}</div>
<div>{{ participationOptions.find((option) => option.value === team.participationStatus)?.label }}<span v-if="team.participationSource === 'automatic'"> · автоматически</span></div>
<div v-if="!team.captainEmail">Почта капитана не указана</div>
<div v-else-if="team.participationEmailError">Ошибка отправки: {{ team.participationEmailError }}</div>
<div v-else-if="team.participationEmailSent">Письмо отправлено</div>
<div v-if="team.participationStatus !== 'cancelled'">Поездки: {{ team.actionsCount }}</div>
</div>
<n-flex justify="end">
<n-button @click="openQR(getTeamStoryURL(team.id!, team.password!), 'Подсмотреть игру')">
<n-button v-if="team.participationStatus !== 'cancelled'" @click="openQR(getTeamStoryURL(team.id!, team.password!), 'Подсмотреть игру')">
QR
</n-button>
<n-button @click="toTeamStory(team.id!, team.password!, true)">
<n-button v-if="team.participationStatus !== 'cancelled'" @click="toTeamStory(team.id!, team.password!, true)">
Подсмотреть игру
</n-button>
<n-button type="error" ghost @click="deleteTeam(team)"> Удалить </n-button>
+118
View File
@@ -0,0 +1,118 @@
<script setup lang="ts">
import { NButton, NCard, NSpace, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import type { ParticipationRsp } from '@/api/generated/crabs/evening_detective_server'
import { buildClient } from '@/api/generated/crabs/evening_detective_server/client'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { useAuthStore } from '@/stores/auth'
import { formatGameDateTime } from '@/utils/datetime'
const route = useRoute()
const params = new URLSearchParams(route.hash.slice(1))
const token = params.get('token') || ''
const answer = ref(params.get('answer') === 'cancelled' ? 'cancelled' : 'confirmed')
const client = buildClient()
const authStore = useAuthStore()
const message = useMessage()
const participation = ref<ParticipationRsp>()
const loading = ref(false)
const error = ref('')
async function load() {
loading.value = true
error.value = ''
try {
const res = await client.GetParticipation({ token })
if (res.error) {
error.value = res.error
message.error(res.error)
return
}
participation.value = res
} catch {
error.value = 'Не удалось загрузить данные. Попробуйте ещё раз.'
} finally {
loading.value = false
}
}
async function submit() {
if (loading.value) return
loading.value = true
error.value = ''
try {
const res = await client.AnswerParticipation({ token, answer: answer.value })
if (res.status) participation.value = res
if (res.error) {
error.value = res.error
message.error(res.error)
}
} catch {
error.value = 'Не удалось сохранить ответ. Попробуйте ещё раз.'
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<HeaderMenu v-if="authStore.isAuthenticated" />
<div class="center-block-custom">
<div class="width700">
<n-card title="Участие в игре">
<p v-if="error" role="alert">{{ error }}</p>
<template v-if="participation">
<h2>{{ participation.gameName }}</h2>
<p>Команда: {{ participation.teamName }}</p>
<p>Начало: {{ formatGameDateTime(participation.startAt) }}</p>
<p>Участников: {{ participation.participantsCount }}</p>
<p v-if="participation.status === 'confirmed'" role="status">
Участие подтверждено. Ждём вас на игре! Решение окончательное.
</p>
<p v-else-if="participation.status === 'cancelled'" role="status">
Запись отменена. Решение окончательное.
</p>
<p v-else-if="participation.closed">
Приём ответов закрыт. Ваша запись сохранена со статусом «Ожидает ответа».
</p>
<template v-else>
<p><strong>Решение окончательное: изменить его нельзя.</strong></p>
<p>Без ответа запись сохраняется со статусом «Ожидает ответа».</p>
<n-space vertical>
<n-space>
<n-button
:disabled="loading"
:type="answer === 'confirmed' ? 'primary' : 'default'"
:aria-pressed="answer === 'confirmed'"
@click="answer = 'confirmed'"
>Мы участвуем</n-button
>
<n-button
:disabled="loading"
:type="answer === 'cancelled' ? 'error' : 'default'"
:aria-pressed="answer === 'cancelled'"
@click="answer = 'cancelled'"
>Мы не участвуем</n-button
>
</n-space>
<n-button
:loading="loading"
:disabled="loading"
:type="answer === 'cancelled' ? 'error' : 'primary'"
@click="submit"
>
{{ answer === 'confirmed' ? 'Подтверждаю участие' : 'Подтверждаю отмену' }}
</n-button>
</n-space>
</template>
</template>
<n-button v-else :loading="loading" @click="load">{{
loading ? 'Загрузка' : 'Повторить'
}}</n-button>
</n-card>
</div>
</div>
</template>