Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
43400bb933 | |||
93ef656f7b | |||
13375abf24 | |||
f9ca0bdbd9 | |||
b9ab746cfd |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
*.md
|
||||||
|
dist
|
4
.env
4
.env
@ -1,4 +1,6 @@
|
|||||||
# 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_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
|
51
.gitea/workflows/publish.yaml
Normal file
51
.gitea/workflows/publish.yaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: release-tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: krbl
|
||||||
|
DOCKER_LATEST: nightly
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
IMAGE_NAME: white-nights-ts
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
config-inline: |
|
||||||
|
[registry."gitea.unprism.ru"]
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.unprism.ru
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea.unprism.ru/${{ env.DOCKER_ORG }}/${{ env.IMAGE_NAME }}:${{ env.DOCKER_LATEST }}
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM node:lts-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
34
nginx.conf
Normal file
34
nginx.conf
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost; # Замените на ваш домен, если необходимо
|
||||||
|
|
||||||
|
# Корневая директория для статических файлов
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Важно для Single Page Applications (SPA) вроде Vue
|
||||||
|
# Пытается отдать файл по $uri, затем директорию $uri/,
|
||||||
|
# иначе отдает /index.html (чтобы Vue Router обработал маршрут)
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Опционально: gzip сжатие для улучшения производительности
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
|
||||||
|
# Опционально: Заголовки кэширования для статических ассетов
|
||||||
|
location ~* \.(?:jpg|jpeg|gif|png|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Опционально: Обработка ошибок (рекомендуется)
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
max-height: calc(100vh - 100px);
|
||||||
margin: auto 25px 25px 25px;
|
margin: auto 25px 25px 25px;
|
||||||
/* height: 100%; */
|
/* height: 100%; */
|
||||||
/* background: #806c58; */
|
/* background: #806c58; */
|
||||||
@ -114,17 +115,21 @@ body {
|
|||||||
.stopdescription {
|
.stopdescription {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
padding: 0 15px 15px 15px;
|
padding: 0 15px 0 15px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
max-height: calc(100% - 430px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.landmarks {
|
.landmarks {
|
||||||
background: #806c58;
|
background: #806c58;
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
width: 450px;
|
||||||
margin: 0 25px;
|
margin: 0 25px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -133,17 +138,56 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landmarks-arrow {
|
/* .landmarks-arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 25px;
|
left: 25px;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.landmarks-container {
|
.landmarks-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-toggle {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 1001;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-option {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 1001;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landmarks-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.stoparticles {
|
.stoparticles {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -167,6 +211,12 @@ body {
|
|||||||
.stoparticle-option {
|
.stoparticle-option {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-button {
|
.description-button {
|
||||||
@ -520,23 +570,37 @@ body {
|
|||||||
.carrier-toggle {
|
.carrier-toggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
left: 310px;
|
|
||||||
background: rgba(0, 0, 0, 0);
|
background: rgba(0, 0, 0, 0);
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 5px 10px;
|
padding: 5px 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
|
left: var(--panel-offset, 310px);
|
||||||
transition: left 0.3s ease;
|
transition: left 0.3s ease;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carrierinfo .bg.hidden + .carrier-toggle {
|
.lang-toggle {
|
||||||
left: 10px;
|
margin-left: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lang-option:nth-of-type(1) {
|
||||||
|
margin-left: 60px;
|
||||||
|
}
|
||||||
|
.lang-option:nth-of-type(2) {
|
||||||
|
margin-left: 120px;
|
||||||
|
}
|
||||||
|
.lang-option:nth-of-type(3) {
|
||||||
|
margin-left: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .carrierinfo .bg.hidden + .carrier-toggle {
|
||||||
|
left: 10px;
|
||||||
|
} */
|
||||||
|
|
||||||
.bg.hidden {
|
.bg.hidden {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
@ -594,10 +658,6 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carrierinfo .bg.hidden + .carrier-toggle {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg.hidden {
|
.bg.hidden {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
@ -669,7 +729,13 @@ body {
|
|||||||
);
|
);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: white;
|
color: white;
|
||||||
padding-bottom: 20px;
|
}
|
||||||
|
|
||||||
|
.sight-preview-panel.left-panel {
|
||||||
|
right: 490px;
|
||||||
|
left: auto;
|
||||||
|
top: auto;
|
||||||
|
bottom: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sight-preview-panel img {
|
.sight-preview-panel img {
|
||||||
@ -686,7 +752,7 @@ body {
|
|||||||
|
|
||||||
.sight-preview-panel p {
|
.sight-preview-panel p {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin: 0;
|
margin: 0 0 20px 0;
|
||||||
padding: 10px 10px 0 10px;
|
padding: 10px 10px 0 10px;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -770,13 +836,14 @@ li.checked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sight-card {
|
.sight-card {
|
||||||
width: 30%;
|
/* width: 30%; */
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sight-thumbnail {
|
.sight-thumbnail {
|
||||||
|
@ -589,10 +589,7 @@
|
|||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="gos-name"
|
<span class="gos-name" v-html="t('govt_support')"></span>
|
||||||
>При поддержке Правительства <br />
|
|
||||||
Санкт-Петербурга</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
@ -602,13 +599,13 @@
|
|||||||
class="stop-button yellow"
|
class="stop-button yellow"
|
||||||
@click="toggleGovernorAppeal"
|
@click="toggleGovernorAppeal"
|
||||||
>
|
>
|
||||||
{{ governorAppealTitle || "Обращение" }}
|
{{ governorAppealTitle || t("appeal") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div ref="dropdownWrapper" class="dropdown-wrapper">
|
<div ref="dropdownWrapper" class="dropdown-wrapper">
|
||||||
<div class="stop-buttons-container">
|
<div class="stop-buttons-container">
|
||||||
<button class="stop-button white" @click="toggleSights">
|
<button class="stop-button white" @click="toggleSights">
|
||||||
Достопримечательности
|
{{ t("sights") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
@ -635,7 +632,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
Достопримечательности
|
{{ t("sights") }}
|
||||||
</div>
|
</div>
|
||||||
<li
|
<li
|
||||||
v-for="sight in sights"
|
v-for="sight in sights"
|
||||||
@ -644,10 +641,29 @@
|
|||||||
@click="selectSight(sight.id)"
|
@click="selectSight(sight.id)"
|
||||||
>
|
>
|
||||||
<div class="sight-name">{{ sight.name }}</div>
|
<div class="sight-name">{{ sight.name }}</div>
|
||||||
|
<div
|
||||||
|
class="sight-transfer-list"
|
||||||
|
v-if="
|
||||||
|
selectedSightId === sight.id && hasTransfers(sight.transfers)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="transfer-row"
|
||||||
|
v-for="item in filteredEntries(sight.transfers)"
|
||||||
|
:key="item.key"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="transferIcons[item.key]"
|
||||||
|
class="transfer-icon"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<span class="transfer-text">{{ item.val }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<button class="stop-button white" @click="toggleList">
|
<button class="stop-button white" @click="toggleList">
|
||||||
Остановки
|
{{ t("stops") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -671,7 +687,7 @@
|
|||||||
style="mix-blend-mode: soft-light"
|
style="mix-blend-mode: soft-light"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Остановки
|
{{ t("stops") }}
|
||||||
</div>
|
</div>
|
||||||
<li v-for="station in stations" :key="station.id">
|
<li v-for="station in stations" :key="station.id">
|
||||||
<div class="sight-name">{{ station.name }}</div>
|
<div class="sight-name">{{ station.name }}</div>
|
||||||
@ -681,7 +697,7 @@
|
|||||||
|
|
||||||
<img class="carrier-img" src="../assets/img/get_new.svg" />
|
<img class="carrier-img" src="../assets/img/get_new.svg" />
|
||||||
|
|
||||||
<span class="hashtag">#ВсемПоПути</span>
|
<span class="hashtag">{{ t("hashtag") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="carrier-toggle" @click="toggleCarrierInfo">
|
<button class="carrier-toggle" @click="toggleCarrierInfo">
|
||||||
<svg
|
<svg
|
||||||
@ -711,29 +727,142 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<!-- single‑button globe – shown until the user clicks it -->
|
||||||
<div v-if="showGovernorAppeal" class="governor-appeal sight-preview-panel">
|
<button
|
||||||
<img :src="governorAppealImage" v-if="governorAppealImage" />
|
v-if="showLangToggle"
|
||||||
<h3>{{ governorAppealTitle }}</h3>
|
class="carrier-toggle lang-toggle"
|
||||||
<p>{{ governorAppealText }}</p>
|
@click="toggleLanguageOptions"
|
||||||
</div>
|
>
|
||||||
<div v-if="showSightPreview" class="sight-preview-panel">
|
<svg
|
||||||
<div class="sight-preview-wrapper" style="position: relative">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<img :src="selectedSightImage" v-if="selectedSightImage" />
|
width="48"
|
||||||
<img
|
height="48"
|
||||||
v-if="selectedSightWatermarkLU"
|
fill="none"
|
||||||
:src="selectedSightWatermarkLU"
|
viewBox="0 0 48 48"
|
||||||
class="watermark watermark-lu"
|
>
|
||||||
/>
|
<path
|
||||||
<img
|
fill="#fff"
|
||||||
v-if="selectedSightWatermarkRD"
|
fill-rule="evenodd"
|
||||||
:src="selectedSightWatermarkRD"
|
d="M0 24C0 10.75 10.75 0 24 0s24 10.75 24 24-10.75 24-24 24S0 37.25 0 24Zm36.05-4.59 2.22 6.06 2.2-6.08-1.3.38c-.01-.02-.01-.42-.01-.45-.58-6.91-6.17-12.17-13.22-12.17-.52 0-.95.43-.95.95s.43.95.95.95c5.98 0 10.74 4.42 11.32 10.26l.005.235.005.235-1.22-.37ZM8.903 27.537c2.45-.58 4.73-1.693 6.667-3.253a13.372 13.372 0 0 0 5.911 3.268c.106.013.213.013.32 0a1.483 1.483 0 0 0 1.337-.567c.31-.406.37-.94.16-1.402a1.46 1.46 0 0 0-1.178-.835 10.091 10.091 0 0 1-4.357-2.399 20.41 20.41 0 0 0 5.403-8.413 1.368 1.368 0 0 0-.29-1.22 1.469 1.469 0 0 0-1.163-.56H16.63v-1.754c0-.5-.277-.963-.726-1.214a1.495 1.495 0 0 0-1.452 0c-.449.25-.726.713-.726 1.214v1.78H8.643c-.52 0-.998.268-1.258.701-.26.434-.26.97 0 1.403.26.433.74.7 1.258.7h11.022a19.358 19.358 0 0 1-3.95 5.413 19.152 19.152 0 0 1-2.295-3.24 1.428 1.428 0 0 0-.83-.806 1.498 1.498 0 0 0-1.176.052 1.416 1.416 0 0 0-.75.877c-.11.382-.048.79.171 1.125a22.29 22.29 0 0 0 2.716 3.745 13.713 13.713 0 0 1-5.243 2.58 1.472 1.472 0 0 0-.982.515 1.374 1.374 0 0 0 .216 1.983c.3.236.686.346 1.071.307a.982.982 0 0 0 .29 0ZM34.9 38.88c.351.15.748.16 1.107.027.374-.12.681-.383.851-.727.17-.344.188-.74.05-1.096l-6.565-16.307a1.41 1.41 0 0 0-.532-.65 1.49 1.49 0 0 0-1.635-.007c-.24.157-.426.381-.534.642l-6.506 15.761c-.192.465-.112.995.21 1.389a1.48 1.48 0 0 0 1.35.519c.514-.073.95-.404 1.141-.87l1.568-3.912h6.913l1.816 4.459c.14.344.415.622.766.772Zm-5.995-13.715 2.28 5.68h-4.619l2.339-5.68Z"
|
||||||
class="watermark watermark-rd"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- three language icons – appear after the globe is pressed -->
|
||||||
|
<div v-if="showLanguageOptions" class="language-options">
|
||||||
|
<!-- RU -->
|
||||||
|
<button
|
||||||
|
class="carrier-toggle lang-option"
|
||||||
|
:style="{ opacity: selectedLang === 'ru' ? 1 : 0.8 }"
|
||||||
|
@click="setLanguage('ru')"
|
||||||
|
>
|
||||||
|
<!-- RU SVG -->
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M24 0C10.75 0 0 10.75 0 24s10.75 24 24 24 24-10.75 24-24S37.25 0 24 0Zm.2 33.55h-4.28l-3.63-7.09h-3.18v7.09H9.12V14.18h7.2c2.29 0 4.05.51 5.3 1.53s1.86 2.46 1.86 4.32c0 1.32-.29 2.42-.86 3.31-.57.88-1.44 1.59-2.6 2.11l4.19 7.92v.19l-.01-.01Zm16.1-6.61c0 2.12-.66 3.8-1.99 5.03-1.33 1.23-3.14 1.85-5.44 1.85-2.3 0-4.06-.6-5.39-1.8-1.33-1.2-2.01-2.84-2.04-4.94v-12.9h3.99v12.79c0 1.27.3 2.19.91 2.77.61.58 1.45.87 2.52.87 2.24 0 3.38-1.18 3.42-3.54V14.18h4v12.76h.02Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M16.309 17.41h-3.21v5.81h3.22c1 0 1.78-.26 2.33-.77s.82-1.21.82-2.11c0-.9-.26-1.63-.78-2.16-.52-.53-1.32-.79-2.39-.79l.01.02Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="a"><path fill="#fff" d="M0 0h48v48H0z" /></clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- ZH -->
|
||||||
|
<button
|
||||||
|
class="carrier-toggle lang-option"
|
||||||
|
:style="{ opacity: selectedLang === 'zh' ? 1 : 0.8 }"
|
||||||
|
@click="setLanguage('zh')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M10.287 20.382H6.291v3.765h3.996v-3.765Zm3.417 3.765h4.017v-3.765h-4.017v3.765Zm22.421-4.101h-6.267c.803 1.895 1.86 3.598 3.193 5.076 1.26-1.43 2.28-3.11 3.074-5.076Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M24 48c13.255 0 24-10.745 24-24S37.255 0 24 0 0 10.745 0 24s10.745 24 24 24ZM10.287 13.5h3.417v3.654h7.413v11.292h-3.396v-1.071h-4.017v6.594h-3.417v-6.594H6.291v1.176H3V17.154h7.287V13.5Zm21.063 0h3.354v3.318h8.379v3.228h-3.194c-1.085 2.905-2.514 5.337-4.308 7.36 2.068 1.518 4.584 2.648 7.587 3.323l.679.153-.5.483c-.572.551-1.37 1.712-1.758 2.429l-.14.258-.285-.074c-3.325-.87-6.04-2.226-8.267-4.053-2.267 1.77-4.958 3.108-8.095 4.09l-.313.098-.139-.297c-.278-.596-1.057-1.787-1.545-2.377l-.374-.452.568-.15c2.937-.772 5.404-1.867 7.438-3.343-1.76-2.088-3.118-4.58-4.196-7.448h-3.144v-3.228h8.253V13.5Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- EN -->
|
||||||
|
<button
|
||||||
|
class="carrier-toggle lang-option"
|
||||||
|
:style="{ opacity: selectedLang === 'en' ? 1 : 0.8 }"
|
||||||
|
@click="setLanguage('en')"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#b)">
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M24 0C10.75 0 0 10.75 0 24s10.75 24 24 24 24-10.75 24-24S37.25 0 24 0Zm-2.43 33.79H8.41V14.15h13.14v3.28h-9.1v4.68h7.77v3.17h-7.77v5.26h9.12v3.25Zm17.97 0h-4.05l-7.88-12.92v12.92h-4.05V14.15h4.05L35.5 27.1V14.15h4.03v19.64h.01Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="b"><path fill="#fff" d="M0 0h48v48H0z" /></clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h3>{{ selectedSightName }}</h3>
|
|
||||||
<p>{{ selectedSightText }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<div v-if="showGovernorAppeal" class="governor-appeal sight-preview-panel">
|
||||||
|
<img :src="governorAppealImage" v-if="governorAppealImage" />
|
||||||
|
<h3>{{ governorAppealTitle }}</h3>
|
||||||
|
<p>{{ governorAppealText }}</p>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<transition name="slide-fade" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="showSightPreview"
|
||||||
|
:key="selectedSightId"
|
||||||
|
class="sight-preview-panel"
|
||||||
|
>
|
||||||
|
<div class="sight-preview-wrapper" style="position: relative">
|
||||||
|
<img :src="selectedSightImage" v-if="selectedSightImage" />
|
||||||
|
<img
|
||||||
|
v-if="selectedSightWatermarkLU"
|
||||||
|
:src="selectedSightWatermarkLU"
|
||||||
|
class="watermark watermark-lu"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-if="selectedSightWatermarkRD"
|
||||||
|
:src="selectedSightWatermarkRD"
|
||||||
|
class="watermark watermark-rd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3>{{ selectedSightName }}</h3>
|
||||||
|
<p>{{ selectedSightText }}</p>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -764,8 +893,47 @@ export default {
|
|||||||
selectedSightWatermarkLU: "",
|
selectedSightWatermarkLU: "",
|
||||||
selectedSightWatermarkRD: "",
|
selectedSightWatermarkRD: "",
|
||||||
inactivityTimer: null,
|
inactivityTimer: null,
|
||||||
|
stopsInterval: null,
|
||||||
|
sightTransfers: {},
|
||||||
|
showSightTransfers: false,
|
||||||
|
transferIcons: {
|
||||||
|
tram: require("@/icons/tram.svg"),
|
||||||
|
trolleybus: require("@/icons/trolleybus.svg"),
|
||||||
|
bus: require("@/icons/bus.svg"),
|
||||||
|
train: require("@/icons/train.svg"),
|
||||||
|
metro_red: require("@/icons/metro_red.svg"),
|
||||||
|
metro_green: require("@/icons/metro_green.svg"),
|
||||||
|
metro_blue: require("@/icons/metro_blue.svg"),
|
||||||
|
metro_purple: require("@/icons/metro_purple.svg"),
|
||||||
|
metro_orange: require("@/icons/metro_orange.svg"),
|
||||||
|
},
|
||||||
|
selectedLang: localStorage.getItem("selectedLang") || "ru",
|
||||||
|
showLangToggle: true,
|
||||||
|
showLanguageOptions: false,
|
||||||
|
languageOptionsTimer: null,
|
||||||
|
langRevertTimer: null,
|
||||||
|
translations: {
|
||||||
|
sights: { ru: "Достопримечательности", en: "Landmarks", zh: "景点" },
|
||||||
|
stops: { ru: "Остановки", en: "Stops", zh: "站点" },
|
||||||
|
appeal: { ru: "Обращение", en: "Appeal", zh: "致辞" },
|
||||||
|
govt_support: {
|
||||||
|
ru: "При поддержке Правительства <br /> Санкт-Петербурга",
|
||||||
|
en: "Supported by the Government <br /> of St Petersburg",
|
||||||
|
zh: "圣彼得堡政府支持",
|
||||||
|
},
|
||||||
|
hashtag: { ru: "#ВсемПоПути", en: "#OnOurWay", zh: "#同路人" },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
filteredSightTransfers() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.sightTransfers || {}).filter(
|
||||||
|
([, v]) => v && v.toString().trim() !== ""
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getCookie(name) {
|
getCookie(name) {
|
||||||
const matches = document.cookie.match(
|
const matches = document.cookie.match(
|
||||||
@ -777,16 +945,47 @@ export default {
|
|||||||
);
|
);
|
||||||
return matches ? decodeURIComponent(matches[1]) : undefined;
|
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||||
},
|
},
|
||||||
|
t(key) {
|
||||||
|
const dict = this.translations[key] || {};
|
||||||
|
return dict[this.selectedLang] || dict.ru || key;
|
||||||
|
},
|
||||||
|
async loadGovernorAppeal(appealId) {
|
||||||
|
try {
|
||||||
|
const articleRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/article/${appealId}`)
|
||||||
|
);
|
||||||
|
const articleData = await articleRes.json();
|
||||||
|
this.governorAppealTitle = articleData.heading;
|
||||||
|
this.governorAppealText = articleData.body;
|
||||||
|
|
||||||
|
const mediaRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/article/${appealId}/media`)
|
||||||
|
);
|
||||||
|
const mediaData = await mediaRes.json();
|
||||||
|
this.governorAppealImage = mediaData.length
|
||||||
|
? (
|
||||||
|
await fetch(
|
||||||
|
this.addLangParam(
|
||||||
|
`${API_URL}/media/${mediaData[0].id}/download`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).url
|
||||||
|
: "";
|
||||||
|
|
||||||
|
this.hasGovernorAppeal = true; // кнопка появится (v-if)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка при получении обращения губернатора:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
async fetchStops() {
|
async fetchStops() {
|
||||||
try {
|
try {
|
||||||
const geoResponse = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
const geoResponse = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
||||||
const geoData = await geoResponse.json();
|
const geoData = await geoResponse.json();
|
||||||
const routeDetailsRes = await fetch(
|
const routeDetailsRes = await fetch(
|
||||||
`${API_URL}/route/${geoData.routeId}`
|
this.addLangParam(`${API_URL}/route/${geoData.routeId}`)
|
||||||
);
|
);
|
||||||
const routeDetails = await routeDetailsRes.json();
|
const routeDetails = await routeDetailsRes.json();
|
||||||
const appealId = routeDetails.governor_appeal;
|
const appealId = routeDetails.governor_appeal;
|
||||||
this.governorAppealId = appealId;
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Получено значение governor_appeal:",
|
"Получено значение governor_appeal:",
|
||||||
@ -796,33 +995,9 @@ export default {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (appealId && appealId !== 0) {
|
if (appealId && appealId !== 0) {
|
||||||
try {
|
if (this.governorAppealId !== appealId) {
|
||||||
const articleRes = await fetch(`${API_URL}/article/${appealId}`);
|
this.governorAppealId = appealId;
|
||||||
const articleData = await articleRes.json();
|
await this.loadGovernorAppeal(appealId);
|
||||||
this.governorAppealTitle = articleData.heading;
|
|
||||||
this.governorAppealText = articleData.body;
|
|
||||||
|
|
||||||
const mediaRes = await fetch(
|
|
||||||
`${API_URL}/article/${appealId}/media`
|
|
||||||
);
|
|
||||||
const mediaData = await mediaRes.json();
|
|
||||||
if (mediaData.length) {
|
|
||||||
const imageRes = await fetch(
|
|
||||||
`${API_URL}/media/${mediaData[0].id}/download`
|
|
||||||
);
|
|
||||||
this.governorAppealImage = await imageRes.url;
|
|
||||||
} else {
|
|
||||||
this.governorAppealImage = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Обращение губернатора загружено:", {
|
|
||||||
title: this.governorAppealTitle,
|
|
||||||
text: this.governorAppealText,
|
|
||||||
image: this.governorAppealImage,
|
|
||||||
});
|
|
||||||
this.hasGovernorAppeal = true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Ошибка при получении обращения губернатора:", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -830,7 +1005,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(
|
||||||
`${API_URL}/route/${this.routeId}/station`
|
this.addLangParam(`${API_URL}/route/${this.routeId}/station`)
|
||||||
);
|
);
|
||||||
const stopsData = await stopsResponse.json();
|
const stopsData = await stopsResponse.json();
|
||||||
this.stations = stopsData;
|
this.stations = stopsData;
|
||||||
@ -840,20 +1015,94 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchSights() {
|
async fetchSights() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/route/${this.routeId}/sight`);
|
const response = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/route/${this.routeId}/sight`)
|
||||||
|
);
|
||||||
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(`${API_URL}/sight/${sight.id}`);
|
const detailRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/sight/${sight.id}`)
|
||||||
|
);
|
||||||
const detail = await detailRes.json();
|
const detail = await detailRes.json();
|
||||||
return { id: sight.id, name: detail.name };
|
return { id: sight.id, name: detail.name, transfers: {} };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.sights = detailedSights;
|
this.sights = detailedSights;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка при получении достопримечательностей:", error);
|
console.error("Ошибка при получении достопримечательностей:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hasTransfers(transfers) {
|
||||||
|
if (!transfers) return false;
|
||||||
|
return Object.values(transfers).some(
|
||||||
|
(v) => v && v.toString().trim() !== ""
|
||||||
|
);
|
||||||
|
},
|
||||||
|
filteredEntries(transfers) {
|
||||||
|
if (!transfers) return [];
|
||||||
|
return Object.entries(transfers)
|
||||||
|
.filter(([, v]) => v && v.toString().trim() !== "")
|
||||||
|
.map(([key, val]) => ({ key, val }));
|
||||||
|
},
|
||||||
|
startStopsPolling() {
|
||||||
|
if (this.stopsInterval) return;
|
||||||
|
this.stopsInterval = setInterval(async () => {
|
||||||
|
if (!this.stations.length) {
|
||||||
|
await this.fetchStops();
|
||||||
|
} else {
|
||||||
|
clearInterval(this.stopsInterval);
|
||||||
|
this.stopsInterval = null;
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
async findTransfersForSight(sightId) {
|
||||||
|
this.showSightTransfers = false;
|
||||||
|
this.sightTransfers = {};
|
||||||
|
try {
|
||||||
|
const stationsRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/route/${this.routeId}/station`)
|
||||||
|
);
|
||||||
|
const stationsData = await stationsRes.json();
|
||||||
|
|
||||||
|
for (const station of stationsData) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/station/${station.id}/sight`)
|
||||||
|
);
|
||||||
|
const stationSights = await res.json();
|
||||||
|
const hasSight = stationSights.some((s) => s.id === sightId);
|
||||||
|
if (hasSight) {
|
||||||
|
this.sightTransfers = station.transfers || {};
|
||||||
|
// update transfers on the selected sight object
|
||||||
|
const idx = this.sights.findIndex((s) => s.id === sightId);
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.sights[idx].transfers = this.sightTransfers;
|
||||||
|
}
|
||||||
|
this.showSightTransfers = true;
|
||||||
|
console.log(
|
||||||
|
`Пересадки на станции ${station.name}:`,
|
||||||
|
station.transfers
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (innerErr) {
|
||||||
|
console.error(
|
||||||
|
`Ошибка при получении достопримечательностей станции ${station.id}:`,
|
||||||
|
innerErr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
"Ошибка при поиске пересадок для достопримечательности:",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
async toggleSights() {
|
async toggleSights() {
|
||||||
this.showGovernorAppeal = false;
|
this.showGovernorAppeal = false;
|
||||||
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
|
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
|
||||||
@ -865,6 +1114,7 @@ export default {
|
|||||||
this.showSightsList = false;
|
this.showSightsList = false;
|
||||||
this.showSightPreview = false;
|
this.showSightPreview = false;
|
||||||
this.selectedSightId = null;
|
this.selectedSightId = null;
|
||||||
|
this.showSightTransfers = false;
|
||||||
} else {
|
} else {
|
||||||
this.showSightsList = true;
|
this.showSightsList = true;
|
||||||
}
|
}
|
||||||
@ -872,34 +1122,45 @@ export default {
|
|||||||
async selectSight(sightId) {
|
async selectSight(sightId) {
|
||||||
this.selectedSightId = sightId;
|
this.selectedSightId = sightId;
|
||||||
try {
|
try {
|
||||||
const sightRes = await fetch(`${API_URL}/sight/${sightId}`);
|
const sightRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/sight/${sightId}`)
|
||||||
|
);
|
||||||
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(`${API_URL}/article/${articleId}`);
|
const articleRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/article/${articleId}`)
|
||||||
|
);
|
||||||
const articleData = await articleRes.json();
|
const articleData = await articleRes.json();
|
||||||
this.selectedSightText = articleData.body;
|
this.selectedSightText = articleData.body;
|
||||||
|
|
||||||
const mediaRes = await fetch(`${API_URL}/article/${articleId}/media`);
|
const mediaRes = await fetch(
|
||||||
|
this.addLangParam(`${API_URL}/article/${articleId}/media`)
|
||||||
|
);
|
||||||
const mediaData = await mediaRes.json();
|
const mediaData = await mediaRes.json();
|
||||||
if (mediaData.length) {
|
if (mediaData.length) {
|
||||||
const imageRes = await fetch(
|
const imageRes = await fetch(
|
||||||
`${API_URL}/media/${mediaData[0].id}/download`
|
this.addLangParam(`${API_URL}/media/${mediaData[0].id}/download`)
|
||||||
);
|
);
|
||||||
this.selectedSightImage = await imageRes.url;
|
this.selectedSightImage = await imageRes.url;
|
||||||
this.selectedSightWatermarkLU = sightData.watermark_lu
|
this.selectedSightWatermarkLU = sightData.watermark_lu
|
||||||
? `${API_URL}/media/${sightData.watermark_lu}/download`
|
? this.addLangParam(
|
||||||
|
`${API_URL}/media/${sightData.watermark_lu}/download`
|
||||||
|
)
|
||||||
: "";
|
: "";
|
||||||
this.selectedSightWatermarkRD = sightData.watermark_rd
|
this.selectedSightWatermarkRD = sightData.watermark_rd
|
||||||
? `${API_URL}/media/${sightData.watermark_rd}/download`
|
? this.addLangParam(
|
||||||
|
`${API_URL}/media/${sightData.watermark_rd}/download`
|
||||||
|
)
|
||||||
: "";
|
: "";
|
||||||
} else {
|
} else {
|
||||||
this.selectedSightImage = "";
|
this.selectedSightImage = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showSightPreview = true;
|
this.showSightPreview = true;
|
||||||
|
await this.findTransfersForSight(sightId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка при выборе достопримечательности:", err);
|
console.error("Ошибка при выборе достопримечательности:", err);
|
||||||
}
|
}
|
||||||
@ -919,6 +1180,7 @@ export default {
|
|||||||
this.showSightsList = false;
|
this.showSightsList = false;
|
||||||
this.showSightPreview = false;
|
this.showSightPreview = false;
|
||||||
this.showGovernorAppeal = false;
|
this.showGovernorAppeal = false;
|
||||||
|
this.showSightTransfers = false;
|
||||||
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
|
this.$emit("president-appeal-toggle", this.showGovernorAppeal);
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
@ -962,6 +1224,61 @@ export default {
|
|||||||
},
|
},
|
||||||
handleUserActivity() {
|
handleUserActivity() {
|
||||||
this.resetInactivityTimer();
|
this.resetInactivityTimer();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
|
},
|
||||||
|
toggleLanguageOptions() {
|
||||||
|
this.showLangToggle = false;
|
||||||
|
this.showLanguageOptions = true;
|
||||||
|
this.startLanguageOptionsTimer();
|
||||||
|
},
|
||||||
|
startLanguageOptionsTimer() {
|
||||||
|
clearTimeout(this.languageOptionsTimer);
|
||||||
|
this.languageOptionsTimer = setTimeout(this.hideLanguageOptions, 10000);
|
||||||
|
},
|
||||||
|
hideLanguageOptions() {
|
||||||
|
this.showLanguageOptions = false;
|
||||||
|
this.showLangToggle = true;
|
||||||
|
},
|
||||||
|
setLanguage(lang) {
|
||||||
|
if (this.selectedLang === lang) {
|
||||||
|
this.hideLanguageOptions();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedLang = lang;
|
||||||
|
localStorage.setItem("selectedLang", lang);
|
||||||
|
|
||||||
|
this.hideLanguageOptions();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
|
|
||||||
|
// заново тянем данные в нужном языке
|
||||||
|
this.fetchStops(); // остановки + обращение губернатора
|
||||||
|
this.fetchSights(); // список достопримечательностей
|
||||||
|
if (this.selectedSightId) {
|
||||||
|
// если была открыта карточка – обновляем и её
|
||||||
|
this.selectSight(this.selectedSightId);
|
||||||
|
}
|
||||||
|
// подтягиваем заголовок обращения губернатора в новом языке
|
||||||
|
if (this.governorAppealId) {
|
||||||
|
this.loadGovernorAppeal(this.governorAppealId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addLangParam(url) {
|
||||||
|
// add ?lang=XX to every API_URL request when language ≠ ru
|
||||||
|
if (this.selectedLang !== "ru" && url.startsWith(API_URL)) {
|
||||||
|
return (
|
||||||
|
url + (url.includes("?") ? "&" : "?") + "lang=" + this.selectedLang
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
resetLangRevertTimer() {
|
||||||
|
clearTimeout(this.langRevertTimer);
|
||||||
|
this.langRevertTimer = setTimeout(() => {
|
||||||
|
if (this.selectedLang !== "ru") {
|
||||||
|
this.setLanguage("ru");
|
||||||
|
}
|
||||||
|
}, 300000);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -970,6 +1287,7 @@ export default {
|
|||||||
document.addEventListener(evt, this.handleUserActivity)
|
document.addEventListener(evt, this.handleUserActivity)
|
||||||
);
|
);
|
||||||
this.resetInactivityTimer();
|
this.resetInactivityTimer();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
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");
|
||||||
@ -978,6 +1296,7 @@ export default {
|
|||||||
weatherInfo?.classList.add("shifted-left");
|
weatherInfo?.classList.add("shifted-left");
|
||||||
this.fetchStops();
|
this.fetchStops();
|
||||||
this.fetchSights();
|
this.fetchSights();
|
||||||
|
this.startStopsPolling();
|
||||||
console.log("hasGovernorAppeal в mounted:", this.hasGovernorAppeal);
|
console.log("hasGovernorAppeal в mounted:", this.hasGovernorAppeal);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@ -986,6 +1305,76 @@ export default {
|
|||||||
);
|
);
|
||||||
clearTimeout(this.inactivityTimer);
|
clearTimeout(this.inactivityTimer);
|
||||||
document.removeEventListener("click", this.handleClickOutside);
|
document.removeEventListener("click", this.handleClickOutside);
|
||||||
|
if (this.stopsInterval) {
|
||||||
|
clearInterval(this.stopsInterval);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sight-transfers {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.transfer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.transfer-key {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.transfer-value {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.sight-transfer-list {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin: 10px 20px 0 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.transfer-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-text {
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
/* slide‑fade transition */
|
||||||
|
.slide-fade-enter-active,
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
/* simple fade */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.language-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
.lang-option svg {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,5 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- map container -->
|
||||||
<div id="map" style="height: 100vh; background-color: transparent"></div>
|
<div id="map" style="height: 100vh; background-color: transparent"></div>
|
||||||
|
|
||||||
|
<!-- fullscreen video shown after 10 s of inactivity -->
|
||||||
|
<div
|
||||||
|
id="video-overlay"
|
||||||
|
v-show="videoOverlayVisible"
|
||||||
|
style="
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 10000;
|
||||||
|
background: #000;
|
||||||
|
pointer-events: none;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -18,10 +35,10 @@ import { API_URL, GEO_URL } from "../config";
|
|||||||
const arrowTransforms = {
|
const arrowTransforms = {
|
||||||
right: "",
|
right: "",
|
||||||
left: "",
|
left: "",
|
||||||
"top-right": "",
|
"top-right": "translate(-25%, -50%)",
|
||||||
"bottom-right": "",
|
"bottom-right": "translate(-25%, 50%)",
|
||||||
"top-left": "",
|
"top-left": "translate(-100%, -50%)",
|
||||||
"bottom-left": "",
|
"bottom-left": "translate(-100%, 50%)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -36,21 +53,37 @@ export default {
|
|||||||
fullPolyline: null,
|
fullPolyline: null,
|
||||||
passedPolyline: null,
|
passedPolyline: null,
|
||||||
tramDirection: "right",
|
tramDirection: "right",
|
||||||
|
routeId: null,
|
||||||
|
segmentLengths: [],
|
||||||
|
cumulativeDistances: [],
|
||||||
|
totalLength: 0,
|
||||||
|
sightPollTimer: null,
|
||||||
|
sightsData: [],
|
||||||
|
nearestSightId: null,
|
||||||
|
lastActivityTime: Date.now(),
|
||||||
|
videoOverlayVisible: false,
|
||||||
|
currentVideoSrc: null,
|
||||||
|
lastVideoPreviewId: null,
|
||||||
|
inactivityInterval: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
this.initializeMap();
|
this.initializeMap();
|
||||||
this.fetchRoute();
|
await this.fetchContext(); // obtain current routeId
|
||||||
this.fetchSights();
|
|
||||||
this.startTracking();
|
this.startTracking();
|
||||||
|
this.setupActivityListeners();
|
||||||
|
this.startInactivityCheck();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchSights() {
|
async fetchSights() {
|
||||||
|
if (!this.routeId) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/route/1/sight`);
|
const response = await fetch(`${API_URL}/route/${this.routeId}/sight`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log("Данные достопримечательностей:", data);
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
|
this.sightsData = data;
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
|
|
||||||
data.forEach((sight) => {
|
data.forEach((sight) => {
|
||||||
@ -99,14 +132,112 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка при получении достопримечательностей:", error);
|
console.error("Ошибка при получении достопримечательностей:", error);
|
||||||
}
|
}
|
||||||
|
// Stop polling once at least one sight marker exists
|
||||||
|
if (this.sightMarkers.length > 0 && this.sightPollTimer) {
|
||||||
|
clearInterval(this.sightPollTimer);
|
||||||
|
this.sightPollTimer = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Poll fetchSights every 3 s until at least one sight is on the map ──
|
||||||
|
startSightPolling() {
|
||||||
|
if (this.sightPollTimer) return; // already polling
|
||||||
|
this.sightPollTimer = setInterval(() => {
|
||||||
|
this.fetchSights();
|
||||||
|
if (this.sightMarkers.length > 0) {
|
||||||
|
clearInterval(this.sightPollTimer);
|
||||||
|
this.sightPollTimer = null;
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopSightPolling() {
|
||||||
|
if (this.sightPollTimer) {
|
||||||
|
clearInterval(this.sightPollTimer);
|
||||||
|
this.sightPollTimer = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ───────────────── user‑inactivity & video overlay helpers ──────────────────
|
||||||
|
setupActivityListeners() {
|
||||||
|
["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach(
|
||||||
|
(evt) => window.addEventListener(evt, this.resetInactivity)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
resetInactivity() {
|
||||||
|
this.lastActivityTime = Date.now();
|
||||||
|
if (this.videoOverlayVisible) this.hideVideoOverlay();
|
||||||
|
},
|
||||||
|
startInactivityCheck() {
|
||||||
|
if (this.inactivityInterval) return;
|
||||||
|
this.inactivityInterval = setInterval(() => {
|
||||||
|
if (Date.now() - this.lastActivityTime >= 300_000) {
|
||||||
|
this.showNearestSightVideo();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
stopInactivityCheck() {
|
||||||
|
if (this.inactivityInterval) {
|
||||||
|
clearInterval(this.inactivityInterval);
|
||||||
|
this.inactivityInterval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showNearestSightVideo() {
|
||||||
|
if (!this.sightsData.length) return;
|
||||||
|
|
||||||
|
// choose closest sight that has a preview
|
||||||
|
let sight = this.sightsData.find((s) => s.id === this.nearestSightId);
|
||||||
|
if (!sight || !sight.video_preview) {
|
||||||
|
sight = this.sightsData.find((s) => s.video_preview);
|
||||||
|
}
|
||||||
|
if (!sight || !sight.video_preview) return;
|
||||||
|
|
||||||
|
if (this.lastVideoPreviewId === sight.video_preview) {
|
||||||
|
this.videoOverlayVisible = true;
|
||||||
|
return; // already showing it
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = `${API_URL}/media/${sight.video_preview}/download`;
|
||||||
|
this.replaceVideo(src, sight.video_preview);
|
||||||
|
},
|
||||||
|
replaceVideo(src, previewId) {
|
||||||
|
const overlay = document.getElementById("video-overlay");
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
const vid = document.createElement("video");
|
||||||
|
vid.src = src;
|
||||||
|
vid.autoplay = true;
|
||||||
|
vid.muted = true;
|
||||||
|
vid.loop = true;
|
||||||
|
vid.playsInline = true;
|
||||||
|
vid.style.cssText = "width:100%;height:100%;object-fit:cover;";
|
||||||
|
|
||||||
|
vid.addEventListener("canplay", () => {
|
||||||
|
[...overlay.children].forEach((child) => {
|
||||||
|
if (child !== vid) overlay.removeChild(child);
|
||||||
|
});
|
||||||
|
this.videoOverlayVisible = true;
|
||||||
|
this.lastVideoPreviewId = previewId;
|
||||||
|
});
|
||||||
|
|
||||||
|
overlay.appendChild(vid);
|
||||||
|
},
|
||||||
|
hideVideoOverlay() {
|
||||||
|
this.videoOverlayVisible = false;
|
||||||
|
const overlay = document.getElementById("video-overlay");
|
||||||
|
if (overlay) overlay.innerHTML = "";
|
||||||
|
this.lastVideoPreviewId = null;
|
||||||
|
},
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
initializeMap() {
|
initializeMap() {
|
||||||
this.map = L.map("map", {
|
this.map = L.map("map", {
|
||||||
zoomControl: false,
|
zoomControl: false,
|
||||||
attributionControl: false,
|
attributionControl: false,
|
||||||
minZoom: 12,
|
maxZoom: 14, // default max zoom
|
||||||
maxZoom: 14,
|
minZoom: 12, // default min zoom
|
||||||
|
zoomSnap: 0, // allow fractional zoom levels
|
||||||
|
zoomDelta: 0.5, // smoother wheel steps
|
||||||
});
|
});
|
||||||
this.map.whenReady(() => {
|
this.map.whenReady(() => {
|
||||||
const mapPane = this.map.getPane("mapPane") || this.map.getContainer();
|
const mapPane = this.map.getPane("mapPane") || this.map.getContainer();
|
||||||
@ -120,11 +251,27 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetchRoute() {
|
async fetchRoute() {
|
||||||
fetch(`${API_URL}/route/1`)
|
if (!this.routeId) return;
|
||||||
|
fetch(`${API_URL}/route/${this.routeId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.path && Array.isArray(data.path)) {
|
if (data.path && Array.isArray(data.path)) {
|
||||||
this.routeLatlngs = data.path.map((coord) => [coord[0], coord[1]]);
|
this.routeLatlngs = data.path.map((coord) => [coord[0], coord[1]]);
|
||||||
|
// ── Pre-compute cumulative distances for distance-based progress ──
|
||||||
|
this.segmentLengths = [];
|
||||||
|
this.cumulativeDistances = [0];
|
||||||
|
for (let i = 1; i < this.routeLatlngs.length; i++) {
|
||||||
|
const prev = L.latLng(this.routeLatlngs[i - 1]);
|
||||||
|
const curr = L.latLng(this.routeLatlngs[i]);
|
||||||
|
const segLen = prev.distanceTo(curr); // metres
|
||||||
|
this.segmentLengths.push(segLen);
|
||||||
|
this.cumulativeDistances.push(
|
||||||
|
this.cumulativeDistances[i - 1] + segLen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.totalLength =
|
||||||
|
this.cumulativeDistances[this.cumulativeDistances.length - 1] ||
|
||||||
|
0;
|
||||||
if (
|
if (
|
||||||
typeof data.scale_min === "number" &&
|
typeof data.scale_min === "number" &&
|
||||||
typeof data.scale_max === "number"
|
typeof data.scale_max === "number"
|
||||||
@ -160,11 +307,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const polyline = L.polyline(this.routeLatlngs, {
|
const routeBounds = L.latLngBounds(this.routeLatlngs);
|
||||||
color: "red",
|
|
||||||
weight: 7,
|
|
||||||
pane: "routePane",
|
|
||||||
}).addTo(this.map);
|
|
||||||
if (data.center_latitude && data.center_longitude) {
|
if (data.center_latitude && data.center_longitude) {
|
||||||
console.log("Установка центра карты:", {
|
console.log("Установка центра карты:", {
|
||||||
latitude: data.center_latitude,
|
latitude: data.center_latitude,
|
||||||
@ -179,7 +322,7 @@ export default {
|
|||||||
this.map.getCenter()
|
this.map.getCenter()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.map.fitBounds(polyline.getBounds());
|
this.map.fitBounds(routeBounds);
|
||||||
}
|
}
|
||||||
this.fetchStations();
|
this.fetchStations();
|
||||||
}
|
}
|
||||||
@ -190,6 +333,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetchStations() {
|
async fetchStations() {
|
||||||
|
if (!this.routeId) return;
|
||||||
this.stationMarkers.forEach(({ marker }) => {
|
this.stationMarkers.forEach(({ marker }) => {
|
||||||
if (this.map.hasLayer(marker)) {
|
if (this.map.hasLayer(marker)) {
|
||||||
this.map.removeLayer(marker);
|
this.map.removeLayer(marker);
|
||||||
@ -199,8 +343,12 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [ruStations, enStations] = await Promise.all([
|
const [ruStations, enStations] = await Promise.all([
|
||||||
fetch(`${API_URL}/route/1/station`).then((r) => r.json()),
|
fetch(`${API_URL}/route/${this.routeId}/station`).then((r) =>
|
||||||
fetch(`${API_URL}/route/1/station?lang=en`).then((r) => r.json()),
|
r.json()
|
||||||
|
),
|
||||||
|
fetch(`${API_URL}/route/${this.routeId}/station?lang=en`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const enById = {};
|
const enById = {};
|
||||||
@ -326,6 +474,7 @@ export default {
|
|||||||
"top-left": tramTopLeft,
|
"top-left": tramTopLeft,
|
||||||
"bottom-left": tramBottomLeft,
|
"bottom-left": tramBottomLeft,
|
||||||
};
|
};
|
||||||
|
|
||||||
const svgSrc = svgMap[direction] || tramRight;
|
const svgSrc = svgMap[direction] || tramRight;
|
||||||
|
|
||||||
const arrowTransform = arrowTransforms[direction] || "";
|
const arrowTransform = arrowTransforms[direction] || "";
|
||||||
@ -355,7 +504,7 @@ export default {
|
|||||||
background: yellow;
|
background: yellow;
|
||||||
border: 5px solid black;
|
border: 5px solid black;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translate(5px, -50%);
|
||||||
z-index: 2;"></div>
|
z-index: 2;"></div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@ -368,43 +517,100 @@ export default {
|
|||||||
setInterval(this.updateTramPosition, 500);
|
setInterval(this.updateTramPosition, 500);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchContext() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
||||||
|
const data = await response.json();
|
||||||
|
const ctxRouteId =
|
||||||
|
(data && data.routeId) ||
|
||||||
|
(data.routeProgress && data.routeProgress.routeId) ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (ctxRouteId && ctxRouteId !== this.routeId) {
|
||||||
|
this.routeId = ctxRouteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctxNearest =
|
||||||
|
data.nearestSightId ||
|
||||||
|
(data.routeProgress && data.routeProgress.nearestSightId);
|
||||||
|
if (ctxNearest) this.nearestSightId = ctxNearest;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка при получении контекста геолокации:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async updateTramPosition() {
|
async updateTramPosition() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
const response = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (
|
||||||
|
data.routeId &&
|
||||||
|
data.routeId !== 0 &&
|
||||||
|
data.routeId !== this.routeId
|
||||||
|
) {
|
||||||
|
this.routeId = data.routeId;
|
||||||
|
}
|
||||||
// console.log("Текущие координаты трамвая:", data.currentCoordinates);
|
// console.log("Текущие координаты трамвая:", data.currentCoordinates);
|
||||||
|
|
||||||
|
const ctxNearest =
|
||||||
|
data.nearestSightId ||
|
||||||
|
(data.routeProgress && data.routeProgress.nearestSightId);
|
||||||
|
if (ctxNearest) this.nearestSightId = ctxNearest;
|
||||||
|
|
||||||
const { percentageCompleted } = data.routeProgress;
|
const { percentageCompleted } = data.routeProgress;
|
||||||
|
|
||||||
if (this.routeLatlngs.length === 0) return;
|
if (this.totalLength === 0) return;
|
||||||
|
|
||||||
const progressIndex = Math.min(
|
// Дистанция, пройденная трамваем (м)
|
||||||
Math.floor(percentageCompleted * this.routeLatlngs.length),
|
const targetDistance = percentageCompleted * this.totalLength;
|
||||||
this.routeLatlngs.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const tramLatLng = this.routeLatlngs[progressIndex];
|
// Определяем сегмент маршрута, в котором находится трамвай
|
||||||
|
let segIdx = 0;
|
||||||
|
while (
|
||||||
|
segIdx < this.cumulativeDistances.length - 1 &&
|
||||||
|
this.cumulativeDistances[segIdx + 1] < targetDistance
|
||||||
|
) {
|
||||||
|
segIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем старые линии, если есть
|
const segStart = this.routeLatlngs[segIdx];
|
||||||
if (this.passedPolyline) this.map.removeLayer(this.passedPolyline);
|
const segEnd =
|
||||||
if (this.fullPolyline) this.map.removeLayer(this.fullPolyline);
|
this.routeLatlngs[Math.min(segIdx + 1, this.routeLatlngs.length - 1)];
|
||||||
|
const segStartDist = this.cumulativeDistances[segIdx];
|
||||||
|
const segLen = this.cumulativeDistances[segIdx + 1] - segStartDist || 1;
|
||||||
|
|
||||||
// Красная линия — уже проехали
|
// Интерполируем точку на сегменте
|
||||||
this.passedPolyline = L.polyline(
|
const t = segLen === 0 ? 0 : (targetDistance - segStartDist) / segLen;
|
||||||
this.routeLatlngs.slice(0, progressIndex + 1),
|
const tramLatLng = [
|
||||||
{
|
segStart[0] + (segEnd[0] - segStart[0]) * t,
|
||||||
|
segStart[1] + (segEnd[1] - segStart[1]) * t,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Формируем координаты пройденной и оставшейся части
|
||||||
|
const passedCoords = this.routeLatlngs.slice(0, segIdx + 1);
|
||||||
|
passedCoords.push(tramLatLng);
|
||||||
|
const fullCoords = [tramLatLng, ...this.routeLatlngs.slice(segIdx + 1)];
|
||||||
|
|
||||||
|
// ── Update / create progress polylines without flicker ──
|
||||||
|
if (!this.passedPolyline) {
|
||||||
|
this.passedPolyline = L.polyline(passedCoords, {
|
||||||
color: "red",
|
color: "red",
|
||||||
weight: 7,
|
weight: 7,
|
||||||
pane: "routePane",
|
pane: "routePane",
|
||||||
}
|
}).addTo(this.map);
|
||||||
).addTo(this.map);
|
} else {
|
||||||
|
this.passedPolyline.setLatLngs(passedCoords);
|
||||||
|
}
|
||||||
|
|
||||||
// Белая линия — ещё впереди
|
if (!this.fullPolyline) {
|
||||||
this.fullPolyline = L.polyline(this.routeLatlngs.slice(progressIndex), {
|
this.fullPolyline = L.polyline(fullCoords, {
|
||||||
color: "white",
|
color: "white",
|
||||||
weight: 7,
|
weight: 7,
|
||||||
pane: "routePane",
|
pane: "routePane",
|
||||||
}).addTo(this.map);
|
}).addTo(this.map);
|
||||||
|
} else {
|
||||||
|
this.fullPolyline.setLatLngs(fullCoords);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.tramMarker && !this.map.hasLayer(this.tramMarker)) {
|
if (this.tramMarker && !this.map.hasLayer(this.tramMarker)) {
|
||||||
this.tramMarker.addTo(this.map);
|
this.tramMarker.addTo(this.map);
|
||||||
@ -425,8 +631,9 @@ export default {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const isPassed = stationIndex < progressIndex;
|
const stationPassed =
|
||||||
const color = isPassed ? "red" : "white";
|
this.cumulativeDistances[stationIndex] <= targetDistance;
|
||||||
|
const color = stationPassed ? "red" : "white";
|
||||||
|
|
||||||
marker.setStyle({
|
marker.setStyle({
|
||||||
color: "black",
|
color: "black",
|
||||||
@ -461,5 +668,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
// When routeId becomes available or changes, (re)load dependent data
|
||||||
|
routeId(newVal, oldVal) {
|
||||||
|
if (newVal && newVal !== oldVal) {
|
||||||
|
this.fetchRoute();
|
||||||
|
this.fetchSights();
|
||||||
|
this.fetchStations();
|
||||||
|
this.startSightPolling();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.sightPollTimer) {
|
||||||
|
clearInterval(this.sightPollTimer);
|
||||||
|
this.sightPollTimer = null;
|
||||||
|
}
|
||||||
|
this.stopInactivityCheck();
|
||||||
|
["mousemove", "mousedown", "keydown", "touchstart", "scroll"].forEach(
|
||||||
|
(evt) => window.removeEventListener(evt, this.resetInactivity)
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -85,6 +85,8 @@ export default {
|
|||||||
isStartScrolling: false,
|
isStartScrolling: false,
|
||||||
isEndScrolling: false,
|
isEndScrolling: false,
|
||||||
isEnScrolling: false,
|
isEnScrolling: false,
|
||||||
|
pollId: null,
|
||||||
|
hasLoadedStops: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -101,41 +103,69 @@ export default {
|
|||||||
this.$nextTick(this.checkScroll);
|
this.$nextTick(this.checkScroll);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
mounted() {
|
||||||
const contextRes = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
this.startPollingContext();
|
||||||
const data = await contextRes.json();
|
|
||||||
this.routeNumber = data.routeNumber;
|
|
||||||
|
|
||||||
const startStopId = data.startStopId;
|
|
||||||
const endStopId = data.endStopId;
|
|
||||||
|
|
||||||
const [startStopRes, endStopRes] = await Promise.all([
|
|
||||||
fetch(`${API_URL}/station/${startStopId}`),
|
|
||||||
fetch(`${API_URL}/station/${endStopId}`),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const startStopData = await startStopRes.json();
|
|
||||||
const endStopData = await endStopRes.json();
|
|
||||||
|
|
||||||
this.startStopName = startStopData.name;
|
|
||||||
this.endStopName = endStopData.name;
|
|
||||||
|
|
||||||
const [startStopEnRes, endStopEnRes] = await Promise.all([
|
|
||||||
fetch(`${API_URL}/station/${startStopId}?lang=en`),
|
|
||||||
fetch(`${API_URL}/station/${endStopId}?lang=en`),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const startStopEnData = await startStopEnRes.json();
|
|
||||||
const endStopEnData = await endStopEnRes.json();
|
|
||||||
|
|
||||||
this.startStopNameEn = startStopEnData.name;
|
|
||||||
this.endStopNameEn = endStopEnData.name;
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.checkScroll();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
startPollingContext() {
|
||||||
|
// first request immediately, then every 3 s
|
||||||
|
this.fetchContext();
|
||||||
|
this.pollId = setInterval(this.fetchContext, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchContext() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.routeNumber) {
|
||||||
|
this.routeNumber = data.routeNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startStopId, endStopId } = data;
|
||||||
|
|
||||||
|
// when both IDs are available for the first time, load names and stop polling
|
||||||
|
if (startStopId && endStopId && !this.hasLoadedStops) {
|
||||||
|
await this.loadStopNames(startStopId, endStopId);
|
||||||
|
this.hasLoadedStops = true;
|
||||||
|
if (this.pollId) {
|
||||||
|
clearInterval(this.pollId);
|
||||||
|
this.pollId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch geolocation context", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadStopNames(startStopId, endStopId) {
|
||||||
|
try {
|
||||||
|
const [startRuRes, endRuRes] = await Promise.all([
|
||||||
|
fetch(`${API_URL}/station/${startStopId}`),
|
||||||
|
fetch(`${API_URL}/station/${endStopId}`),
|
||||||
|
]);
|
||||||
|
const startRu = await startRuRes.json();
|
||||||
|
const endRu = await endRuRes.json();
|
||||||
|
|
||||||
|
this.startStopName = startRu.name;
|
||||||
|
this.endStopName = endRu.name;
|
||||||
|
|
||||||
|
const [startEnRes, endEnRes] = await Promise.all([
|
||||||
|
fetch(`${API_URL}/station/${startStopId}?lang=en`),
|
||||||
|
fetch(`${API_URL}/station/${endStopId}?lang=en`),
|
||||||
|
]);
|
||||||
|
const startEn = await startEnRes.json();
|
||||||
|
const endEn = await endEnRes.json();
|
||||||
|
|
||||||
|
this.startStopNameEn = startEn.name;
|
||||||
|
this.endStopNameEn = endEn.name;
|
||||||
|
|
||||||
|
this.$nextTick(this.checkScroll);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load station names", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
checkScroll() {
|
checkScroll() {
|
||||||
const threshold = 280;
|
const threshold = 280;
|
||||||
if (this.$refs.startStopRuText) {
|
if (this.$refs.startStopRuText) {
|
||||||
|
@ -1,6 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="stopinfo">
|
<div class="stopinfo">
|
||||||
<div class="bg">
|
<div class="bg">
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<div
|
||||||
|
v-if="cardDetail"
|
||||||
|
class="sight-preview-panel left-panel"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="sight-preview-wrapper">
|
||||||
|
<img v-if="cardDetail.imageUrl" :src="cardDetail.imageUrl" />
|
||||||
|
</div>
|
||||||
|
<h3>{{ cardDetail.name }}</h3>
|
||||||
|
<p>{{ selectedSightArticleBody }}</p>
|
||||||
|
<div class="stoparticles">
|
||||||
|
<span
|
||||||
|
v-for="article in cardDetail.articles"
|
||||||
|
:key="article.id"
|
||||||
|
:class="{ selected: article.id === selectedSightArticleId }"
|
||||||
|
@click.stop="selectSightArticle(article.id)"
|
||||||
|
class="stoparticle-option"
|
||||||
|
>
|
||||||
|
{{ article.heading }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="image-wrapper">
|
<div class="image-wrapper">
|
||||||
<!-- @click="openModal" -->
|
<!-- @click="openModal" -->
|
||||||
@ -116,7 +140,58 @@
|
|||||||
d="M.382 11.643c-.24.227-.382.553-.382.92 0 .737.552 1.29 1.288 1.29.368 0 .708-.128.92-.368L13.052 2.408h-1.515l10.844 11.077c.227.24.566.368.92.368a1.26 1.26 0 0 0 1.289-1.29 1.25 1.25 0 0 0-.383-.92L13.25.425A1.288 1.288 0 0 0 12.302 0a1.32 1.32 0 0 0-.963.425L.382 11.643Z"
|
d="M.382 11.643c-.24.227-.382.553-.382.92 0 .737.552 1.29 1.288 1.29.368 0 .708-.128.92-.368L13.052 2.408h-1.515l10.844 11.077c.227.24.566.368.92.368a1.26 1.26 0 0 0 1.289-1.29 1.25 1.25 0 0 0-.383-.92L13.25.425A1.288 1.288 0 0 0 12.302 0a1.32 1.32 0 0 0-.963.425L.382 11.643Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="header-name">Достопримечательности</span>
|
<span class="header-name">{{ t("sights") }}</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="showLangToggle"
|
||||||
|
class="language-toggle"
|
||||||
|
@click="toggleLanguageOptions"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 28 28"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M0 14C0 6.27 6.27 0 14 0s14 6.27 14 14-6.27 14-14 14S0 21.73 0 14Zm21.03-2.678 1.294 3.536 1.284-3.547-.759.222c-.006-.012-.006-.245-.006-.263-.338-4.03-3.599-7.1-7.711-7.1a.558.558 0 0 0-.554.555c0 .303.25.554.554.554 3.488 0 6.265 2.579 6.603 5.985l.003.137.003.137-.712-.216ZM5.193 16.063a9.797 9.797 0 0 0 3.888-1.897 7.8 7.8 0 0 0 3.449 1.906.784.784 0 0 0 .186 0c.303.032.6-.094.78-.33a.798.798 0 0 0 .094-.819.851.851 0 0 0-.688-.487 5.887 5.887 0 0 1-2.541-1.399 11.906 11.906 0 0 0 3.151-4.907.797.797 0 0 0-.17-.712.857.857 0 0 0-.677-.328H9.701V6.068a.814.814 0 0 0-.424-.708.872.872 0 0 0-.847 0 .813.813 0 0 0-.423.708v1.038H5.042a.854.854 0 0 0-.734.41.793.793 0 0 0 0 .817.854.854 0 0 0 .734.41h6.43A11.292 11.292 0 0 1 9.166 11.9a11.17 11.17 0 0 1-1.338-1.89.833.833 0 0 0-.484-.47.874.874 0 0 0-.687.03.826.826 0 0 0-.437.511.793.793 0 0 0 .1.657 13 13 0 0 0 1.584 2.184 8 8 0 0 1-3.058 1.505.86.86 0 0 0-.574.3.801.801 0 0 0 .126 1.157c.176.138.4.202.625.18a.573.573 0 0 0 .17 0Zm15.164 6.617a.874.874 0 0 0 .646.016.838.838 0 0 0 .497-.424c.099-.2.11-.432.028-.64L17.7 12.12a.824.824 0 0 0-.31-.38.869.869 0 0 0-.954-.004.826.826 0 0 0-.312.375l-3.795 9.194a.796.796 0 0 0 .123.81.864.864 0 0 0 1.453-.205l.915-2.282h4.032l1.06 2.602a.832.832 0 0 0 .446.45Zm-3.497-8 1.33 3.313h-2.694l1.364-3.313Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- тройка языковых иконок -->
|
||||||
|
<div
|
||||||
|
v-if="showLanguageOptions"
|
||||||
|
class="language-options"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<!-- RU -->
|
||||||
|
<button
|
||||||
|
class="language-option"
|
||||||
|
:style="{ opacity: selectedLang === 'ru' ? 1 : 0.8 }"
|
||||||
|
@click="setLanguage('ru')"
|
||||||
|
v-html="icons.ru"
|
||||||
|
/>
|
||||||
|
<!-- EN -->
|
||||||
|
<button
|
||||||
|
class="language-option"
|
||||||
|
:style="{ opacity: selectedLang === 'en' ? 1 : 0.8 }"
|
||||||
|
@click="setLanguage('en')"
|
||||||
|
v-html="icons.en"
|
||||||
|
/>
|
||||||
|
<!-- ZH -->
|
||||||
|
<button
|
||||||
|
class="language-option"
|
||||||
|
:style="{ opacity: selectedLang === 'zh' ? 1 : 0.8 }"
|
||||||
|
@click="setLanguage('zh')"
|
||||||
|
v-html="icons.zh"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="slide-fade">
|
<transition name="slide-fade">
|
||||||
@ -148,6 +223,10 @@
|
|||||||
v-for="sight in group"
|
v-for="sight in group"
|
||||||
:key="sight.id"
|
:key="sight.id"
|
||||||
class="sight-card"
|
class="sight-card"
|
||||||
|
:class="{
|
||||||
|
'selected-card': sight.id === selectedSightCardId,
|
||||||
|
}"
|
||||||
|
@click.stop="openSightCardDetails(sight.id)"
|
||||||
>
|
>
|
||||||
<img class="sight-thumbnail" :src="sight.thumbnailUrl" />
|
<img class="sight-thumbnail" :src="sight.thumbnailUrl" />
|
||||||
<div class="sight-title">{{ sight.name }}</div>
|
<div class="sight-title">{{ sight.name }}</div>
|
||||||
@ -186,9 +265,13 @@ export default {
|
|||||||
articles: [],
|
articles: [],
|
||||||
selectedArticleId: null,
|
selectedArticleId: null,
|
||||||
selectedArticleBody: "",
|
selectedArticleBody: "",
|
||||||
|
selectedSightArticleId: null,
|
||||||
|
selectedSightArticleBody: "",
|
||||||
sights: [],
|
sights: [],
|
||||||
showSightsList: false,
|
showSightsList: false,
|
||||||
selectedLetter: null,
|
selectedLetter: null,
|
||||||
|
selectedSightCardId: null,
|
||||||
|
cardDetail: null,
|
||||||
showTransfers: false,
|
showTransfers: false,
|
||||||
transferIcons: {
|
transferIcons: {
|
||||||
tram: require("@/icons/tram.svg"),
|
tram: require("@/icons/tram.svg"),
|
||||||
@ -204,6 +287,29 @@ export default {
|
|||||||
nextStopTransfers: null,
|
nextStopTransfers: null,
|
||||||
stops: [],
|
stops: [],
|
||||||
routeProgress: null,
|
routeProgress: null,
|
||||||
|
routeId: null,
|
||||||
|
selectedLang: localStorage.getItem("selectedLangRight") || "ru",
|
||||||
|
showLangToggle: true, // видна «глобус»-кнопка
|
||||||
|
showLanguageOptions: false, // виден блок из трёх иконок
|
||||||
|
languageOptionsTimer: null, // таймер на 10 с
|
||||||
|
langRevertTimer: null, // таймер на 30 с
|
||||||
|
icons: {
|
||||||
|
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="M9.513 10.156H7.641v3.389h1.878c.583 0 1.038-.152 1.36-.45.32-.297.478-.705.478-1.23s-.152-.95-.455-1.26c-.304-.31-.77-.46-1.394-.46l.005.01Z"/>
|
||||||
|
</svg>`,
|
||||||
|
en: `<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-1.418 19.71H4.906V8.255h7.665v1.914H7.263v2.73h4.532v1.849H7.263v3.068h5.32v1.896Zm10.483 0h-2.363l-4.596-7.536v7.537h-2.363V8.254h2.363l4.602 7.554V8.254h2.351v11.457h.006Z"/>
|
||||||
|
</svg>`,
|
||||||
|
zh: `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="none" viewBox="0 0 28 28">
|
||||||
|
<path fill="#fff" d="M6 11.89H3.67v2.196H6v-2.197Zm1.994 2.196h2.343v-2.197H7.994v2.197Zm13.079-2.392h-3.656a10.503 10.503 0 0 0 1.863 2.96c.735-.833 1.33-1.813 1.793-2.96Z"/>
|
||||||
|
<path fill="#fff" fill-rule="evenodd" d="M14 28c7.732 0 14-6.268 14-14S21.732 0 14 0 0 6.268 0 14s6.268 14 14 14ZM6 7.875h1.994v2.132h4.324v6.586h-1.98v-.624H7.993v3.846H6.001V15.97H3.67v.686H1.75v-6.649H6V7.876Zm12.288 0h1.956v1.936h4.888v1.883h-1.864c-.632 1.694-1.466 3.113-2.512 4.293 1.206.885 2.674 1.545 4.425 1.938l.396.09-.292.281c-.333.322-.798.999-1.025 1.417l-.082.15-.166-.043c-1.94-.507-3.523-1.298-4.822-2.364-1.323 1.033-2.892 1.814-4.722 2.386l-.183.057-.08-.173c-.163-.348-.617-1.042-.902-1.387l-.218-.263.33-.088c1.714-.45 3.153-1.089 4.34-1.95-1.026-1.218-1.819-2.671-2.448-4.345h-1.834V9.812h4.815V7.875Z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
translations: {
|
||||||
|
sights: { ru: "Достопримечательности", en: "Landmarks", zh: "景点" },
|
||||||
|
// при необходимости — другие подписи
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -246,6 +352,64 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
t(key) {
|
||||||
|
const dict = this.translations[key] || {};
|
||||||
|
return dict[this.selectedLang] || dict.ru || key;
|
||||||
|
},
|
||||||
|
// показать 3 иконки
|
||||||
|
toggleLanguageOptions() {
|
||||||
|
this.showLangToggle = false;
|
||||||
|
this.showLanguageOptions = true;
|
||||||
|
this.startLanguageOptionsTimer();
|
||||||
|
},
|
||||||
|
|
||||||
|
// таймер 10 с
|
||||||
|
startLanguageOptionsTimer() {
|
||||||
|
clearTimeout(this.languageOptionsTimer);
|
||||||
|
this.languageOptionsTimer = setTimeout(this.hideLanguageOptions, 10_000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// скрыть 3 иконки, вернуть глобус
|
||||||
|
hideLanguageOptions() {
|
||||||
|
this.showLanguageOptions = false;
|
||||||
|
this.showLangToggle = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// выбор языка
|
||||||
|
setLanguage(lang) {
|
||||||
|
if (this.selectedLang === lang) {
|
||||||
|
this.hideLanguageOptions();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedLang = lang;
|
||||||
|
localStorage.setItem("selectedLangRight", lang);
|
||||||
|
this.hideLanguageOptions();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
|
|
||||||
|
// перезагрузить данные на новом языке
|
||||||
|
this.fetchSightInfo();
|
||||||
|
this.fetchArticles();
|
||||||
|
this.fetchSights();
|
||||||
|
},
|
||||||
|
|
||||||
|
// добавить ?lang=… к URL-у, если язык не ru
|
||||||
|
addLangParam(url) {
|
||||||
|
if (this.selectedLang !== "ru") {
|
||||||
|
return (
|
||||||
|
url + (url.includes("?") ? "&" : "?") + "lang=" + this.selectedLang
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
|
// таймер 5 минут с возврата на ru
|
||||||
|
resetLangRevertTimer() {
|
||||||
|
clearTimeout(this.langRevertTimer);
|
||||||
|
this.langRevertTimer = setTimeout(() => {
|
||||||
|
if (this.selectedLang !== "ru") this.setLanguage("ru");
|
||||||
|
}, 300_000);
|
||||||
|
},
|
||||||
openModal() {
|
openModal() {
|
||||||
const imageDiv = this.$el.querySelector(".img");
|
const imageDiv = this.$el.querySelector(".img");
|
||||||
if (imageDiv) {
|
if (imageDiv) {
|
||||||
@ -269,7 +433,9 @@ export default {
|
|||||||
console.log("No sightId provided for fetchSightInfo");
|
console.log("No sightId provided for fetchSightInfo");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await axios.get(`${API_URL}/sight/${this.sightId}`);
|
const response = await axios.get(
|
||||||
|
this.addLangParam(`${API_URL}/sight/${this.sightId}`)
|
||||||
|
);
|
||||||
this.stopName = response.data.name;
|
this.stopName = response.data.name;
|
||||||
if (response.data.watermark_lu) {
|
if (response.data.watermark_lu) {
|
||||||
this.watermarkLU = await this.getMediaBlobUrl(
|
this.watermarkLU = await this.getMediaBlobUrl(
|
||||||
@ -292,7 +458,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${API_URL}/sight/${this.sightId}/article`
|
this.addLangParam(`${API_URL}/sight/${this.sightId}/article`)
|
||||||
);
|
);
|
||||||
this.articles = response.data;
|
this.articles = response.data;
|
||||||
if (this.articles.length > 0) {
|
if (this.articles.length > 0) {
|
||||||
@ -301,50 +467,178 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchSights() {
|
async fetchSights() {
|
||||||
const geoRes = await axios.get(`${GEO_URL}/v1/geolocation/context`);
|
// Prefer cached routeId, otherwise fetch context once
|
||||||
const routeId = geoRes.data.routeId;
|
let routeId = this.routeId;
|
||||||
if (!routeId) {
|
if (!routeId) {
|
||||||
console.warn("Missing routeId in geo context:", geoRes.data);
|
try {
|
||||||
|
const geoRes = await axios.get(`${GEO_URL}/v1/geolocation/context`);
|
||||||
|
routeId = geoRes.data.routeId;
|
||||||
|
if (routeId && routeId !== this.routeId) {
|
||||||
|
this.routeId = routeId;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get routeId from context:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!routeId) {
|
||||||
|
console.warn("Missing routeId — skipping fetchSights");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sightsRes = await axios.get(`${API_URL}/route/${routeId}/sight`);
|
try {
|
||||||
const rawSights = sightsRes.data;
|
const sightsRes = await axios.get(
|
||||||
const detailedSights = await Promise.all(
|
this.addLangParam(`${API_URL}/route/${routeId}/sight`)
|
||||||
rawSights.map(async (sight) => {
|
);
|
||||||
const detailRes = await axios.get(`${API_URL}/sight/${sight.id}`);
|
const rawSights = sightsRes.data;
|
||||||
const thumbnailUrl = detailRes.data.thumbnail
|
const detailedSights = await Promise.all(
|
||||||
? await this.getMediaBlobUrl(detailRes.data.thumbnail)
|
rawSights.map(async (sight) => {
|
||||||
: "";
|
const detailRes = await axios.get(
|
||||||
return {
|
this.addLangParam(`${API_URL}/sight/${sight.id}`)
|
||||||
id: sight.id,
|
);
|
||||||
name: detailRes.data.name,
|
const thumbnailUrl = detailRes.data.thumbnail
|
||||||
thumbnailUrl,
|
? await this.getMediaBlobUrl(detailRes.data.thumbnail)
|
||||||
};
|
: "";
|
||||||
})
|
return {
|
||||||
);
|
id: sight.id,
|
||||||
this.sights = detailedSights;
|
name: detailRes.data.name,
|
||||||
|
thumbnailUrl,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.sights = detailedSights;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching sights:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openSightCardDetails(id) {
|
||||||
|
// закрыть, если нажали повторно
|
||||||
|
if (this.selectedSightCardId === id) {
|
||||||
|
this.selectedSightCardId = null;
|
||||||
|
this.cardDetail = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectedSightCardId = id;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async selectSightArticle(id) {
|
||||||
|
this.selectedSightArticleId = id;
|
||||||
|
const selected = this.cardDetail?.articles.find((a) => a.id === id);
|
||||||
|
this.selectedSightArticleBody = selected ? selected.body : "";
|
||||||
|
if (selected && selected.media && selected.media.length > 0) {
|
||||||
|
try {
|
||||||
|
this.cardDetail.imageUrl = await this.getMediaBlobUrl(
|
||||||
|
selected.media[0].id
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
this.cardDetail.imageUrl = this.addLangParam(
|
||||||
|
`${API_URL}/media/${selected.media[0].id}/download`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideSightUI() {
|
||||||
|
this.showSightsList = false;
|
||||||
|
this.selectedSightCardId = null;
|
||||||
|
this.cardDetail = null;
|
||||||
},
|
},
|
||||||
toggleSightsList() {
|
toggleSightsList() {
|
||||||
|
// переключаем видимость
|
||||||
this.showSightsList = !this.showSightsList;
|
this.showSightsList = !this.showSightsList;
|
||||||
if (this.showSightsList) {
|
|
||||||
|
// если список теперь скрыт — закрываем и карточку, сбрасываем таймер
|
||||||
|
if (!this.showSightsList) {
|
||||||
|
this.selectedSightCardId = null;
|
||||||
|
this.cardDetail = null;
|
||||||
|
if (this.sightsInactivityTimer) {
|
||||||
|
clearTimeout(this.sightsInactivityTimer);
|
||||||
|
this.sightsInactivityTimer = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// список открыт — запускаем таймер неактивности
|
||||||
this.resetSightsInactivityTimer();
|
this.resetSightsInactivityTimer();
|
||||||
} else if (this.sightsInactivityTimer) {
|
|
||||||
clearTimeout(this.sightsInactivityTimer);
|
|
||||||
this.sightsInactivityTimer = null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetSightsInactivityTimer() {
|
resetSightsInactivityTimer() {
|
||||||
if (this.articleInactivityTimer) {
|
if (this.sightsInactivityTimer) clearTimeout(this.sightsInactivityTimer);
|
||||||
clearTimeout(this.articleInactivityTimer);
|
|
||||||
}
|
// запускаем новый, если открыт список или карточка
|
||||||
if (this.sightsInactivityTimer) {
|
if (this.showSightsList || this.cardDetail) {
|
||||||
clearTimeout(this.sightsInactivityTimer);
|
|
||||||
}
|
|
||||||
if (this.showSightsList) {
|
|
||||||
this.sightsInactivityTimer = setTimeout(() => {
|
this.sightsInactivityTimer = setTimeout(() => {
|
||||||
this.showSightsList = false;
|
this.hideSightUI(); // скрываем всё
|
||||||
this.sightsInactivityTimer = null;
|
this.sightsInactivityTimer = null;
|
||||||
}, 300000); // 5 m
|
}, 300_000); // 5 m
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetArticleInactivityTimer() {
|
resetArticleInactivityTimer() {
|
||||||
@ -361,8 +655,10 @@ export default {
|
|||||||
}, 300_000); // 5 m
|
}, 300_000); // 5 m
|
||||||
},
|
},
|
||||||
handleUserActivity() {
|
handleUserActivity() {
|
||||||
if (this.showSightsList) this.resetSightsInactivityTimer();
|
if (this.showSightsList || this.cardDetail)
|
||||||
|
this.resetSightsInactivityTimer();
|
||||||
this.resetArticleInactivityTimer();
|
this.resetArticleInactivityTimer();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
},
|
},
|
||||||
selectArticle(id) {
|
selectArticle(id) {
|
||||||
this.resetArticleInactivityTimer();
|
this.resetArticleInactivityTimer();
|
||||||
@ -374,14 +670,16 @@ export default {
|
|||||||
async fetchArticleMedia(articleId) {
|
async fetchArticleMedia(articleId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${API_URL}/article/${articleId}/media`
|
this.addLangParam(`${API_URL}/article/${articleId}/media`)
|
||||||
);
|
);
|
||||||
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;
|
||||||
try {
|
try {
|
||||||
this.imageUrl = await this.getMediaBlobUrl(mediaId);
|
this.imageUrl = await this.getMediaBlobUrl(mediaId);
|
||||||
} catch {
|
} catch {
|
||||||
this.imageUrl = `${API_URL}/media/${mediaId}/download`;
|
this.imageUrl = this.addLangParam(
|
||||||
|
`${API_URL}/media/${mediaId}/download`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.imageUrl = "";
|
this.imageUrl = "";
|
||||||
@ -395,7 +693,7 @@ export default {
|
|||||||
async getMediaBlobUrl(mediaId) {
|
async getMediaBlobUrl(mediaId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${API_URL}/media/${mediaId}/download`,
|
this.addLangParam(`${API_URL}/media/${mediaId}/download`),
|
||||||
{
|
{
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
}
|
}
|
||||||
@ -407,17 +705,27 @@ export default {
|
|||||||
error?.response?.status,
|
error?.response?.status,
|
||||||
error?.response?.data || error.message
|
error?.response?.data || error.message
|
||||||
);
|
);
|
||||||
return `${API_URL}/media/${mediaId}/download`;
|
return this.addLangParam(`${API_URL}/media/${mediaId}/download`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchGeolocationContext() {
|
async fetchGeolocationContext() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${GEO_URL}/v1/geolocation/context`);
|
const response = await axios.get(
|
||||||
this.routeProgress = response.data.routeProgress;
|
this.addLangParam(`${GEO_URL}/v1/geolocation/context`)
|
||||||
const stopsResponse = await axios.get(
|
|
||||||
`${API_URL}/route/${response.data.routeId}/station`
|
|
||||||
);
|
);
|
||||||
this.stops = stopsResponse.data;
|
this.routeProgress = response.data.routeProgress;
|
||||||
|
const newRouteId = response.data.routeId;
|
||||||
|
if (newRouteId && newRouteId !== this.routeId) {
|
||||||
|
this.routeId = newRouteId;
|
||||||
|
}
|
||||||
|
if (this.routeId) {
|
||||||
|
const stopsResponse = await axios.get(
|
||||||
|
this.addLangParam(`${API_URL}/route/${this.routeId}/station`)
|
||||||
|
);
|
||||||
|
this.stops = stopsResponse.data;
|
||||||
|
} else {
|
||||||
|
this.stops = [];
|
||||||
|
}
|
||||||
let newSightId = response.data.nearestSightId;
|
let newSightId = response.data.nearestSightId;
|
||||||
|
|
||||||
if (!newSightId) {
|
if (!newSightId) {
|
||||||
@ -490,6 +798,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
routeId(newVal, oldVal) {
|
||||||
|
if (newVal && newVal !== oldVal) {
|
||||||
|
// Refresh sights because routeId changed
|
||||||
|
this.fetchSights();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.fetchSights();
|
await this.fetchSights();
|
||||||
await this.fetchGeolocationContext();
|
await this.fetchGeolocationContext();
|
||||||
@ -504,6 +820,7 @@ export default {
|
|||||||
window.addEventListener("click", this.handleUserActivity, true);
|
window.addEventListener("click", this.handleUserActivity, true);
|
||||||
// this.fetchSightInfo();
|
// this.fetchSightInfo();
|
||||||
// this.fetchArticles();
|
// this.fetchArticles();
|
||||||
|
this.resetLangRevertTimer();
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
if (this.sightsInactivityTimer) {
|
if (this.sightsInactivityTimer) {
|
||||||
@ -524,3 +841,113 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sight-card {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-card-detail {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 105%;
|
||||||
|
width: 260px;
|
||||||
|
z-index: 20;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-card-detail .detail-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sight-card-detail .detail-name {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-articles {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-article {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.sight-side-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 33%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
padding: 14px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-image-wrapper {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-description {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-articles {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-article-option {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-article-option.selected {
|
||||||
|
background: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
.language-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-option svg {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../assets/style/main.css";
|
import "../assets/style/main.css";
|
||||||
import { API_URL } from "../config";
|
import { API_URL, WEATHER_URL } from "../config";
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import clearIcon from "@/icons/clear-day.svg";
|
import clearIcon from "@/icons/clear-day.svg";
|
||||||
@ -96,7 +96,7 @@ export default {
|
|||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
autoCloseTimer: null,
|
autoCloseTimer: null,
|
||||||
imageUrl: "",
|
imageUrl: "",
|
||||||
sightId: 14,
|
sightId: null,
|
||||||
stopName: "",
|
stopName: "",
|
||||||
articles: [],
|
articles: [],
|
||||||
selectedArticleId: null,
|
selectedArticleId: null,
|
||||||
@ -120,10 +120,16 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchSightInfo() {
|
async fetchSightInfo() {
|
||||||
|
// Do nothing if sightId is null, zero, or empty
|
||||||
|
if (!this.sightId) return;
|
||||||
|
|
||||||
const response = await axios.get(`${API_URL}/sight/${this.sightId}`);
|
const response = await axios.get(`${API_URL}/sight/${this.sightId}`);
|
||||||
this.stopName = response.data.name;
|
this.stopName = response.data.name;
|
||||||
},
|
},
|
||||||
async fetchArticles() {
|
async fetchArticles() {
|
||||||
|
// Do nothing if sightId is null, zero, or empty
|
||||||
|
if (!this.sightId) return;
|
||||||
|
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${API_URL}/sight/${this.sightId}/article`
|
`${API_URL}/sight/${this.sightId}/article`
|
||||||
);
|
);
|
||||||
@ -183,12 +189,9 @@ export default {
|
|||||||
async fetchWeatherData() {
|
async fetchWeatherData() {
|
||||||
console.log("Fetching weather data...");
|
console.log("Fetching weather data...");
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const response = await axios.post(
|
const response = await axios.post(`${WEATHER_URL}/v1/weather`, {
|
||||||
"https://weather.wn.krbl.ru/v1/weather",
|
coordinates: { latitude: 59.938784, longitude: 30.314997 },
|
||||||
{
|
});
|
||||||
coordinates: { latitude: 59.938784, longitude: 30.314997 },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
this.currentWeather = {
|
this.currentWeather = {
|
||||||
...response.data.currentWeather,
|
...response.data.currentWeather,
|
||||||
temperatureCelsius: Math.round(
|
temperatureCelsius: Math.round(
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
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;
|
||||||
|
Loading…
Reference in New Issue
Block a user