From 49576a0be7708535d31ea7f4790cefad69bab819 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 13 Jul 2025 14:43:24 +0300 Subject: [PATCH] carrier info fix, right widget update, live tracking device upd, bug fixes --- src/assets/style/main.css | 42 ++- src/components/carrierinfo.vue | 656 +++------------------------------ src/components/main.vue | 59 ++- src/components/stopinfo.vue | 188 +++++----- src/icons/spb-gerb.svg | 373 +++++++++++++++++++ 5 files changed, 602 insertions(+), 716 deletions(-) create mode 100644 src/icons/spb-gerb.svg diff --git a/src/assets/style/main.css b/src/assets/style/main.css index 56a482b..84a1035 100644 --- a/src/assets/style/main.css +++ b/src/assets/style/main.css @@ -112,6 +112,24 @@ body { 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 { font-size: 16px; font-weight: 400; @@ -687,6 +705,7 @@ body { } .dropdown-name { + position: relative; font-size: 18px; font-weight: 600; color: #fff; @@ -695,7 +714,6 @@ body { display: flex; flex-direction: column; align-items: center; - text-align: center; width: 100%; } @@ -1231,3 +1249,25 @@ li.checked { flex: 0 0 calc((100% - 24px) / 3); 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; +} diff --git a/src/components/carrierinfo.vue b/src/components/carrierinfo.vue index c337402..0c4804b 100644 --- a/src/components/carrierinfo.vue +++ b/src/components/carrierinfo.vue @@ -2,593 +2,7 @@
- +
@@ -620,18 +34,18 @@
- +
+ +
+ {{ carrierSlogan }} +
+
{{ t("hashtag") }}
@@ -896,6 +316,7 @@ export default { stopsInterval: null, sightTransfers: {}, showSightTransfers: false, + spbGerb: require("@/icons/spb-gerb.svg"), transferIcons: { tram: require("@/icons/tram.svg"), trolleybus: require("@/icons/trolleybus.svg"), @@ -923,6 +344,9 @@ export default { }, hashtag: { ru: "#ВсемПоПути", en: "#OnOurWay", zh: "#同路人" }, }, + carrierId: null, + carrierLogoUrl: "", + carrierSlogan: "", }; }, computed: { @@ -977,6 +401,23 @@ export default { console.error("Ошибка при получении обращения губернатора:", err); } }, + async fetchCarrierInfo(carrierId) { + try { + const res = await fetch( + this.addLangParam(`${API_URL}/carrier/${carrierId}`) + ); + const data = await res.json(); + this.carrierSlogan = data.slogan; + if (data.logo) { + const logoRes = await fetch( + this.addLangParam(`${API_URL}/media/${data.logo}/download`) + ); + this.carrierLogoUrl = logoRes.url; + } + } catch (err) { + console.error("Ошибка при получении данных перевозчика:", err); + } + }, async fetchStops() { try { const geoResponse = await fetch(`${GEO_URL}/v1/geolocation/context`); @@ -985,8 +426,14 @@ export default { this.addLangParam(`${API_URL}/route/${geoData.routeId}`) ); const routeDetails = await routeDetailsRes.json(); + const carrierId = routeDetails.carrier_id; const appealId = routeDetails.governor_appeal; + if (carrierId && carrierId !== this.carrierId) { + this.carrierId = carrierId; + await this.fetchCarrierInfo(carrierId); + } + console.log( "Получено значение governor_appeal:", appealId, @@ -1262,6 +709,9 @@ export default { if (this.governorAppealId) { this.loadGovernorAppeal(this.governorAppealId); } + if (this.carrierId) { + this.fetchCarrierInfo(this.carrierId); + } }, addLangParam(url) { // add ?lang=XX to every API_URL request when language ≠ ru @@ -1377,4 +827,8 @@ export default { width: 48px; height: 48px; } + +.toggle-arrow { + margin-top: 15px; +} diff --git a/src/components/main.vue b/src/components/main.vue index db57aae..a469c09 100644 --- a/src/components/main.vue +++ b/src/components/main.vue @@ -74,6 +74,8 @@ export default { manualTramDirection: null, manualDirectionUntil: 0, inactivityTimer: null, + isFollowingTram: false, + followTramTimer: null, chooseTramDirection(canvasPt) { // canvasPt: [x, y] in PIXI coords let bestDir = "right"; @@ -141,6 +143,12 @@ export default { resetInactivity() { this.lastActivityTime = Date.now(); if (this.videoOverlayVisible) this.hideVideoOverlay(); + clearTimeout(this.inactivityTimer); + // stop following on any user activity + this.isFollowingTram = false; + this.stopFollowTram(); + // restart inactivity timer on any user activity + this.resetInactivityTimer(); }, startInactivityCheck() { if (this.inactivityInterval) return; @@ -325,20 +333,17 @@ export default { const minLat = Math.min(...lats); const maxLat = Math.max(...lats); const minLng = Math.min(...lngs); - const maxLng = Math.max(...lngs); - - const canvas = this.pixiApp.canvas; - const width = canvas.width; - const height = canvas.height; const padding = 40; - const scaleX = (width - padding * 2) / (maxLng - minLng || 1); - const scaleY = (height - padding * 2) / (maxLat - minLat || 1); - let scale = 30000; - console.log(scaleX, scaleY, scale); + const scale = 35000; + const midLat = (minLat + maxLat) / 2; + const latFactor = Math.cos((midLat * Math.PI) / 180); + console.log( + `Using fixed scale ${scale} and latFactor ${latFactor.toFixed(4)}` + ); const toPoint = ([lat, lng]) => { - const x = (lng - minLng) * scale + padding; + const x = (lng - minLng) * scale * latFactor + padding; const y = (maxLat - lat) * scale + padding; return { x, y }; }; @@ -1079,29 +1084,39 @@ export default { }, // reset the inactivity timeout resetInactivityTimer() { + console.log("[inactivity] scheduling follow in 15s"); clearTimeout(this.inactivityTimer); this.inactivityTimer = setTimeout(() => { + this.isFollowingTram = true; + this.startFollowTram(); + }, 15000); + }, + + startFollowTram() { + console.log("[follow] startFollowTram() triggered"); + if (this.followTramTimer) return; + this.followTramTimer = setInterval(() => { if (this.tramContainer) { - // compute tram position in viewport world coordinates - const localPoint = new PIXI.Point( + const local = new PIXI.Point( this.tramContainer.x, this.tramContainer.y ); - const worldCenter = this.viewport.toLocal( - localPoint, - this.routeGroup - ); - // smooth pan and zoom to tram + const worldCenter = this.viewport.toLocal(local, this.routeGroup); this.viewport.animate({ time: 600, position: { x: worldCenter.x, y: worldCenter.y }, scale: 1, easing: "easeInOutSine", }); - // schedule next recenter - this.resetInactivityTimer(); } - }, 15000); + }, 500); + }, + stopFollowTram() { + console.log("[follow] stopFollowTram() triggered"); + if (this.followTramTimer) { + clearInterval(this.followTramTimer); + this.followTramTimer = null; + } }, // start watching for user interactions @@ -1125,6 +1140,10 @@ export default { clearInterval(this.inactivityInterval); this.inactivityInterval = null; } + if (this.followTramTimer) { + clearInterval(this.followTramTimer); + this.followTramTimer = null; + } ["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach( (evt) => window.removeEventListener(evt, this.resetInactivity) ); diff --git a/src/components/stopinfo.vue b/src/components/stopinfo.vue index fc5be15..5cc8d17 100644 --- a/src/components/stopinfo.vue +++ b/src/components/stopinfo.vue @@ -43,6 +43,26 @@ class="watermark watermark-rd" /> + {{ stopName }} {{ selectedArticleBody }}
@@ -261,6 +281,11 @@ export default { defaultImageUrl: "https://lh3.googleusercontent.com/gps-cs-s/AB5caB8lUwofb2NIg6n0-cEl8nIWsySAUc52KNj4XezuOdo-aeqTgQlD1kTVa5MaynL2Yg4ByoTYTKNTR7K59f7kjzU9yzpudstjRiT2F6M_ilxFYFpcvMZz6OwlRFF2MrsCPSwUa7vqew=s680-w680-h510", sightId: 17, + manualSightId: null, + nearestSightId: null, + returnTimer: null, + lastUserActivity: Date.now(), + firstLoad: true, stopName: "", watermarkLU: "", watermarkRD: "", @@ -291,10 +316,10 @@ export default { routeProgress: null, routeId: null, selectedLang: localStorage.getItem("selectedLangRight") || "ru", - showLangToggle: true, // видна «глобус»-кнопка - showLanguageOptions: false, // виден блок из трёх иконок - languageOptionsTimer: null, // таймер на 10 с - langRevertTimer: null, // таймер на 30 с + showLangToggle: true, + showLanguageOptions: false, + languageOptionsTimer: null, + langRevertTimer: null, icons: { ru: ` @@ -310,7 +335,7 @@ export default { }, translations: { sights: { ru: "Достопримечательности", en: "Landmarks", zh: "景点" }, - // при необходимости — другие подписи + back: { ru: "Вернуться назад", en: "Back", zh: "返回" }, }, }; }, @@ -352,6 +377,9 @@ export default { Object.entries(this.nextStopTransfers).filter(([, value]) => value) ); }, + showBackButton() { + return this.sightId !== this.nearestSightId; + }, }, methods: { t(key) { @@ -513,86 +541,14 @@ export default { } }, async openSightCardDetails(id) { - // закрыть, если нажали повторно - if (this.selectedSightCardId === id) { - this.selectedSightCardId = null; - this.cardDetail = null; - return; - } - this.selectedSightCardId = id; + this.manualSightId = id; + this.sightId = id; + this.showSightsList = false; + this.selectedSightCardId = null; this.cardDetail = null; - this.resetSightsInactivityTimer(); - - try { - 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); - } + this.resetReturnTimer(); + await this.fetchSightInfo(); + await this.fetchArticles(); }, async selectSightArticle(id) { this.selectedSightArticleId = id; @@ -657,10 +613,12 @@ export default { }, 300_000); // 5 m }, handleUserActivity() { + this.lastUserActivity = Date.now(); if (this.showSightsList || this.cardDetail) this.resetSightsInactivityTimer(); this.resetArticleInactivityTimer(); this.resetLangRevertTimer(); + if (this.returnTimer) this.resetReturnTimer(); }, selectArticle(id) { this.resetArticleInactivityTimer(); @@ -738,23 +696,41 @@ export default { newSightId = this.sights[1].id; } } - if (newSightId && newSightId !== this.sightId) { - this.sightId = newSightId; - await this.fetchSightInfo(); - await this.fetchArticles(); + if (newSightId) { + if (this.firstLoad) { + this.nearestSightId = newSightId; + 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; - // console.log("Fetched next stop ID:", nextStopId); - // console.log("Stops:", this.stops); if (nextStopId && this.stops) { 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) { - // console.log("Transfers at next stop:", nextStop.transfers); this.nextStopTransfers = nextStop.transfers; } else { - // console.log("No transfers found at next stop"); this.nextStopTransfers = null; } } @@ -762,6 +738,29 @@ export default { 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) { const anchorArr = this.$refs[`letter-${letter}`]; const anchor = anchorArr ? anchorArr[0] : null; @@ -818,6 +817,7 @@ export default { async mounted() { await this.fetchSights(); await this.fetchGeolocationContext(); + this.nearestSightId = this.sightId; this.geolocationInterval = setInterval(() => { this.fetchGeolocationContext(); }, 1000); diff --git a/src/icons/spb-gerb.svg b/src/icons/spb-gerb.svg new file mode 100644 index 0000000..875b8cf --- /dev/null +++ b/src/icons/spb-gerb.svg @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +