Compare commits

5 Commits
main ... pre

Author SHA1 Message Date
819246839f route stations update, timeouts in env
All checks were successful
release-tag / release-image (push) Successful in 1m6s
2025-07-14 01:42:38 +03:00
49576a0be7 carrier info fix, right widget update, live tracking device upd, bug fixes
All checks were successful
release-tag / release-image (push) Successful in 55s
2025-07-13 14:43:24 +03:00
17b95707e7 tram icon, video-previews, resizing when inactive
All checks were successful
release-tag / release-image (push) Successful in 53s
2025-07-10 07:58:33 +03:00
12c2a678a5 stations offset fix, live geo tracking, sights on route
All checks were successful
release-tag / release-image (push) Successful in 54s
2025-07-07 02:10:46 +03:00
c1e6c6cfb8 trying new map
All checks were successful
release-tag / release-image (push) Successful in 56s
2025-07-05 15:32:00 +03:00
9 changed files with 1613 additions and 1268 deletions

6
.env
View File

@ -1,6 +1,10 @@
# VUE_APP_API_URL=http://31.129.106.67:8080 # VUE_APP_API_URL=http://31.129.106.67:8080
# VUE_APP_GEO_URL=http://31.129.106.67:6001 # VUE_APP_GEO_URL=http://31.129.106.67:6001
# VUE_APP_WEATHER_URL=http://31.129.106.67:6002 # VUE_APP_WEATHER_URL=http://31.129.106.67:6002
VUE_APP_API_URL=http://127.0.0.1:8080 VUE_APP_API_URL=http://127.0.0.1:8080
VUE_APP_GEO_URL=http://127.0.0.1:6001 VUE_APP_GEO_URL=http://127.0.0.1:6001
VUE_APP_WEATHER_URL=http://127.0.0.1:6002 VUE_APP_WEATHER_URL=http://127.0.0.1:6002
VUE_APP_VIDEO_TIMEOUT=120000
VUE_APP_TRACKING_TIMEOUT=15000

View File

@ -112,6 +112,24 @@ body {
margin-bottom: 15px; margin-bottom: 15px;
} }
.back-button {
border: none;
font-size: 22px;
font-weight: 600;
text-align: left;
padding: 15px;
color: #fff;
background: rgb(187, 179, 170);
background: linear-gradient(
180deg,
rgba(187, 179, 170, 1) 0%,
rgba(159, 148, 135, 1) 100%
);
display: flex;
align-items: center;
gap: 10px;
}
.stopdescription { .stopdescription {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
@ -687,6 +705,7 @@ body {
} }
.dropdown-name { .dropdown-name {
position: relative;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #fff; color: #fff;
@ -695,7 +714,6 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center;
width: 100%; width: 100%;
} }
@ -890,6 +908,8 @@ li.checked {
word-wrap: break-word; word-wrap: break-word;
word-break: break-word; word-break: break-word;
hyphens: auto; hyphens: auto;
max-height: 51px;
overflow-y: auto;
} }
.sight-letter { .sight-letter {
@ -1231,3 +1251,25 @@ li.checked {
flex: 0 0 calc((100% - 24px) / 3); flex: 0 0 calc((100% - 24px) / 3);
box-sizing: border-box; box-sizing: border-box;
} }
.carrier-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 15px;
}
.carrier-img {
width: 100%;
padding: 0;
margin: 0;
}
.carrier-slogan {
font-size: 18px;
color: #fff;
opacity: 70%;
text-align: center;
font-style: italic;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,26 @@
class="watermark watermark-rd" class="watermark watermark-rd"
/> />
</div> </div>
<button
v-if="showBackButton"
class="back-button"
@click="returnToNearestSight"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="21"
fill="none"
viewBox="0 0 12 21"
>
<path
fill="#fff"
d="M3.53 10.574c.188.135.316.203.414.3 2.486 2.495 4.966 4.992 7.451 7.486.445.447.623.96.44 1.574a1.485 1.485 0 0 1-2.383.716c-.09-.076-.171-.16-.254-.244L.658 11.82c-.878-.885-.877-1.673.003-2.56C3.539 6.364 6.414 3.463 9.307.58c.252-.25.606-.46.948-.542.646-.154 1.26.184 1.563.75.307.566.22 1.274-.238 1.764-.352.378-.725.736-1.09 1.101-2.195 2.204-4.389 4.407-6.585 6.609-.082.083-.179.15-.374.312Z"
/>
</svg>
{{ t("back") }}
</button>
<span class="stopname">{{ stopName }}</span> <span class="stopname">{{ stopName }}</span>
<span class="stopdescription">{{ selectedArticleBody }}</span> <span class="stopdescription">{{ selectedArticleBody }}</span>
<div class="stoparticles"> <div class="stoparticles">
@ -261,6 +281,11 @@ export default {
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",
sightId: 17, sightId: 17,
manualSightId: null,
nearestSightId: null,
returnTimer: null,
lastUserActivity: Date.now(),
firstLoad: true,
stopName: "", stopName: "",
watermarkLU: "", watermarkLU: "",
watermarkRD: "", watermarkRD: "",
@ -291,10 +316,10 @@ export default {
routeProgress: null, routeProgress: null,
routeId: null, routeId: null,
selectedLang: localStorage.getItem("selectedLangRight") || "ru", selectedLang: localStorage.getItem("selectedLangRight") || "ru",
showLangToggle: true, // видна «глобус»-кнопка showLangToggle: true,
showLanguageOptions: false, // виден блок из трёх иконок showLanguageOptions: false,
languageOptionsTimer: null, // таймер на 10 с languageOptionsTimer: null,
langRevertTimer: null, // таймер на 30 с langRevertTimer: null,
icons: { icons: {
ru: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28"> ru: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28">
<path fill="#fff" d="M14 0C6.27 0 0 6.27 0 14s6.27 14 14 14 14-6.27 14-14S21.73 0 14 0Zm.117 19.57H11.62l-2.117-4.135H7.646v4.136H5.32V8.27h4.2c1.336 0 2.363.298 3.092.893.729.595 1.085 1.435 1.085 2.52 0 .77-.17 1.412-.502 1.931-.332.513-.84.928-1.517 1.23l2.444 4.62v.112l-.005-.006Zm9.391-3.855c0 1.237-.385 2.217-1.16 2.934-.776.718-1.832 1.08-3.174 1.08-1.341 0-2.368-.35-3.144-1.05-.776-.7-1.173-1.657-1.19-2.882V8.272h2.328v7.46c0 .741.175 1.278.53 1.616.356.339.846.508 1.47.508 1.307 0 1.972-.689 1.995-2.065V8.27h2.334v7.444h.011Z"/> <path fill="#fff" d="M14 0C6.27 0 0 6.27 0 14s6.27 14 14 14 14-6.27 14-14S21.73 0 14 0Zm.117 19.57H11.62l-2.117-4.135H7.646v4.136H5.32V8.27h4.2c1.336 0 2.363.298 3.092.893.729.595 1.085 1.435 1.085 2.52 0 .77-.17 1.412-.502 1.931-.332.513-.84.928-1.517 1.23l2.444 4.62v.112l-.005-.006Zm9.391-3.855c0 1.237-.385 2.217-1.16 2.934-.776.718-1.832 1.08-3.174 1.08-1.341 0-2.368-.35-3.144-1.05-.776-.7-1.173-1.657-1.19-2.882V8.272h2.328v7.46c0 .741.175 1.278.53 1.616.356.339.846.508 1.47.508 1.307 0 1.972-.689 1.995-2.065V8.27h2.334v7.444h.011Z"/>
@ -310,7 +335,7 @@ export default {
}, },
translations: { translations: {
sights: { ru: "Достопримечательности", en: "Landmarks", zh: "景点" }, sights: { ru: "Достопримечательности", en: "Landmarks", zh: "景点" },
// при необходимости — другие подписи back: { ru: "Вернуться назад", en: "Back", zh: "返回" },
}, },
}; };
}, },
@ -352,6 +377,9 @@ export default {
Object.entries(this.nextStopTransfers).filter(([, value]) => value) Object.entries(this.nextStopTransfers).filter(([, value]) => value)
); );
}, },
showBackButton() {
return this.sightId !== this.nearestSightId;
},
}, },
methods: { methods: {
t(key) { t(key) {
@ -513,86 +541,14 @@ export default {
} }
}, },
async openSightCardDetails(id) { async openSightCardDetails(id) {
// закрыть, если нажали повторно this.manualSightId = id;
if (this.selectedSightCardId === id) { this.sightId = id;
this.selectedSightCardId = null; this.showSightsList = false;
this.cardDetail = null; this.selectedSightCardId = null;
return;
}
this.selectedSightCardId = id;
this.cardDetail = null; this.cardDetail = null;
this.resetSightsInactivityTimer(); this.resetReturnTimer();
await this.fetchSightInfo();
try { await this.fetchArticles();
const [detailRes, articlesRes] = await Promise.all([
axios.get(this.addLangParam(`${API_URL}/sight/${id}`)),
axios.get(this.addLangParam(`${API_URL}/sight/${id}/article`)),
]);
// Fetch media for every article
const articlesWithMedia = await Promise.all(
articlesRes.data.map(async (article) => {
try {
const mediaRes = await axios.get(
this.addLangParam(`${API_URL}/article/${article.id}/media`)
);
return { ...article, media: mediaRes.data };
} catch (mediaErr) {
console.error(
`Failed to fetch media for article ${article.id}:`,
mediaErr
);
return { ...article, media: [] };
}
})
);
// Console output: sight name, articles, and their media
console.log("Sight clicked:", {
name: detailRes.data.name,
articles: articlesWithMedia,
});
// Choose preview image: prefer the first article's first media, otherwise fall back to sight thumbnail
let imageUrl = "";
if (
articlesWithMedia.length > 0 &&
articlesWithMedia[0].media &&
articlesWithMedia[0].media.length > 0
) {
const firstMediaId = articlesWithMedia[0].media[0].id;
try {
imageUrl = await this.getMediaBlobUrl(firstMediaId);
} catch {
imageUrl = this.addLangParam(
`${API_URL}/media/${firstMediaId}/download`
);
}
} else if (detailRes.data.thumbnail) {
try {
imageUrl = await this.getMediaBlobUrl(detailRes.data.thumbnail);
} catch {
imageUrl = this.addLangParam(
`${API_URL}/media/${detailRes.data.thumbnail}/download`
);
}
}
this.cardDetail = {
name: detailRes.data.name,
imageUrl,
articles: articlesWithMedia,
};
if (articlesWithMedia.length > 0) {
this.selectedSightArticleId = articlesWithMedia[0].id;
this.selectedSightArticleBody = articlesWithMedia[0].body;
} else {
this.selectedSightArticleId = null;
this.selectedSightArticleBody = "";
}
} catch (err) {
console.error("Failed to load sight card details:", err);
}
}, },
async selectSightArticle(id) { async selectSightArticle(id) {
this.selectedSightArticleId = id; this.selectedSightArticleId = id;
@ -657,10 +613,12 @@ export default {
}, 300_000); // 5 m }, 300_000); // 5 m
}, },
handleUserActivity() { handleUserActivity() {
this.lastUserActivity = Date.now();
if (this.showSightsList || this.cardDetail) if (this.showSightsList || this.cardDetail)
this.resetSightsInactivityTimer(); this.resetSightsInactivityTimer();
this.resetArticleInactivityTimer(); this.resetArticleInactivityTimer();
this.resetLangRevertTimer(); this.resetLangRevertTimer();
if (this.returnTimer) this.resetReturnTimer();
}, },
selectArticle(id) { selectArticle(id) {
this.resetArticleInactivityTimer(); this.resetArticleInactivityTimer();
@ -738,23 +696,41 @@ export default {
newSightId = this.sights[1].id; newSightId = this.sights[1].id;
} }
} }
if (newSightId && newSightId !== this.sightId) { if (newSightId) {
this.sightId = newSightId; if (this.firstLoad) {
await this.fetchSightInfo(); this.nearestSightId = newSightId;
await this.fetchArticles(); this.sightId = newSightId;
await this.fetchSightInfo();
await this.fetchArticles();
this.firstLoad = false;
this.clearReturnTimer();
return;
}
if (this.nearestSightId !== newSightId) {
this.nearestSightId = newSightId;
}
if (!this.manualSightId) {
if (this.sightId !== this.nearestSightId) {
const userActive = Date.now() - this.lastUserActivity < 15_000; // 15-сек. окно
if (userActive) {
this.resetReturnTimer();
} else {
this.clearReturnTimer();
this.sightId = this.nearestSightId;
await this.fetchSightInfo();
await this.fetchArticles();
}
} else {
this.clearReturnTimer();
}
}
} }
const nextStopId = response.data.routeProgress?.endStopId; const nextStopId = response.data.routeProgress?.endStopId;
// console.log("Fetched next stop ID:", nextStopId);
// 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("Matching stop:", nextStop);
if (nextStop && nextStop.transfers) { if (nextStop && 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");
this.nextStopTransfers = null; this.nextStopTransfers = null;
} }
} }
@ -762,6 +738,29 @@ export default {
console.error("Error fetching geolocation context:", error); console.error("Error fetching geolocation context:", error);
} }
}, },
resetReturnTimer() {
this.clearReturnTimer();
this.returnTimer = setTimeout(() => {
this.returnToNearestSight();
}, 90_000);
},
clearReturnTimer() {
if (this.returnTimer) {
clearTimeout(this.returnTimer);
this.returnTimer = null;
}
},
returnToNearestSight() {
if (!this.nearestSightId) return;
this.manualSightId = null;
this.sightId = this.nearestSightId;
this.clearReturnTimer();
this.showSightsList = false;
this.selectedSightCardId = null;
this.cardDetail = null;
this.fetchSightInfo();
this.fetchArticles();
},
selectLetter(letter) { selectLetter(letter) {
const anchorArr = this.$refs[`letter-${letter}`]; const anchorArr = this.$refs[`letter-${letter}`];
const anchor = anchorArr ? anchorArr[0] : null; const anchor = anchorArr ? anchorArr[0] : null;
@ -818,6 +817,7 @@ export default {
async mounted() { async mounted() {
await this.fetchSights(); await this.fetchSights();
await this.fetchGeolocationContext(); await this.fetchGeolocationContext();
this.nearestSightId = this.sightId;
this.geolocationInterval = setInterval(() => { this.geolocationInterval = setInterval(() => {
this.fetchGeolocationContext(); this.fetchGeolocationContext();
}, 1000); }, 1000);

View File

@ -1,3 +1,5 @@
export const API_URL = process.env.VUE_APP_API_URL; export const API_URL = process.env.VUE_APP_API_URL;
export const GEO_URL = process.env.VUE_APP_GEO_URL; export const GEO_URL = process.env.VUE_APP_GEO_URL;
export const WEATHER_URL = process.env.VUE_APP_WEATHER_URL; export const WEATHER_URL = process.env.VUE_APP_WEATHER_URL;
export const VIDEO_TIMEOUT = process.env.VUE_APP_VIDEO_TIMEOUT || 120000;
export const TRACKING_TIMEOUT = process.env.VUE_APP_TRACKING_TIMEOUT || 15000;

21
src/icons/sight.svg Normal file
View File

@ -0,0 +1,21 @@
<svg width="67" height="62" viewBox="0 0 67 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 25.6107H66.3216V20.5767L33.1702 0.100708L0 20.5767V25.6107ZM33.1699 4.90984L5.97994 21.6835H60.341L33.1699 4.90984Z" fill="#A6A6A6"/>
<path d="M66.3216 58.2223H0V62.0001H66.3216V58.2223Z" fill="#A6A6A6"/>
<path d="M62.9961 53.1895H3.32556V56.9673H62.9961V53.1895Z" fill="#A6A6A6"/>
<path d="M11.9052 27.7542H6.91687V50.0529H11.9052V27.7542Z" fill="#A6A6A6"/>
<path d="M14.3989 50.5732H4.42236V52.0466H14.3989V50.5732Z" fill="#A6A6A6"/>
<path d="M5.53691 26.5447C5.15901 26.5447 4.85669 26.8469 4.85669 27.2247C4.85669 27.6025 5.15901 27.9047 5.53691 27.9047C5.91481 27.9047 6.21713 27.6025 6.21713 27.2247H12.6037C12.6037 27.6025 12.906 27.9047 13.2839 27.9047C13.6618 27.9047 13.9641 27.6025 13.9641 27.2247C13.9641 26.8469 13.6618 26.5447 13.2839 26.5447H5.53691Z" fill="#A6A6A6"/>
<path d="M27.8002 27.7542H22.8119V50.0529H27.8002V27.7542Z" fill="#A6A6A6"/>
<path d="M30.2962 50.5732H20.3196V52.0466H30.2962V50.5732Z" fill="#A6A6A6"/>
<path d="M21.4319 26.5447C21.054 26.5447 20.7517 26.8469 20.7517 27.2247C20.7517 27.6025 21.054 27.9047 21.4319 27.9047C21.8098 27.9047 22.1122 27.6025 22.1122 27.2247H28.4987C28.4987 27.6025 28.801 27.9047 29.1789 27.9047C29.5568 27.9047 29.8591 27.6025 29.8591 27.2247C29.8591 26.8469 29.5568 26.5447 29.1789 26.5447H21.4319Z" fill="#A6A6A6"/>
<path d="M43.7068 27.7542H38.7185V50.0529H43.7068V27.7542Z" fill="#A6A6A6"/>
<path d="M46.2005 50.5732H36.2239V52.0466H46.2005V50.5732Z" fill="#A6A6A6"/>
<path d="M37.3385 26.5447C36.9606 26.5447 36.6583 26.8469 36.6583 27.2247C36.6583 27.6025 36.9606 27.9047 37.3385 27.9047C37.7164 27.9047 38.0188 27.6025 38.0188 27.2247H44.4053C44.4053 27.6025 44.7076 27.9047 45.0855 27.9047C45.4634 27.9047 45.7657 27.6025 45.7657 27.2247C45.7657 26.8469 45.4634 26.5447 45.0855 26.5447H37.3385Z" fill="#A6A6A6"/>
<path d="M59.6012 27.754H54.6129V50.0528H59.6012V27.754Z" fill="#A6A6A6"/>
<path d="M62.0972 50.5731H52.1206V52.0465H62.0972V50.5731Z" fill="#A6A6A6"/>
<path d="M53.233 26.5446C52.8551 26.5446 52.5527 26.8468 52.5527 27.2246C52.5527 27.6024 52.8551 27.9046 53.233 27.9046C53.6109 27.9046 53.9132 27.6024 53.9132 27.2246H60.2997C60.2997 27.6024 60.602 27.9046 60.9799 27.9046C61.3578 27.9046 61.6601 27.6024 61.6601 27.2246C61.6601 26.8468 61.3578 26.5446 60.9799 26.5446H53.233Z" fill="#A6A6A6"/>
<path d="M33.7759 12.162C33.7759 11.8598 33.4831 11.6143 33.124 11.6143C32.765 11.6143 32.4722 11.8598 32.4722 12.162V12.5965C32.4722 12.9554 32.765 13.2482 33.124 13.2482C33.4831 13.2482 33.7759 12.9554 33.7759 12.5965V12.162Z" fill="#A6A6A6"/>
<path d="M24.5449 15.0146C24.2426 15.0146 23.9969 15.3074 23.9969 15.6758C23.9969 16.0347 24.2426 16.3369 24.5449 16.3369H25.6786C26.0376 16.3369 26.3399 16.0441 26.3399 15.6758C26.3399 15.3169 26.0471 15.0146 25.6786 15.0146H24.5449Z" fill="#A6A6A6"/>
<path d="M40.5117 15.0146C40.2094 15.0146 39.9637 15.3074 39.9637 15.6758C39.9637 16.0347 40.2094 16.3369 40.5117 16.3369H41.6454C42.0044 16.3369 42.3067 16.0441 42.3067 15.6758C42.3067 15.3169 42.0139 15.0146 41.6454 15.0146H40.5117Z" fill="#A6A6A6"/>
<path d="M34.7458 14.3911L33.9333 13.8717C33.9333 13.8717 33.8388 13.8245 33.7821 13.8245H32.4595C32.4028 13.8245 32.3556 13.8434 32.3083 13.8717L31.4958 14.3911C31.4108 14.4478 31.3636 14.5328 31.3636 14.6273V15.2317C31.3636 15.2884 31.3825 15.3451 31.4108 15.3923L32.1194 16.4406C32.1761 16.5256 32.1855 16.6201 32.1477 16.7145L31.3825 18.4523C31.3825 18.4523 31.3541 18.5279 31.3541 18.5657V20.294C31.3541 20.4546 31.4769 20.5774 31.6376 20.5774H34.5852C34.7458 20.5774 34.8686 20.4546 34.8686 20.294V18.5657C34.8686 18.5657 34.8686 18.4901 34.8403 18.4523L34.075 16.7145C34.0372 16.6295 34.0467 16.5256 34.1034 16.4406L34.8119 15.3923C34.8119 15.3923 34.8592 15.2884 34.8592 15.2317V14.6273C34.8592 14.5328 34.8119 14.4384 34.7269 14.3911H34.7458Z" fill="#A6A6A6"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

373
src/icons/spb-gerb.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 176 KiB

30
src/icons/tram-icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB