Compare commits

..

79 Commits

Author SHA1 Message Date
VLADIMIR 0943fa0400 fix pointer 2026-05-29 09:33:47 +07:00
VLADIMIR e04a877532 fix local 2026-05-29 08:49:07 +07:00
VLADIMIR f314fed7ad up 2026-05-17 17:15:15 +07:00
VLADIMIR a5acda73ae ups 2026-05-17 16:06:19 +07:00
VLADIMIR 578e22b8f0 fix 2026-05-17 02:24:43 +07:00
VLADIMIR 58c402b0f3 compress images 2026-05-11 11:24:39 +07:00
VLADIMIR 74c31cf407 fix 2026-04-21 00:06:09 +07:00
VLADIMIR 84071feb24 update images 2026-04-20 23:42:59 +07:00
VLADIMIR 25dc69e4df add ico 2026-03-25 14:08:18 +07:00
VLADIMIR 4758db0283 fix 2026-03-25 13:15:02 +07:00
VLADIMIR fd6ededacf fix 2026-03-25 13:10:08 +07:00
VLADIMIR f7da699cfb fix 2026-03-25 12:46:24 +07:00
VLADIMIR fdf27203a3 add collapse icon 2026-03-25 12:45:23 +07:00
VLADIMIR ed178d57a9 light photos 2026-03-25 00:01:41 +07:00
VLADIMIR ee4ee4dbd9 fix 2026-03-24 23:55:51 +07:00
VLADIMIR 82e0f9ffb4 add stop window 2026-03-24 22:19:21 +07:00
VLADIMIR 54d4864586 fix 2026-03-24 16:26:55 +07:00
VLADIMIR 9e5c63da5a fix center 2026-03-24 16:18:20 +07:00
VLADIMIR c8dbce1462 update welcome window 2026-03-24 14:55:51 +07:00
VLADIMIR 39928b2615 Update 2026-03-24 02:42:24 +07:00
VLADIMIR cfb58ca82c fix 2026-03-24 02:19:53 +07:00
VLADIMIR faf6725a99 add image border 2026-03-23 01:01:57 +07:00
VLADIMIR 8d8a276da0 add label 2026-03-23 00:48:53 +07:00
VLADIMIR 54ce3e5e7a up 2026-03-22 04:33:32 +07:00
VLADIMIR 977f9cc7eb clear 2026-03-22 03:21:29 +07:00
VLADIMIR f4964e6355 add input form 2026-03-22 02:50:02 +07:00
VLADIMIR 6b261804fd Game input form start 2026-03-22 02:30:23 +07:00
VLADIMIR 7a1480b810 clear 2026-03-22 02:24:01 +07:00
VLADIMIR dda3bc83d8 add message cloud 2026-03-22 02:21:27 +07:00
VLADIMIR 3c10c311d1 Add hr 2026-03-22 02:06:35 +07:00
VLADIMIR 3d01fea198 add message cloud v1 2026-03-22 02:02:12 +07:00
VLADIMIR 4adae4e2d6 add HeaderText 2026-03-22 01:42:08 +07:00
VLADIMIR 20986e86e2 clear 2026-03-22 01:39:16 +07:00
VLADIMIR 9e6ee6e7c3 add welcome block 2026-03-22 01:33:21 +07:00
VLADIMIR e8158eb746 add client 2026-03-22 01:06:04 +07:00
VLADIMIR 614ec9e059 add get team method 2026-03-22 00:52:12 +07:00
VLADIMIR ccad38799f add models.ts 2026-03-22 00:10:55 +07:00
VLADIMIR b56a4570de clear 2026-03-21 01:03:18 +07:00
VLADIMIR 80d581e3b6 add team 2026-03-21 00:37:58 +07:00
VLADIMIR dd41cd08dd add action 2026-03-21 00:29:21 +07:00
VLADIMIR 7b620dffd4 add door 2026-03-21 00:04:16 +07:00
VLADIMIR b676129ec2 add Application model 2026-03-20 23:56:16 +07:00
VLADIMIR 5fee2035aa add header 2026-03-20 23:19:33 +07:00
VLADIMIR 5ea37e155d add template 2026-03-20 22:57:04 +07:00
VLADIMIR 19cf4c68b4 updates 2026-03-20 03:35:52 +07:00
VLADIMIR ac58312aa2 update 2026-03-20 03:02:17 +07:00
VLADIMIR f08399c76c updates 2026-03-20 02:03:15 +07:00
VLADIMIR 880c305731 update 2026-03-19 13:34:36 +07:00
VLADIMIR aa60c54c8f add button 2026-03-19 12:29:31 +07:00
VLADIMIR 25f1da5a32 fix 2026-03-18 03:29:08 +07:00
VLADIMIR bf04f83a15 start new design 2026-03-18 01:56:56 +07:00
VLADIMIR 365a231ed5 add template component 2026-03-18 00:47:08 +07:00
VLADIMIR 200704bf92 add images 2026-03-14 17:00:42 +07:00
VLADIMIR 48d2a814fe add doors 2026-03-07 22:39:06 +07:00
VLADIMIR 4f6a96a49a add team name and rm scroll 2026-03-01 02:01:01 +07:00
VLADIMIR 046ebfc62f add collapse item 2026-03-01 01:28:11 +07:00
VLADIMIR 9bde8ff7ee fix 2025-12-07 23:09:35 +07:00
VLADIMIR 5b3203e5aa fix time 2025-11-04 18:24:47 +07:00
VLADIMIR 875207d9ff update 2025-09-23 01:09:42 +07:00
Владимир Федоров aa7a9bf0cc clear 2025-08-10 20:19:57 +07:00
k.ukolov 1026223957 Update src/components/TheWelcome.vue 2025-06-26 14:49:13 +00:00
k.ukolov 1f5ae42890 Update src/router/index.ts 2025-06-26 14:43:48 +00:00
k.ukolov 3e59047d73 Update src/components/HelloWorld.vue 2025-06-26 14:22:37 +00:00
VLADIMIR 9fe699f11e add qr 2025-06-04 02:37:43 +07:00
VLADIMIR 0280c7158b fix 2025-06-03 03:08:10 +07:00
VLADIMIR 1ca5c8d4a1 fix 2025-06-03 00:37:47 +07:00
VLADIMIR 4c9d6cf6d3 fix 2025-05-31 04:15:22 +07:00
VLADIMIR 06aac3d552 fix 2025-05-31 04:12:24 +07:00
VLADIMIR 6180cbffa1 fix 2025-05-31 04:09:23 +07:00
VLADIMIR ceb744ec12 add game 2025-05-31 04:05:37 +07:00
VLADIMIR 6a8b7edb45 up 2025-05-29 02:36:33 +07:00
VLADIMIR 21160cd349 up 2025-05-20 03:29:23 +07:00
VLADIMIR 39cdf1c162 fix login 2025-05-20 02:04:33 +07:00
VLADIMIR be7ed728a3 update login 2025-05-19 04:52:19 +07:00
VLADIMIR 38518e465d up 2025-05-19 03:55:36 +07:00
VLADIMIR bc88340480 fix 2025-05-19 02:35:35 +07:00
VLADIMIR e97a318237 add net 2025-05-19 00:15:15 +07:00
VLADIMIR 75c11b36a6 rm net 2025-05-18 22:59:20 +07:00
VLADIMIR 02c5451c5b errors 2025-05-18 22:13:25 +07:00
64 changed files with 1920 additions and 1630 deletions
+1
View File
@@ -0,0 +1 @@
VITE_API_URL='https://evening-detective-api.crabs-games.art'
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=''
+9 -4
View File
@@ -1,4 +1,9 @@
build: build-macos:
npm run build npm run build:local_web
rm -rf ../evening_detective/static/user rm -rf ../evening_detective/cmd/evening_detective/static/user
cp -r dist ../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
+643 -1092
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -4,7 +4,9 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "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 {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
@@ -13,13 +15,15 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@chenfengyuan/vue-qrcode": "^2.0.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"qrcode-vue3": "^1.7.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.1", "@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.0", "@types/node": "^22.19.19",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0", "@vue/eslint-config-typescript": "^14.5.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.
+6 -3
View File
@@ -1,5 +1,7 @@
/* color palette from <https://github.com/vuejs/theme> */ /* color palette from <https://github.com/vuejs/theme> */
:root { :root {
color-scheme: only light;
--vt-c-white: #ffffff; --vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8; --vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2; --vt-c-white-mute: #f2f2f2;
@@ -21,8 +23,8 @@
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64); --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
/* Главный цвет */ /* Главный цвет */
--main-color: rgba(115, 185, 83, 1); --main-color: rgba(34, 50, 60, 1);
--second-color: rgba(98, 156, 68, 1); --second-color: rgb(97, 74, 22);
--main-back-color: rgba(240, 240, 240, 1); --main-back-color: rgba(240, 240, 240, 1);
--main-back-item-color: rgba(254, 254, 254, 1); --main-back-item-color: rgba(254, 254, 254, 1);
} }
@@ -67,7 +69,6 @@
body { body {
min-height: 100dvh; min-height: 100dvh;
color: var(--color-text); color: var(--color-text);
background: var(--main-back-color);
transition: transition:
color 0.5s, color 0.5s,
background-color 0.5s; background-color 0.5s;
@@ -89,4 +90,6 @@ body {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-image: url("@/assets/images/forest.png");
background-size: cover;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

+38 -32
View File
@@ -1,41 +1,19 @@
@import './base.css'; @import './base.css';
.header-block { body {
height: 50px; overflow: hidden;
background-color: var(--main-color); background-color: black;
font-size: large; scrollbar-width: none;
color: white;
vertical-align: middle;
padding: 10px 0 10px 16px;
font-weight: 700;
} }
.input-custom { @font-face {
width: 100%; font-family: a_OldTyper;
box-sizing: border-box; /* обязательно! */ src: url('@/assets/a_OldTyper.ttf');
margin-bottom: 15px;
} }
.button-custom { @font-face {
margin-left: auto; font-family: main;
background-color: var(--main-color); src: url('@/assets/main.ttf');
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;
} }
.center-message { .center-message {
@@ -45,3 +23,31 @@
height: calc(100dvh - 100px); height: calc(100dvh - 100px);
text-align: center; /* центрирование текста */ 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;
}
Binary file not shown.
+17
View File
@@ -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

+38
View File
@@ -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>
+28
View File
@@ -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>
+89
View File
@@ -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>
+144
View File
@@ -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>
+166 -136
View File
@@ -1,68 +1,59 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, watch, onMounted } from 'vue'; import { ref, nextTick, watch, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { host } from './net'; 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 = { const isPlaceFilled = ref(false)
name: string const login = ref("")
} const password = ref("")
const team = ref<Team>({ name: "", actions: [] })
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 actions = ref<Action[]>([])
const scrollContainer = ref<HTMLDivElement | null>(); const scrollContainer = ref<HTMLDivElement | null>();
const gameState = ref("STOP")
const gameStateText = ref("")
function getTeam() { const qrurl = ref("-")
fetch(
host+"/team", async function getTeam() {
{ let data: Team
method: "GET", try {
headers: { data = await apiGetTeam(login.value, password.value)
"X-Id": sessionStorage.getItem("teamId") || "", } catch (error: unknown) {
"X-Password": sessionStorage.getItem("password") || "" if (error instanceof UnauthorizedError) {
}, // Действия при 401:
// Сделать редирект на страницу логина
router.push('/login');
} else {
console.error('Неизвестная ошибка:', error);
} }
) return
.then(response => response.json()) }
.then(data => {
const oldActions = team.value.actions
team.value = data team.value = data
const newActions = team.value?.actions 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) { if (actions.value.length !== newActions?.length) {
actions.value = newActions actions.value = newActions
} }
}) for (let i = 0; i < team.value.actions.length; i++) {
.catch(error => { const element = team.value.actions[i];
router.push('/login'); team.value.actions[i].buttons = element.doors.filter((door: Door) => { return door.show })
console.error('Ошибка:', error)
});
} }
function addAction() {
fetch(host+"/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> => {
@@ -75,141 +66,180 @@
} }
}; };
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 // Автоматическая прокрутка при изменении items
watch(actions, () => { watch(actions, () => {
if (isPlaceFilled.value === false) {
return
}
scrollToBottom(); scrollToBottom();
isPlaceFilled.value = false
}, { deep: true }); }, { deep: true });
let intervalId = 0 let intervalId = 0
onMounted(() => { 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() getTeam()
intervalId = setInterval(() => {getTeam()}, 2000); intervalId = setInterval(() => {
getTeam()
getGame()
}, 2000);
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
clearInterval(intervalId); clearInterval(intervalId);
next(); next();
}); });
}); });
</script> </script>
<template> <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> <div>
<input class="input-custom" v-model="place" type="text" placeholder="Место назначения (А-1, а-1, а1)"> <GameHeader></GameHeader>
</div>
<div class="button-container">
<button class="button-custom" type="submit">Поехали</button>
</div>
</form>
</div>
</div>
<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="messages-block" ref="scrollContainer">
<div class="center-block-custom"> <div class="center-block-custom">
<div v-if="!team || !team.actions.length"> <div v-for="(action, index) in team.actions" :key="action.id">
<div class="center-message"> <MessageCloud v-model="isPlaceFilled" :action="action" :gameState="gameState" :login="login"
Пора решать загадку :password="password" :index="index" :count="team.actions.length"></MessageCloud>
</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> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="gameState == 'STOP'" class="modal-overlay">
<div class="modal-content">
<div class="modal-body"></div>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
body { .modal-overlay {
overflow: hidden;
}
.hr {
margin: 7px 0;
}
.body-custom {
font-size: medium;
}
.form-custom {
border: 1px solid #444444;
background-color: var(--main-back-color);
position: fixed; position: fixed;
bottom: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100vw;
padding: 20px; height: 100vh;
color: white; 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 { .modal-content {
border: 1px solid #444444; position: relative;
border-radius: 15px; width: 90%;
margin: 12px 0; max-width: 420px;
padding: 16px; min-height: 250px;
background-color: var(--main-back-item-color); background-position: center;
/* Картинка центрируется */
background-repeat: no-repeat;
overflow: hidden;
background-image: url("@/assets/images/stop.png");
background-size: cover;
} }
.message-header { .modal-body {
font-size: small; position: relative;
z-index: 2;
padding: 30px;
} }
.message-content { .game-input-form-shadow {
font-weight: 500; height: 90px;
} width: 120%;
left: -10%;
.message-footer { top: 3px;
color: var(--second-color); position: absolute;
} box-shadow: 0px -5px 10px black;
z-index: 9;
.form-block { background-color: black;
height: 140px;
} }
.messages-block { .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; overflow-y: auto;
scrollbar-width: none; scrollbar-width: none;
position: relative;
padding: 15px 10px 15px 10px;
} }
@media (min-width: 1025px) { .team-name-block {
.center-block-custom { margin-right: 10px;
width: 700px; width: 50px;
margin: 0 auto; height: 40px;
} font-family: a_OldTyper;
} }
.center-message { .text-middle-wrapper {
height: calc(100dvh - 140px); 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> </style>
+18
View File
@@ -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>
-41
View File
@@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve 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>
+62 -18
View File
@@ -1,34 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { host } from './net'; import { apiGetTeam } from './client';
import { UnauthorizedError } from './UnauthorizedError';
import MessagePaper from './MessagePaper.vue';
const router = useRouter(); const router = useRouter();
const login = ref("") const login = ref("")
const password = ref("") const password = ref("")
const buttonText = ref("Вход")
const errorMsg = ref("")
function onClickLogin() { async function onClickLogin() {
fetch( const oldText = buttonText.value
host+"/team", buttonText.value = "Загрузка..."
{ errorMsg.value = ""
method: "GET",
headers: { try {
"X-Id": login.value, await apiGetTeam(login.value, password.value)
"X-Password": password.value } catch (error: unknown) {
}, if (error instanceof UnauthorizedError) {
// Действия при 401:
// Вывести ошибку
if (login.value == "" && password.value == "") {
return
} }
) errorMsg.value = "Не верны название команды или пароль"
.then(response => { } else {
if (response.status == 200) { errorMsg.value = "Сервер не доступен"
}
return
} finally {
buttonText.value = oldText
}
sessionStorage.setItem("teamId", login.value) sessionStorage.setItem("teamId", login.value)
sessionStorage.setItem("password", password.value) sessionStorage.setItem("password", password.value)
router.push('/'); router.push('/');
} }
})
.catch(error => {console.error('Ошибка:', error)}); onMounted(() => {
} login.value = sessionStorage.getItem("teamId") || ""
password.value = sessionStorage.getItem("password") || ""
onClickLogin() onClickLogin()
})
</script> </script>
<template> <template>
@@ -37,6 +54,7 @@
</div> </div>
<div class="center-message"> <div class="center-message">
<MessagePaper>
<form @submit.prevent="onClickLogin"> <form @submit.prevent="onClickLogin">
<div> <div>
<input class="input-custom" v-model="login" type="text" placeholder="Название команды"> <input class="input-custom" v-model="login" type="text" placeholder="Название команды">
@@ -45,12 +63,38 @@
<input class="input-custom" v-model="password" type="text" placeholder="Пароль" autocapitalize="off"> <input class="input-custom" v-model="password" type="text" placeholder="Пароль" autocapitalize="off">
</div> </div>
<div class="button-container"> <div class="button-container">
<button class="button-custom" type="submit">Вход</button> <button class="button-custom" type="submit">{{ buttonText }}</button>
</div>
<div class="error-message">
{{ errorMsg }}
</div> </div>
</form> </form>
</MessagePaper>
</div> </div>
</template> </template>
<style scoped> <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> </style>
+150
View File
@@ -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>
+65
View File
@@ -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>
+58
View File
@@ -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>
-94
View File
@@ -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>
Vues
<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>
+9
View File
@@ -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)
}
}
+66
View File
@@ -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>
-87
View File
@@ -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>
+88
View File
@@ -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)),
),
)
}
-7
View File
@@ -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>
-7
View File
@@ -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>
-7
View File
@@ -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>
-19
View File
@@ -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>
+66
View File
@@ -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
}
-1
View File
@@ -1 +0,0 @@
export const host = "http://192.168.0.110:8090"
+90
View File
@@ -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>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
/* код */
</script>
<template>
<div></div>
</template>
<style scoped>
/* стили компонента */
</style>
+1 -8
View File
@@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
// TODO: Попробуй сделать чтобы "@" указывала на папку src и получается красивые ссылки
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
@@ -15,14 +16,6 @@ const router = createRouter({
name: 'login', name: 'login',
component: LoginView, 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'),
},
], ],
}) })
-15
View File
@@ -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>