Compare commits
77 Commits
75c11b36a6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0943fa0400 | |||
| e04a877532 | |||
| f314fed7ad | |||
| a5acda73ae | |||
| 578e22b8f0 | |||
| 58c402b0f3 | |||
| 74c31cf407 | |||
| 84071feb24 | |||
| 25dc69e4df | |||
| 4758db0283 | |||
| fd6ededacf | |||
| f7da699cfb | |||
| fdf27203a3 | |||
| ed178d57a9 | |||
| ee4ee4dbd9 | |||
| 82e0f9ffb4 | |||
| 54d4864586 | |||
| 9e5c63da5a | |||
| c8dbce1462 | |||
| 39928b2615 | |||
| cfb58ca82c | |||
| faf6725a99 | |||
| 8d8a276da0 | |||
| 54ce3e5e7a | |||
| 977f9cc7eb | |||
| f4964e6355 | |||
| 6b261804fd | |||
| 7a1480b810 | |||
| dda3bc83d8 | |||
| 3c10c311d1 | |||
| 3d01fea198 | |||
| 4adae4e2d6 | |||
| 20986e86e2 | |||
| 9e6ee6e7c3 | |||
| e8158eb746 | |||
| 614ec9e059 | |||
| ccad38799f | |||
| b56a4570de | |||
| 80d581e3b6 | |||
| dd41cd08dd | |||
| 7b620dffd4 | |||
| b676129ec2 | |||
| 5fee2035aa | |||
| 5ea37e155d | |||
| 19cf4c68b4 | |||
| ac58312aa2 | |||
| f08399c76c | |||
| 880c305731 | |||
| aa60c54c8f | |||
| 25f1da5a32 | |||
| bf04f83a15 | |||
| 365a231ed5 | |||
| 200704bf92 | |||
| 48d2a814fe | |||
| 4f6a96a49a | |||
| 046ebfc62f | |||
| 9bde8ff7ee | |||
| 5b3203e5aa | |||
| 875207d9ff | |||
| aa7a9bf0cc | |||
| 1026223957 | |||
| 1f5ae42890 | |||
| 3e59047d73 | |||
| 9fe699f11e | |||
| 0280c7158b | |||
| 1ca5c8d4a1 | |||
| 4c9d6cf6d3 | |||
| 06aac3d552 | |||
| 6180cbffa1 | |||
| ceb744ec12 | |||
| 6a8b7edb45 | |||
| 21160cd349 | |||
| 39cdf1c162 | |||
| be7ed728a3 | |||
| 38518e465d | |||
| bc88340480 | |||
| e97a318237 |
@@ -0,0 +1 @@
|
||||
VITE_API_URL='https://evening-detective-api.crabs-games.art'
|
||||
@@ -0,0 +1 @@
|
||||
VITE_API_URL=''
|
||||
@@ -1,4 +1,9 @@
|
||||
build:
|
||||
npm run build
|
||||
rm -rf ../evening_detective/static/user
|
||||
cp -r dist ../evening_detective/static/user
|
||||
build-macos:
|
||||
npm run build:local_web
|
||||
rm -rf ../evening_detective/cmd/evening_detective/static/user
|
||||
cp -r dist ../evening_detective/cmd/evening_detective/static/user
|
||||
|
||||
build-linux:
|
||||
npm run build:global_web
|
||||
rm -rf ../evening_detective/cmd/evening_detective/static/user
|
||||
cp -r dist ../evening_detective/cmd/evening_detective/static/user
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --mode local_web",
|
||||
"build:local_web": "vite build --mode local_web",
|
||||
"build:global_web": "vite build --mode global_web",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
@@ -13,13 +15,15 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-qrcode": "^2.0.0",
|
||||
"pinia": "^3.0.1",
|
||||
"qrcode-vue3": "^1.7.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^22.19.19",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 66 KiB |
@@ -1,5 +1,7 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
color-scheme: only light;
|
||||
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
@@ -21,8 +23,8 @@
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
|
||||
/* Главный цвет */
|
||||
--main-color: rgba(115, 185, 83, 1);
|
||||
--second-color: rgba(98, 156, 68, 1);
|
||||
--main-color: rgba(34, 50, 60, 1);
|
||||
--second-color: rgb(97, 74, 22);
|
||||
--main-back-color: rgba(240, 240, 240, 1);
|
||||
--main-back-item-color: rgba(254, 254, 254, 1);
|
||||
}
|
||||
@@ -67,7 +69,6 @@
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
color: var(--color-text);
|
||||
background: var(--main-back-color);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
@@ -89,4 +90,6 @@ body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-image: url("@/assets/images/forest.png");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 802 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 603 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 195 KiB |
@@ -1,41 +1,19 @@
|
||||
@import './base.css';
|
||||
|
||||
.header-block {
|
||||
height: 50px;
|
||||
background-color: var(--main-color);
|
||||
font-size: large;
|
||||
color: white;
|
||||
vertical-align: middle;
|
||||
padding: 10px 0 10px 16px;
|
||||
font-weight: 700;
|
||||
body {
|
||||
overflow: hidden;
|
||||
background-color: black;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.input-custom {
|
||||
width: 100%;
|
||||
box-sizing: border-box; /* обязательно! */
|
||||
margin-bottom: 15px;
|
||||
@font-face {
|
||||
font-family: a_OldTyper;
|
||||
src: url('@/assets/a_OldTyper.ttf');
|
||||
}
|
||||
|
||||
.button-custom {
|
||||
margin-left: auto;
|
||||
background-color: var(--main-color);
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-custom:hover {
|
||||
background-color: var(--second-color);
|
||||
}
|
||||
|
||||
.input-custom, .button-custom {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
@font-face {
|
||||
font-family: main;
|
||||
src: url('@/assets/main.ttf');
|
||||
}
|
||||
|
||||
.center-message {
|
||||
@@ -45,3 +23,31 @@
|
||||
height: calc(100dvh - 100px);
|
||||
text-align: center; /* центрирование текста */
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.center-block-custom {
|
||||
width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.center-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.controller-metal {
|
||||
width: 30px;
|
||||
height: calc(100% + 2px);
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.controller-metal-left {
|
||||
left: -15px;
|
||||
}
|
||||
|
||||
.controller-metal-right {
|
||||
right: -15px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="System / Qr_Code">
|
||||
<path
|
||||
id="Vector"
|
||||
d="M19 20H20M16 20H14V17M17 17H20V14H19M14 14H16M4 16.9997C4 16.0679 4 15.6019 4.15224 15.2344C4.35523 14.7443 4.74432 14.3552 5.23438 14.1522C5.60192 14 6.06786 14 6.99974 14C7.93163 14 8.39808 14 8.76562 14.1522C9.25568 14.3552 9.64467 14.7443 9.84766 15.2344C9.9999 15.6019 9.9999 16.0681 9.9999 17C9.9999 17.9319 9.9999 18.3978 9.84766 18.7654C9.64467 19.2554 9.25568 19.6447 8.76562 19.8477C8.39808 19.9999 7.93162 19.9999 6.99974 19.9999C6.06786 19.9999 5.60192 19.9999 5.23438 19.8477C4.74432 19.6447 4.35523 19.2557 4.15224 18.7656C4 18.3981 4 17.9316 4 16.9997ZM14 6.99974C14 6.06786 14 5.60192 14.1522 5.23438C14.3552 4.74432 14.7443 4.35523 15.2344 4.15224C15.6019 4 16.0679 4 16.9997 4C17.9316 4 18.3981 4 18.7656 4.15224C19.2557 4.35523 19.6447 4.74432 19.8477 5.23438C19.9999 5.60192 19.9999 6.06812 19.9999 7C19.9999 7.93188 19.9999 8.39783 19.8477 8.76537C19.6447 9.25542 19.2557 9.64467 18.7656 9.84766C18.3981 9.9999 17.9316 9.9999 16.9997 9.9999C16.0679 9.9999 15.6019 9.9999 15.2344 9.84766C14.7443 9.64467 14.3552 9.25568 14.1522 8.76562C14 8.39808 14 7.93163 14 6.99974ZM4 6.99974C4 6.06786 4 5.60192 4.15224 5.23438C4.35523 4.74432 4.74432 4.35523 5.23438 4.15224C5.60192 4 6.06786 4 6.99974 4C7.93163 4 8.39808 4 8.76562 4.15224C9.25568 4.35523 9.64467 4.74432 9.84766 5.23438C9.9999 5.60192 9.9999 6.06812 9.9999 7C9.9999 7.93188 9.9999 8.39783 9.84766 8.76537C9.64467 9.25542 9.25568 9.64467 8.76562 9.84766C8.39808 9.9999 7.93162 9.9999 6.99974 9.9999C6.06786 9.9999 5.60192 9.9999 5.23438 9.84766C4.74432 9.64467 4.35523 9.25568 4.15224 8.76562C4 8.39808 4 7.93163 4 6.99974Z"
|
||||
stroke="#fdfdfd"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="shadow shadow-top"></div>
|
||||
<div class="shadow shadow-bottom"></div>
|
||||
<div class="belt-block">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.belt-block {
|
||||
height: 100%;
|
||||
background-color: rgb(0, 0, 0);
|
||||
background-image: url("@/assets/images/belt.png");
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.shadow-top {
|
||||
top: 0px;
|
||||
box-shadow: 0px -5px 10px black;
|
||||
}
|
||||
|
||||
.shadow-bottom {
|
||||
bottom: 0px;
|
||||
box-shadow: 0px 5px 10px black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="shadow"></div>
|
||||
<div class="belt-block">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.belt-block {
|
||||
height: 100%;
|
||||
background-image: url("@/assets/images/belt_mini.png");
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
background-color: black;
|
||||
box-shadow: 0px 5px 10px black;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import BeltMiniBlock from './BeltMiniBlock.vue';
|
||||
import BeltBlock from './BeltBlock.vue';
|
||||
import HeaderText from './HeaderText.vue';
|
||||
import MetalPlate from './MetalPlate.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="game-header">
|
||||
<div class="center">
|
||||
<MetalPlate class="controller-metal controller-metal-left"></MetalPlate>
|
||||
<MetalPlate class="controller-metal controller-metal-right"></MetalPlate>
|
||||
<img alt="Вечерний детектив" class="logo" src="@/assets/images/logo_belt.png" />
|
||||
<BeltMiniBlock class="belt-mini"></BeltMiniBlock>
|
||||
<BeltBlock class="belt">
|
||||
<div class="position-right-center-block">
|
||||
<HeaderText>Вечерний детектив</HeaderText>
|
||||
<!-- <MetalPlate class="team-name-block">
|
||||
<div class="text-middle-wrapper text-truncate">
|
||||
<p>...</p>
|
||||
</div>
|
||||
</MetalPlate> -->
|
||||
</div>
|
||||
</BeltBlock>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-header {
|
||||
height: 100px;
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: relative;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 90px;
|
||||
height: 88px;
|
||||
float: left;
|
||||
margin: 0 10px;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.belt-mini {
|
||||
height: 30px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.belt {
|
||||
height: 60px;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.position-right-center-block {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.controller-metal {
|
||||
width: 30px;
|
||||
height: calc(100% + 2px);
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.controller-metal-left {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.controller-metal-right {
|
||||
right: -30px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import BeltBlock from './BeltBlock.vue';
|
||||
import { apiLetsgo } from './client';
|
||||
import MetalPlate from './MetalPlate.vue';
|
||||
import HeaderText from './HeaderText.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const place = ref("")
|
||||
|
||||
interface Props {
|
||||
gameState: string
|
||||
login: string
|
||||
password: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const isPlaceFilled = defineModel<boolean>();
|
||||
|
||||
async function addAction() {
|
||||
isPlaceFilled.value = true
|
||||
const placeValue = place.value.trim()
|
||||
if (placeValue === "") {
|
||||
place.value = ""
|
||||
return
|
||||
}
|
||||
await apiLetsgo(props.login, props.password, placeValue)
|
||||
place.value = ""
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-custom">
|
||||
<BeltBlock class="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">
|
||||
<input id="run" class="game-input-run" v-model="place" type="text" placeholder="Место назначения"
|
||||
:disabled="props.gameState !== 'RUN'">
|
||||
</div>
|
||||
<div class="game-button-run-shadow"></div>
|
||||
<button class="game-button-run" type="submit" :disabled="props.gameState !== 'RUN'">
|
||||
<HeaderText>Поехали</HeaderText>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</BeltBlock>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-custom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
color: white;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.input-form {
|
||||
height: 76px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
margin: 0 auto;
|
||||
max-width: 1920px;
|
||||
}
|
||||
|
||||
.controller-metal {
|
||||
width: 30px;
|
||||
height: calc(100% + 2px);
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.controller-metal-left {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.controller-metal-right {
|
||||
right: -30px;
|
||||
}
|
||||
|
||||
.controller {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-input {
|
||||
position: relative;
|
||||
top: 14px;
|
||||
left: 15px;
|
||||
height: 50px;
|
||||
width: calc(100% - 150px - 25px);
|
||||
}
|
||||
|
||||
.game-input-run {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-left: 27px;
|
||||
background-image: url("@/assets/images/input_center.png");
|
||||
background-size: cover;
|
||||
border: 0;
|
||||
font-size: 18px;
|
||||
font-family: a_OldTyper;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.game-input-run::placeholder {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.game-input-run:focus {
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.game-button-run-shadow {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: -5px;
|
||||
height: 80px;
|
||||
width: 150px;
|
||||
box-shadow: -5px 5px 10px black;
|
||||
}
|
||||
|
||||
.game-button-run {
|
||||
background-image: url("@/assets/images/button.png");
|
||||
background-size: cover;
|
||||
font-size: 1.5em;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: -5px;
|
||||
height: 80px;
|
||||
width: 155px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,73 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, nextTick, watch, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import GameHeader from './GameHeader.vue';
|
||||
import type { Action, Door, Team } from './models';
|
||||
import { apiGetGame, apiGetTeam } from './client';
|
||||
import { UnauthorizedError } from './UnauthorizedError';
|
||||
import WelcomeGameBlock from './WelcomeGameBlock.vue';
|
||||
import MessageCloud from './MessageCloud.vue';
|
||||
import GameInputForm from './GameInputForm.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
type Application = {
|
||||
name: string
|
||||
const isPlaceFilled = ref(false)
|
||||
const login = ref("")
|
||||
const password = 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("-")
|
||||
|
||||
async function getTeam() {
|
||||
let data: Team
|
||||
try {
|
||||
data = await apiGetTeam(login.value, password.value)
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// Действия при 401:
|
||||
// Сделать редирект на страницу логина
|
||||
router.push('/login');
|
||||
} else {
|
||||
console.error('Неизвестная ошибка:', error);
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Action = {
|
||||
id: string
|
||||
place: string
|
||||
name: string
|
||||
text: string
|
||||
applications: Application[]
|
||||
}
|
||||
|
||||
type Team = {
|
||||
actions: Action[]
|
||||
}
|
||||
|
||||
const place = ref("")
|
||||
const team = ref<Team>({actions: []})
|
||||
const actions = ref<Action[]>([])
|
||||
const scrollContainer = ref<HTMLDivElement | null>();
|
||||
|
||||
function getTeam() {
|
||||
fetch(
|
||||
"/team",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Id": sessionStorage.getItem("teamId") || "",
|
||||
"X-Password": sessionStorage.getItem("password") || ""
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(response => response.json())
|
||||
.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
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
router.push('/login');
|
||||
console.error('Ошибка:', error)
|
||||
});
|
||||
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: Door) => { return door.show })
|
||||
}
|
||||
}
|
||||
|
||||
function addAction() {
|
||||
fetch(
|
||||
"/team/actions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Id": sessionStorage.getItem("teamId") || "",
|
||||
"X-Password": sessionStorage.getItem("password") || ""
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"place": place.value
|
||||
})
|
||||
}
|
||||
)
|
||||
.then(async () => {place.value = ""})
|
||||
}
|
||||
|
||||
const scrollToBottom = async (behavior: ScrollBehavior = 'smooth'): Promise<void> => {
|
||||
const scrollToBottom = async (behavior: ScrollBehavior = 'smooth'): Promise<void> => {
|
||||
await nextTick();
|
||||
if (scrollContainer.value) {
|
||||
scrollContainer.value.scrollTo({
|
||||
@@ -75,143 +64,182 @@
|
||||
behavior
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Автоматическая прокрутка при изменении items
|
||||
watch(actions, () => {
|
||||
async function getGame() {
|
||||
qrurl.value = location.href
|
||||
const data = await apiGetGame(login.value, password.value)
|
||||
gameState.value = data.state
|
||||
if (data.state === "NEW") {
|
||||
gameStateText.value = "Игра ещё не началась"
|
||||
}
|
||||
if (data.state === "RUN") {
|
||||
gameStateText.value = ""
|
||||
}
|
||||
if (data.state === "STOP") {
|
||||
gameStateText.value = "Игра остановлена"
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматическая прокрутка при изменении items
|
||||
watch(actions, () => {
|
||||
if (isPlaceFilled.value === false) {
|
||||
return
|
||||
}
|
||||
scrollToBottom();
|
||||
}, { deep: true });
|
||||
isPlaceFilled.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)
|
||||
}
|
||||
|
||||
let intervalId = 0
|
||||
onMounted(() => {
|
||||
getTeam()
|
||||
|
||||
intervalId = setInterval(() => {getTeam()}, 2000);
|
||||
intervalId = setInterval(() => {
|
||||
getTeam()
|
||||
getGame()
|
||||
}, 2000);
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
clearInterval(intervalId);
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div class="body-custom">
|
||||
|
||||
<div class="header-block">
|
||||
Вечерний детектив
|
||||
</div>
|
||||
|
||||
<!-- Форма ввода -->
|
||||
<div class="form-custom form-block">
|
||||
<div class="center-block-custom">
|
||||
<form @submit.prevent="addAction">
|
||||
<div>
|
||||
<input class="input-custom" v-model="place" type="text" placeholder="Место назначения (А-1, а-1, а1)">
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<button class="button-custom" type="submit">Поехали</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<GameHeader></GameHeader>
|
||||
|
||||
<GameInputForm v-model="isPlaceFilled" :gameState="gameState" :login="login" :password="password"></GameInputForm>
|
||||
|
||||
<!-- Qr Код -->
|
||||
<div v-if="!team || !team.actions.length">
|
||||
<div class="messages-block center-container">
|
||||
<WelcomeGameBlock :qrurl="qrurl" :team="team.name"></WelcomeGameBlock>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Действия -->
|
||||
<div v-else>
|
||||
<div class="messages-block" ref="scrollContainer">
|
||||
<div class="center-block-custom">
|
||||
<div v-if="!team || !team.actions.length">
|
||||
<div class="center-message">
|
||||
Пора решать загадку
|
||||
</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 }}
|
||||
</div>
|
||||
<hr class="hr"/>
|
||||
<div class="message-content">
|
||||
{{ action.text }}
|
||||
</div>
|
||||
<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 v-for="(action, index) in team.actions" :key="action.id">
|
||||
<MessageCloud v-model="isPlaceFilled" :action="action" :gameState="gameState" :login="login"
|
||||
:password="password" :index="index" :count="team.actions.length"></MessageCloud>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="gameState == 'STOP'" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hr {
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
.body-custom {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.form-custom {
|
||||
border: 1px solid #444444;
|
||||
background-color: var(--main-back-color);
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
/* Полупрозрачный черный цвет */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* Центрируем по горизонтали */
|
||||
align-items: center;
|
||||
/* Центрируем по вертикали */
|
||||
z-index: 9999;
|
||||
/* Поверх всех остальных элементов */
|
||||
backdrop-filter: blur(3px);
|
||||
/* Эффект размытия заднего фона (по желанию) */
|
||||
}
|
||||
|
||||
.message-cloud {
|
||||
border: 1px solid #444444;
|
||||
border-radius: 15px;
|
||||
margin: 12px 0;
|
||||
padding: 16px;
|
||||
background-color: var(--main-back-item-color);
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
min-height: 250px;
|
||||
background-position: center;
|
||||
/* Картинка центрируется */
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
|
||||
background-image: url("@/assets/images/stop.png");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
font-size: small;
|
||||
.modal-body {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-footer {
|
||||
color: var(--second-color);
|
||||
}
|
||||
|
||||
.form-block {
|
||||
height: 140px;
|
||||
.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;
|
||||
}
|
||||
|
||||
.messages-block {
|
||||
height: calc(100dvh - 140px - 50px);
|
||||
top: 90px;
|
||||
/* height: calc(100dvh - 100px - 76px); */
|
||||
/* 90px от верхнего края экрана до второго ремня */
|
||||
/* 100px от верхнего края экрана до края иконки */
|
||||
/* 76px от нижнего края экрана до края ремня */
|
||||
height: calc(100dvh - 90px - 76px);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
position: relative;
|
||||
padding: 15px 10px 15px 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.center-block-custom {
|
||||
width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.team-name-block {
|
||||
margin-right: 10px;
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
font-family: a_OldTyper;
|
||||
}
|
||||
|
||||
.center-message {
|
||||
height: calc(100dvh - 140px);
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
/* код */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="text-with-font"><slot></slot></span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-with-font {
|
||||
font-family: a_OldTyper;
|
||||
color: #bfa07d;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
line-height: 20px;
|
||||
font-size: 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { apiGetTeam } from './client';
|
||||
import { UnauthorizedError } from './UnauthorizedError';
|
||||
import MessagePaper from './MessagePaper.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
const login = ref("")
|
||||
const password = ref("")
|
||||
const buttonText = ref("Вход")
|
||||
const errorMsg = ref("")
|
||||
const login = ref("")
|
||||
const password = ref("")
|
||||
const buttonText = ref("Вход")
|
||||
const errorMsg = ref("")
|
||||
|
||||
function onClickLogin() {
|
||||
async function onClickLogin() {
|
||||
const oldText = buttonText.value
|
||||
buttonText.value = "Загрузка..."
|
||||
errorMsg.value = ""
|
||||
fetch(
|
||||
"/team",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Id": login.value,
|
||||
"X-Password": password.value
|
||||
},
|
||||
|
||||
try {
|
||||
await apiGetTeam(login.value, password.value)
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
// Действия при 401:
|
||||
// Вывести ошибку
|
||||
if (login.value == "" && password.value == "") {
|
||||
return
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
if (response.status == 200) {
|
||||
errorMsg.value = "Не верны название команды или пароль"
|
||||
} else {
|
||||
errorMsg.value = "Сервер не доступен"
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
buttonText.value = oldText
|
||||
}
|
||||
|
||||
sessionStorage.setItem("teamId", login.value)
|
||||
sessionStorage.setItem("password", password.value)
|
||||
router.push('/');
|
||||
}
|
||||
if (response.status == 401) {
|
||||
errorMsg.value = "Не верны название команды или пароль"
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
errorMsg.value = "Сервер не доступен"
|
||||
})
|
||||
.finally(() => {buttonText.value = oldText});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
login.value = sessionStorage.getItem("teamId") || ""
|
||||
password.value = sessionStorage.getItem("password") || ""
|
||||
|
||||
onClickLogin()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -46,6 +54,7 @@
|
||||
</div>
|
||||
|
||||
<div class="center-message">
|
||||
<MessagePaper>
|
||||
<form @submit.prevent="onClickLogin">
|
||||
<div>
|
||||
<input class="input-custom" v-model="login" type="text" placeholder="Название команды">
|
||||
@@ -60,12 +69,32 @@
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
</form>
|
||||
</MessagePaper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-custom {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* обязательно! */
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: brown;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.input-custom,
|
||||
.button-custom {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import type { Action } from './models';
|
||||
import { apiLetsgo } from './client';
|
||||
import MessagePaper from './MessagePaper.vue';
|
||||
|
||||
interface Props {
|
||||
action: Action
|
||||
gameState: string
|
||||
login: string
|
||||
password: string
|
||||
index: number
|
||||
count: number
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const isPlaceFilled = defineModel<boolean>();
|
||||
|
||||
function clickCollapse() {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.action.isOpen = !props.action.isOpen
|
||||
}
|
||||
|
||||
async function letsgo(place: string) {
|
||||
isPlaceFilled.value = true
|
||||
await apiLetsgo(props.login, props.password, place)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MessagePaper>
|
||||
<div class="message-header">
|
||||
{{ props.action.place }}: {{ props.action.name }}
|
||||
<span v-if="props.action.isOpen" class="collapse-icon collapse-icon-up" @click="clickCollapse"></span>
|
||||
<span v-else class="collapse-icon" @click="clickCollapse"></span>
|
||||
</div>
|
||||
<div v-show="props.action.isOpen">
|
||||
<hr class="hr" />
|
||||
<div class="message-content">
|
||||
<div v-if="props.action.image.length">
|
||||
<div class="message-image-border">
|
||||
<img v-bind:src="props.action.image" class="message-image" />
|
||||
</div>
|
||||
</div>{{ props.action.text }}
|
||||
</div>
|
||||
<div v-if="index == count - 1">
|
||||
<hr class="hr" v-if="props.action.buttons?.length" />
|
||||
<div v-for="door in props.action.buttons" :key="door.code" class="button-dialog" v-on:click="letsgo(door.code)"
|
||||
:disabled="gameState !== 'RUN' || !door.show">
|
||||
<div class="button-dialog-text">
|
||||
{{ door.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="hr" v-if="props.action.applications.length" />
|
||||
<div class="message-footer" v-for="application in props.action.applications" :key="application.name">
|
||||
Приложение: {{ application.name }}
|
||||
<div class="application-label">{{ application.number }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MessagePaper>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hr {
|
||||
margin: 10px 0;
|
||||
border: dashed 1px;
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
font-size: 20px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 45px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
background-image: url("@/assets/images/collapse.png");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.collapse-icon-up {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-image-border {
|
||||
width: 40%;
|
||||
float: left;
|
||||
padding: 7px;
|
||||
margin-right: 15px;
|
||||
background-image: url("@/assets/images/paper_white.jpg");
|
||||
background-size: cover;
|
||||
box-shadow: 0px 3px 15px rgb(98, 98, 98);
|
||||
transform: rotate(-3deg);
|
||||
}
|
||||
|
||||
.message-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-dialog {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
background-image: url("@/assets/images/belt.png");
|
||||
background-size: cover;
|
||||
color: #bfa07d;
|
||||
}
|
||||
|
||||
.button-dialog-text {
|
||||
cursor: pointer;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.message-footer {
|
||||
padding-right: 50px;
|
||||
font-weight: 400;
|
||||
color: var(--second-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.application-label {
|
||||
background-image: url("@/assets/images/label.png");
|
||||
background-size: cover;
|
||||
width: 30px;
|
||||
height: 52px;
|
||||
text-align: center;
|
||||
padding-top: 22px;
|
||||
color: black;
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: 5px;
|
||||
transform: rotate(9deg);
|
||||
font-size: 30px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
/* код */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main">
|
||||
<div class="message-cloud-3"></div>
|
||||
<div class="message-cloud-2"></div>
|
||||
<div class="message-cloud">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-cloud, .message-cloud-2, .message-cloud-3 {
|
||||
background-color: bisque;
|
||||
}
|
||||
|
||||
.message-cloud {
|
||||
margin: 15px 0;
|
||||
padding: 16px;
|
||||
font-family: main;
|
||||
color: black;
|
||||
line-height: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-cloud,
|
||||
.message-cloud-2,
|
||||
.message-cloud-3 {
|
||||
border-radius: 5px;
|
||||
background-image: url("@/assets/images/paper.jpg");
|
||||
background-size: cover;
|
||||
display: flow-root;
|
||||
box-shadow: 0px 0 5px black;
|
||||
}
|
||||
|
||||
.message-cloud-2,
|
||||
.message-cloud-3 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.message-cloud-2 {
|
||||
transform: rotate(-3deg);
|
||||
filter: brightness(50%);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-cloud-3 {
|
||||
transform: rotate(2deg);
|
||||
filter: brightness(80%);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plate-block">
|
||||
<div class="metal-plate-block">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="pin pin-top-left"></div>
|
||||
<div class="pin pin-top-right"></div>
|
||||
<div class="pin pin-bottom-right"></div>
|
||||
<div class="pin pin-bottom-left"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.plate-block {
|
||||
position: relative;
|
||||
box-shadow: 0px 0px 10px black;
|
||||
}
|
||||
|
||||
.metal-plate-block {
|
||||
height: 100%;
|
||||
background-image: url("@/assets/images/metal.png");
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pin {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
position: absolute;
|
||||
background-image: url("@/assets/images/pin.png");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.pin-top-left {
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.pin-top-right {
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.pin-bottom-right {
|
||||
bottom: 3px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.pin-bottom-left {
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(message: string = 'Пользователь не авторизован (401)') {
|
||||
super(message)
|
||||
this.name = 'UnauthorizedError'
|
||||
|
||||
// Восстанавливаем цепочку прототипов (нужно для корректной работы instanceof в TS)
|
||||
Object.setPrototypeOf(this, UnauthorizedError.prototype)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import VueQrcode from '@chenfengyuan/vue-qrcode';
|
||||
import MessagePaper from './MessagePaper.vue';
|
||||
|
||||
interface QROptions {
|
||||
width?: number;
|
||||
margin?: number;
|
||||
color?: {
|
||||
dark: string;
|
||||
light: string;
|
||||
};
|
||||
}
|
||||
const qrOptions = ref<QROptions>({
|
||||
width: 200,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#303030',
|
||||
light: '#f0f0f0'
|
||||
}
|
||||
});
|
||||
|
||||
interface Props {
|
||||
qrurl: string
|
||||
team: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MessagePaper>
|
||||
<div class="qr">
|
||||
<div class="team-name">
|
||||
{{ team }}
|
||||
</div>
|
||||
<VueQrcode :value="props.qrurl" :options="qrOptions" tag="svg" class="qr-code" />
|
||||
<div class="message">
|
||||
Пора решать загадку
|
||||
</div>
|
||||
</div>
|
||||
</MessagePaper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qr {
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
margin: 12px 0;
|
||||
box-shadow: 0px 3px 15px rgb(98, 98, 98);
|
||||
}
|
||||
|
||||
.team-name {
|
||||
margin: 10px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 7px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { Game, Team } from './models'
|
||||
import { UnauthorizedError } from './UnauthorizedError'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
export const apiGetTeam = async (login: string, password: string): Promise<Team> => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/team'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Id': encodeUTF8ToBase64(login),
|
||||
'X-Password': password,
|
||||
},
|
||||
})
|
||||
if (response.status === 401) {
|
||||
throw new UnauthorizedError('Ошибка авторизации')
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`http error status: ${response.status}`)
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('[apiGetTeam] error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const apiLetsgo = async (login: string, password: string, place: string) => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/team/actions'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Id': encodeUTF8ToBase64(login),
|
||||
'X-Password': password,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
place: place,
|
||||
}),
|
||||
})
|
||||
if (response.status === 401) {
|
||||
throw new UnauthorizedError('Ошибка авторизации')
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`http error status: ${response.status}`)
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('[apiLetsgo] error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const apiGetGame = async (login: string, password: string): Promise<Game> => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/game'), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Id': encodeUTF8ToBase64(login),
|
||||
'X-Password': password,
|
||||
},
|
||||
})
|
||||
if (response.status === 401) {
|
||||
throw new UnauthorizedError('Ошибка авторизации')
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`http error status: ${response.status}`)
|
||||
}
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('[apiGetGame] error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function getApiUrl(path: string) {
|
||||
if (API_URL === '') {
|
||||
return 'http://' + window.location.host.split(':')[0] + ':8090' + path
|
||||
}
|
||||
return API_URL + path
|
||||
}
|
||||
|
||||
function encodeUTF8ToBase64(s: string) {
|
||||
return btoa(
|
||||
encodeURIComponent(s).replace(/%([0-9A-F]{2})/g, (_, p1) =>
|
||||
String.fromCharCode(parseInt(p1, 16)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
// Игра
|
||||
export type Game = {
|
||||
// Статус игры (NEW, RUN, STOP)
|
||||
state: string
|
||||
// Время начала игры
|
||||
startAt: string
|
||||
// Время окончания игры
|
||||
endAt: string
|
||||
}
|
||||
|
||||
// Команда
|
||||
export type Team = {
|
||||
// Название
|
||||
name: string
|
||||
// Совершенные действия
|
||||
actions: Action[]
|
||||
}
|
||||
|
||||
// Действие
|
||||
// Посещение точки игры
|
||||
export type Action = {
|
||||
// Идентификатор действия
|
||||
id: string
|
||||
// Код точки
|
||||
place: string
|
||||
// Название точки
|
||||
name: string
|
||||
// Текст точки
|
||||
text: string
|
||||
// Ссылка на картинку
|
||||
image: string
|
||||
// Список приложений
|
||||
applications: Application[]
|
||||
// Видимость/доступность точки
|
||||
hidden: boolean
|
||||
// Двери точки
|
||||
doors: Door[]
|
||||
|
||||
// Поля для интерфейса
|
||||
// Сворачивание
|
||||
isOpen: boolean
|
||||
// Кнопки
|
||||
buttons: Door[]
|
||||
}
|
||||
|
||||
// Приложение
|
||||
// Улики найденые игроками (Карта, фотография...)
|
||||
export type Application = {
|
||||
// Наименование
|
||||
name: string
|
||||
// Номер
|
||||
number: number
|
||||
}
|
||||
|
||||
// Дверь
|
||||
// Выбор пути в диалоге с игроком
|
||||
// Или действие игрока которое можно не использовать
|
||||
// Или открытие действия без участия игрока
|
||||
export type Door = {
|
||||
// Код точки куда открывается дверь (Целевая точка должна существовать)
|
||||
code: string
|
||||
// Текст кнопки (Должен совпадать с названием целевой точки)
|
||||
name: string
|
||||
// Видимость кнопки
|
||||
show: boolean
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
// Интерфейсы и типы
|
||||
export interface Todo {
|
||||
id: number
|
||||
title: string
|
||||
completed: boolean
|
||||
priority: 'low' | 'medium' | 'high'
|
||||
createdAt: Date
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
// Тип для пропсов
|
||||
interface Props {
|
||||
todo: Todo
|
||||
showPriority?: boolean
|
||||
index?: number
|
||||
}
|
||||
|
||||
// Тип для событий
|
||||
interface Emits {
|
||||
(e: 'update', todo: Todo): void
|
||||
(e: 'delete', id: number): void
|
||||
(e: 'click', index: number): void
|
||||
}
|
||||
|
||||
// Определение пропсов с типами
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showPriority: true,
|
||||
index: 0
|
||||
})
|
||||
|
||||
// Определение событий с типами
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Реактивные данные с типами
|
||||
// const isHovered = ref<boolean>(false)
|
||||
const clickCount = ref<number>(0)
|
||||
|
||||
// Вычисляемые свойства с типами
|
||||
const priorityClass = computed((): string => {
|
||||
const priorityMap = {
|
||||
low: 'priority-low',
|
||||
medium: 'priority-medium',
|
||||
high: 'priority-high'
|
||||
}
|
||||
return priorityMap[props.todo.priority]
|
||||
})
|
||||
|
||||
// Методы с типами
|
||||
const handleClick = (event: MouseEvent): void => {
|
||||
clickCount.value++
|
||||
emit('click', props.index)
|
||||
console.log(`Клик #${clickCount.value}`, event)
|
||||
}
|
||||
|
||||
const toggleComplete = (event: Event): void => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const updatedTodo: Todo = {
|
||||
...props.todo,
|
||||
completed: input.checked
|
||||
}
|
||||
emit('update', updatedTodo)
|
||||
}
|
||||
|
||||
const deleteTodo = (): void => {
|
||||
emit('delete', props.todo.id)
|
||||
}
|
||||
|
||||
// Типизированные watchers
|
||||
watch(() => props.todo.completed, (newValue: boolean, oldValue: boolean) => {
|
||||
console.log(`Статус изменен: ${oldValue} -> ${newValue}`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="todo-item" :class="{ completed: todo.completed }" @click="handleClick">
|
||||
<input type="checkbox" :checked="todo.completed" @change="toggleComplete" />
|
||||
<span>{{ todo.title }}</span>
|
||||
<span class="priority" :class="priorityClass">
|
||||
{{ todo.priority }}
|
||||
</span>
|
||||
<button @click.stop="deleteTodo">Удалить</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* стили компонента */
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
/* код */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* стили компонента */
|
||||
</style>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
// TODO: Попробуй сделать чтобы "@" указывала на папку src и получается красивые ссылки
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
|
||||
@@ -15,14 +16,6 @@ const router = createRouter({
|
||||
name: 'login',
|
||||
component: LoginView,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||