2026-03-20 03:35:52 +07:00

581 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, nextTick, watch, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { encodeUTF8ToBase64, getApiUrl } from './utils';
import VueQrcode from '@chenfengyuan/vue-qrcode';
import BeltMiniBlock from './BeltMiniBlock.vue';
import BeltBlock from './BeltBlock.vue';
import MetalPlate from './MetalPlate.vue';
const router = useRouter();
const route = useRoute();
type Application = {
name: string
}
type Door = {
code: string
name: string
show: boolean
}
type Action = {
id: string
place: string
name: string
text: string
image: string
applications: Application[]
hidden: boolean
doors: Door[]
isOpen: boolean
buttons: Door[]
}
type Team = {
name: string
actions: Action[]
}
const inputPlace = ref(false)
const login = ref("")
const password = ref("")
const place = ref("")
const team = ref<Team>({ name: "", actions: [] })
const actions = ref<Action[]>([])
const scrollContainer = ref<HTMLDivElement | null>();
const gameState = ref("STOP")
const gameStateText = ref("")
const qrurl = ref("-")
interface QROptions {
width?: number;
margin?: number;
color?: {
dark: string;
light: string;
};
}
const qrOptions = ref<QROptions>({
width: 200,
margin: 1,
color: {
dark: '#303030',
light: 'f0f0f0'
}
});
function getTeam() {
fetch(
getApiUrl("/team"),
{
method: "GET",
headers: {
"X-Id": encodeUTF8ToBase64(login.value),
"X-Password": password.value
},
}
)
.then(response => {
if (response.status == 401) {
router.push('/login');
return
}
const res = response.json()
return res
})
.then(data => {
const oldActions = team.value.actions
team.value = data
const newActions = team.value?.actions
newActions.forEach(item => {
item.isOpen = true
})
for (let i = 0; i < actions.value.length; i++) {
newActions[i].isOpen = oldActions[i].isOpen
}
if (actions.value.length !== newActions?.length) {
actions.value = newActions
}
for (let i = 0; i < team.value.actions.length; i++) {
const element = team.value.actions[i];
team.value.actions[i].buttons = element.doors.filter((door) => { return door.show })
}
})
.catch(error => {
console.error('Ошибка:', error)
});
}
function addAction() {
inputPlace.value = true
const placeValue = place.value.trim()
if (placeValue === "") {
place.value = ""
return
}
letsgo(placeValue)
place.value = ""
}
function letsgo(place: string) {
console.log("letsgo to " + place)
fetch(
getApiUrl("/team/actions"),
{
method: "POST",
headers: {
"X-Id": encodeUTF8ToBase64(login.value),
"X-Password": password.value
},
body: JSON.stringify({
"place": place
})
}
)
}
const scrollToBottom = async (behavior: ScrollBehavior = 'smooth'): Promise<void> => {
await nextTick();
if (scrollContainer.value) {
scrollContainer.value.scrollTo({
top: scrollContainer.value.scrollHeight,
behavior
});
}
};
function getGame() {
qrurl.value = location.href
fetch(
getApiUrl("/game")
)
.then(response => response.json())
.then(data => {
gameState.value = data.state
if (data.state === "NEW") {
gameStateText.value = "Игра ещё не началась"
}
if (data.state === "RUN") {
gameStateText.value = ""
}
if (data.state === "STOP") {
gameStateText.value = "Игра остановлена"
}
})
.catch(error => {
console.error('Ошибка:', error)
});
}
// Автоматическая прокрутка при изменении items
watch(actions, () => {
if (inputPlace.value === false) {
return
}
scrollToBottom();
inputPlace.value = false
}, { deep: true });
let intervalId = 0
onMounted(() => {
login.value = sessionStorage.getItem("teamId") || ""
password.value = sessionStorage.getItem("password") || ""
if (login.value == "") {
login.value = route.query["name"]?.toString() || ""
password.value = route.query["password"]?.toString() || ""
sessionStorage.setItem("teamId", login.value)
sessionStorage.setItem("password", password.value)
}
getTeam()
intervalId = setInterval(() => {
getTeam()
getGame()
}, 2000);
router.beforeEach((to, from, next) => {
clearInterval(intervalId);
next();
});
});
</script>
<template>
<div class="body-custom">
<div class="game-header">
<img alt="Вечерний детектив" class="logo" src="@/assets/logo_belt.png" />
<BeltMiniBlock class="game-header-belt-mini">
</BeltMiniBlock>
<BeltBlock class="game-header-belt center-bold-text">
<div class="position-right-center">
<span class="text-with-font">Вечерний детектив</span>
<MetalPlate class="team-name-block">
<div class="text-middle-wrapper text-truncate">
<p>...</p>
</div>
</MetalPlate>
</div>
</BeltBlock>
</div>
<!-- Форма ввода -->
<div class="form-custom">
<BeltBlock class="game-input-form">
<MetalPlate class="controller-metal controller-metal-left"></MetalPlate>
<MetalPlate class="controller-metal controller-metal-right"></MetalPlate>
<div class="center-block-custom">
<form @submit.prevent="addAction">
<div class="controller">
<div class="game-input">
<div class="game-input-run-left"></div>
<input id="run" class="game-input-run" v-model="place" type="text" placeholder="Место назначения"
:disabled="gameState !== 'RUN'">
</div>
<div class="game-button-run-shadow"></div>
<button class="game-button-run text-with-font" type="submit" :disabled="gameState !== 'RUN'">Поехали</button>
</div>
</form>
</div>
</BeltBlock>
</div>
<!-- Действия -->
<div class="messages-block" ref="scrollContainer">
<div class="center-block-custom">
<div v-if="!team || !team.actions.length">
<div class="center-message">
<div class="qr">
<VueQrcode :value="qrurl" :options="qrOptions" tag="svg" class="qr-code" />
<div>
Пора решать загадку
</div>
</div>
</div>
</div>
<div v-else>
<div v-for="action in team.actions" :key="action.id">
<div class="message-cloud">
<div class="message-header">
{{ action.place }}: {{ action.name }}
<span class="collapse-icon" @click="action.isOpen = !action.isOpen" style="float: right;">{{
action.isOpen ? '' : '+' }}</span>
</div>
<div v-show="action.isOpen">
<hr class="hr" />
<div class="message-content">
<div v-if="action.image.length">
<img v-bind:src="action.image" class="message-image" />
</div>{{ action.text }}
</div>
<hr class="hr" v-if="action.buttons?.length" />
<button v-for="door in action.buttons" :key="door.code" class="button-dialog"
v-on:click="letsgo(door.code)" :disabled="gameState !== 'RUN' || !door.show">
{{ door.name }}
</button>
<hr class="hr" v-if="action.applications.length" />
<div class="message-footer" v-for="application in action.applications" :key="application.name">
Приложение: {{ application.name }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
body {
overflow: hidden;
}
@font-face {
font-family: a_OldTyper;
src: url('@/assets/a_OldTyper.ttf');
}
.hr {
margin: 7px 0;
}
.body-custom {
font-size: medium;
height: calc(100vh - 100px);
background-color: gray;
}
.form-custom {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
color: white;
z-index: 1000;
}
.game-input-form {
height: 76px;
position: relative;
z-index: 1000;
}
.game-input-form-shadow {
height: 90px;
width: 120%;
left: -10%;
top: 3px;
position: absolute;
box-shadow: 0px -5px 10px black;
z-index: 9;
background-color: black;
}
.message-cloud {
border: 1px solid #444444;
border-radius: 15px;
margin: 12px 10px;
padding: 16px;
background-color: var(--main-back-item-color);
display: flow-root;
}
.message-header {
font-size: large;
font-weight: 200;
}
.message-content {
font-weight: 500;
white-space: pre-wrap;
}
.message-image {
width: 40%;
float: left;
margin-right: 15px;
}
.message-footer {
font-weight: 400;
color: var(--second-color);
}
.messages-block {
top: 95px;
height: calc(100dvh - 100px - 76px);
overflow-y: auto;
scrollbar-width: none;
position: relative;
padding: 5px 0 15px 0;
}
@media (min-width: 1025px) {
.center-block-custom {
width: 700px;
margin: 0 auto;
}
}
.center-message {
height: calc(100dvh - 140px);
}
.qr {
text-align: center;
width: 200px;
}
.collapse-icon {
padding: 0 15px;
cursor: pointer;
}
.team-name-block {
margin-right: 10px;
width: 50px;
height: 40px;
font-family: a_OldTyper;
}
.text-middle-wrapper {
position: relative;
height: 100%;
}
.text-middle-wrapper p {
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%)
}
.text-truncate {
text-align: center;
white-space: nowrap;
/* Запрещаем перенос текста */
overflow: hidden;
/* Обрезаем все, что не помещается */
text-overflow: ellipsis;
/* Добавляем троеточие */
font-size: medium;
}
.controller {
display: flex;
}
.game-header {
height: 100px;
position: fixed;
top: 5px;
left: 0;
width: 100%;
color: white;
z-index: 2000;
}
.game-header-belt-mini {
height: 30px;
position: relative;
z-index: 10;
}
.game-header-belt {
height: 60px;
position: relative;
top: -5px;
}
.position-right-center {
height: 100%;
display: flex;
align-items: center;
justify-content: right;
}
.logo {
width: 90px;
height: 88px;
float: left;
margin: 0 10px;
position: relative;
z-index: 20;
top: 10px;
}
.center-bold-text {
font-size: large;
color: white;
vertical-align: middle;
font-weight: 700;
}
.game-button-run {
background-image: url("@/assets/button.png");
background-size: cover;
font-size: 1.5em;
color: white;
position: absolute;
right: 10px;
top: -5px;
height: 80px;
width: 155px;
border: 0;
background-color: transparent;
margin: 0;
padding: 0;
}
.game-button-run-shadow {
position: absolute;
right: 10px;
top: -5px;
height: 80px;
width: 150px;
box-shadow: -5px 5px 10px black;
}
.game-button-run:hover {
/* TODO */
}
.game-button-run:disabled {
/* TODO */
}
.game-input {
position: relative;
top: 14px;
left: 25px;
height: 50px;
width: calc(100% - 150px - 50px);
}
.game-input-run-left {
height: 50px;
width: 25px;
position: absolute;
left: 0px;
background-image: url("@/assets/input_left.png");
background-size: cover;
}
.game-input-run {
height: 100%;
width: 100%;
margin-left: 15px;
padding-left: 12px;
background-image: url("@/assets/input_center.png");
background-size: cover;
border: 0;
font-size: 18px;
font-family: a_OldTyper;
}
.game-input-run::placeholder {
color: #333333;
}
.game-input-run:focus {
border: 0;
outline: none;
}
.controller-metal {
width: 30px;
height: calc(100% + 2px);
position: absolute;
top: -1px;
}
.controller-metal-left {
left: -15px;
}
.controller-metal-right {
right: -15px;
}
.text-with-font {
font-family: a_OldTyper;
color: #bfa07d;
font-weight: 600;
letter-spacing: 2px;
line-height: 20px;
font-size: 22px;
}
</style>