This commit is contained in:
Владимир Фёдоров 2026-03-26 03:53:54 +07:00
parent 073afde7c0
commit 0aac04e9bd
8 changed files with 201 additions and 78 deletions

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
build:
npm run build
rm -rf ../pinned_message/cmd/pinned_message/static/user
cp -r dist ../pinned_message/cmd/pinned_message/static/user

View File

@ -1,85 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router' import SchedulePage from './components/SchedulePage.vue';
import HelloWorld from './components/HelloWorld.vue'
</script> </script>
<template> <template>
<header> <div>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> <SchedulePage></SchedulePage>
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div> </div>
</header>
<RouterView />
</template> </template>
<style scoped> <style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style> </style>

View File

@ -3,7 +3,7 @@
#app { #app {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 1rem;
font-weight: normal; font-weight: normal;
} }

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { apiGetDays } from './client';
import type { Schedule } from './models';
import { formatRussianDate, getRelativeDayName } from './date';
import { capitalizeFirstLetter } from './text';
const schedule = ref<Schedule>({ days: [] })
onMounted(async () => {
schedule.value = await apiGetDays()
})
</script>
<template>
<div>
<div v-for="day in schedule.days" :key="day.date" class="day-block">
{{ capitalizeFirstLetter(formatRussianDate(day.date)) }}
<span v-if="getRelativeDayName(day.date) !== ''">({{ getRelativeDayName(day.date) }})</span>
<div v-for="performance in day.performances" :key="performance.name" class="performance-block">
<div>
{{ performance.time_collection }}
</div>
<div>
{{ performance.time_start }}
</div>
<div>
Место: {{ capitalizeFirstLetter(performance.place) }}
</div>
<div>
{{ capitalizeFirstLetter(performance.name) }}
</div>
<div>
Номера:
<div v-for="number in performance.numbers" :key="number.name">
- {{ number.name }}
</div>
</div>
<div v-if="performance.costumes !== '' && performance.costumes !== '-'">
{{ capitalizeFirstLetter(performance.costumes) }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.day-block {
border: solid 1px rebeccapurple;
margin: 10px 0;
padding: 15px;
border-radius: 10px;
}
.performance-block {
}
</style>

27
src/components/client.ts Normal file
View File

@ -0,0 +1,27 @@
import type { Schedule } from './models'
export const apiGetDays = async (): Promise<Schedule> => {
try {
const response = await fetch(getApiUrl('/schedule'), { method: 'GET' })
if (!response.ok) {
throw new Error(`http error status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error('[apiGetDays] error:', error)
throw error
}
}
export function getApiUrl(path: string) {
const url = 'http://' + window.location.host.split(':')[0] + ':8090' + path
return url
}
export function encodeUTF8ToBase64(s: string) {
return btoa(
encodeURIComponent(s).replace(/%([0-9A-F]{2})/g, (_, p1) =>
String.fromCharCode(parseInt(p1, 16)),
),
)
}

75
src/components/date.ts Normal file
View File

@ -0,0 +1,75 @@
export function formatRussianDate(dateStr: string): string {
// 1. Вытаскиваем только саму дату (2026-03-24) с помощью регулярного выражения
// Это защитит нас от багов с часовыми поясами при парсинге
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!match) {
throw new Error("Неверный формат входной строки");
}
// 2. Преобразуем строки в числа
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10);
const day = parseInt(match[3], 10);
// 3. Создаем объект даты (обратите внимание: в JS месяцы начинаются с 0, поэтому month - 1)
const dateObj = new Date(year, month - 1, day);
// 4. Словари для точного форматирования (родительный падеж для месяцев)
const monthsRu = [
"января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря"
];
// В JS 0 - это воскресенье, 1 - понедельник и т.д.
const weekdaysRu = [
"воскресенье", "понедельник", "вторник", "среда",
"четверг", "пятница", "суббота"
];
// 5. Получаем нужные значения
const monthName = monthsRu[dateObj.getMonth()];
const weekdayName = weekdaysRu[dateObj.getDay()];
// 6. Собираем итоговую строку
return `${weekdayName} ${day} ${monthName}`;
}
export function getRelativeDayName(dateStr: string): string {
// 1. Вытаскиваем только дату (например, 2026-03-24)
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!match) {
throw new Error("Неверный формат входной строки");
}
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1; // Месяцы в JS начинаются с 0
const day = parseInt(match[3], 10);
// 2. Создаем объект целевой даты и сбрасываем время в 00:00:00
const targetDate = new Date(year, month, day, 0, 0, 0, 0);
// 3. Создаем объект СЕГОДНЯШНЕЙ даты и тоже сбрасываем время в 00:00:00
const today = new Date();
today.setHours(0, 0, 0, 0);
// 4. Вычисляем разницу в миллисекундах и переводим в дни
// Используем Math.round, чтобы избежать багов при переходе на летнее/зимнее время
const diffInMs = targetDate.getTime() - today.getTime();
const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));
// 5. Возвращаем результат в зависимости от разницы в днях
switch (diffInDays) {
case 0:
return "сегодня";
case 1:
return "завтра";
case 2:
return "послезавтра"; // бонус :)
case -1:
return "вчера"; // бонус :)
default:
// Если это не сегодня и не завтра, можно вернуть пустую строку,
// либо просто количество дней, либо отформатированную дату (как в прошлом ответе)
return "";
}
}

21
src/components/models.ts Normal file
View File

@ -0,0 +1,21 @@
export type Schedule = {
days: Day[]
}
export type Day = {
date: string
performances: Performance[]
}
export type Performance = {
time_collection: string
time_start: string
place: string
name: string
numbers: Number[]
costumes: string
}
export type Number = {
name: string
}

9
src/components/text.ts Normal file
View File

@ -0,0 +1,9 @@
export function capitalizeFirstLetter(str: string): string {
// Проверка на пустую строку, чтобы избежать ошибок
if (!str) {
return str;
}
// Берем первый символ, делаем его большим + приклеиваем остаток строки (начиная со 2-го символа)
return str.charAt(0).toUpperCase() + str.slice(1);
}