This commit is contained in:
Владимир Фёдоров 2025-06-17 03:04:08 +07:00
parent c52e9a6cc6
commit 7ebae67378
2 changed files with 213 additions and 156 deletions

View File

@ -23,7 +23,18 @@
color: white; color: white;
} }
.button-custom:hover { .button-custom-inline {
margin: 10px;
background-color: var(--main-color);
font-weight: 600;
color: white;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 15px;
font-size: 14px;
}
.button-custom:hover, .button-custom-inline:hover {
background-color: var(--second-color); background-color: var(--second-color);
} }

View File

@ -1,45 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { getApiUrl } from './net'; import { getApiUrl } from './net';
import router from '@/router'; import router from '@/router';
import VueQrcode from '@chenfengyuan/vue-qrcode'; import VueQrcode from '@chenfengyuan/vue-qrcode';
type Application = { type Application = {
id: number id: number
name: string name: string
} }
type Team = { type Team = {
id: number id: number
name: string name: string
password: string password: string
url: string url: string
spendTime: number spendTime: number
applications: Application[] applications: Application[]
} }
type Teams = { type Teams = {
teams: Team[] teams: Team[]
} }
type Game = { type Game = {
state: string state: string
startAt: string startAt: string
endAt: string endAt: string
} }
const qrurl = ref("-") const qrurl = ref("-")
const qrteam = ref("-") const qrteam = ref("-")
const gameState = ref("") const gameState = ref("")
const game = ref<Game>() const game = ref<Game>()
const teams = ref<Teams>({teams: []}) const teams = ref<Teams>({ teams: [] })
function getTeams() { function getTeams() {
fetch( fetch(
getApiUrl("/teams") getApiUrl("/teams")
) )
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
teams.value = data teams.value = data
@ -47,62 +47,62 @@
.catch(error => { .catch(error => {
console.error('Ошибка:', error) console.error('Ошибка:', error)
}); });
} }
function gaveApplication(teamId: number, id: number) { function gaveApplication(teamId: number, id: number) {
fetch( fetch(
getApiUrl("/teams/" + teamId + "/applications"), getApiUrl("/teams/" + teamId + "/applications"),
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
"applications": [{"id": id}] "applications": [{ "id": id }]
}) })
}
)
.then(() => {})
.catch(error => {
console.error('Ошибка:', error)
});
}
const teamName = ref("")
function addTeam() {
fetch(
getApiUrl("/teams"),
{
method: "POST",
body: JSON.stringify({
"teams": [{"name": teamName.value}]
})
}
)
.then(() => {teamName.value = ""})
.catch(error => {
console.error('Ошибка:', error)
});
}
interface QROptions {
width?: number;
margin?: number;
color?: {
dark: string;
light: string;
};
}
const qrOptions = ref<QROptions>({
width: 100,
margin: 1,
color: {
dark: '#000000',
light: 'f0f0f0'
} }
}); )
.then(() => { })
.catch(error => {
console.error('Ошибка:', error)
});
}
function getGame() { const teamName = ref("")
fetch( function addTeam() {
getApiUrl("/game") fetch(
) getApiUrl("/teams"),
{
method: "POST",
body: JSON.stringify({
"teams": [{ "name": teamName.value }]
})
}
)
.then(() => { teamName.value = "" })
.catch(error => {
console.error('Ошибка:', error)
});
}
interface QROptions {
width?: number;
margin?: number;
color?: {
dark: string;
light: string;
};
}
const qrOptions = ref<QROptions>({
width: 100,
margin: 1,
color: {
dark: '#000000',
light: 'f0f0f0'
}
});
function getGame() {
fetch(
getApiUrl("/game")
)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
game.value = data game.value = data
@ -119,44 +119,77 @@
.catch(error => { .catch(error => {
console.error('Ошибка:', error) console.error('Ошибка:', error)
}); });
} }
function startGame() { function startGame() {
gameState.value = "Загрузка..." gameState.value = "Загрузка..."
fetch( fetch(
getApiUrl("/game/start"), getApiUrl("/game/start"),
{method: "POST"} { method: "POST" }
) )
.catch(error => { .catch(error => {
console.error('Ошибка:', error) console.error('Ошибка:', error)
}); });
} }
function stopGame() { function stopGame() {
gameState.value = "Загрузка..." gameState.value = "Загрузка..."
fetch( fetch(
getApiUrl("/game/stop"), getApiUrl("/game/stop"),
{method: "POST"} { method: "POST" }
) )
.catch(error => { .catch(error => {
console.error('Ошибка:', error) console.error('Ошибка:', error)
}); });
} }
let intervalId = 0 function base64ToBytes(base64: string): Uint8Array {
onMounted(() => { const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
function downloadQrCodesFile() {
fetch(
getApiUrl("/teams/pdf")
)
.then(response => response.json())
.then(data => {
const b = base64ToBytes(data.result)
downloadFile(b, 'teams_qr_code.pdf', 'application/pdf;teams_qr_code.pdf')
})
.catch(error => {
console.error('Ошибка:', error)
});
}
function downloadFile(bytes: Uint8Array, fileName: string, mimeType: string) {
const blob = new Blob([bytes], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
URL.revokeObjectURL(url);
};
let intervalId = 0
onMounted(() => {
getTeams()
intervalId = setInterval(() => {
getTeams() getTeams()
getGame()
}, 2000);
intervalId = setInterval(() => { router.beforeEach((to, from, next) => {
getTeams() clearInterval(intervalId);
getGame() next();
}, 2000);
router.beforeEach((to, from, next) => {
clearInterval(intervalId);
next();
});
}); });
});
</script> </script>
<template> <template>
@ -165,12 +198,7 @@
</div> </div>
<div class="qr"> <div class="qr">
<VueQrcode <VueQrcode :value="qrurl" :options="qrOptions" tag="svg" class="qr-code" />
:value="qrurl"
:options="qrOptions"
tag="svg"
class="qr-code"
/>
<div> <div>
{{ qrteam }} {{ qrteam }}
</div> </div>
@ -181,33 +209,34 @@
<a v-on:click="stopGame" class="button-menu">Остановить</a> <a v-on:click="stopGame" class="button-menu">Остановить</a>
</div> </div>
<table class="table-custom"> <table>
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th>Название команды</th> <th>Название команды</th>
<th>Поездки</th> <th>Поездки</th>
<th>Приложения</th> <th>Приложения</th>
<th>Действия</th> <th><button v-on:click="downloadQrCodesFile" class="button-custom-inline">Скачать qr-ы</button></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(team, number) in teams.teams" :key="team.name"> <tr v-for="(team, number) in teams.teams" :key="team.name">
<td> <td>
{{ number + 1 }} {{ number + 1 }}
</td> </td>
<td class="team-name">{{ team.name }} <td class="team-name">{{ team.name }}
<a :href="team.url" target="_blank">[url]</a> <a :href="team.url" target="_blank">[url]</a>
</td> </td>
<td>{{ team.spendTime }}</td> <td>{{ team.spendTime }}</td>
<td> <td>
<div v-for="application in team.applications" :key="application.id"> <div v-for="application in team.applications" :key="application.id">
{{ application.name }} <button class="link-button" @click="gaveApplication(team.id, application.id)">Выдано</button> {{ application.name }} <button class="link-button"
</div> @click="gaveApplication(team.id, application.id)">Выдано</button>
</td> </div>
<td> </td>
<a v-on:click="qrurl = team.url, qrteam = team.name">QR</a> <td>
</td> <a v-on:click="qrurl = team.url, qrteam = team.name">QR</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -229,45 +258,57 @@
<style scoped> <style scoped>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
margin: 20px; margin: 20px;
} }
.buttons-block { .buttons-block {
padding-top: 20px; padding-top: 20px;
} }
.button-menu { .button-menu {
margin: 5px; margin: 5px;
} }
table { table {
width: 700px; width: 700px;
border-collapse: collapse; border-collapse: collapse;
margin: 30px auto; margin: 30px auto;
border: 1px solid #444444; border: 1px solid #444444;
} }
th, td {
padding: 12px; th,
text-align: left; td {
padding: 12px;
text-align: left;
} }
th { th {
background-color: var(--main-color); background-color: var(--main-color);
color: white; color: white;
font-weight: bold; font-weight: bold;
} }
tr:nth-child(odd) { tr:nth-child(odd) {
background-color: rgb(239, 239, 239); background-color: rgb(239, 239, 239);
} }
tr:nth-child(even) { tr:nth-child(even) {
background-color: rgb(255, 255, 255); background-color: rgb(255, 255, 255);
} }
tr:hover { tr:hover {
background-color: rgb(207, 207, 207); background-color: rgb(207, 207, 207);
} }
.time { .time {
white-space: nowrap; white-space: nowrap;
} }
.team-name { .team-name {
font-weight: 600; font-weight: 600;
} }
.link-button { .link-button {
/* Основные стили */ /* Основные стили */
display: inline; display: inline;
@ -277,7 +318,8 @@ tr:hover {
margin: 0; margin: 0;
font: inherit; font: inherit;
cursor: pointer; cursor: pointer;
color: var(--main-color); /* Стандартный цвет ссылки */ color: var(--main-color);
/* Стандартный цвет ссылки */
text-decoration: underline; text-decoration: underline;
font-weight: 600; font-weight: 600;
@ -293,20 +335,24 @@ tr:hover {
/* Состояние при наведении */ /* Состояние при наведении */
.link-button:hover { .link-button:hover {
color: var(--second-color); /* Немного темнее */ color: var(--second-color);
/* Немного темнее */
text-decoration: none; text-decoration: none;
} }
/* Состояние при активации (нажатии) */ /* Состояние при активации (нажатии) */
.link-button:active { .link-button:active {
color: #003366; /* Еще темнее */ color: #003366;
/* Еще темнее */
} }
/* Состояние фокуса (для доступности) */ /* Состояние фокуса (для доступности) */
.link-button:focus { .link-button:focus {
outline: none; /* Убираем стандартный outline */ outline: none;
/* Убираем стандартный outline */
text-decoration: none; text-decoration: none;
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.3); /* Кастомный фокус */ box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.3);
/* Кастомный фокус */
} }
.form-block { .form-block {