This commit is contained in:
Иван Антонович Козлов 2025-05-27 01:28:23 +03:00
parent 3f30854c04
commit 295ed58901
15 changed files with 827 additions and 567 deletions

5
.env
View File

@ -1 +1,4 @@
VITE_API_BASE_URL=https://45.146.164.63:8080 # VUE_APP_API_URL=http://31.129.106.67:8080
# VUE_APP_GEO_URL=http://31.129.106.67:6001
VUE_APP_API_URL=http://127.0.0.1:8080
VUE_APP_GEO_URL=http://127.0.0.1:6001

View File

@ -1,8 +1,8 @@
<template> <template>
<Main /> <Main />
<StopInfo /> <StopInfo />
<CarrierInfo /> <CarrierInfo @president-appeal-toggle="presidentOpen = $event" />
<WeatherInfo /> <WeatherInfo :is-president-address-open="presidentOpen" />
<RouteInfo /> <RouteInfo />
</template> </template>
@ -22,6 +22,11 @@ export default {
WeatherInfo, WeatherInfo,
RouteInfo, RouteInfo,
}, },
data() {
return {
presidentOpen: false,
};
},
}; };
</script> </script>

View File

@ -18,18 +18,29 @@ body {
user-select: none; user-select: none;
} }
.station-label-no-bg { .station-label-no-bg,
.station-name-ru {
background: transparent !important; background: transparent !important;
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
font-size: 16px;
font-weight: 600;
color: #fff;
/* max-width: 200px; */ /* max-width: 200px; */
padding: 0 !important; padding: 0 !important;
/* text-wrap: revert; */ /* text-wrap: revert; */
} }
.station-name-ru {
font-size: 16px;
font-weight: 600;
color: #fff;
}
.station-name-en {
font-size: 14px;
color: #ffffffad;
text-align: left;
}
.station-label-no-bg:before { .station-label-no-bg:before {
display: none !important; display: none !important;
} }
@ -40,6 +51,7 @@ body {
top: 0; top: 0;
right: 0; right: 0;
height: 100%; height: 100%;
pointer-events: none;
} }
.stopinfo .bg { .stopinfo .bg {
@ -48,6 +60,15 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
pointer-events: none;
}
.stopinfo .bg * {
pointer-events: auto;
}
.stopinfo .container {
margin-bottom: 75px !important;
} }
.container { .container {
@ -118,14 +139,22 @@ body {
top: 18px; top: 18px;
} }
.landmarks-container {
position: absolute;
bottom: 0;
}
.stoparticles { .stoparticles {
padding: 0 15px;
height: 50px; height: 50px;
color: #fff; color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: auto; margin-top: auto;
gap: 45px; text-align: center;
gap: 15px;
justify-content: space-around;
background: rgb(187, 179, 170); background: rgb(187, 179, 170);
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
@ -150,6 +179,7 @@ body {
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
margin: 0 15px 15px 15px; margin: 0 15px 15px 15px;
pointer-events: auto;
} }
.stop-buttons-container { .stop-buttons-container {
@ -167,6 +197,7 @@ body {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
margin-top: 15px; margin-top: 15px;
pointer-events: auto;
} }
.white { .white {
@ -225,6 +256,7 @@ body {
left: 0; left: 0;
height: 100%; height: 100%;
width: 350px; width: 350px;
pointer-events: none;
} }
.carrierinfo .bg { .carrierinfo .bg {
@ -310,7 +342,7 @@ body {
.weatherinfo { .weatherinfo {
z-index: 450; z-index: 450;
width: 225px; width: 225px;
padding: 10px 15px; padding: 10px 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -336,8 +368,9 @@ body {
.weatherinfo .date { .weatherinfo .date {
text-align: center; text-align: center;
font-size: 18px; font-size: 15px;
padding: 2px 0 8px 0; font-weight: lighter;
padding: 0 0 8px 0;
width: 100%; width: 100%;
border-bottom: 1px solid #ffffff7a; border-bottom: 1px solid #ffffff7a;
} }
@ -346,13 +379,16 @@ body {
width: 75px; width: 75px;
} }
.current-weather {
padding-left: 10px;
}
.current-weather div { .current-weather div {
text-align: center; text-align: center;
margin-top: 5px;
} }
.temperature-celsius { .temperature-celsius {
font-size: 35px; font-size: 50px;
} }
.forecast-day { .forecast-day {
@ -366,7 +402,7 @@ body {
.weather-forecast { .weather-forecast {
width: 100%; width: 100%;
padding: 0 10px; padding: 0;
margin-top: 10px; margin-top: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -376,15 +412,22 @@ body {
.forecast { .forecast {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 7px; gap: 10px;
width: 90px;
}
.forecast-day {
font-size: 17px;
} }
.additional-forecast { .additional-forecast {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-top: 1px solid #ffffff7a; border-top: 1px solid #ffffff7a;
padding: 10px 0 0 7px; padding: 10px 0 0 3px;
gap: 7px; gap: 7px;
font-size: 18px;
font-weight: bold;
} }
.additional-forecast .humidity, .additional-forecast .humidity,
@ -417,7 +460,7 @@ body {
.route-names { .route-names {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; /* gap: 2px; */
padding: 0 10px; padding: 0 10px;
} }
@ -478,7 +521,7 @@ body {
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
left: 310px; left: 310px;
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0);
border: none; border: none;
color: #fff; color: #fff;
font-size: 24px; font-size: 24px;
@ -487,6 +530,7 @@ body {
border-radius: 5px; border-radius: 5px;
z-index: 1001; z-index: 1001;
transition: left 0.3s ease; transition: left 0.3s ease;
pointer-events: auto;
} }
.carrierinfo .bg.hidden + .carrier-toggle { .carrierinfo .bg.hidden + .carrier-toggle {
@ -550,21 +594,6 @@ body {
overflow-y: auto; overflow-y: auto;
} }
.carrier-toggle {
position: absolute;
bottom: 10px;
left: 310px;
background: rgba(0, 0, 0, 0.4);
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
padding: 5px 10px;
border-radius: 5px;
z-index: 1001;
transition: left 0.3s ease;
}
.carrierinfo .bg.hidden + .carrier-toggle { .carrierinfo .bg.hidden + .carrier-toggle {
left: 10px; left: 10px;
} }
@ -576,6 +605,7 @@ body {
.bg { .bg {
transition: transform 0.3s ease; transition: transform 0.3s ease;
pointer-events: auto;
} }
.dropdown-name { .dropdown-name {
@ -680,16 +710,6 @@ li.checked {
.stoparticle-option { .stoparticle-option {
cursor: pointer; cursor: pointer;
margin-right: 10px;
}
.stoparticle-option.selected {
text-decoration: underline;
}
.stoparticle-option {
cursor: pointer;
margin-right: 10px;
} }
.stoparticle-option.selected { .stoparticle-option.selected {
@ -958,3 +978,21 @@ li.checked {
.sight-preview-wrapper { .sight-preview-wrapper {
max-height: 300px; max-height: 300px;
} }
.hidden {
display: none !important;
}
.governor-appeal {
top: 140px;
width: 440px;
}
.governor-appeal p {
max-height: 400px;
line-height: 22px;
}
.governor-appeal h3 {
font-size: 22px;
}

View File

@ -589,7 +589,6 @@
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
<span class="gos-name" <span class="gos-name"
>При поддержке Правительства <br /> >При поддержке Правительства <br />
Санкт-Петербурга</span Санкт-Петербурга</span
@ -713,7 +712,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div v-if="showGovernorAppeal" class="sight-preview-panel"> <div v-if="showGovernorAppeal" class="governor-appeal sight-preview-panel">
<img :src="governorAppealImage" v-if="governorAppealImage" /> <img :src="governorAppealImage" v-if="governorAppealImage" />
<h3>{{ governorAppealTitle }}</h3> <h3>{{ governorAppealTitle }}</h3>
<p>{{ governorAppealText }}</p> <p>{{ governorAppealText }}</p>
@ -739,16 +738,17 @@
<script> <script>
import "../assets/style/main.css"; import "../assets/style/main.css";
import { API_URL, GEO_URL } from "../config";
export default { export default {
name: "StopInfo", name: "CarrierInfo",
data() { data() {
return { return {
stations: [], stations: [],
showList: false, showList: false,
sights: [], sights: [],
showSightsList: false, showSightsList: false,
routeId: null, routeId: 1,
isHidden: true, isHidden: true,
selectedSightId: null, selectedSightId: null,
selectedSightName: "", selectedSightName: "",
@ -763,6 +763,7 @@ export default {
governorAppealId: null, governorAppealId: null,
selectedSightWatermarkLU: "", selectedSightWatermarkLU: "",
selectedSightWatermarkRD: "", selectedSightWatermarkRD: "",
inactivityTimer: null,
}; };
}, },
methods: { methods: {
@ -778,16 +779,10 @@ export default {
}, },
async fetchStops() { async fetchStops() {
try { try {
const geoResponse = await fetch( const geoResponse = await fetch(`${GEO_URL}/v1/geolocation/context`);
"http://31.129.106.67:6001/v1/geolocation/context"
);
const geoData = await geoResponse.json(); const geoData = await geoResponse.json();
const token = this.getCookie("auth_token");
const routeDetailsRes = await fetch( const routeDetailsRes = await fetch(
`https://wn-ts.krbl.ru/route/${geoData.routeId}`, `${API_URL}/route/${geoData.routeId}`
{
headers: { Authorization: `Bearer ${token}` },
}
); );
const routeDetails = await routeDetailsRes.json(); const routeDetails = await routeDetailsRes.json();
const appealId = routeDetails.governor_appeal; const appealId = routeDetails.governor_appeal;
@ -802,29 +797,18 @@ export default {
if (appealId && appealId !== 0) { if (appealId && appealId !== 0) {
try { try {
const articleRes = await fetch( const articleRes = await fetch(`${API_URL}/article/${appealId}`);
`https://wn-ts.krbl.ru/article/${appealId}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
const articleData = await articleRes.json(); const articleData = await articleRes.json();
this.governorAppealTitle = articleData.heading; this.governorAppealTitle = articleData.heading;
this.governorAppealText = articleData.body; this.governorAppealText = articleData.body;
const mediaRes = await fetch( const mediaRes = await fetch(
`https://wn-ts.krbl.ru/article/${appealId}/media`, `${API_URL}/article/${appealId}/media`
{
headers: { Authorization: `Bearer ${token}` },
}
); );
const mediaData = await mediaRes.json(); const mediaData = await mediaRes.json();
if (mediaData.length) { if (mediaData.length) {
const imageRes = await fetch( const imageRes = await fetch(
`http://31.129.106.67:8080/media/${mediaData[0].id}/download`, `${API_URL}/media/${mediaData[0].id}/download`
{
headers: { Authorization: `Bearer ${token}` },
}
); );
this.governorAppealImage = await imageRes.url; this.governorAppealImage = await imageRes.url;
} else { } else {
@ -846,10 +830,7 @@ export default {
if (!this.routeId) throw new Error("Route number not found"); if (!this.routeId) throw new Error("Route number not found");
const stopsResponse = await fetch( const stopsResponse = await fetch(
`https://wn-ts.krbl.ru/route/${this.routeId}/station`, `${API_URL}/route/${this.routeId}/station`
{
headers: { Authorization: `Bearer ${token}` },
}
); );
const stopsData = await stopsResponse.json(); const stopsData = await stopsResponse.json();
this.stations = stopsData; this.stations = stopsData;
@ -859,22 +840,11 @@ export default {
}, },
async fetchSights() { async fetchSights() {
try { try {
const token = this.getCookie("auth_token"); const response = await fetch(`${API_URL}/route/${this.routeId}/sight`);
const response = await fetch(
`https://wn-ts.krbl.ru/route/${this.routeId}/sight`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
const rawSights = await response.json(); const rawSights = await response.json();
const detailedSights = await Promise.all( const detailedSights = await Promise.all(
rawSights.map(async (sight) => { rawSights.map(async (sight) => {
const detailRes = await fetch( const detailRes = await fetch(`${API_URL}/sight/${sight.id}`);
`https://wn-ts.krbl.ru/sight/${sight.id}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
const detail = await detailRes.json(); const detail = await detailRes.json();
return { id: sight.id, name: detail.name }; return { id: sight.id, name: detail.name };
}) })
@ -886,6 +856,8 @@ export default {
}, },
async toggleSights() { async toggleSights() {
this.showGovernorAppeal = false; this.showGovernorAppeal = false;
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
if (!this.sights.length) { if (!this.sights.length) {
await this.fetchSights(); await this.fetchSights();
} }
@ -899,45 +871,29 @@ export default {
}, },
async selectSight(sightId) { async selectSight(sightId) {
this.selectedSightId = sightId; this.selectedSightId = sightId;
const token = this.getCookie("auth_token");
try { try {
const sightRes = await fetch(`https://wn-ts.krbl.ru/sight/${sightId}`, { const sightRes = await fetch(`${API_URL}/sight/${sightId}`);
headers: { Authorization: `Bearer ${token}` },
});
const sightData = await sightRes.json(); const sightData = await sightRes.json();
this.selectedSightName = sightData.name; this.selectedSightName = sightData.name;
const articleId = sightData.left_article; const articleId = sightData.left_article;
const articleRes = await fetch( const articleRes = await fetch(`${API_URL}/article/${articleId}`);
`https://wn-ts.krbl.ru/article/${articleId}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
const articleData = await articleRes.json(); const articleData = await articleRes.json();
this.selectedSightText = articleData.body; this.selectedSightText = articleData.body;
const mediaRes = await fetch( const mediaRes = await fetch(`${API_URL}/article/${articleId}/media`);
`https://wn-ts.krbl.ru/article/${articleId}/media`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
const mediaData = await mediaRes.json(); const mediaData = await mediaRes.json();
if (mediaData.length) { if (mediaData.length) {
const imageRes = await fetch( const imageRes = await fetch(
`http://31.129.106.67:8080/media/${mediaData[0].id}/download`, `${API_URL}/media/${mediaData[0].id}/download`
{
headers: { Authorization: `Bearer ${token}` },
}
); );
this.selectedSightImage = await imageRes.url; this.selectedSightImage = await imageRes.url;
this.selectedSightWatermarkLU = sightData.watermark_lu this.selectedSightWatermarkLU = sightData.watermark_lu
? `http://31.129.106.67:8080/media/${sightData.watermark_lu}/download` ? `${API_URL}/media/${sightData.watermark_lu}/download`
: ""; : "";
this.selectedSightWatermarkRD = sightData.watermark_rd this.selectedSightWatermarkRD = sightData.watermark_rd
? `http://31.129.106.67:8080/media/${sightData.watermark_rd}/download` ? `${API_URL}/media/${sightData.watermark_rd}/download`
: ""; : "";
} else { } else {
this.selectedSightImage = ""; this.selectedSightImage = "";
@ -950,6 +906,8 @@ export default {
}, },
async toggleList() { async toggleList() {
this.showGovernorAppeal = false; this.showGovernorAppeal = false;
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
if (!this.stations.length) { if (!this.stations.length) {
await this.fetchStops(); await this.fetchStops();
} }
@ -961,6 +919,7 @@ export default {
this.showSightsList = false; this.showSightsList = false;
this.showSightPreview = false; this.showSightPreview = false;
this.showGovernorAppeal = false; this.showGovernorAppeal = false;
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
const root = document.documentElement; const root = document.documentElement;
root.style.setProperty( root.style.setProperty(
@ -980,6 +939,7 @@ export default {
weatherInfo?.classList.remove("shifted-left"); weatherInfo?.classList.remove("shifted-left");
} }
}); });
this.resetInactivityTimer();
}, },
handleClickOutside() { handleClickOutside() {
// пупупу // пупупу
@ -988,10 +948,28 @@ export default {
console.log("Кнопка обращения губернатора нажата"); console.log("Кнопка обращения губернатора нажата");
this.showGovernorAppeal = !this.showGovernorAppeal; this.showGovernorAppeal = !this.showGovernorAppeal;
this.showSightPreview = false; this.showSightPreview = false;
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
},
resetInactivityTimer() {
clearTimeout(this.inactivityTimer);
this.inactivityTimer = setTimeout(this.hidePanelIfActive, 300000); // 5m
},
hidePanelIfActive() {
if (!this.isHidden) {
this.toggleCarrierInfo();
}
},
handleUserActivity() {
this.resetInactivityTimer();
}, },
}, },
mounted() { mounted() {
document.addEventListener("click", this.handleClickOutside); document.addEventListener("click", this.handleClickOutside);
["mousemove", "touchstart", "keydown", "click"].forEach((evt) =>
document.addEventListener(evt, this.handleUserActivity)
);
this.resetInactivityTimer();
const root = document.documentElement; const root = document.documentElement;
root.style.setProperty("--panel-offset", "20px"); root.style.setProperty("--panel-offset", "20px");
const routeInfo = document.querySelector(".routeinfo"); const routeInfo = document.querySelector(".routeinfo");
@ -1003,6 +981,10 @@ export default {
console.log("hasGovernorAppeal в mounted:", this.hasGovernorAppeal); console.log("hasGovernorAppeal в mounted:", this.hasGovernorAppeal);
}, },
beforeUnmount() { beforeUnmount() {
["mousemove", "touchstart", "keydown", "click"].forEach((evt) =>
document.removeEventListener(evt, this.handleUserActivity)
);
clearTimeout(this.inactivityTimer);
document.removeEventListener("click", this.handleClickOutside); document.removeEventListener("click", this.handleClickOutside);
}, },
}; };

File diff suppressed because one or more lines are too long

View File

@ -2,20 +2,64 @@
<div class="routeinfo"> <div class="routeinfo">
<div class="route-number">{{ routeNumber }}</div> <div class="route-number">{{ routeNumber }}</div>
<div class="route-names"> <div class="route-names">
<div class="route-name" v-if="startStopName && endStopName"> <!-- Russian names -->
<div class="route-name ru">
<div class="scroll-wrapper"> <div class="scroll-wrapper">
<div class="scroll-inner"> <div :class="isStartScrolling ? 'scroll-inner scrolling' : ''">
<div class="scroll-content"> <div class="scroll-content">
<div class="name-block"> <div class="name-block">
<span class="name"> <span class="name" ref="startStopRuText">
{{ startStopName }} {{ endStopName }} &nbsp;&nbsp;&nbsp; <template v-if="isStartScrolling">
{{ startStopName }} {{ endStopName }} &nbsp;&nbsp;&nbsp; {{ startStopName }} &nbsp;&nbsp;&nbsp;
{{ startStopName }} {{ endStopName }} &nbsp;&nbsp;&nbsp; {{ startStopName }} &nbsp;&nbsp;&nbsp; {{ startStopName }}
</template>
<template v-else>
{{ startStopName }}
</template>
</span> </span>
<span class="translate"> </div>
{{ startStopNameEn }} {{ endStopNameEn }} &nbsp;&nbsp;&nbsp; </div>
{{ startStopNameEn }} {{ endStopNameEn }} &nbsp;&nbsp;&nbsp; </div>
{{ startStopNameEn }} {{ endStopNameEn }} &nbsp;&nbsp;&nbsp; </div>
</div>
<div class="route-name ru">
<div class="scroll-wrapper">
<div :class="isEndScrolling ? 'scroll-inner scrolling' : ''">
<div class="scroll-content">
<div class="name-block">
<span class="name" ref="endStopRuText">
<template v-if="isEndScrolling">
{{ endStopName }} &nbsp;&nbsp;&nbsp;
{{ endStopName }} &nbsp;&nbsp;&nbsp; {{ endStopName }}
</template>
<template v-else>
{{ endStopName }}
</template>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- English names -->
<div class="route-name en">
<div class="scroll-wrapper">
<div :class="isEnScrolling ? 'scroll-inner scrolling' : ''">
<div class="scroll-content">
<div class="name-block">
<span class="translate" ref="enText">
<template v-if="isEnScrolling">
{{ startStopNameEn }}
{{ endStopNameEn }} &nbsp;&nbsp;&nbsp;
{{ startStopNameEn }}
{{ endStopNameEn }} &nbsp;&nbsp;&nbsp;
{{ startStopNameEn }} {{ endStopNameEn }}
</template>
<template v-else>
{{ startStopNameEn }} {{ endStopNameEn }}
</template>
</span> </span>
</div> </div>
</div> </div>
@ -27,47 +71,7 @@
</template> </template>
<script> <script>
import Cookies from "js-cookie"; import { API_URL, GEO_URL } from "../config";
async function requestAuth() {
const response = await fetch("https://wn-ts.krbl.ru/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "admin",
password: "changeme",
}),
});
const data = await response.json();
Cookies.set("auth_token", data.token);
return data.token;
}
async function checkAuth() {
const token = Cookies.get("auth_token");
try {
if (!token) {
return await requestAuth();
} else {
const response = await fetch("https://wn-ts.krbl.ru/auth/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.status === 401) {
return await requestAuth();
}
}
return token;
} catch (error) {
console.error("Auth check failed:", error);
return null;
}
}
export default { export default {
name: "RouteInfo", name: "RouteInfo",
@ -78,37 +82,36 @@ export default {
endStopName: "", endStopName: "",
startStopNameEn: "", startStopNameEn: "",
endStopNameEn: "", endStopNameEn: "",
isStartScrolling: false,
isEndScrolling: false,
isEnScrolling: false,
}; };
}, },
watch: {
startStopName() {
this.$nextTick(this.checkScroll);
},
endStopName() {
this.$nextTick(this.checkScroll);
},
startStopNameEn() {
this.$nextTick(this.checkScroll);
},
endStopNameEn() {
this.$nextTick(this.checkScroll);
},
},
async mounted() { async mounted() {
const contextRes = await fetch( const contextRes = await fetch(`${GEO_URL}/v1/geolocation/context`);
"http://31.129.106.67:6001/v1/geolocation/context"
);
const data = await contextRes.json(); const data = await contextRes.json();
this.routeNumber = data.routeNumber; this.routeNumber = data.routeNumber;
const startStopId = data.startStopId; const startStopId = data.startStopId;
const endStopId = data.endStopId; const endStopId = data.endStopId;
const token = await checkAuth();
if (!token) {
console.error(
"No valid token received, skipping wn-ts.krbl.ru requests."
);
return;
}
const [startStopRes, endStopRes] = await Promise.all([ const [startStopRes, endStopRes] = await Promise.all([
fetch(`https://wn-ts.krbl.ru/station/${startStopId}`, { fetch(`${API_URL}/station/${startStopId}`),
headers: { fetch(`${API_URL}/station/${endStopId}`),
Authorization: `Bearer ${token}`,
},
}),
fetch(`https://wn-ts.krbl.ru/station/${endStopId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
}),
]); ]);
const startStopData = await startStopRes.json(); const startStopData = await startStopRes.json();
@ -118,16 +121,8 @@ export default {
this.endStopName = endStopData.name; this.endStopName = endStopData.name;
const [startStopEnRes, endStopEnRes] = await Promise.all([ const [startStopEnRes, endStopEnRes] = await Promise.all([
fetch(`https://wn-ts.krbl.ru/station/${startStopId}?lang=en`, { fetch(`${API_URL}/station/${startStopId}?lang=en`),
headers: { fetch(`${API_URL}/station/${endStopId}?lang=en`),
Authorization: `Bearer ${token}`,
},
}),
fetch(`https://wn-ts.krbl.ru/station/${endStopId}?lang=en`, {
headers: {
Authorization: `Bearer ${token}`,
},
}),
]); ]);
const startStopEnData = await startStopEnRes.json(); const startStopEnData = await startStopEnRes.json();
@ -135,7 +130,45 @@ export default {
this.startStopNameEn = startStopEnData.name; this.startStopNameEn = startStopEnData.name;
this.endStopNameEn = endStopEnData.name; this.endStopNameEn = endStopEnData.name;
this.$nextTick(() => {
this.checkScroll();
});
},
methods: {
checkScroll() {
const threshold = 280;
if (this.$refs.startStopRuText) {
this.isStartScrolling =
this.$refs.startStopRuText.offsetWidth > threshold;
}
if (this.$refs.endStopRuText) {
this.isEndScrolling = this.$refs.endStopRuText.offsetWidth > threshold;
}
if (this.$refs.enText) {
this.isEnScrolling = this.$refs.enText.offsetWidth > threshold;
}
},
}, },
methods: {},
}; };
</script> </script>
<style scoped>
.route-name {
overflow: hidden;
width: 280px;
white-space: nowrap;
}
.scroll-inner.scrolling {
animation: marquee 12s linear infinite;
}
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
</style>

View File

@ -98,7 +98,7 @@
</div> </div>
</transition> </transition>
<transition name="expand-collapse-transition"> <transition name="expand-collapse-transition">
<div> <div class="landmarks-container">
<div class="landmarks" @click="toggleSightsList"> <div class="landmarks" @click="toggleSightsList">
<div class="landmarks-header"> <div class="landmarks-header">
<svg <svg
@ -149,10 +149,7 @@
:key="sight.id" :key="sight.id"
class="sight-card" class="sight-card"
> >
<img <img class="sight-thumbnail" :src="sight.thumbnailUrl" />
class="sight-thumbnail"
:src="`http://31.129.106.67:8080/media/${sight.thumbnail}/download`"
/>
<div class="sight-title">{{ sight.name }}</div> <div class="sight-title">{{ sight.name }}</div>
</div> </div>
</div> </div>
@ -167,8 +164,9 @@
<script> <script>
import "../assets/style/main.css"; import "../assets/style/main.css";
import { API_URL, GEO_URL } from "../config";
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie";
export default { export default {
name: "StopInfo", name: "StopInfo",
@ -176,6 +174,8 @@ export default {
return { return {
isModalOpen: false, isModalOpen: false,
autoCloseTimer: null, autoCloseTimer: null,
articleInactivityTimer: null,
sightsInactivityTimer: null,
imageUrl: "", imageUrl: "",
defaultImageUrl: defaultImageUrl:
"https://lh3.googleusercontent.com/gps-cs-s/AB5caB8lUwofb2NIg6n0-cEl8nIWsySAUc52KNj4XezuOdo-aeqTgQlD1kTVa5MaynL2Yg4ByoTYTKNTR7K59f7kjzU9yzpudstjRiT2F6M_ilxFYFpcvMZz6OwlRFF2MrsCPSwUa7vqew=s680-w680-h510", "https://lh3.googleusercontent.com/gps-cs-s/AB5caB8lUwofb2NIg6n0-cEl8nIWsySAUc52KNj4XezuOdo-aeqTgQlD1kTVa5MaynL2Yg4ByoTYTKNTR7K59f7kjzU9yzpudstjRiT2F6M_ilxFYFpcvMZz6OwlRFF2MrsCPSwUa7vqew=s680-w680-h510",
@ -246,41 +246,6 @@ export default {
}, },
}, },
methods: { methods: {
async checkAuth() {
let token = Cookies.get("auth_token");
if (!token) {
await this.requestAuth();
return;
}
try {
await axios.get("https://wn-ts.krbl.ru/auth/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (e) {
await this.requestAuth();
}
},
async requestAuth() {
const response = await axios.post("https://wn-ts.krbl.ru/auth/login", {
email: "admin",
password: "changeme",
});
const token = response.data.token;
Cookies.set("auth_token", token);
document.cookie = `auth_token=${token}; path=/;`;
},
getCookie(name) {
const matches = document.cookie.match(
new RegExp(
"(?:^|; )" +
name.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1") +
"=([^;]*)"
)
);
return matches ? decodeURIComponent(matches[1]) : undefined;
},
openModal() { openModal() {
const imageDiv = this.$el.querySelector(".img"); const imageDiv = this.$el.querySelector(".img");
if (imageDiv) { if (imageDiv) {
@ -301,74 +266,59 @@ export default {
}, },
async fetchSightInfo() { async fetchSightInfo() {
if (!this.sightId) { if (!this.sightId) {
console.warn("No sightId provided for fetchSightInfo"); console.log("No sightId provided for fetchSightInfo");
return; return;
} }
const token = this.getCookie("auth_token"); const response = await axios.get(`${API_URL}/sight/${this.sightId}`);
const response = await axios.get(
`https://wn-ts.krbl.ru/sight/${this.sightId}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
this.stopName = response.data.name; this.stopName = response.data.name;
this.watermarkLU = response.data.watermark_lu if (response.data.watermark_lu) {
? `http://31.129.106.67:8080/media/${response.data.watermark_lu}/download` this.watermarkLU = await this.getMediaBlobUrl(
: ""; response.data.watermark_lu
this.watermarkRD = response.data.watermark_rd );
? `http://31.129.106.67:8080/media/${response.data.watermark_rd}/download` } else {
: ""; this.watermarkLU = "";
}
if (response.data.watermark_rd) {
this.watermarkRD = await this.getMediaBlobUrl(
response.data.watermark_rd
);
} else {
this.watermarkRD = "";
}
}, },
async fetchArticles() { async fetchArticles() {
if (!this.sightId) { if (!this.sightId) {
console.warn("No sightId provided for fetchArticles"); console.warn("No sightId provided for fetchArticles");
return; return;
} }
const token = this.getCookie("auth_token");
const response = await axios.get( const response = await axios.get(
`https://wn-ts.krbl.ru/sight/${this.sightId}/article`, `${API_URL}/sight/${this.sightId}/article`
{
headers: {
Authorization: `Bearer ${token}`,
},
}
); );
this.articles = response.data; this.articles = response.data;
if (this.articles.length > 0) { if (this.articles.length > 0) {
this.selectArticle(this.articles[0].id); this.selectArticle(this.articles[0].id);
this.resetArticleInactivityTimer();
} }
}, },
async fetchSights() { async fetchSights() {
const token = this.getCookie("auth_token"); const geoRes = await axios.get(`${GEO_URL}/v1/geolocation/context`);
const geoRes = await axios.get(
"http://31.129.106.67:6001/v1/geolocation/context"
);
const routeId = geoRes.data.routeId; const routeId = geoRes.data.routeId;
if (!routeId) { if (!routeId) {
console.warn("Missing routeId in geo context:", geoRes.data); console.warn("Missing routeId in geo context:", geoRes.data);
return; return;
} }
const sightsRes = await axios.get( const sightsRes = await axios.get(`${API_URL}/route/${routeId}/sight`);
`https://wn-ts.krbl.ru/route/${routeId}/sight`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
const rawSights = sightsRes.data; const rawSights = sightsRes.data;
const detailedSights = await Promise.all( const detailedSights = await Promise.all(
rawSights.map(async (sight) => { rawSights.map(async (sight) => {
const detailRes = await axios.get( const detailRes = await axios.get(`${API_URL}/sight/${sight.id}`);
`https://wn-ts.krbl.ru/sight/${sight.id}`, const thumbnailUrl = detailRes.data.thumbnail
{ ? await this.getMediaBlobUrl(detailRes.data.thumbnail)
headers: { Authorization: `Bearer ${token}` }, : "";
}
);
return { return {
id: sight.id, id: sight.id,
name: detailRes.data.name, name: detailRes.data.name,
thumbnail: detailRes.data.thumbnail, thumbnailUrl,
}; };
}) })
); );
@ -376,27 +326,63 @@ export default {
}, },
toggleSightsList() { toggleSightsList() {
this.showSightsList = !this.showSightsList; this.showSightsList = !this.showSightsList;
if (this.showSightsList) {
this.resetSightsInactivityTimer();
} else if (this.sightsInactivityTimer) {
clearTimeout(this.sightsInactivityTimer);
this.sightsInactivityTimer = null;
}
},
resetSightsInactivityTimer() {
if (this.articleInactivityTimer) {
clearTimeout(this.articleInactivityTimer);
}
if (this.sightsInactivityTimer) {
clearTimeout(this.sightsInactivityTimer);
}
if (this.showSightsList) {
this.sightsInactivityTimer = setTimeout(() => {
this.showSightsList = false;
this.sightsInactivityTimer = null;
}, 300000); // 5 m
}
},
resetArticleInactivityTimer() {
if (this.articleInactivityTimer)
clearTimeout(this.articleInactivityTimer);
this.articleInactivityTimer = setTimeout(() => {
if (
this.articles.length > 0 &&
this.selectedArticleId !== this.articles[0].id
) {
this.selectArticle(this.articles[0].id);
}
}, 300_000); // 5 m
},
handleUserActivity() {
if (this.showSightsList) this.resetSightsInactivityTimer();
this.resetArticleInactivityTimer();
}, },
selectArticle(id) { selectArticle(id) {
this.resetArticleInactivityTimer();
this.selectedArticleId = id; this.selectedArticleId = id;
const selected = this.articles.find((article) => article.id === id); const selected = this.articles.find((article) => article.id === id);
this.selectedArticleBody = selected ? selected.body : ""; this.selectedArticleBody = selected ? selected.body : "";
this.fetchArticleMedia(id); this.fetchArticleMedia(id);
}, },
async fetchArticleMedia(articleId) { async fetchArticleMedia(articleId) {
const token = this.getCookie("auth_token");
try { try {
const response = await axios.get( const response = await axios.get(
`https://wn-ts.krbl.ru/article/${articleId}/media`, `${API_URL}/article/${articleId}/media`
{
headers: {
Authorization: `Bearer ${token}`,
},
}
); );
if (response.data && response.data.length > 0) { if (response.data && response.data.length > 0) {
const mediaId = response.data[0].id; const mediaId = response.data[0].id;
this.imageUrl = `http://31.129.106.67:8080/media/${mediaId}/download`; try {
this.imageUrl = await this.getMediaBlobUrl(mediaId);
} catch {
this.imageUrl = `${API_URL}/media/${mediaId}/download`;
}
} else { } else {
this.imageUrl = ""; this.imageUrl = "";
} }
@ -405,39 +391,60 @@ export default {
this.imageUrl = ""; this.imageUrl = "";
} }
}, },
async fetchGeolocationContext() {
async getMediaBlobUrl(mediaId) {
try { try {
const response = await axios.get( const response = await axios.get(
"http://31.129.106.67:6001/v1/geolocation/context" `${API_URL}/media/${mediaId}/download`,
);
this.routeProgress = response.data.routeProgress;
console.log("Updated routeProgress:", this.routeProgress);
const token = this.getCookie("auth_token");
const stopsResponse = await axios.get(
`https://wn-ts.krbl.ru/route/${response.data.routeId}/station`,
{ {
headers: { Authorization: `Bearer ${token}` }, responseType: "blob",
} }
); );
return URL.createObjectURL(response.data);
} catch (error) {
console.error(
`Failed to download media ${mediaId}:`,
error?.response?.status,
error?.response?.data || error.message
);
return `${API_URL}/media/${mediaId}/download`;
}
},
async fetchGeolocationContext() {
try {
const response = await axios.get(`${GEO_URL}/v1/geolocation/context`);
this.routeProgress = response.data.routeProgress;
const stopsResponse = await axios.get(
`${API_URL}/route/${response.data.routeId}/station`
);
this.stops = stopsResponse.data; this.stops = stopsResponse.data;
const newSightId = response.data.nearestSightId; let newSightId = response.data.nearestSightId;
if (!newSightId) {
if (!this.sights || this.sights.length === 0) {
await this.fetchSights();
}
if (this.sights && this.sights.length > 0) {
newSightId = this.sights[1].id;
}
}
if (newSightId && newSightId !== this.sightId) { if (newSightId && newSightId !== this.sightId) {
this.sightId = newSightId; this.sightId = newSightId;
await this.fetchSightInfo(); await this.fetchSightInfo();
await this.fetchArticles(); await this.fetchArticles();
} }
const nextStopId = response.data.routeProgress?.endStopId; const nextStopId = response.data.routeProgress?.endStopId;
console.log("Fetched next stop ID:", nextStopId); // console.log("Fetched next stop ID:", nextStopId);
console.log("Stops:", this.stops); // console.log("Stops:", this.stops);
if (nextStopId && this.stops) { if (nextStopId && this.stops) {
const nextStop = this.stops.find((stop) => stop.id == nextStopId); const nextStop = this.stops.find((stop) => stop.id == nextStopId);
console.log("Fetched next stop ID:", nextStopId); // console.log("Fetched next stop ID:", nextStopId);
console.log("Matching stop:", nextStop); // console.log("Matching stop:", nextStop);
if (nextStop && nextStop.transfers) { if (nextStop && nextStop.transfers) {
console.log("Transfers at next stop:", nextStop.transfers); // console.log("Transfers at next stop:", nextStop.transfers);
this.nextStopTransfers = nextStop.transfers; this.nextStopTransfers = nextStop.transfers;
} else { } else {
console.log("No transfers found at next stop"); // console.log("No transfers found at next stop");
this.nextStopTransfers = null; this.nextStopTransfers = null;
} }
} }
@ -456,7 +463,7 @@ export default {
toggleTransfers() { toggleTransfers() {
console.log("Transfer toggle clicked"); console.log("Transfer toggle clicked");
this.showTransfers = !this.showTransfers; this.showTransfers = !this.showTransfers;
console.log("showTransfers:", this.showTransfers); // console.log("showTransfers:", this.showTransfers);
if (!this.showTransfers) { if (!this.showTransfers) {
return; return;
@ -468,32 +475,46 @@ export default {
} }
const nextStopId = this.routeProgress.endStopId; const nextStopId = this.routeProgress.endStopId;
console.log("Next stop ID from routeProgress:", nextStopId); // console.log("Next stop ID from routeProgress:", nextStopId);
console.log("All stops:", this.stops); // console.log("All stops:", this.stops);
const nextStop = this.stops.find((stop) => stop.id === nextStopId); const nextStop = this.stops.find((stop) => stop.id === nextStopId);
console.log("Next stop object:", nextStop); // console.log("Next stop object:", nextStop);
if (nextStop && nextStop.transfers) { if (nextStop && nextStop.transfers) {
console.log("Transfers at next stop:", nextStop.transfers); // console.log("Transfers at next stop:", nextStop.transfers);
this.nextStopTransfers = nextStop.transfers; this.nextStopTransfers = nextStop.transfers;
} else { } else {
console.log("No transfers found at next stop"); // console.log("No transfers found at next stop");
this.nextStopTransfers = null; this.nextStopTransfers = null;
} }
}, },
}, },
async mounted() { async mounted() {
await this.checkAuth();
await this.fetchGeolocationContext();
await this.fetchSights(); await this.fetchSights();
await this.fetchGeolocationContext();
this.geolocationInterval = setInterval(() => { this.geolocationInterval = setInterval(() => {
this.fetchGeolocationContext(); this.fetchGeolocationContext();
}, 1000); }, 1000);
window.addEventListener("mousemove", this.handleUserActivity);
window.addEventListener("touchstart", this.handleUserActivity, {
passive: true,
});
window.addEventListener("keydown", this.handleUserActivity);
window.addEventListener("click", this.handleUserActivity, true);
// this.fetchSightInfo(); // this.fetchSightInfo();
// this.fetchArticles(); // this.fetchArticles();
}, },
unmounted() { unmounted() {
if (this.sightsInactivityTimer) {
clearTimeout(this.sightsInactivityTimer);
}
window.removeEventListener("mousemove", this.handleUserActivity);
window.removeEventListener("touchstart", this.handleUserActivity, {
passive: true,
});
window.removeEventListener("keydown", this.handleUserActivity);
window.removeEventListener("click", this.handleUserActivity, true);
if (this.autoCloseTimer) { if (this.autoCloseTimer) {
clearTimeout(this.autoCloseTimer); clearTimeout(this.autoCloseTimer);
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="weatherinfo"> <div class="weatherinfo" :class="{ hidden: isPresidentAddressOpen }">
<div class="time">{{ formattedTime }}</div> <div class="time">{{ formattedTime }}</div>
<div class="date">{{ formattedDate }}, {{ dayOfWeek }}</div> <div class="date">{{ formattedDate }}, {{ dayOfWeek }}</div>
@ -10,7 +10,7 @@
alt="Weather Icon" alt="Weather Icon"
/> />
<div class="temperature-celsius"> <div class="temperature-celsius">
{{ currentWeather.temperatureCelsius }}°C {{ currentWeather.temperatureCelsius }}°
</div> </div>
<div> <div>
{{ currentWeather.description }} {{ currentWeather.description }}
@ -27,8 +27,8 @@
<div class="humidity"> <div class="humidity">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="12" width="13"
height="16" height="18"
fill="none" fill="none"
viewBox="0 0 12 16" viewBox="0 0 12 16"
> >
@ -43,8 +43,8 @@
<div class="wind"> <div class="wind">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="18"
height="16" height="18"
fill="none" fill="none"
viewBox="0 0 16 16" viewBox="0 0 16 16"
> >
@ -72,8 +72,9 @@
<script> <script>
import "../assets/style/main.css"; import "../assets/style/main.css";
import { API_URL } from "../config";
import axios from "axios"; import axios from "axios";
import Cookies from "js-cookie";
import clearIcon from "@/icons/clear-day.svg"; import clearIcon from "@/icons/clear-day.svg";
import cloudsIcon from "@/icons/cloudy.svg"; import cloudsIcon from "@/icons/cloudy.svg";
import rainIcon from "@/icons/rainy.svg"; import rainIcon from "@/icons/rainy.svg";
@ -84,6 +85,12 @@ import fogIcon from "@/icons/fog.svg";
export default { export default {
name: "WeatherInfo", name: "WeatherInfo",
props: {
isPresidentAddressOpen: {
type: Boolean,
default: false,
},
},
data() { data() {
return { return {
isModalOpen: false, isModalOpen: false,
@ -101,55 +108,24 @@ export default {
dayOfWeek: "", dayOfWeek: "",
timerInterval: null, timerInterval: null,
weatherDescriptionMap: { weatherDescriptionMap: {
thunderstorm: "Гроза", thunderstorm: "гроза",
drizzle: "Слабый дождь", drizzle: "слабый дождь",
rain: "Дождь", rain: "дождь",
snow: "Снег", snow: "снег",
atmosphere: "Туман", atmosphere: "туман",
clear: "Ясно", clear: "ясно",
clouds: "Облачно", clouds: "облачно",
}, },
}; };
}, },
methods: { methods: {
async getToken() {
let token = Cookies.get("auth_token");
try {
await axios.get("https://wn-ts.krbl.ru/auth/me", {
headers: { Authorization: `Bearer ${token}` },
});
} catch (error) {
if (error.response && error.response.status === 401) {
const response = await axios.post(
"https://wn-ts.krbl.ru/auth/login",
{
email: "admin",
password: "changeme",
}
);
token = response.data.token;
Cookies.set("auth_token", token);
}
}
return token;
},
async fetchSightInfo() { async fetchSightInfo() {
const token = await this.getToken(); const response = await axios.get(`${API_URL}/sight/${this.sightId}`);
const response = await axios.get(
`https://wn-ts.krbl.ru/sight/${this.sightId}`,
{
headers: { Authorization: `Bearer ${token}` },
}
);
this.stopName = response.data.name; this.stopName = response.data.name;
}, },
async fetchArticles() { async fetchArticles() {
const token = await this.getToken();
const response = await axios.get( const response = await axios.get(
`https://wn-ts.krbl.ru/sight/${this.sightId}/article`, `${API_URL}/sight/${this.sightId}/article`
{
headers: { Authorization: `Bearer ${token}` },
}
); );
this.articles = response.data; this.articles = response.data;
if (this.articles.length > 0) { if (this.articles.length > 0) {

2
src/config.js Normal file
View File

@ -0,0 +1,2 @@
export const API_URL = process.env.VUE_APP_API_URL;
export const GEO_URL = process.env.VUE_APP_GEO_URL;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

35
src/icons/tram-left.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

35
src/icons/tram-right.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB