2026-03-19 12:29:31 +07:00

544 lines
11 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';
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">
<div class="game-header-empty-block"></div>
<img alt="Вечерний детектив" class="logo" src="@/assets/logo_belt.png" />
<div class="game-header-belt-mini-shadow"></div>
<BeltMiniBlock class="game-header-belt-mini">
</BeltMiniBlock>
<div class="game-header-belt-shadow"></div>
<BeltBlock class="game-header-belt center-bold-text">
<!-- Вечерний детектив -->
<!-- <span class="team-name-block text-truncate">{{ team.name }}</span> -->
</BeltBlock>
</div>
<!-- Форма ввода -->
<div class="form-custom">
<div class="game-input-form-shadow"></div>
<BeltBlock class="game-input-form">
<div class="center-block-custom">
<form @submit.prevent="addAction">
<div class="controller">
<input class="game-input-run" v-model="place" type="text" placeholder="Место назначения (А-1, а-1, а1)"
:disabled="gameState !== 'RUN'">
<div class="game-button-run-shadow"></div>
<button class="game-button-run" 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;
background-color: gray;
}
.hr {
margin: 7px 0;
}
.body-custom {
font-size: medium;
background-color: gray;
height: 100vh;
}
.info-custom {
padding-left: 15px;
}
.logo-right {
float: right;
margin: 12px;
}
.second-color {
color: var(--second-color);
}
.form-custom {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
color: white;
z-index: 1000;
}
.game-input-form {
height: 76px;
padding: 13px;
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-line; */
white-space: pre-wrap;
}
.message-image {
/* width: 150px; */
width: 40%;
float: left;
margin-right: 15px;
}
.message-footer {
font-weight: 400;
color: var(--second-color);
}
.messages-block {
top: 95px;
height: calc(100dvh - 95px - 90px + 5px);
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 {
float: right;
padding: 0 20px;
}
.text-truncate {
width: 100px;
text-align: center;
white-space: nowrap;
/* Запрещаем перенос текста */
overflow: hidden;
/* Обрезаем все, что не помещается */
text-overflow: ellipsis;
/* Добавляем троеточие */
padding: 2px 7px;
margin: 0 20px;
background: rgb(40, 69, 87);
border-radius: 4px;
font-size: medium;
}
.belt-mini-block {
height: 30px;
width: 100%;
position: absolute;
top: 0;
}
.controller {
display: flex;
}
.game-header {
height: 100px;
position: fixed;
top: 0;
left: 0;
width: 100%;
color: white;
z-index: 1000;
}
.game-header-empty-block {
height: 5px;
}
.game-header-belt-mini {
height: 30px;
position: relative;
z-index: 10;
}
.game-header-belt-mini-shadow {
height: 20px;
width: 120%;
left: -10%;
position: absolute;
top: 10px;
box-shadow: 0px 5px 10px black;
z-index: 9;
background-color: black;
}
.game-header-belt {
height: 60px;
position: relative;
top: -5px;
z-index: 1;
}
.game-header-belt-shadow {
width: 120%;
left: -10%;
height: 60px;
top: 30px;
position: absolute;
background-color: black;
box-shadow: 0px 5px 10px black;
}
.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-run {
height: 50px;
width: calc(100% - 150px);
}
</style>