generated from VLADIMIR/template_frontend
fixes
This commit is contained in:
+256
-222
@@ -1,19 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NConfigProvider, NFlex, NInput, NQrCode } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { getAuthClient } from '@/api/auth_client';
|
||||
import type { Game, Introduction, Place, Question } from '@/api/generated/crabs/evening_detective_server';
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
import type {
|
||||
Game,
|
||||
Introduction,
|
||||
Place,
|
||||
Question,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import GameInputForm from '@/components/GameInputForm.vue'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import IntroBlock from '@/components/IntroBlock.vue'
|
||||
import PlaceBlock from '@/components/PlaceBlock.vue'
|
||||
import { useSimplePolling } from '@/composables/useSimplePolling';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useSimplePolling } from '@/composables/useSimplePolling'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
import MessagePaper from './MessagePaper.vue';
|
||||
import MessagePaper from './MessagePaper.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const client = getAuthClient()
|
||||
@@ -24,22 +29,22 @@ const password = route.query.password
|
||||
|
||||
const newPlacesCount = ref(0)
|
||||
|
||||
const scrollContainer = ref<HTMLDivElement | null>();
|
||||
const scrollContainer = ref<HTMLDivElement | null>()
|
||||
|
||||
const story = ref<AdvancedStory>({
|
||||
introduction: undefined,
|
||||
places: [],
|
||||
questions: []
|
||||
introduction: undefined,
|
||||
places: [],
|
||||
questions: [],
|
||||
})
|
||||
|
||||
const game = ref<Game | undefined>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
startAt: undefined,
|
||||
status: undefined,
|
||||
scenario: undefined,
|
||||
teams: undefined
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
startAt: undefined,
|
||||
status: undefined,
|
||||
scenario: undefined,
|
||||
teams: undefined,
|
||||
})
|
||||
|
||||
const code = ref('')
|
||||
@@ -47,134 +52,135 @@ const code = ref('')
|
||||
const url = window.location.href
|
||||
|
||||
const isAtBottom = () => {
|
||||
const container = scrollContainer.value;
|
||||
if (!container) return false;
|
||||
const container = scrollContainer.value
|
||||
if (!container) return false
|
||||
|
||||
return container.scrollHeight - container.scrollTop - container.clientHeight < 1;
|
||||
};
|
||||
return container.scrollHeight - container.scrollTop - container.clientHeight < 1
|
||||
}
|
||||
|
||||
async function getStory() {
|
||||
const oldStory = story.value
|
||||
const oldStory = story.value
|
||||
|
||||
const res = await client.GetTeamStory({
|
||||
id: Number(teamId),
|
||||
password: password as string
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
const res = await client.GetTeamStory({
|
||||
id: Number(teamId),
|
||||
password: password as string,
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
|
||||
game.value = res.game
|
||||
game.value = res.game
|
||||
|
||||
const wasAtBottom = isAtBottom();
|
||||
const wasAtBottom = isAtBottom()
|
||||
|
||||
if (wasAtBottom) {
|
||||
newPlacesCount.value = 0
|
||||
}
|
||||
if (wasAtBottom) {
|
||||
newPlacesCount.value = 0
|
||||
}
|
||||
|
||||
const oldCount = story.value.places.length
|
||||
const oldCount = story.value.places.length
|
||||
|
||||
story.value = {
|
||||
introduction: res.story!.introduction,
|
||||
places: res.story!.places?.map((item, i) => {
|
||||
console.log(oldStory?.places[i]?.mode)
|
||||
return {
|
||||
place: item,
|
||||
mode: oldStory?.places[i]?.mode || 'show'
|
||||
}
|
||||
}) || [],
|
||||
questions: res.story!.questions || []
|
||||
}
|
||||
story.value = {
|
||||
introduction: res.story!.introduction,
|
||||
places:
|
||||
res.story!.places?.map((item, i) => {
|
||||
console.log(oldStory?.places[i]?.mode)
|
||||
return {
|
||||
place: item,
|
||||
mode: oldStory?.places[i]?.mode || 'show',
|
||||
}
|
||||
}) || [],
|
||||
questions: res.story!.questions || [],
|
||||
}
|
||||
|
||||
const newCount = story.value.places.length
|
||||
const newCount = story.value.places.length
|
||||
|
||||
if (newCount > oldCount && oldCount != 0) {
|
||||
newPlacesCount.value += newCount - oldCount
|
||||
}
|
||||
if (newCount > oldCount && oldCount != 0) {
|
||||
newPlacesCount.value += newCount - oldCount
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
if (wasAtBottom && oldCount != 0) {
|
||||
const container = scrollContainer.value;
|
||||
if (!container) return false;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
await nextTick()
|
||||
if (wasAtBottom && oldCount != 0) {
|
||||
const container = scrollContainer.value
|
||||
if (!container) return false
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}
|
||||
getStory()
|
||||
getAnswers()
|
||||
|
||||
async function addAction(newCode: string) {
|
||||
const res = await client.AddTeamAction({
|
||||
id: Number(teamId),
|
||||
password: password as string,
|
||||
code: newCode
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
await getStory()
|
||||
code.value = ''
|
||||
const res = await client.AddTeamAction({
|
||||
id: Number(teamId),
|
||||
password: password as string,
|
||||
code: newCode,
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
await getStory()
|
||||
code.value = ''
|
||||
|
||||
await nextTick();
|
||||
newPlacesCount.value = 0
|
||||
await nextTick()
|
||||
newPlacesCount.value = 0
|
||||
}
|
||||
|
||||
type AdvancedStory = {
|
||||
introduction?: Introduction;
|
||||
places: AdvancedPlace[];
|
||||
questions: Question[];
|
||||
introduction?: Introduction
|
||||
places: AdvancedPlace[]
|
||||
questions: Question[]
|
||||
}
|
||||
|
||||
type AdvancedPlace = {
|
||||
place: Place;
|
||||
mode: string;
|
||||
place: Place
|
||||
mode: string
|
||||
}
|
||||
|
||||
// Ответы команды на вопросы дела: код вопроса -> текст ответа
|
||||
const answers = ref<Record<string, string>>({})
|
||||
|
||||
async function getAnswers() {
|
||||
const res = await client.GetTeamAnswers({
|
||||
id: Number(teamId),
|
||||
password: password as string
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
const next: Record<string, string> = {}
|
||||
for (const answer of res.answers || []) {
|
||||
next[answer.questionCode || ''] = answer.answer || ''
|
||||
}
|
||||
answers.value = next
|
||||
const res = await client.GetTeamAnswers({
|
||||
id: Number(teamId),
|
||||
password: password as string,
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
const next: Record<string, string> = {}
|
||||
for (const answer of res.answers || []) {
|
||||
next[answer.questionCode || ''] = answer.answer || ''
|
||||
}
|
||||
answers.value = next
|
||||
}
|
||||
|
||||
async function submitAnswers() {
|
||||
const res = await client.SubmitTeamAnswers({
|
||||
id: Number(teamId),
|
||||
password: password as string,
|
||||
answers: (story.value.questions || []).map((question) => ({
|
||||
questionCode: question.code,
|
||||
answer: answers.value[question.code || ''] || ''
|
||||
}))
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
message.success('Ответы сохранены')
|
||||
await getAnswers()
|
||||
const res = await client.SubmitTeamAnswers({
|
||||
id: Number(teamId),
|
||||
password: password as string,
|
||||
answers: (story.value.questions || []).map((question) => ({
|
||||
questionCode: question.code,
|
||||
answer: answers.value[question.code || ''] || '',
|
||||
})),
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
message.success('Ответы сохранены')
|
||||
await getAnswers()
|
||||
}
|
||||
|
||||
// Отвечать можно только пока игра идёт (run/pause); после завершения
|
||||
// ответы фиксируются для оценки организатором.
|
||||
const canAnswer = computed(() => {
|
||||
return game.value?.status == 'run' || game.value?.status == 'pause'
|
||||
return game.value?.status == 'run' || game.value?.status == 'pause'
|
||||
})
|
||||
|
||||
const hasQuestions = computed(() => {
|
||||
return (story.value.questions?.length || 0) > 0
|
||||
return (story.value.questions?.length || 0) > 0
|
||||
})
|
||||
|
||||
// Поля ввода и кнопка на светлом бумажном листе: тёмная тема naive-ui
|
||||
@@ -183,188 +189,216 @@ const hasQuestions = computed(() => {
|
||||
// «чернильной» рамкой (бумага просвечивает), шрифт наследуется
|
||||
// печатной машинки от родителя.
|
||||
const paperControlsOverrides = {
|
||||
Input: {
|
||||
border: '1px dashed black',
|
||||
borderDisabled: '1px dashed black',
|
||||
borderFocus: '1px solid black',
|
||||
borderHover: '1px dashed black',
|
||||
boxShadowFocus: '0 0 0 2px rgb(99 226 183 / 30%)',
|
||||
caretColor: '#63e2b7',
|
||||
color: 'transparent',
|
||||
colorDisabled: 'transparent',
|
||||
colorFocus: 'transparent',
|
||||
fontSizeMedium: '18px',
|
||||
placeholderColor: 'black',
|
||||
placeholderColorDisabled: 'black',
|
||||
textColor: 'black',
|
||||
textColorDisabled: 'black',
|
||||
},
|
||||
Input: {
|
||||
border: '1px dashed black',
|
||||
borderDisabled: '1px dashed black',
|
||||
borderFocus: '1px solid black',
|
||||
borderHover: '1px dashed black',
|
||||
boxShadowFocus: '0 0 0 2px rgb(99 226 183 / 30%)',
|
||||
caretColor: '#63e2b7',
|
||||
color: 'transparent',
|
||||
colorDisabled: 'transparent',
|
||||
colorFocus: 'transparent',
|
||||
fontSizeMedium: '18px',
|
||||
placeholderColor: 'black',
|
||||
placeholderColorDisabled: 'black',
|
||||
textColor: 'black',
|
||||
textColorDisabled: 'black',
|
||||
},
|
||||
}
|
||||
|
||||
async function deleteLast() {
|
||||
const res = await client.DeleteLastTeamAction({
|
||||
id: Number(teamId),
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
await getStory()
|
||||
code.value = ''
|
||||
const res = await client.DeleteLastTeamAction({
|
||||
id: Number(teamId),
|
||||
})
|
||||
if (res.error != '') {
|
||||
message.error(res.error!)
|
||||
return
|
||||
}
|
||||
await getStory()
|
||||
code.value = ''
|
||||
}
|
||||
|
||||
useSimplePolling(() => {
|
||||
getStory()
|
||||
}, 2000);
|
||||
|
||||
getStory()
|
||||
}, 2000)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center-block-custom bottom-margin smooth-scroll" ref="scrollContainer">
|
||||
<div class="width700">
|
||||
<GameInputForm :addAction="addAction" :allPlacesCount="story.places.length" :newPlacesCount="newPlacesCount"
|
||||
:gameStatus="game?.status || ''"></GameInputForm>
|
||||
<div class="center-block-custom bottom-margin smooth-scroll" ref="scrollContainer">
|
||||
<div class="width700">
|
||||
<GameInputForm
|
||||
:addAction="addAction"
|
||||
:allPlacesCount="story.places.length"
|
||||
:newPlacesCount="newPlacesCount"
|
||||
:gameStatus="game?.status || ''"
|
||||
></GameInputForm>
|
||||
|
||||
<IntroBlock
|
||||
v-if="game?.status && game.status != 'draft' && story.introduction && (story.introduction.text || story.introduction.audio)"
|
||||
:introduction="story.introduction" mode="show">
|
||||
</IntroBlock>
|
||||
<IntroBlock
|
||||
v-if="
|
||||
game?.status &&
|
||||
game.status != 'draft' &&
|
||||
story.introduction &&
|
||||
(story.introduction.text || story.introduction.audio)
|
||||
"
|
||||
:introduction="story.introduction"
|
||||
mode="show"
|
||||
>
|
||||
</IntroBlock>
|
||||
|
||||
<MessagePaper v-if="hasQuestions && game?.status && game.status != 'draft'" class="questions-block">
|
||||
<div class="font">Вопросы дела</div>
|
||||
<hr class="questions-hr" />
|
||||
<p class="questions-hint font">
|
||||
Ответьте на вопросы дела. Ответы можно менять, пока игра идёт.
|
||||
</p>
|
||||
<n-config-provider :theme-overrides="paperControlsOverrides">
|
||||
<div v-for="question in story.questions" :key="question.code" class="question-item font">
|
||||
<div class="question-text">{{ question.code }}: {{ question.text }}</div>
|
||||
<n-input v-model:value="answers[question.code || '']" type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }" :disabled="!canAnswer" placeholder="Ваш ответ..." />
|
||||
</div>
|
||||
<n-flex v-if="canAnswer" justify="end" class="questions-actions">
|
||||
<button type="button" class="paper-button" @click="submitAnswers()">Сохранить ответы</button>
|
||||
</n-flex>
|
||||
</n-config-provider>
|
||||
</MessagePaper>
|
||||
<MessagePaper
|
||||
v-if="hasQuestions && game?.status && game.status != 'draft'"
|
||||
class="questions-block"
|
||||
>
|
||||
<div class="font">Вопросы дела</div>
|
||||
<hr class="questions-hr" />
|
||||
<p class="questions-hint font">
|
||||
Ответьте на вопросы дела. Ответы можно менять, пока игра идёт.
|
||||
</p>
|
||||
<n-config-provider :theme-overrides="paperControlsOverrides">
|
||||
<div v-for="question in story.questions" :key="question.code" class="question-item font">
|
||||
<div class="question-text">{{ question.code }}: {{ question.text }}</div>
|
||||
<n-input
|
||||
v-model:value="answers[question.code || '']"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
:disabled="!canAnswer"
|
||||
placeholder="Ваш ответ..."
|
||||
/>
|
||||
</div>
|
||||
<n-flex v-if="canAnswer" justify="end" class="questions-actions">
|
||||
<button type="button" class="paper-button" @click="submitAnswers()">
|
||||
Сохранить ответы
|
||||
</button>
|
||||
</n-flex>
|
||||
</n-config-provider>
|
||||
</MessagePaper>
|
||||
|
||||
<PlaceBlock :place="advancedPlace.place" v-model:mode="advancedPlace.mode"
|
||||
v-for="advancedPlace in story.places" v-bind:key="advancedPlace.place.code" :addAction="addAction">
|
||||
</PlaceBlock>
|
||||
<PlaceBlock
|
||||
:place="advancedPlace.place"
|
||||
v-model:mode="advancedPlace.mode"
|
||||
v-for="advancedPlace in story.places"
|
||||
v-bind:key="advancedPlace.place.code"
|
||||
:addAction="addAction"
|
||||
>
|
||||
</PlaceBlock>
|
||||
|
||||
<MessagePaper class="qr-main-block" v-if="story.places.length == 0 && game?.status == 'draft'">
|
||||
<n-space vertical>
|
||||
<h2 class="qr-main-block-text">Это код этого дела</h2>
|
||||
<p class="qr-main-block-text">Поделись с командой</p>
|
||||
<div class="qr-block">
|
||||
<n-qr-code :value="url" :size="300" :padding="0" />
|
||||
</div>
|
||||
<p class="qr-main-block-text">Игра скоро начнется</p>
|
||||
</n-space>
|
||||
</MessagePaper>
|
||||
<MessagePaper
|
||||
class="qr-main-block"
|
||||
v-if="story.places.length == 0 && game?.status == 'draft'"
|
||||
>
|
||||
<n-space vertical>
|
||||
<h2 class="qr-main-block-text">Это код этого дела</h2>
|
||||
<p class="qr-main-block-text">Поделись с командой</p>
|
||||
<div class="qr-block">
|
||||
<n-qr-code :value="url" :size="300" :padding="0" />
|
||||
</div>
|
||||
<p class="qr-main-block-text">Игра скоро начнется</p>
|
||||
</n-space>
|
||||
</MessagePaper>
|
||||
|
||||
<div v-if="authStore.hasRole('author') && story.places.length > 0" class="edit-panel">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="deleteLast">Удалить последний ход</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authStore.hasRole('author') && story.places.length > 0" class="edit-panel">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="deleteLast">Удалить последний ход</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeaderMenu active="games" />
|
||||
<HeaderMenu active="games" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bottom-margin {
|
||||
height: calc(100dvh - 70px - 76px);
|
||||
height: calc(100dvh - 70px - 76px);
|
||||
}
|
||||
|
||||
.smooth-scroll {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.edit-panel {
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.qr-main-block {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.questions-hr {
|
||||
margin: 10px 0;
|
||||
border: dashed 1px;
|
||||
border-color: black;
|
||||
border: dashed 1px;
|
||||
border-color: black;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.questions-hint {
|
||||
color: black;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.question-item {
|
||||
margin: 15px 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.questions-actions {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Кнопка действия на бумажном листе — как «двери» в точках:
|
||||
текстура ремня, бронзовый текст печатной машинки */
|
||||
.paper-button {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 5px 12px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background-image: url('@/assets/images/belt.png');
|
||||
background-size: cover;
|
||||
color: #bfa07d;
|
||||
font-family: 'font_old_typer', serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background-image: url('@/assets/images/belt.png');
|
||||
background-size: cover;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
color: #bfa07d;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-family: 'font_old_typer', serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.paper-button:hover {
|
||||
color: #63e2b7;
|
||||
color: #63e2b7;
|
||||
}
|
||||
|
||||
.font {
|
||||
font-family: 'font_old_typer', serif;
|
||||
font-size: 18px;
|
||||
font-family: 'font_old_typer', serif;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.qr-main-block-text {
|
||||
font-family: 'font_old_typer', serif;
|
||||
font-size: 18px;
|
||||
margin: 10px 0;
|
||||
text-align: left;
|
||||
font-family: 'font_old_typer', serif;
|
||||
font-size: 18px;
|
||||
margin: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p.qr-main-block-text {
|
||||
font-weight: 500;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.qr-block {
|
||||
display: inline-block;
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
background-color: white;
|
||||
display: inline-block;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@media (width >=1024px) {
|
||||
.bottom-margin {
|
||||
height: calc(100dvh - 70px - 76px);
|
||||
}
|
||||
.bottom-margin {
|
||||
height: calc(100dvh - 70px - 76px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user